name: NixOS CI on: pull_request: branches: - main push: branches: - main workflow_dispatch: permissions: contents: read concurrency: group: nixos-ci-${{ github.event.pull_request.number || github.run_id }} cancel-in-progress: ${{ github.event_name == 'pull_request' }} jobs: validate: name: Validate flake runs-on: ubuntu-latest timeout-minutes: 30 outputs: hosts: ${{ steps.hosts.outputs.hosts }} steps: - name: Checkout repository uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 with: persist-credentials: false - name: Install Nix uses: cachix/install-nix-action@8aa03977d8d733052d78f4e008a241fd1dbf36b3 # v31.10.6 with: extra_nix_config: | experimental-features = nix-command flakes accept-flake-config = true access-tokens = github.com=${{ github.token }} - name: Check flake and evaluate all outputs run: nix flake check --all-systems --no-build --show-trace - name: Discover NixOS hosts id: hosts run: | hosts=$(nix eval --json '.#nixosConfigurations' --apply 'configs: builtins.attrNames configs') echo "hosts=$hosts" >> "$GITHUB_OUTPUT" echo "Discovered hosts: $hosts" build: name: Build ${{ matrix.host }} needs: validate if: ${{ needs.validate.outputs.hosts != '[]' }} runs-on: ubuntu-latest timeout-minutes: 120 strategy: fail-fast: false matrix: host: ${{ fromJSON(needs.validate.outputs.hosts) }} steps: - name: Checkout repository uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 with: persist-credentials: false - name: Install Nix uses: cachix/install-nix-action@8aa03977d8d733052d78f4e008a241fd1dbf36b3 # v31.10.6 with: extra_nix_config: | experimental-features = nix-command flakes accept-flake-config = true access-tokens = github.com=${{ github.token }} - name: Build NixOS system run: | nix build ".#nixosConfigurations.${{ matrix.host }}.config.system.build.toplevel" \ --no-link \ --print-build-logs \ --show-trace report-main-status: name: Report main status needs: - validate - build if: ${{ always() && !cancelled() && github.event_name == 'push' && github.ref == 'refs/heads/main' }} runs-on: ubuntu-latest timeout-minutes: 5 permissions: contents: read issues: write env: CI_FAILED: ${{ needs.validate.result == 'failure' || needs.build.result == 'failure' }} JOB_RESULTS: ${{ toJSON(needs) }} steps: - name: Create or resolve failure issue uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9 with: script: | const owner = context.repo.owner; const repo = context.repo.repo; const title = "NixOS CI is failing on main"; const marker = ""; const failed = process.env.CI_FAILED === "true"; const jobs = JSON.parse(process.env.JOB_RESULTS); const failedJobs = Object.entries(jobs) .filter(([, job]) => job.result === "failure") .map(([name]) => `\`${name}\``) .join(", "); const runUrl = `${context.serverUrl}/${owner}/${repo}/actions/runs/${context.runId}`; const commitUrl = `${context.serverUrl}/${owner}/${repo}/commit/${context.sha}`; const issues = await github.paginate(github.rest.issues.listForRepo, { owner, repo, state: "open", per_page: 100, }); const existing = issues.find( (issue) => !issue.pull_request && issue.title === title && issue.body?.includes(marker), ); if (failed) { const body = [ marker, "The NixOS CI workflow failed after a push to `main`.", "", `- Failed jobs: ${failedJobs || "unknown"}`, `- Commit: [\`${context.sha.slice(0, 7)}\`](${commitUrl})`, `- Workflow run: [${context.runId}](${runUrl})`, "", "This issue is updated on subsequent failures and closed automatically after CI recovers.", ].join("\n"); if (existing) { await github.rest.issues.update({ owner, repo, issue_number: existing.number, body, }); } else { await github.rest.issues.create({ owner, repo, title, body }); } return; } if (existing) { await github.rest.issues.createComment({ owner, repo, issue_number: existing.number, body: `CI recovered in [workflow run ${context.runId}](${runUrl}).`, }); await github.rest.issues.update({ owner, repo, issue_number: existing.number, state: "closed", state_reason: "completed", }); }