Skip to content

ci: add gated provenance-emitting release workflow - #17

Open
yakimoto wants to merge 3 commits into
mainfrom
ci/release-provenance-workflow
Open

ci: add gated provenance-emitting release workflow#17
yakimoto wants to merge 3 commits into
mainfrom
ci/release-provenance-workflow

Conversation

@yakimoto

@yakimoto yakimoto commented Aug 5, 2026

Copy link
Copy Markdown
Contributor

What this does

Adds .github/workflows/release.yml, a v*-tag-triggered, gated public-npm
release path for @wave-av/cli, modelled directly on wave-av/adk's
release.yml (the file that published @wave-av/adk@1.0.15 today with
OIDC trusted-publisher + --provenance).

Three gates, in order: secret-scan (pinned+checksummed gitleaks + the
existing WAVE content-policy script) -> verify (install, lint/type-check/
test if declared, build, then an e2e-smoke that packs the real tarball,
installs it into a throwaway project, and imports it) -> publish (OIDC
trusted publishing only — no NODE_AUTH_TOKEN/NPM_TOKEN anywhere in this
file — gated on a tag-version == package.json-version equality check, and
on both prior gates being green). All third-party actions are pinned to
full commit SHAs, copied from the adk file.

This PR does NOT publish anything and does NOT push a tag. It only adds
the workflow file. The first v* tag push is a separate, explicit,
operator-authorized act — not part of this change.

Known gap — please read before merging

main in this repo currently has no package.json and no src/
only governance scaffolding (AGENTS.md, capabilities.json, the guard
CI). @wave-av/cli was hand-published to npm from a checkout that was
never committed to this repo. Until a package.json + build (the repo
would need tsup/eslint/vitest per the shape already published as
@wave-av/cli@1.0.8, confirmed by fetching that version's manifest from
the npm registry) lands on main, the verify job in this workflow will
fail at npm ci (no manifest to install against). This PR builds the
release path; it does not by itself make the repo publishable — that
needs a separate PR that actually commits the source.

Before the first tag push

The Trusted Publisher for @wave-av/cli (org: wave-av, repo: cli,
workflow: release.yml) must be registered/verified in the npmjs.com UI
before any v* tag is pushed. If it isn't registered (or the
repo/workflow name doesn't match exactly), the publish job's npm publish --provenance step fails with npm error code EOTP — that's a
2FA prompt, and it's how an unregistered/mismatched Trusted Publisher
binding actually presents, not an obviously-labelled "not configured"
error.

Verification performed

  • python3 -c "import yaml; yaml.safe_load(open('.github/workflows/release.yml'))" — parses clean, all 3 jobs (secret-scan, verify, publish) present.
  • actionlint .github/workflows/release.yml — exit 0, zero findings.
  • Checked every step name: for the unquoted-colon-space bug that broke wave-av/sdks' publish-npm.yml for 28 runs — none present.

Tag findings (read-only, reported, not acted on)

A v*-shaped tag already exists in this repo: v1.0.0 (only one found via
gh api repos/wave-av/cli/tags). It predates the currently-published
1.0.8 on npm, so it does not correspond to the latest hand-published
version — worth the operator's attention separately from this PR.


Note

Medium Risk
Introduces the production npm publish path with OIDC and provenance, but publish only runs after secret-scan and verify pass; misconfiguration (Trusted Publisher, tag/version mismatch) fails closed rather than shipping a bad release.

Overview
Adds .github/workflows/release.yml, a v* tag-triggered path to publish @wave-av/cli to public npm. Nothing is published by merging this PR alone; the first release still requires a separate tag push and npm Trusted Publisher setup.

The workflow runs three sequential gates: secret-scan (pinned gitleaks + the same WAVE content-policy.sh as merge CI), verify (npm ci, optional lint/type-check/test when declared in package.json, build, then tarball e2e-smoke with ESM import and wave bin checks), and publish (only if both prior jobs pass). Publishing uses npm OIDC trusted publishing with --provenance—no long-lived npm token—and enforces tag version == package.json version, with prereleases tagged next and stable releases latest.

Known gap: main still has no package.json/src/; the verify job will fail at npm ci until the package source is committed in a follow-up PR.

