From 1fa47edb67021818e11492ee60d369a2320fc697 Mon Sep 17 00:00:00 2001 From: adanamanya Date: Tue, 28 Jul 2026 23:39:20 +0700 Subject: [PATCH 1/2] ci(kiro-extension): publish from a job that can read environment secrets The PAT lives under a GitHub environment, and an environment secret is only injected into a job that declares `environment:`. The build matrix does not and should not: it runs on every pull request, so an environment carrying protection rules would gate ordinary PR builds behind manual approval. Uploading to a registry is an HTTP request, not a build, so it does not need the platform it targets. Publishing therefore moves into its own job that declares the environment, is gated on a release tag, and fans out over the four targets on ubuntu with fail-fast disabled, so one failed upload cannot strand the other three. BEHAVIOUR CHANGE: publishing no longer starts the moment a single platform finishes; it now waits for all four builds. That is a deliberate reversal. Open VSX treats a version as immutable, so if darwin had already published 0.1.0 when win32 failed to build, that gap could never be filled, only superseded by 0.1.1. Waiting is the difference between retrying a release and burning a version number. The environment name defaults to `open-vsx` and can be redirected with an OVSX_ENVIRONMENT repository variable, since `vars` is one of the few contexts available to the `environment` key. Adds scripts/publish.mjs for the upload half, carrying the same guards release.mjs had: refuse without a token, refuse when the tag disagrees with package.json, and create the namespace idempotently, treating "already exists" as success because four jobs race to create it. Verified: YAML parses, every run: block parses under bash -n, the build matrix no longer references the publish path, and all three publish.mjs refusals were executed (no token, tag mismatch, missing VSIX). The upload itself is unexercised until a real tag with a real token. --- .github/workflows/kiro-extension.yml | 65 +++++++++++++------ kiro-extension/scripts/publish.mjs | 94 ++++++++++++++++++++++++++++ 2 files changed, 141 insertions(+), 18 deletions(-) create mode 100644 kiro-extension/scripts/publish.mjs diff --git a/.github/workflows/kiro-extension.yml b/.github/workflows/kiro-extension.yml index b41edb5..9d0840a 100644 --- a/.github/workflows/kiro-extension.yml +++ b/.github/workflows/kiro-extension.yml @@ -174,27 +174,56 @@ jobs: gh release upload "$TAG" release/*.vsix --clobber - # Publishes the moment THIS platform is ready, without waiting for the - # others. Only ever runs from a release tag. + # Uploading to the registry is an HTTP request, not a build, so it does not need + # the platform it targets. Splitting it out buys two things. The secret can live + # under a GitHub environment, which is only readable by a job declaring + # `environment:`, and no half-published version is possible: Open VSX treats a + # version as immutable, so if one platform failed to build after another had + # already published, that gap could never be filled, only superseded by a + # higher version. + publish: + name: publish ${{ matrix.target }} + needs: release + if: startsWith(github.ref, 'refs/tags/kiro-extension-v') + runs-on: ubuntu-22.04 + timeout-minutes: 15 + + # Only needs to read back the build artifact. Stated explicitly so a + # restrictive repository default cannot fail the download confusingly. + permissions: + contents: read + actions: read + + # Reads OVSX_PAT from an environment secret. Defaults to an environment named + # `open-vsx`; set an OVSX_ENVIRONMENT repository variable to point elsewhere. + environment: ${{ vars.OVSX_ENVIRONMENT || 'open-vsx' }} + + strategy: + # One platform failing to upload must not strand the others. + fail-fast: false + matrix: + target: [darwin-arm64, darwin-x64, win32-x64, linux-x64] + + defaults: + run: + working-directory: kiro-extension + + steps: + - uses: actions/checkout@v4 + + - uses: actions/setup-node@v4 + with: + node-version: 20 + + - uses: actions/download-artifact@v4 + with: + name: vsix-${{ matrix.target }} + path: kiro-extension/release + - name: Publish ${{ matrix.target }} to Open VSX - if: startsWith(github.ref, 'refs/tags/kiro-extension-v') env: OVSX_PAT: ${{ secrets.OVSX_PAT }} - shell: bash - run: | - if [ -z "$OVSX_PAT" ]; then - echo "OVSX_PAT resolved to an empty string, refusing to publish." >&2 - echo "" >&2 - echo "If you added it under Settings > Environments, it is only" >&2 - echo "visible to a job that declares 'environment:'. This job does" >&2 - echo "not, deliberately: an environment with protection rules would" >&2 - echo "also gate every pull request build." >&2 - echo "" >&2 - echo "Add it as a repository secret instead:" >&2 - echo "Settings > Secrets and variables > Actions > New repository secret" >&2 - exit 1 - fi - node scripts/release.mjs --publish + run: node scripts/publish.mjs --target=${{ matrix.target }} # A platform-specific VSIX declares TargetPlatform, and a mismatched host # refuses to install it. This produces one file with every companion binary and diff --git a/kiro-extension/scripts/publish.mjs b/kiro-extension/scripts/publish.mjs new file mode 100644 index 0000000..e27ffa3 --- /dev/null +++ b/kiro-extension/scripts/publish.mjs @@ -0,0 +1,94 @@ +/** + * Publishes an already-packaged VSIX to Open VSX, the registry Kiro installs + * from. Packaging happens on the platform that can compile the native mascot; + * uploading is just an HTTP request, so it is split out and runs anywhere. + * + * node scripts/publish.mjs --target darwin-arm64 + * + * Expects the matching VSIX to already exist in release/, which is how the CI + * publish job consumes the artifact the build job produced. + */ +import { spawnSync } from 'node:child_process'; +import fs from 'node:fs/promises'; +import path from 'node:path'; +import { fileURLToPath } from 'node:url'; + +const root = path.dirname(path.dirname(fileURLToPath(import.meta.url))); +const manifest = JSON.parse(await fs.readFile(path.join(root, 'package.json'), 'utf-8')); + +const target = process.argv.find((arg) => arg.startsWith('--target='))?.split('=')[1]; +if (!target) { + console.error('[doraemon] usage: node scripts/publish.mjs --target='); + process.exit(1); +} + +if (!process.env['OVSX_PAT']) { + console.error('[doraemon] OVSX_PAT resolved to an empty string, refusing to publish.'); + console.error(''); + console.error('[doraemon] In CI this usually means the secret lives under an'); + console.error('[doraemon] environment, but the job did not declare "environment:".'); + console.error('[doraemon] Locally, create a token at:'); + console.error('[doraemon] https://open-vsx.org/user-settings/tokens'); + process.exit(1); +} + +/* + * The tag names the release but the version comes from package.json. Publishing a + * mismatch is unrecoverable: Open VSX treats a version as immutable, so the wrong + * number cannot be replaced afterwards, only superseded by a higher one. + */ +const tag = process.env['GITHUB_REF_NAME']; +if (tag?.startsWith('kiro-extension-v')) { + const tagged = tag.slice('kiro-extension-v'.length); + if (tagged !== manifest.version) { + console.error(`[doraemon] tag ${tag} does not match package.json ${manifest.version}.`); + console.error('[doraemon] bump the version and retag, rather than publishing a mismatch.'); + process.exit(1); + } +} + +// Mirrors the name release.mjs packages under, so no globbing is needed. +const vsix = path.join(root, 'release', `${manifest.name}-${manifest.version}-${target}.vsix`); +try { + await fs.access(vsix); +} catch { + console.error(`[doraemon] ${path.relative(root, vsix)} is missing.`); + console.error('[doraemon] package it first, or check the build artifact was downloaded.'); + process.exit(1); +} + +const { size } = await fs.stat(vsix); +console.log(`[doraemon] publishing ${path.relative(root, vsix)} (${(size / 1024 / 1024).toFixed(2)} MB)`); + +/* + * Publishing into a namespace that does not exist fails outright. Creating it is + * idempotent, so doing it here means the first ever release needs no manual step. + * Four platforms race to create the same namespace and three will lose, which is + * why an "already exists" result is treated as success. + */ +console.log(`[doraemon] ensuring namespace "${manifest.publisher}" exists`); +const namespace = spawnSync('npx', ['--yes', 'ovsx', 'create-namespace', manifest.publisher], { + cwd: root, + encoding: 'utf-8', + shell: process.platform === 'win32', +}); +const namespaceOutput = `${namespace.stdout ?? ''}${namespace.stderr ?? ''}`; +if (namespace.status !== 0 && !/already (exists|owned)/i.test(namespaceOutput)) { + console.error('[doraemon] could not create the namespace:'); + console.error(namespaceOutput.trim()); + console.error('[doraemon] claim it manually at https://open-vsx.org and retry.'); + process.exit(1); +} + +console.log(`[doraemon] uploading ${target} as "${manifest.publisher}"`); +const publish = spawnSync('npx', ['--yes', 'ovsx', 'publish', vsix, '--target', target], { + cwd: root, + stdio: 'inherit', + shell: process.platform === 'win32', +}); +if (publish.status !== 0) { + console.error(`[doraemon] publishing ${target} failed`); + process.exit(publish.status ?? 1); +} + +console.log(`[doraemon] published ${manifest.name} ${manifest.version} (${target})`); From 9b3fb63650af7549e1c6e49302dadca3f62c8018 Mon Sep 17 00:00:00 2001 From: adanamanya Date: Tue, 28 Jul 2026 23:43:22 +0700 Subject: [PATCH 2/2] ci(kiro-extension): point the publish job at the OVSX_PAT environment The environment holding the token is named OVSX_PAT, the same name as the secret inside it, so the previous default of `open-vsx` would not have matched and the secret would have resolved to an empty string. Names it directly and drops the vars.OVSX_ENVIRONMENT indirection, which only existed because the environment name was unknown. Confirmed against the repository settings: no required reviewers and no wait timer, so declaring this environment introduces no approval gate, and deployment branches and tags is set to no restriction, so a release tag is permitted to access it. --- .github/workflows/kiro-extension.yml | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/.github/workflows/kiro-extension.yml b/.github/workflows/kiro-extension.yml index 9d0840a..aad5a34 100644 --- a/.github/workflows/kiro-extension.yml +++ b/.github/workflows/kiro-extension.yml @@ -194,9 +194,10 @@ jobs: contents: read actions: read - # Reads OVSX_PAT from an environment secret. Defaults to an environment named - # `open-vsx`; set an OVSX_ENVIRONMENT repository variable to point elsewhere. - environment: ${{ vars.OVSX_ENVIRONMENT || 'open-vsx' }} + # Names the environment holding the OVSX_PAT secret. This must match the + # environment exactly or the secret below silently resolves to an empty + # string. The environment happens to be named after the secret it carries. + environment: OVSX_PAT strategy: # One platform failing to upload must not strand the others.