Skip to content
Draft
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
164 changes: 164 additions & 0 deletions .github/workflows/windows-release.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,164 @@
name: Release for Windows

on:
push:
tags:
- "v*"
workflow_dispatch:
inputs:
tag:
description: "Git tag to build"
required: true
type: string

jobs:
resolve-release:
runs-on: ubuntu-latest
outputs:
tag_name: ${{ steps.parse-tag.outputs.tag_name }}
tag_version: ${{ steps.parse-tag.outputs.tag_version }}
release_env: ${{ steps.parse-tag.outputs.release_env }}
steps:
- name: Resolve tag and release environment
id: parse-tag
run: |
if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then
TAG_NAME="${{ inputs.tag }}"
else
TAG_NAME="${GITHUB_REF_NAME}"
fi

if [[ ! "$TAG_NAME" =~ ^v.+$ ]]; then
echo "::error title=Invalid tag::Tag must start with 'v'. Got: $TAG_NAME"
exit 1
fi

TAG_NO_PREFIX="${TAG_NAME#v}"
if [[ "$TAG_NAME" == *-dev ]]; then
RELEASE_ENV="dev"
TAG_VERSION="${TAG_NO_PREFIX%-dev}"
else
RELEASE_ENV="prod"
TAG_VERSION="$TAG_NO_PREFIX"
fi

if [ -z "$TAG_VERSION" ]; then
echo "::error title=Invalid tag::Could not derive version from tag: $TAG_NAME"
exit 1
fi

echo "tag_name=$TAG_NAME" >> "$GITHUB_OUTPUT"
echo "tag_version=$TAG_VERSION" >> "$GITHUB_OUTPUT"
echo "release_env=$RELEASE_ENV" >> "$GITHUB_OUTPUT"

build-windows-unsigned:
needs: resolve-release
strategy:
fail-fast: false
matrix:
include:
- arch: x64
runner: windows-2022
- arch: arm64
runner: windows-11-arm
runs-on: ${{ matrix.runner }}

steps:
- name: Checkout repository at release tag
uses: actions/checkout@v6
with:
ref: refs/tags/${{ needs.resolve-release.outputs.tag_name }}

- name: Install node
uses: actions/setup-node@v6
with:
node-version: "24.x"
cache: "npm"

- name: Make sure the version string matches the tag version
shell: pwsh
run: |
$pkgVersion = node -e "console.log(require('./package.json').version)"
$pkgBaseVersion = $pkgVersion -replace '-dev$',''
$tagVersion = "${{ needs.resolve-release.outputs.tag_version }}"
if ($pkgBaseVersion -ne $tagVersion) {
Write-Error "Version check failed: package.json version '$pkgVersion' does not match tag version '$tagVersion'"
exit 1
}

- name: Build unsigned Windows release artifacts
shell: pwsh
run: |
npm run make-windows-unsigned -- "${{ needs.resolve-release.outputs.release_env }}" "${{ matrix.arch }}"

- name: Generate checksums for unsigned artifacts
shell: pwsh
run: |
$checksumFile = "windows-unsigned-checksums-${{ matrix.arch }}.txt"
if (Test-Path "out/make") {
$artifactDir = "out/make"
} elseif (Test-Path "out") {
$artifactDir = "out"
} else {
Write-Error "No artifact output directory found. Expected out/make or out"
exit 1
}

"ARTIFACT_DIR=$artifactDir" | Out-File -FilePath $env:GITHUB_ENV -Encoding utf8 -Append

$files = Get-ChildItem -Path $artifactDir -File -Recurse | Sort-Object FullName
if (-not $files -or $files.Count -eq 0) {
Write-Error "No files found under $artifactDir to checksum"
exit 1
}

$lines = @()
foreach ($file in $files) {
$relativePath = [System.IO.Path]::GetRelativePath($PWD.Path, $file.FullName) -replace '\\','/'
$hash = (Get-FileHash -Path $file.FullName -Algorithm SHA256).Hash.ToLower()
$lines += "$hash $relativePath"
}

Set-Content -Path $checksumFile -Value $lines

