Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/hip-memes-rhyme.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@zemd/gha": patch
---

Use token-free OIDC staging by default, automatically direct-publish only first releases with an optional npm token, preserve submitted versions as immutable releases even when npm approval is rejected, and advance the private shared-workflow contract version in release pull requests.
400 changes: 318 additions & 82 deletions .github/scripts/gha.mjs

Large diffs are not rendered by default.

35 changes: 30 additions & 5 deletions .github/workflows-examples/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ Copy-paste callers for the reusable workflows published from this repository.
| File | Calls | Purpose |
| :----------------------------------- | :--------------------- | :------------------------------------------------------------------------- |
| [`ci.yml`](./ci.yml) | `shared-ci.yml` | Lint, format, typecheck, build, test matrix, Playwright, dependency review |
| [`release.yml`](./release.yml) | `shared-release.yml` | Release pull request, npm publish, git tags, GitHub release |
| [`release.yml`](./release.yml) | `shared-release.yml` | Release pull request, npm submission, git tags, GitHub release |
| [`codeql.yml`](./codeql.yml) | `shared-codeql.yml` | CodeQL analysis |
| [`scorecard.yml`](./scorecard.yml) | `shared-scorecard.yml` | OpenSSF Scorecard |
| [`zizmor.yml`](./zizmor.yml) | `shared-zizmor.yml` | Blocking security lint for GitHub Actions and Dependabot |
Expand Down Expand Up @@ -38,21 +38,46 @@ Dependabot rewrites both the SHA and the trailing `# v1` comment from then on.
When it updates `release.yml`, keep `shared-tooling-ref` equal to the SHA in the
`uses:` line so the release scripts and reusable workflow stay on one revision.

`contract-version-package` is empty by default. Set it to a private package's
manifest only when that package versions a release contract but is never
published to npm. The release workflow advances it from its matching change
intents before pnpm prepares the release pull request.

## Release setup

`shared-release.yml` expects [`pnpm change`](https://pnpm.io) intents on `main`.
On every push it either opens/refreshes a `release/main` pull request, or — when
no intents are pending — publishes, tags and creates a combined GitHub release.
no intents are pending — stages packages on npm, tags them and creates a combined
GitHub release. A maintainer must then review and approve each staged package
with 2FA before it becomes available from npm. If any publishable workspace
package does not exist in the registry, the workflow publishes that package
regularly so it can be created while still staging updates to existing packages.

Submission is the immutable release boundary. The workflow tags both directly
published and staged package versions immediately. Approval only controls npm
availability: rejecting a staged package does not roll back its release or let a
later run reuse that version. Record a new change intent so the next attempt uses
the next version.

[Staged publishing](https://docs.npmjs.com/staged-publishing/) is the default.
Set `staged-publishing: false` in the caller's `with:` block when packages must
always publish immediately. npm cannot stage a package that does not exist yet,
so first-release detection overrides the staged default for that package.

For npm **trusted publishing**:
For npm [**trusted publishing**](https://docs.npmjs.com/trusted-publishers/):

- Keep the caller named `release.yml`. npm validates the calling workflow's
filename, not the reusable workflow that runs the publish.
- Register the trusted publisher per package with the _consumer_ repository and
`release.yml`.
- Configure each existing package's trusted publisher to allow only
`npm stage publish` for the default behavior. Consumers that disable staged
publishing must allow `npm publish` instead (or allow both actions).
- `id-token: write` must be granted by the caller job, which the example does.
- Keep `NPM_TOKEN` until every package exists on npm; a trusted publisher cannot
be configured for a package that was never published.
- Pass `NPM_TOKEN` as the optional reusable-workflow secret until every package
exists on npm. It is exposed only to regular publishing and is required when
first-release detection adds the package-creation step. After the first release,
configure that package's stage-only trusted publisher.
- `repository.url` in each `package.json` must match the repository exactly.

## Repository settings
Expand Down
8 changes: 5 additions & 3 deletions .github/workflows-examples/release.yml
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
# Keep this file named `release.yml`: npm trusted publishing validates the
# *calling* workflow filename, not the reusable workflow that runs `npm publish`.
# *calling* workflow filename, not the reusable workflow that submits packages.
name: Release

permissions: {}
Expand Down Expand Up @@ -31,10 +31,12 @@ jobs:
# base-branch: main
# release-branch: release/main
# release-title: "chore(release): version packages"
# contract-version-package: "" # Private workflow/tooling contract, if any.
# build-script: build
# publint-script: lint-publish
# staged-publishing: true # Set false when every release should publish directly.
# registry-url: "https://registry.npmjs.org"
secrets:
# Only needed until every package exists on npm: a trusted publisher
# cannot be configured for a package that has never been published.
# Optional after every package exists. First releases automatically use
# regular publishing and require this repository secret.
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
1 change: 1 addition & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ jobs:
shared-version:
name: Shared workflow contract
runs-on: ubuntu-latest
timeout-minutes: 10
permissions:
contents: read

Expand Down
3 changes: 3 additions & 0 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,8 @@ jobs:
with:
shared-tooling-repository: ${{ github.repository }}
shared-tooling-ref: ${{ github.sha }}
# pnpm does not advance this private, unpublished contract package.
contract-version-package: internal/gha/package.json
secrets:
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}

Expand All @@ -34,6 +36,7 @@ jobs:
needs: release
if: needs.release.outputs.pending == 'false'
runs-on: ubuntu-latest
timeout-minutes: 10
permissions:
contents: write # publish the shared workflow tags and GitHub release

Expand Down
4 changes: 4 additions & 0 deletions .github/workflows/shared-ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,7 @@ jobs:
quality:
name: Lint & Format
runs-on: ubuntu-latest
timeout-minutes: 10
permissions:
contents: read

Expand Down Expand Up @@ -124,6 +125,7 @@ jobs:
test:
name: "Build & Test: ${{ matrix.os }}"
runs-on: ${{ matrix.os }}
timeout-minutes: 15
permissions:
contents: read

Expand Down Expand Up @@ -180,6 +182,7 @@ jobs:
name: Browser Tests
if: inputs.browser-test-script != ''
runs-on: ubuntu-latest
timeout-minutes: 15
permissions:
contents: read

Expand Down Expand Up @@ -235,6 +238,7 @@ jobs:
name: Dependency Review
if: inputs.dependency-review && github.event_name == 'pull_request'
runs-on: ubuntu-latest
timeout-minutes: 10
permissions:
contents: read

Expand Down
1 change: 1 addition & 0 deletions .github/workflows/shared-codeql.yml
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ jobs:
analyze:
name: "Analyze: ${{ matrix.language }}"
runs-on: ubuntu-latest
timeout-minutes: 15
permissions:
contents: read # checkout the caller repository for analysis
actions: read # let CodeQL read workflow-run metadata
Expand Down
Loading