Skip to content

Template Drift Scan #21

Template Drift Scan

Template Drift Scan #21

Workflow file for this run

# Weekly fleet-wide template-drift scan. Iterates over every repository that
# consumes a netresearch/.github template, compares its .github/ tree to the
# matching template, and opens/updates one issue per drifting repo.
#
# The fleet is discovered, not listed: a repository declares its membership by
# carrying .github/template.yaml, and scripts/list-consumers.py asks the
# organisation for those. The matrix used to be written out here and mirrored by
# hand in scripts/sync-all-consumers.sh; both had drifted to six Go repos while
# the fleet had grown to 57 across five templates, so this scan was watching a
# ninth of what its own comment claimed.
#
# This is a monitoring workflow — informational only, it does not block
# any PR. Per-repo enforcement is handled by check-template-drift.yml
# which runs on PR events in each consuming repo.
name: Template Drift Scan
on:
schedule:
# Monday 06:00 UTC — surfaces drift at start of week for triage.
- cron: "0 6 * * 1"
workflow_dispatch:
inputs:
template:
description: "Only scan consumers of this template (go-app, go-lib, php-module, skill, typo3-extension). Empty: the whole fleet."
required: false
type: string
default: ""
permissions: {}
jobs:
discover:
name: Discover consumers
runs-on: ubuntu-latest
timeout-minutes: 5
permissions:
contents: read
outputs:
matrix: ${{ steps.list.outputs.matrix }}
count: ${{ steps.list.outputs.count }}
steps:
- name: Harden Runner
uses: step-security/harden-runner@05e31511f85b41b11d1cf0ef85d0992719546e2c # v2.21.0
with:
egress-policy: audit
- name: Checkout
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
with:
persist-credentials: false
- name: List consumers
id: list
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
TEMPLATE_FILTER: ${{ inputs.template }}
run: |
set -euo pipefail
# list-consumers.py exits non-zero on an empty fleet, so a discovery
# failure stops the run here rather than producing an empty matrix
# that would report a clean scan of nothing.
MATRIX=$(python3 scripts/list-consumers.py --json \
${TEMPLATE_FILTER:+--template "$TEMPLATE_FILTER"})
COUNT=$(printf '%s' "$MATRIX" | python3 -c 'import json,sys; print(len(json.load(sys.stdin)["include"]))')
echo "matrix=$MATRIX" >> "$GITHUB_OUTPUT"
echo "count=$COUNT" >> "$GITHUB_OUTPUT"
echo "::notice::Scanning $COUNT consumer(s)."
scan:
name: Scan consumer
needs: discover
runs-on: ubuntu-latest
timeout-minutes: 10
permissions:
contents: read
issues: write
strategy:
fail-fast: false
# The fleet is 57 repositories on a free-plan job quota. Starting
# them all at once starves the pool — during one bulk campaign that
# left merge-queue entries waiting for a runner until they timed
# out. Six at a time keeps a weekly scan out of everyone's way.
max-parallel: 6
matrix: ${{ fromJSON(needs.discover.outputs.matrix) }}
steps:
- name: Harden Runner
uses: step-security/harden-runner@05e31511f85b41b11d1cf0ef85d0992719546e2c # v2.21.0
with:
egress-policy: audit
- name: Checkout templates
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
with:
persist-credentials: false
path: templates-src
- name: Checkout consumer repo
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
with:
repository: ${{ matrix.repo }}
persist-credentials: false
path: consumer
- name: Run drift check
id: drift
continue-on-error: true
env:
CONSUMER_DIR: ${{ github.workspace }}/consumer
TEMPLATE_DIR: ${{ github.workspace }}/templates-src/templates/${{ matrix.template }}
run: |
set +e
OUTPUT=$(bash "$GITHUB_WORKSPACE/templates-src/scripts/check-drift.sh" "$CONSUMER_DIR" "$TEMPLATE_DIR" 2>&1)
STATUS=$?
{
echo "status=$STATUS"
# Preserve multi-line output via GHA heredoc syntax.
echo "output<<DRIFT_EOF"
echo "$OUTPUT"
echo "DRIFT_EOF"
} >> "$GITHUB_OUTPUT"
exit 0
- name: Open or update drift issue
if: steps.drift.outputs.status != '0'
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
TARGET_REPO: ${{ matrix.repo }}
TEMPLATE_NAME: ${{ matrix.template }}
DRIFT_OUTPUT: ${{ steps.drift.outputs.output }}
run: |
set -euo pipefail
TITLE="Template drift: ${TARGET_REPO#*/} vs ${TEMPLATE_NAME}"
BODY=$(cat <<MARKDOWN
## Template drift detected
Repo: \`$TARGET_REPO\`
Template: \`$TEMPLATE_NAME\` (netresearch/.github)
Scan date: $(date -u +"%Y-%m-%dT%H:%M:%SZ")
Auto-opened by \`.github/workflows/drift-scan.yml\`. Resolve by running
\`scripts/sync-template.sh $TEMPLATE_NAME $TARGET_REPO\`, then opening
the generated PR in the consumer repo.
If the drift is intentional, add an entry to \`$TARGET_REPO\`'s
\`.github/template.yaml\` \`intentional-drift:\` list and close this issue.
### Drift output
\`\`\`
$DRIFT_OUTPUT
\`\`\`
MARKDOWN
)
# Find existing open drift issue for this repo; update it if present.
EXISTING=$(gh issue list --repo "$GITHUB_REPOSITORY" --search "$TITLE in:title" --state open --json number --jq '.[0].number // empty')
if [ -n "$EXISTING" ]; then
gh issue edit "$EXISTING" --repo "$GITHUB_REPOSITORY" --body "$BODY"
echo "Updated issue #$EXISTING"
else
gh issue create --repo "$GITHUB_REPOSITORY" --title "$TITLE" --body "$BODY" --label "template-drift" || \
gh issue create --repo "$GITHUB_REPOSITORY" --title "$TITLE" --body "$BODY"
fi
- name: Close stale drift issue
if: steps.drift.outputs.status == '0'
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
TARGET_REPO: ${{ matrix.repo }}
TEMPLATE_NAME: ${{ matrix.template }}
run: |
set -euo pipefail
TITLE="Template drift: ${TARGET_REPO#*/} vs ${TEMPLATE_NAME}"
EXISTING=$(gh issue list --repo "$GITHUB_REPOSITORY" --search "$TITLE in:title" --state open --json number --jq '.[0].number // empty')
if [ -n "$EXISTING" ]; then
gh issue close "$EXISTING" --repo "$GITHUB_REPOSITORY" --comment "Drift resolved — closed automatically by drift-scan on $(date -u +"%Y-%m-%dT%H:%M:%SZ")."
echo "Closed stale issue #$EXISTING"
fi