diff --git a/.dockerignore b/.dockerignore index 6bd3581..6677b90 100644 --- a/.dockerignore +++ b/.dockerignore @@ -2,6 +2,7 @@ .direnv .env .relay-owner.nsec +artifacts data result result-* diff --git a/.gitignore b/.gitignore index 4aff86f..7fa3c5c 100644 --- a/.gitignore +++ b/.gitignore @@ -4,6 +4,9 @@ # Rust build artifacts (single workspace target at repo root) target/ +# Generated OCI release layouts +/artifacts/ + # Working directory (session-specific temporary files) work/* !work/README.md diff --git a/.ngit/act/workflows/container_backfill.yaml b/.ngit/act/workflows/container_backfill.yaml new file mode 100644 index 0000000..ad24bd1 --- /dev/null +++ b/.ngit/act/workflows/container_backfill.yaml @@ -0,0 +1,90 @@ +on: + workflow_dispatch: + +name: container release backfill + +jobs: + linux-x86_64: + runs-on: ubuntu-latest + timeout-minutes: 60 + steps: + - uses: actions/checkout@v5 + - uses: danconwaydev/setup-ngit@v3 + - uses: cachix/install-nix-action@v31 + with: + nix_path: nixpkgs=channel:nixos-unstable + - name: Require the disposable guest container daemon + shell: bash + run: | + if [[ ! -S /var/run/docker.sock ]]; then + echo "container daemon socket is not available" >&2 + echo "this workflow requires an ngit-ci operator opt-in" >&2 + exit 1 + fi + - name: Build exact release OCI layout + shell: bash + run: | + set -euo pipefail + + case "$GITHUB_REF" in + refs/tags/v*) ;; + *) + echo "manual container publication requires a refs/tags/v* context" >&2 + exit 1 + ;; + esac + + version="${GITHUB_REF#refs/tags/v}" + if [[ -z "$version" || "$version" == *[!A-Za-z0-9._-]* ]]; then + echo "unsupported release version from tag: $GITHUB_REF" >&2 + exit 1 + fi + + git fetch --depth=1 --no-tags \ + https://ngit.dev/ngit-grasp.git "$GITHUB_REF" + revision="$(git rev-parse 'FETCH_HEAD^{commit}')" + source_directory="$RUNNER_TEMP/ngit-grasp-${revision}" + mkdir "$source_directory" + git archive FETCH_HEAD | tar -xf - -C "$source_directory" + + source_version="$(nix eval --raw "path:${source_directory}#static.version")" + if [[ "$version" != "$source_version" ]]; then + echo "tag version $version does not match package version $source_version" >&2 + exit 1 + fi + + layout="$PWD/artifacts/ngit-grasp" + CONTAINER_CONTEXT="$source_directory" \ + CONTAINER_EXACT_ONLY=true \ + nix shell nixpkgs#docker-client nixpkgs#jq nixpkgs#skopeo \ + --command scripts/build-container-layout.sh \ + "$version" "$revision" "$layout" + - name: Publish exact container tag + shell: bash + env: + NGIT_PUBLISHER_NBUNKSEC: ${{ secrets.NGIT_PUBLISHER_NBUNKSEC }} + run: | + set -euo pipefail + + : "${NGIT_PUBLISHER_NBUNKSEC:?Set the NGIT_PUBLISHER_NBUNKSEC repository secret}" + + umask 077 + signer_file="$RUNNER_TEMP/ngit-container-nbunksec" + trap 'rm -f "$signer_file"' EXIT + printf '%s\n' "$NGIT_PUBLISHER_NBUNKSEC" > "$signer_file" + + ngit container publish ngit-grasp \ + --repo nostr://npub15qydau2hjma6ngxkl2cyar74wzyjshvl65za5k5rl69264ar2exs5cyejr/relay.ngit.dev/ngit-grasp \ + --manifest .ngit/containers.yaml \ + --nbunksec-file "$signer_file" \ + --defaults \ + --repo-relay-only \ + --json + - name: Pull and verify the published image + shell: bash + run: | + set -euo pipefail + + version="${GITHUB_REF#refs/tags/v}" + nix shell nixpkgs#docker-client \ + --command scripts/verify-published-container.sh "$version" diff --git a/.ngit/act/workflows/deployment_e2e.yaml b/.ngit/act/workflows/deployment_e2e.yaml index 7eff82d..49c4691 100644 --- a/.ngit/act/workflows/deployment_e2e.yaml +++ b/.ngit/act/workflows/deployment_e2e.yaml @@ -10,7 +10,9 @@ on: - "build.rs" - "compose*.yaml" - "deploy/**" + - "scripts/build-container-layout.sh" - "scripts/test-container-deployment.sh" + - "scripts/test-container-release.sh" - "src/**" pull_request: @@ -32,7 +34,12 @@ jobs: echo "this workflow requires an ngit-ci operator opt-in" >&2 exit 1 fi - - name: Build, replace, and verify the container + - name: Build, import, replace, and verify the OCI release layout + shell: bash run: | - nix shell nixpkgs#docker-client nixpkgs#curl nixpkgs#jq \ - --command scripts/test-container-deployment.sh + set -euo pipefail + + version="$(nix eval --raw .#static.version)" + revision="$(git rev-parse 'HEAD^{commit}')" + nix shell nixpkgs#docker-client nixpkgs#curl nixpkgs#jq nixpkgs#skopeo \ + --command scripts/test-container-release.sh "$version" "$revision" diff --git a/.ngit/act/workflows/release.yaml b/.ngit/act/workflows/release.yaml index 521ff61..c38e7f9 100644 --- a/.ngit/act/workflows/release.yaml +++ b/.ngit/act/workflows/release.yaml @@ -14,6 +14,14 @@ jobs: - uses: cachix/install-nix-action@v31 with: nix_path: nixpkgs=channel:nixos-unstable + - name: Require the disposable guest container daemon + shell: bash + run: | + if [[ ! -S /var/run/docker.sock ]]; then + echo "container daemon socket is not available" >&2 + echo "this workflow requires an ngit-ci operator opt-in" >&2 + exit 1 + fi - name: Restore Nix store cache continue-on-error: true uses: nix-community/cache-nix-action@v7 @@ -53,12 +61,50 @@ jobs: tar --sort=name --mtime="@${source_date_epoch}" \ --owner=0 --group=0 --numeric-owner -C release-stage \ -czf "dist/${archive}.tar.gz" "$archive" + - name: Build OCI image layout + shell: bash + run: | + set -euo pipefail + + version="${GITHUB_REF_NAME#v}" + revision="$(git rev-parse "${GITHUB_REF_NAME}^{commit}")" + nix shell nixpkgs#docker-client nixpkgs#jq nixpkgs#skopeo \ + --command scripts/build-container-layout.sh "$version" "$revision" - name: Upload release assets uses: actions/upload-artifact@v4 with: name: ngit-grasp-release-assets path: dist/*.tar.gz if-no-files-found: error + - name: Publish OCI container + shell: bash + env: + NGIT_PUBLISHER_NBUNKSEC: ${{ secrets.NGIT_PUBLISHER_NBUNKSEC }} + run: | + set -euo pipefail + + : "${NGIT_PUBLISHER_NBUNKSEC:?Set the NGIT_PUBLISHER_NBUNKSEC repository secret}" + + umask 077 + signer_file="$RUNNER_TEMP/ngit-container-nbunksec" + trap 'rm -f "$signer_file"' EXIT + printf '%s\n' "$NGIT_PUBLISHER_NBUNKSEC" > "$signer_file" + + ngit container publish ngit-grasp \ + --repo nostr://npub15qydau2hjma6ngxkl2cyar74wzyjshvl65za5k5rl69264ar2exs5cyejr/relay.ngit.dev/ngit-grasp \ + --manifest .ngit/containers.yaml \ + --nbunksec-file "$signer_file" \ + --defaults \ + --repo-relay-only \ + --json + - name: Pull and verify the published image + shell: bash + run: | + set -euo pipefail + + version="${GITHUB_REF_NAME#v}" + nix shell nixpkgs#docker-client \ + --command scripts/verify-published-container.sh "$version" - name: Publish NIP-82 release shell: bash env: diff --git a/.ngit/containers.yaml b/.ngit/containers.yaml new file mode 100644 index 0000000..272152e --- /dev/null +++ b/.ngit/containers.yaml @@ -0,0 +1,17 @@ +schema: 1 +publication: + blossom_servers: + - https://blossom.primal.net + - https://blossom.ditto.pub + - https://haven.danconwaydev.com + relays: + - wss://relay.zapstore.dev + - wss://relay.ditto.pub + - wss://relay.dreamith.to + - wss://relay.primal.net +containers: + ngit-grasp: + layout: artifacts/ngit-grasp + title: ngit-grasp + description: A self-hostable Nostr relay and Git server for decentralized repositories. + source: https://ngit.dev/ngit-grasp.git diff --git a/CHANGELOG.md b/CHANGELOG.md index efaf27b..191bb90 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,13 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### Changed + +- Publish release container images as Nostr kind-30624 repositories backed by + redundant Blossom storage. Stable images receive version and `latest` tags; + prereleases receive version and channel tags. Maintainers can backfill an + exact prior-release tag through CI without moving a stable channel backwards. + ## [3.0.1] - 2026-08-29 ### Fixed diff --git a/Dockerfile b/Dockerfile index cbe1004..8c1339e 100644 --- a/Dockerfile +++ b/Dockerfile @@ -39,7 +39,7 @@ ARG NGIT_IMAGE_REVISION=unknown LABEL org.opencontainers.image.title="ngit-grasp" \ org.opencontainers.image.description="GRASP relay and Git Smart HTTP server" \ org.opencontainers.image.licenses="MIT" \ - org.opencontainers.image.source="https://gitnostr.com/npub15qydau2hjma6ngxkl2cyar74wzyjshvl65za5k5rl69264ar2exs5cyejr/ngit-grasp.git" \ + org.opencontainers.image.source="https://ngit.dev/ngit-grasp.git" \ org.opencontainers.image.version="${NGIT_IMAGE_VERSION}" \ org.opencontainers.image.revision="${NGIT_IMAGE_REVISION}" diff --git a/README.md b/README.md index c537b07..8dfb873 100644 --- a/README.md +++ b/README.md @@ -417,10 +417,15 @@ The shortest fresh-VPS path uses Docker Compose and Caddy: ```bash cp deploy.env.example .env # Set NGIT_DOMAIN in .env and point DNS at this server. -docker compose -f compose.yaml -f compose.caddy.yaml up --build -d +export NGIT_IMAGE="ncontainer.io/npub15qydau2hjma6ngxkl2cyar74wzyjshvl65za5k5rl69264ar2exs5cyejr/ngit-grasp:latest" +docker compose -f compose.yaml -f compose.caddy.yaml pull ngit-grasp +docker compose -f compose.yaml -f compose.caddy.yaml up --no-build -d scripts/verify-deployment.sh https://ngit.example.com ``` +Pin an explicit release version instead of `latest` for reproducible +production deployments. The Docker guide also covers source builds. + For development from source: ```bash diff --git a/docs/how-to/deploy-docker.md b/docs/how-to/deploy-docker.md index a1ab3e6..93da553 100644 --- a/docs/how-to/deploy-docker.md +++ b/docs/how-to/deploy-docker.md @@ -7,6 +7,17 @@ Caddy overlay for automatic HTTPS. The image contains ngit-grasp, Git, CA certificates, and a small init process. It prepares `/data` and then runs ngit-grasp as UID/GID `10001`. +Stable release images are published through Nostr, with their OCI blobs stored +on Blossom. Docker and Podman can pull them through the ncontainer gateway: + +```bash +docker pull ncontainer.io/npub15qydau2hjma6ngxkl2cyar74wzyjshvl65za5k5rl69264ar2exs5cyejr/ngit-grasp:latest +``` + +`latest` tracks the newest stable release. Replace it with an explicit release +version for a reproducible deployment. Release candidates are also published +under their prerelease channel, such as `rc`. + ## Prerequisites - Docker Engine with Compose v2, or a compatible Podman Compose setup @@ -28,7 +39,9 @@ cp deploy.env.example .env Set `NGIT_DOMAIN` in `.env`, then start the relay and Caddy: ```bash -docker compose -f compose.yaml -f compose.caddy.yaml up --build -d +export NGIT_IMAGE="ncontainer.io/npub15qydau2hjma6ngxkl2cyar74wzyjshvl65za5k5rl69264ar2exs5cyejr/ngit-grasp:latest" +docker compose -f compose.yaml -f compose.caddy.yaml pull ngit-grasp +docker compose -f compose.yaml -f compose.caddy.yaml up --no-build -d docker compose -f compose.yaml -f compose.caddy.yaml ps scripts/verify-deployment.sh https://ngit.example.com ``` @@ -46,10 +59,15 @@ its path routing explicitly. Start only the relay service: ```bash -docker compose up --build -d +export NGIT_IMAGE="ncontainer.io/npub15qydau2hjma6ngxkl2cyar74wzyjshvl65za5k5rl69264ar2exs5cyejr/ngit-grasp:latest" +docker compose pull ngit-grasp +docker compose up --no-build -d curl -H 'Accept: application/nostr+json' http://127.0.0.1:7334 ``` +To build a checked-out source revision instead, leave `NGIT_IMAGE` unset and +use `docker compose up --build -d` with the same Compose file selection. + Proxy the public HTTPS hostname to `http://127.0.0.1:7334`. Preserve WebSocket upgrades, request methods, bodies, and query strings. Forwarded client IP headers are ignored by default; configure `NGIT_TRUSTED_PROXY_CIDRS` only after @@ -109,16 +127,21 @@ non-public deployment before relying on it. ## Upgrade -Pin or check out the intended tag, read `CHANGELOG.md`, take a backup, and -rebuild without overlapping the old and new writers: +Read `CHANGELOG.md`, take a backup, select an explicit release tag, and replace +the container without overlapping the old and new writers: ```bash +export NGIT_IMAGE="ncontainer.io/npub15qydau2hjma6ngxkl2cyar74wzyjshvl65za5k5rl69264ar2exs5cyejr/ngit-grasp:3.0.2" +docker compose pull ngit-grasp docker compose stop ngit-grasp -docker compose build --pull ngit-grasp -docker compose up -d ngit-grasp +docker compose up --no-build -d ngit-grasp scripts/verify-deployment.sh https://ngit.example.com ``` +Replace `3.0.2` with the intended release. For a source-built deployment, +check out that tag and run `docker compose build --pull ngit-grasp` before +starting the service instead. + For storage-changing releases, follow the linked migration guide and restore the pre-upgrade volume snapshot before attempting a binary rollback. diff --git a/docs/how-to/deploy-paas.md b/docs/how-to/deploy-paas.md index 157568c..42a6aee 100644 --- a/docs/how-to/deploy-paas.md +++ b/docs/how-to/deploy-paas.md @@ -76,9 +76,18 @@ created. The fields follow Render's [persistent disk guide](https://render.com/docs/disks) describes the storage and scaling constraints. -Render Blueprints require a repository connected through one of Render's -supported Git providers. Until ngit-grasp has an authorized mirror or published -OCI image, use Render's **Public Git Repository** flow with the canonical URL: +The published image is available to provider workflows that accept a public +Docker-compatible registry reference: + +```text +ncontainer.io/npub15qydau2hjma6ngxkl2cyar74wzyjshvl65za5k5rl69264ar2exs5cyejr/ngit-grasp:latest +``` + +Pin an explicit release version instead of `latest` for repeatable production +deployments. The supplied `render.yaml` remains source-backed because Render +Blueprints require a repository connected through one of Render's supported +Git providers. Use Render's **Public Git Repository** flow with the canonical +URL: ```text https://gitnostr.com/npub15qydau2hjma6ngxkl2cyar74wzyjshvl65za5k5rl69264ar2exs5cyejr/ngit-grasp.git diff --git a/docs/how-to/deploy.md b/docs/how-to/deploy.md index ed864c6..36e619c 100644 --- a/docs/how-to/deploy.md +++ b/docs/how-to/deploy.md @@ -6,7 +6,7 @@ guide matching the host you already operate; every guide implements the same | Environment | Start here | Supplied artifact | | --- | --- | --- | -| Docker or Podman host | [Docker and Compose](deploy-docker.md) | `Dockerfile`, `compose.yaml`, optional Caddy overlay | +| Docker or Podman host | [Docker and Compose](deploy-docker.md) | Published OCI image, `compose.yaml`, optional Caddy overlay | | NixOS | [NixOS module](deploy-nixos.md) | `nixosModules.default` | | Debian, Ubuntu, or another systemd Linux | [Static binary and systemd](deploy-linux.md) | Static flake package and service unit | | Proxmox LXC or VM | [Proxmox](deploy-proxmox-lxc.md) | Direct systemd or Compose path | @@ -18,7 +18,9 @@ with the Caddy overlay: ```bash cp deploy.env.example .env # Set NGIT_DOMAIN in .env and point its DNS records at this server. -docker compose -f compose.yaml -f compose.caddy.yaml up --build -d +export NGIT_IMAGE="ncontainer.io/npub15qydau2hjma6ngxkl2cyar74wzyjshvl65za5k5rl69264ar2exs5cyejr/ngit-grasp:latest" +docker compose -f compose.yaml -f compose.caddy.yaml pull ngit-grasp +docker compose -f compose.yaml -f compose.caddy.yaml up --no-build -d scripts/verify-deployment.sh https://ngit.example.com ``` diff --git a/scripts/build-container-layout.sh b/scripts/build-container-layout.sh new file mode 100755 index 0000000..c4ccc7d --- /dev/null +++ b/scripts/build-container-layout.sh @@ -0,0 +1,174 @@ +#!/bin/sh +set -eu + +if [ "$#" -lt 2 ] || [ "$#" -gt 3 ]; then + echo "usage: $0 <40-character-git-revision> [layout]" >&2 + exit 2 +fi + +version=$1 +revision=$2 +script_dir=$(CDPATH='' cd -- "$(dirname -- "$0")" && pwd) +repository_root=$(CDPATH='' cd -- "${script_dir}/.." && pwd) +container_context=${CONTAINER_CONTEXT:-${repository_root}} +container_context=$(CDPATH='' cd -- "${container_context}" && pwd) +exact_only=${CONTAINER_EXACT_ONLY:-false} + +case "${exact_only}" in + true | false) ;; + *) + echo "CONTAINER_EXACT_ONLY must be true or false" >&2 + exit 2 + ;; +esac + +case "${version}" in + [A-Za-z0-9_]*) ;; + *) + echo "version is not a valid OCI tag: ${version}" >&2 + exit 2 + ;; +esac +case "${version}" in + *[!A-Za-z0-9._-]*) + echo "version is not a valid OCI tag: ${version}" >&2 + exit 2 + ;; +esac +if [ "${#version}" -gt 128 ]; then + echo "version is longer than the OCI tag limit: ${version}" >&2 + exit 2 +fi +case "${revision}" in + *[!0-9a-f]*) + echo "revision is not a lowercase hexadecimal Git object ID: ${revision}" >&2 + exit 2 + ;; +esac +if [ "${#revision}" -ne 40 ]; then + echo "revision is not a 40-character Git object ID: ${revision}" >&2 + exit 2 +fi + +layout=${3:-artifacts/ngit-grasp} +case "${layout}" in + "") + echo "layout path must not be empty" >&2 + exit 2 + ;; + /*) ;; + *) layout=${container_context}/${layout} ;; +esac +if [ -e "${layout}" ]; then + echo "refusing to replace existing OCI layout: ${layout}" >&2 + exit 2 +fi + +for required_command in docker du jq skopeo; do + if ! command -v "${required_command}" >/dev/null 2>&1; then + echo "missing required command: ${required_command}" >&2 + exit 2 + fi +done + +image_name=ngit-grasp-release-layout:${revision}-$$ +case "${image_name}" in + ngit-grasp-release-layout:[0-9a-f]*-[0-9]*) ;; + *) + echo "refusing to use unexpected temporary image name" >&2 + exit 2 + ;; +esac + +remove_image=false +cleanup() { + if [ "${remove_image}" = true ]; then + docker image rm --force "${image_name}" >/dev/null 2>&1 || true + fi +} +trap cleanup EXIT HUP INT TERM + +docker build \ + --pull \ + --platform linux/amd64 \ + --build-arg "NGIT_BUILD_REVISION=${revision}" \ + --build-arg "NGIT_IMAGE_REVISION=${revision}" \ + --build-arg "NGIT_IMAGE_VERSION=${version}" \ + --tag "${image_name}" \ + "${container_context}" +remove_image=true + +mkdir -p "$(dirname -- "${layout}")" +skopeo --insecure-policy copy --format oci \ + "docker-daemon:${image_name}" \ + "oci:${layout}:${version}" + +if [ "${exact_only}" = true ]; then + alias_tag=${version} +else + case "${version}" in + *-*) + prerelease=${version#*-} + alias_tag=${prerelease%%[.+]*} + ;; + *) alias_tag=latest ;; + esac +fi +case "${alias_tag}" in + [A-Za-z0-9_]*) ;; + *) + echo "derived channel is not a valid OCI tag: ${alias_tag}" >&2 + exit 2 + ;; +esac +case "${alias_tag}" in + *[!A-Za-z0-9_.-]*) + echo "derived channel is not a valid OCI tag: ${alias_tag}" >&2 + exit 2 + ;; +esac + +index=${layout}/index.json +temporary_index=${index}.tmp.$$ +jq --arg version "${version}" --arg alias "${alias_tag}" ' + if .schemaVersion != 2 then + error("OCI layout index schemaVersion must be 2") + elif (.manifests | length) != 1 then + error("OCI exporter must produce exactly one root manifest") + else + .manifests[0] as $image + | .manifests = ( + [($image | .annotations = ((.annotations // {}) + { + "org.opencontainers.image.ref.name": $version + }))] + + if $alias == $version then [] else [ + ($image | .annotations = ((.annotations // {}) + { + "org.opencontainers.image.ref.name": $alias + })) + ] end + ) + end +' "${index}" > "${temporary_index}" +mv "${temporary_index}" "${index}" + +jq -e --arg version "${version}" --arg alias "${alias_tag}" ' + [.manifests[].annotations["org.opencontainers.image.ref.name"]] + == (if $version == $alias then [$version] else [$version, $alias] end) +' "${index}" >/dev/null + +layout_measure=$(du --apparent-size --block-size=1 --summarize -- "${layout}") +layout_bytes=${layout_measure%%[!0-9]*} +case "${layout_bytes}" in + '' | *[!0-9]*) + echo "failed to measure OCI layout: ${layout_measure}" >&2 + exit 1 + ;; +esac + +if [ "${version}" = "${alias_tag}" ]; then + printf 'OCI layout ready at %s with tag %s (%s apparent bytes)\n' \ + "${layout}" "${version}" "${layout_bytes}" +else + printf 'OCI layout ready at %s with tags %s and %s (%s apparent bytes)\n' \ + "${layout}" "${version}" "${alias_tag}" "${layout_bytes}" +fi diff --git a/scripts/test-container-deployment.sh b/scripts/test-container-deployment.sh index 8ec0bbb..e31704f 100755 --- a/scripts/test-container-deployment.sh +++ b/scripts/test-container-deployment.sh @@ -7,9 +7,11 @@ container_name=ngit-grasp-deployment-test-${test_suffix} volume_name=ngit-grasp-deployment-test-${test_suffix} if [ -n "${NGIT_TEST_IMAGE:-}" ]; then image_name=${NGIT_TEST_IMAGE} + build_test_image=false remove_test_image=false else image_name=ngit-grasp:deployment-test-${test_suffix} + build_test_image=true remove_test_image=true fi @@ -71,7 +73,9 @@ relay_pubkey() { "$1" | jq -er '.pubkey' } -"${container_engine}" build --tag "${image_name}" . +if [ "${build_test_image}" = true ]; then + "${container_engine}" build --tag "${image_name}" . +fi "${container_engine}" volume create "${volume_name}" >/dev/null start_container diff --git a/scripts/test-container-release.sh b/scripts/test-container-release.sh new file mode 100755 index 0000000..1c985b9 --- /dev/null +++ b/scripts/test-container-release.sh @@ -0,0 +1,81 @@ +#!/bin/sh +set -eu + +if [ "$#" -ne 2 ]; then + echo "usage: $0 <40-character-git-revision>" >&2 + exit 2 +fi + +version=$1 +revision=$2 +script_dir=$(CDPATH='' cd -- "$(dirname -- "$0")" && pwd) + +case "${revision}" in + *[!0-9a-f]*) + echo "revision is not a lowercase hexadecimal Git object ID: ${revision}" >&2 + exit 2 + ;; +esac +if [ "${#revision}" -ne 40 ]; then + echo "revision is not a 40-character Git object ID: ${revision}" >&2 + exit 2 +fi + +for required_command in docker jq skopeo; do + if ! command -v "${required_command}" >/dev/null 2>&1; then + echo "missing required command: ${required_command}" >&2 + exit 2 + fi +done + +temporary_parent=${RUNNER_TEMP:-${TMPDIR:-/tmp}} +temporary_parent=${temporary_parent%/} +case "${temporary_parent}" in + /*) ;; + *) + echo "temporary directory parent must be absolute: ${temporary_parent}" >&2 + exit 2 + ;; +esac +if [ ! -d "${temporary_parent}" ]; then + echo "temporary directory parent does not exist: ${temporary_parent}" >&2 + exit 2 +fi +test_directory=$(mktemp -d "${temporary_parent}/ngit-grasp-container-release-test.XXXXXX") +case "${test_directory}" in + "${temporary_parent}"/ngit-grasp-container-release-test.??????) ;; + *) + echo "refusing to use unexpected test directory: ${test_directory}" >&2 + exit 2 + ;; +esac + +layout=${test_directory}/layout +image_name=ngit-grasp:release-test-${revision}-$$ +case "${image_name}" in + ngit-grasp:release-test-[0-9a-f]*-[0-9]*) ;; + *) + echo "refusing to use unexpected test image name" >&2 + exit 2 + ;; +esac + +cleanup() { + docker image rm --force "${image_name}" >/dev/null 2>&1 || true + rm -rf -- "${test_directory}" +} +trap cleanup EXIT HUP INT TERM + +"${script_dir}/build-container-layout.sh" \ + "${version}" \ + "${revision}" \ + "${layout}" + +skopeo --insecure-policy copy --format v2s2 \ + "oci:${layout}:${version}" \ + "docker-daemon:${image_name}" + +NGIT_TEST_IMAGE=${image_name} \ + "${script_dir}/test-container-deployment.sh" + +printf 'OCI release layout and imported image verified for %s\n' "${version}" diff --git a/scripts/verify-published-container.sh b/scripts/verify-published-container.sh new file mode 100755 index 0000000..9a40e8a --- /dev/null +++ b/scripts/verify-published-container.sh @@ -0,0 +1,52 @@ +#!/bin/sh +set -eu + +if [ "$#" -ne 1 ]; then + echo "usage: $0 " >&2 + exit 2 +fi + +version=$1 +case "${version}" in + [A-Za-z0-9_]*) ;; + *) + echo "version is not a valid OCI tag: ${version}" >&2 + exit 2 + ;; +esac +case "${version}" in + *[!A-Za-z0-9._-]*) + echo "version is not a valid OCI tag: ${version}" >&2 + exit 2 + ;; +esac + +if ! command -v docker >/dev/null 2>&1; then + echo "missing required command: docker" >&2 + exit 2 +fi + +image="ncontainer.io/npub15qydau2hjma6ngxkl2cyar74wzyjshvl65za5k5rl69264ar2exs5cyejr/ngit-grasp:${version}" +cleanup() { + docker image rm --force "${image}" >/dev/null 2>&1 || true +} +trap cleanup EXIT HUP INT TERM + +deadline=$(($(date +%s) + 300)) +until docker pull "${image}"; do + if [ "$(date +%s)" -ge "${deadline}" ]; then + echo "published image did not become pullable within five minutes" >&2 + exit 1 + fi + sleep 5 +done + +actual=$(docker run --rm --entrypoint /usr/local/bin/ngit-grasp \ + "${image}" --version) +expected="ngit-grasp ${version}" +if [ "${actual}" != "${expected}" ]; then + echo "expected '${expected}' from the published image, got '${actual}'" >&2 + exit 1 +fi + +printf 'published image verified: %s\n' "${image}"