ratatui_ruby-devtools 0.1.3 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (52) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +11 -0
  3. data/LICENSES/BSD-2-Clause.txt +9 -0
  4. data/README.md +1 -1
  5. data/exe/hbs +149 -209
  6. data/exe/scaffold +169 -282
  7. data/lib/ratatui_ruby/devtools/tasks/lint.rake +3 -7
  8. data/lib/ratatui_ruby/devtools/tasks/release/ci_run.rb +44 -0
  9. data/lib/ratatui_ruby/devtools/tasks/release/github_cli.rb +36 -0
  10. data/lib/ratatui_ruby/devtools/tasks/release/native_gem_version.rb +70 -0
  11. data/lib/ratatui_ruby/devtools/tasks/release/platform_gem.rb +63 -0
  12. data/lib/ratatui_ruby/devtools/tasks/release/versioned_binary.rb +36 -0
  13. data/lib/ratatui_ruby/devtools/tasks/release.rake +87 -0
  14. data/lib/ratatui_ruby/devtools/tasks/resources/rubies.yml +1 -1
  15. data/lib/ratatui_ruby/devtools/templates/.github/workflows/build-gems.yml.erb +124 -0
  16. data/lib/ratatui_ruby/devtools/templates/.github/workflows/ci.yml.erb +77 -0
  17. data/lib/ratatui_ruby/devtools/templates/.gitignore.erb +2 -0
  18. data/lib/ratatui_ruby/devtools/templates/.rubocop.yml.erb +6 -4
  19. data/lib/ratatui_ruby/devtools/templates/AGENTS.md.erb +101 -28
  20. data/lib/ratatui_ruby/devtools/templates/CODE_OF_CONDUCT.md.erb +44 -0
  21. data/lib/ratatui_ruby/devtools/templates/CONTRIBUTING.md.erb +80 -0
  22. data/lib/ratatui_ruby/devtools/templates/Gemfile.erb +2 -2
  23. data/lib/ratatui_ruby/devtools/templates/README.md.erb +46 -13
  24. data/lib/ratatui_ruby/devtools/templates/REUSE.toml.erb +2 -2
  25. data/lib/ratatui_ruby/devtools/templates/Rakefile.erb +4 -4
  26. data/lib/ratatui_ruby/devtools/templates/bin/setup.erb +21 -11
  27. data/lib/ratatui_ruby/devtools/templates/bin/setup.ps1.erb +105 -0
  28. data/lib/ratatui_ruby/devtools/templates/doc/custom.css.erb +3 -3
  29. data/lib/ratatui_ruby/devtools/templates/doc/getting_started/quickstart.md.erb +3 -3
  30. data/lib/ratatui_ruby/devtools/templates/doc/index.md.erb +2 -2
  31. data/lib/ratatui_ruby/devtools/templates/ext/.cargo/config.toml.erb +15 -0
  32. data/lib/ratatui_ruby/devtools/templates/ext/.gitignore.erb +6 -0
  33. data/lib/ratatui_ruby/devtools/templates/ext/Cargo.toml.erb +20 -0
  34. data/lib/ratatui_ruby/devtools/templates/ext/clippy.toml.erb +9 -0
  35. data/lib/ratatui_ruby/devtools/templates/ext/extconf.rb.erb +23 -0
  36. data/lib/ratatui_ruby/devtools/templates/ext/src/lib.rs.erb +13 -0
  37. data/lib/ratatui_ruby/devtools/templates/gemspec.erb +21 -14
  38. data/lib/ratatui_ruby/devtools/templates/lib/main.rb.erb +19 -0
  39. data/lib/ratatui_ruby/devtools/templates/lib/test_helper.rb.erb +26 -0
  40. data/lib/ratatui_ruby/devtools/templates/lib/version.rb.erb +10 -0
  41. data/lib/ratatui_ruby/devtools/templates/mise.toml.erb +3 -3
  42. data/lib/ratatui_ruby/devtools/templates/tasks/example_viewer.html.erb +23 -23
  43. data/lib/ratatui_ruby/devtools/templates/tasks/resources/index.html.erb +15 -16
  44. data/lib/ratatui_ruby/devtools/templates/tasks/resources/rubies.yml.erb +5 -6
  45. data/lib/ratatui_ruby/devtools/templates/test/test_helper.rb.erb +15 -0
  46. data/lib/ratatui_ruby/devtools/version.rb +1 -1
  47. data/lib/ratatui_ruby/devtools.rb +2 -2
  48. metadata +24 -5
  49. data/lib/ratatui_ruby/devtools/tasks/resources/build.yml.erb +0 -54
  50. data/lib/ratatui_ruby/devtools/tasks/sourcehut.rake +0 -94
  51. data/lib/ratatui_ruby/devtools/templates/tasks/resources/build.yml.erb +0 -62
  52. /data/lib/ratatui_ruby/devtools/templates/{vendor/goodcop/base.yml → lib/rubocop.yml} +0 -0
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 178d6ede6d1e403e0bfe2cf20840e22bbfbd572d656fd99bb0dd4993c37f2c28
4
- data.tar.gz: b5503f3d81a218ca9eca8f1216287ee73cdb3296afcd4c0fc7ab69708baae11b
3
+ metadata.gz: 9c8afb78c4a4395a9d4e4db32fcec1e83e6c1b495d6e7f5928c1b12d50aa775b
4
+ data.tar.gz: afb0898067b88c44a135e9f56053b7b265b82cc14ace609453fdbaaec0f36461
5
5
  SHA512:
