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

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
49 changes: 26 additions & 23 deletions .github/workflows/merge-bot-pull-request.yml
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,18 @@
name: Merge bot pull request action

# Enables auto-merge on bot pull requests (Dependabot and codegen) and disables it when a maintainer
# pushes to a bot branch. Auto-merge completes only once the required checks pass, so a breaking update
# blocks itself. Merge method follows the base: squash for develop, merge commit for main.
#
# Every job mints an App token. The built-in GITHUB_TOKEN will not do: its merge commits do not trigger
# downstream workflows (GitHub's recursion guard), and on a Dependabot pull request it is read-only.
#
# Trigger is pull_request_target, not pull_request, so action SHAs resolve from the base branch while the
# jobs hold the App key. Safe because this workflow never checks out PR code, it only runs `gh pr merge`
# against the PR URL.
# Auto-merges in-repo bot PRs (Dependabot, codegen): enable on opened/reopened, disable on a maintainer push.
# Carried from the fleet reference trimmed to the jobs this repo needs (no upstream-version tracker).
# - Merge method by base: develop = squash, main = merge.
# - App token, not GITHUB_TOKEN: fires downstream workflows on merge, and grants write on read-only Dependabot PRs.
# - pull_request_target, not pull_request: jobs hold the App key, so the workflow + action SHAs resolve from the
# trusted base, not PR head. Safe because no job checks out PR code (each runs gh pr merge by URL).
on:
pull_request_target:
types: [opened, reopened, synchronize]

# Key on the PR number, not github.ref (the base branch under pull_request_target, which would serialize
# every bot PR). cancel-in-progress: false so a synchronize does not cancel an in-flight opened run before
# it enables auto-merge.
# Concurrency keys on the PR number, not github.ref (the base branch under pull_request_target, which would
# serialize every bot PR against it), so each PR queues independently. cancel-in-progress: false so a follow-up
# synchronize doesn't cancel an in-flight opened run before it enables auto-merge.
concurrency:
group: ${{ github.workflow }}-${{ github.event.pull_request.number }}
cancel-in-progress: false
Expand All@@ -26,8 +22,7 @@ jobs:
merge-dependabot:
name: Merge dependabot pull request job
runs-on: ubuntu-latest
# In-repo Dependabot PRs only, on opened/reopened so the disable job stays sticky. Every tier
# auto-merges, semver-major included: the required checks are the gate, not the version bump.
# Dependabot PRs from this repo (not forks). Only on opened/reopened so the disable job stays sticky.
if: >-
(github.event.action == 'opened' || github.event.action == 'reopened') &&
github.event.pull_request.user.login == 'dependabot[bot]' &&
Expand All@@ -45,24 +40,28 @@ jobs:
client-id: ${{ secrets.CODEGEN_APP_CLIENT_ID }}
private-key: ${{ secrets.CODEGEN_APP_PRIVATE_KEY }}

# Auto-merge every tier, semver-major included: the required checks are the gate, not the bump magnitude.
- name: Merge pull request step
run: |
set -euo pipefail
case "${{ github.event.pull_request.base.ref }}" in
develop) method=--squash ;;
main) method=--merge ;;
*) echo "::error::Unsupported base branch: ${{ github.event.pull_request.base.ref }}"; exit 1 ;;
*)
echo "::error::Unsupported base branch: ${{ github.event.pull_request.base.ref }}"
exit 1
;;
esac
gh pr merge --auto --delete-branch "$method" "$PR_URL"
gh pr merge --auto "$method" "$PR_URL"
env:
PR_URL: ${{ github.event.pull_request.html_url }}
GH_TOKEN: ${{ steps.app-token.outputs.token }}

merge-codegen:
name: Merge codegen pull request job
runs-on: ubuntu-latest
# In-repo codegen PRs, on opened/reopened, with the head/base pairing pinned
# (codegen-main -> main, codegen-develop -> develop).
# Codegen PRs from this repo. Head/base pairing is enforced strictly (codegen-main->main, codegen-develop->
# develop). Only on opened/reopened so the disable job stays sticky.
if: >-
(github.event.action == 'opened' || github.event.action == 'reopened') &&
github.event.pull_request.user.login == 'ptr727-codegen[bot]' &&
Expand DownExpand Up@@ -90,18 +89,21 @@ jobs:
case "${{ github.event.pull_request.base.ref }}" in
develop) method=--squash ;;
main) method=--merge ;;
*) echo "::error::Unsupported base branch: ${{ github.event.pull_request.base.ref }}"; exit 1 ;;
*)
echo "::error::Unsupported base branch: ${{ github.event.pull_request.base.ref }}"
exit 1
;;
esac
gh pr merge --auto --delete-branch "$method" "$PR_URL"
gh pr merge --auto "$method" "$PR_URL"
env:
PR_URL: ${{ github.event.pull_request.html_url }}
GH_TOKEN: ${{ steps.app-token.outputs.token }}

