Branch Protection and Bot Commits
Fast Forward repositories keep generated documentation out of protected
branches until the related pull request is merged. This keeps main
stable,
lets maintainers review generated output before it becomes canonical, and
avoids requiring automation to bypass branch protection for normal preview
updates.
The model has two publishing lanes:
- pull requests provide report previews and read-only Wiki artifacts;
- merges into
mainpublish the final wiki and reports.
The active wiki.yml
calls wiki-direct.yml
. Its generation job has
read-only permissions on pull requests; after merge, a separate job publishes
from main
to the Wiki repository. It does not create preview branches,
commit repository pointers, or require Wiki-related bot commits on PR branches.
Legacy consumers that have not migrated still use separate reusable workflows:
.github/workflows/wiki-preview.ymlhandles preview generation for pull requests through a legacy consumer wrapper;.github/workflows/wiki-maintenance.ymlhandles post-merge promotion, closed-pull-request cleanup, and scheduled orphan cleanup and is triggered through a legacy consumer maintenance wrapper. The producer maintenance entry and current packaged consumer maintenance stub are manual-only notices.
Wiki Preview Lifecycle
This section describes legacy preview/submodule consumers only. The direct workflow uses a generated artifact on pull requests instead.
The legacy wiki workflow writes generated Markdown to the GitHub wiki repository, but
it does not publish pull request content directly to the wiki master
branch.
Instead, each pull request receives a dedicated wiki branch.
For pull request 123
the workflow:
- runs
dev-tools wiki -- --target=.github/wikithrough the shared workflow runtime bootstrap; - commits the generated wiki content to the wiki branch
pr-123; - updates the parent repository submodule pointer at
.github/wiki; - commits that pointer update back to the pull request branch.
The parent repository commit is important because reviewers can see exactly
which generated wiki revision belongs to the pull request. The bot commit SHOULD
stay on the pull request branch because protected main
branches usually
reject direct commits.
After the pull request is merged into main
, the maintenance workflow copies
the content from the wiki preview branch, such as pr-123
, to the wiki
master
branch. That makes the reviewed wiki content live only after the
source code merge is complete. The maintenance lane validates that remote
master
points to the expected preview commit before it deletes the
pr-123
branch because the preview branch is the last rollback source for
that generated content.
If the pull request is closed without merge, the maintenance workflow deletes
the matching wiki preview branch without promoting it to master
. A
scheduled cleanup lane also scans existing pr-<number>
wiki branches and
removes branches whose pull requests are already closed.
Reports Preview Lifecycle
Reports use the same review-before-publish idea, but the output is served from GitHub Pages instead of the wiki repository.
On pull requests, the reports workflow:
- generates docs, coverage, and report assets from the pull request code;
- publishes them under
previews/pr-<number>/in the Pages branch; - comments on the pull request with preview links when possible;
- cancels older in-progress preview runs when a newer push updates the same pull request;
- keeps workflow artifacts available as a fallback when Pages publishing or the comment update is unavailable.
On main
, the reports workflow publishes the final site at the root of the
Pages branch. Pull request previews use PR-specific directories, so multiple
open pull requests can have independent previews without overwriting each other.
When a pull request is closed, the workflow SHOULD remove its preview directory.
This prevents stale documentation and coverage reports from looking like active
review artifacts. A scheduled cleanup also scans previews/pr-<number>/
directories and removes preview directories whose pull requests are already
closed.
Branch Protection Interactions
Protected branches usually block direct pushes, require status checks, and may require signed commits or linear history. The preview workflows are designed to respect those rules:
- bot commits update the pull request branch, not
main; - final wiki publication runs after the merge into
main; - final reports publication runs from the already-accepted
mainrevision; - preview branches such as
pr-123are temporary review targets.
The pointer and preview-branch guidance below applies only to legacy Wiki consumers. Direct Wiki publication does not push to pull request branches.
If a repository restricts bot pushes to pull request branches, maintainers
should either allow the workflow token to update PR branches or require authors
to refresh generated pointers manually. The preferred path is to allow bot
updates on PR branches while keeping main
protected.
Required test checks must still report for workflow-managed pointer commits.
The tests workflow therefore triggers on every pull request update without
top-level path filters. This ensures GitHub always creates the required
Run Tests
matrix checks for ordinary pull request updates.
Workflow-managed .github/wiki
pointer commits need one extra step. GitHub
does not start another pull_request
or push
workflow run for commits
pushed with the built-in workflow token. After the wiki preview workflow commits
a parent-repository pointer update, it explicitly dispatches tests.yml
for
the pull request head branch so the newest bot-authored commit receives the
required Run Tests
matrix checks. Because manually dispatched workflow check
runs are not always treated as pull-request required checks, that dispatched
test run enables two separate status publishers. After PHP version resolution,
the checkout-free pending publisher validates the matrix and the current run
attempt, then marks every configured required context pending. The test matrix
waits for this job. Disabling mirroring or skipping publication for Dependabot
still permits tests to run; a failed pending publisher blocks the matrix.
The final publisher requires successful pending publication and a matrix that
actually finished with success or failure. It does not run for skipped or
cancelled matrices, so a publication failure cannot make it select earlier
successful jobs. Neither publisher checks out repository files or executes
consumer code. The final publisher reads GitHub job attempts for that exact
run, selects the newest attempt for each PHP version, and requires that result
to be completed before publishing it under its required-check name, such as
Run Tests (8.3)
, Run Tests (8.4)
, and Run Tests (8.5)
.
When only failed jobs are rerun, successful versions can remain in an earlier attempt. The publisher retains those completed results and replaces only the versions that have a newer attempt. An incomplete or ambiguous newest attempt fails publication rather than falling back to an older successful result.
Full reruns execute the pending phase again before the new matrix starts. A missing, ambiguous, incomplete or wrong-run result makes final publication fail before posting terminal statuses. When investigating a blocked check, inspect both isolated publishers and the corresponding matrix job. Tests never publish statuses from their own code-executing jobs. Test workflow concurrency cancels older in-progress runs for the same pull request.
Pending publication is not instantaneous or atomic. Earlier statuses may remain visible while the run is scheduled or PHP versions are resolved. An API failure before the first pending POST leaves old statuses unchanged; failure between POSTs can leave only some contexts updated. Rerunning an individual successful test job can retain its successful pending-job ancestor instead of executing that ancestor again. Prefer the exact native qualified GitHub Actions checks when deliberately migrating a consumer's branch protection policy.
Jobs that execute checked-out code have read-only contents access and no status
write permission; all checkouts disable credential persistence. Only the
isolated pending and final publishers can write commit statuses. Both are
skipped for Dependabot. Consumers requiring unqualified aliases for Dependabot
pushes can explicitly install the optional default-branch lifecycle bridge
described in GitHub Actions Workflows. It is outside normal
dev-tools:sync
workflow installation and requires verification of the
workflow name/path, caller job prefix, configured PHP versions and protection
contexts. Its metadata-backed pending and terminal phases reject stale
attempts; a verified failed or cancelled attempt that did not reach the matrix
must not publish earlier successful results.
Status mirroring is opt-in and defaults to disabled. The isolated publishers
have actions: read
to access run and job metadata in public or private
repositories and statuses: write
to submit pending and completed results.
Reusable-workflow callers
must include both permissions in their maximum permission set, even when
mirroring is disabled: GitHub checks the reusable workflow's permission ceiling
before evaluating its individual job conditions. The packaged test wrapper
declares this maximum; jobs that execute consumer code explicitly reduce both
Actions and status permissions to none
.
The external Changelog workflow owns changelog/version
and verifies its signed
transaction evidence. Branch names alone do not bypass checks. Its exact-head
commit status is Fast Forward Changelog
. Confirm the status and all independent
repository test gates on a pilot generated PR before changing protection.
The official caller uses GITHUB_TOKEN
and the external lifecycle defaults.
Account for GitHub's restrictions on subsequent CI runs. Do not treat the
Changelog status as evidence that independent consumer tests ran; inspect their
results on the exact version PR head before approval.
At a high level, the workflows need permission to read repository contents, write generated preview commits, update pull request comments, and publish Pages content. Keep those permissions scoped to the workflow jobs that actually need them.
Workflow Permission Scope
The reusable workflows default to read-only repository access and grant write permissions at the job level when generated content must be pushed or pull requests must be updated.
The code-executing jobs in tests.yml
need only contents: read
to check
out code, install dependencies and run PHPUnit. Its separate pending and final
publishers have actions: read
and statuses: write
so workflow-dispatched
test runs can mirror each required matrix context onto bot-authored wiki pointer
commits. The publishers do not check out code or run consumer scripts. The
optional Dependabot lifecycle bridge grants the same two scopes only to its
isolated status job; it does not add write permission to consumer tests.
reports.yml
keeps contents: write
on jobs that publish or clean
gh-pages
content. The pull request preview comment runs as a separate job
with pull-requests: write
because it posts or updates the sticky preview
comment. Scheduled preview cleanup uses pull-requests: read
so it can
distinguish open pull requests from closed or merged ones before deleting
previews/pr-<number>
directories.
The active wiki.yml
caller permits contents: write
so the separate
wiki-direct.yml
publication job can publish from main
. Its generation
job explicitly keeps contents: read
and checkouts do not persist
credentials. Pull requests never run the publication job.
The legacy wiki-preview.yml
keeps
contents: write
for wiki preview commits and parent-repository submodule
pointer updates, actions: write
to dispatch tests.yml
after bot-authored
pointer commits, and pull-requests: read
to inspect pull request metadata
safely.
The legacy reusable wiki-maintenance.yml
keeps
contents: write
for wiki publication and cleanup tasks, and
pull-requests: read
on jobs that need to distinguish merged, closed, and
open pull requests before publishing or deleting preview branches.
The label synchronization workflow declares issues: read
to copy labels from
the linked issue and pull-requests: write
to apply those labels to the pull
request. The project-board automation workflow keeps issues: write
and
pull-requests: write
because assignment and board-state changes are write
operations on both event types, and it also declares
repository-projects: write
so linked issues and pull requests can be added
to the configured GitHub Project and moved through the delivery pipeline.
To enable reusable project automation in consumer repositories, pass
project
through the thin workflow_call
wrapper or configure the
repository variable PROJECT
so the workflow can resolve the target GitHub
Project without hardcoding repository-specific board identifiers into the
packaged workflow itself. Consumer wrappers that call reusable project-board
workflows MUST also grant repository-projects: write
at the caller level;
otherwise GitHub rejects the workflow before its automation starts. The workflow derives the owner from
github.repository_owner
and uses the built-in workflow token for the actual
project mutations. Inside php-fast-forward
repositories, the reusable
workflow MAY fall back to the first organization Project V2 when no explicit
project number is provided. The reusable project-board helpers now live under
.github/actions/project-board/*
, which keeps project-state transitions
shared across issue, pull-request and review workflows.
The external changelog lifecycle requires contents: write
,
pull-requests: write
, issues: read
and statuses: write
at its caller.
The official caller contains only the external Changelog job.
Resolving .github/wiki
Pointer Conflicts
This section applies only to legacy submodule consumers. Direct Wiki publication does not create repository pointers.
Submodule pointer conflicts happen when main
and the pull request point to
different generated wiki commits. Legacy consumers should migrate to direct
Wiki publication or resolve these conflicts manually. For a manual resolution, inspect
both pointers, rebase the pull request and select the verified preview commit
that belongs to it.
For pull request 123
:
git fetch origin main
git rebase origin/main
git -C .github/wiki fetch origin master pr-123
git -C .github/wiki switch --detach origin/pr-123
git add .github/wiki
git rebase --continue
git push --force-with-lease
If the conflict appears after the preview job has already produced a newer bot
commit, prefer the latest pr-123
wiki commit. If the wiki branch no longer
exists because the pull request was closed or merged, rerun the wiki workflow or
regenerate the wiki locally before updating the pointer.
Operational Checklist
- Review report preview links and generated Wiki artifacts before merging documentation-heavy changes.
- Merge only through the protected
mainflow. - Let the direct Wiki publication job publish from
main. - For legacy consumers, keep generated pointers with their source changes and use the separate preview and maintenance workflows for publication and cleanup.
- Let closed pull requests and scheduled cleanup remove report preview directories.
See Consumer Automation for how reusable workflows and consumer stubs fit into this model, and GitHub Actions Workflows for the workflow summary.