From 2932421cab3e715cc5f82db86039891cfaab2fa1 Mon Sep 17 00:00:00 2001 From: Trevor Scheer Date: Wed, 13 May 2026 21:47:27 -0700 Subject: [PATCH 1/5] Tag private packages so VSCode extensions get GitHub Releases MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Set `privatePackages.tag` to `true` so `changeset publish` emits "New tag" lines for private packages (the three VSCode extension packages). `changesets/action` will then create GitHub Releases for them, matching the behavior for public npm packages. Also set `privatePackages.version: true` explicitly — it already matches the default, but documents intent and is defensive against future default changes. Bump the `$schema` URL to `@changesets/config@3.0.2` to match the installed version (was stale at `2.0.1`). --- .changeset/config.json | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) 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 } From 7fe98beb4dbbb78b690b71c7dfa2f3d7f6d9334a Mon Sep 17 00:00:00 2001 From: Trevor Scheer Date: Wed, 13 May 2026 21:47:50 -0700 Subject: [PATCH 2/5] Add `scripts/release-vscode.mts` to orchestrate VSCode releases MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds a typed release-orchestration script with four commands: - `build` — runs `vsce:package` for each just-released VSCode workspace. - `attach` — uploads each built `.vsix` to its GitHub Release tag via `gh release upload --clobber`. - `publish-vsce` — publishes each `.vsix` to the VSCode Marketplace via `@vscode/vsce`'s `publishVSIX` (with `skipDuplicate`). - `publish-ovsx` — publishes each `.vsix` to Open VSX via `ovsx`'s `publish` (with `skipDuplicate`). Inspects the returned `PromiseSettledResult[]` and throws if any target was rejected. Each command operates only on the extensions actually released in this run (filtered from the Changesets `publishedPackages` env var) and aggregates failures across packages rather than short-circuiting on the first error. Also pulls `@vscode/vsce` and `ovsx` up to root devDependencies (at the same versions the workspaces already use) so the script can import their JS APIs directly with proper TypeScript types. They go in `devDependencies` rather than `dependencies` because the root monorepo isn't published and they're dev tooling — `js-green-licenses` skips devDeps via `dev: false`. --- package.json | 2 + scripts/release-vscode.mts | 166 +++++++++++++++++++++++++++++++++++++ yarn.lock | 2 + 3 files changed, 170 insertions(+) create mode 100644 scripts/release-vscode.mts diff --git a/package.json b/package.json index 63384736e7a..956ed860c41 100644 --- a/package.json +++ b/package.json @@ -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/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" From 66910fa9e936dae90525d8296d3965fcf1f1cd8c Mon Sep 17 00:00:00 2001 From: Trevor Scheer Date: Wed, 13 May 2026 21:48:08 -0700 Subject: [PATCH 3/5] Rewire `release.yml` around `release-vscode.mts` MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replaces the old per-workspace `vsce:publish && open-vsx:publish` chain (which short-circuited on the first failure and was swallowed by the top-level `release` script's `wsrun ... || true`) with a flow built around the new orchestration script: - The `release` job runs Changesets, then for any newly-released VSCode extension: builds the `.vsix` (`release-vscode.mts build`), attaches it to its GitHub Release tag (`release-vscode.mts attach`), and uploads the `.vsix` files as a `vscode-extensions` workflow artifact. - A separate `publish-vscode-extensions` matrix job (per registry) downloads the artifact and runs `release-vscode.mts publish-vsce` or `publish-ovsx`. The two registry shards run independently — a `vsce` failure no longer blocks `ovsx`, and the GHA UI shows them as distinct statuses. Also simplifies the top-level `release` script to drop `wsrun`; the workspace-level scripts it used to invoke are removed in the next commit. `--skipDuplicate` on both publishers makes reruns idempotent. When automated publishing is unavailable (expired PAT, registry down), the `.vsix` is still available on the GitHub Release for manual upload. --- .github/workflows/release.yml | 65 +++++++++++++++++++++++++++++++++-- package.json | 2 +- 2 files changed, 64 insertions(+), 3 deletions(-) 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/package.json b/package.json index 956ed860c41..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", From 101f65aed558f7e8c09e0668442881abb30e417b Mon Sep 17 00:00:00 2001 From: Trevor Scheer Date: Wed, 13 May 2026 21:48:08 -0700 Subject: [PATCH 4/5] Drop redundant workspace publish scripts; document release flow MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The old release flow ran per-workspace `release`, `vsce:publish`, and `open-vsx:publish` scripts (plus a `vsce:prepublish` hook in two of the workspaces). All of those are now dead callers — publishing goes through `scripts/release-vscode.mts`'s programmatic `publishVSIX` / `ovsx.publish` APIs. `vsce:package` is kept; it's still used by the script's `build` command and by the manual local-test flow documented in `vscode-graphql-syntax/README.md`. Also adds a `VSCode extension releases` section to `RELEASING.md` covering what the script does, why we attach `.vsix` files to GitHub Releases (registry redundancy / manual re-uploads / users who want the exact bits), and why the logic lives in a type-checked TS file rather than `run:` bash blocks in `release.yml`. --- RELEASING.md | 24 +++++++++++++++++++ .../vscode-graphql-execution/package.json | 6 +---- packages/vscode-graphql-syntax/package.json | 4 ---- packages/vscode-graphql/package.json | 5 +--- 4 files changed, 26 insertions(+), 13 deletions(-) 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/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", From 56f96cab01c66796fa796e4cf1624f95832a3e64 Mon Sep 17 00:00:00 2001 From: Trevor Scheer Date: Wed, 13 May 2026 21:48:08 -0700 Subject: [PATCH 5/5] Add changeset to republish VSCode extensions The previous release attempted to publish these extensions but failed (`vsce:publish` errored, which short-circuited `open-vsx:publish` too). Bump patch versions to deliver the marketplace updates that were skipped. --- .changeset/vscode-publish-retry.md | 7 +++++++ 1 file changed, 7 insertions(+) create mode 100644 .changeset/vscode-publish-retry.md 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.