Reviewed by Cursor Bugbot for commit 07b5ef5. Configure here.

Review in cubic

Note

Add gated provenance-emitting release workflow for npm publishing

  • Adds release.yml, a GitHub Actions workflow triggered on v* tags, with three sequential gated jobs: secret-scan, verify, and publish.
  • secret-scan runs gitleaks and a content policy script against the source tree, blocking the release if secrets or internal content are detected.
  • verify installs dependencies, conditionally runs lint/type-check/tests, builds the package, rescans dist/ with gitleaks, and e2e smoke-tests the packed tarball (ESM import + wave bin presence and executability).
  • publish validates that the git tag version matches package.json, selects a dist-tag (latest or next), and publishes to npm using OIDC-based trusted publishing with provenance (no secret token required).
  • Risk: publish requires npm ≥ 11.5.1 for OIDC trusted publishing; the workflow enforces this and fails if the version floor is not met.

Macroscope summarized d95fbd3.

…red to a tag push)

Adds .github/workflows/release.yml on a v* tag trigger, modelled on
wave-av/adk's proven release.yml (secret-scan -> verify -> publish, OIDC
trusted publishing + --provenance, tag/package.json version equality gate,
no NODE_AUTH_TOKEN, all third-party actions pinned to full commit SHAs).

Does NOT publish anything and pushes no tag. See PR description for the
known gap: main has no package.json/src today, so the verify job cannot
succeed until source is committed.
@cursor

cursor Bot commented Aug 5, 2026

Copy link
Copy Markdown

Bugbot couldn't run - usage limit reached

Bugbot is counted against Cursor usage for this user or team, and this run hit a usage or spend limit.

A user or team admin can review and increase usage limits in the Cursor dashboard.

(requestId: serverGenReqId_42662894-ad53-446b-a573-85615cd135c2)

@coderabbitai

coderabbitai Bot commented Aug 5, 2026

Copy link
Copy Markdown

Warning

Review limit reached

You’ve reached a temporary PR review limit under our Fair Usage Limits Policy.

Your recent review volume is higher than typical usage, so adaptive limits are currently applied.

Next review available in: 6 minutes

Your organization has reached its usage spending cap. Adjust your spending cap in the billing tab.

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: b989d557-bb22-4287-9d06-f95b7e25d23e

📥 Commits

Reviewing files that changed from the base of the PR and between 80d078b and 64cb6b3.

📒 Files selected for processing (1)
  • .github/workflows/release.yml

Comment @coderabbitai help to get the list of available commands.

@macroscopeapp

macroscopeapp Bot commented Aug 5, 2026

Copy link
Copy Markdown

Approvability

Verdict: Needs human review

Unable to check for correctness in d95fbd3. This PR establishes significant release infrastructure (npm publishing pipeline with OIDC provenance), which warrants human review despite the author owning the file. While most review comment concerns appear addressed in the current code, the security model and the architectural choice of building twice rather than publishing the exact verified artifact should be reviewed by a human.

You can customize Macroscope's approvability policy. Learn more.

@qodo-code-review

Copy link
Copy Markdown

PR Summary by Qodo

CI: add gated, provenance-emitting npm release workflow

⚙️ Configuration changes ✨ Enhancement 🕐 20-40 Minutes

Grey Divider

AI Description

• Add a v*-tag-triggered GitHub Actions release pipeline for public npm publishing.
• Gate publishing behind secret scanning, build/verify, and version-match checks.
• Use OIDC Trusted Publishing with provenance and pinned third-party action SHAs.
Diagram

graph TD
  A(("v* tag push")) --> B["Job: secret-scan"] --> C["Job: verify"] --> D{"Tag version == package.json?"}
  D --> E["Job: publish (OIDC + provenance)"] --> F{{"npm registry"}}
  D --> X["Fail release"]

  subgraph Legend
    direction LR
    _start(("Trigger")) ~~~ _job["Job"] ~~~ _dec{"Decision"} ~~~ _ext{{"External"}}
  end
Loading
High-Level Assessment

The following are alternative approaches to this PR:

