Skip to content

Publish from one workflow, with a job per registry - #711

Open
TemplarGeneral wants to merge 1 commit into
mainfrom
fix/publish-workflow
Open

Publish from one workflow, with a job per registry#711
TemplarGeneral wants to merge 1 commit into
mainfrom
fix/publish-workflow

Conversation

@TemplarGeneral

Copy link
Copy Markdown
Contributor

Two confirmed bugs, one structural cause, one replacement workflow.

What was broken

No stable release had ever published. Every Tag release run ended in startup_failure inside a second — runs 34244103258, 34244162650, 34266213493, 34266444443, which is every run since the workflow landed. tag-release.yml set top-level permissions: contents: read and its release: calling job set none of its own, so the reusable release.yml was capped at read while its publish job asked for contents: write. A called workflow may only narrow the caller's token, so GitHub rejected the whole graph before any job started.

Nothing had reached Open VSX, on either channel. open-vsx.org/api/milleniumdawnmodteam returns {"extensions":{}} — the namespace exists and is empty, so nothing needed creating. The release path never ran (above). On the pre-release path the Marketplace step ran first in the same job and timed out on /_apis/gallery pushing all six platform VSIX files through one vsce publish call; a failed step aborts the job, so the Open VSX step below it never executed. Both pre-release runs ever attempted died exactly this way.

Both are the same structural fault: three workflows racing on one push, with all three publish targets as sequential steps in a single job.

What replaces it

pre-release.yml, release.yml and tag-release.yml become one publish.yml:

check ──> verify ──> build-server (×5) ──> package ──┬─> Publish: VS Code Marketplace
(channel)  (tokens)                                  ├─> Publish: Open VSX
                                                     └─> Publish: GitHub
                                                           └─> Release: open a fix PR
  • check picks the channel once and everything downstream reads it. A release commit no longer also publishes a pre-release, so the five-platform Rust matrix runs once per push instead of twice.
  • verify fails before the 45-minute matrix when a release is missing a publish token — and now checks OPEN_VSX_TOKEN, not just VSCE_TOKEN. A release skipping Open VSX in silence is half of why this went unnoticed.
  • Three publish jobs, in parallel, from one packaged artifact. A failure names itself in the Actions UI, one registry can no longer cancel another, and re-running one red job re-publishes one target (--skip-duplicate on both registries makes that safe).
  • The tag is created by the GitHub job, not ahead of the build — so a tag means published, not attempted, and a release that failed is retried by the next push to main.
  • release-failed opens a draft fix/release-v<x.y.z> PR listing the jobs that broke with links to their logs, using the same GitHub App as release-pr.yml.

There is no reusable-workflow call left anywhere, and tests/scripts/test_publish_workflow.py asserts there is none, so the permission ceiling cannot come back.

Marketplace robustness

publish_to_marketplace now uploads one VSIX per vsce publish call with --skip-duplicate, retries each three times with a 15s/45s backoff, and collects failures instead of stopping at the first — a flaky gallery upload can no longer strand the other five platforms, and a re-run only retries what is missing. publish-marketplace.yml gets the same treatment. The GitHub half splits out as publish-github, which generates pre-release notes rather than demanding a CHANGELOG section and passes --target so it can tag from a branch ref; that is what lets one job serve both channels.

Verification

  • actionlint v1.7.7: 0 errors in 7 files, including the new publish.yml.
  • pytest (269 tests), ruff, black --check, mypy, pylint, npm run check — all clean. Two pre-existing Windows-only local failures (test_esbuild WinError 193, pylint on signal.SIGKILL) are untouched by this change and green on Linux CI.
  • The check job's shell was run against this tree: it correctly resolves manifest=3.4.1 changelog=3.4.1, no v3.4.1 tag → release, and prerelease-identity3.5.42 / v3.5.42-pre.1 on the other branch.

Heads-up on merging

extension/package/package.json is at 3.4.1, the top changelog heading is 3.4.1, and no v3.4.1 tag exists — because tag-release.yml never worked. So the merge commit will be read as a release commit and this PR's own push to main will publish v3.4.1 to the Marketplace, Open VSX and GitHub Releases. That is the intended catch-up, but it means the first run is a real release rather than a pre-release. The changelog bullet for this change sits under ### Unreleased, so release-pr.yml will follow with a 3.4.2 PR.

Risks worth knowing

  1. --skip-duplicate means a fix that changes shipped code must bump the version — a registry that already accepted the version silently keeps the broken upload. The fix-PR body says so.
  2. While a release keeps failing, every push to main re-attempts it, so the pre-release channel pauses until it lands.
  3. If Publish: GitHub succeeds and only a registry fails, the tag exists, so check will not retry: re-run that one job or dispatch publish-marketplace.yml. Documented in CONTRIBUTING.

