Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 33 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -379,6 +379,39 @@ jobs:
$config.Output.Verbosity = "Detailed"
Invoke-Pester -Configuration $config

# altimate_change start — Windows ripgrep E2E (issue #1072)
# ---------------------------------------------------------------------------
# Real Windows check for ripgrep binary resolution. Downloads and extracts the
# actual archive with PowerShell stripped from PATH, then executes the binary.
# This is the condition that broke grep for 99 Windows machines; no amount of
# unit testing on Linux/macOS covers it.
# ---------------------------------------------------------------------------
windows-ripgrep-e2e:
name: Windows ripgrep E2E
needs: changes
if: needs.changes.outputs.typescript == 'true' || github.event_name == 'push'

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Run this job when its E2E script changes.

changes.outputs.typescript does not include script/windows-ripgrep-e2e.ts. A pull request that changes only this script skips the job. Add this script to the typescript filter, or add a dedicated output for it.

Proposed fix
             typescript:
+              - 'script/windows-ripgrep-e2e.ts'
               # altimate_change start — upstream_fix: typecheck every declared TypeScript workspace
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.github/workflows/ci.yml at line 392, Update the changes filter
configuration used by the TypeScript job condition to include
script/windows-ripgrep-e2e.ts in changes.outputs.typescript, ensuring the job
runs when that E2E script changes while preserving the existing push behavior.

runs-on: windows-latest
timeout-minutes: 15
steps:
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4

- uses: oven-sh/setup-bun@ecf28ddc73e819eb6fa29df6b34ef8921c743461 # v2
with:
bun-version: "1.3.14"

# --ignore-scripts: `tree-sitter-powershell` has no Windows prebuild and its fallback
# compile needs Visual Studio Build Tools, which this runner does not have (see
# anomalyco/opencode#25563). This check only needs the pure-JS dependency graph
# (effect, @zip.js/zip.js, which, xdg-basedir), so skipping lifecycle scripts is enough.
- name: Install dependencies
run: bun install --ignore-scripts

# Run from packages/core so `effect` and the other deps resolve — they are not root deps.
- name: Resolve ripgrep with PowerShell unavailable
working-directory: packages/core
run: bun run script/windows-ripgrep-e2e.ts

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Run the new core regression test in CI

On PRs that change packages/core/**, this Windows job installs the core deps but only runs script/windows-ripgrep-e2e.ts; I checked the workflows with rg and there is no invocation of packages/core/test/ripgrep-windows.test.ts or the core test suite, while the main TypeScript job runs bun test from packages/opencode. That leaves the new no-spawn/CRC/empty-archive regression coverage unexecuted in CI, so a broken in-process extractor can still merge as long as this E2E script passes; add the core regression test, or a focused core test pass, to this job.

Useful? React with 👍 / 👎.

# altimate_change end

# ---------------------------------------------------------------------------
# dbt-tools E2E — slow (~3 min), only on push to main.
# Tests dbt CLI fallbacks against real dbt versions (1.8, 1.10, 1.11) and
Expand Down
1 change: 1 addition & 0 deletions bun.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

18 changes: 18 additions & 0 deletions docs/docs/reference/telemetry.md
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,24 @@ export ALTIMATE_TELEMETRY_DISABLED=true

When telemetry is disabled, no events are sent and no network requests are made to the telemetry endpoint.

### Test runs are excluded

Test runners never reach the default telemetry endpoint. Telemetry is suppressed when `NODE_ENV=test`,
`BUN_TEST`, `VITEST`, or `JEST_WORKER_ID` is present. This exists because test processes regenerate
their machine ID on every run, so without the exclusion they dominate install and active-machine counts.

Running in CI is **not** excluded — that is ordinary product usage (for example
[altimate-code-actions](https://github.com/AltimateAI/altimate-code-actions) wraps this CLI), so
`CI` and `GITHUB_ACTIONS` on their own do not suppress anything.

Two escape hatches exist for reporting from a test run deliberately:

- Set `APPLICATIONINSIGHTS_CONNECTION_STRING` to your own endpoint — an explicitly-configured sink
is always honoured, which is how the project's own telemetry tests work.
- Set `ALTIMATE_TELEMETRY_FORCE=true` to use the default endpoint anyway.

`ALTIMATE_TELEMETRY_DISABLED` and the config opt-out take precedence over both.

## Privacy

We take your privacy seriously. Altimate Code telemetry **never** collects:
Expand Down
1 change: 1 addition & 0 deletions packages/core/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,7 @@
"@opentelemetry/sdk-trace-base": "2.6.1",
"@parcel/watcher": "2.5.1",
"@silvia-odwyer/photon-node": "0.3.4",
"@zip.js/zip.js": "2.7.62",
"@openrouter/ai-sdk-provider": "2.9.0",
"ai-gateway-provider": "3.1.2",
"bun-pty": "0.4.8",
Expand Down
150 changes: 150 additions & 0 deletions packages/core/script/windows-ripgrep-e2e.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,150 @@
/**
* Windows end-to-end check for ripgrep binary resolution.
*
* Reproduces as much as a GitHub runner allows of the condition behind the outage in
* https://github.com/AltimateAI/altimate-code/issues/1072: a cold cache on a Windows machine
* where PowerShell cannot be resolved from PATH. The old implementation extracted ripgrep's zip
* by shelling out to `powershell.exe -Command Expand-Archive`, so cross-spawn fell back to
* `cmd.exe /d /s /c` and the extraction died with "is not recognized as an internal or external
* command" — which `Effect.cached` then replayed for the rest of the session.
*
* This performs a real download and a real extraction, then executes the resulting binary. It is
* deliberately not a unit test: the point is to exercise the actual filesystem, the actual archive
* and the actual process launch on a real Windows host.
*
* Scope note: stripping PATH does NOT make PowerShell unspawnable on Windows (see the control
* probe at the end), so this does not reproduce the affected machines. The guarantee that
* extraction spawns nothing at all is established by test/ripgrep-windows.test.ts.
*
* Run: bun run script/windows-ripgrep-e2e.ts (from packages/core — `effect` resolves there)
*/
import { execFileSync } from "node:child_process"
import { mkdtempSync, existsSync, readdirSync, statSync, rmSync } from "node:fs"
import { tmpdir } from "node:os"
import path from "node:path"

function fail(message: string): never {
console.error(`FAIL: ${message}`)
process.exit(1)
}
Comment on lines +26 to +29

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win

Clean up the temporary cache on failure.

fail() calls process.exit(1), so it bypasses line 110. A thrown execFileSync error also bypasses line 110. Wrap the test body in try/finally, and remove the directory in finally. Avoid direct process.exit() before cleanup runs.

Also applies to: 110-110

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@script/windows-ripgrep-e2e.ts` around lines 22 - 25, Ensure the test body and
any failing execFileSync calls in the Windows ripgrep E2E flow are wrapped in
try/finally so the temporary cache directory is always removed. Move the cleanup
currently at the post-test path into finally, and update fail() to report
failure without directly calling process.exit before cleanup executes.

Source: Coding guidelines


function ok(message: string) {
console.log(`ok ${message}`)
}

if (process.platform !== "win32") fail(`this check only means anything on Windows (got ${process.platform})`)

// Cold cache: point the XDG cache root at a throwaway directory *before* importing anything that
// reads it, since Global computes its paths at module load.
const cacheRoot = mkdtempSync(path.join(tmpdir(), "rg-e2e-"))
process.env.XDG_CACHE_HOME = cacheRoot
process.env.LOCALAPPDATA = cacheRoot

// Remove every PATH entry that could provide PowerShell. System32 itself is kept so cmd.exe and
// the rest of Windows still work — this simulates the locked-down/sanitized-PATH machines in the
// telemetry, not a broken OS.
const originalPath = process.env.PATH ?? process.env.Path ?? ""
const stripped = originalPath
.split(path.delimiter)
.filter((entry) => entry && !/powershell/i.test(entry))
.join(path.delimiter)
process.env.PATH = stripped
process.env.Path = stripped

const { which } = await import("../src/util/which")

// Guard against a vacuous run: if PowerShell is still resolvable, this proves nothing.
const ps = which("powershell.exe")
const pwsh = which("pwsh.exe")
if (ps || pwsh) fail(`PowerShell is still resolvable (${ps ?? pwsh}) — the scenario was not reproduced`)
ok("PowerShell is not resolvable on PATH (failure condition reproduced)")

// Also assert the binary really is absent, so we exercise download + extract rather than a cache hit.
const { Global } = await import("../src/global")
const target = path.join(Global.Path.bin, "rg.exe")
if (existsSync(target)) fail(`expected a cold cache but ${target} already exists`)
ok(`cold cache at ${Global.Path.bin}`)

const { Effect } = await import("effect")
const { RipgrepBinary } = await import("../src/ripgrep/binary")

/** Resolve through the real layer: real HTTP, real filesystem, real process launch. */
async function resolveBinary(): Promise<string> {
const program = Effect.gen(function* () {
const binary = yield* RipgrepBinary.Service
return yield* binary.filepath
}).pipe(Effect.provide(RipgrepBinary.defaultLayer))
return (await Effect.runPromise(program as never)) as string
}

let resolved: string
try {
resolved = await resolveBinary()
} catch (err: unknown) {
fail(`binary.filepath failed: ${err instanceof Error ? err.message : String(err)}`)
}

ok(`resolved ${resolved}`)

if (!existsSync(resolved)) fail(`resolved path does not exist: ${resolved}`)
const size = statSync(resolved).size
if (size < 100_000) fail(`resolved binary is implausibly small (${size} bytes) — likely a partial write`)
ok(`binary present, ${size} bytes`)

// No staging files should survive a successful install.
const leftovers = readdirSync(Global.Path.bin).filter((f) => f.endsWith(".tmp"))
if (leftovers.length > 0) fail(`staging files left behind: ${leftovers.join(", ")}`)
ok("no staging files left behind")

// The real proof: the extracted binary actually executes.
const version = execFileSync(resolved, ["--version"], { encoding: "utf8" })
if (!/ripgrep\s+\d/.test(version)) fail(`unexpected --version output: ${version.trim()}`)
ok(`executes: ${version.split("\n")[0]!.trim()}`)

// And it can actually search.
const hit = execFileSync(resolved, ["--no-config", "NEEDLE_MARKER", "--", import.meta.filename], {
encoding: "utf8",
})
if (!hit.includes("NEEDLE_MARKER")) fail("ripgrep did not return the expected match")
ok("search returns matches") // NEEDLE_MARKER

// A second resolve must hit the cache and stay valid.
const again = await resolveBinary()
if (again !== resolved) fail(`second resolve returned a different path: ${again}`)
ok("second resolve hits the cache")

// ---------------------------------------------------------------------------
// Control probe — informational, deliberately not a hard failure.
//
// It would be neater to also show that the OLD implementation fails here, making this a true
// counterfactual. It does not, and that is worth recording: emptying PATH is not enough to make
// PowerShell unspawnable on Windows. cross-spawn falls back to `cmd.exe /d /s /c`, and Windows
// process creation searches beyond PATH (the caller's directory, the system directories, and the
// App Paths registry key), so `powershell.exe` still starts on a stock GitHub runner even though
// `which()` cannot see it.
//
// So this job does NOT reproduce the affected machines. What it does prove is the part that
// matters: on real Windows the new path downloads, extracts in-process, installs and produces a
// working rg.exe. That PowerShell's availability is irrelevant to it is established separately and
// structurally by `test/ripgrep-windows.test.ts`, which drives the same code with a spawner that
// fails the test if anything is launched at all.
// ---------------------------------------------------------------------------
const launch = (await import("cross-spawn")).default
const control = launch.sync("powershell.exe", ["-NoProfile", "-NonInteractive", "-Command", "exit 0"], {
encoding: "utf8",
})

if (control.status === 0) {
console.log(
"note this runner still spawns powershell.exe despite the stripped PATH (Windows resolves\n" +
" executables beyond PATH), so the affected environment is not reproduced here — the\n" +
" no-spawn guarantee comes from the unit layer test, not from this job",
)
} else {
const output = `${control.stderr ?? ""}${control.stdout ?? ""}${control.error?.message ?? ""}`.trim()
ok(`bonus: the old PowerShell path also fails here (${output.split("\n")[0]?.slice(0, 100) || `status=${control.status}`})`)
}

rmSync(cacheRoot, { recursive: true, force: true })
console.log("\nPASS — on real Windows, ripgrep downloads, extracts in-process, installs atomically")
console.log(" and runs. See the note above for what this job does and does not establish.")
6 changes: 5 additions & 1 deletion packages/core/src/ripgrep.ts
Original file line number Diff line number Diff line change
Expand Up @@ -140,7 +140,11 @@ export const layer = Layer.effect(
return yield* new InvalidPatternError({ pattern: input.pattern, message: stderr.trim() })
}
if (code !== 0 && code !== 1 && code !== 2) {
return yield* failure(stderr.trim() || `ripgrep failed with code ${code}`)
// altimate_change start — upstream_fix: keep child stderr attributable to ripgrep.
// Reporting stderr verbatim made shell-level failures (e.g. a Windows "not recognized"
// message) look like they came from the tool itself, with no hint of the real source.
return yield* failure(`ripgrep failed with code ${code}: ${stderr.trim() || "no output"}`)
// altimate_change end
Comment thread
coderabbitai[bot] marked this conversation as resolved.
}
return { items: code === 1 ? [] : rows, truncated: false, partial: code === 2 }
}),
Expand Down
Loading
Loading