feat: add Proxmox provisioning service with CI and deployment tooling
CI / javascript-check (push) Successful in 51s
CI / container-policy (push) Successful in 2s
CI / container-verify (push) Canceled after 0s
CI / container-publish (push) Canceled after 0s
CI / python-tests (push) Canceled after 6m59s

This commit is contained in:
BartelLuis
2026-09-14 20:09:12 +02:00
commit 06c3474636
52 changed files with 9779 additions and 0 deletions
+147
View File
@@ -0,0 +1,147 @@
#!/bin/sh
# Build and smoke-test the same image that is optionally published to GHCR.
set -eu
fail() {
printf '%s\n' "ERROR: $*" >&2
exit 1
}
mode=${1:-}
case "$mode" in
verify|publish) ;;
*) fail 'Usage: sh ci/container.sh verify|publish' ;;
esac
: "${GITHUB_SHA:?GITHUB_SHA is required}"
: "${GITHUB_RUN_ID:?GITHUB_RUN_ID is required}"
: "${GITHUB_RUN_ATTEMPT:?GITHUB_RUN_ATTEMPT is required}"
: "${GITHUB_JOB:?GITHUB_JOB is required}"
: "${GITHUB_REPOSITORY:?GITHUB_REPOSITORY is required}"
: "${GITHUB_SERVER_URL:?GITHUB_SERVER_URL is required}"
printf '%s\n' "$GITHUB_SHA" | grep -Eq '^[0-9a-f]{40}$' || fail 'Expected a full Git commit SHA'
for run_identifier in "$GITHUB_RUN_ID" "$GITHUB_RUN_ATTEMPT"; do
case "$run_identifier" in
''|*[!0-9]*) fail 'GitHub run ID and attempt must be numeric' ;;
esac
done
case "$GITHUB_JOB" in
''|[!A-Za-z_]*|*[!A-Za-z0-9_-]*) fail 'GITHUB_JOB must be a safe job identifier' ;;
esac
printf '%s\n' "$GITHUB_REPOSITORY" | grep -Eq '^[A-Za-z0-9][A-Za-z0-9-]*/[A-Za-z0-9_][A-Za-z0-9_.-]*$' || fail 'Expected GITHUB_REPOSITORY in owner/repository form'
registry_image="ghcr.io/$(printf '%s' "$GITHUB_REPOSITORY" | LC_ALL=C tr '[:upper:]' '[:lower:]')"
project_url="${GITHUB_SERVER_URL%/}/${GITHUB_REPOSITORY}"
job_identifier="${GITHUB_RUN_ID}-${GITHUB_RUN_ATTEMPT}-${GITHUB_JOB}"
# Read the literal PEP 621 version without needing the test job's virtual
# environment, installing dependencies, or evaluating project code.
project_version=$(awk '
/^\[project\][[:space:]]*$/ { in_project = 1; next }
/^\[/ { in_project = 0 }
in_project && /^[[:space:]]*version[[:space:]]*=/ {
value = $0
sub(/^[^=]*=[[:space:]]*"/, "", value)
sub(/"[[:space:]]*(#.*)?$/, "", value)
print value
exit
}
' pyproject.toml)
[ -n "$project_version" ] || fail 'Cannot read [project].version from pyproject.toml'
publish_tag=
if [ "$mode" = publish ]; then
[ "${GITHUB_REF_PROTECTED:-}" = true ] || fail 'Publication requires a protected branch or tag'
case "${GITHUB_EVENT_NAME:-}" in
push|workflow_dispatch) ;;
*) fail 'Publication is allowed only from push or workflow_dispatch events' ;;
esac
case "${GITHUB_REF_TYPE:-}" in
tag)
printf '%s\n' "${GITHUB_REF_NAME:-}" | grep -Eq '^v(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)$' || fail 'Release tags must have the form vX.Y.Z'
publish_tag=${GITHUB_REF_NAME#v}
[ "$publish_tag" = "$project_version" ] || fail "Release tag $GITHUB_REF_NAME does not match project version $project_version"
;;
branch)
[ -n "${DEFAULT_BRANCH:-}" ] && [ "${GITHUB_REF_NAME:-}" = "$DEFAULT_BRANCH" ] || fail 'Only the default branch or a version tag may publish'
publish_tag=edge
;;
*) fail 'Publication requires a branch or tag ref' ;;
esac
: "${GITHUB_ACTOR:?GITHUB_ACTOR is required for publication}"
: "${GHCR_TOKEN:?GHCR_TOKEN is required for publication}"
fi
BUILD_IMAGE="${registry_image}:ci-${job_identifier}"
smoke_container="ais-smoke-${job_identifier}"
smoke_started=false
build_started=false
docker_config_dir=
cleanup() {
if [ "$smoke_started" = true ]; then
docker rm --force "$smoke_container" >/dev/null 2>&1 || true
fi
if [ "$build_started" = true ]; then
# A self-hosted runner may use a persistent daemon. Remove only this job's
# unique tag; published SHA/channel tags may be used by other jobs.
docker image rm "$BUILD_IMAGE" >/dev/null 2>&1 || true
fi
if [ -n "$docker_config_dir" ]; then
rm -f "$docker_config_dir/config.json"
rmdir "$docker_config_dir" >/dev/null 2>&1 || true
fi
}
trap cleanup EXIT
trap 'exit 130' INT
trap 'exit 143' TERM
rm -f build.env deploy.env
build_started=true
# Load one single-platform image locally so the smoke test and both published
# tags all use the exact image built here, without separate manifest artifacts.
docker build --pull --platform linux/amd64 \
--provenance=false --sbom=false \
--label "org.opencontainers.image.source=$project_url" \
--label "org.opencontainers.image.revision=$GITHUB_SHA" \
--label "org.opencontainers.image.version=$project_version" \
--tag "$BUILD_IMAGE" .
# Stream the smoke script through stdin, so no checkout bind mount is needed.
smoke_started=true
docker run --rm --interactive --name "$smoke_container" \
--read-only --cap-drop ALL --security-opt no-new-privileges:true \
--network none --tmpfs /tmp:rw,noexec,nosuid,size=128m \
--entrypoint python "$BUILD_IMAGE" - < tests/container_smoke.py
printf 'BUILD_IMAGE=%s\nIMAGE_REVISION=%s\nIMAGE_VERSION=%s\n' \
"$BUILD_IMAGE" "$GITHUB_SHA" "$project_version" > build.env
if [ "$mode" = publish ]; then
docker_config_dir=$(mktemp -d "${TMPDIR:-/tmp}/ais-ci-docker.XXXXXXXX")
DOCKER_CONFIG=$docker_config_dir
export DOCKER_CONFIG
printf '%s' "$GHCR_TOKEN" | docker login ghcr.io \
--username "$GITHUB_ACTOR" --password-stdin
commit_image="${registry_image}:sha-${GITHUB_SHA}"
channel_image="${registry_image}:${publish_tag}"
docker tag "$BUILD_IMAGE" "$commit_image"
docker push "$commit_image"
docker tag "$BUILD_IMAGE" "$channel_image"
docker push "$channel_image"
# Tags can be reassigned by a later rebuild; use the registry digest for
# a deployment reference that keeps identifying precisely this image.
repo_digests=$(docker image inspect --format '{{range .RepoDigests}}{{println .}}{{end}}' "$BUILD_IMAGE")
image_digest=
while IFS= read -r candidate; do
case "$candidate" in
"$registry_image"@sha256:*) image_digest=$candidate; break ;;
esac
done <<EOF
$repo_digests
EOF
[ -n "$image_digest" ] || fail 'Published image has no registry digest'
printf '%s\n' "${image_digest#*@sha256:}" | grep -Eq '^[0-9a-f]{64}$' || fail 'Registry returned an invalid image digest'
printf 'PROVISIONER_IMAGE=%s\n' "$image_digest" > deploy.env
printf 'Published %s and %s\nDeployment image: %s\n' "$commit_image" "$channel_image" "$image_digest"
fi
+51
View File
@@ -0,0 +1,51 @@
#!/bin/sh
# Run tests in an isolated, disposable environment on a Linux Actions runner.
set -eu
fail() {
printf '%s\n' "ERROR: $*" >&2
exit 1
}
: "${GITHUB_WORKSPACE:?GITHUB_WORKSPACE is required}"
: "${GITHUB_RUN_ID:?GITHUB_RUN_ID is required}"
: "${GITHUB_RUN_ATTEMPT:?GITHUB_RUN_ATTEMPT is required}"
: "${GITHUB_JOB:?GITHUB_JOB is required}"
for run_identifier in "$GITHUB_RUN_ID" "$GITHUB_RUN_ATTEMPT"; do
case "$run_identifier" in
''|*[!0-9]*) fail 'GitHub run ID and attempt must be numeric' ;;
esac
done
case "$GITHUB_JOB" in
''|[!A-Za-z_]*|*[!A-Za-z0-9_-]*) fail 'GITHUB_JOB must be a safe job identifier' ;;
esac
job_identifier="${GITHUB_RUN_ID}-${GITHUB_RUN_ATTEMPT}-${GITHUB_JOB}"
for required_tool in python3 bash openssl ssh-keygen; do
command -v "$required_tool" >/dev/null 2>&1 || fail "Required runner tool is missing: $required_tool"
done
python3 -c 'import sys; sys.version_info >= (3, 12) or sys.exit("Python 3.12 or newer is required")'
cd "$GITHUB_WORKSPACE"
project_root=$(pwd -P)
[ -f pyproject.toml ] || fail 'GITHUB_WORKSPACE does not contain pyproject.toml'
# Resolve the checkout first and allocate a fresh directory inside it. The
# cleanup target is never taken from an arbitrary environment-provided path.
venv_dir=$(mktemp -d "$project_root/.venv-ci-${job_identifier}.XXXXXXXX")
cleanup() {
cleanup_status=$?
trap - EXIT
case "$venv_dir" in
"$project_root"/.venv-ci-"$job_identifier".*) rm -rf -- "$venv_dir" ;;
*) printf '%s\n' 'ERROR: Refusing to clean an unexpected virtual environment path' >&2 ;;
esac
exit "$cleanup_status"
}
trap cleanup EXIT
trap 'exit 130' INT
trap 'exit 143' TERM
python3 -m venv "$venv_dir"
"$venv_dir/bin/python" -m pip install '.[dev]'
mkdir -p reports
"$venv_dir/bin/python" -m pytest --junitxml=reports/pytest.xml