fix: unblock release desktop and universal builds (#1522)
This commit is contained in:
@@ -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
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -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.
|
||||||
|
|||||||
@@ -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
@@ -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.
|
||||||
|
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
|
|||||||
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