Merge pull request 'feat: writer-branch workflow macros (editor writes to a branch, merge in Gitea)' (#2) from feat/writer-branch-macros into master
This commit was merged in pull request #2.
This commit is contained in:
@@ -55,10 +55,12 @@ posts and commit them to Gitea — without VS Code, without a cloud IDE, and wit
|
||||
├── RUNBOOK.md ← operational guide; START AT STEP 1
|
||||
├── docs/
|
||||
│ ├── security-notes.md credential model + hardening TODOs if ever exposed
|
||||
│ └── resiliency-notes.md what a non-PoC deployment would need
|
||||
│ ├── resiliency-notes.md what a non-PoC deployment would need
|
||||
│ └── writer-branch-workflow.md how writing/publishing works from the editor
|
||||
├── files/
|
||||
│ ├── atheos.conf nginx vhost (install to /etc/nginx/http.d/atheos.conf)
|
||||
│ └── ssh-config ~atheos/.ssh/config (the gitea-blog alias)
|
||||
│ ├── ssh-config ~atheos/.ssh/config (the gitea-blog alias)
|
||||
│ └── macros.db.php installed copy of the Atheos writer macros
|
||||
├── scripts/
|
||||
│ ├── provision.sh rebuild the whole guest from scratch
|
||||
│ └── update-atheos.sh update Atheos to latest upstream + re-pin
|
||||
@@ -99,6 +101,12 @@ and [`docs/security-notes.md`](docs/security-notes.md).
|
||||
|
||||
## Security at a glance
|
||||
|
||||
- **Writing happens on a branch, never on `master`.** The editor commits to a long-lived
|
||||
`atheos-writer` branch and you merge via PR in Gitea — see
|
||||
[`docs/writer-branch-workflow.md`](docs/writer-branch-workflow.md).
|
||||
⚠️ This only holds if **branch protection is enabled on `armistace/blog`'s `master`** — without it
|
||||
the deploy key could force-push master directly. Confirm with the `branch_protections` API call in
|
||||
that document.
|
||||
- Git auth: **deploy key scoped to `armistace/blog`**, private key mode `600`, owned by the php-fpm user.
|
||||
- The SSH alias uses `IdentitiesOnly yes`, so no other key is ever offered.
|
||||
- The web app runs as a **dedicated unprivileged user** (`atheos`), which also owns the workspace.
|
||||
|
||||
+23
@@ -296,3 +296,26 @@ ssh root@192.168.178.69 "pct exec 136 -- sh -c 'echo \"<your-pubkey>\" >> /root/
|
||||
| Date | Change |
|
||||
|---|---|
|
||||
| 2026-09-29 | Initial build: LXC 136 on scotty, Alpine 3.23.2, nginx + php83-fpm (socket), Atheos pinned at `f90cd18` (build 611), `armistace/blog` cloned over HTTPS, per-repo deploy key generated. LAN-only. |
|
||||
| 2026-09-29 | Added the **writer-branch workflow**: 6 macros installed into Atheos (start/join, commit+push, destructive reset-to-master, status, refresh master, merge master) writing to a long-lived `atheos-writer` branch; merging stays in Gitea. See `docs/writer-branch-workflow.md`. |
|
||||
|
||||
## Writer-branch workflow (quick reference)
|
||||
|
||||
The editor writes to `atheos-writer`; you merge to `master` in Gitea.
|
||||
|
||||
**Macros** (right-click the `blog` folder → Macros):
|
||||
|
||||
| # | Macro | Purpose |
|
||||
|---|---|---|
|
||||
| 1 | Start/Join Writer Branch | creates or re-joins `atheos-writer` (idempotent) |
|
||||
| 2 | Commit + Push Writer Branch | the everyday button |
|
||||
| 3 | **DESTRUCTIVE**: Reset Writer Branch to Master | wipes writer's commits, keeps your files |
|
||||
| 4 | Status + Recent Log | read-only |
|
||||
| 5 | Refresh Master (ff-only) | pull master's merges down |
|
||||
| 6 | Merge Master Into Writer Branch | when master has moved ahead |
|
||||
|
||||
⚠️ **Requires branch protection on `armistace/blog`'s `master`** — it is **not currently enabled**,
|
||||
so the deploy key can force-push master directly. See `docs/writer-branch-workflow.md` §Setup.
|
||||
⚠️ **Macro 2 uses `git add -A`** — everything left in the repo gets published to Gitea. Keep scratch
|
||||
files outside `workspace/blog/`.
|
||||
|
||||
Full detail, recovery steps and caveats: [`docs/writer-branch-workflow.md`](docs/writer-branch-workflow.md).
|
||||
|
||||
@@ -0,0 +1,125 @@
|
||||
# Writer-Branch Workflow
|
||||
|
||||
How writing and publishing work from the Atheos editor, and why the branch is structured this way.
|
||||
|
||||
## The model
|
||||
|
||||
The editor **never touches `master`**. It writes to a long-lived branch (`atheos-writer`), and
|
||||
merging into `master` is done by the operator in Gitea.
|
||||
|
||||
```
|
||||
┌──────────────────────────────────────────┐
|
||||
Atheos editor ────┤ atheos-writer (long-lived) │
|
||||
(macros) │ • write here │
|
||||
│ • commit + push here │
|
||||
│ • can be reset --hard back to master │
|
||||
└───────────────┬──────────────────────────┘
|
||||
│ operator opens a PR in Gitea
|
||||
│ operator reviews
|
||||
▼
|
||||
master (protected)
|
||||
│
|
||||
▼
|
||||
existing Pelican build pipeline
|
||||
```
|
||||
|
||||
**Why:** the editor is a web app with shell-exec reach (`codegit` + `macro` both run commands as the
|
||||
php-fpm user). Bounding what it can reach is the whole point of design decision D6. Keeping `master`
|
||||
behind a PR means a compromised or fat-fingered editor **cannot** publish directly — it can only
|
||||
propose.
|
||||
|
||||
## Setup — do these once
|
||||
|
||||
### 1. Set your git identity (required, RUNBOOK STEP 1)
|
||||
|
||||
```sh
|
||||
ssh root@192.168.178.57
|
||||
su -s /bin/sh atheos
|
||||
git config --global user.name "Your Name"
|
||||
git config --global user.email "you@example.com"
|
||||
```
|
||||
|
||||
This is the *same* global gitconfig php-fpm uses (`HOME=/home/atheos` — verified), so macros commit
|
||||
as **you**. If it is not set, macro **2** refuses with an actionable error rather than
|
||||
committing under a bogus identity.
|
||||
|
||||
### 2. ⚠️ Protect `master` on the blog repo
|
||||
|
||||
**Not yet enabled — the deploy key can currently force-push `master` directly**, which defeats the
|
||||
whole model. Set it in Gitea (repo owner only):
|
||||
|
||||
`armistace/blog` → **Settings** → **Branches** → add rule for `master`:
|
||||
|
||||
| Setting | Value |
|
||||
|---|---|
|
||||
| **Enable push** | **off** ← direct pushes blocked, including the deploy key |
|
||||
| **Enable merge whitelist** | **on** → add `armistace` |
|
||||
| Require approvals | optional (you're the only human here) |
|
||||
|
||||
Verify:
|
||||
|
||||
```bash
|
||||
curl -s -H "Authorization: token $GITEA_TOKEN" \
|
||||
https://git.aridgwayweb.com/api/v1/repos/armistace/blog/branch_protections
|
||||
```
|
||||
|
||||
### 3. The macros
|
||||
|
||||
Installed directly into Atheos's macro store (`data/macros.db.php`); a copy is kept in this repo at
|
||||
`files/macros.db.php` so a rebuild reproduces them. They appear when you **right-click the `blog`
|
||||
folder** in the file tree → **Macros**.
|
||||
|
||||
| # | Macro | Command | Notes |
|
||||
|---|---|---|---|
|
||||
| 1 | **Start/Join Writer Branch** | `git checkout atheos-writer \|\| git checkout -b atheos-writer` then `push -u` | idempotent — safe to run repeatedly |
|
||||
| 2 | **Commit + Push Writer Branch** | identity guard → `git add -A` → commit (only if staged changes) → `git push origin atheos-writer` | the everyday button |
|
||||
| 3 | **DESTRUCTIVE: Reset Writer Branch to Master** | `git fetch` → `git reset --hard origin/master` → `git push --force-with-lease` | wipes the branch's commits, keeps your files |
|
||||
| 4 | **Status + Recent Log** | `git status --short --branch`, `git log --oneline -8`, `git branch` | read-only |
|
||||
| 5 | **Refresh Master (ff-only)** | `git checkout master` → `git pull --ff-only` → back to `atheos-writer` | brings master's merges down |
|
||||
| 6 | **Merge Master Into Writer Branch** | `git merge origin/master` → push | for when master has moved ahead |
|
||||
|
||||
Output is returned into the UI — you see git's actual stdout/stderr, not a silent success.
|
||||
|
||||
## Day-to-day
|
||||
|
||||
1. Right-click `blog` → Macros → **1. Start/Join Writer Branch** (first time; also safe later)
|
||||
2. Write your post, save
|
||||
3. Right-click `blog` → Macros → **2. Commit + Push Writer Branch**
|
||||
4. Open a PR: `atheos-writer` → `master`
|
||||
5. Review the diff in Gitea, merge
|
||||
|
||||
### ⚠️ Important: macro 2 uses `git add -A`
|
||||
|
||||
`git add -A` stages **everything** in the repo, including files you may not intend to publish
|
||||
(drafts, scratch files, `.orig`/`.bak` leftovers). A `reset --hard` will also **delete any untracked
|
||||
file that has been staged**.
|
||||
|
||||
Practical consequences:
|
||||
|
||||
- `atheos-writer` is a **publishing** branch — everything you leave in the tree gets committed and
|
||||
pushed to Gitea.
|
||||
- If you want a scratch area, keep it **outside** the repo (e.g. `workspace/scratch/`, not
|
||||
`workspace/blog/`).
|
||||
- To unstage before a reset: `git reset` (clears the index without touching your files).
|
||||
|
||||
## Recovering a deleted file
|
||||
|
||||
`reset --hard` does not touch the object database, so a file that was ever staged or committed is
|
||||
still recoverable:
|
||||
|
||||
```sh
|
||||
su -s /bin/sh atheos
|
||||
cd /var/www/atheos/workspace/blog
|
||||
git fsck --lost-found | grep commit # find dangling commits
|
||||
git ls-tree <commit> path/to/file # confirm it is there
|
||||
git cat-file -p <blob-sha> > path/to/file # restore it
|
||||
```
|
||||
|
||||
`git reflog --all` also shows recently discarded commits. This is how the empty
|
||||
`src/content/please_no_facebook.md` was recovered after a `reset --hard` in testing.
|
||||
|
||||
## Changing the branch name
|
||||
|
||||
Edit macros 1, 2, 3, 5 and 6 in **Settings → Macro** and replace `atheos-writer`. The branch name is
|
||||
literal in each command — there is no variable. (Macros have no input prompt, so a
|
||||
branch name that changes per task would need edit-then-run.)
|
||||
@@ -0,0 +1,78 @@
|
||||
<?php/*|[
|
||||
{
|
||||
"uuid": "macro-947e-4763-b7b6f2",
|
||||
"title": "Execute",
|
||||
"type": "file",
|
||||
"fTypes": [
|
||||
"sh"
|
||||
],
|
||||
"command": "%PATH%",
|
||||
"icon": "fas fa-magic",
|
||||
"owner": "ATHEOS"
|
||||
},
|
||||
{
|
||||
"uuid": "macro-53a0-4ebb-56e800",
|
||||
"title": "Execute",
|
||||
"type": "file",
|
||||
"fTypes": [
|
||||
"php"
|
||||
],
|
||||
"command": "cd %FOLDER% && php %BASENAME%",
|
||||
"icon": "fas fa-magic",
|
||||
"owner": "ATHEOS"
|
||||
},
|
||||
{
|
||||
"uuid": "macro-writer-0001-start",
|
||||
"title": "1. Start/Join Writer Branch",
|
||||
"type": "folder",
|
||||
"fTypes": "N/A",
|
||||
"command": "cd %PATH% && (git checkout atheos-writer 2>/dev/null || git checkout -b atheos-writer) && git push -u origin atheos-writer",
|
||||
"icon": "fas fa-code-branch",
|
||||
"owner": "armistace"
|
||||
},
|
||||
{
|
||||
"uuid": "macro-writer-0002-commit",
|
||||
"title": "2. Commit + Push Writer Branch",
|
||||
"type": "folder",
|
||||
"fTypes": "N/A",
|
||||
"command": "cd %PATH% && (git config --get user.email >/dev/null || { echo 'ERROR: no git identity set. Run over SSH: git config --global user.name \"Your Name\" && git config --global user.email \"you@example.com\" (RUNBOOK STEP 1)'; exit 1; }) && git add -A && (git diff --cached --quiet || git commit -m \"writer: $(date +%F_%H%M)\") && git push origin atheos-writer",
|
||||
"icon": "fas fa-cloud-upload-alt",
|
||||
"owner": "armistace"
|
||||
},
|
||||
{
|
||||
"uuid": "macro-writer-0003-reset",
|
||||
"title": "3. DESTRUCTIVE: Reset Writer Branch to Master",
|
||||
"type": "folder",
|
||||
"fTypes": "N/A",
|
||||
"command": "cd %PATH% && git fetch origin && git reset --hard origin/master && git push --force-with-lease origin atheos-writer",
|
||||
"icon": "fas fa-exclamation-triangle",
|
||||
"owner": "armistace"
|
||||
},
|
||||
{
|
||||
"uuid": "macro-writer-0004-status",
|
||||
"title": "4. Status + Recent Log",
|
||||
"type": "folder",
|
||||
"fTypes": "N/A",
|
||||
"command": "cd %PATH% && git status --short --branch && echo '--- recent commits:' && git log --oneline -8 && echo '--- local branches:' && git branch",
|
||||
"icon": "fas fa-info-circle",
|
||||
"owner": "armistace"
|
||||
},
|
||||
{
|
||||
"uuid": "macro-writer-0005-refresh",
|
||||
"title": "5. Refresh Master (ff-only)",
|
||||
"type": "folder",
|
||||
"fTypes": "N/A",
|
||||
"command": "cd %PATH% && git checkout master && git pull --ff-only && git checkout atheos-writer",
|
||||
"icon": "fas fa-sync-alt",
|
||||
"owner": "armistace"
|
||||
},
|
||||
{
|
||||
"uuid": "macro-writer-0006-pullmaster",
|
||||
"title": "6. Merge Master Into Writer Branch",
|
||||
"type": "folder",
|
||||
"fTypes": "N/A",
|
||||
"command": "cd %PATH% && (git config --get user.email >/dev/null || { echo 'ERROR: no git identity set. Run over SSH: git config --global user.name \"Your Name\" && git config --global user.email \"you@example.com\" (RUNBOOK STEP 1)'; exit 1; }) && git fetch origin && git merge origin/master -m \"merge master into atheos-writer\" && git push origin atheos-writer",
|
||||
"icon": "fas fa-code-merge",
|
||||
"owner": "armistace"
|
||||
}
|
||||
]|*/?>
|
||||
@@ -17,7 +17,14 @@ echo "=============================================================="
|
||||
CODE=$(wget -qO /tmp/idx.html --server-response http://127.0.0.1/ 2>&1 | sed -n 's/.*HTTP\/1\.[01] \([0-9]*\).*/\1/p' | head -1)
|
||||
chk "HTTP status for /" "$CODE" "200"
|
||||
if grep -qi '<title>Atheos IDE</title>' /tmp/idx.html 2>/dev/null; then ok "served page is the Atheos IDE"; else bad "served page is not Atheos"; fi
|
||||
if grep -qiE 'install|setup' /tmp/idx.html 2>/dev/null; then ok "first-run installer/login markers present"; else bad "no installer markers"; fi
|
||||
# Either state is valid: pre-install shows the installer, post-install shows the login/app.
|
||||
if grep -qiE 'install|setup' /tmp/idx.html 2>/dev/null; then
|
||||
ok "installer markers present (pre-install state)"
|
||||
elif [ -f "$W/config.php" ] && [ -s "$W/config.php" ]; then
|
||||
ok "installer already completed (config.php populated)"
|
||||
else
|
||||
bad "neither installer markers nor a populated config.php"
|
||||
fi
|
||||
SOCK=$(sed -n 's/^listen = //p' /etc/php83/php-fpm.d/www.conf | head -1)
|
||||
NGX=$(sed -n 's/.*fastcgi_pass unix:\([^;]*\);.*/\1/p' /etc/nginx/http.d/atheos.conf | head -1)
|
||||
echo " --- socket contract (I-1):"
|
||||
@@ -29,7 +36,8 @@ echo
|
||||
echo "=============================================================="
|
||||
echo "AC-3 'blog' is present with files that can be opened"
|
||||
echo "=============================================================="
|
||||
chk "posts found" "$(ls $B/src/content/*.md 2>/dev/null | wc -l | tr -d ' ')" "20"
|
||||
POSTS=$(ls $B/src/content/*.md 2>/dev/null | wc -l | tr -d ' ')
|
||||
if [ "$POSTS" -ge 20 ]; then ok "posts found ($POSTS, expected >= 20)"; else bad "posts found ($POSTS, expected >= 20)"; fi
|
||||
if [ -d "$W/workspace/blog/.git" ]; then ok "it is a git working copy"; else bad "not a git working copy"; fi
|
||||
chk "theme present" "$(ls $B/src/themes 2>/dev/null | tr -d ' ')" "cleanblog"
|
||||
chk "workspace owner" "$(stat -c '%U' $W/workspace/blog)" "atheos"
|
||||
@@ -56,7 +64,13 @@ case "$REMOTE" in
|
||||
gitea-blog:armistace/blog*) ok "remote targets armistace/blog via the deploy-key alias" ;;
|
||||
*) bad "remote does not target the blog repo" ;;
|
||||
esac
|
||||
chk "branch" "$(asuser "git -C $B rev-parse --abbrev-ref HEAD" 2>/dev/null)" "master"
|
||||
# A repo may legitimately sit on master, or on the long-lived writer branch (docs/writer-branch-workflow.md).
|
||||
BR=$(asuser "git -C $B rev-parse --abbrev-ref HEAD" 2>/dev/null)
|
||||
case "$BR" in
|
||||
master|main) ok "branch ($BR)" ;;
|
||||
atheos-writer) ok "branch ($BR - writer-branch workflow)" ;;
|
||||
*) bad "branch is unexpected: $BR" ;;
|
||||
esac
|
||||
|
||||
# Phase 2: once the SSH alias is in use, prove the transport actually authenticates
|
||||
case "$REMOTE" in
|
||||
@@ -92,6 +106,26 @@ for d in mysqld mariadbd postgres redis-server; do
|
||||
if command -v "$d" >/dev/null 2>&1; then bad "$d present - unexpected"; else ok "$d absent (correct)"; fi
|
||||
done
|
||||
|
||||
echo
|
||||
echo "=============================================================="
|
||||
echo "GUARD: is master PROTECTED on the blog repo? (the writer-branch model depends on it)"
|
||||
echo "=============================================================="
|
||||
# The whole branch-based workflow assumes the deploy key CANNOT push master. That is a
|
||||
# repo-side setting we cannot verify from here by inspection - so test it the way an
|
||||
# attacker would: attempt a direct push to master and require it to be REJECTED.
|
||||
if [ "${SKIP_MASTER_PUSH_TEST:-0}" = "1" ]; then
|
||||
echo " SKIPPED (SKIP_MASTER_PUSH_TEST=1)"
|
||||
else
|
||||
PUSH_OUT=$(asuser "git -C $B push origin HEAD:master --dry-run" 2>&1)
|
||||
if echo "$PUSH_OUT" | grep -qiE 'protected|not allowed|denied|forbidden|remote rejected'; then
|
||||
ok "master is protected - the deploy key CANNOT push it"
|
||||
else
|
||||
bad "master is NOT protected - the deploy key could push master directly!"
|
||||
echo " Enable it: armistace/blog -> Settings -> Branches -> add rule for 'master',"
|
||||
echo " Enable push = OFF, merge whitelist = armistace. See docs/writer-branch-workflow.md"
|
||||
fi
|
||||
fi
|
||||
|
||||
echo
|
||||
echo "=============================================================="
|
||||
echo "FR-11 GUARD: agent created NO identity or armistace credential"
|
||||
|
||||
Reference in New Issue
Block a user