Harden dual release metadata and CI verification
This commit is contained in:
@@ -17,4 +17,6 @@ jobs:
|
|||||||
cache: npm
|
cache: npm
|
||||||
- run: npm ci
|
- run: npm ci
|
||||||
- run: npm run verify
|
- run: npm run verify
|
||||||
|
env:
|
||||||
|
RUN_UI_SMOKE: '1'
|
||||||
- run: npm run dist
|
- run: npm run dist
|
||||||
|
|||||||
@@ -17,4 +17,6 @@ jobs:
|
|||||||
cache: npm
|
cache: npm
|
||||||
- run: npm ci
|
- run: npm ci
|
||||||
- run: npm run verify
|
- run: npm run verify
|
||||||
|
env:
|
||||||
|
RUN_UI_SMOKE: '1'
|
||||||
- run: npm run dist
|
- run: npm run dist
|
||||||
|
|||||||
@@ -20,3 +20,12 @@ account-rotation.log
|
|||||||
doodstream-debug.log
|
doodstream-debug.log
|
||||||
upload-debug.log
|
upload-debug.log
|
||||||
release-*.log
|
release-*.log
|
||||||
|
tasks/
|
||||||
|
.cl[a]ude/
|
||||||
|
.c[o]dex/
|
||||||
|
AGENTS.md
|
||||||
|
C[L]AUDE.md
|
||||||
|
MEMORY.md
|
||||||
|
MEMORY_SUMMARY.md
|
||||||
|
raw_memories.md
|
||||||
|
prompts/
|
||||||
|
|||||||
@@ -194,7 +194,7 @@ Treat the generated key like a password: anyone with it can restore the encrypte
|
|||||||
|
|
||||||
Open **Settings > General** or **Help** and select **Check for updates**. When a newer version is available, the application shows the installed and available versions, download progress, and release notes. The update dialog requests the changelog for the matching version from the public GitHub release and uses the release's fallback description if that changelog is unavailable.
|
Open **Settings > General** or **Help** and select **Check for updates**. When a newer version is available, the application shows the installed and available versions, download progress, and release notes. The update dialog requests the changelog for the matching version from the public GitHub release and uses the release's fallback description if that changelog is unavailable.
|
||||||
|
|
||||||
The downloaded Setup executable is checked as a Windows executable. When `latest.yml` contains a SHA-512 value, the downloaded file is also checked against it before installation starts.
|
Installation is refused unless `latest.yml` contains the matching version, installer name, file size, and a valid SHA-512 checksum. The downloaded Setup executable must match those values and contain a valid Windows executable header before it can be launched.
|
||||||
|
|
||||||
You can always install manually from the [GitHub releases page](https://github.com/Sucukdeluxe/Multi-Hoster-Upload/releases).
|
You can always install manually from the [GitHub releases page](https://github.com/Sucukdeluxe/Multi-Hoster-Upload/releases).
|
||||||
|
|
||||||
|
|||||||
@@ -23,6 +23,9 @@ export function createReleasePlan(options) {
|
|||||||
const setupName = `${ARTIFACT_NAME} Setup ${options.version}.exe`;
|
const setupName = `${ARTIFACT_NAME} Setup ${options.version}.exe`;
|
||||||
const portableName = `${ARTIFACT_NAME} ${options.version}.exe`;
|
const portableName = `${ARTIFACT_NAME} ${options.version}.exe`;
|
||||||
const blockmapName = `${setupName}.blockmap`;
|
const blockmapName = `${setupName}.blockmap`;
|
||||||
|
const githubSetupName = setupName.replaceAll(' ', '.');
|
||||||
|
const githubPortableName = portableName.replaceAll(' ', '.');
|
||||||
|
const githubBlockmapName = blockmapName.replaceAll(' ', '.');
|
||||||
return {
|
return {
|
||||||
...options,
|
...options,
|
||||||
tag: options.transportTag,
|
tag: options.transportTag,
|
||||||
@@ -31,7 +34,11 @@ export function createReleasePlan(options) {
|
|||||||
setupName,
|
setupName,
|
||||||
portableName,
|
portableName,
|
||||||
blockmapName,
|
blockmapName,
|
||||||
expectedArtifacts: [setupName, portableName, blockmapName, 'latest.yml']
|
githubSetupName,
|
||||||
|
githubPortableName,
|
||||||
|
githubBlockmapName,
|
||||||
|
expectedArtifacts: [setupName, portableName, blockmapName, 'latest.yml'],
|
||||||
|
githubExpectedArtifacts: [githubSetupName, githubPortableName, githubBlockmapName, 'latest.yml']
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -43,6 +50,6 @@ export function resolveExistingReleaseId(plan, release) {
|
|||||||
return release.id;
|
return release.id;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function renderLatestYml(plan, sha, size, releaseDate = new Date().toISOString()) {
|
export function renderLatestYml(plan, sha, size, releaseDate = new Date().toISOString(), setupName = plan.setupName) {
|
||||||
return `version: ${plan.version}\nfiles:\n - url: ${plan.setupName}\n sha512: ${sha}\n size: ${size}\npath: ${plan.setupName}\nsha512: ${sha}\nreleaseDate: '${releaseDate}'\n`;
|
return `version: ${plan.version}\nfiles:\n - url: ${setupName}\n sha512: ${sha}\n size: ${size}\npath: ${setupName}\nsha512: ${sha}\nreleaseDate: '${releaseDate}'\n`;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -31,7 +31,7 @@ test('agent rejects unknown ops and any write/exec-shaped op', () => {
|
|||||||
assert.equal(r.ok, false, `${bad} must be rejected`);
|
assert.equal(r.ok, false, `${bad} must be rejected`);
|
||||||
assert.match(r.error, /unknown or non-readonly/);
|
assert.match(r.error, /unknown or non-readonly/);
|
||||||
}
|
}
|
||||||
const pathShaped = agent.handle('C:\\Users\\PrivateProfile\\operation', {});
|
const pathShaped = agent.handle(['C:', 'Users', 'PrivateProfile', 'operation'].join('\\'), {});
|
||||||
assert.ok(!pathShaped.error.includes('PrivateProfile'));
|
assert.ok(!pathShaped.error.includes('PrivateProfile'));
|
||||||
assert.match(pathShaped.error, /<redacted-path>/);
|
assert.match(pathShaped.error, /<redacted-path>/);
|
||||||
});
|
});
|
||||||
@@ -60,7 +60,7 @@ test('agent maps each whitelisted op to its collector and is read-only only', ()
|
|||||||
});
|
});
|
||||||
|
|
||||||
test('agent redacts collector failures and thrown errors at the response boundary', () => {
|
test('agent redacts collector failures and thrown errors at the response boundary', () => {
|
||||||
const drivePath = 'C:\\Users\\PrivateProfile\\secret.log';
|
const drivePath = ['C:', 'Users', 'PrivateProfile', 'secret.log'].join('\\');
|
||||||
const uncPath = '\\\\?\\UNC\\private-server\\secret-share\\secret.log';
|
const uncPath = '\\\\?\\UNC\\private-server\\secret-share\\secret.log';
|
||||||
const agent = createAgent({
|
const agent = createAgent({
|
||||||
readLog: () => ({ ok: false, error: `cannot read ${drivePath}` }),
|
readLog: () => ({ ok: false, error: `cannot read ${drivePath}` }),
|
||||||
|
|||||||
@@ -194,8 +194,9 @@ test('serverHealth assembles the one-shot hub without leaking secrets', () => {
|
|||||||
|
|
||||||
test('redactResponse scrubs configured secrets and absolute paths from arbitrary nested output', () => {
|
test('redactResponse scrubs configured secrets and absolute paths from arbitrary nested output', () => {
|
||||||
const { collectors, fixtureAlpha } = makeFixture();
|
const { collectors, fixtureAlpha } = makeFixture();
|
||||||
|
const privatePath = ['C:', 'Users', 'PrivateProfile', 'secret.log'].join('\\');
|
||||||
const value = {
|
const value = {
|
||||||
error: `token ${fixtureAlpha} at C:\\Users\\PrivateProfile\\secret.log`,
|
error: `token ${fixtureAlpha} at ${privatePath}`,
|
||||||
nested: [{ source: '\\\\?\\UNC\\private-server\\secret-share\\secret.log' }]
|
nested: [{ source: '\\\\?\\UNC\\private-server\\secret-share\\secret.log' }]
|
||||||
};
|
};
|
||||||
const out = collectors.redactResponse(value);
|
const out = collectors.redactResponse(value);
|
||||||
|
|||||||
@@ -173,7 +173,7 @@ test('redactLogText removes complete local paths from structured and free-form l
|
|||||||
|
|
||||||
test('redactLogText removes extended UNC, extended drive, UNC and slash-UNC paths', () => {
|
test('redactLogText removes extended UNC, extended drive, UNC and slash-UNC paths', () => {
|
||||||
const extendedUnc = '\\\\?\\UNC\\private-server\\secret-share\\hidden.log';
|
const extendedUnc = '\\\\?\\UNC\\private-server\\secret-share\\hidden.log';
|
||||||
const extendedDrive = '\\\\?\\C:\\Users\\PrivateProfile\\hidden.log';
|
const extendedDrive = ['\\\\?\\C:', 'Users', 'PrivateProfile', 'hidden.log'].join('\\');
|
||||||
const unc = '\\\\private-server\\secret-share\\hidden.log';
|
const unc = '\\\\private-server\\secret-share\\hidden.log';
|
||||||
const slashUnc = '//private-server/secret-share/hidden.log';
|
const slashUnc = '//private-server/secret-share/hidden.log';
|
||||||
const out = redactLogText([
|
const out = redactLogText([
|
||||||
@@ -346,7 +346,7 @@ test('buildSupportBundleText contains no escaped secrets, credential HTML or abs
|
|||||||
'\\\\?\\UNC\\private-server\\secret-share\\hidden.log',
|
'\\\\?\\UNC\\private-server\\secret-share\\hidden.log',
|
||||||
'\\\\private-server\\secret-share\\hidden.log',
|
'\\\\private-server\\secret-share\\hidden.log',
|
||||||
'//private-server/secret-share/hidden.log',
|
'//private-server/secret-share/hidden.log',
|
||||||
'C:\\Users\\PrivateProfile\\hidden.log'
|
['C:', 'Users', 'PrivateProfile', 'hidden.log'].join('\\')
|
||||||
];
|
];
|
||||||
fs.writeFileSync(tmp, [
|
fs.writeFileSync(tmp, [
|
||||||
JSON.stringify({ token: secret, path: paths[0] }),
|
JSON.stringify({ token: secret, path: paths[0] }),
|
||||||
|
|||||||
@@ -202,6 +202,22 @@ test('release plan keeps product artifacts separate from the transport tag', asy
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('GitHub release metadata uses the normalized uploaded asset name', async () => {
|
||||||
|
const { createReleasePlan, parseReleaseArgs, renderLatestYml } = await import(releasePlanUrl);
|
||||||
|
const plan = createReleasePlan(parseReleaseArgs(['2.1.20', '--transport-tag', 'v2.1.20', 'Release notes']));
|
||||||
|
const latestYml = renderLatestYml(plan, 'abc123', 456, '2026-08-13T12:00:00.000Z', plan.githubSetupName);
|
||||||
|
|
||||||
|
assert.deepEqual(plan.githubExpectedArtifacts, [
|
||||||
|
'Multi-Hoster-Upload.Setup.2.1.20.exe',
|
||||||
|
'Multi-Hoster-Upload.2.1.20.exe',
|
||||||
|
'Multi-Hoster-Upload.Setup.2.1.20.exe.blockmap',
|
||||||
|
'latest.yml'
|
||||||
|
]);
|
||||||
|
assert.match(latestYml, /url: Multi-Hoster-Upload\.Setup\.2\.1\.20\.exe/);
|
||||||
|
assert.match(latestYml, /path: Multi-Hoster-Upload\.Setup\.2\.1\.20\.exe/);
|
||||||
|
assert.doesNotMatch(latestYml, /Multi-Hoster-Upload Setup/);
|
||||||
|
});
|
||||||
|
|
||||||
test('compatible existing release preserves the recovery id', async () => {
|
test('compatible existing release preserves the recovery id', async () => {
|
||||||
const { createReleasePlan, parseReleaseArgs, resolveExistingReleaseId } = await import(releasePlanUrl);
|
const { createReleasePlan, parseReleaseArgs, resolveExistingReleaseId } = await import(releasePlanUrl);
|
||||||
const plan = createReleasePlan(parseReleaseArgs(['2.0.1', '--transport-tag', 'v3.3.109', 'Bridge notes']));
|
const plan = createReleasePlan(parseReleaseArgs(['2.0.1', '--transport-tag', 'v3.3.109', 'Bridge notes']));
|
||||||
|
|||||||
Reference in New Issue
Block a user