Repository navigation
Move the data pipeline to GitHub Actions and Cloudflare R2 - #2
nofurtherinformation wants to merge 10 commits into
Conversation
clean_calpip now streams with DuckDB instead of loading every year into pandas: 7 years take 7 s and 1.2 GB (capped) instead of 200 s and 11 GB. Output matches the previous calpip_full on all 31.2M rows except the township ID, which now comes straight from COMTRS. That places 1,405 rows (182,696 lbs) the geopandas join dropped and fixes 18 malformed IDs. Uploaded zips are checked for the CalPIP columns the pipeline needs. parse_meta reads only the 5 columns it uses (9.3 GB -> 6.0 GB, identical output) and creates data/meta itself. output_census: fix a path that divided two strings and a relative path. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ables build_r2 writes per-geography parquet (detail and summary), demography, filter lookups and a manifest. The FE will query them in the browser with DuckDB-WASM. fe-migration/ has the FE plan, reference SQL for every FE query, and 25 snapshots of the live nectr API; the reference SQL matches all of them. join_census_units and the S3 helpers only fed nectr, so they go. The CLI imports only the step it runs, so the pipeline needs just duckdb, pandas, pyarrow and openpyxl. compare_builds prints yearly totals against the live build for the job summary. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Build data syncs raw/ from R2, builds, uploads an immutable version folder, points preview.json at it, and compares yearly totals with the live build. Publish data points manifest.json at a build (the latest by default, or any earlier version to roll back). The FE reads the manifest on load, so data updates need no FE rebuild. The whole run fits a standard GitHub runner, so the Batch job, Docker image and ECR scripts are removed. The AWS resources themselves are untouched until the FE switches over. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Replaces the March 2025 AWS guide: requesting a year from CalPIP, uploading the zip to R2 (with Cyberduck for files the dashboard can't handle), and running Build data and Publish data. docs/guide/make_guide.py regenerates the PDF. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
From the review of this PR (all verified by running): - clean: extract uploaded zips to a fixed file name, so the name inside a zip never reaches SQL; read records split by a line break (the 2021 export has one) the way pandas did instead of crashing. - check_clean.py: one small assert script on fake data covering the zip path (column check, newest zip wins, split records, 'nan' and code cleaning, HM/MDM/SBM townships). Build data runs it first. - parse_meta: name products, sites and chemicals that the PUR code tables don't have yet from the CalPIP exports (24 products, 3 sites today). compare flags chemicals missing from PAN's category sheet. - reference_queries: NaN now counts as a mismatch; escape every literal; report the township change as expected after v1. - workflows: least-privilege permissions, separate concurrency groups, download only the inputs the pipeline reads, accurate publish hints. Restore the old Batch trigger (action bumped to v4) until the FE moves. - Pin duckdb/pandas/pyarrow/openpyxl to the verified versions; add a format field to the manifest. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- RUNBOOK.md: the bucket's current state, one-time setup (inputs under raw/, manifests, cache headers, CORS, keys), FE cutover, AWS teardown, routine work and fixes. - Guide: upload with Cyberduck and a bucket-scoped S3 key (no Cloudflare account role needed), transition note, publish by version name, built-in fonts so it regenerates anywhere. - PLAN.md: repo paths instead of a local one, v1 status, manifest format check, the self-hosted DuckDB-WASM bundle, preview wording. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Inputs copied under raw/, manifest.json / preview.json / v1/manifest.json written, immutable cache headers on v1/, and a rehearsal of Build data against R2 that matched v1 at 0.00% for every year. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
timhannifan
left a comment
There was a problem hiding this comment.
Claude (automated review):
Verdict: APPROVE WITH COMMENTS (posted as a comment; approving is up to a maintainer)
Overview
This PR replaces AWS Batch and the nectr API with two GitHub Actions workflows (Build data and Publish data) and static parquet files on Cloudflare R2. It also ports the clean step from pandas to DuckDB, and adds a new
build_r2step, a front-end migration spec, a runbook and a guide for PAN. It delivers what the description promises.I checked the main claims by running them, not just by reading:
check_clean.pypasses.reference_queries.pymatches all 25 snapshots of the live API against thev1build on R2.- A full rebuild from the real R2 inputs, using the workflow's own download filters, takes about 30 seconds. It matches
v1on 22 of 26 files. The rest differ exactly where the PR says they would:
- township totals, by +182,696 lbs
- product and site lookups, by +24 and +3 entries
- chemical yearly averages, by float noise in the last digit
- I also ran the old pandas cleaning and the new DuckDB cleaning on the same tricky inputs and got identical output.
The design is careful. Each build goes into its own folder that never changes. Writing the version's manifest last means a half-finished upload can never be published. Rollback is just publishing an older version. The workflow input handling is solid.
No finding blocks merge. The six Important items fall into three groups:
- Who can change what (two security notes). These are about the key and permission setup in
RUNBOOK.md, not the code. They should be settled before PAN gets any keys.- Test coverage (three notes). The tests don't cover the new aggregation code, and the one check that runs in the workflow misses a few realistic regressions. Each fix is small, and I have working drafts I can share.
- One runbook command that deletes files tracked in git.
I'd fix all six in this branch. None of them require changing the pipeline's design.
Severity summary
Severity Count High 0 Important 6 Personal preference 3 Opinion 7 The 6 Important findings are inline comments on this review. The preferences and opinions are below; none of them needs a change.
RUNBOOK.mdL23OPINION[security]A custom domain makes the whole bucket readable — Once the domain is connected, anyone who knows a path can fetch anything in the bucket, includingraw/and the legacy root folders this public repo names. That's fine if none of it is private. Is that the case? No change needed if so.cli/build_r2.pyL50PREF[correctness]Theprd_sharekey is looser than the olduse_id— Partitioning by year anduse_numberpools every nulluse_numberrow in a year into one "use", and it splits use numbers that repeat across counties. On real data that's 2 null rows plus 2 collisions in 2021, which comes to 36 lbs out of 2.37 billion, so it's negligible today. Addingcounty_cd, comtrs, lbs_prd_usedto the partition, as the old key did, would close it for future years. Your call.cli/build_r2.pyL58–60OPINION[correctness]max(prd)per cell can overcount whenprodnocleaned to'0'— A cell with a junk product number can mix different products, so an AI filter returns the whole cell's product lbs. This is rare and probably not worth handling. No action needed.cli/build_r2.pyL84OPINION[correctness]The year range comes from every row's 2-digit DATE year — One mistyped date like01-JAN-02would make the site offer 2002 onward. Would taking the range from the files' YEAR values be safer? No action needed for the current data, which gives 2017–2023.cli/build_r2.pyL47OPINION[correctness]No DuckDB memory cap is set, unlike the PR table's "2.7 GB with the cap" — Locallybuild_r2peaked at 7.0 GB. DuckDB's default limit is 80% of RAM, which is fine on a 16 GiB runner. This only affects how the PR description reads.fe-migration/reference_queries.pyL81, L108PREF[tests]EXPECTED_AFTER_V1waives any township difference, even when checkingv1itself — A fake fixture with the wrong row count and made-up values still counted as "expected". You could apply the waiver only when the base isn'tv1and still require the row count to match. Also, make sure the waiver doesn't get carried over intopnpm parity.fe-migration/reference_queries.pyL42–49OPINION[correctness]A timeseries with no detail filter is sent tosummary/, which errors — Both the usetype series without anai_typefilter and the product series without aproductfilter group by a columnsummary/doesn't have. The FE's value checks probably prevent this case, but since this file is the spec for the TypeScript port, routing onkeystoo would make it safe.fe-migration/PLAN.mdL67OPINION[security]The TS port needs more than quote escaping — The Python version is safe becausefloat()and the allowlists do real work. Spelling that out (numbers go throughNumber.isFinite;geo,seriesandusetypeare checked against allowlists) would stop a literal port from building SQL from a shared link without validation. The impact is small, since it runs in the browser against public files..github/workflows/build-data.ymlL36–37PREF[tests]check_cleanonly runs after merge, inside the manual Build — A ~10-lineon: pull_requestworkflow would catch a broken clean step in review, not when PAN runs a build..github/workflows/build-data.ymlL19–26OPINION[security]The R2 secrets are visible to every step — Today that gains an attacker nothing over an uploader key. If you split the buckets, moving the secrets to the Download, Upload and Compare steps keepspip installand the parsers from seeing a key that now has more rights.
| The last command re-uploads `v1` with the immutable cache header (about 420 MB). | ||
| 5. **Keys.** Create these under R2, Manage API tokens: | ||
| - **CI:** Object Read & Write on `pesticide-data`. In GitHub (open-spatial-lab/cpr, Settings, Secrets and variables, Actions), set the secrets `R2_ACCOUNT_ID`, `R2_ACCESS_KEY_ID` and `R2_SECRET_ACCESS_KEY`, and the variable `R2_BUCKET=pesticide-data`. Set `SITE_URL` only after the FE cutover, so Build summaries don't link to a preview the old site can't show. | ||
| - **Uploaders (PAN):** a separate Object Read & Write token on `pesticide-data`, one per person, so one can be revoked alone. Send them three values: Server (`<account id>.r2.cloudflarestorage.com`), Access Key ID, and Secret Access Key. R2 tokens can't be limited to a folder, so the guide tells uploaders to stay in `raw/calpip/`. If that's not enough, move the inputs to a second bucket and give uploaders a key for that one only. |
There was a problem hiding this comment.
Claude (automated review):
IMPORTANT[security]Uploader keys can overwrite the DuckDB-WASM code every site visitor runsEach PAN uploader gets Object Read & Write on all of
pesticide-data.PLAN.mdL58 has the site load its DuckDB worker JS and.wasmfromduckdb-wasm/in that same bucket (I confirmed it's there on R2). So one leaked Cyberduck key would let someone run their own JavaScript in every visitor's browser, put fake numbers on PAN's site, or delete old versions so rollback stops working. Build and Publish don't stand in the way. The runbook's own fallback is the cheap fix, and it's easiest before any keys go out: a second bucket for inputs that uploaders can write and CI can only read. At minimum, moveduckdb-wasm/somewhere uploader keys can't reach, and say in step 5 that the bucket holds site code.
There was a problem hiding this comment.
Claude Code:
Agreed. I went with the two-bucket split in 50e45a0.
- Build data now reads inputs from
R2_RAW_BUCKET(pesticide-data-raw, private). - Uploader tokens are scoped to that bucket only; the CI token covers both.
pesticide-datawill hold onlyv*/, the manifests andduckdb-wasm/.
RUNBOOK steps 3 and 5 cover creating the bucket and moving the inputs out. Today they're still in pesticide-data, but no uploader keys have been issued yet. I've updated the guide and README to say uploads go to pesticide-data-raw/calpip/.
| 5. **Keys.** Create these under R2, Manage API tokens: | ||
| - **CI:** Object Read & Write on `pesticide-data`. In GitHub (open-spatial-lab/cpr, Settings, Secrets and variables, Actions), set the secrets `R2_ACCOUNT_ID`, `R2_ACCESS_KEY_ID` and `R2_SECRET_ACCESS_KEY`, and the variable `R2_BUCKET=pesticide-data`. Set `SITE_URL` only after the FE cutover, so Build summaries don't link to a preview the old site can't show. | ||
| - **Uploaders (PAN):** a separate Object Read & Write token on `pesticide-data`, one per person, so one can be revoked alone. Send them three values: Server (`<account id>.r2.cloudflarestorage.com`), Access Key ID, and Secret Access Key. R2 tokens can't be limited to a folder, so the guide tells uploaders to stay in `raw/calpip/`. If that's not enough, move the inputs to a second bucket and give uploaders a key for that one only. | ||
| - Give uploaders the Write role on open-spatial-lab/cpr so they can run Actions. |
There was a problem hiding this comment.
Claude (automated review):
IMPORTANT[security]The Write role lets uploaders read repo secrets, including the old AWS keysWrite is needed to run workflows, but it also lets someone push a branch with a new workflow file. That workflow gets every repo-level secret, including
AWS_ACCESS_KEY_IDandAWS_SECRET_ACCESS_KEY.mainalso appears to have no branch protection. A hijacked PAN GitHub account would get the AWS keys. Fix: put theR2_*andAWS_*secrets in a GitHub Environment limited tomain, addenvironment:to the three jobs, and require PRs onmain. Uploaders can still run the workflows frommain.
There was a problem hiding this comment.
Claude Code:
Agreed. All three jobs (Build, Publish and the old Batch trigger) now run in a data environment, in 50e45a0.
RUNBOOK step 6 covers the settings:
- create
datawith deployment branches limited tomain; - move the
R2_*and the oldAWS_*/BATCH_*secrets into it, then delete the repo-level copies; - require pull requests on
main.
I haven't changed any settings yet. The AWS secret values can't be read back, so someone who has them needs to re-enter them.
| | New chemicals in the summary's "no class or health information" list | Add them to `AI Cat Data.xlsx` (PAN), upload it to `raw/pur/meta/`, run Build data. | | ||
| | Update PUR code tables | Optional. Product, site and chemical names missing from the `.txt` tables are taken from the CalPIP exports. Upload newer tables from DPR's annual PUR release to `raw/pur/meta/` if you want DPR's canonical names. | | ||
| | New census geography or ACS data | Locally: `pip install geopandas requests bs4`, then `python3 cli download_geo`, `intersect` and `output`. Upload the changed files under `raw/census_geos/`, `raw/census_data/` and `raw/output/`, then run Build data. | | ||
| | Run the pipeline locally | `rclone sync r2:pesticide-data/raw data`, then `python3 cli/check_clean.py && python3 cli clean && python3 cli meta && python3 cli build_r2`. The output is `data/r2/`. `python3 cli compare data/r2 <other build dir>` diffs yearly totals. | |
There was a problem hiding this comment.
Claude (automated review):
IMPORTANT[correctness]rclone sync r2:pesticide-data/raw datadeletes tracked repo files
raw/on R2 has nosections/orpur/*.json, sosyncdeletesdata/sections/sections.parquetand the fourdata/pur/*.jsonfiles that git tracks. Userclone copy r2:pesticide-data/raw data, andrclone synconly forraw/calpipintodata/calpipif you want old year files cleared.
There was a problem hiding this comment.
Claude Code:
Good catch. I confirmed that data/pur/*.json and data/sections/sections.parquet are tracked.
- The runbook now says
rclone copyand explains why. syncis suggested only forcalpip/→data/calpip, when stale years need clearing.- The workflow's
aws s3 synchas no--delete, so it never removes local files.
| got = {r[0][:4]: r for r in con.sql(f"""select monthyear, chem_code, county_cd, lbs_chm_used, lbs_prd_used, amount_planted, prodno, | ||
| site_code, usetype, MeridianTownshipRange from '{d}/calpip_full.parquet' where monthyear is not null""").fetchall()} |
There was a problem hiding this comment.
Claude (automated review):
IMPORTANT[tests]The check drops null-month rows and never counts rows, so duplicates and the DATE filter go uncheckedResults are stored in a dict keyed by year, filtered with
where monthyear is not null. Mutation runs showed these changes pass the check:
- reading every row twice, which would double every total on the site
- removing the DATE filter in
build_full- breaking the filter's
'nan'handlingThe same filter hides the second half of the split record, which has a null month and shifted junk. Fix: assert the full row list (or at least
count(*)) without the filter, add a blank-DATE row, and havebuild_fulldrop rows whose month doesn't parse (clean_calpip.pyL85:and monthyear is not null). That last change also stops a stray null month from printing a "None … removed" line in the Build summary, which the guide tells PAN to treat as a stop sign. Real data has one such row today, and it happens to join no geography. I have a hardened draft of this check that kills all four surviving mutants, and can share it.
There was a problem hiding this comment.
Claude Code:
Confirmed: all three of those mutants passed the old check.
check_clean.pynow asserts every column of the full output with no filter. It also adds a blank-DATE row and a'nan'-DATE row.build_fulldrops rows whose month doesn't parse (where monthyear is not null). This replaces the separate DATE filter.- On real data that removes exactly one row: the shifted half of the split 2021 record (0 AI lbs, 1.5 product lbs). Nothing else in the 31.2M rows changes.
The hardened check now catches all 10 mutants I ran, including yours.
| row = dict(YEAR="2024", DATE="27-FEB-24", COMTRS="34M03N03E01", AG_NONAG="AG", CHEMICAL_CODE="3551", COUNTY_CODE="34", | ||
| POUNDS_CHEMICAL_APPLIED="2.5", POUNDS_PRODUCT_APPLIED="10", AMOUNT_PLANTED="ACRES", PRODUCT_NUMBER="", | ||
| SITE_CODE="GA", USE_NUMBER="7") |
There was a problem hiding this comment.
Claude (automated review):
IMPORTANT[tests]use_number,comtrs,aerial_groundandprodchem_pctare never assertedMutants that set
use_numberto NULL, takeaerial_groundfrom the wrong column, or null outcomtrsall pass.use_numberdrives theprd_sharesplit,comtrsis the join key for four geographies, andaerial_groundis a filter on the site. Add the four columns to the select and give the fixture values forAERIAL_GROUND_INDICATORandPRODUCT_CHEMICAL_PERCENT.
There was a problem hiding this comment.
Claude Code:
Done in 50e45a0. All four columns are asserted now, and the fixture sets AERIAL_GROUND_INDICATOR ('G') and PRODUCT_CHEMICAL_PERCENT (25.5). These mutants now fail the check:
- null
use_number; aerial_groundtaken from the wrong column;- null
comtrs; - zeroed
prodchem_pct.
| # prd_share: a use's product lbs split across its AI rows, so summing any set of whole uses counts each use's product once | ||
| con.sql(f"""create table base as select comtrs, county_cd, MeridianTownshipRange township, monthyear, usetype, aerial_ground, | ||
| site_code, prodno, chem_code, lbs_chm_used chm, | ||
| lbs_prd_used / count(*) over (partition by monthyear[1:4], use_number) prd_share | ||
| from '{DATA_DIR / "calpip" / "calpip_full.parquet"}'""") | ||
|
|
||
| for geo, (rows, demog, key, has_name) in GEOS.items(): | ||
| print("building", geo) | ||
| con.sql(f"create or replace view rows as {rows}") | ||
| # prd is the product lbs of the whole cell, repeated on each of its AI rows: take max(prd) per cell when querying. | ||
| # float32 keeps totals within ~1e-6 and cuts ~25% of the bytes a query reads; the summary tables stay exact | ||
| con.sql(f"""copy (select * exclude (prd_part) replace (chm::float as chm), (sum(prd_part) over (partition by {CELL}))::float prd from ( | ||
| select {CELL}, chem_code, sum(chm * r) chm, sum(prd_share * r) prd_part from rows group by all | ||
| ) order by monthyear, geo, site_code, prodno, chem_code) to '{OUT}/use/{geo}.parquet' {PARQUET}""") |
There was a problem hiding this comment.
Claude (automated review):
IMPORTANT[tests]The aggregation behind every number on the site has no automated checkThe
prd_sharesplit, area-ratio weighting, the per-cellprdinuse/, and the manifest years are untested. The compare step can't catch problems there either. It reads onlysummary/county.parquet, so my rebuild showed 0.00% for every year even though the township tables changed by 182,696 lbs. An 85-line synthetic check I drafted that runs in 0.16 s and catches 10 of 12 realistic mutations. Either add a trimmed version, or add a step to Build data that runspython fe-migration/reference_queries.py data/r2. That step covers all six geographies on real data in a few seconds.
There was a problem hiding this comment.
Claude Code:
I added cli/check_build.py in 50e45a0. It runs build_r2 on a 6-row synthetic dataset in 0.1 s and asserts:
summary/for all six geographies;use/countyanduse/tract;- the geo table's values and types;
- the manifest and the counties lookup.
It caught all 12 mutants I tried:
- product lbs not split across AI rows;
- the year missing from the use key;
- the area ratio not applied in
use/orsummary/; prodnomissing from the cell;- non-ag leaking into tract or section;
- per-row instead of per-cell
prd; - the county code join;
- bigint demography;
- the square-mile conversion;
- the manifest start year.
It runs in a new Checks workflow on every PR and at the start of Build data.
I didn't make reference_queries.py a Build gate. Its fixtures are 2021–2023 snapshots, so a legitimate re-pull of those years would fail the build.
…hecks From timhannifan's review of this PR: - Workflows read inputs from a separate bucket (R2_RAW_BUCKET), so uploader keys never reach the bucket the site serves, which also holds the DuckDB-WASM code every visitor runs. Jobs use a `data` environment (limited to main in settings) and only the R2 steps see the key. - check_clean.py asserts every output row and column (catches duplicated rows, the date filter, use_number/comtrs/aerial_ground/prodchem_pct); build_full drops rows whose month doesn't parse (1 junk row today). - New check_build.py runs build_r2 on synthetic data: product lbs once per application, area-ratio splits, per-cell prd, ag-only geographies, year in the use key, manifest years. Catches 12 of 12 mutants. - New Checks workflow runs both on every pull request. - build_r2: main() takes a data dir; the use key is the old pipeline's (year, use number, county, COMTRS, product lbs). - reference_queries: township waiver only for builds after v1 and only with the same row count; timeseries that need AI or product columns read use/; start/end/usetype are validated. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- RUNBOOK: split inputs into private pesticide-data-raw (uploader keys only reach it), the `data` environment and main protection, CI vs uploader keys, and `rclone copy` (sync would delete tracked data/ files). - Guide and README: uploads go to pesticide-data-raw/calpip/. - PLAN: input validation the TypeScript port needs beyond escaping; don't carry the township waiver into pnpm parity. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Thanks for the careful review. The six Important items are answered inline. On the preferences and opinions from the review body:
Re-verified at 50e45a0:
Repo settings and R2 changes (the environment, |
The data environment (deployable from main only), pull requests required on main, and the R2_BUCKET / R2_RAW_BUCKET variables. Secrets are still to add. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Summary
Moves the data pipeline from AWS Batch and nectr to GitHub Actions and Cloudflare R2. There are two buckets: a private
pesticide-data-rawfor inputs, andpesticide-datafor what the site serves. The explorer (cpr-explorer-v2) will read static parquet files from R2 with DuckDB-WASM.fe-migration/PLAN.mdis the plan for that side.New flow for adding a year:
pesticide-data-raw/calpip/with Cyberduck and an uploader key scoped to that bucket. No Cloudflare account role is needed.v<date>-<time>/folder. It then pointspreview.json(the site's?previewlink) at it and writes yearly totals next to the live build's, plus any chemicals missing from PAN's category sheet, to the run summary.manifest.jsonat the build; any earlier version also works, which is how you roll back. The FE reads the manifest on load, so data updates need no FE rebuild.Docs:
docs/updating-the-data.pdfis the step-by-step guide for PAN and replaces the March 2025 AWS guide. Once this merges, GitHub Pages will serve it at https://open-spatial-lab.github.io/cpr/updating-the-data.pdf.RUNBOOK.mdcovers setup, the FE cutover, AWS teardown and routine work.Why AWS Batch isn't needed any more
Batch existed for RAM. With the clean step in DuckDB, the whole run takes under a minute. The "after" figures below were measured with DuckDB capped at 2 GB. Uncapped, DuckDB uses up to 80% of RAM: locally
build_r2reached 7 GB, which still fits a standard public-repo runner (4 vCPUs, 16 GiB).Verification
All of this was run, not just read, in a fresh Python 3.12 venv with the pinned
requirements.txt.calpip_full: matches the old pipeline's on all 31,205,967 rows except the township ID, which now comes from COMTRS. 1,405 rows (182,696 lbs) that the old join dropped now get an ID, and 18 malformed IDs are fixed.cli/check_clean.pyandcli/check_build.py: they run the clean and build steps on tiny synthetic datasets in under a second, and assert every output row and column. Together they caught 22 of 22 mutants (10 in clean, 12 in build). They run in a new Checks workflow on every PR and at the start of Build data.fe-migration/reference_queries.pyreproduces 25 snapshots of the live nectr API againstv1, both locally and read straight from R2. Newer builds report the township case as an expected difference.v1/: all 26 files match the local build on schema, rows and sums.actionlintpasses on all four.Reviews
Running
/pr-review(5 single-factor reviewers plus a false-positive pass) found no blocking issues and 13 Important ones. All 13 are fixed in1344cbfand0b13d32:start,endandusetypeweren't escaped.permissions:blocks;nothing";Setup before the first run (see RUNBOOK.md)
v1/and the manifests are inpesticide-data, with cache headers set, and a rehearsal of Build data against R2 shows 0.00% for every year.pesticide-data-rawand move the inputs into it;pesticide-data;pesticide-dataonly.dataenvironment limited tomain, holdingR2_ACCOUNT_ID,R2_ACCESS_KEY_IDandR2_SECRET_ACCESS_KEY;AWS_*/BATCH_*secrets into it;main;R2_BUCKET=pesticide-dataandR2_RAW_BUCKET=pesticide-data-raw, andSITE_URLafter the FE cutover.pesticide-data-rawonly.A second review from @timhannifan raised 6 Important items, all fixed in
50e45a0:main-only environment;rclone syncwould have deleted tracked files;The replies are inline.
Transition
configure-aws-credentialsaction is bumped from v2, which no longer runs on GitHub, to v4.docs/*.htmlstill call nectr.🤖 Generated with Claude Code