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
30 changes: 30 additions & 0 deletions .github/actions/php/setup-composer/composer-without-plugins.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
#!/usr/bin/env php
<?php

declare(strict_types=1);

/**
* Keeps Composer subprocesses plugin-free in dependency-health CI jobs.
*
* @license https://opensource.org/licenses/MIT MIT License
*/

$composerBinary = getenv('FAST_FORWARD_CI_COMPOSER_BINARY');

if (! is_string($composerBinary) || '' === $composerBinary || realpath($composerBinary) === realpath(__FILE__)) {
fwrite(STDERR, "A separate Composer binary is required for plugin-free CI checks.\n");
exit(1);
}

$process = proc_open(
[$composerBinary, '--no-plugins', ...array_slice($argv, 1)],
[STDIN, STDOUT, STDERR],
$pipes,
);

if (! is_resource($process)) {
exit(1);
}

$exitCode = proc_close($process);
exit(-1 === $exitCode ? 1 : $exitCode);
2 changes: 1 addition & 1 deletion .github/wiki
Submodule wiki updated from eed2e6 to df4904
233 changes: 198 additions & 35 deletions .github/workflows/tests.yml

Large diffs are not rendered by default.

6 changes: 5 additions & 1 deletion AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -161,7 +161,10 @@ Release and publishing behavior is driven primarily through
`wiki-preview.yml`, `wiki-maintenance.yml`, `auto-assign.yml`, and
`label-sync.yml`, with reusable local workflow building blocks grouped under
`.github/actions/` and packaged consumer workflow wrappers living under
`resources/github-actions/`. Packaged skills live under `.agents/skills/`
`resources/github-actions/`. Contract-specific templates in
`resources/github-actions-optional/` require explicit adoption and are not
installed by `dev-tools:sync`; read each companion guide before copying one
into a consumer repository. Packaged skills live under `.agents/skills/`
alongside mirrored project-agent prompts under `.agents/agents/`.

**Package Details:**
Expand Down Expand Up @@ -198,6 +201,7 @@ composer dev-tools
- `.github/workflows/`: CI and release automation truth, especially `tests.yml`, `reports.yml`, `review.yml`, `wiki.yml`, `wiki-preview.yml`, `wiki-maintenance.yml`, `changelog.yml`, `auto-assign.yml`, and `label-sync.yml`
- `.github/actions/`: shared workflow building blocks for `php`, `project-board`, `github-pages`, `review`, `summary`, `wiki`, `changelog`, and `label-sync`
- `resources/github-actions/`: consumer-facing workflow wrappers synchronized by `dev-tools:sync`
- [resources/github-actions-optional/test-statuses.md](resources/github-actions-optional/test-statuses.md): explicit opt-in guide for Dependabot status aliases; verify source workflow identity, PHP matrix and protection contexts before adopting the checkout-free template
- `.github/pull_request_template.md`: expected PR structure and reviewer checklist
- `src/Sync/`: shared packaged-directory synchronization primitives used by `skills` and `agents`
- `.agents/skills/`: packaged procedural skills shipped to consumer repositories
Expand Down
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

### Fixed

- Keep required per-version statuses current across normal and Dependabot reruns through checkout-free lifecycle publishers using verified GitHub run and job metadata.
- Keep Composer audit and nested dependency-health checks plugin-free after CI installs without plugins, so consumer allowlists do not block vulnerability or dependency analysis.
- Isolate completed per-version status publication from repository-controlled test jobs, preserve access to private GitHub job metadata, and disable persisted checkout credentials in the reusable test workflow.
- Restore tests, reports, and dependency checks after ECS and Rector API changes, and replace the abandoned rector/jack dependency checker with Rector Swiss Knife.

## [1.25.6] - 2026-05-22
Expand Down
73 changes: 63 additions & 10 deletions docs/advanced/branch-protection-and-bot-commits.rst
Original file line number Diff line number Diff line change
Expand Up @@ -109,12 +109,62 @@ 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 first publishes pending commit statuses for the resolved PHP matrix and
then lets each matrix job publish its own final status. The status contexts use
the same required-check names, such as ``Run Tests (8.3)``, ``Run Tests (8.4)``,
and ``Run Tests (8.5)``. Test workflow concurrency cancels older in-progress
runs for the same pull request so the newest commit owns the required check
contexts.
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 :doc:`../usage/github-actions`. 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 predictable-conflict workflow MAY also refresh pull request branches when
the only conflicts are ``.github/wiki`` pointer drift and/or ``CHANGELOG.md``
Expand Down Expand Up @@ -155,10 +205,13 @@ 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.

``tests.yml`` needs ``contents: read`` because it checks out code, installs
dependencies, and runs PHPUnit. It also declares ``statuses: write`` so
workflow-dispatched test runs can mirror required matrix contexts onto
bot-authored wiki pointer commits.
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
Expand Down
105 changes: 104 additions & 1 deletion docs/usage/github-actions.rst
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ FastForward DevTools provides a set of reusable GitHub Actions workflows that au
Workflow Layers
---------------

The automation model now has three layers:
The automation model separates shared implementations from consumer triggers:

* **Local composite and JavaScript actions** in ``.github/actions/`` inside
this repository. These contain the reusable implementation details for PHP
Expand Down Expand Up @@ -45,6 +45,13 @@ Example of an inherited workflow:

This approach ensures that all libraries in the ecosystem benefit from infrastructure updates without requiring manual changes to every repository.

