# 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 (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 on: pull_request: paths-ignore: - '*.md' permissions: contents: read env: # Odin release to use (matches `dev-2026-07`, the version used locally). # Bump this when you want CI to track a newer Odin release. ODIN_VERSION: dev-2026-07 jobs: changes: name: Detect changed paths runs-on: ubuntu-latest permissions: contents: read pull-requests: read 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 with: filters: | odin: - 'gui/**' - '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 needs: changes if: ${{ needs.changes.outputs.odin == 'true' }} runs-on: ubuntu-latest steps: - uses: actions/checkout@v6 with: submodules: recursive # Cache the extracted Odin install so only the first run downloads the # ~60 MB tarball; keyed on the pinned version (cache paths live outside # the checkout, so no sharing between jobs/refs). - name: Cache Odin uses: actions/cache@v4 with: path: /tmp/odin key: odin-${{ env.ODIN_VERSION }}-${{ runner.arch }} - name: Install Odin ${{ env.ODIN_VERSION }} run: scripts/install_odin.sh - name: Install clang run: | if command -v sudo >/dev/null 2>&1; then sudo apt-get update sudo apt-get install -y clang else apt-get update apt-get install -y clang fi - name: Run Odin unit tests run: make -C gui test - name: Verify build run: make -C gui build api: name: API unit tests and lint needs: changes if: ${{ needs.changes.outputs.api == 'true' }} runs-on: ubuntu-latest steps: - uses: actions/checkout@v6 with: submodules: recursive - name: Install Rust uses: dtolnay/rust-toolchain@stable - name: Test Rust API run: | cd api cargo test - name: Lint Rust API run: | cd api cargo clippy --all-targets -- -D warnings - name: Format-check Rust API run: | cd api cargo fmt --all --check infra: 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: submodules: recursive - uses: actions/setup-go@v6 with: go-version: '1.26' - name: Run Go tests run: | cd infra go mod download go test ./... - name: Vet Go code 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