Skip to content

Commit d8a05de

Browse files
Add staged OIDC publish workflow (#524)
* Add staged OIDC publish workflow Replace token-based npm publishing with OIDC trusted publishing + npm staged publishing: CI authenticates with a short-lived OIDC token (no stored npm token) and stages the release; a maintainer promotes it from the npm staging area with 2FA. The verify job asserts the Release tag matches package.json and refuses releases not reachable from the default branch. Listed in .fernignore so it is not overwritten by code generation. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * Pin stage-publish to the ancestry-checked SHA (close TOCTOU window) verify now outputs the validated $GITHUB_SHA; stage-publish checks out that exact SHA instead of re-resolving the mutable release tag, so the commit that is published is provably the one the branch-ancestry guard approved. Mirrors the hardening already in intercom-react-native. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * Bump actions/checkout to v7.0.0 Pin to 9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 (v7.0.0). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * Fix Node-floor comment and add verify timeout - Correct the setup-node comment: staged publishing needs Node >= 22.14.0, not >= 20 (addresses review feedback on a misleading comment). - Add timeout-minutes: 5 to the verify job as a hung-step backstop, matching the bound already on stage-publish. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent a6ec1f0 commit d8a05de

2 files changed

Lines changed: 93 additions & 1 deletion

File tree

‎.fernignore‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,4 +5,5 @@
55
LICENSE
66
REPO_OWNER
77
tests/integration
8-
.github/workflows/ci.yml
8+
.github/workflows/ci.yml
9+
.github/workflows/publish.yml

‎.github/workflows/publish.yml‎

Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
# Release workflow for intercom-client.
2+
#
3+
# Publishing model: OIDC trusted publishing + npm staged publishing. CI authenticates to
4+
# npm with a short-lived OIDC token (no stored npm token) and stages the release; a
5+
# maintainer then promotes it from the npm staging area with 2FA. Replaces token-based publishing.
6+
#
7+
# Listed in .fernignore so it is not overwritten by code generation.
8+
name: Publish (staged)
9+
10+
on:
11+
release:
12+
types: [published] # cutting a Release creates the tag AND fires this
13+
14+
permissions:
15+
contents: read # workflow default (least privilege); only stage-publish also needs id-token, granted on that job
16+
17+
concurrency:
18+
group: publish-${{ github.workflow }} # serialize publishes; no dist-tag races
19+
cancel-in-progress: false # queue, don't kill an in-flight publish
20+
21+
jobs:
22+
verify:
23+
runs-on: ubuntu-latest
24+
timeout-minutes: 5 # backstop a hung step (checkout/guards only; no install/build)
25+
outputs:
26+
sha: ${{ steps.resolve.outputs.sha }} # ancestry-checked commit, pinned for downstream
27+
steps:
28+
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
29+
with:
30+
persist-credentials: false
31+
fetch-depth: 0 # full history for the ancestry check below
32+
- uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
33+
with:
34+
node-version: '22' # staged publishing needs Node >= 22.14.0 (npm >= 11.15.0); repo has no .nvmrc
35+
package-manager-cache: false # release-triggered: disable auto-cache (zizmor cache-poisoning)
36+
- name: Assert Release tag matches package.json version
37+
env:
38+
RELEASE_TAG: ${{ github.event.release.tag_name }}
39+
run: |
40+
PKG="$(node -p "require('./package.json').version")"
41+
[ "${RELEASE_TAG#v}" = "$PKG" ] || { echo "tag $RELEASE_TAG != package.json v$PKG"; exit 1; }
42+
- name: Refuse releases not on the default branch
43+
id: resolve
44+
env:
45+
RELEASE_TAG: ${{ github.event.release.tag_name }}
46+
DEFAULT_BRANCH: ${{ github.event.repository.default_branch }}
47+
run: |
48+
git merge-base --is-ancestor "$GITHUB_SHA" "origin/$DEFAULT_BRANCH" \
49+
|| { echo "release $RELEASE_TAG not reachable from $DEFAULT_BRANCH — refusing"; exit 1; }
50+
echo "sha=$GITHUB_SHA" >> "$GITHUB_OUTPUT" # downstream checks out this exact SHA, not the mutable tag
51+
52+
stage-publish:
53+
needs: verify
54+
runs-on: ubuntu-latest
55+
timeout-minutes: 15 # cap a hung publish
56+
permissions:
57+
contents: read
58+
id-token: write # OIDC trusted publishing: only this job mints the token
59+
steps:
60+
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
61+
with:
62+
persist-credentials: false
63+
ref: ${{ needs.verify.outputs.sha }} # the ancestry-checked SHA, immune to tag re-pointing (TOCTOU)
64+
- uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
65+
with:
66+
node-version: '22'
67+
registry-url: 'https://registry.npmjs.org'
68+
package-manager-cache: false
69+
- run: corepack enable
70+
- run: pnpm install --frozen-lockfile
71+
- run: pnpm run build # mandatory: dist/ is gitignored, so the published artifact is built here
72+
- run: npm install -g npm@11.15.0 # npm CLI: staged publishing needs npm >= 11.15.0
73+
- name: Resolve dist-tag (a prerelease must never go to `latest`)
74+
id: disttag
75+
env:
76+
ALPHA_TAG: alpha
77+
BETA_TAG: beta
78+
PRERELEASE_TAG: next
79+
run: |
80+
VERSION="$(node -p "require('./package.json').version")"
81+
case "$VERSION" in
82+
*-alpha.*) TAG="$ALPHA_TAG" ;;
83+
*-beta.*) TAG="$BETA_TAG" ;;
84+
*-*) TAG="$PRERELEASE_TAG" ;;
85+
*) TAG="latest" ;;
86+
esac
87+
echo "tag=$TAG" >> "$GITHUB_OUTPUT"
88+
- name: Stage publish
89+
env:
90+
DIST_TAG: ${{ steps.disttag.outputs.tag }}
91+
run: npm stage publish --tag "$DIST_TAG"

0 commit comments

Comments
 (0)