CI / verify (pull_request) Successful in 1m1s
CI / infrastructure-smoke (pull_request) Successful in 35s
CI / migration-smoke (pull_request) Successful in 1m53s
CI / package-release (pull_request) Failing after 43s
CI / package-publication (pull_request) Skipped
CI / image-build (pull_request) Successful in 29s
CI / deploy-smoke (split) (pull_request) Canceled after 0s
CI / deploy-smoke (same-origin) (pull_request) Canceled after 38s
Publishes one package release train to the Gitea npm registry under River_of_Glory_Church, whose packages are publicly readable on purpose: exactly the .tgz files the package-release gate proved, and nothing else. Nothing is published by this change. - scripts/publish-package-train.ts validates the whole local train before any request (gate manifest hash, version, revision, unmodified source, the nine packages, every artifact's size, SHA-256, integrity, SHA256SUMS line, and inner package.json), then publishes in dependency order with `npm publish <file.tgz>` under a staging tag. Each package is reconciled first (absent: publish; same bytes: accept; other bytes: hard stop, version burned), then read back: metadata (name, version, integrity, dependency maps, normalized bin, tarball on the registry) and the tarball itself, byte for byte, also anonymously. `latest` moves only after all nine verify; reruns converge and never delete anything. It writes a publication record. The token comes from EKKLESIA_PACKAGES_PUBLISH_TOKEN only, through a temporary npm config that refers to the variable. - scripts/test-package-publication.sh proves it against a disposable Gitea 1.27.2 with public owners, a publisher team and an outsider: anonymous reads, refused unauthorized publishing, byte-identical round trips, the registry's own 409, partial-train recovery, the burned version hard stop, and dist-tag retry convergence. - CI: package-release keeps its proven release and hands it on as an artifact with its manifest hash; the new package-publication job publishes exactly that artifact to the disposable Gitea. Artifact actions v3 (node20): v4 refuses any server other than github.com. - .github/workflows/publish-packages.yml publishes to the real registry only by manual dispatch on main, after its own gate run, with the secret in the publish step alone. - docs: the public-read model, the workflow, the record, credentials, immutability policy, provider moves, and the first-publication steps. Co-Authored-By: Claude Opus 5.5 <[email protected]>
137 lines
5.9 KiB
Bash
Executable File
137 lines
5.9 KiB
Bash
Executable File
#!/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 <release directory>]
|
|
#
|
|
# 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 <release directory>]" ;;
|
|
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 [email protected] --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."
|