398 lines
20 KiB
YAML
398 lines
20 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
|
|
binfmt_image:
|
|
description: Digest-pinned tonistiigi/binfmt image for arm64 CI execution
|
|
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 }}
|
|
BINFMT_IMAGE: ${{ inputs.binfmt_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",
|
|
"BINFMT_IMAGE",
|
|
):
|
|
if image_pattern.fullmatch(os.environ[name]) is None:
|
|
raise SystemExit(f"{name} must be an exact sha256 image reference")
|
|
PY
|
|
- name: Resolve immutable release source
|
|
working-directory: govoplan
|
|
env:
|
|
VERSION: ${{ inputs.version }}
|
|
run: |
|
|
git fetch --force --no-tags origin "refs/tags/v$VERSION:refs/tags/v$VERSION"
|
|
mkdir -p runtime-output
|
|
git rev-parse "v$VERSION^{commit}" > runtime-output/release-source-commit
|
|
grep -Eq '^[0-9a-f]{40}$' runtime-output/release-source-commit
|
|
- 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
|
|
env:
|
|
VERSION: ${{ inputs.version }}
|
|
GOVOPLAN_PACKAGE_USERNAME: ${{ secrets.GOVOPLAN_PACKAGE_USERNAME }}
|
|
GOVOPLAN_PACKAGE_TOKEN: ${{ secrets.GOVOPLAN_PACKAGE_TOKEN }}
|
|
run: |
|
|
python -m venv .runtime-build
|
|
.runtime-build/bin/python -m pip install --upgrade pip cryptography
|
|
.runtime-build/bin/python tools/release/generate-release-package-set.py \
|
|
--version "$VERSION" \
|
|
--output runtime-output/release-packages.json
|
|
.runtime-build/bin/python tools/release/resolve-package-artifacts.py \
|
|
--package-set runtime-output/release-packages.json \
|
|
--wheelhouse runtime-output/local-wheels \
|
|
--webui-packages runtime-output/webui-packages \
|
|
--lock-output runtime-output/package-artifacts.lock.json \
|
|
--requirements-output runtime-output/requirements-release.packages.txt \
|
|
--python .runtime-build/bin/python
|
|
PYTHON="$PWD/.runtime-build/bin/python" \
|
|
GOVOPLAN_WEBUI_PACKAGE_LOCK="$PWD/runtime-output/package-artifacts.lock.json" \
|
|
GOVOPLAN_WEBUI_PACKAGE_DIR="$PWD/runtime-output/webui-packages" \
|
|
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: Register arm64 execution for runtime smoke
|
|
working-directory: govoplan
|
|
env:
|
|
BINFMT_IMAGE: ${{ inputs.binfmt_image }}
|
|
run: docker run --privileged --rm "$BINFMT_IMAGE" --install arm64
|
|
- 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 }}
|
|
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: |
|
|
SOURCE_COMMIT="$(cat runtime-output/release-source-commit)"
|
|
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" \
|
|
--package-lock runtime-output/package-artifacts.lock.json \
|
|
--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"
|
|
--probe-image "$(jq -r '.platforms["linux/amd64"]' runtime-output/api-metadata.json)"
|
|
- name: Publish immutable Gitea release assets
|
|
working-directory: govoplan
|
|
env:
|
|
VERSION: ${{ inputs.version }}
|
|
GITEA_RELEASE_TOKEN: ${{ secrets.GOVOPLAN_RELEASE_TOKEN }}
|
|
run: |
|
|
SOURCE_COMMIT="$(cat runtime-output/release-source-commit)"
|
|
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/release-packages.json \
|
|
--asset runtime-output/package-artifacts.lock.json \
|
|
--asset runtime-output/requirements-release.packages.txt \
|
|
--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
|