fix: unblock release desktop and universal builds (#1522)
This commit is contained in:
@@ -788,7 +788,7 @@ jobs:
|
||||
base="$(basename "$artifact")"
|
||||
dest="$out_dir/kandev-desktop-${{ matrix.platform }}-$base"
|
||||
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))
|
||||
done < <(find "$bundle_dir" -type f \( \
|
||||
-name '*.dmg' -o \
|
||||
|
||||
+40
-6
@@ -14,25 +14,59 @@
|
||||
# Run:
|
||||
# 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
|
||||
|
||||
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
|
||||
# (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
|
||||
# PR creation (az repos pr create). apprise is installed via pipx for
|
||||
# notification fan-out.
|
||||
# is not set. Node/npm support agent CLI installs and the universal image's
|
||||
# pnpm layer. Azure CLI + azure-devops extension support agentctl Azure Repos PR
|
||||
# creation (az repos pr create). apprise is installed via pipx for notification
|
||||
# fan-out.
|
||||
RUN apt-get update && \
|
||||
apt-get install -y --no-install-recommends \
|
||||
git \
|
||||
gh \
|
||||
apt-transport-https \
|
||||
ca-certificates \
|
||||
curl \
|
||||
git \
|
||||
gh \
|
||||
gosu \
|
||||
tini \
|
||||
python3 \
|
||||
python3-venv \
|
||||
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/* && \
|
||||
PIPX_HOME=/opt/pipx PIPX_BIN_DIR=/usr/local/bin pipx install apprise
|
||||
|
||||
|
||||
@@ -162,17 +162,17 @@ RUN set -eux; \
|
||||
rm /tmp/rustup-init /tmp/rustup-init.sha256; \
|
||||
chmod -R a+rX "$RUSTUP_HOME" "$CARGO_HOME"
|
||||
|
||||
# pnpm via corepack - install system-wide so users don't need
|
||||
# `corepack enable` themselves. Drops shims into /usr/local/bin.
|
||||
# pnpm via npm - install system-wide so users don't need to bootstrap it
|
||||
# themselves. Drops shims into /usr/local/bin.
|
||||
#
|
||||
# No separate sha256 step: corepack downloads from registry.npmjs.org and
|
||||
# verifies the tarball against the registry's `integrity` field (SHA-512)
|
||||
# before activating. The other tools above pin upstream `.sha256` sidecars
|
||||
# because they're served from CDNs that don't expose package integrity at
|
||||
# the API level; npm does. Decision is deliberate, not an oversight.
|
||||
# No separate sha256 step: npm downloads from registry.npmjs.org and verifies
|
||||
# the tarball against the registry's `integrity` field (SHA-512) before
|
||||
# installing. The other tools above pin upstream `.sha256` sidecars because
|
||||
# they're served from CDNs that don't expose package integrity at the API level;
|
||||
# npm does. Decision is deliberate, not an oversight.
|
||||
RUN set -eux; \
|
||||
corepack enable; \
|
||||
corepack prepare "pnpm@${PNPM_VERSION}" --activate
|
||||
npm install -g --prefix /usr/local "pnpm@${PNPM_VERSION}"; \
|
||||
pnpm --version
|
||||
|
||||
# fd is installed as `fdfind` on Debian to avoid clashing with the legacy
|
||||
# fd package - symlink so agents calling plain `fd` still work.
|
||||
|
||||
@@ -19,6 +19,10 @@ function extractDockerPnpmVersion(dockerfile: string): string | undefined {
|
||||
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 {
|
||||
const packageManager = (JSON.parse(packageJSON) as { packageManager?: string }).packageManager;
|
||||
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;
|
||||
}
|
||||
|
||||
describe("release package manager version", () => {
|
||||
it("pins pnpm consistently for Docker and GitHub Actions", () => {
|
||||
describe("release runtime tooling configuration", () => {
|
||||
it("pins runtime tooling consistently across Docker and GitHub Actions", () => {
|
||||
const packagePnpmVersion = extractPackageManagerPnpmVersion(readRepoFile("apps/package.json"));
|
||||
const dockerfile = readRepoFile("Dockerfile");
|
||||
const universalDockerfile = readRepoFile("Dockerfile.universal");
|
||||
const dockerPnpmVersion = extractDockerPnpmVersion(dockerfile);
|
||||
const universalDockerPnpmVersion = extractDockerPnpmVersion(universalDockerfile);
|
||||
const dockerNodeMajor = extractDockerNodeMajor(dockerfile);
|
||||
|
||||
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) {
|
||||
expect(dockerPnpmVersion, "Dockerfile: PNPM_VERSION must match apps/package.json").toBe(
|
||||
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(
|
||||
(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", () => {
|
||||
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 homebrewScript = readRepoFile("scripts/release/update-homebrew-tap.sh");
|
||||
|
||||
@@ -219,7 +255,12 @@ describe("release desktop artifacts", () => {
|
||||
expect(workflow).toContain("pattern: desktop-*");
|
||||
expect(workflow).toContain("scripts/release/verify-desktop-assets.sh");
|
||||
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).not.toContain("kandev-desktop-");
|
||||
|
||||
+1
-1
@@ -11,7 +11,7 @@ Kandev publishes two container image flavors to GitHub Container Registry. Both
|
||||
|
||||
## 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.
|
||||
|
||||
|
||||
@@ -115,10 +115,24 @@ assets_dir="$TMP_DIR/assets"
|
||||
mkdir -p "$assets_dir"
|
||||
artifact="$assets_dir/kandev-desktop-linux-x64-test.deb"
|
||||
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
|
||||
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"
|
||||
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"
|
||||
|
||||
@@ -34,6 +34,20 @@ if [ ! -d "$ASSETS_DIR" ]; then
|
||||
exit 1
|
||||
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
|
||||
|
||||
for platform in "${REQUIRED_PLATFORMS[@]}"; do
|
||||
@@ -50,10 +64,7 @@ for platform in "${REQUIRED_PLATFORMS[@]}"; do
|
||||
echo "Missing desktop checksum: $checksum_file" >&2
|
||||
exit 1
|
||||
fi
|
||||
(
|
||||
cd "$ASSETS_DIR"
|
||||
shasum -a 256 -c "$(basename "$checksum_file")" >/dev/null
|
||||
) || {
|
||||
verify_checksum "$ASSETS_DIR" "$(basename "$checksum_file")" >/dev/null || {
|
||||
echo "Checksum verification failed for: $artifact" >&2
|
||||
exit 1
|
||||
}
|
||||
|
||||
Executable
+51
@@ -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
|
||||
Reference in New Issue
Block a user