Skip to content

feat: draft release with a signable zip and a NEVR-PLUGIN-BUILD stamp - #2

Merged
thesprockee merged 6 commits into
mainfrom
feat/release-draft
Oct 28, 2026
Merged

thesprockee merged 6 commits into
mainfrom
feat/release-draft

Conversation

@thesprockee

@thesprockee thesprockee commented Oct 26, 2026 •

Copy link
Copy Markdown
Member

Release step for the nEVR release signer: a v* tag builds, checks and attests the mod, then opens a draft release with the three files the signer takes. Built to Teth's spec, docs/plugin-release.md in thesprockee/release-signer PR #25 (head 642888d), section numbers below are that file's.

Stacked on #1 (base chore/standards): build.yml has to stop curl-ing the old check_mod.py before it can check the new layout, and AGENTS.md shares rows with #1. Merge #1 first; this retargets to main by itself, or I rebase it.

What changes

Spec Change
§5 identity src/plugin.cpp carries one \0NEVR-PLUGIN-BUILD <version> <commit40>\0 (never NEVR-BUILD), logged in Init so --gc-sections keeps it. CMakeLists.txt sets MOD_COMMIT from GITHUB_SHA, else git rev-parse HEAD, else 40 zeros. check_mod.py refuses a DLL with none, two, a NEVR-BUILD anywhere, the wrong version, or (in a release) a commit that isn't source.commit.
§4 assets package.py makes <id>-v<ver>-windows.zip (DLL, LICENSE, NOTICE if present, <Stem>.plugin.json if the mod has settings, inner SHA256SUMS; flat, fixed dates), echo-mod.json without sha256/size and with package, and a SHA256SUMS of those two. No loose DLL. A mod with no LICENSE is not packed. check_mod.py --published enforces exactly that: three files, name <id>-v<ver>-windows.zip, exact zip entries, no directories/symlinks/special entries/colliding names, fixed dates, entries ≤ 16 MiB, both sums files agree.
§3 workflow release.yml: build job contents: read + id-token + attestations: write builds, runs this repo's own check_mod.py, attests dist/*, uploads it; draft job (contents: write, no build code) downloads it and runs gh release create --draft --verify-tag (--prerelease from the manifest). Top-level permissions: {}. Every action pinned by SHA. No curl.
docs AGENTS.md rule 14 (the identity), the layout table, the Releasing section; a NOTICE template that goes in the zip.

Failing-first tests

Written before the code. First run on the new tests, old tools (Ran 30 tests ... FAILED (failures=6, errors=20); the 4 that passed include "never the runtime's literal"):

ERROR: test_the_built_dll_carries_one_literal_with_its_version (Identity)
ERROR: test_the_zip_is_flat_dated_and_has_inner_sums (Published)
FAIL:  test_a_mod_is_exactly_three_assets (Published)
FAIL:  test_a_loose_dll_is_refused (Layout)
... 26 in all

After: python3 tests/test_check_mod.py -> Ran 30 tests in 1.969s OK. They cover the identity (wrong version, wrong commit, NEVR-BUILD, none, two, unset commit) and the layout (extra asset, loose DLL, sha256 in the manifest, wrong package name, extra sums line, directory / nested / unknown / colliding / backslash entries, symlink, dates, bad inner sums, wrong DLL name), plus "same files -> same zip" and "no LICENSE -> not packed".

