Skip to content

Refresh status table #292

Refresh status table

Refresh status table #292

name: Refresh status table
# Two-tier status refresh for the morning-triage workflow:
#
# 1. profile/README.md (this repo) — public org-wide table showing
# every public repo's tests + today's run tally. The org page at
# https://github.com/erphq surfaces this directly.
#
# 2. STATUS.md in each org repo that has one — per-repo "Today's CI"
# block (between BEGIN/END markers) so an agent that's already
# landed in a specific repo sees today's run/pass/fail/latest-
# failure-link without leaving the file they're reading.
#
# Cadence: hourly. Light: a couple dozen gh-api reads + one edit per
# repo that actually changed.
#
# Auth: the per-repo update step writes to OTHER repos in the org, so
# it needs a PAT scoped to `repo` (or a fine-grained PAT with
# Contents: Write on org repos). Store it as the repo secret
# `ORG_REFRESH_PAT`. Without it, the per-repo refresh job is skipped
# (the org-table job still works on the default GITHUB_TOKEN since
# it only reads + edits this repo).
on:
schedule:
# Every hour at :05 — staggered off :00 so it doesn't pile onto
# the busiest minute of cron-time across GH Actions.
- cron: '5 * * * *'
workflow_dispatch: {}
push:
branches: [main]
paths:
- 'scripts/build-status-table.sh'
- 'scripts/build-repo-status.sh'
- 'scripts/update-repo-status.sh'
- 'scripts/inject-ci-today.py'
- '.github/workflows/refresh-status.yml'
permissions:
contents: write
concurrency:
group: refresh-status
cancel-in-progress: false
jobs:
refresh:
runs-on: ubuntu-latest
timeout-minutes: 10
steps:
- uses: actions/checkout@v6
- name: Build status table
env:
# Prefer the cross-repo PAT; fall back to the workflow's
# default token (which can only see this repo's runs).
GH_TOKEN: ${{ secrets.ORG_REFRESH_PAT || secrets.GITHUB_TOKEN }}
ORG: erphq
run: |
bash scripts/build-status-table.sh > /tmp/status-table.md
echo "--- preview ---"
head -20 /tmp/status-table.md
- name: Inject + commit + push (race-safe)
run: |
git config user.name 'github-actions[bot]'
git config user.email '41898282+github-actions[bot]@users.noreply.github.com'
# Inject the freshly-built table into profile/README.md by
# replacing whatever sits between the BEGIN/END markers.
# Because the source is the marker block (not a 3-way merge),
# this is safe to repeat after a pull — the previous content
# is just overwritten verbatim.
inject() {
python3 <<'PY'
import re, pathlib
table = pathlib.Path('/tmp/status-table.md').read_text().rstrip()
path = pathlib.Path('profile/README.md')
src = path.read_text()
new = re.sub(
r'(<!-- BEGIN: ci-status -->\n).*?(\n<!-- END: ci-status -->)',
lambda m: m.group(1) + table + m.group(2),
src,
flags=re.S,
)
if new == src and '<!-- BEGIN: ci-status -->' not in src:
raise SystemExit('marker block not found — aborting')
path.write_text(new)
PY
}
# Pull-then-inject-then-push, retrying the whole loop on race.
# We re-inject after every pull so the marker block always
# reflects the freshly-built table on top of the latest main —
# no rebase conflict is possible because the only edits to
# this region come from this workflow.
for attempt in 1 2 3; do
git fetch origin main
git reset --hard origin/main
inject
if git diff --quiet profile/README.md; then
echo "No changes — table already up-to-date."
exit 0
fi
git add profile/README.md
git commit -m "chore: refresh org CI status table"
if git push; then exit 0; fi
echo "Push lost a race on attempt $attempt; pulling and re-injecting…"
sleep 5
done
echo "::error::push failed after 3 attempts"
exit 1
refresh-per-repo:
# Updates each org repo's STATUS.md "Today's CI" block.
# Independent of the org-table job above so a per-repo failure
# doesn't take down the org-page refresh.
runs-on: ubuntu-latest
timeout-minutes: 15
# Only runs when ORG_REFRESH_PAT is configured — without it we
# can't write to the other org repos (default GITHUB_TOKEN is
# repo-scoped to .github only).
if: ${{ vars.RUN_PER_REPO_REFRESH != 'false' }}
steps:
- uses: actions/checkout@v6
- name: Update each repo's STATUS.md
env:
GH_TOKEN: ${{ secrets.ORG_REFRESH_PAT || secrets.GITHUB_TOKEN }}
ORG: erphq
run: |
if [[ -z "${{ secrets.ORG_REFRESH_PAT }}" ]]; then
echo "ORG_REFRESH_PAT not set — per-repo refresh would only be"
echo "able to read/write this .github repo. Skipping."
exit 0
fi
# Discover repos that have a STATUS.md at root. Survey is
# cheap (one /contents/STATUS.md HEAD per repo) and means
# we don't have to maintain a hand-curated list — a new
# repo gets the block as soon as it has STATUS.md.
repos=$(gh api "orgs/$ORG/repos" --paginate -q '.[] | select(.archived | not) | .name')
for repo in $repos; do
if gh api "repos/$ORG/$repo/contents/STATUS.md" -q .name >/dev/null 2>&1; then
echo "::group::$repo"
# Each invocation is independent — a single repo failure
# shouldn't break the rest. Use `|| true` to swallow.
bash scripts/update-repo-status.sh "$repo" || echo "::warning::$repo update failed"
echo "::endgroup::"
fi
done