Everything below was read out of the code that performs it:
build_release_zip.py (the builder), verify_github.py (the offline verifier),
tests/autocheck.py (the local gate), tests/run_tests.py (the suite runner),
tests/test_release_contract.py (the release contract tests), app/settings_io.py,
native/launcher.rc and _render_docs.py. Where a step is a convention rather
than something a program enforces, the text says so. English only, like the rest
of the maintainer documentation.
All commands are run from the repository root with the bundled interpreter:
runtime\python.exe <script> ...
The example version throughout is 1.15.2; replace it with the real one.
| # | Asset | Who produces it |
|---|---|---|
| 1 | neuralscreen-vX.Y.Z-full.zip |
build_release_zip.py |
| 2 | SHA256SUMS |
build_release_zip.py (sidecar beside the ZIP) |
| 3 | runtime-manifest.json |
tracked file, validated and pinned by the builder (sidecar) |
| 4 | THIRD-PARTY-NOTICES.md |
tracked file, validated and pinned by the builder (sidecar) |
| 5 | README.md |
you upload it separately |
| 6 | README.ru.md |
you upload it separately |
| 7 | TECHNICAL.md |
you upload it separately |
| 8 | TECHNICAL.ru.md |
you upload it separately |
Eight assets, no more and no less. verify_github.py holds exactly this set in
required_assets; a missing one is reported as release vX is missing asset README.md, and anything extra is reported as release vX carries N asset(s) beyond the release set: .... The set is built from RELEASE_DOCUMENTS, the
constant build_release_zip.py uses as well, and
tests/test_release_documents_agree.py fails if the two lists ever diverge -
the failure mode that matters, because the verifier only reports a missing
document after the release is already published. This is not a formality:
documentation images were
uploaded as assets once and the "required are present" check did not notice,
which is why the extra-asset check exists.
The four documents are inside the ZIP as payload as well (they are part of the package inventory), but the release page still needs them as separate assets.
docs/*.png are not release assets. They live in the repository and are
read by the verifier from the tagged Git blobs
(https://raw.githubusercontent.com/perseval-BLR/NeuralScreen/<tag>/docs/...).
They must be committed and the tag must be pushed, or the verifier reports
<name>: fetch failed.
With the default output directory (the repository root) the builder writes
neuralscreen-vX.Y.Z-full.zip and SHA256SUMS; runtime-manifest.json and
THIRD-PARTY-NOTICES.md are the tracked files themselves (they are only
rewritten when the output directory is somewhere else). It prints
built <name> (<bytes> bytes) and
sidecars: SHA256SUMS, runtime-manifest.json, THIRD-PARTY-NOTICES.md.
| File | Form | For 1.15.2 |
|---|---|---|
build_release_zip.py (line 35) |
VERSION = "X.Y.Z" |
VERSION = "1.15.2" |
app/settings_io.py (line 192) |
APP_VERSION = "X.Y.Z" |
APP_VERSION = "1.15.2" |
native/launcher.rc |
the numeric pair, comma-separated, four fields, FILEVERSION 1,15,2,0 |
FILEVERSION 1,15,2,0 |
native/launcher.rc |
the numeric pair, second field of the same pair | PRODUCTVERSION 1,15,2,0 |
native/launcher.rc |
the string pair, four fields, VALUE "FileVersion", "1.15.2.0" |
VALUE "FileVersion", "1.15.2.0" |
native/launcher.rc |
the string pair, three fields, VALUE "ProductVersion", "1.15.2" |
VALUE "ProductVersion", "1.15.2" |
So: three files, and the launcher .rc holds the version twice over (once as
comma numbers, once as strings) - four places, six lines. The comma form is
X,Y,Z,0; the FileVersion string is X.Y.Z.0; the ProductVersion string is
X.Y.Z with no fourth field. Getting that shape wrong is a failure, not a
warning: the builder validates all four .rc values by regex and compares them
to the expected dictionary.
Exactly one match of each pattern may exist in the file. A second (even
commented-out) FILEVERSION line fails with
native/launcher.rc must declare exactly one FILEVERSION; found 2.
Version drift is caught in three independent places:
- the builder,
assert_version_coherence:version drift: builder, APP_VERSION and launcher.rc disagree; expected {...}, got {...}- exit code 2, nothing is written; andassert_launcher_version, which reads the same four values out of the builtNeuralScreen.exe(a stale exe isNeuralScreen.exe version resource differs from the release; ...); - the verifier:
version source differs from manifest version: <path>,tagged version values differ from manifest: <path>, andrequested tag 'vX.Y.Z' does not match builder vY.W.Z; tests/test_release_contract.py(version drift), which covers the.rcstring fields as well as the other sources;- the local gate inherits it:
zip: integrity and contentsfails withrelease manifest: version drift: ....
The VERSION constant is the source of truth. The tag argument only has to
agree with it - v1.15.2 while VERSION = "1.15.1" fails with
expected tag must be v1.15.1, got 'v1.15.2'. Release candidates are refused by
the same check.
Each step says why it is where it is. The order is not a preference - the builder refuses a dirty tracked tree and an untagged HEAD, and the manifest pins the tracked inventory, so any commit after the re-pin invalidates it.
Change all six lines of section 1. Nothing else needs a version: VERSION.txt
is generated inside the archive from VERSION and the commit, and the product
defaults (config.default.json) carry no version.
native\build-host.bat Rebuilds native/nvngx.dll_ns-forwarder.dll first, then native/nvngx.dll (in place; a compile error exits non-zero)
native\build-launcher.bat Compiles native/launcher.rc to launcher.res and writes NeuralScreen.exe to the repository root (the icon and the .rc version come from here)
Why: the static gate worker: fresh, with the hook fails with
worker is older than <source> - rerun build-host.bat when a native input is
newer than the worker, and all three binaries are REQUIRED_RUNTIME_ARTIFACTS
and generated (not tracked) package members, so the builder refuses with
required runtime artifact is missing: <path> if they are absent.
native\launcher.rc changed in step 1, so the launcher exe has to be rebuilt
for the change to reach the shipped binary. The builder reads the packaged
NeuralScreen.exe's VERSIONINFO (the fixed FILEVERSION/PRODUCTVERSION
pair and the FileVersion/ProductVersion strings) and refuses an exe that
does not report the release version:
NeuralScreen.exe version resource differs from the release; expected {...}, got {...} - rerun native\build-launcher.bat.
runtime\python.exe _render_docs.py
Writes the four tracked images: docs/screenshot-main-light.png,
docs/screenshot-main-dark.png, docs/screenshot-settings.png,
docs/screenshot-windows.png. The renderer takes the version from
main.APP_VERSION, so it has to run after the bump, or the shipped pictures
show yesterday's version. _render_docs.py itself is gitignored
(_render_*.py), the PNGs are tracked.
Why before the manifest: the re-pin in step 4 requires a clean tracked tree, and any tracked byte that changes later invalidates the pinned inventory.
runtime\python.exe build_release_zip.py vX.Y.Z --write-runtime-manifest
Commit steps 1-3 first: the writer calls assert_clean_tracked_tree and fails
with tracked working tree is dirty; commit or restore these paths: ....
What it does: writes runtime-manifest.json (schema 3) in canonical JSON -
indent=2, ensure_ascii=False, sort_keys=True plus a trailing newline; any other
formatting is rejected later with
runtime-manifest.json is not canonical UTF-8 JSON. The manifest pins
version_sources- the three version-source files with their extracted values;runtime.*- every file underruntime/that ships (count, tree digest, per-file size and sha256) plus the nine required runtime artifacts. What does not ship:runtime/Scripts/(pip launchers carry the builder's absolutepython.exepath in their shebang),runtime/Include/, Tk, bytecode caches, test folders and theDROP_PACKAGESsite-packages - a dropped name matches its folder,name.*(name.py,name.libs,name.cp313-win_amd64.pyd) andname-*(name-1.0.dist-info);package.*- the complete payload inventory, each record markedorigin: git(with its Git blob OID) ororigin: generated. Size and sha256 are those of the bytes in the ZIP. For a tracked file whose.gitattributesat the ref sayeol=crlf(today onlyNeuralScreen.bat; cmd.exe needs CRLF) the builder ships the CRLF form of the LF blob, as a checkout would write it, and the record carrieseol: crlf- its size and sha256 are the CRLF bytes, itsgit_blobis the tagged blob;source_inventory- every tracked native source/shader, version source and shipped resource with its Git blob OID, size and sha256 of the blob itself (for aneol: crlffile these differ from itspackagerecord by design).
It fails closed if a mandatory input is untracked
(mandatory release input is untracked at HEAD: <path>), if a required runtime
artifact is missing, or on version drift. It prints wrote <path>.
Then commit it:
git add runtime-manifest.json && git commit -m "chore: repin the runtime manifest for vX.Y.Z"
Re-pin again after every later commit that changes a tracked shipped file. The manifest records the Git blob of each of those files, so one commit after the re-pin makes it stale for the tag. The failures are specific:
- builder:
runtime-manifest.json does not match tagged runtime/source inventory; regenerate and review it before the release commit; - local gate (
tests\autocheck.py, checkzip: integrity and contents):release manifest: runtime-manifest.json does not match tagged runtime/source inventory; ...- this is the zip: integrity error; - verifier (if it got past the builder):
tagged Git blob differs from manifest: <path>,package Git blob differs from tag: <path>.
runtime\python.exe tests\run_tests.py --gui
The screen is taken over several times; leave the machine alone and make sure no
NeuralScreen instance is running (the smoke and GUI stages refuse to start if
one is: NeuralScreen is already running (...) - stop it first).
Two rules the runner enforces:
- Every tracked test must be assigned to exactly one group. A new
tests/test_*.pythat is not inTEST_GROUPSstops the whole run withRESULT: ERROR - unrouted test: <name>(exit 1). Being in two groups istest has multiple groups: <name> -> [...]. - Group order decides execution order:
unit/static,WARP,GPU,GUI-E2E. The static stage (autocheck.py) runs first, the smoke test is aGPUjob, the GUI cycle is the lastGUI-E2Ejob.--only <fragment>runs one test and skips the static and smoke stages.
Green looks like RESULT: PASS - <N> checks, <M> skipped, <seconds>, with the
per-group counters above it. Anything FAIL/ERROR/TIMEOUT exits non-zero.
Also check the archive situation before this run: the static gate validates
neuralscreen-v<APP_VERSION>-full.zip when that file exists in the root. For
a new version it does not exist yet and the check reports
ZIP waits for vX.Y.Z (a pass). A stale archive of the same version left over
from an earlier build is validated and can fail
(release ZIP: ..., a release ZIP exists but tag vX.Y.Z does not ...).
git status --short # must be empty (config.json is ignored anyway)
The builder will refuse anything else. Ignored files never trip it: runtime/,
config.json, *.zip, SHA256SUMS, _work/, _render_*.py and the rest are
outside the gate.
git tag vX.Y.Z
git push origin main
git push origin vX.Y.Z
The tag has to resolve exactly to HEAD:
tag vX.Y.Z resolves to <sha>, HEAD is <sha>. Tagging and then committing again
produces the same message, so tag last.
The verifier needs both sides: it resolves the local tag and the remote tag
through the GitHub API and compares them
(local/remote tag mismatch: <a> != <b>, local tag vX.Y.Z is missing or invalid).
runtime\python.exe build_release_zip.py vX.Y.Z
This is the step that must not run earlier. The builder calls, in order:
assert_clean_tracked_tree -> assert_release_tag (tag exists and equals HEAD)
-> mandatory inputs present at the tag -> validate_runtime_manifest against the
tag (git_ref=tag, not HEAD) -> DLL architecture gate -> payload packaging ->
assert_no_builder_paths (no packaged file may name the repository root or the
builder's home folder, in either slash form, UTF-8 or UTF-16) ->
assert_launcher_version (the packaged NeuralScreen.exe reports the release
version) -> the ZIP.
It refuses, with exit code 2 and RELEASE CONTRACT FAILED: <message>:
| Situation | Message |
|---|---|
| dirty tracked tree | tracked working tree is dirty; commit or restore these paths: ... |
tag argument not v<version> |
expected tag must be vX.Y.Z, got '...' |
| tag missing | required tag vX.Y.Z does not exist |
| tag not at HEAD | tag vX.Y.Z resolves to <sha>, HEAD is <sha> |
| mandatory file absent | mandatory release files are missing: <paths> |
| mandatory file untracked at the tag | mandatory release input is untracked at vX.Y.Z: <path> |
| other input not in the tag | release input is not tracked by the tag: <path> |
| required runtime artifact absent | required runtime artifact is missing: <path> |
| manifest out of date | runtime-manifest.json does not match tagged runtime/source inventory; regenerate and review it before the release commit |
| manifest formatting | runtime-manifest.json must use canonical UTF-8 JSON formatting |
| version drift | version drift: builder, APP_VERSION and launcher.rc disagree; expected {...}, got {...} |
| missing kernel archs | runtime architecture mismatch: missing sm_75, sm_89 |
| file changed during the build | package file ... changed during build: <path> |
| a file names the build machine | package files contain the builder machine's absolute path: <paths> |
| launcher not rebuilt after the bump | NeuralScreen.exe version resource differs from the release; expected {...}, got {...} - rerun native\build-launcher.bat |
| launcher without a version resource | NeuralScreen.exe has no readable version resource: ... |
The build is deterministic: ZIP timestamps are pinned to 1980-01-01, modes to
0644, VERSION.txt carries no wall-clock line, so two builds of the same commit
are byte-identical.
Draft the notes in _work/ (gitignored) - see section 3 - then:
gh release create vX.Y.Z \
-R perseval-BLR/NeuralScreen \
--title "vX.Y.Z - <one line>" \
--notes-file _work/release-XYZ-notes.md \
neuralscreen-vX.Y.Z-full.zip SHA256SUMS runtime-manifest.json THIRD-PARTY-NOTICES.md \
README.md README.ru.md TECHNICAL.md TECHNICAL.ru.md
The four documents are the ones the builder does not emit. gh infers the
repository from origin, so -R is optional; the tracked scripts use
perseval-BLR/NeuralScreen, which is the legacy name of the same repository
(GitHub serves it at perseval-BLR/DLSS5-NeuralScreen; the READMEs' raw URLs and
verify_github.py still use the legacy name, so the redirect is relied upon).
The release must become the Latest one - the verifier fails with
vX.Y.Z is not the Latest release (GitHub says 'vY.W.Z').
runtime\python.exe verify_github.py vX.Y.Z
It downloads the assets through gh, checks the documentation against the tagged
Git blobs over raw.githubusercontent.com, resolves the tag on GitHub, reads the
release body, and validates the whole set offline (see section 4). It needs
network access and an authenticated gh; it writes downloads into
_work\verify-github\ (gitignored).
PASS prints one [OK] <name> matches tag vX.Y.Z (<sha12>) line per document, a
[OK] repo description: ... line, and closes with:
============================================================
OK: vX.Y.Z on GitHub matches tag, manifest and checksums
The notes are the release body, written once, English only. _work/release-1151-notes.md
is the draft that went out as v1.15.1 and the published body is byte-identical to
it - that file is the format to copy.
- Language: English only. The two-language body (EN, then
---, then RU) was retired; the length check measures the whole body now, not the part before the separator. - Length: under 5000 characters, measured by the static gate
tests\autocheck.py, checkrelease notes: concise. It resolves the latest release throughghand reportsthe release body is <N> characters - too long. Two things follow: the check can only judge a release after it is published, and it always judges the latest one - run before publishing, it measures the previous release. Recent bodies were 3934 (v1.15.0) and 4675 (v1.15.1); four older ones are already over the line - v1.4.0 9995, v1.4.1 5232, v1.7.0 5130, v1.11.0 5082 - and nothing caught them at the time. Stay well under. - Line endings: LF, no BOM. The drafts are plain UTF-8 with LF; write the file
with LF and publish it with
--notes-fileso the bytes on GitHub are the bytes you reviewed. - Two blocks are mandatory verbatim (the verifier compares the exact strings,
not prefixes):
- the notice line
> **Notice.** Not affiliated with NVIDIA; NVIDIA, DLSS and the NVIDIA logo are NVIDIA Corporation's trademarks. The bundled NVIDIA runtimes are NVIDIA's property, included unmodified as received, research/educational use only, no warranty, use at your own risk.- a missing or reworded notice is
release vX is missing standard NVIDIA notice;
- a missing or reworded notice is
- the driver warning, exactly:
> [!WARNING] > NeuralScreen requires the latest NVIDIA driver. Operation with older drivers or unsupported/non-standard configurations is not guaranteed.- a missing or reworded warning is
release vX is missing driver warning alert. Note the two lines are compared as one string, so both lines and the space after both>markers have to match.
- a missing or reworded warning is
- the notice line
- No AMD tester call. Releases v1.13.1-v2.2.0 carried a
> [!IMPORTANT]block calling for Radeon testers ofNeuralScreen-AMD; the owner dropped it on 10.10.2026 as no longer current - do not put it back. - Then: one paragraph naming what the release is,
## Fixes(one bolded symptom per paragraph, with the mechanism and the measurement), and a## Testssection with the suite count andPASS / FAIL / SKIPplus the GUI-E2E result. - Punctuation: the owner's rule is plain hyphens, never em-dashes, in text written for them. (The bodies published so far are full of em-dashes; do not add to that.) No Russian twin of the notes - the body is English only.
verify_github.py first checks the tag against the builder's VERSION, then, in
order:
- every document against the tagged Git blob (
git show <tag>:<path>vsraw.githubusercontent.com/<tag>/<path>); - the release exists and resolves to a commit, and the local and remote tag commits agree;
- the release body carries the two mandatory blocks;
- the asset set is exactly the eight (missing and extra both reported);
- all eight assets are downloaded, and the six that are tracked files (the four
documents,
runtime-manifest.json,THIRD-PARTY-NOTICES.md) must be byte-identical togit show <tag>:<name>- an asset uploaded from a checkout other than the tag is a failure, not merely "present"; - the built set is validated offline:
canonical manifest JSON, schema/product identity, tag/manifest agreement,
version sources, package and runtime inventories,
source_inventoryvs the tagged blobs, outerSHA256SUMS(exactly the ZIP, manifest, notices) with matching digests, ZIP member order, deterministic timestamps and modes, member checksums against the ZIP's ownSHA256SUMS,VERSION.txt(version, tag, commit, manifest digest), and - if the local archive is still in the root - the published ZIP against the local one; - the release is the Latest one;
- the repository description still contains
user presetsand12 languages.
Exit code 0 means PASS. Exit code 1 means FAIL: <N> followed by one line per
failure. The messages worth recognising:
- assets:
release vX is missing asset README.md,release vX carries 2 asset(s) beyond the release set: docs/screenshot-main-dark.png, ... - documents:
<name>: missing from local tag vX,<name>: fetch failed,<name>: tagged blob mismatch local=<sha12> github=<sha12> - uploaded tracked assets:
release asset <name> differs from tag vX: asset=<sha12> tag=<sha12>,release asset <name>: missing from local tag vX,could not download release asset <name> - body:
release vX is missing standard NVIDIA notice,release vX is missing driver warning alert - release/tag:
release vX does not exist,local tag vX is missing or invalid,requested tag 'vX' does not match builder vY,could not resolve remote tag vX to a commit,local/remote tag mismatch: <a> != <b>,vX is not the Latest release (GitHub says 'vY') - set integrity:
release is missing asset <file>,invalid runtime-manifest.json: ...,runtime-manifest.json version is not X.Y.Z,tag/manifest mismatch: requested=..., manifest=..., version=...,runtime-manifest.json is not canonical UTF-8 JSON,outer SHA256SUMS inventory mismatch: expected [...], got [...],outer checksum mismatch: <name>,ZIP member order is not canonical,ZIP member timestamp is not deterministic: <name>,ZIP has undeclared payload members: [...],ZIP is missing declared payload members: [...],package file checksum mismatch: <name>,runtime manifest file inventory differs from ZIP,runtime manifest tree digest differs from ZIP,ZIP checksum mismatch: <name>,published ZIP differs from the local release ZIP,tagged Git blob differs from manifest: <path>,package Git blob differs from tag: <path>,package Git file checksum differs from tag: <path>(for aneol: crlfrecord the tag's blob is converted to CRLF first),packaged CRLF file has stray line endings: <name>,package record has an unknown eol conversion: <path> - repository:
repo description lost the marker 'user presets',could not read repository description
| Mistake | What you see |
|---|---|
| The four documents were not uploaded as assets | release vX is missing asset README.md (one line per missing name) |
| Documents uploaded but not committed to the tag (or the tag not pushed) | <name>: missing from local tag vX, <name>: fetch failed, <name>: tagged blob mismatch ... |
| A document, the manifest or the notices uploaded from a checkout other than the tag | release asset README.md differs from tag vX: asset=<sha12> tag=<sha12> |
| Documentation PNGs uploaded as assets | release vX carries N asset(s) beyond the release set: docs/screenshot-... |
| Manifest not re-pinned after a later commit | builder: runtime-manifest.json does not match tagged runtime/source inventory; regenerate and review it before the release commit; local gate: release manifest: runtime-manifest.json does not match ... (the zip: integrity error); verifier: tagged Git blob differs from manifest: <path> |
| Manifest re-pinned but not committed | tracked working tree is dirty; commit or restore these paths: on the next builder run |
Version drift (usually the .rc pair forgotten) |
version drift: builder, APP_VERSION and launcher.rc disagree; expected {...}, got {...}; in the local gate the same text under release manifest:; in tests\test_release_contract.py, version drift |
.rc bumped but native\build-launcher.bat not rerun |
builder: NeuralScreen.exe version resource differs from the release; ... |
An old FILEVERSION line left in the .rc |
native/launcher.rc must declare exactly one FILEVERSION; found 2 |
| Dirty tracked tree | tracked working tree is dirty; commit or restore these paths: - nothing is written, exit 2 |
| Builder run before tagging | required tag vX.Y.Z does not exist |
| Anything committed after the tag | tag vX.Y.Z resolves to <a>, HEAD is <b> |
| A mandatory file not committed | mandatory release input is untracked at vX.Y.Z: <path> / release input is not tracked by the tag: <path> |
| A native binary or the runtime missing | required runtime artifact is missing: native/nvngx_dlssg.dll |
| Wrong DLL build | runtime architecture mismatch: missing sm_86 (the DLL must carry sm_75/86/89/120 fatbins) |
| Notes too long | release notes: concise: the release body is 5180 characters - too long (only after publishing) |
| Notes missing a mandatory block | release vX is missing driver warning alert |
| The release was not promoted to Latest | vX is not the Latest release (GitHub says 'vB') |
| A stale archive of the same version left in the root | zip: integrity and contents fails on it: release ZIP: ... / a release ZIP exists but tag vX.Y.Z does not - the archive cannot be matched to a release |
| A new test file with no group | RESULT: ERROR - unrouted test: test_new_thing.py from tests\run_tests.py |
MANDATORY_FILES / REQUIRED_RUNTIME_ARTIFACTS edited by hand |
tests\test_release_lists_pinned.py fails: MANDATORY_FILES lost <path> - a file the contract ... |
| Repository description edited | repo description lost the marker 'user presets' |
- There is no release script.
git ls-filescontains no.github/and no workflow; the whole procedure is manual (build_release_zip.py->gh release). Step 9's command line is reconstructed from the asset set the verifier demands; the exact flags the owner uses are not recorded anywhere in the repository. - Nothing enforces screenshot freshness.
_render_docs.pyis gitignored and no check compares the PNGs with the version or the commit; the verifier only requires that the tagged blob equals what GitHub serves. Regenerating them every release is the correct reading of "the screenshots must show the real version", which the script itself states - but it is not machine-enforced. - Step order for the screenshots (before the manifest re-pin) is what the code allows, because the re-pin requires a clean tracked tree; it is not written down as a procedure anywhere.
- The body beyond the two mandatory blocks is verified only in the sense
that a human reads the release;
verify_github.pychecks the notice and the driver warning and nothing else in the body. - The notes-length check reads the latest release, so it cannot gate a draft. There is no pre-publish length gate in the code.