Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
44 changes: 44 additions & 0 deletions .github/workflows/issue-branch-policy.yml
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,8 @@ jobs:
local pull_request_number="$1"
local expected_head_sha="$2"
local pull_request_json branch_name head_sha
local head_repo_full_name head_commit_json
local head_commit_author_login head_commit_verified head_commit_verification_reason
local category issue branch_date issue_json category_lines
local issue_categories=()
local issue_category
Expand Down Expand Up @@ -152,6 +154,48 @@ jobs:
return 2
fi

head_repo_full_name="$(jq -r '.head.repo.full_name // ""' <<< "$pull_request_json")" || {
printf 'Pull request #%s did not provide a readable head repository.\n' \
"$pull_request_number" >&2
return 1
}

head_commit_json="$(gh api "repos/$GITHUB_REPOSITORY/commits/$head_sha")" || {
printf 'Pull request #%s did not provide a readable head commit.\n' \
"$pull_request_number" >&2
return 1
}
head_commit_author_login="$(jq -r '.author.login // ""' <<< "$head_commit_json")" || {
printf 'Pull request #%s did not provide a readable head commit author.\n' \
"$pull_request_number" >&2
return 1
}
head_commit_verified="$(jq -r '.commit.verification.verified // false' <<< "$head_commit_json")" || {
printf 'Pull request #%s did not provide readable head commit verification state.\n' \
"$pull_request_number" >&2
return 1
}
head_commit_verification_reason="$(jq -r '.commit.verification.reason // ""' <<< "$head_commit_json")" || {
printf 'Pull request #%s did not provide a readable head commit verification reason.\n' \
"$pull_request_number" >&2
return 1
}

# Dependabot branches are generated by GitHub and cannot use the
# issue-backed branch naming convention. The PR author is not a
# trustworthy identity check: a collaborator can push a new head
# commit to an existing Dependabot branch after it is opened. Use
# the current head commit's verified GitHub identity instead.
if [[ "$head_commit_author_login" == "dependabot[bot]" &&
"$head_commit_verified" == true &&
"$head_commit_verification_reason" == valid &&
"$head_repo_full_name" == "$GITHUB_REPOSITORY" &&
"$branch_name" == dependabot/* ]]; then
printf 'Validated Dependabot branch %s for pull request #%s.\n' \
"$branch_name" "$pull_request_number"
return 0
fi

if [[ ! "$branch_name" =~ $branch_pattern ]]; then
printf 'Pull request #%s: branch name is not canonical.\n' \
"$pull_request_number" >&2
Expand Down
92 changes: 92 additions & 0 deletions tests/test_issue_branch_policy.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
from __future__ import annotations

import os
import shutil
import stat
import subprocess
import tempfile
from pathlib import Path

import pytest
import yaml


def test_dependabot_policy_rejects_an_unverified_collaborator_head_commit() -> None:
if os.name == "nt":
pytest.skip("the policy under test is a Bash workflow and is validated on POSIX runners")
if shutil.which("bash") is None:
pytest.skip("bash is required to execute the workflow policy")
if shutil.which("jq") is None:
pytest.skip("jq is required by the workflow policy")

root = Path(__file__).resolve().parents[1]
workflow = yaml.safe_load((root / ".github/workflows/issue-branch-policy.yml").read_text(encoding="utf-8"))
policy_run = workflow["jobs"]["policy"]["steps"][0]["run"]
head_sha = "a" * 40

fake_gh = """#!/bin/sh
set -eu
path=""
for argument in "$@"; do
case "$argument" in
repos/*) path="$argument"; break ;;
esac
done
case "$path" in
repos/basefoundry/base-cli/pulls\\?state=open\\&per_page=100)
printf '474\\t%s\\n' "$TEST_HEAD_SHA"
;;
repos/basefoundry/base-cli/pulls/474)
printf '%s\\n' '{"number":474,"state":"open","user":{"login":"dependabot[bot]"},"head":{"sha":"'"$TEST_HEAD_SHA"'","ref":"dependabot/github_actions/astral-sh/setup-uv-10.3.1","repo":{"full_name":"basefoundry/base-cli"}}}'
;;
repos/basefoundry/base-cli/commits/*)
printf '%s\\n' '{"author":{"login":"collaborator"},"commit":{"verification":{"verified":false,"reason":"unsigned"}}}'
;;
repos/basefoundry/base-cli/statuses/*)
;;
*)
printf 'unexpected gh api path: %s\\n' "$path" >&2
exit 2
;;
esac
"""

with tempfile.TemporaryDirectory() as directory:
fake_bin = Path(directory) / "bin"
fake_bin.mkdir()
gh_path = fake_bin / "gh"
gh_path.write_text(fake_gh, encoding="utf-8")
gh_path.chmod(gh_path.stat().st_mode | stat.S_IXUSR)

environment = os.environ.copy()
environment.update(
{
"PATH": os.pathsep.join((str(fake_bin), environment["PATH"])),
"GITHUB_REPOSITORY": "basefoundry/base-cli",
"GITHUB_SERVER_URL": "https://github.com",
"GITHUB_RUN_ID": "123",
"GH_TOKEN": "test-token",
"POLICY_CONTEXT": "base/issue-branch-policy",
"PULL_REQUEST_NUMBER": "474",
"ISSUE_EVENT_NUMBER": "",
"ISSUE_EVENT_LABEL": "",
"DEFAULT_BRANCH": "main",
"EVENT_NAME": "pull_request_target",
"EVENT_HEAD_SHA": head_sha,
"DISPATCH_HEAD_SHA": "",
"PREVIOUS_HEAD_SHA": "",
"RUN_SHA": "b" * 40,
"TEST_HEAD_SHA": head_sha,
}
)
completed = subprocess.run(
["bash", "-euo", "pipefail", "-c", policy_run],
cwd=root,
env=environment,
capture_output=True,
text=True,
)

assert completed.returncode != 0
assert "One or more PRs sharing this commit violate branch policy" in completed.stderr
assert "Validated Dependabot branch" not in completed.stdout
Loading