Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Refactor ios_check_beta_deps action to make it more resilient & detect Pods referenced by commit/branch #557

Merged
merged 9 commits into from
Apr 11, 2024
3 changes: 2 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,8 @@

### Breaking Changes

_None_
- Make `ios_check_beta_deps` use the `Podfile.lock` instead of `Podfile` for its detection, and also be able to detect Pods referenced by commits and branches.
mokagio marked this conversation as resolved.
Show resolved Hide resolved
If your `Fastfile` called this action with an explicit `podfile: …` argument, you'll have to update the call to use `lockfile:` instead (or rely on defaults). [#557]

### New Features

Expand Down
4 changes: 4 additions & 0 deletions MIGRATION.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
# Migration Instructions for Major Releases

## From `10.0.0` to `11.0.0`

- The `ios_check_beta_deps` now uses the `Podfile.lock` instead of `Podfile` for its detection. If you called this action with an explicit `podfile: …` argument, you'll have to update the call to use `lockfile:` instead—or remove that argument completely from your call and rely on its default value being `Podfile.lock`.
mokagio marked this conversation as resolved.
Show resolved Hide resolved

## From `9.0.0` to `10.0.0`

- The new minimum required Ruby version is `3.2.2`. Please make sure to upgrade your projects before upgrading to this version to avoid compatibility issues.
Expand Down
Original file line number Diff line number Diff line change
@@ -1,31 +1,56 @@
require 'yaml'

module Fastlane
module Actions
class IosCheckBetaDepsAction < Action
def self.run(params)
require_relative '../../helper/ios/ios_version_helper'
require_relative '../../helper/ios/ios_git_helper'

beta_pods = []
File.open(params[:podfile]).each do |li|
beta_pods << li if li.match('^\s*\t*pod.*beta')
yaml = YAML.load_file(params[:lockfile])
non_stable_pods = {} # Key will be pod name, value will be reason for flagging

# Find pods referenced by commit and branch to a repo
yaml['EXTERNAL SOURCES'].each do |pod, options|
Copy link
Contributor Author

@AliSoftware AliSoftware Apr 10, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I just realized that some Podfile.lock files might not have this section at all (if they don't have any external dependency), so better cover for the nil case.

Suggested change
yaml['EXTERNAL SOURCES'].each do |pod, options|
yaml['EXTERNAL SOURCES']&.each do |pod, options|

Ideally we should add a spec example to cover that too—the one from Simplenote-macOS can serve as a fixture for that case.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@spencertransier I've fixed this (and added test coverage for it) via dc5992f + b1a13d9, so PR is now ready for review again 🙇

non_stable_pods[pod] = 'commit' if params[:report_commits] && options.key?(:commit)
non_stable_pods[pod] = 'branch' if params[:report_branches] && options.key?(:branch)
end

if beta_pods.count.zero?
UI.message('No beta pods found. You can continue with the code freeze.')
# Find pods whose resolved version matches the regex
version_pattern = params[:report_version_pattern]
unless version_pattern.nil? || version_pattern.empty?
regex = begin
Regexp.new(version_pattern)
rescue RegexpError
UI.user_error!("Invalid regex pattern: `#{version_pattern}`")
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Side note. I wanted to share appreciation for this kind of error handling where the things that's wrong is clearly reported in the error message. Recently, I've bump my head against some vague error messages; seeing this is encouraging. Thanks!

end
resolved_pods = yaml['PODS'].flat_map { |entry| entry.is_a?(Hash) ? entry.keys : entry }
resolved_pods.each do |line|
(pod, version) = /(.*) \((.*)\)/.match(line)&.captures
non_stable_pods[pod] = regex.source if regex.match?(version)
end
end

message = ''
if non_stable_pods.empty?
message << ALL_PODS_STABLE_MESSAGE
else
message = "The following pods are still in beta:\n"
beta_pods.each do |bpod|
message << "#{bpod}\n"
message << NON_STABLE_PODS_MESSAGE
non_stable_pods.sort.each do |pod, reason|
message << " - #{pod} (currently points to #{reason})\n"
end
message << 'Please update to the released version before continuing with the code freeze.'
end
UI.important(message)
{ message: message, pods: non_stable_pods }
end

#####################################################
# @!group Documentation
#####################################################

ALL_PODS_STABLE_MESSAGE = 'All pods are pointing to a stable version. You can continue with the code freeze.'.freeze
NON_STABLE_PODS_MESSAGE = "Please create a new stable version of those pods and update the Podfile to the newly released version before continuing with the code freeze:\n".freeze
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why declare these constants here instead of at the top of the run method?

Copy link
Contributor Author

@AliSoftware AliSoftware Apr 10, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We need those constant to be accessible from outside the code of def self.run because they are also referenced by our unit tests (here and here) 😉

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

While we couldn't move them at the top of the run method, I've moved them at the top of the class definition in 25420dd, at least

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah, got it. Thank you!


def self.description
'Runs some prechecks before finalizing a release'
end
Expand All @@ -36,18 +61,45 @@ def self.details

def self.available_options
[
FastlaneCore::ConfigItem.new(key: :podfile,
env_name: 'FL_IOS_CHECKBETADEPS_PODFILE',
description: 'Path to the Podfile to analyse',
type: String),
FastlaneCore::ConfigItem.new(
key: :lockfile,
description: 'Path to the Podfile.lock to analyse',
default_value: 'Podfile.lock',
type: String,
verify_block: proc do |value|
UI.user_error!("File `#{value}` does not exist") unless File.exist?(value)
end
),
FastlaneCore::ConfigItem.new(
key: :report_commits,
description: 'Whether to report pods referenced by commit',
default_value: true,
type: Boolean
),
FastlaneCore::ConfigItem.new(
key: :report_branches,
description: 'Whether to report pods referenced by branch',
default_value: true,
type: Boolean
),
FastlaneCore::ConfigItem.new(
key: :report_version_pattern,
description: 'Report any pod whose tag name or version constraint matches this Regex pattern. Set to empty string to ignore',
AliSoftware marked this conversation as resolved.
Show resolved Hide resolved
default_value: '-beta|-rc',
type: String
),
]
end

def self.output
end

def self.return_value
''
<<~RETURN_VALUE
A Hash with keys `message` and `pods`.
- The `message` can be used to e.g. post a Buildkite annotation.
- The `pods` is itself a `Hash` whose keys are the pod names and values are the reason for the violation
RETURN_VALUE
end

def self.authors
Expand Down
129 changes: 129 additions & 0 deletions spec/ios_check_beta_deps_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
require 'spec_helper'

describe Fastlane::Actions::IosCheckBetaDepsAction do
let(:lockfile_fixture_path) { File.join(File.dirname(__FILE__), 'test-data', 'Podfile.lock') }

def violation_message(**pods)
list = pods.map do |pod, reason|
" - #{pod} (currently points to #{reason})\n"
end
Fastlane::Actions::IosCheckBetaDepsAction::NON_STABLE_PODS_MESSAGE + list.join
end

before do
allow(FastlaneCore::UI).to receive(:important)
end

it 'reports Pods referenced by commits, branches and -beta by default' do
expected_violations = {
'Gridicons' => '-beta|-rc',
'NSURL+IDN' => '-beta|-rc',
'WordPressAuthenticator' => 'commit',
'WordPressUI' => 'branch'
}
expected_message = violation_message(**expected_violations)
expect(FastlaneCore::UI).to receive(:important).with(expected_message)

result = run_described_fastlane_action(
lockfile: lockfile_fixture_path
)

expect(result[:pods]).to eq(expected_violations)
expect(result[:message]).to eq(expected_message)
end

it 'does not report Pods referenced by commits if option disabled' do
expected_violations = {
'Gridicons' => '-beta|-rc',
'NSURL+IDN' => '-beta|-rc',
'WordPressUI' => 'branch'
}
expected_message = violation_message(**expected_violations)
expect(FastlaneCore::UI).to receive(:important).with(expected_message)

result = run_described_fastlane_action(
lockfile: lockfile_fixture_path,
report_commits: false
)

expect(result[:pods]).to eq(expected_violations)
expect(result[:message]).to eq(expected_message)
end

it 'does not report Pods referenced by branch if option disabled' do
expected_violations = {
'Gridicons' => '-beta|-rc',
'NSURL+IDN' => '-beta|-rc',
'WordPressAuthenticator' => 'commit'
}
expected_message = violation_message(**expected_violations)
expect(FastlaneCore::UI).to receive(:important).with(expected_message)

result = run_described_fastlane_action(
lockfile: lockfile_fixture_path,
report_branches: false
)

expect(result[:pods]).to eq(expected_violations)
expect(result[:message]).to eq(expected_message)
end

it 'does not report Pods referenced by *-beta if regex is empty' do
expected_violations = {
'WordPressAuthenticator' => 'commit',
'WordPressUI' => 'branch'
}
expected_message = violation_message(**expected_violations)
expect(FastlaneCore::UI).to receive(:important).with(expected_message)

result = run_described_fastlane_action(
lockfile: lockfile_fixture_path,
report_version_pattern: ''
)

expect(result[:pods]).to eq(expected_violations)
expect(result[:message]).to eq(expected_message)
end

it 'report Pods referenced by version matching custom regex' do
expected_violations = {
'NSURL+IDN' => '.*-rc-\d',
'WordPressAuthenticator' => 'commit',
'WordPressUI' => 'branch'
}
expected_message = violation_message(**expected_violations)
expect(FastlaneCore::UI).to receive(:important).with(expected_message)

result = run_described_fastlane_action(
lockfile: lockfile_fixture_path,
report_version_pattern: '.*-rc-\d'
)

expect(result[:pods]).to eq(expected_violations)
expect(result[:message]).to eq(expected_message)
end

it 'does not report any error if everything is disabled' do
expected_message = Fastlane::Actions::IosCheckBetaDepsAction::ALL_PODS_STABLE_MESSAGE
expect(FastlaneCore::UI).to receive(:important).with(expected_message)

result = run_described_fastlane_action(
lockfile: lockfile_fixture_path,
report_commits: false,
report_branches: false,
report_version_pattern: ''
)

expect(result[:pods]).to eq({})
expect(result[:message]).to eq(expected_message)
end

it 'raises user_error! if regex is invalid' do
expect do
run_described_fastlane_action(
lockfile: lockfile_fixture_path,
report_version_pattern: '*-rc-\d'
)
end.to raise_exception(FastlaneCore::Interface::FastlaneError, 'Invalid regex pattern: `*-rc-\d`')
end
end
Loading