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
11 changes: 11 additions & 0 deletions CI.md
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,17 @@ requests directly. Run the full tier on its branch first with
`gh pr merge --merge --admin`. Apart from an incident (below), nothing else
bypasses the queue.

`ci-mode` keeps such a pull request out of the queue, so nobody has to
remember it. A pull request must land as a merge commit when it contains a
merge whose first parent is on the base and whose second parent is not: the
merge brings in a line of history that a squash would drop. `ci-mode` fails
the merge group of such a pull request before any test job starts, and the
queue removes it. Merging the base into a branch, or two copies of a branch
into each other, is not such a merge. A pull request stacked on a merge pull
request that has not landed yet is refused too, until the merge lands. When
the check can't run, the merge group fails: queueing again costs one build,
and a squashed merge costs a repair merge.

## Urgent changes

An urgent fix jumps the queue: it goes to the front, is built next on top of
Expand Down
48 changes: 45 additions & 3 deletions actions/ci-mode/action.yml
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
name: CI mode
description: >-
Decides whether this run executes the full CI tier or only the fast one.
See CI.md in cocoindex-io/.github for the tiers. The calling job needs
`checks: read` and `contents: read`.
Decides whether this run executes the full CI tier or only the fast one,
and fails the merge group of a pull request that must land as a merge
commit. See CI.md in cocoindex-io/.github for the tiers. The calling job
needs `checks: read` and `contents: read`.
outputs:
full:
description: "'true' when this run executes the full tier, 'false' for the fast tier."
Expand All @@ -22,6 +23,9 @@ runs:
BASE: ${{ github.event.pull_request.stack.base.ref || github.event.pull_request.base.ref }}
STACK_POSITION: ${{ github.event.pull_request.stack.position }}
STACK_SIZE: ${{ github.event.pull_request.stack.size }}
SERVER_URL: ${{ github.server_url }}
GROUP_REF: ${{ github.event.merge_group.head_ref }}
GROUP_BASE: ${{ github.event.merge_group.base_sha }}
run: |
full=true
reason="a $EVENT run executes the full tier"
Expand Down Expand Up @@ -50,6 +54,44 @@ runs:
else
echo "::warning::Could not read the checks of $SHA, so this run executes the full tier."
fi
elif [ "$EVENT" = merge_group ]; then
# The queue squashes, and a squash keeps only a merge's first
# parent. A pull request that merges another line of history onto
# its base, such as an upstream merge, must land as a merge commit
# (CI.md, Merge commits), so its merge group fails here, before any
# test job starts, and the queue removes it. When the check can't
# run, the merge group fails too: queueing again costs one build,
# and a squashed merge costs a repair merge.
pr=${GROUP_REF##*/pr-}
pr=${pr%%-*}
if ! [[ $pr =~ ^[0-9]+$ ]]; then
echo "::error::Could not find the pull request number in the merge group ref $GROUP_REF."
exit 1
fi
# Commits only: the check reads no trees or files.
history="$RUNNER_TEMP/ci-mode-history.git"
git init --quiet --bare "$history"
git --git-dir="$history" remote add origin "$SERVER_URL/$REPO"
auth="AUTHORIZATION: basic $(printf 'x-access-token:%s' "$GH_TOKEN" | base64 | tr -d '\n')"
if ! git --git-dir="$history" -c http.extraheader="$auth" fetch --quiet --no-tags \
--filter=tree:0 origin "$GROUP_BASE" "+refs/pull/$pr/head:refs/pr"; then
echo "::error::Could not fetch the history of #$pr, so its merge group fails. Queue it again."
exit 1
fi
# A merge whose first parent is on the base and whose second is not
# brings in a line of history that only a merge commit keeps.
if ! merges=$(git --git-dir="$history" rev-list --merges --parents "$GROUP_BASE..refs/pr"); then
echo "::error::Could not list the merges of #$pr, so its merge group fails. Queue it again."
exit 1
fi
while read -r merge first second _; do
[ -n "$merge" ] || continue
if git --git-dir="$history" merge-base --is-ancestor "$first" "$GROUP_BASE" \
&& ! git --git-dir="$history" merge-base --is-ancestor "$second" "$GROUP_BASE"; then
echo "::error::#$pr merges ${second:0:12} onto its base in ${merge:0:12}, and the queue's squash would drop that parent. Land #$pr as a merge commit around the queue, after a full run on its branch passes (CI.md, Merge commits)."
exit 1
fi
done <<<"$merges"
fi
echo "full=$full" >> "$GITHUB_OUTPUT"
tier=fast
Expand Down
12 changes: 8 additions & 4 deletions scripts/apply_rulesets.py
Original file line number Diff line number Diff line change
Expand Up @@ -146,12 +146,16 @@ def main() -> int:
repos = opted_in(gh_list(f"orgs/{args.org}/properties/values"), prop["property_name"])
print(f"repositories with {prop['property_name']}={OPT_IN_VALUE}: {', '.join(repos) or 'none'}")
settings = json.loads(SETTINGS_FILE.read_text())
# A merge-queue rule may only name a merge method the repository allows,
# so methods are enabled before the rulesets and disabled after them.
enabling = {key: value for key, value in settings.items() if key.startswith("allow_") and value is True}
for repo in repos:
scope = f"repos/{args.org}/{repo}"
if not args.check:
ok = sync_settings(scope, enabling, check=False) and ok
for path in sorted((ROOT / "repo").glob("*.json")):
ok = sync_ruleset(f"repos/{args.org}/{repo}", json.loads(path.read_text()), args.check) and ok
# After the rulesets: the queue's merge method has to stay allowed
# while the ruleset still names the old one.
ok = sync_settings(f"repos/{args.org}/{repo}", settings, args.check) and ok
ok = sync_ruleset(scope, json.loads(path.read_text()), args.check) and ok
ok = sync_settings(scope, settings, args.check) and ok
return 0 if ok else 1


Expand Down
Loading