Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
74 changes: 52 additions & 22 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
@@ -1,25 +1,37 @@
# A tag v<version> (the version in echo-mod.json, a semantic version: semver.org) publishes a
# GitHub release: the DLL (or the pack's zip), echo-mod.json with the checksums filled in, and
# SHA256SUMS, with a build attestation that shows they were built here, from this tag.
# A version with a prerelease part (1.2.0-beta.1) becomes a prerelease.
# A tag v<version> (the version in echo-mod.json, a semantic version: semver.org) builds the mod,
# checks it, attests the release files (a build attestation that shows they were built here, from this
# tag) and opens a DRAFT GitHub release with exactly three files:
#
# Keep this file as it is: the launcher only takes releases that this workflow built.
# <id>-v<version>-windows.zip the DLL, LICENSE, NOTICE, the settings sheet and a SHA256SUMS
# echo-mod.json the manifest, with the package name and the source
# SHA256SUMS the zip and echo-mod.json
#
# The draft is not live. The project's release signer signs the DLL inside the zip and publishes the
# release; you never upload anything. A version with a prerelease part (1.2.0-beta.1) becomes a
# prerelease.
#
# Keep this file as it is: the signer only takes releases that this workflow built, and it compares
# this file with the template's. Two jobs, on purpose: the one that builds and attests can't write to
# the repo, the one that writes the release runs no build code.
name: release

on:
push:
tags: ["v*"]

permissions:
contents: write
id-token: write
attestations: write
permissions: {}

jobs:
release:
build:
runs-on: ubuntu-24.04
permissions:
contents: read
id-token: write
attestations: write
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false

- name: Toolchain
run: |
Expand All @@ -28,6 +40,7 @@ jobs:
sudo update-alternatives --set x86_64-w64-mingw32-gcc /usr/bin/x86_64-w64-mingw32-gcc-posix
sudo update-alternatives --set x86_64-w64-mingw32-g++ /usr/bin/x86_64-w64-mingw32-g++-posix

# GITHUB_SHA, the tag's commit, goes into the DLL as its NEVR-PLUGIN-BUILD identity.
- name: Build, smoke test, check
run: tools/ci-build.sh "$GITHUB_REF_NAME"

Expand All @@ -36,22 +49,39 @@ jobs:
file=$(python3 -c 'import json; print(json.load(open("echo-mod.json"))["file"])')
python3 tools/package.py --dll "build/mingw/$file"

# What the launcher's catalogue checks for every release, by the template's current rules
# (they may be newer than this repo's copy): a release it would refuse is never published.
# What the launcher's catalogue and the release signer expect of every release, by this repo's own
# reviewed checker (nothing is fetched here: this job can attest). A release they would refuse is
# never drafted.
- name: Check the release
run: |
curl -fsSL -m 30 -o build/check_mod_current.py \
https://raw.githubusercontent.com/EchoTools/echo-mod-template-repo/main/tools/check_mod.py \
|| cp tools/check_mod.py build/check_mod_current.py
python3 build/check_mod_current.py --published dist --tag "$GITHUB_REF_NAME" --smoke build/smoke/smoke.json
run: python3 tools/check_mod.py --published dist --tag "$GITHUB_REF_NAME" --smoke build/smoke/smoke.json

- uses: actions/attest-build-provenance@v2
- uses: actions/attest-build-provenance@e8998f949152b193b063cb0ec769d69d929409be # v2.4.0
with:
subject-path: dist/*

- name: Release
- uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
with:
name: dist
path: dist
if-no-files-found: error

draft:
needs: build
runs-on: ubuntu-24.04
permissions:
contents: write
steps:
- uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0
with:
name: dist
path: dist

- name: Open the draft release
env:
GH_TOKEN: ${{ github.token }}
TAG: ${{ github.ref_name }}
REPO: ${{ github.repository }}
run: |
pre=$(python3 -c 'import json; print("--prerelease" if json.load(open("dist/echo-mod.json"))["prerelease"] else "")')
gh release create "$GITHUB_REF_NAME" dist/* --title "$GITHUB_REF_NAME" --generate-notes --verify-tag $pre
flags=(--draft --verify-tag --title "$TAG" --generate-notes)
if [[ $(jq -r '.prerelease' dist/echo-mod.json) == true ]]; then flags+=(--prerelease); fi
gh release create "$TAG" dist/* -R "$REPO" "${flags[@]}"
36 changes: 25 additions & 11 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,12 +31,13 @@ mod, or nEVR won't load it.
| `tests/test_check_mod.py`, `tests/test_check_pr_title.py` | Tests of the checks themselves | add to them |
| `justfile` | `just verify` and the recipes under it | rarely |
| `.clang-format` | The code style (4 spaces, 120 columns); `just fmt` applies it | no |
| `.github/workflows/release.yml` | Builds and publishes releases with a build attestation | **never** |
| `NOTICE` | Credits and notices that travel with the DLL in the release zip | yes |
| `.github/workflows/release.yml` | Builds, attests and drafts releases (the release signer publishes them) | **never** |
| `.github/workflows/build.yml` | Format, lint, build, smoke test, checks and tests on every push | no |
| `.github/workflows/pr-title-check.yml` | Fails a PR whose title isn't a Conventional Commits header | no |

The launcher only takes release files built and attested by this repo's unmodified
`release.yml`. Don't edit it, don't rename it, don't attach files to a release by hand.
The launcher and the release signer only take release files built and attested by this repo's
unmodified `release.yml`. Don't edit it, don't rename it, don't attach files to a release by hand.

## Commands

Expand Down Expand Up @@ -177,6 +178,12 @@ beside its own plugins. A mod should work there too or return non-zero from init
12. **No cheats, no data collection,** no network use beyond what the mod is for.
13. **Never write into the game's folders** except the mod's own log folder and a settings file
the `.plugin.json` names. Never touch other plugins' files, `config.yaml`, or `BugSplat64.dll`.
14. **The DLL carries its build identity:** exactly one `\0NEVR-PLUGIN-BUILD <version> <commit>\0`
literal (`src/plugin.cpp`; CMake fills in the version from `echo-mod.json` and the commit from
`GITHUB_SHA`, or `git rev-parse HEAD` on your machine, or 40 zeros with no git). The release signer
compares it with the tag and the commit that was built. Never `NEVR-BUILD`: nEVR's installer takes any
DLL holding that for the runtime itself. `Init` logs the literal; keep a use of it, or the linker's
`--gc-sections` drops it. `check_mod.py` refuses a DLL with none, two, or the wrong version.

## Commits and PR titles

Expand Down Expand Up @@ -301,14 +308,21 @@ launcher installs NvrContentOverlay itself, and packs need an nEVR with the earl
1. Bump `version` in `echo-mod.json` (semver: a breaking change for players or other mods bumps
MAJOR, a feature MINOR, a fix PATCH; a test build gets a prerelease part: `1.3.0-beta.1`).
2. Commit, tag `v<version>` on that commit, push the tag. Nothing else.
3. `release.yml` builds, smoke-tests, checks the tag against the manifest, packs `dist/`
(`<file>` or the pack zip, `echo-mod.json` with `version`, `prerelease`, `sha256`, `size`,
`settings` and `source` filled in, `SHA256SUMS`), checks `dist/` with the template's
current `tools/check_mod.py --published` (the same check the launcher's catalogue applies),
attests the files and creates the GitHub release (a prerelease when the version has a
prerelease part).
4. Within about 5 minutes the catalogue has it; the result ("live", or why not) is a comment
on the mod's submission issue.
3. `release.yml` builds (the tag's commit goes into the DLL's identity, rule 14), smoke-tests, checks
the tag against the manifest, packs `dist/`, checks `dist/` with this repo's own
`tools/check_mod.py --published`, attests the files and opens a **draft** GitHub release (a
prerelease when the version has a prerelease part). The draft has exactly three files:
- `<id>-v<version>-windows.zip`: the DLL, `LICENSE`, `NOTICE` (when the repo has one), the
`<Stem>.plugin.json` settings sheet (when the mod has settings) and a `SHA256SUMS` over those.
Flat, fixed dates, so the same files make the same zip.
- `echo-mod.json`: the manifest with `version`, `prerelease`, `settings`, `package` and `source`
filled in. It has no `sha256` or `size`: signing changes the DLL's bytes.
- `SHA256SUMS`: the zip and `echo-mod.json`.
4. A draft is not live. For a mod EchoTools releases, the project's release signer takes the
attested files, signs the DLL inside the zip, replaces the zip and its sums in the draft and
publishes it; the signer's plugin-release spec lists what it checks. Nobody uploads or edits files
by hand, the author included. Once the release is published the catalogue picks it up (about 5
minutes) and the result ("live", or why not) is a comment on the mod's submission issue.

Published versions are immutable: never move or reuse a tag, never replace release files. A
broken release is fixed by a new version. A published version that must not be used any more
Expand Down
27 changes: 26 additions & 1 deletion CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,30 @@ if(NOT MOD_FILE MATCHES "^([A-Za-z0-9._-]+)\\.dll$")
endif()
set(MOD_STEM ${CMAKE_MATCH_1})

# The commit this DLL is built from, for the NEVR-PLUGIN-BUILD literal in src/plugin.cpp (release-signer
# compares it with the tag's commit): GITHUB_SHA in CI, `git rev-parse HEAD` on your machine, else 40 zeros.
set(MOD_COMMIT "0000000000000000000000000000000000000000")
set(mod_commit_from "$ENV{GITHUB_SHA}")
if(NOT mod_commit_from)
find_package(Git QUIET)
if(GIT_FOUND)
execute_process(COMMAND "${GIT_EXECUTABLE}" rev-parse HEAD
WORKING_DIRECTORY "${CMAKE_CURRENT_SOURCE_DIR}"
OUTPUT_VARIABLE mod_commit_from OUTPUT_STRIP_TRAILING_WHITESPACE
RESULT_VARIABLE git_status ERROR_QUIET)
if(NOT git_status EQUAL 0)
set(mod_commit_from "")
endif()
endif()
endif()
# (CMake's regex has no {40}: check the length apart.)
string(LENGTH "${mod_commit_from}" mod_commit_length)
if(mod_commit_length EQUAL 40 AND mod_commit_from MATCHES "^[0-9a-f]+$")
set(MOD_COMMIT "${mod_commit_from}")
elseif(mod_commit_from)
message(WARNING "not a full commit id: \"${mod_commit_from}\"; the build identity gets 40 zeros")
endif()

# capabilities -> NEVR_PLUGIN_CAP_* bits
set(MOD_CAPS 0)
string(JSON caps_count LENGTH "${MOD_JSON}" capabilities)
Expand All @@ -56,7 +80,7 @@ if(caps_count GREATER 0)
endif()

project(${MOD_STEM} VERSION ${MOD_VERSION_MAJOR}.${MOD_VERSION_MINOR}.${MOD_VERSION_PATCH} LANGUAGES C CXX)
message(STATUS "${MOD_NAME} ${MOD_VERSION} -> ${MOD_FILE} (capabilities ${MOD_CAPS})")
message(STATUS "${MOD_NAME} ${MOD_VERSION} -> ${MOD_FILE} (capabilities ${MOD_CAPS}, commit ${MOD_COMMIT})")

if(NOT CMAKE_BUILD_TYPE AND NOT CMAKE_CONFIGURATION_TYPES)
set(CMAKE_BUILD_TYPE Release CACHE STRING "" FORCE)
Expand Down Expand Up @@ -84,6 +108,7 @@ target_compile_definitions(mod PRIVATE
MOD_STEM="${MOD_STEM}"
MOD_SUMMARY="${MOD_SUMMARY}"
MOD_VERSION="${MOD_VERSION}"
MOD_COMMIT="${MOD_COMMIT}"
MOD_VERSION_MAJOR=${MOD_VERSION_MAJOR}
MOD_VERSION_MINOR=${MOD_VERSION_MINOR}
MOD_VERSION_PATCH=${MOD_VERSION_PATCH}
Expand Down
10 changes: 10 additions & 0 deletions NOTICE
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
This mod is built from the nEVR mod template (https://github.com/EchoTools/nevr-runtime-plugin-template, MIT).

It does not contain any part of the game, and it is not affiliated with or endorsed by the makers of Echo VR.

Credits
-------
Replace this list with the people whose code, research or findings the mod builds on: who, what, and under
what licence. If you copy someone's code, keep their licence text here or beside it.

- (name) - (what) - (licence, link)
7 changes: 7 additions & 0 deletions src/plugin.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,12 @@

namespace {

// The build identity: one NUL-bounded literal that release-signer compares with the release's tag and
// the commit it was built from. It is NEVR-PLUGIN-BUILD, never NEVR-BUILD (nEVR's installer takes any DLL
// holding "NEVR-BUILD " for the runtime itself). Keep the leading \0 and keep a use of it: Init logs it, and
// the linker's --gc-sections would drop an array nothing refers to.
const char kBuildIdentity[] = "\0NEVR-PLUGIN-BUILD " MOD_VERSION " " MOD_COMMIT;

bool g_started = false; // Start ran and returned true
bool g_tried = false; // InitEx or Init was called: the host calls only one, but be safe

Expand All @@ -18,6 +24,7 @@ int Init(const NvrGameContext* ctx, const char* args_json) {
g_tried = true;
mod::OpenLog(ctx);
mod::Log("%s %s", MOD_STEM, MOD_VERSION);
mod::Log("%s", kBuildIdentity + 1);
mod::SetArgs(args_json ? args_json : "{}");

uint32_t stamp = 0;
Expand Down
Loading
Loading