Automate climate publication from generated CLI to release surfaces#1
Conversation
Add a docs-first landing refresh, Homebrew tap guidance, tagged-release tap sync, and a new publish command that creates GitHub repositories for generated CLIs and bootstraps CI/release lifecycle files. Constraint: Cross-repo Homebrew sync requires a dedicated token because GITHUB_TOKEN cannot push to a separate tap repository Rejected: Require gh CLI for generated CLI publishing | would add an external runtime dependency to climate Confidence: high Scope-risk: moderate Directive: Only overwrite lifecycle files while they still carry the climate-managed marker; preserve user-edited files otherwise Tested: go test ./...; go run ./cmd/climate --help; go run ./cmd/climate publish --help; brew audit --strict disk0Dancer/tap/climate; brew install --build-from-source --dry-run climate Not-tested: End-to-end tagged release syncing the Homebrew tap from GitHub Actions before HOMEBREW_TAP_TOKEN is configured
There was a problem hiding this comment.
Pull request overview
This PR expands climate beyond local CLI generation into a “publish + lifecycle” flow, while refreshing the docs landing and adding Homebrew tap automation on tagged releases.
Changes:
- Add
climate publishto create/reuse GitHub repos for generated CLIs, bootstrap lifecycle files, and push source. - Introduce a minimal GitHub REST client and manifest fields to track publish metadata.
- Refresh docs (new landing/LLM index/robots/config) and add a release workflow job to sync the Homebrew tap formula.
Reviewed changes
Copilot reviewed 20 out of 20 changed files in this pull request and generated 8 comments.
Show a summary per file
| File | Description |
|---|---|
skills/climate.md |
Documents new publish command and updated install guidance. |
skills/climate-generator/SKILL.md |
Adds skills.sh-compatible skill doc including publish workflow. |
scripts/render_homebrew_formula.sh |
New script to render Homebrew formula from tag + source sha256. |
internal/publish/templates.go |
Adds managed lifecycle templates (README, gitignore, CI, release workflow). |
internal/publish/templates_test.go |
Tests marker-based overwrite/preserve behavior for lifecycle files. |
internal/publish/publish.go |
Implements GitHub repo ensure + local git bootstrap/push + manifest updates. |
internal/manifest/manifest.go |
Extends manifest entries with repository/publish metadata and preserves it on upsert. |
internal/manifest/manifest_test.go |
Tests preservation of publish metadata across Upsert updates. |
internal/githubutil/githubutil.go |
Adds minimal GitHub REST client for repo creation/reuse. |
internal/githubutil/githubutil_test.go |
Tests repo creation and reuse flows via mocked HTTP transport. |
internal/generator/generator.go |
Makes generated CLI version build-overridable (via -ldflags -X). |
internal/generator/generator_test.go |
Tests that generated root.go declares and uses the overridable version variable. |
docs/site-config.js |
Adds optional GA4 measurement ID configuration. |
docs/robots.txt |
Adds explicit crawler policy for the docs site. |
docs/llms.txt |
Adds machine-readable documentation index for LLM tooling. |
docs/index.md |
Adds Markdown companion docs page for the landing site. |
docs/index.html |
Replaces/refreshes the HTML landing page with docs-first structure and optional analytics loader. |
cmd/climate/commands/publish.go |
Adds the publish cobra command wiring to internal publish logic. |
README.md |
Reworks README to docs-first positioning and includes publish/Homebrew guidance. |
.github/workflows/release.yml |
Adds post-release job to sync Homebrew tap (requires HOMEBREW_TAP_TOKEN). |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| runs-on: ubuntu-latest | ||
| needs: release | ||
| steps: | ||
| - name: Check required secret | ||
| env: | ||
| HOMEBREW_TAP_TOKEN: ${{ secrets.HOMEBREW_TAP_TOKEN }} | ||
| run: | | ||
| if [ -z "${HOMEBREW_TAP_TOKEN}" ]; then | ||
| echo "::error::Set the HOMEBREW_TAP_TOKEN repository secret to enable Homebrew tap updates." | ||
| exit 1 | ||
| fi | ||
|
|
There was a problem hiding this comment.
The update-homebrew-tap job hard-fails the entire release workflow when HOMEBREW_TAP_TOKEN is not set. Until that secret is configured, tagging a release will fail even though publishing GitHub Release assets could otherwise succeed. Consider guarding the whole job/steps with a conditional (run only when the secret is present) or turning the missing-token path into a no-op/warning so releases can proceed without tap sync.
| runs-on: ubuntu-latest | |
| needs: release | |
| steps: | |
| - name: Check required secret | |
| env: | |
| HOMEBREW_TAP_TOKEN: ${{ secrets.HOMEBREW_TAP_TOKEN }} | |
| run: | | |
| if [ -z "${HOMEBREW_TAP_TOKEN}" ]; then | |
| echo "::error::Set the HOMEBREW_TAP_TOKEN repository secret to enable Homebrew tap updates." | |
| exit 1 | |
| fi | |
| if: ${{ secrets.HOMEBREW_TAP_TOKEN != '' }} | |
| runs-on: ubuntu-latest | |
| needs: release | |
| steps: |
| repo="${GITHUB_REPOSITORY:-disk0Dancer/climate}" | ||
|
|
||
| cat <<EOF | ||
| class Climate < Formula | ||
| desc "Generate auth-aware Go CLIs from OpenAPI specifications" | ||
| homepage "https://disk0dancer.github.io/climate/" | ||
| url "https://github.com/${repo}/archive/refs/tags/${tag_version}.tar.gz" | ||
| sha256 "${source_sha256}" | ||
| license "Apache-2.0" | ||
| head "https://github.com/${repo}.git", branch: "main" | ||
|
|
||
| depends_on "go" => :build | ||
|
|
||
| def install | ||
| ldflags = "-s -w -X github.com/disk0Dancer/climate/cmd/climate/commands.version=${tag_version}" | ||
| system "go", "build", *std_go_args(ldflags: ldflags, output: bin/"climate"), "./cmd/climate" | ||
| end |
There was a problem hiding this comment.
The formula URL/head are parameterized via $GITHUB_REPOSITORY, but the ldflags import path is hardcoded to github.com/disk0Dancer/climate/.... If this script is ever run from a fork/rename (or reused for another repo), the build will fail because -X won’t match the actual package path. Consider deriving the import path from the same repo variable (or otherwise keeping these consistent).
| if data, err := os.ReadFile(path); err == nil { | ||
| if !strings.Contains(string(data), marker) { | ||
| return false, nil | ||
| } | ||
| if string(data) == content { | ||
| return false, nil | ||
| } | ||
| } |
There was a problem hiding this comment.
writeManagedFile() treats any ReadFile error as if the file is missing, and will then overwrite it. That means permission errors / transient IO errors could cause an unintended overwrite instead of surfacing the underlying problem. Consider returning an error for non-ENOENT read failures (e.g., only proceed when errors.Is(err, os.ErrNotExist)).
| ## Install | ||
|
|
||
| ~~~bash | ||
| go install github.com/%s@latest | ||
| ~~~ |
There was a problem hiding this comment.
The generated README advertises go install github.com/<owner>/<repo>@latest, but generated CLIs currently have go.mod declaring module <cli-name> and imports like <cli-name>/internal/client (see internal/generator/generator.go). After publishing to GitHub, go install github.com/<owner>/<repo>@latest will fail due to module path mismatch. Publish likely needs to rewrite the generated module path (and internal imports) to github.com/<owner>/<repo> (or adjust the README/install guidance accordingly).
| if err != nil { | ||
| return nil, err | ||
| } | ||
| if repo.DefaultBranch != "" { |
There was a problem hiding this comment.
opts.DefaultBranch is effectively ignored because branch is always overwritten with repo.DefaultBranch when it is non-empty (GitHub typically returns a non-empty default_branch for both new and existing repos). If the intent is to honor --default-branch for newly-created repos but follow the existing repo's default branch when reusing, this should be conditional (e.g., only override from repo.DefaultBranch when reusing an existing repo).
| if repo.DefaultBranch != "" { | |
| if !created && repo.DefaultBranch != "" { |
| remoteURL := repo.SSHURL | ||
| if remoteURL == "" { | ||
| remoteURL = repo.CloneURL |
There was a problem hiding this comment.
syncGitRepository() prefers repo.SSHURL over repo.CloneURL, so climate publish will attempt an SSH push even though it already required an API token. On machines without SSH keys configured for GitHub, publish will fail despite having a valid token. Consider preferring HTTPS authenticated with the provided token (or offering an explicit flag to choose SSH vs HTTPS).
| remoteURL := repo.SSHURL | |
| if remoteURL == "" { | |
| remoteURL = repo.CloneURL | |
| remoteURL := repo.CloneURL | |
| if remoteURL == "" { | |
| remoteURL = repo.SSHURL |
| result, err := publish.Publish(context.Background(), entry, openAPI, publish.Options{ | ||
| Token: publishGitHubToken, | ||
| Owner: publishOwner, | ||
| RepoName: publishRepo, | ||
| Description: publishDescription, | ||
| Homepage: publishHomepage, | ||
| Visibility: visibility, | ||
| DefaultBranch: publishDefaultBranch, | ||
| ReuseExisting: publishReuseExisting, | ||
| }) |
There was a problem hiding this comment.
The command calls publish.Publish(context.Background(), ...), which drops cancellation/deadline propagation from Cobra. Using cmd.Context() here would allow Ctrl-C / parent context cancellation to stop API calls and git operations cleanly.
| if getErr != nil { | ||
| return nil, false, fmt.Errorf("repository already exists but could not be fetched: %w", getErr) | ||
| } | ||
| return existing, false, nil |
There was a problem hiding this comment.
EnsureRepository() treats any HTTP 422 response as “repo already exists” when ReuseExisting is true. GitHub also returns 422 for other validation failures (invalid name, missing permissions, etc.), so this can mask the real error and replace it with a confusing “already exists but could not be fetched”. Consider only taking the reuse path when the 422 payload explicitly indicates already_exists (or attempt GET and, if it’s 404, return the original 422 error).
| if getErr != nil { | |
| return nil, false, fmt.Errorf("repository already exists but could not be fetched: %w", getErr) | |
| } | |
| return existing, false, nil | |
| if getErr == nil { | |
| return existing, false, nil | |
| } | |
| if getAPIErr, ok := getErr.(*Error); ok && getAPIErr.StatusCode == http.StatusNotFound { | |
| return nil, false, statusErr | |
| } | |
| return nil, false, fmt.Errorf("repository already exists but could not be fetched: %w", getErr) |
Add a docs-first landing refresh, Homebrew tap guidance, tagged-release tap sync, and a new publish command that creates GitHub repositories for generated CLIs and bootstraps CI/release lifecycle files.
Constraint: Cross-repo Homebrew sync requires a dedicated token because GITHUB_TOKEN cannot push to a separate tap repository
Rejected: Require gh CLI for generated CLI publishing | would add an external runtime dependency to climate
Confidence: high
Scope-risk: moderate
Directive: Only overwrite lifecycle files while they still carry the climate-managed marker; preserve user-edited files otherwise
Tested: go test ./...; go run ./cmd/climate --help; go run ./cmd/climate publish --help; brew audit --strict disk0Dancer/tap/climate; brew install --build-from-source --dry-run climate
Not-tested: End-to-end tagged release syncing the Homebrew tap from GitHub Actions before HOMEBREW_TAP_TOKEN is configured