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.
Files changed (198) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +188 -0
  3. package/commands/redline-init.md +54 -0
  4. package/commands/redline-verify.md +69 -0
  5. package/dist/bin/redline.js +605 -0
  6. package/dist/bin/redline.js.map +1 -0
  7. package/dist/commands/exempt.js +45 -0
  8. package/dist/commands/exempt.js.map +1 -0
  9. package/dist/commands/init.js +615 -0
  10. package/dist/commands/init.js.map +1 -0
  11. package/dist/commands/policy.js +25 -0
  12. package/dist/commands/policy.js.map +1 -0
  13. package/dist/commands/remove.js +427 -0
  14. package/dist/commands/remove.js.map +1 -0
  15. package/dist/commands/review.js +72 -0
  16. package/dist/commands/review.js.map +1 -0
  17. package/dist/commands/sync.js +42 -0
  18. package/dist/commands/sync.js.map +1 -0
  19. package/dist/commands/verify.js +478 -0
  20. package/dist/commands/verify.js.map +1 -0
  21. package/dist/config/redline-json.js +187 -0
  22. package/dist/config/redline-json.js.map +1 -0
  23. package/dist/core/errors.js +25 -0
  24. package/dist/core/errors.js.map +1 -0
  25. package/dist/core/git.js +152 -0
  26. package/dist/core/git.js.map +1 -0
  27. package/dist/core/log.js +22 -0
  28. package/dist/core/log.js.map +1 -0
  29. package/dist/core/severity.js +16 -0
  30. package/dist/core/severity.js.map +1 -0
  31. package/dist/core/version.js +15 -0
  32. package/dist/core/version.js.map +1 -0
  33. package/dist/detect/scan.js +79 -0
  34. package/dist/detect/scan.js.map +1 -0
  35. package/dist/detect/stack.js +71 -0
  36. package/dist/detect/stack.js.map +1 -0
  37. package/dist/enforce/ladder.js +148 -0
  38. package/dist/enforce/ladder.js.map +1 -0
  39. package/dist/exempt/parse.js +86 -0
  40. package/dist/exempt/parse.js.map +1 -0
  41. package/dist/metrics/options.js +257 -0
  42. package/dist/metrics/options.js.map +1 -0
  43. package/dist/metrics/run.js +72 -0
  44. package/dist/metrics/run.js.map +1 -0
  45. package/dist/platforms/azure/client.js +53 -0
  46. package/dist/platforms/azure/client.js.map +1 -0
  47. package/dist/platforms/azure/index.js +69 -0
  48. package/dist/platforms/azure/index.js.map +1 -0
  49. package/dist/platforms/azure/install.js +917 -0
  50. package/dist/platforms/azure/install.js.map +1 -0
  51. package/dist/platforms/azure/policy-types.js +70 -0
  52. package/dist/platforms/azure/policy-types.js.map +1 -0
  53. package/dist/platforms/azure/verify.js +309 -0
  54. package/dist/platforms/azure/verify.js.map +1 -0
  55. package/dist/platforms/detect.js +40 -0
  56. package/dist/platforms/detect.js.map +1 -0
  57. package/dist/platforms/github/client.js +49 -0
  58. package/dist/platforms/github/client.js.map +1 -0
  59. package/dist/platforms/github/index.js +54 -0
  60. package/dist/platforms/github/index.js.map +1 -0
  61. package/dist/platforms/github/install.js +558 -0
  62. package/dist/platforms/github/install.js.map +1 -0
  63. package/dist/platforms/github/push.js +102 -0
  64. package/dist/platforms/github/push.js.map +1 -0
  65. package/dist/platforms/github/remote.js +36 -0
  66. package/dist/platforms/github/remote.js.map +1 -0
  67. package/dist/platforms/github/verify.js +366 -0
  68. package/dist/platforms/github/verify.js.map +1 -0
  69. package/dist/platforms/http.js +58 -0
  70. package/dist/platforms/http.js.map +1 -0
  71. package/dist/platforms/pull-request-templates.js +157 -0
  72. package/dist/platforms/pull-request-templates.js.map +1 -0
  73. package/dist/platforms/remote.js +21 -0
  74. package/dist/platforms/remote.js.map +1 -0
  75. package/dist/platforms/resolve.js +69 -0
  76. package/dist/platforms/resolve.js.map +1 -0
  77. package/dist/platforms/shape.js +19 -0
  78. package/dist/platforms/shape.js.map +1 -0
  79. package/dist/platforms/types.js +24 -0
  80. package/dist/platforms/types.js.map +1 -0
  81. package/dist/policy/checks.js +91 -0
  82. package/dist/policy/checks.js.map +1 -0
  83. package/dist/policy/diff.js +50 -0
  84. package/dist/policy/diff.js.map +1 -0
  85. package/dist/registry/discover.js +74 -0
  86. package/dist/registry/discover.js.map +1 -0
  87. package/dist/registry/serialize.js +41 -0
  88. package/dist/registry/serialize.js.map +1 -0
  89. package/dist/registry/types.js +2 -0
  90. package/dist/registry/types.js.map +1 -0
  91. package/dist/remove/host.js +294 -0
  92. package/dist/remove/host.js.map +1 -0
  93. package/dist/render/commands.js +139 -0
  94. package/dist/render/commands.js.map +1 -0
  95. package/dist/render/manifest.js +82 -0
  96. package/dist/render/manifest.js.map +1 -0
  97. package/dist/render/markers.js +166 -0
  98. package/dist/render/markers.js.map +1 -0
  99. package/dist/render/profile.js +23 -0
  100. package/dist/render/profile.js.map +1 -0
  101. package/dist/render/standards.js +169 -0
  102. package/dist/render/standards.js.map +1 -0
  103. package/dist/render/vendors.js +199 -0
  104. package/dist/render/vendors.js.map +1 -0
  105. package/dist/review/engines/api.js +95 -0
  106. package/dist/review/engines/api.js.map +1 -0
  107. package/dist/review/engines/embedded.js +20 -0
  108. package/dist/review/engines/embedded.js.map +1 -0
  109. package/dist/review/engines/types.js +2 -0
  110. package/dist/review/engines/types.js.map +1 -0
  111. package/dist/review/glob.js +29 -0
  112. package/dist/review/glob.js.map +1 -0
  113. package/dist/review/prompt.js +45 -0
  114. package/dist/review/prompt.js.map +1 -0
  115. package/dist/review/rules.js +25 -0
  116. package/dist/review/rules.js.map +1 -0
  117. package/dist/review/schema.js +84 -0
  118. package/dist/review/schema.js.map +1 -0
  119. package/dist/review/scope.js +27 -0
  120. package/dist/review/scope.js.map +1 -0
  121. package/dist/sarif/map.js +58 -0
  122. package/dist/sarif/map.js.map +1 -0
  123. package/dist/sarif/parse.js +88 -0
  124. package/dist/sarif/parse.js.map +1 -0
  125. package/dist/sarif/types.js +2 -0
  126. package/dist/sarif/types.js.map +1 -0
  127. package/dist/sync/host.js +21 -0
  128. package/dist/sync/host.js.map +1 -0
  129. package/dist/sync/plan.js +47 -0
  130. package/dist/sync/plan.js.map +1 -0
  131. package/dist/sync/render.js +57 -0
  132. package/dist/sync/render.js.map +1 -0
  133. package/dist/sync/run.js +118 -0
  134. package/dist/sync/run.js.map +1 -0
  135. package/dist/verify/host.js +52 -0
  136. package/dist/verify/host.js.map +1 -0
  137. package/dist/verify/remote.js +146 -0
  138. package/dist/verify/remote.js.map +1 -0
  139. package/package.json +52 -0
  140. package/platforms/azure/gate-template.yml +105 -0
  141. package/rulesets/redline-org-ruleset.json +46 -0
  142. package/rulesets/redline-ruleset.json +49 -0
  143. package/scripts/assign-rule-ids.mjs +129 -0
  144. package/scripts/build-baseline.mjs +124 -0
  145. package/scripts/build-correlation.mjs +74 -0
  146. package/scripts/build-dashboard.mjs +584 -0
  147. package/scripts/build-digest.mjs +164 -0
  148. package/scripts/build-inbox.mjs +133 -0
  149. package/scripts/build-registry.mjs +42 -0
  150. package/scripts/build-roi.mjs +181 -0
  151. package/scripts/check-pins.mjs +88 -0
  152. package/scripts/collect-telemetry.mjs +300 -0
  153. package/scripts/lib/__tests__/baseline.test.mjs +132 -0
  154. package/scripts/lib/__tests__/correlate.test.mjs +131 -0
  155. package/scripts/lib/__tests__/dora.test.mjs +110 -0
  156. package/scripts/lib/__tests__/exemptions.test.mjs +85 -0
  157. package/scripts/lib/__tests__/metrics-scanner.test.mjs +65 -0
  158. package/scripts/lib/__tests__/sarif.test.mjs +125 -0
  159. package/scripts/lib/__tests__/spend.test.mjs +64 -0
  160. package/scripts/lib/baseline.mjs +156 -0
  161. package/scripts/lib/correlate.mjs +125 -0
  162. package/scripts/lib/dora.mjs +133 -0
  163. package/scripts/lib/exemptions.mjs +79 -0
  164. package/scripts/lib/metrics.mjs +172 -0
  165. package/scripts/lib/rules.mjs +107 -0
  166. package/scripts/lib/sarif.mjs +108 -0
  167. package/scripts/lib/spend.mjs +69 -0
  168. package/scripts/measure-context.mjs +101 -0
  169. package/scripts/render-self.mjs +18 -0
  170. package/scripts/score-seeds.mjs +294 -0
  171. package/scripts/validate.mjs +394 -0
  172. package/standards/core.md +124 -0
  173. package/standards/manifest.json +214 -0
  174. package/standards/stacks/csharp.md +29 -0
  175. package/standards/stacks/go.md +30 -0
  176. package/standards/stacks/java.md +30 -0
  177. package/standards/stacks/javascript.md +42 -0
  178. package/standards/stacks/kotlin.md +27 -0
  179. package/standards/stacks/microservices.md +37 -0
  180. package/standards/stacks/nodejs.md +34 -0
  181. package/standards/stacks/python.md +29 -0
  182. package/standards/stacks/react-native.md +29 -0
  183. package/standards/stacks/react.md +46 -0
  184. package/standards/stacks/swift.md +26 -0
  185. package/standards/stacks/terraform.md +27 -0
  186. package/templates/CODEOWNERS +29 -0
  187. package/templates/azure/pull_request_template.md +72 -0
  188. package/templates/github/pull_request_template.md +72 -0
  189. package/templates/redline.yml +35 -0
  190. package/templates/repo-context.md +64 -0
  191. package/workflows/dashboard.yml +119 -0
  192. package/workflows/inbox.yml +97 -0
  193. package/workflows/redline-collect.yml +61 -0
  194. package/workflows/redline-gate.yml +301 -0
  195. package/workflows/redline-sync.yml +79 -0
  196. package/workflows/seed-canary.yml +231 -0
  197. package/workflows/verify-onboarding.yml +115 -0
  198. 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"