diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 9f8188f..68a92e0 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -1,10 +1,14 @@ -# The gates of AGENTS.md and the commit check of CONTRIBUTING, on every push -# and pull request; on a push to main that passes both, the release. +# The gates of AGENTS.md and the commit check of CONTRIBUTING, on every pull +# request and push to main; on a push to main that passes both, the release. name: ci on: push: + branches: + - main pull_request: + # edited: a new title is a new merge subject, so it is checked again. + types: [opened, synchronize, reopened, edited] permissions: contents: read @@ -58,6 +62,22 @@ jobs: uses: cocogitto/cocogitto-action@v4.2.0 with: install-only: true + - name: Give cog the author identity cog verify reads + run: | + git config user.name "github-actions[bot]" + git config user.email "41898282+github-actions[bot]@users.noreply.github.com" + - name: Check the pull request title as the merge subject + if: github.event_name == 'pull_request' + env: + PR_TITLE: ${{ github.event.pull_request.title }} + run: | + set -eu + printf '%s\n' "$PR_TITLE" > "$RUNNER_TEMP/merge-subject" + grep -Eq '^merge\([a-z0-9-]+\): ' "$RUNNER_TEMP/merge-subject" || { + echo "::error::the title becomes the merge commit's subject: merge(): " + exit 1 + } + cog verify --file "$RUNNER_TEMP/merge-subject" - name: Check the pushed range env: EVENT: ${{ github.event_name }} diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index e6a09f4..f81cf30 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -88,9 +88,11 @@ The highest bump across the batch wins. The core stays at `0.y.z` until the eval ## Branches and integration -- `main` is always releasable. Work happens on a topic branch named `/`, for example `feat/outcome-record`. -- Integrate by rebasing the branch onto `main`, then merging with `--no-ff`. The merge commit is `merge(): `, so the merge itself records the reason. -- Remove the branch and its worktree once it has landed. +- `main` is always releasable and changes only through pull requests. Work happens on a topic branch named `/`, for example `feat/outcome-record`. +- Push the branch and open a pull request against `main`. Its title becomes the merge commit's subject, `merge(): `, so the merge itself records the reason. Its body becomes the merge commit's body; an agent-authored one ends with the `Co-Authored-By:` trailer. +- CI runs the gates and the commit check on the pull request. Before merging, rebase the branch onto `main` (`git rebase origin/main`, then `git push --force-with-lease`). +- Merge with a merge commit; the repository refuses squash and rebase merges. The merge is a push to `main`, which runs CI again and then the release (see [Enforcement](#enforcement)). +- GitHub deletes the merged branch. Remove the local branch and its worktree, then pull `main`, which may also carry the release job's `chore(release)` commit. ## Tools @@ -113,7 +115,8 @@ Git does not copy hooks on clone, so `cog install-hook --all` is the per-clone s | Check | Runs | Rejects | |---|---|---| | `commit-msg` hook | On every local commit, once `cog install-hook --all` has been run | A subject without a `type(scope):` prefix, then, through `cog verify`, a type or scope outside the tables above | -| `cog check ..` | In CI (`.github/workflows/ci.yml`), on the range each push or pull request adds (run it by hand as `cog check main..HEAD`) | The same, for every commit in the range | +| `cog check ..` | In CI (`.github/workflows/ci.yml`), on the range each pull request or push to `main` adds (run it by hand as `cog check main..HEAD`) | The same, for every commit in the range | +| Pull request title | In CI, on every pull request, as the subject of the merge commit it becomes | A title that is not `merge(): …` with a scope from the tables above | | `release-plz` | In CI (`.github/workflows/ci.yml`), on a push to `main` once the gates and the commit check pass: `release-plz update` commits the new versions and changelogs as `chore(release): … [skip ci]`, then `release-plz release` tags each new version and creates its GitHub release | Nothing; a push with no release-triggering commit releases nothing, and nothing goes to crates.io | The types and scopes live in `cog.toml` and equal the tables in this file; change both together. `cog` itself does not require a scope, which is why the hook checks the prefix first. A checkout without `cog.toml` passes the hook, so a branch cut before the file landed can still commit.