Skip to content

Add main→dev0 AKS continuous deploy (OIDC, digest roll) - #8

Draft
cookieofcode wants to merge 5 commits into
mainfrom
cursor/dev0-aks-cd-1409
Draft

cookieofcode wants to merge 5 commits into
mainfrom
cursor/dev0-aks-cd-1409

Conversation

@cookieofcode

@cookieofcode cookieofcode commented Sep 15, 2026

Copy link
Copy Markdown
Owner

Summary

Every push to main builds the app image, pushes it to this repo’s GHCR package by digest (ghcr.io/cookieofcode/torqvoice), and rolls Deployment/torqvoice in namespace torqvoice on dev0 AKS.

This is app CD, not terraform apply. Hetzner staging/prod compose workflows are unchanged (they still pull upstream). Prod AKS is not a target. We do not push to ghcr.io/torqvoice/torqvoice (no write access; confirmed).

Quality: CD seam (this update)

Quality blocked this draft on workflow lint, not on infra. Terraform already ran on this head (not skip) after PR #13 landed on main. This update adds the missing PR check:

Check What it proves What it does not prove
Actionlint (skip vs run) Workflow syntax; SHA pins on deploy-dev0.yml Live Azure deploy
Terraform (already green, not skip) fmt / validate / env-gate fixtures terraform apply
Lint & Build / Playwright App CI CD. Deploy still only fires on push to main.
  • Pinned actionlint 1.7.12 (linux_amd64 checksum 8aca8db9…). No curl of actionlint@main.
  • Path filters live in a Paths job (same shape as Terraform / Playwright) so a required Actionlint status is green on app-only PRs instead of pending.
  • When this PR’s files are in the tree, Actionlint runs (it touches .github/workflows/deploy-dev0.yml).
  • No Azure secrets in PR CI. No apply. No azure/login. No GHCR push. No AKS roll.
  • Pre-existing Hetzner compose/rollback shellcheck findings are ignored in .github/actionlint.yaml; deploy-dev0.yml and this lint workflow still get shellcheck.

Why

Product rule: any version on main must run on dev0. Infra apply stays Leo-gated; the next apply must not revert the image CD just rolled. Security stamps OIDC/RBAC before merge.

Image store (cookieofcode GHCR only)

CD write + live pull ghcr.io/cookieofcode/torqvoice via GITHUB_TOKEN + packages:write (IMAGE_NAME: ${{ github.repository }})
First-apply pin only Temporary public upstream digest so the first terraform apply can create pods before this fork’s package exists. CD replaces it. We never push there.
How AKS pulls Preferred: make the cookieofcode package Public after the first Actions push (repo is already public). Fallback if it stays private: ESO + Key Vault dockerconfigjson (k8s/image-pull-secret.yaml.example). No Azure ACR.
Package today Does not exist yet (docker-publish is tag-only). First [Dev0] Deploy on main creates it.

What landed

  • .github/workflows/deploy-dev0.yml — GitHub-hosted runner; azure/login OIDC only; all uses: pinned by commit SHA; deploy by digest; rollback on failed roll. Still on: push to main only.
  • .github/workflows/actionlint.yml — PR check; skip vs run; actionlint 1.7.12 + SHA-pin assertion on deploy-dev0.yml.
  • .github/actionlint.yaml — Hetzner runner label; ignore pre-existing compose/rollback shellcheck.
  • kubernetes.tflifecycle.ignore_changes on the container image; maxSurge=0 for the RWO PVC.
  • identity.tf — optional namespace-scoped CD RBAC (not Owner/Contributor/Cluster Admin).
  • infra/azure/README.md — CD vs apply, Security stamp, cookieofcode GHCR as sole write target, public vs pull-secret.
  • Hetzner deploy-staging.yml / deploy-prod.yml not changed.

Security stamp (OIDC / RBAC)

Requirement In this PR
Federated subject exact repo:cookieofcode/torqvoice:ref:refs/heads/main only. Job has no environment:.
No wildcard subjects Documented forbid-list.
Named Environment alternative Documented, not enabled.
No long-lived Azure SP secrets/keys OIDC only. AZURE_* are GUIDs.
Actions pinned by SHA All seven uses: in deploy-dev0.yml (CI asserts this).
No app secret values in workflow YAML App secrets stay Key Vault → ESO.
Least-privilege deploy identity Cluster Reader + Cluster User + namespace torqvoice RBAC Writer.

