fix(ci): keep the freshness signal alive and the push resilient
`lastConfirmedAt` lives in gitignored bookkeeping that only refresh.yml restored, so the workflow that actually builds and deploys never saw it: every source reported its last *content change* as its last success, and the UI flagged anything whose bytes had not moved in two days as stale — which is most wiki pages most of the time. ci.yml now restores the same cache read-only before building the feed. The refresh push was a bare `git push`, so a human push landing in between made it non-fast-forward: the job failed and threw away pages it had just fetched, while the bookkeeping had already been saved, so those sources would not be re-asked for six hours. Rebase and retry instead — never force. The cache save key used run_id, which is stable across re-runs, so a re-run saved nothing and the run after it restored stale bookkeeping. Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
This commit is contained in:
co-authored by
Claude Opus 5
parent
c85ec0b2d1
commit
831fcbe6c2
@@ -28,6 +28,20 @@ jobs:
|
||||
|
||||
- run: bun install --frozen-lockfile
|
||||
|
||||
# `lastConfirmedAt` — the "we asked the wiki and it said this is still
|
||||
# current" half of the freshness badge — lives in the gitignored
|
||||
# snapshots/*.state.json, so without this the feed built here has only
|
||||
# `contentChangedAt` to go on and every source reads as stale two days
|
||||
# after its bytes last moved, which for a wiki page is most of the time.
|
||||
# Restore-only: refresh.yml owns writing this cache, and a miss just
|
||||
# returns the pre-existing fallback behaviour.
|
||||
- name: Restore refresh bookkeeping
|
||||
uses: actions/cache/restore@v4
|
||||
with:
|
||||
path: snapshots/*.state.json
|
||||
key: refresh-state-
|
||||
restore-keys: refresh-state-
|
||||
|
||||
- name: Typecheck
|
||||
run: bun run typecheck
|
||||
|
||||
@@ -65,6 +79,16 @@ jobs:
|
||||
with:
|
||||
bun-version: "1.3"
|
||||
- run: bun install --frozen-lockfile
|
||||
|
||||
# This is the job whose output is deployed, so this is the one that must
|
||||
# see the refresh bookkeeping; see the same step in `check`.
|
||||
- name: Restore refresh bookkeeping
|
||||
uses: actions/cache/restore@v4
|
||||
with:
|
||||
path: snapshots/*.state.json
|
||||
key: refresh-state-
|
||||
restore-keys: refresh-state-
|
||||
|
||||
- name: Build
|
||||
# Pages serves from /<repo>/, so the app is built with a matching base
|
||||
# href. Built at the domain root it would 404 on every asset.
|
||||
|
||||
@@ -67,7 +67,7 @@ jobs:
|
||||
uses: actions/cache/restore@v4
|
||||
with:
|
||||
path: snapshots/*.state.json
|
||||
key: refresh-state-${{ github.run_id }}
|
||||
key: refresh-state-
|
||||
restore-keys: refresh-state-
|
||||
|
||||
- name: Refresh
|
||||
@@ -88,12 +88,16 @@ jobs:
|
||||
fi
|
||||
bun run refresh "${args[@]}"
|
||||
|
||||
# The key must differ every time this step runs. `run_id` is stable across
|
||||
# re-runs, so a re-run's save hits an existing key, is skipped, and the
|
||||
# next run restores the bookkeeping from before the re-run — records of
|
||||
# requests we did make, lost. `run_attempt` increments per attempt.
|
||||
- name: Save refresh bookkeeping
|
||||
if: always()
|
||||
uses: actions/cache/save@v4
|
||||
with:
|
||||
path: snapshots/*.state.json
|
||||
key: refresh-state-${{ github.run_id }}
|
||||
key: refresh-state-${{ github.run_id }}-${{ github.run_attempt }}
|
||||
|
||||
# git is the authority on "did anything change" — a 304, an unchanged
|
||||
# body, or a rejected parse all leave the working tree clean.
|
||||
@@ -108,15 +112,40 @@ jobs:
|
||||
echo "changed=false" >> "$GITHUB_OUTPUT"
|
||||
fi
|
||||
|
||||
# A human push landing between the checkout and this push makes the push
|
||||
# non-fast-forward. Failing there would throw away pages we have already
|
||||
# fetched while the bookkeeping above (saved with `if: always()`) has
|
||||
# already spent their six-hour budget — the wikis would be asked again for
|
||||
# nothing. So rebase onto whatever landed and try again. Never force: this
|
||||
# commit is only ever new files under snapshots/, so it has nothing to say
|
||||
# about anyone else's work.
|
||||
- name: Commit refreshed snapshots
|
||||
if: steps.diff.outputs.changed == 'true' && inputs.dry_run != true
|
||||
env:
|
||||
BRANCH: ${{ github.ref_name }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
git config user.name "github-actions[bot]"
|
||||
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
|
||||
git add -- snapshots
|
||||
git commit -m "chore(data): refresh source snapshots" \
|
||||
-m "Automated fetch from ${{ github.workflow }} run ${{ github.run_id }}."
|
||||
git push
|
||||
|
||||
for attempt in 1 2 3; do
|
||||
if git push origin "HEAD:$BRANCH"; then
|
||||
exit 0
|
||||
fi
|
||||
echo "push rejected (attempt $attempt); rebasing onto origin/$BRANCH"
|
||||
git fetch origin "$BRANCH"
|
||||
if ! git rebase "origin/$BRANCH"; then
|
||||
git rebase --abort || true
|
||||
echo "::error::snapshot commit conflicts with $BRANCH; not force-pushing"
|
||||
exit 1
|
||||
fi
|
||||
sleep $((attempt * 5))
|
||||
done
|
||||
echo "::error::could not push refreshed snapshots after 3 attempts"
|
||||
exit 1
|
||||
|
||||
# ci.yml owns typecheck, tests, the feed sanity check, the image and the
|
||||
# Pages deploy. Dispatching it is how the refreshed data reaches the site
|
||||
|
||||
Reference in New Issue
Block a user