Reusable GitHub Actions workflows for the NeuralTrust organization.
Every repo follows the same standardized pipeline:
┌──────────────┐ ┌────────────────────────────────────────────────────────────────┐
│ Push develop │────▶│ deploy.yml → build + image scan + kustomize + Slack notify │──▶ Dev
└──────────────┘ └────────────────────────────────────────────────────────────────┘
┌──────────────┐ ┌──────────────────────────────────────────────────────────────┐
│ PR → main │────▶│ ci.yml → Tests + SAST/security + metadata validation │
└──────────────┘ └──────────────────────────────────────────────────────────────┘
┌──────────────┐ ┌──────────────────────────────────────────────────┐
│ Push main │────▶│ auto-release.yml → AI Semver Bump → GitHub Release│
└──────────────┘ └────────────────────────┬─────────────────────────┘
│ triggers
┌─────────────────────────▼──────────────────────────────────────┐
│ release.yml → Smart Release (promote or rebuild) │
│ │
│ develop→main PR? ──YES──▶ SCAN → PROMOTE (crane copy) │──▶ Prod
│ hotfix/direct? ──YES──▶ REBUILD → SCAN │──▶ Prod
│ │
│ Scan blocks CRITICAL/HIGH fixable CVEs (see image-scan.yml) │
└────────────────────────────────────────────────────────────────┘
┌──────────────┐ ┌──────────────────────────────────────────────────┐
│ Weekly │────▶│ Dependabot → dependency update PRs (auto) │
└──────────────┘ └──────────────────────────────────────────────────┘
| File | Trigger | What it does |
|---|---|---|
deploy.yml |
Push to develop |
Build with COMMIT_SHA → deploy to dev |
ci.yml |
PR to main |
Tests + SAST/security + PR metadata validation |
auto-release.yml |
Push to main |
AI determines semver bump → creates GitHub Release |
release.yml |
GitHub Release published | Smart release (promote or rebuild) → deploy to prod |
| PR Target | Merge Method | Why |
|---|---|---|
Feature → develop |
Squash merge | Keeps develop history clean, one commit per feature |
develop → main |
Merge commit | Preserves commit graph so histories stay in sync |
Important: Using squash merge for
develop→maincauses history divergence — the next comparison will show old commits even though their content was already merged. Always use "Create a merge commit" when mergingdevelopintomain.
| Workflow | Description |
|---|---|
docker-build-deploy.yml |
Build + push + kustomize (single image, COMMIT_SHA) |
multi-image-deploy.yml |
Matrix build + kustomize (multiple images, COMMIT_SHA) |
| Workflow | Description |
|---|---|
release-promote.yml |
Recommended — Smart release: promote from dev or rebuild (single image) |
multi-image-release-promote.yml |
Recommended — Smart release for multi-image services |
python-publish.yml |
Build and publish Python package to GCP Artifact Registry (uv/poetry) |
multi-image-release.yml |
Legacy — Always rebuilds (multiple images, release tag) |
| Workflow | Description |
|---|---|
tests.yml |
Generic test runner (Go/Python/Node/Rust) + optional services |
sast.yml |
Trivy + Gitleaks + language SAST and reachable Go vulnerability scanning |
image-scan.yml |
Trivy container image scan (warn on dev, block on prod release) |
seo-check.yml |
Static Next.js SEO audit (metadata, sitemap/robots presence); job summary |
seo-live-url.yml |
Live site: homepage / robots.txt / sitemap content audit + Lighthouse SEO |
ai-release-bump.yml |
AI-powered semver classification + GitHub Release creation (also promotes ## [Unreleased] in CHANGELOG) |
| Workflow | Description |
|---|---|
smoke-test.yml |
Post-deploy verification: HTTP health polling and/or FluxCD rollout check |
e2e-playwright.yml |
Playwright E2E tests from the e2e-tests image + Allure reporting |
| Workflow | Description |
|---|---|
build-push-image.yml |
Standalone Docker build + push (for advanced composition) |
Is your dev and prod registry in the same GCP project?
│
├─ NO (different projects) ← most NeuralTrust services
│ │
│ │ How many Docker images?
│ │
│ ├─ 1 image → release-promote.yml (smart: promote + rebuild fallback)
│ │
│ └─ 2+ images → multi-image-release-promote.yml
│
└─ YES (same project)
│
└─ 2+ images → multi-image-release.yml
release-promote.yml — The recommended release workflow. Automatically detects the best strategy:
| Scenario | Strategy | Time | What happens |
|---|---|---|---|
| develop → main PR merged | Promote | ~10 seconds | Scans dev image → copies to prod if clean (byte-identical) |
| Hotfix / direct push to main | Rebuild | ~2-5 minutes | Full Docker build → scan prod image before kustomize update |
Before any prod tag is applied or kustomization is updated, the release image is scanned for fixable CRITICAL/HIGH CVEs. A failure blocks the release and sends a Slack alert.
| Strategy | Image scanned | When copy/build completes |
|---|---|---|
| Promote | Dev source image (by commit SHA) | Crane copy runs after scan passes — a dirty dev image never gets a prod release tag |
| Rebuild | Freshly built prod image | Kustomize update runs after scan passes |
Dev deploys use the same scanner with block_vulnerability_findings: true. HIGH/CRITICAL findings fail the scan job (and the parent run), but kustomize only needs the build, so develop still ships.
- Queries the GitHub API for PRs associated with the release commit
- If a merged PR from
develop→mainis found:- Gets the develop branch tip SHA from the PR
- Verifies the dev image exists in the dev registry (by commit SHA)
- If found → promote (crane copy)
- If not found → falls back to rebuild
- If no develop→main PR → rebuild (hotfix path)
release (detect strategy + build if hotfix)
│
▼
scan (Trivy — blocks on fixable CRITICAL/HIGH)
│
├─ promote ──▶ promote-copy (crane dev → prod)
│
└─ rebuild ──▶ (promote-copy skipped)
│
▼
update-kustomization + Slack notify
- Zero build time — image copy takes ~10 seconds vs minutes for a full build
- Byte-identical binary — what was tested in dev is exactly what runs in prod
- Scan-before-promote — prod tags are only applied after the dev image passes the vulnerability gate
- Version injection via ConfigMap —
APPLICATION_VERSIONis set inconfig.env, picked up by kustomizeconfigMapGenerator, and overrides the build-time value at runtime
# .github/workflows/release.yml name: Release on: release: types: [published] permissions: contents: write id-token: write jobs: release: uses: NeuralTrust/workflows/.github/workflows/release-promote.yml@main with: image_name: my-service gcp_project_id: ${{ vars.PROD_GCP_PROJECT_ID }} dev_gcp_project_id: ${{ vars.DEV_GCP_PROJECT_ID }} secrets: WIF_PROVIDER: ${{ secrets.PROD_WIF_PROVIDER }} WIF_SERVICE_ACCOUNT: ${{ secrets.PROD_WIF_SERVICE_ACCOUNT }} GH_TOKEN: ${{ secrets.GH_TOKEN }}
With build args (used only for the rebuild/hotfix fallback):
jobs: release: uses: NeuralTrust/workflows/.github/workflows/release-promote.yml@main with: image_name: my-service gcp_project_id: ${{ vars.PROD_GCP_PROJECT_ID }} dev_gcp_project_id: ${{ vars.DEV_GCP_PROJECT_ID }} build_args: | VERSION=${{ github.event.release.tag_name }} GIT_COMMIT=${{ github.sha }} secrets: WIF_PROVIDER: ${{ secrets.PROD_WIF_PROVIDER }} WIF_SERVICE_ACCOUNT: ${{ secrets.PROD_WIF_SERVICE_ACCOUNT }} BUILD_SECRETS: | GITHUB_TOKEN=${{ secrets.GH_TOKEN }} GH_TOKEN: ${{ secrets.GH_TOKEN }} SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }}
| Input | Required | Default | Description |
|---|---|---|---|
image_name |
Yes | — | Docker image name in the registry |
gcp_project_id |
Yes | — | Prod GCP Project ID (destination) |
dev_gcp_project_id |
Yes | — | Dev GCP Project ID (source for promote) |
registry |
No | europe-west1-docker.pkg.dev |
Artifact Registry host |
repository |
No | nt-docker |
Artifact Registry repository |
dockerfile |
No | Dockerfile |
Dockerfile path (rebuild only) |
context |
No | . |
Build context (rebuild only) |
build_target |
No | — | Multi-stage target (rebuild only) |
build_args |
No | — | Build args (rebuild only) |
tag_prefix |
No | — | Tag prefix (e.g., gpu-) |
tag_suffix |
No | — | Tag suffix |
kustomize_name |
No | image_name |
Image name in kustomization.yaml |
overlay_path |
No | k8s/overlays/prod |
Primary prod overlay. If k8s/overlays/prod-us exists, release jobs update it in the same commit (US regional Flux) |
config_env_file |
No | config.env |
Config env file name in overlay |
dev_branch |
No | develop |
Dev branch name (for PR detection) |
| Secret | Required | Description |
|---|---|---|
WIF_PROVIDER |
Yes | GCP Workload Identity Federation provider |
WIF_SERVICE_ACCOUNT |
Yes | GCP service account email |
BUILD_SECRETS |
No | Secret build args (rebuild only) |
GH_TOKEN |
Yes | GitHub PAT for kustomization pushes |
SLACK_WEBHOOK_URL |
No | Slack webhook for notifications |
multi-image-release-promote.yml — Same smart promote/rebuild logic for multi-image services.
jobs: release: uses: NeuralTrust/workflows/.github/workflows/multi-image-release-promote.yml@main with: images: | [ {"name": "my-service", "target": "api"}, {"name": "my-service-cli", "target": "cli"} ] kustomize_images: | [ {"kustomize_name": "my-service", "image_name": "my-service"}, {"kustomize_name": "my-service-cli", "image_name": "my-service-cli"} ] gcp_project_id: ${{ vars.PROD_GCP_PROJECT_ID }} dev_gcp_project_id: ${{ vars.DEV_GCP_PROJECT_ID }} commit_message_prefix: my-service secrets: WIF_PROVIDER: ${{ secrets.PROD_WIF_PROVIDER }} WIF_SERVICE_ACCOUNT: ${{ secrets.PROD_WIF_SERVICE_ACCOUNT }} GH_TOKEN: ${{ secrets.GH_TOKEN }} SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }}
Detection runs once; each image is released in parallel via a matrix job. Per image:
release (detect strategy + build if rebuild) → scan (image-scan.yml) → promote-copy → update-kustomization
Each image is scanned for fixable CRITICAL/HIGH CVEs before crane copy or kustomize update — same gate as single-image release-promote.yml.
All services expose a /health endpoint returning:
{
"status": "healthy",
"version": "v1.2.3",
"time": "2026年02月07日T20:06:41Z"
}The version flows through two mechanisms:
| Environment | Mechanism | Value |
|---|---|---|
| Dev (develop push) | Dockerfile ENV APPLICATION_VERSION=${APP_VERSION} |
Commit SHA |
| Prod (release) | Kubernetes ConfigMap via config.env → APPLICATION_VERSION=v1.2.3 |
Release tag |
In production, the ConfigMap env var overrides the Dockerfile value because Kubernetes env vars take precedence.
Go — init() function reads from env, overriding ldflags:
func init() { if v := os.Getenv("APPLICATION_VERSION"); v != "" { Version = v } }
Python — reads from env directly:
app_version = os.environ.get("APPLICATION_VERSION", "unknown")
Node.js — reads from env directly:
version: process.env.APPLICATION_VERSION || 'unknown'
All deploy/release workflows automatically update APPLICATION_VERSION in the overlay's config.env:
- Dev deploy:
APPLICATION_VERSION=<commit-sha> - Prod release:
APPLICATION_VERSION=<release-tag>(e.g.,v1.2.3)
The kustomize configMapGenerator picks this up, regenerates the ConfigMap with a new hash suffix, and Kubernetes triggers a rollout.
ai-release-bump.yml — Analyzes commits since the last release using OpenAI and determines the appropriate semver bump:
- MAJOR — incompatible API changes, breaking changes
- MINOR — new features, backward-compatible functionality
- PATCH — bug fixes, docs, refactoring, dependency updates
Then creates a GitHub Release with the new version tag. The release event triggers the repo's release.yml for deployment.
# .github/workflows/auto-release.yml name: Auto Release on: push: branches: [main] permissions: contents: write jobs: release: uses: NeuralTrust/workflows/.github/workflows/ai-release-bump.yml@main secrets: OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }} GH_TOKEN: ${{ secrets.GH_TOKEN }} SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }}
| Input | Default | Description |
|---|---|---|
model |
gpt-5.6-luna |
OpenAI model for classification |
default_bump |
patch |
Fallback if AI classification fails |
initial_version |
v0.1.0 |
Version for repos with no previous tags |
tag_prefix |
v |
Prefix for version tags |
dry_run |
false |
Compute version without creating release |
version_update_script |
'' |
Shell commands to update version files. Receives VERSION and TAG env vars. |
update_changelog |
true |
If true, promote ## [Unreleased] → ## [vX.Y.Z] — YYYY-MM-DD in the changelog file before tagging. |
changelog_path |
CHANGELOG.md |
Path of the changelog file to promote. |
unreleased_heading |
## [Unreleased] |
Heading used for the unreleased section in the changelog. |
| Output | Description |
|---|---|
new_version |
The new version tag (e.g., v1.2.3) |
bump_type |
The bump type (major, minor, patch) |
previous_version |
The previous version tag |
The workflow creates a minimal GitHub Release body:
- What changed — bump type and AI reason
- Commits — list since the previous tag
It does not append installation instructions, container image tables, or an AI semver footer. Repos that need those (e.g. neuraltrust-platform) extend the release in a follow-up workflow (publish-chart.yml appends Container images and Installation).
If release creation fails (image-scan gate, missing dev image, publish error, or bump failure), the workflow posts a Slack message with the reason. Pass SLACK_WEBHOOK_URL from the caller; without it the job still fails and writes the reason to the step summary.
The bump-guard skips this workflow when the head commit message starts with:
chore: bump version to ...— this workflow's own version bump commit.docs(changelog):— the prefix historically used by changelog-feed automation.
If you configure additional automated commits that push to main, prefix them similarly so they don't retrigger the release pipeline.
Important:
GH_TOKENmust be a PAT (notGITHUB_TOKEN) so the created release event can trigger other workflows.
tests.yml — Generic test runner supporting multiple languages with optional service containers.
| Language | Setup | Default Lint | Default Test |
|---|---|---|---|
go |
actions/setup-go |
golangci-lint run --timeout=10m ./... |
go test -v -race -coverprofile=coverage.txt ./... |
python |
actions/setup-python |
ruff check . |
pytest -v --cov --cov-report=xml |
node |
actions/setup-node |
npx eslint . |
npm test |
rust |
dtolnay/rust-toolchain |
cargo fmt --check && cargo clippy -D warnings |
cargo test --verbose |
Go lint installs the pinned golangci_version from source with the selected
language_version toolchain. This avoids running a prebuilt linter compiled by
an older Go release against code that uses a newer language or standard-library
surface.
Callers that need a temporary Go build experiment can set go_experiment; the
value is exported as GOEXPERIMENT in both the lint and test jobs. Leave it
empty for the toolchain default.
Start service containers with health checks and auto-exported env vars:
| Service | Input | Env Vars Exported |
|---|---|---|
| PostgreSQL | postgres_enabled: true |
POSTGRES_HOST, POSTGRES_PORT, POSTGRES_USER, POSTGRES_PASSWORD, POSTGRES_DB, DATABASE_URL |
| ClickHouse | clickhouse_enabled: true |
CLICKHOUSE_HOST, CLICKHOUSE_PORT, CLICKHOUSE_HTTP_PORT, CLICKHOUSE_URL |
| Redis | redis_enabled: true |
REDIS_HOST, REDIS_PORT, REDIS_URL |
| Kafka (KRaft) | kafka_enabled: true |
KAFKA_SERVER, KAFKA_BOOTSTRAP_SERVERS |
# Minimal (Go) jobs: test: uses: NeuralTrust/workflows/.github/workflows/tests.yml@main with: language: go language_version: '1.27'
# Split tests (recommended pattern — lint only once, services only where needed) jobs: unit-tests: uses: NeuralTrust/workflows/.github/workflows/tests.yml@main with: language: go language_version: '1.27' go_private_modules: true lint_enabled: true coverage_enabled: true test_command: | go test -v -race -coverprofile=coverage.txt -covermode=atomic ./internal/... setup_commands: | cp .env.example .env secrets: GH_TOKEN: ${{ secrets.GH_TOKEN }} integration-tests: uses: NeuralTrust/workflows/.github/workflows/tests.yml@main with: language: go language_version: '1.27' go_private_modules: true lint_enabled: false postgres_enabled: true postgres_db: my_app redis_enabled: true test_command: | go test -v -race ./tests/integration secrets: GH_TOKEN: ${{ secrets.GH_TOKEN }} TEST_SECRETS: | OPENAI_API_KEY=${{ secrets.OPENAI_API_KEY }}
# Python with Kafka + PostgreSQL jobs: test: uses: NeuralTrust/workflows/.github/workflows/tests.yml@main with: language: python language_version: '3.12' postgres_enabled: true kafka_enabled: true coverage_enabled: true coverage_file: coverage.xml
| Input | Required | Default | Description |
|---|---|---|---|
language |
Yes | — | go, python, node, rust |
language_version |
Yes | — | Version string (e.g., 1.25.1, 3.12, 20, 1.78) |
test_enabled |
No | true |
Enable test execution |
test_command |
No | (auto) | Custom test command (overrides language default) |
lint_enabled |
No | true |
Enable linting |
lint_command |
No | (auto) | Custom lint command (overrides language default) |
ty_enabled |
No | false |
Run Astral ty check during default Python linting (preview type checker) |
coverage_enabled |
No | false |
Generate coverage report in job summary |
coverage_file |
No | coverage.txt |
Coverage file path |
working_directory |
No | . |
Working directory for all commands |
setup_commands |
No | — | Commands to run before tests (e.g., install deps, copy env files) |
uv_sync_args |
No | --frozen --all-extras --all-groups |
Arguments for uv sync (Python only). Override when extras conflict, e.g. --frozen --extra cpu --extra dev |
go_private_modules |
No | false |
Enable private Go module access for github.com/NeuralTrust/* |
golangci_version |
No | v2.13.2 |
golangci-lint version built with the selected Go toolchain |
go_experiment |
No | — | Optional GOEXPERIMENT value for Go lint and test jobs |
kreuzberg_enabled |
No | false |
Install Kreuzberg FFI + Tesseract OCR before tests (Go CGO builds) |
kreuzberg_version |
No | v4.1.0 |
Kreuzberg FFI release version (only when kreuzberg_enabled: true) |
Service inputs:
| Input | Default | Description |
|---|---|---|
postgres_enabled |
false |
Start PostgreSQL container |
postgres_version |
16 |
PostgreSQL version |
postgres_db |
testdb |
Database name |
postgres_user |
postgres |
Database user |
postgres_password |
postgres |
Database password |
clickhouse_enabled |
false |
Start ClickHouse container |
clickhouse_version |
24 |
ClickHouse version |
redis_enabled |
false |
Start Redis container |
redis_version |
7 |
Redis version |
kafka_enabled |
false |
Start Kafka (KRaft mode) container |
kafka_version |
7.6.0 |
Confluent Kafka version |
Secrets:
| Secret | Required | Description |
|---|---|---|
GH_TOKEN |
No | GitHub token for private module/repo access |
TEST_SECRETS |
No | Additional secrets as KEY=VALUE pairs (one per line), exported as env vars |
The workflow runs two parallel jobs:
- Lint — Language-specific linting (skipped if
lint_enabled: false) - Test — Service startup + setup_commands + test execution (skipped if
test_enabled: false)
sast.yml — Multi-layered security scanning with universal tools and language-specific SAST:
Universal (all repos):
| Tool | Category | What it finds |
|---|---|---|
| Trivy | SCA (dependencies) | Known CVEs in dependencies, IaC misconfigs, license issues |
| Gitleaks | Secret detection | Hardcoded secrets/credentials in git history |
Language analysis (opt-in per language):
| Tool | Language | What it finds |
|---|---|---|
| Gosec | Go | SQL injection, weak crypto, command injection, hardcoded creds |
| govulncheck | Go | Vulnerabilities that are reachable from the repository's compiled call graph |
| Bandit | Python | SQL injection, insecure functions (eval, exec, pickle), hardcoded passwords, weak crypto |
| njsscan | Node/JS | XSS, prototype pollution, insecure regex, eval injection, command injection |
| (not needed) | Rust | Rust's type system + borrow checker + clippy covers most issues |
# Go projects jobs: security: uses: NeuralTrust/workflows/.github/workflows/sast.yml@main with: gosec_enabled: true govulncheck_enabled: true
govulncheck is blocking when enabled. By default it reads the Go toolchain
from go.mod, scans ./..., and installs the pinned scanner version. Consumers
with a container/toolchain version newer than their go.mod directive can set
govulncheck_go_version explicitly so CI scans with the production toolchain.
Private-module consumers must also set go_private_modules: true and pass
GH_TOKEN.
# Python projects jobs: security: uses: NeuralTrust/workflows/.github/workflows/sast.yml@main with: bandit_enabled: true bandit_args: '-r src/ -ll -x tests'
# Node projects jobs: security: uses: NeuralTrust/workflows/.github/workflows/sast.yml@main with: njsscan_enabled: true
We don't have GitHub Advanced Security, so the Security tab / code scanning is unavailable on private repos. sast.yml therefore does not push SARIF to the Security tab. Instead:
- Job log — the Trivy table scan prints findings inline (this is also the blocking gate, via
trivy_exit_code). - Step summary — a short markdown summary per scanner.
- Run artifact — the full SARIF is saved as the
trivy-sast-sarifartifact (7-day retention), downloadable from the run's Artifacts section.
No special permissions are required — contents: read is enough:
permissions: contents: read jobs: security: uses: NeuralTrust/workflows/.github/workflows/sast.yml@main
Older callers that still grant
security-events: write/actions: readfor the (removed) Security-tab upload are harmless — those scopes are simply unused now and can be dropped.
| Input | Default | Description |
|---|---|---|
trivy_severity |
CRITICAL,HIGH |
Severity levels to scan |
trivy_exit_code |
1 |
Exit code on findings (1=fail, 0=warn) |
scan_type |
fs |
Trivy scan type (fs, config, repo) |
gitleaks_enabled |
false |
Enable informational secret detection across full git history |
gosec_enabled |
false |
Enable Go SAST |
gosec_args |
./... |
Gosec arguments |
govulncheck_enabled |
false |
Enable blocking reachable Go vulnerability scanning |
govulncheck_version |
1.3.0 |
golang.org/x/vuln scanner version (latest is also accepted) |
govulncheck_go_version |
(from go.mod) |
Optional explicit Go toolchain version |
govulncheck_go_mod_path |
go.mod |
Module file used by setup-go and its dependency cache |
govulncheck_working_directory |
. |
Directory in which the scanner runs |
govulncheck_package_pattern |
./... |
Go package pattern scanned by govulncheck |
go_private_modules |
false |
Authenticate Go scanners for github.com/NeuralTrust/*; requires GH_TOKEN |
bandit_enabled |
false |
Enable Python SAST |
bandit_args |
-r . -ll |
Bandit arguments |
njsscan_enabled |
false |
Enable Node/JS SAST |
njsscan_args |
. |
njsscan target path |
seo-check.yml — Fast static scan via scripts/seo-static-audit.mjs (metadata, robots / sitemap routes, common gaps). See workflow file header for workflow_call inputs.
Example static site repo (e.g. web-public .github/workflows/site.yml): on PR — static seo-check.yml plus bundle npm run build:budget in parallel; on push to main — seo-live-url.yml when repo variable BASE_URL is set. Copy site.yml into other repos and adjust app_roots / scripts as needed.
seo-live-url.yml — curl homepage, optional robots.txt + sitemap HEAD, then seo-live-content-audit.mjs: parse sitemap(s), validate <loc> URLs (scheme/host vs base_url), sample HEAD/GET on up to N URLs, check robots.txt for Sitemap: lines, and on homepage + Lighthouse paths audit H1, <link rel="canonical">, and application/ld+json (valid JSON). Then Lighthouse SEO on base_url. Inputs: content_audit_enabled, content_audit_max_url_checks, content_audit_strict. Set repository variable BASE_URL; job skipped when unset.
docker-build-deploy.yml — Builds a single Docker image with COMMIT_SHA tags, updates kustomization and config.env, and sends Slack notifications.
# .github/workflows/deploy.yml name: Build & Deploy on: push: branches: [develop] permissions: contents: write id-token: write jobs: deploy: uses: NeuralTrust/workflows/.github/workflows/docker-build-deploy.yml@main with: image_name: my-service gcp_project_id: ${{ vars.DEV_GCP_PROJECT_ID }} secrets: WIF_PROVIDER: ${{ secrets.DEV_WIF_PROVIDER }} WIF_SERVICE_ACCOUNT: ${{ secrets.DEV_WIF_SERVICE_ACCOUNT }} GH_TOKEN: ${{ secrets.GH_TOKEN }}
| Input | Required | Default | Description |
|---|---|---|---|
image_name |
Yes | — | Docker image name in registry |
gcp_project_id |
Yes | — | GCP Project ID |
registry |
No | europe-west1-docker.pkg.dev |
Registry host |
repository |
No | nt-docker |
Artifact Registry repo |
dockerfile |
No | Dockerfile |
Dockerfile path (relative to context) |
context |
No | . |
Docker build context |
build_target |
No | — | Docker multi-stage target |
build_args |
No | — | Non-secret build args (KEY=VALUE, one per line) |
tag_prefix |
No | — | Tag prefix (e.g., gpu-) |
tag_suffix |
No | — | Tag suffix (e.g., -with-models) |
kustomize_name |
No | image_name |
Image name in kustomization.yaml |
overlay_dev_path |
No | k8s/overlays/dev |
Dev overlay path |
overlay_prod_path |
No | k8s/overlays/prod |
Prod overlay path |
free_disk_space |
No | false |
Free ~30GB disk before build (needed for large base images like pytorch/cuda) |
- Builds Docker image with tags:
COMMIT_SHA,latest,cache - Injects
APP_VERSION=<commit-sha>as a build arg - Scans the pushed image via
image-scan.yml(CRITICAL/HIGH; scan job fails on findings, deploy is not gated) - Updates
kustomization.yaml(image tag) andconfig.env(APPLICATION_VERSION) in parallel with the scan - Commits and pushes overlay updates (deploy workflows use
paths-ignore: k8s/**so this does not retrigger builds) - Sends Slack notification
multi-image-deploy.yml — Matrix-based parallel build of multiple images + kustomization update.
jobs: deploy: uses: NeuralTrust/workflows/.github/workflows/multi-image-deploy.yml@main with: images: '[{"name":"api","target":"api"},{"name":"worker","target":"worker"}]' kustomize_images: '[{"kustomize_name":"api","image_name":"api"}]' gcp_project_id: ${{ vars.DEV_GCP_PROJECT_ID }} secrets: WIF_PROVIDER: ${{ secrets.DEV_WIF_PROVIDER }} WIF_SERVICE_ACCOUNT: ${{ secrets.DEV_WIF_SERVICE_ACCOUNT }} GH_TOKEN: ${{ secrets.GH_TOKEN }}
| Field | Required | Default | Description |
|---|---|---|---|
name |
Yes | — | Docker image name |
target |
No | — | Build target |
context |
No | . |
Build context |
dockerfile |
No | Dockerfile |
Dockerfile path |
build_args |
No | — | Extra build args |
tag_prefix |
No | — | Tag prefix |
tag_suffix |
No | — | Tag suffix |
multi-image-release.yml — Always rebuilds. Use multi-image-release-promote.yml instead.
python-publish.yml — Builds a Python package (wheel + sdist) and publishes it to a GCP Artifact Registry Python repository. Supports both uv and poetry build tools.
# Using uv (default) — e.g., TrustTest jobs: publish: uses: NeuralTrust/workflows/.github/workflows/python-publish.yml@main with: gcp_project_id: ${{ vars.PROD_GCP_PROJECT_ID }} secrets: WIF_PROVIDER: ${{ secrets.PROD_WIF_PROVIDER }} WIF_SERVICE_ACCOUNT: ${{ secrets.PROD_WIF_SERVICE_ACCOUNT }}
# Using poetry — e.g., internal-sdk jobs: publish: uses: NeuralTrust/workflows/.github/workflows/python-publish.yml@main with: gcp_project_id: ${{ vars.PROD_GCP_PROJECT_ID }} build_tool: poetry python_version: '3.10' secrets: WIF_PROVIDER: ${{ secrets.PROD_WIF_PROVIDER }} WIF_SERVICE_ACCOUNT: ${{ secrets.PROD_WIF_SERVICE_ACCOUNT }}
Python library repos follow a slightly different pattern from Docker-based services:
┌──────────────┐ ┌──────────────────────────────────────────────────────┐
│ Push develop │────▶│ publish-dev.yml → python-publish → DEV AR registry │
└──────────────┘ └──────────────────────────────────────────────────────┘
┌──────────────┐ ┌──────────────────────────────────────────────────────┐
│ PR → main │────▶│ ci.yml → Lint + SAST/security + metadata validation │
└──────────────┘ └──────────────────────────────────────────────────────┘
┌──────────────┐ ┌──────────────────────────────────────────┐
│ Push main │────▶│ auto-release.yml → AI Semver Bump + GitHub│
│ │ │ Release + update pyproject.toml version │
└──────────────┘ └─────────────────┬────────────────────────┘
│ triggers
┌──────────────────▼──────────────────────────────────┐
│ release.yml → python-publish → PROD AR registry │
└────────────────────────────────────────────────────┘
| Input | Required | Default | Description |
|---|---|---|---|
gcp_project_id |
Yes | — | GCP Project ID that owns the Artifact Registry |
build_tool |
No | uv |
Build tool: uv or poetry |
python_version |
No | 3.11 |
Python version |
ar_location |
No | europe-west1 |
Artifact Registry location |
ar_repository |
No | nt-python |
Artifact Registry Python repository name |
working_directory |
No | . |
Working directory containing pyproject.toml |
| Secret | Required | Description |
|---|---|---|
WIF_PROVIDER |
Yes | GCP Workload Identity Federation provider |
WIF_SERVICE_ACCOUNT |
Yes | GCP service account email |
smoke-test.yml — Post-deploy health check that polls a service endpoint waiting for the expected version. Designed for FluxCD/GitOps where deployment is async.
| Result | What happened | Slack | Exit code |
|---|---|---|---|
| SUCCESS | New version detected within timeout | None (or green) | 0 |
| WARNING | Service healthy but old version still running | Orange alert | 0 |
| PROBLEM | Service not responding at all | Red alert | 1 |
# Smoke test after dev deploy jobs: deploy: uses: NeuralTrust/workflows/.github/workflows/docker-build-deploy.yml@main with: image_name: my-service gcp_project_id: ${{ vars.DEV_GCP_PROJECT_ID }} secrets: WIF_PROVIDER: ${{ secrets.DEV_WIF_PROVIDER }} WIF_SERVICE_ACCOUNT: ${{ secrets.DEV_WIF_SERVICE_ACCOUNT }} GH_TOKEN: ${{ secrets.GH_TOKEN }} smoke-test: needs: deploy uses: NeuralTrust/workflows/.github/workflows/smoke-test.yml@main with: health_url: https://api.dev.example.com/health expected_version: ${{ github.sha }} environment: dev secrets: SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }}
| Input | Required | Default | Description |
|---|---|---|---|
health_url |
Yes | — | URL to poll (must return 200 with version in body) |
expected_version |
Yes | — | Version string to grep for (commit SHA or release tag) |
timeout_minutes |
No | 10 |
Max minutes to wait |
poll_interval |
No | 30 |
Seconds between polls |
initial_delay |
No | 30 |
Seconds to wait before first poll |
environment |
No | dev |
Environment name for notifications |
service_name |
No | repo name | Service name for notifications |
- Waits
initial_delayseconds (gives FluxCD time to detect the kustomization change) - Polls
health_urleverypoll_intervalseconds - If response contains
expected_version→ SUCCESS - After
timeout_minutes, checks one final time:- Service responds but version doesn't match → WARNING
- Service doesn't respond → PROBLEM
- Sends Slack notification for WARNING and PROBLEM outcomes
e2e-playwright.yml — Runs Playwright tests from the shared e2e-tests Docker image against a deployed service, publishes results to Allure, and uploads HTML/Allure artifacts.
# After dev deploy jobs: deploy: uses: NeuralTrust/workflows/.github/workflows/docker-build-deploy.yml@main # ... e2e: needs: deploy uses: NeuralTrust/workflows/.github/workflows/e2e-playwright.yml@main with: service: app base_url: https://app.dev.neuraltrust.ai expected_version: ${{ github.sha }} e2e_tests_channel: prod image_tag: latest secrets: TWINGATE_SERVICE_KEY: ${{ secrets.TWINGATE_SERVICE_KEY }} ALLURE_USER: ${{ secrets.ALLURE_USER }} ALLURE_PASS: ${{ secrets.ALLURE_PASS }} PROD_WIF_PROVIDER: ${{ secrets.PROD_WIF_PROVIDER }} PROD_WIF_SERVICE_ACCOUNT: ${{ secrets.PROD_WIF_SERVICE_ACCOUNT }} DEV_WIF_PROVIDER: ${{ secrets.DEV_WIF_PROVIDER }} DEV_WIF_SERVICE_ACCOUNT: ${{ secrets.DEV_WIF_SERVICE_ACCOUNT }} E2E_SECRETS_JSON: ${{ secrets.E2E_APP_SECRETS_JSON }}
Stable vs experimental tests: base_url (dev or prod app) is independent of the test image. By default callers use e2e_tests_channel: prod and pull latest from the prod Artifact Registry (built from e2e-tests main). Set e2e_tests_channel: develop (repo var E2E_TESTS_CHANNEL in app) to validate in-flight test changes from the dev registry (built from e2e-tests develop).
# app — opt into experimental tests while still hitting dev URL with: base_url: https://app.dev.neuraltrust.ai e2e_tests_channel: develop # or vars.E2E_TESTS_CHANNEL=develop image_tag: latest
| Runner | Use case |
|---|---|
ubuntu-latest (default) |
Public/Twingate URLs — connects via Twingate before tests |
arc-runner-medium |
In-cluster URLs (k8s service DNS) — Twingate disabled automatically |
| Input | Required | Default | Description |
|---|---|---|---|
service |
Yes | — | Playwright service name (app, trustgate, admin-console, data-plane) |
base_url |
Yes | — | Base URL of the service under test |
expected_version |
No | — | Commit SHA expected in health endpoint (empty = skip version gate) |
health_path |
No | /api/health |
Health check path for version polling |
e2e_tests_channel |
No | prod |
prod = stable tests (prod registry) · develop = experimental tests (dev registry) |
image_tag |
No | latest |
e2e-tests image tag in Artifact Registry |
runner |
No | ubuntu-latest |
GitHub Actions runner label |
twingate_enabled |
No | true |
Connect Twingate before tests (ignored on ARC runners) |
grep |
No | — | Playwright --grep filter (empty = service default) |
allure_project |
No | service |
Allure project_id |
e2e_run_enabled |
No | true |
Run tests (false = list only) |
version_wait_minutes |
No | 20 |
Health gate timeout in minutes |
dev_gcp_project_id |
No | inferred | Dev GCP project ID for the develop e2e-tests image channel (falls back to parsing the dev service account email) |
prod_gcp_project_id |
No | inferred | Prod GCP project ID for the prod e2e-tests image channel (falls back to parsing the prod service account email) |
allure_public_url |
No | https://allure.internal.neuraltrust.ai |
Public Allure URL shown in summaries and used by public runners |
allure_internal_url |
No | http://allure-api.allure.svc.cluster.local:5050 |
Internal Allure URL used by ARC runners |
e2e_vars_json |
No | {} |
JSON map of non-secret E2E_* variables (login paths, team selectors, workspace names, etc.) |
| Secret | Required | Description |
|---|---|---|
ALLURE_USER |
Yes | Allure server credentials |
ALLURE_PASS |
Yes | Allure server credentials |
PROD_WIF_PROVIDER |
No* | GCP WIF for prod registry (stable tests) |
PROD_WIF_SERVICE_ACCOUNT |
No* | GCP service account email |
DEV_WIF_PROVIDER |
No* | GCP WIF for dev registry (experimental tests) |
DEV_WIF_SERVICE_ACCOUNT |
No* | GCP service account email |
WIF_PROVIDER / WIF_SERVICE_ACCOUNT |
No | Deprecated fallback when channel-specific WIF is omitted |
TWINGATE_SERVICE_KEY |
No | Twingate service key (public runners) |
E2E_SECRETS_JSON |
No | JSON map of E2E_* env vars (overrides individual secrets) |
E2E_USER, E2E_PASSWORD, etc. |
No | Individual E2E credentials when not using E2E_SECRETS_JSON |
Pass non-secret service-specific E2E settings via e2e_vars_json; pass credentials through E2E_SECRETS_JSON or the individual E2E secrets.
Container images are scanned with Trivy via image-scan.yml. Deploy and release workflows call it automatically; you can also invoke it standalone.
| Pipeline stage | Mode | Blocks? | What is scanned |
|---|---|---|---|
Dev deploy (docker-build-deploy.yml, multi-image-deploy.yml) |
Warn (exit_code: 0) |
No | Image just pushed to dev registry |
Prod release (release-promote.yml, multi-image-release-promote.yml) |
Block (exit_code: 1) |
Yes | Dev source (promote) or rebuilt prod image (hotfix) |
Both modes scan for CRITICAL,HIGH severity and count only fixable CVEs (ignore_unfixed: true).
image-scan.yml — Pulls an image from Artifact Registry, scans OS packages and application dependencies, prints findings to the job log, and saves the SARIF as a downloadable run artifact.
Findings are always printed as a table in the job log and the blocking gate lives there (exit_code), so a blocked promote always shows which CVEs caused it. The SARIF report is saved as the trivy-image-sarif run artifact (7-day retention) — there's no Security-tab upload (we don't have GitHub Advanced Security). Only contents: read + id-token: write (to pull the image) are needed.
jobs: scan: uses: NeuralTrust/workflows/.github/workflows/image-scan.yml@main permissions: contents: read id-token: write # GCP WIF auth to pull the image with: image_ref: europe-west1-docker.pkg.dev/my-proj/nt-docker/my-svc:abc123 exit_code: '1' # '0' = warn, '1' = block secrets: WIF_PROVIDER: ${{ secrets.DEV_WIF_PROVIDER }} WIF_SERVICE_ACCOUNT: ${{ secrets.DEV_WIF_SERVICE_ACCOUNT }}
| Input | Default | Description |
|---|---|---|
image_ref |
— | Full image reference including registry, project, repo, and tag |
severity |
CRITICAL,HIGH |
Severity levels to scan |
exit_code |
0 |
0 = warn (never blocks), 1 = block on findings |
ignore_unfixed |
true |
Only count vulnerabilities with a fix available |
scanners |
vuln |
Trivy scanners (vuln only by default; avoids secret false positives in dependency test fixtures) |
trivyignore_path |
(auto) | Path to .trivyignore; auto-detected from repo root if present |
upload_sarif |
true |
Save the SARIF report as a downloadable run artifact (trivy-image-sarif). Name kept for backward compatibility — no longer a Security-tab upload |
registry |
europe-west1-docker.pkg.dev |
Registry host for docker login |
Image scans use scanners: vuln by default so prod gates block only on fixable CVEs, not Trivy secret hits in third-party packages (e.g. masked tokens in trusttest dataset YAML). Repository secret detection stays in sast.yml / Gitleaks.
Add a .trivyignore file at the repo root (same convention as sast.yml) to suppress accepted CVE findings. Both deploy and release scans auto-detect it.
Deploy and release workflows send Slack notifications on success or failure.
Auto-release (ai-release-bump.yml) posts only on failure, with the reason the GitHub Release was not created (image-scan gate, missing dev image, publish error, or bump failure).
Smart release notifications include the strategy used:
- "promoted from dev" (for develop→main releases)
- "rebuilt (hotfix)" (for direct pushes)
- "Blocked by image scan" (fixable CRITICAL/HIGH CVEs found — see the job log /
trivy-image-sarifartifact)
To enable:
- Create a Slack Incoming Webhook
- Add it as an organization secret:
SLACK_WEBHOOK_URL - Callers using
secrets: inheritget notifications automatically
If the secret is not set, the notification step is silently skipped.
All repos include a dependabot.yml that creates automated PRs for dependency updates:
| Ecosystem | Schedule | Behavior |
|---|---|---|
github-actions |
Weekly (Monday) | Updates action versions |
docker |
Weekly (Monday) | Updates base image versions |
gomod / pip / npm |
Weekly (Monday) | Groups minor+patch updates, ignores major |
PRs target the develop branch only. Since all changes must be tested before promotion to main, dependency updates follow the same flow: merged into develop → tested → promoted via PR to main. Dependabot PRs trigger the standard CI pipeline (tests + SAST/security + metadata validation), so safe updates follow the same review path as other changes.
Security updates are different. GitHub Dependabot security PRs always open against the repository default branch (main in this org). There is no organization-level setting and no dependabot.yml key that can change that.
dependabot-retarget.yml is the org-wide workaround: twice a day at 00:00 and 12:00 UTC (and workflow_dispatch) it searches NeuralTrust for open Dependabot PRs against main and moves them onto develop when that branch exists. Repos without develop stay on main. Dependabot rebase can reset the base back to main, which is why the job is scheduled rather than one-shot.
Every service still needs a per-repo .github/dependabot.yml with target-branch: develop so version updates (weekly bumps) open against develop rather than main.
Because every repo in the org consumes these reusable workflows and composite actions, a mistake here breaks pipelines org-wide. This repo therefore guards its own Actions surface:
| Feature | File | What it does |
|---|---|---|
| Dependabot | .github/dependabot.yml |
Weekly github-actions updates across .github/workflows/** and .github/actions/**. Minor/patch grouped into one PR, with a 7-day cooldown so a yanked or compromised release has time to be pulled. |
| Workflows CI | .github/workflows/workflows-ci.yml |
Runs on any change to .github/** or the hygiene scripts. All three jobs are blocking: actionlint (syntax/shell), hygiene (reusable-workflow rules), zizmor (Actions security, ratcheted against a baseline). |
| OpenSSF Scorecard | .github/workflows/scorecard.yml |
Scheduled supply-chain posture check (branch protection, token scopes, pinned deps, dangerous patterns); SARIF saved as a run artifact. |
| CODEOWNERS | .github/CODEOWNERS |
Requires owner review for changes under .github/ when branch protection enforces it. |
| zizmor policy | .github/zizmor.yml |
Accepts release-tag (ref-pin) pins so intentional @vN pins maintained by Dependabot are not flagged as unpinned-uses. |
| zizmor baseline | .github/zizmor-baseline.json |
Per-file accepted finding counts enforced by scripts/zizmor-ratchet.py. |
| Hygiene checks | scripts/workflow-hygiene.py |
Reusable-workflow rules that actionlint and zizmor can't express. |
scripts/workflow-hygiene.py enforces two rules on every workflow that exposes
workflow_call. Run it locally before pushing:
python3 scripts/workflow-hygiene.py
| Rule | Why |
|---|---|
No uses: ./.github/actions/... |
A reusable workflow runs in the caller's checkout, so a local path resolves against the consumer repo, not this one. Composite actions must use NeuralTrust/workflows/.github/actions/<name>@main. This is the exact bug that broke consumer deploys. |
Must declare workflow-level concurrency: |
Without one, a caller invoking the same reusable workflow twice in a run has no way to scope queueing, and superseded runs keep burning runner minutes. |
Self-triggered workflows in this repo (e.g. workflows-ci.yml, scorecard.yml)
are not covered by the concurrency rule — they aren't consumed by other repos
and set their own concurrency directly.
zizmor is a blocking gate, but is ratcheted rather than all-or-nothing. The
repo carries a backlog of template-injection findings where ${{ inputs.* }}
is interpolated directly into run: blocks. Those counts are pinned per file in
.github/zizmor-baseline.json; a PR fails when any file goes above its
baseline. New findings block, fixes are always allowed.
Disabling the audit outright was rejected: it would hide genuinely new findings.
A blanket --fix was also rejected, since it rewrites expansions inside quoted
heredocs where shell expansion never happens.
# reproduce the gate locally uvx zizmor@1.25.2 --config .github/zizmor.yml --no-exit-codes --format json . > zizmor.json python3 scripts/zizmor-ratchet.py zizmor.json # after fixing findings, lower the baseline so the fix can't regress python3 scripts/zizmor-ratchet.py zizmor.json --update
To work the backlog down, move the interpolation into an env: block and use
shell expansion (${VAR}, not ${{ env.VAR }}), then re-run with --update.
To enable the gates fully (recommended), turn on branch protection for
main with: require PR review, require "Workflows CI" status check, and require
review from Code Owners.
Run the setup script to configure all GCP and GitHub infrastructure in one go:
cd workflows
chmod +x setup-pipeline.sh
./setup-pipeline.shThe script handles:
- GCP Workload Identity Federation (WIF) pools and OIDC providers for dev and prod
- GCP Service Accounts with Artifact Registry writer permissions
- Cross-project read access (prod SA → dev registry) for the image promote strategy
- GitHub org-level variables (
DEV_GCP_PROJECT_ID,PROD_GCP_PROJECT_ID) - GitHub org-level secrets (WIF providers, service accounts, tokens)
Prerequisites: gcloud, gh, and jq must be installed and authenticated.
Organization-level variables (not sensitive):
| Name | Description |
|---|---|
DEV_GCP_PROJECT_ID |
Dev GCP project ID (source registry for promote, target for dev deploys) |
PROD_GCP_PROJECT_ID |
Prod GCP project ID (target for releases) |
Organization-level secrets:
| Name | What it is | Why it's needed |
|---|---|---|
DEV_WIF_PROVIDER |
GCP WIF provider path (dev project) | Authenticates dev deployments to GCP. Format: projects/<NUMBER>/locations/global/workloadIdentityPools/<POOL>/providers/<PROVIDER> |
DEV_WIF_SERVICE_ACCOUNT |
GCP SA email (dev project) | Identity for dev deployments. Format: <SA_NAME>@<PROJECT_ID>.iam.gserviceaccount.com |
PROD_WIF_PROVIDER |
GCP WIF provider path (prod project) | Authenticates prod releases to GCP. Same format as above |
PROD_WIF_SERVICE_ACCOUNT |
GCP SA email (prod project) | Identity for prod releases. Same format as above |
GH_TOKEN |
GitHub PAT with contents: write |
Single PAT for creating releases, pushing kustomization/changelog commits, and private Go module access. Must be a PAT (not GITHUB_TOKEN) so release events can trigger downstream workflows |
OPENAI_API_KEY |
OpenAI API key | Powers AI semver bump |
SLACK_WEBHOOK_URL |
Slack Incoming Webhook URL (optional) | Deploy/release/smoke-test notifications. Can be overridden per-repo with a repo-level secret for different Slack channels |
Note: Callers explicitly reference the correct org secret by name —
deploy.ymlusesDEV_*secrets,release.ymlusesPROD_*secrets. The reusable workflows accept genericWIF_PROVIDER/WIF_SERVICE_ACCOUNTinputs and are unchanged.
Per-repo secrets (only where needed):
| Secret | Description |
|---|---|
HUGGINGFACE_TOKEN |
HuggingFace model downloads (repos that use HF models) |
GOOGLE_API_KEY |
Google API access for tests (repos that need it) |
SLACK_WEBHOOK_URL |
Overrides the org-level webhook for per-repo Slack channels |
IAM requirements (for promote strategy):
The prod WIF service account needs read access to the dev Artifact Registry:
gcloud artifacts repositories add-iam-policy-binding nt-docker \ --project=<DEV_GCP_PROJECT_ID> \ --location=europe-west1 \ --member="serviceAccount:<PROD_WIF_SA_EMAIL>" \ --role="roles/artifactregistry.reader"
Run
./setup-pipeline.shfrom the workflows repo to automate the full WIF + secrets setup.
| Trigger | Docker Tags | Image Scan | Kustomization | config.env | Environment |
|---|---|---|---|---|---|
Push develop |
COMMIT_SHA, latest, cache |
Warn (non-blocking) | k8s/overlays/dev |
APPLICATION_VERSION=<sha> |
Dev |
PR to main |
— (no build) | — | — | — | — (CI only) |
Push main |
— (no build) | — | — | — | — (creates release) |
| GitHub Release (promote) | v1.2.3, SHA, latest, cache |
Block (dev source) | k8s/overlays/prod (+ prod-us when present) |
APPLICATION_VERSION=v1.2.3 |
Prod / US |
| GitHub Release (rebuild) | v1.2.3, SHA, latest, cache |
Block (prod image) | k8s/overlays/prod (+ prod-us when present) |
APPLICATION_VERSION=v1.2.3 |
Prod / US |
| Action | Description |
|---|---|
docker-build-push |
Build and push a Docker image (buildx) with shared tag/cache/secrets handling. Shared by all build/release workflows. |
kustomize-set-images |
Update image names/tags in kustomization.yaml via kustomize edit set image (pinned binary) instead of fragile sed. |
update-release-overlays |
Release helper: set image tags + APPLICATION_VERSION on k8s/overlays/prod and, when present, k8s/overlays/prod-us in one pass. |
git-commit-push |
Commit selected paths and push with pull --rebase + retry to avoid overlay-update races. Outputs the pushed commit SHA. |
free-disk-space |
Free runner disk space for large Docker builds. |
setup-crane |
Install a pinned crane binary for image promotion (crane copy). |
setup-kreuzberg |
Install Kreuzberg FFI + Tesseract OCR for Go CGO builds. Used by tests.yml when kreuzberg_enabled: true. |