Initial public ModelForge release

This commit is contained in:
Jens
2026-09-01 21:30:16 +02:00
commit 7082ab955a
490 changed files with 104252 additions and 0 deletions
+292
View File
@@ -0,0 +1,292 @@
name: Managed validation
# The gate on protected master. Before v1 this keyed off manifests at the repository root, and this
# repository has none — pyproject.toml, package.json and the lock files all live in backend/,
# frontend/, node-agent/ and runtime-worker/. The "full" profile therefore completed in nine seconds
# having run a whitespace check, a merge-marker scan and py_compile, and no test suite at all, while
# reporting success to a branch protection rule that required it.
#
# It now targets the component roots explicitly, and refuses to report success when a component that
# should have run tests ran none. A gate that passes because it found nothing to do is worse than no
# gate: it produces the paperwork of validation without the fact of it.
on:
# Public exports require explicit owner dispatch; fork PRs never reach private runners.
workflow_dispatch:
inputs:
profile:
description: Allowlisted validation profile
required: true
default: full
type: choice
options: [test, lint, typecheck, build, security, full]
permissions:
contents: read
concurrency:
group: managed-validation-${{ gitea.repository }}-${{ gitea.ref }}
cancel-in-progress: true
jobs:
full:
name: full
runs-on: ubuntu-latest
timeout-minutes: 45
env:
PROFILE: ${{ inputs.profile || 'full' }}
# An explicit interpreter path rather than PATH manipulation: how a runner propagates PATH
# between steps varies, and a validation gate should not depend on that detail.
VENV: /tmp/modelforge-validation-venv
steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- name: Production delivery policy
shell: bash
run: python3 -m unittest discover -s .gitea/tests -p 'test_*.py' -v
- name: Validate the requested profile
shell: bash
run: |
set -euo pipefail
case "${PROFILE}" in
test|lint|typecheck|build|security|full) ;;
*) echo "Profile is not allowlisted: ${PROFILE}" >&2; exit 2 ;;
esac
echo "profile=${PROFILE}"
echo "commit=$(git rev-parse HEAD)"
- name: Repository hygiene
shell: bash
run: |
set -euo pipefail
git diff --check
if git grep -nE '^(<<<<<<< |=======$|>>>>>>> )' -- . ':!*.lock' ':!*.patch'; then
echo "Unresolved merge markers detected" >&2
exit 1
fi
echo "hygiene: clean"
- name: Prepare the report directory
shell: bash
run: mkdir -p reports
# Deliberately not using setup-python/setup-node: this runs on a self-hosted runner whose
# image already carries both, and the previous workflow depended on that too. What changes is
# that a missing toolchain now stops the run instead of quietly reducing what gets validated.
- name: Toolchain
shell: bash
run: |
set -euo pipefail
command -v python3 >/dev/null || { echo "python3 is not on PATH" >&2; exit 1; }
command -v node >/dev/null || { echo "node is not on PATH" >&2; exit 1; }
command -v npm >/dev/null || { echo "npm is not on PATH" >&2; exit 1; }
echo " python $(python3 --version)"
echo " node $(node --version)"
echo " npm $(npm --version)"
python3 -m venv "${VENV}"
"${VENV}/bin/python" -m pip install --disable-pip-version-check --quiet --upgrade pip
if [[ "${PROFILE}" == security || "${PROFILE}" == full ]]; then
command -v gitleaks >/dev/null || {
echo "gitleaks is required for the security profile" >&2
exit 1
}
"${VENV}/bin/python" -m pip install \
--disable-pip-version-check --quiet 'pip-audit==2.10.0'
fi
echo " venv $("${VENV}/bin/python" --version) at ${VENV}"
- name: Backend — install, lint, typecheck, test
id: backend
shell: bash
working-directory: backend
run: |
set -euo pipefail
"${VENV}/bin/python" -m pip install --disable-pip-version-check --quiet -e '.[dev]'
if [[ "${PROFILE}" == lint || "${PROFILE}" == full ]]; then
"${VENV}/bin/python" -m ruff check src tests
fi
if [[ "${PROFILE}" == typecheck || "${PROFILE}" == full ]]; then
"${VENV}/bin/python" -m mypy src
fi
if [[ "${PROFILE}" == test || "${PROFILE}" == full ]]; then
"${VENV}/bin/python" -m pytest -q --no-header --junitxml=../reports/backend.xml
fi
- name: Node Agent — install, lint, typecheck, test
id: node_agent
shell: bash
working-directory: node-agent
run: |
set -euo pipefail
"${VENV}/bin/python" -m pip install --disable-pip-version-check --quiet -e '.[dev]'
if [[ "${PROFILE}" == lint || "${PROFILE}" == full ]]; then
"${VENV}/bin/python" -m ruff check src
fi
if [[ "${PROFILE}" == typecheck || "${PROFILE}" == full ]]; then
"${VENV}/bin/python" -m mypy src
fi
if [[ "${PROFILE}" == test || "${PROFILE}" == full ]]; then
"${VENV}/bin/python" -m pytest -q --no-header --junitxml=../reports/node-agent.xml
fi
- name: Runtime Worker — install, lint, typecheck, test
id: runtime_worker
shell: bash
working-directory: runtime-worker
run: |
set -euo pipefail
"${VENV}/bin/python" -m pip install --disable-pip-version-check --quiet -e '.[dev]'
if [[ "${PROFILE}" == lint || "${PROFILE}" == full ]]; then
"${VENV}/bin/python" -m ruff check src
fi
if [[ "${PROFILE}" == typecheck || "${PROFILE}" == full ]]; then
"${VENV}/bin/python" -m mypy src
fi
if [[ "${PROFILE}" == test || "${PROFILE}" == full ]]; then
"${VENV}/bin/python" -m pytest -q --no-header --junitxml=../reports/runtime-worker.xml
fi
- name: Console — install frozen, typecheck, test, build
id: console
shell: bash
working-directory: frontend
run: |
set -euo pipefail
npm ci
if [[ "${PROFILE}" == typecheck || "${PROFILE}" == full ]]; then npx tsc --noEmit; fi
if [[ "${PROFILE}" == test || "${PROFILE}" == full ]]; then
npx vitest run --reporter=junit --outputFile=../reports/frontend.xml
fi
if [[ "${PROFILE}" == build || "${PROFILE}" == full ]]; then npm run build; fi
- name: Security — secrets and vulnerable dependencies
if: ${{ env.PROFILE == 'full' || env.PROFILE == 'security' }}
shell: bash
run: |
set -euo pipefail
gitleaks git . --no-banner --redact --report-format json \
--report-path reports/gitleaks.json
"${VENV}/bin/python" -m pip_audit --strict --skip-editable --desc=on \
--format=json --output=reports/python-audit.json
cd frontend
npm audit --audit-level=high --omit=dev --json > ../reports/npm-audit.json
- name: Compose projections
if: ${{ env.PROFILE == 'full' || env.PROFILE == 'build' }}
shell: bash
run: |
set -euo pipefail
if ! command -v docker >/dev/null; then
echo "SKIPPED: no docker CLI on this runner; compose projections are validated by the"
echo "local release gate instead. This step never reports a pass it did not earn."
exit 0
fi
# Compose interpolation needs values, not live credentials. These fixed validation-only
# strings never reach a service and ensure the fail-closed production projection is
# actually parsed on every full/build run.
export MODELFORGE_POSTGRES_DB=modelforge_validation
export MODELFORGE_POSTGRES_ADMIN_PASSWORD=validation-admin-only
export MODELFORGE_MIGRATION_DB_PASSWORD=validation-owner-only
export MODELFORGE_RUNTIME_DB_PASSWORD=validation-runtime-only
export MODELFORGE_MIGRATION_DATABASE_URL=postgresql+psycopg://modelforge:validation-owner-only@postgres:5432/modelforge_validation
export MODELFORGE_RUNTIME_DATABASE_URL=postgresql+psycopg://modelforge_runtime:validation-runtime-only@postgres:5432/modelforge_validation
export MODELFORGE_OPERATOR_API_KEY=validation-operator-key-32-characters # gitleaks:allow — synthetic Compose interpolation only
export MODELFORGE_BACKUP_ENCRYPTION_KEY=dmFsaWRhdGlvbi1vbmx5LWtleS0zMi1ieXRlcw== # gitleaks:allow — base64 of a public validation-only string
export MODELFORGE_CORS_ORIGINS=https://modelforge.example.test
export VITE_API_BASE_URL=https://modelforge.example.test
export MODELFORGE_VERSION=1.2.1
export MODELFORGE_AGENT_CONTROL_PLANE_HOST_ADDRESS=127.0.0.1
export MODELFORGE_AGENT_CA_CERT_PATH=./config/ca.crt
failures=0
check() {
if docker compose "$@" config -q >/dev/null 2>&1; then
echo " OK $*"
else
echo " FAIL $*"; failures=$((failures + 1))
fi
}
check -f docker-compose.yml
for overlay in backup dr gpu node-agent node-recovery production runtime-worker; do
check -f docker-compose.yml -f "docker-compose.${overlay}.yml"
done
check -f docker-compose.yml -f docker-compose.node-agent.yml \
-f docker-compose.node-agent.private-ca.yml
check -f docker-compose.yml -f docker-compose.runtime-worker.yml \
-f docker-compose.runtime-worker.private-ca.yml
[[ "${failures}" -eq 0 ]] || { echo "${failures} projection(s) invalid" >&2; exit 1; }
- name: Configuration documentation is current
if: ${{ env.PROFILE == 'full' }}
shell: bash
run: |
"${VENV}/bin/python" scripts/generate_configuration_docs.py --check
# The rule that makes the rest of this meaningful. Every component above declares a manifest,
# so every component must have reported a test count. Zero tests where tests were expected is
# a failure, not a pass — that is exactly how the previous workflow reported success.
- name: Refuse a validation that silently ran no tests
if: ${{ env.PROFILE == 'full' || env.PROFILE == 'test' }}
shell: bash
run: |
set -euo pipefail
"${VENV}/bin/python" - <<'PY'
import sys
import xml.etree.ElementTree as ET
from pathlib import Path
expected = {
"backend.xml": "backend",
"node-agent.xml": "node-agent",
"runtime-worker.xml": "runtime-worker",
"frontend.xml": "frontend",
}
reports = Path("reports")
failures = []
total = 0
for filename, component in expected.items():
path = reports / filename
if not path.is_file():
failures.append(f"{component}: no test report was produced")
continue
root = ET.parse(path).getroot()
suites = [root] if root.tag == "testsuite" else list(root.iter("testsuite"))
tests = sum(int(suite.get("tests", 0)) for suite in suites)
errors = sum(int(suite.get("errors", 0)) for suite in suites)
failed = sum(int(suite.get("failures", 0)) for suite in suites)
skipped = sum(int(suite.get("skipped", 0)) for suite in suites)
executed = tests - skipped
total += tests
print(f" {component:16} {tests:5} tests, {skipped} skipped, "
f"{failed} failed, {errors} errors")
if executed <= 0:
failures.append(f"{component}: {tests} tests collected, {executed} executed")
if failed or errors:
failures.append(f"{component}: {failed} failed, {errors} errors")
print(f" {'TOTAL':16} {total:5} tests")
if failures:
print("\nManaged validation refused:", file=sys.stderr)
for failure in failures:
print(f" - {failure}", file=sys.stderr)
sys.exit(1)
PY
- name: Validation summary
if: always()
shell: bash
run: |
echo "commit: $(git rev-parse HEAD)"
echo "profile: ${PROFILE}"
ls -l reports/ 2>/dev/null || echo "no reports directory"
# Gitea only lists dispatchable workflow files from the default branch. Reusing the dedicated
# workflow from the existing managed entry point lets an unmerged branch prove its exact public
# export on the server. Ordinary PR validation and every profile except an explicit `build`
# dispatch remain unchanged.
public_candidate_acceptance:
name: Public candidate server acceptance
if: ${{ gitea.event_name == 'workflow_dispatch' && inputs.profile == 'build' }}
uses: ./.gitea/workflows/public-candidate-acceptance.yml
with:
source_commit: ${{ gitea.sha }}
public_api_origin: https://modelforge.example.test
@@ -0,0 +1,213 @@
name: Public candidate server acceptance
# Builds only an exact, curated public-source commit in a disposable Compose namespace. This job
# never calls the Unraid deploy controller and cannot select the production deployment action.
on:
workflow_dispatch:
inputs:
source_commit:
description: Exact canonical 40-character commit SHA to export and validate
required: true
type: string
public_api_origin:
description: Bare API origin compiled into the candidate Console image
required: true
default: https://modelforge.example.test
type: string
workflow_call:
inputs:
source_commit:
description: Exact canonical 40-character commit SHA to export and validate
required: true
type: string
public_api_origin:
description: Bare API origin compiled into the candidate Console image
required: true
type: string
permissions:
contents: read
concurrency:
group: public-candidate-acceptance-${{ inputs.source_commit }}
cancel-in-progress: false
jobs:
acceptance:
name: Four images and isolated clean install
runs-on: ubuntu-latest
timeout-minutes: 240
env:
GITLEAKS_VERSION: 8.30.1
GITLEAKS_SHA256: 551f6fc83ea457d62a0d98237cbad105af8d557003051f41f3e7ca7b3f2470eb
TRIVY_VERSION: 0.74.0
TRIVY_SHA256: 2ae6fe3ee734b7fdf11335663e18c75ea12dccc76062f09f164a3b0f8be4371a
steps:
- name: Validate immutable acceptance inputs
shell: bash
env:
SOURCE_COMMIT: ${{ inputs.source_commit }}
PUBLIC_API_ORIGIN: ${{ inputs.public_api_origin }}
run: |
set -euo pipefail
[[ "${SOURCE_COMMIT}" =~ ^[0-9a-f]{40}$ ]] || {
echo "source_commit must be an exact lowercase commit SHA" >&2; exit 2;
}
python3 - "${PUBLIC_API_ORIGIN}" <<'PY'
import sys
from urllib.parse import urlparse
parsed = urlparse(sys.argv[1])
if parsed.scheme not in {"http", "https"} or not parsed.netloc:
raise SystemExit("public_api_origin must be an absolute HTTP(S) origin")
if parsed.path or parsed.params or parsed.query or parsed.fragment:
raise SystemExit("public_api_origin must be a bare origin without a path")
PY
- name: Check out the exact canonical source
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
with:
ref: ${{ inputs.source_commit }}
fetch-depth: 0
persist-credentials: false
- name: Verify checkout and runner isolation toolchain
shell: bash
env:
SOURCE_COMMIT: ${{ inputs.source_commit }}
run: |
set -euo pipefail
[[ "$(git rev-parse HEAD)" == "${SOURCE_COMMIT}" ]]
[[ -z "$(git status --porcelain)" ]]
command -v python3 >/dev/null
command -v node >/dev/null
command -v docker >/dev/null
docker version
docker compose version
python3 -m unittest discover -s .gitea/tests -p 'test_*.py' -v
- name: Install checksum-pinned acceptance tools
shell: bash
run: |
set -euo pipefail
tool_root="$(mktemp -d /tmp/modelforge-acceptance-tools.XXXXXXXX)"
trivy_archive="${tool_root}/trivy.tar.gz"
curl --fail --location --show-error --retry 3 --retry-all-errors \
--output "${trivy_archive}" \
"https://github.com/aquasecurity/trivy/releases/download/v${TRIVY_VERSION}/trivy_${TRIVY_VERSION}_Linux-64bit.tar.gz"
printf '%s %s\n' "${TRIVY_SHA256}" "${trivy_archive}" \
| sha256sum --check --strict
tar --extract --gzip --file "${trivy_archive}" \
--directory "${tool_root}" trivy
chmod 0755 "${tool_root}/trivy"
gitleaks_archive="${tool_root}/gitleaks.tar.gz"
curl --fail --location --show-error --retry 3 --retry-all-errors \
--output "${gitleaks_archive}" \
"https://github.com/gitleaks/gitleaks/releases/download/v${GITLEAKS_VERSION}/gitleaks_${GITLEAKS_VERSION}_linux_x64.tar.gz"
printf '%s %s\n' "${GITLEAKS_SHA256}" "${gitleaks_archive}" \
| sha256sum --check --strict
tar --extract --gzip --file "${gitleaks_archive}" \
--directory "${tool_root}" gitleaks
chmod 0755 "${tool_root}/gitleaks"
printf 'TRIVY=%s\n' "${tool_root}/trivy" >> "${GITHUB_ENV}"
printf 'GITLEAKS=%s\n' "${tool_root}/gitleaks" >> "${GITHUB_ENV}"
- name: Export and validate the curated public source
shell: bash
env:
SOURCE_COMMIT: ${{ inputs.source_commit }}
run: |
set -euo pipefail
public_source="$(mktemp -d /tmp/modelforge-public-parent.XXXXXXXX)/candidate"
reports="$(mktemp -d /tmp/modelforge-public-reports.XXXXXXXX)"
node scripts/export-public-source.mjs \
--output "${public_source}" --report "${reports}/export-report.json"
(cd "${public_source}" && node scripts/validate-public-source.mjs)
"${GITLEAKS}" dir "${public_source}" --no-banner --redact \
--report-format json --report-path "${reports}/gitleaks.json"
source_date="$(git show -s --format=%cI "${SOURCE_COMMIT}")"
git -C "${public_source}" init --initial-branch=main
git -C "${public_source}" config user.name "ModelForge acceptance"
git -C "${public_source}" config user.email "acceptance@modelforge.invalid"
git -C "${public_source}" add --all
GIT_AUTHOR_DATE="${source_date}" GIT_COMMITTER_DATE="${source_date}" \
git -C "${public_source}" commit -m "Public candidate from ${SOURCE_COMMIT}"
printf 'PUBLIC_SOURCE=%s\n' "${public_source}" >> "${GITHUB_ENV}"
printf 'ACCEPTANCE_REPORTS=%s\n' "${reports}" >> "${GITHUB_ENV}"
- name: Build, scan and clean-install the public candidate
shell: bash
env:
PUBLIC_API_ORIGIN: ${{ inputs.public_api_origin }}
run: |
set -euo pipefail
cd "${PUBLIC_SOURCE}"
python3 scripts/rc_server_acceptance.py \
--public-api-origin "${PUBLIC_API_ORIGIN}" \
--trivy "${TRIVY}" \
--output acceptance-evidence \
--project-suffix "${GITHUB_RUN_ID}-${GITHUB_RUN_ATTEMPT}"
- name: Always remove acceptance Docker resources
if: always()
shell: bash
run: |
set +e
cleanup_failed=0
suffix="$(printf '%s' "${GITHUB_RUN_ID}-${GITHUB_RUN_ATTEMPT}" \
| tr '[:upper:]' '[:lower:]' | tr -cd '[:alnum:]' | tail -c 24)"
project="modelforge-rc-${suffix}"
for kind in container volume network; do
while IFS= read -r resource; do
[[ -n "${resource}" ]] || continue
case "${kind}" in
container) docker container rm --force "${resource}" || cleanup_failed=1 ;;
volume) docker volume rm --force "${resource}" || cleanup_failed=1 ;;
network) docker network rm "${resource}" || cleanup_failed=1 ;;
esac
done < <(docker "${kind}" ls --quiet \
--filter "label=com.docker.compose.project=${project}" 2>/dev/null)
done
if [[ -n "${PUBLIC_SOURCE:-}" && -d "${PUBLIC_SOURCE}/.git" ]]; then
candidate_commit="$(git -C "${PUBLIC_SOURCE}" rev-parse HEAD 2>/dev/null)"
version="$(tr -d '\r\n' < "${PUBLIC_SOURCE}/VERSION" 2>/dev/null)"
for image in modelforge-api modelforge-web modelforge-node-agent \
modelforge-runtime-worker; do
tag="${image}:${version}"
revision="$(docker inspect --format \
'{{index .Config.Labels "org.opencontainers.image.revision"}}' \
"${tag}" 2>/dev/null)"
if [[ -n "${candidate_commit}" && "${revision}" == "${candidate_commit}" ]]; then
docker image rm --force "${tag}" || cleanup_failed=1
fi
done
fi
leftovers="$(docker container ls --all --quiet \
--filter "label=com.docker.compose.project=${project}" 2>/dev/null)"
[[ -z "${leftovers}" ]] || {
echo "Acceptance containers remain after cleanup: ${leftovers}" >&2
exit 1
}
[[ "${cleanup_failed}" == 0 ]] || {
echo "One or more exact acceptance resources could not be removed" >&2
exit 1
}
- name: Upload acceptance evidence
if: always()
uses: https://gitea.com/actions/gitea-upload-artifact@81f940d004763f986ba3582c007fd842dd5cb0d7
with:
name: public-candidate-${{ gitea.run_id }}-${{ gitea.run_attempt }}-${{ inputs.source_commit }}
path: |
${{ env.ACCEPTANCE_REPORTS }}/export-report.json
${{ env.ACCEPTANCE_REPORTS }}/gitleaks.json
${{ env.PUBLIC_SOURCE }}/PUBLIC_SOURCE_EXPORT.md
${{ env.PUBLIC_SOURCE }}/PUBLIC_SOURCE_MANIFEST.json
${{ env.PUBLIC_SOURCE }}/acceptance-evidence/*.json
${{ env.PUBLIC_SOURCE }}/acceptance-evidence/*.txt
${{ env.PUBLIC_SOURCE }}/acceptance-evidence/release/*.json
${{ env.PUBLIC_SOURCE }}/acceptance-evidence/release/*SHA256SUMS
if-no-files-found: warn
retention-days: 90
+132
View File
@@ -0,0 +1,132 @@
name: Unraid stable release deployment
on:
workflow_dispatch:
inputs:
release_tag:
description: Annotated stable release tag (for example, v1.2.1)
required: true
type: string
release_commit:
description: Exact lowercase 40-character commit SHA referenced by the tag
required: true
type: string
action:
description: Verify provenance only, or deploy the verified stable release
required: true
default: VERIFY_ONLY
type: choice
options:
- VERIFY_ONLY
- DEPLOY_STABLE_TO_PRODUCTION
concurrency:
group: unraid-production-itworx-modelforge
cancel-in-progress: false
permissions:
contents: read
jobs:
deploy:
name: Verify and optionally deploy a stable release
runs-on: unraid-deploy
timeout-minutes: 180
steps:
- name: Validate immutable dispatch inputs
shell: bash
env:
RELEASE_TAG: ${{ inputs.release_tag }}
RELEASE_COMMIT: ${{ inputs.release_commit }}
DEPLOY_ACTION: ${{ inputs.action }}
run: |
set -euo pipefail
if [[ "${GITHUB_REF:-}" != "refs/heads/master" ]]; then
echo "Stable production deployment must be dispatched from refs/heads/master" >&2
exit 2
fi
if [[ ! "${RELEASE_TAG}" =~ ^v(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)$ ]]; then
echo "release_tag must be a strict stable SemVer tag such as v1.2.1" >&2
exit 2
fi
if [[ ! "${RELEASE_COMMIT}" =~ ^[0-9a-f]{40}$ ]]; then
echo "release_commit must be an exact lowercase 40-character SHA" >&2
exit 2
fi
case "${DEPLOY_ACTION}" in
VERIFY_ONLY|DEPLOY_STABLE_TO_PRODUCTION) ;;
*) echo "action is not allowlisted: ${DEPLOY_ACTION}" >&2; exit 2 ;;
esac
- name: Check out the exact release commit with full history
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
with:
ref: ${{ inputs.release_commit }}
fetch-depth: 0
# The following provenance step performs an authenticated exact-ref fetch.
persist-credentials: true
- name: Verify stable release provenance
shell: bash
env:
RELEASE_TAG: ${{ inputs.release_tag }}
RELEASE_COMMIT: ${{ inputs.release_commit }}
run: |
set -euo pipefail
git fetch --force --no-recurse-submodules origin \
"refs/tags/${RELEASE_TAG}:refs/tags/${RELEASE_TAG}" \
"refs/heads/master:refs/remotes/origin/master"
tag_ref="refs/tags/${RELEASE_TAG}"
if [[ "$(git cat-file -t "${tag_ref}")" != "tag" ]]; then
echo "${RELEASE_TAG} must be an annotated tag" >&2
exit 1
fi
tag_object="$(git rev-parse "${tag_ref}^{tag}")"
tag_commit="$(git rev-parse "${tag_ref}^{commit}")"
head_commit="$(git rev-parse HEAD)"
if [[ "${tag_commit}" != "${RELEASE_COMMIT}" ]]; then
echo "Tag commit ${tag_commit} does not match release_commit ${RELEASE_COMMIT}" >&2
exit 1
fi
if [[ "${head_commit}" != "${RELEASE_COMMIT}" ]]; then
echo "Checked-out HEAD ${head_commit} does not match release_commit ${RELEASE_COMMIT}" >&2
exit 1
fi
if [[ ! -f VERSION ]]; then
echo "VERSION is missing at the release commit" >&2
exit 1
fi
version="$(tr -d '\r\n' < VERSION)"
expected_version="${RELEASE_TAG#v}"
if [[ "${version}" != "${expected_version}" ]]; then
echo "VERSION ${version} does not match release tag ${RELEASE_TAG}" >&2
exit 1
fi
if ! git merge-base --is-ancestor "${RELEASE_COMMIT}" refs/remotes/origin/master; then
echo "Release commit ${RELEASE_COMMIT} is not an ancestor of origin/master" >&2
exit 1
fi
printf 'DEPLOY_COMMIT=%s\n' "${RELEASE_COMMIT}" >> "${GITHUB_ENV}"
echo "Verified annotated ${RELEASE_TAG} object ${tag_object} at immutable commit ${RELEASE_COMMIT}"
- name: Verification-only result
if: ${{ inputs.action == 'VERIFY_ONLY' }}
shell: bash
run: echo "Stable release provenance verified; production was not changed."
- name: Deploy verified stable release to production
if: ${{ inputs.action == 'DEPLOY_STABLE_TO_PRODUCTION' }}
shell: bash
run: |
set -euo pipefail
test -n "${DEPLOY_COMMIT:-}"
docker exec gitea-deploy-control \
/opt/gitea-deploy/deploy.py deploy \
"${GITHUB_REPOSITORY}" "${DEPLOY_COMMIT}"