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
9 changes: 9 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,14 @@
# Changelog

## 2.0.4 — 2026-07-27

### Fixed

- Generated Node checks now cover JSX, MTS, CTS, and root TypeScript
configuration files.
- Git-cancellation integration tests now allow the complete documented
termination and cleanup window.

## 2.0.3 — 2026-07-27

### Fixed
Expand Down
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,12 +16,12 @@ release.
IntentCI v2 supports Apple Silicon Macs.
Source builds on other platforms are incidental and unsupported.

Download `intentci_2.0.3_darwin_arm64.tar.gz` from
Download `intentci_2.0.4_darwin_arm64.tar.gz` from
[GitHub Releases](https://github.com/hypertrial/intentci/releases), or install
from source with Go 1.23 or newer:

```bash
go install github.com/hypertrial/intentci/v2/cmd/intentci@v2.0.3
go install github.com/hypertrial/intentci/v2/cmd/intentci@v2.0.4
```

## Start
Expand Down
9 changes: 9 additions & 0 deletions docs/releases/v2.0.4.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
# IntentCI v2.0.4

IntentCI v2.0.4 improves the generated configuration for Node repositories.
New checks created by `intentci init` now cover JSX, MTS, CTS, and root
TypeScript configuration files in addition to the existing Node paths.

Existing `.intentci.yaml` files are unchanged. Version 2 remains intentionally
MacBook-first and incompatible with the evidence-oriented v1 line. See the
[migration guide](https://github.com/hypertrial/intentci/blob/v2.0.4/docs/migration-v1-to-v2.md).
3 changes: 2 additions & 1 deletion internal/app/app.go
Original file line number Diff line number Diff line change
Expand Up @@ -260,7 +260,8 @@ func detect(root string) config.Check {
return config.Check{
ID: "node-tests", Intent: "Node changes must keep tests passing.",
Paths: []string{
"**/*.js", "**/*.mjs", "**/*.cjs", "**/*.ts", "**/*.tsx",
"**/*.js", "**/*.jsx", "**/*.mjs", "**/*.cjs",
"**/*.ts", "**/*.tsx", "**/*.mts", "**/*.cts", "tsconfig*.json",
"package.json", "package-lock.json", "pnpm-lock.yaml", "yarn.lock",
},
Run: run,
Expand Down
135 changes: 101 additions & 34 deletions internal/app/app_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -81,69 +81,77 @@ func TestSelectChecks(t *testing.T) {
}
}

func detectionCases() []struct {
name string
files []string
want config.Check
} {
return []struct {
name string
files []string
want config.Check
}{
{"go", []string{"go.mod"}, config.Check{
type detectionCase struct {
name string
files []string
matches []string
want config.Check
}

func detectionCases() []detectionCase {
return []detectionCase{
{"go", []string{"go.mod"}, []string{"src/main.go", "go.mod", "go.sum"}, config.Check{
ID: "go-tests", Intent: "Go changes must keep tests passing.",
Paths: []string{"**/*.go", "go.mod", "go.sum"}, Run: "go test ./...",
}},
{"pnpm", []string{"package.json", "pnpm-lock.yaml"}, config.Check{
{"pnpm", []string{"package.json", "pnpm-lock.yaml"}, nodeMatches(), config.Check{
ID: "node-tests", Intent: "Node changes must keep tests passing.",
Paths: []string{
"**/*.js", "**/*.mjs", "**/*.cjs", "**/*.ts", "**/*.tsx",
"**/*.js", "**/*.jsx", "**/*.mjs", "**/*.cjs",
"**/*.ts", "**/*.tsx", "**/*.mts", "**/*.cts", "tsconfig*.json",
"package.json", "package-lock.json", "pnpm-lock.yaml", "yarn.lock",
},
Run: "pnpm test",
}},
{"yarn", []string{"package.json", "yarn.lock"}, config.Check{
{"yarn", []string{"package.json", "yarn.lock"}, nodeMatches(), config.Check{
ID: "node-tests", Intent: "Node changes must keep tests passing.",
Paths: []string{
"**/*.js", "**/*.mjs", "**/*.cjs", "**/*.ts", "**/*.tsx",
"**/*.js", "**/*.jsx", "**/*.mjs", "**/*.cjs",
"**/*.ts", "**/*.tsx", "**/*.mts", "**/*.cts", "tsconfig*.json",
"package.json", "package-lock.json", "pnpm-lock.yaml", "yarn.lock",
},
Run: "yarn test",
}},
{"npm", []string{"package.json"}, config.Check{
{"npm", []string{"package.json"}, nodeMatches(), config.Check{
ID: "node-tests", Intent: "Node changes must keep tests passing.",
Paths: []string{
"**/*.js", "**/*.mjs", "**/*.cjs", "**/*.ts", "**/*.tsx",
"**/*.js", "**/*.jsx", "**/*.mjs", "**/*.cjs",
"**/*.ts", "**/*.tsx", "**/*.mts", "**/*.cts", "tsconfig*.json",
"package.json", "package-lock.json", "pnpm-lock.yaml", "yarn.lock",
},
Run: "npm test",
}},
{"uv", []string{"pyproject.toml", "uv.lock"}, config.Check{
ID: "python-tests", Intent: "Python changes must keep tests passing.",
Paths: []string{"**/*.py", "pyproject.toml", "requirements*.txt", "uv.lock"},
Run: "uv run pytest -q",
}},
{"python", []string{"pyproject.toml"}, config.Check{
ID: "python-tests", Intent: "Python changes must keep tests passing.",
Paths: []string{"**/*.py", "pyproject.toml", "requirements*.txt", "uv.lock"},
Run: "python3 -m pytest -q",
}},
{"rust", []string{"Cargo.toml"}, config.Check{
{"uv", []string{"pyproject.toml", "uv.lock"},
[]string{"src/app.py", "pyproject.toml", "requirements-dev.txt", "uv.lock"}, config.Check{
ID: "python-tests", Intent: "Python changes must keep tests passing.",
Paths: []string{"**/*.py", "pyproject.toml", "requirements*.txt", "uv.lock"},
Run: "uv run pytest -q",
}},
{"python", []string{"pyproject.toml"},
[]string{"src/app.py", "pyproject.toml", "requirements-dev.txt", "uv.lock"}, config.Check{
ID: "python-tests", Intent: "Python changes must keep tests passing.",
Paths: []string{"**/*.py", "pyproject.toml", "requirements*.txt", "uv.lock"},
Run: "python3 -m pytest -q",
}},
{"rust", []string{"Cargo.toml"}, []string{"src/lib.rs", "Cargo.toml", "Cargo.lock"}, config.Check{
ID: "rust-tests", Intent: "Rust changes must keep tests passing.",
Paths: []string{"**/*.rs", "Cargo.toml", "Cargo.lock"}, Run: "cargo test",
}},
{"maven wrapper", []string{"pom.xml", "mvnw"}, config.Check{
{"maven wrapper", []string{"pom.xml", "mvnw"}, []string{
"src/main/java/App.java", "pom.xml", ".mvn/wrapper/maven-wrapper.properties", "mvnw", "mvnw.cmd",
}, config.Check{
ID: "java-tests", Intent: "Java changes must keep tests passing.",
Paths: []string{"**/*.java", "pom.xml", ".mvn/**", "mvnw", "mvnw.cmd"},
Run: "./mvnw test",
}},
{"maven", []string{"pom.xml"}, config.Check{
{"maven", []string{"pom.xml"}, []string{
"src/main/java/App.java", "pom.xml", ".mvn/wrapper/maven-wrapper.properties", "mvnw", "mvnw.cmd",
}, config.Check{
ID: "java-tests", Intent: "Java changes must keep tests passing.",
Paths: []string{"**/*.java", "pom.xml", ".mvn/**", "mvnw", "mvnw.cmd"},
Run: "mvn test",
}},
{"gradle wrapper", []string{"build.gradle", "gradlew"}, config.Check{
{"gradle wrapper", []string{"build.gradle", "gradlew"}, gradleMatches(), config.Check{
ID: "java-tests", Intent: "Java changes must keep tests passing.",
Paths: []string{
"**/*.java", "build.gradle", "build.gradle.kts",
Expand All @@ -152,7 +160,7 @@ func detectionCases() []struct {
},
Run: "./gradlew test",
}},
{"gradle", []string{"build.gradle.kts"}, config.Check{
{"gradle", []string{"build.gradle.kts"}, gradleMatches(), config.Check{
ID: "java-tests", Intent: "Java changes must keep tests passing.",
Paths: []string{
"**/*.java", "build.gradle", "build.gradle.kts",
Expand All @@ -161,7 +169,7 @@ func detectionCases() []struct {
},
Run: "gradle test",
}},
{"gradle settings", []string{"settings.gradle"}, config.Check{
{"gradle settings", []string{"settings.gradle"}, gradleMatches(), config.Check{
ID: "java-tests", Intent: "Java changes must keep tests passing.",
Paths: []string{
"**/*.java", "build.gradle", "build.gradle.kts",
Expand All @@ -170,14 +178,31 @@ func detectionCases() []struct {
},
Run: "gradle test",
}},
{"unknown", nil, config.Check{
{"unknown", nil, []string{"README.md"}, config.Check{
ID: "tests", Intent: "Repository changes must pass its configured tests.",
Paths: []string{"**"},
Run: `echo "Edit .intentci.yaml and replace this command." >&2; exit 1`,
}},
}
}

func nodeMatches() []string {
return []string{
"src/app.js", "src/App.jsx", "src/module.mjs", "src/module.cjs",
"src/app.ts", "src/App.tsx", "src/module.mts", "src/module.cts",
"tsconfig.json", "tsconfig.build.json",
"package.json", "package-lock.json", "pnpm-lock.yaml", "yarn.lock",
}
}

func gradleMatches() []string {
return []string{
"src/main/java/App.java", "build.gradle", "build.gradle.kts",
"settings.gradle", "settings.gradle.kts", "gradle/wrapper/gradle-wrapper.properties",
"gradle.lockfile", "gradlew", "gradlew.bat",
}
}

func TestDetectStacks(t *testing.T) {
for _, test := range detectionCases() {
t.Run(test.name, func(t *testing.T) {
Expand Down Expand Up @@ -235,10 +260,52 @@ func TestInitializeStacks(t *testing.T) {
if len(cfg.Checks) != 1 || !reflect.DeepEqual(cfg.Checks[0], test.want) {
t.Fatalf("initialized checks = %#v\nwant %#v", cfg.Checks, test.want)
}
for _, changed := range test.matches {
if got := selectChecks(cfg.Checks, []string{changed}); len(got) != 1 {
t.Errorf("%q selected %#v", changed, got)
}
}
got := selectChecks(cfg.Checks, []string{"README.md"})
if test.name == "unknown" {
if len(got) != 1 {
t.Errorf("unknown stack selected %#v for README.md", got)
}
} else if got != nil {
t.Errorf("README.md selected %#v", got)
}
})
}
}

func TestNodeJSXWorkflow(t *testing.T) {
root := gitRepo(t)
writeFile(t, root, "package.json", "{}")
if err := initialize(root); err != nil {
t.Fatal(err)
}
commitAll(t, root)
writeFile(t, root, "src/App.jsx", "export default function App() {}\n")

toolDir := t.TempDir()
marker := filepath.Join(t.TempDir(), "npm")
writeFile(t, toolDir, "npm", `#!/bin/sh
printf '%s\n%s\n' "$PWD" "$*" > "$INTENTCI_NPM_MARKER"
`)
t.Setenv("PATH", toolDir+":"+os.Getenv("PATH"))
t.Setenv("INTENTCI_NPM_MARKER", marker)

for _, args := range [][]string{nil, {"--all"}} {
code, stdout, stderr := runFrom(t, context.Background(), root, args...)
if code != 0 || !strings.Contains(stdout, "RUN node-tests") {
t.Fatalf("args %v = %d\n%s\n%s", args, code, stdout, stderr)
}
data, err := os.ReadFile(marker)
if err != nil || string(data) != root+"\ntest\n" {
t.Fatalf("npm invocation = %q, %v", data, err)
}
}
}

func TestInitializeRefusesOverwriteAndV1(t *testing.T) {
root := gitRepo(t)
if err := initialize(root); err != nil {
Expand Down
2 changes: 1 addition & 1 deletion internal/repo/repo_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -168,7 +168,7 @@ sleep 30
if !errors.Is(err, context.Canceled) || strings.Contains(err.Error(), "not a Git repository") {
t.Fatalf("Root() error = %v", err)
}
case <-time.After(time.Second):
case <-time.After(3 * time.Second):
t.Fatal("Git process ignored cancellation")
}
if err := syscall.Kill(pid, 0); !errors.Is(err, syscall.ESRCH) {
Expand Down
2 changes: 1 addition & 1 deletion internal/version/version.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
package version

const DefaultVersion = "2.0.3"
const DefaultVersion = "2.0.4"

var Version = DefaultVersion

Expand Down