diff --git a/CHANGELOG.md b/CHANGELOG.md index 64ab72b13..cdaf97b07 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,10 +12,16 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - **The `github/review` composite action can be downloaded by GitHub Actions again.** The release tree contained three VS Code image symlinks whose removed targets caused GitHub's action downloader to reject the entire archive before the review step started. The images are now self-contained files and a release-critical test prevents dangling links from returning. - **A valid dbt manifest is no longer mislabeled as a lint-only run.** Manifest availability is now checked independently from changed-model lookup, so new models and other valid manifests receive the correct full-run status. +- **The release-version lookup is rate-limit resilient and never caches a floating `latest`.** The composite action now authenticates its GitHub release-API call with the workflow token (lifting the 60→1,000 req/hr unauthenticated limit) and skips the binary cache entirely when the version resolves to `latest`, so a single rate-limited or offline lookup can no longer pin a stale binary across all subsequent runs. + ### Added - **Direct GitHub onboarding and a live dbt review demo.** The GitHub App installer now opens GitHub's repository-selection screen directly, and the README/docs link to the public `dbt-pr-review-demo` pull requests. +### Security + +- **The hosted Altimate API key is no longer placed on the `jq` process arg list.** The credential write reads the key from the environment inside the `jq` program, keeping it out of `argv` (which is visible to other processes and printed verbatim when `ACTIONS_STEP_DEBUG` enables `set -x`). + ## [0.8.4] - 2026-06-05 A trace-durability patch. Open `/traces` mid-session and you'd see a rich waterfall — then the moment the agent finished its turn the view collapsed to a single "system-prompt" span, the Summary tab's *"What was asked"* showed *"No prompt recorded"*, and the Chat tab dropped every user turn but the last. The data was genuinely gone from disk, not just hidden in the viewer. This release stops the on-disk trace from being overwritten after each turn and makes the file authoritative across worker restarts. A five-persona pre-release review drove a follow-up wording fix so a reconstructed trace isn't misread as a failed run. diff --git a/github/review/action.yml b/github/review/action.yml index bb0b9e4db..48c369b4e 100644 --- a/github/review/action.yml +++ b/github/review/action.yml @@ -59,13 +59,22 @@ runs: shell: bash env: ACTION_REF: ${{ github.action_ref }} + # Authenticated API calls lift the unauthenticated 60 req/hr IP limit to + # 1,000 req/hr, so a busy org's runners don't get rate-limited into the + # "latest" fallback below. + GITHUB_TOKEN: ${{ github.token }} run: | set -euo pipefail if [[ "${ACTION_REF:-}" =~ ^v?([0-9]+\.[0-9]+\.[0-9]+)$ ]]; then VERSION="${BASH_REMATCH[1]}" else + # Only attach the auth header when a token is present; the + # ${AUTH[@]+"${AUTH[@]}"} idiom expands to nothing on an empty array + # even under `set -u` (portable back to bash 3.2). + AUTH=() + [[ -n "${GITHUB_TOKEN:-}" ]] && AUTH=(-H "Authorization: Bearer ${GITHUB_TOKEN}") VERSION=$( - curl -sf --connect-timeout 5 --max-time 15 \ + curl -sf --connect-timeout 5 --max-time 15 ${AUTH[@]+"${AUTH[@]}"} \ https://api.github.com/repos/AltimateAI/altimate-code/releases/latest \ | grep -o '"tag_name": *"[^"]*"' | cut -d'"' -f4 || true ) @@ -75,6 +84,11 @@ runs: - name: Cache altimate-code id: cache + # Never cache against the floating "latest" key: a single rate-limited or + # offline lookup would otherwise pin whatever binary it grabbed and reuse + # it forever, silently ignoring later releases. A resolved semver caches + # normally; "latest" falls through to a fresh install each run. + if: steps.version.outputs.version != 'latest' uses: actions/cache@v4 with: path: ~/.altimate/bin @@ -120,11 +134,13 @@ runs: fi umask 077 mkdir -p "$HOME/.altimate" + # Read the API key from the environment inside the jq program rather + # than passing it via --arg: argv is visible to other processes and + # is printed verbatim if a user enables ACTIONS_STEP_DEBUG (set -x). jq -nc \ --arg url "${IN_ALT_URL:-https://api.myaltimate.com}" \ --arg inst "$IN_ALT_INSTANCE" \ - --arg key "$IN_ALT_KEY" \ - '{altimateUrl:$url, altimateInstanceName:$inst, altimateApiKey:$key}' \ + '{altimateUrl:$url, altimateInstanceName:$inst, altimateApiKey:$ENV.IN_ALT_KEY}' \ > "$HOME/.altimate/altimate.json" echo "Advisory lane: hosted altimate model (tenant: $IN_ALT_INSTANCE)." elif [[ -n "${IN_MODEL:-}" ]]; then diff --git a/packages/opencode/test/skill/release-v0.8.5-adversarial.test.ts b/packages/opencode/test/skill/release-v0.8.5-adversarial.test.ts index b4b72eddc..08d361834 100644 --- a/packages/opencode/test/skill/release-v0.8.5-adversarial.test.ts +++ b/packages/opencode/test/skill/release-v0.8.5-adversarial.test.ts @@ -61,10 +61,7 @@ describe("v0.8.5 adversarial - composite action", () => { const output = path.join(tmp.path, "github-output") const curlMarker = path.join(tmp.path, "curl-called") await fs.mkdir(bin) - await Bun.write( - path.join(bin, "curl"), - `#!/usr/bin/env bash\ntouch "$CURL_MARKER"\nexit 99\n`, - ) + await Bun.write(path.join(bin, "curl"), `#!/usr/bin/env bash\ntouch "$CURL_MARKER"\nexit 99\n`) await fs.chmod(path.join(bin, "curl"), 0o755) const result = await runBash(await actionScript("Get altimate-code version"), { @@ -76,7 +73,12 @@ describe("v0.8.5 adversarial - composite action", () => { expect(result.exitCode).toBe(0) expect(await fs.readFile(output, "utf8")).toBe("version=0.8.5\n") - expect(await fs.stat(curlMarker).then(() => true).catch(() => false)).toBe(false) + expect( + await fs + .stat(curlMarker) + .then(() => true) + .catch(() => false), + ).toBe(false) }) test("the non-semver action ref fallback bounds the release lookup", async () => { @@ -113,10 +115,7 @@ describe("v0.8.5 adversarial - composite action", () => { const capture = path.join(tmp.path, "args") const sentinel = path.join(tmp.path, "injected") await fs.mkdir(bin) - await Bun.write( - path.join(bin, "altimate"), - `#!/usr/bin/env bash\nprintf '%s\\0' "$@" > "$CAPTURE"\n`, - ) + await Bun.write(path.join(bin, "altimate"), `#!/usr/bin/env bash\nprintf '%s\\0' "$@" > "$CAPTURE"\n`) await fs.chmod(path.join(bin, "altimate"), 0o755) const manifest = `target/manifest $(touch ${sentinel}) .json` @@ -138,7 +137,12 @@ describe("v0.8.5 adversarial - composite action", () => { }) expect(result.exitCode).toBe(0) - expect(await fs.stat(sentinel).then(() => true).catch(() => false)).toBe(false) + expect( + await fs + .stat(sentinel) + .then(() => true) + .catch(() => false), + ).toBe(false) const args = (await fs.readFile(capture, "utf8")).split("\0").filter(Boolean) expect(args).toEqual([ "review", @@ -197,9 +201,12 @@ describe("v0.8.5 adversarial - composite action", () => { expect(result.exitCode).toBe(1) expect(result.stdout + result.stderr).not.toContain(secret) expect(result.stdout + result.stderr).toContain("altimate_instance is empty") - expect(await fs.stat(path.join(tmp.path, ".altimate/altimate.json")).then(() => true).catch(() => false)).toBe( - false, - ) + expect( + await fs + .stat(path.join(tmp.path, ".altimate/altimate.json")) + .then(() => true) + .catch(() => false), + ).toBe(false) }) test("the committed archive contains regular, non-empty action dependencies", async () => { @@ -225,15 +232,102 @@ describe("v0.8.5 adversarial - composite action", () => { const version = changelog.match(/^## \[(\d+\.\d+\.\d+)\] - Unreleased$/m)?.[1] expect(version).toBe("0.8.5") - for (const relative of [ - "docs/docs/usage/dbt-pr-review.md", - "github/review/examples/altimate-ingestion.yml", - ]) { + for (const relative of ["docs/docs/usage/dbt-pr-review.md", "github/review/examples/altimate-ingestion.yml"]) { const content = await fs.readFile(path.join(repoRoot, relative), "utf8") expect(content).toContain(`AltimateAI/altimate-code/github/review@v${version}`) expect(content).not.toContain("AltimateAI/altimate-code/github/review@v0.8.4") } }) + + test("the release lookup authenticates the api when a github token is present", async () => { + await using tmp = await tmpdir() + const bin = path.join(tmp.path, "bin") + const output = path.join(tmp.path, "github-output") + const argsPath = path.join(tmp.path, "curl-args") + await fs.mkdir(bin) + await Bun.write( + path.join(bin, "curl"), + `#!/usr/bin/env bash\nprintf '%s\\0' "$@" > "$CURL_ARGS"\nprintf '{"tag_name":"v0.8.5"}\\n'\n`, + ) + await fs.chmod(path.join(bin, "curl"), 0o755) + + const result = await runBash(await actionScript("Get altimate-code version"), { + ACTION_REF: "main", + GITHUB_TOKEN: "ghs-test-token", + CURL_ARGS: argsPath, + GITHUB_OUTPUT: output, + PATH: `${bin}:${process.env.PATH}`, + }) + + expect(result.exitCode, result.stderr).toBe(0) + const curlArgs = (await fs.readFile(argsPath, "utf8")).split("\0").filter(Boolean) + expect(curlArgs).toContain("-H") + expect(curlArgs).toContain("Authorization: Bearer ghs-test-token") + }) + + test("the release lookup omits auth and still succeeds when no token is present", async () => { + await using tmp = await tmpdir() + const bin = path.join(tmp.path, "bin") + const output = path.join(tmp.path, "github-output") + const argsPath = path.join(tmp.path, "curl-args") + await fs.mkdir(bin) + await Bun.write( + path.join(bin, "curl"), + `#!/usr/bin/env bash\nprintf '%s\\0' "$@" > "$CURL_ARGS"\nprintf '{"tag_name":"v0.8.5"}\\n'\n`, + ) + await fs.chmod(path.join(bin, "curl"), 0o755) + + // An empty token must not crash under `set -u` (the ${AUTH[@]+...} idiom) + // and must not attach an Authorization header. + const result = await runBash(await actionScript("Get altimate-code version"), { + ACTION_REF: "main", + GITHUB_TOKEN: "", + CURL_ARGS: argsPath, + GITHUB_OUTPUT: output, + PATH: `${bin}:${process.env.PATH}`, + }) + + expect(result.exitCode, result.stderr).toBe(0) + expect(await fs.readFile(output, "utf8")).toBe("version=0.8.5\n") + const curlArgs = (await fs.readFile(argsPath, "utf8")).split("\0").filter(Boolean) + expect(curlArgs).not.toContain("-H") + expect(curlArgs.some((arg) => arg.startsWith("Authorization:"))).toBe(false) + }) + + test("the cache step is skipped for the floating latest version", async () => { + const action = YAML.parse(await fs.readFile(actionPath, "utf8")) + const cache = action.runs.steps.find((step: { name?: string }) => step.name === "Cache altimate-code") + expect(cache?.if).toBe("steps.version.outputs.version != 'latest'") + }) + + test("the hosted credential write keeps the api key out of the jq argv", async () => { + await using tmp = await tmpdir() + const bin = path.join(tmp.path, "bin") + const argsPath = path.join(tmp.path, "jq-args") + const secret = "argv-secret-must-not-appear" + await fs.mkdir(bin) + // Fake jq captures its argv (and writes nothing), so we can assert the + // secret is delivered via the environment, not the process arg list. + await Bun.write(path.join(bin, "jq"), `#!/usr/bin/env bash\nprintf '%s\\0' "$@" > "$JQ_ARGS"\n`) + await fs.chmod(path.join(bin, "jq"), 0o755) + + const result = await runBash(await actionScript("Configure advisory reviewer model + credentials"), { + HOME: tmp.path, + GITHUB_ENV: path.join(tmp.path, "github-env"), + JQ_ARGS: argsPath, + IN_ALT_KEY: secret, + IN_ALT_INSTANCE: "demo", + IN_ALT_URL: "https://api.example.test", + IN_MODEL: "", + IN_MODEL_API_KEY: "", + PATH: `${bin}:${process.env.PATH}`, + }) + + expect(result.exitCode, result.stderr).toBe(0) + const jqArgs = (await fs.readFile(argsPath, "utf8")).split("\0").filter(Boolean) + expect(jqArgs.some((arg) => arg.includes(secret))).toBe(false) + expect(jqArgs).toContain("{altimateUrl:$url, altimateInstanceName:$inst, altimateApiKey:$ENV.IN_ALT_KEY}") + }) }) describe("v0.8.5 end-to-end - real review pipeline", () => {