Repository navigation
Zero-idle self-hosted GitHub Actions runners for multi-tenant Kubernetes.
Warning
An unpinned kubectl get now returns v2, and v2 cannot show a deprecated FQDN alias. An EgressProxy still on CiliumFQDN or CalicoFQDN reads as egressPolicyMode: FQDN, with the alias carried in an annotation. Writing that view back keeps the alias. Replacing the object with a v2 manifest that lacks the annotation, as kubectl replace or a GitOps tool that replaces objects does, silently moves the pool onto whatever --fqdn-policy-backend names. Find any pool still on an alias before you upgrade: read Upgrading below.
Note
The five actions-gateway.com validating webhooks are renamed to end in -v2. An admission rejection now reads admission webhook "vrunnerset-v2.kb.io" denied the request, so an alert, log query or runbook matching the old -v2alpha1 names stops matching. The checks and their messages are unchanged.
Highlights
v2 is served beside v2beta1 on every actions-gateway.com kind. This is the release v2.0.0 rolls back to, which is why it exists. v2.0.0 will rewrite stored objects as v2, and a cluster cannot return to a release whose CustomResourceDefinitions (CRDs) do not define v2; serving both here is what gives that upgrade a way back. v2beta1 stays the storage version, so nothing stored is rewritten by this upgrade, and manifests can stay on v2beta1. v2 is identical to v2beta1 except that EgressProxy.spec.egressPolicyMode accepts only CIDR and FQDN.
The validating webhooks now validate v2. Each rule names v2, so the apiserver converts a write at v2alpha1, v2beta1 or v2 to v2 before the Gateway Manager Controller (GMC) validates it. v2.0.0 stops serving the other two versions, and a rule naming only those would stop matching without an error, so the rules move a release early and the v2 validators run for a whole release before v2.0.0 depends on them.
A new CiliumFQDN or CalicoFQDN EgressProxy is now rejected at admission. v2 does not define either alias and v2.0.0 removes every version that does, so the GMC stops the population growing while there is still time to drain it. Only a write that introduces an alias is refused: re-applying a pool already on one, editing the rest of its spec, and migrating it off are all still admitted, with the existing warning. Setting egressPolicyMode: FQDN and the matching GMC --fqdn-policy-backend enforces the same policy on the wire.
Prometheus now scrapes a v2 ActionsGateway's Actions Gateway Controller (AGC). The GMC created the AGC's metrics Service under v2 but not the ServiceMonitor that tells the Prometheus Operator to read it, so with metrics.serviceMonitor.enabled=true a v2 tenant's AGC metrics were never collected. It now creates <gateway>-agc-metrics beside the proxy's monitor, presenting the gateway's own <gateway>-agc-metrics-client bundle, and deletes it with the gateway.
A scale-set worker running a job is no longer reaped as orphaned. GitHub gives a scale-set job to whichever of the set's runners asks first, so the worker created for one job can end up running another. The AGC marked the worker named for a completed job as finished, and five minutes later killed whatever that worker was actually running as orphaned_running. Measured on the dogfood cluster on 2026-09-30: a runner was still running lint when the job it was registered for completed elsewhere, and its pod was reaped 3 seconds before lint finished. The AGC now reclaims the worker whose runner GitHub names as having held the completed job.
The default worker image moves to actions/runner 2.337.0. GitHub stops queueing jobs to a runner that has not been updated within 30 days of a newer release being made available, and the runner logs Listening for Jobs either way.
Upgrading
No breaking change, and nothing is removed. The upgrade is the ordinary one:
- Apply both charts' CRDs, as every upgrade does: the
actions-gateway-crds-v2render, andhelm show crdsfor the main chart'sPriorityClassAllowlist. Both now definev2. helm upgradethe release.- Wait for the GMC rollout to complete before writing any
actions-gateway.comobject. Until every GMC pod runs this release, the new webhook configuration can reach an old pod that does not serve the new-v2path, and the write fails with a webhook call error; retry once the rollout completes.
The full command and its options are in Upgrading.
Find EgressProxys still on a deprecated FQDN alias. Pin the read to v2beta1, because an unpinned read now returns v2 and shows every alias as FQDN. The pre-upgrade check has the commands. A pool it finds keeps running on this release; migrate it before v2.0.0, and migrate it deliberately before any tool replaces it with a v2 manifest.
Update anything matching the old webhook names. vactionsgateway-v2alpha1.kb.io, vegressproxy-v2alpha1.kb.io, vrunnerset-v2alpha1.kb.io, vrunnertemplate-v2alpha1.kb.io and vclusterrunnertemplate-v2alpha1.kb.io each become the same name ending -v2.kb.io.
Check a custom workerImage's runner version. The bump above changes only the default. A pool pinned to its own image keeps the runner it was built on, and RunnerVersionTooOld checks GitHub's registration floor rather than the 30-day window, so nothing on the cluster names the cause when jobs stop arriving (what to compare).
Rolling back is safe for stored objects, because they are stored at v2beta1. A manifest written at apiVersion: actions-gateway.com/v2 fails to apply on an older release, and the webhook-path error above can appear in reverse while the rollback runs.
Nothing else moves. No metric was removed or renamed, no field, default, condition reason or Event reason was removed or renamed, no existing default changed, no chart values key changed, and no command-line flag changed on any binary.
Deprecations
Nothing is removed in this release, and no notice is new. Every removal below was already announced, and this is the last release before v2.0.0 carries them out.
v2beta1, v2alpha1 and v1alpha1 are removed at v2.0.0, together with the Classic acquisition protocol. Move v2beta1 manifests to v2, which is a re-apply: v2 is now served, so this is the release to do it in. Tenants on v1alpha1 migrate with gag-migrate (Migrating a tenant to v2).
The CiliumFQDN and CalicoFQDN egressPolicyMode aliases are removed at v2.0.0. A new write naming one is now rejected, as above.
Everything since v1.8.0
107 commits. Six feat and fix commits touch the released surface, as scripts/release/semver-floor.sh reads it rather than reading commit subjects; the toolchain and dependency bumps under Project and tooling rebuild every binary too, and are not counted here. Build, CI, test, tooling and documentation work is left out and summarised under Project and tooling below.
Features (4)
Fixes (2)
Operator-visible work that ships in no image or chart (2)
- The
privileged-dindreference template runs ondocker:28-dind, matchingkata-dind(#2004) - An opt-in Dragonfly peer-to-peer back end for the registry mirrors: the five mirrors (each a CNCF Distribution registry) fetch through it, and workers never reach it directly, with its trust trade-off in Security operations (#2027, #2042)
API and metric surface
One new API version and one new annotation key, and nothing else. No field, enum value, default, metric, condition reason or Event reason was removed or renamed in any existing version, no existing default changed, and the chart's values keys are unchanged.
What v2 adds, and what it leaves out
actions-gateway.com/v2 is served on all six kinds: ActionsGateway, RunnerSet, RunnerTemplate, ClusterRunnerTemplate and EgressProxy from actions-gateway-crds-v2, and PriorityClassAllowlist from the main chart. Its fields, enum values and defaults are v2beta1's, with one difference: EgressProxy.spec.egressPolicyMode drops CiliumFQDN and CalicoFQDN, and the destinationFQDNs rule requires FQDN alone. A stored alias reaches the v2 view through a new annotation, conversion.actions-gateway.com/egress-policy-mode, which the conversion webhook writes and reads. It appears only on an EgressProxy still on an alias, and only in the v2 view; admission rejects a write that sets it to introduce an alias.
v2beta1 remains the storage version, and the conversion webhook's hub stays at v2beta1.
No new condition types or reasons, Event reasons, labels, metrics, CLI flags or chart values. Metric names declared outside tests are the same 74 as at v1.8.0.
Validation
v1.9.0-rc.2 passed its dogfood validation on 2026-10-05, the first candidate validated by the release gate running in CI rather than on a maintainer's machine. It is the candidate this release ships: check-artifact-unchanged.sh v1.9.0-rc.2 origin/main exits 0, so nothing on the released surface has moved since it was validated. The publish pipeline also refuses a stable tag until the gate's own evidence shows a candidate of its line passing every check in the table below after the first. rc.2 ships the same released files as rc.1, which passed the same gate run on a maintainer's machine on 2026-10-03. rc.2 was cut so the validation would also cover the DinD and Dragonfly e2e runs, and run in CI.
What was verified, and how
| Step | Verdict |
|---|---|
| Artifacts and provenance | PASS. 9/9 assets, draft: false, immutable: true; all eight cosign signatures verified. |
| Kata | PASS. The e2e matrix on Kata workers; both sizing profiles actuating, Throughput on 397 real samples and NodeShare deriving 1500m where the templates ask 2 and 3; the quota rung bound at zero headroom and released on restore. |
| CRD smoke | PASS. The signed v2 CRD manifest verified against the publish identity. |
v2 soak readings |
ALL POSITIVE. Every v2beta1 kind carried traffic; a round trip across v2alpha1 and v2beta1 left the spec identical; and all eight standing objects read the same at v2beta1 and v2, as did an EgressProxy applied at v2. |
| DinD | PASS. The e2e matrix on the shipped privileged-dind template, which this release moves to docker:28-dind. |
| Dragonfly | PASS. The e2e matrix on Kata with the registry mirrors' Dragonfly back end, every mirror serving and refusing uploads. |
The provenance is the check that discriminates: signatures prove who built an image, and only the digest proves what was built. The candidate's signer URI ends publish.yml@refs/tags/v1.9.0-rc.2 and its sourceRepositoryDigest is the tagged commit, 65ba797c2. Re-run against a deliberately wrong signer workflow it exits 1, so the pass discriminates rather than merely exiting 0.
The candidate took four CI runs, and none failed on the candidate. The fixes between them (#2038, #2040, #2042, #2043) changed the gate and the dogfood mirror configuration and no released file. The last run was taken once the publish pipeline could read the gate's evidence, so that a verdict it accepts exists.
Project and tooling
The Go toolchain moves from 1.26.6 to 1.27.1, and the Kubernetes client libraries from 0.36.4 to 0.37.1, so every binary is rebuilt even where its source did not change. Neither is a security fix: the bump recorded that Go's release history lists no security fix in 1.26.7, 1.26.8 or 1.27.1, and govulncheck was clean at 1.26.6. The toolchain moved because the image builder had been bumped to Go 1.27 while every module still declared 1.26.6, which would have built the release images on a toolchain CI never runs.
Security
No security advisory accompanies this release, and no reported vulnerability in this project's own code is patched.
The validating-webhook retype is the security-relevant change. Admission is a security boundary here, and v2.0.0 would otherwise have been the first release to run the v2-typed validators, beside a storage migration and four removals. Two checks read a value v2 has no field for, and each now reads it from a conversion annotation: the alias rejection reads the alias, and the scale-set label-uniqueness check reads the Classic protocol. Both are covered by envtest writes made at v2alpha1, so a write at an older version is checked as it was before.
A pull request that changes code runs govulncheck and a Trivy image scan of every built image; one that changes the chart runs a Polaris posture check of it.
Container images
Pin these immutable multi-arch index digests rather than the floating v1.9.0 tag. Each index serves both linux/amd64 and linux/arm64, so one pinned digest schedules on either.
- gmc:
ghcr.io/actions-gateway/gmc@sha256:31a16151c31b124e209877efd9966ac2450d35eca6c46134d05dcbe48435ce4d - agc:
ghcr.io/actions-gateway/agc@sha256:bf14f1ffb3c4a87cb5f8c3a3400114fde45b424edbb4dd384d4fc5b51e60a16c - proxy:
ghcr.io/actions-gateway/proxy@sha256:3e51f7f88d33fdcd04d5c9837920053ac6e44f24a9e7b864f603d0dcd84509be - worker:
ghcr.io/actions-gateway/worker@sha256:3a88809e9b3d9310c9e71504ad2048d6bb924d0e09814f50cb147f0ed92bc8eb - wrapper:
ghcr.io/actions-gateway/wrapper@sha256:83f76611062b793f61bd0e61c5c09bd0456aed3e96f95be03c10340e1fbaabb7
Verifying this release
We sign every image and both charts.
make verify-release VERSION=v1.9.0The signed v2 CRD manifest and SHA256SUMS ship as release assets with detached cosign bundles, and the publish workflow attests build provenance.