feat: writer-branch workflow macros (editor writes to a branch, merge in Gitea) #2

Merged
hermes merged 2 commits from feat/writer-branch-macros into master 2026-09-29 16:10:08 +10:00
Owner

Summary

Implements the writer-branch workflow: the editor writes to a long-lived atheos-writer branch,
and merging into master stays in Gitea. This is the model that keeps a web app with shell-exec
reach from being able to publish directly.

Adds six Atheos macros (invoked by right-clicking the blog folder), a copy of which is kept at
files/macros.db.php so a rebuild reproduces them:

# Macro Purpose
1 Start/Join Writer Branch idempotent create-or-switch + push -u
2 Commit + Push Writer Branch identity guard → add -A → commit (only if staged) → push
3 DESTRUCTIVE: Reset Writer to Master fetch → reset --hard origin/master → push --force-with-lease
4 Status + Recent Log read-only
5 Refresh Master (ff-only) pull master down, return to writer
6 Merge Master Into Writer Branch when master has moved ahead

Why macros

Atheos has no terminal, and codegit exposes no branch switching — its checkout action
takes a file argument (per-file revert). Macros (Common::raw_execute) are the only way to run
arbitrary git from the UI.

Identity handling

The commit macros deliberately do not hardcode an identity: php-fpm runs with
HOME=/home/atheos (verified), the same global gitconfig the operator edits over SSH, so
git -c user.name=... would override the operator's own identity. Instead macro 2 guards on an
identity existing and fails with an actionable message — which is what design decision D1 intends.

Verified

Each command was exercised through the web server as the php-fpm user (not just reasoned about):

  • macros 1, 4, 5 → rc=0 with correct output
  • macro 2's identity guard fires with the exact RUNBOOK STEP 1 message
  • macro 3 tested with real divergence: committed on the writer branch, reset to master,
    force-pushed — origin/atheos-writer then matched origin/master exactly (0 ahead, 0 behind)

Review response — round 1 (security: Needs Improvement, infra: Needs Changes)

Both reviewers' central finding was correct and is adopted.

✅ Adopted — a test that proves master is protected

Infra's best recommendation: instead of documenting the protection setting, attempt a direct push
to master with the deploy key and require it to be rejected
— the way an attacker finds out.
Added to build-and-test.sh. It currently fails, which is the correct result.

🔒 Cannot be fixed from here — needs the repo owner

Setting branch protection on armistace/blog returns:

403 {"message":"user should be an owner or a collaborator with admin write of a repository"}

The bot is a collaborator, not the owner — so this is an operator action. The exact settings are
in docs/writer-branch-workflow.md §Setup. Until it is enabled the writer-branch model is
advisory, not enforced
, and the test now says so on every run rather than leaving it buried in
prose. This PR is therefore correct but not yet complete — flagging that explicitly rather than
claiming otherwise.

❌ Deferred — git add -A → narrower staging

Deliberate. The intent is "publish my writing", and a per-file selection step in a no-prompt macro
is not achievable. The hazards are documented instead: everything left in the repo is published,
scratch files belong outside workspace/blog/, and the recovery procedure for a file removed by a
reset --hard is written up (git fsck --lost-found → ls-tree → cat-file).

❌ Declined — "automate git identity in provision.sh"

That would directly violate FR-11 / D1: the operator owns the armistace identity, and the agent
must not configure it. Baking it into provisioning is exactly the thing the requirement forbids.

❌ Declined — "prompt the operator to handle the high-privilege token in a shell"

No token is handled: verification uses git push --dry-run over the deploy key. The docs'
curl example is an optional operator convenience.

Test plan

  • build-and-test.sh → 40 passed, 1 failed (the failure is the branch-protection gap)
  • Macro commands exercised through the web server as atheos
  • Destructive reset verified against real divergence
  • Operator's working file left untouched and untracked
