captive-release 0.4.3 → 0.5.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 (41) hide show
  1. checksums.yaml +4 -4
  2. data/lib/captive_release/build_command.rb +52 -0
  3. data/lib/captive_release/build_config.rb +20 -0
  4. data/lib/captive_release/build_config_prompt.rb +31 -0
  5. data/lib/captive_release/build_dispatch_run.rb +35 -0
  6. data/lib/captive_release/changes_since.rb +18 -0
  7. data/lib/captive_release/cli.rb +27 -382
  8. data/lib/captive_release/config.rb +2 -44
  9. data/lib/captive_release/config_file.rb +30 -0
  10. data/lib/captive_release/config_loader.rb +34 -0
  11. data/lib/captive_release/config_writer.rb +45 -0
  12. data/lib/captive_release/framework_detector.rb +30 -0
  13. data/lib/captive_release/git_commands.rb +19 -26
  14. data/lib/captive_release/git_release.rb +24 -0
  15. data/lib/captive_release/github_api.rb +45 -0
  16. data/lib/captive_release/github_dispatch.rb +12 -27
  17. data/lib/captive_release/github_http.rb +34 -25
  18. data/lib/captive_release/github_release.rb +16 -32
  19. data/lib/captive_release/github_repo.rb +31 -0
  20. data/lib/captive_release/github_token.rb +18 -0
  21. data/lib/captive_release/help.rb +50 -0
  22. data/lib/captive_release/init_command.rb +44 -0
  23. data/lib/captive_release/menu.rb +33 -0
  24. data/lib/captive_release/options.rb +27 -0
  25. data/lib/captive_release/release_command.rb +50 -0
  26. data/lib/captive_release/release_preconditions.rb +24 -0
  27. data/lib/captive_release/release_publication.rb +30 -0
  28. data/lib/captive_release/repo_files.rb +35 -0
  29. data/lib/captive_release/shell.rb +20 -0
  30. data/lib/captive_release/terminal.rb +40 -0
  31. data/lib/captive_release/version.rb +1 -1
  32. data/lib/captive_release/version_choice.rb +56 -0
  33. data/lib/captive_release/version_detector.rb +26 -57
  34. data/lib/captive_release/version_file.rb +14 -79
  35. data/lib/captive_release/version_file_bootstrap.rb +25 -0
  36. data/lib/captive_release/version_file_selection.rb +53 -0
  37. data/lib/captive_release/version_files_argument.rb +21 -0
  38. data/lib/captive_release/version_files_writing.rb +22 -0
  39. data/lib/captive_release/version_format.rb +86 -0
  40. data/lib/captive_release.rb +30 -0
  41. metadata +30 -1
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 87ceb43e08f3eacbf2ee69552a5b8011fe25798929ea0d476f9af8e882b48f2f
4
- data.tar.gz: 27ced4d77d33181bea207d245704410d18e2c599e60b5d6df6b56f28fef992a1
3
+ metadata.gz: 03403604d3d533136b2f1901345f1a1192d698ec43ac8e54589e966e33d8a328
4
+ data.tar.gz: f822d4f46347d765a3c1beb5415b5e19d223f35f55a94592555d38fe4633ac4c
5
5
  SHA512:
6
- metadata.gz: 5eefc83ed0eddfbb557763b3c4cdc2a9a2206d990b155a0d56c869ce4f4c65aae19e6140ac415580a4b1fd4c1990f93f62cd857f1df7b6995aec83ddd7d85e4e
7
- data.tar.gz: 445fa1fffd733617434c9e4e7d6f535b3460ad446606e031b449afd5eaaddd67e6f603fdcada943335d6219739eae7110c9c1925201c694aa64db25fcd53b01f
6
+ metadata.gz: c7db3c9d1f2289de5eaf34d7057dd8abdc2e1d39c3d52eedcca661f3f8d494810b36e130320137bb9c58993e33ecef257407ee6cfd8ff5caceb036975c1a4a83
7
+ data.tar.gz: d4f457a4cc32c465ca677def528172deb6d0efb2e48bc95ae5911c9ca742a34c0d1a79abfa2c31551383c04531ec16569745e84e15786e97cd462fe4bb5b48ea
@@ -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
@@ -0,0 +1,18 @@
1
+ # frozen_string_literal: true
2
+
3
+ module CaptiveRelease
4
+ # Le lien vers les commits inclus depuis la dernière version taguée. Absent tant que ce
5
+ # tag n'existe pas ou que le remote ne mène pas à GitHub — la release continue sans lui.
6
+ #
7
+ # Cette unité change quand la façon de comparer deux versions change, ReleaseCommand quand
8
+ # le moment où on l'affiche change.
9
+ class ChangesSince
10
+ def self.url(version)
11
+ return nil unless GitCommands.tag?("v#{version}")
12
+
13
+ GithubRepo.from_remote(GitCommands.remote_url).compare_url("v#{version}", GitCommands.current_branch)
14
+ rescue StandardError
15
+ nil
16
+ end
17
+ end
18
+ end
@@ -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
- @argv = argv
12
+ @options = Options.new(argv)
14
13
  end
15
14
 
16
15
  def run
17
- command = @argv.shift
16
+ command = @options.next_argument
18
17
 
19
18
  case command
20
- when "init" then run_init
21
- when "new" then run_release
22
- when "build" then run_build
23
- when "version", "--version", "-v" then puts "captive-release #{VERSION}"
24
- when "help", "--help", "-h", nil then print_help
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
- files_arg = extract_option("--files")
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
- def detect_type(path)
165
- File.basename(path) == "package.json" ? "package_json" : "plain"
166
- end
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
- bump_arg = @argv.shift
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
- def resolve_version(current, type)
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
- profile_arg = extract_option("--profile")
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 github_token
353
- return ENV["GITHUB_TOKEN"] unless ENV["GITHUB_TOKEN"].to_s.empty?
47
+ def release_command = ReleaseCommand.new(**collaborateurs)
354
48
 
355
- token = `gh auth token 2>/dev/null`.strip
356
- token.empty? ? nil : token
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 safe_read(file)
360
- file.read_version
361
- rescue
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 extract_option(flag)
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 bold(str) = "\e[1m#{str}\e[0m"
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
- ExpoConfig = Struct.new(:build_workflow, keyword_init: true) do
9
- def self.from_hash(h)
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