captive-release 0.10.0 → 0.12.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 9814ab9187493f6122ec99042578ab98456b44581b22b6c2925a5d1985cdc46f
4
- data.tar.gz: dedfed153b9ca6fa4a4b43eb1a844d3733053828eb24b0b2582ae62d1451a2ad
3
+ metadata.gz: 6a8fd7809824a87b08805de31f9ddc40af157b6ba41e0b76e19f90edd4c9ade2
4
+ data.tar.gz: 33dff63edd19f1889f5fa014271eff8640b6cba8effc7937ab9332fe2bf14316
5
5
  SHA512:
6
- metadata.gz: cea079ba56805defa3e5380cc9d145edceaa8acf799c16d859ed01da7910490b00696e2a4fee3e0b73037ca2a8db3198c93026e2f65eca2307670e65c6c73326
7
- data.tar.gz: 1c74d0e65fa4634858556cb06caa312b6dd654d366bf3bd85ad1cb0fa1c181da5c295c8e7907aef47fb80bec606ae9f6dc52400b422608e4eba520f58c5c30e5
6
+ metadata.gz: 9cfbc9751d8b2607e05512a7120f3773d3553526786e57d47a45d557e97bda793e26f09be493f015c3caaf982329f5363576a61e14399ecef1fa4157d913b352
7
+ data.tar.gz: 6a114be8ba3248b560619b8e345e1ec8e9a9d9b009b73d7723be59ba743bae25ba519f1f9baf533e772e16608741d0a77fdc1ab42007a359a6a38b49b266ffee
@@ -8,11 +8,22 @@ module CaptiveRelease
8
8
  new(argv.dup).run
9
9
  end
10
10
 
11
- def initialize(argv)
12
- @options = Options.new(argv)
11
+ def initialize(argv, update_check: UpdateCheck.new)
12
+ @options = Options.new(argv)
13
+ @update_check = update_check
13
14
  end
14
15
 
16
+ # Le verdict de mise à jour s'affiche après une commande menée à terme ; une commande
17
+ # inconnue sort avant (exit 1) et n'en a pas.
15
18
  def run
19
+ @update_check.start
20
+ dispatch
21
+ @update_check.report
22
+ end
23
+
24
+ private
25
+
26
+ def dispatch
16
27
  command = @options.next_argument
17
28
 
18
29
  case command
@@ -25,8 +36,6 @@ module CaptiveRelease
25
36
  end
26
37
  end
27
38
 
28
- private
29
-
30
39
  def run_init
31
40
  InitCommand.new(terminal: terminal).run(@options.value("--files"))
32
41
  end
@@ -0,0 +1,37 @@
1
+ # frozen_string_literal: true
2
+
3
+ module CaptiveRelease
4
+ # Complète les notes générées par GitHub, qui ne listent que les PR mergées : un commit
5
+ # poussé directement sur la branche n'y apparaît jamais, même quand il porte la feature
6
+ # qui justifie le bump.
7
+ class DirectCommits
8
+ COMMIT_VERSION = /\A🔖 Set version /
9
+
10
+ def initialize(api)
11
+ @api = api
12
+ end
13
+
14
+ # Sans plage, il n'y a pas de tag précédent : lister les commits reviendrait à lister
15
+ # l'historique entier.
16
+ def completer(notes)
17
+ plage = notes.plage or return notes
18
+ lignes = commits(*plage).map { ligne(_1) }
19
+ lignes.empty? ? notes : notes.ajouter("## Commits directs\n#{lignes.join("\n")}\n")
20
+ end
21
+
22
+ private
23
+
24
+ def commits(depuis, jusqu_a)
25
+ @api.get("compare/#{depuis}...#{jusqu_a}")["commits"].reject do |commit|
26
+ titre(commit).match?(COMMIT_VERSION) || !@api.get("commits/#{commit["sha"]}/pulls").empty?
27
+ end
28
+ end
29
+
30
+ def ligne(commit)
31
+ auteur = commit.dig("author", "login")
32
+ "* #{titre(commit)}#{" by @#{auteur}" if auteur} in #{commit["sha"]}"
33
+ end
34
+
35
+ def titre(commit) = commit.dig("commit", "message")[/.*/]
36
+ end
37
+ end
@@ -23,15 +23,22 @@ module CaptiveRelease
23
23
  @http = http || GithubHttp.new(token: token)
24
24
  end
25
25
 
26
+ def get(chemin) = analyser(@http.get(adresse(chemin)), "200")
27
+
26
28
  def post(chemin, corps, attendu:)
27
- reponse = @http.post(URI("#{HOTE}/#{depot.path(chemin)}"), JSON.generate(corps))
29
+ analyser(@http.post(adresse(chemin), JSON.generate(corps)), attendu)
30
+ end
31
+
32
+ private
33
+
34
+ def adresse(chemin) = URI("#{HOTE}/#{depot.path(chemin)}")
35
+
36
+ def analyser(reponse, attendu)
28
37
  refuser(reponse) unless reponse.code == attendu
