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
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module CaptiveRelease
|
|
4
|
+
# Le geste de release : bumper les fichiers de version, commiter, taguer, pousser, publier.
|
|
5
|
+
#
|
|
6
|
+
# Cette unité change quand le geste change, la CLI quand l'ensemble des commandes ou le
|
|
7
|
+
# parsing des options change.
|
|
8
|
+
class ReleaseCommand
|
|
9
|
+
def initialize(terminal:, config:, token_provider:)
|
|
10
|
+
@terminal = terminal
|
|
11
|
+
@config = config
|
|
12
|
+
@token_provider = token_provider
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def run(bump_arg, confirmation:)
|
|
16
|
+
ReleasePreconditions.new(terminal: @terminal).verifier!
|
|
17
|
+
courante = @config.current_version
|
|
18
|
+
@terminal.say "\n#{@terminal.bold("captive-release")}\n"
|
|
19
|
+
@terminal.say "Current version: #{@terminal.bold(courante)}\n"
|
|
20
|
+
|
|
21
|
+
prochaine = VersionChoice.new(terminal: @terminal).call(courante, bump_arg)
|
|
22
|
+
confirmer!(prochaine) if confirmation
|
|
23
|
+
|
|
24
|
+
VersionFilesWriting.new(terminal: @terminal).call(prochaine, @config.version_files)
|
|
25
|
+
GitRelease.new(terminal: @terminal).call(prochaine, @config.version_files.map(&:path))
|
|
26
|
+
publication.call(prochaine)
|
|
27
|
+
@terminal.say "\n🎉 #{@terminal.bold("Version v#{prochaine} released successfully!")}\n"
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
private
|
|
31
|
+
|
|
32
|
+
def confirmer!(version)
|
|
33
|
+
@terminal.say "\nNew version: #{@terminal.bold(version)}"
|
|
34
|
+
reponse = @terminal.ask("Confirm? [Y/n] ").downcase
|
|
35
|
+
exit 1 unless reponse == "y" || reponse.empty?
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def publication
|
|
39
|
+
ReleasePublication.new(terminal: @terminal, token_provider: @token_provider)
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
end
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module CaptiveRelease
|
|
4
|
+
# Ce qui doit être vrai avant qu'une release parte.
|
|
5
|
+
#
|
|
6
|
+
# Le commit de version part avec --only, mais un arbre sale reste un mauvais point de
|
|
7
|
+
# départ : le tag désignerait un état que personne n'a relu.
|
|
8
|
+
#
|
|
9
|
+
# Cette unité change quand les conditions changent, ReleaseCommand quand la suite des
|
|
10
|
+
# étapes change.
|
|
11
|
+
class ReleasePreconditions
|
|
12
|
+
def initialize(terminal:)
|
|
13
|
+
@terminal = terminal
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def verifier!
|
|
17
|
+
return unless GitCommands.dirty?
|
|
18
|
+
|
|
19
|
+
@terminal.say @terminal.error("L'arbre de travail n'est pas propre.")
|
|
20
|
+
@terminal.say "Commitez ou remisez vos modifications avant de releaser.\n"
|
|
21
|
+
exit 1
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
end
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module CaptiveRelease
|
|
4
|
+
# Publie la release sur GitHub, ou dit pourquoi elle ne l'est pas. Le jeton arrive par un
|
|
5
|
+
# fournisseur et non par une valeur, pour n'être réclamé qu'au moment où il sert.
|
|
6
|
+
#
|
|
7
|
+
# Cette unité change quand les règles de publication changent, ReleaseCommand quand les
|
|
8
|
+
# étapes du geste changent.
|
|
9
|
+
class ReleasePublication
|
|
10
|
+
def initialize(terminal:, token_provider:)
|
|
11
|
+
@terminal = terminal
|
|
12
|
+
@token_provider = token_provider
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def call(version)
|
|
16
|
+
@terminal.say "\n#{@terminal.bold("Creating GitHub release...")}"
|
|
17
|
+
jeton = @token_provider.call
|
|
18
|
+
return sans_jeton unless jeton
|
|
19
|
+
|
|
20
|
+
url = GithubRelease.create(version: version, remote_url: GitCommands.remote_url, token: jeton)
|
|
21
|
+
@terminal.done url
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
private
|
|
25
|
+
|
|
26
|
+
def sans_jeton
|
|
27
|
+
@terminal.say " #{@terminal.warn("⚠️ GITHUB_TOKEN not set - skipping GitHub release creation.")}"
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "pathname"
|
|
4
|
+
|
|
5
|
+
module CaptiveRelease
|
|
6
|
+
# Où chercher dans un dépôt, et où ne pas chercher.
|
|
7
|
+
#
|
|
8
|
+
# Cette unité change quand la liste des dossiers à ignorer change. Sans elle, chaque
|
|
9
|
+
# détection réimplémentait l'exclusion, et en oublier une faisait remonter un
|
|
10
|
+
# node_modules/**/app.json comme s'il était celui du projet.
|
|
11
|
+
class RepoFiles
|
|
12
|
+
EXCLUDED_DIRS = %w[node_modules .git vendor tmp coverage dist .next .nuxt Pods].freeze
|
|
13
|
+
|
|
14
|
+
def initialize(root)
|
|
15
|
+
@root = Pathname.new(root).expand_path
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def path(*segments) = @root.join(*segments)
|
|
19
|
+
|
|
20
|
+
def glob(*segments)
|
|
21
|
+
Dir.glob(path(*segments).to_s).sort.reject { |chemin| excluded?(chemin) }
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def relative(chemin) = Pathname.new(chemin).relative_path_from(@root).to_s
|
|
25
|
+
|
|
26
|
+
private
|
|
27
|
+
|
|
28
|
+
def excluded?(chemin)
|
|
29
|
+
relatif = relative(chemin)
|
|
30
|
+
EXCLUDED_DIRS.any? do |dossier|
|
|
31
|
+
relatif.start_with?("#{dossier}/") || relatif.include?("/#{dossier}/")
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module CaptiveRelease
|
|
4
|
+
# Comment on parle au shell : échapper ce qu'on lui passe, lire ce qu'il rend, et échouer
|
|
5
|
+
# bruyamment quand la commande échoue.
|
|
6
|
+
#
|
|
7
|
+
# Cette unité change quand la façon de lancer une commande change, GitCommands quand les
|
|
8
|
+
# commandes d'une release changent.
|
|
9
|
+
module Shell
|
|
10
|
+
def self.run!(commande)
|
|
11
|
+
system(commande) || raise("Command failed: #{commande}")
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def self.capture(commande) = `#{commande}`.strip
|
|
15
|
+
|
|
16
|
+
# Rubocop veut Shellwords, mais son échappement diffère du nôtre sur des chemins que git
|
|
17
|
+
# accepte : la liste blanche dit exactement ce qui passe sans protection.
|
|
18
|
+
def self.escape(chaine) = chaine.gsub(%r{[^a-zA-Z0-9._\-/]}) { |caractere| "\\#{caractere}" }
|
|
19
|
+
end
|
|
20
|
+
end
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module CaptiveRelease
|
|
4
|
+
# Tout ce qui touche au terminal : la couleur et la saisie.
|
|
5
|
+
#
|
|
6
|
+
# Cette unité change quand l'habillage change, les commandes quand leur geste change. Les
|
|
7
|
+
# deux flux sont injectés, ce qui rend chaque commande testable sans toucher à $stdin.
|
|
8
|
+
class Terminal
|
|
9
|
+
def initialize(input: $stdin, output: $stdout)
|
|
10
|
+
@input = input
|
|
11
|
+
@output = output
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def say(message = "") = @output.puts(message)
|
|
15
|
+
|
|
16
|
+
def prompt(message) = @output.print(message)
|
|
17
|
+
|
|
18
|
+
def ask(message = "> ")
|
|
19
|
+
prompt(message)
|
|
20
|
+
@input.gets&.chomp || ""
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
# Un choix hors menu vaut 0, qui ne correspond à aucune entrée : l'appelant décide quoi
|
|
24
|
+
# en faire plutôt que de voir une exception.
|
|
25
|
+
def ask_choice(message = "\n> ") = ask(message).to_i
|
|
26
|
+
|
|
27
|
+
# La ligne d'une étape réussie : six unités la réécrivaient, et leurs deux espaces
|
|
28
|
+
# finissaient par ne plus être les mêmes.
|
|
29
|
+
def done(etape) = say(" #{ok("✅")} #{etape}")
|
|
30
|
+
|
|
31
|
+
def bold(str) = "\e[1m#{str}\e[0m"
|
|
32
|
+
def dim(str) = "\e[2m#{str}\e[0m"
|
|
33
|
+
def ok(str) = "\e[32m#{str}\e[0m"
|
|
34
|
+
def warn(str) = "\e[33m#{str}\e[0m"
|
|
35
|
+
def error(str) = "\e[31m#{str}\e[0m"
|
|
36
|
+
end
|
|
37
|
+
end
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module CaptiveRelease
|
|
4
|
+
# Comment on désigne la prochaine version : un mot-clé de bump, un semver écrit à la main,
|
|
5
|
+
# ou un menu quand rien n'est donné.
|
|
6
|
+
#
|
|
7
|
+
# Cette unité change quand les façons de nommer une version changent, ReleaseCommand quand
|
|
8
|
+
# le geste de release change.
|
|
9
|
+
class VersionChoice
|
|
10
|
+
BUMP_TYPES = %w[patch minor major].freeze
|
|
11
|
+
SEMVER_RE = /\A\d+\.\d+\.\d+\z/
|
|
12
|
+
CUSTOM = "Custom"
|
|
13
|
+
|
|
14
|
+
def initialize(terminal:)
|
|
15
|
+
@terminal = terminal
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def call(current, bump_arg)
|
|
19
|
+
bump_arg ? resolve(current, bump_arg) : ask(current)
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
private
|
|
23
|
+
|
|
24
|
+
def resolve(current, type)
|
|
25
|
+
return VersionBumper.bump(current, type) if type.match?(SEMVER_RE) || BUMP_TYPES.include?(type)
|
|
26
|
+
|
|
27
|
+
refuse("Invalid bump type: #{type}. Use patch|minor|major|x.y.z")
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def ask(current)
|
|
31
|
+
choisi = Menu.new(terminal: @terminal).choose("Choose release type:", candidates_for(current))
|
|
32
|
+
choisi == CUSTOM ? type_it : choisi
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
# La valeur rendue est la version elle-même, sauf pour Custom qui n'en a pas encore une.
|
|
36
|
+
def candidates_for(current)
|
|
37
|
+
candidats = BUMP_TYPES.to_h do |type|
|
|
38
|
+
version = VersionBumper.bump(current, type)
|
|
39
|
+
[ "#{type.capitalize} → #{@terminal.bold(version)}", version ]
|
|
40
|
+
end
|
|
41
|
+
candidats.merge("#{CUSTOM} → ?" => CUSTOM)
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def type_it
|
|
45
|
+
version = @terminal.ask("Enter version (x.y.z): ")
|
|
46
|
+
return version if version.match?(SEMVER_RE)
|
|
47
|
+
|
|
48
|
+
refuse("Invalid semver: #{version}")
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def refuse(message)
|
|
52
|
+
@terminal.say @terminal.error(message)
|
|
53
|
+
exit 1
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
end
|
|
@@ -1,84 +1,53 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
require "pathname"
|
|
4
|
-
|
|
5
3
|
module CaptiveRelease
|
|
4
|
+
# Quels fichiers d'un dépôt portent une version, et sous quel format.
|
|
6
5
|
class VersionDetector
|
|
7
|
-
|
|
6
|
+
RUBY_VERSION_PATTERN = /VERSION\s*=\s*["'].+?["']/
|
|
7
|
+
GRADLE_PATH = %w[android app build.gradle].freeze
|
|
8
8
|
|
|
9
9
|
def self.scan(root = ".")
|
|
10
10
|
new(root).scan
|
|
11
11
|
end
|
|
12
12
|
|
|
13
13
|
def initialize(root)
|
|
14
|
-
@
|
|
14
|
+
@files = RepoFiles.new(root)
|
|
15
15
|
end
|
|
16
16
|
|
|
17
17
|
def scan
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
end
|
|
25
|
-
|
|
26
|
-
def expo?
|
|
27
|
-
find_files("app.config.js", "app_config_js").any? ||
|
|
28
|
-
find_files("app.json", "plain").any? { |f| File.read(File.join(@root, f.path)).include?('"expo"') }
|
|
29
|
-
rescue
|
|
30
|
-
false
|
|
31
|
-
end
|
|
32
|
-
|
|
33
|
-
def capacitor?
|
|
34
|
-
%w[capacitor.config.ts capacitor.config.json capacitor.config.js].any? do |f|
|
|
35
|
-
File.exist?(@root.join(f))
|
|
36
|
-
end
|
|
37
|
-
rescue
|
|
38
|
-
false
|
|
18
|
+
by_name("package.json", "package_json") +
|
|
19
|
+
by_name("app.config.js", "app_config_js") +
|
|
20
|
+
by_content(%w[lib ** version.rb], "ruby_version", RUBY_VERSION_PATTERN) +
|
|
21
|
+
by_name("VERSION", "plain") +
|
|
22
|
+
android_gradle +
|
|
23
|
+
by_content(%w[ios ** Info.plist], "ios_plist", /CFBundleShortVersionString/)
|
|
39
24
|
end
|
|
40
25
|
|
|
41
26
|
private
|
|
42
27
|
|
|
43
|
-
def
|
|
44
|
-
|
|
45
|
-
return [] unless File.exist?(path) && File.read(path).match?(/versionName\s+['"]/)
|
|
46
|
-
|
|
47
|
-
[ VersionFile.new("android/app/build.gradle", "android_gradle") ]
|
|
48
|
-
rescue
|
|
49
|
-
[]
|
|
28
|
+
def by_name(filename, type)
|
|
29
|
+
@files.glob("**", filename).map { |chemin| version_file(chemin, type) }
|
|
50
30
|
end
|
|
51
31
|
|
|
52
|
-
def
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
VersionFile.new(relative, "ios_plist")
|
|
58
|
-
end
|
|
59
|
-
rescue
|
|
32
|
+
def by_content(segments, type, pattern)
|
|
33
|
+
@files.glob(*segments)
|
|
34
|
+
.select { |chemin| File.read(chemin).match?(pattern) }
|
|
35
|
+
.map { |chemin| version_file(chemin, type) }
|
|
36
|
+
rescue StandardError
|
|
60
37
|
[]
|
|
61
38
|
end
|
|
62
39
|
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
VersionFile.new(relative, "ruby_version")
|
|
69
|
-
end
|
|
70
|
-
end
|
|
40
|
+
# Le build.gradle est à un chemin fixe : le chercher par glob remonterait ceux des
|
|
41
|
+
# modules Android embarqués, qui ne portent pas la version de l'app.
|
|
42
|
+
def android_gradle
|
|
43
|
+
chemin = @files.path(*GRADLE_PATH)
|
|
44
|
+
return [] unless File.exist?(chemin) && File.read(chemin).match?(/versionName\s+['"]/)
|
|
71
45
|
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
VersionFile.new(relative, type)
|
|
76
|
-
end
|
|
46
|
+
[ VersionFile.new(GRADLE_PATH.join("/"), "android_gradle") ]
|
|
47
|
+
rescue StandardError
|
|
48
|
+
[]
|
|
77
49
|
end
|
|
78
50
|
|
|
79
|
-
def
|
|
80
|
-
relative = Pathname.new(path).relative_path_from(@root).to_s
|
|
81
|
-
EXCLUDED_DIRS.any? { |dir| relative.start_with?("#{dir}/") || relative.include?("/#{dir}/") }
|
|
82
|
-
end
|
|
51
|
+
def version_file(chemin, type) = VersionFile.new(@files.relative(chemin), type)
|
|
83
52
|
end
|
|
84
53
|
end
|
|
@@ -1,44 +1,30 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
require "json"
|
|
4
|
-
|
|
5
3
|
module CaptiveRelease
|
|
4
|
+
# Un fichier qui porte une version : son chemin, son type, et les garde-fous qui disent
|
|
5
|
+
# au développeur ce qui manque. La façon dont la version s'exprime dans le texte vit dans
|
|
6
|
+
# VersionFormat, qui ne connaît pas le disque.
|
|
6
7
|
class VersionFile
|
|
7
|
-
TYPES =
|
|
8
|
+
TYPES = VersionFormat.types
|
|
8
9
|
|
|
9
10
|
attr_reader :path, :type
|
|
10
11
|
|
|
11
12
|
def initialize(path, type)
|
|
12
13
|
raise ArgumentError, "Unknown type #{type}. Must be one of: #{TYPES.join(", ")}" unless TYPES.include?(type)
|
|
13
14
|
|
|
14
|
-
@path
|
|
15
|
-
@type
|
|
15
|
+
@path = path
|
|
16
|
+
@type = type
|
|
17
|
+
@format = VersionFormat.for(type)
|
|
16
18
|
end
|
|
17
19
|
|
|
18
20
|
def read_version
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
case type
|
|
22
|
-
when "package_json" then JSON.parse(File.read(path))["version"]
|
|
23
|
-
when "plain" then File.read(path).strip
|
|
24
|
-
when "app_config_js" then read_app_config_js
|
|
25
|
-
when "ruby_version" then read_ruby_version
|
|
26
|
-
when "android_gradle" then read_android_gradle
|
|
27
|
-
when "ios_plist" then read_ios_plist
|
|
28
|
-
end
|
|
21
|
+
@format.read(lire!) ||
|
|
22
|
+
raise("Cannot read version from #{path} : expected pattern: #{@format.expected}")
|
|
29
23
|
end
|
|
30
24
|
|
|
31
25
|
def write_version(new_version)
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
case type
|
|
35
|
-
when "package_json" then write_package_json(new_version)
|
|
36
|
-
when "plain" then File.write(path, "#{new_version}\n")
|
|
37
|
-
when "app_config_js" then write_app_config_js(new_version)
|
|
38
|
-
when "ruby_version" then write_ruby_version(new_version)
|
|
39
|
-
when "android_gradle" then write_android_gradle(new_version)
|
|
40
|
-
when "ios_plist" then write_ios_plist(new_version)
|
|
41
|
-
end
|
|
26
|
+
contenu = lire!
|
|
27
|
+
File.write(path, @format.write(contenu, read_version, new_version))
|
|
42
28
|
end
|
|
43
29
|
|
|
44
30
|
def to_h
|
|
@@ -47,61 +33,10 @@ module CaptiveRelease
|
|
|
47
33
|
|
|
48
34
|
private
|
|
49
35
|
|
|
50
|
-
def
|
|
51
|
-
|
|
52
|
-
old_version = JSON.parse(content)["version"]
|
|
53
|
-
File.write(path, content.gsub(/"version": "#{Regexp.escape(old_version)}"/, %("version": "#{new_version}")))
|
|
54
|
-
end
|
|
55
|
-
|
|
56
|
-
def read_ruby_version
|
|
57
|
-
File.read(path).match(/VERSION\s*=\s*["'](.+?)["']/)[1]
|
|
58
|
-
rescue
|
|
59
|
-
raise "Cannot read version from #{path} — expected pattern: VERSION = \"x.y.z\""
|
|
60
|
-
end
|
|
61
|
-
|
|
62
|
-
def write_ruby_version(new_version)
|
|
63
|
-
content = File.read(path)
|
|
64
|
-
old_version = read_ruby_version
|
|
65
|
-
File.write(path, content.gsub(/VERSION\s*=\s*["']#{Regexp.escape(old_version)}["']/) do |match|
|
|
66
|
-
match.include?('"') ? %(VERSION = "#{new_version}") : %(VERSION = '#{new_version}')
|
|
67
|
-
end)
|
|
68
|
-
end
|
|
69
|
-
|
|
70
|
-
def read_app_config_js
|
|
71
|
-
File.read(path).match(/version:\s*['"](.+?)['"]/)[1]
|
|
72
|
-
rescue
|
|
73
|
-
raise "Cannot read version from #{path} — expected pattern: version: 'x.y.z'"
|
|
74
|
-
end
|
|
75
|
-
|
|
76
|
-
def write_app_config_js(new_version)
|
|
77
|
-
content = File.read(path)
|
|
78
|
-
old_version = read_app_config_js
|
|
79
|
-
File.write(path, content.gsub(/version:\s*['"]#{Regexp.escape(old_version)}['"]/, "version: '#{new_version}'"))
|
|
80
|
-
end
|
|
81
|
-
|
|
82
|
-
def read_android_gradle
|
|
83
|
-
File.read(path).match(/versionName\s+['"](.+?)['"]/)&.[](1) ||
|
|
84
|
-
raise("Cannot read version from #{path} — expected pattern: versionName 'x.y.z'")
|
|
85
|
-
end
|
|
86
|
-
|
|
87
|
-
def write_android_gradle(new_version)
|
|
88
|
-
content = File.read(path)
|
|
89
|
-
old_version = read_android_gradle
|
|
90
|
-
File.write(path, content.gsub(/versionName\s+['"]#{Regexp.escape(old_version)}['"]/, "versionName '#{new_version}'"))
|
|
91
|
-
end
|
|
92
|
-
|
|
93
|
-
def read_ios_plist
|
|
94
|
-
File.read(path).match(/<key>CFBundleShortVersionString<\/key>\s*<string>(.+?)<\/string>/)&.[](1) ||
|
|
95
|
-
raise("Cannot read version from #{path} — expected CFBundleShortVersionString key")
|
|
96
|
-
end
|
|
36
|
+
def lire!
|
|
37
|
+
raise "File not found: #{path}" unless File.exist?(path)
|
|
97
38
|
|
|
98
|
-
|
|
99
|
-
content = File.read(path)
|
|
100
|
-
old_version = read_ios_plist
|
|
101
|
-
File.write(path, content.gsub(
|
|
102
|
-
/(<key>CFBundleShortVersionString<\/key>\s*<string>)#{Regexp.escape(old_version)}(<\/string>)/,
|
|
103
|
-
"\\1#{new_version}\\2"
|
|
104
|
-
))
|
|
39
|
+
File.read(path)
|
|
105
40
|
end
|
|
106
41
|
end
|
|
107
42
|
end
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module CaptiveRelease
|
|
4
|
+
# Le dépôt ne porte aucun fichier de version : proposer d'en créer un, et le créer.
|
|
5
|
+
#
|
|
6
|
+
# Cette unité change quand ce que reçoit un dépôt nu change, InitCommand quand la façon de
|
|
7
|
+
# réunir les fichiers change.
|
|
8
|
+
class VersionFileBootstrap
|
|
9
|
+
VERSION_INITIALE = "0.1.0"
|
|
10
|
+
|
|
11
|
+
def initialize(terminal:)
|
|
12
|
+
@terminal = terminal
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def call(motif)
|
|
16
|
+
@terminal.say @terminal.warn(motif)
|
|
17
|
+
@terminal.say "\nCreate a #{@terminal.bold("VERSION")} file? [y/N] "
|
|
18
|
+
exit 1 unless @terminal.ask.downcase == "y"
|
|
19
|
+
|
|
20
|
+
File.write("VERSION", "#{VERSION_INITIALE}\n")
|
|
21
|
+
@terminal.done "VERSION créé avec #{@terminal.bold(VERSION_INITIALE)}"
|
|
22
|
+
[ VersionFile.new("VERSION", "plain") ]
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module CaptiveRelease
|
|
4
|
+
# Parmi les fichiers de version détectés, lesquels retenir.
|
|
5
|
+
#
|
|
6
|
+
# La saisie nomme ce qu'on ne veut **pas** : sur un dépôt qui porte dix fichiers de
|
|
7
|
+
# version, exclure est plus court que tout énumérer. Cette unité change quand la façon de
|
|
8
|
+
# choisir change, InitCommand quand ce qu'on fait du choix change.
|
|
9
|
+
class VersionFileSelection
|
|
10
|
+
def initialize(terminal:)
|
|
11
|
+
@terminal = terminal
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def call(found)
|
|
15
|
+
list(found)
|
|
16
|
+
found.size == 1 ? seul(found) : exclude(found, demander)
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
private
|
|
20
|
+
|
|
21
|
+
def list(found)
|
|
22
|
+
found.each_with_index do |fichier, rang|
|
|
23
|
+
@terminal.say " #{@terminal.dim("[#{rang + 1}]")} #{fichier.path} " \
|
|
24
|
+
"#{@terminal.dim("(#{readable_version(fichier)})")}"
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def demander
|
|
29
|
+
@terminal.say "\nAppuyez sur #{@terminal.bold("Entrée")} pour tout sélectionner, " \
|
|
30
|
+
"ou entrez les numéros à #{@terminal.bold("exclure")} (ex: 2,3) :"
|
|
31
|
+
@terminal.ask
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def seul(found)
|
|
35
|
+
@terminal.say ""
|
|
36
|
+
@terminal.done "1 fichier détecté : sélectionné automatiquement."
|
|
37
|
+
found
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def exclude(found, saisie)
|
|
41
|
+
return found if saisie.strip.empty?
|
|
42
|
+
|
|
43
|
+
rangs = saisie.split(",").map { |numero| numero.strip.to_i - 1 }
|
|
44
|
+
found.reject.with_index { |_, rang| rangs.include?(rang) }
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def readable_version(fichier)
|
|
48
|
+
fichier.read_version
|
|
49
|
+
rescue StandardError
|
|
50
|
+
"?"
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
end
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module CaptiveRelease
|
|
4
|
+
# Lit la valeur de `--files` : une liste de chemins séparés par des virgules, chacun pouvant
|
|
5
|
+
# nommer son type après un deux-points.
|
|
6
|
+
#
|
|
7
|
+
# Cette unité change quand la forme de l'argument change, InitCommand quand ce qu'on fait
|
|
8
|
+
# des fichiers change.
|
|
9
|
+
module VersionFilesArgument
|
|
10
|
+
def self.parse(valeur)
|
|
11
|
+
valeur.split(",").map(&:strip).map do |entree|
|
|
12
|
+
chemin, type = entree.split(":")
|
|
13
|
+
VersionFile.new(chemin, type || type_deduit(chemin))
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def self.type_deduit(chemin)
|
|
18
|
+
File.basename(chemin) == "package.json" ? "package_json" : "plain"
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
end
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module CaptiveRelease
|
|
4
|
+
# Poser la nouvelle version dans chaque fichier qui la porte, et le dire au fur et à mesure :
|
|
5
|
+
# une release qui s'arrête doit laisser voir jusqu'où elle est allée.
|
|
6
|
+
#
|
|
7
|
+
# Cette unité change quand la façon d'écrire change, ReleaseCommand quand la suite des
|
|
8
|
+
# étapes change.
|
|
9
|
+
class VersionFilesWriting
|
|
10
|
+
def initialize(terminal:)
|
|
11
|
+
@terminal = terminal
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def call(version, fichiers)
|
|
15
|
+
@terminal.say "\n#{@terminal.bold("Updating version files...")}"
|
|
16
|
+
fichiers.each do |fichier|
|
|
17
|
+
fichier.write_version(version)
|
|
18
|
+
@terminal.done fichier.path
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
end
|