First-party wrappers intentionally follow the reviewed ``@main`` workflow
contract so centrally deployed fixes reach consumers. This is an explicit
shared-infrastructure update policy, distinct from pinning third-party
actions. A workflow SHA alone would not freeze the existing action-source
checkout, which also follows DevTools ``main``; a fully immutable migration
would need to version both surfaces together.

The packaged wrappers currently include:

* ``tests.yml``
Expand All @@ -57,6 +64,10 @@ The packaged wrappers currently include:
* ``auto-assign.yml``
* ``label-sync.yml``

The optional ``resources/github-actions-optional/test-statuses.yml`` template
is kept outside this synchronized directory. It requires an explicit copy and
repository-specific verification; ``dev-tools:sync`` does not install it.

For the protected-branch-safe preview and publish model, see
:doc:`../advanced/branch-protection-and-bot-commits`.

Expand All @@ -65,6 +76,98 @@ local Composer dependency. The shared ``setup-composer`` action prefers the
consumer ``vendor/bin/dev-tools`` when it exists and otherwise exposes a
``dev-tools`` wrapper backed by the checked-out ``.dev-tools-actions`` source.

Dependabot Required Test Statuses
---------------------------------

The optional standalone ``test-statuses.yml`` workflow supplies commit-status
aliases for consumers whose branch protection requires unqualified names such
as ``Run Tests (8.3)``. The current first-party rollout targets twelve audited
consumer repositories with PHP 8.3, 8.4 and 8.5; this template does not infer an
arbitrary consumer's matrix or protection policy. Consumers protecting the
native qualified GitHub Actions checks do not need these aliases.

To opt in, copy
``resources/github-actions-optional/test-statuses.yml`` from a reviewed
DevTools checkout or installed package into the consumer repository as
``.github/workflows/test-statuses.yml``. Review these contracts before merging
the copied file:

* The ``workflow_run.workflows`` name and the PHP metadata check must match the
source workflow name, currently ``Fast Forward Test Suite``.
* The source workflow path must match the API path check, currently
``.github/workflows/tests.yml``.
* The caller job name must match all job-name checks. The template expects
``tests / Run Tests (<version>)`` and control jobs prefixed with ``tests /``.
* ``EXPECTED_PHP_VERSIONS`` must list every expected version as JSON strings,
currently ``["8.3","8.4","8.5"]``. Align those versions and the resulting
``Run Tests (<version>)`` contexts with branch protection.

A different caller prefix or matrix needs a reviewed adjustment to the copied
template. An additional observed test version, missing expected version,
ambiguous job or invalid attempt makes final publication fail closed; the
publisher must not silently mirror only a subset of the matrix.

After the file reaches the default branch, same-repository Dependabot ``push``
runs trigger it through ``workflow_run`` requested, in-progress and completed
events. Active attempts receive pending statuses, including reruns; terminal
attempts receive the actual per-version outcomes. A delayed start event reads
the current Run API state instead of overwriting completed results with pending.

The publisher does not check out source, install dependencies, retrieve
artifacts or caches, or run caller code. Its own job alone receives
``actions: read`` and ``statuses: write``. It verifies the source
repository, SHA, workflow ID/path/name, actor, event and attempt through
the Run API and rechecks that snapshot before writing. It validates the
configured matrix before publishing terminal results. Partial retries retain
completed results for unaffected versions and select the newest attempt for
rerun versions. A full rerun that fails or is cancelled before the matrix runs
must not reuse earlier successes: verified failed control jobs or a failed
source attempt produce failure statuses for blocked versions. Stale events and
superseded runs stop publication.

The target is the source push's verified ``head_sha``, never the
publisher's ``github.sha``, which points to the default branch.
Pull-request runs are deliberately excluded because they can test a
different merge commit; Dependabot's push run supplies this bridge.
Fork pull requests are outside this workflow's scope.

This bridge becomes active only after its file is merged into the default
branch; merely adding the template to a pull request does not activate its
lifecycle events. Verify a real Dependabot push and its required contexts after
deployment. API reads and status writes are separate operations: scheduling,
API availability and a state transition between requests prevent an atomic or
instantaneous update of every context.

Ordinary Required Test Statuses
-------------------------------

For ordinary opt-in runs, the reusable test workflow has a separate
checkout-free pending publisher after PHP version resolution. It verifies the
current run attempt and complete matrix, then marks all configured contexts
pending before the test matrix can start. Opt-out and Dependabot skips still
permit tests to run; Dependabot uses the optional lifecycle bridge described
above when its protection policy needs aliases.

If pending publication fails, the matrix is blocked and the final publisher
does not run. The final publisher requires successful pending publication and
an actual success or failure matrix outcome; skipped or cancelled matrices
cannot cause it to republish older successful jobs. Final results come from
GitHub job metadata for that exact run, selecting the newest attempt for each
version and requiring it to be completed. A failed-only retry can retain results from
unaffected versions, while incomplete or ambiguous newest results stop
publication.

Full reruns reexecute the ordered pending publisher. Rerunning only an
individual successful job may retain its successful ancestors and therefore
does not guarantee another pending publication. There is also a scheduling and
PHP-resolution gap before the pending job runs. A failed API read before its
first POST leaves existing statuses unchanged, and later API failures may leave
only some contexts updated. Commit-status mirroring does not provide an atomic
replacement of earlier results. Prefer native qualified checks when migrating
a repository's protection policy. See
:doc:`../advanced/branch-protection-and-bot-commits` for the permission ceiling
and bot-authored commit flow.

Fast Forward Reports
--------------------

Expand Down
Loading
Loading