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
94 changes: 94 additions & 0 deletions .github/workflows/ci-autofix.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
name: CI Auto-Fix

on:
workflow_run:
workflows: ["CI"]
types: [completed]

permissions:
contents: write
actions: write

concurrency:
group: ci-autofix-${{ github.event.workflow_run.head_branch }}
cancel-in-progress: true

jobs:
autofix:
if: >
github.event.workflow_run.conclusion == 'failure' &&
github.event.workflow_run.head_repository.full_name == github.repository &&
github.event.workflow_run.head_branch != ''
runs-on: ubuntu-latest
steps:
- name: Checkout failed branch
uses: actions/checkout@v4
with:
ref: ${{ github.event.workflow_run.head_branch }}

- name: Stop on stale branch head
id: freshness
run: |
current_sha="$(git rev-parse HEAD)"
expected_sha="${{ github.event.workflow_run.head_sha }}"
if [ "$current_sha" != "$expected_sha" ]; then
echo "should_skip=true" >> "$GITHUB_OUTPUT"
echo "Branch advanced from ${expected_sha} to ${current_sha}; skipping stale auto-fix run."
exit 0
fi
echo "should_skip=false" >> "$GITHUB_OUTPUT"

- name: Set up Go
if: steps.freshness.outputs.should_skip != 'true'
uses: actions/setup-go@v5
with:
go-version-file: go.mod

- name: Download dependencies
if: steps.freshness.outputs.should_skip != 'true'
run: go mod download

- name: Run gofmt auto-fix
if: steps.freshness.outputs.should_skip != 'true'
run: gofmt -w .

- name: Run golangci-lint auto-fix
id: golangci_autofix
if: steps.freshness.outputs.should_skip != 'true'
continue-on-error: true
uses: golangci/golangci-lint-action@v7
with:
version: v2.11.3
args: --fix

- name: Detect changes
id: changes
if: steps.freshness.outputs.should_skip != 'true'
run: |
if git diff --quiet; then
echo "changed=false" >> "$GITHUB_OUTPUT"
echo "No auto-fixable changes detected."
exit 0
fi
echo "changed=true" >> "$GITHUB_OUTPUT"

- name: Commit and push auto-fixes
if: steps.changes.outputs.changed == 'true'
run: |
git config user.name "github-actions[bot]"
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
git add -A
git commit -m "ci: auto-fix formatting and lint"
git push origin HEAD:${{ github.event.workflow_run.head_branch }}

- name: Re-dispatch CI
if: steps.changes.outputs.changed == 'true'
env:
GH_TOKEN: ${{ github.token }}
run: |
curl --fail --silent --show-error \
-X POST \
-H "Authorization: Bearer ${GH_TOKEN}" \
-H "Accept: application/vnd.github+json" \
https://api.github.com/repos/${{ github.repository }}/actions/workflows/ci.yml/dispatches \
-d '{"ref":"${{ github.event.workflow_run.head_branch }}"}'
15 changes: 15 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ on:
branches: [main]
pull_request:
branches: [main]
workflow_dispatch:

permissions:
contents: read
Expand Down Expand Up @@ -35,6 +36,20 @@ jobs:
- name: Download dependencies
run: go mod download

- name: Check formatting
run: |
unformatted="$(gofmt -l .)"
if [ -n "$unformatted" ]; then
echo "These files are not gofmt-formatted:"
echo "$unformatted"
exit 1
fi

- name: Run golangci-lint
uses: golangci/golangci-lint-action@v7
with:
version: v2.11.3

- name: Run tests
run: go test -race -coverprofile=coverage.out ./...

Expand Down
8 changes: 8 additions & 0 deletions .golangci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
version: "2"

linters:
default: none
enable:
- govet
- ineffassign
- unused
5 changes: 3 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ npx skills add https://github.com/disk0Dancer/climate --skill climate-generator

## Publish

Push a generated CLI to GitHub with CI and release workflows:
Push a generated CLI to GitHub with CI, CI auto-fix, and release workflows:

