defmodule GitGudWeb.GitGudDebPackageTest do
@moduledoc """
The forge's own `.deb` — `packaging/deb/`, built by CI in
`.forgejo/workflows/build.yml` and published to this forge's apt
repository.
The dpkg-deb assembly itself is CI's business (and needs Debian
tooling). What's checked here is the part that can silently be wrong
and only show up as a failed `apt install`: the control stanza our
template renders, read back by the same parser and index renderer
that serve it.
"""
use GitGudWeb.ConnCase, async: false
alias GitGud.Packages
alias GitGud.Packages.Formats.Deb.Control
alias GitGud.Repositories.Storage
import GitGud.ForgeFixtures
@packaging Path.expand("../../packaging/deb", __DIR__)
setup do
{user, repo} = repository_fixture(%{visibility: "public"})
{:ok, _t, token} = Packages.create_registry_token(repo, "ci", ["pull", "push"])
{:ok, repo: repo, token: token, root: "/pkg/deb/#{Storage.owner_handle(user)}/#{repo.name}"}
end
test "the rendered control stanza carries what apt requires" do
assert {:ok, control} = Control.parse_stanza(rendered_control())
assert control["Package"] == "gitgud"
assert control["Version"] == "0.1.0~ci20260912000000.gabcdef123456"
assert control["Architecture"] == "amd64"
assert control["Maintainer"] =~ "@"
assert control["Section"] == "devel"
assert control["Priority"] == "optional"
assert String.to_integer(control["Installed-Size"]) > 0
# The runtime the release can't bundle: git and curl are load-bearing
# (the Cli backend, and the push hooks), openssh-client ships
# ssh-keygen for the embedded daemon's host keys.
for dep <- ~w(git curl openssh-client ca-certificates adduser) do
assert control["Depends"] =~ dep
end
# `locales` is deliberately absent: the unit runs under C.UTF-8,
# which glibc carries, so nothing needs locale-gen.
refute control["Depends"] =~ "locales"
# A folded description: the synopsis on the Description line, the
# rest indented by one space, with `.` for blank lines. Getting this
# wrong turns the following fields into description text.
assert [synopsis | body] = String.split(control["Description"], "\n")
assert synopsis =~ "git forge"
assert Enum.any?(body, &(&1 =~ "systemd"))
assert Enum.any?(body, &(&1 =~ "abcdef123456")), "the commit should be recorded"
end
test "a .deb with that control publishes and indexes", ctx do
{:ok, control} = Control.parse_stanza(rendered_control())
package = deb(control)
conn =
build_conn()
|> put_req_header("authorization", "Bearer #{ctx.token}")
|> put_req_header("content-type", "application/vnd.debian.binary-package")
|> post("#{ctx.root}/upload?dist=trixie&component=main", package)
assert json_response(conn, 200) == %{
"package" => "gitgud",
"version" => control["Version"],
"architecture" => "amd64",
"dist" => "trixie",
"component" => "main"
}
packages =
build_conn()
|> get("#{ctx.root}/dists/trixie/main/binary-amd64/Packages")
|> response(200)
assert packages =~ "Package: gitgud"
assert packages =~ "Version: #{control["Version"]}"
assert packages =~ "Filename: pool/main/"
# apt 2.7.7+ refuses an index without it.
assert packages =~ "Description-md5: "
end
test "the same release publishes to trixie and forky as separate versions", ctx do
{:ok, base} = Control.parse_stanza(rendered_control())
# What `SUITE=<suite> build-deb.sh` produces: one release, one
# version (and so one pool filename) per suite.
for suite <- ~w(trixie forky) do
control = Map.put(base, "Version", base["Version"] <> "+" <> suite)
conn =
build_conn()
|> put_req_header("authorization", "Bearer #{ctx.token}")
|> put_req_header("content-type", "application/vnd.debian.binary-package")
|> post("#{ctx.root}/upload?dist=#{suite}&component=main", deb(control))
assert json_response(conn, 200)["version"] == control["Version"]
end
for suite <- ~w(trixie forky) do
index =
build_conn()
|> get("#{ctx.root}/dists/#{suite}/main/binary-amd64/Packages")
|> response(200)
# Each suite's index names its own build and only its own.
assert index =~ "Version: #{base["Version"]}+#{suite}"
refute index =~ "+#{other_suite(suite)}"
assert index =~ "Filename: pool/main/"
end
end
defp other_suite("trixie"), do: "forky"
defp other_suite("forky"), do: "trixie"
test "the maintainer scripts are POSIX sh and set -e" do
for name <- ~w(postinst prerm postrm) do
script = File.read!(Path.join(@packaging, name))
assert String.starts_with?(script, "#!/bin/sh\n")
assert script =~ "set -e"
# `dpkg` runs these on a machine that may have no systemd (a
# container, a chroot); every call has to be guarded.
if script =~ "systemctl" do
assert script =~ "[ -d /run/systemd/system ]"
end
end
end
test "the env file and unit agree on the paths the postinst creates" do
env = File.read!(Path.join(@packaging, "gitgud.env"))
unit = File.read!(Path.join(@packaging, "gitgud.service"))
postinst = File.read!(Path.join(@packaging, "postinst"))
assert env =~ "GIT_REPOS_PATH=/var/lib/gitgud/repos"
assert env =~ "STORAGE_DISK_ROOT=/var/lib/gitgud/storage"
assert postinst =~ "/var/lib/gitgud/repos"
assert postinst =~ "/var/lib/gitgud/storage"
# RELEASE_TMP has to be writable by the service user, and
# /opt/gitgud is not.
assert unit =~ "Environment=RELEASE_TMP=/var/lib/gitgud/tmp"
# systemd starts a service with no locale, and the BEAM then comes
# up with native name encoding latin1 — Elixir warns on every boot
# and non-ASCII filenames mangle. Verified by booting the release in
# a bare trixie container: without this, `:file.native_name_encoding`
# is `latin1`; with it, `utf8`.
assert unit =~ "Environment=LANG=C.UTF-8"
assert unit =~ "Environment=LC_ALL=C.UTF-8"
assert postinst =~ "/var/lib/gitgud/tmp"
assert unit =~ "EnvironmentFile=/etc/gitgud/gitgud.env"
assert unit =~ "ExecStart=/opt/gitgud/bin/server"
end
# ── helpers ──────────────────────────────────────────────────────────
# What `build-deb.sh`'s `sed` produces, with the same substitutions.
defp rendered_control do
@packaging
|> Path.join("control.in")
|> File.read!()
|> String.replace("@VERSION@", "0.1.0~ci20260912000000.gabcdef123456")
|> String.replace("@ARCH@", "amd64")
|> String.replace("@COMMIT@", "abcdef123456789")
|> String.replace("@MAINTAINER@", "GitGud CI <noreply@example.com>")
|> String.replace("@HOMEPAGE@", "https://example.com/acme/gitgud")
|> String.replace("@INSTALLED_SIZE@", "180000")
end
defp deb(control) do
stanza =
control
|> Enum.sort()
|> Enum.map_join(fn {k, v} -> "#{k}: #{v}\n" end)
ar([
{"debian-binary", "2.0\n"},
{"control.tar.gz", :zlib.gzip(tar([{~c"./control", stanza}]))},
{"data.tar.gz", :zlib.gzip(tar([{~c"./opt/gitgud/bin/server", "#!/bin/sh\n"}]))}
])
end
defp tar(members) do
path = Path.join(System.tmp_dir!(), "t-#{System.unique_integer([:positive])}.tar")
:ok = :erl_tar.create(String.to_charlist(path), members, [])
File.read!(path)
end
defp ar(members) do
body =
Enum.map_join(members, fn {name, data} ->
header =
String.pad_trailing(name, 16) <>
String.pad_trailing("0", 12) <>
String.pad_trailing("0", 6) <>
String.pad_trailing("0", 6) <>
String.pad_trailing("100644", 8) <>
String.pad_trailing(Integer.to_string(byte_size(data)), 10) <>
<<0x60, 0x0A>>
padding = if rem(byte_size(data), 2) == 1, do: "\n", else: ""
header <> data <> padding
end)
"!<arch>\n" <> body
end
end
neiam
/gitgud
Git Gud
public · Issues · Pulls · Labels · Forks · Compare · Actions success · Badges · Packages
⭐
1
Log in to mark this repository.
8.0 KiB · text
History
b868907