From 666cef9debd1a444683350fd97ee854a9b894ecc Mon Sep 17 00:00:00 2001 From: Louis Parkin Date: Mon, 3 Aug 2026 09:47:32 +0200 Subject: [PATCH 1/7] STAC-25463 Add GitHub Actions check-test workflow (integrations CI migration phase 1) Ports the pure-Python half of .gitlab-ci.yml to GitHub Actions as the first phase of moving stackstate-agent-integrations off GitLab (STAC-25142). What moved: * The `test_` job family -> a single `check-tests` matrix. * The per-job `changes:` rules -> .github/scripts/select-checks.sh, which reproduces them with `git diff` against the merge base. Done in plain git because StackVista enforces a strict third-party action allowlist. * The `checksdev validate *` commands that rode inside test_stackstate_checks_base -> their own job, so a metadata failure reads as its own PR check. Scoped out, with reasons recorded in the workflow header: * The five Docker-dependent suites (splunk x4 + stackstate_checks_dev) are phase 2; both public ARC runners ship a DinD sidecar, so this is about exercising that path, not provisioning it. * test_postgres is dead config - it tests a `postgres` check that does not exist in this repository. * print_env is not ported at any phase: a bare `printenv` on a PUBLIC repo publishes every CI credential in scope to a world-readable log. * There is no Windows lane to port. `.gitlab-ci.yml` defines a `.windows_env` anchor that no job has ever referenced, and Windows is not a supported target. * Publishing stays on GitLab until integrations gets its own publishing role (pulumi-infra section 7.4), so the GitLab pipeline remains live. Phase 1 needs no new secret provisioning: REGISTRY_HOST/USER/PASSWORD are org-level visibility=all (STAC-25350) and so already reach this PUBLIC repo. The image reference composes vars.REGISTRY_HOST + /docker rather than using vars.REGISTRY_DOCKER_URL, which is private-visibility and unreadable here. Validated: actionlint clean (with a new .github/actionlint.yaml declaring the self-hosted labels), zizmor clean, shellcheck clean, and the selector exercised against real git history for the push, single-check, multi-check, shared-library and no-match cases. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .github/actionlint.yaml | 9 ++ .github/scripts/select-checks.sh | 128 +++++++++++++++++ .github/workflows/checks-tests.yml | 214 +++++++++++++++++++++++++++++ 3 files changed, 351 insertions(+) create mode 100644 .github/actionlint.yaml create mode 100755 .github/scripts/select-checks.sh create mode 100644 .github/workflows/checks-tests.yml diff --git a/.github/actionlint.yaml b/.github/actionlint.yaml new file mode 100644 index 00000000..898078e3 --- /dev/null +++ b/.github/actionlint.yaml @@ -0,0 +1,9 @@ +# Self-hosted runner labels available to this repository, so actionlint does not +# report them as unknown. These are ARC scale sets in the `public` runner group +# (StackVista/argocd-apps, cluster_definitions/tooling-main/apps/github-runner-*); +# the `-public` tier is the one a PUBLIC repository is allowed to schedule on. +self-hosted-runner: + labels: + - docker-public + - xlarge-public + - arm64-xlarge-public diff --git a/.github/scripts/select-checks.sh b/.github/scripts/select-checks.sh new file mode 100755 index 00000000..e0683e8f --- /dev/null +++ b/.github/scripts/select-checks.sh @@ -0,0 +1,128 @@ +#!/usr/bin/env bash +# +# Selects which integration check suites the test matrix should run, reproducing +# the `changes:` rules that gated each `test_` job in .gitlab-ci.yml +# (GitLab -> GitHub migration, STAC-25463). +# +# GitLab evaluated a per-job `changes:` list; GitHub has no job-level path filter, +# so the equivalent is computed once here and fanned out as a matrix. This is done +# in plain git rather than a path-filter action: StackVista enforces a strict +# third-party action allowlist, and `git diff` against the merge base is exactly +# what the GitLab rule meant. +# +# Selection rules, ported from .gitlab-ci.yml: +# * A change to a shared library, the setup scripts, or this CI wiring runs +# EVERY suite (GitLab: the `base_changes` anchor). +# * Otherwise only the suites whose own directory changed run. +# * GitLab's `splunk_base_build_rule` -- a change to splunk_base also runs the +# other three splunk suites, which import its test helpers -- is not ported +# here because no splunk suite runs yet. It lands with them in phase 2. +# * push / workflow_dispatch run everything (GitLab: `master_branch`, +# `release_branch`). +# +# Writes `checks=` to $GITHUB_OUTPUT for `fromJson()` in a matrix. + +set -euo pipefail + +# Suites currently running on GitHub Actions. Phase 1 is the 15 pure-Python +# suites: they need no Docker daemon, so they are the low-risk half of the port. +# +# Deliberately NOT here yet (phase 2, needs the DinD story exercised first): +# splunk_base, splunk_health, splunk_metric, splunk_topology +# -- each drives a real Splunk container via docker-compose. +# stackstate_checks_dev +# -- its tests exercise the toolkit's own Docker helpers. +# Both public ARC runners provide a DinD sidecar, so this is a matter of proving +# it rather than provisioning anything. +# +# Deliberately dropped, not pending: +# postgres -- .gitlab-ci.yml carried a `test_postgres` job for a check that does +# not exist in this repository. It is dead config, not a gap. +CHECKS=( + agent_integration_sample + agent_v2_integration_sample + agent_v2_integration_stateful_sample + agent_v2_integration_transactional_sample + dynatrace_base + dynatrace_health + dynatrace_topology + kubelet + openmetrics + servicenow + stackstate_checks_base + static_health + static_topology + vsphere + zabbix +) + +# A change anywhere here invalidates every suite: the base classes and the test +# helpers are imported by all of them, and the setup scripts build the venv the +# suites run in. +SHARED_PATHS=( + stackstate_checks_base/ + stackstate_checks_dev/ + stackstate_checks_tests_helper/ + .setup-scripts/ + .github/workflows/checks-tests.yml + .github/scripts/select-checks.sh +) + +emit() { + local -a selected=("$@") + local json + if [ "${#selected[@]}" -eq 0 ]; then + json="[]" + else + json=$(printf '%s\n' "${selected[@]}" | sort -u | jq -R . | jq -c -s .) + fi + echo "checks=${json}" >>"${GITHUB_OUTPUT}" + echo "Selected suites: ${json}" +} + +# Anything that is not a pull request is a full run. On the release branch the +# whole matrix is the point (it is what Cerberus reports on), and a manual +# dispatch is an explicit request for everything. +if [ "${EVENT_NAME}" != "pull_request" ]; then + echo "Event '${EVENT_NAME}' is not a pull request: running every suite." + emit "${CHECKS[@]}" + exit 0 +fi + +# Diffing against the merge base keeps a stale base branch from dragging +# unrelated commits into the change set. +MERGE_BASE=$(git merge-base "origin/${BASE_REF}" HEAD) +mapfile -t CHANGED < <(git diff --name-only "${MERGE_BASE}" HEAD) + +echo "Changed files (${#CHANGED[@]}) against ${BASE_REF} @ ${MERGE_BASE}:" +printf ' %s\n' "${CHANGED[@]}" + +matches_prefix() { + local file=$1 prefix + shift + for prefix in "$@"; do + case "${file}" in + "${prefix}"*) return 0 ;; + esac + done + return 1 +} + +for file in "${CHANGED[@]}"; do + if matches_prefix "${file}" "${SHARED_PATHS[@]}"; then + echo "'${file}' is shared CI or library code: running every suite." + emit "${CHECKS[@]}" + exit 0 + fi +done + +SELECTED=() +for file in "${CHANGED[@]}"; do + for check in "${CHECKS[@]}"; do + if [ "${file#"${check}"/}" != "${file}" ]; then + SELECTED+=("${check}") + fi + done +done + +emit "${SELECTED[@]+"${SELECTED[@]}"}" diff --git a/.github/workflows/checks-tests.yml b/.github/workflows/checks-tests.yml new file mode 100644 index 00000000..d40905a8 --- /dev/null +++ b/.github/workflows/checks-tests.yml @@ -0,0 +1,214 @@ +name: Check tests + +# Ported from .gitlab-ci.yml as part of the GitLab -> GitHub migration +# (STAC-25142 / STAC-25463), phase 1: the pure-Python check suites. +# +# WHAT MOVED +# linux_deps + the `test_` job family -> the `check-tests` matrix below. +# The per-job `changes:` rules -> .github/scripts/select-checks.sh. +# The validate suite that rode along inside `test_stackstate_checks_base` +# -> its own `validate` job, so a metadata +# failure is legible as its own PR check +# instead of hiding inside a test job. +# +# WHAT IS DELIBERATELY NOT HERE +# splunk_{base,health,metric,topology} and stackstate_checks_dev +# The only five suites that need a Docker daemon (the four splunk suites +# drive a real Splunk container via docker-compose; checks_dev tests the +# toolkit's own Docker helpers). Both public ARC runners ship a DinD +# sidecar, so this is about exercising that path rather than provisioning +# anything -- phase 2. +# test_postgres +# Dead config: .gitlab-ci.yml tests a `postgres` check that does not exist +# in this repository. Dropped, not pending. +# print_env +# A bare `printenv`. This repository is PUBLIC, so that job publishes every +# CI credential in scope to a world-readable log. Not ported at any phase; +# `secrets: inherit` is likewise never used here. +# The Windows lane +# There is none to port. `.gitlab-ci.yml` defines a `.windows_env` anchor +# but no job has ever referenced it, and Windows is not a supported target, +# so the orphaned .setup-scripts/conda_env.ps1 + windows_*.cmd helpers can +# be retired with the GitLab pipeline (STAC-25464). +# publish-checks-dev / the runner-image `docker` job +# Publishing needs write credentials this repo does not hold; pulumi-infra +# schedules integrations' publishing role for its section 7.4. Until then +# releases keep running from GitLab, so the GitLab pipeline stays live. +# A Cerberus failure notification +# Unlike stackstate-agent, this pipeline has never had one -- there is no +# notify job in .gitlab-ci.yml and no .cerberus directory -- so adding it +# would be new capability, not a port. It also needs CERBERUS_LAMBDA_URL, +# which is a private-visibility org secret and so unreadable from this +# PUBLIC repo without a pulumi-infra grant. Tracked as a follow-up. +# +# CREDENTIALS +# Pulling the runner image needs vars.REGISTRY_HOST / vars.REGISTRY_USER and +# secrets.REGISTRY_PASSWORD, which are org-level visibility=all (STAC-25350) +# and therefore already reach this PUBLIC repo -- phase 1 needs no new secret +# provisioning. Note it composes vars.REGISTRY_HOST + /docker rather than using +# vars.REGISTRY_DOCKER_URL: that variable is private-visibility and so is NOT +# readable here. Org secrets are never exposed to fork PRs, hence the same-repo +# guard on every job that pulls the image. + +on: + pull_request: + # Mirrors GitLab's `master_branch` rule, which hardcoded the release branch the + # same way: the full matrix runs there regardless of what a given commit + # touched, so the branch always has a complete verdict. + push: + branches: + - stackstate-7.78.2 + workflow_dispatch: + +permissions: + contents: read + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +defaults: + run: + shell: bash + +env: + # Tag of the prebuilt test image. Kept in lockstep with RUNNER_IMAGE_TAG in + # .gitlab-ci.yml until the GitLab pipeline is retired; the image is still built + # and published by that pipeline's `docker` job. + RUNNER_IMAGE_TAG: 20260625-py313 + +jobs: + select-checks: + name: Select check suites to run + # Not strictly credential-bound, but there is nothing to select for a fork PR + # whose downstream jobs cannot pull the image anyway. + if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository + runs-on: docker-public + timeout-minutes: 10 + outputs: + checks: ${{ steps.select.outputs.checks }} + runner_image: ${{ steps.image.outputs.ref }} + steps: + - name: Check out repository + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + # Full history: the selector diffs against the merge base with the base + # branch, which a shallow clone cannot resolve. + fetch-depth: 0 + persist-credentials: false + + - name: Resolve the runner image reference + id: image + env: + REGISTRY_HOST: ${{ vars.REGISTRY_HOST }} + run: | + set -euo pipefail + # Same image the GitLab pipeline runs on, via the same read-only proxy: + # GitLab's ${REGISTRY_DOCKER_URL} is registry.tooling.stackstate.io/docker. + echo "ref=${REGISTRY_HOST}/docker/stackstate/stackstate-agent-integrations-runner:${RUNNER_IMAGE_TAG}" >>"${GITHUB_OUTPUT}" + + - name: Select check suites + id: select + env: + EVENT_NAME: ${{ github.event_name }} + BASE_REF: ${{ github.base_ref }} + run: | + set -euo pipefail + # The ARC work volume is owned by the runner uid; mark it safe so the + # selector's git calls are not rejected as "dubious ownership". + git config --global --add safe.directory '*' + bash .github/scripts/select-checks.sh + + validate: + name: Check metadata validation (checksdev validate) + # Ported from the `checksdev validate *` commands that opened + # test_stackstate_checks_base. Cheap, repo-wide, and independent of the + # matrix, so it runs on every change rather than per suite. + if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository + needs: select-checks + runs-on: docker-public + timeout-minutes: 30 + container: + # Tag-pinned rather than digest-pinned: the tag is produced by the GitLab + # `docker` job and updated in lockstep with .gitlab-ci.yml, so pinning a + # digest here would silently drift from the pipeline that builds it. + image: ${{ needs.select-checks.outputs.runner_image }} # zizmor: ignore[unpinned-images] + credentials: + username: ${{ vars.REGISTRY_USER }} + password: ${{ secrets.REGISTRY_PASSWORD }} + steps: + - name: Check out repository + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + persist-credentials: false + + - name: Build the toolchain virtualenv + run: | + set -eo pipefail + git config --global --add safe.directory '*' + # Creates venv/ and installs checksdev; the GitLab `linux_deps` job did + # this once and shipped venv/ as an artifact. Here each job builds its + # own: the matrix legs run in parallel, so repeating it costs runner + # time but no wall-clock, and it avoids relocating a venv through the + # artifact store. Worth revisiting with real timings, the way the + # agent's cache image was justified (STAC-25429). + source .setup-scripts/setup_env.sh + + - name: checksdev validate + run: | + set -eo pipefail + source venv/bin/activate + checksdev validate config + checksdev validate dep + checksdev validate manifest --include-extras + checksdev validate metadata + checksdev validate service-checks + + check-tests: + name: Check tests (${{ matrix.check }}) + if: >- + ${{ (github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository) + && needs.select-checks.outputs.checks != '[]' }} + needs: select-checks + runs-on: docker-public + timeout-minutes: 45 + strategy: + # One suite's failure should not mask the state of the other fourteen. + fail-fast: false + matrix: + check: ${{ fromJson(needs.select-checks.outputs.checks) }} + container: + image: ${{ needs.select-checks.outputs.runner_image }} # zizmor: ignore[unpinned-images] + credentials: + username: ${{ vars.REGISTRY_USER }} + password: ${{ secrets.REGISTRY_PASSWORD }} + steps: + - name: Check out repository + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + persist-credentials: false + + - name: Build the toolchain virtualenv + run: | + set -eo pipefail + git config --global --add safe.directory '*' + source .setup-scripts/setup_env.sh + + - name: checksdev test ${{ matrix.check }} + env: + CHECK: ${{ matrix.check }} + run: | + set -eo pipefail + source venv/bin/activate + checksdev test --cov "${CHECK}" + + - name: checksdev benchmarks ${{ matrix.check }} + env: + CHECK: ${{ matrix.check }} + # Non-blocking, matching GitLab's `|| true`: benchmarks are reported for + # information and have never gated a merge. + continue-on-error: true + run: | + set -eo pipefail + source venv/bin/activate + checksdev test "${CHECK}" --bench From 1e5640c9da10464b7da349ccabc9f9fb61cdb739 Mon Sep 17 00:00:00 2001 From: Louis Parkin Date: Mon, 3 Aug 2026 15:44:33 +0200 Subject: [PATCH 2/7] STAC-25463 Re-trigger CI now that the public runner group grant is applied pulumi-infra #262 is merged and applied (~ 1 updated), so docker-public can now pick up jobs from this repository. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> From 7f95e232c4e498050277ba5f1711dd5e654379f8 Mon Sep 17 00:00:00 2001 From: Louis Parkin Date: Mon, 3 Aug 2026 16:25:58 +0200 Subject: [PATCH 3/7] STAC-25463: configure the private PyPI index before running check tests The GitLab `.linux_test` anchor ran .setup-scripts/setup_artifact_registry.sh immediately before `checksdev test`, and the port dropped it. Fourteen of the fifteen phase-1 suites resolve everything from public PyPI and did not notice, but the vsphere suite pins vsphere-automation-sdk==1.82.0, which public PyPI does not carry (only a 0.0.1 placeholder), so it failed with: ERROR: Could not find a version that satisfies the requirement vsphere-automation-sdk==1.82.0 (from versions: 0.0.1) Restores the step in the same position as GitLab had it. The script writes ~/.pip/pip.conf and a 0600 ~/.netrc, so credentials never appear in the index URL. The step is guarded on the credentials being present rather than run unconditionally: only one suite actually needs the private index, so an absent credential should not fail the fourteen that do not. When it is missing the job emits a warning annotation naming the consequence, and the affected suite still fails visibly on its own unresolvable pin. Requires the pull-only grant in StackVista/pulumi-infra#263. setup_artifactory_docker.sh stays out; it is a registry docker login that only the deferred Docker suites need, and it comes across with them in phase 2. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .github/workflows/checks-tests.yml | 47 ++++++++++++++++++++++++++---- 1 file changed, 41 insertions(+), 6 deletions(-) diff --git a/.github/workflows/checks-tests.yml b/.github/workflows/checks-tests.yml index d40905a8..17ee86c6 100644 --- a/.github/workflows/checks-tests.yml +++ b/.github/workflows/checks-tests.yml @@ -17,7 +17,9 @@ name: Check tests # drive a real Splunk container via docker-compose; checks_dev tests the # toolkit's own Docker helpers). Both public ARC runners ship a DinD # sidecar, so this is about exercising that path rather than provisioning -# anything -- phase 2. +# anything -- phase 2. That phase also brings across +# .setup-scripts/setup_artifactory_docker.sh (the registry docker login) +# and COMPOSE_HTTP_TIMEOUT, which only those suites need. # test_postgres # Dead config: .gitlab-ci.yml tests a `postgres` check that does not exist # in this repository. Dropped, not pending. @@ -44,11 +46,19 @@ name: Check tests # CREDENTIALS # Pulling the runner image needs vars.REGISTRY_HOST / vars.REGISTRY_USER and # secrets.REGISTRY_PASSWORD, which are org-level visibility=all (STAC-25350) -# and therefore already reach this PUBLIC repo -- phase 1 needs no new secret -# provisioning. Note it composes vars.REGISTRY_HOST + /docker rather than using -# vars.REGISTRY_DOCKER_URL: that variable is private-visibility and so is NOT -# readable here. Org secrets are never exposed to fork PRs, hence the same-repo -# guard on every job that pulls the image. +# and therefore already reach this PUBLIC repo. Note it composes +# vars.REGISTRY_HOST + /docker rather than using vars.REGISTRY_DOCKER_URL: that +# variable is private-visibility and so is NOT readable here. Org secrets are +# never exposed to fork PRs, hence the same-repo guard on every job that pulls +# the image. +# +# Resolving check requirements additionally needs the private PyPI index, for +# the handful of pins that are not on public PyPI. That is +# vars.GITLAB_PACKAGE_REGISTRY_PYPI_SIMPLE_URL and +# secrets.GITLAB_PACKAGE_REGISTRY_USER, granted to this repo in pulumi-infra +# (StackVista/pulumi-infra#263), alongside the already-org-wide +# secrets.GITLAB_PACKAGE_REGISTRY_READONLY_PASSWORD. Pull-only: this repo's +# *publishing* role is still deferred, per the note above. on: pull_request: @@ -194,6 +204,31 @@ jobs: git config --global --add safe.directory '*' source .setup-scripts/setup_env.sh + - name: Configure the private PyPI index (GitLab Package Registry) + env: + GITLAB_PACKAGE_REGISTRY_PYPI_SIMPLE_URL: ${{ vars.GITLAB_PACKAGE_REGISTRY_PYPI_SIMPLE_URL }} + GITLAB_PACKAGE_REGISTRY_USER: ${{ secrets.GITLAB_PACKAGE_REGISTRY_USER }} + GITLAB_PACKAGE_REGISTRY_READONLY_PASSWORD: ${{ secrets.GITLAB_PACKAGE_REGISTRY_READONLY_PASSWORD }} + # A few checks pin wheels that are not on public PyPI and are served from the + # central registry instead (currently only vsphere, which pins + # vsphere-automation-sdk). This mirrors what the GitLab `.linux_test` anchor did + # immediately before `checksdev test`. The script writes ~/.pip/pip.conf and a + # 0600 ~/.netrc, keeping credentials out of the index URL. + # + # Guarded rather than unconditional: only one suite actually needs the private + # index, so a missing credential should not fail the fourteen that resolve + # everything from public PyPI. When it is missing we say so loudly, and the + # affected suite still fails visibly on its own unresolvable pin. + run: | + set -eo pipefail + if [ -z "${GITLAB_PACKAGE_REGISTRY_PYPI_SIMPLE_URL}" ] \ + || [ -z "${GITLAB_PACKAGE_REGISTRY_USER}" ] \ + || [ -z "${GITLAB_PACKAGE_REGISTRY_READONLY_PASSWORD}" ]; then + echo "::warning title=Private PyPI index not configured::GitLab Package Registry credentials are not available to this job. Checks that pin wheels absent from public PyPI (currently 'vsphere') will fail to resolve their requirements." + exit 0 + fi + .setup-scripts/setup_artifact_registry.sh + - name: checksdev test ${{ matrix.check }} env: CHECK: ${{ matrix.check }} From 29b6c54d1ae38ae193bd27eabf41e15013f25558 Mon Sep 17 00:00:00 2001 From: Louis Parkin Date: Tue, 4 Aug 2026 09:06:10 +0200 Subject: [PATCH 4/7] STAC-25463: re-trigger CI now that the PyPI pull credentials are provisioned pulumi-infra#263 merged and applied (+ 2 created), so GITLAB_PACKAGE_REGISTRY_PYPI_SIMPLE_URL and GITLAB_PACKAGE_REGISTRY_USER now resolve in this repo. Expecting the vsphere suite to resolve its vsphere-automation-sdk pin and the matrix to go fully green. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> From 7800da092a111c96a81c3d787d2c17ba97303b3a Mon Sep 17 00:00:00 2001 From: Louis Parkin Date: Tue, 4 Aug 2026 09:33:24 +0200 Subject: [PATCH 5/7] STAC-25463: defer the vsphere suite; its SDK is not served by any index vsphere pins vsphere-automation-sdk==1.82.0. Public PyPI serves only a 0.0.1 placeholder, and with the private index now provisioned and configured the result is unchanged: pip still reports "from versions: 0.0.1". The index itself is fine. setup_artifact_registry.sh runs and writes its config, pip reports no authentication error, and the same credentials and project are used by stackstate-agent, whose DEB build resolves against them. So the registry is reachable and simply does not carry this package. requirements.in says the wheel was to be built and published to artifactory.tooling.stackstate.io, which has since been retired in favour of the GitLab package registry. Whether the wheel was migrated, and to which project, needs GitLab API access to settle, so it is tracked separately rather than left failing in this PR. Phase 1 therefore ships the fourteen suites that resolve entirely from public PyPI. The registry step stays: it is correct, proven to work, and the deferred Docker suites will need it. Also drops a stale reference to Cerberus in the selector, which this pipeline deliberately does not wire up. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .github/scripts/select-checks.sh | 22 +++++++++++++++++----- .github/workflows/checks-tests.yml | 29 ++++++++++++++++++----------- 2 files changed, 35 insertions(+), 16 deletions(-) diff --git a/.github/scripts/select-checks.sh b/.github/scripts/select-checks.sh index e0683e8f..c77a73cc 100755 --- a/.github/scripts/select-checks.sh +++ b/.github/scripts/select-checks.sh @@ -24,8 +24,9 @@ set -euo pipefail -# Suites currently running on GitHub Actions. Phase 1 is the 15 pure-Python -# suites: they need no Docker daemon, so they are the low-risk half of the port. +# Suites currently running on GitHub Actions. Phase 1 is the 14 pure-Python +# suites that resolve entirely from public PyPI, so they need neither a Docker +# daemon nor the private package index. # # Deliberately NOT here yet (phase 2, needs the DinD story exercised first): # splunk_base, splunk_health, splunk_metric, splunk_topology @@ -35,6 +36,17 @@ set -euo pipefail # Both public ARC runners provide a DinD sidecar, so this is a matter of proving # it rather than provisioning anything. # +# vsphere +# -- blocked on sourcing vsphere-automation-sdk==1.82.0, which public PyPI +# does not carry (it serves only a 0.0.1 placeholder). The private index +# is wired up correctly and authenticates -- pip reports no 401 and the +# same credentials work for stackstate-agent -- but the registry does not +# serve this package either, so pip still sees only 0.0.1. requirements.in +# says the wheel was to be built and published to +# artifactory.tooling.stackstate.io, which has since been retired, so +# where it lives now is an open question that needs GitLab API access to +# settle. Tracked separately rather than left failing here. +# # Deliberately dropped, not pending: # postgres -- .gitlab-ci.yml carried a `test_postgres` job for a check that does # not exist in this repository. It is dead config, not a gap. @@ -52,7 +64,6 @@ CHECKS=( stackstate_checks_base static_health static_topology - vsphere zabbix ) @@ -81,8 +92,9 @@ emit() { } # Anything that is not a pull request is a full run. On the release branch the -# whole matrix is the point (it is what Cerberus reports on), and a manual -# dispatch is an explicit request for everything. +# whole matrix is the point -- the branch should always carry a complete verdict, +# regardless of what a given commit touched -- and a manual dispatch is an +# explicit request for everything. if [ "${EVENT_NAME}" != "pull_request" ]; then echo "Event '${EVENT_NAME}' is not a pull request: running every suite." emit "${CHECKS[@]}" diff --git a/.github/workflows/checks-tests.yml b/.github/workflows/checks-tests.yml index 17ee86c6..7475d6e4 100644 --- a/.github/workflows/checks-tests.yml +++ b/.github/workflows/checks-tests.yml @@ -23,6 +23,14 @@ name: Check tests # test_postgres # Dead config: .gitlab-ci.yml tests a `postgres` check that does not exist # in this repository. Dropped, not pending. +# vsphere +# Blocked upstream, not by this port. It pins vsphere-automation-sdk==1.82.0, +# which public PyPI does not serve (only a 0.0.1 placeholder) and which the +# private index does not serve either -- pip reports no auth error and the +# same credentials work for stackstate-agent, so the index is reachable and +# simply does not carry it. requirements.in says the wheel was to be built and +# published to the since-retired artifactory.tooling.stackstate.io, so where it +# lives now needs settling before this suite can run anywhere. # print_env # A bare `printenv`. This repository is PUBLIC, so that job publishes every # CI credential in scope to a world-readable log. Not ported at any phase; @@ -183,7 +191,7 @@ jobs: runs-on: docker-public timeout-minutes: 45 strategy: - # One suite's failure should not mask the state of the other fourteen. + # One suite's failure should not mask the state of the others. fail-fast: false matrix: check: ${{ fromJson(needs.select-checks.outputs.checks) }} @@ -209,22 +217,21 @@ jobs: GITLAB_PACKAGE_REGISTRY_PYPI_SIMPLE_URL: ${{ vars.GITLAB_PACKAGE_REGISTRY_PYPI_SIMPLE_URL }} GITLAB_PACKAGE_REGISTRY_USER: ${{ secrets.GITLAB_PACKAGE_REGISTRY_USER }} GITLAB_PACKAGE_REGISTRY_READONLY_PASSWORD: ${{ secrets.GITLAB_PACKAGE_REGISTRY_READONLY_PASSWORD }} - # A few checks pin wheels that are not on public PyPI and are served from the - # central registry instead (currently only vsphere, which pins - # vsphere-automation-sdk). This mirrors what the GitLab `.linux_test` anchor did - # immediately before `checksdev test`. The script writes ~/.pip/pip.conf and a - # 0600 ~/.netrc, keeping credentials out of the index URL. + # Mirrors what the GitLab `.linux_test` anchor did immediately before + # `checksdev test`. The script writes ~/.pip/pip.conf and a 0600 ~/.netrc, + # keeping credentials out of the index URL. # - # Guarded rather than unconditional: only one suite actually needs the private - # index, so a missing credential should not fail the fourteen that resolve - # everything from public PyPI. When it is missing we say so loudly, and the - # affected suite still fails visibly on its own unresolvable pin. + # No phase-1 suite depends on this today -- all fourteen resolve from public + # PyPI -- but the deferred Docker suites will, and keeping it here means the + # index is configured wherever `checksdev test` runs. Guarded rather than + # unconditional so an absent credential cannot fail suites that do not use it; + # when it is missing we say so loudly rather than failing obscurely later. run: | set -eo pipefail if [ -z "${GITLAB_PACKAGE_REGISTRY_PYPI_SIMPLE_URL}" ] \ || [ -z "${GITLAB_PACKAGE_REGISTRY_USER}" ] \ || [ -z "${GITLAB_PACKAGE_REGISTRY_READONLY_PASSWORD}" ]; then - echo "::warning title=Private PyPI index not configured::GitLab Package Registry credentials are not available to this job. Checks that pin wheels absent from public PyPI (currently 'vsphere') will fail to resolve their requirements." + echo "::warning title=Private PyPI index not configured::GitLab Package Registry credentials are not available to this job. Any check pinning a wheel that public PyPI does not serve will fail to resolve its requirements." exit 0 fi .setup-scripts/setup_artifact_registry.sh From ecad1da9554c390027f52685be826eb30131096c Mon Sep 17 00:00:00 2001 From: Louis Parkin Date: Tue, 4 Aug 2026 11:49:12 +0200 Subject: [PATCH 6/7] STAC-25463 Run the vsphere suite: pass HOME through tox so pip sees the private index The suite was deferred on the theory that vsphere-automation-sdk==1.82.0 was unavailable. It is not: the wheel is published in the central registry (GitLab project 71271774) at exactly the pinned version, alongside its vapi/nsx dependencies. The real cause is that pip never contacted the index at all. tox drops every variable a testenv does not name in `passenv`, HOME among them, and pip then resolves `~` from the passwd database instead of the environment. A container job on GitHub Actions runs with HOME=/github/home, so setup_artifact_registry.sh wrote /github/home/.pip/pip.conf while pip inside tox read /root/.pip/pip.conf -- absent -- and quietly resolved from public PyPI alone, which serves only a 0.0.1 placeholder for that name. That is also why the failure carried no 401: there was no request to authenticate. And why GitLab is unaffected: its container runs as root with HOME=/root, so the passwd fallback lands on the same path the script wrote. Adding HOME to the vsphere testenv's passenv is enough. Re-enables the suite, taking phase 1 from 14 to 15, and records the trap next to both the setup step and the passenv entry so the deferred Docker suites do not rediscover it. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .github/scripts/select-checks.sh | 17 +++-------------- .github/workflows/checks-tests.yml | 26 +++++++++++++------------- vsphere/tox.ini | 8 ++++++++ 3 files changed, 24 insertions(+), 27 deletions(-) diff --git a/.github/scripts/select-checks.sh b/.github/scripts/select-checks.sh index c77a73cc..870b4b61 100755 --- a/.github/scripts/select-checks.sh +++ b/.github/scripts/select-checks.sh @@ -24,9 +24,8 @@ set -euo pipefail -# Suites currently running on GitHub Actions. Phase 1 is the 14 pure-Python -# suites that resolve entirely from public PyPI, so they need neither a Docker -# daemon nor the private package index. +# Suites currently running on GitHub Actions. Phase 1 is the 15 suites that need +# no Docker daemon. # # Deliberately NOT here yet (phase 2, needs the DinD story exercised first): # splunk_base, splunk_health, splunk_metric, splunk_topology @@ -36,17 +35,6 @@ set -euo pipefail # Both public ARC runners provide a DinD sidecar, so this is a matter of proving # it rather than provisioning anything. # -# vsphere -# -- blocked on sourcing vsphere-automation-sdk==1.82.0, which public PyPI -# does not carry (it serves only a 0.0.1 placeholder). The private index -# is wired up correctly and authenticates -- pip reports no 401 and the -# same credentials work for stackstate-agent -- but the registry does not -# serve this package either, so pip still sees only 0.0.1. requirements.in -# says the wheel was to be built and published to -# artifactory.tooling.stackstate.io, which has since been retired, so -# where it lives now is an open question that needs GitLab API access to -# settle. Tracked separately rather than left failing here. -# # Deliberately dropped, not pending: # postgres -- .gitlab-ci.yml carried a `test_postgres` job for a check that does # not exist in this repository. It is dead config, not a gap. @@ -64,6 +52,7 @@ CHECKS=( stackstate_checks_base static_health static_topology + vsphere zabbix ) diff --git a/.github/workflows/checks-tests.yml b/.github/workflows/checks-tests.yml index 7475d6e4..70138097 100644 --- a/.github/workflows/checks-tests.yml +++ b/.github/workflows/checks-tests.yml @@ -23,14 +23,6 @@ name: Check tests # test_postgres # Dead config: .gitlab-ci.yml tests a `postgres` check that does not exist # in this repository. Dropped, not pending. -# vsphere -# Blocked upstream, not by this port. It pins vsphere-automation-sdk==1.82.0, -# which public PyPI does not serve (only a 0.0.1 placeholder) and which the -# private index does not serve either -- pip reports no auth error and the -# same credentials work for stackstate-agent, so the index is reachable and -# simply does not carry it. requirements.in says the wheel was to be built and -# published to the since-retired artifactory.tooling.stackstate.io, so where it -# lives now needs settling before this suite can run anywhere. # print_env # A bare `printenv`. This repository is PUBLIC, so that job publishes every # CI credential in scope to a world-readable log. Not ported at any phase; @@ -221,11 +213,19 @@ jobs: # `checksdev test`. The script writes ~/.pip/pip.conf and a 0600 ~/.netrc, # keeping credentials out of the index URL. # - # No phase-1 suite depends on this today -- all fourteen resolve from public - # PyPI -- but the deferred Docker suites will, and keeping it here means the - # index is configured wherever `checksdev test` runs. Guarded rather than - # unconditional so an absent credential cannot fail suites that do not use it; - # when it is missing we say so loudly rather than failing obscurely later. + # `vsphere` is the one phase-1 suite that needs this; the deferred Docker + # suites will too. Guarded rather than unconditional so an absent credential + # cannot fail the suites that do not use it; when it is missing we say so + # loudly rather than failing obscurely later. + # + # NOTE: writing these under $HOME is only half the job. tox drops every + # variable absent from a testenv's `passenv`, HOME included, and pip then + # resolves `~` from the passwd database rather than the environment. Because + # a container job here runs with HOME=/github/home instead of the account + # default, a testenv that installs from this index must list HOME in its + # passenv or it will silently read a pip.conf that was never written and fall + # back to public PyPI alone -- with no auth error to show for it. See + # vsphere/tox.ini. run: | set -eo pipefail if [ -z "${GITLAB_PACKAGE_REGISTRY_PYPI_SIMPLE_URL}" ] \ diff --git a/vsphere/tox.ini b/vsphere/tox.ini index 3b0e5544..a4b86bec 100644 --- a/vsphere/tox.ini +++ b/vsphere/tox.ini @@ -14,9 +14,17 @@ deps = setuptools<78 -e../stackstate_checks_base[deps] -rrequirements-dev.txt +; vsphere-automation-sdk is pinned to a build that only exists in the private +; GitLab package index, so `pip install -r requirements.in` below has to see the +; ~/.pip/pip.conf and ~/.netrc that .setup-scripts/setup_artifact_registry.sh +; writes. tox drops every variable not listed in passenv, and pip then resolves +; `~` from the passwd database instead of the environment -- which silently points +; at the wrong home whenever HOME is not the account's default, as in a GitHub +; Actions container job where HOME is /github/home. Hence HOME below. passenv = DOCKER* COMPOSE* + HOME commands = pip install -r requirements.in pip install -r requirements-dev.txt From bea6ddf8bbf2561192fc6bf5c7f893b41f9657b3 Mon Sep 17 00:00:00 2001 From: Louis Parkin Date: Tue, 4 Aug 2026 13:42:32 +0200 Subject: [PATCH 7/7] STAC-25463 Address review: BCI image, credential-free PR runs, stable gate, tag coverage Four findings from review, in order. Tag coverage. GitLab's .base_integration_rules includes the release_branch anchor (if: $CI_COMMIT_TAG), so every test job also ran on tag pipelines. The workflow declared only push.branches and silently lost that. Adds a tags filter using '**' rather than '*', which would skip any tag name containing a slash. The selector already treats every non-pull_request event as a full run, so no change was needed there. BCI base image, which also removes the credentials. The GitLab runner image is FROM python:3.13.14-bookworm, against the mandatory SUSE BCI policy. That image exists mainly to carry Docker CE and docker-compose, which only the phase-2 Splunk and checks_dev suites use -- phase 1 needs a Python toolchain and nothing else. Switches the container to a digest-pinned registry.suse.com/bci/python, adding gcc, python313-devel and libffi-devel, which the image does not ship and which cython and pyyaml==6.0.1 need to compile from source. Because that registry is public, the jobs no longer need vars.REGISTRY_USER or secrets.REGISTRY_PASSWORD. This is the substantive part of the credential finding: every job here runs PR-authored workflow, setup and test code on a PUBLIC repository, so a secret reachable from that code is reachable by anyone who can open a branch. It also unblocks Dependabot, which receives no Actions secrets and so could never have pulled the private image. What remains is the read-only private PyPI pull, scoped to the one step that needs it and guarded so its absence warns rather than fails. The fork boundary stays, now justified by running unreviewed code on self-hosted runners rather than by secret exposure. BCI ships 3.13.13 against the 3.13.14 the agent embeds. Patch releases are bugfix-only, and the full matrix including vsphere against the private index was verified green on 3.13.13 before making the switch. Stable required check. The only test statuses were dynamically named matrix legs in a job that disappears when the selector returns an empty array, which branch protection cannot depend on. Adds a ci-success job that always runs and aggregates the results, treating skipped as legitimate but failure and cancelled as fatal, plus the requested Zizmor job. ci-success deliberately checks out nothing so it remains safe on fork PRs, where it is the only way a required check can report at all. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .github/workflows/checks-tests.yml | 200 +++++++++++++++++++++++------ 1 file changed, 164 insertions(+), 36 deletions(-) diff --git a/.github/workflows/checks-tests.yml b/.github/workflows/checks-tests.yml index 70138097..c45ec075 100644 --- a/.github/workflows/checks-tests.yml +++ b/.github/workflows/checks-tests.yml @@ -19,7 +19,10 @@ name: Check tests # sidecar, so this is about exercising that path rather than provisioning # anything -- phase 2. That phase also brings across # .setup-scripts/setup_artifactory_docker.sh (the registry docker login) -# and COMPOSE_HTTP_TIMEOUT, which only those suites need. +# and COMPOSE_HTTP_TIMEOUT, which only those suites need. It will also need +# a docker client, which the BCI Python image used here does not carry -- +# either a BCI image with docker added, or a DinD service container, rather +# than a return to the private python:3.13.14-bookworm runner image. # test_postgres # Dead config: .gitlab-ci.yml tests a `postgres` check that does not exist # in this repository. Dropped, not pending. @@ -44,21 +47,29 @@ name: Check tests # PUBLIC repo without a pulumi-infra grant. Tracked as a follow-up. # # CREDENTIALS -# Pulling the runner image needs vars.REGISTRY_HOST / vars.REGISTRY_USER and -# secrets.REGISTRY_PASSWORD, which are org-level visibility=all (STAC-25350) -# and therefore already reach this PUBLIC repo. Note it composes -# vars.REGISTRY_HOST + /docker rather than using vars.REGISTRY_DOCKER_URL: that -# variable is private-visibility and so is NOT readable here. Org secrets are -# never exposed to fork PRs, hence the same-repo guard on every job that pulls -# the image. +# The container image is SUSE BCI from registry.suse.com, which is public, so +# these jobs need no registry credentials at all. That is deliberate: this is a +# PUBLIC repository, and every job here executes PR-authored workflow, setup and +# test code. Any secret exposed to that code is exposed to whoever can open a +# branch. The earlier design pulled a private runner image with +# vars.REGISTRY_USER / secrets.REGISTRY_PASSWORD; dropping it removes the +# registry password from the PR path entirely and, as a side effect, lets +# Dependabot PRs run -- they receive no Actions secrets, so the image pull +# could never have succeeded for them. # -# Resolving check requirements additionally needs the private PyPI index, for -# the handful of pins that are not on public PyPI. That is +# One credential remains, and only on the step that needs it: the read-only +# pull from the private PyPI index, for pins that public PyPI does not serve +# (currently vsphere-automation-sdk). That is # vars.GITLAB_PACKAGE_REGISTRY_PYPI_SIMPLE_URL and # secrets.GITLAB_PACKAGE_REGISTRY_USER, granted to this repo in pulumi-infra # (StackVista/pulumi-infra#263), alongside the already-org-wide -# secrets.GITLAB_PACKAGE_REGISTRY_READONLY_PASSWORD. Pull-only: this repo's -# *publishing* role is still deferred, per the note above. +# secrets.GITLAB_PACKAGE_REGISTRY_READONLY_PASSWORD. It is pull-only and +# least-privilege by construction; this repo's *publishing* role is still +# deferred, per the note above. The step is guarded so its absence degrades to +# a warning rather than failing suites that do not use the index. +# +# Fork PRs are still excluded from the credentialed jobs. Secret exposure is no +# longer the reason -- running unreviewed code on self-hosted runners is. on: pull_request: @@ -68,6 +79,13 @@ on: push: branches: - stackstate-7.78.2 + # GitLab's `release_branch` anchor (`if: $CI_COMMIT_TAG`) put every test job + # in .base_integration_rules on tag pipelines too, so releases carry the same + # verdict as the branch they cut from. Tags here are `-` + # (7.78.2-4), but the rule was any-tag and this stays faithful to it: `**` + # matches tag names containing `/`, which a bare `*` would silently skip. + tags: + - '**' workflow_dispatch: permissions: @@ -82,22 +100,40 @@ defaults: shell: bash env: - # Tag of the prebuilt test image. Kept in lockstep with RUNNER_IMAGE_TAG in - # .gitlab-ci.yml until the GitLab pipeline is retired; the image is still built - # and published by that pipeline's `docker` job. - RUNNER_IMAGE_TAG: 20260625-py313 + # SUSE BCI Python, per the mandatory BCI base-image policy. Digest-pinned so a + # run is reproducible and so Zizmor's unpinned-images audit passes; the comment + # carries the human-readable version the digest resolves to. + # + # This deliberately replaces the GitLab pipeline's + # stackstate-agent-integrations-runner image (FROM python:3.13.14-bookworm). + # That image exists mainly to carry Docker CE and docker-compose, which only the + # phase-2 Splunk / checks_dev suites use; the phase-1 suites need a Python + # toolchain and nothing else. Dropping it also drops the registry credentials + # these jobs used to need -- see CREDENTIALS below. + # + # BCI publishes 3.13.13, one patch behind the 3.13.14 the agent embeds and the + # GitLab image pinned. CPython patch releases are bugfix-only, and the full + # phase-1 matrix (including vsphere against the private index) was verified + # green on 3.13.13 before this switch. Worth realigning when BCI ships .14. + BCI_PYTHON_IMAGE: registry.suse.com/bci/python:3.13@sha256:7d36dd3ba6596fb690e31d956952059fd010604ad6309f06462c02c4c9c01461 # 3.13.13 + + # Packages the BCI image does not ship but the toolchain build needs: cython and + # pyyaml==6.0.1 have no cp313 wheels and are compiled from source. + BCI_BUILD_PACKAGES: gcc python313-devel libffi-devel jobs: select-checks: name: Select check suites to run - # Not strictly credential-bound, but there is nothing to select for a fork PR - # whose downstream jobs cannot pull the image anyway. + # Fork PRs never run: this is a PUBLIC repository on self-hosted runners, and + # a fork branch is unreviewed code. The jobs no longer carry registry + # credentials, but running arbitrary PR code on our own runners is a separate + # concern from secret exposure, so the boundary stays. if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository runs-on: docker-public timeout-minutes: 10 outputs: checks: ${{ steps.select.outputs.checks }} - runner_image: ${{ steps.image.outputs.ref }} + image: ${{ steps.image.outputs.ref }} steps: - name: Check out repository uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 @@ -107,15 +143,14 @@ jobs: fetch-depth: 0 persist-credentials: false - - name: Resolve the runner image reference + - name: Resolve the container image reference id: image - env: - REGISTRY_HOST: ${{ vars.REGISTRY_HOST }} + # `container.image` cannot read the `env` context, so the pin defined once + # at workflow level is republished here as an output the container jobs can + # reference. Keeps a single source of truth for the digest. run: | set -euo pipefail - # Same image the GitLab pipeline runs on, via the same read-only proxy: - # GitLab's ${REGISTRY_DOCKER_URL} is registry.tooling.stackstate.io/docker. - echo "ref=${REGISTRY_HOST}/docker/stackstate/stackstate-agent-integrations-runner:${RUNNER_IMAGE_TAG}" >>"${GITHUB_OUTPUT}" + echo "ref=${BCI_PYTHON_IMAGE}" >>"${GITHUB_OUTPUT}" - name: Select check suites id: select @@ -139,19 +174,22 @@ jobs: runs-on: docker-public timeout-minutes: 30 container: - # Tag-pinned rather than digest-pinned: the tag is produced by the GitLab - # `docker` job and updated in lockstep with .gitlab-ci.yml, so pinning a - # digest here would silently drift from the pipeline that builds it. - image: ${{ needs.select-checks.outputs.runner_image }} # zizmor: ignore[unpinned-images] - credentials: - username: ${{ vars.REGISTRY_USER }} - password: ${{ secrets.REGISTRY_PASSWORD }} + # Digest-pinned at workflow level (BCI_PYTHON_IMAGE); the ignore is only + # because Zizmor cannot follow the pin through a job output. + image: ${{ needs.select-checks.outputs.image }} # zizmor: ignore[unpinned-images] steps: - name: Check out repository uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: persist-credentials: false + - name: Install toolchain build dependencies + run: | + set -eo pipefail + zypper --non-interactive --gpg-auto-import-keys refresh + # shellcheck disable=SC2086 # deliberately word-split into package args + zypper --non-interactive install ${BCI_BUILD_PACKAGES} + - name: Build the toolchain virtualenv run: | set -eo pipefail @@ -188,16 +226,22 @@ jobs: matrix: check: ${{ fromJson(needs.select-checks.outputs.checks) }} container: - image: ${{ needs.select-checks.outputs.runner_image }} # zizmor: ignore[unpinned-images] - credentials: - username: ${{ vars.REGISTRY_USER }} - password: ${{ secrets.REGISTRY_PASSWORD }} + # Digest-pinned at workflow level (BCI_PYTHON_IMAGE); the ignore is only + # because Zizmor cannot follow the pin through a job output. + image: ${{ needs.select-checks.outputs.image }} # zizmor: ignore[unpinned-images] steps: - name: Check out repository uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: persist-credentials: false + - name: Install toolchain build dependencies + run: | + set -eo pipefail + zypper --non-interactive --gpg-auto-import-keys refresh + # shellcheck disable=SC2086 # deliberately word-split into package args + zypper --non-interactive install ${BCI_BUILD_PACKAGES} + - name: Build the toolchain virtualenv run: | set -eo pipefail @@ -254,3 +298,87 @@ jobs: set -eo pipefail source venv/bin/activate checksdev test "${CHECK}" --bench + + workflow-security: + name: Workflow security scan (Zizmor) + # Same fork boundary as the rest: this repository is PUBLIC and the runners are + # self-hosted, so unreviewed fork code is not checked out here even for a + # read-only audit. Depends on select-checks only to reuse the pinned image. + if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository + needs: select-checks + runs-on: docker-public + timeout-minutes: 15 + container: + # Digest-pinned at workflow level (BCI_PYTHON_IMAGE); the ignore is only + # because Zizmor cannot follow the pin through a job output. + image: ${{ needs.select-checks.outputs.image }} # zizmor: ignore[unpinned-images] + permissions: + contents: read + steps: + - name: Check out repository + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + persist-credentials: false + + - name: Install Zizmor + env: + ZIZMOR_VERSION: 1.28.0 + run: | + set -eo pipefail + python3.13 -m venv /tmp/zizmor-venv + /tmp/zizmor-venv/bin/pip install --quiet "zizmor==${ZIZMOR_VERSION}" + + - name: Zizmor audit + run: | + set -eo pipefail + # Offline: the online audits need a GitHub token, and handing this job a + # token to scan PR-authored workflows is the very shape Zizmor exists to + # catch. Persona `regular` keeps it to findings worth blocking on. + /tmp/zizmor-venv/bin/zizmor \ + --persona regular \ + --collect=workflows,actions \ + . + + ci-success: + name: CI success + # The single stable status for branch protection. Every other status here is + # either dynamically named (the matrix legs are `Check tests ()`) or + # conditional (check-tests is skipped entirely when the selector returns an + # empty array, and everything is skipped for fork PRs). A required check must + # exist on every run, so this job always runs and decides the verdict itself. + # That includes fork PRs, where everything upstream is skipped: without a + # status here a fork PR could never satisfy branch protection. It checks out + # nothing and runs no repository code, so it stays safe to run in that context. + if: always() + needs: + - select-checks + - validate + - check-tests + - workflow-security + runs-on: docker-public + timeout-minutes: 5 + steps: + - name: Evaluate upstream job results + env: + # `skipped` is a legitimate outcome and must not be conflated with + # success: check-tests skips when no suite was selected, and the whole + # pipeline skips for fork PRs. Only `failure` and `cancelled` fail here. + RESULTS: >- + select-checks=${{ needs.select-checks.result }} + validate=${{ needs.validate.result }} + check-tests=${{ needs.check-tests.result }} + workflow-security=${{ needs.workflow-security.result }} + run: | + set -euo pipefail + failed=0 + for entry in ${RESULTS}; do + echo " ${entry}" + case "${entry#*=}" in + failure | cancelled) failed=1 ;; + esac + done + if [ "${failed}" -ne 0 ]; then + echo "::error title=CI failed::One or more required jobs did not succeed (see above)." + exit 1 + fi + echo "All required jobs succeeded or were legitimately skipped."