6
- metadata.gz: 9512eab47f5280108963e898bb4c7e903a834782ade183ef130e19bffd7156ce98362c1faa2e1496b9675fef047fbf72c63c542b63979b545f9b09c63942b121
7
- data.tar.gz: 230dcc59e440c07f5b5fae2bbb60decd19f0ec2b2a512b6c2eb024127f518b3838ff903ab17e14c11d04b7594fe650c6ee23499fc43b6f28efdc72a9d0c73fa9
6
+ metadata.gz: aed171dd40f1a07454b7ec6f82d404cda4847335b1b899d7738ec83346aedd7a4b107c3df71e08b115d0fd81081673dc4438c7f144c80fb2b9eb067112a6ab3b
7
+ data.tar.gz: f0e0abce6a99bf42a4891e1cb1e0bdbe0739d4a0e9bec1473081d7f6080482a3e3fa54d090efe40044d1c82b3b0a589fd367428d920053bb112e36cb9bf8af7b
data/CHANGELOG.md CHANGED
@@ -21,6 +21,16 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
21
21
 
22
22
  ### Removed
23
23
 
24
+ ## [0.2.0] - 2026-02-16
25
+
26
+ ### Added
27
+
28
+ ### Changed
29
+
30
+ ### Fixed
31
+
32
+ ### Removed
33
+
24
34
  ## [0.1.3] - 2026-02-02
25
35
 
26
36
  ### Added
@@ -76,6 +86,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
76
86
  ### Removed
77
87
 
78
88
  [Unreleased]: https://git.sr.ht/~kerrick/ratatui_ruby-devtools/refs/HEAD
89
+ [0.2.0]: https://git.sr.ht/~kerrick/ratatui_ruby-devtools/refs/v0.2.0
79
90
  [0.1.3]: https://git.sr.ht/~kerrick/ratatui_ruby-devtools/refs/v0.1.3
80
91
  [0.1.2]: https://git.sr.ht/~kerrick/ratatui_ruby-devtools/refs/v0.1.2
81
92
  [0.1.1]: https://git.sr.ht/~kerrick/ratatui_ruby-devtools/refs/v0.1.1
@@ -0,0 +1,9 @@
1
+ Copyright (c) <year> <owner>
2
+
3
+ Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:
4
+
5
+ 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.
6
+
7
+ 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution.
8
+
9
+ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
data/README.md CHANGED
@@ -23,7 +23,7 @@ This is a **non-runtime** gem. It provides build tooling—not application featu
23
23
 
24
24
  ### The Ecosystem
25
25
 
