From 2cadd21b7de67652969ea2a84a70341fffee7ae3 Mon Sep 17 00:00:00 2001 From: Etienne Lescot Date: Tue, 11 Aug 2026 00:05:26 +0200 Subject: [PATCH 1/4] ci(homebrew): stop reporting an unpublished cask as a green release MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The tap configuration lived in the job-level `if:`, so an unconfigured job resolved to `skipped` — which is green. Neither HOMEBREW_TAP_OWNER nor HOMEBREW_TAP_REPO has ever existed on this repository, so the cask has never been published once, on any release, with every run reporting success. Same failure as #148 for winget, and fixed the same way: the check moves into a step that names each missing piece in a warning. Manual replay also gains the tag guard aur-publish.yml already has. workflow_dispatch takes free text and the prerelease filter only covers the release event, so replaying v1.9.4-rc.2 would have published an RC as the stable cask. Refs #335 --- .github/workflows/update-homebrew-cask.yml | 50 ++++++++++++++++++- .../engineering/ci-workflows.md | 4 +- .../engineering/release-and-secrets.md | 4 ++ 3 files changed, 54 insertions(+), 4 deletions(-) diff --git a/.github/workflows/update-homebrew-cask.yml b/.github/workflows/update-homebrew-cask.yml index 26f04248..575bd1b0 100644 --- a/.github/workflows/update-homebrew-cask.yml +++ b/.github/workflows/update-homebrew-cask.yml @@ -16,14 +16,42 @@ 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 + if [[ -n "$TAP_OWNER" && -n "$TAP_REPO" && "$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- 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 }} @@ -34,11 +62,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 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 }} @@ -73,6 +114,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 }} @@ -109,6 +151,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 }} @@ -125,6 +168,7 @@ 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 }} @@ -132,6 +176,7 @@ jobs: path: tap - name: Write cask file + if: steps.config.outputs.configured == 'true' env: REPO: ${{ github.repository }} TAG: ${{ steps.meta.outputs.tag }} @@ -185,6 +230,7 @@ jobs: EOF - name: Commit and push to tap + if: steps.config.outputs.configured == 'true' working-directory: tap env: VERSION: ${{ steps.meta.outputs.version }} diff --git a/technical-documentation/engineering/ci-workflows.md b/technical-documentation/engineering/ci-workflows.md index 01152944..3dc9f782 100644 --- a/technical-documentation/engineering/ci-workflows.md +++ b/technical-documentation/engineering/ci-workflows.md @@ -120,12 +120,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 diff --git a/technical-documentation/engineering/release-and-secrets.md b/technical-documentation/engineering/release-and-secrets.md index 4adac7d4..565584ce 100644 --- a/technical-documentation/engineering/release-and-secrets.md +++ b/technical-documentation/engineering/release-and-secrets.md @@ -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-` — 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, which are unsigned and un-notarized (`mac.notarize: false`). A cask does not change that — `brew install --cask` runs the same Gatekeeper path as a manual download, so 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. From 9647a244573c77259ddfa0c6d04c89dd45e93215 Mon Sep 17 00:00:00 2001 From: Etienne Lescot Date: Tue, 11 Aug 2026 10:01:33 +0200 Subject: [PATCH 2/4] feat(packaging): add the upstream AppStream metadata Flathub requires Flathub will not take a MetaInfo file written by the packager, so the file has to exist upstream before any manifest can be submitted. It is also missing where it would already help: the shipped 1.9.2 deb installs a .desktop file and nine icon sizes and no /usr/share/metainfo/ at all, so GNOME Software and Discover have nothing to show but an icon. Component ID is com.getopenscreen.OpenScreen, not the Electron appId com.etiennelescot.openscreen: Flathub requires the ID to map to a domain the project controls, getopenscreen.com is that domain, and the Electron appId decides the userData path of every existing install. Nothing consumes the file yet, so CI validating it is the only thing between an edit here and a rejected submission months later. Caught its own first bug that way, in a container: an XML comment may not contain a double hyphen. Refs #335 --- .github/workflows/ci.yml | 20 +++ .../com.getopenscreen.OpenScreen.metainfo.xml | 136 ++++++++++++++++++ .../engineering/ci-workflows.md | 3 + 3 files changed, 159 insertions(+) create mode 100644 build/com.getopenscreen.OpenScreen.metainfo.xml diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 704a2129..ee9f3f0e 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -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 diff --git a/build/com.getopenscreen.OpenScreen.metainfo.xml b/build/com.getopenscreen.OpenScreen.metainfo.xml new file mode 100644 index 00000000..4281aafa --- /dev/null +++ b/build/com.getopenscreen.OpenScreen.metainfo.xml @@ -0,0 +1,136 @@ + + + + com.getopenscreen.OpenScreen + + OpenScreen + Record your screen and polish the demo + + CC0-1.0 + MIT + + + Etienne Lescot + + + +

