Skip to content

chore(release): v0.16.0 - #44

Merged
arximus88 merged 18 commits into
devfrom
staging
Aug 7, 2026
Merged

chore(release): v0.16.0#44
arximus88 merged 18 commits into
devfrom
staging

Conversation

@arximus88

Copy link
Copy Markdown
Owner

Release PR for v0.16.0. Tag is held locally and gets pushed only after this merges.

Electron 43.3.0 (Chromium 150)

The project was pinned to 42.0.1 because 42.3.0 broke /app_auth/redeem. Verified against the bundled binary — the one electron-builder ships, not a distro rebuild: first login on clean storage completes, the session survives restarts, image paste is unaffected by 43's sRGB normalisation in toBitmap(), and window decorations are unchanged despite the rounded-corners default on Linux CSD.

NixOS support

Flake from @iamcalledrob (#42), reworked so version and both platform hashes live in one release block that CI rewrites as a unit after each release. The new flake job pushes to dev first — where Nix resolves the input from — and mirrors to staging.

Fixes found during manual testing of this branch

  • Export queue (Unable to use certain plugins and features that require external websites #41) — Figma Motion's queue opened empty and stayed empty until you switched tabs away and back. A CDP session ruled out visibility, size, rAF and the preload bridge; reload, resize, maximise and window focus all failed to fix it. The tab is now held detached until did-finish-load and attached after, which is the ordering the manual switch was really providing.
  • Zenity dialogs--ellipsize cut prompts off mid-sentence, --text was dropped entirely for dialogs without a detail line, and the cancel button read "No" where the native provider says "Cancel".
  • WebGPU now follows the toggle on X11 instead of being forced on for every X11 session.
  • AUR PKGBUILD license is rewritten from package.json each release — it was hardcoded MIT while the project is GPL-2.0-only.

Test baselines

frame-signature failed on both styles: 1168px recorded, 1200px rendered. Not a regression — the value reproduces nowhere, including at the commit that wrote the file, under Electron 42.8 and 43.3, under xvfb and on a live display. 1200px is exactly WINDOW_DEFAULT_OPTIONS.width. Refreshed; the diff is four viewport-derived widths, structure untouched.

Checks

bun test 214 pass · bun run test:e2e 20 pass · tsc --noEmit clean · biome check clean · svelte-check 0 errors

Manually exercised on a live session: login, image paste, both frame styles, WebGPU toggle, fonts, warm tab, export queue, Zenity and native dialogs.

Rob Mason and others added 18 commits July 29, 2026 23:29
enable-unsafe-webgpu was applied on any X11 session (!isWayland),
leaving WebGPU silently ON for X11 users who had the enableWebGPU
toggle OFF — contradicting the setting. Figma probes navigator.gpu
(exposed by enable-unsafe-webgpu) to switch on its Shader/Halftone/
Noise effects, so the flag belongs behind the toggle alongside the
shaders.

Now gate it on shadersActive (X11 + enableWebGPU) with the existing
userForcesVulkan escape hatch preserved. Only the X11 + toggle-off
quadrant changes; X11/Wayland toggle-on and Wayland toggle-off are
unaffected. The change is scoped to WebGPU only — ignore-gpu-blocklist
and enable-webgl stay on X11, so the normal WebGL canvas keeps its
hardware acceleration; a toggle-off X11 user loses only the shader
effects.

Update the truth-table tests to lock all four quadrants.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QWRSUEUKBsLy4paZR4noBb
"Export video" calls window.open("https://www.figma.com/export/<fileKey>?fuid=…").
/export/ was missing from isFigmaRunUrl's validPaths, so the URL fell through to
shell.openExternal and landed in the system browser — where the render queue is
always empty, because it lives in the app's session. Logging in there does not
help, as reported in #41.

- add /^\/export\// to validPaths so the queue opens as an in-app tab
- give export tabs their own dedup prefix (export:<key>), so a repeated
  "Export video" click reuses the queue tab and cannot collide with the
  design tab for the same file
- log every window.open with url/frameName/disposition/features — Tab had no
  logging at all here, which is why this took a live capture to find
- route only http(s) to shell.openExternal; other schemes were passed through
  as a silent no-op and are now logged as blocked

Verified against a live capture: the export URL above, and the Linear plugin's
OAuth (response_type=stringauth, redirect_uri=figma-plugin.linear.app/callback),
which completes in the browser and returns through Linear's own callback.

Refs #41
The flake read its version from package.json but carried hard-coded sha256
hashes, so every `perl scripts/bump_version.pl` left it naming a release whose
binaries it could not hash — `nix build` then failed on a hash mismatch until
someone recomputed both by hand. The PR shipped with a "Chore: update hashes
when release binaries change" comment; this replaces the comment with the
mechanism.

- flake.nix: version and both hashes live in one `release` block, so they are
  rewritten as a unit and cannot drift
- scripts/update_flake_release.py: rewrites that block, converting sha256sum
  hex digests to the SRI form Nix expects; fails loudly if the layout moved
- release.yml: new `flake` job hashes both release zips after the release is
  published and commits the pinned flake to staging (dev is protected, so the
  flake there trails by one release)
- README: note that the flake installs prebuilt binaries and how to pin a tag
- CLAUDE.md, build skill: document that flake.nix is CI-owned, never bumped
  by hand

Not verified with `nix build` — no Nix on this machine.

Follow-up to #42
Nix resolves `github:arximus88/figma-linux-next` from the default branch, which
is dev. Committing the release hashes only to staging meant NixOS installed
whatever dev still named — always the previous release, since staging reaches
dev with the next release PR.

The flake job now checks out dev with RELEASE_PAT (fine-grained, Contents:write
on this repo only) and pushes there first, then mirrors the same change to
staging. The default GITHUB_TOKEN cannot do this: it is refused by the branch
protection on dev.

The job still runs last and depends on `release`, so a rejected push leaves the
release, its binaries and both AUR packages already published — the only
casualty is the flake update, and the failure is visible in the run.

Token expiry (2027-08-05) is recorded in CLAUDE.md and the build skill, because
a 403 on that push is the sole symptom and would otherwise be a blind diagnosis.
The gemini-cli copy under .agent was byte-identical to the global
~/.claude/skills/gemini-cli. The .gemini skills duplicated capabilities
already available (frontend-design, the Figma MCP workflows, generic
refactoring smells) or contradicted this project: electron-app-planning
prescribes raw ipcMain.handle and electron-updater, neither of which
applies here. .gemini/settings.json listed the same MCP servers as
.mcp.json plus figma-remote.

One item survives as a CLAUDE.md note instead of a file:
`bunx @sveltejs/mcp svelte-autofixer <file>` catches Svelte 5 rune
antipatterns that svelte-check misses, which matters because Biome
does not lint .svelte.

The gitignore "Agents" block matched only .gemini/settings.*, so the
skills stayed tracked despite it; widened to .gemini and dropped the
duplicate .agent/skills entry.
… Nix section

The Nix material was already current — the skill was written alongside
358ada0/75b23e4. Verifying it line by line against release.yml,
update_flake_release.py and flake.nix surfaced five other divergences:

- The release checklist pushed the tag together with the branch, ahead of
  the PR steps, contradicting the rule stated at the top of the same file.
  Following it literally would publish a release before dev was updated.
- push_aur_dev_git.yml was listed as an existing workflow; 3c3a762 deleted it.
- RELEASE_PAT was described in prose but missing from the secrets table,
  so its 2027-08-05 expiry was not where someone would look for it.
- The aur/aur-bin/flake pre-release guard from e011888 was undocumented:
  an -rc tag produces a GitHub Release and no AUR or flake update.
- Key Files listed no flake file at all — neither flake.nix nor
  scripts/update_flake_release.py.
CLAUDE.md:
- Critical Files pointed at src/utils/Render/frameConfig.ts and
  frameStyles.ts; neither exists. The icon/component config is
  frameTheme.ts and the frames themselves are Svelte components under
  src/renderer/Panel/frames/.
- Utils/Render/defaultSettings.ts was called the authoritative settings
  schema. It is a four-line re-export whose own comment calls itself a
  placeholder; the values live in Utils/Common/defaultSettings.ts as
  BASE_DEFAULT_SETTINGS. Documented all three same-named files so
  "add a new setting" lands in the right one.
- The path-alias example imported a symbol named `defaultSettings`,
  which nothing exports.
- FrameStyle was listed as three values; the union in
  src/types/Common/index.d.ts has four (kde was missing) and the
  default is gnome.
- ci.yml was described as PRs to dev; it runs on push and PR to both
  staging and dev.

README.md:
- The AppImage snippet globbed figma-linux-next-*.AppImage with a dash.
  electron-builder names artifacts ${name}_${version}_${platform}_${arch},
  so the glob matched nothing.
- The NixOS section set programs.figma-linux-next.enable without
  importing nixosModules.default, which fails evaluation with an
  undefined option.
Both AUR packages publish license=('MIT') while the project is
GPL-2.0-only — LICENSE is GNU GPL v2, and package.json, src/package.json
and flake.nix all say so. Verified against the live AUR RPC:
figma-linux-next 0.15.0-1 and figma-linux-next-bin both report
License: ["MIT"].

The line drifted because the aur and aur-bin jobs rewrite pkgver, pkgrel
and sha256sums and nothing else, so whatever license the AUR repo was
seeded with survived every release. Deriving it from package.json means
the two can no longer disagree; the value there is already an exact SPDX
identifier, so it drops into the PKGBUILD verbatim.

python3 is used rather than jq because the archlinux container installs
python and not jq.

Verified: actionlint clean, YAML parses, and the sed produces
license=('GPL-2.0-only') against a real PKGBUILD.
A clone under /home/arx/aur/ had drifted five releases behind live AUR
(0.13.1 vs 0.15.0) while still looking like a working copy. Pushing it
would have rolled both packages back, and CI would not have caught it —
each run clones whatever AUR currently holds and edits that in place.

Also records which fields CI rewrites (now including license) versus the
ones that only change by hand, and that AUR maintenance takes down the
SSH endpoint alone: push fails while the web UI and HTTPS git stay up.
The project sat on 42.0.1 because 42.3.0 broke `/app_auth/redeem` — Figma
rejected the request headers from that Chromium roll and returned login HTML
instead of Set-Cookie, killing first-login and add-account.

Verified against the bundled 43.3.0 binary (Chromium 150.0.7871.212), which is
what electron-builder ships: first login on clean storage completes, the
figma:// redeem redirect lands, and the session survives restarts. Image paste
and frameless window decorations also check out, covering the two behaviour
changes in 43 that touch us — sRGB normalisation in nativeImage.toBitmap() and
rounded corners on Linux CSD.

Chromium 148 -> 150 also brings three months of security fixes, a faster boot
(startup snapshot for main, bytecode-cached preloads) and a newer WebGPU stack.

CLAUDE.md now records how to run the OAuth test correctly: use the bundled
binary rather than a distro Electron, carry mimeapps.list into the isolated
config, and shadow the installed .desktop so the redirect comes back.
Electron 43, NixOS packaging, Figma Motion export fix
Both signatures failed on the panel root: 1168px recorded, 1200px rendered.
The gap is not a regression — the value does not reproduce anywhere. Checked
against Electron 42.8.0 and 43.3.0, under xvfb and on a live Wayland session,
and at e761686, the very commit that wrote the file: every combination renders
1200px, which is exactly WINDOW_DEFAULT_OPTIONS.width. That default has not
changed in years, so the recorded 1168px came from an environment we no longer
have — most likely an Electron 42.x patch that aligned the frameless client
area with the requested window size.

The refreshed files differ by four lines, all of them viewport-derived widths
(root 1168->1200, tab strip 911->943). Structure, colours and spacing are
untouched, so the oracle keeps its value.

Verified passing both under xvfb and on a real display after the update.
Found while exercising the Zenity provider by hand against the restart prompt.
Three defects, all in both showMessageBox and showMessageBoxSync:

--ellipsize was passed on every dialog. It exists to keep the window small on
long text, and it did that by cutting the sentence off: the restart prompt read
"Figma needs to be restarted to app…", so the user never saw which setting
required the restart. Replaced with --width=460, which bounds the window and
wraps instead.

--text was pushed only when `detail` was set, so a message-only dialog rendered
with an empty body — the message was dropped on the floor. It is now always
passed, with detail appended when present.

A zenity --question labels its reject button "No" while the native provider
says "Cancel", so the same prompt read differently depending on the provider.
The cancel label now defaults to "Cancel".

Verified by hand: the restart prompt shows the full detail line and Restart /
Cancel buttons under the Zenity provider.
Opening Figma Motion's video-export queue left it stuck on "Your rendering
queue is empty" even though the render was queued and progressing. Switching
tabs away and back was the only cure.

A CDP session on the stuck page ruled out every visible explanation. It is
indistinguishable from a working one: document.visibilityState is "visible"
and no visibilitychange ever fires, innerWidth/innerHeight are identical
before and after the switch, requestAnimationFrame runs at 60fps, and the
preload bridge is installed. Reloading the tab, resizing the window,
maximising it and refocusing it all fail; only removeChildView followed by
addChildView works. Doing that pair programmatically right after load did
nothing either — Chromium collapses a detach/attach that happens in one
synchronous block, so the layer is never rebuilt.

What the manual switch actually provides is order: the page finishes loading
while detached, and is attached afterwards. So the tab is now held back
instead of nudged. It loads in the background at 0x0 like a warm tab and is
attached once did-finish-load arrives. The tab strip is unaffected — addTab
already sent didTabAdd — only the view is deferred, with a timeout so a
failed load cannot leave the tab hidden.

isExportQueueUrl keeps the deferral scoped to the queue; a design file with
"export" in its slug is not affected, which the test covers.
@arximus88 arximus88 closed this Aug 7, 2026
@arximus88 arximus88 reopened this Aug 7, 2026
@arximus88
arximus88 merged commit 5b53534 into dev Aug 7, 2026
1 of 3 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants