fix: unblock release desktop and universal builds (#1522)

This commit is contained in:
Carlos Florêncio
2026-06-28 20:35:59 +01:00
committed by GitHub
parent 7a7c398e12
commit 0f8da4ff28
8 changed files with 176 additions and 25 deletions
+1 -1
View File
@@ -788,7 +788,7 @@ jobs:
base="$(basename "$artifact")" base="$(basename "$artifact")"
dest="$out_dir/kandev-desktop-${{ matrix.platform }}-$base" dest="$out_dir/kandev-desktop-${{ matrix.platform }}-$base"
cp "$artifact" "$dest" cp "$artifact" "$dest"
(cd "$out_dir" && shasum -a 256 "$(basename "$dest")" > "$(basename "$dest").sha256") scripts/release/write-sha256.sh "$dest" "$dest.sha256"
found=$((found + 1)) found=$((found + 1))
done < <(find "$bundle_dir" -type f \( \ done < <(find "$bundle_dir" -type f \( \
-name '*.dmg' -o \ -name '*.dmg' -o \
+40 -6
View File
@@ -14,25 +14,59 @@
# Run: # Run:
# docker run -p 38429:38429 -v kandev-data:/data ghcr.io/kdlbs/kandev:latest # docker run -p 38429:38429 -v kandev-data:/data ghcr.io/kdlbs/kandev:latest
FROM debian:bookworm-slim AS apt-keys
RUN apt-get update && \
apt-get install -y --no-install-recommends ca-certificates curl gnupg && \
curl -fsSL https://deb.nodesource.com/gpgkey/nodesource-repo.gpg.key \
| gpg --dearmor -o /nodesource.gpg && \
curl -fsSL https://packages.microsoft.com/keys/microsoft.asc \
| gpg --dearmor -o /microsoft.gpg && \
rm -rf /var/lib/apt/lists/*
FROM debian:bookworm-slim FROM debian:bookworm-slim
ARG NODE_MAJOR=24
COPY --from=apt-keys /nodesource.gpg /usr/share/keyrings/nodesource.gpg
COPY --from=apt-keys /microsoft.gpg /usr/share/keyrings/microsoft.gpg
# Install runtime dependencies. gh is included because the GitHub integration # Install runtime dependencies. gh is included because the GitHub integration
# (PR review, webhooks) shells out to it for auth fallback when GITHUB_TOKEN # (PR review, webhooks) shells out to it for auth fallback when GITHUB_TOKEN
# is not set. Azure CLI + azure-devops extension support agentctl Azure Repos # is not set. Node/npm support agent CLI installs and the universal image's
# PR creation (az repos pr create). apprise is installed via pipx for # pnpm layer. Azure CLI + azure-devops extension support agentctl Azure Repos PR
# notification fan-out. # creation (az repos pr create). apprise is installed via pipx for notification
# fan-out.
RUN apt-get update && \ RUN apt-get update && \
apt-get install -y --no-install-recommends \ apt-get install -y --no-install-recommends \
git \ apt-transport-https \
gh \
ca-certificates \ ca-certificates \
curl \ curl \
git \
gh \
gosu \ gosu \
tini \ tini \
python3 \ python3 \
python3-venv \ python3-venv \
pipx && \ pipx && \
curl -sL https://aka.ms/InstallAzureCLIDeb | bash && \ echo "deb [signed-by=/usr/share/keyrings/nodesource.gpg] https://deb.nodesource.com/node_${NODE_MAJOR}.x nodistro main" \
> /etc/apt/sources.list.d/nodesource.list && \
arch="$(dpkg --print-architecture)" && \
printf "%s\n" \
"Types: deb" \
"URIs: https://packages.microsoft.com/repos/azure-cli/" \
"Suites: bookworm" \
"Components: main" \
"Architectures: $arch" \
"Signed-by: /usr/share/keyrings/microsoft.gpg" \
> /etc/apt/sources.list.d/azure-cli.sources && \
apt-get update && \
apt-get install -y --no-install-recommends nodejs azure-cli && \
rm -f \
/etc/apt/sources.list.d/nodesource.list \
/etc/apt/sources.list.d/azure-cli.sources \
/usr/share/keyrings/nodesource.gpg \
/usr/share/keyrings/microsoft.gpg && \
rm -rf /var/lib/apt/lists/* && \ rm -rf /var/lib/apt/lists/* && \
PIPX_HOME=/opt/pipx PIPX_BIN_DIR=/usr/local/bin pipx install apprise PIPX_HOME=/opt/pipx PIPX_BIN_DIR=/usr/local/bin pipx install apprise
+9 -9
View File
@@ -162,17 +162,17 @@ RUN set -eux; \
rm /tmp/rustup-init /tmp/rustup-init.sha256; \ rm /tmp/rustup-init /tmp/rustup-init.sha256; \
chmod -R a+rX "$RUSTUP_HOME" "$CARGO_HOME" chmod -R a+rX "$RUSTUP_HOME" "$CARGO_HOME"
# pnpm via corepack - install system-wide so users don't need # pnpm via npm - install system-wide so users don't need to bootstrap it
# `corepack enable` themselves. Drops shims into /usr/local/bin. # themselves. Drops shims into /usr/local/bin.
# #
# No separate sha256 step: corepack downloads from registry.npmjs.org and # No separate sha256 step: npm downloads from registry.npmjs.org and verifies
# verifies the tarball against the registry's `integrity` field (SHA-512) # the tarball against the registry's `integrity` field (SHA-512) before
# before activating. The other tools above pin upstream `.sha256` sidecars # installing. The other tools above pin upstream `.sha256` sidecars because
# because they're served from CDNs that don't expose package integrity at # they're served from CDNs that don't expose package integrity at the API level;
# the API level; npm does. Decision is deliberate, not an oversight. # npm does. Decision is deliberate, not an oversight.
RUN set -eux; \ RUN set -eux; \
corepack enable; \ npm install -g --prefix /usr/local "pnpm@${PNPM_VERSION}"; \
corepack prepare "pnpm@${PNPM_VERSION}" --activate pnpm --version
# fd is installed as `fdfind` on Debian to avoid clashing with the legacy # fd is installed as `fdfind` on Debian to avoid clashing with the legacy
# fd package - symlink so agents calling plain `fd` still work. # fd package - symlink so agents calling plain `fd` still work.
+44 -3
View File
@@ -19,6 +19,10 @@ function extractDockerPnpmVersion(dockerfile: string): string | undefined {
return dockerfile.match(/^ARG PNPM_VERSION=([0-9]+\.[0-9]+\.[0-9]+)$/m)?.[1]; return dockerfile.match(/^ARG PNPM_VERSION=([0-9]+\.[0-9]+\.[0-9]+)$/m)?.[1];
} }
function extractDockerNodeMajor(dockerfile: string): string | undefined {
return dockerfile.match(/^ARG NODE_MAJOR=([0-9]+)$/m)?.[1];
}
function extractPackageManagerPnpmVersion(packageJSON: string): string { function extractPackageManagerPnpmVersion(packageJSON: string): string {
const packageManager = (JSON.parse(packageJSON) as { packageManager?: string }).packageManager; const packageManager = (JSON.parse(packageJSON) as { packageManager?: string }).packageManager;
const version = packageManager?.match(/^pnpm@([0-9]+\.[0-9]+\.[0-9]+)$/)?.[1]; const version = packageManager?.match(/^pnpm@([0-9]+\.[0-9]+\.[0-9]+)$/)?.[1];
@@ -145,18 +149,48 @@ function assertWorkflowPnpmVersions(file: string, expectedVersion: string): numb
return versions.length; return versions.length;
} }
describe("release package manager version", () => { describe("release runtime tooling configuration", () => {
it("pins pnpm consistently for Docker and GitHub Actions", () => { it("pins runtime tooling consistently across Docker and GitHub Actions", () => {
const packagePnpmVersion = extractPackageManagerPnpmVersion(readRepoFile("apps/package.json")); const packagePnpmVersion = extractPackageManagerPnpmVersion(readRepoFile("apps/package.json"));
const dockerfile = readRepoFile("Dockerfile"); const dockerfile = readRepoFile("Dockerfile");
const universalDockerfile = readRepoFile("Dockerfile.universal");
const dockerPnpmVersion = extractDockerPnpmVersion(dockerfile); const dockerPnpmVersion = extractDockerPnpmVersion(dockerfile);
const universalDockerPnpmVersion = extractDockerPnpmVersion(universalDockerfile);
const dockerNodeMajor = extractDockerNodeMajor(dockerfile);
expect(dockerfile).not.toContain("pnpm@latest"); expect(dockerfile).not.toContain("pnpm@latest");
expect(dockerNodeMajor, "Dockerfile: NODE_MAJOR must stay on the release Node line").toBe("24");
expect(dockerfile).toContain("AS apt-keys");
expect(dockerfile).toContain(
"COPY --from=apt-keys /nodesource.gpg /usr/share/keyrings/nodesource.gpg",
);
expect(dockerfile).toContain(
"COPY --from=apt-keys /microsoft.gpg /usr/share/keyrings/microsoft.gpg",
);
expect(dockerfile).toContain("https://deb.nodesource.com/node_${NODE_MAJOR}.x");
expect(dockerfile).toContain("/etc/apt/sources.list.d/azure-cli.sources");
expect(dockerfile).not.toContain("InstallAzureCLIDeb");
expect(dockerfile).not.toContain(" gnupg \\");
expect(dockerfile).toMatch(
/rm -f[\s\\]+\/etc\/apt\/sources\.list\.d\/nodesource\.list[\s\\]+\/etc\/apt\/sources\.list\.d\/azure-cli\.sources[\s\\]+\/usr\/share\/keyrings\/nodesource\.gpg[\s\\]+\/usr\/share\/keyrings\/microsoft\.gpg/,
);
expect(dockerfile).toMatch(/\bnodejs\b/);
expect(dockerfile).toMatch(/\bazure-cli\b/);
if (dockerPnpmVersion !== undefined) { if (dockerPnpmVersion !== undefined) {
expect(dockerPnpmVersion, "Dockerfile: PNPM_VERSION must match apps/package.json").toBe( expect(dockerPnpmVersion, "Dockerfile: PNPM_VERSION must match apps/package.json").toBe(
packagePnpmVersion, packagePnpmVersion,
); );
} }
expect(
universalDockerPnpmVersion,
"Dockerfile.universal: PNPM_VERSION must match apps/package.json",
).toBe(packagePnpmVersion);
expect(universalDockerfile).not.toContain("pnpm@latest");
expect(universalDockerfile).not.toContain("corepack enable");
expect(universalDockerfile).not.toContain("corepack prepare");
expect(universalDockerfile).toContain(
'npm install -g --prefix /usr/local "pnpm@${PNPM_VERSION}"',
);
const workflowSetupCount = workflowFiles().reduce( const workflowSetupCount = workflowFiles().reduce(
(count, file) => count + assertWorkflowPnpmVersions(file, packagePnpmVersion), (count, file) => count + assertWorkflowPnpmVersions(file, packagePnpmVersion),
@@ -210,6 +244,8 @@ describe("release desktop artifacts", () => {
it("publishes desktop artifacts while leaving npm and Homebrew tied to runtime tarballs", () => { it("publishes desktop artifacts while leaving npm and Homebrew tied to runtime tarballs", () => {
const workflow = releaseWorkflow(); const workflow = releaseWorkflow();
const checksumScript = readRepoFile("scripts/release/write-sha256.sh");
const verifyAssetsScript = readRepoFile("scripts/release/verify-desktop-assets.sh");
const publishNpmScript = readRepoFile("scripts/release/publish-npm.sh"); const publishNpmScript = readRepoFile("scripts/release/publish-npm.sh");
const homebrewScript = readRepoFile("scripts/release/update-homebrew-tap.sh"); const homebrewScript = readRepoFile("scripts/release/update-homebrew-tap.sh");
@@ -219,7 +255,12 @@ describe("release desktop artifacts", () => {
expect(workflow).toContain("pattern: desktop-*"); expect(workflow).toContain("pattern: desktop-*");
expect(workflow).toContain("scripts/release/verify-desktop-assets.sh"); expect(workflow).toContain("scripts/release/verify-desktop-assets.sh");
expect(workflow).toContain("dist/release-assets/kandev-desktop-*"); expect(workflow).toContain("dist/release-assets/kandev-desktop-*");
expect(workflow).toContain('shasum -a 256 "$(basename "$dest")"'); expect(workflow).toContain('scripts/release/write-sha256.sh "$dest" "$dest.sha256"');
expect(workflow).not.toContain('shasum -a 256 "$(basename "$dest")"');
expect(checksumScript).toContain("command -v shasum");
expect(checksumScript).toContain("command -v sha256sum");
expect(verifyAssetsScript).toContain("command -v shasum");
expect(verifyAssetsScript).toContain("command -v sha256sum");
expect(publishNpmScript).toContain('asset="kandev-${platform}.tar.gz"'); expect(publishNpmScript).toContain('asset="kandev-${platform}.tar.gz"');
expect(publishNpmScript).not.toContain("kandev-desktop-"); expect(publishNpmScript).not.toContain("kandev-desktop-");
+1 -1
View File
@@ -11,7 +11,7 @@ Kandev publishes two container image flavors to GitHub Container Registry. Both
## What's in `:universal` (and what's not) ## What's in `:universal` (and what's not)
**Language toolchains:** Go (latest stable), Rust (rustup, stable default toolchain), pnpm (preinstalled via corepack so `pnpm` is on `$PATH` for everyone). **Language toolchains:** Go (latest stable), Rust (rustup, stable default toolchain), pnpm (preinstalled through npm into `/usr/local` so `pnpm` is on `$PATH` for everyone).
**Build essentials:** `build-essential` (gcc, g++, make, libc-dev), `pkg-config`, `python3-dev`, `libssl-dev`. These cover CGO compilation, native Python pip wheels, and native Node modules. **Build essentials:** `build-essential` (gcc, g++, make, libc-dev), `pkg-config`, `python3-dev`, `libssl-dev`. These cover CGO compilation, native Python pip wheels, and native Node modules.
+15 -1
View File
@@ -115,10 +115,24 @@ assets_dir="$TMP_DIR/assets"
mkdir -p "$assets_dir" mkdir -p "$assets_dir"
artifact="$assets_dir/kandev-desktop-linux-x64-test.deb" artifact="$assets_dir/kandev-desktop-linux-x64-test.deb"
printf 'desktop artifact\n' > "$artifact" printf 'desktop artifact\n' > "$artifact"
(cd "$assets_dir" && shasum -a 256 "$(basename "$artifact")" > "$(basename "$artifact").sha256") "$ROOT_DIR/scripts/release/write-sha256.sh" "$artifact" "$artifact.sha256"
"$ROOT_DIR/scripts/release/verify-desktop-assets.sh" "$assets_dir" linux-x64 >/dev/null "$ROOT_DIR/scripts/release/verify-desktop-assets.sh" "$assets_dir" linux-x64 >/dev/null
pass "verify-desktop-assets accepts matching checksums" pass "verify-desktop-assets accepts matching checksums"
fallback_tools_dir="$TMP_DIR/sha256sum-tools"
fallback_assets_dir="$TMP_DIR/sha256sum-assets"
mkdir -p "$fallback_tools_dir" "$fallback_assets_dir"
for tool in bash basename dirname mkdir mv rm sha256sum; do
ln -s "$(command -v "$tool")" "$fallback_tools_dir/$tool"
done
fallback_artifact="$fallback_assets_dir/kandev-desktop-linux-x64-fallback.deb"
printf 'desktop artifact\n' > "$fallback_artifact"
PATH="$fallback_tools_dir" bash "$ROOT_DIR/scripts/release/write-sha256.sh" \
"$fallback_artifact" "$fallback_artifact.sha256"
PATH="$fallback_tools_dir" bash "$ROOT_DIR/scripts/release/verify-desktop-assets.sh" \
"$fallback_assets_dir" linux-x64 >/dev/null
pass "desktop asset checksums work without shasum"
printf 'tampered artifact\n' > "$artifact" printf 'tampered artifact\n' > "$artifact"
if "$ROOT_DIR/scripts/release/verify-desktop-assets.sh" "$assets_dir" linux-x64 >"$OUT_FILE" 2>"$ERR_FILE"; then if "$ROOT_DIR/scripts/release/verify-desktop-assets.sh" "$assets_dir" linux-x64 >"$OUT_FILE" 2>"$ERR_FILE"; then
fail "verify-desktop-assets should reject checksum mismatches" fail "verify-desktop-assets should reject checksum mismatches"
+15 -4
View File
@@ -34,6 +34,20 @@ if [ ! -d "$ASSETS_DIR" ]; then
exit 1 exit 1
fi fi
verify_checksum() {
local assets_dir="$1"
local checksum_file="$2"
if command -v shasum >/dev/null 2>&1; then
(cd "$assets_dir" && shasum -a 256 -c "$checksum_file")
elif command -v sha256sum >/dev/null 2>&1; then
(cd "$assets_dir" && sha256sum -c "$checksum_file")
else
echo "Missing checksum tool: need shasum or sha256sum" >&2
return 1
fi
}
shopt -s nullglob shopt -s nullglob
for platform in "${REQUIRED_PLATFORMS[@]}"; do for platform in "${REQUIRED_PLATFORMS[@]}"; do
@@ -50,10 +64,7 @@ for platform in "${REQUIRED_PLATFORMS[@]}"; do
echo "Missing desktop checksum: $checksum_file" >&2 echo "Missing desktop checksum: $checksum_file" >&2
exit 1 exit 1
fi fi
( verify_checksum "$ASSETS_DIR" "$(basename "$checksum_file")" >/dev/null || {
cd "$ASSETS_DIR"
shasum -a 256 -c "$(basename "$checksum_file")" >/dev/null
) || {
echo "Checksum verification failed for: $artifact" >&2 echo "Checksum verification failed for: $artifact" >&2
exit 1 exit 1
} }
+51
View File
@@ -0,0 +1,51 @@
#!/usr/bin/env bash
# Write a SHA-256 checksum file for a release artifact.
set -euo pipefail
usage() {
cat <<'EOF'
Usage: write-sha256.sh <artifact> [checksum-file]
Writes a checksum file using the portable "<sha256> <filename>" format. The
filename in the checksum is always the artifact basename so the file can be
verified from the artifact directory on Linux, macOS, or Windows runners.
EOF
}
if [ "${1:-}" = "-h" ] || [ "${1:-}" = "--help" ]; then
usage
exit 0
fi
if [ "$#" -lt 1 ] || [ "$#" -gt 2 ]; then
usage >&2
exit 2
fi
artifact="$1"
checksum_file="${2:-$artifact.sha256}"
if [ ! -f "$artifact" ]; then
echo "Missing artifact: $artifact" >&2
exit 1
fi
artifact_dir="$(cd "$(dirname "$artifact")" && pwd -P)"
artifact_name="$(basename "$artifact")"
checksum_dir="$(dirname "$checksum_file")"
mkdir -p "$checksum_dir"
tmp="${checksum_file}.tmp.$$"
trap 'rm -f "$tmp"' EXIT
if command -v shasum >/dev/null 2>&1; then
(cd "$artifact_dir" && shasum -a 256 "$artifact_name") > "$tmp"
elif command -v sha256sum >/dev/null 2>&1; then
(cd "$artifact_dir" && sha256sum "$artifact_name") > "$tmp"
else
echo "Missing checksum tool: need shasum or sha256sum" >&2
exit 1
fi
mv "$tmp" "$checksum_file"
trap - EXIT