STAC-25463 Add GitHub Actions check-test workflow (integrations CI migration phase 1) - #362
Conversation
…gration 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_<check>` 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>
…plied 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>
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>
|
|
…isioned 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>
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>
|
|
LouisLotter
left a comment
There was a problem hiding this comment.
Requesting changes for one credential-exposure issue and three CI migration gaps: a stable required gate, release-tag coverage, and BCI compliance.
…he 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>
|
|
… 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>
|
|
Follow-up found while this PR was waiting: branch protection here is broken independently of this changeWhile checking what actually gates merges on this repo, I found the branch protection is in a bad state, and it makes the Verified today:
The GitLab project was archived, so nothing can ever report The release branches have the opposite problem: they require nothing, so the workflow in this PR would run and be ignored by branch protection. The fix (STAC-25522) is to adopt this repo into Worth stating explicitly, because it is the reason the aggregate job exists: branch protection must not require the per-suite Sequencing: STAC-25522 must land after this PR, otherwise protection would require a check that master does not yet produce — recreating the very deadlock it is meant to fix. |
LouisLotter
left a comment
There was a problem hiding this comment.
Three follow-ups remain after the first review: private-PyPI credentials are still exposed to PR-controlled code, the aggregate gate succeeds when all substantive CI is skipped, and the direct BCI pull violates the self-hosted registry-proxy policy.
Addresses the second review on #362. Credential exposure (P1). setup_artifact_registry.sh writes the GitLab Package Registry password to ~/.netrc, which the suite's own PR-authored test code could then read. Scoping the env block to one step never limited that. The suites needing the private index are now split into check-tests-private-index, behind the private-package-index GitHub Environment with dev-team as required reviewers and prevent_self_review enabled, so the credential is released only after someone other than the author approves that run. The other 14 suites no longer reference any secret. select-checks.sh partitions the selection into `checks` and `private_checks` to drive the split. Aggregate gate (P1). ci-success treated every non-failure as success, so a run where everything skipped reported green and could satisfy branch protection with no CI executed. It now requires select-checks, validate and workflow-security to succeed outright, and permits a matrix skip only when the selector chose nothing -- plus, for the private-index matrix only, when the PR is from a fork and could not have had the credential. Runners (P2). Everything moves to GitHub-hosted runners. The phase-1 suites are pure Python and need no Docker daemon, so docker-public bought no capability while forcing fork PRs to be excluded -- which is what made the all-skipped green verdict reachable. Forks now run the full credential-free matrix, and the direct BCI pull no longer crosses the self-hosted NAT. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
|
Thanks @LouisLotter — both P1s were real. I've pushed 863b1cd which restructures the credential handling rather than patching around it. Details per finding: P1 — credentials reachable by PR-controlled code ✅ fixedYou were right that the fork guard wasn't the boundary I implied. Fixed by making the split structural instead of procedural:
So the credential is no longer reachable from a job that runs untrusted code without a human first approving that specific run. Verified live on this PR's run — I also made the registry setup fail loudly ( Residual, and I want to be honest about it rather than overclaim: the secrets are still repo-level ( P1 — aggregate gate green when everything skips ✅ fixedConfirmed and worse than described. The loop only failed on
Anything else fails the gate. Related: I found this repo's branch protection currently requires the P2 — BCI pulled directly rather than through the proxy 🔄 mootThis one I'd have pushed back on, but it's moot now: every job moved to For the record on the original point — the merged precedent in Also worth noting on the Dependabot sub-point: it isn't enabled on this repo, so there's nothing consuming that config today. Runner move — it's fasterSomewhat to my surprise, GitHub-hosted beat the self-hosted lane on this workload, presumably because we weren't queuing behind other jobs:
All 16 credential-free jobs green, no changes needed to the BCI container or Validated with actionlint, shellcheck and Zizmor ( 🙏 One ask: this PR's |
The header claimed "releases keep running from GitLab, so the GitLab pipeline stays live". That is no longer true: the GitLab project (stackvista/agent/stackstate-agent-integrations, id 13169244) was archived on 2026-07-20, so publish-checks-dev cannot run there either. Publishing a new stackstate_checks_dev is currently impossible on any platform, and the five unported suites have no CI at all. Say so plainly rather than leaving a reviewer with a false premise. Also drop the stale claim that phase 2 rides on an ARC DinD sidecar -- every job now runs on ubuntu-latest, which already provides a Docker daemon, so phase 2 only needs a docker client in the job image. Deferred work now carries ticket references instead of vague "phase 2" / "a follow-up" pointers: STAC-25531 the five Docker-daemon suites STAC-25532 publish-checks-dev STAC-25533 Cerberus failure notification Comments only; no behaviour change. actionlint, shellcheck and zizmor clean, selector smoke-tested. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
|
Follow-up commit b1715b4 — comments only, no behaviour change, but it corrects a factual claim reviewers would otherwise rely on. The header said "releases keep running from GitLab, so the GitLab pipeline stays live." That is no longer true. The GitLab project ( Two consequences worth being explicit about, because they change the deferred items from "scheduled" to "currently broken":
Neither is caused by this PR — this PR is what restores coverage for the other 15 — but the header shouldn't imply a safety net that isn't there. I also dropped a second stale claim: phase 2 no longer rides on an ARC DinD sidecar, since every job now runs on Deferred work now carries ticket references rather than vague pointers:
actionlint, shellcheck and Zizmor clean; selector smoke-tested (14 + 1 unchanged). |
|
@LouisLotter apologies — you approved this at 08:19 and I knocked it straight back out at 08:37. This base branch has Rebuilding your approval on a comment-only diff is a poor trade for your time, and I should have batched it with the review response instead of pushing separately. Re-requesting now. Nothing else changed since you approved: still 16/16 credential-free jobs green, and the vsphere job is still gated behind the One thing that still needs a second person regardless of the approval: the run is sitting in |
|
The vsphere suite is the only one that needs a credential: it pins
vsphere-automation-sdk==1.82.0, an unmodified upstream VMware wheel that
VMware withdrew from public PyPI, so it resolves only from our GitLab
Package Registry. Public PyPI serves a 0.0.1 placeholder squatting the
name, which installs cleanly and fails much later.
setup_artifact_registry.sh left the registry password in a 0600 ~/.netrc
for the remainder of the job, so the suite's tests, its tox environment
and its whole dependency tree could read it. Test code is PR-authored,
which made the credential effectively readable by whoever opened the pull
request (STAC-25463 review, P1).
That was contained by putting the job behind the private-package-index
GitHub Environment, releasing the credential only after a human approved
each run. It worked, but SHARED_PATHS covers the CI files, so it fired on
roughly one commit in six and blocked authors on their own pull requests.
Replace it with a narrower fix. fetch_private_wheels.sh writes the netrc,
downloads one hardcoded package set into a wheelhouse under RUNNER_TEMP,
deletes the netrc, and rewrites ~/.pip/pip.conf to resolve that wheelhouse
via find-links. Everything afterwards runs with no credential on disk and
no authenticated index configured, and the environment gate is gone.
Details that matter:
* The package set is hardcoded, not read from the tree: while the
credential is on disk, a pull request must not be able to redirect pip.
* --only-binary=:all:, because downloading an sdist executes its
setup.py and would hand upstream code a readable ~/.netrc.
* A system interpreter, never the toolchain virtualenv, whose pip is
built by repository code. The path is resolved rather than hardcoded
-- bci/python:3.13 ships /usr/bin/python3.13 and no /usr/bin/python3,
and `python3` on PATH is a /usr/local/bin shim -- and the result is
then rejected if it resolves inside GITHUB_WORKSPACE, which is what
makes "system" a guarantee rather than an assumption.
* An EXIT trap revokes the credential on the failure paths too, and the
script then asserts the netrc is gone and the wheel actually arrived.
Rejected: a separate credentialed job publishing the wheels with
upload-artifact. Workflow artifacts on a public repository are anonymously
downloadable, so that would have amounted to publicly redistributing a
vendor artifact VMware withdrew, under no SPDX licence.
Residual risk, recorded in the workflow: a pull request that edits the
workflow itself can still reach the secret, since pull_request runs the
PR's own copy. That holds for every secret in every repository and is
contained by review, not CI. What changed is that ordinary test code and
its dependencies no longer see it.
setup_artifact_registry.sh is untouched; .gitlab-ci.yml still uses it.
Verified: shellcheck and actionlint clean, Zizmor clean (no findings).
Verified inside the pinned BCI image that the interpreter resolves to
/usr/bin/python3.13 and that a workspace-resident interpreter is refused.
Verified that a missing argument, missing credentials, a workspace
interpreter and a failed download all exit non-zero with no netrc left
behind and the password in no file or log; and that find-links in
~/.pip/pip.conf is honoured with no command-line flags, which is what tox
relies on via passenv HOME.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…ndex STAC-25540: fetch private-index wheels, then drop the credential
Status: fully green, and the environment gate is gone@LouisLotter — #364 merged into this branch, so what you'll be reviewing has changed materially since your last pass. Summary of where your P1 now stands. Your finding: I originally took the second. It worked but was the wrong trade: This branch now implements your first remedy instead. Four details are load-bearing, and I'd welcome your eye on them specifically:
Latest run 31009548529: all 19 jobs green, Two things I want to be straight about:
Also, apologies again for dismissing your approval this morning; that was my push, not you. Ready for another look whenever suits. |
The previous revision confined the GitLab package registry credential to a
single step and claimed that reaching it would require editing the workflow.
That claim was wrong. A `pull_request` run executes the pull request's own copy
of the workflow *and* of every script it calls, so a pull request could rewrite
fetch_private_wheels.sh, reorder the steps, or add one of its own. Confining a
secret inside pull-request-controlled code is hardening, not a boundary.
So the private-index suites no longer run on pull requests at all. They run on
push, tag and workflow_dispatch events, whose contents are reviewed before they
reach the release branch, and where the secret is therefore not exposed to
unreviewed code.
* select-checks.sh moves private-index suites to a new
`deferred_private_checks` output on pull requests, so the matrix is empty
rather than gated, and the reason is reported.
* check-tests-private-index is conditioned on the event, replacing the fork
guard that is now redundant.
* fetch_private_wheels.sh refuses to run on a pull_request event, so the rule
survives an edit to the workflow condition.
* ci-success reports the deferral, so a green pull request never implies
vsphere was covered when it was not.
The cost is that vsphere is verified on the release branch rather than on the
pull request that changes it. That is accepted knowingly: the suite changes a
handful of times a year, and the alternatives cost more than they return. The
durable fix is to stop needing the private index -- VMware now publishes this
SDK to public PyPI under renamed packages -- which deletes the credential, the
job and this gap together.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…pr-path STAC-25540: keep the private-index credential off the pull request path
|
@LouisLotter — agreed on the call, and #365 is merged into this branch. Thread resolved. Your one remaining condition, checked
Verified against the GitLab API rather than taken on trust. Project
The only live token carries One thing worth recording that supports moving on: Follow-up ticket filed as promised: STAC-25544 — move vsphere to the publicly-published VMware SDK packages ( Sorry — one more approval neededMerging #365 pushed to this branch, so Worth fixing as part of STAC-25522 (branch protection): |
Jira: https://stackstate.atlassian.net/browse/STAC-25463 (parent: STAC-25142)
First phase of moving
stackstate-agent-integrationsCI off GitLab. Purely additive —.gitlab-ci.ymlis untouched and keeps running, so this can land without a cutover.What moved
test_<check>job family (20 near-identical jobs)check-testsmatrixchanges:rules.github/scripts/select-checks.sh(plaingit diffvs merge base)checksdev validate *riding insidetest_stackstate_checks_basevalidatejobservices: docker:24-dind+DOCKER_HOST: tcp://docker:2375Path filtering is done in
gitrather than a path-filter action because StackVista enforces a strict third-party action allowlist, andgit diffagainst the merge base is precisely what the GitLab rule meant.Scoped out (each reasoned in the workflow header)
splunk_{base,health,metric,topology}+stackstate_checks_dev. These are the only suites needing a Docker daemon. Phase 2; the runners already provide DinD, so it is about exercising that path, not provisioning it.test_postgres— dead config. It tests apostgrescheck that does not exist in this repository. Dropped, not pending.print_env— not ported at any phase. It is a bareprintenv, and this repo is public, so it publishes every CI credential in scope to a world-readable log. Same hazard class as thesign_debian_package.shfinding on the agent..gitlab-ci.ymldefines a.windows_envanchor that no job has ever referenced, and Windows is not a supported target. The orphanedconda_env.ps1/windows_*.cmdhelpers can retire with the GitLab pipeline (STAC-25464).dockerjob stay on GitLab until integrations gets its own publishing role (pulumi-infra section 7.4).CERBERUS_LAMBDA_URLis a private-visibility org secret unreadable from a public repo. Follow-up.Credentials
Phase 1 needs no new secret provisioning:
REGISTRY_HOST/REGISTRY_USER/REGISTRY_PASSWORDare org-levelvisibility: all(STAC-25350) and already reach this public repo. Note the image ref composesvars.REGISTRY_HOST+/dockerrather than usingvars.REGISTRY_DOCKER_URL, which is private-visibility and not readable here.Validation
actionlintclean — added.github/actionlint.yamldeclaring the self-hosted*-publiclabelszizmorclean (2 suppressed = the documentedunpinned-imagesignores on the container ref)shellcheckcleanSelector exercised against real git history:
push/ dispatchzabbix/["zabbix"]kubelet/+vsphere/["kubelet","vsphere"][](matrix skips)This PR self-validates — the workflow runs against this very PR, so the image path and runner labels are proven before merge.