Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
67 changes: 67 additions & 0 deletions .github/workflows/attach-release-vsix.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
name: Attach universal VSIX to release draft

# Builds the universal VSIX once (build-vsix.yml) and attaches it, with its
# recorded SHA-256, to the release as a downloadable asset — at draft
# creation, before Valerii publishes it. Every registry-publish job
# downstream of a published release downloads this exact asset instead of
# repackaging, so the artefact verified before release is the one that ships.
#
# Runs on release *creation*, filtered to drafts: the release-runbook draft
# step happens first, so the asset is already attached by the time
# publishing the draft fires the registry-publish workflows.

on:
release:
types: [created]

permissions:
contents: read

jobs:
build:
if: github.event.release.draft == true
permissions:
contents: read
id-token: write
attestations: write
uses: ./.github/workflows/build-vsix.yml

attach:
needs: build
if: github.event.release.draft == true
runs-on: ubuntu-latest
permissions:
contents: write
steps:
- name: Download universal VSIX artifact
uses: actions/download-artifact@v4
with:
name: ${{ needs.build.outputs.artifact-name }}
path: output

- name: Verify downloaded artifact matches the attested hash
shell: bash
run: |
vsix=$(ls output/*.vsix | head -n 1)
expected="${{ needs.build.outputs.sha256 }}"
actual=$(sha256sum "$vsix" | cut -d' ' -f1)
if [ "$actual" != "$expected" ]; then
echo "::error::downloaded artifact does not match the build job's recorded SHA-256"
exit 1
fi

- name: Attach VSIX and SHA-256 to the release
env:
GH_TOKEN: ${{ github.token }}
shell: bash
run: |
for f in output/*.vsix output/*.sha256; do
name=$(basename "$f")
gh api \
--method POST \
-H "Accept: application/vnd.github+json" \
-H "Content-Type: application/octet-stream" \
"/repos/${{ github.repository }}/releases/${{ github.event.release.id }}/assets?name=$name" \
--input "$f" > /dev/null
echo "Attached $name"
done
51 changes: 30 additions & 21 deletions .github/workflows/openvsx-publish.yml
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,12 @@ name: Open VSX — publish
# Triggered on every GitHub Release to publish the universal VSIX to the
# Open VSX Registry (the registry Cursor, VSCodium, and Windsurf read).
#
# A workflow_dispatch trigger is available for manual re-runs or to publish a
# specific branch/tag without creating a new release.
# Publishes the exact file attach-release-vsix.yml attached to the release
# at draft time — downloaded here, never rebuilt, so the artefact verified
# before release is the artefact that ships.
#
# A workflow_dispatch trigger is available for manual re-runs; pass `tag` to
# target a specific release without creating a new one.
#
# Prerequisites (one-time maintainer setup — see docs/internal/openvsx-publish-runbook.md):
# - OVSX_PAT repo Actions secret set to an Open VSX personal access token
Expand All @@ -13,7 +17,11 @@ name: Open VSX — publish
on:
release:
types: [published]
workflow_dispatch: {}
workflow_dispatch:
inputs:
tag:
description: Release tag to publish (required for a manual run)
required: false

permissions:
contents: read
Expand All @@ -24,29 +32,30 @@ jobs:
runs-on: ubuntu-latest

steps:
- name: Checkout
uses: actions/checkout@v4

- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: 20
cache: npm

- name: Install root dependencies
run: npm ci

- name: Build extension bundle
# extension:prep bundles the extension and the compiler — no runtime
# dependency install step (extension/package.json declares none).
run: npm run extension:prep

- name: Verify extension packaging
run: npm run verify-extension-packaging
- name: Download the release-attached VSIX
env:
GH_TOKEN: ${{ github.token }}
shell: bash
run: |
tag="${{ github.event.release.tag_name || inputs.tag }}"
if [ -z "$tag" ]; then
echo "::error::No release tag to publish — pass one via workflow_dispatch."
exit 1
fi
mkdir -p output
gh release download "$tag" --repo "${{ github.repository }}" \
--pattern '*.vsix' --pattern '*.sha256' --dir output

- name: Package VSIX
working-directory: extension
run: npx vsce package --out ../output/
- name: Verify the downloaded VSIX against its recorded SHA-256
shell: bash
run: |
cd output
sha256sum -c ./*.sha256

- name: Publish to Open VSX
shell: bash
Expand All @@ -55,4 +64,4 @@ jobs:
run: |
vsix=$(ls output/*.vsix | head -n 1)
echo "Publishing: $vsix"
npx ovsx publish "$vsix" --pat "$OVSX_PAT"
npx --yes ovsx publish "$vsix" --pat "$OVSX_PAT"
10 changes: 4 additions & 6 deletions .github/workflows/vscode-marketplace-publish.yml
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
name: VS Code Marketplace — publish

# Triggered on every GitHub Release to publish the universal VSIX to the
# VS Code Marketplace via `vsce publish`.
# Publishes the universal VSIX to the VS Code Marketplace via `vsce publish`.
#
# A workflow_dispatch trigger is available for manual re-runs or to re-publish
# a specific tag without creating a new release.
# workflow_dispatch only — do not restore an automatic `release: published`
# trigger without a maintainer decision first; see
# docs/internal/vscode-marketplace-publish-runbook.md before changing this.
#
# Prerequisites (one-time maintainer setup — see docs/internal/vscode-marketplace-publish-runbook.md):
# - VSCE_PAT repo Actions secret set to an Azure DevOps personal access token
Expand All @@ -14,8 +14,6 @@ name: VS Code Marketplace — publish
# fail loudly rather than silently skipping.

on:
release:
types: [published]
workflow_dispatch: {}

permissions:
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
### Changed

- **Open VSX publish consumes the release-attached VSIX instead of rebuilding.** Creating a release draft now builds the universal VSIX once and attaches it, with its SHA-256, as a release asset; the Open VSX publish job downloads and verifies that exact file rather than repackaging at publish time. The VS Code Marketplace publish workflow no longer triggers automatically on a release — it runs by `workflow_dispatch` only.
41 changes: 20 additions & 21 deletions docs/internal/openvsx-publish-runbook.md
Original file line number Diff line number Diff line change
Expand Up @@ -47,22 +47,19 @@ first Open VSX publish session:

Open VSX does not require 2FA for publishes; the token gates the write.

## Pre-flight checklist (run for every release)

Run from a clean checkout of the tag/commit being released, after the
Marketplace pre-flight has passed:

- [ ] The VS Code Marketplace publish for this version is **done** and
visible at <https://marketplace.visualstudio.com/items?itemName=transitrix.transitrix-studio>.
Open VSX is the second hop, not the source of truth.
- [ ] The universal VSIX file from the Marketplace step is still on
disk (or rebuild it per [`packaging.md`](packaging.md)).
- [ ] The `version` field in `extension/package.json` matches the
Marketplace listing exactly.
- [ ] `extension/README.md` and the icon (`extension/icon.png`) match
what shipped to the Marketplace — Open VSX renders these on the
listing page from the VSIX itself, so there is nothing extra to
sync if the same artefact is used.
## Pre-flight checklist (manual publish only)

The CI path needs none of this — it downloads the release-attached VSIX
directly. Run these checks only when publishing by hand from a clean
checkout of the tag/commit being released:

- [ ] The universal VSIX to publish is on disk, either downloaded from the
release's assets or freshly built per [`packaging.md`](packaging.md).
- [ ] The `version` field in `extension/package.json` matches the release
tag.
- [ ] `extension/README.md` and the icon (`extension/icon.png`) are as
intended — Open VSX renders these on the listing page from the VSIX
itself, so there is nothing extra to sync.
- [ ] `OVSX_PAT` is set in the current shell and `ovsx` is on PATH:
```bash
ovsx --version
Expand Down Expand Up @@ -105,16 +102,18 @@ verification step needed beyond a spot check.

## Keeping Open VSX in sync on future releases

The Open VSX publish is a **second hop** after every VS Code
Marketplace release.
The Open VSX publish runs from the GitHub Release directly — it does not
wait on a VS Code Marketplace publish.

### CI path (recommended)

The `.github/workflows/openvsx-publish.yml` workflow runs automatically
on every GitHub Release (`release: types: [published]`) from a single
`ubuntu-latest` job: builds the universal VSIX (`npm run extension:prep`
+ `vsce package`, no `--target`) and publishes it with `ovsx publish`.
The workflow reads `OVSX_PAT` from the repo Actions secret.
`ubuntu-latest` job: downloads the universal VSIX that
`.github/workflows/attach-release-vsix.yml` already attached to the
release at draft time (no rebuild), verifies it against the recorded
SHA-256, and publishes it with `ovsx publish`. The workflow reads
`OVSX_PAT` from the repo Actions secret.

### Manual fallback

Expand Down
41 changes: 26 additions & 15 deletions docs/internal/release-runbook.md
Original file line number Diff line number Diff line change
@@ -1,17 +1,20 @@
# Release runbook

How a Transitrix Studio release ships: **publishing the GitHub Release is
the trigger for everything** — npm packages, both VS Code marketplaces, and
the JetBrains plugin all publish from CI.
How a Transitrix Studio release ships: **creating the release draft builds
and attaches the universal VSIX; publishing the draft triggers the
registry-publish jobs**, which consume that same attached file rather than
rebuilding — npm packages, Open VSX, and the JetBrains plugin all publish
from CI.

## What publishes where

| Artifact | Pipeline | Trigger |
|---|---|---|
| Universal VSIX, attached to the release + SHA-256 recorded | `.github/workflows/attach-release-vsix.yml` | GitHub Release **created** as a draft |
| `@transitrix/diagrams` + `@transitrix/cli` → npm | `.github/workflows/npm-publish.yml` | GitHub Release **published** (or `workflow_dispatch`) |
| VS Code extension → VS Code Marketplace | `.github/workflows/vscode-marketplace-publish.yml` | same |
| VS Code extension → Open VSX (Cursor / VSCodium / Windsurf) | `.github/workflows/openvsx-publish.yml` | same |
| IntelliJ plugin → JetBrains Marketplace | `.github/workflows/jetbrains-publish.yml` | same (plugin version derived from the release tag, `v` prefix stripped) |
| VS Code extension → VS Code Marketplace | `.github/workflows/vscode-marketplace-publish.yml` | `workflow_dispatch` only — see that workflow's header before changing this |
| VS Code extension → Open VSX (Cursor / VSCodium / Windsurf) | `.github/workflows/openvsx-publish.yml` | GitHub Release **published** (or `workflow_dispatch`); downloads the asset `attach-release-vsix.yml` attached rather than rebuilding |
| IntelliJ plugin → JetBrains Marketplace | `.github/workflows/jetbrains-publish.yml` | GitHub Release **published** (plugin version derived from the release tag, `v` prefix stripped) |

Secrets backing the automation (repo Actions secrets): `NPM_TOKEN`
(read-write on **both** `@transitrix/diagrams` and `@transitrix/cli`;
Expand Down Expand Up @@ -77,28 +80,36 @@ notes PRs:
`## What's changed` heading. (A draft creates no tag; the tag is created
on the then-current target when the draft is published — so always merge
the release PR **before** drafting.)
- Valerii reviews the draft, verifies the branch/commit it targets, and
publishes it — see step 4.
- Creating the draft fires `attach-release-vsix.yml`: it builds the
universal VSIX once, records its SHA-256, and attaches both to the draft
as release assets. Confirm the assets are present before the next step —
a draft published without them leaves the registry-publish jobs with
nothing to download.
- Valerii reviews the draft, verifies the branch/commit it targets and the
attached asset, and publishes it — see step 4.

### 4. Publish the release → automation fires

Publishing the release starts all four workflows. Watch them under
Actions → filter event `release`:
Publishing the release starts the registry-publish workflows. Watch them
under Actions → filter event `release`:

- `npm — publish packages` — `@transitrix/diagrams` first, then
`@transitrix/cli` (versions that are already on the registry are
skipped). Verify with `npm view @transitrix/diagrams version` and
`npm view @transitrix/cli version`.
- `VS Code Marketplace — multi-platform publish` — per-platform VSIX build
(`extension:prep` installs the platform-correct `@resvg/resvg-js-*`
binary) + `vsce publish`.
- `Open VSX — multi-platform publish` — same build matrix, `ovsx publish`.
- `Open VSX — publish` — downloads the VSIX `attach-release-vsix.yml`
attached to this release (no rebuild) and runs `ovsx publish` on that
exact file.
- `JetBrains Marketplace — publish` — sets `pluginVersion` in
`intellij/gradle.properties` from the release tag, builds, signs,
publishes.

`VS Code Marketplace — publish` does **not** fire automatically — it is
`workflow_dispatch` only; see that workflow's header comment before
changing this.

Every workflow also supports `workflow_dispatch` for re-runs (e.g. a
transient marketplace failure) without re-publishing the release.
transient registry failure) without re-publishing the release.

### 5. Post-publish sanity check (optional)

Expand Down
19 changes: 10 additions & 9 deletions docs/internal/vscode-marketplace-publish-runbook.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,12 @@ How to publish Transitrix Studio's VS Code extension to the
[Visual Studio Marketplace](https://marketplace.visualstudio.com/items?itemName=transitrix.transitrix-studio)
so VS Code users can install it from within their editor.

Publishing a GitHub Release triggers `.github/workflows/vscode-marketplace-publish.yml`,
which builds and publishes a single universal VSIX with `vsce publish`. The
Open VSX publish (for Cursor, VSCodium, Windsurf) is a separate workflow;
see [`openvsx-publish-runbook.md`](openvsx-publish-runbook.md).
`.github/workflows/vscode-marketplace-publish.yml` builds and publishes a
single universal VSIX with `vsce publish`. It is currently
**`workflow_dispatch` only** — publishing a GitHub Release does not trigger
it; check with a maintainer before restoring that trigger. The Open VSX
publish (for Cursor, VSCodium, Windsurf) is a separate workflow that does
still run on release; see [`openvsx-publish-runbook.md`](openvsx-publish-runbook.md).

## What gets published

Expand Down Expand Up @@ -59,12 +61,11 @@ Rotation procedure:
3. Trigger a `workflow_dispatch` run of `vscode-marketplace-publish.yml` to
confirm the new token authenticates before the next release.

## CI path (automated)
## CI path (manual trigger)

`.github/workflows/vscode-marketplace-publish.yml` runs automatically on
every GitHub Release (`release: types: [published]`) and publishes the
universal VSIX from a single job. The workflow also exposes a
`workflow_dispatch` trigger for manual re-runs without creating a new release.
`.github/workflows/vscode-marketplace-publish.yml` publishes the universal
VSIX from a single job, run via its `workflow_dispatch` trigger in the
Actions tab. It does not fire automatically on a GitHub Release.

The job:
1. Checks out the release tag.
Expand Down
Loading