Compare commits
2
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
57cb7a2aca
|
||
|
|
f1d26b69d7 |
No files matched your search
@@ -416,7 +416,18 @@ checkout
|
||||
--> sast-scan (parallel)
|
||||
```
|
||||
|
||||
For `release-candidate` workflows, `docker-build-push` runs after all quality checks pass, followed by `sbom`, `update-cd-repo` (targeting staging), and `github-release` (marked as pre-release). For `production` workflows, `docker-build-push` runs directly after `build-and-test` (no quality gates -- those were enforced on the release branch), followed by `update-cd-repo` (targeting prod) and `github-release` (full release). The `github-release` job auto-creates an annotated git tag and GitHub Release using the version calculated by GitVersion.
|
||||
For `release-candidate` workflows, `docker-build-push` runs after all quality checks pass, followed by `sbom`, `update-cd-repo` (targeting staging), and `github-release` (marked as pre-release). For `production` workflows, `docker-build-push` runs directly after `build-and-test` (no quality gates -- those were enforced on the release branch), followed by `update-cd-repo` (targeting prod) and `github-release` (full release). The `github-release` job creates the GitHub Release for the version tag reserved during `build-and-test` (see Release version integrity).
|
||||
|
||||
### Release version integrity
|
||||
|
||||
Production versions on `main` are unique per commit, even when merges land minutes apart:
|
||||
|
||||
- **Atomic reservation** -- `build-and-test` on `main` reserves `vX.Y.Z` by creating the git tag through the GitHub API before the version is baked into artifacts. Tag creation is create-if-absent, so concurrent pipelines serialize on it: if another commit already owns the tag, the build takes the next patch version. A rerun of the same commit reuses its tag. Existing tags are never moved. Requires the GitHub context on `build-and-test` (present in the generated `production` workflows).
|
||||
- **Immutable image tags** -- images carry an `org.opencontainers.image.revision` label. `docker-build-push` refuses to push a version tag (main or supplemental) that already exists for a different commit, and fails closed if the registry cannot be inspected. Environment tags such as `develop` stay mutable.
|
||||
- **Ownership-checked GitOps bump** -- `update-cd-repo` verifies that `vX.Y.Z` points at the pipeline's commit before writing the version to the CD repository.
|
||||
- **Release targets the built commit** -- `github-release` targets `CIRCLE_SHA1`, not the moving `main` head, and refuses if the tag points elsewhere.
|
||||
|
||||
`cd_path` is rendered into generated pipelines only when `cd.path` is set, so manifests that pin an older `orb_version` keep validating.
|
||||
|
||||
## Release Workflow
|
||||
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
description: >
|
||||
Atomically reserve the release version on main by creating the v<version>
|
||||
git tag through the GitHub API. If another commit already owns the tag
|
||||
(concurrent merges), the next patch version is reserved instead. Existing
|
||||
tags are never moved. No-op on non-main branches and pre-release versions.
|
||||
steps:
|
||||
- run:
|
||||
name: Reserve release version (atomic git tag)
|
||||
command: << include(scripts/allocate_version.sh) >>
|
||||
@@ -0,0 +1,34 @@
|
||||
description: >
|
||||
Refuse to overwrite immutable version tags. Fails when the main image or any
|
||||
supplemental image already exists at this version for a different commit
|
||||
(org.opencontainers.image.revision label). Reruns from the same commit pass.
|
||||
parameters:
|
||||
image_name:
|
||||
type: string
|
||||
description: Docker image name (without registry prefix).
|
||||
registry:
|
||||
type: string
|
||||
default: ghcr.io/KofTwentyTwo
|
||||
description: Container registry URL.
|
||||
supplemental_images:
|
||||
type: string
|
||||
default: "[]"
|
||||
description: JSON array of supplemental image definitions.
|
||||
ghcr_token_env:
|
||||
type: env_var_name
|
||||
default: GHCR_TOKEN
|
||||
description: Environment variable containing the GHCR token.
|
||||
ghcr_user_env:
|
||||
type: env_var_name
|
||||
default: GHCR_USERNAME
|
||||
description: Environment variable containing the GHCR username.
|
||||
steps:
|
||||
- run:
|
||||
name: Guard immutable version tags
|
||||
environment:
|
||||
IMAGE_NAME: << parameters.image_name >>
|
||||
REGISTRY: << parameters.registry >>
|
||||
SUPPLEMENTAL_IMAGES_JSON: << parameters.supplemental_images >>
|
||||
GHCR_TOKEN_VAR: << parameters.ghcr_token_env >>
|
||||
GHCR_USER_VAR: << parameters.ghcr_user_env >>
|
||||
command: << include(scripts/guard_image_tags.sh) >>
|
||||
@@ -53,6 +53,10 @@ steps:
|
||||
- run:
|
||||
name: Restore version metadata
|
||||
command: <<include(scripts/restore_version_metadata.sh)>>
|
||||
- guard_image_tags:
|
||||
image_name: << parameters.image_name >>
|
||||
registry: << parameters.registry >>
|
||||
supplemental_images: << parameters.supplemental_images >>
|
||||
- generate_dockerfile
|
||||
- docker_build:
|
||||
image_name: << parameters.image_name >>
|
||||
|
||||
@@ -25,6 +25,7 @@ steps:
|
||||
- restore_gradle_cache
|
||||
- calculate_version
|
||||
- export_version_vars
|
||||
- allocate_version
|
||||
- gradle_build
|
||||
- gradle_test
|
||||
- run:
|
||||
|
||||
@@ -25,6 +25,7 @@ steps:
|
||||
- restore_maven_cache
|
||||
- calculate_version
|
||||
- export_version_vars
|
||||
- allocate_version
|
||||
- mvn_set_version
|
||||
- mvn_build
|
||||
- mvn_test
|
||||
|
||||
@@ -73,6 +73,7 @@ steps:
|
||||
test_setup: << parameters.test_setup >>
|
||||
- calculate_version
|
||||
- export_version_vars
|
||||
- allocate_version
|
||||
- run:
|
||||
name: Persist version metadata
|
||||
command: <<include(scripts/persist_version_metadata.sh)>>
|
||||
|
||||
@@ -0,0 +1,161 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
# Source shared helpers
|
||||
MUNITOR_HELPERS="${MUNITOR_HELPERS:-$(cd "$(dirname "${BASH_SOURCE[0]:-$0}")" && pwd)/munitor_helpers.sh}"
|
||||
# shellcheck source=munitor_helpers.sh
|
||||
if [[ -f "${MUNITOR_HELPERS}" ]]; then source "${MUNITOR_HELPERS}"
|
||||
elif ! type munitor_header &>/dev/null; then
|
||||
munitor_header() { echo "=== Munitor: ${1:-unknown} ==="; }
|
||||
munitor_check_tool() { command -v "$1" &>/dev/null || { echo "ERROR: $1 not found"; exit 1; }; }
|
||||
munitor_download_with_retry() { curl -fsSL --retry 3 "$1" -o "$2"; }
|
||||
fi
|
||||
|
||||
munitor_header "allocate_version"
|
||||
|
||||
# Atomically reserve the release version for this commit.
|
||||
#
|
||||
# GitVersion derives the next version from the tags it can see, so two main
|
||||
# merges that build concurrently both compute the same version. The reservation
|
||||
# is the git tag itself: GitHub creates refs/tags/v<version> only if it does not
|
||||
# exist, which serializes concurrent pipelines without a lock service.
|
||||
# - created -> this commit owns the version
|
||||
# - exists on this commit -> rerun of the same pipeline; reuse it
|
||||
# - exists on another -> taken; try the next patch version
|
||||
# An existing tag is never moved or deleted.
|
||||
|
||||
VERSION="${PROJECT_VERSION:?PROJECT_VERSION not set -- run export_version_vars first}"
|
||||
BRANCH="${CIRCLE_BRANCH:-}"
|
||||
|
||||
if [[ "${BRANCH}" != "main" ]]; then
|
||||
echo "Branch '${BRANCH:-none}' is not main; version ${VERSION} needs no reservation."
|
||||
exit 0
|
||||
fi
|
||||
|
||||
if [[ ! "${VERSION}" =~ ^([0-9]+)\.([0-9]+)\.([0-9]+)$ ]]; then
|
||||
echo "Version ${VERSION} is not a release version; no reservation needed."
|
||||
exit 0
|
||||
fi
|
||||
|
||||
MAJOR="${BASH_REMATCH[1]}"
|
||||
MINOR="${BASH_REMATCH[2]}"
|
||||
PATCH="${BASH_REMATCH[3]}"
|
||||
|
||||
SHA="${CIRCLE_SHA1:?CIRCLE_SHA1 not set -- cannot reserve a version without the commit SHA}"
|
||||
REPO="${MUNITOR_SOURCE_REPO:-${CIRCLE_PROJECT_USERNAME:-}/${CIRCLE_PROJECT_REPONAME:-}}"
|
||||
MAX_ATTEMPTS="${MUNITOR_VERSION_ALLOC_ATTEMPTS:-25}"
|
||||
|
||||
if [[ "${REPO}" == "/" || "${REPO}" == */ || "${REPO}" == /* ]]; then
|
||||
echo "ERROR: Cannot determine the source repository (CIRCLE_PROJECT_USERNAME/CIRCLE_PROJECT_REPONAME)."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [[ -z "${MUNITOR_GH_API_CMD:-}" && -z "${GITHUB_TOKEN:-}" ]]; then
|
||||
echo "ERROR: GITHUB_TOKEN is not set; release versions on main must be reserved as git tags."
|
||||
echo "Add the GitHub context to the build-and-test job."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# gh_api METHOD PATH [JSON_BODY]
|
||||
# Prints the response body. Returns 0 on 2xx, 2 on 404, 3 on 409/422, 1 otherwise.
|
||||
# MUNITOR_GH_API_CMD substitutes the transport (used by tests).
|
||||
gh_api() {
|
||||
if [[ -n "${MUNITOR_GH_API_CMD:-}" ]]; then
|
||||
"${MUNITOR_GH_API_CMD}" "$@"
|
||||
return
|
||||
fi
|
||||
|
||||
local method="$1" path="$2" body="${3:-}"
|
||||
local out code
|
||||
out=$(mktemp)
|
||||
local args=(-sS -o "${out}" -w '%{http_code}' -X "${method}"
|
||||
-H "Authorization: Bearer ${GITHUB_TOKEN}"
|
||||
-H "Accept: application/vnd.github+json"
|
||||
-H "X-GitHub-Api-Version: 2022-11-28")
|
||||
if [[ -n "${body}" ]]; then
|
||||
args+=(-H "Content-Type: application/json" -d "${body}")
|
||||
fi
|
||||
|
||||
if ! code=$(curl "${args[@]}" "https://api.github.com/repos/${REPO}/${path}"); then
|
||||
cat "${out}"
|
||||
rm -f "${out}"
|
||||
return 1
|
||||
fi
|
||||
cat "${out}"
|
||||
rm -f "${out}"
|
||||
|
||||
case "${code}" in
|
||||
2??) return 0 ;;
|
||||
404) return 2 ;;
|
||||
409|422) return 3 ;;
|
||||
*) echo "GitHub API ${method} ${path} returned HTTP ${code}" >&2; return 1 ;;
|
||||
esac
|
||||
}
|
||||
|
||||
# tag_commit TAG -> prints the commit SHA the tag points at (dereferences annotated tags)
|
||||
tag_commit() {
|
||||
local tag="$1" ref obj_type obj_sha rc=0
|
||||
ref=$(gh_api GET "git/ref/tags/${tag}") || rc=$?
|
||||
if [[ ${rc} -ne 0 ]]; then
|
||||
echo "ERROR: Could not read tag ${tag} (rc=${rc}): ${ref}" >&2
|
||||
return 1
|
||||
fi
|
||||
obj_type=$(echo "${ref}" | jq -r '.object.type')
|
||||
obj_sha=$(echo "${ref}" | jq -r '.object.sha')
|
||||
if [[ "${obj_type}" == "tag" ]]; then
|
||||
rc=0
|
||||
ref=$(gh_api GET "git/tags/${obj_sha}") || rc=$?
|
||||
if [[ ${rc} -ne 0 ]]; then
|
||||
echo "ERROR: Could not dereference annotated tag ${tag} (rc=${rc})" >&2
|
||||
return 1
|
||||
fi
|
||||
obj_sha=$(echo "${ref}" | jq -r '.object.sha')
|
||||
fi
|
||||
echo "${obj_sha}"
|
||||
}
|
||||
|
||||
ALLOCATED=""
|
||||
for ((attempt = 1; attempt <= MAX_ATTEMPTS; attempt++)); do
|
||||
CANDIDATE="${MAJOR}.${MINOR}.${PATCH}"
|
||||
TAG="v${CANDIDATE}"
|
||||
rc=0
|
||||
response=$(gh_api POST "git/refs" "{\"ref\":\"refs/tags/${TAG}\",\"sha\":\"${SHA}\"}") || rc=$?
|
||||
|
||||
if [[ ${rc} -eq 0 ]]; then
|
||||
echo "Reserved ${TAG} for ${SHA}."
|
||||
ALLOCATED="${CANDIDATE}"
|
||||
break
|
||||
fi
|
||||
|
||||
if [[ ${rc} -ne 3 ]]; then
|
||||
echo "ERROR: Failed to create tag ${TAG} (rc=${rc}): ${response}"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
OWNER=$(tag_commit "${TAG}")
|
||||
if [[ "${OWNER}" == "${SHA}" ]]; then
|
||||
echo "${TAG} already points at ${SHA} (pipeline rerun); reusing it."
|
||||
ALLOCATED="${CANDIDATE}"
|
||||
break
|
||||
fi
|
||||
|
||||
echo "${TAG} is already taken by ${OWNER}; trying the next patch version."
|
||||
PATCH=$((PATCH + 1))
|
||||
done
|
||||
|
||||
if [[ -z "${ALLOCATED}" ]]; then
|
||||
echo "ERROR: Could not reserve a release version after ${MAX_ATTEMPTS} attempts (last tried ${TAG})."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [[ "${ALLOCATED}" != "${VERSION}" ]]; then
|
||||
echo "GitVersion computed ${VERSION}, but it belongs to another commit; this build is ${ALLOCATED}."
|
||||
fi
|
||||
|
||||
{
|
||||
echo "export PROJECT_VERSION='${ALLOCATED}'"
|
||||
echo "export GITVERSION_PATCH='${PATCH}'"
|
||||
echo "export MUNITOR_RELEASE_TAG='v${ALLOCATED}'"
|
||||
} >> "${BASH_ENV}"
|
||||
|
||||
echo "Release version: ${ALLOCATED} (tag v${ALLOCATED} -> ${SHA})"
|
||||
@@ -70,7 +70,11 @@ for i in $(seq 0 $((COUNT - 1))); do
|
||||
|
||||
# Build
|
||||
echo "Building ${FULL_TAG} ..."
|
||||
docker build -f "${DOCKERFILE}" -t "${FULL_TAG}" .
|
||||
BUILD_ARGS=(-f "${DOCKERFILE}" -t "${FULL_TAG}")
|
||||
if [[ -n "${CIRCLE_SHA1:-}" ]]; then
|
||||
BUILD_ARGS+=(--label "org.opencontainers.image.revision=${CIRCLE_SHA1}")
|
||||
fi
|
||||
docker build "${BUILD_ARGS[@]}" .
|
||||
|
||||
if [[ -n "${ENV_TAG}" ]]; then
|
||||
ENV_FULL_TAG="${REGISTRY}/${DERIVED_NAME}:${ENV_TAG}"
|
||||
|
||||
@@ -27,6 +27,11 @@ BUILD_CMD=(docker build -f "${DOCKERFILE}" -t "${FULL_IMAGE}:${VERSION}")
|
||||
if [[ -n "${ENV_TAG}" ]]; then
|
||||
BUILD_CMD+=(-t "${FULL_IMAGE}:${ENV_TAG}")
|
||||
fi
|
||||
if [[ -n "${CIRCLE_SHA1:-}" ]]; then
|
||||
# Provenance label: guard_image_tags and update_cd_repo use it to prove which
|
||||
# commit a version tag was built from.
|
||||
BUILD_CMD+=(--label "org.opencontainers.image.revision=${CIRCLE_SHA1}")
|
||||
fi
|
||||
BUILD_CMD+=(--build-arg "VERSION=${VERSION}" --build-arg "GIT_COMMIT_SHA=${CIRCLE_SHA1:-}" .)
|
||||
|
||||
"${BUILD_CMD[@]}"
|
||||
|
||||
@@ -206,6 +206,14 @@ extract_munitor_vars() {
|
||||
MUNITOR_CD_NON_PROD="false"
|
||||
fi
|
||||
|
||||
# cd_path is only emitted when set, so manifests without cd.path still render
|
||||
# pipelines that validate against orb versions predating the parameter.
|
||||
if [[ -n "${MUNITOR_CD_PATH}" ]]; then
|
||||
MUNITOR_CD_EXPLICIT_PATH="true"
|
||||
else
|
||||
MUNITOR_CD_EXPLICIT_PATH="false"
|
||||
fi
|
||||
|
||||
if [[ -n "${MUNITOR_CONTEXT_NVD}" ]]; then
|
||||
MUNITOR_NVD="true"
|
||||
else
|
||||
@@ -232,7 +240,7 @@ extract_munitor_vars() {
|
||||
export MUNITOR_CI_EMAIL MUNITOR_CI_NAME MUNITOR_NPM_DEFAULT_SCOPE MUNITOR_ORB_SLUG
|
||||
export MUNITOR_HEALTH_PATH MUNITOR_HEALTH_PORT MUNITOR_HEALTH_DB MUNITOR_HEALTH_MIGRATIONS MUNITOR_HEALTH_SMOKE_PATHS
|
||||
export MUNITOR_SUPPLEMENTAL MUNITOR_SUPPLEMENTAL_IMAGES_JSON
|
||||
export MUNITOR_SONAR MUNITOR_CD MUNITOR_CD_NON_PROD MUNITOR_NVD
|
||||
export MUNITOR_SONAR MUNITOR_CD MUNITOR_CD_NON_PROD MUNITOR_NVD MUNITOR_CD_EXPLICIT_PATH
|
||||
}
|
||||
|
||||
# Build the envsubst variable list
|
||||
|
||||
@@ -201,7 +201,7 @@ process_conditionals() {
|
||||
cp "${input}" "${tmpfile}"
|
||||
|
||||
# Process each conditional flag
|
||||
for flag in E2E SBOM SONAR NPM_AUTH SERVICES TEST_SETUP CUSTOM_TEST COVERAGE_CMD GITHUB_RELEASE SAST CD CD_NON_PROD OWASP NVD SUPPLEMENTAL; do
|
||||
for flag in E2E SBOM SONAR NPM_AUTH SERVICES TEST_SETUP CUSTOM_TEST COVERAGE_CMD GITHUB_RELEASE SAST CD CD_NON_PROD CD_EXPLICIT_PATH OWASP NVD SUPPLEMENTAL; do
|
||||
local var_name="MUNITOR_${flag}"
|
||||
local value="${!var_name:-false}"
|
||||
local flag_file
|
||||
|
||||
@@ -35,7 +35,9 @@ if [[ "${VERSION}" =~ -(RC|alpha|beta|SNAPSHOT) ]]; then
|
||||
TARGET="${CIRCLE_BRANCH:-main}"
|
||||
else
|
||||
PRERELEASE_FLAG=""
|
||||
TARGET="main"
|
||||
# Target the commit this pipeline built, never the moving branch head: a
|
||||
# later merge must not receive this version's tag.
|
||||
TARGET="${CIRCLE_SHA1:-main}"
|
||||
fi
|
||||
|
||||
echo "Target: ${TARGET}"
|
||||
@@ -44,6 +46,16 @@ echo "Pre-release: ${PRERELEASE_FLAG:-no}"
|
||||
# Create tag + GitHub Release via API (skip if already exists)
|
||||
# Uses gh release create which handles both tag and release in one call,
|
||||
# bypassing CircleCI's read-only SSH deploy key entirely.
|
||||
# The release tag is normally reserved by allocate_version. If it exists it
|
||||
# must point at this commit; an existing tag is never moved.
|
||||
if [[ -n "${CIRCLE_SHA1:-}" ]] && TAG_SHA=$(gh api "repos/{owner}/{repo}/commits/${TAG}" --jq '.sha' 2>/dev/null); then
|
||||
if [[ "${TAG_SHA}" != "${CIRCLE_SHA1}" ]]; then
|
||||
echo "ERROR: Tag ${TAG} already points at ${TAG_SHA}, not ${CIRCLE_SHA1}. Refusing to release."
|
||||
exit 1
|
||||
fi
|
||||
echo "Tag ${TAG} points at ${CIRCLE_SHA1}."
|
||||
fi
|
||||
|
||||
if gh release view "${TAG}" >/dev/null 2>&1; then
|
||||
echo "Release ${TAG} already exists, skipping."
|
||||
else
|
||||
|
||||
@@ -0,0 +1,87 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
# Source shared helpers
|
||||
MUNITOR_HELPERS="${MUNITOR_HELPERS:-$(cd "$(dirname "${BASH_SOURCE[0]:-$0}")" && pwd)/munitor_helpers.sh}"
|
||||
# shellcheck source=munitor_helpers.sh
|
||||
if [[ -f "${MUNITOR_HELPERS}" ]]; then source "${MUNITOR_HELPERS}"
|
||||
elif ! type munitor_header &>/dev/null; then
|
||||
munitor_header() { echo "=== Munitor: ${1:-unknown} ==="; }
|
||||
munitor_check_tool() { command -v "$1" &>/dev/null || { echo "ERROR: $1 not found"; exit 1; }; }
|
||||
munitor_download_with_retry() { curl -fsSL --retry 3 "$1" -o "$2"; }
|
||||
fi
|
||||
|
||||
munitor_header "guard_image_tags"
|
||||
|
||||
# Version tags are immutable. Before anything is pushed, verify that every
|
||||
# version tag this job will publish is either absent from the registry or was
|
||||
# built from this same commit (a rerun). A tag built from a different commit
|
||||
# means two pipelines claimed the same version; overwriting it would silently
|
||||
# change what an already-deployed tag points at, so fail instead.
|
||||
# Mutable environment tags (develop, staging) are not checked.
|
||||
|
||||
munitor_check_tool docker --version
|
||||
munitor_check_tool jq --version
|
||||
|
||||
IMAGE="${IMAGE_NAME:?IMAGE_NAME not set}"
|
||||
REGISTRY="${REGISTRY:-ghcr.io/KofTwentyTwo}"
|
||||
VERSION="${PROJECT_VERSION:?PROJECT_VERSION not set -- run export_version_vars first}"
|
||||
SHA="${CIRCLE_SHA1:?CIRCLE_SHA1 not set}"
|
||||
SUPP_JSON="${SUPPLEMENTAL_IMAGES_JSON:-[]}"
|
||||
REVISION_LABEL="org.opencontainers.image.revision"
|
||||
|
||||
if [[ -z "${MUNITOR_SKIP_REGISTRY_LOGIN:-}" ]]; then
|
||||
TOKEN="${!GHCR_TOKEN_VAR:-}"
|
||||
USER="${!GHCR_USER_VAR:-}"
|
||||
if [[ -z "${TOKEN}" || -z "${USER}" ]]; then
|
||||
echo "ERROR: GHCR credentials not set (\$${GHCR_TOKEN_VAR:-GHCR_TOKEN}, \$${GHCR_USER_VAR:-GHCR_USERNAME})"
|
||||
exit 1
|
||||
fi
|
||||
echo "${TOKEN}" | docker login "${REGISTRY%%/*}" -u "${USER}" --password-stdin
|
||||
fi
|
||||
|
||||
REFS=("${REGISTRY}/${IMAGE}:${VERSION}")
|
||||
if [[ "${SUPP_JSON}" != "[]" && "${SUPP_JSON}" != "null" && -n "${SUPP_JSON}" ]]; then
|
||||
while IFS= read -r name; do
|
||||
name="${name%$'\r'}"
|
||||
REFS+=("${REGISTRY}/${IMAGE}-${name}:${VERSION}")
|
||||
done < <(echo "${SUPP_JSON}" | jq -r '.[].name')
|
||||
fi
|
||||
|
||||
CONFLICTS=0
|
||||
for ref in "${REFS[@]}"; do
|
||||
err=$(mktemp)
|
||||
rc=0
|
||||
config=$(docker buildx imagetools inspect "${ref}" --format '{{json .Image}}' 2>"${err}") || rc=$?
|
||||
if [[ ${rc} -ne 0 ]]; then
|
||||
if grep -qiE 'not found|manifest unknown|MANIFEST_UNKNOWN|no such manifest' "${err}"; then
|
||||
echo "OK: ${ref} does not exist yet."
|
||||
rm -f "${err}"
|
||||
continue
|
||||
fi
|
||||
echo "ERROR: Could not inspect ${ref}; refusing to push without proving the tag is free:"
|
||||
cat "${err}"
|
||||
rm -f "${err}"
|
||||
exit 1
|
||||
fi
|
||||
rm -f "${err}"
|
||||
|
||||
# Single-platform images return one config; multi-platform images return a map of configs.
|
||||
revision=$(echo "${config}" | jq -r --arg l "${REVISION_LABEL}" \
|
||||
'[.. | objects | select(has("Labels")) | .Labels // {} | .[$l] // empty] | first // ""')
|
||||
|
||||
if [[ "${revision}" == "${SHA}" ]]; then
|
||||
echo "OK: ${ref} already exists for this commit (rerun); it will be republished from ${SHA}."
|
||||
else
|
||||
echo "ERROR: ${ref} already exists for a different commit (${revision:-unknown, no ${REVISION_LABEL} label})."
|
||||
CONFLICTS=$((CONFLICTS + 1))
|
||||
fi
|
||||
done
|
||||
|
||||
if [[ ${CONFLICTS} -gt 0 ]]; then
|
||||
echo "Refusing to overwrite ${CONFLICTS} immutable version tag(s). Version ${VERSION} was claimed by another build;"
|
||||
echo "rerun from a fresh pipeline so allocate_version reserves a new version."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "All ${#REFS[@]} version tag(s) are free for ${SHA}."
|
||||
@@ -303,6 +303,14 @@ extract_munitor_vars() {
|
||||
MUNITOR_CD_NON_PROD="false"
|
||||
fi
|
||||
|
||||
# cd_path is only emitted when set, so manifests without cd.path still render
|
||||
# pipelines that validate against orb versions predating the parameter.
|
||||
if [[ -n "${MUNITOR_CD_PATH}" ]]; then
|
||||
MUNITOR_CD_EXPLICIT_PATH="true"
|
||||
else
|
||||
MUNITOR_CD_EXPLICIT_PATH="false"
|
||||
fi
|
||||
|
||||
if [[ -n "${MUNITOR_CONTEXT_NVD}" ]]; then
|
||||
MUNITOR_NVD="true"
|
||||
else
|
||||
@@ -329,7 +337,7 @@ extract_munitor_vars() {
|
||||
export MUNITOR_CI_EMAIL MUNITOR_CI_NAME MUNITOR_NPM_DEFAULT_SCOPE MUNITOR_ORB_SLUG
|
||||
export MUNITOR_HEALTH_PATH MUNITOR_HEALTH_PORT MUNITOR_HEALTH_DB MUNITOR_HEALTH_MIGRATIONS MUNITOR_HEALTH_SMOKE_PATHS
|
||||
export MUNITOR_SUPPLEMENTAL MUNITOR_SUPPLEMENTAL_IMAGES_JSON
|
||||
export MUNITOR_SONAR MUNITOR_CD MUNITOR_CD_NON_PROD MUNITOR_NVD
|
||||
export MUNITOR_SONAR MUNITOR_CD MUNITOR_CD_NON_PROD MUNITOR_NVD MUNITOR_CD_EXPLICIT_PATH
|
||||
}
|
||||
|
||||
# Build the envsubst variable list
|
||||
@@ -572,7 +580,9 @@ workflows:
|
||||
environment: ${MUNITOR_CD_ENV_RELEASE}
|
||||
cd_format: ${MUNITOR_CD_FORMAT}
|
||||
cd_image_name: ${MUNITOR_DOCKER_REGISTRY}/${MUNITOR_IMAGE_NAME}
|
||||
##IF_CD_EXPLICIT_PATH##
|
||||
cd_path: ${MUNITOR_CD_PATH}
|
||||
##ENDIF_CD_EXPLICIT_PATH##
|
||||
##IF_SUPPLEMENTAL##
|
||||
supplemental_images: '${MUNITOR_SUPPLEMENTAL_IMAGES_JSON}'
|
||||
##ENDIF_SUPPLEMENTAL##
|
||||
@@ -636,7 +646,9 @@ workflows:
|
||||
environment: ${MUNITOR_CD_ENV_PROD}
|
||||
cd_format: ${MUNITOR_CD_FORMAT}
|
||||
cd_image_name: ${MUNITOR_DOCKER_REGISTRY}/${MUNITOR_IMAGE_NAME}
|
||||
##IF_CD_EXPLICIT_PATH##
|
||||
cd_path: ${MUNITOR_CD_PATH}
|
||||
##ENDIF_CD_EXPLICIT_PATH##
|
||||
##IF_SUPPLEMENTAL##
|
||||
supplemental_images: '${MUNITOR_SUPPLEMENTAL_IMAGES_JSON}'
|
||||
##ENDIF_SUPPLEMENTAL##
|
||||
@@ -896,7 +908,9 @@ workflows:
|
||||
environment: ${MUNITOR_CD_ENV_RELEASE}
|
||||
cd_format: ${MUNITOR_CD_FORMAT}
|
||||
cd_image_name: ${MUNITOR_DOCKER_REGISTRY}/${MUNITOR_IMAGE_NAME}
|
||||
##IF_CD_EXPLICIT_PATH##
|
||||
cd_path: ${MUNITOR_CD_PATH}
|
||||
##ENDIF_CD_EXPLICIT_PATH##
|
||||
##IF_SUPPLEMENTAL##
|
||||
supplemental_images: '${MUNITOR_SUPPLEMENTAL_IMAGES_JSON}'
|
||||
##ENDIF_SUPPLEMENTAL##
|
||||
@@ -960,7 +974,9 @@ workflows:
|
||||
environment: ${MUNITOR_CD_ENV_PROD}
|
||||
cd_format: ${MUNITOR_CD_FORMAT}
|
||||
cd_image_name: ${MUNITOR_DOCKER_REGISTRY}/${MUNITOR_IMAGE_NAME}
|
||||
##IF_CD_EXPLICIT_PATH##
|
||||
cd_path: ${MUNITOR_CD_PATH}
|
||||
##ENDIF_CD_EXPLICIT_PATH##
|
||||
##IF_SUPPLEMENTAL##
|
||||
supplemental_images: '${MUNITOR_SUPPLEMENTAL_IMAGES_JSON}'
|
||||
##ENDIF_SUPPLEMENTAL##
|
||||
@@ -1248,7 +1264,9 @@ workflows:
|
||||
environment: ${MUNITOR_CD_ENV_RELEASE}
|
||||
cd_format: ${MUNITOR_CD_FORMAT}
|
||||
cd_image_name: ${MUNITOR_DOCKER_REGISTRY}/${MUNITOR_IMAGE_NAME}
|
||||
##IF_CD_EXPLICIT_PATH##
|
||||
cd_path: ${MUNITOR_CD_PATH}
|
||||
##ENDIF_CD_EXPLICIT_PATH##
|
||||
##IF_SUPPLEMENTAL##
|
||||
supplemental_images: '${MUNITOR_SUPPLEMENTAL_IMAGES_JSON}'
|
||||
##ENDIF_SUPPLEMENTAL##
|
||||
@@ -1325,7 +1343,9 @@ workflows:
|
||||
environment: ${MUNITOR_CD_ENV_PROD}
|
||||
cd_format: ${MUNITOR_CD_FORMAT}
|
||||
cd_image_name: ${MUNITOR_DOCKER_REGISTRY}/${MUNITOR_IMAGE_NAME}
|
||||
##IF_CD_EXPLICIT_PATH##
|
||||
cd_path: ${MUNITOR_CD_PATH}
|
||||
##ENDIF_CD_EXPLICIT_PATH##
|
||||
##IF_SUPPLEMENTAL##
|
||||
supplemental_images: '${MUNITOR_SUPPLEMENTAL_IMAGES_JSON}'
|
||||
##ENDIF_SUPPLEMENTAL##
|
||||
@@ -1613,7 +1633,9 @@ workflows:
|
||||
environment: ${MUNITOR_CD_ENV_RELEASE}
|
||||
cd_format: ${MUNITOR_CD_FORMAT}
|
||||
cd_image_name: ${MUNITOR_DOCKER_REGISTRY}/${MUNITOR_IMAGE_NAME}
|
||||
##IF_CD_EXPLICIT_PATH##
|
||||
cd_path: ${MUNITOR_CD_PATH}
|
||||
##ENDIF_CD_EXPLICIT_PATH##
|
||||
##IF_SUPPLEMENTAL##
|
||||
supplemental_images: '${MUNITOR_SUPPLEMENTAL_IMAGES_JSON}'
|
||||
##ENDIF_SUPPLEMENTAL##
|
||||
@@ -1690,7 +1712,9 @@ workflows:
|
||||
environment: ${MUNITOR_CD_ENV_PROD}
|
||||
cd_format: ${MUNITOR_CD_FORMAT}
|
||||
cd_image_name: ${MUNITOR_DOCKER_REGISTRY}/${MUNITOR_IMAGE_NAME}
|
||||
##IF_CD_EXPLICIT_PATH##
|
||||
cd_path: ${MUNITOR_CD_PATH}
|
||||
##ENDIF_CD_EXPLICIT_PATH##
|
||||
##IF_SUPPLEMENTAL##
|
||||
supplemental_images: '${MUNITOR_SUPPLEMENTAL_IMAGES_JSON}'
|
||||
##ENDIF_SUPPLEMENTAL##
|
||||
@@ -2621,7 +2645,9 @@ cat > /tmp/munitor/templates/partials/java-webapp-deploy.yml.tpl << 'MUNITOR_PAR
|
||||
environment: __CD_ENVIRONMENT__
|
||||
cd_format: ${MUNITOR_CD_FORMAT}
|
||||
cd_image_name: ${MUNITOR_DOCKER_REGISTRY}/${MUNITOR_IMAGE_NAME}
|
||||
##IF_CD_EXPLICIT_PATH##
|
||||
cd_path: ${MUNITOR_CD_PATH}
|
||||
##ENDIF_CD_EXPLICIT_PATH##
|
||||
##IF_SUPPLEMENTAL##
|
||||
supplemental_images: '${MUNITOR_SUPPLEMENTAL_IMAGES_JSON}'
|
||||
##ENDIF_SUPPLEMENTAL##
|
||||
@@ -2771,7 +2797,9 @@ cat > /tmp/munitor/templates/partials/gradle-webapp-deploy.yml.tpl << 'MUNITOR_P
|
||||
environment: __CD_ENVIRONMENT__
|
||||
cd_format: ${MUNITOR_CD_FORMAT}
|
||||
cd_image_name: ${MUNITOR_DOCKER_REGISTRY}/${MUNITOR_IMAGE_NAME}
|
||||
##IF_CD_EXPLICIT_PATH##
|
||||
cd_path: ${MUNITOR_CD_PATH}
|
||||
##ENDIF_CD_EXPLICIT_PATH##
|
||||
##IF_SUPPLEMENTAL##
|
||||
supplemental_images: '${MUNITOR_SUPPLEMENTAL_IMAGES_JSON}'
|
||||
##ENDIF_SUPPLEMENTAL##
|
||||
@@ -2934,7 +2962,9 @@ cat > /tmp/munitor/templates/partials/node-api-deploy.yml.tpl << 'MUNITOR_PARTIA
|
||||
environment: __CD_ENVIRONMENT__
|
||||
cd_format: ${MUNITOR_CD_FORMAT}
|
||||
cd_image_name: ${MUNITOR_DOCKER_REGISTRY}/${MUNITOR_IMAGE_NAME}
|
||||
##IF_CD_EXPLICIT_PATH##
|
||||
cd_path: ${MUNITOR_CD_PATH}
|
||||
##ENDIF_CD_EXPLICIT_PATH##
|
||||
##IF_SUPPLEMENTAL##
|
||||
supplemental_images: '${MUNITOR_SUPPLEMENTAL_IMAGES_JSON}'
|
||||
##ENDIF_SUPPLEMENTAL##
|
||||
@@ -3097,7 +3127,9 @@ cat > /tmp/munitor/templates/partials/node-webapp-deploy.yml.tpl << 'MUNITOR_PAR
|
||||
environment: __CD_ENVIRONMENT__
|
||||
cd_format: ${MUNITOR_CD_FORMAT}
|
||||
cd_image_name: ${MUNITOR_DOCKER_REGISTRY}/${MUNITOR_IMAGE_NAME}
|
||||
##IF_CD_EXPLICIT_PATH##
|
||||
cd_path: ${MUNITOR_CD_PATH}
|
||||
##ENDIF_CD_EXPLICIT_PATH##
|
||||
##IF_SUPPLEMENTAL##
|
||||
supplemental_images: '${MUNITOR_SUPPLEMENTAL_IMAGES_JSON}'
|
||||
##ENDIF_SUPPLEMENTAL##
|
||||
@@ -3321,7 +3353,7 @@ process_conditionals() {
|
||||
cp "${input}" "${tmpfile}"
|
||||
|
||||
# Process each conditional flag
|
||||
for flag in E2E SBOM SONAR NPM_AUTH SERVICES TEST_SETUP CUSTOM_TEST COVERAGE_CMD GITHUB_RELEASE SAST CD CD_NON_PROD OWASP NVD SUPPLEMENTAL; do
|
||||
for flag in E2E SBOM SONAR NPM_AUTH SERVICES TEST_SETUP CUSTOM_TEST COVERAGE_CMD GITHUB_RELEASE SAST CD CD_NON_PROD CD_EXPLICIT_PATH OWASP NVD SUPPLEMENTAL; do
|
||||
local var_name="MUNITOR_${flag}"
|
||||
local value="${!var_name:-false}"
|
||||
local flag_file
|
||||
|
||||
@@ -303,6 +303,14 @@ extract_munitor_vars() {
|
||||
MUNITOR_CD_NON_PROD="false"
|
||||
fi
|
||||
|
||||
# cd_path is only emitted when set, so manifests without cd.path still render
|
||||
# pipelines that validate against orb versions predating the parameter.
|
||||
if [[ -n "${MUNITOR_CD_PATH}" ]]; then
|
||||
MUNITOR_CD_EXPLICIT_PATH="true"
|
||||
else
|
||||
MUNITOR_CD_EXPLICIT_PATH="false"
|
||||
fi
|
||||
|
||||
if [[ -n "${MUNITOR_CONTEXT_NVD}" ]]; then
|
||||
MUNITOR_NVD="true"
|
||||
else
|
||||
@@ -329,7 +337,7 @@ extract_munitor_vars() {
|
||||
export MUNITOR_CI_EMAIL MUNITOR_CI_NAME MUNITOR_NPM_DEFAULT_SCOPE MUNITOR_ORB_SLUG
|
||||
export MUNITOR_HEALTH_PATH MUNITOR_HEALTH_PORT MUNITOR_HEALTH_DB MUNITOR_HEALTH_MIGRATIONS MUNITOR_HEALTH_SMOKE_PATHS
|
||||
export MUNITOR_SUPPLEMENTAL MUNITOR_SUPPLEMENTAL_IMAGES_JSON
|
||||
export MUNITOR_SONAR MUNITOR_CD MUNITOR_CD_NON_PROD MUNITOR_NVD
|
||||
export MUNITOR_SONAR MUNITOR_CD MUNITOR_CD_NON_PROD MUNITOR_NVD MUNITOR_CD_EXPLICIT_PATH
|
||||
}
|
||||
|
||||
# Build the envsubst variable list
|
||||
|
||||
@@ -230,7 +230,9 @@ workflows:
|
||||
environment: ${MUNITOR_CD_ENV_RELEASE}
|
||||
cd_format: ${MUNITOR_CD_FORMAT}
|
||||
cd_image_name: ${MUNITOR_DOCKER_REGISTRY}/${MUNITOR_IMAGE_NAME}
|
||||
##IF_CD_EXPLICIT_PATH##
|
||||
cd_path: ${MUNITOR_CD_PATH}
|
||||
##ENDIF_CD_EXPLICIT_PATH##
|
||||
##IF_SUPPLEMENTAL##
|
||||
supplemental_images: '${MUNITOR_SUPPLEMENTAL_IMAGES_JSON}'
|
||||
##ENDIF_SUPPLEMENTAL##
|
||||
@@ -294,7 +296,9 @@ workflows:
|
||||
environment: ${MUNITOR_CD_ENV_PROD}
|
||||
cd_format: ${MUNITOR_CD_FORMAT}
|
||||
cd_image_name: ${MUNITOR_DOCKER_REGISTRY}/${MUNITOR_IMAGE_NAME}
|
||||
##IF_CD_EXPLICIT_PATH##
|
||||
cd_path: ${MUNITOR_CD_PATH}
|
||||
##ENDIF_CD_EXPLICIT_PATH##
|
||||
##IF_SUPPLEMENTAL##
|
||||
supplemental_images: '${MUNITOR_SUPPLEMENTAL_IMAGES_JSON}'
|
||||
##ENDIF_SUPPLEMENTAL##
|
||||
|
||||
@@ -230,7 +230,9 @@ workflows:
|
||||
environment: ${MUNITOR_CD_ENV_RELEASE}
|
||||
cd_format: ${MUNITOR_CD_FORMAT}
|
||||
cd_image_name: ${MUNITOR_DOCKER_REGISTRY}/${MUNITOR_IMAGE_NAME}
|
||||
##IF_CD_EXPLICIT_PATH##
|
||||
cd_path: ${MUNITOR_CD_PATH}
|
||||
##ENDIF_CD_EXPLICIT_PATH##
|
||||
##IF_SUPPLEMENTAL##
|
||||
supplemental_images: '${MUNITOR_SUPPLEMENTAL_IMAGES_JSON}'
|
||||
##ENDIF_SUPPLEMENTAL##
|
||||
@@ -294,7 +296,9 @@ workflows:
|
||||
environment: ${MUNITOR_CD_ENV_PROD}
|
||||
cd_format: ${MUNITOR_CD_FORMAT}
|
||||
cd_image_name: ${MUNITOR_DOCKER_REGISTRY}/${MUNITOR_IMAGE_NAME}
|
||||
##IF_CD_EXPLICIT_PATH##
|
||||
cd_path: ${MUNITOR_CD_PATH}
|
||||
##ENDIF_CD_EXPLICIT_PATH##
|
||||
##IF_SUPPLEMENTAL##
|
||||
supplemental_images: '${MUNITOR_SUPPLEMENTAL_IMAGES_JSON}'
|
||||
##ENDIF_SUPPLEMENTAL##
|
||||
|
||||
@@ -258,7 +258,9 @@ workflows:
|
||||
environment: ${MUNITOR_CD_ENV_RELEASE}
|
||||
cd_format: ${MUNITOR_CD_FORMAT}
|
||||
cd_image_name: ${MUNITOR_DOCKER_REGISTRY}/${MUNITOR_IMAGE_NAME}
|
||||
##IF_CD_EXPLICIT_PATH##
|
||||
cd_path: ${MUNITOR_CD_PATH}
|
||||
##ENDIF_CD_EXPLICIT_PATH##
|
||||
##IF_SUPPLEMENTAL##
|
||||
supplemental_images: '${MUNITOR_SUPPLEMENTAL_IMAGES_JSON}'
|
||||
##ENDIF_SUPPLEMENTAL##
|
||||
@@ -335,7 +337,9 @@ workflows:
|
||||
environment: ${MUNITOR_CD_ENV_PROD}
|
||||
cd_format: ${MUNITOR_CD_FORMAT}
|
||||
cd_image_name: ${MUNITOR_DOCKER_REGISTRY}/${MUNITOR_IMAGE_NAME}
|
||||
##IF_CD_EXPLICIT_PATH##
|
||||
cd_path: ${MUNITOR_CD_PATH}
|
||||
##ENDIF_CD_EXPLICIT_PATH##
|
||||
##IF_SUPPLEMENTAL##
|
||||
supplemental_images: '${MUNITOR_SUPPLEMENTAL_IMAGES_JSON}'
|
||||
##ENDIF_SUPPLEMENTAL##
|
||||
|
||||
@@ -258,7 +258,9 @@ workflows:
|
||||
environment: ${MUNITOR_CD_ENV_RELEASE}
|
||||
cd_format: ${MUNITOR_CD_FORMAT}
|
||||
cd_image_name: ${MUNITOR_DOCKER_REGISTRY}/${MUNITOR_IMAGE_NAME}
|
||||
##IF_CD_EXPLICIT_PATH##
|
||||
cd_path: ${MUNITOR_CD_PATH}
|
||||
##ENDIF_CD_EXPLICIT_PATH##
|
||||
##IF_SUPPLEMENTAL##
|
||||
supplemental_images: '${MUNITOR_SUPPLEMENTAL_IMAGES_JSON}'
|
||||
##ENDIF_SUPPLEMENTAL##
|
||||
@@ -335,7 +337,9 @@ workflows:
|
||||
environment: ${MUNITOR_CD_ENV_PROD}
|
||||
cd_format: ${MUNITOR_CD_FORMAT}
|
||||
cd_image_name: ${MUNITOR_DOCKER_REGISTRY}/${MUNITOR_IMAGE_NAME}
|
||||
##IF_CD_EXPLICIT_PATH##
|
||||
cd_path: ${MUNITOR_CD_PATH}
|
||||
##ENDIF_CD_EXPLICIT_PATH##
|
||||
##IF_SUPPLEMENTAL##
|
||||
supplemental_images: '${MUNITOR_SUPPLEMENTAL_IMAGES_JSON}'
|
||||
##ENDIF_SUPPLEMENTAL##
|
||||
|
||||
@@ -131,7 +131,9 @@
|
||||
environment: __CD_ENVIRONMENT__
|
||||
cd_format: ${MUNITOR_CD_FORMAT}
|
||||
cd_image_name: ${MUNITOR_DOCKER_REGISTRY}/${MUNITOR_IMAGE_NAME}
|
||||
##IF_CD_EXPLICIT_PATH##
|
||||
cd_path: ${MUNITOR_CD_PATH}
|
||||
##ENDIF_CD_EXPLICIT_PATH##
|
||||
##IF_SUPPLEMENTAL##
|
||||
supplemental_images: '${MUNITOR_SUPPLEMENTAL_IMAGES_JSON}'
|
||||
##ENDIF_SUPPLEMENTAL##
|
||||
|
||||
@@ -131,7 +131,9 @@
|
||||
environment: __CD_ENVIRONMENT__
|
||||
cd_format: ${MUNITOR_CD_FORMAT}
|
||||
cd_image_name: ${MUNITOR_DOCKER_REGISTRY}/${MUNITOR_IMAGE_NAME}
|
||||
##IF_CD_EXPLICIT_PATH##
|
||||
cd_path: ${MUNITOR_CD_PATH}
|
||||
##ENDIF_CD_EXPLICIT_PATH##
|
||||
##IF_SUPPLEMENTAL##
|
||||
supplemental_images: '${MUNITOR_SUPPLEMENTAL_IMAGES_JSON}'
|
||||
##ENDIF_SUPPLEMENTAL##
|
||||
|
||||
@@ -144,7 +144,9 @@
|
||||
environment: __CD_ENVIRONMENT__
|
||||
cd_format: ${MUNITOR_CD_FORMAT}
|
||||
cd_image_name: ${MUNITOR_DOCKER_REGISTRY}/${MUNITOR_IMAGE_NAME}
|
||||
##IF_CD_EXPLICIT_PATH##
|
||||
cd_path: ${MUNITOR_CD_PATH}
|
||||
##ENDIF_CD_EXPLICIT_PATH##
|
||||
##IF_SUPPLEMENTAL##
|
||||
supplemental_images: '${MUNITOR_SUPPLEMENTAL_IMAGES_JSON}'
|
||||
##ENDIF_SUPPLEMENTAL##
|
||||
|
||||
@@ -144,7 +144,9 @@
|
||||
environment: __CD_ENVIRONMENT__
|
||||
cd_format: ${MUNITOR_CD_FORMAT}
|
||||
cd_image_name: ${MUNITOR_DOCKER_REGISTRY}/${MUNITOR_IMAGE_NAME}
|
||||
##IF_CD_EXPLICIT_PATH##
|
||||
cd_path: ${MUNITOR_CD_PATH}
|
||||
##ENDIF_CD_EXPLICIT_PATH##
|
||||
##IF_SUPPLEMENTAL##
|
||||
supplemental_images: '${MUNITOR_SUPPLEMENTAL_IMAGES_JSON}'
|
||||
##ENDIF_SUPPLEMENTAL##
|
||||
|
||||
@@ -35,6 +35,31 @@ if [[ -z "${GITHUB_TOKEN:-}" ]]; then
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Release versions are owned by exactly one commit (allocate_version reserves the
|
||||
# v<version> tag atomically). Refuse to point the GitOps repo at a version that
|
||||
# belongs to a different commit: the image tag would not contain this build.
|
||||
if [[ "${CIRCLE_BRANCH:-}" == "main" && "${VERSION}" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ && -z "${MUNITOR_SKIP_TAG_OWNERSHIP_CHECK:-}" ]]; then
|
||||
SOURCE_REPO="${MUNITOR_SOURCE_REPO:-${CIRCLE_PROJECT_USERNAME:-}/${CIRCLE_PROJECT_REPONAME:-}}"
|
||||
SOURCE_SHA="${CIRCLE_SHA1:?CIRCLE_SHA1 not set}"
|
||||
github_get() {
|
||||
curl -fsS -H "Authorization: Bearer ${GITHUB_TOKEN}" -H "Accept: application/vnd.github+json" "https://api.github.com/repos/${SOURCE_REPO}/$1"
|
||||
}
|
||||
if ! TAG_REF=$(github_get "git/ref/tags/v${VERSION}"); then
|
||||
echo "ERROR: Release tag v${VERSION} does not exist in ${SOURCE_REPO}; the version was never reserved."
|
||||
exit 1
|
||||
fi
|
||||
TAG_SHA=$(echo "${TAG_REF}" | jq -r '.object.sha')
|
||||
if [[ "$(echo "${TAG_REF}" | jq -r '.object.type')" == "tag" ]]; then
|
||||
TAG_SHA=$(github_get "git/tags/${TAG_SHA}" | jq -r '.object.sha')
|
||||
fi
|
||||
if [[ "${TAG_SHA}" != "${SOURCE_SHA}" ]]; then
|
||||
echo "ERROR: Version ${VERSION} belongs to commit ${TAG_SHA}, not ${SOURCE_SHA}."
|
||||
echo "Refusing to bump the GitOps repo: the image tag ${VERSION} was not built from this commit."
|
||||
exit 1
|
||||
fi
|
||||
echo "Verified v${VERSION} -> ${SOURCE_SHA}."
|
||||
fi
|
||||
|
||||
echo "Updating CD repo: ${REPO} (format=${FORMAT}, version=${VERSION})"
|
||||
|
||||
# Clone the CD repo using explicit token auth
|
||||
@@ -151,7 +176,7 @@ if git diff --cached --quiet; then
|
||||
exit 0
|
||||
fi
|
||||
|
||||
git commit -m "chore: update ${IMAGE_NAME:-image} to ${VERSION}"
|
||||
git commit -m "chore: update ${IMAGE_NAME:-image} to ${VERSION}" -m "Source: ${CIRCLE_PROJECT_REPONAME:-unknown}@${CIRCLE_SHA1:-unknown}"
|
||||
|
||||
# Independent pipelines may update a shared GitOps repository concurrently.
|
||||
for attempt in 1 2 3; do
|
||||
|
||||
@@ -0,0 +1,182 @@
|
||||
#!/usr/bin/env bash
|
||||
# Tests for allocate_version.sh: atomic release version reservation via git tags.
|
||||
#
|
||||
# The GitHub refs API is replaced by a fake backed by a directory. Creating a
|
||||
# ref uses a hard link from a fully-written temp file, which is atomic and fails
|
||||
# if the target exists -- the same create-if-absent semantics GitHub provides
|
||||
# for POST /git/refs (422 "Reference already exists").
|
||||
set -euo pipefail
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
PROJECT_DIR="$(cd "${SCRIPT_DIR}/.." && pwd)"
|
||||
SCRIPT="${PROJECT_DIR}/src/scripts/allocate_version.sh"
|
||||
PASS=0
|
||||
FAIL=0
|
||||
|
||||
WORK=$(mktemp -d)
|
||||
trap 'rm -rf "${WORK}"' EXIT
|
||||
|
||||
pass() { echo "PASS"; PASS=$((PASS + 1)); }
|
||||
fail() { echo "FAIL${1:+ ($1)}"; FAIL=$((FAIL + 1)); }
|
||||
|
||||
# Fake GitHub API: gh_api METHOD PATH [BODY]; state lives in ${FAKE_GH_DIR}/tags/<tag>
|
||||
FAKE_API="${WORK}/fake_gh_api.sh"
|
||||
cat > "${FAKE_API}" <<'EOF'
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
method="$1" path="$2" body="${3:-}"
|
||||
dir="${FAKE_GH_DIR:?}/tags"
|
||||
mkdir -p "${dir}"
|
||||
if [[ -n "${FAKE_GH_FAIL:-}" ]]; then
|
||||
echo '{"message":"Server Error"}'
|
||||
exit 1
|
||||
fi
|
||||
case "${method} ${path}" in
|
||||
"POST git/refs")
|
||||
tag=$(echo "${body}" | jq -r '.ref' | sed 's#^refs/tags/##')
|
||||
sha=$(echo "${body}" | jq -r '.sha')
|
||||
# Widen the race window so concurrent allocators genuinely interleave.
|
||||
sleep "0.0$((RANDOM % 9))"
|
||||
tmp=$(mktemp "${dir}/.tmp.XXXXXX")
|
||||
echo "${sha}" > "${tmp}"
|
||||
if ln "${tmp}" "${dir}/${tag}" 2>/dev/null; then
|
||||
rm -f "${tmp}"
|
||||
echo "{\"ref\":\"refs/tags/${tag}\",\"object\":{\"type\":\"commit\",\"sha\":\"${sha}\"}}"
|
||||
exit 0
|
||||
fi
|
||||
rm -f "${tmp}"
|
||||
echo '{"message":"Reference already exists"}'
|
||||
exit 3
|
||||
;;
|
||||
"GET git/ref/tags/"*)
|
||||
tag="${path#git/ref/tags/}"
|
||||
if [[ -f "${dir}/${tag}" ]]; then
|
||||
sha=$(cat "${dir}/${tag}")
|
||||
if [[ -f "${dir}/${tag}.annotated" ]]; then
|
||||
echo "{\"object\":{\"type\":\"tag\",\"sha\":\"tagobj-${sha}\"}}"
|
||||
else
|
||||
echo "{\"object\":{\"type\":\"commit\",\"sha\":\"${sha}\"}}"
|
||||
fi
|
||||
exit 0
|
||||
fi
|
||||
echo '{"message":"Not Found"}'
|
||||
exit 2
|
||||
;;
|
||||
"GET git/tags/tagobj-"*)
|
||||
echo "{\"object\":{\"type\":\"commit\",\"sha\":\"${path#git/tags/tagobj-}\"}}"
|
||||
exit 0
|
||||
;;
|
||||
esac
|
||||
echo "unexpected call: ${method} ${path}" >&2
|
||||
exit 1
|
||||
EOF
|
||||
chmod +x "${FAKE_API}"
|
||||
|
||||
# run_alloc STATE_DIR SHA VERSION [BRANCH] -> prints allocated PROJECT_VERSION (from BASH_ENV) or "-"
|
||||
run_alloc() {
|
||||
local state="$1" sha="$2" version="$3" branch="${4:-main}"
|
||||
local env_file
|
||||
env_file=$(mktemp "${WORK}/env.XXXXXX")
|
||||
FAKE_GH_DIR="${state}" MUNITOR_GH_API_CMD="${FAKE_API}" BASH_ENV="${env_file}" \
|
||||
PROJECT_VERSION="${version}" CIRCLE_BRANCH="${branch}" CIRCLE_SHA1="${sha}" \
|
||||
CIRCLE_PROJECT_USERNAME="KofTwentyTwo" CIRCLE_PROJECT_REPONAME="Example" \
|
||||
bash "${SCRIPT}" > "${env_file}.log" 2>&1 || { echo "ERR"; return 0; }
|
||||
local v
|
||||
v=$(grep "^export PROJECT_VERSION=" "${env_file}" | tail -1 | sed "s/^export PROJECT_VERSION='\(.*\)'$/\1/")
|
||||
echo "${v:--}"
|
||||
}
|
||||
|
||||
echo "=== allocate_version.sh Tests ==="
|
||||
|
||||
echo -n " TEST: reserves the computed version when free... "
|
||||
S="${WORK}/s1"
|
||||
got=$(run_alloc "${S}" sha-a 0.1.24)
|
||||
if [[ "${got}" == "0.1.24" && "$(cat "${S}/tags/v0.1.24")" == "sha-a" ]]; then pass; else fail "got ${got}"; fi
|
||||
|
||||
echo -n " TEST: rerun on the same commit reuses its version... "
|
||||
got=$(run_alloc "${S}" sha-a 0.1.24)
|
||||
if [[ "${got}" == "0.1.24" && "$(cat "${S}/tags/v0.1.24")" == "sha-a" ]]; then pass; else fail "got ${got}"; fi
|
||||
|
||||
echo -n " TEST: collision with another commit takes the next patch (the PR #65/#66 case)... "
|
||||
got=$(run_alloc "${S}" sha-b 0.1.24)
|
||||
if [[ "${got}" == "0.1.25" && "$(cat "${S}/tags/v0.1.24")" == "sha-a" && "$(cat "${S}/tags/v0.1.25")" == "sha-b" ]]; then
|
||||
pass
|
||||
else
|
||||
fail "got ${got}"
|
||||
fi
|
||||
|
||||
echo -n " TEST: existing tags are never moved... "
|
||||
if [[ "$(cat "${S}/tags/v0.1.24")" == "sha-a" ]]; then pass; else fail "v0.1.24 moved"; fi
|
||||
|
||||
echo -n " TEST: annotated tag on the same commit is recognized as a rerun... "
|
||||
S2="${WORK}/s2"; mkdir -p "${S2}/tags"; echo sha-x > "${S2}/tags/v1.0.0"; touch "${S2}/tags/v1.0.0.annotated"
|
||||
got=$(run_alloc "${S2}" sha-x 1.0.0)
|
||||
if [[ "${got}" == "1.0.0" ]]; then pass; else fail "got ${got}"; fi
|
||||
|
||||
echo -n " TEST: skips several already-taken versions... "
|
||||
S3="${WORK}/s3"; mkdir -p "${S3}/tags"
|
||||
for p in 5 6 7; do echo "other-${p}" > "${S3}/tags/v2.3.${p}"; done
|
||||
got=$(run_alloc "${S3}" sha-y 2.3.5)
|
||||
if [[ "${got}" == "2.3.8" ]]; then pass; else fail "got ${got}"; fi
|
||||
|
||||
echo -n " TEST: non-main branches are not reserved... "
|
||||
S4="${WORK}/s4"
|
||||
got=$(run_alloc "${S4}" sha-z 0.2.0-SNAPSHOT.abc develop)
|
||||
if [[ "${got}" == "-" && ! -e "${S4}/tags/v0.2.0-SNAPSHOT.abc" ]]; then pass; else fail "got ${got}"; fi
|
||||
|
||||
echo -n " TEST: pre-release versions on main are not reserved... "
|
||||
got=$(run_alloc "${S4}" sha-z 0.2.0-RC.1 main)
|
||||
if [[ "${got}" == "-" ]]; then pass; else fail "got ${got}"; fi
|
||||
|
||||
echo -n " TEST: API failure fails the build instead of guessing... "
|
||||
S5="${WORK}/s5"
|
||||
got=$(FAKE_GH_FAIL=1 run_alloc "${S5}" sha-q 0.1.0)
|
||||
if [[ "${got}" == "ERR" ]]; then pass; else fail "got ${got}"; fi
|
||||
|
||||
echo -n " TEST: missing GITHUB_TOKEN fails on main... "
|
||||
rc=0
|
||||
env -u GITHUB_TOKEN BASH_ENV=/dev/null PROJECT_VERSION=0.1.0 CIRCLE_BRANCH=main CIRCLE_SHA1=abc \
|
||||
CIRCLE_PROJECT_USERNAME=o CIRCLE_PROJECT_REPONAME=r bash "${SCRIPT}" >/dev/null 2>&1 || rc=$?
|
||||
if [[ ${rc} -ne 0 ]]; then pass; else fail "expected failure"; fi
|
||||
|
||||
# Concurrent simulation: N pipelines for N different merge commits all computed
|
||||
# the same version from GitVersion and race to reserve it.
|
||||
N=8
|
||||
echo -n " TEST: ${N} concurrent pipelines get ${N} distinct, contiguous versions... "
|
||||
SC="${WORK}/concurrent"; mkdir -p "${SC}/tags"
|
||||
pids=()
|
||||
for i in $(seq 1 "${N}"); do
|
||||
( run_alloc "${SC}" "merge-${i}" 0.1.24 > "${WORK}/result.${i}" ) &
|
||||
pids+=($!)
|
||||
done
|
||||
for pid in "${pids[@]}"; do wait "${pid}"; done
|
||||
|
||||
results=$(for i in $(seq 1 "${N}"); do cat "${WORK}/result.${i}"; done | sort -t. -k3,3n)
|
||||
expected=$(for p in $(seq 24 $((24 + N - 1))); do echo "0.1.${p}"; done)
|
||||
ok=true
|
||||
[[ "${results}" == "${expected}" ]] || ok=false
|
||||
# Every tag maps to exactly the commit that claims that version.
|
||||
for i in $(seq 1 "${N}"); do
|
||||
v=$(cat "${WORK}/result.${i}")
|
||||
[[ -f "${SC}/tags/v${v}" && "$(cat "${SC}/tags/v${v}")" == "merge-${i}" ]] || ok=false
|
||||
done
|
||||
if ${ok}; then pass; else fail "results: $(echo "${results}" | tr '\n' ' ')"; fi
|
||||
|
||||
echo -n " TEST: concurrent reruns of the same commits keep their versions... "
|
||||
pids=()
|
||||
for i in $(seq 1 "${N}"); do
|
||||
( run_alloc "${SC}" "merge-${i}" 0.1.24 > "${WORK}/rerun.${i}" ) &
|
||||
pids+=($!)
|
||||
done
|
||||
for pid in "${pids[@]}"; do wait "${pid}"; done
|
||||
ok=true
|
||||
for i in $(seq 1 "${N}"); do
|
||||
[[ "$(cat "${WORK}/rerun.${i}")" == "$(cat "${WORK}/result.${i}")" ]] || ok=false
|
||||
done
|
||||
tag_count=$(find "${SC}/tags" -name 'v*' | wc -l | tr -d ' ')
|
||||
[[ "${tag_count}" == "${N}" ]] || ok=false
|
||||
if ${ok}; then pass; else fail "reruns changed versions or created extra tags (${tag_count})"; fi
|
||||
|
||||
echo ""
|
||||
echo "Results: ${PASS} passed, ${FAIL} failed"
|
||||
[[ ${FAIL} -eq 0 ]]
|
||||
@@ -0,0 +1,134 @@
|
||||
#!/usr/bin/env bash
|
||||
# Tests for the release guards: immutable image tags and conditional cd_path emission.
|
||||
set -euo pipefail
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
PROJECT_DIR="$(cd "${SCRIPT_DIR}/.." && pwd)"
|
||||
SRC_SCRIPTS="${PROJECT_DIR}/src/scripts"
|
||||
PASS=0
|
||||
FAIL=0
|
||||
|
||||
WORK=$(mktemp -d)
|
||||
trap 'rm -rf "${WORK}"' EXIT
|
||||
|
||||
pass() { echo "PASS"; PASS=$((PASS + 1)); }
|
||||
fail() { echo "FAIL${1:+ ($1)}"; FAIL=$((FAIL + 1)); }
|
||||
|
||||
# Fake docker: `docker buildx imagetools inspect REF --format ...` reads
|
||||
# ${FAKE_REGISTRY}/<ref with / and : replaced by _> holding the revision label.
|
||||
mkdir -p "${WORK}/bin"
|
||||
cat > "${WORK}/bin/docker" <<'EOF'
|
||||
#!/usr/bin/env bash
|
||||
if [[ "$1" == "--version" ]]; then echo "Docker version 99"; exit 0; fi
|
||||
if [[ "$1 $2 $3" == "buildx imagetools inspect" ]]; then
|
||||
ref="$4"
|
||||
if [[ -n "${FAKE_REGISTRY_ERROR:-}" ]]; then echo "unauthorized: authentication required" >&2; exit 1; fi
|
||||
key=$(echo "${ref}" | tr '/:' '__')
|
||||
f="${FAKE_REGISTRY}/${key}"
|
||||
if [[ ! -f "${f}" ]]; then echo "ERROR: ${ref}: not found" >&2; exit 1; fi
|
||||
rev=$(cat "${f}")
|
||||
if [[ -z "${rev}" ]]; then echo '{"config":{"Labels":{}}}'; exit 0; fi
|
||||
echo "{\"architecture\":\"amd64\",\"config\":{\"Labels\":{\"org.opencontainers.image.revision\":\"${rev}\"}}}"
|
||||
exit 0
|
||||
fi
|
||||
echo "unexpected docker call: $*" >&2
|
||||
exit 1
|
||||
EOF
|
||||
chmod +x "${WORK}/bin/docker"
|
||||
|
||||
put_image() { mkdir -p "${FAKE_REGISTRY}"; echo "$2" > "${FAKE_REGISTRY}/$(echo "$1" | tr '/:' '__')"; }
|
||||
|
||||
run_guard() {
|
||||
PATH="${WORK}/bin:${PATH}" MUNITOR_SKIP_REGISTRY_LOGIN=1 FAKE_REGISTRY="${FAKE_REGISTRY}" \
|
||||
IMAGE_NAME=website-backend REGISTRY=ghcr.io/koftwentytwo PROJECT_VERSION="$1" CIRCLE_SHA1="$2" \
|
||||
SUPPLEMENTAL_IMAGES_JSON="${3:-[]}" bash "${SRC_SCRIPTS}/guard_image_tags.sh" >"${WORK}/guard.log" 2>&1
|
||||
}
|
||||
|
||||
echo "=== guard_image_tags.sh Tests ==="
|
||||
SUPP='[{"name":"migrations","dockerfile":"docker/Dockerfile.migrations"}]'
|
||||
|
||||
FAKE_REGISTRY="${WORK}/reg1"
|
||||
echo -n " TEST: passes when the version tag does not exist... "
|
||||
if run_guard 0.1.32 sha-new "${SUPP}"; then pass; else fail "$(cat "${WORK}/guard.log")"; fi
|
||||
|
||||
echo -n " TEST: passes on rerun when the tag was built from the same commit... "
|
||||
put_image ghcr.io/koftwentytwo/website-backend:0.1.32 sha-new
|
||||
put_image ghcr.io/koftwentytwo/website-backend-migrations:0.1.32 sha-new
|
||||
if run_guard 0.1.32 sha-new "${SUPP}"; then pass; else fail "$(cat "${WORK}/guard.log")"; fi
|
||||
|
||||
echo -n " TEST: refuses to overwrite a version tag built from another commit (0.1.24 incident)... "
|
||||
if run_guard 0.1.32 sha-other "${SUPP}"; then fail "guard allowed overwrite"; else pass; fi
|
||||
|
||||
echo -n " TEST: refuses when only a supplemental image collides... "
|
||||
FAKE_REGISTRY="${WORK}/reg2"
|
||||
put_image ghcr.io/koftwentytwo/website-backend-migrations:0.2.0 sha-old
|
||||
if run_guard 0.2.0 sha-new "${SUPP}"; then fail "guard allowed supplemental overwrite"; else pass; fi
|
||||
|
||||
echo -n " TEST: refuses when an existing tag has no provenance label... "
|
||||
FAKE_REGISTRY="${WORK}/reg3"
|
||||
put_image ghcr.io/koftwentytwo/website-backend:0.3.0 ""
|
||||
if run_guard 0.3.0 sha-new; then fail "unlabeled image overwritten"; else pass; fi
|
||||
|
||||
echo -n " TEST: registry errors fail closed... "
|
||||
if FAKE_REGISTRY_ERROR=1 run_guard 0.4.0 sha-new; then fail "error treated as free"; else pass; fi
|
||||
|
||||
echo -n " TEST: docker_build labels images with the commit revision... "
|
||||
if grep -q 'org.opencontainers.image.revision=${CIRCLE_SHA1}' "${SRC_SCRIPTS}/docker_build.sh" &&
|
||||
grep -q 'org.opencontainers.image.revision=${CIRCLE_SHA1}' "${SRC_SCRIPTS}/build_push_supplemental.sh"; then
|
||||
pass
|
||||
else
|
||||
fail "revision label missing"
|
||||
fi
|
||||
|
||||
echo -n " TEST: docker_build_push guards tags before building and pushing... "
|
||||
JOB="${PROJECT_DIR}/src/jobs/docker_build_push.yml"
|
||||
g=$(grep -n 'guard_image_tags' "${JOB}" | head -1 | cut -d: -f1)
|
||||
p=$(grep -n 'docker_push_ghcr' "${JOB}" | head -1 | cut -d: -f1)
|
||||
if [[ -n "${g}" && -n "${p}" && ${g} -lt ${p} ]]; then pass; else fail "guard not before push"; fi
|
||||
|
||||
echo -n " TEST: build jobs reserve the version before persisting it... "
|
||||
ok=true
|
||||
for j in mvn gradle node; do
|
||||
f="${PROJECT_DIR}/src/jobs/${j}_build_and_test.yml"
|
||||
a=$(grep -n 'allocate_version' "${f}" | head -1 | cut -d: -f1)
|
||||
e=$(grep -n 'export_version_vars' "${f}" | head -1 | cut -d: -f1)
|
||||
m=$(grep -n 'persist_version_metadata' "${f}" | head -1 | cut -d: -f1)
|
||||
[[ -n "${a}" && ${a} -gt ${e} && ${a} -lt ${m} ]] || ok=false
|
||||
done
|
||||
if ${ok}; then pass; else fail "allocate_version misplaced"; fi
|
||||
|
||||
echo -n " TEST: github_release targets the built commit, not the branch head... "
|
||||
if grep -q 'TARGET="${CIRCLE_SHA1:-main}"' "${SRC_SCRIPTS}/github_release.sh" &&
|
||||
grep -q 'Refusing to release' "${SRC_SCRIPTS}/github_release.sh"; then
|
||||
pass
|
||||
else
|
||||
fail
|
||||
fi
|
||||
|
||||
echo -n " TEST: update_cd_repo verifies the release tag belongs to this commit... "
|
||||
if grep -q 'Refusing to bump the GitOps repo' "${SRC_SCRIPTS}/update_cd_repo.sh"; then pass; else fail; fi
|
||||
|
||||
echo ""
|
||||
echo "=== cd_path emission (orb version compatibility) ==="
|
||||
|
||||
render() {
|
||||
local fixture="$1" out="$2"
|
||||
MUNITOR_CONFIG="${fixture}" MUNITOR_OUTPUT_FILE="${out}" \
|
||||
bash "${SRC_SCRIPTS}/generate_config.sh" >/dev/null 2>&1 || true
|
||||
}
|
||||
|
||||
echo -n " TEST: manifest without cd.path emits no cd_path (older orbs reject it)... "
|
||||
render "${SCRIPT_DIR}/fixtures/java-webapp.munitor.yml" "${WORK}/java.yml"
|
||||
if [[ -s "${WORK}/java.yml" ]] && grep -q 'munitor/update_cd_repo' "${WORK}/java.yml" && ! grep -q 'cd_path' "${WORK}/java.yml"; then
|
||||
pass
|
||||
else
|
||||
fail "cd_path present or render failed"
|
||||
fi
|
||||
|
||||
echo -n " TEST: manifest with cd.path still emits cd_path... "
|
||||
render "${SCRIPT_DIR}/fixtures/node-webapp-consolidated.munitor.yml" "${WORK}/node.yml"
|
||||
if grep -q 'cd_path: tenants/investinginchester/website/kustomization.yaml' "${WORK}/node.yml"; then pass; else fail; fi
|
||||
|
||||
echo ""
|
||||
echo "Results: ${PASS} passed, ${FAIL} failed"
|
||||
[[ ${FAIL} -eq 0 ]]
|
||||
Reference in new issue
Block a user