captive-release 0.4.3 → 0.4.4
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/lib/captive_release/build_command.rb +52 -0
- data/lib/captive_release/build_config.rb +20 -0
- data/lib/captive_release/build_config_prompt.rb +31 -0
- data/lib/captive_release/build_dispatch_run.rb +35 -0
- data/lib/captive_release/cli.rb +27 -382
- data/lib/captive_release/config.rb +2 -44
- data/lib/captive_release/config_file.rb +30 -0
- data/lib/captive_release/config_loader.rb +34 -0
- data/lib/captive_release/config_writer.rb +45 -0
- data/lib/captive_release/framework_detector.rb +30 -0
- data/lib/captive_release/git_commands.rb +18 -27
- data/lib/captive_release/git_release.rb +24 -0
- data/lib/captive_release/github_api.rb +45 -0
- data/lib/captive_release/github_dispatch.rb +12 -27
- data/lib/captive_release/github_http.rb +34 -25
- data/lib/captive_release/github_release.rb +16 -32
- data/lib/captive_release/github_repo.rb +29 -0
- data/lib/captive_release/github_token.rb +18 -0
- data/lib/captive_release/help.rb +50 -0
- data/lib/captive_release/init_command.rb +44 -0
- data/lib/captive_release/menu.rb +33 -0
- data/lib/captive_release/options.rb +27 -0
- data/lib/captive_release/release_command.rb +42 -0
- data/lib/captive_release/release_preconditions.rb +24 -0
- data/lib/captive_release/release_publication.rb +30 -0
- data/lib/captive_release/repo_files.rb +35 -0
- data/lib/captive_release/shell.rb +20 -0
- data/lib/captive_release/terminal.rb +37 -0
- data/lib/captive_release/version.rb +1 -1
- data/lib/captive_release/version_choice.rb +56 -0
- data/lib/captive_release/version_detector.rb +26 -57
- data/lib/captive_release/version_file.rb +14 -79
- data/lib/captive_release/version_file_bootstrap.rb +25 -0
- data/lib/captive_release/version_file_selection.rb +53 -0
- data/lib/captive_release/version_files_argument.rb +21 -0
- data/lib/captive_release/version_files_writing.rb +22 -0
- data/lib/captive_release/version_format.rb +86 -0
- data/lib/captive_release.rb +29 -0
- metadata +29 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 53520ddc8d72e4aaba6efd5cf52398ddfb5b621316cd366269a9d3d3c78fa38e
|
|
4
|
+
data.tar.gz: ed836c92b2e341a49b57136266a62e8a575ea97440db814f1b387ef9846cdc8d
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: a3c33fc6776c8dedee9e5686b6a59ff069a40035890097fc6a7c6f8f695dbabb3eaf86a6e4796c82f88c127818a99746019176f52d2dfd0cb14367fb8a27c843
|
|
7
|
+
data.tar.gz: 50e1922a0ba1296719bab58e09cc1620b42e905adc75d4bd7653b7b3980b9fc8d71146f558a454d90fac2503c1ed77eb3ca3375beee8269f0ceb5bf21b7590bf
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module CaptiveRelease
|
|
4
|
+
# Déclenche le workflow de build mobile (EAS ou Capacitor) sur la branche courante.
|
|
5
|
+
#
|
|
6
|
+
# Cette unité change quand le geste de build change, la CLI quand l'ensemble des commandes
|
|
7
|
+
# ou le parsing des options change. Le jeton arrive par un fournisseur et non par une
|
|
8
|
+
# valeur, pour n'être réclamé qu'au moment où il sert vraiment.
|
|
9
|
+
class BuildCommand
|
|
10
|
+
def initialize(terminal:, config:, token_provider:)
|
|
11
|
+
@terminal = terminal
|
|
12
|
+
@config = config
|
|
13
|
+
@token_provider = token_provider
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def run(profile_arg)
|
|
17
|
+
refuser_sans_configuration
|
|
18
|
+
@terminal.say "\n#{@terminal.bold("captive-release build")}\n"
|
|
19
|
+
profil = profile_arg || demander_le_profil
|
|
20
|
+
BuildDispatchRun.new(terminal: @terminal)
|
|
21
|
+
.call(profil: profil, workflow: workflow_file, jeton: jeton!)
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
private
|
|
25
|
+
|
|
26
|
+
def refuser_sans_configuration
|
|
27
|
+
return if @config.expo? || @config.capacitor?
|
|
28
|
+
|
|
29
|
+
@terminal.say @terminal.error("Aucune configuration de build dans #{CONFIG_FILE}.")
|
|
30
|
+
@terminal.say "Lancez #{@terminal.bold("captive-release init")} pour configurer le workflow de build.\n\n"
|
|
31
|
+
exit 1
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def jeton!
|
|
35
|
+
jeton = @token_provider.call
|
|
36
|
+
return jeton if jeton
|
|
37
|
+
|
|
38
|
+
@terminal.say @terminal.error("GITHUB_TOKEN or `gh auth login` required to dispatch build workflows.")
|
|
39
|
+
exit 1
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def demander_le_profil
|
|
43
|
+
Menu.new(terminal: @terminal).choose("Choose profile:",
|
|
44
|
+
{ "staging" => "staging",
|
|
45
|
+
"production" => "production", })
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def workflow_file
|
|
49
|
+
@config.expo? ? @config.expo.build_workflow : @config.capacitor.build_workflow
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
end
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module CaptiveRelease
|
|
4
|
+
EXPO_DEFAULT_WORKFLOW = "build.yml"
|
|
5
|
+
CAPACITOR_DEFAULT_WORKFLOW = "capacitor-build.yml"
|
|
6
|
+
|
|
7
|
+
# Le workflow de build à déclencher. Expo et Capacitor ne se distinguaient que par le nom
|
|
8
|
+
# du workflow par défaut : deux structures identiques pour une seule notion.
|
|
9
|
+
BuildConfig = Struct.new(:build_workflow, keyword_init: true) do
|
|
10
|
+
def self.from_hash(hash, default_workflow)
|
|
11
|
+
return nil unless hash
|
|
12
|
+
|
|
13
|
+
new(build_workflow: hash["build_workflow"] || default_workflow)
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def to_h
|
|
17
|
+
{ "build_workflow" => build_workflow }
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
end
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module CaptiveRelease
|
|
4
|
+
# Demande au développeur s'il veut dispatcher le build d'un framework détecté, et sous quel
|
|
5
|
+
# workflow. Expo et Capacitor posent la même question à deux valeurs près.
|
|
6
|
+
#
|
|
7
|
+
# Cette unité change quand la question change, ConfigWriter quand la forme du fichier change.
|
|
8
|
+
class BuildConfigPrompt
|
|
9
|
+
def initialize(terminal:)
|
|
10
|
+
@terminal = terminal
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def call(framework, workflow_par_defaut)
|
|
14
|
+
@terminal.say "\n#{@terminal.bold("#{framework} project detected.")} " \
|
|
15
|
+
"Configure build dispatch?\n"
|
|
16
|
+
@terminal.say " #{@terminal.dim("1)")} Yes"
|
|
17
|
+
@terminal.say " #{@terminal.dim("2)")} No"
|
|
18
|
+
return nil unless @terminal.ask_choice == 1
|
|
19
|
+
|
|
20
|
+
BuildConfig.new(build_workflow: workflow(workflow_par_defaut))
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
private
|
|
24
|
+
|
|
25
|
+
def workflow(workflow_par_defaut)
|
|
26
|
+
@terminal.say "\nWorkflow file name #{@terminal.dim("(default: #{workflow_par_defaut})")}:"
|
|
27
|
+
saisie = @terminal.ask
|
|
28
|
+
saisie.empty? ? workflow_par_defaut : saisie
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module CaptiveRelease
|
|
4
|
+
# Déclenche le workflow de build sur la branche courante, après avoir dit ce qu'il va faire :
|
|
5
|
+
# le profil, le workflow et la branche sont les trois choses qu'on vérifie avant de lancer.
|
|
6
|
+
#
|
|
7
|
+
# Cette unité change quand ce qu'on déclenche change, BuildCommand quand la façon de
|
|
8
|
+
# constituer la demande change.
|
|
9
|
+
class BuildDispatchRun
|
|
10
|
+
def initialize(terminal:)
|
|
11
|
+
@terminal = terminal
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def call(profil:, workflow:, jeton:)
|
|
15
|
+
branche = GitCommands.current_branch
|
|
16
|
+
annoncer(profil, workflow, branche)
|
|
17
|
+
|
|
18
|
+
url = GithubDispatch.dispatch(workflow_file: workflow, ref: branche,
|
|
19
|
+
inputs: { "profile" => profil },
|
|
20
|
+
remote_url: GitCommands.remote_url, token: jeton)
|
|
21
|
+
|
|
22
|
+
@terminal.done "Build dispatched!"
|
|
23
|
+
@terminal.say " #{url}\n"
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
private
|
|
27
|
+
|
|
28
|
+
def annoncer(profil, workflow, branche)
|
|
29
|
+
@terminal.say "Profile: #{@terminal.bold(profil)}"
|
|
30
|
+
@terminal.say "Workflow: #{@terminal.bold(workflow)}"
|
|
31
|
+
@terminal.say "Branch: #{@terminal.bold(branche)}"
|
|
32
|
+
@terminal.say "\nDispatching...\n"
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
data/lib/captive_release/cli.rb
CHANGED
|
@@ -1,419 +1,64 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
module CaptiveRelease
|
|
4
|
+
# Aiguille vers la commande demandée et lui câble ses collaborateurs. Elle ne porte aucun
|
|
5
|
+
# geste : chaque commande vit dans sa classe.
|
|
4
6
|
class CLI
|
|
5
|
-
BUMP_TYPES = %w[patch minor major].freeze
|
|
6
|
-
SEMVER_RE = /\A\d+\.\d+\.\d+\z/
|
|
7
|
-
|
|
8
7
|
def self.run(argv = ARGV)
|
|
9
8
|
new(argv.dup).run
|
|
10
9
|
end
|
|
11
10
|
|
|
12
11
|
def initialize(argv)
|
|
13
|
-
@
|
|
12
|
+
@options = Options.new(argv)
|
|
14
13
|
end
|
|
15
14
|
|
|
16
15
|
def run
|
|
17
|
-
command = @
|
|
16
|
+
command = @options.next_argument
|
|
18
17
|
|
|
19
18
|
case command
|
|
20
|
-
when "init"
|
|
21
|
-
when "new"
|
|
22
|
-
when "build"
|
|
23
|
-
when "version", "--version", "-v" then
|
|
24
|
-
when "help", "--help", "-h", nil then
|
|
25
|
-
else
|
|
26
|
-
puts error("Unknown command: #{command}")
|
|
27
|
-
print_help
|
|
28
|
-
exit 1
|
|
19
|
+
when "init" then run_init
|
|
20
|
+
when "new" then run_release
|
|
21
|
+
when "build" then run_build
|
|
22
|
+
when "version", "--version", "-v" then terminal.say "captive-release #{VERSION}"
|
|
23
|
+
when "help", "--help", "-h", nil then aide.show
|
|
24
|
+
else refuser(command)
|
|
29
25
|
end
|
|
30
26
|
end
|
|
31
27
|
|
|
32
28
|
private
|
|
33
29
|
|
|
34
|
-
# ── init ────────────────────────────────────────────────────────────────
|
|
35
|
-
|
|
36
30
|
def run_init
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
if files_arg
|
|
40
|
-
run_init_non_interactive(files_arg)
|
|
41
|
-
else
|
|
42
|
-
run_init_interactive
|
|
43
|
-
end
|
|
44
|
-
end
|
|
45
|
-
|
|
46
|
-
def run_init_interactive
|
|
47
|
-
puts "\n#{bold("captive-release init")}\n\n"
|
|
48
|
-
puts "Scanning for version files...\n\n"
|
|
49
|
-
|
|
50
|
-
found = VersionDetector.scan
|
|
51
|
-
|
|
52
|
-
if found.empty?
|
|
53
|
-
puts warn("No version files found (package.json, VERSION).")
|
|
54
|
-
create_version_file_if_confirmed
|
|
55
|
-
return
|
|
56
|
-
end
|
|
57
|
-
|
|
58
|
-
found.each_with_index do |f, i|
|
|
59
|
-
version = safe_read(f)
|
|
60
|
-
puts " #{dim("[#{i + 1}]")} #{f.path} #{dim("(#{version})")}"
|
|
61
|
-
end
|
|
62
|
-
|
|
63
|
-
selected = if found.size == 1
|
|
64
|
-
puts "\n#{ok("✅")} 1 fichier détecté — sélectionné automatiquement."
|
|
65
|
-
found
|
|
66
|
-
else
|
|
67
|
-
puts "\nAppuyez sur #{bold("Entrée")} pour tout sélectionner, ou entrez les numéros à #{bold("exclure")} (ex: 2,3) :"
|
|
68
|
-
print "> "
|
|
69
|
-
input = $stdin.gets&.chomp || ""
|
|
70
|
-
if input.strip.empty?
|
|
71
|
-
found
|
|
72
|
-
else
|
|
73
|
-
excluded = input.split(",").map { |n| n.strip.to_i - 1 }
|
|
74
|
-
found.reject.with_index { |_, i| excluded.include?(i) }
|
|
75
|
-
end
|
|
76
|
-
end
|
|
77
|
-
|
|
78
|
-
if selected.empty?
|
|
79
|
-
puts warn("Aucun fichier sélectionné.")
|
|
80
|
-
create_version_file_if_confirmed
|
|
81
|
-
return
|
|
82
|
-
end
|
|
83
|
-
|
|
84
|
-
write_config(selected)
|
|
85
|
-
end
|
|
86
|
-
|
|
87
|
-
def create_version_file_if_confirmed
|
|
88
|
-
puts "\nCreate a #{bold("VERSION")} file? [y/N] "
|
|
89
|
-
print "> "
|
|
90
|
-
answer = $stdin.gets&.chomp&.downcase || ""
|
|
91
|
-
exit 1 unless answer == "y"
|
|
92
|
-
|
|
93
|
-
initial = "0.1.0"
|
|
94
|
-
File.write("VERSION", "#{initial}\n")
|
|
95
|
-
puts " #{ok("✅")} VERSION créé avec #{bold(initial)}"
|
|
96
|
-
|
|
97
|
-
file = VersionFile.new("VERSION", "plain")
|
|
98
|
-
write_config([ file ])
|
|
99
|
-
end
|
|
100
|
-
|
|
101
|
-
def run_init_non_interactive(files_arg)
|
|
102
|
-
entries = files_arg.split(",").map(&:strip)
|
|
103
|
-
selected = entries.map do |entry|
|
|
104
|
-
path, type = entry.split(":")
|
|
105
|
-
type ||= detect_type(path)
|
|
106
|
-
VersionFile.new(path, type)
|
|
107
|
-
end
|
|
108
|
-
write_config(selected)
|
|
109
|
-
end
|
|
110
|
-
|
|
111
|
-
def write_config(files, expo: nil, capacitor: nil)
|
|
112
|
-
if files.empty?
|
|
113
|
-
puts error("No files selected.")
|
|
114
|
-
exit 1
|
|
115
|
-
end
|
|
116
|
-
|
|
117
|
-
detector = VersionDetector.new(".")
|
|
118
|
-
expo ||= ask_expo_config if detector.expo?
|
|
119
|
-
capacitor ||= ask_capacitor_config if detector.capacitor?
|
|
120
|
-
|
|
121
|
-
Config.write(files, expo: expo, capacitor: capacitor)
|
|
122
|
-
puts "\n#{ok("✅ captive-release.json written with #{files.size} version file(s).")}"
|
|
123
|
-
puts " #{dim("Expo build workflow: #{expo.build_workflow}")}" if expo
|
|
124
|
-
puts " #{dim("Capacitor build workflow: #{capacitor.build_workflow}")}" if capacitor
|
|
125
|
-
puts
|
|
126
|
-
end
|
|
127
|
-
|
|
128
|
-
def ask_expo_config
|
|
129
|
-
puts "\n#{bold("Expo project detected.")} Configure EAS build dispatch?\n"
|
|
130
|
-
puts " #{dim("1)")} Yes"
|
|
131
|
-
puts " #{dim("2)")} No"
|
|
132
|
-
print "\n> "
|
|
133
|
-
choice = $stdin.gets&.chomp&.to_i || 0
|
|
134
|
-
return nil unless choice == 1
|
|
135
|
-
|
|
136
|
-
puts "\nWorkflow file name #{dim("(default: build.yml)")}:"
|
|
137
|
-
print "> "
|
|
138
|
-
workflow = $stdin.gets&.chomp || ""
|
|
139
|
-
workflow = "build.yml" if workflow.empty?
|
|
140
|
-
|
|
141
|
-
ExpoConfig.new(build_workflow: workflow)
|
|
142
|
-
end
|
|
143
|
-
|
|
144
|
-
def ask_capacitor_config
|
|
145
|
-
puts "\n#{bold("Capacitor project detected.")} Configure build dispatch?\n"
|
|
146
|
-
puts " #{dim("1)")} Yes"
|
|
147
|
-
puts " #{dim("2)")} No"
|
|
148
|
-
print "\n> "
|
|
149
|
-
choice = $stdin.gets&.chomp&.to_i || 0
|
|
150
|
-
return nil unless choice == 1
|
|
151
|
-
|
|
152
|
-
puts "\nWorkflow file name #{dim("(default: capacitor-build.yml)")}:"
|
|
153
|
-
print "> "
|
|
154
|
-
workflow = $stdin.gets&.chomp || ""
|
|
155
|
-
workflow = "capacitor-build.yml" if workflow.empty?
|
|
156
|
-
|
|
157
|
-
CapacitorConfig.new(build_workflow: workflow)
|
|
158
|
-
end
|
|
159
|
-
|
|
160
|
-
def parse_selection(input, found)
|
|
161
|
-
input.split(",").map { |n| found[n.strip.to_i - 1] }.compact
|
|
31
|
+
InitCommand.new(terminal: terminal).run(@options.value("--files"))
|
|
162
32
|
end
|
|
163
33
|
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
# ── release ─────────────────────────────────────────────────────────────
|
|
169
|
-
|
|
34
|
+
# La confirmation est demandée même quand la version est donnée en argument : sans elle,
|
|
35
|
+
# une faute de frappe commitait, taguait et poussait sans aucun point d'arrêt. --yes reste
|
|
36
|
+
# le chemin explicite pour l'automatisation.
|
|
170
37
|
def run_release
|
|
171
|
-
|
|
172
|
-
config = load_config
|
|
173
|
-
|
|
174
|
-
current = config.current_version
|
|
175
|
-
puts "\n#{bold("captive-release")}\n\n"
|
|
176
|
-
puts "Current version: #{bold(current)}\n\n"
|
|
177
|
-
|
|
178
|
-
next_version =
|
|
179
|
-
if bump_arg
|
|
180
|
-
resolve_version(current, bump_arg)
|
|
181
|
-
else
|
|
182
|
-
ask_version(current)
|
|
183
|
-
end
|
|
184
|
-
|
|
185
|
-
confirm!(next_version) unless bump_arg
|
|
186
|
-
|
|
187
|
-
puts "\n#{bold("Updating version files...")}"
|
|
188
|
-
config.version_files.each do |f|
|
|
189
|
-
f.write_version(next_version)
|
|
190
|
-
puts " #{ok("✅")} #{f.path}"
|
|
191
|
-
end
|
|
192
|
-
|
|
193
|
-
puts "\n#{bold("Running git commands...")}"
|
|
194
|
-
last_tag = GitCommands.last_tag
|
|
195
|
-
GitCommands.add_and_commit(next_version, config.version_files.map(&:path))
|
|
196
|
-
puts " #{ok("✅")} git commit \"🔖 Set version v#{next_version}\""
|
|
197
|
-
GitCommands.tag(next_version)
|
|
198
|
-
puts " #{ok("✅")} git tag v#{next_version}"
|
|
199
|
-
GitCommands.push(next_version)
|
|
200
|
-
puts " #{ok("✅")} git push + tag"
|
|
201
|
-
|
|
202
|
-
puts "\n#{bold("Creating GitHub release...")}"
|
|
203
|
-
token = github_token
|
|
204
|
-
if token
|
|
205
|
-
commits = GitCommands.commits_since(last_tag)
|
|
206
|
-
url = GithubRelease.create(
|
|
207
|
-
version: next_version,
|
|
208
|
-
body: commits,
|
|
209
|
-
remote_url: GitCommands.remote_url,
|
|
210
|
-
token: token
|
|
211
|
-
)
|
|
212
|
-
puts " #{ok("✅")} #{url}"
|
|
213
|
-
else
|
|
214
|
-
puts " #{warn("⚠️ GITHUB_TOKEN not set — skipping GitHub release creation.")}"
|
|
215
|
-
end
|
|
216
|
-
|
|
217
|
-
puts "\n🎉 #{bold("Version v#{next_version} released successfully!")}\n\n"
|
|
218
|
-
end
|
|
38
|
+
sans_confirmation = @options.flag?("--yes")
|
|
219
39
|
|
|
220
|
-
|
|
221
|
-
unless type.match?(SEMVER_RE) || BUMP_TYPES.include?(type)
|
|
222
|
-
puts error("Invalid bump type: #{type}. Use patch|minor|major|x.y.z")
|
|
223
|
-
exit 1
|
|
224
|
-
end
|
|
225
|
-
VersionBumper.bump(current, type)
|
|
40
|
+
release_command.run(@options.next_argument, confirmation: !sans_confirmation)
|
|
226
41
|
end
|
|
227
42
|
|
|
228
|
-
def ask_version(current)
|
|
229
|
-
candidates = {
|
|
230
|
-
"Patch" => VersionBumper.bump(current, "patch"),
|
|
231
|
-
"Minor" => VersionBumper.bump(current, "minor"),
|
|
232
|
-
"Major" => VersionBumper.bump(current, "major"),
|
|
233
|
-
"Custom" => nil,
|
|
234
|
-
}
|
|
235
|
-
|
|
236
|
-
puts "Choose release type:\n"
|
|
237
|
-
candidates.each_with_index do |(label, version), i|
|
|
238
|
-
preview = version ? "→ #{bold(version)}" : "→ ?"
|
|
239
|
-
puts " #{dim("#{i + 1})")} #{label} #{preview}"
|
|
240
|
-
end
|
|
241
|
-
|
|
242
|
-
print "\n> "
|
|
243
|
-
choice = $stdin.gets&.chomp&.to_i || 0
|
|
244
|
-
|
|
245
|
-
labels = candidates.keys
|
|
246
|
-
chosen = labels[choice - 1]
|
|
247
|
-
|
|
248
|
-
if chosen.nil?
|
|
249
|
-
puts error("Invalid choice.")
|
|
250
|
-
exit 1
|
|
251
|
-
end
|
|
252
|
-
|
|
253
|
-
if chosen == "Custom"
|
|
254
|
-
print "Enter version (x.y.z): "
|
|
255
|
-
version = $stdin.gets&.chomp || ""
|
|
256
|
-
unless version.match?(SEMVER_RE)
|
|
257
|
-
puts error("Invalid semver: #{version}")
|
|
258
|
-
exit 1
|
|
259
|
-
end
|
|
260
|
-
version
|
|
261
|
-
else
|
|
262
|
-
candidates[chosen]
|
|
263
|
-
end
|
|
264
|
-
end
|
|
265
|
-
|
|
266
|
-
def confirm!(version)
|
|
267
|
-
puts "\nNew version: #{bold(version)}"
|
|
268
|
-
print "Confirm? [Y/n] "
|
|
269
|
-
answer = $stdin.gets&.chomp&.downcase || ""
|
|
270
|
-
exit 1 unless answer == "y" || answer == ""
|
|
271
|
-
end
|
|
272
|
-
|
|
273
|
-
# ── build ────────────────────────────────────────────────────────────────
|
|
274
|
-
|
|
275
43
|
def run_build
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
config = load_config
|
|
279
|
-
|
|
280
|
-
unless config.expo? || config.capacitor?
|
|
281
|
-
puts error("Aucune configuration de build dans #{CONFIG_FILE}.")
|
|
282
|
-
puts "Lancez #{bold("captive-release init")} pour configurer le workflow de build.\n\n"
|
|
283
|
-
exit 1
|
|
284
|
-
end
|
|
285
|
-
|
|
286
|
-
puts "\n#{bold("captive-release build")}\n\n"
|
|
287
|
-
|
|
288
|
-
profile = profile_arg || ask_profile
|
|
289
|
-
|
|
290
|
-
token = github_token
|
|
291
|
-
unless token
|
|
292
|
-
puts error("GITHUB_TOKEN or `gh auth login` required to dispatch build workflows.")
|
|
293
|
-
exit 1
|
|
294
|
-
end
|
|
295
|
-
|
|
296
|
-
ref = current_branch
|
|
297
|
-
workflow_file = config.expo? ? config.expo.build_workflow : config.capacitor.build_workflow
|
|
298
|
-
|
|
299
|
-
puts "Profile: #{bold(profile)}"
|
|
300
|
-
puts "Workflow: #{bold(workflow_file)}"
|
|
301
|
-
puts "Branch: #{bold(ref)}"
|
|
302
|
-
puts "\nDispatching...\n"
|
|
303
|
-
|
|
304
|
-
url = GithubDispatch.dispatch(
|
|
305
|
-
workflow_file: workflow_file,
|
|
306
|
-
ref: ref,
|
|
307
|
-
inputs: { "profile" => profile },
|
|
308
|
-
remote_url: GitCommands.remote_url,
|
|
309
|
-
token: token
|
|
310
|
-
)
|
|
311
|
-
|
|
312
|
-
puts "#{ok("✅")} Build dispatched!"
|
|
313
|
-
puts " #{url}\n\n"
|
|
314
|
-
end
|
|
315
|
-
|
|
316
|
-
def ask_profile
|
|
317
|
-
puts "Choose profile:\n"
|
|
318
|
-
puts " #{dim("1)")} staging"
|
|
319
|
-
puts " #{dim("2)")} production"
|
|
320
|
-
print "\n> "
|
|
321
|
-
choice = $stdin.gets&.chomp&.to_i || 0
|
|
322
|
-
|
|
323
|
-
case choice
|
|
324
|
-
when 1 then "staging"
|
|
325
|
-
when 2 then "production"
|
|
326
|
-
else
|
|
327
|
-
puts error("Invalid choice.")
|
|
328
|
-
exit 1
|
|
329
|
-
end
|
|
330
|
-
end
|
|
331
|
-
|
|
332
|
-
def current_branch
|
|
333
|
-
branch = `git rev-parse --abbrev-ref HEAD 2>/dev/null`.strip
|
|
334
|
-
branch.empty? ? "main" : branch
|
|
335
|
-
end
|
|
336
|
-
|
|
337
|
-
# ── helpers ─────────────────────────────────────────────────────────────
|
|
338
|
-
|
|
339
|
-
def load_config
|
|
340
|
-
return Config.load if File.exist?(CONFIG_FILE)
|
|
341
|
-
|
|
342
|
-
files = VersionDetector.scan
|
|
343
|
-
if files.empty?
|
|
344
|
-
puts error("Aucun fichier de version trouvé (package.json, lib/**/version.rb, VERSION).")
|
|
345
|
-
puts "Créez un fichier #{bold("VERSION")} ou lancez #{bold("captive-release init")}.\n\n"
|
|
346
|
-
exit 1
|
|
347
|
-
end
|
|
348
|
-
|
|
349
|
-
Config.new(files)
|
|
44
|
+
BuildCommand.new(**collaborateurs).run(@options.value("--profile"))
|
|
350
45
|
end
|
|
351
46
|
|
|
352
|
-
def
|
|
353
|
-
return ENV["GITHUB_TOKEN"] unless ENV["GITHUB_TOKEN"].to_s.empty?
|
|
47
|
+
def release_command = ReleaseCommand.new(**collaborateurs)
|
|
354
48
|
|
|
355
|
-
|
|
356
|
-
|
|
49
|
+
def collaborateurs
|
|
50
|
+
{ terminal: terminal, config: ConfigLoader.new(terminal: terminal).call,
|
|
51
|
+
token_provider: GithubToken.method(:call), }
|
|
357
52
|
end
|
|
358
53
|
|
|
359
|
-
def
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
54
|
+
def refuser(command)
|
|
55
|
+
terminal.say terminal.error("Unknown command: #{command}")
|
|
56
|
+
aide.show
|
|
57
|
+
exit 1
|
|
363
58
|
end
|
|
364
59
|
|
|
365
|
-
def
|
|
366
|
-
idx = @argv.index { |a| a.start_with?("#{flag}=") || a == flag }
|
|
367
|
-
return nil if idx.nil?
|
|
368
|
-
|
|
369
|
-
arg = @argv.delete_at(idx)
|
|
370
|
-
if arg.include?("=")
|
|
371
|
-
arg.split("=", 2).last
|
|
372
|
-
else
|
|
373
|
-
@argv.delete_at(idx)
|
|
374
|
-
end
|
|
375
|
-
end
|
|
376
|
-
|
|
377
|
-
def print_help
|
|
378
|
-
puts <<~HELP
|
|
379
|
-
|
|
380
|
-
#{bold("captive-release")} — Semver release tool
|
|
381
|
-
|
|
382
|
-
#{bold("Usage:")}
|
|
383
|
-
captive-release init [--files path[:type],...]
|
|
384
|
-
captive-release new [patch|minor|major|x.y.z]
|
|
385
|
-
captive-release version
|
|
386
|
-
|
|
387
|
-
#{bold("Commands:")}
|
|
388
|
-
init Detect version files and write captive-release.json
|
|
389
|
-
new Bump version, commit, tag, push, create GitHub release
|
|
390
|
-
build Dispatch an EAS build workflow (staging/production) via GitHub Actions
|
|
391
|
-
|
|
392
|
-
#{bold("Options for init:")}
|
|
393
|
-
--files Non-interactive: comma-separated list of files with optional type
|
|
394
|
-
Type is auto-detected (package_json or plain) when omitted
|
|
395
|
-
Example: --files=package.json,ios/VERSION:plain
|
|
396
|
-
|
|
397
|
-
#{bold("Examples:")}
|
|
398
|
-
captive-release init
|
|
399
|
-
captive-release init --files=package.json,apps/mobile/package.json
|
|
400
|
-
captive-release new
|
|
401
|
-
captive-release new patch
|
|
402
|
-
captive-release new 2.0.0
|
|
403
|
-
captive-release build
|
|
404
|
-
captive-release build --profile staging
|
|
405
|
-
captive-release build --profile production
|
|
406
|
-
|
|
407
|
-
#{bold("GitHub release:")}
|
|
408
|
-
Set GITHUB_TOKEN env var or authenticate with `gh auth login`
|
|
409
|
-
|
|
410
|
-
HELP
|
|
411
|
-
end
|
|
60
|
+
def terminal = @terminal ||= Terminal.new
|
|
412
61
|
|
|
413
|
-
def
|
|
414
|
-
def dim(str) = "\e[2m#{str}\e[0m"
|
|
415
|
-
def ok(str) = "\e[32m#{str}\e[0m"
|
|
416
|
-
def warn(str) = "\e[33m#{str}\e[0m"
|
|
417
|
-
def error(str) = "\e[31m#{str}\e[0m"
|
|
62
|
+
def aide = Help.new(terminal: terminal)
|
|
418
63
|
end
|
|
419
64
|
end
|
|
@@ -1,55 +1,13 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
require "json"
|
|
4
|
-
|
|
5
3
|
module CaptiveRelease
|
|
6
4
|
CONFIG_FILE = "captive-release.json"
|
|
7
5
|
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
return nil unless h
|
|
11
|
-
|
|
12
|
-
new(build_workflow: h["build_workflow"] || "build.yml")
|
|
13
|
-
end
|
|
14
|
-
|
|
15
|
-
def to_h
|
|
16
|
-
{ "build_workflow" => build_workflow }
|
|
17
|
-
end
|
|
18
|
-
end
|
|
19
|
-
|
|
20
|
-
CapacitorConfig = Struct.new(:build_workflow, keyword_init: true) do
|
|
21
|
-
def self.from_hash(h)
|
|
22
|
-
return nil unless h
|
|
23
|
-
|
|
24
|
-
new(build_workflow: h["build_workflow"] || "capacitor-build.yml")
|
|
25
|
-
end
|
|
26
|
-
|
|
27
|
-
def to_h
|
|
28
|
-
{ "build_workflow" => build_workflow }
|
|
29
|
-
end
|
|
30
|
-
end
|
|
31
|
-
|
|
6
|
+
# Ce qu'un dépôt déclare pour ses releases : les fichiers qui portent la version, et les
|
|
7
|
+
# workflows de build s'il en a. Sa forme sur le disque vit dans ConfigFile.
|
|
32
8
|
class Config
|
|
33
9
|
attr_reader :version_files, :expo, :capacitor
|
|
34
10
|
|
|
35
|
-
def self.load(path = CONFIG_FILE)
|
|
36
|
-
raise "#{CONFIG_FILE} not found. Run `captive-release init` first." unless File.exist?(path)
|
|
37
|
-
|
|
38
|
-
data = JSON.parse(File.read(path))
|
|
39
|
-
new(
|
|
40
|
-
data["version_files"].map { |f| VersionFile.new(f["path"], f["type"]) },
|
|
41
|
-
ExpoConfig.from_hash(data["expo"]),
|
|
42
|
-
CapacitorConfig.from_hash(data["capacitor"])
|
|
43
|
-
)
|
|
44
|
-
end
|
|
45
|
-
|
|
46
|
-
def self.write(version_files, path = CONFIG_FILE, expo: nil, capacitor: nil)
|
|
47
|
-
data = { "version_files" => version_files.map(&:to_h) }
|
|
48
|
-
data["expo"] = expo.to_h if expo
|
|
49
|
-
data["capacitor"] = capacitor.to_h if capacitor
|
|
50
|
-
File.write(path, "#{JSON.pretty_generate(data)}\n")
|
|
51
|
-
end
|
|
52
|
-
|
|
53
11
|
def initialize(version_files, expo = nil, capacitor = nil)
|
|
54
12
|
raise ArgumentError, "version_files cannot be empty" if version_files.empty?
|
|
55
13
|
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "json"
|
|
4
|
+
|
|
5
|
+
module CaptiveRelease
|
|
6
|
+
# La forme de captive-release.json sur le disque : comment la configuration s'y écrit et
|
|
7
|
+
# comment elle s'en relit.
|
|
8
|
+
#
|
|
9
|
+
# Cette unité change quand la forme du fichier change, Config quand le sens de la
|
|
10
|
+
# configuration change.
|
|
11
|
+
module ConfigFile
|
|
12
|
+
def self.load(path = CONFIG_FILE)
|
|
13
|
+
raise "#{CONFIG_FILE} not found. Run `captive-release init` first." unless File.exist?(path)
|
|
14
|
+
|
|
15
|
+
data = JSON.parse(File.read(path))
|
|
16
|
+
Config.new(
|
|
17
|
+
data["version_files"].map { |f| VersionFile.new(f["path"], f["type"]) },
|
|
18
|
+
BuildConfig.from_hash(data["expo"], EXPO_DEFAULT_WORKFLOW),
|
|
19
|
+
BuildConfig.from_hash(data["capacitor"], CAPACITOR_DEFAULT_WORKFLOW),
|
|
20
|
+
)
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def self.write(version_files, path = CONFIG_FILE, expo: nil, capacitor: nil)
|
|
24
|
+
data = { "version_files" => version_files.map(&:to_h) }
|
|
25
|
+
data["expo"] = expo.to_h if expo
|
|
26
|
+
data["capacitor"] = capacitor.to_h if capacitor
|
|
27
|
+
File.write(path, "#{JSON.pretty_generate(data)}\n")
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|