Human setup before the first live roll (not this merge)

  1. Leo-approved apply so the cluster and Deployment exist.
  2. Entra app without a client secret + federated credential + the three GitHub secrets (IDs).
  3. Least-privilege RBAC (CLI or optional tfvars principal ID).
  4. First CD push creates ghcr.io/cookieofcode/torqvoice — set that package Public (or ESO ghcr-pull if it must stay private).

Team + Security verify; do not merge until Leo.

FinOps (cost-only)

Verdict

Acceptable (lean) — no new always-on Azure SKUs; spend is GitHub Actions minutes. No ACR. Actionlint downloads a ~2MB binary only when .github/workflows/** changes.

Ballpark

  • Monthly Azure run-rate: unchanged (~$90–110 CHF/USD lean baseline).
  • Delta: $0 always-on. Public GHCR and Entra OIDC are free. Private GHCR + ESO pull-secret adds no SKU (existing Key Vault).
  • Not included: Actions minutes per main push, GHCR pull egress, a few extra minutes on workflow-touching PRs.

Blockers

none

Should-fix

  • Azure Budget + alert on rg-torqvoice-dev0 (and tfstate RG) before any approved apply — unchanged recommendation.

Nice-to-have

  • Flip the new GHCR package to Public so AKS does not need a pull secret.

Risks

  • First CD fails closed (and rolls back) if the package is still private and no pull-secret is wired.
  • maxSurge=0 is the accepted single-node + RWO downtime trade.
Open in Web Open in Cursor 

cursoragent and others added 3 commits September 16, 2026 07:05
Every push to main builds a GHCR image by digest and rolls
Deployment/torqvoice on aks-torqvoice-dev0. Terraform ignores the live
container image so infra apply cannot revert CD. Auth is Entra federation
only; RBAC is namespace-scoped, not Cluster Admin.

Co-authored-by: Severin Pereto <cookieofcode@gmail.com>
Pin deploy-dev0 Actions by commit SHA, drop the job environment so the
OIDC subject stays repo:cookieofcode/torqvoice:ref:refs/heads/main, and
document the exact-subject allow-list, no-SP-secret rule, namespace-scoped
RBAC, and an optional Environment reviewer gate.

Co-authored-by: Severin Pereto <cookieofcode@gmail.com>
Leo confirmed no write access to ghcr.io/torqvoice/torqvoice. Keep the
upstream digest as a temporary first-apply pin only; CD push/pull source
of truth is ghcr.io/cookieofcode/torqvoice via GITHUB_TOKEN. Prefer a
public package; ESO pull-secret if it stays private. No ACR.

Co-authored-by: Severin Pereto <cookieofcode@gmail.com>
@cursor
cursor Bot force-pushed the cursor/dev0-aks-cd-1409 branch from 58d58a2 to f38dac4 Compare September 16, 2026 07:06
PRs that touch .github/workflows now run actionlint 1.7.12 (syntax)
and assert SHA pins on deploy-dev0.yml. Path filters live in a Paths
job so a required Actionlint status is green on app-only PRs. No
deploy, no apply, no Azure secrets.

Co-authored-by: Severin Pereto <cookieofcode@gmail.com>
@cursor

cursor Bot commented Sep 16, 2026

Copy link
Copy Markdown

CD-seam check is now green on this head.

The remaining red check is Playwright shard 3 of 4 in e2e/specs/security/files.spec.ts:118: route.fulfill: Route is already handled!. This is a pre-existing test bug where the loop registers a new page.route before the previous handler is removed; it is unrelated to the workflow/infra changes in this PR and should be fixed separately.

…load test

The forged-URL test registered a fresh page.route handler inside the loop
but unroute was called after the iteration, and a previous handler could
still be active when the next file input triggered the same route. This
caused route.fulfill to throw 'Route is already handled!' on shard 3.

Move the Documents panel open into a one-time setup, define a named
handler per iteration, and unroute it in a finally block before the next
forged URL is attempted. Security assertions are unchanged.

Co-authored-by: Severin Pereto <cookieofcode@gmail.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants