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
6 changes: 3 additions & 3 deletions .github/scripts/create-ci-config.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -127,9 +127,9 @@ test("keeps pull request validation secretless and release credentials isolated"

// pull_request_target은 default branch 정의에 쓰기 권한을 줄 수 있어 원칙적으로 금지한다.
// 예외는 PR code/artifact/cache를 실행하지 않고 default branch 정책과 API JSON만 읽는 좁은
// bridge 둘뿐이다 — 닫힌 스택 멤버 알림과 merge queue 방출 처리(#1892). 그 불변식을 둘 다에
// 함께 고정한다.
const narrowBridges = ["merge-queue-dequeue.yml", "stack-integrity-notify.yml"];
// bridge 셋뿐이다 — 닫힌 스택 멤버 알림, merge queue 방출 처리(#1892), 머지 순서 가드(#1977,
// PR 사본 YAML 로 판정하던 구멍과 재게시 guard 두 줄을 없앤다). 그 불변식을 셋 모두에 고정한다.
const narrowBridges = ["merge-order-guard.yml", "merge-queue-dequeue.yml", "stack-integrity-notify.yml"];
assert.deepEqual([...pullRequestTargetWorkflows].sort(), narrowBridges);
for (const bridgeName of narrowBridges) {
const bridge = workflows.get(bridgeName);
Expand Down
30 changes: 19 additions & 11 deletions .github/scripts/merge-order-guard-policy.test.mjs
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import assert from "node:assert/strict";
import { mkdtempSync, rmSync, writeFileSync } from "node:fs";
import { existsSync, mkdtempSync, rmSync, writeFileSync } from "node:fs";
import { readFile } from "node:fs/promises";
import { spawnSync } from "node:child_process";
import { tmpdir } from "node:os";
Expand Down Expand Up @@ -231,7 +231,7 @@ test("merge-group guard queries complete native-stack membership and fails close
});

test("ordinary PR guard stays non-stale while merge queue performs the live verdict", () => {
assert.match(guard, /^\s{4}types: \[opened, reopened, synchronize, edited\]$/m);
assert.match(guard, /^\s{2}pull_request_target:\n\s{4}types: \[opened, reopened, synchronize, edited\]$/m);
assert.doesNotMatch(guard, /statuses: write/);
const start = guard.indexOf('if [ "$EVENT_NAME" = "merge_group" ]');
const end = guard.indexOf(" # GitHub 가 close keyword", start);
Expand Down Expand Up @@ -434,12 +434,20 @@ gh() { echo "$*"; [[ "$*" != *pull_request_number=101* ]]; }
assert.match(result.stdout, /pull_request_number=102/);
});

test("old PR workflows trigger a trusted default-branch refresh without dispatch loops or PR checkout", async () => {
const refresh = await readFile(new URL("../workflows/refresh-merge-order-policy.yml", import.meta.url), "utf8");
assert.match(refresh, /workflow_run:\n\s+workflows: \[merge-order-guard\]\n\s+types: \[completed\]/);
assert.match(refresh, /github.event.workflow_run.event == 'pull_request'/);
assert.match(refresh, /^permissions: \{\}$/m);
assert.match(refresh, /select\(\.headRefOid == \$sha\)/);
assert.match(refresh, /gh workflow run merge-order-guard.yml --ref "\$DEFAULT_BRANCH"/);
assert.doesNotMatch(refresh, /uses:.*checkout|checks: write|contents: write|pull_request_target/);
});
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 의
// check-run 이 임의 suite 에 얹혀 무관한 워크플로 이름 아래 guard 가 두 줄로 떴다.
assert.doesNotMatch(guard, /^\s{2}pull_request:$/m);
assert.doesNotMatch(guard, /github\.event_name == 'pull_request'/);
assert.match(guard, /github\.event_name == 'pull_request_target'/);
assert.doesNotMatch(guard, /github\.event\.pull_request\.head\.(sha|ref)/);
assert.doesNotMatch(guard, /actions\/checkout[\s\S]*?ref: \$\{\{ github\.event\.pull_request/);
assert.match(guard, /ref: \$\{\{ github\.event\.repository\.default_branch \}\}/);
assert.ok(
!existsSync(new URL("../workflows/refresh-merge-order-policy.yml", import.meta.url)),
"inline 종료 뒤 최신 정책을 다시 dispatch 하는 refresh 워크플로는 pull_request_target 이 대체했다",
);
// dispatch 재게시는 이벤트가 나지 않는 SHA(토큰 커밋·선행 이슈 닫힘) 전용으로만 남는다.
assert.match(guard, /github\.event_name == 'workflow_dispatch' &&\n\s+github\.ref_name == github\.event\.repository\.default_branch/);
});
2 changes: 1 addition & 1 deletion .github/scripts/remote-dev-workflows-policy.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -406,5 +406,5 @@ test("required checks expose manual dispatch for token-authored commits", async

assert.match(validation, /^\s{2}workflow_dispatch:$/m);
assert.match(guard, /^\s{2}workflow_dispatch:\n\s{4}inputs:/m);
assert.match(guard, /github\.event_name == 'pull_request' \|\| github\.event_name == 'workflow_dispatch'/);
assert.match(guard, /github\.event_name == 'pull_request_target' \|\| github\.event_name == 'workflow_dispatch'/);
});
31 changes: 22 additions & 9 deletions .github/workflows/merge-order-guard.yml
Original file line number Diff line number Diff line change
Expand Up @@ -7,35 +7,47 @@
#
# 네이티브 스택의 닫힌 미병합 멤버는 merge queue synthetic SHA에서 live로 다시 검사한다.
# 중간 멤버가 닫힌 직후의 조치 안내는 stack-integrity-notify가 상위 PR에 남긴다.
#
# 판정은 항상 default branch 의 정책으로 한다 (#1977). pull_request 로 돌리면 PR 사본의
# 이 YAML 이 실행돼 낡은 브랜치는 옛 정책으로 판정하고, PR 이 가드를 고쳐 스스로 통과할 수
# 있다. 종전에는 그 구멍을 inline 종료 뒤 최신 정책을 dispatch 해 check-run 으로 재게시하는
# 두 번째 판정으로 메웠는데, GITHUB_TOKEN 이 만든 check-run 은 같은 SHA 의 임의 check
# suite 에 얹혀 무관한 워크플로 이름 아래 «guard» 가 두 줄로 떴다. pull_request_target 은
# base 의 YAML 과 스크립트로 돌면서 check 는 PR HEAD 에 붙으므로 첫 판정이 곧 최종 판정이다.
# PR 코드는 체크아웃하지 않고 권한은 read 뿐이다(stack-integrity-notify 와 같은 경계).
name: merge-order-guard