+ 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. +

+

+ Recordings open straight into a timeline where the polish is the point: +

+
    +
  • Automatic zoom that follows the cursor, adjustable per region
  • +
  • Backgrounds, padding, rounded corners and shadows on the frame
  • +
  • Trim, cut and per-region playback speed
  • +
  • Webcam picture-in-picture, cursor smoothing and custom cursor themes
  • +
  • Local speech-to-text for captions — the model runs on your machine
  • +
  • Export to MP4 or GIF, rendered on the GPU
  • +
+

+ 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. +

+
+ + + openscreen.desktop + + https://getopenscreen.com/ + https://github.com/getopenscreen/openscreen/issues + https://github.com/getopenscreen/openscreen + https://github.com/getopenscreen/openscreen/blob/main/CONTRIBUTING.md + + + + + https://raw.githubusercontent.com/getopenscreen/openscreen/main/public/preview4.png + Editing a recording: zoom regions, video effects and export settings + + + https://raw.githubusercontent.com/getopenscreen/openscreen/main/public/preview3.png + The timeline, with trim, speed and zoom regions on separate tracks + + + + + AudioVideo + Video + Recorder + + + + screen recorder + screencast + screen capture + video editor + demo + webcam + + + + pointing + keyboard + + + + + + + + + +
diff --git a/technical-documentation/engineering/ci-workflows.md b/technical-documentation/engineering/ci-workflows.md index 3dc9f782..3ab5e98e 100644 --- a/technical-documentation/engineering/ci-workflows.md +++ b/technical-documentation/engineering/ci-workflows.md @@ -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`. From bdf74265c1c2518f653b03309cc42407b9722e24 Mon Sep 17 00:00:00 2001 From: Etienne Lescot Date: Tue, 11 Aug 2026 22:21:33 +0200 Subject: [PATCH 3/4] fix(ci): refuse a tap name brew cannot resolve MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The configuration step accepted any non-empty HOMEBREW_TAP_REPO, so `openscreen-tap` set configured=true — and the job would then clone, commit and push a cask to it successfully. Green run, cask published, nothing installable: `brew tap getopenscreen/openscreen` expands to `getopenscreen/homebrew-openscreen`, which is a different repository. That is the same shape as the bug this workflow change exists to fix. The step already names the homebrew- rule in its warning, and the release documentation already spells out that a wrongly-named tap "would be checked out and pushed to successfully and still be untappable" — the check just never applied the rule it was describing. Now it does. `homebrew-?*` rather than the prefix alone, so a repository named the bare `homebrew-` is refused too. A name with a capital H is refused as well, which Homebrew would in fact resolve; the cost of that strictness is a warning naming the convention, not a silent mispublish. --- .github/workflows/update-homebrew-cask.yml | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/.github/workflows/update-homebrew-cask.yml b/.github/workflows/update-homebrew-cask.yml index 575bd1b0..8c867d01 100644 --- a/.github/workflows/update-homebrew-cask.yml +++ b/.github/workflows/update-homebrew-cask.yml @@ -41,7 +41,14 @@ jobs: id: config run: | set -euo pipefail - if [[ -n "$TAP_OWNER" && -n "$TAP_REPO" && "$HAS_TOKEN" == "true" ]]; then + # `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 From 588fa1cc4246b2964f2d240c3f73edf150098c62 Mon Sep 17 00:00:00 2001 From: Etienne Lescot Date: Tue, 11 Aug 2026 22:21:54 +0200 Subject: [PATCH 4/4] docs(release): stop calling the notarized DMGs unsigned MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The cask note read `mac.notarize: false` out of electron-builder.json5 and reported it as the state of the shipped artifact. That field only says electron-builder does not notarize; build.yml does it afterwards by hand — `Sign DMG`, `Notarize DMG`, `Staple notarization ticket`, `Validate stapled DMG`, all four gated on the Apple credentials being present, on every tag including pre-releases. So the paragraph contradicted its own file: sixty-seven lines above, the Apple signing section already says every tag signs, notarizes, staples and validates, and falls back to an ad-hoc signature only when a value is missing. Both states are now named, and the quarantine step is attached to the one that actually needs it. --- technical-documentation/engineering/release-and-secrets.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/technical-documentation/engineering/release-and-secrets.md b/technical-documentation/engineering/release-and-secrets.md index 565584ce..ffbd4e82 100644 --- a/technical-documentation/engineering/release-and-secrets.md +++ b/technical-documentation/engineering/release-and-secrets.md @@ -149,7 +149,7 @@ The bot token comes from a Discord application authorized with the `bot` scope. **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-` — 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, which are unsigned and un-notarized (`mac.notarize: false`). A cask does not change that — `brew install --cask` runs the same Gatekeeper path as a manual download, so users still need the `xattr -rd com.apple.quarantine` step the README documents. What the tap buys is discovery and `brew upgrade`, not trust. +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`.