Home / Blog / One Deploy Per Push: A Day of Deleting Things at JCRT

I spent this weekend and last several deleting things from the Journal for Cultural and Religious Theory repository. Twenty-four scripts became eleven. Four GitHub workflows became three. A Netlify deploy that took a minute and twelve seconds now takes forty-two. And a bug I had been living with for two months, one I had not noticed because every symptom looked like normal housekeeping, is fixed. This post is the write-up, mostly for future Adam, since I know from experience that I will need it.

I did the work with assistance from Claude Code, running Anthropic's Fable 5.1 model, in a session that started as "study this repo and tell me what to cut." The audit part was a swarm of read-only agents, each assigned one corner of the codebase. What follows is what they found and what we changed.

The bug I was looking at every day #

Since July, every article on jcrt.org has been registered as a site.standard.document record on the AT Protocol, the network behind Bluesky, using Standard.site and Steve Dylan's sequoia-cli. A GitHub workflow runs after every content push, publishes anything new, and commits a small map of paths to record URIs back to the repo. The templates read that map and put a <link rel="site.standard.document"> in each article's head.

Here is what I had been seeing without seeing it. Every one of those bot commits changed about 1,666 files. Since August 23 there were 21 of them. Each one rewrote the ATProto URI on every article with a new record key. Each one triggered a second full Netlify deploy, because 1,830 pages had changed. And again and again my next git push was rejected because the bot had pushed first, which is why my history has ten "Merge branch 'main'" commits in it. I read all of that as the cost of running automation. It was a loop.

The cause is a single line. sequoia-cli decides what to do with each staged file by comparing a hash of the file against the hash it stored last time. If they differ and the file carries an atUri in its front matter, it updates the record in place. If they differ and there is no atUri, it creates a new record. After a create it writes the atUri line into the file and stores the hash of the file with that line in it. My staging script regenerated every file from scratch on every run, without the line. So every hash mismatched, no file had an atUri, and every article was created again with a fresh key. The old records were never deleted.

When Claude pulled the Actions log for a 20-file push, it read: "Found 1826 posts. 1826 posts to publish. Errors: 160, Rate Limit Exceeded." Bluesky's data server allows 5,000 write points an hour and a create costs three. 5,000 divided by three is 1,666. I had been spending the journal's entire hourly write budget on every push. The nine articles from issue 1.1 never got a record at all, because they were always in the part of the alphabet that hit the limit. A read-only listing of the repository found 76,239 document records for 1,816 real articles.

The fix is the line I was not writing. The staging step now looks each article up in the committed map and emits atUri: "at://…" as the last line of its front matter, byte for byte as the tool would have written it. A unit test carries a copy of the tool's own inserter and asserts that the two agree, so the format cannot drift without failing the build. On the first real run after the change the log read "Published: 11, Updated: 5." Those eleven were the articles the rate limit had always blocked. The workflow now commits only when a record was genuinely added or edited, and a content push gets one deploy.

Two smaller things came out of the same dig. Two author bios contain a literal --- in their description, and sequoia-cli finds the end of the front matter by searching for the first --- after the opening one, so it had been inserting the URI into the middle of a sentence. Those are escaped now. And the workflow had been rewriting an atproto: key into 1,842 content files on every run that nothing reads. The map is the only source the templates use. That rewrite is gone.

The 74,000 orphaned records are still on the server. There is a prune command now that lists them, keeps everything in the map, and deletes the rest in batches within the rate budget, but at 35,000 deletes a day that is a job for a few evenings, by hand.

Fewer scripts #

The scripts/ folder had grown the way these folders do. Five files formed the Standard.site chain. Five more were build-time validators that each printed a report and called process.exit. There was a script that greps the source code of another script to check that it uploads an icon. There was a 526-line ORCID lookup I had paused in August, a Zotero import checker nothing called, and a purge-by-URL mode for Cloudflare that nothing used.

The audit agents produced a table of every script, every caller, and every private copy of a helper that already lived in lib/. Six functions had been reimplemented between two and seven times each: splitting an author string, normalising a date, resolving a content file to its URL. Then we cut.

  • The five Standard.site scripts are one file with subcommands.
  • The five validators are one file with subcommands. They throw instead of calling process.exit, which is what let the next change happen.
  • The build orchestrator is gone. Eleventy has an eleventy.after event, and that is where the CSS purge, the Pagefind index, and the post-build checks run now, in parallel, production builds only. A check that fails throws, and a throw there fails the Eleventy process, which is all a deploy gate needs. npm run build:netlify is four commands in a row.
  • The OAI-PMH feed, the DOAJ feed, and two citation sitemaps used to be generated by a script and committed to the repo, which meant they were stale whenever I forgot to run it. They were stale this morning. They are built now, from a data file and one JavaScript template, and the five outputs compared byte-identical to the committed copies before I deleted those.
  • The IndexNow workflow rebuilt the entire site on every push to read three sitemaps, then announced 5,541 URLs to Bing about a minute before Netlify had deployed any of them. It is a Netlify plugin now. It runs after the deploy is live and sends only the URLs that are new since the last one.
  • XSD validation of the OAI feed, which only ever ran by hand, is gone with its four schema files. The protocol checks stayed.

Eleven files remain, and five of those are tests. The tests are the part I would defend hardest. Every one of these consolidations was verified by building the site twice and diffing the output tree, and the only differences were the ones we intended.

One billed minute #

Netlify now counts any started minute as a full build minute, so a deploy at 1:12 costs two and a deploy at 0:59 costs one. Claude read the deploy log stage by stage. The build command was 31 seconds. Lighthouse, a plugin I had installed from the Netlify dashboard and forgotten, was 11. A second Cloudflare purge plugin, also from the dashboard, was purging a cache my own plugin had purged a second earlier.

The code-side change that mattered was Pagefind. Its cost is reading and parsing HTML, not indexing. Marking the 4,900 tag and keyword pages as ignorable saved nothing, because it still read them. Leaving them out of the file glob cut the run from 7.6 seconds to 5.6 on Netlify. Those pages are lists of articles the index already holds. The glob is built from the output directory at build time, so a new section is indexed by default and I do not have to remember anything.

The next deploy log read "Netlify Build completed in 42.3s." Fifty-four seconds end to end. One minute, billed.

What I would tell someone doing this #

The bug was invisible because every symptom was individually plausible. Large bot commits, second deploys, rejected pushes: each has an innocent explanation. What surfaced it was an agent counting the files in a commit and asking why the number was the same every time. I had never asked.

Deleting is the harder half of maintenance and the more valuable one. Most of what we removed was working. It was also work nobody needed, running on every push, and every one of those scripts was a place for the next bug to live.

If you run an Eleventy site with a publishing step that writes back to git, check what your bot commits contain. If the number of changed files is the size of your site, something is regenerating instead of updating. Write to me if you find one. I would like to know I am not the only one.

Tags : jcrt eleventy web-development netlify at-protocol digital-humanities

Webmentions

No webmentions yet.

Previous

Genealogy, Enchantment, and the War for the Mind: An Interview with Jason Ānanda Josephson Storm in JCRT

A pointer to the JCRT interview with Jason Ānanda Josephson Storm, posted on my Gemini capsule.

Next

New Publication: Sacralizing Dominion

Betty Hill (Lyons) and I have a new open access article in Religions on the Doctrine of Christian Discovery and white Christian nationalism.