on:
pull_request:
pull_request_target:
types: [opened, reopened, synchronize, edited]
# merge queue 는 큐 ref 에서 guard context 를 다시 요구한다. 큐 ref
# (gh-readonly-queue/<base>/pr-<N>-<sha>) 에서 PR 번호를 파싱해 같은 검사를 돌린다.
merge_group:
types: [checks_requested]
issues:
types: [closed, reopened]
# GITHUB_TOKEN 이 만든 baseline/stack merge commit 에도 최신 SHA 기준 guard 를 붙인다.
# GITHUB_TOKEN 이 만든 baseline/stack merge commit 은 pull_request_target 도 내지 않으므로,
# 그 SHA 와 선행 이슈가 닫힌 뒤의 PR HEAD 에는 dispatch 결과를 check-run 으로 붙인다.
workflow_dispatch:
inputs:
pull_request_number:
description: "검사할 열린 PR 번호"
required: true
type: number

permissions:
contents: read
issues: read
pull-requests: read
# pull_request_target 은 base 권한으로 돌므로 워크플로 기본 권한은 비우고 job 마다 필요한
# 읽기·쓰기만 준다(create-ci-config 정책 테스트의 narrow bridge 불변식).
permissions: {}

jobs:
guard:
if: >-
github.event_name == 'pull_request' || github.event_name == 'workflow_dispatch' ||
github.event_name == 'pull_request_target' || github.event_name == 'workflow_dispatch' ||
github.event_name == 'merge_group'
permissions:
contents: read
issues: read
pull-requests: read
runs-on: ubuntu-latest
timeout-minutes: 5
outputs:
Expand All @@ -56,8 +68,9 @@ jobs:
printf 'sha=%s\n' "$(jq -r '.headRefOid' "$RUNNER_TEMP/guard-target.json")" >> "$GITHUB_OUTPUT"

# 게이트 스크립트는 **default branch 사본**으로 돌린다. PR 사본을 체크아웃하면 PR 이
# 자기를 검사하는 가드를 고쳐 통과시킬 수 있다. 필요한 건 .github/scripts 뿐이라
# sparse-checkout 으로 좁힌다.
# 자기를 검사하는 가드를 고쳐 통과시킬 수 있다. pull_request_target 의 기본 ref 도
# base 지만 dispatch·merge_group 경로와 같은 사본을 명시한다. 필요한 건 .github/scripts
# 뿐이라 sparse-checkout 으로 좁힌다.
- name: Clone gate scripts from the default branch
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
Expand Down
39 changes: 0 additions & 39 deletions .github/workflows/refresh-merge-order-policy.yml

This file was deleted.

Loading