#!/usr/bin/env ysh # SPDX-FileCopyrightText: Amolith # # SPDX-License-Identifier: CC0-1.0 const collection = 'sh.tangled.repo.artifact' const maximum_blob_size = 50 * 1024 * 1024 const maximum_scan_limit = 10000 const go = $(mise which go) const upx = $(mise which upx) const targets = [ {os: 'linux', arch: 'amd64', extension: '', pack: true, static: true}, {os: 'linux', arch: 'arm64', extension: '', pack: true, static: true}, {os: 'darwin', arch: 'amd64', extension: '', pack: false, static: false}, {os: 'darwin', arch: 'arm64', extension: '', pack: false, static: false}, {os: 'windows', arch: 'amd64', extension: '.exe', pack: false, static: false}, {os: 'freebsd', arch: 'amd64', extension: '', pack: false, static: true}, ] var build_only = false var tag = '' if (len(ARGV) === 2 and ARGV[0] === '--build-only') { setvar build_only = true setvar tag = ARGV[1] } elif (len(ARGV) !== 0) { echo "Usage: $0 [--build-only TAG]" >&2 exit 2 } proc build_artifacts (staging, release_tag) { install -d -m 0755 "$staging" for target in (targets) { var suffix = "$[target.os]-$[target.arch]$[target.extension]" var artifact = "$staging/git-format-$release_tag-$suffix" var ldflags = "-s -w -buildid= -X main.version=$release_tag" if (target.static) { setvar ldflags = "-d $ldflags -extldflags=-static" } echo "Building $[target.os]/$[target.arch] ..." env \ CGO_ENABLED=0 \ GOOS="$[target.os]" \ GOARCH="$[target.arch]" \ GOTOOLCHAIN=go1.27.0 \ "$go" build \ -trimpath \ -tags 'netgo,osusergo,static_build' \ -ldflags "$ldflags" \ -o "$artifact" \ . if (target.pack) { echo "Packing $[target.os]/$[target.arch] ..." "$upx" --best --lzma --quiet "$artifact" "$upx" --test --quiet "$artifact" } } } proc verify_artifacts (staging, release_tag) { for target in (targets) { var artifact = "$staging/git-format-$release_tag-$[target.os]-$[target.arch]$[target.extension]" if ! test -s "$artifact" { echo "Expected artifact is missing or empty: $artifact" >&2 exit 1 } var size = int($(wc -c < "$artifact")) if (size > maximum_blob_size) { echo "Artifact exceeds the 50 MiB lexicon limit: $artifact" >&2 exit 1 } } } if (build_only) { if (tag !~ '^v.+$') { echo "Build-only tag must start with v: $tag" >&2 exit 2 } rm -rf -- dist build_artifacts dist "$tag" verify_artifacts dist "$tag" echo "Built $[len(targets)] release artifacts for $tag in dist/" exit 0 } const pds = get(ENV, 'GIT_FORMAT_RELEASE_PDS', 'https://sequeste.red') const identifier = get(ENV, 'GIT_FORMAT_RELEASE_IDENTIFIER', 'secluded.site') const password_file = get(ENV, 'GIT_FORMAT_RELEASE_APP_PASSWORD_FILE', '') const scan_limit_text = get(ENV, 'GIT_FORMAT_RELEASE_RECORD_SCAN_LIMIT', '100') if (password_file === '') { echo 'GIT_FORMAT_RELEASE_APP_PASSWORD_FILE is required' >&2 exit 1 } if ! test -s "$password_file" { echo "App password file is missing or empty: $password_file" >&2 exit 1 } if (scan_limit_text !~ '^[0-9]+$') { echo 'GIT_FORMAT_RELEASE_RECORD_SCAN_LIMIT must be a positive integer' >&2 exit 1 } const scan_limit = int(scan_limit_text) if (scan_limit < 1 or scan_limit > maximum_scan_limit) { echo "GIT_FORMAT_RELEASE_RECORD_SCAN_LIMIT must be between 1 and $maximum_scan_limit" >&2 exit 1 } if (pds !~ '^https://') { echo 'GIT_FORMAT_RELEASE_PDS must use HTTPS' >&2 exit 1 } if (pds ~ '/$') { echo 'GIT_FORMAT_RELEASE_PDS must not end with a slash' >&2 exit 1 } const required_environment = [ 'TANGLED_PIPELINE_KIND', 'TANGLED_REF', 'TANGLED_REF_NAME', 'TANGLED_REF_TYPE', 'TANGLED_SHA', 'TANGLED_REPO_DID', 'TANGLED_REPO_REPO_DID', ] for name in (required_environment) { if (get(ENV, name, '') === '') { echo "$name is required" >&2 exit 1 } } setvar tag = ENV.TANGLED_REF_NAME if (ENV.TANGLED_PIPELINE_KIND !== 'push') { echo 'Releases require a push pipeline' >&2 exit 1 } if (ENV.TANGLED_REF_TYPE !== 'tag') { echo 'Releases require a tag ref' >&2 exit 1 } if (ENV.TANGLED_REF !== "refs/tags/$tag") { echo "TANGLED_REF does not match TANGLED_REF_NAME: $[ENV.TANGLED_REF]" >&2 exit 1 } if (tag !~ '^v.+$') { echo "Release tag must start with v: $tag" >&2 exit 1 } const work = $(mktemp -d) trap --add EXIT { rm -rf -- "$work" } chmod 0700 "$work" const tag_object = $(git rev-parse --verify "$tag^{tag}") if (tag_object !== ENV.TANGLED_SHA) { echo 'The pushed tag object does not match TANGLED_SHA' >&2 exit 1 } const release_commit = $(git rev-parse --verify "$tag^{}") const checkout_commit = $(git rev-parse --verify HEAD) if (release_commit !== checkout_commit) { echo 'The annotated tag does not resolve to the checked-out commit' >&2 exit 1 } const tag_bytes = $(printf '%s' "$tag_object" | xxd -r -p | base64 -w 0 | tr -d '=') if (len(tag_bytes) !== 27) { echo 'The annotated tag object ID is not a 20-byte SHA-1 value' >&2 exit 1 } const auth_config = "$work/curl-auth.conf" const password = $(cat -- "$password_file") var session_request = { identifier: identifier, password: password, } json write (session_request) > "$work/session-request.json" chmod 0600 "$work/session-request.json" const curl_arguments = [ '--silent', '--show-error', '--fail-with-body', '--connect-timeout', '10', '--max-time', '120', ] echo 'Authenticating to the release PDS ...' curl @curl_arguments \ --request POST \ --header 'Content-Type: application/json' \ --data-binary "@$work/session-request.json" \ "$pds/xrpc/com.atproto.server.createSession" \ --output "$work/session.json" var session = null try { json read (&session) < "$work/session.json" 2>/dev/null } if (_status !== 0) { echo 'PDS returned an invalid session response' >&2 exit 1 } const publisher_did = session.did const access_token = session.accessJwt if (publisher_did !== ENV.TANGLED_REPO_DID) { echo 'Authenticated PDS account does not match TANGLED_REPO_DID' >&2 exit 1 } printf 'header = "Authorization: Bearer %s"\n' "$access_token" > "$auth_config" chmod 0600 "$auth_config" proc scan_release (output_path) { var matches = [] var cursor = '' var scanned = 0 while (scanned < scan_limit) { var page_size = scan_limit - scanned if (page_size > 100) { setvar page_size = 100 } var cursor_arguments = [] if (cursor !== '') { setvar cursor_arguments = ['--data-urlencode', "cursor=$cursor"] } curl @curl_arguments \ --retry 2 \ --retry-all-errors \ --get \ --data-urlencode "repo=$publisher_did" \ --data-urlencode "collection=$collection" \ --data-urlencode "limit=$page_size" \ @cursor_arguments \ "$pds/xrpc/com.atproto.repo.listRecords" \ --output "$work/records-page.json" var page = null json read (&page) < "$work/records-page.json" for record in (page.records) { setvar scanned += 1 var value = record.value var record_tag = get(value, 'tag', {}) var record_tag_bytes = get(record_tag, '$bytes', '') if ( get(value, 'repoDid', '') === ENV.TANGLED_REPO_REPO_DID and (record_tag_bytes === tag_bytes or record_tag_bytes === "$tag_bytes=") ) { call matches->append(record) } } if (len(page.records) === 0 or scanned >= scan_limit) { break } setvar cursor = get(page, 'cursor', '') if (cursor === '') { break } } json write (matches) > "$output_path" } echo "Checking the newest $scan_limit artifact records for $tag ..." scan_release "$work/preflight-records.json" var preflight_records = null json read (&preflight_records) < "$work/preflight-records.json" if (len(preflight_records) !== 0) { echo "This release already has published artifacts in the newest $scan_limit records" >&2 echo 'Create and push a new tag instead of overwriting a release' >&2 exit 1 } const staging = "$work/dist" build_artifacts "$staging" "$tag" verify_artifacts "$staging" "$tag" var records = [] const created_at = $(date --utc '+%Y-%m-%dT%H:%M:%SZ') for target in (targets) { const name = "git-format-$tag-$[target.os]-$[target.arch]$[target.extension]" const artifact = "$staging/$name" const size = int($(wc -c < "$artifact")) echo "Uploading $name ..." curl @curl_arguments \ --config "$auth_config" \ --request POST \ --header 'Content-Type: application/octet-stream' \ --data-binary "@$artifact" \ "$pds/xrpc/com.atproto.repo.uploadBlob" \ --output "$work/blob.json" var upload = null json read (&upload) < "$work/blob.json" if (upload.blob.size !== size) { echo "PDS returned invalid blob metadata for $name" >&2 exit 1 } call records->append({ '$type': collection, name: name, repoDid: ENV.TANGLED_REPO_REPO_DID, tag: {'$bytes': tag_bytes}, createdAt: created_at, artifact: upload.blob, }) } curl @curl_arguments \ --retry 2 \ --retry-all-errors \ --get \ --data-urlencode "did=$publisher_did" \ "$pds/xrpc/com.atproto.sync.getLatestCommit" \ --output "$work/latest-commit.json" var latest_commit = null json read (&latest_commit) < "$work/latest-commit.json" # Read the commit before rechecking so swapCommit rejects any concurrent write # between this point and applyWrites. echo 'Rechecking publication state before creating records ...' scan_release "$work/prewrite-records.json" var prewrite_records = null json read (&prewrite_records) < "$work/prewrite-records.json" if (len(prewrite_records) !== 0) { echo 'Another pipeline published this release while artifacts were being prepared' >&2 echo 'Create and push a new tag instead of overwriting a release' >&2 exit 1 } var writes = [] for record in (records) { call writes->append({ '$type': 'com.atproto.repo.applyWrites#create', collection: collection, value: record, }) } var apply_request = { repo: publisher_did, swapCommit: latest_commit.cid, writes: writes, } json write (apply_request) > "$work/apply-request.json" echo 'Publishing artifact records ...' var http_status = '' try { setvar http_status = $(curl @curl_arguments \ --config "$auth_config" \ --request POST \ --header 'Content-Type: application/json' \ --data-binary "@$work/apply-request.json" \ --output "$work/apply-response.json" \ --write-out '%{http_code}' \ "$pds/xrpc/com.atproto.repo.applyWrites") } const apply_curl_status = _status # A response can be lost after the PDS commits. Reconcile before deciding # whether publication failed; never blindly repeat applyWrites. echo 'Confirming published artifact records ...' scan_release "$work/published-records.json" var published_records = null json read (&published_records) < "$work/published-records.json" var exact_publication = len(published_records) === len(records) for expected in (records) { var match_count = 0 for published in (published_records) { var value = published.value var value_tag = get(value, 'tag', {}) var value_tag_bytes = get(value_tag, '$bytes', '') if (value_tag_bytes === "$tag_bytes=") { setvar value.tag = {'$bytes': tag_bytes} } if (value === expected) { setvar match_count += 1 } } if (match_count !== 1) { setvar exact_publication = false } } if (exact_publication) { echo "Published $tag with $[len(records)] artifacts" exit 0 } if (len(published_records) !== 0) { echo 'Publication left an ambiguous or partial release state' >&2 echo 'Inspect the PDS records and create a new tag before retrying' >&2 exit 1 } if (apply_curl_status !== 0 or http_status !~ '^2[0-9][0-9]$') { echo "PDS did not publish the release (curl status $apply_curl_status, HTTP $http_status)" >&2 echo 'No matching artifact records were found; this tag may be retried after diagnosing the failure' >&2 exit 1 } echo 'PDS accepted applyWrites but the expected records were not found' >&2 exit 1