Skip to content

Commit 580252a

Browse files
committed
chore: consolidate sdk release scripts
1 parent ed54a78 commit 580252a

5 files changed

Lines changed: 243 additions & 138 deletions

File tree

.github/workflows/release-cli-sdk.yml

Lines changed: 2 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -199,42 +199,9 @@ jobs:
199199
# Python SDK publish (only if semantic-release created a release)
200200
# PyPI uses OIDC trusted publishing via gh-action-pypi-publish.
201201
# ---------------------------------------------------------------
202-
- name: Prepare Python SDK tools
202+
- name: Build and verify Python SDK
203203
if: steps.sdk_release.outputs.released == 'true'
204-
run: |
205-
rm -rf packages/sdk/langs/python/superdoc/tools
206-
cp -r packages/sdk/tools packages/sdk/langs/python/superdoc/tools
207-
rm -f packages/sdk/langs/python/superdoc/tools/__init__.py
208-
209-
- name: Build companion Python wheels
210-
if: steps.sdk_release.outputs.released == 'true'
211-
run: node packages/sdk/scripts/build-python-companion-wheels.mjs
212-
213-
- name: Verify companion wheels
214-
if: steps.sdk_release.outputs.released == 'true'
215-
run: node packages/sdk/scripts/verify-python-companion-wheels.mjs --companions-only
216-
217-
- name: Build main Python SDK wheel
218-
if: steps.sdk_release.outputs.released == 'true'
219-
run: python -m build
220-
working-directory: packages/sdk/langs/python
221-
222-
- name: Verify main wheel
223-
if: steps.sdk_release.outputs.released == 'true'
224-
run: node packages/sdk/scripts/verify-python-companion-wheels.mjs --root-only
225-
226-
- name: Smoke test (wheelhouse install + marker resolution)
227-
if: steps.sdk_release.outputs.released == 'true'
228-
run: |
229-
python3 -m venv /tmp/sdk-smoke
230-
mkdir -p /tmp/sdk-wheelhouse
231-
cp packages/sdk/langs/python/dist/*.whl /tmp/sdk-wheelhouse/
232-
cp packages/sdk/langs/python/companion-dist/*.whl /tmp/sdk-wheelhouse/
233-
/tmp/sdk-smoke/bin/pip install superdoc-sdk --find-links /tmp/sdk-wheelhouse --no-index
234-
/tmp/sdk-smoke/bin/python -c "from superdoc import SuperDocClient; SuperDocClient()"
235-
/tmp/sdk-smoke/bin/python -c "from superdoc.embedded_cli import resolve_embedded_cli_path; import subprocess; p = resolve_embedded_cli_path(); r = subprocess.run([p, '--help'], capture_output=True, text=True, timeout=10); assert r.returncode == 0, f'CLI exited {r.returncode}: {r.stderr}'; print(f'CLI binary OK: {p}')"
236-
echo "Smoke test passed."
237-
rm -rf /tmp/sdk-smoke /tmp/sdk-wheelhouse
204+
run: node packages/sdk/scripts/build-python-sdk.mjs
238205

239206
- name: Publish companion Python packages to PyPI
240207
if: steps.sdk_release.outputs.released == 'true'

.github/workflows/release-sdk.yml

Lines changed: 4 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -119,42 +119,13 @@ jobs:
119119
pnpm --prefix apps/cli run build:native:all
120120
pnpm --prefix apps/cli run build:stage
121121
122-
- name: Prepare Python SDK tools
123-
run: |
124-
rm -rf packages/sdk/langs/python/superdoc/tools
125-
cp -r packages/sdk/tools packages/sdk/langs/python/superdoc/tools
126-
rm -f packages/sdk/langs/python/superdoc/tools/__init__.py
127-
122+
# Staging is a prerequisite — in the automated workflow, sdk-release-publish.mjs
123+
# handles it (step 5). Here we must do it explicitly.
128124
- name: Stage CLI binaries into companion packages
129125
run: node packages/sdk/scripts/stage-python-companion-cli.mjs
130126

131-
- name: Build companion Python wheels
132-
run: node packages/sdk/scripts/build-python-companion-wheels.mjs
133-
134-
- name: Verify companion wheels
135-
run: node packages/sdk/scripts/verify-python-companion-wheels.mjs --companions-only
136-
137-
- name: Build main Python SDK wheel
138-
run: python -m build
139-
working-directory: packages/sdk/langs/python
140-
141-
- name: Verify main wheel
142-
run: node packages/sdk/scripts/verify-python-companion-wheels.mjs --root-only
143-
144-
# Pre-publish install smoke gate — validates marker-driven resolution
145-
# end-to-end before anything reaches PyPI. Uses a wheelhouse so pip
146-
# resolves the marker deps from local files, not the network.
147-
- name: Smoke test (wheelhouse install + marker resolution)
148-
run: |
149-
python3 -m venv /tmp/sdk-smoke
150-
mkdir -p /tmp/sdk-wheelhouse
151-
cp packages/sdk/langs/python/dist/*.whl /tmp/sdk-wheelhouse/
152-
cp packages/sdk/langs/python/companion-dist/*.whl /tmp/sdk-wheelhouse/
153-
/tmp/sdk-smoke/bin/pip install superdoc-sdk --find-links /tmp/sdk-wheelhouse --no-index
154-
/tmp/sdk-smoke/bin/python -c "from superdoc import SuperDocClient; SuperDocClient()"
155-
/tmp/sdk-smoke/bin/python -c "from superdoc.embedded_cli import resolve_embedded_cli_path; import subprocess; p = resolve_embedded_cli_path(); r = subprocess.run([p, '--help'], capture_output=True, text=True, timeout=10); assert r.returncode == 0, f'CLI exited {r.returncode}: {r.stderr}'; print(f'CLI binary OK: {p}')"
156-
echo "Smoke test passed — marker resolution + binary execution work."
157-
rm -rf /tmp/sdk-smoke /tmp/sdk-wheelhouse
127+
- name: Build and verify Python SDK
128+
run: node packages/sdk/scripts/build-python-sdk.mjs
158129

159130
# Publish companions first — root depends on them being on PyPI.
160131
- name: Publish companion Python packages to PyPI
Lines changed: 228 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,228 @@
1+
#!/usr/bin/env node
2+
3+
/**
4+
* Consolidated Python SDK build pipeline.
5+
*
6+
* Runs the full 6-step pipeline: prepare tools, build companion wheels,
7+
* verify companions, build main wheel, verify main wheel, smoke test.
8+
*
9+
* Usage:
10+
* node build-python-sdk.mjs
11+
* node build-python-sdk.mjs --skip-smoke-test
12+
*
13+
* Prerequisites (checked at startup, fail-fast):
14+
* - packages/sdk/tools/catalog.json exists (tools generated)
15+
* - All companion packages have a staged CLI binary
16+
*/
17+
18+
import { execFileSync } from 'node:child_process';
19+
import { cpSync, existsSync, lstatSync, mkdtempSync, readdirSync, rmSync, symlinkSync } from 'node:fs';
20+
import path from 'node:path';
21+
import { tmpdir } from 'node:os';
22+
import { fileURLToPath } from 'node:url';
23+
import { PYTHON_CLI_PLATFORM_TARGETS } from './python-embedded-cli-targets.mjs';
24+
25+
const __filename = fileURLToPath(import.meta.url);
26+
const __dirname = path.dirname(__filename);
27+
const REPO_ROOT = path.resolve(__dirname, '../../../');
28+
29+
const PYTHON_SDK_DIR = path.join(REPO_ROOT, 'packages/sdk/langs/python');
30+
const TOOLS_SOURCE = path.join(REPO_ROOT, 'packages/sdk/tools');
31+
const TOOLS_DEST = path.join(PYTHON_SDK_DIR, 'superdoc', 'tools');
32+
const CATALOG_PATH = path.join(TOOLS_SOURCE, 'catalog.json');
33+
const PYTHON_PLATFORMS_ROOT = path.join(PYTHON_SDK_DIR, 'platforms');
34+
35+
const skipSmokeTest = process.argv.slice(2).includes('--skip-smoke-test');
36+
37+
// ---------------------------------------------------------------------------
38+
// Helpers
39+
// ---------------------------------------------------------------------------
40+
41+
function step(number, label) {
42+
console.log(`\n${'='.repeat(60)}`);
43+
console.log(` Step ${number}/6: ${label}`);
44+
console.log(`${'='.repeat(60)}\n`);
45+
}
46+
47+
function run(command, args, { cwd = REPO_ROOT } = {}) {
48+
execFileSync(command, args, { cwd, stdio: 'inherit', env: process.env });
49+
}
50+
51+
// ---------------------------------------------------------------------------
52+
// Prerequisites
53+
// ---------------------------------------------------------------------------
54+
55+
function checkPrerequisites() {
56+
if (!existsSync(CATALOG_PATH)) {
57+
throw new Error(
58+
`Prerequisite failed: ${path.relative(REPO_ROOT, CATALOG_PATH)} not found.\n` +
59+
`Run "pnpm run generate:all" first to generate SDK tool artifacts.`
60+
);
61+
}
62+
63+
const missing = PYTHON_CLI_PLATFORM_TARGETS.filter((target) => {
64+
const binDir = path.join(PYTHON_PLATFORMS_ROOT, target.companionPypiName, target.companionModuleName, 'bin');
65+
try {
66+
const entries = readdirSync(binDir);
67+
return !entries.some((e) => e === target.binaryName);
68+
} catch {
69+
return true;
70+
}
71+
});
72+
73+
if (missing.length > 0) {
74+
throw new Error(
75+
`Prerequisite failed: staged CLI binary missing for ${missing.length} target(s):\n` +
76+
missing.map((t) => ` - ${t.id} (${t.companionPypiName})`).join('\n') + '\n' +
77+
`Run the CLI build + stage steps first:\n` +
78+
` pnpm --prefix apps/cli run build:native:all\n` +
79+
` pnpm --prefix apps/cli run build:stage\n` +
80+
` node packages/sdk/scripts/stage-python-companion-cli.mjs`
81+
);
82+
}
83+
}
84+
85+
// ---------------------------------------------------------------------------
86+
// Steps
87+
// ---------------------------------------------------------------------------
88+
89+
function prepareTools() {
90+
step(1, 'Prepare Python SDK tools');
91+
92+
rmSync(TOOLS_DEST, { recursive: true, force: true });
93+
cpSync(TOOLS_SOURCE, TOOLS_DEST, { recursive: true });
94+
95+
// Remove Python package marker — not needed inside the SDK package
96+
const initPy = path.join(TOOLS_DEST, '__init__.py');
97+
rmSync(initPy, { force: true });
98+
99+
// Verify catalog.json was copied
100+
if (!existsSync(path.join(TOOLS_DEST, 'catalog.json'))) {
101+
throw new Error('Failed to copy catalog.json into Python SDK tools directory.');
102+
}
103+
104+
console.log('Tools prepared.');
105+
}
106+
107+
function buildCompanionWheels() {
108+
step(2, 'Build companion Python wheels');
109+
run('node', [path.join(__dirname, 'build-python-companion-wheels.mjs')]);
110+
}
111+
112+
function verifyCompanionWheels() {
113+
step(3, 'Verify companion wheels');
114+
run('node', [path.join(__dirname, 'verify-python-companion-wheels.mjs'), '--companions-only']);
115+
}
116+
117+
function buildMainWheel() {
118+
step(4, 'Build main Python SDK wheel');
119+
120+
// Clean previous build artifacts so the verifier doesn't pick up stale wheels
121+
rmSync(path.join(PYTHON_SDK_DIR, 'dist'), { recursive: true, force: true });
122+
rmSync(path.join(PYTHON_SDK_DIR, 'build'), { recursive: true, force: true });
123+
124+
run('python3', ['-m', 'build'], { cwd: PYTHON_SDK_DIR });
125+
}
126+
127+
function verifyMainWheel() {
128+
step(5, 'Verify main wheel');
129+
run('node', [path.join(__dirname, 'verify-python-companion-wheels.mjs'), '--root-only']);
130+
}
131+
132+
function smokeTest() {
133+
if (skipSmokeTest) {
134+
step(6, 'Smoke test (skipped — --skip-smoke-test)');
135+
return;
136+
}
137+
138+
step(6, 'Smoke test (wheelhouse install + marker resolution)');
139+
140+
const venvDir = mkdtempSync(path.join(tmpdir(), 'sdk-smoke-'));
141+
const wheelhouseDir = mkdtempSync(path.join(tmpdir(), 'sdk-wheelhouse-'));
142+
143+
try {
144+
// Create venv
145+
run('python3', ['-m', 'venv', venvDir]);
146+
147+
// Determine pip/python paths (cross-platform)
148+
const binDir = process.platform === 'win32' ? 'Scripts' : 'bin';
149+
const pip = path.join(venvDir, binDir, 'pip');
150+
const python = path.join(venvDir, binDir, 'python');
151+
152+
// Copy all wheels to wheelhouse
153+
const distDir = path.join(PYTHON_SDK_DIR, 'dist');
154+
const companionDistDir = path.join(PYTHON_SDK_DIR, 'companion-dist');
155+
156+
for (const dir of [distDir, companionDistDir]) {
157+
for (const entry of readdirSync(dir)) {
158+
if (entry.endsWith('.whl')) {
159+
cpSync(path.join(dir, entry), path.join(wheelhouseDir, entry));
160+
}
161+
}
162+
}
163+
164+
// Install from wheelhouse (offline)
165+
run(pip, ['install', 'superdoc-sdk', '--find-links', wheelhouseDir, '--no-index']);
166+
167+
// Verify import
168+
run(python, ['-c', 'from superdoc import SuperDocClient; SuperDocClient()']);
169+
170+
// Verify embedded CLI binary resolution + execution
171+
run(python, [
172+
'-c',
173+
'from superdoc.embedded_cli import resolve_embedded_cli_path; ' +
174+
'import subprocess; ' +
175+
'p = resolve_embedded_cli_path(); ' +
176+
'r = subprocess.run([p, "--help"], capture_output=True, text=True, timeout=10); ' +
177+
'assert r.returncode == 0, f"CLI exited {r.returncode}: {r.stderr}"; ' +
178+
'print(f"CLI binary OK: {p}")',
179+
]);
180+
181+
console.log('Smoke test passed.');
182+
} finally {
183+
rmSync(venvDir, { recursive: true, force: true });
184+
rmSync(wheelhouseDir, { recursive: true, force: true });
185+
}
186+
}
187+
188+
// ---------------------------------------------------------------------------
189+
// Main
190+
// ---------------------------------------------------------------------------
191+
192+
function main() {
193+
console.log('Python SDK Build Pipeline');
194+
if (skipSmokeTest) console.log(' --skip-smoke-test: smoke test will be skipped');
195+
196+
checkPrerequisites();
197+
198+
// Save symlink state — prepareTools() replaces it with a real copy.
199+
// Restore on exit so local dev symlinks aren't lost.
200+
let wasSymlink = false;
201+
try {
202+
const stat = lstatSync(TOOLS_DEST);
203+
wasSymlink = stat.isSymbolicLink();
204+
} catch { /* doesn't exist — nothing to restore */ }
205+
206+
try {
207+
prepareTools();
208+
buildCompanionWheels();
209+
verifyCompanionWheels();
210+
buildMainWheel();
211+
verifyMainWheel();
212+
smokeTest();
213+
214+
console.log('\nPython SDK build pipeline complete.');
215+
} finally {
216+
if (wasSymlink) {
217+
rmSync(TOOLS_DEST, { recursive: true, force: true });
218+
symlinkSync('../../../tools', TOOLS_DEST);
219+
}
220+
}
221+
}
222+
223+
try {
224+
main();
225+
} catch (error) {
226+
console.error(`\nPython SDK build pipeline failed: ${error.message}`);
227+
process.exitCode = 1;
228+
}

