From c8dae3e06b9323253d25576cfd60ad62fddd7da7 Mon Sep 17 00:00:00 2001 From: Paul Thurlow Date: Wed, 25 Mar 2026 11:54:59 -0700 Subject: [PATCH 1/2] Add completions command and include in brew formula --- .github/workflows/publish-homebrew.yml | 65 ++++++++++++++++++++++++++ .github/workflows/release.yml | 54 +++++---------------- Cargo.lock | 10 ++++ Cargo.toml | 1 + dist-workspace.toml | 5 +- src/command.rs | 7 +++ src/main.rs | 7 +++ 7 files changed, 103 insertions(+), 46 deletions(-) create mode 100644 .github/workflows/publish-homebrew.yml diff --git a/.github/workflows/publish-homebrew.yml b/.github/workflows/publish-homebrew.yml new file mode 100644 index 0000000..7cc0d6c --- /dev/null +++ b/.github/workflows/publish-homebrew.yml @@ -0,0 +1,65 @@ +name: Publish Homebrew Formula + +on: + workflow_call: + inputs: + plan: + required: true + type: string + +jobs: + publish-homebrew-formula: + runs-on: ubuntu-22.04 + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + PLAN: ${{ inputs.plan }} + GITHUB_USER: "axo bot" + GITHUB_EMAIL: "admin+bot@axo.dev" + if: ${{ !fromJson(inputs.plan).announcement_is_prerelease || fromJson(inputs.plan).publish_prereleases }} + steps: + - uses: actions/checkout@v6 + with: + persist-credentials: true + repository: "hotdata-dev/homebrew-tap" + token: ${{ secrets.HOMEBREW_TAP_TOKEN }} + + - name: Fetch homebrew formulae + uses: actions/download-artifact@v7 + with: + pattern: artifacts-* + path: Formula/ + merge-multiple: true + + - name: Patch and commit formula files + run: | + git config --global user.name "${GITHUB_USER}" + git config --global user.email "${GITHUB_EMAIL}" + + for release in $(echo "$PLAN" | jq --compact-output '.releases[] | select([.artifacts[] | endswith(".rb")] | any)'); do + filename=$(echo "$release" | jq '.artifacts[] | select(endswith(".rb"))' --raw-output) + name=$(echo "$filename" | sed "s/\.rb$//") + version=$(echo "$release" | jq .app_version --raw-output) + + export PATH="/home/linuxbrew/.linuxbrew/bin:$PATH" + brew update + brew style --except-cops FormulaAudit/Homepage,FormulaAudit/Desc,FormulaAuditStrict --fix "Formula/${filename}" || true + + # Add shell completions generation if not already present + if ! grep -q 'generate_completions_from_executable' "Formula/${filename}"; then + sed -i '/bin\.install "hotdata"/a\ generate_completions_from_executable(bin/"hotdata", "completions")' "Formula/${filename}" + fi + + git add "Formula/${filename}" + git commit -m "${name} ${version}" + done + git push + + - name: Remove .rb from GitHub Release assets + run: | + TAG=$(echo "$PLAN" | jq -r '.announcement_tag') + if [ -z "$TAG" ] || [ "$TAG" = "null" ]; then + exit 0 + fi + for asset in $(gh release view "$TAG" --repo hotdata-dev/hotdata-cli --json assets -q '.assets[].name' | grep '\.rb$'); do + gh release delete-asset "$TAG" "$asset" --repo hotdata-dev/hotdata-cli --yes + done diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index ce1a32b..9075f88 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -278,61 +278,29 @@ jobs: gh release create "${{ needs.plan.outputs.tag }}" --target "$RELEASE_COMMIT" $PRERELEASE_FLAG --title "$ANNOUNCEMENT_TITLE" --notes-file "$RUNNER_TEMP/notes.txt" artifacts/* - publish-homebrew-formula: + custom-publish-homebrew: needs: - plan - host - runs-on: "ubuntu-22.04" - env: - GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} - PLAN: ${{ needs.plan.outputs.val }} - GITHUB_USER: "axo bot" - GITHUB_EMAIL: "admin+bot@axo.dev" if: ${{ !fromJson(needs.plan.outputs.val).announcement_is_prerelease || fromJson(needs.plan.outputs.val).publish_prereleases }} - steps: - - uses: actions/checkout@v6 - with: - persist-credentials: true - repository: "hotdata-dev/homebrew-tap" - token: ${{ secrets.HOMEBREW_TAP_TOKEN }} - # So we have access to the formula - - name: Fetch homebrew formulae - uses: actions/download-artifact@v7 - with: - pattern: artifacts-* - path: Formula/ - merge-multiple: true - # This is extra complex because you can make your Formula name not match your app name - # so we need to find releases with a *.rb file, and publish with that filename. - - name: Commit formula files - run: | - git config --global user.name "${GITHUB_USER}" - git config --global user.email "${GITHUB_EMAIL}" - - for release in $(echo "$PLAN" | jq --compact-output '.releases[] | select([.artifacts[] | endswith(".rb")] | any)'); do - filename=$(echo "$release" | jq '.artifacts[] | select(endswith(".rb"))' --raw-output) - name=$(echo "$filename" | sed "s/\.rb$//") - version=$(echo "$release" | jq .app_version --raw-output) - - export PATH="/home/linuxbrew/.linuxbrew/bin:$PATH" - brew update - # We avoid reformatting user-provided data such as the app description and homepage. - brew style --except-cops FormulaAudit/Homepage,FormulaAudit/Desc,FormulaAuditStrict --fix "Formula/${filename}" || true - - git add "Formula/${filename}" - git commit -m "${name} ${version}" - done - git push + uses: ./.github/workflows/publish-homebrew.yml + with: + plan: ${{ needs.plan.outputs.val }} + secrets: inherit + # publish jobs get escalated permissions + permissions: + "id-token": "write" + "packages": "write" announce: needs: - plan - host - - publish-homebrew-formula + - custom-publish-homebrew # use "always() && ..." to allow us to wait for all publish jobs while # still allowing individual publish jobs to skip themselves (for prereleases). # "host" however must run to completion, no skipping allowed! - if: ${{ always() && needs.host.result == 'success' && (needs.publish-homebrew-formula.result == 'skipped' || needs.publish-homebrew-formula.result == 'success') }} + if: ${{ always() && needs.host.result == 'success' && (needs.custom-publish-homebrew.result == 'skipped' || needs.custom-publish-homebrew.result == 'success') }} runs-on: "ubuntu-22.04" env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/Cargo.lock b/Cargo.lock index 0c9870e..23799da 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -190,6 +190,15 @@ dependencies = [ "strsim", ] +[[package]] +name = "clap_complete" +version = "4.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19c9f1dde76b736e3681f28cec9d5a61299cbaae0fce80a68e43724ad56031eb" +dependencies = [ + "clap", +] + [[package]] name = "clap_derive" version = "4.5.55" @@ -657,6 +666,7 @@ dependencies = [ "anstyle", "base64", "clap", + "clap_complete", "crossterm 0.28.1", "directories", "dotenvy", diff --git a/Cargo.toml b/Cargo.toml index 0efcff1..f94f357 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -27,6 +27,7 @@ rand = "0.8" sha2 = "0.10" tiny_http = "0.12" tabled = { version = "0.20", features = ["ansi"] } +clap_complete = "4" inquire = "0.9.4" indicatif = "0.17" nix = { version = "0.29", features = ["fs"] } diff --git a/dist-workspace.toml b/dist-workspace.toml index 4ee8b3a..d99e1bb 100644 --- a/dist-workspace.toml +++ b/dist-workspace.toml @@ -11,13 +11,12 @@ ci = "github" installers = ["shell", "homebrew"] # Target platforms to build apps for (Rust target-triple syntax) targets = ["aarch64-apple-darwin", "aarch64-unknown-linux-gnu", "x86_64-apple-darwin", "x86_64-unknown-linux-gnu"] -# A GitHub repo to push Homebrew formulas to -tap = "hotdata-dev/homebrew-tap" +# Customize the Homebrew formula name formula = "cli" # Path that installers should place binaries in install-path = "~/.hotdata/cli" # Publish jobs to run in CI -publish-jobs = ["homebrew"] +publish-jobs = ["./publish-homebrew"] # Whether to install an updater program install-updater = false diff --git a/src/command.rs b/src/command.rs index b35e3c9..0c02b9e 100644 --- a/src/command.rs +++ b/src/command.rs @@ -104,6 +104,13 @@ pub enum Commands { #[command(subcommand)] command: Option, }, + + /// Generate shell completions + Completions { + /// Shell to generate completions for + #[arg(value_parser = ["bash", "zsh", "fish"])] + shell: String, + }, } #[derive(Subcommand)] diff --git a/src/main.rs b/src/main.rs index 6b20eda..3aa8426 100644 --- a/src/main.rs +++ b/src/main.rs @@ -195,6 +195,13 @@ fn main() { } } } + Commands::Completions { shell } => { + use clap::CommandFactory; + use clap_complete::{Shell, generate}; + let shell: Shell = shell.parse().unwrap(); + let mut cmd = Cli::command(); + generate(shell, &mut cmd, "hotdata", &mut std::io::stdout()); + } }, } } From 23794555b275ed2e0a3964a4b2e985652ea19e62 Mon Sep 17 00:00:00 2001 From: Paul Thurlow Date: Wed, 25 Mar 2026 12:23:52 -0700 Subject: [PATCH 2/2] clean up completions shell types for clap parsing --- src/command.rs | 21 +++++++++++++++++++-- src/main.rs | 4 ++-- 2 files changed, 21 insertions(+), 4 deletions(-) diff --git a/src/command.rs b/src/command.rs index 0c02b9e..b1f8d78 100644 --- a/src/command.rs +++ b/src/command.rs @@ -108,11 +108,28 @@ pub enum Commands { /// Generate shell completions Completions { /// Shell to generate completions for - #[arg(value_parser = ["bash", "zsh", "fish"])] - shell: String, + #[arg(value_enum)] + shell: ShellChoice, }, } +#[derive(Clone, clap::ValueEnum)] +pub enum ShellChoice { + Bash, + Zsh, + Fish, +} + +impl From for clap_complete::Shell { + fn from(s: ShellChoice) -> Self { + match s { + ShellChoice::Bash => clap_complete::Shell::Bash, + ShellChoice::Zsh => clap_complete::Shell::Zsh, + ShellChoice::Fish => clap_complete::Shell::Fish, + } + } +} + #[derive(Subcommand)] pub enum AuthCommands { /// Remove authentication for a profile diff --git a/src/main.rs b/src/main.rs index 3aa8426..94470ec 100644 --- a/src/main.rs +++ b/src/main.rs @@ -197,8 +197,8 @@ fn main() { } Commands::Completions { shell } => { use clap::CommandFactory; - use clap_complete::{Shell, generate}; - let shell: Shell = shell.parse().unwrap(); + use clap_complete::generate; + let shell: clap_complete::Shell = shell.into(); let mut cmd = Cli::command(); generate(shell, &mut cmd, "hotdata", &mut std::io::stdout()); }