disable-auto-merge-on-maintainer-push:
name: Disable auto-merge on maintainer push job
runs-on: ubuntu-latest
# A maintainer push to a bot branch (synchronize, actor is not the bot) disables auto-merge so the
# maintainer's commits do not merge with the bot's. Re-enable manually. The disable call is idempotent.
# Fires when a maintainer pushes to a bot's branch (synchronize, actor != bot). Disables auto-merge so the
# maintainer's commits don't merge with the bot's, and they re-enable it manually. The disable call is idempotent.
if: >-
github.event.action == 'synchronize' &&
github.event.pull_request.head.repo.full_name == github.repository &&
Expand All@@ -116,6 +118,7 @@ jobs:
steps:

- name: Generate GitHub App token step
# App token because a Dependabot PR's GITHUB_TOKEN is read-only regardless of who triggered the event.
id: app-token
uses: actions/create-github-app-token@bcd2ba49218906704ab6c1aa796996da409d3eb1 # v3.2.0
with:
Expand Down
73 changes: 73 additions & 0 deletions AUDIT.md
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
# AUDIT.md

How this repository audits itself against its committed baseline and reports drift. This is the repo-scoped adaptation of the fleet-wide AUDIT.md kept at the fleet hub (carried per the [repo-config downstream carry][repo-config-readme]); the hub's fleet-wide audit remains authoritative. The ground truth here is the committed [`repo-config/`][repo-config] payloads and [`spec/secrets.json`][secrets]; the prose authorities are [`AGENTS.md`][agents], [`CODESTYLE.md`][codestyle], and [`WORKFLOW.md`][workflow].

The audit is read-only: it diffs live state against the committed baseline and reports findings; it never applies changes. The verdict vocabulary is [`WORKFLOW.md`][workflow]'s: **operational / not operational**, **N/A**, **defect**, and the applicable/absent rule.

## Scope

This is a release-model repo: the self-audit covers the `main` and `develop` rulesets, general repository settings, and secret names. Code-project conformance (analyzers, tests, coverage, publish workflows) is CI's job and the fleet hub's fleet-wide audit's, not this self-audit's - see [AGENTS.md "Branching Model"][agents-branching-model] for the model this baseline encodes.

## General Settings

Diff the live repository settings against [`repo-config/settings.json`][repo-config-settings]. The two state-dependent settings are not in the file: `has_discussions` follows visibility (public on / private off) and `default_branch` is `main`.

```sh
repo="$(gh repo view --json nameWithOwner --jq '.nameWithOwner')"
live=$(gh api "repos/$repo" --jq '{has_wiki,has_projects,allow_merge_commit,allow_squash_merge,allow_rebase_merge,allow_auto_merge,allow_update_branch,delete_branch_on_merge}')
diff <(jq -S . repo-config/settings.json) <(jq -S . <<<"$live") \
&& echo "settings: in sync" || echo "settings: DRIFT"
```

## Rulesets

Diff each live ruleset against the committed expected payload with a normalized comparison (sort the order-insensitive `rules[]` and `bypass_actors[]` before diffing so a reordered but equivalent ruleset does not read as drift). This release carry keeps its `develop` payload at [`repo-config/develop.json`][repo-config-develop].

```sh
repo="$(gh repo view --json nameWithOwner --jq '.nameWithOwner')"
norm='{name,target,enforcement,bypass_actors,conditions,rules} | .rules|=sort_by(.type) | .bypass_actors|=sort_by(.actor_id)'
for b in develop main; do
file="repo-config/$b.json"
id=$(gh api "repos/$repo/rulesets" --jq ".[]|select(.name==\"$b\").id")
diff <(jq -S "$norm" "$file") \
<(gh api "repos/$repo/rulesets/$id" --jq '{name,target,enforcement,bypass_actors,conditions,rules}' | jq -S "$norm") \
&& echo "$b: in sync" || echo "$b: DRIFT"
done
Comment thread
ptr727 marked this conversation as resolved.
```

The result must be exactly two rulesets named `develop` and `main` - a missing ruleset or a divergent payload is a **defect**; a duplicate or stray ruleset is a **drift finding**.

## Secrets

Confirm each name [`spec/secrets.json`][secrets] requires exists in the stores its mechanism claims, and no forbidden name is present (names only; values are not readable). The baseline App pair and `NUGET_USERNAME` live in both the Actions and Dependabot stores; `CODECOV_TOKEN` is claimed in the Actions store.

