diff --git a/.github/workflows/track-vendor.yml b/.github/workflows/track-vendor.yml new file mode 100644 index 0000000000..938c239f95 --- /dev/null +++ b/.github/workflows/track-vendor.yml @@ -0,0 +1,80 @@ +name: Track vendor + +on: + workflow_dispatch: + schedule: + - cron: "0 0 * * *" + +permissions: + contents: write + pull-requests: write + +jobs: + track-vendor: + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v4 + + - uses: pnpm/action-setup@v4 + + - uses: actions/setup-node@v4 + with: + node-version: 24 + cache: pnpm + + - run: pnpm install + + - name: Track vendor and get hash + id: track + shell: bash + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + node ./scripts/track-vendor.ts + + if git diff --quiet; then + echo "No unstaged changes." + echo "dirty=false" >> "$GITHUB_OUTPUT" + else + echo "Found unstaged changes." + echo "dirty=true" >> "$GITHUB_OUTPUT" + fi + + - name: Create branch and draft PR + if: steps.track.outputs.track_hash != '' && steps.track.outputs.dirty == 'true' + shell: bash + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + HASH="${{ steps.track.outputs.track_hash }}" + BRANCH="track/$HASH" + + if git ls-remote --exit-code --heads origin "$BRANCH" >/dev/null 2>&1; then + echo "Remote branch already exists: $BRANCH" + exit 0 + else + echo "Remote branch does not exist: $BRANCH" + git switch -c "$BRANCH" + fi + + git config user.name "github-actions[bot]" + git config user.email "github-actions[bot]@users.noreply.github.com" + + git add . + + if git diff --cached --quiet; then + echo "Nothing to commit. Exit successfully." + exit 0 + fi + + git commit -m "chore(vendor): update vendor snapshot" + + git push --set-upstream origin "$BRANCH" + + gh pr create \ + --draft \ + --base "${GITHUB_REF_NAME}" \ + --head "$BRANCH" \ + --title "chore(vendor): update vendor \`$HASH\`" \ + --body "Vendor snapshot updated. Hash: \`$HASH\`" diff --git a/package.json b/package.json index 277136436e..2b9e0a6885 100644 --- a/package.json +++ b/package.json @@ -6,6 +6,7 @@ "watch": "tsgo -b -w", "test": "npm run build && vitest run", "test:grammar": "vitest run extensions/vscode/tests/grammar.spec.ts", + "test:vendor": "TEST_VENDOR=1 vitest run vendor", "format": "dprint fmt", "lint": "tsslint --project {tsconfig.json,packages/*/tsconfig.json,extensions/*/tsconfig.json}", "lint:fix": "npm run lint -- --fix" diff --git a/scripts/track-vendor.ts b/scripts/track-vendor.ts new file mode 100644 index 0000000000..e43fd3b0db --- /dev/null +++ b/scripts/track-vendor.ts @@ -0,0 +1,18 @@ +import { appendFileSync, readFileSync } from 'node:fs'; +import * as path from 'node:path'; +import { fileURLToPath } from 'node:url'; +import { crc32 } from 'node:zlib'; +import { startVitest } from 'vitest/node'; + +const vitest = await startVitest('test', ['vendor'], { update: true, run: true, env: { TEST_VENDOR: '1' } }); +await vitest?.close(); + +const dir = path.dirname(fileURLToPath(import.meta.url)); +const TARGET_FILE = path.join(dir, '..', 'tests', '__snapshots__', 'vendor.spec.ts.snap'); +const hex = (crc32(readFileSync(TARGET_FILE)) >>> 0).toString(16).padStart(8, '0'); +console.log(`track hash: ${hex}`); + +const out = process.env.GITHUB_OUTPUT; +if (out) { + appendFileSync(out, `track_hash=${hex}\n`); +} diff --git a/tests/__snapshots__/vendor.spec.ts.snap b/tests/__snapshots__/vendor.spec.ts.snap new file mode 100644 index 0000000000..61c8825c62 --- /dev/null +++ b/tests/__snapshots__/vendor.spec.ts.snap @@ -0,0 +1,41 @@ +// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html + +exports[`ensure vendor is updated 1`] = ` +[ + { + "commit": "96acaa52902feb1320e1d8ec8936b8669cca447d", + "path": "src/services/types.ts", + "repo": "microsoft/TypeScript", + }, + { + "commit": "a134f3050c22fe80954241467cd429811792a81d", + "path": "src/services/htmlFormatter.ts", + "repo": "microsoft/vscode-html-languageservice", + }, + { + "commit": "210541906e5a96ab39f9c753f921b1bd35f4138b", + "path": "extensions/css/syntaxes/css.tmLanguage.json", + "repo": "microsoft/vscode", + }, + { + "commit": "45324363153075dab0482312ae24d8c068d81e4f", + "path": "extensions/html/syntaxes/html.tmLanguage.json", + "repo": "microsoft/vscode", + }, + { + "commit": "210541906e5a96ab39f9c753f921b1bd35f4138b", + "path": "extensions/javascript/syntaxes/JavaScript.tmLanguage.json", + "repo": "microsoft/vscode", + }, + { + "commit": "cf8d61ebd2f022f4ce8280171f0360d1fe0a206d", + "path": "extensions/scss/syntaxes/scss.tmLanguage.json", + "repo": "microsoft/vscode", + }, + { + "commit": "210541906e5a96ab39f9c753f921b1bd35f4138b", + "path": "extensions/typescript-basics/syntaxes/TypeScript.tmLanguage.json", + "repo": "microsoft/vscode", + }, +] +`; diff --git a/tests/vendor.spec.ts b/tests/vendor.spec.ts new file mode 100644 index 0000000000..a1f5f8b533 --- /dev/null +++ b/tests/vendor.spec.ts @@ -0,0 +1,53 @@ +import { expect, test } from 'vitest'; + +const VENDOR_LIST: { repo: string; path: string }[] = [ + { repo: 'microsoft/TypeScript', path: 'src/services/types.ts' }, + { repo: 'microsoft/vscode-html-languageservice', path: 'src/services/htmlFormatter.ts' }, + { repo: 'microsoft/vscode', path: 'extensions/css/syntaxes/css.tmLanguage.json' }, + { repo: 'microsoft/vscode', path: 'extensions/html/syntaxes/html.tmLanguage.json' }, + { repo: 'microsoft/vscode', path: 'extensions/javascript/syntaxes/JavaScript.tmLanguage.json' }, + { repo: 'microsoft/vscode', path: 'extensions/scss/syntaxes/scss.tmLanguage.json' }, + { repo: 'microsoft/vscode', path: 'extensions/typescript-basics/syntaxes/TypeScript.tmLanguage.json' }, +]; + +test.skipIf(process.env.TEST_VENDOR !== '1')(`ensure vendor is updated`, async () => { + const promises = VENDOR_LIST.map(async item => ({ + ...item, + commit: await retry(() => getRemoteCommit(item.repo, item.path)), + })); + const snapshot = await Promise.all(promises); + expect(snapshot).toMatchSnapshot(); +}); + +async function getRemoteCommit(repo: string, path: string): Promise { + const token = process.env.GH_TOKEN; + console.log(`fetching${token ? ` with token` : ''}`, repo, path); + const headers: Record = { + 'Accept': 'application/vnd.github+json', + 'X-GitHub-Api-Version': '2022-11-28', + ...token && { 'Authorization': `Bearer ${token}` }, + }; + const response = await fetch(`https://api.github.com/repos/${repo}/commits?path=${path}&per_page=1`, { headers }); + const data = await response.json(); + const sha: string | undefined = data[0]?.sha; + if (!sha) { + throw new Error(`No commits found for ${repo}/${path}`); + } + return sha; +} + +async function retry(fn: () => Promise, retries = 3, delay = 1000): Promise { + try { + return await fn(); + } + catch (error) { + if (retries > 0) { + console.warn(`Retrying... (${retries} left)`); + await new Promise(res => setTimeout(res, delay)); + return retry(fn, retries - 1, delay); + } + else { + throw error; + } + } +}