~ knotless Search/
↑↓ move↵ open⇧↵ all results

chadtmiller.com/knotless

git that lives in your Atmosphere account

knotless.dev
1 collaborator

knotless 0.13.0 Latest 2 packages, 1 image

1
10

The workflows use shared steps in place of their pasted blocks

Merged chadtmiller.com asked to merge into main from chadtmiller.com/knotless:ci/shared-steps
.knotless/workflows/check.yml+2−4
@@ -16,9 +16,7 @@ on:
pull: {base: [refs/heads/main, refs/heads/feature/*]}
cache: [target, /usr/local/cargo/registry, /usr/local/rustup]
steps:
- name: fmt
run: rustup component add rustfmt && cargo fmt --all --check
- name: clippy
run: rustup component add clippy && cargo clippy --workspace --all-targets --locked -- -D warnings
# cargo fmt --check, then clippy over the workspace, warnings denied.
- uses: chadtmiller.com/steps/rust-lint@1
.knotless/workflows/check.ymlline 20

This uses step makes check.yml fail to load: unknown field 'uses'. The runner has no support for it yet, and the same error hits every workflow in this pull.

- name: changes
run: harness/changes.sh
.knotless/workflows/docs.yml+7−9
@@ -13,17 +13,15 @@ on:
push: {refs: [refs/heads/main]}
pull:
base: [refs/heads/main, refs/heads/feature/*]
paths: [docs/**, README.md, AGENTS.md, deploy/README.md, harness/docs.mjs, docs-site/**, lexicons/**, web/src/tokens.css, web/package-lock.json, .knotless/workflows/docs.yml]
paths: [docs/**, README.md, AGENTS.md, deploy/README.md, harness/docs.mjs, docs-site/**, lexicons/**, web/src/tokens.css, web/package-lock.json, .knotless/workflows/docs.yml, .knotless/uses.lock]
cache: [web/node_modules, docs-site/node_modules]
steps:
- name: packages
run: |
for dir in web docs-site; do
lock=$(sha256sum $dir/package-lock.json)
[ "$(cat $dir/node_modules/.lock 2>/dev/null)" = "$lock" ] && continue
(cd $dir && npm ci --no-audit --no-fund)
echo "$lock" > $dir/node_modules/.lock
done
- uses: chadtmiller.com/steps/npm-ci@1
name: web
with: {dir: web}
- uses: chadtmiller.com/steps/npm-ci@1
name: docs-site
with: {dir: docs-site}
- name: docs
run: node harness/docs.mjs
- name: site
.knotless/workflows/e2e.yml+9−14
@@ -21,7 +21,7 @@ on:
push: {refs: [refs/heads/main]}
pull:
base: [refs/heads/main, refs/heads/feature/*]
paths: [web/**, lexicons/**, crates/knotless/**, crates/knotless-serve/**, crates/knotless-mcp/**, Cargo.toml, Cargo.lock, harness/e2e/**, harness/shots/world.mjs, harness/shots/fixture/**, harness/pds.mjs, harness/tls-proxy.mjs, harness/package.json, harness/package-lock.json, .knotless/workflows/e2e.yml]
paths: [web/**, lexicons/**, crates/knotless/**, crates/knotless-serve/**, crates/knotless-mcp/**, Cargo.toml, Cargo.lock, harness/e2e/**, harness/shots/world.mjs, harness/shots/fixture/**, harness/pds.mjs, harness/tls-proxy.mjs, harness/package.json, harness/package-lock.json, .knotless/workflows/e2e.yml, .knotless/uses.lock]
cache: [target, /usr/local/cargo/registry, /usr/local/rustup, /opt/node, /opt/browsers, /opt/chromium-debs, /opt/atproto, web/node_modules, harness/node_modules]
env:
PLAYWRIGHT_BROWSERS_PATH: /opt/browsers
@@ -30,19 +30,14 @@ env:
ATPROTO_COMMIT: 11c5d98cd50261c5e1201fedc5c6c05a7b8d1ccf
ALPHA_BUILD: "2"
steps:
- name: node
run: |
[ -x /opt/node/bin/node ] || curl -fsSL https://nodejs.org/dist/v24.22.1/node-v24.22.1-linux-x64.tar.xz | tar -xJ -C /opt/node --strip-components 1
/opt/node/bin/node --version
- name: packages
run: |
export PATH=/opt/node/bin:$PATH
for dir in web harness; do
lock=$(sha256sum $dir/package-lock.json)
[ "$(cat $dir/node_modules/.lock 2>/dev/null)" = "$lock" ] && continue
(cd $dir && npm ci --no-audit --no-fund)
echo "$lock" > $dir/node_modules/.lock
done
- uses: chadtmiller.com/steps/node@1
with: {version: 24.22.1}
- uses: chadtmiller.com/steps/npm-ci@1
name: web
with: {dir: web}
- uses: chadtmiller.com/steps/npm-ci@1
name: harness
with: {dir: harness}
- name: chromium
run: |
export PATH=/opt/node/bin:$PATH
.knotless/workflows/publish-lexicons.yml+28−34
@@ -1,11 +1,12 @@
# Publishes the lexicons as atproto resolves them, once main's checks
# pass at a commit that changes them (deploy/publish-lexicons.mjs): the
# `_lexicon.<authority>` TXT records in the knotless.dev zone, naming
# lexicons.knotless.dev, and a com.atproto.lexicon.schema record there
# for each lexicon. What is new or changed is written, and what main no longer has
# is deleted, schemas and the records of authorities none uses now. Each
# run makes the whole set match main, so a run cut off by a newer one
# leaves nothing the next does not finish.
# pass at a commit that changes them, with the shared publish-lexicons
# step (chadtmiller.com/steps): the `_lexicon.<authority>` TXT records in
# the knotless.dev zone, naming lexicons.knotless.dev, and a
# com.atproto.lexicon.schema record there for each lexicon. What is new or
# changed is written, and what main no longer has is deleted, schemas and
# the records of authorities none uses now. Each run makes the whole set
# match main, so a run cut off by a newer one leaves nothing the next does
# not finish.
#
# lexicons.knotless.dev is an account of its own, on a pds.js worker
# (knotless-lexicons-pds), so that what CI holds writes lexicons and
@@ -14,39 +15,32 @@
# resolvers see it, and changes only with CLOUDFLARE_API_TOKEN, a token
# with Zone:DNS:Edit on knotless.dev that the host's address may use: it
# is needed only when an authority is added or dropped, and is not
# granted now, so such a run fails, saying which record to make.
# Asked for on any other branch,
# `knotless rerun <repo> --ref refs/heads/<branch> --workflow
# publish-lexicons`, it says what it would change and changes nothing.
# granted now, so such a run writes the schemas, then fails, saying which
# record to make.
#
# Asked for on any other branch, `knotless rerun <repo> --ref
# refs/heads/<branch> --workflow publish-lexicons`, it plans and skips the
# wait for checks. It is given no secrets there, so its write passes only
# when there is nothing to change, and otherwise fails having written
# nothing.
on:
push: {refs: [refs/heads/main], paths: [lexicons/**, deploy/publish-lexicons.mjs]}
push: {refs: [refs/heads/main], paths: [lexicons/**, .knotless/workflows/publish-lexicons.yml]}
asked: {refs: [refs/heads/*]}
image: node:24.22.1
secrets: [LEX_PASSWORD, CLOUDFLARE_API_TOKEN]
env:
REPO: knotless://chadtmiller.com/knotless
KNOTLESS_APPVIEW: https://knotless.dev
LEX_DID: did:plc:yqsg7znbbhfmj3a2nrpnaerk
LEX_ZONE: knotless.dev
# Cloudflare refuses a token limited to an address over IPv6.
NODE_OPTIONS: --dns-result-order=ipv4first
steps:
- name: plan
run: node deploy/publish-lexicons.mjs
- name: checks pass
timeout: 30
run: |
[ "$KNOTLESS_RUN_REF" = refs/heads/main ] || exit 0
curl -fsSL https://knotless.dev/install.sh | sh
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=$(~/.local/bin/knotless checks "$REPO" --ref "$KNOTLESS_RUN_REF" --commit "$KNOTLESS_RUN_SHA" --workflow "$wf" --wait "$left")
echo "$out"
case "$out" in success*) ;; *) echo "$wf has not passed: not publishing"; exit 1 ;; esac
done
- name: publish
run: |
[ "$KNOTLESS_RUN_REF" = refs/heads/main ] || exit 0
node deploy/publish-lexicons.mjs --write
# What a write would change, with no secrets.
- uses: chadtmiller.com/steps/publish-lexicons@1
name: plan
with: {did: "did:plc:yqsg7znbbhfmj3a2nrpnaerk", zone: knotless.dev}
- uses: chadtmiller.com/steps/wait-for-checks@1
name: checks pass
with: {workflows: check test web lexicons shots docs e2e, only-on: refs/heads/main}
- uses: chadtmiller.com/steps/publish-lexicons@1
name: publish
with: {did: "did:plc:yqsg7znbbhfmj3a2nrpnaerk", zone: knotless.dev, write: "true"}
secrets: [LEX_PASSWORD, CLOUDFLARE_API_TOKEN]
.knotless/workflows/release.yml+6−12
@@ -36,18 +36,12 @@ steps:
elif [ "$KNOTLESS_RUN_REF" != refs/heads/main ]; then
echo "on $KNOTLESS_RUN_REF: building $version, publishing nothing"
fi
- name: checks pass
timeout: 30
run: |
[ ! -e ~/.done ] && [ "$KNOTLESS_RUN_REF" = refs/heads/main ] || exit 0
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 "$KNOTLESS_RUN_REF" --commit "$KNOTLESS_RUN_SHA" --workflow "$wf" --wait "$left")
echo "$out"
case "$out" in success*) ;; *) echo "$wf has not passed: not publishing"; exit 1 ;; esac
done
# All seven, over 25 minutes, with the knotless just built. On main
# only, and even when the version is published already, as the step
# cannot see ~/.done: it then waits for checks it does not need.
- uses: chadtmiller.com/steps/wait-for-checks@1
name: checks pass
with: {workflows: check test web lexicons shots docs e2e, only-on: refs/heads/main}
- name: tools
run: |
[ ! -e ~/.done ] || exit 0
.knotless/workflows/scenarios.yml+5−11
@@ -28,22 +28,16 @@ env:
ATPROTO_COMMIT: 11c5d98cd50261c5e1201fedc5c6c05a7b8d1ccf
ALPHA_BUILD: "2"
steps:
- name: node
run: |
[ -x /opt/node/bin/node ] || curl -fsSL https://nodejs.org/dist/v24.22.1/node-v24.22.1-linux-x64.tar.xz | tar -xJ -C /opt/node --strip-components 1
/opt/node/bin/node --version
- uses: chadtmiller.com/steps/node@1
with: {version: 24.22.1}
- name: claude
run: |
export PATH=/opt/node/bin:$PATH
[ -x /opt/node/bin/claude ] || npm install -g --no-audit --no-fund @anthropic-ai/claude-code
claude --version
- name: harness
run: |
cd harness
lock=$(sha256sum package-lock.json)
[ "$(cat node_modules/.lock 2>/dev/null)" = "$lock" ] && exit 0
PATH=/opt/node/bin:$PATH npm ci --omit=dev --no-audit --no-fund
echo "$lock" > node_modules/.lock
- uses: chadtmiller.com/steps/npm-ci@1
name: harness
with: {dir: harness, omit: dev}
- name: spaces alpha
run: |
case " $TRACKS " in *" private "*) ;; *) exit 0 ;; esac
.knotless/workflows/test.yml+9−18
@@ -27,25 +27,16 @@ env:
ATPROTO_COMMIT: 11c5d98cd50261c5e1201fedc5c6c05a7b8d1ccf
# Bumped when the steps that build the alpha change, to build it again.
ALPHA_BUILD: "2"
LLVM_COV_VERSION: 0.9.1
steps:
- name: node
run: |
[ -x /opt/node/bin/node ] || curl -fsSL https://nodejs.org/dist/v24.22.1/node-v24.22.1-linux-x64.tar.xz | tar -xJ -C /opt/node --strip-components 1
/opt/node/bin/node --version
- name: harness
run: |
cd harness
lock=$(sha256sum package-lock.json)
[ "$(cat node_modules/.lock 2>/dev/null)" = "$lock" ] && exit 0
PATH=/opt/node/bin:$PATH npm ci --omit=dev --no-audit --no-fund
echo "$lock" > node_modules/.lock
- name: tools
run: |
rustup component add llvm-tools-preview
[ "$(cat /opt/tools/.llvm-cov 2>/dev/null)" = "$LLVM_COV_VERSION" ] && exit 0
curl -fsSL "https://github.com/taiki-e/cargo-llvm-cov/releases/download/v$LLVM_COV_VERSION/cargo-llvm-cov-x86_64-unknown-linux-gnu.tar.gz" | tar -xz -C /opt/tools
echo "$LLVM_COV_VERSION" > /opt/tools/.llvm-cov
# Node into /opt/node, the harness's packages when their lockfile
# changes, and cargo-llvm-cov into /opt/tools, each kept in the cache.
- uses: chadtmiller.com/steps/node@1
with: {version: 24.22.1}
- uses: chadtmiller.com/steps/npm-ci@1
name: harness
with: {dir: harness, omit: dev}
- uses: chadtmiller.com/steps/cargo-llvm-cov@1
with: {version: 0.9.1}
- name: spaces alpha
run: |
[ "$(cat /opt/atproto/.built 2>/dev/null)" = "$ATPROTO_COMMIT $ALPHA_BUILD" ] && exit 0
.knotless/workflows/web.yml+4−8
@@ -12,16 +12,12 @@ on:
push: {refs: [refs/heads/main]}
pull:
base: [refs/heads/main, refs/heads/feature/*]
paths: [web/**, lexicons/**, harness/layers.mjs, harness/contrast.mjs, .knotless/workflows/web.yml]
paths: [web/**, lexicons/**, harness/layers.mjs, harness/contrast.mjs, .knotless/workflows/web.yml, .knotless/uses.lock]
cache: [web/node_modules]
steps:
- name: packages
run: |
cd web
lock=$(sha256sum package-lock.json)
[ "$(cat node_modules/.lock 2>/dev/null)" = "$lock" ] && exit 0
npm ci --no-audit --no-fund
echo "$lock" > node_modules/.lock
- uses: chadtmiller.com/steps/npm-ci@1
name: packages
with: {dir: web}
- name: layers
run: node harness/layers.mjs
- name: contrast
.knotless/uses.locknew+25−0
@@ -0,0 +1,25 @@
# Written by `knotless uses update`. Commit it: runs read it.
chadtmiller.com/steps/cargo-llvm-cov@1:
version: 1.0.0
commit: 686cdbb38978730bb1ba5d1c999967e48497ce15
release: at://did:plc:bcgltzqazw5tb6k2g3ttenbj/dev.knotless.release/3mza7iq7tundv.tbnmqntp7ufhx7yd6i3rdqewys.sjjb7q6l3fsl3spvqsuzdoe73w
chadtmiller.com/steps/node@1:
version: 1.0.0
commit: 686cdbb38978730bb1ba5d1c999967e48497ce15
release: at://did:plc:bcgltzqazw5tb6k2g3ttenbj/dev.knotless.release/3mza7iq7tundv.qespbvwvof6xwjdaedpzq3koia.sjjb7q6l3fsl3spvqsuzdoe73w
chadtmiller.com/steps/npm-ci@1:
version: 1.0.0
commit: 686cdbb38978730bb1ba5d1c999967e48497ce15
release: at://did:plc:bcgltzqazw5tb6k2g3ttenbj/dev.knotless.release/3mza7iq7tundv.dzyz2ceqihhxzgk2f2637ygpfw.sjjb7q6l3fsl3spvqsuzdoe73w
chadtmiller.com/steps/publish-lexicons@1:
version: 1.0.0
commit: cc436177fab7461a33e84ec8876a2d66656a5c09
release: at://did:plc:bcgltzqazw5tb6k2g3ttenbj/dev.knotless.release/3mza7iq7tundv.ez5tgppxbpaxxrztjj3cnlyyhc.sjjb7q6l3fsl3spvqsuzdoe73w
chadtmiller.com/steps/rust-lint@1:
version: 1.0.0
commit: 686cdbb38978730bb1ba5d1c999967e48497ce15
release: at://did:plc:bcgltzqazw5tb6k2g3ttenbj/dev.knotless.release/3mza7iq7tundv.zvxvlxa6fe7s4pnwsnmgmm63aj.sjjb7q6l3fsl3spvqsuzdoe73w
chadtmiller.com/steps/wait-for-checks@1:
version: 1.0.0
commit: 686cdbb38978730bb1ba5d1c999967e48497ce15
release: at://did:plc:bcgltzqazw5tb6k2g3ttenbj/dev.knotless.release/3mza7iq7tundv.nglt7rswdf5dzx2tmob2gquhpu.sjjb7q6l3fsl3spvqsuzdoe73w
deploy/publish-lexicons.mjsdeleted+0−180
@@ -1,180 +0,0 @@
// Publishes the lexicons under lexicons/ as atproto resolves them: a TXT
// record `_lexicon.<authority>` naming the account, for each authority
// (`_lexicon.knotless.dev` for `dev.knotless.*`), and in that account a
// com.atproto.lexicon.schema record for each lexicon, keyed by its NSID.
// It adds and updates what is new or changed, and deletes what the
// repository no longer has: a schema of its namespaces, and a `_lexicon`
// record naming the account for an authority none of them uses now. The
// rest of the account and the zone it leaves alone.
//
// node deploy/publish-lexicons.mjs # says what it would do
// node deploy/publish-lexicons.mjs --write
//
// LEX_DID is the account; LEX_PASSWORD, an app password of its, writes the
// schemas, and CLOUDFLARE_API_TOKEN, with Zone:DNS:Edit on LEX_ZONE, the
// DNS. Without the token it reads the DNS as resolvers see it, and fails
// to write while a record is missing or names another account.
import { readdir, readFile } from "node:fs/promises";
import { join } from "node:path";
const COLLECTION = "com.atproto.lexicon.schema";
const ROOT = new URL("../lexicons", import.meta.url).pathname;
const write = process.argv.includes("--write");
const { LEX_DID: did, LEX_ZONE: zone = "knotless.dev", LEX_PASSWORD, CLOUDFLARE_API_TOKEN: cfToken } = process.env;
if (!did) throw new Error("LEX_DID names the account to publish into");
async function* files(dir) {
for (const e of await readdir(dir, { withFileTypes: true })) {
const p = join(dir, e.name);
if (e.isDirectory()) yield* files(p);
else if (e.name.endsWith(".json")) yield p;
}
}
const shipped = new Map();
for await (const p of files(ROOT)) {
const doc = JSON.parse(await readFile(p, "utf8"));
shipped.set(doc.id, doc);
}
async function json(url, init = {}) {
const res = await fetch(url, init);
if (!res.ok) throw new Error(`${init.method ?? "GET"} ${url.split("?")[0]}: ${res.status} ${await res.text()}`);
return res.json();
}
// DNS. An NSID's authority is all but its name, reversed into a domain:
// each has a `_lexicon` record of its own, since resolution never walks up.
const authority = (nsid) => nsid.split(".").slice(0, -1).reverse().join(".");
const domains = [...new Set([...shipped.keys()].map(authority))].sort();
const outside = domains.filter((d) => d !== zone && !d.endsWith(`.${zone}`));
if (outside.length) throw new Error(`authorities outside ${zone}: ${outside.join(", ")}`);
const wanted = `did=${did}`;
// The zone's `_lexicon` TXT records, from Cloudflare with a token, else as
// a resolver sees them now, over DNS-over-HTTPS rather than through what
// this machine has cached.
const cf = (path, init = {}) =>
json(`https://api.cloudflare.com/client/v4/${path}`, {
...init,
headers: { authorization: `Bearer ${cfToken}`, "content-type": "application/json" },
});
let zoneId;
const records = []; // { name, content, id? }
if (cfToken) {
zoneId = (await cf(`zones?${new URLSearchParams({ name: zone })}`)).result[0]?.id;
if (!zoneId) throw new Error(`the token sees no zone ${zone}`);
for (let page = 1; ; page++) {
const { result, result_info } = await cf(`zones/${zoneId}/dns_records?type=TXT&per_page=500&page=${page}`);
for (const r of result)
if (r.name.startsWith("_lexicon.")) records.push({ id: r.id, name: r.name, content: r.content.replace(/^"|"$/g, "") });
if (page >= result_info.total_pages) break;
}
} else {
for (const domain of domains) {
const name = `_lexicon.${domain}`;
const { Answer = [] } = await json(`https://cloudflare-dns.com/dns-query?${new URLSearchParams({ name, type: "TXT" })}`, {
headers: { accept: "application/dns-json" },
});
// Each string of a record quoted, and a long one split into several.
for (const a of Answer.filter((a) => a.type === 16))
records.push({ name, content: [...a.data.matchAll(/"((?:[^"\\]|\\.)*)"/g)].map((m) => m[1]).join("") });
}
}
const dnsAdd = [];
const dnsFix = [];
for (const domain of domains) {
const name = `_lexicon.${domain}`;
const mine = records.filter((r) => r.name === name && r.content.startsWith("did="));
if (mine.some((r) => r.content === wanted)) continue;
if (mine.length) dnsFix.push(...mine);
else dnsAdd.push(name);
}
// Only records naming this account: another's `_lexicon` in the zone is
// some other publisher's.
const dnsGone = records.filter((r) => r.content === wanted && !domains.includes(r.name.slice("_lexicon.".length)));
for (const name of dnsAdd) console.log(` ${name} TXT "${wanted}" new`);
for (const r of dnsFix) console.log(` ${r.name} TXT "${wanted}" updated, from "${r.content}"`);
for (const r of dnsGone) console.log(` ${r.name} TXT removed`);
// Schemas. What the account holds now of the namespaces published from
// here: the first two segments of every NSID shipped (`dev.knotless`).
const { service = [] } = await json(
did.startsWith("did:web:") ? `https://${did.slice(8)}/.well-known/did.json` : `https://plc.directory/${did}`,
);
const pds = service.find((s) => s.id === "#atproto_pds" || s.id === `${did}#atproto_pds`)?.serviceEndpoint;
if (!pds) throw new Error(`${did} names no PDS`);
const xrpc = (method, { query, body, token } = {}) =>
json(`${pds}/xrpc/${method}${query ? `?${new URLSearchParams(query)}` : ""}`, {
method: body ? "POST" : "GET",
headers: {
...(token && { authorization: `Bearer ${token}` }),
...(body && { "content-type": "application/json" }),
},
body: body && JSON.stringify(body),
});
const roots = new Set([...shipped.keys()].map((id) => id.split(".").slice(0, 2).join(".")));
const ours = (nsid) => [...roots].some((r) => nsid.startsWith(`${r}.`));
const held = new Map();
let cursor;
do {
const page = await xrpc("com.atproto.repo.listRecords", {
query: { repo: did, collection: COLLECTION, limit: "100", ...(cursor && { cursor }) },
});
for (const r of page.records) {
const rkey = r.uri.split("/").pop();
if (ours(rkey)) held.set(rkey, r.value);
}
cursor = page.records.length ? page.cursor : undefined;
} while (cursor);
// JSON with its keys sorted, to compare a record with its file whatever
// order the PDS hands its keys back in.
const canon = (v) =>
Array.isArray(v)
? v.map(canon)
: v && typeof v === "object"
? Object.fromEntries(
Object.keys(v)
.filter((k) => k !== "$type" || v.$type !== COLLECTION)
.sort()
.map((k) => [k, canon(v[k])]),
)
: v;
const same = (a, b) => JSON.stringify(canon(a)) === JSON.stringify(canon(b));
const put = [...shipped.keys()].filter((id) => !held.has(id) || !same(held.get(id), shipped.get(id))).sort();
const gone = [...held.keys()].filter((id) => !shipped.has(id)).sort();
for (const id of put) console.log(` ${id} ${held.has(id) ? "updated" : "new"}`);
for (const id of gone) console.log(` ${id} removed`);
const dnsChanges = dnsAdd.length + dnsFix.length + dnsGone.length;
console.log(
`DNS: ${dnsChanges} to change. Schemas in ${did} at ${pds}: ` +
`${put.length} to put, ${gone.length} to delete, ${shipped.size - put.length} unchanged.`,
);
if (!write || (!dnsChanges && !put.length && !gone.length)) process.exit(0);
if (dnsChanges && !cfToken) throw new Error("CLOUDFLARE_API_TOKEN is needed to change the DNS");
if ((put.length || gone.length) && !LEX_PASSWORD) throw new Error("LEX_PASSWORD is needed to write the schemas");
// The DNS that names the account first, then the schemas, then what no
// longer resolves: a resolver never meets a record pointing at nothing new.
const txt = (name) => ({ type: "TXT", name, content: `"${wanted}"`, ttl: 1, comment: "lexicon authority" });
for (const name of dnsAdd) await cf(`zones/${zoneId}/dns_records`, { method: "POST", body: JSON.stringify(txt(name)) });
for (const r of dnsFix) await cf(`zones/${zoneId}/dns_records/${r.id}`, { method: "PUT", body: JSON.stringify(txt(r.name)) });
if (put.length || gone.length) {
const session = await xrpc("com.atproto.server.createSession", { body: { identifier: did, password: LEX_PASSWORD } });
if (session.did !== did) throw new Error(`signed in as ${session.did}, not ${did}`);
const token = session.accessJwt;
for (const id of put)
await xrpc("com.atproto.repo.putRecord", {
token,
body: { repo: did, collection: COLLECTION, rkey: id, record: { $type: COLLECTION, ...shipped.get(id) } },
});
for (const id of gone)
await xrpc("com.atproto.repo.deleteRecord", { token, body: { repo: did, collection: COLLECTION, rkey: id } });
}
for (const r of dnsGone) await cf(`zones/${zoneId}/dns_records/${r.id}`, { method: "DELETE" });
console.log("published");
deploy/README.md+7−5