# Runs on version tags (e.g. `v1.0.0`) tagged off `main`: detects which # sections changed since the previous tag, builds only those, and runs # `pulumi up` to deploy. Infra always runs (it reconciles the cluster) while # the Odin web build and the API are only built/deployed when their code # changed. name: Deploy on: push: tags: - 'v*' concurrency: group: desi-explorer-deploy-${{ github.ref }} cancel-in-progress: true permissions: contents: read env: # Odin release to use (matches `dev-2026-07`, the version used locally). ODIN_VERSION: dev-2026-07 jobs: changes: name: Detect what changed since previous tag 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 }} steps: - uses: actions/checkout@v6 with: fetch-depth: 0 - name: Determine previous tag id: prev run: | git fetch --tags --force PREV="$(git tag --sort=-v:refname --list 'v*' | sed -n 2p)" if [ -z "$PREV" ]; then PREV="origin/main"; fi echo "prev=${PREV}" >> "$GITHUB_OUTPUT" echo "Comparing ${GITHUB_REF_NAME} against ${PREV}" - uses: dorny/paths-filter@v4 id: filter with: base: ${{ steps.prev.outputs.prev }} filters: | odin: - 'src/**' - 'lib/**' - 'scripts/**' - 'odinfmt.json' api: - 'api/**' infra: - 'infra/**' build-web: name: Build web (Odin/WASM) needs: changes if: ${{ needs.changes.outputs.odin == 'true' }} runs-on: ubuntu-latest steps: - uses: actions/checkout@v6 with: submodules: recursive - name: Install Odin ${{ env.ODIN_VERSION }} run: scripts/install_odin.sh - name: Install Emscripten run: | git clone --depth 1 https://github.com/emscripten-core/emsdk.git /tmp/emsdk /tmp/emsdk/emsdk install latest /tmp/emsdk/emsdk activate latest - name: Build WebAssembly run: | source /tmp/emsdk/emsdk_env.sh scripts/build_web.sh - name: Package run: | mkdir -p dist cd build/web python3 - <<'EOF' import glob, zipfile z = zipfile.ZipFile('../../dist/desi-explorer-web.zip', 'w', zipfile.ZIP_DEFLATED) for f in glob.glob('*'): z.write(f) EOF - uses: christopherHX/gitea-upload-artifact@v4 with: name: web path: dist/desi-explorer-web.zip build-api: name: Build API (Rust) 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: Build release binary run: | cd api cargo build --release - uses: christopherHX/gitea-upload-artifact@v4 with: name: api path: api/target/release/desi-explorer-api deploy: name: Deploy (infra up) needs: [changes, build-web, build-api] if: ${{ !failure() && !cancelled() }} runs-on: ubuntu-latest steps: - uses: actions/checkout@v6 with: submodules: recursive - uses: actions/setup-go@v6 with: go-version: '1.26' # Pulumi CLI. State uses the local file backend (infra/Pulumi.yaml -> # file://~/.pulumi), so no Pulumi Cloud token is needed — but the # self-hosted runner must persist ~/.pulumi between runs to keep state. - name: Install Pulumi run: | curl -fsSL https://get.pulumi.com | sh echo "$HOME/.pulumi/bin" >> "$GITHUB_PATH" # The runner must reach the on-prem k3s cluster. For a self-hosted runner # this is the ambient kubeconfig; otherwise populate it from a KUBECONFIG # secret here: - name: Configure kubeconfig 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: Deploy with Pulumi run: | go mod download pulumi up --yes --skip-preview working-directory: infra