mirror of
https://github.com/Twigpine/zero.git
synced 2026-11-10 05:01:28 -10:00
* fix(npm): wait for npm to apply each publish, and drop the one-time @gitlawb/zero steps npm applies a publish asynchronously: `npm publish` returns once the upload is accepted, and the version appears later. In the 0.10.0 release the @twigpine/zero wrapper appeared 9 seconds after publish-packages.sh stopped checking for it, and the last platform payload about ten minutes after its upload. Two things went wrong: - The wrapper became visible before four of its five platform payloads, so installs in that window resolved aliases that 404 and fell back to the GitHub-release download. Ordering the uploads does not order what npm shows. - The final `latest` check gave up after 25 seconds and failed the job even though every publish landed, which also skipped the @gitlawb/zero steps until a re-run. publish-packages.sh now waits until npm shows every platform version before it publishes the wrapper, then waits until `latest` is the wrapper version. One deadline bounds the waits (ZERO_NPM_PUBLISH_WAIT_TIMEOUT, default 30 minutes). On timeout it fails before the wrapper goes out and says what npm never showed. These lookups treat an error as "not yet", unlike the skip decision, which still stops on anything but E404. The test fake can now apply a publish only after a number of later lookups, and records which aliased versions were missing when the wrapper was published. Against the script as released in v0.10.0, the two new tests fail the way the release did: the job exits 1 although everything landed, and the wrapper goes out while its platform payload is not visible. The two release steps that published 0.10.0 once more as @gitlawb/zero have done their job and are removed; docs/NPM_PACKAGING.md and the comments that pointed at them now describe what happened. The Go and wrapper handling of the old name stays, since only a build installed under it can trigger it. * fix(npm): validate the publish wait settings, and correct the legacy publishing claim ZERO_NPM_PUBLISH_WAIT_TIMEOUT went into Bash arithmetic as typed. A leading zero reads as octal (0600 waited 384 seconds), and 08 is an arithmetic error that, in a script, does not stop it: Bash prints the error, leaves the deadline unset and carries on, so the run only died at its first wait, after the platform payloads were published. A negative timeout ended the wait before it started, and a bad ZERO_NPM_PUBLISH_RETRY_DELAY only failed at the first sleep, also after publishing began. Both settings are now checked before anything is published: whole seconds only, converted with 10# so a leading zero is decimal, bounded before the conversion, and capped (timeout 86400, delay 600). A bad value fails with the variable's name. TestPublishPackagesReadsTheWaitTimeoutAsDecimal makes the script wait, which is where the old code broke: against the reviewed script it publishes the platform payload and then exits 1 on "08: value too great for base". TestPublishPackagesRejectsAnInvalidWaitSetting covers -5, abc, 1e3, an over-range and an overflowing timeout, and a non-numeric delay; the old script published with five of them and crashed on the sixth with "abc: unbound variable". docs/NPM_PACKAGING.md said nothing could publish @gitlawb/zero again. Removing the trusted publisher stops this workflow; a maintainer could still publish by hand, and the doc now says that.
216 lines
9.8 KiB
YAML
216 lines
9.8 KiB
YAML
name: Release Artifacts
|
|
|
|
on:
|
|
workflow_dispatch:
|
|
inputs:
|
|
ref:
|
|
description: Git ref to package. Defaults to the workflow branch.
|
|
required: false
|
|
type: string
|
|
push:
|
|
tags:
|
|
- 'v*'
|
|
|
|
jobs:
|
|
package:
|
|
name: Package (${{ matrix.os }})
|
|
runs-on: ${{ matrix.os }}
|
|
permissions:
|
|
contents: write # create/upload the GitHub Release on a tag push
|
|
strategy:
|
|
fail-fast: false
|
|
matrix:
|
|
# `zero-release package` builds + smoke-tests natively (host==target), so each
|
|
# target platform needs its own native runner. These five cover the platforms
|
|
# install.sh / install.ps1 / the npm wrapper resolve.
|
|
os:
|
|
- ubuntu-latest # linux-x64
|
|
- ubuntu-24.04-arm # linux-arm64
|
|
- macos-latest # macos-arm64 (Apple Silicon)
|
|
- macos-15-intel # macos-x64 (Intel)
|
|
- windows-latest # windows-x64
|
|
|
|
steps:
|
|
- name: Checkout
|
|
if: ${{ github.event_name != 'workflow_dispatch' || inputs.ref == '' }}
|
|
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
|
|
with:
|
|
persist-credentials: false
|
|
|
|
- name: Checkout requested ref
|
|
if: ${{ github.event_name == 'workflow_dispatch' && inputs.ref != '' }}
|
|
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
|
|
with:
|
|
ref: ${{ inputs.ref }}
|
|
persist-credentials: false
|
|
|
|
- name: Setup Go
|
|
uses: actions/setup-go@40f1582b2485089dde7abd97c1529aa768e1baff
|
|
with:
|
|
go-version-file: go.mod
|
|
cache: true
|
|
|
|
- name: Test
|
|
run: go test ./...
|
|
|
|
# zero-release names assets zero-v<package.json version>-… while the release
|
|
# is published under the pushed git tag. If they disagree, install.sh/install.ps1
|
|
# and the npm postinstall all build a download URL that 404s. Fail fast so a
|
|
# forgotten version bump can never publish mis-versioned, un-installable assets.
|
|
# Matches on the ref (not event_name) because release-please dispatches this
|
|
# workflow at the tag ref — its GITHUB_TOKEN-created tag cannot fire `push`.
|
|
- name: Check tag matches package.json version
|
|
if: ${{ startsWith(github.ref, 'refs/tags/v') }}
|
|
shell: bash
|
|
run: |
|
|
pkg_version="$(node -p "require('./package.json').version")"
|
|
if [ "v$pkg_version" != "$GITHUB_REF_NAME" ]; then
|
|
echo "::error::tag $GITHUB_REF_NAME does not match package.json version v$pkg_version — bump package.json before tagging so the release assets and the npm package agree." >&2
|
|
exit 1
|
|
fi
|
|
echo "tag $GITHUB_REF_NAME matches package.json version v$pkg_version."
|
|
|
|
- name: Package release artifact
|
|
run: go run ./cmd/zero-release package
|
|
|
|
- name: Verify release checksums
|
|
run: go run ./cmd/zero-release verify
|
|
|
|
- name: Upload package
|
|
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
|
|
with:
|
|
name: zero-${{ runner.os }}-${{ runner.arch }}
|
|
path: dist/release/*
|
|
if-no-files-found: error
|
|
|
|
# On a `v*` tag ref (pushed manually, or dispatched at the tag by the
|
|
# release-please workflow), publish the packaged archive + .sha256 as GitHub
|
|
# Release assets so the documented `scripts/install.sh` / `install.ps1` path
|
|
# (which reads /releases/latest) works. Uses the preinstalled `gh` (no new
|
|
# action dependency); each matrix OS creates the release if absent
|
|
# (best-effort — release-please normally creates it first) then uploads its
|
|
# own assets with --clobber. Tags come from merging the release-please PR.
|
|
- name: Publish to GitHub Release
|
|
if: ${{ startsWith(github.ref, 'refs/tags/v') }}
|
|
shell: bash
|
|
env:
|
|
GH_TOKEN: ${{ github.token }}
|
|
run: |
|
|
tag="${GITHUB_REF_NAME}"
|
|
gh release create "$tag" --repo "$GITHUB_REPOSITORY" --title "$tag" --generate-notes 2>/dev/null || true
|
|
gh release upload "$tag" dist/release/* --repo "$GITHUB_REPOSITORY" --clobber
|
|
|
|
# Publishes the npm package named in package.json (@twigpine/zero): first
|
|
# the five platform payloads (versions of the SAME package at suffixed
|
|
# versions like X.Y.Z-linux-x64, carrying the native binary + vendored
|
|
# helpers), then the wrapper at X.Y.Z whose optionalDependencies alias those
|
|
# platform versions. Platform versions MUST publish first — the wrapper pins
|
|
# them exactly, so the reverse order would open a window where installs
|
|
# resolve aliases that 404. See docs/NPM_PACKAGING.md for the full contract.
|
|
#
|
|
# Auth is npm OIDC trusted publishing: no NPM_TOKEN secret. The package's
|
|
# npmjs.com settings must list this repo + workflow file + the npm-publish
|
|
# environment as a trusted publisher, and provenance is then generated
|
|
# automatically. npm checks provenance against package.json `repository`,
|
|
# so that must name this repository. Note npm only allows configuring a
|
|
# trusted publisher on an EXISTING package, so a new package name starts
|
|
# with a one-time manual `npm publish` of an empty 0.0.0 placeholder.
|
|
# Because the platform payloads are versions of the same package, they reuse
|
|
# that single trusted-publisher configuration.
|
|
publish-npm:
|
|
name: Publish npm package
|
|
needs: package
|
|
if: ${{ startsWith(github.ref, 'refs/tags/v') }}
|
|
runs-on: ubuntu-latest
|
|
# Manual approval gate: this environment carries a required-reviewer rule
|
|
# (Settings -> Environments -> npm-publish), so the job pauses until a
|
|
# maintainer approves it in the Actions UI before anything reaches npm.
|
|
environment:
|
|
name: npm-publish
|
|
url: https://www.npmjs.com/package/@twigpine/zero
|
|
permissions:
|
|
contents: read
|
|
id-token: write # OIDC token exchange for npm trusted publishing + provenance
|
|
steps:
|
|
- name: Checkout
|
|
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
|
|
with:
|
|
persist-credentials: false
|
|
|
|
# No registry-url here on purpose: setup-node's registry-url writes an
|
|
# .npmrc line referencing $NODE_AUTH_TOKEN, and npm hard-fails on the
|
|
# unset env var. Trusted publishing needs no token at all.
|
|
- name: Setup Node
|
|
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
|
|
with:
|
|
node-version: 24
|
|
|
|
# OIDC trusted publishing needs npm >= 11.5.1. Use the npm bundled with
|
|
# the pinned Node toolchain (Node 24 ships >= 11.5.1) rather than pulling
|
|
# npm@latest from the registry inside the publish path — an unpinned
|
|
# install there would be avoidable supply-chain exposure. Fail fast if a
|
|
# future runner/toolchain change ever regresses the bundled npm.
|
|
- name: Check npm supports trusted publishing
|
|
run: |
|
|
npm --version | node -e '
|
|
const version = require("fs").readFileSync(0, "utf8").trim();
|
|
const [major, minor, patch] = version.split(".").map(Number);
|
|
const ok =
|
|
major > 11 ||
|
|
(major === 11 && (minor > 5 || (minor === 5 && patch >= 1)));
|
|
if (!ok) {
|
|
console.error(
|
|
`::error::bundled npm ${version} is too old for OIDC trusted ` +
|
|
`publishing (needs >= 11.5.1) — bump node-version in this job.`,
|
|
);
|
|
process.exit(1);
|
|
}
|
|
console.log(`npm ${version} supports trusted publishing.`);
|
|
'
|
|
|
|
# Probe the exact URLs the wrapper's first-run downloader builds
|
|
# (internal/release/release.go asset-name scheme) as an anonymous client
|
|
# — install.sh/install.ps1 and `zero update` read them too. This also
|
|
# blocks publishing while the repo is still private: private release
|
|
# assets 404 without auth, exactly as they would for an installing user.
|
|
- name: Verify release assets are downloadable
|
|
shell: bash
|
|
run: |
|
|
version="$(node -p "require('./package.json').version")"
|
|
base="https://github.com/${GITHUB_REPOSITORY}/releases/download/v${version}"
|
|
for asset in \
|
|
"zero-v${version}-linux-x64.tar.gz" \
|
|
"zero-v${version}-linux-arm64.tar.gz" \
|
|
"zero-v${version}-macos-x64.tar.gz" \
|
|
"zero-v${version}-macos-arm64.tar.gz" \
|
|
"zero-v${version}-windows-x64.zip"; do
|
|
for url in "${base}/${asset}" "${base}/${asset}.sha256"; do
|
|
if ! curl -fsSLI -o /dev/null "$url"; then
|
|
echo "::error::${url} is not publicly downloadable — the wrapper's fallback downloader and install.sh would fail. Is the repo public and are all assets uploaded?" >&2
|
|
exit 1
|
|
fi
|
|
done
|
|
done
|
|
echo "all release assets for v${version} are publicly downloadable."
|
|
|
|
- name: Download release artifacts
|
|
uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0
|
|
with:
|
|
pattern: zero-*
|
|
path: dist/artifacts
|
|
merge-multiple: true
|
|
|
|
- name: Assemble npm packages
|
|
run: node scripts/npm/build-platform-packages.mjs --artifacts-dir dist/artifacts --out-dir dist/npm
|
|
|
|
# Platform payloads first under the `platform` dist-tag, then the wrapper,
|
|
# with the `latest` dist-tag checked before and after (platform versions
|
|
# are semver PRERELEASES of the wrapper version and must never become
|
|
# `latest`). npm applies publishes asynchronously, so the script waits
|
|
# until npm shows every platform version before the wrapper goes out,
|
|
# and until `latest` moves after it. Versions already on the registry
|
|
# are skipped, so "Re-run failed jobs" is safe after a partial publish.
|
|
- name: Publish npm packages
|
|
shell: bash
|
|
run: scripts/npm/publish-packages.sh dist/npm
|