Skip to content

Data pipeline: fetch, ingest, score and open one checked PR; stop the nowcast's silent no-op - #221

Draft
vahid-ahmadi wants to merge 6 commits into
mainfrom
vahid/r5-pipeline
Draft

vahid-ahmadi wants to merge 6 commits into
mainfrom
vahid/r5-pipeline

Conversation

@vahid-ahmadi

@vahid-ahmadi vahid-ahmadi commented Nov 6, 2026 •

Copy link
Copy Markdown
Contributor

The forecast track record has not moved since 26 August, although UK GDP 2026Q2 and CPI 2026Q3 are both out. This PR fixes the pipeline so that a scheduled refresh goes fetch → ingest → score → one PR that has already passed every gate. It does not touch data/vintages/, forecasts/rounds/, forecasts/outturns.json or forecasts/scorecard.json. Those files only change when the workflow itself runs.

What was wrong (evidence)

workflow last success what it is doing
Fetch data vintages 23 Oct (run 39919833434) red every weekday since 26 Oct
Archive a forecast round (nowcast) 5 Nov green, but a no-op every day since 17 Sep
forecast-archive / data-immutability / site-provenance / site-tests on main 30 Oct green (main last moved with #198)
  1. Refresh official data vintages #195 has never had a CI run. Head 18442ea has Vercel checks only. The bot pushes with GITHUB_TOKEN, and GitHub does not start pull_request workflows for those pushes. None of the append-only, provenance or archive gates has ever run on a bot refresh. Main has no protection or auto-merge, so the PR just sits there.
  2. Merging Refresh official data vintages #195 as it stands would turn main red. I checked out 18442ea and ran every PR-time check. All pass except forecasts/ingest_outturns.py --check:
    FAIL missing outturn cpi 2026Q3 (3.10, vintage 2026-11-05) and FAIL missing outturn gdp 2026Q2 (1.37, vintage 2026-11-05). forecast-archive runs that check on every push to main.
  3. Scoring never runs. fetch-vintages.yml ran fetch → notes → pages → PR. It never called ingest_outturns.py or score.py. Those scripts only appear as --check gates in forecast-archive.yml, which (see 1) never runs on the bot PR.
  4. Since 26 Oct the PR step fails: ##[error]GitHub Actions is not permitted to create or approve pull requests. peter-evans/create-pull-request tries to create the PR first and only updates on a 422 "already exists". Up to 23 Oct the log shows Updated pull request #195. Now the create call returns 403 before the update is tried. The repo API shows can_approve_pull_request_reviews: false. The org policy is not readable with this token, so I can't see what changed on 24–25 Oct. The push happens before that step, so Refresh official data vintages #195's head still moved every day.
  5. Daily snapshots were thrown away. The action resets the branch onto main and force-pushes it, so while Refresh official data vintages #195 waited, only the latest day survived. The 27 Aug to 4 Nov snapshots exist only in 50 orphaned commits (Daily data vintages from 27 Aug to 4 Nov 2026 exist only in orphaned bot commits: archive or recover them before GitHub drops them #222).
  6. The nowcast "succeeds" by doing nothing. nowcast.py --due reads next_release from data/latest on main, which dates from 26 Aug: GDP "13 August 2026", unemployment "15 September", CPI "16 September". Run 41972099442 prints no scored series releases within 3 days of 2026-11-05 and exits green.

Outturns now published but not scored: GDP 2026Q2 y/y 1.37% (07-29 round median 1.135), CPI 2026Q3 3.1% (07-29 median 2.539), and unemployment 2026Q2 4.9% (satellite median 4.9714; this one is already on main but MAPPING has no unemployment row, which #200 fixes). A dry run of ingest + score on a scratch copy of #195 took the record from 1 to 5 scored forecasts, with next period 2026Q3.

The fix

  • data/refresh.py: one driver with a fixed order: carry → fetch → notes → ingest → score → fans → pages → gates → append-only. GATES lists every check from forecast-archive, site-provenance, data-immutability and site-tests. A test fails if one of those workflows gains a check that GATES lacks, so the bot PR is green before anyone looks at it.
    • carry: copies the vintage files that the open refresh branch has and main lacks, but only if they are newer than main's newest snapshot of that series. The branch then builds up dated snapshots until it is merged, instead of being overwritten each day. Only data/vintages/ files are carried. MANIFEST, latest/, notes, pages, outturns and the scorecard are rebuilt from them, so a stale branch never conflicts with main.
    • notes: calls notes/generate_release_notes.py --vintage <date> for each new vintage date, and the gate calls --check. Main's per-series generator and Redesign the static site: Autumn Budget look, one fact per home, readable layouts #200's per-date digest generator both accept these two flags, so this step works whichever is merged. generate_release_notes.py itself is untouched.
  • .github/scripts/open-bot-pr.sh replaces peter-evans/create-pull-request in both bot workflows. It looks for an open PR first and updates it over REST, so Refresh official data vintages #195 keeps updating even with the Actions PR setting off. Only a brand-new PR needs that permission, and if it is missing the job says what to change. The script refuses any branch outside data/* and forecasts/*, so it can never push to main. It pushes with force-with-lease against the tip it saw, and does nothing when nothing changed.
  • fetch-vintages.yml: runs the driver, writes a PR body listing new vintages, outturns added and scorecard status, then calls the script. New workflow_dispatch inputs branch, carry_from and open_pr let you test into a throwaway data/... branch without touching Refresh official data vintages #195. An optional DATA_PR_TOKEN secret is used for checkout and PR calls when set; with it, bot pushes also trigger PR-time CI.
  • nowcast.py --due now exits 2 when every announced release is in the past ("stale release calendar … merge the data vintage PR"). nowcast-round.yml fails on 2 instead of reporting a green no-op. It also uses the PR script.
  • Not included: the MAPPING unemployment row. Redesign the static site: Autumn Budget look, one fact per home, readable layouts #200 already adds it along with the matching outturns.json/scorecard regeneration. Adding it here would force a scorecard regeneration that conflicts with Redesign the static site: Autumn Budget look, one fact per home, readable layouts #200's rewritten score.py.

Tests

  • tests/test_refresh_pipeline.py (17): step order; ingest and score run in write mode; GATES ⊇ every PR-workflow check; the notes step uses only flags both generators accept; the fetch workflow runs the driver before the PR step and no longer uses peter-evans; the nowcast stale branch exits 1; carry keeps newer snapshots and skips older ones or ones main already has; the PR script refuses main, master, feature/x and vahid/...; the PR script looks up the existing PR before creating one.
  • forecasts/tests/test_nowcast_due.py (6): all-past is stale, one future date is not, a release today is not, an empty calendar is stale, due() is unchanged, and the CLI exits 2.
  • Fail-before: these tests were run against origin/main with only data/refresh.py copied in. 7 of the 17 pipeline tests fail (both workflow tests, the four branch-refusal cases and the update-before-create test), and 5 of the 6 nowcast tests fail or error. All 23 pass on this branch. PR CI is green.
  • End-to-end on GitHub (run 42148395148): workflow_dispatch on this branch with branch=data/r5-pipeline-test, carry_from=data/vintage-refresh, open_pr=false. It carried Refresh official data vintages #195's 20 snapshots from 5 Nov and fetched 5 new ones for 6 Nov (Bank Rate, three gilts, US 10y). It wrote notes for both dates and added cpi 2026Q3 = 3.10 and gdp 2026Q2 = 1.37 (vintage 2026-11-05), taking the scorecard from 1 to 4 scored forecasts. Every gate passed, including the 1974 site tests, ingest --check and the append-only check. It then pushed only the test branch: no M/D/R under data/vintages/ or forecasts/rounds/. I then tested the path step 3 below will take, after adding a fix that leases the push on the tip the job started from. Run 42148913882 recreated the test branch. Run 42149008458 carried from that same branch: 25 files carried (the 5 Nov and 6 Nov snapshots kept), nothing new written, and a force-with-lease update d27a861...89358de. Both green. The test branch is deleted. Nothing touched main or Refresh official data vintages #195. The REST update of an existing PR (PATCH /pulls/195) is the one step not exercised, because exercising it would edit Refresh official data vintages #195.

What you need to do to get the record current

GDP 2026Q3 is published 12 Nov, unemployment 17 Nov and CPI 18 Nov. The archived rounds already forecast 2026Q3, so each release adds scored forecasts once the data is on main.

  1. Allow the bot to open PRs (Bot PRs: GitHub Actions can no longer open PRs here (since 26 Oct), and bot pushes never trigger CI; choose a token or re-enable the setting #223). Either tick Settings → Actions → General → "Allow GitHub Actions to create and approve pull requests" (if it is greyed out, an org owner has to allow it), or add a DATA_PR_TOKEN secret. The token is better, because it also makes CI run on bot PRs. Step 3 works without either, since Refresh official data vintages #195 already exists. Every later refresh needs one of them.

  2. Merge this PR.

  3. Actions → Fetch data vintages → Run workflow on main with the defaults. It can only push data/vintage-refresh. Refresh official data vintages #195 then gets the carried 5 Nov snapshots plus today's, the outturns, the scorecard and the pages, all checked inside the job. Read the "What this refresh contains" block in Refresh official data vintages #195's new body.

  4. Without a token, close and reopen Refresh official data vintages #195 so its PR checks run (a human reopened event does start workflows). Then merge it, ideally before Thu 12 Nov.

  5. Then rebase Redesign the static site: Autumn Budget look, one fact per home, readable layouts #200 on main:

    The reverse order (Redesign the static site: Autumn Budget look, one fact per home, readable layouts #200 first, then steps 2–4) also works, because the bot rebuilds from main. The record just stays stale until Redesign the static site: Autumn Budget look, one fact per home, readable layouts #200 lands.

  6. New rounds before the 12 Nov release are not possible as deployed: the hosted forecast_uk reads boe-var-model at the pinned 42cbcbb, whose data/boe_var_data.csv ends in 2026Q1. Refreshing the data store does not change that, so nowcast.py will refuse with "data edge 2026Q1 is already archived" (as it did on 13 Aug and 19 Aug). See Nowcast can't archive a new round: the hosted forecast_uk is pinned to boe-var data ending 2026Q1 #224.

Also noted (no issue opened)

score.py decides whether a forecast predates its outturn by comparing archived_utc with the store vintage, not with the publisher's release date (release_updated). The one entry scored today, the 07-21 round's CPI 2026Q2, passes only because the archive date and the vintage are the same day (25 Jul), even though ONS released the figure on 22 Jul. The entry is still legitimate: the artifact it archives (sha256 991ba7f…) was committed to main in b66cadf at 14:54 on 21 Jul, before the release. The guard is loose, but no published score is wrong.

Not done

  • Nothing merged. No run that writes to main has been triggered.
  • data/vintages/, rounds, outturns and scorecard are unchanged.
  • The orphaned snapshots have not been recovered; that needs your decision.

Nowcast edge gate (#224, commit d980660)

nowcast.py --edge-gate reads the hosted boe-var-model data edge from GitHub at BOE_REVISION and skips the round, with no Modal call, when that edge is not after the newest archived data_edge. Today it skips (2026Q1 hosted, 2026Q2 required). This saves about $0.15 of Modal time per due day (~$1.30 in November). Tests: forecasts/tests/test_nowcast_edge_gate.py (9, all fail before). No published number changes.

🤖 Generated with Claude Code

…at CI has already checked

fetch-vintages.yml now runs data/refresh.py: carry -> fetch -> notes ->
ingest -> score -> fans -> pages -> every PR-time gate -> append-only check,
then pushes the bot branch and opens or updates one PR. Ingest and score never
ran in this job before, so new outturns (GDP 2026Q2, CPI 2026Q3) could not
reach the record and #195 could not merge without turning main red.

- Carry vintages from the open refresh branch so daily snapshots stop being
  overwritten while the PR waits (27 Aug-4 Nov 2026 were lost this way).
- Replace peter-evans/create-pull-request, which tries to create before it
  updates and has failed with 403 since 26 Oct, with a script that updates an
  existing PR first and refuses any branch outside data/* and forecasts/*.
- nowcast.py --due exits 2 when every announced release is in the past; the
  nowcast job now fails on that instead of reporting a green no-op.
- Optional DATA_PR_TOKEN secret so bot pushes trigger PR-time CI.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@vercel

vercel Bot commented Nov 6, 2026 •

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated
macromod Ready Ready Preview Nov 6, 2026 3:51am UTC

Request Review

…sh time

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…e is already archived

nowcast.py --edge-gate reads BOE_REVISION from integration/modal_app.py,
fetches boe-var-model's data/boe_var_data.csv at that commit from GitHub,
and exits 1 when its last quarter is not after every archived boe-svar
round's data edge. nowcast-round.yml runs it after --due and before the
MCP install, so a due day with a stale hosted model logs a notice and
stays green instead of a ~5.5-minute cold Modal solve that write_round
would refuse as already archived (#224). An unreadable edge proceeds.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@vahid-ahmadi

Copy link
Copy Markdown
Contributor Author

Merge order with the redesign stack (#200): simulated 6 Nov

Recommendation: if #200 can land by about 11 Nov, merge it first. Then this PR → token/setting (#223) → #228 recovery → dispatch fetch-vintages → close/reopen #195 for CI → merge #195 before 12 Nov. Otherwise keep the current order (this PR → dispatch → #195), and #200 takes the rebase steps in its comment.

Findings (local scratch worktrees; nothing pushed or merged):

Side notes (nothing needed to merge):

  • Redesign the static site: Autumn Budget look, one fact per home, readable layouts #200 adds a multi-line run: | check to site-provenance.yml (npm ci + tools/prerender_math.py --check). test_driver_runs_every_check_the_pr_workflows_run only parses single-line run: python3, so it doesn't see that check, and GATES doesn't run it. Harmless for a data refresh.
  • The pages step doesn't run assets/make_current_outlooks.py, but GATES checks it. It passed here. If a refresh ever changes an outlook input, the job stops at the gate instead of regenerating.

No pre-emptive commits: all conflicts are generated output.

vahid-ahmadi and others added 3 commits November 6, 2026 03:40
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…stacks

Both stacks add entries; a shared heading with a separator line lets them
merge in either order without a CHANGELOG.md conflict.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Merges the shared anchor (84e9539); the entry now sits below the separator
line, so it merges with #200's in either order.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
vahid-ahmadi added a commit that referenced this pull request Nov 6, 2026
Merges the shared [Unreleased] anchor (84e9539) and lists every
redesign-stack PR's bullets here, by PR number. The stacked PRs no longer
edit CHANGELOG.md, so they stop conflicting with each other, and the
pipeline stack (#221, #228) adds its entries below the separator line.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
vahid-ahmadi added a commit that referenced this pull request Nov 6, 2026
…ne group

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

This branch was successfully deployed

1 active deployment
Preview — 115128e8 Deployed Nov 6, 2026 by vercel[bot]
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant