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
35 changes: 31 additions & 4 deletions .github/workflows/build.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -9,17 +9,44 @@ on:
- main

jobs:
build-windows:
name: Build Windows
runs-on: blacksmith-4vcpu-windows-2025
env:
GOWORK: off
steps:
- name: Checkout code
uses: actions/checkout@v5

- name: Set up Go
uses: actions/setup-go@v6
with:
go-version-file: go.mod
cache: true

- name: Download dependencies
run: go mod download

- name: Test
run: go test -v -count=1 -timeout=3m ./...

- name: Build
run: go build -o openapi-changes.exe .

build:
name: Build
runs-on: blacksmith-4vcpu-ubuntu-2404
env:
GOWORK: off
steps:
- name: Checkout code
uses: actions/checkout@v5

- name: Set up Go 1.x
- name: Set up Go
uses: actions/setup-go@v6
with:
go-version: ^1.24
go-version-file: go.mod
cache: true
id: go

- name: Set up Node.js
Expand All @@ -36,8 +63,8 @@ jobs:
status="$(git status --porcelain --untracked-files=all -- html-report/ui/build/static)"
test -z "$status" || { echo "$status"; exit 1; }

- name: Get dependencies
run: go get -v -t -d ./...
- name: Download dependencies
run: go mod download

- name: Test
run: go test ./...
Expand Down
3 changes: 2 additions & 1 deletion .github/workflows/publish.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,8 @@ jobs:
uses: actions/setup-go@v6
id: go
with:
go-version: ^1.24
go-version-file: go.mod
cache: true

