From 0a2c5dcc3b043ce224e64084396e3635445226d4 Mon Sep 17 00:00:00 2001 From: Etienne Lescot Date: Thu, 16 Jul 2026 17:47:04 +0200 Subject: [PATCH] docs(agents): add development principles, slim release flow, correct branch contract MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Contributor feedback on AGENTS.md, plus doc corrections the release-pipeline fixes made necessary. AGENTS.md: - Add a "Development principles" section: simplest readable solution, no speculative abstraction, and an explicit "no mandated app-stack choice yet" rule so agents stop imposing one state/data library across the codebase. Deliberately avoids "prefer one-liners" phrasing, which pushes models toward dense code that hides control flow. - Relabel "README tone" -> "Product constraints". The note bundles copy rules with a hard product rule (no paywalls/premium gating); "tone" read as if the model were being asked to reflect on the business model rather than follow a constraint. - Slim "Release flow" from 305 to 186 words, deferring the branch contract, cherry-pick rules and manual fallback to .harness/docs/git-workflow.md, which already documents them in full. The section was duplicated context loaded into every agent run. - Make the npm pin rationale explicit: it exists because the native helpers are rebuilt against Electron's ABI via package-lock.json, not as a style choice. .harness/docs/git-workflow.md — corrected to match the shipped pipeline: - Release branch is release/vX.Y.Z (one per stable version, created at rc.1 and reused), not release/vX.Y.Z-rc.N. The old naming is what broke promote, which resolves release/v${STABLE_VERSION}. - The RC tag push does NOT trigger build.yml; prerelease.yml dispatches it explicitly with --ref pinned to the tag. Documented why. - Manual fallback: fix the branch naming and add the missing `git tag` commands (it pushed tags it never created). Co-Authored-By: Claude Opus 4.8 --- .harness/docs/git-workflow.md | 35 ++++++++++++++++++----------------- AGENTS.md | 23 +++++++++++++++-------- 2 files changed, 33 insertions(+), 25 deletions(-) diff --git a/.harness/docs/git-workflow.md b/.harness/docs/git-workflow.md index cd0895fdc..805db9dad 100644 --- a/.harness/docs/git-workflow.md +++ b/.harness/docs/git-workflow.md @@ -55,8 +55,8 @@ The workflow: 1. Computes the next SemVer from `package.json` + `bump`, builds `vX.Y.Z-rc.N`. 2. Migrates every issue/PR in the rolling `Next Release` milestone into a fresh `vX.Y.Z` milestone. Each migrated item gets a hidden marker comment so re-running is idempotent. -3. Commits `package.json` → `X.Y.Z-rc.N` on a fresh branch `release/vX.Y.Z-rc.N`. **The branch is NOT merged into `main`** — it stays frozen so the RC build only contains what was on `main` at the moment of cut. -4. Pushes the tag `vX.Y.Z-rc.N` at the release branch tip. This triggers `build.yml`, which publishes a **GitHub pre-release** (badged as such, does not become "Latest"). macOS notarization is skipped on RC tags. +3. Creates `release/vX.Y.Z` (at rc.1) or reuses it (rc.2+), and commits `package.json` → `X.Y.Z-rc.N` there. **The branch is NOT merged into `main`** — it stays frozen, so the RC contains the cut snapshot plus cherry-picks only. The branch is named for the **stable** version, with no `-rc.N` suffix, because `promote.yml` has to resolve the same ref. +4. Pushes the tag `vX.Y.Z-rc.N` at the release branch tip, then **explicitly dispatches `build.yml` with `--ref` pinned to that tag**, which publishes a **GitHub pre-release** (badged as such, does not become "Latest"). Two reasons the dispatch is explicit and pinned: a `GITHUB_TOKEN` tag push does not fire `build.yml`'s `push:` trigger, and the build must check out the *tag* — `main` still carries the previous stable version and would fail the publish step's version guard. macOS notarization is skipped on RC tags. 5. Posts in `#rc-testing` on Discord with the download link. Tier 3 (homebrew/winget/nix/aur) does **not** run on pre-releases — they're already gated on `!prerelease`. @@ -65,9 +65,9 @@ Tier 3 (homebrew/winget/nix/aur) does **not** run on pre-releases — they're al Pin the pre-release link in `#rc-testing`. Get the maintainer team + a few early adopters to install and smoke-test. -**Between RC cut and promote**, the only thing that may happen on `release/vX.Y.Z-rc.N` is **cherry-picks of bugfixes** that address problems discovered in the RC. Features, refactors, and CI/docs changes are **not** applied to the release branch — they live on `main` and ship in the next release cycle. +**Between RC cut and promote**, the only thing that may happen on `release/vX.Y.Z` is **cherry-picks of bugfixes** that address problems discovered in the RC. Features, refactors, and CI/docs changes are **not** applied to the release branch — they live on `main` and ship in the next release cycle. -If the RC has a regression, fix forward on `main`, then **cherry-pick the fix commit onto the release branch** with `git cherry-pick `, then re-cut as `vX.Y.Z-rc.(N+1)` (the rerun of `prerelease.yml` re-tags the release branch tip; no rebase required because the branch is frozen). The previous RC is auto-superseded by GitHub. +If the RC has a regression, fix forward on `main`, then **cherry-pick the fix commit onto the release branch** with `git cherry-pick `, then re-cut as `vX.Y.Z-rc.(N+1)` (the rerun of `prerelease.yml` reuses the frozen branch and re-tags its tip; no rebase required). The previous RC is auto-superseded by GitHub. ### Step 3: promote to stable @@ -80,8 +80,8 @@ The workflow: 1. Validates the tag matches `^vX.Y.Z-(rc|beta|alpha)\.N$`. 2. Closes the `vX.Y.Z` milestone (snapshotting it for the release notes). -3. Checks out `release/vX.Y.Z-rc.N` (the frozen branch), strips `-rc.N` from `package.json`, and commits the bump there. The stable tag points at this tip — the released code is the exact RC + cherry-picks. -4. Pushes the tag `vX.Y.Z` and triggers `build.yml` (full notarization). The `release: published` event fires Tier 3 (homebrew/winget/nix/aur) thanks to `OPENSCREEN_RELEASE_TOKEN`. +3. Checks out `release/vX.Y.Z` (the frozen branch), strips `-rc.N` from `package.json`, and commits the bump there. The stable tag points at this tip — the released code is the exact RC + cherry-picks. +4. Pushes the tag `vX.Y.Z` and dispatches `build.yml` pinned to that tag (full notarization). The `release: published` event fires Tier 3 (homebrew/winget/nix/aur) thanks to `OPENSCREEN_RELEASE_TOKEN`. 5. Opens a **release-sync PR** (e.g. `release/v1.6.0-sync → main`) that brings `main` into line with the released snapshot. Rebase-merged via PAT (EtienneLescot is a ruleset bypass actor). 6. Posts in `#announcements` on Discord with the release notes + a "Closed issues in this release" list pulled from the milestone. @@ -89,20 +89,21 @@ The release branch itself **stays around** indefinitely — it is the frozen his ### Release branches (the contract) -Every released version has a corresponding **frozen branch**: +Every released version has **exactly one frozen branch**, named for the stable version, living from the first RC cut onward: ``` -release/vX.Y.Z-rc.N exists from RC cut until promote finishes +release/vX.Y.Z created at rc.1, frozen through promote, kept for backports release/vX.Y.Z-sync ephemeral, created by promote to merge into main -release/vX.Y.Z stable snapshot post-promote (kept for backports) ``` +The name carries **no `-rc.N` suffix**. `prerelease.yml` and `promote.yml` must resolve the same ref, and every RC of a version re-cuts from this one branch. + Key rules: -1. **`prerelease.yml` creates the branch.** Nothing else pushes to it except the cherry-pick workflow during the RC window. +1. **`prerelease.yml` creates the branch at rc.1 and reuses it for later RCs.** It must never delete or recreate it: that would drop the cherry-picks and silently re-cut from `main`, which defeats the freeze this contract exists to guarantee. 2. **`promote.yml` is the only writer** that turns `-rc.N` into the stable version on the branch. 3. **`main` is never frozen.** Develop as usual. The release branch is the freeze. -4. **Cherry-picks during the RC window** are committed manually by a maintainer (`git checkout release/vX.Y.Z-rc.N && git cherry-pick `), or rerun `prerelease.yml` to re-tag the branch tip with the same RC version (then bump rc_number). +4. **Cherry-picks during the RC window** are committed manually by a maintainer (`git checkout release/vX.Y.Z && git cherry-pick `), then rerun `prerelease.yml` with the next `rc_number` to re-tag the branch tip. This exists because of the v1.6.0 incident (2026-07-05): the original `promote.yml` checked out `main`, so the stable tag captured the post-RC tip of `main` rather than the RC snapshot. Twenty-three commits (Tiptap, NotesWindow, an in-recorder lint button, AI handoff) ended up in v1.6.0 without ever being in v1.6.0-rc.1. The re-release of v1.6.0 on 2026-07-05 used `release/v1.6.0` and cherry-picked only the truly safe commits. @@ -112,21 +113,21 @@ If the dispatch UI is unavailable, the workflow still works from a shell: ```bash # Cut RC (skips milestone migration and Discord announce) -git checkout -b release/v1.5.0-rc.1 main +git checkout -b release/v1.5.0 main # rc.2+: git checkout release/v1.5.0 instead sed -i -E 's|("version"[[:space:]]*:[[:space:]]*")[^"]*(")|\11.5.0-rc.1\2|' package.json git add package.json && git commit -m "chore(release): bump to 1.5.0-rc.1 [skip ci]" -git push origin release/v1.5.0-rc.1 -git push origin v1.5.0-rc.1 +git push origin release/v1.5.0 +git tag v1.5.0-rc.1 && git push origin v1.5.0-rc.1 # Promote (skips milestone close and Discord announce) -git checkout release/v1.5.0-rc.1 +git checkout release/v1.5.0 sed -i -E 's|("version"[[:space:]]*:[[:space:]]*")[^"]*(")|\11.5.0\2|' package.json git commit -am "chore(release): bump to 1.5.0 [skip ci]" git push origin release/v1.5.0 -git push origin v1.5.0 +git tag v1.5.0 && git push origin v1.5.0 ``` -The pipeline can't tell the difference between a manually-pushed tag and a workflow-pushed one — same `build.yml` runs either way. +A tag pushed manually with your own credentials **does** fire `build.yml`'s `push:` trigger, so the release publishes on its own. (The workflows push tags with `GITHUB_TOKEN`, which does *not* fire it — that's why they dispatch `build.yml` explicitly.) Either way the same `build.yml` builds and publishes. ### Backports / patch on a previous line diff --git a/AGENTS.md b/AGENTS.md index 566132003..0a28aa0c6 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -15,6 +15,15 @@ OpenScreen is a free, open-source screen recorder and video editor (Electron + R - Format: `npm run format` (Biome, tabs, double quotes, 100-col) - i18n check: `npm run i18n:check` (validates the 13 locale files) +**Use npm, not bun/pnpm/yarn/Deno.** Not a style preference: the native Swift (macOS) and C++ (Windows) capture helpers are rebuilt against Electron's ABI by electron-builder + `@electron/rebuild` resolving `package-lock.json`, and other package managers break that path. `packageManager` + `engines` in `package.json` pin the versions; CI installs with `npm ci`. + +## Development principles + +- Prefer the simplest solution that stays readable. No abstraction for hypothetical needs (YAGNI). +- **No mandated app-stack choice yet.** Contributors pick their own state/data library. Don't impose one across the codebase and don't refactor existing code onto a different one — keep each addition self-contained and consistent within its own module. A single choice may be enforced later. +- Don't optimize for line count. A dense one-liner that hides control flow is worse than the explicit version. +- Match the surrounding code's idiom rather than introducing a new pattern next to it. + ## Project layout - `src/` — React app: UI, editor components, timeline, i18n, captioning/cursor/exporter libs @@ -85,15 +94,13 @@ Unit/browser tests can't exercise real capture (native screen recording, a physi ## Release flow -Two `workflow_dispatch` workflows cut a release with a pre-release candidate (RC) first, then promote to stable. Trunk-based, no extra branch. Full operational guide in `.harness/docs/git-workflow.md` § Release flow. - -- **Cut RC**: Actions → "Cut a release candidate" → Run workflow. Inputs: `bump` (patch|minor|major), `rc_number` (default 1), optional `target_version` override. Snaps issues out of the rolling `Next Release` milestone into a versioned `vX.Y.Z` milestone, bumps `package.json`, pushes the `vX.Y.Z-rc.N` tag, which triggers the existing `build.yml` to publish a GitHub pre-release. Notarization is skipped on RCs. Notifies `#rc-testing` on Discord. -- **Promote RC**: Actions → "Promote RC to stable release" → Run workflow. Input: `rc_tag` (e.g. `v1.5.0-rc.2`), optional `release_notes_extra`. Closes the `vX.Y.Z` milestone, strips `-rc.N` from `package.json`, pushes `vX.Y.Z` tag, which triggers `build.yml` to publish a stable release (full notarization, Tier 3 homebrew/winget/nix/aur fires). Notifies `#announcements` on Discord. -- **Manual fallback**: `git tag vX.Y.Z-rc.N && git push origin vX.Y.Z-rc.N` does the same as Cut RC (minus the milestone migration and Discord announce) — useful for emergency cuts. +Two `workflow_dispatch` workflows: cut an RC, then promote it to stable. **Full operational guide, branch contract, cherry-pick rules, and manual fallback: `.harness/docs/git-workflow.md` § Release flow.** Read it before touching a release. -Both workflows require the `OPENSCREEN_RELEASE_TOKEN` secret (a fine-grained PAT with `contents: write` + `issues: write`). This is the standard fix for `release: published` not triggering downstream workflows when the release is created by `GITHUB_TOKEN`. See `docs/secrets.md`. +- **Cut RC**: Actions → "Cut a release candidate". Inputs: `bump` (patch|minor|major), `rc_number`, optional `target_version`. Migrates the rolling `Next Release` milestone into a versioned one, creates or reuses `release/vX.Y.Z`, bumps `package.json`, pushes `vX.Y.Z-rc.N`, then dispatches `build.yml` **pinned to that tag** to publish a GitHub pre-release. Notarization is skipped on RCs. Announces in `#rc-testing`. +- **Promote**: Actions → "Promote RC to stable release". Input: `rc_tag`. Closes the milestone, strips `-rc.N` on the same branch, tags `vX.Y.Z`, publishes a stable release (notarized; Tier 3 homebrew/winget/nix/aur fires), and opens a `release/vX.Y.Z-sync → main` PR. Announces in `#announcements`. +- **One release branch per stable version** (`release/vX.Y.Z`), created at rc.1 and **frozen** until promote: only cherry-picked bugfixes land on it, so anything merged to `main` after the cut ships in the next cycle. Later RCs re-cut from that same branch. -**Release branches freeze the build between cut and promote.** Every RC cut creates `release/vX.Y.Z-rc.N`. The branch is *not* merged into `main` until the stable tag is published; only cherry-picks of bugfixes land on the release branch during the RC window. The stable tag points at the branch tip (RC + cherry-picks), then `promote.yml` opens a `release/vX.Y.Z-sync → main` PR to bring main into line. This contract exists because of the v1.6.0 incident (2026-07-05) where the original promote workflow tagged `main` instead of the RC snapshot, causing 23 unreleased commits to ship in `v1.6.0`. Full rules in `.harness/docs/git-workflow.md` § Release branches. +Both workflows need the `OPENSCREEN_RELEASE_TOKEN` secret (see `docs/secrets.md`); `GITHUB_TOKEN`-created releases don't fire the downstream `release: published` workflows. ## Security @@ -107,4 +114,4 @@ Both workflows require the `OPENSCREEN_RELEASE_TOKEN` secret (a fine-grained PAT - **Pixi.js v8** is the rendering engine. Filters come from `pixi-filters` and `@pixi/filter-drop-shadow`. GSAP + `motion` for animation. - **i18n**: 13 locales in `src/i18n/locales//` (e.g. `src/i18n/locales/en/settings.json`). The `i18n:check` script validates them — run it after touching translation files. - **Build pipeline**: `npm run build` is full electron-builder. For iterating on renderer only, use `npm run build-vite` (Vite + tsc, no packaging). -- **README tone**: the project is explicitly "not production-grade" and free forever — don't add paywalls, premium tiers, or upsell language to UI/copy. +- **Product constraints**: the project is free forever and explicitly "not production-grade". Don't add paywalls, premium tiers, or feature-gating logic, and don't add upsell language to the README or UI copy. This is a hard constraint, not a judgement call.