diff --git a/.tangled/workflows/mirror.yml b/.tangled/workflows/mirror.yml
index f055085..5b1bbc4 100644
--- a/.tangled/workflows/mirror.yml
+++ b/.tangled/workflows/mirror.yml
@@ -1,6 +1,7 @@
# mirrors main to github and gitea on every push. tangled is the source of
-# truth, so both get force-pushed to match it. needs the GH_MIRROR_TOKEN and
-# GITEA_MIRROR_TOKEN secrets
+# truth, so both get force-pushed to match it. the same file works in every
+# repo: it pushes to the repo with the same name on github and gitea.
+# needs the GH_MIRROR_TOKEN and GITEA_MIRROR_KEY secrets
when:
- event: ["push", "manual"]
@@ -8,6 +9,10 @@ when:
engine: "nixery"
+dependencies:
+ nixpkgs:
+ - openssh
+
steps:
# spindle clones one commit deep (depth: 0 means 1 too), and the mirrors
# need the whole history. this also points origin/main at the newest main,
@@ -20,9 +25,22 @@ steps:
- name: "Mirror to GitHub"
command: |
set -euo pipefail
- git push --force "https://x-access-token:${GH_MIRROR_TOKEN}@github.com/daniel-daum/saturn.git" refs/remotes/origin/main:refs/heads/main
+ git push --force "https://x-access-token:${GH_MIRROR_TOKEN}@github.com/daniel-daum/${TANGLED_REPO_NAME}.git" refs/remotes/origin/main:refs/heads/main
+ # over ssh to git-ssh, which skips cloudflare. https to git.danieldaum.net
+ # goes through it, and its bot protection blocks spindle at random
- name: "Mirror to Gitea"
command: |
set -euo pipefail
- git push --force "https://daniel-daum:${GITEA_MIRROR_TOKEN}@git.danieldaum.net/daniel-daum/saturn.git" refs/remotes/origin/main:refs/heads/main
+
+ # ssh refuses to run without a passwd entry, and nixery images have none
+ mkdir -p /etc
+ [ -e /etc/passwd ] || echo "root:x:0:0:root:${HOME}:/bin/bash" > /etc/passwd
+
+ mkdir -p "${HOME}/.ssh"
+ printf '%s\n' "${GITEA_MIRROR_KEY}" > "${HOME}/.ssh/gitea"
+ chmod 600 "${HOME}/.ssh/gitea"
+ ssh-keyscan -p 2222 git-ssh.danieldaum.net > "${HOME}/.ssh/known_hosts"
+
+ export GIT_SSH_COMMAND="ssh -i ${HOME}/.ssh/gitea -o IdentitiesOnly=yes -o UserKnownHostsFile=${HOME}/.ssh/known_hosts"
+ git push --force "ssh://git@git-ssh.danieldaum.net:2222/daniel-daum/${TANGLED_REPO_NAME}.git" refs/remotes/origin/main:refs/heads/main
diff --git a/internal/site-guide.md b/internal/site-guide.md
index 63c9c59..28b567c 100644
--- a/internal/site-guide.md
+++ b/internal/site-guide.md
@@ -255,17 +255,17 @@ A project shows up in three places, newest first:
**Pills.** `
` with `#new`. `#in-development` also carries `pill--dev`, a pulsing dot that is commented out in `global.css` for now. They're grey, never lime. Take them off when the project isn't new any more.
-**Commit log.** `scripts/sync-commits.mjs` reads recent commits from my Gitea's public API and writes them between `` markers. It runs with the hourly sync, or locally with `mise run sync-commits`. To add a repo, put the markers inside a `.commit-log` and add the page to `PAGES` in the script. On the `/projects/` card, the log is `aria-hidden` because the whole card is a link, and it hides the hash to save room.
+**Commit log.** `scripts/sync-commits.mjs` reads recent commits from the Tangled knot (`knot1.tangled.sh`, `sh.tangled.repo.log`, no login) and writes them between `` markers. It runs with the hourly sync, or locally with `mise run sync-commits`. To add a repo, put the markers inside a `.commit-log` and add the page to `PAGES` in the script. On the `/projects/` card, the log is `aria-hidden` because the whole card is a link, and it hides the hash to save room.
## 8. ci (tangled spindle)
Three workflows in `.tangled/workflows/`, run by the public spindle (`spindle.tangled.sh`). Status shows under Pipelines on the repo page.
- **deploy.yml** (every push to `main`): stages the public files and runs `npx wrangler@4 pages deploy`.
-- **mirror.yml** (every push to `main`): force-pushes the newest `main` to GitHub and Gitea. Never push to those directly; it gets overwritten.
+- **mirror.yml** (every push to `main`): force-pushes the newest `main` to GitHub (https + token) and Gitea (ssh to `git-ssh.danieldaum.net:2222`, which skips Cloudflare; https to `git.danieldaum.net` goes through Cloudflare's bot protection, which blocks spindle at random). Never push to those directly; it gets overwritten.
- **sync.yml** (hourly, plus Retry): checks out the newest `main`, runs the three sync scripts, and if anything changed commits "chore: sync" and pushes back over SSH, which runs deploy and mirror.
-**Secrets** (Tangled → saturn → Settings → Pipelines): `CLOUDFLARE_API_TOKEN`, `CLOUDFLARE_ACCOUNT_ID`, `GH_MIRROR_TOKEN` (GitHub, contents write on this repo), `GITEA_MIRROR_TOKEN`, `ATPROTO_APP_PASSWORD`, `TANGLED_PUSH_KEY` (private half of the `saturn-spindle` SSH key on my Tangled account).
+**Secrets** (Tangled → saturn → Settings → Pipelines): `CLOUDFLARE_API_TOKEN`, `CLOUDFLARE_ACCOUNT_ID`, `GH_MIRROR_TOKEN` (GitHub, contents write on this repo), `GITEA_MIRROR_KEY` (private half of the `spindle-mirror` SSH key on my Gitea account), `ATPROTO_APP_PASSWORD`, `TANGLED_PUSH_KEY` (private half of the `saturn-spindle` SSH key on my Tangled account).
**Gotchas** (nixery engine):
diff --git a/mise.toml b/mise.toml
index 47555eb..7a139a7 100644
--- a/mise.toml
+++ b/mise.toml
@@ -19,7 +19,7 @@ run = "node scripts/sync-standard.mjs"
# mise run sync-commits
[tasks.sync-commits]
-description = "Copy recent commits from my gitea into the project pages"
+description = "Copy recent commits from tangled into the project pages"
run = "node scripts/sync-commits.mjs"
# mise run words now/sep-2026/index.html
diff --git a/projects/holonet/index.html b/projects/holonet/index.html
index 373cc80..21e6a96 100644
--- a/projects/holonet/index.html
+++ b/projects/holonet/index.html
@@ -204,12 +204,17 @@
diff --git a/scripts/sync-commits.mjs b/scripts/sync-commits.mjs
index ee19148..91e8fe9 100644
--- a/scripts/sync-commits.mjs
+++ b/scripts/sync-commits.mjs
@@ -1,4 +1,5 @@
-// pulls recent commits from my gitea and writes them into the static html.
+// pulls recent commits from my repos on tangled and writes them into the
+// static html.
// run with `mise run sync-commits`. no dependencies, node 18+.
//
// a page opts in with a pair of marker comments naming the repo and how many
@@ -6,13 +7,14 @@
//
// ...
//
-// commits come from the repo's default branch, newest first. if gitea can't
-// be reached, the script exits non-zero and touches nothing.
+// commits come from main, newest first, read from the knot that hosts my
+// repos (no login needed). if it can't be reached, the script exits non-zero
+// and touches nothing.
import { readFile, writeFile } from "node:fs/promises";
-const GITEA = "https://git.danieldaum.net";
-const OWNER = "daniel-daum";
+const KNOT = "https://knot1.tangled.sh";
+const OWNER = "did:plc:e3z5gj5iyuwgqao562bo4bdf"; // danieldaum.net
const TZ = "America/Los_Angeles";
const PAGES = ["projects/index.html", "projects/holonet/index.html"];
@@ -23,15 +25,14 @@ const MARKER = /^([ \t]*)[\s\S]*?