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
20 changes: 20 additions & 0 deletions .github/dependabot.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
version: 2

updates:
# Composer bumps arrive as their own pull request, so a dependency moving is
# a release of its own rather than something folded into unrelated work.
- package-ecosystem: composer
directory: /
schedule:
interval: weekly
commit-message:
prefix: fix
prefix-development: chore
include: scope

- package-ecosystem: github-actions
directory: /
schedule:
interval: weekly
commit-message:
prefix: ci
35 changes: 35 additions & 0 deletions .github/workflows/pr-title.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
name: PR Title

# Merges are squashed with the pull request title as the commit subject, so the
# title is what semantic-release will read. Checking it here is the difference
# between a release that happens and one that silently does not.
on:
pull_request_target:
types: [opened, edited, synchronize, reopened]

permissions:
pull-requests: read

jobs:
lint:
runs-on: ubuntu-latest
steps:
- uses: amannn/action-semantic-pull-request@v6
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
types: |
feat
fix
perf
refactor
docs
test
build
ci
chore
revert
requireScope: false
subjectPattern: ^(?![A-Z]).+[^.]$
subjectPatternError: |
The subject "{subject}" should start lower case and not end with a period.
42 changes: 42 additions & 0 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
name: Release

# Every merge to main is a candidate. semantic-release reads the commit subjects
# since the last tag and decides whether there is anything to cut — a docs or
# chore change releases nothing at all.
on:
push:
branches: [main]

# The tag, the GitHub release, and the CHANGELOG commit all need writing.
permissions:
contents: write
issues: write
pull-requests: write

concurrency:
group: release
cancel-in-progress: false

jobs:
release:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
with:
# Tags are how semantic-release knows the current version, so the
# whole history has to be there.
fetch-depth: 0
persist-credentials: false

- uses: actions/setup-node@v7
with:
node-version: 22

- uses: cycjimmy/semantic-release-action@v6
with:
extra_plugins: |
@semantic-release/changelog@6
@semantic-release/git@10
conventional-changelog-conventionalcommits@8
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
23 changes: 23 additions & 0 deletions .github/workflows/static-analysis.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
name: Static Analysis

on:
pull_request:
push:
branches: [main]

jobs:
phpstan:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7

- name: Install PHP
uses: shivammathur/setup-php@v2
with:
php-version: 8.5

- name: Install packages
run: composer install --prefer-dist --no-progress

- name: Run PHPStan
run: composer analyse
38 changes: 38 additions & 0 deletions .github/workflows/tests.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
name: Tests

on:
pull_request:
push:
branches: [main]

jobs:
tests:
name: PHP ${{ matrix.php }}
runs-on: ubuntu-latest

strategy:
fail-fast: false
matrix:
php: ['8.5']
experimental: [false]
include:
# 8.6 is still in development and parts of the Tempest dependency tree
# do not install on it yet, so it reports without blocking a merge.
- php: '8.6'
experimental: true

continue-on-error: ${{ matrix.experimental }}

steps:
- uses: actions/checkout@v7

- name: Install PHP ${{ matrix.php }}
uses: shivammathur/setup-php@v2
with:
php-version: ${{ matrix.php }}

- name: Install packages
run: composer install --prefer-dist --no-progress

- name: Run tests
run: composer test
32 changes: 32 additions & 0 deletions .releaserc.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
{
"branches": ["main"],
"tagFormat": "v${version}",
"plugins": [
[
"@semantic-release/commit-analyzer",
{
"preset": "conventionalcommits"
}
],
[
"@semantic-release/release-notes-generator",
{
"preset": "conventionalcommits"
}
],
[
"@semantic-release/changelog",
{
"changelogFile": "CHANGELOG.md"
}
],
"@semantic-release/github",
[
"@semantic-release/git",
{
"assets": ["CHANGELOG.md"],
"message": "chore(release): ${nextRelease.version} [skip ci]\n\n${nextRelease.notes}"
}
]
]
}
58 changes: 58 additions & 0 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
# Contributing

## Commit messages decide releases

Merges are squashed, and the pull request title becomes the commit subject on
`main`. That subject is read by semantic-release, which cuts the tag and the
GitHub release — so the title is not a formality, it is the version bump.

Titles follow [Conventional Commits](https://www.conventionalcommits.org):

```
feat(cron): support step values over a range
fix(runner): keep a task that throws in the schedule
docs: explain why both day fields restricted means either
```

| Prefix | Release |
| --- | --- |
| `fix`, `perf` | patch — `1.2.3` → `1.2.4` |
| `feat` | minor — `1.2.3` → `1.3.0` |
| `feat!`, or `BREAKING CHANGE:` in the body | major — `1.2.3` → `2.0.0` |
| `docs`, `test`, `refactor`, `build`, `ci`, `chore` | none |

A pull request whose title does not parse is rejected by a check before it can
be merged, because a subject semantic-release cannot read is a release that
silently never happens.

Put the reasoning in the pull request body. It becomes the commit body, and it
is the part someone reads in a year when they are trying to work out why.

## Breaking changes

Mark them, and say what to do instead:

```
feat(task)!: name a method task after its class as well

BREAKING CHANGE: an unnamed method task is now "Housekeeping::sweep"
rather than "sweep". Pass name: to keep the old one.
```

## Before opening a pull request

```bash
composer test # PHPUnit
composer analyse # PHPStan
```

## Testing a scheduler

The suite drives a fake loop rather than waiting out the schedules it exercises
— an interval of a second is the smallest the attribute allows, and a cron
task's next turn can be an hour away. `FakeLoop` records what was armed and
fires it on demand, so a test can say exactly when a turn happens and assert on
the wait that was chosen.

Nothing here should need `sleep`. If a test seems to, the thing it is testing
probably wants a seam rather than the test wanting patience.
Loading
Loading