29
38
 
30
39
  reponse.body.to_s.empty? ? nil : JSON.parse(reponse.body)
31
40
  end
32
41
 
33
- private
34
-
35
42
  def refuser(reponse)
36
43
  raise Messages.t(:github_api_error, code: reponse.code, message: GithubErrorMessage.from(reponse.body.to_s))
37
44
  end
@@ -4,7 +4,7 @@ require "net/http"
4
4
  require "uri"
5
5
 
6
6
  module CaptiveRelease
7
- # Le transport : poster sur l'API GitHub, authentifié, en suivant les redirections qu'elle
7
+ # Le transport : lire et poster sur l'API GitHub, authentifié, en suivant les redirections qu'elle
8
8
  # sert quand un dépôt a été renommé. Elle ne sait rien de ce qui est posté ni de ce qui
9
9
  # compte comme un échec, qui appartiennent à GithubApi.
10
10
  class GithubHttp
@@ -20,9 +20,17 @@ module CaptiveRelease
20
20
  @token = token
21
21
  end
22
22
 
23
+ def get(uri) = suivre(uri) { |cible| Net::HTTP::Get.new(cible.path) }
24
+
23
25
  def post(uri, corps)
26
+ suivre(uri) { |cible| Net::HTTP::Post.new(cible.path).tap { |requete| requete.body = corps } }
27
+ end
28
+
29
+ private
30
+
31
+ def suivre(uri)
24
32
  MAX_REDIRECTIONS.times do
25
- reponse = envoyer(uri, corps)
33
+ reponse = envoyer(uri, yield(uri))
26
34
  return reponse unless REDIRECTIONS.include?(reponse.code)
27
35
 
28
36
  uri = URI(ailleurs!(reponse))
@@ -31,15 +39,11 @@ module CaptiveRelease
31
39
  raise Messages.t(:too_many_redirects)
32
40
  end
33
41
 
34
- private
35
-
36
- def envoyer(uri, corps)
42
+ def envoyer(uri, requete)
37
43
  http = Net::HTTP.new(uri.host, uri.port)
38
44
  http.use_ssl = true
39
- requete = Net::HTTP::Post.new(uri.path)
40
45
  EN_TETES.each { |nom, valeur| requete[nom] = valeur }
41
46
  requete["Authorization"] = "token #{@token}"
42
- requete.body = corps
43
47
  http.request(requete)
44
48
  end
45
49
 
@@ -1,6 +1,8 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require_relative "github_api"
4
+ require_relative "direct_commits"
5
+ require_relative "release_notes"
4
6
 
5
7
  module CaptiveRelease
6
8
  # Crée la release GitHub d'une version. Elle compose ce qu'elle demande ; l'API, son
@@ -9,7 +11,6 @@ module CaptiveRelease
9
11
  # GitHub refuse (422) un corps de release plus long. Sans tag précédent, les notes
10
12
  # générées couvrent tout l'historique et dépassent vite cette limite.
11
13
  CORPS_MAX = 125_000
12
- COUPURE = "\n\n…"
13
14
 
14
15
  def self.create(version:, remote_url:, token:, api: nil)
15
16
  api ||= GithubApi.new(remote_url: remote_url, token: token)
@@ -35,21 +36,7 @@ module CaptiveRelease
35
36
 
36
37
  def notes(tag)
37
38
  corps = @api.post("releases/generate-notes", { "tag_name" => tag }, attendu: "200")["body"].to_s
38
- corps.length > CORPS_MAX ? tronquer(corps) : corps
39
- end
40
-
41
- # Coupe à une fin de ligne et garde le lien Full Changelog final, qui mène à la liste complète.
42
- def tronquer(corps)
43
- tete, pied = separer_pied(corps)
44
- place = CORPS_MAX - COUPURE.length - pied.length
45
- coupe = tete[0, place]
46
- coupe = coupe[0, coupe.rindex("\n")] if coupe.include?("\n")
47
- "#{coupe}#{COUPURE}#{pied}"
48
- end
49
-
50
- def separer_pied(corps)
51
- index = corps.rindex("\n**Full Changelog**")
52
- index ? [ corps[0, index], "\n#{corps[index..]}" ] : [ corps, "" ]
39
+ DirectCommits.new(@api).completer(ReleaseNotes.new(corps)).tronquer(CORPS_MAX).to_s
53
40
  end
54
41
  end
55
42
  end
@@ -41,6 +41,7 @@ en:
41
41
  'no': 'No'
42
42
  invalid_choice: Invalid choice.
43
43
  unknown_command: 'Unknown command: %{command}'
44
+ update_available: 'A new version of captive-release is available: %{current} → %{latest}. Run: brew upgrade captive-release'
44
45
  scanning: Scanning for version files...
45
46
  no_version_files_found: No version files found (package.json, VERSION).
46
47
  no_files_selected: No files selected.
@@ -41,6 +41,7 @@ fr:
41
41
  'no': Non
42
42
  invalid_choice: Choix invalide.
43
43
  unknown_command: 'Commande inconnue : %{command}'
44
+ update_available: 'Une nouvelle version de captive-release est disponible : %{current} → %{latest}. Lancez : brew upgrade captive-release'
44
45
  scanning: Recherche des fichiers de version...
45
46
  no_version_files_found: Aucun fichier de version trouvé (package.json, VERSION).
46
47
  no_files_selected: Aucun fichier sélectionné.
@@ -0,0 +1,19 @@
1
+ # frozen_string_literal: true
2
+
3
+ module CaptiveRelease
4
+ # L'itération d'une pré-release : même canal, dernier identifiant + 1 ; sans numéro final,
5
+ # on en ouvre un (.1). Changer de canal (alpha → beta) reste une saisie manuelle.
6
+ #
7
+ # Cette unité change quand la règle d'itération change, VersionBumper quand les types de
8
+ # bump changent.
9
+ module PrereleaseIteration
10
+ def self.next(version)
11
+ raise ArgumentError, Messages.t(:nothing_to_iterate) unless Semver.prerelease?(version)
12
+
13
+ dernier = version[/\d+\z/]
14
+ return "#{version}.1" unless dernier && version.match?(/[.-]\d+\z/)
15
+
16
+ version.delete_suffix(dernier) + (dernier.to_i + 1).to_s
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,32 @@
1
+ # frozen_string_literal: true
2
+
3
+ module CaptiveRelease
4
+ # Le texte des notes d'une release, découpé autour de son pied : le lien Full Changelog,
5
+ # qui mène à la liste complète et doit survivre à tout ajout ou toute coupe.
6
+ class ReleaseNotes
7
+ PIED = /(?:\n|\A)\*\*Full Changelog\*\*/
8
+ COMPARAISON = %r{/compare/(?<depuis>\S+?)\.\.\.(?<jusqu_a>\S+)}
9
+ COUPURE = "\n\n…"
10
+
11
+ def initialize(corps)
12
+ index = corps.rindex(PIED)
13
+ @tete, @pied = index ? [ corps[0, index], corps[index..] ] : [ corps, "" ]
14
+ end
15
+
16
+ # Les tags qui bornent les notes ; nil sans tag précédent.
17
+ def plage = @pied.match(COMPARAISON)&.captures
18
+
19
+ def ajouter(section) = ReleaseNotes.new("#{@tete.rstrip}\n\n#{section}#{@pied}")
20
+
21
+ # Coupe à une fin de ligne, pour ne pas laisser un lien ou un émoji à moitié.
22
+ def tronquer(max)
23
+ return self if to_s.length <= max
24
+
25
+ coupe = @tete[0, max - COUPURE.length - @pied.length - 1]
26
+ coupe = coupe[0, coupe.rindex("\n")] if coupe.include?("\n")
27
+ ReleaseNotes.new("#{coupe}#{COUPURE}\n#{@pied}")
28
+ end
29
+
30
+ def to_s = "#{@tete}#{@pied}"
31
+ end
32
+ end
@@ -0,0 +1,43 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "json"
4
+
5
+ module CaptiveRelease
6
+ # Signale qu'une version plus récente est publiée, à la manière de Scalingo CLI : la requête
7
+ # part au démarrage dans un thread, le verdict s'affiche après la commande. Le dépôt est
8
+ # privé, d'où le jeton ; sans jeton, pas de vérification.
9
+ #
10
+ # Cette unité change quand la source de la dernière version change. Toute erreur est muette :
11
+ # un réseau absent ne doit jamais gêner une release. L'attente est bornée par le join, pas
12
+ # par la requête : un thread encore en vol meurt avec le process.
13
+ class UpdateCheck
14
+ LATEST = URI("https://api.github.com/repos/captive-studio/captive-release/releases/latest")
15
+ ATTENTE = 2
16
+ DESACTIVE = "CAPTIVE_RELEASE_DISABLE_UPDATE_CHECKER"
17
+
18
+ def initialize(output: $stderr, token_provider: GithubToken.method(:call))
19
+ @output = output
20
+ @token_provider = token_provider
21
+ end
22
+
23
+ def start
24
+ @thread = Thread.new { derniere_version rescue nil } unless ENV[DESACTIVE] # rubocop:disable Style/RescueModifier
25
+ self
26
+ end
27
+
28
+ def report(courante = VERSION)
29
+ derniere = @thread&.join(ATTENTE)&.value
30
+ return unless derniere && Gem::Version.new(derniere) > Gem::Version.new(courante)
31
+
32
+ @output.puts "\e[31m#{Messages.t(:update_available, current: courante, latest: derniere)}\e[0m"
33
+ end
34
+
35
+ private
36
+
37
+ def derniere_version
38
+ token = @token_provider.call or return
39
+ reponse = GithubHttp.new(token: token).get(LATEST)
40
+ JSON.parse(reponse.body).fetch("tag_name").delete_prefix("v") if reponse.is_a?(Net::HTTPSuccess)
41
+ end
42
+ end
43
+ end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module CaptiveRelease
4
- VERSION = "0.10.0"
4
+ VERSION = "0.12.0"
5
5
  end
@@ -26,7 +26,7 @@ module CaptiveRelease
26
26
  when "minor" then "#{@major}.#{@minor + 1}.0"
27
27
  when "patch" then "#{@major}.#{@minor}.#{@patch + 1}"
28
28
  when PROMOTE then promouvoir
29
- when ITERATE then iterer
29
+ when ITERATE then PrereleaseIteration.next(@current)
30
30
  else saisie(type)
31
31
  end
32
32
  end
@@ -47,16 +47,6 @@ module CaptiveRelease
47
47
  Semver.core(@current)
48
48
  end
49
49
 
50
- # Même canal, dernier identifiant + 1 ; sans numéro final, on en ouvre un (.1).
51
- def iterer
52
- raise ArgumentError, Messages.t(:nothing_to_iterate) unless Semver.prerelease?(@current)
53
-
54
- dernier = @current[/\d+\z/]
55
- return "#{@current}.1" unless dernier && @current.match?(/[.-]\d+\z/)
56
-
57
- @current.delete_suffix(dernier) + (dernier.to_i + 1).to_s
58
- end
59
-
60
50
  def saisie(type)
61
51
  raise ArgumentError, Messages.t(:invalid_version, value: type) unless Semver.valid?(type)
62
52
 
@@ -3,6 +3,7 @@
3
3
  require_relative "captive_release/version"
4
4
  require_relative "captive_release/messages"
5
5
  require_relative "captive_release/semver"
6
+ require_relative "captive_release/prerelease_iteration"
6
7
  require_relative "captive_release/version_bumper"
7
8
  require_relative "captive_release/ruby_gems_version"
8
9
  require_relative "captive_release/version_format"
@@ -50,6 +51,7 @@ require_relative "captive_release/release_preconditions"
50
51
  require_relative "captive_release/release_overview"
51
52
  require_relative "captive_release/release_confirmation"
52
53
  require_relative "captive_release/release_command"
54
+ require_relative "captive_release/update_check"
53
55
  require_relative "captive_release/help"
54
56
  require_relative "captive_release/cli"
55
57
 
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: captive-release
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.10.0
4
+ version: 0.12.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Captive Studio
@@ -33,6 +33,7 @@ files:
33
33
  - lib/captive_release/config_file.rb
34
34
  - lib/captive_release/config_loader.rb
35
35
  - lib/captive_release/config_writer.rb
36
+ - lib/captive_release/direct_commits.rb
36
37
  - lib/captive_release/exclusion_input.rb
37
38
  - lib/captive_release/framework_detector.rb
38
39
  - lib/captive_release/gemfile_lock_refresh.rb
@@ -55,8 +56,10 @@ files:
55
56
  - lib/captive_release/messages.rb
56
57
  - lib/captive_release/options.rb
57
58
  - lib/captive_release/prerelease_guard.rb
59
+ - lib/captive_release/prerelease_iteration.rb
58
60
  - lib/captive_release/release_command.rb
59
61
  - lib/captive_release/release_confirmation.rb
62
+ - lib/captive_release/release_notes.rb
60
63
  - lib/captive_release/release_overview.rb
61
64
  - lib/captive_release/release_preconditions.rb
62
65
  - lib/captive_release/release_publication.rb
@@ -65,6 +68,7 @@ files:
65
68
  - lib/captive_release/semver.rb
66
69
  - lib/captive_release/shell.rb
67
70
  - lib/captive_release/terminal.rb
71
+ - lib/captive_release/update_check.rb
68
72
  - lib/captive_release/version.rb
69
73
  - lib/captive_release/version_bumper.rb
70
74
  - lib/captive_release/version_choice.rb
@@ -94,7 +98,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
94
98
  - !ruby/object:Gem::Version
95
99
  version: '0'
96
100
  requirements: []
97
- rubygems_version: 4.0.16
101
+ rubygems_version: 4.0.20
98
102
  specification_version: 4
99
103
  summary: CLI de release semver pour projets Captive
100
104
  test_files: []