```sh
repo="$(gh repo view --json nameWithOwner --jq '.nameWithOwner')"
for store in actions dependabot; do
names=$(gh api "repos/$repo/$store/secrets" --jq '.secrets[].name')
want="CODEGEN_APP_CLIENT_ID CODEGEN_APP_PRIVATE_KEY NUGET_USERNAME"
[ "$store" = "actions" ] && want="$want CODECOV_TOKEN"
Comment thread
ptr727 marked this conversation as resolved.
for s in $want; do
grep -qx "$s" <<<"$names" && echo "$store/$s: present" || echo "$store/$s: MISSING (defect)"
done
for s in CODEGEN_APP_ID NUGET_API_KEY; do
grep -qx "$s" <<<"$names" && echo "$store/$s: forbidden name present (defect)" || true
done
done
Comment thread
ptr727 marked this conversation as resolved.
```

## Verdict and Follow-Up

A missing required item or a divergent payload is a **defect** (not operational); an equivalent outcome in a non-standard form is a **drift finding**. N/A items are excluded, never counted as failures. Surface findings as repository issues; fixes land as a pull request to `develop` per [AGENTS.md "Branching Model"][agents-branching-model]. To re-apply the whole baseline, run `repo-config/configure.sh` (see [repo-config/README.md][repo-config-readme]).

<!-- Repo -->

[agents]: ./AGENTS.md
[agents-branching-model]: ./AGENTS.md#branching-model
[codestyle]: ./CODESTYLE.md
[repo-config]: ./repo-config/
[repo-config-develop]: ./repo-config/develop.json
[repo-config-readme]: ./repo-config/README.md
[repo-config-settings]: ./repo-config/settings.json
[secrets]: ./spec/secrets.json
[workflow]: ./WORKFLOW.md
29 changes: 16 additions & 13 deletions WORKFLOW.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -610,13 +610,13 @@ determined by NBGV from the checkout state in section 3.*

### 5D. Configuration audit

Run [`repo-config/configure.sh check`](./repo-config/) (section 6). It confirms the listed secrets exist,
the `main`/`develop` rulesets enforce the required merge method + status check + signed commits +
strict-off, and the repository settings (auto-merge, allowed merge methods) are in place, exiting non-zero
on any drift. A missing or incorrect configuration item is a defect (D10). Secret *values* cannot be read
back, so the audit asserts the names exist and a GitHub App is installed. The NuGet.org trusted-publishing
policy (D4.7) lives outside GitHub and cannot be checked by `gh api`; the script flags it as a manual
verification item.
Run the self-audit in [`AUDIT.md`](./AUDIT.md) (section 6). It confirms the listed secret names exist,
the `main`/`develop` rulesets match the committed payloads in [`repo-config/`](./repo-config/) (merge
method + status check + signed commits + strict-off), and the repository settings (auto-merge, allowed
merge methods) are in place. A missing or incorrect configuration item is a defect (D10). Secret *values*
cannot be read back, so the audit asserts the names exist and a GitHub App is installed. The NuGet.org
trusted-publishing policy (D4.7) lives outside GitHub and cannot be checked by `gh api`; it remains a
manual verification item.

### Assessment

Expand All@@ -642,7 +642,9 @@ in its own right, not merely discoverable by failure (D10; audit 5D).
**Secrets.**

- `NUGET_USERNAME` - the NuGet.org profile name passed to `NuGet/login@v1` for OIDC trusted publishing
(D4.7). Actions store. **No `NUGET_API_KEY`** secret is used; publishing is keyless.
(D4.7). Kept in **both** the Actions and Dependabot secret stores, like the App pair, so a publish run
triggered by a Dependabot-actor merge still resolves it (Dependabot-triggered runs get the Dependabot
store, not Actions secrets). **No `NUGET_API_KEY`** secret is used; publishing is keyless.
- `CODEGEN_APP_CLIENT_ID` / `CODEGEN_APP_PRIVATE_KEY` - the GitHub App credentials the merge-bot and
codegen mint the App token from. Required in **both** the Actions and Dependabot secret stores: codegen
and the publisher read them from Actions, but the merge-bot reads them from the Dependabot store when it
Expand DownExpand Up@@ -679,8 +681,9 @@ first successful publish locks it to the repo and owner IDs.
- The GitHub App installed with the scopes above.

**Validation.** This configuration is codified in [`repo-config/`](./repo-config/): the branch rulesets
and repository settings as JSON, applied and audited by an idempotent `gh api` script.
`repo-config/configure.sh check` reads the live rulesets, settings, and secret names and exits non-zero
on any drift; that command **is** the 5D audit. `repo-config/configure.sh apply` configures a fresh repo
to match. Secret values cannot be read back, so the audit asserts the names exist and a GitHub App is
installed rather than checking contents.
and repository settings as JSON. The self-audit in [`AUDIT.md`](./AUDIT.md) diffs the live rulesets,
settings, and secret names against the committed baseline; that self-audit **is** the 5D audit.
`repo-config/configure.sh` applies the rulesets and settings idempotently to a repository; Dependabot
vulnerability alerts and security updates are enabled once at provisioning time, outside the script.
Secret values cannot be read back, so the audit asserts the names exist and a GitHub App is installed
rather than checking contents.
Loading