Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 6 additions & 6 deletions distribution/channels.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -183,7 +183,7 @@ channels:
strategy: local_file
files:
- charts/libredb-studio/Chart.yaml
extract: 'appVersion: "(\d+\.\d+\.\d+)"'
extract: "appVersion:\\s*[\"']?(\\d+\\.\\d+\\.\\d+)"
note: Enforcement lives in the required chart:check CI gate (#138); this row is visibility.

- id: homebrew
Expand All @@ -206,7 +206,7 @@ channels:
pin:
strategy: remote_file
url: https://raw.githubusercontent.com/libredb/homebrew-tap/main/Formula/libredb-studio.rb
extract: 'version "(\d+\.\d+\.\d+)"'
extract: "version\\s+[\"'](\\d+\\.\\d+\\.\\d+)"
note: Confirms the release-CI tap push actually landed.

- id: snap
Expand Down Expand Up @@ -425,7 +425,7 @@ channels:
pin:
strategy: remote_file
url: https://raw.githubusercontent.com/caprover/one-click-apps/master/public/v4/apps/libredb-studio.yml
extract: "defaultValue: '(\\d+\\.\\d+\\.\\d+)'"
extract: "defaultValue:\\s*[\"']?(\\d+\\.\\d+\\.\\d+)"
note: The single CapRover row. A LibreDB-owned 3rd-party repo
(libredb/caprover-one-click-apps) served the app while this submission was in review and was
retired once it merged; it had its own inventory entry until that duplicated CapRover in the
Expand Down Expand Up @@ -508,7 +508,7 @@ channels:
pin:
strategy: remote_file
url: https://raw.githubusercontent.com/kubero-dev/kubero/main/services/libredb-studio/app.yaml
extract: 'ghcr\.io/libredb/libredb-studio\s+tag:\s*(\d+\.\d+\.\d+)'
extract: "ghcr\\.io/libredb/libredb-studio\\s+tag:\\s*[\"']?(\\d+\\.\\d+\\.\\d+)"

- id: sealos
name: Sealos App Store template
Expand Down Expand Up @@ -561,7 +561,7 @@ channels:
pin:
strategy: remote_file
url: https://raw.githubusercontent.com/truenas/apps/master/ix-dev/community/libredb-studio/ix_values.yaml
extract: 'ghcr\.io/libredb/libredb-studio\s+tag:\s*(\d+\.\d+\.\d+)'
extract: "ghcr\\.io/libredb/libredb-studio\\s+tag:\\s*[\"']?(\\d+\\.\\d+\\.\\d+)"
note: Merged in truenas/apps#5501 (14 Aug 2026); live in the TrueNAS SCALE community catalog. The pin
reads the ghcr repo and tag in ix-dev/community/libredb-studio/ix_values.yaml (the maintainer-edited
source a bump PR touches) - the container_utils_image tag is a different repository and must not be
Expand Down Expand Up @@ -785,7 +785,7 @@ channels:
pin:
strategy: remote_file
url: https://raw.githubusercontent.com/YunoHost-Apps/libredb_studio_ynh/master/manifest.toml
extract: 'version = "(\d+\.\d+\.\d+)~ynh\d+"'
extract: "version\\s*=\\s*[\"']?(\\d+\\.\\d+\\.\\d+)"
note: Listed by YunoHost/apps#3618, merged 2026-09-20. Unlike the other self-hosting catalogs here, the
package installs no container; it unpacks the standalone Linux tarball under systemd and fetches its
own Node 24 through YunoHost's `n`, which is why platforms is linux and the runtime is
Expand Down
61 changes: 61 additions & 0 deletions tests/unit/distribution-check.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -425,6 +425,18 @@ describe("extractPin", () => {
"0.9.27",
);
});

// TrueNAS started quoting and digest-pinning the same field, and the pattern
// then matched nothing, so the channel reported UNKNOWN on every run for weeks
// and nobody saw it. This is that exact shape, so the quote cannot go
// unreadable again without a red test.
test("extracts a quoted, digest-pinned tag (truenas style)", () => {
const content =
' repository: ghcr.io/libredb/libredb-studio\n tag: "0.17.0@sha256:ce4d58724e2507a4467bcce6702d00760475e0f339071d878fce83e90c99718f"\n';
expect(
extractPin(content, 'ghcr\\.io/libredb/libredb-studio\\s+tag:\\s*"?(\\d+\\.\\d+\\.\\d+)', "truenas-scale"),
).toBe("0.17.0");
});
});

function helmChannel() {
Expand Down Expand Up @@ -686,6 +698,55 @@ describe("update.ci_enabled", () => {
const declared = channels.filter((c: { update: { ci_enabled?: boolean } }) => c.update.ci_enabled !== undefined);
expect(declared.map((c: { id: string }) => c.id).sort()).toEqual([...SWITCHABLE_CHANNEL_IDS].sort());
});

// TrueNAS quoted and digest-pinned a tag it had always written bare, the
// pattern stopped matching, and the channel reported UNKNOWN on every run for
// weeks with nobody noticing: strictFailures only covers local_file channels
// on every_release, so a remote pin that measures nothing never fails anything.
// Every pin below reads a field whose quoting the upstream owner can change
// without telling us, so each is asserted against all three legal spellings.
test("every quote-sensitive pin reads the value bare, single- and double-quoted", () => {
const channels = parseChannels(readFileSync(join(import.meta.dir, "../../distribution/channels.yaml"), "utf8"));
const shapes: Record<string, (v: string) => string> = {
"truenas-scale": (v) => ` repository: ghcr.io/libredb/libredb-studio\n tag: ${v}\n`,
kubero: (v) => ` repository: ghcr.io/libredb/libredb-studio\n tag: ${v}\n`,
helm: (v) => `appVersion: ${v}\n`,
homebrew: (v) => ` version ${v}\n`,
"caprover-official": (v) => ` defaultValue: ${v}\n`,
yunohost: (v) => `version = ${v}\n`,
};
// A formatter run upstream can turn one space into two, or into a tab, as
// easily as it can change a quote. Homebrew's pattern used to spell the gap
// as a single literal space while every other one used \\s.
const gaps: Record<string, string[]> = { homebrew: [" version ", " version ", " version\t"] };
for (const [id, shape] of Object.entries(shapes)) {
const channel = channels.find((c: { id: string }) => c.id === id);
expect(channel, `${id} is missing from the inventory`).toBeDefined();
const pattern = channel.pin.extract as string;
// Homebrew writes a Ruby string literal, so bare is not a spelling there:
// `version 0.17.0` is not valid Ruby, and accepting it would only widen the
// pattern towards matching something that is not the version.
const spellings = id === "homebrew" ? ['"9.9.9"', "'9.9.9'"] : ["9.9.9", '"9.9.9"', "'9.9.9'"];
for (const spelling of spellings) {
expect(extractPin(shape(spelling), pattern, id), `${id} cannot read ${spelling}`).toBe("9.9.9");
}
for (const gap of gaps[id] ?? []) {
expect(extractPin(`${gap}"9.9.9"\n`, pattern, id), `${id} cannot read the gap ${JSON.stringify(gap)}`).toBe(
"9.9.9",
);
}
}
});

// The digest is part of the tag TrueNAS publishes, and the capture has to stop
// before it or the channel reads a version no release ever carried.
test("a digest-pinned tag captures the version and stops at the digest", () => {
const channels = parseChannels(readFileSync(join(import.meta.dir, "../../distribution/channels.yaml"), "utf8"));
const truenas = channels.find((c: { id: string }) => c.id === "truenas-scale");
const body =
' image:\n repository: ghcr.io/libredb/libredb-studio\n tag: "0.17.0@sha256:ce4d58724e2507a4467bcce6702d00760475e0f339071d878fce83e90c99718f"\n container_utils_image:\n repository: ixsystems/container-utils\n tag: "1.0.2@sha256:46eba20714c1cc6784f60e245c32c33a2d9f616e47d804694a9854248c89a992"\n';
expect(extractPin(body, truenas.pin.extract, "truenas-scale")).toBe("0.17.0");
});
});

const SCRIPT = join(import.meta.dir, "../../scripts/distribution-check.mjs");
Expand Down
Loading