Compare commits
52
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
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 | ||
|
|
50ae000759 | ||
|
|
b357559760 | ||
|
|
c8bbf2ef39 | ||
|
|
ae7d93328f | ||
|
|
d2e31ed001 | ||
|
|
e84008ee19 | ||
|
|
d0ce5266ee | ||
|
|
9c487cfa04 | ||
|
|
836cb20c5b |
No files matched your search
+54
-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,31 @@ tag-only: &tag-only
|
||||
tags:
|
||||
only: /^v[0-9]+\.[0-9]+\.[0-9]+$/
|
||||
|
||||
jobs:
|
||||
test:
|
||||
docker:
|
||||
- image: cimg/node:22.12
|
||||
resource_class: medium
|
||||
steps:
|
||||
- checkout
|
||||
- run:
|
||||
name: Install test tools
|
||||
command: |
|
||||
sudo apt-get update -qq
|
||||
sudo apt-get install -y -qq gettext-base shellcheck >/dev/null
|
||||
YQ_VERSION=v4.44.6
|
||||
sudo curl -fsSL -o /usr/local/bin/yq \
|
||||
"https://github.com/mikefarah/yq/releases/download/${YQ_VERSION}/yq_linux_amd64"
|
||||
sudo chmod +x /usr/local/bin/yq
|
||||
git clone -q --depth 1 --branch v1.11.1 https://github.com/bats-core/bats-core.git /tmp/bats-core
|
||||
sudo /tmp/bats-core/install.sh /usr/local
|
||||
- run:
|
||||
name: Script and template tests
|
||||
command: make test-scripts test-templates
|
||||
- run:
|
||||
name: BATS tests
|
||||
command: bats tests
|
||||
|
||||
workflows:
|
||||
lint-pack:
|
||||
jobs:
|
||||
@@ -23,36 +49,55 @@ workflows:
|
||||
- orb-tools/review:
|
||||
filters: *filters
|
||||
exclude: RC006,RC007,RC010 # RC006/RC007: private repo URL unreachable; RC010: machine executors
|
||||
- test:
|
||||
filters: *filters
|
||||
|
||||
test-deploy:
|
||||
jobs:
|
||||
- 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,2 @@
|
||||
*.sh text eol=lf
|
||||
*.bats text eol=lf
|
||||
@@ -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,12 +29,17 @@ GitHub Issues (org: KofTwentyTwo). Commit format: `feat(#N): description` or `Cl
|
||||
|
||||
- Namespace: `kof22`
|
||||
- Orb: `kof22/munitor`
|
||||
- Latest stable: `kof22/munitor@0.3.0`
|
||||
- Latest stable: `kof22/munitor@0.3.3`
|
||||
- Dev snapshot: `kof22/munitor@dev:snapshot`
|
||||
|
||||
`0.3.0` includes first-class pnpm support, repository-owned Dockerfiles for
|
||||
`node-webapp`, and consolidated Kustomize GitOps write-back with optional
|
||||
production-only behavior and bounded push-race retries.
|
||||
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). `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.8` |
|
||||
| **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.
|
||||
@@ -58,6 +66,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.8` (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.8`.
|
||||
- `.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.8"
|
||||
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 +372,7 @@ contexts:
|
||||
|
||||
**What runs:** Maven build/test, Checkstyle, SpotBugs, PMD, JaCoCo coverage, OWASP + Trivy security, Docker build/push (Dockerfile auto-generated), gitleaks. OWASP dependency check requires the `nvd` context; if omitted, the security scan runs without NVD database enrichment.
|
||||
|
||||
**Dockerfile:** Auto-generated production-ready image using Eclipse Temurin JRE Alpine. Includes non-root user (uid 1001), JVM container tuning flags, Docker HEALTHCHECK, and configurable port via `health.port`. The pre-built JAR from CI is copied directly (no redundant Maven build stage).
|
||||
**Dockerfile:** Auto-generated production-ready image using Eclipse Temurin JRE Alpine. Includes non-root user (uid 1001), JVM container tuning flags, Docker HEALTHCHECK, and configurable port via `health.port`. The pre-built JAR from CI is copied directly (no redundant Maven build stage). PR and release image builds pull the current base and bypass Docker layer caching for generated Java/Gradle runtime images, ensuring the Alpine package upgrade runs on every build. Node builds retain their layer cache. Trivy gates remain required.
|
||||
|
||||
### `terraform`
|
||||
|
||||
@@ -395,7 +490,7 @@ Each workflow runs on specific branches and includes jobs based on your config:
|
||||
|
||||
| Workflow | Branches | Jobs |
|
||||
|----------|----------|------|
|
||||
| `pr-checks` | `feature/*`, `hotfix/*` | Build, test, lint, coverage, security scans |
|
||||
| `pr-checks` | `feature/*`, `hotfix/*` | Build, test, lint, coverage, security scans, container image scan (Docker modules) |
|
||||
| `develop` | `develop` | All pr-checks + Docker build/push + CD update (dev env) |
|
||||
| `staging` | `staging` | All pr-checks + Docker build/push + CD update (staging env) |
|
||||
| `release-candidate` | `release/*` | All pr-checks + Docker, CD (staging), SonarCloud, SBOM, GitHub Release (pre-release) |
|
||||
@@ -403,11 +498,20 @@ Each workflow runs on specific branches and includes jobs based on your config:
|
||||
|
||||
For `validate-cd-repo` and `argocd-apps`, all three workflows (`pr-checks`, `develop`, `release`) run the same 5-job set: yaml-lint, kustomize-validate, kubesec-scan, kube-linter, secrets-scan.
|
||||
|
||||
For `java-webapp`, `gradle-webapp`, `node-api`, and `node-webapp`, PRs run
|
||||
`image-scan` after `build-and-test`: build the application and any supplemental
|
||||
images locally, then fail on HIGH/CRITICAL CVEs with the same Trivy policy and
|
||||
repository-root `.trivyignore` as release builds. JSON reports are saved as
|
||||
artifacts. This job does not log in, publish images, reserve tags, or update CD.
|
||||
Release candidates already scan through `docker-build-push` and do not run a
|
||||
second image scan job.
|
||||
|
||||
### Job Dependency Graph (node-api pr-checks)
|
||||
|
||||
```
|
||||
checkout
|
||||
--> build-and-test
|
||||
--> image-scan (local build + Trivy, including supplemental images)
|
||||
--> code-quality
|
||||
--> coverage
|
||||
--> security-scan
|
||||
@@ -416,7 +520,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 +580,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 +591,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 |
|
||||
@@ -569,3 +696,31 @@ Pipeline generation flow:
|
||||
5. Runs `envsubst` for variable substitution
|
||||
6. Processes `##IF_*##` / `##ENDIF_*##` conditional blocks
|
||||
7. Validates output YAML and passes to CircleCI continuation
|
||||
|
||||
## Temporary npm audit exceptions
|
||||
|
||||
The default npm audit gate remains unchanged. For a reviewed development-only HIGH
|
||||
advisory with no patched package release, a repository may commit
|
||||
`.npm-audit-exceptions.json` with schemaVersion 1 and an exceptions array. Each entry
|
||||
requires a register ID, exact GHSA advisory ID, package, version, development scope
|
||||
and UTC expiry date. See Website-Frontend's EX-0008 for the first consumer.
|
||||
|
||||
The full npm audit runs alongside a production audit. All production HIGH/CRITICAL
|
||||
and all CRITICAL findings fail. Only the named advisory and its propagated HIGH
|
||||
findings can pass, and every affected lockfile path must have dev true and the exact
|
||||
excepted version. Audit errors, unknown chains and expired exceptions fail. A new
|
||||
upstream package release also fails and requires review. Trivy remains unchanged.
|
||||
This mechanism requires npm audit report version 2 and a package-path lockfile;
|
||||
unsupported reports fail. BATS and Node are required for the audit regression tests.
|
||||
|
||||
## 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,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,82 @@
|
||||
## 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`
|
||||
promotes the bounded publishing CI fixes to main. Full make all and the feature
|
||||
CircleCI lint/pack/review/snapshot publication passed. Stable publication still
|
||||
requires the v0.3.2 tag on main and successful production orb publication.
|
||||
|
||||
EX-0008 is registered by standards PR27 (5c21fc7); Website-Frontend may select the
|
||||
new stable orb after publication. Default audit policy, Trivy and GitOps contracts
|
||||
are preserved. Website-Backend's Java installation now avoids interactive Ubuntu
|
||||
service-restart dialogs. Website consumers and production remain pending.
|
||||
|
||||
## Current status
|
||||
|
||||
The `develop` branch contains the complete `0.3.0` feature set. PR #6 added
|
||||
@@ -23,3 +100,28 @@ ArgoCD.
|
||||
Run `make all` before release. The stable tag must publish successfully through
|
||||
CircleCI before consumer repositories move from immutable development pins to
|
||||
`0.3`.
|
||||
|
||||
## Publishing release CI fixes (2026-10-10)
|
||||
|
||||
Feature branch `feature/scoped-audit-exceptions` prepares development-only npm
|
||||
exceptions with expiry, exact advisory/package/version checks, an independent
|
||||
production audit and reassessment on a new upstream release. Website-Frontend's
|
||||
proposed EX-0008 covers braces 3.0.3 through 2026-11-10. Regression tests exercise
|
||||
acceptance and rejection paths. The default audit and Trivy remain unchanged.
|
||||
|
||||
Website-Backend CI job 1135 timed out in Ubuntu's interactive needrestart prompt
|
||||
after Java installation. APT now receives noninteractive debconf and automatic
|
||||
needrestart settings; a stubbed integration test verifies both APT invocations.
|
||||
These fixes require normal feature-to-develop review and stable tag publication
|
||||
from main before website consumers can select the new version. Nothing deployed.
|
||||
|
||||
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.
|
||||
+15
-1
@@ -2,6 +2,20 @@
|
||||
|
||||
## Pending
|
||||
|
||||
- [ ] Validate and publish fast Java CI patch, canary actual cache size/timings, then adopt stable orb (docs/PLAN-lean-java-ci.md).
|
||||
|
||||
- [ ] 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.
|
||||
- [x] Publish v0.3.2 from main.
|
||||
- [ ] Publish v0.3.3 (atomic release versions); bump Website-Backend/Frontend pins to 0.3.3.
|
||||
- [ ] Bump Concilium (0.2) and makers4 (0.1) to 0.3.3 when next active.
|
||||
|
||||
- [ ] Follow makers4 and terraform-proxmox-talos on CircleCI when those repos are next active.
|
||||
- [ ] Adopt supplemental images on an appropriate consumer.
|
||||
|
||||
@@ -13,7 +27,7 @@
|
||||
- [x] Validate first-class pnpm support and merge PR #6.
|
||||
- [x] Add opt-in repository Dockerfile preservation for node-webapp (GH-7)
|
||||
- [x] Implement first-class pnpm support (GH-5, 17 commits, 219 tests)
|
||||
- [x] Rename all npm_* orb components to node_* (backward-incompatible, pre-1.0)
|
||||
- [x] Rename all `npm_*` orb components to `node_*` (backward-incompatible, pre-1.0)
|
||||
- [x] Thread package_manager through full job/command/script chain
|
||||
- [x] Add corepack activation for pnpm/yarn in install_node
|
||||
- [x] Add pnpm Dockerfile variants for node-api and node-webapp
|
||||
|
||||
@@ -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
|
||||
@@ -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[@]}"
|
||||
|
||||
@@ -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"
|
||||
ACTUAL_KEYS=$(yq 'keys | .[]' "${CONFIG_FILE}" 2>/dev/null || true)
|
||||
for key in ${ACTUAL_KEYS}; do
|
||||
if ! echo "${KNOWN_KEYS}" | grep -qw "${key}"; then
|
||||
@@ -201,7 +201,7 @@ process_conditionals() {
|
||||
cp "${input}" "${tmpfile}"
|
||||
|
||||
# Process each conditional flag
|
||||
for flag in E2E SBOM SONAR NPM_AUTH SERVICES TEST_SETUP CUSTOM_TEST COVERAGE_CMD GITHUB_RELEASE SAST CD CD_NON_PROD OWASP NVD SUPPLEMENTAL; do
|
||||
for flag in E2E SBOM SONAR NPM_AUTH SERVICES TEST_SETUP CUSTOM_TEST COVERAGE_CMD GITHUB_RELEASE SAST CD CD_NON_PROD CD_EXPLICIT_PATH OWASP NVD SUPPLEMENTAL; do
|
||||
local var_name="MUNITOR_${flag}"
|
||||
local value="${!var_name:-false}"
|
||||
local flag_file
|
||||
|
||||
@@ -269,6 +269,9 @@ DOCKERFILE
|
||||
fi
|
||||
else
|
||||
echo "Generating Dockerfile for node-webapp (node ${MUNITOR_NODE_VERSION})"
|
||||
# Single source for the runtime base. debian12 ships libssl3 3.0.18 with
|
||||
# open HIGH/CRITICAL CVEs that fail the Trivy gate; debian13 scans clean.
|
||||
NODE_WEBAPP_RUNTIME_IMAGE="gcr.io/distroless/nodejs${MUNITOR_NODE_VERSION}-debian13"
|
||||
case "${MUNITOR_PACKAGE_MANAGER}" in
|
||||
pnpm)
|
||||
cat > Dockerfile <<DOCKERFILE
|
||||
@@ -282,7 +285,7 @@ ARG GIT_COMMIT_SHA
|
||||
ENV GIT_COMMIT_SHA=\${GIT_COMMIT_SHA}
|
||||
RUN pnpm run build
|
||||
|
||||
FROM gcr.io/distroless/nodejs${MUNITOR_NODE_VERSION}-debian12 AS runner
|
||||
FROM ${NODE_WEBAPP_RUNTIME_IMAGE} AS runner
|
||||
WORKDIR /app
|
||||
ENV NODE_ENV=production
|
||||
COPY --from=builder /app/.next/standalone ./
|
||||
@@ -303,7 +306,7 @@ ARG GIT_COMMIT_SHA
|
||||
ENV GIT_COMMIT_SHA=\${GIT_COMMIT_SHA}
|
||||
RUN npm run build
|
||||
|
||||
FROM gcr.io/distroless/nodejs${MUNITOR_NODE_VERSION}-debian12 AS runner
|
||||
FROM ${NODE_WEBAPP_RUNTIME_IMAGE} AS runner
|
||||
WORKDIR /app
|
||||
ENV NODE_ENV=production
|
||||
COPY --from=builder /app/.next/standalone ./
|
||||
|
||||
@@ -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}."
|
||||
@@ -26,8 +26,9 @@ echo "deb [signed-by=/etc/apt/keyrings/adoptium.asc] \
|
||||
$(. /etc/os-release && echo "${VERSION_CODENAME}") main" \
|
||||
| sudo tee /etc/apt/sources.list.d/adoptium.list > /dev/null
|
||||
|
||||
sudo apt-get update -qq
|
||||
sudo apt-get install -y -qq "temurin-${VERSION}-jdk"
|
||||
# CI cannot answer debconf or needrestart's service-restart dialog.
|
||||
sudo env DEBIAN_FRONTEND=noninteractive NEEDRESTART_MODE=a apt-get update -qq
|
||||
sudo env DEBIAN_FRONTEND=noninteractive NEEDRESTART_MODE=a apt-get install -y -qq "temurin-${VERSION}-jdk"
|
||||
|
||||
# Set JAVA_HOME for subsequent steps
|
||||
JAVA_HOME_DIR="/usr/lib/jvm/temurin-${VERSION}-jdk-$(dpkg --print-architecture)"
|
||||
|
||||
@@ -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
|
||||
@@ -15,6 +15,106 @@ munitor_header "node_audit"
|
||||
|
||||
echo "Running npm audit..."
|
||||
|
||||
if [[ -f .npm-audit-exceptions.json ]]; then
|
||||
# The optional, repository-reviewed exception file never changes runtime policy.
|
||||
REPORT_DIR=$(mktemp -d)
|
||||
trap 'rm -rf "${REPORT_DIR}"' EXIT
|
||||
export REPORT_DIR
|
||||
npm audit --json --audit-level=high > "${REPORT_DIR}/full.json" || AUDIT_STATUS=$?
|
||||
npm audit --json --omit=dev --audit-level=high > "${REPORT_DIR}/production.json" || PRODUCTION_STATUS=$?
|
||||
export AUDIT_STATUS="${AUDIT_STATUS:-0}" PRODUCTION_STATUS="${PRODUCTION_STATUS:-0}"
|
||||
node <<'AUDIT_JS'
|
||||
const fs = require('node:fs');
|
||||
const { execFileSync } = require('node:child_process');
|
||||
function fail(message) { throw new Error(message); }
|
||||
function readReport(name, status) {
|
||||
const report = JSON.parse(fs.readFileSync(`${process.env.REPORT_DIR}/${name}.json`, 'utf8'));
|
||||
if (report.error || report.auditReportVersion !== 2 || !report.vulnerabilities || !report.metadata?.vulnerabilities)
|
||||
fail(`${name}: npm audit returned an error or unsupported report`);
|
||||
if (![0, 1].includes(Number(status))) fail(`${name}: npm audit failed (${status})`);
|
||||
const counts = report.metadata.vulnerabilities;
|
||||
if (Object.values(report.vulnerabilities).some(v => !v || !['info', 'low', 'moderate', 'high', 'critical'].includes(v.severity)))
|
||||
fail(`${name}: invalid vulnerability severity`);
|
||||
for (const severity of ['info', 'low', 'moderate', 'high', 'critical']) {
|
||||
if (!Number.isInteger(counts[severity]) || counts[severity] < 0) fail(`${name}: invalid severity counts`);
|
||||
if (Object.values(report.vulnerabilities).filter(v => v.severity === severity).length !== counts[severity])
|
||||
fail(`${name}: inconsistent severity counts`);
|
||||
}
|
||||
return report;
|
||||
}
|
||||
try {
|
||||
const full = readReport('full', process.env.AUDIT_STATUS);
|
||||
const production = readReport('production', process.env.PRODUCTION_STATUS);
|
||||
if (production.metadata.vulnerabilities.high || production.metadata.vulnerabilities.critical)
|
||||
fail('Production dependencies contain high/critical vulnerabilities; exceptions do not apply');
|
||||
if (full.metadata.vulnerabilities.critical) fail('Critical vulnerabilities cannot be excepted');
|
||||
const config = JSON.parse(fs.readFileSync('.npm-audit-exceptions.json', 'utf8'));
|
||||
if (config.schemaVersion !== 1 || !Array.isArray(config.exceptions) || !config.exceptions.length)
|
||||
fail('Invalid exception configuration');
|
||||
const today = new Date().toISOString().slice(0, 10);
|
||||
const lock = JSON.parse(fs.readFileSync('package-lock.json', 'utf8'));
|
||||
if (!lock.packages) fail('Exceptions require a package-lock.json with package paths');
|
||||
const allowed = new Map();
|
||||
for (const entry of config.exceptions) {
|
||||
if (!/^EX-\d{4}$/.test(entry.id) || !/^GHSA-[a-z0-9]{4}-[a-z0-9]{4}-[a-z0-9]{4}$/.test(entry.advisory)
|
||||
|| entry.scope !== 'development' || !/^\d{4}-\d{2}-\d{2}$/.test(entry.expires)
|
||||
|| !Number.isFinite(Date.parse(entry.expires))
|
||||
|| new Date(entry.expires).toISOString().slice(0, 10) !== entry.expires || today >= entry.expires
|
||||
|| typeof entry.package !== 'string' || !/^(@[a-z0-9._-]+\/)?[a-z0-9._-]+$/.test(entry.package)
|
||||
|| !/^\d+\.\d+\.\d+$/.test(entry.version) || allowed.has(entry.advisory))
|
||||
fail('Invalid, duplicated or expired development exception');
|
||||
allowed.set(entry.advisory, entry);
|
||||
}
|
||||
function roots(name, seen = new Set()) {
|
||||
if (seen.has(name)) fail(`Cyclic vulnerability chain: ${name}`);
|
||||
const vulnerability = full.vulnerabilities[name];
|
||||
if (!vulnerability || !Array.isArray(vulnerability.via) || !vulnerability.via.length)
|
||||
fail(`Unresolved vulnerability chain: ${name}`);
|
||||
const next = new Set([...seen, name]);
|
||||
return vulnerability.via.flatMap(via => {
|
||||
if (typeof via === 'string') return roots(via, next);
|
||||
if (!via || typeof via.url !== 'string' || typeof via.severity !== 'string')
|
||||
fail(`Invalid advisory: ${name}`);
|
||||
return [{ advisory: via, vulnerability }];
|
||||
});
|
||||
}
|
||||
const used = new Set();
|
||||
for (const [name, vulnerability] of Object.entries(full.vulnerabilities)) {
|
||||
if (vulnerability.severity !== 'high') continue;
|
||||
for (const { advisory, vulnerability: root } of roots(name)) {
|
||||
if (!['high', 'critical'].includes(advisory.severity)) continue;
|
||||
const id = advisory.url.replace('https://github.com/advisories/', '');
|
||||
const exception = allowed.get(id);
|
||||
if (!exception || advisory.url !== `https://github.com/advisories/${id}`
|
||||
|| advisory.severity !== 'high' || advisory.name !== exception.package
|
||||
|| root.name !== exception.package || !Array.isArray(root.nodes) || !root.nodes.length)
|
||||
fail(`Unexcepted high vulnerability: ${name} (${advisory.url})`);
|
||||
for (const path of root.nodes) {
|
||||
if (!lock.packages[path] || lock.packages[path].dev !== true || lock.packages[path].version !== exception.version)
|
||||
fail(`Exception does not cover package path or version: ${path}`);
|
||||
}
|
||||
used.add(id);
|
||||
}
|
||||
if (!roots(name).some(r => ['high', 'critical'].includes(r.advisory.severity)))
|
||||
fail(`High vulnerability has no resolvable high advisory: ${name}`);
|
||||
}
|
||||
for (const id of used) {
|
||||
const entry = allowed.get(id);
|
||||
// Any newer release requires reassessment, even if npm proposes a parent downgrade.
|
||||
const latest = execFileSync('npm', ['view', entry.package, 'version', '--json'],
|
||||
{ encoding: 'utf8', timeout: 30000 });
|
||||
if (JSON.parse(latest) !== entry.version) fail(`New ${entry.package} release available; reassess ${entry.id}`);
|
||||
console.log(`Accepted ${entry.id}: ${id}, development only, expires ${entry.expires}`);
|
||||
}
|
||||
console.log('Full audit checked; production audit has no high/critical findings.');
|
||||
} catch (error) {
|
||||
console.error(`npm audit rejected: ${error.message}`);
|
||||
process.exit(1);
|
||||
}
|
||||
AUDIT_JS
|
||||
exit 0
|
||||
fi
|
||||
|
||||
npm audit --audit-level=high || {
|
||||
EXIT_CODE=$?
|
||||
echo "npm audit found high/critical vulnerabilities."
|
||||
|
||||
@@ -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
|
||||
@@ -350,6 +358,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 +368,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 +397,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}"
|
||||
@@ -431,6 +434,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 +456,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 +490,6 @@ workflows:
|
||||
sonar_project_key: ${MUNITOR_SONAR_PROJECT_KEY}
|
||||
requires:
|
||||
- build-and-test
|
||||
- coverage
|
||||
context:
|
||||
- ${MUNITOR_CONTEXT_SONAR}
|
||||
filters:
|
||||
@@ -525,8 +510,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 +533,7 @@ workflows:
|
||||
context:
|
||||
- ${MUNITOR_CONTEXT_GITHUB}
|
||||
requires:
|
||||
- code-quality
|
||||
- coverage
|
||||
- build-and-test
|
||||
- secrets-scan
|
||||
- sast-scan
|
||||
- security-scan
|
||||
@@ -572,7 +555,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##
|
||||
@@ -603,7 +588,7 @@ workflows:
|
||||
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 +608,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 +621,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##
|
||||
@@ -682,6 +669,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 +897,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 +963,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 +1026,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 +1267,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 +1346,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 +1409,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 +1650,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 +1729,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 +2533,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 +2552,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 +2584,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:
|
||||
@@ -2576,8 +2601,7 @@ cat > /tmp/munitor/templates/partials/java-webapp-deploy.yml.tpl << 'MUNITOR_PAR
|
||||
supplemental_images: '${MUNITOR_SUPPLEMENTAL_IMAGES_JSON}'
|
||||
##ENDIF_SUPPLEMENTAL##
|
||||
requires:
|
||||
- code-quality
|
||||
- coverage
|
||||
- build-and-test
|
||||
- secrets-scan
|
||||
- sast-scan
|
||||
- security-scan
|
||||
@@ -2599,8 +2623,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 +2644,9 @@ cat > /tmp/munitor/templates/partials/java-webapp-deploy.yml.tpl << 'MUNITOR_PAR
|
||||
environment: __CD_ENVIRONMENT__
|
||||
cd_format: ${MUNITOR_CD_FORMAT}
|
||||
cd_image_name: ${MUNITOR_DOCKER_REGISTRY}/${MUNITOR_IMAGE_NAME}
|
||||
##IF_CD_EXPLICIT_PATH##
|
||||
cd_path: ${MUNITOR_CD_PATH}
|
||||
##ENDIF_CD_EXPLICIT_PATH##
|
||||
##IF_SUPPLEMENTAL##
|
||||
supplemental_images: '${MUNITOR_SUPPLEMENTAL_IMAGES_JSON}'
|
||||
##ENDIF_SUPPLEMENTAL##
|
||||
@@ -2771,7 +2796,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 +2961,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 +3126,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 +3248,7 @@ validate_required_fields "${PIPELINE_TYPE}" "${CONFIG_FILE}"
|
||||
# --------------------------------------------------------------------------
|
||||
# Warn about unknown top-level keys (catches typos like java_verion)
|
||||
# --------------------------------------------------------------------------
|
||||
KNOWN_KEYS="pipeline orb_version image_name server_module java_version node_version sonar docker cd coverage e2e sbom owasp contexts npm node services test terraform sast health kustomize kube_linter_config package_manager yamllint_paths supplemental_images"
|
||||
KNOWN_KEYS="pipeline orb_version org image_name server_module java_version node_version sonar docker cd coverage e2e sbom owasp contexts npm node services test terraform sast health kustomize kube_linter_config package_manager yamllint_paths supplemental_images"
|
||||
ACTUAL_KEYS=$(yq 'keys | .[]' "${CONFIG_FILE}" 2>/dev/null || true)
|
||||
for key in ${ACTUAL_KEYS}; do
|
||||
if ! echo "${KNOWN_KEYS}" | grep -qw "${key}"; then
|
||||
@@ -3321,7 +3352,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
|
||||
@@ -613,6 +621,9 @@ DOCKERFILE
|
||||
fi
|
||||
else
|
||||
echo "Generating Dockerfile for node-webapp (node ${MUNITOR_NODE_VERSION})"
|
||||
# Single source for the runtime base. debian12 ships libssl3 3.0.18 with
|
||||
# open HIGH/CRITICAL CVEs that fail the Trivy gate; debian13 scans clean.
|
||||
NODE_WEBAPP_RUNTIME_IMAGE="gcr.io/distroless/nodejs${MUNITOR_NODE_VERSION}-debian13"
|
||||
case "${MUNITOR_PACKAGE_MANAGER}" in
|
||||
pnpm)
|
||||
cat > Dockerfile <<DOCKERFILE
|
||||
@@ -626,7 +637,7 @@ ARG GIT_COMMIT_SHA
|
||||
ENV GIT_COMMIT_SHA=\${GIT_COMMIT_SHA}
|
||||
RUN pnpm run build
|
||||
|
||||
FROM gcr.io/distroless/nodejs${MUNITOR_NODE_VERSION}-debian12 AS runner
|
||||
FROM ${NODE_WEBAPP_RUNTIME_IMAGE} AS runner
|
||||
WORKDIR /app
|
||||
ENV NODE_ENV=production
|
||||
COPY --from=builder /app/.next/standalone ./
|
||||
@@ -647,7 +658,7 @@ ARG GIT_COMMIT_SHA
|
||||
ENV GIT_COMMIT_SHA=\${GIT_COMMIT_SHA}
|
||||
RUN npm run build
|
||||
|
||||
FROM gcr.io/distroless/nodejs${MUNITOR_NODE_VERSION}-debian12 AS runner
|
||||
FROM ${NODE_WEBAPP_RUNTIME_IMAGE} AS runner
|
||||
WORKDIR /app
|
||||
ENV NODE_ENV=production
|
||||
COPY --from=builder /app/.next/standalone ./
|
||||
|
||||
+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##
|
||||
|
||||
@@ -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}"
|
||||
@@ -89,6 +84,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 +106,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 +140,6 @@ workflows:
|
||||
sonar_project_key: ${MUNITOR_SONAR_PROJECT_KEY}
|
||||
requires:
|
||||
- build-and-test
|
||||
- coverage
|
||||
context:
|
||||
- ${MUNITOR_CONTEXT_SONAR}
|
||||
filters:
|
||||
@@ -183,8 +160,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 +183,7 @@ workflows:
|
||||
context:
|
||||
- ${MUNITOR_CONTEXT_GITHUB}
|
||||
requires:
|
||||
- code-quality
|
||||
- coverage
|
||||
- build-and-test
|
||||
- secrets-scan
|
||||
- sast-scan
|
||||
- security-scan
|
||||
@@ -230,7 +205,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##
|
||||
@@ -261,7 +238,7 @@ workflows:
|
||||
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 +258,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 +271,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##
|
||||
|
||||
@@ -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:
|
||||
@@ -86,8 +70,7 @@
|
||||
supplemental_images: '${MUNITOR_SUPPLEMENTAL_IMAGES_JSON}'
|
||||
##ENDIF_SUPPLEMENTAL##
|
||||
requires:
|
||||
- code-quality
|
||||
- coverage
|
||||
- build-and-test
|
||||
- secrets-scan
|
||||
- sast-scan
|
||||
- security-scan
|
||||
@@ -109,8 +92,7 @@
|
||||
context:
|
||||
- ${MUNITOR_CONTEXT_GITHUB}
|
||||
requires:
|
||||
- code-quality
|
||||
- coverage
|
||||
- build-and-test
|
||||
- secrets-scan
|
||||
- sast-scan
|
||||
- security-scan
|
||||
@@ -131,7 +113,9 @@
|
||||
environment: __CD_ENVIRONMENT__
|
||||
cd_format: ${MUNITOR_CD_FORMAT}
|
||||
cd_image_name: ${MUNITOR_DOCKER_REGISTRY}/${MUNITOR_IMAGE_NAME}
|
||||
##IF_CD_EXPLICIT_PATH##
|
||||
cd_path: ${MUNITOR_CD_PATH}
|
||||
##ENDIF_CD_EXPLICIT_PATH##
|
||||
##IF_SUPPLEMENTAL##
|
||||
supplemental_images: '${MUNITOR_SUPPLEMENTAL_IMAGES_JSON}'
|
||||
##ENDIF_SUPPLEMENTAL##
|
||||
|
||||
@@ -144,7 +144,9 @@
|
||||
environment: __CD_ENVIRONMENT__
|
||||
cd_format: ${MUNITOR_CD_FORMAT}
|
||||
cd_image_name: ${MUNITOR_DOCKER_REGISTRY}/${MUNITOR_IMAGE_NAME}
|
||||
##IF_CD_EXPLICIT_PATH##
|
||||
cd_path: ${MUNITOR_CD_PATH}
|
||||
##ENDIF_CD_EXPLICIT_PATH##
|
||||
##IF_SUPPLEMENTAL##
|
||||
supplemental_images: '${MUNITOR_SUPPLEMENTAL_IMAGES_JSON}'
|
||||
##ENDIF_SUPPLEMENTAL##
|
||||
|
||||
@@ -144,7 +144,9 @@
|
||||
environment: __CD_ENVIRONMENT__
|
||||
cd_format: ${MUNITOR_CD_FORMAT}
|
||||
cd_image_name: ${MUNITOR_DOCKER_REGISTRY}/${MUNITOR_IMAGE_NAME}
|
||||
##IF_CD_EXPLICIT_PATH##
|
||||
cd_path: ${MUNITOR_CD_PATH}
|
||||
##ENDIF_CD_EXPLICIT_PATH##
|
||||
##IF_SUPPLEMENTAL##
|
||||
supplemental_images: '${MUNITOR_SUPPLEMENTAL_IMAGES_JSON}'
|
||||
##ENDIF_SUPPLEMENTAL##
|
||||
|
||||
+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 ]
|
||||
}
|
||||
+31
@@ -0,0 +1,31 @@
|
||||
const fs = require('node:fs');
|
||||
const [directory, scenario = 'allowed'] = process.argv.slice(2);
|
||||
const advisory = {
|
||||
name: 'braces', severity: 'high', url: 'https://github.com/advisories/GHSA-vfj7-8cjw-p6xm'
|
||||
};
|
||||
const root = { name: 'braces', severity: 'high', via: [advisory], nodes: ['node_modules/braces'] };
|
||||
const full = { auditReportVersion: 2, vulnerabilities: {
|
||||
braces: root, parent: { name: 'parent', severity: 'high', via: ['braces'] }
|
||||
}, metadata: { vulnerabilities: { info: 0, low: 0, moderate: 0, high: 2, critical: 0 } } };
|
||||
const production = { auditReportVersion: 2, vulnerabilities: {},
|
||||
metadata: { vulnerabilities: { info: 0, low: 0, moderate: 0, high: 0, critical: 0 } } };
|
||||
const exception = { id: 'EX-0008', advisory: 'GHSA-vfj7-8cjw-p6xm', package: 'braces',
|
||||
version: '3.0.3', scope: 'development', expires: '2099-01-01' };
|
||||
const lock = { lockfileVersion: 3, packages: { 'node_modules/braces': { version: '3.0.3', dev: true } } };
|
||||
if (scenario === 'expired') exception.expires = '2020-01-01';
|
||||
if (scenario === 'invalid-date') exception.expires = '2099-02-30';
|
||||
if (scenario === 'different') advisory.url = 'https://github.com/advisories/GHSA-aaaa-bbbb-cccc';
|
||||
if (scenario === 'mixed') root.via.push({ ...advisory, url: 'https://github.com/advisories/GHSA-aaaa-bbbb-cccc' });
|
||||
if (scenario === 'critical') { root.severity = advisory.severity = 'critical'; full.metadata.vulnerabilities.high--; full.metadata.vulnerabilities.critical++; }
|
||||
if (scenario === 'production') { production.vulnerabilities.braces = root; production.metadata.vulnerabilities.high = 1; }
|
||||
if (scenario === 'runtime-path') lock.packages['node_modules/braces'].dev = false;
|
||||
if (scenario === 'different-version') lock.packages['node_modules/braces'].version = '3.0.2';
|
||||
if (scenario === 'unknown-chain') full.vulnerabilities.parent.via = ['missing'];
|
||||
if (scenario === 'cycle') root.via = ['parent'];
|
||||
if (scenario === 'counts') full.metadata.vulnerabilities.high = 0;
|
||||
if (scenario === 'registry-error') full.error = { code: 'ENETUNREACH' };
|
||||
fs.mkdirSync(directory, { recursive: true });
|
||||
for (const [name, data] of Object.entries({ 'full.json': full, 'production.json': production,
|
||||
'.npm-audit-exceptions.json': { schemaVersion: 1, exceptions: [exception] }, 'package-lock.json': lock })) {
|
||||
fs.writeFileSync(`${directory}/${name}`, JSON.stringify(data));
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
#!/usr/bin/env bats
|
||||
|
||||
setup() {
|
||||
export PROJECT_DIR="${BATS_TEST_DIRNAME}/.."
|
||||
export WORK="${BATS_TEST_TMPDIR}/work"
|
||||
mkdir -p "${WORK}/bin"
|
||||
export BASH_ENV="${WORK}/bash-env" JAVA_VERSION=21
|
||||
export APT_CALLS="${WORK}/apt-calls"
|
||||
cat > "${WORK}/bin/sudo" <<'MOCK'
|
||||
#!/usr/bin/env bash
|
||||
if [[ "$1" == env ]]; then
|
||||
[[ "$2" == DEBIAN_FRONTEND=noninteractive && "$3" == NEEDRESTART_MODE=a && "$4" == apt-get ]] || exit 2
|
||||
printf '%s\n' "$*" >> "$APT_CALLS"
|
||||
elif [[ "$1" == tee ]]; then
|
||||
cat > /dev/null
|
||||
fi
|
||||
MOCK
|
||||
printf '#!/usr/bin/env bash\necho key\n' > "${WORK}/bin/curl"
|
||||
printf '#!/usr/bin/env bash\necho amd64\n' > "${WORK}/bin/dpkg"
|
||||
printf '#!/usr/bin/env bash\necho java-version-21\n' > "${WORK}/bin/java"
|
||||
chmod +x "${WORK}/bin/"*
|
||||
export PATH="${WORK}/bin:${PATH}"
|
||||
}
|
||||
|
||||
@test "APT update and Java installation cannot open an interactive restart prompt" {
|
||||
run bash "${PROJECT_DIR}/src/scripts/install_java.sh"
|
||||
[ "$status" -eq 0 ]
|
||||
[ "$(wc -l < "$APT_CALLS")" -eq 2 ]
|
||||
grep -q 'apt-get update -qq' "$APT_CALLS"
|
||||
grep -q 'apt-get install -y -qq temurin-21-jdk' "$APT_CALLS"
|
||||
grep -q "export JAVA_HOME='/usr/lib/jvm/temurin-21-jdk-amd64'" "$BASH_ENV"
|
||||
}
|
||||
@@ -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,55 @@
|
||||
#!/usr/bin/env bats
|
||||
|
||||
setup() {
|
||||
export PROJECT_DIR="${BATS_TEST_DIRNAME}/.."
|
||||
export WORK="${BATS_TEST_TMPDIR}/work"
|
||||
node "${BATS_TEST_DIRNAME}/fixtures/npm-audit/build-fixture.cjs" "${WORK}"
|
||||
mkdir -p "${WORK}/bin"
|
||||
cat > "${WORK}/bin/npm" <<'MOCK'
|
||||
#!/usr/bin/env bash
|
||||
if [[ "$1" == view ]]; then printf '"%s"\n' "${LATEST_VERSION:-3.0.3}"; exit 0; fi
|
||||
if [[ "$*" == *--omit=dev* ]]; then cat production.json; exit 0; fi
|
||||
cat full.json
|
||||
exit "${MOCK_AUDIT_STATUS:-1}"
|
||||
MOCK
|
||||
chmod +x "${WORK}/bin/npm"
|
||||
export PATH="${WORK}/bin:${PATH}"
|
||||
cd "${WORK}"
|
||||
}
|
||||
|
||||
@test "accepts only the registered advisory and its propagated development findings" {
|
||||
run bash "${PROJECT_DIR}/src/scripts/node_audit.sh"
|
||||
[ "$status" -eq 0 ]
|
||||
[[ "$output" == *"Accepted EX-0008"* ]]
|
||||
}
|
||||
|
||||
@test "fails closed for expired, invalid, unrelated, mixed, critical and runtime findings" {
|
||||
for scenario in expired invalid-date different mixed critical production runtime-path different-version unknown-chain cycle counts registry-error; do
|
||||
node "${BATS_TEST_DIRNAME}/fixtures/npm-audit/build-fixture.cjs" "$WORK" "$scenario"
|
||||
run bash "${PROJECT_DIR}/src/scripts/node_audit.sh"
|
||||
if [ "$status" -eq 0 ]; then echo "Unexpected acceptance: $scenario"; return 1; fi
|
||||
done
|
||||
}
|
||||
|
||||
@test "a new upstream release requires reassessment" {
|
||||
export LATEST_VERSION=3.0.4
|
||||
run bash "${PROJECT_DIR}/src/scripts/node_audit.sh"
|
||||
[ "$status" -ne 0 ]
|
||||
[[ "$output" == *"New braces release available"* ]]
|
||||
}
|
||||
|
||||
@test "malformed reports and unexpected audit exit codes fail closed" {
|
||||
export MOCK_AUDIT_STATUS=2
|
||||
run bash "${PROJECT_DIR}/src/scripts/node_audit.sh"
|
||||
[ "$status" -ne 0 ]
|
||||
printf 'invalid json' > full.json
|
||||
run bash "${PROJECT_DIR}/src/scripts/node_audit.sh"
|
||||
[ "$status" -ne 0 ]
|
||||
}
|
||||
|
||||
@test "repositories without exceptions retain their existing audit gate" {
|
||||
rm .npm-audit-exceptions.json
|
||||
run bash "${PROJECT_DIR}/src/scripts/node_audit.sh"
|
||||
[ "$status" -eq 1 ]
|
||||
[[ "$output" == *"npm audit found high/critical"* ]]
|
||||
}
|
||||
@@ -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,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 ]]
|
||||
@@ -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" \
|
||||
|
||||
@@ -472,10 +472,25 @@ image_name: my-webapp
|
||||
node_version: "22"
|
||||
EOF
|
||||
run_generate > /dev/null
|
||||
if grep -q "gcr.io/distroless/nodejs22-debian12" "${WORK_DIR}/Dockerfile"; then
|
||||
if grep -q "FROM gcr.io/distroless/nodejs22-debian13 AS runner" "${WORK_DIR}/Dockerfile"; then
|
||||
pass
|
||||
else
|
||||
fail "expected distroless runner image in Dockerfile"
|
||||
fail "expected distroless debian13 runner image in Dockerfile"
|
||||
fi
|
||||
teardown
|
||||
|
||||
echo -n " TEST: does not use the debian12 runner (open OpenSSL CVEs)... "
|
||||
setup
|
||||
cat > "${WORK_DIR}/.munitor.yml" <<'EOF'
|
||||
pipeline: node-webapp
|
||||
image_name: my-webapp
|
||||
node_version: "22"
|
||||
EOF
|
||||
run_generate > /dev/null
|
||||
if ! grep -q "debian12" "${WORK_DIR}/Dockerfile"; then
|
||||
pass
|
||||
else
|
||||
fail "expected no debian12 base image"
|
||||
fi
|
||||
teardown
|
||||
|
||||
@@ -589,6 +604,22 @@ else
|
||||
fi
|
||||
teardown
|
||||
|
||||
echo -n " TEST: pnpm node-webapp uses distroless debian13 runner... "
|
||||
setup
|
||||
cat > "${WORK_DIR}/.munitor.yml" <<'EOF'
|
||||
pipeline: node-webapp
|
||||
image_name: my-webapp
|
||||
node_version: "22"
|
||||
package_manager: pnpm
|
||||
EOF
|
||||
run_generate > /dev/null
|
||||
if grep -q "FROM gcr.io/distroless/nodejs22-debian13 AS runner" "${WORK_DIR}/Dockerfile"; then
|
||||
pass
|
||||
else
|
||||
fail "expected distroless debian13 runner image in pnpm Dockerfile"
|
||||
fi
|
||||
teardown
|
||||
|
||||
# =============================================================================
|
||||
# Test: node-api (nextjs) with pnpm generates correct Dockerfile
|
||||
# =============================================================================
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
bats "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/install_java.bats"
|
||||
@@ -0,0 +1,3 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
bats "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/node_audit.bats"
|
||||
@@ -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 ]]
|
||||
+12
-3
@@ -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)... "
|
||||
|
||||
@@ -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