🤖 Generated with Claude Code

No stable release had ever published. tag-release.yml set top-level
`permissions: contents: read` and its calling job set none of its own, so the
reusable release.yml was capped at read while its publish job asked for
`contents: write`. A called workflow may only narrow the caller's token, so
GitHub rejected the graph before any job started -- every Tag release run since
the workflow landed ended in startup_failure inside a second.

Nothing had reached Open VSX either, on either channel. The release path never
ran at all. On the pre-release path the Marketplace step ran first in the same
job and timed out on /_apis/gallery pushing all six platform VSIX files through
one `vsce publish` call; a failed step aborts the job, so the Open VSX step
below it never executed. The namespace has been sitting empty since it was
created.

Both are the same structural fault -- three workflows racing on one push, with
all three publish targets as sequential steps in a single job -- so replace
pre-release.yml, release.yml and tag-release.yml with one publish.yml:

* `check` picks the channel once. A commit whose manifest matches the top
  changelog heading with no tag for it yet is a release; everything else is a
  pre-release. A release commit no longer also publishes a pre-release, so the
  five-platform Rust matrix runs once per push instead of twice.
* `verify` fails the run before the 45-minute matrix when a release is missing
  a publish token, and now checks OPEN_VSX_TOKEN as well as VSCE_TOKEN. A
  release skipping Open VSX in silence is half of why this went unnoticed.
* Marketplace, Open VSX and GitHub Releases each publish as their own job, in
  parallel, from one packaged artifact. A failure names itself in the Actions
  UI, one registry can no longer cancel another, and re-running a single red
  job re-publishes a single target.
* The tag is created by the GitHub job rather than ahead of the build, so a tag
  means published, not attempted -- and a release that failed to publish is
  retried by the next push to main.
* `release-failed` opens a draft fix/release-v<x.y.z> pull request listing the
  jobs that broke with links to their logs.

There is no reusable-workflow call left anywhere, and a test asserts there is
none, so the permission ceiling cannot come back.

publish_to_marketplace now uploads one VSIX per `vsce publish` call with
--skip-duplicate, retrying each three times with a backoff and collecting
failures rather than stopping at the first, so a flaky gallery upload cannot
strand the other five platforms and a re-run only retries what is missing.
publish-marketplace.yml gets the same treatment. The GitHub release half splits
out as `publish-github`, which generates pre-release notes instead of demanding
a CHANGELOG section and passes --target so it can tag from a branch ref -- that
is what lets one job serve both channels.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@github-actions

github-actions Bot commented Sep 8, 2026

Copy link
Copy Markdown
Contributor

Coverage

Suite % Lines % Stmts % Branch % Funcs
Rust engine 🟢 91.9 🟢 91.9 ⚪ - 🟢 93.8
Extension-host client 🟢 81.4 🟢 81.4 🟡 71.6 🟡 78.5
Node unit 🟡 77.6 🟡 77.0 🟡 74.1 🟡 76.2

🟢 ≥80% · 🟡 ≥50% · 🔴 <50% · ⚪ unavailable. Expand a section for file coverage and the full-report artifact.

Rust engine details: 20 worst-covered files of 136
File % Lines % Stmts % Branch % Funcs
engine/crates/game/src/constants.rs 🟡 70.4 🟡 70.4 ⚪ - 🟢 100.0
engine/crates/lsp/src/completion/mod.rs 🟡 74.3 🟡 74.3 ⚪ - 🟡 71.0
engine/crates/rules/src/rules_converter/mod.rs 🟡 77.0 🟡 77.0 ⚪ - 🟢 95.5
engine/crates/index/src/lib.rs 🟡 77.4 🟡 77.4 ⚪ - 🟢 87.5
engine/crates/lsp/src/completion/builders.rs 🟡 77.5 🟡 77.5 ⚪ - 🟢 93.3
engine/crates/validation/src/position.rs 🟡 77.8 🟡 77.8 ⚪ - 🟢 90.6
engine/crates/lsp/src/config.rs 🟡 78.8 🟡 78.8 ⚪ - 🟢 81.7
engine/crates/rules/src/rules_converter/subtypes.rs 🟢 81.9 🟢 81.9 ⚪ - 🟢 92.3
engine/crates/lsp/src/navigation/structure.rs 🟢 82.5 🟢 82.5 ⚪ - 🟢 81.8
engine/crates/index/src/variables.rs 🟢 83.0 🟢 83.0 ⚪ - 🟢 85.2
engine/crates/rules/src/rules_types.rs 🟢 83.8 🟢 83.8 ⚪ - 🟡 74.7
engine/crates/lsp/src/server.rs 🟢 84.2 🟢 84.2 ⚪ - 🟢 84.6
engine/crates/cli/src/commands/discover.rs 🟢 84.2 🟢 84.2 ⚪ - 🟢 100.0
engine/crates/cli/src/commands/cache.rs 🟢 84.5 🟢 84.5 ⚪ - 🟢 100.0
engine/crates/index/src/path_match.rs 🟢 84.5 🟢 84.5 ⚪ - 🟢 81.8
engine/crates/cli/src/commands/validate.rs 🟢 84.6 🟢 84.6 ⚪ - 🟢 85.7
engine/crates/validation/src/rule_core/children.rs 🟢 84.6 🟢 84.6 ⚪ - 🟢 97.2
engine/crates/lsp/src/format.rs 🟢 84.7 🟢 84.7 ⚪ - 🟢 96.0
engine/crates/cli/src/commands/fix.rs 🟢 84.9 🟢 84.9 ⚪ - 🔴 41.4
engine/crates/lsp/src/cursor.rs 🟢 85.0 🟢 85.0 ⚪ - 🟢 90.0

