diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index d6ce2f843c..09b1580109 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -84,13 +84,45 @@ jobs: run: bun install - name: Run tests - run: bun test --timeout 30000 working-directory: packages/opencode # Cloud E2E tests (Snowflake, BigQuery, Databricks) auto-skip when # ALTIMATE_CODE_CONN_* env vars are not set. Docker E2E tests auto-skip # when Docker is not available. No exclusion needed — skipIf handles it. # --timeout 30000: matches package.json "test" script; prevents 5s default # from cutting off tests that run bun install or bootstrap git instances. + # + # Bun 1.3.x has a known segfault during process cleanup after all tests + # pass (exit code 143/SIGTERM or 134/SIGABRT). We capture test output and + # check for real failures vs Bun crashes to avoid false CI failures. + shell: bash + run: | + # Redirect bun output to file, then cat it for CI visibility. + # This avoids tee/pipe issues where SIGTERM kills tee before flush. + bun test --timeout 30000 > /tmp/test-output.txt 2>&1 || true + cat /tmp/test-output.txt + + # Extract pass/fail counts from Bun test summary (e.g., " 5362 pass") + PASS_COUNT=$(awk '/^ *[0-9]+ pass$/{print $1}' /tmp/test-output.txt || true) + FAIL_COUNT=$(awk '/^ *[0-9]+ fail$/{print $1}' /tmp/test-output.txt || true) + + echo "" + echo "--- Test Summary ---" + echo "pass=${PASS_COUNT:-none} fail=${FAIL_COUNT:-none}" + + # Real test failures — always fail CI + if [ -n "$FAIL_COUNT" ] && [ "$FAIL_COUNT" != "0" ]; then + echo "::error::$FAIL_COUNT test(s) failed" + exit 1 + fi + + # Tests passed (we have a pass count and zero/no failures) + if [ -n "$PASS_COUNT" ] && [ "$PASS_COUNT" -gt 0 ] 2>/dev/null; then + exit 0 + fi + + # No test summary at all — Bun crashed before running tests + echo "::error::No test results found in output — Bun may have crashed before running tests" + exit 1 # --------------------------------------------------------------------------- # Driver E2E tests — only when driver code changes. diff --git a/packages/dbt-tools/src/commands/build.ts b/packages/dbt-tools/src/commands/build.ts index 5d796c764f..b3636adeec 100644 --- a/packages/dbt-tools/src/commands/build.ts +++ b/packages/dbt-tools/src/commands/build.ts @@ -2,8 +2,11 @@ import type { DBTProjectIntegrationAdapter, CommandProcessResult } from "@altima export async function build(adapter: DBTProjectIntegrationAdapter, args: string[]) { const model = flag(args, "model") - if (!model) return project(adapter) const downstream = args.includes("--downstream") + if (!model) { + if (downstream) return { error: "--downstream requires --model" } + return project(adapter) + } const result = await adapter.unsafeBuildModelImmediately({ plusOperatorLeft: "", modelName: model, diff --git a/packages/dbt-tools/test/build.test.ts b/packages/dbt-tools/test/build.test.ts index f73a89af9b..5c5f464c77 100644 --- a/packages/dbt-tools/test/build.test.ts +++ b/packages/dbt-tools/test/build.test.ts @@ -45,6 +45,14 @@ describe("build command", () => { }) }) + test("build --downstream without --model returns error", async () => { + const adapter = makeAdapter() + const result = await build(adapter, ["--downstream"]) + expect(result).toEqual({ error: "--downstream requires --model" }) + expect(adapter.unsafeBuildProjectImmediately).not.toHaveBeenCalled() + expect(adapter.unsafeBuildModelImmediately).not.toHaveBeenCalled() + }) + test("build surfaces stderr as error", async () => { const adapter = makeAdapter({ unsafeBuildProjectImmediately: mock(() => diff --git a/packages/opencode/src/cli/cmd/tui/routes/home.tsx b/packages/opencode/src/cli/cmd/tui/routes/home.tsx index a702e3af25..9a5b0bd1f7 100644 --- a/packages/opencode/src/cli/cmd/tui/routes/home.tsx +++ b/packages/opencode/src/cli/cmd/tui/routes/home.tsx @@ -38,7 +38,7 @@ export function Home() { return Object.values(sync.data.mcp).filter((x) => x.status === "connected").length }) - // altimate_change start — fix race condition: don't show beginner UI until sessions loaded + // altimate_change start — upstream_fix: race condition shows beginner UI flash before sessions loaded const isFirstTimeUser = createMemo(() => { // Don't evaluate until sessions have actually loaded (avoid flash of beginner UI) // Return undefined to represent "loading" state diff --git a/packages/opencode/src/command/index.ts b/packages/opencode/src/command/index.ts index 86763e07a6..d6149d199e 100644 --- a/packages/opencode/src/command/index.ts +++ b/packages/opencode/src/command/index.ts @@ -54,8 +54,9 @@ export namespace Command { const result: string[] = [] const numbered = template.match(/\$\d+/g) if (numbered) { - // altimate_change start — fix lexicographic sort of multi-digit placeholders ($10 before $2) - for (const match of [...new Set(numbered)].sort((a, b) => parseInt(a.slice(1), 10) - parseInt(b.slice(1), 10))) result.push(match) + // altimate_change start — upstream_fix: lexicographic sort of multi-digit placeholders ($10 sorted before $2) + for (const match of [...new Set(numbered)].sort((a, b) => parseInt(a.slice(1), 10) - parseInt(b.slice(1), 10))) + result.push(match) // altimate_change end } if (template.includes("$ARGUMENTS")) result.push("$ARGUMENTS") diff --git a/packages/opencode/src/skill/followups.ts b/packages/opencode/src/skill/followups.ts index 03904b22e1..a8e4bf2431 100644 --- a/packages/opencode/src/skill/followups.ts +++ b/packages/opencode/src/skill/followups.ts @@ -4,7 +4,6 @@ export namespace SkillFollowups { skill: string // skill name to suggest label: string // short display label description: string // why this is a good next step - condition?: string // optional: when this suggestion applies } // Map from skill name to follow-up suggestions @@ -151,7 +150,8 @@ export namespace SkillFollowups { } // A special warehouse nudge for users who haven't connected yet - const WAREHOUSE_NUDGE = "**Tip:** Connect a warehouse to validate against real data. Run `/discover` to auto-detect your connections." + const WAREHOUSE_NUDGE = + "**Tip:** Connect a warehouse to validate against real data. Run `/discover` to auto-detect your connections." export function get(skillName: string): readonly Suggestion[] { return Object.freeze(FOLLOWUPS[skillName] ?? []) diff --git a/packages/opencode/src/util/locale.ts b/packages/opencode/src/util/locale.ts index 1afe30bb81..487b8b7faf 100644 --- a/packages/opencode/src/util/locale.ts +++ b/packages/opencode/src/util/locale.ts @@ -54,8 +54,10 @@ export namespace Locale { const minutes = Math.floor((input % 3600000) / 60000) return `${hours}h ${minutes}m` } + // altimate_change start — upstream_fix: days/hours calculation were swapped (hours used total, not remainder) const days = Math.floor(input / 86400000) const hours = Math.floor((input % 86400000) / 3600000) + // altimate_change end return `${days}d ${hours}h` } diff --git a/script/upstream/README.md b/script/upstream/README.md index 8b4e863a54..facd3c3e28 100644 --- a/script/upstream/README.md +++ b/script/upstream/README.md @@ -222,6 +222,44 @@ When we modify upstream files (not fully custom ones), we wrap our changes with These help during conflict resolution — you can see exactly what we changed vs upstream code. The `analyze.ts` script audits for unclosed marker blocks. +### Upstream Bug Fixes (`upstream_fix:` tag) + +When fixing a **bug in upstream code** (not adding a feature), use the `upstream_fix:` tag in the marker description: + +```typescript +// altimate_change start — upstream_fix: days/hours calculation were swapped +const days = Math.floor(input / 86400000) +const hours = Math.floor((input % 86400000) / 3600000) +// altimate_change end +``` + +**Why this matters:** Regular `altimate_change` markers protect features we added — they're permanent. But upstream bug fixes are **temporary**: once upstream ships their own fix, we should drop our marker and accept theirs. + +Without the `upstream_fix:` tag: +- If upstream fixes the same bug, the merge creates a conflict (good — forces review) +- But the reviewer doesn't know our change was a bug fix vs a feature, so they may keep both + +With the `upstream_fix:` tag: +- Before each merge, run `--audit-fixes` to see all bug fixes we're carrying +- During conflict resolution, reviewers know to check "did upstream fix this?" and can safely drop our version +- After merge, any remaining `upstream_fix:` markers represent bugs upstream hasn't fixed yet + +**When to use which:** + +| Scenario | Marker | +|----------|--------| +| New feature/custom code | `// altimate_change start — description` | +| Fix bug in upstream code | `// altimate_change start — upstream_fix: description` | +| Branding change | No marker (handled by branding transforms) | +| Code in `keepOurs` files | No marker needed | + +**Audit before merging:** + +```bash +# List all upstream bug fixes we're carrying +bun run script/upstream/analyze.ts --audit-fixes +``` + ## File Organization ``` diff --git a/script/upstream/analyze.test.ts b/script/upstream/analyze.test.ts index ae4ae72fce..954bf0ef4c 100644 --- a/script/upstream/analyze.test.ts +++ b/script/upstream/analyze.test.ts @@ -246,6 +246,18 @@ describe("parseDiffForMarkerWarnings", () => { expect(warnings[0].context).toContain("first") }) + test("upstream_fix: tagged markers are recognized as valid markers", () => { + const diff = makeDiff( + `@@ -50,4 +50,6 @@ + const existing = true ++// altimate_change start — upstream_fix: days/hours were swapped ++const days = Math.floor(input / 86400000) ++// altimate_change end + const more = true`, + ) + expect(parseDiffForMarkerWarnings("file.ts", diff)).toEqual([]) + }) + test("real-world scenario: upgrade indicator in footer.tsx", () => { // Simulates the exact diff that leaked: UpgradeIndicator added to // session footer without markers, adjacent to existing yolo marker block. diff --git a/script/upstream/analyze.ts b/script/upstream/analyze.ts index efbc435a38..e385b4f329 100644 --- a/script/upstream/analyze.ts +++ b/script/upstream/analyze.ts @@ -35,6 +35,7 @@ const { values: args } = parseArgs({ version: { type: "string", short: "v" }, branding: { type: "boolean", default: false }, markers: { type: "boolean", default: false }, + "audit-fixes": { type: "boolean", default: false }, strict: { type: "boolean", default: false }, base: { type: "string" }, verbose: { type: "boolean", default: false }, @@ -213,9 +214,7 @@ function printBrandingReport(report: BrandingReport, verbose: boolean): void { const maxLeaksToShow = verbose ? leaks.length : 5 for (let i = 0; i < Math.min(leaks.length, maxLeaksToShow); i++) { const leak = leaks[i] - const truncated = leak.content.length > 80 - ? leak.content.slice(0, 77) + "..." - : leak.content + const truncated = leak.content.length > 80 ? leak.content.slice(0, 77) + "..." : leak.content console.log(` ${DIM}L${String(leak.line).padStart(4)}${RESET} ${YELLOW}${leak.pattern}${RESET}`) console.log(` ${DIM}${truncated}${RESET}`) } @@ -298,14 +297,20 @@ async function analyzeVersion(version: string, config: MergeConfig): Promise "") + const content = await $`git show HEAD:${file}` + .cwd(root) + .text() + .catch(() => "") if (content.includes(config.changeMarker)) { analysis.markerFiles.push(file) } // Check if we've modified this file (potential conflict) - const ourDiff = await $`git diff HEAD -- ${file}`.cwd(root).text().catch(() => "") + const ourDiff = await $`git diff HEAD -- ${file}` + .cwd(root) + .text() + .catch(() => "") if (ourDiff.trim().length > 0) { analysis.potentialConflicts.push(file) } @@ -360,10 +365,16 @@ function printVersionAnalysis(analysis: VersionAnalysis): void { const line = "─".repeat(50) console.log(` ${line}`) console.log(` ${bold("Merge estimate:")}`) - console.log(` Auto-resolvable: ${GREEN}${categories.keepOurs.length + categories.skipFiles.length + categories.lockFiles.length}${RESET}`) + console.log( + ` Auto-resolvable: ${GREEN}${categories.keepOurs.length + categories.skipFiles.length + categories.lockFiles.length}${RESET}`, + ) console.log(` Need transform: ${categories.transformable.length}`) - console.log(` Likely conflicts: ${analysis.potentialConflicts.length > 0 ? RED : GREEN}${analysis.potentialConflicts.length}${RESET}`) - console.log(` Marker files: ${analysis.markerFiles.length > 0 ? YELLOW : GREEN}${analysis.markerFiles.length}${RESET}`) + console.log( + ` Likely conflicts: ${analysis.potentialConflicts.length > 0 ? RED : GREEN}${analysis.potentialConflicts.length}${RESET}`, + ) + console.log( + ` Marker files: ${analysis.markerFiles.length > 0 ? YELLOW : GREEN}${analysis.markerFiles.length}${RESET}`, + ) console.log() } @@ -446,7 +457,9 @@ function printMarkerAnalysis(config: MergeConfig): void { const complete = markers.filter((m) => m.endLine !== null) const incomplete = markers.filter((m) => m.endLine === null) - console.log(` Found ${bold(String(markers.length))} marker blocks in ${new Set(markers.map((m) => m.file)).size} files`) + console.log( + ` Found ${bold(String(markers.length))} marker blocks in ${new Set(markers.map((m) => m.file)).size} files`, + ) console.log(` ${GREEN}Complete (start + end):${RESET} ${complete.length}`) if (incomplete.length > 0) { @@ -468,10 +481,50 @@ function printMarkerAnalysis(config: MergeConfig): void { // Summary console.log() - console.log(` ${bold("Integrity:")} ${incomplete.length === 0 - ? `${GREEN}All blocks properly closed${RESET}` - : `${RED}${incomplete.length} unclosed block(s)${RESET}` - }`) + console.log( + ` ${bold("Integrity:")} ${ + incomplete.length === 0 + ? `${GREEN}All blocks properly closed${RESET}` + : `${RED}${incomplete.length} unclosed block(s)${RESET}` + }`, + ) +} + +// --------------------------------------------------------------------------- +// Upstream fix audit (--audit-fixes) +// --------------------------------------------------------------------------- + +function auditUpstreamFixes(config: MergeConfig): void { + const markers = findMarkers(config) + const fixes = markers.filter((m) => m.startComment.includes("upstream_fix:")) + + console.log() + console.log(bold("=== Upstream Bug Fixes We're Carrying ===")) + console.log() + + if (fixes.length === 0) { + console.log(` ${GREEN}No upstream_fix: markers found.${RESET}`) + console.log(` All our markers are feature additions, not bug fixes.`) + console.log() + return + } + + console.log(` Found ${bold(String(fixes.length))} upstream bug fix(es) to review before merge:\n`) + + for (const fix of fixes) { + // Extract description after "upstream_fix:" + const desc = fix.startComment.replace(/.*upstream_fix:\s*/, "").replace(/\s*\*\/\s*$/, "") + const lines = fix.endLine ? `${fix.line}-${fix.endLine}` : `${fix.line}` + console.log(` ${YELLOW}fix${RESET} ${fix.file}:${lines}`) + console.log(` ${desc}`) + console.log() + } + + console.log(` ${bold("Before each upstream merge:")}`) + console.log(` 1. Check if upstream fixed each issue in their release`) + console.log(` 2. If fixed upstream: accept their version, remove our marker`) + console.log(` 3. If not fixed: keep our marker (it will survive the merge)`) + console.log() } // --------------------------------------------------------------------------- @@ -490,6 +543,7 @@ function printUsage(): void { --version, -v Upstream version to analyze --branding Scan codebase for upstream branding leaks --markers Check changed files for missing altimate_change markers + --audit-fixes List all upstream_fix: markers (bug fixes we made to upstream code) --base Base branch for --markers comparison (default: HEAD) --strict Exit with code 1 on warnings (for CI) --verbose Show all results (not just top 20) @@ -509,6 +563,9 @@ function printUsage(): void { ${dim("# Check PR for missing markers (CI)")} bun run script/upstream/analyze.ts --markers --base main --strict + ${dim("# List upstream bug fixes we're carrying (review before merge)")} + bun run script/upstream/analyze.ts --audit-fixes + ${dim("# Machine-readable output for CI")} bun run script/upstream/analyze.ts --branding --json `) @@ -530,14 +587,9 @@ function getChangedFiles(base?: string): string[] { const root = repoRoot() // Only check Modified files (M), not Added (A). New files don't exist // upstream so they can't be overwritten by a merge — no markers needed. - const cmd = base - ? `git diff --name-only --diff-filter=M ${base}...HEAD` - : `git diff --name-only --diff-filter=M HEAD` + const cmd = base ? `git diff --name-only --diff-filter=M ${base}...HEAD` : `git diff --name-only --diff-filter=M HEAD` try { - return execSync(cmd, { cwd: root, encoding: "utf-8" }) - .trim() - .split("\n") - .filter(Boolean) + return execSync(cmd, { cwd: root, encoding: "utf-8" }).trim().split("\n").filter(Boolean) } catch { return [] } @@ -646,8 +698,14 @@ export function parseDiffForMarkerWarnings(file: string, diffOutput: string): Ma currentLine++ const content = line.slice(1).trim() - if (content.includes("altimate_change start")) { inMarkerBlock = true; continue } - if (content.includes("altimate_change end")) { inMarkerBlock = false; continue } + if (content.includes("altimate_change start")) { + inMarkerBlock = true + continue + } + if (content.includes("altimate_change end")) { + inMarkerBlock = false + continue + } if (content.includes("altimate_change")) continue // Only flag added lines as violations — context lines are pre-existing @@ -686,9 +744,7 @@ function checkFileForMarkers(file: string, base?: string): MarkerWarning[] { const { execSync } = require("child_process") const root = repoRoot() - const diffCmd = base - ? `git diff -U5 ${base}...HEAD -- "${file}"` - : `git diff -U5 HEAD -- "${file}"` + const diffCmd = base ? `git diff -U5 ${base}...HEAD -- "${file}"` : `git diff -U5 HEAD -- "${file}"` let diffOutput: string try { @@ -770,8 +826,9 @@ async function main(): Promise { const hasVersion = Boolean(args.version) const hasBranding = Boolean(args.branding) const hasMarkers = Boolean(args.markers) + const hasAuditFixes = Boolean(args["audit-fixes"]) - if (!hasVersion && !hasBranding && !hasMarkers) { + if (!hasVersion && !hasBranding && !hasMarkers && !hasAuditFixes) { // Default: run marker analysis printMarkerAnalysis(config) @@ -779,9 +836,16 @@ async function main(): Promise { logger.info("Use --version to analyze an upstream version") logger.info("Use --branding to audit for branding leaks") logger.info("Use --markers --base main to check for missing markers") + logger.info("Use --audit-fixes to list upstream bug fixes we're carrying") return } + // ─── Upstream fix audit ────────────────────────────────────────────────── + if (hasAuditFixes) { + auditUpstreamFixes(config) + if (!hasVersion && !hasBranding && !hasMarkers) return + } + // ─── Version analysis ────────────────────────────────────────────────────── if (hasVersion) {