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
11 changes: 5 additions & 6 deletions .github/workflows/release-config.yml
Original file line number Diff line number Diff line change
Expand Up @@ -102,9 +102,9 @@ jobs:
fi

# The build, gate, and pack steps also run on private-blocked pushes
# (should_release=false, version set) — every config push rehearses the
# plan half of the release train while CLI-2169 hasn't flipped `private`
# yet. The publish half stays unexercised until then.
# (should_release=false, version set): if `private` were ever flipped
# back on, every config push would still rehearse the plan half of the
# release train while the publish half stays parked.
- name: Build @supabase/config
if: steps.plan.outputs.version != ''
run: pnpm exec turbo run @supabase/config#build
Expand Down Expand Up @@ -159,8 +159,7 @@ jobs:
# would run straight through unreviewed. Fail closed here — before a
# real (non-dry) release can reach the publish job — if the rule is
# missing or unreadable. Private-blocked rehearsals (should_release
# false) are unaffected, so this only bites once CLI-2169 flips
# `private`, which is exactly when it must.
# false) are unaffected — this only gates real releases.
- name: Assert the release approval gate is armed
if: steps.plan.outputs.should_release == 'true' && steps.plan.outputs.dry_run != 'true'
env:
Expand Down Expand Up @@ -264,7 +263,7 @@ jobs:
[[ "$(jq -r .name package/package.json)" == "@supabase/config" ]]
[[ "$(jq -r .version package/package.json)" == "${VERSION}" ]]
if [[ "$(jq -r .private package/package.json)" == "true" ]]; then
echo "packages/config is still private: true — flip it under CLI-2169 before publishing." >&2
echo "packages/config is private: true — refusing to publish a private manifest (was it flipped back deliberately?)." >&2
exit 1
fi

Expand Down
2 changes: 2 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,8 @@ Generic linting (`oxlint`), formatting (`oxfmt`), and unused-code analysis (`kni
Expected exceptions:

- `apps/cli` is published, so it is not `private`
- `packages/config` is published (on its own release train — see `packages/config/AGENTS.md`), so
it is not `private`
- `apps/docs` is a Next.js app and does not follow the standard package template
- `packages/cli-*` are binary wrapper packages and do not follow the standard TypeScript workspace template

Expand Down
47 changes: 26 additions & 21 deletions packages/config/AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -156,24 +156,29 @@ elsewhere in the monorepo never releases `@supabase/config`, and vice versa.
- **Local dry runs:** `scripts/release-plan.ts` runs the plan locally without publishing;
`tools/config-release-gate.ts --tarball` rehearses the type-surface gate locally.

### One-time setup (tracked under CLI-2169)

Four things must be settled before the first real publish:

1. The `config-release` GitHub environment needs required reviewers configured in repo settings. An
environment referenced by a workflow is auto-created WITHOUT protection rules — the plan job
asserts the rule exists and refuses to plan a real release until it does, so the first release
attempt fails closed rather than publishing unreviewed.
2. npm trusted publishing must be configured for the package, which requires the package to exist
first. The very first publish is a manual bootstrap — use a granular, single-package,
short-expiry token and revoke it as soon as the trusted publisher is configured (repo
`supabase/cli`, workflow `release-config.yml`, environment `config-release`).
3. Push a baseline `config-v*` tag (e.g. `config-v0.1.0`) on a `develop` commit. This is required,
not optional: with no baseline, semantic-release would cut `1.0.0` with release notes generated
from the entire monorepo history — a whole-history changelog as both the approval artifact and
the public GH release body. `scripts/release-plan.ts` refuses to plan without a baseline tag
(escape hatch: `CONFIG_RELEASE_ALLOW_NO_BASELINE=1`). This is the single exception to the
"never hand-push a `config-v*` tag" rule above.
4. Add a repository tag ruleset protecting `config-v*` (alongside `v*`), restricted to the release
App. The last `config-v*` tag is the version oracle: a stray hand-pushed tag permanently skews
versioning, and a deleted tag makes the next plan re-cut an already-published version.
### Standing release configuration (set up under CLI-2169)

The one-time go-live setup is complete. These are the standing invariants — verify them if a
release fails unexpectedly, and restore them if repo or npm settings are ever rebuilt:

1. **The `config-release` GitHub environment has required reviewers.** An environment referenced
by a workflow is auto-created WITHOUT protection rules — the plan job asserts the rule exists
and refuses to plan a real release without it, so a stripped environment fails closed rather
than publishing unreviewed.
2. **npm trusted publishing is configured** for the package (repo `supabase/cli`, workflow
`release-config.yml`, environment `config-release`); no `NPM_TOKEN` exists anywhere. Trusted
publishing can only be configured on a package that already exists, so the package was seeded
with a manually published `0.0.0` placeholder (no `dist/`), and the bootstrap token was
revoked immediately after.
3. **The baseline tag `config-v0.0.0` matches that placeholder** — the tag oracle and the registry
must always agree on the last released version. With no baseline tag, semantic-release would
cut `1.0.0` with release notes generated from the entire monorepo history — a whole-history
changelog as both the approval artifact and the public GH release body — so
`scripts/release-plan.ts` refuses to plan in that state (escape hatch:
`CONFIG_RELEASE_ALLOW_NO_BASELINE=1`). Seeding it was the single exception to the "never
hand-push a `config-v*` tag" rule above.
4. **The "Protect `config-v*` release tags" ruleset** restricts creating, moving, and deleting
`config-v*` tags to the `supabase-cli-releaser` App (the same App the release workflows mint
tokens from). The last `config-v*` tag is the version oracle: a stray hand-pushed tag
permanently skews versioning, and a deleted tag wedges the next plan on an already-published
version.
3 changes: 1 addition & 2 deletions packages/config/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,8 +30,7 @@ import { getDefaultCliConfig, toProjectConfig } from "@supabase/config";
const projectConfig = toProjectConfig({ cliConfig: getDefaultCliConfig() });
```

This package is not yet published (`private: true`; publishing is tracked separately). Once it
is, install it alongside the peers your runtime needs.
Install it alongside the peers your runtime needs.

This package requires Effect 4.x, currently only published under the `rc` dist-tag — `effect@latest`
still resolves to 3.x, which will not satisfy this package's peer range.
Expand Down
2 changes: 1 addition & 1 deletion packages/config/package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "@supabase/config",
"version": "0.1.0",
"private": true,
"private": false,
"description": "Supabase project configuration schema, parsing, and validation, built on Effect Schema.",
"keywords": [
"config",
Expand Down
6 changes: 3 additions & 3 deletions packages/config/scripts/release-plan.ts
Original file line number Diff line number Diff line change
Expand Up @@ -174,8 +174,8 @@ export function renderStepSummary(plan: ReleasePlan): string {
if (plan.isPrivate) {
lines.push(
"> [!WARNING]",
"> `packages/config` is still `private: true`, so publishing is blocked — flip it under " +
"CLI-2169. This run validated the release pipeline only; nothing will be published.",
"> `packages/config` is `private: true`, so publishing is blocked. This run validated the " +
"release pipeline only; nothing will be published.",
"",
);
}
Expand Down Expand Up @@ -204,7 +204,7 @@ function renderLocalPlan(plan: ReleasePlan): string {
`${PACKAGE_PATH_PREFIX} since the last config-v* tag).`
);
}
const privateNote = plan.isPrivate ? " (blocked: packages/config is still private: true)" : "";
const privateNote = plan.isPrivate ? " (blocked: packages/config is private: true)" : "";
return `[release-plan] @supabase/config would release ${plan.version} (${plan.bumpType})${privateNote}.`;
}

Expand Down
Loading