Skip to content

Commit 88222ec

Browse files
committed
fix(vscode): create the Open VSX namespace before publishing
The 0.12.0 release put 0.8.6 on the Visual Studio Marketplace but left Open VSX with no version at all: all six targets failed because Open VSX rejects a publish into a namespace that does not exist, and the Marketplace has no such concept so nothing upstream catches it. The publish now creates the namespace first and treats an existing one as success, so a re-run is safe. The cause was invisible in the logs. runLocalCli captures the CLI's output, but the per-target summary kept only the first line — the 'Local ovsx exited with code 1:' wrapper — and dropped the registry error after it. Six identical failures reported no reason at all. The summary now carries the registry's own words.
1 parent 5fe0536 commit 88222ec

5 files changed

Lines changed: 147 additions & 4 deletions

File tree

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"pythinker-code": patch
3+
---
4+
5+
Publish the VS Code extension to Open VSX by creating the publisher namespace first, so Cursor, VSCodium and Windsurf can install it, and report the registry's own error when a publish fails instead of only the CLI exit line.

apps/vscode/scripts/ovsx-publish.mjs

Lines changed: 33 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,43 @@
11
#!/usr/bin/env node
2-
import { existsSync } from 'node:fs';
2+
import { existsSync, readFileSync } from 'node:fs';
3+
import { join } from 'node:path';
34

45
import { runLocalCli } from './local-cli.mjs';
56
import { parsePublishArguments, publishUsage } from './publish-args.mjs';
67
import { messageOf, publishEachTarget } from './publish-retry.mjs';
78
import { extensionRoot, isMainModule } from './vsix-targets.mjs';
89
import { verifyVsix } from './vsix-verify.mjs';
910

11+
/**
12+
* Open VSX refuses every publish into a namespace that does not exist yet, and
13+
* the Marketplace has no such concept — so a publisher that works there fails
14+
* here on all six targets at once. Creating it is idempotent from our side: the
15+
* namespace already existing is the success case, not an error.
16+
*
17+
* This is why 0.8.6 reached the Marketplace but no version ever reached Open VSX.
18+
*/
19+
export function ensureNamespace(namespace, run = runLocalCli) {
20+
try {
21+
run('ovsx', 'ovsx', ['create-namespace', namespace], {
22+
cwd: extensionRoot,
23+
encoding: 'utf8',
24+
stdio: 'pipe',
25+
});
26+
console.log(`Created Open VSX namespace ${namespace}.`);
27+
} catch (error) {
28+
if (/already exists|already owned/i.test(messageOf(error))) return;
29+
throw error;
30+
}
31+
}
32+
33+
function publisherName() {
34+
const manifest = JSON.parse(readFileSync(join(extensionRoot, 'package.json'), 'utf8'));
35+
if (typeof manifest.publisher !== 'string' || manifest.publisher === '') {
36+
throw new Error('apps/vscode/package.json has no publisher to use as the Open VSX namespace.');
37+
}
38+
return manifest.publisher;
39+
}
40+
1041
async function main() {
1142
const options = parsePublishArguments(process.argv.slice(2));
1243
if (options.help) {
@@ -16,6 +47,7 @@ async function main() {
1647
if (!process.env.OVSX_PAT) throw new Error('OVSX_PAT is required to publish.');
1748

1849
await verifyInputs(options);
50+
ensureNamespace(publisherName());
1951
await publishEachTarget({
2052
targets: options.targets,
2153
files: options.files,

apps/vscode/scripts/publish-retry.mjs

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,24 @@ export function messageOf(error) {
1212
return error instanceof Error ? error.message : String(error);
1313
}
1414

15+
/**
16+
* One line for a summary, chosen so it carries the registry's own words.
17+
*
18+
* `runLocalCli` wraps a CLI failure as `Local ovsx exited with code 1:` followed
19+
* by the captured output, so reporting only the first line printed six identical
20+
* `FAILED <target>: Local ovsx exited with code 1:` entries with the actual cause
21+
* — a missing Open VSX namespace — cut off right after the colon.
22+
*/
23+
export function summaryLine(error) {
24+
const lines = messageOf(error)
25+
.split('\n')
26+
.map((line) => line.trim())
27+
.filter((line) => line !== '');
28+
if (lines.length === 0) return '';
29+
const [wrapper, ...rest] = lines;
30+
return rest.length === 0 ? wrapper : `${wrapper} ${rest.join(' ')}`.slice(0, 400);
31+
}
32+
1533
/**
1634
* `auth` aborts the whole run — every remaining target would fail identically.
1735
* `transient` is worth retrying. `fatal` fails one target and lets the rest go.
@@ -48,7 +66,7 @@ export async function withRetry(action, options = {}) {
4866
}
4967
const wait = backoffMs[Math.min(attempt - 1, backoffMs.length - 1)];
5068
console.warn(`${label}: ${kind} failure on attempt ${attempt}/${attempts}, retrying in ${wait / 1000}s...`);
51-
console.warn(` ${messageOf(error).split('\n')[0]}`);
69+
console.warn(` ${summaryLine(error)}`);
5270
await delay(wait);
5371
}
5472
}
@@ -79,7 +97,7 @@ export async function publishEachTarget({ targets, files, registry, publishOne }
7997
(outcome === 'skipped' ? skipped : published).push(target);
8098
} catch (error) {
8199
const kind = classifyError(error);
82-
failures.push({ target, message: messageOf(error).split('\n')[0] });
100+
failures.push({ target, message: summaryLine(error) });
83101
if (kind === 'auth') {
84102
abortReason = 'aborted after an authentication failure';
85103
}
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
import { describe, expect, it, vi } from 'vitest';
2+
3+
// @ts-expect-error -- plain .mjs build script, no type declarations
4+
import { ensureNamespace } from '../scripts/ovsx-publish.mjs';
5+
6+
/**
7+
* Open VSX rejects a publish into a namespace that does not exist, which is why
8+
* the 0.12.0 release put 0.8.6 on the Marketplace but left Open VSX with no
9+
* version at all. The Marketplace has no namespace concept, so nothing upstream
10+
* of this catches it.
11+
*/
12+
describe('ensureNamespace', () => {
13+
it('creates the namespace before any publish is attempted', () => {
14+
const run = vi.fn();
15+
16+
ensureNamespace('pythoughts', run);
17+
18+
expect(run).toHaveBeenCalledTimes(1);
19+
const [pkg, bin, args] = run.mock.calls[0] as [string, string, string[]];
20+
expect([pkg, bin]).toEqual(['ovsx', 'ovsx']);
21+
expect(args).toEqual(['create-namespace', 'pythoughts']);
22+
});
23+
24+
it('treats an existing namespace as success, so a re-run is safe', () => {
25+
const run = vi.fn(() => {
26+
throw new Error('Local ovsx exited with code 1:\nERROR Namespace already exists: pythoughts');
27+
});
28+
29+
expect(() => ensureNamespace('pythoughts', run)).not.toThrow();
30+
});
31+
32+
it('propagates a real failure instead of publishing into a broken namespace', () => {
33+
const run = vi.fn(() => {
34+
throw new Error('Local ovsx exited with code 1:\nERROR Response code 401 (Unauthorized)');
35+
});
36+
37+
expect(() => ensureNamespace('pythoughts', run)).toThrow(/401/u);
38+
});
39+
});

apps/vscode/test/publish-retry.test.ts

Lines changed: 50 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { describe, expect, it, vi } from 'vitest';
22

33
// @ts-expect-error -- plain .mjs build script, no type declarations
4-
import { classifyError, publishEachTarget, withRetry } from '../scripts/publish-retry.mjs';
4+
import { classifyError, publishEachTarget, summaryLine, withRetry } from '../scripts/publish-retry.mjs';
55

66
const TARGETS = ['darwin-x64', 'darwin-arm64', 'linux-x64'];
77
const FILES = TARGETS.map((target) => `/tmp/${target}.vsix`);
@@ -50,7 +50,56 @@ describe('withRetry', () => {
5050
});
5151
});
5252

53+
describe('summaryLine', () => {
54+
/**
55+
* The exact shape `runLocalCli` throws, and the exact reason the 0.12.0 release
56+
* printed six `FAILED <target>: Local ovsx exited with code 1:` lines with no
57+
* cause: the summary kept only the wrapper line and dropped the output after it.
58+
*/
59+
it('keeps the registry error that follows the CLI wrapper line', () => {
60+
const error = new Error(
61+
'Local ovsx exited with code 1:\nERROR Unknown namespace: pythoughts\n',
62+
);
63+
64+
const line = summaryLine(error);
65+
66+
expect(line).toContain('Unknown namespace: pythoughts');
67+
expect(line).toContain('exited with code 1');
68+
});
69+
70+
it('leaves a single-line error alone and survives a blank one', () => {
71+
expect(summaryLine(new Error('Response code 401 (Unauthorized)')))
72+
.toBe('Response code 401 (Unauthorized)');
73+
// A CLI that failed without writing anything: every line is blank.
74+
expect(summaryLine(' \n \n')).toBe('');
75+
});
76+
});
77+
5378
describe('publishEachTarget', () => {
79+
it('reports the underlying cause for a failed target, not just the wrapper', async () => {
80+
const publishOne = vi.fn().mockRejectedValue(
81+
new Error('Local ovsx exited with code 1:\nERROR Unknown namespace: pythoughts'),
82+
);
83+
const logged: string[] = [];
84+
const log = vi.spyOn(console, 'log').mockImplementation((...args) => {
85+
logged.push(args.join(' '));
86+
});
87+
88+
try {
89+
await expect(
90+
publishEachTarget({ targets: TARGETS, files: FILES, registry: 'Open VSX', publishOne }),
91+
).rejects.toThrow('3 of 3 target(s) failed');
92+
} finally {
93+
log.mockRestore();
94+
}
95+
96+
const failures = logged.filter((line) => line.includes('FAILED'));
97+
expect(failures).toHaveLength(3);
98+
for (const failure of failures) {
99+
expect(failure).toContain('Unknown namespace: pythoughts');
100+
}
101+
});
102+
54103
it('keeps publishing after one target fails, so a flake cannot strand the rest', async () => {
55104
const publishOne = vi.fn(async (_file: string, target: string) => {
56105
if (target === 'darwin-arm64') throw new Error('Extension rejected');

0 commit comments

Comments
 (0)