From 80aaa34528dc661b5eb4a030ee43e718554d5657 Mon Sep 17 00:00:00 2001 From: Seongmin Lee Date: Tue, 4 Aug 2026 15:41:25 +0300 Subject: [PATCH] localinfra: init-data seed some data. repos, issues, PRs, comments, etc etc Signed-off-by: Seongmin Lee --- docker-compose.yml | 21 +- localinfra/scripts/init-accounts.sh | 74 +---- localinfra/scripts/init-data.sh | 438 ++++++++++++++++++++++++++++ localinfra/scripts/lib.sh | 63 ++++ 4 files changed, 533 insertions(+), 63 deletions(-) create mode 100644 localinfra/scripts/init-data.sh create mode 100644 localinfra/scripts/lib.sh diff --git a/docker-compose.yml b/docker-compose.yml index 6f8c8544..0658a72e 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -52,14 +52,31 @@ services: KNOT_HOSTNAME: knot.tngl.boltless.dev SPINDLE_HOSTNAME: spindle.tngl.boltless.dev volumes: - - ./localinfra/scripts/init-accounts.sh:/init.sh:ro + - ./localinfra/scripts:/scripts:ro - init-state:/shared - command: sh -c "apk add --no-cache curl jq >/dev/null && sh /init.sh" + command: sh -c "apk add --no-cache curl jq >/dev/null && sh /scripts/init-accounts.sh" depends_on: pds: condition: service_healthy networks: [tngl] + init-data: + image: alpine:3.22 + restart: "no" + env_file: localinfra/pds.env + environment: + PDS_URL: http://pds:3000 + KNOT_URL: http://knot:5555 + KNOT_HOSTNAME: knot.tngl.boltless.dev + volumes: + - ./localinfra/scripts:/scripts:ro + - init-state:/shared:ro + command: sh -c "apk add --no-cache curl jq >/dev/null && sh /scripts/init-data.sh" + depends_on: + knot: + condition: service_healthy + networks: [tngl] + plc: platform: linux/amd64 image: ghcr.io/bluesky-social/did-method-plc:plc-f2ab7516bac5bc0f3f86842fa94e996bd1b3815b diff --git a/localinfra/scripts/init-accounts.sh b/localinfra/scripts/init-accounts.sh index dcf6ce1a..1b4c778d 100644 --- a/localinfra/scripts/init-accounts.sh +++ b/localinfra/scripts/init-accounts.sh @@ -5,8 +5,7 @@ # - create system label definitions under SYSTEM_DID set -eu -: "${PDS_URL:?PDS_URL must be set}" -PASSWORD="password" +. /scripts/lib.sh USERS="alice bob charlie david" OWNER_USER="${OWNER_USER:-alice}" @@ -15,19 +14,6 @@ SHARED_DIR="${SHARED_DIR:-/shared}" # --- helpers --- -# resolve_handle HANDLE → DID on stdout -resolve_handle() { - resp=$(curl -sS -w '\n%{http_code}' \ - "${PDS_URL}/xrpc/com.atproto.identity.resolveHandle?handle=$1") - body=$(printf '%s\n' "$resp" | sed '$d') - status=$(printf '%s\n' "$resp" | tail -n1) - case "$status" in - 200) printf '%s\n' "$body" | jq -er '.did' ;; - 400) : ;; # not found — expected - *) printf 'resolveHandle %s: HTTP %s: %s\n' "$1" "$status" "$body" >&2; return 1 ;; - esac -} - # ensure_account USERNAME → DID on stdout. Creates account if missing. ensure_account() { username="$1" @@ -56,34 +42,6 @@ ensure_account() { printf '%s\n' "$did" } -# login DID/Handle → access JWT on stdout -login() { - curl -fsS -H "Content-Type: application/json" \ - -d "{\"identifier\":\"$1\",\"password\":\"${PASSWORD}\"}" \ - "${PDS_URL}/xrpc/com.atproto.server.createSession" \ - | jq -er '.accessJwt' -} - -# put_record JWT DID COLLECTION RKEY RECORD_JSON -put_record() { - jwt="$1"; did="$2"; collection="$3"; rkey="$4"; record="$5" - - payload=$(jq -nc \ - --arg repo "$did" \ - --arg collection "$collection" \ - --arg rkey "$rkey" \ - --argjson record "$record" \ - '{repo:$repo, collection:$collection, rkey:$rkey, record:$record}') - - curl -fsS \ - -H "Content-Type: application/json" \ - -H "Authorization: Bearer ${jwt}" \ - -d "$payload" \ - "${PDS_URL}/xrpc/com.atproto.repo.putRecord" >/dev/null - - printf '[record] at://%s/%s/%s\n' "$did" "$collection" "$rkey" >&2 -} - # ensure accounts OWNER_DID="" SYSTEM_DID="" @@ -107,11 +65,7 @@ printf '%s' "$SYSTEM_DID" > "${SHARED_DIR}/system-did" printf '[system] %s → %s/system-did\n' "$SYSTEM_USER" "$SHARED_DIR" >&2 # label definitions (under SYSTEM_DID) -JWT=$(login "$SYSTEM_DID") - -CREATED_AT="2025-09-22T11:14:35+01:00" - -put_record "$JWT" "$SYSTEM_DID" "sh.tangled.label.definition" "wontfix" "$(cat </dev/null -put_record "$JWT" "$SYSTEM_DID" "sh.tangled.label.definition" "good-first-issue" "$(cat </dev/null -put_record "$JWT" "$SYSTEM_DID" "sh.tangled.label.definition" "duplicate" "$(cat </dev/null -put_record "$JWT" "$SYSTEM_DID" "sh.tangled.label.definition" "documentation" "$(cat </dev/null -put_record "$JWT" "$SYSTEM_DID" "sh.tangled.label.definition" "assignee" "$(cat </dev/null # shared env values for appview LABEL_GFI=at://${SYSTEM_DID}/sh.tangled.label.definition/good-first-issue @@ -184,9 +138,7 @@ printf '%s' "$LABEL_DEFAULTS" > "${SHARED_DIR}/label-defaults" printf '[env] wrote label-defaults, label-gfi\n' >&2 # service definitions (under OWNER_DID) -JWT=$(login "$OWNER_DID") - -put_record "$JWT" "$OWNER_DID" "sh.tangled.knot" $KNOT_HOSTNAME "{\"createdAt\": \"${CREATED_AT}\"}" -put_record "$JWT" "$OWNER_DID" "sh.tangled.spindle" "$SPINDLE_HOSTNAME" "{\"createdAt\": \"${CREATED_AT}\"}" +put_record "at://$OWNER_DID/sh.tangled.knot/$KNOT_HOSTNAME" "{\"createdAt\": \"${CREATED_AT}\"}" >/dev/null +put_record "at://$OWNER_DID/sh.tangled.spindle/$SPINDLE_HOSTNAME" "{\"createdAt\": \"${CREATED_AT}\"}" >/dev/null printf 'done.\n' >&2 diff --git a/localinfra/scripts/init-data.sh b/localinfra/scripts/init-data.sh new file mode 100644 index 00000000..6fdc28cd --- /dev/null +++ b/localinfra/scripts/init-data.sh @@ -0,0 +1,438 @@ +#!/bin/sh +set -eu + +: "${KNOT_URL:?KNOT_URL must be set}" +: "${KNOT_HOSTNAME:?KNOT_HOSTNAME must be set}" +: "${PDS_HOSTNAME:?PDS_HOSTNAME must be set}" + +SHARED_DIR="${SHARED_DIR:-/shared}" + +. /scripts/lib.sh + +OWNER_DID=$(cat "${SHARED_DIR}/owner-did") +OWNER_JWT=$(login "$OWNER_DID") + +BOB_DID=$(resolve_handle "bob.${PDS_HOSTNAME}") +BOB_JWT=$(login "$BOB_DID") + +CHARLIE_DID=$(resolve_handle "charlie.${PDS_HOSTNAME}") +DAVID_DID=$(resolve_handle "david.${PDS_HOSTNAME}") + +# every record this script seeds, and who authors it +REPO_CORE=at://$OWNER_DID/sh.tangled.repo/core +REPO_EMPTY=at://$BOB_DID/sh.tangled.repo/empty-repo + +ISSUE_1=at://$OWNER_DID/sh.tangled.repo.issue/3lzg6f3aia222 +ISSUE_2=at://$OWNER_DID/sh.tangled.repo.issue/3lzg6guhjy222 +ISSUE_3=at://$OWNER_DID/sh.tangled.repo.issue/3lzg6inolq222 + +STATE_1=at://$OWNER_DID/sh.tangled.repo.issue.state/3lzg6kgvni222 + +PULL_1=at://$OWNER_DID/sh.tangled.repo.pull/3lzg7tkxvq222 +PULL_2=at://$OWNER_DID/sh.tangled.repo.pull/3ms6rk5d6gk22 + +STRING_CODE=at://$OWNER_DID/sh.tangled.string/3lzg6ma4pa222 +STRING_MARKDOWN=at://$OWNER_DID/sh.tangled.string/3lzg6nzdqy222 +STRING_TEXT=at://$BOB_DID/sh.tangled.string/3lzg6psksq222 + +COMMENT_ISSUE=at://$BOB_DID/sh.tangled.feed.comment/3lzg6rlrui222 +COMMENT_ISSUE_REPLY=at://$OWNER_DID/sh.tangled.feed.comment/3lzg6teywa222 +COMMENT_STRING=at://$BOB_DID/sh.tangled.feed.comment/3lzg6v67xy222 +COMMENT_STRING_REPLY=at://$OWNER_DID/sh.tangled.feed.comment/3lzg6wxgzq222 + +REACT_ISSUE_A=at://$BOB_DID/sh.tangled.feed.reaction/3lzg6yqo3i222 +REACT_ISSUE_B=at://$OWNER_DID/sh.tangled.feed.reaction/3lzg72jv5a222 +REACT_COMMENT_A=at://$OWNER_DID/sh.tangled.feed.reaction/3lzg74d46y222 +REACT_COMMENT_B=at://$OWNER_DID/sh.tangled.feed.reaction/3lzg764daq222 + +FOLLOW_ALICE_BOB=at://$OWNER_DID/sh.tangled.graph.follow/3lzg77vkci222 +FOLLOW_ALICE_CHARLIE=at://$OWNER_DID/sh.tangled.graph.follow/3lzg7borea222 +FOLLOW_BOB_ALICE=at://$BOB_DID/sh.tangled.graph.follow/3lzg7dhyfy222 +FOLLOW_CHARLIE_ALICE=at://$CHARLIE_DID/sh.tangled.graph.follow/3lzg7fb7hq222 +FOLLOW_DAVID_ALICE=at://$DAVID_DID/sh.tangled.graph.follow/3lzg7h2gji222 +FOLLOW_DAVID_BOB=at://$DAVID_DID/sh.tangled.graph.follow/3lzg7itnla222 + +# a vouch's subject is its rkey, not a record field +VOUCH_ALICE_BOB=at://$OWNER_DID/sh.tangled.graph.vouch/$BOB_DID +VOUCH_BOB_CHARLIE=at://$BOB_DID/sh.tangled.graph.vouch/$CHARLIE_DID +VOUCH_CHARLIE_DAVID=at://$CHARLIE_DID/sh.tangled.graph.vouch/$DAVID_DID +VOUCH_DAVID_ALICE=at://$DAVID_DID/sh.tangled.graph.vouch/$OWNER_DID +VOUCH_BOB_DAVID=at://$BOB_DID/sh.tangled.graph.vouch/$DAVID_DID + +# service_token JWT LXM → service auth JWT for the knot, on stdout +service_token() { + aud="did:web:$(printf '%s' "$KNOT_HOSTNAME" | sed 's/:/%3A/g')" + + curl -fsS -H "Authorization: Bearer $1" \ + "${PDS_URL}/xrpc/com.atproto.server.getServiceAuth?aud=${aud}&lxm=$2&exp=$(( $(date +%s) + 600 ))" \ + | jq -er '.token' +} + +# --- repo fixtures --- + +REPO_DID=$(curl -fsS \ + -H "Content-Type: application/json" \ + -H "Authorization: Bearer $(service_token "$OWNER_JWT" sh.tangled.repo.create)" \ + -d '{"rkey":"core", "name":"core", "defaultBranch":"master", + "source":"https://tangled.org/boltless.me/core.archive"}' \ + "${KNOT_URL}/xrpc/sh.tangled.repo.create" | jq -er '.repoDid') + +printf '[knot] core = %s\n' "$REPO_DID" >&2 + +put_record "$REPO_CORE" "$(jq -nc \ + --arg knot "$KNOT_HOSTNAME" \ + --arg repoDid "$REPO_DID" \ + --arg source 'https://tangled.org/boltless.me/core.archive' \ + --arg createdAt "$CREATED_AT" \ + '{knot:$knot, name:"core", repoDid:$repoDid, source:$source, createdAt:$createdAt}')" >/dev/null + +# bob needs server:member on the knot before he may create a repo there +curl -fsS -X POST \ + -H "Content-Type: application/json" \ + -H "Authorization: Bearer $(service_token "$OWNER_JWT" sh.tangled.knot.addMember)" \ + -d "$(jq -nc --arg subject "$BOB_DID" '{subject:$subject}')" \ + "${KNOT_URL}/xrpc/sh.tangled.knot.addMember" + +EMPTY_REPO_DID=$(curl -fsS \ + -H "Content-Type: application/json" \ + -H "Authorization: Bearer $(service_token "$BOB_JWT" sh.tangled.repo.create)" \ + -d '{"rkey":"empty-repo", "name":"empty-repo", "defaultBranch":"master"}' \ + "${KNOT_URL}/xrpc/sh.tangled.repo.create" | jq -er '.repoDid') + +printf '[knot] empty-repo = %s\n' "$EMPTY_REPO_DID" >&2 + +put_record "$REPO_EMPTY" "$(jq -nc \ + --arg knot "$KNOT_HOSTNAME" \ + --arg repoDid "$EMPTY_REPO_DID" \ + --arg createdAt "$CREATED_AT" \ + '{knot:$knot, name:"empty-repo", repoDid:$repoDid, createdAt:$createdAt}')" >/dev/null + +# --- issue fixtures --- + +put_record "$ISSUE_1" "$(jq -nc \ + --arg repo "$REPO_DID" \ + --arg title 'Repo tree does not render symlinks' \ + --arg body "$(cat <<'MARKDOWN' +Symlinked entries in the file tree show up with a blank size and a link that +404s. Expected them to render with the target path, the way a plain file does. + +Reproduced on the default branch with `contrib/` present. +MARKDOWN +)" \ + --arg createdAt '2025-09-22T10:14:35Z' \ + '{repo:$repo, title:$title, body:$body, createdAt:$createdAt}')" >/dev/null + +ISSUE_2_CID=$(put_record "$ISSUE_2" "$(jq -nc \ + --arg repo "$REPO_DID" \ + --arg title 'Document the knot bootstrap flow' \ + --arg body "$(cat <<'MARKDOWN' +## What's missing + +> blah blah blah + +``` +I won't let claude to yap some nonsense here. +``` + +--- + +something _something_ +MARKDOWN +)" \ + --arg createdAt '2025-09-22T10:15:35Z' \ + '{repo:$repo, title:$title, body:$body, createdAt:$createdAt}')") + +put_record "$ISSUE_3" "$(jq -nc \ + --arg repo "$REPO_DID" \ + --arg title 'Clone over ssh fails on first push' \ + --arg body "$(cat <<'MARKDOWN' +Fresh clone, first push to a branch nobody has pushed before, and the remote +hangs up: + +``` +$ git push origin sl/my-branch +Enumerating objects: 12, done. +remote: error: cannot lock ref 'refs/heads/sl/my-branch' +To git@knot.tngl.boltless.dev:alice.pds.tngl.boltless.dev/core + ! [remote rejected] sl/my-branch -> sl/my-branch (failed to update ref) +``` + +Second attempt succeeds, so it looks like a race in the hook rather than a +permissions problem. +MARKDOWN +)" \ + --arg createdAt '2025-09-22T10:16:35Z' \ + '{repo:$repo, title:$title, body:$body, createdAt:$createdAt}')" >/dev/null + +put_record "$STATE_1" "$(jq -nc \ + --arg issue "$ISSUE_1" \ + --arg state "sh.tangled.repo.issue.state.closed" \ + --arg createdAt '2025-09-22T10:17:35Z' \ + '{issue:$issue, state:$state, createdAt:$createdAt}')" >/dev/null + +# --- pull request fixtures --- + +PULL_BASE=01117fddd3502c57bc20a65a75a66a4900d5d67d +PULL_HEAD_1=f3c356fc8da46818a8ab0c24f22ee61854fe4b5f +PULL_HEAD_2=9a925efef6a0e6dfcd2d4317b4a1eee8752928b8 + +patch_blob() { + curl -fsS "${KNOT_URL}/xrpc/sh.tangled.repo.compare?repo=${REPO_DID}&rev1=${PULL_BASE}&rev2=$1" \ + | jq -j '.patch' \ + | gzip \ + | curl -fsS --data-binary @- \ + -H "Content-Type: application/gzip" \ + -H "Authorization: Bearer ${OWNER_JWT}" \ + "${PDS_URL}/xrpc/com.atproto.repo.uploadBlob" \ + | jq -ec '.blob' +} + +put_record "$PULL_1" "$(jq -nc \ + --arg repo "$REPO_DID" \ + --arg title 'legacy PR' \ + --arg body 'two rounds, each a gzipped format-patch blob' \ + --argjson round1 "$(patch_blob "$PULL_HEAD_1")" \ + --argjson round2 "$(patch_blob "$PULL_HEAD_2")" \ + --arg createdAt1 '2025-09-22T10:40:35Z' \ + --arg createdAt2 '2025-09-22T10:41:35Z' \ + '{title:$title, body:$body, createdAt:$createdAt1, + source:{repo:$repo, branch:"sl/ref-based-pr"}, + target:{repo:$repo, branch:"master"}, + rounds:[{patchBlob:$round1, createdAt:$createdAt1}, + {patchBlob:$round2, createdAt:$createdAt2}]}')" >/dev/null + +keep_commit() { + did=${3#at://} + did=${did%%/*} + + curl -fsS \ + -H "Content-Type: application/json" \ + -H "Authorization: Bearer $(service_token "$(login "$did")" sh.tangled.git.keepCommit)" \ + -d "$(jq -nc --arg repo "$1" --arg oid "$2" --arg record "$3" \ + '{repo:$repo, record:$record, + source:{"$type":"sh.tangled.git.keepCommit#commit", repo:$repo, oid:$oid}}')" \ + "${KNOT_URL}/xrpc/sh.tangled.git.keepCommit" >/dev/null \ + && printf '[keep] %s %s\n' "$2" "$3" >&2 \ + || printf '[keep] %s failed, knot has no keepCommit\n' "$2" >&2 +} + +keep_commit "$REPO_DID" "$PULL_HEAD_1" "$PULL_2" +keep_commit "$REPO_DID" "$PULL_HEAD_2" "$PULL_2" + +put_record "$PULL_2" "$(jq -nc \ + --arg repo "$REPO_DID" \ + --arg title 'ref based PR' \ + --arg body 'something something markdown' \ + --arg head1 "$PULL_HEAD_1" \ + --arg head2 "$PULL_HEAD_2" \ + --arg base "$PULL_BASE" \ + --arg createdAt1 '2025-09-22T10:42:35Z' \ + --arg createdAt2 '2025-09-22T10:43:35Z' \ + '{title:$title, body:$body, createdAt:$createdAt1, + source:{repo:$repo, branch:"sl/ref-based-pr"}, + target:{repo:$repo, branch:"master"}, + versions:[{head:$head1, base:$base, createdAt:$createdAt1}, + {head:$head2, base:$base, createdAt:$createdAt2}]}')" >/dev/null + +# --- string fixtures --- + +STRING_CODE_CID=$(put_record "$STRING_CODE" "$(jq -nc \ + --arg filename 'sieve.go' \ + --arg description 'primes below n, no allocations past the sieve' \ + --arg contents "$(cat <<'CODE' +package main + +import "fmt" + +func sieve(n int) []int { + composite := make([]bool, n+1) + var primes []int + for i := 2; i <= n; i++ { + if composite[i] { + continue + } + primes = append(primes, i) + for j := i * i; j <= n; j += i { + composite[j] = true + } + } + return primes +} + +func main() { + fmt.Println(sieve(50)) +} +CODE +)" \ + --arg createdAt '2025-09-22T10:18:35Z' \ + '{filename:$filename, description:$description, contents:$contents, createdAt:$createdAt}')") + +put_record "$STRING_MARKDOWN" "$(jq -nc \ + --arg filename 'notes.md' \ + --arg description 'scratch notes from bringing up the local stack' \ + --arg contents "$(cat <<'MARKDOWN' +# Local stack notes + +Things that bit me, in order: + +1. the dev CA has to be in the **system** trust store, not just the browser +2. `TANGLED_APPVIEW_HOST` must be a loopback IP, not `localhost` +3. the knot wants `/shared/owner-did` before it will start + +## Handy + +```sh +docker compose logs -f appview | grep ingest +``` + +Still unclear: how [spindle](https://tangled.org/tangled.org/core) picks up +`.tangled/workflows`. +MARKDOWN +)" \ + --arg createdAt '2025-09-22T10:19:35Z' \ + '{filename:$filename, description:$description, contents:$contents, createdAt:$createdAt}')" >/dev/null + +put_record "$STRING_TEXT" "$(jq -nc \ + --arg filename 'todo.txt' \ + --arg description '' \ + --arg contents "$(cat <<'TEXT' +- [ ] figure out why the first ssh push races +- [x] get the local knot talking to the mirror +- [ ] write up the bootstrap order somewhere findable +- [ ] ask about symlinks in the tree view +TEXT +)" \ + --arg createdAt '2025-09-22T10:20:35Z' \ + '{filename:$filename, description:$description, contents:$contents, createdAt:$createdAt}')" >/dev/null + +# --- comment fixtures --- + +COMMENT_ISSUE_CID=$(put_record "$COMMENT_ISSUE" "$(jq -nc \ + --arg subjectUri "$ISSUE_2" \ + --arg subjectCid "$ISSUE_2_CID" \ + --arg text "$(cat <<'MARKDOWN' +Worth calling out the ordering explicitly — I lost an afternoon to the knot +exiting because `/shared/owner-did` wasn't there yet. +MARKDOWN +)" \ + --arg createdAt '2025-09-22T10:21:35Z' \ + '{subject:{uri:$subjectUri, cid:$subjectCid}, createdAt:$createdAt, + body:{"$type":"sh.tangled.markup.markdown", text:$text, original:$text}}')") + +put_record "$COMMENT_ISSUE_REPLY" "$(jq -nc \ + --arg subjectUri "$ISSUE_2" \ + --arg subjectCid "$ISSUE_2_CID" \ + --arg replyUri "$COMMENT_ISSUE" \ + --arg replyCid "$COMMENT_ISSUE_CID" \ + --arg text 'Agreed. The compose `depends_on` encodes it already, just nowhere a human would look.' \ + --arg createdAt '2025-09-22T10:22:35Z' \ + '{subject:{uri:$subjectUri, cid:$subjectCid}, + replyTo:{uri:$replyUri, cid:$replyCid}, createdAt:$createdAt, + body:{"$type":"sh.tangled.markup.markdown", text:$text, original:$text}}')" >/dev/null + +COMMENT_STRING_CID=$(put_record "$COMMENT_STRING" "$(jq -nc \ + --arg subjectUri "$STRING_CODE" \ + --arg subjectCid "$STRING_CODE_CID" \ + --arg text 'Starting the inner loop at `i*i` is the bit everyone forgets.' \ + --arg createdAt '2025-09-22T10:23:35Z' \ + '{subject:{uri:$subjectUri, cid:$subjectCid}, createdAt:$createdAt, + body:{"$type":"sh.tangled.markup.markdown", text:$text, original:$text}}')") + +put_record "$COMMENT_STRING_REPLY" "$(jq -nc \ + --arg subjectUri "$STRING_CODE" \ + --arg subjectCid "$STRING_CODE_CID" \ + --arg replyUri "$COMMENT_STRING" \ + --arg replyCid "$COMMENT_STRING_CID" \ + --arg text 'Only correct because the outer loop skips composites — otherwise it would miss factors.' \ + --arg createdAt '2025-09-22T10:24:35Z' \ + '{subject:{uri:$subjectUri, cid:$subjectCid}, + replyTo:{uri:$replyUri, cid:$replyCid}, createdAt:$createdAt, + body:{"$type":"sh.tangled.markup.markdown", text:$text, original:$text}}')" >/dev/null + +# --- reaction fixtures --- + +put_record "$REACT_ISSUE_A" "$(jq -nc \ + --arg subject "$ISSUE_2" \ + --arg reaction '🎉' \ + --arg createdAt '2025-09-22T10:25:35Z' \ + '{subject:$subject, reaction:$reaction, createdAt:$createdAt}')" >/dev/null + +put_record "$REACT_ISSUE_B" "$(jq -nc \ + --arg subject "$ISSUE_2" \ + --arg reaction '👀' \ + --arg createdAt '2025-09-22T10:26:35Z' \ + '{subject:$subject, reaction:$reaction, createdAt:$createdAt}')" >/dev/null + +put_record "$REACT_COMMENT_A" "$(jq -nc \ + --arg subject "$COMMENT_ISSUE" \ + --arg reaction '👍' \ + --arg createdAt '2025-09-22T10:27:35Z' \ + '{subject:$subject, reaction:$reaction, createdAt:$createdAt}')" >/dev/null + +put_record "$REACT_COMMENT_B" "$(jq -nc \ + --arg subject "$COMMENT_STRING" \ + --arg reaction '🚀' \ + --arg createdAt '2025-09-22T10:28:35Z' \ + '{subject:$subject, reaction:$reaction, createdAt:$createdAt}')" >/dev/null + +# --- follow fixtures --- + +put_record "$FOLLOW_ALICE_BOB" "$(jq -nc \ + --arg subject "$BOB_DID" \ + --arg createdAt '2025-09-22T10:29:35Z' \ + '{subject:$subject, createdAt:$createdAt}')" >/dev/null + +put_record "$FOLLOW_ALICE_CHARLIE" "$(jq -nc \ + --arg subject "$CHARLIE_DID" \ + --arg createdAt '2025-09-22T10:30:35Z' \ + '{subject:$subject, createdAt:$createdAt}')" >/dev/null + +put_record "$FOLLOW_BOB_ALICE" "$(jq -nc \ + --arg subject "$OWNER_DID" \ + --arg createdAt '2025-09-22T10:31:35Z' \ + '{subject:$subject, createdAt:$createdAt}')" >/dev/null + +put_record "$FOLLOW_CHARLIE_ALICE" "$(jq -nc \ + --arg subject "$OWNER_DID" \ + --arg createdAt '2025-09-22T10:32:35Z' \ + '{subject:$subject, createdAt:$createdAt}')" >/dev/null + +put_record "$FOLLOW_DAVID_ALICE" "$(jq -nc \ + --arg subject "$OWNER_DID" \ + --arg createdAt '2025-09-22T10:33:35Z' \ + '{subject:$subject, createdAt:$createdAt}')" >/dev/null + +put_record "$FOLLOW_DAVID_BOB" "$(jq -nc \ + --arg subject "$BOB_DID" \ + --arg createdAt '2025-09-22T10:34:35Z' \ + '{subject:$subject, createdAt:$createdAt}')" >/dev/null + +# --- vouch fixtures (subject is the rkey, not a record field) --- + +put_record "$VOUCH_ALICE_BOB" "$(jq -nc \ + --arg reason 'Reviewed his bootstrap-ordering work on core, knows the stack.' \ + --arg evidence "$COMMENT_ISSUE" \ + --arg createdAt '2025-09-22T10:35:35Z' \ + '{kind:"vouch", reason:$reason, evidences:[$evidence], createdAt:$createdAt}')" >/dev/null + +put_record "$VOUCH_BOB_CHARLIE" "$(jq -nc \ + --arg reason 'Worked together on the knot ACL rewrite.' \ + --arg createdAt '2025-09-22T10:36:35Z' \ + '{kind:"vouch", reason:$reason, createdAt:$createdAt}')" >/dev/null + +put_record "$VOUCH_CHARLIE_DAVID" "$(jq -nc \ + --arg reason 'Long-time collaborator, reviews carefully.' \ + --arg createdAt '2025-09-22T10:37:35Z' \ + '{kind:"vouch", reason:$reason, createdAt:$createdAt}')" >/dev/null + +put_record "$VOUCH_DAVID_ALICE" "$(jq -nc \ + --arg reason 'Runs the knot, has never lost a repo.' \ + --arg createdAt '2025-09-22T10:38:35Z' \ + '{kind:"vouch", reason:$reason, createdAt:$createdAt}')" >/dev/null + +put_record "$VOUCH_BOB_DAVID" "$(jq -nc \ + --arg reason 'Force-pushed over master twice in one week.' \ + --arg createdAt '2025-09-22T10:39:35Z' \ + '{kind:"denounce", reason:$reason, createdAt:$createdAt}')" >/dev/null + +printf 'done.\n' >&2 diff --git a/localinfra/scripts/lib.sh b/localinfra/scripts/lib.sh new file mode 100644 index 00000000..72536ed7 --- /dev/null +++ b/localinfra/scripts/lib.sh @@ -0,0 +1,63 @@ +#!/bin/sh +# shared helpers for the dev bootstrap scripts. source, don't run. + +: "${PDS_URL:?PDS_URL must be set}" +PASSWORD="${PASSWORD:-password}" +CREATED_AT="${CREATED_AT:-2025-09-22T11:14:35+01:00}" + +# resolve_handle HANDLE → DID on stdout +resolve_handle() { + resp=$(curl -sS -w '\n%{http_code}' \ + "${PDS_URL}/xrpc/com.atproto.identity.resolveHandle?handle=$1") + body=$(printf '%s\n' "$resp" | sed '$d') + status=$(printf '%s\n' "$resp" | tail -n1) + case "$status" in + 200) printf '%s\n' "$body" | jq -er '.did' ;; + 400) : ;; # not found — expected + *) printf 'resolveHandle %s: HTTP %s: %s\n' "$1" "$status" "$body" >&2; return 1 ;; + esac +} + +# login DID/Handle → access JWT on stdout +login() { + curl -fsS -H "Content-Type: application/json" \ + -d "{\"identifier\":\"$1\",\"password\":\"${PASSWORD}\"}" \ + "${PDS_URL}/xrpc/com.atproto.server.createSession" \ + | jq -er '.accessJwt' +} + +# put_record at://DID/COLLECTION/RKEY RECORD_JSON +# → the record's cid on stdout, for use in a strongRef (comment subject, replyTo). +# callers that don't need it can ignore stdout. +put_record() { + case "$1" in + at://did:*/*/*) ;; + *) printf 'put_record: expected at://DID/COLLECTION/RKEY, got %s\n' "$1" >&2; return 1 ;; + esac + + rest=${1#at://} + did=${rest%%/*} + rest=${rest#*/} + collection=${rest%%/*} + rkey=${rest#*/} + record="$2" + + # one login per record — the dev PDS runs with rate limits disabled + jwt=$(login "$did") + + payload=$(jq -nc \ + --arg repo "$did" \ + --arg collection "$collection" \ + --arg rkey "$rkey" \ + --argjson record "$record" \ + '{repo:$repo, collection:$collection, rkey:$rkey, record:$record}') + + resp=$(curl -fsS \ + -H "Content-Type: application/json" \ + -H "Authorization: Bearer ${jwt}" \ + -d "$payload" \ + "${PDS_URL}/xrpc/com.atproto.repo.putRecord") + + printf '[record] %s\n' "$1" >&2 + printf '%s\n' "$resp" | jq -er '.cid' # -e: fail loudly if the cid is absent +} -- 2.51.2