Persist optional terminal backend identity on the task record (MIG-003) #1338
Workflow file for this run
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| name: Build | |
| on: | |
| pull_request: | |
| branches: [main] | |
| permissions: | |
| contents: read | |
| jobs: | |
| lint: | |
| runs-on: ubuntu-latest | |
| steps: | |
| - uses: actions/checkout@v5 | |
| - name: Setup Bun | |
| uses: oven-sh/setup-bun@v2 | |
| with: | |
| bun-version: "1.3.10" | |
| - name: Restore node_modules cache | |
| id: cache-deps | |
| uses: actions/cache/restore@v5 | |
| with: | |
| path: ./node_modules | |
| key: bun-deps-linux-x64-${{ hashFiles('bun.lock') }} | |
| - name: Install dependencies | |
| if: steps.cache-deps.outputs.cache-hit != 'true' | |
| run: bun install | |
| - name: Save node_modules cache | |
| if: steps.cache-deps.outputs.cache-hit != 'true' | |
| uses: actions/cache/save@v5 | |
| with: | |
| path: ./node_modules | |
| key: bun-deps-linux-x64-${{ hashFiles('bun.lock') }} | |
| - name: Generate build files | |
| run: bun scripts/generate-build-info.ts && bun scripts/generate-changelog.ts | |
| - name: Type check | |
| run: bun run lint | |
| test_shards: | |
| name: test (${{ matrix.shard }}/${{ strategy.job-total }}) | |
| runs-on: ubuntu-latest | |
| continue-on-error: true | |
| strategy: | |
| fail-fast: false | |
| matrix: | |
| shard: [1, 2, 3, 4, 5] | |
| steps: | |
| - uses: actions/checkout@v5 | |
| - name: Setup Bun | |
| uses: oven-sh/setup-bun@v2 | |
| with: | |
| bun-version: "1.3.10" | |
| - name: Restore node_modules cache | |
| id: cache-deps | |
| uses: actions/cache/restore@v5 | |
| with: | |
| path: ./node_modules | |
| key: bun-deps-linux-x64-${{ hashFiles('bun.lock') }} | |
| - name: Install dependencies | |
| if: steps.cache-deps.outputs.cache-hit != 'true' | |
| run: bun install | |
| - name: Save node_modules cache | |
| if: steps.cache-deps.outputs.cache-hit != 'true' && matrix.shard == 1 | |
| uses: actions/cache/save@v5 | |
| with: | |
| path: ./node_modules | |
| key: bun-deps-linux-x64-${{ hashFiles('bun.lock') }} | |
| - name: Generate build files | |
| run: bun scripts/generate-build-info.ts && bun scripts/generate-changelog.ts | |
| # Each suite is teed to a per-suite log so the aggregate `test` job can | |
| # surface the exact failing tests without anyone opening this shard job. | |
| # | |
| # `shell: bash` is MANDATORY on every step that pipes: the default `run:` | |
| # shell is `bash -e {0}` WITHOUT pipefail, so `vitest | tee` reports tee's | |
| # exit code (0) and a genuinely failing suite gets recorded as success — | |
| # which silently turned this gate into a rubber stamp once already. | |
| # `shell: bash` runs `bash --noprofile --norc -eo pipefail {0}`. | |
| - name: Run mainview tests | |
| id: mainview_tests | |
| continue-on-error: true | |
| shell: bash | |
| run: | | |
| mkdir -p test-shard-result | |
| bunx vitest run --shard=${{ matrix.shard }}/${{ strategy.job-total }} \ | |
| 2>&1 | tee "test-shard-result/shard-${{ matrix.shard }}-mainview.log" | |
| - name: Run bun tests | |
| id: bun_tests | |
| continue-on-error: true | |
| shell: bash | |
| run: | | |
| mkdir -p test-shard-result | |
| bunx vitest run --config vitest.config.bun.ts --shard=${{ matrix.shard }}/${{ strategy.job-total }} \ | |
| 2>&1 | tee "test-shard-result/shard-${{ matrix.shard }}-bun.log" | |
| - name: Run CLI tests | |
| id: cli_tests | |
| continue-on-error: true | |
| shell: bash | |
| run: | | |
| mkdir -p test-shard-result | |
| bunx vitest run --config vitest.config.cli.ts --shard=${{ matrix.shard }}/${{ strategy.job-total }} \ | |
| 2>&1 | tee "test-shard-result/shard-${{ matrix.shard }}-cli.log" | |
| - name: Record shard result | |
| if: always() | |
| env: | |
| SHARD: ${{ matrix.shard }} | |
| SHARD_TOTAL: ${{ strategy.job-total }} | |
| MAINVIEW_RESULT: ${{ steps.mainview_tests.outcome }} | |
| BUN_RESULT: ${{ steps.bun_tests.outcome }} | |
| CLI_RESULT: ${{ steps.cli_tests.outcome }} | |
| run: | | |
| mkdir -p test-shard-result | |
| printf '%s %s %s %s %s\n' "$SHARD" "$SHARD_TOTAL" "$MAINVIEW_RESULT" "$BUN_RESULT" "$CLI_RESULT" \ | |
| > "test-shard-result/shard-$SHARD.txt" | |
| # For each failed suite, distil a concise failure summary the aggregate | |
| # can print verbatim; drop the bulky raw logs so the artifact stays tiny. | |
| strip_ansi() { sed -E 's/\x1b\[[0-9;]*m//g'; } | |
| for suite in mainview bun cli; do | |
| case "$suite" in | |
| mainview) outcome="$MAINVIEW_RESULT" ;; | |
| bun) outcome="$BUN_RESULT" ;; | |
| cli) outcome="$CLI_RESULT" ;; | |
| esac | |
| log="test-shard-result/shard-$SHARD-$suite.log" | |
| fail="test-shard-result/shard-$SHARD-$suite.fail" | |
| if [ "$outcome" != "success" ] && [ -f "$log" ]; then | |
| strip_ansi < "$log" \ | |
| | grep -E ' FAIL |Test Files +[0-9]+ failed|Tests +[0-9]+ failed| × ' \ | |
| | head -80 > "$fail" || true | |
| if [ ! -s "$fail" ]; then | |
| strip_ansi < "$log" | tail -80 > "$fail" || true | |
| fi | |
| fi | |
| rm -f "$log" | |
| done | |
| - name: Upload shard result | |
| if: always() | |
| uses: actions/upload-artifact@v7 | |
| with: | |
| name: test-shard-${{ matrix.shard }} | |
| path: test-shard-result/ | |
| if-no-files-found: error | |
| retention-days: 1 | |
| test: | |
| name: test | |
| if: always() | |
| needs: test_shards | |
| runs-on: ubuntu-latest | |
| steps: | |
| - name: Download shard results | |
| uses: actions/download-artifact@v8 | |
| with: | |
| pattern: test-shard-* | |
| path: test-shard-results | |
| merge-multiple: true | |
| - name: Verify test shards | |
| run: | | |
| shopt -s nullglob | |
| result_files=(test-shard-results/shard-*.txt) | |
| failed=0 | |
| expected_total=0 | |
| declare -A seen=() | |
| failed_suites=() # "shard/total suite" for every non-success suite | |
| if [ "${#result_files[@]}" -eq 0 ]; then | |
| echo "::error::No test shard results were downloaded" | |
| exit 1 | |
| fi | |
| for result_file in "${result_files[@]}"; do | |
| if ! read -r shard total mainview bun cli < "$result_file"; then | |
| echo "::error::Malformed result in $result_file" | |
| failed=1 | |
| continue | |
| fi | |
| if [[ ! "$shard" =~ ^[1-9][0-9]*$ ]] || [[ ! "$total" =~ ^[1-9][0-9]*$ ]]; then | |
| echo "::error::Invalid shard identity in $result_file: shard=$shard total=$total" | |
| failed=1 | |
| continue | |
| fi | |
| if [ "$result_file" != "test-shard-results/shard-$shard.txt" ]; then | |
| echo "::error::Shard identity does not match artifact filename: $result_file declares $shard" | |
| failed=1 | |
| continue | |
| fi | |
| if [ "$expected_total" -eq 0 ]; then | |
| expected_total="$total" | |
| elif [ "$total" -ne "$expected_total" ]; then | |
| echo "::error::Inconsistent shard total in $result_file: expected=$expected_total actual=$total" | |
| failed=1 | |
| fi | |
| if [ "$shard" -gt "$total" ] || [[ -n "${seen[$shard]:-}" ]]; then | |
| echo "::error::Invalid or duplicate test shard $shard/$total" | |
| failed=1 | |
| continue | |
| fi | |
| seen[$shard]=1 | |
| echo "shard $shard/$total: mainview=$mainview bun=$bun cli=$cli" | |
| for suite in mainview bun cli; do | |
| case "$suite" in | |
| mainview) outcome="$mainview" ;; | |
| bun) outcome="$bun" ;; | |
| cli) outcome="$cli" ;; | |
| esac | |
| if [ "$outcome" != "success" ]; then | |
| failed=1 | |
| failed_suites+=("$shard/$total $suite") | |
| fi | |
| done | |
| done | |
| if [ "$expected_total" -eq 0 ] || [ "${#result_files[@]}" -ne "$expected_total" ]; then | |
| echo "::error::Expected $expected_total shard results, found ${#result_files[@]}" | |
| failed=1 | |
| else | |
| for ((shard = 1; shard <= expected_total; shard++)); do | |
| if [[ -z "${seen[$shard]:-}" ]]; then | |
| echo "::error::Missing result for test shard $shard/$expected_total" | |
| failed=1 | |
| fi | |
| done | |
| fi | |
| # Self-contained failure report: which shard, which suite, which tests — | |
| # visible right here and on the run summary, no need to open a shard job. | |
| if [ "${#failed_suites[@]}" -gt 0 ]; then | |
| echo "" | |
| echo "==================================================================" | |
| echo " ❌ TEST SHARDS FAILED (${#failed_suites[@]} suite(s)):" | |
| for entry in "${failed_suites[@]}"; do | |
| echo " ✗ shard ${entry%% *} · ${entry##* }" | |
| done | |
| echo "==================================================================" | |
| echo "" | |
| echo " This job only reads the shard artifacts — it never runs a test." | |
| echo " Re-running ONLY this job re-reads the same artifacts and fails" | |
| echo " identically. Re-run the whole workflow instead:" | |
| echo " gh run rerun $GITHUB_RUN_ID # all jobs" | |
| echo " gh run rerun $GITHUB_RUN_ID --failed # failed jobs (incl. shards)" | |
| echo "" | |
| { | |
| echo "## ❌ Failing test suites" | |
| echo "" | |
| for entry in "${failed_suites[@]}"; do | |
| echo "- **shard ${entry%% *}** · \`${entry##* }\`" | |
| done | |
| echo "" | |
| echo "> ⚠️ **Re-running the \`test\` job alone will not help.** It only reads the" | |
| echo "> shard artifacts; re-run the whole workflow with" | |
| echo "> \`gh run rerun $GITHUB_RUN_ID\` (or **Re-run all jobs**) so the shards" | |
| echo "> actually execute again." | |
| echo "" | |
| } >> "$GITHUB_STEP_SUMMARY" | |
| for entry in "${failed_suites[@]}"; do | |
| shard_total="${entry%% *}" | |
| suite="${entry##* }" | |
| shard="${shard_total%%/*}" | |
| fail="test-shard-results/shard-${shard}-${suite}.fail" | |
| echo "" | |
| echo "::group::❌ shard ${shard_total} · ${suite} — failing tests" | |
| if [ -s "$fail" ]; then | |
| cat "$fail" | |
| else | |
| echo "(no captured failure detail — inspect the shard job log)" | |
| fi | |
| echo "::endgroup::" | |
| first_fail="$(grep -E ' FAIL | × ' "$fail" 2>/dev/null | head -1 | sed -E 's/^[[:space:]]*//')" | |
| echo "::error title=Test shard ${shard_total} (${suite})::${first_fail:-suite ${suite} failed — see the failing-tests group above}" | |
| { | |
| echo "<details><summary>shard ${shard_total} · <code>${suite}</code></summary>" | |
| echo "" | |
| echo '```' | |
| if [ -s "$fail" ]; then cat "$fail"; else echo "(no captured detail)"; fi | |
| echo '```' | |
| echo "" | |
| echo "</details>" | |
| echo "" | |
| } >> "$GITHUB_STEP_SUMMARY" | |
| done | |
| fi | |
| exit "$failed" | |
| build-check: | |
| runs-on: ubuntu-latest | |
| steps: | |
| - uses: actions/checkout@v5 | |
| - name: Setup Bun | |
| uses: oven-sh/setup-bun@v2 | |
| with: | |
| bun-version: "1.3.10" | |
| - name: Restore node_modules cache | |
| id: cache-deps | |
| uses: actions/cache/restore@v5 | |
| with: | |
| path: ./node_modules | |
| key: bun-deps-linux-x64-${{ hashFiles('bun.lock') }} | |
| - name: Install dependencies | |
| if: steps.cache-deps.outputs.cache-hit != 'true' | |
| run: bun install | |
| - name: Save node_modules cache | |
| if: steps.cache-deps.outputs.cache-hit != 'true' | |
| uses: actions/cache/save@v5 | |
| with: | |
| path: ./node_modules | |
| key: bun-deps-linux-x64-${{ hashFiles('bun.lock') }} | |
| - name: Generate build files | |
| run: bun scripts/generate-build-info.ts && bun scripts/generate-changelog.ts | |
| - name: Build check | |
| run: bunx vite build && bun run build:cli | |
| trivy-scan: | |
| runs-on: ubuntu-latest | |
| permissions: | |
| contents: read | |
| pull-requests: write | |
| steps: | |
| - uses: actions/checkout@v5 | |
| - name: Run Trivy vulnerability scanner | |
| uses: aquasecurity/trivy-action@master | |
| with: | |
| scan-type: 'fs' | |
| scan-ref: '.' | |
| format: 'table' | |
| output: 'trivy-results.txt' | |
| exit-code: '0' | |
| severity: 'CRITICAL,HIGH,MEDIUM' | |
| trivy-config: '' | |
| env: | |
| TRIVY_INCLUDE_DEV_DEPS: 'true' | |
| continue-on-error: true | |
| - name: Post Trivy results to PR | |
| if: always() | |
| uses: actions/github-script@v8 | |
| with: | |
| script: | | |
| const fs = require('fs'); | |
| const { buildTrivyCommentBody } = require('./src/bun/trivy-pr-comment.js'); | |
| let body = null; | |
| if (fs.existsSync('trivy-results.txt')) { | |
| const results = fs.readFileSync('trivy-results.txt', 'utf8'); | |
| body = buildTrivyCommentBody(results); | |
| } else { | |
| body = '## 🔍 Trivy Vulnerability Scan\n\n⚠️ Trivy results file not found.\n'; | |
| } | |
| const { data: comments } = await github.rest.issues.listComments({ | |
| owner: context.repo.owner, | |
| repo: context.repo.repo, | |
| issue_number: context.issue.number, | |
| }); | |
| const trivyComment = comments.find((comment) => | |
| comment.body.includes('Trivy Vulnerability Scan'), | |
| ); | |
| if (!body) { | |
| if (trivyComment) { | |
| await github.rest.issues.deleteComment({ | |
| owner: context.repo.owner, | |
| repo: context.repo.repo, | |
| comment_id: trivyComment.id, | |
| }); | |
| } | |
| return; | |
| } | |
| if (trivyComment) { | |
| await github.rest.issues.updateComment({ | |
| owner: context.repo.owner, | |
| repo: context.repo.repo, | |
| comment_id: trivyComment.id, | |
| body, | |
| }); | |
| } else { | |
| await github.rest.issues.createComment({ | |
| owner: context.repo.owner, | |
| repo: context.repo.repo, | |
| issue_number: context.issue.number, | |
| body, | |
| }); | |
| } | |
| continue-on-error: true |