Skip to content

Commit a290b43

Browse files
anandgupta42claude
andcommitted
fix: add CI gate and pre-release check for napi export verification
Prevents stale `@altimateai/altimate-core` platform binaries from reaching production. Two new defense layers: 1. **CI test** (`napi-exports.test.ts`): verifies all 40+ expected function exports exist in the installed binary. Fails CI if a version bump doesn't update the lock file. 2. **Pre-release check** (step 2b): validates 17 critical napi exports before `bun run pre-release` passes. Blocks release tagging when the binary is stale. Together with the runtime validation in altimate-core-internal PR #113, this creates 3 layers of defense: - Build-time: validate-exports.js patches index.js (core-internal) - CI: napi-exports.test.ts fails on missing exports (altimate-code) - Pre-release: pre-release-check.ts blocks stale binaries (altimate-code) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 627d80e commit a290b43

2 files changed

Lines changed: 152 additions & 4 deletions

File tree

packages/opencode/script/pre-release-check.ts

Lines changed: 31 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ function fail(msg: string) {
3636
// ---------------------------------------------------------------------------
3737
// Check 1: Required externals are in package.json dependencies
3838
// ---------------------------------------------------------------------------
39-
console.log("\n[1/4] Checking required externals in package.json...")
39+
console.log("\n[1/5] Checking required externals in package.json...")
4040

4141
const requiredExternals = ["@altimateai/altimate-core"]
4242

@@ -51,7 +51,7 @@ for (const ext of requiredExternals) {
5151
// ---------------------------------------------------------------------------
5252
// Check 2: Required externals are resolvable in node_modules
5353
// ---------------------------------------------------------------------------
54-
console.log("\n[2/4] Checking required externals are installed...")
54+
console.log("\n[2/5] Checking required externals are installed...")
5555

5656
for (const ext of requiredExternals) {
5757
try {
@@ -62,10 +62,37 @@ for (const ext of requiredExternals) {
6262
}
6363
}
6464

65+
// ---------------------------------------------------------------------------
66+
// Check 2b: Verify altimate-core napi binary has all expected exports
67+
// ---------------------------------------------------------------------------
68+
console.log("\n[2b/5] Verifying altimate-core napi exports...")
69+
70+
const CRITICAL_EXPORTS = [
71+
"getStatementTypes", "formatSql", "lint", "validate", "transpile",
72+
"extractMetadata", "columnLineage", "trackLineage", "diffSchemas",
73+
"importDdl", "exportDdl", "optimizeContext", "pruneSchema",
74+
"compareQueries", "classifyPii", "checkQueryPii", "parseDbtProject",
75+
]
76+
77+
try {
78+
const core = require("@altimateai/altimate-core")
79+
const missing = CRITICAL_EXPORTS.filter((name) => typeof core[name] !== "function")
80+
if (missing.length > 0) {
81+
fail(
82+
`altimate-core binary is missing ${missing.length} export(s): ${missing.join(", ")}.\n` +
83+
` The platform binary may be stale. Fix: rm -rf node_modules && bun install`,
84+
)
85+
} else {
86+
pass(`All ${CRITICAL_EXPORTS.length} critical napi exports verified`)
87+
}
88+
} catch (e: any) {
89+
fail(`altimate-core failed to load: ${e.message}`)
90+
}
91+
6592
// ---------------------------------------------------------------------------
6693
// Check 3: Build and smoke-test the binary
6794
// ---------------------------------------------------------------------------
68-
console.log("\n[3/4] Building local binary...")
95+
console.log("\n[3/5] Building local binary...")
6996

7097
const buildResult = spawnSync("bun", ["run", "build:local"], {
7198
cwd: pkgDir,
@@ -105,7 +132,7 @@ if (buildResult.status !== 0) {
105132
if (!binaryPath) {
106133
fail("No binary found in dist/ after build")
107134
} else {
108-
console.log("\n[4/4] Smoke-testing compiled binary...")
135+
console.log("\n[4/5] Smoke-testing compiled binary...")
109136

110137
// Resolve NODE_PATH like the bin wrapper does — start from pkgDir
111138
// to include workspace-level node_modules where NAPI modules live
Lines changed: 121 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,121 @@
1+
/**
2+
* CI gate: verify @altimateai/altimate-core napi binary exports.
3+
*
4+
* This test fails when the installed altimate-core binary is missing expected
5+
* function exports. Catches:
6+
* - Stale platform binary from a cached npm install
7+
* - Version bump in package.json without updating the lock file
8+
* - Broken napi build that silently drops exports
9+
*
10+
* Maintenance: when adding new #[napi] functions in altimate-core-internal,
11+
* add them to EXPECTED_EXPORTS below.
12+
*/
13+
14+
import { describe, test, expect } from "bun:test"
15+
16+
// Every function exported by @altimateai/altimate-core that altimate-code uses.
17+
// This list must be kept in sync with the Rust source at:
18+
// altimate-core-internal/crates/altimate-core-node/src/*.rs (#[napi] functions)
19+
const EXPECTED_EXPORTS = [
20+
// polyglot.rs
21+
"transpile",
22+
"formatSql",
23+
"extractMetadata",
24+
"extractOutputColumns",
25+
"getStatementTypes",
26+
"compareQueries",
27+
// context.rs
28+
"optimizeContext",
29+
"optimizeForQuery",
30+
"pruneSchema",
31+
"diffSchemas",
32+
"importDdl",
33+
"exportDdl",
34+
"schemaFingerprint",
35+
"introspectionSql",
36+
// safety.rs
37+
"lint",
38+
"scanSql",
39+
"isSafe",
40+
"classifyPii",
41+
"checkQueryPii",
42+
"resolveTerm",
43+
"analyzeTags",
44+
// lineage.rs
45+
"columnLineage",
46+
"diffLineage",
47+
"trackLineage",
48+
// tools.rs
49+
"complete",
50+
"rewrite",
51+
"generateTests",
52+
"analyzeMigration",
53+
"parseDbtProject",
54+
// intelligence.rs (via tools)
55+
"correct",
56+
"evaluate",
57+
"explain",
58+
"fix",
59+
"validate",
60+
"checkEquivalence",
61+
"checkPolicy",
62+
"checkSemantics",
63+
// sdk.rs
64+
"initSdk",
65+
"resetSdk",
66+
"flushSdk",
67+
] as const
68+
69+
describe("@altimateai/altimate-core napi exports", () => {
70+
let core: Record<string, unknown>
71+
72+
test("module loads without error", () => {
73+
core = require("@altimateai/altimate-core")
74+
expect(core).toBeDefined()
75+
})
76+
77+
test("all expected function exports exist", () => {
78+
if (!core) return // skip if module failed to load
79+
80+
const missing: string[] = []
81+
for (const name of EXPECTED_EXPORTS) {
82+
if (typeof core[name] !== "function") {
83+
missing.push(name)
84+
}
85+
}
86+
87+
if (missing.length > 0) {
88+
throw new Error(
89+
`@altimateai/altimate-core is missing ${missing.length} expected export(s):\n` +
90+
` ${missing.join(", ")}\n\n` +
91+
`This usually means the platform binary is stale. Fix:\n` +
92+
` rm -rf node_modules && bun install\n\n` +
93+
`If you added new #[napi] exports in altimate-core-internal,\n` +
94+
`publish a new version and update the dependency in package.json.`,
95+
)
96+
}
97+
})
98+
99+
test("Schema class is exported", () => {
100+
if (!core) return
101+
expect(typeof core.Schema).toBe("function")
102+
})
103+
104+
test("getStatementTypes returns expected shape", () => {
105+
if (!core || typeof core.getStatementTypes !== "function") return
106+
const result = (core.getStatementTypes as (sql: string) => any)("SELECT 1")
107+
expect(result).toHaveProperty("statements")
108+
expect(result).toHaveProperty("statement_count")
109+
expect(result).toHaveProperty("types")
110+
expect(result).toHaveProperty("categories")
111+
expect(result.statement_count).toBe(1)
112+
expect(result.categories).toContain("query")
113+
})
114+
115+
test("no unexpected exports removed (detect accidental deletion)", () => {
116+
if (!core) return
117+
const actual = Object.keys(core).filter((k) => typeof core[k] === "function").sort()
118+
// Should have at least as many exports as expected
119+
expect(actual.length).toBeGreaterThanOrEqual(EXPECTED_EXPORTS.length)
120+
})
121+
})

0 commit comments

Comments
 (0)