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
65 changes: 58 additions & 7 deletions .github/scripts/merge-order-guard-policy.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -195,7 +195,7 @@ test("closing a blocker refreshes PRs found only through unlinked close keywords
test("refresh queries every open PR page with the same repository-aware closing data", () => {
assert.match(guard, /gh api graphql --paginate --slurp/);
assert.match(guard, /pullRequests\(states:OPEN,first:100,after:\$endCursor\)/);
assert.match(guard, /nodes\{number title body closingIssuesReferences/);
assert.match(guard, /nodes\{number headRefOid title body closingIssuesReferences/);
assert.match(guard, /pageInfo\{hasNextPage endCursor\}/);
});

Expand Down Expand Up @@ -380,11 +380,20 @@ test("merge queue still fails on an open issue blocker", () => {
assert.equal(result.status, 1, result.stderr);
});

test("blocker changes dispatch current default-branch policy instead of rerunning old workflow code", () => {
test("blocker changes rerun the trusted pull_request_target run and dispatch only when none exists (#1983)", () => {
// #1955 는 rerun 이 PR 사본의 옛 워크플로 코드를 돌린다는 이유로 dispatch 재게시로 바꿨다.
// #1977 뒤 PR HEAD 의 run 은 base YAML·default-branch 스크립트로 도는 run 이라 재실행이 곧
// 최신 판정이고, check-run 재게시가 임의 suite 에 얹혀 무관한 워크플로 이름 아래 guard 가
// 또 뜨는 일을 피한다. rerun 대상은 pull_request_target run 으로만 좁힌다.
assert.match(guard, /gh run list --workflow merge-order-guard.yml --event pull_request_target \\\n\s+--commit "\$sha" --status completed --limit 1/);
assert.match(guard, /\[ -n "\$run_id" \] && gh run rerun "\$run_id"/);
assert.doesNotMatch(guard, /gh run list --workflow merge-order-guard.yml --branch/);
assert.match(guard, /gh workflow run merge-order-guard.yml --ref "\$DEFAULT_BRANCH" -f pull_request_number="\$pr"/);
assert.doesNotMatch(guard, /gh run rerun/);
assert.match(guard, /github.ref_name == github.event.repository.default_branch/);
assert.match(guard, /needs.guard.outputs.target_sha/);
// HEAD SHA 는 이미 받은 GraphQL 페이지에서 읽는다 — PR 마다 REST 왕복을 더하지 않는다 (#1465).
assert.match(guard, /nodes\{number headRefOid title body closingIssuesReferences/);
assert.doesNotMatch(guard.slice(guard.indexOf("dispatch_failed=0")), /gh pr view/);
});

test("dispatch publication refuses changed or closed HEAD and publishes failure on the captured HEAD", () => {
Expand Down Expand Up @@ -420,20 +429,62 @@ test("dispatch publication refuses changed or closed HEAD and publishes failure
});


test("one failed refresh dispatch does not prevent the remaining PRs from refreshing", () => {
function runRefreshLoop(ghMock, prs) {
const start = guard.indexOf(" dispatch_failed=0");
assert.ok(start >= 0);
const script = guard.slice(start).split("\n").map((line) => line.slice(10)).join("\n");
const result = spawnSync("bash", ["-c", `set -euo pipefail
heads="101 102"
const directory = mkdtempSync(join(tmpdir(), "merge-order-refresh-loop-"));
const prsFile = join(directory, "open-prs.json");
writeFileSync(prsFile, JSON.stringify([{ data: { repository: { pullRequests: { nodes: prs } } } }]));
try {
return spawnSync("bash", ["-c", `set -euo pipefail
heads="${prs.map((pr) => pr.number).join(" ")}"
DEFAULT_BRANCH=develop
gh() { echo "$*"; [[ "$*" != *pull_request_number=101* ]]; }
prs_file="${prsFile}"
${ghMock}
` + script], { encoding: "utf8" });
} finally {
rmSync(directory, { recursive: true, force: true });
}
}

test("one failed refresh dispatch does not prevent the remaining PRs from refreshing", () => {
const result = runRefreshLoop(`gh() {
[ "$1 $2" != "run list" ] || return 0
echo "$*"; [[ "$*" != *pull_request_number=101* ]]; }`,
[{ number: 101, headRefOid: "aaa" }, { number: 102, headRefOid: "bbb" }]);
assert.equal(result.status, 1, result.stderr);
assert.match(result.stdout, /pull_request_number=101/);
assert.match(result.stdout, /pull_request_number=102/);
});

test("refresh reruns the existing pull_request_target run in place instead of publishing a new check-run", () => {
const result = runRefreshLoop(`gh() {
case "$1 $2" in
"run list") [[ "$*" == *"--event pull_request_target"* && "$*" == *"--commit aaa"* ]] && echo 555; return 0 ;;
"run rerun") echo "rerun $3"; return 0 ;;
*) echo "$*"; return 0 ;;
esac }`,
[{ number: 101, headRefOid: "aaa" }]);
assert.equal(result.status, 0, result.stderr);
assert.match(result.stdout, /^rerun 555$/m);
assert.doesNotMatch(result.stdout, /workflow run/);
});

test("refresh falls back to dispatch when no pull_request_target run exists or the rerun is refused", () => {
const result = runRefreshLoop(`gh() {
case "$1 $2" in
"run list") [[ "$*" == *"--commit aaa"* ]] && echo 555; return 0 ;;
"run rerun") echo "rerun refused" >&2; return 1 ;;
*) echo "$*"; return 0 ;;
esac }`,
[{ number: 101, headRefOid: "aaa" }, { number: 102, headRefOid: "bbb" }, { number: 103 }]);
assert.equal(result.status, 0, result.stderr);
assert.match(result.stdout, /pull_request_number=101/);
assert.match(result.stdout, /pull_request_number=102/);
assert.match(result.stdout, /pull_request_number=103/);
});

test("PR HEAD guard runs default-branch policy once, so no second verdict is republished (#1977)", () => {
// pull_request 로 돌면 PR 사본 YAML 이 판정해 낡은 브랜치는 옛 정책이고 PR 이 가드를 고칠 수
// 있다. 종전 처방(inline 종료 → 최신 정책 dispatch → check-run 재게시)은 GITHUB_TOKEN 의
Expand Down
28 changes: 25 additions & 3 deletions .github/workflows/merge-order-guard.yml
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,10 @@ on:
issues:
types: [closed, reopened]
# GITHUB_TOKEN 이 만든 baseline/stack merge commit 은 pull_request_target 도 내지 않으므로,
# 그 SHA 와 선행 이슈가 닫힌 뒤의 PR HEAD 에는 dispatch 결과를 check-run 으로 붙인다.
# 그 SHA 에는 dispatch 결과를 check-run 으로 붙인다. 선행 이슈가 닫힌 뒤의 PR HEAD 는 이미
# pull_request_target run 이 있으니 refresh 가 그 run 을 재실행하고, run 이 없을 때만 여기로
# 내려온다 (#1983). PR HEAD 에 check-run 을 새로 만들면 임의 suite 에 얹혀 무관한 워크플로
# 이름 아래 «guard» 가 또 뜬다.
workflow_dispatch:
inputs:
pull_request_number:
Expand Down Expand Up @@ -304,7 +307,7 @@ jobs:
refresh:
if: github.event_name == 'issues'
permissions:
actions: write # 최신 default-branch 정책을 workflow_dispatch로 실행
actions: write # 기존 pull_request_target run 재실행, 없으면 workflow_dispatch
issues: read
pull-requests: read
runs-on: ubuntu-latest
Expand Down Expand Up @@ -335,7 +338,7 @@ jobs:
# 닫힐 때 같은 parser로 열린 PR을 다시 찾아야, 그 PR의 stale red check가 갱신된다.
prs_file="$RUNNER_TEMP/merge-order-open-prs.json"
gh api graphql --paginate --slurp \
-f query='query($owner:String!,$repo:String!,$endCursor:String){repository(owner:$owner,name:$repo){pullRequests(states:OPEN,first:100,after:$endCursor){nodes{number title body closingIssuesReferences(first:20){nodes{number repository{nameWithOwner}}}} pageInfo{hasNextPage endCursor}}}}' \
-f query='query($owner:String!,$repo:String!,$endCursor:String){repository(owner:$owner,name:$repo){pullRequests(states:OPEN,first:100,after:$endCursor){nodes{number headRefOid title body closingIssuesReferences(first:20){nodes{number repository{nameWithOwner}}}} pageInfo{hasNextPage endCursor}}}}' \
-f owner="$owner" -f repo="$repo" > "$prs_file"
heads=$(
BLOCKED="$blocked" PRS_FILE="$prs_file" node <<'NODE'
Expand Down Expand Up @@ -377,8 +380,27 @@ jobs:
exit 0
fi

# PR HEAD 에는 pull_request_target 으로 돈 guard run 이 이미 있다. 그 run 은 base 의
# YAML 과 default branch 의 스크립트로 돌았고, 재실행도 스크립트를 default branch 에서
# 다시 받으므로 같은 suite 안의 guard 가 제자리에서 갱신된다. dispatch 결과를 check-run
# 으로 새로 붙이면 임의 suite 에 얹혀 무관한 워크플로 이름 아래 guard 가 또 뜬다 (#1983).
# run 이 없거나(토큰 커밋) 재실행이 거절되면(진행 중·보존 기간 초과) dispatch 로 내려간다.
# HEAD SHA 는 위 GraphQL 결과에서 읽는다 — PR 마다 REST 를 또 부르면 한도를 태운다 (#1465).
dispatch_failed=0
for pr in $heads; do
sha=$(jq -r --argjson n "$pr" \
'[.[].data.repository.pullRequests.nodes[] | select(.number == $n) | .headRefOid][0] // empty' \
"$prs_file")
run_id=""
if [ -n "$sha" ]; then
run_id=$(gh run list --workflow merge-order-guard.yml --event pull_request_target \
--commit "$sha" --status completed --limit 1 --json databaseId \
--jq '.[0].databaseId // empty' || true)
fi
if [ -n "$run_id" ] && gh run rerun "$run_id"; then
echo "PR #$pr: pull_request_target run $run_id 재실행 — 같은 suite 안에서 guard 갱신"
continue
fi
if ! gh workflow run merge-order-guard.yml --ref "$DEFAULT_BRANCH" -f pull_request_number="$pr"; then
echo "::error::PR #$pr 최신 가드 dispatch 실패"
dispatch_failed=1
Expand Down
Loading