Files
kandev/apps/cli/src/release-config.test.ts
T
2026-08-07 12:58:53 +01:00

552 lines
23 KiB
TypeScript

import { readdirSync, readFileSync } from "node:fs";
import { resolve } from "node:path";
import { describe, expect, it } from "vitest";
const repoRoot = resolve(__dirname, "../../..");
const externalPnpmWorkflowFiles = new Set([".github/workflows/notify-docs.yml"]);
function readRepoFile(path: string): string {
return readFileSync(resolve(repoRoot, path), "utf8");
}
function runtimePackagesIn(source: string): string[] {
return [...new Set(source.match(/@kdlbs\/runtime-[a-z0-9-]+/g) ?? [])].sort();
}
function workflowFiles(): string[] {
const workflowDir = resolve(repoRoot, ".github/workflows");
return readdirSync(workflowDir)
.filter((name) => name.endsWith(".yml") || name.endsWith(".yaml"))
.map((name) => `.github/workflows/${name}`);
}
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];
expect(version, "apps/package.json: packageManager must pin pnpm").toBeDefined();
if (version === undefined) {
throw new Error("apps/package.json: packageManager must pin pnpm");
}
return version;
}
function indentation(line: string): number {
return line.search(/\S/);
}
function isBlankOrComment(line: string): boolean {
const trimmed = line.trim();
return trimmed.length === 0 || trimmed.startsWith("#");
}
function parseVersionLine(line: string): string | undefined {
const match = line.match(/^\s*version:\s*(?:"([^"]+)"|'([^']+)'|([^"'\s#]+))\s*(?:#.*)?$/);
return match?.[1] ?? match?.[2] ?? match?.[3];
}
function findVersionInWithBlock(
lines: string[],
start: number,
withIndent: number,
): string | undefined {
for (let index = start; index < lines.length; index += 1) {
const line = lines[index];
if (isBlankOrComment(line)) {
continue;
}
if (indentation(line) <= withIndent) {
return undefined;
}
const version = parseVersionLine(line);
if (version !== undefined) {
return version;
}
}
return undefined;
}
function findPnpmSetupVersion(
lines: string[],
start: number,
stepIndent: number,
): string | undefined {
for (let index = start; index < lines.length; index += 1) {
const line = lines[index];
if (isBlankOrComment(line)) {
continue;
}
if (indentation(line) <= stepIndent) {
return undefined;
}
const withMatch = line.match(/^(\s*)with:\s*(?:#.*)?$/);
if (withMatch !== null) {
return findVersionInWithBlock(lines, index + 1, withMatch[1].length);
}
}
return undefined;
}
function matchPnpmSetupUsesLine(line: string): RegExpMatchArray | null {
const match = line.match(
/^(\s*)(?:-\s*)?uses:\s*["']?pnpm\/action-setup@([^"'\s#]+)["']?\s*(#.*)?$/,
);
if (match === null) return null;
const ref = match[2];
const comment = match[3] ?? "";
const versionTagPattern = /^v[0-9]+(?:\.[0-9]+\.[0-9]+)?$/;
const versionTag = versionTagPattern.test(ref);
const shaPinned =
/^[0-9a-f]{40}$/.test(ref) && /^#\s*v[0-9]+(?:\.[0-9]+\.[0-9]+)?$/.test(comment);
expect(
versionTag || shaPinned,
"pnpm/action-setup must use a version tag or a 40-character SHA with the version tag in a comment",
).toBe(true);
return match;
}
function findStepIndent(lines: string[], usesLineIndex: number, usesIndent: number): number {
if (lines[usesLineIndex].trimStart().startsWith("- ")) {
return usesIndent;
}
for (let index = usesLineIndex - 1; index >= 0; index -= 1) {
const line = lines[index];
if (isBlankOrComment(line)) {
continue;
}
const lineIndent = indentation(line);
if (lineIndent < usesIndent && line.slice(lineIndent).startsWith("- ")) {
return lineIndent;
}
}
return Math.max(0, usesIndent - 2);
}
function extractWorkflowPnpmVersions(workflow: string): Array<string | undefined> {
const lines = workflow.split(/\r?\n/);
const versions: Array<string | undefined> = [];
for (let index = 0; index < lines.length; index += 1) {
const line = lines[index];
const setupMatch = matchPnpmSetupUsesLine(line);
if (setupMatch !== null) {
const stepIndent = findStepIndent(lines, index, setupMatch[1].length);
versions.push(findPnpmSetupVersion(lines, index + 1, stepIndent));
}
}
return versions;
}
function assertWorkflowPnpmVersions(file: string, expectedVersion?: string): number {
const versions = extractWorkflowPnpmVersions(readRepoFile(file));
for (const version of versions) {
if (version === undefined) {
expect(version, `${file}: pnpm/action-setup step is missing a version pin`).toBeDefined();
continue;
}
if (expectedVersion !== undefined) {
expect(version, `${file}: pnpm/action-setup version must match apps/package.json`).toBe(
expectedVersion,
);
}
}
return versions.length;
}
describe("release runtime tooling configuration", () => {
it("rejects malformed SHA version comments for pnpm/action-setup", () => {
expect(() =>
matchPnpmSetupUsesLine(
"uses: pnpm/action-setup@0123456789abcdef0123456789abcdef01234567 # v4-old",
),
).toThrow(/40-character SHA with the version tag/);
});
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) => {
const expectedVersion = externalPnpmWorkflowFiles.has(file) ? undefined : packagePnpmVersion;
return count + assertWorkflowPnpmVersions(file, expectedVersion);
}, 0);
expect(workflowSetupCount).toBeGreaterThan(0);
});
});
describe("release npm publishing", () => {
it("accepts either GitHub release assets or local nightly assets", () => {
const script = readRepoFile("scripts/release/publish-npm.sh");
for (const flag of ["--version", "--dist-tag", "--release-tag", "--assets-dir"]) {
expect(script).toContain(flag);
}
expect(script).toContain('DIST_TAG=""');
expect(script).toContain('case "$DIST_TAG" in');
expect(script).toContain("latest)");
expect(script).toContain("nightly)");
expect(script).toContain('if [[ -n "$RELEASE_TAG" && -n "$SOURCE_ASSETS_DIR" ]]');
expect(script).toContain('elif [[ -z "$RELEASE_TAG" && -z "$SOURCE_ASSETS_DIR" ]]');
expect(script).toContain('elif [[ "$DIST_TAG" == "latest" && -z "$RELEASE_TAG" ]]');
expect(script).toContain('elif [[ "$DIST_TAG" == "nightly" && -z "$SOURCE_ASSETS_DIR" ]]');
expect(script).toContain('ASSETS_DIR="$SOURCE_ASSETS_DIR"');
});
it("publishes all packages under the requested dist-tag and pins the launcher version", () => {
const script = readRepoFile("scripts/release/publish-npm.sh");
const packages = readRepoFile("scripts/release/npm-packages.sh");
const loopStart = script.indexOf('for pkg in "${RUNTIME_PACKAGES[@]}"; do');
const loopEnd = script.indexOf("\ndone", loopStart);
expect(loopStart).toBeGreaterThanOrEqual(0);
expect(loopEnd).toBeGreaterThan(loopStart);
const runtimePublishLoop = script.slice(loopStart, loopEnd);
expect(runtimePublishLoop).toContain(
'npm publish --access public --provenance --tag "$DIST_TAG"',
);
const mainPublish = script.slice(script.indexOf("Publishing kandev@$VERSION"));
expect(mainPublish).toContain('npm publish --access public --provenance --tag "$DIST_TAG"');
expect(script).toContain("pkg.version = version;");
expect(script).toContain('CLI_PACKAGE_BACKUP="$WORK_DIR/cli-package.json"');
expect(script).toContain('cp "$CLI_PACKAGE_BACKUP" "$CLI_PACKAGE_JSON"');
for (const packageName of [
"@kdlbs/runtime-linux-x64",
"@kdlbs/runtime-linux-arm64",
"@kdlbs/runtime-darwin-x64",
"@kdlbs/runtime-darwin-arm64",
"@kdlbs/runtime-win32-x64",
]) {
expect(packages).toContain(`"${packageName}"`);
}
expect(packages).toContain('NIGHTLY_PACKAGES=("kandev" "${RUNTIME_PACKAGES[@]}")');
expect(script).toContain('source "$ROOT_DIR/scripts/release/npm-packages.sh"');
});
it("keeps every runtime package consumer aligned with the publishing inventory", () => {
const inventory = runtimePackagesIn(readRepoFile("scripts/release/npm-packages.sh"));
expect(inventory.length).toBeGreaterThan(0);
for (const path of [
"apps/cli/src/runtime.ts",
"apps/cli/bin/native-shim.js",
"apps/cli/package.json",
]) {
expect(runtimePackagesIn(readRepoFile(path)), path).toEqual(inventory);
}
expect(readRepoFile("scripts/release/package-npm-runtime.sh")).toContain(
'source "$ROOT_DIR/scripts/release/npm-packages.sh"',
);
expect(readRepoFile("scripts/release/publish-npm.test.mjs")).toContain("loadRuntimeInventory");
});
it("only treats an existing nightly version as idempotent when the nightly tag matches", () => {
const script = readRepoFile("scripts/release/publish-npm.sh");
expect(script).toContain('npm_view_version "${pkg}@${VERSION}"');
expect(script).toContain('npm_view_version "${pkg}@${DIST_TAG}"');
expect(script).toContain('if [[ "$DIST_TAG" == "nightly" ]]');
expect(script).toContain('if [[ "$tagged_version" != "$VERSION" ]]');
expect(script).toContain("refusing idempotent success");
expect(script).toContain("EPUBLISHCONFLICT");
expect(script).toContain("treated as idempotent success");
});
});
describe("release desktop artifacts", () => {
const desktopPlatforms = ["macos-arm64", "macos-x64", "linux-x64", "linux-arm64", "windows-x64"];
function releaseWorkflow(): string {
return readRepoFile(".github/workflows/release.yml");
}
it("builds desktop artifacts for every supported runtime platform", () => {
const workflow = releaseWorkflow();
expect(workflow).toMatch(/\n build-desktop:\n/);
expect(workflow).toContain("needs: [prepare, build-bundles]");
expect(workflow).toContain("scripts/release/prepare-desktop-runtime.sh");
expect(workflow).toContain('--platform "${{ matrix.platform }}"');
expect(workflow).toContain(
'rustup toolchain install stable --profile minimal --target "${{ matrix.rust_target }}"',
);
expect(workflow).toMatch(/Swatinem\/rust-cache@[0-9a-f]{40}\s+# v2/);
for (const platform of desktopPlatforms) {
expect(workflow, `release.yml must include desktop platform ${platform}`).toContain(
`platform: ${platform}`,
);
}
});
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");
expect(workflow).toContain(
"needs: [prepare, build-bundles, build-desktop, docker-universal-manifest]",
);
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('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-");
expect(homebrewScript).toContain('local sha_file="kandev-${platform}.tar.gz.sha256"');
expect(homebrewScript).not.toContain("kandev-desktop-");
});
it("keeps the generated tap formula on the installed runtime smoke contract", () => {
const formula = readRepoFile("scripts/release/kandev.rb");
expect(formula).toContain(
'assert_equal "v#{version}", shell_output("#{bin}/kandev --version").strip',
);
expect(formula).toContain('spawn bin/"kandev", "--headless", "--port", port.to_s');
expect(formula).toContain("/health");
expect(formula).toContain('"status":"ok"');
expect(formula).toContain("<title>Kandev</title>");
expect(formula).not.toContain('version "__VERSION__"');
});
it("bumps desktop package and Tauri versions during release preparation", () => {
const workflow = releaseWorkflow();
expect(workflow).toContain('(cd apps/desktop && npm version --no-git-tag-version "$NEXT")');
expect(workflow).toContain("tauriConfig.version = next");
expect(workflow).toContain('cargoToml.replace(/^version = ".*"$/m, `version = "${next}"`)');
expect(workflow).toContain('name = "kandev-desktop"');
expect(workflow).toContain("apps/desktop/src-tauri/tauri.conf.json");
expect(workflow).toContain("apps/desktop/src-tauri/Cargo.toml");
expect(workflow).toContain("apps/desktop/src-tauri/Cargo.lock");
});
it("pins linux x64 desktop release builds to Ubuntu 22.04", () => {
const workflow = releaseWorkflow();
expect(workflow).toContain(`- os: ubuntu-22.04
platform: linux-x64
goos: linux
goarch: amd64`);
expect(workflow).toContain(`- platform: linux-x64
os: ubuntu-22.04
rust_target: x86_64-unknown-linux-gnu
tauri_bundles: appimage,deb,rpm`);
});
it("builds and publishes a complete signed Tauri updater feed", () => {
const workflow = releaseWorkflow();
const tauriConfig = JSON.parse(readRepoFile("apps/desktop/src-tauri/tauri.conf.json"));
const manifestScript = readRepoFile("scripts/release/updater-manifest.mjs");
const verifyAssetsScript = readRepoFile("scripts/release/verify-desktop-assets.sh");
expect(tauriConfig.bundle.createUpdaterArtifacts).toBe(false);
expect(workflow).toContain(
"TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY }}",
);
expect(workflow).toContain(
"TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY_PASSWORD }}",
);
expect(workflow).toContain('createUpdaterArtifacts":"v1Compatible"');
expect(workflow).toContain("scripts/release/updater-manifest.mjs generate");
expect(workflow).toContain("scripts/release/updater-manifest.mjs verify");
expect(workflow).toContain("dist/release-assets/latest.json");
for (const target of [
"darwin-aarch64",
"darwin-x86_64",
"linux-aarch64",
"linux-x86_64",
"windows-x86_64",
]) {
expect(manifestScript, `updater manifest must include ${target}`).toContain(`"${target}"`);
}
for (const extension of ["*.app.tar.gz", "*.AppImage.tar.gz", "*.nsis.zip", "*.sig"]) {
expect(workflow, `desktop collector must retain ${extension}`).toContain(
`-name '${extension}'`,
);
expect(verifyAssetsScript, `asset verification must recognize ${extension}`).toContain(
extension,
);
}
});
it("keeps updater signing independent from platform identity signing", () => {
const workflow = releaseWorkflow();
const signingDocs = readRepoFile("docs/desktop-tauri-signing.md");
const updaterSigningScript = readRepoFile("scripts/release/updater-signing-ready.sh");
for (const key of ["TAURI_SIGNING_PRIVATE_KEY", "TAURI_SIGNING_PRIVATE_KEY_PASSWORD"]) {
expect(workflow).toContain(key);
expect(signingDocs).toContain(key);
}
expect(signingDocs).toContain("distinct from macOS and Windows code-signing credentials");
expect(signingDocs).toContain("Installers remain available");
expect(signingDocs).toContain("`latest.json` is not");
expect(workflow).toContain('bash "$helper" "${{ matrix.platform }}"');
expect(updaterSigningScript).not.toContain("MACOS_SIGNING_ENABLED");
expect(updaterSigningScript).not.toContain("WINDOWS_SIGNING_ENABLED");
expect(signingDocs).toContain("Missing OS credentials do not block Tauri-signed updater");
expect(workflow).not.toContain(
"Refusing to build an updater artifact that is not also signed for its operating system.",
);
});
it("builds unsigned desktop releases when signing inputs are incomplete", () => {
const workflow = releaseWorkflow();
const signingDocs = readRepoFile("docs/desktop-tauri-signing.md");
const tauriConfig = readRepoFile("apps/desktop/src-tauri/tauri.conf.json");
const windowsSignScript = readRepoFile("apps/desktop/src-tauri/windows-sign.ps1");
expect(workflow).not.toContain("allow_unsigned_desktop");
expect(workflow).not.toContain("ALLOW_UNSIGNED_DESKTOP");
expect(workflow).toContain("desktop_validation_only");
expect(workflow).toContain("ref: ${{ needs.prepare.outputs.ref }}");
expect(workflow).toContain("persist-credentials: false");
expect(workflow).toContain("Desktop validation summary");
expect(workflow).toContain("No release PR, tag, GitHub release, public container tags");
expect(workflow).toMatch(
/github\.event_name == 'workflow_dispatch' &&\s+inputs\.channel == 'stable' &&\s+!inputs\.dry_run &&\s+!inputs\.desktop_validation_only/,
);
expect(workflow).toContain('if [ "$DESKTOP_VALIDATION_ONLY" = "true" ]; then');
expect(workflow).toContain("scripts/release/desktop-signing-ready.sh macos");
expect(workflow).toContain("scripts/release/desktop-signing-ready.sh windows");
expect(workflow).toContain("MACOS_SIGNING_ENABLED=false");
expect(workflow).toContain("MACOS_SIGNING_ENABLED=true");
expect(workflow).toContain("WINDOWS_SIGNING_ENABLED=false");
expect(workflow).toContain("WINDOWS_SIGNING_ENABLED=true");
expect(workflow).toContain(
"macOS signing/notarization inputs are incomplete; building unsigned desktop artifact.",
);
expect(workflow).toContain(
"Windows signing inputs are incomplete; building unsigned desktop artifact.",
);
expect(workflow).toContain("Add unsigned desktop warning to release notes");
expect(workflow).toContain(
"desktop installers in this release are unsigned development builds.",
);
expect(workflow).toContain("docker-amd64:");
expect(workflow).toContain("docker-universal-manifest:");
expect(workflow).toContain("unset APPLE_CERTIFICATE");
expect(workflow).toContain("unset APPLE_ID");
expect(workflow).toContain("unset WINDOWS_CERTIFICATE");
expect(workflow).toContain("unset WINDOWS_CERTIFICATE_PASSWORD");
expect(workflow).toContain("unset WINDOWS_TIMESTAMP_URL");
expect(workflow).toContain("unset WINDOWS_SIGNTOOL_PATH");
for (const key of [
"APPLE_CERTIFICATE",
"APPLE_CERTIFICATE_PASSWORD",
"KEYCHAIN_PASSWORD",
"APPLE_SIGNING_IDENTITY",
"APPLE_API_KEY_P8",
"APPLE_API_KEY_PATH",
"APPLE_TEAM_ID",
]) {
expect(workflow, `release.yml must wire ${key}`).toContain(key);
}
for (const key of ["WINDOWS_CERTIFICATE", "WINDOWS_CERTIFICATE_PASSWORD", "signtool sign"]) {
expect(workflow, `release.yml must wire ${key}`).toContain(key);
}
expect(workflow).toContain("Invoke-SignTool");
expect(workflow).toContain("$LASTEXITCODE -ne 0");
expect(workflow).toContain("Sign macOS desktop runtime binaries");
expect(workflow).toContain("resources/kandev/bin");
expect(workflow).toContain(
"for binary in kandev agentctl agentctl-darwin-arm64 agentctl-darwin-amd64; do",
);
expect(workflow).toContain(
'codesign --force --options runtime --timestamp --sign "$APPLE_SIGNING_IDENTITY"',
);
expect(workflow).toContain("codesign --verify --strict");
expect(workflow).toContain("Sign Windows desktop runtime binaries");
expect(workflow).toContain('foreach ($binary in @("kandev.exe", "agentctl.exe"))');
expect(workflow).toContain("Remove-Item -LiteralPath $certificatePath");
expect(tauriConfig).toContain('"publisher": "Kandev"');
expect(tauriConfig).toContain('"timestampUrl"');
expect(tauriConfig).toContain('"timestampUrl": "https://timestamp.digicert.com"');
expect(tauriConfig).toContain('"signCommand"');
expect(tauriConfig).toContain("windows-sign.ps1");
expect(tauriConfig).not.toContain('"csp": null');
expect(windowsSignScript).toContain('"https://timestamp.digicert.com"');
expect(windowsSignScript).toContain('WINDOWS_SIGNING_ENABLED -eq "false"');
expect(windowsSignScript).not.toContain("ALLOW_UNSIGNED_DESKTOP");
expect(windowsSignScript).toContain("Skipping Windows signing for unsigned desktop artifact");
expect(windowsSignScript).toContain("Remove-Item -LiteralPath $certificatePath");
expect(signingDocs).toContain("signs desktop artifacts opportunistically");
expect(signingDocs).toContain("Missing or incomplete inputs do not block the release");
expect(signingDocs).not.toContain("allow_unsigned_desktop");
expect(signingDocs).toContain("desktop_validation_only");
expect(signingDocs).toContain("does not publish a GitHub release");
expect(signingDocs).toContain("public container tags");
expect(signingDocs).toContain("Ubuntu 22.04");
});
});