[AI] Fix flaky Compare Sizes CI job

The job intermittently failed with "==> (not found) Artifact: build-stats"
due to a race condition: action-wait-for-check waited by commit SHA while
action-download-artifact searched by PR number, picking a newer still-running
workflow run whose artifacts hadn't been uploaded yet.

Fixes:
- Use commit SHA instead of PR number for PR artifact downloads to match
  the exact workflow run that was waited on
- Add if_no_artifact_found: warn to all download steps (defense in depth)
- Upgrade CLI download steps from v11 to v18 to support this parameter
- Add timeoutSeconds/intervalSeconds to all wait-for-check steps
- Expand failure detection to cover base branch builds and add warning
  step for incomplete builds
- Make bundle-stats-comment.mjs gracefully skip missing stats files
  instead of throwing
- Add if: !cancelled() to generate/post steps so they run after warnings

https://claude.ai/code/session_015TDkitgqM2TsgFSCuGaFuF
This commit is contained in:
Claude
2026-03-19 20:09:53 +00:00
parent f5a72448bd
commit 7318015a1f
2 changed files with 65 additions and 9 deletions

View File

@@ -50,6 +50,8 @@ jobs:
token: ${{ secrets.GITHUB_TOKEN }} token: ${{ secrets.GITHUB_TOKEN }}
checkName: web checkName: web
ref: ${{github.base_ref}} ref: ${{github.base_ref}}
timeoutSeconds: 1200
intervalSeconds: 30
- name: Wait for ${{github.base_ref}} API build to succeed - name: Wait for ${{github.base_ref}} API build to succeed
uses: fountainhead/action-wait-for-check@5a908a24814494009c4bb27c242ea38c93c593be # v1.2.0 uses: fountainhead/action-wait-for-check@5a908a24814494009c4bb27c242ea38c93c593be # v1.2.0
id: master-api-build id: master-api-build
@@ -57,6 +59,8 @@ jobs:
token: ${{ secrets.GITHUB_TOKEN }} token: ${{ secrets.GITHUB_TOKEN }}
checkName: api checkName: api
ref: ${{github.base_ref}} ref: ${{github.base_ref}}
timeoutSeconds: 1200
intervalSeconds: 30
- name: Wait for ${{github.base_ref}} CLI build to succeed - name: Wait for ${{github.base_ref}} CLI build to succeed
uses: fountainhead/action-wait-for-check@5a908a24814494009c4bb27c242ea38c93c593be # v1.2.0 uses: fountainhead/action-wait-for-check@5a908a24814494009c4bb27c242ea38c93c593be # v1.2.0
id: master-cli-build id: master-cli-build
@@ -64,6 +68,8 @@ jobs:
token: ${{ secrets.GITHUB_TOKEN }} token: ${{ secrets.GITHUB_TOKEN }}
checkName: cli checkName: cli
ref: ${{github.base_ref}} ref: ${{github.base_ref}}
timeoutSeconds: 1200
intervalSeconds: 30
- name: Wait for PR build to succeed - name: Wait for PR build to succeed
uses: fountainhead/action-wait-for-check@5a908a24814494009c4bb27c242ea38c93c593be # v1.2.0 uses: fountainhead/action-wait-for-check@5a908a24814494009c4bb27c242ea38c93c593be # v1.2.0
@@ -72,6 +78,8 @@ jobs:
token: ${{ secrets.GITHUB_TOKEN }} token: ${{ secrets.GITHUB_TOKEN }}
checkName: web checkName: web
ref: ${{github.event.pull_request.head.sha}} ref: ${{github.event.pull_request.head.sha}}
timeoutSeconds: 1200
intervalSeconds: 30
- name: Wait for API PR build to succeed - name: Wait for API PR build to succeed
uses: fountainhead/action-wait-for-check@5a908a24814494009c4bb27c242ea38c93c593be # v1.2.0 uses: fountainhead/action-wait-for-check@5a908a24814494009c4bb27c242ea38c93c593be # v1.2.0
id: wait-for-api-build id: wait-for-api-build
@@ -79,6 +87,8 @@ jobs:
token: ${{ secrets.GITHUB_TOKEN }} token: ${{ secrets.GITHUB_TOKEN }}
checkName: api checkName: api
ref: ${{github.event.pull_request.head.sha}} ref: ${{github.event.pull_request.head.sha}}
timeoutSeconds: 1200
intervalSeconds: 30
- name: Wait for CLI PR build to succeed - name: Wait for CLI PR build to succeed
uses: fountainhead/action-wait-for-check@5a908a24814494009c4bb27c242ea38c93c593be # v1.2.0 uses: fountainhead/action-wait-for-check@5a908a24814494009c4bb27c242ea38c93c593be # v1.2.0
id: wait-for-cli-build id: wait-for-cli-build
@@ -86,12 +96,32 @@ jobs:
token: ${{ secrets.GITHUB_TOKEN }} token: ${{ secrets.GITHUB_TOKEN }}
checkName: cli checkName: cli
ref: ${{github.event.pull_request.head.sha}} ref: ${{github.event.pull_request.head.sha}}
timeoutSeconds: 1200
intervalSeconds: 30
- name: Report build failure - name: Report build failure
if: steps.wait-for-web-build.outputs.conclusion == 'failure' || steps.wait-for-api-build.outputs.conclusion == 'failure' || steps.wait-for-cli-build.outputs.conclusion == 'failure' if: |
steps.wait-for-web-build.outputs.conclusion == 'failure' ||
steps.wait-for-api-build.outputs.conclusion == 'failure' ||
steps.wait-for-cli-build.outputs.conclusion == 'failure' ||
steps.master-web-build.outputs.conclusion == 'failure' ||
steps.master-api-build.outputs.conclusion == 'failure' ||
steps.master-cli-build.outputs.conclusion == 'failure'
run: | run: |
echo "Build failed on PR branch or ${{github.base_ref}}" echo "Build failed on PR branch or ${{github.base_ref}}"
exit 1 exit 1
- name: Warn on incomplete builds
if: |
steps.wait-for-web-build.outputs.conclusion != 'success' ||
steps.wait-for-api-build.outputs.conclusion != 'success' ||
steps.wait-for-cli-build.outputs.conclusion != 'success' ||
steps.master-web-build.outputs.conclusion != 'success' ||
steps.master-api-build.outputs.conclusion != 'success' ||
steps.master-cli-build.outputs.conclusion != 'success'
run: |
echo "::warning::Some builds did not complete successfully. Bundle stats may be incomplete."
echo "Base branch - web: ${{ steps.master-web-build.outputs.conclusion }}, api: ${{ steps.master-api-build.outputs.conclusion }}, cli: ${{ steps.master-cli-build.outputs.conclusion }}"
echo "PR - web: ${{ steps.wait-for-web-build.outputs.conclusion }}, api: ${{ steps.wait-for-api-build.outputs.conclusion }}, cli: ${{ steps.wait-for-cli-build.outputs.conclusion }}"
- name: Download web build artifact from ${{github.base_ref}} - name: Download web build artifact from ${{github.base_ref}}
uses: dawidd6/action-download-artifact@1f8785ff7a5130826f848e7f72725c85d241860f # v18 uses: dawidd6/action-download-artifact@1f8785ff7a5130826f848e7f72725c85d241860f # v18
@@ -102,6 +132,7 @@ jobs:
workflow_conclusion: '' # ignore the conclusion of the workflow, since we already checked it workflow_conclusion: '' # ignore the conclusion of the workflow, since we already checked it
name: build-stats name: build-stats
path: base path: base
if_no_artifact_found: warn
- name: Download API build artifact from ${{github.base_ref}} - name: Download API build artifact from ${{github.base_ref}}
uses: dawidd6/action-download-artifact@1f8785ff7a5130826f848e7f72725c85d241860f # v18 uses: dawidd6/action-download-artifact@1f8785ff7a5130826f848e7f72725c85d241860f # v18
id: pr-api-build id: pr-api-build
@@ -111,41 +142,46 @@ jobs:
workflow_conclusion: '' # ignore the conclusion of the workflow, since we already checked it workflow_conclusion: '' # ignore the conclusion of the workflow, since we already checked it
name: api-build-stats name: api-build-stats
path: base path: base
if_no_artifact_found: warn
- name: Download build stats from PR - name: Download build stats from PR
uses: dawidd6/action-download-artifact@1f8785ff7a5130826f848e7f72725c85d241860f # v18 uses: dawidd6/action-download-artifact@1f8785ff7a5130826f848e7f72725c85d241860f # v18
with: with:
pr: ${{github.event.pull_request.number}} commit: ${{github.event.pull_request.head.sha}}
workflow: build.yml workflow: build.yml
workflow_conclusion: '' # ignore the conclusion of the workflow, since we already checked it workflow_conclusion: '' # ignore the conclusion of the workflow, since we already checked it
name: build-stats name: build-stats
path: head path: head
allow_forks: true allow_forks: true
if_no_artifact_found: warn
- name: Download API stats from PR - name: Download API stats from PR
uses: dawidd6/action-download-artifact@1f8785ff7a5130826f848e7f72725c85d241860f # v18 uses: dawidd6/action-download-artifact@1f8785ff7a5130826f848e7f72725c85d241860f # v18
with: with:
pr: ${{github.event.pull_request.number}} commit: ${{github.event.pull_request.head.sha}}
workflow: build.yml workflow: build.yml
workflow_conclusion: '' # ignore the conclusion of the workflow, since we already checked it workflow_conclusion: '' # ignore the conclusion of the workflow, since we already checked it
name: api-build-stats name: api-build-stats
path: head path: head
allow_forks: true allow_forks: true
if_no_artifact_found: warn
- name: Download CLI build artifact from ${{github.base_ref}} - name: Download CLI build artifact from ${{github.base_ref}}
uses: dawidd6/action-download-artifact@ac66b43f0e6a346234dd65d4d0c8fbb31cb316e5 # v11 uses: dawidd6/action-download-artifact@1f8785ff7a5130826f848e7f72725c85d241860f # v18
with: with:
branch: ${{github.base_ref}} branch: ${{github.base_ref}}
workflow: build.yml workflow: build.yml
workflow_conclusion: '' # ignore the conclusion of the workflow, since we already checked it workflow_conclusion: '' # ignore the conclusion of the workflow, since we already checked it
name: cli-build-stats name: cli-build-stats
path: base path: base
if_no_artifact_found: warn
- name: Download CLI stats from PR - name: Download CLI stats from PR
uses: dawidd6/action-download-artifact@ac66b43f0e6a346234dd65d4d0c8fbb31cb316e5 # v11 uses: dawidd6/action-download-artifact@1f8785ff7a5130826f848e7f72725c85d241860f # v18
with: with:
pr: ${{github.event.pull_request.number}} commit: ${{github.event.pull_request.head.sha}}
workflow: build.yml workflow: build.yml
workflow_conclusion: '' # ignore the conclusion of the workflow, since we already checked it workflow_conclusion: '' # ignore the conclusion of the workflow, since we already checked it
name: cli-build-stats name: cli-build-stats
path: head path: head
allow_forks: true allow_forks: true
if_no_artifact_found: warn
- name: Strip content hashes from stats files - name: Strip content hashes from stats files
run: | run: |
if [ -f ./head/web-stats.json ]; then if [ -f ./head/web-stats.json ]; then
@@ -162,6 +198,7 @@ jobs:
fi fi
done done
- name: Generate combined bundle stats comment - name: Generate combined bundle stats comment
if: ${{ !cancelled() }}
run: | run: |
node packages/ci-actions/bin/bundle-stats-comment.mjs \ node packages/ci-actions/bin/bundle-stats-comment.mjs \
--base desktop-client=./base/web-stats.json \ --base desktop-client=./base/web-stats.json \
@@ -175,6 +212,7 @@ jobs:
--identifier combined \ --identifier combined \
--format pr-body > bundle-stats-comment.md --format pr-body > bundle-stats-comment.md
- name: Post combined bundle stats comment - name: Post combined bundle stats comment
if: ${{ !cancelled() }}
env: env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
GITHUB_REPOSITORY: ${{ github.repository }} GITHUB_REPOSITORY: ${{ github.repository }}

