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
6 changes: 3 additions & 3 deletions .github/scripts/release-pipeline.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1070,9 +1070,9 @@ export function stablePublishVerdict(version: string, packument: Packument | nul
};
}
const latest = packument["dist-tags"].latest;
// Until the first release, latest names a pre-release: a packument always carries that key (npm/registry
// REGISTRY-API.md, "dist-tags: an object with at least one key, latest"), so the first publish took it whatever
// --tag asked for. A release must take latest over from it, so only a newer RELEASE holds one back.
// Until the first stable release, latest names whatever the first publish was (the 0.0.0 placeholder, or a
// pre-release): a packument always carries that key (npm/registry REGISTRY-API.md, "dist-tags: an object with at
// least one key, latest"), whatever --tag asked for. A release must take latest over, so only a newer RELEASE holds one back.
const held = latest === undefined ? null : parseMinted(latest);
if (held?.sha7 === null && newerRelease(held.release, parseMinted(version).release)) {
return {
Expand Down
9 changes: 4 additions & 5 deletions .github/workflows/auto-fix.yml
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,8 @@
# Dependabot-triggered runs read the Dependabot secrets store, not the Actions one, so REPO_PLATFORM_TOKEN goes in
# both. A push made with github.token starts no workflows.
# no token, install succeeds -> the fix lands but CI does not re-run; the job posts a PR comment and a warning (close/reopen the PR)
# no token, Dependabot bun PR -> the stale lockfile fails the frozen install here until dependabot-bun-lockfile.yml's
# dedupe push retriggers this workflow, which only a real token does
# no token, Dependabot bun PR -> a bun.lock the Bun updater left stale fails the frozen install here; a close/reopen
# re-runs it against the same stale lockfile; only a push made with a real token brings a fresh one

name: Auto Fix

Expand Down Expand Up @@ -153,9 +153,8 @@ jobs:
PRUNED: ${{ needs.build.outputs.pruned }}
shell: bash
run: |
# The branch can move between the build and this push (a Dependabot rebase, or dependabot-bun-lockfile.yml's
# dedupe commit, which also invalidates the fix): the patch belongs to HEAD_SHA only, and the newer head's
# own run owns the rebuild.
# The branch can move between the build and this push (a Dependabot rebase): the patch belongs to
# HEAD_SHA only, and the newer head's own run owns the rebuild.
superseded() {
git fetch --quiet origin "$HEAD_REF"
[ "$(git rev-parse "origin/$HEAD_REF")" != "$HEAD_SHA" ]
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/update-release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -170,7 +170,7 @@ jobs:
# registry: a version already there (a rerun) or a latest that is a
# newer release (an older release's job rerun after a newer release)
# publishes nothing, so no release ever displaces a newer one from
# latest (a pre-release there, the bootstrap's, is taken over). GITHUB_SHA on the
# latest (the first publish's placeholder or pre-release there is taken over). GITHUB_SHA on the
# publish line sets only the SLSA predicate's source digest; the
# sigstore certificate still carries this run's own head, so a release
# recovered by a newer push's run has provenance naming two commits.
Expand Down
18 changes: 9 additions & 9 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,29 +32,29 @@ GitHub Settings as Code: GitHub Action applying declarative repository settings:
<!-- Add project-specific instructions below the END marker; they are this repository's own and survive every sync. -->
<!-- END REPO-PLATFORM MANAGED -->

Code is the source of truth: this section holds only the rules and the decisions a reader could not recover from the code.
Code is the source of truth: this section holds only the rules and the decisions a reader could not recover from the code. The mechanics (toolchain, generated output, tests, releases) are in CONTRIBUTING.md.

### Hard rules

- Generated artifacts (`lib/settings.schema.json`, `src/upstream-gaps/index.ts`, the generated docs and `action.yml` regions) are regenerated, never hand-edited; `.github/scripts/generated.ts` lists them and `bun run build:check` fails on drift.
- `lib/index.js` (the action bundle) and `lib/pkg/` (the npm library) are built, never committed on main; the packaged commits off main carry them.
- Generated artifacts are regenerated, never hand-edited; `.github/scripts/generated.ts` is the one list of them.
- `lib/index.js` and `lib/pkg/` are built, never committed on main.
- Every GitHub list call goes through `listAll()` or `listAllEnveloped()`, and every API error through `call()`/`failureFor()`, so the permission policy holds (`src/sections/contract/requests.ts`).
- What can be known wrong from the settings file alone is refused when the file is parsed, naming the key and the fix, never discovered at apply time: GET-only fields, enum violations, contradictory key pairs, unknown keys in a closed GitHub shape. Open passthrough shapes keep unknown keys and note them at check time when GitHub does not echo them back.
- The import layering of `src/` is declared in `architecture.yml`; a new cross-layer import is a deliberate edit to that file.
- A type a section module exposes is exported from its home module, or the bundled declarations cannot reach it and the package-smoke job fails.
- New sections and endpoints ship with e2e scenarios, and `bun run test:e2e` runs green before they land.
- New sections and endpoints ship with e2e scenarios.
- Errors are values: `throw` only for `BUG:` invariants (programming errors no user can cause), a bare rethrow in its `catch`, and where a third party's contract demands it. The Biome plugin `lint/never-throw.grit` enforces it.
- No backward-compatibility shims: a change that breaks an input, key, format, or behavior ships the break behind a major with a loud error naming the fix; a one-shot migration only when many files must move at once. A shim that stays anyway carries a `COMPAT(vN)` marker naming the major that deletes it.
- No backward-compatibility shims: a change that breaks an input, key, format, or behavior ships the break behind a major with a loud error naming the fix; a one-shot migration only when many files must move at once.

### Decisions a reader would otherwise reverse

- A flat `src/sections/<key>/` directory means repository scope, permanently; org/user scopes arrive as sibling scope directories with their own document, keys, and registry (the ":" reservation in `src/sections/registry.ts`).
- The section directory is the unit of work: the compiler flags every forgotten registration step, its tests and e2e fragments mirror it under `test/sections/<key>/`, and its prose is `docs/sections/<key>.docs.yml`.
- The section directory is the unit of work; its prose is `docs/sections/<key>.docs.yml`.
- `src/upstream-gaps/` holds one file per GitHub feature an upstream artifact lags; `gap.ts` states how each kind graduates.
- The layered fold is a CSS-like cascade in which `null` is a value: GitHub's EMPTY or OFF state, not CSS `unset`. docs/operate/layering.md owns the rules, including the two CSS has no analogue for (an undeclared key keeps GitHub's current value; `_remove: true` drops a keyed entry).
- The layered fold is a CSS-like cascade in which `null` is a value: GitHub's EMPTY or OFF state, not CSS `unset`. docs/operate/layering.md owns the rules, including the two CSS has no analogue for.

### Releases

- The `release` job in ci.yml stays out of all-green's `needs`: it runs downstream of the gate so releases only happen on a green main.
- Every consumable ref points at a packaged commit off main, and a pointer only ever moves forward along main; main stays source-only and no tag ever lands on it. Topology and the rule: `.github/scripts/release-pipeline.ts`.
- The `release` job in ci.yml stays out of all-green's `needs`, so a release only happens from a green main.
- Every consumable ref points at a packaged commit off main, and a pointer only ever moves forward along main; no tag ever lands on main.
- The npm package publishes from the release hooks through trusted publishing; docs/reference/library.md states the versioning rules.
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@ Apply declarative repository settings from `.github/settings.yml`: a loud, state

The same engine is the npm package `@vivswan/github-settings-as-code` (ESM, Node 22.14 or newer): validate, merge, check, and apply from your own code.

- `npm install @vivswan/github-settings-as-code` installs the released version; `@next` installs the pre-release of the `main` commit release-please last refreshed the release PR on ([Versioning](docs/reference/library.md#versioning)). The [library reference](docs/reference/library.md) has the API by group and the versioning rules.
- Until the first stable release publishes the package, ask for the `next` tag: `npm install @vivswan/github-settings-as-code@next` installs the pre-release of the `main` commit release-please last refreshed the release PR on ([Versioning](docs/reference/library.md#versioning)). The [library reference](docs/reference/library.md) has the API by group and the versioning rules.
- `npx @vivswan/github-settings-as-code@next check --repository o/r --settings-file .github/settings.yml` runs the action's check from a terminal. The [command line guide](docs/start/cli.md) has every command.

## Docs
Expand Down
6 changes: 3 additions & 3 deletions docs/reference/library.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,8 @@ The engine behind the action is also an npm package, `@vivswan/github-settings-a
Three ways in, one package:

```bash
npm install @vivswan/github-settings-as-code # the released version (npm dist-tag latest)
npm install @vivswan/github-settings-as-code@next # the pre-release of the main commit the release PR was last refreshed on
npm install @vivswan/github-settings-as-code@next # the pre-release of the main commit the release PR was last refreshed on; ask for it until the first stable release
npm install @vivswan/github-settings-as-code # the released version (npm dist-tag latest), a 0.0.0 placeholder until the first stable release
npm install github:Vivswan/github-settings-as-code#<packaged sha> # one packaged commit: a build tag's or a release tag's
```

Expand Down Expand Up @@ -386,7 +386,7 @@ The package and the action share one version, the one in `.release-please-manife
| npm dist-tag | Publishes on | Version | Install |
|---|---|---|---|
| `next` | Every refresh of the release PR: release-please creates or refreshes it when a releasable commit (feat, fix, perf, revert, or a breaking marker) lands on `main` | The manifest's next patch, then `-main.<count>.<date>.g<sha7>`: `2.0.1-main.446.20260913.g95d081d` | `npm install @vivswan/github-settings-as-code@next` |
| `latest` | Every release cut | The released version: `2.1.0`. Until the first release it names a `next` pre-release: a packument always carries `latest` (npm/registry REGISTRY-API.md, "dist-tags: an object with at least one key, latest"), so the first publish took it whatever `--tag` asked for, and the first stable release moves it | `npm install @vivswan/github-settings-as-code` |
| `latest` | Every release cut | The released version: `2.1.0`. Until the first stable release it names the `0.0.0` placeholder that reserved the package name: a packument always carries `latest` (npm/registry REGISTRY-API.md, "dist-tags: an object with at least one key, latest"), so the first publish took it, and the first stable release moves it; ask for `@next` until then | `npm install @vivswan/github-settings-as-code` |
Comment thread
Vivswan marked this conversation as resolved.
| none | Every green push to `main` (`build/<position>.<sha7>`, the ten newest kept) and every release tag | The commit itself | `npm install github:Vivswan/github-settings-as-code#<packaged sha>` |

The npm dist-tag `latest` is not the git tag `latest`: the git tag names the packaged commit of the newest `main` commit (every green push moves it forward, never back), the dist-tag names the newest release on the registry.
Expand Down
2 changes: 1 addition & 1 deletion src/sections/webhooks/events.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
* The events GitHub delivers to repository webhooks, spelled as the wire event name: every webhook in GitHub's
* webhooks OpenAPI description, as @octokit/openapi-webhooks ships it (generated/api.github.com.json), whose
* supported-webhook-types names "repository". The list is committed, not imported: bundling the descriptor would
* double lib/index.js. test/sections/webhooks-events.test.ts recomputes it from the package and fails with the
* double lib/index.js. test/sections/webhooks/events.test.ts recomputes it from the package and fails with the
* names added and dropped when a bump moves the list; the fix is to edit this file to match.
*/

Expand Down
2 changes: 1 addition & 1 deletion test/e2e/generators.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1027,7 +1027,7 @@ export function validateAgainstPublishedSchema(doc: unknown): void {
}

export interface GenScenarioOptions {
/** Restrict generation to these sections (a smoke or PR-diff subset). */
/** Restrict generation to these sections (the `--sections` flag); the default is every registered section. */
sections?: SectionKey[];
}

Expand Down
4 changes: 2 additions & 2 deletions test/e2e/runner.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -225,8 +225,8 @@ describe("snapshotCheckInputs (the round-trip check's inputs)", () => {

describe("bundle build parity (harness vs production)", () => {
test("the declared build:bundle script matches what the harness builds", () => {
// A unit test on purpose: a package.json-only diff selects no sections and skips the e2e smoke
// job, so this is the only place the pin can fire on the PR that trips it.
// A unit test on purpose: builtBundle() checks the same pin as a fast local signal that aborts
// the whole e2e run; this test is the binding assertion, failing by name on the PR that drifts.
expect(bundleBuildParityFailure(declaredBuildBundleScript())).toBeUndefined();
});

Expand Down
4 changes: 2 additions & 2 deletions test/e2e/runner.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,8 +52,8 @@ import { collectYmlFiles, type Expect, type Scenario, settingsYamlFor } from "./
const BUILD_BUNDLE_SCRIPT = "bun build src/main.ts --target=node --outfile lib/index.js";

/**
* Exported so a UNIT test asserts the parity on every PR: builtBundle() runs only when a scenario
* runs, and the e2e smoke job skips on a package.json-only diff, the very PR that changes this script.
* Exported so a UNIT test asserts the parity by name on every PR. builtBundle() checks it too, but
* as a fast local signal that aborts the whole e2e run; the unit test is the binding assertion.
*/
export function bundleBuildParityFailure(script: string | undefined): string | undefined {
return script === BUILD_BUNDLE_SCRIPT
Expand Down
Loading