-
Notifications
You must be signed in to change notification settings - Fork 82
docs(agents): add development principles, slim release flow, correct branch contract #96
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
9d08f34
8da0c3c
98e38c9
ca5124b
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -14,6 +14,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`, which resolve the tree through `package-lock.json`. Another package manager writes a different lockfile, so that rebuild breaks. `packageManager` + `engines` in `package.json` pin the versions; CI installs with `npm ci`. | ||
|
|
||
| ## Development principles | ||
|
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. ✓ The "No mandated app-stack choice yet." qualifier with the asterisk'd "may be enforced later" is the right escape hatch. Tells future agents this is provisional, not a forever rule. Small wording nit: "Open to React/Vue/Svelte/whatever" is a bit informal. Consider "No mandated UI framework; React is current, but other choices are open" — same meaning, less jokey.
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. No change needed here — the string you quoted isn't in the diff. The bullet reads:
No "React/Vue/Svelte/whatever" anywhere in |
||
|
|
||
| - 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 | ||
|
|
@@ -118,15 +127,9 @@ 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. RCs are notarized like stable releases, which also rehearses the credentials before the promotion build depends on them. 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. | ||
|
|
||
| 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 `technical-documentation/engineering/release-and-secrets.md`. | ||
| Two `workflow_dispatch` workflows: cut an RC, then promote it to stable. **Full operational guide, branch contract, cherry-pick rules, and manual fallback: `technical-documentation/engineering/release-and-secrets.md`.** Read it before touching a release. | ||
|
|
||
| **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. | ||
| The one rule to know before you merge anything: **there is 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, not the one in flight. | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win Use a noun for the promotion event.
The supplied LanguageTool hint identifies this line. 🧰 Tools🪛 LanguageTool[grammar] ~132-~132: Ensure spelling is correct (QB_NEW_EN_ORTHOGRAPHY_ERROR_IDS_1) 🤖 Prompt for AI AgentsSource: Linters/SAST tools |
||
|
|
||
| ## Security | ||
|
|
||
|
|
@@ -140,4 +143,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 logic that gates a feature on who the user is, and don't add upsell language to the README or UI copy. This is a hard constraint, not a judgement call. (A flag that hides an unfinished capture backend is fine — it gates on readiness, not on the user.) | ||
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
| @@ -1,15 +1,25 @@ | ||||||
| #!/usr/bin/env node | ||||||
| // Docs lint for technical-documentation/: relative links resolve, no legacy | ||||||
| // identifiers are presented as current, and every expected file is real. | ||||||
| // ponytail: three regex passes over ~30 files, no deps. Run: node scripts/check-docs.mjs | ||||||
| // Docs lint: relative links resolve, no legacy identifiers are presented as | ||||||
| // current, and every expected file is real. | ||||||
| // ponytail: three regex passes over ~40 files, no deps. Run: node scripts/check-docs.mjs | ||||||
| import { readdirSync, readFileSync, statSync } from "node:fs"; | ||||||
| import { dirname, join, relative, resolve } from "node:path"; | ||||||
|
|
||||||
| const ROOT = resolve(import.meta.dirname, ".."); | ||||||
| const DOCS = join(ROOT, "technical-documentation"); | ||||||
|
|
||||||
| // Names of components / docs that no longer exist on this branch. A doc may only | ||||||
| // mention them inside a "removed / superseded" note, which lives in decisions.md. | ||||||
| // `.harness/` is prose too, and it rots the same way — it just had no checker. | ||||||
| // `.harness/docs/git-workflow.md` described the release-branch naming that #90 | ||||||
| // had already replaced, and `.harness/memory/MEMORY.md` repeated it, for a | ||||||
| // month, while the same fact stayed correct under technical-documentation/ | ||||||
| // because this script was watching that tree and not this one. Every file here | ||||||
| // is loaded into agent runs, so a stale one is worse than a stale page nobody | ||||||
| // opens. Only REQUIRED (the "expected file is real" pass) stays docs-only. | ||||||
| const TREES = [DOCS, join(ROOT, ".harness")]; | ||||||
|
|
||||||
| // Names of components, docs, or conventions that no longer exist on this branch. | ||||||
| // A doc may only mention them inside a "removed / superseded" note, which lives | ||||||
| // in decisions.md. | ||||||
| const LEGACY = [ | ||||||
| "TimelinePane", | ||||||
| "RightPanelStack", | ||||||
|
|
@@ -32,7 +42,20 @@ const LEGACY = [ | |||||
| "github-actions-workflows", | ||||||
| "ux-ui-spec", | ||||||
| ]; | ||||||
| const LEGACY_ALLOWED = new Set(["architecture/decisions.md"]); | ||||||
|
|
||||||
| // Pre-#90 release-branch naming. One branch per *stable* version now | ||||||
| // (`release/vX.Y.Z`), created at rc.1 and reused, because prerelease.yml and | ||||||
| // promote.yml have to resolve the same ref; a doc that reintroduces the | ||||||
| // suffixed name sends a maintainer to a branch nothing ever creates. | ||||||
| // | ||||||
| // A LEGACY substring entry is not enough here: the rot used BOTH spellings, and | ||||||
| // the one that mattered was the concrete `release/v1.5.0-rc.1` sitting in a | ||||||
| // copy-pasteable shell block, not the `release/vX.Y.Z-rc.N` placeholder in the | ||||||
| // prose. Someone restoring that block from git history would have passed the | ||||||
| // lint. Matches the branch form only — the RC *tags* (`v1.6.0-rc.1`, no | ||||||
| // `release/` prefix) are current and appear in the v1.6.0 postmortem. | ||||||
| const RETIRED_BRANCH = /release\/v(?:X\.Y\.Z|\d+\.\d+\.\d+)-(?:rc|beta|alpha)\.(?:N|\d+)/g; | ||||||
| const LEGACY_ALLOWED = new Set(["technical-documentation/architecture/decisions.md"]); | ||||||
|
|
||||||
| const REQUIRED = [ | ||||||
| "README.md", | ||||||
|
|
@@ -59,7 +82,7 @@ const REQUIRED = [ | |||||
| "testing/native-cursor-diagnostics.md", | ||||||
| ]; | ||||||
|
|
||||||
| // `--only a.md,b/c.md` limits both checks to those docs-relative paths, so a | ||||||
| // `--only a.md,b/c.md` limits both checks to those repo-relative paths, so a | ||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win Document the supported The parser only reads an argument that starts with This follows directly from the parser at Lines 79-80. Proposed fix-// `--only a.md,b/c.md` limits both checks to those repo-relative paths, so a
+// `--only=a.md,b/c.md` limits both checks to those repo-relative paths, so a📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||
| // task that owns a slice of the tree can gate on its slice alone. | ||||||
| const onlyArg = process.argv.find((a) => a.startsWith("--only=")); | ||||||
| const only = onlyArg ? new Set(onlyArg.slice("--only=".length).split(",")) : null; | ||||||
|
|
@@ -77,23 +100,24 @@ function walk(dir) { | |||||
| } | ||||||
|
|
||||||
| const errors = []; | ||||||
| const files = walk(DOCS); | ||||||
| const files = TREES.flatMap(walk); | ||||||
|
|
||||||
| for (const abs of REQUIRED) { | ||||||
| if (!owned(abs)) continue; | ||||||
| const rel = `technical-documentation/${abs}`; | ||||||
| if (!owned(rel)) continue; | ||||||
| const full = join(DOCS, abs); | ||||||
| let lines = -1; | ||||||
| try { | ||||||
| lines = readFileSync(full, "utf8").split("\n").length; | ||||||
| } catch { | ||||||
| errors.push(`missing: technical-documentation/${abs}`); | ||||||
| errors.push(`missing: ${rel}`); | ||||||
| continue; | ||||||
| } | ||||||
| if (lines < 30) errors.push(`stub (${lines} lines): technical-documentation/${abs}`); | ||||||
| if (lines < 30) errors.push(`stub (${lines} lines): ${rel}`); | ||||||
| } | ||||||
|
|
||||||
| for (const file of files) { | ||||||
| const rel = relative(DOCS, file).replaceAll("\\", "/"); | ||||||
| const rel = relative(ROOT, file).replaceAll("\\", "/"); | ||||||
| if (!owned(rel)) continue; | ||||||
| const text = readFileSync(file, "utf8"); | ||||||
|
|
||||||
|
|
@@ -123,6 +147,10 @@ for (const file of files) { | |||||
| for (const name of LEGACY) { | ||||||
| if (text.includes(name)) errors.push(`${rel}: mentions removed "${name}"`); | ||||||
| } | ||||||
|
|
||||||
| for (const [match] of text.matchAll(RETIRED_BRANCH)) { | ||||||
| errors.push(`${rel}: retired release-branch naming "${match}" (it is release/vX.Y.Z)`); | ||||||
| } | ||||||
| } | ||||||
|
|
||||||
| if (errors.length) { | ||||||
|
|
||||||
Uh oh!
There was an error while loading. Please reload this page.