redlinegate 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +188 -0
- package/commands/redline-init.md +54 -0
- package/commands/redline-verify.md +69 -0
- package/dist/bin/redline.js +605 -0
- package/dist/bin/redline.js.map +1 -0
- package/dist/commands/exempt.js +45 -0
- package/dist/commands/exempt.js.map +1 -0
- package/dist/commands/init.js +615 -0
- package/dist/commands/init.js.map +1 -0
- package/dist/commands/policy.js +25 -0
- package/dist/commands/policy.js.map +1 -0
- package/dist/commands/remove.js +427 -0
- package/dist/commands/remove.js.map +1 -0
- package/dist/commands/review.js +72 -0
- package/dist/commands/review.js.map +1 -0
- package/dist/commands/sync.js +42 -0
- package/dist/commands/sync.js.map +1 -0
- package/dist/commands/verify.js +478 -0
- package/dist/commands/verify.js.map +1 -0
- package/dist/config/redline-json.js +187 -0
- package/dist/config/redline-json.js.map +1 -0
- package/dist/core/errors.js +25 -0
- package/dist/core/errors.js.map +1 -0
- package/dist/core/git.js +152 -0
- package/dist/core/git.js.map +1 -0
- package/dist/core/log.js +22 -0
- package/dist/core/log.js.map +1 -0
- package/dist/core/severity.js +16 -0
- package/dist/core/severity.js.map +1 -0
- package/dist/core/version.js +15 -0
- package/dist/core/version.js.map +1 -0
- package/dist/detect/scan.js +79 -0
- package/dist/detect/scan.js.map +1 -0
- package/dist/detect/stack.js +71 -0
- package/dist/detect/stack.js.map +1 -0
- package/dist/enforce/ladder.js +148 -0
- package/dist/enforce/ladder.js.map +1 -0
- package/dist/exempt/parse.js +86 -0
- package/dist/exempt/parse.js.map +1 -0
- package/dist/metrics/options.js +257 -0
- package/dist/metrics/options.js.map +1 -0
- package/dist/metrics/run.js +72 -0
- package/dist/metrics/run.js.map +1 -0
- package/dist/platforms/azure/client.js +53 -0
- package/dist/platforms/azure/client.js.map +1 -0
- package/dist/platforms/azure/index.js +69 -0
- package/dist/platforms/azure/index.js.map +1 -0
- package/dist/platforms/azure/install.js +917 -0
- package/dist/platforms/azure/install.js.map +1 -0
- package/dist/platforms/azure/policy-types.js +70 -0
- package/dist/platforms/azure/policy-types.js.map +1 -0
- package/dist/platforms/azure/verify.js +309 -0
- package/dist/platforms/azure/verify.js.map +1 -0
- package/dist/platforms/detect.js +40 -0
- package/dist/platforms/detect.js.map +1 -0
- package/dist/platforms/github/client.js +49 -0
- package/dist/platforms/github/client.js.map +1 -0
- package/dist/platforms/github/index.js +54 -0
- package/dist/platforms/github/index.js.map +1 -0
- package/dist/platforms/github/install.js +558 -0
- package/dist/platforms/github/install.js.map +1 -0
- package/dist/platforms/github/push.js +102 -0
- package/dist/platforms/github/push.js.map +1 -0
- package/dist/platforms/github/remote.js +36 -0
- package/dist/platforms/github/remote.js.map +1 -0
- package/dist/platforms/github/verify.js +366 -0
- package/dist/platforms/github/verify.js.map +1 -0
- package/dist/platforms/http.js +58 -0
- package/dist/platforms/http.js.map +1 -0
- package/dist/platforms/pull-request-templates.js +157 -0
- package/dist/platforms/pull-request-templates.js.map +1 -0
- package/dist/platforms/remote.js +21 -0
- package/dist/platforms/remote.js.map +1 -0
- package/dist/platforms/resolve.js +69 -0
- package/dist/platforms/resolve.js.map +1 -0
- package/dist/platforms/shape.js +19 -0
- package/dist/platforms/shape.js.map +1 -0
- package/dist/platforms/types.js +24 -0
- package/dist/platforms/types.js.map +1 -0
- package/dist/policy/checks.js +91 -0
- package/dist/policy/checks.js.map +1 -0
- package/dist/policy/diff.js +50 -0
- package/dist/policy/diff.js.map +1 -0
- package/dist/registry/discover.js +74 -0
- package/dist/registry/discover.js.map +1 -0
- package/dist/registry/serialize.js +41 -0
- package/dist/registry/serialize.js.map +1 -0
- package/dist/registry/types.js +2 -0
- package/dist/registry/types.js.map +1 -0
- package/dist/remove/host.js +294 -0
- package/dist/remove/host.js.map +1 -0
- package/dist/render/commands.js +139 -0
- package/dist/render/commands.js.map +1 -0
- package/dist/render/manifest.js +82 -0
- package/dist/render/manifest.js.map +1 -0
- package/dist/render/markers.js +166 -0
- package/dist/render/markers.js.map +1 -0
- package/dist/render/profile.js +23 -0
- package/dist/render/profile.js.map +1 -0
- package/dist/render/standards.js +169 -0
- package/dist/render/standards.js.map +1 -0
- package/dist/render/vendors.js +199 -0
- package/dist/render/vendors.js.map +1 -0
- package/dist/review/engines/api.js +95 -0
- package/dist/review/engines/api.js.map +1 -0
- package/dist/review/engines/embedded.js +20 -0
- package/dist/review/engines/embedded.js.map +1 -0
- package/dist/review/engines/types.js +2 -0
- package/dist/review/engines/types.js.map +1 -0
- package/dist/review/glob.js +29 -0
- package/dist/review/glob.js.map +1 -0
- package/dist/review/prompt.js +45 -0
- package/dist/review/prompt.js.map +1 -0
- package/dist/review/rules.js +25 -0
- package/dist/review/rules.js.map +1 -0
- package/dist/review/schema.js +84 -0
- package/dist/review/schema.js.map +1 -0
- package/dist/review/scope.js +27 -0
- package/dist/review/scope.js.map +1 -0
- package/dist/sarif/map.js +58 -0
- package/dist/sarif/map.js.map +1 -0
- package/dist/sarif/parse.js +88 -0
- package/dist/sarif/parse.js.map +1 -0
- package/dist/sarif/types.js +2 -0
- package/dist/sarif/types.js.map +1 -0
- package/dist/sync/host.js +21 -0
- package/dist/sync/host.js.map +1 -0
- package/dist/sync/plan.js +47 -0
- package/dist/sync/plan.js.map +1 -0
- package/dist/sync/render.js +57 -0
- package/dist/sync/render.js.map +1 -0
- package/dist/sync/run.js +118 -0
- package/dist/sync/run.js.map +1 -0
- package/dist/verify/host.js +52 -0
- package/dist/verify/host.js.map +1 -0
- package/dist/verify/remote.js +146 -0
- package/dist/verify/remote.js.map +1 -0
- package/package.json +52 -0
- package/platforms/azure/gate-template.yml +105 -0
- package/rulesets/redline-org-ruleset.json +46 -0
- package/rulesets/redline-ruleset.json +49 -0
- package/scripts/assign-rule-ids.mjs +129 -0
- package/scripts/build-baseline.mjs +124 -0
- package/scripts/build-correlation.mjs +74 -0
- package/scripts/build-dashboard.mjs +584 -0
- package/scripts/build-digest.mjs +164 -0
- package/scripts/build-inbox.mjs +133 -0
- package/scripts/build-registry.mjs +42 -0
- package/scripts/build-roi.mjs +181 -0
- package/scripts/check-pins.mjs +88 -0
- package/scripts/collect-telemetry.mjs +300 -0
- package/scripts/lib/__tests__/baseline.test.mjs +132 -0
- package/scripts/lib/__tests__/correlate.test.mjs +131 -0
- package/scripts/lib/__tests__/dora.test.mjs +110 -0
- package/scripts/lib/__tests__/exemptions.test.mjs +85 -0
- package/scripts/lib/__tests__/metrics-scanner.test.mjs +65 -0
- package/scripts/lib/__tests__/sarif.test.mjs +125 -0
- package/scripts/lib/__tests__/spend.test.mjs +64 -0
- package/scripts/lib/baseline.mjs +156 -0
- package/scripts/lib/correlate.mjs +125 -0
- package/scripts/lib/dora.mjs +133 -0
- package/scripts/lib/exemptions.mjs +79 -0
- package/scripts/lib/metrics.mjs +172 -0
- package/scripts/lib/rules.mjs +107 -0
- package/scripts/lib/sarif.mjs +108 -0
- package/scripts/lib/spend.mjs +69 -0
- package/scripts/measure-context.mjs +101 -0
- package/scripts/render-self.mjs +18 -0
- package/scripts/score-seeds.mjs +294 -0
- package/scripts/validate.mjs +394 -0
- package/standards/core.md +124 -0
- package/standards/manifest.json +214 -0
- package/standards/stacks/csharp.md +29 -0
- package/standards/stacks/go.md +30 -0
- package/standards/stacks/java.md +30 -0
- package/standards/stacks/javascript.md +42 -0
- package/standards/stacks/kotlin.md +27 -0
- package/standards/stacks/microservices.md +37 -0
- package/standards/stacks/nodejs.md +34 -0
- package/standards/stacks/python.md +29 -0
- package/standards/stacks/react-native.md +29 -0
- package/standards/stacks/react.md +46 -0
- package/standards/stacks/swift.md +26 -0
- package/standards/stacks/terraform.md +27 -0
- package/templates/CODEOWNERS +29 -0
- package/templates/azure/pull_request_template.md +72 -0
- package/templates/github/pull_request_template.md +72 -0
- package/templates/redline.yml +35 -0
- package/templates/repo-context.md +64 -0
- package/workflows/dashboard.yml +119 -0
- package/workflows/inbox.yml +97 -0
- package/workflows/redline-collect.yml +61 -0
- package/workflows/redline-gate.yml +301 -0
- package/workflows/redline-sync.yml +79 -0
- package/workflows/seed-canary.yml +231 -0
- package/workflows/verify-onboarding.yml +115 -0
- package/workflows/weekly-digest.yml +78 -0
|
@@ -0,0 +1,301 @@
|
|
|
1
|
+
# Reusable readiness gate. Host in the org `.github` repo at
|
|
2
|
+
# .github/workflows/redline-gate.yml; repos call it from .github/workflows/redline.yml
|
|
3
|
+
# (see templates/redline.yml).
|
|
4
|
+
#
|
|
5
|
+
# IMPORTANT — required-status-check naming. A reusable workflow reports its check runs
|
|
6
|
+
# as "<caller job id> / <called job id>". templates/redline.yml names the caller job
|
|
7
|
+
# `redline-gate`, and the aggregate job below is `gate`, so the context a branch ruleset
|
|
8
|
+
# must require is exactly:
|
|
9
|
+
#
|
|
10
|
+
# redline-gate / gate
|
|
11
|
+
#
|
|
12
|
+
# Changing either job id changes the required context and will silently block every PR
|
|
13
|
+
# in every onboarded repo. `npx redlinegate verify` checks the
|
|
14
|
+
# real name after onboarding.
|
|
15
|
+
name: Redline Gate
|
|
16
|
+
|
|
17
|
+
on:
|
|
18
|
+
workflow_call:
|
|
19
|
+
inputs:
|
|
20
|
+
adr-diff-threshold:
|
|
21
|
+
description: Changed lines above which an ADR link (or the no-adr label) is required
|
|
22
|
+
type: number
|
|
23
|
+
default: 300
|
|
24
|
+
fail-on-dependency-severity:
|
|
25
|
+
description: Minimum dependency-review severity that fails the gate
|
|
26
|
+
type: string
|
|
27
|
+
default: high
|
|
28
|
+
soft-fail-labels:
|
|
29
|
+
description: >-
|
|
30
|
+
Comma-separated labels that downgrade the process checks (checklist, ADR) to
|
|
31
|
+
warnings. Security checks are never downgraded.
|
|
32
|
+
type: string
|
|
33
|
+
default: redline-exempt,redline-sync
|
|
34
|
+
rung:
|
|
35
|
+
description: >-
|
|
36
|
+
The enforcement rung this repository sits at: observe, warn, block-blocker or
|
|
37
|
+
block-high. `observe` and `warn` report and never block; the block-* rungs stop a
|
|
38
|
+
merge on a finding at or above their severity. Written by `redline init --rung`,
|
|
39
|
+
which refuses a promotion the repository's recorded evidence does not support.
|
|
40
|
+
type: string
|
|
41
|
+
default: observe
|
|
42
|
+
exemption-enforcement:
|
|
43
|
+
description: >-
|
|
44
|
+
What a soft-fail label without a valid "## Redline exemption" block does.
|
|
45
|
+
`warn` accepts it and says what is missing; `require` refuses it. Repos move
|
|
46
|
+
from warn to require one standards version after the block was introduced, so
|
|
47
|
+
nobody's open pull request is failed by a rule that did not exist when they
|
|
48
|
+
opened it.
|
|
49
|
+
type: string
|
|
50
|
+
default: warn
|
|
51
|
+
|
|
52
|
+
permissions:
|
|
53
|
+
contents: read
|
|
54
|
+
pull-requests: write
|
|
55
|
+
|
|
56
|
+
jobs:
|
|
57
|
+
checklist:
|
|
58
|
+
name: PR checklist
|
|
59
|
+
runs-on: ubuntu-latest
|
|
60
|
+
steps:
|
|
61
|
+
- name: Verify the launch-readiness checklist is complete
|
|
62
|
+
env:
|
|
63
|
+
GH_TOKEN: ${{ github.token }}
|
|
64
|
+
PR: ${{ github.event.pull_request.number }}
|
|
65
|
+
REPO: ${{ github.repository }}
|
|
66
|
+
run: |
|
|
67
|
+
set -euo pipefail
|
|
68
|
+
body=$(gh pr view "$PR" --repo "$REPO" --json body --jq '.body // ""')
|
|
69
|
+
|
|
70
|
+
# Only the Launch readiness section is enforced. "Change type" is a pick-one
|
|
71
|
+
# list, so requiring every box there would make the gate unpassable.
|
|
72
|
+
section=$(printf '%s\n' "$body" | awk '
|
|
73
|
+
/^##[[:space:]]+Launch readiness/ { inside = 1; next }
|
|
74
|
+
/^##[[:space:]]/ { inside = 0 }
|
|
75
|
+
inside { print }
|
|
76
|
+
')
|
|
77
|
+
|
|
78
|
+
if [[ -z "${section//[[:space:]]/}" ]]; then
|
|
79
|
+
echo "::error::No '## Launch readiness' section in the PR description. Restore .github/pull_request_template.md and complete the checklist."
|
|
80
|
+
exit 1
|
|
81
|
+
fi
|
|
82
|
+
|
|
83
|
+
unchecked=$(printf '%s\n' "$section" | grep -c '^[[:space:]]*- \[ \]' || true)
|
|
84
|
+
if [[ "$unchecked" -gt 0 ]]; then
|
|
85
|
+
printf '%s\n' "$section" | grep '^[[:space:]]*- \[ \]' || true
|
|
86
|
+
echo "::error::$unchecked unchecked launch-readiness item(s). Tick each one, or delete the line and say why in the PR description."
|
|
87
|
+
exit 1
|
|
88
|
+
fi
|
|
89
|
+
echo "Launch readiness complete."
|
|
90
|
+
|
|
91
|
+
adr:
|
|
92
|
+
name: ADR required for significant changes
|
|
93
|
+
runs-on: ubuntu-latest
|
|
94
|
+
steps:
|
|
95
|
+
- name: Check diff size against ADR link
|
|
96
|
+
env:
|
|
97
|
+
GH_TOKEN: ${{ github.token }}
|
|
98
|
+
PR: ${{ github.event.pull_request.number }}
|
|
99
|
+
REPO: ${{ github.repository }}
|
|
100
|
+
THRESHOLD: ${{ inputs.adr-diff-threshold }}
|
|
101
|
+
run: |
|
|
102
|
+
set -euo pipefail
|
|
103
|
+
pr=$(gh pr view "$PR" --repo "$REPO" --json additions,deletions,body,labels)
|
|
104
|
+
changed=$(jq '.additions + .deletions' <<<"$pr")
|
|
105
|
+
has_adr=$(jq -r '.body // ""' <<<"$pr" | grep -c 'docs/adr/' || true)
|
|
106
|
+
exempt=$(jq -r '.labels[].name' <<<"$pr" | grep -cx 'no-adr' || true)
|
|
107
|
+
echo "changed=$changed threshold=$THRESHOLD adr-links=$has_adr no-adr-label=$exempt"
|
|
108
|
+
if [[ "$changed" -gt "$THRESHOLD" && "$has_adr" -eq 0 && "$exempt" -eq 0 ]]; then
|
|
109
|
+
echo "::error::${changed} changed lines with no ADR link. Add docs/adr/NNNN-*.md and link it in the PR body, or apply the 'no-adr' label with a justification comment."
|
|
110
|
+
exit 1
|
|
111
|
+
fi
|
|
112
|
+
|
|
113
|
+
policy:
|
|
114
|
+
# The deterministic tier: the rules a checker can decide, evaluated with no
|
|
115
|
+
# model call. They cost nothing to run and they cannot hallucinate, which is
|
|
116
|
+
# the point — an author cannot argue with a model about whether the word TODO
|
|
117
|
+
# appears on a line.
|
|
118
|
+
#
|
|
119
|
+
# Soft-fail, like the other process checks. The never-exemptible set is
|
|
120
|
+
# exactly dependency-review and secrets and this does not join it: these are
|
|
121
|
+
# code-standard findings, not a security boundary.
|
|
122
|
+
name: Deterministic policy
|
|
123
|
+
runs-on: ubuntu-latest
|
|
124
|
+
steps:
|
|
125
|
+
- uses: actions/checkout@v4
|
|
126
|
+
with:
|
|
127
|
+
fetch-depth: 0
|
|
128
|
+
- uses: actions/setup-node@v4
|
|
129
|
+
with:
|
|
130
|
+
node-version: 22
|
|
131
|
+
- name: Evaluate the deterministic rules
|
|
132
|
+
env:
|
|
133
|
+
BASE: ${{ github.event.pull_request.base.sha }}
|
|
134
|
+
HEAD: ${{ github.event.pull_request.head.sha }}
|
|
135
|
+
RUNG: ${{ inputs.rung }}
|
|
136
|
+
REDLINE_CLI_VERSION: '0.0.1'
|
|
137
|
+
run: |
|
|
138
|
+
set -euo pipefail
|
|
139
|
+
# Three dots: the diff against the merge base, not against the tip of
|
|
140
|
+
# the base branch. Two dots would attribute every commit that landed on
|
|
141
|
+
# main since this branch started to this pull request's author.
|
|
142
|
+
git diff "${BASE}...${HEAD}" > /tmp/redline.diff
|
|
143
|
+
# The rung decides the floor. Without passing it, block-high behaved
|
|
144
|
+
# exactly like block-blocker — the strictest rung on the ladder did
|
|
145
|
+
# nothing the one below it did not already do.
|
|
146
|
+
case "$RUNG" in
|
|
147
|
+
block-high) fail_on=HIGH ;;
|
|
148
|
+
*) fail_on=BLOCKER ;;
|
|
149
|
+
esac
|
|
150
|
+
npx --yes "redlinegate@${REDLINE_CLI_VERSION}" policy \
|
|
151
|
+
--diff-file /tmp/redline.diff --fail-on "$fail_on"
|
|
152
|
+
|
|
153
|
+
dependency-review:
|
|
154
|
+
name: Dependency review
|
|
155
|
+
runs-on: ubuntu-latest
|
|
156
|
+
steps:
|
|
157
|
+
- uses: actions/checkout@v4
|
|
158
|
+
- uses: actions/dependency-review-action@v4
|
|
159
|
+
with:
|
|
160
|
+
fail-on-severity: ${{ inputs.fail-on-dependency-severity }}
|
|
161
|
+
comment-summary-in-pr: on-failure
|
|
162
|
+
|
|
163
|
+
secrets:
|
|
164
|
+
name: Secret scan (diff)
|
|
165
|
+
runs-on: ubuntu-latest
|
|
166
|
+
steps:
|
|
167
|
+
- uses: actions/checkout@v4
|
|
168
|
+
with:
|
|
169
|
+
fetch-depth: 0
|
|
170
|
+
- name: Scan the PR range
|
|
171
|
+
# Pinned to the commit behind tag v3.97.1 (2026-08-24). This action runs with
|
|
172
|
+
# access to the PR diff, so the ref must be immutable — a tag can be moved.
|
|
173
|
+
# GitHub Actions does not allow expressions in a step `uses:`, so the pin is
|
|
174
|
+
# reviewed here, in the org `.github` repo, which is the right place for it.
|
|
175
|
+
# scripts/check-pins.mjs re-resolves it against the upstream tag in CI.
|
|
176
|
+
uses: trufflesecurity/trufflehog@20652fbbdefffcdaa493a5bf57ab2ac6b1db715b # v3.97.1
|
|
177
|
+
with:
|
|
178
|
+
base: ${{ github.event.pull_request.base.sha }}
|
|
179
|
+
head: ${{ github.event.pull_request.head.sha }}
|
|
180
|
+
# verified only: `unknown` produces false positives, and this is a blocking
|
|
181
|
+
# check. Unverified candidates are caught by GHAS secret scanning instead.
|
|
182
|
+
extra_args: --results=verified --fail
|
|
183
|
+
|
|
184
|
+
gate:
|
|
185
|
+
# Aggregate. The branch ruleset requires the check "redline-gate / gate".
|
|
186
|
+
name: gate
|
|
187
|
+
needs: [checklist, adr, policy, dependency-review, secrets]
|
|
188
|
+
if: always()
|
|
189
|
+
runs-on: ubuntu-latest
|
|
190
|
+
steps:
|
|
191
|
+
- name: Aggregate results
|
|
192
|
+
env:
|
|
193
|
+
RESULTS: ${{ toJSON(needs) }}
|
|
194
|
+
LABELS: ${{ toJSON(github.event.pull_request.labels.*.name) }}
|
|
195
|
+
SOFT_FAIL_LABELS: ${{ inputs.soft-fail-labels }}
|
|
196
|
+
EXEMPTION_ENFORCEMENT: ${{ inputs.exemption-enforcement }}
|
|
197
|
+
RUNG: ${{ inputs.rung }}
|
|
198
|
+
# Through the environment, never interpolated into the script body:
|
|
199
|
+
# `${{ github.event.pull_request.body }}` inline is a script injection
|
|
200
|
+
# anyone who can open a pull request can use.
|
|
201
|
+
PR_BODY: ${{ github.event.pull_request.body }}
|
|
202
|
+
# Pinned, not @latest: the gate's behaviour must not change under a
|
|
203
|
+
# repository because a release happened overnight.
|
|
204
|
+
REDLINE_CLI_VERSION: '0.0.1'
|
|
205
|
+
run: |
|
|
206
|
+
set -euo pipefail
|
|
207
|
+
|
|
208
|
+
# Security checks can never be downgraded by a label.
|
|
209
|
+
HARD_JOBS='["dependency-review","secrets"]'
|
|
210
|
+
|
|
211
|
+
hard_failed=$(jq -r --argjson hard "$HARD_JOBS" \
|
|
212
|
+
'to_entries[] | select(.value.result != "success") | select(.key as $k | $hard | index($k)) | .key' <<<"$RESULTS")
|
|
213
|
+
soft_failed=$(jq -r --argjson hard "$HARD_JOBS" \
|
|
214
|
+
'to_entries[] | select(.value.result != "success") | select(.key as $k | $hard | index($k) | not) | .key' <<<"$RESULTS")
|
|
215
|
+
|
|
216
|
+
labelled=false
|
|
217
|
+
for label in ${SOFT_FAIL_LABELS//,/ }; do
|
|
218
|
+
if jq -e --arg l "$label" 'index($l)' <<<"$LABELS" >/dev/null; then
|
|
219
|
+
labelled=true
|
|
220
|
+
echo "Soft-fail label present: $label"
|
|
221
|
+
fi
|
|
222
|
+
done
|
|
223
|
+
|
|
224
|
+
# A label alone records nothing: not who accepted the failing check, not
|
|
225
|
+
# why, not until when. The pull request must also carry a
|
|
226
|
+
# "## Redline exemption" block with a reason and an expiry, and the CLI
|
|
227
|
+
# is what reads it — re-implementing that parse here would eventually
|
|
228
|
+
# disagree with the audit that reads the same block later.
|
|
229
|
+
#
|
|
230
|
+
# The body is written to a file rather than passed as an argument: a
|
|
231
|
+
# pull request body is attacker-controlled text full of backticks and
|
|
232
|
+
# $(...), and interpolating it into a command is how one becomes a
|
|
233
|
+
# command.
|
|
234
|
+
exempt=false
|
|
235
|
+
if [[ "$labelled" == true ]]; then
|
|
236
|
+
printf '%s' "$PR_BODY" > /tmp/redline-pr-body.md
|
|
237
|
+
# Every failing soft check must be covered, one at a time. Calling
|
|
238
|
+
# exempt without --scope never evaluated the scope at all, so an
|
|
239
|
+
# exemption written for `checklist` silently waived `adr` and the
|
|
240
|
+
# deterministic `policy` job too — the field was recorded and ignored.
|
|
241
|
+
exempt=true
|
|
242
|
+
for check in $soft_failed; do
|
|
243
|
+
if ! npx --yes "redlinegate@${REDLINE_CLI_VERSION}" exempt \
|
|
244
|
+
--body-file /tmp/redline-pr-body.md --scope "$check"; then
|
|
245
|
+
exempt=false
|
|
246
|
+
break
|
|
247
|
+
fi
|
|
248
|
+
done
|
|
249
|
+
if [[ "$exempt" == true ]]; then
|
|
250
|
+
:
|
|
251
|
+
elif [[ "$EXEMPTION_ENFORCEMENT" != "require" ]]; then
|
|
252
|
+
echo "::warning::This exemption has no valid \`## Redline exemption\` block. It is being accepted this time; once this repository moves to require, it will not be."
|
|
253
|
+
exempt=true
|
|
254
|
+
else
|
|
255
|
+
echo "::error::A soft-fail label was applied without a valid \`## Redline exemption\` block (reason and expiry required)."
|
|
256
|
+
fi
|
|
257
|
+
fi
|
|
258
|
+
|
|
259
|
+
{
|
|
260
|
+
echo "### Redline gate"
|
|
261
|
+
echo
|
|
262
|
+
jq -r 'to_entries[] | "- \(.key): \(.value.result)"' <<<"$RESULTS"
|
|
263
|
+
} >> "$GITHUB_STEP_SUMMARY"
|
|
264
|
+
|
|
265
|
+
# The security floor is not on the ladder. Dependency review and the
|
|
266
|
+
# secret scan block at every rung including observe: the ladder governs
|
|
267
|
+
# how strictly a repository's own standards are enforced, not whether
|
|
268
|
+
# the organisation's security minimum applies to it.
|
|
269
|
+
if [[ -n "$hard_failed" ]]; then
|
|
270
|
+
echo "::error::Redline gate failed on security checks (not label-exemptible, and not on the enforcement ladder): $hard_failed"
|
|
271
|
+
exit 1
|
|
272
|
+
fi
|
|
273
|
+
|
|
274
|
+
# Below block-blocker the process checks report and do not block. The
|
|
275
|
+
# findings are still produced, still commented, and still collected —
|
|
276
|
+
# observe is not "off", it is "measured and not yet enforced", which is
|
|
277
|
+
# the whole point of having a rung below blocking at all.
|
|
278
|
+
# A rung this gate does not recognise is NOT enforcing. cli/config
|
|
279
|
+
# reads an unrecognised value back as `observe` for the same reason: a
|
|
280
|
+
# typo must never be able to make a repository stricter than anyone
|
|
281
|
+
# chose, and the two halves disagreeing meant a typo blocked every pull
|
|
282
|
+
# request here while the CLI reported the repository as observing.
|
|
283
|
+
case "$RUNG" in
|
|
284
|
+
block-blocker|block-high) enforcing=true ;;
|
|
285
|
+
*) enforcing=false ;;
|
|
286
|
+
esac
|
|
287
|
+
|
|
288
|
+
if [[ -n "$soft_failed" ]]; then
|
|
289
|
+
if [[ "$enforcing" != true ]]; then
|
|
290
|
+
echo "::warning::Process checks failed: $soft_failed. This repository is at rung '$RUNG', which reports and does not block. The findings are recorded either way."
|
|
291
|
+
echo "- **not enforced (rung: $RUNG):** $soft_failed" >> "$GITHUB_STEP_SUMMARY"
|
|
292
|
+
elif [[ "$exempt" == true ]]; then
|
|
293
|
+
echo "::warning::Process checks failed but a valid exemption is recorded: $soft_failed. A reviewer is accepting this deliberately, with a reason and an expiry."
|
|
294
|
+
echo "- **exempted:** $soft_failed" >> "$GITHUB_STEP_SUMMARY"
|
|
295
|
+
else
|
|
296
|
+
echo "::error::Redline gate failed: $soft_failed"
|
|
297
|
+
exit 1
|
|
298
|
+
fi
|
|
299
|
+
fi
|
|
300
|
+
|
|
301
|
+
echo "Redline gate passed."
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
# Lives in the Redline source repo. Distributes the rendered standards to onboarded
|
|
2
|
+
# repos as pull requests — never as direct pushes, and never as a merge.
|
|
3
|
+
#
|
|
4
|
+
# Targets come from registry.json, the derived register the Redline Registry workflow
|
|
5
|
+
# refreshes nightly from the .redline.json each onboarded repo carries. Nothing here is
|
|
6
|
+
# hand-maintained: a repository that removes Redline leaves the register, and stops being
|
|
7
|
+
# a target, without anyone editing a list.
|
|
8
|
+
#
|
|
9
|
+
# What a repository does with its sync pull request is the repository's decision. Sync
|
|
10
|
+
# makes the change available; the estate dashboard's coverage figure is what makes an
|
|
11
|
+
# ignored one visible.
|
|
12
|
+
name: Redline Sync
|
|
13
|
+
|
|
14
|
+
on:
|
|
15
|
+
push:
|
|
16
|
+
branches: [main]
|
|
17
|
+
paths:
|
|
18
|
+
- 'standards/**'
|
|
19
|
+
- 'cli/render/**'
|
|
20
|
+
- 'templates/**'
|
|
21
|
+
- '.github/pull_request_template.md'
|
|
22
|
+
workflow_dispatch:
|
|
23
|
+
inputs:
|
|
24
|
+
dry-run:
|
|
25
|
+
description: Print the plan without opening pull requests
|
|
26
|
+
type: boolean
|
|
27
|
+
default: false
|
|
28
|
+
only:
|
|
29
|
+
description: Sync a single repo, as owner/name
|
|
30
|
+
type: string
|
|
31
|
+
required: false
|
|
32
|
+
|
|
33
|
+
permissions:
|
|
34
|
+
contents: read
|
|
35
|
+
|
|
36
|
+
concurrency:
|
|
37
|
+
group: redline-sync
|
|
38
|
+
cancel-in-progress: false
|
|
39
|
+
|
|
40
|
+
jobs:
|
|
41
|
+
sync:
|
|
42
|
+
runs-on: ubuntu-latest
|
|
43
|
+
timeout-minutes: 60
|
|
44
|
+
steps:
|
|
45
|
+
- uses: actions/checkout@v4
|
|
46
|
+
- uses: actions/setup-node@v4
|
|
47
|
+
with:
|
|
48
|
+
node-version: 22
|
|
49
|
+
|
|
50
|
+
- name: Install
|
|
51
|
+
run: npm ci
|
|
52
|
+
|
|
53
|
+
# A sync that distributed stale artifacts would propagate the staleness to
|
|
54
|
+
# every onboarded repository at once, as a pull request each team is being
|
|
55
|
+
# asked to trust. This runs first for that reason.
|
|
56
|
+
- name: Verify rendered artifacts in this repo are current
|
|
57
|
+
run: node scripts/render-self.mjs --check
|
|
58
|
+
|
|
59
|
+
- name: Build CLI
|
|
60
|
+
run: npm run build
|
|
61
|
+
|
|
62
|
+
- name: Open sync pull requests
|
|
63
|
+
env:
|
|
64
|
+
# Fine-grained PAT or GitHub App installation token. Required permissions on
|
|
65
|
+
# every target repo: contents:write, pull_requests:write, workflows:write.
|
|
66
|
+
# Without `workflows` the push of .github/workflows/redline.yml is rejected.
|
|
67
|
+
GH_TOKEN: ${{ secrets.REDLINE_SYNC_TOKEN }}
|
|
68
|
+
DRY: ${{ inputs.dry-run && '--dry-run' || '' }}
|
|
69
|
+
ONLY: ${{ inputs.only }}
|
|
70
|
+
run: |
|
|
71
|
+
set -euo pipefail
|
|
72
|
+
args=()
|
|
73
|
+
[[ -n "$DRY" ]] && args+=("$DRY")
|
|
74
|
+
[[ -n "$ONLY" ]] && args+=(--repo "$ONLY")
|
|
75
|
+
node dist/bin/redline.js sync ${args[@]+"${args[@]}"}
|
|
76
|
+
|
|
77
|
+
- name: Summarise
|
|
78
|
+
if: always()
|
|
79
|
+
run: echo "Sync finished with status ${{ job.status }}" >> "$GITHUB_STEP_SUMMARY"
|
|
@@ -0,0 +1,231 @@
|
|
|
1
|
+
# Lives in the redline-metrics repo. Opens a throwaway PR of known-bad code against a
|
|
2
|
+
# canary repo, waits for the automated reviewer, scores it, records the result, and
|
|
3
|
+
# closes the PR.
|
|
4
|
+
#
|
|
5
|
+
# This is the regression test for the review layer itself. Standards drift, models change
|
|
6
|
+
# underneath you, and a repo can silently lose its Copilot review entitlement — none of
|
|
7
|
+
# which shows up in ordinary telemetry, because "no findings" and "nothing to find" look
|
|
8
|
+
# identical. Only a corpus with known answers tells them apart.
|
|
9
|
+
#
|
|
10
|
+
# Configuration (repo variables / secrets on redline-metrics):
|
|
11
|
+
# vars.CANARY_TARGETS JSON array, e.g.
|
|
12
|
+
# [{"repo":"acme/pilot-web","stack":"react"},
|
|
13
|
+
# {"repo":"acme/pilot-api","stack":"nodejs"}]
|
|
14
|
+
# vars.REDLINE_SOURCE owner/repo of the Redline source bundle (default <owner>/redline)
|
|
15
|
+
# secrets.REDLINE_CANARY_TOKEN contents:write + pull_requests:write on the canary
|
|
16
|
+
# repos ONLY. Never an org-wide token.
|
|
17
|
+
# secrets.REDLINE_ORG_READ_TOKEN read access, used to check out the source bundle.
|
|
18
|
+
name: Redline Seed Canary
|
|
19
|
+
|
|
20
|
+
on:
|
|
21
|
+
schedule:
|
|
22
|
+
- cron: '0 3 * * 1'
|
|
23
|
+
workflow_dispatch:
|
|
24
|
+
inputs:
|
|
25
|
+
targets:
|
|
26
|
+
description: Override CANARY_TARGETS with a JSON array for this run
|
|
27
|
+
type: string
|
|
28
|
+
required: false
|
|
29
|
+
|
|
30
|
+
permissions:
|
|
31
|
+
contents: write
|
|
32
|
+
|
|
33
|
+
concurrency:
|
|
34
|
+
group: seed-canary
|
|
35
|
+
cancel-in-progress: false
|
|
36
|
+
|
|
37
|
+
jobs:
|
|
38
|
+
plan:
|
|
39
|
+
runs-on: ubuntu-latest
|
|
40
|
+
outputs:
|
|
41
|
+
targets: ${{ steps.plan.outputs.targets }}
|
|
42
|
+
any: ${{ steps.plan.outputs.any }}
|
|
43
|
+
steps:
|
|
44
|
+
- id: plan
|
|
45
|
+
env:
|
|
46
|
+
OVERRIDE: ${{ inputs.targets }}
|
|
47
|
+
CONFIGURED: ${{ vars.CANARY_TARGETS }}
|
|
48
|
+
run: |
|
|
49
|
+
set -euo pipefail
|
|
50
|
+
raw="${OVERRIDE:-${CONFIGURED:-[]}}"
|
|
51
|
+
if ! jq -e 'type == "array"' <<<"$raw" >/dev/null 2>&1; then
|
|
52
|
+
echo "::error::CANARY_TARGETS must be a JSON array of {repo, stack} objects."
|
|
53
|
+
exit 1
|
|
54
|
+
fi
|
|
55
|
+
count=$(jq 'length' <<<"$raw")
|
|
56
|
+
echo "targets=$(jq -c . <<<"$raw")" >> "$GITHUB_OUTPUT"
|
|
57
|
+
echo "any=$([[ "$count" -gt 0 ]] && echo true || echo false)" >> "$GITHUB_OUTPUT"
|
|
58
|
+
echo "$count canary target(s)" >> "$GITHUB_STEP_SUMMARY"
|
|
59
|
+
|
|
60
|
+
score:
|
|
61
|
+
needs: plan
|
|
62
|
+
if: needs.plan.outputs.any == 'true'
|
|
63
|
+
runs-on: ubuntu-latest
|
|
64
|
+
timeout-minutes: 75
|
|
65
|
+
strategy:
|
|
66
|
+
fail-fast: false
|
|
67
|
+
max-parallel: 2
|
|
68
|
+
matrix:
|
|
69
|
+
target: ${{ fromJSON(needs.plan.outputs.targets) }}
|
|
70
|
+
steps:
|
|
71
|
+
- name: Check out the Redline source bundle
|
|
72
|
+
uses: actions/checkout@v4
|
|
73
|
+
with:
|
|
74
|
+
repository: ${{ vars.REDLINE_SOURCE || format('{0}/redline', github.repository_owner) }}
|
|
75
|
+
token: ${{ secrets.REDLINE_ORG_READ_TOKEN }}
|
|
76
|
+
path: redline
|
|
77
|
+
|
|
78
|
+
- uses: actions/setup-node@v4
|
|
79
|
+
with:
|
|
80
|
+
node-version: 22
|
|
81
|
+
|
|
82
|
+
- name: Open the seed pull request
|
|
83
|
+
id: open
|
|
84
|
+
env:
|
|
85
|
+
GH_TOKEN: ${{ secrets.REDLINE_CANARY_TOKEN }}
|
|
86
|
+
TARGET_REPO: ${{ matrix.target.repo }}
|
|
87
|
+
STACK: ${{ matrix.target.stack }}
|
|
88
|
+
run: |
|
|
89
|
+
set -euo pipefail
|
|
90
|
+
branch="redline/seed-canary-$(date -u +%Y%m%d%H%M%S)"
|
|
91
|
+
work=$(mktemp -d)
|
|
92
|
+
gh repo clone "$TARGET_REPO" "$work" -- --depth 1 --quiet
|
|
93
|
+
cd "$work"
|
|
94
|
+
git checkout -q -b "$branch"
|
|
95
|
+
|
|
96
|
+
mkdir -p seeded
|
|
97
|
+
cp -r "$GITHUB_WORKSPACE/redline/seeded/$STACK" seeded/
|
|
98
|
+
cp -r "$GITHUB_WORKSPACE/redline/seeded/clean" seeded/
|
|
99
|
+
|
|
100
|
+
git -c user.name="redline-bot" -c user.email="redline-bot@users.noreply.github.com" \
|
|
101
|
+
add seeded
|
|
102
|
+
git -c user.name="redline-bot" -c user.email="redline-bot@users.noreply.github.com" \
|
|
103
|
+
commit -q -m "test(redline): seed canary for $STACK — DO NOT MERGE"
|
|
104
|
+
git push -q origin "$branch"
|
|
105
|
+
|
|
106
|
+
# redline-exempt keeps the readiness gate from blocking a PR nobody will merge.
|
|
107
|
+
pr=$(gh pr create --repo "$TARGET_REPO" --head "$branch" \
|
|
108
|
+
--title "DO NOT MERGE — Redline seed canary ($STACK)" \
|
|
109
|
+
--body "Automated validation run. This branch contains deliberately broken code and fake credentials. It is scored and closed automatically; never merge it." \
|
|
110
|
+
--label redline-exempt 2>/dev/null || \
|
|
111
|
+
gh pr create --repo "$TARGET_REPO" --head "$branch" \
|
|
112
|
+
--title "DO NOT MERGE — Redline seed canary ($STACK)" \
|
|
113
|
+
--body "Automated validation run. Scored and closed automatically; never merge it.")
|
|
114
|
+
|
|
115
|
+
number="${pr##*/}"
|
|
116
|
+
echo "branch=$branch" >> "$GITHUB_OUTPUT"
|
|
117
|
+
echo "pr=$number" >> "$GITHUB_OUTPUT"
|
|
118
|
+
echo "Opened $TARGET_REPO#$number on $branch"
|
|
119
|
+
|
|
120
|
+
- name: Wait for the automated review to settle
|
|
121
|
+
env:
|
|
122
|
+
GH_TOKEN: ${{ secrets.REDLINE_CANARY_TOKEN }}
|
|
123
|
+
TARGET_REPO: ${{ matrix.target.repo }}
|
|
124
|
+
PR: ${{ steps.open.outputs.pr }}
|
|
125
|
+
run: |
|
|
126
|
+
set -euo pipefail
|
|
127
|
+
# Reviewers stream comments in over several minutes. Poll until the count stops
|
|
128
|
+
# moving for two consecutive checks, so a slow reviewer is not scored half-done.
|
|
129
|
+
deadline=$(( SECONDS + 2400 ))
|
|
130
|
+
previous=-1
|
|
131
|
+
stable=0
|
|
132
|
+
while (( SECONDS < deadline )); do
|
|
133
|
+
sleep 60
|
|
134
|
+
count=$(gh api "repos/$TARGET_REPO/pulls/$PR/comments" --paginate --jq 'length' | paste -sd+ - | bc)
|
|
135
|
+
echo " comments: $count (previous $previous, stable $stable)"
|
|
136
|
+
if [[ "$count" -eq "$previous" && "$count" -gt 0 ]]; then
|
|
137
|
+
stable=$(( stable + 1 ))
|
|
138
|
+
[[ "$stable" -ge 2 ]] && break
|
|
139
|
+
else
|
|
140
|
+
stable=0
|
|
141
|
+
fi
|
|
142
|
+
previous="$count"
|
|
143
|
+
done
|
|
144
|
+
if [[ "${previous:-0}" -le 0 ]]; then
|
|
145
|
+
echo "::warning::No review comments appeared within the wait window. Scoring will report zero recall, which is itself the finding."
|
|
146
|
+
fi
|
|
147
|
+
|
|
148
|
+
- name: Score against the corpus
|
|
149
|
+
id: score
|
|
150
|
+
env:
|
|
151
|
+
GH_TOKEN: ${{ secrets.REDLINE_CANARY_TOKEN }}
|
|
152
|
+
TARGET_REPO: ${{ matrix.target.repo }}
|
|
153
|
+
PR: ${{ steps.open.outputs.pr }}
|
|
154
|
+
working-directory: redline
|
|
155
|
+
run: |
|
|
156
|
+
set -euo pipefail
|
|
157
|
+
mkdir -p "$GITHUB_WORKSPACE/scores"
|
|
158
|
+
npx --yes "redlinegate@0.0.1" metrics score-seeds --repo "$TARGET_REPO" --pr "$PR" --json \
|
|
159
|
+
> "$GITHUB_WORKSPACE/scores/$(echo "$TARGET_REPO" | tr / -).json" || echo "score-seeds exited non-zero"
|
|
160
|
+
npx --yes "redlinegate@0.0.1" metrics score-seeds --repo "$TARGET_REPO" --pr "$PR" \
|
|
161
|
+
| tee -a "$GITHUB_STEP_SUMMARY" || true
|
|
162
|
+
|
|
163
|
+
- name: Close the canary pull request
|
|
164
|
+
if: always()
|
|
165
|
+
env:
|
|
166
|
+
GH_TOKEN: ${{ secrets.REDLINE_CANARY_TOKEN }}
|
|
167
|
+
TARGET_REPO: ${{ matrix.target.repo }}
|
|
168
|
+
PR: ${{ steps.open.outputs.pr }}
|
|
169
|
+
BRANCH: ${{ steps.open.outputs.branch }}
|
|
170
|
+
run: |
|
|
171
|
+
set -euo pipefail
|
|
172
|
+
# Cleanup must not be skippable: the branch contains fake credentials and code
|
|
173
|
+
# that exists only to fail review.
|
|
174
|
+
if [[ -n "${PR:-}" ]]; then
|
|
175
|
+
gh pr close "$PR" --repo "$TARGET_REPO" --delete-branch --comment "Scored automatically by the Redline seed canary. Closing." || true
|
|
176
|
+
fi
|
|
177
|
+
if [[ -n "${BRANCH:-}" ]]; then
|
|
178
|
+
gh api -X DELETE "repos/$TARGET_REPO/git/refs/heads/$BRANCH" >/dev/null 2>&1 || true
|
|
179
|
+
fi
|
|
180
|
+
echo "cleanup done"
|
|
181
|
+
|
|
182
|
+
- uses: actions/upload-artifact@v4
|
|
183
|
+
if: always()
|
|
184
|
+
with:
|
|
185
|
+
name: seed-score-${{ strategy.job-index }}
|
|
186
|
+
path: scores/
|
|
187
|
+
if-no-files-found: ignore
|
|
188
|
+
|
|
189
|
+
record:
|
|
190
|
+
needs: score
|
|
191
|
+
if: always() && needs.score.result != 'skipped'
|
|
192
|
+
runs-on: ubuntu-latest
|
|
193
|
+
steps:
|
|
194
|
+
- uses: actions/checkout@v4
|
|
195
|
+
- uses: actions/download-artifact@v4
|
|
196
|
+
with:
|
|
197
|
+
path: scores
|
|
198
|
+
merge-multiple: true
|
|
199
|
+
|
|
200
|
+
- name: Append scores and fail on regression
|
|
201
|
+
run: |
|
|
202
|
+
set -euo pipefail
|
|
203
|
+
mkdir -p data
|
|
204
|
+
touch data/seed-scores.jsonl
|
|
205
|
+
shopt -s nullglob
|
|
206
|
+
files=(scores/*.json)
|
|
207
|
+
if [[ ${#files[@]} -eq 0 ]]; then
|
|
208
|
+
echo "::error::No score artifacts were produced — every canary run failed before scoring."
|
|
209
|
+
exit 1
|
|
210
|
+
fi
|
|
211
|
+
|
|
212
|
+
regressed=0
|
|
213
|
+
for file in "${files[@]}"; do
|
|
214
|
+
jq -c . "$file" >> data/seed-scores.jsonl
|
|
215
|
+
repo=$(jq -r .repo "$file")
|
|
216
|
+
recall=$(jq -r .totals.blocker_recall "$file")
|
|
217
|
+
fps=$(jq -r .totals.false_positives_on_clean "$file")
|
|
218
|
+
attribution=$(jq -r .totals.rule_attribution "$file")
|
|
219
|
+
echo "- $repo: BLOCKER recall $recall, rule attribution $attribution, false positives $fps" >> "$GITHUB_STEP_SUMMARY"
|
|
220
|
+
awk -v r="$recall" 'BEGIN { exit (r >= 1 ? 0 : 1) }' || { echo "::error::$repo BLOCKER recall $recall is below 1.0"; regressed=1; }
|
|
221
|
+
[[ "$fps" -gt 0 ]] && { echo "::error::$repo produced $fps false positive(s) on the clean corpus"; regressed=1; }
|
|
222
|
+
done
|
|
223
|
+
|
|
224
|
+
git config user.name "redline-bot"
|
|
225
|
+
git config user.email "redline-bot@users.noreply.github.com"
|
|
226
|
+
git add data/seed-scores.jsonl
|
|
227
|
+
git commit -q -m "data: seed canary scores $(date -u +%Y-%m-%d)" || echo "nothing to commit"
|
|
228
|
+
git pull --rebase origin "${GITHUB_REF_NAME}"
|
|
229
|
+
git push origin "HEAD:${GITHUB_REF_NAME}"
|
|
230
|
+
|
|
231
|
+
exit "$regressed"
|