chore(chart): re-vendor upstream dashboards @ bdba9e60 #172
Workflow file for this run
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| --- | |
| name: CI | |
| on: | |
| push: | |
| branches: [main] | |
| pull_request: | |
| # Least privilege by default; jobs widen only what they need. | |
| permissions: | |
| contents: read | |
| concurrency: | |
| group: ci-${{ github.workflow }}-${{ github.ref }} | |
| cancel-in-progress: true | |
| # Third-party actions are pinned to a full commit SHA (a re-pointed tag can ship | |
| # malicious code — cf. the March 2026 KICS action compromise). The trailing | |
| # comment records the human-readable version; .github/dependabot.yml bumps them. | |
| jobs: | |
| # Detect whether ansible/, the Helm chart or compose/ changed so heavy jobs skip | |
| # unrelated PRs. | |
| changes: | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 5 # ~5s in practice | |
| permissions: | |
| contents: read | |
| pull-requests: read | |
| outputs: | |
| ansible: ${{ steps.filter.outputs.ansible }} | |
| helm: ${{ steps.filter.outputs.helm }} | |
| compose: ${{ steps.filter.outputs.compose }} | |
| steps: | |
| - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 | |
| - uses: dorny/paths-filter@ceb8a2b8f2d89434be7ff52d3de7ec3738c5cc9d # v4.0.3 | |
| id: filter | |
| with: | |
| filters: | | |
| # ci.yml is listed because the ansible jobs' cache wiring lives here, so a | |
| # change to it must re-run them — same reason the helm filter claims it. | |
| ansible: | |
| - 'ansible/**' | |
| - '.github/workflows/ci.yml' | |
| # The chart shares the schema-key inventory and checker with molecule. | |
| helm: | |
| - 'charts/**' | |
| - 'ansible/molecule/schema/files/**' | |
| - 'Makefile' | |
| - '.github/workflows/ci.yml' | |
| # lint-compose and security-compose live in the root Makefile. | |
| compose: | |
| - 'compose/**' | |
| - 'Makefile' | |
| - '.github/workflows/ci.yml' | |
| # Ansible style + best-practice + the production-profile SECURITY rules, | |
| # plus a syntax-check of every playbook. Runs only when ansible/ changed. | |
| ansible-lint: | |
| needs: changes | |
| if: needs.changes.outputs.ansible == 'true' | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 20 # ~1m15s in practice | |
| defaults: | |
| run: | |
| working-directory: ansible | |
| steps: | |
| - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 | |
| # `cache: pip` needs a file to key on and this repo ships no pip manifest, so | |
| # the workflow is the dependency declaration — it is where the package list | |
| # literally lives. The installs stay unpinned `--upgrade`, so pip still queries | |
| # the PyPI index to resolve; the cache saves the download, not the round trip. | |
| # That is the right trade: PyPI is not the flaky dependency here, Galaxy is | |
| # (see the collections cache below). setup-python derives its own key and takes | |
| # no custom suffix, so this job and galaxy-build share one pip cache; their | |
| # package sets overlap heavily (ansible-core, ansible-lint) so whichever | |
| # populates it first covers most of both. To split them, drop `cache:` and use | |
| # actions/cache on ~/.cache/pip with explicit per-job keys. | |
| - uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 | |
| with: | |
| python-version: '3.12' | |
| cache: pip | |
| cache-dependency-path: .github/workflows/ci.yml | |
| - name: Install Ansible tooling | |
| run: python -m pip install --upgrade ansible ansible-lint yamllint | |
| # A warm collections tree removes the Galaxy round trip outright: with the | |
| # requirements already satisfied on disk, `ansible-galaxy collection install` | |
| # prints "Nothing to do" and never contacts a server. So a hit is IMMUNE to the | |
| # transient `Connection reset by peer` that failed #50 mid-resolve on the | |
| # transitive community.crypto — not merely faster than it. | |
| # | |
| # restore/save are split rather than the all-in-one action on purpose: the | |
| # failure being defended against is Galaxy dying part way through an install, | |
| # and the combined action's post step would persist that half-written tree. | |
| # Saving only after `make deps` returns 0 keeps a poisoned cache out. | |
| # | |
| # No restore-keys — an older cache still satisfies the `>=` ranges in | |
| # requirements.yml, so `make deps` would find nothing to do and the cache-epoch | |
| # bump documented in that file would silently be a no-op. | |
| # | |
| # `path:` and `hashFiles()` resolve against the workspace root, NOT this job's | |
| # `working-directory: ansible`. The key is shared with galaxy-build and with | |
| # molecule.yml; when they miss together they race to save it and the losers log | |
| # a harmless "Cache already exists". | |
| - uses: actions/cache/restore@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 | |
| id: galaxy-cache | |
| with: | |
| path: ansible/collections | |
| key: galaxy-${{ runner.os }}-${{ hashFiles('ansible/requirements.yml') }} | |
| - name: Install Galaxy collections | |
| run: make deps # must precede lint/syntax-check | |
| - uses: actions/cache/save@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 | |
| if: steps.galaxy-cache.outputs.cache-hit != 'true' | |
| with: | |
| path: ansible/collections | |
| key: galaxy-${{ runner.os }}-${{ hashFiles('ansible/requirements.yml') }} | |
| - name: Lint (yamllint + ansible-lint) | |
| run: make lint | |
| - name: Syntax-check playbooks | |
| # Dummy inventory: no real hosts.yml is committed upstream (only hosts.yml.example), | |
| # and --syntax-check only parses, it never connects. Every playbook, so a new | |
| # one (backup, decommission, ...) is covered without editing this step. | |
| run: | | |
| for pb in playbooks/*.yml; do | |
| ansible-playbook "$pb" --syntax-check -i localhost, | |
| done | |
| # Build the public `decdn.node` collection and run galaxy-importer's checks — | |
| # the same validation Galaxy runs on upload (metadata, license, README, embedded | |
| # ansible-lint). This is a readiness GATE only: it never publishes and needs no | |
| # token. Runs only when ansible/ changed. | |
| galaxy-build: | |
| needs: changes | |
| if: needs.changes.outputs.ansible == 'true' | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 20 # ~1m45s in practice | |
| defaults: | |
| run: | |
| working-directory: ansible | |
| steps: | |
| - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 | |
| # Shares the pip cache with ansible-lint — see that job for why the key is | |
| # the workflow file. | |
| - uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 | |
| with: | |
| python-version: '3.12' | |
| cache: pip | |
| cache-dependency-path: .github/workflows/ci.yml | |
| - name: Install build + import tooling | |
| run: python -m pip install --upgrade ansible-core ansible-lint galaxy-importer | |
| # Same cache as ansible-lint (same key, same rationale — see that job). | |
| - uses: actions/cache/restore@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 | |
| id: galaxy-cache | |
| with: | |
| path: ansible/collections | |
| key: galaxy-${{ runner.os }}-${{ hashFiles('ansible/requirements.yml') }} | |
| - name: Vendor collection dependencies | |
| # So galaxy-importer's embedded ansible-lint can resolve the roles' FQCNs | |
| # (devsec.hardening, ansible.posix). Installed under ansible/collections. | |
| run: make deps | |
| env: | |
| ANSIBLE_COLLECTIONS_PATH: collections | |
| - uses: actions/cache/save@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 | |
| if: steps.galaxy-cache.outputs.cache-hit != 'true' | |
| with: | |
| path: ansible/collections | |
| key: galaxy-${{ runner.os }}-${{ hashFiles('ansible/requirements.yml') }} | |
| - name: Build + validate the decdn.node collection | |
| run: make galaxy-check | |
| env: | |
| ANSIBLE_COLLECTIONS_PATH: collections | |
| - name: Upload collection artifact | |
| if: always() | |
| uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 | |
| with: | |
| name: decdn-node-collection | |
| path: ansible/build/decdn-node-*.tar.gz | |
| if-no-files-found: ignore | |
| # Helm chart: `helm lint --strict`, positive + negative render tests, kubeconform | |
| # (digest-pinned image) and the upstream schema-key check shared with molecule — | |
| # all via `make lint-helm`, the same command developers run locally. | |
| helm: | |
| needs: changes | |
| if: needs.changes.outputs.helm == 'true' | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 20 # pulls the kubeconform image; ~1m in practice | |
| steps: | |
| - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 | |
| # Helm version is pinned here, in the kics job below and twice in release.yml; | |
| # bump all four. | |
| - uses: azure/setup-helm@9bc31f4ebc9c6b171d7bfbaa5d006ae7abdb4310 # v5.0.1 | |
| with: | |
| version: v4.3.0 | |
| - uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 | |
| with: | |
| python-version: '3.12' | |
| - name: Chart lint + render tests | |
| # yq (mikefarah) and Docker ship on ubuntu-latest. No decdn binary here, so | |
| # the real `decdn config validate` step reports SKIPPED; run it locally with | |
| # DECDN_CLI=... before bumping the decdn version. | |
| run: make lint-helm | |
| # Docker Compose path: render compose/compose.yaml with its example env files and | |
| # assert the invariants its README promises (host network, no published ports, | |
| # digest-pinned image, read-only rootfs, no capabilities, graceful stop). Runs | |
| # only when compose/ or the root Makefile changed. | |
| compose: | |
| needs: changes | |
| if: needs.changes.outputs.compose == 'true' | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 5 # docker compose + jq ship on ubuntu-latest; seconds in practice | |
| steps: | |
| - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 | |
| - name: Compose invariants | |
| run: make lint-compose | |
| # Render roles/grafana_alloy's templates and validate them with the REAL pinned | |
| # Grafana Alloy binary. The molecule `grafana-cloud` scenario deliberately runs | |
| # against a stub that exits 0 for every subcommand, so it proves plumbing but | |
| # would happily ship a config Alloy cannot load (an unknown component, a block | |
| # in the wrong parent, a CLI flag that does not exist). This job is that gate. | |
| # Runs only when ansible/ changed. | |
| alloy-config: | |
| needs: changes | |
| if: needs.changes.outputs.ansible == 'true' | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 15 # ~1m in practice, most of it the first .deb fetch | |
| steps: | |
| - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 | |
| - uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 | |
| with: | |
| python-version: '3.12' | |
| cache: pip | |
| cache-dependency-path: .github/workflows/ci.yml | |
| - name: Install Ansible | |
| run: python -m pip install --upgrade ansible | |
| # Keyed on the role defaults, which is where the version + digest pin | |
| # lives: bumping grafana_alloy_version invalidates the cache by | |
| # construction, so a bump is always validated against the new binary. | |
| - uses: actions/cache@caa296126883cff596d87d8935842f9db880ef25 # v5.1.0 | |
| with: | |
| path: ansible/.cache/alloy | |
| key: alloy-${{ runner.os }}-${{ hashFiles('ansible/roles/grafana_alloy/defaults/main.yml') }} | |
| - name: Validate the rendered Alloy configuration | |
| run: make lint-alloy | |
| # Dedicated IaC security scan of the Ansible tree, the rendered Helm chart and | |
| # compose/, driven straight from the digest-pinned KICS *engine* image by | |
| # `make security` — the exact command | |
| # developers run locally, so CI and local results cannot drift. KICS severities | |
| # are CRITICAL/HIGH/MEDIUM/LOW/INFO; the engine's own `--fail-on high` exit | |
| # code is the gate. | |
| # | |
| # SUPPLY-CHAIN NOTE: we deliberately do NOT use Checkmarx/kics-github-action. | |
| # Its git tags were hijacked in the March 2026 TeamPCP attack (CISA KEV), and | |
| # beyond that history its entrypoint `apk add`s nodejs/npm at *run* time inside | |
| # its digest-pinned base image and then executes the result — an unpinned fetch | |
| # that defeats the pinning it advertises. That fetch also breaks the action | |
| # outright today: Chainguard's current nodejs wants a newer glibc than the | |
| # pinned base ships, so `node dist/index.js` dies and the step exits non-zero | |
| # no matter what the scan found. Driving the engine image ourselves drops the | |
| # Node layer entirely and leaves one pinned artifact — `KICS_IMAGE` in the | |
| # Makefile, pinned by Docker Hub digest (a different artifact from the | |
| # hijacked action). See CONTRIBUTING.md. | |
| kics: | |
| needs: changes | |
| if: >- | |
| needs.changes.outputs.ansible == 'true' || needs.changes.outputs.helm == 'true' | |
| || needs.changes.outputs.compose == 'true' | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 20 # pulls the KICS engine image; ~30s in practice | |
| steps: | |
| - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 | |
| - uses: azure/setup-helm@9bc31f4ebc9c6b171d7bfbaa5d006ae7abdb4310 # v5.0.1 | |
| with: | |
| version: v4.3.0 | |
| - name: KICS security scan of ansible/, the rendered chart and compose/ (fail on HIGH) | |
| run: make security | |
| # Replaces the action's `enable_jobs_summary`. | |
| - name: Summarise KICS findings | |
| if: always() | |
| run: | | |
| for scan in ansible helm compose; do | |
| results=kics-results/results.json | |
| [ "$scan" != ansible ] && results="kics-results/$scan/results.json" | |
| { | |
| echo "### KICS IaC scan ($scan)" | |
| echo | |
| if [ -f "$results" ]; then | |
| jq -r '.severity_counters | |
| | "| CRITICAL | HIGH | MEDIUM | LOW | INFO |", | |
| "|---|---|---|---|---|", | |
| "| \(.CRITICAL) | \(.HIGH) | \(.MEDIUM) | \(.LOW) | \(.INFO) |"' "$results" | |
| echo | |
| jq -r 'if (.total_counter // 0) == 0 then "No findings." | |
| else (.queries[] | .query_name as $q | .severity as $s | |
| | .files[] | "- **\($s)** \($q) — `\(.file_name):\(.line)`") | |
| end' "$results" | |
| else | |
| echo "No results file — the scan did not complete." | |
| fi | |
| echo | |
| } >> "$GITHUB_STEP_SUMMARY" | |
| done | |
| - name: Upload KICS results | |
| if: always() | |
| uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 | |
| with: | |
| name: kics-results | |
| path: kics-results/ | |
| if-no-files-found: ignore | |
| # To surface KICS findings in the repo's Security tab, enable GitHub code | |
| # scanning (free on public repos) and un-comment the block below (add | |
| # `security-events: write` to this job's permissions): | |
| # - name: Upload SARIF to code scanning | |
| # if: always() | |
| # uses: github/codeql-action/upload-sarif@d77b13a0df3134d64a457ea9003f600b09fa1c8a # v3.36.1 | |
| # with: | |
| # sarif_file: kics-results/results.sarif | |
| # Every pre-commit hook on every file: hygiene, shellcheck, markdownlint and | |
| # yamllint. The same gate `make lint` runs locally, so contributors who never | |
| # installed the hook still meet it. The manual-stage ansible-lint hook is skipped | |
| # (the ansible-lint job above covers it). Always runs: it is cheap and covers | |
| # every path. | |
| pre-commit: | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 10 # hook environments are cached; ~1m cold | |
| steps: | |
| - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 | |
| - uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 | |
| with: | |
| python-version: '3.12' | |
| - name: Install pre-commit | |
| run: python -m pip install --upgrade pre-commit | |
| # Hook environments (node for markdownlint, the shellcheck image, yamllint's | |
| # virtualenv) keyed on the config that pins them. | |
| - uses: actions/cache@caa296126883cff596d87d8935842f9db880ef25 # v5.1.0 | |
| with: | |
| path: ~/.cache/pre-commit | |
| key: pre-commit-${{ runner.os }}-${{ hashFiles('.pre-commit-config.yaml') }} | |
| - name: Run every hook on every file | |
| run: pre-commit run --all-files --show-diff-on-failure | |
| # The repo's own guard rails: ansible/Makefile's scoping guards (dry runs), the | |
| # release gate, and lint-compose's negative cases (tests/scripts-test.sh). Always | |
| # runs: seconds, and it covers files every path filter above would miss. | |
| scripts: | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 5 | |
| steps: | |
| - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 | |
| - name: Script and guard tests | |
| run: make test-scripts | |
| # Lint the workflow files themselves. | |
| actionlint: | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 10 # ~15s in practice | |
| permissions: | |
| contents: read | |
| checks: write | |
| steps: | |
| - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 | |
| - uses: reviewdog/action-actionlint@8b682e1e7e512151d73a6b9449cbff173846226e # v1.74.0 | |
| with: | |
| reporter: github-check | |
| fail_on_error: true |