-
Notifications
You must be signed in to change notification settings - Fork 20
fix: add release smoke tests to prevent broken binary publishes #463
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,16 +1,26 @@ | ||
| fix: Codespaces — skip machine-scoped `GITHUB_TOKEN`, cap retries, fix phantom command | ||
| fix: add release smoke tests to prevent broken binary publishes | ||
|
|
||
| Closes #413 | ||
| Closes #462 | ||
|
|
||
| - Skip auto-enabling `github-models` and `github-copilot` providers in | ||
| machine environments (Codespaces: `CODESPACES=true`, GitHub Actions: | ||
| `GITHUB_ACTIONS=true`) when only machine-scoped tokens (`GITHUB_TOKEN`, | ||
| `GH_TOKEN`) are available. The Codespace/Actions token lacks | ||
| `models:read` scope needed for GitHub Models API. | ||
| - Cap retry attempts at 5 (`RETRY_MAX_ATTEMPTS`) to prevent infinite | ||
| retry loops. Log actionable warning when retries exhaust. | ||
| - Replace phantom `/discover-and-add-mcps` toast with actionable message. | ||
| - Add `.devcontainer/` config (Node 22, Bun 1.3.10) for Codespaces. | ||
| - Add 32 adversarial e2e tests covering full Codespace/Actions env | ||
| simulation, `GH_TOKEN`, token variations, config overrides, retry bounds. | ||
| - Update docs to reference `mcp_discover` tool. | ||
| v0.5.10 shipped with `@altimateai/altimate-core` missing from standalone | ||
| distributions, crashing on startup. Add three-layer defense: | ||
|
|
||
| - **CI smoke test**: run the compiled `linux-x64` binary after build and | ||
| before npm publish — catches runtime crashes that compile fine | ||
| - **Build-time verification**: validate all `requiredExternals` are in | ||
| `package.json` `dependencies` (not just `devDependencies`) so they | ||
| ship in the npm wrapper package | ||
| - **Local pre-release script**: `bun run pre-release` builds + smoke-tests | ||
| the binary before tagging — mandatory step in RELEASING.md | ||
|
|
||
| Also adds `smoke-test-binary.test.ts` with 3 tests: version check, | ||
| standalone graceful-failure, and `--help` output. | ||
|
|
||
| Reviewed by 5 AI models (Claude, GPT 5.2 Codex, Gemini 3.1 Pro, | ||
| Kimi K2.5, MiniMax M2.5). Key fixes from review: | ||
| - Include workspace `node_modules` in `NODE_PATH` (Gemini) | ||
| - Restrict dep check to `dependencies` only (GPT, Gemini, Kimi) | ||
| - Hard-fail pre-publish gate when binary not found (Claude, GPT) | ||
| - Tighten exit code assertion for signal safety (MiniMax) | ||
|
|
||
| Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,151 @@ | ||
| #!/usr/bin/env bun | ||
|
|
||
| /** | ||
| * Pre-release sanity check — run BEFORE tagging a release. | ||
| * | ||
| * Verifies: | ||
| * 1. All required external NAPI modules are in package.json dependencies | ||
| * 2. The publish script will include them in the wrapper package | ||
| * 3. A local build produces a binary that actually starts | ||
| * | ||
| * Usage: bun run packages/opencode/script/pre-release-check.ts | ||
| */ | ||
|
|
||
| import fs from "fs" | ||
| import path from "path" | ||
| import { spawnSync } from "child_process" | ||
| import { fileURLToPath } from "url" | ||
|
|
||
| const __dirname = path.dirname(fileURLToPath(import.meta.url)) | ||
| const pkgDir = path.resolve(__dirname, "..") | ||
| const repoRoot = path.resolve(pkgDir, "../..") | ||
|
|
||
| const pkg = JSON.parse(fs.readFileSync(path.join(pkgDir, "package.json"), "utf-8")) | ||
|
|
||
| let failures = 0 | ||
|
|
||
| function pass(msg: string) { | ||
| console.log(` ✓ ${msg}`) | ||
| } | ||
|
|
||
| function fail(msg: string) { | ||
| console.error(` ✗ ${msg}`) | ||
| failures++ | ||
| } | ||
|
|
||
| // --------------------------------------------------------------------------- | ||
| // Check 1: Required externals are in package.json dependencies | ||
| // --------------------------------------------------------------------------- | ||
| console.log("\n[1/4] Checking required externals in package.json...") | ||
|
|
||
| const requiredExternals = ["@altimateai/altimate-core"] | ||
|
|
||
| for (const ext of requiredExternals) { | ||
| if (pkg.dependencies?.[ext]) { | ||
| pass(`${ext} is in dependencies (${pkg.dependencies[ext]})`) | ||
| } else { | ||
| fail(`${ext} is NOT in dependencies — binary will crash at runtime`) | ||
| } | ||
| } | ||
|
|
||
| // --------------------------------------------------------------------------- | ||
| // Check 2: Required externals are resolvable in node_modules | ||
| // --------------------------------------------------------------------------- | ||
| console.log("\n[2/4] Checking required externals are installed...") | ||
|
|
||
| for (const ext of requiredExternals) { | ||
| try { | ||
| require.resolve(ext) | ||
| pass(`${ext} resolves from node_modules`) | ||
| } catch { | ||
| fail(`${ext} is NOT installed — run \`bun install\``) | ||
| } | ||
| } | ||
|
|
||
| // --------------------------------------------------------------------------- | ||
| // Check 3: Build and smoke-test the binary | ||
| // --------------------------------------------------------------------------- | ||
| console.log("\n[3/4] Building local binary...") | ||
|
|
||
| const buildResult = spawnSync("bun", ["run", "build:local"], { | ||
| cwd: pkgDir, | ||
| encoding: "utf-8", | ||
| timeout: 120_000, | ||
| env: { | ||
| ...process.env, | ||
| MODELS_DEV_API_JSON: path.join(pkgDir, "test/tool/fixtures/models-api.json"), | ||
| }, | ||
| }) | ||
|
|
||
| if (buildResult.status !== 0) { | ||
| fail(`Build failed:\n${buildResult.stderr}`) | ||
| } else { | ||
| pass("Local build succeeded") | ||
|
|
||
| // Find the binary — walk recursively for scoped packages (@altimateai/...) | ||
| const distDir = path.join(pkgDir, "dist") | ||
| let binaryPath: string | undefined | ||
| const binaryNames = process.platform === "win32" ? ["altimate.exe", "altimate"] : ["altimate"] | ||
| function searchDist(dir: string): string | undefined { | ||
| if (!fs.existsSync(dir)) return undefined | ||
| for (const entry of fs.readdirSync(dir, { withFileTypes: true })) { | ||
| if (!entry.isDirectory()) continue | ||
| const sub = path.join(dir, entry.name) | ||
| for (const name of binaryNames) { | ||
| const candidate = path.join(sub, "bin", name) | ||
| if (fs.existsSync(candidate)) return candidate | ||
| } | ||
| const nested = searchDist(sub) | ||
| if (nested) return nested | ||
| } | ||
| return undefined | ||
| } | ||
| binaryPath = searchDist(distDir) | ||
|
|
||
| if (!binaryPath) { | ||
| fail("No binary found in dist/ after build") | ||
| } else { | ||
| console.log("\n[4/4] Smoke-testing compiled binary...") | ||
|
|
||
| // Resolve NODE_PATH like the bin wrapper does — start from pkgDir | ||
| // to include workspace-level node_modules where NAPI modules live | ||
| const nodePaths: string[] = [] | ||
| let current = pkgDir | ||
| for (;;) { | ||
| const nm = path.join(current, "node_modules") | ||
| if (fs.existsSync(nm)) nodePaths.push(nm) | ||
| const parent = path.dirname(current) | ||
| if (parent === current) break | ||
| current = parent | ||
| } | ||
|
|
||
| const smokeResult = spawnSync(binaryPath, ["--version"], { | ||
| encoding: "utf-8", | ||
| timeout: 15_000, | ||
| env: { | ||
| ...process.env, | ||
| NODE_PATH: nodePaths.join(path.delimiter), | ||
| OPENCODE_DISABLE_TELEMETRY: "1", | ||
coderabbitai[bot] marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| }, | ||
| }) | ||
|
|
||
| if (smokeResult.status === 0) { | ||
| const version = (smokeResult.stdout ?? "").trim() | ||
| pass(`Binary starts successfully (${version})`) | ||
| } else { | ||
| const output = (smokeResult.stdout ?? "") + (smokeResult.stderr ?? "") | ||
| fail(`Binary crashed on startup:\n${output}`) | ||
| } | ||
| } | ||
| } | ||
|
|
||
| // --------------------------------------------------------------------------- | ||
| // Summary | ||
| // --------------------------------------------------------------------------- | ||
| console.log("") | ||
| if (failures > 0) { | ||
| console.error(`FAILED: ${failures} check(s) failed. Do NOT tag a release.`) | ||
| process.exit(1) | ||
| } else { | ||
| console.log("ALL CHECKS PASSED. Safe to tag a release.") | ||
| } | ||
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.