Class: Fastlane::Actions::IsCheckRequiredAction
- Inherits:
-
Action
- Object
- Action
- Fastlane::Actions::IsCheckRequiredAction
- Defined in:
- lib/fastlane/plugin/stream_actions/actions/is_check_required.rb
Documentation collapse
Class Method Summary collapse
-
.changed_file_paths(params) ⇒ Object
For pull_request: use full PR for
opened(etc.); forsynchronizepass github_event_before/after (e.g. github.event.before/after) to scope to this push. -
.commit_files(repo, sha) ⇒ Object
Files changed by a single commit (relative to its first parent).
- .compare_push_files(repo, before, after) ⇒ Object
- .gh_path_lines(output) ⇒ Object
-
.latest_conclusions(output) ⇒ Object
Reduces "name\tconclusion\tcompleted_at" lines to the latest conclusion per check name, so re-runs of the same check supersede earlier attempts.
-
.pr_commit_shas(pr_num) ⇒ Object
PR commits, newest first (gh returns them oldest first).
-
.required_checks_passed?(repo, sha, required_checks) ⇒ Boolean
True only if every required check has its latest run concluded as 'success' on the commit.
-
.required_due_to_history(params, required_checks) ⇒ Object
Walks the PR commits from newest to oldest until the last commit that changed :sources is found, then returns whether the check must run based on that commit's required check runs.
- .run(params) ⇒ Object
- .touches_sources?(files, sources) ⇒ Boolean
Class Method Details
.available_options ⇒ Object
159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 |
# File 'lib/fastlane/plugin/stream_actions/actions/is_check_required.rb', line 159 def self. [ FastlaneCore::ConfigItem.new( key: :sources, description: 'Array of paths to scan', is_string: false, verify_block: proc do |array| UI.user_error!("Sources have to be specified") unless array.kind_of?(Array) && array.size.positive? end ), FastlaneCore::ConfigItem.new( env_name: 'GITHUB_PR_NUM', key: :github_pr_num, description: 'GitHub PR number', optional: true ), FastlaneCore::ConfigItem.new( key: :required_checks, description: 'Names of GitHub check runs that must have concluded as success on the last commit ' \ 'that changed :sources for the check to be skipped when the current push does not ' \ 'touch :sources. When empty, the check is skipped as soon as the current push does ' \ 'not touch :sources', is_string: false, optional: true, default_value: [] ), FastlaneCore::ConfigItem.new( key: :force_check, description: 'GitHub PR number', optional: true, is_string: false ), FastlaneCore::ConfigItem.new( env_name: 'GITHUB_EVENT_ACTION', key: :github_event_action, description: 'pull_request action: e.g. opened, synchronize. When synchronize and before/after ' \ 'are set, only files in that push are considered', optional: true ), FastlaneCore::ConfigItem.new( env_name: 'GITHUB_EVENT_BEFORE', key: :github_event_before, description: 'github.event.before (head ref before the push) for pull_request', optional: true ), FastlaneCore::ConfigItem.new( env_name: 'GITHUB_EVENT_AFTER', key: :github_event_after, description: 'github.event.after (head ref after the push) for pull_request', optional: true ), FastlaneCore::ConfigItem.new( env_name: 'GITHUB_REPOSITORY', key: :github_repository, description: 'owner/repo; required for push-scoped file list (defaults to GITHUB_REPOSITORY in CI)', optional: true ) ] end |
.changed_file_paths(params) ⇒ Object
For pull_request: use full PR for opened (etc.); for synchronize pass
github_event_before/after (e.g. github.event.before/after) to scope to this push.
117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 |
# File 'lib/fastlane/plugin/stream_actions/actions/is_check_required.rb', line 117 def self.changed_file_paths(params) action = params[:github_event_action].to_s before = params[:github_event_before].to_s.strip after = params[:github_event_after].to_s.strip repo = (params[:github_repository] || ENV['GITHUB_REPOSITORY']).to_s if action == 'synchronize' && !before.empty? && !after.empty? && !repo.empty? if before.match?(/\A0+\z/) || !before.match?(/\A[0-9a-f]{7,40}\z/i) || !after.match?(/\A[0-9a-f]{7,40}\z/i) UI.important("Invalid before/after for compare; falling back to full PR file list") else out = self.compare_push_files(repo, before, after) return out unless out.nil? UI.important("Could not list push diff (e.g. fork/cross-repo); falling back to full PR file list") end end self.gh_path_lines(Actions.sh("gh pr view #{params[:github_pr_num]} --json files -q '.files[].path'")) end |
.commit_files(repo, sha) ⇒ Object
Files changed by a single commit (relative to its first parent).
78 79 80 81 82 |
# File 'lib/fastlane/plugin/stream_actions/actions/is_check_required.rb', line 78 def self.commit_files(repo, sha) self.gh_path_lines(Actions.sh("gh api repos/#{repo}/commits/#{sha} --paginate -q '.files[].filename'")) rescue StandardError [] end |
.compare_push_files(repo, before, after) ⇒ Object
137 138 139 140 141 142 143 144 145 |
# File 'lib/fastlane/plugin/stream_actions/actions/is_check_required.rb', line 137 def self.compare_push_files(repo, before, after) self.gh_path_lines(Actions.sh( "gh api \"repos/#{repo}/compare/#{before}...#{after}\" " \ "-H \"Accept: application/vnd.github.v3+json\" " \ "-q '.files[].filename'" )) rescue StandardError nil end |
.description ⇒ Object
155 156 157 |
# File 'lib/fastlane/plugin/stream_actions/actions/is_check_required.rb', line 155 def self.description 'Analyzes the impact of changes on PR' end |
.gh_path_lines(output) ⇒ Object
147 148 149 |
# File 'lib/fastlane/plugin/stream_actions/actions/is_check_required.rb', line 147 def self.gh_path_lines(output) output.to_s.split("\n", -1).map(&:strip).reject(&:empty?) end |
.is_supported?(platform) ⇒ Boolean
219 220 221 |
# File 'lib/fastlane/plugin/stream_actions/actions/is_check_required.rb', line 219 def self.is_supported?(platform) true end |
.latest_conclusions(output) ⇒ Object
Reduces "name\tconclusion\tcompleted_at" lines to the latest conclusion per check name, so re-runs of the same check supersede earlier attempts.
99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 |
# File 'lib/fastlane/plugin/stream_actions/actions/is_check_required.rb', line 99 def self.latest_conclusions(output) latest = {} seen_at = {} output.to_s.split("\n", -1).each do |line| name, conclusion, completed_at = line.split("\t", -1) next if name.nil? || name.strip.empty? completed_at = completed_at.to_s next if seen_at.key?(name) && completed_at < seen_at[name] seen_at[name] = completed_at latest[name] = conclusion.to_s end latest end |
.pr_commit_shas(pr_num) ⇒ Object
PR commits, newest first (gh returns them oldest first).
71 72 73 74 75 |
# File 'lib/fastlane/plugin/stream_actions/actions/is_check_required.rb', line 71 def self.pr_commit_shas(pr_num) self.gh_path_lines(Actions.sh("gh pr view #{pr_num} --json commits -q '.commits[].oid'")).reverse rescue StandardError [] end |
.required_checks_passed?(repo, sha, required_checks) ⇒ Boolean
True only if every required check has its latest run concluded as 'success' on the commit.
85 86 87 88 89 90 91 92 93 94 95 |
# File 'lib/fastlane/plugin/stream_actions/actions/is_check_required.rb', line 85 def self.required_checks_passed?(repo, sha, required_checks) out = Actions.sh( "gh api \"repos/#{repo}/commits/#{sha}/check-runs?per_page=100\" --paginate " \ "-H \"Accept: application/vnd.github.v3+json\" " \ "-q '.check_runs[] | \"\\(.name)\\t\\(.conclusion)\\t\\(.completed_at)\"'" ) latest = self.latest_conclusions(out) required_checks.all? { |name| latest[name] == 'success' } rescue StandardError false end |
.required_due_to_history(params, required_checks) ⇒ Object
Walks the PR commits from newest to oldest until the last commit that changed :sources is found, then returns whether the check must run based on that commit's required check runs.
41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 |
# File 'lib/fastlane/plugin/stream_actions/actions/is_check_required.rb', line 41 def self.required_due_to_history(params, required_checks) repo = (params[:github_repository] || ENV['GITHUB_REPOSITORY']).to_s if repo.empty? UI.important("No repository provided; cannot verify previous runs, running check") return true end shas = self.pr_commit_shas(params[:github_pr_num]) if shas.empty? UI.important("Could not list PR commits; running check") return true end sources_sha = shas.find { |sha| self.touches_sources?(self.commit_files(repo, sha), params[:sources]) } if sources_sha.nil? UI.("No commit in this PR changed sources; nothing to test") return false end short = sources_sha[0, 7] if self.required_checks_passed?(repo, sources_sha, required_checks) UI.("Last sources commit #{short} passed required checks; safe to skip") false else UI.important("Last sources commit #{short} did not pass required checks; running check") true end end |
.run(params) ⇒ Object
4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 |
# File 'lib/fastlane/plugin/stream_actions/actions/is_check_required.rb', line 4 def self.run(params) return true if params[:force_check] || params[:github_pr_num].nil? || params[:github_pr_num].strip.empty? UI.("Checking if check is required for PR ##{params[:github_pr_num]}") changed_files = self.changed_file_paths(params) too_many_files = changed_files.size > 99 # TODO: https://github.com/cli/cli/issues/5368 if too_many_files UI.important("Check is required because there were too many files changed.") return true end if self.touches_sources?(changed_files, params[:sources]) UI.important("Check is required: true") return true end required_checks = params[:required_checks].to_a if required_checks.empty? UI.important("Check is required: false") return false end # The current push does not touch :sources. It is only safe to skip if the last commit that # did change :sources had all required checks pass; otherwise :sources were never verified. is_check_required = self.required_due_to_history(params, required_checks) UI.important("Check is required: #{is_check_required}") is_check_required end |
.touches_sources?(files, sources) ⇒ Boolean
35 36 37 |
# File 'lib/fastlane/plugin/stream_actions/actions/is_check_required.rb', line 35 def self.touches_sources?(files, sources) files.any? { |path| sources.any? { |required| path.start_with?(required) } } end |