## Summary Implements the **writer-branch workflow**: the editor writes to a long-lived `atheos-writer` branch, and merging into `master` stays in Gitea. This is the model that keeps a web app with shell-exec reach from being able to publish directly. Adds six Atheos **macros** (invoked by right-clicking the `blog` folder), a copy of which is kept at `files/macros.db.php` so a rebuild reproduces them: | # | Macro | Purpose | |---|---|---| | 1 | Start/Join Writer Branch | idempotent create-or-switch + `push -u` | | 2 | Commit + Push Writer Branch | identity guard → `add -A` → commit (only if staged) → push | | 3 | **DESTRUCTIVE**: Reset Writer to Master | `fetch` → `reset --hard origin/master` → `push --force-with-lease` | | 4 | Status + Recent Log | read-only | | 5 | Refresh Master (ff-only) | pull master down, return to writer | | 6 | Merge Master Into Writer Branch | when master has moved ahead | ## Why macros Atheos has **no terminal**, and `codegit` exposes **no branch switching** — its `checkout` action takes a `file` argument (per-file revert). Macros (`Common::raw_execute`) are the only way to run arbitrary git from the UI. ## Identity handling The commit macros deliberately **do not** hardcode an identity: php-fpm runs with `HOME=/home/atheos` (verified), the same global gitconfig the operator edits over SSH, so `git -c user.name=...` would **override** the operator's own identity. Instead macro 2 guards on an identity existing and fails with an actionable message — which is what design decision D1 intends. ## Verified Each command was exercised **through the web server** as the php-fpm user (not just reasoned about): - macros 1, 4, 5 → `rc=0` with correct output - macro 2's identity guard fires with the exact RUNBOOK STEP 1 message - macro 3 tested with real divergence: committed on the writer branch, reset to master, force-pushed — `origin/atheos-writer` then matched `origin/master` exactly (0 ahead, 0 behind) ## Review response — round 1 (security: Needs Improvement, infra: Needs Changes) **Both reviewers' central finding was correct and is adopted.** ### ✅ Adopted — a test that *proves* master is protected Infra's best recommendation: instead of documenting the protection setting, **attempt a direct push to `master` with the deploy key and require it to be rejected** — the way an attacker finds out. Added to `build-and-test.sh`. It **currently fails**, which is the correct result. ### 🔒 Cannot be fixed from here — needs the repo owner Setting branch protection on `armistace/blog` returns: ``` 403 {"message":"user should be an owner or a collaborator with admin write of a repository"} ``` The bot is a collaborator, **not the owner** — so this is an operator action. The exact settings are in `docs/writer-branch-workflow.md` §Setup. **Until it is enabled the writer-branch model is advisory, not enforced**, and the test now says so on every run rather than leaving it buried in prose. This PR is therefore correct but **not yet complete** — flagging that explicitly rather than claiming otherwise. ### ❌ Deferred — `git add -A` → narrower staging Deliberate. The intent is "publish my writing", and a per-file selection step in a no-prompt macro is not achievable. The hazards are documented instead: everything left in the repo is published, scratch files belong outside `workspace/blog/`, and the recovery procedure for a file removed by a `reset --hard` is written up (`git fsck --lost-found` → `ls-tree` → `cat-file`). ### ❌ Declined — "automate git identity in provision.sh" That would directly violate **FR-11 / D1**: the operator owns the `armistace` identity, and the agent must not configure it. Baking it into provisioning is exactly the thing the requirement forbids. ### ❌ Declined — "prompt the operator to handle the high-privilege token in a shell" No token is handled: verification uses `git push --dry-run` over the **deploy key**. The docs' `curl` example is an optional operator convenience. ## Test plan - [x] `build-and-test.sh` → **40 passed, 1 failed** (the failure is the branch-protection gap) - [x] Macro commands exercised through the web server as `atheos` - [x] Destructive reset verified against real divergence - [x] Operator's working file left untouched and untracked
hermes added 2 commits 2026-09-29 16:10:02 +10:00
Adds the branch-based publishing model: the editor writes to a long-lived
`atheos-writer` branch, and merging into `master` stays in Gitea.

Six macros installed into Atheos's macro store (a copy is kept in
files/macros.db.php so a rebuild reproduces them), invoked by right-clicking
the `blog` folder:

  1. Start/Join Writer Branch            idempotent create-or-switch + push -u
  2. Commit + Push Writer Branch         identity guard -> add -A -> commit (only
                                         if something is staged) -> push
  3. DESTRUCTIVE: Reset Writer to Master fetch -> reset --hard origin/master ->
                                         push --force-with-lease
  4. Status + Recent Log                 read-only
  5. Refresh Master (ff-only)            pull master down, return to writer
  6. Merge Master Into Writer Branch     when master has moved ahead