```bash
climate publish myapi --owner disk0Dancer
Expand All @@ -64,14 +64,15 @@ Demo: [disk0Dancer/github](https://github.com/disk0Dancer/github) — 1 100+ end
| `list` | Show registered CLIs |
| `remove` | Delete a generated CLI |
| `upgrade` | Regenerate from updated spec |
| `publish` | Push CLI to GitHub with CI/release |
| `publish` | Push CLI to GitHub with CI/auto-fix/release |
| `skill generate` | Emit agent skill prompt |

## Docs

- [Site](https://disk0dancer.github.io/climate/)
- [LLM index](https://disk0dancer.github.io/climate/llms.txt)
- [Compose design](docs/design-compose.md)
- [CI auto-fix design](docs/design-ci-autofix.md)
- [Mock design](docs/design-mock.md)
- [OpenAPI 3.0 support matrix](docs/openapi-3-support-matrix.md)

Expand Down
4 changes: 2 additions & 2 deletions cmd/climate/commands/compose.go
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,7 @@ Examples:
// Update manifest.
mf, err := manifest.Load()
if err != nil {
fmt.Fprintf(cmd.ErrOrStderr(), "Warning: could not load manifest: %v\n", err)
_, _ = fmt.Fprintf(cmd.ErrOrStderr(), "Warning: could not load manifest: %v\n", err)
} else {
mf.Upsert(manifest.CLIEntry{
Name: result.CLIName,
Expand All @@ -85,7 +85,7 @@ Examples:
OpenAPISpec: opts.SpecSource,
})
if saveErr := mf.Save(); saveErr != nil {
fmt.Fprintf(cmd.ErrOrStderr(), "Warning: could not save manifest: %v\n", saveErr)
_, _ = fmt.Fprintf(cmd.ErrOrStderr(), "Warning: could not save manifest: %v\n", saveErr)
}
}

Expand Down
4 changes: 2 additions & 2 deletions cmd/climate/commands/generate.go
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ Examples:
// Update manifest
mf, err := manifest.Load()
if err != nil {
fmt.Fprintf(cmd.ErrOrStderr(), "Warning: could not load manifest: %v\n", err)
_, _ = fmt.Fprintf(cmd.ErrOrStderr(), "Warning: could not load manifest: %v\n", err)
} else {
mf.Upsert(manifest.CLIEntry{
Name: result.CLIName,
Expand All @@ -69,7 +69,7 @@ Examples:
OpenAPISpec: specSource,
})
if saveErr := mf.Save(); saveErr != nil {
fmt.Fprintf(cmd.ErrOrStderr(), "Warning: could not save manifest: %v\n", saveErr)
_, _ = fmt.Fprintf(cmd.ErrOrStderr(), "Warning: could not save manifest: %v\n", saveErr)
}
}

Expand Down
12 changes: 6 additions & 6 deletions cmd/climate/commands/mock.go
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,7 @@ Examples:
if err != nil {
exitError("Failed to emit event", err)
}
fmt.Fprintf(cmd.OutOrStdout(),
_, _ = fmt.Fprintf(cmd.OutOrStdout(),
"Emitted %s event from %s to %s (status: %d)\n",
method, mockEventPath, mockEmitURL, statusCode)
return nil
Expand All @@ -89,14 +89,14 @@ Examples:
latency := time.Duration(mockLatency) * time.Millisecond
s := mock.NewServer(openAPI, addr, latency)

fmt.Fprintf(cmd.OutOrStdout(), "Mock server for %q listening on http://localhost%s\n",
_, _ = fmt.Fprintf(cmd.OutOrStdout(), "Mock server for %q listening on http://localhost%s\n",
openAPI.Info.Title, addr)
if mockLatency > 0 {
fmt.Fprintf(cmd.OutOrStdout(), "Artificial latency: %dms\n", mockLatency)
_, _ = fmt.Fprintf(cmd.OutOrStdout(), "Artificial latency: %dms\n", mockLatency)
}
fmt.Fprintln(cmd.OutOrStdout(), "\nRoutes:")
fmt.Fprint(cmd.OutOrStdout(), s.Summary())
fmt.Fprintln(cmd.OutOrStdout(), "\nPress Ctrl+C to stop.")
_, _ = fmt.Fprintln(cmd.OutOrStdout(), "\nRoutes:")
_, _ = fmt.Fprint(cmd.OutOrStdout(), s.Summary())
_, _ = fmt.Fprintln(cmd.OutOrStdout(), "\nPress Ctrl+C to stop.")

if err := s.ListenAndServe(); err != nil {
exitError("Mock server error", err)
Expand Down
2 changes: 1 addition & 1 deletion cmd/climate/commands/publish.go
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,7 @@ Authentication is read from --github-token, GITHUB_TOKEN, or GH_TOKEN.`,

