Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 37 additions & 0 deletions .editorconfig
Original file line number Diff line number Diff line change
@@ -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
66 changes: 64 additions & 2 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -38,9 +38,68 @@ 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

# 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

# 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")"
Comment thread
jeremy marked this conversation as resolved.
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'
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:
Expand All @@ -58,9 +117,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

Expand Down
2 changes: 2 additions & 0 deletions .mise.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
[tools]
node = "24.18.0"
Comment thread
jeremy marked this conversation as resolved.
21 changes: 21 additions & 0 deletions LICENSE
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
MIT License

Copyright (c) 37signals LLC
Comment thread
jeremy marked this conversation as resolved.

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.
92 changes: 92 additions & 0 deletions RELEASING.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
# 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.
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 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,
**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
> 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
> 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:

```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.
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):

```bash
mise exec -- npm trust list
```

## Later release (first real OIDC proof)

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

- `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 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`).
2 changes: 1 addition & 1 deletion biome.json
Original file line number Diff line number Diff line change
@@ -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"]
},
Expand Down
Loading
Loading