diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 298146f..b2c930a 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -40,3 +40,42 @@ jobs: - name: Verify formatting run: dotnet format whitespace WitcherScriptMerger.sln --verify-no-changes + + # The Vortex extension is roughly half of recent activity in this repo and had no CI + # coverage at all - build.yml ran only .NET steps, so a TypeScript compile error, a lint + # failure, or a broken test could land on main unnoticed. Runs on ubuntu (no .NET or + # Windows dependency here) and only when the extension actually changed. + vortex-extension: + name: Vortex extension (typecheck, lint, test) + runs-on: ubuntu-latest + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Setup Node + uses: actions/setup-node@v4 + with: + node-version: '20' + cache: npm + cache-dependency-path: vortex-extension/package-lock.json + + # `npm ci` (not `npm install`) so the lockfile is authoritative and a drifted + # lockfile fails the build instead of being silently rewritten. + - name: Install dependencies + working-directory: vortex-extension + run: npm ci + + - name: Typecheck + working-directory: vortex-extension + run: npm run typecheck + + - name: Lint + working-directory: vortex-extension + run: npm run lint + + # `npm test` is the src/ unit suite. The test/ integration suite is deliberately NOT + # run here: it spawns a real WitcherScriptMerger binary, which CI has no copy of. + - name: Test + working-directory: vortex-extension + run: npm test diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index cd8e944..c2f31aa 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -18,7 +18,10 @@ Match the existing source (e.g. `Inventory/FileMerger.cs`, `Controls/SMTree.cs`) - **`main` is protected.** No direct commits or pushes — all changes land via pull request. Force-pushes and branch deletion are disabled on `main` at the GitHub level. - **Branch per feature/fix**, off `main`: `feature/` for new functionality, `fix/` for bug fixes, `chore/` for tooling/process/docs changes not tied to a feature or bug. Keep the description short and kebab-case (e.g. `fix/kdiff3-encoding-mismatch`). -- **Pull requests require 2 approving reviews** before merge (GitHub branch protection on `main`). This applies to everyone, including repository admins in normal circumstances — admin bypass exists at the platform level for genuine emergencies, not as a routine shortcut. +- **Pull requests require 1 approving review** before merge (GitHub branch protection on `main`, + verified via `gh api repos/TheValiantOne/WitcherScriptMerger/branches/main/protection`: + `required_approving_review_count: 1`). This file previously said 2, which did not match the + repository's actual settings. This applies to everyone, including repository admins in normal circumstances — admin bypass exists at the platform level for genuine emergencies, not as a routine shortcut. - **PR description should cover**: what changed and why, and specifically *how you verified it* (see Testing below). "Builds successfully" is necessary but not sufficient for anything touching hash output, `MergeInventory.xml` schema, QuickBMS/wcc_lite invocation, the DiffPlex-based merge engine, or encoding handling; see `WitcherScriptMerger.Core/CLAUDE.md`'s "Hash format", "DiffPlexMergeEngine", and "Text-merge input encoding" sections for why those are load-bearing, and `WitcherScriptMerger.Tests/CLAUDE.md` for the verification pattern this codebase uses to cover them. - Commit messages are short, descriptive sentences (e.g. `Fixed crash after canceling file-open.`, `Replace hand-ported xxHash32 with System.IO.Hashing`). A `Category:` prefix (`Fixed:`, etc.) shows up occasionally but isn't enforced. No Conventional Commits format required. - GitHub Actions CI (`.github/workflows/build.yml`) runs `dotnet build --configuration Release` and `dotnet format whitespace --verify-no-changes` on every PR targeting `main`, but don't rely on it to catch problems for you — run both locally first: `dotnet build WitcherScriptMerger.sln --configuration Release` and `dotnet format whitespace WitcherScriptMerger.sln --verify-no-changes` before opening a PR. Catching failures before CI does saves a round trip. diff --git a/vortex-extension/README.md b/vortex-extension/README.md index 197d02f..fcc024f 100644 --- a/vortex-extension/README.md +++ b/vortex-extension/README.md @@ -130,7 +130,11 @@ bundling/redistributing anything itself: wcc_lite's) - the status tile only detects an existing local install or links to QuickBMS's own homepage, mirroring WSM's own GUI for this exact dependency. -## Install (manual - not yet published anywhere) +## Install (manual) + +> A `companion-0.1.0` GitHub release exists; this section covers installing it (or a +> locally-built zip) by hand. The extension is not yet listed in Vortex's in-app +> extension browser. ``` cd vortex-extension @@ -149,7 +153,12 @@ To install: extract that zip's contents (or copy the staged folder's contents) s `index.js` and `info.json` land directly inside ``` -%APPDATA%\Vortex\plugins\witcherscriptmerger-vortex\ +\plugins\witcherscriptmerger-vortex\n +where depends on how Vortex was installed: + %APPDATA%\Vortex (default, per-user) + C:\ProgramData ortex (shared/multi-user storage) + +If unsure, Vortex's own Settings -> Mods page shows the paths it is using. ``` The folder name under `plugins\` is arbitrary - `info.json` declares no explicit `id` diff --git a/vortex-extension/info.json b/vortex-extension/info.json index cfa6afa..fde5511 100644 --- a/vortex-extension/info.json +++ b/vortex-extension/info.json @@ -1,4 +1,5 @@ { + "id": "witcherscriptmerger-vortex", "name": "WitcherScriptMerger Companion", "author": "TheValiantOne/WitcherScriptMerger contributors", "version": "0.1.0", diff --git a/vortex-extension/scripts/package.mjs b/vortex-extension/scripts/package.mjs index 941ad97..de07139 100644 --- a/vortex-extension/scripts/package.mjs +++ b/vortex-extension/scripts/package.mjs @@ -39,10 +39,12 @@ const INFO_JSON = path.join(ROOT, 'info.json'); const RELEASE_DIR = path.join(ROOT, 'release'); const pkg = JSON.parse(fs.readFileSync(path.join(ROOT, 'package.json'), 'utf8')); -// The staged folder's own name - not read from info.json (which declares no explicit -// "id" field; @nexusmods/vortex-api's own IExtension typing marks `id` optional), so -// there's no single canonical extension id to derive this from. package.json's own -// `name` is used instead, matching this repo's git history/npm package identity. +// The staged folder's own name. info.json now declares an explicit "id" +// (@nexusmods/vortex-api's IExtension typing marks it optional, but Vortex uses it as +// the extension's stable identity), and the two are asserted equal below - so this could +// read from either. It stays on package.json's `name` because that's also what the zip +// filename and this repo's npm package identity use; the assertion is what keeps them +// from drifting apart. const stageName = pkg.name; const stageDir = path.join(RELEASE_DIR, stageName); @@ -55,6 +57,37 @@ if (!fs.existsSync(INFO_JSON)) { process.exit(1); } +// package.json and info.json carry the version independently, and nothing used to +// reconcile them: the produced zip is named from package.json's version while the +// manifest Vortex actually reads is info.json's, so a one-sided bump ships an archive +// whose filename disagrees with the version Vortex reports. Fail the package step +// rather than emit that. +const info = JSON.parse(fs.readFileSync(INFO_JSON, 'utf8')); +if (info.version !== pkg.version) { + console.error( + `Version mismatch: package.json says '${pkg.version}' but info.json says '${info.version}'. ` + + `The zip is named from package.json while Vortex reads info.json, so these must agree - ` + + `update both before packaging.`, + ); + process.exit(1); +} + +// The id is what Vortex uses as the extension's stable identity. Without it, identity +// and the installed folder name derive from the archive filename and can change between +// releases, which makes an update look like a different extension. +if (!info.id) { + console.error(`info.json is missing an 'id' - Vortex needs a stable extension id that doesn't change between releases.`); + process.exit(1); +} +if (info.id !== pkg.name) { + console.error( + `Identity mismatch: package.json name is '${pkg.name}' but info.json id is '${info.id}'. ` + + `The staged folder and zip are named from the former while Vortex identifies the extension ` + + `by the latter, so a divergence installs under one name and registers under another.`, + ); + process.exit(1); +} + fs.rmSync(RELEASE_DIR, { recursive: true, force: true }); fs.mkdirSync(stageDir, { recursive: true }); @@ -115,6 +148,8 @@ console.log(`\nPackaged: ${zipPath}`); console.log(`Staged (unzipped) folder: ${stageDir}`); console.log( `\nManual install: extract the zip (or copy the staged folder's contents) so they land directly in\n` + - ` %APPDATA%\\Vortex\\plugins\\${stageName}\\\n` + + ` \\plugins\\${stageName}\\\n` + + `where is %APPDATA%\\Vortex for a default per-user install, or\n` + + `C:\\ProgramData\\vortex when Vortex is set up with shared/multi-user storage.\n` + `i.e. that folder should directly contain index.js and info.json, not a nested subfolder.`, );