Skip to content
Merged
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
20 changes: 20 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,26 @@ jobs:
node-version: 22
- run: npm run docs:check

appstream:
name: AppStream metadata
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
# `appstream` is not on the runner image, and it is the only thing this job
# needs — no Node, no npm ci. Same reasoning as the Docs job above.
- run: sudo apt-get update -qq && sudo apt-get install -y -qq appstream
# This gate exists because nothing else reads the file. It is upstream
# metadata with no consumer in this repository yet, so without validation
# here the first thing to discover a broken edit would be a Flathub
# reviewer, weeks later, on a submission that then has to be redone.
#
# --no-net deliberately: with network validation on, this job also fetches
# every screenshot, and an unreachable raw.githubusercontent.com would turn
# a metadata check into a flaky one on unrelated PRs. Dead screenshot URLs
# are caught by Flathub at submission time, which is the only place the
# answer actually matters.
- run: appstreamcli validate --no-net --explain build/com.getopenscreen.OpenScreen.metainfo.xml

test:
name: Test
runs-on: ubuntu-latest
Expand Down
57 changes: 55 additions & 2 deletions .github/workflows/update-homebrew-cask.yml
Original file line number Diff line number Diff line change
Expand Up @@ -16,14 +16,49 @@ permissions:
jobs:
update-cask:
runs-on: ubuntu-latest
if: (github.event_name == 'workflow_dispatch' || !github.event.release.prerelease) && vars.HOMEBREW_TAP_OWNER != '' && vars.HOMEBREW_TAP_REPO != ''
# The tap configuration has LEFT this `if:`, and that is the point of the change.
# `vars.HOMEBREW_TAP_OWNER != '' && vars.HOMEBREW_TAP_REPO != ''` here made the job
# `skipped`, and a skipped job is green: every release since this workflow was
# written has "succeeded" without publishing a cask, because neither variable has
# ever existed on the repository. Same failure as #148, where publish-winget.yml
# spent eight releases green and silent for exactly this reason — see the comment
# above its own `if:`. A guard that does nothing quietly guards nothing.
#
# So the job always starts, and a step announces the missing configuration. It costs
# a runner-minute per release; it buys "nothing was published" being visible in the
# run summary instead of inferable from a tap nobody thought to look at.
if: github.event_name == 'workflow_dispatch' || !github.event.release.prerelease
env:
TAP_OWNER: ${{ vars.HOMEBREW_TAP_OWNER }}
TAP_REPO: ${{ vars.HOMEBREW_TAP_REPO }}
CASK_NAME: ${{ vars.HOMEBREW_CASK_NAME || 'openscreen' }}
# `secrets` is not a context an `if:` can read, at job level or step level — only
# `env` is. Hence this boolean-as-string, which exposes whether the token is set
# without ever exposing its value. Same trick as publish-winget.yml.
HAS_TOKEN: ${{ secrets.HOMEBREW_TAP_TOKEN != '' }}
steps:
- name: Resolve tag and version
- name: Check the tap configuration
id: config
run: |
set -euo pipefail
# `homebrew-?*`, not `-n`: the fourth requirement in the warning below is a
# rule about the name itself, and a name test is the one of the four this step
# can actually apply. `getopenscreen/openscreen-tap` would clone, commit and
# push exactly like a real tap and still be untappable — a green run publishing
# to somewhere `brew tap` cannot resolve, which is the failure this whole
# workflow change exists to stop. `?*` also rejects a repository named the bare
# `homebrew-`, which the prefix alone would accept.
if [[ -n "$TAP_OWNER" && "$TAP_REPO" == homebrew-?* && "$HAS_TOKEN" == "true" ]]; then
echo "configured=true" >> "$GITHUB_OUTPUT"
echo "Tap: ${TAP_OWNER}/${TAP_REPO}, cask ${CASK_NAME}."
exit 0
fi
echo "configured=false" >> "$GITHUB_OUTPUT"
echo "::warning title=Homebrew cask not updated::No cask was published. Needs (1) the repository variable HOMEBREW_TAP_OWNER, currently ${TAP_OWNER:-UNSET}; (2) HOMEBREW_TAP_REPO, currently ${TAP_REPO:-UNSET}; (3) the secret HOMEBREW_TAP_TOKEN, currently $([[ "$HAS_TOKEN" == "true" ]] && echo set || echo UNSET), with contents write on that repository; and (4) the tap repository itself, which must be named homebrew-<something> for Homebrew to recognise it. See https://github.com/getopenscreen/openscreen/issues/335"

- name: Resolve and validate tag
id: meta
if: steps.config.outputs.configured == 'true'
env:
GH_EVENT_TAG: ${{ github.event.release.tag_name }}
INPUT_TAG: ${{ inputs.tag }}
Expand All @@ -34,11 +69,24 @@ jobs:
echo "::error::No tag resolved from release event or workflow input"
exit 1
fi
# The `prerelease` filter only covers the `release` event. `workflow_dispatch`
# takes free text — no git ref rule applies to it — so a manual replay of
# `v1.9.4-rc.2` would publish an RC as THE cask, and `brew upgrade` would hand
# it to everyone on stable. Homebrew accepts that version string happily; only
# this check refuses it. The same hole was closed in aur-publish.yml, where the
# free-text input also escaped a `sed` expression; here every use is quoted, so
# what is left is the wrong-version case and the tag reaching a Ruby file that
# users execute.
if [[ ! "$TAG" =~ ^v[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
echo "::error::Refusing tag '$TAG' — expected a stable vMAJOR.MINOR.PATCH tag"
exit 1
fi
Comment thread
coderabbitai[bot] marked this conversation as resolved.
VERSION="${TAG#v}"
echo "tag=$TAG" >> "$GITHUB_OUTPUT"
echo "version=$VERSION" >> "$GITHUB_OUTPUT"

- name: Wait for release DMG assets
if: steps.config.outputs.configured == 'true'
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
TAG: ${{ steps.meta.outputs.tag }}
Expand Down Expand Up @@ -73,6 +121,7 @@ jobs:

- name: Find macOS DMG assets
id: assets
if: steps.config.outputs.configured == 'true'
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
TAG: ${{ steps.meta.outputs.tag }}
Expand Down Expand Up @@ -109,6 +158,7 @@ jobs:

- name: Download DMGs and compute sha256
id: shas
if: steps.config.outputs.configured == 'true'
env:
REPO: ${{ github.repository }}
TAG: ${{ steps.meta.outputs.tag }}
Expand All @@ -125,13 +175,15 @@ jobs:
echo "x64_sha=$X64_SHA" >> "$GITHUB_OUTPUT"

- name: Checkout tap
if: steps.config.outputs.configured == 'true'
uses: actions/checkout@v7
with:
repository: ${{ env.TAP_OWNER }}/${{ env.TAP_REPO }}
token: ${{ secrets.HOMEBREW_TAP_TOKEN }}
path: tap

- name: Write cask file
if: steps.config.outputs.configured == 'true'
env:
REPO: ${{ github.repository }}
TAG: ${{ steps.meta.outputs.tag }}
Expand Down Expand Up @@ -185,6 +237,7 @@ jobs:
EOF

- name: Commit and push to tap
if: steps.config.outputs.configured == 'true'
working-directory: tap
env:
VERSION: ${{ steps.meta.outputs.version }}
Expand Down
136 changes: 136 additions & 0 deletions build/com.getopenscreen.OpenScreen.metainfo.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--
Upstream AppStream metadata. This is what a software centre reads to show the
app as something other than a bare icon and a filename: name, summary,
description, licence, screenshots, release history.

It lives here rather than in a packaging repository on purpose. Flathub
requires the MetaInfo file to come from upstream — a reviewer will not accept
one written by the packager — and the same file serves the deb and rpm, whose
/usr/share/metainfo/ is empty today (checked against the shipped 1.9.2 deb:
it installs a .desktop file, nine icon sizes, and nothing else). So it is
authored once here and consumed by whatever packaging exists later.

The component ID is com.getopenscreen.OpenScreen, NOT the Electron appId
com.etiennelescot.openscreen. Two different namespaces, and only one of them
has to satisfy Flathub's rule that the ID map to a domain the project
controls: getopenscreen.com is the project's site, etiennelescot.com is not a
domain anyone here owns (the maintainer's is .fr). Changing the Electron
appId is not on the table — it decides the userData directory of every
existing install.

Editing this file: `appstreamcli validate` is what CI runs, and it is worth
running before pushing. Note that an XML comment may not contain a double
hyphen, so the flags CI passes are spelled out in ci.yml and not here — this
very paragraph failed validation once for naming them. Nothing consumes the
file yet — the Flatpak manifest that will is not written — so CI validating it
is the only thing standing between an edit here and a submission rejected for
it months later.
-->
<component type="desktop-application">
<id>com.getopenscreen.OpenScreen</id>

<name>OpenScreen</name>
<summary>Record your screen and polish the demo</summary>

<metadata_license>CC0-1.0</metadata_license>
<project_license>MIT</project_license>

<developer id="com.getopenscreen">
<name>Etienne Lescot</name>
</developer>

<description>
<p>
OpenScreen records your screen, webcam and audio, then hands you an editor
built for the thing you actually wanted: a demo worth watching. No
watermark, no subscription, no upload step.
</p>
<p>
Recordings open straight into a timeline where the polish is the point:
</p>
<ul>
<li>Automatic zoom that follows the cursor, adjustable per region</li>
<li>Backgrounds, padding, rounded corners and shadows on the frame</li>
<li>Trim, cut and per-region playback speed</li>
<li>Webcam picture-in-picture, cursor smoothing and custom cursor themes</li>
<li>Local speech-to-text for captions — the model runs on your machine</li>
<li>Export to MP4 or GIF, rendered on the GPU</li>
</ul>
<p>
Screen capture goes through the desktop portal, so it works on Wayland and
X11 alike and asks for permission the way the rest of your desktop does.
</p>
</description>

<!--
The desktop file electron-builder generates and installs is openscreen.desktop
(verified in the shipped deb, /usr/share/applications/openscreen.desktop).
A Flatpak build renames both that file and this one to the component ID, as
Flathub requires, and has to rewrite this launchable to match — the rename is
not optional there and a stale launchable makes the app unlaunchable from the
software centre.
-->
<launchable type="desktop-id">openscreen.desktop</launchable>

<url type="homepage">https://getopenscreen.com/</url>
<url type="bugtracker">https://github.com/getopenscreen/openscreen/issues</url>
<url type="vcs-browser">https://github.com/getopenscreen/openscreen</url>
<url type="contribute">https://github.com/getopenscreen/openscreen/blob/main/CONTRIBUTING.md</url>

<!--
Pinned to `main` rather than to a tag: a tagged URL is stable but goes stale
the moment the UI changes, and a screenshot of a two-year-old build is worse
than a moving one. Both files are tracked in this repository, so the URLs
stay ours. Flathub fetches these at build time and fails the submission if
either 404s.
-->
<screenshots>
<screenshot type="default">
<image type="source">https://raw.githubusercontent.com/getopenscreen/openscreen/main/public/preview4.png</image>
<caption>Editing a recording: zoom regions, video effects and export settings</caption>
</screenshot>
<screenshot>
<image type="source">https://raw.githubusercontent.com/getopenscreen/openscreen/main/public/preview3.png</image>
<caption>The timeline, with trim, speed and zoom regions on separate tracks</caption>
</screenshot>
</screenshots>

<categories>
<category>AudioVideo</category>
<category>Video</category>
<category>Recorder</category>
</categories>

<keywords>
<keyword>screen recorder</keyword>
<keyword>screencast</keyword>
<keyword>screen capture</keyword>
<keyword>video editor</keyword>
<keyword>demo</keyword>
<keyword>webcam</keyword>
</keywords>

<supports>
<control>pointing</control>
<control>keyboard</control>
</supports>

<!-- Empty element means every OARS category is "none", which is the honest
answer for a screen recorder: it renders what the user recorded. -->
<content_rating type="oars-1.1" />

<!--
ponytail: one hand-written entry, the release published when this file was
written. It exists because validators warn on a component with no release
history at all, not because anyone will remember to append to it — and a
version restated by hand is exactly what nix/package.nix refuses to do,
having watched one drift two minors behind. Upgrade path: whatever packaging
ends up consuming this file injects the entry from package.json at build
time, and this block goes away. Until something consumes it, a stale date
here is visible to nobody.
-->
<releases>
<release version="1.9.2" date="2026-08-10" />
</releases>
</component>
7 changes: 5 additions & 2 deletions technical-documentation/engineering/ci-workflows.md
Original file line number Diff line number Diff line change
Expand Up @@ -86,8 +86,11 @@ The STT workflow uploads standalone archives for binary refresh and does not cur
| `typecheck` | Ubuntu | `npx tsc --noEmit` |
| `test` | Ubuntu | Vitest unit tests, Chromium installation, then browser-mode Vitest |
| `build` | Ubuntu | `npx vite build`; this is not electron-builder packaging |
| `appstream` | Ubuntu | `appstreamcli validate` on `build/com.getopenscreen.OpenScreen.metainfo.xml` |
| `semantic-pr` | Ubuntu | Validates Conventional Commit-style PR titles |

`build/com.getopenscreen.OpenScreen.metainfo.xml` is upstream AppStream metadata: the name, summary, description, licence, screenshots and release history a software centre shows instead of a bare icon. Nothing in this repository consumes it yet — the shipped deb installs a `.desktop` file and nine icon sizes and no `/usr/share/metainfo/` at all — so the `appstream` job is the only thing that can catch a broken edit before a Flathub reviewer does. Its component ID is `com.getopenscreen.OpenScreen`, deliberately not the Electron `appId` `com.etiennelescot.openscreen`: Flathub requires the ID to map to a domain the project controls, and `getopenscreen.com` is that domain.

Jobs that need the root dependencies use `.github/actions/setup`, which requests Node 22 and runs `npm ci`; callers perform checkout themselves.

`docs.yml` is separate from the technical-documentation checker. It installs dependencies in `website/`, type-checks and builds the site, uploads a Pages artifact, and deploys only after a push to `main`.
Expand Down Expand Up @@ -120,12 +123,12 @@ At a high level, the RC workflow creates or reuses `release/vX.Y.Z`, tags its ti

These workflows run for stable published releases and support manual replay with a tag:

- `update-homebrew-cask.yml` waits for both macOS DMGs, hashes them, writes a cask, and pushes to the configured tap.
- `update-homebrew-cask.yml` waits for both macOS DMGs, hashes them, writes a cask, and pushes to the configured tap. Manual replay refuses any tag that is not a stable `vMAJOR.MINOR.PATCH`, because `workflow_dispatch` takes free text and the `prerelease` filter only covers the `release` event.
- `publish-winget.yml` passes the matching NSIS release asset to `winget-releaser`.
- `bump-nix-package.yml` computes `npmDepsHash`, updates `nix/package.nix`, and opens a PR.
- `aur-publish.yml` hashes the pacman release asset, updates `PKGBUILD` and `.SRCINFO`, and pushes over SSH.

Each workflow gates itself on its required variables or credentials. `bump-nix-package.yml` uses the repository `GITHUB_TOKEN`; the others require external registry credentials described in [release and secrets](release-and-secrets.md).
Each workflow needs variables or credentials, and where it checks for them decides whether a missing one is visible. `update-homebrew-cask.yml` and `publish-winget.yml` check inside a step that emits a warning, so an unconfigured channel says so in the run summary; a job-level `if:` would instead report `skipped`, which reads as green and hid #148 for eight releases and the Homebrew cask for its entire existence (#335). `bump-nix-package.yml` uses the repository `GITHUB_TOKEN`; the others require the external registry credentials described in [release and secrets](release-and-secrets.md).

## Tier 4: automation and diagnostics

Expand Down
4 changes: 4 additions & 0 deletions technical-documentation/engineering/release-and-secrets.md
Original file line number Diff line number Diff line change
Expand Up @@ -147,6 +147,10 @@ The bot token comes from a Discord application authorized with the `bot` scope.

`bump-nix-package.yml` uses the workflow-scoped `GITHUB_TOKEN`; it requires repository contents and pull-request write permissions as declared in the workflow and has no additional long-lived secret.

**Homebrew publishing does not complete yet, and now says so.** `update-homebrew-cask.yml` has never published a cask — not once since it was written for the v1.5.0 pipeline. Neither `HOMEBREW_TAP_OWNER` nor `HOMEBREW_TAP_REPO` has ever existed on this repository, both sat in the job-level `if`, and an unconfigured job resolves to `skipped`, which is green: every release run reads as a success. The same failure as WinGet below, found the same way and fixed the same way — the configuration test now lives in a step that names what is missing (#335). Three things are needed, and the third is the one a variable cannot supply: `HOMEBREW_TAP_OWNER` and `HOMEBREW_TAP_REPO`; the `HOMEBREW_TAP_TOKEN` secret with contents write on that repository; and the tap repository itself, which **must** be named `homebrew-<something>` — that prefix is how `brew tap` resolves a repository at all, so `getopenscreen/openscreen-tap` would be checked out and pushed to successfully and still be untappable. With `getopenscreen/homebrew-openscreen`, the install command is `brew install --cask getopenscreen/openscreen/openscreen`.

Note what it would publish before turning it on: the two DMGs attached to the release — signed, notarized and stapled when the Apple credentials above are complete, ad-hoc-signed and un-notarized when they are not. A cask does not change either state, because `brew install --cask` runs the same Gatekeeper path as a manual download: on the ad-hoc artifact users still need the `xattr -rd com.apple.quarantine` step the README documents. What the tap buys is discovery and `brew upgrade`, not trust.

**WinGet publishing does not complete yet, and now says so.** `publish-winget.yml` starts on every stable release; whether it publishes depends on four prerequisites, and it names the missing ones in a `::warning::` instead of passing quietly. It used to pass quietly: the configuration test sat in the job-level `if`, an unconfigured job resolved to `skipped`, and a skipped job is green — so eight releases in a row reported success while publishing nothing, which is how #148 stayed open without anyone noticing. The four are: `WINGET_IDENTIFIER` (set, `OpenScreen.OpenScreen`); `WINGET_ACC_TOKEN` (absent — it must be a *classic* PAT with `public_repo`, since `winget-releaser` does not support fine-grained ones); a fork of `microsoft/winget-pkgs` under `getopenscreen`, which is where the action pushes its branch; and at least one version of the package already merged into `winget-pkgs`, because the action writes each manifest from the previous one and refuses to author the first. That first submission is manual, via `wingetcreate new`.

Note what it would publish before turning it on: `winget-releaser` submits the **NSIS `.exe`** attached to the release to the community repository, and that installer is unsigned. Users who install through the Microsoft Store, or through `winget --source msstore`, get the Store package that Microsoft signs during certification instead. Publishing to the community source therefore adds a second, unsigned route alongside the signed one — worth doing deliberately rather than by flipping a variable.
Expand Down
Loading