From ba32a9416657626a3b908e50b80a1051b0928553 Mon Sep 17 00:00:00 2001 From: Adhik Joshi Date: Mon, 24 Aug 2026 22:36:25 +0530 Subject: [PATCH] fix(release): publish with relative paths, and stop skipping the wheel build MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit v0.1.3 reached neither registry. Two faults, both mine, both invisible to the way I tested. publish.sh used `node -p "require('${dir}/package.json')"` - Node treats a path that does not begin with ./ or / as a MODULE specifier, so `require('dist/npm/@modelslab/cli-linux-x64/package.json')` is a module lookup that fails with MODULE_NOT_FOUND. It worked in every local test because those passed absolute paths; CI passes a relative dist dir. Replaced with jq, which reads a file as a file. The PyPI guard was on the wrong step - `if: always()` was on the upload alone. npm failed, the wheel BUILD was skipped as an ordinary downstream skip, and the upload then ran and died on "Cannot find file dist/pypi/*.whl". A guard on the last step of a chain protects nothing. Every PyPI step now carries it, as `!cancelled()` so a cancelled run still stops. CI gains a dry-run of publish.sh against a stubbed npm, from the repo root with a relative dist dir — the shape the release actually uses. It asserts seven packages and that the unscoped entry package is published last. Both of the above would have failed it. Verified locally with a relative dist dir: the old call reproduces "Cannot find module", the new one resolves, and the publisher emits all six scoped platform packages followed by the entry package, with six wheels present for the PyPI step. --- .github/workflows/ci.yml | 24 ++++++++++++++++++++++++ .github/workflows/release.yml | 9 ++++++++- packaging/npm/publish.sh | 8 ++++++-- 3 files changed, 38 insertions(+), 3 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 3a77d26..f43ba8a 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -88,6 +88,30 @@ jobs: # The real check: the shim resolves the binary and the binary runs. ./node_modules/.bin/modelslab --version + # Dry-runs publish.sh against a stubbed npm, from the repo root with a + # relative dist dir — the shape CI and the release both use. v0.1.3 died + # here on a `node -p require('dist/npm/...')` that resolves fine with an + # absolute path and not at all with a relative one, so local testing with + # absolute paths never saw it. + - name: Dry-run the npm publisher + run: | + set -euo pipefail + mkdir -p /tmp/stub + cat > /tmp/stub/npm <<'STUB' + #!/usr/bin/env bash + case "$1" in + view) exit 1 ;; + publish) echo "would publish $2"; exit 0 ;; + *) exit 0 ;; + esac + STUB + chmod +x /tmp/stub/npm + output=$(PATH="/tmp/stub:$PATH" bash packaging/npm/publish.sh dist/npm) + echo "$output" + # Seven packages, and the unscoped entry must come last. + test "$(grep -c '^publish ' <<<"$output")" -eq 7 + test "$(grep '^publish ' <<<"$output" | tail -1)" = "publish modelslab-cli@0.0.0" + - name: Build PyPI wheels run: python3 packaging/pypi/build.py v0.0.0 artifacts dist/pypi diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 70eefa8..5278ced 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -84,11 +84,18 @@ jobs: if: ${{ env.NODE_AUTH_TOKEN != '' }} run: bash packaging/npm/publish.sh dist/npm + # `!cancelled()` on every PyPI step, not just the upload. v0.1.3 had the + # guard on the upload alone: npm failed, the build step was skipped as a + # normal downstream skip, and the upload then ran and died on + # "Cannot find file dist/pypi/*.whl". A guard on the last step of a chain + # protects nothing. - uses: actions/setup-python@v5 + if: ${{ !cancelled() }} with: python-version: "3.12" - name: Build PyPI wheels + if: ${{ !cancelled() }} run: python3 packaging/pypi/build.py "${GITHUB_REF_NAME}" artifacts dist/pypi # `if: always()` because npm and PyPI are independent registries and a @@ -100,7 +107,7 @@ jobs: TWINE_USERNAME: __token__ TWINE_PASSWORD: ${{ secrets.PYPI_TOKEN }} # `secrets` is not an available context in a step-level `if` — hence env. - if: ${{ always() && env.TWINE_PASSWORD != '' }} + if: ${{ !cancelled() && env.TWINE_PASSWORD != '' }} run: | set -euo pipefail python3 -m pip install --quiet twine diff --git a/packaging/npm/publish.sh b/packaging/npm/publish.sh index af2ccdf..86e7d5d 100755 --- a/packaging/npm/publish.sh +++ b/packaging/npm/publish.sh @@ -33,8 +33,12 @@ already_published() { publish_one() { local dir="$1" local name version attempt delay output - name=$(node -p "require('${dir}/package.json').name") - version=$(node -p "require('${dir}/package.json').version") + # jq, not `node -p require(...)`. Node treats a path that does not start with + # ./ or / as a MODULE specifier, so `require('dist/npm/@modelslab/cli-linux-x64/package.json')` + # is a module lookup that fails with MODULE_NOT_FOUND. It only worked in local + # testing because that passed absolute paths; CI passes a relative dist dir. + name=$(jq -r .name "${dir}/package.json") + version=$(jq -r .version "${dir}/package.json") if already_published "$name" "$version"; then echo "skip ${name}@${version} (already on the registry)"