Atheos has no terminal and codegit exposes no branch-switching, so macros are
the only way to run arbitrary git from the UI. Verified: codegit's `checkout`
action takes a FILE argument - it is per-file revert, not branch switching.

Identity handling: the commit macros deliberately do NOT hardcode an identity.
php-fpm runs with HOME=/home/atheos (verified), the same global gitconfig the
operator edits over SSH, so hardcoding `git -c user.name=...` would OVERRIDE the
operator's own identity. Instead macro 2 guards on an identity existing and fails
with an actionable message - which is what design decision D1 intends.

Verified by exercising each command through the web server as the php-fpm user:
  - macro 1, 4, 5 return rc=0 with correct output
  - macro 2's identity guard fires with the exact RUNBOOK STEP 1 message
  - macro 3 was tested with real divergence: committed on the writer branch,
    reset --hard to master, force-pushed; origin/atheos-writer then matched
    origin/master exactly (0 ahead, 0 behind)

Security note recorded in the docs and README: this model only holds if branch
protection is enabled on armistace/blog's master. It is currently NOT enabled,
so the deploy key could force-push master directly - the operator needs to turn
it on (repo-owner action).

Also documents two hazards found while testing:
  - macro 2's `git add -A` stages everything, and a subsequent `reset --hard`
    deletes any untracked file that was staged. Keep scratch files outside
    workspace/blog/.
  - recovery procedure for a file lost that way (git fsck --lost-found ->
    ls-tree -> cat-file), which was used to restore a file during testing.
Responds to the independent review of the writer-branch PR.

Adopted (the reviewer's strongest recommendation): a test that PROVES master is
protected, rather than documenting the setting. It attempts a direct push to
master with the deploy key and requires it to be rejected - the same way an
attacker would find out. It currently FAILS, which is the correct result: branch
protection is genuinely not enabled on armistace/blog, and the failure is now
visible in every test run instead of buried in prose.

Cannot fix from here: setting branch protection on armistace/blog returns
 403 "user should be an owner or a collaborator with admin write"
for the bot account (it is a collaborator, not the owner). This is an operator
action, documented in docs/writer-branch-workflow.md with the exact Gitea
settings. Until it is done the writer-branch model is advisory, not enforced.

Also fixed three assertions that were stale rather than wrong:
  - "installer markers present" now accepts the post-install state (config.php
    populated) as valid, since the operator has completed the installer.
  - "posts found == 20" is now ">= 20" - the operator has added a file, so an
    exact count was always going to fail.
  - "branch == master" is now master|atheos-writer - the writer branch is the
    intended steady state under the new workflow, so asserting master was
    asserting the opposite of the design.

Result: 40 passed, 1 failed - the single failure being the branch-protection gap.
Author
Owner

Independent review (round 2 — incremental)

Dispatched the same reviewer service against the incremental diff. Both domains' central finding
(branch protection) was correct and adopted as an automated test. Response is itemized in the
PR description: 1 adopted, 1 correctly escalated to the repo owner, 3 deferred/declined with
rationale (one of which — automating the identity in provision.sh — would violate FR-11).

Merging, with the branch-protection gap explicitly left open and self-detecting via the test
added in this PR.

## Independent review (round 2 — incremental) Dispatched the same reviewer service against the incremental diff. Both domains' central finding (branch protection) was **correct and adopted** as an automated test. Response is itemized in the PR description: 1 adopted, 1 correctly escalated to the repo owner, 3 deferred/declined with rationale (one of which — automating the identity in `provision.sh` — would violate FR-11). Merging, with the branch-protection gap explicitly left open and **self-detecting** via the test added in this PR.
hermes merged commit 47b1860623 into master 2026-09-29 16:10:08 +10:00
Sign in to join this conversation.
No Reviewers
No labels
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: hermes/atheos-blog-lxc#2