Skip to content

fix(web): make the SEO lastmod manifest resolvable on conflict and explain itself - #712

Merged
blove merged 1 commit into
mainfrom
fix/lastmod-merge-conflicts
Sep 17, 2026
Merged

blove merged 1 commit into
mainfrom
fix/lastmod-merge-conflicts

Conversation

@blove

@blove blove commented Sep 17, 2026

Copy link
Copy Markdown
Contributor

Fixes #710.

Problem

apps/web/app/seo/lastmod.generated.json is generated and committed, and nearly every PR restages part of it — including package-only changes, since the API reference pages move whenever a package's surface does. Two PRs touching the same page always overlap, because the generator stamps one timestamp onto every route whose digest changed.

Three recent overlaps on main, within about a day:

Route Rewritten by
/docs/deployment/kubernetes #694 and #699
/docs/middleware #694 and #702
/docs/api/permissions #699 and #707

The conflict was expensive twice over. Git wrote conflict markers into the JSON, which left it unparseable, so the generator could not run and the one command that resolves the conflict was blocked behind hand-editing generated content. And when the manifest was merely stale, the test asserted on the exit code alone, so CI showed expected 1 to be +0 while the generator's own message — which names the exact fix command — was discarded.

Change

  • .gitattributes marks the manifest -merge. A conflict here is never informative: the file is wholly derived from content plus recorded timestamps, so the correct resolution is always to regenerate rather than to pick a side. Git still reports the conflict, but leaves valid JSON in the worktree, so pnpm --dir apps/web seo:lastmod && git add resolves it. Chosen over a custom merge driver, which every clone would have to register in its own git config before it took effect.
  • linguist-generated collapses it in review.
  • The --check assertions carry the child's own output, so a stale manifest explains itself and names its fix.
  • A guard test pins the attribute through git check-attr.
  • AGENTS.md records the regenerate-in-the-same-commit rule and the conflict resolution.

Verification

Both new behaviours were checked against their failure modes rather than only their success path.

The -merge attribute, in a scratch repo. Without it, the merged file contains markers and is not parseable JSON; with it, one clean side survives:

