Something went wrong. Try again.
Central platform for European atproto.<cc> country community websites atproto.eu
community atproto
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154#!/usr/bin/env bash# Community PR quality checker (ADVISORY). Scheduled on the NAS because Tangled does not fire# pull_request pipelines and `tg` is not available inside the Spindle. For each OPEN pull request it# checks out the PR head, runs the same gates as the pre-push hook (tests + copy-voice), and posts a# single pass/fail comment per PR round. Idempotent: it records `<rkey>:<updatedAt>` and comments once# per round, so a 30-minute cron never spams. When the contributor pushes a new round, updatedAt bumps# and it re-checks.## It is ADVISORY: Tangled has no required-status-check, so this informs the contributor + reviewer but# cannot block a merge. Replace with a blocking checks.yml once Tangled ships required PR checks.## SECURITY: a PR is untrusted code. The checks (npm ci / npm test / copy-voice) run inside a throwaway# container with NO secrets, NO host mounts beyond the checkout, so a malicious PR cannot reach the# NAS's Tangled keys or tg session. Only the trusted git + tg orchestration runs on the host.## Prereqs on the host:# - `tg` on PATH, authenticated (`tg auth login`) as the account that should post the comments# (a bot or @gui.do). Comments are attributed to that account.# - git, and a container runtime (docker/podman).# - Env (no infra defaults point anywhere real):# PR_CHECK_DIR working dir for the clone + dedupe state (required)# DOCKER_BIN container command (default: docker)# PR_CHECK_REPO target repo handle/repo (default: atcommons.eu/website)# DRY_RUN 1 = check + print, do NOT post comments (default: 0)# Schedule every ~30 min. Uses a lock, so overlapping cron ticks are safe.set -euo pipefail
DIR="${PR_CHECK_DIR:?set PR_CHECK_DIR=/path/on/host (clone + dedupe state live here)}"DOCKER="${DOCKER_BIN:-docker}"REPO_SLUG="${PR_CHECK_REPO:-atcommons.eu/website}"# tg must read the CANONICAL appview, not the older default (bobbin.klbr.net), which lags badly and# reports wrong PR state (closed PRs shown open, recent PRs missing). Everything downstream depends on# accurate `tg pr list`, so pin it here (overridable). See tg v0.6.0's stale default.export TG_APPVIEW="${TG_APPVIEW:-https://api.tangled.org}"# HTTPS clone (public, no SSH key needed on the host or in the container). Tangled redirects to the knot.TANGLED_URL="${PR_CHECK_CLONE_URL:-https://tangled.org/atcommons.eu/website}"CLONE="$DIR/website"STATE="$DIR/pr-checked.log" # one line per handled round: <rkey>:<updatedAt>LOCK="$DIR/.pr-check.lock"
mkdir -p "$DIR"touch "$STATE"
# Single instance: a long check must not overlap the next cron tick. flock is Linux-only (present on# the NAS, absent on macOS), so guard it: without flock we run unlocked rather than not at all.if command -v flock >/dev/null 2>&1; then exec 9>"$LOCK" if ! flock -n 9; then echo "[pr-check] another run holds the lock; skipping"; exit 0; fielse echo "[pr-check] flock not available; running without a lock"fi
# Fresh, clean checkout of main to run tg against (tg pr checkout needs a repo checkout).if [ -d "$CLONE/.git" ]; then git -C "$CLONE" fetch -q origin main git -C "$CLONE" checkout -q -f main git -C "$CLONE" reset -q --hard origin/main git -C "$CLONE" clean -qfdxelse rm -rf "$CLONE" git clone -q "$TANGLED_URL" "$CLONE"ficd "$CLONE"
# Enumerate OPEN PRs as: rkey <tab> updatedAt <tab> sourceBranch <tab> authorHandle. `tg pr list`# has no --repo flag; it infers the repo from this checkout's origin (read-only, no auth needed).prs="$(tg pr list --json)"rows="$(printf '%s' "$prs" | node -e ' const d = JSON.parse(require("fs").readFileSync(0, "utf8")); const items = Array.isArray(d) ? d : (d.pulls || d.items || []); for (const p of items) { if ((p.state || "") !== "open") continue; const a = (p.author && p.author.handle) || ""; process.stdout.write([p.rkey, p.updatedAt, p.sourceBranch || "", a].join("\t") + "\n"); }')"
[ -n "$rows" ] || { echo "[pr-check] no open PRs"; exit 0; }
printf '%s\n' "$rows" | while IFS="$(printf '\t')" read -r rkey updated branch author; do [ -n "$rkey" ] || continue key="$rkey:$updated" if grep -qxF "$key" "$STATE"; then echo "[pr-check] PR $rkey ($branch) already checked at $updated; skip" continue fi echo "[pr-check] checking PR $rkey by @$author ($branch)"
# Clean slate, then apply the PR onto current main. Tangled PRs are patch-based: `tg pr checkout` # does `git am` (3-way). `git am --abort` is REQUIRED in the reset -- checkout/clean do not clear a # half-applied am, and a leftover one jams every later PR. A PR that will not apply onto current main # (stale, needs rebase) leaves an am session; that is itself the verdict, not a test failure. git am --abort >/dev/null 2>&1 || true git checkout -q -f main; git reset -q --hard origin/main; git clean -qfdx tg pr checkout "$rkey" -R "$REPO_SLUG" -b "pr-$rkey" -f >/dev/null 2>&1 || true if [ -d .git/rebase-apply ]; then # git am is mid-conflict: the PR does not apply cleanly onto current main. git am --abort >/dev/null 2>&1 || true git checkout -q -f main; git reset -q --hard origin/main; git clean -qfdx rc=5 head_sha="$(git rev-parse --short origin/main)" else head_sha="$(git rev-parse --short HEAD)" if [ "$head_sha" = "$(git rev-parse --short origin/main)" ]; then # No branch/commit produced (fork unreachable or nothing applied): skip, retry next run. echo "[pr-check] could not check out PR $rkey (fork unreachable?); will retry next run" continue fi base="$(git merge-base origin/main HEAD 2>/dev/null || echo origin/main)" rc=-1 fi
# Run the gates in a SANDBOX (only if the PR applied cleanly): untrusted PR code, no secrets, no # host access beyond the checkout. Exit codes: 0 pass, 1 tests, 2 copy-voice, 3 npm ci, 5 needs rebase. if [ "$rc" = "-1" ]; then set +e timeout 900 "$DOCKER" run --rm -e BASE="$base" -v "$CLONE:/app" -w /app node:24-alpine sh -c ' apk add --no-cache git >/dev/null 2>&1 || true git config --global --add safe.directory /app npm ci --silent || exit 3 npm test || exit 1 node scripts/check-copy-voice.mjs --base "$BASE" || exit 2 exit 0 ' rc=$? set -e fi
case "$rc" in 0) body="Automated checks passed for this round (\`$head_sha\`): tests and copy-voice are green. Advisory only (Tangled cannot block a merge on this yet); a maintainer still reviews." ;; 1) body="Automated checks: tests FAILED for this round (\`$head_sha\`). Please run \`npm test\` locally, fix, and push a new round. Advisory check." ;; 2) body="Automated checks: copy-voice FAILED for this round (\`$head_sha\`) - a banned character (em dash, curly quote, or horizontal bar) is in new copy. Run \`node scripts/check-copy-voice.mjs --base main\` to see where, then push a new round. Advisory check." ;; 3) body="Automated checks: \`npm ci\` failed for this round (\`$head_sha\`) - dependencies did not install. Advisory check." ;; 5) body="This PR no longer applies cleanly onto \`main\` (\`$head_sha\`) - it needs a rebase before it can be merged or checked. Please rebase onto the latest main and push a new round. Advisory check." ;; 124) body="Automated checks timed out for this round (\`$head_sha\`). Advisory check." ;; *) body="Automated checks could not complete for this round (\`$head_sha\`, exit $rc). Advisory check." ;; esac
if [ "${DRY_RUN:-0}" = "1" ]; then echo "[pr-check] DRY_RUN rc=$rc; would comment on $rkey: $body" continue fi if tg pr comment "$rkey" -R "$REPO_SLUG" -b "$body" >/dev/null 2>&1; then printf '%s\n' "$key" >> "$STATE" echo "[pr-check] posted result (rc=$rc) on PR $rkey" else echo "[pr-check] failed to post comment on PR $rkey; will retry next run" fidone
# Reset back to a clean main so the checkout is not left on a PR branch.git checkout -q -f main; git reset -q --hard origin/main; git clean -qfdxecho "[pr-check] done"