Skip to content
Open
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
39 changes: 39 additions & 0 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
5 changes: 4 additions & 1 deletion CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -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/<short-description>` for new functionality, `fix/<short-description>` for bug fixes, `chore/<short-description>` 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.
Expand Down
13 changes: 11 additions & 2 deletions vortex-extension/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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\
<Vortex userData>\plugins\witcherscriptmerger-vortex\n
where <Vortex userData> 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`
Expand Down
1 change: 1 addition & 0 deletions vortex-extension/info.json
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
{
"id": "witcherscriptmerger-vortex",
"name": "WitcherScriptMerger Companion",
"author": "TheValiantOne/WitcherScriptMerger contributors",
"version": "0.1.0",
Expand Down
45 changes: 40 additions & 5 deletions vortex-extension/scripts/package.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -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);

Expand All @@ -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 });

Expand Down Expand Up @@ -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` +
` <Vortex userData>\\plugins\\${stageName}\\\n` +
`where <Vortex userData> 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.`,
);
Loading