zudo-text

検索したい単語を入力

いつでも検索バーを開ける

CI-Skip Markers and Merge Hygiene

CI-skip markers are useful in the PR-first workflow, but a marker on the wrong commit can suppress every workflow triggered by a push, including lightweight CI and mock Preview Deploy. The five real-service production deploys use the separate verified-candidate gate described in the CI Verification Policy.

The invariant

No commit that becomes the push head on main may contain a CI-skip instruction.

The rule is about the push head, not every commit that lands on main. GitHub evaluates the message of the commit at the head of the push when it decides whether to skip workflows. A regular merge can therefore retain a marked start commit as an ancestor in main's history without suppressing the workflows for the merge commit itself. That distinction is why the guard never scans branch commit subjects: the PR-first workflow's empty start commit is expected to carry a marker. Under the repository's PR_TITLE + BLANK settings, the PR title is the only PR text that can become the squash commit message, so the pull-request guard scans that title and checks the settings for drift before merge.

The guard recognizes all six GitHub skip instructions, case-insensitively:

  • [skip ci], [ci skip], [no ci], [skip actions], and [actions skip]

  • The skip-checks: true or skip-checks:true trailer

Why the marker exists

The PR-first workflow opens a draft PR with an empty-diff start commit. Its [skip ci] subject suppresses the pull_request: opened run that would have no changes to test. The marker is intentional on that branch commit and remains harmless as long as it does not become the message of the commit pushed to main.

Before the repository setting was corrected, squash merging was the propagation channel. squash_merge_commit_message: COMMIT_MESSAGES tells GitHub to concatenate the branch commit subjects into the squash commit body. GitHub's skip detection reads the whole commit message, including the body, so the start commit's marker reached main and suppressed every push-triggered workflow. The safe repository settings are PR_TITLE for squash_merge_commit_title and BLANK for squash_merge_commit_message, which make the squash commit message exactly the PR title.

Evidence from the incident

The failure was silent: the marked squash commits produced no workflow runs at all.

CommitMerge styleMarkerWorkflow runs
a81aa72a (PR #5360, touched tauri-app/**)squashyeszero
49e613a0 (PR #5011)squashyeszero
Current pre-cutover main HEAD (control at incident time)regular mergenoCI + Preview Deploy + Deploy Web Editor, all green; historical evidence from before gated promotion

At incident time, deploy-web-editor.yml listened for push: main changes under tauri-app/**, while preview-deploy.yml had no path filter on its push trigger. The first marked squash therefore also skipped a production deploy, even though the merge itself looked successful. Under the v1 policy, production promotion is triggered only by verified Full Verification evidence; the marker can still suppress lightweight post-merge CI and Preview Deploy.

Merge-mode inventory

ModeMarker reaches the push head?Notes
Regular merge (house default)NoThe merge commit message never contains branch commit subjects.
Squash, after this fixNoThe message is exactly the PR title.
Squash, before this fixYesThis is the observed bug.
Rebase mergeNoGitHub drops commits that were empty to begin with.
gh pr merge --squash --body "…"Yes — residualAn explicit message overrides the repository settings and is not visible to the title guard. The global /prc convention is the merge-enforcement path; direct merge can bypass it because server-side required checks are unavailable.
Merge queueN/AUnavailable on this plan. If it becomes available, the guard must also subscribe to merge_group.

Verify that a merge ran its workflows

Workflow creation can lag behind the merge. Poll the main branch and match the merged commit's headSha on the client side:

merge_sha="<merge-sha>"
gh run list --branch main \
  --json headSha,workflowName,status,conclusion \
  | jq --arg sha "$merge_sha" '[.[] | select(.headSha == $sha)]'

Repeat the query with a bounded retry until the expected workflows appear and reach a terminal status. Check workflowName, status, and conclusion, not just whether one run exists. For a merge touching .github/** and scripts/**, the expected post-merge set includes at least lightweight CI and Preview Deploy. Changes to static docs/manual content may also run their build-verified site workflows. The five real-service deploy workflows follow Full Verification and deploy only the exact candidate accepted by the promotion gate; a normal main push does not itself make them run.

Do not use gh run list --commit <sha> for this check. It has been observed to omit queued runs, and “not created yet” is not the same as “suppressed”.

Residual risks

Two paths cannot be closed by this repository alone:

  1. gh pr merge --squash --body "…" supplies an explicit squash message and overrides the repository settings. That later CLI override is invisible to the pull-request title guard, and this repository has no required policy capable of blocking it.

  2. This repository is private on a free organization plan. Branch protection and required status checks are unavailable — the protection and ruleset API returns 403. The global /prc convention is the merge-enforcement path; if rulesets become available, only the always-reporting checks job can be required. The repository's PR_TITLE + BLANK squash settings also keep marked branch messages from becoming the push-head message.