chadtmiller.com/pds.js
An atproto PDS in JavaScript, for Cloudflare Workers and Node
0
0
ci: run the workflows on knotless
.knotless/workflows/canary-worker.ymlnew+32−0
| @@ -0,0 +1,32 @@ | ||
# Publishes a worker artifact to the canary channel on every push to main. | ||
# Its version is the released one plus `-canary.<commit>`. The publish | ||
# refuses to move the channel to a commit behind the one it has, so a | ||
# revert needs a publish by hand from a machine with the credentials. | ||
on: | ||
push: {refs: [refs/heads/main]} | ||
image: node:22-trixie | ||
cache: [/root/.local/share/pnpm/store, /root/.cache/node/corepack] | ||
secrets: [PDSJS_SITE_TOKEN] | ||
env: | ||
COREPACK_ENABLE_DOWNLOAD_PROMPT: "0" | ||
PDS_URL: https://pds.pdsjs.dev | ||
PDS_IDENTIFIER: pdsjs.dev | ||
steps: | ||
- name: check credentials | ||
run: | | ||
if [ -z "${PDSJS_SITE_TOKEN:-}" ]; then | ||
echo "PDSJS_SITE_TOKEN is not granted, so this push would publish nothing." | ||
echo "Grant it in the runner's secrets file. It is an app password for $PDS_IDENTIFIER." | ||
exit 1 | ||
fi | ||
echo "publishing to $PDS_URL as $PDS_IDENTIFIER" | ||
- name: build | ||
run: | | ||
corepack enable | ||
pnpm install --frozen-lockfile | ||
npm run build:ui | ||
VERSION_SUFFIX="-canary.$(echo "$KNOTLESS_RUN_SHA" | cut -c1-7)" npm run build:worker | ||
test -s dist/worker/index.js | ||
test -s dist/worker/manifest.json | ||
- name: publish | ||
run: PDS_PASSWORD="$PDSJS_SITE_TOKEN" CHANNEL=canary node scripts/publish-worker-artifact.mjs | ||
.knotless/workflows/ci.ymlnew+23−0
| @@ -0,0 +1,23 @@ | ||
# The gate: `npm run ci`, on every branch pushed and on pulls into main. | ||
# The UI build in it comes first: @pdsjs/account-ui exports a gitignored | ||
# bundle, and without it tsc and the tests cannot resolve the node and | ||
# cloudflare entrypoints. The e2e suite needs the docker-compose stack, so | ||
# it is not part of the gate. | ||
# | ||
# trixie, not bookworm: better-sqlite3's prebuilt binary needs glibc 2.38 | ||
# or newer. corepack activates the pnpm that package.json pins. | ||
on: | ||
push: {refs: [refs/heads/*]} | ||
pull: {base: [refs/heads/main]} | ||
queue: first | ||
image: node:22-trixie | ||
cache: [/root/.local/share/pnpm/store, /root/.cache/node/corepack] | ||
env: | ||
COREPACK_ENABLE_DOWNLOAD_PROMPT: "0" | ||
steps: | ||
- name: install | ||
run: | | ||
corepack enable | ||
pnpm install --frozen-lockfile | ||
- name: ci | ||
run: npm run ci | ||
.knotless/workflows/mirror-repo.ymlnew+45−0
| @@ -0,0 +1,45 @@ | ||
# Mirrors main and each version tag to atproto://pdsjs.dev/pds.js, the | ||
# dev.pdsjs.git.repo record that packages/git describes. Stock git reads it | ||
# at https://pds.pdsjs.dev/git/pdsjs.dev/pds.js. One run pushes one ref, the | ||
# one it runs on, so a tag push mirrors that tag alone. Nothing pushes the | ||
# other way, and the mirror only fast-forwards to the commit run here. | ||
# | ||
# The push uses this repository's own helper, so the code being mirrored | ||
# writes the mirror. PDSJS_SITE_TOKEN is an app password for pdsjs.dev, | ||
# granted in the runner's secrets file to main and v* tags. | ||
on: | ||
push: {refs: [refs/heads/main, refs/tags/v*]} | ||
image: node:22-trixie | ||
cache: [/root/.local/share/pnpm/store, /root/.cache/node/corepack] | ||
secrets: [PDSJS_SITE_TOKEN] | ||
env: | ||
COREPACK_ENABLE_DOWNLOAD_PROMPT: "0" | ||
PDS_IDENTIFIER: pdsjs.dev | ||
MIRROR_REMOTE: atproto://pdsjs.dev/pds.js | ||
steps: | ||
- name: check credentials | ||
run: | | ||
if [ -z "${PDSJS_SITE_TOKEN:-}" ]; then | ||
echo "PDSJS_SITE_TOKEN is not granted, so this push would mirror nothing." | ||
echo "Grant it in the runner's secrets file. It is an app password for $PDS_IDENTIFIER." | ||
exit 1 | ||
fi | ||
- name: install the helper | ||
run: | | ||
corepack enable | ||
pnpm --filter @pdsjs/git install --frozen-lockfile | ||
ln -sf "$PWD/packages/git/src/cli.js" /usr/local/bin/git-remote-atproto | ||
# A push that loses the record's compare-and-swap to another is told to | ||
# fetch and retry. Each attempt reads the record again. | ||
- name: push | ||
run: | | ||
git config --global --add safe.directory "$PWD" | ||
refspec="$KNOTLESS_RUN_SHA:$KNOTLESS_RUN_REF" | ||
for attempt in 1 2 3; do | ||
echo "push $refspec to $MIRROR_REMOTE (attempt $attempt)" | ||
if ATPROTO_GIT_PASSWORD="$PDSJS_SITE_TOKEN" git push "$MIRROR_REMOTE" "$refspec"; then | ||
exit 0 | ||
fi | ||
sleep 5 | ||
done | ||
exit 1 | ||
.knotless/workflows/publish-git-app.ymlnew+29−0
| @@ -0,0 +1,29 @@ | ||
# Publishes the repository browser to git.pdsjs.dev on every push to main, | ||
# and repins the manifest that an install tracks. The deploy writes a | ||
# record only when the build changes it, so a push that leaves the browser | ||
# alone moves no CID. | ||
on: | ||
push: {refs: [refs/heads/main]} | ||
image: node:22-trixie | ||
cache: [/root/.local/share/pnpm/store, /root/.cache/node/corepack] | ||
secrets: [PDSJS_SITE_TOKEN] | ||
env: | ||
COREPACK_ENABLE_DOWNLOAD_PROMPT: "0" | ||
PDS_URL: https://pds.pdsjs.dev | ||
PDS_DID: did:plc:ye5hhvya5o2fwvouv3wttkkn | ||
steps: | ||
- name: check credentials | ||
run: | | ||
if [ -z "${PDSJS_SITE_TOKEN:-}" ]; then | ||
echo "PDSJS_SITE_TOKEN is not granted, so this push would publish nothing." | ||
echo "Grant it in the runner's secrets file. It is an app password for pdsjs.dev." | ||
exit 1 | ||
fi | ||
echo "publishing to $PDS_URL as $PDS_DID" | ||
- name: build | ||
run: | | ||
corepack enable | ||
pnpm install --frozen-lockfile | ||
pnpm --filter @pdsjs/git-ui run build | ||
- name: publish | ||
run: PDS_APP_PASSWORD="$PDSJS_SITE_TOKEN" node packages/git-ui/scripts/deploy.mjs | ||
.knotless/workflows/publish-spaces-app.ymlnew+33−0
| @@ -0,0 +1,33 @@ | ||
# Publishes the space browser to spaces.pdsjs.dev on every push to main. | ||
# The deploy writes a record only when the build changes it. | ||
on: | ||
push: {refs: [refs/heads/main]} | ||
image: node:22-trixie | ||
cache: [/root/.local/share/pnpm/store, /root/.cache/node/corepack] | ||
secrets: [PDSJS_SITE_TOKEN] | ||
env: | ||
COREPACK_ENABLE_DOWNLOAD_PROMPT: "0" | ||
PDS_URL: https://pds.pdsjs.dev | ||
PDS_DID: did:plc:ye5hhvya5o2fwvouv3wttkkn | ||
# The site record's key is the subdomain it answers on, and the origin is | ||
# where that subdomain answers. The two must agree: the origin goes into | ||
# the OAuth client document, and an authorization server refuses a | ||
# document that names anywhere other than where it was fetched from. | ||
SPACES_SITE: spaces | ||
SPACES_ORIGIN: https://spaces.pdsjs.dev | ||
steps: | ||
- name: check credentials | ||
run: | | ||
if [ -z "${PDSJS_SITE_TOKEN:-}" ]; then | ||
echo "PDSJS_SITE_TOKEN is not granted, so this push would publish nothing." | ||
echo "Grant it in the runner's secrets file. It is an app password for pdsjs.dev." | ||
exit 1 | ||
fi | ||
echo "publishing to $PDS_URL as $PDS_DID, signing in as $SPACES_ORIGIN/client-metadata.json" | ||
- name: build | ||
run: | | ||
corepack enable | ||
pnpm install --frozen-lockfile | ||
pnpm --filter @pdsjs/spaces-ui run build | ||
- name: publish | ||
run: PDS_APP_PASSWORD="$PDSJS_SITE_TOKEN" node packages/spaces-ui/scripts/deploy.mjs | ||
.knotless/workflows/publish-wizard.ymlnew+29−0
| @@ -0,0 +1,29 @@ | ||
# Publishes the setup wizard to start.pdsjs.dev on every push to main. | ||
on: | ||
push: {refs: [refs/heads/main]} | ||
image: node:22-trixie | ||
cache: [/root/.local/share/pnpm/store, /root/.cache/node/corepack] | ||
secrets: [PDSJS_SITE_TOKEN] | ||
env: | ||
COREPACK_ENABLE_DOWNLOAD_PROMPT: "0" | ||
PDS_URL: https://pds.pdsjs.dev | ||
PDS_IDENTIFIER: pdsjs.dev | ||
# Public either way: the site serves them in config.json. | ||
OAUTH_CLIENT_ID: 54445ecf5107dc657b05f88b9ab1f12d | ||
OAUTH_SCOPES: account-settings.read workers-scripts.write workers-r2.write zone.read dns.write | ||
steps: | ||
- name: check credentials | ||
run: | | ||
if [ -z "${PDSJS_SITE_TOKEN:-}" ]; then | ||
echo "PDSJS_SITE_TOKEN is not granted, so this push would publish nothing." | ||
echo "Grant it in the runner's secrets file. It is an app password for $PDS_IDENTIFIER." | ||
exit 1 | ||
fi | ||
echo "publishing to $PDS_URL as $PDS_IDENTIFIER" | ||
- name: build | ||
run: | | ||
corepack enable | ||
pnpm install --frozen-lockfile | ||
pnpm --filter @pdsjs/start run build | ||
- name: publish | ||
run: PDS_PASSWORD="$PDSJS_SITE_TOKEN" pnpm --filter @pdsjs/start run publish-site | ||
.knotless/workflows/README.mdnew+35−0
| @@ -0,0 +1,35 @@ | ||
# Workflows | ||
| ||
knotless.dev's hosted runner runs these for chadtmiller.com/pds.js. The | ||
format is in knotless's | ||
[workflow reference](https://knotless.dev/chadtmiller.com/knotless/blob/main/docs/reference/workflows.md). | ||
The runner reads only the `.yml`, `.yaml` and `.json` files here. Each | ||
workflow says what it does, and why, in its comments. | ||
| ||
- `ci` is the gate, on every branch and on pulls into main. | ||
- `mirror-repo` pushes main and each `v*` tag to `atproto://pdsjs.dev/pds.js`. | ||
- `canary-worker`, `publish-git-app`, `publish-spaces-app` and | ||
`publish-wizard` publish on every push to main. | ||
- `release-npm`, `release-worker`, `release-image` and | ||
`release-runner-image` publish on a `v*` tag, which `npm run release:cut` | ||
pushes. | ||
| ||
Each step runs in a container with no docker daemon, so the two image | ||
workflows build with kaniko. | ||
| ||
## Secrets | ||
| ||
A pull gets no secrets. The runner grants these from its secrets file, | ||
under `did:plc:bcgltzqazw5tb6k2g3ttenbj/pds.js`, to the refs named: | ||
| ||
- `PDSJS_SITE_TOKEN`, an app password for pdsjs.dev, to main and `v*` tags. | ||
The mirror, the sites, the worker artifacts and the PDS registry copies | ||
use it. | ||
- `NPM_TOKEN`, an npm automation token with publish rights for the | ||
`@pdsjs` scope, to `v*` tags. | ||
- `RELEASE_PASSWORD`, an app password for chadtmiller.com, to `v*` tags. | ||
The image workflows publish the images with it, as the repository's | ||
owner. | ||
| ||
A workflow that is not granted a secret it needs fails at its first step | ||
and says which one. | ||
.knotless/workflows/release-image.ymlnew+122−0
| @@ -0,0 +1,122 @@ | ||
# Builds the PDS container image on a version tag, and publishes it as the | ||
# `pds.js` release of this repository, which knotless.dev serves as | ||
# `docker pull knotless.dev/chadtmiller.com/pds.js/pds.js:<version>`. It | ||
# copies it to the pdsjs.dev PDS's own registry (@pdsjs/oci) too. | ||
# | ||
# The runner gives a step no docker daemon, so kaniko builds the image. Once | ||
# a build ends, kaniko leaves the image's filesystem at /, and | ||
# scripts/run-image.mjs runs the smoke test in it as the image would run. | ||
# The image goes up as the OCI layout kaniko writes, with `knotless publish | ||
# --oci`, as the repository's owner: RELEASE_PASSWORD is an app password of | ||
# theirs, granted to v* tags in the runner's secrets file. | ||
# | ||
# Asked for at a branch, `knotless rerun knotless://chadtmiller.com/pds.js | ||
# --ref refs/heads/<branch> --workflow release-image`, it builds and tests | ||
# the image and publishes nothing. | ||
on: | ||
push: {refs: [refs/tags/v*]} | ||
asked: {refs: [refs/heads/*]} | ||
superseded: finish | ||
image: buildpack-deps:trixie-curl | ||
secrets: [RELEASE_PASSWORD, PDSJS_SITE_TOKEN] | ||
env: | ||
REPO: knotless://chadtmiller.com/pds.js | ||
PACKAGE: pds.js | ||
DOCKERFILE: Dockerfile | ||
KNOTLESS_APPVIEW: https://knotless.dev | ||
PDS_HOST: pds.pdsjs.dev | ||
CRANE_VERSION: v0.20.2 | ||
KANIKO_IMAGE: gcr.io/kaniko-project/executor:v1.23.2 | ||
# Where kaniko's own image keeps them. A build replaces the rest of /. | ||
SSL_CERT_DIR: /kaniko/ssl/certs | ||
DOCKER_CONFIG: /kaniko/.docker | ||
steps: | ||
- name: check credentials | ||
run: | | ||
case "$KNOTLESS_RUN_REF" in | ||
refs/tags/v*) | ||
if [ -z "${RELEASE_PASSWORD:-}" ]; then | ||
echo "RELEASE_PASSWORD is not granted, so this tag would publish nothing." | ||
echo "Grant it in the runner's secrets file. It is an app password for the repository's owner." | ||
exit 1 | ||
fi ;; | ||
*) echo "at $KNOTLESS_RUN_REF: building and testing, publishing nothing" ;; | ||
esac | ||
# kaniko's own image starts kaniko, not the command the runner keeps a | ||
# container up with, so the steps take kaniko from its image. They run in | ||
# Debian, whose /sbin is a link as in the images built: the runner mounts | ||
# docker-init under /sbin, and a build cannot replace a directory that | ||
# holds a mount with a link. The tools go under /kaniko, which a build | ||
# leaves alone. | ||
- name: tools | ||
run: | | ||
mkdir -p /kaniko/tools | ||
cd /kaniko/tools | ||
curl -fsSL "https://github.com/google/go-containerregistry/releases/download/$CRANE_VERSION/go-containerregistry_Linux_x86_64.tar.gz" | tar -xzf - crane | ||
./crane export "$KANIKO_IMAGE" - | tar -xf - -C / kaniko | ||
/kaniko/executor version | ||
v=$(curl -fsSL https://knotless.dev/releases/latest) | ||
name="knotless-$v-x86_64-unknown-linux-musl" | ||
curl -fsSLO "https://knotless.dev/releases/$v/$name.tar.gz" | ||
curl -fsSL -o sum "https://knotless.dev/releases/$v/$name.tar.gz.sha256" | ||
[ "$(sha256sum "$name.tar.gz" | cut -d' ' -f1)" = "$(cut -d' ' -f1 sum)" ] | ||
tar -xzf "$name.tar.gz" | ||
mv "$name/knotless" knotless | ||
- name: build | ||
timeout: 45 | ||
run: | | ||
# kaniko builds over the files of the image the steps run in, and | ||
# removes them only between stages. A first stage of nothing makes it | ||
# remove them before the Dockerfile's own stages. | ||
{ echo 'FROM scratch AS empty'; cat "$DOCKERFILE"; } > /kaniko/Dockerfile | ||
/kaniko/executor --force --context "dir://$PWD" --dockerfile /kaniko/Dockerfile \ | ||
--no-push --oci-layout-path /kaniko/image --snapshot-mode redo | ||
cat /kaniko/image/index.json | ||
# A tagged image that cannot boot is worse than no image. Every variable | ||
# the entrypoint demands is set here: a new one in docker/server.js fails | ||
# this test until it is added here too. | ||
- name: smoke test | ||
run: | | ||
node scripts/run-image.mjs /kaniko/image \ | ||
PDS_HOSTNAME=pds.example.com JWT_SECRET=smoke-test-secret \ | ||
PDS_PASSWORD=smoke-test-password PDS_ENABLE_SPACES=true > /tmp/server.log 2>&1 & | ||
server=$! | ||
health='fetch("http://127.0.0.1:3000/xrpc/_health").then(r=>process.exit(r.ok?0:1),()=>process.exit(1))' | ||
for _ in $(seq 1 30); do | ||
if node -e "$health"; then | ||
kill "$server" | ||
echo "the image answers its health check" | ||
exit 0 | ||
fi | ||
kill -0 "$server" 2>/dev/null || break | ||
sleep 1 | ||
done | ||
tail -30 /tmp/server.log | ||
kill "$server" 2>/dev/null || true | ||
exit 1 | ||
- name: publish | ||
run: | | ||
case "$KNOTLESS_RUN_REF" in refs/tags/v*) ;; *) exit 0 ;; esac | ||
version="${KNOTLESS_RUN_REF#refs/tags/v}" | ||
KNOTLESS_PASSWORD="$RELEASE_PASSWORD" /kaniko/tools/knotless publish "$REPO" "$PACKAGE" "$version" --oci /kaniko/image | ||
# A copy, like a mirror: it never fails a release on its own. A PDS | ||
# without the feature answers /v2/ with 404 rather than the challenge. | ||
- name: copy to the PDS | ||
run: | | ||
case "$KNOTLESS_RUN_REF" in refs/tags/v*) ;; *) exit 0 ;; esac | ||
if [ -z "${PDSJS_SITE_TOKEN:-}" ]; then | ||
echo "PDSJS_SITE_TOKEN is not granted, so the PDS copy is skipped" | ||
exit 0 | ||
fi | ||
status=$(node -e "fetch('https://$PDS_HOST/v2/').then((r) => console.log(r.status)).catch(() => console.log(0))") | ||
if [ "$status" != "401" ]; then | ||
echo "https://$PDS_HOST/v2/ answered $status, not the challenge, so the PDS registry is off" | ||
exit 0 | ||
fi | ||
tag="${KNOTLESS_RUN_REF#refs/tags/}" | ||
crane=/kaniko/tools/crane | ||
echo "$PDSJS_SITE_TOKEN" | $crane auth login "$PDS_HOST" -u pdsjs.dev --password-stdin | ||
if ! $crane push /kaniko/image "$PDS_HOST/$PACKAGE:$tag" || ! $crane tag "$PDS_HOST/$PACKAGE:$tag" latest; then | ||
echo "the PDS refused the push. A layer can be over the platform's request limit" | ||
echo "(100 MB on the lowest Cloudflare plan). The release stands on knotless." | ||
fi | ||
.pdsjs/workflows/release-npm.yml → .knotless/workflows/release-npm.ymlrenamed+26−44
| @@ -1,96 +1,78 @@ | ||
# Publishes the packages to npm on a version tag. The tag reaches the | ||
# repository record because mirror-repo.yml mirrors it, one tag per run. | ||
name: release-npm | ||
refs: [refs/tags/v*] | ||
| ||
# Visible to anyone reading the repo, so only non-secret values belong here. | ||
env: | ||
PDS_HOST: "pds.pdsjs.dev" | ||
| ||
# Publishes the packages to npm on a version tag, and copies them to the | ||
# pdsjs.dev PDS's own npm registry (@pdsjs/npm). changeset publish takes | ||
# every package whose version is not on npm, so a package releases for the | ||
# first time with no changeset at all. | ||
on: | ||
push: {refs: [refs/tags/v*]} | ||
superseded: finish | ||
image: node:22-trixie | ||
cache: [/root/.local/share/pnpm/store, /root/.cache/node/corepack] | ||
secrets: [NPM_TOKEN, PDSJS_SITE_TOKEN] | ||
| ||
env: | ||
COREPACK_ENABLE_DOWNLOAD_PROMPT: "0" | ||
PDS_HOST: pds.pdsjs.dev | ||
steps: | ||
- name: check publish credentials | ||
- name: check credentials | ||
run: | | ||
set -e | ||
if [ -z "${NPM_TOKEN:-}" ]; then | ||
echo "NPM_TOKEN is not granted, so this tag would publish nothing." | ||
echo "Grant it in the runner's secrets file. It is an npm automation token with publish rights for the @pdsjs scope." | ||
exit 1 | ||
fi | ||
| ||
# A tag that does not name the versioned state would mislabel every | ||
# package, and changeset publish cannot be taken back. | ||
- name: check the tag names the versioned state | ||
run: | | ||
set -e | ||
tag="${PDSJS_CI_REF#refs/tags/}" | ||
tag="${KNOTLESS_RUN_REF#refs/tags/}" | ||
core=$(node -p 'require("./packages/core/package.json").version') | ||
if [ "v$core" != "$tag" ]; then | ||
echo "tag is $tag but @pdsjs/core is $core" | ||
echo "the tag is $tag but @pdsjs/core is $core" | ||
exit 1 | ||
fi | ||
pending=$(ls .changeset/*.md 2>/dev/null | grep -cv README || true) | ||
if [ "$pending" != "0" ]; then | ||
echo "$pending changeset(s) still pending. Run version-packages first." | ||
echo "$pending changesets are pending. Run version-packages first." | ||
exit 1 | ||
fi | ||
| ||
# changeset publish takes every package whose version is not on npm, which | ||
# is how a package releases for the first time with no changeset at all. | ||
- name: build and publish to npm | ||
- name: publish to npm | ||
run: | | ||
set -e | ||
echo "==> pnpm install" | ||
corepack enable | ||
pnpm install --frozen-lockfile | ||
echo '==> build (generates the .d.ts files publish ships)' | ||
pnpm run build | ||
{ | ||
echo "//registry.npmjs.org/:_authToken=${NPM_TOKEN}" | ||
echo "git-checks=false" | ||
} > .npmrc | ||
echo "==> changeset publish" | ||
pnpm exec changeset publish --no-git-tag | ||
| ||
# The pdsjs.dev PDS is itself an npm registry (the @pdsjs/npm package), so | ||
# the release also lands in the account that owns the @pdsjs scope. The | ||
# site token, an app password for pdsjs.dev, signs in. Every public | ||
# package at its current version goes up, skipping versions the PDS | ||
# already holds, so a run after a gap backfills and ordinary runs carry | ||
# only the new versions. A copy like the ghcr mirror: it never fails a | ||
# release on its own. | ||
- name: publish to the account's PDS | ||
# Every public package goes up at its current version, less the versions | ||
# the PDS holds, so a run after a gap fills it in. Like a mirror, it never | ||
# fails a release on its own. | ||
- name: publish to the PDS | ||
run: | | ||
set -e | ||
if [ -z "${PDSJS_SITE_TOKEN:-}" ]; then | ||
echo "PDSJS_SITE_TOKEN is not granted, so the PDS copy is skipped" | ||
exit 0 | ||
fi | ||
| ||
registry="https://$PDS_HOST/npm/" | ||
status=$(node -e "fetch('$registry-/ping').then((r) => console.log(r.status)).catch(() => console.log(0))") | ||
if [ "$status" != "200" ]; then | ||
echo "$registry answered $status to the ping; the PDS registry is off" | ||
echo "and this step skips" | ||
echo "$registry answered $status to the ping, so the PDS registry is off and this step skips" | ||
exit 0 | ||
fi | ||
| ||
echo "//$PDS_HOST/npm/:_authToken=${PDSJS_SITE_TOKEN}" >> .npmrc | ||
| ||
for dir in packages/*/; do | ||
name=$(node -p "require('./$dir/package.json').name" 2>/dev/null) || continue | ||
private=$(node -p "Boolean(require('./$dir/package.json').private)") | ||
version=$(node -p "require('./$dir/package.json').version") | ||
[ "$private" = "true" ] && continue | ||
if npm view "$name@$version" version --registry "$registry" >/dev/null 2>&1; then | ||
echo "$name@$version is already on the PDS" | ||
echo "$name@$version is on the PDS already" | ||
continue | ||
fi | ||
# pnpm rewrites workspace:* ranges at pack time, the way the npmjs | ||
# publish path does through changesets. | ||
# pnpm rewrites workspace:* ranges as it packs, as changesets does. | ||
if (cd "$dir" && pnpm publish --registry "$registry" --no-git-checks --access public); then | ||
echo "published $name@$version to the PDS" | ||
else | ||
echo "the PDS refused $name@$version; the release already stands on npmjs" | ||
echo "the PDS refused $name@$version; the release stands on npmjs" | ||
fi | ||
done | ||
.knotless/workflows/release-runner-image.ymlnew+132−0
| @@ -0,0 +1,132 @@ | ||
# Builds the image of @pdsjs/git-ci, the check runner, on a version tag, | ||
# and publishes it as the `pds.js-git-ci` release of this repository: | ||
# `docker pull knotless.dev/chadtmiller.com/pds.js/pds.js-git-ci:<version>`. | ||
# It copies it to the pdsjs.dev PDS's own registry too. release-image.yml | ||
# says how the image is built and published with no docker daemon. | ||
on: | ||
push: {refs: [refs/tags/v*]} | ||
asked: {refs: [refs/heads/*]} | ||
superseded: finish | ||
image: buildpack-deps:trixie-curl | ||
secrets: [RELEASE_PASSWORD, PDSJS_SITE_TOKEN] | ||
env: | ||
REPO: knotless://chadtmiller.com/pds.js | ||
PACKAGE: pds.js-git-ci | ||
DOCKERFILE: packages/git-ci/Dockerfile | ||
KNOTLESS_APPVIEW: https://knotless.dev | ||
PDS_HOST: pds.pdsjs.dev | ||
CRANE_VERSION: v0.20.2 | ||
KANIKO_IMAGE: gcr.io/kaniko-project/executor:v1.23.2 | ||
# Where kaniko's own image keeps them. A build replaces the rest of /. | ||
SSL_CERT_DIR: /kaniko/ssl/certs | ||
DOCKER_CONFIG: /kaniko/.docker | ||
steps: | ||
- name: check credentials | ||
run: | | ||
case "$KNOTLESS_RUN_REF" in | ||
refs/tags/v*) | ||
if [ -z "${RELEASE_PASSWORD:-}" ]; then | ||
echo "RELEASE_PASSWORD is not granted, so this tag would publish nothing." | ||
echo "Grant it in the runner's secrets file. It is an app password for the repository's owner." | ||
exit 1 | ||
fi ;; | ||
*) echo "at $KNOTLESS_RUN_REF: building and testing, publishing nothing" ;; | ||
esac | ||
# kaniko's own image starts kaniko, not the command the runner keeps a | ||
# container up with, so the steps take kaniko from its image. They run in | ||
# Debian, whose /sbin is a link as in the images built: the runner mounts | ||
# docker-init under /sbin, and a build cannot replace a directory that | ||
# holds a mount with a link. The tools go under /kaniko, which a build | ||
# leaves alone. | ||
- name: tools | ||
run: | | ||
mkdir -p /kaniko/tools | ||
cd /kaniko/tools | ||
curl -fsSL "https://github.com/google/go-containerregistry/releases/download/$CRANE_VERSION/go-containerregistry_Linux_x86_64.tar.gz" | tar -xzf - crane | ||
./crane export "$KANIKO_IMAGE" - | tar -xf - -C / kaniko | ||
/kaniko/executor version | ||
v=$(curl -fsSL https://knotless.dev/releases/latest) | ||
name="knotless-$v-x86_64-unknown-linux-musl" | ||
curl -fsSLO "https://knotless.dev/releases/$v/$name.tar.gz" | ||
curl -fsSL -o sum "https://knotless.dev/releases/$v/$name.tar.gz.sha256" | ||
[ "$(sha256sum "$name.tar.gz" | cut -d' ' -f1)" = "$(cut -d' ' -f1 sum)" ] | ||
tar -xzf "$name.tar.gz" | ||
mv "$name/knotless" knotless | ||
- name: build | ||
timeout: 45 | ||
run: | | ||
# kaniko builds over the files of the image the steps run in, and | ||
# removes them only between stages. A first stage of nothing makes it | ||
# remove them before the Dockerfile's own stages. | ||
{ echo 'FROM scratch AS empty'; cat "$DOCKERFILE"; } > /kaniko/Dockerfile | ||
/kaniko/executor --force --context "dir://$PWD" --dockerfile /kaniko/Dockerfile \ | ||
--no-push --oci-layout-path /kaniko/image --snapshot-mode redo | ||
cat /kaniko/image/index.json | ||
# The daemon listens on no port and exits without credentials, so the | ||
# test is what it prints, and as which user. | ||
- name: smoke test | ||
run: | | ||
run="node scripts/run-image.mjs /kaniko/image" | ||
| ||
# Named no repositories, the CLI prints its usage and exits 129. | ||
code=0 | ||
usage=$($run 2>&1) || code=$? | ||
if [ "$code" != 129 ]; then | ||
echo "expected exit 129 with no repositories, got $code" | ||
echo "$usage" | ||
exit 1 | ||
fi | ||
case "$usage" in | ||
*PDSJS_CI_IDENTIFIER*) ;; | ||
*) echo "the usage text is missing: $usage"; exit 1 ;; | ||
esac | ||
| ||
# Given a repository and no credentials, it reaches the credential check. | ||
fatal=$($run PDSJS_CI_REPOS=alice.example.com/repo 2>&1) || true | ||
case "$fatal" in | ||
*"must both be set"*) ;; | ||
*) echo "expected the credential error, got: $fatal"; exit 1 ;; | ||
esac | ||
| ||
# The entrypoint takes the state directory as root, then drops. | ||
who=$($run -- id -un) | ||
if [ "$who" != node ]; then | ||
echo "steps would run as $who, not node" | ||
exit 1 | ||
fi | ||
| ||
# A workflow clones the repository under test, installs it, and may | ||
# build an image. Without any one of these a run fails at its first step. | ||
$run -- sh -c ' | ||
set -e | ||
git --version | ||
corepack --version | ||
command -v git-remote-atproto | ||
command -v g++ | ||
docker --version | ||
docker buildx version | ||
' | ||
- name: publish | ||
run: | | ||
case "$KNOTLESS_RUN_REF" in refs/tags/v*) ;; *) exit 0 ;; esac | ||
version="${KNOTLESS_RUN_REF#refs/tags/v}" | ||
KNOTLESS_PASSWORD="$RELEASE_PASSWORD" /kaniko/tools/knotless publish "$REPO" "$PACKAGE" "$version" --oci /kaniko/image | ||
- name: copy to the PDS | ||
run: | | ||
case "$KNOTLESS_RUN_REF" in refs/tags/v*) ;; *) exit 0 ;; esac | ||
if [ -z "${PDSJS_SITE_TOKEN:-}" ]; then | ||
echo "PDSJS_SITE_TOKEN is not granted, so the PDS copy is skipped" | ||
exit 0 | ||
fi | ||
status=$(node -e "fetch('https://$PDS_HOST/v2/').then((r) => console.log(r.status)).catch(() => console.log(0))") | ||
if [ "$status" != "401" ]; then | ||
echo "https://$PDS_HOST/v2/ answered $status, not the challenge, so the PDS registry is off" | ||
exit 0 | ||
fi | ||
tag="${KNOTLESS_RUN_REF#refs/tags/}" | ||
crane=/kaniko/tools/crane | ||
echo "$PDSJS_SITE_TOKEN" | $crane auth login "$PDS_HOST" -u pdsjs.dev --password-stdin | ||
if ! $crane push /kaniko/image "$PDS_HOST/$PACKAGE:$tag" || ! $crane tag "$PDS_HOST/$PACKAGE:$tag" latest; then | ||
echo "the PDS refused the push. A layer can be over the platform's request limit" | ||
echo "(100 MB on the lowest Cloudflare plan). The release stands on knotless." | ||
fi | ||