1. Reusable org workflow (workflow_call) shared across repos
  • ➕ Reduces drift vs wave-av/adk and other repos; one place to patch security gates
  • ➕ Centralizes action pin updates and hardening changes
  • ➖ Adds indirection and requires cross-repo governance/versioning of the reusable workflow
  • ➖ May be harder to customize repo-specific verification (e2e tarball checks, scripts)
2. GitHub Environments with manual approvals for the publish job
  • ➕ Adds an explicit human gate before publishing
  • ➕ Clear audit trail for who approved a release
  • ➖ Slower releases; operational friction for routine patch publishing
  • ➖ Does not replace the need for version matching, provenance, and secret scanning
3. Release automation tool (e.g., Changesets or release-please) to manage tags/versions
  • ➕ Avoids manual tag/version mismatch by generating tags from versioned changes
  • ➕ Provides changelog generation and consistent versioning practices
  • ➖ More moving parts and repo conventions required
  • ➖ Overkill if the repo expects occasional, operator-driven tagged releases

Recommendation: The PR’s approach is sound for a security-first, operator-driven tagged release path: three explicit gates, pinned actions, checksum-verified gitleaks, and OIDC-only publishing with provenance. If multiple wave-av repos will adopt the same hardened pipeline, consider evolving this into a reusable workflow to prevent drift; otherwise, keeping it local (as done here) is appropriate for repo-specific verification like the tarball install/import/bin checks.

Files changed (1) +265 / -0

Other (1) +265 / -0
release.ymlAdd gated v*-tag release workflow with OIDC provenance publishing +265/-0

Add gated v*-tag release workflow with OIDC provenance publishing

• Introduces a three-stage release pipeline triggered by v* tags: secret scanning (gitleaks + content policy), verification (install/lint/type-check/test if present, build, and tarball e2e smoke), and gated publish. Publishing uses npm OIDC Trusted Publishing with --provenance, enforces tag-version == package.json version, and selects dist-tag (next vs latest) based on prerelease status.

.github/workflows/release.yml

devin-ai-integration[bot]

This comment was marked as resolved.

@qodo-code-review

qodo-code-review Bot commented Aug 5, 2026

Copy link
Copy Markdown

Code Review by Qodo

🐞 Bugs (1) 📘 Rule violations (0) 📜 Skill insights (0)

Grey Divider


Action required

1. OIDC job runs npm ci ✓ Resolved 🐞 Bug ⛨ Security
Description
The publish job has id-token: write but runs npm ci, which executes dependency lifecycle
scripts by default. This unnecessarily exposes the OIDC-enabled job to arbitrary code execution from
dependencies, increasing the blast radius of any compromised dependency/lockfile.
Code

.github/workflows/release.yml[R199-202]

+    permissions:
+      id-token: write   # mint the OIDC token npm exchanges for a publish credential
+      contents: read
+    steps:
Evidence
The publish job explicitly grants id-token: write and then runs npm ci, meaning dependency
install-time code runs within the same OIDC-enabled job context.

.github/workflows/release.yml[194-206]
.github/workflows/release.yml[231-232]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
The `publish` job enables `id-token: write` and then executes `npm ci`, which by default runs dependency install scripts. Any code that runs in this job has access to the job’s GitHub Actions environment and can attempt to request/exfiltrate an OIDC token.

## Issue Context
This workflow already has a dedicated `verify` job that can handle dependency installation/building/testing without `id-token: write`. The `publish` job should be minimized to only the steps required to publish.

## Fix Focus Areas
- .github/workflows/release.yml[194-232]

## Suggested fix
- Move dependency installation/build/pack into `verify` (where there is no `id-token: write`).
- In `publish`, avoid `npm ci` entirely by publishing the tarball artifact produced by `verify`.
- If you must keep `npm ci` in `publish`, at minimum reorder to run the tag/version check before any installs, and use `npm ci --ignore-scripts` (but note this may conflict with packages relying on publish-time lifecycle scripts).

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


2. Publishes unverified package 🐞 Bug ≡ Correctness
Description
The verify job smoke-tests a tarball produced by npm pack, but the publish job runs `npm
publish from a fresh checkout and does not publish the tested tarball (and does not run npm run
build`). This can ship a different/broken artifact than the one that passed Gate 2.
Code

