seed-canary.yml
Weekly regression test against the seeded corpus. Lives in the redline-metrics repo.
What this is
The regression test for the review layer itself: opens a throwaway PR of known-bad code against a canary repo, waits for the automated reviewer, scores it, records the result, and closes the PR. Distinguishes "nothing to find" from "stopped finding things", which ordinary telemetry can't.
How to onboard it
Copy this file into the redline-metrics repository and set CANARY_TARGETS, REDLINE_CANARY_TOKEN (scoped to the canary repos only) and REDLINE_ORG_READ_TOKEN. It opens and closes pull requests on the targets, so scope that token narrowly.
- Lives in: The redline-metrics repo — not this one.
- Trigger: Weekly, Monday 03:00 UTC, plus workflow_dispatch with a targets override.
How to use it
Nothing, normally — it runs itself weekly. Trigger it by hand with workflow_dispatch and a targets override when validating a new AI reviewer vendor or a standards change.
What a run does, in order:
- plan — reads the CANARY_TARGETS repo variable (or a dispatch override), a JSON array of {repo, stack}.
- score (matrix, one run per target) — opens a branch on the target repo carrying seeded/<stack> and seeded/clean, labelled redline-exempt so the readiness gate doesn't block a PR nobody will merge; waits for review comments to stop arriving (two stable polls, up to 40 minutes); scores with scripts/score-seeds.mjs --json; always closes and deletes the PR/branch afterward, even on failure.
- record — appends every score to data/seed-scores.jsonl and fails the run if any target's BLOCKER recall is below 1.0 or produced a false positive on the clean corpus.
Works, but only where installed: needs CANARY_TARGETS, REDLINE_CANARY_TOKEN scoped to the canary repos only, and REDLINE_ORG_READ_TOKEN to check out this repo's seeded corpus.
Expected output
One score per target appended to data/seed-scores.jsonl, and a failed run if any target's BLOCKER recall drops below 1.0 or the clean corpus attracts a false positive. Every throwaway PR and branch it opens is closed and deleted afterwards, including when the run fails.
How to edit it
- Edit the YAML in workflows/ or .github/workflows/workflows/ holds files destined for other repositories; .github/workflows/ is this repository's own CI. The two are not interchangeable — check where this one lives before editing.
- actionlintCI lints .github/workflows/*.yml, workflows/*.yml and templates/redline.yml together. workflows/ is pointed at explicitly because actionlint's own discovery would skip it.
- node scripts/check-pins.mjsIf you add a third-party action, pin it to a 40-character commit SHA with a trailing # vX.Y.Z comment. First-party actions/* are referenced by tag. The pin checker re-resolves the SHA against the tag the comment claims.
- node scripts/validate.mjsAsserts the workflow files the bundle depends on still exist, and that the gate's job ids still match the required check name derived from them.
The full file
# Lives in the redline-metrics repo. Opens a throwaway PR of known-bad code against a
# canary repo, waits for the automated reviewer, scores it, records the result, and
# closes the PR.
#
# This is the regression test for the review layer itself. Standards drift, models change
# underneath you, and a repo can silently lose its Copilot review entitlement — none of
# which shows up in ordinary telemetry, because "no findings" and "nothing to find" look
# identical. Only a corpus with known answers tells them apart.
#
# Configuration (repo variables / secrets on redline-metrics):
# vars.CANARY_TARGETS JSON array, e.g.
# [{"repo":"acme/pilot-web","stack":"react"},
# {"repo":"acme/pilot-api","stack":"nodejs"}]
# vars.REDLINE_SOURCE owner/repo of the Redline source bundle (default <owner>/redline)
# secrets.REDLINE_CANARY_TOKEN contents:write + pull_requests:write on the canary
# repos ONLY. Never an org-wide token.
# secrets.REDLINE_ORG_READ_TOKEN read access, used to check out the source bundle.
name: Redline Seed Canary
on:
schedule:
- cron: '0 3 * * 1'
workflow_dispatch:
inputs:
targets:
description: Override CANARY_TARGETS with a JSON array for this run
type: string
required: false
permissions:
contents: write
concurrency:
group: seed-canary
cancel-in-progress: false
jobs:
plan:
runs-on: ubuntu-latest
outputs:
targets: ${{ steps.plan.outputs.targets }}
any: ${{ steps.plan.outputs.any }}
steps:
- id: plan
env:
OVERRIDE: ${{ inputs.targets }}
CONFIGURED: ${{ vars.CANARY_TARGETS }}
run: |
set -euo pipefail
raw="${OVERRIDE:-${CONFIGURED:-[]}}"
if ! jq -e 'type == "array"' <<<"$raw" >/dev/null 2>&1; then
echo "::error::CANARY_TARGETS must be a JSON array of {repo, stack} objects."
exit 1
fi
count=$(jq 'length' <<<"$raw")
echo "targets=$(jq -c . <<<"$raw")" >> "$GITHUB_OUTPUT"
echo "any=$([[ "$count" -gt 0 ]] && echo true || echo false)" >> "$GITHUB_OUTPUT"
echo "$count canary target(s)" >> "$GITHUB_STEP_SUMMARY"
score:
needs: plan
if: needs.plan.outputs.any == 'true'
runs-on: ubuntu-latest
timeout-minutes: 75
strategy:
fail-fast: false
max-parallel: 2
matrix:
target: ${{ fromJSON(needs.plan.outputs.targets) }}
steps:
- name: Check out the Redline source bundle
uses: actions/checkout@v4
with:
repository: ${{ vars.REDLINE_SOURCE || format('{0}/redline', github.repository_owner) }}
token: ${{ secrets.REDLINE_ORG_READ_TOKEN }}
path: redline
- uses: actions/setup-node@v4
with:
node-version: 22
- name: Open the seed pull request
id: open
env:
GH_TOKEN: ${{ secrets.REDLINE_CANARY_TOKEN }}
TARGET_REPO: ${{ matrix.target.repo }}
STACK: ${{ matrix.target.stack }}
run: |
set -euo pipefail
branch="redline/seed-canary-$(date -u +%Y%m%d%H%M%S)"
work=$(mktemp -d)
gh repo clone "$TARGET_REPO" "$work" -- --depth 1 --quiet
cd "$work"
git checkout -q -b "$branch"
mkdir -p seeded
cp -r "$GITHUB_WORKSPACE/redline/seeded/$STACK" seeded/
cp -r "$GITHUB_WORKSPACE/redline/seeded/clean" seeded/
git -c user.name="redline-bot" -c user.email="redline-bot@users.noreply.github.com" \
add seeded
git -c user.name="redline-bot" -c user.email="redline-bot@users.noreply.github.com" \
commit -q -m "test(redline): seed canary for $STACK — DO NOT MERGE"
git push -q origin "$branch"
# redline-exempt keeps the readiness gate from blocking a PR nobody will merge.
pr=$(gh pr create --repo "$TARGET_REPO" --head "$branch" \
--title "DO NOT MERGE — Redline seed canary ($STACK)" \
--body "Automated validation run. This branch contains deliberately broken code and fake credentials. It is scored and closed automatically; never merge it." \
--label redline-exempt 2>/dev/null || \
gh pr create --repo "$TARGET_REPO" --head "$branch" \
--title "DO NOT MERGE — Redline seed canary ($STACK)" \
--body "Automated validation run. Scored and closed automatically; never merge it.")
number="${pr##*/}"
echo "branch=$branch" >> "$GITHUB_OUTPUT"
echo "pr=$number" >> "$GITHUB_OUTPUT"
echo "Opened $TARGET_REPO#$number on $branch"
- name: Wait for the automated review to settle
env:
GH_TOKEN: ${{ secrets.REDLINE_CANARY_TOKEN }}
TARGET_REPO: ${{ matrix.target.repo }}
PR: ${{ steps.open.outputs.pr }}
run: |
set -euo pipefail
# Reviewers stream comments in over several minutes. Poll until the count stops
# moving for two consecutive checks, so a slow reviewer is not scored half-done.
deadline=$(( SECONDS + 2400 ))
previous=-1
stable=0
while (( SECONDS < deadline )); do
sleep 60
count=$(gh api "repos/$TARGET_REPO/pulls/$PR/comments" --paginate --jq 'length' | paste -sd+ - | bc)
echo " comments: $count (previous $previous, stable $stable)"
if [[ "$count" -eq "$previous" && "$count" -gt 0 ]]; then
stable=$(( stable + 1 ))
[[ "$stable" -ge 2 ]] && break
else
stable=0
fi
previous="$count"
done
if [[ "${previous:-0}" -le 0 ]]; then
echo "::warning::No review comments appeared within the wait window. Scoring will report zero recall, which is itself the finding."
fi
- name: Score against the corpus
id: score
env:
GH_TOKEN: ${{ secrets.REDLINE_CANARY_TOKEN }}
TARGET_REPO: ${{ matrix.target.repo }}
PR: ${{ steps.open.outputs.pr }}
working-directory: redline
run: |
set -euo pipefail
mkdir -p "$GITHUB_WORKSPACE/scores"
npx --yes "redlinegate@0.0.3" metrics score-seeds --repo "$TARGET_REPO" --pr "$PR" --json \
> "$GITHUB_WORKSPACE/scores/$(echo "$TARGET_REPO" | tr / -).json" || echo "score-seeds exited non-zero"
npx --yes "redlinegate@0.0.3" metrics score-seeds --repo "$TARGET_REPO" --pr "$PR" \
| tee -a "$GITHUB_STEP_SUMMARY" || true
- name: Close the canary pull request
if: always()
env:
GH_TOKEN: ${{ secrets.REDLINE_CANARY_TOKEN }}
TARGET_REPO: ${{ matrix.target.repo }}
PR: ${{ steps.open.outputs.pr }}
BRANCH: ${{ steps.open.outputs.branch }}
run: |
set -euo pipefail
# Cleanup must not be skippable: the branch contains fake credentials and code
# that exists only to fail review.
if [[ -n "${PR:-}" ]]; then
gh pr close "$PR" --repo "$TARGET_REPO" --delete-branch --comment "Scored automatically by the Redline seed canary. Closing." || true
fi
if [[ -n "${BRANCH:-}" ]]; then
gh api -X DELETE "repos/$TARGET_REPO/git/refs/heads/$BRANCH" >/dev/null 2>&1 || true
fi
echo "cleanup done"
- uses: actions/upload-artifact@v4
if: always()
with:
name: seed-score-${{ strategy.job-index }}
path: scores/
if-no-files-found: ignore
record:
needs: score
if: always() && needs.score.result != 'skipped'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/download-artifact@v4
with:
path: scores
merge-multiple: true
- name: Append scores and fail on regression
run: |
set -euo pipefail
mkdir -p data
touch data/seed-scores.jsonl
shopt -s nullglob
files=(scores/*.json)
if [[ ${#files[@]} -eq 0 ]]; then
echo "::error::No score artifacts were produced — every canary run failed before scoring."
exit 1
fi
regressed=0
for file in "${files[@]}"; do
jq -c . "$file" >> data/seed-scores.jsonl
repo=$(jq -r .repo "$file")
recall=$(jq -r .totals.blocker_recall "$file")
fps=$(jq -r .totals.false_positives_on_clean "$file")
attribution=$(jq -r .totals.rule_attribution "$file")
echo "- $repo: BLOCKER recall $recall, rule attribution $attribution, false positives $fps" >> "$GITHUB_STEP_SUMMARY"
awk -v r="$recall" 'BEGIN { exit (r >= 1 ? 0 : 1) }' || { echo "::error::$repo BLOCKER recall $recall is below 1.0"; regressed=1; }
[[ "$fps" -gt 0 ]] && { echo "::error::$repo produced $fps false positive(s) on the clean corpus"; regressed=1; }
done
git config user.name "redline-bot"
git config user.email "redline-bot@users.noreply.github.com"
git add data/seed-scores.jsonl
git commit -q -m "data: seed canary scores $(date -u +%Y-%m-%d)" || echo "nothing to commit"
git pull --rebase origin "${GITHUB_REF_NAME}"
git push origin "HEAD:${GITHUB_REF_NAME}"
exit "$regressed"