Commands run, on 89f3433

  • just verify: exit 0. Ran 5 tests ... OK (PR-title), Ran 30 tests ... OK; the release check printed ok.
  • actionlint .github/workflows/*.yml: exit 0 (it found SC2086 on the first version; fixed in the last commit).
  • The literal in the built binary, with GITHUB_SHA=abab...ab: [b'\x00NEVR-PLUGIN-BUILD 0.1.0 abababababababababababababababababababab\x00'], and d.count(b"NEVR-BUILD ") == 0.
  • Reproducible: two clean rebuilds of one commit gave identical files:
    352517f1...bb47b3 example-mod-v0.1.0-windows.zip, fc162f99...15ee echo-mod.json, 597b69b3...0242c SHA256SUMS (both runs).
  • The zip: ExampleMod.dll, LICENSE, NOTICE, ExampleMod.plugin.json, SHA256SUMS, all dated 1980-01-01.

Not run

  • release.yml has not run on GitHub. It has now: one run on v0.0.0-test.1, see the update below. Not run: the signer itself, and a signed DLL loading.
  • The signer, the attestation verification, and a signed DLL loading: not touched.

Things the template can't meet, or that change under people

For Spritz / Teth, not worked around:

  1. The new layout breaks the readers that exist today. The catalogue and echo-mods' inspect_submission.py expect a loose DLL plus sha256/size in echo-mod.json; they won't read this release until design items E2/E3 (Mia) land. Merge this when that is ready, or expect the "result" comment on submissions to fail.
  2. §6 "exports only Nvr*": check_mod.py refuses any export outside the Nvr* ABI (an error since 52676c5, as Spritz asked; the test renames one export in a copy of the built DLL and failed before the change). The template now matches the signer on this one.
  3. §6 "imports only the allowed system DLLs": the template's list is SYSTEM_DLLS in check_mod.py plus any api-ms-win-*; a vendored copy in the signer will need that same list. The built DLL imports KERNEL32, SHELL32 and api-ms-win-crt-*.
  4. §6 "release.yml byte-equal to the template's": met by construction (nothing in the file is per-mod), but it ties every mod to this file's text; a later change to it needs a new pinned commit in the signer.
  5. §4 "NOTICE if the repo declares one": the template now ships one with placeholder credits, so a mod that never edits it releases with the placeholder text. package.py packs whatever NOTICE contains.
  6. Toolchain drift: the mingw-w64 and wine packages are the runner's current Ubuntu 24.04 ones, unpinned. Rebuilds are byte-identical on one machine; a reviewer on another toolchain may differ.
  7. Unverified: that actions/attest-build-provenance@v2.4.0 attestations satisfy the signer's gh attestation verify --signer-workflow for a two-job workflow (the attesting job is build, in release.yml, as §3 says).

Quest: not applicable (PC-only template). No issue opened for this unit (the order named none).

Co-Authored-By: Andrew Bates a@sprock.io

Update 2026-10-28: main moved, brought up to date by merge

chore/standards (#1) now contains origin/main (b53afe8, plugin API 7). Merged it into this branch (merge commit 1802a59, a plain fast-forward push, no force-push). The text merge was clean; I checked beyond that:

just verify                      exit 0
  [host log] smoke level 1: NEVR-PLUGIN-BUILD 0.1.0 89f34331ba19ea72511062137ec738a60af33d57
  python3 tests/test_check_pr_title.py   Ran 5 tests ... OK
  python3 tests/test_check_mod.py        Ran 31 tests ... OK

An "api": 7 build with -DECHO_MOD_ASSET_EXAMPLE=ON -DECHO_MOD_WERROR=ON and GITHUB_SHA=abab...ab (scratch build dir, manifest restored afterwards):

build exit=0
python3 -I tools/check_mod.py --dll .../ExampleMod.dll     ok   (check_mod exit=0)
grep -c -a 'NEVR-PLUGIN-BUILD 0.1.0 abababababababababababababababababababab' ExampleMod.dll   1

So the identity literal survives an API 7 build with the example, and check_mod.py accepts it.

CI on 1802a59: build (push and PR) pass, PR-title check pass.

Co-Authored-By: Andrew Bates a@sprock.io

Update 2026-10-28 (2): exports are an error, and release.yml ran once on GitHub

Exports. check_mod.py now errors on any export outside the Nvr* ABI (52676c5). Failing-first: tests/test_check_mod.py class Exports renames NvrPluginShutdown to XvrPluginShutdown in a copy of the built DLL (same length) and expects an error naming it.

before the change:  FAIL: test_a_dll_exporting_an_extra_symbol_is_refused
                    AssertionError: False is not true : []        FAILED (failures=1)
after:  just verify exit 0     Ran 5 tests ... OK     Ran 33 tests ... OK

The built DLL and the Aaliyah-tablet DLL both still pass.

Release dry run (approved by Spritz). A throwaway signed commit b88ac0a on top of 52676c5 (only echo-mod.json version set to 0.0.0-test.1; no branch, only the tag was pushed), tag v0.0.0-test.1. Run 40709349413: build success, draft success. It made one draft prerelease with exactly three assets:

echo-mod.json                            1228  sha256:86270b49...
example-mod-v0.0.0-test.1-windows.zip   95050  sha256:cd65550b...
SHA256SUMS                                184  sha256:f6be7a38...
sha256sum -c SHA256SUMS                  echo-mod.json: OK   example-mod-v0.0.0-test.1-windows.zip: OK
  • zip: ExampleMod.dll, LICENSE, NOTICE, ExampleMod.plugin.json, SHA256SUMS; flat, all dated 1980-01-01; the inner sums list the other four.
  • echo-mod.json: package = the zip name, source = repository, repository_id, tag, commit b88ac0ac..., workflow .../release.yml@refs/tags/v0.0.0-test.1, prerelease: true, no sha256 or size.
  • DLL: NEVR-PLUGIN-BUILD 0.0.0-test.1 b88ac0ac98cdceae57acf121e7b1622fe6f99ac3; no NEVR-BUILD .
  • gh attestation verify <asset> --repo EchoTools/nevr-runtime-plugin-template --signer-workflow EchoTools/nevr-runtime-plugin-template/.github/workflows/release.yml --source-digest b88ac0ac... exit 0 for all three assets: signer .../release.yml@refs/tags/v0.0.0-test.1, source digest b88ac0ac..., ref refs/tags/v0.0.0-test.1.

So item 7 below (two-job attestation vs gh attestation verify) is now verified with that command. The signer's own invocation may differ; I haven't run the signer.

Cleaned up in the same turn: gh release delete v0.0.0-test.1 --cleanup-tag --yes; after it gh release list is [] and git ls-remote --tags origin is empty; branches untouched. Left behind (not deletable by me from here): the Actions run record and the three attestations, which stay in the attestation log.

CI on 52676c5: build (push and PR) pass, PR-title check pass.

Co-Authored-By: Andrew Bates a@sprock.io

…le zip

The release signer (docs/plugin-release.md) takes a mod release as exactly three files and
compares the DLL's build identity with the tag and the attested commit.

- plugin.cpp carries one \0NEVR-PLUGIN-BUILD <version> <commit>\0 literal, never
  NEVR-BUILD (nEVR's installer takes that for the runtime). Init logs it, so --gc-sections
  keeps it. CMake sets MOD_COMMIT from GITHUB_SHA, else git, else 40 zeros.
- package.py packs a mod as <id>-v<version>-windows.zip (DLL, LICENSE, NOTICE if any,
  settings sheet, inner SHA256SUMS; flat, fixed dates) beside echo-mod.json (no sha256 or
  size, with package) and a SHA256SUMS of those two.
- check_mod.py refuses a DLL without exactly one matching literal, and a release that is
  not that layout (extra files, directories, symlinks, dates, sums that disagree).
- Tests written first; they failed (26 of 30) before the change.

Co-Authored-By: Andrew Bates <a@sprock.io>
…ownloaded code

release.yml's build job holds contents: read, id-token and attestations: write and nothing
else, and runs this repo's own check_mod.py instead of curl-ing the template's main. A second
job, with contents: write and no build code, creates the release as a draft: the signer
writes only into drafts and publishes after signing.

Co-Authored-By: Andrew Bates <a@sprock.io>
…edits

Co-Authored-By: Andrew Bates <a@sprock.io>
chore/standards now contains origin/main (b53afe8, plugin API 7 and the asset
example). The merge was textually clean; checked beyond that:

- just verify exits 0 (31 tests)
- an api 7 build with -DECHO_MOD_ASSET_EXAMPLE=ON -DECHO_MOD_WERROR=ON passes
  check_mod.py --dll and carries the NEVR-PLUGIN-BUILD literal

Co-Authored-By: Andrew Bates <a@sprock.io>
AGENTS.md rule 7 says only the Nvr* exports leave the DLL, and the signer's RS3
check refuses anything else, but check_mod only warned. A release that passed the
template's check could then be refused at signing. It is an error now.

The new test renames one export in a copy of the built DLL (same length, so the PE
layout stays) and expects an error naming it. It failed before the change
(AssertionError: False is not true : []) and passes after. The built DLL still
passes, and so does Aaliyah's tablet DLL.

Co-Authored-By: Andrew Bates <a@sprock.io>
@thesprockee
thesprockee changed the base branch from chore/standards to main October 28, 2026 14:50
@thesprockee
thesprockee merged commit de90621 into main Oct 28, 2026
5 checks passed
@thesprockee
thesprockee deleted the feat/release-draft branch October 28, 2026 14:51
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.

1 participant