ci: make release promotion tag-driven and keep audits informational
Some checks failed
CICD / Build and Push CICD Images (pull_request) Failing after 1m4s
CICD / Source Checks (pull_request) Has been skipped
CICD / Dependency Audits (Informational) (pull_request) Has been skipped
CICD / Build Release Images (pull_request) Has been skipped
CICD / Build Tester Images (pull_request) Has been skipped
CICD / Build CICD Image Failure Postmortem (pull_request) Successful in 11s
CICD / CICD Tests Complete (pull_request) Failing after 6s
CICD / Production Images Complete (pull_request) Failing after 7s
CICD / Runtime Black-Box Integration Tests (pull_request) Has been skipped
CICD / Production Image Failures Postmortem (pull_request) Has been skipped
CICD / End-to-End Tests (pull_request) Has been skipped
CICD / Promote Staging Images To Release (pull_request) Has been skipped
CICD / Source Lanes Failure Postmortem (pull_request) Has been skipped
CICD / Integration Tests Failure Postmortem (pull_request) Has been skipped
CICD / E2E Tests Failure Postmortem (pull_request) Has been skipped

This commit is contained in:
copilotcoder
2026-07-17 12:17:38 -04:00
parent 8318294e0b
commit f5f5c75949
3 changed files with 118 additions and 38 deletions

View File

@@ -788,6 +788,7 @@ jobs:
dependency-audits:
name: Dependency Audits (Informational)
runs-on: ubuntu-act
continue-on-error: true
timeout-minutes: 15
needs: build_cicd
steps:
@@ -2465,9 +2466,13 @@ jobs:
outputs:
release_version: ${{ steps.promote.outputs.release_version }}
release_short_sha: ${{ steps.promote.outputs.release_short_sha }}
release_tagged_version: ${{ steps.promote.outputs.release_tagged_version }}
previous_release_version: ${{ steps.promote.outputs.previous_release_version }}
backend_release_version_ref: ${{ steps.promote.outputs.backend_release_version_ref }}
backend_release_tagged_ref: ${{ steps.promote.outputs.backend_release_tagged_ref }}
backend_release_build_ref: ${{ steps.promote.outputs.backend_release_build_ref }}
frontend_release_version_ref: ${{ steps.promote.outputs.frontend_release_version_ref }}
frontend_release_tagged_ref: ${{ steps.promote.outputs.frontend_release_tagged_ref }}
frontend_release_build_ref: ${{ steps.promote.outputs.frontend_release_build_ref }}
steps:
- *identify_runner_step
@@ -2485,7 +2490,32 @@ jobs:
run: |
set -euo pipefail
umask 077
trap 'rm -f /tmp/release_ssh_key' EXIT
TMP_REPO_DIR="$(mktemp -d)"
trap 'rm -f /tmp/release_ssh_key; rm -rf "${TMP_REPO_DIR}"' EXIT
retry_cmd() {
attempts="${1:-5}"
backoff="${2:-2}"
shift 2
attempt=1
while [ "${attempt}" -le "${attempts}" ]; do
if "$@"; then
return 0
fi
if [ "${attempt}" -lt "${attempts}" ]; then
sleep_seconds=$((backoff * attempt))
echo "Command failed (attempt ${attempt}/${attempts}): $*"
echo "Retrying in ${sleep_seconds}s"
sleep "${sleep_seconds}"
fi
attempt=$((attempt + 1))
done
echo "Command failed after ${attempts} attempts: $*"
return 1
}
docker_login_with_retry() {
attempts="${1:-5}"
@@ -2555,50 +2585,88 @@ jobs:
| sort -V \
| tail -n 1)"
if [ -n "${HEAD_SEMVER_TAG}" ]; then
RELEASE_VERSION="${HEAD_SEMVER_TAG}"
else
BASE_TAG="$({ GIT_SSH_COMMAND="${GIT_SSH_COMMAND}" git ls-remote --tags --refs "${GITEA_REPO_SSH_URL}" 2>/dev/null || true; } \
| awk '{sub("refs/tags/", "", $2); print $2}' \
| grep -E '^v[0-9]+\.[0-9]+\.[0-9]+$' \
| sort -V \
| tail -n 1)"
if [ -z "${BASE_TAG}" ]; then
BASE_TAG="v0.0.0"
fi
BASE_MAJOR="$(printf '%s' "${BASE_TAG}" | sed -E 's/^v([0-9]+)\.([0-9]+)\.([0-9]+)$/\1/')"
BASE_MINOR="$(printf '%s' "${BASE_TAG}" | sed -E 's/^v([0-9]+)\.([0-9]+)\.([0-9]+)$/\2/')"
BASE_PATCH="$(printf '%s' "${BASE_TAG}" | sed -E 's/^v([0-9]+)\.([0-9]+)\.([0-9]+)$/\3/')"
EXISTING_TAGS_JSON="$({ curl -fsSL -u "${REGISTRY_USER}:${PACKAGE_ACCESS_TOKEN}" "http://${GITEA_REGISTRY}/v2/darkhelm.org/deployable-backend/tags/list" 2>/dev/null || true; })"
MAX_PATCH_FOR_LINE="${BASE_PATCH}"
if [ -n "${EXISTING_TAGS_JSON}" ] && command -v python3 >/dev/null 2>&1; then
REGISTRY_MAX_PATCH="$(printf '%s' "${EXISTING_TAGS_JSON}" | python3 -c "import json,re,sys; data=json.load(sys.stdin); tags=data.get('tags') or []; major=int(sys.argv[1]); minor=int(sys.argv[2]); patches=[int(m.group(3)) for tag in tags for m in [re.match(r'^v(\\d+)\\.(\\d+)\\.(\\d+)$', str(tag))] if m and int(m.group(1))==major and int(m.group(2))==minor]; print(max(patches) if patches else -1)" "${BASE_MAJOR}" "${BASE_MINOR}")"
if [ "${REGISTRY_MAX_PATCH}" -ge "${MAX_PATCH_FOR_LINE}" ]; then
MAX_PATCH_FOR_LINE="${REGISTRY_MAX_PATCH}"
fi
fi
NEXT_PATCH="$((MAX_PATCH_FOR_LINE + 1))"
RELEASE_VERSION="v${BASE_MAJOR}.${BASE_MINOR}.${NEXT_PATCH}"
if [ -z "${HEAD_SEMVER_TAG}" ]; then
echo "No semver tag found on HEAD (${HEAD_SHA}); skipping release promotion and release notes generation."
echo "release_version=" >> "$GITHUB_OUTPUT"
echo "release_short_sha=${SHORT_SHA}" >> "$GITHUB_OUTPUT"
echo "release_tagged_version=" >> "$GITHUB_OUTPUT"
echo "previous_release_version=" >> "$GITHUB_OUTPUT"
echo "backend_release_version_ref=" >> "$GITHUB_OUTPUT"
echo "backend_release_tagged_ref=" >> "$GITHUB_OUTPUT"
echo "backend_release_build_ref=" >> "$GITHUB_OUTPUT"
echo "frontend_release_version_ref=" >> "$GITHUB_OUTPUT"
echo "frontend_release_tagged_ref=" >> "$GITHUB_OUTPUT"
echo "frontend_release_build_ref=" >> "$GITHUB_OUTPUT"
exit 0
fi
RELEASE_BUILD_TAG="${RELEASE_VERSION}-${SHORT_SHA}"
TAG_MAJOR="$(printf '%s' "${HEAD_SEMVER_TAG}" | sed -E 's/^v([0-9]+)\.([0-9]+)\.([0-9]+)$/\1/')"
TAG_MINOR="$(printf '%s' "${HEAD_SEMVER_TAG}" | sed -E 's/^v([0-9]+)\.([0-9]+)\.([0-9]+)$/\2/')"
RELEASE_VERSION="v${TAG_MAJOR}.${TAG_MINOR}.0"
RELEASE_TAGGED_VERSION="${HEAD_SEMVER_TAG}"
RELEASE_BUILD_TAG="${RELEASE_TAGGED_VERSION}-${SHORT_SHA}"
RELEASE_TAG_LIST="$({ GIT_SSH_COMMAND="${GIT_SSH_COMMAND}" git ls-remote --tags --refs "${GITEA_REPO_SSH_URL}" 2>/dev/null || true; } \
| awk '{sub("refs/tags/", "", $2); print $2}' \
| grep -E '^v[0-9]+\.[0-9]+\.0$' \
| sort -V)"
PREVIOUS_RELEASE_VERSION="$(printf '%s\n' "${RELEASE_TAG_LIST}" | awk -v current="${RELEASE_VERSION}" 'NF {if ($0 == current) {print prev; found=1; exit} prev=$0} END {if (!found) print prev}')"
RELEASE_BACKEND_REPO="${GITEA_REGISTRY}/darkhelm.org/deployable-backend"
RELEASE_FRONTEND_REPO="${GITEA_REGISTRY}/darkhelm.org/deployable-frontend"
BACKEND_RELEASE_VERSION_REF="${RELEASE_BACKEND_REPO}:${RELEASE_VERSION}"
BACKEND_RELEASE_TAGGED_REF="${RELEASE_BACKEND_REPO}:${RELEASE_TAGGED_VERSION}"
BACKEND_RELEASE_BUILD_REF="${RELEASE_BACKEND_REPO}:${RELEASE_BUILD_TAG}"
BACKEND_RELEASE_LATEST_REF="${RELEASE_BACKEND_REPO}:latest"
FRONTEND_RELEASE_VERSION_REF="${RELEASE_FRONTEND_REPO}:${RELEASE_VERSION}"
FRONTEND_RELEASE_TAGGED_REF="${RELEASE_FRONTEND_REPO}:${RELEASE_TAGGED_VERSION}"
FRONTEND_RELEASE_BUILD_REF="${RELEASE_FRONTEND_REPO}:${RELEASE_BUILD_TAG}"
FRONTEND_RELEASE_LATEST_REF="${RELEASE_FRONTEND_REPO}:latest"
retry_cmd 5 2 git -C "${TMP_REPO_DIR}" init -q
retry_cmd 5 3 env GIT_SSH_COMMAND="${GIT_SSH_COMMAND}" git -C "${TMP_REPO_DIR}" remote add origin "${GITEA_REPO_SSH_URL}"
retry_cmd 5 3 env GIT_SSH_COMMAND="${GIT_SSH_COMMAND}" git -C "${TMP_REPO_DIR}" fetch --no-tags --depth 200 origin "${HEAD_SHA}"
retry_cmd 5 3 env GIT_SSH_COMMAND="${GIT_SSH_COMMAND}" git -C "${TMP_REPO_DIR}" fetch --tags --force origin
RELEASE_NOTES_FILE="$(mktemp)"
if [ -n "${PREVIOUS_RELEASE_VERSION}" ]; then
LOG_RANGE="${PREVIOUS_RELEASE_VERSION}..${HEAD_SHA}"
CHANGES_HEADER="Changes since previous release ${PREVIOUS_RELEASE_VERSION}:"
else
LOG_RANGE="${HEAD_SHA}"
CHANGES_HEADER="Changes included in this initial tracked release:"
fi
{
echo "## Release ${RELEASE_VERSION}"
echo
echo "- Release series tag: ${RELEASE_VERSION}"
echo "- Version tag on commit: ${RELEASE_TAGGED_VERSION}"
echo "- Build tag: ${RELEASE_BUILD_TAG}"
if [ -n "${PREVIOUS_RELEASE_VERSION}" ]; then
echo "- Previous release: ${PREVIOUS_RELEASE_VERSION}"
else
echo "- Previous release: (none)"
fi
echo
echo "${CHANGES_HEADER}"
git -C "${TMP_REPO_DIR}" log --no-merges --pretty='- %s (%h)' "${LOG_RANGE}" || true
} > "${RELEASE_NOTES_FILE}"
if ! grep -q '^- ' "${RELEASE_NOTES_FILE}"; then
{
echo
echo "- No commit-summary entries were found in the selected range."
} >> "${RELEASE_NOTES_FILE}"
fi
{
echo "### Generated Release Notes"
cat "${RELEASE_NOTES_FILE}"
} >> "$GITHUB_STEP_SUMMARY"
docker_login_with_retry 5 3
retry_registry_op pull "${STAGING_BACKEND_DIGEST_REF}" 5 3
retry_registry_op pull "${STAGING_FRONTEND_DIGEST_REF}" 5 3
@@ -2607,24 +2675,32 @@ jobs:
FRONTEND_IMAGE_ID="$(docker image inspect --format '{{.Id}}' "${STAGING_FRONTEND_DIGEST_REF}")"
docker tag "${BACKEND_IMAGE_ID}" "${BACKEND_RELEASE_VERSION_REF}"
docker tag "${BACKEND_IMAGE_ID}" "${BACKEND_RELEASE_TAGGED_REF}"
docker tag "${BACKEND_IMAGE_ID}" "${BACKEND_RELEASE_BUILD_REF}"
docker tag "${BACKEND_IMAGE_ID}" "${BACKEND_RELEASE_LATEST_REF}"
docker tag "${FRONTEND_IMAGE_ID}" "${FRONTEND_RELEASE_VERSION_REF}"
docker tag "${FRONTEND_IMAGE_ID}" "${FRONTEND_RELEASE_TAGGED_REF}"
docker tag "${FRONTEND_IMAGE_ID}" "${FRONTEND_RELEASE_BUILD_REF}"
docker tag "${FRONTEND_IMAGE_ID}" "${FRONTEND_RELEASE_LATEST_REF}"
retry_registry_op push "${BACKEND_RELEASE_VERSION_REF}" 5 4
retry_registry_op push "${BACKEND_RELEASE_TAGGED_REF}" 5 4
retry_registry_op push "${BACKEND_RELEASE_BUILD_REF}" 5 4
retry_registry_op push "${BACKEND_RELEASE_LATEST_REF}" 5 4
retry_registry_op push "${FRONTEND_RELEASE_VERSION_REF}" 5 4
retry_registry_op push "${FRONTEND_RELEASE_TAGGED_REF}" 5 4
retry_registry_op push "${FRONTEND_RELEASE_BUILD_REF}" 5 4
retry_registry_op push "${FRONTEND_RELEASE_LATEST_REF}" 5 4
echo "release_version=${RELEASE_VERSION}" >> "$GITHUB_OUTPUT"
echo "release_short_sha=${SHORT_SHA}" >> "$GITHUB_OUTPUT"
echo "release_tagged_version=${RELEASE_TAGGED_VERSION}" >> "$GITHUB_OUTPUT"
echo "previous_release_version=${PREVIOUS_RELEASE_VERSION}" >> "$GITHUB_OUTPUT"
echo "backend_release_version_ref=${BACKEND_RELEASE_VERSION_REF}" >> "$GITHUB_OUTPUT"
echo "backend_release_tagged_ref=${BACKEND_RELEASE_TAGGED_REF}" >> "$GITHUB_OUTPUT"
echo "backend_release_build_ref=${BACKEND_RELEASE_BUILD_REF}" >> "$GITHUB_OUTPUT"
echo "frontend_release_version_ref=${FRONTEND_RELEASE_VERSION_REF}" >> "$GITHUB_OUTPUT"
echo "frontend_release_tagged_ref=${FRONTEND_RELEASE_TAGGED_REF}" >> "$GITHUB_OUTPUT"
echo "frontend_release_build_ref=${FRONTEND_RELEASE_BUILD_REF}" >> "$GITHUB_OUTPUT"
- *failure_diagnostics_step

View File

@@ -171,16 +171,19 @@ jobs:
promote-release-images:
needs: [build_cicd, build-release-images, integration-tests, e2e-tests]
# retags staging images to deployable-backend/deployable-frontend with:
# - release version tag (semver)
# - release build tag (<semver>-<7-char-short-sha>)
# - release series tag (<major>.<minor>.0)
# - exact git semver tag from HEAD (authoritative release tag)
# - release build tag (<git-semver>-<7-char-short-sha>)
# - latest
```
### Release Tagging Model
- If the current commit has a semver git tag (`v<major>.<minor>.<patch>`), promotion uses that exact version.
- Otherwise, promotion finds the latest semver baseline and auto-increments patch for the next successful release in that major/minor line.
- Each promoted release also publishes a build-distinguishing tag: `<semver>-<short_sha>`, where `<short_sha>` is the 7-character commit shorthand.
- Promotion is release-tag driven: if HEAD has no semver git tag (`v<major>.<minor>.<patch>`), promotion is skipped.
- Release identity is derived from the git tag on HEAD.
- Release series tag is normalized to `<major>.<minor>.0`.
- Each promoted release also publishes a build-distinguishing tag: `<git-semver>-<short_sha>`, where `<short_sha>` is the 7-character commit shorthand.
- Promotion emits generated release notes summarizing commit subjects since the previous `<major>.<minor>.0` release tag.
### Responsibility Split

View File

@@ -415,7 +415,7 @@ The pipeline is intentionally staged so expensive image jobs run only after sour
4. Runtime validation lanes:
`Runtime Black-Box Integration Tests` and `End-to-End Tests` validate staged runtime artifacts.
5. Promotion lane:
`Promote Release Images` retags validated staging artifacts to release repos (`deployable-backend`, `deployable-frontend`) with `latest`, `<semver>`, and `<semver>-<7-char-short-sha>` tags.
`Promote Release Images` runs only when HEAD has a semver git tag and retags validated staging artifacts to release repos (`deployable-backend`, `deployable-frontend`) with `latest`, `<major>.<minor>.0`, `<tagged-semver>`, and `<tagged-semver>-<7-char-short-sha>` tags.
6. Postmortem lanes:
targeted postmortem jobs run when key lanes fail to capture diagnostics even when primary jobs fail early.
@@ -426,6 +426,7 @@ Current workflow behavior includes:
- registry auth realm host pinning from `WWW-Authenticate` challenge when registry tokens are issued from a different host
- bounded retry logic for docker login/pull/push operations in image lanes
- digest/tag contract checks for deployable image references before runtime black-box tests
- release-note summary generation in the promotion lane (commit bullets since previous `<major>.<minor>.0` release tag)
- context hydration for image-build lanes by copying `/workspace` from the published CICD image
- runner split between `ubuntu-act` and `ubuntu-act-8gb` based on lane resource requirements