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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +11 -0
- data/LICENSES/BSD-2-Clause.txt +9 -0
- data/README.md +1 -1
- data/exe/hbs +149 -209
- data/exe/scaffold +169 -282
- data/lib/ratatui_ruby/devtools/tasks/lint.rake +3 -7
- data/lib/ratatui_ruby/devtools/tasks/release/ci_run.rb +44 -0
- data/lib/ratatui_ruby/devtools/tasks/release/github_cli.rb +36 -0
- data/lib/ratatui_ruby/devtools/tasks/release/native_gem_version.rb +70 -0
- data/lib/ratatui_ruby/devtools/tasks/release/platform_gem.rb +63 -0
- data/lib/ratatui_ruby/devtools/tasks/release/versioned_binary.rb +36 -0
- data/lib/ratatui_ruby/devtools/tasks/release.rake +87 -0
- data/lib/ratatui_ruby/devtools/tasks/resources/rubies.yml +1 -1
- data/lib/ratatui_ruby/devtools/templates/.github/workflows/build-gems.yml.erb +124 -0
- data/lib/ratatui_ruby/devtools/templates/.github/workflows/ci.yml.erb +77 -0
- data/lib/ratatui_ruby/devtools/templates/.gitignore.erb +2 -0
- data/lib/ratatui_ruby/devtools/templates/.rubocop.yml.erb +6 -4
- data/lib/ratatui_ruby/devtools/templates/AGENTS.md.erb +101 -28
- data/lib/ratatui_ruby/devtools/templates/CODE_OF_CONDUCT.md.erb +44 -0
- data/lib/ratatui_ruby/devtools/templates/CONTRIBUTING.md.erb +80 -0
- data/lib/ratatui_ruby/devtools/templates/Gemfile.erb +2 -2
- data/lib/ratatui_ruby/devtools/templates/README.md.erb +46 -13
- data/lib/ratatui_ruby/devtools/templates/REUSE.toml.erb +2 -2
- data/lib/ratatui_ruby/devtools/templates/Rakefile.erb +4 -4
- data/lib/ratatui_ruby/devtools/templates/bin/setup.erb +21 -11
- data/lib/ratatui_ruby/devtools/templates/bin/setup.ps1.erb +105 -0
- data/lib/ratatui_ruby/devtools/templates/doc/custom.css.erb +3 -3
- data/lib/ratatui_ruby/devtools/templates/doc/getting_started/quickstart.md.erb +3 -3
- data/lib/ratatui_ruby/devtools/templates/doc/index.md.erb +2 -2
- data/lib/ratatui_ruby/devtools/templates/ext/.cargo/config.toml.erb +15 -0
- data/lib/ratatui_ruby/devtools/templates/ext/.gitignore.erb +6 -0
- data/lib/ratatui_ruby/devtools/templates/ext/Cargo.toml.erb +20 -0
- data/lib/ratatui_ruby/devtools/templates/ext/clippy.toml.erb +9 -0
- data/lib/ratatui_ruby/devtools/templates/ext/extconf.rb.erb +23 -0
- data/lib/ratatui_ruby/devtools/templates/ext/src/lib.rs.erb +13 -0
- data/lib/ratatui_ruby/devtools/templates/gemspec.erb +21 -14
- data/lib/ratatui_ruby/devtools/templates/lib/main.rb.erb +19 -0
- data/lib/ratatui_ruby/devtools/templates/lib/test_helper.rb.erb +26 -0
- data/lib/ratatui_ruby/devtools/templates/lib/version.rb.erb +10 -0
- data/lib/ratatui_ruby/devtools/templates/mise.toml.erb +3 -3
- data/lib/ratatui_ruby/devtools/templates/tasks/example_viewer.html.erb +23 -23
- data/lib/ratatui_ruby/devtools/templates/tasks/resources/index.html.erb +15 -16
- data/lib/ratatui_ruby/devtools/templates/tasks/resources/rubies.yml.erb +5 -6
- data/lib/ratatui_ruby/devtools/templates/test/test_helper.rb.erb +15 -0
- data/lib/ratatui_ruby/devtools/version.rb +1 -1
- data/lib/ratatui_ruby/devtools.rb +2 -2
- metadata +24 -5
- data/lib/ratatui_ruby/devtools/tasks/resources/build.yml.erb +0 -54
- data/lib/ratatui_ruby/devtools/tasks/sourcehut.rake +0 -94
- data/lib/ratatui_ruby/devtools/templates/tasks/resources/build.yml.erb +0 -62
- /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:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 9c8afb78c4a4395a9d4e4db32fcec1e83e6c1b495d6e7f5928c1b12d50aa775b
|
|
4
|
+
data.tar.gz: afb0898067b88c44a135e9f56053b7b265b82cc14ace609453fdbaaec0f36461
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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://
|
|
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:
|
|
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
|
|
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
|
|
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
|
-
#
|
|
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
|
-
#
|
|
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
|
-
#
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
#
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
# Returns the
|
|
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
|
|
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(
|
|
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
|
|
85
|
+
# Returns the commit at the upstream ref, or +nil+ if unavailable.
|
|
77
86
|
def self.origin_head
|
|
78
|
-
sha = `git rev-parse
|
|
87
|
+
sha = `git rev-parse #{upstream_ref} 2>/dev/null`.strip
|
|
79
88
|
return nil if sha.empty?
|
|
80
89
|
|
|
81
|
-
new(
|
|
90
|
+
new(sha:)
|
|
82
91
|
end
|
|
83
92
|
|
|
84
|
-
#
|
|
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",
|
|
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
|
-
#
|
|
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
|
|
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
|
-
#
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
#
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
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
|
-
#
|
|
119
|
-
|
|
120
|
-
|
|
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
|
-
#
|
|
127
|
-
|
|
128
|
-
|
|
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
|
-
#
|
|
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("
|
|
131
|
+
out, = Open3.capture2("gh", "run", "view", id.to_s)
|
|
140
132
|
out
|
|
141
133
|
end
|
|
142
134
|
|
|
143
|
-
#
|
|
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
|
-
|
|
149
|
-
output.lines.drop(1).map(&:rstrip)
|
|
137
|
+
output.lines.map(&:rstrip)
|
|
150
138
|
end
|
|
151
139
|
|
|
152
|
-
#
|
|
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
|
-
|
|
158
|
-
|
|
159
|
-
|
|
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
|
-
#
|
|
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("
|
|
168
|
-
|
|
169
|
-
Build.
|
|
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
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
#
|
|
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
|
-
|
|
188
|
-
|
|
189
|
-
.
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
.
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
#
|
|
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
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
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
|
-
|
|
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
|
|
239
|
-
when "SUCCESS" then
|
|
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
|
-
#
|
|
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
|
-
#
|
|
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
|
-
#
|
|
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
|
-
#
|
|
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
|
|
226
|
+
# Extract commit SHA from the first build - this is the canonical source
|
|
288
227
|
return nil if builds.empty?
|
|
289
228
|
|
|
290
|
-
|
|
291
|
-
|
|
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
|
-
#
|
|
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
|
-
#
|
|
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
|
|
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
|
-
#
|
|
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("
|
|
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
|
-
#
|
|
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[
|
|
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
|
|
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
|
|
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
|
-
#
|
|
640
|
-
default_build_count =
|
|
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 }
|