bytecodealliance / bytecodealliance/endive

Harden release CI

Open
#28 1 comment 0 reactions 0 assignees View on GitHub
Dominant language
Java
Stars
296
Forks
21
Avg merge
2d 12h
Merged PRs (30d)
29

Description

## 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`:

1. **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`.
2. **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.
3. **`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`.
4. **Attestations would land AFTER `mvn deploy` succeeds** — if attestation issuance fails, jars are already on Central, unattested. Want: `verify` → attest the bytes → `deploy`.
5. **PAT (`GH_TOKEN`) used to push to `main`.** `mvn versions:set` + `git push` bypasses branch protection. The PAT is long-lived and exfiltratable.
6. **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.
7. **`mvn -X` in the deploy step.** Full debug output; some Maven plugins log credentials and signing-key paths at DEBUG.
8. **No SHA pinning on any action.** `actions/checkout@v6`, `actions/setup-java@v5`, etc. across every workflow. Mutable tags = compromised-publisher / tag-mover risk.
9. **Over-scoped `permissions:`** at the workflow level — unused `deployments: write` and `packages: write`.
10. **`softprops/action-gh-release` creates an empty GH release** — no jars, no SBOM, no checksums attached.
11. **Redundant `Compile` step** runs before deploy and is then thrown away by the subsequent `clean deploy`.
12. **`SONATYPE_USERNAME` / `SONATYPE_PASSWORD`** — need to confirm these are Central Portal user tokens, not legacy OSSRH credentials.
13. **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`.
14. **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."
15. **`central-publishing-maven-plugin` 0.10.0** — currently the latest, but worth flagging for periodic bumps.
16. **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@ # v3` against `**/target/*.{jar,pom,jar.asc,pom.asc}`.
- Add a `cyclonedx-maven-plugin` execution in the `release` profile (root-only via `false`, `makeAggregateBom` bound to `package`).
- Add `actions/attest-sbom@ # v3` against `**/target/*.jar` with the aggregate `target/bom.json`.

Configure the SBOM plugin with `false` 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 `false` because central-publishing sets `maven.deploy.skip=true` on the aggregator pom.

### 5. Reproducible builds

Set `1980-02-01T00:00:00Z` 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 `.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..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

- [ ] Create `maven-central` GitHub Environment with required reviewers + deployment-branch policy. Move the five release secrets onto it.
- [ ] Confirm `SONATYPE_USERNAME` / `SONATYPE_PASSWORD` are Central Portal user tokens (not legacy OSSRH credentials).
- [ ] After first release with both GPG and Sigstore signatures, decide on Sigstore phase 2 (drop GPG, retire long-lived key).
- [ ] Decide on R5 / R13 direction (`${revision}` vs release-please vs status quo).

Contributor guide

Open the contributing guide

Assessment

This issue has not been assessed yet.

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.