develop is the trunk; main holds released code and moves by fast-forward at a release

CI runs on merges to develop (and on fork pull requests); the release workflow builds the draft
from a push to main. AGENTS.md, CONTRIBUTING.md and docs/releasing.md follow.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NmAttHhUZTokgPeEJia8sm
This commit is contained in:
jyvikoandClaude Opus 5.5 authored and Irakli Kourtis committed 2026-10-14 08:32:27 -04:00
1 parent 9469ac0853
commit fe7d536719
5 files changed
+46 -41

No files matched your search

+8 -7
View File
@@ -1,7 +1,8 @@
name: CI name: CI
# CI runs once per merge, on main (AGENTS.md, "Workflow for every change"): the core team's pull # CI runs once per merge on `develop`, the trunk (AGENTS.md, "Workflow for every change"): the core
# requests are checked locally on the crates they touch and merged; a red main is fixed forward. # team's pull requests are checked locally on the crates they touch and merged; a red trunk is
# fixed forward. `main` holds released code only (docs/releasing.md).
# Pull requests from forks (outside contributors) run the full gate, once the maintainer approves # Pull requests from forks (outside contributors) run the full gate, once the maintainer approves
# the run, so the review sees it. # the run, so the review sees it.
on: on:
@@ -13,7 +14,7 @@ on:
- 'docs/features.md' - 'docs/features.md'
- 'docs/oracle.md' - 'docs/oracle.md'
push: push:
branches: [main] branches: [develop]
# A merge that only touches prose starts no run. The generated pages are checked by the jobs # A merge that only touches prose starts no run. The generated pages are checked by the jobs
# (features.md by `cargo xtask ci`, oracle.md by the oracle job), so they still count. # (features.md by `cargo xtask ci`, oracle.md by the oracle job), so they still count.
# (Later patterns override earlier ones.) # (Later patterns override earlier ones.)
@@ -24,7 +25,7 @@ on:
- 'docs/features.md' - 'docs/features.md'
- 'docs/oracle.md' - 'docs/oracle.md'
schedule: schedule:
# Nightly: the heavy tests on main. # Nightly, on the default branch (develop): the heavy tests.
- cron: '23 3 * * *' - cron: '23 3 * * *'
workflow_dispatch: workflow_dispatch:
@@ -54,7 +55,7 @@ jobs:
targets: wasm32-unknown-unknown targets: wasm32-unknown-unknown
- uses: Swatinem/rust-cache@6323deb102c322ba6fcbdcafc7e3dddab59af2b6 # v2.9.2 - uses: Swatinem/rust-cache@6323deb102c322ba6fcbdcafc7e3dddab59af2b6 # v2.9.2
with: with:
save-if: ${{ github.ref == 'refs/heads/main' }} save-if: ${{ github.ref == 'refs/heads/develop' }}
- run: cargo xtask ci - run: cargo xtask ci
heavy: heavy:
@@ -68,7 +69,7 @@ jobs:
- uses: dtolnay/rust-toolchain@stable - uses: dtolnay/rust-toolchain@stable
- uses: Swatinem/rust-cache@6323deb102c322ba6fcbdcafc7e3dddab59af2b6 # v2.9.2 - uses: Swatinem/rust-cache@6323deb102c322ba6fcbdcafc7e3dddab59af2b6 # v2.9.2
with: with:
save-if: ${{ github.ref == 'refs/heads/main' }} save-if: ${{ github.ref == 'refs/heads/develop' }}
- run: cargo xtask test --heavy - run: cargo xtask test --heavy
oracle: oracle:
@@ -85,7 +86,7 @@ jobs:
- uses: Swatinem/rust-cache@6323deb102c322ba6fcbdcafc7e3dddab59af2b6 # v2.9.2 - uses: Swatinem/rust-cache@6323deb102c322ba6fcbdcafc7e3dddab59af2b6 # v2.9.2
with: with:
key: oracle key: oracle
save-if: ${{ github.ref == 'refs/heads/main' }} save-if: ${{ github.ref == 'refs/heads/develop' }}
- run: cargo xtask oracle - run: cargo xtask oracle
- name: docs/oracle.md is current - name: docs/oracle.md is current
run: git diff --exit-code docs/oracle.md run: git diff --exit-code docs/oracle.md
+11 -10
View File
@@ -1,11 +1,12 @@
# Release builds (docs/releasing.md), after SolidCraft's and VectorCraft's release.yml # Release builds (docs/releasing.md), after SolidCraft's and VectorCraft's release.yml
# (MIT OR Apache-2.0; see ATTRIBUTION.md). # (MIT OR Apache-2.0; see ATTRIBUTION.md).
# #
# A push to `release` (fast-forwarded from main) builds every package and creates or updates the # A push to `main` (fast-forwarded from a verified `develop` commit; main holds released code only)
# builds every package and creates or updates the
# DRAFT GitHub Release "FieldCraft v<version>", with SHA256SUMS.txt and generated notes. The # DRAFT GitHub Release "FieldCraft v<version>", with SHA256SUMS.txt and generated notes. The
# version is [workspace.package] version in Cargo.toml; a suffix (0.2.0-rc.1) makes a # version is [workspace.package] version in Cargo.toml; a suffix (0.2.0-rc.1) makes a
# pre-release. A person verifies the draft and publishes it; this workflow never publishes, # pre-release. A person verifies the draft and publishes it; this workflow never publishes,
# and pushes to main or tags never start it. # and pushes to develop or tags never start it.
# #
# macOS universal (arm64 + x86_64) .dmg with FieldCraft.app, the CLI as a .zip packaging/macos # macOS universal (arm64 + x86_64) .dmg with FieldCraft.app, the CLI as a .zip packaging/macos
# Linux x86_64 (glibc 2.35+) .tar.gz and .AppImage packaging/linux # Linux x86_64 (glibc 2.35+) .tar.gz and .AppImage packaging/linux
@@ -15,17 +16,17 @@
# otherwise (the release notes say which). Linux and Windows builds are not signed. # otherwise (the release notes say which). Linux and Windows builds are not signed.
# #
# Dry run: `gh workflow run release.yml --ref <branch>` builds everything and uploads the # Dry run: `gh workflow run release.yml --ref <branch>` builds everything and uploads the
# `release-assets` artifact without creating a release. `-f draft=true` on `release` retries # `release-assets` artifact without creating a release. `-f draft=true` on `main` retries
# the draft. # the draft.
name: Release name: Release
on: on:
push: push:
branches: [release] branches: [main]
workflow_dispatch: workflow_dispatch:
inputs: inputs:
draft: draft:
description: "Create/update the draft release (release branch only; off: build artifacts only)" description: "Create/update the draft release (main only; off: build artifacts only)"
type: boolean type: boolean
default: false default: false
@@ -60,8 +61,8 @@ jobs:
echo "::error::Cargo.toml must contain a version like 1.2.3 or 1.2.3-rc.1, not '$VERSION'" echo "::error::Cargo.toml must contain a version like 1.2.3 or 1.2.3-rc.1, not '$VERSION'"
exit 1 exit 1
fi fi
if [ "$REQUEST_DRAFT" = true ] && [ "$GITHUB_REF" != refs/heads/release ]; then if [ "$REQUEST_DRAFT" = true ] && [ "$GITHUB_REF" != refs/heads/main ]; then
echo "::error::Draft releases are built from the release branch only" echo "::error::Draft releases are built from main only"
exit 1 exit 1
fi fi
case "$VERSION" in *-*) pre=true ;; *) pre=false ;; esac case "$VERSION" in *-*) pre=true ;; *) pre=false ;; esac
@@ -86,7 +87,7 @@ jobs:
with: with:
key: release-macos-universal key: release-macos-universal
# Release runs are rare: only release-branch runs keep a cache, so dry runs don't evict CI's. # Release runs are rare: only release-branch runs keep a cache, so dry runs don't evict CI's.
save-if: ${{ github.ref == 'refs/heads/release' }} save-if: ${{ github.ref == 'refs/heads/main' }}
- name: Import the signing certificate (skipped without APPLE_CERTIFICATE) - name: Import the signing certificate (skipped without APPLE_CERTIFICATE)
env: env:
APPLE_CERTIFICATE: ${{ secrets.APPLE_CERTIFICATE }} APPLE_CERTIFICATE: ${{ secrets.APPLE_CERTIFICATE }}
@@ -129,7 +130,7 @@ jobs:
with: with:
key: release-linux-x86_64 key: release-linux-x86_64
# Release runs are rare: only release-branch runs keep a cache, so dry runs don't evict CI's. # Release runs are rare: only release-branch runs keep a cache, so dry runs don't evict CI's.
save-if: ${{ github.ref == 'refs/heads/release' }} save-if: ${{ github.ref == 'refs/heads/main' }}
- name: Build and package (tar.gz, AppImage) - name: Build and package (tar.gz, AppImage)
run: packaging/linux/package.sh run: packaging/linux/package.sh
- name: Check the packages - name: Check the packages
@@ -167,7 +168,7 @@ jobs:
with: with:
key: release-windows-x64 key: release-windows-x64
# Release runs are rare: only release-branch runs keep a cache, so dry runs don't evict CI's. # Release runs are rare: only release-branch runs keep a cache, so dry runs don't evict CI's.
save-if: ${{ github.ref == 'refs/heads/release' }} save-if: ${{ github.ref == 'refs/heads/main' }}
- name: Build and package (zip) - name: Build and package (zip)
shell: pwsh shell: pwsh
run: ./packaging/windows/package.ps1 run: ./packaging/windows/package.ps1
+12 -10
View File
@@ -99,7 +99,7 @@ People trust FieldCraft with their models; a crash loses their work. **This outr
1. Work in your own worktree on a branch, with your own `CARGO_TARGET_DIR`. While you work: 1. Work in your own worktree on a branch, with your own `CARGO_TARGET_DIR`. While you work:
`cargo check` and the tests of the crates you touch (`cargo test -p <crate>`). Commit after `cargo check` and the tests of the crates you touch (`cargo test -p <crate>`). Commit after
each working step (a layer that compiles with its tests passing), not one big commit per PR. each working step (a layer that compiles with its tests passing), not one big commit per PR.
Every commit lands on `main` as it is, so each one must build and pass its tests and say Every commit lands on `develop` as it is, so each one must build and pass its tests and say
what it does: `git bisect` walks them one by one. what it does: `git bisect` walks them one by one.
2. When the change is done: review it once with `/simplify`, then `cargo fmt --all` and 2. When the change is done: review it once with `/simplify`, then `cargo fmt --all` and
`cargo clippy -p <crate> --all-targets -- -D warnings` for each crate you touched, once. `cargo clippy -p <crate> --all-targets -- -D warnings` for each crate you touched, once.
@@ -108,11 +108,12 @@ People trust FieldCraft with their models; a crash loses their work. **This outr
oracle cases or solver numerics change (`cargo xtask oracle`). oracle cases or solver numerics change (`cargo xtask oracle`).
3. Fold every fix into the commit it fixes, never a separate "fmt", "clippy", "review fixes" or 3. Fold every fix into the commit it fixes, never a separate "fmt", "clippy", "review fixes" or
"regenerate features.md" commit: `git commit --fixup <sha>`, then "regenerate features.md" commit: `git commit --fixup <sha>`, then
`git rebase -i --autosquash origin/main` (`GIT_SEQUENCE_EDITOR=true` keeps it `git rebase -i --autosquash origin/develop` (`GIT_SEQUENCE_EDITOR=true` keeps it
non-interactive). A regenerated page goes into the commit that changed what it lists. non-interactive). A regenerated page goes into the commit that changed what it lists.
4. Rebase onto the latest `origin/main` (never merge `main` into a branch), rerun the tests of 4. Branch from and rebase onto the latest `origin/develop`, the trunk (never merge `develop` into
the touched crates, push, open the PR and merge it with a rebase merge, which keeps `main` a branch), rerun the tests of
linear and every commit bisectable. `main` is protected (CODEOWNERS review, linear history, the touched crates, push, open the PR and merge it with a rebase merge, which keeps `develop`
linear and every commit bisectable. `develop` and `main` are protected (CODEOWNERS review, linear history,
rebase merges only); the core team merges as the maintainer with rebase merges only); the core team merges as the maintainer with
`gh pr merge <N> --rebase --admin --delete-branch`, run as its own command. Outside `gh pr merge <N> --rebase --admin --delete-branch`, run as its own command. Outside
contributors' pull requests wait for the maintainer's review (CONTRIBUTING.md). Never squash. contributors' pull requests wait for the maintainer's review (CONTRIBUTING.md). Never squash.
@@ -120,20 +121,21 @@ People trust FieldCraft with their models; a crash loses their work. **This outr
5. Never run the full gate locally: no `cargo xtask ci`, no `cargo clippy --workspace`, no 5. Never run the full gate locally: no `cargo xtask ci`, no `cargo clippy --workspace`, no
`cargo xtask wasm` or `oracle` unless step 2 calls for it. Never watch CI. GitHub CI runs `cargo xtask wasm` or `oracle` unless step 2 calls for it. Never watch CI. GitHub CI runs
`cargo xtask ci` (fmt, assets, licences, layers, clippy -D warnings, tests, wasm), the `cargo xtask ci` (fmt, assets, licences, layers, clippy -D warnings, tests, wasm), the
oracle, the heavy tests and the advisories once per merge on `main`; the coordinator oracle, the heavy tests and the advisories once per merge on `develop`; the coordinator
watches it and sends a failure back to the change that caused it, which is fixed forward watches it and sends a failure back to the change that caused it, which is fixed forward
at once. at once.
- **Keep the main gate fast** (under ~3 minutes on CI). A test that takes more than a few seconds - **Keep the trunk gate fast** (under ~3 minutes on CI). A test that takes more than a few seconds
(convergence studies, the whole corpus, cross-checks between solvers) is (convergence studies, the whole corpus, cross-checks between solvers) is
`#[ignore = "heavy: cargo xtask test --heavy (CI heavy job)"]`, with a fast representative `#[ignore = "heavy: cargo xtask test --heavy (CI heavy job)"]`, with a fast representative
test left in the main gate. The `heavy` CI job runs `cargo xtask test --heavy` (every ignored test left in the trunk gate. The `heavy` CI job runs `cargo xtask test --heavy` (every ignored
test, benchmarks included, and the doc tests) on every merge to `main`, nightly and on test, benchmarks included, and the doc tests) on every merge to `develop`, nightly and on
demand (`gh workflow run CI --ref <branch>`). Hostile-input tests run commands on sessions with small demand (`gh workflow run CI --ref <branch>`). Hostile-input tests run commands on sessions with small
work caps (`Session::limits`) so refusals are instant; never lower the production caps for a work caps (`Session::limits`) so refusals are instant; never lower the production caps for a
test. test.
- **Publishing**: `origin` is github.com/jyviko/fieldcraft. Changes land through pull requests - **Publishing**: `origin` is github.com/jyviko/fieldcraft. Changes land through pull requests
as the workflow above describes; the coordinator reads every merged diff and sends findings as the workflow above describes; the coordinator reads every merged diff and sends findings
back as fix-forward work. Never force-push `main`. Never commit `plan/`, `.agent-files/`, back as fix-forward work. Never force-push `develop` or `main`; `main` holds released code and moves only by
fast-forward at a release (docs/releasing.md). Never commit `plan/`, `.agent-files/`,
`worktrees/` or `target*/`; check `git ls-files` before pushing. `worktrees/` or `target*/`; check `git ls-files` before pushing.
## Running ## Running
+4 -2
View File
@@ -18,7 +18,8 @@ Issues labelled **in progress** are being worked on by the core team; please don
## Making the change ## Making the change
1. Branch from `main`, keep a pull request to one topic. 1. Branch from `develop` (the trunk; `main` holds released code only) and open the pull request
against `develop`. Keep it to one topic.
2. Before pushing: `cargo fmt --all`, and `cargo clippy -p <crate> --all-targets -- -D warnings` 2. Before pushing: `cargo fmt --all`, and `cargo clippy -p <crate> --all-targets -- -D warnings`
and `cargo test -p <crate>` for the crates you touched. CI runs the full gate on your pull and `cargo test -p <crate>` for the crates you touched. CI runs the full gate on your pull
request once the maintainer approves the run. request once the maintainer approves the run.
@@ -30,6 +31,7 @@ Issues labelled **in progress** are being worked on by the core team; please don
By contributing you agree your work is licensed under MIT OR Apache-2.0. By contributing you agree your work is licensed under MIT OR Apache-2.0.
`main` is protected: changes land only through pull requests that the maintainer has reviewed, `develop` and `main` are protected: changes land on `develop` only through pull requests that the
maintainer has reviewed,
as rebase merges (keep each commit building and passing its tests). Be kind: see the as rebase merges (keep each commit building and passing its tests). Be kind: see the
[code of conduct](CODE_OF_CONDUCT.md). [code of conduct](CODE_OF_CONDUCT.md).
+11 -12
View File
@@ -1,9 +1,9 @@
# Releasing FieldCraft # Releasing FieldCraft
Releases are deliberate. Development lands on `main`; pushing the chosen commit to the `release` Releases are deliberate. Development lands on `develop`, the trunk; `main` holds released code
branch builds every package and creates a **draft** GitHub Release; a maintainer verifies the only. Fast-forwarding `main` to the chosen, verified `develop` commit builds every package and
draft and publishes it. Pushes to `main` and tags never start a release, and nothing bumps the creates a **draft** GitHub Release; a maintainer verifies the draft and publishes it. Pushes to
version automatically. The workflow is `.github/workflows/release.yml`; the packaging scripts `develop` and tags never start a release, and nothing bumps the version automatically. The workflow is `.github/workflows/release.yml`; the packaging scripts
are in `packaging/` and run the same way on a developer machine. are in `packaging/` and run the same way on a developer machine.
## What a release contains ## What a release contains
@@ -57,22 +57,21 @@ certificate is added.
notes); `-rc.N` for a release candidate. `1.0` is the owner's decision. notes); `-rc.N` for a release candidate. `1.0` is the owner's decision.
2. Set `[workspace.package] version` in the root `Cargo.toml` and run 2. Set `[workspace.package] version` in the root `Cargo.toml` and run
`cargo update --workspace --offline`, which changes only the workspace crates' versions in `cargo update --workspace --offline`, which changes only the workspace crates' versions in
`Cargo.lock`. Land it on `main` through a pull request like any change, and let CI on `main` `Cargo.lock`. Land it on `develop` through a pull request like any change, and let CI on
pass. Note the commit to release. `develop` pass. Note the commit to release.
## Build the draft ## Build the draft
```sh ```sh
git fetch origin git fetch origin
git switch release # the first time: git switch -c release origin/main git push origin <commit>:main # fast-forward main to the verified develop commit
git merge --ff-only origin/main
git push origin release
``` ```
Only fast-forward `release` and never force-push it. The run builds the three platforms; when `main` holds released code only: it moves only by fast-forwarding to a verified `develop`
commit (a maintainer push; the branch is protected), never by merges, rewrites or force-pushes. The run builds the three platforms; when
all succeed, the last job checks that every file is there, writes `SHA256SUMS.txt` and the notes, all succeed, the last job checks that every file is there, writes `SHA256SUMS.txt` and the notes,
and creates the draft `FieldCraft v<version>` targeting the pushed commit. Pushing and creates the draft `FieldCraft v<version>` targeting the pushed commit. Pushing
`release` again with the same version rebuilds and replaces the draft's files (its notes are `main` again with the same version rebuilds and replaces the draft's files (its notes are
kept). The job refuses a version whose tag exists or whose release is already published: bump kept). The job refuses a version whose tag exists or whose release is already published: bump
the version instead. the version instead.
@@ -98,6 +97,6 @@ Publishing is a maintainer's call; an agent preparing a release stops at the dra
`gh workflow run release.yml -R jyviko/fieldcraft --ref <branch>` builds every package and `gh workflow run release.yml -R jyviko/fieldcraft --ref <branch>` builds every package and
uploads them with the notes as the `release-assets` artifact of the run, without creating a uploads them with the notes as the `release-assets` artifact of the run, without creating a
release. `-f draft=true` creates or updates the draft, and is accepted on `release` only. release. `-f draft=true` creates or updates the draft, and is accepted on `main` only.
Locally, `packaging/macos/package.sh --arch aarch64` (or `x86_64`, `universal`) and Locally, `packaging/macos/package.sh --arch aarch64` (or `x86_64`, `universal`) and
`packaging/linux/package.sh` write to `dist/release/`. `packaging/linux/package.sh` write to `dist/release/`.