Skip to content

Commit 22a6a1b

Browse files
jasonprafulclaude
andauthored
chore: migrate npm release from CircleCI to GitHub Actions (#360)
* chore: migrate npm release from CircleCI to GitHub Actions Use OIDC trusted publishing for npm releases instead of token-based auth. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * chore: harden release workflow (OIDC staged publish, SHA pins, least-privilege) Pull in security hardening from #447: SHA-pinned actions, env-var indirection for release tag (script-injection fix), top-level least-privilege permissions, persist-credentials: false, disabled package-manager cache, concurrency + timeout, default-branch ancestry gate, dist-tag resolution, and npm staged publishing via OIDC trusted publishing. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * chore: keep m4pro.medium runner, restore Socket Firewall check, trim workflow comments - Revert CircleCI resource_class back to m4pro.medium (M4) - Restore Socket Firewall registry verification in install-dependencies - Remove redundant inline comments from release.yml (keep SHA version pins) Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * chore: drop workflow_dispatch trigger from release workflow Release is now triggered only by publishing a GitHub Release; remove the manual dispatch input and its now-dead github.event.inputs.tag fallbacks. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix: pin test/publish to the SHA validated in validate (tag-mutability TOCTOU) validate emits the resolved, ancestry-checked SHA; test and publish check out that SHA instead of re-resolving the mutable tag by name, closing the window where a force-updated tag could ship a malicious build with provenance. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
1 parent cc75ad2 commit 22a6a1b

2 files changed

Lines changed: 144 additions & 51 deletions

File tree

.circleci/config.yml

Lines changed: 1 addition & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -29,14 +29,6 @@ run_always: &run_always
2929
tags:
3030
only: /.*/
3131

32-
# Only run on release
33-
run_on_release: &run_on_release
34-
filters:
35-
tags:
36-
only: /.*/
37-
branches:
38-
ignore: /.*/
39-
4032
commands:
4133
attach_project:
4234
steps:
@@ -58,6 +50,7 @@ jobs:
5850
REGISTRY=$(npm config get registry)
5951
echo "npm registry: $REGISTRY"
6052
echo "$REGISTRY" | grep -q socket-firewall-registry || { echo "FAIL: npm not routed through Socket Firewall"; exit 1; }
53+
6154
- run:
6255
name: Install dependencies
6356
command: |
@@ -210,35 +203,6 @@ jobs:
210203
path: ~/.maestro/tests
211204
destination: maestro-tests
212205

213-
release-to-npm:
214-
executor: default
215-
steps:
216-
- checkout
217-
- run:
218-
name: Verify Socket Firewall registry is active
219-
command: |
220-
REGISTRY=$(npm config get registry)
221-
echo "npm registry: $REGISTRY"
222-
echo "$REGISTRY" | grep -q socket-firewall-registry || { echo "FAIL: npm not routed through Socket Firewall"; exit 1; }
223-
- run:
224-
name: Add npm registry auth key
225-
command: |
226-
echo "//registry.npmjs.org/:_authToken=$NPM_TOKEN" > ~/project/.npmrc
227-
npm config set scope $ORG_NAME
228-
229-
- restore_cache:
230-
keys:
231-
- dependencies-{{ checksum "package.json" }}
232-
233-
- run:
234-
name: Install dependencies
235-
command: |
236-
yarn install
237-
238-
- run:
239-
name: Publish the package
240-
command: npm publish
241-
242206
workflows:
243207
version: 2.1
244208
build-and-test:
@@ -278,17 +242,3 @@ workflows:
278242
# - typescript
279243
# - unit-tests
280244
# - build-package
281-
282-
- release-to-npm:
283-
<<: *run_on_release
284-
context:
285-
- react-native-context
286-
requires:
287-
- install-dependencies
288-
- lint
289-
- typescript
290-
- unit-tests
291-
- build-package
292-
# Temporarily removed e2e test dependencies
293-
# - ios-e2e-test
294-
# - android-e2e-test

.github/workflows/release.yml

Lines changed: 143 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,143 @@
1+
name: Release to npm
2+
3+
on:
4+
release:
5+
types: [published]
6+
7+
permissions:
8+
contents: read
9+
10+
concurrency:
11+
group: release-${{ github.workflow }}
12+
cancel-in-progress: false
13+
14+
jobs:
15+
validate:
16+
name: Validate Release
17+
runs-on: ubuntu-latest
18+
outputs:
19+
version: ${{ steps.version.outputs.version }}
20+
sha: ${{ steps.resolve.outputs.sha }}
21+
steps:
22+
- name: Checkout
23+
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
24+
with:
25+
ref: ${{ github.event.release.tag_name }}
26+
persist-credentials: false
27+
fetch-depth: 0
28+
29+
- name: Extract and validate version
30+
id: version
31+
env:
32+
RELEASE_TAG: ${{ github.event.release.tag_name }}
33+
run: |
34+
VERSION="${RELEASE_TAG#v}"
35+
36+
if ! echo "$VERSION" | grep -qE '^[0-9]+\.[0-9]+\.[0-9]+(-[a-zA-Z0-9.]+)?$'; then
37+
echo "Error: Invalid version format: $VERSION"
38+
exit 1
39+
fi
40+
41+
PKG_VERSION=$(node -p "require('./package.json').version")
42+
if [ "$VERSION" != "$PKG_VERSION" ]; then
43+
echo "Error: Tag version ($VERSION) does not match package.json version ($PKG_VERSION)"
44+
exit 1
45+
fi
46+
47+
echo "version=$VERSION" >> "$GITHUB_OUTPUT"
48+
49+
- name: Refuse releases not on the default branch, pin SHA
50+
id: resolve
51+
env:
52+
DEFAULT_BRANCH: ${{ github.event.repository.default_branch }}
53+
run: |
54+
HEAD_SHA="$(git rev-parse HEAD)"
55+
git merge-base --is-ancestor "$HEAD_SHA" "origin/$DEFAULT_BRANCH" \
56+
|| { echo "release commit $HEAD_SHA not reachable from $DEFAULT_BRANCH — refusing"; exit 1; }
57+
echo "sha=$HEAD_SHA" >> "$GITHUB_OUTPUT"
58+
59+
test:
60+
name: Test
61+
runs-on: ubuntu-latest
62+
needs: validate
63+
steps:
64+
- name: Checkout
65+
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
66+
with:
67+
ref: ${{ needs.validate.outputs.sha }}
68+
persist-credentials: false
69+
70+
- name: Setup Node.js
71+
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
72+
with:
73+
node-version-file: '.nvmrc'
74+
package-manager-cache: false
75+
76+
- name: Enable Corepack
77+
run: corepack enable
78+
79+
- name: Install dependencies
80+
run: yarn install --immutable
81+
82+
- name: Lint
83+
run: yarn lint
84+
85+
- name: TypeScript
86+
run: yarn typescript
87+
88+
- name: Unit tests
89+
run: yarn test
90+
91+
- name: Build package
92+
run: yarn prepare
93+
94+
publish:
95+
name: Publish to npm (staged)
96+
runs-on: ubuntu-latest
97+
needs: [validate, test]
98+
timeout-minutes: 15
99+
permissions:
100+
contents: read
101+
id-token: write
102+
steps:
103+
- name: Checkout
104+
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
105+
with:
106+
ref: ${{ needs.validate.outputs.sha }}
107+
persist-credentials: false
108+
109+
- name: Setup Node.js
110+
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
111+
with:
112+
node-version-file: '.nvmrc'
113+
registry-url: 'https://registry.npmjs.org'
114+
package-manager-cache: false
115+
116+
- name: Enable Corepack
117+
run: corepack enable
118+
119+
- name: Install dependencies
120+
run: yarn install --immutable
121+
122+
- name: Build package
123+
run: yarn prepare
124+
125+
- name: Upgrade npm
126+
run: npm install -g npm@11.15.0
127+
128+
- name: Resolve dist-tag (a prerelease must never go to `latest`)
129+
id: disttag
130+
env:
131+
PRERELEASE_TAG: beta
132+
run: |
133+
VERSION="$(node -p "require('./package.json').version")"
134+
case "$VERSION" in
135+
*-*) TAG="$PRERELEASE_TAG" ;;
136+
*) TAG="latest" ;;
137+
esac
138+
echo "tag=$TAG" >> "$GITHUB_OUTPUT"
139+
140+
- name: Stage publish
141+
env:
142+
DIST_TAG: ${{ steps.disttag.outputs.tag }}
143+
run: npm stage publish --tag "$DIST_TAG"

0 commit comments

Comments
 (0)