Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
93 changes: 62 additions & 31 deletions .github/workflows/publish-js.yml
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,11 @@
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
Expand All @@ -29,27 +34,29 @@

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
Expand Down Expand Up @@ -87,6 +94,7 @@
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
ref: ${{ needs.verify-source.outputs.sha }}
persist-credentials: false

- name: Setup pnpm
Expand Down Expand Up @@ -118,11 +126,11 @@
target/
key: ${{ matrix.settings.target }}-cargo-${{ matrix.settings.host }}

- name: Install dependencies
run: pnpm install --frozen-lockfile
working-directory: crates/bashkit-js

- name: Build

Check failure

Code scanning / CodeQL

Cache Poisoning via execution of untrusted code High

Potential cache poisoning in the context of the default branch due to privilege checkout of untrusted code from
needs.verify-source.outputs.sha
. (
workflow_dispatch
).
run: ${{ matrix.settings.build }}
shell: bash
working-directory: crates/bashkit-js
Expand Down Expand Up @@ -174,7 +182,7 @@
# ============================================================================
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:
Expand All @@ -193,8 +201,21 @@
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:
Expand Down Expand Up @@ -304,7 +325,7 @@
# ============================================================================
test-js-linux:
name: Test JS ${{ matrix.target }} - node@${{ matrix.node }}
needs: [build-js]
needs: [verify-source, build-js]
strategy:
fail-fast: false
matrix:
Expand All @@ -316,8 +337,21 @@
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:
Expand Down Expand Up @@ -480,6 +514,7 @@
name: Release to NPM
runs-on: ubuntu-latest
needs:
- verify-source
- test-js-macos-windows
- test-js-linux

Expand All @@ -491,16 +526,14 @@

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
Expand Down Expand Up @@ -550,22 +583,20 @@
- 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: |
Expand Down
59 changes: 31 additions & 28 deletions .github/workflows/publish-wasm.yml
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,11 @@
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
Expand All @@ -25,22 +30,24 @@
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
Expand All @@ -52,6 +59,7 @@
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
ref: ${{ needs.verify-source.outputs.sha }}
persist-credentials: false

- name: Install Rust
Expand All @@ -61,29 +69,29 @@

- uses: Swatinem/rust-cache@d1527c8f67ef9af357290475d4af4b0fab8f12e0 # v2

- name: Install wasm-bindgen-cli
run: bash scripts/install-wasm-bindgen.sh

# Pinned + checksummed, because this job's output is what ships to npm.
# See scripts/install-binaryen.sh.
- name: Install binaryen (wasm-opt)

Check failure

Code scanning / CodeQL

Cache Poisoning via execution of untrusted code High

Potential cache poisoning in the context of the default branch due to privilege checkout of untrusted code from
needs.verify-source.outputs.sha
. (
workflow_dispatch
).
run: sudo ./scripts/install-binaryen.sh

- name: Build wasm bundle

Check failure

Code scanning / CodeQL

Cache Poisoning via execution of untrusted code High

Potential cache poisoning in the context of the default branch due to privilege checkout of untrusted code from
needs.verify-source.outputs.sha
. (
workflow_dispatch
).
run: bash crates/bashkit-wasm/scripts/build.sh release

- name: Integration tests (headless Node)

Check failure

Code scanning / CodeQL

Cache Poisoning via execution of untrusted code High

Potential cache poisoning in the context of the default branch due to privilege checkout of untrusted code from
needs.verify-source.outputs.sha
. (
workflow_dispatch
).
run: node --test "crates/bashkit-wasm/__test__/*.test.mjs"

- name: Browser streaming (main thread and worker)
run: |
npm install --prefix "$RUNNER_TEMP/bashkit-browser-test" --no-save playwright@1.62.1
export PLAYWRIGHT_MODULE="$RUNNER_TEMP/bashkit-browser-test/node_modules/playwright/index.mjs"
node "$RUNNER_TEMP/bashkit-browser-test/node_modules/playwright/cli.js" install --with-deps chromium
export CHROME_EXECUTABLE=$(node --input-type=module -e 'const {chromium} = await import(process.env.PLAYWRIGHT_MODULE); console.log(chromium.executablePath())')
node crates/bashkit-wasm/scripts/test-streaming-browser.mjs

- name: Build optional runtime bundle

Check failure

Code scanning / CodeQL

Cache Poisoning via execution of untrusted code High

Potential cache poisoning in the context of the default branch due to privilege checkout of untrusted code from
needs.verify-source.outputs.sha
. (
workflow_dispatch
).
run: BASHKIT_WASM_VARIANT=runtimes BASHKIT_WASM_FEATURES=terminal,python,typescript bash crates/bashkit-wasm/scripts/build.sh release

- name: Embedded runtime callback tests
Expand All @@ -109,24 +117,21 @@

publish-wasm:
name: Publish to npm
needs: build-wasm
needs: [verify-source, build-wasm]
runs-on: ubuntu-latest
permissions:
contents: read
id-token: write # npm trusted publishing + provenance
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
Expand Down Expand Up @@ -154,20 +159,18 @@
- 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
Expand Down
4 changes: 4 additions & 0 deletions knowledge/log.md
Original file line number Diff line number Diff line change
@@ -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).
Expand Down
20 changes: 15 additions & 5 deletions knowledge/operations/release-process.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
56 changes: 56 additions & 0 deletions scripts/resolve_publish_source.sh
Original file line number Diff line number Diff line change
@@ -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"
Loading
Loading