$ git merge other        # with `-merge`
{
  "routes": {
    "/a": { "t": "MINE" },

The guard test fails when the entry is removed:

AssertionError: expected 'apps/web/app/seo/lastmod.generated.js…' to match /merge: unset$/

And a deliberately stale manifest now reports the cause and the fix instead of a bare exit code:

AssertionError: SEO last-modified manifest is stale: regenerate with pnpm --dir apps/web seo:lastmod --as-of 2026-09-17: expected 1 to be +0

pnpm lint, node scripts/check-docs.mjs and the apps/web SEO suite (59 tests) pass.

Scope

This makes the conflict trivial to resolve; it does not stop git from reporting one. Eliminating it entirely would need either a merge driver requiring per-clone setup or moving regeneration to a post-merge job on main, both more invasive than the problem warrants. No changeset: nothing under packages/*/src changes.

🤖 Generated with Claude Code

…plain itself

apps/web/app/seo/lastmod.generated.json is generated and committed, and nearly
every PR restages part of it — including package-only changes, because the API
reference pages move with a package's surface. Two PRs touching the same page
therefore conflict, and that conflict was expensive twice over (#710).

- .gitattributes marks it `-merge`. A conflict here is never informative: the
  file is wholly derived, so the answer is always to regenerate rather than to
  pick a side. Without this git wrote conflict markers INTO the JSON, which
  left it unparseable — so the generator could not run and the one command
  that resolves the conflict was blocked behind hand-editing generated
  content. Git still reports the conflict; it now leaves valid JSON.
  Preferred over a merge driver, which every clone must register itself.
- linguist-generated collapses it in review.
- the --check assertions now carry the child's own output. The generator
  already prints the exact fix command, and asserting on the exit code alone
  discarded it: a stale manifest reds the required validate check with
  "expected 1 to be +0" and nothing else.
- a guard test pins the attribute through `git check-attr`, verified to fail
  when the entry is removed.
- AGENTS.md records both the regenerate-in-the-same-commit rule and the
  conflict resolution.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@blove
blove enabled auto-merge (squash) September 17, 2026 20:22
@vercel

vercel Bot commented Sep 17, 2026 •

Copy link
Copy Markdown

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

Project Deployment Actions Updated
b4-run Ready Ready Preview Sep 17, 2026 8:23pm UTC

Request Review

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Automated approval: this PR received an intelligent (AI) code review. See the review comments on this PR.

@blove
blove merged commit 3048199 into main Sep 17, 2026
30 of 31 checks passed
@blove
blove deleted the fix/lastmod-merge-conflicts branch September 17, 2026 21:17
blove added a commit that referenced this pull request Sep 17, 2026
None of the three touches this page: verify registry error codes,
body-less middleware reject status, and a k8s smoke watchdog. Only the
generated SEO manifest conflicted, resolved by regenerating per the
procedure #712 added to AGENTS.md.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
blove added a commit that referenced this pull request Sep 18, 2026
…main rule

#712 landed a bullet telling contributors to regenerate the SEO manifest in
the same commit or CI goes red. This branch's own bullet, a few lines above,
says the opposite — do NOT regenerate for an ordinary content edit — because
the workflow now does it on main. Leaving both made the standards doc
contradict itself on the one point it was added to settle.

Removes the older bullet and keeps the half of it that is still true and not
covered by the new one: what to do when the manifest does conflict, which a
route add or removal can still cause.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
blove added a commit that referenced this pull request Sep 18, 2026
…713)

* chore(web): regenerate the SEO lastmod manifest on main, not in PRs

Every docs branch had to run the generator to satisfy a freshness
assertion, so two branches touching any page conflicted inside a
generated artifact. That cost five rebases on one PR in a single day.

- New SEO lastmod workflow regenerates and commits the manifest after a
  content change reaches main, serialized against itself and scoped to
  the paths the manifest derives from so it cannot retrigger itself.
- The freshness tests now assert the generator's own round trip instead
  of asserting the committed file is current, so a stale timestamp no
  longer fails a PR.
- A new --check-routes mode keeps the half a PR must still own: a route
  the manifest has never seen has no timestamp at all, and
  requireValidLastModified throws during the build. Adding or removing a
  page still fails until the author regenerates.

The file stays committed deliberately. It is the only record of when
each route last changed, and the sitemap imports it statically, so
generating it from scratch during a build would stamp every route with
the build clock and destroy the signal.

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

* chore(release): audit the SEO lastmod workflow entrypoints

The workflow contracts pin every workflow's executable entrypoints in
two allowlists, so a new workflow file is rejected until both describe
it. The audit reports a mismatch without a diff, deliberately, so these
entries are the descriptor it computes: the three pinned actions, the
three run steps, and the job's contents: write permission.

Pinning the push step's exact shell text is the point — changing it
later has to be a deliberate, re-audited edit.

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

* docs(agents): fold the manifest conflict note into the regenerate-on-main rule

#712 landed a bullet telling contributors to regenerate the SEO manifest in
the same commit or CI goes red. This branch's own bullet, a few lines above,
says the opposite — do NOT regenerate for an ordinary content edit — because
the workflow now does it on main. Leaving both made the standards doc
contradict itself on the one point it was added to settle.

Removes the older bullet and keeps the half of it that is still true and not
covered by the new one: what to do when the manifest does conflict, which a
route add or removal can still cause.

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

* chore(release): format the workflow entrypoints fixture

Biome reports the fixture this branch adds as unformatted, which is an error
rather than a warning, so `pnpm lint` exits non-zero and reds source-validate
and the required validate check.

Formatting only. `pnpm test:release-integrity` and the workflow-contracts
digest snapshot both still pass, so no pinned digest moved.

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

---------

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>

This branch was successfully deployed

1 active and 1 inactive deployments
Preview — a7c22e60 Deployed Sep 17, 2026 by vercel[bot]
vercel-preview — a7c22e60 Deployed Sep 17, 2026 by blove via vercel-native #2004
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.

apps/web SEO lastmod manifest conflicts between concurrent PRs, and its staleness failure is unreadable

1 participant