feat: publish the exact @ekklesia package train to the npm registry (ADR 0053)
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]>
This commit is contained in:
ekklesia
2026-10-08 09:49:27 -05:00
co-authored by Claude Opus 5.5
parent 57df2e15f2
commit d3c62e63bf
17 changed files with 2393 additions and 33 deletions
+136
View File
@@ -0,0 +1,136 @@
#!/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."