From 920fcd6a1a52acb10cafa5f210eaec0b235b67ee Mon Sep 17 00:00:00 2001 From: Samuel O'Neal Date: Mon, 31 Aug 2026 14:03:26 -0600 Subject: [PATCH] Add Pulumi preview to CI infra job MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Infra now always runs unit tests + vet on PRs, and additionally runs `pulumi preview --diff` (posted as a PR comment) when the infra, gui, or api code changes — since any of those can change what gets deployed. --- .gitea/workflows/ci.yml | 93 ++++++++++++++++++++++++++++++++++++++++- AGENTS.md | 2 +- 2 files changed, 92 insertions(+), 3 deletions(-) diff --git a/.gitea/workflows/ci.yml b/.gitea/workflows/ci.yml index f72e84c..8182802 100644 --- a/.gitea/workflows/ci.yml +++ b/.gitea/workflows/ci.yml @@ -1,7 +1,9 @@ # Runs on pull requests: unit tests plus a compile check. # Split into separate jobs (Odin, API, Infra) so they can run concurrently, # and gated by changed paths so each only runs when its code changes. -# Infra always runs regardless of what changed. +# Infra always runs (unit tests + vet) regardless of what changed; its Pulumi +# preview (posted as a PR comment) runs when the infra, gui, or api code +# changed, since any of those can change what gets deployed. name: CI @@ -28,6 +30,8 @@ jobs: outputs: odin: ${{ steps.filter.outputs.odin }} api: ${{ steps.filter.outputs.api }} + infra: ${{ steps.filter.outputs.infra }} + preview: ${{ steps.decide.outputs.run }} steps: - uses: dorny/paths-filter@v4 id: filter @@ -38,6 +42,19 @@ jobs: - 'scripts/**' api: - 'api/**' + infra: + - 'infra/**' + + - name: Decide whether to run the infra preview + id: decide + run: | + if [ "${{ steps.filter.outputs.infra }}" = "true" ] || \ + [ "${{ steps.filter.outputs.odin }}" = "true" ] || \ + [ "${{ steps.filter.outputs.api }}" = "true" ]; then + echo "run=true" >> "$GITHUB_OUTPUT" + else + echo "run=false" >> "$GITHUB_OUTPUT" + fi odin: name: Odin unit tests and build @@ -97,8 +114,13 @@ jobs: cargo fmt --all --check infra: - name: Infra unit tests and vet + name: Infra unit tests, vet, and preview + needs: changes runs-on: ubuntu-latest + permissions: + contents: read + pull-requests: write + issues: write steps: - uses: actions/checkout@v6 with: @@ -118,3 +140,70 @@ jobs: run: | cd infra go vet ./... + + # Pulumi CLI. State uses the local file backend (infra/Pulumi.yaml -> + # file://~/.pulumi), so no Pulumi Cloud token is needed. Preview diffs + # the current source against the last-known stack state; on an ephemeral + # runner the self-hosted environment must persist ~/.pulumi to get a + # meaningful diff (otherwise preview shows the pending creates). + - name: Install Pulumi + if: ${{ needs.changes.outputs.preview == 'true' }} + run: | + curl -fsSL https://get.pulumi.com | sh + echo "$HOME/.pulumi/bin" >> "$GITHUB_PATH" + + # The runner must reach the on-prem k3s cluster for preview to diff + # against live state. For a self-hosted runner this is the ambient + # kubeconfig; otherwise populate it from the KUBECONFIG secret here. + - name: Configure kubeconfig + if: ${{ needs.changes.outputs.preview == 'true' }} + run: | + mkdir -p "$HOME/.kube" + if [ -n "$KUBECONFIG_B64" ]; then + printf '%s' "$KUBECONFIG_B64" | base64 -d > "$HOME/.kube/config" + fi + env: + KUBECONFIG_B64: ${{ secrets.KUBECONFIG }} + + - name: Pulumi preview + if: ${{ needs.changes.outputs.preview == 'true' }} + working-directory: infra + env: + PULUMI_SKIP_UPDATE_CHECK: 'true' + run: | + go mod download + pulumi preview --diff 2>&1 | tee /tmp/pulumi-preview.txt || true + + - name: Comment preview on PR + if: ${{ needs.changes.outputs.preview == 'true' }} + env: + GITEA_API: ${{ gitea.api_url }} + REPO: ${{ gitea.repository }} + TOKEN: ${{ github.token }} + PR_NUMBER: ${{ github.event.pull_request.number }} + run: | + python3 - <<'PY' + import json, os, urllib.request + + with open('/tmp/pulumi-preview.txt') as f: + preview = f.read().strip() or '(no changes)' + + body = "### Pulumi preview\n\n```\n" + preview + "\n```" + payload = json.dumps({"body": body}).encode() + + url = ( + f"{os.environ['GITEA_API']}/repos/{os.environ['REPO']}" + f"/issues/{os.environ['PR_NUMBER']}/comments" + ) + req = urllib.request.Request( + url, + data=payload, + method='POST', + headers={ + 'Authorization': f"token {os.environ['TOKEN']}", + 'Content-Type': 'application/json', + }, + ) + urllib.request.urlopen(req) + print('Posted Pulumi preview comment') + PY diff --git a/AGENTS.md b/AGENTS.md index 3046628..a0ab0c2 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -18,7 +18,7 @@ The root `Makefile` is a lean delegator: base commands (`run`, `build`, `test`, - `api/` — Rust + axum backend API that will serve DESI catalog data. `src/lib.rs` exposes `routes`/`models`/`config`; `src/main.rs` is the server entrypoint (binds `API_BIND_ADDR`, default `0.0.0.0:8080`). Crate name is `desi-explorer-api` (lib `desi_explorer_api`); Rust version pinned in `api/rust-toolchain.toml`. `api/Makefile` owns the Rust targets. - `infra/` — Go + Pulumi (`runtime: go`) targeting the on-prem k3s cluster via the default kubeconfig, mirroring the `homelab` repo's pattern. Module name is `desi-explorer-infra`. `infra/Makefile` owns the Pulumi/Go targets. - `scripts/` — repo-level build helpers (Odin toolchain install). -- `.gitea/workflows/` — `ci.yml` (PR: install Odin, Rust, `odin test` + build, `cargo test` + clippy, `go test`) and `release.yml` (tag `v*`: build native + WASM + publish a Gitea release). +- `.gitea/workflows/` — `ci.yml` (PR: `odin test` + build, `cargo test` + clippy, `go test` + vet; infra always runs tests/vet and posts a `pulumi preview --diff` as a PR comment when infra/gui/api changed) and `release.yml` (tag `v*`: build native + WASM + publish a Gitea release). ## Conventions - Odin code lives in `gui/src/`; external deps go in `gui/lib/` and are wired via `-collection:lib=lib/local` (or a git submodule imported by relative path).