Skip to content
Open
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
35 changes: 18 additions & 17 deletions .harness/docs/git-workflow.md
Original file line number Diff line number Diff line change
Expand Up @@ -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`.
Expand All @@ -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 <sha>`, 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 <sha>`, 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

Expand All @@ -80,29 +80,30 @@ 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.

The release branch itself **stays around** indefinitely — it is the frozen history of the release, useful for backports and forensics. Deletion happens only when a future major cuts over and supersedes it.

### 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.
Comment on lines +103 to 104

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Clarify that promote.yml is the only normal automated writer.

This absolute wording conflicts with the documented manual fallback at Lines 122-127, where a maintainer directly performs the stable-version bump. Reword it to distinguish the workflow contract from the emergency procedure.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.harness/docs/git-workflow.md around lines 103 - 104, Revise the promote.yml
statement in the git workflow documentation to say it is the only normal
automated writer that converts -rc.N to the stable version. Preserve the
documented manual maintainer fallback for emergency use, while keeping the
prerelease.yml branch reuse requirement unchanged.

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 <sha>`), 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 <sha>`), 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.

Expand All @@ -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
Comment on lines +116 to +120

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Make the RC fallback work for rc.2+.

The comment advertises rc.2 support, but the commands still hardcode 1.5.0-rc.1 and v1.5.0-rc.1. A subsequent RC would overwrite/recreate the wrong tag instead of publishing rc.N.

Proposed fix
-# Cut RC (skips milestone migration and Discord announce)
-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]"
+RC_NUMBER=1
+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.${RC_NUMBER}\2|" package.json
+git add package.json && git commit -m "chore(release): bump to 1.5.0-rc.${RC_NUMBER} [skip ci]"
 git push origin release/v1.5.0
-git tag v1.5.0-rc.1 && git push origin v1.5.0-rc.1
+git tag "v1.5.0-rc.${RC_NUMBER}" && git push origin "v1.5.0-rc.${RC_NUMBER}"
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.harness/docs/git-workflow.md around lines 116 - 120, Update the release
workflow commands around the rc.1 version and tag literals so the RC fallback
for rc.2+ derives or accepts the intended RC number consistently. Ensure the
package.json version, commit message, and pushed tag all use the same 1.5.0-rc.N
value while preserving the existing rc.1 initial-release path.


# 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

Expand Down
23 changes: 15 additions & 8 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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 <sha> && 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

Expand All @@ -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/<locale>/` (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.
Loading