-
Notifications
You must be signed in to change notification settings - Fork 2
270 lines (255 loc) · 12.5 KB
/
Copy pathbranch-protection-audit.yml
File metadata and controls
270 lines (255 loc) · 12.5 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
# branch-protection-audit — reusable workflow shape (cycle 331.1 Phase E).
#
# Satisfies the `branch-protection-audit` required-status-check context
# in dark-factory's own main1 ruleset (id 16808560) and is published as
# a reusable workflow for consumers (sage3c / cerebe-platform / external
# repos) to invoke from their own CI in Phases G/H.
#
# Phase E posture: dark-factory ships its own ruleset at
# `.github/rulesets/main.json` and would benefit from drift-auditing it
# the same way sage3c audits `tools/branch-protection/spec.yaml`. But
# dark-factory hasn't yet provisioned the GitHub-App token that
# `df audit-branch-protection` needs (`BRANCH_PROTECTION_AUDIT_TOKEN` per
# cycle 331.1 § Reusable workflow shapes). So Phase E's posture is:
# - On dark-factory's own PRs: no-op and pass. Audit runs once the
# `BRANCH_PROTECTION_AUDIT_TOKEN` secret is provisioned on
# dark-factory's GH Actions secrets (Phase F or earlier).
# - On consumer (workflow_call) invocations: fail closed if the
# gate is enabled in config AND the token is missing (matches the
# cycle-doc § "FAIL CLOSED in-job" invariant), pass if disabled.
#
# Real audit wiring (`df audit-branch-protection` already extracted to
# `packages/cli/src/branch-protection/` in Phase C) is gated on the
# secret being provisioned. The cycle doc's full integrity-bake + Doppler
# fallback dance lands in Phase B-PUBLISH.
#
# Dual trigger:
# 1. `pull_request:` — runs on dark-factory's own PRs so the ruleset
# is satisfied here (context = the JOB NAME below).
# 2. `workflow_call:` — consumers `uses:` this from their own
# workflows. Consumer caller-job-id is the first segment of the
# resulting context; consumers MUST name their caller job
# `branch-protection-audit:` for the rule to match.
# 3. `schedule:` — weekly cron at Monday 06:00 UTC catches passive
# drift on dark-factory's own ruleset. No-op until the audit
# token is provisioned.
# 4. `workflow_dispatch:` — manual re-audit during incidents.
#
# Secrets surface (declared at workflow_call level for forward-
# compatibility with the real audit — currently NOT read by the Phase E
# stub):
# - BRANCH_PROTECTION_AUDIT_TOKEN: GitHub App installation token (or
# fine-grained PAT) with `Metadata: Read` + `Administration: Read`
# scopes. Declared `required: false` so the disable-via-config
# escape hatch is reachable per cycle doc § codex iter-final-N+3
# HIGH fix. Failure mode is FAIL CLOSED when the gate is enabled
# in config AND the token is missing.
#
# Phase B-PUBLISH-wf (cycle 331.1): the `cli-version != 'local'` branch
# installs the pinned CLI from npm using `npm install` inside an isolated
# $RUNNER_TEMP dir. See pr-status-check.yml header for the same pattern
# and the deferred-hardening note re: full integrity-baked `npm pack`.
# The audit-branch-protection subcommand wraps a Python script bundled
# in the CLI's `dist/branch-protection/`, so the consumer path still
# requires `actions/setup-python@v6`.
#
# Consumer token naming: BRANCH_PROTECTION_AUDIT_TOKEN is kept as the
# `workflow_call.secrets` name for forward-compat with consumers that
# mint their App installation token however they prefer (e.g. sage3c
# mints via `CI_BOT_APP_ID` + `CI_BOT_PRIVATE_KEY` in the caller
# workflow and passes the resulting installation token in). This keeps
# the reusable contract simple: one pre-minted token in, audit out.
#
# Security: all input values flow through env vars before any run script
# references them, per
# https://github.blog/security/vulnerability-research/how-to-catch-github-actions-workflow-injections-before-attackers-do/
name: Branch Protection Audit
on:
pull_request:
branches: [main]
types: [opened, synchronize, reopened]
merge_group:
schedule:
- cron: '0 6 * * 1'
workflow_dispatch:
workflow_call:
inputs:
cli-version:
description: |
CLI version to install. 'local' (default) builds from the
checked-out workspace. Consumers pass an explicit semver tag
(e.g. '0.1.0-alpha.5') — see pr-status-check.yml for the same
contract.
type: string
required: false
default: 'local'
gate-enabled:
description: |
When 'false', the audit runs as a documented no-op (matches
the cycle-doc `gates.branch_protection_audit.enabled: false`
escape hatch). When 'true' AND the token is missing, the
workflow FAILS CLOSED.
type: string
required: false
default: 'true'
secrets:
BRANCH_PROTECTION_AUDIT_TOKEN:
description: |
GitHub App installation token (or fine-grained PAT) with
`Metadata: Read` + `Administration: Read` scopes. Required
when `gate-enabled: 'true'` (the default). The default-Actions
`GITHUB_TOKEN` does NOT have Administration permissions and
cannot substitute (cycle 331.1 § codex iter-final-N+2 HIGH).
required: false
MOMENTIQ_NPM_READ_TOKEN:
description: |
Deprecated — no longer used (the `@momentiq/dark-factory-cli`
package is published to the public npm registry; no auth is
required to install it). Preserved here as `required: false`
so callers still passing it via `secrets:` don't error. Will
be removed in a future major version of this reusable workflow.
required: false
permissions:
contents: read
pull-requests: read
concurrency:
group: branch-protection-audit-${{ github.event.pull_request.number || github.event.merge_group.head_sha || github.ref }}
cancel-in-progress: true
jobs:
branch-protection-audit:
# IMPORTANT: this job name is the status-check context surfaced to
# the ruleset. Must match `required_status_checks.context:
# branch-protection-audit` exactly.
name: branch-protection-audit
runs-on: ubuntu-latest
timeout-minutes: 5
env:
REQUESTED_VERSION: ${{ inputs.cli-version || 'local' }}
GATE_ENABLED: ${{ inputs.gate-enabled || 'true' }}
EVENT_NAME: ${{ github.event_name }}
REPO_FULL_NAME: ${{ github.repository }}
AUDIT_TOKEN: ${{ secrets.BRANCH_PROTECTION_AUDIT_TOKEN }}
steps:
- name: merge_group fast-path
if: github.event_name == 'merge_group'
run: |
{
echo "## Branch Protection Audit — merge_group"
echo ""
echo "Audit runs on the originating PR (or the weekly cron); merge_group runs reuse that verdict."
} >> "$GITHUB_STEP_SUMMARY"
- uses: actions/checkout@v6
if: env.EVENT_NAME != 'merge_group'
- name: Setup Node 20
if: env.EVENT_NAME != 'merge_group'
uses: actions/setup-node@v6
with:
node-version: '20'
- name: Install workspace (lockfile-strict)
if: env.EVENT_NAME != 'merge_group' && env.REQUESTED_VERSION == 'local'
run: npm ci
- name: Build workspace
if: env.EVENT_NAME != 'merge_group' && env.REQUESTED_VERSION == 'local'
run: npm run build
- name: Install @momentiq/dark-factory-cli (consumer path)
# Consumer install path. The CLI is published to the public npm
# registry — no auth is needed — so `npm install` resolves
# against the default registry without a custom `.npmrc`. The
# install runs in an isolated `$RUNNER_TEMP` dir so it can't be
# influenced by the consumer's lockfile, and `--ignore-scripts`
# blocks arbitrary lifecycle scripts (sqlite3's prebuilt is
# re-enabled via the whitelisted `npm rebuild` below).
if: env.EVENT_NAME != 'merge_group' && env.REQUESTED_VERSION != 'local'
run: |
set -euo pipefail
INSTALL_DIR="$RUNNER_TEMP/df-cli"
mkdir -p "$INSTALL_DIR"
cd "$INSTALL_DIR"
cat > package.json <<'EOF'
{"name": "df-cli-runtime", "version": "0.0.0", "private": true}
EOF
npm install --no-audit --no-fund --ignore-scripts \
"@momentiq/dark-factory-cli@${REQUESTED_VERSION}"
# dark-factory#11 + sage3c#2198 — rebuild sqlite3 prebuilt only.
# @cursor/sdk pulls in sqlite3 which needs its native binding.
# `npm rebuild sqlite3` runs ONLY sqlite3's lifecycle (fetches
# the prebuilt via node-pre-gyp); the rest of the tree stays
# sealed under --ignore-scripts. See agent-critic.yml rationale.
npm rebuild sqlite3
DF_CLI="$INSTALL_DIR/node_modules/.bin/df"
if [ ! -x "$DF_CLI" ]; then
echo "::error::Installed @momentiq/dark-factory-cli@$REQUESTED_VERSION but binary not found at $DF_CLI." >&2
exit 1
fi
echo "DF_CLI=$DF_CLI" >> "$GITHUB_ENV"
- name: Setup Python 3.11 (audit backend, consumer path)
# `df audit-branch-protection` shells out to a Python script
# bundled in the CLI under `dist/branch-protection/`
# (Phase C extraction from sage3c). Until pure-TS port lands,
# the consumer path needs python3 on PATH.
if: env.EVENT_NAME != 'merge_group' && env.REQUESTED_VERSION != 'local'
uses: actions/setup-python@v6
with:
python-version: '3.11'
- name: Validate audit credential against gate config
# Implements the cycle-doc § "Failure mode is FAIL CLOSED in-job"
# invariant. When the audit gate is enabled (default) and the
# token is missing, exit non-zero with the structured error.
# When the gate is explicitly disabled via input, log and pass.
# Path-independent: same shell logic for local and consumer paths.
if: env.EVENT_NAME != 'merge_group'
run: |
set -euo pipefail
if [[ "$GATE_ENABLED" == "false" ]]; then
echo "branch-protection-audit: gate explicitly disabled via inputs.gate-enabled='false' — audit-disabled-by-config."
exit 0
fi
if [[ -z "$AUDIT_TOKEN" ]]; then
if [[ "$REPO_FULL_NAME" == "momentiq-ai/dark-factory" ]]; then
echo "::warning::BRANCH_PROTECTION_AUDIT_TOKEN is not provisioned on dark-factory yet — Phase E posture is to log-and-pass on dark-factory's own runs."
echo " Provision the App token to enable the real audit; see cycle 331.1 § Reusable workflow shapes."
exit 0
fi
echo "::error::[required_secret_missing] BRANCH_PROTECTION_AUDIT_TOKEN" >&2
echo "::error::Audit gate is enabled (inputs.gate-enabled='true', default) but no token was provided." >&2
echo "::error::Provision a GitHub App installation token (or fine-grained PAT) with 'Metadata: Read' + 'Administration: Read' scopes." >&2
echo "::error::OR explicitly disable the gate by passing inputs.gate-enabled='false' from your caller workflow." >&2
exit 1
fi
- name: Run audit (local path, token present)
# Real audit invokes the Phase C-extracted service. The Python
# backend reads $GH_TOKEN — set it from the workflow_call secret.
# Skipped today on dark-factory because the token isn't
# provisioned and the validate-step above logs-and-passes; will
# run automatically once the token lands on dark-factory's GH
# Actions secrets.
if: env.EVENT_NAME != 'merge_group' && env.REQUESTED_VERSION == 'local' && env.GATE_ENABLED == 'true' && env.AUDIT_TOKEN != ''
env:
GH_TOKEN: ${{ secrets.BRANCH_PROTECTION_AUDIT_TOKEN }}
run: |
set -euo pipefail
node ./packages/cli/dist/cli.js audit-branch-protection
- name: Run audit (consumer path, token present)
# Consumer path: invoke the installed CLI binary. Same env
# contract as the local-path audit step.
if: env.EVENT_NAME != 'merge_group' && env.REQUESTED_VERSION != 'local' && env.GATE_ENABLED == 'true' && env.AUDIT_TOKEN != ''
env:
GH_TOKEN: ${{ secrets.BRANCH_PROTECTION_AUDIT_TOKEN }}
run: |
set -euo pipefail
"$DF_CLI" audit-branch-protection
- name: Summary
if: always()
env:
JOB_STATUS: ${{ job.status }}
run: |
set -euo pipefail
{
echo "## branch-protection-audit Summary"
echo ""
if [[ "$JOB_STATUS" == "success" ]]; then
echo "branch-protection-audit passed."
else
echo "branch-protection-audit failed — see the audit output above (or [required_secret_missing] hint)."
fi
} >> "$GITHUB_STEP_SUMMARY"