Why now
A static-analysis pass with zizmor flagged 37 high-severity findings across .github/workflows/, including six template-injection sites in the release workflow itself. Independent of that, the workflow has no tests gating the publish step, so a regression on main ships to Central. To fix, its largely mechanical but they're coupled enough that doing them as one design pass is cleaner than one-off PRs.
Problems in the current release pipeline
Working through .github/workflows/release.yaml and the release profile in pom.xml:
- No tests gate the release. Both
mvn -Dquickly and mvn deploy ... -DskipTests=true skip the test suite. The dispatch button publishes whatever is on main.
- No protected environment around the deploy.
workflow_dispatch can be invoked by anyone with workflow write access; the release secrets (GPG_PRIVATE_KEY, SONATYPE_*, GH_TOKEN) are exposed at the repo level.
branch: input is dead. The workflow takes a branch: input but actions/checkout never references it. Dispatching from a non-default branch silently releases main.
- Attestations would land AFTER
mvn deploy succeeds — if attestation issuance fails, jars are already on Central, unattested. Want: verify → attest the bytes → deploy.
- PAT (
GH_TOKEN) used to push to main. mvn versions:set + git push bypasses branch protection. The PAT is long-lived and exfiltratable.
- GPG key imported twice; once via insecure shell expansion.
setup-java already imports the GPG key; the second cat <(echo -e "${{ secrets.GPG_PRIVATE_KEY }}") | gpg --batch --import step expands the secret into a rendered shell command.
mvn -X in the deploy step. Full debug output; some Maven plugins log credentials and signing-key paths at DEBUG.
- No SHA pinning on any action.
actions/checkout@v6, actions/setup-java@v5, etc. across every workflow. Mutable tags = compromised-publisher / tag-mover risk.
- Over-scoped
permissions: at the workflow level — unused deployments: write and packages: write.
softprops/action-gh-release creates an empty GH release — no jars, no SBOM, no checksums attached.
- Redundant
Compile step runs before deploy and is then thrown away by the subsequent clean deploy.
SONATYPE_USERNAME / SONATYPE_PASSWORD — need to confirm these are Central Portal user tokens, not legacy OSSRH credentials.
- Two commits to
main per release (Release version update X and Snapshot version update). Tag points at a commit that's no longer on main.
- No reproducible-build flag. Re-running the workflow produces a different jar SHA each time; attestations only prove "built once at this time," not "this exact bytes."
central-publishing-maven-plugin 0.10.0 — currently the latest, but worth flagging for periodic bumps.
- No concurrency group — two simultaneous dispatches will conflict on git pushes.
Proposed changes
1. Test-gated release (P0)
Replace the deploy invocation with a three-phase flow:
mvn verify -Drelease — compile, run full test suite, sign (GPG), generate aggregate CycloneDX SBOM. A failure here stops the release before anything is published.
actions/attest-build-provenance and actions/attest-sbom against the bytes produced.
mvn deploy -Drelease -DskipTests=true -Dgpg.skip=true — reuses the artifacts and signatures from verify.
2. Defer all git pushes until after Central deploy succeeds (P0)
Edit pom.xml versions in the working tree, build, attest, deploy. Only then create the version commit, tag, and push. If anything fails before deploy, the remote is untouched and the dispatch is safe to re-run.
3. maven-central GitHub Environment (P0)
Add environment: maven-central to the release job. Repo admin will need to:
- Create the environment in repo settings.
- Add required reviewers.
- Set a deployment-branch policy restricting refs.
- Move the release secrets (
GPG_PRIVATE_KEY, GPG_PASSPHRASE, SONATYPE_USERNAME, SONATYPE_PASSWORD, GH_TOKEN) onto the environment so they cannot be read by unrelated workflows.
4. SLSA build provenance and SBOM attestations
- Add
actions/attest-build-provenance@<sha> # v3 against **/target/*.{jar,pom,jar.asc,pom.asc}.
- Add a
cyclonedx-maven-plugin execution in the release profile (root-only via <inherited>false</inherited>, makeAggregateBom bound to package).
- Add
actions/attest-sbom@<sha> # v3 against **/target/*.jar with the aggregate target/bom.json.
Configure the SBOM plugin with <includeBomSerialNumber>false</includeBomSerialNumber> so it's deterministic across rebuilds at the same SHA (otherwise the SBOM hash changes between the verify we attest and the deploy re-package). Also <skipNotDeployed>false</skipNotDeployed> because central-publishing sets maven.deploy.skip=true on the aggregator pom.
5. Reproducible builds
Set <project.build.outputTimestamp>1980-02-01T00:00:00Z</project.build.outputTimestamp> in the root pom. Strips timestamps from JAR entries so re-runs at the same SHA produce byte-identical artifacts. Required for SLSA-3 verification to mean what people think it means.
Note: the maven-shade-plugin's intermediate original-*.jar files (never deployed) will not be reproducible. Released artifacts will be.
6. SHA-pin every action
Replace every major-tag reference with a commit SHA + trailing version comment. The actions currently in use, with versions resolved live from each upstream's GitHub repo at the time this issue was drafted:
| Action |
Tag |
Commit SHA |
actions/checkout |
v6.0.2 |
de0fac2e4500dabe0009e67214ff5f5447ce83dd |
actions/setup-java |
v5.2.0 |
be666c2fcd27ec809703dec50e508c2fdc7f6654 |
actions/setup-node |
v6.4.0 |
48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e |
actions/upload-artifact |
v7.0.1 |
043fb46d1a93c77aae656e7c1c64a875d1fc6a0a |
actions/upload-pages-artifact |
v5.0.0 |
fc324d3547104276b827a68afc52ff2a11cc49c9 |
actions/deploy-pages |
v5.0.0 |
cd2ce8fcbc39b97be8ca5fce6e763baed58fa128 |
gradle/actions/setup-gradle |
v6.1.0 |
50e97c2cd7a37755bbfafc9c5b7cafaece252f6e |
actions/attest-build-provenance |
v3 |
977bb373ede98d70efdf65b84cb5f73e068dcc2a |
actions/attest-sbom |
v3 |
4651f806c01d8637787e274ac3bdf724ef169f34 |
Versions should be re-resolved at PR time in case there are newer point releases.
7. Dependabot grouping for github-actions
.github/dependabot.yml already has package-ecosystem: github-actions on a daily schedule. With ~9 SHA-pinned actions across the repo, each Dependabot run will fan out into one PR per action. Add a groups: block batching all action bumps into one weekly PR. Dependabot preserves the # vX.Y.Z trailing comment when bumping a SHA reference, so the audit trail stays readable.
8. zizmor findings to fix in release.yaml
Running uvx zizmor .github/workflows/release.yaml:
- 6 ×
template-injection (High) — ${{ github.event.inputs.* }} expanded directly into shell commands. Route inputs through env: vars referenced as "$VAR" so the shell, not the YAML preprocessor, expands them.
- 1 ×
superfluous-actions (Info) — softprops/action-gh-release is redundant when gh release create is available via the GitHub-supplied gh CLI. Drop the third-party action dependency.
In --persona=auditor mode, additional fixes:
- 3 ×
excessive-permissions — scope contents/id-token/attestations: write to the job level. Set permissions: {} at workflow scope as default-deny.
undocumented-permissions — add inline trailing comments justifying each permission line.
9. Sigstore keyless signing (getting into only nice-to-have territory)
Add dev.sigstore:sigstore-maven-plugin 1.3.0 to the release profile, bound to verify. Produces <artifact>.sigstore.json bundles OIDC-bound to the workflow identity. These ride with the artifact to Maven Central so consumers can verify without contacting GitHub. In the future, you could phase out maven-gpg although I don't think that's practical in the near-term.
10. Other repo-wide hygiene
- Remove the redundant Compile step.
- Remove
mvn -X from production.
- Remove the duplicate
gpg --batch --import step (setup-java already handles it).
persist-credentials: false on actions/checkout; explicit git remote set-url to inject GH_TOKEN only at the push step.
- Drop unused
deployments: write and packages: write permissions.
- Attach the deployed jars +
sbom-cyclonedx.json + SHA256SUMS to the GitHub release.
- Add a
concurrency: { group: release, cancel-in-progress: false } block.
11. Other workflows still have zizmor findings
Not in scope for the release pipeline rebuild, but worth tracking:
- 2 High + 2 Medium
excessive-permissions in ci.yaml, perf.yaml, deploy-docs.yml (workflow-level perms that should be job-scoped).
- 14 Low
artipacked warnings — every actions/checkout without persist-credentials: false. zizmor --fix=safe handles these mechanically.
- 7 Info
template-injection in perf.yaml — ${{ needs.<job>.outputs.result-link }} in shell. Low practical risk (input source is a JMH result URL from an upstream job) but the env-var fix is mechanical.
Out of scope (deferred)
R5 / R13 — eliminating the PAT-push-to-main pattern entirely. The proper fix is ${revision} placeholders + flatten-maven-plugin so no version-edit commits are needed, or a release-please-style PR flow. Both touch every module pom and deserve a dedicated design discussion.
Action items requiring repo-admin or human follow-up
Why now
A static-analysis pass with
zizmorflagged 37 high-severity findings across.github/workflows/, including six template-injection sites in the release workflow itself. Independent of that, the workflow has no tests gating the publish step, so a regression onmainships to Central. To fix, its largely mechanical but they're coupled enough that doing them as one design pass is cleaner than one-off PRs.Problems in the current release pipeline
Working through
.github/workflows/release.yamland thereleaseprofile inpom.xml:mvn -Dquicklyandmvn deploy ... -DskipTests=trueskip the test suite. The dispatch button publishes whatever is onmain.workflow_dispatchcan be invoked by anyone withworkflowwrite access; the release secrets (GPG_PRIVATE_KEY,SONATYPE_*,GH_TOKEN) are exposed at the repo level.branch:input is dead. The workflow takes abranch:input butactions/checkoutnever references it. Dispatching from a non-default branch silently releasesmain.mvn deploysucceeds — if attestation issuance fails, jars are already on Central, unattested. Want:verify→ attest the bytes →deploy.GH_TOKEN) used to push tomain.mvn versions:set+git pushbypasses branch protection. The PAT is long-lived and exfiltratable.setup-javaalready imports the GPG key; the secondcat <(echo -e "${{ secrets.GPG_PRIVATE_KEY }}") | gpg --batch --importstep expands the secret into a rendered shell command.mvn -Xin the deploy step. Full debug output; some Maven plugins log credentials and signing-key paths at DEBUG.actions/checkout@v6,actions/setup-java@v5, etc. across every workflow. Mutable tags = compromised-publisher / tag-mover risk.permissions:at the workflow level — unuseddeployments: writeandpackages: write.softprops/action-gh-releasecreates an empty GH release — no jars, no SBOM, no checksums attached.Compilestep runs before deploy and is then thrown away by the subsequentclean deploy.SONATYPE_USERNAME/SONATYPE_PASSWORD— need to confirm these are Central Portal user tokens, not legacy OSSRH credentials.mainper release (Release version update XandSnapshot version update). Tag points at a commit that's no longer onmain.central-publishing-maven-plugin0.10.0 — currently the latest, but worth flagging for periodic bumps.Proposed changes
1. Test-gated release (P0)
Replace the deploy invocation with a three-phase flow:
mvn verify -Drelease— compile, run full test suite, sign (GPG), generate aggregate CycloneDX SBOM. A failure here stops the release before anything is published.actions/attest-build-provenanceandactions/attest-sbomagainst the bytes produced.mvn deploy -Drelease -DskipTests=true -Dgpg.skip=true— reuses the artifacts and signatures fromverify.2. Defer all git pushes until after Central deploy succeeds (P0)
Edit
pom.xmlversions in the working tree, build, attest, deploy. Only then create the version commit, tag, and push. If anything fails before deploy, the remote is untouched and the dispatch is safe to re-run.3.
maven-centralGitHub Environment (P0)Add
environment: maven-centralto the release job. Repo admin will need to:GPG_PRIVATE_KEY,GPG_PASSPHRASE,SONATYPE_USERNAME,SONATYPE_PASSWORD,GH_TOKEN) onto the environment so they cannot be read by unrelated workflows.4. SLSA build provenance and SBOM attestations
actions/attest-build-provenance@<sha> # v3against**/target/*.{jar,pom,jar.asc,pom.asc}.cyclonedx-maven-pluginexecution in thereleaseprofile (root-only via<inherited>false</inherited>,makeAggregateBombound topackage).actions/attest-sbom@<sha> # v3against**/target/*.jarwith the aggregatetarget/bom.json.Configure the SBOM plugin with
<includeBomSerialNumber>false</includeBomSerialNumber>so it's deterministic across rebuilds at the same SHA (otherwise the SBOM hash changes between theverifywe attest and thedeployre-package). Also<skipNotDeployed>false</skipNotDeployed>because central-publishing setsmaven.deploy.skip=trueon the aggregator pom.5. Reproducible builds
Set
<project.build.outputTimestamp>1980-02-01T00:00:00Z</project.build.outputTimestamp>in the root pom. Strips timestamps from JAR entries so re-runs at the same SHA produce byte-identical artifacts. Required for SLSA-3 verification to mean what people think it means.Note: the maven-shade-plugin's intermediate
original-*.jarfiles (never deployed) will not be reproducible. Released artifacts will be.6. SHA-pin every action
Replace every major-tag reference with a commit SHA + trailing version comment. The actions currently in use, with versions resolved live from each upstream's GitHub repo at the time this issue was drafted:
actions/checkoutde0fac2e4500dabe0009e67214ff5f5447ce83ddactions/setup-javabe666c2fcd27ec809703dec50e508c2fdc7f6654actions/setup-node48b55a011bda9f5d6aeb4c2d9c7362e8dae4041eactions/upload-artifact043fb46d1a93c77aae656e7c1c64a875d1fc6a0aactions/upload-pages-artifactfc324d3547104276b827a68afc52ff2a11cc49c9actions/deploy-pagescd2ce8fcbc39b97be8ca5fce6e763baed58fa128gradle/actions/setup-gradle50e97c2cd7a37755bbfafc9c5b7cafaece252f6eactions/attest-build-provenance977bb373ede98d70efdf65b84cb5f73e068dcc2aactions/attest-sbom4651f806c01d8637787e274ac3bdf724ef169f34Versions should be re-resolved at PR time in case there are newer point releases.
7. Dependabot grouping for
github-actions.github/dependabot.ymlalready haspackage-ecosystem: github-actionson a daily schedule. With ~9 SHA-pinned actions across the repo, each Dependabot run will fan out into one PR per action. Add agroups:block batching all action bumps into one weekly PR. Dependabot preserves the# vX.Y.Ztrailing comment when bumping a SHA reference, so the audit trail stays readable.8.
zizmorfindings to fix inrelease.yamlRunning
uvx zizmor .github/workflows/release.yaml:template-injection(High) —${{ github.event.inputs.* }}expanded directly into shell commands. Route inputs throughenv:vars referenced as"$VAR"so the shell, not the YAML preprocessor, expands them.superfluous-actions(Info) —softprops/action-gh-releaseis redundant whengh release createis available via the GitHub-suppliedghCLI. Drop the third-party action dependency.In
--persona=auditormode, additional fixes:excessive-permissions— scopecontents/id-token/attestations: writeto the job level. Setpermissions: {}at workflow scope as default-deny.undocumented-permissions— add inline trailing comments justifying each permission line.9. Sigstore keyless signing (getting into only nice-to-have territory)
Add
dev.sigstore:sigstore-maven-plugin1.3.0 to thereleaseprofile, bound toverify. Produces<artifact>.sigstore.jsonbundles OIDC-bound to the workflow identity. These ride with the artifact to Maven Central so consumers can verify without contacting GitHub. In the future, you could phase out maven-gpg although I don't think that's practical in the near-term.10. Other repo-wide hygiene
mvn -Xfrom production.gpg --batch --importstep (setup-java already handles it).persist-credentials: falseonactions/checkout; explicitgit remote set-urlto injectGH_TOKENonly at the push step.deployments: writeandpackages: writepermissions.sbom-cyclonedx.json+SHA256SUMSto the GitHub release.concurrency: { group: release, cancel-in-progress: false }block.11. Other workflows still have
zizmorfindingsNot in scope for the release pipeline rebuild, but worth tracking:
excessive-permissionsinci.yaml,perf.yaml,deploy-docs.yml(workflow-level perms that should be job-scoped).artipackedwarnings — everyactions/checkoutwithoutpersist-credentials: false.zizmor --fix=safehandles these mechanically.template-injectioninperf.yaml—${{ needs.<job>.outputs.result-link }}in shell. Low practical risk (input source is a JMH result URL from an upstream job) but the env-var fix is mechanical.Out of scope (deferred)
R5 / R13 — eliminating the PAT-push-to-main pattern entirely. The proper fix is
${revision}placeholders +flatten-maven-pluginso no version-edit commits are needed, or a release-please-style PR flow. Both touch every module pom and deserve a dedicated design discussion.Action items requiring repo-admin or human follow-up
maven-centralGitHub Environment with required reviewers + deployment-branch policy. Move the five release secrets onto it.SONATYPE_USERNAME/SONATYPE_PASSWORDare Central Portal user tokens (not legacy OSSRH credentials).${revision}vs release-please vs status quo).