From 198df3b06a9dc7c204a9952398f5021775beccf1 Mon Sep 17 00:00:00 2001 From: Abel Paz Date: Mon, 5 Oct 2026 12:03:52 +0200 Subject: [PATCH 1/7] Added script to detect pgModeler versions to build --- scripts/detect_versions.py | 177 ++++++++++++++++++++++++++++++++ scripts/test_detect_versions.py | 164 +++++++++++++++++++++++++++++ 2 files changed, 341 insertions(+) create mode 100644 scripts/detect_versions.py create mode 100644 scripts/test_detect_versions.py diff --git a/scripts/detect_versions.py b/scripts/detect_versions.py new file mode 100644 index 0000000..817bfe9 --- /dev/null +++ b/scripts/detect_versions.py @@ -0,0 +1,177 @@ +#!/usr/bin/env python3 +"""Decide which pgModeler versions have to be built and published. + +Compares the upstream GitHub releases with the tags already on Docker Hub and +writes a GitHub Actions build matrix. Uses only the standard library. Running it +locally is always safe: it only reads public APIs and prints the result. + +Examples: + detect_versions.py every missing version >= MIN_VERSION + detect_versions.py --versions 1.2.3 only these (skipping published ones) + detect_versions.py --versions 1.2.0 --force rebuild even if already published + detect_versions.py --validate highest version, never tagged latest +""" +import argparse +import json +import os +import re +import sys +import urllib.request + +UPSTREAM_REPO = "nullptrlabs/pgmodeler" +IMAGE = "apazga/docker-pgmodeler" +# Oldest version built automatically; older ones are only built on request +MIN_VERSION = "1.2.0" +# Versions known not to build, never selected automatically (explicit requests still work) +SKIP_VERSIONS = frozenset() +# Base image per pgModeler major version: 1.x needs Qt >= 6.4, 2.x needs Qt >= 6.6 +BASE_IMAGES = {1: "ubuntu:24.04", 2: "ubuntu:26.04"} + +VERSION_RE = re.compile(r"^v?(\d+)\.(\d+)\.(\d+)(?:-(alpha|beta|rc)(\d*))?$") +STAGE_ORDER = {"alpha": 0, "beta": 1, "rc": 2, None: 3} + + +def parse_version(tag): + """Return the version of a release tag without the "v" prefix, or None if it is not one.""" + tag = tag.strip() + return tag.removeprefix("v") if VERSION_RE.match(tag) else None + + +def version_key(version): + """Sort key: 2.0.0-alpha < 2.0.0-alpha1 < 2.0.0-beta < 2.0.0-rc1 < 2.0.0.""" + match = VERSION_RE.match(version) + if not match: + raise ValueError(f"not a pgModeler version: {version!r}") + major, minor, patch, stage, number = match.groups() + return (int(major), int(minor), int(patch), STAGE_ORDER[stage], int(number or 0)) + + +def base_image_for(version): + major = version_key(version)[0] + return BASE_IMAGES.get(major, BASE_IMAGES[max(BASE_IMAGES)]) + + +def parse_requested(text): + """Split a "1.2.3, v2.0.0-beta1" style list into normalized versions.""" + versions = [] + for item in re.split(r"[,\s]+", text.strip()): + if not item: + continue + version = parse_version(item) + if version is None: + raise ValueError(f"not a pgModeler version: {item!r}") + versions.append(version) + return versions + + +def select_versions(upstream, published, requested=None, force=False, validate=False, + min_version=MIN_VERSION, skip=SKIP_VERSIONS): + """Return the build matrix entries: [{"version", "base_image", "latest"}, ...]. + + upstream are the versions released upstream and published the tags already on + Docker Hub. "latest" only goes to the highest upstream version >= min_version + that is not skipped, so building older versions never moves it backwards. + """ + if force and not requested: + raise ValueError("--force needs --versions") + floor = version_key(min_version) + candidates = sorted((v for v in set(upstream) if version_key(v) >= floor and v not in skip), + key=version_key) + highest = candidates[-1] if candidates else None + + if validate: + chosen = candidates[-1:] + elif requested: + unknown = sorted(set(requested) - set(upstream)) + if unknown: + raise ValueError(f"not released upstream: {', '.join(unknown)}") + chosen = sorted(set(requested), key=version_key) + if not force: + chosen = [v for v in chosen if v not in published] + else: + chosen = [v for v in candidates if v not in published] + + return [{"version": v, "base_image": base_image_for(v), "latest": v == highest and not validate} + for v in chosen] + + +def http_get_json(url, token=None): + headers = {"Accept": "application/json", "User-Agent": "docker-pgmodeler-ci"} + if token: + headers["Authorization"] = f"Bearer {token}" + with urllib.request.urlopen(urllib.request.Request(url, headers=headers), timeout=30) as response: + return json.load(response) + + +def fetch_upstream_versions(repo, token=None, get=http_get_json): + """Versions of the published (non-draft) GitHub releases of repo.""" + versions, page = [], 1 + while True: + releases = get(f"https://api.github.com/repos/{repo}/releases?per_page=100&page={page}", token) + if not releases: + return versions + for release in releases: + if release.get("draft"): + continue + version = parse_version(release["tag_name"]) + if version: + versions.append(version) + else: + print(f"warning: ignoring upstream tag {release['tag_name']!r}", file=sys.stderr) + page += 1 + + +def fetch_published_tags(image, get=http_get_json): + """Every tag of the Docker Hub repository image.""" + tags, url = set(), f"https://hub.docker.com/v2/repositories/{image}/tags?page_size=100" + while url: + data = get(url) + tags.update(tag["name"] for tag in data.get("results", [])) + url = data.get("next") + return tags + + +def write_github_output(entries, path): + with open(path, "a", encoding="utf-8") as f: + f.write(f"matrix={json.dumps({'include': entries})}\n") + f.write(f"count={len(entries)}\n") + + +def summary(entries): + if not entries: + return "Nothing to build: every upstream version is already on Docker Hub.\n" + lines = ["| Version | Base image | latest |", "|---|---|---|"] + lines += [f"| {e['version']} | {e['base_image']} | {'yes' if e['latest'] else ''} |" for e in entries] + return "\n".join(lines) + "\n" + + +def main(argv=None): + parser = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter) + parser.add_argument("--upstream", default=UPSTREAM_REPO, help="GitHub repository of pgModeler") + parser.add_argument("--image", default=IMAGE, help="Docker Hub repository") + parser.add_argument("--min-version", default=MIN_VERSION, help="oldest version built automatically") + parser.add_argument("--versions", default="", help="comma or space separated versions to build") + parser.add_argument("--force", action="store_true", help="rebuild versions already on Docker Hub") + parser.add_argument("--validate", action="store_true", help="only the highest version, never as latest") + args = parser.parse_args(argv) + + try: + requested = parse_requested(args.versions) + upstream = fetch_upstream_versions(args.upstream, os.environ.get("GITHUB_TOKEN")) + published = fetch_published_tags(args.image) + entries = select_versions(upstream, published, requested, args.force, args.validate, args.min_version) + except ValueError as error: + parser.error(str(error)) + + text = summary(entries) + print(text, end="") + if os.environ.get("GITHUB_OUTPUT"): + write_github_output(entries, os.environ["GITHUB_OUTPUT"]) + if os.environ.get("GITHUB_STEP_SUMMARY"): + with open(os.environ["GITHUB_STEP_SUMMARY"], "a", encoding="utf-8") as f: + f.write("### pgModeler versions to build\n\n" + text) + return 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/scripts/test_detect_versions.py b/scripts/test_detect_versions.py new file mode 100644 index 0000000..6608619 --- /dev/null +++ b/scripts/test_detect_versions.py @@ -0,0 +1,164 @@ +"""Unit tests for detect_versions.py (no network access).""" +import json +import os +import tempfile +import unittest + +import detect_versions as dv + +# Upstream releases and Docker Hub tags as of 2026-10-04 +UPSTREAM = [ + "2.0.0-beta1", "2.0.0-beta", "2.0.0-alpha1", "1.2.3", "2.0.0-alpha", + "1.2.2", "1.2.1", "1.2.0", "1.2.0-beta1", "1.2.0-beta", "1.1.6", "1.1.5", +] +PUBLISHED = {"latest", "2.0.0-alpha", "1.2.0", "1.1.5", "1.2.0-alpha1", "1.2.0-alpha", "1.1.4"} + + +class ParseVersionTest(unittest.TestCase): + def test_strips_v_prefix(self): + self.assertEqual(dv.parse_version("v2.0.0-beta1"), "2.0.0-beta1") + self.assertEqual(dv.parse_version("1.2.3"), "1.2.3") + self.assertEqual(dv.parse_version("v2.1.0-rc1"), "2.1.0-rc1") + + def test_rejects_unexpected_formats(self): + for tag in ("v1.1.0-alpha1a", "latest", "v2.0", "2.0.0-snapshot", ""): + with self.subTest(tag=tag): + self.assertIsNone(dv.parse_version(tag)) + + +class VersionOrderTest(unittest.TestCase): + def test_prereleases_sort_before_final(self): + expected = ["1.2.0-beta", "1.2.0-beta1", "1.2.0", "2.0.0-alpha", "2.0.0-alpha1", + "2.0.0-beta", "2.0.0-beta1", "2.0.0-rc1", "2.0.0", "2.0.1"] + shuffled = ["2.0.0", "2.0.0-beta1", "1.2.0", "2.0.1", "2.0.0-alpha", "1.2.0-beta1", + "2.0.0-rc1", "2.0.0-beta", "1.2.0-beta", "2.0.0-alpha1"] + self.assertEqual(sorted(shuffled, key=dv.version_key), expected) + + def test_numeric_not_lexicographic(self): + self.assertLess(dv.version_key("1.2.9"), dv.version_key("1.2.10")) + + def test_invalid_version_raises(self): + with self.assertRaises(ValueError): + dv.version_key("latest") + + +class BaseImageTest(unittest.TestCase): + def test_major_versions(self): + self.assertEqual(dv.base_image_for("1.2.3"), "ubuntu:24.04") + self.assertEqual(dv.base_image_for("2.0.0-beta1"), "ubuntu:26.04") + + def test_unknown_major_uses_newest_base(self): + self.assertEqual(dv.base_image_for("3.0.0"), "ubuntu:26.04") + + +class ParseRequestedTest(unittest.TestCase): + def test_commas_spaces_and_v_prefix(self): + self.assertEqual(dv.parse_requested(" v1.2.3, 2.0.0-beta1 2.0.0-beta "), + ["1.2.3", "2.0.0-beta1", "2.0.0-beta"]) + + def test_empty(self): + self.assertEqual(dv.parse_requested(""), []) + self.assertEqual(dv.parse_requested(" "), []) + + def test_invalid_version_fails(self): + with self.assertRaises(ValueError): + dv.parse_requested("1.2.3,latest") + + +class SelectVersionsTest(unittest.TestCase): + def test_missing_versions_from_min_version(self): + entries = dv.select_versions(UPSTREAM, PUBLISHED) + self.assertEqual([e["version"] for e in entries], + ["1.2.1", "1.2.2", "1.2.3", "2.0.0-alpha1", "2.0.0-beta", "2.0.0-beta1"]) + + def test_only_highest_version_is_latest(self): + entries = dv.select_versions(UPSTREAM, PUBLISHED) + self.assertEqual([e["version"] for e in entries if e["latest"]], ["2.0.0-beta1"]) + + def test_entries_carry_base_image(self): + entries = {e["version"]: e for e in dv.select_versions(UPSTREAM, PUBLISHED)} + self.assertEqual(entries["1.2.3"]["base_image"], "ubuntu:24.04") + self.assertEqual(entries["2.0.0-beta1"]["base_image"], "ubuntu:26.04") + + def test_nothing_to_build(self): + self.assertEqual(dv.select_versions(UPSTREAM, set(UPSTREAM)), []) + + def test_no_upstream_versions(self): + self.assertEqual(dv.select_versions([], PUBLISHED), []) + self.assertEqual(dv.select_versions([], PUBLISHED, validate=True), []) + + def test_backfill_does_not_move_latest(self): + entries = dv.select_versions(UPSTREAM, PUBLISHED | {"2.0.0-beta1"}) + self.assertTrue(entries) + self.assertFalse(any(e["latest"] for e in entries)) + + def test_skip_versions(self): + entries = dv.select_versions(UPSTREAM, PUBLISHED, skip={"2.0.0-beta1"}) + self.assertNotIn("2.0.0-beta1", [e["version"] for e in entries]) + self.assertEqual([e["version"] for e in entries if e["latest"]], ["2.0.0-beta"]) + + def test_requested_versions_skip_published_without_force(self): + entries = dv.select_versions(UPSTREAM, PUBLISHED, requested=["1.2.0", "1.2.3"]) + self.assertEqual([e["version"] for e in entries], ["1.2.3"]) + + def test_force_rebuilds_requested_versions(self): + entries = dv.select_versions(UPSTREAM, PUBLISHED, requested=["2.0.0-alpha", "1.2.0"], force=True) + self.assertEqual([e["version"] for e in entries], ["1.2.0", "2.0.0-alpha"]) + self.assertFalse(any(e["latest"] for e in entries)) + + def test_requested_highest_version_is_latest(self): + self.assertEqual(dv.select_versions(UPSTREAM, PUBLISHED, requested=["2.0.0-beta1"]), + [{"version": "2.0.0-beta1", "base_image": "ubuntu:26.04", "latest": True}]) + + def test_requested_unknown_version_fails(self): + with self.assertRaises(ValueError): + dv.select_versions(UPSTREAM, PUBLISHED, requested=["9.9.9"]) + + def test_force_without_requested_versions_fails(self): + with self.assertRaises(ValueError): + dv.select_versions(UPSTREAM, PUBLISHED, force=True) + + def test_validate_builds_highest_without_latest(self): + self.assertEqual(dv.select_versions(UPSTREAM, set(UPSTREAM), validate=True), + [{"version": "2.0.0-beta1", "base_image": "ubuntu:26.04", "latest": False}]) + + +class FetchTest(unittest.TestCase): + def test_upstream_releases_are_paginated_and_filtered(self): + pages = { + 1: [{"tag_name": "v2.0.0-beta1", "draft": False}, + {"tag_name": "v2.0.0-beta2", "draft": True}, + {"tag_name": "v1.1.0-alpha1a", "draft": False}], + 2: [{"tag_name": "v1.2.3", "draft": False}], + 3: [], + } + + def fake_get(url, token=None): + return pages[int(url.rsplit("page=", 1)[1])] + + self.assertEqual(dv.fetch_upstream_versions("owner/repo", get=fake_get), ["2.0.0-beta1", "1.2.3"]) + + def test_docker_hub_tags_follow_next_links(self): + first = "https://hub.docker.com/v2/repositories/user/image/tags?page_size=100" + responses = { + first: {"results": [{"name": "latest"}, {"name": "1.2.0"}], "next": "https://hub.example/page2"}, + "https://hub.example/page2": {"results": [{"name": "1.1.5"}], "next": None}, + } + tags = dv.fetch_published_tags("user/image", get=lambda url, token=None: responses[url]) + self.assertEqual(tags, {"latest", "1.2.0", "1.1.5"}) + + +class OutputTest(unittest.TestCase): + def test_github_output_contains_matrix_and_count(self): + entries = [{"version": "1.2.3", "base_image": "ubuntu:24.04", "latest": False}] + with tempfile.TemporaryDirectory() as tmp: + path = os.path.join(tmp, "github_output") + dv.write_github_output(entries, path) + with open(path, encoding="utf-8") as f: + lines = f.read().splitlines() + self.assertEqual(json.loads(lines[0].split("=", 1)[1]), {"include": entries}) + self.assertEqual(lines[1], "count=1") + + +if __name__ == "__main__": + unittest.main() From 3e8c9376997eab905a77f0055fd3800a12cdbb6d Mon Sep 17 00:00:00 2001 From: Abel Paz Date: Mon, 5 Oct 2026 12:31:11 +0200 Subject: [PATCH 2/7] Rewrote Dockerfile as a multi-stage build --- .dockerignore | 1 + .gitattributes | 2 + Dockerfile | 143 +++++++++++++++++++++++++++--------------- scripts/smoke-test.sh | 98 +++++++++++++++++++++++++++++ 4 files changed, 195 insertions(+), 49 deletions(-) create mode 100644 .dockerignore create mode 100755 scripts/smoke-test.sh diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..72e8ffc --- /dev/null +++ b/.dockerignore @@ -0,0 +1 @@ +* diff --git a/.gitattributes b/.gitattributes index 9b4b230..c4d06d4 100644 --- a/.gitattributes +++ b/.gitattributes @@ -2,3 +2,5 @@ # Fix shell scripts with lines ending with "CRLF" on Windows * text=auto *.sh text eol=lf +# Dockerfile heredocs break with CRLF +Dockerfile text eol=lf diff --git a/Dockerfile b/Dockerfile index 222e38c..f57f176 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,49 +1,94 @@ -FROM ubuntu:25.10 -LABEL Author="@apazga" - -# pgModeler version to use (can be overridden with --build-arg) -# Note: 2.0.0-alpha requires Qt 6.6+ which is available in Ubuntu 25.10 -ARG PG_VERSION=2.0.0-alpha - -ADD https://codeload.github.com/pgmodeler/pgmodeler/tar.gz/v${PG_VERSION} /usr/local/src/ -WORKDIR /usr/local/src/ - -# Add universe repository -RUN apt-get update && apt-get install -y software-properties-common \ - && add-apt-repository universe - -# Install dependencies -# Note: pgModeler 2.0.0-alpha requires Qt 6.6+ which is available in Ubuntu 25.10 -RUN BUILD_PKGS="qmake6 build-essential libxml2-dev libpq-dev pkg-config cmake clang" \ - && RUNTIME_PKGS="qtdeclarative5-dev libqt6core6 libqt6svg6 qttools5-dev qttools5-dev-tools postgresql-server-dev-all qt6-base-dev libqt6svg6-dev" \ - && SECURITY_PKGS="ca-certificates" \ - && DEBIAN_FRONTEND=noninteractive \ - && apt-get update \ - && apt-get -y install --no-install-recommends ${BUILD_PKGS} ${RUNTIME_PKGS} ${SECURITY_PKGS} - -ENV PATH="/usr/lib/qt6/bin:$PATH" - -# Compile pgmodeler -# Version 2.x uses CMake build system, 1.x uses qmake -RUN tar xvzf v${PG_VERSION} \ - && cd pgmodeler-${PG_VERSION} \ - && MAJOR_VERSION=$(echo ${PG_VERSION} | cut -d. -f1) \ - && if [ "$MAJOR_VERSION" = "2" ]; then \ - echo "Building pgModeler 2.x with CMake..."; \ - cmake -S . -B ./cmake-build --fresh; \ - cmake --build ./cmake-build --target all; \ - cmake --install ./cmake-build; \ - else \ - echo "Building pgModeler 1.x with qmake..."; \ - qmake pgmodeler.pro; \ - make && make install; \ - fi \ - && mkdir -p /usr/local/lib/pgmodeler/plugins \ - && chmod 777 /usr/local/lib/pgmodeler/plugins - -# Clean unnecessary packages -RUN apt-get remove --purge -y $BUILD_PKGS \ - && rm -rf /var/lib/apt/lists/* - -# Run pgModeler -ENTRYPOINT ["/usr/local/bin/pgmodeler"] \ No newline at end of file +# syntax=docker/dockerfile:1 + +# Ubuntu release used to build and run pgModeler: 2.x needs Qt >= 6.6 (ubuntu:26.04), +# 1.x builds on ubuntu:24.04 (Qt 6.4). Override with --build-arg BASE_IMAGE=... +ARG BASE_IMAGE=ubuntu:26.04 + +FROM ${BASE_IMAGE} AS build + +# pgModeler version to build (required), e.g. --build-arg PG_VERSION=2.0.0-beta1 +ARG PG_VERSION +ARG DEBIAN_FRONTEND=noninteractive + +RUN test -n "$PG_VERSION" \ + || { echo "PG_VERSION is required, e.g. --build-arg PG_VERSION=2.0.0-beta1" >&2; exit 1; } + +# libxext-dev is needed by qmake builds (1.x), Qt's mkspec links against -lXext +RUN apt-get update \ + && apt-get install -y --no-install-recommends \ + build-essential ca-certificates clang cmake curl libpq-dev libxext-dev libxml2-dev \ + pkg-config qmake6 qt6-base-dev qt6-svg-dev \ + && rm -rf /var/lib/apt/lists/* + +WORKDIR /usr/local/src/pgmodeler +RUN curl -fsSL "https://codeload.github.com/nullptrlabs/pgmodeler/tar.gz/v${PG_VERSION}" \ + | tar xz --strip-components=1 + +# Compile pgModeler (2.x uses CMake, 1.x uses qmake) and install it under /out +RUN <<'EOF' +set -eu +if [ "${PG_VERSION%%.*}" -ge 2 ]; then + echo "Building pgModeler ${PG_VERSION} with CMake..." + cmake -S . -B build -DCMAKE_BUILD_TYPE=Release + cmake --build build --parallel "$(nproc)" + DESTDIR=/out cmake --install build +else + echo "Building pgModeler ${PG_VERSION} with qmake..." + qmake6 pgmodeler.pro + make -j"$(nproc)" + make install INSTALL_ROOT=/out +fi +EOF + +# Find the Ubuntu packages that provide the shared libraries pgModeler links against +RUN <<'EOF' +set -eu +find /out -type f \( -perm -u+x -o -name '*.so*' \) -exec ldd {} + 2>/dev/null \ + | awk '$2 == "=>" && $3 ~ /^\// { print $3 }' | sort -u > /tmp/libs.txt +: > /tmp/packages.txt +while read -r lib; do + case "$lib" in /out/*) continue ;; esac + owner= + for path in "$lib" "$(readlink -f "$lib")" "/usr$lib"; do + if owner=$(dpkg -S "$path" 2>/dev/null); then + break + fi + done + if [ -n "$owner" ]; then + echo "${owner%%:*}" >> /tmp/packages.txt + else + echo "warning: no package provides $lib" >&2 + fi +done < /tmp/libs.txt +sort -u /tmp/packages.txt > /runtime-packages.txt +cat /runtime-packages.txt +EOF + + +FROM ${BASE_IMAGE} + +ARG DEBIAN_FRONTEND=noninteractive +LABEL org.opencontainers.image.authors="@apazga" +# Qt expects a UTF-8 locale and warns on every start without it +ENV LANG=C.UTF-8 + +COPY --from=build /runtime-packages.txt /tmp/runtime-packages.txt + +# Libraries found above plus what Qt loads at runtime: X11 platform plugin, SVG plugins and fonts +RUN <<'EOF' +set -eu +apt-get update +extra="ca-certificates fontconfig fonts-dejavu-core qt6-qpa-plugins" +# Ubuntu 26.04 ships the SVG plugins in their own package, 24.04 inside libqt6svg6 +if apt-cache show qt6-svg-plugins >/dev/null 2>&1; then + extra="$extra qt6-svg-plugins" +fi +xargs -a /tmp/runtime-packages.txt apt-get install -y --no-install-recommends $extra +rm -rf /var/lib/apt/lists/* /tmp/runtime-packages.txt +EOF + +COPY --from=build /out/usr/local/ /usr/local/ +RUN mkdir -p /usr/local/lib/pgmodeler/plugins + +# Run pgModeler +ENTRYPOINT ["/usr/local/bin/pgmodeler"] diff --git a/scripts/smoke-test.sh b/scripts/smoke-test.sh new file mode 100755 index 0000000..d53d2df --- /dev/null +++ b/scripts/smoke-test.sh @@ -0,0 +1,98 @@ +#!/usr/bin/env bash +# Smoke test for a pgModeler image, run before publishing it: +# 1. every shared library of pgModeler resolves (ldd) +# 2. pgmodeler-cli starts and prints its version +# 3. the GUI starts on an X server (Xvfb in a helper container) and keeps running +# +# Usage: scripts/smoke-test.sh [expected-version] +set -euo pipefail + +# Git Bash on Windows would rewrite the container paths below into Windows paths +export MSYS_NO_PATHCONV=1 + +IMAGE=${1:?usage: smoke-test.sh [expected-version]} +EXPECTED_VERSION=${2:-} +XVFB_IMAGE=${XVFB_IMAGE:-ubuntu:24.04} +GUI_WAIT=${GUI_WAIT:-15} + +suffix="$$-$RANDOM" +network="pgm-smoke-$suffix" +xvfb="pgm-xvfb-$suffix" +app="pgm-app-$suffix" + +cleanup() { + docker rm -f "$app" "$xvfb" >/dev/null 2>&1 || true + docker network rm "$network" >/dev/null 2>&1 || true +} +trap cleanup EXIT + +fail() { + echo "FAIL: $*" >&2 + exit 1 +} + +echo "==> [1/3] Shared libraries" +missing=$(docker run --rm --entrypoint sh "$IMAGE" -c ' + test -x /usr/local/bin/pgmodeler || echo "/usr/local/bin/pgmodeler is missing" + find /usr/local/bin /usr/local/lib/pgmodeler -type f \( -name "pgmodeler*" -o -name "*.so*" \) \ + -exec ldd {} + 2>/dev/null | grep "not found" || true +') +if [ -n "$missing" ]; then + echo "$missing" >&2 + fail "unresolved libraries in $IMAGE" +fi + +echo "==> [2/3] pgmodeler-cli" +# 2.x prints its version with --help, 1.x only when it runs an operation like --list-plugins +version_line= +for option in --help --list-plugins; do + if ! cli_output=$(docker run --rm --entrypoint pgmodeler-cli "$IMAGE" "$option" 2>&1); then + echo "$cli_output" >&2 + fail "pgmodeler-cli $option exited with an error" + fi + version_line=$(grep -m1 -E "Version [0-9]" <<<"$cli_output" || true) + if [ -n "$version_line" ]; then + break + fi +done +if [ -z "$version_line" ]; then + echo "$cli_output" >&2 + fail "pgmodeler-cli did not print its version" +fi +echo "$version_line" +if [ -n "$EXPECTED_VERSION" ] && [[ "$version_line" != *"Version $EXPECTED_VERSION "* ]]; then + echo "::warning::$IMAGE reports '$version_line', expected version $EXPECTED_VERSION" +fi + +echo "==> [3/3] GUI start-up under Xvfb" +docker network create "$network" >/dev/null +docker run -d --name "$xvfb" --network "$network" "$XVFB_IMAGE" sh -c ' + apt-get update -qq && + DEBIAN_FRONTEND=noninteractive apt-get install -y -qq --no-install-recommends xvfb >/dev/null && + exec Xvfb :99 -screen 0 1280x1024x24 -listen tcp -ac' >/dev/null +for _ in $(seq 1 90); do + if docker exec "$xvfb" test -e /tmp/.X11-unix/X99 2>/dev/null; then + break + fi + sleep 2 +done +if ! docker exec "$xvfb" test -e /tmp/.X11-unix/X99 2>/dev/null; then + docker logs "$xvfb" >&2 + fail "Xvfb did not start" +fi + +docker run -d --name "$app" --network "$network" -e DISPLAY="$xvfb:99" "$IMAGE" >/dev/null +sleep "$GUI_WAIT" +app_logs=$(docker logs "$app" 2>&1) +if [ "$(docker inspect -f '{{.State.Running}}' "$app")" != true ]; then + echo "$app_logs" >&2 + fail "pgmodeler exited during start-up" +fi +if grep -qiE "could not (load|connect)|qt\.qpa\.plugin|segmentation fault" <<<"$app_logs"; then + echo "$app_logs" >&2 + fail "Qt reported a display or plugin problem" +fi +if [ -n "$app_logs" ]; then + echo "$app_logs" +fi +echo "OK: $IMAGE passed the smoke test" From df98a1d90773a48a2dd745d974a6345b35391f6e Mon Sep 17 00:00:00 2001 From: Abel Paz Date: Mon, 5 Oct 2026 12:32:21 +0200 Subject: [PATCH 3/7] Added workflows to build and publish images --- .github/workflows/build-image.yml | 160 ++++++++++++++++++++++++++++++ .github/workflows/release.yml | 88 ++++++++++++++++ .gitignore | 3 +- 3 files changed, 250 insertions(+), 1 deletion(-) create mode 100644 .github/workflows/build-image.yml create mode 100644 .github/workflows/release.yml diff --git a/.github/workflows/build-image.yml b/.github/workflows/build-image.yml new file mode 100644 index 0000000..87706e1 --- /dev/null +++ b/.github/workflows/build-image.yml @@ -0,0 +1,160 @@ +name: build-image + +on: + workflow_call: + inputs: + version: + description: pgModeler version, e.g. 2.0.0-beta1 + type: string + required: true + base_image: + description: Ubuntu image used to build and run pgModeler + type: string + required: true + latest: + description: Also tag the image as latest + type: boolean + default: false + push: + description: Publish the image and the git tag (false = build and test only) + type: boolean + default: false + +permissions: + contents: read + +env: + IMAGE: apazga/docker-pgmodeler + +jobs: + build: + name: build (${{ matrix.arch }}) + runs-on: ${{ matrix.runner }} + timeout-minutes: 180 + strategy: + fail-fast: true + matrix: + include: + - arch: amd64 + platform: linux/amd64 + runner: ubuntu-24.04 + - arch: arm64 + platform: linux/arm64 + runner: ubuntu-24.04-arm + steps: + - uses: actions/checkout@v7 + + - uses: docker/setup-buildx-action@v4 + + - name: Log in to Docker Hub + if: inputs.push + uses: docker/login-action@v4 + with: + username: ${{ vars.DOCKERHUB_USERNAME }} + password: ${{ secrets.DOCKERHUB_TOKEN }} + + - name: Image labels + id: meta + uses: docker/metadata-action@v6 + with: + images: ${{ env.IMAGE }} + labels: | + org.opencontainers.image.title=pgModeler + org.opencontainers.image.description=pgModeler ${{ inputs.version }} running in a Docker container + org.opencontainers.image.version=${{ inputs.version }} + + - name: Build + uses: docker/build-push-action@v7 + with: + context: . + platforms: ${{ matrix.platform }} + build-args: | + PG_VERSION=${{ inputs.version }} + BASE_IMAGE=${{ inputs.base_image }} + labels: ${{ steps.meta.outputs.labels }} + tags: pgmodeler-smoke:${{ inputs.version }} + load: true + + - name: Smoke test + env: + VERSION: ${{ inputs.version }} + run: bash scripts/smoke-test.sh "pgmodeler-smoke:$VERSION" "$VERSION" + + - name: Push by digest + id: push + if: inputs.push + uses: docker/build-push-action@v7 + with: + context: . + platforms: ${{ matrix.platform }} + build-args: | + PG_VERSION=${{ inputs.version }} + BASE_IMAGE=${{ inputs.base_image }} + labels: ${{ steps.meta.outputs.labels }} + outputs: type=image,name=${{ env.IMAGE }},push-by-digest=true,name-canonical=true,push=true + + - name: Export digest + if: inputs.push + env: + DIGEST: ${{ steps.push.outputs.digest }} + run: | + mkdir -p "$RUNNER_TEMP/digests" + touch "$RUNNER_TEMP/digests/${DIGEST#sha256:}" + + - name: Upload digest + if: inputs.push + uses: actions/upload-artifact@v7 + with: + name: digests-${{ inputs.version }}-${{ matrix.arch }} + path: ${{ runner.temp }}/digests/* + if-no-files-found: error + retention-days: 1 + + merge: + if: inputs.push + needs: build + runs-on: ubuntu-24.04 + permissions: + contents: write + steps: + - uses: actions/checkout@v7 + + - name: Download digests + uses: actions/download-artifact@v8 + with: + pattern: digests-${{ inputs.version }}-* + path: ${{ runner.temp }}/digests + merge-multiple: true + + - uses: docker/setup-buildx-action@v4 + + - name: Log in to Docker Hub + uses: docker/login-action@v4 + with: + username: ${{ vars.DOCKERHUB_USERNAME }} + password: ${{ secrets.DOCKERHUB_TOKEN }} + + - name: Publish multi-arch image + working-directory: ${{ runner.temp }}/digests + env: + VERSION: ${{ inputs.version }} + LATEST: ${{ inputs.latest }} + run: | + digests=(*) + if [ "${#digests[@]}" -ne 2 ]; then + echo "::error::expected 2 digests (amd64 + arm64), found ${#digests[@]}" + exit 1 + fi + tags=(-t "$IMAGE:$VERSION") + if [ "$LATEST" = true ]; then + tags+=(-t "$IMAGE:latest") + fi + docker buildx imagetools create "${tags[@]}" "${digests[@]/#/$IMAGE@sha256:}" + docker buildx imagetools inspect "$IMAGE:$VERSION" + + - name: Tag repository + env: + VERSION: ${{ inputs.version }} + run: | + git tag -f "$VERSION" "$GITHUB_SHA" + git push -f origin "refs/tags/$VERSION" diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..bd6f9bb --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,88 @@ +name: release + +on: + schedule: + - cron: "0 4 * * 1" # Mondays at 04:00 UTC + workflow_dispatch: + inputs: + versions: + description: Versions to build, comma separated (empty = every version missing on Docker Hub) + type: string + default: "" + force: + description: Rebuild the given versions even if they are already on Docker Hub + type: boolean + default: false + dry_run: + description: Only list the versions that would be built + type: boolean + default: false + pull_request: + paths: + - Dockerfile + - .dockerignore + - scripts/** + - .github/workflows/** + push: + branches: [master] + paths: + - Dockerfile + - .dockerignore + - scripts/** + - .github/workflows/** + +permissions: + contents: read + +concurrency: + group: ${{ github.event_name == 'pull_request' && format('validate-{0}', github.ref) || 'release' }} + cancel-in-progress: ${{ github.event_name == 'pull_request' }} + +jobs: + detect: + runs-on: ubuntu-24.04 + outputs: + matrix: ${{ steps.detect.outputs.matrix }} + count: ${{ steps.detect.outputs.count }} + steps: + - uses: actions/checkout@v7 + + - name: Test version detection + run: python3 -m unittest discover -s scripts -v + + - name: Detect versions to build + id: detect + env: + GITHUB_TOKEN: ${{ github.token }} + EVENT: ${{ github.event_name }} + VERSIONS: ${{ inputs.versions }} + FORCE: ${{ inputs.force }} + run: | + args=() + if [ "$EVENT" = pull_request ] || [ "$EVENT" = push ]; then + args+=(--validate) + fi + if [ -n "$VERSIONS" ]; then + args+=(--versions "$VERSIONS") + fi + if [ "$FORCE" = true ]; then + args+=(--force) + fi + python3 scripts/detect_versions.py "${args[@]}" + + build: + name: ${{ matrix.version }} + needs: detect + if: needs.detect.outputs.count != '0' && !inputs.dry_run + strategy: + fail-fast: false + matrix: ${{ fromJSON(needs.detect.outputs.matrix) }} + permissions: + contents: write + uses: ./.github/workflows/build-image.yml + with: + version: ${{ matrix.version }} + base_image: ${{ matrix.base_image }} + latest: ${{ matrix.latest }} + push: ${{ github.event_name == 'schedule' || github.event_name == 'workflow_dispatch' }} + secrets: inherit diff --git a/.gitignore b/.gitignore index e46db4f..9324266 100644 --- a/.gitignore +++ b/.gitignore @@ -2,4 +2,5 @@ .env.linux .env.macos .env.windows.ps1 -.run.ps1 \ No newline at end of file +.run.ps1 +__pycache__/ From bca29a9807243c8f3a2194c233f163db1bebb3a6 Mon Sep 17 00:00:00 2001 From: Abel Paz Date: Mon, 5 Oct 2026 12:33:00 +0200 Subject: [PATCH 4/7] Fixed plugins path and defaults in launch scripts --- .env.linux.example | 6 ++++-- .env.macos.example | 6 ++++-- .env.windows.ps1.example | 7 +++++++ run.ps1 | 28 +++++++++++++++++++--------- run.sh | 26 ++++++++++++++++++-------- run_macos.sh | 28 +++++++++++++++++++--------- run_macos_local.sh | 37 ++++++++++++++++++++----------------- 7 files changed, 91 insertions(+), 47 deletions(-) create mode 100644 .env.windows.ps1.example mode change 100644 => 100755 run.sh mode change 100644 => 100755 run_macos_local.sh diff --git a/.env.linux.example b/.env.linux.example index f3d09f6..3ed5f1b 100644 --- a/.env.linux.example +++ b/.env.linux.example @@ -1,5 +1,7 @@ # Make a copy of this file and name it ".env.linux" # Used for Linux environments. HOST_IP=192.168.1.100 -PGMODELER_IMAGE=apazga/docker-pgmodeler:2.0.0-alpha -PROJECT_ROOT=/home/toto/git/db-pgmodeler +# Use a fixed version instead of the latest one, e.g. apazga/docker-pgmodeler:1.2.3 +PGMODELER_IMAGE=apazga/docker-pgmodeler:latest +# Folder that contains the "data" directory (defaults to the folder of run.sh) +#PROJECT_ROOT=/home/toto/docker-pgmodeler diff --git a/.env.macos.example b/.env.macos.example index 54b19a8..ed446d2 100644 --- a/.env.macos.example +++ b/.env.macos.example @@ -1,5 +1,7 @@ # Make a copy of this file and name it ".env.macos" # Used for MacOS environments. HOST_IP=$(ifconfig en0 | grep "inet " | cut -d ' ' -f 2) -PGMODELER_IMAGE=apazga/docker-pgmodeler:2.0.0-alpha -PROJECT_ROOT=/Users/toto/docker-pgmodeler +# Use a fixed version instead of the latest one, e.g. apazga/docker-pgmodeler:1.2.3 +PGMODELER_IMAGE=apazga/docker-pgmodeler:latest +# Folder that contains the "data" directory (defaults to the folder of the run script) +#PROJECT_ROOT=/Users/toto/docker-pgmodeler diff --git a/.env.windows.ps1.example b/.env.windows.ps1.example new file mode 100644 index 0000000..e4a168c --- /dev/null +++ b/.env.windows.ps1.example @@ -0,0 +1,7 @@ +# Make a copy of this file and name it ".env.windows.ps1" +# Used by run.ps1 on Windows. Uncomment the values you want to change. +# $DISPLAY = '192.168.1.100:0.0' +# Use a fixed version instead of the latest one +# $PGMODELER_IMAGE = 'apazga/docker-pgmodeler:1.2.3' +# Folder that contains the "data" directory (defaults to the folder of run.ps1) +# $PROJECT_ROOT = 'D:\pgmodeler' diff --git a/run.ps1 b/run.ps1 index 38a65f2..5a89124 100644 --- a/run.ps1 +++ b/run.ps1 @@ -1,14 +1,24 @@ -<# - Script to launch pgModeler - - Mounts data folder in current directory to save pgModeler projects, config and plugins +<# + Script to launch pgModeler on Windows + - Mounts the data folder next to this script to save pgModeler projects, config and plugins #> -# These settings are default one and should not be modified here, because a "git pull" would override your changes. -# Instead, define your variables in a separate .env file. -Set-Variable -name DISPLAY -value 'host.docker.internal:0.0' -Set-Variable -name PGMODELER_IMAGE -value 'apazga/docker-pgmodeler:2.0.0-alpha' -Set-Variable -name PROJECT_ROOT -value $PSScriptRoot +# These settings are the default ones and should not be modified here, because a "git pull" would override your changes. +# Instead, define your variables in a .env.windows.ps1 file (see .env.windows.ps1.example). +$DISPLAY = 'host.docker.internal:0.0' +$PGMODELER_IMAGE = 'apazga/docker-pgmodeler:latest' +$PROJECT_ROOT = $PSScriptRoot +# Override the variables with those from the .env.windows.ps1 file +$EnvFile = Join-Path $PSScriptRoot '.env.windows.ps1' +if (Test-Path $EnvFile) { . $EnvFile } -docker run --rm -ti -e DISPLAY=$DISPLAY -v $PROJECT_ROOT/data/root:/root -v $PROJECT_ROOT/data/usr/local/lib/docker-pgmodeler/plugins:/usr/local/lib/docker-pgmodeler/plugins $PGMODELER_IMAGE +$DockerArgs = @( + 'run', '--rm', '-ti', + '-e', "DISPLAY=$DISPLAY", + '-v', "$PROJECT_ROOT/data/root:/root", + '-v', "$PROJECT_ROOT/data/usr/local/lib/pgmodeler/plugins:/usr/local/lib/pgmodeler/plugins", + $PGMODELER_IMAGE +) +docker @DockerArgs diff --git a/run.sh b/run.sh old mode 100644 new mode 100755 index 191d30f..9908188 --- a/run.sh +++ b/run.sh @@ -1,14 +1,24 @@ +#!/usr/bin/env bash # Script to launch pgModeler on Linux -# - Use custom IP (192.168.1.100) for DISPLAY environment variable -# - Mounts data folder in current directory to save pgModeler projects, config and plugins +# - Uses HOST_IP for the DISPLAY environment variable +# - Mounts the data folder next to this script to keep pgModeler projects, config and plugins + +# These settings are the default ones and should not be modified here, because a "git pull" would override your changes. +# Instead, define your variables in a .env.linux file (see .env.linux.example). +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" -# These settings are default one and should not be modified here, because a "git pull" would override your changes. -# Instead, define your variables in a separate .env file. HOST_IP=192.168.1.100 -PGMODELER_IMAGE=apazga/docker-pgmodeler:2.0.0-alpha -PROJECT_ROOT=/home/toto/docker-pgmodeler +PGMODELER_IMAGE=apazga/docker-pgmodeler:latest +PROJECT_ROOT="$SCRIPT_DIR" # Override environment variables with those from the .env file -source .env.linux +if [ -f "$SCRIPT_DIR/.env.linux" ]; then + # shellcheck source=/dev/null + . "$SCRIPT_DIR/.env.linux" +fi -docker run --rm -ti -e DISPLAY=$HOST_IP:0.0 -v $PROJECT_ROOT/data/root:/root -v $PROJECT_ROOT/data/usr/local/lib/docker-pgmodeler/plugins:/usr/local/lib/docker-pgmodeler/plugins $PGMODELER_IMAGE \ No newline at end of file +docker run --rm -ti \ + -e DISPLAY="$HOST_IP:0.0" \ + -v "$PROJECT_ROOT/data/root:/root" \ + -v "$PROJECT_ROOT/data/usr/local/lib/pgmodeler/plugins:/usr/local/lib/pgmodeler/plugins" \ + "$PGMODELER_IMAGE" diff --git a/run_macos.sh b/run_macos.sh index a025182..dbf22fa 100755 --- a/run_macos.sh +++ b/run_macos.sh @@ -1,18 +1,28 @@ +#!/usr/bin/env bash # Script to launch pgModeler on macOS -# - Use host IP for DISPLAY environment variable -# - Mounts data folder in current directory to save pgModeler projects, config and plugins +# - Uses the host IP for the DISPLAY environment variable +# - Mounts the data folder next to this script to keep pgModeler projects, config and plugins + +# These settings are the default ones and should not be modified here, because a "git pull" would override your changes. +# Instead, define your variables in a .env.macos file (see .env.macos.example). +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" -# These settings are default one and should not be modified here, because a "git pull" would override your changes. -# Instead, define your variables in a separate .env file. HOST_IP=$(ifconfig en0 | grep "inet " | cut -d ' ' -f 2) -PGMODELER_IMAGE=apazga/docker-pgmodeler:2.0.0-alpha -PROJECT_ROOT=/Users/abelpaz/Documents/_repos/docker-pgmodeler +PGMODELER_IMAGE=apazga/docker-pgmodeler:latest +PROJECT_ROOT="$SCRIPT_DIR" # Override environment variables with those from the .env file -. .env.macos +if [ -f "$SCRIPT_DIR/.env.macos" ]; then + # shellcheck source=/dev/null + . "$SCRIPT_DIR/.env.macos" +fi # Add local machine to authorized host by XQuartz xhost + -echo HOST_IP: $HOST_IP -docker run --rm -e DISPLAY=$HOST_IP:0 -v $PROJECT_ROOT/data/root:/root -v $PROJECT_ROOT/data/usr/local/lib/docker-pgmodeler/plugins:/usr/local/lib/docker-pgmodeler/plugins $PGMODELER_IMAGE +echo "HOST_IP: $HOST_IP" +docker run --rm \ + -e DISPLAY="$HOST_IP:0" \ + -v "$PROJECT_ROOT/data/root:/root" \ + -v "$PROJECT_ROOT/data/usr/local/lib/pgmodeler/plugins:/usr/local/lib/pgmodeler/plugins" \ + "$PGMODELER_IMAGE" diff --git a/run_macos_local.sh b/run_macos_local.sh old mode 100644 new mode 100755 index dd96289..dd85514 --- a/run_macos_local.sh +++ b/run_macos_local.sh @@ -1,25 +1,28 @@ -#!/bin/bash -# Script to launch pgModeler on macOS -# - Use host.docker.internal for DISPLAY -# - Mounts data folder in current directory to save pgModeler projects, config, and plugins +#!/usr/bin/env bash +# Script to launch pgModeler on macOS without a network connection +# - Uses host.docker.internal for DISPLAY +# - Mounts the data folder next to this script to keep pgModeler projects, config and plugins -# These settings are default and should not be modified here, because a "git pull" would override your changes. -# Instead, define your variables in a separate .env file. -PGMODELER_IMAGE=apazga/docker-pgmodeler:2.0.0-alpha -PROJECT_ROOT=/Users/abelpaz/Documents/_repos/docker-pgmodeler +# These settings are the default ones and should not be modified here, because a "git pull" would override your changes. +# Instead, define your variables in a .env.macos file (see .env.macos.example). +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" + +LOCAL_DISPLAY=host.docker.internal:0 +PGMODELER_IMAGE=apazga/docker-pgmodeler:latest +PROJECT_ROOT="$SCRIPT_DIR" # Override environment variables with those from the .env file -. .env.macos +if [ -f "$SCRIPT_DIR/.env.macos" ]; then + # shellcheck source=/dev/null + . "$SCRIPT_DIR/.env.macos" +fi # Add local machine to authorized host by XQuartz (allow 127.0.0.1) xhost + 127.0.0.1 -# Use host.docker.internal for DISPLAY -DISPLAY=host.docker.internal:0 - -echo "DISPLAY: $DISPLAY" +echo "DISPLAY: $LOCAL_DISPLAY" docker run --rm \ - -e DISPLAY=$DISPLAY \ - -v $PROJECT_ROOT/data/root:/root \ - -v $PROJECT_ROOT/data/usr/local/lib/docker-pgmodeler/plugins:/usr/local/lib/docker-pgmodeler/plugins \ - $PGMODELER_IMAGE \ No newline at end of file + -e DISPLAY="$LOCAL_DISPLAY" \ + -v "$PROJECT_ROOT/data/root:/root" \ + -v "$PROJECT_ROOT/data/usr/local/lib/pgmodeler/plugins:/usr/local/lib/pgmodeler/plugins" \ + "$PGMODELER_IMAGE" From 62be25211928bfc943e5b710b1716bd6dcde5e01 Mon Sep 17 00:00:00 2001 From: Abel Paz Date: Mon, 5 Oct 2026 12:34:01 +0200 Subject: [PATCH 5/7] Updated README.md --- README.md | 82 ++++++++++++++++++------------------------------------- 1 file changed, 26 insertions(+), 56 deletions(-) diff --git a/README.md b/README.md index ba189a2..011778d 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,10 @@ # pgModeler Docker container -![Docker Pulls](https://img.shields.io/docker/pulls/apazga/docker-pgmodeler) ![GitHub Repo stars](https://img.shields.io/github/stars/apazga/docker-pgmodeler) +![Docker Pulls](https://img.shields.io/docker/pulls/apazga/docker-pgmodeler) ![GitHub Repo stars](https://img.shields.io/github/stars/apazga/docker-pgmodeler) [![release](https://github.com/apazga/docker-pgmodeler/actions/workflows/release.yml/badge.svg)](https://github.com/apazga/docker-pgmodeler/actions/workflows/release.yml) -This image compiles & run pgModeler inside a Docker container. +This image compiles & run [pgModeler](https://github.com/nullptrlabs/pgmodeler) inside a Docker container. + +New pgModeler releases are built and published automatically every week, for `linux/amd64` and `linux/arm64` (Apple Silicon included). ## Usage @@ -28,22 +30,22 @@ This image compiles & run pgModeler inside a Docker container. Use it with volumes if needed (e.g. to save!): - ```docker run -ti -e DISPLAY=$DISPLAY -v F:\data\root:/root apazga/docker-pgmodeler:2.0.0-alpha``` + ```docker run -ti -e DISPLAY=$DISPLAY -v F:\data\root:/root apazga/docker-pgmodeler``` You can also specify your DISPLAY IP directly if you don't want to define an environment variable: - ```docker run -ti -e DISPLAY=192.168.1.100:0.0 -v F:\data\root:/root apazga/docker-pgmodeler:2.0.0-alpha``` + ```docker run -ti -e DISPLAY=192.168.1.100:0.0 -v F:\data\root:/root apazga/docker-pgmodeler``` #### Windows (PowerShell script) -To ease launch of pgmodeler, make a copy of the `run.ps1.example` and name it `run.ps1`. Feel free to modify the environment variables inside the file. Making a copy will avoid losing your settings when you will update the repository. +To ease launch of pgModeler, use the provided `run.ps1` script. It uses `host.docker.internal:0.0` as DISPLAY and saves your projects, config and plugins in the `data` folder next to the script. -### Linux +To change these settings, make a copy of `.env.windows.ps1.example`, name it `.env.windows.ps1` and set your values there. Making a copy will avoid losing your settings when you update the repository. +### Linux Make a copy of the `.env.linux.example` file and name it `.env.linux`. Feel free to modify the environment variables. - Then use the provided script `run.sh`. ### MacOS @@ -53,7 +55,7 @@ Check first [this Medium post](https://yuryalencar.medium.com/pgmodeler-docker-1 On MacOS (following instructions tested on MacOS Big Sure), if you wish to run this image, you need to install XQuartz. With brew installed, do this: ```brew install --cask xquartz``` - + Then open XQuartz and allow connections: ```xquartz > preferences > security > [mark to allow connections from network clients]``` @@ -74,60 +76,28 @@ For using it without a network connection, you can also use the script `run_maco I may have forgotten some steps, if any problem please open an issue. +## Tags + +- `latest`: the highest pgModeler version available, **including alpha and beta releases**. +- `X.Y.Z`, `X.Y.Z-alphaN`, `X.Y.Z-betaN`: a specific pgModeler version. + +All the available tags are listed on [Docker Hub](https://hub.docker.com/r/apazga/docker-pgmodeler/tags). New images support `linux/amd64` and `linux/arm64`; some older tags are `linux/amd64` only. + +The scripts use `latest` by default. To use a specific version set `PGMODELER_IMAGE` in your `.env` file, e.g. `PGMODELER_IMAGE=apazga/docker-pgmodeler:1.2.3`. + ## Build image -If you want to build the image using the Dockerfile provided (it can take a while!) +If you want to build the image using the Dockerfile provided (it can take a while!), pass the pgModeler version to build: -```docker build -t apazga/docker-pgmodeler .``` +```docker build --build-arg PG_VERSION=2.0.0-beta1 -t apazga/docker-pgmodeler:2.0.0-beta1 .``` -To build a specific version, use the `--build-arg` parameter: +pgModeler 2.x requires Qt 6.6+ and builds on Ubuntu 26.04, the default base image. For 1.x versions use Ubuntu 24.04: -```docker build --build-arg PG_VERSION=2.0.0-alpha -t apazga/docker-pgmodeler:2.0.0-alpha .``` +```docker build --build-arg PG_VERSION=1.2.3 --build-arg BASE_IMAGE=ubuntu:24.04 -t apazga/docker-pgmodeler:1.2.3 .``` **Important:** The Dockerfile automatically detects whether you're building version 1.x or 2.x and uses the appropriate build system (qmake for 1.x, CMake for 2.x). -## Tags - -- 0.9.1 -- 0.9.2-alpha -- 0.9.2-alpha1 -- 0.9.2-beta -- 0.9.2 -- 0.9.3-beta1 -- 0.9.3 -- 0.9.4-alpha -- 0.9.4-beta -- 0.9.4-beta1 -- 0.9.4 -- 1.0.0-alpha -- 1.0.0-alpha1 -- 1.0.0-beta -- 1.0.0-beta1 -- 1.0.0 -- 1.0.1 -- 1.0.2 -- 1.0.3 -- 1.0.4 -- 1.1.0-alpha -- 1.0.5 -- 1.0.6 -- 1.0.7 -- 1.1.0 -- 1.1.1 -- 1.1.2 -- 1.1.3 -- 1.1.4 -- 1.1.5 -- 1.2.0-alpha -- 1.2.0-alpha1 -- 1.2.0 -- 2.0.0-alpha - -**Note:** pgModeler 2.x introduces significant changes including a new CMake-based build system. The Dockerfile automatically detects the version and uses the appropriate build method. - -**Important:** Version 2.0.0-alpha requires Qt 6.6+ (for `QPalette::Accent` feature). This branch uses Ubuntu 25.10 which provides Qt 6.6+. For building version 1.2.0 or earlier, you can use Ubuntu 24.04 LTS by changing the base image in the Dockerfile. - -Full changelog: +Full changelog: ## Contributors @@ -135,8 +105,8 @@ Full changelog: Date: Mon, 5 Oct 2026 12:52:59 +0200 Subject: [PATCH 6/7] Fixed digest artifacts and concurrency in workflows --- .github/workflows/build-image.yml | 5 +-- .github/workflows/release.yml | 5 +-- scripts/test_workflows.py | 59 +++++++++++++++++++++++++++++++ 3 files changed, 65 insertions(+), 4 deletions(-) create mode 100644 scripts/test_workflows.py diff --git a/.github/workflows/build-image.yml b/.github/workflows/build-image.yml index 87706e1..40c6633 100644 --- a/.github/workflows/build-image.yml +++ b/.github/workflows/build-image.yml @@ -105,7 +105,7 @@ jobs: if: inputs.push uses: actions/upload-artifact@v7 with: - name: digests-${{ inputs.version }}-${{ matrix.arch }} + name: digests-${{ matrix.arch }}-${{ inputs.version }} path: ${{ runner.temp }}/digests/* if-no-files-found: error retention-days: 1 @@ -122,7 +122,8 @@ jobs: - name: Download digests uses: actions/download-artifact@v8 with: - pattern: digests-${{ inputs.version }}-* + # The version goes last so 2.1.0 does not also match the digests of 2.1.0-rc1 + pattern: digests-*-${{ inputs.version }} path: ${{ runner.temp }}/digests merge-multiple: true diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index bd6f9bb..3d4b2d1 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -35,8 +35,9 @@ permissions: contents: read concurrency: - group: ${{ github.event_name == 'pull_request' && format('validate-{0}', github.ref) || 'release' }} - cancel-in-progress: ${{ github.event_name == 'pull_request' }} + # Publishing runs queue one after another; validation runs never replace a pending publish + group: ${{ (github.event_name == 'schedule' || github.event_name == 'workflow_dispatch') && 'release' || format('validate-{0}', github.ref) }} + cancel-in-progress: ${{ github.event_name != 'schedule' && github.event_name != 'workflow_dispatch' }} jobs: detect: diff --git a/scripts/test_workflows.py b/scripts/test_workflows.py new file mode 100644 index 0000000..f324ace --- /dev/null +++ b/scripts/test_workflows.py @@ -0,0 +1,59 @@ +"""Checks on the GitHub Actions workflows that can be verified without running them.""" +import fnmatch +import pathlib +import re +import unittest + +WORKFLOWS = pathlib.Path(__file__).resolve().parent.parent / ".github" / "workflows" + + +def workflow(name): + return (WORKFLOWS / name).read_text(encoding="utf-8") + + +def evaluate(expression, event, ref="refs/heads/master"): + """Evaluate a simple GitHub Actions expression (==, &&, ||, format) for an event.""" + python = (expression.replace("github.event_name", "event").replace("github.ref", "ref") + .replace("&&", " and ").replace("||", " or ").replace("format(", "_format(")) + return eval(python, {"_format": lambda text, *args: text.format(*args)}, {"event": event, "ref": ref}) + + +class DigestArtifactsTest(unittest.TestCase): + def test_versions_do_not_collect_digests_of_other_versions(self): + text = workflow("build-image.yml") + name = re.search(r"name: (digests-.*)$", text, re.M).group(1) + pattern = re.search(r"pattern: (digests-.*)$", text, re.M).group(1) + + def render(template, version, arch="*"): + return template.replace("${{ inputs.version }}", version).replace("${{ matrix.arch }}", arch) + + versions = ["2.1.0", "2.1.0-rc1", "2.1.0-beta2", "12.1.0"] + artifacts = [render(name, v, arch) for v in versions for arch in ("amd64", "arm64")] + for version in versions: + with self.subTest(version=version): + matched = [a for a in artifacts if fnmatch.fnmatchcase(a, render(pattern, version))] + self.assertEqual(matched, [render(name, version, "amd64"), render(name, version, "arm64")]) + + +class ConcurrencyTest(unittest.TestCase): + def setUp(self): + text = workflow("release.yml") + block = re.search(r"^concurrency:\n((?: .*\n)+)", text, re.M).group(1) + self.group = re.search(r"group: \$\{\{ (.*) \}\}", block).group(1) + self.cancel = re.search(r"cancel-in-progress: \$\{\{ (.*) \}\}", block).group(1) + + def test_publishing_runs_queue_in_the_release_group(self): + for event in ("schedule", "workflow_dispatch"): + with self.subTest(event=event): + self.assertEqual(evaluate(self.group, event), "release") + self.assertFalse(evaluate(self.cancel, event)) + + def test_validation_runs_never_join_the_release_group(self): + for event in ("pull_request", "push"): + with self.subTest(event=event): + self.assertNotEqual(evaluate(self.group, event), "release") + self.assertTrue(evaluate(self.cancel, event)) + + +if __name__ == "__main__": + unittest.main() From d778f86eb5603127514162e79504606948f2c57e Mon Sep 17 00:00:00 2001 From: Abel Paz Date: Mon, 5 Oct 2026 15:25:46 +0200 Subject: [PATCH 7/7] Updated contributors in README.md --- README.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 011778d..a54f0f3 100644 --- a/README.md +++ b/README.md @@ -102,9 +102,9 @@ Full changelog: