Skip to content

Persist optional terminal backend identity on the task record (MIG-003) #1338

Persist optional terminal backend identity on the task record (MIG-003)

Persist optional terminal backend identity on the task record (MIG-003) #1338

Workflow file for this run

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