- name: Upload unsigned Windows artifacts
uses: actions/upload-artifact@v7
with:
name: windows-unsigned-${{ needs.resolve-release.outputs.release_env }}-${{ matrix.arch }}
path: |
${{ env.ARTIFACT_DIR }}/**
windows-unsigned-checksums-${{ matrix.arch }}.txt

publish-manifest:
needs:
- resolve-release
- build-windows-unsigned
runs-on: ubuntu-latest

steps:
- name: Checkout repository
uses: actions/checkout@v6

- name: Download unsigned Windows artifacts
uses: actions/download-artifact@v8
with:
pattern: windows-unsigned-${{ needs.resolve-release.outputs.release_env }}-*
path: downloaded-artifacts

- name: Build windows-release-manifest.json
env:
RELEASE_ENV: ${{ needs.resolve-release.outputs.release_env }}
TAG_NAME: ${{ needs.resolve-release.outputs.tag_name }}
TAG_VERSION: ${{ needs.resolve-release.outputs.tag_version }}
COMMIT_SHA: ${{ github.sha }}
RUN_ID: ${{ github.run_id }}
RUN_ATTEMPT: ${{ github.run_attempt }}
ARTIFACTS_ROOT: downloaded-artifacts
run: |
node ./scripts/build-windows-release-manifest.mjs

- name: Upload windows release manifest
uses: actions/upload-artifact@v7
with:
name: windows-release-manifest-${{ needs.resolve-release.outputs.release_env }}
path: windows-release-manifest.json
4 changes: 2 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

6 changes: 5 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "cyd",
"private": true,
"version": "1.1.22",
"version": "1.1.23-dev",
"main": ".vite/build/main.js",
"description": "Automatically delete your data from tech platforms, except for what you want to keep",
"license": "proprietary",
Expand All @@ -16,6 +16,10 @@
"make-local": "node ./scripts/make.js make local",
"make-dev": "node ./scripts/make.js make dev",
"make-prod": "node ./scripts/make.js make prod",
"make-windows-unsigned": "node ./scripts/make-windows-unsigned.mjs",
"finalize-windows-release": "node ./scripts/finalize-windows-release.mjs",
"release-windows-dev": "powershell -ExecutionPolicy Bypass -File ./scripts/release-windows-dev.ps1",
"release-windows-prod": "powershell -ExecutionPolicy Bypass -File ./scripts/release-windows-prod.ps1",
"make-dev-docker": "docker build -t cyd-builder . && docker run --rm -v \"$(pwd)\":/workspace -w /workspace cyd-builder npm run make-dev",
"make-prod-docker": "docker build -t cyd-builder . && docker run --rm -v \"$(pwd)\":/workspace -w /workspace cyd-builder npm run make-prod",
"publish-dev": "node ./scripts/make.js publish dev",
Expand Down
83 changes: 83 additions & 0 deletions scripts/build-windows-release-manifest.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
/* global process */
import fs from "fs";
import path from "path";

const root = process.env.ARTIFACTS_ROOT || "downloaded-artifacts";

const artifactDirs = fs
.readdirSync(root, { withFileTypes: true })
.filter((entry) => entry.isDirectory())
.map((entry) => entry.name)
.sort();

if (artifactDirs.length === 0) {
throw new Error(`No downloaded artifacts found in ${root}.`);
}

function parseChecksums(text) {
const lines = text
.split(/\r?\n/)
.map((line) => line.trim())
.filter(Boolean);

return lines.map((line) => {
const match = line.match(/^([a-f0-9]{64})\s+(.+)$/i);
if (!match) {
throw new Error(`Invalid checksum line: ${line}`);
}

return {
sha256: match[1].toLowerCase(),
path: match[2],
};
});
}

