Actions-based wiki-sync workflow (retire the post-receive hook) #2

Open
opened 2026-10-05 14:11:59 +00:00 by pit · 3 comments
Owner

Problem Statement

Docs are mirrored into the Forgejo wiki by a server-side post-receive hook on the main repo. It works and is tested, but it couples the forgejo Ansible role to one repo's doc layout, offers no visibility (no run logs or history in the Forgejo UI — a failed sync is a silent stale wiki), and exists only because Actions was believed permanently disabled. Pedro wants doc publishing to become a first-class, observable pipeline: an Actions workflow that runs the sync with visible logs and history in the Forgejo UI, after which the hook is retired, reversibly.

Solution

Once platform enablement lands (#3: instance Actions toggle, per-repo flag, pinned runner on the Prod Guest), a wiki-sync workflow runs on every push to main (plus manual dispatch). The workflow maps the canonical repo docs to wiki pages — the same mapping and mirror semantics as the hook — and pushes them to the wiki's backing git repo. Run logs and history are visible in the Forgejo UI. Once a green sync is observed, the post-receive hook is retired, reversibly, via a role toggle.

User Stories

  1. As Pedro, I want platform enablement (instance Actions toggle, per-repo flag, pinned runner) completed as specced in #3, so that this issue's workflow can run at all.
  2. As Pedro, I want a token stored as a repository secret for pushing to the wiki repo, so that no personal credential is embedded in the workflow file.
  3. As Pedro, I want the workflow to trigger on every push to main, so that the wiki stays in sync with no manual step.
  4. As Pedro, I want the workflow to skip non-main branches, so that in-flight branch docs never fight over the published pages.
  5. As Pedro, I want the workflow also triggerable manually, so that I can re-publish without crafting an empty commit.
  6. As Pedro, I want run logs and run history visible in the Forgejo UI, so that I can diagnose a failed sync without SSH-ing into the Guest.
  7. As Pedro, I want a failed sync to show as a red run in the UI, so that a silently stale wiki cannot happen.
  8. As Pedro, I want the same page mapping as the hook, so that switching mechanisms does not change the published shape.
  9. As Pedro, I want mirror semantics preserved, so that deleting a doc source deletes its wiki page.
  10. As Pedro, I want content-identical pushes to create no wiki commit, so that wiki history stays meaningful.
  11. As Pedro, I want the post-receive hook retired after the workflow is proven, so that exactly one sync mechanism exists.
  12. As Pedro, I want hook retirement reversible by a role variable and a playbook re-run, so that rollback needs no server surgery.
  13. As Pedro, I want the playbook to remain idempotent, so that a second run changes nothing.
  14. As an agent, I want the sync logic callable as a plain script from the workflow, so that I can test it without a runner.
  15. As an agent, I want the existing mocked-bare-repo test harness reused, so that the sync script inherits the coverage the hook had.
  16. As a future engineer, I want ADR-0001 amended (or superseded) once the workflow is live, so that the recorded decision reflects reality.
  17. As a future engineer, I want the workflow definition reviewed in PRs like everything else, so that the pipeline has the same guarantees as the code.
  18. As a wiki reader, I want the wiki to reflect the repo's docs after each merged PR, so that what I read matches what is canonical.

Implementation Decisions

  • Platform enablement — the instance-wide Actions toggle in the app.ini template, the per-repo has_actions flag, runner install/registration/systemd — is specced and tracked in #3. This issue assumes #3 is done and does not touch the enablement tasks.

  • Auth: a Forgejo token with write access to this repo, stored as a repository secret; the workflow pushes to the wiki backing repo over HTTPS using it. No personal tokens in the workflow file.

  • The sync core is refactored out of the post-receive hook into a standalone script the workflow invokes. The page mapping is the contract and moves unchanged:

    Repo source Wiki page
    README Home
    GLOSSARY Glossary
    docs/ARCHITECTURE Architecture
    each docs/adr NNNN-slug file ADR-NNNN-slug
  • The script keeps the empty-index tree rebuild proven by the current tests, so mirror semantics and no-op detection (tree comparison against the last synced tree) hold by construction.

  • The workflow is thin glue — trigger (push to main + manual dispatch), checkout, invoke the script, push the wiki repo — because the workflow layer is the one part that cannot be unit-tested without a runner. All logic lives in the script.

  • Hook retirement: the role gains a boolean toggle for the wiki-sync hook; flipping it removes the hook from the repo's hooks directory. The default flips only after the first successful workflow sync is observed.

  • ADR-0001 is amended or superseded once the workflow is live: its "no runners available" premise changes, and the new trade-off (observable pipeline vs. a zero-runtime hook) is recorded. (The enablement-and-labels ADR belongs to #3.)

Testing Decisions

  • A good test asserts external behavior only: given a source repo state and a wiki backing repo, the resulting wiki tree (page names and contents). Never the internal git plumbing used to get there.
  • The sync script is the single test seam, at the highest existing point: mocked Forgejo layout (bare main repo + bare wiki repo), exactly the harness the hook test already uses — that test is the prior art and is directly reusable.
  • Scenarios carried over from the hook tests: initial publication, add/update/delete mirror, no-op on content-identical input, unsafe ADR filenames, non-default wiki branch. One scenario changes shape: non-main branch filtering moves from hook stdin handling to workflow trigger filtering, tested at the workflow contract level only by inspection.
  • One new scenario: invoking the script from a plain checkout (the workflow's shape) rather than hook stdin.
  • The workflow file itself stays glue-only so its untestability carries no logic. The first live push after rollout is the integration test; it must be observed green before the hook is retired.

Out of Scope

  • Platform enablement itself: the instance Actions toggle, per-repo flag, and runner provisioning are #3.
  • Enabling Actions or runners for any other repo on the instance.
  • Any CI for the tofu/ansible code (lint, plan-diff reviews) — separate work, even though this depends on the same enablement.
  • Changing the page mapping or adding new doc sources.
  • Migrating or preserving wiki history (the wiki is a disposable mirror per ADR-0001).
  • PRE Guest work; this targets Prod only.
  • Runner clustering or runners on other hosts.

Further Notes

  • Sequencing: #3 lands first (role-side, PR-reviewed per convention 1); then this issue's workflow merges; observe a green sync; retire the hook. The wiki stays published throughout because the hook keeps running until retired.
  • Verified at spec time: the wiki is enabled and empty; Actions instance state and runner details are recorded in #3.
  • The existing hook and its tests remain until retirement is proven; the sync script is a refactor of the hook's core, not a rewrite.
## Problem Statement Docs are mirrored into the Forgejo wiki by a server-side `post-receive` hook on the main repo. It works and is tested, but it couples the forgejo Ansible role to one repo's doc layout, offers no visibility (no run logs or history in the Forgejo UI — a failed sync is a silent stale wiki), and exists only because Actions was believed permanently disabled. Pedro wants doc publishing to become a first-class, observable pipeline: an Actions workflow that runs the sync with visible logs and history in the Forgejo UI, after which the hook is retired, reversibly. ## Solution Once platform enablement lands (#3: instance Actions toggle, per-repo flag, pinned runner on the Prod Guest), a wiki-sync workflow runs on every push to `main` (plus manual dispatch). The workflow maps the canonical repo docs to wiki pages — the same mapping and mirror semantics as the hook — and pushes them to the wiki's backing git repo. Run logs and history are visible in the Forgejo UI. Once a green sync is observed, the post-receive hook is retired, reversibly, via a role toggle. ## User Stories 1. As Pedro, I want platform enablement (instance Actions toggle, per-repo flag, pinned runner) completed as specced in #3, so that this issue's workflow can run at all. 2. As Pedro, I want a token stored as a repository secret for pushing to the wiki repo, so that no personal credential is embedded in the workflow file. 3. As Pedro, I want the workflow to trigger on every push to `main`, so that the wiki stays in sync with no manual step. 4. As Pedro, I want the workflow to skip non-`main` branches, so that in-flight branch docs never fight over the published pages. 5. As Pedro, I want the workflow also triggerable manually, so that I can re-publish without crafting an empty commit. 6. As Pedro, I want run logs and run history visible in the Forgejo UI, so that I can diagnose a failed sync without SSH-ing into the Guest. 7. As Pedro, I want a failed sync to show as a red run in the UI, so that a silently stale wiki cannot happen. 8. As Pedro, I want the same page mapping as the hook, so that switching mechanisms does not change the published shape. 9. As Pedro, I want mirror semantics preserved, so that deleting a doc source deletes its wiki page. 10. As Pedro, I want content-identical pushes to create no wiki commit, so that wiki history stays meaningful. 11. As Pedro, I want the post-receive hook retired after the workflow is proven, so that exactly one sync mechanism exists. 12. As Pedro, I want hook retirement reversible by a role variable and a playbook re-run, so that rollback needs no server surgery. 13. As Pedro, I want the playbook to remain idempotent, so that a second run changes nothing. 14. As an agent, I want the sync logic callable as a plain script from the workflow, so that I can test it without a runner. 15. As an agent, I want the existing mocked-bare-repo test harness reused, so that the sync script inherits the coverage the hook had. 16. As a future engineer, I want ADR-0001 amended (or superseded) once the workflow is live, so that the recorded decision reflects reality. 17. As a future engineer, I want the workflow definition reviewed in PRs like everything else, so that the pipeline has the same guarantees as the code. 18. As a wiki reader, I want the wiki to reflect the repo's docs after each merged PR, so that what I read matches what is canonical. ## Implementation Decisions - Platform enablement — the instance-wide Actions toggle in the app.ini template, the per-repo `has_actions` flag, runner install/registration/systemd — is specced and tracked in #3. This issue assumes #3 is done and does not touch the enablement tasks. - Auth: a Forgejo token with write access to this repo, stored as a repository secret; the workflow pushes to the wiki backing repo over HTTPS using it. No personal tokens in the workflow file. - The sync core is refactored out of the post-receive hook into a standalone script the workflow invokes. The page mapping is the contract and moves unchanged: | Repo source | Wiki page | |---|---| | README | Home | | GLOSSARY | Glossary | | docs/ARCHITECTURE | Architecture | | each docs/adr `NNNN-slug` file | `ADR-NNNN-slug` | - The script keeps the empty-index tree rebuild proven by the current tests, so mirror semantics and no-op detection (tree comparison against the last synced tree) hold by construction. - The workflow is thin glue — trigger (push to `main` + manual dispatch), checkout, invoke the script, push the wiki repo — because the workflow layer is the one part that cannot be unit-tested without a runner. All logic lives in the script. - Hook retirement: the role gains a boolean toggle for the wiki-sync hook; flipping it removes the hook from the repo's hooks directory. The default flips only after the first successful workflow sync is observed. - ADR-0001 is amended or superseded once the workflow is live: its "no runners available" premise changes, and the new trade-off (observable pipeline vs. a zero-runtime hook) is recorded. (The enablement-and-labels ADR belongs to #3.) ## Testing Decisions - A good test asserts external behavior only: given a source repo state and a wiki backing repo, the resulting wiki tree (page names and contents). Never the internal git plumbing used to get there. - The sync script is the single test seam, at the highest existing point: mocked Forgejo layout (bare main repo + bare wiki repo), exactly the harness the hook test already uses — that test is the prior art and is directly reusable. - Scenarios carried over from the hook tests: initial publication, add/update/delete mirror, no-op on content-identical input, unsafe ADR filenames, non-default wiki branch. One scenario changes shape: non-`main` branch filtering moves from hook stdin handling to workflow trigger filtering, tested at the workflow contract level only by inspection. - One new scenario: invoking the script from a plain checkout (the workflow's shape) rather than hook stdin. - The workflow file itself stays glue-only so its untestability carries no logic. The first live push after rollout is the integration test; it must be observed green before the hook is retired. ## Out of Scope - Platform enablement itself: the instance Actions toggle, per-repo flag, and runner provisioning are #3. - Enabling Actions or runners for any other repo on the instance. - Any CI for the tofu/ansible code (lint, plan-diff reviews) — separate work, even though this depends on the same enablement. - Changing the page mapping or adding new doc sources. - Migrating or preserving wiki history (the wiki is a disposable mirror per ADR-0001). - PRE Guest work; this targets Prod only. - Runner clustering or runners on other hosts. ## Further Notes - Sequencing: #3 lands first (role-side, PR-reviewed per convention 1); then this issue's workflow merges; observe a green sync; retire the hook. The wiki stays published throughout because the hook keeps running until retired. - Verified at spec time: the wiki is enabled and empty; Actions instance state and runner details are recorded in #3. - The existing hook and its tests remain until retirement is proven; the sync script is a refactor of the hook's core, not a rewrite.
Author
Owner

From #12 (publish manifest, PR #18). This issue owns the sync, so the page mapping and the numbering collision are recorded here rather than built there.

The mapping has a manifest now

The set of documents published to the wiki, and the page each becomes, now lives in the repo at docs/wiki-pages.yml — allowlist by default, carrying the exclusions. The sync (this issue) should read that file instead of hardcoding the map. tests/check-wiki-pages.py fails when a document is neither published nor excluded, so adding, renaming or moving a doc forces a decision here.

Repo source Wiki page Note
docs/index.md Home was README before the #7 restructure
docs/architecture.md Architecture was docs/ARCHITECTURE.md
docs/runbooks/index.md Runbooks new section
docs/runbooks/0001-deploy-and-rollback.md Runbook-0001-deploy-and-rollback Runbook-NNNN-* for runbook pages
docs/adr/index.md Decisions new section
docs/adr/0001-forgejo-actions-runner-on-guest.md ADR-0001-forgejo-actions-runner-on-guest ADR-NNNN-*, as the hook already did
docs/actions/index.md Actions new section
GLOSSARY.md Glossary unchanged
docs/agents/* (excluded) repo-only agent tooling

What changed from the parked hook's contract: Home is now the docs entry point, not the README; docs/ARCHITECTURE.md became docs/architecture.md; the runbooks, decisions and actions indexes are published too. Mirror semantics are unchanged. A sidebar listing the published sections is still this issue's to define.

ADR-0001 numbering collision

The parked wiki-sync branch adds docs/adr/0001-repo-docs-canonical-wiki-mirror.md, while main already carries docs/adr/0001-forgejo-actions-runner-on-guest.md. Two ADR-0001s. It has to be resolved before both land — renumber the parked one (it is unmerged, so it is the cheap one to move). #12 deliberately did not touch it, because the parked ADR is this issue's file.

From #12 (publish manifest, PR #18). This issue owns the sync, so the page mapping and the numbering collision are recorded here rather than built there. ## The mapping has a manifest now The set of documents published to the wiki, and the page each becomes, now lives in the repo at `docs/wiki-pages.yml` — allowlist by default, carrying the exclusions. The sync (this issue) should read that file instead of hardcoding the map. `tests/check-wiki-pages.py` fails when a document is neither published nor excluded, so adding, renaming or moving a doc forces a decision here. | Repo source | Wiki page | Note | | --- | --- | --- | | `docs/index.md` | Home | was README before the #7 restructure | | `docs/architecture.md` | Architecture | was `docs/ARCHITECTURE.md` | | `docs/runbooks/index.md` | Runbooks | new section | | `docs/runbooks/0001-deploy-and-rollback.md` | Runbook-0001-deploy-and-rollback | `Runbook-NNNN-*` for runbook pages | | `docs/adr/index.md` | Decisions | new section | | `docs/adr/0001-forgejo-actions-runner-on-guest.md` | ADR-0001-forgejo-actions-runner-on-guest | `ADR-NNNN-*`, as the hook already did | | `docs/actions/index.md` | Actions | new section | | `GLOSSARY.md` | Glossary | unchanged | | `docs/agents/*` | (excluded) | repo-only agent tooling | What changed from the parked hook's contract: Home is now the docs entry point, not the README; `docs/ARCHITECTURE.md` became `docs/architecture.md`; the runbooks, decisions and actions indexes are published too. Mirror semantics are unchanged. A sidebar listing the published sections is still this issue's to define. ## ADR-0001 numbering collision The parked wiki-sync branch adds `docs/adr/0001-repo-docs-canonical-wiki-mirror.md`, while `main` already carries `docs/adr/0001-forgejo-actions-runner-on-guest.md`. Two ADR-0001s. It has to be resolved before both land — renumber the parked one (it is unmerged, so it is the cheap one to move). #12 deliberately did not touch it, because the parked ADR is this issue's file.
Author
Owner

From #11. The coverage check the manifest promises is not in the repo (PR #18 landed docs/wiki-pages.yml without it) and #11 deliberately excluded tests, so it is filed here with the sync that has to read the manifest.

A first pass was implemented and verified (not committed anywhere yet): tests/check-wiki-pages.py, ~114 lines, stdlib only. It parses the manifest's exact shape, discovers every docs/**/*.md plus GLOSSARY.md, and fails (exit 1) with a per-document message when a document is neither published nor excluded — plus when an entry names a file that no longer exists (the rename case). Verified:

$ python3 tests/check-wiki-pages.py
OK: 11 documents covered (9 published, 2 excluded)

$ printf '# Scratch\n' > docs/scratch-tmp.md && python3 tests/check-wiki-pages.py
FAIL: docs/wiki-pages.yml does not cover the docs tree
  - coverage gap: docs/scratch-tmp.md is neither published nor excluded

$ mv docs/adr/0002-main-protected-at-the-forge.md docs/adr/0002-tmp-rename.md && python3 tests/check-wiki-pages.py
FAIL: docs/wiki-pages.yml does not cover the docs tree
  - coverage gap: docs/adr/0002-tmp-rename.md is neither published nor excluded
  - manifest entry does not exist in the tree: docs/adr/0002-main-protected-at-the-forge.md

Two known gaps to settle when this lands, so the check is not mistaken for complete:

  • Its discovery set is docs/**/*.md + GLOSSARY.md. README.md, AGENTS.md, LICENSE and tests/ sit outside the invariant, so the check would not flag them.
  • Nothing runs it yet — the repo has no workflow on main (a consequence of ADR 0002's "status checks off for now" is that nothing can require it). It belongs with the sync workflow this issue builds, which is where it would get exercised in CI.

The script text is available on request; it was left out of #11's PR to keep that change documentation-scoped per its ticket.

From #11. The coverage check the manifest promises is not in the repo (PR #18 landed `docs/wiki-pages.yml` without it) and #11 deliberately excluded tests, so it is filed here with the sync that has to read the manifest. A first pass was implemented and verified (not committed anywhere yet): `tests/check-wiki-pages.py`, ~114 lines, stdlib only. It parses the manifest's exact shape, discovers every `docs/**/*.md` plus `GLOSSARY.md`, and fails (exit 1) with a per-document message when a document is neither published nor excluded — plus when an entry names a file that no longer exists (the rename case). Verified: ``` $ python3 tests/check-wiki-pages.py OK: 11 documents covered (9 published, 2 excluded) $ printf '# Scratch\n' > docs/scratch-tmp.md && python3 tests/check-wiki-pages.py FAIL: docs/wiki-pages.yml does not cover the docs tree - coverage gap: docs/scratch-tmp.md is neither published nor excluded $ mv docs/adr/0002-main-protected-at-the-forge.md docs/adr/0002-tmp-rename.md && python3 tests/check-wiki-pages.py FAIL: docs/wiki-pages.yml does not cover the docs tree - coverage gap: docs/adr/0002-tmp-rename.md is neither published nor excluded - manifest entry does not exist in the tree: docs/adr/0002-main-protected-at-the-forge.md ``` Two known gaps to settle when this lands, so the check is not mistaken for complete: - Its discovery set is `docs/**/*.md` + `GLOSSARY.md`. `README.md`, `AGENTS.md`, `LICENSE` and `tests/` sit outside the invariant, so the check would not flag them. - Nothing runs it yet — the repo has no workflow on `main` (a consequence of ADR 0002's "status checks off for now" is that nothing can require it). It belongs with the sync workflow this issue builds, which is where it would get exercised in CI. The script text is available on request; it was left out of #11's PR to keep that change documentation-scoped per its ticket.
Author
Owner

Done — docs/adr/* replaces the three individual ADR entries, so a new decision no longer edits the manifest. The header comment now says entries may be globs (a whole section published the same way, with exclude for a specific one).

Note for the sync in #2: the manifest now carries globs, so the sync has to expand them rather than treat entries as exact paths. Left as-is rather than a second mechanism (e.g. a publish_dirs list) — the exclusion case you mention works unchanged under exclude: list the specific ADR path there and it wins.

Done — `docs/adr/*` replaces the three individual ADR entries, so a new decision no longer edits the manifest. The header comment now says entries may be globs (a whole section published the same way, with `exclude` for a specific one). Note for the sync in #2: the manifest now carries globs, so the sync has to expand them rather than treat entries as exact paths. Left as-is rather than a second mechanism (e.g. a `publish_dirs` list) — the exclusion case you mention works unchanged under `exclude`: list the specific ADR path there and it wins.
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
olympus/infra-forge#2
No description provided.