fastlane-plugin-google_data_safety 0.2.0 → 0.3.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/README.md +11 -10
- data/lib/fastlane/plugin/google_data_safety/actions/prompt_create_data_safety_csv_action.rb +405 -160
- data/lib/fastlane/plugin/google_data_safety/actions/upload_google_data_safety_action.rb +20 -2
- data/lib/fastlane/plugin/google_data_safety/version.rb +1 -1
- metadata +9 -9
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 4c996cc8d59b250f9fd4c899a6b3d426b74b7644c514fd045e4bf03c335f3c5b
|
|
4
|
+
data.tar.gz: 9ff7950db800e8506ecd23fc970e421d910096c828209205286766bf27e103ad
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 5b64fb9a63d98823b7301c8a7c6cdb6c4dbe9259f414f16164fb690718de766d8512b5f6a9fc7ae20d02c94ee3373df38dd8dd56844f3d40ef33f66795194841
|
|
7
|
+
data.tar.gz: 7b66cc3fa41a90b82340e87df73f2477ab1387e5a53d22a40ddbe6c1c4b8b314fedc438d5ca6e2dc57eca300cf851bc1c60b60fcd3d81be48f38f16954c5fd55
|
data/README.md
CHANGED
|
@@ -40,13 +40,14 @@ upload_google_data_safety(
|
|
|
40
40
|
|
|
41
41
|
#### Parameters
|
|
42
42
|
|
|
43
|
-
| Key | Description
|
|
44
|
-
|
|
|
45
|
-
| json_key_data | The raw service account JSON data used to authenticate with Google
|
|
46
|
-
| json_key | The path to a file containing service account JSON, used to authenticate with Google |
|
|
47
|
-
| package_name | The package name of the application to use
|
|
48
|
-
| csv_file | The path to a file containing Google data safety csv
|
|
49
|
-
| csv_content | The raw csv data used for Google data safety
|
|
43
|
+
| Key | Environment Variable | Description |
|
|
44
|
+
| ------------- | -------------------- | ------------------------------------------------------------------------------------ |
|
|
45
|
+
| json_key_data | JSON_KEY_DATA | The raw service account JSON data used to authenticate with Google |
|
|
46
|
+
| json_key | JSON_KEY_FILE | The path to a file containing service account JSON, used to authenticate with Google |
|
|
47
|
+
| package_name | PACKAGE_NAME | The package name of the application to use |
|
|
48
|
+
| csv_file | CSV_FILE | The path to a file containing Google data safety csv |
|
|
49
|
+
| csv_content | CSV_CONTENT | The raw csv data used for Google data safety |
|
|
50
|
+
| debug | DEBUG | Provide debug log messaging |
|
|
50
51
|
|
|
51
52
|
### prompt_create_data_safety_csv
|
|
52
53
|
|
|
@@ -63,9 +64,9 @@ prompt_create_data_safety_csv(
|
|
|
63
64
|
|
|
64
65
|
#### Parameters
|
|
65
66
|
|
|
66
|
-
| Key | Description
|
|
67
|
-
|
|
|
68
|
-
| csv_file | The path to save csv file for upload to Google Play Console |
|
|
67
|
+
| Key | Environment Variable | Description |
|
|
68
|
+
| -------- | -------------------- | ----------------------------------------------------------- |
|
|
69
|
+
| csv_file | CSV_FILE | The path to save csv file for upload to Google Play Console |
|
|
69
70
|
|
|
70
71
|
#### Issue reporting
|
|
71
72
|
|
|
@@ -8,8 +8,12 @@ require 'csv'
|
|
|
8
8
|
module Fastlane
|
|
9
9
|
module Actions
|
|
10
10
|
class PromptCreateDataSafetyCsvAction < Action
|
|
11
|
-
|
|
11
|
+
FORMULA_CHARS = %w[= + - @].freeze
|
|
12
12
|
|
|
13
|
+
# Run the prompt_create_data_safety_csv action.
|
|
14
|
+
#
|
|
15
|
+
# @param param [hash] Fastlane parameters
|
|
16
|
+
def self.run(params)
|
|
13
17
|
if !UI.interactive?
|
|
14
18
|
UI.error("Prompts questions can only display in interactive.")
|
|
15
19
|
end
|
|
@@ -24,21 +28,26 @@ module Fastlane
|
|
|
24
28
|
|
|
25
29
|
template_data = Helper::PromptCreateDataSafetyHelper.template_array()
|
|
26
30
|
|
|
27
|
-
template_data.each_with_index do | row |
|
|
31
|
+
safety_data = template_data.each_with_index do | row |
|
|
28
32
|
hash_key = "#{row[0]},#{row[1]}"
|
|
29
33
|
if hash.key?(hash_key)
|
|
30
34
|
row[2] = hash[hash_key]
|
|
31
35
|
end
|
|
32
36
|
end
|
|
33
37
|
|
|
34
|
-
|
|
35
|
-
template_data.each do |row|
|
|
36
|
-
csv << row
|
|
37
|
-
end
|
|
38
|
-
end
|
|
39
|
-
|
|
38
|
+
self.saveCsvFile(data: safety_data, csv_file_path: csv_file_path)
|
|
40
39
|
end
|
|
41
40
|
|
|
41
|
+
# Array of valid answer strings for true.
|
|
42
|
+
def self.valid_true_answers = ["y", "yes", "true", "t"]
|
|
43
|
+
# Array of valid answer strings for false.
|
|
44
|
+
def self.valid_false_answers = ["n", "no", "false", "f"]
|
|
45
|
+
# Array of valid answer strings for ninety days selection (delete account data).
|
|
46
|
+
def self.valid_90_day_answers = ["90", "ninety"]
|
|
47
|
+
|
|
48
|
+
# Displays terminal header of Fastlane action and about the upcoming prompts.
|
|
49
|
+
#
|
|
50
|
+
# @param csv_file [String] The CSV file location that display to user where the file will be saved in
|
|
42
51
|
def self.headerDisplay(csv_file: nil)
|
|
43
52
|
UI.important("")
|
|
44
53
|
UI.important(" ____ _ ____ _ ____ __ _ ")
|
|
@@ -51,85 +60,144 @@ module Fastlane
|
|
|
51
60
|
UI.important("By: Owen Bean")
|
|
52
61
|
UI.important("Report problems at https://github.com/owenbean400/fastlane-plugin-google_data_safety/issues")
|
|
53
62
|
UI.important("")
|
|
54
|
-
UI.important("Answer the following
|
|
63
|
+
UI.important("Answer the following prompts below to generate CSV file to upload at #{csv_file}")
|
|
55
64
|
UI.important("")
|
|
56
65
|
end
|
|
57
66
|
|
|
67
|
+
# Prompt questions for Google Data Safety sheet.
|
|
68
|
+
#
|
|
69
|
+
# @param hash [hash] The hash to save data of answers to questions
|
|
58
70
|
def self.startingQuestion(hash: nil)
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
71
|
+
isDataInputAppInfo = self.promptForAnswerBoolean(
|
|
72
|
+
question: "Does your app collect or share any of the required user data types?",
|
|
73
|
+
hash: hash,
|
|
74
|
+
question_id: "PSL_DATA_COLLECTION_COLLECTS_PERSONAL_DATA",
|
|
75
|
+
response_id: "",
|
|
76
|
+
is_false_blank: false)
|
|
77
|
+
|
|
78
|
+
if isDataInputAppInfo
|
|
79
|
+
|
|
80
|
+
self.promptForAnswerBoolean(
|
|
81
|
+
question: "Is all of the user data collected by your app encrypted in transit?",
|
|
82
|
+
hash: hash,
|
|
83
|
+
question_id: "PSL_DATA_COLLECTION_ENCRYPTED_IN_TRANSIT",
|
|
84
|
+
response_id: "",
|
|
85
|
+
is_false_blank: false)
|
|
86
|
+
|
|
87
|
+
isDataInputAccountCreate = self.promptForAnswerBoolean(
|
|
88
|
+
question: "Does the app allow for account creation from within the app?",
|
|
89
|
+
hash: nil,
|
|
90
|
+
question_id: nil,
|
|
91
|
+
response_id: nil,
|
|
92
|
+
is_false_blank: false)
|
|
93
|
+
|
|
94
|
+
if isDataInputAccountCreate
|
|
95
|
+
self.promptForAnswerBoolean(
|
|
96
|
+
question: "Does the app allow for account creation from username and password?",
|
|
97
|
+
hash: hash,
|
|
98
|
+
question_id: "PSL_SUPPORTED_ACCOUNT_CREATION_METHODS",
|
|
99
|
+
response_id: "PSL_ACM_USER_ID_PASSWORD",
|
|
100
|
+
is_false_blank: true)
|
|
101
|
+
|
|
102
|
+
self.promptForAnswerBoolean(
|
|
103
|
+
question: "Does the app allow for account creation from username and other authentication?",
|
|
104
|
+
hash: hash,
|
|
105
|
+
question_id: "PSL_SUPPORTED_ACCOUNT_CREATION_METHODS",
|
|
106
|
+
response_id: "PSL_ACM_USER_ID_OTHER_AUTH",
|
|
107
|
+
is_false_blank: true)
|
|
108
|
+
|
|
109
|
+
self.promptForAnswerBoolean(
|
|
110
|
+
question: "Does the app allow for account creation from username, password, and other authentication?",
|
|
111
|
+
hash: hash,
|
|
112
|
+
question_id: "PSL_SUPPORTED_ACCOUNT_CREATION_METHODS",
|
|
113
|
+
response_id: "PSL_ACM_USER_ID_PASSWORD_OTHER_AUTH",
|
|
114
|
+
is_false_blank: true)
|
|
115
|
+
|
|
116
|
+
self.promptForAnswerBoolean(
|
|
117
|
+
question: "Does the app allow for account creation from OAuth?",
|
|
118
|
+
hash: hash,
|
|
119
|
+
question_id: "PSL_SUPPORTED_ACCOUNT_CREATION_METHODS",
|
|
120
|
+
response_id: "PSL_ACM_OAUTH",
|
|
121
|
+
is_false_blank: true)
|
|
122
|
+
|
|
123
|
+
isDataInputCreateOther = self.promptForAnswerBoolean(
|
|
124
|
+
question: "Does the app allow for other account creation method?",
|
|
125
|
+
hash: hash,
|
|
126
|
+
question_id: "PSL_SUPPORTED_ACCOUNT_CREATION_METHODS",
|
|
127
|
+
response_id: "PSL_ACM_OTHER",
|
|
128
|
+
is_false_blank: true)
|
|
129
|
+
|
|
130
|
+
if isDataInputCreateOther
|
|
131
|
+
self.promptForAnswer(
|
|
132
|
+
question: "Describe the method of account creation that your app supports",
|
|
133
|
+
hash: hash,
|
|
134
|
+
question_id: "PSL_ACM_SPECIFY",
|
|
135
|
+
response_id: "")
|
|
92
136
|
end
|
|
93
137
|
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
138
|
+
self.promptForUrl(
|
|
139
|
+
question: "Add a valid URL for account deletion",
|
|
140
|
+
hash: hash,
|
|
141
|
+
question_id: "PSL_ACCOUNT_DELETION_URL",
|
|
142
|
+
response_id: "")
|
|
97
143
|
else
|
|
98
|
-
self.
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
self.
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
144
|
+
isDataInputLoginOutside = self.promptForAnswerBoolean(
|
|
145
|
+
question: "Can users login to your app with accounts created outside of the app?",
|
|
146
|
+
hash: hash,
|
|
147
|
+
question_id: "PSL_HAS_OUTSIDE_APP_ACCOUNTS",
|
|
148
|
+
response_id: "",
|
|
149
|
+
is_false_blank: true)
|
|
150
|
+
|
|
151
|
+
if isDataInputLoginOutside
|
|
152
|
+
self.promptForAnswerBoolean(
|
|
153
|
+
question: "Can users create account outside of app by app identification (e.g. SIM binding, service subscription)?",
|
|
154
|
+
hash: hash,
|
|
155
|
+
question_id: "PSL_OUTSIDE_APP_ACCOUNT_TYPES",
|
|
156
|
+
response_id: "PSL_LOGIN_WITH_OUTSIDE_APP_ID",
|
|
157
|
+
is_false_blank: true)
|
|
158
|
+
|
|
159
|
+
self.promptForAnswerBoolean(
|
|
160
|
+
question: "Can users create account outside of app by employment or enterprise account?",
|
|
161
|
+
hash: hash,
|
|
162
|
+
question_id: "PSL_OUTSIDE_APP_ACCOUNT_TYPES",
|
|
163
|
+
response_id: "PSL_LOGIN_THROUGH_EMPLOYMENT_OR_ENTERPRISE_ACCOUNT",
|
|
164
|
+
is_false_blank: true)
|
|
165
|
+
|
|
166
|
+
isDataInputLoginOutsideOther = self.promptForAnswerBoolean(
|
|
167
|
+
question: "Is there other way users can create account outside of app?",
|
|
168
|
+
hash: hash,
|
|
169
|
+
question_id: "PSL_OUTSIDE_APP_ACCOUNT_TYPES",
|
|
170
|
+
response_id: "PSL_OUTSIDE_APP_ACCOUNT_TYPE_OTHER",
|
|
171
|
+
is_false_blank: true)
|
|
172
|
+
|
|
173
|
+
if isDataInputLoginOutsideOther
|
|
174
|
+
self.promptForAnswer(
|
|
175
|
+
question: "Describe how these accounts are created",
|
|
176
|
+
hash: hash,
|
|
177
|
+
question_id: "PSL_OUTSIDE_APP_ACCOUNT_TYPE_SPECIFY",
|
|
178
|
+
response_id: "")
|
|
119
179
|
end
|
|
120
|
-
|
|
121
180
|
end
|
|
122
|
-
|
|
123
181
|
end
|
|
124
182
|
|
|
125
183
|
dataInputDeleteData = UI.input("Do you provide a way for users to request that their data is deleted? Type 90 for deletion after 90 days. Y/N/90")
|
|
126
184
|
|
|
127
|
-
|
|
185
|
+
while !self.valid_true_answers.include?(dataInputDeleteData.downcase) &&
|
|
186
|
+
!self.valid_false_answers.include?(dataInputDeleteData.downcase) &&
|
|
187
|
+
!self.valid_90_day_answers.include?(dataInputDeleteData.downcase)
|
|
188
|
+
UI.important("Please provide an answer of either yes, no, or 90")
|
|
189
|
+
dataInputDeleteData = UI.input("Do you provide a way for users to request that their data is deleted? Type 90 for deletion after 90 days. Y/N/90")
|
|
190
|
+
end
|
|
191
|
+
|
|
192
|
+
if self.valid_true_answers.include?(dataInputDeleteData.downcase)
|
|
128
193
|
self.addToHash(hash: hash, question_id: "PSL_SUPPORT_DATA_DELETION_BY_USER", response_id: "DATA_DELETION_YES", value: "true")
|
|
129
194
|
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
195
|
+
self.promptForUrl(
|
|
196
|
+
question: "What URL link can users request to delete data?",
|
|
197
|
+
hash: hash,
|
|
198
|
+
question_id: "PSL_DATA_DELETION_URL",
|
|
199
|
+
response_id: "")
|
|
200
|
+
elsif self.valid_90_day_answers.include?(dataInputDeleteData.downcase)
|
|
133
201
|
self.addToHash(hash: hash, question_id: "PSL_SUPPORT_DATA_DELETION_BY_USER", response_id: "DATA_DELETION_NO_AUTO_DELETED", value: "true")
|
|
134
202
|
else
|
|
135
203
|
self.addToHash(hash: hash, question_id: "PSL_SUPPORT_DATA_DELETION_BY_USER", response_id: "DATA_DELETION_NO", value: "true")
|
|
@@ -139,7 +207,14 @@ module Fastlane
|
|
|
139
207
|
end
|
|
140
208
|
end
|
|
141
209
|
|
|
210
|
+
# Prompt question data type group question for Google Data Safety sheet.
|
|
211
|
+
#
|
|
212
|
+
# @param hash [hash] The hash to save data of answers to questions
|
|
142
213
|
def self.questionDataInfo(hash: nil)
|
|
214
|
+
UI.important("*** Below questions will be asked about location data ***")
|
|
215
|
+
self.questionDataType(title: "approximate location", code: "PSL_APPROX_LOCATION", group_code: "PSL_DATA_TYPES_LOCATION", hash: hash)
|
|
216
|
+
self.questionDataType(title: "precise location", code: "PSL_PRECISE_LOCATION", group_code: "PSL_DATA_TYPES_LOCATION", hash: hash)
|
|
217
|
+
|
|
143
218
|
UI.important("*** Below questions will be asked about personal information data ***")
|
|
144
219
|
self.questionDataType(title: "name", code: "PSL_NAME", group_code: "PSL_DATA_TYPES_PERSONAL", hash: hash)
|
|
145
220
|
self.questionDataType(title: "email", code: "PSL_EMAIL", group_code: "PSL_DATA_TYPES_PERSONAL", hash: hash)
|
|
@@ -157,12 +232,9 @@ module Fastlane
|
|
|
157
232
|
self.questionDataType(title: "credit score", code: "PSL_CREDIT_SCORE", group_code: "PSL_DATA_TYPES_FINANCIAL", hash: hash)
|
|
158
233
|
self.questionDataType(title: "other financial info", code: "PSL_OTHER", group_code: "PSL_DATA_TYPES_FINANCIAL", hash: hash)
|
|
159
234
|
|
|
160
|
-
UI.important("*** Below questions will be asked about
|
|
161
|
-
self.questionDataType(title: "
|
|
162
|
-
self.questionDataType(title: "
|
|
163
|
-
|
|
164
|
-
UI.important("*** Below questions will be asked about location data ***")
|
|
165
|
-
self.questionDataType(title: "web browser history", code: "PSL_WEB_BROWSING_HISTORY", group_code: "PSL_DATA_TYPES_SEARCH_AND_BROWSING", hash: hash)
|
|
235
|
+
UI.important("\n*** Below questions will be asked about audio and music data ***")
|
|
236
|
+
self.questionDataType(title: "health info", code: "PSL_HEALTH", group_code: "PSL_DATA_TYPES_HEALTH_AND_FITNESS", hash: hash)
|
|
237
|
+
self.questionDataType(title: "fitness info", code: "PSL_FITNESS", group_code: "PSL_DATA_TYPES_HEALTH_AND_FITNESS", hash: hash)
|
|
166
238
|
|
|
167
239
|
UI.important("*** Below questions will be asked about email and text data ***")
|
|
168
240
|
self.questionDataType(title: "email", code: "PSL_EMAILS", group_code: "PSL_DATA_TYPES_EMAIL_AND_TEXT", hash: hash)
|
|
@@ -178,127 +250,300 @@ module Fastlane
|
|
|
178
250
|
self.questionDataType(title: "music files", code: "PSL_MUSIC", group_code: "PSL_DATA_TYPES_AUDIO", hash: hash)
|
|
179
251
|
self.questionDataType(title: "Other audio files", code: "PSL_OTHER_AUDIO", group_code: "PSL_DATA_TYPES_AUDIO", hash: hash)
|
|
180
252
|
|
|
181
|
-
UI.important("
|
|
182
|
-
self.questionDataType(title: "
|
|
183
|
-
self.questionDataType(title: "fitness info", code: "PSL_FITNESS", group_code: "PSL_DATA_TYPES_HEALTH_AND_FITNESS", hash: hash)
|
|
184
|
-
|
|
185
|
-
UI.important("*** Below questions will be asked about contact data ***")
|
|
186
|
-
self.questionDataType(title: "contacts", code: "PSL_CONTACTS", group_code: "PSL_DATA_TYPES_CONTACTS", hash: hash)
|
|
253
|
+
UI.important("*** Below questions will be asked about files and documents data ***")
|
|
254
|
+
self.questionDataType(title: "files and docs", code: "PSL_FILES_AND_DOCS", group_code: "PSL_DATA_TYPES_FILES_AND_DOCS", hash: hash)
|
|
187
255
|
|
|
188
256
|
UI.important("*** Below questions will be asked about calendar data ***")
|
|
189
257
|
self.questionDataType(title: "calendar events", code: "PSL_CALENDAR", group_code: "PSL_DATA_TYPES_CALENDAR", hash: hash)
|
|
190
258
|
|
|
191
|
-
UI.important("*** Below questions will be asked about
|
|
192
|
-
self.questionDataType(title: "
|
|
193
|
-
self.questionDataType(title: "performance diagnostics", code: "PSL_PERFORMANCE_DIAGNOSTICS", group_code: "PSL_DATA_TYPES_APP_PERFORMANCE", hash: hash)
|
|
194
|
-
self.questionDataType(title: "other app performance data", code: "PSL_OTHER_PERFORMANCE", group_code: "PSL_DATA_TYPES_APP_PERFORMANCE", hash: hash)
|
|
195
|
-
|
|
196
|
-
UI.important("*** Below questions will be asked about files and documents data ***")
|
|
197
|
-
self.questionDataType(title: "files and docs", code: "PSL_FILES_AND_DOCS", group_code: "PSL_DATA_TYPES_FILES_AND_DOCS", hash: hash)
|
|
259
|
+
UI.important("*** Below questions will be asked about contact data ***")
|
|
260
|
+
self.questionDataType(title: "contacts", code: "PSL_CONTACTS", group_code: "PSL_DATA_TYPES_CONTACTS", hash: hash)
|
|
198
261
|
|
|
199
262
|
UI.important("*** Below questions will be asked about app activity data ***")
|
|
200
263
|
self.questionDataType(title: "app interactions", code: "PSL_USER_INTERACTION", group_code: "PSL_DATA_TYPES_APP_ACTIVITY", hash: hash)
|
|
201
|
-
self.questionDataType(title: "
|
|
264
|
+
self.questionDataType(title: "in-app search history", code: "PSL_IN_APP_SEARCH_HISTORY", group_code: "PSL_DATA_TYPES_APP_ACTIVITY", hash: hash)
|
|
265
|
+
self.questionDataType(title: "installed apps", code: "PSL_APPS_ON_DEVICE", group_code: "PSL_DATA_TYPES_APP_ACTIVITY", hash: hash)
|
|
202
266
|
self.questionDataType(title: "other user-generated content", code: "PSL_USER_GENERATED_CONTENT", group_code: "PSL_DATA_TYPES_APP_ACTIVITY", hash: hash)
|
|
203
267
|
self.questionDataType(title: "other actions", code: "PSL_OTHER_APP_ACTIVITY", group_code: "PSL_DATA_TYPES_APP_ACTIVITY", hash: hash)
|
|
204
268
|
|
|
269
|
+
UI.important("*** Below questions will be asked about location data ***")
|
|
270
|
+
self.questionDataType(title: "web browser history", code: "PSL_WEB_BROWSING_HISTORY", group_code: "PSL_DATA_TYPES_SEARCH_AND_BROWSING", hash: hash)
|
|
271
|
+
|
|
272
|
+
UI.important("*** Below questions will be asked about app performance data ***")
|
|
273
|
+
self.questionDataType(title: "crash log", code: "PSL_CRASH_LOGS", group_code: "PSL_DATA_TYPES_APP_PERFORMANCE", hash: hash)
|
|
274
|
+
self.questionDataType(title: "performance diagnostics", code: "PSL_PERFORMANCE_DIAGNOSTICS", group_code: "PSL_DATA_TYPES_APP_PERFORMANCE", hash: hash)
|
|
275
|
+
self.questionDataType(title: "other app performance data", code: "PSL_OTHER_PERFORMANCE", group_code: "PSL_DATA_TYPES_APP_PERFORMANCE", hash: hash)
|
|
276
|
+
|
|
205
277
|
UI.important("*** Below questions will be asked about identifier data ***")
|
|
206
278
|
self.questionDataType(title: "device or other IDs", code: "PSL_DEVICE_ID", group_code: "PSL_DATA_TYPES_IDENTIFIERS", hash: hash)
|
|
207
279
|
end
|
|
208
280
|
|
|
281
|
+
# Prompt question of a specific data type about data sharing and data collection for Google Data Safety sheet.
|
|
282
|
+
#
|
|
283
|
+
# @param title [String] The data type title
|
|
284
|
+
# @param code [String] Google Data Safety code name for CSV file format
|
|
285
|
+
# @param group_code [String] Google Data Safety group code name for CSV file format
|
|
286
|
+
# @param hash [hash] The hash to save data of answers to questions
|
|
209
287
|
def self.questionDataType(title: "", code: "", group_code: "", hash: nil)
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
self.
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
288
|
+
isDataInputCollectOrShare = self.promptForAnswerBoolean(
|
|
289
|
+
question: "Is #{title} data being collected or shared?",
|
|
290
|
+
hash: hash,
|
|
291
|
+
question_id: group_code,
|
|
292
|
+
response_id: code,
|
|
293
|
+
is_false_blank: true)
|
|
294
|
+
|
|
295
|
+
if isDataInputCollectOrShare
|
|
296
|
+
isDataInputShared = self.promptForAnswerBoolean(
|
|
297
|
+
question: "Is #{title} data being shared?",
|
|
298
|
+
hash: hash,
|
|
299
|
+
question_id: "PSL_DATA_USAGE_RESPONSES:#{code}:PSL_DATA_USAGE_COLLECTION_AND_SHARING",
|
|
300
|
+
response_id: "PSL_DATA_USAGE_ONLY_SHARED",
|
|
301
|
+
is_false_blank: true)
|
|
302
|
+
|
|
303
|
+
if isDataInputShared
|
|
304
|
+
self.promptForAnswerBoolean(
|
|
305
|
+
question: "Is #{title} shared for app functionality?",
|
|
306
|
+
hash: hash,
|
|
307
|
+
question_id: "PSL_DATA_USAGE_RESPONSES:#{code}:DATA_USAGE_SHARING_PURPOSE",
|
|
308
|
+
response_id: "PSL_APP_FUNCTIONALITY",
|
|
309
|
+
is_false_blank: true)
|
|
310
|
+
|
|
311
|
+
self.promptForAnswerBoolean(
|
|
312
|
+
question: "Is #{title} shared for analytics?",
|
|
313
|
+
hash: hash,
|
|
314
|
+
question_id: "PSL_DATA_USAGE_RESPONSES:#{code}:DATA_USAGE_SHARING_PURPOSE",
|
|
315
|
+
response_id: "PSL_ANALYTICS",
|
|
316
|
+
is_false_blank: true)
|
|
317
|
+
|
|
318
|
+
self.promptForAnswerBoolean(
|
|
319
|
+
question: "Is #{title} shared for developer communications?",
|
|
320
|
+
hash: hash,
|
|
321
|
+
question_id: "PSL_DATA_USAGE_RESPONSES:#{code}:DATA_USAGE_SHARING_PURPOSE",
|
|
322
|
+
response_id: "PSL_DEVELOPER_COMMUNICATIONS",
|
|
323
|
+
is_false_blank: true)
|
|
324
|
+
|
|
325
|
+
self.promptForAnswerBoolean(
|
|
326
|
+
question: "Is #{title} shared for advertising or marketing?",
|
|
327
|
+
hash: hash,
|
|
328
|
+
question_id: "PSL_DATA_USAGE_RESPONSES:#{code}:DATA_USAGE_SHARING_PURPOSE",
|
|
329
|
+
response_id: "PSL_ADVERTISING",
|
|
330
|
+
is_false_blank: true)
|
|
331
|
+
|
|
332
|
+
self.promptForAnswerBoolean(
|
|
333
|
+
question: "Is #{title} shared for fraud prevention, security, or compliance?",
|
|
334
|
+
hash: hash,
|
|
335
|
+
question_id: "PSL_DATA_USAGE_RESPONSES:#{code}:DATA_USAGE_SHARING_PURPOSE",
|
|
336
|
+
response_id: "PSL_FRAUD_PREVENTION_SECURITY",
|
|
337
|
+
is_false_blank: true)
|
|
338
|
+
|
|
339
|
+
self.promptForAnswerBoolean(
|
|
340
|
+
question: "Is #{title} shared for personalizing the app?",
|
|
341
|
+
hash: hash,
|
|
342
|
+
question_id: "PSL_DATA_USAGE_RESPONSES:#{code}:DATA_USAGE_SHARING_PURPOSE",
|
|
343
|
+
response_id: "PSL_PERSONALIZATION",
|
|
344
|
+
is_false_blank: true)
|
|
345
|
+
|
|
346
|
+
self.promptForAnswerBoolean(
|
|
347
|
+
question: "Is #{title} shared to manage account?",
|
|
348
|
+
hash: hash,
|
|
349
|
+
question_id: "PSL_DATA_USAGE_RESPONSES:#{code}:DATA_USAGE_SHARING_PURPOSE",
|
|
350
|
+
response_id: "PSL_ACCOUNT_MANAGEMENT",
|
|
351
|
+
is_false_blank: true)
|
|
239
352
|
end
|
|
240
353
|
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
354
|
+
isDataInputCollected = self.promptForAnswerBoolean(
|
|
355
|
+
question: "Is #{title} data being collected?",
|
|
356
|
+
hash: hash,
|
|
357
|
+
question_id: "PSL_DATA_USAGE_RESPONSES:#{code}:PSL_DATA_USAGE_COLLECTION_AND_SHARING",
|
|
358
|
+
response_id: "PSL_DATA_USAGE_ONLY_COLLECTED",
|
|
359
|
+
is_false_blank: true)
|
|
360
|
+
|
|
361
|
+
if isDataInputCollected
|
|
362
|
+
self.promptForAnswerBoolean(
|
|
363
|
+
question: "Is #{title} data processed ephemerally?",
|
|
364
|
+
hash: hash,
|
|
365
|
+
question_id: "PSL_DATA_USAGE_RESPONSES:#{code}:PSL_DATA_USAGE_EPHEMERAL",
|
|
366
|
+
response_id: "",
|
|
367
|
+
is_false_blank: false)
|
|
368
|
+
|
|
369
|
+
isDataInputRequired = self.promptForAnswerBoolean(
|
|
370
|
+
question: "Is #{title} data required for your app?",
|
|
371
|
+
hash: nil,
|
|
372
|
+
question_id: nil,
|
|
373
|
+
response_id: nil,
|
|
374
|
+
is_false_blank: false)
|
|
375
|
+
|
|
376
|
+
if isDataInputRequired
|
|
251
377
|
self.addToHash(hash: hash, question_id: "PSL_DATA_USAGE_RESPONSES:#{code}:DATA_USAGE_USER_CONTROL", response_id: "PSL_DATA_USAGE_USER_CONTROL_REQUIRED", value: "true")
|
|
252
378
|
else
|
|
253
379
|
self.addToHash(hash: hash, question_id: "PSL_DATA_USAGE_RESPONSES:#{code}:DATA_USAGE_USER_CONTROL", response_id: "PSL_DATA_USAGE_USER_CONTROL_OPTIONAL", value: "true")
|
|
254
380
|
end
|
|
255
381
|
|
|
382
|
+
self.promptForAnswerBoolean(
|
|
383
|
+
question: "Is #{title} collected for app functionality?",
|
|
384
|
+
hash: hash,
|
|
385
|
+
question_id: "PSL_DATA_USAGE_RESPONSES:#{code}:DATA_USAGE_COLLECTION_PURPOSE",
|
|
386
|
+
response_id: "PSL_APP_FUNCTIONALITY",
|
|
387
|
+
is_false_blank: true)
|
|
388
|
+
|
|
389
|
+
self.promptForAnswerBoolean(
|
|
390
|
+
question: "Is #{title} collected for analytics?",
|
|
391
|
+
hash: hash,
|
|
392
|
+
question_id: "PSL_DATA_USAGE_RESPONSES:#{code}:DATA_USAGE_COLLECTION_PURPOSE",
|
|
393
|
+
response_id: "PSL_ANALYTICS",
|
|
394
|
+
is_false_blank: true)
|
|
395
|
+
|
|
396
|
+
self.promptForAnswerBoolean(
|
|
397
|
+
question: "Is #{title} collected for developer communications?",
|
|
398
|
+
hash: hash,
|
|
399
|
+
question_id: "PSL_DATA_USAGE_RESPONSES:#{code}:DATA_USAGE_COLLECTION_PURPOSE",
|
|
400
|
+
response_id: "PSL_DEVELOPER_COMMUNICATIONS",
|
|
401
|
+
is_false_blank: true)
|
|
402
|
+
|
|
403
|
+
self.promptForAnswerBoolean(
|
|
404
|
+
question: "Is #{title} collected for advertising or marketing?",
|
|
405
|
+
hash: hash,
|
|
406
|
+
question_id: "PSL_DATA_USAGE_RESPONSES:#{code}:DATA_USAGE_COLLECTION_PURPOSE",
|
|
407
|
+
response_id: "PSL_ADVERTISING",
|
|
408
|
+
is_false_blank: true)
|
|
409
|
+
|
|
410
|
+
self.promptForAnswerBoolean(
|
|
411
|
+
question: "Is #{title} collected for fraud prevention, security, or compliance?",
|
|
412
|
+
hash: hash,
|
|
413
|
+
question_id: "PSL_DATA_USAGE_RESPONSES:#{code}:DATA_USAGE_COLLECTION_PURPOSE",
|
|
414
|
+
response_id: "PSL_FRAUD_PREVENTION_SECURITY",
|
|
415
|
+
is_false_blank: true)
|
|
416
|
+
|
|
417
|
+
self.promptForAnswerBoolean(
|
|
418
|
+
question: "Is #{title} collected for personalizing the app?",
|
|
419
|
+
hash: hash,
|
|
420
|
+
question_id: "PSL_DATA_USAGE_RESPONSES:#{code}:DATA_USAGE_COLLECTION_PURPOSE",
|
|
421
|
+
response_id: "PSL_PERSONALIZATION",
|
|
422
|
+
is_false_blank: true)
|
|
423
|
+
|
|
424
|
+
self.promptForAnswerBoolean(
|
|
425
|
+
question: "Is #{title} collected to manage account?",
|
|
426
|
+
hash: hash,
|
|
427
|
+
question_id: "PSL_DATA_USAGE_RESPONSES:#{code}:DATA_USAGE_COLLECTION_PURPOSE",
|
|
428
|
+
response_id: "PSL_ACCOUNT_MANAGEMENT",
|
|
429
|
+
is_false_blank: true)
|
|
430
|
+
end
|
|
431
|
+
end
|
|
432
|
+
end
|
|
256
433
|
|
|
257
|
-
|
|
258
|
-
|
|
434
|
+
# Save CSV file with data sanitation.
|
|
435
|
+
#
|
|
436
|
+
# @param data [hash] Google Data Safety hash of CSV content
|
|
437
|
+
# @param csv_file_path [String] CSV file path to save file at
|
|
438
|
+
def self.saveCsvFile(data: nil, csv_file_path: "")
|
|
439
|
+
FileUtils.mkdir_p(File.dirname(csv_file_path))
|
|
259
440
|
|
|
260
|
-
|
|
261
|
-
|
|
441
|
+
CSV.open(csv_file_path, "w") do |csv|
|
|
442
|
+
data.each do |row|
|
|
443
|
+
sanitized_row = row.map do |cell|
|
|
444
|
+
if cell.is_a?(String)
|
|
445
|
+
cleaned = cell.gsub(/[\t\r\n]/, '').strip
|
|
262
446
|
|
|
263
|
-
|
|
264
|
-
|
|
447
|
+
if cleaned.lstrip.start_with?(*FORMULA_CHARS)
|
|
448
|
+
cleaned = "'#{cleaned}"
|
|
449
|
+
end
|
|
265
450
|
|
|
266
|
-
|
|
267
|
-
|
|
451
|
+
cleaned
|
|
452
|
+
else
|
|
453
|
+
cell
|
|
454
|
+
end
|
|
455
|
+
end
|
|
268
456
|
|
|
269
|
-
|
|
270
|
-
|
|
457
|
+
csv << sanitized_row
|
|
458
|
+
end
|
|
459
|
+
end
|
|
460
|
+
end
|
|
271
461
|
|
|
272
|
-
|
|
273
|
-
|
|
462
|
+
# Prompt question to terminal for a yes or no answer. Reprompt user if answer is not a valid yes or no.
|
|
463
|
+
#
|
|
464
|
+
# @param question [String] The question that displays in terminal
|
|
465
|
+
# @param hash [hash] The hash data for Google Data Safety.
|
|
466
|
+
# @param question_id [String] The question id to add in the hash data. Review CSV file from Google Data Safety sheet for more details.
|
|
467
|
+
# @param response_id [String] The response id to add in the hash data. Review CSV file from Google Data Safety sheet for more details.
|
|
468
|
+
# @param is_false_blank [Boolean] Whether the false answer should be false or blank in CSV file. It is dependent on the question ID and response ID for Google Data Safety sheet format.
|
|
469
|
+
# @return [Boolean] The user's answer to the question
|
|
470
|
+
def self.promptForAnswerBoolean(question: "", hash: nil, question_id: nil, response_id: "", is_false_blank: false)
|
|
471
|
+
answer = UI.input(question + " Y/N")
|
|
472
|
+
|
|
473
|
+
while !self.valid_true_answers.include?(answer.downcase) && !self.valid_false_answers.include?(answer.downcase)
|
|
474
|
+
UI.important("Please provide \"y\", \"yes\", \"true\", or \"t\" for yes; and \"n\", \"no\", \"false\", \"f\" for no")
|
|
475
|
+
answer = UI.input(question + " Y/N")
|
|
476
|
+
end
|
|
274
477
|
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
478
|
+
if self.valid_true_answers.include?(answer.downcase)
|
|
479
|
+
self.addToHash(hash: hash, question_id: question_id, response_id: response_id, value: "true")
|
|
480
|
+
return true
|
|
481
|
+
else
|
|
482
|
+
self.addToHash(hash: hash, question_id: question_id, response_id: response_id, value: is_false_blank ? "" : "false")
|
|
483
|
+
return false
|
|
278
484
|
end
|
|
279
485
|
end
|
|
280
486
|
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
487
|
+
# Prompt question to terminal for a string answer. Reprompt user if answer is empty string.
|
|
488
|
+
#
|
|
489
|
+
# @param question [String] The question that displays in terminal
|
|
490
|
+
# @param hash [hash] The hash data for Google Data Safety
|
|
491
|
+
# @param question_id [String] The question id to add in the hash data. Review CSV file from Google Data Safety sheet for more details.
|
|
492
|
+
# @param response_id [String] The response id to add in the hash data. Review CSV file from Google Data Safety sheet for more details.
|
|
493
|
+
# @return [String] The user's answer to question
|
|
494
|
+
def self.promptForAnswer(question: "", hash: nil, question_id: nil, response_id: "")
|
|
495
|
+
answer = UI.input(question)
|
|
496
|
+
|
|
497
|
+
while answer.empty?
|
|
498
|
+
UI.important("Please provide an answer")
|
|
499
|
+
answer = UI.input(question)
|
|
284
500
|
end
|
|
501
|
+
|
|
502
|
+
self.addToHash(hash: hash, question_id: question_id, response_id: response_id, value: answer)
|
|
503
|
+
return answer
|
|
285
504
|
end
|
|
286
505
|
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
506
|
+
# Prompt question to terminal for URL answer. Reprompt user if answer is not a valid URL.
|
|
507
|
+
#
|
|
508
|
+
# @param question [String] The question that displays in terminal
|
|
509
|
+
# @param hash [hash] The hash data for Google Data Safety
|
|
510
|
+
# @param question_id [String] The question id to add in the hash data. Review CSV file from Google Data Safety sheet for more details.
|
|
511
|
+
# @param response_id [String] The response id to add in the hash data. Review CSV file from Google Data Safety sheet for more details.
|
|
512
|
+
# @return [String] The user's answer to question
|
|
513
|
+
def self.promptForUrl(question: "", hash: nil, question_id: nil, response_id: "")
|
|
514
|
+
answer = UI.input(question)
|
|
515
|
+
|
|
516
|
+
while !self.validUrl?(answer)
|
|
517
|
+
UI.important("Please provide a proper URL")
|
|
518
|
+
answer = UI.input(question)
|
|
292
519
|
end
|
|
520
|
+
|
|
521
|
+
self.addToHash(hash: hash, question_id: question_id, response_id: response_id, value: answer)
|
|
522
|
+
return answer
|
|
293
523
|
end
|
|
294
524
|
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
525
|
+
# Detect if URL is valid format.
|
|
526
|
+
#
|
|
527
|
+
# @param url [String] The url to validate
|
|
528
|
+
# @return [Boolean] The url is valid
|
|
529
|
+
def self.validUrl?(url)
|
|
530
|
+
uri = URI.parse(url)
|
|
531
|
+
uri.is_a?(URI::HTTP) && !uri.host.nil?
|
|
532
|
+
rescue URI::InvalidURIError
|
|
533
|
+
false
|
|
534
|
+
end
|
|
535
|
+
|
|
536
|
+
# Prompt question and answer to Google Data Safety hash
|
|
537
|
+
#
|
|
538
|
+
# @param hash [hash] The hash data for Google Data Safety
|
|
539
|
+
# @param question_id [String] The question id to add in the hash data. Review CSV file from Google Data Safety sheet for more details.
|
|
540
|
+
# @param response_id [String] The response id to add in the hash data. Review CSV file from Google Data Safety sheet for more details.
|
|
541
|
+
# @param value [String] The answer value
|
|
542
|
+
def self.addToHash(hash: nil, question_id: nil, response_id: "", value: nil)
|
|
543
|
+
if !value.nil? && !hash.nil?
|
|
544
|
+
hash["#{question_id},#{response_id}"] = value
|
|
300
545
|
end
|
|
301
|
-
|
|
546
|
+
end
|
|
302
547
|
|
|
303
548
|
def self.description
|
|
304
549
|
"Prompt user questions to generate Google data safety sheet"
|
|
@@ -17,7 +17,13 @@ module Fastlane
|
|
|
17
17
|
|
|
18
18
|
access_token = body["access_token"]
|
|
19
19
|
|
|
20
|
-
|
|
20
|
+
debug = false
|
|
21
|
+
|
|
22
|
+
if !params[:debug].nil?
|
|
23
|
+
debug = params[:debug]
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
self.send_data_safety_sheet(package_name: params[:package_name], auth_token: access_token, csv_content: csv_content, debug: debug)
|
|
21
27
|
|
|
22
28
|
end
|
|
23
29
|
|
|
@@ -79,7 +85,7 @@ module Fastlane
|
|
|
79
85
|
token
|
|
80
86
|
end
|
|
81
87
|
|
|
82
|
-
def self.send_data_safety_sheet(package_name: nil, auth_token: nil, csv_content: nil)
|
|
88
|
+
def self.send_data_safety_sheet(package_name: nil, auth_token: nil, csv_content: nil, debug: false)
|
|
83
89
|
if package_name.nil?
|
|
84
90
|
if UI.interactive?
|
|
85
91
|
UI.important("To not be asked package name, you can specify it using 'package_name'.")
|
|
@@ -111,6 +117,11 @@ module Fastlane
|
|
|
111
117
|
else
|
|
112
118
|
UI.error("Google Data Safety sheet upload error with API")
|
|
113
119
|
end
|
|
120
|
+
|
|
121
|
+
if debug
|
|
122
|
+
UI.important("Google Data Safety API response body")
|
|
123
|
+
UI.important(response.body)
|
|
124
|
+
end
|
|
114
125
|
end
|
|
115
126
|
|
|
116
127
|
end
|
|
@@ -169,6 +180,13 @@ module Fastlane
|
|
|
169
180
|
optional: true,
|
|
170
181
|
type: Hash
|
|
171
182
|
),
|
|
183
|
+
FastlaneCore::ConfigItem.new(
|
|
184
|
+
key: :debug,
|
|
185
|
+
env_name: "DEBUG",
|
|
186
|
+
description: "Set to debug mode for troubleshooting",
|
|
187
|
+
optional: true,
|
|
188
|
+
type: Boolean
|
|
189
|
+
),
|
|
172
190
|
]
|
|
173
191
|
end
|
|
174
192
|
|
metadata
CHANGED
|
@@ -1,30 +1,30 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: fastlane-plugin-google_data_safety
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.3.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Owen Bean
|
|
8
|
-
autorequire:
|
|
8
|
+
autorequire:
|
|
9
9
|
bindir: bin
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date:
|
|
11
|
+
date: 2026-08-05 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: googleauth
|
|
15
15
|
requirement: !ruby/object:Gem::Requirement
|
|
16
16
|
requirements:
|
|
17
|
-
- - "
|
|
17
|
+
- - ">="
|
|
18
18
|
- !ruby/object:Gem::Version
|
|
19
19
|
version: 1.8.1
|
|
20
20
|
type: :runtime
|
|
21
21
|
prerelease: false
|
|
22
22
|
version_requirements: !ruby/object:Gem::Requirement
|
|
23
23
|
requirements:
|
|
24
|
-
- - "
|
|
24
|
+
- - ">="
|
|
25
25
|
- !ruby/object:Gem::Version
|
|
26
26
|
version: 1.8.1
|
|
27
|
-
description:
|
|
27
|
+
description:
|
|
28
28
|
email: owenbean400@gmail.com
|
|
29
29
|
executables: []
|
|
30
30
|
extensions: []
|
|
@@ -43,7 +43,7 @@ licenses:
|
|
|
43
43
|
- MIT
|
|
44
44
|
metadata:
|
|
45
45
|
rubygems_mfa_required: 'true'
|
|
46
|
-
post_install_message:
|
|
46
|
+
post_install_message:
|
|
47
47
|
rdoc_options: []
|
|
48
48
|
require_paths:
|
|
49
49
|
- lib
|
|
@@ -58,8 +58,8 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
58
58
|
- !ruby/object:Gem::Version
|
|
59
59
|
version: '0'
|
|
60
60
|
requirements: []
|
|
61
|
-
rubygems_version: 3.
|
|
62
|
-
signing_key:
|
|
61
|
+
rubygems_version: 3.5.22
|
|
62
|
+
signing_key:
|
|
63
63
|
specification_version: 4
|
|
64
64
|
summary: Google safety data Fastlane plugin for automation.
|
|
65
65
|
test_files: []
|