remote managed wiki content drifted; rerun with --force to overwrite
The operator has not edited any wiki page on NodeBB. The last full re-publish
was days earlier and succeeded. The run still reports drifted: 23 and exits 1,
so the wiki never ships from a v* tag without a manual force=true dispatch.
The drift guard exists to protect hand edits on managed pages. Right now it
fires when nobody edited anything, which trains the operator to leave WIKI_DEPLOY_FORCE=true on permanently — and that removes the protection the
guard was built for.
Solution
Make the drift verdict mean what its message says: "the live page changed after
we last wrote it."
Three changes to crucible wiki deploy:
Record what NodeBB actually holds after each successful write, and compare
the next run against that, instead of comparing NodeBB's stored copy against
the hash of the text Crucible rendered.
Do not call a page drifted when the remote post carries no sourceContent.
That page was written before sourceContent sync existed; the content read
back is NodeBB's rendered HTML, not our source, so it can never hash-equal
anything we wrote. It needs the existing SourceContentSynced repair, not a
drift refusal.
Name the drifted pages in the error and in the plan output, capped, with a
total. An operator cannot decide whether --force is safe when the tool
reports a bare count.
After this, a tag deploy passes unless a human really edited a managed page, and --force goes back to being the rare deliberate answer.
User Stories
As a wiki operator, I want a v* tag deploy to succeed when nobody edited the
live wiki, so that releases ship without a manual break-glass dispatch.
As a wiki operator, I want --force to stay off by default, so that hand
edits on managed pages keep their protection.
As a wiki operator, I want the drift error to list the page IDs that drifted,
so that I can look at those pages before I decide to overwrite them.
As a wiki operator, I want the drifted page list capped with a total count, so
that a mass-drift run does not flood the CI log.
As a wiki operator, I want a page whose remote post predates sourceContent
sync to be repaired on the next deploy, so that the backlog of unsynced pages
drains instead of blocking every run.
As a wiki operator, I want a page that a person really edited to still be
reported as drifted and still block the deploy, so that the guard keeps
working.
As a wiki operator, I want an old deploy manifest (one written before this
change) to keep working, so that I do not have to re-seed managed namespaces
to get a green deploy.
As a wiki operator, I want the manifest to converge after one successful
deploy, so that the second run in a row reports zero drift.
As a wiki operator, I want a --dry-run preview to report the same drift set
as the live run, so that the preview is worth reading.
As a wiki operator, I want --force to still overwrite drifted pages, so
that the break-glass path is unchanged.
As a release engineer, I want the extra remote reads to be proportional to
pages written, not to pages in the wiki, so that a 1200-page deploy with 24
changes stays fast.
As a release engineer, I want the run to fail closed if the post-write read
fails, so that the manifest never records a hash we did not verify.
As a topdata maintainer, I want sow-topdata's WIKI_DEPLOY_FORCE to stay false in Gitea, so that the repository variable is not a permanent
workaround.
As a topdata maintainer, I want the stale/archive/purge policies to behave
exactly as before, so that this fix carries no destructive risk.
As an agent working on the wiki deployer, I want the dead RemoteHash plan
field either used or deleted, so that the code does not suggest a check that
never runs.
Implementation Decisions
Module: internal/topdata (wiki_deploy.go). No new package, no new command,
no new CLI flag.
Manifest schema.wikiDeployManifestPage gains one field, remote_hash
(omitempty). It holds computeManagedHash of the post content read back from
NodeBB immediately after a successful create or update. The existing hash
field keeps its current meaning: the hash of the generated managed region, used
for the "nothing changed locally, skip" decision.
Drift comparison. The drift check compares the remote hash observed at plan
time against remote_hash when it is set. When remote_hash is empty (a
manifest written before this change), the check falls back to today's comparison
against hash, so old manifests do not turn into a mass-drift event.
sourceContent gate. When the remote post has no sourceContent, the page is
not counted as drifted. parseNodeBBPost already reports this: it falls back to
the rendered content and leaves SourceContent empty. Such a page flows into
the existing SourceContentSynced repair update, which rewrites both content
and sourceContent and then records remote_hash.
Post-write read-back. After a successful create or update, the deployer
re-reads the post through the existing getPost and stores the hash. One extra
GET per page written; pages that skip are never fetched, so a normal deploy
costs a handful of reads. A failed read-back fails the deploy rather than
recording an unverified hash.
Error and progress output. The NodeBB wiki plan: progress line keeps its
counts. The drift error lists up to 10 drifted page IDs and the total, and keeps
the existing rerun with --force to overwrite instruction.
Unchanged: the --force, --create, --stale-policy, --reset-managed-namespaces semantics; the edit-lock flow; the managed/manual
region merge; the dry-run early return that reports the plan without failing on
drift.
Testing Decisions
A good test here drives DeployWikiWithOptions against the existing newFakeNodeBB HTTP double, asserts on the returned DeployResult, on the
manifest written to disk, and on the requests the fake server received. It never
reaches for an unexported hash helper to prove an internal step ran. Prior art: TestDeployWikiRepairsManifestedPageMissingSourceContentSync and TestDeployWikiCreatesNodeBBTopicAndWritesManifest in internal/topdata/wiki_deploy_test.go — same seam, same shape.
That seam is the only one used. No new seam is introduced.
Cases to cover:
NodeBB returns a normalized copy of what we wrote (differs from the generated
text), the manifest holds a matching remote_hash → deploy succeeds, drifted == 0.
A person edited the live page (remote differs from the recorded remote_hash) → drifted == 1, deploy fails, error names the page ID.
Remote post has no sourceContent → not drifted, the repair update runs,
manifest records SourceContentSynced and remote_hash.
Manifest with no remote_hash (old format) and a remote copy equal to the
recorded hash → no drift, and the run records remote_hash for next time.
--force with a genuinely drifted page → page is overwritten and remote_hash is refreshed.
Two runs back to back with unchanged input → second run reports all pages
skipped and issues no post reads.
make check covers the package; the repo's existing Go test conventions apply.
Out of Scope
Any change to the NodeBB westgate-wiki plugin or how it stores sourceContent.
Three-way merge, conflict markers, or any attempt to keep a human edit and the
generated text at the same time.
A per-page force flag or an interactive picker.
Changing sow-topdata's workflow, its WIKI_DEPLOY_* variables, or the
stale-policy defaults. Once this ships, sow-topdata keeps force=false.
Backfilling remote_hash for pages that are currently skipped. They gain it
the next time they change.
Further Notes
Observed in sow-topdata CI: local pages: 1200, updated: 1, skipped: 1176, drifted: 23. Pages whose generated text did not change are skipped before the
remote is ever read, so the 23 are drawn from the ~24 pages that did change —
nearly all of them. A human-edit explanation would not correlate with "the text
we happened to regenerate this week"; a systematic mismatch does.
wikiDeployPlan.RemoteHash already exists, is computed on the update path, and
is never read. This spec gives it its purpose.
## Problem Statement
Every `deploy-wiki` run in `sow-topdata` fails with:
```
remote managed wiki content drifted; rerun with --force to overwrite
```
The operator has not edited any wiki page on NodeBB. The last full re-publish
was days earlier and succeeded. The run still reports `drifted: 23` and exits 1,
so the wiki never ships from a `v*` tag without a manual `force=true` dispatch.
The drift guard exists to protect hand edits on managed pages. Right now it
fires when nobody edited anything, which trains the operator to leave
`WIKI_DEPLOY_FORCE=true` on permanently — and that removes the protection the
guard was built for.
## Solution
Make the drift verdict mean what its message says: "the live page changed after
we last wrote it."
Three changes to `crucible wiki deploy`:
1. Record what NodeBB actually holds after each successful write, and compare
the next run against that, instead of comparing NodeBB's stored copy against
the hash of the text Crucible rendered.
2. Do not call a page drifted when the remote post carries no `sourceContent`.
That page was written before `sourceContent` sync existed; the content read
back is NodeBB's rendered HTML, not our source, so it can never hash-equal
anything we wrote. It needs the existing `SourceContentSynced` repair, not a
drift refusal.
3. Name the drifted pages in the error and in the plan output, capped, with a
total. An operator cannot decide whether `--force` is safe when the tool
reports a bare count.
After this, a tag deploy passes unless a human really edited a managed page, and
`--force` goes back to being the rare deliberate answer.
## User Stories
1. As a wiki operator, I want a `v*` tag deploy to succeed when nobody edited the
live wiki, so that releases ship without a manual break-glass dispatch.
2. As a wiki operator, I want `--force` to stay off by default, so that hand
edits on managed pages keep their protection.
3. As a wiki operator, I want the drift error to list the page IDs that drifted,
so that I can look at those pages before I decide to overwrite them.
4. As a wiki operator, I want the drifted page list capped with a total count, so
that a mass-drift run does not flood the CI log.
5. As a wiki operator, I want a page whose remote post predates `sourceContent`
sync to be repaired on the next deploy, so that the backlog of unsynced pages
drains instead of blocking every run.
6. As a wiki operator, I want a page that a person really edited to still be
reported as drifted and still block the deploy, so that the guard keeps
working.
7. As a wiki operator, I want an old deploy manifest (one written before this
change) to keep working, so that I do not have to re-seed managed namespaces
to get a green deploy.
8. As a wiki operator, I want the manifest to converge after one successful
deploy, so that the second run in a row reports zero drift.
9. As a wiki operator, I want a `--dry-run` preview to report the same drift set
as the live run, so that the preview is worth reading.
10. As a wiki operator, I want `--force` to still overwrite drifted pages, so
that the break-glass path is unchanged.
11. As a release engineer, I want the extra remote reads to be proportional to
pages written, not to pages in the wiki, so that a 1200-page deploy with 24
changes stays fast.
12. As a release engineer, I want the run to fail closed if the post-write read
fails, so that the manifest never records a hash we did not verify.
13. As a topdata maintainer, I want `sow-topdata`'s `WIKI_DEPLOY_FORCE` to stay
`false` in Gitea, so that the repository variable is not a permanent
workaround.
14. As a topdata maintainer, I want the stale/archive/purge policies to behave
exactly as before, so that this fix carries no destructive risk.
15. As an agent working on the wiki deployer, I want the dead `RemoteHash` plan
field either used or deleted, so that the code does not suggest a check that
never runs.
## Implementation Decisions
Module: `internal/topdata` (`wiki_deploy.go`). No new package, no new command,
no new CLI flag.
**Manifest schema.** `wikiDeployManifestPage` gains one field, `remote_hash`
(`omitempty`). It holds `computeManagedHash` of the post content read back from
NodeBB immediately after a successful create or update. The existing `hash`
field keeps its current meaning: the hash of the generated managed region, used
for the "nothing changed locally, skip" decision.
**Drift comparison.** The drift check compares the remote hash observed at plan
time against `remote_hash` when it is set. When `remote_hash` is empty (a
manifest written before this change), the check falls back to today's comparison
against `hash`, so old manifests do not turn into a mass-drift event.
**sourceContent gate.** When the remote post has no `sourceContent`, the page is
not counted as drifted. `parseNodeBBPost` already reports this: it falls back to
the rendered `content` and leaves `SourceContent` empty. Such a page flows into
the existing `SourceContentSynced` repair update, which rewrites both `content`
and `sourceContent` and then records `remote_hash`.
**Post-write read-back.** After a successful create or update, the deployer
re-reads the post through the existing `getPost` and stores the hash. One extra
GET per page written; pages that skip are never fetched, so a normal deploy
costs a handful of reads. A failed read-back fails the deploy rather than
recording an unverified hash.
**Error and progress output.** The `NodeBB wiki plan:` progress line keeps its
counts. The drift error lists up to 10 drifted page IDs and the total, and keeps
the existing `rerun with --force to overwrite` instruction.
**Unchanged:** the `--force`, `--create`, `--stale-policy`,
`--reset-managed-namespaces` semantics; the edit-lock flow; the managed/manual
region merge; the dry-run early return that reports the plan without failing on
drift.
## Testing Decisions
A good test here drives `DeployWikiWithOptions` against the existing
`newFakeNodeBB` HTTP double, asserts on the returned `DeployResult`, on the
manifest written to disk, and on the requests the fake server received. It never
reaches for an unexported hash helper to prove an internal step ran. Prior art:
`TestDeployWikiRepairsManifestedPageMissingSourceContentSync` and
`TestDeployWikiCreatesNodeBBTopicAndWritesManifest` in
`internal/topdata/wiki_deploy_test.go` — same seam, same shape.
That seam is the only one used. No new seam is introduced.
Cases to cover:
1. NodeBB returns a normalized copy of what we wrote (differs from the generated
text), the manifest holds a matching `remote_hash` → deploy succeeds,
`drifted == 0`.
2. A person edited the live page (remote differs from the recorded
`remote_hash`) → `drifted == 1`, deploy fails, error names the page ID.
3. Remote post has no `sourceContent` → not drifted, the repair update runs,
manifest records `SourceContentSynced` and `remote_hash`.
4. Manifest with no `remote_hash` (old format) and a remote copy equal to the
recorded `hash` → no drift, and the run records `remote_hash` for next time.
5. `--force` with a genuinely drifted page → page is overwritten and
`remote_hash` is refreshed.
6. Two runs back to back with unchanged input → second run reports all pages
skipped and issues no post reads.
`make check` covers the package; the repo's existing Go test conventions apply.
## Out of Scope
- Any change to the NodeBB `westgate-wiki` plugin or how it stores
`sourceContent`.
- Three-way merge, conflict markers, or any attempt to keep a human edit and the
generated text at the same time.
- A per-page force flag or an interactive picker.
- Changing `sow-topdata`'s workflow, its `WIKI_DEPLOY_*` variables, or the
stale-policy defaults. Once this ships, `sow-topdata` keeps `force=false`.
- Backfilling `remote_hash` for pages that are currently skipped. They gain it
the next time they change.
## Further Notes
Observed in `sow-topdata` CI: `local pages: 1200, updated: 1, skipped: 1176,
drifted: 23`. Pages whose generated text did not change are skipped before the
remote is ever read, so the 23 are drawn from the ~24 pages that did change —
nearly all of them. A human-edit explanation would not correlate with "the text
we happened to regenerate this week"; a systematic mismatch does.
`wikiDeployPlan.RemoteHash` already exists, is computed on the update path, and
is never read. This spec gives it its purpose.
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
Problem Statement
Every
deploy-wikirun insow-topdatafails with:The operator has not edited any wiki page on NodeBB. The last full re-publish
was days earlier and succeeded. The run still reports
drifted: 23and exits 1,so the wiki never ships from a
v*tag without a manualforce=truedispatch.The drift guard exists to protect hand edits on managed pages. Right now it
fires when nobody edited anything, which trains the operator to leave
WIKI_DEPLOY_FORCE=trueon permanently — and that removes the protection theguard was built for.
Solution
Make the drift verdict mean what its message says: "the live page changed after
we last wrote it."
Three changes to
crucible wiki deploy:the next run against that, instead of comparing NodeBB's stored copy against
the hash of the text Crucible rendered.
sourceContent.That page was written before
sourceContentsync existed; the content readback is NodeBB's rendered HTML, not our source, so it can never hash-equal
anything we wrote. It needs the existing
SourceContentSyncedrepair, not adrift refusal.
total. An operator cannot decide whether
--forceis safe when the toolreports a bare count.
After this, a tag deploy passes unless a human really edited a managed page, and
--forcegoes back to being the rare deliberate answer.User Stories
v*tag deploy to succeed when nobody edited thelive wiki, so that releases ship without a manual break-glass dispatch.
--forceto stay off by default, so that handedits on managed pages keep their protection.
so that I can look at those pages before I decide to overwrite them.
that a mass-drift run does not flood the CI log.
sourceContentsync to be repaired on the next deploy, so that the backlog of unsynced pages
drains instead of blocking every run.
reported as drifted and still block the deploy, so that the guard keeps
working.
change) to keep working, so that I do not have to re-seed managed namespaces
to get a green deploy.
deploy, so that the second run in a row reports zero drift.
--dry-runpreview to report the same drift setas the live run, so that the preview is worth reading.
--forceto still overwrite drifted pages, sothat the break-glass path is unchanged.
pages written, not to pages in the wiki, so that a 1200-page deploy with 24
changes stays fast.
fails, so that the manifest never records a hash we did not verify.
sow-topdata'sWIKI_DEPLOY_FORCEto stayfalsein Gitea, so that the repository variable is not a permanentworkaround.
exactly as before, so that this fix carries no destructive risk.
RemoteHashplanfield either used or deleted, so that the code does not suggest a check that
never runs.
Implementation Decisions
Module:
internal/topdata(wiki_deploy.go). No new package, no new command,no new CLI flag.
Manifest schema.
wikiDeployManifestPagegains one field,remote_hash(
omitempty). It holdscomputeManagedHashof the post content read back fromNodeBB immediately after a successful create or update. The existing
hashfield keeps its current meaning: the hash of the generated managed region, used
for the "nothing changed locally, skip" decision.
Drift comparison. The drift check compares the remote hash observed at plan
time against
remote_hashwhen it is set. Whenremote_hashis empty (amanifest written before this change), the check falls back to today's comparison
against
hash, so old manifests do not turn into a mass-drift event.sourceContent gate. When the remote post has no
sourceContent, the page isnot counted as drifted.
parseNodeBBPostalready reports this: it falls back tothe rendered
contentand leavesSourceContentempty. Such a page flows intothe existing
SourceContentSyncedrepair update, which rewrites bothcontentand
sourceContentand then recordsremote_hash.Post-write read-back. After a successful create or update, the deployer
re-reads the post through the existing
getPostand stores the hash. One extraGET per page written; pages that skip are never fetched, so a normal deploy
costs a handful of reads. A failed read-back fails the deploy rather than
recording an unverified hash.
Error and progress output. The
NodeBB wiki plan:progress line keeps itscounts. The drift error lists up to 10 drifted page IDs and the total, and keeps
the existing
rerun with --force to overwriteinstruction.Unchanged: the
--force,--create,--stale-policy,--reset-managed-namespacessemantics; the edit-lock flow; the managed/manualregion merge; the dry-run early return that reports the plan without failing on
drift.
Testing Decisions
A good test here drives
DeployWikiWithOptionsagainst the existingnewFakeNodeBBHTTP double, asserts on the returnedDeployResult, on themanifest written to disk, and on the requests the fake server received. It never
reaches for an unexported hash helper to prove an internal step ran. Prior art:
TestDeployWikiRepairsManifestedPageMissingSourceContentSyncandTestDeployWikiCreatesNodeBBTopicAndWritesManifestininternal/topdata/wiki_deploy_test.go— same seam, same shape.That seam is the only one used. No new seam is introduced.
Cases to cover:
text), the manifest holds a matching
remote_hash→ deploy succeeds,drifted == 0.remote_hash) →drifted == 1, deploy fails, error names the page ID.sourceContent→ not drifted, the repair update runs,manifest records
SourceContentSyncedandremote_hash.remote_hash(old format) and a remote copy equal to therecorded
hash→ no drift, and the run recordsremote_hashfor next time.--forcewith a genuinely drifted page → page is overwritten andremote_hashis refreshed.skipped and issues no post reads.
make checkcovers the package; the repo's existing Go test conventions apply.Out of Scope
westgate-wikiplugin or how it storessourceContent.generated text at the same time.
sow-topdata's workflow, itsWIKI_DEPLOY_*variables, or thestale-policy defaults. Once this ships,
sow-topdatakeepsforce=false.remote_hashfor pages that are currently skipped. They gain itthe next time they change.
Further Notes
Observed in
sow-topdataCI:local pages: 1200, updated: 1, skipped: 1176, drifted: 23. Pages whose generated text did not change are skipped before theremote is ever read, so the 23 are drawn from the ~24 pages that did change —
nearly all of them. A human-edit explanation would not correlate with "the text
we happened to regenerate this week"; a systematic mismatch does.
wikiDeployPlan.RemoteHashalready exists, is computed on the update path, andis never read. This spec gives it its purpose.