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)"