chadtmiller.com/knotless
git that lives in your Atmosphere account
7
# Deploys main to knotless.dev once this commit's checks all pass,
# with deploy/deploy.sh over SSH, as a deploy from one's own checkout
# does. DEPLOY_SSH_KEY is granted to pushes to main alone, in the
# runner's secrets file on the host; the settings below are what
# deploy/local.env holds for a deploy by hand. The runner itself is
# restarted last, once this run is filed, and only if what it runs
# changed (deploy.sh).
#
# The image is built first, while the checks run, as a candidate,
# knotless:build-<commit>, which changes nothing that runs; once they
# pass, it goes up. Until then a deploy gives way to a newer commit: when
# main moves past it, before or during the build or while it waits for
# the checks, it stops there, removes its candidate and passes, leaving
# the deploy to the newer one, as it does when main has moved by the time
# the checks pass. A check that fails removes the candidate too, and the
# next build removes any other left behind. Once it goes up, it runs to
# its end: a newer commit's deploy waits for this one rather than cutting
# it off halfway (superseded: finish). Its checks can wait for a job
# behind other runs, so it waits for them over two steps, each under the
# runner's 30 minutes a step.
#
# Once up, the image is published as the release of the `knotless-server`
# package at main's version (docs/design/packages.md#images), if that
# version has none yet: `docker save` on the host, as an OCI image layout,
# into `knotless publish --oci` there, as the owner, with RELEASE_PASSWORD,
# an app password of theirs granted to pushes to main as release.yml's is.
# It is then `docker pull knotless.dev/chadtmiller.com/knotless/knotless-server`:
# one image for serve, the pages, the runner and the triager, as here.
on:
push:
refs: [refs/heads/main]
superseded: finish
image: docker:28-cli
secrets: [DEPLOY_SSH_KEY, RELEASE_PASSWORD]
env:
KNOTLESS_HOST: root@knotless.dev
KNOTLESS_DOMAIN: knotless.dev
KNOTLESS_DATA: /mnt/data/knotless
KNOTLESS_ADMIN_DIDS: did:plc:bcgltzqazw5tb6k2g3ttenbj
# The runner's account (ci.knotless.dev), the triager's
# (triage.knotless.dev) and the reviewer's (review.knotless.dev), which
# log in at /oauth/bot for an OAuth session that reads private
# repositories.
KNOTLESS_BOT_DIDS: did:plc:ejh2v3z5ieqhqgz3ybk6r35x,did:plc:hwfywp7r4cfumv54v2xk3jz7,did:plc:kqi2glvzerg4dt66k64co2sl
# knotless, and the owner's deploys elsewhere, kept off the runner host
# that runs anyone's code: each of their workflows given a secret names
# ci.knotless.dev in `runners`, and its secrets are sealed to it alone.
# Their other workflows name runner.knotless.dev, so this one runs none.
KNOTLESS_RUN_REPOS: chadtmiller.com/knotless chadtmiller.com/pds.js grain.social/grain grain.social/grain-ios
# Lands the merge queue in batches. Open pulls run on the runner host
# (--pulls-only there, deploy/runner-host), off this one's CPU.
KNOTLESS_RUN_ARGS: --queue
# The runner host's account (deploy/runner-host), which a new public
# repository names unless its owner names others: it runs anyone's.
KNOTLESS_DEFAULT_RUNNER_DID: did:plc:4bocpkn5zkwe4wuu4plrj5kt
# Labels issues with Clef, as its own account, whose labels count only
# for the scopes the owner names it a triager of (knotless triagers):
# here, and in every repository that names it.
KNOTLESS_TRIAGE_REPOS: chadtmiller.com/knotless
KNOTLESS_TRIAGE_ARGS: --model clef --scopes kind,area --discover
# Reviews pulls with Claude Code, as review.knotless.dev, once
# /etc/knotless/review.env holds its logins (deploy/deploy.sh): every
# open pull by the owner or a collaborator, asked or not (--all), in
# every repository the owner holds, and grain.social's (--owner).
KNOTLESS_REVIEW_REPOS: --owner chadtmiller.com --owner grain.social
# Five rounds, where a merge of the base into a pull spends none.
KNOTLESS_REVIEW_ARGS: --all --rounds 5
steps:
- name: tools
run: |
apk add --no-cache bash
# A script run with the knotless CLI, in one container on the host
# from the image running there, with the repository, ref and commit,
# and labeled with the commit for unless-moved to end.
cat > /usr/local/bin/on-host <<'EOF'
#!/bin/sh
exec env DOCKER_HOST="ssh://$KNOTLESS_HOST" docker run --rm \
--label "dev.knotless.deploy=$KNOTLESS_RUN_SHA" \
-e KNOTLESS_APPVIEW="https://$KNOTLESS_DOMAIN" \
-e REPO="knotless://$KNOTLESS_RUN_REPO" -e SHA="$KNOTLESS_RUN_SHA" \
-e REF="$KNOTLESS_RUN_REF" \
knotless:current sh -c "$1"
EOF
# Whether main has moved past this commit: 0 if it has, leaving
# ~/.moved for the steps after, 1 if not, 2 if it cannot tell.
cat > /usr/local/bin/moved <<'EOF'
#!/bin/sh
set -o pipefail
tip=$(on-host 'knotless refs "$REPO"' | awk -F '\t' -v ref="$KNOTLESS_RUN_REF" '$2 == ref { print $1 }') || exit 2
[ "$tip" != "$KNOTLESS_RUN_SHA" ] || exit 1
echo "$KNOTLESS_RUN_REF is at ${tip:-nothing} now, not $KNOTLESS_RUN_SHA: a newer commit will deploy"
touch ~/.moved
EOF
# Runs a command until it ends, as it ends, or until main moves past
# this commit, asked every 30s: then it is ended, with what it ran on
# the host (a build is cancelled with its client), and this passes.
cat > /usr/local/bin/unless-moved <<'EOF'
#!/bin/sh
setsid "$@" &
pid=$!
while :; do
i=0
while [ $i -lt 30 ] && kill -0 $pid 2>/dev/null; do sleep 1; i=$((i + 1)); done
kill -0 $pid 2>/dev/null || break
if moved; then
kill -TERM -$pid 2>/dev/null
wait $pid
ids=$(DOCKER_HOST="ssh://$KNOTLESS_HOST" docker ps -q --filter "label=dev.knotless.deploy=$KNOTLESS_RUN_SHA")
[ -z "$ids" ] || DOCKER_HOST="ssh://$KNOTLESS_HOST" docker rm -f $ids >/dev/null
exit 0
fi
done
wait $pid
EOF
chmod +x /usr/local/bin/on-host /usr/local/bin/moved /usr/local/bin/unless-moved
- name: key
run: |
mkdir -p ~/.ssh && chmod 700 ~/.ssh
printf '%s\n' "$DEPLOY_SSH_KEY" > ~/.ssh/id_ed25519 && chmod 600 ~/.ssh/id_ed25519
printf 'Host *\n StrictHostKeyChecking accept-new\n' > ~/.ssh/config
- name: build
run: |
# Unsure where main is, it builds: the deploy step asks again.
moved && exit 0
unless-moved deploy/deploy.sh build
[ ! -e ~/.moved ] || deploy/deploy.sh drop
- name: checks done
run: |
[ ! -e ~/.moved ] || exit 0
# 25 minutes over all seven, and never a failure: the next step judges.
unless-moved on-host '
end=$(($(date +%s) + 1500))
for wf in check test web lexicons shots docs e2e; do
left=$((end - $(date +%s)))
[ "$left" -gt 0 ] || break
knotless checks "$REPO" --ref "$REF" --commit "$SHA" --workflow "$wf" --wait "$left" || true
done' || true
[ ! -e ~/.moved ] || deploy/deploy.sh drop
- name: checks pass
run: |
[ ! -e ~/.moved ] || exit 0
if ! on-host '
end=$(($(date +%s) + 1500))
for wf in check test web lexicons shots docs e2e; do
left=$((end - $(date +%s)))
[ "$left" -gt 0 ] || left=1
out=$(knotless checks "$REPO" --ref "$REF" --commit "$SHA" --workflow "$wf" --wait "$left")
echo "$out"
case "$out" in success*) ;; *) echo "$wf has not passed: not deploying"; exit 1 ;; esac
done'; then
deploy/deploy.sh drop
exit 1
fi
- name: deploy
run: |
[ ! -e ~/.moved ] || exit 0
at=0
moved || at=$?
case $at in
0) deploy/deploy.sh drop && exit 0 ;;
2) echo "cannot tell where $KNOTLESS_RUN_REF is: not deploying"; deploy/deploy.sh drop; exit 1 ;;
esac
deploy/deploy.sh up
touch ~/.up
- name: image
run: |
[ ! -e ~/.moved ] && [ -e ~/.up ] || exit 0
[ -n "$RELEASE_PASSWORD" ] || { echo "no RELEASE_PASSWORD: not publishing the image"; exit 0; }
version=$(sed -n 's/^version = "\(.*\)"/\1/p' Cargo.toml | head -1)
if on-host "knotless releases \"\$REPO\" knotless-server | cut -f2 | grep -qxF '$version'"; then
echo "knotless-server $version is published already"
exit 0
fi
# The image as it runs, saved on the host and read there, in a
# container of it, given the password from this one's environment.
DOCKER_HOST="ssh://$KNOTLESS_HOST" docker save knotless:current |
KNOTLESS_PASSWORD="$RELEASE_PASSWORD" DOCKER_HOST="ssh://$KNOTLESS_HOST" docker run --rm -i \
-e KNOTLESS_APPVIEW="https://$KNOTLESS_DOMAIN" -e REPO="knotless://$KNOTLESS_RUN_REPO" \
-e SHA="$KNOTLESS_RUN_SHA" -e VERSION="$version" -e KNOTLESS_PASSWORD \
knotless:current sh -c '
set -e
dir=$(mktemp -d)
tar -x -C "$dir"
knotless publish "$REPO" knotless-server "$VERSION" --oci "$dir" --commit "$SHA"
'