packages/sdk/scripts/sdk-release-publish.mjs

Lines changed: 3 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -86,27 +86,10 @@ function main() {
8686

8787
// 7. Python publish (unless --npm-only, which defers to workflow-level PyPI action)
8888
if (npmOnly) {
89-
console.log('\n Skipping Python publish (--npm-only). PyPI publish handled by workflow.\n');
89+
console.log('\n Skipping Python build (--npm-only). Python build + PyPI publish handled by workflow.\n');
9090
} else {
91-
// Build companion wheels
92-
run('node', [path.join(__dirname, 'build-python-companion-wheels.mjs')], {
93-
label: 'Step 7a/7: Build Python companion wheels',
94-
});
95-
96-
// Verify companion wheels
97-
run('node', [path.join(__dirname, 'verify-python-companion-wheels.mjs'), '--companions-only'], {
98-
label: 'Step 7b/7: Verify companion wheels',
99-
});
100-
101-
// Build main Python SDK wheel
102-
run('python', ['-m', 'build'], {
103-
cwd: path.join(REPO_ROOT, 'packages/sdk/langs/python'),
104-
label: 'Step 7c/7: Build main Python SDK wheel',
105-
});
106-
107-
// Verify main wheel
108-
run('node', [path.join(__dirname, 'verify-python-companion-wheels.mjs'), '--root-only'], {
109-
label: 'Step 7d/7: Verify main Python wheel',
91+
run('node', [path.join(__dirname, 'build-python-sdk.mjs')], {
92+
label: 'Step 7/7: Build and verify Python SDK',
11093
});
11194

11295
if (!dryRun) {

0 commit comments

Comments
 (0)