.github/workflows/release.yml[R260-265]

+      - name: npm publish (OIDC trusted publishing)
+        env:
+          DIST_TAG: ${{ steps.ver.outputs.dist_tag }}
+        run: |
+          set -euo pipefail
+          npm publish --access public --provenance --tag "$DIST_TAG"
Evidence
The workflow explicitly packs and tests a tarball in verify, but publish does not consume any
artifact from verify and instead publishes from its own checkout, so the published contents are
not guaranteed to match the tarball that was tested.

.github/workflows/release.yml[145-183]
.github/workflows/release.yml[231-265]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
Gate 2 (`verify`) tests a tarball built via `npm pack`, but Gate 3 (`publish`) does not publish that tarball; it publishes from a separate workspace. This breaks the guarantee that what was verified is what gets released.

## Issue Context
- `verify` builds and then runs an e2e smoke test against the packed tarball.
- `publish` checks out again and directly runs `npm publish` from the repo directory.

## Fix Focus Areas
- .github/workflows/release.yml[145-183]
- .github/workflows/release.yml[231-265]

## Suggested fix
1. In `verify`, after `npm pack`, persist the tarball path/name and upload it via `actions/upload-artifact` (pinned to a commit SHA).
2. In `publish`, download that artifact via `actions/download-artifact` (pinned), and publish **that tarball**: `npm publish <tarball> --access public --provenance --tag "$DIST_TAG"`.
3. Optionally remove `npm ci` from `publish` entirely (or keep only what is strictly necessary) so the publish job becomes “publish-only” and cannot diverge from the verified build outputs.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools



Remediation recommended

3. Release workflow breaks without package.json ✗ Dismissed 📘 Rule violation ✧ Quality
Description
The new release workflow runs npm ci and reads ./package.json, but it also documents that this
repo currently has no package.json, so the workflow will fail when triggered. This introduces a
new CI failure path, violating the requirement to not introduce new lint/type/test-related CI
failures.
Code

.github/workflows/release.yml[111]

+      - run: npm ci
Evidence
PR Compliance ID 2497928 requires that configured lint/type/test CI commands not start failing after
the change. The added workflow explicitly notes the repo has no package.json and that verify
will fail at npm ci, and the workflow still runs npm ci / reads ./package.json in verify and
publish.

Rule 2497928: Changed code must not introduce new lint, type, or test failures in the existing CI configuration
.github/workflows/release.yml[6-12]
.github/workflows/release.yml[111-112]
.github/workflows/release.yml[236-247]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
The new `.github/workflows/release.yml` workflow executes `npm ci` / reads `./package.json`, but the repo currently has no `package.json`, so the workflow will fail when a `v*` tag is pushed.

## Issue Context
This PR adds a tag-triggered release path; however, until the actual package source/manifest exists in-repo, the release workflow should not introduce a failing CI path.

## Fix Focus Areas
- .github/workflows/release.yml[6-12]
- .github/workflows/release.yml[111-112]
- .github/workflows/release.yml[236-247]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools



Informational

4. NaN bypasses npm check ✓ Resolved 🐞 Bug ☼ Reliability
Description
The npm version floor check parses npm --version using split('.').map(Number), so versions
containing prerelease/build metadata can yield NaN parts and incorrectly pass the check. This can
allow the job to proceed without actually proving the runner’s npm meets the intended minimum.
Code

.github/workflows/release.yml[R225-228]

+            const cur=process.env.NPM_VER.split('.').map(Number), min=[11,5,1];
+            for(let i=0;i<3;i++){
+              if(cur[i]>min[i]) process.exit(0);
+              if(cur[i]<min[i]){console.error('npm '+process.env.NPM_VER+' < 11.5.1 - trusted publishing unavailable');process.exit(1);}
Evidence
The current logic converts each dot-separated segment with Number(...) and compares it; if a
segment becomes NaN, both > and < comparisons are false and the script falls through
successfully.

.github/workflows/release.yml[224-229]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
The version gate assumes dot-separated numeric components, but semver strings with suffixes (e.g. `11.5.1-alpha.0` or `11.5.1+build`) can produce `NaN` and bypass the intended comparison.

## Issue Context
This is a defensive check intended to guarantee trusted-publishing capability.

## Fix Focus Areas
- .github/workflows/release.yml[224-229]

## Suggested fix
- Parse the version with a strict regex and reject non-numeric major/minor/patch (and optionally reject any prerelease/build suffix), e.g. extract `major/minor/patch` via `match(/^([0-9]+)\.([0-9]+)\.([0-9]+)/)` and ensure all groups are present.
- Explicitly fail if any component is missing or non-numeric before doing the comparison.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


Grey Divider

Context used
✅ Compliance rules (platform): 2 rules
Review mode: ⚖️ Balanced: This adds a security-sensitive, behavior-affecting GitHub Actions release and npm OIDC publishing workflow; despite one file/hunk, its supply-chain and release blast radius warrants a complete single-pass review.

To customize comments, go to the Qodo configuration screen, or learn more in the docs.

Qodo Logo

qodo-code-review[bot]

This comment was marked as resolved.

@qodo-code-review

Copy link
Copy Markdown

Qodo Fixer

✅ Merged (0) · ☑ Fixed (0)

Process

  • No fixes were applied (no_fixes_applied)

cubic-dev-ai[bot]

This comment was marked as resolved.

…sert wave bin, fix version-floor NaN gap

- publish job: run npm ci --ignore-scripts (id-token: write job should not
  execute untrusted dependency lifecycle scripts) then npm run build before
  npm publish, so the tarball actually contains dist/ and matches what verify
  proved working (jobs do not share a workspace/filesystem)
- verify job: re-run the pinned+checksum-verified gitleaks over dist/ after
  build, since gate 1 only scans the pre-build source tree and never sees
  generated build output before it reaches npm
- e2e-smoke: require the packed bins to include a key literally named "wave",
  not just any bin
- ESM smoke test: assert the imported module actually has exports instead of
  a truthy-object check that can never fail
- npm version floor check: parse with a strict x.y.z regex so a
  non-numeric/prerelease npm --version cannot silently produce NaN
  comparisons that pass the >=11.5.1 gate
@yakimoto

yakimoto commented Aug 5, 2026

Copy link
Copy Markdown
Contributor Author

Reply to unresolved review thread re: .github/workflows/release.yml:111 ("Release workflow breaks without package.json"). This is correct and already known — see the KNOWN GAP block at the top of the file (lines 6-17) and the PR description: this repo has no package.json/src/ on main today; @wave-av/cli was hand-published from an uncommitted working copy. This workflow only triggers on a v* tag push (on.push.tags), so it is inert until source lands on main. Adding the path now (mirroring wave-av/adk's proven, already-published release.yml) is the point of this PR; weakening npm ci/removing the manifest read would defeat the workflow's purpose once source does land. Resolving as refuted-but-acknowledged.

devin-ai-integration[bot]

This comment was marked as resolved.

…ish jobs own build too

- both dist/ scans now pass --config .gitleaks.toml, matching gate 1 and the
  merge-path gitleaks invocation, instead of falling back to defaults with
  no allowlist/stopwords
- publish job (fresh checkout + its own npm run build) now also gets a
  gitleaks scan of its dist/ output before publish - that build was
  previously the one artifact in the whole workflow no secret-scan gate
  ever touched

@devin-ai-integration devin-ai-integration Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Devin Review found 1 new potential issue.

Open in Devin Review

Comment on lines +89 to +92
- name: content policy (WAVE trade-secret / internal-leak gate)
env:
GUARD_PRIVATE_REPOS: ${{ vars.GUARD_PRIVATE_REPOS }}
run: bash scripts/public-repo-guard/content-policy.sh .

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔍 content-policy gate is not re-run on build output

The release path re-runs gitleaks over dist/ in both verify and publish, but the WAVE content-policy script (private-repo references, developer absolute paths, account IDs) is only run once over the source tree in secret-scan. Note the script's own IGNORE list excludes **/dist/**, so pointing it at the build output would require passing dist as the root. If bundlers can inline absolute developer paths or internal repo names into dist/, this asymmetry leaves that class of leak unscanned.

Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant