Skip to content

feat: add reusable container-CI workflows (lint / security / smoke-test)#141

Merged
CybotTM merged 6 commits into
mainfrom
feat/reusable-container-workflows
May 21, 2026
Merged

feat: add reusable container-CI workflows (lint / security / smoke-test)#141
CybotTM merged 6 commits into
mainfrom
feat/reusable-container-workflows

Conversation

@CybotTM
Copy link
Copy Markdown
Member

@CybotTM CybotTM commented May 21, 2026

Summary

Add three reusable workflows to consolidate the container-CI duplication
across netresearch/snipe-it-docker-compose-stack and netresearch/phpbu-docker
(and future container repos), filling the gap left by the existing
build-container.yml:

  • lint-container.yml — hadolint (via hadolint:latest-alpine image so we
    track current releases; the v3.1.0 action ships hadolint v2.12.0 which
    crashes on Docker 25's HEALTHCHECK --start-interval) plus optional
    shellcheck against caller-specified scandirs.
  • security-container.yml — post-build Trivy scan against a single already-
    published image reference, SARIF upload to GitHub code-scanning. Caller fans
    out tag matrices. Distinct from build-container.yml's build-time scan:
    this runs on schedule / workflow_run AFTER push and catches CVEs
    disclosed since the build.
  • smoke-test-container.yml — build locally amd64 + --load + run
    container-structure-test against a caller config path.

Plus docs/container-workflows.md documenting the full set
(build / lint / security / smoke-test + supporting workflows) with caller
snippets and an explicit "what stays in the caller" list.

Type of Change

  • New feature (non-breaking) — purely additive, no existing workflows touched

Scope (deliberately narrow)

The reusables do NOT cover, by design:

  • docker compose config / compose up orchestration — needs .env.example
    bootstrap with secret-shaped placeholders and known HTTP routes; stays in
    caller workflows (snipe-it's lint.yml::compose-validate and
    smoke-test.yml::compose-up).
  • init.sh idempotency tests — caller-specific contract.
  • osv-scanner against language lockfiles inside images — needs lockfile
    path + language; varies per stack.
  • Multi-track / multi-composer-mode build matrices — caller defines matrix
    axes and tag schemes; build-container.yml already supports one ref
    per call which is the right granularity for fan-out.

Caller impact (not in this PR)

Phase 2 (separate PRs on the consumer repos) will migrate
snipe-it-docker-compose-stack and phpbu-docker to call these reusables.
Expected reduction for snipe-it: lint.yml (~87 lines → ~20-25 lines,
keeping compose-validate inline), security.yml Trivy job (~50 lines
→ ~15 lines), smoke-test.yml::image-surface job (~25 lines → ~10
lines). The track×composer build matrix stays in the caller because
the orchestration logic (ref resolution, tag fan-out, continue-on-error: rolling) is genuinely snipe-it-specific.

Conventions followed

  • SPDX MIT header + Copyright (c) 2026 Netresearch DTT GmbH on every file.
  • All third-party actions SHA-pinned with # vX.Y.Z comments
    (SHAs copied from the existing build-container.yml and verified current
    via gh api repos/OWNER/REPO/tags).
  • harden-runner as the first step in every job.
  • permissions: enumerated per job; top-level contents: read only.
  • ${{ ... }} interpolation passes through env: to run: blocks
    (SonarCloud S7630).
  • persist-credentials: false on every checkout.
  • No secrets: inherit — these reusables don't consume any secrets;
    consumers passing ${{ secrets.GITHUB_TOKEN }} happens at the
    build-container.yml layer only.

Test Plan

  • Local lint pass — actionlint and yamllint (using the same inline
    config the org lint-yaml.yml reusable applies) both clean on all
    three new workflow files.
  • markdownlint-cli2 clean on docs/container-workflows.md.
  • Trailing-newline check (tail -c 4 | xxd -p) — all files end in
    a single 0a, no 0a0a (yamllint empty-lines rule).
  • CI on this PR — lint-workflows.yml (actionlint), lint-yaml.yml
    (yamllint), lint-markdown.yml will be the authoritative gates.
  • Phase 2 consumer migration (separate PRs) will exercise the workflows
    end-to-end against real Dockerfiles before we tag a stable ref.

CybotTM added 4 commits May 21, 2026 22:29
Add hadolint (via hadolint:latest-alpine image to track current
releases — the action ships with v2.12.0 which crashes on Docker 25's
HEALTHCHECK --start-interval flag) plus optional shellcheck against
caller-specified scandirs.

Consumers: snipe-it-docker-compose-stack, phpbu-docker — both
currently duplicate this exact pattern in repo-local lint workflows.

Signed-off-by: Sebastian Mendel <sebastian.mendel@netresearch.de>
Scan a single already-published image reference with Trivy, SARIF
upload to GitHub code-scanning. Caller fans out matrices over tags
(latest / rolling / branch tags) — one ref per invocation.

Distinct from build-container.yml's build-time scan: this is intended
for schedule / workflow_run triggers AFTER push, so it catches CVEs
disclosed since the build. Default exit-code 0 (informational) matches
the snipe-it pattern where third-party transitive CVEs can't be patched
downstream — alert surface is the GitHub Security tab via SARIF.

Consumers: snipe-it-docker-compose-stack security.yml (latest+rolling
matrix), phpbu-docker security.yml (ci+ci-full matrix).

Signed-off-by: Sebastian Mendel <sebastian.mendel@netresearch.de>
…kflow

Build the caller's image locally (amd64, --load), then run
container-structure-test against a config file in the caller's repo.

Scope is intentionally narrow: only the structure-test surface that's
shared across container repos. Consumer-specific orchestration (docker
compose up + HTTP probe, init-script idempotency, env bootstrap) stays
in the caller's workflow — those depend on app-specific routes,
healthcheck semantics, and secret-shaped placeholders that don't
generalise.

Consumers: snipe-it-docker-compose-stack smoke-test.yml, phpbu-docker
test.yml — both currently duplicate the buildx + load + install-cst +
run-cst pattern.

Signed-off-by: Sebastian Mendel <sebastian.mendel@netresearch.de>
…w set

Add docs/container-workflows.md covering the four container-focused
reusable workflows (build-container, lint-container,
security-container, smoke-test-container) plus the supporting set
(scorecard, ghcr-retention, gitleaks, lint-*, auto-merge-deps).

Includes minimal caller patterns and an explicit list of what stays
in the caller (compose orchestration, init-script idempotency,
osv-scanner against language lockfiles, multi-track build matrices)
so contributors don't try to push those into the abstraction.

Signed-off-by: Sebastian Mendel <sebastian.mendel@netresearch.de>
Copilot AI review requested due to automatic review settings May 21, 2026 20:30
Copy link
Copy Markdown

@gemini-code-assist gemini-code-assist Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request introduces a new documentation file, docs/container-workflows.md, which outlines reusable GitHub Actions workflows for container repositories. It provides descriptions of workflows for building, linting, and security scanning, along with example caller patterns and organizational conventions. A review comment correctly identified that the security scan example was missing the security-events: write permission, which is necessary for the SARIF upload step to function.

Comment thread docs/container-workflows.md
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR introduces a set of reusable GitHub Actions workflows aimed at standardizing and de-duplicating container CI across Netresearch container repositories, complementing the existing build-container.yml reusable.

Changes:

  • Adds lint-container.yml reusable for Dockerfile linting (hadolint) plus optional shellcheck.
  • Adds security-container.yml reusable for post-publish Trivy scanning with SARIF upload to GitHub code-scanning.
  • Adds smoke-test-container.yml reusable to build a local amd64 image and run container-structure-test, plus documentation for usage patterns.

Reviewed changes

Copilot reviewed 4 out of 4 changed files in this pull request and generated 5 comments.

File Description
docs/container-workflows.md Documents the full container workflow suite and provides caller snippets + delineation of caller-owned responsibilities.
.github/workflows/lint-container.yml New reusable workflow for hadolint (via container) and optional shellcheck.
.github/workflows/security-container.yml New reusable workflow for scanning already-published images with Trivy and uploading SARIF.
.github/workflows/smoke-test-container.yml New reusable workflow to locally build amd64 images and run container-structure-test.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread .github/workflows/security-container.yml
Comment thread .github/workflows/security-container.yml
Comment thread .github/workflows/smoke-test-container.yml Outdated
Comment thread .github/workflows/lint-container.yml
Comment thread .github/workflows/lint-container.yml
… GHCR auth

Six review threads from copilot-pull-request-reviewer and
gemini-code-assist on PR 141. Five are addressed in this commit; the
sixth is a false-positive (separate inline reply with explanation).

ADDRESSED (5):

1. security-container.yml — missing `packages: read` permission.
   Without it, Trivy gets HTTP 401 when scanning a private GHCR image
   (or an org-locked public package). Added at job level alongside
   `security-events: write`.

2. security-container.yml — no registry login before Trivy. Trivy
   spawns its own docker-client and does NOT inherit pre-existing
   daemon credentials. Added a conditional `docker/login-action` step
   gated on `startsWith(image-ref, 'ghcr.io/')` so non-GHCR callers
   (docker.io, internal registries) aren't forced through a bogus
   GHCR auth. Other-registry callers add their own login in a wrapper.

3. smoke-test-container.yml — container-structure-test was fetched
   from `https://storage.googleapis.com/.../latest/...` (mutable). Now
   pinned to v1.22.1 by SHA256 (fa35e89...) fetched from the GitHub
   release asset, with verification before install. Renovate
   regex-managers can bump version+sha in tandem.

4. lint-container.yml — hadolint was `hadolint/hadolint:latest-alpine`
   (mutable tag). Pinned by digest: `:v2.12.0-alpine@sha256:3c206a...`.
   Bumped via Renovate's docker-tag updater. Design-notes comment
   block updated to explain the pin.

5. docs/container-workflows.md — caller `permissions:` example was
   `{ contents: read }` only. Added preamble explaining that
   `security-container.yml` declares its own job-level permissions
   (security-events: write, packages: read, contents: read) so the
   caller's top-level `permissions:` block only needs to cover OTHER
   jobs. Keeps the documented caller-side minimum at `contents: read`.

NOT ADDRESSED (1, push-back via inline reply):

6. lint-container.yml line 135 — Copilot suggested the action version
   comment `# 2.0.0` should be `# v2.0.0` to "match the established
   convention". Rejected: `ludeeus/action-shellcheck` upstream uses
   tags WITHOUT a v prefix (verified via
   `gh api repos/ludeeus/action-shellcheck/tags`). The SHA pin's
   comment must match upstream's actual tag string, not a synthetic
   v-prefixed alias. Reply posted to the review thread.

VERIFICATION:
- actionlint 1.7.12 clean on all three files.
- yamllint exit 0 (only pre-existing warnings about single-space SHA
  pin comments, which the repo's reusable lint-yaml allows).
- All files end with single newline (no trailing blank line).

Signed-off-by: Sebastian Mendel <sebastian.mendel@netresearch.de>
CybotTM added a commit to netresearch/snipe-it-docker-compose-stack that referenced this pull request May 21, 2026
The netresearch/.github reusable lint-container.yml on branch
feat/reusable-container-workflows (HEAD 1a56c99) pins hadolint to
v2.12.0-alpine by digest. That's exactly the version the original
snipe-it lint.yml warned against — it predates Docker 25's
HEALTHCHECK --start-interval flag and crashes on the snipe-it
Dockerfile's healthcheck line:

  /dev/stdin:228:5 invalid flag: --start-interval
  Process completed with exit code 1.

The reusable's docstring still claims latest-alpine, so this is an
upstream regression between PR review and the current branch HEAD.

Reverting the lint.yml migration until netresearch/.github either:
  - re-pins to a hadolint version >= v2.13 (which supports
    --start-interval), or
  - adds a hadolint-image input so callers can override.

The shellcheck migration is reverted together with hadolint — they
share the same caller-job wrapping.

Refs: netresearch/.github#141
Signed-off-by: Sebastian Mendel <info@sebastianmendel.de>
The previous commit (1a56c99) pinned hadolint to v2.12.0-alpine in
response to Copilot's "mutable tag" review. That digest pins the
March 2023 release, which is exactly the broken version the DESIGN
NOTES warned about: it crashes with `invalid flag: --start-interval`
on any Dockerfile that uses Docker 25's HEALTHCHECK --start-interval=
syntax.

I assumed `:latest-alpine` and `:v2.12.0-alpine` shared the same
hadolint binary (just rebuilt with newer Alpine base). They don't:
upstream cut v2.13.1 (Sep 2025) and v2.14.0 (Sep 2025) without
updating my memory of the project. The Phase 2 sub-agent caught the
regression on the snipe-it caller PR — `lint.yml` failing on
snipe-it's HEALTHCHECK --start-interval=5s.

Pin corrected to v2.14.0-alpine
(@sha256:7aba693c1442eb31c0b015c129697cb3b6cb7da589d85c7562f9deb435a6657c).
Comment block extended with a "MUST be >= v2.13" guard so the same
mistake doesn't recur on the next Renovate bump.

Verified locally against a Dockerfile with the same HEALTHCHECK form
as the snipe-it runtime stage:
  v2.12.0-alpine: /dev/stdin:2:48 invalid flag: --start-interval
  v2.14.0-alpine: DL3006 warning (unrelated, just a base-image hint)

Signed-off-by: Sebastian Mendel <sebastian.mendel@netresearch.de>
@sonarqubecloud
Copy link
Copy Markdown

@CybotTM CybotTM merged commit 7477e5e into main May 21, 2026
9 checks passed
@CybotTM CybotTM deleted the feat/reusable-container-workflows branch May 21, 2026 20:58
CybotTM added a commit to netresearch/snipe-it-docker-compose-stack that referenced this pull request May 21, 2026
PR netresearch/.github#141 merged at 2026-05-21T20:58:39Z with merge
commit 7477e5e. Two follow-ups in this commit:

1. smoke-test.yml: ref flip
   @feat/reusable-container-workflows → @main

2. lint.yml: re-apply the container-lint migration that was reverted
   in f78c9e5. The revert was needed because the reusable's initial
   hadolint pin (v2.12.0-alpine) was the version that crashes on
   HEALTHCHECK --start-interval — exactly the bug the reusable's
   DESIGN NOTES warned about. The reusable's pin was corrected to
   v2.14.0 in netresearch/.github commit a4a763e (also part of the
   merged PR 141), so the migration now works end-to-end.

   Inline yamllint job stays as-is (the snipe-it repo has no
   .yamllint.yml so the contract is local — caller-specific, doesn't
   generalise to the reusable).

Verified locally: actionlint clean on both files. CI should pick up
the new state on push.

TODOs still open (from the Phase 2 sub-agent's report):

- build.yml not migrated. build-container.yml needs six additional
  inputs (custom tags fan-out, build-args, target, OCI labels,
  per-cell cache-scope, provenance/sbom) before the snipe-it
  track×composer matrix can call it. Follow-up PR upstream.
- security.yml::trivy not migrated. Needs a tolerate-pull-failure
  step-level input on security-container.yml so callers can keep
  `continue-on-error: ${{ matrix.tag == 'rolling' }}` semantics
  (GitHub forbids continue-on-error on reusable-caller jobs).
  Follow-up PR upstream.
- security.yml::osv-scanner stays inline (composer.lock extraction
  is snipe-it-specific).

Signed-off-by: Sebastian Mendel <info@sebastianmendel.de>
CybotTM added a commit to netresearch/snipe-it-docker-compose-stack that referenced this pull request May 22, 2026
Phase 2 of the container-CI consolidation. Phase 1 reusables
(lint-container, security-container, smoke-test-container) are now on
`main` in netresearch/.github — PR netresearch/.github#141 merged at
7477e5e.

## What's migrated

| Workflow | Before | After | Reusable |
|---|---|---|---|
| `lint.yml::container-lint` | 87 lines inline | delegates to
`lint-container.yml@main` | hadolint v2.14.0 + shellcheck |
| `smoke-test.yml::image-surface` | 25 lines inline | delegates to
`smoke-test-container.yml@main` | buildx --load +
container-structure-test |
| `scorecard.yml` | 64 lines local | delegates to `scorecard.yml@main` |
OpenSSF Scorecard + SARIF upload |

What STAYS in this caller (snipe-it-specific, doesn't generalise):
- `lint.yml::compose-validate` — `.env.example` placeholder substitution
+ `docker compose config --quiet`
- `lint.yml::yamllint` — no `.yamllint.yml` config in this repo, rules
passed via `config_data`
-
`smoke-test.yml::{stack-boots-healthy,init.sh-is-idempotent,upstream-tests}`
— depend on snipe-it's `.env` shape + HTTP probes
- `build.yml` — track×composer matrix + `.snipe-it-version` ref
resolution + tag fan-out + `continue-on-error: rolling`
- `security.yml::trivy` — needs `continue-on-error: ${{ matrix.tag ==
'rolling' }}` which GitHub forbids on reusable-caller jobs
- `security.yml::osv-scanner` — composer.lock extraction is
snipe-it-specific

## Open dependencies (separate PRs upstream)

To migrate `build.yml` to the reusable,
`netresearch/.github/.github/workflows/build-container.yml` needs six
additional inputs: custom `tags` fan-out, `build-args`, `target`, OCI
`labels`, per-cell `cache-scope`, `provenance: mode=max` / `sbom: true`.
Tracking as follow-up.

To migrate `security.yml::trivy`, `security-container.yml` needs a
step-level `tolerate-pull-failure` boolean input so callers can keep
rolling-tag-may-not-exist semantics without using job-level
`continue-on-error`. Tracking as follow-up.

## Manual action required from a SonarCloud admin

The reusable refs use `@main` (org policy: first-party reusables are NOT
SHA-pinned). SonarCloud's `githubactions:S7637` rule ("Use full commit
SHA hash for this dependency") flags these as 3 LOW security hotspots.
Tried SHA-pinning in b38104d; reverted in 5aa57b9 per maintainer
feedback ("WIR PINNEN UNSERE EIGENEN RE-USABLE WORKFLOWS NICHT").

These cannot be marked via API without a SonarCloud admin token (not in
this repo's secrets). Please mark them as REVIEWED + SAFE in the
SonarCloud UI:


https://sonarcloud.io/project/security_hotspots?id=netresearch_snipe-it-docker-compose-stack&pullRequest=11

Or deactivate the rule for this project in its Quality Profile.

## Test plan

- [x] container-lint / hadolint green (hadolint v2.14.0 handles
`HEALTHCHECK --start-interval` correctly)
- [x] container-lint / shellcheck green
- [x] image-surface / container-structure-test green
- [x] scorecard SARIF uploaded
- [x] All 6 build matrix cells succeeded (1 expected continue-on-error
on tag/rolling)
- [ ] SonarCloud hotspots marked SAFE by admin (manual)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants