fix(KAN-65): normalize Hetzner conditional write ETags
CI/CD / preconditions (pull_request) Successful in 1s
Native JavaScript checks / Native launcher and release tests (pull_request) Successful in 1m5s
macOS checks / Native Rust tests and unsigned build (pull_request) Successful in 1m34s
CI/CD / Publish immutable image (pull_request) Canceled after 0s
CI/CD / test_and_check_app (pull_request) Canceled after 5m16s
CI/CD / preconditions (pull_request) Successful in 1s
Native JavaScript checks / Native launcher and release tests (pull_request) Successful in 1m5s
macOS checks / Native Rust tests and unsigned build (pull_request) Successful in 1m34s
CI/CD / Publish immutable image (pull_request) Canceled after 0s
CI/CD / test_and_check_app (pull_request) Canceled after 5m16s
This commit is contained in:
+67
-10
@@ -63,6 +63,23 @@ export function storageConfiguration(env = process.env) {
|
||||
|
||||
const sha256 = (data) => createHash('sha256').update(data).digest('hex');
|
||||
|
||||
export function conditionalEtag(etag) {
|
||||
const value =
|
||||
typeof etag === 'string' && etag.startsWith('"') && etag.endsWith('"')
|
||||
? etag.slice(1, -1)
|
||||
: etag;
|
||||
if (
|
||||
typeof value !== 'string' ||
|
||||
!/^[a-f\d]{32}(?:-[1-9]\d*)?$/i.test(value)
|
||||
) {
|
||||
throw new Error(
|
||||
'Cannot atomically update the feed without a valid S3 ETag.',
|
||||
);
|
||||
}
|
||||
// Hetzner returns quoted ETags on GET but compares bare values on conditional PUT.
|
||||
return value;
|
||||
}
|
||||
|
||||
function requiresExistingObjectCheck(error) {
|
||||
// AWS CLI can crash while formatting Hetzner's empty error Message.
|
||||
// This only permits a readback; content checks and conditional writes still apply.
|
||||
@@ -346,12 +363,7 @@ export async function publishMacos({
|
||||
);
|
||||
}
|
||||
} else {
|
||||
if (!response.ETag) {
|
||||
throw new Error(
|
||||
'Cannot atomically update the feed without its ETag.',
|
||||
);
|
||||
}
|
||||
publishLatest(['--if-match', response.ETag]);
|
||||
publishLatest(['--if-match', conditionalEtag(response.ETag)]);
|
||||
}
|
||||
}
|
||||
await verifyPublic(
|
||||
@@ -365,6 +377,49 @@ export async function publishMacos({
|
||||
}
|
||||
}
|
||||
|
||||
export async function resumeMacos(options = {}) {
|
||||
const env = options.env ?? process.env;
|
||||
const metadata = options.metadata ?? releaseMetadata(env);
|
||||
const storage = storageConfiguration(env);
|
||||
const feedBase = options.feedBase ?? updateSource.baseUrl;
|
||||
if (`${storage.publicBase}/${storage.prefix}` !== feedBase) {
|
||||
throw new Error(
|
||||
'Release storage does not match the update source bundled in the app.',
|
||||
);
|
||||
}
|
||||
const directory = mkdtempSync(join(tmpdir(), 'kanbangit-resume-'));
|
||||
const fetchPublic = options.fetchPublic ?? fetch;
|
||||
const prefix = `${feedBase}/${metadata.target}/${metadata.version}`;
|
||||
try {
|
||||
for (const name of [
|
||||
metadata.filename,
|
||||
`${metadata.filename}.sha256`,
|
||||
metadata.updaterFilename,
|
||||
`${metadata.updaterFilename}.sha256`,
|
||||
`${metadata.updaterFilename}.sig`,
|
||||
]) {
|
||||
const response = await fetchPublic(`${prefix}/${name}`, {
|
||||
redirect: 'error',
|
||||
signal: AbortSignal.timeout(60000),
|
||||
});
|
||||
if (!response.ok) {
|
||||
await response.body?.cancel();
|
||||
throw new Error(
|
||||
`Cannot resume incomplete release: ${name} HTTP ${response.status}.`,
|
||||
);
|
||||
}
|
||||
writeFileSync(
|
||||
join(directory, name),
|
||||
Buffer.from(await response.arrayBuffer()),
|
||||
);
|
||||
}
|
||||
// Reuse signature, checksum, immutable content and feed concurrency checks.
|
||||
return await publishMacos({ ...options, env, metadata, directory });
|
||||
} finally {
|
||||
rmSync(directory, { recursive: true, force: true });
|
||||
}
|
||||
}
|
||||
|
||||
if (
|
||||
process.argv[1] &&
|
||||
import.meta.url === pathToFileURL(resolve(process.argv[1])).href
|
||||
@@ -373,10 +428,12 @@ if (
|
||||
if (process.argv[2] === 'config') {
|
||||
console.log(JSON.stringify(storageConfiguration()));
|
||||
} else if (
|
||||
process.argv[2] === 'upload' &&
|
||||
process.env.RELEASE_DIRECTORY
|
||||
process.argv[2] === 'resume' ||
|
||||
(process.argv[2] === 'upload' && process.env.RELEASE_DIRECTORY)
|
||||
) {
|
||||
const url = await publishMacos({
|
||||
const publish =
|
||||
process.argv[2] === 'resume' ? resumeMacos : publishMacos;
|
||||
const url = await publish({
|
||||
directory: process.env.RELEASE_DIRECTORY,
|
||||
metadata: releaseMetadata(),
|
||||
});
|
||||
@@ -389,7 +446,7 @@ if (
|
||||
}
|
||||
} else {
|
||||
throw new Error(
|
||||
'Use config or upload; upload requires RELEASE_DIRECTORY.',
|
||||
'Use config, upload or resume; upload requires RELEASE_DIRECTORY.',
|
||||
);
|
||||
}
|
||||
} catch (error) {
|
||||
|
||||
Reference in New Issue
Block a user