diff --git a/common/lib/dependabot/git_commit_checker.rb b/common/lib/dependabot/git_commit_checker.rb index 293e4c79f7e..b13848140ab 100644 --- a/common/lib/dependabot/git_commit_checker.rb +++ b/common/lib/dependabot/git_commit_checker.rb @@ -86,6 +86,10 @@ def head_commit_for_current_branch raise Dependabot::GitDependencyReferenceNotFound, dependency.name end + def head_commit_for_local_branch(name) + local_repo_git_metadata_fetcher.head_commit_for_ref(name) + end + def local_tags_for_latest_version_commit_sha tags = allowed_version_tags max_tag = max_version_tag(tags) diff --git a/common/spec/dependabot/git_commit_checker_spec.rb b/common/spec/dependabot/git_commit_checker_spec.rb index 76f5aeefbc9..dd939978479 100644 --- a/common/spec/dependabot/git_commit_checker_spec.rb +++ b/common/spec/dependabot/git_commit_checker_spec.rb @@ -884,6 +884,29 @@ end end + describe "#head_commit_for_local_branch" do + let(:tip_of_example) { "303b8a83c87d5c6d749926cf02620465a5dcd0f2" } + + subject { checker.head_commit_for_local_branch("example") } + + let(:repo_url) { "https://github.com/gocardless/business.git" } + let(:service_pack_url) { repo_url + "/info/refs?service=git-upload-pack" } + before do + stub_request(:get, service_pack_url). + to_return( + status: 200, + body: fixture("git", "upload_packs", upload_pack_fixture), + headers: { + "content-type" => "application/x-git-upload-pack-advertisement" + } + ) + end + + let(:upload_pack_fixture) { "monolog" } + + it { is_expected.to eq(tip_of_example) } + end + describe "#local_tag_for_latest_version" do subject { checker.local_tag_for_latest_version } let(:repo_url) { "https://github.com/gocardless/business.git" } diff --git a/github_actions/lib/dependabot/github_actions.rb b/github_actions/lib/dependabot/github_actions.rb index ce40cd003ab..9339dc16468 100644 --- a/github_actions/lib/dependabot/github_actions.rb +++ b/github_actions/lib/dependabot/github_actions.rb @@ -22,3 +22,6 @@ require "dependabot/dependency" Dependabot::Dependency. register_production_check("github_actions", ->(_) { true }) + +require "dependabot/utils" +Dependabot::Utils.register_always_clone("github_actions") diff --git a/github_actions/lib/dependabot/github_actions/update_checker.rb b/github_actions/lib/dependabot/github_actions/update_checker.rb index 92273b7b483..b01df66e836 100644 --- a/github_actions/lib/dependabot/github_actions/update_checker.rb +++ b/github_actions/lib/dependabot/github_actions/update_checker.rb @@ -59,7 +59,7 @@ def fetch_latest_version end def fetch_latest_version_for_git_dependency - return git_commit_checker.head_commit_for_current_branch unless git_commit_checker.pinned? + return current_commit unless git_commit_checker.pinned? # If the dependency is pinned to a tag that looks like a version then # we want to update that tag. @@ -70,11 +70,11 @@ def fetch_latest_version_for_git_dependency return latest_version end - # If the dependency is pinned to a commit SHA, we return a *version* so - # that we get nice behaviour in PullRequestCreator::MessageBuilder - if git_commit_checker.pinned_ref_looks_like_commit_sha? - latest_tag = git_commit_checker.local_tag_for_latest_version - return latest_tag.fetch(:version) + if git_commit_checker.pinned_ref_looks_like_commit_sha? && latest_version_tag + latest_version = latest_version_tag.fetch(:version) + return latest_commit_for_pinned_ref unless git_commit_checker.branch_or_ref_in_release?(latest_version) + + return latest_version end # If the dependency is pinned to a tag that doesn't look like a @@ -82,6 +82,15 @@ def fetch_latest_version_for_git_dependency nil end + def latest_commit_for_pinned_ref + @latest_commit_for_pinned_ref ||= + SharedHelpers.in_a_temporary_repo_directory("/", repo_contents_path) do + ref_branch = find_container_branch(current_commit) + + git_commit_checker.head_commit_for_local_branch(ref_branch) + end + end + def latest_version_tag @latest_version_tag ||= begin return git_commit_checker.local_tag_for_latest_version if dependency.version.nil? @@ -119,18 +128,28 @@ def updated_source return dependency_source_details.merge(ref: new_tag.fetch(:tag)) end - latest_tag = git_commit_checker.local_tag_for_latest_version - # Update the pinned git commit if one is available if git_commit_checker.pinned_ref_looks_like_commit_sha? && - latest_tag.fetch(:commit_sha) != current_commit - return dependency_source_details.merge(ref: latest_tag.fetch(:commit_sha)) + (new_commit_sha = latest_commit_sha) && + new_commit_sha != current_commit + return dependency_source_details.merge(ref: new_commit_sha) end # Otherwise return the original source dependency_source_details end + def latest_commit_sha + new_tag = latest_version_tag + return unless new_tag + + if git_commit_checker.branch_or_ref_in_release?(new_tag.fetch(:version)) + new_tag.fetch(:commit_sha) + else + latest_commit_for_pinned_ref + end + end + def dependency_source_details sources = dependency.requirements.map { |r| r.fetch(:source) }.uniq.compact @@ -180,6 +199,23 @@ def shortened_semver_version_eq?(base_version, other_version) shortened_semver_eq?(base, other) || shortened_semver_eq?(other, base) end + + def find_container_branch(sha) + SharedHelpers.run_shell_command("git fetch #{current_commit}") + + branches_including_ref = SharedHelpers.run_shell_command("git branch --contains #{sha}").split("\n") + + current_branch = branches_including_ref.find { |line| line.start_with?("* ") } + + if current_branch + current_branch.delete_prefix("* ") + elsif branches_including_ref.size > 1 + # If there are multiple non default branches including the pinned SHA, then it's unclear how we should proceed + raise "Multiple ambiguous branches (#{branches_including_ref.join(', ')}) include #{current_commit}!" + else + branches_including_ref.first + end + end end end end diff --git a/github_actions/spec/dependabot/github_actions/update_checker_spec.rb b/github_actions/spec/dependabot/github_actions/update_checker_spec.rb index 462591da163..b5bce5c61c8 100644 --- a/github_actions/spec/dependabot/github_actions/update_checker_spec.rb +++ b/github_actions/spec/dependabot/github_actions/update_checker_spec.rb @@ -54,6 +54,14 @@ "https://github.com/actions/setup-node.git/info/refs" \ "?service=git-upload-pack" end + let(:git_commit_checker) do + Dependabot::GitCommitChecker.new( + dependency: dependency, + credentials: github_credentials, + ignored_versions: ignored_versions, + raise_on_ignored: raise_on_ignored + ) + end before do stub_request(:get, service_pack_url). to_return( @@ -128,7 +136,7 @@ it { is_expected.to be_falsey } end - context "that is a git commit SHA" do + context "that is a git commit SHA pointing to the tip of a branch" do let(:upload_pack_fixture) { "setup-node" } let(:reference) { "1c24df3" } @@ -141,6 +149,9 @@ body: comparison_response, headers: { "Content-Type" => "application/json" } ) + + checker.instance_variable_set(:@git_commit_checker, git_commit_checker) + allow(git_commit_checker).to receive(:branch_or_ref_in_release?).and_return(true) end context "when the specified commit has diverged from the latest release" do @@ -202,9 +213,11 @@ describe "#latest_version" do subject { checker.latest_version } + let(:tip_of_master) { "d963e800e3592dd31d6c76252092562d0bc7a3ba" } + context "given a dependency with a branch reference" do let(:reference) { "master" } - it { is_expected.to eq("d963e800e3592dd31d6c76252092562d0bc7a3ba") } + it { is_expected.to eq(tip_of_master) } end context "given a dependency with a tag reference" do @@ -276,9 +289,8 @@ } end - allow_any_instance_of(Dependabot::GitCommitChecker). - to receive(:local_tags_for_latest_version_commit_sha). - and_return(version_tags) + checker.instance_variable_set(:@git_commit_checker, git_commit_checker) + allow(git_commit_checker).to receive(:local_tags_for_latest_version_commit_sha).and_return(version_tags) end context "using the major version" do @@ -325,7 +337,7 @@ it { is_expected.to eq(Dependabot::GithubActions::Version.new("3")) } end - context "given a git commit SHA" do + context "given a git commit SHA pointing to the tip of a branch" do let(:reference) { "1c24df3" } let(:repo_url) { "https://api.github.com/repos/actions/setup-node" } @@ -337,6 +349,9 @@ body: comparison_response, headers: { "Content-Type" => "application/json" } ) + + checker.instance_variable_set(:@git_commit_checker, git_commit_checker) + allow(git_commit_checker).to receive(:branch_or_ref_in_release?).and_return(true) end context "when the specified commit has diverged from the latest release" do @@ -370,6 +385,70 @@ expect(subject).to eq(Gem::Version.new("2.2.0")) end end + + context "that is a git commit SHA not pointing to the tip of a branch" do + let(:reference) { "1c24df3" } + let(:exit_status) { double(success?: true) } + + before do + checker.instance_variable_set(:@git_commit_checker, git_commit_checker) + allow(git_commit_checker).to receive(:branch_or_ref_in_release?).and_return(false) + allow(git_commit_checker).to receive(:head_commit_for_current_branch).and_return(reference) + + allow(Open3).to receive(:capture2e).with(anything, "git fetch #{reference}").and_return(["", exit_status]) + end + + context "and it's in the current (default) branch" do + before do + allow(Open3).to receive(:capture2e). + with(anything, "git branch --contains #{reference}"). + and_return(["* master\n", exit_status]) + end + + it "can update to the latest version" do + expect(subject).to eq(tip_of_master) + end + end + + context "and it's on a different branch" do + let(:tip_of_releases_v1) { "5273d0df9c603edc4284ac8402cf650b4f1f6686" } + + before do + allow(Open3).to receive(:capture2e). + with(anything, "git branch --contains #{reference}"). + and_return(["releases/v1\n", exit_status]) + end + + it "can update to the latest version" do + expect(subject).to eq(tip_of_releases_v1) + end + end + + context "and multiple branches include it, the current (default) branch among them" do + before do + allow(Open3).to receive(:capture2e). + with(anything, "git branch --contains #{reference}"). + and_return(["* master\nv1.1\n", exit_status]) + end + + it "can update to the latest version" do + expect(subject).to eq(tip_of_master) + end + end + + context "and multiple branches include it, the current (default) branch NOT among them" do + before do + allow(Open3).to receive(:capture2e). + with(anything, "git branch --contains #{reference}"). + and_return(["3.3-stable\nproduction\n", exit_status]) + end + + it "raises an error" do + expect { subject }. + to raise_error("Multiple ambiguous branches (3.3-stable, production) include #{reference}!") + end + end + end end describe "#latest_resolvable_version" do @@ -387,7 +466,7 @@ it { is_expected.to eq(dependency.requirements) } end - context "given a git commit SHA" do + context "given a git commit SHA pointing to the tip of a branch" do let(:reference) { "1c24df3" } let(:repo_url) { "https://api.github.com/repos/actions/setup-node" } @@ -399,6 +478,9 @@ body: comparison_response, headers: { "Content-Type" => "application/json" } ) + + checker.instance_variable_set(:@git_commit_checker, git_commit_checker) + allow(git_commit_checker).to receive(:branch_or_ref_in_release?).and_return(true) end context "when the specified reference is not in the latest release" do