Full report is in the rust-coverage artifact.

Extension-host client details: 7 files

Measured labels: host. Source scope: extension/src/host, extension/src/common. Modules measured only by Vitest are excluded.

File % Lines % Stmts % Branch % Funcs
extension/src/host/lspClient.ts 🟡 66.5 🟡 66.5 🔴 44.4 🟡 57.1
extension/src/host/graphPanel.ts 🟢 82.0 🟢 82.0 🟢 80.0 🟢 100.0
extension/src/host/detectGame.ts 🟢 86.2 🟢 86.2 🔴 35.7 🟡 75.0
extension/src/host/extension.ts 🟢 86.2 🟢 86.2 🟡 68.2 🟢 80.0
extension/src/host/serverNotifications.ts 🟢 89.5 🟢 89.5 🟢 81.2 🟢 100.0
extension/src/host/fileExplorer.ts 🟢 91.6 🟢 91.6 🟢 90.2 🟡 77.8
extension/src/common/graphTypes.ts 🟢 100.0 🟢 100.0 🟢 100.0 🟡 50.0

Full report is in the coverage-html artifact.

Node unit details: 19 files
File % Lines % Stmts % Branch % Funcs
extension/src/webview/graph.ts 🟡 60.0 🟡 59.1 🔴 46.6 🟡 50.9
extension/src/host/commands.ts 🟡 67.0 🟡 67.0 🟡 58.2 🟡 59.2
extension/src/host/rulesSetup.ts 🟡 70.1 🟡 70.1 🟡 59.6 🟡 75.0
extension/src/host/documentLanguage.ts 🟢 90.3 🟢 84.0 🟡 63.3 🟢 90.0
extension/src/host/rulesManifest.ts 🟢 91.0 🟢 90.0 🟢 86.0 🟢 92.3
extension/src/host/diagnosticsSignature.ts 🟢 94.1 🟢 94.4 🟡 68.8 🟢 100.0
extension/src/host/commandProgress.ts 🟢 100.0 🟢 100.0 🟢 100.0 🟢 90.0
extension/src/host/engine.ts 🟢 100.0 🟢 98.1 🟢 92.6 🟢 100.0
extension/src/host/executable.ts 🟢 100.0 🟢 100.0 🟢 100.0 🟢 100.0
extension/src/host/fileListSignature.ts 🟢 100.0 🟢 100.0 🟢 100.0 🟢 100.0
extension/src/host/fnv1a.ts 🟢 100.0 🟢 100.0 🟢 100.0 🟢 100.0
extension/src/host/focusTracking.ts 🟢 100.0 🟢 100.0 🟢 100.0 🟢 100.0
extension/src/host/games.ts 🟢 100.0 🟢 100.0 🟢 100.0 🟢 100.0
extension/src/host/graphAvailability.ts 🟢 100.0 🟢 100.0 🟢 100.0 🟢 100.0
extension/src/host/logger.ts 🟢 100.0 🟢 100.0 🟢 100.0 🟢 100.0
extension/src/host/reindexSettings.ts 🟢 100.0 🟢 100.0 🟢 100.0 🟢 100.0
extension/src/host/serverBlockedDialog.ts 🟢 100.0 🟢 100.0 🟢 100.0 🟢 100.0
extension/src/host/trustedPaths.ts 🟢 100.0 🟢 100.0 🟢 95.2 🟢 100.0
extension/src/host/watchedFiles.ts 🟢 100.0 🟢 100.0 🟢 83.3 🟢 100.0

Full report is in the coverage-node artifact.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant