#!/usr/bin/env bash # The package train publication proof (ADR 0053), against a disposable Gitea of the same version as # the real registry, never the real one: the publication tool publishes one exact release train, # and the suite proves the round trip, partial-train recovery, the hard stop on a burned version, # dist-tag convergence, and that the registry's public owner serves packages anonymously while only # the publisher team may publish. # # bash scripts/test-package-publication.sh [--release ] # # With --release, it publishes that release (CI hands it the package-release job's artifact). # Without, it builds one from a worktree of HEAD, so local changes to the tool are tested against a # release of the committed packages. set -Eeuo pipefail repository_root="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")/.." && pwd)" cd "$repository_root" # shellcheck source=scripts/lib/compose-job-network.sh source "$repository_root/scripts/lib/compose-job-network.sh" # shellcheck source=scripts/lib/compose-diagnostics.sh source "$repository_root/scripts/lib/compose-diagnostics.sh" # shellcheck source=scripts/lib/ci-project.sh source "$repository_root/scripts/lib/ci-project.sh" fail() { echo "package publication proof failed: $*" >&2 exit 1 } release="" while (($#)); do case "$1" in --release) release="${2:-}" shift 2 || fail "--release needs a directory" ;; *) fail "usage: test-package-publication.sh [--release ]" ;; esac done command -v docker >/dev/null || fail "Docker is required" docker compose version >/dev/null 2>&1 || fail "Docker Compose is required" compose_job_network_initialize || fail "the current Docker job container could not be identified" project="$(ci_project_name package-registry)" work="$(mktemp -d "${TMPDIR:-/tmp}/ekklesia-package-publication-XXXXXX")" worktree="$work/release-checkout" if [[ "$COMPOSE_JOB_NETWORK_RUNNING_IN_CONTAINER" == "true" ]]; then export GITEA_PORT="0" GITEA_ROOT_URL="http://gitea:3000/" else GITEA_PORT="$(node -e 'const s = require("net").createServer().listen(0, "127.0.0.1", () => { console.log(s.address().port); s.close(); })')" export GITEA_PORT GITEA_ROOT_URL="http://127.0.0.1:$GITEA_PORT/" fi compose=(docker compose --project-name "$project" --file scripts/lib/package-registry.compose.yaml) cleanup() { local exit_status=$? trap - EXIT INT TERM compose_job_network_disconnect || exit_status=1 "${compose[@]}" down --volumes --remove-orphans --timeout 10 >/dev/null 2>&1 || exit_status=1 if [[ -d "$worktree" ]]; then git -C "$repository_root" worktree remove --force "$worktree" >/dev/null 2>&1 || true fi rm -rf "$work" exit "$exit_status" } trap cleanup EXIT trap 'exit 130' INT trap 'exit 143' TERM if [[ -z "$release" ]]; then echo "==> Building a release of HEAD to publish" store="$(dirname "$(pnpm store path)")" git worktree add --detach --quiet "$worktree" HEAD (cd "$worktree" && pnpm install --offline --frozen-lockfile --store-dir "$store" --reporter=silent) node "$worktree/scripts/package-release.ts" --out "$work/release" >/dev/null release="$(find "$work/release" -mindepth 1 -maxdepth 1 -type d)" fi release="$(cd "$release" && pwd)" # Masked in any Compose diagnostics below. COMPOSE_DIAGNOSTICS_SECRETS=() echo "==> Starting a disposable Gitea 1.27.2" "${compose[@]}" config --quiet || fail "the disposable registry's configuration is invalid" if ! up_output="$("${compose[@]}" up --detach --wait --wait-timeout 180 gitea 2>&1)"; then compose_report_not_ready "$up_output" "${compose[@]}" fail "the disposable Gitea did not become ready" fi if [[ "$COMPOSE_JOB_NETWORK_RUNNING_IN_CONTAINER" == "true" ]]; then compose_job_network_connect gitea "${compose[@]}" || fail "the job could not join the registry's network" fi origin="${GITEA_ROOT_URL%/}" admin_password="admin-$RANDOM$RANDOM$RANDOM-$RANDOM" publisher_password="publisher-$RANDOM$RANDOM$RANDOM-$RANDOM" outsider_password="outsider-$RANDOM$RANDOM$RANDOM-$RANDOM" "${compose[@]}" exec -T -u git gitea gitea admin user create --admin --username ekklesia-admin \ --password "$admin_password" --email admin@example.invalid --must-change-password=false >/dev/null api() { local method="$1" path="$2" body="${3:-}" auth="${4:-ekklesia-admin:$admin_password}" curl -fsS -X "$method" -u "$auth" -H "Content-Type: application/json" \ ${body:+--data "$body"} "$origin/api/v1$path" } echo "==> Creating public package owners, the publisher team, and an outsider" for user in publisher outsider; do password_variable="${user}_password" api POST /admin/users "{\"username\":\"$user\",\"password\":\"${!password_variable}\",\"email\":\"$user@example.invalid\",\"must_change_password\":false}" >/dev/null done owners=(River_of_Glory_Church ekklesia-burned-version ekklesia-tag-retry) for owner in "${owners[@]}"; do api POST /orgs "{\"username\":\"$owner\",\"visibility\":\"public\"}" >/dev/null team="$(api POST "/orgs/$owner/teams" '{"name":"package-publishers","includes_all_repositories":false,"can_create_org_repo":false,"units":["repo.packages"],"units_map":{"repo.packages":"write"}}' | node -e 'process.stdout.write(String(JSON.parse(require("fs").readFileSync(0, "utf8")).id))')" api PUT "/teams/$team/members/publisher" >/dev/null done token_for() { local user="$1" password="$2" api POST "/users/$user/tokens" "{\"name\":\"publish-$RANDOM\",\"scopes\":[\"write:package\"]}" "$user:$password" | node -e 'process.stdout.write(JSON.parse(require("fs").readFileSync(0, "utf8")).sha1)' } publish_token="$(token_for publisher "$publisher_password")" outsider_token="$(token_for outsider "$outsider_password")" echo "==> Publishing the release train to it" EKKLESIA_TEST_REGISTRY_ORIGIN="$origin" \ EKKLESIA_TEST_RELEASE="$release" \ EKKLESIA_TEST_PUBLISH_TOKEN="$publish_token" \ EKKLESIA_TEST_OUTSIDER_TOKEN="$outsider_token" \ EKKLESIA_TEST_OWNERS="${owners[*]}" \ pnpm exec vitest run --config vitest.packages.integration.config.ts echo "Package publication proof passed."