diff --git a/README.rst b/README.rst index 208b7dc..a0c723e 100644 --- a/README.rst +++ b/README.rst @@ -32,16 +32,24 @@ and on top of that: Supervised Manual GitLab Update ------------------------------- -It is recommended to always first check the `GitLab documentation`_ prior to -update. It is also recommended that you ensure you have a full backup (TKLBAM -is a good option, but there are other methods). Once you are statisfied, -update to the latest stable release via apt:: +Check the installed and eligible versions without changing the appliance:: + + gitlab-update --check + +Before an update, consult the `GitLab upgrade path`_ and the release-specific +`GitLab documentation`_. GitLab requires intermediate upgrade stops. Back up +the appliance, then install the next eligible version explicitly:: apt update - apt install gitlab-ce + apt install gitlab-ce= + +Repeat the application acceptance checks before proceeding to another required +stop. Available versions are listed by ``apt-cache madison gitlab-ce`` and the +`GitLab release blog`_. -You can view available versions via the `GitLab 'release' blog tag`_. We also -highly recommend subscribing to receive email notifications. +If APT reports an expired repository key or ``NO_PUBKEY``, follow the +`repository-key rotation procedure`_. It preserves the per-repository +``signed-by`` restriction and verifies GitLab's full published fingerprint. Credentials *(passwords set at first boot)* ------------------------------------------- @@ -53,4 +61,6 @@ Credentials *(passwords set at first boot)* .. _TurnKey Core: https://www.turnkeylinux.org/core .. _Omnibus package: https://docs.gitlab.com/omnibus/ .. _GitLab documentation: https://docs.gitlab.com/omnibus/update/README.html -.. _GitLab 'release' blog tag: https://about.gitlab.com/blog/categories/releases/ +.. _GitLab upgrade path: https://docs.gitlab.com/update/upgrade_paths/ +.. _GitLab release blog: https://about.gitlab.com/blog/categories/releases/ +.. _repository-key rotation procedure: docs/update-apt-repo-key.rst diff --git a/changelog b/changelog index ecedccd..a127a56 100644 --- a/changelog +++ b/changelog @@ -1,3 +1,20 @@ +turnkey-gitlab-19.0 (1) turnkey; urgency=low + + * Install GitLab CE 19.3.0 from its official Debian 13 repository with a + pinned version, repository key fingerprint, and signed package metadata. + + * Add a non-mutating update check and document GitLab's supervised required + upgrade stops. + + * Verify firstboot failures and keep the root password out of child process + arguments. + + * Add v19 application acceptance coverage and README evidence crosswalk. + + * Upgrade the base distribution to Debian 13 Trixie. + + -- TurnKey Linux release engineering Tue, 25 Aug 2026 00:00:00 +0000 + turnkey-gitlab-18.1 (1) turnkey; urgency=low * Update GitLab to latest GitLab-CE v17.3.0- via upstream apt repo. @@ -337,4 +354,3 @@ turnkey-gitlab-12.0 (1) turnkey; urgency=low appliances. Here we only describe changes specific to this appliance. -- Alon Swartz Wed, 01 Aug 2012 08:00:00 +0200 - diff --git a/conf.d/main b/conf.d/main index ba2a293..5fbe805 100755 --- a/conf.d/main +++ b/conf.d/main @@ -6,16 +6,51 @@ ADMIN_PASS=Turnkey1 APP_NAME="TurnKey GitLab" DISPLAY_NAME="$APP_NAME Admin" CONF=/etc/gitlab/gitlab.rb +GITLAB_VERSION=19.3.0-ce.0 +GITLAB_PACKAGE_SHA256=f88f80cd61d6b2beb35aa7207591d4abdfed0e6c2c42e6ed753dd29ea5de076d +GITLAB_KEY_SHA256=003c0ca2fea61767f8c6de7a1c0f49fc88ea3c8db95e3cd1856b32ce9d876e0f +GITLAB_KEY_FINGERPRINT=F6403F6544A38863DAA0B6E03F01618A51312F3F +SOURCE_RECORD=/usr/local/share/turnkey-gitlab/source [ "$FAB_HTTP_PROXY" ] && export http_proxy=$FAB_HTTP_PROXY APT_KEY_URL=https://packages.gitlab.com/gpg.key -curl -sS $APT_KEY_URL | apt-key --keyring /usr/share/keyrings/gitlab-ce.gpg add - +key=$(mktemp) +runsvdir_pid= +cleanup() { + if [[ -n $runsvdir_pid ]]; then + kill "$runsvdir_pid" 2>/dev/null || true + wait "$runsvdir_pid" 2>/dev/null || true + fi + find "$key" -maxdepth 0 -type f -delete +} +trap cleanup EXIT +curl -fsSL "$APT_KEY_URL" -o "$key" +echo "$GITLAB_KEY_SHA256 $key" | sha256sum -c - +test "$(gpg --show-keys --with-colons "$key" | awk -F: '$1 == "fpr" { print $10; exit }')" = "$GITLAB_KEY_FINGERPRINT" +gpg --batch --yes --dearmor --output /usr/share/keyrings/gitlab-ce.gpg "$key" apt-get update -apt-get install gitlab-ce -y - -# tweak GitLab defaults for build within TKLDev +metadata_sha256=$(apt-cache show "gitlab-ce=$GITLAB_VERSION" | + awk '$1 == "SHA256:" { print $2; exit }') +test "$metadata_sha256" = "$GITLAB_PACKAGE_SHA256" + +# GitLab's package configures itself from its postinst. Stage the verified +# package so its bundled runit can supervise services during that configure. +apt-get install --download-only -y "gitlab-ce=$GITLAB_VERSION" +package=/var/cache/apt/archives/gitlab-ce_${GITLAB_VERSION}_amd64.deb +test -f "$package" +echo "$GITLAB_PACKAGE_SHA256 $package" | sha256sum -c - +dpkg --unpack "$package" + +install -d -m 0755 "$(dirname "$CONF")" /opt/gitlab/service +cp /opt/gitlab/etc/gitlab.rb.template "$CONF" +chmod 0600 "$CONF" +sed -i "s|GENERATED_EXTERNAL_URL|http://$DOMAIN|" "$CONF" + +# Tweak GitLab defaults before the package's automatic reconfigure. Disabling +# init detection is required inside the TKLDev chroot, where systemd is not PID +# 1. The temporary runit process below provides the supervisor GitLab expects. sed -i "/^external_url/ s|'.*|'http://$DOMAIN'|" $CONF sed -i "/postgresql\['dynamic_shared_memory_type'\]/ s|^# *||" $CONF sed -i "/postgresql\['dynamic_shared_memory_type'\]/ s|=.*|= 'mmap'|" $CONF @@ -34,46 +69,82 @@ sed -i "/gitlab_rails\['gitlab_email_subject_suffix'\]/ s|=.*|= '\[$APP_NAME\]'| echo "package['detect_init'] = false" >> "$CONF" echo "package['modify_kernel_parameters'] = false" >> "$CONF" +set +x +/opt/gitlab/embedded/bin/runsvdir-start >/tmp/gitlab-runsvdir-build.log 2>&1 & +runsvdir_pid=$! +set -x +kill -0 "$runsvdir_pid" + +EXTERNAL_URL="http://$DOMAIN" apt-get install -y "gitlab-ce=$GITLAB_VERSION" +test "$(dpkg-query -W -f='${Version}' gitlab-ce)" = "$GITLAB_VERSION" + +mkdir -p "$(dirname "$SOURCE_RECORD")" +cat >"$SOURCE_RECORD" <"$staged_record" + chmod --reference="$source_record" "$staged_record" -.. _provide instructions: https://docs.gitlab.com/omnibus/update/package_signatures.html#fetching-new-keys-after-2020-04-06 -.. _GitLab "NO_PUBKEY" error: https://github.com/turnkeylinux/tracker/issues/1441 -.. _sources.list entry: https://github.com/turnkeylinux-apps/gitlab/blob/master/overlay/etc/apt/sources.list.d/gitlab-ce.list#L4 + mv -f -- "$staged_keyring" "$keyring" + staged_keyring= + mv -f -- "$staged_record" "$source_record" + staged_record= + + apt-get update + gitlab-update --check | tee "$work/update-check" + grep -Fxq "integrity=APT-signed-by-$expected_fingerprint" \ + "$work/update-check" + grep -Fxq "repository_key_download_sha256=$key_sha256" \ + "$work/update-check" + +Every trust check occurs before APT refreshes repository metadata. If the +command is interrupted between the two final moves, ``gitlab-update --check`` +fails because the keyring and source record disagree. Rerun the complete +procedure rather than weakening the ``signed-by`` restriction. + + +.. _GitLab Linux package signatures: https://docs.gitlab.com/omnibus/update/package_signatures/ diff --git a/docs/v19.0-testing.md b/docs/v19.0-testing.md new file mode 100644 index 0000000..ac4ed00 --- /dev/null +++ b/docs/v19.0-testing.md @@ -0,0 +1,408 @@ +# GitLab v19 acceptance + +## Dependency source decision + +The appliance plan inherits its operating system packages from Debian Trixie. +Debian does not package the GitLab application, so the application exception is +GitLab CE 19.3.0 from GitLab's official Debian 13 repository. GitLab documents +Debian 13 support from GitLab 18.5 through Debian's expected June 2030 end of +life in its [Linux package supported platforms][supported-platforms]. + +The exception is bounded as follows: + +- version `19.3.0-ce.0` is selected explicitly; +- repository key fingerprint is + `F6403F6544A38863DAA0B6E03F01618A51312F3F`; +- repository key SHA-256 is + `003c0ca2fea61767f8c6de7a1c0f49fc88ea3c8db95e3cd1856b32ce9d876e0f`; +- package SHA-256 is + `f88f80cd61d6b2beb35aa7207591d4abdfed0e6c2c42e6ed753dd29ea5de076d`; +- the build verifies the key bytes and primary fingerprint, reads the package + digest from APT's signed metadata, downloads through APT, and verifies the + downloaded Debian package again before unpacking it; +- `gitlab-update --check` verifies the installed version, repository key, + Trixie channel, and eligible candidate without changing the appliance. + +GitLab's Debian post-install script configures the application immediately. +The TKLDev build therefore stages the verified package with `dpkg --unpack`, +writes the non-init chroot settings, starts GitLab's bundled runit, and lets APT +perform the normal configure step. Runtime upgrades use the normal APT path +under the running appliance service manager. + +## README crosswalk + +The focused command in every application-specific row is `tests/v19.sh`, run +inside the configured Docker root by the exact acceptance command below. +Unless noted otherwise, PASS results refer to scoped exact run +`20260826t214418z-934198-11400` and its report with SHA-256 +`9deb2382b15c6b5bed63e9ebe3a628c8e2c4f50527cb6f651f14eaf51d38e0aa`. + +| README contract | Focused command and expected result | Actual result and retained evidence | +| --- | --- | --- | +| Root firstboot password, email, and domain | Submit the rendered sign-in form with `TKL_TEST_APP_PASS`, then query the authenticated user endpoint. The session identifies `root` and reports the firstboot email at the configured domain. | PASS: product loop 5 persisted `admin@example.invalid`, the password-change mail used the same recipient, and the exact authenticated API assertion returned that address for `root`. | +| Project hosting and Git repository readback | Create a private project, push a commit, clone it to a fresh directory, and read the committed file through GitLab HTTP. The marker survives every read path. | PASS: `project-api`, `ssh-git-round-trip`, and `authenticated-web-read` preserved the same private-project marker. | +| Git over SSH | Register a throwaway Ed25519 key and use `git@127.0.0.1` for push and clone. Both operations succeed through GitLab Shell. | PASS: the throwaway key pushed and cloned through GitLab Shell. | +| Omnibus Nginx, PostgreSQL, Redis, Gitaly, and Sidekiq | Require every supervisor process, query the created project through `gitlab-psql`, enqueue `ProjectCacheWorker`, and wait for its uniquely keyed scheduled follow-up. The database row is exact and the background job completes without retry. | PASS: service checks, `database-readback`, and `background-jobs` completed. Sidekiq returned marker `37465613dcfb7fbbe65c2bd5`. | +| Postfix and Webmin Postfix module | Require `postfix.service` active and enabled and the packaged Webmin module directory present. Local appliance integration passes; public delivery is outside local acceptance. | PASS: the appliance contract found active Postfix and the packaged Webmin module. | +| Supervised manual updates | Run `gitlab-update --check`. It reports installed `19.3.0-ce.0`, the signed Trixie candidate, and the required-stop policy without mutation. | PASS: the exact corrected check required the appliance `signed-by` keyring and recorded key-download hash, then reported `up-to-date; candidate=19.3.0-ce.0`. | +| GitLab-owned Nginx and Confconsole Let's Encrypt integration | Require GitLab Nginx running and the GitLab-specific `get_certificate.py` plugin present. Public issuance is not attempted without a public DNS fixture. | PASS: the appliance contract found GitLab Nginx and the product-specific Confconsole plugin. | +| Standard Core administration | Inherited SSH, Webmin, cron, Trixie identity, ISO, and normal init behavior are not customized by this application. | Core 19 PASS run `20260824t010251z-1634-32241`, source `24c82ee3540ce545422742b0e28ba6b687c53ec2`, report SHA-256 `1a6c2d266b6a2898e98a073421e49212dd0237598861200a3cbd6e81d35a2936`. | + +The fixture revokes its personal access token and SSH key, removes the scheduled +test follow-up, requests project deletion, and deletes its local Git trees on +exit. GitLab retains revoked-token audit records according to normal product +behavior. + +## Commands, results, and evidence + +Static boundary checks pass for Bash syntax, Python compilation, ShellCheck, +`git diff --check`, active old-suite detection, result-protocol fields, and the +documentation ban on em dashes. A disposable Debian Trixie repository probe +also verifies the selected key hash, fingerprint, signed package-metadata hash, +and current update candidate. + +For the JSON instrumentation correction, the retained loop-5 retry build log +records `python3-minimal` and `python3` version `3.13.5-1` installed from +Debian Trixie. A disposable Trixie probe confirms the same signed Debian +candidate, runs ShellCheck, and passes the exact extracted JSON helper for +valid string and integer values. Wrong types, a boolean in place of an integer, +a missing key, and malformed JSON all fail closed. + +Product loop 1 used source commit +`f6ea46a50f0781f4028717dc64b4f86c3408d475` in run +`20260826t084052z-323768-26314`. The build reached GitLab's package +auto-reconfigure, then its Cinc log stopped at `2026-08-26T08:49:19Z` after +creating the logrotate runit supervision directories. The process chain was +`apt-get install` to `gitlab-ctl upgrade` to `cinc-client`, and the log remained +unchanged through `2026-08-26T09:09:48Z`. The run was stopped and retained as a +build-stage FAIL. The correction moves the existing non-init configuration +ahead of package configure and supplies the required runit supervisor. + +Product loop 2 used source commit +`9ca6dc9bb57c6e06c11a555b88ed7e9da76f98f7` in run +`20260826t093948z-997460-8925`. It passed the former runit hang and completed +GitLab package configuration. The build then stopped while clearing generated +secrets because GitLab 19.3's Rails dbconsole passed an empty +`statement_timeout` to PostgreSQL. Earlier `gitlab-psql` calls against the same +main database succeeded. The correction sends the existing SQL block through +that direct Omnibus PostgreSQL wrapper. + +Product loop 3 used source commit +`21ad50fcec350f2c62bd4d6b304895af80d3169c` in run +`20260826t100426z-1257552-1524`. It passed both earlier blockers and connected +with `gitlab-psql`. The scrub then found that GitLab 19 no longer has the +`ci_runners.token` column or `web_hook_logs` relation. The final service stop +also returned a timeout while Puma was still exiting. GitLab's v19.3 schema +confirms that `ci_runners.token_encrypted` and every retained truncate target +exist. The correction removes the obsolete targets, makes the SQL fail fast, +and retries service shutdown three times before returning a terminal failure. + +Product loop 4 used source commit +`9c6323064650909dc87d54975e5a4b99f1ad0656` in run +`20260826t104919z-1723156-31927`. The build completed, including the fail-fast +v19 scrub and the bounded service stop, and firstboot completed normally. The +focused test's first UI request then received HTTP 502 while GitLab was still +settling after firstboot restarted Puma and Workhorse. GitLab documents that +its [readiness probe][health-check] holds traffic until Rails and dependent +services are ready, including the brief Puma restart window. The correction +polls that probe with a five-minute bound before attempting the normal sign-in +flow. Extracted-function fixtures pass for immediate success, success on the +third probe, and exhaustion after exactly 61 probes and 60 two-second sleeps. + +An infrastructure-only run after product loop 4 used source commit +`e2286dbca78775ed877f2c1539ed8a5e1632d918` in run +`20260826t113407z-2022901-16188`. The build and image import completed, +including the fail-fast v19 scrub and bounded service stop. The harness then +reported that the runtime container stopped before reaching health. Docker's +event record shows the container remained alive through repeated multi-user +health polls, with no preceding `die` event, until harness cleanup sent signal +9 at nanosecond timestamp `1787746535441440994`; the resulting exit was 137. +The harness treated a transient or empty `docker inspect` result as a stopped +container. GitLab readiness and the focused test were not reached, so this run +provides build and import evidence but no application acceptance evidence. + +The infrastructure retry of that run used the same source commit with +reviewed harness commit `5746bdca17347598f92767d1a9c76b6406a160d4` in run +`20260826t124900z-2678316-2241`. Build, import, runtime health, GitLab +readiness, and root web login passed. The test then stopped at its first JSON +assertion because `jq` was a test-only assumption and is not an appliance +dependency. The correction uses Debian Python already inherited through Core, +preserves string and integer type checks, and does not change appliance +packages or runtime behavior. This is test instrumentation, not a product +loop. + +The scoped infrastructure retry used source commit +`40267755e0a037c5f25a2517b009ad1b0e26e667`, pinned harness commit +`152bc9b876557266b90ed4ded77b611d9b817aff`, and scoped label +`org.turnkeylinux.v19-harness.wave2-2` in run +`20260826t181108z-156555-30392`. The exact rootfs build, configured-root +fallback, scoped image import, runtime health, GitLab readiness, root login, +typed JSON checks, private project creation, and both SSH Git operations +passed. The focused test then exited after the SSH operations without a child +result or assertion message. The retained report is +`/home/agent/.local/state/turnkey-v19-harness-wave2-2/failures/gitlab/current/report.txt`, +with SHA-256 +`739f72a48e3f4b0388fb0ceb1ca43250f466ca12a2b2244ec6553312c7574ade`. + +The next assertion used a personal access token on the non-API web raw route. +GitLab 19.3's [raw controller][raw-controller] requests `:blob` sessionless +authentication. Its [request authenticator][request-authenticator] tries web +personal access tokens and then static-object tokens, but the +[authentication finder][auth-finders] does not accept `:blob` as a personal +access token web format. The documented personal access token contract is the +[repository files API raw route][repository-files-api]. GitLab also documents +that normal web sign-in sets the `_gitlab_session` cookie used by its web +frontend in the [REST authentication guide][rest-authentication]. + +A disposable rootless probe used the official GitLab CE 19.3.0 container image +with digest +`sha256:f7e453ff51d1910235365085fe836e4589716d26b44d99a8aa3e2c41377f034f`. +It created a private project and file, then recorded this response A/B: + +```text +PRIVATE-TOKEN, /root/.../-/raw/main/README.md: +HTTP/1.1 302 Found +Location: http://gitlab-probe.invalid:18081/users/sign_in + +_gitlab_session cookie, /root/.../-/raw/main/README.md: +HTTP/1.1 200 OK +GitLab v19 private raw authentication probe + +PRIVATE-TOKEN, /api/v4/projects/1/repository/files/README.md/raw?ref=main: +HTTP/1.1 200 OK +GitLab v19 private raw authentication probe +``` + +This directly confirms a test-only authentication mismatch. The focused test +now reuses its signed-in session cookie on the web route. Named phase markers, +an error trap with the failing line, and explicit messages on every `grep` +assertion prevent another assertion from exiting silently. The disposable +probe container and image were removed after capture. An extracted marker +matrix confirms that a passing assertion reports its phase, an unannotated +failure reports phase, status, and line, and an explicit assertion failure +reports its phase and message. The three cases return status 0, 1, and 1. + +The second scoped infrastructure retry used source commit +`dce631550cd32d6d45fd59571f654dc05408da97` and the same pinned harness in +run `20260826t190222z-347388-9218`. Build, configured-root fallback, image +import, runtime health, readiness, login, typed JSON checks, the private +project and SSH Git round trip, authenticated web read, and database readback +passed. The background-job phase then failed at line 227 because the test +wrote `background-job.rb` below its root-owned mode-700 private fixture +directory. `gitlab-rails runner` could not traverse that directory after its +normal privilege drop. Cleanup, identity, and disk checks passed. The retained +source archive SHA-256 is +`3c4172f3b0046d335a975232a1c873f68b1683545fc47047b34d9e05dcd0b91d`, +the input-tree SHA-256 is +`042c8fdd27ae27520ffe5216a9619f7b833382f5f3304eb42481d03d7661a19f`, +and the build-tree SHA-256 is +`932d9ae36483a530798f22110e84704e4e4ef4b26c2097fb46a8d57d7c44a95f`. +The retained report at +`/home/agent/.local/state/turnkey-v19-harness-wave2-2/failures/gitlab/current/report.txt` +has SHA-256 +`3dbbd281a2b243550eb09a6a63143a62a8d8524386e0873c0d12320d8880fab5` +and the retained run log has SHA-256 +`e5ed10fdaeb4936d970ed51118a37747e748f88252a0925b3a842b7e88dd76d1`. +The retained `SHA256SUMS` and `RETAINED-SHA256SUMS` files have SHA-256 +`8d8c4b64b171197754dbc007cfc42271308f41ae239bd99c7266ad8a6acdcdf8` +and `6a84f16f7ed12ee71a3e98d12a06090e2eb9f4b4d26a8323ff414cb4e794487b`. + +A disposable probe used the same official GitLab CE 19.3.0 image digest as +the raw-authentication probe. Its `/opt/gitlab/bin/gitlab-rails` wrapper has +SHA-256 +`fab022de950858249d0ef68c02b443ef00c026b0d4b3d93c4c65c5dac7814fd9` +and drops to the configured `git:git` service identity. An exact +`gitlab-rails runner` matrix recorded: + +```text +root:root mode 0700 parent, root:root mode 0644 script: +status=1 +The file .../background-job.rb could not be found, please check and try again. + +root:git mode 0640 script directly under /tmp: +status=0 +gitlab-rails runner user: 998:998 +``` + +The correction creates only the generated background-job script as an +exclusive `mktemp` file directly under `/tmp`, then grants read access to the +`git` group with mode 0640. The script contains no credential or secret, the +project identifier remains an environment variable, and cleanup removes the +script. Cookies, the personal access token, the SSH private key, and Git trees +remain below the original mode-700 directory. This is another test-only +permission correction, so the product ledger remains at 4 of 6 loops. + +The scoped pre-review run used source commit +`adbe2afbf46d2bffc3205451a17468ea86060f5a`, pinned harness commit +`152bc9b876557266b90ed4ded77b611d9b817aff`, and scoped label +`org.turnkeylinux.v19-harness.wave2-2` in run +`20260826t194621z-508720-29542`. The source commit and transport archives both +have SHA-256 +`114a632a43584f130408b55cfbf9684e8b661d4d983b0899ed269f3c8917888a`. +The input-tree SHA-256 is +`f4dcd9c4936703c741c8af9f3a90b26973a9ba839f71196512ce60b0cb3715ae` +and the build-tree SHA-256 is +`a152d803893465195ebafef769a6a56d26efb23e52831c7dbdfc467d849c4064`. + +Preflight, the exact rootfs build, scoped image import, runtime health, GitLab +readiness, and every focused test phase passed. The build crossed the pinned +GitLab package configuration, v19 schema scrub, and bounded service-stop +boundaries. The configured-root check recorded the expected overlayfs fallback +with status 32 before import. Runtime reached multi-user state, completed +inithooks, and reported the appliance service active. The test passed +`appliance-contract`, `readiness`, `web-login`, `project-api`, +`ssh-git-round-trip`, `authenticated-web-read`, `database-readback`, +`background-jobs`, `package-update-channel`, `result`, and `complete`. +Sidekiq returned marker `fccc077c7203d30d44ba9566`. The updater reported +`up-to-date; candidate=19.3.0-ce.0` on the official GitLab CE Debian Trixie +supervised-update channel. Product and runtime tests returned status 0, cleanup +was verified, and the final verdict was PASS. These final authentication and +service-readable-script corrections remain test instrumentation, so the +product ledger remained at 4 of 6 loops. + +The retained report is +`/home/agent/.local/state/turnkey-v19-harness-wave2-2/runs/gitlab/20260826t194621z-508720-29542/report.txt` +with SHA-256 +`4414e9e1ba4f188e6f7614e8748a3f942c30450beebd518cee02ba2e53abc1b0`. +The run-log SHA-256 is +`cf303ace0f1d54a182ff3d58ecce842206946e1572e0180a815e689103bcdbdf`, +and the child and runtime result files both have SHA-256 +`a9abda94ef91953914eefea03c422f1f3895f718e4fe2c020b23b77be2efafc0`. +The retained `SHA256SUMS` file, whose entries verify successfully, has SHA-256 +`cbe2e2bc16c11566d9ea5f49eb7addf7c117eea2077737ab464468538a514990`. + +Independent review then found that the firstboot prompt promised to set the +root user's email, while the implementation only changed +`gitlab_email_from`. Run `20260826t194621z-508720-29542` used +`APP_EMAIL=admin@example.invalid`, but its focused login assertion checked only +the username. This is an application behavior defect and consumes product loop +5. The correction passes the address through the environment to +`gitlab-rails runner`, calls `skip_reconfirmation!`, and persists the primary +email with `update!`. GitLab 19.3's [user model specification][root-email-spec] +confirms that this sequence persists a confirmed primary email without an +unconfirmed value. The focused login phase now reads the expected firstboot +value from the exact harness's nonsecret fixture contract and asserts the +authenticated API's `email` field. The product ledger is now 5 of 6 loops. + +The same review found Trixie-incompatible `apt-key` recovery instructions. The +replacement procedure uses only the repository's `signed-by` keyring, verifies +the full fingerprint published by GitLab before changing trust, updates the +recorded fingerprint and download hash, and reruns `gitlab-update --check`. +The updater now fails closed unless the source line names the exact appliance +keyring and the source record contains a valid download hash. + +A disposable Debian Trixie probe fetched GitLab's current key and reconfirmed +download SHA-256 +`003c0ca2fea61767f8c6de7a1c0f49fc88ea3c8db95e3cd1856b32ce9d876e0f` +and fingerprint `F6403F6544A38863DAA0B6E03F01618A51312F3F`. The exact updater returned +status 0 with matching keyring, source line, fingerprint, and download hash. +An incorrect `signed-by` path, an incorrect fingerprint, and a malformed +download hash each returned status 1. A stubbed firstboot matrix confirmed +that the email is passed only through `TKL_ROOT_EMAIL`, successful persistence +continues to password reset, and a persistence status of 17 stops before the +password reset. The extracted authenticated-user assertion accepts the +firstboot address and rejects the former `admin@example.com` value. + +Product loop 5 exact run `20260826t205442z-743235-32572` used source commit +`d9f4cd9323de76d4f81e93bf47e448c74cd4b7ae`, commit and transport archive +SHA-256 +`57df01af0b2268979631647fdb28b26a8617eff2ccd6f4e4e491a7d01090e353`, +input-tree SHA-256 +`b223e1612840b51c3de6589fda156fdcc712eb7b1096af7801d939b154a0423e`, +and build-tree SHA-256 +`b2d8a225dc32f2cabf938022b1028de49f47f242660235870719936a9b843c16`. +The rootfs build, configured-root fallback, scoped import, and runtime health +passed. Inithooks completed and the retained journal recorded the root-email +update, successful `40gitlab`, and a password-change message addressed to +`admin@example.invalid`. This proves the corrected product behavior reached +the prompted root address. + +The focused test then failed before submitting web login because Core had +scrubbed `APP_EMAIL` from the mounted firstboot preseed after inithooks. The +expected-value lookup was test instrumentation, not appliance behavior. The +correction uses the exact harness's nonsecret `admin@example.invalid` fixture +constant and retains the positive and negative API assertion. This failure +does not consume another product loop, so the ledger remains 5 of 6. The +retained report at +`/home/agent/.local/state/turnkey-v19-harness-wave2-2/failures/gitlab/current/report.txt` +has SHA-256 +`2b751293e90e4bfa5053feecfb4af4fb21119bbc21f9cd7e42ec9af3ec438970`. +The run-log SHA-256 is +`fab0bece3897765f7b4181d9b6512c2285d761ead38ebc449588b6fe9a99e35d`, +the `SHA256SUMS` SHA-256 is +`a5d264feb3cbcaaef556d116d964251f962c2cc34febb898213caf61c4522ac7`, +and the `RETAINED-SHA256SUMS` SHA-256 is +`bd40294884492d6eb1f413bd20d8637521cd1345137f753f720818d495e8e1a5`. + +The test-only retry used source commit +`99e44733373078290fe7a45072a64764277a8309`, pinned harness commit +`152bc9b876557266b90ed4ded77b611d9b817aff`, and scoped label +`org.turnkeylinux.v19-harness.wave2-2` in run +`20260826t214418z-934198-11400`. The commit and transport archives both have +SHA-256 +`7a35cdb9ca0fbfbdee7af43cfb4a6aa684ed7b27963fe4a924fb17625203c36a`. +The input-tree SHA-256 is +`7b9865913cfb8db0352564d1e51a01d2d8f677dd7418bcd28936da0fff910518` +and the build-tree SHA-256 is +`74762474f70bc4b1b4d21de8cf3bc59a8c0e19d6cedea34d4195b78bb4e7da2b`. + +The HTTPS preflight, exact rootfs build, configured-root fallback, scoped +image import, runtime health, and every focused phase passed. Runtime reached +multi-user state, completed inithooks, and reported the appliance service +active. The test passed `appliance-contract`, `readiness`, `web-login`, +`project-api`, `ssh-git-round-trip`, `authenticated-web-read`, +`database-readback`, `background-jobs`, `package-update-channel`, `result`, +and `complete`. The authenticated user API returned the exact firstboot +address, the private repository marker survived the API, SSH, clone, and web +read paths, PostgreSQL returned the created project, Sidekiq returned marker +`37465613dcfb7fbbe65c2bd5`, and the updater validated the synchronized trust +record before reporting `up-to-date; candidate=19.3.0-ce.0`. + +Product and runtime tests returned status 0, scoped cleanup was verified, and +the final verdict was PASS. The retained report at +`/home/agent/.local/state/turnkey-v19-harness-wave2-2/runs/gitlab/20260826t214418z-934198-11400/report.txt` +has SHA-256 +`9deb2382b15c6b5bed63e9ebe3a628c8e2c4f50527cb6f651f14eaf51d38e0aa`. +The run-log SHA-256 is +`7cf0e9d5dfd53b0b9c0b0831cdfdb4c06ac898b21c83faf1b5a05e6e09739bbe`, +the child and runtime result files both have SHA-256 +`cb3019189142fade79cddcfabbf4b2b51c32d19c044030f63886f240a292cb4a`, +and the retained `SHA256SUMS` file has SHA-256 +`5054869a7790d7abea30787cb399e3d08b1fea23a20bf95184550aa68ae73d6e`. +Every manifest entry verifies. This retry corrected only test instrumentation, +so the product ledger remains 5 of 6 loops. + +The exact passing command was: + +```sh +TKLDEV_CONTAINER=tkldev19-wave2-2 \ +TKL_HARNESS_STATE_DIR=/home/agent/.local/state/turnkey-v19-harness-wave2-2 \ +TKL_HARNESS_LOCK_FILE=/home/agent/.local/state/turnkey-v19-harness-wave2-2/build.lock \ +TKL_HARNESS_DOCKER_LIMIT_BYTES=137438953472 \ +TKL_HARNESS_DOCKER_OBJECT_LABEL=org.turnkeylinux.v19-harness.wave2-2 \ +/home/agent/.local/worktrees/turnkey/harness-wave2-2-152bc9b/tools/test-v19-appliance gitlab \ + --source /home/agent/.local/worktrees/turnkey-apps/gitlab/wish-gitlab-v19-trixie +``` + +## Deferred issues + +- GitLab upgrades are supervised because required stops occur at minor + versions x.2, x.5, x.8, and x.11. The helper reports candidates but does not + choose or install the next stop. +- The local test checks that the product-specific Let's Encrypt integration is + retained. Public certificate issuance needs an externally resolvable fixture. +- API project deletion can complete asynchronously. The test submits cleanup, + but does not wait for GitLab's background deletion worker. +- Docker acceptance does not repeat installer, kernel, or hardware checks. +- The retained product-loop-5 failure bundle's original `SHA256SUMS` still + names the pruned `failure-status.txt`. Its `RETAINED-SHA256SUMS` is the + authoritative post-retention manifest and verifies every retained file. + +[supported-platforms]: https://docs.gitlab.com/install/package/#supported-platforms +[health-check]: https://docs.gitlab.com/administration/monitoring/health_check/ +[raw-controller]: https://gitlab.com/gitlab-org/gitlab/-/blob/v19.3.0-ee/app/controllers/projects/raw_controller.rb +[request-authenticator]: https://gitlab.com/gitlab-org/gitlab/-/blob/v19.3.0-ee/lib/gitlab/auth/request_authenticator.rb +[auth-finders]: https://gitlab.com/gitlab-org/gitlab/-/blob/v19.3.0-ee/lib/gitlab/auth/auth_finders.rb +[repository-files-api]: https://docs.gitlab.com/api/repository_files/#retrieve-a-raw-file-from-a-repository +[rest-authentication]: https://docs.gitlab.com/api/rest/authentication/#session-cookie +[root-email-spec]: https://gitlab.com/gitlab-org/gitlab/-/blob/v19.3.0-ee/spec/models/user_spec.rb#L2971-2984 diff --git a/overlay/etc/apt/sources.list.d/gitlab-ce.list b/overlay/etc/apt/sources.list.d/gitlab-ce.list index ed00e1c..d49de92 100644 --- a/overlay/etc/apt/sources.list.d/gitlab-ce.list +++ b/overlay/etc/apt/sources.list.d/gitlab-ce.list @@ -1,5 +1,4 @@ # this file was created by TurnKey Linux for use with the # repository at https://packages.gitlab.com/gitlab/gitlab-ce -deb [signed-by=/usr/share/keyrings/gitlab-ce.gpg] https://packages.gitlab.com/gitlab/gitlab-ce/debian/ bullseye main -deb-src [signed-by=/usr/share/keyrings/gitlab-ce.gpg] https://packages.gitlab.com/gitlab/gitlab-ce/debian/ bullseye main +deb [signed-by=/usr/share/keyrings/gitlab-ce.gpg] https://packages.gitlab.com/gitlab/gitlab-ce/debian/ trixie main diff --git a/overlay/usr/lib/inithooks/bin/gitlab.py b/overlay/usr/lib/inithooks/bin/gitlab.py index 499ab1e..81ac6b0 100755 --- a/overlay/usr/lib/inithooks/bin/gitlab.py +++ b/overlay/usr/lib/inithooks/bin/gitlab.py @@ -12,10 +12,11 @@ """ -import sys import getopt +import os +import sys from libinithooks import inithooks_cache -from subprocess import run, Popen, PIPE +from subprocess import run from libinithooks.dialog_wrapper import Dialog @@ -39,7 +40,7 @@ def main(): except getopt.GetoptError as e: usage(e) - password = "" + password = os.environ.get("APP_PASS", "") email = "" domain = "" schema = "" @@ -97,24 +98,45 @@ def main(): domain = f"{schema}{domain}" else: domain = f"http://{domain}" - run(["sed", "-i", f"/^external_url/ s|'.*|'{domain}'|", config]) + run(["sed", "-i", f"/^external_url/ s|'.*|'{domain}'|", config], + check=True) run(["sed", "-i", fr"/^gitlab_rails\['gitlab_email_from'\]/ s|=.*|= '{email}'|", - config]) - run(["gitlab-ctl", "reconfigure"]) + config], check=True) + run(["gitlab-ctl", "reconfigure"], check=True) + + print("Setting GitLab 'root' user email. This might take a while.") + email_env = os.environ.copy() + email_env.pop('APP_PASS', None) + email_env['TKL_ROOT_EMAIL'] = email + update_email = run( + ["gitlab-rails", "runner", + "root = User.find_by!(username: 'root'); " + "root.skip_reconfirmation!; " + "root.update!(email: ENV.fetch('TKL_ROOT_EMAIL'))"], + env=email_env, + text=True, + capture_output=True, + ) + if update_email.stdout: + print(update_email.stdout, end="") + if update_email.stderr: + print(update_email.stderr, file=sys.stderr, end="") + if update_email.returncode: + sys.exit(update_email.returncode) print("Setting GitLab 'root' user password. This might take a while.") - p1 = Popen(["echo", "-e", f"{password}\n{password}\n"], stdout=PIPE) - p2 = Popen(["gitlab-rake", "gitlab:password:reset[root]"], - stdin=p1.stdout, stdout=PIPE) - p1.stdout.close() - if p2.returncode == 0: - stream = sys.stdout - else: - stream = sys.stderr - output = p2.communicate()[0] - print(output.decode(), file=stream) - sys.exit(p2.returncode) + reset = run( + ["gitlab-rake", "gitlab:password:reset[root]"], + input=f"{password}\n{password}\n", + text=True, + capture_output=True, + ) + if reset.stdout: + print(reset.stdout, end="") + if reset.stderr: + print(reset.stderr, file=sys.stderr, end="") + sys.exit(reset.returncode) if __name__ == "__main__": diff --git a/overlay/usr/lib/inithooks/firstboot.d/40gitlab b/overlay/usr/lib/inithooks/firstboot.d/40gitlab index e8b4fc7..9023eb3 100755 --- a/overlay/usr/lib/inithooks/firstboot.d/40gitlab +++ b/overlay/usr/lib/inithooks/firstboot.d/40gitlab @@ -11,10 +11,12 @@ if ! systemctl is-active --quiet gitlab-runsvdir.service; then fi if [[ -f "$INITHOOKS_CONF" ]]; then + # shellcheck disable=SC1090 source "$INITHOOKS_CONF" else echo "$(basename "$0"): Warning: $INITHOOKS_CONF not found or is not a file (expected if not preseeded" >&2 fi -"$INITHOOKS_PATH/bin/gitlab.py" --pass="$APP_PASS" --email="$APP_EMAIL" --domain="$APP_DOMAIN" \ +APP_PASS="$APP_PASS" "$INITHOOKS_PATH/bin/gitlab.py" \ + --email="$APP_EMAIL" --domain="$APP_DOMAIN" \ || fatal "$INITHOOKS_PATH/bin/gitlab.py failed" diff --git a/overlay/usr/local/sbin/gitlab-update b/overlay/usr/local/sbin/gitlab-update new file mode 100755 index 0000000..fe7563f --- /dev/null +++ b/overlay/usr/local/sbin/gitlab-update @@ -0,0 +1,46 @@ +#!/bin/bash +set -Eeuo pipefail + +if [[ ${1:-} != --check || $# -ne 1 ]]; then + echo "usage: gitlab-update --check" >&2 + exit 2 +fi + +source_record=/usr/local/share/turnkey-gitlab/source +keyring=/usr/share/keyrings/gitlab-ce.gpg +source_list=/etc/apt/sources.list.d/gitlab-ce.list +source_line="deb [signed-by=$keyring] https://packages.gitlab.com/gitlab/gitlab-ce/debian/ trixie main" + +# shellcheck disable=SC1090 +. "$source_record" +: "${installed_version:?installed_version is missing from $source_record}" +: "${repository_key_fingerprint:?repository_key_fingerprint is missing from $source_record}" +: "${repository_key_sha256:?repository_key_sha256 is missing from $source_record}" + +installed=$(dpkg-query -W -f='${Version}' gitlab-ce) +candidate=$(apt-cache policy gitlab-ce | + awk '/Candidate:/ { print $2; exit }') +fingerprint=$(gpg --show-keys --with-colons "$keyring" | + awk -F: '$1 == "fpr" { print $10; exit }') + +test "$installed" = "$installed_version" +test "$fingerprint" = "$repository_key_fingerprint" +[[ $repository_key_sha256 =~ ^[0-9a-f]{64}$ ]] +test "$candidate" != "(none)" +grep -Fxq "$source_line" "$source_list" + +if dpkg --compare-versions "$candidate" eq "$installed"; then + status=up-to-date +else + status=supervised-upgrade-available +fi + +cat <&2 + exit 1 +} + +report_error() { + local status=$1 + local line=$2 + + trap - ERR + printf 'error phase=%s status=%s line=%s\n' \ + "$phase" "$status" "$line" >&2 + exit "$status" +} + +trap 'report_error "$?" "$LINENO"' ERR + +base=$(sed -n "s/^external_url '\([^']*\)'.*/\1/p" /etc/gitlab/gitlab.rb) +scheme=${base%%://*} +host=${base#*://} +host=${host%%/*} +host=${host%%:*} +if [[ $scheme == https ]]; then + port=443 +else + port=80 +fi +curl_local=(curl --insecure --silent --show-error --resolve "$host:$port:127.0.0.1") + +wait_gitlab_ready() { + for _ in {1..60}; do + if "${curl_local[@]}" --fail --connect-timeout 1 --max-time 3 \ + --output /dev/null "$base/-/readiness?all=1" 2>/dev/null; then + return 0 + fi + sleep 2 + done + "${curl_local[@]}" --fail --connect-timeout 1 --max-time 3 \ + --output /dev/null "$base/-/readiness?all=1" +} + +json_value() { + local expected_type=$1 + local key=$2 + + python3 -c ' +import json +import sys + +expected_type, key = sys.argv[1:] +value = json.load(sys.stdin)[key] +if expected_type == "string": + valid = isinstance(value, str) +elif expected_type == "integer": + valid = isinstance(value, int) and not isinstance(value, bool) +else: + raise SystemExit(2) +if not valid: + raise SystemExit(1) +sys.stdout.write(str(value)) +' "$expected_type" "$key" +} + +cleanup() { + set +e + trap - ERR + if [[ -n $key_id ]]; then + "${curl_local[@]}" --request DELETE \ + --header "PRIVATE-TOKEN: $token" \ + "$base/api/v4/user/keys/$key_id" >/dev/null + fi + if [[ -n $project_id ]]; then + "${curl_local[@]}" --request DELETE \ + --header "PRIVATE-TOKEN: $token" \ + "$base/api/v4/projects/$project_id" >/dev/null + fi + gitlab-rails runner \ + "item = PersonalAccessToken.find_by_token('$token'); item.revoke! if item" \ + >/dev/null 2>&1 + if [[ $ruby == /tmp/gitlab-v19-background-job.*.rb ]]; then + rm -f -- "$ruby" + fi + find "$work" -depth -delete +} +trap cleanup EXIT + +mark_phase appliance-contract +for unit in gitlab-runsvdir.service postfix.service; do + systemctl --quiet is-active "$unit" + systemctl --quiet is-enabled "$unit" +done +for component in nginx postgresql redis sidekiq gitaly; do + gitlab-ctl status "$component" | grep -Fq "run: $component:" || + fail "GitLab component is not running: $component" +done +grep -Fxq 'VERSION_CODENAME=trixie' /etc/os-release || + fail 'operating system is not Debian Trixie' +grep -Eq '^turnkey-gitlab-19\.0' /etc/turnkey_version || + fail 'appliance version is not GitLab 19.0' +test -d /usr/share/webmin/postfix +test -f /usr/lib/confconsole/plugins.d/Lets_Encrypt/get_certificate.py + +# shellcheck disable=SC1090 +. "$source_file" +: "${installed_version:?installed_version is missing from $source_file}" +: "${package_sha256:?package_sha256 is missing from $source_file}" +: "${repository_key_fingerprint:?repository_key_fingerprint is missing from $source_file}" +: "${repository_key_sha256:?repository_key_sha256 is missing from $source_file}" +test "$(dpkg-query -W -f='${Version}' gitlab-ce)" = "$installed_version" +test "$installed_version" = 19.3.0-ce.0 +test "$package_sha256" = f88f80cd61d6b2beb35aa7207591d4abdfed0e6c2c42e6ed753dd29ea5de076d +test "$repository_key_sha256" = 003c0ca2fea61767f8c6de7a1c0f49fc88ea3c8db95e3cd1856b32ce9d876e0f +test "$(gpg --show-keys --with-colons /usr/share/keyrings/gitlab-ce.gpg | + awk -F: '$1 == "fpr" { print $10; exit }')" = \ + "$repository_key_fingerprint" + +mark_phase readiness +wait_gitlab_ready +mark_phase web-login +"${curl_local[@]}" --fail --cookie-jar "$cookie" \ + "$base/users/sign_in" >"$page" +csrf=$(sed -n 's/.*name="authenticity_token" value="\([^"]*\)".*/\1/p' \ + "$page") +test -n "$csrf" +"${curl_local[@]}" --fail --location --cookie "$cookie" \ + --cookie-jar "$cookie" \ + --data-urlencode "authenticity_token=$csrf" \ + --data-urlencode 'user[login]=root' \ + --data-urlencode "user[password]=$app_password" \ + --data-urlencode 'user[remember_me]=0' \ + "$base/users/sign_in" >"$page" +root_account=$("${curl_local[@]}" --fail --cookie "$cookie" \ + "$base/api/v4/user") +test "$(json_value string username <<<"$root_account")" = root || + fail 'authenticated GitLab account is not root' +test "$(json_value string email <<<"$root_account")" = \ + "$expected_root_email" || + fail 'root account email does not match the firstboot value' + +mark_phase project-api +gitlab-rails runner \ + "item = User.find_by_username('root').personal_access_tokens.create!(scopes: ['api'], name: '$fixture', expires_at: 1.day.from_now); item.set_token('$token'); item.save!" + +project=$("${curl_local[@]}" --fail --request POST \ + --header "PRIVATE-TOKEN: $token" \ + --data-urlencode "name=$fixture" \ + --data-urlencode "path=$fixture" \ + --data 'visibility=private' \ + "$base/api/v4/projects") +project_id=$(json_value integer id <<<"$project") +test "$(json_value string path <<<"$project")" = "$fixture" + +ssh-keygen -q -t ed25519 -N '' -f "$work/id" +key=$("${curl_local[@]}" --fail --request POST \ + --header "PRIVATE-TOKEN: $token" \ + --data-urlencode "title=$fixture" \ + --data-urlencode "key=$(<"$work/id.pub")" \ + "$base/api/v4/user/keys") +key_id=$(json_value integer id <<<"$key") +ssh-keyscan -T 10 127.0.0.1 >"$work/known_hosts" 2>/dev/null +export GIT_SSH_COMMAND="ssh -i $work/id -o IdentitiesOnly=yes -o UserKnownHostsFile=$work/known_hosts" + +mark_phase ssh-git-round-trip +git -C "$work" init -q repository +git -C "$work/repository" config user.name 'TurnKey acceptance' +git -C "$work/repository" config user.email 'acceptance@example.invalid' +printf 'GitLab v19 project round trip\n' >"$work/repository/README.md" +git -C "$work/repository" add README.md +git -C "$work/repository" commit -qm 'Add acceptance marker' +git -C "$work/repository" remote add origin \ + "git@127.0.0.1:root/$fixture.git" +git -C "$work/repository" push -q -u origin HEAD:main +git clone -q "git@127.0.0.1:root/$fixture.git" "$work/readback" +grep -Fxq 'GitLab v19 project round trip' "$work/readback/README.md" || + fail 'cloned repository does not contain the pushed marker' + +mark_phase authenticated-web-read +"${curl_local[@]}" --fail --location --cookie "$cookie" \ + "$base/root/$fixture/-/raw/main/README.md" | + grep -Fxq 'GitLab v19 project round trip' || + fail 'authenticated web raw read does not contain the pushed marker' + +mark_phase database-readback +gitlab-psql --no-align --tuples-only --command \ + "SELECT path FROM projects WHERE id = $project_id;" | + grep -Fxq "$fixture" || + fail 'PostgreSQL project readback does not match the fixture' + +mark_phase background-jobs +gitlab-ctl status sidekiq | grep -Fq 'run: sidekiq:' || + fail 'Sidekiq is not running before the job round trip' +ruby=$(mktemp /tmp/gitlab-v19-background-job.XXXXXXXX.rb) +cat >"$ruby" <<'RUBY' +require 'sidekiq/api' +project_id = Integer(ENV.fetch('TKL_PROJECT_ID'), 10) +statistics = ['repository_size'] +lease_key = ['project_cache_worker', project_id, *statistics].join(':') +jid = ProjectCacheWorker.perform_async(project_id, [], statistics) +deadline = 90.seconds.from_now +loop do + followup = Sidekiq::ScheduledSet.new.find do |job| + job.klass == 'UpdateProjectStatisticsWorker' && + job.args[0] == lease_key && job.args[1] == project_id + end + if followup + followup.delete + puts "Sidekiq project cache round trip: #{jid}" + break + end + retry_job = Sidekiq::RetrySet.new.find_job(jid) + raise "ProjectCacheWorker entered retry: #{retry_job.error_message}" if retry_job + raise 'ProjectCacheWorker timed out' if Time.current >= deadline + sleep 1 +end +RUBY +chgrp git "$ruby" +chmod 0640 "$ruby" +TKL_PROJECT_ID=$project_id gitlab-rails runner "$ruby" + +mark_phase package-update-channel +gitlab-update --check >"$work/update" +candidate=$(sed -n 's/^candidate=//p' "$work/update") +status=$(sed -n 's/^status=//p' "$work/update") +test -n "$candidate" +grep -Fxq 'channel=official-gitlab-ce-debian-trixie' "$work/update" || + fail 'updater did not report the official GitLab CE Debian Trixie channel' +grep -Fxq "integrity=APT-signed-by-$repository_key_fingerprint" "$work/update" || + fail 'updater did not report the pinned repository key fingerprint' +grep -Fxq "repository_key_download_sha256=$repository_key_sha256" \ + "$work/update" || + fail 'updater did not report the verified repository key download hash' + +mark_phase result +cat >"$result" <