CI / Coding Standards (pull_request) Successful in 23s
CI / Tests (PHP 8.5) (pull_request) Successful in 27s
CI / Tests (PHP 8.2) (pull_request) Successful in 35s
CI / Static Analysis (pull_request) Successful in 39s
CI / Tests (PHP 8.3) (pull_request) Successful in 40s
CI / Tests (PHP 8.1) (pull_request) Successful in 49s
CI / No Debug Code (pull_request) Successful in 35s
CI / Build Plugin Zip (pull_request) Skipped
main now requires signed commits, and Gitea refuses to merge a pull request carrying an unsigned one, so the post-release bump PR could not be merged without disabling the protection first. The runner now signs that commit with a dedicated release-bot SSH key held as an organisation Actions secret, trusted by the instance through [repository.signing] TRUSTED_SSH_KEYS. The key Gitea signs merge commits with lives on the server and no runner can reach it. The step fails fast when the secret is missing, unparseable or passphrase protected, and the commit is re-read before pushing so an unsigned one is caught here rather than days later at merge time. Co-Authored-By: Claude Opus 5 <[email protected]>
102 lines
4.3 KiB
Markdown
102 lines
4.3 KiB
Markdown
# CI
|
|
|
|
CI and release jobs do not install PHP. They run inside the shared images
|
|
maintained in [Unsupervised/ci-php](https://git.unsupervised.ca/Unsupervised/ci-php):
|
|
|
|
```
|
|
git.unsupervised.ca/unsupervised/ci-php:8.1
|
|
git.unsupervised.ca/unsupervised/ci-php:8.2
|
|
git.unsupervised.ca/unsupervised/ci-php:8.3
|
|
git.unsupervised.ca/unsupervised/ci-php:8.5
|
|
```
|
|
|
|
The `Unsupervised` org is public, so they pull anonymously — no registry
|
|
credentials in any job here. What the images contain, how they are published,
|
|
and how to add a PHP version are documented in that repository's README.
|
|
|
|
## Which job runs where
|
|
|
|
| Job | Runs in |
|
|
|---|---|
|
|
| Coding Standards (PHPCS) | `ci-php:8.3` |
|
|
| Static Analysis (PHPStan) | `ci-php:8.3` |
|
|
| Tests | `ci-php:${{ matrix.php }}` |
|
|
| Build Plugin Zip | `ci-php:8.3` |
|
|
| No Debug Code | runner image — no PHP, and it uses GNU `grep --include` |
|
|
| Open next-version bump PR (release.yml) | runner image — no PHP |
|
|
|
|
PHPCS and PHPStan are separate jobs so a coding-standards failure still lets
|
|
the static analysis result through. They run in parallel.
|
|
|
|
## Composer
|
|
|
|
`composer.lock` is committed, so every job installs the same dependency set
|
|
and two builds of the same tag ship the same vendor tree. `bin/build-zip.sh`
|
|
stages the lock into its build directory for the same reason, then removes it
|
|
before writing the zip.
|
|
|
|
The Composer download cache lives at `/composer/cache` — `COMPOSER_HOME` is
|
|
`/composer` in the image — and is keyed on `composer.lock`.
|
|
|
|
## Adding a PHP version to the test matrix
|
|
|
|
The image has to exist first. Add the version to the `php` matrix in
|
|
`ci-php`'s `.gitea/workflows/publish.yml` and merge, then add it to the `test`
|
|
matrix in `.gitea/workflows/ci.yml` here.
|
|
|
|
## Signing the version bump commit
|
|
|
|
`main` is a protected branch that requires signed commits, and Gitea will not
|
|
merge a pull request containing an unsigned one. The `bump-version` job in
|
|
`release.yml` therefore signs the commit it makes, using a dedicated
|
|
`release-bot` SSH key rather than the key Gitea signs merge commits with —
|
|
that one is `[repository.signing] SIGNING_KEY` on the server and no runner can
|
|
reach it. Keeping the CI key separate also means it can be rotated on its own
|
|
if the secret ever leaks.
|
|
|
|
There is deliberately no `release-bot` Gitea account. A key attached to an
|
|
account is only consulted for signature checking after it has been through the
|
|
web *Verify* flow, and that flow has no API — a bot account would need an
|
|
interactive login to be worth anything. Listing the key under
|
|
`TRUSTED_SSH_KEYS` instead makes Gitea verify commits signed with it without
|
|
any account lookup, which is all the protected branch asks for.
|
|
|
|
Set up once for the instance, and again only if the key is rotated:
|
|
|
|
1. Generate a passphrase-less key (it has to be usable unattended):
|
|
|
|
```
|
|
ssh-keygen -t ed25519 -C '[email protected]' -f release-bot -N ''
|
|
```
|
|
|
|
2. Add the public half to `app.ini` and restart Gitea:
|
|
|
|
```ini
|
|
[repository.signing]
|
|
TRUSTED_SSH_KEYS = ssh-ed25519 AAAAC3Nza... [email protected]
|
|
```
|
|
|
|
3. Store the private half as the **organisation** Actions secret
|
|
`RELEASE_BOT_SIGNING_KEY` (Org → Settings → Actions → Secrets): the whole
|
|
`release-bot` file verbatim, `-----BEGIN OPENSSH PRIVATE KEY-----` header
|
|
and footer included — not the `.pub`, and not a GPG export. Organisation
|
|
secrets are readable as `secrets.RELEASE_BOT_SIGNING_KEY` from every
|
|
repository in the org, so no repository-level copy is needed. Delete both
|
|
local files afterwards.
|
|
|
|
Two consequences of trusting the key instance-wide are worth knowing. Any
|
|
commit signed with it verifies in *every* repository on the instance, not just
|
|
these — the trust is in the key, not in a user with permissions you can scope.
|
|
And the signature is attributed to `SIGNING_NAME` / `SIGNING_EMAIL`, not to the
|
|
`Release Bot <release-bot@unsupervised.ca>` committer the job sets; that
|
|
address backs no account and is only a label.
|
|
|
|
The job fails fast if the secret is missing or `ssh-keygen` is absent from the
|
|
runner image, and it re-reads the commit it just made to confirm a signature
|
|
is attached before pushing — an unsigned bump commit would otherwise look fine
|
|
until someone tried to merge the PR.
|
|
|
|
Nothing else in the pipeline signs anything: release tags are made by a human
|
|
through Gitea's release UI, and the merge commit is signed by the server when
|
|
the PR is merged.
|