You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
Commit dc7d044
Browse filesBrowse the repository at this point in the historyBrowse files
fix(settings)!: unknown underscore keys fail everywhere; teams gets the _undeclared knob; GSAC_RETRY_BASE_MS
Four v3 rule decisions, each a small change:
- Underscore rule: an unknown `_x` key is rejected at the document root as it
already was inside a wrapper, with an error naming the two directives
(`_layering`, `_undeclared`); a note belongs in a YAML comment. A misspelled
`_layerin: replace` can no longer pass as a private note and merge a layer
meant to replace.
- teams takes `knobbed(TeamConfig)` with default `keep`: every plan lists the
repository's teams, notes each undeclared direct grant, and revokes them under
`_undeclared: delete` (DELETE /orgs/{org}/teams/{slug}/repos/{owner}/{repo});
access granted above the repository is noted, never touched. The snapshot
writes the wrapper form.
- RETRY_BASE_MS becomes GSAC_RETRY_BASE_MS, documented in the inputs reference;
the old name is ignored, as an environment variable has no loud-error channel.
- The 12 scenario files named with a section key's underscore are dashed, and
a test over the corpus pins the rule.
- Drift lines read declared-first everywhere: collaborators and teams said
`live role X != declared Y`, the other sections `declared X != live Y`.
Copy file name to clipboardExpand all lines: COVERAGE.md
+1-1Lines changed: 1 addition & 1 deletion
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -36,7 +36,7 @@ The tenet: this action can control everything about a repository and nothing abo
36
36
|[Code scanning default setup](https://docs.github.com/en/rest/code-scanning/code-scanning)|`code_scanning_default_setup`| GET/PATCH /repos/{owner}/{repo}/code-scanning/default-setup, PATCH body verbatim (state, query_suite, languages, runner_type, runner_label, threat_model). A 202 answer means GitHub rolls the change out in a configuration run, which the log names. Check compares declared keys only, languages as a set. Needs GitHub Advanced Security on private repositories; a 403 can mean that (or an archived repository) rather than a missing permission. Snapshot reads the GET body back on the declared keys; a null the slice does not take (a not-configured setup's runner_type) is omitted. |
37
37
| [Code quality setup](https://docs.github.com/en/rest/code-quality/code-quality) | `code_quality_setup` | GET/PATCH /repos/{owner}/{repo}/code-quality/setup, PATCH body verbatim (state, languages, runner_type, runner_label, ai_findings_option), the near field-for-field mirror of code_scanning_default_setup. A 202 answer means GitHub rolls the change out in a configuration run, which the log names; a 409 means a configuration run is already in progress (re-run the workflow after it finishes); a 403 can mean code quality is unavailable on the repository or the repository is archived, rather than a missing permission, and a 422 (the change cannot be applied) carries GitHub's message verbatim. Check compares declared keys only, languages as a set. ASSUMPTION: the GET is graded as a plain read under the Administration permission, mirroring code scanning - the fine-grained grading of these endpoints is not verified against production. The sibling /code-quality/findings endpoints are read-only and stay out of scope. Snapshot reads the GET body back on the declared keys; a null the slice does not take (a not-configured setup's runner_type) is omitted. |
38
38
| [Collaborators](https://docs.github.com/en/rest/collaborators/collaborators) (direct) | `collaborators` | PUT/DELETE /repos/{owner}/{repo}/collaborators/{username} (affiliation=direct); pending invitations reconciled alongside via GET /repos/{owner}/{repo}/invitations and PATCH/DELETE /repos/{owner}/{repo}/invitations/{invitation_id}: a declared user's matching pending invitation converges, a stale permission is PATCHed in place (a declared custom role cannot be verified against an invitation - the standard-roles enum - so the invitation is kept with a note), an expired invitation is cancelled and re-sent, and undeclared pending invitations follow the section's one undeclared policy (email invitations, which no username can declare, are noted and left untouched); vocabulary mapping push<->write, pull<->read for check mode; custom org role names pass through; undeclared direct collaborators REMOVED by default (kept as notes under the wrapped `_undeclared: keep` form), owner never touched; new users get invitations. Snapshot reads the direct collaborators and the pending username invitations back as entries under the delete default, each role through the inverse mapping (write -> push, read -> pull, custom role names verbatim); the repository owner, email invitations, and expired invitations are noted, not declared, and a live role no declaration plans as (a custom role named "push" or "pull") fails the collaborators section; the snapshot is written as partial without it. |
39
-
|[Team repository permissions](https://docs.github.com/en/rest/teams/teams) (org repos) |`teams`| PUT /orgs/{org}/teams/{slug}/repos/{owner}/{repo}; probes GET /orgs/{owner} and no-ops with a note on personal accounts (404 only; 403/5xx still fail). Both modes probe each team's access with the v3.repository media type to read role_name, and the grant is issued only when the team lacks access or its live role diverges. Snapshot lists the teams with access (GET /repos/{owner}/{repo}/teams) and reads each team's role through the same probe, so a custom role reads back by name; noted, not declared: access granted at the organization or enterprise level, a probe 404 (no access, or a fine-grained token missing the grant; the note names both), a role the probe does not report, and a role no declaration plans as (a custom role named "push" or "pull"). A personal account snapshots nothing. |
39
+
| [Team repository permissions](https://docs.github.com/en/rest/teams/teams) (org repos) | `teams` | PUT/DELETE /orgs/{org}/teams/{slug}/repos/{owner}/{repo}; probes GET /orgs/{owner} and no-ops with a note on personal accounts (404 only; 403/5xx still fail). Both modes probe each declared team's access with the v3.repository media type to read role_name, and the grant is issued only when the team lacks access or its live role diverges. The repository's team list (GET /repos/{owner}/{repo}/teams) names the undeclared teams: kept by default and noted (a team is often granted by the org for reasons outside one repository's file); under the wrapped `_undeclared: delete` form a team's DIRECT grant is revoked, while access granted at the organization or enterprise level is noted as beyond the repository's reach. Snapshot lists the same teams and reads each team's role through the same probe, so a custom role reads back by name, and writes the wrapped form with the keep default spelled out; noted, not declared: access granted at the organization or enterprise level, a probe 404 (no access, or a fine-grained token missing the grant; the note names both), a role the probe does not report, and a role no declaration plans as (a custom role named "push" or "pull"). A personal account snapshots nothing. |
40
40
|[Milestones](https://docs.github.com/en/rest/issues/milestones)|`milestones`| POST/PATCH/DELETE /repos/{owner}/{repo}/milestones, matched by title, declared-keys-only (description/state untouched unless declared); undeclared milestones kept by default and surfaced as notes, because deleting a milestone DETACHES it from every issue carrying it - the wrapped `_undeclared: delete` form opts into exactly that. Snapshot reads every milestone, open and closed, back as entries under the keep default. |
41
41
| [Interaction limits](https://docs.github.com/en/rest/interactions/repos) | `interaction_limits` | GET/PUT/DELETE /repos/{owner}/{repo}/interaction-limits, plus the two routed keys below. Limits self-expire (expiry tops out at six_months), so apply re-arms the declared limit on EVERY run and check mode reports drift once it lapses - schedule apply more often than the chosen expiry to keep it armed. The declared expiry is write-only (GitHub reads back only the computed expires_at), so check verifies the limit value, not the duration. `interaction_limits: null` clears a live repo-level limit - the base limit only, never the cap or bypass list below. In a mode: merge fold, interaction_limits: null over a lower layer's declaration removes the key from the merged document instead, mirroring pages: null. A 409 means an organization- or user-level limit overrides the repository's; writes surface that as a note, not a failure, while check mode still reports a mismatched declaration as drift - the org is the place to change it. `pull_request_creation_cap` routes to GET/PATCH /repos/{owner}/{repo}/interaction-limits/pulls/creation-cap: persistent desired state with no self-expiry (enabled plus max_open_pull_requests 1-1000, read back verbatim), so check diffs it exactly and apply PATCHes only on divergence - no re-arm. Where the cap is unavailable the endpoints answer 405, which apply surfaces as a note (like the 409) and check as honest drift. `pull_request_creation_bypass` routes to GET/PUT/DELETE /repos/{owner}/{repo}/interaction-limits/pulls/bypass-list and reconciles: apply DELETEs only the undeclared logins and then PUTs only the missing ones ({users: [logins]}, compared case-insensitively; removals go first because the list holds at most 100 users, which also caps the declaration), never a wholesale replace; a declared empty list removes everyone. Snapshot reads back the repository-level limit (an inherited one is noted, not declared, and the expiry duration is not readable), the cap when the feature exists and is enabled, and the bypass list when it has members. |
42
42
|[Actions variables](https://docs.github.com/en/rest/actions/variables)|`actions_variables`| GET/POST /repos/{owner}/{repo}/actions/variables and PATCH/DELETE /repos/{owner}/{repo}/actions/variables/{name}: plain-text repository variables, upserted by name. Names are case-insensitive (GitHub stores them uppercased regardless of how they are entered), so matching and duplicate rejection compare uppercased names. Values read back in full, which is what makes exact check-mode diffing possible; undeclared variables are DELETED by default, kept as notes under the wrapped `_undeclared: keep` form. The list endpoint caps per_page at 30 (not the usual 100), which the page loop honors so a large inventory is never truncated. Secrets are write-only material and deliberately NOT this section: they live in the `actions_secrets` section above. Snapshot reads the variables back as name and value entries under the delete default. |
Copy file name to clipboardExpand all lines: docs/operate/layering.md
+5-4Lines changed: 5 additions & 4 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -70,7 +70,7 @@ The step ends with `result: merged` and exit 0, or exit 1 with an error naming t
70
70
| `_layering` | A list section's `{entries}` wrapper, or a file's top level | Merge time | Overrides `layering` for that section, or for every section of that file. Consumed by the merge: the merged file never carries it |
71
71
| `_undeclared` | A list section's `{entries}` wrapper | Live state | What apply does to live resources the document does not declare: `keep`or `delete`. Travels through the merge and is resolved in the merged file. The [undeclared policy](../reference/undeclared-policy.md) page owns it |
72
72
73
-
The two underscore keys are this action's directives, never GitHub settings. Any other underscore key at a file's top level is a private note and is dropped from the merged file.
73
+
The two underscore keys are this action's directives, never GitHub settings, and they are the whole underscore vocabulary: any other underscore key, at a file's top level or on a wrapper, fails validation with an error naming these two (a note belongs in a YAML comment). A misspelled directive can therefore never pass as a private note and quietly merge a layer meant to replace.
74
74
75
75
## The rules
76
76
@@ -193,8 +193,8 @@ Reading it back:
193
193
194
194
The merged file is exactly what apply runs, so it is worth knowing its shape:
195
195
196
-
- Every list section that takes the `_undeclared` knob (the fifteen the [undeclared policy](../reference/undeclared-policy.md) lists) is in its `{_undeclared, entries}` wrapper form, with `_undeclared` resolved to an explicit `keep` or `delete`. Other lists (`branches`, `environments`, and the nested per-environment lists) stay as written.
197
-
- No `_layering` anywhere, and no private underscore notes: both are consumed before the file is written.
196
+
- Every list section that takes the `_undeclared` knob (the sixteen the [undeclared policy](../reference/undeclared-policy.md) lists) is in its `{_undeclared, entries}` wrapper form, with `_undeclared` resolved to an explicit `keep` or `delete`. Other lists (`branches`, `environments`, and the nested per-environment lists) stay as written.
197
+
- No `_layering` anywhere: the directive is consumed before the file is written, and YAML comments do not survive the fold.
198
198
- A `null` that met nothing below stays, keeping its engine meaning.
199
199
- Every layer was validated on its own before the fold, and the result is validated again before it is written.
200
200
@@ -256,6 +256,7 @@ The per-layer validation catches what a standalone settings file could not say,
256
256
| A list section that is not a list or an `{entries}` wrapper (`labels: oops`) | Validation: `labels: Invalid input: expected a list of entries, or a mapping with "entries" (and an optional "_undeclared" policy), but this section parsed as string` |
257
257
| A keyed entry without its key, here a label with no `name` (`labels: [{name: bug}, {color: d73a4a}]`) | Validation: `labels[1].name: Invalid input: expected string, received undefined` |
258
258
| A non-mapping entry (`milestones: [v2]`) | Validation: `milestones[0]: Invalid input: expected object, received string` |
259
+
| An underscore key that is not a directive on a wrapper (`labels: {_notes: x, entries: [{name: bug}]}`) | Validation: `labels: Unrecognized key: "_notes"; the wrapper's directives are "_undeclared" and, on a top-level section, "_layering", and nothing else - there are no private-note keys. Remove the key, or keep the note as a YAML comment` |
259
260
260
261
The fold itself refuses what only a merge can judge (`layer ".github/settings/repo.yml": ...`). A fold refusal names the key path (entries by index) and the kind of problem, never a value from the document: the merge runs without a repository's redaction context, so a label name or rule type echoed here could put a private repository's settings into a public log.
261
262
@@ -285,7 +286,7 @@ A merge-mode log can therefore show your settings file's structure and, through
285
286
| `_layering: merge` on a section with no layering key, on its wrapper or reached from the file level (`milestones: {_layering: merge, entries: [{title: v1}]}` or `{_layering: merge, milestones: [{title: v1}]}`) | `milestones has no layering key, so it cannot be layered by "merge"; declare _layering: replace or drop the directive` |
286
287
| An unknown `_layering` value on a wrapper (`labels: {_layering: union, entries: [{name: bug}]}`) | `labels._layering must be "merge" or "replace"; got a string that is neither` |
287
288
| An unknown `_layering` value at the file's top level (`_layering: union`) | `_layering must be "merge" or "replace"; got a string that is neither` |
288
-
| A YAML anchor aliased inside its own node (`_notes: &loop {self: *loop}`) | `the document contains a reference cycle (a YAML anchor that includes itself); layers must be trees` |
289
+
| A YAML anchor aliased inside its own node (`repository: &loop {self: *loop}`) | `the document contains a reference cycle (a YAML anchor that includes itself); layers must be trees` |
Copy file name to clipboardExpand all lines: docs/operate/troubleshooting.md
+2-2Lines changed: 2 additions & 2 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -50,10 +50,10 @@ What you see: the run fails during validation, naming the unknown keys and listi
50
50
51
51
What it means: a misspelled section that silently did nothing would break the loud-failure promise, so unknown top-level keys are hard errors (see [Forward compatibility](../reference/forward-compatibility.md)).
52
52
53
-
What to do: the message names all three options. Fix the typo. Or, for a deliberate private key, prefix it with an underscore, which the validator skips:
53
+
What to do: the message names both options. Fix the typo, keeping any note as a YAML comment (an underscore key is not a private note: `_owner_notes: ...` fails with its own error naming the two directives the underscore is reserved for):
54
54
55
55
```yaml settings
56
-
_owner_notes: contact the platform team before editing this file
56
+
# owner notes: contact the platform team before editing this file
Copy file name to clipboardExpand all lines: docs/playbooks/incident-freeze.md
+1-2Lines changed: 1 addition & 2 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -7,8 +7,7 @@ order: 390
7
7
During an incident, merges must stop in minutes and be restored just as fast, with both directions on the record. Two profile files and one dispatch workflow do it. The freeze:
Copy file name to clipboardExpand all lines: docs/reference/inputs.md
+12Lines changed: 12 additions & 0 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -48,3 +48,15 @@ The discovery-only inputs apply to `repos: "*"`; the [multi-repo guide](../opera
48
48
All three outputs are set on every run, whatever the mode and however it ended.
49
49
50
50
A `check` run exits 1 on any drift, so a downstream step usually reads `result` only when the step runs with `continue-on-error: true` or through `if: always()`. [Check mode](../operate/check-mode.md) has the exit-code table.
51
+
52
+
## Environment variables
53
+
54
+
Beside the inputs, a run reads these from its environment; the Actions runner sets the `GITHUB_*` ones, and the [command line](../start/cli.md) reads the same names.
55
+
56
+
| Variable | Read for |
57
+
|---|---|
58
+
|`GITHUB_TOKEN`| The token when the `token` input (or `--token`) is empty |
59
+
|`GITHUB_REPOSITORY`| The default `repository`, the owner of a bare `<name>.yml` under `repos-dir`, and the one target never redacted |
60
+
|`GITHUB_SERVER_URL`, `GITHUB_RUN_ID`| The run link a private report carries |
61
+
|`GITHUB_API_URL`| The REST base URL (GitHub Enterprise Server) |
62
+
|`GSAC_RETRY_BASE_MS`| A test knob: the real milliseconds in one plugin second, for the retry backoff, the rate-limit `Retry-After` waits, and the write limiter's gap. Unset, a second is a second. Set it only to make a retry scenario finish in milliseconds against a mock; against GitHub a shortened wait earns the next secondary rate limit |
0 commit comments