mirror of
https://github.com/microsoft/vscode.git
synced 2026-07-10 16:58:55 -05:00
* CI: speed up node_modules cache with zstd + shared scripts
Switch the Linux/macOS node_modules cache from single-threaded gzip
(tar -czf) to multi-threaded zstd. The "Create node_modules archive"
step was spending ~5min of single-core gzip on a multi-GB tree on every
cache miss; zstd -T0 uses all cores and decompresses much faster, so
cache-hit jobs benefit too. Windows stays on 7-Zip (already threaded).
Extract the archive/extract commands into shared per-platform scripts
under .github/workflows/node_modules_cache/ (cache.sh / cache.ps1, each
dispatching on an archive|extract argument) so the format and flags live
in one place instead of being duplicated across ~8 workflows. Bump
build/.cachesalt to invalidate existing gzip caches.
Also remove the obsolete extensions/copilot CI workflows
(copilot-setup-steps.yml, ensure-node-modules-cache.yml, pr.yml) and the
unused build/listBuildCacheFiles.js, and drop their now-stale entries
(plus lit-html and signals-core) from .eslint-allowed-javascript-files.
* ci: seed copilot node_modules cache on main and rename cache keys
Add copilot-linux and copilot-windows jobs to pr-node-modules.yml so the
copilot node_modules cache is populated on main. Rename the copilot cache
keys to copilot-node_modules-linux / copilot-node_modules-windows in pr.yml.
* ci: extract node_modules cache into composite actions
Factor the repeated node_modules cache plumbing into two local composite
actions, restore-node-modules and save-node-modules, and migrate all
workflows that used the cache.sh/cache.ps1 archive flow (pr, pr-node-modules,
pr-{linux,darwin,win32}-test, copilot-setup-steps, component-fixtures,
css-order-scan).
- restore-node-modules computes the key, restores the cache, optionally
extracts on a hit, and exports the resolved key via $GITHUB_ENV.
- save-node-modules archives node_modules and saves it to the cache, reusing
the key exported by restore so callers don't repeat the prefix.
- Bespoke install steps stay in the workflows, so per-job env/secrets never
cross the action boundary.
- Only seed the cache on branch pushes (component-fixtures skips PRs, whose
caches aren't shared).
* save the node_modules cache for now to test it
* ci: fix node_modules cache save dropping the archive
cache.sh wrote its archive as cache.tzst, but actions/cache reserves that
name for its own tarball and passes --exclude cache.tzst, so our archive was
excluded and an empty (~200 B) cache was saved on Linux/macOS. Rename the
archive to node-modules.tzst and bump build/.cachesalt to invalidate the
broken cache entries.
* empty commit
* Remove again saving to the node modules cache from PR steps
431 lines
18 KiB
YAML
431 lines
18 KiB
YAML
name: Component Fixtures
|
|
|
|
on:
|
|
push:
|
|
branches:
|
|
- main
|
|
- 'release/*'
|
|
pull_request:
|
|
branches:
|
|
- main
|
|
- 'release/*'
|
|
|
|
permissions:
|
|
contents: read
|
|
statuses: write
|
|
pull-requests: write
|
|
id-token: write
|
|
|
|
concurrency:
|
|
group: component-fixtures-${{ github.event.pull_request.number || github.sha }}
|
|
cancel-in-progress: true
|
|
|
|
jobs:
|
|
screenshots:
|
|
name: Screenshots & Tests
|
|
runs-on: ubuntu-latest
|
|
steps:
|
|
- name: Checkout
|
|
uses: actions/checkout@v6
|
|
with:
|
|
# Need enough history for the merge-base lookup below to succeed even
|
|
# when the target branch has advanced since the PR was opened. Full
|
|
# clone would be wasteful for this large repo, so cap at 50.
|
|
fetch-depth: 50
|
|
|
|
- name: Setup Node.js
|
|
uses: actions/setup-node@v6
|
|
with:
|
|
node-version-file: .nvmrc
|
|
|
|
- name: Restore node_modules cache
|
|
id: cache-node-modules
|
|
uses: ./.github/actions/restore-node-modules
|
|
with:
|
|
key-prefix: node_modules-component-fixtures
|
|
key-args: "compile $(node -p process.arch)"
|
|
|
|
- name: Install dependencies
|
|
if: steps.cache-node-modules.outputs.cache-hit != 'true'
|
|
run: npm ci --ignore-scripts
|
|
env:
|
|
ELECTRON_SKIP_BINARY_DOWNLOAD: 1
|
|
PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD: 1
|
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
|
|
- name: Install build dependencies
|
|
if: steps.cache-node-modules.outputs.cache-hit != 'true'
|
|
run: npm ci
|
|
working-directory: build
|
|
|
|
- name: Install rspack dependencies
|
|
if: steps.cache-node-modules.outputs.cache-hit != 'true'
|
|
run: npm ci
|
|
working-directory: build/rspack
|
|
|
|
- name: Save node_modules cache
|
|
# Only seed the cache on branch pushes — PR-scoped caches are not shared with other refs.
|
|
if: github.event_name != 'pull_request' && steps.cache-node-modules.outputs.cache-hit != 'true'
|
|
uses: ./.github/actions/save-node-modules
|
|
|
|
- name: Copy codicons
|
|
run: cp node_modules/@vscode/codicons/dist/codicon.ttf src/vs/base/browser/ui/codicons/codicon/codicon.ttf
|
|
|
|
- name: Transpile source
|
|
run: npm run transpile-client
|
|
|
|
- name: Install Playwright Chromium
|
|
run: npx playwright install chromium
|
|
|
|
- name: Install Playwright test dependencies
|
|
run: npm ci
|
|
working-directory: test/componentFixtures/playwright
|
|
|
|
- name: Install Playwright Chromium (tests)
|
|
run: npx playwright install chromium
|
|
working-directory: test/componentFixtures/playwright
|
|
|
|
- name: Run Playwright fixture tests
|
|
run: npx playwright test
|
|
working-directory: test/componentFixtures/playwright
|
|
|
|
- name: Upload Playwright test results
|
|
if: failure()
|
|
uses: actions/upload-artifact@v7
|
|
with:
|
|
name: playwright-test-results
|
|
path: test/componentFixtures/playwright/test-results/
|
|
|
|
- name: Capture screenshots
|
|
run: ./node_modules/.bin/component-explorer render --project ./test/componentFixtures/component-explorer.json
|
|
|
|
- name: Check fixture errors
|
|
id: fixture_errors
|
|
if: always()
|
|
# TODO: "render" should just return a non-zero exit code if any fixture had an error (and print the errors), so we don't need to parse the manifest here.
|
|
# Then we can remove this step.
|
|
run: |
|
|
MANIFEST="test/componentFixtures/.screenshots/current/manifest.json"
|
|
if [ ! -f "$MANIFEST" ]; then
|
|
echo "::warning::No manifest found — render may have failed entirely"
|
|
exit 0
|
|
fi
|
|
# Log per-fixture errors but do not fail here — let later steps run
|
|
# (artifact upload, diff, PR comment) so failures are debuggable.
|
|
# The final "Fail if fixtures had errors" step turns this into a
|
|
# job failure.
|
|
if node -e "
|
|
const m = require('./$MANIFEST');
|
|
const errs = m.fixtures.filter(f => f.hasError);
|
|
if (!errs.length) { console.log('No fixture errors.'); process.exit(0); }
|
|
console.log(errs.length + ' fixture(s) with errors:');
|
|
for (const f of errs) {
|
|
const errorEvents = (f.events || []).filter(e => e.isError);
|
|
const msg = errorEvents.map(e => e.message).join('; ') || 'unknown error';
|
|
console.log('::error::' + f.fixtureId + ': ' + msg);
|
|
for (const e of errorEvents) {
|
|
if (e.stack) { console.log(' stack: ' + e.stack); }
|
|
}
|
|
const nonErrorEvents = (f.events || []).filter(e => !e.isError);
|
|
for (const e of nonErrorEvents) { console.log(' event: ' + JSON.stringify(e)); }
|
|
}
|
|
process.exit(1);
|
|
"; then
|
|
echo "has_errors=false" >> "$GITHUB_OUTPUT"
|
|
else
|
|
echo "has_errors=true" >> "$GITHUB_OUTPUT"
|
|
fi
|
|
|
|
- name: Check blocks-ci screenshots
|
|
id: blocks-ci
|
|
if: always()
|
|
# The service URL is baked into the committed blocks-ci-screenshots.md
|
|
# as `` (see
|
|
# build/lib/screenshotBlocksCi.ts). It is passed here so the script
|
|
# regenerates the markdown using the same URL prefix; otherwise the
|
|
# equality check against the committed file would always fail. No HTTP
|
|
# call is made — the URL is purely a string written into the markdown.
|
|
run: |
|
|
node build/lib/screenshotBlocksCi.ts \
|
|
test/componentFixtures/.screenshots/current/manifest.json \
|
|
test/componentFixtures/blocks-ci-screenshots.md \
|
|
https://hediet-screenshots.azurewebsites.net \
|
|
> /tmp/blocks-ci-updated.md 2>/tmp/blocks-ci-stderr.txt \
|
|
&& echo "match=true" >> "$GITHUB_OUTPUT" \
|
|
|| {
|
|
echo "match=false" >> "$GITHUB_OUTPUT"
|
|
cat /tmp/blocks-ci-stderr.txt >&2
|
|
CONTENT=$(cat /tmp/blocks-ci-updated.md)
|
|
echo "content<<BLOCKS_CI_EOF" >> "$GITHUB_OUTPUT"
|
|
echo "$CONTENT" >> "$GITHUB_OUTPUT"
|
|
echo "BLOCKS_CI_EOF" >> "$GITHUB_OUTPUT"
|
|
PATCH=$(diff -u test/componentFixtures/blocks-ci-screenshots.md /tmp/blocks-ci-updated.md || true)
|
|
echo "patch<<BLOCKS_CI_PATCH_EOF" >> "$GITHUB_OUTPUT"
|
|
echo "$PATCH" >> "$GITHUB_OUTPUT"
|
|
echo "BLOCKS_CI_PATCH_EOF" >> "$GITHUB_OUTPUT"
|
|
}
|
|
|
|
- name: Upload screenshots as artifact
|
|
uses: actions/upload-artifact@v7
|
|
if: always()
|
|
with:
|
|
name: screenshots
|
|
path: test/componentFixtures/.screenshots/current/
|
|
|
|
- name: Determine base SHA
|
|
id: base
|
|
run: |
|
|
if [ "${{ github.event_name }}" = "pull_request" ]; then
|
|
# For PRs, diff against the merge-base with the target branch.
|
|
TARGET_REF="origin/${{ github.event.pull_request.base.ref }}"
|
|
git fetch --no-tags --depth=50 origin "${{ github.event.pull_request.base.ref }}"
|
|
BASE_SHA=$(git merge-base "${{ github.sha }}" "$TARGET_REF")
|
|
else
|
|
# For push events, diff against the parent commit.
|
|
BASE_SHA=$(git rev-parse "${{ github.sha }}^")
|
|
fi
|
|
echo "base_sha=$BASE_SHA" >> "$GITHUB_OUTPUT"
|
|
echo "Using base SHA: $BASE_SHA (base for ${{ github.sha }})"
|
|
|
|
- name: Get OIDC token (non-fork only)
|
|
id: oidc
|
|
if: github.event_name == 'push' || (github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name == github.repository)
|
|
run: |
|
|
TOKEN=$(curl -sS -H "Authorization: bearer $ACTIONS_ID_TOKEN_REQUEST_TOKEN" \
|
|
"$ACTIONS_ID_TOKEN_REQUEST_URL&audience=https://hediet-screenshots.azurewebsites.net" \
|
|
| jq -r .value)
|
|
echo "::add-mask::$TOKEN"
|
|
echo "token=$TOKEN" >> "$GITHUB_OUTPUT"
|
|
|
|
- name: Upload screenshots to service
|
|
if: steps.oidc.outputs.token
|
|
run: |
|
|
cd test/componentFixtures/.screenshots/current
|
|
zip -qr "$GITHUB_WORKSPACE/screenshots.zip" .
|
|
set +e
|
|
STATUS=$(curl -sS -o /tmp/screenshot-upload.out -w '%{http_code}' \
|
|
-X POST "https://hediet-screenshots.azurewebsites.net/upload" \
|
|
-H "Content-Type: application/zip" \
|
|
-H "Authorization: Bearer $SCREENSHOT_SERVICE_TOKEN" \
|
|
--data-binary @"$GITHUB_WORKSPACE/screenshots.zip")
|
|
CURL_EXIT=$?
|
|
set -e
|
|
if [ "$CURL_EXIT" -ne 0 ]; then
|
|
echo "::warning::Screenshot service unreachable (curl exit $CURL_EXIT) -- skipping upload."
|
|
elif [ "${STATUS:0:1}" != "2" ]; then
|
|
echo "::warning::Screenshot service returned HTTP $STATUS -- skipping upload."
|
|
cat /tmp/screenshot-upload.out || true
|
|
else
|
|
echo "Uploaded screenshots (HTTP $STATUS)."
|
|
fi
|
|
env:
|
|
SCREENSHOT_SERVICE_TOKEN: ${{ steps.oidc.outputs.token }}
|
|
|
|
- name: Fetch base commit manifest
|
|
id: base_manifest
|
|
env:
|
|
BASE_SHA: ${{ steps.base.outputs.base_sha }}
|
|
run: |
|
|
BASE_MANIFEST=/tmp/base-manifest.json
|
|
URL="https://hediet-screenshots.azurewebsites.net/commits/${{ github.repository_owner }}/${{ github.event.repository.name }}/$BASE_SHA"
|
|
set +e
|
|
STATUS=$(curl -sS -o "$BASE_MANIFEST" -w '%{http_code}' "$URL")
|
|
CURL_EXIT=$?
|
|
set -e
|
|
if [ "$CURL_EXIT" -ne 0 ]; then
|
|
echo "::warning::Screenshot service unreachable (curl exit $CURL_EXIT) -- skipping diff."
|
|
elif [ "$STATUS" = "200" ]; then
|
|
echo "path=$BASE_MANIFEST" >> "$GITHUB_OUTPUT"
|
|
echo "Fetched base manifest for $BASE_SHA"
|
|
else
|
|
echo "::warning::Base manifest unavailable (HTTP $STATUS) -- skipping diff."
|
|
cat "$BASE_MANIFEST" || true
|
|
fi
|
|
|
|
- name: Diff screenshots
|
|
id: diff
|
|
if: always()
|
|
run: |
|
|
BODY=$(node build/lib/screenshotDiffReport.ts \
|
|
https://hediet-screenshots.azurewebsites.net \
|
|
"${{ steps.base.outputs.base_sha }}" \
|
|
${{ github.sha }} \
|
|
"${{ steps.base_manifest.outputs.path }}" \
|
|
test/componentFixtures/.screenshots/current/manifest.json)
|
|
if [ -n "$BODY" ]; then
|
|
echo "has_changes=true" >> "$GITHUB_OUTPUT"
|
|
echo "body<<SCREENSHOT_EOF" >> "$GITHUB_OUTPUT"
|
|
echo "$BODY" >> "$GITHUB_OUTPUT"
|
|
echo "SCREENSHOT_EOF" >> "$GITHUB_OUTPUT"
|
|
else
|
|
echo "No screenshot changes to report."
|
|
fi
|
|
if [ -f .tmp/screenshotDiffReport.json ]; then
|
|
echo "::group::Diff JSON"
|
|
cat .tmp/screenshotDiffReport.json
|
|
echo "::endgroup::"
|
|
fi
|
|
|
|
- name: Build comment body
|
|
id: comment_body
|
|
if: always() && (steps.diff.outputs.has_changes == 'true' || steps.blocks-ci.outputs.match == 'false')
|
|
# Single source of truth for the markdown body shared by the job
|
|
# summary (the only surface fork PRs see) and the non-fork PR comment.
|
|
# Fixture rendering errors are already part of COMMENT_BODY via the
|
|
# "Errored" section emitted by screenshotDiffReport.ts, so reporting
|
|
# errors is not tied to blocks-ci here.
|
|
run: |
|
|
{
|
|
if [ -n "$COMMENT_BODY" ]; then
|
|
printf '%s\n' "$COMMENT_BODY"
|
|
fi
|
|
if [ -n "$BLOCKS_CI_CONTENT" ]; then
|
|
if [ -n "$COMMENT_BODY" ]; then printf '\n---\n\n'; fi
|
|
printf '### blocks-ci screenshots changed\n\n'
|
|
printf 'Replace the contents of `test/componentFixtures/blocks-ci-screenshots.md` with:\n\n'
|
|
printf '<details>\n<summary>Updated blocks-ci-screenshots.md</summary>\n\n'
|
|
printf '```md\n%s\n```\n\n' "$BLOCKS_CI_CONTENT"
|
|
printf '</details>\n'
|
|
if [ -n "$BLOCKS_CI_PATCH" ]; then
|
|
printf '\n<details open>\n<summary>Patch</summary>\n\n'
|
|
printf '```diff\n%s\n```\n\n' "$BLOCKS_CI_PATCH"
|
|
printf '</details>\n'
|
|
fi
|
|
fi
|
|
} > /tmp/comment-body.md
|
|
echo "path=/tmp/comment-body.md" >> "$GITHUB_OUTPUT"
|
|
env:
|
|
COMMENT_BODY: ${{ steps.diff.outputs.body }}
|
|
BLOCKS_CI_CONTENT: ${{ steps.blocks-ci.outputs.content }}
|
|
BLOCKS_CI_PATCH: ${{ steps.blocks-ci.outputs.patch }}
|
|
|
|
- name: Write job summary
|
|
if: always() && steps.comment_body.outputs.path
|
|
run: cat "${{ steps.comment_body.outputs.path }}" >> "$GITHUB_STEP_SUMMARY"
|
|
|
|
- name: Post PR comment (non-fork PR only)
|
|
if: always() && github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name == github.repository
|
|
uses: actions/github-script@v9
|
|
with:
|
|
script: |
|
|
const fs = require('fs');
|
|
const marker = '<!-- screenshot-diff-report -->';
|
|
const bodyPath = process.env.COMMENT_BODY_PATH;
|
|
let body = bodyPath && fs.existsSync(bodyPath)
|
|
? fs.readFileSync(bodyPath, 'utf8').replace(/\s+$/, '')
|
|
: '';
|
|
|
|
const { data: comments } = await github.rest.issues.listComments({
|
|
owner: context.repo.owner,
|
|
repo: context.repo.repo,
|
|
issue_number: context.issue.number,
|
|
per_page: 100,
|
|
});
|
|
const existing = comments.find(c => c.body?.startsWith(marker));
|
|
|
|
if (!body) {
|
|
// No changes to report — update existing comment if present, otherwise do nothing
|
|
if (existing) {
|
|
const baseSha = (process.env.BASE_SHA || '').slice(0, 8);
|
|
const currentSha = (process.env.CURRENT_SHA || '').slice(0, 8);
|
|
let noChangesBody = '~No screenshot changes.~';
|
|
if (baseSha && currentSha) {
|
|
noChangesBody = `**Base:** \`${baseSha}\` **Current:** \`${currentSha}\`\n\n` + noChangesBody;
|
|
}
|
|
await github.rest.issues.updateComment({
|
|
owner: context.repo.owner,
|
|
repo: context.repo.repo,
|
|
comment_id: existing.id,
|
|
body: marker + '\n' + noChangesBody,
|
|
});
|
|
}
|
|
return;
|
|
}
|
|
|
|
body = marker + '\n' + body;
|
|
|
|
if (existing) {
|
|
await github.rest.issues.updateComment({
|
|
owner: context.repo.owner,
|
|
repo: context.repo.repo,
|
|
comment_id: existing.id,
|
|
body,
|
|
});
|
|
} else {
|
|
await github.rest.issues.createComment({
|
|
owner: context.repo.owner,
|
|
repo: context.repo.repo,
|
|
issue_number: context.issue.number,
|
|
body,
|
|
});
|
|
}
|
|
env:
|
|
COMMENT_BODY_PATH: ${{ steps.comment_body.outputs.path }}
|
|
BASE_SHA: ${{ steps.base.outputs.base_sha }}
|
|
CURRENT_SHA: ${{ github.sha }}
|
|
|
|
- name: Fail if blocks-ci hashes changed
|
|
if: steps.blocks-ci.outputs.match == 'false'
|
|
run: |
|
|
echo "::error::blocks-ci screenshot hashes do not match committed file. See PR comment or job summary for the updated content."
|
|
echo ""
|
|
echo "Diff between committed and expected blocks-ci-screenshots.md:"
|
|
diff -u test/componentFixtures/blocks-ci-screenshots.md /tmp/blocks-ci-updated.md || true
|
|
exit 1
|
|
|
|
- name: Fail if fixtures had errors
|
|
if: always() && steps.fixture_errors.outputs.has_errors == 'true'
|
|
run: |
|
|
echo "::error::One or more component fixtures failed to render. See the 'Check fixture errors' step for details."
|
|
exit 1
|
|
|
|
|
|
# - name: Prepare explorer artifact
|
|
# run: |
|
|
# mkdir -p /tmp/explorer-artifact/screenshot-report
|
|
# cp -r build/vite/dist/* /tmp/explorer-artifact/
|
|
# if [ -d test/componentFixtures/.screenshots/report ]; then
|
|
# cp -r test/componentFixtures/.screenshots/report/* /tmp/explorer-artifact/screenshot-report/
|
|
# fi
|
|
|
|
# - name: Upload explorer artifact
|
|
# uses: actions/upload-artifact@v7
|
|
# with:
|
|
# name: component-explorer
|
|
# path: /tmp/explorer-artifact/
|
|
|
|
|
|
# - name: Set check title
|
|
# env:
|
|
# GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
# run: |
|
|
# REPORT="test/componentFixtures/.screenshots/report/report.json"
|
|
# STATE="success"
|
|
# if [ -f "$REPORT" ]; then
|
|
# CHANGED=$(node -e "const r = require('./$REPORT'); console.log(r.summary.added + r.summary.removed + r.summary.changed)")
|
|
# TITLE="⚠ ${CHANGED} screenshots changed"
|
|
# BLOCKS_CI=$(node -e "
|
|
# const r = require('./$REPORT');
|
|
# const blocking = Object.entries(r.fixtures).filter(([, f]) =>
|
|
# f.status !== 'unchanged' && (f.labels || []).includes('blocks-ci')
|
|
# );
|
|
# if (blocking.length > 0) {
|
|
# console.log(blocking.map(([name]) => name).join(', '));
|
|
# }
|
|
# ")
|
|
# if [ -n "$BLOCKS_CI" ]; then
|
|
# STATE="failure"
|
|
# TITLE="❌ ${CHANGED} screenshots changed (blocks CI: ${BLOCKS_CI})"
|
|
# fi
|
|
# else
|
|
# TITLE="✅ Screenshots match"
|
|
# fi
|
|
|
|
# SHA="${{ github.event.pull_request.head.sha || github.sha }}"
|
|
# DETAILS_URL="https://hediet-ghartifactpreview.azurewebsites.net/${{ github.repository }}/run/${{ github.run_id }}/component-explorer/___explorer.html?report=./screenshot-report/report.json&search=changed"
|
|
|
|
# gh api "repos/${{ github.repository }}/statuses/$SHA" \
|
|
# --input - <<EOF || echo "::warning::Could not create commit status (expected for fork PRs)"
|
|
# {"state":"$STATE","target_url":"$DETAILS_URL","description":"$TITLE","context":"Component Screenshots"}
|
|
# EOF
|