diff --git a/.github/workflows/cli-release.yaml b/.github/workflows/cli-release.yaml index e36bec7f..fb88f0a8 100644 --- a/.github/workflows/cli-release.yaml +++ b/.github/workflows/cli-release.yaml @@ -62,3 +62,99 @@ jobs: done env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + + # Its own job, after the GitHub release, so the release never waits on + # npm and a failed publish can be re-run alone. + npm: + name: Publish to npm + needs: releaseandpublish + runs-on: ubuntu-latest + permissions: + contents: read + steps: + - name: ⬇️ Checkout repo + uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 + with: + persist-credentials: false + + - name: ⎔ Setup bun + uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2.2.0 + + - name: 📥 Download deps + run: bash "$GITHUB_WORKSPACE/.github/scripts/bun-install.sh" + working-directory: cli + + # Packing builds the bundle (prepack). A prerelease goes to the + # alpha dist-tag and a release to latest. npm gives a package's + # first version latest whatever its tag, and latest can't be + # removed, so until a stable release exists latest follows the + # newest prerelease instead of staying on the first one. What + # latest is gets read before publishing, and a failed read stops + # the step: it never counts as "no stable release yet". The step + # checks the registry's dist-tags before it calls it done. + - name: 📦 Publish to npm + working-directory: cli + run: | + if [ -z "${NPM_CONFIG_TOKEN:-}" ]; then + echo "::error::The NPM_TOKEN repository secret is not set" + exit 1 + fi + VERSION="$GITHUB_REF_NAME" + TAGS_URL="https://registry.npmjs.org/-/package/allthings/dist-tags" + # The dist-tags as JSON, {} if the package doesn't exist + # (404); any other answer is retried, then fails. + tags() { + local out code + for _ in 1 2 3 4 5 6; do + if out=$(curl -sS -w '\n%{http_code}' "$TAGS_URL"); then + code="${out##*$'\n'}" + case "$code" in + 200) printf '%s\n' "${out%$'\n'*}"; return 0 ;; + 404) echo '{}'; return 0 ;; + esac + fi + sleep 10 + done + echo "::error::Could not read npm dist-tags" >&2 + return 1 + } + # Its own assignment, so a failed read stops the step. + BEFORE=$(tags) + LATEST=$(jq -r '.latest // ""' <<<"$BEFORE") + if [[ "$VERSION" == *-* ]]; then + bun publish --access public --tag alpha + WANT_ALPHA="$VERSION" + if [[ -z "$LATEST" || "$LATEST" == *-* ]]; then + # Retried: the new version may not be visible yet. + for _ in 1 2 3 4 5 6; do + if curl -fsS -X PUT "$TAGS_URL/latest" \ + -H "authorization: Bearer $NPM_CONFIG_TOKEN" \ + -H "content-type: application/json" \ + --data "\"$VERSION\"" > /dev/null; then + break + fi + sleep 10 + done + WANT_LATEST="$VERSION" + else + WANT_LATEST="$LATEST" + fi + else + bun publish --access public --tag latest + WANT_LATEST="$VERSION" + WANT_ALPHA="" + fi + # The registry may take a moment to show a new version. + for _ in 1 2 3 4 5 6; do + TAGS=$(tags) + if [[ "$(jq -r '.latest // ""' <<<"$TAGS")" == "$WANT_LATEST" ]] && + [[ -z "$WANT_ALPHA" || "$(jq -r '.alpha // ""' <<<"$TAGS")" == "$WANT_ALPHA" ]]; then + echo "npm dist-tags: $TAGS" + exit 0 + fi + sleep 10 + done + echo "::error::npm dist-tags are $TAGS, not latest=$WANT_LATEST${WANT_ALPHA:+ alpha=$WANT_ALPHA}" + exit 1 + env: + NPM_CONFIG_TOKEN: ${{ secrets.NPM_TOKEN }} diff --git a/.github/workflows/cli.yaml b/.github/workflows/cli.yaml index 9cd7bc7e..dafdee69 100644 --- a/.github/workflows/cli.yaml +++ b/.github/workflows/cli.yaml @@ -68,3 +68,18 @@ jobs: # The old name still runs, with its note on stderr. ln -s allthings atw ./atw --version 2>&1 >/dev/null | grep -q "atw is now allthings" + + # What npm would get: the packed bundle, installed and run as bunx runs it. + - name: 📦 Pack for npm and run it with bunx + working-directory: cli + run: | + pack="$RUNNER_TEMP/pack" + bun pm pack --destination "$pack" + try="$RUNNER_TEMP/try" + mkdir -p "$try" + cd "$try" + echo '{"name":"try","private":true}' > package.json + bun add "$pack"/allthings-*.tgz + bunx allthings --version + bunx allthings --help + bunx atw --version 2>&1 >/dev/null | grep -q "atw is now allthings" diff --git a/cli/README.md b/cli/README.md index 2b0999ac..5e95ea58 100644 --- a/cli/README.md +++ b/cli/README.md @@ -12,6 +12,12 @@ That installs the latest stable release in `~/.allthings/bin`. `allthings` 2 is curl -fsSL https://allthingsweb-dev.github.io/allthingsweb/install.bash | ALLTHINGS_VERSION=2.0.0-alpha.2 bash ``` +It is also on npm as `allthings`, for Bun. Prereleases carry the `alpha` dist-tag: + +```sh +bunx allthings@alpha events +``` + ## Use ```sh @@ -41,4 +47,6 @@ bun run src/index.ts events `src/config.ts` holds the site it talks to, `siteOrigin`. It stays on allthingsweb.dev until allthings.dev serves the new site: until then allthings.dev only redirects, and a redirect drops an MCP POST. It flips with the domain move (`infra/docs/r2-migration.md`, Phase 4). -Releases are GitHub releases, cut by pushing a tag that matches `version` in `package.json` (`.github/workflows/cli-release.yaml`). A hyphenated tag (`2.0.0-alpha.2`) is a prerelease, and the installer skips it unless asked. Nothing is published to npm: the package is private. +Releases are GitHub releases, cut by pushing an annotated tag that matches `version` in `package.json` (`.github/workflows/cli-release.yaml`). A hyphenated tag (`2.0.0-alpha.2`) is a prerelease, and the installer skips it unless asked. + +The same workflow then publishes the package to npm with `bun publish`, after the binaries. `bun run build` (which packing runs) bundles `src/index.ts` and `src/atw.ts` for Bun into `dist/`, and the package ships only that, with zod inside. A prerelease gets the `alpha` dist-tag, a release `latest`. npm gives a package's first version `latest` whatever its tag, and `latest` can't be removed, so until there is a stable release `latest` follows the newest prerelease. The step then checks the registry's dist-tags. Publishing uses the `NPM_TOKEN` repository secret. diff --git a/cli/bun.lock b/cli/bun.lock index 2b8e8187..db593a7d 100644 --- a/cli/bun.lock +++ b/cli/bun.lock @@ -4,13 +4,11 @@ "workspaces": { "": { "name": "allthings", - "dependencies": { - "zod": "^4.6.5", - }, "devDependencies": { "@types/bun": "^1.4.2", "prettier": "^3.6.2", "typescript": "7.0.2", + "zod": "^4.6.5", }, }, }, diff --git a/cli/package.json b/cli/package.json index f457cb0a..a780c371 100644 --- a/cli/package.json +++ b/cli/package.json @@ -2,24 +2,44 @@ "name": "allthings", "version": "2.0.0-alpha.2", "description": "all things from your terminal and your agents: the evenings, who is on stage and how to get in.", + "keywords": [ + "allthings", + "events", + "meetups", + "san francisco", + "mcp", + "cli" + ], + "homepage": "https://allthings.dev", + "repository": { + "type": "git", + "url": "git+https://github.com/allthingsweb-dev/allthingsweb.git", + "directory": "cli" + }, + "bugs": "https://github.com/allthingsweb-dev/allthingsweb/issues", "type": "module", - "private": true, "bin": { - "allthings": "./src/index.ts", - "atw": "./src/atw.ts" + "allthings": "./dist/index.js", + "atw": "./dist/atw.js" + }, + "files": [ + "dist" + ], + "engines": { + "bun": ">=1.3" }, "scripts": { + "build": "rm -rf dist && bun build src/index.ts src/atw.ts --target=bun --minify --splitting --outdir dist", + "prepack": "bun run build", "fmt": "prettier --write .", "check": "prettier --check .", "typecheck": "tsc --noEmit", "test": "bun test" }, - "dependencies": { - "zod": "^4.6.5" - }, "devDependencies": { "@types/bun": "^1.4.2", "prettier": "^3.6.2", - "typescript": "7.0.2" + "typescript": "7.0.2", + "zod": "^4.6.5" } } diff --git a/cli/tests/config.test.ts b/cli/tests/config.test.ts index 2342d574..eb15674b 100644 --- a/cli/tests/config.test.ts +++ b/cli/tests/config.test.ts @@ -52,11 +52,16 @@ describe("config", () => { } }); - test("the package installs allthings, with atw as its alias", () => { + test("the package installs allthings, with atw as its alias, from its bundle", () => { expect(packageJson.name).toBe("allthings"); expect(packageJson.bin).toEqual({ - allthings: "./src/index.ts", - atw: "./src/atw.ts", + allthings: "./dist/index.js", + atw: "./dist/atw.js", }); + // npm gets the bundle alone, which needs nothing installed beside it. + expect(packageJson.files).toEqual(["dist"]); + expect(packageJson).not.toHaveProperty("dependencies"); + // bun publish refuses a private package. + expect(packageJson).not.toHaveProperty("private"); }); });