- name: Set up Node.js
uses: actions/setup-node@v5
Expand Down
2 changes: 1 addition & 1 deletion AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -122,7 +122,7 @@ All `cmd/` implementation files use their canonical names (e.g., `cmd/summary.go
- `git/read_local.go` still forces `ExcludeExtensionRefs = true`, which weakens the practical effect of `--ext-refs` on that path.
- libopenapi breaking-rule configuration is global. `cmd/engine.go` guards this with `breakingConfigMu`, but `git/read_local.go` also writes to the global config via `SetActiveBreakingRulesConfig` — if the `git` layer is ever parallelized, it needs its own guard.
- The residual `tui/` package still exists in-repo, but it is not the canonical console implementation. The canonical console is `tui/v2/` (Bubbletea).
- **Dependency version sensitivity**: `doctor` (currently `v0.0.49`) and `libopenapi` (currently `v0.34.4`) version bumps can silently change comparison output, count semantics, or tree structure. After upgrading either dependency, always re-run the petstore regression fixtures and verify counts match expectations.
- **Dependency version sensitivity**: `doctor` (currently `v0.0.65`) and `libopenapi` (currently `v0.36.1`) version bumps can silently change comparison output, count semantics, or tree structure. After upgrading either dependency, always re-run the petstore regression fixtures and verify counts match expectations.

## Regression Fixtures

Expand Down
19 changes: 18 additions & 1 deletion cmd/common.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import (
"os"

"charm.land/lipgloss/v2"
"github.com/charmbracelet/x/term"
"github.com/pb33f/doctor/terminal"
whatChangedModel "github.com/pb33f/libopenapi/what-changed/model"
"github.com/pb33f/openapi-changes/model"
Expand Down Expand Up @@ -73,6 +74,22 @@ func commandStylesFor(palette terminal.Palette) commandStyles {
}
}

func commandPaletteForTheme(theme terminal.ThemeName) terminal.Palette {
if canQueryTerminalBackground(os.Stdin, os.Stderr) {
return terminal.PaletteForTheme(theme)
}
env := os.Environ()
return terminal.PaletteFor(theme, os.Stdout, env, terminal.DetectDarkBackgroundFromEnv(env))
}

func canQueryTerminalBackground(in, out *os.File) bool {
return isTerminalFile(in) && isTerminalFile(out)
}

func isTerminalFile(file *os.File) bool {
return file != nil && term.IsTerminal(file.Fd())
}

func addTerminalThemeFlags(cmd *cobra.Command) {
cmd.Flags().BoolP("no-color", "n", false, "Use the light Roger monochrome terminal theme")
cmd.Flags().Bool("roger-mode", false, "Alias for --no-color")
Expand Down Expand Up @@ -101,7 +118,7 @@ func readCommonFlags(cmd *cobra.Command) (opts summaryOpts, configFlag string, e
if err != nil {
return opts, configFlag, err
}
opts.palette = terminal.PaletteForTheme(opts.theme)
opts.palette = commandPaletteForTheme(opts.theme)
return
}

Expand Down
19 changes: 19 additions & 0 deletions cmd/common_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
package cmd

import (
"os"
"testing"

"github.com/pb33f/doctor/terminal"
Expand Down Expand Up @@ -50,6 +51,24 @@ func TestResolveTheme_RejectsConflictingFlags(t *testing.T) {
assert.Contains(t, err.Error(), "--no-color/--roger-mode and --tektronix cannot be used together")
}

func TestCanQueryTerminalBackground_RejectsRedirectedHandles(t *testing.T) {
stdinReader, stdinWriter, err := os.Pipe()
require.NoError(t, err)
t.Cleanup(func() {
_ = stdinReader.Close()
_ = stdinWriter.Close()
})

stderrReader, stderrWriter, err := os.Pipe()
require.NoError(t, err)
t.Cleanup(func() {
_ = stderrReader.Close()
_ = stderrWriter.Close()
})

assert.False(t, canQueryTerminalBackground(stdinReader, stderrWriter))
}

// --- prepareCommandRun ---

func TestPrepareCommandRun_ZeroArgs_PrintsUsageAndReturnsNil(t *testing.T) {
Expand Down
8 changes: 8 additions & 0 deletions cmd/loaders_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -308,6 +308,10 @@ func TestLoadCommitsFromArgs_GitRefUsesLeftRightDispatch(t *testing.T) {
}

func TestLoadCommitsFromArgs_LocalColonPathOutsideGitRepoStaysFileComparison(t *testing.T) {
if os.PathSeparator == '\\' {
t.Skip("colon filenames are not portable on Windows")
}

dir := t.TempDir()
chdirForTest(t, dir)

Expand All @@ -325,6 +329,10 @@ func TestLoadCommitsFromArgs_LocalColonPathOutsideGitRepoStaysFileComparison(t *
}

func TestLoadCommitsFromArgs_RepoHistoryColonPathUsesHistoryDispatch(t *testing.T) {
if os.PathSeparator == '\\' {
t.Skip("colon filenames are not portable on Windows")
}

repoDir := createGitSpecRepoForFile(t, "v1:beta.yaml")
chdirForTest(t, t.TempDir())

Expand Down
3 changes: 2 additions & 1 deletion cmd/markdown_report_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
package cmd

import (
"path/filepath"
"strings"
"testing"
"time"
Expand Down Expand Up @@ -273,7 +274,7 @@ func TestMarkdownReportCommand_TooManyArgs(t *testing.T) {
func TestMarkdownReportCommand_LeftRightFiles(t *testing.T) {
cmd := testRootCmd(GetMarkdownReportCommand(),
"--no-logo", "--no-color",
"--report-file", "/dev/null",
"--report-file", filepath.Join(t.TempDir(), "report.md"),
"../sample-specs/petstorev3-original.json",
"../sample-specs/petstorev3.json",
)
Expand Down
2 changes: 1 addition & 1 deletion cmd/paths_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ func TestAbsoluteRepoPath(t *testing.T) {
})

t.Run("returns absolute path", func(t *testing.T) {
p := "/home/user/repo"
p := t.TempDir()

have, err := absoluteRepoPath(p)
assert.NoError(t, err)
Expand Down
4 changes: 4 additions & 0 deletions cmd/report_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -526,6 +526,10 @@ func TestReportCommand_GitRefUsesLeftRightMode(t *testing.T) {
}

func TestReportCommand_RepoHistoryColonPathUsesHistoryMode(t *testing.T) {
if os.PathSeparator == '\\' {
t.Skip("colon filenames are not portable on Windows")
}

repoDir := createGitSpecRepoForFile(t, "v1:beta.yaml")
chdirForTest(t, t.TempDir())

Expand Down
40 changes: 32 additions & 8 deletions cmd/summary_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,20 +28,32 @@ func captureStdout(t *testing.T, fn func()) string {
reader, writer, err := os.Pipe()
require.NoError(t, err)

outputCh := make(chan struct {
data []byte
err error
}, 1)
go func() {
data, readErr := io.ReadAll(reader)
outputCh <- struct {
data []byte
err error
}{data: data, err: readErr}
}()

os.Stdout = writer
t.Cleanup(func() {
os.Stdout = oldStdout
})

fn()

os.Stdout = oldStdout
require.NoError(t, writer.Close())
output, err := io.ReadAll(reader)
require.NoError(t, err)
output := <-outputCh
require.NoError(t, output.err)
require.NoError(t, reader.Close())
os.Stdout = oldStdout

return string(output)
return string(output.data)
}

func captureStderr(t *testing.T, fn func()) string {
Expand All @@ -51,20 +63,32 @@ func captureStderr(t *testing.T, fn func()) string {
reader, writer, err := os.Pipe()
require.NoError(t, err)

outputCh := make(chan struct {
data []byte
err error
}, 1)
go func() {
data, readErr := io.ReadAll(reader)
outputCh <- struct {
data []byte
err error
}{data: data, err: readErr}
}()

os.Stderr = writer
t.Cleanup(func() {
os.Stderr = oldStderr
})

fn()

os.Stderr = oldStderr
require.NoError(t, writer.Close())
output, err := io.ReadAll(reader)
require.NoError(t, err)
output := <-outputCh
require.NoError(t, output.err)
require.NoError(t, reader.Close())
os.Stderr = oldStderr

return string(output)
return string(output.data)
}

func mustMakeDoctorOnlyCommitFromSpecs(t *testing.T, hash, left, right string) *model.Commit {
Expand Down
Loading