Compare commits

...
Author SHA1 Message Date
james.maesandCodex 21517b25e7 fix: refresh generated JVM runtime packages on every build
Co-Authored-By: Codex <noreply@openai.com>
Signed-off-by: James Maes <james@kof22.com>
2026-10-19 07:50:52 -05:00
james.maes a3d680f130 Merge pull request #37 from KofTwentyTwo/main
chore: back-merge main into develop after 0.3.6
2026-10-13 17:24:24 -05:00
james.maes 57dffdd9a3 Merge pull request #36 from KofTwentyTwo/release/0.3.6
Release Munitor 0.3.6
2026-10-13 17:17:42 -05:00
james.maesandClaude Opus 5.5 9e2c45e79a docs(readme): adoption examples use 0.3.6
Signed-off-by: James Maes <james@kof22.com>

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-13 17:11:51 -05:00
james.maes fbeec65cb5 Merge pull request #35 from KofTwentyTwo/feature/supplemental-push-boolean
fix: push supplemental images on release builds again (0.3.5 regression)
2026-10-13 17:11:47 -05:00
james.maesandClaude Opus 5.5 69898922be fix: push supplemental images on release builds again
CircleCI renders the boolean push parameter as 1/0 in the step
environment, but build_push_supplemental.sh only pushed when
PUSH_IMAGES was the string "true". Since 0.3.5 every release build
built and scanned supplemental images and never pushed them, so
deployments referencing them hit ImagePullBackOff (kof22 0.1.75).

PUSH_IMAGES now accepts true/1/yes/on and false/0/no/off/empty; unset
still means push, and an unrecognised value fails before any build.
Tests cover release-mode pushes (1, true, TRUE) of every supplemental
image, PR-mode non-pushes (0, false, empty), and the invalid value.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: James Maes <james@kof22.com>
2026-10-13 17:05:34 -05:00
james.maes 59eb5294fe Merge pull request #34 from KofTwentyTwo/main
chore: back-merge main into develop after 0.3.5
2026-10-13 15:44:21 -05:00
james.maes 16079c5ca8 Merge pull request #33 from KofTwentyTwo/release/0.3.5
Release Munitor 0.3.5
2026-10-13 15:37:54 -05:00
james.maesandClaude Opus 5.5 578c7ea0eb docs(readme): adoption examples use 0.3.5
Signed-off-by: James Maes <james@kof22.com>

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-13 15:31:55 -05:00
james.maes 53c04937b0 Merge pull request #32 from KofTwentyTwo/feature/pr-image-scan
feat: scan container images in PR pipelines
2026-10-13 14:42:28 -05:00
james.maesandClaude Opus 5.5 b09ced056f feat: scan container images in PR pipelines
New munitor/docker_scan job (image-scan) in the pr-checks workflow of
java-webapp, gradle-webapp, node-api and node-webapp: builds the app
image and every supplemental image, runs the same Trivy 0.69.3
HIGH/CRITICAL --exit-code 1 scan as the release pipeline (same
.trivyignore), stores the JSON results, and never pushes.
build_push_supplemental gains push (default true, unchanged behavior).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-13 14:18:41 -05:00
james.maes fb232fb6fe Merge pull request #31 from KofTwentyTwo/main
chore: back-merge main into develop after 0.3.4
2026-10-12 06:24:42 -05:00
james.maes 687944e872 Merge pull request #30 from KofTwentyTwo/release/0.3.4
Release Munitor 0.3.4
2026-10-12 06:19:46 -05:00
james.maes e54d739554 Merge pull request #29 from KofTwentyTwo/feature/signed-gitops-commits
fix: sign GitOps bumps, run tests in CI, scope feature snapshots
2026-10-12 06:14:14 -05:00
james.maesandClaude Opus 5.5 71c262fb62 docs: cite EX-0011 for the 2-space shell layout
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: James Maes <james@kof22.com>
2026-10-12 06:09:01 -05:00
james.maes 6cbe4256d5 Merge pull request #28 from KofTwentyTwo/feature/adoption-checklist-link
docs(readme): link the QQQ adoption checklist
2026-10-12 06:04:10 -05:00
james.maesandClaude Opus 5.5 d7960161fb test: cover signed API commits and head-refresh retries in update_cd_repo
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: James Maes <james@kof22.com>
2026-10-12 06:02:52 -05:00
james.maesandClaude Opus 5.5 807120f2af ci: install bats-core from its release tag
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: James Maes <james@kof22.com>
2026-10-12 05:56:11 -05:00
james.maesandClaude Opus 5.5 1c64d1e18e fix: sign GitOps bumps, run tests in CI, scope feature snapshots
update_cd_repo creates the CD commit through the GraphQL
createCommitOnBranch mutation so GitHub signs it, letting the GitOps
repository require signed commits without a bypass actor. Concurrent
bumps retry against the refreshed head.

CI pins circleci/orb-tools to 12.5.0 and adds a test job running the
script, template and BATS suites. Feature branches publish only the
immutable dev:<sha> tag; develop keeps dev:snapshot (#26). The
generator accepts the documented org key (#27). Adds .editorconfig.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: James Maes <james@kof22.com>
2026-10-12 05:51:41 -05:00
james.maesandClaude Opus 5.5 24482fd030 docs(readme): link the QQQ adoption checklist; neutral example
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: James Maes <james@kof22.com>
2026-10-12 05:47:45 -05:00
james.maes ad1d30681b Merge pull request #25 from KofTwentyTwo/feature/adoption-standards-docs
docs: add adoption guide, security policy and code owners
2026-10-12 05:30:48 -05:00
24 changed files with 722 additions and 135 deletions

No files matched your search

+46 -1
View File
@@ -1,7 +1,8 @@
version: 2.1
orbs:
orb-tools: circleci/orb-tools@12.1
# Pinned to an exact version (K22-CI-11); bump deliberately.
orb-tools: circleci/orb-tools@12.5.0
filters: &filters
tags:
@@ -13,6 +14,31 @@ tag-only: &tag-only
tags:
only: /^v[0-9]+\.[0-9]+\.[0-9]+$/
jobs:
test:
docker:
- image: cimg/node:22.12
resource_class: medium
steps:
- checkout
- run:
name: Install test tools
command: |
sudo apt-get update -qq
sudo apt-get install -y -qq gettext-base shellcheck >/dev/null
YQ_VERSION=v4.44.6
sudo curl -fsSL -o /usr/local/bin/yq \
"https://github.com/mikefarah/yq/releases/download/${YQ_VERSION}/yq_linux_amd64"
sudo chmod +x /usr/local/bin/yq
git clone -q --depth 1 --branch v1.11.1 https://github.com/bats-core/bats-core.git /tmp/bats-core
sudo /tmp/bats-core/install.sh /usr/local
- run:
name: Script and template tests
command: make test-scripts test-templates
- run:
name: BATS tests
command: bats tests
workflows:
lint-pack:
jobs:
@@ -23,6 +49,8 @@ workflows:
- orb-tools/review:
filters: *filters
exclude: RC006,RC007,RC010 # RC006/RC007: private repo URL unreachable; RC010: machine executors
- test:
filters: *filters
test-deploy:
jobs:
@@ -38,11 +66,15 @@ workflows:
- orb-publishing
filters: *tag-only
# develop owns the shared dev:snapshot tag. Feature branches publish only an
# immutable dev:<sha> tag so open branches cannot overwrite each other's
# snapshot (issue #26).
snapshot:
jobs:
- orb-tools/pack:
filters: *filters
- orb-tools/publish:
name: publish-develop-snapshot
orb_name: kof22/munitor
vcs_type: << pipeline.project.type >>
pub_type: dev
@@ -55,4 +87,17 @@ workflows:
branches:
only:
- develop
- orb-tools/publish:
name: publish-feature-sha
orb_name: kof22/munitor
vcs_type: << pipeline.project.type >>
pub_type: dev
dev_tags: "dev:${CIRCLE_SHA1}"
requires:
- orb-tools/pack
context:
- orb-publishing
filters:
branches:
only:
- /feature\/.*/
+26
View File
@@ -0,0 +1,26 @@
# KofTwentyTwo .editorconfig (standards K22-REPO-03, configs/editorconfig).
# Exception: the existing Bash scripts and CircleCI templates use 2-space
# indentation; K22-CODE-SH-02 specifies 3. K22 exception EX-0011.
# Do not mix widths in a file.
root = true
[*]
charset = utf-8
end_of_line = lf
insert_final_newline = true
trim_trailing_whitespace = true
indent_style = space
indent_size = 2
[*.md]
trim_trailing_whitespace = false
[{Makefile,makefile,*.mk}]
indent_style = tab
# Shell: shfmt reads these keys (K22-CODE-SH-02), apart from the indent width above.
[*.{sh,bash,bats}]
binary_next_line = true
switch_case_indent = true
space_redirects = true
function_next_line = true
+4 -2
View File
@@ -2,9 +2,10 @@
## Workflow
- **Branches.** Branch `feature/<name>` or `hotfix/<name>` from `develop` and open a pull request back to `develop`. Releases go through `release/X.Y.Z` into `main`; pushing the `vX.Y.Z` tag publishes `kof22/munitor@X.Y.Z`, and every push to `develop` or `feature/*` publishes `dev:snapshot`. Back-merge `main` into `develop` after a release.
- **Branches.** Branch `feature/<name>` or `hotfix/<name>` from `develop` and open a pull request back to `develop`. Releases go through `release/X.Y.Z` into `main`; pushing the `vX.Y.Z` tag publishes `kof22/munitor@X.Y.Z`, and every push to `develop` publishes `dev:snapshot`, and every push to `feature/*` publishes only the immutable `dev:<commit-sha>` tag. Back-merge `main` into `develop` after a release.
- **Commits.** Conventional Commits (`feat(#N): ...`, `fix: ...`, `docs: ...`), signed (`git commit -S`), with a Developer Certificate of Origin sign-off line (`git commit -s`, which adds `Signed-off-by: Name <email>`). Reference the GitHub issue (`Closes #N`) where there is one.
- **Checks.** Run `make all` (yamllint, shellcheck, pack plus `circleci orb validate`, script and template tests) and `bats tests` (the `.bats` suites) before pushing. CircleCI runs only the orb-tools lint, pack and review jobs; the script, template and BATS tests run locally, so a green CI run does not replace them.
- **Checks.** Run `make all` (yamllint, shellcheck, pack plus `circleci orb validate`, script and template tests) and `bats tests` (the `.bats` suites) before pushing. CircleCI runs the orb-tools lint, pack and review jobs plus a `test` job (`make test-scripts test-templates` and `bats tests`). `circleci orb validate` still runs only locally.
- **Formatting.** Shell scripts and templates use 2-space indentation (see `.editorconfig`); this deviates from K22-CODE-SH-02 under standards exception EX-0011.
- **Security.** Report vulnerabilities privately as described in [SECURITY.md](SECURITY.md), never in a public issue.
- **AI assistance.** Disclose AI-assisted changes in the pull request description.
@@ -331,6 +332,7 @@ These components are shared across all modules -- you don't need to recreate the
| `secrets_scan` job | Gitleaks (all modules) |
| `sast_scan` job | Semgrep (all modules, opt-in) |
| `docker_build_push` job | Build + health check + Trivy scan + GHCR push |
| `docker_scan` job | PR image build + Trivy scan (including supplemental images), JSON artifacts, no publishing |
| `health_check` command | Container health check smoke test (all Docker modules) |
| `update_cd_repo` job | GitOps CD repo update |
| `github_release` job | Annotated git tags + GitHub Releases (auto-enabled with `contexts.github`) |
+17 -7
View File
@@ -6,7 +6,7 @@ Latin: *munitor* (fortifier, protector) -- KofTwentyTwo's unified CircleCI orb f
| --- | --- |
| **Tier** | Internal until classified -- listed under *Awaiting classification* in the KofTwentyTwo standards [MAINTAINERS.md](https://github.com/KofTwentyTwo/standards/blob/main/MAINTAINERS.md); Product-tier requirements are being adopted |
| **Standards** | Targets [KofTwentyTwo standards](https://github.com/KofTwentyTwo/standards) 1.0.0 |
| **Orb** | [`kof22/munitor`](https://circleci.com/developer/orbs/orb/kof22/munitor), latest stable `0.3.3` |
| **Orb** | [`kof22/munitor`](https://circleci.com/developer/orbs/orb/kof22/munitor), latest stable `0.3.6` |
| **Security** | [SECURITY.md](SECURITY.md) |
| **Contributing** | [CONTRIBUTING.md](CONTRIBUTING.md) |
@@ -68,18 +68,18 @@ Munitor validates branch names and will fail on non-conforming names.
## Adopting in another app
Munitor is a public CircleCI orb: any CircleCI organization can use it without forking. This checklist covers what a new consumer needs, for example a Java/Maven service such as me-health-portal.
Munitor is a public CircleCI orb: any CircleCI organization can use it without forking. This checklist covers what a new consumer needs, for example a Java/Maven web service. A QQQ application should also read the [QQQ adoption checklist](https://github.com/QRun-IO/qqq/blob/develop/docs/migration/adoption-checklist.adoc) (section 7 covers release tooling).
**Version.** Use `kof22/munitor@0.3.3` (or `@0.3` to float on the 0.3 line) in `.circleci/config.yml`, and the same value as `orb_version` in `.munitor.yml`. Keep the setup orb and the generated pipeline on the same line.
**Version.** Use `kof22/munitor@0.3.6` (or `@0.3` to float on the 0.3 line) in `.circleci/config.yml`, and the same value as `orb_version` in `.munitor.yml`. Keep the setup orb and the generated pipeline on the same line.
### 1. Repository files
- `.circleci/config.yml`: the setup config from [Quick Start](#quick-start), pinned to `kof22/munitor@0.3.3`.
- `.circleci/config.yml`: the setup config from [Quick Start](#quick-start), pinned to `kof22/munitor@0.3.6`.
- `.munitor.yml`: pick a [pipeline type](#pipeline-types). A Java service is `java-webapp`. KofTwentyTwo's Website-Backend is a working example; a generalized version:
```yaml
pipeline: java-webapp
orb_version: "0.3.3"
orb_version: "0.3.6"
image_name: my-service
java_version: "21"
docker:
@@ -372,7 +372,7 @@ contexts:
**What runs:** Maven build/test, Checkstyle, SpotBugs, PMD, JaCoCo coverage, OWASP + Trivy security, Docker build/push (Dockerfile auto-generated), gitleaks. OWASP dependency check requires the `nvd` context; if omitted, the security scan runs without NVD database enrichment.
**Dockerfile:** Auto-generated production-ready image using Eclipse Temurin JRE Alpine. Includes non-root user (uid 1001), JVM container tuning flags, Docker HEALTHCHECK, and configurable port via `health.port`. The pre-built JAR from CI is copied directly (no redundant Maven build stage).
**Dockerfile:** Auto-generated production-ready image using Eclipse Temurin JRE Alpine. Includes non-root user (uid 1001), JVM container tuning flags, Docker HEALTHCHECK, and configurable port via `health.port`. The pre-built JAR from CI is copied directly (no redundant Maven build stage). PR and release image builds pull the current base and bypass Docker layer caching for generated Java/Gradle runtime images, ensuring the Alpine package upgrade runs on every build. Node builds retain their layer cache. Trivy gates remain required.
### `terraform`
@@ -490,7 +490,7 @@ Each workflow runs on specific branches and includes jobs based on your config:
| Workflow | Branches | Jobs |
|----------|----------|------|
| `pr-checks` | `feature/*`, `hotfix/*` | Build, test, lint, coverage, security scans |
| `pr-checks` | `feature/*`, `hotfix/*` | Build, test, lint, coverage, security scans, container image scan (Docker modules) |
| `develop` | `develop` | All pr-checks + Docker build/push + CD update (dev env) |
| `staging` | `staging` | All pr-checks + Docker build/push + CD update (staging env) |
| `release-candidate` | `release/*` | All pr-checks + Docker, CD (staging), SonarCloud, SBOM, GitHub Release (pre-release) |
@@ -498,11 +498,20 @@ Each workflow runs on specific branches and includes jobs based on your config:
For `validate-cd-repo` and `argocd-apps`, all three workflows (`pr-checks`, `develop`, `release`) run the same 5-job set: yaml-lint, kustomize-validate, kubesec-scan, kube-linter, secrets-scan.
For `java-webapp`, `gradle-webapp`, `node-api`, and `node-webapp`, PRs run
`image-scan` after `build-and-test`: build the application and any supplemental
images locally, then fail on HIGH/CRITICAL CVEs with the same Trivy policy and
repository-root `.trivyignore` as release builds. JSON reports are saved as
artifacts. This job does not log in, publish images, reserve tags, or update CD.
Release candidates already scan through `docker-build-push` and do not run a
second image scan job.
### Job Dependency Graph (node-api pr-checks)
```
checkout
--> build-and-test
--> image-scan (local build + Trivy, including supplemental images)
--> code-quality
--> coverage
--> security-scan
@@ -520,6 +529,7 @@ Production versions on `main` are unique per commit, even when merges land minut
- **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.
- **Signed GitOps commits** -- `update-cd-repo` creates the CD repository commit through the GitHub GraphQL API (`createCommitOnBranch`) instead of `git push`. GitHub signs API-created commits, so they show as Verified and pass a CD repository ruleset that requires signed commits, with no bypass actor. The commit is authored by the identity behind `GITHUB_TOKEN`; the source repository and commit are recorded in the commit body. Concurrent bumps retry against the new branch head (`expectedHeadOid`). The `ci_git_email`/`ci_git_name` parameters and `org.ci_email`/`org.ci_name` no longer affect these commits.
- **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.
+32
View File
@@ -1,5 +1,35 @@
# Munitor session state
## JVM runtime package freshness (2026-10-19)
Website-Backend release6258 and PR scans6266/6277 reused a cached apk upgrade
layer containing vulnerable libexpat. The shared docker_build command now pulls
the base and bypasses layer caching for generated Java/Gradle runtimes, while
Node caching and all Trivy gates remain intact. Narrow behavioral tests cover
both pipeline types, alternate/missing manifests, tags, provenance, and both
PR/release job routes. All eight focused shell cases passed via the coordinating
agent; ShellCheck and git diff --check passed. Full make/BATS/orb validation and
real-image scanning remain pending. Stable release and consumer adoption remain
pending; no unchanged vulnerable image should be retried.
## PR container scanning (2026-10-13)
Uncommitted work on `feature/pr-image-scan` adds `docker_scan` after
`build-and-test` in the four application PR templates. It restores the existing
feature/hotfix version metadata, builds locally, and applies the release Trivy
HIGH/CRITICAL policy from the repository root. Supplemental images use
`push: false`; the command defaults to publishing for existing callers. Main
and supplemental JSON reports are artifacts. Release candidates already scan
through `docker_build_push`, so no duplicate job was added.
Verified: native CircleCI orb pack/validate, ShellCheck on all source scripts
and the changed tests, template structure/filters/parameter wiring, packed
template synchronization, and `git diff --check`. Requested make and BATS runs
were attempted but Git Bash cannot start in this sandbox (CreateFileMapping,
Win32 error 5); yamllint is not installed. New behavioral tests therefore need
a Linux/CI run, including real Docker/Trivy integration. No commit or publish.
The Second Brain vault is outside writable roots; continuity is recorded here.
## Release 0.3.3 (2026-10-11)
PR22 (00f349c) fixes release version collisions. Website-Backend #65/#66 (0.1.24)
@@ -66,3 +96,5 @@ 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.
Runtime freshness validation: coordinating agent completed make all (lint, pack, orb validation, scripts,277template assertions) and all7BATS tests in Ubuntu. Real consumer image scan remains required after release.
+5
View File
@@ -2,6 +2,11 @@
## Pending
- [ ] Release JVM runtime cache fix and adopt the stable orb in Website-Backend; verify fresh image Trivy before release.
- [ ] Run PR image-scan script/template/BATS tests on Linux and verify a consumer PR with Docker/Trivy before publishing.
- [x] Add PR-only image scans for the four application templates, supplemental scan-only mode, JSON artifacts, and regression tests (uncommitted).
- [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.
+11 -3
View File
@@ -1,8 +1,12 @@
description: >
Build, scan, and push supplemental Docker images alongside the main application image.
Each supplemental image shares the same version tag. Requires docker_push_ghcr to have
run first (for registry auth and Trivy installation).
Each supplemental image shares the same version tag. Requires run_trivy_image first
for Trivy installation, and docker_push_ghcr for registry auth only when push is true.
parameters:
push:
type: boolean
default: true
description: Publish scanned images to the registry. Disable for PR scans.
supplemental_images:
type: string
default: "[]"
@@ -16,9 +20,13 @@ parameters:
description: Container registry URL.
steps:
- run:
name: Build and push supplemental images
name: Build and scan supplemental images (optionally push)
environment:
PUSH_IMAGES: << parameters.push >>
SUPPLEMENTAL_IMAGES_JSON: << parameters.supplemental_images >>
IMAGE_NAME: << parameters.image_name >>
REGISTRY: << parameters.registry >>
command: << include(scripts/build_push_supplemental.sh) >>
- store_artifacts:
path: /tmp/trivy-supplemental-results
destination: trivy-supplemental-scans
+5 -2
View File
@@ -39,11 +39,14 @@ parameters:
ci_git_email:
type: string
default: ""
description: Git author email for CD repo commits.
description: >
Deprecated and ignored. GitOps commits are created through the GitHub API,
signed by GitHub and authored by the GITHUB_TOKEN identity.
ci_git_name:
type: string
default: ""
description: Git author name for CD repo commits.
description: >
Deprecated and ignored. See ci_git_email.
supplemental_images:
type: string
default: "[]"
+49
View File
@@ -0,0 +1,49 @@
description: >
Build and scan application and supplemental images without publishing.
Requires the source and version metadata workspace from build-and-test.
parameters:
image_name:
type: string
description: Docker image name (without registry prefix).
registry:
type: string
default: ghcr.io/KofTwentyTwo
description: Container registry URL.
resource_class:
type: enum
enum:
- medium
- large
default: large
description: Machine resource class.
docker_layer_caching:
type: boolean
default: true
description: Enable Docker Layer Caching for faster builds.
supplemental_images:
type: string
default: "[]"
description: JSON array of supplemental images to build and scan.
executor:
name: machine
resource_class: << parameters.resource_class >>
docker_layer_caching: << parameters.docker_layer_caching >>
steps:
- attach_workspace:
at: .
- run:
name: Restore version metadata
command: <<include(scripts/restore_version_metadata.sh)>>
- generate_dockerfile
- docker_build:
image_name: << parameters.image_name >>
registry: << parameters.registry >>
- restore_trivy_cache
- run_trivy_image:
image_tag: << parameters.registry >>/<< parameters.image_name >>:${DOCKER_TAG}
- save_trivy_cache
- build_push_supplemental:
supplemental_images: << parameters.supplemental_images >>
image_name: << parameters.image_name >>
registry: << parameters.registry >>
push: false
+5 -2
View File
@@ -31,11 +31,14 @@ parameters:
ci_git_email:
type: string
default: ""
description: Git author email for CD repo commits.
description: >
Deprecated and ignored. GitOps commits are created through the GitHub API,
signed by GitHub and authored by the GITHUB_TOKEN identity.
ci_git_name:
type: string
default: ""
description: Git author name for CD repo commits.
description: >
Deprecated and ignored. See ci_git_email.
supplemental_images:
type: string
default: "[]"
+28 -8
View File
@@ -27,6 +27,19 @@ IMAGE="${IMAGE_NAME:?IMAGE_NAME not set}"
REGISTRY="${REGISTRY:-ghcr.io/KofTwentyTwo}"
VERSION="${PROJECT_VERSION:?PROJECT_VERSION not set -- run export_version_vars first}"
ENV_TAG="${DOCKER_ENV_TAG:-}"
# CircleCI renders a boolean orb parameter as "1"/"0" in a step environment, not
# "true"/"false", so accept both spellings. Unset means push (release builds).
case "$(echo "${PUSH_IMAGES-true}" | tr '[:upper:]' '[:lower:]')" in
true | 1 | yes | on) PUSH_IMAGES=true ;;
false | 0 | no | off | "") PUSH_IMAGES=false ;;
*)
echo "ERROR: PUSH_IMAGES must be true/false (or 1/0), got '${PUSH_IMAGES}'" >&2
exit 1
;;
esac
# run_trivy_image installs here in a previous CircleCI step.
export PATH="${HOME}/bin:${PATH}"
mkdir -p /tmp/trivy-supplemental-results
COUNT=$(echo "${IMAGES_JSON}" | jq 'length')
echo "Found ${COUNT} supplemental image(s) to build."
@@ -76,26 +89,33 @@ for i in $(seq 0 $((COUNT - 1))); do
fi
docker build "${BUILD_ARGS[@]}" .
if [[ -n "${ENV_TAG}" ]]; then
if [[ "${PUSH_IMAGES}" == "true" && -n "${ENV_TAG}" ]]; then
ENV_FULL_TAG="${REGISTRY}/${DERIVED_NAME}:${ENV_TAG}"
docker tag "${FULL_TAG}" "${ENV_FULL_TAG}"
fi
# Trivy scan
echo "Scanning ${FULL_TAG} with Trivy ..."
trivy image --severity HIGH,CRITICAL --exit-code 1 "${FULL_TAG}"
trivy image --format json --output "/tmp/trivy-supplemental-results/${NAME}.json" \
--severity HIGH,CRITICAL --exit-code 1 "${FULL_TAG}" || {
EXIT_CODE=$?
trivy image --severity HIGH,CRITICAL "${FULL_TAG}"
exit "${EXIT_CODE}"
}
# Push
echo "Pushing ${FULL_TAG} ..."
docker push "${FULL_TAG}"
if [[ "${PUSH_IMAGES}" == "true" ]]; then
echo "Pushing ${FULL_TAG} ..."
docker push "${FULL_TAG}"
if [[ -n "${ENV_TAG}" ]]; then
echo "Pushing ${ENV_FULL_TAG} ..."
docker push "${ENV_FULL_TAG}"
if [[ -n "${ENV_TAG}" ]]; then
echo "Pushing ${ENV_FULL_TAG} ..."
docker push "${ENV_FULL_TAG}"
fi
fi
echo "--- Done: ${DERIVED_NAME} ---"
done
echo ""
echo "All supplemental images built, scanned, and pushed."
echo "All supplemental images built and scanned (push=${PUSH_IMAGES})."
+10 -1
View File
@@ -23,7 +23,16 @@ munitor_header "docker_build (${FULL_IMAGE}:${VERSION})"
munitor_check_tool docker --version
# Build with version tag; add env tag when set (e.g., develop, staging)
BUILD_CMD=(docker build -f "${DOCKERFILE}" -t "${FULL_IMAGE}:${VERSION}")
BUILD_CMD=(docker build --pull -f "${DOCKERFILE}" -t "${FULL_IMAGE}:${VERSION}")
# Generated JVM images only copy a prebuilt JAR. Re-run their package upgrades:
# a cached apk upgrade layer can retain vulnerable packages indefinitely.
CONFIG_FILE="${MUNITOR_CONFIG:-.munitor.yml}"
if [[ -f "${CONFIG_FILE}" ]]; then
munitor_check_tool yq --version
case "$(yq -r '.pipeline' "${CONFIG_FILE}")" in
java-webapp|gradle-webapp) BUILD_CMD+=(--no-cache) ;;
esac
fi
if [[ -n "${ENV_TAG}" ]]; then
BUILD_CMD+=(-t "${FULL_IMAGE}:${ENV_TAG}")
fi
+1 -1
View File
@@ -97,7 +97,7 @@ validate_required_fields "${PIPELINE_TYPE}" "${CONFIG_FILE}"
# --------------------------------------------------------------------------
# Warn about unknown top-level keys (catches typos like java_verion)
# --------------------------------------------------------------------------
KNOWN_KEYS="pipeline orb_version image_name server_module java_version node_version sonar docker cd coverage e2e sbom owasp contexts npm node services test terraform sast health kustomize kube_linter_config package_manager yamllint_paths supplemental_images"
KNOWN_KEYS="pipeline orb_version org image_name server_module java_version node_version sonar docker cd coverage e2e sbom owasp contexts npm node services test terraform sast health kustomize kube_linter_config package_manager yamllint_paths supplemental_images"
ACTUAL_KEYS=$(yq 'keys | .[]' "${CONFIG_FILE}" 2>/dev/null || true)
for key in ${ACTUAL_KEYS}; do
if ! echo "${KNOWN_KEYS}" | grep -qw "${key}"; then
+57 -1
View File
@@ -366,6 +366,20 @@ workflows:
only:
- /feature\/.*/
- /hotfix\/.*/
- munitor/docker_scan:
name: image-scan
image_name: ${MUNITOR_IMAGE_NAME}
registry: ${MUNITOR_DOCKER_REGISTRY}
##IF_SUPPLEMENTAL##
supplemental_images: '${MUNITOR_SUPPLEMENTAL_IMAGES_JSON}'
##ENDIF_SUPPLEMENTAL##
requires:
- build-and-test
filters:
branches:
only:
- /feature\/.*/
- /hotfix\/.*/
- munitor/secrets_scan:
name: secrets-scan
filters:
@@ -694,6 +708,20 @@ workflows:
only:
- /feature\/.*/
- /hotfix\/.*/
- munitor/docker_scan:
name: image-scan
image_name: ${MUNITOR_IMAGE_NAME}
registry: ${MUNITOR_DOCKER_REGISTRY}
##IF_SUPPLEMENTAL##
supplemental_images: '${MUNITOR_SUPPLEMENTAL_IMAGES_JSON}'
##ENDIF_SUPPLEMENTAL##
requires:
- build-and-test
filters:
branches:
only:
- /feature\/.*/
- /hotfix\/.*/
- munitor/secrets_scan:
name: secrets-scan
filters:
@@ -1037,6 +1065,20 @@ workflows:
only:
- /feature\/.*/
- /hotfix\/.*/
- munitor/docker_scan:
name: image-scan
image_name: ${MUNITOR_IMAGE_NAME}
registry: ${MUNITOR_DOCKER_REGISTRY}
##IF_SUPPLEMENTAL##
supplemental_images: '${MUNITOR_SUPPLEMENTAL_IMAGES_JSON}'
##ENDIF_SUPPLEMENTAL##
requires:
- build-and-test
filters:
branches:
only:
- /feature\/.*/
- /hotfix\/.*/
- munitor/secrets_scan:
name: secrets-scan
filters:
@@ -1406,6 +1448,20 @@ workflows:
only:
- /feature\/.*/
- /hotfix\/.*/
- munitor/docker_scan:
name: image-scan
image_name: ${MUNITOR_IMAGE_NAME}
registry: ${MUNITOR_DOCKER_REGISTRY}
##IF_SUPPLEMENTAL##
supplemental_images: '${MUNITOR_SUPPLEMENTAL_IMAGES_JSON}'
##ENDIF_SUPPLEMENTAL##
requires:
- build-and-test
filters:
branches:
only:
- /feature\/.*/
- /hotfix\/.*/
- munitor/secrets_scan:
name: secrets-scan
filters:
@@ -3249,7 +3305,7 @@ validate_required_fields "${PIPELINE_TYPE}" "${CONFIG_FILE}"
# --------------------------------------------------------------------------
# Warn about unknown top-level keys (catches typos like java_verion)
# --------------------------------------------------------------------------
KNOWN_KEYS="pipeline orb_version image_name server_module java_version node_version sonar docker cd coverage e2e sbom owasp contexts npm node services test terraform sast health kustomize kube_linter_config package_manager yamllint_paths supplemental_images"
KNOWN_KEYS="pipeline orb_version org image_name server_module java_version node_version sonar docker cd coverage e2e sbom owasp contexts npm node services test terraform sast health kustomize kube_linter_config package_manager yamllint_paths supplemental_images"
ACTUAL_KEYS=$(yq 'keys | .[]' "${CONFIG_FILE}" 2>/dev/null || true)
for key in ${ACTUAL_KEYS}; do
if ! echo "${KNOWN_KEYS}" | grep -qw "${key}"; then
@@ -16,6 +16,20 @@ workflows:
only:
- /feature\/.*/
- /hotfix\/.*/
- munitor/docker_scan:
name: image-scan
image_name: ${MUNITOR_IMAGE_NAME}
registry: ${MUNITOR_DOCKER_REGISTRY}
##IF_SUPPLEMENTAL##
supplemental_images: '${MUNITOR_SUPPLEMENTAL_IMAGES_JSON}'
##ENDIF_SUPPLEMENTAL##
requires:
- build-and-test
filters:
branches:
only:
- /feature\/.*/
- /hotfix\/.*/
- munitor/secrets_scan:
name: secrets-scan
filters:
+14
View File
@@ -16,6 +16,20 @@ workflows:
only:
- /feature\/.*/
- /hotfix\/.*/
- munitor/docker_scan:
name: image-scan
image_name: ${MUNITOR_IMAGE_NAME}
registry: ${MUNITOR_DOCKER_REGISTRY}
##IF_SUPPLEMENTAL##
supplemental_images: '${MUNITOR_SUPPLEMENTAL_IMAGES_JSON}'
##ENDIF_SUPPLEMENTAL##
requires:
- build-and-test
filters:
branches:
only:
- /feature\/.*/
- /hotfix\/.*/
- munitor/secrets_scan:
name: secrets-scan
filters:
+14
View File
@@ -31,6 +31,20 @@ workflows:
only:
- /feature\/.*/
- /hotfix\/.*/
- munitor/docker_scan:
name: image-scan
image_name: ${MUNITOR_IMAGE_NAME}
registry: ${MUNITOR_DOCKER_REGISTRY}
##IF_SUPPLEMENTAL##
supplemental_images: '${MUNITOR_SUPPLEMENTAL_IMAGES_JSON}'
##ENDIF_SUPPLEMENTAL##
requires:
- build-and-test
filters:
branches:
only:
- /feature\/.*/
- /hotfix\/.*/
- munitor/secrets_scan:
name: secrets-scan
filters:
+14
View File
@@ -31,6 +31,20 @@ workflows:
only:
- /feature\/.*/
- /hotfix\/.*/
- munitor/docker_scan:
name: image-scan
image_name: ${MUNITOR_IMAGE_NAME}
registry: ${MUNITOR_DOCKER_REGISTRY}
##IF_SUPPLEMENTAL##
supplemental_images: '${MUNITOR_SUPPLEMENTAL_IMAGES_JSON}'
##ENDIF_SUPPLEMENTAL##
requires:
- build-and-test
filters:
branches:
only:
- /feature\/.*/
- /hotfix\/.*/
- munitor/secrets_scan:
name: secrets-scan
filters:
+135 -104
View File
@@ -68,128 +68,159 @@ git clone "https://x-access-token:${GITHUB_TOKEN}@github.com/${REPO}.git" "${CLO
cd "${CLONE_DIR}"
if [[ "${FORMAT}" == "kustomize" ]]; then
# --- Kustomize mode ---
if [[ -z "${IMAGE_NAME}" ]]; then
echo "ERROR: cd_image_name is required for Kustomize repos."
exit 1
fi
# Explicit paths support consolidated GitOps repositories. Existing dedicated
# repositories retain the overlays/<environment> convention by default.
if [[ -n "${CONFIGURED_PATH}" ]]; then
if [[ "${CONFIGURED_PATH}" == /* || "${CONFIGURED_PATH}" == *".."* || "${CONFIGURED_PATH}" != *kustomization.yaml ]]; then
echo "ERROR: cd.path must be a relative path to a kustomization.yaml file without '..'."
apply_updates() {
if [[ "${FORMAT}" == "kustomize" ]]; then
# --- Kustomize mode ---
if [[ -z "${IMAGE_NAME}" ]]; then
echo "ERROR: cd_image_name is required for Kustomize repos."
exit 1
fi
KUST_FILE="${CONFIGURED_PATH}"
elif [[ -n "${ENVIRONMENT}" ]]; then
KUST_FILE="overlays/${ENVIRONMENT}/kustomization.yaml"
# Explicit paths support consolidated GitOps repositories. Existing dedicated
# repositories retain the overlays/<environment> convention by default.
if [[ -n "${CONFIGURED_PATH}" ]]; then
if [[ "${CONFIGURED_PATH}" == /* || "${CONFIGURED_PATH}" == *".."* || "${CONFIGURED_PATH}" != *kustomization.yaml ]]; then
echo "ERROR: cd.path must be a relative path to a kustomization.yaml file without '..'."
exit 1
fi
KUST_FILE="${CONFIGURED_PATH}"
elif [[ -n "${ENVIRONMENT}" ]]; then
KUST_FILE="overlays/${ENVIRONMENT}/kustomization.yaml"
else
KUST_FILE="kustomization.yaml"
fi
if [[ ! -f "${KUST_FILE}" ]]; then
echo "ERROR: Kustomize file '${KUST_FILE}' not found in CD repo."
if [[ -z "${CONFIGURED_PATH}" ]]; then
echo "Available overlays:"
ls overlays/ 2>/dev/null || echo " (no overlays directory)"
fi
exit 1
fi
echo "Updating ${KUST_FILE}: ${IMAGE_NAME} -> ${VERSION}"
if ! yq -e ".images[] | select(.name == \"${IMAGE_NAME}\")" "${KUST_FILE}" >/dev/null; then
echo "ERROR: Image '${IMAGE_NAME}' is not declared in ${KUST_FILE}."
exit 1
fi
yq -i "(.images[] | select(.name == \"${IMAGE_NAME}\")).newTag = \"${VERSION}\"" "${KUST_FILE}"
yq -i "del((.images[] | select(.name == \"${IMAGE_NAME}\")).digest)" "${KUST_FILE}"
git add "${KUST_FILE}"
else
KUST_FILE="kustomization.yaml"
fi
# --- Helm mode (default) ---
echo "Updating ${KEY} -> ${VERSION}"
if [[ ! -f "${KUST_FILE}" ]]; then
echo "ERROR: Kustomize file '${KUST_FILE}' not found in CD repo."
if [[ -z "${CONFIGURED_PATH}" ]]; then
echo "Available overlays:"
ls overlays/ 2>/dev/null || echo " (no overlays directory)"
fi
exit 1
fi
echo "Updating ${KUST_FILE}: ${IMAGE_NAME} -> ${VERSION}"
if ! yq -e ".images[] | select(.name == \"${IMAGE_NAME}\")" "${KUST_FILE}" >/dev/null; then
echo "ERROR: Image '${IMAGE_NAME}' is not declared in ${KUST_FILE}."
exit 1
fi
yq -i "(.images[] | select(.name == \"${IMAGE_NAME}\")).newTag = \"${VERSION}\"" "${KUST_FILE}"
yq -i "del((.images[] | select(.name == \"${IMAGE_NAME}\")).digest)" "${KUST_FILE}"
git add "${KUST_FILE}"
else
# --- Helm mode (default) ---
echo "Updating ${KEY} -> ${VERSION}"
# Resolve environment-specific values path when CD_ENVIRONMENT is set
if [[ -n "${ENVIRONMENT}" ]]; then
ENV_VALUES="environments/${ENVIRONMENT}/${VALUES}"
if [[ -f "${ENV_VALUES}" ]]; then
VALUES="${ENV_VALUES}"
echo "Using environment path: ${VALUES}"
else
echo "Environment path '${ENV_VALUES}' not found, falling back to root '${VALUES}'"
fi
fi
if [[ ! -f "${VALUES}" ]]; then
echo "ERROR: Values file '${VALUES}' not found in CD repo."
exit 1
fi
yq -i ".${KEY} = \"${VERSION}\"" "${VALUES}"
git add "${VALUES}"
fi
# Update supplemental image tags (if any)
SUPP_JSON="${SUPPLEMENTAL_IMAGES_JSON:-[]}"
if [[ "${SUPP_JSON}" != "[]" && "${SUPP_JSON}" != "null" ]]; then
SUPP_COUNT=$(echo "${SUPP_JSON}" | jq 'length')
echo "Updating ${SUPP_COUNT} supplemental image tag(s)..."
for ((i = 0; i < SUPP_COUNT; i++)); do
SUPP_NAME=$(echo "${SUPP_JSON}" | jq -r ".[$i].name")
if [[ "${FORMAT}" == "kustomize" ]]; then
SUPP_CD_IMAGE=$(echo "${SUPP_JSON}" | jq -r ".[$i].cd.image_name // \"\"")
if [[ -z "${SUPP_CD_IMAGE}" ]]; then
SUPP_CD_IMAGE="${IMAGE_NAME}-${SUPP_NAME}"
fi
echo " Kustomize: ${SUPP_CD_IMAGE} -> ${VERSION}"
# Upsert: add entry if it doesn't exist, then update
EXISTING=$(yq ".images[] | select(.name == \"${SUPP_CD_IMAGE}\") | .name" "${KUST_FILE}")
if [[ -z "${EXISTING}" ]]; then
echo " (creating new image entry)"
yq -i ".images += [{\"name\": \"${SUPP_CD_IMAGE}\", \"newTag\": \"${VERSION}\"}]" "${KUST_FILE}"
# Resolve environment-specific values path when CD_ENVIRONMENT is set
if [[ -n "${ENVIRONMENT}" ]]; then
ENV_VALUES="environments/${ENVIRONMENT}/${VALUES}"
if [[ -f "${ENV_VALUES}" ]]; then
VALUES="${ENV_VALUES}"
echo "Using environment path: ${VALUES}"
else
yq -i "(.images[] | select(.name == \"${SUPP_CD_IMAGE}\")).newTag = \"${VERSION}\"" "${KUST_FILE}"
yq -i "del((.images[] | select(.name == \"${SUPP_CD_IMAGE}\")).digest)" "${KUST_FILE}"
fi
git add "${KUST_FILE}"
else
SUPP_IMAGE_KEY=$(echo "${SUPP_JSON}" | jq -r ".[$i].cd.image_key // \"\"")
if [[ -n "${SUPP_IMAGE_KEY}" ]]; then
echo " Helm: ${SUPP_IMAGE_KEY} -> ${VERSION}"
yq -i ".${SUPP_IMAGE_KEY} = \"${VERSION}\"" "${VALUES}"
git add "${VALUES}"
echo "Environment path '${ENV_VALUES}' not found, falling back to root '${VALUES}'"
fi
fi
done
fi
# Commit and push
git config user.email "${CI_GIT_EMAIL:-${MUNITOR_CI_EMAIL:-munitor-ci@koftwentytwo.com}}"
git config user.name "${CI_GIT_NAME:-${MUNITOR_CI_NAME:-Munitor CI}}"
if [[ ! -f "${VALUES}" ]]; then
echo "ERROR: Values file '${VALUES}' not found in CD repo."
exit 1
fi
if git diff --cached --quiet; then
echo "No changes to commit (version already up to date)."
exit 0
fi
yq -i ".${KEY} = \"${VERSION}\"" "${VALUES}"
git add "${VALUES}"
fi
git commit -m "chore: update ${IMAGE_NAME:-image} to ${VERSION}" -m "Source: ${CIRCLE_PROJECT_REPONAME:-unknown}@${CIRCLE_SHA1:-unknown}"
# Update supplemental image tags (if any)
SUPP_JSON="${SUPPLEMENTAL_IMAGES_JSON:-[]}"
if [[ "${SUPP_JSON}" != "[]" && "${SUPP_JSON}" != "null" ]]; then
SUPP_COUNT=$(echo "${SUPP_JSON}" | jq 'length')
echo "Updating ${SUPP_COUNT} supplemental image tag(s)..."
for ((i = 0; i < SUPP_COUNT; i++)); do
SUPP_NAME=$(echo "${SUPP_JSON}" | jq -r ".[$i].name")
if [[ "${FORMAT}" == "kustomize" ]]; then
SUPP_CD_IMAGE=$(echo "${SUPP_JSON}" | jq -r ".[$i].cd.image_name // \"\"")
if [[ -z "${SUPP_CD_IMAGE}" ]]; then
SUPP_CD_IMAGE="${IMAGE_NAME}-${SUPP_NAME}"
fi
echo " Kustomize: ${SUPP_CD_IMAGE} -> ${VERSION}"
# Upsert: add entry if it doesn't exist, then update
EXISTING=$(yq ".images[] | select(.name == \"${SUPP_CD_IMAGE}\") | .name" "${KUST_FILE}")
if [[ -z "${EXISTING}" ]]; then
echo " (creating new image entry)"
yq -i ".images += [{\"name\": \"${SUPP_CD_IMAGE}\", \"newTag\": \"${VERSION}\"}]" "${KUST_FILE}"
else
yq -i "(.images[] | select(.name == \"${SUPP_CD_IMAGE}\")).newTag = \"${VERSION}\"" "${KUST_FILE}"
yq -i "del((.images[] | select(.name == \"${SUPP_CD_IMAGE}\")).digest)" "${KUST_FILE}"
fi
git add "${KUST_FILE}"
else
SUPP_IMAGE_KEY=$(echo "${SUPP_JSON}" | jq -r ".[$i].cd.image_key // \"\"")
if [[ -n "${SUPP_IMAGE_KEY}" ]]; then
echo " Helm: ${SUPP_IMAGE_KEY} -> ${VERSION}"
yq -i ".${SUPP_IMAGE_KEY} = \"${VERSION}\"" "${VALUES}"
git add "${VALUES}"
fi
fi
done
fi
}
# Commit through the GitHub GraphQL API (createCommitOnBranch) rather than pushing local commits.
# GitHub signs API-created commits, so they show as Verified and satisfy a
# "require signed commits" ruleset on the GitOps branch without a bypass actor.
# The commit author is the GITHUB_TOKEN identity; the source build is recorded
# in the commit body. expectedHeadOid makes concurrent updates fail cleanly and
# retry against the new head instead of overwriting each other.
BRANCH="$(git branch --show-current)"
HEADLINE="chore: update ${IMAGE_NAME:-image} to ${VERSION}"
MESSAGE_BODY="Source: ${CIRCLE_PROJECT_REPONAME:-unknown}@${CIRCLE_SHA1:-unknown}"
commit_via_api() {
local head_oid additions payload response file
head_oid="$(git rev-parse HEAD)"
additions='[]'
while IFS= read -r file; do
base64 -w0 "${file}" > /tmp/cd-file.b64
additions="$(jq --arg path "${file}" --rawfile contents /tmp/cd-file.b64 '. + [{path: $path, contents: $contents}]' <<< "${additions}")"
done < <(git diff --cached --name-only)
payload="$(jq -n --arg repo "${REPO}" --arg branch "${BRANCH}" --arg oid "${head_oid}" --arg headline "${HEADLINE}" --arg body "${MESSAGE_BODY}" --argjson additions "${additions}" '{query: "mutation($input: CreateCommitOnBranchInput!) { createCommitOnBranch(input: $input) { commit { oid url } } }",
variables: {input: {branch: {repositoryNameWithOwner: $repo, branchName: $branch},
expectedHeadOid: $oid, message: {headline: $headline, body: $body},
fileChanges: {additions: $additions}}}}')"
if ! response="$(curl -sS -X POST -H "Authorization: bearer ${GITHUB_TOKEN}" -H "Content-Type: application/json" https://api.github.com/graphql --data @- <<< "${payload}")"; then
echo "GraphQL request failed."
return 1
fi
if [[ "$(jq -r '.data.createCommitOnBranch.commit.oid // empty' <<< "${response}")" == "" ]]; then
echo "GraphQL commit rejected: $(jq -c '.errors // .' <<< "${response}")"
return 1
fi
echo "Signed commit: $(jq -r '.data.createCommitOnBranch.commit.url' <<< "${response}")"
}
# Independent pipelines may update a shared GitOps repository concurrently.
for attempt in 1 2 3; do
if git push origin HEAD; then
apply_updates
if git diff --cached --quiet; then
echo "No changes to commit (version already up to date)."
exit 0
fi
if commit_via_api; then
echo "CD repo updated successfully."
exit 0
fi
if [[ "${attempt}" == "3" ]]; then
echo "ERROR: Could not push the GitOps update after ${attempt} attempts."
echo "ERROR: Could not commit the GitOps update after ${attempt} attempts."
exit 1
fi
echo "Push raced with another GitOps update; rebasing before retry ${attempt}/3."
git pull --rebase origin "$(git branch --show-current)"
echo "Commit raced with another GitOps update; refreshing before retry ${attempt}/3."
git fetch -q origin "${BRANCH}"
git reset -q --hard "origin/${BRANCH}"
done
+6
View File
@@ -0,0 +1,6 @@
#!/usr/bin/env bats
@test "PR and release JVM builds refresh packages without disabling Node caching or Trivy" {
run bash "${BATS_TEST_DIRNAME}/test_docker_build.sh"
[ "$status" -eq 0 ]
}
+53
View File
@@ -0,0 +1,53 @@
#!/usr/bin/env bash
# Assert the actual shared PR/release build command without a Docker daemon.
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
PROJECT_DIR="$(cd "${SCRIPT_DIR}/.." && pwd)"
WORK=$(mktemp -d)
trap 'rm -rf "${WORK}"' EXIT
mkdir -p "${WORK}/bin" "${WORK}/repo"
cat > "${WORK}/bin/docker" <<'MOCK'
#!/usr/bin/env bash
[[ "$1" == --version ]] && exit 0
printf '%s\n' "$@" > "${CALL_LOG}"
MOCK
cat > "${WORK}/bin/yq" <<'MOCK'
#!/usr/bin/env bash
[[ "$1" == --version ]] && exit 0
[[ "$1" == -r && "$2" == .pipeline ]] || exit 2
sed -n 's/^pipeline: //p' "$3"
MOCK
chmod +x "${WORK}/bin/docker" "${WORK}/bin/yq"
export PATH="${WORK}/bin:${PATH}"
export CALL_LOG="${WORK}/calls" BASH_ENV="${WORK}/env"
export IMAGE_NAME=app REGISTRY=ghcr.io/example PROJECT_VERSION=1.2.3
export CIRCLE_SHA1=abc123 DOCKER_ENV_TAG=develop
cd "${WORK}/repo"
for pipeline in java-webapp gradle-webapp node-api node-webapp; do
printf 'pipeline: %s\n' "${pipeline}" > .munitor.yml
bash "${PROJECT_DIR}/src/scripts/docker_build.sh" > "${WORK}/output"
grep -qx -- '--pull' "${CALL_LOG}"
if [[ "${pipeline}" == java-webapp || "${pipeline}" == gradle-webapp ]]; then
grep -qx -- '--no-cache' "${CALL_LOG}"
else
! grep -qx -- '--no-cache' "${CALL_LOG}"
fi
grep -qx 'org.opencontainers.image.revision=abc123' "${CALL_LOG}"
grep -qx 'ghcr.io/example/app:1.2.3' "${CALL_LOG}"
grep -qx 'ghcr.io/example/app:develop' "${CALL_LOG}"
echo "PASS: ${pipeline} cache policy, provenance and tags"
done
mv .munitor.yml 'custom config.yml'
printf 'pipeline: java-webapp\n' > 'custom config.yml'
MUNITOR_CONFIG='custom config.yml' bash "${PROJECT_DIR}/src/scripts/docker_build.sh" > "${WORK}/output"
grep -qx -- '--no-cache' "${CALL_LOG}"
echo 'PASS: alternate manifest path'
bash "${PROJECT_DIR}/src/scripts/docker_build.sh" > "${WORK}/output"
grep -qx -- '--pull' "${CALL_LOG}"
! grep -qx -- '--no-cache' "${CALL_LOG}"
echo 'PASS: standalone build without manifest'
for job in docker_scan docker_build_push; do
grep -q '^ - docker_build:' "${PROJECT_DIR}/src/jobs/${job}.yml"
grep -q '^ - run_trivy_image:' "${PROJECT_DIR}/src/jobs/${job}.yml"
echo "PASS: ${job} uses shared builder and retains Trivy"
done
+134
View File
@@ -0,0 +1,134 @@
#!/usr/bin/env bash
# Exercise image scanning with local tool stubs; no Docker daemon or network needed.
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
SRC_SCRIPTS="$(cd "${SCRIPT_DIR}/../src/scripts" && pwd)"
WORK=$(mktemp -d)
trap 'rm -rf "${WORK}"' EXIT
PASS=0
FAIL=0
pass() { echo "PASS"; PASS=$((PASS + 1)); }
fail() { echo "FAIL"; FAIL=$((FAIL + 1)); }
mkdir -p "${WORK}/bin" "${WORK}/home/bin" "${WORK}/repo"
cat > "${WORK}/bin/docker" <<'EOF'
#!/usr/bin/env bash
printf 'docker %s\n' "$*" >> "${CALL_LOG}"
EOF
cat > "${WORK}/home/bin/trivy" <<'EOF'
#!/usr/bin/env bash
[[ -f .trivyignore ]] || exit 99
printf 'trivy %s\n' "$*" >> "${CALL_LOG}"
# Redirect the production /tmp output path into the test workspace.
while [[ $# -gt 0 ]]; do
if [[ "$1" == --output ]]; then
printf '{"Results":[]}\n' > "${REPORT_DIR}/$(basename "$2")"
fi
shift
done
exit "${SCAN_EXIT:-0}"
EOF
chmod +x "${WORK}/bin/docker" "${WORK}/home/bin/trivy"
export PATH="${WORK}/bin:${PATH}" HOME="${WORK}/home"
export CALL_LOG="${WORK}/calls" REPORT_DIR="${WORK}"
export IMAGE_NAME=app REGISTRY=ghcr.io/example CIRCLE_SHA1=abcdef123456
export SUPPLEMENTAL_IMAGES_JSON='[{"name":"migrations","dockerfile":"Dockerfile.migrations"},{"name":"worker","base":"example/base:1"}]'
cd "${WORK}/repo"
touch .trivyignore
echo "=== PR image scan tests ==="
echo -n " TEST: feature metadata survives workspace restore and supplies Docker scan tag... "
export BASH_ENV="${WORK}/build-env" CIRCLE_BRANCH=feature/image-scan
export GITVERSION_MAJOR=1 GITVERSION_MINOR=2 GITVERSION_PATCH=3
export GITVERSION_SHA=abcdef123456
bash "${SRC_SCRIPTS}/export_version_vars.sh" > "${WORK}/version.log"
bash "${SRC_SCRIPTS}/persist_version_metadata.sh" >> "${WORK}/version.log"
export BASH_ENV="${WORK}/scan-env"
bash "${SRC_SCRIPTS}/restore_version_metadata.sh" >> "${WORK}/version.log"
bash "${SRC_SCRIPTS}/docker_build.sh" >> "${WORK}/version.log"
# shellcheck disable=SC1090
source "${BASH_ENV}"
export IMAGE_TAG='ghcr.io/example/app:${DOCKER_TAG}'
if bash "${SRC_SCRIPTS}/run_trivy_image.sh" >> "${WORK}/version.log" &&
grep -q 'trivy image .*--severity HIGH,CRITICAL --exit-code 1 ghcr.io/example/app:1.2.3-image-scan-abcdef123456-SNAPSHOT' "${CALL_LOG}"; then
pass
else
fail
fi
echo -n " TEST: push=false builds and scans every supplemental without publishing... "
: > "${CALL_LOG}"
echo 'export DOCKER_ENV_TAG=develop' >> "${BASH_ENV}"
if PUSH_IMAGES=false bash "${SRC_SCRIPTS}/build_push_supplemental.sh" > "${WORK}/supplemental.log" &&
[[ $(grep -c '^docker build ' "${CALL_LOG}") == 2 ]] &&
[[ $(grep -c '^trivy image .*--severity HIGH,CRITICAL --exit-code 1 ' "${CALL_LOG}") == 2 ]] &&
! grep -Eq '^docker (push|login|tag|buildx) ' "${CALL_LOG}" &&
[[ -s "${WORK}/migrations.json" && -s "${WORK}/worker.json" ]]; then
pass
else
fail
fi
echo -n " TEST: default push still publishes version and environment tags... "
: > "${CALL_LOG}"
unset PUSH_IMAGES
if bash "${SRC_SCRIPTS}/build_push_supplemental.sh" > "${WORK}/supplemental.log" &&
[[ $(grep -c '^docker push ' "${CALL_LOG}") == 4 ]]; then
pass
else
fail
fi
# Regression (0.3.5): CircleCI renders the boolean "push" parameter as "1"/"0" in
# the step environment, and release builds silently skipped the push.
for push in 1 true TRUE; do
echo -n " TEST: push=${push} (release builds) publishes every supplemental image... "
: > "${CALL_LOG}"
if PUSH_IMAGES="${push}" bash "${SRC_SCRIPTS}/build_push_supplemental.sh" > "${WORK}/supplemental.log" &&
[[ $(grep -c '^docker push ' "${CALL_LOG}") == 4 ]] &&
grep -q '^docker push ghcr.io/example/app-migrations:' "${CALL_LOG}" &&
grep -q '^docker push ghcr.io/example/app-worker:' "${CALL_LOG}" &&
grep -q 'push=true' "${WORK}/supplemental.log"; then
pass
else
fail
fi
done
for push in 0 false ""; do
echo -n " TEST: push='${push}' (PR scans) publishes nothing... "
: > "${CALL_LOG}"
if PUSH_IMAGES="${push}" bash "${SRC_SCRIPTS}/build_push_supplemental.sh" > "${WORK}/supplemental.log" &&
! grep -Eq '^docker (push|tag) ' "${CALL_LOG}"; then
pass
else
fail
fi
done
echo -n " TEST: an unrecognised push value fails instead of guessing... "
: > "${CALL_LOG}"
if PUSH_IMAGES=maybe bash "${SRC_SCRIPTS}/build_push_supplemental.sh" > "${WORK}/supplemental.log" 2>&1; then
fail
elif ! grep -Eq '^docker (build|tag|push) ' "${CALL_LOG}" && grep -q "must be true/false" "${WORK}/supplemental.log"; then
pass
else
fail
fi
for push in false true; do
echo -n " TEST: push=${push} fails on HIGH/CRITICAL findings before any push... "
: > "${CALL_LOG}"
rm -f "${WORK}/migrations.json"
if SCAN_EXIT=1 PUSH_IMAGES="${push}" bash "${SRC_SCRIPTS}/build_push_supplemental.sh" > "${WORK}/supplemental.log" 2>&1; then
fail
elif ! grep -q '^docker push ' "${CALL_LOG}" && [[ -s "${WORK}/migrations.json" ]]; then
pass
else
fail
fi
done
echo "Results: ${PASS} passed, ${FAIL} failed"
[[ ${FAIL} -eq 0 ]]
+30
View File
@@ -157,6 +157,36 @@ run_negative_context_test() {
echo "=== Template Rendering Tests ==="
# Container scans belong only to PR jobs; RC already builds and scans before release.
for module in java-webapp gradle-webapp node-api node-webapp; do
fixture="${FIXTURES_DIR}/${module}.munitor.yml"
run_context_test "${module}: PR builds and scans the image" \
"${fixture}" "pr-checks:" " develop:" 'munitor/docker_scan:'
run_context_test "${module}: image scan is named image-scan" \
"${fixture}" "munitor/docker_scan:" "munitor/secrets_scan:" 'name: image-scan'
run_context_test "${module}: image scan waits for build-and-test" \
"${fixture}" "munitor/docker_scan:" "munitor/secrets_scan:" '\- build-and-test'
run_negative_context_test "${module}: no duplicate scan outside PR workflow" \
"${fixture}" " develop:" "END_OF_CONFIG" 'munitor/docker_scan:'
run_context_test "${module}: RC already builds and scans" \
"${fixture}" "release-candidate:" "production:" 'munitor/docker_build_push:'
echo -n " TEST: ${module}: every image scan has exactly the PR branch filters... "
output=$(render_fixture "${fixture}")
filters=$(yq -o=json -I=0 '[.workflows[].jobs[] | select(has("munitor/docker_scan")) | ."munitor/docker_scan".filters.branches.only]' <<< "${output}")
if [[ "${filters}" == '[["/feature\\/.*/","/hotfix\\/.*/"]]' ]]; then
echo "PASS"
PASS=$((PASS + 1))
else
echo "FAIL (unexpected scan filters: ${filters})"
FAIL=$((FAIL + 1))
fi
done
run_context_test "supplemental images are wired into the PR image scan" \
"${FIXTURES_DIR}/java-webapp-supplemental.munitor.yml" \
"munitor/docker_scan:" "munitor/secrets_scan:" 'supplemental_images:.*migrations'
# Test java-webapp renders with correct values
run_test "java-webapp: renders image_name" \
"${FIXTURES_DIR}/java-webapp.munitor.yml" \
+12 -3
View File
@@ -328,12 +328,21 @@ else
fail "should support and validate an explicit Kustomize path"
fi
echo -n " TEST: retries concurrent shared-repository pushes... "
echo -n " TEST: retries concurrent shared-repository commits against the new head... "
if grep -q 'for attempt in 1 2 3' "${SRC_SCRIPTS}/update_cd_repo.sh" &&
grep -q 'git pull --rebase' "${SRC_SCRIPTS}/update_cd_repo.sh"; then
grep -q 'expectedHeadOid' "${SRC_SCRIPTS}/update_cd_repo.sh" &&
grep -q 'git reset -q --hard "origin/${BRANCH}"' "${SRC_SCRIPTS}/update_cd_repo.sh"; then
pass
else
fail "should rebase and retry shared-repository push races"
fail "should retry commit races against the refreshed branch head"
fi
echo -n " TEST: commits through the signed GraphQL API, never git push... "
if grep -q 'createCommitOnBranch' "${SRC_SCRIPTS}/update_cd_repo.sh" &&
! grep -q 'git push' "${SRC_SCRIPTS}/update_cd_repo.sh"; then
pass
else
fail "should create GitOps commits with createCommitOnBranch so GitHub signs them"
fi
echo -n " TEST: deletes digest field when setting newTag (kustomize)... "