Something went wrong. Try again.
A simple DAV sync application / cli
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140#!/bin/sh# Release davsync from CI, on the spindle: build the bundle, attach it to the tag# being released, vouch for it with an atproto attestation, and point the# `davsync` bundle listing at it, so that## bundle install @<account>/davsync## works, and `bundle update` follows new releases.## The bundle is not signed: who stands behind it is said by the attestation# (`bundle attest --kind published`), which installs check.## Secrets (set them with `tangled secret set <NAME>`):# TANGLED_HANDLE, TANGLED_APP_PASSWORD the account that uploads the release file# (the repository's owner, or a collaborator)# Optional secrets:# BUNDLE_ATPROTO_IDENTIFIER, BUNDLE_ATPROTO_PASSWORD# another account to attest and publish as# (default: the uploading account)# TANGLED_RELEASE_TAG the tag (default: TANGLED_REF_NAME, set by the spindle)## Run by hand it is a dry run, unless it is run on the tag and told to release# that tag, which is how a release that failed part way is finished (every# step can be repeated: the upload replaces, the attestation and listing are# rewritten, and the bundle builds the same bytes again):## tangled run trigger -w release.yml -r refs/tags/v0.1.0 -f release=v0.1.0set -eu
# Retry a command through eurosky's createSession rate-limit window: four# simultaneous releases 429'd the PDS login endpoint, and the tools' own quick# retries cannot outlast the window. Generous sleeps, then fail loudly.with_backoff() { tries=4 delay=45 i=1 while :; do if "$@"; then return 0; fi [ "$i" -ge "$tries" ] && { echo "error: still failing after $i attempts: $*" >&2; return 1; } echo "* attempt $i failed - retrying in ${delay}s (rate-limit backoff)" >&2 sleep "$delay" i=$((i + 1)); delay=$((delay * 2)) done}
DRY_RUN="${RELEASE_DRY_RUN:-}"if [ "${TANGLED_PIPELINE_KIND:-}" = "manual" ]; then if [ "${TANGLED_REF_TYPE:-}" = "tag" ] && [ -n "${TANGLED_INPUT_RELEASE:-}" ] && [ "${TANGLED_INPUT_RELEASE}" = "${TANGLED_REF_NAME:-}" ]; then echo "* run by hand, and told to release ${TANGLED_REF_NAME}" else DRY_RUN=1 fifi
TAG="${TANGLED_RELEASE_TAG:-${TANGLED_REF_NAME:-}}"export TAGif [ -z "$DRY_RUN" ]; then : "${TAG:?no tag to release}" : "${TANGLED_HANDLE:?set the TANGLED_HANDLE secret}" : "${TANGLED_APP_PASSWORD:?set the TANGLED_APP_PASSWORD secret}" BUNDLE_ATPROTO_IDENTIFIER="${BUNDLE_ATPROTO_IDENTIFIER:-$TANGLED_HANDLE}" BUNDLE_ATPROTO_PASSWORD="${BUNDLE_ATPROTO_PASSWORD:-$TANGLED_APP_PASSWORD}" export BUNDLE_ATPROTO_IDENTIFIER BUNDLE_ATPROTO_PASSWORDfi
echo "* releasing ${TAG:-(dry run)}"
if ! node -e 'const [a,b]=process.versions.node.split(".").map(Number);process.exit(a>26||(a===26&&b>=11)?0:1)' 2>/dev/null; then echo "error: node $(node --version 2>/dev/null || echo missing) is older than 26.11.1; add a newer nodejs to the workflow's dependencies" >&2 exit 1fiecho "* node $(node --version)"
# The tag names the version the app reports; they must agree.version="v$(node -p 'require("./package.json").version')"if [ -z "$DRY_RUN" ] && [ "$TAG" != "$version" ]; then echo "error: the tag is $TAG, but package.json says $version; set the version and tag that" >&2 exit 1fi
npm ci --no-audit --no-fundnpm testnpm run test:packagesnpm run packnpm run smoke./build/davsync.nzip --version
if [ -n "$DRY_RUN" ]; then echo "* dry run: built and checked build/davsync.nzip ($(sha256sum build/davsync.nzip | cut -d' ' -f1)); nothing uploaded, attested or published" exit 0fi
BUNDLE="node --no-warnings --experimental-vfs node_modules/@pipobscure/bundle/dist/main.js"
# The tangled CLI, to upload release files: installed with bundle, accepted# only with its publisher's attestation (ci/policy.json), into this run alone.CI_HOME="$PWD/.tangled-ci"mkdir -p "$CI_HOME/bin"env HOME="$CI_HOME" XDG_CONFIG_HOME="$CI_HOME/config" XDG_DATA_HOME="$CI_HOME/data" XDG_STATE_HOME="$CI_HOME/state" XDG_CACHE_HOME="$CI_HOME/cache" \ BUNDLE_POLICY="$PWD/ci/policy.json" BUNDLE_SYSTEM_POLICY="$CI_HOME/none.json" \ $BUNDLE install --dir "$CI_HOME/bin" @pipobscure.com/tangled < /dev/nullexport TANGLED="$CI_HOME/bin/tangled"export TANGLED_CONFIG_DIR="$CI_HOME/tangled" TANGLED_NO_PROMPT=1
with_backoff sh -c 'printf '\''%s\n'\'' "$TANGLED_APP_PASSWORD" | "$TANGLED" auth login --app-password --password-stdin "$TANGLED_HANDLE"'with_backoff sh -c '"$TANGLED" release upload --clobber --json "$TAG" build/davsync.nzip > .release.json'cat .release.json
# blob.pipobscure.com serves a file once it has seen the record, which may# take a while (up to its 300 s cache window); the uploader's PDS serves it at# once. Prefer the first, wait a little for it, and fall back to the second.# Prints "<file> <url>" lines.node -e ' const files = JSON.parse(require("fs").readFileSync(".release.json", "utf8")); const deadline = Date.now() + 5 * 60_000; (async () => { for (const entry of files) { let url = entry.blobUrl; for (;;) { // Only the file itself, at its size, counts as served. const response = await fetch(entry.url).catch(() => undefined); const bytes = response?.ok ? Buffer.from(await response.arrayBuffer()) : undefined; const head = bytes?.subarray(0, 2).toString(); if (bytes?.length === entry.size && (head === "#!" || head === "PK")) { url = entry.url; break; } if (Date.now() > deadline) break; await new Promise((resolve) => setTimeout(resolve, 10_000)); } console.log(`${entry.name} ${url}`); } })();' > .release-urls
with_backoff $BUNDLE attest --kind published --note "davsync $TAG" build/davsync.nzip
with_backoff $BUNDLE publish --as "$BUNDLE_ATPROTO_IDENTIFIER" --title davsync \ --description "$(node -p 'require("./package.json").description')" \ davsync "$(sed -n 's#^davsync.nzip ##p' .release-urls)"
echo "* released $TAG:"echo " bundle install @$BUNDLE_ATPROTO_IDENTIFIER/davsync"