From f0bacce84737876498cc5e20f68fea9edc4bab86 Mon Sep 17 00:00:00 2001 From: Jason Praful Date: Thu, 22 Jan 2026 10:16:15 +0000 Subject: [PATCH 1/5] 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 --- .circleci/config.yml | 47 +-------------- .github/workflows/release.yml | 108 ++++++++++++++++++++++++++++++++++ 2 files changed, 109 insertions(+), 46 deletions(-) create mode 100644 .github/workflows/release.yml diff --git a/.circleci/config.yml b/.circleci/config.yml index 5271235e..0a254ed0 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -29,14 +29,6 @@ run_always: &run_always tags: only: /.*/ -# Only run on release -run_on_release: &run_on_release - filters: - tags: - only: /.*/ - branches: - ignore: /.*/ - commands: attach_project: steps: @@ -204,29 +196,6 @@ jobs: path: ~/.maestro/tests destination: maestro-tests - release-to-npm: - executor: default - steps: - - checkout - - run: - name: Add npm registry auth key - command: | - echo "//registry.npmjs.org/:_authToken=$NPM_TOKEN" > ~/project/.npmrc - npm config set scope $ORG_NAME - - - restore_cache: - keys: - - dependencies-{{ checksum "package.json" }} - - - run: - name: Install dependencies - command: | - yarn install - - - run: - name: Publish the package - command: npm publish - workflows: version: 2.1 build-and-test: @@ -265,18 +234,4 @@ workflows: # - lint # - typescript # - unit-tests - # - build-package - - - release-to-npm: - <<: *run_on_release - context: - - react-native-context - requires: - - install-dependencies - - lint - - typescript - - unit-tests - - build-package - # Temporarily removed e2e test dependencies - # - ios-e2e-test - # - android-e2e-test + # - build-package \ No newline at end of file diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 00000000..9cd922ad --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,108 @@ +name: Release to npm + +on: + release: + types: [published] + workflow_dispatch: + inputs: + tag: + description: 'Tag to publish (e.g., 9.5.0)' + required: true + type: string + +jobs: + validate: + name: Validate Release + runs-on: ubuntu-latest + outputs: + version: ${{ steps.version.outputs.version }} + steps: + - name: Checkout + uses: actions/checkout@v4 + with: + ref: ${{ github.event.release.tag_name || github.event.inputs.tag }} + + - name: Extract and validate version + id: version + run: | + TAG="${{ github.event.release.tag_name || github.event.inputs.tag }}" + VERSION="${TAG#v}" + + if ! echo "$VERSION" | grep -qE '^[0-9]+\.[0-9]+\.[0-9]+(-[a-zA-Z0-9.]+)?$'; then + echo "Error: Invalid version format: $VERSION" + exit 1 + fi + + PKG_VERSION=$(node -p "require('./package.json').version") + if [ "$VERSION" != "$PKG_VERSION" ]; then + echo "Error: Tag version ($VERSION) does not match package.json version ($PKG_VERSION)" + exit 1 + fi + + echo "version=$VERSION" >> $GITHUB_OUTPUT + + test: + name: Test + runs-on: ubuntu-latest + needs: validate + steps: + - name: Checkout + uses: actions/checkout@v4 + with: + ref: ${{ github.event.release.tag_name || github.event.inputs.tag }} + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: '22.14.0' + cache: 'yarn' + + - name: Enable Corepack + run: corepack enable + + - name: Install dependencies + run: yarn install --immutable + + - name: Lint + run: yarn lint + + - name: TypeScript + run: yarn typescript + + - name: Unit tests + run: yarn test + + - name: Build package + run: yarn prepare + + publish: + name: Publish to npm + runs-on: ubuntu-latest + needs: [validate, test] + permissions: + contents: read + id-token: write + steps: + - name: Checkout + uses: actions/checkout@v4 + with: + ref: ${{ github.event.release.tag_name || github.event.inputs.tag }} + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: '22.14.0' + cache: 'yarn' + registry-url: 'https://registry.npmjs.org' + + - name: Enable Corepack + run: corepack enable + + - name: Install dependencies + run: yarn install --immutable + + - name: Build package + run: yarn prepare + + - name: Publish to npm + run: npm publish --access public From 8e96460c6b64574ba3fc6b8b209acc9b8a8201b5 Mon Sep 17 00:00:00 2001 From: Jason Praful Date: Mon, 15 Jun 2026 17:36:24 +0100 Subject: [PATCH 2/5] 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) --- .github/workflows/release.yml | 72 ++++++++++++++++++++++++++--------- 1 file changed, 55 insertions(+), 17 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 9cd922ad..68c1107f 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -2,7 +2,7 @@ name: Release to npm on: release: - types: [published] + types: [published] # cutting a Release creates the tag AND fires this workflow_dispatch: inputs: tag: @@ -10,6 +10,13 @@ on: required: true type: string +permissions: + contents: read # workflow default (least privilege); only the publish job also needs id-token, granted on that job + +concurrency: + group: release-${{ github.workflow }} # serialize publishes; no dist-tag races + cancel-in-progress: false # queue, don't kill an in-flight publish + jobs: validate: name: Validate Release @@ -18,15 +25,18 @@ jobs: version: ${{ steps.version.outputs.version }} steps: - name: Checkout - uses: actions/checkout@v4 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: ref: ${{ github.event.release.tag_name || github.event.inputs.tag }} + persist-credentials: false + fetch-depth: 0 # full history for the ancestry check below - name: Extract and validate version id: version + env: + RELEASE_TAG: ${{ github.event.release.tag_name || github.event.inputs.tag }} run: | - TAG="${{ github.event.release.tag_name || github.event.inputs.tag }}" - VERSION="${TAG#v}" + VERSION="${RELEASE_TAG#v}" if ! echo "$VERSION" | grep -qE '^[0-9]+\.[0-9]+\.[0-9]+(-[a-zA-Z0-9.]+)?$'; then echo "Error: Invalid version format: $VERSION" @@ -39,7 +49,15 @@ jobs: exit 1 fi - echo "version=$VERSION" >> $GITHUB_OUTPUT + echo "version=$VERSION" >> "$GITHUB_OUTPUT" + + - name: Refuse releases not on the default branch + env: + DEFAULT_BRANCH: ${{ github.event.repository.default_branch }} + run: | + HEAD_SHA="$(git rev-parse HEAD)" + git merge-base --is-ancestor "$HEAD_SHA" "origin/$DEFAULT_BRANCH" \ + || { echo "release commit $HEAD_SHA not reachable from $DEFAULT_BRANCH — refusing"; exit 1; } test: name: Test @@ -47,15 +65,16 @@ jobs: needs: validate steps: - name: Checkout - uses: actions/checkout@v4 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: ref: ${{ github.event.release.tag_name || github.event.inputs.tag }} + persist-credentials: false - name: Setup Node.js - uses: actions/setup-node@v4 + uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0 with: - node-version: '22.14.0' - cache: 'yarn' + node-version-file: '.nvmrc' # pin >= 22.14.0 + package-manager-cache: false # release-triggered: disable auto-cache (zizmor cache-poisoning) - name: Enable Corepack run: corepack enable @@ -76,24 +95,26 @@ jobs: run: yarn prepare publish: - name: Publish to npm + name: Publish to npm (staged) runs-on: ubuntu-latest needs: [validate, test] + timeout-minutes: 15 # cap a hung publish permissions: contents: read - id-token: write + id-token: write # OIDC trusted publishing: only this job mints the token steps: - name: Checkout - uses: actions/checkout@v4 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: ref: ${{ github.event.release.tag_name || github.event.inputs.tag }} + persist-credentials: false - name: Setup Node.js - uses: actions/setup-node@v4 + uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0 with: - node-version: '22.14.0' - cache: 'yarn' + node-version-file: '.nvmrc' registry-url: 'https://registry.npmjs.org' + package-manager-cache: false - name: Enable Corepack run: corepack enable @@ -104,5 +125,22 @@ jobs: - name: Build package run: yarn prepare - - name: Publish to npm - run: npm publish --access public + - name: Upgrade npm + run: npm install -g npm@11.15.0 # npm CLI: staged publishing + OIDC needs npm >= 11.15.0 + + - name: Resolve dist-tag (a prerelease must never go to `latest`) + id: disttag + env: + PRERELEASE_TAG: beta + run: | + VERSION="$(node -p "require('./package.json').version")" + case "$VERSION" in + *-*) TAG="$PRERELEASE_TAG" ;; + *) TAG="latest" ;; + esac + echo "tag=$TAG" >> "$GITHUB_OUTPUT" + + - name: Stage publish + env: + DIST_TAG: ${{ steps.disttag.outputs.tag }} + run: npm stage publish --tag "$DIST_TAG" # queues the release for a maintainer to approve in the npm UI From f61f632b03db43a95d23e9dfffcbf7631bd42986 Mon Sep 17 00:00:00 2001 From: Jason Praful Date: Mon, 15 Jun 2026 17:42:12 +0100 Subject: [PATCH 3/5] 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) --- .circleci/config.yml | 11 +++++++++-- .github/workflows/release.yml | 22 +++++++++++----------- 2 files changed, 20 insertions(+), 13 deletions(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index 0a254ed0..68193306 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -4,7 +4,7 @@ version: 2.1 macos_config: &macos_config macos: xcode: 16.0.0 - resource_class: macos.m1.large.gen1 + resource_class: m4pro.medium shell: /bin/bash --login -eo pipefail setup_env_file: &setup_env_file @@ -44,6 +44,13 @@ jobs: - node/install: node-version: 22.14.0 + - run: + name: Verify Socket Firewall registry is active + command: | + REGISTRY=$(npm config get registry) + echo "npm registry: $REGISTRY" + echo "$REGISTRY" | grep -q socket-firewall-registry || { echo "FAIL: npm not routed through Socket Firewall"; exit 1; } + - run: name: Install dependencies command: | @@ -234,4 +241,4 @@ workflows: # - lint # - typescript # - unit-tests - # - build-package \ No newline at end of file + # - build-package diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 68c1107f..2831c2cd 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -2,7 +2,7 @@ name: Release to npm on: release: - types: [published] # cutting a Release creates the tag AND fires this + types: [published] workflow_dispatch: inputs: tag: @@ -11,11 +11,11 @@ on: type: string permissions: - contents: read # workflow default (least privilege); only the publish job also needs id-token, granted on that job + contents: read concurrency: - group: release-${{ github.workflow }} # serialize publishes; no dist-tag races - cancel-in-progress: false # queue, don't kill an in-flight publish + group: release-${{ github.workflow }} + cancel-in-progress: false jobs: validate: @@ -29,7 +29,7 @@ jobs: with: ref: ${{ github.event.release.tag_name || github.event.inputs.tag }} persist-credentials: false - fetch-depth: 0 # full history for the ancestry check below + fetch-depth: 0 - name: Extract and validate version id: version @@ -73,8 +73,8 @@ jobs: - name: Setup Node.js uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0 with: - node-version-file: '.nvmrc' # pin >= 22.14.0 - package-manager-cache: false # release-triggered: disable auto-cache (zizmor cache-poisoning) + node-version-file: '.nvmrc' + package-manager-cache: false - name: Enable Corepack run: corepack enable @@ -98,10 +98,10 @@ jobs: name: Publish to npm (staged) runs-on: ubuntu-latest needs: [validate, test] - timeout-minutes: 15 # cap a hung publish + timeout-minutes: 15 permissions: contents: read - id-token: write # OIDC trusted publishing: only this job mints the token + id-token: write steps: - name: Checkout uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 @@ -126,7 +126,7 @@ jobs: run: yarn prepare - name: Upgrade npm - run: npm install -g npm@11.15.0 # npm CLI: staged publishing + OIDC needs npm >= 11.15.0 + run: npm install -g npm@11.15.0 - name: Resolve dist-tag (a prerelease must never go to `latest`) id: disttag @@ -143,4 +143,4 @@ jobs: - name: Stage publish env: DIST_TAG: ${{ steps.disttag.outputs.tag }} - run: npm stage publish --tag "$DIST_TAG" # queues the release for a maintainer to approve in the npm UI + run: npm stage publish --tag "$DIST_TAG" From 7637280cd5866bcf5b86285c85775d63f40bf43c Mon Sep 17 00:00:00 2001 From: Jason Praful Date: Mon, 15 Jun 2026 17:49:45 +0100 Subject: [PATCH 4/5] 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) --- .github/workflows/release.yml | 14 ++++---------- 1 file changed, 4 insertions(+), 10 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 2831c2cd..95f2cdee 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -3,12 +3,6 @@ name: Release to npm on: release: types: [published] - workflow_dispatch: - inputs: - tag: - description: 'Tag to publish (e.g., 9.5.0)' - required: true - type: string permissions: contents: read @@ -27,14 +21,14 @@ jobs: - name: Checkout uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: - ref: ${{ github.event.release.tag_name || github.event.inputs.tag }} + ref: ${{ github.event.release.tag_name }} persist-credentials: false fetch-depth: 0 - name: Extract and validate version id: version env: - RELEASE_TAG: ${{ github.event.release.tag_name || github.event.inputs.tag }} + RELEASE_TAG: ${{ github.event.release.tag_name }} run: | VERSION="${RELEASE_TAG#v}" @@ -67,7 +61,7 @@ jobs: - name: Checkout uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: - ref: ${{ github.event.release.tag_name || github.event.inputs.tag }} + ref: ${{ github.event.release.tag_name }} persist-credentials: false - name: Setup Node.js @@ -106,7 +100,7 @@ jobs: - name: Checkout uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: - ref: ${{ github.event.release.tag_name || github.event.inputs.tag }} + ref: ${{ github.event.release.tag_name }} persist-credentials: false - name: Setup Node.js From 6d565efa0feb7e1fe6e0bc7c38e7c183a55226c1 Mon Sep 17 00:00:00 2001 From: Jason Praful Date: Mon, 15 Jun 2026 17:55:05 +0100 Subject: [PATCH 5/5] 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) --- .github/workflows/release.yml | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 95f2cdee..af49bfbc 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -17,6 +17,7 @@ jobs: runs-on: ubuntu-latest outputs: version: ${{ steps.version.outputs.version }} + sha: ${{ steps.resolve.outputs.sha }} steps: - name: Checkout uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 @@ -45,13 +46,15 @@ jobs: echo "version=$VERSION" >> "$GITHUB_OUTPUT" - - name: Refuse releases not on the default branch + - name: Refuse releases not on the default branch, pin SHA + id: resolve env: DEFAULT_BRANCH: ${{ github.event.repository.default_branch }} run: | HEAD_SHA="$(git rev-parse HEAD)" git merge-base --is-ancestor "$HEAD_SHA" "origin/$DEFAULT_BRANCH" \ || { echo "release commit $HEAD_SHA not reachable from $DEFAULT_BRANCH — refusing"; exit 1; } + echo "sha=$HEAD_SHA" >> "$GITHUB_OUTPUT" test: name: Test @@ -61,7 +64,7 @@ jobs: - name: Checkout uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: - ref: ${{ github.event.release.tag_name }} + ref: ${{ needs.validate.outputs.sha }} persist-credentials: false - name: Setup Node.js @@ -100,7 +103,7 @@ jobs: - name: Checkout uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: - ref: ${{ github.event.release.tag_name }} + ref: ${{ needs.validate.outputs.sha }} persist-credentials: false - name: Setup Node.js