const artifacts = artifactDirs.map((artifactName) => {
const artifactPath = path.join(root, artifactName);
const files = fs.readdirSync(artifactPath);
const checksumFile = files.find(
(name) =>
name.startsWith("windows-unsigned-checksums-") && name.endsWith(".txt"),
);

if (!checksumFile) {
throw new Error(
`Missing checksum file in artifact directory: ${artifactName}`,
);
}

const arch = checksumFile
.replace("windows-unsigned-checksums-", "")
.replace(".txt", "");

const checksumText = fs.readFileSync(
path.join(artifactPath, checksumFile),
"utf8",
);
const checksums = parseChecksums(checksumText);

return {
name: artifactName,
arch,
checksumFile,
fileCount: checksums.length,
checksums,
};
});

const manifest = {
tag: process.env.TAG_NAME,
version: process.env.TAG_VERSION,
env: process.env.RELEASE_ENV,
commitSha: process.env.COMMIT_SHA,
workflowRunId: Number(process.env.RUN_ID),
workflowRunAttempt: Number(process.env.RUN_ATTEMPT),
generatedAt: new Date().toISOString(),
artifacts,
};

fs.writeFileSync(
"windows-release-manifest.json",
JSON.stringify(manifest, null, 2) + "\n",
);
34 changes: 34 additions & 0 deletions scripts/finalize-windows-release.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
/* global console */
import process from "process";
import { execSync } from "child_process";

const validModes = ["dev", "prod"];
const mode = process.argv[2];
if (!validModes.includes(mode)) {
console.error(
`Invalid mode: "${mode}". Valid modes are: ${validModes.join(", ")}`,
);
process.exit(1);
}

const args = process.argv.slice(3);
const arch = args.find((value) => !value.startsWith("--"));
const skipClean = args.includes("--skip-clean");

process.env.CYD_ENV = mode;
process.env.WINDOWS_RELEASE = "true";
process.env.SQUIRREL_TEMP = "build\\SquirrelTemp";
process.env.DEBUG =
"electron-packager,electron-universal,electron-forge*,electron-installer*";

try {
if (!skipClean) {
execSync("node ./scripts/clean.mjs", { stdio: "inherit" });
}

const archFlag = arch ? ` --arch ${arch}` : "";
execSync(`electron-forge publish${archFlag}`, { stdio: "inherit" });
} catch (error) {
console.error("Error executing Windows finalization:", error.message);
process.exit(1);
}
34 changes: 34 additions & 0 deletions scripts/make-windows-unsigned.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
/* global console */
import process from "process";
import { execSync } from "child_process";

const validModes = ["dev", "prod"];
const mode = process.argv[2];
if (!validModes.includes(mode)) {
console.error(
`Invalid mode: "${mode}". Valid modes are: ${validModes.join(", ")}`,
);
process.exit(1);
}

const arch = process.argv.slice(3).find((value) => !value.startsWith("--"));

process.env.CYD_ENV = mode;
process.env.WINDOWS_RELEASE = "false";
process.env.SQUIRREL_TEMP = "build\\SquirrelTemp";
process.env.DEBUG =
"electron-packager,electron-universal,electron-forge*,electron-installer*";

try {
// Keep parity with existing release flow until CI-only build path is introduced.
execSync("node ./scripts/clean.mjs", { stdio: "inherit" });

const archFlag = arch ? ` --arch ${arch}` : "";
execSync(
`electron-forge make --platform win32 --targets @electron-forge/maker-squirrel${archFlag}`,
{ stdio: "inherit" },
);
} catch (error) {
console.error("Error executing unsigned Windows build:", error.message);
process.exit(1);
}
20 changes: 20 additions & 0 deletions scripts/make.js
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,26 @@ if (platform == "win32") {
}

try {
if (platform === "win32" && command === "publish") {
if (mode === "local") {
console.error('Windows publish supports only "dev" and "prod" modes.');
process.exit(1);
}

// Phase 1 split: keep current publish entrypoint but delegate to explicit
// unsigned-build and finalization scripts.
execSync(`node ./scripts/make-windows-unsigned.mjs ${mode}`, {
stdio: "inherit",
});
execSync(
`node ./scripts/finalize-windows-release.mjs ${mode} --skip-clean`,
{
stdio: "inherit",
},
);
process.exit(0);
}

// Clean up previous builds and install dependencies
execSync(`node ./scripts/clean.mjs`, { stdio: "inherit" });

Expand Down
Loading
Loading