From ae2c03042e8fd6947fea63784a5e65d73cd137b3 Mon Sep 17 00:00:00 2001 From: wiiiii123 Date: Mon, 27 Apr 2026 15:26:03 +0700 Subject: [PATCH] ci: attest packaged release artifacts --- .github/workflows/build.yml | 33 ++++++++++++ .github/workflows/release.yml | 79 +++++++++++++++++++++++++++++ package.json | 1 + scripts/write-release-checksums.mjs | 57 +++++++++++++++++++++ 4 files changed, 170 insertions(+) create mode 100644 scripts/write-release-checksums.mjs diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index b2f85bb8..642e4585 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -4,6 +4,12 @@ name: Build Electron App on: workflow_dispatch: +permissions: + contents: read + id-token: write + attestations: write + artifact-metadata: write + jobs: build-windows: runs-on: windows-latest @@ -40,6 +46,14 @@ jobs: - name: Smoke test packaged Windows paths run: npm run smoke:packaged-binaries + - name: Generate Windows artifact checksums + run: npm run checksums:release -- SHA256SUMS-windows.txt + + - name: Attest Windows artifacts + uses: actions/attest@v4 + with: + subject-checksums: release/SHA256SUMS-windows.txt + - name: Upload Windows build uses: actions/upload-artifact@v4 with: @@ -48,6 +62,7 @@ jobs: release/*.exe release/*.blockmap release/latest*.yml + release/SHA256SUMS*.txt if-no-files-found: error retention-days: 30 @@ -102,6 +117,14 @@ jobs: PACKAGED_SMOKE_ARCH_TAGS: ${{ matrix.arch_tag }} run: npm run smoke:packaged-binaries + - name: Generate macOS artifact checksums + run: npm run checksums:release -- SHA256SUMS-macos-${{ matrix.arch }}.txt + + - name: Attest macOS artifacts + uses: actions/attest@v4 + with: + subject-checksums: release/SHA256SUMS-macos-${{ matrix.arch }}.txt + - name: Upload macOS build uses: actions/upload-artifact@v4 with: @@ -110,6 +133,7 @@ jobs: release/**/*.dmg release/**/*.zip release/latest-mac.yml + release/SHA256SUMS*.txt if-no-files-found: error retention-days: 30 @@ -155,6 +179,14 @@ jobs: - name: Smoke test packaged Linux paths run: npm run smoke:packaged-binaries + - name: Generate Linux artifact checksums + run: npm run checksums:release -- SHA256SUMS-linux.txt + + - name: Attest Linux artifacts + uses: actions/attest@v4 + with: + subject-checksums: release/SHA256SUMS-linux.txt + - name: Upload Linux build uses: actions/upload-artifact@v4 with: @@ -162,5 +194,6 @@ jobs: path: | release/**/*.AppImage release/latest-linux.yml + release/SHA256SUMS*.txt if-no-files-found: error retention-days: 30 diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index cbb5f2df..493be50d 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -13,6 +13,9 @@ on: permissions: contents: write actions: write + id-token: write + attestations: write + artifact-metadata: write concurrency: group: release-${{ github.event.release.tag_name || github.event.inputs.tag_name || github.run_id }} @@ -153,6 +156,14 @@ jobs: PACKAGED_SMOKE_ARCH_TAGS: darwin-x64 run: npm run smoke:packaged-binaries + - name: Generate macOS x64 artifact checksums + run: npm run checksums:release -- SHA256SUMS-macos-x64.txt + + - name: Attest macOS x64 artifacts + uses: actions/attest@v4 + with: + subject-checksums: release/SHA256SUMS-macos-x64.txt + - name: Upload macOS x64 artifacts uses: actions/upload-artifact@v4 with: @@ -162,6 +173,7 @@ jobs: release/*.zip release/*.blockmap release/latest-mac.yml + release/SHA256SUMS*.txt if-no-files-found: error build-macos-arm64: @@ -245,6 +257,14 @@ jobs: PACKAGED_SMOKE_ARCH_TAGS: darwin-arm64 run: npm run smoke:packaged-binaries + - name: Generate macOS arm64 artifact checksums + run: npm run checksums:release -- SHA256SUMS-macos-arm64.txt + + - name: Attest macOS arm64 artifacts + uses: actions/attest@v4 + with: + subject-checksums: release/SHA256SUMS-macos-arm64.txt + - name: Upload macOS arm64 artifacts uses: actions/upload-artifact@v4 with: @@ -254,6 +274,7 @@ jobs: release/*.zip release/*.blockmap release/latest-mac.yml + release/SHA256SUMS*.txt if-no-files-found: error merge-macos-update-metadata: @@ -399,6 +420,14 @@ jobs: - name: Smoke test packaged Windows x64 paths run: npm run smoke:packaged-binaries + - name: Generate Windows x64 artifact checksums + run: npm run checksums:release -- SHA256SUMS-windows-x64.txt + + - name: Attest Windows x64 artifacts + uses: actions/attest@v4 + with: + subject-checksums: release/SHA256SUMS-windows-x64.txt + - name: Upload Windows x64 artifacts uses: actions/upload-artifact@v4 with: @@ -407,6 +436,7 @@ jobs: release/*.exe release/*.blockmap release/latest*.yml + release/SHA256SUMS*.txt if-no-files-found: error build-linux-x64: @@ -464,6 +494,14 @@ jobs: - name: Smoke test packaged Linux x64 paths run: npm run smoke:packaged-binaries + - name: Generate Linux x64 artifact checksums + run: npm run checksums:release -- SHA256SUMS-linux-x64.txt + + - name: Attest Linux x64 artifacts + uses: actions/attest@v4 + with: + subject-checksums: release/SHA256SUMS-linux-x64.txt + - name: Upload Linux x64 artifacts uses: actions/upload-artifact@v4 with: @@ -472,6 +510,7 @@ jobs: release/*.AppImage release/*.blockmap release/latest-linux.yml + release/SHA256SUMS*.txt if-no-files-found: error publish-release-assets: @@ -515,6 +554,45 @@ jobs: name: linux-x64-release path: release-assets/linux-x64 + - name: Generate release asset checksums + shell: bash + run: | + set -euo pipefail + shopt -s nullglob + + assets=( + release-assets/macos-x64/*.dmg + release-assets/macos-x64/*.zip + release-assets/macos-x64/*.blockmap + release-assets/macos-arm64/*.dmg + release-assets/macos-arm64/*.zip + release-assets/macos-arm64/*.blockmap + release-assets/macos-merged/latest-mac.yml + release-assets/windows-x64/*.exe + release-assets/windows-x64/*.blockmap + release-assets/windows-x64/latest*.yml + release-assets/linux-x64/*.AppImage + release-assets/linux-x64/*.blockmap + release-assets/linux-x64/latest-linux.yml + ) + + if [ ${#assets[@]} -eq 0 ]; then + echo "No release assets found to checksum." + exit 1 + fi + + : > release-assets/SHA256SUMS.txt + for file in "${assets[@]}"; do + digest="$(sha256sum "$file" | awk '{print $1}')" + printf '%s %s\n' "$digest" "$(basename "$file")" >> release-assets/SHA256SUMS.txt + done + sort -k2,2 release-assets/SHA256SUMS.txt -o release-assets/SHA256SUMS.txt + + - name: Attest release assets + uses: actions/attest@v4 + with: + subject-checksums: release-assets/SHA256SUMS.txt + - name: Upload release assets to GitHub shell: bash run: | @@ -535,6 +613,7 @@ jobs: release-assets/linux-x64/*.AppImage release-assets/linux-x64/*.blockmap release-assets/linux-x64/latest-linux.yml + release-assets/SHA256SUMS.txt ) if [ ${#assets[@]} -eq 0 ]; then diff --git a/package.json b/package.json index 7c47f8d4..a2ca0da2 100644 --- a/package.json +++ b/package.json @@ -34,6 +34,7 @@ "i18n:check": "node scripts/i18n-check.mjs", "benchmark:export-queues": "node scripts/benchmark-export-queues.mjs", "smoke:packaged-binaries": "node scripts/smoke-packaged-binaries.mjs", + "checksums:release": "node scripts/write-release-checksums.mjs", "release:create": "node scripts/create-release.mjs", "test": "vitest --run", "test:watch": "vitest" diff --git a/scripts/write-release-checksums.mjs b/scripts/write-release-checksums.mjs new file mode 100644 index 00000000..08ef39e9 --- /dev/null +++ b/scripts/write-release-checksums.mjs @@ -0,0 +1,57 @@ +import { createHash } from "node:crypto"; +import { existsSync, readdirSync, readFileSync, statSync, writeFileSync } from "node:fs"; +import path from "node:path"; + +const projectRoot = process.cwd(); +const releaseRoot = path.join(projectRoot, "release"); +const outputFileName = process.argv[2] ?? "SHA256SUMS.txt"; +const outputPath = path.join(releaseRoot, outputFileName); +const releaseArtifactExtensions = new Set([".AppImage", ".blockmap", ".dmg", ".exe", ".zip"]); + +function relativePath(filePath) { + return path.relative(projectRoot, filePath).replaceAll("\\", "/"); +} + +function artifactSubjectName(filePath) { + return path.basename(filePath); +} + +function isReleaseArtifact(fileName) { + if (fileName.startsWith("SHA256SUMS")) { + return false; + } + + if (fileName.startsWith("latest") && fileName.endsWith(".yml")) { + return true; + } + + return releaseArtifactExtensions.has(path.extname(fileName)); +} + +function sha256(filePath) { + return createHash("sha256").update(readFileSync(filePath)).digest("hex"); +} + +if (!existsSync(releaseRoot)) { + throw new Error("[release-checksums] release directory is missing"); +} + +const releaseArtifacts = readdirSync(releaseRoot) + .map((entry) => path.join(releaseRoot, entry)) + .filter((filePath) => statSync(filePath).isFile()) + .filter((filePath) => isReleaseArtifact(path.basename(filePath))) + .sort((left, right) => relativePath(left).localeCompare(relativePath(right))); + +if (releaseArtifacts.length === 0) { + throw new Error("[release-checksums] no release artifacts found"); +} + +const checksums = releaseArtifacts.map((filePath) => { + return `${sha256(filePath)} ${artifactSubjectName(filePath)}`; +}); + +writeFileSync(outputPath, `${checksums.join("\n")}\n`); +console.log(`[release-checksums] wrote ${relativePath(outputPath)}`); +for (const artifact of releaseArtifacts) { + console.log(`[release-checksums] ${relativePath(artifact)}`); +}