Compare commits
59
Commits
release/0.3.2
...
develop
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
cc435bb7eb | ||
|
|
e4500339b4 | ||
|
|
7f85781879
|
||
|
|
d76a46ce11 | ||
|
|
18ea857841
|
||
|
|
ee31744cb9 | ||
|
|
73c3d4d753
|
||
|
|
81ac29eec4 | ||
|
|
19192d07a5 | ||
|
|
d5a7d59e96
|
||
|
|
f9ef625019 | ||
|
|
a56ef1d8e7
|
||
|
|
674fcd1bc7 | ||
|
|
7e89e28a7a | ||
|
|
154d453a00
|
||
|
|
64831e83f1 | ||
|
|
77cdf931c2
|
||
|
|
d7c278b344
|
||
|
|
c8fd974bfd
|
||
|
|
6cd50b0a90 | ||
|
|
5d4a9ea420 | ||
|
|
f402fb8347
|
||
|
|
11d5704b47 | ||
|
|
576de90e03
|
||
|
|
9678be0cff | ||
|
|
baca55bd77 | ||
|
|
01176862d4
|
||
|
|
f7487b356a | ||
|
|
21517b25e7
|
||
|
|
34ddd25ac9 | ||
|
|
4aeccb801e
|
||
|
|
a3d680f130 | ||
|
|
57dffdd9a3 | ||
|
|
9e2c45e79a | ||
|
|
fbeec65cb5 | ||
|
|
69898922be
|
||
|
|
59eb5294fe | ||
|
|
16079c5ca8 | ||
|
|
578c7ea0eb | ||
|
|
53c04937b0 | ||
|
|
b09ced056f
|
||
|
|
fb232fb6fe | ||
|
|
687944e872 | ||
|
|
e54d739554 | ||
|
|
71c262fb62
|
||
|
|
6cbe4256d5 | ||
|
|
d7960161fb
|
||
|
|
807120f2af
|
||
|
|
1c64d1e18e
|
||
|
|
24482fd030
|
||
|
|
ad1d30681b | ||
|
|
b4a9d120eb
|
||
|
|
b3b90f5886 | ||
|
|
d9b4da9e9e | ||
|
|
4b99bbec09
|
||
|
|
00f349cb0c | ||
|
|
57cb7a2aca
|
||
|
|
f1d26b69d7 | ||
|
|
ac3eb0e162 |
No files matched your search
+64
-9
@@ -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,33 @@ tag-only: &tag-only
|
||||
tags:
|
||||
only: /^v[0-9]+\.[0-9]+\.[0-9]+$/
|
||||
|
||||
jobs:
|
||||
ci-ready:
|
||||
type: no-op
|
||||
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,36 +51,63 @@ workflows:
|
||||
- orb-tools/review:
|
||||
filters: *filters
|
||||
exclude: RC006,RC007,RC010 # RC006/RC007: private repo URL unreachable; RC010: machine executors
|
||||
- test:
|
||||
filters: *filters
|
||||
|
||||
- ci-ready:
|
||||
requires:
|
||||
- orb-tools/lint
|
||||
- orb-tools/pack
|
||||
- orb-tools/review
|
||||
- test
|
||||
filters: *filters
|
||||
|
||||
test-deploy:
|
||||
jobs:
|
||||
- orb-tools/pack:
|
||||
filters: *tag-only
|
||||
- orb-tools/publish:
|
||||
orb_name: kof22/munitor
|
||||
vcs_type: << pipeline.project.type >>
|
||||
pub_type: production
|
||||
requires:
|
||||
- orb-tools/lint
|
||||
- orb-tools/pack
|
||||
- orb-tools/review
|
||||
- test
|
||||
context:
|
||||
- orb-publishing
|
||||
filters: *tag-only
|
||||
|
||||
snapshot:
|
||||
jobs:
|
||||
- orb-tools/pack:
|
||||
filters: *filters
|
||||
# All publishers consume the same packed workspace after all checks pass.
|
||||
# Only develop updates dev:snapshot; feature branches publish dev:<sha>.
|
||||
- orb-tools/publish:
|
||||
name: publish-develop-snapshot
|
||||
orb_name: kof22/munitor
|
||||
vcs_type: << pipeline.project.type >>
|
||||
pub_type: dev
|
||||
dev_tags: "dev:${CIRCLE_SHA1},dev:snapshot"
|
||||
requires:
|
||||
- orb-tools/lint
|
||||
- orb-tools/pack
|
||||
- orb-tools/review
|
||||
- test
|
||||
context:
|
||||
- orb-publishing
|
||||
filters:
|
||||
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/lint
|
||||
- orb-tools/pack
|
||||
- orb-tools/review
|
||||
- test
|
||||
context:
|
||||
- orb-publishing
|
||||
filters:
|
||||
branches:
|
||||
only:
|
||||
- /feature\/.*/
|
||||
@@ -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
|
||||
@@ -0,0 +1,3 @@
|
||||
# Every path has an owner (K22-REPO-08). Add narrower rules below for areas that
|
||||
# need a specific reviewer once the project has more than one maintainer.
|
||||
* @KofTwentyTwo
|
||||
@@ -5,7 +5,7 @@ CircleCI orb that generates CI/CD pipeline configurations from a `.munitor.yml`
|
||||
## Key Commands
|
||||
|
||||
```bash
|
||||
make test # Run all tests (BATS)
|
||||
make test-scripts # Script unit tests (bats tests for the .bats suites)
|
||||
make pack # Pack orb YAML from src/
|
||||
make all # lint + pack + test
|
||||
make validate # Validate packed orb with CircleCI CLI
|
||||
@@ -29,15 +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`
|
||||
- Release candidate: `kof22/munitor@0.3.2` (stable tag publication pending)
|
||||
- Latest stable: `kof22/munitor@0.3.11`
|
||||
- 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
|
||||
|
||||
|
||||
@@ -1,5 +1,14 @@
|
||||
# Contributing to Munitor
|
||||
|
||||
## 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` 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 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.
|
||||
|
||||
## Module Inventory
|
||||
|
||||
Munitor organizes CI/CD pipelines into **modules** (called "pipeline types" in `.munitor.yml`). Each module is a self-contained set of templates, jobs, commands, and scripts that handle a specific application stack.
|
||||
@@ -323,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`) |
|
||||
|
||||
@@ -30,7 +30,7 @@ lint:
|
||||
test-scripts:
|
||||
@echo "Running script tests..."
|
||||
@for f in tests/test_*.sh; do \
|
||||
if [ -f "$$f" ]; then \
|
||||
if [ -f "$$f" ] && [ "$$f" != "tests/test_generate_config.sh" ]; then \
|
||||
echo " Running $$f"; \
|
||||
bash "$$f" || exit 1; \
|
||||
fi; \
|
||||
|
||||
@@ -2,6 +2,14 @@
|
||||
|
||||
Latin: *munitor* (fortifier, protector) -- KofTwentyTwo's unified CircleCI orb for building, testing, and deploying all repositories. Pipeline changes happen in one place instead of 80+ repos.
|
||||
|
||||
| | |
|
||||
| --- | --- |
|
||||
| **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.11` |
|
||||
| **Security** | [SECURITY.md](SECURITY.md) |
|
||||
| **Contributing** | [CONTRIBUTING.md](CONTRIBUTING.md) |
|
||||
|
||||
## How It Works
|
||||
|
||||
Munitor uses CircleCI's [dynamic configuration](https://circleci.com/docs/dynamic-config/) to generate your full CI/CD pipeline at runtime. Each repo has a minimal `.circleci/config.yml` that triggers Munitor, and a `.munitor.yml` that declares what your project needs. Munitor reads your config, selects the right template, and generates a complete CircleCI workflow.
|
||||
@@ -44,6 +52,47 @@ Munitor validates branch names and will fail on non-conforming names.
|
||||
| `release/<version>` | Release prep |
|
||||
| `hotfix/<name>` | Production fixes |
|
||||
|
||||
## Optional docs-only Java checks
|
||||
|
||||
The new `java-webapp` docs-only path is opt-in and must be adopted **after** the
|
||||
aggregate required check is published and verified. Do not enable it against an
|
||||
older orb or while individual build jobs remain required:
|
||||
|
||||
```yaml
|
||||
ci:
|
||||
docs_only: true
|
||||
```
|
||||
|
||||
Only changes confined to root `README.md`, `CHANGELOG.md`, `CONTRIBUTING.md`, or
|
||||
Markdown files under `docs/` (including nested directories) qualify. Feature and
|
||||
hotfix branches use their complete diff from the merge base with `main`; `main`
|
||||
uses its first-parent diff. Tags, release and other branches, shallow history,
|
||||
failed comparisons, empty diffs, and any uncertain classification keep the full
|
||||
pipeline. The docs path retains secret scanning and emits the aggregate gate;
|
||||
it does not build or deploy an image.
|
||||
|
||||
Migration order: publish the gate-capable orb, adopt it with docs-only disabled,
|
||||
and verify the real GitHub context `ci/circleci: ci-ready` on a consumer run. Then
|
||||
replace that repository's required-check list with the verified aggregate gate,
|
||||
preserving all other branch rules. A single adoption PR may update both orb pins
|
||||
and enable `ci.docs_only`: those config changes force full CI. Verify the gate
|
||||
and migrate required checks before merging it or opening a docs-only PR. Munitor's
|
||||
own native no-op gate reports `ci/circleci: lint-pack/ci-ready`. Both features
|
||||
ship in 0.3.11. Verify each consumer's actual successful context before changing
|
||||
its rules; see [the adoption record](docs/PLAN-agile-ci.md).
|
||||
|
||||
Use native [Website-Backend Insights](https://app.circleci.com/insights/github/KofTwentyTwo/Website-Backend/workflows),
|
||||
[PR timings](https://app.circleci.com/insights/github/KofTwentyTwo/Website-Backend/workflows/pr-checks),
|
||||
and [Test Insights](https://app.circleci.com/insights/github/KofTwentyTwo/Website-Backend/workflows/pr-checks/tests)
|
||||
to measure results. Compare matching branches/windows and track the `docs-checks`
|
||||
median separately from full builds. The historical local report
|
||||
`C:/Users/james/.cache/ci-insights-report.md` ends October 17 and does not establish
|
||||
the speed of this change. No additional reporting job is needed.
|
||||
|
||||
Native auto-cancel is enabled for Website-Backend, whose default `main` is exempt.
|
||||
It remains disabled for Munitor: its default is `develop`, so `main` release work
|
||||
would not receive that exemption.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
1. **CircleCI project**: Add your repo at https://app.circleci.com
|
||||
@@ -58,6 +107,93 @@ Munitor validates branch names and will fail on non-conforming names.
|
||||
| `sonarcloud` | `SONAR_TOKEN` | SonarCloud analysis |
|
||||
| `nvd` | `NVD_API_KEY` | OWASP dependency check (Java only, optional) |
|
||||
|
||||
## 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 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.11` (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.11`.
|
||||
- `.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.11"
|
||||
image_name: my-service
|
||||
java_version: "21"
|
||||
docker:
|
||||
registry: ghcr.io/my-org # the org default is ghcr.io/KofTwentyTwo -- always set this
|
||||
cd:
|
||||
repo: my-org/my-gitops-repo # omit to skip GitOps write-back
|
||||
format: kustomize # default is helm (image.tag in values.yaml)
|
||||
path: apps/my-service/kustomization.yaml # optional explicit file in a consolidated repo
|
||||
production_only: true # only main updates the GitOps repo
|
||||
supplemental_images: # optional extra images built from the same commit and version
|
||||
- name: migrations
|
||||
dockerfile: docker/Dockerfile.migrations
|
||||
cd:
|
||||
image_name: ghcr.io/my-org/my-service-migrations
|
||||
health:
|
||||
path: /health
|
||||
port: "8000"
|
||||
db: "true" # start a PostgreSQL sidecar for the health gate
|
||||
migrations: migrations # run the <image>-migrations image against it first
|
||||
smoke_paths: [/health]
|
||||
sonar:
|
||||
project_key: my-org_my-service # omit to skip SonarCloud
|
||||
contexts:
|
||||
registry: ghcr
|
||||
github: github
|
||||
sonar: sonarcloud # required only with sonar.project_key
|
||||
nvd: nvd # optional
|
||||
org: # organization defaults (see Known limitations)
|
||||
ci_email: ci@example.com
|
||||
ci_name: My CI
|
||||
```
|
||||
|
||||
- A health endpoint that returns HTTP 200 within 30 seconds of container start (see [Health Check Requirement](#health-check-requirement)).
|
||||
- A Dockerfile only for `node-webapp` (`docker.use_repo_dockerfile: true`) or for `supplemental_images`; `java-webapp` and `node-api` generate theirs.
|
||||
|
||||
### 2. CircleCI project settings
|
||||
|
||||
See [Prerequisites](#prerequisites): enable dynamic config (setup workflows) and allow uncertified orbs in the consuming organization.
|
||||
|
||||
### 3. Contexts and secrets
|
||||
|
||||
CircleCI organization contexts; the context names are set through `contexts.*`.
|
||||
|
||||
| Context | Variables | Needed for |
|
||||
| --- | --- | --- |
|
||||
| registry (e.g. `ghcr`) | `GHCR_USERNAME`, `GHCR_TOKEN` (`write:packages`) | Pushing images and inspecting existing tags; the immutable-tag guard fails closed without registry access |
|
||||
| github | `GITHUB_TOKEN` with contents write on the app repository and on the GitOps repository | Reserving `vX.Y.Z` tags on `main` (required by `production`), GitHub Releases, GitOps write-back |
|
||||
| sonar (optional) | `SONAR_TOKEN`; SonarCloud organization from `SONAR_ORG` (default `KofTwentyTwo`) | SonarCloud analysis |
|
||||
| nvd (optional) | `NVD_API_KEY` | OWASP dependency check; `owasp: false` disables the check |
|
||||
|
||||
### 4. Branch model
|
||||
|
||||
Branch names are enforced: `main`, `develop`, `staging`, `feature/*`, `release/*`, `hotfix/*`. A merge to `main` is a production release: it reserves the next `vX.Y.Z` tag, pushes the image, updates the GitOps repository and creates the GitHub Release. Protect `main` accordingly.
|
||||
|
||||
### 5. Deployment infrastructure (outside Munitor)
|
||||
|
||||
Munitor stops at the GitOps commit. The consumer provides:
|
||||
|
||||
- a GitOps repository whose `kustomization.yaml` (an `images[]` entry named `<registry>/<image_name>`) or Helm `values.yaml` (`image.tag`) Munitor edits;
|
||||
- Argo CD (or another controller) syncing that repository, with pull access to the registry;
|
||||
- the runtime database and its backups. KofTwentyTwo's website uses a CloudNativePG cluster with an object-store backup and a `ScheduledBackup`, declared in `KofTwentyTwo/cluster-gitops` (`tenants/kof22/website`). Munitor only starts a throwaway PostgreSQL sidecar for the CI health gate and never touches a real database;
|
||||
- schema migrations. Either the application migrates at start-up, or the deployment runs the `migrations` supplemental image (the KofTwentyTwo website runs a pre-migration backup image and then the migrations image as init containers). `health.migrations` only exercises that image in CI.
|
||||
|
||||
### 6. Known limitations
|
||||
|
||||
- Organization defaults point at KofTwentyTwo: registry `ghcr.io/KofTwentyTwo`, npm scope `@koftwentytwo`, CI email `munitor-ci@koftwentytwo.com`, SonarCloud organization `KofTwentyTwo`. Set `docker.registry`, the `org.*` keys and `SONAR_ORG` explicitly. The `org` block is read, but the unknown-key check prints a harmless `Unknown key 'org'` warning.
|
||||
- `production` builds on `main` skip the quality gates, which run on `release/*` and `hotfix/*` pull requests. Do not merge unreviewed branches straight to `main`.
|
||||
- CircleCI only; there is no GitHub Actions equivalent. Docker jobs use machine executors.
|
||||
|
||||
### 7. Upgrade notes
|
||||
|
||||
`0.3.3` reserves release tags atomically on `main` (needs the github context on `build-and-test`, which generated `production` workflows include), refuses to overwrite a version image tag built from another commit, and checks tag ownership before the GitOps bump. A consumer moving from a Munitor-derived orb with a tag-driven production trigger should compare that model with Munitor's merge-to-`main` model before switching.
|
||||
|
||||
## Pipeline Types
|
||||
|
||||
### `node-api`
|
||||
@@ -277,7 +413,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`
|
||||
|
||||
@@ -395,7 +531,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) |
|
||||
@@ -403,11 +539,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
|
||||
@@ -416,7 +561,19 @@ 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.
|
||||
- **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.
|
||||
|
||||
## Release Workflow
|
||||
|
||||
@@ -464,7 +621,7 @@ Tags follow the format `vX.Y.Z` (git tag) and `X.Y.Z` (Docker tag). Tags are imm
|
||||
| Field | Type | Default | Pipelines | Description |
|
||||
|-------|------|---------|-----------|-------------|
|
||||
| `pipeline` | string | *required* | all | Pipeline type: `node-api`, `node-webapp`, `java-webapp`, `terraform`, `sdk-distribution`, `validate-cd-repo`, `argocd-apps` |
|
||||
| `orb_version` | string | *required* | all | Orb version for the generated pipeline (e.g., `1`, `dev:snapshot`) |
|
||||
| `orb_version` | string | *required* | all | Orb version for the generated pipeline (e.g., `"0.3.3"`, `"0.3"`, `dev:snapshot`) |
|
||||
| `image_name` | string | *required** | node-api, node-webapp, java-webapp | Docker image name (without registry prefix) |
|
||||
| `node_version` | string | `20` | node-api, node-webapp | Node.js major version |
|
||||
| `java_version` | string | `21` | java-webapp | Java major version |
|
||||
@@ -475,6 +632,17 @@ Tags follow the format `vX.Y.Z` (git tag) and `X.Y.Z` (Docker tag). Tags are imm
|
||||
| `cd.path` | string | -- | node-api, node-webapp, java-webapp | Explicit repository-relative `kustomization.yaml` path for consolidated GitOps repositories |
|
||||
| `cd.production_only` | bool | `false` | node-api, node-webapp, java-webapp | Disable CD write-back from develop, staging, and release workflows |
|
||||
| `cd.env.release` | string | `staging` | node-api, node-webapp, java-webapp | CD target environment for release branches |
|
||||
| `cd.env.develop` / `cd.env.staging` / `cd.env.prod` | string | `develop` / `staging` / `prod` | node-api, node-webapp, java-webapp | CD target environment names for the develop, staging and main workflows |
|
||||
| `cd.format` | string | `helm` | node-api, node-webapp, java-webapp | `helm` (sets `image.tag` in `values.yaml`, per environment under `environments/<env>/` when present) or `kustomize` (sets `images[].newTag`) |
|
||||
| `server_module` | string | -- | gradle-webapp | Gradle module whose `build/libs` JAR goes into the generated image (multi-module builds) |
|
||||
| `owasp` | bool | `true` | java-webapp | Run the OWASP dependency check |
|
||||
| `package_manager` | string | `npm` | node-api, node-webapp | `npm` or `pnpm` |
|
||||
| `supplemental_images` | list | `[]` | node-api, node-webapp, java-webapp | Extra images (`name`, `dockerfile`, `cd.image_name`) built and tagged with the same version |
|
||||
| `health.db` | bool | `false` | node-api, node-webapp, java-webapp | Start a PostgreSQL sidecar for the health check |
|
||||
| `health.migrations` | string | -- | node-api, node-webapp, java-webapp | Supplemental image suffix run against the sidecar before the health check |
|
||||
| `health.smoke_paths` | list | `[]` | node-api, node-webapp, java-webapp | Extra paths that must return HTTP 200 during the health check |
|
||||
| `test.coverage.summary_path` | string | `coverage/coverage-summary.json` | node-api, node-webapp | Coverage summary file read by the coverage gate |
|
||||
| `org.docker_registry` / `org.npm_scope` / `org.ci_email` / `org.ci_name` / `org.orb_slug` | string | KofTwentyTwo values | all | Organization defaults; see [Adopting in another app](#adopting-in-another-app) |
|
||||
| `coverage.min_instruction` | int | `70` | node-api, node-webapp, java-webapp | Minimum coverage percentage |
|
||||
| `e2e` | bool | `false` | node-api, node-webapp, java-webapp | Enable Playwright E2E tests |
|
||||
| `sbom` | bool | `false` | node-api, node-webapp, java-webapp | Enable SBOM generation |
|
||||
@@ -585,3 +753,15 @@ excepted version. Audit errors, unknown chains and expired exceptions fail. A ne
|
||||
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.
|
||||
|
||||
## Security
|
||||
|
||||
Report vulnerabilities privately; see [SECURITY.md](SECURITY.md).
|
||||
|
||||
## Contributing
|
||||
|
||||
See [CONTRIBUTING.md](CONTRIBUTING.md).
|
||||
|
||||
## License
|
||||
|
||||
[GPL-3.0](LICENSE)
|
||||
+56
@@ -0,0 +1,56 @@
|
||||
# Security Policy
|
||||
|
||||
## Reporting a vulnerability
|
||||
|
||||
**Do not open a public issue for a security problem.**
|
||||
|
||||
Report it privately through GitHub's
|
||||
[private vulnerability reporting](https://github.com/KofTwentyTwo/Munitor/security/advisories/new)
|
||||
(the repository's **Security** tab, then **Report a vulnerability**). If that form is
|
||||
unavailable, contact the maintainer privately through GitHub before disclosing anything.
|
||||
|
||||
Security contact: **James Maes** ([@KofTwentyTwo](https://github.com/KofTwentyTwo)),
|
||||
maintainer.
|
||||
|
||||
Please include the affected orb version, the `.munitor.yml` pipeline type, how to
|
||||
reproduce it, and the impact you see. Never include secrets or CircleCI context values.
|
||||
|
||||
## What to expect
|
||||
|
||||
KofTwentyTwo follows coordinated vulnerability disclosure. Timeframes are counted
|
||||
from the day the report arrives and come from the
|
||||
[KofTwentyTwo security program](https://github.com/KofTwentyTwo/standards/blob/main/policies/security-program.md):
|
||||
|
||||
| Step | Target |
|
||||
| --- | --- |
|
||||
| Acknowledge the report | 7 days |
|
||||
| Assess it: confirm or reject, with a severity | 14 days |
|
||||
| Release a fix for a CRITICAL vulnerability | 7 days |
|
||||
| Release a fix for a HIGH vulnerability | 30 days |
|
||||
| Release a fix for a MEDIUM vulnerability | 90 days |
|
||||
| Release a fix for a LOW vulnerability | The next release |
|
||||
| Publish the advisory (GitHub Security Advisory, CVE where applicable) | When the fix ships, or 90 days after the report, whichever comes first |
|
||||
|
||||
Reporters are credited in the advisory unless they ask not to be.
|
||||
|
||||
## Supported versions
|
||||
|
||||
| Version | Security fixes |
|
||||
| --- | --- |
|
||||
| 0.3.x (`kof22/munitor@0.3`) | Yes |
|
||||
| Older | No; upgrade to the latest release |
|
||||
|
||||
Only the latest minor line receives fixes; a line ends when the next minor version is
|
||||
published to the CircleCI orb registry.
|
||||
|
||||
## Scope notes
|
||||
|
||||
Munitor runs inside the consumer's CircleCI project with the consumer's contexts
|
||||
(registry, GitHub, SonarCloud, NVD). Issues that let a generated pipeline leak context
|
||||
secrets, push an image or GitOps change for the wrong commit, or bypass a security
|
||||
gate are in scope.
|
||||
|
||||
## Published vulnerabilities
|
||||
|
||||
Fixed vulnerabilities are published as
|
||||
[GitHub Security Advisories](https://github.com/KofTwentyTwo/Munitor/security/advisories).
|
||||
@@ -0,0 +1,61 @@
|
||||
# Agile CI adoption
|
||||
|
||||
Released as 0.3.10: opt-in documentation-only Java CI and aggregate merge results.
|
||||
PR48 implemented it; PR49 released main 19192d07, signed tag v0.3.10 published,
|
||||
and PR50 back-merged to develop 81ac29ee. All hosted checks passed. Munitor's
|
||||
actual `ci/circleci: lint-pack/ci-ready` now protects main/develop; unrelated
|
||||
policy is unchanged. Backend PR333 passed full consumer validation before its
|
||||
required checks changed. Docs PR335 passed the two-job path and cancellation proof.
|
||||
|
||||
## Contract
|
||||
|
||||
- `ci.docs_only: true` permits only root README/CHANGELOG/CONTRIBUTING.md and
|
||||
`docs/**/*.md` (including Markdown directly in docs). Secret scanning stays on.
|
||||
- Feature/hotfix classification uses the complete branch diff from the merge base
|
||||
with main, not the latest commit. Main uses its first parent. Tags, release/other
|
||||
branches, shallow history, empty or uncertain comparisons use full CI.
|
||||
- Docs checks emit the aggregate gate without image build/deploy. Full workflows
|
||||
retain their existing gates. No custom reporting framework or reporting job.
|
||||
|
||||
## Required order
|
||||
|
||||
1. Local make all, 20 BATS, 31 classifier cases, eight source/packed graph cases, and CircleCI config validation passed. Hosted CI and stable publication also passed.
|
||||
2. Adopt it in the consumer with docs-only disabled. Observe successful actual
|
||||
GitHub context `ci/circleci: ci-ready`; do not infer the name from YAML alone.
|
||||
3. Replace the consumer required-check list with that verified aggregate context,
|
||||
preserving review, signature, conversation, bypass and all other branch rules.
|
||||
4. Enable `ci.docs_only: true`; prove docs-only and mixed/code changes select the
|
||||
expected paths and retain the required gate.
|
||||
5. Separately verify Munitor's native no-op context (observed
|
||||
`ci/circleci: lint-pack/ci-ready`) before migrating its required checks.
|
||||
|
||||
Publication and both required-check migrations are complete, following successful actual-context verification.
|
||||
To avoid duplicate full builds, the pin and opt-in may share one adoption PR:
|
||||
the manifest/config changes force full CI. Verify its gate and migrate required
|
||||
checks before merging or opening a docs-only PR.
|
||||
|
||||
## Measure using existing CircleCI views
|
||||
|
||||
[Project Insights](https://app.circleci.com/insights/github/KofTwentyTwo/Website-Backend/workflows),
|
||||
[PR checks](https://app.circleci.com/insights/github/KofTwentyTwo/Website-Backend/workflows/pr-checks),
|
||||
and [Tests](https://app.circleci.com/insights/github/KofTwentyTwo/Website-Backend/workflows/pr-checks/tests)
|
||||
already provide the dashboard. The historical API baseline was retrieved
|
||||
October 19 but its returned data ends October 17 (PR P50 13m58s, P95 34m37s).
|
||||
It is not a measurement of this implementation. After Insights catches up, compare
|
||||
equivalent windows/branches and report the docs-checks median separately; do not
|
||||
blend docs runs into a claimed full-build improvement or sum parallel job medians.
|
||||
|
||||
Backend native auto-cancel is on; main is its exempt default branch. Munitor stays
|
||||
off because develop is its default and main/release work is not similarly exempt.
|
||||
|
||||
## Observed consumer results
|
||||
|
||||
Backend PR333 (664efcad) ran the complete pipeline in 15m33, with all 777 test
|
||||
identities preserved and every check successful, including the actual required
|
||||
ci/circleci: ci-ready. Required lists on main/develop were migrated only after
|
||||
that proof; all other rules were verified unchanged. The latest full run is a
|
||||
sample, not a new median; the earlier successful parallel-suite run was 14m49.
|
||||
|
||||
Docs PR335 selected only secrets-scan and ci-ready. A follow-up documentation
|
||||
commit caused native cancellation of the older workflow after 44 seconds, with
|
||||
no explicit cancel API call. The final docs run passed in 5m23 (313s secrets scanning, 4s aggregate result), then merged as 8dda8339. Main also selected only those two jobs, with no application release.
|
||||
@@ -0,0 +1,41 @@
|
||||
# Fast Java CI
|
||||
|
||||
Backend ticket work is paused while this performance change is delivered.
|
||||
|
||||
## Measured baseline
|
||||
|
||||
Website-Backend PR311, CircleCI jobs6314–6318 on2026-10-19:
|
||||
|
||||
- Build/test job:30m15s. Compilation49s; tests14m24s; Java installation4m27s.
|
||||
- Maven cache48GiB: restore5m32s, save4m19s.
|
||||
- Quality, coverage and security each restore the same48GiB again (5–6min).
|
||||
- Coverage computation itself takes5.4s. Quality tools take57s combined.
|
||||
- OWASP is disabled in this consumer, but security still installs Java and restores Maven.
|
||||
- Test results are uploaded by both the test command and its enclosing job:777 results appear twice.
|
||||
|
||||
Evidence: coordinator cache/cicd-baseline.json, cicd-test-timings.json and cicd-github-friction.md.
|
||||
|
||||
## First implementation
|
||||
|
||||
1. Replace the old broad Maven fallback with an exact v2 key based on checked-out dependency definitions. Keep that key stable across release-version rewrites and invalidate for nested POM or Maven configuration changes. Never restore the48GiB v1 cache.
|
||||
2. Skip Java/Maven/NVD setup when OWASP is disabled; preserve Trivy.
|
||||
3. Upload Maven test results once.
|
||||
4. Pack the orb once per pipeline; publish only after its existing checks pass, preserving branch/tag behavior and required check names.
|
||||
5. Run template tests only in their dedicated Make target, rather than also discovering them in the generic script loop.
|
||||
6. Run Checkstyle, SpotBugs, PMD and coverage on the existing build worker for generated PR/develop/staging/release workflows. Keep the standalone jobs available for custom consumers and preserve production behavior. Generated downstream jobs depend on the completed build gate.
|
||||
7. Render each immutable test fixture once per test run; retain all277 assertions.
|
||||
|
||||
Maven caches are dependency reuse, not artifact handoff. A real three-module experiment failed SpotBugs with current target outputs and an older installed-module cache. Preserve locally installed version directories, mutable SNAPSHOT dependencies and ancestor metadata in a workspace archive, then overlay it after dependency-cache restore. The experiment passes for release versions, snapshots and classifier dependencies. Maven Resolver identifies local installs with empty repository IDs in `_remote.repositories`; this implementation depends on that Maven3 format. Do not replace this archive with the entire local repository.
|
||||
|
||||
Cold dependency downloads on changed POMs are deliberate: no cross-version fallback accumulation. Do not delete caches, artifacts, tags or change branch protections.
|
||||
|
||||
## Verification and rollout
|
||||
|
||||
- Focused behavioral/config regressions, then required make all and BATS suites once the combined patch is complete.
|
||||
- Publish immutable feature orb; run a backend feature canary with both pins matching that SHA.
|
||||
- Compare real cache size and build/check durations against the baseline. No claimed speedup before measurement; do not merge the consumer until tests and all existing security gates pass.
|
||||
- Release the proven orb and adopt its stable version through normal PRs. Track the already-running backend release separately; do not resume paused ticket work.
|
||||
|
||||
## Follow-up decisions after measurement
|
||||
|
||||
If tests dominate after cache repair, inspect measured slow fixtures and isolate/parallelize only tests proven independent. Exact-source artifact promotion is a separate future change requiring verified source identity; do not skip merged-source validation on the strength of an earlier PR run. Avoid a new orchestration framework or speculative configuration options.
|
||||
@@ -1,5 +1,133 @@
|
||||
## Release 0.3.11 (2026-10-19)
|
||||
|
||||
PR52 passed all eight hosted checks on 18ea857 and merged as d76a46c.
|
||||
This release forwards configured Java migration images and smoke paths to develop
|
||||
and staging. Source and packed generator regressions, make all and 20 BATS pass.
|
||||
Release metadata is the only change from the verified implementation; stable
|
||||
publication and backend adoption follow. Production 0.1.138 remains healthy.
|
||||
|
||||
## Java deployment health parameter correction (2026-10-19)
|
||||
|
||||
Backend develop job 6405 exposed missing migration/smoke configuration in the
|
||||
shared Java deployment partial: startup failed because slack_template was absent.
|
||||
The partial now forwards health_migrations and health_smoke_paths just like the
|
||||
production and release workflows. No application or scan policy changes.
|
||||
|
||||
tests/test_java_deploy_health.sh checks configured values and empty defaults in
|
||||
develop, staging, production and release-candidate using both source and packed
|
||||
generators. Full make all, 20 BATS tests, focused regression rerun and shell lint
|
||||
passed through WSL. Packed scripts are synchronized. Publication and consumer
|
||||
verification are pending.
|
||||
|
||||
## Verified agile CI rollout (2026-10-19)
|
||||
|
||||
Munitor 0.3.10 is published from main 19192d07 (PR48 implementation, PR49 release,
|
||||
PR50 back-merge). Backend PR333 adopted it and passed the complete workflow:
|
||||
777 identical test identities, zero failures/errors, five skips, 15m33 elapsed.
|
||||
Its actual ci/circleci: ci-ready result now protects main/develop; Munitor uses
|
||||
ci/circleci: lint-pack/ci-ready. Only the required-check lists changed; other
|
||||
branch rules are unchanged. Published source and actual consumer compilation passed.
|
||||
|
||||
Backend docs PR335 selected exactly secrets-scan plus ci-ready, without build,
|
||||
image or deployment jobs. Native auto-cancel canceled the first docs run after
|
||||
44 seconds when the final documentation commit arrived. No cancellation script
|
||||
or manual cancellation was used. The final docs run passed in 5m23 and merged as 8dda8339; main also selected only those two jobs.
|
||||
|
||||
Native Insights supplies ongoing P50/P95 and flaky-test reporting. Historical
|
||||
windows lag current work; compare matching samples and keep docs-checks separate.
|
||||
Backend auto-cancel is enabled; Munitor remains off because its default develop
|
||||
would not exempt main release workflows. No new orchestration or reporting service.
|
||||
|
||||
## Release 0.3.10 (2026-10-19)
|
||||
|
||||
PR48 merged as f9ef625 after all hosted checks passed on a56ef1d. Local make all,
|
||||
20 BATS, 31 classifier cases and eight source/packed generator cases passed.
|
||||
Munitor main/develop now require the verified native ci/circleci: lint-pack/ci-ready
|
||||
result; all other rules are unchanged. This release changes metadata only from
|
||||
that verified implementation. Stable publication and backend adoption follow.
|
||||
|
||||
## Agile CI (2026-10-19)
|
||||
|
||||
Fast Java CI shipped as Munitor 0.3.9 and Website-Backend PR330. The warm
|
||||
consumer PR workflow fell from 37m04 to 20m45; cache restore fell from 5m32 to
|
||||
4.76s (48GiB to 565MiB). Backend PR332 then preserved all 777 test identities
|
||||
while reducing its verified PR workflow to 14m49 and tests to 8m10. Timing-sensitive
|
||||
performance tests run serially after the parallel suite; thresholds are unchanged.
|
||||
|
||||
Current feature/agile-ci adds conservative opt-in documentation-only validation
|
||||
and one aggregate merge result. Backend native auto-cancel is enabled; Munitor
|
||||
remains off because its default branch is develop. Native Insights provides
|
||||
runtime/flaky-test reporting without another service. Local classifier and
|
||||
workflow-graph regressions pass; publication and consumer adoption are pending.
|
||||
See docs/PLAN-agile-ci.md for migration order and measurement limits.
|
||||
|
||||
## Fast Java CI (2026-10-19)
|
||||
|
||||
Active user priority: fast, agile, lightweight CI/CD; backend ticket goal paused.
|
||||
Measured48GiB Maven cache restored four times and saved once per backend PR.
|
||||
Work on feature/lean-java-ci: stable exact dependency cache keys with new namespace,
|
||||
conditional OWASP setup, single test-result upload, and one orb pack per pipeline.
|
||||
The first patch passed make all and15BATS. Further simplification combines quality
|
||||
and coverage with the build, renders fixtures once, and carries installed reactor
|
||||
artifacts in a small workspace archive. A real multi-module experiment proved this
|
||||
artifact handoff necessary, including snapshot/classifier coverage. Expanded make
|
||||
all and20BATS pass, including all277template assertions. Real consumer timing and
|
||||
stable publication/adoption remain pending. No deployed speedup claimed.
|
||||
See docs/PLAN-lean-java-ci.md.
|
||||
|
||||
## Supplemental runtime freshness (2026-10-19)
|
||||
|
||||
Backend PR311 image scan6307 failed because db-backup reused a cached apk-upgrade layer (libexpat2.8.5-r0, fixed2.9.0-r0). Supplemental builds now use --pull --no-cache in both PR scan-only and release publishing paths. Trivy and immutable-tag guards remain unchanged. Targeted tests cover both Dockerfile forms, fresh-build flags, scan-before-push ordering and scan failure propagation. Validation pending; no release or consumer adoption yet.
|
||||
|
||||
# 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)
|
||||
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`
|
||||
@@ -49,3 +177,18 @@ 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.
|
||||
|
||||
## Release 0.3.7 preparation (2026-10-19)
|
||||
Includes merged PR38 (filesystem Trivy findings fail CI; current Semgrep image and zero-scan/configuration failures) and PR39 (pull fresh runtime bases and bypass cached JVM package-upgrade layers). Both feature PRs passed all CircleCI gates; PR39 also passed full local make all and seven BATS tests. Publish the stable orb only from a signed v0.3.7 tag on the reviewed main merge. Website-Backend must update both orb pins and pass real image/SAST scans before rollout. No vulnerability waiver or immutable-tag overwrite is part of this release.
|
||||
|
||||
Validation completed: focused scan tests12/12, full make all (lint, packed orb validation, script/template tests), and7BATS passed under WSL/Nix on2026-10-19. Real backend supplemental image scans remain required after stable release adoption.
|
||||
|
||||
## Release candidate 0.3.8 (2026-10-19)
|
||||
|
||||
Promotes PR42 supplemental build freshness fix (develop11d5704) to main. Full local make all and seven BATS passed; all PR42 CI checks passed. Release metadata changes only; source/tests match the verified feature. Stable publication still requires the approved main merge and signed v0.3.8 tag. Backend adoption and real supplemental scans remain pending.
|
||||
|
||||
## Release candidate 0.3.9 (2026-10-19)
|
||||
|
||||
Promotes PR45 (develop64831e8): consolidated Java quality/coverage, bounded dependency caches with current artifact/SNAPSHOT handoff, conditional OWASP setup, and deduplicated orb/test work. Source and tests are identical to verified77cdf93: make all and20BATS passed; actual CircleCI compiler preserves required build-and-test name. Cold backend canary PR330 passed every test/scan: combined build19m02s vs old build/test30m15s, Maven cache565MiB vs old48GiB. Stable publication and backend warm-cache adoption remain pending. Production build retains its prior operations under a distinct name to avoid PR check suffixes.
|
||||
+16
-1
@@ -2,8 +2,23 @@
|
||||
|
||||
## Pending
|
||||
|
||||
- [ ] Validate and release Java develop/staging health_migrations and health_smoke_paths propagation; verify both generators and a corrected consumer build before claiming rollout success.
|
||||
|
||||
- [x] Publish 0.3.10, adopt in backend PR333, verify aggregate check names and migrate required lists; docs PR335 passed in 5m23 and native cancellation was observed (docs/PLAN-agile-ci.md).
|
||||
|
||||
- [x] Publish fast Java CI as 0.3.9; backend PR330 adopted it and measured the warm workflow at 20m45 (from 37m04).
|
||||
|
||||
- [ ] Validate and release supplemental-image freshness fix, then adopt it in backend PR311 and verify all supplemental Trivy scans.
|
||||
|
||||
- [ ] 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.
|
||||
- [ ] Publish v0.3.2 from main; update website consumer orb pins.
|
||||
- [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.
|
||||
|
||||
@@ -38,8 +38,14 @@ echo "Packing generate_config.sh..."
|
||||
echo 'MUNITOR_EXTRACT_EOF'
|
||||
echo ''
|
||||
|
||||
# Embed conservative documentation-only classification.
|
||||
echo "cat > /tmp/munitor/docs_only.sh << 'MUNITOR_DOCS_EOF'"
|
||||
cat "${SRC}/docs_only.sh"
|
||||
echo 'MUNITOR_DOCS_EOF'
|
||||
echo ''
|
||||
|
||||
# Embed each template
|
||||
for tpl in java-webapp gradle-webapp node-api node-webapp terraform sdk-distribution validate-cd-repo argocd-apps obsidian-plugin; do
|
||||
for tpl in java-docs java-webapp gradle-webapp node-api node-webapp terraform sdk-distribution validate-cd-repo argocd-apps obsidian-plugin; do
|
||||
echo "cat > /tmp/munitor/templates/${tpl}.yml.tpl << 'MUNITOR_TPL_EOF'"
|
||||
cat "${SRC}/templates/${tpl}.yml.tpl"
|
||||
echo 'MUNITOR_TPL_EOF'
|
||||
|
||||
@@ -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) >>
|
||||
@@ -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
|
||||
@@ -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) >>
|
||||
@@ -1,7 +1,12 @@
|
||||
description: >
|
||||
Restore the Maven local repository cache based on pom.xml checksum.
|
||||
Restore only the cache for the checked-out Maven dependency definitions.
|
||||
steps:
|
||||
- run:
|
||||
name: Identify Maven dependency cache
|
||||
command: <<include(scripts/maven_cache_key.sh)>>
|
||||
- restore_cache:
|
||||
keys:
|
||||
- maven-v1-{{ checksum "pom.xml" }}
|
||||
- maven-v1-
|
||||
- maven-v2-{{ checksum ".munitor-maven-cache-key" }}
|
||||
- run:
|
||||
name: Restore this build's installed Maven artifacts
|
||||
command: <<include(scripts/maven_workspace.sh)>>
|
||||
@@ -1,7 +1,12 @@
|
||||
description: >
|
||||
Save the Maven local repository to cache for future builds.
|
||||
Save the Maven repository under the same immutable key used at restore time.
|
||||
steps:
|
||||
- save_cache:
|
||||
key: maven-v1-{{ checksum "pom.xml" }}
|
||||
key: maven-v2-{{ checksum ".munitor-maven-cache-key" }}
|
||||
paths:
|
||||
- ~/.m2/repository
|
||||
- run:
|
||||
name: Preserve installed Maven artifacts for this workspace
|
||||
environment:
|
||||
MAVEN_WORKSPACE_ACTION: save
|
||||
command: <<include(scripts/maven_workspace.sh)>>
|
||||
@@ -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: "[]"
|
||||
|
||||
@@ -3,8 +3,8 @@ description: >
|
||||
parameters:
|
||||
version:
|
||||
type: string
|
||||
default: "1.61.1"
|
||||
default: "1.181.0"
|
||||
description: Semgrep image version tag.
|
||||
docker:
|
||||
- image: returntocorp/semgrep:<< parameters.version >>
|
||||
- image: semgrep/semgrep:<< parameters.version >>
|
||||
resource_class: medium
|
||||
@@ -0,0 +1,7 @@
|
||||
description: >
|
||||
Single merge result. Workflow dependencies enforce the applicable checks.
|
||||
executor: base
|
||||
steps:
|
||||
- run:
|
||||
name: Required checks passed
|
||||
command: echo "All applicable CI checks passed."
|
||||
@@ -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 >>
|
||||
|
||||
@@ -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
|
||||
@@ -25,6 +25,7 @@ steps:
|
||||
- restore_gradle_cache
|
||||
- calculate_version
|
||||
- export_version_vars
|
||||
- allocate_version
|
||||
- gradle_build
|
||||
- gradle_test
|
||||
- run:
|
||||
|
||||
@@ -3,6 +3,14 @@ description: >
|
||||
version, builds, tests, saves cache, and persists the workspace for
|
||||
downstream jobs.
|
||||
parameters:
|
||||
quality_checks:
|
||||
type: boolean
|
||||
default: false
|
||||
description: Run quality and coverage checks on the build worker before sharing artifacts.
|
||||
min_instruction:
|
||||
type: string
|
||||
default: "70"
|
||||
description: Minimum instruction coverage when quality_checks is enabled.
|
||||
java_version:
|
||||
type: string
|
||||
default: "21"
|
||||
@@ -25,11 +33,24 @@ steps:
|
||||
- restore_maven_cache
|
||||
- calculate_version
|
||||
- export_version_vars
|
||||
- allocate_version
|
||||
- mvn_set_version
|
||||
- mvn_build
|
||||
- mvn_test
|
||||
- store_test_results:
|
||||
path: target/surefire-reports
|
||||
- when:
|
||||
condition: << parameters.quality_checks >>
|
||||
steps:
|
||||
- mvn_checkstyle
|
||||
- mvn_spotbugs
|
||||
- mvn_pmd
|
||||
- mvn_jacoco:
|
||||
min_instruction: << parameters.min_instruction >>
|
||||
- store_artifacts:
|
||||
path: target/checkstyle-result.xml
|
||||
destination: checkstyle-result.xml
|
||||
- store_artifacts:
|
||||
path: target/site/jacoco
|
||||
destination: coverage-report
|
||||
- run:
|
||||
name: Persist version metadata
|
||||
command: <<include(scripts/persist_version_metadata.sh)>>
|
||||
|
||||
@@ -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)>>
|
||||
|
||||
@@ -23,12 +23,12 @@ executor:
|
||||
steps:
|
||||
- attach_workspace:
|
||||
at: .
|
||||
- install_java:
|
||||
version: << parameters.java_version >>
|
||||
- restore_maven_cache
|
||||
- when:
|
||||
condition: << parameters.owasp >>
|
||||
steps:
|
||||
- install_java:
|
||||
version: << parameters.java_version >>
|
||||
- restore_maven_cache
|
||||
- restore_cache:
|
||||
keys:
|
||||
- nvd-cache-v1-{{ .Branch }}
|
||||
|
||||
@@ -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: "[]"
|
||||
|
||||
@@ -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})"
|
||||
@@ -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."
|
||||
@@ -70,28 +83,40 @@ for i in $(seq 0 $((COUNT - 1))); do
|
||||
|
||||
# Build
|
||||
echo "Building ${FULL_TAG} ..."
|
||||
docker build -f "${DOCKERFILE}" -t "${FULL_TAG}" .
|
||||
# Refresh base images and rerun package upgrades in supplemental Dockerfiles.
|
||||
BUILD_ARGS=(--pull --no-cache -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
|
||||
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})."
|
||||
@@ -23,10 +23,24 @@ 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
|
||||
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[@]}"
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
#!/usr/bin/env bash
|
||||
# Source this helper. Any uncertainty keeps the full pipeline.
|
||||
munitor_docs_only() (
|
||||
[[ -z "${CIRCLE_TAG:-}" ]] || return 1
|
||||
local base paths path seen=0
|
||||
case "${CIRCLE_BRANCH:-}" in
|
||||
feature/*|hotfix/*)
|
||||
git fetch --no-tags origin '+refs/heads/main:refs/remotes/origin/main' >/dev/null 2>&1 || return 1
|
||||
base=$(git merge-base HEAD refs/remotes/origin/main 2>/dev/null) || return 1
|
||||
;;
|
||||
main)
|
||||
base=$(git rev-parse --verify 'HEAD^1' 2>/dev/null) || return 1
|
||||
;;
|
||||
*) return 1 ;;
|
||||
esac
|
||||
[[ "$(git rev-parse --is-shallow-repository 2>/dev/null)" == false ]] || return 1
|
||||
paths=$(mktemp 2>/dev/null) || return 1
|
||||
trap 'rm -f -- "$paths"' EXIT
|
||||
git diff --no-ext-diff --no-renames --name-only -z "$base" HEAD >"$paths" 2>/dev/null || return 1
|
||||
while IFS= read -r -d '' path; do
|
||||
case "$path" in
|
||||
README.md|CHANGELOG.md|CONTRIBUTING.md|docs/*.md) ;;
|
||||
*) return 1 ;;
|
||||
esac
|
||||
seen=1
|
||||
done <"$paths"
|
||||
[[ "$seen" -eq 1 ]]
|
||||
)
|
||||
@@ -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
|
||||
|
||||
@@ -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 ci"
|
||||
ACTUAL_KEYS=$(yq 'keys | .[]' "${CONFIG_FILE}" 2>/dev/null || true)
|
||||
for key in ${ACTUAL_KEYS}; do
|
||||
if ! echo "${KNOWN_KEYS}" | grep -qw "${key}"; then
|
||||
@@ -110,6 +110,17 @@ done
|
||||
# --------------------------------------------------------------------------
|
||||
TEMPLATE_FILE="${TEMPLATE_DIR}/${PIPELINE_TYPE}.yml.tpl"
|
||||
|
||||
# Opt in only after the consumer requires ci-ready instead of individual build jobs.
|
||||
# Missing history or any non-documentation path keeps the complete pipeline.
|
||||
if [[ "${PIPELINE_TYPE}" == "java-webapp" && "$(yq '.ci.docs_only // false' "${CONFIG_FILE}")" == "true" ]]; then
|
||||
# shellcheck source=docs_only.sh
|
||||
source "${SCRIPT_DIR}/docs_only.sh"
|
||||
if munitor_docs_only; then
|
||||
TEMPLATE_FILE="${TEMPLATE_DIR}/java-docs.yml.tpl"
|
||||
echo "Documentation-only change: retaining secret scanning; no application build or deployment."
|
||||
fi
|
||||
fi
|
||||
|
||||
if [[ ! -f "${TEMPLATE_FILE}" ]]; then
|
||||
echo "ERROR: No template found for pipeline type '${PIPELINE_TYPE}'."
|
||||
echo "Available templates:"
|
||||
@@ -201,7 +212,7 @@ process_conditionals() {
|
||||
cp "${input}" "${tmpfile}"
|
||||
|
||||
# Process each conditional flag
|
||||
for flag in E2E SBOM SONAR NPM_AUTH SERVICES TEST_SETUP CUSTOM_TEST COVERAGE_CMD GITHUB_RELEASE SAST CD CD_NON_PROD OWASP NVD SUPPLEMENTAL; do
|
||||
for flag in E2E SBOM SONAR NPM_AUTH SERVICES TEST_SETUP CUSTOM_TEST COVERAGE_CMD GITHUB_RELEASE SAST CD CD_NON_PROD CD_EXPLICIT_PATH OWASP NVD SUPPLEMENTAL; do
|
||||
local var_name="MUNITOR_${flag}"
|
||||
local value="${!var_name:-false}"
|
||||
local flag_file
|
||||
|
||||
@@ -35,7 +35,9 @@ if [[ "${VERSION}" =~ -(RC|alpha|beta|SNAPSHOT) ]]; then
|
||||
TARGET="${CIRCLE_BRANCH:-main}"
|
||||
else
|
||||
PRERELEASE_FLAG=""
|
||||
TARGET="main"
|
||||
# Target the commit this pipeline built, never the moving branch head: a
|
||||
# later merge must not receive this version's tag.
|
||||
TARGET="${CIRCLE_SHA1:-main}"
|
||||
fi
|
||||
|
||||
echo "Target: ${TARGET}"
|
||||
@@ -44,6 +46,16 @@ echo "Pre-release: ${PRERELEASE_FLAG:-no}"
|
||||
# Create tag + GitHub Release via API (skip if already exists)
|
||||
# Uses gh release create which handles both tag and release in one call,
|
||||
# bypassing CircleCI's read-only SSH deploy key entirely.
|
||||
# The release tag is normally reserved by allocate_version. If it exists it
|
||||
# must point at this commit; an existing tag is never moved.
|
||||
if [[ -n "${CIRCLE_SHA1:-}" ]] && TAG_SHA=$(gh api "repos/{owner}/{repo}/commits/${TAG}" --jq '.sha' 2>/dev/null); then
|
||||
if [[ "${TAG_SHA}" != "${CIRCLE_SHA1}" ]]; then
|
||||
echo "ERROR: Tag ${TAG} already points at ${TAG_SHA}, not ${CIRCLE_SHA1}. Refusing to release."
|
||||
exit 1
|
||||
fi
|
||||
echo "Tag ${TAG} points at ${CIRCLE_SHA1}."
|
||||
fi
|
||||
|
||||
if gh release view "${TAG}" >/dev/null 2>&1; then
|
||||
echo "Release ${TAG} already exists, skipping."
|
||||
else
|
||||
|
||||
@@ -0,0 +1,87 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
# Source shared helpers
|
||||
MUNITOR_HELPERS="${MUNITOR_HELPERS:-$(cd "$(dirname "${BASH_SOURCE[0]:-$0}")" && pwd)/munitor_helpers.sh}"
|
||||
# shellcheck source=munitor_helpers.sh
|
||||
if [[ -f "${MUNITOR_HELPERS}" ]]; then source "${MUNITOR_HELPERS}"
|
||||
elif ! type munitor_header &>/dev/null; then
|
||||
munitor_header() { echo "=== Munitor: ${1:-unknown} ==="; }
|
||||
munitor_check_tool() { command -v "$1" &>/dev/null || { echo "ERROR: $1 not found"; exit 1; }; }
|
||||
munitor_download_with_retry() { curl -fsSL --retry 3 "$1" -o "$2"; }
|
||||
fi
|
||||
|
||||
munitor_header "guard_image_tags"
|
||||
|
||||
# Version tags are immutable. Before anything is pushed, verify that every
|
||||
# version tag this job will publish is either absent from the registry or was
|
||||
# built from this same commit (a rerun). A tag built from a different commit
|
||||
# means two pipelines claimed the same version; overwriting it would silently
|
||||
# change what an already-deployed tag points at, so fail instead.
|
||||
# Mutable environment tags (develop, staging) are not checked.
|
||||
|
||||
munitor_check_tool docker --version
|
||||
munitor_check_tool jq --version
|
||||
|
||||
IMAGE="${IMAGE_NAME:?IMAGE_NAME not set}"
|
||||
REGISTRY="${REGISTRY:-ghcr.io/KofTwentyTwo}"
|
||||
VERSION="${PROJECT_VERSION:?PROJECT_VERSION not set -- run export_version_vars first}"
|
||||
SHA="${CIRCLE_SHA1:?CIRCLE_SHA1 not set}"
|
||||
SUPP_JSON="${SUPPLEMENTAL_IMAGES_JSON:-[]}"
|
||||
REVISION_LABEL="org.opencontainers.image.revision"
|
||||
|
||||
if [[ -z "${MUNITOR_SKIP_REGISTRY_LOGIN:-}" ]]; then
|
||||
TOKEN="${!GHCR_TOKEN_VAR:-}"
|
||||
USER="${!GHCR_USER_VAR:-}"
|
||||
if [[ -z "${TOKEN}" || -z "${USER}" ]]; then
|
||||
echo "ERROR: GHCR credentials not set (\$${GHCR_TOKEN_VAR:-GHCR_TOKEN}, \$${GHCR_USER_VAR:-GHCR_USERNAME})"
|
||||
exit 1
|
||||
fi
|
||||
echo "${TOKEN}" | docker login "${REGISTRY%%/*}" -u "${USER}" --password-stdin
|
||||
fi
|
||||
|
||||
REFS=("${REGISTRY}/${IMAGE}:${VERSION}")
|
||||
if [[ "${SUPP_JSON}" != "[]" && "${SUPP_JSON}" != "null" && -n "${SUPP_JSON}" ]]; then
|
||||
while IFS= read -r name; do
|
||||
name="${name%$'\r'}"
|
||||
REFS+=("${REGISTRY}/${IMAGE}-${name}:${VERSION}")
|
||||
done < <(echo "${SUPP_JSON}" | jq -r '.[].name')
|
||||
fi
|
||||
|
||||
CONFLICTS=0
|
||||
for ref in "${REFS[@]}"; do
|
||||
err=$(mktemp)
|
||||
rc=0
|
||||
config=$(docker buildx imagetools inspect "${ref}" --format '{{json .Image}}' 2>"${err}") || rc=$?
|
||||
if [[ ${rc} -ne 0 ]]; then
|
||||
if grep -qiE 'not found|manifest unknown|MANIFEST_UNKNOWN|no such manifest' "${err}"; then
|
||||
echo "OK: ${ref} does not exist yet."
|
||||
rm -f "${err}"
|
||||
continue
|
||||
fi
|
||||
echo "ERROR: Could not inspect ${ref}; refusing to push without proving the tag is free:"
|
||||
cat "${err}"
|
||||
rm -f "${err}"
|
||||
exit 1
|
||||
fi
|
||||
rm -f "${err}"
|
||||
|
||||
# Single-platform images return one config; multi-platform images return a map of configs.
|
||||
revision=$(echo "${config}" | jq -r --arg l "${REVISION_LABEL}" \
|
||||
'[.. | objects | select(has("Labels")) | .Labels // {} | .[$l] // empty] | first // ""')
|
||||
|
||||
if [[ "${revision}" == "${SHA}" ]]; then
|
||||
echo "OK: ${ref} already exists for this commit (rerun); it will be republished from ${SHA}."
|
||||
else
|
||||
echo "ERROR: ${ref} already exists for a different commit (${revision:-unknown, no ${REVISION_LABEL} label})."
|
||||
CONFLICTS=$((CONFLICTS + 1))
|
||||
fi
|
||||
done
|
||||
|
||||
if [[ ${CONFLICTS} -gt 0 ]]; then
|
||||
echo "Refusing to overwrite ${CONFLICTS} immutable version tag(s). Version ${VERSION} was claimed by another build;"
|
||||
echo "rerun from a fresh pipeline so allocate_version reserves a new version."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "All ${#REFS[@]} version tag(s) are free for ${SHA}."
|
||||
@@ -0,0 +1,19 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
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} ==="; }
|
||||
fi
|
||||
munitor_header "maven_cache_key"
|
||||
|
||||
# Hash the checked-out dependency definitions, not release-version rewrites.
|
||||
# The same index travels with the workspace to downstream Maven jobs.
|
||||
git ls-files -s -- pom.xml '**/pom.xml' '.mvn/*' > .munitor-maven-cache-key
|
||||
if [[ ! -s .munitor-maven-cache-key ]]; then
|
||||
echo "ERROR: Maven cache requires tracked dependency definitions." >&2
|
||||
exit 1
|
||||
fi
|
||||
@@ -0,0 +1,47 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
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} ==="; }
|
||||
fi
|
||||
munitor_header "maven_workspace"
|
||||
|
||||
archive="${PWD}/.munitor-maven-installed.tgz"
|
||||
repository="${HOME}/.m2/repository"
|
||||
case "${MAVEN_WORKSPACE_ACTION:-restore}" in
|
||||
save)
|
||||
# Maven marks local installs with an empty repository ID (artifact>=).
|
||||
# Carry build outputs and mutable SNAPSHOT dependencies separately from the cache.
|
||||
files=$(mktemp)
|
||||
trap 'rm -f "${files}"' EXIT
|
||||
cd "${repository}"
|
||||
while IFS= read -r -d '' tracking; do
|
||||
if [[ "${tracking}" == */*-SNAPSHOT/_remote.repositories ]] ||
|
||||
grep -qE '^[^#].*>=[[:space:]]*$' "${tracking}"; then
|
||||
directory=$(dirname "${tracking}")
|
||||
printf '%s\0' "${directory}" >> "${files}"
|
||||
while [[ "${directory}" != . ]]; do
|
||||
directory=$(dirname "${directory}")
|
||||
if [[ -f "${directory}/maven-metadata-local.xml" ]]; then
|
||||
printf '%s\0' "${directory}/maven-metadata-local.xml" >> "${files}"
|
||||
fi
|
||||
done
|
||||
fi
|
||||
done < <(find . -name _remote.repositories -type f -print0)
|
||||
tar -czf "${archive}" --null -T "${files}"
|
||||
;;
|
||||
restore)
|
||||
if [[ -f "${archive}" ]]; then
|
||||
mkdir -p "${repository}"
|
||||
tar -xzf "${archive}" -C "${repository}"
|
||||
fi
|
||||
;;
|
||||
*)
|
||||
echo "ERROR: Unknown Maven workspace action." >&2
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
@@ -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
|
||||
@@ -339,6 +347,54 @@ get_envsubst_vars() {
|
||||
}
|
||||
MUNITOR_EXTRACT_EOF
|
||||
|
||||
cat > /tmp/munitor/docs_only.sh << 'MUNITOR_DOCS_EOF'
|
||||
#!/usr/bin/env bash
|
||||
# Source this helper. Any uncertainty keeps the full pipeline.
|
||||
munitor_docs_only() (
|
||||
[[ -z "${CIRCLE_TAG:-}" ]] || return 1
|
||||
local base paths path seen=0
|
||||
case "${CIRCLE_BRANCH:-}" in
|
||||
feature/*|hotfix/*)
|
||||
git fetch --no-tags origin '+refs/heads/main:refs/remotes/origin/main' >/dev/null 2>&1 || return 1
|
||||
base=$(git merge-base HEAD refs/remotes/origin/main 2>/dev/null) || return 1
|
||||
;;
|
||||
main)
|
||||
base=$(git rev-parse --verify 'HEAD^1' 2>/dev/null) || return 1
|
||||
;;
|
||||
*) return 1 ;;
|
||||
esac
|
||||
[[ "$(git rev-parse --is-shallow-repository 2>/dev/null)" == false ]] || return 1
|
||||
paths=$(mktemp 2>/dev/null) || return 1
|
||||
trap 'rm -f -- "$paths"' EXIT
|
||||
git diff --no-ext-diff --no-renames --name-only -z "$base" HEAD >"$paths" 2>/dev/null || return 1
|
||||
while IFS= read -r -d '' path; do
|
||||
case "$path" in
|
||||
README.md|CHANGELOG.md|CONTRIBUTING.md|docs/*.md) ;;
|
||||
*) return 1 ;;
|
||||
esac
|
||||
seen=1
|
||||
done <"$paths"
|
||||
[[ "$seen" -eq 1 ]]
|
||||
)
|
||||
MUNITOR_DOCS_EOF
|
||||
|
||||
cat > /tmp/munitor/templates/java-docs.yml.tpl << 'MUNITOR_TPL_EOF'
|
||||
version: 2.1
|
||||
|
||||
orbs:
|
||||
munitor: ${MUNITOR_ORB_SLUG}@${MUNITOR_ORB_VERSION}
|
||||
|
||||
workflows:
|
||||
docs-checks:
|
||||
jobs:
|
||||
- munitor/secrets_scan:
|
||||
name: secrets-scan
|
||||
- munitor/ci_ready:
|
||||
name: ci-ready
|
||||
requires:
|
||||
- secrets-scan
|
||||
MUNITOR_TPL_EOF
|
||||
|
||||
cat > /tmp/munitor/templates/java-webapp.yml.tpl << 'MUNITOR_TPL_EOF'
|
||||
version: 2.1
|
||||
|
||||
@@ -350,6 +406,8 @@ workflows:
|
||||
jobs:
|
||||
- munitor/mvn_build_and_test:
|
||||
name: build-and-test
|
||||
quality_checks: true
|
||||
min_instruction: "${MUNITOR_COVERAGE_MIN}"
|
||||
java_version: "${MUNITOR_JAVA_VERSION}"
|
||||
context:
|
||||
- ${MUNITOR_CONTEXT_GITHUB}
|
||||
@@ -358,6 +416,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:
|
||||
@@ -373,27 +445,6 @@ workflows:
|
||||
only:
|
||||
- /feature\/.*/
|
||||
- /hotfix\/.*/
|
||||
- munitor/mvn_code_quality:
|
||||
name: code-quality
|
||||
java_version: "${MUNITOR_JAVA_VERSION}"
|
||||
requires:
|
||||
- build-and-test
|
||||
filters:
|
||||
branches:
|
||||
only:
|
||||
- /feature\/.*/
|
||||
- /hotfix\/.*/
|
||||
- munitor/mvn_coverage:
|
||||
name: coverage
|
||||
java_version: "${MUNITOR_JAVA_VERSION}"
|
||||
min_instruction: "${MUNITOR_COVERAGE_MIN}"
|
||||
requires:
|
||||
- build-and-test
|
||||
filters:
|
||||
branches:
|
||||
only:
|
||||
- /feature\/.*/
|
||||
- /hotfix\/.*/
|
||||
- munitor/security_scan:
|
||||
name: security-scan
|
||||
java_version: "${MUNITOR_JAVA_VERSION}"
|
||||
@@ -424,6 +475,23 @@ workflows:
|
||||
- /hotfix\/.*/
|
||||
##ENDIF_E2E##
|
||||
|
||||
- munitor/ci_ready:
|
||||
name: ci-ready
|
||||
requires:
|
||||
- build-and-test
|
||||
- image-scan
|
||||
- secrets-scan
|
||||
- sast-scan
|
||||
- security-scan
|
||||
##IF_E2E##
|
||||
- e2e-tests
|
||||
##ENDIF_E2E##
|
||||
filters:
|
||||
branches:
|
||||
only:
|
||||
- /feature\/.*/
|
||||
- /hotfix\/.*/
|
||||
|
||||
##INCLUDE_DEPLOY java-webapp-deploy develop develop ${MUNITOR_CD_ENV_DEVELOP}##
|
||||
##INCLUDE_DEPLOY java-webapp-deploy staging staging ${MUNITOR_CD_ENV_STAGING}##
|
||||
|
||||
@@ -431,6 +499,8 @@ workflows:
|
||||
jobs:
|
||||
- munitor/mvn_build_and_test:
|
||||
name: build-and-test
|
||||
quality_checks: true
|
||||
min_instruction: "${MUNITOR_COVERAGE_MIN}"
|
||||
java_version: "${MUNITOR_JAVA_VERSION}"
|
||||
context:
|
||||
- ${MUNITOR_CONTEXT_GITHUB}
|
||||
@@ -451,25 +521,6 @@ workflows:
|
||||
branches:
|
||||
only:
|
||||
- /release\/.*/
|
||||
- munitor/mvn_code_quality:
|
||||
name: code-quality
|
||||
java_version: "${MUNITOR_JAVA_VERSION}"
|
||||
requires:
|
||||
- build-and-test
|
||||
filters:
|
||||
branches:
|
||||
only:
|
||||
- /release\/.*/
|
||||
- munitor/mvn_coverage:
|
||||
name: coverage
|
||||
java_version: "${MUNITOR_JAVA_VERSION}"
|
||||
min_instruction: "${MUNITOR_COVERAGE_MIN}"
|
||||
requires:
|
||||
- build-and-test
|
||||
filters:
|
||||
branches:
|
||||
only:
|
||||
- /release\/.*/
|
||||
- munitor/security_scan:
|
||||
name: security-scan
|
||||
java_version: "${MUNITOR_JAVA_VERSION}"
|
||||
@@ -504,7 +555,6 @@ workflows:
|
||||
sonar_project_key: ${MUNITOR_SONAR_PROJECT_KEY}
|
||||
requires:
|
||||
- build-and-test
|
||||
- coverage
|
||||
context:
|
||||
- ${MUNITOR_CONTEXT_SONAR}
|
||||
filters:
|
||||
@@ -525,8 +575,7 @@ workflows:
|
||||
supplemental_images: '${MUNITOR_SUPPLEMENTAL_IMAGES_JSON}'
|
||||
##ENDIF_SUPPLEMENTAL##
|
||||
requires:
|
||||
- code-quality
|
||||
- coverage
|
||||
- build-and-test
|
||||
- secrets-scan
|
||||
- sast-scan
|
||||
- security-scan
|
||||
@@ -549,8 +598,7 @@ workflows:
|
||||
context:
|
||||
- ${MUNITOR_CONTEXT_GITHUB}
|
||||
requires:
|
||||
- code-quality
|
||||
- coverage
|
||||
- build-and-test
|
||||
- secrets-scan
|
||||
- sast-scan
|
||||
- security-scan
|
||||
@@ -572,7 +620,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##
|
||||
@@ -600,10 +650,28 @@ workflows:
|
||||
- /release\/.*/
|
||||
##ENDIF_GITHUB_RELEASE##
|
||||
|
||||
- munitor/ci_ready:
|
||||
name: ci-ready
|
||||
requires:
|
||||
- docker-build-push
|
||||
##IF_SBOM##
|
||||
- sbom
|
||||
##ENDIF_SBOM##
|
||||
##IF_CD_NON_PROD##
|
||||
- update-cd-repo
|
||||
##ENDIF_CD_NON_PROD##
|
||||
##IF_GITHUB_RELEASE##
|
||||
- github-release
|
||||
##ENDIF_GITHUB_RELEASE##
|
||||
filters:
|
||||
branches:
|
||||
only:
|
||||
- /release\/.*/
|
||||
|
||||
production:
|
||||
jobs:
|
||||
- munitor/mvn_build_and_test:
|
||||
name: build-and-test
|
||||
name: production-build-and-test
|
||||
java_version: "${MUNITOR_JAVA_VERSION}"
|
||||
context:
|
||||
- ${MUNITOR_CONTEXT_GITHUB}
|
||||
@@ -623,7 +691,7 @@ workflows:
|
||||
supplemental_images: '${MUNITOR_SUPPLEMENTAL_IMAGES_JSON}'
|
||||
##ENDIF_SUPPLEMENTAL##
|
||||
requires:
|
||||
- build-and-test
|
||||
- production-build-and-test
|
||||
context:
|
||||
- ${MUNITOR_CONTEXT_REGISTRY}
|
||||
filters:
|
||||
@@ -636,7 +704,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##
|
||||
@@ -661,6 +731,20 @@ workflows:
|
||||
branches:
|
||||
only: main
|
||||
##ENDIF_GITHUB_RELEASE##
|
||||
|
||||
- munitor/ci_ready:
|
||||
name: ci-ready
|
||||
requires:
|
||||
- docker-build-push
|
||||
##IF_CD##
|
||||
- update-cd-repo
|
||||
##ENDIF_CD##
|
||||
##IF_GITHUB_RELEASE##
|
||||
- github-release
|
||||
##ENDIF_GITHUB_RELEASE##
|
||||
filters:
|
||||
branches:
|
||||
only: main
|
||||
MUNITOR_TPL_EOF
|
||||
|
||||
cat > /tmp/munitor/templates/gradle-webapp.yml.tpl << 'MUNITOR_TPL_EOF'
|
||||
@@ -682,6 +766,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:
|
||||
@@ -896,7 +994,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 +1060,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##
|
||||
@@ -1021,6 +1123,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:
|
||||
@@ -1248,7 +1364,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 +1443,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##
|
||||
@@ -1386,6 +1506,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:
|
||||
@@ -1613,7 +1747,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 +1826,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##
|
||||
@@ -2492,6 +2630,8 @@ cat > /tmp/munitor/templates/partials/java-webapp-deploy.yml.tpl << 'MUNITOR_PAR
|
||||
jobs:
|
||||
- munitor/mvn_build_and_test:
|
||||
name: build-and-test
|
||||
quality_checks: true
|
||||
min_instruction: "${MUNITOR_COVERAGE_MIN}"
|
||||
java_version: "${MUNITOR_JAVA_VERSION}"
|
||||
context:
|
||||
- ${MUNITOR_CONTEXT_GITHUB}
|
||||
@@ -2509,23 +2649,6 @@ cat > /tmp/munitor/templates/partials/java-webapp-deploy.yml.tpl << 'MUNITOR_PAR
|
||||
filters:
|
||||
branches:
|
||||
only: __BRANCH_FILTER__
|
||||
- munitor/mvn_code_quality:
|
||||
name: code-quality
|
||||
java_version: "${MUNITOR_JAVA_VERSION}"
|
||||
requires:
|
||||
- build-and-test
|
||||
filters:
|
||||
branches:
|
||||
only: __BRANCH_FILTER__
|
||||
- munitor/mvn_coverage:
|
||||
name: coverage
|
||||
java_version: "${MUNITOR_JAVA_VERSION}"
|
||||
min_instruction: "${MUNITOR_COVERAGE_MIN}"
|
||||
requires:
|
||||
- build-and-test
|
||||
filters:
|
||||
branches:
|
||||
only: __BRANCH_FILTER__
|
||||
- munitor/security_scan:
|
||||
name: security-scan
|
||||
java_version: "${MUNITOR_JAVA_VERSION}"
|
||||
@@ -2558,7 +2681,6 @@ cat > /tmp/munitor/templates/partials/java-webapp-deploy.yml.tpl << 'MUNITOR_PAR
|
||||
sonar_project_key: ${MUNITOR_SONAR_PROJECT_KEY}
|
||||
requires:
|
||||
- build-and-test
|
||||
- coverage
|
||||
context:
|
||||
- ${MUNITOR_CONTEXT_SONAR}
|
||||
filters:
|
||||
@@ -2572,12 +2694,13 @@ cat > /tmp/munitor/templates/partials/java-webapp-deploy.yml.tpl << 'MUNITOR_PAR
|
||||
health_path: ${MUNITOR_HEALTH_PATH}
|
||||
health_port: "${MUNITOR_HEALTH_PORT}"
|
||||
health_db: "${MUNITOR_HEALTH_DB}"
|
||||
health_migrations: "${MUNITOR_HEALTH_MIGRATIONS}"
|
||||
health_smoke_paths: "${MUNITOR_HEALTH_SMOKE_PATHS}"
|
||||
##IF_SUPPLEMENTAL##
|
||||
supplemental_images: '${MUNITOR_SUPPLEMENTAL_IMAGES_JSON}'
|
||||
##ENDIF_SUPPLEMENTAL##
|
||||
requires:
|
||||
- code-quality
|
||||
- coverage
|
||||
- build-and-test
|
||||
- secrets-scan
|
||||
- sast-scan
|
||||
- security-scan
|
||||
@@ -2599,8 +2722,7 @@ cat > /tmp/munitor/templates/partials/java-webapp-deploy.yml.tpl << 'MUNITOR_PAR
|
||||
context:
|
||||
- ${MUNITOR_CONTEXT_GITHUB}
|
||||
requires:
|
||||
- code-quality
|
||||
- coverage
|
||||
- build-and-test
|
||||
- secrets-scan
|
||||
- sast-scan
|
||||
- security-scan
|
||||
@@ -2621,7 +2743,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##
|
||||
@@ -2635,6 +2759,20 @@ cat > /tmp/munitor/templates/partials/java-webapp-deploy.yml.tpl << 'MUNITOR_PAR
|
||||
branches:
|
||||
only: __BRANCH_FILTER__
|
||||
##ENDIF_CD_NON_PROD##
|
||||
|
||||
- munitor/ci_ready:
|
||||
name: ci-ready
|
||||
requires:
|
||||
- docker-build-push
|
||||
##IF_SBOM##
|
||||
- sbom
|
||||
##ENDIF_SBOM##
|
||||
##IF_CD_NON_PROD##
|
||||
- update-cd-repo
|
||||
##ENDIF_CD_NON_PROD##
|
||||
filters:
|
||||
branches:
|
||||
only: __BRANCH_FILTER__
|
||||
MUNITOR_PARTIAL_EOF
|
||||
|
||||
cat > /tmp/munitor/templates/partials/gradle-webapp-deploy.yml.tpl << 'MUNITOR_PARTIAL_EOF'
|
||||
@@ -2771,7 +2909,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 +3074,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 +3239,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##
|
||||
@@ -3217,7 +3361,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 ci"
|
||||
ACTUAL_KEYS=$(yq 'keys | .[]' "${CONFIG_FILE}" 2>/dev/null || true)
|
||||
for key in ${ACTUAL_KEYS}; do
|
||||
if ! echo "${KNOWN_KEYS}" | grep -qw "${key}"; then
|
||||
@@ -3230,6 +3374,17 @@ done
|
||||
# --------------------------------------------------------------------------
|
||||
TEMPLATE_FILE="${TEMPLATE_DIR}/${PIPELINE_TYPE}.yml.tpl"
|
||||
|
||||
# Opt in only after the consumer requires ci-ready instead of individual build jobs.
|
||||
# Missing history or any non-documentation path keeps the complete pipeline.
|
||||
if [[ "${PIPELINE_TYPE}" == "java-webapp" && "$(yq '.ci.docs_only // false' "${CONFIG_FILE}")" == "true" ]]; then
|
||||
# shellcheck source=docs_only.sh
|
||||
source "${SCRIPT_DIR}/docs_only.sh"
|
||||
if munitor_docs_only; then
|
||||
TEMPLATE_FILE="${TEMPLATE_DIR}/java-docs.yml.tpl"
|
||||
echo "Documentation-only change: retaining secret scanning; no application build or deployment."
|
||||
fi
|
||||
fi
|
||||
|
||||
if [[ ! -f "${TEMPLATE_FILE}" ]]; then
|
||||
echo "ERROR: No template found for pipeline type '${PIPELINE_TYPE}'."
|
||||
echo "Available templates:"
|
||||
@@ -3321,7 +3476,7 @@ process_conditionals() {
|
||||
cp "${input}" "${tmpfile}"
|
||||
|
||||
# Process each conditional flag
|
||||
for flag in E2E SBOM SONAR NPM_AUTH SERVICES TEST_SETUP CUSTOM_TEST COVERAGE_CMD GITHUB_RELEASE SAST CD CD_NON_PROD OWASP NVD SUPPLEMENTAL; do
|
||||
for flag in E2E SBOM SONAR NPM_AUTH SERVICES TEST_SETUP CUSTOM_TEST COVERAGE_CMD GITHUB_RELEASE SAST CD CD_NON_PROD CD_EXPLICIT_PATH OWASP NVD SUPPLEMENTAL; do
|
||||
local var_name="MUNITOR_${flag}"
|
||||
local value="${!var_name:-false}"
|
||||
local flag_file
|
||||
|
||||
@@ -303,6 +303,14 @@ extract_munitor_vars() {
|
||||
MUNITOR_CD_NON_PROD="false"
|
||||
fi
|
||||
|
||||
# cd_path is only emitted when set, so manifests without cd.path still render
|
||||
# pipelines that validate against orb versions predating the parameter.
|
||||
if [[ -n "${MUNITOR_CD_PATH}" ]]; then
|
||||
MUNITOR_CD_EXPLICIT_PATH="true"
|
||||
else
|
||||
MUNITOR_CD_EXPLICIT_PATH="false"
|
||||
fi
|
||||
|
||||
if [[ -n "${MUNITOR_CONTEXT_NVD}" ]]; then
|
||||
MUNITOR_NVD="true"
|
||||
else
|
||||
@@ -329,7 +337,7 @@ extract_munitor_vars() {
|
||||
export MUNITOR_CI_EMAIL MUNITOR_CI_NAME MUNITOR_NPM_DEFAULT_SCOPE MUNITOR_ORB_SLUG
|
||||
export MUNITOR_HEALTH_PATH MUNITOR_HEALTH_PORT MUNITOR_HEALTH_DB MUNITOR_HEALTH_MIGRATIONS MUNITOR_HEALTH_SMOKE_PATHS
|
||||
export MUNITOR_SUPPLEMENTAL MUNITOR_SUPPLEMENTAL_IMAGES_JSON
|
||||
export MUNITOR_SONAR MUNITOR_CD MUNITOR_CD_NON_PROD MUNITOR_NVD
|
||||
export MUNITOR_SONAR MUNITOR_CD MUNITOR_CD_NON_PROD MUNITOR_NVD MUNITOR_CD_EXPLICIT_PATH
|
||||
}
|
||||
|
||||
# Build the envsubst variable list
|
||||
|
||||
+34
-11
@@ -15,22 +15,45 @@ munitor_header "run_semgrep"
|
||||
|
||||
CONFIG="${SEMGREP_CONFIG:-auto}"
|
||||
FAIL_ON_FINDINGS="${SEMGREP_FAIL_ON_FINDINGS:-true}"
|
||||
REPORT="${SEMGREP_REPORT:-/tmp/semgrep-results.json}"
|
||||
|
||||
echo "Running Semgrep SAST scan (config: ${CONFIG}, fail_on_findings: ${FAIL_ON_FINDINGS})..."
|
||||
|
||||
EXIT_CODE=0
|
||||
semgrep ci \
|
||||
--config "${CONFIG}" \
|
||||
--json \
|
||||
--output /tmp/semgrep-results.json || {
|
||||
EXIT_CODE=$?
|
||||
if [[ "${FAIL_ON_FINDINGS}" == "true" ]]; then
|
||||
echo "ERROR: Semgrep found issues."
|
||||
echo "Review the report at: /tmp/semgrep-results.json"
|
||||
exit "${EXIT_CODE}"
|
||||
else
|
||||
echo "WARNING: Semgrep found issues (warn-only mode, not failing build)."
|
||||
echo "Review the report at: /tmp/semgrep-results.json"
|
||||
fi
|
||||
}
|
||||
--output "${REPORT}" || EXIT_CODE=$?
|
||||
|
||||
# Summarise the report, and fail a scan that ran no rules or scanned no files:
|
||||
# semgrep exits 0 when the downloaded rules fail to parse (an outdated semgrep),
|
||||
# which used to pass this job without scanning anything.
|
||||
python3 - "${REPORT}" <<'PY'
|
||||
import json, sys
|
||||
report = json.load(open(sys.argv[1]))
|
||||
scanned = len(report.get("paths", {}).get("scanned", []))
|
||||
results = report.get("results", [])
|
||||
errors = report.get("errors", [])
|
||||
rule_errors = [e for e in errors if "rule" in str(e.get("type", "")).lower()
|
||||
or "config" in str(e.get("message", "")).lower()]
|
||||
print(f"Semgrep: {scanned} files scanned, {len(results)} findings, {len(errors)} errors.")
|
||||
for r in results[:50]:
|
||||
print(f" {r.get('check_id')} {r.get('path')}:{r.get('start', {}).get('line')}")
|
||||
if scanned == 0 or rule_errors:
|
||||
for e in rule_errors[:5]:
|
||||
print(f" error: {str(e.get('message', e))[:300]}")
|
||||
print("ERROR: Semgrep did not run its rules (no files scanned or invalid rule configuration).")
|
||||
sys.exit(2)
|
||||
PY
|
||||
|
||||
if [[ "${EXIT_CODE}" -ne 0 ]]; then
|
||||
if [[ "${FAIL_ON_FINDINGS}" == "true" ]]; then
|
||||
echo "ERROR: Semgrep found issues."
|
||||
echo "Review the report at: ${REPORT}"
|
||||
exit "${EXIT_CODE}"
|
||||
fi
|
||||
echo "WARNING: Semgrep found issues (warn-only mode, not failing build)."
|
||||
echo "Review the report at: ${REPORT}"
|
||||
fi
|
||||
|
||||
echo "Semgrep scan completed."
|
||||
@@ -36,13 +36,17 @@ fi
|
||||
|
||||
echo "Running Trivy filesystem scan..."
|
||||
|
||||
# --exit-code 1: without it trivy exits 0 even when it reports HIGH/CRITICAL
|
||||
# findings, so this job could never fail.
|
||||
trivy fs \
|
||||
--format json \
|
||||
--output /tmp/trivy-results.json \
|
||||
--severity HIGH,CRITICAL \
|
||||
--exit-code 1 \
|
||||
. || {
|
||||
EXIT_CODE=$?
|
||||
echo "Trivy found vulnerabilities. See /tmp/trivy-results.json"
|
||||
trivy fs --severity HIGH,CRITICAL . || true
|
||||
exit "${EXIT_CODE}"
|
||||
}
|
||||
|
||||
|
||||
@@ -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:
|
||||
@@ -230,7 +244,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 +310,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##
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
version: 2.1
|
||||
|
||||
orbs:
|
||||
munitor: ${MUNITOR_ORB_SLUG}@${MUNITOR_ORB_VERSION}
|
||||
|
||||
workflows:
|
||||
docs-checks:
|
||||
jobs:
|
||||
- munitor/secrets_scan:
|
||||
name: secrets-scan
|
||||
- munitor/ci_ready:
|
||||
name: ci-ready
|
||||
requires:
|
||||
- secrets-scan
|
||||
@@ -8,6 +8,8 @@ workflows:
|
||||
jobs:
|
||||
- munitor/mvn_build_and_test:
|
||||
name: build-and-test
|
||||
quality_checks: true
|
||||
min_instruction: "${MUNITOR_COVERAGE_MIN}"
|
||||
java_version: "${MUNITOR_JAVA_VERSION}"
|
||||
context:
|
||||
- ${MUNITOR_CONTEXT_GITHUB}
|
||||
@@ -16,6 +18,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:
|
||||
@@ -31,27 +47,6 @@ workflows:
|
||||
only:
|
||||
- /feature\/.*/
|
||||
- /hotfix\/.*/
|
||||
- munitor/mvn_code_quality:
|
||||
name: code-quality
|
||||
java_version: "${MUNITOR_JAVA_VERSION}"
|
||||
requires:
|
||||
- build-and-test
|
||||
filters:
|
||||
branches:
|
||||
only:
|
||||
- /feature\/.*/
|
||||
- /hotfix\/.*/
|
||||
- munitor/mvn_coverage:
|
||||
name: coverage
|
||||
java_version: "${MUNITOR_JAVA_VERSION}"
|
||||
min_instruction: "${MUNITOR_COVERAGE_MIN}"
|
||||
requires:
|
||||
- build-and-test
|
||||
filters:
|
||||
branches:
|
||||
only:
|
||||
- /feature\/.*/
|
||||
- /hotfix\/.*/
|
||||
- munitor/security_scan:
|
||||
name: security-scan
|
||||
java_version: "${MUNITOR_JAVA_VERSION}"
|
||||
@@ -82,6 +77,23 @@ workflows:
|
||||
- /hotfix\/.*/
|
||||
##ENDIF_E2E##
|
||||
|
||||
- munitor/ci_ready:
|
||||
name: ci-ready
|
||||
requires:
|
||||
- build-and-test
|
||||
- image-scan
|
||||
- secrets-scan
|
||||
- sast-scan
|
||||
- security-scan
|
||||
##IF_E2E##
|
||||
- e2e-tests
|
||||
##ENDIF_E2E##
|
||||
filters:
|
||||
branches:
|
||||
only:
|
||||
- /feature\/.*/
|
||||
- /hotfix\/.*/
|
||||
|
||||
##INCLUDE_DEPLOY java-webapp-deploy develop develop ${MUNITOR_CD_ENV_DEVELOP}##
|
||||
##INCLUDE_DEPLOY java-webapp-deploy staging staging ${MUNITOR_CD_ENV_STAGING}##
|
||||
|
||||
@@ -89,6 +101,8 @@ workflows:
|
||||
jobs:
|
||||
- munitor/mvn_build_and_test:
|
||||
name: build-and-test
|
||||
quality_checks: true
|
||||
min_instruction: "${MUNITOR_COVERAGE_MIN}"
|
||||
java_version: "${MUNITOR_JAVA_VERSION}"
|
||||
context:
|
||||
- ${MUNITOR_CONTEXT_GITHUB}
|
||||
@@ -109,25 +123,6 @@ workflows:
|
||||
branches:
|
||||
only:
|
||||
- /release\/.*/
|
||||
- munitor/mvn_code_quality:
|
||||
name: code-quality
|
||||
java_version: "${MUNITOR_JAVA_VERSION}"
|
||||
requires:
|
||||
- build-and-test
|
||||
filters:
|
||||
branches:
|
||||
only:
|
||||
- /release\/.*/
|
||||
- munitor/mvn_coverage:
|
||||
name: coverage
|
||||
java_version: "${MUNITOR_JAVA_VERSION}"
|
||||
min_instruction: "${MUNITOR_COVERAGE_MIN}"
|
||||
requires:
|
||||
- build-and-test
|
||||
filters:
|
||||
branches:
|
||||
only:
|
||||
- /release\/.*/
|
||||
- munitor/security_scan:
|
||||
name: security-scan
|
||||
java_version: "${MUNITOR_JAVA_VERSION}"
|
||||
@@ -162,7 +157,6 @@ workflows:
|
||||
sonar_project_key: ${MUNITOR_SONAR_PROJECT_KEY}
|
||||
requires:
|
||||
- build-and-test
|
||||
- coverage
|
||||
context:
|
||||
- ${MUNITOR_CONTEXT_SONAR}
|
||||
filters:
|
||||
@@ -183,8 +177,7 @@ workflows:
|
||||
supplemental_images: '${MUNITOR_SUPPLEMENTAL_IMAGES_JSON}'
|
||||
##ENDIF_SUPPLEMENTAL##
|
||||
requires:
|
||||
- code-quality
|
||||
- coverage
|
||||
- build-and-test
|
||||
- secrets-scan
|
||||
- sast-scan
|
||||
- security-scan
|
||||
@@ -207,8 +200,7 @@ workflows:
|
||||
context:
|
||||
- ${MUNITOR_CONTEXT_GITHUB}
|
||||
requires:
|
||||
- code-quality
|
||||
- coverage
|
||||
- build-and-test
|
||||
- secrets-scan
|
||||
- sast-scan
|
||||
- security-scan
|
||||
@@ -230,7 +222,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##
|
||||
@@ -258,10 +252,28 @@ workflows:
|
||||
- /release\/.*/
|
||||
##ENDIF_GITHUB_RELEASE##
|
||||
|
||||
- munitor/ci_ready:
|
||||
name: ci-ready
|
||||
requires:
|
||||
- docker-build-push
|
||||
##IF_SBOM##
|
||||
- sbom
|
||||
##ENDIF_SBOM##
|
||||
##IF_CD_NON_PROD##
|
||||
- update-cd-repo
|
||||
##ENDIF_CD_NON_PROD##
|
||||
##IF_GITHUB_RELEASE##
|
||||
- github-release
|
||||
##ENDIF_GITHUB_RELEASE##
|
||||
filters:
|
||||
branches:
|
||||
only:
|
||||
- /release\/.*/
|
||||
|
||||
production:
|
||||
jobs:
|
||||
- munitor/mvn_build_and_test:
|
||||
name: build-and-test
|
||||
name: production-build-and-test
|
||||
java_version: "${MUNITOR_JAVA_VERSION}"
|
||||
context:
|
||||
- ${MUNITOR_CONTEXT_GITHUB}
|
||||
@@ -281,7 +293,7 @@ workflows:
|
||||
supplemental_images: '${MUNITOR_SUPPLEMENTAL_IMAGES_JSON}'
|
||||
##ENDIF_SUPPLEMENTAL##
|
||||
requires:
|
||||
- build-and-test
|
||||
- production-build-and-test
|
||||
context:
|
||||
- ${MUNITOR_CONTEXT_REGISTRY}
|
||||
filters:
|
||||
@@ -294,7 +306,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##
|
||||
@@ -319,3 +333,17 @@ workflows:
|
||||
branches:
|
||||
only: main
|
||||
##ENDIF_GITHUB_RELEASE##
|
||||
|
||||
- munitor/ci_ready:
|
||||
name: ci-ready
|
||||
requires:
|
||||
- docker-build-push
|
||||
##IF_CD##
|
||||
- update-cd-repo
|
||||
##ENDIF_CD##
|
||||
##IF_GITHUB_RELEASE##
|
||||
- github-release
|
||||
##ENDIF_GITHUB_RELEASE##
|
||||
filters:
|
||||
branches:
|
||||
only: main
|
||||
@@ -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:
|
||||
@@ -258,7 +272,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 +351,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##
|
||||
|
||||
@@ -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:
|
||||
@@ -258,7 +272,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 +351,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##
|
||||
|
||||
@@ -2,6 +2,8 @@
|
||||
jobs:
|
||||
- munitor/mvn_build_and_test:
|
||||
name: build-and-test
|
||||
quality_checks: true
|
||||
min_instruction: "${MUNITOR_COVERAGE_MIN}"
|
||||
java_version: "${MUNITOR_JAVA_VERSION}"
|
||||
context:
|
||||
- ${MUNITOR_CONTEXT_GITHUB}
|
||||
@@ -19,23 +21,6 @@
|
||||
filters:
|
||||
branches:
|
||||
only: __BRANCH_FILTER__
|
||||
- munitor/mvn_code_quality:
|
||||
name: code-quality
|
||||
java_version: "${MUNITOR_JAVA_VERSION}"
|
||||
requires:
|
||||
- build-and-test
|
||||
filters:
|
||||
branches:
|
||||
only: __BRANCH_FILTER__
|
||||
- munitor/mvn_coverage:
|
||||
name: coverage
|
||||
java_version: "${MUNITOR_JAVA_VERSION}"
|
||||
min_instruction: "${MUNITOR_COVERAGE_MIN}"
|
||||
requires:
|
||||
- build-and-test
|
||||
filters:
|
||||
branches:
|
||||
only: __BRANCH_FILTER__
|
||||
- munitor/security_scan:
|
||||
name: security-scan
|
||||
java_version: "${MUNITOR_JAVA_VERSION}"
|
||||
@@ -68,7 +53,6 @@
|
||||
sonar_project_key: ${MUNITOR_SONAR_PROJECT_KEY}
|
||||
requires:
|
||||
- build-and-test
|
||||
- coverage
|
||||
context:
|
||||
- ${MUNITOR_CONTEXT_SONAR}
|
||||
filters:
|
||||
@@ -82,12 +66,13 @@
|
||||
health_path: ${MUNITOR_HEALTH_PATH}
|
||||
health_port: "${MUNITOR_HEALTH_PORT}"
|
||||
health_db: "${MUNITOR_HEALTH_DB}"
|
||||
health_migrations: "${MUNITOR_HEALTH_MIGRATIONS}"
|
||||
health_smoke_paths: "${MUNITOR_HEALTH_SMOKE_PATHS}"
|
||||
##IF_SUPPLEMENTAL##
|
||||
supplemental_images: '${MUNITOR_SUPPLEMENTAL_IMAGES_JSON}'
|
||||
##ENDIF_SUPPLEMENTAL##
|
||||
requires:
|
||||
- code-quality
|
||||
- coverage
|
||||
- build-and-test
|
||||
- secrets-scan
|
||||
- sast-scan
|
||||
- security-scan
|
||||
@@ -109,8 +94,7 @@
|
||||
context:
|
||||
- ${MUNITOR_CONTEXT_GITHUB}
|
||||
requires:
|
||||
- code-quality
|
||||
- coverage
|
||||
- build-and-test
|
||||
- secrets-scan
|
||||
- sast-scan
|
||||
- security-scan
|
||||
@@ -131,7 +115,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##
|
||||
@@ -145,3 +131,17 @@
|
||||
branches:
|
||||
only: __BRANCH_FILTER__
|
||||
##ENDIF_CD_NON_PROD##
|
||||
|
||||
- munitor/ci_ready:
|
||||
name: ci-ready
|
||||
requires:
|
||||
- docker-build-push
|
||||
##IF_SBOM##
|
||||
- sbom
|
||||
##ENDIF_SBOM##
|
||||
##IF_CD_NON_PROD##
|
||||
- update-cd-repo
|
||||
##ENDIF_CD_NON_PROD##
|
||||
filters:
|
||||
branches:
|
||||
only: __BRANCH_FILTER__
|
||||
@@ -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##
|
||||
|
||||
+160
-104
@@ -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
|
||||
@@ -43,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}"
|
||||
# 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
|
||||
@@ -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 ]
|
||||
}
|
||||
@@ -0,0 +1,84 @@
|
||||
#!/usr/bin/env bats
|
||||
|
||||
setup() {
|
||||
ROOT="${BATS_TEST_DIRNAME}/.."
|
||||
CONFIG="${BATS_TEST_TMPDIR}/rendered.yml"
|
||||
}
|
||||
|
||||
check_workflows() {
|
||||
local expected="$1"
|
||||
yq -o=json '.' "$CONFIG" | EXPECTED_COVERAGE="$expected" node -e '
|
||||
const assert = require("node:assert/strict");
|
||||
const config = JSON.parse(require("node:fs").readFileSync(0,"utf8"));
|
||||
for (const [workflow, definition] of Object.entries(config.workflows)) {
|
||||
const jobs = definition.jobs.map(entry => {
|
||||
const [type, options] = Object.entries(entry)[0];
|
||||
assert.ok(!["munitor/mvn_code_quality", "munitor/mvn_coverage"].includes(type));
|
||||
return {type, ...options};
|
||||
});
|
||||
const names = jobs.map(job => job.name);
|
||||
assert.equal(new Set(names).size, names.length, workflow);
|
||||
const buildName = workflow === "production" ? "production-build-and-test" : "build-and-test";
|
||||
const build = jobs.find(job => job.name === buildName);
|
||||
assert.ok(build, workflow);
|
||||
assert.equal(jobs.filter(job => job.type === "munitor/mvn_build_and_test").length, 1);
|
||||
assert.ok(!names.includes(workflow === "production" ? "build-and-test" : "production-build-and-test"));
|
||||
const expectedFilter = {
|
||||
"pr-checks": ["/feature\\/.*/", "/hotfix\\/.*/"],
|
||||
develop: "develop", staging: "staging",
|
||||
"release-candidate": ["/release\\/.*/"], production: "main"
|
||||
}[workflow];
|
||||
assert.deepEqual(build.filters.branches.only, expectedFilter, workflow);
|
||||
if (workflow === "production") {
|
||||
assert.equal(build.quality_checks, undefined);
|
||||
assert.equal(build.min_instruction, undefined);
|
||||
} else {
|
||||
assert.equal(build.quality_checks, true, workflow);
|
||||
assert.equal(String(build.min_instruction), process.env.EXPECTED_COVERAGE, workflow);
|
||||
for (const gate of ["secrets-scan", "sast-scan", "security-scan"]) assert.ok(names.includes(gate), workflow);
|
||||
}
|
||||
for (const job of jobs) {
|
||||
assert.deepEqual(job.filters, build.filters, `${workflow}/${job.name} filters`);
|
||||
const dependencies = job.requires || [];
|
||||
assert.equal(new Set(dependencies).size, dependencies.length, job.name);
|
||||
for (const name of dependencies) assert.ok(names.includes(name), `${workflow}/${job.name}: ${name}`);
|
||||
if (["image-scan", "security-scan", "e2e-tests", "sonar-scan"].includes(job.name)) assert.ok(dependencies.includes(buildName));
|
||||
if (["docker-build-push", "sbom"].includes(job.name)) {
|
||||
assert.ok(dependencies.includes(buildName));
|
||||
if (workflow !== "production") for (const gate of ["secrets-scan", "sast-scan", "security-scan", "e2e-tests", "sonar-scan"].filter(name => names.includes(name))) assert.ok(dependencies.includes(gate), `${workflow}/${job.name}/${gate}`);
|
||||
}
|
||||
if (["update-cd-repo", "github-release"].includes(job.name)) assert.ok(dependencies.includes("docker-build-push"));
|
||||
}
|
||||
if (workflow === "pr-checks") assert.ok(names.includes("image-scan"));
|
||||
}
|
||||
for (const workflow of ["pr-checks", "develop", "staging", "release-candidate", "production"]) assert.ok(config.workflows[workflow]);
|
||||
'
|
||||
}
|
||||
|
||||
@test "full Java workflow folds quality into build and preserves optional gates" {
|
||||
MUNITOR_CONFIG="${ROOT}/tests/fixtures/java-webapp.munitor.yml" MUNITOR_OUTPUT_FILE="$CONFIG" bash "${ROOT}/src/scripts/generate_config.sh"
|
||||
check_workflows 80
|
||||
[ "$(yq '.workflows.develop.jobs[] | .[] | select(.name == "sonar-scan") | .name' "$CONFIG")" = "sonar-scan" ]
|
||||
}
|
||||
|
||||
@test "minimal Java workflow retains default coverage and production behavior" {
|
||||
MUNITOR_CONFIG="${ROOT}/tests/fixtures/java-webapp-minimal.munitor.yml" MUNITOR_OUTPUT_FILE="$CONFIG" bash "${ROOT}/src/scripts/generate_config.sh"
|
||||
check_workflows 70
|
||||
}
|
||||
|
||||
@test "quality steps finish before workspace persistence and forward coverage threshold" {
|
||||
yq -o=json '.' "${ROOT}/src/jobs/mvn_build_and_test.yml" | node -e '
|
||||
const assert = require("node:assert/strict");
|
||||
const job = JSON.parse(require("node:fs").readFileSync(0,"utf8"));
|
||||
assert.equal(job.parameters.quality_checks.default, false);
|
||||
assert.equal(job.parameters.min_instruction.default, "70");
|
||||
const index = name => job.steps.findIndex(step => step === name || typeof step === "object" && name in step);
|
||||
assert.ok(index("mvn_test") < index("when"));
|
||||
assert.ok(index("when") < index("persist_to_workspace"));
|
||||
const block = job.steps[index("when")].when;
|
||||
assert.equal(block.condition, "<< parameters.quality_checks >>");
|
||||
const commands = block.steps.map(step => typeof step === "string" ? step : Object.keys(step)[0]);
|
||||
assert.deepEqual(commands.slice(0,4), ["mvn_checkstyle", "mvn_spotbugs", "mvn_pmd", "mvn_jacoco"]);
|
||||
assert.equal(block.steps[3].mvn_jacoco.min_instruction, "<< parameters.min_instruction >>");
|
||||
'
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
#!/usr/bin/env bats
|
||||
|
||||
setup() {
|
||||
SCRIPT="${BATS_TEST_DIRNAME}/../src/scripts/maven_cache_key.sh"
|
||||
cd "$BATS_TEST_TMPDIR"
|
||||
git init -q
|
||||
printf '<project><version>1-SNAPSHOT</version></project>\n' > pom.xml
|
||||
git add pom.xml
|
||||
}
|
||||
|
||||
@test "release rewrites retain the restored cache identity" {
|
||||
bash "$SCRIPT"
|
||||
cp .munitor-maven-cache-key before
|
||||
printf '<project><version>1.2.3</version></project>\n' > pom.xml
|
||||
bash "$SCRIPT"
|
||||
cmp before .munitor-maven-cache-key
|
||||
}
|
||||
|
||||
@test "nested checked-out dependency changes invalidate the cache" {
|
||||
mkdir module
|
||||
printf '<project><dependencies/></project>\n' > module/pom.xml
|
||||
git add module/pom.xml
|
||||
bash "$SCRIPT"
|
||||
cp .munitor-maven-cache-key before
|
||||
printf '<project><dependencies>changed</dependencies></project>\n' > module/pom.xml
|
||||
git add module/pom.xml
|
||||
bash "$SCRIPT"
|
||||
! cmp -s before .munitor-maven-cache-key
|
||||
}
|
||||
|
||||
@test "source changes do not invalidate dependency cache" {
|
||||
bash "$SCRIPT"
|
||||
cp .munitor-maven-cache-key before
|
||||
printf 'class Example {}\n' > Example.java
|
||||
git add Example.java
|
||||
bash "$SCRIPT"
|
||||
cmp before .munitor-maven-cache-key
|
||||
}
|
||||
|
||||
@test "restore has no broad fallback and save uses the same key" {
|
||||
command_root="${BATS_TEST_DIRNAME}/../src/commands"
|
||||
restore=$(sed -n '/maven-v2-/p' "$command_root/restore_maven_cache.yml")
|
||||
save=$(sed -n '/maven-v2-/p' "$command_root/save_maven_cache.yml")
|
||||
[[ "$restore" == *'maven-v2-{{ checksum ".munitor-maven-cache-key" }}'* ]]
|
||||
[[ "$save" == *'maven-v2-{{ checksum ".munitor-maven-cache-key" }}'* ]]
|
||||
! grep -q 'maven-v1\|maven-v2-$' "$command_root/restore_maven_cache.yml"
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
#!/usr/bin/env bats
|
||||
|
||||
setup() {
|
||||
SCRIPT="${BATS_TEST_DIRNAME}/../src/scripts/maven_workspace.sh"
|
||||
export HOME="${BATS_TEST_TMPDIR}/home"
|
||||
cd "$BATS_TEST_TMPDIR"
|
||||
}
|
||||
|
||||
@test "workspace overlays current installed artifacts and snapshot metadata without copying remote dependencies" {
|
||||
repo="$HOME/.m2/repository"
|
||||
mkdir -p "$repo/example/library/2-SNAPSHOT" "$repo/vendor/dependency/1"
|
||||
printf 'library-2-SNAPSHOT.jar>=\r\nlibrary-2-SNAPSHOT.pom>=\r\nlibrary-2-SNAPSHOT-tests.jar>=\r\n' > "$repo/example/library/2-SNAPSHOT/_remote.repositories"
|
||||
for extension in jar pom; do
|
||||
printf 'current build\n' > "$repo/example/library/2-SNAPSHOT/library-2-SNAPSHOT.$extension"
|
||||
done
|
||||
printf 'current test classes\n' > "$repo/example/library/2-SNAPSHOT/library-2-SNAPSHOT-tests.jar"
|
||||
printf 'snapshot metadata\n' > "$repo/example/library/maven-metadata-local.xml"
|
||||
printf 'plugin prefix metadata\n' > "$repo/example/maven-metadata-local.xml"
|
||||
printf 'dependency-1.jar>central=\n' > "$repo/vendor/dependency/1/_remote.repositories"
|
||||
printf 'remote dependency\n' > "$repo/vendor/dependency/1/dependency-1.jar"
|
||||
mkdir -p "$repo/vendor/mutable/2-SNAPSHOT"
|
||||
printf 'mutable-2-SNAPSHOT.jar>central=\n' > "$repo/vendor/mutable/2-SNAPSHOT/_remote.repositories"
|
||||
printf 'updated snapshot\n' > "$repo/vendor/mutable/2-SNAPSHOT/mutable-2-SNAPSHOT.jar"
|
||||
MAVEN_WORKSPACE_ACTION=save bash "$SCRIPT"
|
||||
run tar -tzf .munitor-maven-installed.tgz
|
||||
[ "$status" -eq 0 ]
|
||||
[[ "$output" != *vendor/dependency* ]]
|
||||
export HOME="${BATS_TEST_TMPDIR}/downstream"
|
||||
bash "$SCRIPT"
|
||||
restored="$HOME/.m2/repository/example/library"
|
||||
cmp "$repo/example/library/2-SNAPSHOT/library-2-SNAPSHOT.jar" "$restored/2-SNAPSHOT/library-2-SNAPSHOT.jar"
|
||||
cmp "$repo/example/library/maven-metadata-local.xml" "$restored/maven-metadata-local.xml"
|
||||
cmp "$repo/example/maven-metadata-local.xml" "$restored/../maven-metadata-local.xml"
|
||||
[ -f "$restored/2-SNAPSHOT/library-2-SNAPSHOT-tests.jar" ]
|
||||
[ ! -e "$HOME/.m2/repository/vendor/dependency" ]
|
||||
cmp "$repo/vendor/mutable/2-SNAPSHOT/mutable-2-SNAPSHOT.jar" "$HOME/.m2/repository/vendor/mutable/2-SNAPSHOT/mutable-2-SNAPSHOT.jar"
|
||||
}
|
||||
|
||||
@test "initial build without a workspace archive needs no overlay" {
|
||||
run bash "$SCRIPT"
|
||||
[ "$status" -eq 0 ]
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
// Public CI config only: no network, credentials or publication.
|
||||
const assert = require('node:assert/strict');
|
||||
const fs = require('node:fs');
|
||||
const config = JSON.parse(fs.readFileSync(0, 'utf8'));
|
||||
const workflows = Object.values(config.workflows);
|
||||
assert.equal(workflows.length, 1, 'all publishers must share the checked pack workspace');
|
||||
const jobs = workflows.flatMap(workflow => workflow.jobs).map(job => {
|
||||
const [type, settings] = Object.entries(job)[0];
|
||||
return { type, ...settings, name: settings.name || type };
|
||||
});
|
||||
assert.equal(jobs.filter(job => job.type === 'orb-tools/pack').length, 1, 'pack once per pipeline');
|
||||
const gates = ['orb-tools/lint', 'orb-tools/pack', 'orb-tools/review', 'test'];
|
||||
for (const gate of gates) assert.equal(jobs.filter(job => job.name === gate).length, 1, `retain required context ${gate}`);
|
||||
const publishers = jobs.filter(job => job.type === 'orb-tools/publish');
|
||||
assert.equal(publishers.length, 3);
|
||||
for (const job of publishers) {
|
||||
assert.deepEqual([...job.requires].sort(), [...gates].sort(), `${job.name} must wait for every gate`);
|
||||
assert.equal(job.orb_name, 'kof22/munitor');
|
||||
assert.deepEqual(job.context, ['orb-publishing']);
|
||||
}
|
||||
function matches(pattern, value) {
|
||||
return pattern.startsWith('/') && pattern.endsWith('/')
|
||||
? new RegExp(`^(?:${pattern.slice(1, -1)})$`).test(value) : pattern === value;
|
||||
}
|
||||
function runs(job, kind, ref) {
|
||||
const filter = job.filters?.[kind];
|
||||
if (!filter) return kind === 'branches'; // CircleCI ignores tags unless opted in.
|
||||
const list = value => Array.isArray(value) ? value : [value];
|
||||
return (!filter.only || list(filter.only).some(pattern => matches(pattern, ref)))
|
||||
&& (!filter.ignore || !list(filter.ignore).some(pattern => matches(pattern, ref)));
|
||||
}
|
||||
const cases = [
|
||||
['branches', 'develop', ['publish-develop-snapshot']],
|
||||
['branches', 'feature/ci-speed', ['publish-feature-sha']],
|
||||
['branches', 'main', []], ['branches', 'hotfix/1.2.3', []],
|
||||
['branches', 'release/1.2.3', []], ['branches', 'fix/ci-speed', []],
|
||||
['tags', 'v1.2.3', ['orb-tools/publish']],
|
||||
['tags', 'v1.2.3-RC.1', []], ['tags', 'v1.2', []], ['tags', 'other', []],
|
||||
];
|
||||
for (const [kind, ref, expected] of cases) {
|
||||
assert.deepEqual(publishers.filter(job => runs(job, kind, ref)).map(job => job.name).sort(), expected.sort(), `${kind}:${ref}`);
|
||||
for (const gate of gates) assert.ok(runs(jobs.find(job => job.name === gate), kind, ref), `${gate} must run for ${ref}`);
|
||||
}
|
||||
assert.equal(jobs.find(job => job.name === 'publish-develop-snapshot').dev_tags, 'dev:${CIRCLE_SHA1},dev:snapshot');
|
||||
assert.equal(jobs.find(job => job.name === 'publish-feature-sha').dev_tags, 'dev:${CIRCLE_SHA1}');
|
||||
assert.equal(jobs.find(job => job.name === 'orb-tools/publish').pub_type, 'production');
|
||||
assert.equal(jobs.find(job => job.name === 'publish-feature-sha').pub_type, 'dev');
|
||||
assert.equal(jobs.find(job => job.name === 'publish-develop-snapshot').pub_type, 'dev');
|
||||
console.log('PASS: one pack, unchanged required contexts, all publish gates and 10 branch/tag cases');
|
||||
@@ -0,0 +1,45 @@
|
||||
#!/usr/bin/env bats
|
||||
|
||||
setup() {
|
||||
export JOB="${BATS_TEST_DIRNAME}/../src/jobs/security_scan.yml"
|
||||
}
|
||||
|
||||
# Resolve only this job's compile-time OWASP condition, rejecting unknown ones.
|
||||
resolved_steps() {
|
||||
yq -o=json '.' "$JOB" | jq --argjson enabled "$1" '
|
||||
def expand:
|
||||
.[] | if type == "object" and has("when") then
|
||||
if .when.condition != "<< parameters.owasp >>" then error("unknown condition")
|
||||
elif $enabled then .when.steps | expand else empty end
|
||||
else . end;
|
||||
[.steps | expand]'
|
||||
}
|
||||
|
||||
@test "OWASP disabled skips Java Maven and NVD but retains Trivy and its artifact" {
|
||||
run resolved_steps false
|
||||
[ "$status" -eq 0 ]
|
||||
echo "$output" | jq -e '
|
||||
map(if type == "string" then . else keys[0] end) ==
|
||||
["attach_workspace", "restore_trivy_cache", "run_trivy", "save_trivy_cache", "store_artifacts"]' >/dev/null
|
||||
echo "$output" | jq -e '.[-1].store_artifacts.path == "/tmp/trivy-results.json"' >/dev/null
|
||||
}
|
||||
|
||||
@test "OWASP enabled retains ordered Java Maven NVD scan and report workflow" {
|
||||
run resolved_steps true
|
||||
[ "$status" -eq 0 ]
|
||||
echo "$output" | jq -e '
|
||||
map(if type == "string" then . else keys[0] end) ==
|
||||
["attach_workspace", "install_java", "restore_maven_cache", "restore_cache",
|
||||
"mvn_owasp", "save_cache", "restore_trivy_cache", "run_trivy", "save_trivy_cache",
|
||||
"store_artifacts", "store_artifacts"]' >/dev/null
|
||||
echo "$output" | jq -e '
|
||||
.[1].install_java.version == "<< parameters.java_version >>" and
|
||||
.[3].restore_cache.keys == ["nvd-cache-v1-{{ .Branch }}", "nvd-cache-v1-"] and
|
||||
.[5].save_cache.paths == ["~/.dependency-check"] and
|
||||
.[9].store_artifacts.path == "target/dependency-check-report.html" and
|
||||
.[10].store_artifacts.path == "/tmp/trivy-results.json"' >/dev/null
|
||||
}
|
||||
|
||||
@test "OWASP remains enabled by default" {
|
||||
[ "$(yq '.parameters.owasp.default' "$JOB")" = true ]
|
||||
}
|
||||
@@ -0,0 +1,182 @@
|
||||
#!/usr/bin/env bash
|
||||
# Tests for allocate_version.sh: atomic release version reservation via git tags.
|
||||
#
|
||||
# The GitHub refs API is replaced by a fake backed by a directory. Creating a
|
||||
# ref uses a hard link from a fully-written temp file, which is atomic and fails
|
||||
# if the target exists -- the same create-if-absent semantics GitHub provides
|
||||
# for POST /git/refs (422 "Reference already exists").
|
||||
set -euo pipefail
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
PROJECT_DIR="$(cd "${SCRIPT_DIR}/.." && pwd)"
|
||||
SCRIPT="${PROJECT_DIR}/src/scripts/allocate_version.sh"
|
||||
PASS=0
|
||||
FAIL=0
|
||||
|
||||
WORK=$(mktemp -d)
|
||||
trap 'rm -rf "${WORK}"' EXIT
|
||||
|
||||
pass() { echo "PASS"; PASS=$((PASS + 1)); }
|
||||
fail() { echo "FAIL${1:+ ($1)}"; FAIL=$((FAIL + 1)); }
|
||||
|
||||
# Fake GitHub API: gh_api METHOD PATH [BODY]; state lives in ${FAKE_GH_DIR}/tags/<tag>
|
||||
FAKE_API="${WORK}/fake_gh_api.sh"
|
||||
cat > "${FAKE_API}" <<'EOF'
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
method="$1" path="$2" body="${3:-}"
|
||||
dir="${FAKE_GH_DIR:?}/tags"
|
||||
mkdir -p "${dir}"
|
||||
if [[ -n "${FAKE_GH_FAIL:-}" ]]; then
|
||||
echo '{"message":"Server Error"}'
|
||||
exit 1
|
||||
fi
|
||||
case "${method} ${path}" in
|
||||
"POST git/refs")
|
||||
tag=$(echo "${body}" | jq -r '.ref' | sed 's#^refs/tags/##')
|
||||
sha=$(echo "${body}" | jq -r '.sha')
|
||||
# Widen the race window so concurrent allocators genuinely interleave.
|
||||
sleep "0.0$((RANDOM % 9))"
|
||||
tmp=$(mktemp "${dir}/.tmp.XXXXXX")
|
||||
echo "${sha}" > "${tmp}"
|
||||
if ln "${tmp}" "${dir}/${tag}" 2>/dev/null; then
|
||||
rm -f "${tmp}"
|
||||
echo "{\"ref\":\"refs/tags/${tag}\",\"object\":{\"type\":\"commit\",\"sha\":\"${sha}\"}}"
|
||||
exit 0
|
||||
fi
|
||||
rm -f "${tmp}"
|
||||
echo '{"message":"Reference already exists"}'
|
||||
exit 3
|
||||
;;
|
||||
"GET git/ref/tags/"*)
|
||||
tag="${path#git/ref/tags/}"
|
||||
if [[ -f "${dir}/${tag}" ]]; then
|
||||
sha=$(cat "${dir}/${tag}")
|
||||
if [[ -f "${dir}/${tag}.annotated" ]]; then
|
||||
echo "{\"object\":{\"type\":\"tag\",\"sha\":\"tagobj-${sha}\"}}"
|
||||
else
|
||||
echo "{\"object\":{\"type\":\"commit\",\"sha\":\"${sha}\"}}"
|
||||
fi
|
||||
exit 0
|
||||
fi
|
||||
echo '{"message":"Not Found"}'
|
||||
exit 2
|
||||
;;
|
||||
"GET git/tags/tagobj-"*)
|
||||
echo "{\"object\":{\"type\":\"commit\",\"sha\":\"${path#git/tags/tagobj-}\"}}"
|
||||
exit 0
|
||||
;;
|
||||
esac
|
||||
echo "unexpected call: ${method} ${path}" >&2
|
||||
exit 1
|
||||
EOF
|
||||
chmod +x "${FAKE_API}"
|
||||
|
||||
# run_alloc STATE_DIR SHA VERSION [BRANCH] -> prints allocated PROJECT_VERSION (from BASH_ENV) or "-"
|
||||
run_alloc() {
|
||||
local state="$1" sha="$2" version="$3" branch="${4:-main}"
|
||||
local env_file
|
||||
env_file=$(mktemp "${WORK}/env.XXXXXX")
|
||||
FAKE_GH_DIR="${state}" MUNITOR_GH_API_CMD="${FAKE_API}" BASH_ENV="${env_file}" \
|
||||
PROJECT_VERSION="${version}" CIRCLE_BRANCH="${branch}" CIRCLE_SHA1="${sha}" \
|
||||
CIRCLE_PROJECT_USERNAME="KofTwentyTwo" CIRCLE_PROJECT_REPONAME="Example" \
|
||||
bash "${SCRIPT}" > "${env_file}.log" 2>&1 || { echo "ERR"; return 0; }
|
||||
local v
|
||||
v=$(grep "^export PROJECT_VERSION=" "${env_file}" | tail -1 | sed "s/^export PROJECT_VERSION='\(.*\)'$/\1/")
|
||||
echo "${v:--}"
|
||||
}
|
||||
|
||||
echo "=== allocate_version.sh Tests ==="
|
||||
|
||||
echo -n " TEST: reserves the computed version when free... "
|
||||
S="${WORK}/s1"
|
||||
got=$(run_alloc "${S}" sha-a 0.1.24)
|
||||
if [[ "${got}" == "0.1.24" && "$(cat "${S}/tags/v0.1.24")" == "sha-a" ]]; then pass; else fail "got ${got}"; fi
|
||||
|
||||
echo -n " TEST: rerun on the same commit reuses its version... "
|
||||
got=$(run_alloc "${S}" sha-a 0.1.24)
|
||||
if [[ "${got}" == "0.1.24" && "$(cat "${S}/tags/v0.1.24")" == "sha-a" ]]; then pass; else fail "got ${got}"; fi
|
||||
|
||||
echo -n " TEST: collision with another commit takes the next patch (the PR #65/#66 case)... "
|
||||
got=$(run_alloc "${S}" sha-b 0.1.24)
|
||||
if [[ "${got}" == "0.1.25" && "$(cat "${S}/tags/v0.1.24")" == "sha-a" && "$(cat "${S}/tags/v0.1.25")" == "sha-b" ]]; then
|
||||
pass
|
||||
else
|
||||
fail "got ${got}"
|
||||
fi
|
||||
|
||||
echo -n " TEST: existing tags are never moved... "
|
||||
if [[ "$(cat "${S}/tags/v0.1.24")" == "sha-a" ]]; then pass; else fail "v0.1.24 moved"; fi
|
||||
|
||||
echo -n " TEST: annotated tag on the same commit is recognized as a rerun... "
|
||||
S2="${WORK}/s2"; mkdir -p "${S2}/tags"; echo sha-x > "${S2}/tags/v1.0.0"; touch "${S2}/tags/v1.0.0.annotated"
|
||||
got=$(run_alloc "${S2}" sha-x 1.0.0)
|
||||
if [[ "${got}" == "1.0.0" ]]; then pass; else fail "got ${got}"; fi
|
||||
|
||||
echo -n " TEST: skips several already-taken versions... "
|
||||
S3="${WORK}/s3"; mkdir -p "${S3}/tags"
|
||||
for p in 5 6 7; do echo "other-${p}" > "${S3}/tags/v2.3.${p}"; done
|
||||
got=$(run_alloc "${S3}" sha-y 2.3.5)
|
||||
if [[ "${got}" == "2.3.8" ]]; then pass; else fail "got ${got}"; fi
|
||||
|
||||
echo -n " TEST: non-main branches are not reserved... "
|
||||
S4="${WORK}/s4"
|
||||
got=$(run_alloc "${S4}" sha-z 0.2.0-SNAPSHOT.abc develop)
|
||||
if [[ "${got}" == "-" && ! -e "${S4}/tags/v0.2.0-SNAPSHOT.abc" ]]; then pass; else fail "got ${got}"; fi
|
||||
|
||||
echo -n " TEST: pre-release versions on main are not reserved... "
|
||||
got=$(run_alloc "${S4}" sha-z 0.2.0-RC.1 main)
|
||||
if [[ "${got}" == "-" ]]; then pass; else fail "got ${got}"; fi
|
||||
|
||||
echo -n " TEST: API failure fails the build instead of guessing... "
|
||||
S5="${WORK}/s5"
|
||||
got=$(FAKE_GH_FAIL=1 run_alloc "${S5}" sha-q 0.1.0)
|
||||
if [[ "${got}" == "ERR" ]]; then pass; else fail "got ${got}"; fi
|
||||
|
||||
echo -n " TEST: missing GITHUB_TOKEN fails on main... "
|
||||
rc=0
|
||||
env -u GITHUB_TOKEN BASH_ENV=/dev/null PROJECT_VERSION=0.1.0 CIRCLE_BRANCH=main CIRCLE_SHA1=abc \
|
||||
CIRCLE_PROJECT_USERNAME=o CIRCLE_PROJECT_REPONAME=r bash "${SCRIPT}" >/dev/null 2>&1 || rc=$?
|
||||
if [[ ${rc} -ne 0 ]]; then pass; else fail "expected failure"; fi
|
||||
|
||||
# Concurrent simulation: N pipelines for N different merge commits all computed
|
||||
# the same version from GitVersion and race to reserve it.
|
||||
N=8
|
||||
echo -n " TEST: ${N} concurrent pipelines get ${N} distinct, contiguous versions... "
|
||||
SC="${WORK}/concurrent"; mkdir -p "${SC}/tags"
|
||||
pids=()
|
||||
for i in $(seq 1 "${N}"); do
|
||||
( run_alloc "${SC}" "merge-${i}" 0.1.24 > "${WORK}/result.${i}" ) &
|
||||
pids+=($!)
|
||||
done
|
||||
for pid in "${pids[@]}"; do wait "${pid}"; done
|
||||
|
||||
results=$(for i in $(seq 1 "${N}"); do cat "${WORK}/result.${i}"; done | sort -t. -k3,3n)
|
||||
expected=$(for p in $(seq 24 $((24 + N - 1))); do echo "0.1.${p}"; done)
|
||||
ok=true
|
||||
[[ "${results}" == "${expected}" ]] || ok=false
|
||||
# Every tag maps to exactly the commit that claims that version.
|
||||
for i in $(seq 1 "${N}"); do
|
||||
v=$(cat "${WORK}/result.${i}")
|
||||
[[ -f "${SC}/tags/v${v}" && "$(cat "${SC}/tags/v${v}")" == "merge-${i}" ]] || ok=false
|
||||
done
|
||||
if ${ok}; then pass; else fail "results: $(echo "${results}" | tr '\n' ' ')"; fi
|
||||
|
||||
echo -n " TEST: concurrent reruns of the same commits keep their versions... "
|
||||
pids=()
|
||||
for i in $(seq 1 "${N}"); do
|
||||
( run_alloc "${SC}" "merge-${i}" 0.1.24 > "${WORK}/rerun.${i}" ) &
|
||||
pids+=($!)
|
||||
done
|
||||
for pid in "${pids[@]}"; do wait "${pid}"; done
|
||||
ok=true
|
||||
for i in $(seq 1 "${N}"); do
|
||||
[[ "$(cat "${WORK}/rerun.${i}")" == "$(cat "${WORK}/result.${i}")" ]] || ok=false
|
||||
done
|
||||
tag_count=$(find "${SC}/tags" -name 'v*' | wc -l | tr -d ' ')
|
||||
[[ "${tag_count}" == "${N}" ]] || ok=false
|
||||
if ${ok}; then pass; else fail "reruns changed versions or created extra tags (${tag_count})"; fi
|
||||
|
||||
echo ""
|
||||
echo "Results: ${PASS} passed, ${FAIL} failed"
|
||||
[[ ${FAIL} -eq 0 ]]
|
||||
@@ -0,0 +1,114 @@
|
||||
#!/usr/bin/env bash
|
||||
# Exercise the real generator and validate its complete dependency graph.
|
||||
set -euo pipefail
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
GENERATOR="${SCRIPT_DIR}/../src/scripts/generate_config.sh"
|
||||
WORK=$(mktemp -d)
|
||||
trap 'rm -rf -- "$WORK"' EXIT
|
||||
export CIRCLE_BRANCH=main CIRCLE_TAG=""
|
||||
export GIT_CONFIG_NOSYSTEM=1 GIT_CONFIG_GLOBAL=/dev/null
|
||||
export GIT_AUTHOR_NAME=Fixture GIT_AUTHOR_EMAIL=fixture@example.invalid
|
||||
export GIT_COMMITTER_NAME=Fixture GIT_COMMITTER_EMAIL=fixture@example.invalid
|
||||
PASS=0
|
||||
|
||||
render_check() {
|
||||
local config="$1" mode="$2" label="$3" generator="${4:-$GENERATOR}"
|
||||
MUNITOR_CONFIG="$config" MUNITOR_OUTPUT_FILE="${WORK}/generated.yml" \
|
||||
bash "$generator" >"${WORK}/generator.log" 2>&1 || { cat "${WORK}/generator.log"; return 1; }
|
||||
yq -o=json '.' "${WORK}/generated.yml" >"${WORK}/generated.json"
|
||||
python3 - "${WORK}/generated.json" "$mode" <<'PY'
|
||||
import json, sys
|
||||
data = json.load(open(sys.argv[1], encoding='utf-8'))
|
||||
workflows = data['workflows']
|
||||
assert workflows, 'no generated workflows'
|
||||
if sys.argv[2] == 'docs':
|
||||
assert set(workflows) == {'docs-checks'}, workflows.keys()
|
||||
else:
|
||||
assert {'pr-checks', 'production', 'release-candidate'} <= set(workflows), workflows.keys()
|
||||
assert 'docs-checks' not in workflows
|
||||
for workflow, body in workflows.items():
|
||||
jobs = {}
|
||||
filters = {}
|
||||
for invocation in body['jobs']:
|
||||
if isinstance(invocation, str):
|
||||
command, options = invocation, {}
|
||||
else:
|
||||
assert len(invocation) == 1
|
||||
command, options = next(iter(invocation.items()))
|
||||
options = options or {}
|
||||
name = options.get('name', command)
|
||||
assert name not in jobs, (workflow, 'duplicate job', name)
|
||||
jobs[name] = (command, options.get('requires', []))
|
||||
filters[name] = options.get('filters', {})
|
||||
assert 'ci-ready' in jobs, (workflow, 'missing ci-ready')
|
||||
assert jobs['ci-ready'][0] == 'munitor/ci_ready'
|
||||
for name, job_filters in filters.items():
|
||||
assert job_filters == filters['ci-ready'], (workflow, name, 'filters differ from ci-ready', job_filters, filters['ci-ready'])
|
||||
for name, (_, dependencies) in jobs.items():
|
||||
assert isinstance(dependencies, list)
|
||||
assert set(dependencies) <= jobs.keys(), (workflow, name, 'dangling dependency', dependencies)
|
||||
def closure(name, ancestors):
|
||||
assert name not in ancestors, (workflow, 'dependency cycle', name)
|
||||
reached = set()
|
||||
for dependency in jobs[name][1]:
|
||||
reached.add(dependency)
|
||||
reached.update(closure(dependency, ancestors | {name}))
|
||||
return reached
|
||||
assert closure('ci-ready', set()) == set(jobs) - {'ci-ready'}, (workflow, 'ungated job')
|
||||
if sys.argv[2] == 'docs':
|
||||
assert set(jobs) == {'secrets-scan', 'ci-ready'}
|
||||
assert jobs['secrets-scan'][0] == 'munitor/secrets_scan'
|
||||
else:
|
||||
assert any(command == 'munitor/mvn_build_and_test' for command, _ in jobs.values()), workflow
|
||||
print('validated', len(workflows), 'workflow graphs')
|
||||
PY
|
||||
printf 'PASS: %s\n' "$label"
|
||||
PASS=$((PASS + 1))
|
||||
}
|
||||
|
||||
render_check "${SCRIPT_DIR}/fixtures/java-webapp.munitor.yml" full 'full Java fixture gates every job'
|
||||
render_check "${SCRIPT_DIR}/fixtures/java-webapp-minimal.munitor.yml" full 'minimal Java fixture has no dangling optional dependencies'
|
||||
# Disable all optional publication/scanner additions as well as e2e/SBOM.
|
||||
cat >"${WORK}/minimal.yml" <<'YAML'
|
||||
pipeline: java-webapp
|
||||
orb_version: dev:snapshot
|
||||
image_name: fixture
|
||||
java_version: "21"
|
||||
docker:
|
||||
registry: ghcr.io/example
|
||||
e2e: false
|
||||
sbom: false
|
||||
owasp: false
|
||||
sast: false
|
||||
YAML
|
||||
render_check "${WORK}/minimal.yml" full 'optional jobs absent without dangling gates'
|
||||
|
||||
git init -q -b main "${WORK}/repo"
|
||||
cd "${WORK}/repo"
|
||||
cp "${WORK}/minimal.yml" .munitor.yml
|
||||
printf '\nci:\n docs_only: true\n' >> .munitor.yml
|
||||
printf 'original\n' > README.md
|
||||
mkdir src
|
||||
printf 'original\n' > src/App.java
|
||||
git add .
|
||||
git -c commit.gpgsign=false commit -qm baseline
|
||||
printf 'documentation\n' >> README.md
|
||||
git add README.md
|
||||
git -c commit.gpgsign=false commit -qm documentation
|
||||
render_check "$PWD/.munitor.yml" docs 'opted-in main README-only change retains only secrets and ci-ready'
|
||||
# The shipped script stages /tmp/munitor helpers; invoke sequentially, never in parallel.
|
||||
render_check "$PWD/.munitor.yml" docs 'packed generator retains docs-only secrets and ci-ready' \
|
||||
"${SCRIPT_DIR}/../src/scripts/packed_generate_config.sh"
|
||||
|
||||
# Opt-out must retain the full workflow even with the same docs-only Git diff.
|
||||
yq '.ci.docs_only = false' .munitor.yml >"${WORK}/optout.yml"
|
||||
render_check "${WORK}/optout.yml" full 'docs change with opt-out still builds'
|
||||
printf 'source change\n' >> src/App.java
|
||||
git add src/App.java
|
||||
git -c commit.gpgsign=false commit -qm source
|
||||
render_check "$PWD/.munitor.yml" full 'source change still builds'
|
||||
printf '\n# configuration change\n' >> .munitor.yml
|
||||
git add .munitor.yml
|
||||
git -c commit.gpgsign=false commit -qm configuration
|
||||
render_check "$PWD/.munitor.yml" full 'configuration change still builds'
|
||||
printf '%s ci-ready generator checks passed\n' "$PASS"
|
||||
@@ -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
|
||||
@@ -0,0 +1,139 @@
|
||||
#!/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 refreshes both Dockerfile forms and scans 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 --pull --no-cache -f ' "${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 refreshes both Dockerfile forms and scans before publishing tags... "
|
||||
: > "${CALL_LOG}"
|
||||
unset PUSH_IMAGES
|
||||
if bash "${SRC_SCRIPTS}/build_push_supplemental.sh" > "${WORK}/supplemental.log" &&
|
||||
[[ $(grep -c '^docker build --pull --no-cache -f ' "${CALL_LOG}") == 2 ]] &&
|
||||
[[ $(grep -c '^docker push ' "${CALL_LOG}") == 4 ]] &&
|
||||
awk '
|
||||
/^trivy image / { image=$NF; sub(/:[^:]+$/, "", image); scanned[image]=1 }
|
||||
/^docker push / { image=$NF; sub(/:[^:]+$/, "", image); if (!scanned[image]) exit 1 }
|
||||
' "${CALL_LOG}"; 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 ]]
|
||||
@@ -0,0 +1,171 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
# shellcheck source=src/scripts/docs_only.sh
|
||||
source "${SCRIPT_DIR}/../src/scripts/docs_only.sh"
|
||||
WORK=$(mktemp -d)
|
||||
trap 'rm -rf -- "$WORK"' EXIT
|
||||
export GIT_CONFIG_NOSYSTEM=1 GIT_CONFIG_GLOBAL=/dev/null
|
||||
export GIT_AUTHOR_NAME=Fixture GIT_AUTHOR_EMAIL=fixture@example.invalid
|
||||
export GIT_COMMITTER_NAME=Fixture GIT_COMMITTER_EMAIL=fixture@example.invalid
|
||||
export GIT_TERMINAL_PROMPT=0
|
||||
export CIRCLE_BRANCH=feature/docs CIRCLE_TAG=""
|
||||
PASS=0
|
||||
git init -q --bare "${WORK}/origin.git"
|
||||
git init -q -b main "${WORK}/seed"
|
||||
cd "${WORK}/seed"
|
||||
mkdir -p src docs/nested
|
||||
printf 'base\n' > README.md
|
||||
printf 'base\n' > src/app.java
|
||||
printf 'base\n' > docs/old.md
|
||||
git add .
|
||||
git -c commit.gpgsign=false commit -qm base
|
||||
git remote add origin "${WORK}/origin.git"
|
||||
git push -q origin main
|
||||
git --git-dir="${WORK}/origin.git" symbolic-ref HEAD refs/heads/main
|
||||
|
||||
fresh() {
|
||||
local repo="${WORK}/case-${PASS}"
|
||||
git clone -q "${WORK}/origin.git" "$repo"
|
||||
cd "$repo"
|
||||
git switch -qc feature/docs
|
||||
export CIRCLE_BRANCH=feature/docs CIRCLE_TAG=""
|
||||
}
|
||||
|
||||
commit() {
|
||||
git add -A
|
||||
git -c commit.gpgsign=false commit -qm change
|
||||
}
|
||||
|
||||
check() {
|
||||
local expected="$1" name="$2" status=0 output
|
||||
output=$(munitor_docs_only 2>&1) || status=$?
|
||||
if [[ "$status" -ne "$expected" || -n "$output" ]]; then
|
||||
printf 'FAIL: %s (status %s, expected %s; output must be empty)\n' "$name" "$status" "$expected"
|
||||
exit 1
|
||||
fi
|
||||
printf 'PASS: %s\n' "$name"
|
||||
PASS=$((PASS + 1))
|
||||
}
|
||||
|
||||
fresh
|
||||
printf 'docs\n' >> README.md
|
||||
mkdir -p docs/nested
|
||||
printf 'docs\n' > 'docs/nested/space name.md'
|
||||
printf 'docs\n' > $'docs/nested/line\nbreak.md'
|
||||
printf 'docs\n' > CHANGELOG.md
|
||||
printf 'docs\n' > CONTRIBUTING.md
|
||||
commit
|
||||
check 0 'feature docs with nested, spaced and newline paths'
|
||||
CIRCLE_BRANCH=hotfix/docs check 0 'hotfix docs'
|
||||
for branch in develop staging release/1.0 unknown ''; do
|
||||
CIRCLE_BRANCH="$branch" check 1 "ineligible branch: $branch"
|
||||
done
|
||||
CIRCLE_TAG=v1.0 check 1 'tag cannot skip release'
|
||||
|
||||
fresh
|
||||
check 1 'empty branch diff'
|
||||
printf 'code\n' >> src/app.java
|
||||
commit
|
||||
printf 'docs\n' >> README.md
|
||||
commit
|
||||
check 1 'earlier code commit followed by latest docs commit'
|
||||
|
||||
fresh
|
||||
printf 'code\n' >> src/app.java
|
||||
printf 'docs\n' >> README.md
|
||||
commit
|
||||
check 1 'mixed source and docs'
|
||||
|
||||
fresh
|
||||
git mv src/app.java docs/disguised.md
|
||||
commit
|
||||
check 1 'code renamed into allowed docs path'
|
||||
|
||||
fresh
|
||||
git mv docs/old.md docs/new.md
|
||||
commit
|
||||
check 0 'docs-only rename'
|
||||
|
||||
fresh
|
||||
git rm -q src/app.java
|
||||
commit
|
||||
check 1 'deleted code'
|
||||
|
||||
fresh
|
||||
git rm -q docs/old.md
|
||||
commit
|
||||
check 0 'deleted documentation'
|
||||
|
||||
for path in pom.xml .circleci/config.yml docs/image.png docs/example.java readme.md nested/README.md; do
|
||||
fresh
|
||||
mkdir -p "$(dirname "$path")"
|
||||
printf 'not docs\n' > "$path"
|
||||
commit
|
||||
check 1 "non-allowlisted path: $path"
|
||||
done
|
||||
|
||||
fresh
|
||||
printf 'docs\n' >> README.md
|
||||
commit
|
||||
git remote remove origin
|
||||
check 1 'missing origin fails closed'
|
||||
|
||||
fresh
|
||||
printf 'docs\n' >> README.md
|
||||
commit
|
||||
git remote set-url origin "${WORK}/missing.git"
|
||||
check 1 'fetch failure fails closed despite cached origin/main'
|
||||
|
||||
fresh
|
||||
printf 'docs\n' >> README.md
|
||||
commit
|
||||
# Advance origin main after this checkout; an old tracking ref is not enough.
|
||||
cd "${WORK}/seed"
|
||||
printf 'main\n' >> README.md
|
||||
commit
|
||||
git push -q origin main
|
||||
latest=$(git rev-parse HEAD)
|
||||
cd "${WORK}/case-${PASS}"
|
||||
check 0 'fetch updated main then compare merge-base'
|
||||
[[ "$(git rev-parse origin/main)" == "$latest" ]]
|
||||
|
||||
fresh
|
||||
git switch -q main
|
||||
printf 'docs\n' >> README.md
|
||||
commit
|
||||
CIRCLE_BRANCH=main check 0 'main docs commit compares first parent'
|
||||
printf 'code\n' >> src/app.java
|
||||
commit
|
||||
CIRCLE_BRANCH=main check 1 'main source commit'
|
||||
|
||||
fresh
|
||||
printf 'docs\n' >> README.md
|
||||
commit
|
||||
git switch -q main
|
||||
git -c commit.gpgsign=false merge --no-ff -qm 'docs merge' feature/docs
|
||||
CIRCLE_BRANCH=main check 0 'main docs merge uses first parent'
|
||||
|
||||
fresh
|
||||
printf 'code\n' >> src/app.java
|
||||
commit
|
||||
printf 'docs\n' >> README.md
|
||||
commit
|
||||
git switch -q main
|
||||
git -c commit.gpgsign=false merge --no-ff -qm 'mixed merge' feature/docs
|
||||
CIRCLE_BRANCH=main check 1 'main merge includes whole source change'
|
||||
|
||||
git init -q -b main "${WORK}/root-only"
|
||||
cd "${WORK}/root-only"
|
||||
printf 'docs\n' > README.md
|
||||
commit
|
||||
CIRCLE_BRANCH=main check 1 'root commit has no parent'
|
||||
git remote add origin "${WORK}/origin.git"
|
||||
CIRCLE_BRANCH=feature/docs check 1 'unrelated history has no merge base'
|
||||
|
||||
git clone -q --depth=1 "file://${WORK}/origin.git" "${WORK}/shallow"
|
||||
cd "${WORK}/shallow"
|
||||
printf 'docs\n' >> README.md
|
||||
commit
|
||||
CIRCLE_BRANCH=main check 1 'shallow history fails closed'
|
||||
printf '%s docs-only checks passed\n' "$PASS"
|
||||
@@ -8,8 +8,8 @@ PASS=0
|
||||
FAIL=0
|
||||
|
||||
GENERATE_SCRIPT="${PROJECT_DIR}/src/scripts/generate_config.sh"
|
||||
GENERATED_CONFIG=$(mktemp)
|
||||
trap 'rm -f "${GENERATED_CONFIG}"' EXIT
|
||||
GENERATED_DIR=$(mktemp -d)
|
||||
trap 'rm -rf "${GENERATED_DIR}"' EXIT
|
||||
|
||||
# --------------------------------------------------------------------------
|
||||
# Helper: render a fixture by calling the real generate_config.sh script
|
||||
@@ -17,11 +17,15 @@ trap 'rm -f "${GENERATED_CONFIG}"' EXIT
|
||||
# --------------------------------------------------------------------------
|
||||
render_fixture() {
|
||||
local fixture="$1"
|
||||
local generated_config="${GENERATED_DIR}/$(basename "${fixture}")"
|
||||
|
||||
# Run the real script; capture stderr for diagnostics, return generated YAML
|
||||
MUNITOR_CONFIG="${fixture}" MUNITOR_OUTPUT_FILE="${GENERATED_CONFIG}" \
|
||||
bash "${GENERATE_SCRIPT}" >/dev/null 2>&1
|
||||
cat "${GENERATED_CONFIG}"
|
||||
# Fixtures are immutable and uniquely named: render once, assert many times.
|
||||
if [[ ! -f "${generated_config}" ]]; then
|
||||
MUNITOR_CONFIG="${fixture}" MUNITOR_OUTPUT_FILE="${generated_config}.tmp" \
|
||||
bash "${GENERATE_SCRIPT}" >/dev/null 2>&1 || return 1
|
||||
mv "${generated_config}.tmp" "${generated_config}"
|
||||
fi
|
||||
cat "${generated_config}"
|
||||
}
|
||||
|
||||
run_test() {
|
||||
@@ -157,6 +161,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" \
|
||||
@@ -642,17 +676,17 @@ run_negative_context_test "java-webapp: production has no security-scan" \
|
||||
"security_scan"
|
||||
|
||||
# java-webapp: release-candidate has quality gates
|
||||
run_context_test "java-webapp: release-candidate has code-quality" \
|
||||
run_context_test "java-webapp: release-candidate enables quality checks" \
|
||||
"${FIXTURES_DIR}/java-webapp.munitor.yml" \
|
||||
"release-candidate:" \
|
||||
"production:" \
|
||||
"code-quality"
|
||||
"quality_checks: true"
|
||||
|
||||
run_context_test "java-webapp: release-candidate has coverage" \
|
||||
run_context_test "java-webapp: release-candidate forwards coverage threshold" \
|
||||
"${FIXTURES_DIR}/java-webapp.munitor.yml" \
|
||||
"release-candidate:" \
|
||||
"production:" \
|
||||
"mvn_coverage"
|
||||
'min_instruction: "80"'
|
||||
|
||||
run_context_test "java-webapp: release-candidate has security-scan" \
|
||||
"${FIXTURES_DIR}/java-webapp.munitor.yml" \
|
||||
|
||||
@@ -0,0 +1,54 @@
|
||||
#!/usr/bin/env bash
|
||||
# Verify deployment health configuration reaches source and shipped generators.
|
||||
set -euo pipefail
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
WORK=$(mktemp -d)
|
||||
trap 'rm -rf -- "$WORK"' EXIT
|
||||
export CIRCLE_BRANCH=develop CIRCLE_TAG=""
|
||||
cp "${SCRIPT_DIR}/fixtures/java-webapp.munitor.yml" "${WORK}/defaults.yml"
|
||||
cp "${WORK}/defaults.yml" "${WORK}/configured.yml"
|
||||
cat >>"${WORK}/configured.yml" <<'YAML'
|
||||
health:
|
||||
path: /health/ready
|
||||
port: 8181
|
||||
db: "true"
|
||||
migrations: migrations
|
||||
smoke_paths:
|
||||
- /api/templates
|
||||
- /api/version
|
||||
YAML
|
||||
|
||||
# Packed helpers share /tmp/munitor; run these generators sequentially.
|
||||
for generator in generate_config.sh packed_generate_config.sh; do
|
||||
for mode in configured defaults; do
|
||||
MUNITOR_CONFIG="${WORK}/${mode}.yml" MUNITOR_OUTPUT_FILE="${WORK}/generated.yml" \
|
||||
bash "${SCRIPT_DIR}/../src/scripts/${generator}" >"${WORK}/generator.log" 2>&1 || {
|
||||
cat "${WORK}/generator.log"
|
||||
exit 1
|
||||
}
|
||||
yq -o=json '.' "${WORK}/generated.yml" >"${WORK}/generated.json"
|
||||
python3 - "${WORK}/generated.json" "$mode" <<'PY'
|
||||
import json
|
||||
import sys
|
||||
|
||||
with open(sys.argv[1], encoding='utf-8') as source:
|
||||
workflows = json.load(source)['workflows']
|
||||
expected = {'health_migrations': '', 'health_smoke_paths': ''}
|
||||
if sys.argv[2] == 'configured':
|
||||
expected = {
|
||||
'health_path': '/health/ready',
|
||||
'health_port': '8181',
|
||||
'health_db': 'true',
|
||||
'health_migrations': 'migrations',
|
||||
'health_smoke_paths': '/api/templates,/api/version',
|
||||
}
|
||||
for workflow in ('develop', 'staging', 'production', 'release-candidate'):
|
||||
jobs = [job['munitor/docker_build_push'] for job in workflows[workflow]['jobs']
|
||||
if isinstance(job, dict) and 'munitor/docker_build_push' in job]
|
||||
assert len(jobs) == 1, (workflow, 'expected one Docker build job', jobs)
|
||||
for parameter, value in expected.items():
|
||||
assert jobs[0].get(parameter) == value, (workflow, parameter, jobs[0].get(parameter), value)
|
||||
PY
|
||||
printf 'PASS: %s %s deployment health parameters\n' "$generator" "$mode"
|
||||
done
|
||||
done
|
||||
@@ -0,0 +1,5 @@
|
||||
#!/usr/bin/env bash
|
||||
# Protect required CI contexts and publication gates when consolidating workflows.
|
||||
set -euo pipefail
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
yq -o=json '.' "${SCRIPT_DIR}/../.circleci/config.yml" | node "${SCRIPT_DIR}/own_ci_regression.cjs"
|
||||
@@ -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 ]]
|
||||
@@ -0,0 +1,93 @@
|
||||
#!/usr/bin/env bash
|
||||
# The filesystem Trivy scan and the Semgrep scan must fail when they find
|
||||
# something, and Semgrep must fail when it ran no rules. Local tool stubs; no
|
||||
# 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}/home/bin" "${WORK}/bin" "${WORK}/repo"
|
||||
cat > "${WORK}/home/bin/trivy" <<'EOF'
|
||||
#!/usr/bin/env bash
|
||||
printf 'trivy %s\n' "$*" >> "${CALL_LOG}"
|
||||
# Real trivy exits 0 on findings unless --exit-code is given.
|
||||
for a in "$@"; do [[ "$a" == --exit-code ]] && exit "${SCAN_EXIT:-0}"; done
|
||||
exit 0
|
||||
EOF
|
||||
cat > "${WORK}/bin/semgrep" <<'EOF'
|
||||
#!/usr/bin/env bash
|
||||
printf '%s\n' "${SEMGREP_FAKE_REPORT}" > "${SEMGREP_REPORT}"
|
||||
exit "${SEMGREP_FAKE_EXIT:-0}"
|
||||
EOF
|
||||
chmod +x "${WORK}/home/bin/trivy" "${WORK}/bin/semgrep"
|
||||
export PATH="${WORK}/bin:${PATH}" HOME="${WORK}/home" CALL_LOG="${WORK}/calls"
|
||||
export SEMGREP_REPORT="${WORK}/semgrep.json"
|
||||
cd "${WORK}/repo"
|
||||
|
||||
echo "=== Scan gate tests ==="
|
||||
echo -n " TEST: trivy fs fails the job on HIGH/CRITICAL findings... "
|
||||
if ! SCAN_EXIT=1 bash "${SRC_SCRIPTS}/run_trivy.sh" > "${WORK}/trivy.log" 2>&1 &&
|
||||
grep -q '^trivy fs .*--severity HIGH,CRITICAL --exit-code 1 ' "${CALL_LOG}"; then
|
||||
pass
|
||||
else
|
||||
fail
|
||||
fi
|
||||
|
||||
echo -n " TEST: trivy fs passes a clean scan... "
|
||||
if SCAN_EXIT=0 bash "${SRC_SCRIPTS}/run_trivy.sh" > "${WORK}/trivy.log" 2>&1; then
|
||||
pass
|
||||
else
|
||||
fail
|
||||
fi
|
||||
|
||||
if command -v python3 &>/dev/null; then
|
||||
CLEAN='{"results":[],"errors":[],"paths":{"scanned":["a.java","b.java"]}}'
|
||||
FOUND='{"results":[{"check_id":"java.lang.security.x","path":"a.java","start":{"line":3}}],"errors":[],"paths":{"scanned":["a.java"]}}'
|
||||
NORULES='{"results":[],"errors":[{"type":"InvalidRuleSchemaError","message":"invalid configuration file found (1 configs were invalid)"}],"paths":{"scanned":[]}}'
|
||||
|
||||
echo -n " TEST: semgrep passes a clean scan... "
|
||||
if SEMGREP_FAKE_REPORT="${CLEAN}" bash "${SRC_SCRIPTS}/run_semgrep.sh" > "${WORK}/sg.log" 2>&1 &&
|
||||
grep -q '2 files scanned, 0 findings' "${WORK}/sg.log"; then
|
||||
pass
|
||||
else
|
||||
fail
|
||||
fi
|
||||
|
||||
echo -n " TEST: semgrep fails on findings and lists them... "
|
||||
if ! SEMGREP_FAKE_REPORT="${FOUND}" SEMGREP_FAKE_EXIT=1 bash "${SRC_SCRIPTS}/run_semgrep.sh" > "${WORK}/sg.log" 2>&1 &&
|
||||
grep -q 'java.lang.security.x a.java:3' "${WORK}/sg.log"; then
|
||||
pass
|
||||
else
|
||||
fail
|
||||
fi
|
||||
|
||||
echo -n " TEST: semgrep warn-only mode passes with findings... "
|
||||
if SEMGREP_FAIL_ON_FINDINGS=false SEMGREP_FAKE_REPORT="${FOUND}" SEMGREP_FAKE_EXIT=1 \
|
||||
bash "${SRC_SCRIPTS}/run_semgrep.sh" > "${WORK}/sg.log" 2>&1; then
|
||||
pass
|
||||
else
|
||||
fail
|
||||
fi
|
||||
|
||||
echo -n " TEST: semgrep fails when no rules ran, even with exit 0 and warn-only... "
|
||||
if ! SEMGREP_FAIL_ON_FINDINGS=false SEMGREP_FAKE_REPORT="${NORULES}" SEMGREP_FAKE_EXIT=0 \
|
||||
bash "${SRC_SCRIPTS}/run_semgrep.sh" > "${WORK}/sg.log" 2>&1 &&
|
||||
grep -q 'did not run its rules' "${WORK}/sg.log"; then
|
||||
pass
|
||||
else
|
||||
fail
|
||||
fi
|
||||
else
|
||||
echo " SKIP: semgrep gate tests need python3"
|
||||
fi
|
||||
|
||||
echo ""
|
||||
echo "Results: ${PASS} passed, ${FAIL} failed"
|
||||
[[ "${FAIL}" -eq 0 ]]
|
||||
+16
-6
@@ -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)... "
|
||||
@@ -834,8 +843,8 @@ echo ""
|
||||
echo "=== Error Handling Tests ==="
|
||||
|
||||
echo -n " TEST: all scripts use set -euo pipefail... "
|
||||
# munitor_helpers.sh is a sourced library, not a standalone script -- exclude it
|
||||
SCRIPTS_WITHOUT_STRICT=$(find "${SRC_SCRIPTS}" -name "*.sh" ! -name "munitor_helpers.sh" -exec grep -L 'set -euo pipefail' {} \;)
|
||||
# Sourced libraries must not change the caller shell options.
|
||||
SCRIPTS_WITHOUT_STRICT=$(find "${SRC_SCRIPTS}" -name "*.sh" ! -name "munitor_helpers.sh" ! -name "docs_only.sh" -exec grep -L 'set -euo pipefail' {} \;)
|
||||
if [[ -z "${SCRIPTS_WITHOUT_STRICT}" ]]; then
|
||||
pass
|
||||
else
|
||||
@@ -892,8 +901,9 @@ echo "=== Munitor Helpers Integration Tests ==="
|
||||
# Scripts that are excluded from the munitor_header requirement:
|
||||
# - munitor_helpers.sh: the library itself
|
||||
# - extract_munitor_vars.sh: low-level helper, sourced by other scripts
|
||||
# - docs_only.sh: status-only predicate sourced by the generator
|
||||
# - packed_*: auto-generated, embed helpers differently
|
||||
EXCLUDED_PATTERN="munitor_helpers.sh|extract_munitor_vars.sh|packed_"
|
||||
EXCLUDED_PATTERN="munitor_helpers.sh|extract_munitor_vars.sh|docs_only.sh|packed_"
|
||||
|
||||
echo -n " TEST: all scripts source munitor_helpers... "
|
||||
MISSING_SOURCE=""
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
bats "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/security_scan_owasp.bats"
|
||||
@@ -0,0 +1,11 @@
|
||||
#!/usr/bin/env bats
|
||||
|
||||
@test "combined script and template targets run each suite once" {
|
||||
mkdir -p "$BATS_TEST_TMPDIR/tests"
|
||||
printf 'echo SCRIPT_EXECUTED\n' > "$BATS_TEST_TMPDIR/tests/test_example.sh"
|
||||
printf 'echo TEMPLATE_EXECUTED\n' > "$BATS_TEST_TMPDIR/tests/test_generate_config.sh"
|
||||
run make --no-print-directory -f "$BATS_TEST_DIRNAME/../Makefile" -C "$BATS_TEST_TMPDIR" test-scripts test-templates
|
||||
[ "$status" -eq 0 ]
|
||||
[ "$(echo "$output" | grep -c '^SCRIPT_EXECUTED$')" -eq 1 ]
|
||||
[ "$(echo "$output" | grep -c '^TEMPLATE_EXECUTED$')" -eq 1 ]
|
||||
}
|
||||
Reference in new issue
Block a user