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
41 changes: 41 additions & 0 deletions scripts/__tests__/release-workflow.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -310,6 +310,47 @@ test('local verification can repair mismatched metadata when requested', async (
assert.equal(updated.files[0].size, installerBuffer.length);
});

test('local verification resolves renamed Windows installers when metadata lacks architecture suffix', async (t) => {
const workspace = await createTemporaryWorkspace(t);
const version = '0.0.4';
const releaseDir = path.join(workspace, 'release-artifacts', 'docforge-windows-ia32');
await fs.mkdir(releaseDir, { recursive: true });

const installerName = `DocForge-Setup-${version}-ia32.exe`;
const installerPath = path.join(releaseDir, installerName);
const binary = crypto.randomBytes(4096);
await fs.writeFile(installerPath, binary);

const metadataPath = path.join(releaseDir, 'latest.yml');
const metadata = {
version,
path: `DocForge-Setup-${version}.exe`,
sha512: 'mismatched',
files: [
{
url: `DocForge-Setup-${version}.exe`,
sha512: 'mismatched',
size: 123,
},
],
};
await fs.writeFile(metadataPath, YAML.stringify(metadata), 'utf8');

await runLocalVerification(releaseDir, ['--fix-metadata']);

const expectedSha = computeSha512Base64(binary);
const updated = YAML.parse(await fs.readFile(metadataPath, 'utf8'));
assert.equal(updated.path, installerName);
assert.equal(updated.sha512, expectedSha);
if (Object.prototype.hasOwnProperty.call(updated, 'size')) {
assert.equal(updated.size, binary.length);
}
assert(Array.isArray(updated.files) && updated.files.length === 1);
assert.equal(updated.files[0].url, installerName);
assert.equal(updated.files[0].sha512, expectedSha);
assert.equal(updated.files[0].size, binary.length);
});

test('metadata updates remain isolated across artifact directories with identical installer names', async (t) => {
const workspace = await createTemporaryWorkspace(t);
const version = '0.0.3';
Expand Down
53 changes: 49 additions & 4 deletions scripts/test-auto-update.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -139,17 +139,17 @@ async function resolveLocalAsset(metadataDir, key, digestCache) {
addCandidate(normalised);
}

for (const candidate of candidates) {
const tryResolveCandidate = async (candidate) => {
const candidatePath = path.resolve(metadataDir, candidate);
const relative = path.relative(metadataDir, candidatePath);
if (relative.startsWith('..') || path.isAbsolute(relative)) {
continue;
return null;
}

try {
const stats = await fs.stat(candidatePath);
if (!stats.isFile()) {
continue;
return null;
}

let digest = digestCache.get(candidatePath);
Expand All @@ -165,7 +165,52 @@ async function resolveLocalAsset(metadataDir, key, digestCache) {
size: digest.size,
};
} catch {
// Ignore missing assets at this stage; verification will surface the issue.
return null;
}
};

let index = 0;
while (index < candidates.length) {
const candidate = candidates[index];
index += 1;
const asset = await tryResolveCandidate(candidate);
if (asset) {
return asset;
}
}

const extension = normalised ? path.extname(normalised) : path.extname(key ?? '');
const extensionLower = extension.toLowerCase();
const canonicalBase = extension ? (normalised || key).slice(0, -extension.length) : null;

if (canonicalBase && extensionLower === '.exe') {
let entries = [];
try {
entries = await fs.readdir(metadataDir);
} catch {
entries = [];
}

for (const entry of entries) {
if (seen.has(entry)) {
continue;
}
if (!entry.toLowerCase().endsWith(extensionLower)) {
continue;
}
const baseName = entry.slice(0, -extension.length);
if (baseName === canonicalBase || baseName.startsWith(`${canonicalBase}-`)) {
addCandidate(entry);
}
}

while (index < candidates.length) {
const candidate = candidates[index];
index += 1;
const asset = await tryResolveCandidate(candidate);
if (asset) {
return asset;
}
}
}

Expand Down