26
- **RatatuiRuby:** [Core engine](https://git.sr.ht/~kerrick/ratatui_ruby) • **Tea:** [MVU architecture](https://git.sr.ht/~kerrick/ratatui_ruby-tea) • **Kit:** [Component architecture](https://git.sr.ht/~kerrick/ratatui_ruby-kit) (Planned) • **DSL:** [Glimmer syntax](https://sr.ht/~kerrick/ratatui_ruby/#chapter-4-the-syntax) (Planned) • **Framework:** [Omakase framework](https://git.sr.ht/~kerrick/ratatui_ruby-framework) (Planned) • **UI:** [Polished widgets](https://git.sr.ht/~kerrick/ratatui_ruby-ui) (Planned) • **UI Pro:** [More polished widgets](https://sr.ht/~kerrick/ratatui_ruby#chapter-6-licensing) (Planned)
26
+ **RatatuiRuby:** [Core engine](https://git.sr.ht/~kerrick/ratatui_ruby) • **Tea:** [MVU architecture](https://github.com/setdef/Rooibos) • **Kit:** [Component architecture](https://git.sr.ht/~kerrick/ratatui_ruby-kit) (Planned) • **DSL:** [Glimmer syntax](https://sr.ht/~kerrick/ratatui_ruby/#chapter-4-the-syntax) (Planned) • **Framework:** [Omakase framework](https://git.sr.ht/~kerrick/ratatui_ruby-framework) (Planned) • **UI:** [Polished widgets](https://git.sr.ht/~kerrick/ratatui_ruby-ui) (Planned) • **UI Pro:** [More polished widgets](https://sr.ht/~kerrick/ratatui_ruby#chapter-6-licensing) (Planned)
27
27
 
28
28
  ### For App Developers
29
29
 
data/exe/hbs CHANGED
@@ -2,20 +2,20 @@
2
2
  # frozen_string_literal: true
3
3
 
4
4
  #--
5
- # SPDX-FileCopyrightText: 2025 Kerrick Long <me@kerricklong.com>
5
+ # SPDX-FileCopyrightText: 2026 Kerrick Long <me@kerricklong.com>
6
6
  # SPDX-License-Identifier: AGPL-3.0-or-later
7
7
  #++
8
8
 
9
9
  ####
10
10
  # ======================================================================
11
- # hbs - watch SourceHut builds for current commit
11
+ # hbs - Head Build Status: watch GitHub Actions for HEAD commit
12
12
  # ======================================================================
13
13
  #
14
14
  # SYNOPSIS
15
15
  # bin/hbs [-N|--no-wait] [-b|--builds N] [-y|--yes] [-S|--no-say]
16
16
  #
17
17
  # DESCRIPTION
18
- # Check SourceHut builds for the current HEAD commit and wait for
18
+ # Check GitHub Actions for the current HEAD commit and wait for
19
19
  # them to complete. Announces result via macOS text-to-speech.
20
20
  #
21
21
  # Fetches from origin first. If HEAD has not been pushed, prompts
@@ -28,7 +28,7 @@
28
28
  # OPTIONS
29
29
  # -N, --no-wait Exit 1 immediately if no builds exist for HEAD.
30
30
  # -b, --builds N Number of builds to watch (default: count of
31
- # .yml files in .builds/ directory).
31
+ # workflows matching current branch).
32
32
  # -y, --yes Auto-confirm all prompts (non-interactive).
33
33
  # -S, --no-say Disable text-to-speech announcements.
34
34
  #
@@ -47,7 +47,7 @@
47
47
  # bin/hbs -N
48
48
  #
49
49
  # DEPENDENCIES
50
- # hut SourceHut CLI (https://sr.ht/~xenrox/hut/)
50
+ # gh GitHub CLI (https://cli.github.com)
51
51
  # git To get current HEAD commit
52
52
  # say macOS text-to-speech
53
53
  #
@@ -55,284 +55,204 @@
55
55
 
56
56
  require "optparse"
57
57
  require "open3"
58
+ require "json"
59
+ require "yaml"
58
60
  require "rdoc"
59
61
 
60
- # Represents a git commit for build lookups.
61
- #
62
- # SourceHut builds are identified by commit SHA. Determining whether HEAD is
63
- # pushed and how far ahead we are requires git commands. This class wraps
64
- # those queries.
65
- #
66
- # [short_sha] The 7-character short SHA.
67
- Commit = Data.define(:short_sha) do
68
- # Returns the current HEAD commit.
62
+ # A git commit identified by its full SHA.
63
+ class Commit < Data.define(:sha)
64
+ ##
65
+ # The first seven characters of the SHA.
66
+ def short_sha
67
+ sha[0, 7]
68
+ end
69
+
70
+ # Returns the commit at HEAD.
69
71
  def self.head
70
- sha = `git rev-parse --short HEAD 2>/dev/null`.strip
72
+ sha = `git rev-parse HEAD 2>/dev/null`.strip
71
73
  raise "Not in a git repository" if sha.empty?
72
74
 
73
- new(short_sha: sha)
75
+ new(sha:)
76
+ end
77
+
78
+ # The upstream tracking ref, falling back to +origin/trunk+.
79
+ def self.upstream_ref
80
+ # Get the upstream tracking branch for the current branch (e.g., origin/release/1.0)
81
+ ref = `git rev-parse --abbrev-ref --symbolic-full-name @{u} 2>/dev/null`.strip
82
+ ref.empty? ? "origin/trunk" : ref # Fall back to origin/trunk if no upstream
74
83
  end
75
84
 
76
- # Returns the origin/trunk HEAD commit, or nil if not available.
85
+ # Returns the commit at the upstream ref, or +nil+ if unavailable.
77
86
  def self.origin_head
78
- sha = `git rev-parse --short origin/trunk 2>/dev/null`.strip
87
+ sha = `git rev-parse #{upstream_ref} 2>/dev/null`.strip
79
88
  return nil if sha.empty?
80
89
 
81
- new(short_sha: sha)
90
+ new(sha:)
82
91
  end
83
92
 
84
- # Checks whether this commit exists on the remote.
85
- #
86
- # Watching builds for unpushed commits wastes time. The remote hasn't run
87
- # CI for code it hasn't received. Check this before querying SourceHut.
93
+ # Whether this commit is an ancestor of the upstream ref.
88
94
  def pushed?
89
- system("git", "merge-base", "--is-ancestor", short_sha, "origin/trunk",
95
+ system("git", "merge-base", "--is-ancestor", sha, self.class.upstream_ref,
90
96
  out: File::NULL, err: File::NULL)
91
97
  end
92
98
 
93
- # Counts commits between local HEAD and origin.
94
- #
95
- # Users need to know how far ahead they are. A large number suggests pushing
96
- # before watching builds. Use this to inform the user about sync status.
99
+ # Number of commits ahead of the upstream ref.
97
100
  def commits_ahead
98
- `git rev-list --count origin/trunk..HEAD 2>/dev/null`.strip.to_i
101
+ `git rev-list --count #{self.class.upstream_ref}..HEAD 2>/dev/null`.strip.to_i
99
102
  end
100
103
  end
101
104
 
102
- # Represents a single SourceHut build job.
103
- #
104
- # Build jobs have IDs, statuses, and output logs. Querying status, fetching
105
- # output, and refreshing require hut CLI calls. This class wraps those.
106
- #
107
- # [id] The numeric build ID.
108
- # [status] The current status string (RUNNING, SUCCESS, FAILED, etc.)
109
- Build = Data.define(:id, :status) do
110
- # Indicates whether the build is still in progress.
111
- #
112
- # Completed builds have final results. Running builds need polling. Check
113
- # this to determine whether to wait or report.
114
- def running?
115
- %w[RUNNING PENDING QUEUED].include?(status)
105
+ # A single GitHub Actions workflow run.
106
+ class Build < Data.define(:id, :name, :status, :conclusion, :url)
107
+ ##
108
+ # Constructs a Build from a parsed GitHub API JSON hash.
109
+ def self.from_json(data)
110
+ new(
111
+ id: data.fetch("databaseId"),
112
+ name: data.fetch("workflowName"),
113
+ status: data.fetch("status"),
114
+ conclusion: data.fetch("conclusion") || "",
115
+ url: data.fetch("url")
116
+ )
116
117
  end
117
118
 
118
- # Indicates whether the build failed.
119
- #
120
- # A failed build blocks releases. Success requires all builds to pass. Check
121
- # this to gate release actions.
122
- def failed?
123
- status == "FAILED"
119
+ # Whether this run is still in progress.
120
+ def running?
121
+ %w[in_progress queued requested waiting pending].include?(status)
124
122
  end
125
123
 
126
- # Generates the SourceHut web URL for this build.
127
- #
128
- # Users need direct links to investigate failures or review logs. Use this
129
- # to create clickable references in output.
130
- def url
131
- "https://builds.sr.ht/~kerrick/job/#{id}"
124
+ # Whether this run completed without success.
125
+ def failed?
126
+ status == "completed" && conclusion != "success"
132
127
  end
133
128
 
134
- # Fetches the full build output from SourceHut.
135
- #
136
- # Debugging failures requires log access. The hut CLI retrieves logs. Use
137
- # this to display build details or extract specific information.
129
+ # The full text output from +gh run view+.
138
130
  def output
139
- out, = Open3.capture2("hut", "builds", "show", id.to_s)
131
+ out, = Open3.capture2("gh", "run", "view", id.to_s)
140
132
  out
141
133
  end
142
134
 
143
- # Extracts displayable log lines from build output.
144
- #
145
- # Build output includes headers. Display needs just the log content. Use
146
- # this when showing logs in a formatted UI.
135
+ # Output lines with trailing whitespace stripped.
147
136
  def content_lines
148
- # Skip the URL line (first line) since it becomes the title
149
- output.lines.drop(1).map(&:rstrip)
137
+ output.lines.map(&:rstrip)
150
138
  end
151
139
 
152
- # Extracts the most recent timestamp from the build log.
153
- #
154
- # Long-running builds need progress indication. The latest timestamp shows
155
- # recency. Use this to label build sections or detect stalled jobs.
140
+ # The most recent +updatedAt+ timestamp from the GitHub API.
156
141
  def last_timestamp
157
- # Extract the latest timestamp from the runtime log (format: YYYY/MM/DD HH:MM:SS)
158
- timestamps = output.scan(%r{\d{4}/\d{2}/\d{2} \d{2}:\d{2}:\d{2}})
159
- timestamps.last
142
+ out, st = Open3.capture2("gh", "run", "view", id.to_s, "--json", "updatedAt", "--jq", ".updatedAt")
143
+ return nil unless st.success?
144
+ ts = out.strip
145
+ ts.empty? ? nil : ts
160
146
  end
161
147
 
162
- # Queries SourceHut for the current build status.
163
- #
164
- # Build status changes over time. Stale data misleads users. Call this
165
- # periodically to get fresh status and detect completion.
148
+ # Returns a new Build with fresh status from the API.
166
149
  def refresh
167
- out, = Open3.capture2("hut", "builds", "show", id.to_s)
168
- new_status = out.lines.first(3).join.match(/RUNNING|SUCCESS|FAILED|PENDING|QUEUED/)&.[](0) || status
169
- Build.new(id:, status: new_status)
150
+ out, st = Open3.capture2("gh", "run", "view", id.to_s, "--json", "databaseId,workflowName,status,conclusion,url")
151
+ return self unless st.success?
152
+ Build.from_json(JSON.parse(out))
170
153
  end
171
154
  end
172
155
 
173
- # A collection of SourceHut builds.
174
- #
175
- # Watching builds requires finding them by commit, checking statuses, and
176
- # refreshing as a group. Individual Build objects lack this context.
177
- #
178
- # This class manages collections. It queries builds by commit or status.
179
- # It refreshes all builds together. It reports aggregate status.
180
- #
181
- # [builds] Array of Build objects.
182
- BuildSet = Data.define(:builds) do
183
- # Finds builds for a specific commit.
184
- #
185
- # [commit] A Commit object to search for.
156
+ # A collection of GitHub Actions workflow runs.
157
+ class BuildSet < Data.define(:builds)
158
+ ##
159
+ # Queries runs for a specific commit.
186
160
  def self.for_commit(commit)
187
- output, = Open3.capture2("hut", "builds", "list")
188
- ids = output.lines
189
- .each_cons(6)
190
- .select { |chunk| chunk.any? { |l| l.include?(commit.short_sha) } }
191
- .filter_map { |chunk| chunk.first[/#(\d+)/, 1]&.to_i }
192
- .uniq
193
-
194
- builds = ids.map { |id| Build.new(id:, status: fetch_status(id)) }
195
- new(builds:)
196
- end
197
-
198
- # Finds currently running builds.
199
- def self.running
200
- output, = Open3.capture2("hut", "builds", "list")
201
- ids = output.lines
202
- .select { |l| l.match?(/RUNNING|PENDING|QUEUED/) }
203
- .filter_map { |l| l[/#(\d+)/, 1]&.to_i }
204
- .uniq
205
-
206
- builds = ids.map { |id| Build.new(id:, status: fetch_status(id)) }
207
- new(builds:)
208
- end
209
-
210
- # Finds the most recent builds.
211
- #
212
- # [count] Number of builds to return.
213
- def self.latest(count = 4)
214
- output, = Open3.capture2("hut", "builds", "list")
215
- ids = output.lines
216
- .filter_map { |l| l[/#(\d+)/, 1]&.to_i }
217
- .uniq
218
- .first(count)
219
-
220
- builds = ids.map { |id| Build.new(id:, status: fetch_status(id)) }
221
- new(builds:)
222
- end
223
-
224
- # Finds builds with a specific status.
225
- #
226
- # [target_status] The status to search for (FAILED, SUCCESS).
227
- # [count] Number of builds per set.
161
+ out, status = Open3.capture2(
162
+ "gh", "run", "list",
163
+ "--commit", commit.sha,
164
+ "--json", "databaseId,workflowName,status,conclusion,url",
165
+ "--limit", "20"
166
+ )
167
+ return new(builds: []) unless status.success?
168
+
169
+ data = JSON.parse(out)
170
+ new(builds: data.map { |d| Build.from_json(d) })
171
+ end
172
+
173
+ # Returns the most recent runs.
174
+ def self.latest(count = 12)
175
+ out, status = Open3.capture2(
176
+ "gh", "run", "list",
177
+ "--json", "databaseId,workflowName,status,conclusion,url",
178
+ "--limit", count.to_s
179
+ )
180
+ return new(builds: []) unless status.success?
181
+
182
+ data = JSON.parse(out)
183
+ new(builds: data.map { |d| Build.from_json(d) })
184
+ end
185
+
186
+ # Finds the first batch of runs matching +target_status+.
228
187
  def self.with_status(target_status, count = 4)
229
- output, = Open3.capture2("hut", "builds", "list")
230
- all_ids = output.lines
231
- .filter_map { |l| l[/#(\d+)/, 1]&.to_i }
232
- .uniq
188
+ out, status = Open3.capture2(
189
+ "gh", "run", "list",
190
+ "--json", "databaseId,workflowName,status,conclusion,url",
191
+ "--limit", "50"
192
+ )
193
+ return new(builds: []) unless status.success?
194
+
195
+ all = JSON.parse(out).map { |d| Build.from_json(d) }
233
196
 
234
197
  # Group builds into sets of `count` and find the first set with any matching status
235
- all_ids.each_slice(count) do |set_ids|
236
- builds = set_ids.map { |id| Build.new(id:, status: fetch_status(id)) }
198
+ all.each_slice(count) do |set_builds|
237
199
  has_match = case target_status
238
- when "FAILED" then builds.any?(&:failed?)
239
- when "SUCCESS" then builds.none?(&:failed?)
200
+ when "FAILED" then set_builds.any?(&:failed?)
201
+ when "SUCCESS" then set_builds.none?(&:failed?)
240
202
  end
241
- return new(builds:) if has_match
203
+ return new(builds: set_builds) if has_match
242
204
  end
243
205
 
244
206
  new(builds: [])
245
207
  end
246
208
 
247
- # Queries the status of a specific build by ID.
248
- #
249
- # Build creation returns only the ID. Status requires a separate query. Use
250
- # this when constructing Build objects from ID lists.
251
- #
252
- # [id] The build ID.
253
- def self.fetch_status(id)
254
- output, = Open3.capture2("hut", "builds", "show", id.to_s)
255
- output.lines.first(3).join.match(/RUNNING|SUCCESS|FAILED|PENDING|QUEUED/)&.[](0) || "UNKNOWN"
256
- end
257
-
258
- # Indicates whether the collection has no builds.
259
- #
260
- # A missing commit has no builds. Watching nothing wastes time. Check this
261
- # to abort early or prompt the user about missing CI.
209
+ # Whether or not any builds were found.
262
210
  def empty?
263
211
  builds.empty?
264
212
  end
265
213
 
266
- # Indicates whether any build is still in progress.
267
- #
268
- # Completed builds have final results. Running builds need polling. Check
269
- # this to determine whether to wait or report.
214
+ # Whether any build is still in progress.
270
215
  def any_running?
271
216
  builds.any?(&:running?)
272
217
  end
273
218
 
274
- # Indicates whether any build failed.
275
- #
276
- # A failed build blocks releases. Success requires all builds to pass. Check
277
- # this to gate release actions.
219
+ # Whether any build has failed.
278
220
  def any_failed?
279
221
  builds.any?(&:failed?)
280
222
  end
281
223
 
282
- # Extracts the commit SHA from build output.
283
- #
284
- # Builds track which commit they ran. Displaying the SHA confirms the correct
285
- # code was tested. Use this to show the user what was verified.
224
+ # The short SHA from the first build in the set.
286
225
  def commit_sha
287
- # Extract commit SHA from the first build's output - this is the canonical source
226
+ # Extract commit SHA from the first build - this is the canonical source
288
227
  return nil if builds.empty?
289
228
 
290
- sha_match = builds.first.output.match(/\[([a-f0-9]{7})\]\[0\]/)
291
- sha_match&.[](1)
229
+ out, st = Open3.capture2("gh", "run", "view", builds.first.id.to_s, "--json", "headSha", "--jq", ".headSha")
230
+ return nil unless st.success?
231
+ sha = out.strip
232
+ sha.empty? ? nil : sha[0, 7]
292
233
  end
293
234
 
294
- # Queries SourceHut for fresh status of all builds.
295
- #
296
- # Build status changes over time. Watching requires periodic updates. Call
297
- # this in a loop to poll until all builds complete.
235
+ # Returns a new BuildSet with refreshed status for all builds.
298
236
  def refresh
299
237
  BuildSet.new(builds: builds.map(&:refresh))
300
238
  end
301
239
 
302
- # Collects the build IDs for display.
303
- #
304
- # Users need to identify which builds are being watched. IDs enable lookup
305
- # and linking. Use this when showing build references.
240
+ # The build IDs in this set.
306
241
  def ids
307
242
  builds.map(&:id)
308
243
  end
309
244
  end
310
245
 
311
- # Watches SourceHut builds and reports results.
312
- #
313
- # CI builds run asynchronously. Developers need to know when they finish and
314
- # whether they passed. Polling manually is tedious. Parsing output is error-prone.
315
- #
316
- # This class watches builds. It polls until complete. It announces results via
317
- # macOS text-to-speech. It displays formatted output with clear pass/fail status.
318
- #
319
- # Use it to wait for CI before releasing.
246
+ # Watches GitHub Actions builds and reports results via terminal UI.
320
247
  class BuildWatch
321
248
  # Seconds between status polls.
322
249
  POLL_INTERVAL = 30
323
-
324
250
  # Seconds to wait for builds to appear.
325
251
  WAIT_INTERVAL = 5
326
-
327
252
  # Maximum attempts to find builds before giving up.
328
253
  MAX_WAIT_ATTEMPTS = 6
329
254
 
330
- # Creates a new BuildWatch.
331
- #
332
- # [wait_for_builds] Whether to wait for builds to appear.
333
- # [build_count] Number of builds to expect.
334
- # [auto_yes] Whether to auto-confirm prompts.
335
- # [no_say] Whether to disable text-to-speech.
255
+ # Configures the watcher with polling and display preferences.
336
256
  def initialize(wait_for_builds: true, build_count: 4, auto_yes: false, no_say: false)
337
257
  @wait_for_builds = wait_for_builds
338
258
  @build_count = build_count
@@ -453,7 +373,7 @@ class BuildWatch
453
373
  puts block_line
454
374
  puts
455
375
  end
456
- system("hut", "builds", "show", build.id.to_s)
376
+ system("gh", "run", "view", build.id.to_s)
457
377
  end
458
378
  else
459
379
  # Normal builds: gray single-boxes with URL titles
@@ -463,11 +383,7 @@ class BuildWatch
463
383
  end
464
384
  end
465
385
 
466
- # Starts the build watching TUI.
467
- #
468
- # Users invoke hbs to monitor CI. This method checks prerequisites, resolves
469
- # target commit, queries builds, and polls until completion. It reports
470
- # results with terminal notifications.
386
+ # Entry point: fetches builds for HEAD, watches until complete, reports.
471
387
  def run
472
388
  check_dependencies!
473
389
 
@@ -516,12 +432,17 @@ class BuildWatch
516
432
  end
517
433
 
518
434
  private def check_dependencies!
519
- %w[hut say].each do |cmd|
435
+ %w[gh say].each do |cmd|
520
436
  unless system("command", "-v", cmd, out: File::NULL, err: File::NULL)
521
437
  warn "Error: '#{cmd}' not found."
522
438
  exit 1
523
439
  end
524
440
  end
441
+
442
+ unless system("gh", "auth", "status", out: File::NULL, err: File::NULL)
443
+ warn "Error: 'gh' is not authenticated. Run: gh auth login"
444
+ exit 1
445
+ end
525
446
  end
526
447
 
527
448
  private def find_builds(commit)
@@ -552,7 +473,7 @@ class BuildWatch
552
473
  )
553
474
 
554
475
  unless @auto_yes
555
- print "Watch latest SourceHut builds anyway? [Y/n] "
476
+ print "Watch latest GitHub Actions builds anyway? [Y/n] "
556
477
  reply = $stdin.gets&.strip || ""
557
478
  if reply.downcase.start_with?("n")
558
479
  puts
@@ -566,7 +487,7 @@ class BuildWatch
566
487
  if build_set.empty?
567
488
  puts
568
489
  say("No builds found")
569
- box("✗✗✗ NO BUILDS ON SOURCEHUT ✗✗✗", style: :error)
490
+ box("✗✗✗ NO BUILDS ON GITHUB ✗✗✗", style: :error)
570
491
  exit 1
571
492
  end
572
493
 
@@ -608,6 +529,7 @@ class BuildWatch
608
529
  end
609
530
  end
610
531
 
532
+ # Displays final build results and exits with the appropriate status.
611
533
  private def report(build_set)
612
534
  puts
613
535
  show_builds(build_set)
@@ -627,6 +549,7 @@ class BuildWatch
627
549
  end
628
550
  end
629
551
 
552
+ # Announces a message via macOS text-to-speech (async, non-blocking).
630
553
  private def say(message)
631
554
  return if @no_say
632
555
 
@@ -636,8 +559,25 @@ class BuildWatch
636
559
  end
637
560
  end
638
561
 
639
- # Parse options
640
- default_build_count = Dir.glob(".builds/*.yml").size
562
+ # Count workflows that would trigger for the current branch
563
+ default_build_count = begin
564
+ branch = `git branch --show-current 2>/dev/null`.strip
565
+ Dir.glob(".github/workflows/*.yml").count do |f|
566
+ on = YAML.safe_load_file(f).dig("on", "push") || {}
567
+ branches = on["branches"]
568
+ ignores = on["branches-ignore"]
569
+
570
+ if branches
571
+ branches.any? { |pattern| File.fnmatch(pattern, branch) }
572
+ elsif ignores
573
+ ignores.none? { |pattern| File.fnmatch(pattern, branch) }
574
+ else
575
+ true # No branch filter means all branches
576
+ end
577
+ end
578
+ rescue
579
+ 0
580
+ end
641
581
  default_build_count = 4 if default_build_count.zero?
642
582
 
643
583
  options = { wait_for_builds: true, build_count: default_build_count, auto_yes: false, no_say: false }