diff --git a/CHANGELOG.md b/CHANGELOG.md index 9652e71fd..494d528d9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,19 @@ # Changelog +## [Unreleased] + +**/cso doctor no longer calls a Go repository "stack: python".** A repository with a root `go.mod` and no Node, Bun, Rails or JVM build matched no supported stack, so preparation fell back to Python and doctor reported `stack: python` with `MISSING_METADATA requirements.txt`, which sent Go users looking for a Python file they never needed (#3263). Go runtime support itself is not planned for now (Garry, 2026-10-24), so /cso keeps running static assessment on Go repositories; the runtime catalog and the /cso release contract are unchanged. + +### Fixed +- **`lib/cso/preparation.ts`**: a root `go.mod` with no other supported stack is reported as `stack: go` with `UNSUPPORTED_STACK` ("Go (go.mod) is not a supported runtime stack yet; supported runtime stacks are Node, Bun, Python, and Rails."), the way a Maven or Gradle build is already reported as Java. Python helper metadata beside `go.mod` (a docs `requirements.txt`, say) no longer turns the repository into Python. A `go.mod` beside a Node, Bun or Rails lockfile leaves that stack's detection as it was. + +### Tests +- `test/cso-preparation.test.ts` checks that a Go module is `go` with exactly that prerequisite and never mentions `requirements.txt`, and that a hashed `requirements.txt` beside `go.mod` stays Go. Both fail on main. Controls: `go.mod` beside a Node lockfile stays a ready Node plan, a Python project without `go.mod` stays Python, and a repository with no stack metadata keeps the Python fallback. +- `test/helpers/cso-runtime-catalog.ts` leaves `go` out of its qualified-runtime fixture list, as it already did `java`. + +### Gate integrity +- No gate edit. No threshold, budget, timeout, trial count or case kind changed. + ## [1.91.186.0] - 2026-10-24 **Approving an autoplan run no longer fails one time in ten because its run id looks like a card number.** Today's daily flaky-fix pass found `test/capy-captain-flow.test.ts` failing once and passing on the serial retry in 7 of the 57 Free Tests runs from 2026-10-21 to 2026-10-23, one of them a push to main (39878113535). Every failure had the same assertion: `gstack-approvals import` refused the approval row with "field ask: pii.cc". The default autoplan run id is `run-YYYYMMDDHHMMSS`, the import writes it into the row's ask and source, and the credit-card rule saw those 14 digits as a card number whenever they passed the Luhn check, which a timestamp does about one time in ten. A real user's approval import failed the same way. diff --git a/lib/cso/preparation.ts b/lib/cso/preparation.ts index 26a68bc73..35612e9ee 100644 --- a/lib/cso/preparation.ts +++ b/lib/cso/preparation.ts @@ -5,8 +5,8 @@ import { createHash } from 'node:crypto'; import { lstatSync, readFileSync, realpathSync } from 'node:fs'; import { isAbsolute, relative, resolve, sep } from 'node:path'; -/** 'java' (Maven or Gradle) is detected so it is reported as itself; no runtime prepares it. */ -export type CsoStack = 'node' | 'bun' | 'python' | 'rails' | 'java'; +/** 'java' (Maven or Gradle) and 'go' (go.mod) are detected so they are reported as themselves; no runtime prepares them. */ +export type CsoStack = 'node' | 'bun' | 'python' | 'rails' | 'java' | 'go'; export interface PreparationPrerequisite { code: string; message: string; @@ -1205,12 +1205,15 @@ export function inspectPreparation(snapshotPath: string, stack?: CsoStack): Prep detectedStacks.push('node'); if (read(root, 'Gemfile.lock', true) !== undefined || read(root, 'Gemfile', true) !== undefined) detectedStacks.push('rails'); - // A root Maven or Gradle build names the repository's language; Python - // metadata beside it is helper tooling, not a second application stack. + // A root Maven or Gradle build, or a root go.mod with no other supported + // stack, names the repository's language; Python metadata beside it is + // helper tooling, not a second application stack. const jvmBuild = ['pom.xml', 'build.gradle', 'build.gradle.kts'].find( (file) => read(root, file, true) !== undefined, ); if (jvmBuild) detectedStacks.push('java'); + else if (detectedStacks.length === 0 && read(root, 'go.mod', true) !== undefined) + detectedStacks.push('go'); else if ( read(root, 'uv.lock', true) !== undefined || read(root, 'requirements.txt', true) !== undefined || @@ -1231,10 +1234,16 @@ export function inspectPreparation(snapshotPath: string, stack?: CsoStack): Prep `Java (${jvmBuild ?? 'Maven or Gradle'}) is not a supported runtime stack yet; supported runtime stacks are Node, Bun, Python, and Rails.`, jvmBuild, ); + if (detected === 'go') + fail( + 'UNSUPPORTED_STACK', + 'Go (go.mod) is not a supported runtime stack yet; supported runtime stacks are Node, Bun, Python, and Rails.', + 'go.mod', + ); if (!['node', 'bun', 'python', 'rails'].includes(detected)) fail('UNSUPPORTED_STACK', 'Supported runtime stacks are Node, Bun, Python, and Rails.'); ({ node: inspectNode, bun: inspectBun, python: inspectPython, rails: inspectRails })[ - detected as Exclude + detected as Exclude ](root, plan); } catch (error) { plan.status = 'prerequisites'; diff --git a/test/cso-preparation.test.ts b/test/cso-preparation.test.ts index 6addab47f..174ebd8b3 100644 --- a/test/cso-preparation.test.ts +++ b/test/cso-preparation.test.ts @@ -170,6 +170,32 @@ describe('CSO inert Python preparation', () => { }); }); +describe('CSO preparation names a Go module as Go (#3263)', () => { + const goFiles = { + 'go.mod': 'module example.com/app\n\ngo 1.23\n\nrequire github.com/google/uuid v1.6.0\n', + 'go.sum': 'github.com/google/uuid v1.6.0 h1:abc=\n', + 'main.go': 'package main\n\nfunc main() {}\n', + }; + const GO_UNSUPPORTED = { code: 'UNSUPPORTED_STACK', message: 'Go (go.mod) is not a supported runtime stack yet; supported runtime stacks are Node, Bun, Python, and Rails.', path: 'go.mod' }; + test('a go.mod repository is Go, unsupported, and never asks for requirements.txt', () => { + const plan = inspectPreparation(fixture(goFiles)); + expect([plan.stack, plan.runtimeProfile, plan.status]).toEqual(['go', 'go', 'prerequisites']); + expect(plan.prerequisites).toEqual([GO_UNSUPPORTED]); + expect(JSON.stringify(plan)).not.toContain('requirements.txt'); + }); + test('Python helper metadata beside go.mod stays Go', () => { + const plan = inspectPreparation(fixture({ ...goFiles, 'requirements.txt': `mkdocs==1.6.1 --hash=sha256:${hash}\n` })); + expect(plan.stack).toBe('go'); + expect(plan.prerequisites).toEqual([GO_UNSUPPORTED]); + }); + test('controls: go.mod beside a Node lockfile stays Node; Python without go.mod stays Python', () => { + const node = inspectPreparation(fixture({ ...goFiles, ...nodeFiles() })); + expect([node.stack, node.status]).toEqual(['node', 'ready']); + expect(inspectPreparation(fixture({ 'requirements.txt': `Flask==3.1.0 --hash=sha256:${hash}\n` })).stack).toBe('python'); + expect(inspectPreparation(fixture({ 'go.sum': '', 'main.go': '' })).stack).toBe('python'); + }); +}); + describe('CSO inert Rails preparation', () => { test('does not silently choose Node in a multi-stack repository',()=>{const plan=inspectPreparation(fixture({...nodeFiles(),'Gemfile.lock':gemLock,'Gemfile':''}));expect(plan.status).toBe('prerequisites');expect(plan.prerequisites[0].code).toBe('MULTIPLE_STACKS');expect(inspectPreparation(fixture({...nodeFiles(),'Gemfile.lock':gemLock,'Gemfile':''}),'rails').stack).toBe('rails');}); test('fetches exact gems without evaluating Gemfiles or building native extensions', () => { diff --git a/test/helpers/cso-runtime-catalog.ts b/test/helpers/cso-runtime-catalog.ts index 1e55877d1..10c5a414d 100644 --- a/test/helpers/cso-runtime-catalog.ts +++ b/test/helpers/cso-runtime-catalog.ts @@ -1,6 +1,6 @@ import type { CsoStack } from '../../lib/cso/preparation'; -type RuntimeStack = Exclude; +type RuntimeStack = Exclude; import { canonical, sha256 } from '../../lib/cso/contracts'; import { ISOLATION_POLICY_HASH } from '../../lib/cso/docker'; import {