mf.Upsert(publish.PublishedManifestEntry(entry, result))
if saveErr := mf.Save(); saveErr != nil {
fmt.Fprintf(cmd.ErrOrStderr(), "Warning: could not save manifest: %v\n", saveErr)
_, _ = fmt.Fprintf(cmd.ErrOrStderr(), "Warning: could not save manifest: %v\n", saveErr)
}

writeJSON(result)
Expand Down
4 changes: 2 additions & 2 deletions cmd/climate/commands/skill.go
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,7 @@ Modes:
return nil
}

fmt.Fprint(cmd.OutOrStdout(), prompt)
_, _ = fmt.Fprint(cmd.OutOrStdout(), prompt)
return nil
},
}
Expand All @@ -86,7 +86,7 @@ This is the content of skills/climate.md shipped with climate.
An LLM agent can read it to learn how to use climate and self-register
it as the climate.generator skill.`,
RunE: func(cmd *cobra.Command, args []string) error {
fmt.Fprint(cmd.OutOrStdout(), skills.ClimateMD)
_, _ = fmt.Fprint(cmd.OutOrStdout(), skills.ClimateMD)
return nil
},
}
Expand Down
42 changes: 42 additions & 0 deletions docs/design-ci-autofix.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
# CI Auto-Fix Design

## Problem

The primary CI workflow should remain deterministic and reviewable, but simple
formatting and auto-fixable lint failures are still noisy and time-consuming to
clean up by hand.

## Goals

- Keep `CI` as a check-only workflow.
- Run a separate workflow automatically after a failed `CI` run.
- Apply `gofmt` and `golangci-lint --fix` on same-repository branches.
- Push fixes back to the branch and explicitly re-dispatch `CI` on the updated
branch head.
- Reuse the same lifecycle behavior for repositories bootstrapped by
`climate publish`.

## Non-Goals

- Auto-fixing test, build, or `go vet` failures.
- Pushing changes to forked pull request branches.
- Rewriting user-managed workflow files that do not carry the climate marker.

## Workflow Shape

1. `CI` runs on `push`, `pull_request`, and `workflow_dispatch`.
2. `CI Auto-Fix` listens for failed `CI` completions via `workflow_run`.
3. The auto-fix job only runs when the failing branch belongs to the same
repository and the branch head still matches the SHA that failed.
4. The job runs `gofmt -w .` and `golangci-lint --fix`.
5. If changes were produced, the workflow commits and pushes them, then calls
the Actions dispatch API to run `CI` again on that branch.

## Edge Cases

- If the branch advanced after the failing `CI` run, the auto-fix job exits
without writing to a stale branch state.
- If the failure is not auto-fixable, the workflow exits cleanly with no commit.
- A push created with `GITHUB_TOKEN` does not trigger `push` workflows, so the
explicit re-dispatch step is required for the fixed commit to get a fresh
`CI` run.
3 changes: 2 additions & 1 deletion docs/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ petstore pet get --pet-id 1
| `list` | Show registered CLIs |
| `remove` | Delete a generated CLI |
| `upgrade` | Regenerate from updated spec |
| `publish` | Push CLI to GitHub with CI/release |
| `publish` | Push CLI to GitHub with CI/auto-fix/release |
| `skill generate` | Emit agent skill prompt |

## Agent skills
Expand All @@ -66,6 +66,7 @@ npx skills add https://github.com/disk0Dancer/climate --skill climate-generator
## Design docs

- [Compose design](./design-compose.md)
- [CI auto-fix design](./design-ci-autofix.md)
- [Mock design](./design-mock.md)
- [OpenAPI 3.0 support matrix](./openapi-3-support-matrix.md)

Expand Down
Loading
Loading