|
| 1 | +--- |
| 2 | +name: ci-cd-pipeline |
| 3 | +description: >- |
| 4 | + Use when modifying Azure Pipelines, build templates, signing |
| 5 | + configuration, Maven Central publishing, GitHub Packages publishing, |
| 6 | + or release process. Covers GPG signing, JAR code signing, version |
| 7 | + strategy, and critical pipeline pitfalls. |
| 8 | +--- |
| 9 | + |
| 10 | +# CI/CD Pipeline |
| 11 | + |
| 12 | +The project uses **Azure Pipelines** (`azure-pipelines.yml`) with shared templates in |
| 13 | +`pipeline-templates/`. |
| 14 | + |
| 15 | +## Pipeline architecture |
| 16 | + |
| 17 | +The pipeline has **two jobs**, matching the SafeguardDotNet pattern: |
| 18 | + |
| 19 | +| Job | Runs when | What it does | |
| 20 | +|---|---|---| |
| 21 | +| **PRValidation** | Pull requests only | `mvn package` — compile, lint, SpotBugs | |
| 22 | +| **BuildAndPublish** | Merges to `master`/`release-*` | Build, GPG sign, JAR sign, publish to Maven Central + GitHub Packages, create GitHub Release | |
| 23 | + |
| 24 | +Both jobs share `build-steps.yml` with different parameters. PRValidation uses defaults |
| 25 | +(`package`, no signing); BuildAndPublish passes `deploy`, release profile flags, and |
| 26 | +`signJars: true`. |
| 27 | + |
| 28 | +## Pipeline template files |
| 29 | + |
| 30 | +| File | Scope | Contents | |
| 31 | +|---|---|---| |
| 32 | +| `global-variables.yml` | Pipeline-level | `semanticVersion`, `isPrerelease`, `versionSuffix` | |
| 33 | +| `job-variables.yml` | Job-level | `version` (composed), `targetDir`, `gpgKeyName` | |
| 34 | +| `build-steps.yml` | Steps template | Maven task + optional Docker JAR signing + artifact publishing | |
| 35 | + |
| 36 | +## Version strategy |
| 37 | + |
| 38 | +Version is composed from runtime variables: `$(semanticVersion).$(Build.BuildId)$(versionSuffix)` |
| 39 | + |
| 40 | +- When `isPrerelease: 'true'` → `versionSuffix` is `-SNAPSHOT` → e.g. `8.2.0.355537-SNAPSHOT` |
| 41 | +- When `isPrerelease: 'false'` → `versionSuffix` is empty → e.g. `8.2.0.355537` |
| 42 | + |
| 43 | +The pom.xml uses `<version>${revision}</version>` with CI-friendly properties. The actual |
| 44 | +version is always injected via `-Drevision=$(version)` on the Maven command line. |
| 45 | + |
| 46 | +## Service connections and key vaults |
| 47 | + |
| 48 | +| Service connection | Key vault | Secrets | |
| 49 | +|---|---|---| |
| 50 | +| `SafeguardOpenSource` | `SafeguardBuildSecrets` | `SonatypeUserToken`, `SonatypeRepositoryPassword`, `GpgCodeSigningKey`, `SigningStorePassword`, `GitHubPackagesToken` | |
| 51 | +| `OneIdentity.Infrastructure.SPPCodeSigning` | `SPPCodeSigning` | `SPPCodeSigning-Password`, `SPPCodeSigning-TotpPrivateKey` | |
| 52 | +| `PangaeaBuild-GitHub` | *(none)* | GitHub service connection for Release creation | |
| 53 | + |
| 54 | +## Two signing mechanisms |
| 55 | + |
| 56 | +The pipeline uses **two independent signing mechanisms** that serve different purposes: |
| 57 | + |
| 58 | +### 1. GPG signing (`maven-gpg-plugin`) |
| 59 | + |
| 60 | +Produces `.asc` detached signature files required by Maven Central for release validation. |
| 61 | +Configured in the `release` Maven profile. |
| 62 | + |
| 63 | +- GPG private key: vault secret `GpgCodeSigningKey`, imported via `gpg --batch --import` |
| 64 | +- GPG passphrase: vault secret `SigningStorePassword` |
| 65 | +- `settings/settings.xml` maps the passphrase via a server entry: |
| 66 | + `<id>${gpgkeyname}</id>` + `<passphrase>${signingkeystorepassword}</passphrase>` |
| 67 | +- Maven command line needs both `-Dsigningkeystorepassword=$(SigningStorePassword)` and |
| 68 | + `-Dgpgkeyname=$(gpgKeyName)` |
| 69 | +- The plugin uses `--batch --pinentry-mode loopback` for non-interactive passphrase input |
| 70 | +- **CRITICAL:** The GPG plugin is bound to the `verify` lifecycle phase. `mvn package` |
| 71 | + stops at the `package` phase and **never triggers GPG signing**. Only `mvn deploy` |
| 72 | + (or `mvn verify` / `mvn install`) reaches the GPG plugin. This means you cannot test |
| 73 | + GPG signing with `mvn package -P release` — it will succeed silently without signing. |
| 74 | + |
| 75 | +### 2. JAR code signing (SSL.com CodeSigner Docker image) |
| 76 | + |
| 77 | +Embeds certificates in `META-INF/` (CERT.SF, CERT.RSA) inside the JAR for Java runtime |
| 78 | +signature verification. |
| 79 | + |
| 80 | +- Uses Docker image `ghcr.io/sslcom/codesigner:latest` which bundles its own JRE |
| 81 | +- eSigner account: `ssl.oid.safeguardpp@groups.quest.com` |
| 82 | +- Credentials: `SPPCodeSigning-Password` (password) + `SPPCodeSigning-TotpPrivateKey` (TOTP) |
| 83 | +- The `-override` flag signs the JAR **in place** (no separate output file) |
| 84 | +- `ENVIRONMENT_NAME=PROD` is required for production signing (vs sandbox) |
| 85 | +- No `CREDENTIAL_ID` is needed when the eSigner account has only one certificate |
| 86 | +- Runs as a **post-build step** after Maven completes, not as a Maven plugin |
| 87 | + |
| 88 | +## CodeSignTool pitfalls (IMPORTANT) |
| 89 | + |
| 90 | +**Do NOT attempt to run CodeSignTool directly on the build agent.** The standalone |
| 91 | +CodeSignTool v1.3.2 is compiled for Java 11 and has strict JVM compatibility requirements: |
| 92 | + |
| 93 | +- **Java 8** → `UnsupportedClassVersionError` (class file version 55.0, needs Java 11+) |
| 94 | +- **Java 11** → Works, but requires a separate JDK install on the build agent |
| 95 | +- **Java 17+** → `IllegalAccessError` due to JPMS module access restrictions |
| 96 | + |
| 97 | +The **Docker image is the correct approach** — it bundles a compatible JRE inside the |
| 98 | +container, isolating CodeSignTool from the host Java version entirely. The build agent |
| 99 | +only needs Docker installed (which Azure Pipelines `ubuntu-latest` provides by default). |
| 100 | + |
| 101 | +## Maven Central publishing |
| 102 | + |
| 103 | +Publishing uses the `central-publishing-maven-plugin` v0.7.0 with `autoPublish: false` |
| 104 | +and `waitUntil: validated`. The plugin's `publishingServerId: central` maps to the |
| 105 | +`<server id="central">` entry in `settings/settings.xml`. |
| 106 | + |
| 107 | +**SNAPSHOT publishing** requires: |
| 108 | +- `central-publishing-maven-plugin` v0.7.0+ (earlier versions don't support SNAPSHOTs) |
| 109 | +- Explicit enablement per namespace at central.sonatype.com → Namespaces → dropdown → |
| 110 | + "Enable SNAPSHOTs" |
| 111 | +- Without enablement, SNAPSHOT deploys return **403 Forbidden** |
| 112 | +- SNAPSHOTs are not validated, can be overwritten, and are cleaned up after ~90 days |
| 113 | + |
| 114 | +## GitHub Packages publishing |
| 115 | + |
| 116 | +Uses `mvn deploy:deploy-file` with explicit artifact coordinates. **Do NOT use |
| 117 | +`-DpomFile=pom.xml`** — the `deploy-file` goal reads the pom literally without resolving |
| 118 | +Maven properties. Since our pom has `<version>${revision}</version>`, it would see |
| 119 | +`${revision}` as the literal version string (invalid characters: `$`, `{`, `}`). Instead, |
| 120 | +specify coordinates directly: `-DgroupId=... -DartifactId=... -Dversion=... -Dpackaging=jar`. |
| 121 | + |
| 122 | +Authentication uses a `<server id="github">` entry in `settings/settings.xml` with |
| 123 | +`${githubusername}` and `${githubtoken}` properties, resolved via `-D` flags on the |
| 124 | +command line. |
| 125 | + |
| 126 | +## Azure Pipelines variable scoping (CRITICAL pitfall) |
| 127 | + |
| 128 | +Azure Pipelines has two expression syntaxes with **very different scoping rules**: |
| 129 | + |
| 130 | +- **`${{ variables.X }}`** — Compile-time expression. Can **only** see variables defined |
| 131 | + in the **same template file**. Variables from other templates are invisible at compile |
| 132 | + time, even if they are in the same pipeline. |
| 133 | +- **`$(X)`** — Runtime macro. Resolves **after** all variable scopes (pipeline + job) are |
| 134 | + merged. Works across template boundaries. |
| 135 | + |
| 136 | +This means: |
| 137 | +- `${{ if eq(variables.isPrerelease, 'true') }}` in `job-variables.yml` **cannot** see |
| 138 | + `isPrerelease` defined in `global-variables.yml` — it will always evaluate to false. |
| 139 | +- The fix: compute dependent values (like `versionSuffix`) in the **same template** as |
| 140 | + the variables they reference, then use `$(versionSuffix)` runtime macros elsewhere. |
| 141 | +- Also note: `${{ true }}` as a variable value becomes the string `'True'` (capital T). |
| 142 | + Use the string `'true'` (lowercase) to avoid comparison mismatches. |
| 143 | + |
| 144 | +### Example of the scoping pitfall |
| 145 | + |
| 146 | +```yaml |
| 147 | +# global-variables.yml — defines isPrerelease |
| 148 | +variables: |
| 149 | + isPrerelease: 'true' |
| 150 | + |
| 151 | +# job-variables.yml — WRONG: can't see isPrerelease from global-variables.yml |
| 152 | +variables: |
| 153 | + versionSuffix: ${{ if eq(variables.isPrerelease, 'true') }}-SNAPSHOT${{ else }}${{ endif }} |
| 154 | + # ↑ Always evaluates to empty string because isPrerelease is invisible here |
| 155 | + |
| 156 | +# FIX: compute versionSuffix in global-variables.yml alongside isPrerelease, |
| 157 | +# then reference as $(versionSuffix) runtime macro in job-variables.yml |
| 158 | +``` |
| 159 | + |
| 160 | +## Pipeline modification checklist |
| 161 | + |
| 162 | +When modifying the CI/CD pipeline: |
| 163 | +1. **Test GPG signing with `mvn deploy`**, not `mvn package` (GPG is in `verify` phase) |
| 164 | +2. **Never use `-DpomFile=pom.xml`** with `deploy:deploy-file` — use explicit coordinates |
| 165 | +3. **Keep compile-time conditionals in the same template** as the variables they reference |
| 166 | +4. **Use string `'true'`/`'false'`** for boolean pipeline variables, not `${{ true }}` |
| 167 | +5. **Verify CodeSignTool via Docker only** — never install it directly on the build agent |
| 168 | +6. **All credentials flow through `settings/settings.xml`** via Maven property placeholders |
| 169 | + resolved by `-D` flags — Azure DevOps auto-masks vault secrets in logs |
| 170 | + |
| 171 | +## Full deploy command reference |
| 172 | + |
| 173 | +The complete `mvn deploy` invocation for a release build (for reference): |
| 174 | + |
| 175 | +```bash |
| 176 | +mvn deploy -P release \ |
| 177 | + --settings settings/settings.xml \ |
| 178 | + "-Drevision=$(version)" \ |
| 179 | + "-Dsigningkeystorepassword=$(SigningStorePassword)" \ |
| 180 | + "-Dgpgkeyname=$(gpgKeyName)" \ |
| 181 | + "-Dcentralusername=$(SonatypeUserToken)" \ |
| 182 | + "-Dcentralpassword=$(SonatypeRepositoryPassword)" |
| 183 | +``` |
| 184 | + |
| 185 | +All `-D` properties map to `<server>` entries in `settings/settings.xml` via Maven |
| 186 | +property placeholders (e.g., `<passphrase>${signingkeystorepassword}</passphrase>`). |
| 187 | +Azure DevOps automatically masks vault secrets in log output. |
0 commit comments