From 3a47fc6045540d122b6d4e18f4e35aac8a751252 Mon Sep 17 00:00:00 2001 From: Jeremy Daer Date: Wed, 22 Jul 2026 16:56:20 -0700 Subject: [PATCH 1/4] Bump @37signals/basecamp to 0.8.0; freshen dev-setup and release engineering Move the plugin onto SDK 0.8.0 (proven consumer-compatible) plus safe, non-breaking tooling bumps and the metadata/tooling a first publish needs. Dependencies: - @37signals/basecamp 0.7.3 -> 0.8.0 - @sinclair/typebox ^0.34.51 -> ^0.34.52 - @biomejs/biome ^2.5.3 -> 2.5.4 (exact); biome.json $schema re-pinned to 2.5.4 - vitest ^4.0.18 -> ^4.1.10 (lockstep with @vitest/coverage-v8) - Remove the no-op @hono/node-server override (root override never touched openclaw's nested dev-only copy; no 1.x is secure per GHSA-frvp-7c67-39w9, a dev-only exposure deferred to the OpenClaw adaptation) - Hold openclaw (2026.7.1 breaks channel.ts) and typescript (7.x major) Tooling: - .mise.toml pinning node 22.23.1 (satisfies openclaw's >=22.19.0 floor) - .editorconfig matching biome's 2-space formatter - LICENSE (MIT) Publishing: - package.json license/repository/publishConfig for provenance - release.yml publish job -> Node 24 (npm >= 11.5.1 for OIDC trusted publishing) - RELEASING.md documenting the first-publish bootstrap sequence --- .editorconfig | 37 ++++++++++ .github/workflows/release.yml | 5 +- .mise.toml | 2 + LICENSE | 21 ++++++ RELEASING.md | 68 +++++++++++++++++ biome.json | 2 +- package-lock.json | 133 ++++++++++++---------------------- package.json | 19 +++-- 8 files changed, 191 insertions(+), 96 deletions(-) create mode 100644 .editorconfig create mode 100644 .mise.toml create mode 100644 LICENSE create mode 100644 RELEASING.md diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..63a2d56 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,37 @@ +# EditorConfig helps maintain consistent coding styles across IDEs +# https://editorconfig.org + +root = true + +# Default for all files +[*] +charset = utf-8 +end_of_line = lf +insert_final_newline = true +trim_trailing_whitespace = true + +# TypeScript/JavaScript +[*.{ts,tsx,js,jsx,mjs,cjs}] +indent_style = space +indent_size = 2 + +# JSON files +[*.json] +indent_style = space +indent_size = 2 + +# YAML files +[*.{yml,yaml}] +indent_style = space +indent_size = 2 + +# Markdown +[*.md] +indent_style = space +indent_size = 2 +trim_trailing_whitespace = false + +# Shell scripts +[*.sh] +indent_style = space +indent_size = 2 diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 3714154..dcbf6a4 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -58,9 +58,12 @@ jobs: git fetch origin main git merge-base --is-ancestor "$GITHUB_SHA" origin/main + # Node 24 bundles npm 11.16, past the 11.5.1 that OIDC trusted publishing + # requires. Publishing therefore uses the npm shipped in the verified Node + # tarball rather than installing one from the registry at release time. - uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0 # zizmor: ignore[cache-poisoning] -- workflow only triggers on tag push and workflow_dispatch; cache is keyed by lockfile hash and default branch with: - node-version: 22 + node-version: 24 registry-url: https://registry.npmjs.org cache: npm diff --git a/.mise.toml b/.mise.toml new file mode 100644 index 0000000..427d56d --- /dev/null +++ b/.mise.toml @@ -0,0 +1,2 @@ +[tools] +node = "22.23.1" diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..5ab457d --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) Basecamp, LLC + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/RELEASING.md b/RELEASING.md new file mode 100644 index 0000000..1676408 --- /dev/null +++ b/RELEASING.md @@ -0,0 +1,68 @@ +# Releasing `@37signals/openclaw-basecamp` + +This package is **not yet published** — `npm view @37signals/openclaw-basecamp` +returns 404. npm **trusted publishing (OIDC) requires the package to already +exist** before a trusted publisher can be configured, so the first publish is a +one-time manual bootstrap. Everything after it flows through +`.github/workflows/release.yml` on tag push. + +The steps below are grouped by **who does them and when**. Do not collapse them: +the bootstrap publish creates the package but does **not** prove the CI pipeline +works — only the first *subsequent* version published through `release.yml` +does that. + +## In this PR (code changes included here) + +1. Publication metadata in `package.json`: `name`, `version`, `license`, + `repository`, `publishConfig.access = "public"`. +2. Top-level `LICENSE` (MIT). +3. `release.yml` `publish` job pinned to **Node 24** (bundles npm ≥ 11.16, past + the 11.5.1 that OIDC trusted publishing requires). The `test`/`security` jobs + stay on Node 22. + +## Post-merge bootstrap (maintainer / operator — a human, not CI) + +Held under separate authorization. Do **not** run these as part of merging this PR. + +4. **Create and protect the `release` GitHub environment** in repo settings. + `release.yml`'s `publish` job declares `environment: release`; that + environment does not exist yet (only `copilot` is defined), so the job cannot + gate a publish until it is created. +5. **Authorized first publication** by a maintainer from a clean checkout at an + exact reviewed commit SHA, after all gates pass. Run through `mise` so the + pinned dev Node is used: + + ```bash + mise install + mise exec -- npm ci + mise exec -- npm run check + mise exec -- npm run build + mise exec -- bash scripts/verify-pack.sh + mise exec -- npm publish --access public # granular automation token or 2FA + ``` + + This **only creates the package** on npm. It does not validate OIDC. +6. **Configure the npm trusted publisher** (GitHub Actions OIDC): this repo, the + `release.yml` workflow, and the `release` environment. Verify with `npm trust + list` — note it needs **npm ≥ 11.5.1**, which the pinned dev Node (22.23.1, + npm 10.9.8) does not ship, so run it under Node 24: + + ```bash + mise x node@24 -- npm trust list + ``` + +## Later release (first real OIDC proof) + +7. Bump `package.json` `version`, tag `vX.Y.Z`, and push the tag. `release.yml` + builds, runs `scripts/verify-pack.sh`, and publishes via OIDC with + `--provenance`. **This tag-driven publish is the first real proof the OIDC + pipeline works** — a green `workflow_dispatch` dry-run does not exercise + authentication, and the bootstrap publish (step 5) only created the package. + +## Notes + +- `openclaw.plugin.json` also carries a `version` field, kept in sync with + `package.json` manually. `release.yml` only checks `package.json` against the + tag. +- The `publish` job runs on Node 24 for npm ≥ 11.5.1; `test`/`security` stay on + Node 22, matching the runtime floor (`engines.node >= 22.5`). diff --git a/biome.json b/biome.json index 09fd487..eafef82 100644 --- a/biome.json +++ b/biome.json @@ -1,5 +1,5 @@ { - "$schema": "https://biomejs.dev/schemas/2.4.6/schema.json", + "$schema": "https://biomejs.dev/schemas/2.5.4/schema.json", "files": { "includes": ["src/**", "tests/**", "index.ts", "vitest.config.ts"] }, diff --git a/package-lock.json b/package-lock.json index dd49bd3..d6b698c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -7,26 +7,27 @@ "": { "name": "@37signals/openclaw-basecamp", "version": "0.1.0", + "license": "MIT", "dependencies": { - "@37signals/basecamp": "0.7.3", - "@sinclair/typebox": "^0.34.51", + "@37signals/basecamp": "0.8.0", + "@sinclair/typebox": "^0.34.52", "zod": "^4.4.3" }, "devDependencies": { - "@biomejs/biome": "^2.5.3", + "@biomejs/biome": "2.5.4", "@vitest/coverage-v8": "^4.1.10", "openclaw": "2026.6.11", "typescript": "^6.0.3", - "vitest": "^4.0.18" + "vitest": "^4.1.10" }, "engines": { "node": ">=22.5" } }, "node_modules/@37signals/basecamp": { - "version": "0.7.3", - "resolved": "https://registry.npmjs.org/@37signals/basecamp/-/basecamp-0.7.3.tgz", - "integrity": "sha512-UNJpDIbOTR5imXbxtaFe0Uic8ekwZUtkSRYCeJAu87UDRPYcBehjAy99vk1O3s0v2eQgqdWiEB1TavDqv9tBVw==", + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/@37signals/basecamp/-/basecamp-0.8.0.tgz", + "integrity": "sha512-0mwPGXZDOyUC+ZLiDCZiHTyZD+kZnWOtxmdSWh79qXH6rddmmre2HGP6Dmt57WApyZNQoyYVFy9QR2wZuGCr0g==", "license": "MIT", "dependencies": { "openapi-fetch": "^0.17.0" @@ -99,9 +100,9 @@ } }, "node_modules/@biomejs/biome": { - "version": "2.5.3", - "resolved": "https://registry.npmjs.org/@biomejs/biome/-/biome-2.5.3.tgz", - "integrity": "sha512-MrJswFdei9EfDwwUy2tQrPDpK0AO+RmMFvBoaaJ6ayBc3sUbHdCE+XG5N8vp+5So41ZupZJQm0roHFFhMGVD7A==", + "version": "2.5.4", + "resolved": "https://registry.npmjs.org/@biomejs/biome/-/biome-2.5.4.tgz", + "integrity": "sha512-xy5FNE5kQJKyK5MR1gJy6ztXYx4WBAbYGlK04lMEgmyPRWKybY9NFwiG9yo0XdzOU8Xvhj41u034J1ywfoWfMw==", "dev": true, "license": "MIT OR Apache-2.0", "bin": { @@ -115,20 +116,20 @@ "url": "https://opencollective.com/biome" }, "optionalDependencies": { - "@biomejs/cli-darwin-arm64": "2.5.3", - "@biomejs/cli-darwin-x64": "2.5.3", - "@biomejs/cli-linux-arm64": "2.5.3", - "@biomejs/cli-linux-arm64-musl": "2.5.3", - "@biomejs/cli-linux-x64": "2.5.3", - "@biomejs/cli-linux-x64-musl": "2.5.3", - "@biomejs/cli-win32-arm64": "2.5.3", - "@biomejs/cli-win32-x64": "2.5.3" + "@biomejs/cli-darwin-arm64": "2.5.4", + "@biomejs/cli-darwin-x64": "2.5.4", + "@biomejs/cli-linux-arm64": "2.5.4", + "@biomejs/cli-linux-arm64-musl": "2.5.4", + "@biomejs/cli-linux-x64": "2.5.4", + "@biomejs/cli-linux-x64-musl": "2.5.4", + "@biomejs/cli-win32-arm64": "2.5.4", + "@biomejs/cli-win32-x64": "2.5.4" } }, "node_modules/@biomejs/cli-darwin-arm64": { - "version": "2.5.3", - "resolved": "https://registry.npmjs.org/@biomejs/cli-darwin-arm64/-/cli-darwin-arm64-2.5.3.tgz", - "integrity": "sha512-QhYP9muVQ0nUO5zztFuPbEwi4+94sJWVjaZds9aMi1l/KNZBiUjdiSUrGHsTaMGDXrYl+r4AS2sUKfgH3w+V3g==", + "version": "2.5.4", + "resolved": "https://registry.npmjs.org/@biomejs/cli-darwin-arm64/-/cli-darwin-arm64-2.5.4.tgz", + "integrity": "sha512-4o3NFRobXHynkgcFVrlZsoDAFtF2ldlEGN8sORSws5ZQqyY4PXnPUIylu4ksfyHuwkfvDREuWh3JK+niRwGq3w==", "cpu": [ "arm64" ], @@ -143,9 +144,9 @@ } }, "node_modules/@biomejs/cli-darwin-x64": { - "version": "2.5.3", - "resolved": "https://registry.npmjs.org/@biomejs/cli-darwin-x64/-/cli-darwin-x64-2.5.3.tgz", - "integrity": "sha512-NC1Ss13UaW7QZX+y8j44bF7AP0jSJdBl6iRhe0MAkvaSqZy+mWg3GaXsrb+eSoHoGDBtaXWEbMVV0iVN2cZ7cQ==", + "version": "2.5.4", + "resolved": "https://registry.npmjs.org/@biomejs/cli-darwin-x64/-/cli-darwin-x64-2.5.4.tgz", + "integrity": "sha512-D32P5HkU2Y6PySuC/WsVDTOgsDwVFmujzhhhOQjajtATpVWFDXuVd3oRbsWNSEA+aaFzyzZm22szsyydBYlSyQ==", "cpu": [ "x64" ], @@ -160,16 +161,13 @@ } }, "node_modules/@biomejs/cli-linux-arm64": { - "version": "2.5.3", - "resolved": "https://registry.npmjs.org/@biomejs/cli-linux-arm64/-/cli-linux-arm64-2.5.3.tgz", - "integrity": "sha512-ksx1KWeyYW18ILL04msF/J4ZBtBDN33znYK8Z/aNv/vlBVxL9/g3mGP+omgHJKy4+KWbK87vcmmpmurfNjSgiA==", + "version": "2.5.4", + "resolved": "https://registry.npmjs.org/@biomejs/cli-linux-arm64/-/cli-linux-arm64-2.5.4.tgz", + "integrity": "sha512-pSEfW7B8kTsXUjUxC1xVVK+y85Ht3C5XxZ9gclmC7/3Ku9Vqz8jmI7k0p/BNIjQ6t4sFERI2sFeH73ybiZl6YQ==", "cpu": [ "arm64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT OR Apache-2.0", "optional": true, "os": [ @@ -180,16 +178,13 @@ } }, "node_modules/@biomejs/cli-linux-arm64-musl": { - "version": "2.5.3", - "resolved": "https://registry.npmjs.org/@biomejs/cli-linux-arm64-musl/-/cli-linux-arm64-musl-2.5.3.tgz", - "integrity": "sha512-fccix0w6xp6csCXgxeC0dU/3ecgRQal0y+cv2SP9ajNlhe7Yrk2Ug7UDe2j9AT9ZDYitkXpvUKgZjjuoYeP4Vg==", + "version": "2.5.4", + "resolved": "https://registry.npmjs.org/@biomejs/cli-linux-arm64-musl/-/cli-linux-arm64-musl-2.5.4.tgz", + "integrity": "sha512-Rpm5/AT1m+DlJmUoYvS4/vXc+0tXJPJ2NQz25TGPyHVF5JrWy75PE0GH6kVxsKtQDuCH4OgzquZq0R4kj/wCVg==", "cpu": [ "arm64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MIT OR Apache-2.0", "optional": true, "os": [ @@ -200,16 +195,13 @@ } }, "node_modules/@biomejs/cli-linux-x64": { - "version": "2.5.3", - "resolved": "https://registry.npmjs.org/@biomejs/cli-linux-x64/-/cli-linux-x64-2.5.3.tgz", - "integrity": "sha512-yMkJtilsgvILDcVkh187aVLTb64xYsrxYajx5kym+r1ULkO5HUOfu9AYKLGQbOVLwJtT2utNw7hhFNg+17mUYA==", + "version": "2.5.4", + "resolved": "https://registry.npmjs.org/@biomejs/cli-linux-x64/-/cli-linux-x64-2.5.4.tgz", + "integrity": "sha512-FNxojWJkL7EajAuzBgoLe0T2G0y112M4lBrDIFl/DomFTx8yqenYOIdsRLNXvOvBBofE8hJi85LjzLmBDpY7/Q==", "cpu": [ "x64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT OR Apache-2.0", "optional": true, "os": [ @@ -220,16 +212,13 @@ } }, "node_modules/@biomejs/cli-linux-x64-musl": { - "version": "2.5.3", - "resolved": "https://registry.npmjs.org/@biomejs/cli-linux-x64-musl/-/cli-linux-x64-musl-2.5.3.tgz", - "integrity": "sha512-O/yU9YKRUiHhmcjF2f38PSjseVk3G4VLWYc0G2HWpzdBVREV6G8IGWIVEFf7MFPfWIzNUIvPsEjeAZQIOgnLcQ==", + "version": "2.5.4", + "resolved": "https://registry.npmjs.org/@biomejs/cli-linux-x64-musl/-/cli-linux-x64-musl-2.5.4.tgz", + "integrity": "sha512-aby/PohmmgbShcHqFsZVzG8H6D98+P+A6xRWRrQcLW1pCjabcov5UUlke4UqNQBYTkDQav+jB4zyyDDeKB2GaA==", "cpu": [ "x64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MIT OR Apache-2.0", "optional": true, "os": [ @@ -240,9 +229,9 @@ } }, "node_modules/@biomejs/cli-win32-arm64": { - "version": "2.5.3", - "resolved": "https://registry.npmjs.org/@biomejs/cli-win32-arm64/-/cli-win32-arm64-2.5.3.tgz", - "integrity": "sha512-cX5z+GYwRcqEok0AH3KSfQGgqYd0Nomfp6Fbe1uiTtELE38hdH2k842wQ9wLNaF/JJ7r4rjJQ4VR+ce+fRmQbw==", + "version": "2.5.4", + "resolved": "https://registry.npmjs.org/@biomejs/cli-win32-arm64/-/cli-win32-arm64-2.5.4.tgz", + "integrity": "sha512-emoXexPZIPAZkz2RKmA95WJUqK3I5MJNYtwEbL5ESciRzhmFMMyekDhNG8hpeOaK+ZGRDxAU4wvGuA5IHQ0h0w==", "cpu": [ "arm64" ], @@ -257,9 +246,9 @@ } }, "node_modules/@biomejs/cli-win32-x64": { - "version": "2.5.3", - "resolved": "https://registry.npmjs.org/@biomejs/cli-win32-x64/-/cli-win32-x64-2.5.3.tgz", - "integrity": "sha512-ExSaJWi4/u6+GXCszlSKpWSjKNbDseAYqqkCznsCsZ/4uidZ/BEqsCc5/3ctlq6dfIubdIIRSVLC/PG9xPl70Q==", + "version": "2.5.4", + "resolved": "https://registry.npmjs.org/@biomejs/cli-win32-x64/-/cli-win32-x64-2.5.4.tgz", + "integrity": "sha512-U1jaluLw1qQc2Tx7/CeSoL9N5XcqIH+GWjpUAy1ouB5nVjSCMNO+NNHdY3RAs8zxNurLWAdj6pehQdCA2zyU+Q==", "cpu": [ "x64" ], @@ -457,9 +446,6 @@ "arm64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -477,9 +463,6 @@ "arm64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -497,9 +480,6 @@ "ppc64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -517,9 +497,6 @@ "s390x" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -537,9 +514,6 @@ "x64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -557,9 +531,6 @@ "x64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -647,9 +618,9 @@ "license": "MIT" }, "node_modules/@sinclair/typebox": { - "version": "0.34.51", - "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.34.51.tgz", - "integrity": "sha512-Nu4sNPT4G3QgAvxmdUCR/NBmsi23F2tEIcbMOZJVHS+qEgk+rmXxQikEeoKFyaX+UEggAoTvHUvIlj8MfYPzXQ==", + "version": "0.34.52", + "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.34.52.tgz", + "integrity": "sha512-XiMQh7qqVlxZzcVD+kkGMNGMzcTrDMLWI7S4x7z1MkCkbDPrekpZXEUK0eZqZFMuHQg2a2DZOcDIh9o5v3Gonw==", "license": "MIT" }, "node_modules/@standard-schema/spec": { @@ -1154,9 +1125,6 @@ "arm64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MPL-2.0", "optional": true, "os": [ @@ -1178,9 +1146,6 @@ "arm64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MPL-2.0", "optional": true, "os": [ @@ -1202,9 +1167,6 @@ "x64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MPL-2.0", "optional": true, "os": [ @@ -1226,9 +1188,6 @@ "x64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MPL-2.0", "optional": true, "os": [ diff --git a/package.json b/package.json index a462d35..6ab1a1a 100644 --- a/package.json +++ b/package.json @@ -2,6 +2,14 @@ "name": "@37signals/openclaw-basecamp", "version": "0.1.0", "description": "OpenClaw Basecamp channel plugin — Campfire, cards, todos, check-ins, pings", + "license": "MIT", + "repository": { + "type": "git", + "url": "git+https://github.com/basecamp/openclaw-basecamp.git" + }, + "publishConfig": { + "access": "public" + }, "type": "module", "main": "./dist/index.js", "types": "./dist/index.d.ts", @@ -30,11 +38,11 @@ "lint:fix": "biome check --write ." }, "devDependencies": { - "@biomejs/biome": "^2.5.3", + "@biomejs/biome": "2.5.4", "@vitest/coverage-v8": "^4.1.10", "openclaw": "2026.6.11", "typescript": "^6.0.3", - "vitest": "^4.0.18" + "vitest": "^4.1.10" }, "openclaw": { "extensions": [ @@ -54,11 +62,8 @@ } }, "dependencies": { - "@37signals/basecamp": "0.7.3", - "@sinclair/typebox": "^0.34.51", + "@37signals/basecamp": "0.8.0", + "@sinclair/typebox": "^0.34.52", "zod": "^4.4.3" - }, - "overrides": { - "@hono/node-server": "1.19.10" } } From c153f83368189cef22fe370e65467e5969a0f119 Mon Sep 17 00:00:00 2001 From: Jeremy Daer Date: Wed, 22 Jul 2026 17:12:24 -0700 Subject: [PATCH 2/4] Address PR review: 37signals LLC license holder, Node 24 dev toolchain, release-env precondition - LICENSE: copyright holder -> 37signals LLC - .mise.toml: dev Node 24.18.0 (latest LTS), aligning with the publish job; full check + build + pack re-verified green on Node 24 (npm 11.16.0) - RELEASING.md: npm trust list runs on the dev Node directly (npm 11.16); document that a missing release environment is auto-created UNPROTECTED, so creating+protecting it is a hard precondition for pushing any v* tag --- .mise.toml | 2 +- LICENSE | 2 +- RELEASING.md | 29 +++++++++++++++++++---------- 3 files changed, 21 insertions(+), 12 deletions(-) diff --git a/.mise.toml b/.mise.toml index 427d56d..d6bd41f 100644 --- a/.mise.toml +++ b/.mise.toml @@ -1,2 +1,2 @@ [tools] -node = "22.23.1" +node = "24.18.0" diff --git a/LICENSE b/LICENSE index 5ab457d..18b3418 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ MIT License -Copyright (c) Basecamp, LLC +Copyright (c) 37signals LLC Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/RELEASING.md b/RELEASING.md index 1676408..db08920 100644 --- a/RELEASING.md +++ b/RELEASING.md @@ -24,10 +24,18 @@ does that. Held under separate authorization. Do **not** run these as part of merging this PR. -4. **Create and protect the `release` GitHub environment** in repo settings. - `release.yml`'s `publish` job declares `environment: release`; that - environment does not exist yet (only `copilot` is defined), so the job cannot - gate a publish until it is created. +4. **Create and protect the `release` GitHub environment** in repo settings, + with required reviewers / a deployment branch rule, **before any `v*` tag is + pushed**. + + > ⚠️ A missing environment does **not** block the job. GitHub auto-creates a + > referenced-but-missing environment on first use **without protection rules + > or secrets** ([docs](https://docs.github.com/actions/deployment/targeting-different-environments/using-environments-for-deployment)). + > So a `v*` tag pushed before this step completes would run the publish job + > against an unprotected `release` environment — the absent environment is + > **not** a gate. Treat "environment created and protected" as a hard + > precondition for pushing any release tag, and do not push tags until + > steps 4–6 are done. 5. **Authorized first publication** by a maintainer from a clean checkout at an exact reviewed commit SHA, after all gates pass. Run through `mise` so the pinned dev Node is used: @@ -43,12 +51,11 @@ Held under separate authorization. Do **not** run these as part of merging this This **only creates the package** on npm. It does not validate OIDC. 6. **Configure the npm trusted publisher** (GitHub Actions OIDC): this repo, the - `release.yml` workflow, and the `release` environment. Verify with `npm trust - list` — note it needs **npm ≥ 11.5.1**, which the pinned dev Node (22.23.1, - npm 10.9.8) does not ship, so run it under Node 24: + `release.yml` workflow, and the `release` environment. Verify with (needs + **npm ≥ 11.5.1**, which the pinned dev Node ships): ```bash - mise x node@24 -- npm trust list + mise exec -- npm trust list ``` ## Later release (first real OIDC proof) @@ -64,5 +71,7 @@ Held under separate authorization. Do **not** run these as part of merging this - `openclaw.plugin.json` also carries a `version` field, kept in sync with `package.json` manually. `release.yml` only checks `package.json` against the tag. -- The `publish` job runs on Node 24 for npm ≥ 11.5.1; `test`/`security` stay on - Node 22, matching the runtime floor (`engines.node >= 22.5`). +- The dev toolchain (`.mise.toml`) and the `publish` job both run on Node 24 + (latest LTS; bundles npm ≥ 11.16 for OIDC trusted publishing). The CI + `test`/`security` jobs stay on Node 22 to exercise the package's runtime floor + (`engines.node >= 22.5`). From 63b1804d28b155abe950c3bd2becacb72c915868 Mon Sep 17 00:00:00 2001 From: Jeremy Daer Date: Wed, 22 Jul 2026 17:29:15 -0700 Subject: [PATCH 3/4] Enforce release-environment protection with a fail-closed preflight job Documentation alone does not make the release hold fail closed: GitHub auto-creates a referenced-but-missing environment WITHOUT protection rules, so `environment: release` on the publish job is not a gate. Add a release-preflight job that runs before publish (in publish.needs), reads the release environment via the API (actions: read, no id-token), and fails the release on a tag push unless the environment exists with at least one required reviewer and a deployment policy. Dry-runs skip the check. The OIDC-capable publish job never starts until protection is proven. RELEASING.md: document the enforced gate; specify 'at least one required reviewer and a v* tag deployment policy' rather than the ambiguous earlier wording; renumber steps. --- .github/workflows/release.yml | 44 ++++++++++++++++++++++++++++++++++- RELEASING.md | 40 ++++++++++++++++++------------- 2 files changed, 67 insertions(+), 17 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index dcbf6a4..d652ffa 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -38,9 +38,51 @@ jobs: - run: npm test - run: npm run build + # Fail the release closed if the `release` environment is not configured to + # gate publishing. GitHub auto-creates a referenced-but-missing environment + # WITHOUT protection rules, so `environment: release` on the publish job is not + # itself a gate. This job runs before the OIDC-capable publish job and never + # requests id-token, so a misconfigured environment blocks publication before + # any credential is minted. Skipped for workflow_dispatch dry-runs, which do + # not publish. + release-preflight: + name: Verify release environment is protected + runs-on: ubuntu-latest + permissions: + actions: read + steps: + # Only tag pushes publish. Enforce the gate there; leave the job a no-op + # for workflow_dispatch dry-runs so publish's `needs` is still satisfied. + - name: Require a protected release environment + if: github.event_name == 'push' + env: + GH_TOKEN: ${{ github.token }} + run: | + if ! env_json="$(gh api "repos/${GITHUB_REPOSITORY}/environments/release" 2>/dev/null)"; then + echo "::error::The 'release' environment does not exist or is not readable. Create and protect it before tagging a release (see RELEASING.md)." + exit 1 + fi + + reviewers="$(jq '[.protection_rules[]? | select(.type == "required_reviewers") | .reviewers[]?] | length' <<<"$env_json")" + if [ "${reviewers:-0}" -lt 1 ]; then + echo "::error::The 'release' environment has no required reviewers. Add at least one required reviewer (see RELEASING.md)." + exit 1 + fi + + if [ "$(jq '.deployment_branch_policy == null' <<<"$env_json")" = "true" ]; then + echo "::error::The 'release' environment has no deployment branch/tag policy. Restrict deployments to a v* tag policy (see RELEASING.md)." + exit 1 + fi + + echo "release environment is protected: ${reviewers} required reviewer(s), deployment policy present." + + - name: Dry-run note + if: github.event_name != 'push' + run: echo "::notice::workflow_dispatch dry-run — skipping release-environment protection check." + publish: name: Publish to npm - needs: [security, test] + needs: [security, test, release-preflight] runs-on: ubuntu-latest environment: release permissions: diff --git a/RELEASING.md b/RELEASING.md index db08920..03ba78a 100644 --- a/RELEASING.md +++ b/RELEASING.md @@ -19,24 +19,31 @@ does that. 3. `release.yml` `publish` job pinned to **Node 24** (bundles npm ≥ 11.16, past the 11.5.1 that OIDC trusted publishing requires). The `test`/`security` jobs stay on Node 22. +4. `release.yml` `release-preflight` job — a hard, fail-closed gate that runs + before `publish` (and is in `publish.needs`). On a tag push it reads the + `release` environment via the API (`actions: read`, no `id-token`) and fails + the release if the environment is missing, has no required reviewer, or has + no deployment policy — so the OIDC-capable publish job never starts against + an unprotected environment. Dry-runs (`workflow_dispatch`) skip the check. ## Post-merge bootstrap (maintainer / operator — a human, not CI) Held under separate authorization. Do **not** run these as part of merging this PR. -4. **Create and protect the `release` GitHub environment** in repo settings, - with required reviewers / a deployment branch rule, **before any `v*` tag is - pushed**. +5. **Create and protect the `release` GitHub environment** in repo settings, + with **at least one required reviewer** and a **deployment policy restricted + to `v*` tags**, **before any `v*` tag is pushed**. > ⚠️ A missing environment does **not** block the job. GitHub auto-creates a > referenced-but-missing environment on first use **without protection rules - > or secrets** ([docs](https://docs.github.com/actions/deployment/targeting-different-environments/using-environments-for-deployment)). - > So a `v*` tag pushed before this step completes would run the publish job - > against an unprotected `release` environment — the absent environment is - > **not** a gate. Treat "environment created and protected" as a hard - > precondition for pushing any release tag, and do not push tags until - > steps 4–6 are done. -5. **Authorized first publication** by a maintainer from a clean checkout at an + > or secrets** ([docs](https://docs.github.com/actions/deployment/targeting-different-environments/using-environments-for-deployment)), + > so `environment: release` on the publish job is not itself a gate. The + > `release-preflight` job (step 4) enforces this: on a tag push it fails the + > release closed unless the environment exists with a required reviewer and a + > deployment policy, before any credential is minted. Configuring the + > environment as described here is what makes that gate pass — do not push a + > `v*` tag until steps 5–7 are done. +6. **Authorized first publication** by a maintainer from a clean checkout at an exact reviewed commit SHA, after all gates pass. Run through `mise` so the pinned dev Node is used: @@ -50,7 +57,7 @@ Held under separate authorization. Do **not** run these as part of merging this ``` This **only creates the package** on npm. It does not validate OIDC. -6. **Configure the npm trusted publisher** (GitHub Actions OIDC): this repo, the +7. **Configure the npm trusted publisher** (GitHub Actions OIDC): this repo, the `release.yml` workflow, and the `release` environment. Verify with (needs **npm ≥ 11.5.1**, which the pinned dev Node ships): @@ -60,11 +67,12 @@ Held under separate authorization. Do **not** run these as part of merging this ## Later release (first real OIDC proof) -7. Bump `package.json` `version`, tag `vX.Y.Z`, and push the tag. `release.yml` - builds, runs `scripts/verify-pack.sh`, and publishes via OIDC with - `--provenance`. **This tag-driven publish is the first real proof the OIDC - pipeline works** — a green `workflow_dispatch` dry-run does not exercise - authentication, and the bootstrap publish (step 5) only created the package. +8. Bump `package.json` `version`, tag `vX.Y.Z`, and push the tag. `release.yml` + runs `release-preflight`, builds, runs `scripts/verify-pack.sh`, and + publishes via OIDC with `--provenance`. **This tag-driven publish is the + first real proof the OIDC pipeline works** — a green `workflow_dispatch` + dry-run does not exercise authentication, and the bootstrap publish (step 6) + only created the package. ## Notes From f421b4fa65340a3e58443e12db5eb31f9b705d62 Mon Sep 17 00:00:00 2001 From: Jeremy Daer Date: Wed, 22 Jul 2026 17:36:04 -0700 Subject: [PATCH 4/4] Preflight: enforce a custom tag deployment rule, not just any policy The earlier check only asserted deployment_branch_policy != null, which the error text and docs over-claimed as a 'v* tag policy'. A null-check passes a policy that allows any ref, and even passes a protected-branches-only policy that would actually block the tag deploy. Tighten it: require custom_branch_policies == true, then read the deployment-branch-policies API and require at least one type: tag rule. Message and RELEASING.md now state exactly what is enforced (a custom tag rule exists) versus the recommended pattern (v*), which the operator sets. --- .github/workflows/release.yml | 23 ++++++++++++++++++++--- RELEASING.md | 21 ++++++++++++++------- 2 files changed, 34 insertions(+), 10 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index d652ffa..4f74610 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -69,12 +69,29 @@ jobs: exit 1 fi - if [ "$(jq '.deployment_branch_policy == null' <<<"$env_json")" = "true" ]; then - echo "::error::The 'release' environment has no deployment branch/tag policy. Restrict deployments to a v* tag policy (see RELEASING.md)." + # Require a *custom* deployment policy (specific selected patterns). + # null => any ref may deploy; protected_branches-only can't match a tag + # and would silently block the release. Only custom_branch_policies + # supports the tag rule this tag-triggered workflow needs. + policy="$(jq -r '.deployment_branch_policy | if . == null then "none" elif .custom_branch_policies == true then "custom" else "protected-branches" end' <<<"$env_json")" + if [ "$policy" != "custom" ]; then + echo "::error::The 'release' environment must restrict deployments to selected tags via a custom deployment policy (Settings → Environments → release → Deployment branches and tags → 'Selected branches and tags'); found: ${policy}. See RELEASING.md." exit 1 fi - echo "release environment is protected: ${reviewers} required reviewer(s), deployment policy present." + # Verify at least one *tag* rule exists, so only release tags can + # deploy — not an arbitrary branch pattern. + if ! policies_json="$(gh api "repos/${GITHUB_REPOSITORY}/environments/release/deployment-branch-policies" 2>/dev/null)"; then + echo "::error::Could not read the 'release' environment's deployment policies. See RELEASING.md." + exit 1 + fi + tag_rules="$(jq '[.branch_policies[]? | select(.type == "tag")] | length' <<<"$policies_json")" + if [ "${tag_rules:-0}" -lt 1 ]; then + echo "::error::The 'release' environment has no tag deployment rule. Add a tag rule (set it to 'v*') so only release tags can deploy. See RELEASING.md." + exit 1 + fi + + echo "release environment is protected: ${reviewers} required reviewer(s), ${tag_rules} tag deployment rule(s)." - name: Dry-run note if: github.event_name != 'push' diff --git a/RELEASING.md b/RELEASING.md index 03ba78a..415a90c 100644 --- a/RELEASING.md +++ b/RELEASING.md @@ -23,16 +23,21 @@ does that. before `publish` (and is in `publish.needs`). On a tag push it reads the `release` environment via the API (`actions: read`, no `id-token`) and fails the release if the environment is missing, has no required reviewer, or has - no deployment policy — so the OIDC-capable publish job never starts against - an unprotected environment. Dry-runs (`workflow_dispatch`) skip the check. + no custom **tag** deployment rule (it reads the deployment-branch-policies + API and requires at least one `type: tag` rule — a `null` or + protected-branches-only policy fails) — so the OIDC-capable publish job never + starts against an unprotected environment. Dry-runs (`workflow_dispatch`) + skip the check. ## Post-merge bootstrap (maintainer / operator — a human, not CI) Held under separate authorization. Do **not** run these as part of merging this PR. 5. **Create and protect the `release` GitHub environment** in repo settings, - with **at least one required reviewer** and a **deployment policy restricted - to `v*` tags**, **before any `v*` tag is pushed**. + **before any `v*` tag is pushed**, with: + - **at least one required reviewer**, and + - **"Selected branches and tags"** (a custom deployment policy) with a **tag + rule set to `v*`** — so only release tags can deploy. > ⚠️ A missing environment does **not** block the job. GitHub auto-creates a > referenced-but-missing environment on first use **without protection rules @@ -40,9 +45,11 @@ Held under separate authorization. Do **not** run these as part of merging this > so `environment: release` on the publish job is not itself a gate. The > `release-preflight` job (step 4) enforces this: on a tag push it fails the > release closed unless the environment exists with a required reviewer and a - > deployment policy, before any credential is minted. Configuring the - > environment as described here is what makes that gate pass — do not push a - > `v*` tag until steps 5–7 are done. + > custom tag deployment rule, before any credential is minted. The automation + > requires a tag rule to exist; setting its pattern to `v*` (rather than a + > broader glob) is the recommended value. Configuring the environment as + > described here is what makes that gate pass — do not push a `v*` tag until + > steps 5–7 are done. 6. **Authorized first publication** by a maintainer from a clean checkout at an exact reviewed commit SHA, after all gates pass. Run through `mise` so the pinned dev Node is used: