fastlane-plugin-wpmreleasetoolkit 14.10.0 → 14.11.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/lib/fastlane/plugin/wpmreleasetoolkit/actions/ios/ios_lint_localizations.rb +12 -7
- data/lib/fastlane/plugin/wpmreleasetoolkit/actions/macos/macos_verify_code_signing.rb +146 -0
- data/lib/fastlane/plugin/wpmreleasetoolkit/helper/github_helper.rb +19 -1
- data/lib/fastlane/plugin/wpmreleasetoolkit/helper/ios/ios_l10n_helper.rb +43 -15
- data/lib/fastlane/plugin/wpmreleasetoolkit/helper/ios/ios_strings_file_validation_helper.rb +214 -11
- data/lib/fastlane/plugin/wpmreleasetoolkit/version.rb +1 -1
- metadata +6 -149
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 80e9dbdf5a3843f12c90985ff55ff40660ef4f7e2346119992c9d344c9b90697
|
|
4
|
+
data.tar.gz: 495ce50a196928419b024c54647f1656c731aaabb5bfba446d1817baec978d45
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 53c1b51793d9cafaea27b5af8ddd0ec92a4b29ea2942078102e6bdf6007d21b92ed5c2910a52d1796f46c400babfd1f44ea53e4a0671f07ea06e7a6e4de97192
|
|
7
|
+
data.tar.gz: 1d65caa4489b6f54e2384bc939ddd9b8bd0fd50933fecf353f7e86d3eda1240891633769d6a229d14627fb8f387d590f0a9a3880a362371d48329fdce1338a20
|
|
@@ -56,15 +56,20 @@ module Fastlane
|
|
|
56
56
|
language = File.basename(File.dirname(file), '.lproj')
|
|
57
57
|
path = File.join(params[:input_dir], file)
|
|
58
58
|
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
duplicate_keys[language] =
|
|
63
|
-
|
|
59
|
+
status, payload = Fastlane::Helper::Ios::StringsFileValidationHelper.scan_for_duplicate_keys(file: path)
|
|
60
|
+
case status
|
|
61
|
+
when :scanned
|
|
62
|
+
duplicate_keys[language] = payload.map { |key, value| "`#{key}` was found at multiple lines: #{value.join(', ')}" } unless payload.empty?
|
|
63
|
+
when :unsupported_format
|
|
64
64
|
UI.important <<~WRONG_FORMAT
|
|
65
|
-
File `#{path}` is in #{
|
|
66
|
-
Since your files are in #{
|
|
65
|
+
File `#{path}` is in #{payload} format, while finding duplicate keys can only occur on files that are in ASCII-plist format.
|
|
66
|
+
Since your files are in #{payload} format, you should probably disable the `check_duplicate_keys` option from this `#{action_name}` call.
|
|
67
67
|
WRONG_FORMAT
|
|
68
|
+
when :unscannable
|
|
69
|
+
UI.important <<~UNSCANNABLE
|
|
70
|
+
Could not check `#{path}` for duplicate keys: #{payload.strip}
|
|
71
|
+
The file parses as a property list but isn't a flat `.strings` file the duplicate-key scanner understands, so duplicate detection was skipped for it.
|
|
72
|
+
UNSCANNABLE
|
|
68
73
|
end
|
|
69
74
|
end
|
|
70
75
|
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'fastlane/action'
|
|
4
|
+
require 'fastlane_core/ui/ui'
|
|
5
|
+
|
|
6
|
+
module Fastlane
|
|
7
|
+
module Actions
|
|
8
|
+
class MacosVerifyCodeSigningAction < Action
|
|
9
|
+
def self.run(params)
|
|
10
|
+
paths = params[:artifact_path]
|
|
11
|
+
UI.user_error!('No artifact to verify: `artifact_path` is empty') if paths.empty?
|
|
12
|
+
|
|
13
|
+
paths.each do |path|
|
|
14
|
+
UI.user_error!("There is no artifact at #{path}") unless File.exist?(path)
|
|
15
|
+
|
|
16
|
+
UI.message("Verifying #{path}")
|
|
17
|
+
|
|
18
|
+
case File.extname(path).downcase
|
|
19
|
+
when '.app'
|
|
20
|
+
verify_app_bundle(path: path, expected_authority: params[:expected_authority], verify_notarization: params[:verify_notarization])
|
|
21
|
+
when '.dmg'
|
|
22
|
+
verify_disk_image(path: path, expected_authority: params[:expected_authority], verify_notarization: params[:verify_notarization])
|
|
23
|
+
else
|
|
24
|
+
UI.user_error!("Don't know how to verify #{path}. Supported artifacts are `.app` bundles and `.dmg` disk images")
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
UI.success("Verified #{paths.length} artifact(s)")
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def self.verify_app_bundle(path:, expected_authority:, verify_notarization:)
|
|
32
|
+
UI.user_error!("#{path} is not signed at all") unless signed?(path, deep: true)
|
|
33
|
+
verify_authority!(path: path, expected_authority: expected_authority) unless expected_authority.nil?
|
|
34
|
+
|
|
35
|
+
return unless verify_notarization
|
|
36
|
+
|
|
37
|
+
verify!("#{path} was rejected by Gatekeeper", 'spctl', '--assess', '--type', 'execute', '--verbose=2', path)
|
|
38
|
+
verify!("#{path} has no notarization ticket stapled to it", 'xcrun', 'stapler', 'validate', path)
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
# Unlike an app bundle, a disk image is often not signed at all — `electron-builder`, for one,
|
|
42
|
+
# only signs the app inside it. Gatekeeper then rejects the image itself with `no usable
|
|
43
|
+
# signature` even when it carries a notarization ticket, so the stapled ticket is the only
|
|
44
|
+
# check that means anything for an unsigned image.
|
|
45
|
+
#
|
|
46
|
+
def self.verify_disk_image(path:, expected_authority:, verify_notarization:)
|
|
47
|
+
# Said up front because `sh` logs a non-zero exit in red regardless of it being handled,
|
|
48
|
+
# which reads as a broken build in the CI log of an otherwise passing job.
|
|
49
|
+
UI.message("Checking whether #{path} is signed. Disk images usually aren't, so a `codesign` failure due to the image being unsigned is expected and tolerated (other signature failures will still fail).")
|
|
50
|
+
|
|
51
|
+
if signed?(path)
|
|
52
|
+
verify_authority!(path: path, expected_authority: expected_authority) unless expected_authority.nil?
|
|
53
|
+
verify!("#{path} was rejected by Gatekeeper", 'spctl', '--assess', '--type', 'open', '--context', 'context:primary-signature', '--verbose=2', path) if verify_notarization
|
|
54
|
+
else
|
|
55
|
+
UI.important("#{path} is not signed — skipping its signature checks. We expect that the app it contains is the one carrying the signature.")
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
verify!("#{path} has no notarization ticket stapled to it", 'xcrun', 'stapler', 'validate', path) if verify_notarization
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
# Distinguishes an artifact that carries no signature at all from one whose signature is
|
|
62
|
+
# broken: the former is expected for a disk image, the latter always a failure.
|
|
63
|
+
#
|
|
64
|
+
# @param deep [Boolean] Whether to also verify the nested code an app bundle embeds.
|
|
65
|
+
#
|
|
66
|
+
def self.signed?(path, deep: false)
|
|
67
|
+
command = ['codesign', '--verify']
|
|
68
|
+
command << '--deep' if deep
|
|
69
|
+
command += ['--strict', '--verbose=2', path]
|
|
70
|
+
|
|
71
|
+
exitstatus, output = sh(*command) { |status, result, _| [status.exitstatus, result] }
|
|
72
|
+
|
|
73
|
+
return true if exitstatus.zero?
|
|
74
|
+
return false if output.include?('not signed at all')
|
|
75
|
+
|
|
76
|
+
UI.user_error!("The code signature of #{path} is not valid:\n#{output}")
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
def self.verify!(error_message, *command)
|
|
80
|
+
sh(*command, error_callback: ->(_) { UI.user_error!(error_message) })
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
def self.verify_authority!(path:, expected_authority:)
|
|
84
|
+
details = sh('codesign', '--display', '--verbose=2', path)
|
|
85
|
+
return if details.include?("Authority=#{expected_authority}")
|
|
86
|
+
|
|
87
|
+
UI.user_error!("#{path} is not signed by '#{expected_authority}':\n#{details}")
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
#####################################################
|
|
91
|
+
# @!group Documentation
|
|
92
|
+
#####################################################
|
|
93
|
+
|
|
94
|
+
def self.description
|
|
95
|
+
'Verify that macOS artifacts are properly code signed and notarized'
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
def self.details
|
|
99
|
+
<<~DETAILS
|
|
100
|
+
Verify that the given macOS artifacts are signed, and optionally notarized.
|
|
101
|
+
|
|
102
|
+
The checks that apply are picked from the artifact's extension:
|
|
103
|
+
|
|
104
|
+
- `.app` — the signature is valid and satisfies its designated requirement, Gatekeeper accepts
|
|
105
|
+
the bundle for execution, and a notarization ticket is stapled to it.
|
|
106
|
+
- `.dmg` — if the image is signed, the signature is valid (and can be checked against the expected authority) and Gatekeeper accepts opening it; when `verify_notarization` is true, a notarization ticket is stapled to the image.
|
|
107
|
+
DETAILS
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
def self.available_options
|
|
111
|
+
[
|
|
112
|
+
FastlaneCore::ConfigItem.new(
|
|
113
|
+
key: :artifact_path,
|
|
114
|
+
description: 'The path, or list of paths, to the `.app` bundle(s) or `.dmg` disk image(s) to verify',
|
|
115
|
+
type: Array,
|
|
116
|
+
verify_block: proc do |value|
|
|
117
|
+
UI.user_error!('`artifact_path` must be a String or an Array of Strings') unless value.all?(String)
|
|
118
|
+
end
|
|
119
|
+
),
|
|
120
|
+
FastlaneCore::ConfigItem.new(
|
|
121
|
+
key: :expected_authority,
|
|
122
|
+
description: 'The signing authority the artifact is expected to be signed by, e.g. `Developer ID Application: ACME, Inc. (ABCDE12345)`. ' \
|
|
123
|
+
+ 'When omitted, any valid signature is accepted',
|
|
124
|
+
type: String,
|
|
125
|
+
optional: true,
|
|
126
|
+
default_value: nil
|
|
127
|
+
),
|
|
128
|
+
FastlaneCore::ConfigItem.new(
|
|
129
|
+
key: :verify_notarization,
|
|
130
|
+
description: 'Whether to also assert that the artifact is accepted by Gatekeeper and has a notarization ticket stapled to it',
|
|
131
|
+
type: Boolean,
|
|
132
|
+
default_value: true
|
|
133
|
+
),
|
|
134
|
+
]
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
def self.authors
|
|
138
|
+
['Automattic']
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
def self.is_supported?(platform)
|
|
142
|
+
platform == :mac
|
|
143
|
+
end
|
|
144
|
+
end
|
|
145
|
+
end
|
|
146
|
+
end
|
|
@@ -200,7 +200,7 @@ module Fastlane
|
|
|
200
200
|
# @raise [Fastlane::UI::Error] UI.user_error! if the release does not exist.
|
|
201
201
|
#
|
|
202
202
|
def get_release(repository:, version:)
|
|
203
|
-
release =
|
|
203
|
+
release = find_release(repository: repository, version: version)
|
|
204
204
|
return release unless release.nil?
|
|
205
205
|
|
|
206
206
|
UI.user_error!("Could not find GitHub Release for tag #{version} in #{repository}")
|
|
@@ -244,6 +244,24 @@ module Fastlane
|
|
|
244
244
|
release.html_url
|
|
245
245
|
end
|
|
246
246
|
|
|
247
|
+
def find_release(repository:, version:)
|
|
248
|
+
release = client.releases(repository).find { |candidate| candidate.tag_name == version }
|
|
249
|
+
return release unless release.nil?
|
|
250
|
+
|
|
251
|
+
release_for_tag(repository: repository, version: version)
|
|
252
|
+
end
|
|
253
|
+
|
|
254
|
+
def release_for_tag(repository:, version:)
|
|
255
|
+
client.release_for_tag(repository, version)
|
|
256
|
+
rescue Octokit::NotFound
|
|
257
|
+
# A 404 only means the fallback did not find a release for this tag.
|
|
258
|
+
# Other API errors should be surfaced.
|
|
259
|
+
nil
|
|
260
|
+
end
|
|
261
|
+
|
|
262
|
+
private :find_release
|
|
263
|
+
private :release_for_tag
|
|
264
|
+
|
|
247
265
|
# Use the GitHub API to generate release notes based on the list of PRs between current tag and previous tag.
|
|
248
266
|
# @note This API uses the `.github/release.yml` config file to classify the PRs by category in the generated list according to PR labels.
|
|
249
267
|
#
|
|
@@ -15,18 +15,24 @@ module Fastlane
|
|
|
15
15
|
# Returns the type of a `.strings` file (XML, binary or ASCII)
|
|
16
16
|
#
|
|
17
17
|
# @param [String] path The path to the `.strings` file to check
|
|
18
|
+
# @param [Boolean] assume_valid Skip the `plutil -lint` validity check when the caller has already
|
|
19
|
+
# confirmed the file parses (e.g. via `read_strings_file_as_hash`), avoiding a redundant
|
|
20
|
+
# `plutil` invocation. Only the format detection (`file`) then runs.
|
|
18
21
|
# @return [Symbol] The file format used by the `.strings` file. Can be one of:
|
|
19
22
|
# - `:text` for the ASCII-plist file format (containing typical `"key" = "value";` lines)
|
|
20
23
|
# - `:xml` for XML plist file format (can be used if machine-generated, especially since there's no official way/tool to generate the ASCII-plist file format as output)
|
|
21
24
|
# - `:binary` for binary plist file format (usually only true for `.strings` files converted by Xcode at compile time and included in the final `.app`/`.ipa`)
|
|
22
25
|
# - `nil` if the file does not exist or is neither of those format (e.g. not a `.strings` file at all)
|
|
23
26
|
#
|
|
24
|
-
def self.strings_file_type(path:)
|
|
27
|
+
def self.strings_file_type(path:, assume_valid: false)
|
|
25
28
|
return :text if File.empty?(path) # If completely empty file, consider it as a valid `.strings` files in textual format
|
|
26
29
|
|
|
27
|
-
# Start by checking it seems like a valid property-list file (and not e.g. an image or plain text file)
|
|
28
|
-
|
|
29
|
-
|
|
30
|
+
# Start by checking it seems like a valid property-list file (and not e.g. an image or plain text file).
|
|
31
|
+
# A caller that has already parsed the file can skip this redundant check via `assume_valid: true`.
|
|
32
|
+
unless assume_valid
|
|
33
|
+
_, status = Open3.capture2('/usr/bin/plutil', '-lint', path)
|
|
34
|
+
return nil unless status.success?
|
|
35
|
+
end
|
|
30
36
|
|
|
31
37
|
# If it is a valid property-list file, determine the actual format used
|
|
32
38
|
format_desc, status = Open3.capture2('/usr/bin/file', path)
|
|
@@ -72,6 +78,11 @@ module Fastlane
|
|
|
72
78
|
# @note The method is able to handle input files which are using different encodings,
|
|
73
79
|
# guessing the encoding of each input file using the BOM (and defaulting to UTF8).
|
|
74
80
|
# The generated file will always be in utf-8, by convention.
|
|
81
|
+
# @note Dictionary- and array-valued entries (`"k" = { … };`, `"k" = ( … );`, nesting allowed) are
|
|
82
|
+
# prefixed on their outer key with the value preserved verbatim. If a file still holds some
|
|
83
|
+
# construct the tokenizer can't rewrite, its lines are copied through unprefixed with a warning
|
|
84
|
+
# (and its keys are then bookkept unprefixed too, so the reported duplicates stay accurate)
|
|
85
|
+
# rather than aborting the whole merge.
|
|
75
86
|
#
|
|
76
87
|
# @raise [RuntimeError] If one of the paths provided is not in text format (but XML or binary instead), or if any of the files are missing.
|
|
77
88
|
#
|
|
@@ -88,20 +99,37 @@ module Fastlane
|
|
|
88
99
|
raise "The file `#{input_file}` does not exist or is of unknown format." if fmt.nil?
|
|
89
100
|
raise "The file `#{input_file}` is in #{fmt} format but we currently only support merging `.strings` files in text format." unless fmt == :text
|
|
90
101
|
|
|
91
|
-
|
|
92
|
-
duplicates += (string_keys & all_keys_found) # Find duplicates using Array intersection, and add those to duplicates list
|
|
93
|
-
all_keys_found += string_keys
|
|
102
|
+
raw_keys = read_strings_file_as_hash(path: input_file).keys
|
|
94
103
|
|
|
95
104
|
tmp_file.write("/* MARK: - #{File.basename(input_file)} */\n\n")
|
|
96
|
-
#
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
105
|
+
# Add the prefix to every key. We tokenize via `StringsFileValidationHelper.prefix_keys` rather than
|
|
106
|
+
# matching keys with a line-based regex, so that keys are found regardless of where `.strings` comments
|
|
107
|
+
# sit (e.g. `CFBundleName /* note */ = WordPress;`) and `key = value`-looking text inside a comment is
|
|
108
|
+
# left alone. It also handles dictionary/array values (`"k" = { … };`) — prefixing the outer key and
|
|
109
|
+
# copying the value verbatim.
|
|
110
|
+
lines = read_utf8_lines(input_file)
|
|
111
|
+
applied_prefix = prefix
|
|
112
|
+
begin
|
|
113
|
+
lines = Fastlane::Helper::Ios::StringsFileValidationHelper.prefix_keys(lines: lines, prefix: prefix)
|
|
114
|
+
rescue StandardError => e
|
|
115
|
+
# `plutil` may still accept a construct the tokenizer can't rewrite: it parses fine (so the file
|
|
116
|
+
# clears the `:text` gate above) yet `prefix_keys` raises on it. Fail soft: copy this file's lines
|
|
117
|
+
# through unprefixed rather than aborting the whole merge — mirroring the scanner path, where
|
|
118
|
+
# `scan_for_duplicate_keys` returns `:unscannable` instead of crashing the lane. `lines` is untouched
|
|
119
|
+
# by the raise (the assignment above never completes), so it still holds the original file contents,
|
|
120
|
+
# and `applied_prefix` records that the keys went out *unprefixed* so the bookkeeping below matches.
|
|
121
|
+
applied_prefix = ''
|
|
122
|
+
UI.important("Could not add prefix `#{prefix}` to the keys in `#{input_file}` (#{e.message}); copying its lines through unprefixed.")
|
|
104
123
|
end
|
|
124
|
+
|
|
125
|
+
# Bookkeep the keys as they were actually written — prefixed, or unprefixed on the fail-soft path.
|
|
126
|
+
# Doing this *after* the rewrite keeps the reported duplicates consistent with the merged file even
|
|
127
|
+
# when prefixing fell back, so a genuine collision is still surfaced rather than silently collapsed.
|
|
128
|
+
string_keys = raw_keys.map { |k| "#{applied_prefix}#{k}" }
|
|
129
|
+
duplicates += (string_keys & all_keys_found) # Find duplicates using Array intersection, and add those to duplicates list
|
|
130
|
+
all_keys_found += string_keys
|
|
131
|
+
|
|
132
|
+
lines.each { |line| tmp_file.write(line) }
|
|
105
133
|
tmp_file.write("\n")
|
|
106
134
|
end
|
|
107
135
|
tmp_file.close # ensure we flush the content to disk
|
|
@@ -5,24 +5,101 @@ module Fastlane
|
|
|
5
5
|
module Ios
|
|
6
6
|
class StringsFileValidationHelper
|
|
7
7
|
# context can be one of:
|
|
8
|
-
# :root, :maybe_comment_start, :in_line_comment, :in_block_comment,
|
|
9
|
-
# :maybe_block_comment_end, :in_quoted_key,
|
|
10
|
-
# :after_quoted_key_before_eq, :
|
|
11
|
-
# :in_quoted_value, :after_quoted_value
|
|
12
|
-
|
|
8
|
+
# :root, :maybe_comment_start, :maybe_comment_or_value, :in_line_comment, :in_block_comment,
|
|
9
|
+
# :maybe_block_comment_end, :in_quoted_key, :in_unquoted_key,
|
|
10
|
+
# :after_quoted_key_before_eq, :after_quoted_key_and_eq,
|
|
11
|
+
# :in_quoted_value, :in_unquoted_value, :after_quoted_value
|
|
12
|
+
#
|
|
13
|
+
# `resume_context` holds the context to return to once a comment ends. Comments are valid not only at
|
|
14
|
+
# the top level but also *between* the tokens of a statement (e.g. `"key" /* note */ = "value";`), so a
|
|
15
|
+
# comment must resume the state it interrupted rather than always dropping back to `:root`.
|
|
16
|
+
# `depth` tracks how deeply nested we are inside a container value (`{ … }` / `( … )`); see `:in_container_value`.
|
|
17
|
+
State = Struct.new(:context, :buffer, :in_escaped_ctx, :found_key, :resume_context, :depth)
|
|
18
|
+
|
|
19
|
+
# Characters allowed in an *unquoted* string — a key or a value. Unquoted strings are valid
|
|
20
|
+
# `.strings` syntax (the old-style ASCII property-list format) and are common in `InfoPlist.strings`
|
|
21
|
+
# (e.g. `CFBundleName = WordPress;`). `plutil` accepts alphanumerics plus `_ . - $ : /` in an
|
|
22
|
+
# unquoted string, so we match the same set: this scanner only ever runs on input `plutil` has
|
|
23
|
+
# already accepted, and matching its grammar keeps a file it parses from tripping the scanner.
|
|
24
|
+
# An unquoted key runs until the first whitespace or `=`; an unquoted value until whitespace or `;`.
|
|
25
|
+
UNQUOTED_STRING_CHARACTER = %r{[a-zA-Z0-9_.$:/-]}u
|
|
26
|
+
|
|
27
|
+
# Enter a comment from an inter-token position, remembering where to resume once it ends.
|
|
28
|
+
# (`state.context` is still the originating state when a transition lambda runs — it's only reassigned
|
|
29
|
+
# to the lambda's return value afterwards — so this captures the state the comment interrupts.)
|
|
30
|
+
ENTER_COMMENT = lambda do |state, _c|
|
|
31
|
+
state.resume_context = state.context
|
|
32
|
+
:maybe_comment_start
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
# A `/` between `=` and the value is ambiguous: it can start a comment (`/* … */` or `// …`) or be the
|
|
36
|
+
# first character of an unquoted value (e.g. a path like `/usr/bin`). Defer the decision by one character
|
|
37
|
+
# via `:maybe_comment_or_value`.
|
|
38
|
+
ENTER_COMMENT_OR_VALUE = lambda do |state, _c|
|
|
39
|
+
state.resume_context = state.context
|
|
40
|
+
:maybe_comment_or_value
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
# Restore the context a comment interrupted (defaults to `:root`), then clear the saved context.
|
|
44
|
+
RESUME_AFTER_COMMENT = lambda do |state, _c|
|
|
45
|
+
resume = state.resume_context || :root
|
|
46
|
+
state.resume_context = :root
|
|
47
|
+
resume
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
# A value can be a nested container — a dictionary `{ … }` or an array `( … )` — which `plutil` accepts
|
|
51
|
+
# (e.g. `"k" = { a = b; };` or `"k" = ( "a", "b" );`). We don't rewrite or record anything *inside* a
|
|
52
|
+
# container: its inner keys are not top-level keys, and `prefix_keys` copies the value through verbatim.
|
|
53
|
+
# We only need to find the matching close delimiter, so we just count nesting depth. `OPEN_CONTAINER`
|
|
54
|
+
# doubles as the entry transition from `:after_quoted_key_and_eq` and as the nested-open transition.
|
|
55
|
+
OPEN_CONTAINER = lambda do |state, _c|
|
|
56
|
+
state.depth += 1
|
|
57
|
+
:in_container_value
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
# Close one level of nesting. The value is only finished — and we go back to expecting the terminating
|
|
61
|
+
# `;` — once depth returns to 0; otherwise we're still inside an outer container.
|
|
62
|
+
CLOSE_CONTAINER = lambda do |state, _c|
|
|
63
|
+
state.depth -= 1
|
|
64
|
+
state.depth.zero? ? :after_quoted_value : :in_container_value
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
# A `/` inside a container may start a comment — whose body can contain `{ } ( ) ; "` that must NOT count
|
|
68
|
+
# toward nesting — or just be an ordinary value character (e.g. a path). Defer the decision one char,
|
|
69
|
+
# resuming the container in either case.
|
|
70
|
+
ENTER_CONTAINER_COMMENT = lambda do |state, _c|
|
|
71
|
+
state.resume_context = :in_container_value
|
|
72
|
+
:maybe_container_comment
|
|
73
|
+
end
|
|
13
74
|
|
|
14
75
|
TRANSITIONS = {
|
|
15
76
|
root: {
|
|
16
77
|
/\s/u => :root,
|
|
17
78
|
'/' => :maybe_comment_start,
|
|
18
|
-
'"' => :in_quoted_key
|
|
79
|
+
'"' => :in_quoted_key,
|
|
80
|
+
# An unquoted key, e.g. `CFBundleName = "…";` as used by `InfoPlist.strings`.
|
|
81
|
+
UNQUOTED_STRING_CHARACTER => lambda do |state, c|
|
|
82
|
+
state.buffer.write(c)
|
|
83
|
+
:in_unquoted_key
|
|
84
|
+
end
|
|
19
85
|
},
|
|
20
86
|
maybe_comment_start: {
|
|
21
87
|
'/' => :in_line_comment,
|
|
22
88
|
/\*/u => :in_block_comment
|
|
23
89
|
},
|
|
90
|
+
# Reached only from `:after_quoted_key_and_eq`, where a leading `/` might begin a comment or an
|
|
91
|
+
# unquoted value. A `*` or `/` confirms a comment; anything else means the `/` was the value's first
|
|
92
|
+
# character (values aren't buffered, so we just continue scanning the value).
|
|
93
|
+
maybe_comment_or_value: {
|
|
94
|
+
/\*/u => :in_block_comment,
|
|
95
|
+
'/' => :in_line_comment,
|
|
96
|
+
/./mu => lambda do |state, _c|
|
|
97
|
+
state.resume_context = :root
|
|
98
|
+
:in_unquoted_value
|
|
99
|
+
end
|
|
100
|
+
},
|
|
24
101
|
in_line_comment: {
|
|
25
|
-
"\n" =>
|
|
102
|
+
"\n" => RESUME_AFTER_COMMENT,
|
|
26
103
|
/./u => :in_line_comment
|
|
27
104
|
},
|
|
28
105
|
in_block_comment: {
|
|
@@ -30,7 +107,7 @@ module Fastlane
|
|
|
30
107
|
/./mu => :in_block_comment
|
|
31
108
|
},
|
|
32
109
|
maybe_block_comment_end: {
|
|
33
|
-
'/' =>
|
|
110
|
+
'/' => RESUME_AFTER_COMMENT,
|
|
34
111
|
/./mu => :in_block_comment
|
|
35
112
|
},
|
|
36
113
|
in_quoted_key: {
|
|
@@ -44,21 +121,78 @@ module Fastlane
|
|
|
44
121
|
:in_quoted_key
|
|
45
122
|
end
|
|
46
123
|
},
|
|
124
|
+
in_unquoted_key: {
|
|
125
|
+
# The key ends at the first whitespace or `=`. Whitespace still expects an `=` next;
|
|
126
|
+
# an `=` moves straight on to the value.
|
|
127
|
+
/[\s=]/u => lambda do |state, c|
|
|
128
|
+
state.found_key = state.buffer.string.dup
|
|
129
|
+
state.buffer = StringIO.new
|
|
130
|
+
c == '=' ? :after_quoted_key_and_eq : :after_quoted_key_before_eq
|
|
131
|
+
end,
|
|
132
|
+
UNQUOTED_STRING_CHARACTER => lambda do |state, c|
|
|
133
|
+
state.buffer.write(c)
|
|
134
|
+
:in_unquoted_key
|
|
135
|
+
end
|
|
136
|
+
},
|
|
47
137
|
after_quoted_key_before_eq: {
|
|
138
|
+
# A comment may sit between the key and the `=` (e.g. `"key" /* note */ = "value";`).
|
|
139
|
+
'/' => ENTER_COMMENT,
|
|
48
140
|
/\s/u => :after_quoted_key_before_eq,
|
|
49
141
|
'=' => :after_quoted_key_and_eq
|
|
50
142
|
},
|
|
51
143
|
after_quoted_key_and_eq: {
|
|
144
|
+
# A `/` here may start a comment or an unquoted value (which can contain `/`); disambiguate one char
|
|
145
|
+
# later. This entry must precede `UNQUOTED_STRING_CHARACTER` below, which also matches `/`.
|
|
146
|
+
'/' => ENTER_COMMENT_OR_VALUE,
|
|
52
147
|
/\s/u => :after_quoted_key_and_eq,
|
|
53
|
-
'"' => :in_quoted_value
|
|
148
|
+
'"' => :in_quoted_value,
|
|
149
|
+
# A container value — a dictionary `{ … }` or an array `( … )`, which may nest (e.g. `"k" = { a = b; };`).
|
|
150
|
+
/[{(]/u => OPEN_CONTAINER,
|
|
151
|
+
# An unquoted value, e.g. `CFBundleName = WordPress;` as used by `InfoPlist.strings`.
|
|
152
|
+
UNQUOTED_STRING_CHARACTER => :in_unquoted_value
|
|
54
153
|
},
|
|
55
154
|
in_quoted_value: {
|
|
56
155
|
'"' => :after_quoted_value,
|
|
57
156
|
/./mu => :in_quoted_value
|
|
58
157
|
},
|
|
158
|
+
in_unquoted_value: {
|
|
159
|
+
# The value ends at the first whitespace or the terminating `;`. Its contents are irrelevant
|
|
160
|
+
# to duplicate-key detection, so — unlike a key — we don't buffer it.
|
|
161
|
+
';' => :root,
|
|
162
|
+
/\s/u => :after_quoted_value,
|
|
163
|
+
UNQUOTED_STRING_CHARACTER => :in_unquoted_value
|
|
164
|
+
},
|
|
59
165
|
after_quoted_value: {
|
|
166
|
+
# A comment may sit between the value and the terminating `;` (e.g. `"key" = "value" /* note */;`).
|
|
167
|
+
'/' => ENTER_COMMENT,
|
|
60
168
|
/\s/u => :after_quoted_value,
|
|
61
169
|
';' => :root
|
|
170
|
+
},
|
|
171
|
+
# Inside a container value (`{ … }` / `( … )`). We ignore the contents — inner keys aren't top-level
|
|
172
|
+
# keys and the value is copied verbatim by `prefix_keys` — and only track nesting so we can find the
|
|
173
|
+
# matching close. Quoted strings and comments are entered explicitly because their bodies can contain
|
|
174
|
+
# `{ } ( ) ;` that must not affect the depth count; everything else (`=`, `;`, `,`, whitespace, unquoted
|
|
175
|
+
# text, newlines) is consumed by the catch-all, which must stay LAST so the specific keys win first.
|
|
176
|
+
in_container_value: {
|
|
177
|
+
/[{(]/u => OPEN_CONTAINER,
|
|
178
|
+
/[})]/u => CLOSE_CONTAINER,
|
|
179
|
+
'"' => :in_container_quoted_string,
|
|
180
|
+
'/' => ENTER_CONTAINER_COMMENT,
|
|
181
|
+
/./mu => :in_container_value
|
|
182
|
+
},
|
|
183
|
+
# A quoted string inside a container. Skipped wholesale (its `{ } ( ) ; ,` are literal, not structural)
|
|
184
|
+
# until the closing quote returns us to the container. Escapes are handled globally (see the escape
|
|
185
|
+
# branch in `find_duplicated_keys`, whose allow-list includes this context).
|
|
186
|
+
in_container_quoted_string: {
|
|
187
|
+
'"' => :in_container_value,
|
|
188
|
+
/./mu => :in_container_quoted_string
|
|
189
|
+
},
|
|
190
|
+
# One char after a `/` inside a container: `*`/`/` confirm a comment (which resumes the container once
|
|
191
|
+
# it ends), anything else means the `/` was just a value character and we stay in the container.
|
|
192
|
+
maybe_container_comment: {
|
|
193
|
+
/\*/u => :in_block_comment,
|
|
194
|
+
'/' => :in_line_comment,
|
|
195
|
+
/./mu => :in_container_value
|
|
62
196
|
}
|
|
63
197
|
}.freeze
|
|
64
198
|
|
|
@@ -70,7 +204,7 @@ module Fastlane
|
|
|
70
204
|
def self.find_duplicated_keys(file:)
|
|
71
205
|
keys_with_lines = Hash.new { |h, k| h[k] = [] }
|
|
72
206
|
|
|
73
|
-
state = State.new(context: :root, buffer: StringIO.new, in_escaped_ctx: false, found_key: nil)
|
|
207
|
+
state = State.new(context: :root, buffer: StringIO.new, in_escaped_ctx: false, found_key: nil, resume_context: :root, depth: 0)
|
|
74
208
|
|
|
75
209
|
# Using our `each_utf8_line` helper instead of `File.readlines` ensures we can also read files that are
|
|
76
210
|
# encoded in UTF-16, yet process each of their lines as a UTF-8 string, so that `RegExp#match?` don't throw
|
|
@@ -81,7 +215,7 @@ module Fastlane
|
|
|
81
215
|
# This is more straightforward than having to account for it in the `TRANSITIONS` table.
|
|
82
216
|
if state.in_escaped_ctx || c == '\\'
|
|
83
217
|
# Just because we check for escaped characters at the global level, it doesn't mean we allow them in every context.
|
|
84
|
-
allowed_contexts_for_escaped_characters = %i[in_quoted_key in_quoted_value in_block_comment in_line_comment]
|
|
218
|
+
allowed_contexts_for_escaped_characters = %i[in_quoted_key in_quoted_value in_block_comment in_line_comment in_container_quoted_string]
|
|
85
219
|
raise "Found escaped character outside of allowed contexts on line #{line_no + 1} (current context: #{state.context})" unless allowed_contexts_for_escaped_characters.include?(state.context)
|
|
86
220
|
|
|
87
221
|
state.buffer.write(c) if state.context == :in_quoted_key
|
|
@@ -106,6 +240,75 @@ module Fastlane
|
|
|
106
240
|
|
|
107
241
|
keys_with_lines.keep_if { |_, lines| lines.count > 1 }
|
|
108
242
|
end
|
|
243
|
+
|
|
244
|
+
# Detects the file format and, when applicable, scans for duplicate keys — in one step, so
|
|
245
|
+
# callers don't each re-implement the "`:text`-only" gate. `find_duplicated_keys` only
|
|
246
|
+
# understands the flat ASCII-plist syntax; an xml/binary plist can't be tokenized by it (though
|
|
247
|
+
# `plutil` collapses any duplicate to its last value when parsing those anyway).
|
|
248
|
+
#
|
|
249
|
+
# @param [String] file The path to the `.strings` file to inspect.
|
|
250
|
+
# @param [Boolean] assume_valid Forwarded to `strings_file_type`: skip the redundant `plutil -lint`
|
|
251
|
+
# when the caller has already confirmed the file parses.
|
|
252
|
+
# @return [Array] A `[status, payload]` pair, one of:
|
|
253
|
+
# - `[:scanned, { key => [lines] }]` — a `:text` file we tokenized (hash empty if none).
|
|
254
|
+
# - `[:unsupported_format, format]` — not a `:text` file (`:xml`, `:binary`, or `nil`); not scanned.
|
|
255
|
+
# - `[:unscannable, error_message]` — a `:text` file the tokenizer couldn't read.
|
|
256
|
+
# Each caller decides how to react (warn-and-skip, fail closed, …) from this one source of truth.
|
|
257
|
+
def self.scan_for_duplicate_keys(file:, assume_valid: false)
|
|
258
|
+
format = Fastlane::Helper::Ios::L10nHelper.strings_file_type(path: file, assume_valid: assume_valid)
|
|
259
|
+
return [:unsupported_format, format] unless format == :text
|
|
260
|
+
|
|
261
|
+
[:scanned, find_duplicated_keys(file: file)]
|
|
262
|
+
rescue StandardError => e
|
|
263
|
+
[:unscannable, e.message]
|
|
264
|
+
end
|
|
265
|
+
|
|
266
|
+
# Rewrites `.strings` lines so every key carries `prefix`, leaving comments, values, whitespace and
|
|
267
|
+
# formatting untouched. A quoted key gets the prefix inside its quotes (`"key"` → `"<prefix>key"`); an
|
|
268
|
+
# unquoted key is wrapped in quotes (`key` → `"<prefix>key"`). Because it tokenizes the file the same way
|
|
269
|
+
# `find_duplicated_keys` does, it is comment-aware: a key sitting behind an inter-token comment (e.g.
|
|
270
|
+
# `key /* note */ = value;`) is still prefixed, and `key = value`-looking text *inside* a comment is left
|
|
271
|
+
# alone — a distinction a line-based regex can't reliably make. It is likewise container-aware: a
|
|
272
|
+
# dictionary or array value (`"k" = { … };` / `"k" = ( … );`, nesting allowed) has only its outer key
|
|
273
|
+
# prefixed, with the value — including any keys *inside* it — copied through verbatim.
|
|
274
|
+
#
|
|
275
|
+
# @param [Array<String>] lines The file's lines, already decoded to UTF-8 (e.g. via `L10nHelper.read_utf8_lines`).
|
|
276
|
+
# @param [String] prefix The prefix to insert before every key. A nil/empty prefix returns `lines` unchanged.
|
|
277
|
+
# @return [Array<String>] The rewritten lines.
|
|
278
|
+
def self.prefix_keys(lines:, prefix:)
|
|
279
|
+
return lines if prefix.nil? || prefix.empty?
|
|
280
|
+
|
|
281
|
+
state = State.new(context: :root, buffer: StringIO.new, in_escaped_ctx: false, found_key: nil, resume_context: :root, depth: 0)
|
|
282
|
+
lines.map do |line|
|
|
283
|
+
rewritten = +''
|
|
284
|
+
line.each_char do |c|
|
|
285
|
+
# Escaped characters only occur inside quoted strings or comments — never around a key boundary —
|
|
286
|
+
# so they're copied through verbatim (mirroring `find_duplicated_keys`' global escape handling).
|
|
287
|
+
if state.in_escaped_ctx || c == '\\'
|
|
288
|
+
state.in_escaped_ctx = !state.in_escaped_ctx
|
|
289
|
+
rewritten << c
|
|
290
|
+
next
|
|
291
|
+
end
|
|
292
|
+
|
|
293
|
+
previous_context = state.context
|
|
294
|
+
(_, transition) = TRANSITIONS[previous_context].find { |regex, _| c.match?(regex) } || [nil, nil]
|
|
295
|
+
raise "Invalid character `#{c}` found (current context: #{previous_context})" if transition.nil?
|
|
296
|
+
|
|
297
|
+
state.context = transition.is_a?(Proc) ? transition.call(state, c) : transition
|
|
298
|
+
|
|
299
|
+
if previous_context == :root && state.context == :in_quoted_key
|
|
300
|
+
rewritten << c << prefix # opening `"` of a quoted key — the prefix goes inside the quotes
|
|
301
|
+
elsif previous_context == :root && state.context == :in_unquoted_key
|
|
302
|
+
rewritten << '"' << prefix << c # first char of an unquoted key — open a quote + prefix, then the char
|
|
303
|
+
elsif previous_context == :in_unquoted_key && state.context != :in_unquoted_key
|
|
304
|
+
rewritten << '"' << c # the unquoted key just ended — close the quote, then the delimiter
|
|
305
|
+
else
|
|
306
|
+
rewritten << c
|
|
307
|
+
end
|
|
308
|
+
end
|
|
309
|
+
rewritten
|
|
310
|
+
end
|
|
311
|
+
end
|
|
109
312
|
end
|
|
110
313
|
end
|
|
111
314
|
end
|
metadata
CHANGED
|
@@ -1,14 +1,13 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: fastlane-plugin-wpmreleasetoolkit
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 14.
|
|
4
|
+
version: 14.11.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Automattic
|
|
8
|
-
autorequire:
|
|
9
8
|
bindir: bin
|
|
10
9
|
cert_chain: []
|
|
11
|
-
date:
|
|
10
|
+
date: 1980-01-02 00:00:00.000000000 Z
|
|
12
11
|
dependencies:
|
|
13
12
|
- !ruby/object:Gem::Dependency
|
|
14
13
|
name: buildkit
|
|
@@ -72,14 +71,14 @@ dependencies:
|
|
|
72
71
|
requirements:
|
|
73
72
|
- - "~>"
|
|
74
73
|
- !ruby/object:Gem::Version
|
|
75
|
-
version: '2.
|
|
74
|
+
version: '2.237'
|
|
76
75
|
type: :runtime
|
|
77
76
|
prerelease: false
|
|
78
77
|
version_requirements: !ruby/object:Gem::Requirement
|
|
79
78
|
requirements:
|
|
80
79
|
- - "~>"
|
|
81
80
|
- !ruby/object:Gem::Version
|
|
82
|
-
version: '2.
|
|
81
|
+
version: '2.237'
|
|
83
82
|
- !ruby/object:Gem::Dependency
|
|
84
83
|
name: gettext
|
|
85
84
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -260,147 +259,6 @@ dependencies:
|
|
|
260
259
|
- - "~>"
|
|
261
260
|
- !ruby/object:Gem::Version
|
|
262
261
|
version: '1.31'
|
|
263
|
-
- !ruby/object:Gem::Dependency
|
|
264
|
-
name: activesupport
|
|
265
|
-
requirement: !ruby/object:Gem::Requirement
|
|
266
|
-
requirements:
|
|
267
|
-
- - "~>"
|
|
268
|
-
- !ruby/object:Gem::Version
|
|
269
|
-
version: '8.1'
|
|
270
|
-
type: :development
|
|
271
|
-
prerelease: false
|
|
272
|
-
version_requirements: !ruby/object:Gem::Requirement
|
|
273
|
-
requirements:
|
|
274
|
-
- - "~>"
|
|
275
|
-
- !ruby/object:Gem::Version
|
|
276
|
-
version: '8.1'
|
|
277
|
-
- !ruby/object:Gem::Dependency
|
|
278
|
-
name: bundler
|
|
279
|
-
requirement: !ruby/object:Gem::Requirement
|
|
280
|
-
requirements:
|
|
281
|
-
- - "~>"
|
|
282
|
-
- !ruby/object:Gem::Version
|
|
283
|
-
version: '2.0'
|
|
284
|
-
type: :development
|
|
285
|
-
prerelease: false
|
|
286
|
-
version_requirements: !ruby/object:Gem::Requirement
|
|
287
|
-
requirements:
|
|
288
|
-
- - "~>"
|
|
289
|
-
- !ruby/object:Gem::Version
|
|
290
|
-
version: '2.0'
|
|
291
|
-
- !ruby/object:Gem::Dependency
|
|
292
|
-
name: fastlane
|
|
293
|
-
requirement: !ruby/object:Gem::Requirement
|
|
294
|
-
requirements:
|
|
295
|
-
- - "~>"
|
|
296
|
-
- !ruby/object:Gem::Version
|
|
297
|
-
version: '2.210'
|
|
298
|
-
type: :development
|
|
299
|
-
prerelease: false
|
|
300
|
-
version_requirements: !ruby/object:Gem::Requirement
|
|
301
|
-
requirements:
|
|
302
|
-
- - "~>"
|
|
303
|
-
- !ruby/object:Gem::Version
|
|
304
|
-
version: '2.210'
|
|
305
|
-
- !ruby/object:Gem::Dependency
|
|
306
|
-
name: pry
|
|
307
|
-
requirement: !ruby/object:Gem::Requirement
|
|
308
|
-
requirements:
|
|
309
|
-
- - "~>"
|
|
310
|
-
- !ruby/object:Gem::Version
|
|
311
|
-
version: 0.12.2
|
|
312
|
-
type: :development
|
|
313
|
-
prerelease: false
|
|
314
|
-
version_requirements: !ruby/object:Gem::Requirement
|
|
315
|
-
requirements:
|
|
316
|
-
- - "~>"
|
|
317
|
-
- !ruby/object:Gem::Version
|
|
318
|
-
version: 0.12.2
|
|
319
|
-
- !ruby/object:Gem::Dependency
|
|
320
|
-
name: rmagick
|
|
321
|
-
requirement: !ruby/object:Gem::Requirement
|
|
322
|
-
requirements:
|
|
323
|
-
- - "~>"
|
|
324
|
-
- !ruby/object:Gem::Version
|
|
325
|
-
version: '5.3'
|
|
326
|
-
type: :development
|
|
327
|
-
prerelease: false
|
|
328
|
-
version_requirements: !ruby/object:Gem::Requirement
|
|
329
|
-
requirements:
|
|
330
|
-
- - "~>"
|
|
331
|
-
- !ruby/object:Gem::Version
|
|
332
|
-
version: '5.3'
|
|
333
|
-
- !ruby/object:Gem::Dependency
|
|
334
|
-
name: rspec
|
|
335
|
-
requirement: !ruby/object:Gem::Requirement
|
|
336
|
-
requirements:
|
|
337
|
-
- - "~>"
|
|
338
|
-
- !ruby/object:Gem::Version
|
|
339
|
-
version: '3.8'
|
|
340
|
-
type: :development
|
|
341
|
-
prerelease: false
|
|
342
|
-
version_requirements: !ruby/object:Gem::Requirement
|
|
343
|
-
requirements:
|
|
344
|
-
- - "~>"
|
|
345
|
-
- !ruby/object:Gem::Version
|
|
346
|
-
version: '3.8'
|
|
347
|
-
- !ruby/object:Gem::Dependency
|
|
348
|
-
name: rspec_junit_formatter
|
|
349
|
-
requirement: !ruby/object:Gem::Requirement
|
|
350
|
-
requirements:
|
|
351
|
-
- - "~>"
|
|
352
|
-
- !ruby/object:Gem::Version
|
|
353
|
-
version: 0.4.1
|
|
354
|
-
type: :development
|
|
355
|
-
prerelease: false
|
|
356
|
-
version_requirements: !ruby/object:Gem::Requirement
|
|
357
|
-
requirements:
|
|
358
|
-
- - "~>"
|
|
359
|
-
- !ruby/object:Gem::Version
|
|
360
|
-
version: 0.4.1
|
|
361
|
-
- !ruby/object:Gem::Dependency
|
|
362
|
-
name: rubocop
|
|
363
|
-
requirement: !ruby/object:Gem::Requirement
|
|
364
|
-
requirements:
|
|
365
|
-
- - "~>"
|
|
366
|
-
- !ruby/object:Gem::Version
|
|
367
|
-
version: '1.65'
|
|
368
|
-
type: :development
|
|
369
|
-
prerelease: false
|
|
370
|
-
version_requirements: !ruby/object:Gem::Requirement
|
|
371
|
-
requirements:
|
|
372
|
-
- - "~>"
|
|
373
|
-
- !ruby/object:Gem::Version
|
|
374
|
-
version: '1.65'
|
|
375
|
-
- !ruby/object:Gem::Dependency
|
|
376
|
-
name: rubocop-rspec
|
|
377
|
-
requirement: !ruby/object:Gem::Requirement
|
|
378
|
-
requirements:
|
|
379
|
-
- - '='
|
|
380
|
-
- !ruby/object:Gem::Version
|
|
381
|
-
version: '3.0'
|
|
382
|
-
type: :development
|
|
383
|
-
prerelease: false
|
|
384
|
-
version_requirements: !ruby/object:Gem::Requirement
|
|
385
|
-
requirements:
|
|
386
|
-
- - '='
|
|
387
|
-
- !ruby/object:Gem::Version
|
|
388
|
-
version: '3.0'
|
|
389
|
-
- !ruby/object:Gem::Dependency
|
|
390
|
-
name: simplecov
|
|
391
|
-
requirement: !ruby/object:Gem::Requirement
|
|
392
|
-
requirements:
|
|
393
|
-
- - "~>"
|
|
394
|
-
- !ruby/object:Gem::Version
|
|
395
|
-
version: 0.16.1
|
|
396
|
-
type: :development
|
|
397
|
-
prerelease: false
|
|
398
|
-
version_requirements: !ruby/object:Gem::Requirement
|
|
399
|
-
requirements:
|
|
400
|
-
- - "~>"
|
|
401
|
-
- !ruby/object:Gem::Version
|
|
402
|
-
version: 0.16.1
|
|
403
|
-
description:
|
|
404
262
|
email: mobile@automattic.com
|
|
405
263
|
executables: []
|
|
406
264
|
extensions: []
|
|
@@ -477,6 +335,7 @@ files:
|
|
|
477
335
|
- lib/fastlane/plugin/wpmreleasetoolkit/actions/ios/ios_send_app_size_metrics.rb
|
|
478
336
|
- lib/fastlane/plugin/wpmreleasetoolkit/actions/ios/ios_update_metadata_source.rb
|
|
479
337
|
- lib/fastlane/plugin/wpmreleasetoolkit/actions/ios/ios_update_release_notes.rb
|
|
338
|
+
- lib/fastlane/plugin/wpmreleasetoolkit/actions/macos/macos_verify_code_signing.rb
|
|
480
339
|
- lib/fastlane/plugin/wpmreleasetoolkit/env_manager/env_manager.rb
|
|
481
340
|
- lib/fastlane/plugin/wpmreleasetoolkit/helper/android/android_emulator_helper.rb
|
|
482
341
|
- lib/fastlane/plugin/wpmreleasetoolkit/helper/android/android_localize_helper.rb
|
|
@@ -532,7 +391,6 @@ homepage: https://github.com/wordpress-mobile/release-toolkit
|
|
|
532
391
|
licenses:
|
|
533
392
|
- MIT
|
|
534
393
|
metadata: {}
|
|
535
|
-
post_install_message:
|
|
536
394
|
rdoc_options: []
|
|
537
395
|
require_paths:
|
|
538
396
|
- lib
|
|
@@ -547,8 +405,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
547
405
|
- !ruby/object:Gem::Version
|
|
548
406
|
version: '0'
|
|
549
407
|
requirements: []
|
|
550
|
-
rubygems_version: 3.
|
|
551
|
-
signing_key:
|
|
408
|
+
rubygems_version: 3.6.9
|
|
552
409
|
specification_version: 4
|
|
553
410
|
summary: Fastlane plugin for release automation
|
|
554
411
|
test_files: []
|