chore: bump version to v0.0.821 #9335
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: Update Snapshots | |
| on: | |
| issue_comment: | |
| types: [created] | |
| permissions: | |
| actions: read | |
| contents: write | |
| issues: write | |
| pull-requests: write | |
| jobs: | |
| update-snapshots: | |
| name: Update snapshots | |
| if: | | |
| github.event_name == 'issue_comment' && | |
| github.event.issue.pull_request && | |
| github.event.comment.user.type != 'Bot' && | |
| ( | |
| startsWith(github.event.comment.body, '/update-snapshots') || | |
| startsWith(github.event.comment.body, '/usf') || | |
| startsWith(github.event.comment.body, '/us') | |
| ) && | |
| ( | |
| github.event.comment.author_association == 'OWNER' || | |
| github.event.comment.author_association == 'MEMBER' || | |
| github.event.comment.author_association == 'COLLABORATOR' | |
| ) | |
| runs-on: ${{ contains(github.event.comment.body, '--ubuntu-latest') && 'ubuntu-latest' || vars.BENCHMARK_RUNNER || 'blacksmith-32vcpu-ubuntu-2404-arm' }} | |
| timeout-minutes: 60 | |
| steps: | |
| - name: Parse command and create status comment | |
| id: parse | |
| uses: actions/github-script@v7 | |
| with: | |
| github-token: ${{ secrets.TSCIRCUIT_BOT_GITHUB_TOKEN }} | |
| script: | | |
| const pr = await github.rest.pulls.get({ | |
| owner: context.repo.owner, | |
| repo: context.repo.repo, | |
| pull_number: context.issue.number, | |
| }) | |
| const commandParts = (context.payload.comment.body || '').trim().split(/\s+/) | |
| const command = commandParts[0] || '' | |
| const focused = command === '/usf' | |
| const runnerMode = commandParts.includes('--ubuntu-latest') | |
| ? 'ubuntu-latest' | |
| : 'benchmark runner' | |
| const isSameRepo = pr.data.head.repo.full_name === `${context.repo.owner}/${context.repo.repo}` | |
| const title = focused ? '## 📸 Update Snapshots (focused)' : '## 📸 Update Snapshots' | |
| const statusLine = focused | |
| ? `⏳ Finding recent failed test files and updating relevant snapshots on \`${pr.data.head.sha.slice(0, 7)}\` using ${runnerMode}...` | |
| : `⏳ Running snapshot update on \`${pr.data.head.sha.slice(0, 7)}\` using ${runnerMode}...` | |
| const statusComment = await github.rest.issues.createComment({ | |
| owner: context.repo.owner, | |
| repo: context.repo.repo, | |
| issue_number: context.issue.number, | |
| body: [ | |
| title, | |
| '', | |
| statusLine, | |
| '', | |
| `🔗 Workflow: [View run](${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }})`, | |
| ].join('\n'), | |
| }) | |
| core.setOutput('head_sha', pr.data.head.sha) | |
| core.setOutput('head_ref', pr.data.head.ref) | |
| core.setOutput('can_push', isSameRepo ? 'true' : 'false') | |
| core.setOutput('focused', focused ? 'true' : 'false') | |
| core.setOutput('runner_mode', runnerMode) | |
| core.setOutput('status_comment_id', String(statusComment.data.id)) | |
| - name: Checkout PR branch | |
| if: steps.parse.outputs.can_push == 'true' | |
| uses: actions/checkout@v4 | |
| with: | |
| token: ${{ secrets.TSCIRCUIT_BOT_GITHUB_TOKEN }} | |
| ref: ${{ steps.parse.outputs.head_sha }} | |
| fetch-depth: 0 | |
| - name: Setup bun | |
| if: steps.parse.outputs.can_push == 'true' | |
| uses: oven-sh/setup-bun@v2 | |
| with: | |
| bun-version: 1.3.8 | |
| - name: Install dependencies | |
| if: steps.parse.outputs.can_push == 'true' | |
| run: bun install | |
| - name: Find failed test files | |
| if: steps.parse.outputs.can_push == 'true' && steps.parse.outputs.focused == 'true' | |
| id: failed_tests | |
| continue-on-error: true | |
| uses: actions/github-script@v7 | |
| with: | |
| github-token: ${{ secrets.TSCIRCUIT_BOT_GITHUB_TOKEN }} | |
| script: | | |
| const fs = require('fs') | |
| const owner = context.repo.owner | |
| const repo = context.repo.repo | |
| const headSha = '${{ steps.parse.outputs.head_sha }}' | |
| const headRef = '${{ steps.parse.outputs.head_ref }}' | |
| const failedConclusions = new Set(['failure', 'timed_out']) | |
| const stripAnsi = (value) => | |
| value.replace(/\u001b\[[0-9;]*m/g, '') | |
| const normalizeFile = (value) => value.replace(/^\.\//, '') | |
| const testFilePattern = /\b(?:\.\/)?tests\/[A-Za-z0-9_./-]+\.test\.(?:ts|tsx|js|jsx)\b/g | |
| const stackFilePattern = /\b(?:\.\/)?(tests\/[^\s:)]+\.test\.(?:ts|tsx|js|jsx)):\d+:\d+/g | |
| const fileHeaderPattern = /\b(?:\.\/)?(tests\/[^\s:]+\.test\.(?:ts|tsx|js|jsx)):\s*$/ | |
| const collectPathsFromLine = (line, files) => { | |
| testFilePattern.lastIndex = 0 | |
| let match | |
| while ((match = testFilePattern.exec(line)) !== null) { | |
| files.add(normalizeFile(match[0])) | |
| } | |
| } | |
| const extractFailedTestFiles = (rawLogs) => { | |
| const files = new Set() | |
| const lines = stripAnsi(rawLogs).split('\n') | |
| let currentFile = '' | |
| for (const line of lines) { | |
| if (line.includes('##[endgroup]')) { | |
| currentFile = '' | |
| continue | |
| } | |
| const headerMatch = line.match(fileHeaderPattern) | |
| if (headerMatch) { | |
| currentFile = normalizeFile(headerMatch[1]) | |
| } | |
| stackFilePattern.lastIndex = 0 | |
| let stackMatch | |
| while ((stackMatch = stackFilePattern.exec(line)) !== null) { | |
| files.add(normalizeFile(stackMatch[1])) | |
| } | |
| if (line.includes('(fail)')) { | |
| if (currentFile) files.add(currentFile) | |
| collectPathsFromLine(line, files) | |
| } | |
| } | |
| if (files.size === 0) { | |
| for (let i = 0; i < lines.length; i++) { | |
| if (!lines[i].includes('(fail)') && !/failed/i.test(lines[i])) continue | |
| for (let j = Math.max(0, i - 6); j <= Math.min(lines.length - 1, i + 6); j++) { | |
| collectPathsFromLine(lines[j], files) | |
| } | |
| } | |
| } | |
| return files | |
| } | |
| const downloadJobLogs = async (job) => { | |
| const response = await github.request('GET /repos/{owner}/{repo}/actions/jobs/{job_id}/logs', { | |
| owner, | |
| repo, | |
| job_id: job.id, | |
| }) | |
| const data = response.data | |
| if (typeof data === 'string') return data | |
| if (data instanceof ArrayBuffer) return Buffer.from(data).toString('utf8') | |
| if (ArrayBuffer.isView(data)) return Buffer.from(data).toString('utf8') | |
| return String(data) | |
| } | |
| const runs = await github.paginate(github.rest.actions.listWorkflowRunsForRepo, { | |
| owner, | |
| repo, | |
| branch: headRef, | |
| status: 'completed', | |
| per_page: 50, | |
| }) | |
| const dateValue = (value) => value.updated_at || value.completed_at || value.started_at || 0 | |
| const byDateDesc = (a, b) => new Date(dateValue(b)) - new Date(dateValue(a)) | |
| const exactShaRuns = runs | |
| .filter((run) => run.head_sha === headSha) | |
| .filter((run) => failedConclusions.has(run.conclusion)) | |
| .filter((run) => String(run.id) !== String(context.runId)) | |
| .sort(byDateDesc) | |
| const fallbackBranchRuns = runs | |
| .filter((run) => run.head_sha !== headSha) | |
| .filter((run) => failedConclusions.has(run.conclusion)) | |
| .sort(byDateDesc) | |
| const candidateRuns = [...exactShaRuns, ...fallbackBranchRuns].slice(0, 10) | |
| for (const run of candidateRuns) { | |
| const jobs = await github.paginate(github.rest.actions.listJobsForWorkflowRun, { | |
| owner, | |
| repo, | |
| run_id: run.id, | |
| per_page: 100, | |
| }) | |
| const failedJobs = jobs | |
| .filter((job) => failedConclusions.has(job.conclusion)) | |
| .sort(byDateDesc) | |
| const files = new Set() | |
| const testPlanNodes = new Set() | |
| for (const job of failedJobs) { | |
| try { | |
| const logs = await downloadJobLogs(job) | |
| const jobFiles = extractFailedTestFiles(logs) | |
| for (const file of jobFiles) { | |
| files.add(file) | |
| } | |
| const nodeMatch = job.name.match(/^test \((\d+)\)$/) | |
| if (jobFiles.size > 0 && nodeMatch) { | |
| testPlanNodes.add(Number(nodeMatch[1])) | |
| } | |
| } catch (error) { | |
| core.warning(`Unable to read logs for job ${job.id}: ${error.message}`) | |
| } | |
| } | |
| if (files.size > 0) { | |
| const relevantFiles = Array.from(files).filter((file) => fs.existsSync(file)).sort() | |
| if (relevantFiles.length === 0) continue | |
| core.setOutput('relevant_files', relevantFiles.join(' ')) | |
| core.setOutput('source_run_url', run.html_url) | |
| core.setOutput( | |
| 'test_plan_nodes', | |
| Array.from(testPlanNodes).sort((a, b) => a - b).join(' '), | |
| ) | |
| core.info(`Focused snapshot update will run: ${relevantFiles.join(' ')}`) | |
| core.info(`Failed test plan nodes: ${Array.from(testPlanNodes).join(', ') || 'unknown'}`) | |
| return | |
| } | |
| } | |
| core.warning('No failed test files were found in recent failed workflow logs; falling back to the full snapshot update.') | |
| core.setOutput('relevant_files', '') | |
| core.setOutput('source_run_url', '') | |
| core.setOutput('test_plan_nodes', '') | |
| - name: Install bun-test-plan | |
| if: steps.parse.outputs.can_push == 'true' && steps.failed_tests.outputs.test_plan_nodes != '' | |
| run: npm install -g @tscircuit/bun-test-plan | |
| - name: Generate test plans | |
| if: steps.parse.outputs.can_push == 'true' && steps.failed_tests.outputs.test_plan_nodes != '' | |
| run: bun-test-plan | |
| - name: Update snapshots | |
| if: steps.parse.outputs.can_push == 'true' | |
| id: update_snapshots | |
| env: | |
| BUN_UPDATE_SNAPSHOTS: "1" | |
| FOCUSED: ${{ steps.parse.outputs.focused }} | |
| RELEVANT_FILES: ${{ steps.failed_tests.outputs.relevant_files }} | |
| TEST_PLAN_NODES: ${{ steps.failed_tests.outputs.test_plan_nodes }} | |
| run: | | |
| set +e | |
| relevant_files="${RELEVANT_FILES}" | |
| exit_code=0 | |
| if [ "$FOCUSED" = "true" ] && [ -n "$TEST_PLAN_NODES" ]; then | |
| for node in $TEST_PLAN_NODES; do | |
| test_plan=".bun-test-plan/testplans/testplan${node}.txt" | |
| if [ ! -f "$test_plan" ]; then | |
| echo "Test plan not found: $test_plan" | |
| exit_code=1 | |
| continue | |
| fi | |
| mapfile -t test_files < "$test_plan" | |
| if [ "${#test_files[@]}" -eq 0 ]; then | |
| echo "No test files found in: $test_plan" | |
| exit_code=1 | |
| continue | |
| fi | |
| echo "Updating snapshots in CI shard $node (${#test_files[@]} test files)" | |
| bun test "${test_files[@]}" --timeout 300000 || exit_code=$? | |
| done | |
| elif [ "$FOCUSED" = "true" ] && [ -n "$relevant_files" ]; then | |
| echo "Running focused snapshot update: bun test --timeout 120_000 $relevant_files" | |
| bun test --timeout 120_000 $relevant_files || exit_code=$? | |
| else | |
| if [ "$FOCUSED" = "true" ]; then | |
| echo "No relevant failed test files found; falling back to the full snapshot update." | |
| fi | |
| bun test --timeout 120_000 || exit_code=$? | |
| fi | |
| echo "exit_code=$exit_code" >> "$GITHUB_OUTPUT" | |
| { | |
| echo "relevant_files<<EOF" | |
| printf '%s\n' "$relevant_files" | |
| echo "EOF" | |
| } >> "$GITHUB_OUTPUT" | |
| if [ "$exit_code" -ne 0 ]; then | |
| echo "bun test exited with code $exit_code; continuing so snapshot updates can still be committed." | |
| fi | |
| exit 0 | |
| - name: Verify focused tests | |
| if: steps.parse.outputs.can_push == 'true' && steps.parse.outputs.focused == 'true' && steps.failed_tests.outputs.relevant_files != '' | |
| id: verify_focused | |
| env: | |
| RELEVANT_FILES: ${{ steps.failed_tests.outputs.relevant_files }} | |
| TEST_PLAN_NODES: ${{ steps.failed_tests.outputs.test_plan_nodes }} | |
| run: | | |
| set +e | |
| exit_code=0 | |
| if [ -n "$TEST_PLAN_NODES" ]; then | |
| for node in $TEST_PLAN_NODES; do | |
| test_plan=".bun-test-plan/testplans/testplan${node}.txt" | |
| if [ ! -f "$test_plan" ]; then | |
| echo "Test plan not found: $test_plan" | |
| exit_code=1 | |
| continue | |
| fi | |
| mapfile -t test_files < "$test_plan" | |
| echo "Verifying CI shard $node (${#test_files[@]} test files)" | |
| bun test "${test_files[@]}" --timeout 300000 || exit_code=$? | |
| done | |
| else | |
| bun test $RELEVANT_FILES || exit_code=$? | |
| fi | |
| echo "exit_code=$exit_code" >> "$GITHUB_OUTPUT" | |
| if [ "$exit_code" -ne 0 ]; then | |
| echo "Focused verification exited with code $exit_code; continuing so snapshot updates can still be committed." | |
| fi | |
| exit 0 | |
| - name: Commit and push snapshot updates | |
| if: steps.parse.outputs.can_push == 'true' && always() | |
| id: commit | |
| env: | |
| BRANCH_NAME: ${{ steps.parse.outputs.head_ref }} | |
| EXPECTED_HEAD_SHA: ${{ steps.parse.outputs.head_sha }} | |
| FOCUSED: ${{ steps.parse.outputs.focused }} | |
| RELEVANT_FILES: ${{ steps.failed_tests.outputs.relevant_files }} | |
| run: | | |
| git config user.name "tscircuit-bot" | |
| git config user.email "actions@github.com" | |
| if [ "$FOCUSED" = "true" ] && [ -n "$RELEVANT_FILES" ]; then | |
| for test_file in $RELEVANT_FILES; do | |
| test_stem="${test_file%.test.ts}" | |
| test_stem="${test_stem%.test.tsx}" | |
| test_stem="${test_stem%.test.js}" | |
| test_stem="${test_stem%.test.jsx}" | |
| snapshot_dir="$(dirname "$test_stem")/__snapshots__" | |
| snapshot_base="$(basename "$test_stem")" | |
| if [ -d "$snapshot_dir" ]; then | |
| find "$snapshot_dir" -maxdepth 1 -type f -name "${snapshot_base}*.snap.svg" -exec git add -- {} + | |
| fi | |
| done | |
| else | |
| git add -A | |
| fi | |
| if git diff --cached --quiet; then | |
| echo "changed=false" >> "$GITHUB_OUTPUT" | |
| echo "commit_sha=" >> "$GITHUB_OUTPUT" | |
| echo "changed_files=" >> "$GITHUB_OUTPUT" | |
| exit 0 | |
| fi | |
| { | |
| echo "changed_files<<EOF" | |
| git diff --cached --name-only | |
| echo "EOF" | |
| } >> "$GITHUB_OUTPUT" | |
| git commit -m "Update test snapshots" | |
| remote_head="$(git ls-remote origin "refs/heads/$BRANCH_NAME" | cut -f1)" | |
| if [ "$remote_head" != "$EXPECTED_HEAD_SHA" ]; then | |
| echo "PR branch moved from $EXPECTED_HEAD_SHA to ${remote_head:-missing}; refusing to push stale snapshots." | |
| exit 1 | |
| fi | |
| git push origin "HEAD:refs/heads/$BRANCH_NAME" | |
| echo "changed=true" >> "$GITHUB_OUTPUT" | |
| echo "commit_sha=$(git rev-parse HEAD)" >> "$GITHUB_OUTPUT" | |
| - name: Mark workflow failed when bun test fails | |
| if: | | |
| steps.parse.outputs.can_push == 'true' && | |
| always() && | |
| ( | |
| steps.update_snapshots.outputs.exit_code != '0' || | |
| ( | |
| steps.parse.outputs.focused == 'true' && | |
| steps.failed_tests.outputs.relevant_files != '' && | |
| steps.verify_focused.outputs.exit_code != '0' | |
| ) | |
| ) | |
| run: exit 1 | |
| - name: Update status comment | |
| if: always() | |
| uses: actions/github-script@v7 | |
| env: | |
| CAN_PUSH: ${{ steps.parse.outputs.can_push }} | |
| CHANGED: ${{ steps.commit.outputs.changed }} | |
| COMMIT_SHA: ${{ steps.commit.outputs.commit_sha }} | |
| FOCUSED: ${{ steps.parse.outputs.focused }} | |
| RELEVANT_FILES: ${{ steps.failed_tests.outputs.relevant_files }} | |
| SOURCE_RUN_URL: ${{ steps.failed_tests.outputs.source_run_url }} | |
| TEST_PLAN_NODES: ${{ steps.failed_tests.outputs.test_plan_nodes }} | |
| CHANGED_FILES: ${{ steps.commit.outputs.changed_files }} | |
| RUNNER_MODE: ${{ steps.parse.outputs.runner_mode }} | |
| RUNNER_ARCH: ${{ runner.arch }} | |
| BUN_VERSION: "1.3.8" | |
| TEST_EXIT_CODE: ${{ steps.update_snapshots.outputs.exit_code }} | |
| VERIFY_EXIT_CODE: ${{ steps.verify_focused.outputs.exit_code }} | |
| with: | |
| github-token: ${{ secrets.TSCIRCUIT_BOT_GITHUB_TOKEN }} | |
| script: | | |
| const runUrl = `${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}` | |
| const focused = process.env.FOCUSED === 'true' | |
| const relevantFiles = (process.env.RELEVANT_FILES || '').trim().split(/\s+/).filter(Boolean) | |
| const testPlanNodes = (process.env.TEST_PLAN_NODES || '').trim().split(/\s+/).filter(Boolean) | |
| const changedFiles = (process.env.CHANGED_FILES || '').trim().split(/\s+/).filter(Boolean) | |
| const title = focused ? '## 📸 Update Snapshots (focused)' : '## 📸 Update Snapshots' | |
| const focusedDetails = () => { | |
| if (!focused) return [] | |
| if (relevantFiles.length === 0) { | |
| return ['', 'Focused mode did not find recent failed test files; ran the full snapshot update.'] | |
| } | |
| const shownFiles = relevantFiles.slice(0, 20).map((file) => `- \`${file}\``) | |
| const hiddenCount = relevantFiles.length - shownFiles.length | |
| return [ | |
| '', | |
| 'Focused test files:', | |
| ...shownFiles, | |
| ...(hiddenCount > 0 ? [`- ...and ${hiddenCount} more`] : []), | |
| ...(testPlanNodes.length > 0 | |
| ? [`CI test shard${testPlanNodes.length === 1 ? '' : 's'}: ${testPlanNodes.map((node) => `\`${node}\``).join(', ')}`] | |
| : []), | |
| ...(process.env.SOURCE_RUN_URL ? [``, `Source failure run: ${process.env.SOURCE_RUN_URL}`] : []), | |
| ...(changedFiles.length > 0 | |
| ? ['', 'Changed snapshots:', ...changedFiles.map((file) => `- \`${file}\``)] | |
| : []), | |
| ] | |
| } | |
| const makeBody = (messageLines) => [ | |
| title, | |
| '', | |
| ...messageLines, | |
| ...focusedDetails(), | |
| '', | |
| `Environment: ${process.env.RUNNER_MODE} (\`${process.env.RUNNER_ARCH}\`), Bun \`${process.env.BUN_VERSION}\``, | |
| '', | |
| `🔗 Workflow: [View run](${runUrl})`, | |
| ].join('\n') | |
| const testFailed = process.env.TEST_EXIT_CODE && process.env.TEST_EXIT_CODE !== '0' | |
| const verifyFailed = process.env.VERIFY_EXIT_CODE && process.env.VERIFY_EXIT_CODE !== '0' | |
| let body = '' | |
| if (process.env.CAN_PUSH !== 'true') { | |
| body = makeBody([ | |
| '⚠️ This command only supports PR branches from this repository (not forks).', | |
| ]) | |
| } else if ((testFailed || verifyFailed) && process.env.CHANGED === 'true') { | |
| const failureLines = [ | |
| ...(testFailed ? [`Snapshot update test exited with code ${process.env.TEST_EXIT_CODE}.`] : []), | |
| ...(verifyFailed ? [`Focused verification exited with code ${process.env.VERIFY_EXIT_CODE}.`] : []), | |
| ] | |
| body = makeBody([ | |
| `⚠️ Snapshot updates were committed to \`${(process.env.COMMIT_SHA || '').slice(0, 7)}\`, but not all tests passed.`, | |
| ...failureLines, | |
| ]) | |
| } else if (testFailed || verifyFailed) { | |
| body = makeBody([ | |
| '❌ Snapshot update failed.', | |
| ...(testFailed ? [`Snapshot update test exited with code ${process.env.TEST_EXIT_CODE}.`] : []), | |
| ...(verifyFailed ? [`Focused verification exited with code ${process.env.VERIFY_EXIT_CODE}.`] : []), | |
| ]) | |
| } else if ('${{ job.status }}' !== 'success') { | |
| body = makeBody([ | |
| '❌ Snapshot update failed. Please inspect workflow logs.', | |
| ]) | |
| } else if (process.env.CHANGED === 'true') { | |
| body = makeBody([ | |
| `✅ Snapshot updates committed to \`${(process.env.COMMIT_SHA || '').slice(0, 7)}\`.`, | |
| ]) | |
| } else { | |
| body = makeBody([ | |
| '✅ No snapshot changes were produced; nothing to commit.', | |
| ]) | |
| } | |
| await github.rest.issues.updateComment({ | |
| owner: context.repo.owner, | |
| repo: context.repo.repo, | |
| comment_id: Number('${{ steps.parse.outputs.status_comment_id }}'), | |
| body, | |
| }) |