diff --git a/.github/workflows/loxone-java-release.yml b/.github/workflows/loxone-java-release.yml new file mode 100644 index 0000000..23d08b6 --- /dev/null +++ b/.github/workflows/loxone-java-release.yml @@ -0,0 +1,58 @@ +--- +name: Loxone Java release + +on: + workflow_dispatch: + inputs: + versionIncrement: + description: "What version number to increment" + required: true + type: choice + default: incrementPatch + options: + - incrementPatch + - incrementMinor + - incrementMajor + +jobs: + release: + permissions: + contents: write + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + with: + ref: master + token: ${{ secrets.SMARTEON_GIT_TOKEN }} + + - name: Setup JDK 11 + uses: actions/setup-java@v3 + with: + java-version: '11' + distribution: 'adopt' + + - name: Set up git configuration + run: | + git config --global user.name 'Smarteon Git' + git config --global user.email 'accounts+git@smarteon.cz' + + - name: Setup Gradle + uses: gradle/gradle-build-action@v2 + + - name: Release and push tag + shell: bash + run: | + # Fetch a full copy of the repo, as required by release plugin: + git fetch --tags --unshallow + # Run release: + ./gradlew release -Prelease.versionIncrementer=${{ inputs.versionIncrement }} + + - name: Publish to OSS Sonatype + shell: bash + env: + OSS_USER: ${{ secrets.OSS_USER }} + OSS_PASS: ${{ secrets.OSS_PASS }} + SIGNING_KEY: ${{ secrets.SIGNING_KEY }} + SIGNING_PASS: ${{ secrets.SIGNING_PASS }} + run: | + ./gradlew publishToSonatype closeAndReleaseSonatypeStagingRepository diff --git a/ENVIRONMENT_VARIABLES.md b/ENVIRONMENT_VARIABLES.md new file mode 100644 index 0000000..790a438 --- /dev/null +++ b/ENVIRONMENT_VARIABLES.md @@ -0,0 +1,200 @@ +# Environment Variables and Secrets Summary + +This document provides a quick reference for all environment variables and GitHub secrets required for the automated release process. + +## GitHub Secrets (Required) + +These must be configured in **Settings → Secrets and variables → Actions** in the GitHub repository: + +### 1. OSS_USER +- **Purpose**: Sonatype OSS username for publishing to Maven Central +- **Type**: Sonatype JIRA account username +- **How to obtain**: Create account at https://issues.sonatype.org/ +- **Example**: `your-jira-username` + +### 2. OSS_PASS +- **Purpose**: Sonatype OSS password for authentication +- **Type**: Sonatype JIRA account password +- **How to obtain**: Password from your JIRA account +- **Security**: Keep this secret secure, rotate regularly + +### 3. SIGNING_KEY +- **Purpose**: GPG private key for signing Maven artifacts +- **Type**: ASCII-armored GPG private key, base64 encoded +- **How to obtain**: + ```bash + # WARNING: This exports your private key. Handle with extreme care! + # The output file should be immediately added to GitHub secrets and then securely deleted. + gpg --export-secret-keys -a YOUR_KEY_ID | base64 -w0 > signing-key.txt + + # After copying to GitHub secrets: + shred -u signing-key.txt # Linux - securely delete + # rm -P signing-key.txt # macOS - securely delete + ``` +- **Requirements**: + - Must be a valid GPG/PGP key + - Public key must be published to key servers + - Must be base64 encoded for storage +- **Security**: Never commit this to version control or expose in logs + +### 4. SIGNING_PASS +- **Purpose**: Passphrase for the GPG signing key +- **Type**: String passphrase +- **How to obtain**: The passphrase you set when creating your GPG key +- **Security**: Store securely, never commit to repository + +### 5. SMARTEON_GIT_TOKEN +- **Purpose**: GitHub Personal Access Token for pushing release tags +- **Type**: GitHub PAT with `repo` scope +- **How to obtain**: + 1. GitHub Settings → Developer settings → Personal access tokens + 2. Generate new token (classic) + 3. Select `repo` scope +- **Permissions Required**: Full control of private repositories + +## Local Development Environment Variables (Optional) + +For local testing of the release and publish process: + +### Environment Variables + +```bash +# WARNING: Setting environment variables in shell exposes them in: +# - Shell history (use `set +o history` BEFORE entering commands) +# - Process lists (visible to other users via ps/top) +# - Parent shell environment +# +# Recommended secure approach: +# 1. Start a new shell session with history disabled: bash --noprofile --norc -c "set +o history; bash" +# 2. Or disable history before entering commands: set +o history +# 3. Use a .env file that's in .gitignore +# 4. Clear shell history after: history -c && history -w +# 5. Unset variables after use: unset OSS_USER OSS_PASS SIGNING_KEY SIGNING_PASS + +# Sonatype credentials +export OSS_USER="your-jira-username" +export OSS_PASS="your-jira-password" + +# GPG signing - RECOMMENDED: read from secure file instead of inline command +# First, create the key file (see RELEASE_SETUP.md for instructions) +export SIGNING_KEY="$(cat /secure/path/signing-key.txt)" +export SIGNING_PASS="your-gpg-passphrase" + +# ALTERNATIVE (less secure - exposes key in process list during execution): +# export SIGNING_KEY="$(gpg --export-secret-keys -a YOUR_KEY_ID | base64 -w0)" +``` + +### Alternative: Gradle Properties + +Instead of environment variables, you can use `~/.gradle/gradle.properties`: + +```properties +# Sonatype credentials (legacy support - environment variables preferred) +ossUser=your-jira-username +ossPass=your-jira-password + +# GPG signing (file-based approach) +signing.keyId=YOUR_KEY_ID +signing.password=your-gpg-passphrase +signing.secretKeyRingFile=/path/to/.gnupg/secring.gpg +``` + +**Note**: The environment variable approach (used in CI/CD) is preferred as it's more secure. + +## Gradle Tasks Reference + +### Version and Release Tasks + +```bash +# Check current version (derived from git tags) +./gradlew currentVersion + +# Create a new release (increment patch version) +./gradlew release -Prelease.versionIncrementer=incrementPatch + +# Create a new release (increment minor version) +./gradlew release -Prelease.versionIncrementer=incrementMinor + +# Create a new release (increment major version) +./gradlew release -Prelease.versionIncrementer=incrementMajor + +# Dry run release (doesn't create tags) +./gradlew release -Prelease.dryRun +``` + +### Publishing Tasks + +```bash +# Publish to local Maven repository (testing) +./gradlew publishToMavenLocal + +# Publish to Sonatype (requires OSS_USER and OSS_PASS) +./gradlew publishToSonatype + +# Close and release staging repository +./gradlew closeAndReleaseSonatypeStagingRepository + +# Full publish workflow (what CI/CD does) +./gradlew publishToSonatype closeAndReleaseSonatypeStagingRepository +``` + +## Security Best Practices + +1. **Never commit secrets** to the repository +2. **Rotate credentials regularly**, especially if they may have been exposed +3. **Use environment variables** in CI/CD instead of storing in files +4. **Limit access** to GitHub secrets to repository administrators only +5. **Enable 2FA** on your Sonatype JIRA account +6. **Back up your GPG key** in a secure location +7. **Use a strong passphrase** for your GPG key + +## Troubleshooting + +### Missing Secret Error +``` +Error: Secret OSS_USER not found +``` +**Solution**: Ensure all required secrets are configured in GitHub repository settings + +### GPG Signing Failed +``` +Error: Invalid signing key +``` +**Solution**: +- Verify SIGNING_KEY is base64 encoded +- Verify SIGNING_PASS matches the key passphrase +- Test locally: `echo $SIGNING_KEY | base64 -d | gpg --import` + +### Publishing Unauthorized +``` +401 Unauthorized +``` +**Solution**: +- Verify OSS_USER and OSS_PASS are correct +- Ensure your account has access to `cz.smarteon` group ID +- Check if your Sonatype account is active + +### Version Already Exists +``` +Tag X.Y.Z already exists +``` +**Solution**: +- The version tag already exists in git +- Either delete the tag or increment to the next version +- Check: `git tag -l` to list all tags + +## Support + +For issues with: +- **Gradle build**: Check project documentation +- **Sonatype/Maven Central**: https://issues.sonatype.org/ +- **GPG keys**: https://www.gnupg.org/documentation/ +- **GitHub Actions**: GitHub repository Issues + +## References + +- [Sonatype OSSRH Guide](https://central.sonatype.org/publish/publish-guide/) +- [Maven Central Requirements](https://central.sonatype.org/publish/requirements/) +- [GPG Key Management](https://central.sonatype.org/publish/requirements/gpg/) +- [Axion Release Plugin](https://axion-release-plugin.readthedocs.io/) +- [Nexus Publish Plugin](https://github.com/gradle-nexus/publish-plugin) diff --git a/README.md b/README.md index 9029228..c6d88f2 100644 --- a/README.md +++ b/README.md @@ -92,6 +92,54 @@ MiniserverDiscovererTest > should discover() FAILED It is adviced to install the lombok plugin for your IDE of choice, this makes coding & debugging easier, you can find the instructions for installing the plugin for various IDE's on the following location https://projectlombok.org/setup/ +### Releasing + +This project uses automated releases via GitHub Actions with the [axion-release](https://github.com/allegro/axion-release-plugin) plugin and publishes to [OSS Sonatype](https://oss.sonatype.org/). + +#### Release Process + +To create a new release: + +1. Navigate to **Actions** → **Loxone Java release** in the GitHub repository +2. Click **Run workflow** +3. Select the version increment type: + - `incrementPatch` - for bug fixes (e.g., 2.9.2 → 2.9.3) + - `incrementMinor` - for new features (e.g., 2.9.2 → 2.10.0) + - `incrementMajor` - for breaking changes (e.g., 2.9.2 → 3.0.0) +4. Click **Run workflow** + +The workflow will: +- Create and push a new version tag +- Build the project +- Sign the artifacts +- Publish to OSS Sonatype +- Automatically close and release the staging repository + +#### Required GitHub Secrets + +The following secrets must be configured in the repository settings: + +- `OSS_USER` - Sonatype OSS username (JIRA account) +- `OSS_PASS` - Sonatype OSS password +- `SIGNING_KEY` - GPG private key for signing artifacts (ASCII-armored, base64 encoded) +- `SIGNING_PASS` - Passphrase for the GPG key +- `SMARTEON_GIT_TOKEN` - GitHub Personal Access Token with `repo` scope for pushing tags + +#### Local Release Testing + +To test the release process locally: + +```bash +# Check current version +./gradlew currentVersion + +# Test release process (dry-run) +./gradlew release -Prelease.dryRun + +# Test publishing to local Maven repository +./gradlew publishToMavenLocal +``` + ### Writing unit tests We prefer [Junit5 with Kotlin](https://www.baeldung.com/kotlin/junit-5-kotlin), [Strikt](https://strikt.io/) and [Mockk](https://mockk.io/) for writing unit testing. Please follow the guidelines and documentation on the internet. Kotlin is easy to grab fro Java programmers and in many cases it allows more concise code. However Java + Junit5 unit diff --git a/RELEASE_SETUP.md b/RELEASE_SETUP.md new file mode 100644 index 0000000..1f3e4c9 --- /dev/null +++ b/RELEASE_SETUP.md @@ -0,0 +1,163 @@ +# Release Setup Guide + +This document describes how to set up the automated release process for loxone-java. + +## Prerequisites + +Before you can publish releases, you need: + +1. **Sonatype JIRA account** with permissions to publish to `cz.smarteon` group +2. **GPG key** for signing artifacts +3. **GitHub repository admin access** to configure secrets + +## Step 1: Sonatype OSS Setup + +If you don't already have access to publish to `cz.smarteon`: + +1. Create a Sonatype JIRA account at https://issues.sonatype.org +2. Request access to the `cz.smarteon` group ID (or create a new ticket if this is the first time) +3. Wait for approval (usually takes 1-2 business days) + +Reference: [Sonatype OSSRH Guide](https://central.sonatype.org/publish/publish-guide/) + +## Step 2: GPG Key Setup + +### Generate a GPG Key (if you don't have one) + +```bash +# Generate a new key +gpg --gen-key + +# Follow the prompts: +# - Use RSA and RSA +# - Key size: 4096 bits +# - Expiration: choose appropriate duration +# - Enter your name and email +# - Set a strong passphrase +``` + +### Export the GPG Key + +```bash +# List your keys to find the key ID +gpg --list-secret-keys --keyid-format=long + +# Export the private key (replace KEY_ID with your actual key ID) +gpg --export-secret-keys -a KEY_ID | base64 -w0 > signing-key.txt + +# The signing-key.txt now contains your base64-encoded private key +``` + +### Publish Your Public Key + +```bash +# Publish to key servers (replace KEY_ID with your actual key ID) +gpg --keyserver keyserver.ubuntu.com --send-keys KEY_ID +gpg --keyserver keys.openpgp.org --send-keys KEY_ID +``` + +Reference: [Working with PGP Signatures](https://central.sonatype.org/publish/requirements/gpg/) + +## Step 3: Configure GitHub Secrets + +Add the following secrets in your GitHub repository settings (**Settings** → **Secrets and variables** → **Actions**): + +### Required Secrets + +| Secret Name | Description | How to Obtain | +|------------|-------------|---------------| +| `OSS_USER` | Sonatype JIRA username | Your JIRA account username | +| `OSS_PASS` | Sonatype JIRA password | Your JIRA account password | +| `SIGNING_KEY` | GPG private key (base64) | Content of `signing-key.txt` from Step 2 | +| `SIGNING_PASS` | GPG key passphrase | The passphrase you set when creating the GPG key | +| `SMARTEON_GIT_TOKEN` | GitHub Personal Access Token | See below | + +### Creating GitHub Personal Access Token + +1. Go to GitHub **Settings** → **Developer settings** → **Personal access tokens** → **Tokens (classic)** +2. Click **Generate new token (classic)** +3. Select scopes: + - `repo` (Full control of private repositories) +4. Click **Generate token** +5. Copy the token and add it as `SMARTEON_GIT_TOKEN` secret + +## Step 4: Verify Setup + +### Test Gradle Configuration + +```bash +# Verify the build works +./gradlew clean build -x test + +# Check version detection +./gradlew currentVersion + +# Test signing configuration (will skip if not on a release version) +SIGNING_KEY="$(cat signing-key.txt)" SIGNING_PASS="your-passphrase" ./gradlew build +``` + +### Test Publishing (Optional) + +```bash +# Publish to local Maven repository +./gradlew publishToMavenLocal + +# Check the published artifacts +ls ~/.m2/repository/cz/smarteon/loxone-java/ +``` + +## Step 5: First Release + +Once all secrets are configured: + +1. Go to **Actions** → **Loxone Java release** +2. Click **Run workflow** +3. Select `incrementPatch` for the first automated release +4. Click **Run workflow** + +The workflow will: +- Create a new git tag (e.g., `2.11.1`) +- Build and test the project +- Sign all artifacts with your GPG key +- Publish to OSS Sonatype staging repository +- Automatically close and release the staging repository +- Artifacts will be available on Maven Central within ~10 minutes + +## Troubleshooting + +### Build Fails with "invalid signature" + +- Verify `SIGNING_KEY` is base64-encoded correctly +- Ensure `SIGNING_PASS` matches your GPG key passphrase + +### Publish Fails with "401 Unauthorized" + +- Verify `OSS_USER` and `OSS_PASS` are correct +- Ensure your Sonatype account has permission for `cz.smarteon` group + +### Tag Already Exists + +- The release process will fail if the version tag already exists +- Delete the tag if needed: `git push --delete origin ` + +### Staging Repository Not Closing + +- Check Sonatype Nexus UI: https://oss.sonatype.org/#stagingRepositories +- Look for validation errors in the "Activity" tab +- Common issues: missing signatures, invalid POM metadata + +## Version Management + +This project uses [axion-release](https://github.com/allegro/axion-release-plugin) for version management: + +- Versions are derived from git tags +- Current version: `./gradlew currentVersion` +- When on a tagged commit: release version (e.g., `2.11.1`) +- When ahead of a tag: snapshot version (e.g., `2.11.2-SNAPSHOT`) + +## Additional Resources + +- [Axion Release Plugin Documentation](https://axion-release-plugin.readthedocs.io/) +- [Nexus Publish Plugin](https://github.com/gradle-nexus/publish-plugin) +- [Maven Central Publishing Guide](https://central.sonatype.org/publish/) +- [Sonatype Nexus Repository Manager](https://oss.sonatype.org/) diff --git a/build.gradle.kts b/build.gradle.kts index 9e1422e..4f86b2a 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -1,11 +1,21 @@ group = "cz.smarteon" +scmVersion { + tag { + prefix.set("") + versionSeparator.set("") + } +} + +project.version = scmVersion.version + plugins { `java-library` signing `maven-publish` jacoco - id("net.researchgate.release") version "2.6.0" + id("pl.allegro.tech.build.axion-release") version "1.14.4" + id("io.github.gradle-nexus.publish-plugin") version "1.3.0" id("ru.vyarus.quality") version "4.8.0" kotlin("jvm") version "1.7.10" } @@ -78,8 +88,8 @@ dependencies { testImplementation("org.awaitility:awaitility-kotlin:4.1.1") } -val ossUser: String? by project -val ossPass: String? by project +val ossUser: String? = System.getenv("OSS_USER") +val ossPass: String? = System.getenv("OSS_PASS") publishing { publications { @@ -128,22 +138,30 @@ publishing { } } } +} - repositories { - maven { - name = "oss" - url = uri("https://oss.sonatype.org/service/local/staging/deploy/maven2/") - authentication { - credentials { - username = ossUser - password = ossPass - } +if (ossUser != null && ossPass != null) { + nexusPublishing { + repositories { + sonatype { + username.set(ossUser) + password.set(ossPass) } } } } -if (hasProperty("signing.keyId")) { +val signingKey: String? = System.getenv("SIGNING_KEY") +val signingPassword: String? = System.getenv("SIGNING_PASS") +if (signingKey != null && signingPassword != null) { + signing { + setRequired({ + !project.version.toString().endsWith("-SNAPSHOT") + }) + useInMemoryPgpKeys(signingKey, signingPassword) + sign(publishing.publications["library"]) + } +} else if (hasProperty("signing.keyId")) { signing { setRequired({ !project.version.toString().endsWith("-SNAPSHOT") @@ -182,8 +200,4 @@ tasks { check { dependsOn(jacocoTestReport) } - - afterReleaseBuild { - dependsOn(getByName("publishLibraryPublicationToOssRepository")) - } } diff --git a/gradle.properties b/gradle.properties index f637d47..6612600 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,4 +1,2 @@ -version=2.11.1-SNAPSHOT - # do not auto add kotlin stdlib to library dependencies kotlin.stdlib.default.dependency=false