From 763f65e9e98c7b1bb08e635dfbb4b7f33d3713ba Mon Sep 17 00:00:00 2001 From: Vivswan Shah <58091053+Vivswan@users.noreply.github.com> Date: Tue, 22 Sep 2026 06:50:51 -0400 Subject: [PATCH] build(schema): build lib/settings.schema.json instead of committing it lib/settings.schema.json is untracked and gitignored beside lib/index.js and lib/pkg/, and every packaged commit off main now carries it as a required built file. The test and fuzz scripts, the e2e-smoke job, the package smoke, and the packaging and publish jobs run build:schema before anything loads or packs the schema. The drift machinery (the generated-output table row, the auto-fix schema branch, the schema-check job, the release hook's byte check) goes with the committed file. The $schema hints and the $id are unchanged; the examples page links the schema at the moving major tag, which release-please now rewrites. --- .github/SECURITY.md | 4 +- .github/scripts/gen-settings-schema.ts | 10 +- .github/scripts/generated.ts | 3 +- .github/scripts/package-smoke.ts | 5 +- .github/scripts/release-pipeline.ts | 13 +- .github/workflows/auto-fix.yml | 23 +- .github/workflows/checks.yml | 14 +- .github/workflows/nightly-fuzz.yml | 4 + .github/workflows/post-green.yml | 3 +- .github/workflows/update-release-pr.yml | 3 +- .github/workflows/update-release.yml | 13 +- .gitignore | 10 +- AGENTS.md | 2 +- CONTRIBUTING.md | 11 +- docs/operate/troubleshooting.md | 2 +- docs/reference/library.md | 8 +- docs/start/examples.md | 2 +- lefthook.yml | 2 +- lib/settings.schema.json | 4128 ------------------- package.json | 4 +- release-please-config.json | 1 + test/docs/library-examples.test.ts | 4 +- test/docs/readme.test.ts | 4 +- test/e2e/fuzz.ts | 4 +- test/e2e/generators.ts | 9 +- test/package-json.test.ts | 4 +- test/published-schema.test.ts | 8 +- test/schema-corpus.test.ts | 4 +- test/scripts/generated.test.ts | 2 +- test/scripts/release-pipeline-build.test.ts | 6 +- test/scripts/release-pipeline-fixture.ts | 21 +- test/scripts/release-pipeline.test.ts | 6 +- test/settings-schema.ts | 21 + 33 files changed, 131 insertions(+), 4227 deletions(-) delete mode 100644 lib/settings.schema.json create mode 100644 test/settings-schema.ts diff --git a/.github/SECURITY.md b/.github/SECURITY.md index dd8ddce82..a5766a09f 100644 --- a/.github/SECURITY.md +++ b/.github/SECURITY.md @@ -11,14 +11,14 @@ This action holds a repository-admin token and writes repository settings, so th - Token handling. The token travels only in the Authorization header and is never printed, not even in debug traces. Any path that puts it in logs, annotations, the step summary, or outputs is a vulnerability. - Workflow-command injection. API responses and settings-file content are echoed into annotations and the step summary, escaped for workflow commands (%, CR, LF) and for summary tables (pipes, backslashes). Input that breaks out of that escaping and injects commands or forged log lines is a vulnerability. - Settings escalation. A crafted settings file must never touch a repository or setting it does not declare, nor bypass the preflight barrier or the required-sections policy. -- Supply chain. A packaged commit whose bundle a rebuild of its parent (its source commit) does not reproduce, or whose tree is not that source's plus the bundle and library build, minus `package.json`'s preparation scripts (`prepare` and the install hooks, which would make npm rebuild a `github:` install), is a vulnerability. The next section says what each ref points at and how to verify it. +- Supply chain. A packaged commit whose bundle a rebuild of its parent (its source commit) does not reproduce, or whose tree is not that source's plus the bundle, the schema, and the library build, minus `package.json`'s preparation scripts (`prepare` and the install hooks, which would make npm rebuild a `github:` install), is a vulnerability. The next section says what each ref points at and how to verify it. - npm provenance. Every CI-published version of `@vivswan/github-settings-as-code` carries an npm provenance attestation naming this repository and workflow, which `npm audit signatures` checks in a project that installs it. A version without one, or whose attestation names another repository or workflow, is a vulnerability: every version on the registry is CI-published, and no exception exists. ## Verifying a release What a `uses:` pin points at: -- The `vX.Y.Z` tags, the moving major, and `latest` point at packaged commits: each the child of its source commit on `main`, carrying that tree plus the bundle and library built from it, minus `package.json`'s preparation scripts, by the workflow run its message names when CI minted it (a package minted by hand in the release recovery names none; see below). `main` carries no executable bundle. +- The `vX.Y.Z` tags, the moving major, and `latest` point at packaged commits: each the child of its source commit on `main`, carrying that tree plus the bundle, the schema, and the library built from it, minus `package.json`'s preparation scripts, by the workflow run its message names when CI minted it (a package minted by hand in the release recovery names none; see below). `main` carries no executable bundle. - The tags up to v2.0.0 point at release commits on `main` from when `main` still committed the bundle. Nothing re-verifies them; the release-tags ruleset is what keeps them where they are. - The release-tags ruleset freezes version tags for everything except deliberate repository-admin repair. The release workflow never moves a version tag; a rerun verifies the existing one byte-for-byte. - npm publishes through trusted publishing (OIDC) from `ci.yml`. The package disallows tokens, so no registry token exists anywhere. diff --git a/.github/scripts/gen-settings-schema.ts b/.github/scripts/gen-settings-schema.ts index 56502f2ef..9ce371d67 100644 --- a/.github/scripts/gen-settings-schema.ts +++ b/.github/scripts/gen-settings-schema.ts @@ -12,10 +12,10 @@ * defaulted keys -> OPTIONAL: io: "input" describes the file, not the parsed output, so a key the slice * fills at parse (a ruleset's target) stays out of required and keeps its default keyword * root layout -> zod's own, passed through verbatim - * $id -> stamped (SCHEMA_ID); definitions sorted so the committed file diffs deterministically + * $id -> stamped (SCHEMA_ID); definitions sorted so the built file is deterministic */ -import { writeFileSync } from "node:fs"; +import { mkdirSync, writeFileSync } from "node:fs"; import { join } from "node:path"; import { z } from "zod"; import { SettingsFile } from "../../src/schema.js"; @@ -93,13 +93,15 @@ function encodeRefs(node: unknown): void { } encodeRefs(generated); -// No layout assumption is guarded here: a future zod's shape change surfaces as schema-check drift, and a broken -// emission fails the published-schema tests (ajv compile plus fixture round-trips). +// No layout assumption is guarded here: a future zod's shape change or a broken emission fails the +// published-schema tests (ajv compile plus fixture round-trips), which load the file this script writes. const { definitions, ...rest } = generated; const sortedDefinitions = Object.fromEntries( Object.entries(definitions ?? {}).sort(([a], [b]) => (a < b ? -1 : 1)), ); +// A fresh checkout has no lib/: nothing under it is committed. +mkdirSync(join(ROOT, "lib"), { recursive: true }); const schemaPath = join(ROOT, "lib", "settings.schema.json"); writeFileSync( schemaPath, diff --git a/.github/scripts/generated.ts b/.github/scripts/generated.ts index 26d63e145..47bf7684d 100644 --- a/.github/scripts/generated.ts +++ b/.github/scripts/generated.ts @@ -25,10 +25,9 @@ function regions(generator: string, paths: readonly string[]): GeneratedOutput[] } /** A page two generators write into (docs/reference/inputs.md) has one row per generator. Table order is run order: - * the schema, docs, and action.yml generators import the gaps index through src/, and action.yml feeds the inputs table, so each renders first, or a new gap file or a bump would leave a run stale. */ + * the docs and action.yml generators import the gaps index through src/, and action.yml feeds the inputs table, so each renders first, or a new gap file or a bump would leave a run stale. */ export const GENERATED_OUTPUTS: readonly GeneratedOutput[] = [ { path: INDEX_PATH, generator: "build:gaps-index", kind: "file" }, - { path: "lib/settings.schema.json", generator: "build:schema", kind: "file" }, ...regions("build:docs", [COVERAGE_PATH, ...Object.keys(PAGE_REGIONS)]), ...regions("build:action-docs", Object.keys(GENERATED_REGIONS)), { path: INPUTS_PAGE_PATH, generator: "build:inputs-table", kind: "regions" }, diff --git a/.github/scripts/package-smoke.ts b/.github/scripts/package-smoke.ts index 3a30bcb5c..62628833d 100644 --- a/.github/scripts/package-smoke.ts +++ b/.github/scripts/package-smoke.ts @@ -1,6 +1,6 @@ /** - * The package smoke, the gate behind the npm library build: build lib/pkg/, - * judge the package shape (publint, attw), pack a tarball, install it into a + * The package smoke, the gate behind the npm library build: build lib/pkg/ + * and the schema, judge the package shape (publint, attw), pack a tarball, install it into a * fresh consumer project, import it under Node (both entries and the schema * subpath), and compile a TypeScript consumer against the bundled index.d.ts * and internal.d.ts with skipLibCheck off - a declaration that leaks a devDependency type, or a @@ -168,6 +168,7 @@ export function packedTarball(packJson: string, destination: string): string { } async function main(): Promise { + run("bun", ["run", "build:schema"], REPO_ROOT); run("bun", ["run", "build:lib"], REPO_ROOT); run("bun", ["run", "lint:package"], REPO_ROOT); await withSmokeDirs("gsac-smoke-", ({ pack, consumer }) => { diff --git a/.github/scripts/release-pipeline.ts b/.github/scripts/release-pipeline.ts index abe404635..3fbd5c145 100644 --- a/.github/scripts/release-pipeline.ts +++ b/.github/scripts/release-pipeline.ts @@ -2,7 +2,8 @@ * The release pipeline's git topology. main stays source-only, no version tag ever lands on it, and every ref a * consumer names points at a packaged commit: the child of one main commit, carrying that commit's build. * - * packaged commit = parent: the main commit; tree: its tree + lib/index.js + lib/pkg/, package.json minus its preparation scripts + * packaged commit = parent: the main commit; tree: its tree + lib/index.js + lib/settings.schema.json + lib/pkg/, + * package.json minus its preparation scripts * refs/tags/build/. -> the packaged commit of the main commit at first-parent position ; created once, never moved; the ten newest kept * refs/tags/latest -> the packaged commit of the newest main commit * refs/tags/vX.Y.Z -> the packaged commit of the release's merge commit; never moved @@ -42,10 +43,14 @@ const MANIFEST_FILE = ".release-please-manifest.json"; const CONFIG_FILE = "release-please-config.json"; const MANIFEST = "package.json"; /** What a packaged commit carries beyond its source (a directory entry stages every file under it). */ -const PACKAGED_PATHS = ["lib/index.js", "lib/pkg/"] as const; +const PACKAGED_PATHS = ["lib/index.js", "lib/settings.schema.json", "lib/pkg/"] as const; /** What every packaged commit must carry as non-empty regular files. */ -const REQUIRED_BUILT_FILES = ["lib/index.js", "lib/pkg/index.js"] as const; -const PACKAGED = "lib/index.js and lib/pkg/"; +const REQUIRED_BUILT_FILES = [ + "lib/index.js", + "lib/settings.schema.json", + "lib/pkg/index.js", +] as const; +const PACKAGED = "lib/index.js, lib/settings.schema.json, and lib/pkg/"; const LATEST_REF = "refs/tags/latest"; const BUILD_TAG_PREFIX = "refs/tags/build/"; const BUILD_TAG = /^refs\/tags\/build\/([1-9]\d*)\.[0-9a-f]{7}$/; diff --git a/.github/workflows/auto-fix.yml b/.github/workflows/auto-fix.yml index f17d2c667..4ae72d1dd 100644 --- a/.github/workflows/auto-fix.yml +++ b/.github/workflows/auto-fix.yml @@ -1,13 +1,12 @@ # The commit-back fixes a same-repo PR can need, pushed to its branch; an already-clean tree gets no commit. A PR # with unrelated type errors fails the build job by design: the graduation script refuses to half-fix a red build. -# lib/settings.schema.json -> build:schema (a Dependabot generator bump changes its bytes on a branch nobody builds) # README, action.yml, docs/ regions -> build:docs, build:action-docs, build:inputs-table (action-docs renders # the inputs table from action.yml) # src/upstream-gaps/ -> graduate-upstream-gaps.ts retires the gap files @octokit/types caught up with; # gen-gaps-index.ts re-renders index.ts -# lib/index.js -> never: main carries no bundle +# lib/ -> never: main carries no bundle, library, or schema # -# Regenerating the schema executes PR-controlled code, so the fix is computed in an unprivileged job and crosses to +# Regenerating the generated files executes PR-controlled code, so the fix is computed in an unprivileged job and crosses to # the push job as a git patch over the fix-owned paths only; applying a patch executes nothing, and the push job # re-checks the applied paths before committing. # --ignore-scripts on the build job's install -> lefthook and dependency lifecycle scripts never run @@ -25,7 +24,6 @@ on: pull_request: paths: - "src/**" - - "lib/settings.schema.json" - "README.md" - "action.yml" - "docs/reference/undeclared-policy.md" @@ -43,7 +41,6 @@ on: - "bun.lock" - "tsconfig.json" - ".bun-version" - - ".github/scripts/gen-settings-schema.ts" - ".github/scripts/graduate-upstream-gaps.ts" - ".github/scripts/gen-gaps-index.ts" - ".github/scripts/gen-docs.ts" @@ -82,7 +79,7 @@ jobs: - name: Graduate upstream gaps octokit now ships shell: bash run: bun .github/scripts/graduate-upstream-gaps.ts - - name: Regenerate the gaps index, schema, docs, and inputs table and stage the fix patch + - name: Regenerate the gaps index, docs, and inputs table and stage the fix patch id: rebuild shell: bash run: | @@ -90,20 +87,19 @@ jobs: # gaps index through src/, and the inputs table reads action.yml, so each source renders first. # test/scripts/auto-fix-allowlist.test.ts pins the list and the order. bun run build:gaps-index - bun run build:schema bun run build:docs bun run build:action-docs bun run build:inputs-table # Anything the earlier steps left staged is not this workflow's fix: start from an empty index so the # patch holds exactly the allowed paths. git reset -q - git add -A -- lib/settings.schema.json README.md action.yml \ + git add -A -- README.md action.yml \ docs/reference/coverage.md docs/reference/undeclared-policy.md docs/reference/permissions.md \ docs/operate/check-mode.md docs/reference/sections.md docs/reference/inputs.md \ docs/reference/architecture.md docs/start/getting-started.md src/upstream-gaps/ git diff --cached --binary > "$RUNNER_TEMP/autofix.patch" if [ ! -s "$RUNNER_TEMP/autofix.patch" ]; then - echo "schema, docs, and upstream gaps already fresh" + echo "docs and upstream gaps already fresh" echo "changed=false" >> "$GITHUB_OUTPUT" echo "pruned=false" >> "$GITHUB_OUTPUT" else @@ -169,12 +165,12 @@ jobs: # of a protected path cannot hide behind an allowed destination. while IFS= read -r -d '' path; do case "$path" in - lib/settings.schema.json | README.md | action.yml | \ + README.md | action.yml | \ docs/reference/coverage.md | docs/reference/undeclared-policy.md | docs/reference/permissions.md | \ docs/operate/check-mode.md | docs/reference/sections.md | docs/reference/inputs.md | \ docs/reference/architecture.md | docs/start/getting-started.md | src/upstream-gaps/*) ;; *) - echo "::error::the fix patch staged '$path', outside lib/settings.schema.json, the generated docs" \ + echo "::error::the fix patch staged '$path', outside the generated docs" \ "(README.md, action.yml, docs/reference/coverage.md, docs/reference/undeclared-policy.md," \ "docs/reference/permissions.md, docs/operate/check-mode.md, docs/reference/sections.md," \ "docs/reference/inputs.md, docs/reference/architecture.md, docs/start/getting-started.md)," \ @@ -190,10 +186,7 @@ jobs: git config user.name "github-actions[bot]" git config user.email "github-actions[bot]@users.noreply.github.com" subject="build: regenerate generated files" - if ! git diff --cached --quiet -- lib/settings.schema.json; then - subject="build: regenerate settings schema" - fi - if git diff --cached --quiet -- lib/settings.schema.json src/upstream-gaps/; then + if git diff --cached --quiet -- src/upstream-gaps/; then subject="docs: regenerate generated docs" fi if [ "$PRUNED" = "true" ]; then diff --git a/.github/workflows/checks.yml b/.github/workflows/checks.yml index 95519a08c..93e2c85c0 100644 --- a/.github/workflows/checks.yml +++ b/.github/workflows/checks.yml @@ -24,6 +24,8 @@ jobs: run: bun run typecheck - name: Compat markers run: bun run check:compat + # The built, gitignored lib/settings.schema.json bun test loads is what + # `bun run test` builds first, so a failing generator fails the Test step. - name: Test run: bun run test @@ -61,15 +63,6 @@ jobs: - name: Require the release PR to carry this cycle's anchor run: bun .github/scripts/release-pipeline.ts anchor-check - schema-check: - runs-on: ubuntu-latest - timeout-minutes: 15 - steps: - - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - - uses: ./.github/actions/setup - - name: Regenerate the schema and compare - run: bun run build:check - # The packed tarball is imported from a fresh consumer project under Node and under tsc with skipLibCheck off, so a # declaration leaking a devDependency type fails here, not on a consumer's machine. # The consumer runs on the engines floor package.json advertises and on current node; tsdown needs a newer node @@ -132,6 +125,9 @@ jobs: - uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 with: node-version: 24 + # The fuzzer validates every drawn document against the built, gitignored schema. + - name: Build the schema + run: bun run build:schema - name: Run scenarios and fuzz run: | bun test/e2e/run.ts diff --git a/.github/workflows/nightly-fuzz.yml b/.github/workflows/nightly-fuzz.yml index c3808de5a..6c1a175a0 100644 --- a/.github/workflows/nightly-fuzz.yml +++ b/.github/workflows/nightly-fuzz.yml @@ -57,6 +57,10 @@ jobs: with: node-version: 24 + # The fuzzer validates every drawn document against the built, gitignored schema. + - name: Build the schema + run: bun run build:schema + - name: Fuzz env: SEED: ${{ inputs.seed }} diff --git a/.github/workflows/post-green.yml b/.github/workflows/post-green.yml index bc518d4ea..0d7034d98 100644 --- a/.github/workflows/post-green.yml +++ b/.github/workflows/post-green.yml @@ -64,10 +64,11 @@ jobs: rm -f probe.err - uses: ./.github/actions/setup if: steps.token.outputs.proceed == 'true' - - name: Build the bundle and the library + - name: Build the bundle, the schema, and the library if: steps.token.outputs.proceed == 'true' run: | bun run build:bundle + bun run build:schema bun run build:lib - name: Package this commit under its build tag, prune the window, and point latest at the newest main source if: steps.token.outputs.proceed == 'true' diff --git a/.github/workflows/update-release-pr.yml b/.github/workflows/update-release-pr.yml index 76ac3baf5..cc4f456fc 100644 --- a/.github/workflows/update-release-pr.yml +++ b/.github/workflows/update-release-pr.yml @@ -103,9 +103,10 @@ jobs: echo "::error::npm $(npm --version) cannot publish through OIDC; trusted publishing needs npm $floor or newer." exit 1 fi + # The tarball's files list names the schema beside lib/pkg/. - name: Build the library if: steps.oidc.outputs.proceed == 'true' - run: bun run build:lib + run: bun run build:schema && bun run build:lib # scripts.prepare is dropped from the published manifest: it installs # lefthook, a devDependency the tarball does not carry, and npm blocks # install scripts from a provenance-attested package anyway. diff --git a/.github/workflows/update-release.yml b/.github/workflows/update-release.yml index 93436e5ec..a96b3c49b 100644 --- a/.github/workflows/update-release.yml +++ b/.github/workflows/update-release.yml @@ -4,7 +4,7 @@ # (the topology is in .github/scripts/release-pipeline.ts; release-please-config.json sets `draft` and pins # `force-tag-creation` off). # -# resolve the merge commit from the draft's target commitish -> rebuild the bundle, verify the committed schema byte-for-byte +# resolve the merge commit from the draft's target commitish -> build the bundle, the schema, and the library # package -> find or mint the merge commit's packaged commit under its build tag, mint vX.Y.Z there ONCE, move latest forward # retag-major -> move vX there (forward only), then upload bundle and schema to the draft (a published release freezes its assets) # verify-release -> confirm the draft's assets; the managed publish stage then attests and flips the release live @@ -68,12 +68,8 @@ jobs: # new version tag names the build tag by the commit's position; a shallow checkout can do none of that. fetch-depth: 0 - uses: ./.github/actions/setup - - name: Build the bundle and the schema - run: | - bun run build - # The schema asset and the raw URLs at the tags must serve the bytes committed at the merge commit; a - # divergent regeneration (generator drift, or a compromised generator) stops the release here. - git diff --exit-code lib/settings.schema.json + - name: Build the bundle, the schema, and the library + run: bun run build # Both paths (mint, or byte-verify on a rerun) leave the worktree bundle equal to the tagged one, which the # upload below relies on. - name: Create or verify the version tag on the build commit @@ -162,8 +158,9 @@ jobs: echo "::error::npm $(npm --version) cannot publish through OIDC; trusted publishing needs npm $floor or newer." exit 1 fi + # The tarball's files list names the schema beside lib/pkg/. - name: Build the library - run: bun run build:lib + run: bun run build:schema && bun run build:lib # scripts.prepare is dropped from the published manifest, as in # update-release-pr.yml's publish-next. # The verdict holds the built package.json to the tag and reads the diff --git a/.gitignore b/.gitignore index 9fe94bc11..2201f5aa4 100644 --- a/.gitignore +++ b/.gitignore @@ -6,11 +6,13 @@ test/e2e/.artifacts/ # Review tooling scratch directory. .rubber-duck-tmp/ # The bundled entrypoint is built, not committed: CI jobs and release tags -# build it with `bun run build:bundle`; only lib/settings.schema.json is -# committed under lib/. A release job that commits the bundle onto a tag -# must stage it with `git add -f` - a plain `git add lib/` silently skips -# the ignored file. +# build it with `bun run build:bundle`; nothing under lib/ is committed on +# main. A release job that commits the bundle onto a tag must stage it with +# `git add -f` - a plain `git add lib/` silently skips the ignored file. lib/index.js +# The published JSON Schema is built the same way (`bun run build:schema`, +# which the test scripts run first); the packaged commits carry it. +lib/settings.schema.json # BEGIN REPO-PLATFORM MANAGED # Generated from github/gitignore - do not edit between the BEGIN/END diff --git a/AGENTS.md b/AGENTS.md index feb37a10e..b723a0c58 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -37,7 +37,7 @@ Code is the source of truth: this section holds only the rules and the decisions ### Hard rules - 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. +- `lib/index.js`, `lib/settings.schema.json`, 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. diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 6e568380f..ab6779a2e 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -6,9 +6,10 @@ The fleet-wide conventions - Conventional Commit titles, squash merges, the `all - `src/` is TypeScript built with [bun](https://bun.com). The scripts in `package.json` are the commands; `bun run check` is the whole local gate. - GitHub's OpenAPI descriptor and GraphQL schema come from the `@octokit/openapi` and `@octokit/graphql-schema` devDependencies, so no test or generator touches the network. Dependabot moves the pins; a bump that stops documenting a path the action calls, starts documenting an upstream gap, or retires a field a query selects fails the schema tests on that PR by name. -- Committed generated output is the table in `.github/scripts/generated.ts`: `lib/settings.schema.json`, `src/upstream-gaps/index.ts`, and the generated regions of `action.yml` and the docs pages. +- `bun run test` and `bun run fuzz` run `bun run build:schema` first: the tests and the fuzzer load the built, gitignored `lib/settings.schema.json`. +- Committed generated output is the table in `.github/scripts/generated.ts`: `src/upstream-gaps/index.ts` and the generated regions of `action.yml` and the docs pages. - `bun run build:check` regenerates every table entry and fails on drift. -- `lib/index.js` (the action bundle) and `lib/pkg/` (the npm library) are built where they are needed and never committed on `main`. Every runtime dependency is compiled into them. +- `lib/index.js` (the action bundle), `lib/settings.schema.json` (the published schema), and `lib/pkg/` (the npm library) are built where they are needed and never committed on `main`. Every runtime dependency is compiled into the bundle and the library. - [docs/reference/coverage.md](docs/reference/coverage.md) is the inventory of the supported API surface, one link per call. A change that adds or extends a section keeps its `.docs.yml` rows and `.github/scripts/endpoint-docs.yml` in step. ## Backward compatibility @@ -44,13 +45,13 @@ The end-to-end tests build the bundle to a temp path and run it as a subprocess The fuzzer is deterministic. It prints a master seed and a per-iteration seed: ```sh -FUZZ_SEED= bun run fuzz # replay a whole run -bun test/e2e/fuzz.ts --seed --iterations 1 # replay one failing iteration +FUZZ_SEED= bun run fuzz # replay a whole run +bun run fuzz --seed --iterations 1 # replay one failing iteration ``` ## Releases - Releases run downstream of the `all-green` gate: ci.yml calls the fleet's release workflow, so a release or a release-PR refresh only happens from a green `main`. - release-please does the version math, the changelog, the version pins, and the release PR; merging that PR cuts the release. -- Every ref a `uses:` pin can name (`vX.Y.Z`, the moving major, `latest`) points at a packaged commit: the child of one `main` commit, carrying its tree plus the built bundle and library; every green push mints one under a `build/.` tag, the ten newest kept. The tags up to v2.0.0 point at `main` commits from when `main` committed the bundle. +- Every ref a `uses:` pin can name (`vX.Y.Z`, the moving major, `latest`) points at a packaged commit: the child of one `main` commit, carrying its tree plus the built bundle, schema, and library; every green push mints one under a `build/.` tag, the ten newest kept. The tags up to v2.0.0 point at `main` commits from when `main` committed the bundle. - The repo-owned hooks `update-release.yml` and `update-release-pr.yml` mint the tags and keep release-please's boundary (`last-release-sha`) fresh. The git topology lives in `.github/scripts/release-pipeline.ts` and its test. diff --git a/docs/operate/troubleshooting.md b/docs/operate/troubleshooting.md index 4eea39977..3f30d9bb6 100644 --- a/docs/operate/troubleshooting.md +++ b/docs/operate/troubleshooting.md @@ -75,4 +75,4 @@ Every API call the action makes is traced as a debug line: method, path, request ## Behavior does not match src/ (missing or stale bundle) -When you run the action from a ref (`uses: your-fork/github-settings-as-code@your-branch`), what executes is `lib/index.js`, the bundle, not the TypeScript under `src/`. The release tags (`vX.Y.Z`) and the moving major tag carry a freshly built bundle; branches do not - the bundle is not committed on main. So on a fork or working branch, build it yourself: run `bun run build:bundle` and commit the result on your branch (and `bun run build:schema` if you changed the settings types). A ref without the bundle fails to start; a branch where you rebuilt `src/` without rebuilding behaves like the old code and no error tells you so. The rebuild is on you. +When you run the action from a ref (`uses: your-fork/github-settings-as-code@your-branch`), what executes is `lib/index.js`, the bundle, not the TypeScript under `src/`. The release tags (`vX.Y.Z`) and the moving major tag carry a freshly built bundle; branches do not - the bundle is not committed on main. So on a fork or working branch, build it yourself: run `bun run build:bundle` and commit the result on your branch (`lib/index.js` is gitignored, so stage it with `git add -f`). A ref without the bundle fails to start; a branch where you rebuilt `src/` without rebuilding behaves like the old code and no error tells you so. The rebuild is on you. diff --git a/docs/reference/library.md b/docs/reference/library.md index 6632ccf49..9e9ad7ee5 100644 --- a/docs/reference/library.md +++ b/docs/reference/library.md @@ -18,13 +18,13 @@ npm install github:Vivswan/github-settings-as-code# # one packag `bun add` takes the same three forms. A pre-release version looks like `2.0.1-main.446.20260913.g95d081d`; the [Versioning](#versioning) section says how the three relate. -The `github:` form installs a packaged commit: the child of one `main` commit, carrying that commit's tree plus `lib/pkg/` (the library build) beside `lib/index.js` (the action bundle), both built from that commit; a package CI minted names its workflow run in its message, one minted by hand in the release recovery does not. +The `github:` form installs a packaged commit: the child of one `main` commit, carrying that commit's tree plus `lib/pkg/` (the library build), `lib/settings.schema.json` (the published schema), and `lib/index.js` (the action bundle), all built from that commit; a package CI minted names its workflow run in its message, one minted by hand in the release recovery does not. - Its `package.json` carries none of the scripts npm's git fetcher takes as a reason to install devDependencies and run a prepare step (`prepare`, `prepack`, `build`, the install hooks), so nothing is built or installed on your side. - Every green push to `main` mints one under the tag `build/.` and then prunes the tags to the ten newest: once ten newer commits have been packaged, a tag is deleted and GitHub may collect its commit, so a pin taken from an old tag can go on the next merge. A durable pin names a release tag's commit (`git rev-parse v2.1.0`) or an npm version. - The tags up to v2.0.0 point at release commits on `main` from when main still committed the bundle, not at packaged commits; the packaged commits minted before the per-commit tags lived on the `build` branch, deleted on 2026-09-13. -To build the package from a checkout instead, `bun install && bun run build:lib` writes `lib/pkg/`: the entry and the internal entry with their declarations, and the CLI, the files the manifest's `exports` and `bin` point at. +To build the package from a checkout instead, `bun install && bun run build:schema && bun run build:lib` writes `lib/settings.schema.json` and `lib/pkg/`: the entry and the internal entry with their declarations, and the CLI, the files the manifest's `exports` and `bin` point at. ## The two entries @@ -33,7 +33,7 @@ To build the package from a checkout instead, `bun install && bun run build:lib` | `@vivswan/github-settings-as-code` | The documented library: every name in [the API by group](#the-api-by-group), and nothing else | Semver: a rename or a removal is a major, listed in the [upgrading guide](../upgrading/README.md) | | `@vivswan/github-settings-as-code/internal` | What the action, the CLI, and this repository's tests import beyond the library (the input declarations, the engine's per-repository run, the redaction helpers, ...) | None: a name here may move or go in any release. Nothing outside this repository should import it | -Two more paths ride along: the committed settings.yml JSON Schema (`./settings.schema.json`) and the package's own manifest (`./package.json`). +Two more paths ride along: the built settings.yml JSON Schema (`./settings.schema.json`) and the package's own manifest (`./package.json`). The entries are [src/index.ts](https://github.com/Vivswan/github-settings-as-code/blob/main/src/index.ts) and [src/internal.ts](https://github.com/Vivswan/github-settings-as-code/blob/main/src/internal.ts), each a list of re-exports. The tables below are the public entry's contract: a test derives the list of names from this page and fails when `src/index.ts` exports a name no table names, or names one it does not export. @@ -110,7 +110,7 @@ console.log(merged.value.yaml, merged.value.notices.length); | `UNDECLARED_POLICY_SECTIONS` | const | The list sections whose wrapper takes `_undeclared` beside `_layering`; `environments`, `branches`, and `workflows` layer by key too, through a `{_layering, entries}` wrapper of their own (`LIST_SECTIONS` in the schema module) | | `UndeclaredPolicySection` | type | One of them | -The schema subpath serves the committed JSON Schema. +The schema subpath serves the built JSON Schema. ```ts import { SECTION_KEYS, SettingsFile } from "@vivswan/github-settings-as-code"; diff --git a/docs/start/examples.md b/docs/start/examples.md index 35da3ad49..970587d2e 100644 --- a/docs/start/examples.md +++ b/docs/start/examples.md @@ -296,7 +296,7 @@ A `mode: render` fold keeps all three meanings: a higher `pages: null` or `inter A multi-repo `defaults-file` never merges into a target's file, so a `null` there keeps the meanings above. -A few individual fields accept `null` as a value of their own too, such as `pages.cname` to remove a custom domain; the [published schema](https://github.com/Vivswan/github-settings-as-code/blob/main/lib/settings.schema.json) marks those. +A few individual fields accept `null` as a value of their own too, such as `pages.cname` to remove a custom domain; the [published schema](https://github.com/Vivswan/github-settings-as-code/blob/v2/lib/settings.schema.json) marks those. ## Notes in the file diff --git a/lefthook.yml b/lefthook.yml index a5a9dd71e..2b390a94e 100644 --- a/lefthook.yml +++ b/lefthook.yml @@ -1,5 +1,5 @@ # Git hooks, installed by `bun install` (the prepare script). Deliberately cheap: staged lint plus a whole-project -# typecheck; builds, tests, and schema freshness are CI's job behind the all-green gate. +# typecheck; builds and tests are CI's job behind the all-green gate. pre-commit: parallel: true jobs: diff --git a/lib/settings.schema.json b/lib/settings.schema.json deleted file mode 100644 index 90d07c5bc..000000000 --- a/lib/settings.schema.json +++ /dev/null @@ -1,4128 +0,0 @@ -{ - "$schema": "http://json-schema.org/draft-07/schema#", - "$ref": "#/definitions/SettingsFile", - "$id": "https://raw.githubusercontent.com/Vivswan/github-settings-as-code/HEAD/lib/settings.schema.json", - "definitions": { - "ActionsConfig": { - "description": "GitHub Actions settings, routed to the right endpoint by key.", - "type": "object", - "properties": { - "enabled": { - "description": "PUT /repos/{r}/actions/permissions: whether Actions runs at all.", - "type": "boolean" - }, - "allowed_actions": { - "description": "Which actions may run; \"selected\" pairs with selected_actions below.", - "type": "string", - "enum": [ - "all", - "local_only", - "selected" - ] - }, - "sha_pinning_required": { - "description": "Whether actions must be pinned to a full-length commit SHA.", - "type": "boolean" - }, - "selected_actions": { - "description": "PUT /repos/{r}/actions/permissions/selected-actions (allowed_actions: selected): the allowlist, e.g. { github_owned_allowed: true, patterns_allowed: [octocat/*] }. Only the three documented keys are accepted, so a misspelled one is refused instead of re-sent on every run.", - "type": "object", - "properties": { - "github_owned_allowed": { - "description": "Whether actions in GitHub's own organizations (such as actions/*) may run.", - "type": "boolean" - }, - "verified_allowed": { - "description": "Whether actions by GitHub Marketplace verified creators may run.", - "type": "boolean" - }, - "patterns_allowed": { - "description": "Patterns naming the actions and reusable workflows allowed to run, e.g. octocat/hello-world@v2 or octocat/*; GitHub applies them to public repositories only.", - "type": "array", - "items": { - "type": "string" - } - } - }, - "additionalProperties": false - }, - "default_workflow_permissions": { - "description": "PUT /repos/{r}/actions/permissions/workflow: the default GITHUB_TOKEN grant.", - "type": "string", - "enum": [ - "read", - "write" - ] - }, - "can_approve_pull_request_reviews": { - "description": "Whether workflows may approve pull request reviews.", - "type": "boolean" - }, - "access_level": { - "description": "PUT /repos/{r}/actions/permissions/access (private repositories only)", - "type": "string", - "enum": [ - "none", - "user", - "organization" - ] - }, - "artifact_and_log_retention": { - "description": "PUT /repos/{r}/actions/permissions/artifact-and-log-retention: how many days artifacts and workflow logs are kept, e.g. { days: 90 }. The body passes through verbatim, so future fields GitHub adds work unchanged, and a key the GET never echoes is noted at check time as never converging; maximum_allowed_days, which the GET reports and the PUT does not take, is refused.", - "type": "object", - "properties": { - "days": { - "description": "How many days artifacts and workflow logs are kept; a whole number of days within the plan's maximum.", - "type": "integer", - "exclusiveMinimum": 0 - } - }, - "required": [ - "days" - ] - }, - "cache": { - "description": "Actions cache limits, each key routed to its own endpoint: max_cache_retention_days -> PUT /repos/{r}/actions/cache/retention-limit, max_cache_size_gb -> PUT /repos/{r}/actions/cache/storage-limit. Keys other than these two are rejected (each limit has its own single-field endpoint, so an extra key could only be a typo).", - "type": "object", - "properties": { - "max_cache_retention_days": { - "description": "How many days an unused cache entry is kept before GitHub evicts it.", - "type": "integer", - "exclusiveMinimum": 0 - }, - "max_cache_size_gb": { - "description": "The total cache storage the repository may use, in GB.", - "type": "integer", - "exclusiveMinimum": 0 - } - }, - "additionalProperties": false - }, - "oidc_customization_sub": { - "description": "PUT /repos/{r}/actions/oidc/customization/sub: the OIDC subject claim template for this repository's workflow tokens, e.g. { use_default: false, include_claim_keys: [repo, context] }, an endpoint pair that needs the \"Actions\" PAT permission rather than Administration. A list under use_default: true is refused, since GitHub ignores it; so is sub_claim_prefix, which only the GET reports. Other keys pass through verbatim, and one the GET never echoes is noted at check time as never converging.", - "oneOf": [ - { - "type": "object", - "properties": { - "use_default": { - "description": "true: GitHub's default subject claim format; this variant carries no claim-key list.", - "type": "boolean", - "const": true - }, - "use_immutable_subject": { - "description": "Switch the whole subject to the stable repository-ID-based format; omitted, the organization setting or the repository's creation date decides, and only a declared value is compared.", - "type": "boolean" - } - }, - "required": [ - "use_default" - ] - }, - { - "type": "object", - "properties": { - "use_default": { - "description": "false: a custom template, joined from include_claim_keys.", - "type": "boolean", - "const": false - }, - "include_claim_keys": { - "description": "The claim keys the custom subject joins, in order (e.g. repo, context); each is unique and holds only letters, digits, and underscores. Claim-key ORDER defines the subject format, so check mode compares a declared list positionally; an omitted list opts into the organization template and is not compared.", - "type": "array", - "items": { - "type": "string", - "pattern": "^[A-Za-z0-9_]+$" - } - }, - "use_immutable_subject": { - "description": "Switch the whole subject to the stable repository-ID-based format; omitted, the organization setting or the repository's creation date decides, and only a declared value is compared.", - "type": "boolean" - } - }, - "required": [ - "use_default" - ] - } - ] - }, - "fork_pr_contributor_approval": { - "description": "PUT /repos/{r}/actions/permissions/fork-pr-contributor-approval: when workflows triggered by fork pull requests need a maintainer's approval before they run, e.g. { approval_policy: first_time_contributors }. The policies GitHub accepts are first_time_contributors_new_to_github, first_time_contributors, and all_external_contributors; any other value is refused when the file is parsed. The body passes through verbatim, so future fields GitHub adds work unchanged, and a key the GET never echoes is noted at check time as never converging.", - "type": "object", - "properties": { - "approval_policy": { - "description": "Which fork contributors need a maintainer's approval before their workflows run: first_time_contributors_new_to_github, first_time_contributors, or all_external_contributors.", - "type": "string", - "enum": [ - "first_time_contributors_new_to_github", - "first_time_contributors", - "all_external_contributors" - ] - } - }, - "required": [ - "approval_policy" - ] - }, - "fork_pr_workflows_private_repos": { - "description": "PUT /repos/{r}/actions/permissions/fork-pr-workflows-private-repos: whether pull requests from forks may run workflows on this private repository, and what those workflows receive. Only run_workflows_from_fork_pull_requests is required; GitHub does not document whether the PUT preserves an omitted toggle, so declare all four. Other keys pass through verbatim, and one the GET never echoes is noted at check time as never converging.", - "type": "object", - "properties": { - "run_workflows_from_fork_pull_requests": { - "description": "Whether pull requests from forks may run workflows at all.", - "type": "boolean" - }, - "send_write_tokens_to_workflows": { - "description": "Whether those workflows receive a GITHUB_TOKEN with write permissions.", - "type": "boolean" - }, - "send_secrets_and_variables": { - "description": "Whether those workflows receive the repository's secrets and variables.", - "type": "boolean" - }, - "require_approval_for_fork_pr_workflows": { - "description": "Whether a repository administrator must approve each fork workflow run before it starts.", - "type": "boolean" - } - }, - "required": [ - "run_workflows_from_fork_pull_requests" - ] - } - } - }, - "ActionsSecretConfig": { - "description": "One repository Actions secret, matched by case-insensitive name (GitHub stores secret names uppercase). Keys other than name and value are rejected: the API body is built from the sealed value alone, so an extra key would silently do nothing.", - "type": "object", - "properties": { - "name": { - "description": "The secret name, the natural key; compared case-insensitively and written uppercase. GitHub's naming rule is checked when the file is parsed, before any API call: ASCII letters, digits, and underscores only, not starting with a digit, and not starting with the reserved `GITHUB_` prefix in any case (`github_token` is the reserved `GITHUB_TOKEN` once uppercased).", - "type": "string", - "pattern": "^(?![Gg][Ii][Tt][Hh][Uu][Bb]_)[A-Za-z_][A-Za-z0-9_]*$" - }, - "value": { - "description": "A whole-value `$NAME` reference to an environment variable holding the secret - never a literal (settings files are committed plaintext). Resolved from the action step's env at run time and sealed client-side into the sealed box GitHub's secrets API expects (libsodium's crypto_box_seal format) before upload; GitHub cannot return the value, so check mode verifies existence only and apply re-seals it on every run.", - "type": "string" - } - }, - "required": [ - "name", - "value" - ] - }, - "ActionsVariableConfig": { - "description": "One GitHub Actions repository variable, matched by case-insensitive name; workflows read it through the `vars` context.", - "type": "object", - "properties": { - "name": { - "description": "The variable name, the natural key; case-insensitive (stored uppercased by GitHub). GitHub's naming rule is checked when the file is parsed, before any API call: ASCII letters, digits, and underscores only, not starting with a digit, and not starting with the reserved `GITHUB_` prefix in any case.", - "type": "string", - "pattern": "^(?![Gg][Ii][Tt][Hh][Uu][Bb]_)[A-Za-z_][A-Za-z0-9_]*$" - }, - "value": { - "description": "The plain-text value. GitHub caps one variable at 48 KB; a value over 49152 bytes of UTF-8 is refused when the file is parsed, before any API call.", - "type": "string", - "maxLength": 49152 - } - }, - "required": [ - "name", - "value" - ] - }, - "AgentsSecretConfig": { - "description": "One repository Copilot agents secret, matched by case-insensitive name (GitHub stores secret names uppercase). Keys other than name and value are rejected: the API body is built from the sealed value alone, so an extra key would silently do nothing.", - "type": "object", - "properties": { - "name": { - "description": "The secret name, the natural key; compared case-insensitively and written uppercase. GitHub's naming rule is checked when the file is parsed, before any API call: ASCII letters, digits, and underscores only, not starting with a digit, and not starting with the reserved `GITHUB_` prefix in any case (`github_token` is the reserved `GITHUB_TOKEN` once uppercased).", - "type": "string", - "pattern": "^(?![Gg][Ii][Tt][Hh][Uu][Bb]_)[A-Za-z_][A-Za-z0-9_]*$" - }, - "value": { - "description": "A whole-value `$NAME` reference to an environment variable holding the secret - never a literal (settings files are committed plaintext). Resolved from the action step's env at run time and sealed client-side into the sealed box GitHub's secrets API expects (libsodium's crypto_box_seal format) before upload; GitHub cannot return the value, so check mode verifies existence only and apply re-seals it on every run.", - "type": "string" - } - }, - "required": [ - "name", - "value" - ] - }, - "AgentsVariableConfig": { - "description": "One Copilot agents repository variable, matched by case-insensitive name.", - "type": "object", - "properties": { - "name": { - "description": "The variable name, the natural key; case-insensitive (stored uppercased by GitHub). GitHub's naming rule is checked when the file is parsed, before any API call: ASCII letters, digits, and underscores only, not starting with a digit, and not starting with the reserved `GITHUB_` prefix in any case.", - "type": "string", - "pattern": "^(?![Gg][Ii][Tt][Hh][Uu][Bb]_)[A-Za-z_][A-Za-z0-9_]*$" - }, - "value": { - "description": "The plain-text value. GitHub caps one variable at 48 KB; a value over 49152 bytes of UTF-8 is refused when the file is parsed, before any API call.", - "type": "string", - "maxLength": 49152 - } - }, - "required": [ - "name", - "value" - ] - }, - "AutoTriggerCheckConfig": { - "description": "One per-app auto-trigger toggle. Extra fields pass through verbatim.", - "type": "object", - "properties": { - "app_id": { - "description": "The id of the GitHub App the preference applies to: a positive integer (the App's settings page shows it), declared once. 0, fractions, and a repeated app_id fail at parse time; GitHub has no app 0, rejects fractions, and would keep whichever duplicate it reads last with nothing to read it back.", - "type": "integer", - "exclusiveMinimum": 0 - }, - "setting": { - "description": "Whether pushes automatically create check suites for this app; GitHub defaults each app to true.", - "type": "boolean" - } - }, - "required": [ - "app_id", - "setting" - ] - }, - "AutolinkConfig": { - "description": "One autolink reference, matched by key prefix.", - "type": "object", - "properties": { - "key_prefix": { - "description": "Text prefix that triggers the link (e.g. \"TICKET-\"), the natural key; letters, digits, and \". - _ + = : / #\" only, and no declared prefix may begin another.", - "type": "string", - "pattern": "^[A-Za-z0-9.\\-_+=:/#]+$" - }, - "url_template": { - "description": "Target URL template containing \"\".", - "type": "string", - "pattern": "" - }, - "is_alphanumeric": { - "description": "Whether also matches letters; upstream default is true on a create, and a recreate re-sends the live value when undeclared.", - "type": "boolean" - } - }, - "required": [ - "key_prefix", - "url_template" - ] - }, - "BranchConfig": { - "description": "Classic protection for one branch name or wildcard pattern.", - "type": "object", - "properties": { - "name": { - "description": "The branch name, or a wildcard pattern (any name containing `*`, `?`, or `[`, e.g. \"release/*\"). A literal name applies through the REST protection endpoints; a wildcard rule is REST-invisible, so it applies entirely through the GraphQL branch-protection-rule mutations and its protection accepts only the keys this action can round-trip through that surface (the validator names them; prefer rulesets for new pattern-based configuration).", - "type": "string" - }, - "protection": { - "description": "PUT .../protection payload; null removes protection (Probot parity).", - "anyOf": [ - { - "$ref": "#/definitions/BranchProtectionConfig" - }, - { - "type": "null" - } - ] - } - }, - "required": [ - "name", - "protection" - ] - }, - "BranchProtectionConfig": { - "description": "The protection PUT payload, passed through verbatim except its routed keys; a key the GET never echoes is noted at check time as never converging. What the file alone shows wrong is refused upfront, naming the fix: the GET-only keys a copied GET response carries (`name`, `enabled`, `enforcement_level`, `url` and `*_url`, at any depth), a status-check requirement without `strict` or a check list, and a review count outside 0-6.", - "type": "object", - "properties": { - "required_status_checks": { - "description": "Require status checks to pass before merging; `null` turns the requirement off. A mapping carries `strict` and one of `contexts` or `checks`, since GitHub's protection PUT rejects the requirement without either.", - "anyOf": [ - { - "type": "object", - "properties": { - "strict": { - "description": "Whether the branch must also be up to date with its base before merging; `false` requires only the checks to pass.", - "type": "boolean" - }, - "contexts": { - "description": "The required checks by name; `[]` keeps the requirement on while requiring none.", - "type": "array", - "items": { - "type": "string" - } - }, - "checks": { - "description": "The required checks as {context, app_id} pairs, the PUT's second spelling of the list; the names are derived into `contexts` for the PUT when the file declares only `checks`. GitHub's PUT takes no other field in an item, so any other key is refused upfront, named, with the fix.", - "type": "array", - "items": { - "type": "object", - "properties": { - "context": { - "description": "The name of the required check.", - "type": "string" - }, - "app_id": { - "description": "The id of the GitHub App that must report the check. `-1` lets any App report it (`null`, GitHub's read-back spelling of the same, is sent as `-1`); an omitted id pins whichever App reported it last.", - "type": [ - "integer", - "null" - ] - } - }, - "required": [ - "context" - ], - "additionalProperties": false - } - } - }, - "required": [ - "strict" - ], - "additionalProperties": {}, - "anyOf": [ - { - "required": [ - "contexts" - ] - }, - { - "required": [ - "checks" - ] - } - ] - }, - { - "type": "null" - } - ] - }, - "required_pull_request_reviews": { - "description": "Require pull request reviews before merging; `null` turns the requirement off. Every review setting beside the count and the two actor holders (`dismiss_stale_reviews`, `require_code_owner_reviews`, ...) passes through to the PUT.", - "anyOf": [ - { - "type": "object", - "properties": { - "required_approving_review_count": { - "description": "Approving reviews required before merging: 1 to 6, or 0 to require no approvals. GitHub accepts no other value, so the file is refused upfront instead of the PUT failing at apply.", - "type": "integer", - "minimum": 0, - "maximum": 6 - }, - "dismissal_restrictions": { - "description": "Who may dismiss pull request reviews, as `users`, `teams`, and `apps` lists of login/slug strings, each list optional; an empty mapping lets anyone with push access dismiss. GitHub's GET expands each actor into an object, which the PUT rejects, so a copied one is refused naming the string to write.", - "type": "object", - "properties": { - "users": { - "description": "User logins (\"octocat\").", - "type": "array", - "items": { - "type": "string" - } - }, - "teams": { - "description": "Team slugs (\"platform-team\").", - "type": "array", - "items": { - "type": "string" - } - }, - "apps": { - "description": "GitHub App slugs (\"deploy-gate\").", - "type": "array", - "items": { - "type": "string" - } - } - }, - "additionalProperties": {} - }, - "bypass_pull_request_allowances": { - "description": "Who may push without a pull request review, as `users`, `teams`, and `apps` lists of login/slug strings, each list optional. GitHub's GET expands each actor into an object, which the PUT rejects, so a copied one is refused naming the string to write.", - "type": "object", - "properties": { - "users": { - "description": "User logins (\"octocat\").", - "type": "array", - "items": { - "type": "string" - } - }, - "teams": { - "description": "Team slugs (\"platform-team\").", - "type": "array", - "items": { - "type": "string" - } - }, - "apps": { - "description": "GitHub App slugs (\"deploy-gate\").", - "type": "array", - "items": { - "type": "string" - } - } - }, - "additionalProperties": {} - } - }, - "additionalProperties": {} - }, - { - "type": "null" - } - ] - }, - "restrictions": { - "description": "Who may push to the branch, as `users`, `teams`, and `apps` lists of login/slug strings; the PUT requires `users` and `teams` (`[]` when none) and takes `apps` as optional, so a mapping missing either is refused upfront. An all-empty mapping lets nobody push, `null` lifts the restriction, and a copied GET actor object is refused naming the string to write. Literal branches only; wildcard rules point at the rulesets section.", - "anyOf": [ - { - "type": "object", - "properties": { - "users": { - "description": "User logins (\"octocat\"); required, `[]` when none.", - "type": "array", - "items": { - "type": "string" - } - }, - "teams": { - "description": "Team slugs (\"platform-team\"); required, `[]` when none.", - "type": "array", - "items": { - "type": "string" - } - }, - "apps": { - "description": "GitHub App slugs (\"deploy-gate\"); optional.", - "type": "array", - "items": { - "type": "string" - } - } - }, - "required": [ - "users", - "teams" - ], - "additionalProperties": {} - }, - { - "type": "null" - } - ] - }, - "required_signatures": { - "description": "Require signed commits on the branch. On a literal branch this is a routed key the protection PUT silently drops, so it is applied through the POST/DELETE .../protection/required_signatures sub-endpoint: when it drifts, and again after any protection PUT (GitHub does not document whether the PUT preserves an existing requirement), so declare the toggle on any branch that carries one - a declared value is pinned either way. On a wildcard rule it rides the GraphQL rule mutation like every other key.", - "type": "boolean" - }, - "force_push_bypassers": { - "description": "Who may force-push to the branch when \"allow force pushes\" is in its \"specify who\" mode. Each actor is one string: a bare login is a user (\"octocat\"), \"org/team-slug\" is a team, and \"app/slug\" is a GitHub App. A REST-invisible surface: on a literal branch this routed key is stripped from the protection PUT and applied through the updateBranchProtectionRule GraphQL mutation when it drifts, and again after any protection PUT; on a wildcard rule it rides the create or update mutation with the rest of the rule. The live list is read back through GraphQL. An empty list clears every allowance; an absent key leaves the live list untouched.", - "type": "array", - "items": { - "type": "string" - } - }, - "required_deployments": { - "description": "Require deployments to succeed before merging (the checkbox and its environment list). REST-invisible like force_push_bypassers, so the routed key rides the same GraphQL mutation. Declaring `null` turns the requirement OFF; an absent key leaves the live state untouched. GitHub SILENTLY drops environment names that do not exist on the repository, so apply verifies the mutation's read-back and fails loudly naming any dropped name; the environments section runs before branches, so environments declared in the same settings file exist by the time this key applies.", - "anyOf": [ - { - "type": "object", - "properties": { - "environments": { - "description": "The environments whose deployments must succeed before merging.", - "type": "array", - "items": { - "type": "string" - } - } - }, - "required": [ - "environments" - ], - "additionalProperties": false - }, - { - "type": "null" - } - ] - } - }, - "additionalProperties": {} - }, - "BypassActorConfig": { - "description": "One actor that may bypass the ruleset. Integration, RepositoryRole, Team, and User need their numeric actor_id; DeployKey takes none (null); OrganizationAdmin ignores it.", - "type": "object", - "properties": { - "actor_id": { - "description": "The id GitHub assigns the app, role, team, or user. Required for Integration, RepositoryRole, Team, and User; refused for DeployKey (write null or omit it); ignored for OrganizationAdmin.", - "type": [ - "integer", - "null" - ] - }, - "actor_type": { - "description": "Integration, OrganizationAdmin, RepositoryRole, Team, DeployKey, or User, as GitHub spells them.", - "type": "string", - "enum": [ - "Integration", - "OrganizationAdmin", - "RepositoryRole", - "Team", - "DeployKey", - "User" - ] - }, - "bypass_mode": { - "description": "\"always\" (the default), \"pull_request\" (bypass only through a pull request; branch rulesets only, and never for a DeployKey), or \"exempt\" (rules are not run for the actor).", - "type": "string", - "enum": [ - "always", - "pull_request", - "exempt" - ] - } - }, - "required": [ - "actor_type" - ], - "additionalProperties": {} - }, - "CheckSuitePreferencesConfig": { - "description": "PATCH /repos/{r}/check-suites/preferences, sent verbatim. Write-only upstream: GitHub exposes no read endpoint for these preferences, so check mode cannot verify them and apply re-asserts the declared preferences on every run.", - "type": "object", - "properties": { - "auto_trigger_checks": { - "description": "Per-app toggles for whether pushes automatically create check suites, one entry per app_id.", - "type": "array", - "items": { - "$ref": "#/definitions/AutoTriggerCheckConfig" - } - } - }, - "required": [ - "auto_trigger_checks" - ], - "additionalProperties": { - "description": "Future preference fields pass through verbatim." - } - }, - "CodeQualitySetupConfig": { - "description": "PATCH /repos/{r}/code-quality/setup, sent verbatim.", - "type": "object", - "properties": { - "state": { - "description": "Turn code quality analysis on (\"configured\") or off (\"not-configured\").", - "type": "string", - "enum": [ - "configured", - "not-configured" - ] - }, - "languages": { - "description": "Languages to analyze, in the PATCH's vocabulary (the GET's \"rust\" cannot be declared and stays as detected), compared as a set; GitHub auto-detects when omitted.", - "type": "array", - "items": { - "type": "string", - "enum": [ - "csharp", - "go", - "java-kotlin", - "javascript-typescript", - "python", - "ruby" - ] - } - }, - "runner_type": { - "description": "Run on GitHub-hosted (\"standard\") or labeled self-hosted runners.", - "type": "string", - "enum": [ - "standard", - "labeled" - ] - }, - "runner_label": { - "description": "Runner label: a string needs runner_type: \"labeled\" and is refused under any other; null clears it under runner_type: \"standard\" or none.", - "type": [ - "string", - "null" - ] - }, - "ai_findings_option": { - "description": "AI-powered findings: \"on_push\" runs them on every push, \"disabled\" turns them off.", - "type": "string", - "enum": [ - "disabled", - "on_push" - ] - } - } - }, - "CodeScanningDefaultSetupConfig": { - "description": "PATCH /repos/{r}/code-scanning/default-setup, sent verbatim.", - "type": "object", - "properties": { - "state": { - "description": "Turn default setup on (\"configured\") or off (\"not-configured\").", - "type": "string", - "enum": [ - "configured", - "not-configured" - ] - }, - "query_suite": { - "description": "CodeQL query suite to run.", - "type": "string", - "enum": [ - "default", - "extended" - ] - }, - "languages": { - "description": "Languages to scan, in the PATCH's vocabulary (\"javascript-typescript\", never the GET's separate \"javascript\" and \"typescript\"), compared as a set; GitHub auto-detects when omitted.", - "type": "array", - "items": { - "type": "string", - "enum": [ - "actions", - "c-cpp", - "csharp", - "go", - "java-kotlin", - "javascript-typescript", - "python", - "ruby", - "swift" - ] - } - }, - "runner_type": { - "description": "Run on GitHub-hosted (\"standard\") or labeled self-hosted runners.", - "type": "string", - "enum": [ - "standard", - "labeled" - ] - }, - "runner_label": { - "description": "Runner label: a string needs runner_type: \"labeled\" and is refused under any other; null clears it under runner_type: \"standard\" or none.", - "type": [ - "string", - "null" - ] - }, - "threat_model": { - "description": "Whether to model local sources as threats in addition to remote ones.", - "type": "string", - "enum": [ - "remote", - "remote_and_local" - ] - } - } - }, - "CodeScanningToolConfig": { - "description": "One tool that must provide code scanning results.", - "type": "object", - "properties": { - "alerts_threshold": { - "description": "The alert severity that blocks the update: none, errors, errors_and_warnings, or all.", - "type": "string", - "enum": [ - "none", - "errors", - "errors_and_warnings", - "all" - ] - }, - "security_alerts_threshold": { - "description": "The security severity that blocks the update: none, critical, high_or_higher, medium_or_higher, or all.", - "type": "string", - "enum": [ - "none", - "critical", - "high_or_higher", - "medium_or_higher", - "all" - ] - }, - "tool": { - "description": "The code scanning tool's name.", - "type": "string" - } - }, - "required": [ - "alerts_threshold", - "security_alerts_threshold", - "tool" - ], - "additionalProperties": {} - }, - "CodespacesSecretConfig": { - "description": "One repository Codespaces secret, matched by case-insensitive name (GitHub stores secret names uppercase). Keys other than name and value are rejected: the API body is built from the sealed value alone, so an extra key would silently do nothing.", - "type": "object", - "properties": { - "name": { - "description": "The secret name, the natural key; compared case-insensitively and written uppercase. GitHub's naming rule is checked when the file is parsed, before any API call: ASCII letters, digits, and underscores only, not starting with a digit, and not starting with the reserved `GITHUB_` prefix in any case (`github_token` is the reserved `GITHUB_TOKEN` once uppercased).", - "type": "string", - "pattern": "^(?![Gg][Ii][Tt][Hh][Uu][Bb]_)[A-Za-z_][A-Za-z0-9_]*$" - }, - "value": { - "description": "A whole-value `$NAME` reference to an environment variable holding the secret - never a literal (settings files are committed plaintext). Resolved from the action step's env at run time and sealed client-side into the sealed box GitHub's secrets API expects (libsodium's crypto_box_seal format) before upload; GitHub cannot return the value, so check mode verifies existence only and apply re-seals it on every run.", - "type": "string" - } - }, - "required": [ - "name", - "value" - ] - }, - "CollaboratorConfig": { - "description": "One direct collaborator, matched by username. Keys other than username and permission are rejected (a misspelled \"permission\" would otherwise silently grant the default role).", - "type": "object", - "properties": { - "username": { - "description": "GitHub login, the natural key.", - "type": "string" - }, - "permission": { - "description": "\"pull\", \"triage\", \"push\", \"maintain\", \"admin\", or a custom org role name; defaults to \"push\". The grant's own vocabulary, lowercase: \"read\" and \"write\" are what GitHub reports a role as, not what a grant accepts, so they and a mis-cased standard permission fail at parse time, naming the form to declare.", - "type": "string", - "pattern": "^(?:pull|triage|push|maintain|admin|(?!(?:[Pp][Uu][Ll][Ll]|[Tt][Rr][Ii][Aa][Gg][Ee]|[Pp][Uu][Ss][Hh]|[Mm][Aa][Ii][Nn][Tt][Aa][Ii][Nn]|[Aa][Dd][Mm][Ii][Nn]|[Ww][Rr][Ii][Tt][Ee]|[Rr][Ee][Aa][Dd])$)\\S(?:.*\\S)?)$" - } - }, - "required": [ - "username" - ] - }, - "CustomPropertyConfig": { - "description": "One custom property value, matched by the API's property_name verbatim. Keys other than property_name and value are rejected: the bulk PATCH body is built from exactly these two fields, so an extra key would have no destination.", - "type": "object", - "properties": { - "property_name": { - "description": "The organization-defined property's name, the natural key.", - "type": "string" - }, - "value": { - "description": "The value to set: a string (single_select and string properties), a list of strings (multi_select, compared as a set - list each option once), or a boolean (true_false, normalized to the \"true\"/\"false\" string GitHub transports). Numbers are likewise sent as their string form - through YAML's parsed number, so quote any numeric value you want sent verbatim (unquoted, 1.10 arrives as \"1.1\" and 1e21 as \"1e+21\"). `null` unsets the property, reverting to the org default, if any.", - "anyOf": [ - { - "type": "string" - }, - { - "type": "array", - "items": { - "type": "string" - } - }, - { - "type": "boolean" - }, - { - "type": "number" - }, - { - "type": "null" - } - ] - } - }, - "required": [ - "property_name", - "value" - ] - }, - "DependabotSecretConfig": { - "description": "One repository Dependabot secret, matched by case-insensitive name (GitHub stores secret names uppercase). Keys other than name and value are rejected: the API body is built from the sealed value alone, so an extra key would silently do nothing.", - "type": "object", - "properties": { - "name": { - "description": "The secret name, the natural key; compared case-insensitively and written uppercase. GitHub's naming rule is checked when the file is parsed, before any API call: ASCII letters, digits, and underscores only, not starting with a digit, and not starting with the reserved `GITHUB_` prefix in any case (`github_token` is the reserved `GITHUB_TOKEN` once uppercased).", - "type": "string", - "pattern": "^(?![Gg][Ii][Tt][Hh][Uu][Bb]_)[A-Za-z_][A-Za-z0-9_]*$" - }, - "value": { - "description": "A whole-value `$NAME` reference to an environment variable holding the secret - never a literal (settings files are committed plaintext). Resolved from the action step's env at run time and sealed client-side into the sealed box GitHub's secrets API expects (libsodium's crypto_box_seal format) before upload; GitHub cannot return the value, so check mode verifies existence only and apply re-seals it on every run.", - "type": "string" - } - }, - "required": [ - "name", - "value" - ] - }, - "DeployKeyConfig": { - "description": "One deploy key, matched by exact title (GitHub documents no case folding for titles). Extra fields pass through to the create call verbatim.", - "type": "object", - "properties": { - "title": { - "description": "The key title shown in the settings UI, the natural key.", - "type": "string" - }, - "key": { - "description": "The PUBLIC key as one line, \" [comment]\" (the .pub file), the algorithm one of ssh-ed25519, ssh-rsa, ecdsa-sha2-nistp256/384/521, sk-ssh-ed25519@openssh.com, sk-ecdsa-sha2-nistp256@openssh.com (GitHub dropped DSA in 2022). Anything else, a line break included, is refused when the settings file is read; a private key is named, never echoed. Compared as algorithm + blob, comment ignored; immutable upstream, so a changed key is delete plus recreate.", - "type": "string", - "pattern": "^[ \\t]*(ssh-ed25519|ssh-rsa|ecdsa-sha2-nistp256|ecdsa-sha2-nistp384|ecdsa-sha2-nistp521|sk-ssh-ed25519@openssh\\.com|sk-ecdsa-sha2-nistp256@openssh\\.com)[ \\t]+((?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{4}|[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=))(?:[ \\t][^\\r\\n]*)?$" - }, - "read_only": { - "description": "Whether the key is restricted to read-only access; GitHub defaults to false (read/write).", - "type": "boolean" - } - }, - "required": [ - "title", - "key" - ] - }, - "DeploymentBranchPolicyConfig": { - "description": "One custom deployment branch-policy pattern, matched by exact name. Extra fields pass through to the create call verbatim.", - "type": "object", - "properties": { - "name": { - "description": "The name pattern branches or tags must match to deploy (e.g. \"release/*\"), the natural key.", - "type": "string" - }, - "type": { - "description": "What the pattern matches: \"branch\" (the upstream default) or \"tag\". Immutable on GitHub, so changing it is applied as delete plus recreate.", - "type": "string", - "enum": [ - "branch", - "tag" - ] - } - }, - "required": [ - "name" - ] - }, - "DeploymentProtectionRuleConfig": { - "description": "One custom deployment protection rule, matched by the slug of the GitHub App that provides it. No other key is accepted: the enable call sends only the App's resolved integration id, so an extra key would have no destination.", - "type": "object", - "properties": { - "app": { - "description": "The slug of the GitHub App providing the gate (e.g. \"my-gate-app\"), the natural key.", - "type": "string" - } - }, - "required": [ - "app" - ], - "additionalProperties": false - }, - "EnvironmentConfig": { - "description": "One deployment environment, matched by name.", - "type": "object", - "properties": { - "name": { - "description": "The environment name, the natural key.", - "type": "string" - }, - "pinned": { - "description": "Pin this environment on the repository home page's deployments sidebar (GraphQL-only; the REST environment PUT carries no pin field). Pin ORDER is the declaration order of the entries with `pinned: true` - together they must LEAD the repository's pinned list in that order, compared by rank (GitHub's live position numbers may carry holes after an unpin and are never read literally). `pinned: false` unpins; an entry without the key leaves its pin state untouched. Live pins on environments the settings file does not declare are never unpinned; when they sit among the declared ranks, apply moves them after the declared pins (surfaced as a note). GitHub allows at most 10 pinned environments, so more than 10 `pinned: true` entries are rejected upfront.", - "type": "boolean" - }, - "wait_timer": { - "description": "Minutes to wait before deployments proceed, 0 to 43200 (30 days). 0 declares the wait timer off and matches a live environment without one.", - "type": "integer", - "minimum": 0, - "maximum": 43200 - }, - "prevent_self_review": { - "description": "Whether to prevent the deployer from approving their own deployment. The flag lives on the required-reviewers rule, so `true` needs at least one reviewer; `false` declares it off and matches a live environment without the rule.", - "type": "boolean" - }, - "reviewers": { - "description": "Required reviewers by numeric user/team id, at most 6. An empty list declares required reviewers off and matches a live environment without the rule.", - "maxItems": 6, - "type": "array", - "items": { - "type": "object", - "properties": { - "type": { - "description": "User or Team.", - "type": "string", - "enum": [ - "User", - "Team" - ] - }, - "id": { - "description": "The user's or team's numeric id.", - "type": "number" - } - }, - "required": [ - "type", - "id" - ] - } - }, - "deployment_branch_policy": { - "description": "Which branches may deploy; null lets any branch deploy. As an object, exactly one of the two flags is true: both true is rejected by GitHub, and both false is spelled null.", - "anyOf": [ - { - "type": "object", - "properties": { - "protected_branches": { - "description": "Restrict to branches with protection rules.", - "type": "boolean" - }, - "custom_branch_policies": { - "description": "Restrict to name patterns, declared under `deployment_branch_policies`.", - "type": "boolean" - } - }, - "required": [ - "protected_branches", - "custom_branch_policies" - ], - "if": { - "properties": { - "protected_branches": { - "const": true - } - } - }, - "then": { - "properties": { - "custom_branch_policies": { - "const": false - } - } - }, - "else": { - "properties": { - "custom_branch_policies": { - "const": true - } - } - } - }, - { - "type": "null" - } - ] - }, - "deployment_branch_policies": { - "description": "Custom deployment branch-policy patterns for this environment, reconciled only when this key is declared (an absent key leaves the live patterns untouched). Declaring it requires the sibling `deployment_branch_policy` to set `custom_branch_policies: true`; without the flag GitHub rejects every pattern write. A pattern's `type` is immutable on GitHub, so a declared type that differs from the live one is applied as delete plus recreate. Within a declared key, live patterns the entries do not declare are DELETED by default; the wrapped `{_undeclared: keep, entries}` form keeps them as notes.", - "anyOf": [ - { - "type": "array", - "items": { - "$ref": "#/definitions/DeploymentBranchPolicyConfig" - } - }, - { - "$ref": "#/definitions/UndeclaredPolicyList%3CDeploymentBranchPolicyConfig%3E" - } - ] - }, - "deployment_protection_rules": { - "description": "Custom deployment protection rules for this environment, reconciled only when this key is declared (an absent key leaves the live rules untouched). Each rule is a GitHub App gate, declared by its App slug and resolved to the App's integration id at apply time; GitHub offers no update call, so the model is enable/disable only. Within a declared key, live rules the entries do not declare are KEPT by default - Apps can enable themselves as gates, and silently removing a deployment gate is security-relevant - and the wrapped `{_undeclared: delete, entries}` form opts into disabling them.", - "anyOf": [ - { - "type": "array", - "items": { - "$ref": "#/definitions/DeploymentProtectionRuleConfig" - } - }, - { - "$ref": "#/definitions/UndeclaredPolicyList%3CDeploymentProtectionRuleConfig%3E" - } - ] - }, - "variables": { - "description": "Actions variables for this environment, reconciled only when this key is declared (an absent key leaves the live variables untouched). Values are plain text by design - use environment secrets for anything sensitive. Within a declared `variables` key, live variables the entries do not declare are DELETED by default; the wrapped `{_undeclared: keep, entries}` form keeps them as notes. Names match case-insensitively, as GitHub treats them.", - "anyOf": [ - { - "type": "array", - "items": { - "$ref": "#/definitions/EnvironmentVariableConfig" - } - }, - { - "$ref": "#/definitions/UndeclaredPolicyList%3CEnvironmentVariableConfig%3E" - } - ] - }, - "secrets": { - "description": "Actions secrets for this environment, reconciled only when this key is declared (an absent key leaves the live secrets untouched). Each value is a whole-value `$NAME` reference to the action step's environment, never a literal, sealed client-side against the environment's public key; GitHub cannot return a value, so check mode verifies existence only and apply re-seals every declared value on each run. Within a declared `secrets` key, live secrets the entries do not declare are KEPT by default (their values are unrecoverable); the wrapped `{_undeclared: delete, entries}` form opts into deletion.", - "anyOf": [ - { - "type": "array", - "items": { - "$ref": "#/definitions/EnvironmentSecretConfig" - } - }, - { - "$ref": "#/definitions/UndeclaredPolicyList%3CEnvironmentSecretConfig%3E" - } - ] - } - }, - "required": [ - "name" - ], - "allOf": [ - { - "if": { - "required": [ - "deployment_branch_policies" - ] - }, - "then": { - "required": [ - "deployment_branch_policy" - ], - "properties": { - "deployment_branch_policy": { - "type": "object", - "required": [ - "custom_branch_policies" - ], - "properties": { - "custom_branch_policies": { - "const": true - } - } - } - } - } - }, - { - "if": { - "required": [ - "prevent_self_review" - ], - "properties": { - "prevent_self_review": { - "const": true - } - } - }, - "then": { - "required": [ - "reviewers" - ], - "properties": { - "reviewers": { - "minItems": 1 - } - } - } - } - ] - }, - "EnvironmentSecretConfig": { - "description": "One per-environment Actions secret, matched by case-insensitive name (GitHub stores secret names uppercase). Keys other than name and value are rejected: the API body is built from the sealed value alone, so an extra key would silently do nothing.", - "type": "object", - "properties": { - "name": { - "description": "The secret name, the natural key; compared case-insensitively and written uppercase. GitHub's naming rule is checked when the file is parsed, before any API call: ASCII letters, digits, and underscores only, not starting with a digit, and not starting with the reserved `GITHUB_` prefix in any case (`github_token` is the reserved `GITHUB_TOKEN` once uppercased).", - "type": "string", - "pattern": "^(?![Gg][Ii][Tt][Hh][Uu][Bb]_)[A-Za-z_][A-Za-z0-9_]*$" - }, - "value": { - "description": "A whole-value `$NAME` reference to an environment variable holding the secret - never a literal (settings files are committed plaintext). Resolved from the action step's env at run time and sealed client-side into the sealed box GitHub's secrets API expects (libsodium's crypto_box_seal format) before upload; GitHub cannot return the value, so check mode verifies existence only and apply re-seals it on every run.", - "type": "string" - } - }, - "required": [ - "name", - "value" - ], - "additionalProperties": false - }, - "EnvironmentVariableConfig": { - "description": "One per-environment Actions variable, matched by case-insensitive name.", - "type": "object", - "properties": { - "name": { - "description": "The variable name, the natural key (case-insensitive on GitHub). GitHub's naming rule is checked when the file is parsed, before any API call: ASCII letters, digits, and underscores only, not starting with a digit, and not starting with the reserved `GITHUB_` prefix in any case.", - "type": "string", - "pattern": "^(?![Gg][Ii][Tt][Hh][Uu][Bb]_)[A-Za-z_][A-Za-z0-9_]*$" - }, - "value": { - "description": "The plain-text value. GitHub caps one variable at 48 KB; a value over 49152 bytes of UTF-8 is refused when the file is parsed, before any API call.", - "type": "string", - "maxLength": 49152 - } - }, - "required": [ - "name", - "value" - ] - }, - "InteractionLimitsConfig": { - "description": "The `interaction_limits:` section, exactly four keys: `limit` and `expiry` are the body of PUT /repos/{r}/interaction-limits, and the two pull-request keys go to their own .../interaction-limits/pulls sub-endpoints. Any other key fails at parse time, origin and expires_at included: GitHub reports them on the GET but never accepts them. Declare at least one of `limit`, `pull_request_creation_cap`, or `pull_request_creation_bypass`.", - "type": "object", - "properties": { - "limit": { - "description": "Who may interact: \"existing_users\", \"contributors_only\", or \"collaborators_only\"; any other value fails at parse time (GitHub would 422 the PUT). Optional when only the pull-request keys below are declared; an omitted limit leaves the live base limit untouched.", - "type": "string", - "enum": [ - "existing_users", - "contributors_only", - "collaborators_only" - ] - }, - "expiry": { - "description": "How long the limit lasts: \"one_day\", \"three_days\", \"one_week\", \"one_month\", or \"six_months\"; GitHub defaults to one_day, and any other value fails at parse time. Write-only: GitHub reports back the computed expires_at, never the duration, so check mode cannot verify this field and apply re-arms it on every run. Requires a sibling `limit`.", - "type": "string", - "enum": [ - "one_day", - "three_days", - "one_week", - "one_month", - "six_months" - ] - }, - "pull_request_creation_cap": { - "description": "The pull request creation cap, routed to GET/PATCH /repos/{r}/interaction-limits/pulls/creation-cap. Unlike the base limit it is persistent desired state with no self-expiry and reads back verbatim, so check mode diffs it exactly and apply PATCHes only on divergence. max_open_pull_requests is 1-1000. On repositories where the cap is not available, the endpoints answer 405: apply surfaces that as a note, check mode as drift.", - "type": "object", - "properties": { - "enabled": { - "description": "Whether the cap is enforced.", - "type": "boolean" - }, - "max_open_pull_requests": { - "description": "The maximum number of open pull requests one user may have, a whole number from 1 to 1000 (GitHub's range); anything else fails at parse time.", - "type": "integer", - "minimum": 1, - "maximum": 1000 - } - }, - "required": [ - "enabled" - ] - }, - "pull_request_creation_bypass": { - "description": "User logins exempt from the pull request creation cap, routed to GET/PUT/DELETE /repos/{r}/interaction-limits/pulls/bypass-list and reconciled: apply removes the undeclared logins and then adds the missing ones (removals first - the list holds at most 100 users); logins compare case-insensitively. An empty list removes everyone. At most 100 logins.", - "type": "array", - "items": { - "type": "string" - } - } - }, - "additionalProperties": false - }, - "LabelConfig": { - "description": "One label, matched to the live repo by name.", - "type": "object", - "properties": { - "name": { - "description": "The label name, the natural key.", - "type": "string" - }, - "color": { - "description": "Six hex digits, the leading \"#\" optional (\"#d73a4a\" or \"d73a4a\"). Color names and three-digit shorthand fail at parse time; GitHub rejects them.", - "type": "string", - "pattern": "^#?[0-9a-fA-F]{6}$" - }, - "description": { - "description": "Short explanation shown in the label picker, at most 100 characters (GitHub's cap); longer ones fail at parse time.", - "type": "string", - "maxLength": 100 - }, - "new_name": { - "description": "Probot compat: rename an existing label.", - "type": "string" - } - }, - "required": [ - "name" - ] - }, - "LayeredList": { - "description": "The wrapped form of a list section that applies no undeclared policy (environments, branches, workflows): the plain array beside `{_layering, entries}`. The wrapper exists for the render directive alone, so the rendered document always holds the plain array, and with `_layering` omitted it behaves exactly like the plain array. Its keys are strict: anything besides `_layering` and `entries` (there is no `_undeclared` here, since the section keeps every live resource it does not declare) is rejected upfront as a typo.", - "type": "object", - "properties": { - "_layering": { - "description": "A render-time directive: how this section combines with the layers BELOW it in a layered merge, by the section's key (an environment's name, a branch name or pattern, a workflow path). \"replace\" lets this layer's list win wholesale; \"shallow\" unions the entries by key and swaps a same-key entry for this layer's; \"deep\" unions by key and merges a same-key pair field by field, the entry's nested lists (an environment's variables, secrets, branch policies, protection rules, and reviewers, each by its own key) unioning the same way. Omitted, the file's `_layering` applies, else the run's layering. It has no effect on a single document, and the rendered document never carries it.", - "type": "string", - "enum": [ - "replace", - "shallow", - "deep" - ] - }, - "entries": { - "description": "The declared entries, exactly as the plain array form lists them.", - "type": "array", - "items": { - "$ref": "#/definitions/BranchConfig" - } - } - }, - "required": [ - "entries" - ], - "additionalProperties": false - }, - "LayeredList": { - "description": "The wrapped form of a list section that applies no undeclared policy (environments, branches, workflows): the plain array beside `{_layering, entries}`. The wrapper exists for the render directive alone, so the rendered document always holds the plain array, and with `_layering` omitted it behaves exactly like the plain array. Its keys are strict: anything besides `_layering` and `entries` (there is no `_undeclared` here, since the section keeps every live resource it does not declare) is rejected upfront as a typo.", - "type": "object", - "properties": { - "_layering": { - "description": "A render-time directive: how this section combines with the layers BELOW it in a layered merge, by the section's key (an environment's name, a branch name or pattern, a workflow path). \"replace\" lets this layer's list win wholesale; \"shallow\" unions the entries by key and swaps a same-key entry for this layer's; \"deep\" unions by key and merges a same-key pair field by field, the entry's nested lists (an environment's variables, secrets, branch policies, protection rules, and reviewers, each by its own key) unioning the same way. Omitted, the file's `_layering` applies, else the run's layering. It has no effect on a single document, and the rendered document never carries it.", - "type": "string", - "enum": [ - "replace", - "shallow", - "deep" - ] - }, - "entries": { - "description": "The declared entries, exactly as the plain array form lists them.", - "type": "array", - "items": { - "$ref": "#/definitions/EnvironmentConfig" - } - } - }, - "required": [ - "entries" - ], - "additionalProperties": false - }, - "LayeredList": { - "description": "The wrapped form of a list section that applies no undeclared policy (environments, branches, workflows): the plain array beside `{_layering, entries}`. The wrapper exists for the render directive alone, so the rendered document always holds the plain array, and with `_layering` omitted it behaves exactly like the plain array. Its keys are strict: anything besides `_layering` and `entries` (there is no `_undeclared` here, since the section keeps every live resource it does not declare) is rejected upfront as a typo.", - "type": "object", - "properties": { - "_layering": { - "description": "A render-time directive: how this section combines with the layers BELOW it in a layered merge, by the section's key (an environment's name, a branch name or pattern, a workflow path). \"replace\" lets this layer's list win wholesale; \"shallow\" unions the entries by key and swaps a same-key entry for this layer's; \"deep\" unions by key and merges a same-key pair field by field, the entry's nested lists (an environment's variables, secrets, branch policies, protection rules, and reviewers, each by its own key) unioning the same way. Omitted, the file's `_layering` applies, else the run's layering. It has no effect on a single document, and the rendered document never carries it.", - "type": "string", - "enum": [ - "replace", - "shallow", - "deep" - ] - }, - "entries": { - "description": "The declared entries, exactly as the plain array form lists them.", - "type": "array", - "items": { - "$ref": "#/definitions/WorkflowConfig" - } - } - }, - "required": [ - "entries" - ], - "additionalProperties": false - }, - "MilestoneConfig": { - "description": "One milestone, matched by title.", - "type": "object", - "properties": { - "title": { - "description": "The milestone title, the natural key.", - "type": "string" - }, - "description": { - "description": "Longer explanation of the milestone.", - "type": "string" - }, - "state": { - "description": "Open or closed; untouched unless declared.", - "type": "string", - "enum": [ - "open", - "closed" - ] - }, - "due_on": { - "description": "The due day as `YYYY-MM-DD`; GitHub keeps only the day, so only the day is compared, and a snapshot reads it back as the day. The day is sent as noon UTC, `YYYY-MM-DDT12:00:00Z`, which GitHub stores on the same calendar day in PST and PDT. A UTC timestamp `YYYY-MM-DDTHH:MM:SSZ` is accepted for its date part.", - "anyOf": [ - { - "type": "string", - "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))$" - }, - { - "type": "string", - "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d:[0-5]\\d(?:\\.\\d+)?(?:Z))$" - } - ] - } - }, - "required": [ - "title" - ] - }, - "PagesConfig": { - "description": "GitHub Pages site configuration; use `pages: null` to disable the site.", - "type": "object", - "properties": { - "build_type": { - "description": "\"workflow\" (GitHub Actions) or \"legacy\" (branch).", - "type": "string", - "enum": [ - "workflow", - "legacy" - ] - }, - "source": { - "description": "The update PUT requires both branch and path when source is sent.", - "type": "object", - "properties": { - "branch": { - "description": "The branch Pages publishes from.", - "type": "string" - }, - "path": { - "description": "The directory within the branch Pages publishes: / or /docs.", - "type": "string", - "enum": [ - "/", - "/docs" - ] - } - }, - "required": [ - "branch" - ] - }, - "cname": { - "description": "Custom domain; null removes it.", - "type": [ - "string", - "null" - ] - }, - "https_enforced": { - "description": "Whether HTTPS is enforced for the site.", - "type": "boolean" - }, - "public": { - "description": "Enterprise Cloud site visibility: true for public, false for repository members only. Only an organization on Enterprise Cloud can set it; everywhere else GitHub reports true and ignores the field, so false drifts on every run.", - "type": "boolean" - } - }, - "not": { - "anyOf": [ - { - "required": [ - "url" - ] - }, - { - "required": [ - "html_url" - ] - }, - { - "required": [ - "status" - ] - }, - { - "required": [ - "custom_404" - ] - }, - { - "required": [ - "protected_domain_state" - ] - }, - { - "required": [ - "pending_domain_unverified_at" - ] - }, - { - "required": [ - "https_certificate" - ] - } - ] - } - }, - "PatternRuleParameters": { - "description": "The pattern and operator the five *_pattern rule types share.", - "type": "object", - "properties": { - "name": { - "description": "How the rule appears when configuring it.", - "type": "string" - }, - "negate": { - "description": "Whether the rule fails when the pattern matches.", - "type": "boolean" - }, - "operator": { - "description": "starts_with, ends_with, contains, or regex, in that spelling.", - "type": "string", - "enum": [ - "starts_with", - "ends_with", - "contains", - "regex" - ] - }, - "pattern": { - "description": "The pattern to match.", - "type": "string" - } - }, - "required": [ - "operator", - "pattern" - ], - "additionalProperties": {} - }, - "RepositoryConfig": { - "description": "The `repository:` section, where only declared keys are ever applied or compared. A field not documented here is sent verbatim to PATCH /repos/{r}, so a field GitHub adds later works unchanged; the keys below carry GitHub's types (a toggle is true or false, never null or quoted) or route to their own endpoints. GET-only fields (ids, urls, counts, has_downloads, has_pages, custom_properties) are refused, since they would drift on every run.", - "type": "object", - "properties": { - "description": { - "description": "A short description of the repository; null clears it.", - "type": [ - "string", - "null" - ] - }, - "homepage": { - "description": "A URL with more information about the repository; null clears it.", - "type": [ - "string", - "null" - ] - }, - "private": { - "description": "Make the repository private (true) or public (false).", - "type": "boolean" - }, - "visibility": { - "description": "public, private, or internal (Enterprise); a free string, since the spec omits internal.", - "type": "string" - }, - "security_and_analysis": { - "description": "The security_and_analysis object of PATCH /repos/{r}: GitHub Advanced Security, secret scanning, push protection, and the other nested feature toggles.", - "anyOf": [ - { - "$ref": "#/definitions/SecurityAndAnalysisConfig" - }, - { - "type": "null" - } - ] - }, - "has_issues": { - "description": "Enable issues (true) or disable them (false).", - "type": "boolean" - }, - "has_projects": { - "description": "Enable projects (true) or disable them (false); an organization that disabled repository projects rejects true.", - "type": "boolean" - }, - "has_wiki": { - "description": "Enable the wiki (true) or disable it (false).", - "type": "boolean" - }, - "has_discussions": { - "description": "Enable discussions (true) or disable them (false); accepted by the PATCH though the descriptor omits it.", - "type": "boolean" - }, - "has_pull_requests": { - "description": "Allow pull requests (true) or prevent them (false).", - "type": "boolean" - }, - "pull_request_creation_policy": { - "description": "Who may create pull requests: \"all\" (everyone) or \"collaborators_only\".", - "type": "string", - "enum": [ - "all", - "collaborators_only" - ] - }, - "is_template": { - "description": "Offer the repository as a template (true) or not (false).", - "type": "boolean" - }, - "default_branch": { - "description": "The default branch; an existing branch name.", - "type": "string" - }, - "allow_squash_merge": { - "description": "Allow squash-merging pull requests (true) or not (false).", - "type": "boolean" - }, - "allow_merge_commit": { - "description": "Allow merge commits on pull requests (true) or not (false).", - "type": "boolean" - }, - "allow_rebase_merge": { - "description": "Allow rebase-merging pull requests (true) or not (false).", - "type": "boolean" - }, - "allow_auto_merge": { - "description": "Allow auto-merge on pull requests (true) or not (false).", - "type": "boolean" - }, - "delete_branch_on_merge": { - "description": "Delete head branches when pull requests merge (true) or keep them (false).", - "type": "boolean" - }, - "allow_update_branch": { - "description": "Offer to update a pull request branch behind its base (true) or not (false).", - "type": "boolean" - }, - "use_squash_pr_title_as_default": { - "description": "Deprecated by GitHub in favor of squash_merge_commit_title: use the PR title as the squash commit title (true) or not (false).", - "type": "boolean" - }, - "squash_merge_commit_title": { - "description": "The default title of a squash merge commit: PR_TITLE or COMMIT_OR_PR_TITLE. Required beside squash_merge_commit_message; legal pairs are PR_TITLE with PR_BODY, BLANK, or COMMIT_MESSAGES, and COMMIT_OR_PR_TITLE with COMMIT_MESSAGES.", - "type": "string", - "enum": [ - "PR_TITLE", - "COMMIT_OR_PR_TITLE" - ] - }, - "squash_merge_commit_message": { - "description": "The default message of a squash merge commit: PR_BODY, BLANK, or COMMIT_MESSAGES. Needs squash_merge_commit_title declared beside it, in one of the legal pairs.", - "type": "string", - "enum": [ - "PR_BODY", - "BLANK", - "COMMIT_MESSAGES" - ] - }, - "merge_commit_title": { - "description": "The default title of a merge commit: PR_TITLE or MERGE_MESSAGE. Required beside merge_commit_message.", - "type": "string", - "enum": [ - "PR_TITLE", - "MERGE_MESSAGE" - ] - }, - "merge_commit_message": { - "description": "The default message of a merge commit: PR_BODY, PR_TITLE, or BLANK. Needs merge_commit_title declared beside it.", - "type": "string", - "enum": [ - "PR_BODY", - "BLANK", - "PR_TITLE" - ] - }, - "archived": { - "description": "Archive the repository (true) or unarchive it (false).", - "type": "boolean" - }, - "allow_forking": { - "description": "Allow private forks (true) or not (false).", - "type": "boolean" - }, - "web_commit_signoff_required": { - "description": "Require contributors to sign off on web-based commits (true) or not (false).", - "type": "boolean" - }, - "topics": { - "description": "Repository topics, replaced wholesale via PUT /repos/{r}/topics; a comma-separated string or a list, lowercased and deduped. Each topic is 1 to 50 letters, digits, and hyphens, starting with a letter or digit (uppercase is lowercased on the wire); at most 20 distinct topics. An empty entry is refused; an empty list removes every topic.", - "anyOf": [ - { - "type": "string", - "pattern": "^\\s*[A-Za-z0-9][A-Za-z0-9-]{0,49}\\s*(,\\s*[A-Za-z0-9][A-Za-z0-9-]{0,49}\\s*)*$" - }, - { - "type": "array", - "items": { - "type": "string", - "pattern": "^[A-Za-z0-9][A-Za-z0-9-]{0,49}$" - } - } - ] - }, - "enable_vulnerability_alerts": { - "description": "Dependabot alerts, via PUT/DELETE /repos/{r}/vulnerability-alerts. On read, 404 means off.", - "type": "boolean" - }, - "enable_automated_security_fixes": { - "description": "Dependabot security updates, via PUT/DELETE /repos/{r}/automated-security-fixes. On read, 404 means off, as does a 200 body with enabled: false.", - "type": "boolean" - }, - "enable_private_vulnerability_reporting": { - "description": "Private vulnerability reporting, via PUT/DELETE /repos/{r}/private-vulnerability-reporting. Repositories where the feature does not apply (observed: private repos) read as off.", - "type": "boolean" - }, - "enable_git_lfs": { - "description": "Git LFS, via PUT/DELETE /repos/{r}/lfs. Write-only upstream: check mode cannot verify it, and apply re-asserts it on every run.", - "type": "boolean" - }, - "enable_immutable_releases": { - "description": "Immutable releases, via PUT/DELETE /repos/{r}/immutable-releases. On read, 404 means off. When the repository owner enforces immutable releases (enforced_by_owner in the GET body), writes answer 409 and the setting cannot be changed from the repository; apply reports that as a note instead of a change.", - "type": "boolean" - }, - "enable_sponsorships": { - "description": "Display a Sponsor button on the repository, via the GraphQL updateRepository mutation (hasSponsorshipsEnabled) - GraphQL is the setting's only read and write surface (the REST repo PATCH and GET carry no such field). A stored repository toggle independent of any FUNDING.yml content.", - "type": "boolean" - }, - "issue_creation_policy": { - "description": "Who may create issues: \"all\" (everyone) or \"collaborators_only\", mapped to GitHub's ALL/COLLABORATORS_ONLY GraphQL enum at the API boundary. GraphQL-only upstream (Repository.issueCreationPolicy and the updateRepository mutation): the REST repo PATCH accepts an issue_creation_policy field and silently ignores it, and no REST GET returns it.", - "type": "string", - "enum": [ - "all", - "collaborators_only" - ] - } - }, - "additionalProperties": { - "description": "Everything else passes through to PATCH /repos/{r} verbatim." - }, - "allOf": [ - { - "if": { - "required": [ - "squash_merge_commit_message" - ] - }, - "then": { - "required": [ - "squash_merge_commit_title" - ] - } - }, - { - "if": { - "required": [ - "squash_merge_commit_title" - ], - "properties": { - "squash_merge_commit_title": { - "const": "PR_TITLE" - } - } - }, - "then": { - "properties": { - "squash_merge_commit_message": { - "enum": [ - "PR_BODY", - "BLANK", - "COMMIT_MESSAGES" - ] - } - } - } - }, - { - "if": { - "required": [ - "squash_merge_commit_title" - ], - "properties": { - "squash_merge_commit_title": { - "const": "COMMIT_OR_PR_TITLE" - } - } - }, - "then": { - "properties": { - "squash_merge_commit_message": { - "enum": [ - "COMMIT_MESSAGES" - ] - } - } - } - }, - { - "if": { - "required": [ - "merge_commit_message" - ] - }, - "then": { - "required": [ - "merge_commit_title" - ] - } - } - ] - }, - "RequiredReviewerConfig": { - "description": "A team and the file patterns whose changes it must approve.", - "type": "object", - "properties": { - "file_patterns": { - "description": "fnmatch patterns of the files the team must review.", - "type": "array", - "items": { - "type": "string" - } - }, - "minimum_approvals": { - "description": "Approvals required from the team; zero adds the team as a reviewer without requiring approval.", - "type": "integer" - }, - "reviewer": { - "description": "The reviewing team.", - "type": "object", - "properties": { - "id": { - "description": "The team's id.", - "type": "integer" - }, - "type": { - "description": "Always \"Team\".", - "type": "string", - "const": "Team" - } - }, - "required": [ - "id", - "type" - ], - "additionalProperties": {} - } - }, - "required": [ - "file_patterns", - "minimum_approvals", - "reviewer" - ], - "additionalProperties": {} - }, - "ReviewDismissalActorConfig": { - "description": "One actor allowed to dismiss pull request reviews.", - "type": "object", - "properties": { - "id": { - "description": "The actor's id.", - "type": "integer" - }, - "type": { - "description": "User, Team, IntegrationInstallation, or RepositoryRole.", - "type": "string", - "enum": [ - "User", - "Team", - "IntegrationInstallation", - "RepositoryRole" - ] - } - }, - "required": [ - "id", - "type" - ], - "additionalProperties": {} - }, - "Rule": { - "description": "Branch names must match (or not match) a pattern.", - "type": "object", - "properties": { - "type": { - "description": "The rule type, as the rulesets API names it.", - "type": "string", - "const": "branch_name_pattern" - }, - "parameters": { - "description": "The rule's parameters, typed as the spec types them; a field GitHub adds later passes through.", - "allOf": [ - { - "$ref": "#/definitions/PatternRuleParameters" - } - ] - } - }, - "required": [ - "type" - ], - "additionalProperties": {} - }, - "Rule": { - "description": "Named code scanning tools must report before the ref is updated.", - "type": "object", - "properties": { - "type": { - "description": "The rule type, as the rulesets API names it.", - "type": "string", - "const": "code_scanning" - }, - "parameters": { - "description": "The rule's parameters, typed as the spec types them; a field GitHub adds later passes through.", - "type": "object", - "properties": { - "code_scanning_tools": { - "description": "The tools that must provide results.", - "type": "array", - "items": { - "$ref": "#/definitions/CodeScanningToolConfig" - } - } - }, - "required": [ - "code_scanning_tools" - ], - "additionalProperties": {} - } - }, - "required": [ - "type" - ], - "additionalProperties": {} - }, - "Rule": { - "description": "Commit author emails must match (or not match) a pattern.", - "type": "object", - "properties": { - "type": { - "description": "The rule type, as the rulesets API names it.", - "type": "string", - "const": "commit_author_email_pattern" - }, - "parameters": { - "description": "The rule's parameters, typed as the spec types them; a field GitHub adds later passes through.", - "allOf": [ - { - "$ref": "#/definitions/PatternRuleParameters" - } - ] - } - }, - "required": [ - "type" - ], - "additionalProperties": {} - }, - "Rule": { - "description": "Commit messages must match (or not match) a pattern.", - "type": "object", - "properties": { - "type": { - "description": "The rule type, as the rulesets API names it.", - "type": "string", - "const": "commit_message_pattern" - }, - "parameters": { - "description": "The rule's parameters, typed as the spec types them; a field GitHub adds later passes through.", - "allOf": [ - { - "$ref": "#/definitions/PatternRuleParameters" - } - ] - } - }, - "required": [ - "type" - ], - "additionalProperties": {} - }, - "Rule": { - "description": "Committer emails must match (or not match) a pattern.", - "type": "object", - "properties": { - "type": { - "description": "The rule type, as the rulesets API names it.", - "type": "string", - "const": "committer_email_pattern" - }, - "parameters": { - "description": "The rule's parameters, typed as the spec types them; a field GitHub adds later passes through.", - "allOf": [ - { - "$ref": "#/definitions/PatternRuleParameters" - } - ] - } - }, - "required": [ - "type" - ], - "additionalProperties": {} - }, - "Rule": { - "description": "Copilot code review is requested on new pull requests automatically.", - "type": "object", - "properties": { - "type": { - "description": "The rule type, as the rulesets API names it.", - "type": "string", - "const": "copilot_code_review" - }, - "parameters": { - "description": "The rule's parameters, typed as the spec types them; a field GitHub adds later passes through.", - "type": "object", - "properties": { - "review_draft_pull_requests": { - "description": "Whether drafts are reviewed before they are marked ready.", - "type": "boolean" - }, - "review_on_push": { - "description": "Whether each new push is reviewed.", - "type": "boolean" - } - }, - "additionalProperties": {} - } - }, - "required": [ - "type" - ], - "additionalProperties": {} - }, - "Rule": { - "description": "Only actors with bypass permission may create matching refs.", - "type": "object", - "properties": { - "type": { - "description": "The rule type, as the rulesets API names it.", - "type": "string", - "const": "creation" - } - }, - "required": [ - "type" - ], - "additionalProperties": {} - }, - "Rule": { - "description": "Only actors with bypass permission may delete matching refs.", - "type": "object", - "properties": { - "type": { - "description": "The rule type, as the rulesets API names it.", - "type": "string", - "const": "deletion" - } - }, - "required": [ - "type" - ], - "additionalProperties": {} - }, - "Rule": { - "description": "Commits adding files with the listed extensions are refused.", - "type": "object", - "properties": { - "type": { - "description": "The rule type, as the rulesets API names it.", - "type": "string", - "const": "file_extension_restriction" - }, - "parameters": { - "description": "The rule's parameters, typed as the spec types them; a field GitHub adds later passes through.", - "type": "object", - "properties": { - "restricted_file_extensions": { - "description": "The file extensions refused.", - "type": "array", - "items": { - "type": "string" - } - } - }, - "required": [ - "restricted_file_extensions" - ], - "additionalProperties": {} - } - }, - "required": [ - "type" - ], - "additionalProperties": {} - }, - "Rule": { - "description": "Commits touching the listed paths are refused.", - "type": "object", - "properties": { - "type": { - "description": "The rule type, as the rulesets API names it.", - "type": "string", - "const": "file_path_restriction" - }, - "parameters": { - "description": "The rule's parameters, typed as the spec types them; a field GitHub adds later passes through.", - "type": "object", - "properties": { - "restricted_file_paths": { - "description": "The file and folder paths refused.", - "type": "array", - "items": { - "type": "string" - } - } - }, - "required": [ - "restricted_file_paths" - ], - "additionalProperties": {} - } - }, - "required": [ - "type" - ], - "additionalProperties": {} - }, - "Rule": { - "description": "Added or changed dependencies must comply with the organization's license policy.", - "type": "object", - "properties": { - "type": { - "description": "The rule type, as the rulesets API names it.", - "type": "string", - "const": "license_compliance_scanning" - } - }, - "required": [ - "type" - ], - "additionalProperties": {} - }, - "Rule": { - "description": "Commits with file paths over the length cap are refused.", - "type": "object", - "properties": { - "type": { - "description": "The rule type, as the rulesets API names it.", - "type": "string", - "const": "max_file_path_length" - }, - "parameters": { - "description": "The rule's parameters, typed as the spec types them; a field GitHub adds later passes through.", - "type": "object", - "properties": { - "max_file_path_length": { - "description": "The most characters a file path may have (1 to 32767).", - "type": "integer", - "minimum": 1, - "maximum": 32767 - } - }, - "required": [ - "max_file_path_length" - ], - "additionalProperties": {} - } - }, - "required": [ - "type" - ], - "additionalProperties": {} - }, - "Rule": { - "description": "Commits with a file over the size cap are refused.", - "type": "object", - "properties": { - "type": { - "description": "The rule type, as the rulesets API names it.", - "type": "string", - "const": "max_file_size" - }, - "parameters": { - "description": "The rule's parameters, typed as the spec types them; a field GitHub adds later passes through.", - "type": "object", - "properties": { - "max_file_size": { - "description": "The largest file allowed, in megabytes (1 to 100); Git LFS objects are exempt.", - "type": "integer", - "minimum": 1, - "maximum": 100 - } - }, - "required": [ - "max_file_size" - ], - "additionalProperties": {} - } - }, - "required": [ - "type" - ], - "additionalProperties": {} - }, - "Rule": { - "description": "Merges into matching branches go through a merge queue.", - "type": "object", - "properties": { - "type": { - "description": "The rule type, as the rulesets API names it.", - "type": "string", - "const": "merge_queue" - }, - "parameters": { - "description": "The rule's parameters, typed as the spec types them; a field GitHub adds later passes through.", - "type": "object", - "properties": { - "check_response_timeout_minutes": { - "description": "Minutes a required status check may take before it counts as failed (1 to 360).", - "type": "integer", - "minimum": 1, - "maximum": 360 - }, - "grouping_strategy": { - "description": "ALLGREEN (every PR's merge commit must pass all required checks) or HEADGREEN (only the head of the merge group must), in GitHub's upper-case spelling.", - "type": "string", - "enum": [ - "ALLGREEN", - "HEADGREEN" - ] - }, - "max_entries_to_build": { - "description": "Cap on queued pull requests building at the same time (0 to 100).", - "type": "integer", - "minimum": 0, - "maximum": 100 - }, - "max_entries_to_merge": { - "description": "Cap on pull requests merged as one group (0 to 100).", - "type": "integer", - "minimum": 0, - "maximum": 100 - }, - "merge_method": { - "description": "MERGE, SQUASH, or REBASE, in GitHub's upper-case spelling (the lower-case pull_request allowed_merge_methods spelling is refused here).", - "type": "string", - "enum": [ - "MERGE", - "SQUASH", - "REBASE" - ] - }, - "min_entries_to_merge": { - "description": "Minimum pull requests merged as one group (0 to 100).", - "type": "integer", - "minimum": 0, - "maximum": 100 - }, - "min_entries_to_merge_wait_minutes": { - "description": "Minutes the queue waits for the minimum group size before merging a smaller group (0 to 360).", - "type": "integer", - "minimum": 0, - "maximum": 360 - } - }, - "required": [ - "check_response_timeout_minutes", - "grouping_strategy", - "max_entries_to_build", - "max_entries_to_merge", - "merge_method", - "min_entries_to_merge", - "min_entries_to_merge_wait_minutes" - ], - "additionalProperties": {} - } - }, - "required": [ - "type" - ], - "additionalProperties": {} - }, - "Rule": { - "description": "No force pushes to matching refs.", - "type": "object", - "properties": { - "type": { - "description": "The rule type, as the rulesets API names it.", - "type": "string", - "const": "non_fast_forward" - } - }, - "required": [ - "type" - ], - "additionalProperties": {} - }, - "Rule": { - "description": "Changes reach matching branches through a pull request only.", - "type": "object", - "properties": { - "type": { - "description": "The rule type, as the rulesets API names it.", - "type": "string", - "const": "pull_request" - }, - "parameters": { - "description": "The rule's parameters, typed as the spec types them; a field GitHub adds later passes through.", - "type": "object", - "properties": { - "allowed_merge_methods": { - "description": "The merge methods allowed, lower-case merge, squash, rebase (the upper-case merge_queue spelling is refused here); at least one.", - "minItems": 1, - "type": "array", - "items": { - "type": "string", - "enum": [ - "merge", - "squash", - "rebase" - ] - } - }, - "dismiss_stale_reviews_on_push": { - "description": "Whether a new reviewable push dismisses earlier approvals.", - "type": "boolean" - }, - "dismissal_restriction": { - "description": "Who may dismiss pull request reviews.", - "type": "object", - "properties": { - "allowed_actors": { - "description": "The people, teams, or apps allowed to dismiss reviews.", - "type": "array", - "items": { - "$ref": "#/definitions/ReviewDismissalActorConfig" - } - }, - "enabled": { - "description": "Whether review dismissal is restricted to the listed actors.", - "type": "boolean" - } - }, - "required": [ - "enabled" - ], - "additionalProperties": {} - }, - "require_code_owner_review": { - "description": "Whether files with a code owner need that owner's approving review.", - "type": "boolean" - }, - "require_last_push_approval": { - "description": "Whether the most recent reviewable push must be approved by someone other than its pusher.", - "type": "boolean" - }, - "required_approving_review_count": { - "description": "Approving reviews required (0 to 10).", - "type": "integer", - "minimum": 0, - "maximum": 10 - }, - "required_review_thread_resolution": { - "description": "Whether every review conversation must be resolved before merging.", - "type": "boolean" - }, - "required_reviewers": { - "description": "Teams that must review changes to given file patterns (GitHub marks this beta).", - "type": "array", - "items": { - "$ref": "#/definitions/RequiredReviewerConfig" - } - } - }, - "required": [ - "dismiss_stale_reviews_on_push", - "require_code_owner_review", - "require_last_push_approval", - "required_approving_review_count", - "required_review_thread_resolution" - ], - "additionalProperties": {} - } - }, - "required": [ - "type" - ], - "additionalProperties": {} - }, - "Rule": { - "description": "Named environments must deploy successfully before a push lands.", - "type": "object", - "properties": { - "type": { - "description": "The rule type, as the rulesets API names it.", - "type": "string", - "const": "required_deployments" - }, - "parameters": { - "description": "The rule's parameters, typed as the spec types them; a field GitHub adds later passes through.", - "type": "object", - "properties": { - "required_deployment_environments": { - "description": "The environment names that must be deployed to first.", - "type": "array", - "items": { - "type": "string" - } - } - }, - "required": [ - "required_deployment_environments" - ], - "additionalProperties": {} - } - }, - "required": [ - "type" - ], - "additionalProperties": {} - }, - "Rule": { - "description": "No merge commits may be pushed to matching refs.", - "type": "object", - "properties": { - "type": { - "description": "The rule type, as the rulesets API names it.", - "type": "string", - "const": "required_linear_history" - } - }, - "required": [ - "type" - ], - "additionalProperties": {} - }, - "Rule": { - "description": "Commits pushed to matching refs must carry verified signatures.", - "type": "object", - "properties": { - "type": { - "description": "The rule type, as the rulesets API names it.", - "type": "string", - "const": "required_signatures" - } - }, - "required": [ - "type" - ], - "additionalProperties": {} - }, - "Rule": { - "description": "Named status checks must pass before the ref is updated.", - "type": "object", - "properties": { - "type": { - "description": "The rule type, as the rulesets API names it.", - "type": "string", - "const": "required_status_checks" - }, - "parameters": { - "description": "The rule's parameters, typed as the spec types them; a field GitHub adds later passes through.", - "type": "object", - "properties": { - "do_not_enforce_on_create": { - "description": "Whether a new ref may be created even when a check would otherwise block it.", - "type": "boolean" - }, - "required_status_checks": { - "description": "The checks that must pass.", - "type": "array", - "items": { - "$ref": "#/definitions/StatusCheckConfig" - } - }, - "strict_required_status_checks_policy": { - "description": "Whether a pull request must be tested against the latest target commit.", - "type": "boolean" - } - }, - "required": [ - "required_status_checks", - "strict_required_status_checks_policy" - ], - "additionalProperties": {} - } - }, - "required": [ - "type" - ], - "additionalProperties": {} - }, - "Rule": { - "description": "Tag names must match (or not match) a pattern.", - "type": "object", - "properties": { - "type": { - "description": "The rule type, as the rulesets API names it.", - "type": "string", - "const": "tag_name_pattern" - }, - "parameters": { - "description": "The rule's parameters, typed as the spec types them; a field GitHub adds later passes through.", - "allOf": [ - { - "$ref": "#/definitions/PatternRuleParameters" - } - ] - } - }, - "required": [ - "type" - ], - "additionalProperties": {} - }, - "Rule": { - "description": "Only actors with bypass permission may update matching refs.", - "type": "object", - "properties": { - "type": { - "description": "The rule type, as the rulesets API names it.", - "type": "string", - "const": "update" - }, - "parameters": { - "description": "The rule's parameters, typed as the spec types them; a field GitHub adds later passes through.", - "type": "object", - "properties": { - "update_allows_fetch_and_merge": { - "description": "Whether the branch may still pull changes from its upstream repository.", - "type": "boolean" - } - }, - "required": [ - "update_allows_fetch_and_merge" - ], - "additionalProperties": {} - } - }, - "required": [ - "type" - ], - "additionalProperties": {} - }, - "Rule": { - "description": "Named workflows must pass before changes merge into matching branches.", - "type": "object", - "properties": { - "type": { - "description": "The rule type, as the rulesets API names it.", - "type": "string", - "const": "workflows" - }, - "parameters": { - "description": "The rule's parameters, typed as the spec types them; a field GitHub adds later passes through.", - "type": "object", - "properties": { - "do_not_enforce_on_create": { - "description": "Whether a new ref may be created even when a workflow would otherwise block it.", - "type": "boolean" - }, - "workflows": { - "description": "The workflow files that must pass.", - "type": "array", - "items": { - "$ref": "#/definitions/WorkflowFileConfig" - } - } - }, - "required": [ - "workflows" - ], - "additionalProperties": {} - } - }, - "required": [ - "type" - ], - "additionalProperties": {} - }, - "RuleConfig": { - "description": "One rule of the ruleset: a type the spec knows, with its parameters typed, or any other type, passed through as written.", - "anyOf": [ - { - "oneOf": [ - { - "$ref": "#/definitions/Rule%3Ccreation%3E" - }, - { - "$ref": "#/definitions/Rule%3Cupdate%3E" - }, - { - "$ref": "#/definitions/Rule%3Cdeletion%3E" - }, - { - "$ref": "#/definitions/Rule%3Crequired_linear_history%3E" - }, - { - "$ref": "#/definitions/Rule%3Cmerge_queue%3E" - }, - { - "$ref": "#/definitions/Rule%3Crequired_deployments%3E" - }, - { - "$ref": "#/definitions/Rule%3Crequired_signatures%3E" - }, - { - "$ref": "#/definitions/Rule%3Cpull_request%3E" - }, - { - "$ref": "#/definitions/Rule%3Crequired_status_checks%3E" - }, - { - "$ref": "#/definitions/Rule%3Cnon_fast_forward%3E" - }, - { - "$ref": "#/definitions/Rule%3Ccommit_message_pattern%3E" - }, - { - "$ref": "#/definitions/Rule%3Ccommit_author_email_pattern%3E" - }, - { - "$ref": "#/definitions/Rule%3Ccommitter_email_pattern%3E" - }, - { - "$ref": "#/definitions/Rule%3Cbranch_name_pattern%3E" - }, - { - "$ref": "#/definitions/Rule%3Ctag_name_pattern%3E" - }, - { - "$ref": "#/definitions/Rule%3Cworkflows%3E" - }, - { - "$ref": "#/definitions/Rule%3Ccode_scanning%3E" - }, - { - "$ref": "#/definitions/Rule%3Ccopilot_code_review%3E" - }, - { - "$ref": "#/definitions/Rule%3Clicense_compliance_scanning%3E" - }, - { - "$ref": "#/definitions/Rule%3Cfile_path_restriction%3E" - }, - { - "$ref": "#/definitions/Rule%3Cmax_file_path_length%3E" - }, - { - "$ref": "#/definitions/Rule%3Cfile_extension_restriction%3E" - }, - { - "$ref": "#/definitions/Rule%3Cmax_file_size%3E" - } - ] - }, - { - "$ref": "#/definitions/UnknownRule" - } - ] - }, - "RulesetConfig": { - "description": "One repository ruleset, matched to the live repo by name.", - "type": "object", - "properties": { - "name": { - "description": "The ruleset name, the natural key.", - "type": "string" - }, - "target": { - "description": "What the ruleset applies to. Omitted, it is \"branch\", the default GitHub documents, so a live branch ruleset is not drift under an entry without the key.", - "default": "branch", - "type": "string", - "enum": [ - "branch", - "tag", - "push" - ] - }, - "enforcement": { - "description": "\"active\", \"evaluate\", or \"disabled\", in that spelling (\"enabled\" and \"Active\" are refused at parse). Omitted, it is \"active\" (GitHub requires a value on create; this is the one chosen), so a live active ruleset is not drift under an entry without the key.", - "default": "active", - "type": "string", - "enum": [ - "active", - "evaluate", - "disabled" - ] - }, - "conditions": { - "description": "Which refs the ruleset covers.", - "type": "object", - "properties": { - "ref_name": { - "description": "Short ref names are auto-prefixed (staging -> refs/heads/staging).", - "type": "object", - "properties": { - "include": { - "description": "Ref patterns the ruleset targets (fnmatch). The tokens ~DEFAULT_BRANCH and ~ALL are accepted as written; any other value carrying \"~\", or one of \"^\", \":\", \"\\\\\", space, \"..\", \"@{\", or a control character, is refused at parse: git refuses these in a ref name and a ruleset pattern has no use for them. \"*\", \"?\", and \"[\" are pattern syntax and pass.", - "type": "array", - "items": { - "type": "string", - "pattern": "^(?:~ALL|~DEFAULT_BRANCH|(?:(?![~^:\\\\ \\x00-\\x1f\\x7f]|\\.\\.|@\\{)[\\s\\S])*)$" - } - }, - "exclude": { - "description": "Ref patterns excluded from the ruleset; the same character rules apply.", - "type": "array", - "items": { - "type": "string", - "pattern": "^(?:~ALL|~DEFAULT_BRANCH|(?:(?![~^:\\\\ \\x00-\\x1f\\x7f]|\\.\\.|@\\{)[\\s\\S])*)$" - } - } - } - } - } - }, - "rules": { - "description": "Rule list. A rule type the vendored spec knows has its parameters checked at parse; a type it does not know passes through verbatim, so a rule type GitHub ships tomorrow works today.", - "type": "array", - "items": { - "$ref": "#/definitions/RuleConfig" - } - }, - "bypass_actors": { - "description": "Who may bypass the ruleset; each actor is one (actor_type, actor_id) pair, bypass_mode defaulting to always. An entry without the key leaves a live bypass list as drift and apply refuses the PUT; `[]` clears it on purpose. GitHub returns it only to a token with write access to the ruleset, so without Administration write check mode notes it instead of judging drift.", - "type": "array", - "items": { - "$ref": "#/definitions/BypassActorConfig" - } - } - }, - "required": [ - "name" - ] - }, - "SecretScanningPatternConfig": { - "description": "One secret scanning custom pattern, matched by exact name. Only the fields below are accepted: `state` and `push_protection_enabled` are readable but NOT writable through the custom-pattern endpoints, so they cannot be declared. A delimiter, once set, cannot be cleared back to GitHub's default through the update PATCH (the endpoint updates provided fields only); remove the pattern and redeclare it without the field instead.", - "type": "object", - "properties": { - "name": { - "description": "The pattern name, the natural key; immutable upstream, so a rename creates the new name (the old one follows the undeclared policy).", - "type": "string" - }, - "pattern": { - "description": "The regular expression the secret format must match. Like every regex field here it passes a syntax check at parse, or the file is refused naming the field: the PCRE-only forms Hyperscan accepts (`(?P`, `(?#comment)`, `\\x{HH}`, `(?i)`, atomic and possessive forms, `\\Q...\\E`, a leading `(*UTF8)`, `(*UTF)` or `(*UCP)`) are translated, then the result must compile as a flagless JavaScript RegExp. GitHub can still refuse at apply what Hyperscan alone refuses (lookbehind, backreferences, option modifiers).", - "type": "string" - }, - "start_delimiter": { - "description": "Regular expression for the characters that must come before the secret; it passes the syntax check, like `pattern`. An empty string is rejected: a delimiter cannot be cleared through the update call - remove the pattern and redeclare it without the field instead.", - "type": "string", - "minLength": 1 - }, - "end_delimiter": { - "description": "Regular expression for the characters that must come after the secret; it passes the syntax check, like `pattern`. An empty string is rejected, like start_delimiter.", - "type": "string", - "minLength": 1 - }, - "must_match": { - "description": "Additional regular expressions a match must also satisfy, compared in order; each passes the syntax check, like `pattern`.", - "type": "array", - "items": { - "type": "string" - } - }, - "must_not_match": { - "description": "Regular expressions a match must NOT satisfy, compared in order; each passes the syntax check, like `pattern`.", - "type": "array", - "items": { - "type": "string" - } - } - }, - "required": [ - "name", - "pattern" - ] - }, - "SecurityAndAnalysisConfig": { - "description": "The nested security_and_analysis object, closed to the sub-keys the PATCH accepts (GitHub rejects any other with a 422). Each feature is {status: enabled/disabled}. dependabot_security_updates is not one of them: declare enable_automated_security_fixes instead.", - "type": "object", - "properties": { - "advanced_security": { - "description": "GitHub Advanced Security.", - "allOf": [ - { - "$ref": "#/definitions/SecurityFeatureToggle" - } - ] - }, - "code_security": { - "description": "GitHub Code Security.", - "allOf": [ - { - "$ref": "#/definitions/SecurityFeatureToggle" - } - ] - }, - "secret_scanning": { - "description": "Secret scanning.", - "allOf": [ - { - "$ref": "#/definitions/SecurityFeatureToggle" - } - ] - }, - "secret_scanning_push_protection": { - "description": "Secret scanning push protection.", - "allOf": [ - { - "$ref": "#/definitions/SecurityFeatureToggle" - } - ] - }, - "secret_scanning_ai_detection": { - "description": "AI detection of generic secrets.", - "allOf": [ - { - "$ref": "#/definitions/SecurityFeatureToggle" - } - ] - }, - "secret_scanning_non_provider_patterns": { - "description": "Non-provider secret patterns.", - "allOf": [ - { - "$ref": "#/definitions/SecurityFeatureToggle" - } - ] - }, - "secret_scanning_delegated_alert_dismissal": { - "description": "Delegated dismissal of secret scanning alerts.", - "allOf": [ - { - "$ref": "#/definitions/SecurityFeatureToggle" - } - ] - }, - "secret_scanning_delegated_bypass": { - "description": "Delegated bypass of push protection; the reviewers live in secret_scanning_delegated_bypass_options.", - "allOf": [ - { - "$ref": "#/definitions/SecurityFeatureToggle" - } - ] - }, - "secret_scanning_delegated_bypass_options": { - "description": "Who reviews push protection bypass requests; honored only while secret_scanning_delegated_bypass is enabled.", - "type": "object", - "properties": { - "reviewers": { - "description": "The bypass reviewers; left out, the existing set is unchanged.", - "type": "array", - "items": { - "type": "object", - "properties": { - "reviewer_id": { - "description": "The id of the team or role selected as a reviewer.", - "type": "integer" - }, - "reviewer_type": { - "description": "TEAM or ROLE.", - "type": "string", - "enum": [ - "TEAM", - "ROLE" - ] - }, - "mode": { - "description": "ALWAYS (the default: every bypass needs review) or EXEMPT (this reviewer's own pushes skip it).", - "type": "string", - "enum": [ - "ALWAYS", - "EXEMPT" - ] - } - }, - "required": [ - "reviewer_id", - "reviewer_type" - ], - "additionalProperties": false - } - } - }, - "additionalProperties": false - }, - "secret_scanning_validity_checks": { - "description": "Validity checks for detected secrets (documented in the GHEC flavor of the spec).", - "allOf": [ - { - "$ref": "#/definitions/SecurityFeatureToggle" - } - ] - } - }, - "additionalProperties": false - }, - "SecurityFeatureToggle": { - "description": "One security_and_analysis feature.", - "type": "object", - "properties": { - "status": { - "description": "\"enabled\" or \"disabled\".", - "type": "string", - "enum": [ - "enabled", - "disabled" - ] - } - }, - "additionalProperties": false - }, - "SettingsFile": { - "description": "One settings.yml document: every top-level section is optional.", - "type": "object", - "properties": { - "repository": { - "description": "Repo fields sent verbatim to PATCH /repos/{r}, plus the special keys RepositoryConfig documents.", - "allOf": [ - { - "$ref": "#/definitions/RepositoryConfig" - } - ] - }, - "labels": { - "description": "Issue/PR labels; undeclared labels are DELETED by default (Probot parity; the wrapped form can set `_undeclared: keep`).", - "anyOf": [ - { - "type": "array", - "items": { - "$ref": "#/definitions/LabelConfig" - } - }, - { - "$ref": "#/definitions/UndeclaredPolicyList%3CLabelConfig%3E" - } - ] - }, - "rulesets": { - "description": "Repository rulesets, upserted by name; undeclared ones are kept by default (the wrapped form can set `_undeclared: delete`).", - "anyOf": [ - { - "type": "array", - "items": { - "$ref": "#/definitions/RulesetConfig" - } - }, - { - "$ref": "#/definitions/UndeclaredPolicyList%3CRulesetConfig%3E" - } - ] - }, - "branches": { - "description": "Classic branch protection per branch.", - "anyOf": [ - { - "type": "array", - "items": { - "$ref": "#/definitions/BranchConfig" - } - }, - { - "$ref": "#/definitions/LayeredList%3CBranchConfig%3E" - } - ] - }, - "environments": { - "description": "Deployment environments, upserted by name.", - "anyOf": [ - { - "type": "array", - "items": { - "$ref": "#/definitions/EnvironmentConfig" - } - }, - { - "$ref": "#/definitions/LayeredList%3CEnvironmentConfig%3E" - } - ] - }, - "autolinks": { - "description": "Autolink references; undeclared ones are DELETED by default (the wrapped form can set `_undeclared: keep`).", - "anyOf": [ - { - "type": "array", - "items": { - "$ref": "#/definitions/AutolinkConfig" - } - }, - { - "$ref": "#/definitions/UndeclaredPolicyList%3CAutolinkConfig%3E" - } - ] - }, - "actions": { - "description": "GitHub Actions permissions for the repository.", - "allOf": [ - { - "$ref": "#/definitions/ActionsConfig" - } - ] - }, - "actions_secrets": { - "description": "Repository Actions secrets, written by name with values sealed client-side; each value is a whole-value `$NAME` reference to the action step's environment, never a literal. Undeclared secrets are kept by default (the wrapped form can set `_undeclared: delete`; a deleted secret's value is unrecoverable).", - "anyOf": [ - { - "type": "array", - "items": { - "$ref": "#/definitions/ActionsSecretConfig" - } - }, - { - "$ref": "#/definitions/UndeclaredPolicyList%3CActionsSecretConfig%3E" - } - ] - }, - "dependabot_secrets": { - "description": "Repository Dependabot secrets (private-registry credentials Dependabot uses), written by name with values sealed client-side; each value is a whole-value `$NAME` reference to the action step's environment, never a literal. Undeclared secrets are kept by default (the wrapped form can set `_undeclared: delete`; a deleted secret's value is unrecoverable).", - "anyOf": [ - { - "type": "array", - "items": { - "$ref": "#/definitions/DependabotSecretConfig" - } - }, - { - "$ref": "#/definitions/UndeclaredPolicyList%3CDependabotSecretConfig%3E" - } - ] - }, - "codespaces_secrets": { - "description": "Repository Codespaces secrets (development environment secrets), written by name with values sealed client-side; each value is a whole-value `$NAME` reference to the action step's environment, never a literal. Undeclared secrets are kept by default (the wrapped form can set `_undeclared: delete`; a deleted secret's value is unrecoverable).", - "anyOf": [ - { - "type": "array", - "items": { - "$ref": "#/definitions/CodespacesSecretConfig" - } - }, - { - "$ref": "#/definitions/UndeclaredPolicyList%3CCodespacesSecretConfig%3E" - } - ] - }, - "agents_secrets": { - "description": "Repository Copilot agents secrets (the secret store Copilot coding agents read), written by name with values sealed client-side; each value is a whole-value `$NAME` reference to the action step's environment, never a literal. Undeclared secrets are kept by default (the wrapped form can set `_undeclared: delete`; a deleted secret's value is unrecoverable).", - "anyOf": [ - { - "type": "array", - "items": { - "$ref": "#/definitions/AgentsSecretConfig" - } - }, - { - "$ref": "#/definitions/UndeclaredPolicyList%3CAgentsSecretConfig%3E" - } - ] - }, - "workflows": { - "description": "Per-workflow enable/disable state; undeclared workflows are untouched.", - "anyOf": [ - { - "type": "array", - "items": { - "$ref": "#/definitions/WorkflowConfig" - } - }, - { - "$ref": "#/definitions/LayeredList%3CWorkflowConfig%3E" - } - ] - }, - "check_suite_preferences": { - "description": "Check suite preferences: per-GitHub-App `auto_trigger_checks` toggles controlling whether pushes automatically create check suites. Write-only upstream (GitHub exposes no read endpoint), so check mode cannot verify them and apply re-asserts the declared preferences on every run. The token owner must be a repository administrator.", - "allOf": [ - { - "$ref": "#/definitions/CheckSuitePreferencesConfig" - } - ] - }, - "pages": { - "description": "GitHub Pages configuration; null disables Pages on the repository.", - "anyOf": [ - { - "$ref": "#/definitions/PagesConfig" - }, - { - "type": "null" - } - ] - }, - "code_scanning_default_setup": { - "description": "Code scanning default setup (CodeQL).", - "allOf": [ - { - "$ref": "#/definitions/CodeScanningDefaultSetupConfig" - } - ] - }, - "code_quality_setup": { - "description": "Code quality analysis setup.", - "allOf": [ - { - "$ref": "#/definitions/CodeQualitySetupConfig" - } - ] - }, - "collaborators": { - "description": "Direct collaborators, with pending invitations reconciled alongside; undeclared ones are REMOVED (pending invitations cancelled) by default (owner never touched; the wrapped form can set `_undeclared: keep`).", - "anyOf": [ - { - "type": "array", - "items": { - "$ref": "#/definitions/CollaboratorConfig" - } - }, - { - "$ref": "#/definitions/UndeclaredPolicyList%3CCollaboratorConfig%3E" - } - ] - }, - "teams": { - "description": "Org team access to the repo; skipped on personal accounts. Undeclared teams are kept by default and noted; the wrapped form can set `_undeclared: delete` to revoke their direct access (a grant made at the organization level is never touched). A declared child team can hold its access through an undeclared parent's grant, which `delete` revokes: declare the parent too, or keep.", - "anyOf": [ - { - "type": "array", - "items": { - "$ref": "#/definitions/TeamConfig" - } - }, - { - "$ref": "#/definitions/UndeclaredPolicyList%3CTeamConfig%3E" - } - ] - }, - "milestones": { - "description": "Milestones, upserted by title; undeclared ones are kept by default (the wrapped form can set `_undeclared: delete`, which detaches deleted milestones from their issues).", - "anyOf": [ - { - "type": "array", - "items": { - "$ref": "#/definitions/MilestoneConfig" - } - }, - { - "$ref": "#/definitions/UndeclaredPolicyList%3CMilestoneConfig%3E" - } - ] - }, - "interaction_limits": { - "description": "Temporary interaction limits; null clears an active repo-level limit, and an absent key leaves whatever is live untouched. Limits self-expire (GitHub's expiry tops out at six_months), so apply re-arms the declared limit on every run and check mode reports drift once it lapses. The pull_request_creation_cap and pull_request_creation_bypass keys manage the persistent pull request creation cap and its bypass list instead; `interaction_limits: null` clears the base limit only and never touches them.", - "anyOf": [ - { - "$ref": "#/definitions/InteractionLimitsConfig" - }, - { - "type": "null" - } - ] - }, - "actions_variables": { - "description": "GitHub Actions repository variables, upserted by name; undeclared ones are DELETED by default (the wrapped form can set `_undeclared: keep`). Names are case-insensitive (GitHub stores them uppercased). Values are plain text BY DESIGN - variables are readable configuration, which is what makes check-mode diffing possible; secrets are write-only material and deliberately not this section.", - "anyOf": [ - { - "type": "array", - "items": { - "$ref": "#/definitions/ActionsVariableConfig" - } - }, - { - "$ref": "#/definitions/UndeclaredPolicyList%3CActionsVariableConfig%3E" - } - ] - }, - "agents_variables": { - "description": "Copilot agents repository variables (the plain-text configuration Copilot coding agents read), upserted by name; undeclared ones are DELETED by default (the wrapped form can set `_undeclared: keep`). Names are case-insensitive (GitHub stores them uppercased). Values are plain text BY DESIGN - variables are readable configuration, which is what makes check-mode diffing possible; secrets are write-only material and deliberately not this section.", - "anyOf": [ - { - "type": "array", - "items": { - "$ref": "#/definitions/AgentsVariableConfig" - } - }, - { - "$ref": "#/definitions/UndeclaredPolicyList%3CAgentsVariableConfig%3E" - } - ] - }, - "webhooks": { - "description": "Repository webhooks, managed one per config.url; undeclared hooks are kept by default and surfaced as notes, since integrations create their own hooks (the wrapped form can set `_undeclared: delete`).", - "anyOf": [ - { - "type": "array", - "items": { - "$ref": "#/definitions/WebhookConfig" - } - }, - { - "$ref": "#/definitions/UndeclaredPolicyList%3CWebhookConfig%3E" - } - ] - }, - "custom_properties": { - "description": "Values of organization-defined custom properties, set per repository (the property DEFINITIONS are organization-scoped and out of scope); organization repos only, skipped with a note on personal accounts. `value: null` unsets a property (reverting to the org default, if any), and booleans/numbers are normalized to their string form (GitHub transports true_false values as the strings \"true\"/\"false\"). Undeclared live values are kept by default - an unset can revert to an org default this action does not model, and a property whose values only org actors may edit would reject the write - and the wrapped form can set `_undeclared: delete` to opt into unsetting them.", - "anyOf": [ - { - "type": "array", - "items": { - "$ref": "#/definitions/CustomPropertyConfig" - } - }, - { - "$ref": "#/definitions/UndeclaredPolicyList%3CCustomPropertyConfig%3E" - } - ] - }, - "deploy_keys": { - "description": "Deploy keys, matched by title. The declared material is a PUBLIC key, safe in a committed settings file. Keys are immutable upstream, so any change is applied as delete plus recreate. Undeclared keys are kept by default - deleting a live deploy key breaks whatever service authenticates with it, and deployment tooling installs its own keys - and the wrapped form can set `_undeclared: delete`.", - "anyOf": [ - { - "type": "array", - "items": { - "$ref": "#/definitions/DeployKeyConfig" - } - }, - { - "$ref": "#/definitions/UndeclaredPolicyList%3CDeployKeyConfig%3E" - } - ] - }, - "secret_scanning_custom_patterns": { - "description": "Repository-level secret scanning custom patterns, matched by name. The name is immutable upstream (the update PATCH takes no name field), so a renamed entry is applied as a create of the new name - plus, under `_undeclared: delete`, deletion of the old one; under the default keep policy the old pattern stays live and is surfaced as a note. Undeclared patterns are kept by default: removing a pattern disposes of its alerts, so deletion stays a human opt-in (the wrapped form can set `_undeclared: delete`). When this action deletes a pattern it always asks GitHub to RESOLVE the pattern's alerts rather than delete them, keeping the audit trail.", - "anyOf": [ - { - "type": "array", - "items": { - "$ref": "#/definitions/SecretScanningPatternConfig" - } - }, - { - "$ref": "#/definitions/UndeclaredPolicyList%3CSecretScanningPatternConfig%3E" - } - ] - }, - "_layering": { - "description": "mode: render only: a layer-level default for how this document's list sections combine with the layers below it, overriding the run's layering input; a section's own wrapper-level `_layering` overrides it again. `replace` lets each of this document's lists win wholesale; `shallow` unions the entries by the section's key and swaps a same-key entry for this document's; `deep` unions by key and merges a same-key pair field by field, a nested keyed list (a ruleset's rules, by type) unioning the same way. Consumed by the render, never applied: the rendered document carries no `_layering`, and apply and check ignore it like any `_` key.", - "type": "string", - "enum": [ - "replace", - "shallow", - "deep" - ] - }, - "_undeclared": { - "description": "The file-wide undeclared policy: what apply does to a live resource a list does not declare, for every list in this document that takes the `_undeclared` knob (the knobbed sections and an environment's variables, secrets, deployment branch policies, and deployment protection rules) and whose own wrapper sets none. A wrapper's `_undeclared` wins over it; it wins over the run's `undeclared` input; each list's own default applies when none of the three is set. Resolved into every list's wrapper, so the rendered document carries no `_undeclared` at its top. In a layered merge the highest layer that sets it steers the whole fold: a directive, not a merged value.", - "allOf": [ - { - "$ref": "#/definitions/UndeclaredPolicy" - } - ] - } - } - }, - "StatusCheckConfig": { - "description": "One required status check.", - "type": "object", - "properties": { - "context": { - "description": "The status check context name that must be present on the commit.", - "type": "string" - }, - "integration_id": { - "description": "The GitHub App the check must come from, when pinned.", - "type": "integer" - } - }, - "required": [ - "context" - ], - "additionalProperties": {} - }, - "TeamConfig": { - "description": "One org team's access to the repository, matched by team slug. Keys other than name and permission are rejected (a misspelled \"permission\" would otherwise silently grant the default role).", - "type": "object", - "properties": { - "name": { - "description": "The team slug (the name in the team's URL), the natural key: letters, digits, \".\", \"_\", and \"-\". A display name with spaces fails at parse time, since the slug is what every API path takes and a wrong one reads as \"no access\".", - "type": "string", - "pattern": "^(?=.*[A-Za-z0-9])[A-Za-z0-9._-]+$" - }, - "permission": { - "description": "Same vocabulary and parse rules as collaborators; defaults to \"push\".", - "type": "string", - "pattern": "^(?:pull|triage|push|maintain|admin|(?!(?:[Pp][Uu][Ll][Ll]|[Tt][Rr][Ii][Aa][Gg][Ee]|[Pp][Uu][Ss][Hh]|[Mm][Aa][Ii][Nn][Tt][Aa][Ii][Nn]|[Aa][Dd][Mm][Ii][Nn]|[Ww][Rr][Ii][Tt][Ee]|[Rr][Ee][Aa][Dd])$)\\S(?:.*\\S)?)$" - } - }, - "required": [ - "name" - ] - }, - "UndeclaredPolicy": { - "description": "What apply does to live resources the settings file does not declare.", - "type": "string", - "enum": [ - "keep", - "delete" - ] - }, - "UndeclaredPolicyList": { - "description": "The wrapped form of a list, overriding what happens to live resources the file does not declare. The plain array form takes the file's top-level `_undeclared`, else the run's `undeclared` input, else the list's own default (the section default for a top-level section, a fixed default for a nested list such as environments[].variables); this wrapper can set it explicitly, and with `_undeclared` omitted it behaves exactly like the plain array. The wrapper is this action's own vocabulary (nothing here passes through to GitHub), so its keys are strict: anything besides `_undeclared`, `entries`, and `_layering` (the directives wear the underscore; a nested list's wrapper does not take `_layering`) is rejected upfront as a typo.", - "type": "object", - "properties": { - "_undeclared": { - "description": "What apply does to live resources `entries` does not declare: \"delete\" removes them, \"keep\" leaves them alone and surfaces each as a note. Omitted, the file's top-level `_undeclared` applies, else the run's `undeclared` input, else the list's own default.", - "allOf": [ - { - "$ref": "#/definitions/UndeclaredPolicy" - } - ] - }, - "entries": { - "description": "The declared entries, exactly as the plain array form lists them.", - "type": "array", - "items": { - "$ref": "#/definitions/ActionsSecretConfig" - } - }, - "_layering": { - "description": "A render-time directive: how this section combines with the layers BELOW it in a layered merge, by the section's key (a label's name, a secret's name, a webhook's URL, ...). \"replace\" lets this layer's list win wholesale; \"shallow\" unions the entries by key and swaps a same-key entry for this layer's; \"deep\" unions by key and merges a same-key pair field by field, a nested keyed list (a ruleset's rules, by type) unioning the same way. To clear a list, write \"replace\" with an empty list: under \"shallow\" and \"deep\" an empty list adds nothing. Omitted, the file's `_layering` applies, else the run's layering. It has no effect on a single document, the rendered document never carries it, and a list nested inside a section entry does not take it.", - "type": "string", - "enum": [ - "replace", - "shallow", - "deep" - ] - } - }, - "required": [ - "entries" - ], - "additionalProperties": false - }, - "UndeclaredPolicyList": { - "description": "The wrapped form of a list, overriding what happens to live resources the file does not declare. The plain array form takes the file's top-level `_undeclared`, else the run's `undeclared` input, else the list's own default (the section default for a top-level section, a fixed default for a nested list such as environments[].variables); this wrapper can set it explicitly, and with `_undeclared` omitted it behaves exactly like the plain array. The wrapper is this action's own vocabulary (nothing here passes through to GitHub), so its keys are strict: anything besides `_undeclared`, `entries`, and `_layering` (the directives wear the underscore; a nested list's wrapper does not take `_layering`) is rejected upfront as a typo.", - "type": "object", - "properties": { - "_undeclared": { - "description": "What apply does to live resources `entries` does not declare: \"delete\" removes them, \"keep\" leaves them alone and surfaces each as a note. Omitted, the file's top-level `_undeclared` applies, else the run's `undeclared` input, else the list's own default.", - "allOf": [ - { - "$ref": "#/definitions/UndeclaredPolicy" - } - ] - }, - "entries": { - "description": "The declared entries, exactly as the plain array form lists them.", - "type": "array", - "items": { - "$ref": "#/definitions/ActionsVariableConfig" - } - }, - "_layering": { - "description": "A render-time directive: how this section combines with the layers BELOW it in a layered merge, by the section's key (a label's name, a secret's name, a webhook's URL, ...). \"replace\" lets this layer's list win wholesale; \"shallow\" unions the entries by key and swaps a same-key entry for this layer's; \"deep\" unions by key and merges a same-key pair field by field, a nested keyed list (a ruleset's rules, by type) unioning the same way. To clear a list, write \"replace\" with an empty list: under \"shallow\" and \"deep\" an empty list adds nothing. Omitted, the file's `_layering` applies, else the run's layering. It has no effect on a single document, the rendered document never carries it, and a list nested inside a section entry does not take it.", - "type": "string", - "enum": [ - "replace", - "shallow", - "deep" - ] - } - }, - "required": [ - "entries" - ], - "additionalProperties": false - }, - "UndeclaredPolicyList": { - "description": "The wrapped form of a list, overriding what happens to live resources the file does not declare. The plain array form takes the file's top-level `_undeclared`, else the run's `undeclared` input, else the list's own default (the section default for a top-level section, a fixed default for a nested list such as environments[].variables); this wrapper can set it explicitly, and with `_undeclared` omitted it behaves exactly like the plain array. The wrapper is this action's own vocabulary (nothing here passes through to GitHub), so its keys are strict: anything besides `_undeclared`, `entries`, and `_layering` (the directives wear the underscore; a nested list's wrapper does not take `_layering`) is rejected upfront as a typo.", - "type": "object", - "properties": { - "_undeclared": { - "description": "What apply does to live resources `entries` does not declare: \"delete\" removes them, \"keep\" leaves them alone and surfaces each as a note. Omitted, the file's top-level `_undeclared` applies, else the run's `undeclared` input, else the list's own default.", - "allOf": [ - { - "$ref": "#/definitions/UndeclaredPolicy" - } - ] - }, - "entries": { - "description": "The declared entries, exactly as the plain array form lists them.", - "type": "array", - "items": { - "$ref": "#/definitions/AgentsSecretConfig" - } - }, - "_layering": { - "description": "A render-time directive: how this section combines with the layers BELOW it in a layered merge, by the section's key (a label's name, a secret's name, a webhook's URL, ...). \"replace\" lets this layer's list win wholesale; \"shallow\" unions the entries by key and swaps a same-key entry for this layer's; \"deep\" unions by key and merges a same-key pair field by field, a nested keyed list (a ruleset's rules, by type) unioning the same way. To clear a list, write \"replace\" with an empty list: under \"shallow\" and \"deep\" an empty list adds nothing. Omitted, the file's `_layering` applies, else the run's layering. It has no effect on a single document, the rendered document never carries it, and a list nested inside a section entry does not take it.", - "type": "string", - "enum": [ - "replace", - "shallow", - "deep" - ] - } - }, - "required": [ - "entries" - ], - "additionalProperties": false - }, - "UndeclaredPolicyList": { - "description": "The wrapped form of a list, overriding what happens to live resources the file does not declare. The plain array form takes the file's top-level `_undeclared`, else the run's `undeclared` input, else the list's own default (the section default for a top-level section, a fixed default for a nested list such as environments[].variables); this wrapper can set it explicitly, and with `_undeclared` omitted it behaves exactly like the plain array. The wrapper is this action's own vocabulary (nothing here passes through to GitHub), so its keys are strict: anything besides `_undeclared`, `entries`, and `_layering` (the directives wear the underscore; a nested list's wrapper does not take `_layering`) is rejected upfront as a typo.", - "type": "object", - "properties": { - "_undeclared": { - "description": "What apply does to live resources `entries` does not declare: \"delete\" removes them, \"keep\" leaves them alone and surfaces each as a note. Omitted, the file's top-level `_undeclared` applies, else the run's `undeclared` input, else the list's own default.", - "allOf": [ - { - "$ref": "#/definitions/UndeclaredPolicy" - } - ] - }, - "entries": { - "description": "The declared entries, exactly as the plain array form lists them.", - "type": "array", - "items": { - "$ref": "#/definitions/AgentsVariableConfig" - } - }, - "_layering": { - "description": "A render-time directive: how this section combines with the layers BELOW it in a layered merge, by the section's key (a label's name, a secret's name, a webhook's URL, ...). \"replace\" lets this layer's list win wholesale; \"shallow\" unions the entries by key and swaps a same-key entry for this layer's; \"deep\" unions by key and merges a same-key pair field by field, a nested keyed list (a ruleset's rules, by type) unioning the same way. To clear a list, write \"replace\" with an empty list: under \"shallow\" and \"deep\" an empty list adds nothing. Omitted, the file's `_layering` applies, else the run's layering. It has no effect on a single document, the rendered document never carries it, and a list nested inside a section entry does not take it.", - "type": "string", - "enum": [ - "replace", - "shallow", - "deep" - ] - } - }, - "required": [ - "entries" - ], - "additionalProperties": false - }, - "UndeclaredPolicyList": { - "description": "The wrapped form of a list, overriding what happens to live resources the file does not declare. The plain array form takes the file's top-level `_undeclared`, else the run's `undeclared` input, else the list's own default (the section default for a top-level section, a fixed default for a nested list such as environments[].variables); this wrapper can set it explicitly, and with `_undeclared` omitted it behaves exactly like the plain array. The wrapper is this action's own vocabulary (nothing here passes through to GitHub), so its keys are strict: anything besides `_undeclared`, `entries`, and `_layering` (the directives wear the underscore; a nested list's wrapper does not take `_layering`) is rejected upfront as a typo.", - "type": "object", - "properties": { - "_undeclared": { - "description": "What apply does to live resources `entries` does not declare: \"delete\" removes them, \"keep\" leaves them alone and surfaces each as a note. Omitted, the file's top-level `_undeclared` applies, else the run's `undeclared` input, else the list's own default.", - "allOf": [ - { - "$ref": "#/definitions/UndeclaredPolicy" - } - ] - }, - "entries": { - "description": "The declared entries, exactly as the plain array form lists them.", - "type": "array", - "items": { - "$ref": "#/definitions/AutolinkConfig" - } - }, - "_layering": { - "description": "A render-time directive: how this section combines with the layers BELOW it in a layered merge, by the section's key (a label's name, a secret's name, a webhook's URL, ...). \"replace\" lets this layer's list win wholesale; \"shallow\" unions the entries by key and swaps a same-key entry for this layer's; \"deep\" unions by key and merges a same-key pair field by field, a nested keyed list (a ruleset's rules, by type) unioning the same way. To clear a list, write \"replace\" with an empty list: under \"shallow\" and \"deep\" an empty list adds nothing. Omitted, the file's `_layering` applies, else the run's layering. It has no effect on a single document, the rendered document never carries it, and a list nested inside a section entry does not take it.", - "type": "string", - "enum": [ - "replace", - "shallow", - "deep" - ] - } - }, - "required": [ - "entries" - ], - "additionalProperties": false - }, - "UndeclaredPolicyList": { - "description": "The wrapped form of a list, overriding what happens to live resources the file does not declare. The plain array form takes the file's top-level `_undeclared`, else the run's `undeclared` input, else the list's own default (the section default for a top-level section, a fixed default for a nested list such as environments[].variables); this wrapper can set it explicitly, and with `_undeclared` omitted it behaves exactly like the plain array. The wrapper is this action's own vocabulary (nothing here passes through to GitHub), so its keys are strict: anything besides `_undeclared`, `entries`, and `_layering` (the directives wear the underscore; a nested list's wrapper does not take `_layering`) is rejected upfront as a typo.", - "type": "object", - "properties": { - "_undeclared": { - "description": "What apply does to live resources `entries` does not declare: \"delete\" removes them, \"keep\" leaves them alone and surfaces each as a note. Omitted, the file's top-level `_undeclared` applies, else the run's `undeclared` input, else the list's own default.", - "allOf": [ - { - "$ref": "#/definitions/UndeclaredPolicy" - } - ] - }, - "entries": { - "description": "The declared entries, exactly as the plain array form lists them.", - "type": "array", - "items": { - "$ref": "#/definitions/CodespacesSecretConfig" - } - }, - "_layering": { - "description": "A render-time directive: how this section combines with the layers BELOW it in a layered merge, by the section's key (a label's name, a secret's name, a webhook's URL, ...). \"replace\" lets this layer's list win wholesale; \"shallow\" unions the entries by key and swaps a same-key entry for this layer's; \"deep\" unions by key and merges a same-key pair field by field, a nested keyed list (a ruleset's rules, by type) unioning the same way. To clear a list, write \"replace\" with an empty list: under \"shallow\" and \"deep\" an empty list adds nothing. Omitted, the file's `_layering` applies, else the run's layering. It has no effect on a single document, the rendered document never carries it, and a list nested inside a section entry does not take it.", - "type": "string", - "enum": [ - "replace", - "shallow", - "deep" - ] - } - }, - "required": [ - "entries" - ], - "additionalProperties": false - }, - "UndeclaredPolicyList": { - "description": "The wrapped form of a list, overriding what happens to live resources the file does not declare. The plain array form takes the file's top-level `_undeclared`, else the run's `undeclared` input, else the list's own default (the section default for a top-level section, a fixed default for a nested list such as environments[].variables); this wrapper can set it explicitly, and with `_undeclared` omitted it behaves exactly like the plain array. The wrapper is this action's own vocabulary (nothing here passes through to GitHub), so its keys are strict: anything besides `_undeclared`, `entries`, and `_layering` (the directives wear the underscore; a nested list's wrapper does not take `_layering`) is rejected upfront as a typo.", - "type": "object", - "properties": { - "_undeclared": { - "description": "What apply does to live resources `entries` does not declare: \"delete\" removes them, \"keep\" leaves them alone and surfaces each as a note. Omitted, the file's top-level `_undeclared` applies, else the run's `undeclared` input, else the list's own default.", - "allOf": [ - { - "$ref": "#/definitions/UndeclaredPolicy" - } - ] - }, - "entries": { - "description": "The declared entries, exactly as the plain array form lists them.", - "type": "array", - "items": { - "$ref": "#/definitions/CollaboratorConfig" - } - }, - "_layering": { - "description": "A render-time directive: how this section combines with the layers BELOW it in a layered merge, by the section's key (a label's name, a secret's name, a webhook's URL, ...). \"replace\" lets this layer's list win wholesale; \"shallow\" unions the entries by key and swaps a same-key entry for this layer's; \"deep\" unions by key and merges a same-key pair field by field, a nested keyed list (a ruleset's rules, by type) unioning the same way. To clear a list, write \"replace\" with an empty list: under \"shallow\" and \"deep\" an empty list adds nothing. Omitted, the file's `_layering` applies, else the run's layering. It has no effect on a single document, the rendered document never carries it, and a list nested inside a section entry does not take it.", - "type": "string", - "enum": [ - "replace", - "shallow", - "deep" - ] - } - }, - "required": [ - "entries" - ], - "additionalProperties": false - }, - "UndeclaredPolicyList": { - "description": "The wrapped form of a list, overriding what happens to live resources the file does not declare. The plain array form takes the file's top-level `_undeclared`, else the run's `undeclared` input, else the list's own default (the section default for a top-level section, a fixed default for a nested list such as environments[].variables); this wrapper can set it explicitly, and with `_undeclared` omitted it behaves exactly like the plain array. The wrapper is this action's own vocabulary (nothing here passes through to GitHub), so its keys are strict: anything besides `_undeclared`, `entries`, and `_layering` (the directives wear the underscore; a nested list's wrapper does not take `_layering`) is rejected upfront as a typo.", - "type": "object", - "properties": { - "_undeclared": { - "description": "What apply does to live resources `entries` does not declare: \"delete\" removes them, \"keep\" leaves them alone and surfaces each as a note. Omitted, the file's top-level `_undeclared` applies, else the run's `undeclared` input, else the list's own default.", - "allOf": [ - { - "$ref": "#/definitions/UndeclaredPolicy" - } - ] - }, - "entries": { - "description": "The declared entries, exactly as the plain array form lists them.", - "type": "array", - "items": { - "$ref": "#/definitions/CustomPropertyConfig" - } - }, - "_layering": { - "description": "A render-time directive: how this section combines with the layers BELOW it in a layered merge, by the section's key (a label's name, a secret's name, a webhook's URL, ...). \"replace\" lets this layer's list win wholesale; \"shallow\" unions the entries by key and swaps a same-key entry for this layer's; \"deep\" unions by key and merges a same-key pair field by field, a nested keyed list (a ruleset's rules, by type) unioning the same way. To clear a list, write \"replace\" with an empty list: under \"shallow\" and \"deep\" an empty list adds nothing. Omitted, the file's `_layering` applies, else the run's layering. It has no effect on a single document, the rendered document never carries it, and a list nested inside a section entry does not take it.", - "type": "string", - "enum": [ - "replace", - "shallow", - "deep" - ] - } - }, - "required": [ - "entries" - ], - "additionalProperties": false - }, - "UndeclaredPolicyList": { - "description": "The wrapped form of a list, overriding what happens to live resources the file does not declare. The plain array form takes the file's top-level `_undeclared`, else the run's `undeclared` input, else the list's own default (the section default for a top-level section, a fixed default for a nested list such as environments[].variables); this wrapper can set it explicitly, and with `_undeclared` omitted it behaves exactly like the plain array. The wrapper is this action's own vocabulary (nothing here passes through to GitHub), so its keys are strict: anything besides `_undeclared`, `entries`, and `_layering` (the directives wear the underscore; a nested list's wrapper does not take `_layering`) is rejected upfront as a typo.", - "type": "object", - "properties": { - "_undeclared": { - "description": "What apply does to live resources `entries` does not declare: \"delete\" removes them, \"keep\" leaves them alone and surfaces each as a note. Omitted, the file's top-level `_undeclared` applies, else the run's `undeclared` input, else the list's own default.", - "allOf": [ - { - "$ref": "#/definitions/UndeclaredPolicy" - } - ] - }, - "entries": { - "description": "The declared entries, exactly as the plain array form lists them.", - "type": "array", - "items": { - "$ref": "#/definitions/DependabotSecretConfig" - } - }, - "_layering": { - "description": "A render-time directive: how this section combines with the layers BELOW it in a layered merge, by the section's key (a label's name, a secret's name, a webhook's URL, ...). \"replace\" lets this layer's list win wholesale; \"shallow\" unions the entries by key and swaps a same-key entry for this layer's; \"deep\" unions by key and merges a same-key pair field by field, a nested keyed list (a ruleset's rules, by type) unioning the same way. To clear a list, write \"replace\" with an empty list: under \"shallow\" and \"deep\" an empty list adds nothing. Omitted, the file's `_layering` applies, else the run's layering. It has no effect on a single document, the rendered document never carries it, and a list nested inside a section entry does not take it.", - "type": "string", - "enum": [ - "replace", - "shallow", - "deep" - ] - } - }, - "required": [ - "entries" - ], - "additionalProperties": false - }, - "UndeclaredPolicyList": { - "description": "The wrapped form of a list, overriding what happens to live resources the file does not declare. The plain array form takes the file's top-level `_undeclared`, else the run's `undeclared` input, else the list's own default (the section default for a top-level section, a fixed default for a nested list such as environments[].variables); this wrapper can set it explicitly, and with `_undeclared` omitted it behaves exactly like the plain array. The wrapper is this action's own vocabulary (nothing here passes through to GitHub), so its keys are strict: anything besides `_undeclared`, `entries`, and `_layering` (the directives wear the underscore; a nested list's wrapper does not take `_layering`) is rejected upfront as a typo.", - "type": "object", - "properties": { - "_undeclared": { - "description": "What apply does to live resources `entries` does not declare: \"delete\" removes them, \"keep\" leaves them alone and surfaces each as a note. Omitted, the file's top-level `_undeclared` applies, else the run's `undeclared` input, else the list's own default.", - "allOf": [ - { - "$ref": "#/definitions/UndeclaredPolicy" - } - ] - }, - "entries": { - "description": "The declared entries, exactly as the plain array form lists them.", - "type": "array", - "items": { - "$ref": "#/definitions/DeployKeyConfig" - } - }, - "_layering": { - "description": "A render-time directive: how this section combines with the layers BELOW it in a layered merge, by the section's key (a label's name, a secret's name, a webhook's URL, ...). \"replace\" lets this layer's list win wholesale; \"shallow\" unions the entries by key and swaps a same-key entry for this layer's; \"deep\" unions by key and merges a same-key pair field by field, a nested keyed list (a ruleset's rules, by type) unioning the same way. To clear a list, write \"replace\" with an empty list: under \"shallow\" and \"deep\" an empty list adds nothing. Omitted, the file's `_layering` applies, else the run's layering. It has no effect on a single document, the rendered document never carries it, and a list nested inside a section entry does not take it.", - "type": "string", - "enum": [ - "replace", - "shallow", - "deep" - ] - } - }, - "required": [ - "entries" - ], - "additionalProperties": false - }, - "UndeclaredPolicyList": { - "description": "The wrapped form of a list, overriding what happens to live resources the file does not declare. The plain array form takes the file's top-level `_undeclared`, else the run's `undeclared` input, else the list's own default (the section default for a top-level section, a fixed default for a nested list such as environments[].variables); this wrapper can set it explicitly, and with `_undeclared` omitted it behaves exactly like the plain array. The wrapper is this action's own vocabulary (nothing here passes through to GitHub), so its keys are strict: anything besides `_undeclared`, `entries`, and `_layering` (the directives wear the underscore; a nested list's wrapper does not take `_layering`) is rejected upfront as a typo.", - "type": "object", - "properties": { - "_undeclared": { - "description": "What apply does to live resources `entries` does not declare: \"delete\" removes them, \"keep\" leaves them alone and surfaces each as a note. Omitted, the file's top-level `_undeclared` applies, else the run's `undeclared` input, else the list's own default.", - "allOf": [ - { - "$ref": "#/definitions/UndeclaredPolicy" - } - ] - }, - "entries": { - "description": "The declared entries, exactly as the plain array form lists them.", - "type": "array", - "items": { - "$ref": "#/definitions/DeploymentBranchPolicyConfig" - } - } - }, - "required": [ - "entries" - ], - "additionalProperties": false - }, - "UndeclaredPolicyList": { - "description": "The wrapped form of a list, overriding what happens to live resources the file does not declare. The plain array form takes the file's top-level `_undeclared`, else the run's `undeclared` input, else the list's own default (the section default for a top-level section, a fixed default for a nested list such as environments[].variables); this wrapper can set it explicitly, and with `_undeclared` omitted it behaves exactly like the plain array. The wrapper is this action's own vocabulary (nothing here passes through to GitHub), so its keys are strict: anything besides `_undeclared`, `entries`, and `_layering` (the directives wear the underscore; a nested list's wrapper does not take `_layering`) is rejected upfront as a typo.", - "type": "object", - "properties": { - "_undeclared": { - "description": "What apply does to live resources `entries` does not declare: \"delete\" removes them, \"keep\" leaves them alone and surfaces each as a note. Omitted, the file's top-level `_undeclared` applies, else the run's `undeclared` input, else the list's own default.", - "allOf": [ - { - "$ref": "#/definitions/UndeclaredPolicy" - } - ] - }, - "entries": { - "description": "The declared entries, exactly as the plain array form lists them.", - "type": "array", - "items": { - "$ref": "#/definitions/DeploymentProtectionRuleConfig" - } - } - }, - "required": [ - "entries" - ], - "additionalProperties": false - }, - "UndeclaredPolicyList": { - "description": "The wrapped form of a list, overriding what happens to live resources the file does not declare. The plain array form takes the file's top-level `_undeclared`, else the run's `undeclared` input, else the list's own default (the section default for a top-level section, a fixed default for a nested list such as environments[].variables); this wrapper can set it explicitly, and with `_undeclared` omitted it behaves exactly like the plain array. The wrapper is this action's own vocabulary (nothing here passes through to GitHub), so its keys are strict: anything besides `_undeclared`, `entries`, and `_layering` (the directives wear the underscore; a nested list's wrapper does not take `_layering`) is rejected upfront as a typo.", - "type": "object", - "properties": { - "_undeclared": { - "description": "What apply does to live resources `entries` does not declare: \"delete\" removes them, \"keep\" leaves them alone and surfaces each as a note. Omitted, the file's top-level `_undeclared` applies, else the run's `undeclared` input, else the list's own default.", - "allOf": [ - { - "$ref": "#/definitions/UndeclaredPolicy" - } - ] - }, - "entries": { - "description": "The declared entries, exactly as the plain array form lists them.", - "type": "array", - "items": { - "$ref": "#/definitions/EnvironmentSecretConfig" - } - } - }, - "required": [ - "entries" - ], - "additionalProperties": false - }, - "UndeclaredPolicyList": { - "description": "The wrapped form of a list, overriding what happens to live resources the file does not declare. The plain array form takes the file's top-level `_undeclared`, else the run's `undeclared` input, else the list's own default (the section default for a top-level section, a fixed default for a nested list such as environments[].variables); this wrapper can set it explicitly, and with `_undeclared` omitted it behaves exactly like the plain array. The wrapper is this action's own vocabulary (nothing here passes through to GitHub), so its keys are strict: anything besides `_undeclared`, `entries`, and `_layering` (the directives wear the underscore; a nested list's wrapper does not take `_layering`) is rejected upfront as a typo.", - "type": "object", - "properties": { - "_undeclared": { - "description": "What apply does to live resources `entries` does not declare: \"delete\" removes them, \"keep\" leaves them alone and surfaces each as a note. Omitted, the file's top-level `_undeclared` applies, else the run's `undeclared` input, else the list's own default.", - "allOf": [ - { - "$ref": "#/definitions/UndeclaredPolicy" - } - ] - }, - "entries": { - "description": "The declared entries, exactly as the plain array form lists them.", - "type": "array", - "items": { - "$ref": "#/definitions/EnvironmentVariableConfig" - } - } - }, - "required": [ - "entries" - ], - "additionalProperties": false - }, - "UndeclaredPolicyList": { - "description": "The wrapped form of a list, overriding what happens to live resources the file does not declare. The plain array form takes the file's top-level `_undeclared`, else the run's `undeclared` input, else the list's own default (the section default for a top-level section, a fixed default for a nested list such as environments[].variables); this wrapper can set it explicitly, and with `_undeclared` omitted it behaves exactly like the plain array. The wrapper is this action's own vocabulary (nothing here passes through to GitHub), so its keys are strict: anything besides `_undeclared`, `entries`, and `_layering` (the directives wear the underscore; a nested list's wrapper does not take `_layering`) is rejected upfront as a typo.", - "type": "object", - "properties": { - "_undeclared": { - "description": "What apply does to live resources `entries` does not declare: \"delete\" removes them, \"keep\" leaves them alone and surfaces each as a note. Omitted, the file's top-level `_undeclared` applies, else the run's `undeclared` input, else the list's own default.", - "allOf": [ - { - "$ref": "#/definitions/UndeclaredPolicy" - } - ] - }, - "entries": { - "description": "The declared entries, exactly as the plain array form lists them.", - "type": "array", - "items": { - "$ref": "#/definitions/LabelConfig" - } - }, - "_layering": { - "description": "A render-time directive: how this section combines with the layers BELOW it in a layered merge, by the section's key (a label's name, a secret's name, a webhook's URL, ...). \"replace\" lets this layer's list win wholesale; \"shallow\" unions the entries by key and swaps a same-key entry for this layer's; \"deep\" unions by key and merges a same-key pair field by field, a nested keyed list (a ruleset's rules, by type) unioning the same way. To clear a list, write \"replace\" with an empty list: under \"shallow\" and \"deep\" an empty list adds nothing. Omitted, the file's `_layering` applies, else the run's layering. It has no effect on a single document, the rendered document never carries it, and a list nested inside a section entry does not take it.", - "type": "string", - "enum": [ - "replace", - "shallow", - "deep" - ] - } - }, - "required": [ - "entries" - ], - "additionalProperties": false - }, - "UndeclaredPolicyList": { - "description": "The wrapped form of a list, overriding what happens to live resources the file does not declare. The plain array form takes the file's top-level `_undeclared`, else the run's `undeclared` input, else the list's own default (the section default for a top-level section, a fixed default for a nested list such as environments[].variables); this wrapper can set it explicitly, and with `_undeclared` omitted it behaves exactly like the plain array. The wrapper is this action's own vocabulary (nothing here passes through to GitHub), so its keys are strict: anything besides `_undeclared`, `entries`, and `_layering` (the directives wear the underscore; a nested list's wrapper does not take `_layering`) is rejected upfront as a typo.", - "type": "object", - "properties": { - "_undeclared": { - "description": "What apply does to live resources `entries` does not declare: \"delete\" removes them, \"keep\" leaves them alone and surfaces each as a note. Omitted, the file's top-level `_undeclared` applies, else the run's `undeclared` input, else the list's own default.", - "allOf": [ - { - "$ref": "#/definitions/UndeclaredPolicy" - } - ] - }, - "entries": { - "description": "The declared entries, exactly as the plain array form lists them.", - "type": "array", - "items": { - "$ref": "#/definitions/MilestoneConfig" - } - }, - "_layering": { - "description": "A render-time directive: how this section combines with the layers BELOW it in a layered merge, by the section's key (a label's name, a secret's name, a webhook's URL, ...). \"replace\" lets this layer's list win wholesale; \"shallow\" unions the entries by key and swaps a same-key entry for this layer's; \"deep\" unions by key and merges a same-key pair field by field, a nested keyed list (a ruleset's rules, by type) unioning the same way. To clear a list, write \"replace\" with an empty list: under \"shallow\" and \"deep\" an empty list adds nothing. Omitted, the file's `_layering` applies, else the run's layering. It has no effect on a single document, the rendered document never carries it, and a list nested inside a section entry does not take it.", - "type": "string", - "enum": [ - "replace", - "shallow", - "deep" - ] - } - }, - "required": [ - "entries" - ], - "additionalProperties": false - }, - "UndeclaredPolicyList": { - "description": "The wrapped form of a list, overriding what happens to live resources the file does not declare. The plain array form takes the file's top-level `_undeclared`, else the run's `undeclared` input, else the list's own default (the section default for a top-level section, a fixed default for a nested list such as environments[].variables); this wrapper can set it explicitly, and with `_undeclared` omitted it behaves exactly like the plain array. The wrapper is this action's own vocabulary (nothing here passes through to GitHub), so its keys are strict: anything besides `_undeclared`, `entries`, and `_layering` (the directives wear the underscore; a nested list's wrapper does not take `_layering`) is rejected upfront as a typo.", - "type": "object", - "properties": { - "_undeclared": { - "description": "What apply does to live resources `entries` does not declare: \"delete\" removes them, \"keep\" leaves them alone and surfaces each as a note. Omitted, the file's top-level `_undeclared` applies, else the run's `undeclared` input, else the list's own default.", - "allOf": [ - { - "$ref": "#/definitions/UndeclaredPolicy" - } - ] - }, - "entries": { - "description": "The declared entries, exactly as the plain array form lists them.", - "type": "array", - "items": { - "$ref": "#/definitions/RulesetConfig" - } - }, - "_layering": { - "description": "A render-time directive: how this section combines with the layers BELOW it in a layered merge, by the section's key (a label's name, a secret's name, a webhook's URL, ...). \"replace\" lets this layer's list win wholesale; \"shallow\" unions the entries by key and swaps a same-key entry for this layer's; \"deep\" unions by key and merges a same-key pair field by field, a nested keyed list (a ruleset's rules, by type) unioning the same way. To clear a list, write \"replace\" with an empty list: under \"shallow\" and \"deep\" an empty list adds nothing. Omitted, the file's `_layering` applies, else the run's layering. It has no effect on a single document, the rendered document never carries it, and a list nested inside a section entry does not take it.", - "type": "string", - "enum": [ - "replace", - "shallow", - "deep" - ] - } - }, - "required": [ - "entries" - ], - "additionalProperties": false - }, - "UndeclaredPolicyList": { - "description": "The wrapped form of a list, overriding what happens to live resources the file does not declare. The plain array form takes the file's top-level `_undeclared`, else the run's `undeclared` input, else the list's own default (the section default for a top-level section, a fixed default for a nested list such as environments[].variables); this wrapper can set it explicitly, and with `_undeclared` omitted it behaves exactly like the plain array. The wrapper is this action's own vocabulary (nothing here passes through to GitHub), so its keys are strict: anything besides `_undeclared`, `entries`, and `_layering` (the directives wear the underscore; a nested list's wrapper does not take `_layering`) is rejected upfront as a typo.", - "type": "object", - "properties": { - "_undeclared": { - "description": "What apply does to live resources `entries` does not declare: \"delete\" removes them, \"keep\" leaves them alone and surfaces each as a note. Omitted, the file's top-level `_undeclared` applies, else the run's `undeclared` input, else the list's own default.", - "allOf": [ - { - "$ref": "#/definitions/UndeclaredPolicy" - } - ] - }, - "entries": { - "description": "The declared entries, exactly as the plain array form lists them.", - "type": "array", - "items": { - "$ref": "#/definitions/SecretScanningPatternConfig" - } - }, - "_layering": { - "description": "A render-time directive: how this section combines with the layers BELOW it in a layered merge, by the section's key (a label's name, a secret's name, a webhook's URL, ...). \"replace\" lets this layer's list win wholesale; \"shallow\" unions the entries by key and swaps a same-key entry for this layer's; \"deep\" unions by key and merges a same-key pair field by field, a nested keyed list (a ruleset's rules, by type) unioning the same way. To clear a list, write \"replace\" with an empty list: under \"shallow\" and \"deep\" an empty list adds nothing. Omitted, the file's `_layering` applies, else the run's layering. It has no effect on a single document, the rendered document never carries it, and a list nested inside a section entry does not take it.", - "type": "string", - "enum": [ - "replace", - "shallow", - "deep" - ] - } - }, - "required": [ - "entries" - ], - "additionalProperties": false - }, - "UndeclaredPolicyList": { - "description": "The wrapped form of a list, overriding what happens to live resources the file does not declare. The plain array form takes the file's top-level `_undeclared`, else the run's `undeclared` input, else the list's own default (the section default for a top-level section, a fixed default for a nested list such as environments[].variables); this wrapper can set it explicitly, and with `_undeclared` omitted it behaves exactly like the plain array. The wrapper is this action's own vocabulary (nothing here passes through to GitHub), so its keys are strict: anything besides `_undeclared`, `entries`, and `_layering` (the directives wear the underscore; a nested list's wrapper does not take `_layering`) is rejected upfront as a typo.", - "type": "object", - "properties": { - "_undeclared": { - "description": "What apply does to live resources `entries` does not declare: \"delete\" removes them, \"keep\" leaves them alone and surfaces each as a note. Omitted, the file's top-level `_undeclared` applies, else the run's `undeclared` input, else the list's own default.", - "allOf": [ - { - "$ref": "#/definitions/UndeclaredPolicy" - } - ] - }, - "entries": { - "description": "The declared entries, exactly as the plain array form lists them.", - "type": "array", - "items": { - "$ref": "#/definitions/TeamConfig" - } - }, - "_layering": { - "description": "A render-time directive: how this section combines with the layers BELOW it in a layered merge, by the section's key (a label's name, a secret's name, a webhook's URL, ...). \"replace\" lets this layer's list win wholesale; \"shallow\" unions the entries by key and swaps a same-key entry for this layer's; \"deep\" unions by key and merges a same-key pair field by field, a nested keyed list (a ruleset's rules, by type) unioning the same way. To clear a list, write \"replace\" with an empty list: under \"shallow\" and \"deep\" an empty list adds nothing. Omitted, the file's `_layering` applies, else the run's layering. It has no effect on a single document, the rendered document never carries it, and a list nested inside a section entry does not take it.", - "type": "string", - "enum": [ - "replace", - "shallow", - "deep" - ] - } - }, - "required": [ - "entries" - ], - "additionalProperties": false - }, - "UndeclaredPolicyList": { - "description": "The wrapped form of a list, overriding what happens to live resources the file does not declare. The plain array form takes the file's top-level `_undeclared`, else the run's `undeclared` input, else the list's own default (the section default for a top-level section, a fixed default for a nested list such as environments[].variables); this wrapper can set it explicitly, and with `_undeclared` omitted it behaves exactly like the plain array. The wrapper is this action's own vocabulary (nothing here passes through to GitHub), so its keys are strict: anything besides `_undeclared`, `entries`, and `_layering` (the directives wear the underscore; a nested list's wrapper does not take `_layering`) is rejected upfront as a typo.", - "type": "object", - "properties": { - "_undeclared": { - "description": "What apply does to live resources `entries` does not declare: \"delete\" removes them, \"keep\" leaves them alone and surfaces each as a note. Omitted, the file's top-level `_undeclared` applies, else the run's `undeclared` input, else the list's own default.", - "allOf": [ - { - "$ref": "#/definitions/UndeclaredPolicy" - } - ] - }, - "entries": { - "description": "The declared entries, exactly as the plain array form lists them.", - "type": "array", - "items": { - "$ref": "#/definitions/WebhookConfig" - } - }, - "_layering": { - "description": "A render-time directive: how this section combines with the layers BELOW it in a layered merge, by the section's key (a label's name, a secret's name, a webhook's URL, ...). \"replace\" lets this layer's list win wholesale; \"shallow\" unions the entries by key and swaps a same-key entry for this layer's; \"deep\" unions by key and merges a same-key pair field by field, a nested keyed list (a ruleset's rules, by type) unioning the same way. To clear a list, write \"replace\" with an empty list: under \"shallow\" and \"deep\" an empty list adds nothing. Omitted, the file's `_layering` applies, else the run's layering. It has no effect on a single document, the rendered document never carries it, and a list nested inside a section entry does not take it.", - "type": "string", - "enum": [ - "replace", - "shallow", - "deep" - ] - } - }, - "required": [ - "entries" - ], - "additionalProperties": false - }, - "UnknownRule": { - "description": "A rule type the vendored spec does not know, passed through verbatim so a type GitHub ships tomorrow reaches it the day it ships; a typo comes back as GitHub's own 422 naming it.", - "type": "object", - "properties": { - "type": { - "description": "The rule type, as the rulesets API names it; none of the types the spec knows.", - "type": "string", - "not": { - "enum": [ - "creation", - "update", - "deletion", - "required_linear_history", - "merge_queue", - "required_deployments", - "required_signatures", - "pull_request", - "required_status_checks", - "non_fast_forward", - "commit_message_pattern", - "commit_author_email_pattern", - "committer_email_pattern", - "branch_name_pattern", - "tag_name_pattern", - "workflows", - "code_scanning", - "copilot_code_review", - "license_compliance_scanning", - "file_path_restriction", - "max_file_path_length", - "file_extension_restriction", - "max_file_size" - ] - } - }, - "parameters": { - "description": "The rule's parameters, as the API documents them for its type.", - "type": "object", - "additionalProperties": {} - } - }, - "required": [ - "type" - ], - "additionalProperties": {} - }, - "WebhookConfig": { - "description": "One repository webhook, matched to the live repo by config.url. Hook URLs are configuration, not credentials: they appear in drift lines and notes on purpose. The secret never does.", - "type": "object", - "properties": { - "name": { - "description": "GitHub's hook name; \"web\" is the only value modern hooks take, so anything else is rejected.", - "type": "string", - "const": "web" - }, - "config": { - "description": "The delivery settings; config.url is the natural key.", - "allOf": [ - { - "$ref": "#/definitions/WebhookDeliveryConfig" - } - ] - }, - "events": { - "description": "Events that trigger deliveries, compared order-insensitively; GitHub defaults a new hook to [\"push\"]. Each name is one GitHub delivers to repository webhooks, or \"*\" for every event; anything else is refused at parse time, since GitHub would 422 it at apply time. The accepted names come from GitHub's webhooks OpenAPI description, pinned through the @octokit/openapi-webhooks package, so an event GitHub adds is accepted once a release bumps that package.", - "type": "array", - "items": { - "type": "string", - "enum": [ - "branch_protection_configuration", - "branch_protection_rule", - "check_run", - "check_suite", - "code_scanning_alert", - "commit_comment", - "create", - "custom_property_values", - "delete", - "dependabot_alert", - "deploy_key", - "deployment", - "deployment_status", - "discussion", - "discussion_comment", - "fork", - "gollum", - "issue_comment", - "issue_dependencies", - "issues", - "label", - "member", - "meta", - "milestone", - "package", - "page_build", - "ping", - "project", - "project_card", - "project_column", - "public", - "pull_request", - "pull_request_review", - "pull_request_review_comment", - "pull_request_review_thread", - "push", - "registry_package", - "release", - "repository", - "repository_advisory", - "repository_import", - "repository_ruleset", - "repository_vulnerability_alert", - "secret_scanning_alert", - "secret_scanning_alert_location", - "secret_scanning_scan", - "security_and_analysis", - "star", - "status", - "sub_issues", - "team_add", - "watch", - "workflow_job", - "workflow_run", - "*" - ] - } - }, - "active": { - "description": "Whether deliveries fire; GitHub defaults a new hook to true.", - "type": "boolean" - } - }, - "required": [ - "config" - ] - }, - "WebhookDeliveryConfig": { - "description": "A webhook's `config` mapping, sent to the config sub-endpoint on update.", - "type": "object", - "properties": { - "url": { - "description": "The delivery URL, the natural key: a changed url declares a NEW hook (the old one becomes undeclared). Must be an absolute URL; GitHub refuses anything else.", - "type": "string" - }, - "content_type": { - "description": "Payload encoding: exactly \"json\" or \"form\" (GitHub's default); other spellings such as \"JSON\" or a MIME type are refused at parse time.", - "type": "string", - "enum": [ - "json", - "form" - ] - }, - "secret": { - "description": "The shared delivery secret, as a whole-value `$NAME` reference to an environment variable on the action step (never a literal: settings files are committed plaintext). Resolved at apply time; GitHub echoes it back as \"********\", so check mode cannot verify it and apply re-sends it on every run so rotations propagate.", - "type": "string" - }, - "insecure_ssl": { - "description": "Whether to skip TLS verification: \"0\" (verify) or \"1\" (skip), as the string or the number; GitHub stores the string, and any other spelling is refused at parse time.", - "anyOf": [ - { - "type": "string", - "enum": [ - "0", - "1" - ] - }, - { - "type": "number", - "const": 0 - }, - { - "type": "number", - "const": 1 - } - ] - } - }, - "required": [ - "url" - ], - "additionalProperties": { - "description": "Future config fields pass through verbatim." - } - }, - "WorkflowConfig": { - "description": "One workflow's enable/disable state, keyed by its file path. Keys other than path and state are rejected (the enable/disable calls carry no payload, so an extra key could only be a typo).", - "type": "object", - "properties": { - "path": { - "description": "Full \".github/workflows/ci.yml\" or the bare \"ci.yml\" file name.", - "type": "string" - }, - "state": { - "description": "Desired state; every live disabled_* variant counts as \"disabled\".", - "type": "string", - "enum": [ - "active", - "disabled" - ] - } - }, - "required": [ - "path", - "state" - ] - }, - "WorkflowFileConfig": { - "description": "One workflow that must run for the rule to pass.", - "type": "object", - "properties": { - "path": { - "description": "The path to the workflow file.", - "type": "string" - }, - "ref": { - "description": "The branch or tag of the workflow file to use.", - "type": "string" - }, - "repository_id": { - "description": "The id of the repository that holds the workflow.", - "type": "integer" - }, - "sha": { - "description": "The commit of the workflow file to use.", - "type": "string" - } - }, - "required": [ - "path", - "repository_id" - ], - "additionalProperties": {} - } - } -} \ No newline at end of file diff --git a/package.json b/package.json index b6b9164e0..eaf042aa7 100644 --- a/package.json +++ b/package.json @@ -47,9 +47,9 @@ "lint:package": "bun x publint --strict && bun x attw --pack . --profile esm-only", "typecheck": "bun x tsc -p .", "knip": "knip", - "test": "bun test", + "test": "bun run build:schema && bun test", "test:e2e": "bun test/e2e/run.ts", - "fuzz": "bun test/e2e/fuzz.ts", + "fuzz": "bun run build:schema && bun test/e2e/fuzz.ts", "build": "bun run build:gaps-index && bun run build:bundle && bun run build:lib && bun run build:schema && bun run build:docs && bun run build:action-docs && bun run build:inputs-table", "build:bundle": "bun build src/main.ts --target=node --outfile lib/index.js", "build:lib": "bun x tsdown", diff --git a/release-please-config.json b/release-please-config.json index bf25bb1fe..05c57ba96 100644 --- a/release-please-config.json +++ b/release-please-config.json @@ -13,6 +13,7 @@ "extra-files": [ "README.md", "docs/start/getting-started.md", + "docs/start/examples.md", "docs/operate/check-mode.md", "docs/operate/snapshot.md", "docs/operate/multi-repo.md", diff --git a/test/docs/library-examples.test.ts b/test/docs/library-examples.test.ts index a8b0e96dd..b3a4c4832 100644 --- a/test/docs/library-examples.test.ts +++ b/test/docs/library-examples.test.ts @@ -11,6 +11,7 @@ import { spawnSync } from "node:child_process"; import { readFileSync, symlinkSync, writeFileSync } from "node:fs"; import { join, resolve } from "node:path"; import { ROOT } from "../root.js"; +import { SETTINGS_SCHEMA_PATH } from "../settings-schema.js"; import { withTempDir } from "../temp-dir.js"; const PAGE = "docs/reference/library.md"; @@ -18,7 +19,6 @@ const PACKAGE = "@vivswan/github-settings-as-code"; /** Where the package's two import paths land when the fences compile against this checkout. */ const ENTRY = join(ROOT, "src", "index.js"); -const SCHEMA = join(ROOT, "lib", "settings.schema.json"); /** One `ts` fence: its body and the page line (1-based) of the body's first line. */ interface Fence { @@ -67,7 +67,7 @@ export function examplesProgram(fences: readonly Fence[]): { for (const fence of fences) { starts.push(line); const body = fence.body - .replaceAll(`"${PACKAGE}/settings.schema.json"`, JSON.stringify(SCHEMA)) + .replaceAll(`"${PACKAGE}/settings.schema.json"`, JSON.stringify(SETTINGS_SCHEMA_PATH)) .replaceAll(`"${PACKAGE}"`, JSON.stringify(ENTRY)); parts.push(body); line += body.split("\n").length; diff --git a/test/docs/readme.test.ts b/test/docs/readme.test.ts index 84e1a7a3a..172a39ded 100644 --- a/test/docs/readme.test.ts +++ b/test/docs/readme.test.ts @@ -15,6 +15,7 @@ import { grantFor } from "../../src/sections/contract/permissions.js"; import { DOCS } from "../../src/sections/docs-registry.js"; import { SECTIONS } from "../../src/sections/registry.js"; import { ROOT } from "../root.js"; +import { readSettingsSchema } from "../settings-schema.js"; import { defaultClaimProblems, deleteEnumerationProblems } from "./claims.js"; import { fencedBlocks, sectionLines } from "./markdown.js"; import { assertValidSettingsExample } from "./settings-examples.js"; @@ -108,8 +109,7 @@ describe("delete-by-default enumeration", () => { }); describe("schema $schema hints", () => { - const schema = JSON.parse(readFileSync(join(ROOT, "lib", "settings.schema.json"), "utf8")); - const id = schema.$id as string; + const id = readSettingsSchema().$id; /** Every markdown page that may carry a yaml-language-server hint. */ const hintPages = (): Array<{ label: string; path: string }> => [ diff --git a/test/e2e/fuzz.ts b/test/e2e/fuzz.ts index b4c5039e9..0b5dd0fe6 100644 --- a/test/e2e/fuzz.ts +++ b/test/e2e/fuzz.ts @@ -1694,7 +1694,7 @@ async function main(): Promise { (mode === "standard" || mode === "render") && flags.sections ? ` --sections ${flags.sections.join(",")}` : ""; - const replay = `bun test/e2e/fuzz.ts --seed ${seed} --iterations 1${sectionsFlag}`; + const replay = `bun run fuzz --seed ${seed} --iterations 1${sectionsFlag}`; console.log(` iter ${i} [${mode}] seed ${seed} FAIL: ${result.failure}`); console.log(` replay: ${replay}`); reportArtifacts(result, replay); @@ -1709,7 +1709,7 @@ async function main(): Promise { let batteryFailures = 0; if (!replayOne) { - const batteryReplay = `bun test/e2e/fuzz.ts --seed ${master} --iterations 0`; + const batteryReplay = `bun run fuzz --seed ${master} --iterations 0`; type BatteryEntry = [string, (seed: number) => Promise]; const runBattery = async ( header: string, diff --git a/test/e2e/generators.ts b/test/e2e/generators.ts index 905cd4058..7cd2eff33 100644 --- a/test/e2e/generators.ts +++ b/test/e2e/generators.ts @@ -1,12 +1,12 @@ /** * Everything here is a pure function of an Rng, so a failing fuzz iteration replays from its seed. Every settings - * document a section generator draws is also validated against lib/settings.schema.json, so a generator drifting from - * the published schema fails the run instead of fuzzing a shape the schema rejects. + * document a section generator draws is also validated against the built lib/settings.schema.json (`bun run fuzz` + * builds it first), so a generator drifting from the published schema fails the run instead of fuzzing a shape the + * schema rejects. */ import { Ajv, type ValidateFunction } from "ajv"; import addFormats from "ajv-formats"; -import settingsSchema from "../../lib/settings.schema.json" with { type: "json" }; import { validateSettingsDoc } from "../../src/engine/orchestrate.js"; import { SectionSelection } from "../../src/engine/section-selection.js"; import { silentIo } from "../../src/io.js"; @@ -44,6 +44,7 @@ import { genSecretScanningPatterns } from "../sections/secret_scanning_custom_pa import { genTeams } from "../sections/teams/generators.js"; import { genWebhooks } from "../sections/webhooks/generators.js"; import { genWorkflows } from "../sections/workflows/generators.js"; +import { readSettingsSchema } from "../settings-schema.js"; import { ADMIN_SLUG } from "./constants.js"; import { DEFAULT_LAYERING_DIRECTIVE, @@ -993,7 +994,7 @@ function settingsValidator(): ValidateFunction { const ajv = new Ajv({ strict: false, allErrors: true }); const add = (addFormats as unknown as { default?: typeof addFormats }).default ?? addFormats; (add as typeof addFormats)(ajv); - validator = ajv.compile(settingsSchema); + validator = ajv.compile(readSettingsSchema()); } return validator; } diff --git a/test/package-json.test.ts b/test/package-json.test.ts index 477e6b7a5..ec9691708 100644 --- a/test/package-json.test.ts +++ b/test/package-json.test.ts @@ -9,10 +9,12 @@ import { dirname, join, resolve } from "node:path"; import { parseSync } from "oxc-parser"; import { parse as parseYaml } from "yaml"; import manifest from "../.release-please-manifest.json"; -import schema from "../lib/settings.schema.json" with { type: "json" }; import pkg from "../package.json"; import tsdown from "../tsdown.config.js"; import { ROOT } from "./root.js"; +import { readSettingsSchema } from "./settings-schema.js"; + +const schema = readSettingsSchema(); /** tsdown types its options loosely (a glob, a list, or a map); this config is the map form. */ const build = tsdown as { entry: Record; outDir: string }; diff --git a/test/published-schema.test.ts b/test/published-schema.test.ts index eaf430e89..f36b1dfa7 100644 --- a/test/published-schema.test.ts +++ b/test/published-schema.test.ts @@ -5,8 +5,6 @@ */ import { describe, expect, test } from "bun:test"; -import { readFileSync } from "node:fs"; -import { join } from "node:path"; import { Ajv, type ValidateFunction } from "ajv"; import addFormats from "ajv-formats"; import { ok } from "neverthrow"; @@ -14,11 +12,9 @@ import { validateSectionShapes } from "../src/engine/validate.js"; import { SettingsFile, UNDECLARED_POLICY_SECTIONS } from "../src/schema.js"; import { NESTED_KEYS } from "../src/sections/environments/nested.js"; import { ENVIRONMENT_PARSE_FIXTURES } from "./fixtures/environment-parse-rules.js"; -import { ROOT } from "./root.js"; +import { readSettingsSchema } from "./settings-schema.js"; -const schema = JSON.parse(readFileSync(join(ROOT, "lib", "settings.schema.json"), "utf8")) as { - definitions: Record>; -}; +const schema = readSettingsSchema(); // strict: false because the generated schema carries draft-07 idioms AJV's strict mode complains about; validation semantics are unchanged. // The format plugin is loaded so a format keyword, should one ever be emitted, is judged here the way editors and CI linters judge it. diff --git a/test/schema-corpus.test.ts b/test/schema-corpus.test.ts index 988e91c84..ab4f84459 100644 --- a/test/schema-corpus.test.ts +++ b/test/schema-corpus.test.ts @@ -4,13 +4,13 @@ import { basename, join } from "node:path"; import { Ajv, type ValidateFunction } from "ajv"; import addFormats from "ajv-formats"; import { parse } from "yaml"; -import settingsSchema from "../lib/settings.schema.json" with { type: "json" }; import { validateSectionShapes } from "../src/engine/validate.js"; import { SECTION_KEYS } from "../src/schema.js"; import { genSettings } from "./e2e/generators.js"; import { Rng } from "./e2e/prng.js"; import { collectYmlFiles, scenarioRoots } from "./e2e/schema.js"; import { ROOT } from "./root.js"; +import { readSettingsSchema } from "./settings-schema.js"; interface CorpusDoc { /** Where the fragment came from ("labels-apply-converges.yml settings"). */ @@ -207,7 +207,7 @@ describe("published schema agrees with the runtime over the corpus", () => { const ajv = new Ajv({ strict: false, allErrors: true }); const add = (addFormats as unknown as { default?: typeof addFormats }).default ?? addFormats; (add as typeof addFormats)(ajv); - const validate: ValidateFunction = ajv.compile(settingsSchema); + const validate: ValidateFunction = ajv.compile(readSettingsSchema()); test("every scenario fragment and generated document gets one verdict", () => { const disagreements: string[] = []; diff --git a/test/scripts/generated.test.ts b/test/scripts/generated.test.ts index 8f851e2f7..95cb74292 100644 --- a/test/scripts/generated.test.ts +++ b/test/scripts/generated.test.ts @@ -26,7 +26,7 @@ const scripts = ( } ).scripts; /** The outputs the marker scan cannot see: whole generated files. */ -const WHOLE_FILES = ["lib/settings.schema.json", "src/upstream-gaps/index.ts"]; +const WHOLE_FILES = ["src/upstream-gaps/index.ts"]; const tracked = execFileSync("git", ["ls-files", "-z"], { cwd: ROOT, encoding: "utf8" }) .split("\0") diff --git a/test/scripts/release-pipeline-build.test.ts b/test/scripts/release-pipeline-build.test.ts index 2f6129f56..e355a95f6 100644 --- a/test/scripts/release-pipeline-build.test.ts +++ b/test/scripts/release-pipeline-build.test.ts @@ -226,7 +226,7 @@ describe("packageCommit", () => { expect(String((error as Error).message)).toMatch( new RegExp( `^${ref} \\(${packaged}\\) packages ${fx.mergeSha}, but its tree [0-9a-f]{40} is not the tree [0-9a-f]{40} ` + - "this checkout's build packages, so the two differ under lib/index\\.js and lib/pkg/.*Diff the two trees by hand; " + + "this checkout's build packages, so the two differ under lib/index\\.js, lib/settings\\.schema\\.json, and lib/pkg/.*Diff the two trees by hand; " + "no run replaces a packaged commit it did not mint; if the build is wrong, delete the tag by hand and rerun\\.$", ), ); @@ -395,7 +395,7 @@ describe("packageCommit", () => { const pushes = withPushPlans(fx, [], () => { expect(() => packageCommit({ cwd: stale, sourceSha: fx.mergeSha })).toThrow( new RegExp( - `^${LATEST} \\(${bare}\\) is not ${b.sha} plus lib/index\\.js and lib/pkg/, minus package\\.json's preparation scripts, alone: .*; inspect it by hand\\.$`, + `^${LATEST} \\(${bare}\\) is not ${b.sha} plus lib/index\\.js, lib/settings\\.schema\\.json, and lib/pkg/, minus package\\.json's preparation scripts, alone: .*; inspect it by hand\\.$`, ), ); }); @@ -766,7 +766,7 @@ describe("movePointer", () => { const seedPackage = packageCommit({ cwd: seedRun, sourceSha: fx.seedSha }); expect(() => movePointer(fx.work, V2, seedPackage)).toThrow( new RegExp( - `^${V2} \\(${fx.mergeSha}\\) is not ${fx.seedSha} plus lib/index\\.js and lib/pkg/, minus package\\.json's preparation scripts, alone: .*; inspect it by hand\\.$`, + `^${V2} \\(${fx.mergeSha}\\) is not ${fx.seedSha} plus lib/index\\.js, lib/settings\\.schema\\.json, and lib/pkg/, minus package\\.json's preparation scripts, alone: .*; inspect it by hand\\.$`, ), ); }); diff --git a/test/scripts/release-pipeline-fixture.ts b/test/scripts/release-pipeline-fixture.ts index 866fdccb1..de30b94a6 100644 --- a/test/scripts/release-pipeline-fixture.ts +++ b/test/scripts/release-pipeline-fixture.ts @@ -143,11 +143,12 @@ export function commitAll(cwd: string, subject: string): string { return git(cwd, "rev-parse", "HEAD"); } -/** The files a build of `bundle` leaves in a checkout: the action bundle and - * the library build (its module and its declarations), all gitignored on main. */ +/** The files a build of `bundle` leaves in a checkout: the action bundle, the + * schema, and the library build (its module and its declarations), all gitignored on main. */ export function builtFiles(bundle: string): Record { return { "lib/index.js": bundle, + "lib/settings.schema.json": `schema-${bundle}`, "lib/pkg/index.js": `library-${bundle}`, "lib/pkg/index.d.ts": `types-${bundle}`, }; @@ -194,7 +195,8 @@ function treePaths(cwd: string, sha: string): string[] { } /** The paths a packaged commit's diff against its source lists: the build outputs and the stripped manifest. */ -const PACKAGED_DIFF = "lib/index.js\nlib/pkg/index.d.ts\nlib/pkg/index.js\npackage.json"; +const PACKAGED_DIFF = + "lib/index.js\nlib/pkg/index.d.ts\nlib/pkg/index.js\nlib/settings.schema.json\npackage.json"; export const CHANGELOG_21 = `# Changelog @@ -245,6 +247,7 @@ export function expectPackage( "lib/index.js", "lib/pkg/index.d.ts", "lib/pkg/index.js", + "lib/settings.schema.json", "package.json", "release-please-config.json", "src/marker.ts", @@ -278,7 +281,7 @@ export function seedFixture(): Fixture { execFileSync("git", ["init", "--quiet", "--bare", "-b", "main", origin]); disableBackgroundMaintenance(origin); const work = clone(root, origin, "work"); - write(work, ".gitignore", "lib/index.js\nlib/pkg/\n"); + write(work, ".gitignore", "lib/index.js\nlib/settings.schema.json\nlib/pkg/\n"); write(work, ".release-please-manifest.json", `${JSON.stringify({ ".": "2.0.0" }, null, 2)}\n`); write( work, @@ -425,8 +428,13 @@ export function plantCommitIn( } /** The refusal of a child that is not its source plus the build outputs alone, whatever deviates. */ -const NOT_A_PACKAGE = - /is not [0-9a-f]{40} plus lib\/index\.js and lib\/pkg\/, minus package\.json's preparation scripts, alone: its tree is [0-9a-f]{40}, the rebuilt one is [0-9a-f]{40} \(git diff [0-9a-f]{40} [0-9a-f]{40} lists what deviates\); /; +const NOT_A_PACKAGE = new RegExp( + [ + "is not [0-9a-f]{40} plus lib/index\\.js, lib/settings\\.schema\\.json, and lib/pkg/, ", + "minus package\\.json's preparation scripts, alone: its tree is [0-9a-f]{40}, the rebuilt one is ", + "[0-9a-f]{40} \\(git diff [0-9a-f]{40} [0-9a-f]{40} lists what deviates\\); ", + ].join(""), +); /** A hand-planted commit under a packaged commit's name, and the refusal every path (a rerun, the major) answers * with; the remedy tail differs per ref and is the caller's to check. */ @@ -510,6 +518,7 @@ export const PLANTED_PACKAGES: [string, PlantedPackage][] = [ (fx) => ({ ...plantCommit(fx, "planter", fx.mergeSha, fx.mergeSha, { "lib/index.js": "packaged-bundle-bytes-1\n", + "lib/settings.schema.json": "schema-packaged-bundle-bytes-1\n", }), error: /is not the tree [0-9a-f]{40} this checkout's build packages|does not carry a non-empty regular-file lib\/pkg\/index\.js \(no entry\)/, diff --git a/test/scripts/release-pipeline.test.ts b/test/scripts/release-pipeline.test.ts index b0b8cb63c..1f9ce3e0d 100644 --- a/test/scripts/release-pipeline.test.ts +++ b/test/scripts/release-pipeline.test.ts @@ -419,7 +419,7 @@ describe("packageRelease", () => { // The build the packaged commit must carry, spoiled two ways; the shared check refuses both entry points before // any push. The entry shown for the empty file is git's empty blob at ls-tree's size column. - const unbuilt = ["lib/index.js", "lib/pkg/index.js"].flatMap( + const unbuilt = ["lib/index.js", "lib/settings.schema.json", "lib/pkg/index.js"].flatMap( (file): [state: string, file: string, spoil: (dir: string) => void, entry: string][] => [ [ "an empty", @@ -451,7 +451,7 @@ describe("packageRelease", () => { const fx = seedFixture(); write(fx.work, "src/marker.ts", "export const marker = 999;\n"); expect(() => packageRelease({ cwd: fx.work, tag: "v2.1.0", sourceSha: fx.mergeSha })).toThrow( - /pending changes beyond lib\/index\.js and lib\/pkg\//, + /pending changes beyond lib\/index\.js, lib\/settings\.schema\.json, and lib\/pkg\//, ); expect(remoteRef(fx, TAG)).toBe(""); expect(buildTags(fx)).toEqual([]); @@ -559,7 +559,7 @@ describe("retagMajor", () => { git(planter, "push", "--quiet", "origin", TAG); const mover = clone(fx.root, fx.origin, "mover-empty"); expect(() => retagMajor({ cwd: mover, tag: "v2.1.0", sourceSha: fx.mergeSha })).toThrow( - /is not [0-9a-f]{40} plus lib\/index\.js and lib\/pkg\/, minus package\.json's preparation scripts, alone/, + /is not [0-9a-f]{40} plus lib\/index\.js, lib\/settings\.schema\.json, and lib\/pkg\/, minus package\.json's preparation scripts, alone/, ); expect(remoteRef(fx, V2)).toBe(""); }); diff --git a/test/settings-schema.ts b/test/settings-schema.ts new file mode 100644 index 000000000..1ae234d0f --- /dev/null +++ b/test/settings-schema.ts @@ -0,0 +1,21 @@ +import { existsSync, readFileSync } from "node:fs"; +import { join } from "node:path"; +import { ROOT } from "./root.js"; + +/** The built, gitignored schema; `bun run test` and `bun run fuzz` run `bun run build:schema` before loading it. */ +export const SETTINGS_SCHEMA_PATH = join(ROOT, "lib", "settings.schema.json"); + +export interface SettingsSchemaFile { + $id: string; + definitions: Record>; + [keyword: string]: unknown; +} + +/** Read at run time rather than imported: `tsc` resolves a JSON import, so an import would fail every typecheck on + * a checkout that has not built the file. */ +export function readSettingsSchema(): SettingsSchemaFile { + if (!existsSync(SETTINGS_SCHEMA_PATH)) { + throw new Error("lib/settings.schema.json is not built; run `bun run build:schema`"); + } + return JSON.parse(readFileSync(SETTINGS_SCHEMA_PATH, "utf8")) as SettingsSchemaFile; +}