diff --git a/.changeset/config.json b/.changeset/config.json index 032a4ba6cdd..6f10a7e8aae 100644 --- a/.changeset/config.json +++ b/.changeset/config.json @@ -1,5 +1,5 @@ { - "$schema": "https://unpkg.com/@changesets/config@2.0.1/schema.json", + "$schema": "https://unpkg.com/@changesets/config@3.0.2/schema.json", "changelog": ["@changesets/changelog-github", { "repo": "graphql/graphiql" }], "commit": false, "linked": [], @@ -14,6 +14,10 @@ "example-monaco-graphql-webpack" ], "updateInternalDependencies": "patch", + "privatePackages": { + "version": true, + "tag": true + }, "___experimentalUnsafeOptions_WILL_CHANGE_IN_PATCH": { "onlyUpdatePeerDependentsWhenOutOfRange": true } diff --git a/.changeset/vscode-publish-retry.md b/.changeset/vscode-publish-retry.md new file mode 100644 index 00000000000..2e5c9137aac --- /dev/null +++ b/.changeset/vscode-publish-retry.md @@ -0,0 +1,7 @@ +--- +'vscode-graphql-execution': patch +'vscode-graphql-syntax': patch +'vscode-graphql': patch +--- + +Burning patch version due to previous release failure. diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 97cc6aba928..d6929898c6f 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -30,6 +30,10 @@ jobs: # To enable trusted publishing id-token: write + outputs: + vscode-published: ${{ steps.vscode.outputs.published }} + published-packages: ${{ steps.changesets.outputs.publishedPackages }} + steps: - name: Checkout Code uses: actions/checkout@v5 @@ -50,8 +54,65 @@ jobs: version: yarn ci:version # This expects you to have a script called release which does a build for your packages and calls changeset publish publish: yarn release + + - name: Note VSCode extension release + id: vscode + if: ${{ steps.changesets.outputs.published == 'true' && contains(steps.changesets.outputs.publishedPackages, '"name":"vscode-graphql') }} + run: echo "published=true" >> "$GITHUB_OUTPUT" + + - name: Build VSCode extension .vsix files + if: ${{ steps.vscode.outputs.published == 'true' }} + env: + PUBLISHED_PACKAGES: ${{ steps.changesets.outputs.publishedPackages }} + run: node scripts/release-vscode.mts build + + - name: Attach VSCode .vsix files to GitHub Releases + if: ${{ steps.vscode.outputs.published == 'true' }} + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + PUBLISHED_PACKAGES: ${{ steps.changesets.outputs.publishedPackages }} + run: node scripts/release-vscode.mts attach + + - name: Upload VSCode extension .vsix artifacts + if: ${{ steps.vscode.outputs.published == 'true' }} + uses: actions/upload-artifact@v4 + with: + name: vscode-extensions + path: packages/vscode-graphql*/*.vsix + if-no-files-found: error + + publish-vscode-extensions: + name: Publish to ${{ matrix.registry }} + needs: release + if: ${{ needs.release.outputs.vscode-published == 'true' }} + runs-on: ubuntu-latest + permissions: {} + strategy: + fail-fast: false + matrix: + include: + - registry: VSCode Marketplace + command: publish-vsce + - registry: Open VSX Registry + command: publish-ovsx + steps: + - uses: actions/checkout@v5 + + - uses: actions/setup-node@v6 + with: + node-version-file: '.node-version' + cache: yarn + + - run: yarn install --frozen-lockfile --immutable + + - uses: actions/download-artifact@v4 + with: + name: vscode-extensions + path: vsix + + - name: Publish env: - # for vscode marketplace, see https://github.com/microsoft/vscode-vsce/blob/194d59b975523696362ead891dc0f3ddd277b3bd/README.md#linux VSCE_PAT: ${{ secrets.VSCE_PAT }} - # for ovsx, see https://github.com/eclipse/openvsx/blob/master/cli/README.md#publish-extensions OVSX_PAT: ${{ secrets.OPEN_VSX_TOKEN }} + PUBLISHED_PACKAGES: ${{ needs.release.outputs.published-packages }} + run: node scripts/release-vscode.mts ${{ matrix.command }} diff --git a/RELEASING.md b/RELEASING.md index 88e2a3d9615..7c87fb9f74f 100644 --- a/RELEASING.md +++ b/RELEASING.md @@ -1,3 +1,27 @@ # Cutting New Releases TODO: Redo for `changesets`. See [`Changesets Readme`](./.changeset/README.md) + +## VSCode extension releases + +After Changesets publishes the npm packages, the release workflow hands the +VSCode extension pipeline to [`scripts/release-vscode.mts`](./scripts/release-vscode.mts). +It exposes four commands — `build`, `attach`, `publish-vsce`, `publish-ovsx` — +each of which operates only on the extensions actually released in this run +(filtered from the Changesets `publishedPackages` output). + +### Why attach to GitHub Releases + +We upload each built `.vsix` to its GitHub Release tag in addition to +publishing to the VSCode Marketplace and Open VSX. That makes the artifact +downloadable directly from GitHub — useful when a registry is degraded, when +a PAT has expired and a manual re-upload is needed, or for anyone who wants +the exact bits we shipped without going through a marketplace. + +### Why a TS script, not workflow shell + +Per-package iteration, version lookups, and registry-API calls are tidier +(and safer) in a type-checked TS file than spread across `run:` blocks of +bash in `release.yml`. The script type-checks under `scripts/tsconfig.json`, +can be run locally with a mocked `PUBLISHED_PACKAGES` env var, and aggregates +failures across packages rather than short-circuiting on the first error. diff --git a/package.json b/package.json index 63384736e7a..68948410d99 100644 --- a/package.json +++ b/package.json @@ -54,7 +54,7 @@ "svgo": "svgo --pretty --indent 2 --eol lf --final-newline -r -f . --exclude node_modules", "svgo-check": "yarn svgo && git diff --exit-code -- '*.svg'", "ci:version": "yarn changeset version && yarn install --no-immutable && yarn build && yarn fix", - "release": "yarn build && yarn build-bundles && (wsrun release --exclude-missing --serial --recursive --changedSince main -- || true) && yarn changeset publish", + "release": "yarn build && yarn build-bundles && yarn changeset publish", "release:canary": "(node scripts/canary-release.mts && yarn build-bundles && yarn changeset publish --tag canary) || echo Skipping Canary...", "repo:lint": "manypkg check", "repo:fix": "manypkg fix", @@ -126,7 +126,9 @@ "vite": "6.3.4" }, "devDependencies": { + "@vscode/vsce": "^2.22.1-2", "jsdom": "^29.0.2", + "ovsx": "0.8.3", "svgo": "^4.0.1" } } diff --git a/packages/vscode-graphql-execution/package.json b/packages/vscode-graphql-execution/package.json index d06eda24cbc..d7552043645 100644 --- a/packages/vscode-graphql-execution/package.json +++ b/packages/vscode-graphql-execution/package.json @@ -85,11 +85,7 @@ "types:check": "tsc --noEmit", "compile": "node esbuild.js", "build-bundles": "yarn run compile", - "vsce:package": "yarn compile && vsce package --yarn --no-dependencies", - "vsce:prepublish": "yarn run vsce:package", - "vsce:publish": "vsce publish --yarn --no-dependencies", - "open-vsx:publish": "ovsx publish --no-dependencies --pat $OVSX_PAT", - "release": "yarn run compile && yarn run vsce:publish && yarn run open-vsx:publish" + "vsce:package": "yarn compile && vsce package --yarn --no-dependencies" }, "devDependencies": { "@types/capitalize": "2.0.0", diff --git a/packages/vscode-graphql-syntax/package.json b/packages/vscode-graphql-syntax/package.json index 9209201bcdd..b25a3c4997c 100644 --- a/packages/vscode-graphql-syntax/package.json +++ b/packages/vscode-graphql-syntax/package.json @@ -162,10 +162,6 @@ "scripts": { "types:check": "tsc --noEmit", "vsce:package": "vsce package --yarn --no-dependencies", - "vsce:prepublish": "npm run vsce:package", - "vsce:publish": "vsce publish --yarn --no-dependencies", - "open-vsx:publish": "ovsx publish --no-dependencies --pat $OVSX_PAT", - "release": "npm run vsce:publish && npm run open-vsx:publish", "test": "vitest run" } } diff --git a/packages/vscode-graphql/package.json b/packages/vscode-graphql/package.json index b97cdd04f49..46fdce115cb 100644 --- a/packages/vscode-graphql/package.json +++ b/packages/vscode-graphql/package.json @@ -176,10 +176,7 @@ "compile": "node esbuild", "build-bundles": "npm run compile -- --sourcemap", "vsce:package": "vsce package --yarn --no-dependencies", - "env:source": "export $(cat .envrc | xargs)", - "vsce:publish": "vsce publish --yarn --no-dependencies", - "open-vsx:publish": "ovsx publish --no-dependencies --pat $OVSX_PAT", - "release": "npm run vsce:publish && npm run open-vsx:publish" + "env:source": "export $(cat .envrc | xargs)" }, "devDependencies": { "@types/capitalize": "2.0.0", diff --git a/scripts/release-vscode.mts b/scripts/release-vscode.mts new file mode 100644 index 00000000000..5b18e61734c --- /dev/null +++ b/scripts/release-vscode.mts @@ -0,0 +1,166 @@ +import { spawnSync } from 'node:child_process'; +import { readFile } from 'node:fs/promises'; +import { parseArgs } from 'node:util'; +import { publishVSIX } from '@vscode/vsce'; +import { publish as ovsxPublish } from 'ovsx'; + +const PACKAGES = [ + 'vscode-graphql', + 'vscode-graphql-syntax', + 'vscode-graphql-execution', +] as const; +type VscodePackage = (typeof PACKAGES)[number]; + +interface PublishedPackage { + name: string; + version: string; +} + +function publishedVscodePackages(): VscodePackage[] { + const raw = process.env.PUBLISHED_PACKAGES; + if (!raw) { + return []; + } + const all = JSON.parse(raw) as PublishedPackage[]; + const names = new Set(all.map(p => p.name)); + return PACKAGES.filter(p => names.has(p)); +} + +async function readVersion(pkg: VscodePackage): Promise { + const json = await readFile(`packages/${pkg}/package.json`, 'utf8'); + return (JSON.parse(json) as { version: string }).version; +} + +function vsixPath( + baseDir: string, + pkg: VscodePackage, + version: string, +): string { + return `${baseDir}/packages/${pkg}/${pkg}-${version}.vsix`; +} + +function requireEnv(name: string): string { + const value = process.env[name]; + if (!value) { + throw new Error(`Missing required env var: ${name}`); + } + return value; +} + +async function runForEach( + packages: VscodePackage[], + label: string, + fn: (pkg: VscodePackage) => Promise, +): Promise { + let failures = 0; + for (const pkg of packages) { + try { + await fn(pkg); + } catch (err) { + failures += 1; + console.error(`[${label}] failed for ${pkg}:`, err); + } + } + if (failures > 0) { + throw new Error( + `${failures}/${packages.length} ${label} operation(s) failed`, + ); + } +} + +async function build(packages: VscodePackage[]): Promise { + await runForEach(packages, 'build', async pkg => { + console.log(`Building ${pkg}.vsix`); + const { status } = spawnSync('yarn', ['workspace', pkg, 'vsce:package'], { + stdio: 'inherit', + }); + if (status !== 0) { + throw new Error( + `yarn workspace ${pkg} vsce:package exited with status ${status}`, + ); + } + }); +} + +async function attach(packages: VscodePackage[]): Promise { + await runForEach(packages, 'attach', async pkg => { + const version = await readVersion(pkg); + const tag = `${pkg}@${version}`; + const path = vsixPath('.', pkg, version); + console.log(`Attaching ${path} to release ${tag}`); + const { status } = spawnSync( + 'gh', + ['release', 'upload', tag, path, '--clobber'], + { stdio: 'inherit' }, + ); + if (status !== 0) { + throw new Error(`gh release upload exited with status ${status}`); + } + }); +} + +async function publishToVsce(packages: VscodePackage[]): Promise { + const pat = requireEnv('VSCE_PAT'); + await runForEach(packages, 'vsce publish', async pkg => { + const version = await readVersion(pkg); + const path = vsixPath('vsix', pkg, version); + console.log(`Publishing ${path} to VSCode Marketplace`); + await publishVSIX(path, { pat, skipDuplicate: true }); + }); +} + +async function publishToOvsx(packages: VscodePackage[]): Promise { + const pat = requireEnv('OVSX_PAT'); + await runForEach(packages, 'ovsx publish', async pkg => { + const version = await readVersion(pkg); + const path = vsixPath('vsix', pkg, version); + console.log(`Publishing ${path} to Open VSX Registry`); + const results = await ovsxPublish({ + extensionFile: path, + pat, + skipDuplicate: true, + }); + const rejected = results.filter(r => r.status === 'rejected'); + if (rejected.length > 0) { + throw new AggregateError( + rejected.map(r => r.reason), + `${rejected.length} target(s) rejected by Open VSX`, + ); + } + }); +} + +const { positionals } = parseArgs({ + args: process.argv.slice(2), + allowPositionals: true, +}); + +const command = positionals[0]; +const packages = publishedVscodePackages(); + +if (packages.length === 0) { + console.log('No VSCode extensions were published; nothing to do.'); + process.exit(0); +} + +console.log(`VSCode packages: ${packages.join(', ')}`); + +switch (command) { + case 'build': + await build(packages); + break; + case 'attach': + await attach(packages); + break; + case 'publish-vsce': + await publishToVsce(packages); + break; + case 'publish-ovsx': + await publishToOvsx(packages); + break; + default: + console.error( + 'Usage: node scripts/release-vscode.mts ', + ); + process.exit(1); +} diff --git a/yarn.lock b/yarn.lock index 25a24f35a1e..ba8164eed1b 100644 --- a/yarn.lock +++ b/yarn.lock @@ -12921,6 +12921,7 @@ __metadata: "@types/semver": "npm:^7.5.0" "@types/ws": "npm:8.2.2" "@typescript/native-preview": "npm:^7.0.0-dev" + "@vscode/vsce": "npm:^2.22.1-2" babel-plugin-macros: "npm:^3.1.0" babel-plugin-transform-import-meta: "npm:^2.2.1" concurrently: "npm:^7.0.0" @@ -12932,6 +12933,7 @@ __metadata: jsdom: "npm:^29.0.2" mkdirp: "npm:^1.0.4" msw: "npm:^2.13.4" + ovsx: "npm:0.8.3" oxfmt: "npm:^0.45.0" oxlint: "npm:^1" oxlint-plugin-eslint: "npm:^1"