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
11 changes: 11 additions & 0 deletions .github/workflows/publish.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,13 @@ jobs:
type=raw,value=${{ github.event.client_payload.new-tag }}
type=raw,value=latest

- name: Capture Docker build metadata
id: docker_build_meta
run: |
echo "version=${{ github.event.client_payload.new-tag }}" >> "$GITHUB_OUTPUT"
echo "commit=${GITHUB_SHA}" >> "$GITHUB_OUTPUT"
echo "date=$(date -u +%Y-%m-%dT%H:%M:%SZ)" >> "$GITHUB_OUTPUT"

- name: Build and push Docker images
uses: docker/build-push-action@v6
with:
Expand All @@ -134,5 +141,9 @@ jobs:
push: true
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
build-args: |
APP_VERSION=${{ steps.docker_build_meta.outputs.version }}
APP_COMMIT=${{ steps.docker_build_meta.outputs.commit }}
APP_DATE=${{ steps.docker_build_meta.outputs.date }}
cache-from: type=gha
cache-to: type=gha,mode=max
15 changes: 12 additions & 3 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,22 +12,27 @@ Public documentation:

`openapi-changes` is a Go CLI for comparing OpenAPI specifications across:

- direct left/right file or URL comparison
- direct left/right file, URL, or git-revision comparison
- local git history for a file in a repository
- GitHub-hosted file history via file URL

The public command surface is:
The public comparison/report surface is:

- `console`
- `summary`
- `report`
- `markdown-report`
- `html-report`

All five commands use the current engine built on `doctor`, the open source Apache 2.0 library from pb33f:
These five commands use the current engine built on `doctor`, the open source Apache 2.0 library from pb33f:

- https://github.com/pb33f/doctor

Utility commands:

- `completion`
- `version`

## Source Of Truth

The code is the source of truth.
Expand Down Expand Up @@ -65,6 +70,7 @@ All `cmd/` implementation files use their canonical names (e.g., `cmd/summary.go

- Left/right comparisons are synthetic comparisons, not fake git history.
- Do not emit synthetic commit metadata in left/right machine- or human-facing report output.
- Git revision inputs (`revision:path`) resolve `$ref` siblings from the same revision via `GitRevisionFS`, not from the working tree.

### Failure semantics

Expand All @@ -89,6 +95,7 @@ All `cmd/` implementation files use their canonical names (e.g., `cmd/summary.go
## Files That Matter Most

- `cmd/root.go` — root Cobra command, CLI entry point, subcommand registration
- `cmd/version.go` — prints the raw build version string
- `cmd/common.go` — shared option flags and Lip Gloss styling for all doctor-based commands
- `cmd/loaders.go` — loads specs from files, URLs, and git history; progress tracking and error aggregation
- `cmd/engine.go` — wraps doctor changerator for API comparison; manages document resource cleanup and mutex-guarded breaking config
Expand All @@ -99,7 +106,9 @@ All `cmd/` implementation files use their canonical names (e.g., `cmd/summary.go
- `cmd/console.go` — launches the interactive Bubbletea terminal UI
- `cmd/flatten_report.go` — flattens hierarchical change reports into flat structures with hashed changes and normalized paths
- `cmd/report_common.go` — shared utilities: summary report creation from commits, formatted report file writing
- `cmd/left_right_sources.go` — resolves local, URL, and git-revision inputs into uniform comparison sources with proper document configuration
- `git/read_local.go` — local git history extraction via git commands; commit and file content preparation
- `git/revision_fs.go` — virtual filesystem that reads files from a git revision for `$ref` resolution in revision-scoped comparisons
- `git/github.go` — remote file history fetching from GitHub repos via doctor GitHub service
- `html-report/generator.go` — renders self-contained HTML using embedded templates and syntax-highlighted code
- `model/report.go` — hashed change structures with SHA256 hashes and raw paths for serialization
Expand Down
16 changes: 12 additions & 4 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
FROM --platform=$BUILDPLATFORM node:22-bookworm-slim AS ui-builder

WORKDIR /opt/openapi-changes/html-report/ui
COPY html-report/ui/package.json html-report/ui/package-lock.json ./
COPY html-report/ui/package.json html-report/ui/package-lock.json html-report/ui/tsconfig.json html-report/ui/vite.config.ts html-report/ui/index.html ./
COPY html-report/ui/src ./src
RUN npm ci
COPY html-report/ui/ ./
RUN npm run build

FROM --platform=$BUILDPLATFORM golang:1.25-bookworm AS builder
Expand All @@ -12,6 +12,9 @@ ARG TARGETPLATFORM
ARG BUILDPLATFORM
ARG TARGETOS
ARG TARGETARCH
ARG APP_VERSION=dev
ARG APP_COMMIT=unknown
ARG APP_DATE=1970-01-01T00:00:00Z

RUN mkdir -p /opt/openapi-changes

Expand All @@ -21,10 +24,15 @@ COPY . ./
COPY --from=ui-builder /opt/openapi-changes/html-report/ui/build/ html-report/ui/build/

RUN go mod download && go mod verify
RUN CGO_ENABLED=0 GOOS=$TARGETOS GOARCH=$TARGETARCH go build -ldflags="-w -s" -v -o /openapi-changes openapi-changes.go
RUN CGO_ENABLED=0 GOOS=$TARGETOS GOARCH=$TARGETARCH go build \
-ldflags="-w -s -X main.version=${APP_VERSION} -X main.commit=${APP_COMMIT} -X main.date=${APP_DATE}" \
-v -o /openapi-changes openapi-changes.go

FROM --platform=$TARGETPLATFORM debian:bookworm-slim
RUN apt-get update && apt-get --yes install git && rm -rf /var/lib/apt/lists/*
RUN apt-get update \
&& apt-get --yes install git \
&& git config --system --add safe.directory '*' \
&& rm -rf /var/lib/apt/lists/*
WORKDIR /work
COPY --from=builder /openapi-changes /

Expand Down
30 changes: 28 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,8 @@

## The world's **_most powerful and complete_** OpenAPI diff tool.

`openapi-changes` lets you inspect what changed in an OpenAPI specification between two files,
across local git history, or directly from a GitHub-hosted file URL.
`openapi-changes` lets you inspect what changed in an OpenAPI specification between two files,
between git revisions of the same file, across local git history, or directly from a GitHub-hosted file URL.

It can render the same semantic change model as:

Expand Down Expand Up @@ -63,6 +63,8 @@ docker pull pb33f/openapi-changes

Docker images are available for both `linux/amd64` and `linux/arm64`.

The published image configures Git to trust mounted repositories, so local git-history commands work without requiring extra `safe.directory` setup inside the container.

To run a command, mount the current working directory into the container:

```bash
Expand All @@ -75,6 +77,13 @@ To run the interactive `console` through Docker, allocate a TTY with `-it`:
docker run --rm -it -v $PWD:/work:rw pb33f/openapi-changes console . path/to/openapi.yaml
```

## Verify the install

Print the installed version:

```bash
openapi-changes version
```

---

Expand Down Expand Up @@ -128,6 +137,21 @@ A self-contained, offline HTML report with interactive timeline, change explorer

---

## Comparing git revisions

Compare a file at different git revisions without checking out branches:

```bash
openapi-changes summary HEAD~1:openapi.yaml ./openapi.yaml
openapi-changes html-report main:api/openapi.yaml feature-branch:api/openapi.yaml
```

The `revision:path` syntax works with any git ref -- branches, tags, `HEAD~N`, commit SHAs.
The path is relative to the repository root. This works with all commands and supports
multi-file specs with `$ref` references resolved from the same revision.

---

## Documentation

### [Quick Start Guide 🚀](https://pb33f.io/openapi-changes/quickstart/)
Expand All @@ -144,6 +168,7 @@ Full docs: https://pb33f.io/openapi-changes/
- [`markdown-report`](https://pb33f.io/openapi-changes/markdown-report/)
- [`html-report`](https://pb33f.io/openapi-changes/html-report/)
- [`completion`](https://pb33f.io/openapi-changes/completion/)
- [`version`](https://pb33f.io/openapi-changes/version/)
- [About openapi-changes](https://pb33f.io/openapi-changes/about/)

---
Expand Down Expand Up @@ -176,6 +201,7 @@ The current command surface is:
- `markdown-report` for shareable markdown output
- `html-report` for the interactive offline browser report
- `completion` for shell completion scripts
- `version` for raw build version output

Run `openapi-changes --help` or `openapi-changes <command> --help` for the live CLI surface.

Expand Down
1 change: 1 addition & 0 deletions cmd/command_surface_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,4 +15,5 @@ func TestCommandSurface_UsesCanonicalNames(t *testing.T) {
assert.Equal(t, "markdown-report", GetMarkdownReportCommand().Use)
assert.Equal(t, "html-report", GetHTMLReportCommand().Use)
assert.Equal(t, "console", GetConsoleCommand().Use)
assert.Equal(t, "version", GetVersionCommand().Use)
}
21 changes: 13 additions & 8 deletions cmd/common.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@ import (
"image/color"
"net/url"
"os"
"strings"

"charm.land/lipgloss/v2"
"github.com/pb33f/doctor/terminal"
Expand Down Expand Up @@ -209,18 +208,23 @@ func loadCommitsFromArgs(args []string, opts summaryOpts, breakingConfig *whatCh
if len(args) == 1 {
return loadGitHubCommits(args[0], opts, breakingConfig)
}
firstURL, _ := url.Parse(args[0])
if firstURL != nil && strings.HasPrefix(firstURL.Scheme, "http") {
return loadLeftRightCommits(args[0], args[1], opts, breakingConfig)
if isHTTPURL(args[0]) {
return loadLeftRightCommits(args[0], args[1], opts)
}
if _, _, ok := parseGitRef(args[0]); ok {
return loadLeftRightCommits(args[0], args[1], opts)
}
f, statErr := os.Stat(args[0])
if statErr == nil && f.IsDir() {
return loadGitHistoryCommits(args[0], args[1], opts, breakingConfig)
}
if _, _, ok := parseGitRef(args[1]); ok {
return loadLeftRightCommits(args[0], args[1], opts)
}
if statErr != nil {
return nil, fmt.Errorf("cannot open file/repository: '%s'", args[0])
}
if f.IsDir() {
return loadGitHistoryCommits(args[0], args[1], opts, breakingConfig)
}
return loadLeftRightCommits(args[0], args[1], opts, breakingConfig)
return loadLeftRightCommits(args[0], args[1], opts)
}

// printCommandUsage prints lipgloss-styled usage for any doctor-based command.
Expand All @@ -239,6 +243,7 @@ func printCommandUsage(commandName, description string, palette terminal.Palette
fmt.Println()
fmt.Println("Examples:")
fmt.Printf(" %s\n", cmdStyle.Render("openapi-changes "+commandName+" ./specs/old.yaml ./specs/new.yaml"))
fmt.Printf(" %s\n", cmdStyle.Render("openapi-changes "+commandName+" HEAD~1:openapi.yaml ./openapi.yaml"))
fmt.Printf(" %s\n", cmdStyle.Render("openapi-changes "+commandName+" https://github.com/user/repo/blob/main/openapi.yaml"))
fmt.Printf(" %s\n", cmdStyle.Render("openapi-changes "+commandName+" /path/to/git/repo path/to/openapi.yaml"))
fmt.Println()
Expand Down
2 changes: 1 addition & 1 deletion cmd/console.go
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ func GetConsoleCommand() *cobra.Command {
Use: "console",
Short: "Interactive terminal UI for exploring changes",
Long: "Navigate through changes visually in an interactive terminal UI built with Bubbletea, using the doctor changerator engine.",
Example: "openapi-changes console /path/to/git/repo path/to/file/in/repo/openapi.yaml",
Example: "openapi-changes console HEAD~1:openapi.yaml ./openapi.yaml",
RunE: func(cmd *cobra.Command, args []string) error {
input, err := prepareCommandRun(cmd, args, printConsoleUsage)
if err != nil {
Expand Down
Loading