Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
0b954d0
STAC-25142 Add GitHub Actions lint + unit-test workflow (agent CI mig…
LouisParkin Jul 10, 2026
d9ca269
STAC-25142 CI: run lint+unit on dedicated public runners (docker-publ…
LouisParkin Jul 22, 2026
51c513e
STAC-25142 CI: run workflow steps in bash (dash lacks set -o pipefail…
LouisParkin Jul 24, 2026
936010c
STAC-25142 CI: mark workspace safe.directory (git dubious ownership i…
LouisParkin Jul 24, 2026
adc6b70
STAC-25142 CI: full-depth checkout for unit tests so git describe fin…
LouisParkin Jul 24, 2026
c7d7a42
STAC-25142 Pass workspace dir to fix_branding.sh (CI_PROJECT_DIR is G…
LouisParkin Jul 24, 2026
c07d0c8
STAC-25142 Add GitHub Actions binary-build workflow (agent CI migrati…
LouisParkin Jul 27, 2026
ae94fa8
STAC-25429-godeps-cache-image: warm Go module cache via prebuilt cach…
LouisParkin Jul 28, 2026
7bedf74
STAC-25142 Add GitHub Actions omnibus DEB build workflow
LouisParkin Jul 28, 2026
229df15
STAC-25142 Add agent and cluster-agent container image builds
LouisParkin Jul 28, 2026
c45407d
STAC-25142 Read the PyPI index URL as a variable, not a secret
LouisParkin Jul 28, 2026
ecb9c60
STAC-25429 Push the Go dep cache to its own quay repo, not sts-ci-images
LouisParkin Jul 28, 2026
ffabcac
STAC-25429 Correct visibility comments: datadog_build base and cache …
LouisParkin Jul 28, 2026
e2e6e1c
STAC-25142 Restore the cluster-agent binary's exec bit after artifact…
LouisParkin Jul 28, 2026
7049264
STAC-25429 fix godeps cache image baking an empty module cache
LouisParkin Jul 28, 2026
4e986be
STAC-25429 keep only module zips in the godeps cache image
LouisParkin Jul 29, 2026
0ebc9f1
STAC-25459 wire inv check-mod-tidy into CI, gated on the godeps cache…
LouisParkin Jul 29, 2026
1996c90
Merge pull request #445 from StackVista/STAC-25429-godeps-cache-image
LouisParkin Jul 29, 2026
a9a92e0
Merge pull request #447 from StackVista/STAC-25459-mod-tidy-gate
LouisParkin Jul 29, 2026
a0e85fa
Merge remote-tracking branch 'origin/STAC-25142-agent-lint-unit' into…
LouisParkin Jul 29, 2026
9a2658b
STAC-25429 Build the DEB package in the warm Go module cache image
LouisParkin Jul 29, 2026
77780d0
Merge pull request #446 from StackVista/STAC-25142-agent-omnibus-deb
LouisParkin Jul 30, 2026
b048ba9
STAC-25494 stagger godeps-cache builds instead of gating on concurrency
LouisParkin Jul 30, 2026
279f418
Merge pull request #450 from StackVista/STAC-25494-godeps-cache-no-ca…
LouisParkin Jul 30, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
63 changes: 63 additions & 0 deletions .github/docker/godeps-cache/Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
# Go dependency cache image for stackstate-agent CI (STAC-25429). Derives FROM the
# datadog_build CI image and bakes the workspace's external Go module
# downloads into GOMODCACHE, so consumer jobs get a warm module cache with no
# per-job `go mod download`/network round-trips. Rebuilt only when the module graph
# hash changes -- the image tag is content-addressed
# (see .github/scripts/agent-godeps-cache-metadata.sh).
#
# Only the module ZIPS ($GOMODCACHE/cache/download) are kept; the extracted module
# trees are deleted before the layer closes and Go re-extracts on demand. Measured on
# a full cache, the extracted trees are ~79% of the bytes but ~97% of the *files*, and
# image pull cost here is bound by filesystem metadata, not transfer: warm-proxy pulls
# of the full-cache image spent 0.7m downloading and 4.9m extracting. Shipping ~400k
# tiny files through overlayfs cost more than it saved (+3.0m pull against ~1.45m of
# avoided in-build downloading, so ~1.5m/job net loss).
#
# No `# syntax=` frontend directive and no BuildKit secret mounts: the base image is
# pulled by the daemon after `docker login` (creds stay in the daemon, not the
# build), so the build itself needs no secrets and no Docker Hub frontend pull.
ARG BASE_IMAGE
FROM ${BASE_IMAGE}

# `read -d ''` and `pipefail` are bashisms and RUN defaults to /bin/sh (dash), where
# the download loop silently iterates zero times and publishes an empty cache.
SHELL ["/bin/bash", "-o", "pipefail", "-c"]

# Pre-download external modules for every module in the workspace, mirroring what
# `inv deps` does per module (tasks/libs/common/go.py) at the same parallelism.
# manifests/ preserves the repo's directory layout so nested go.mod files (and their
# local `replace ../` targets) resolve. No GOFLAGS override: go.work puts this in
# workspace mode, where anything other than -mod=readonly is rejected outright. Only
# the zip cache is kept -- the manifests and extracted trees are removed in the same
# RUN, so the layer diff only ever contains the final state.
#
# The prune needs `chmod -R u+w` first: Go writes the module cache read-only (dirs
# 0555, files 0444) and `rm -rf` cannot descend into it as a non-root user.
# cache/vcs holds full git clones for any module fetched outside the proxy; it is not
# needed to rebuild a module from its zip, so it goes too.
COPY manifests/ /workspace/
RUN set -eux; \
git config --global --add safe.directory '*'; \
export PATH="${PATH}:/usr/local/go/bin"; \
cd /workspace; \
modules="$(find . -type f -name go.mod | wc -l)"; \
echo "workspace modules: ${modules}"; \
test "${modules}" -gt 0; \
find . -type f -name go.mod -print0 \
| xargs -0 -n1 -P8 bash -c \
'moddir="$(dirname "$1")"; echo "go mod download :: ${moddir}"; cd "${moddir}" && go mod download' _; \
cache="$(go env GOMODCACHE)"; \
echo "== populated module cache =="; \
du -sh "${cache}"; \
echo "files: $(find "${cache}" -type f | wc -l)"; \
echo "== zip cache =="; \
du -sh "${cache}/cache/download"; \
echo "files: $(find "${cache}/cache/download" -type f | wc -l)"; \
test "$(du -sm "${cache}/cache/download" | cut -f1)" -gt 100; \
find "${cache}" -mindepth 1 -maxdepth 1 ! -name cache -exec chmod -R u+w {} +; \
find "${cache}" -mindepth 1 -maxdepth 1 ! -name cache -exec rm -rf {} +; \
if [ -d "${cache}/cache/vcs" ]; then chmod -R u+w "${cache}/cache/vcs"; rm -rf "${cache}/cache/vcs"; fi; \
echo "== retained in image =="; \
du -sh "${cache}"; \
echo "files: $(find "${cache}" -type f | wc -l)"; \
rm -rf /workspace
45 changes: 45 additions & 0 deletions .github/scripts/agent-godeps-cache-metadata.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
#!/usr/bin/env bash
# Computes the content-addressed tag for the stackstate-agent Go dependency cache
# image (STAC-25429) and the two image refs it is referenced by:
# * push ref -> quay.io/stackstate/stackstate-agent-godeps-cache (authoritative)
# * pull ref -> ${REGISTRY_HOST}/quay/... (registry.tooling proxy)
#
# The cache lives in its own quay repo rather than the shared sts-ci-images, which also
# holds the ARC runner images: stackstate-agent is PUBLIC, so whatever credential this
# workflow carries is reachable from any org member's same-repo branch. A dedicated repo
# keeps that reach down to a rebuildable cache.
#
# Mirrors StackVista/StackGraph .github/scripts/stackgraph-ci-metadata.sh: the tag is
# a hash of the dependency-defining inputs, so an unchanged module graph reuses the
# same image across every branch/PR/default build (the tag *is* the cache key), while
# any change to the graph rotates the tag and a stale cache is never reused.

agent_godeps_compute_metadata() {
: "${QUAY_REGISTRY:?QUAY_REGISTRY is required}" # quay.io
: "${REGISTRY_HOST:?REGISTRY_HOST is required}" # registry.tooling.stackstate.io (quay proxy)
: "${BASE_IMAGE_TAG:?BASE_IMAGE_TAG is required}" # datadog_build tag the cache derives FROM

# Hash inputs: every module manifest (go.work + go.work.sum + modules.yml + all
# nested go.mod/go.sum) plus the base image tag and the two files that define the
# cache mechanism itself (Dockerfile + this script), so changing how the cache is
# built also rotates the tag.
local godeps_hash
godeps_hash="$(
{
git ls-files -z -- \
'*go.mod' '*go.sum' 'go.work' 'go.work.sum' 'modules.yml' \
'.github/docker/godeps-cache/Dockerfile' \
'.github/scripts/agent-godeps-cache-metadata.sh' \
| LC_ALL=C sort -z \
| xargs -0 sha256sum
printf 'base:%s\n' "${BASE_IMAGE_TAG}"
} | sha256sum | cut -c1-16
)"

local image_repo="stackstate/stackstate-agent-godeps-cache"
local image_tag="godeps-${godeps_hash}"
# shellcheck disable=SC2034 # consumed by the sourcing workflow step
ci_image_push="${QUAY_REGISTRY}/${image_repo}:${image_tag}"
# shellcheck disable=SC2034 # consumed by the sourcing workflow step
ci_image="${REGISTRY_HOST}/quay/${image_repo}:${image_tag}"
}
262 changes: 262 additions & 0 deletions .github/workflows/build-binaries.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,262 @@
name: Binary builds

# Ported from .gitlab-ci-agent.yml (build_binaries, build_cluster_agent) as part
# of the GitLab -> GitHub migration (STAC-25142), phase 2: binary builds. Tracks
# the stackstate-7.78.2 pipeline. Builds the branded (StackState) production agent
# and cluster-agent from the DataDog fork, validating that the shippable binaries
# compile on GitHub runners before the packaging/image phases build on top of them.
#
# Speedup concepts carried over from the GitLab pipeline work (STAC-25396) and the
# warm-cache follow-up (STAC-25429):
# * WARM Go module cache via a prebuilt cache image (STAC-25429), keyed by a hash
# of the module graph -- the StackGraph ci-metadata/build-ci-image pattern. The
# godeps-cache reusable workflow (.github/workflows/godeps-cache.yml) publishes
# an image FROM datadog_build with all external modules baked into GOMODCACHE;
# the build jobs use it as their `container:` so the cache is already warm. This
# replaces the earlier "each job cold-reconciles its own deps" approach: there
# is no `go clean -modcache` and no per-job `inv deps` (go mod download + tidy).
# The module cache is too big to transport via actions/cache (~540k files,
# 2-3 GB), which is why it ships as an image layer instead. Jobs still run
# `go work sync` + `go work vendor` per checkout (vendoring mutates go.mod, so
# vendor/ is materialised per job) -- but now against the warm cache, offline.
# * deps_deb's only genuinely consumed output was version.txt, which is git-based
# (inv agent.version), not module-cache-based -- so build-cluster-agent just
# generates it in-job rather than pulling it from an upstream job.
# * Each suite runs once on the path to master, deduped via
# `concurrency: cancel-in-progress` (a newer push cancels the in-flight build).
#
# Known follow-ups:
# * go.mod tidiness is no longer verified in these jobs (the `inv deps` reconcile
# is gone). Add a single check-mod-tidy gate rather than paying tidy in every job.
# * Phase 6 (merge queue): move the heavy builds to `merge_group` so they run
# once at land time and drop from `pull_request`; master/version-branch pushes
# then build + publish without re-running these.
#
# Prerequisites for a GREEN run: the GitLab push mirror must stay disabled, and this
# PUBLIC repo needs the read-only registry-proxy credentials (vars REGISTRY_HOST/
# REGISTRY_USER + secret REGISTRY_PASSWORD) to pull the cache image, plus quay push
# credentials (var QUAY_USER + secret QUAY_PASSWORD) for the godeps-cache build job.
# The cache image is PRIVATE (its base datadog_build is private). Org secrets are
# never exposed to fork PRs, so external-fork PRs skip these jobs by design.

on:
pull_request:
workflow_dispatch:

permissions:
contents: read

concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true

# The datadog_build container's default shell is dash, which lacks `set -o pipefail`
# and mis-handles the conda activation the steps rely on; force bash.
defaults:
run:
shell: bash

env:
# conda env baked into the datadog_build image.
CONDA_ENV: ddpy3

jobs:
godeps-cache:
name: Go dependency cache image
# Same-repo PRs only: the registry/quay secrets are not exposed to fork PRs.
if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository
uses: ./.github/workflows/godeps-cache.yml
# Wait for Lint and unit tests to publish the image before building it here
# (STAC-25494). Covers a ~7m build plus runner scheduling; on timeout this builds
# it itself, so a failed sibling costs latency, not correctness.
with:
build_delay_seconds: 900
secrets:
REGISTRY_PASSWORD: ${{ secrets.REGISTRY_PASSWORD }}
QUAY_PASSWORD: ${{ secrets.QUAY_PASSWORD }}

build-agent:
name: Build agent binary (branded / StackState)
# Same-repo PRs only: the registry-proxy secret is not exposed to fork PRs.
if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository
needs: godeps-cache
runs-on: xlarge-public
timeout-minutes: 120
container:
# Warm Go module cache image (godeps-cache job); pulled via the same
# read-only registry proxy + REGISTRY_* credentials as datadog_build. The tag is
# a sha256 content hash of the module graph (see godeps-cache.yml), so this ref
# is effectively digest-pinned; it cannot be a static digest because it is
# computed per run -- hence the narrow unpinned-images ignore below.
image: ${{ needs.godeps-cache.outputs.ci_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
# Full history + tags so `inv agent.build` resolves the version via
# `git describe --tags --match "3.*"` (needs the 3.x tag reachable from HEAD).
fetch-depth: 0

- name: Build branded agent (production, with rtloader)
run: |
set -eo pipefail
export PATH="$PATH:/usr/local/go/bin"
. /root/miniforge3/etc/profile.d/conda.sh
conda activate "${CONDA_ENV}"
# Work volume is owned by the ARC runner uid but the job container runs as
# root, so git rejects the repo as "dubious ownership"; mark it safe.
git config --global --add safe.directory '*'
# GOMODCACHE is pre-warmed by the godeps-cache image (STAC-25429), keyed
# to the module-graph hash, so there is no `go clean -modcache` and no
# per-job `inv deps` (go mod download + tidy) reconcile. Materialise
# vendor/ for this checkout against the warm cache (offline; no download).
go work sync
go work vendor
# rtloader is the C++/Python bridge the full agent links against; the
# cluster-agent doesn't need it, but the production agent does.
inv -e rtloader.make
inv -e rtloader.install
export AGENT_GITHUB_ORG=DataDog
export GITHUB_ORG=DataDog
export BRANDED=true
export AGENT_REPO_NAME=datadog-agent
# Rebrand DataDog -> StackState. fix_branding.sh defaults its target dir to
# $CI_PROJECT_DIR (a GitLab built-in that is unset here); pass the checkout
# root explicitly.
./fix_branding.sh "${GITHUB_WORKSPACE}"
# Production (non-race) build -- the shippable binary, distinct from the
# race-instrumented build in the unit-test workflow.
inv -e agent.build
ls -la bin/agent

build-cluster-agent:
name: Build cluster-agent binary (branded / StackState)
if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository
needs: godeps-cache
runs-on: xlarge-public
timeout-minutes: 120
container:
# Warm Go module cache image (godeps-cache job); pulled via the same
# read-only registry proxy + REGISTRY_* credentials as datadog_build. The tag is
# a sha256 content hash of the module graph (see godeps-cache.yml), so this ref
# is effectively digest-pinned; it cannot be a static digest because it is
# computed per run -- hence the narrow unpinned-images ignore below.
image: ${{ needs.godeps-cache.outputs.ci_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
# Full history + tags so the version resolves via `git describe`.
fetch-depth: 0

- name: Build branded cluster-agent
run: |
set -eo pipefail
export PATH="$PATH:/usr/local/go/bin"
. /root/miniforge3/etc/profile.d/conda.sh
conda activate "${CONDA_ENV}"
# Work volume is owned by the ARC runner uid but the job container runs as
# root, so git rejects the repo as "dubious ownership"; mark it safe.
git config --global --add safe.directory '*'
# GOMODCACHE is pre-warmed by the godeps-cache image (STAC-25429); no
# `go clean -modcache` and no per-job `inv deps` reconcile. Materialise
# vendor/ for this checkout against the warm cache (offline; no download).
go work sync
go work vendor
export AGENT_GITHUB_ORG=DataDog
export GITHUB_ORG=DataDog
export BRANDED=true
export AGENT_REPO_NAME=datadog-agent
# Rebrand DataDog -> StackState; pass the checkout root (CI_PROJECT_DIR is GitLab-only).
./fix_branding.sh "${GITHUB_WORKSPACE}"
inv -e cluster-agent.build
# version.txt is a build artifact for the downstream packaging/image phases.
# deps_deb produced it on GitLab; there is no shared deps job here, so
# generate it in-job. inv agent.version is git-based (not module-cache-based)
# and cheap. No `-e`: its stdout must be only the version string.
inv agent.version -u > version.txt
ls -la bin/stackstate-cluster-agent

- name: Upload cluster-agent build artifacts
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
with:
name: cluster-agent-binary
# Paths match the GitLab build_cluster_agent artifacts; the cluster-agent
# image phase (phase 5) consumes the binary + Dockerfile manifest.
path: |
bin/stackstate-cluster-agent/
Dockerfiles/cluster-agent/stackstate-cluster.yaml
version.txt
retention-days: 5
if-no-files-found: error

build-cluster-agent-image:
name: Build cluster-agent container image (docker build, no push on PR)
# Ported from pre_release_cluster_agent_image. Lives in this workflow rather
# than build-deb.yml so it can `needs:` the job that produces its input --
# cross-workflow artifact hand-off would need run-id plumbing for no gain.
# Stops at `docker build` + a runtime check: the GitLab job also pushed to
# quay.io/stackstate, which the PR lane of a PUBLIC repo must not do.
# Publishing lands in phase 4, gated on the branch.
if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository
needs: build-cluster-agent
# DinD class: docker CLI in the runner image, dockerd in a native sidecar.
runs-on: docker-public
timeout-minutes: 45
env:
LOCAL_IMAGE: stackstate-cluster-agent:ci
steps:
- name: Check out repository
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
persist-credentials: false

- name: Download cluster-agent binary
uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0
with:
name: cluster-agent-binary

- name: Log in to the registry proxy
env:
REGISTRY_HOST: ${{ vars.REGISTRY_HOST }}
REGISTRY_USER: ${{ vars.REGISTRY_USER }}
REGISTRY_PASSWORD: ${{ secrets.REGISTRY_PASSWORD }}
run: |
set -eo pipefail
# Dockerfiles/cluster-agent pulls its ubuntu builder stage through the
# proxy; the BCI stages come from registry.suse.com and need no auth.
printf '%s' "${REGISTRY_PASSWORD}" | docker login -u "${REGISTRY_USER}" --password-stdin "${REGISTRY_HOST}"

- name: Build cluster-agent image
run: |
set -eo pipefail
# actions/download-artifact does not preserve the executable bit (GitLab
# artifacts did). The Dockerfile's `chmod +x` targets
# opt/stackstate-agent/bin/stackstate-cluster-agent, which is the enclosing
# DIRECTORY, not the binary inside it -- so nothing in the build restores
# it and the image ships a non-executable agent.
chmod +x bin/stackstate-cluster-agent/stackstate-cluster-agent
# bin/stackstate-cluster-agent is a DIRECTORY (binary + dist/), and it
# must stay one: the Dockerfile COPYs it to
# /opt/stackstate-agent/bin/stackstate-cluster-agent/ and entrypoint.sh
# puts that directory on PATH so CMD can exec `stackstate-cluster-agent`
# by name. Flattening it to a bare binary breaks the image.
cp -r bin/stackstate-cluster-agent Dockerfiles/cluster-agent/
docker build -t "${LOCAL_IMAGE}" Dockerfiles/cluster-agent

- name: Smoke test cluster-agent image
run: |
set -eo pipefail
# Bypass entrypoint.sh: it hard-exits without STS_API_KEY, which a
# version check has no business needing.
docker run --rm \
--entrypoint /opt/stackstate-agent/bin/stackstate-cluster-agent/stackstate-cluster-agent \
"${LOCAL_IMAGE}" version
Loading
Loading