Skip to documentation

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 main publish 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.yml handles preview generation for pull requests through a legacy consumer wrapper;
  • .github/workflows/wiki-maintenance.yml handles 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:

  1. runs dev-tools wiki -- --target=.github/wiki through the shared workflow runtime bootstrap;
  2. commits the generated wiki content to the wiki branch pr-123 ;
  3. updates the parent repository submodule pointer at .github/wiki ;
  4. 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 main revision;
  • preview branches such as pr-123 are 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 main flow.
  • 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.

Search results