Files
govoplan/.gitea/workflows/runtime-distribution.yml
T

358 lines
18 KiB
YAML

name: Runtime Distribution
on:
workflow_dispatch:
inputs:
version:
description: Release version without leading v
required: true
type: string
python_image:
description: Digest-pinned multi-architecture Python 3.12 slim image
required: true
type: string
nginx_image:
description: Digest-pinned multi-architecture nginx-unprivileged image
required: true
type: string
postgres_image:
description: Digest-pinned PostgreSQL image
required: true
type: string
redis_image:
description: Digest-pinned Redis image
required: true
type: string
load_balancer_image:
description: Digest-pinned HAProxy image
required: true
type: string
managed_ingress_image:
description: Digest-pinned Caddy image
required: true
type: string
garage_image:
description: Digest-pinned Garage image
required: true
type: string
test_mail_image:
description: Digest-pinned GreenMail image
required: true
type: string
jobs:
publish-runtime:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5
with:
path: govoplan
- uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065
with:
python-version: "3.12"
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020
with:
node-version: "22"
- name: Validate immutable release inputs
env:
VERSION: ${{ inputs.version }}
PYTHON_IMAGE: ${{ inputs.python_image }}
NGINX_IMAGE: ${{ inputs.nginx_image }}
POSTGRES_IMAGE: ${{ inputs.postgres_image }}
REDIS_IMAGE: ${{ inputs.redis_image }}
LOAD_BALANCER_IMAGE: ${{ inputs.load_balancer_image }}
MANAGED_INGRESS_IMAGE: ${{ inputs.managed_ingress_image }}
GARAGE_IMAGE: ${{ inputs.garage_image }}
TEST_MAIL_IMAGE: ${{ inputs.test_mail_image }}
run: |
python - <<'PY'
import os
import re
version = os.environ["VERSION"]
if re.fullmatch(r"[0-9]+\.[0-9]+\.[0-9]+(?:[-+][A-Za-z0-9.-]+)?", version) is None:
raise SystemExit("version must be a SemVer value without a leading v")
image_pattern = re.compile(r"^[^@\s]+@sha256:[0-9a-f]{64}$")
for name in (
"PYTHON_IMAGE",
"NGINX_IMAGE",
"POSTGRES_IMAGE",
"REDIS_IMAGE",
"LOAD_BALANCER_IMAGE",
"MANAGED_INGRESS_IMAGE",
"GARAGE_IMAGE",
"TEST_MAIL_IMAGE",
):
if image_pattern.fullmatch(os.environ[name]) is None:
raise SystemExit(f"{name} must be an exact sha256 image reference")
PY
- name: Use HTTPS for GovOPlaN repositories
run: |
git config --global --add url."https://git.add-ideas.de/GovOPlaN/govoplan".insteadOf "git@git.add-ideas.de:GovOPlaN/govoplan"
git config --global --add url."https://git.add-ideas.de/GovOPlaN/govoplan".insteadOf "ssh://git@git.add-ideas.de/GovOPlaN/govoplan"
- name: Bootstrap release sources
working-directory: govoplan
run: python tools/repo/bootstrap-repositories.py --parent .. --transport public-https --reuse-checkout-auth --exclude-repo addideas-govoplan-website
- name: Build release wheel roots and WebUI
working-directory: govoplan
run: |
python -m venv .runtime-build
.runtime-build/bin/python -m pip install --upgrade pip wheel cryptography
mkdir -p runtime-output/local-wheels
.runtime-build/bin/python -m pip wheel --no-deps --wheel-dir runtime-output/local-wheels --requirement requirements-release.txt
bash tools/release/install-webui-release-dependencies.sh ../govoplan-core/webui
npm --prefix ../govoplan-core/webui run build
.runtime-build/bin/python tools/release/prepare-runtime-context.py \
--wheelhouse runtime-output/local-wheels \
--web-dist ../govoplan-core/webui/dist \
--output runtime-output/common \
--required-module tenancy \
--required-module organizations \
--required-module identity \
--required-module idm \
--required-module access \
--required-module admin \
--required-module dashboard \
--required-module policy \
--required-module audit \
--required-module docs \
--required-module ops
- name: Resolve architecture-specific offline wheelhouses
working-directory: govoplan
run: |
mkdir -p runtime-output/wheels-amd64 runtime-output/wheels-arm64
cp runtime-output/local-wheels/*.whl runtime-output/wheels-amd64/
cp runtime-output/local-wheels/*.whl runtime-output/wheels-arm64/
.runtime-build/bin/python -m pip download --only-binary=:all: \
--platform manylinux_2_17_x86_64 --platform manylinux2014_x86_64 \
--implementation cp --python-version 3.12 --abi cp312 \
--find-links runtime-output/local-wheels \
--dest runtime-output/wheels-amd64 \
--requirement runtime-output/common/requirements-runtime.txt
.runtime-build/bin/python -m pip download --only-binary=:all: \
--platform manylinux_2_17_aarch64 --platform manylinux2014_aarch64 \
--implementation cp --python-version 3.12 --abi cp312 \
--find-links runtime-output/local-wheels \
--dest runtime-output/wheels-arm64 \
--requirement runtime-output/common/requirements-runtime.txt
.runtime-build/bin/python tools/release/prepare-runtime-context.py \
--wheelhouse runtime-output/wheels-amd64 \
--web-dist ../govoplan-core/webui/dist \
--output runtime-output/context-amd64
.runtime-build/bin/python tools/release/prepare-runtime-context.py \
--wheelhouse runtime-output/wheels-arm64 \
--web-dist ../govoplan-core/webui/dist \
--output runtime-output/context-arm64
cmp runtime-output/context-amd64/composition.json runtime-output/context-arm64/composition.json
- name: Build one-file deployer
working-directory: govoplan
run: python tools/deployment/build-deployer-zipapp.py --output runtime-output/govoplan-deploy.pyz
- name: Authenticate OCI publication
working-directory: govoplan
env:
REGISTRY_USERNAME: ${{ secrets.GOVOPLAN_REGISTRY_USERNAME }}
REGISTRY_TOKEN: ${{ secrets.GOVOPLAN_REGISTRY_TOKEN }}
run: |
test -n "$REGISTRY_USERNAME"
test -n "$REGISTRY_TOKEN"
printf '%s' "$REGISTRY_TOKEN" | docker login git.add-ideas.de --username "$REGISTRY_USERNAME" --password-stdin
docker buildx create --name govoplan-runtime --use
- name: Build and publish architecture images
working-directory: govoplan
env:
VERSION: ${{ inputs.version }}
PYTHON_IMAGE: ${{ inputs.python_image }}
NGINX_IMAGE: ${{ inputs.nginx_image }}
run: |
COMPOSITION_SHA256="$(sha256sum runtime-output/context-amd64/composition.json | cut -d' ' -f1)"
for ARCH in amd64 arm64; do
docker buildx build --platform "linux/$ARCH" --push \
--file tools/release/runtime/Dockerfile.api \
--build-arg "PYTHON_IMAGE=$PYTHON_IMAGE" \
--build-arg "GOVOPLAN_RELEASE_VERSION=$VERSION" \
--build-arg "GOVOPLAN_COMPOSITION_SHA256=$COMPOSITION_SHA256" \
--tag "git.add-ideas.de/govoplan/runtime-api:$VERSION-$ARCH" \
"runtime-output/context-$ARCH"
docker buildx build --platform "linux/$ARCH" --push \
--file tools/release/runtime/Dockerfile.web \
--build-arg "NGINX_IMAGE=$NGINX_IMAGE" \
--build-arg "GOVOPLAN_RELEASE_VERSION=$VERSION" \
--build-arg "GOVOPLAN_COMPOSITION_SHA256=$COMPOSITION_SHA256" \
--tag "git.add-ideas.de/govoplan/runtime-web:$VERSION-$ARCH" \
"runtime-output/context-$ARCH"
done
docker buildx imagetools create \
--tag "git.add-ideas.de/govoplan/runtime-api:$VERSION" \
"git.add-ideas.de/govoplan/runtime-api:$VERSION-amd64" \
"git.add-ideas.de/govoplan/runtime-api:$VERSION-arm64"
docker buildx imagetools create \
--tag "git.add-ideas.de/govoplan/runtime-web:$VERSION" \
"git.add-ideas.de/govoplan/runtime-web:$VERSION-amd64" \
"git.add-ideas.de/govoplan/runtime-web:$VERSION-arm64"
docker buildx imagetools inspect "git.add-ideas.de/govoplan/runtime-api:$VERSION" --raw > runtime-output/api-index.json
docker buildx imagetools inspect "git.add-ideas.de/govoplan/runtime-web:$VERSION" --raw > runtime-output/web-index.json
API_DIGEST="sha256:$(sha256sum runtime-output/api-index.json | cut -d' ' -f1)"
WEB_DIGEST="sha256:$(sha256sum runtime-output/web-index.json | cut -d' ' -f1)"
python tools/release/resolve-oci-platforms.py --repository git.add-ideas.de/govoplan/runtime-api --index-digest "$API_DIGEST" --index runtime-output/api-index.json --output runtime-output/api-metadata.json
python tools/release/resolve-oci-platforms.py --repository git.add-ideas.de/govoplan/runtime-web --index-digest "$WEB_DIGEST" --index runtime-output/web-index.json --output runtime-output/web-metadata.json
- name: Resolve managed dependency platform images
working-directory: govoplan
env:
POSTGRES_IMAGE: ${{ inputs.postgres_image }}
REDIS_IMAGE: ${{ inputs.redis_image }}
run: |
docker buildx imagetools inspect "$POSTGRES_IMAGE" --raw > runtime-output/postgres-index.json
docker buildx imagetools inspect "$REDIS_IMAGE" --raw > runtime-output/redis-index.json
python tools/release/resolve-oci-platforms.py \
--repository "${POSTGRES_IMAGE%@*}" \
--index-digest "${POSTGRES_IMAGE##*@}" \
--index runtime-output/postgres-index.json \
--output runtime-output/postgres-metadata.json
python tools/release/resolve-oci-platforms.py \
--repository "${REDIS_IMAGE%@*}" \
--index-digest "${REDIS_IMAGE##*@}" \
--index runtime-output/redis-index.json \
--output runtime-output/redis-metadata.json
- name: Exercise amd64 and arm64 runtime images
working-directory: govoplan
run: |
for ARCH in amd64 arm64; do
.runtime-build/bin/python tools/checks/runtime-image-smoke.py \
--api-metadata runtime-output/api-metadata.json \
--web-metadata runtime-output/web-metadata.json \
--postgres-metadata runtime-output/postgres-metadata.json \
--redis-metadata runtime-output/redis-metadata.json \
--platform "linux/$ARCH" \
--output "runtime-output/evidence/runtime-smoke-$ARCH.json"
done
- name: Generate and sign distribution evidence
working-directory: govoplan
env:
VERSION: ${{ inputs.version }}
SOURCE_COMMIT: ${{ gitea.sha }}
SIGNING_KEY: ${{ secrets.RUNTIME_DISTRIBUTION_SIGNING_KEY }}
SIGNING_KEY_ID: ${{ secrets.RUNTIME_DISTRIBUTION_SIGNING_KEY_ID }}
TRUSTED_KEYRING: ${{ secrets.RUNTIME_DISTRIBUTION_KEYRING }}
POSTGRES_IMAGE: ${{ inputs.postgres_image }}
REDIS_IMAGE: ${{ inputs.redis_image }}
LOAD_BALANCER_IMAGE: ${{ inputs.load_balancer_image }}
MANAGED_INGRESS_IMAGE: ${{ inputs.managed_ingress_image }}
GARAGE_IMAGE: ${{ inputs.garage_image }}
TEST_MAIL_IMAGE: ${{ inputs.test_mail_image }}
run: |
test -n "$SIGNING_KEY"
test -n "$SIGNING_KEY_ID"
test -n "$TRUSTED_KEYRING"
printf '%s\n' "$SIGNING_KEY" > runtime-output/signing-key.pem
printf '%s\n' "$TRUSTED_KEYRING" > runtime-output/distribution-keyring.json
chmod 600 runtime-output/signing-key.pem
ARTIFACT_BASE="https://git.add-ideas.de/GovOPlaN/govoplan/releases/download/v$VERSION"
python tools/release/finalize-runtime-distribution.py \
--composition runtime-output/context-amd64/composition.json \
--api-metadata runtime-output/api-metadata.json \
--web-metadata runtime-output/web-metadata.json \
--deployer runtime-output/govoplan-deploy.pyz \
--deployer-url "$ARTIFACT_BASE/govoplan-deploy.pyz" \
--artifact-base-url "$ARTIFACT_BASE" \
--source-commit "$SOURCE_COMMIT" \
--version "$VERSION" \
--sequence "$(date -u +%Y%m%d%H%M)" \
--dependency "postgres=$POSTGRES_IMAGE" \
--dependency "redis=$REDIS_IMAGE" \
--dependency "load_balancer=$LOAD_BALANCER_IMAGE" \
--dependency "managed_ingress=$MANAGED_INGRESS_IMAGE" \
--dependency "garage=$GARAGE_IMAGE" \
--dependency "test_mail=$TEST_MAIL_IMAGE" \
--output-directory runtime-output/evidence \
--descriptor runtime-output/distribution-descriptor.json
.runtime-build/bin/python tools/release/generate-runtime-distribution.py \
--descriptor runtime-output/distribution-descriptor.json \
--signing-key "$SIGNING_KEY_ID=runtime-output/signing-key.pem" \
--output runtime-output/distribution-manifest.json
openssl pkeyutl -sign -inkey runtime-output/signing-key.pem -rawin \
-in runtime-output/govoplan-deploy.pyz \
-out runtime-output/govoplan-deploy.pyz.sig
(cd runtime-output && sha256sum govoplan-deploy.pyz > govoplan-deploy.pyz.sha256)
(cd runtime-output && sha256sum distribution-manifest.json > distribution-manifest.json.sha256)
rm runtime-output/signing-key.pem
- name: Verify the published bundle contract with the zipapp
working-directory: govoplan
env:
VERSION: ${{ inputs.version }}
SIGNING_KEY_ID: ${{ secrets.RUNTIME_DISTRIBUTION_SIGNING_KEY_ID }}
run: |
(cd runtime-output && sha256sum --check govoplan-deploy.pyz.sha256)
(cd runtime-output && sha256sum --check distribution-manifest.json.sha256)
.runtime-build/bin/python - <<'PY'
import json
import os
from pathlib import Path
keyring = json.loads(
Path("runtime-output/distribution-keyring.json").read_text(encoding="utf-8")
)
key_id = os.environ["SIGNING_KEY_ID"]
matches = [item for item in keyring["keys"] if item.get("key_id") == key_id]
if len(matches) != 1 or matches[0].get("status") != "active":
raise SystemExit("runtime signing key is not uniquely active in the keyring")
Path("runtime-output/runtime-release-public.pem").write_text(
matches[0]["public_key_pem"], encoding="utf-8"
)
PY
openssl pkeyutl -verify -pubin \
-inkey runtime-output/runtime-release-public.pem -rawin \
-in runtime-output/govoplan-deploy.pyz \
-sigfile runtime-output/govoplan-deploy.pyz.sig
cp runtime-output/govoplan-deploy.pyz runtime-output/govoplan-deploy.tampered.pyz
printf '\0' >> runtime-output/govoplan-deploy.tampered.pyz
if openssl pkeyutl -verify -pubin \
-inkey runtime-output/runtime-release-public.pem -rawin \
-in runtime-output/govoplan-deploy.tampered.pyz \
-sigfile runtime-output/govoplan-deploy.pyz.sig >/dev/null 2>&1; then
echo "Tampered deployment bootstrap unexpectedly verified" >&2
exit 1
fi
MANIFEST_SHA256="$(cut -d' ' -f1 runtime-output/distribution-manifest.json.sha256)"
python runtime-output/govoplan-deploy.pyz init \
--directory runtime-output/acceptance-install \
--non-interactive --module-set base
python runtime-output/govoplan-deploy.pyz verify-release \
--directory runtime-output/acceptance-install \
--manifest runtime-output/distribution-manifest.json \
--manifest-sha256 "$MANIFEST_SHA256" \
--trusted-keyring runtime-output/distribution-keyring.json \
--adopt
- name: Exercise the managed ingress boundary
working-directory: govoplan
env:
MANAGED_INGRESS_IMAGE: ${{ inputs.managed_ingress_image }}
LOAD_BALANCER_IMAGE: ${{ inputs.load_balancer_image }}
run: >-
python tools/checks/managed-ingress-drill.py
--caddy-image "$MANAGED_INGRESS_IMAGE"
--load-balancer-image "$LOAD_BALANCER_IMAGE"
- name: Publish immutable Gitea release assets
working-directory: govoplan
env:
VERSION: ${{ inputs.version }}
SOURCE_COMMIT: ${{ gitea.sha }}
GITEA_RELEASE_TOKEN: ${{ secrets.GOVOPLAN_RELEASE_TOKEN }}
run: |
python tools/release/publish-runtime-release.py \
--tag "v$VERSION" \
--target-commit "$SOURCE_COMMIT" \
--title "GovOPlaN v$VERSION runtime distribution" \
--asset runtime-output/govoplan-deploy.pyz \
--asset runtime-output/govoplan-deploy.pyz.sig \
--asset runtime-output/govoplan-deploy.pyz.sha256 \
--asset runtime-output/distribution-manifest.json \
--asset runtime-output/distribution-manifest.json.sha256 \
--asset runtime-output/distribution-keyring.json \
--asset runtime-output/context-amd64/composition.json \
--asset runtime-output/evidence/api-sbom.cdx.json \
--asset runtime-output/evidence/web-sbom.cdx.json \
--asset runtime-output/evidence/api-provenance.json \
--asset runtime-output/evidence/web-provenance.json \
--asset runtime-output/evidence/runtime-smoke-amd64.json \
--asset runtime-output/evidence/runtime-smoke-arm64.json