From 287a0a6e9671f9f09863a4e3b572a42440ddbeee Mon Sep 17 00:00:00 2001 From: Mykhailo Chalyi Date: Thu, 29 Oct 2026 03:09:28 +0000 Subject: [PATCH 1/4] ci(npm): re-publish an existing release tag via a tag input publish-js.yml and publish-wasm.yml accept an optional tag input on manual dispatch. scripts/resolve_publish_source.sh resolves the commit every job builds: the tag's exact commit (published as latest) when the input is set, otherwise the workflow ref as before. The workflow definition must be the tag's or current main's, and the source must be reachable from main. Publish JS gates a re-published tag with the workflow commit's JS tests, so test-only fixes landed after the tag (#2780) apply. Recovers 0.20.1, which never reached npm, without cutting a new version. Co-Authored-By: Claude Opus 5.5 --- .github/workflows/publish-js.yml | 93 +++++++++++------ .github/workflows/publish-wasm.yml | 59 ++++++----- knowledge/log.md | 4 + knowledge/operations/release-process.md | 20 +++- scripts/resolve_publish_source.sh | 56 ++++++++++ scripts/tests/test_release_workflow.py | 16 +++ scripts/tests/test_resolve_publish_source.py | 102 +++++++++++++++++++ 7 files changed, 286 insertions(+), 64 deletions(-) create mode 100755 scripts/resolve_publish_source.sh create mode 100644 scripts/tests/test_resolve_publish_source.py diff --git a/.github/workflows/publish-js.yml b/.github/workflows/publish-js.yml index f3d0bfdd6..034c2eff3 100644 --- a/.github/workflows/publish-js.yml +++ b/.github/workflows/publish-js.yml @@ -17,6 +17,11 @@ on: release: types: [published] workflow_dispatch: + inputs: + tag: + description: 'Re-publish an existing release tag (e.g. v0.20.1) as npm latest; empty builds the selected ref' + required: false + type: string permissions: contents: read @@ -29,27 +34,29 @@ env: jobs: # ============================================================================ - # Verify the publish source is on main before running any repo code + # Resolve and verify the publish source before running any repo code # with secrets. Manual dispatch and release events can target arbitrary - # refs; this gate ensures all subsequent jobs see trusted code. + # refs; this gate ensures all subsequent jobs see trusted code. A `tag` + # input re-publishes that release tag's exact commit (see the script). # ============================================================================ verify-source: name: Verify publish source runs-on: ubuntu-latest + outputs: + sha: ${{ steps.source.outputs.sha }} + dist_tag: ${{ steps.source.outputs.dist_tag }} + version: ${{ steps.source.outputs.version }} steps: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: fetch-depth: 0 + persist-credentials: false - - name: Verify publish source is reachable from main - run: | - git fetch --no-tags origin main:refs/remotes/origin/main - SOURCE_SHA=$(git rev-parse HEAD) - if ! git merge-base --is-ancestor "$SOURCE_SHA" origin/main; then - echo "Error: publish source $SOURCE_SHA is not reachable from origin/main" - exit 1 - fi - echo "Publish source verified on origin/main: $SOURCE_SHA" + - name: Resolve publish source + id: source + env: + INPUT_TAG: ${{ inputs.tag }} + run: bash scripts/resolve_publish_source.sh # ============================================================================ # Build native bindings for each platform @@ -87,6 +94,7 @@ jobs: steps: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: + ref: ${{ needs.verify-source.outputs.sha }} persist-credentials: false - name: Setup pnpm @@ -174,7 +182,7 @@ jobs: # ============================================================================ test-js-macos-windows: name: Test JS on ${{ matrix.settings.target }} - node@${{ matrix.node }} - needs: [build-js] + needs: [verify-source, build-js] strategy: fail-fast: false matrix: @@ -193,8 +201,21 @@ jobs: steps: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: + ref: ${{ needs.verify-source.outputs.sha }} persist-credentials: false + # Re-publishing an older tag: ship the tag's package bytes, but gate them + # with this workflow commit's tests (tests are not in the npm tarball). + # v0.20.1's own suite had a host-callback timeout too tight for the + # Rosetta lane, fixed in #2780. + - name: Use the workflow commit's JS tests + if: needs.verify-source.outputs.sha != github.sha + shell: bash + run: | + git fetch --no-tags --depth=1 origin "$GITHUB_SHA" + rm -rf crates/bashkit-js/__test__ + git checkout FETCH_HEAD -- crates/bashkit-js/__test__ + - name: Setup pnpm uses: pnpm/action-setup@ea17c68df8912ef543352723c149a84f56e3d413 # v6.1.0 with: @@ -304,7 +325,7 @@ jobs: # ============================================================================ test-js-linux: name: Test JS ${{ matrix.target }} - node@${{ matrix.node }} - needs: [build-js] + needs: [verify-source, build-js] strategy: fail-fast: false matrix: @@ -316,8 +337,21 @@ jobs: steps: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: + ref: ${{ needs.verify-source.outputs.sha }} persist-credentials: false + # Re-publishing an older tag: ship the tag's package bytes, but gate them + # with this workflow commit's tests (tests are not in the npm tarball). + # v0.20.1's own suite had a host-callback timeout too tight for the + # Rosetta lane, fixed in #2780. + - name: Use the workflow commit's JS tests + if: needs.verify-source.outputs.sha != github.sha + shell: bash + run: | + git fetch --no-tags --depth=1 origin "$GITHUB_SHA" + rm -rf crates/bashkit-js/__test__ + git checkout FETCH_HEAD -- crates/bashkit-js/__test__ + - name: Setup pnpm uses: pnpm/action-setup@ea17c68df8912ef543352723c149a84f56e3d413 # v6.1.0 with: @@ -480,6 +514,7 @@ jobs: name: Release to NPM runs-on: ubuntu-latest needs: + - verify-source - test-js-macos-windows - test-js-linux @@ -491,16 +526,14 @@ jobs: steps: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + ref: ${{ needs.verify-source.outputs.sha }} + persist-credentials: false - - name: Verify publish source is on main - run: | - git fetch --no-tags origin main:refs/remotes/origin/main - SOURCE_SHA=$(git rev-parse HEAD) - if ! git merge-base --is-ancestor "$SOURCE_SHA" origin/main; then - echo "Error: publish source $SOURCE_SHA is not reachable from origin/main" - exit 1 - fi - echo "Publish source verified on origin/main: $SOURCE_SHA" + - name: Confirm checkout is the verified publish source + env: + SOURCE_SHA: ${{ needs.verify-source.outputs.sha }} + run: test "$(git rev-parse HEAD)" = "$SOURCE_SHA" - name: Setup pnpm uses: pnpm/action-setup@ea17c68df8912ef543352723c149a84f56e3d413 # v6.1.0 @@ -550,22 +583,20 @@ jobs: - name: Determine npm dist-tag id: dist-tag working-directory: crates/bashkit-js + env: + DIST_TAG: ${{ needs.verify-source.outputs.dist_tag }} + TAG_VERSION: ${{ needs.verify-source.outputs.version }} run: | PKG_VERSION=$(node -p "require('./package.json').version") echo "version=$PKG_VERSION" # Only verified release tags may publish as npm latest. Branch or # manually selected refs must not become the default install target. - if [[ "$GITHUB_REF" =~ ^refs/tags/v[0-9]+\.[0-9]+\.[0-9]+$ ]]; then - TAG_VERSION="${GITHUB_REF#refs/tags/v}" - if [ "$TAG_VERSION" != "$PKG_VERSION" ]; then - echo "Error: Tag version ($TAG_VERSION) does not match package.json version ($PKG_VERSION)" - exit 1 - fi - echo "tag=latest" >> "$GITHUB_OUTPUT" - else - echo "tag=next" >> "$GITHUB_OUTPUT" + if [ "$DIST_TAG" = latest ] && [ "$TAG_VERSION" != "$PKG_VERSION" ]; then + echo "Error: Tag version ($TAG_VERSION) does not match package.json version ($PKG_VERSION)" + exit 1 fi + echo "tag=$DIST_TAG" >> "$GITHUB_OUTPUT" - name: Publish run: | diff --git a/.github/workflows/publish-wasm.yml b/.github/workflows/publish-wasm.yml index 72bc6b681..bed566c8f 100644 --- a/.github/workflows/publish-wasm.yml +++ b/.github/workflows/publish-wasm.yml @@ -17,6 +17,11 @@ on: release: types: [published] workflow_dispatch: + inputs: + tag: + description: 'Re-publish an existing release tag (e.g. v0.20.1) as npm latest; empty builds the selected ref' + required: false + type: string permissions: contents: read @@ -25,22 +30,24 @@ env: CARGO_TERM_COLOR: always jobs: + # A `tag` input re-publishes that release tag's exact commit; see the script. verify-source: name: Verify publish source runs-on: ubuntu-latest + outputs: + sha: ${{ steps.source.outputs.sha }} + dist_tag: ${{ steps.source.outputs.dist_tag }} + version: ${{ steps.source.outputs.version }} steps: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: fetch-depth: 0 - - name: Verify publish source is reachable from main - run: | - git fetch --no-tags origin main:refs/remotes/origin/main - SOURCE_SHA=$(git rev-parse HEAD) - if ! git merge-base --is-ancestor "$SOURCE_SHA" origin/main; then - echo "Error: publish source $SOURCE_SHA is not reachable from origin/main" - exit 1 - fi - echo "Publish source verified on origin/main: $SOURCE_SHA" + persist-credentials: false + - name: Resolve publish source + id: source + env: + INPUT_TAG: ${{ inputs.tag }} + run: bash scripts/resolve_publish_source.sh build-wasm: name: Build web bundle @@ -52,6 +59,7 @@ jobs: steps: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: + ref: ${{ needs.verify-source.outputs.sha }} persist-credentials: false - name: Install Rust @@ -109,7 +117,7 @@ jobs: publish-wasm: name: Publish to npm - needs: build-wasm + needs: [verify-source, build-wasm] runs-on: ubuntu-latest permissions: contents: read @@ -117,16 +125,13 @@ jobs: steps: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: - fetch-depth: 0 + ref: ${{ needs.verify-source.outputs.sha }} + persist-credentials: false - - name: Verify publish source is reachable from main - run: | - git fetch --no-tags origin main:refs/remotes/origin/main - SOURCE_SHA=$(git rev-parse HEAD) - if ! git merge-base --is-ancestor "$SOURCE_SHA" origin/main; then - echo "Error: publish source $SOURCE_SHA is not reachable from origin/main" - exit 1 - fi + - name: Confirm checkout is the verified publish source + env: + SOURCE_SHA: ${{ needs.verify-source.outputs.sha }} + run: test "$(git rev-parse HEAD)" = "$SOURCE_SHA" - name: Setup node uses: actions/setup-node@949feb2413d6458794dcd2491c4babbbce0c15c1 # v6 @@ -154,20 +159,18 @@ jobs: - name: Determine npm dist-tag id: dist-tag working-directory: pkg + env: + DIST_TAG: ${{ needs.verify-source.outputs.dist_tag }} + TAG_VERSION: ${{ needs.verify-source.outputs.version }} run: | PKG_VERSION=$(node -p "require('./package.json').version") echo "version=$PKG_VERSION" # Only verified release tags publish as npm latest. - if [[ "$GITHUB_REF" =~ ^refs/tags/v[0-9]+\.[0-9]+\.[0-9]+$ ]]; then - TAG_VERSION="${GITHUB_REF#refs/tags/v}" - if [ "$TAG_VERSION" != "$PKG_VERSION" ]; then - echo "Error: Tag version ($TAG_VERSION) does not match package.json version ($PKG_VERSION)" - exit 1 - fi - echo "tag=latest" >> "$GITHUB_OUTPUT" - else - echo "tag=next" >> "$GITHUB_OUTPUT" + if [ "$DIST_TAG" = latest ] && [ "$TAG_VERSION" != "$PKG_VERSION" ]; then + echo "Error: Tag version ($TAG_VERSION) does not match package.json version ($PKG_VERSION)" + exit 1 fi + echo "tag=$DIST_TAG" >> "$GITHUB_OUTPUT" - name: Publish working-directory: pkg diff --git a/knowledge/log.md b/knowledge/log.md index 84d9d623e..114cf1977 100644 --- a/knowledge/log.md +++ b/knowledge/log.md @@ -1,5 +1,9 @@ # Bashkit Knowledge Update Log +## 2026-10-29 + +* **npm release recovery**: `publish-js.yml` and `publish-wasm.yml` accept a `tag` input on manual dispatch to re-publish an existing release tag's exact commit as `latest`, from the current workflow definition. Added after 0.20.1 never reached npm (revoked token in the tag's workflow, Rosetta test timeout). See [Release Process](operations/release-process.md). + ## 2026-10-27 * **Windows Python packaging**: Document the reported 0.20.0 pin for the PondGuard SAC block and a fresh-environment A/B procedure with file hashes, Authenticode checks, and correlated Code Integrity events. Both published 0.20.0/0.20.1 x64 modules lack embedded signatures; both PyPI wheel provenance endpoints returned 404. No Windows reproduction or permanent signing fix is claimed. See [Python Package](runtimes/python-package.md). diff --git a/knowledge/operations/release-process.md b/knowledge/operations/release-process.md index 893165f21..8192b7073 100644 --- a/knowledge/operations/release-process.md +++ b/knowledge/operations/release-process.md @@ -308,11 +308,21 @@ npm >= 11.5.1, provenance automatic); no `NPM_TOKEN`. JS package version is sync from the workspace `Cargo.toml` by `build.rs` updating `package.json`. Only a run whose ref is the release tag (`refs/tags/vX.Y.Z`, version matching -`package.json`) publishes as npm `latest`; every other ref, including a manual -dispatch from `main`, publishes under `next`. So a failed npm publish cannot be -recovered by re-dispatching from `main`: fix the cause and cut a patch release, -whose tag then publishes `latest`. v0.17.0 landed on `next` this way, and -v0.17.1 is what put the native package back on `latest`. +`package.json`) publishes as npm `latest`; every other ref, including a plain +manual dispatch from `main`, publishes under `next`. v0.17.0 landed on `next` +this way, and v0.17.1 is what put the native package back on `latest`. + +To recover a release whose npm lane failed without cutting a new version, +dispatch `publish-js.yml` / `publish-wasm.yml` from `main` with the `tag` +input (`gh workflow run publish-js.yml --ref main -f tag=v0.20.1`). +`scripts/resolve_publish_source.sh` then builds the tag's exact commit and +publishes it as `latest`; the workflow definition must be the tag's or current +`main`'s, and the tag must be reachable from `main`. Package bytes come from +the tag, but `publish-js.yml` gates them with the workflow commit's +`crates/bashkit-js/__test__` (tests are not in the tarball), so a test fix +landed after the tag (v0.20.1's Rosetta timeout, #2780) still applies. Use this +when the failure was in the workflow or credentials, not in the shipped code; +a code bug still needs a patch release. The publish job never runs `tsc`; it ships whatever the `js-stubs` artifact carries. That artifact must list every `tsc` output (`.js` + `.d.ts` for each diff --git a/scripts/resolve_publish_source.sh b/scripts/resolve_publish_source.sh new file mode 100755 index 000000000..3b5359ddf --- /dev/null +++ b/scripts/resolve_publish_source.sh @@ -0,0 +1,56 @@ +#!/usr/bin/env bash +# Resolve the commit an npm publish workflow builds, and its npm dist-tag. +# +# Decisions: +# - Default: build the workflow's own commit. Only a release-tag ref +# (refs/tags/vX.Y.Z) publishes as `latest`; any other ref publishes `next`. +# - Recovery: a manual dispatch may name an existing release tag (`tag` input). +# The workflow definition then comes from that tag or current main, the +# package source from the tag's exact commit, and it publishes as `latest`. +# This re-publishes a release whose npm lane failed without cutting a new +# version (0.20.1: the tag's own workflow still used a revoked NPM_TOKEN). +# - The source commit must always be reachable from origin/main. +# +# Env: INPUT_TAG (may be empty), GITHUB_SHA, GITHUB_REF, GITHUB_OUTPUT. +# Outputs: sha, dist_tag, version (expected package version, empty for next). +set -euo pipefail + +git fetch --no-tags --force origin main:refs/remotes/origin/main +MAIN_SHA=$(git rev-parse origin/main) +TAG_RE='^v[0-9]+\.[0-9]+\.[0-9]+$' + +if [ -n "${INPUT_TAG:-}" ]; then + if [[ ! "$INPUT_TAG" =~ $TAG_RE ]]; then + echo "Error: tag must match vMAJOR.MINOR.PATCH, got '$INPUT_TAG'" >&2 + exit 1 + fi + git fetch --no-tags --force origin "refs/tags/$INPUT_TAG:refs/tags/$INPUT_TAG" + SOURCE_SHA=$(git rev-list -n 1 "refs/tags/$INPUT_TAG") + if [ "$GITHUB_SHA" != "$SOURCE_SHA" ] && [ "$GITHUB_SHA" != "$MAIN_SHA" ]; then + echo "Error: with a tag input the workflow ref must be $INPUT_TAG or current main" >&2 + exit 1 + fi + DIST_TAG=latest + VERSION="${INPUT_TAG#v}" +else + SOURCE_SHA="$GITHUB_SHA" + if [[ "$GITHUB_REF" == refs/tags/* ]] && [[ "${GITHUB_REF#refs/tags/}" =~ $TAG_RE ]]; then + DIST_TAG=latest + VERSION="${GITHUB_REF#refs/tags/v}" + else + DIST_TAG=next + VERSION="" + fi +fi + +if ! git merge-base --is-ancestor "$SOURCE_SHA" "$MAIN_SHA"; then + echo "Error: publish source $SOURCE_SHA is not reachable from origin/main" >&2 + exit 1 +fi + +echo "Publish source $SOURCE_SHA (dist-tag $DIST_TAG${VERSION:+, version $VERSION})" +{ + echo "sha=$SOURCE_SHA" + echo "dist_tag=$DIST_TAG" + echo "version=$VERSION" +} >> "$GITHUB_OUTPUT" diff --git a/scripts/tests/test_release_workflow.py b/scripts/tests/test_release_workflow.py index 24415fbf3..a9852e67a 100644 --- a/scripts/tests/test_release_workflow.py +++ b/scripts/tests/test_release_workflow.py @@ -176,6 +176,22 @@ def test_npm_publish_lanes_use_trusted_publishing(self) -> None: self.assertIn("id-token: write", body.split("\n steps:\n", 1)[0]) self.assertIn("npm install -g npm@11.6.2", body) + def test_npm_publish_lanes_build_only_the_resolved_source(self) -> None: + """A `tag` input re-publishes that tag; every job must build the same commit.""" + for name in ("publish-js.yml", "publish-wasm.yml"): + workflow = (ROOT / ".github/workflows" / name).read_text() + with self.subTest(workflow=name): + self.assertIn("run: bash scripts/resolve_publish_source.sh", workflow) + self.assertIn("INPUT_TAG: ${{ inputs.tag }}", workflow) + self.assertNotIn("GITHUB_REF", workflow) + jobs = workflow.split("\njobs:\n", 1)[1] + for job in re.split(r"\n (?=[a-z][a-z-]*:\n)", jobs): + if job.startswith(("verify-source:", "#")) or "actions/checkout@" not in job: + continue + with self.subTest(job=job.split(":", 1)[0]): + self.assertIn("ref: ${{ needs.verify-source.outputs.sha }}", job) + self.assertRegex(job, r"needs: \[?[^\n]*verify-source|needs:\n(\s+- [a-z-]+\n)*\s+- verify-source") + def test_web_ci_exercises_release_wasm_optimization(self) -> None: build_script = (ROOT / "crates/bashkit-wasm/scripts/build.sh").read_text() ci_workflow = (ROOT / ".github/workflows/ci.yml").read_text() diff --git a/scripts/tests/test_resolve_publish_source.py b/scripts/tests/test_resolve_publish_source.py new file mode 100644 index 000000000..d95cc1d4a --- /dev/null +++ b/scripts/tests/test_resolve_publish_source.py @@ -0,0 +1,102 @@ +"""npm publish source resolution: default ref vs. re-publishing a release tag.""" + +from __future__ import annotations + +import os +import pathlib +import subprocess +import tempfile +import unittest + +SCRIPT = pathlib.Path(__file__).resolve().parents[1] / "resolve_publish_source.sh" + + +def git(cwd, *args): + return subprocess.run( + ["git", *args], cwd=cwd, check=True, capture_output=True, text=True + ).stdout.strip() + + +class ResolvePublishSourceTests(unittest.TestCase): + def setUp(self) -> None: + self.tmp = tempfile.TemporaryDirectory() + root = pathlib.Path(self.tmp.name) + origin = root / "origin" + origin.mkdir() + git(origin, "init", "-q", "-b", "main") + git(origin, "config", "user.email", "t@example.com") + git(origin, "config", "user.name", "t") + git(origin, "commit", "-q", "--allow-empty", "-m", "release") + git(origin, "tag", "v1.2.3") + self.tag_sha = git(origin, "rev-parse", "HEAD") + git(origin, "commit", "-q", "--allow-empty", "-m", "after") + self.main_sha = git(origin, "rev-parse", "HEAD") + git(origin, "checkout", "-q", "-b", "side", self.tag_sha) + git(origin, "commit", "-q", "--allow-empty", "-m", "off main") + self.side_sha = git(origin, "rev-parse", "HEAD") + git(origin, "tag", "v9.9.9") + git(origin, "checkout", "-q", "main") + self.work = root / "work" + git(root, "clone", "-q", str(origin), str(self.work)) + self.output = root / "out" + + def tearDown(self) -> None: + self.tmp.cleanup() + + def resolve(self, *, tag="", sha, ref): + self.output.write_text("") + env = { + **os.environ, + "INPUT_TAG": tag, + "GITHUB_SHA": sha, + "GITHUB_REF": ref, + "GITHUB_OUTPUT": str(self.output), + } + result = subprocess.run( + ["bash", str(SCRIPT)], cwd=self.work, env=env, capture_output=True, text=True + ) + outputs = dict( + line.split("=", 1) for line in self.output.read_text().splitlines() + ) + return result.returncode, outputs, result.stderr + + def test_release_tag_ref_publishes_latest(self) -> None: + code, out, _ = self.resolve(sha=self.tag_sha, ref="refs/tags/v1.2.3") + self.assertEqual(code, 0) + self.assertEqual(out, {"sha": self.tag_sha, "dist_tag": "latest", "version": "1.2.3"}) + + def test_branch_ref_publishes_next(self) -> None: + code, out, _ = self.resolve(sha=self.main_sha, ref="refs/heads/main") + self.assertEqual(code, 0) + self.assertEqual(out, {"sha": self.main_sha, "dist_tag": "next", "version": ""}) + + def test_branch_named_like_a_tag_is_not_latest(self) -> None: + _, out, _ = self.resolve(sha=self.main_sha, ref="refs/heads/v1.2.3") + self.assertEqual(out["dist_tag"], "next") + + def test_tag_input_from_main_builds_the_tag_commit(self) -> None: + code, out, _ = self.resolve(tag="v1.2.3", sha=self.main_sha, ref="refs/heads/main") + self.assertEqual(code, 0) + self.assertEqual(out, {"sha": self.tag_sha, "dist_tag": "latest", "version": "1.2.3"}) + + def test_tag_input_rejects_other_workflow_refs(self) -> None: + code, out, err = self.resolve(tag="v1.2.3", sha=self.side_sha, ref="refs/heads/side") + self.assertEqual(code, 1) + self.assertEqual(out, {}) + self.assertIn("must be v1.2.3 or current main", err) + + def test_tag_input_must_look_like_a_release(self) -> None: + code, out, err = self.resolve(tag="main", sha=self.main_sha, ref="refs/heads/main") + self.assertEqual(code, 1) + self.assertEqual(out, {}) + self.assertIn("vMAJOR.MINOR.PATCH", err) + + def test_source_off_main_is_rejected(self) -> None: + code, out, err = self.resolve(tag="v9.9.9", sha=self.side_sha, ref="refs/tags/v9.9.9") + self.assertEqual(code, 1) + self.assertEqual(out, {}) + self.assertIn("not reachable from origin/main", err) + + +if __name__ == "__main__": + unittest.main() From bb3257ea2b9b544d6a08cf254868e6a8cf562a8e Mon Sep 17 00:00:00 2001 From: Mykhailo Chalyi Date: Thu, 29 Oct 2026 03:11:56 +0000 Subject: [PATCH 2/4] ci(npm): build shipped npm artifacts without caches CodeQL flagged cache poisoning: build-js, build-wasm and release-js restore default-branch caches after checking out the resolved publish source. The source is verified on main, but artifacts that ship should not depend on cache state either, so drop pnpm/cargo/rust-cache in those jobs. Co-Authored-By: Claude Opus 5.5 --- .github/workflows/publish-js.yml | 19 +++---------------- .github/workflows/publish-wasm.yml | 3 ++- 2 files changed, 5 insertions(+), 17 deletions(-) diff --git a/.github/workflows/publish-js.yml b/.github/workflows/publish-js.yml index 034c2eff3..c7913e2de 100644 --- a/.github/workflows/publish-js.yml +++ b/.github/workflows/publish-js.yml @@ -102,30 +102,19 @@ jobs: with: version: 10.33.0 + # No pnpm/cargo caches: the shipped binaries build from a clean state, so + # no cache entry (written by any run in the default-branch scope) can + # reach the npm artifact. - name: Setup node uses: actions/setup-node@949feb2413d6458794dcd2491c4babbbce0c15c1 # v6 with: node-version: "24.22.1" - cache: pnpm - cache-dependency-path: crates/bashkit-js/pnpm-lock.yaml - name: Install Rust uses: dtolnay/rust-toolchain@e081816240890017053eacbb1bdf337761dc5582 # 1.95.0 with: targets: ${{ matrix.settings.target }} - - name: Cache cargo - uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v5 - with: - path: | - ~/.cargo/registry/index/ - ~/.cargo/registry/cache/ - ~/.cargo/git/db/ - ~/.napi-rs - .cargo-cache - target/ - key: ${{ matrix.settings.target }}-cargo-${{ matrix.settings.host }} - - name: Install dependencies run: pnpm install --frozen-lockfile working-directory: crates/bashkit-js @@ -544,8 +533,6 @@ jobs: uses: actions/setup-node@949feb2413d6458794dcd2491c4babbbce0c15c1 # v6 with: node-version: "24.22.1" - cache: pnpm - cache-dependency-path: crates/bashkit-js/pnpm-lock.yaml - name: Require npm with trusted publishing (>= 11.5.1) # npm trusted publishing (GitHub OIDC -> short-lived publish token) diff --git a/.github/workflows/publish-wasm.yml b/.github/workflows/publish-wasm.yml index bed566c8f..cc1f85bb3 100644 --- a/.github/workflows/publish-wasm.yml +++ b/.github/workflows/publish-wasm.yml @@ -67,7 +67,8 @@ jobs: with: targets: wasm32-unknown-unknown - - uses: Swatinem/rust-cache@d1527c8f67ef9af357290475d4af4b0fab8f12e0 # v2 + # No rust-cache: the shipped bundle builds from a clean state, so no cache + # entry (written by any run in the default-branch scope) can reach npm. - name: Install wasm-bindgen-cli run: bash scripts/install-wasm-bindgen.sh From 75b4bb32a00e555d0972f9c87ad4cb20b192be2b Mon Sep 17 00:00:00 2001 From: Mykhailo Chalyi Date: Thu, 29 Oct 2026 03:13:15 +0000 Subject: [PATCH 3/4] fix(deps): bump seroval to 1.6.12 in the site for GHSA-9wx9-5pv9-3349 and GHSA-f86p-h4j2-7qgj Lockfile-only refresh within solid-js's ~1.6.8 range; clears the Audit job. Co-Authored-By: Claude Opus 5.5 --- site/pnpm-lock.yaml | 38 +++++++++++++++++++------------------- 1 file changed, 19 insertions(+), 19 deletions(-) diff --git a/site/pnpm-lock.yaml b/site/pnpm-lock.yaml index 6e9c65213..c982cbd64 100644 --- a/site/pnpm-lock.yaml +++ b/site/pnpm-lock.yaml @@ -53,7 +53,7 @@ importers: devDependencies: '@astrojs/check': specifier: ^0.9.12 - version: 0.9.12(@emnapi/runtime@1.11.1)(prettier@3.9.10)(typescript@7.0.2) + version: 0.9.12(@emnapi/runtime@1.11.3)(prettier@3.9.10)(typescript@7.0.2) '@types/node': specifier: ^26.6.5 version: 26.6.5 @@ -1962,14 +1962,14 @@ packages: engines: {node: '>=10'} hasBin: true - seroval-plugins@1.6.8: - resolution: {integrity: sha512-N7mWAMydj89EnYTHtRpS3LBrAz3J9O6oVrqpuXuvlAHfAUG8WEKqtbAd7SBiJNY5NfCAFhSdyAh7wI4Shjqy4g==} + seroval-plugins@1.6.12: + resolution: {integrity: sha512-+reiwp/lYxiyctSrdvX7OQ1YLrcbWYgBiZrhSFL+V43b0/DGRLNVSDyZINbQe+tWplPOI2fw2dPrdv6hcc09kQ==} engines: {node: '>=10'} peerDependencies: seroval: ^1.0 - seroval@1.6.8: - resolution: {integrity: sha512-HlSgSAkTk4EqHcje1ptJjfZi1YDv5KbhVJ/d3P7T/nAXua2VmDu+AKDX5VTdFfZf48nDkWB2TKYt0DrCSa+3wg==} + seroval@1.6.12: + resolution: {integrity: sha512-CqbIVfk/7GL3ITwZI55lcPbpQbjHZ2DD5cUj4iH7E9YsCfHNJV1mx7yNp86Z84w5IpmkZZS/2s5vPOWmVWSdQg==} engines: {node: '>=10'} sharp@0.35.5: @@ -2454,16 +2454,16 @@ packages: snapshots: - '@astrojs/astro2tsx@0.1.3(@emnapi/runtime@1.11.1)': + '@astrojs/astro2tsx@0.1.3(@emnapi/runtime@1.11.3)': dependencies: '@emnapi/core': 1.11.3 - '@napi-rs/wasm-runtime': 1.2.4(@emnapi/core@1.11.3)(@emnapi/runtime@1.11.1) + '@napi-rs/wasm-runtime': 1.2.4(@emnapi/core@1.11.3)(@emnapi/runtime@1.11.3) transitivePeerDependencies: - '@emnapi/runtime' - '@astrojs/check@0.9.12(@emnapi/runtime@1.11.1)(prettier@3.9.10)(typescript@7.0.2)': + '@astrojs/check@0.9.12(@emnapi/runtime@1.11.3)(prettier@3.9.10)(typescript@7.0.2)': dependencies: - '@astrojs/language-server': 2.17.3(@emnapi/runtime@1.11.1)(prettier@3.9.10)(typescript@7.0.2) + '@astrojs/language-server': 2.17.3(@emnapi/runtime@1.11.3)(prettier@3.9.10)(typescript@7.0.2) chokidar: 5.0.0 kleur: 4.1.5 typescript: 7.0.2 @@ -2535,9 +2535,9 @@ snapshots: unified: 11.0.5 yaml: 2.9.1 - '@astrojs/language-server@2.17.3(@emnapi/runtime@1.11.1)(prettier@3.9.10)(typescript@7.0.2)': + '@astrojs/language-server@2.17.3(@emnapi/runtime@1.11.3)(prettier@3.9.10)(typescript@7.0.2)': dependencies: - '@astrojs/astro2tsx': 0.1.3(@emnapi/runtime@1.11.1) + '@astrojs/astro2tsx': 0.1.3(@emnapi/runtime@1.11.3) '@astrojs/yaml2ts': 0.2.4 '@volar/kit': 2.4.30(typescript@7.0.2) '@volar/language-core': 2.4.30 @@ -2740,11 +2740,11 @@ snapshots: '@emnapi/runtime@1.11.1': dependencies: tslib: 2.8.1 + optional: true '@emnapi/runtime@1.11.3': dependencies: tslib: 2.8.1 - optional: true '@emnapi/wasi-threads@1.2.2': dependencies: @@ -2980,10 +2980,10 @@ snapshots: transitivePeerDependencies: - supports-color - '@napi-rs/wasm-runtime@1.2.4(@emnapi/core@1.11.3)(@emnapi/runtime@1.11.1)': + '@napi-rs/wasm-runtime@1.2.4(@emnapi/core@1.11.3)(@emnapi/runtime@1.11.3)': dependencies: '@emnapi/core': 1.11.3 - '@emnapi/runtime': 1.11.1 + '@emnapi/runtime': 1.11.3 '@tybys/wasm-util': 0.10.4 '@napi-rs/wasm-runtime@1.2.6(@emnapi/core@1.11.1)(@emnapi/runtime@1.11.1)': @@ -4699,11 +4699,11 @@ snapshots: semver@7.8.5: {} - seroval-plugins@1.6.8(seroval@1.6.8): + seroval-plugins@1.6.12(seroval@1.6.12): dependencies: - seroval: 1.6.8 + seroval: 1.6.12 - seroval@1.6.8: {} + seroval@1.6.12: {} sharp@0.35.5(@types/node@26.6.5): dependencies: @@ -4763,8 +4763,8 @@ snapshots: solid-js@1.9.16: dependencies: csstype: 3.2.3 - seroval: 1.6.8 - seroval-plugins: 1.6.8(seroval@1.6.8) + seroval: 1.6.12 + seroval-plugins: 1.6.12(seroval@1.6.12) solid-transition-group@0.2.3(solid-js@1.9.16): dependencies: From 4de39b0ec4d50ee91edb0f1f9b2733ec518c166d Mon Sep 17 00:00:00 2001 From: Mykhailo Chalyi Date: Thu, 29 Oct 2026 03:14:04 +0000 Subject: [PATCH 4/4] Revert "ci(npm): build shipped npm artifacts without caches" CodeQL's cache-poisoning finding is about running code after checking out a computed ref in a default-branch context, not about the cache actions: it stayed at 5 alerts with every cache removed. Keep the caches. Co-Authored-By: Claude Opus 5.5 --- .github/workflows/publish-js.yml | 19 ++++++++++++++++--- .github/workflows/publish-wasm.yml | 3 +-- 2 files changed, 17 insertions(+), 5 deletions(-) diff --git a/.github/workflows/publish-js.yml b/.github/workflows/publish-js.yml index c7913e2de..034c2eff3 100644 --- a/.github/workflows/publish-js.yml +++ b/.github/workflows/publish-js.yml @@ -102,19 +102,30 @@ jobs: with: version: 10.33.0 - # No pnpm/cargo caches: the shipped binaries build from a clean state, so - # no cache entry (written by any run in the default-branch scope) can - # reach the npm artifact. - name: Setup node uses: actions/setup-node@949feb2413d6458794dcd2491c4babbbce0c15c1 # v6 with: node-version: "24.22.1" + cache: pnpm + cache-dependency-path: crates/bashkit-js/pnpm-lock.yaml - name: Install Rust uses: dtolnay/rust-toolchain@e081816240890017053eacbb1bdf337761dc5582 # 1.95.0 with: targets: ${{ matrix.settings.target }} + - name: Cache cargo + uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v5 + with: + path: | + ~/.cargo/registry/index/ + ~/.cargo/registry/cache/ + ~/.cargo/git/db/ + ~/.napi-rs + .cargo-cache + target/ + key: ${{ matrix.settings.target }}-cargo-${{ matrix.settings.host }} + - name: Install dependencies run: pnpm install --frozen-lockfile working-directory: crates/bashkit-js @@ -533,6 +544,8 @@ jobs: uses: actions/setup-node@949feb2413d6458794dcd2491c4babbbce0c15c1 # v6 with: node-version: "24.22.1" + cache: pnpm + cache-dependency-path: crates/bashkit-js/pnpm-lock.yaml - name: Require npm with trusted publishing (>= 11.5.1) # npm trusted publishing (GitHub OIDC -> short-lived publish token) diff --git a/.github/workflows/publish-wasm.yml b/.github/workflows/publish-wasm.yml index cc1f85bb3..bed566c8f 100644 --- a/.github/workflows/publish-wasm.yml +++ b/.github/workflows/publish-wasm.yml @@ -67,8 +67,7 @@ jobs: with: targets: wasm32-unknown-unknown - # No rust-cache: the shipped bundle builds from a clean state, so no cache - # entry (written by any run in the default-branch scope) can reach npm. + - uses: Swatinem/rust-cache@d1527c8f67ef9af357290475d4af4b0fab8f12e0 # v2 - name: Install wasm-bindgen-cli run: bash scripts/install-wasm-bindgen.sh