Skip to content

Automate climate publication from generated CLI to release surfaces#1

Merged
disk0Dancer merged 1 commit into
mainfrom
feature-publish-generated-clis
Apr 17, 2026
Merged

Automate climate publication from generated CLI to release surfaces#1
disk0Dancer merged 1 commit into
mainfrom
feature-publish-generated-clis

Conversation

@disk0Dancer
Copy link
Copy Markdown
Owner

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

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
Copilot AI review requested due to automatic review settings April 17, 2026 12:42
@disk0Dancer disk0Dancer merged commit d56d940 into main Apr 17, 2026
2 checks passed
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 publish to 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.

Comment on lines +81 to +92
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

Copy link

Copilot AI Apr 17, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Suggested change
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:

Copilot uses AI. Check for mistakes.
Comment on lines +12 to +28
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
Copy link

Copilot AI Apr 17, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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).

Copilot uses AI. Check for mistakes.
Comment on lines +54 to +61
if data, err := os.ReadFile(path); err == nil {
if !strings.Contains(string(data), marker) {
return false, nil
}
if string(data) == content {
return false, nil
}
}
Copy link

Copilot AI Apr 17, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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)).

Copilot uses AI. Check for mistakes.
Comment on lines +92 to +96
## Install

~~~bash
go install github.com/%s@latest
~~~
Copy link

Copilot AI Apr 17, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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).

Copilot uses AI. Check for mistakes.
if err != nil {
return nil, err
}
if repo.DefaultBranch != "" {
Copy link

Copilot AI Apr 17, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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).

Suggested change
if repo.DefaultBranch != "" {
if !created && repo.DefaultBranch != "" {

Copilot uses AI. Check for mistakes.
Comment on lines +144 to +146
remoteURL := repo.SSHURL
if remoteURL == "" {
remoteURL = repo.CloneURL
Copy link

Copilot AI Apr 17, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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).

Suggested change
remoteURL := repo.SSHURL
if remoteURL == "" {
remoteURL = repo.CloneURL
remoteURL := repo.CloneURL
if remoteURL == "" {
remoteURL = repo.SSHURL

Copilot uses AI. Check for mistakes.
Comment on lines +69 to +78
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,
})
Copy link

Copilot AI Apr 17, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot uses AI. Check for mistakes.
Comment on lines +126 to +129
if getErr != nil {
return nil, false, fmt.Errorf("repository already exists but could not be fetched: %w", getErr)
}
return existing, false, nil
Copy link

Copilot AI Apr 17, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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).

Suggested change
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)

Copilot uses AI. Check for mistakes.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants