From e0bd09d44f6e82b39c9dbc81d1c7e9aaef88501b Mon Sep 17 00:00:00 2001 From: Alexei Diakonov Date: Thu, 23 Jul 2026 03:22:42 +0300 Subject: [PATCH 1/3] ci: separate release workflows for npm + PyPI (OIDC trusted publishing) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two independent workflows so each package versions/releases on its own and each OIDC trusted publisher is least-privilege (npm can't publish PyPI and vice-versa): - release-npm.yml → tag typescript-v* → @hashlock-tech/sdk (npm, with provenance) - release-pypi.yml → tag python-v* → hashlock-sdk (PyPI) No long-lived tokens in the repo; token fallback documented inline. Co-Authored-By: Claude Opus 4.8 (1M context) --- .github/workflows/release-npm.yml | 33 ++++++++++++++++++++++++++++++ .github/workflows/release-pypi.yml | 31 ++++++++++++++++++++++++++++ 2 files changed, 64 insertions(+) create mode 100644 .github/workflows/release-npm.yml create mode 100644 .github/workflows/release-pypi.yml diff --git a/.github/workflows/release-npm.yml b/.github/workflows/release-npm.yml new file mode 100644 index 0000000..11140c1 --- /dev/null +++ b/.github/workflows/release-npm.yml @@ -0,0 +1,33 @@ +name: Release npm + +# Publish @hashlock-tech/sdk. Trigger: a package-scoped tag, e.g. `typescript-v1.0.0`. +# git tag typescript-v1.0.0 && git push origin typescript-v1.0.0 +# Uses OIDC Trusted Publishing (no long-lived token in the repo). Configure the npm trusted +# publisher once against THIS workflow file (release-npm.yml) — see the setup instructions. +on: + push: + tags: ['typescript-v*'] + +permissions: + contents: read + id-token: write # OIDC → npm trusted publishing + provenance + +jobs: + publish: + name: Publish to npm + runs-on: ubuntu-latest + defaults: + run: + working-directory: typescript + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 + with: + node-version: 22 + registry-url: https://registry.npmjs.org + - run: npm install -g npm@latest # npm >= 11.5.1 supports OIDC trusted publishing + - run: npm install + - run: npm run build + - run: npm publish --provenance --access public + # Trusted publisher configured → no NODE_AUTH_TOKEN needed. + # Token fallback: set env NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} and drop --provenance. diff --git a/.github/workflows/release-pypi.yml b/.github/workflows/release-pypi.yml new file mode 100644 index 0000000..b9f478b --- /dev/null +++ b/.github/workflows/release-pypi.yml @@ -0,0 +1,31 @@ +name: Release PyPI + +# Publish hashlock-sdk. Trigger: a package-scoped tag, e.g. `python-v1.0.0`. +# git tag python-v1.0.0 && git push origin python-v1.0.0 +# Uses OIDC Trusted Publishing (no long-lived token in the repo). Configure the PyPI trusted +# publisher once against THIS workflow file (release-pypi.yml) — see the setup instructions. +on: + push: + tags: ['python-v*'] + +permissions: + contents: read + id-token: write # OIDC → PyPI trusted publishing + +jobs: + publish: + name: Publish to PyPI + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-python@v5 + with: + python-version: '3.11' + - run: pip install build + - run: python -m build + working-directory: python + - uses: pypa/gh-action-pypi-publish@release/v1 + with: + packages-dir: python/dist + # Trusted publisher configured → no token needed. + # Token fallback: add `password: ${{ secrets.PYPI_TOKEN }}`. From 65f2b23a110b7aa4b14ed6f0cc678c9b20616317 Mon Sep 17 00:00:00 2001 From: Alexei Diakonov Date: Thu, 23 Jul 2026 15:52:16 +0300 Subject: [PATCH 2/3] ci(security): harden release workflows per review MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Address the CodeRabbit findings on the release workflows (all verified still-valid): - Split each into a non-privileged build job (npm install/build, python -m build) and a minimal publish job that alone holds id-token: write — the OIDC job never runs untrusted install/build scripts (it publishes only the uploaded artifact). - Guard version drift: fail before publish if the tag (minus the typescript-/python- prefix) != the manifest version (package.json / pyproject.toml). - persist-credentials: false on all checkouts (no leftover GITHUB_TOKEN in git config). - Pin every action to a 40-char commit SHA (checkout, setup-node, setup-python, upload/download-artifact, gh-action-pypi-publish), keeping the audited versions. Co-Authored-By: Claude Opus 4.8 (1M context) --- .github/workflows/release-npm.yml | 50 ++++++++++++++++++++++++------ .github/workflows/release-pypi.yml | 45 +++++++++++++++++++++------ 2 files changed, 76 insertions(+), 19 deletions(-) diff --git a/.github/workflows/release-npm.yml b/.github/workflows/release-npm.yml index 11140c1..9868a3d 100644 --- a/.github/workflows/release-npm.yml +++ b/.github/workflows/release-npm.yml @@ -2,32 +2,62 @@ name: Release npm # Publish @hashlock-tech/sdk. Trigger: a package-scoped tag, e.g. `typescript-v1.0.0`. # git tag typescript-v1.0.0 && git push origin typescript-v1.0.0 -# Uses OIDC Trusted Publishing (no long-lived token in the repo). Configure the npm trusted -# publisher once against THIS workflow file (release-npm.yml) — see the setup instructions. +# Build runs WITHOUT OIDC; only the minimal `publish` job holds `id-token: write` and never runs +# untrusted install/build scripts. Uses OIDC Trusted Publishing (no long-lived token in the repo) — +# configure the npm trusted publisher once against THIS workflow file (release-npm.yml). on: push: tags: ['typescript-v*'] permissions: contents: read - id-token: write # OIDC → npm trusted publishing + provenance jobs: - publish: - name: Publish to npm + build: + name: Build (no OIDC) runs-on: ubuntu-latest defaults: run: working-directory: typescript steps: - - uses: actions/checkout@v4 - - uses: actions/setup-node@v4 + - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4 + with: + persist-credentials: false + - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4 with: node-version: 22 - registry-url: https://registry.npmjs.org - - run: npm install -g npm@latest # npm >= 11.5.1 supports OIDC trusted publishing + - name: Verify tag matches package.json version + run: | + TAG="${GITHUB_REF_NAME#typescript-v}" + PKG="$(node -p "require('./package.json').version")" + echo "tag=$TAG package.json=$PKG" + [ "$TAG" = "$PKG" ] || { echo "::error::tag $GITHUB_REF_NAME does not match package.json version $PKG"; exit 1; } - run: npm install - run: npm run build - - run: npm publish --provenance --access public + - run: npm pack # → typescript/*.tgz (built artifact to publish) + - uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4 + with: + name: npm-package + path: typescript/*.tgz + if-no-files-found: error + + publish: + name: Publish to npm (OIDC) + needs: build + runs-on: ubuntu-latest + permissions: + contents: read + id-token: write # OIDC → npm trusted publishing + provenance + steps: + - uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4 + with: + name: npm-package + - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4 + with: + node-version: 22 + registry-url: https://registry.npmjs.org + - run: npm install -g npm@latest # npm >= 11.5.1 supports OIDC trusted publishing + # Publish the prebuilt tarball only — no untrusted install/build runs in the OIDC job. + - run: npm publish *.tgz --provenance --access public # Trusted publisher configured → no NODE_AUTH_TOKEN needed. # Token fallback: set env NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} and drop --provenance. diff --git a/.github/workflows/release-pypi.yml b/.github/workflows/release-pypi.yml index b9f478b..5b3d63c 100644 --- a/.github/workflows/release-pypi.yml +++ b/.github/workflows/release-pypi.yml @@ -2,30 +2,57 @@ name: Release PyPI # Publish hashlock-sdk. Trigger: a package-scoped tag, e.g. `python-v1.0.0`. # git tag python-v1.0.0 && git push origin python-v1.0.0 -# Uses OIDC Trusted Publishing (no long-lived token in the repo). Configure the PyPI trusted -# publisher once against THIS workflow file (release-pypi.yml) — see the setup instructions. +# Build runs WITHOUT OIDC; only the minimal `publish` job holds `id-token: write` and does nothing but +# publish the prebuilt distributions. Uses OIDC Trusted Publishing (no long-lived token in the repo) — +# configure the PyPI trusted publisher once against THIS workflow file (release-pypi.yml). on: push: tags: ['python-v*'] permissions: contents: read - id-token: write # OIDC → PyPI trusted publishing jobs: - publish: - name: Publish to PyPI + build: + name: Build (no OIDC) runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 - - uses: actions/setup-python@v5 + - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4 + with: + persist-credentials: false + - uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5 with: python-version: '3.11' + - name: Verify tag matches pyproject version + working-directory: python + run: | + TAG="${GITHUB_REF_NAME#python-v}" + PKG="$(grep -E '^version = ' pyproject.toml | head -1 | cut -d'"' -f2)" + echo "tag=$TAG pyproject=$PKG" + [ "$TAG" = "$PKG" ] || { echo "::error::tag $GITHUB_REF_NAME does not match pyproject version $PKG"; exit 1; } - run: pip install build - run: python -m build working-directory: python - - uses: pypa/gh-action-pypi-publish@release/v1 + - uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4 + with: + name: pypi-dist + path: python/dist/* + if-no-files-found: error + + publish: + name: Publish to PyPI (OIDC) + needs: build + runs-on: ubuntu-latest + permissions: + contents: read + id-token: write # OIDC → PyPI trusted publishing + steps: + - uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4 + with: + name: pypi-dist + path: dist + - uses: pypa/gh-action-pypi-publish@ba38be9e461d3875417946c167d0b5f3d385a247 # release/v1 with: - packages-dir: python/dist + packages-dir: dist # Trusted publisher configured → no token needed. # Token fallback: add `password: ${{ secrets.PYPI_TOKEN }}`. From e458825a2bd2fc4942d41cca8dd2192442c505b1 Mon Sep 17 00:00:00 2001 From: Alexei Diakonov Date: Thu, 23 Jul 2026 17:30:27 +0300 Subject: [PATCH 3/3] ci(security): drop mutable global npm from the OIDC publish job MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Per review: don't run `npm install -g npm@latest` (registry-supplied, with install scripts) inside the id-token: write job. Pin the publish job to Node 24.18.0, whose bundled npm 11.16.0 already satisfies OIDC trusted publishing (>= 11.5.1) — no runtime npm upgrade in the privileged context. (Reviewer's '22.14.0' bundles npm 10.9.2; the earliest Node with npm >= 11.5.1 is 24.5.0.) Build job unchanged (no OIDC there). Co-Authored-By: Claude Opus 4.8 (1M context) --- .github/workflows/release-npm.yml | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/.github/workflows/release-npm.yml b/.github/workflows/release-npm.yml index 9868a3d..f784cb8 100644 --- a/.github/workflows/release-npm.yml +++ b/.github/workflows/release-npm.yml @@ -54,10 +54,11 @@ jobs: name: npm-package - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4 with: - node-version: 22 + # Pinned Node whose BUNDLED npm (11.16.0) already satisfies OIDC trusted publishing (>= 11.5.1), + # so the privileged job runs no registry-supplied `npm install -g` in the OIDC context. + node-version: 24.18.0 registry-url: https://registry.npmjs.org - - run: npm install -g npm@latest # npm >= 11.5.1 supports OIDC trusted publishing - # Publish the prebuilt tarball only — no untrusted install/build runs in the OIDC job. + # Publish the prebuilt tarball only — the OIDC job runs no untrusted install/build scripts. - run: npm publish *.tgz --provenance --access public # Trusted publisher configured → no NODE_AUTH_TOKEN needed. # Token fallback: set env NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} and drop --provenance.