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
12 changes: 9 additions & 3 deletions .github/workflows/release-desktop.yml
Original file line number Diff line number Diff line change
Expand Up @@ -702,10 +702,16 @@ jobs:
const metadata = JSON.parse(
fs.readFileSync(path.join(metadataDir, fileName), "utf8"),
);
const feedRelativePaths = [
metadata.feedRelativePath,
...(metadata.feedAliasRelativePaths ?? []),
];
const source = path.join(feedsDir, metadata.feedRelativePath);
const destination = path.join(pagesDir, channel, metadata.feedRelativePath);
fs.mkdirSync(path.dirname(destination), { recursive: true });
fs.copyFileSync(source, destination);
for (const relativePath of feedRelativePaths) {
const destination = path.join(pagesDir, channel, relativePath);
fs.mkdirSync(path.dirname(destination), { recursive: true });
fs.copyFileSync(source, destination);
}
}
EOF

Expand Down
107 changes: 98 additions & 9 deletions apps/desktop/scripts/stage-electron-release-assets.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,16 @@ function findSingleFile(rootDir, matcher, description) {
return matches[0];
}

function findOptionalSingleFile(rootDir, matcher, description) {
const matches = listFilesRecursively(rootDir).filter(matcher);
if (matches.length > 1) {
throw new Error(
`Expected at most one ${description} in ${rootDir}, found ${matches.length}.`,
);
}
return matches[0] ?? null;
}