View File

@@ -5,7 +5,7 @@
* Heavily inspired by https://github.com/twk3/rollup-size-compare-action (MIT). * Heavily inspired by https://github.com/twk3/rollup-size-compare-action (MIT).
*/ */
import { readFile } from 'node:fs/promises'; import { access, readFile } from 'node:fs/promises';
import path from 'node:path'; import path from 'node:path';
import process from 'node:process'; import process from 'node:process';
@@ -179,8 +179,19 @@ function parseArgs(argv) {
} }
async function loadStats(filePath) { async function loadStats(filePath) {
const absolutePath = path.resolve(process.cwd(), filePath);
// Check if the file exists before trying to read it
try {
await access(absolutePath);
} catch {
console.error(
`[bundle-stats] Stats file not found: "${filePath}" — skipping`,
);
return null;
}
try { try {
const absolutePath = path.resolve(process.cwd(), filePath);
const fileContents = await readFile(absolutePath, 'utf8'); const fileContents = await readFile(absolutePath, 'utf8');
const parsed = JSON.parse(fileContents); const parsed = JSON.parse(fileContents);
@@ -196,7 +207,7 @@ async function loadStats(filePath) {
? error.message ? error.message
: 'Unknown error while parsing stats file'; : 'Unknown error while parsing stats file';
console.error(`[bundle-stats] Failed to parse "${filePath}": ${message}`); console.error(`[bundle-stats] Failed to parse "${filePath}": ${message}`);
throw new Error(`Failed to load stats file "${filePath}": ${message}`); return null;
} }
} }
@@ -687,6 +698,13 @@ async function main() {
); );
const headStats = await loadStats(section.headPath); const headStats = await loadStats(section.headPath);
if (!baseStats || !headStats) {
console.error(
`[bundle-stats] Skipping section "${section.name}": missing ${!baseStats ? 'base' : 'head'} stats`,
);
continue;
}
const statsDiff = getStatsDiff(baseStats, headStats); const statsDiff = getStatsDiff(baseStats, headStats);
const chunkDiff = getChunkModuleDiff(baseStats, headStats); const chunkDiff = getChunkModuleDiff(baseStats, headStats);