Compare commits

...
12 Commits
Author SHA1 Message Date
james.maes d9b4da9e9e Merge pull request #23 from KofTwentyTwo/release/0.3.3
Release 0.3.3: atomic release versions
2026-10-11 14:49:46 -05:00
james.maesandClaude Opus 5.5 4b99bbec09 chore: prepare Munitor 0.3.3 release
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-11 14:48:30 -05:00
james.maes 00f349cb0c Merge pull request #22 from KofTwentyTwo/feature/atomic-release-versions
fix: atomic release versions, immutable image tags, conditional cd_path
2026-10-11 14:48:10 -05:00
james.maesandClaude Opus 5.5 57cb7a2aca fix: atomic release versions and immutable image tags
Two main merges built minutes apart both computed the same GitVersion
version. The later pipeline overwrote the image tag in GHCR, github-release
tagged whatever main pointed at (--target main), and the GitOps bump was a
no-op, so production ran an image without the later merge (Website-Backend
0.1.24 and 0.1.32).

- allocate_version: on main, reserve vX.Y.Z by creating the git tag through
  the GitHub refs API (create-if-absent). On conflict with another commit,
  take the next patch; reruns of the same commit reuse their tag. Existing
  tags are never moved. Runs in mvn/gradle/node build-and-test before the
  version is baked into artifacts.
- guard_image_tags: refuse to push a main or supplemental version tag that
  already exists for a different commit (org.opencontainers.image.revision
  label, now set on every build); fail closed on registry errors.
- update_cd_repo: verify vX.Y.Z belongs to this commit before bumping the
  GitOps repo; record the source commit in the GitOps commit message.
- github_release: target CIRCLE_SHA1 instead of main and refuse when the tag
  points elsewhere.
- Generator: render cd_path only when cd.path is set, so manifests pinned to
  orb versions predating the parameter (website develop branches, Concilium,
  makers4) stop failing with "Unexpected argument(s): cd_path".

Tests: tests/test_allocate_version.sh (incl. 8 concurrent allocators against
an atomic fake refs API) and tests/test_release_guards.sh.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-11 13:56:58 -05:00
james.maes f1d26b69d7 Merge pull request #21 from KofTwentyTwo/main
chore: sync Munitor 0.3.2 release to develop
2026-10-10 15:15:54 -05:00
james.maes ac3eb0e162 Merge pull request #20 from KofTwentyTwo/release/0.3.2
chore: release Munitor 0.3.2
2026-10-10 15:12:11 -05:00
James MaesandGPT-6 50ae000759 docs: format orb component name patterns
Co-Authored-By: GPT-6 <codex@openai.com>
Signed-off-by: James Maes <jmaes@dmdbrands.com>
2026-10-10 15:10:30 -05:00
James MaesandGPT-6 b357559760 chore: prepare Munitor 0.3.2 release
Co-Authored-By: GPT-6 <codex@openai.com>
Signed-off-by: James Maes <jmaes@dmdbrands.com>
2026-10-10 15:10:08 -05:00
james.maes c8bbf2ef39 Merge pull request #19 from KofTwentyTwo/feature/scoped-audit-exceptions
fix(#18): unblock website release CI
2026-10-10 15:09:55 -05:00
James MaesandGPT-6 ae7d93328f fix(#18): unblock website release CI
Closes #18

Co-Authored-By: GPT-6 <codex@openai.com>
Signed-off-by: James Maes <jmaes@dmdbrands.com>
2026-10-10 15:06:49 -05:00
james.maes d2e31ed001 Merge pull request #17 from KofTwentyTwo/main
Sync main (v0.3.1) back to develop
2026-09-23 18:05:54 -05:00
james.maes e84008ee19 Merge pull request #16 from KofTwentyTwo/release/0.3.1
Release 0.3.1
2026-09-23 17:54:02 -05:00
38 changed files with 1051 additions and 14 deletions

No files matched your search

+2
View File
@@ -0,0 +1,2 @@
*.sh text eol=lf
*.bats text eol=lf
+5 -2
View File
@@ -29,14 +29,17 @@ GitHub Issues (org: KofTwentyTwo). Commit format: `feat(#N): description` or `Cl
- Namespace: `kof22`
- Orb: `kof22/munitor`
- Latest stable: `kof22/munitor@0.3.1`
- Latest stable: `kof22/munitor@0.3.3`
- Dev snapshot: `kof22/munitor@dev:snapshot`
`0.3.0` includes first-class pnpm support, repository-owned Dockerfiles for
`node-webapp`, and consolidated Kustomize GitOps write-back with optional
production-only behavior and bounded push-race retries. `0.3.1` moves the
generated `node-webapp` runtime to `distroless/nodejs<N>-debian13` (debian12
failed the Trivy gate on OpenSSL CVEs).
failed the Trivy gate on OpenSSL CVEs). `0.3.3` makes release versions on
`main` atomic (git-tag reservation via the GitHub refs API), refuses to
overwrite version image tags built from another commit, checks tag ownership
before the GitOps bump, and renders `cd_path` only when `cd.path` is set.
## Session Continuity
+28 -1
View File
@@ -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
@@ -569,3 +580,19 @@ Pipeline generation flow:
5. Runs `envsubst` for variable substitution
6. Processes `##IF_*##` / `##ENDIF_*##` conditional blocks
7. Validates output YAML and passes to CircleCI continuation
## Temporary npm audit exceptions
The default npm audit gate remains unchanged. For a reviewed development-only HIGH
advisory with no patched package release, a repository may commit
`.npm-audit-exceptions.json` with schemaVersion 1 and an exceptions array. Each entry
requires a register ID, exact GHSA advisory ID, package, version, development scope
and UTC expiry date. See Website-Frontend's EX-0008 for the first consumer.
The full npm audit runs alongside a production audit. All production HIGH/CRITICAL
and all CRITICAL findings fail. Only the named advisory and its propagated HIGH
findings can pass, and every affected lockfile path must have dev true and the exact
excepted version. Audit errors, unknown chains and expired exceptions fail. A new
upstream package release also fails and requires review. Trivy remains unchanged.
This mechanism requires npm audit report version 2 and a package-path lockfile;
unsupported reports fail. BATS and Node are required for the audit regression tests.
+43
View File
@@ -1,5 +1,34 @@
# Munitor session state
## Release 0.3.3 (2026-10-11)
PR22 (00f349c) fixes release version collisions. Website-Backend #65/#66 (0.1.24)
and #80/#79 (0.1.32) built concurrently, computed the same GitVersion version,
overwrote the GHCR tag, and github-release tagged the moving main head, so prod
ran an image missing the later merge. Build jobs now reserve vX.Y.Z atomically
(POST git/refs; next patch on conflict; tags never move), docker_build_push guards
immutable version tags via the org.opencontainers.image.revision label,
update_cd_repo verifies tag ownership, and github_release targets CIRCLE_SHA1.
The generator renders cd_path only when cd.path is set (older-orb manifests on
website develop branches, Concilium, and makers4 failed with "Unexpected
argument(s): cd_path"). Consumer pin bumps are coordinated per repo because a
main merge deploys.
Local Windows runs: 4 test_generate_config health_path assertions fail on clean
develop too (Git-for-Windows rewrites /api/health); CI on Linux is authoritative.
## Release candidate 0.3.2 (2026-10-10)
Munitor PR19 merged to develop at c8bbf2e. Release branch `release/0.3.2`
promotes the bounded publishing CI fixes to main. Full make all and the feature
CircleCI lint/pack/review/snapshot publication passed. Stable publication still
requires the v0.3.2 tag on main and successful production orb publication.
EX-0008 is registered by standards PR27 (5c21fc7); Website-Frontend may select the
new stable orb after publication. Default audit policy, Trivy and GitOps contracts
are preserved. Website-Backend's Java installation now avoids interactive Ubuntu
service-restart dialogs. Website consumers and production remain pending.
## Current status
The `develop` branch contains the complete `0.3.0` feature set. PR #6 added
@@ -23,3 +52,17 @@ ArgoCD.
Run `make all` before release. The stable tag must publish successfully through
CircleCI before consumer repositories move from immutable development pins to
`0.3`.
## Publishing release CI fixes (2026-10-10)
Feature branch `feature/scoped-audit-exceptions` prepares development-only npm
exceptions with expiry, exact advisory/package/version checks, an independent
production audit and reassessment on a new upstream release. Website-Frontend's
proposed EX-0008 covers braces 3.0.3 through 2026-11-10. Regression tests exercise
acceptance and rejection paths. The default audit and Trivy remain unchanged.
Website-Backend CI job 1135 timed out in Ubuntu's interactive needrestart prompt
after Java installation. APT now receives noninteractive debconf and automatic
needrestart settings; a stubbed integration test verifies both APT invocations.
These fixes require normal feature-to-develop review and stable tag publication
from main before website consumers can select the new version. Nothing deployed.
+6 -1
View File
@@ -2,6 +2,11 @@
## Pending
- [x] Review publishing CI fixes and merge PR19 to develop.
- [x] Publish v0.3.2 from main.
- [ ] Publish v0.3.3 (atomic release versions); bump Website-Backend/Frontend pins to 0.3.3.
- [ ] Bump Concilium (0.2) and makers4 (0.1) to 0.3.3 when next active.
- [ ] Follow makers4 and terraform-proxmox-talos on CircleCI when those repos are next active.
- [ ] Adopt supplemental images on an appropriate consumer.
@@ -13,7 +18,7 @@
- [x] Validate first-class pnpm support and merge PR #6.
- [x] Add opt-in repository Dockerfile preservation for node-webapp (GH-7)
- [x] Implement first-class pnpm support (GH-5, 17 commits, 219 tests)
- [x] Rename all npm_* orb components to node_* (backward-incompatible, pre-1.0)
- [x] Rename all `npm_*` orb components to `node_*` (backward-incompatible, pre-1.0)
- [x] Thread package_manager through full job/command/script chain
- [x] Add corepack activation for pnpm/yarn in install_node
- [x] Add pnpm Dockerfile variants for node-api and node-webapp
+9
View File
@@ -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) >>
+34
View File
@@ -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) >>
+4
View File
@@ -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 >>
+1
View File
@@ -25,6 +25,7 @@ steps:
- restore_gradle_cache
- calculate_version
- export_version_vars
- allocate_version
- gradle_build
- gradle_test
- run:
+1
View File
@@ -25,6 +25,7 @@ steps:
- restore_maven_cache
- calculate_version
- export_version_vars
- allocate_version
- mvn_set_version
- mvn_build
- mvn_test
+1
View File
@@ -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)>>
+161
View File
@@ -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})"
+5 -1
View File
@@ -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}"
+5
View File
@@ -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[@]}"
+9 -1
View File
@@ -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
+1 -1
View File
@@ -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
+13 -1
View 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
+87
View File
@@ -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}."
+3 -2
View File
@@ -26,8 +26,9 @@ echo "deb [signed-by=/etc/apt/keyrings/adoptium.asc] \
$(. /etc/os-release && echo "${VERSION_CODENAME}") main" \
| sudo tee /etc/apt/sources.list.d/adoptium.list > /dev/null
sudo apt-get update -qq
sudo apt-get install -y -qq "temurin-${VERSION}-jdk"
# CI cannot answer debconf or needrestart's service-restart dialog.
sudo env DEBIAN_FRONTEND=noninteractive NEEDRESTART_MODE=a apt-get update -qq
sudo env DEBIAN_FRONTEND=noninteractive NEEDRESTART_MODE=a apt-get install -y -qq "temurin-${VERSION}-jdk"
# Set JAVA_HOME for subsequent steps
JAVA_HOME_DIR="/usr/lib/jvm/temurin-${VERSION}-jdk-$(dpkg --print-architecture)"
+100
View File
@@ -15,6 +15,106 @@ munitor_header "node_audit"
echo "Running npm audit..."
if [[ -f .npm-audit-exceptions.json ]]; then
# The optional, repository-reviewed exception file never changes runtime policy.
REPORT_DIR=$(mktemp -d)
trap 'rm -rf "${REPORT_DIR}"' EXIT
export REPORT_DIR
npm audit --json --audit-level=high > "${REPORT_DIR}/full.json" || AUDIT_STATUS=$?
npm audit --json --omit=dev --audit-level=high > "${REPORT_DIR}/production.json" || PRODUCTION_STATUS=$?
export AUDIT_STATUS="${AUDIT_STATUS:-0}" PRODUCTION_STATUS="${PRODUCTION_STATUS:-0}"
node <<'AUDIT_JS'
const fs = require('node:fs');
const { execFileSync } = require('node:child_process');
function fail(message) { throw new Error(message); }
function readReport(name, status) {
const report = JSON.parse(fs.readFileSync(`${process.env.REPORT_DIR}/${name}.json`, 'utf8'));
if (report.error || report.auditReportVersion !== 2 || !report.vulnerabilities || !report.metadata?.vulnerabilities)
fail(`${name}: npm audit returned an error or unsupported report`);
if (![0, 1].includes(Number(status))) fail(`${name}: npm audit failed (${status})`);
const counts = report.metadata.vulnerabilities;
if (Object.values(report.vulnerabilities).some(v => !v || !['info', 'low', 'moderate', 'high', 'critical'].includes(v.severity)))
fail(`${name}: invalid vulnerability severity`);
for (const severity of ['info', 'low', 'moderate', 'high', 'critical']) {
if (!Number.isInteger(counts[severity]) || counts[severity] < 0) fail(`${name}: invalid severity counts`);
if (Object.values(report.vulnerabilities).filter(v => v.severity === severity).length !== counts[severity])
fail(`${name}: inconsistent severity counts`);
}
return report;
}
try {
const full = readReport('full', process.env.AUDIT_STATUS);
const production = readReport('production', process.env.PRODUCTION_STATUS);
if (production.metadata.vulnerabilities.high || production.metadata.vulnerabilities.critical)
fail('Production dependencies contain high/critical vulnerabilities; exceptions do not apply');
if (full.metadata.vulnerabilities.critical) fail('Critical vulnerabilities cannot be excepted');
const config = JSON.parse(fs.readFileSync('.npm-audit-exceptions.json', 'utf8'));
if (config.schemaVersion !== 1 || !Array.isArray(config.exceptions) || !config.exceptions.length)
fail('Invalid exception configuration');
const today = new Date().toISOString().slice(0, 10);
const lock = JSON.parse(fs.readFileSync('package-lock.json', 'utf8'));
if (!lock.packages) fail('Exceptions require a package-lock.json with package paths');
const allowed = new Map();
for (const entry of config.exceptions) {
if (!/^EX-\d{4}$/.test(entry.id) || !/^GHSA-[a-z0-9]{4}-[a-z0-9]{4}-[a-z0-9]{4}$/.test(entry.advisory)
|| entry.scope !== 'development' || !/^\d{4}-\d{2}-\d{2}$/.test(entry.expires)
|| !Number.isFinite(Date.parse(entry.expires))
|| new Date(entry.expires).toISOString().slice(0, 10) !== entry.expires || today >= entry.expires
|| typeof entry.package !== 'string' || !/^(@[a-z0-9._-]+\/)?[a-z0-9._-]+$/.test(entry.package)
|| !/^\d+\.\d+\.\d+$/.test(entry.version) || allowed.has(entry.advisory))
fail('Invalid, duplicated or expired development exception');
allowed.set(entry.advisory, entry);
}
function roots(name, seen = new Set()) {
if (seen.has(name)) fail(`Cyclic vulnerability chain: ${name}`);
const vulnerability = full.vulnerabilities[name];
if (!vulnerability || !Array.isArray(vulnerability.via) || !vulnerability.via.length)
fail(`Unresolved vulnerability chain: ${name}`);
const next = new Set([...seen, name]);
return vulnerability.via.flatMap(via => {
if (typeof via === 'string') return roots(via, next);
if (!via || typeof via.url !== 'string' || typeof via.severity !== 'string')
fail(`Invalid advisory: ${name}`);
return [{ advisory: via, vulnerability }];
});
}
const used = new Set();
for (const [name, vulnerability] of Object.entries(full.vulnerabilities)) {
if (vulnerability.severity !== 'high') continue;
for (const { advisory, vulnerability: root } of roots(name)) {
if (!['high', 'critical'].includes(advisory.severity)) continue;
const id = advisory.url.replace('https://github.com/advisories/', '');
const exception = allowed.get(id);
if (!exception || advisory.url !== `https://github.com/advisories/${id}`
|| advisory.severity !== 'high' || advisory.name !== exception.package
|| root.name !== exception.package || !Array.isArray(root.nodes) || !root.nodes.length)
fail(`Unexcepted high vulnerability: ${name} (${advisory.url})`);
for (const path of root.nodes) {
if (!lock.packages[path] || lock.packages[path].dev !== true || lock.packages[path].version !== exception.version)
fail(`Exception does not cover package path or version: ${path}`);
}
used.add(id);
}
if (!roots(name).some(r => ['high', 'critical'].includes(r.advisory.severity)))
fail(`High vulnerability has no resolvable high advisory: ${name}`);
}
for (const id of used) {
const entry = allowed.get(id);
// Any newer release requires reassessment, even if npm proposes a parent downgrade.
const latest = execFileSync('npm', ['view', entry.package, 'version', '--json'],
{ encoding: 'utf8', timeout: 30000 });
if (JSON.parse(latest) !== entry.version) fail(`New ${entry.package} release available; reassess ${entry.id}`);
console.log(`Accepted ${entry.id}: ${id}, development only, expires ${entry.expires}`);
}
console.log('Full audit checked; production audit has no high/critical findings.');
} catch (error) {
console.error(`npm audit rejected: ${error.message}`);
process.exit(1);
}
AUDIT_JS
exit 0
fi
npm audit --audit-level=high || {
EXIT_CODE=$?
echo "npm audit found high/critical vulnerabilities."
+34 -2
View 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
@@ -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
+9 -1
View 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##
+4
View File
@@ -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##
+26 -1
View File
@@ -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
+31
View File
@@ -0,0 +1,31 @@
const fs = require('node:fs');
const [directory, scenario = 'allowed'] = process.argv.slice(2);
const advisory = {
name: 'braces', severity: 'high', url: 'https://github.com/advisories/GHSA-vfj7-8cjw-p6xm'
};
const root = { name: 'braces', severity: 'high', via: [advisory], nodes: ['node_modules/braces'] };
const full = { auditReportVersion: 2, vulnerabilities: {
braces: root, parent: { name: 'parent', severity: 'high', via: ['braces'] }
}, metadata: { vulnerabilities: { info: 0, low: 0, moderate: 0, high: 2, critical: 0 } } };
const production = { auditReportVersion: 2, vulnerabilities: {},
metadata: { vulnerabilities: { info: 0, low: 0, moderate: 0, high: 0, critical: 0 } } };
const exception = { id: 'EX-0008', advisory: 'GHSA-vfj7-8cjw-p6xm', package: 'braces',
version: '3.0.3', scope: 'development', expires: '2099-01-01' };
const lock = { lockfileVersion: 3, packages: { 'node_modules/braces': { version: '3.0.3', dev: true } } };
if (scenario === 'expired') exception.expires = '2020-01-01';
if (scenario === 'invalid-date') exception.expires = '2099-02-30';
if (scenario === 'different') advisory.url = 'https://github.com/advisories/GHSA-aaaa-bbbb-cccc';
if (scenario === 'mixed') root.via.push({ ...advisory, url: 'https://github.com/advisories/GHSA-aaaa-bbbb-cccc' });
if (scenario === 'critical') { root.severity = advisory.severity = 'critical'; full.metadata.vulnerabilities.high--; full.metadata.vulnerabilities.critical++; }
if (scenario === 'production') { production.vulnerabilities.braces = root; production.metadata.vulnerabilities.high = 1; }
if (scenario === 'runtime-path') lock.packages['node_modules/braces'].dev = false;
if (scenario === 'different-version') lock.packages['node_modules/braces'].version = '3.0.2';
if (scenario === 'unknown-chain') full.vulnerabilities.parent.via = ['missing'];
if (scenario === 'cycle') root.via = ['parent'];
if (scenario === 'counts') full.metadata.vulnerabilities.high = 0;
if (scenario === 'registry-error') full.error = { code: 'ENETUNREACH' };
fs.mkdirSync(directory, { recursive: true });
for (const [name, data] of Object.entries({ 'full.json': full, 'production.json': production,
'.npm-audit-exceptions.json': { schemaVersion: 1, exceptions: [exception] }, 'package-lock.json': lock })) {
fs.writeFileSync(`${directory}/${name}`, JSON.stringify(data));
}
+32
View File
@@ -0,0 +1,32 @@
#!/usr/bin/env bats
setup() {
export PROJECT_DIR="${BATS_TEST_DIRNAME}/.."
export WORK="${BATS_TEST_TMPDIR}/work"
mkdir -p "${WORK}/bin"
export BASH_ENV="${WORK}/bash-env" JAVA_VERSION=21
export APT_CALLS="${WORK}/apt-calls"
cat > "${WORK}/bin/sudo" <<'MOCK'
#!/usr/bin/env bash
if [[ "$1" == env ]]; then
[[ "$2" == DEBIAN_FRONTEND=noninteractive && "$3" == NEEDRESTART_MODE=a && "$4" == apt-get ]] || exit 2
printf '%s\n' "$*" >> "$APT_CALLS"
elif [[ "$1" == tee ]]; then
cat > /dev/null
fi
MOCK
printf '#!/usr/bin/env bash\necho key\n' > "${WORK}/bin/curl"
printf '#!/usr/bin/env bash\necho amd64\n' > "${WORK}/bin/dpkg"
printf '#!/usr/bin/env bash\necho java-version-21\n' > "${WORK}/bin/java"
chmod +x "${WORK}/bin/"*
export PATH="${WORK}/bin:${PATH}"
}
@test "APT update and Java installation cannot open an interactive restart prompt" {
run bash "${PROJECT_DIR}/src/scripts/install_java.sh"
[ "$status" -eq 0 ]
[ "$(wc -l < "$APT_CALLS")" -eq 2 ]
grep -q 'apt-get update -qq' "$APT_CALLS"
grep -q 'apt-get install -y -qq temurin-21-jdk' "$APT_CALLS"
grep -q "export JAVA_HOME='/usr/lib/jvm/temurin-21-jdk-amd64'" "$BASH_ENV"
}
+55
View File
@@ -0,0 +1,55 @@
#!/usr/bin/env bats
setup() {
export PROJECT_DIR="${BATS_TEST_DIRNAME}/.."
export WORK="${BATS_TEST_TMPDIR}/work"
node "${BATS_TEST_DIRNAME}/fixtures/npm-audit/build-fixture.cjs" "${WORK}"
mkdir -p "${WORK}/bin"
cat > "${WORK}/bin/npm" <<'MOCK'
#!/usr/bin/env bash
if [[ "$1" == view ]]; then printf '"%s"\n' "${LATEST_VERSION:-3.0.3}"; exit 0; fi
if [[ "$*" == *--omit=dev* ]]; then cat production.json; exit 0; fi
cat full.json
exit "${MOCK_AUDIT_STATUS:-1}"
MOCK
chmod +x "${WORK}/bin/npm"
export PATH="${WORK}/bin:${PATH}"
cd "${WORK}"
}
@test "accepts only the registered advisory and its propagated development findings" {
run bash "${PROJECT_DIR}/src/scripts/node_audit.sh"
[ "$status" -eq 0 ]
[[ "$output" == *"Accepted EX-0008"* ]]
}
@test "fails closed for expired, invalid, unrelated, mixed, critical and runtime findings" {
for scenario in expired invalid-date different mixed critical production runtime-path different-version unknown-chain cycle counts registry-error; do
node "${BATS_TEST_DIRNAME}/fixtures/npm-audit/build-fixture.cjs" "$WORK" "$scenario"
run bash "${PROJECT_DIR}/src/scripts/node_audit.sh"
if [ "$status" -eq 0 ]; then echo "Unexpected acceptance: $scenario"; return 1; fi
done
}
@test "a new upstream release requires reassessment" {
export LATEST_VERSION=3.0.4
run bash "${PROJECT_DIR}/src/scripts/node_audit.sh"
[ "$status" -ne 0 ]
[[ "$output" == *"New braces release available"* ]]
}
@test "malformed reports and unexpected audit exit codes fail closed" {
export MOCK_AUDIT_STATUS=2
run bash "${PROJECT_DIR}/src/scripts/node_audit.sh"
[ "$status" -ne 0 ]
printf 'invalid json' > full.json
run bash "${PROJECT_DIR}/src/scripts/node_audit.sh"
[ "$status" -ne 0 ]
}
@test "repositories without exceptions retain their existing audit gate" {
rm .npm-audit-exceptions.json
run bash "${PROJECT_DIR}/src/scripts/node_audit.sh"
[ "$status" -eq 1 ]
[[ "$output" == *"npm audit found high/critical"* ]]
}
+182
View File
@@ -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 ]]
+3
View File
@@ -0,0 +1,3 @@
#!/usr/bin/env bash
set -euo pipefail
bats "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/install_java.bats"
+3
View File
@@ -0,0 +1,3 @@
#!/usr/bin/env bash
set -euo pipefail
bats "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/node_audit.bats"
+134
View File
@@ -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 ]]