function stripSuffix(value, suffix) {
if (!value.endsWith(suffix)) {
throw new Error(`Expected "${value}" to end with "${suffix}".`);
Expand All @@ -133,17 +143,25 @@ function electronBuilderLinuxArch(buildTarget) {
throw new Error(`Unsupported Linux build target "${buildTarget}".`);
}

function feedAliasFileNamesForBuildTarget(buildTarget) {
if (buildTarget === "aarch64-unknown-linux-gnu") {
return ["latest-linux-arm64.yml"];
}
return [];
}

function collectArtifacts(distDir, buildTarget) {
const metadataFileName = metadataFileNameForBuildTarget(buildTarget);
const feedPath = findSingleFile(
distDir,
(filePath) => path.basename(filePath) === metadataFileName,
`${metadataFileName} feed`,
);
const findFeedPath = () =>
findSingleFile(
distDir,
(filePath) => path.basename(filePath) === metadataFileName,
`${metadataFileName} feed`,
);

if (buildTarget.endsWith("-apple-darwin")) {
return {
feedPath,
feedPath: findFeedPath(),
manualAssetPath: findSingleFile(
distDir,
(filePath) => filePath.endsWith(".dmg"),
Expand Down Expand Up @@ -173,6 +191,14 @@ function collectArtifacts(distDir, buildTarget) {
const blockmapPath = fs.existsSync(`${appImagePath}.blockmap`)
? `${appImagePath}.blockmap`
: null;
const feedPath =
buildTarget === "aarch64-unknown-linux-gnu"
? findOptionalSingleFile(
distDir,
(filePath) => path.basename(filePath) === metadataFileName,
`${metadataFileName} feed`,
)
: findFeedPath();

return {
feedPath,
Expand All @@ -195,7 +221,7 @@ function collectArtifacts(distDir, buildTarget) {
}

return {
feedPath,
feedPath: findFeedPath(),
manualAssetPath: installerPath,
updaterAssetPath: installerPath,
blockmapPath,
Expand Down Expand Up @@ -225,6 +251,38 @@ function sha512Base64(filePath) {
.digest("base64");
}

function yamlString(value) {
return JSON.stringify(value);
}

function writeSyntheticFeed(destinationFeedPath, version, updaterUrl, metadata) {
fs.writeFileSync(
destinationFeedPath,
[
`version: ${yamlString(version)}`,
`releaseDate: ${yamlString(new Date().toISOString())}`,
`path: ${yamlString(updaterUrl)}`,
`sha512: ${yamlString(metadata.sha512)}`,
"files:",
` - url: ${yamlString(updaterUrl)}`,
` sha512: ${yamlString(metadata.sha512)}`,
` size: ${metadata.size}`,
"",
].join("\n"),
"utf8",
);
}

function copyFeedAliases(destinationFeedPath, outputDir, feedTarget, buildTarget) {
const aliasRelativePaths = [];
for (const aliasFileName of feedAliasFileNamesForBuildTarget(buildTarget)) {
const aliasPath = path.join(outputDir, "feeds", feedTarget, aliasFileName);
fs.copyFileSync(destinationFeedPath, aliasPath);
aliasRelativePaths.push(toPosixRelativePath(feedTarget, aliasFileName));
}
return aliasRelativePaths;
}

function rewriteFeed({
sourceFeedPath,
artifacts,
Expand Down Expand Up @@ -256,7 +314,6 @@ function rewriteFeed({
feedTarget,
metadataFileName,
);
const document = parseDocument(fs.readFileSync(sourceFeedPath, "utf8"));
const publishedAssetUrls = {
manualAssetUrl,
updaterUrl,
Expand All @@ -279,6 +336,31 @@ function rewriteFeed({
}),
};

fs.mkdirSync(path.dirname(destinationFeedPath), { recursive: true });
if (!sourceFeedPath) {
writeSyntheticFeed(
destinationFeedPath,
version,
updaterUrl,
publishedAssetMetadata.updaterUrl,
);
const feedAliasRelativePaths = copyFeedAliases(
destinationFeedPath,
outputDir,
feedTarget,
buildTarget,
);

return {
feedTarget,
metadataFileName,
destinationFeedPath,
updaterUrl,
feedAliasRelativePaths,
};
}

const document = parseDocument(fs.readFileSync(sourceFeedPath, "utf8"));
document.set("path", updaterUrl);
document.set("sha512", publishedAssetMetadata.updaterUrl.sha512);
const files = document.get("files");
Expand Down Expand Up @@ -326,14 +408,20 @@ function rewriteFeed({
}
}

fs.mkdirSync(path.dirname(destinationFeedPath), { recursive: true });
fs.writeFileSync(destinationFeedPath, document.toString(), "utf8");
const feedAliasRelativePaths = copyFeedAliases(
destinationFeedPath,
outputDir,
feedTarget,
buildTarget,
);

return {
feedTarget,
metadataFileName,
destinationFeedPath,
updaterUrl,
feedAliasRelativePaths,
};
}

Expand Down Expand Up @@ -409,6 +497,7 @@ function main() {
feed.feedTarget,
feed.metadataFileName,
),
feedAliasRelativePaths: feed.feedAliasRelativePaths,
manualAssetName,
manualAssetSizeBytes: fileSizeInBytes(
path.join(args.outputDir, manualAssetName),
Expand Down
124 changes: 124 additions & 0 deletions apps/desktop/scripts/stage-electron-release-assets.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import os from "node:os";
import path from "node:path";
import test from "node:test";
import { execFileSync } from "node:child_process";
import { parseDocument } from "yaml";

const repoRoot = path.resolve(import.meta.dirname, "..", "..", "..");
const stageScriptPath = path.join(
Expand Down Expand Up @@ -324,6 +325,7 @@ test("stage-electron-release-assets stages Linux AppImage feeds", () => {
assert.equal(metadata.updaterBlockmapAssetName, null);
assert.equal(metadata.updaterBlockmapSizeBytes, 0);
assert.equal(metadata.feedRelativePath, "linux-x64/latest-linux.yml");
assert.deepEqual(metadata.feedAliasRelativePaths, []);
assert.equal(
fs.existsSync(
path.join(outputDir, "NeverWrite-0.2.0-x64.AppImage.blockmap"),
Expand All @@ -336,3 +338,125 @@ test("stage-electron-release-assets stages Linux AppImage feeds", () => {
);
});
});

test("stage-electron-release-assets synthesizes missing Linux AppImage feeds", () => {
withTempDir((tempDir) => {
const distDir = path.join(tempDir, "dist");
const outputDir = path.join(tempDir, "staged");
const metadataOut = path.join(tempDir, "metadata", "linux-arm64.json");
const appImageContents = "arm64 appimage";
const appImageSha512 = sha512Base64(appImageContents);

writeFile(
path.join(distDir, "NeverWrite-0.2.0-arm64.AppImage"),
appImageContents,
);

execFileSync(
process.execPath,
[
stageScriptPath,
"--dist-dir",
distDir,
"--target",
"aarch64-unknown-linux-gnu",
"--version",
"0.2.0",
"--tag",
"v0.2.0",
"--repo",
"jsgrrchg/NeverWrite",
"--output-dir",
outputDir,
"--metadata-out",
metadataOut,
],
{
cwd: repoRoot,
stdio: "pipe",
},
);

const metadata = JSON.parse(fs.readFileSync(metadataOut, "utf8"));
const rewrittenFeed = fs.readFileSync(
path.join(outputDir, "feeds", "linux-arm64", "latest-linux.yml"),
"utf8",
);

assert.equal(metadata.feedTarget, "linux-arm64");
assert.equal(metadata.metadataFileName, "latest-linux.yml");
assert.equal(metadata.manualAssetName, "NeverWrite-0.2.0-arm64.AppImage");
assert.equal(metadata.updaterAssetName, "NeverWrite-0.2.0-arm64.AppImage");
assert.equal(metadata.updaterBlockmapAssetName, null);
assert.equal(metadata.updaterBlockmapSizeBytes, 0);
assert.equal(metadata.feedRelativePath, "linux-arm64/latest-linux.yml");
assert.deepEqual(metadata.feedAliasRelativePaths, [
"linux-arm64/latest-linux-arm64.yml",
]);

const aliasFeed = fs.readFileSync(
path.join(outputDir, "feeds", "linux-arm64", "latest-linux-arm64.yml"),
"utf8",
);
assert.equal(aliasFeed, rewrittenFeed);

const feedDocument = parseDocument(rewrittenFeed);
const updaterUrl =
"https://github.com/jsgrrchg/NeverWrite/releases/download/v0.2.0/NeverWrite-0.2.0-arm64.AppImage";
assert.equal(feedDocument.get("version"), "0.2.0");
assert.equal(
Number.isNaN(Date.parse(feedDocument.get("releaseDate"))),
false,
);
assert.equal(feedDocument.get("path"), updaterUrl);
assert.equal(feedDocument.get("sha512"), appImageSha512);

const files = feedDocument.get("files", true);
assert.equal(files.items.length, 1);
assert.equal(files.items[0].get("url"), updaterUrl);
assert.equal(files.items[0].get("sha512"), appImageSha512);
assert.equal(files.items[0].get("size"), 14);
});
});

test("stage-electron-release-assets requires generated Linux x64 feeds", () => {
withTempDir((tempDir) => {
const distDir = path.join(tempDir, "dist");
const outputDir = path.join(tempDir, "staged");
const metadataOut = path.join(tempDir, "metadata", "linux-x64.json");

writeFile(
path.join(distDir, "NeverWrite-0.2.0-x86_64.AppImage"),
"x64 appimage",
);

assert.throws(
() =>
execFileSync(
process.execPath,
[
stageScriptPath,
"--dist-dir",
distDir,
"--target",
"x86_64-unknown-linux-gnu",
"--version",
"0.2.0",
"--tag",
"v0.2.0",
"--repo",
"jsgrrchg/NeverWrite",
"--output-dir",
outputDir,
"--metadata-out",
metadataOut,
],
{
cwd: repoRoot,
stdio: "pipe",
},
),
/Expected exactly one latest-linux\.yml feed/,
);
});
});
2 changes: 1 addition & 1 deletion apps/desktop/src-electron/main/updater.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -141,7 +141,7 @@ describe("ElectronAppUpdater configuration", () => {
expect(status).toMatchObject({
enabled: true,
endpoint:
"https://jsgrrchg.github.io/NeverWrite/stable/linux-arm64/latest-linux.yml",
"https://jsgrrchg.github.io/NeverWrite/stable/linux-arm64/latest-linux-arm64.yml",
message: null,
});
});
Expand Down
3 changes: 3 additions & 0 deletions apps/desktop/src-electron/main/updater.ts
Original file line number Diff line number Diff line change
Expand Up @@ -281,6 +281,9 @@ function resolveMetadataFileName() {
return "latest.yml";
}
if (process.platform === "linux") {
if (process.arch === "arm64") {
return "latest-linux-arm64.yml";
}
return "latest-linux.yml";
}
return null;
Expand Down
Loading