Skip to content

fix(release): make unsigned Windows artifacts reproducible - #4690

Merged
Astro-Han merged 6 commits into
mainfrom
fix/windows-reproducible-payload
Sep 4, 2026
Merged

fix(release): make unsigned Windows artifacts reproducible#4690
Astro-Han merged 6 commits into
mainfrom
fix/windows-reproducible-payload

Conversation

@Astro-Han

Copy link
Copy Markdown
Contributor

Summary

Make independently built unsigned Windows x64 payloads byte-identical while keeping latest.yml.releaseDate as real publication metadata.

  • exclude node-pty build projects and link intermediates without dropping its runtime files
  • make shipped PE files deterministic with the minimal /Brepro /DEBUG:NONE linker flags
  • omit input modification/access times from electron-builder ZIPs and NSIS installers
  • exercise the real electron-builder, 7-Zip, and NSIS paths in regression tests

Refs #3276 and #3414. This does not add signing credentials or claim trusted-hardware release validation.

Verification

  • npm run build
  • npm run check:release — 194 tests passed
  • npm run test:windows-archives — 2 tests passed
  • npm run format
  • npm run lint
  • actionlint .github/workflows/release-windows-check.yml
  • Windows dual-build ablation: both clean builds and packaged-release verifiers passed; EXE, ZIP, blockmap, and unpacked payload bytes matched with /INCREMENTAL:NO removed. The comparison job reports only latest.yml and the non-distributed builder-debug.yml.
  • Negative control: removing /DEBUG:NONE reintroduced four unpacked-payload differences, so /Brepro /DEBUG:NONE is the minimal linker set.

AI use

Select exactly one:

  • No generative tool made a substantive contribution
  • Generative tooling made a substantive contribution

Tool(s) and scope: Codex diagnosed the byte differences, implemented the packaging changes and regression tests, and ran the validation and ablation experiments. Every affected commit retains a Generated-by: Codex trailer.

Checklist

  • Tests cover the change and fail without it
  • Lint, format, typecheck and the affected suites pass locally

Does this PR entail a change in behavior?

  • Yes — described under Summary above
  • No

@github-actions github-actions Bot added the effort/M Under 500 readable lines label Sep 3, 2026
Exclude node-gyp projects and link scratch files from the existing global file set while preserving the native addons and ConPTY runtime helpers. This avoids creating a platform-only catch-all matcher that would reintroduce tests and renderer side-files.

Exercise electron-builder normalization and dependency collection against representative runtime and build-output fixtures.

Generated-by: Codex
Apply /Brepro and /DEBUG:NONE through the existing Windows command runner. /Brepro derives PE timestamps from content; /DEBUG:NONE removes unshipped PDB identity and paths after node-gyp adds /DEBUG.

Two clean builds remained byte-identical after removing /INCREMENTAL:NO. Removing /DEBUG:NONE reintroduced four unpacked-payload differences, so the two remaining flags are the minimal set.

Generated-by: Codex
The two clean Windows builds have identical compressed ZIP entry data; all remaining ZIP differences are DOS and NTFS modification times. Disable modification and access times at the existing 7-Zip invocation. This requires a one-line app-builder-lib patch because there is no archive flag passthrough. Test real ZIP bytes with different source timestamps and verify the extracted payload.

Generated-by: Codex
The only differing NSIS header fields are the build-time FILETIMEs of app-64.7z and the uninstaller. SetDateSave off removes those fields at compilation without changing the embedded bytes. A real NSIS fixture verifies identical output after changing input timestamps; the full Windows release remains the acceptance test.

Generated-by: Codex
@Astro-Han
Astro-Han force-pushed the fix/windows-reproducible-payload branch from 98a0157 to ffc1f10 Compare September 4, 2026 13:20
@Astro-Han
Astro-Han marked this pull request as ready for review September 4, 2026 13:31

@jackwener jackwener left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Approving at exact head ffc1f104. All eight checks pass. No findings.

The patch is minimal and the licensing is complete

The change to app-builder-lib is one line — args.push("-mtm=off", "-mta=off") — which stops 7z from storing modification and access times. Timestamps are the usual reason two builds of identical content differ, so this is the smallest edit that reaches the goal.

The LICENSE entry is what makes redistributing that patch correct, and it has every part it needs: the upstream source and repository, the exact version the patch applies to, the path to the patch in this tree, the MIT license it inherits, the full license text, and a sentence stating what the patched material actually does. Nothing here has to be inferred by a downstream consumer.

The tests pin reproducibility rather than merely exercising it

Both new cases build twice with different input timestamps and require the outputs to be byte-identical:

assert.ok(results[0].equals(results[1]), 'ZIP bytes changed with the input file timestamp');
assert.ok(results[0].equals(results[1]), 'NSIS bytes changed with the embedded file timestamp');

What makes them more than a byte comparison is the assertion that comes first. The ZIP case asserts the extracted payload is identical payload, and the NSIS case asserts the produced bytes still contain the embedded payload. Without those, two builds that both produced an empty or truncated archive would compare equal and the test would pass having verified nothing. The precondition is what keeps "identical" from being vacuously true — and it is the part most easily left out.

release-windows-check.yml puts the check in CI rather than leaving it as something to remember to run.

This is a release-tooling fix, so the merge decision remains a human's.

简体中文

ffc1f104 上批准。八项检查全部通过。没有发现问题。

补丁极小,而许可声明是完整的

app-builder-lib 的改动只有一行——args.push("-mtm=off", "-mta=off")——它让 7z 不再存储修改时间与访问时间。时间戳正是「内容相同的两次构建却产出不同字节」的常见原因,所以这是能达到目的的最小改动。

LICENSE 条目是「重新分发这个补丁」得以正当的关键,而它该有的部分一样不缺:上游来源与仓库、补丁所针对的确切版本、补丁在本仓库中的路径、它所继承的 MIT 许可、完整的许可文本,以及一句说明被打补丁的材料实际做了什么。下游使用者不需要去推断任何东西。

测试钉住了可复现性,而不只是演练了它

两条新用例都用不同的输入时间戳构建两次,并要求输出逐字节相同:

assert.ok(results[0].equals(results[1]), 'ZIP bytes changed with the input file timestamp');
assert.ok(results[0].equals(results[1]), 'NSIS bytes changed with the embedded file timestamp');

让它们不止是一次字节比较的,是排在前面的那条断言。 ZIP 那条先断言解出的 payload 是 identical payload,NSIS 那条先断言产出的字节仍包含被嵌入的 payload。没有它们,两次都产出空归档或截断归档的构建同样会「相等」,测试会在什么都没验证的情况下通过。 那条前置条件正是让「相同」不至于平凡为真的东西——而它也是最容易被省略的一部分。

release-windows-check.yml 把这道检查放进了 CI,而不是留成一件「要记得去跑」的事。

这是发布工具链的修复,合并与否仍由人决定。


Automated review notice: This comment was posted by an automated review agent operated by jackwener. It is not an independent human review and does not replace one.

@Astro-Han
Astro-Han merged commit e562a87 into main Sep 4, 2026
8 checks passed
@Astro-Han
Astro-Han deleted the fix/windows-reproducible-payload branch September 4, 2026 17:45
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

effort/M Under 500 readable lines

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants