Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7b73224c80 | ||
|
|
48c8b7e609 | ||
|
|
062099ae5b | ||
|
|
0e7c33bee2 | ||
|
|
5a082916f8 | ||
|
|
0eac05b450 | ||
|
|
7bbbb53e69 | ||
|
|
eb66e6a347 | ||
|
|
36122bc29b | ||
|
|
6d035dc4fa | ||
|
|
ea17a4e0ff | ||
|
|
a972ea204e | ||
|
|
2551643aa1 | ||
|
|
b939b85ecb | ||
|
|
4581fddd8d | ||
|
|
e13b12c492 | ||
|
|
f4b5309267 | ||
|
|
9e985fef5b | ||
|
|
5ecdcd7201 | ||
|
|
f88596f90c | ||
|
|
602c3c165f | ||
|
|
d25a38fe62 | ||
|
|
d92fb68b22 | ||
|
|
2bca0b3cc9 | ||
|
|
2fc4b1f3d4 | ||
|
|
c6ef41a951 | ||
|
|
35c9235d17 | ||
|
|
58c531d831 | ||
|
|
b39f1a8d79 | ||
|
|
d91a2745f0 | ||
|
|
ad6c028cff | ||
|
|
269e32d5ee | ||
|
|
7378060e5c | ||
|
|
d52fe4740e | ||
|
|
67eab1ba89 | ||
|
|
9e95d9a721 | ||
|
|
9be31a019f | ||
|
|
145e511f05 | ||
|
|
a0c8f4857f | ||
|
|
0eefa38f77 | ||
|
|
32db693198 | ||
|
|
d761942d0b | ||
|
|
2afe505ef4 | ||
|
|
730d1e783c | ||
|
|
c4321b9968 | ||
|
|
99f5606e2e | ||
|
|
a1a43e8c28 | ||
|
|
ea293b335e | ||
|
|
a623526cb7 | ||
|
|
39aed6083b | ||
|
|
05b2864d32 | ||
|
|
8c5dd5eadb | ||
|
|
b345337ddc | ||
|
|
b8afe34fbb | ||
|
|
7f21dc4610 | ||
|
|
ac76a81ad2 | ||
|
|
5f9909e109 | ||
|
|
31a5c5418c | ||
|
|
cc1e1ef184 | ||
|
|
1402f7d9cd | ||
|
|
6594977cc3 | ||
|
|
b98470f327 | ||
|
|
35307612fc | ||
|
|
8b381912a8 | ||
|
|
ab749c630c | ||
|
|
1cac42d986 | ||
|
|
f73b51f912 | ||
|
|
733cd5ba5e | ||
|
|
8877330705 | ||
|
|
8d8bcf3748 | ||
|
|
7828c9b2a1 | ||
|
|
09afc34c0e | ||
|
|
2477a94b32 | ||
|
|
bbb5cc250b | ||
|
|
33fab95a73 | ||
|
|
755cfec19e | ||
|
|
5711c4dd5a | ||
|
|
a6374d1ccd | ||
|
|
78afe89752 | ||
|
|
70c6777b5e | ||
|
|
7c5b0b7da7 | ||
|
|
472965ec40 | ||
|
|
610cad0e0e | ||
|
|
72ab768b94 | ||
|
|
5063c76a06 | ||
|
|
607c9eccbf | ||
|
|
e8f7a18c98 | ||
|
|
a00cd1dd43 | ||
|
|
b6a4c7fe15 | ||
|
|
d699acab0b | ||
|
|
5f6d72c686 | ||
|
|
50209794d7 | ||
|
|
5515b54d53 | ||
|
|
a0b4c09772 | ||
|
|
0389ee789a | ||
|
|
26a7d92b0b | ||
|
|
e02150e634 | ||
|
|
3e6f0c0703 | ||
|
|
1b05065eb0 | ||
|
|
8f24dc378d | ||
|
|
f4337dc8c7 | ||
|
|
fd4b47788b | ||
|
|
74003c19dc | ||
|
|
a11fc0b921 | ||
|
|
5811f55b08 | ||
|
|
d1374536d8 | ||
|
|
04684e798a | ||
|
|
8df5306ebd | ||
|
|
ab632646ed | ||
|
|
43d1d59e00 | ||
|
|
149170195d | ||
|
|
e0a2d2138e | ||
|
|
b10718d616 | ||
|
|
3ec0aafa36 | ||
|
|
6a90e4f837 | ||
|
|
ccdb1f6271 | ||
|
|
3d5661ed87 | ||
|
|
6404865bd5 | ||
|
|
e28b84ed67 | ||
|
|
fc12b2e36b | ||
|
|
34908c4093 | ||
|
|
5a56a3f3dc | ||
|
|
bee294a373 | ||
|
|
224c486c58 | ||
|
|
2a952d678d | ||
|
|
3a6f86a711 | ||
|
|
e85db0fb8b | ||
|
|
0cbddf21ba | ||
|
|
e2e588c324 | ||
|
|
6302a70316 | ||
|
|
c41b606795 | ||
|
|
d924c15e79 | ||
|
|
1530b3551d | ||
|
|
32f9b4a8f6 | ||
|
|
6b8394fe31 | ||
|
|
a9f2cb5405 | ||
|
|
7c974c005c | ||
|
|
b3a941c469 | ||
|
|
4682025f0c | ||
|
|
e5b6728fa9 | ||
|
|
fad01ed776 | ||
|
|
d5bd079593 | ||
|
|
5fc540af73 | ||
|
|
1058f34c52 | ||
|
|
3d3eb8feb9 | ||
|
|
125cb3007f | ||
|
|
fa9762f345 | ||
|
|
d24b946909 | ||
|
|
bdb652b772 | ||
|
|
aaa0c8f14d | ||
|
|
641173d0fd | ||
|
|
ebe4617c00 | ||
|
|
a4bdcff7dc | ||
|
|
d082e8db27 | ||
|
|
a3ec78d30c | ||
|
|
2b7a331896 | ||
|
|
79d138867a | ||
|
|
9712467844 | ||
|
|
347312d8f7 | ||
|
|
5065de5ac3 | ||
|
|
a5aa42843c | ||
|
|
ecb6446c31 | ||
|
|
ca21e94436 | ||
|
|
a3ffb577ae | ||
|
|
c711abe98e | ||
|
|
b8dcd80ae1 | ||
|
|
0360a223a4 | ||
|
|
53ccc753e3 | ||
|
|
9f9bb553c4 | ||
|
|
a807c99daf | ||
|
|
86f164d60c | ||
|
|
dee788c581 | ||
|
|
7d3a170b9e | ||
|
|
467a200ca4 | ||
|
|
cf2c096c4f | ||
|
|
e64b766ca3 | ||
|
|
cd82c8e4ec | ||
|
|
25aec55eb2 | ||
|
|
c83e0750c4 | ||
|
|
19a41432ac | ||
|
|
7314f5069b | ||
|
|
5f58828e49 | ||
|
|
9d63ea91e2 | ||
|
|
a0723f9db8 | ||
|
|
2a65e506a6 | ||
|
|
a100b86ff2 | ||
|
|
bdd3282c49 | ||
|
|
f6a2c1037e | ||
|
|
3515541320 | ||
|
|
369c12c1b2 | ||
|
|
88b599e98f | ||
|
|
4671d2f221 | ||
|
|
8f487ac666 | ||
|
|
6e22b29770 | ||
|
|
ad3ba69ca5 | ||
|
|
fd7b462f3d | ||
|
|
efa8ac7948 | ||
|
|
0dc122cb2e | ||
|
|
c8ae16d13f | ||
|
|
dd0d8ac533 | ||
|
|
d0315c58ec | ||
|
|
43d8b3fabf | ||
|
|
e78c7fe020 | ||
|
|
2ba68de777 | ||
|
|
afa0c773ad | ||
|
|
351376af7f | ||
|
|
91e7747226 | ||
|
|
84a76baa8e | ||
|
|
c603393827 | ||
|
|
ce6c049dbc | ||
|
|
bccef24bf7 | ||
|
|
e543aaa6f9 | ||
|
|
720dd3714f | ||
|
|
af5fd2acc3 | ||
|
|
2343f60f04 | ||
|
|
5f102aa36c | ||
|
|
1b392caa0f | ||
|
|
8bca7e0c57 | ||
|
|
475b864e7f | ||
|
|
d04e39cc65 | ||
|
|
b7789be4af | ||
|
|
97a7e55b96 | ||
|
|
ff9819e222 | ||
|
|
355d6d099f | ||
|
|
da0bc3ecd9 | ||
|
|
db2030e9c0 | ||
|
|
34cb0be9a2 | ||
|
|
ca275cbc8d | ||
|
|
c28362013f | ||
|
|
b2d1362b8a | ||
|
|
24a1f24e48 | ||
|
|
e1915c7dc0 | ||
|
|
89bb92dc44 | ||
|
|
42d2e55bcd | ||
|
|
f57e46c7c4 | ||
|
|
9a68105260 | ||
|
|
299a767b1f | ||
|
|
fc6815ae70 | ||
|
|
456b2ddefa | ||
|
|
9039065ee0 | ||
|
|
620f7b8064 | ||
|
|
3cd04b405e | ||
|
|
8057624259 | ||
|
|
750d0630e0 | ||
|
|
c23172af37 | ||
|
|
458b24b5f8 | ||
|
|
4dc7a7bd7b | ||
|
|
6312baa34e | ||
|
|
5f56805162 | ||
|
|
7242be8be6 | ||
|
|
e419d45930 | ||
|
|
7c6fb89b82 | ||
|
|
a2439b78f5 | ||
|
|
14a796c427 | ||
|
|
53729372a7 | ||
|
|
08f643e619 | ||
|
|
2bb59c0b10 | ||
|
|
e1ad058990 | ||
|
|
4e23684096 | ||
|
|
c5e67bdc67 | ||
|
|
07d2f4dbc5 | ||
|
|
ca4fe9d08a | ||
|
|
2d2402c822 | ||
|
|
9195037213 | ||
|
|
9e31eebe26 | ||
|
|
ebbe4e36d8 | ||
|
|
aca8796178 | ||
|
|
27809a95ef | ||
|
|
211f9bf1f3 | ||
|
|
df39657d6d | ||
|
|
79f5bdfb40 | ||
|
|
5c7f0bdb05 | ||
|
|
5060cd8bc0 | ||
|
|
d7a6a89df3 | ||
|
|
7dc7292d53 | ||
|
|
6f322ea160 | ||
|
|
b8ac3485ad | ||
|
|
1a2bbac26e | ||
|
|
67086aabff | ||
|
|
d6262db305 | ||
|
|
d317a93019 | ||
|
|
992ce03b43 | ||
|
|
14b3137ef1 | ||
|
|
1f6076f888 | ||
|
|
2a4492ea5d | ||
|
|
f703f10d83 | ||
|
|
f7cd7f4d63 | ||
|
|
6f7685a111 | ||
|
|
2d8479feab | ||
|
|
e46d9cb08e | ||
|
|
df76b1ba34 | ||
|
|
6920a58d91 | ||
|
|
1545bcc91c | ||
|
|
e4eb334599 | ||
|
|
9f7dff40f1 | ||
|
|
596396acaf | ||
|
|
3c038f8fd5 | ||
|
|
f149cf4b81 | ||
|
|
7ea9a6bb35 | ||
|
|
2db86f9d0a | ||
|
|
5a694d2f48 | ||
|
|
432d74bf1b | ||
|
|
892681f78d | ||
|
|
ad5b6ae2c7 | ||
|
|
5326c1524f | ||
|
|
96b72d76f1 | ||
|
|
d37e8fe798 | ||
|
|
b60ac47346 | ||
|
|
5dcf165a0a | ||
|
|
b6ed164952 | ||
|
|
68536e70ec | ||
|
|
65557f1814 | ||
|
|
c436287365 | ||
|
|
473fe47895 | ||
|
|
c76fd6b3ee | ||
|
|
01cd0454a3 | ||
|
|
778e00fc3b | ||
|
|
6a287fdb87 | ||
|
|
5b1d7ec954 | ||
|
|
346e4deff4 | ||
|
|
c049488a11 | ||
|
|
cde8851993 | ||
|
|
4d76b6190e | ||
|
|
e49593d005 | ||
|
|
e7027e2186 | ||
|
|
2d7e8aedb1 | ||
|
|
d5c71d7063 | ||
|
|
68a5f7781f | ||
|
|
35f3615644 | ||
|
|
c82e8038ee | ||
|
|
997b6c52f9 | ||
|
|
b6627315b4 | ||
|
|
f1681d78d3 | ||
|
|
b55f5997dd | ||
|
|
d0ce7ad552 | ||
|
|
d486241b06 |
No files matched your search
@@ -1,2 +1,7 @@
|
||||
# Auto detect text files and normalize end of lines to LF (\n)
|
||||
* text=auto eol=lf
|
||||
|
||||
# Make.bat has always been stored with CRLF; the blanket eol=lf above would
|
||||
# silently rewrite the whole file the first time anything touches it.
|
||||
Make.bat -text
|
||||
pkg/win32/*.bat text eol=crlf
|
||||
@@ -0,0 +1,338 @@
|
||||
# Installs the PostgreSQL build dependencies published by pgadmin-org/pgbuild.
|
||||
#
|
||||
# Four workflows need these — the QA and snapshot builds on each of macOS and
|
||||
# Windows — and keeping the logic in one place is not merely tidiness: the
|
||||
# first version used the gh CLI, which turned out not to exist on the
|
||||
# self-hosted Windows runner, and that same fix had to be made twice.
|
||||
#
|
||||
# On macOS the archives must land in /opt/pgbuild specifically, because the
|
||||
# install names inside them are absolute: pg_dump asks for
|
||||
# /opt/pgbuild/zstd/lib/libzstd.1.dylib and libpq for
|
||||
# /opt/pgbuild/krb5/lib/libgssapi_krb5.2.2.dylib, among others. _fixup_imports
|
||||
# copies from those paths into the bundle and rewrites the references, so they
|
||||
# have to resolve at build time. On Windows there are no absolute references,
|
||||
# so the location only has to avoid colliding with anything else, which is why
|
||||
# it goes under the runner's temp directory rather than the C:\build64 the
|
||||
# buildfarm's Jenkins worker manages.
|
||||
#
|
||||
# Everything is fetched over plain HTTPS. pgbuild is public and its asset URLs
|
||||
# are predictable, so this needs neither a CLI nor authentication, and both
|
||||
# curl --fail and Invoke-WebRequest fail on a 404 rather than writing an error
|
||||
# page to disk and failing later at the unpack.
|
||||
#
|
||||
# What gets downloaded is decided by pkg/pgbuild-deps.lock rather than by the
|
||||
# rolling -latest tags, whose assets pgbuild replaces in place on every
|
||||
# rebuild. Building from those means that what is Authenticode signed on
|
||||
# Windows and Developer ID signed and notarised on macOS is whatever pgbuild
|
||||
# happened to be publishing that morning, which is neither reproducible nor
|
||||
# something anybody reviewed. The lock names a per-version tag and the SHA-256
|
||||
# of its archive, and an archive that does not hash to the recorded digest, or
|
||||
# that has no lock entry at all, fails the build rather than being unpacked
|
||||
# unverified. pgbuild publishes a .sha256 beside each archive, which is
|
||||
# deliberately not consulted: whoever rebuilds an archive rebuilds that too, so
|
||||
# it is a convenience rather than a source of trust, and the lock is the only
|
||||
# thing here that pins anything.
|
||||
#
|
||||
# Being out of date is a different thing from being wrong, so once an archive
|
||||
# has been verified, pgbuild's releases are consulted to see whether a newer
|
||||
# build of that dependency exists, and if one does the job carries a warning
|
||||
# naming it. It stays a warning, because the build goes on to ship the pinned
|
||||
# version and a dependency published overnight is no reason to turn pgAdmin
|
||||
# red, and the check is best effort, so an API that will not answer leaves a
|
||||
# build whose downloads are already verified alone.
|
||||
# tools/refresh_pgbuild_lock.py is what turns such a warning into a reviewed
|
||||
# commit.
|
||||
|
||||
name: Install pgbuild dependencies
|
||||
description: Download and unpack the PostgreSQL build dependencies from pgadmin-org/pgbuild
|
||||
|
||||
inputs:
|
||||
arch:
|
||||
description: Architecture as pgbuild names it, e.g. arm64, x86_64
|
||||
required: true
|
||||
postgres-major:
|
||||
description: PostgreSQL major version to install
|
||||
required: false
|
||||
default: '18'
|
||||
|
||||
outputs:
|
||||
prefix:
|
||||
description: Directory the dependencies were unpacked into
|
||||
# Whichever of the two platform steps ran. Merging these in a shell step
|
||||
# instead would mean picking a shell that exists on every runner, and the
|
||||
# Windows signing host has no bash.
|
||||
value: ${{ steps.install-macos.outputs.prefix || steps.install-windows.outputs.prefix }}
|
||||
|
||||
runs:
|
||||
using: composite
|
||||
steps:
|
||||
- name: Install the pgbuild dependencies (macOS)
|
||||
id: install-macos
|
||||
if: runner.os == 'macOS'
|
||||
shell: bash
|
||||
env:
|
||||
PGBUILD_ARCH: ${{ inputs.arch }}
|
||||
POSTGRES_MAJOR: ${{ inputs.postgres-major }}
|
||||
# Only so that the out-of-date check does not run into the
|
||||
# unauthenticated rate limit, which is shared with everything else
|
||||
# coming from a hosted runner's address and would leave the warning
|
||||
# firing only sometimes. Nothing else here needs authentication,
|
||||
# pgbuild being public.
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
BASE=https://github.com/pgadmin-org/pgbuild/releases/download
|
||||
RELEASES=https://api.github.com/repos/pgadmin-org/pgbuild/releases
|
||||
|
||||
# GITHUB_ACTION_PATH rather than the workspace, so that the lock is
|
||||
# found even where a workflow checks the repository out somewhere
|
||||
# other than the default directory.
|
||||
LOCK="${GITHUB_ACTION_PATH}/../../../pkg/pgbuild-deps.lock"
|
||||
|
||||
# The hosted macOS images have shasum, which comes with the system
|
||||
# perl, but not sha256sum, whereas a Linux-alike self-hosted runner is
|
||||
# the other way about, so take whichever is there.
|
||||
if command -v shasum > /dev/null; then
|
||||
sha256() { shasum -a 256 "$1" | cut -d' ' -f1; }
|
||||
else
|
||||
sha256() { sha256sum "$1" | cut -d' ' -f1; }
|
||||
fi
|
||||
|
||||
# Column 2 of a lock line is the tag to fetch and column 3 the digest
|
||||
# its archive must hash to. Trailing comments are stripped first,
|
||||
# which also rebuilds awk's idea of the fields.
|
||||
lock_field() {
|
||||
awk -v key="$1" -v col="$2" '
|
||||
{ sub(/#.*/, "") }
|
||||
$1 == key { print $col; exit }
|
||||
' "${LOCK}"
|
||||
}
|
||||
|
||||
# pgbuild's tags, newest first, for the out-of-date check below. This
|
||||
# is deliberately best effort: a build whose downloads have been
|
||||
# verified must not fail because GitHub would not say what else has
|
||||
# been published since.
|
||||
TAGS="${RUNNER_TEMP}/pgbuild-tags.txt"
|
||||
: > "${TAGS}"
|
||||
|
||||
if ! curl --fail --silent --location --max-time 30 \
|
||||
-H "Accept: application/vnd.github+json" \
|
||||
${GH_TOKEN:+-H "Authorization: Bearer ${GH_TOKEN}"} \
|
||||
"${RELEASES}?per_page=100" \
|
||||
| grep -o '"tag_name": *"[^"]*"' \
|
||||
| sed 's/.*"\(.*\)"/\1/' > "${TAGS}"; then
|
||||
echo "Could not list pgbuild's releases, so this build will not"
|
||||
echo "report whether its pinned dependencies are still current."
|
||||
fi
|
||||
|
||||
sudo mkdir -p /opt/pgbuild
|
||||
sudo chown "$(id -u):$(id -g)" /opt/pgbuild
|
||||
|
||||
for PKG in openssl krb5 zstd lz4 "postgresql-${POSTGRES_MAJOR}"; do
|
||||
KEY="${PKG}-macos-${PGBUILD_ARCH}"
|
||||
TAG=$(lock_field "${KEY}" 2)
|
||||
EXPECTED=$(lock_field "${KEY}" 3)
|
||||
|
||||
if [ -z "${TAG}" ] || [ -z "${EXPECTED}" ]; then
|
||||
echo "ERROR: ${KEY} has no entry in pkg/pgbuild-deps.lock"
|
||||
echo
|
||||
echo "An unpinned dependency must not reach a build we sign,"
|
||||
echo "so this is a failure rather than a warning. Add it with:"
|
||||
echo
|
||||
echo " tools/refresh_pgbuild_lock.py ${PKG%%-*}"
|
||||
echo
|
||||
echo "If this is a PostgreSQL major the lock has never seen,"
|
||||
echo "add it to POSTGRES_MAJORS in that script first."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
ARCHIVE="${TAG}.tar.gz"
|
||||
echo "Fetching ${ARCHIVE}..."
|
||||
curl --fail --silent --show-error --location \
|
||||
-o "${RUNNER_TEMP}/${ARCHIVE}" "${BASE}/${TAG}/${ARCHIVE}"
|
||||
|
||||
ACTUAL=$(sha256 "${RUNNER_TEMP}/${ARCHIVE}")
|
||||
|
||||
if [ "${ACTUAL}" != "${EXPECTED}" ]; then
|
||||
echo "ERROR: checksum mismatch for ${ARCHIVE}"
|
||||
echo " expected: ${EXPECTED}"
|
||||
echo " actual: ${ACTUAL}"
|
||||
echo
|
||||
echo "The bytes behind ${TAG} are not the"
|
||||
echo "ones that were reviewed when the lock was written."
|
||||
echo "Usually that means pgbuild rebuilt the tag in place,"
|
||||
echo "which it does for PostgreSQL whenever a new minor comes"
|
||||
echo "out, that tag carrying only the major. Satisfy yourself"
|
||||
echo "that a rebuild is what happened before re-running,"
|
||||
echo "because this is also the one check standing between a"
|
||||
echo "tampered dependency and a signed pgAdmin build, and then"
|
||||
echo "record the new archive, read the diff and commit it:"
|
||||
echo
|
||||
echo " tools/refresh_pgbuild_lock.py ${PKG%%-*}"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "Verified ${ARCHIVE} (sha256 ${ACTUAL})"
|
||||
|
||||
# The first tag of the right shape is the newest, the listing
|
||||
# coming back in reverse chronological order. The rolling tag is
|
||||
# skipped because it names no version.
|
||||
NEWEST=""
|
||||
|
||||
while read -r CANDIDATE; do
|
||||
case "${CANDIDATE}" in
|
||||
*-latest)
|
||||
continue
|
||||
;;
|
||||
${PKG}*-macos-${PGBUILD_ARCH})
|
||||
NEWEST="${CANDIDATE}"
|
||||
break
|
||||
;;
|
||||
esac
|
||||
done < "${TAGS}"
|
||||
|
||||
if [ -n "${NEWEST}" ] && [ "${NEWEST}" != "${TAG}" ]; then
|
||||
echo "::warning title=pgbuild dependency out of date::${KEY} is pinned to ${TAG}, but pgbuild has since published ${NEWEST}. This build is using the pinned version, which is what the lock file is for, and nothing is broken. To move to the newer build, run tools/refresh_pgbuild_lock.py ${PKG%%-*}, review the change to pkg/pgbuild-deps.lock and commit it."
|
||||
fi
|
||||
|
||||
tar -xzf "${RUNNER_TEMP}/${ARCHIVE}" -C /opt/pgbuild
|
||||
done
|
||||
|
||||
ls -l /opt/pgbuild
|
||||
echo "prefix=/opt/pgbuild" >> "${GITHUB_OUTPUT}"
|
||||
|
||||
- name: Install the pgbuild dependencies (Windows)
|
||||
id: install-windows
|
||||
if: runner.os == 'Windows'
|
||||
shell: powershell
|
||||
env:
|
||||
PGBUILD_ARCH: ${{ inputs.arch }}
|
||||
POSTGRES_MAJOR: ${{ inputs.postgres-major }}
|
||||
# See the macOS step: this is for the out-of-date check's rate limit
|
||||
# alone, and the downloads themselves need no authentication.
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
run: |
|
||||
# Without this, Invoke-WebRequest spends most of a 90 MB download
|
||||
# rendering a progress bar into a log nobody will read.
|
||||
$ProgressPreference = 'SilentlyContinue'
|
||||
|
||||
$base = 'https://github.com/pgadmin-org/pgbuild/releases/download'
|
||||
$releases = 'https://api.github.com/repos/pgadmin-org/pgbuild/releases'
|
||||
|
||||
# GITHUB_ACTION_PATH rather than the workspace, so that the lock is
|
||||
# found even where a workflow checks the repository out somewhere
|
||||
# other than the default directory.
|
||||
$lock = Join-Path $env:GITHUB_ACTION_PATH '..\..\..\pkg\pgbuild-deps.lock'
|
||||
|
||||
# Column 2 of a lock line is the tag to fetch and column 3 the digest
|
||||
# its archive must hash to; anything after a # is a comment.
|
||||
function Get-LockEntry($key) {
|
||||
foreach ($line in Get-Content $lock) {
|
||||
$text = ($line -split '#', 2)[0].Trim()
|
||||
|
||||
if ($text -eq '') { continue }
|
||||
|
||||
$fields = $text -split '\s+'
|
||||
|
||||
if ($fields.Count -eq 3 -and $fields[0] -eq $key) {
|
||||
return @{ Tag = $fields[1]; Digest = $fields[2] }
|
||||
}
|
||||
}
|
||||
|
||||
return $null
|
||||
}
|
||||
|
||||
# pgbuild's tags, newest first, for the out-of-date check below. This
|
||||
# is deliberately best effort: a build whose downloads have been
|
||||
# verified must not fail because GitHub would not say what else has
|
||||
# been published since.
|
||||
$tags = @()
|
||||
|
||||
try {
|
||||
$headers = @{ 'Accept' = 'application/vnd.github+json' }
|
||||
|
||||
if ($env:GH_TOKEN) {
|
||||
$headers['Authorization'] = "Bearer $env:GH_TOKEN"
|
||||
}
|
||||
|
||||
$tags = (Invoke-RestMethod -Uri "${releases}?per_page=100" -Headers $headers -UseBasicParsing) | ForEach-Object { $_.tag_name }
|
||||
} catch {
|
||||
Write-Output "Could not list pgbuild's releases, so this build will not"
|
||||
Write-Output "report whether its pinned dependencies are still current."
|
||||
}
|
||||
|
||||
# The first tag of the right shape is the newest, the listing coming
|
||||
# back in reverse chronological order. The rolling tag is skipped
|
||||
# because it names no version.
|
||||
function Get-NewestTag($pattern) {
|
||||
foreach ($tag in $tags) {
|
||||
if ($tag -like '*-latest') { continue }
|
||||
|
||||
if ($tag -like $pattern) { return $tag }
|
||||
}
|
||||
|
||||
return $null
|
||||
}
|
||||
|
||||
$depsDir = Join-Path $env:RUNNER_TEMP 'pgbuild-deps'
|
||||
New-Item -ItemType Directory -Force -Path $depsDir | Out-Null
|
||||
|
||||
foreach ($pkg in @("postgresql-$env:POSTGRES_MAJOR", 'krb5')) {
|
||||
$key = "$pkg-windows-$env:PGBUILD_ARCH"
|
||||
$pin = Get-LockEntry $key
|
||||
$family = ($pkg -split '-')[0]
|
||||
|
||||
if (-not $pin) {
|
||||
Write-Output "ERROR: $key has no entry in pkg/pgbuild-deps.lock"
|
||||
Write-Output ""
|
||||
Write-Output "An unpinned dependency must not reach a build we sign,"
|
||||
Write-Output "so this is a failure rather than a warning. Add it with:"
|
||||
Write-Output ""
|
||||
Write-Output " tools/refresh_pgbuild_lock.py $family"
|
||||
Write-Output ""
|
||||
Write-Output "If this is a PostgreSQL major the lock has never seen,"
|
||||
Write-Output "add it to POSTGRES_MAJORS in that script first."
|
||||
exit 1
|
||||
}
|
||||
|
||||
$archive = "$($pin.Tag).zip"
|
||||
$zip = Join-Path $env:RUNNER_TEMP $archive
|
||||
Write-Output "Fetching $archive..."
|
||||
Invoke-WebRequest -Uri "$base/$($pin.Tag)/$archive" -OutFile $zip -UseBasicParsing
|
||||
|
||||
$actual = (Get-FileHash -Algorithm SHA256 -Path $zip).Hash.ToLower()
|
||||
|
||||
if ($actual -ne $pin.Digest) {
|
||||
Write-Output "ERROR: checksum mismatch for $archive"
|
||||
Write-Output " expected: $($pin.Digest)"
|
||||
Write-Output " actual: $actual"
|
||||
Write-Output ""
|
||||
Write-Output "The bytes behind $($pin.Tag) are not the"
|
||||
Write-Output "ones that were reviewed when the lock was written."
|
||||
Write-Output "Usually that means pgbuild rebuilt the tag in place,"
|
||||
Write-Output "which it does for PostgreSQL whenever a new minor comes"
|
||||
Write-Output "out, that tag carrying only the major. Satisfy yourself"
|
||||
Write-Output "that a rebuild is what happened before re-running,"
|
||||
Write-Output "because this is also the one check standing between a"
|
||||
Write-Output "tampered dependency and a signed pgAdmin build, and then"
|
||||
Write-Output "record the new archive, read the diff and commit it:"
|
||||
Write-Output ""
|
||||
Write-Output " tools/refresh_pgbuild_lock.py $family"
|
||||
exit 1
|
||||
}
|
||||
|
||||
Write-Output "Verified $archive (sha256 $actual)"
|
||||
|
||||
$newest = Get-NewestTag "$pkg*-windows-$env:PGBUILD_ARCH"
|
||||
|
||||
if ($newest -and $newest -ne $pin.Tag) {
|
||||
Write-Output "::warning title=pgbuild dependency out of date::$key is pinned to $($pin.Tag), but pgbuild has since published $newest. This build is using the pinned version, which is what the lock file is for, and nothing is broken. To move to the newer build, run tools/refresh_pgbuild_lock.py $family, review the change to pkg/pgbuild-deps.lock and commit it."
|
||||
}
|
||||
|
||||
Expand-Archive -Path $zip -DestinationPath $depsDir -Force
|
||||
}
|
||||
|
||||
Get-ChildItem $depsDir
|
||||
# Out-File in Windows PowerShell defaults to UTF-16, which the runner
|
||||
# cannot parse, so the encoding is explicit.
|
||||
"prefix=$depsDir" | Out-File -FilePath $env:GITHUB_OUTPUT -Encoding utf8 -Append
|
||||
@@ -0,0 +1,431 @@
|
||||
# Install pgAdmin from a published tree, exactly as the README tells a user to.
|
||||
#
|
||||
# Everything else in this pipeline checks that files exist, that indexes parse
|
||||
# and that signatures validate. All three can be true of a repository apt still
|
||||
# refuses, and two bugs found during the migration were of precisely that
|
||||
# shape: packages one directory too deep, and noarch RPMs filed where no dnf
|
||||
# client looks. Neither failed a build.
|
||||
#
|
||||
# So this does not inspect anything. It follows the instructions in the
|
||||
# repository README, in a clean container, and installs the package. No
|
||||
# --allow-unauthenticated, no --nogpgcheck, no pinning: if the signature is
|
||||
# wrong or the architecture is missing from Release, the install fails, which
|
||||
# is the point.
|
||||
#
|
||||
# It runs against a release candidate on the development server, which serves
|
||||
# its tree the moment it is written. The download server sits behind an hourly
|
||||
# sync to the mirrors, so testing there would mostly measure the sync.
|
||||
|
||||
name: Acceptance test
|
||||
|
||||
on:
|
||||
workflow_call:
|
||||
inputs:
|
||||
base-url:
|
||||
description: The tree to install from, without a trailing slash
|
||||
required: true
|
||||
type: string
|
||||
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
base-url:
|
||||
description: >-
|
||||
The tree to install from, for example
|
||||
https://developer.pgadmin.org/builds/2026-09-21-1
|
||||
required: true
|
||||
type: string
|
||||
|
||||
concurrency:
|
||||
group: 'acceptance-test @ ${{ inputs.base-url }}'
|
||||
cancel-in-progress: true
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
# The URL is fetched from, installed from and, in the apt job, written into
|
||||
# a file, so it is checked once here rather than trusted in nine places. The
|
||||
# host allowlist is the point: everything below downloads and then installs
|
||||
# as root whatever that host serves, so this is what decides who gets to ask
|
||||
# for that.
|
||||
check:
|
||||
runs-on: ubuntu-24.04
|
||||
|
||||
steps:
|
||||
- name: Check the base URL
|
||||
env:
|
||||
BASE_URL: ${{ inputs.base-url }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
case "${BASE_URL}" in
|
||||
https://developer.pgadmin.org/builds/*) ;;
|
||||
https://ftp.postgresql.org/pub/pgadmin/pgadmin4/*) ;;
|
||||
*)
|
||||
echo "Not a tree this test will install from: ${BASE_URL}" >&2
|
||||
echo "Expected developer.pgadmin.org or ftp.postgresql.org." >&2
|
||||
exit 1 ;;
|
||||
esac
|
||||
# No trailing slash, no shell metacharacters, nothing that could be
|
||||
# read as a second word: the value is interpolated into commands.
|
||||
printf '%s' "${BASE_URL}" | grep -Eq '^https://[A-Za-z0-9./_-]+[A-Za-z0-9]$' || {
|
||||
echo "The base URL may contain only letters, digits, dot, slash, underscore and hyphen." >&2
|
||||
exit 1; }
|
||||
|
||||
apt:
|
||||
needs: check
|
||||
runs-on: ubuntu-24.04
|
||||
|
||||
# The images default to sh, which has no pipefail, so every step that set
|
||||
# it died on the first line. All of these carry bash.
|
||||
defaults:
|
||||
run:
|
||||
shell: bash
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
include:
|
||||
- { name: 'bookworm', image: 'debian:12' }
|
||||
- { name: 'trixie', image: 'debian:13' }
|
||||
- { name: 'jammy', image: 'ubuntu:22.04' }
|
||||
- { name: 'noble', image: 'ubuntu:24.04' }
|
||||
- { name: 'resolute', image: 'ubuntu:26.04' }
|
||||
container: ${{ matrix.image }}
|
||||
|
||||
steps:
|
||||
# Only what a user would already have or would obviously need. lsb-release
|
||||
# is here because the README's own sources.list line calls lsb_release,
|
||||
# and following the instruction is the test.
|
||||
- name: Install the prerequisites
|
||||
env:
|
||||
DEBIAN_FRONTEND: noninteractive
|
||||
run: |
|
||||
apt-get update
|
||||
apt-get install -y --no-install-recommends ca-certificates curl gnupg lsb-release
|
||||
|
||||
- name: Follow the README
|
||||
env:
|
||||
BASE_URL: ${{ inputs.base-url }}
|
||||
DEBIAN_FRONTEND: noninteractive
|
||||
run: |
|
||||
set -euo pipefail
|
||||
|
||||
curl -fsS https://www.pgadmin.org/static/packages_pgadmin_org.pub \
|
||||
| gpg --dearmor -o /usr/share/keyrings/packages-pgadmin-org.gpg
|
||||
|
||||
# Written by this shell rather than handed to `sh -c`, which
|
||||
# re-parsed the expanded URL as source and was the one place in the
|
||||
# pipeline where a caller-supplied value reached a shell.
|
||||
CODENAME=$(lsb_release -cs)
|
||||
echo "deb [signed-by=/usr/share/keyrings/packages-pgadmin-org.gpg] ${BASE_URL}/apt/${CODENAME} pgadmin4 main" \
|
||||
> /etc/apt/sources.list.d/pgadmin4.list
|
||||
cat /etc/apt/sources.list.d/pgadmin4.list
|
||||
|
||||
# A repository apt will not verify makes this fail here, before any
|
||||
# package is considered.
|
||||
apt-get update
|
||||
|
||||
- name: Install pgAdmin
|
||||
env:
|
||||
DEBIAN_FRONTEND: noninteractive
|
||||
run: |
|
||||
set -euo pipefail
|
||||
apt-get install -y pgadmin4-server
|
||||
|
||||
# The version proves what installed is this build rather than
|
||||
# something the distribution happened to carry, and the file list
|
||||
# proves the package actually delivered its payload.
|
||||
dpkg-query -W -f '${Package} ${Version}\n' pgadmin4-server
|
||||
# grep without -q, deliberately: -q exits on the first match, the
|
||||
# command feeding it then dies of SIGPIPE, and pipefail turns that
|
||||
# into a failed pipeline even though the match succeeded.
|
||||
dpkg -L pgadmin4-server | grep '^/usr/pgadmin4/' >/dev/null || {
|
||||
echo "pgadmin4-server installed no files under /usr/pgadmin4" >&2
|
||||
exit 1; }
|
||||
|
||||
yum:
|
||||
needs: check
|
||||
runs-on: ubuntu-24.04
|
||||
|
||||
# The images default to sh, which has no pipefail, so every step that set
|
||||
# it died on the first line. All of these carry bash.
|
||||
defaults:
|
||||
run:
|
||||
shell: bash
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
include:
|
||||
- { name: 'rhel-9', image: 'almalinux:9', family: 'redhat' }
|
||||
- { name: 'rhel-10', image: 'almalinux:10', family: 'redhat' }
|
||||
- { name: 'fedora-43', image: 'fedora:43', family: 'fedora' }
|
||||
- { name: 'fedora-44', image: 'fedora:44', family: 'fedora' }
|
||||
container: ${{ matrix.image }}
|
||||
|
||||
steps:
|
||||
- name: Follow the README
|
||||
env:
|
||||
BASE_URL: ${{ inputs.base-url }}
|
||||
FAMILY: ${{ matrix.family }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
|
||||
# The repository RPM carries the .repo file and the packaging key, and
|
||||
# points at the tree it shipped in, so nothing here names a URL twice.
|
||||
# It sets gpgcheck=1 and repo_gpgcheck=1, which is why no --nogpgcheck
|
||||
# appears below: a wrongly signed repository fails at this step.
|
||||
RPM=$(curl -fsS "${BASE_URL}/yum/" \
|
||||
| grep -oE "pgadmin4-${FAMILY}-repo-[0-9.-]+\.noarch\.rpm" \
|
||||
| sort -u | tail -1)
|
||||
echo "Repository package: ${RPM}"
|
||||
rpm -i "${BASE_URL}/yum/${RPM}"
|
||||
cat /etc/yum.repos.d/pgadmin4.repo
|
||||
|
||||
- name: Install pgAdmin
|
||||
run: |
|
||||
set -euo pipefail
|
||||
dnf install -y pgadmin4-server
|
||||
rpm -q pgadmin4-server
|
||||
# grep without -q, deliberately: -q exits on the first match, the
|
||||
# command feeding it then dies of SIGPIPE, and pipefail turns that
|
||||
# into a failed pipeline even though the match succeeded.
|
||||
rpm -ql pgadmin4-server | grep '^/usr/pgadmin4/' >/dev/null || {
|
||||
echo "pgadmin4-server installed no files under /usr/pgadmin4" >&2
|
||||
exit 1; }
|
||||
|
||||
# The Windows installer, actually installed. Nothing else in the pipeline
|
||||
# runs it, and until now nothing verified the signature on the executable
|
||||
# inside it either: Make.bat's own check tested a stale %ERRORLEVEL%, and the
|
||||
# workflow step that claimed to cover the gap only re-verified the installer.
|
||||
windows:
|
||||
needs: check
|
||||
runs-on: windows-2025
|
||||
steps:
|
||||
- name: Download and install
|
||||
shell: pwsh
|
||||
env:
|
||||
BASE_URL: ${{ inputs.base-url }}
|
||||
run: |
|
||||
$ErrorActionPreference = 'Stop'
|
||||
$listing = Invoke-WebRequest -Uri "$env:BASE_URL/" -UseBasicParsing
|
||||
$name = ([regex]'pgadmin4-[0-9.]+-x64\.exe').Matches($listing.Content) |
|
||||
ForEach-Object { $_.Value } | Sort-Object -Unique | Select-Object -Last 1
|
||||
if (-not $name) { throw "No installer found at $env:BASE_URL" }
|
||||
Write-Output "Installer: $name"
|
||||
|
||||
Invoke-WebRequest -Uri "$env:BASE_URL/$name" -OutFile installer.exe -UseBasicParsing
|
||||
|
||||
# The installer's own signature, before running anything from it.
|
||||
$sig = Get-AuthenticodeSignature installer.exe
|
||||
Write-Output "Installer signature: $($sig.Status) / $($sig.SignerCertificate.Subject)"
|
||||
if ($sig.Status -ne 'Valid') { throw "Installer signature is $($sig.Status)" }
|
||||
|
||||
# Inno Setup's silent switches. /DIR is given so the check below does
|
||||
# not depend on the default install location.
|
||||
$p = Start-Process -FilePath .\installer.exe -Wait -PassThru `
|
||||
-ArgumentList '/VERYSILENT','/SUPPRESSMSGBOXES','/NORESTART','/DIR=C:\pgadmin4-test'
|
||||
if ($p.ExitCode -ne 0) { throw "Installer exited with $($p.ExitCode)" }
|
||||
|
||||
- name: Check what was installed
|
||||
shell: pwsh
|
||||
run: |
|
||||
$ErrorActionPreference = 'Stop'
|
||||
$root = 'C:\pgadmin4-test'
|
||||
|
||||
# The components that carry no signature of ours, and so are only
|
||||
# checked for presence. Everything we do sign is covered by the step
|
||||
# below, which checks that it is there as a side effect of verifying
|
||||
# it.
|
||||
foreach ($f in @("$root\python\python.exe", "$root\web\pgAdmin4.py")) {
|
||||
if (-not (Test-Path $f)) { throw "Missing from the installation: $f" }
|
||||
Write-Output "present: $f"
|
||||
}
|
||||
|
||||
# Everything Make.bat's :SIGN_COMPONENTS signs, verified where it lands.
|
||||
# None of it exists outside the installer until the installer has run, so
|
||||
# this is the only place a published build can be checked: the runtime
|
||||
# executable, and the PostgreSQL and Kerberos binaries we ship alongside
|
||||
# it. The Electron, Python and VC++ runtime components are deliberately
|
||||
# not signed by us and so are not listed here.
|
||||
- name: Check the component signatures
|
||||
shell: pwsh
|
||||
run: |
|
||||
$ErrorActionPreference = 'Stop'
|
||||
$root = 'C:\pgadmin4-test'
|
||||
|
||||
# The publisher is taken from the installer rather than hard coded,
|
||||
# so that a component signed by somebody else is caught without this
|
||||
# needing an edit every time the certificate is renewed.
|
||||
$expected = (Get-AuthenticodeSignature installer.exe).SignerCertificate.Subject
|
||||
Write-Output "Expected publisher: $expected"
|
||||
|
||||
# winpgbuild does not always ship these four, and Make.bat copies them
|
||||
# only when they are present, so an empty match is not a failure.
|
||||
$optional = @('libintl-*.dll', 'libiconv-*.dll', 'liblz4.dll', 'libzstd.dll')
|
||||
|
||||
foreach ($pattern in @(
|
||||
'pgAdmin4.exe',
|
||||
'libpq.dll', 'libcrypto-*-x64.dll', 'libssl-*-x64.dll', 'zlib1.dll',
|
||||
'pg_dump.exe', 'pg_dumpall.exe', 'pg_restore.exe', 'psql.exe',
|
||||
'kinit.exe', 'krb5_64.dll', 'comerr64.dll', 'k5sprt64.dll',
|
||||
'gssapi64.dll', 'krbcc64.dll', 'ccapiserver.exe') + $optional) {
|
||||
|
||||
$files = @(Get-ChildItem -Path "$root\runtime\$pattern" -ErrorAction SilentlyContinue)
|
||||
if ($files.Count -eq 0) {
|
||||
if ($optional -contains $pattern) {
|
||||
Write-Output "absent, and optional: $pattern"
|
||||
continue
|
||||
}
|
||||
throw "Missing from the installation: $root\runtime\$pattern"
|
||||
}
|
||||
|
||||
foreach ($f in $files) {
|
||||
$sig = Get-AuthenticodeSignature $f.FullName
|
||||
Write-Output "$($f.Name): $($sig.Status) / $($sig.SignerCertificate.Subject)"
|
||||
if ($sig.Status -ne 'Valid') {
|
||||
throw "$($f.Name) signature is $($sig.Status)"
|
||||
}
|
||||
if ($sig.SignerCertificate.Subject -ne $expected) {
|
||||
throw "$($f.Name) was signed by $($sig.SignerCertificate.Subject)"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
# The macOS disk image as published, rather than the bundle as built. The
|
||||
# snapshot workflow checks the bundle it has just made; this checks what a
|
||||
# user downloads, which has been through a DMG, a web server and a browser's
|
||||
# quarantine attribute.
|
||||
macos:
|
||||
needs: check
|
||||
runs-on: ${{ matrix.runner }}
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
include:
|
||||
- { name: 'arm64', runner: 'macos-15' }
|
||||
- { name: 'x64', runner: 'macos-26-intel' }
|
||||
steps:
|
||||
- name: Download, mount and check
|
||||
env:
|
||||
BASE_URL: ${{ inputs.base-url }}
|
||||
ARCH: ${{ matrix.name }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
name=$(curl -fsS "${BASE_URL}/" \
|
||||
| grep -oE "pgadmin4-[0-9.]+-${ARCH}\.dmg" | sort -u | tail -1)
|
||||
[ -n "${name}" ] || { echo "No ${ARCH} disk image found" >&2; exit 1; }
|
||||
echo "Disk image: ${name}"
|
||||
curl -fsS -o image.dmg "${BASE_URL}/${name}"
|
||||
|
||||
# Quarantine it as a browser would, so Gatekeeper is actually
|
||||
# consulted rather than bypassed because the file arrived by curl.
|
||||
xattr -w com.apple.quarantine \
|
||||
"0081;$(printf '%x' "$(date +%s)");Safari;" image.dmg
|
||||
|
||||
hdiutil attach image.dmg -nobrowse -readonly -mountpoint /tmp/pga
|
||||
app="/tmp/pga/pgAdmin 4.app"
|
||||
|
||||
test -d "${app}"
|
||||
test -x "${app}/Contents/MacOS/pgAdmin 4"
|
||||
test -x "${app}/Contents/SharedSupport/pg_dump"
|
||||
test -f "${app}/Contents/Resources/web/pgAdmin4.py"
|
||||
|
||||
# Gatekeeper first, because that is what decides whether a user can
|
||||
# open this. codesign --strict comes after and is allowed to have its
|
||||
# own opinion: the disk image deliberately carries a FinderInfo xattr
|
||||
# on the bundle, set by SetFile -a E in _create_dmg to hide the .app
|
||||
# extension in the window, and --strict rejects any xattr at all.
|
||||
echo "=== notarisation and Gatekeeper ==="
|
||||
# stapler looks the ticket up in Apple's CloudKit, and a dropped
|
||||
# connection there fails the step with error 68, so it is retried.
|
||||
# A genuinely broken ticket still fails all three attempts.
|
||||
for attempt in 1 2 3; do
|
||||
xcrun stapler validate "${app}" && break
|
||||
[ "${attempt}" -lt 3 ] || exit 1
|
||||
echo "stapler validate failed; retrying in $((attempt * 30))s"
|
||||
sleep $((attempt * 30))
|
||||
done
|
||||
spctl --assess --type execute --verbose=4 "${app}"
|
||||
|
||||
echo "=== signature ==="
|
||||
codesign -dvvv "${app}" 2>&1 | grep -E 'Authority|TeamIdentifier'
|
||||
codesign --verify --deep --verbose=2 "${app}"
|
||||
|
||||
# Reported rather than enforced. If this ever starts failing whilst
|
||||
# spctl still passes, it is the extension-hiding attribute and not a
|
||||
# broken signature; if spctl fails too, the build is broken.
|
||||
if ! codesign --verify --deep --strict --verbose=2 "${app}" 2>/tmp/strict.log; then
|
||||
echo "note: --strict verification failed, Gatekeeper having passed:"
|
||||
tail -2 /tmp/strict.log
|
||||
fi
|
||||
|
||||
# Nothing in the bundle may reference a build machine. This is what
|
||||
# catches a dependency that was linked but not relocated, which is
|
||||
# how an ICU or OpenSSL path would escape.
|
||||
echo "=== linkage of the bundled client binaries ==="
|
||||
for b in pg_dump pg_dumpall pg_restore psql; do
|
||||
echo "--- ${b} ---"
|
||||
otool -L "${app}/Contents/SharedSupport/${b}" | tail -n +2
|
||||
done
|
||||
if otool -L "${app}/Contents/SharedSupport/"* \
|
||||
| grep -E '/opt/pgbuild|/opt/homebrew|/usr/local/(lib|opt)'; then
|
||||
echo "A bundled binary still points outside the bundle" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
hdiutil detach /tmp/pga
|
||||
|
||||
# The downloads nobody installs: the wheel, the tarballs and the documents.
|
||||
# Shallow by design, but it does verify every detached signature from the
|
||||
# published URL rather than on the server, which is what a user would get.
|
||||
artefacts:
|
||||
needs: check
|
||||
runs-on: ubuntu-24.04
|
||||
steps:
|
||||
- name: Fetch and check
|
||||
env:
|
||||
BASE_URL: ${{ inputs.base-url }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
mkdir -p dl && cd dl
|
||||
|
||||
listing=$(curl -fsS "${BASE_URL}/")
|
||||
fetch() {
|
||||
local pattern=$1 name
|
||||
name=$(printf '%s' "${listing}" | grep -oE "${pattern}" | sort -u | tail -1)
|
||||
[ -n "${name}" ] || { echo "Nothing matching ${pattern}" >&2; exit 1; }
|
||||
curl -fsS -O "${BASE_URL}/${name}"
|
||||
echo "${name}"
|
||||
}
|
||||
|
||||
tarball=$(fetch 'pgadmin4-[0-9.]+\.tar\.gz')
|
||||
wheel=$(fetch 'pgadmin4-[0-9.]+-py3-none-any\.whl')
|
||||
docs=$(fetch 'pgadmin4-[0-9.]+-docs\.tar\.gz')
|
||||
pdf=$(fetch 'pgadmin4-[0-9.]+\.pdf')
|
||||
epub=$(fetch 'pgadmin4-[0-9.]+\.epub')
|
||||
|
||||
echo "=== signatures, as a user would verify them ==="
|
||||
export GNUPGHOME=$(mktemp -d); chmod 700 "${GNUPGHOME}"
|
||||
curl -fsS https://www.pgadmin.org/static/packages_pgadmin_org.pub | gpg --import
|
||||
for f in "${tarball}" "${wheel}" "${docs}" "${pdf}" "${epub}"; do
|
||||
curl -fsS -O "${BASE_URL}/${f}.asc"
|
||||
gpg --verify "${f}.asc" "${f}"
|
||||
done
|
||||
|
||||
echo "=== the documents are what they claim to be ==="
|
||||
file "${pdf}" | grep -q 'PDF document'
|
||||
file "${epub}" | grep -q 'EPUB'
|
||||
# A truncated PDF still says PDF, so size is checked too.
|
||||
[ "$(stat -c%s "${pdf}")" -gt 1000000 ] || { echo "PDF is implausibly small" >&2; exit 1; }
|
||||
|
||||
echo "=== the tarballs contain what they should ==="
|
||||
tar tzf "${tarball}" | grep -E '/web/pgAdmin4\.py$' >/dev/null
|
||||
tar tzf "${tarball}" | grep -E '/requirements\.txt$' >/dev/null
|
||||
tar tzf "${docs}" | grep -E 'index\.html$' >/dev/null
|
||||
|
||||
- name: Install the wheel
|
||||
run: |
|
||||
set -euo pipefail
|
||||
cd dl
|
||||
python3 -m venv /tmp/wheeltest
|
||||
/tmp/wheeltest/bin/pip install --quiet pgadmin4-*-py3-none-any.whl
|
||||
/tmp/wheeltest/bin/python -c 'import pgadmin4; print("imported", pgadmin4.__file__)'
|
||||
@@ -1,28 +1,175 @@
|
||||
name: Check container build
|
||||
|
||||
on:
|
||||
# Callable, so that a workflow which needs a container build gets this one
|
||||
# rather than a second definition of it that can drift. Snapshot does not
|
||||
# call it today, the image not being part of the download tree.
|
||||
workflow_call:
|
||||
inputs:
|
||||
publish:
|
||||
description: >-
|
||||
Build for both architectures and push to the registry, tagged
|
||||
with the location below. A build check does neither: it proves
|
||||
the Dockerfile still works and throws the image away.
|
||||
type: boolean
|
||||
required: false
|
||||
default: false
|
||||
location:
|
||||
description: The tag to push, normally the build's date
|
||||
type: string
|
||||
required: false
|
||||
default: ''
|
||||
secrets:
|
||||
DOCKERHUB_USERNAME:
|
||||
required: false
|
||||
DOCKERHUB_TOKEN:
|
||||
required: false
|
||||
|
||||
push:
|
||||
branches: [ "master" ]
|
||||
paths:
|
||||
- 'Dockerfile'
|
||||
- 'pkg/docker/**'
|
||||
- 'web/**'
|
||||
# Not the message catalogues: a .po change cannot break a build or a
|
||||
# test, and check-translations already compiles them, which is the
|
||||
# check that would catch a malformed one.
|
||||
- '!web/pgadmin/translations/**'
|
||||
- 'requirements.txt'
|
||||
- 'docs/**'
|
||||
- '.dockerignore'
|
||||
- 'LICENSE'
|
||||
- '.github/workflows/check-container-build.yml'
|
||||
pull_request:
|
||||
branches: [ "master" ]
|
||||
paths:
|
||||
- 'Dockerfile'
|
||||
- 'pkg/docker/**'
|
||||
- 'web/**'
|
||||
# Not the message catalogues: a .po change cannot break a build or a
|
||||
# test, and check-translations already compiles them, which is the
|
||||
# check that would catch a malformed one.
|
||||
- '!web/pgadmin/translations/**'
|
||||
- 'requirements.txt'
|
||||
- 'docs/**'
|
||||
- '.dockerignore'
|
||||
- 'LICENSE'
|
||||
- '.github/workflows/check-container-build.yml'
|
||||
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
publish:
|
||||
description: Build for both architectures and push to the registries
|
||||
type: boolean
|
||||
required: false
|
||||
default: false
|
||||
location:
|
||||
description: The tag to push, normally the build's date
|
||||
type: string
|
||||
required: false
|
||||
default: ''
|
||||
|
||||
concurrency:
|
||||
group: '${{ github.workflow }} @ ${{ github.event.pull_request.head.label || github.head_ref || github.ref }}'
|
||||
# The workflow's own name, spelled out rather than taken from
|
||||
# github.workflow, because in a reusable workflow that expression is
|
||||
# the CALLER's name. Every workflow Snapshot calls would otherwise
|
||||
# share one group and, with cancel-in-progress, cancel each other.
|
||||
# github.event_name is in the key because a called workflow inherits the
|
||||
# caller's ref: once this is on master, the nightly Snapshot's deb build
|
||||
# and an ordinary push's deb build would otherwise share a group and,
|
||||
# with cancel-in-progress, kill each other. A push landing mid-nightly
|
||||
# would cancel the snapshot's build and skip the publish entirely.
|
||||
group: 'check-container-build @ ${{ github.event.pull_request.head.label || github.head_ref || github.ref }} @ ${{ github.event_name }}'
|
||||
cancel-in-progress: true
|
||||
|
||||
# Read-only by default, as everywhere else here. The publish job below names
|
||||
# the wider set it genuinely needs, and a job-level block replaces this one
|
||||
# rather than adding to it, so its packages: write is unaffected.
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
# The check. No environment, and so no access to the registry credentials:
|
||||
# this runs on every pull request, and a build check has no business being
|
||||
# able to push anything.
|
||||
build-container:
|
||||
if: ${{ !inputs.publish }}
|
||||
runs-on: ubuntu-22.04
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v7
|
||||
with:
|
||||
fetch-depth: 2
|
||||
|
||||
- name: Build the container
|
||||
uses: docker/build-push-action@v6
|
||||
- name: Check the container builds
|
||||
uses: docker/build-push-action@c3c9e263c25d99ce0380d002d59b67737d91b0dc # v7.4.0
|
||||
with:
|
||||
push: false
|
||||
context: .
|
||||
|
||||
# The publish. Declared on the publishing environment, which is where the
|
||||
# Docker Hub credentials live: an environment secret is not available to a
|
||||
# job that does not name the environment, which is what secrets: inherit
|
||||
# alone does not solve.
|
||||
publish-container:
|
||||
if: inputs.publish
|
||||
runs-on: ubuntu-22.04
|
||||
environment: publishing
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
# For GHCR, which takes the workflow's own token rather than a stored
|
||||
# registry credential.
|
||||
packages: write
|
||||
|
||||
steps:
|
||||
# Before anything is logged in to, because this value becomes a tag in
|
||||
# a job holding the Docker Hub credentials, and the tags input is
|
||||
# newline separated: an unchecked value could name a second tag, and a
|
||||
# tag can name another repository. The shape is the one the wrapper
|
||||
# requires of a build location.
|
||||
- name: Check the location
|
||||
env:
|
||||
LOCATION: ${{ inputs.location }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
printf '%s' "${LOCATION}" \
|
||||
| grep -Eq '^[0-9]{4}-[0-9]{2}-[0-9]{2}(-([1-9][0-9]?|[A-Za-z][A-Za-z0-9]*))?$' || {
|
||||
echo "Not a build location: ${LOCATION}" >&2; exit 1; }
|
||||
|
||||
- uses: actions/checkout@v7
|
||||
with:
|
||||
fetch-depth: 2
|
||||
|
||||
# Emulation is slow, so only the publishing path pays for it.
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@c7c53464625b32c7a7e944ae62b3e17d2b600130 # v3
|
||||
|
||||
- name: Set up Buildx
|
||||
uses: docker/setup-buildx-action@8d2750c68a42422c14e847fe6c8ac0403b4cbd6f # v3
|
||||
|
||||
- name: Log in to Docker Hub
|
||||
uses: docker/login-action@c94ce9fb468520275223c153574b00df6fe4bcc9 # v3
|
||||
with:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
|
||||
- name: Log in to GHCR
|
||||
uses: docker/login-action@c94ce9fb468520275223c153574b00df6fe4bcc9 # v3
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
# Tagged with the build's location rather than a version. Promotion
|
||||
# re-tags this exact image, so what ships is the manifest that was
|
||||
# tested rather than a rebuild of the same source.
|
||||
- name: Build and push the container
|
||||
uses: docker/build-push-action@c3c9e263c25d99ce0380d002d59b67737d91b0dc # v7.4.0
|
||||
with:
|
||||
push: true
|
||||
context: .
|
||||
platforms: linux/amd64,linux/arm64
|
||||
tags: |
|
||||
dpage/pgadmin4:${{ inputs.location }}
|
||||
ghcr.io/pgadmin-org/pgadmin4:${{ inputs.location }}
|
||||
@@ -0,0 +1,140 @@
|
||||
# Replaces the pgadmin4-deb-qa Jenkins job. That job runs `make debian` on a
|
||||
# dedicated EC2 VM per distribution; here each distribution is a container on a
|
||||
# hosted runner instead, which is the same build in a reproducible environment
|
||||
# and no VMs to maintain.
|
||||
#
|
||||
# Triggered on master and by hand rather than on pull requests, matching what
|
||||
# the Jenkins job does today (it polls git every five minutes). Running the
|
||||
# full matrix on every PR is a separate decision.
|
||||
|
||||
name: Check DEB builds
|
||||
|
||||
on:
|
||||
# Callable from snapshot.yml, so that what gets published is the
|
||||
# same build that runs on every push rather than a second
|
||||
# definition of it that can drift.
|
||||
workflow_call:
|
||||
|
||||
push:
|
||||
branches: [ "master" ]
|
||||
paths:
|
||||
- 'pkg/debian/**'
|
||||
- 'pkg/linux/**'
|
||||
- 'web/**'
|
||||
# Not the message catalogues: a .po change cannot break a build or a
|
||||
# test, and check-translations already compiles them, which is the
|
||||
# check that would catch a malformed one.
|
||||
- '!web/pgadmin/translations/**'
|
||||
- 'docs/**'
|
||||
- 'requirements.txt'
|
||||
- 'Makefile'
|
||||
- 'tools/setup-python-env.sh'
|
||||
- '.github/workflows/check-deb-build.yml'
|
||||
|
||||
workflow_dispatch:
|
||||
|
||||
concurrency:
|
||||
# The workflow's own name, spelled out rather than taken from
|
||||
# github.workflow, because in a reusable workflow that expression is
|
||||
# the CALLER's name. Every workflow Snapshot calls would otherwise
|
||||
# share one group and, with cancel-in-progress, cancel each other.
|
||||
# github.event_name is in the key because a called workflow inherits the
|
||||
# caller's ref: once this is on master, the nightly Snapshot's deb build
|
||||
# and an ordinary push's deb build would otherwise share a group and,
|
||||
# with cancel-in-progress, kill each other. A push landing mid-nightly
|
||||
# would cancel the snapshot's build and skip the publish entirely.
|
||||
group: 'check-deb-build @ ${{ github.ref }} @ ${{ github.event_name }}'
|
||||
cancel-in-progress: true
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
build-deb:
|
||||
runs-on: ubuntu-24.04
|
||||
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
include:
|
||||
# "name" exists only because artifact names may not contain a colon.
|
||||
- { name: 'debian-12', image: 'debian:12' }
|
||||
- { name: 'debian-13', image: 'debian:13' }
|
||||
- { name: 'ubuntu-2204', image: 'ubuntu:22.04' }
|
||||
- { name: 'ubuntu-2404', image: 'ubuntu:24.04' }
|
||||
- { name: 'ubuntu-2604', image: 'ubuntu:26.04' }
|
||||
|
||||
container: ${{ matrix.image }}
|
||||
|
||||
steps:
|
||||
# Before checkout, so the action has git available and the container has
|
||||
# the certificates it needs to fetch anything.
|
||||
- name: Install the build environment
|
||||
env:
|
||||
DEBIAN_FRONTEND: noninteractive
|
||||
run: |
|
||||
apt-get update
|
||||
apt-get install -y --no-install-recommends \
|
||||
build-essential \
|
||||
ca-certificates \
|
||||
curl \
|
||||
dh-exec \
|
||||
dpkg-dev \
|
||||
fakeroot \
|
||||
git \
|
||||
libffi-dev \
|
||||
libkrb5-dev \
|
||||
libpq-dev \
|
||||
libreadline-dev \
|
||||
libssl-dev \
|
||||
lsb-release \
|
||||
python3-dev \
|
||||
python3-pip \
|
||||
python3-sphinx \
|
||||
python3-venv \
|
||||
python3-wheel \
|
||||
unzip \
|
||||
wget \
|
||||
zlib1g-dev
|
||||
|
||||
# sphinxcontrib-youtube has no distro package. --break-system-packages is
|
||||
# needed from Debian 12 and Ubuntu 24.04 onwards (PEP 668), and is
|
||||
# harmless before that, but the flag itself only exists on newer pip, so
|
||||
# fall back rather than fail.
|
||||
- name: Install the Sphinx extensions
|
||||
run: |
|
||||
python3 -m pip install --break-system-packages sphinxcontrib-youtube \
|
||||
|| python3 -m pip install sphinxcontrib-youtube
|
||||
|
||||
- name: Install syft
|
||||
run: curl -sSfL https://raw.githubusercontent.com/anchore/syft/v1.52.0/install.sh | sh -s -- -b /usr/local/bin v1.52.0
|
||||
|
||||
- uses: actions/setup-node@v7
|
||||
with:
|
||||
node-version: '20.x'
|
||||
|
||||
- uses: actions/checkout@v7
|
||||
|
||||
# The container runs as root while the workspace is owned by the runner
|
||||
# user, so git refuses to operate on it ("detected dubious ownership").
|
||||
# The build shells out to git for the commit hash during `yarn run
|
||||
# bundle`, which fails with exit 128 without this.
|
||||
- name: Mark the workspace as a safe git directory
|
||||
run: git config --global --add safe.directory "${GITHUB_WORKSPACE}"
|
||||
|
||||
# Corepack provisions the pinned Yarn from the packageManager field,
|
||||
# matching run-javascript-tests.yml. The distro nodejs packages are
|
||||
# avoided entirely: on Ubuntu 26.04 the distro node ships without npm,
|
||||
# which is why the buildfarm has to pin NodeSource above it.
|
||||
- name: Enable Corepack
|
||||
run: corepack enable
|
||||
|
||||
- name: Check the DEB build
|
||||
run: make debian
|
||||
|
||||
- name: Archive the packages
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: pgadmin4-deb-build-output-${{ matrix.name }}
|
||||
if-no-files-found: error
|
||||
path: dist/*.deb
|
||||
@@ -1,15 +1,47 @@
|
||||
name: Check documentation builds
|
||||
|
||||
on:
|
||||
# Callable from snapshot.yml, so that what gets published is the
|
||||
# same build that runs on every push rather than a second
|
||||
# definition of it that can drift.
|
||||
workflow_call:
|
||||
|
||||
push:
|
||||
branches: [ "master" ]
|
||||
paths:
|
||||
- 'docs/**'
|
||||
- 'web/version.py'
|
||||
- 'web/branding.py'
|
||||
- 'requirements.txt'
|
||||
- 'Makefile'
|
||||
- 'web/config.py'
|
||||
- 'tools/setup-python-env.sh'
|
||||
- '.github/workflows/check-doc-builds.yml'
|
||||
pull_request:
|
||||
branches: [ "master" ]
|
||||
paths:
|
||||
- 'docs/**'
|
||||
- 'web/version.py'
|
||||
- 'web/branding.py'
|
||||
- 'requirements.txt'
|
||||
- 'Makefile'
|
||||
- 'web/config.py'
|
||||
- 'tools/setup-python-env.sh'
|
||||
- '.github/workflows/check-doc-builds.yml'
|
||||
|
||||
workflow_dispatch:
|
||||
|
||||
concurrency:
|
||||
group: '${{ github.workflow }} @ ${{ github.event.pull_request.head.label || github.head_ref || github.ref }}'
|
||||
# The workflow's own name, spelled out rather than taken from
|
||||
# github.workflow, because in a reusable workflow that expression is
|
||||
# the CALLER's name. Every workflow Snapshot calls would otherwise
|
||||
# share one group and, with cancel-in-progress, cancel each other.
|
||||
# github.event_name is in the key because a called workflow inherits the
|
||||
# caller's ref: once this is on master, the nightly Snapshot's deb build
|
||||
# and an ordinary push's deb build would otherwise share a group and,
|
||||
# with cancel-in-progress, kill each other. A push landing mid-nightly
|
||||
# would cancel the snapshot's build and skip the publish entirely.
|
||||
group: 'check-doc-builds @ ${{ github.event.pull_request.head.label || github.head_ref || github.ref }} @ ${{ github.event_name }}'
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
@@ -17,7 +49,7 @@ jobs:
|
||||
runs-on: ubuntu-22.04
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v7
|
||||
|
||||
- name: Install platform dependencies
|
||||
run: |
|
||||
@@ -33,9 +65,10 @@ jobs:
|
||||
make docs
|
||||
|
||||
- name: Archive HTML
|
||||
uses: actions/upload-artifact@v4
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: pgadmin4-html-build-output
|
||||
if-no-files-found: error
|
||||
path: docs/en_US/_build/html/*
|
||||
|
||||
- name: Check the ePub build
|
||||
@@ -44,9 +77,10 @@ jobs:
|
||||
make docs-epub
|
||||
|
||||
- name: Archive ePub
|
||||
uses: actions/upload-artifact@v4
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: pgadmin4-epub-build-output
|
||||
if-no-files-found: error
|
||||
path: docs/en_US/_build/epub/pgAdmin4.epub
|
||||
|
||||
- name: Check the PDF build
|
||||
@@ -55,7 +89,8 @@ jobs:
|
||||
make docs-pdf
|
||||
|
||||
- name: Archive PDF
|
||||
uses: actions/upload-artifact@v4
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: pgadmin4-pdf-build-output
|
||||
if-no-files-found: error
|
||||
path: docs/en_US/_build/latex/pgadmin4.pdf
|
||||
@@ -0,0 +1,59 @@
|
||||
# Replaces the pgadmin4-helm-buildx-qa Jenkins job, which runs
|
||||
# `helm package pkg/helm` and nothing else. Helm is installed explicitly rather
|
||||
# than relied upon from the runner image, since what the images ship changes
|
||||
# between versions.
|
||||
|
||||
name: Check Helm chart build
|
||||
|
||||
on:
|
||||
# Callable from snapshot.yml, so that what gets published is the
|
||||
# same build that runs on every push rather than a second
|
||||
# definition of it that can drift.
|
||||
workflow_call:
|
||||
|
||||
push:
|
||||
branches: [ "master" ]
|
||||
paths:
|
||||
- 'pkg/helm/**'
|
||||
- '.github/workflows/check-helm-build.yml'
|
||||
|
||||
workflow_dispatch:
|
||||
|
||||
concurrency:
|
||||
# The workflow's own name, spelled out rather than taken from
|
||||
# github.workflow, because in a reusable workflow that expression is
|
||||
# the CALLER's name. Every workflow Snapshot calls would otherwise
|
||||
# share one group and, with cancel-in-progress, cancel each other.
|
||||
# github.event_name is in the key because a called workflow inherits the
|
||||
# caller's ref: once this is on master, the nightly Snapshot's deb build
|
||||
# and an ordinary push's deb build would otherwise share a group and,
|
||||
# with cancel-in-progress, kill each other. A push landing mid-nightly
|
||||
# would cancel the snapshot's build and skip the publish entirely.
|
||||
group: 'check-helm-build @ ${{ github.ref }} @ ${{ github.event_name }}'
|
||||
cancel-in-progress: true
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
build-chart:
|
||||
runs-on: ubuntu-24.04
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v7
|
||||
|
||||
- name: Install Helm
|
||||
run: |
|
||||
curl -fsSL https://raw.githubusercontent.com/helm/helm/v4.3.0/scripts/get-helm-3 -o get-helm-3
|
||||
chmod 700 get-helm-3
|
||||
./get-helm-3
|
||||
|
||||
- name: Check the chart build
|
||||
run: helm package pkg/helm
|
||||
|
||||
- name: Archive the chart
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: pgadmin4-helm-build-output
|
||||
if-no-files-found: error
|
||||
path: pgadmin4-helm-*.tgz
|
||||
@@ -3,8 +3,24 @@ name: Check Javascript style
|
||||
on:
|
||||
push:
|
||||
branches: [ "master" ]
|
||||
paths:
|
||||
- 'web/**/*.js'
|
||||
- 'web/**/*.jsx'
|
||||
- 'web/package.json'
|
||||
- 'web/yarn.lock'
|
||||
- 'web/.eslintrc.js'
|
||||
- 'web/.yarnrc.yml'
|
||||
- '.github/workflows/check-javascript-style.yml'
|
||||
pull_request:
|
||||
branches: [ "master" ]
|
||||
paths:
|
||||
- 'web/**/*.js'
|
||||
- 'web/**/*.jsx'
|
||||
- 'web/package.json'
|
||||
- 'web/yarn.lock'
|
||||
- 'web/.eslintrc.js'
|
||||
- 'web/.yarnrc.yml'
|
||||
- '.github/workflows/check-javascript-style.yml'
|
||||
|
||||
workflow_dispatch:
|
||||
|
||||
@@ -17,7 +33,7 @@ jobs:
|
||||
runs-on: ubuntu-22.04
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v7
|
||||
|
||||
# Corepack reads packageManager from web/package.json and provisions
|
||||
# exactly that Yarn: a single pinned download, with nothing written
|
||||
@@ -28,7 +44,7 @@ jobs:
|
||||
- name: Install Node modules
|
||||
run: |
|
||||
cd web
|
||||
yarn install
|
||||
yarn install --immutable
|
||||
|
||||
- name: Run the linter
|
||||
run: |
|
||||
|
||||
@@ -0,0 +1,126 @@
|
||||
# Replaces the pgadmin4-macos-qa Jenkins job. That job runs `make appbundle` on
|
||||
# the two EC2 Mac workers; this runs the same build on hosted runners, which
|
||||
# cover both architectures.
|
||||
#
|
||||
# The build is unsigned and unnotarised: pkg/mac/build.sh sets CODESIGN=0 and
|
||||
# NOTARIZE=0 when pkg/mac/codesign.conf and pkg/mac/notarization.conf are
|
||||
# absent, so this needs no secrets. Signing belongs to the release and snapshot
|
||||
# builds.
|
||||
#
|
||||
# PostgreSQL comes from Homebrew for pg_config (psycopg builds against it) and
|
||||
# for the four client binaries copied into the bundle. The release build uses
|
||||
# a PostgreSQL built against its own OpenSSL and Kerberos; that difference
|
||||
# does not matter for a build check, and _fixup_imports relocates Homebrew's
|
||||
# libraries into the bundle correctly (verified by _verify_bundle_linkage,
|
||||
# which fails the build if anything still points outside it).
|
||||
|
||||
name: Check macOS builds
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ "master" ]
|
||||
paths:
|
||||
- 'pkg/mac/**'
|
||||
- '.github/actions/install-pgbuild-deps/action.yml'
|
||||
# Which build of each dependency this compiles against, so a
|
||||
# change to it changes what the installer contains.
|
||||
- 'pkg/pgbuild-deps.lock'
|
||||
- 'web/**'
|
||||
# Not the message catalogues: a .po change cannot break a build or a
|
||||
# test, and check-translations already compiles them, which is the
|
||||
# check that would catch a malformed one.
|
||||
- '!web/pgadmin/translations/**'
|
||||
- 'docs/**'
|
||||
- 'requirements.txt'
|
||||
- 'Makefile'
|
||||
- 'tools/setup-python-env.sh'
|
||||
- '.github/workflows/check-macos-build.yml'
|
||||
|
||||
workflow_dispatch:
|
||||
|
||||
concurrency:
|
||||
# The workflow's own name, spelled out rather than taken from
|
||||
# github.workflow, because in a reusable workflow that expression is
|
||||
# the CALLER's name. Every workflow Snapshot calls would otherwise
|
||||
# share one group and, with cancel-in-progress, cancel each other.
|
||||
group: 'check-macos-build @ ${{ github.ref }}'
|
||||
cancel-in-progress: true
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
build-appbundle:
|
||||
runs-on: ${{ matrix.runner }}
|
||||
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
include:
|
||||
# "arch" is pgbuild's spelling of the architecture, which differs
|
||||
# from the one this build uses internally (x64 rather than x86_64).
|
||||
- { name: 'arm64', arch: 'arm64', runner: 'macos-15' }
|
||||
# macos-26-intel rather than macos-15-intel: create-dmg's unmount
|
||||
# scales with how much real Mach-O content lands on the volume, and
|
||||
# the macos-15-intel image is slow enough that the real bundle
|
||||
# crosses hdiutil's 120-second DiskArbitration timeout. Measured with
|
||||
# an identical payload, unmount to "Disk image done": 19s on arm64,
|
||||
# 41s on macos-26-intel, 82s on macos-15-intel, and that was with a
|
||||
# lighter stand-in than the real bundle. It is also the newer image,
|
||||
# which we would move to regardless since macOS 15 retires first.
|
||||
- { name: 'x64', arch: 'x86_64', runner: 'macos-26-intel' }
|
||||
|
||||
timeout-minutes: 120
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v7
|
||||
|
||||
- uses: actions/setup-node@v7
|
||||
with:
|
||||
node-version: '20.x'
|
||||
|
||||
- name: Enable Corepack
|
||||
run: corepack enable
|
||||
|
||||
# syft generates the SBOM, wget fetches Electron. PostgreSQL and its
|
||||
# dependencies no longer come from Homebrew; see below.
|
||||
- name: Install the build dependencies
|
||||
run: brew install syft wget
|
||||
|
||||
# The dependencies come from pgadmin-org/pgbuild, built against each
|
||||
# other with --with-zstd --with-lz4 and a 14.0 deployment target, which
|
||||
# is what the release build ships. Homebrew's PostgreSQL was only ever a
|
||||
# stand-in: it has no zstd or lz4 support (issue #9425), links Homebrew's
|
||||
# OpenSSL rather than ours, and carries whatever minimum OS Homebrew
|
||||
# happened to build for.
|
||||
- name: Install the PostgreSQL build dependencies
|
||||
uses: ./.github/actions/install-pgbuild-deps
|
||||
with:
|
||||
arch: ${{ matrix.arch }}
|
||||
|
||||
- name: Show what pg_dump expects
|
||||
run: otool -L /opt/pgbuild/postgresql/bin/pg_dump
|
||||
|
||||
- name: Check the appbundle build
|
||||
run: |
|
||||
export PGADMIN_POSTGRES_DIR=/opt/pgbuild/postgresql
|
||||
|
||||
# cryptography publishes arm64-only macOS wheels (checked against
|
||||
# 50.0.1), so on Intel pip builds it from the sdist and its
|
||||
# openssl-sys crate links whatever OpenSSL it finds on the build
|
||||
# host. _fixup_imports deliberately skips _rust.abi3.so, so that
|
||||
# reference would survive into the bundle and _verify_bundle_linkage
|
||||
# would fail the build. Linking statically leaves no external
|
||||
# reference at all, and against our own OpenSSL rather than a
|
||||
# different one from Homebrew. See issue #10123.
|
||||
export OPENSSL_DIR=/opt/pgbuild/openssl
|
||||
export OPENSSL_STATIC=1
|
||||
|
||||
make appbundle
|
||||
|
||||
- name: Archive the disk image
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: pgadmin4-macos-build-output-${{ matrix.name }}
|
||||
if-no-files-found: error
|
||||
path: dist/*.dmg
|
||||
@@ -1,15 +1,57 @@
|
||||
name: Check Python build
|
||||
|
||||
on:
|
||||
# Callable from snapshot.yml, so that what gets published is the
|
||||
# same build that runs on every push rather than a second
|
||||
# definition of it that can drift.
|
||||
workflow_call:
|
||||
|
||||
push:
|
||||
branches: [ "master" ]
|
||||
paths:
|
||||
- 'web/**'
|
||||
# Not the message catalogues: a .po change cannot break a build or a
|
||||
# test, and check-translations already compiles them, which is the
|
||||
# check that would catch a malformed one.
|
||||
- '!web/pgadmin/translations/**'
|
||||
- 'pkg/pip/**'
|
||||
- 'requirements.txt'
|
||||
- 'Makefile'
|
||||
- 'docs/**'
|
||||
- 'LICENSE'
|
||||
- 'README.md'
|
||||
- 'tools/setup-python-env.sh'
|
||||
- '.github/workflows/check-python-build.yml'
|
||||
pull_request:
|
||||
branches: [ "master" ]
|
||||
paths:
|
||||
- 'web/**'
|
||||
# Not the message catalogues: a .po change cannot break a build or a
|
||||
# test, and check-translations already compiles them, which is the
|
||||
# check that would catch a malformed one.
|
||||
- '!web/pgadmin/translations/**'
|
||||
- 'pkg/pip/**'
|
||||
- 'requirements.txt'
|
||||
- 'Makefile'
|
||||
- 'docs/**'
|
||||
- 'LICENSE'
|
||||
- 'README.md'
|
||||
- 'tools/setup-python-env.sh'
|
||||
- '.github/workflows/check-python-build.yml'
|
||||
|
||||
workflow_dispatch:
|
||||
|
||||
concurrency:
|
||||
group: '${{ github.workflow }} @ ${{ github.event.pull_request.head.label || github.head_ref || github.ref }}'
|
||||
# The workflow's own name, spelled out rather than taken from
|
||||
# github.workflow, because in a reusable workflow that expression is
|
||||
# the CALLER's name. Every workflow Snapshot calls would otherwise
|
||||
# share one group and, with cancel-in-progress, cancel each other.
|
||||
# github.event_name is in the key because a called workflow inherits the
|
||||
# caller's ref: once this is on master, the nightly Snapshot's deb build
|
||||
# and an ordinary push's deb build would otherwise share a group and,
|
||||
# with cancel-in-progress, kill each other. A push landing mid-nightly
|
||||
# would cancel the snapshot's build and skip the publish entirely.
|
||||
group: 'check-python-build @ ${{ github.event.pull_request.head.label || github.head_ref || github.ref }} @ ${{ github.event_name }}'
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
@@ -17,7 +59,7 @@ jobs:
|
||||
runs-on: ubuntu-22.04
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v7
|
||||
|
||||
- name: Install platform dependencies
|
||||
run: |
|
||||
@@ -33,7 +75,8 @@ jobs:
|
||||
make pip
|
||||
|
||||
- name: Archive Python wheel
|
||||
uses: actions/upload-artifact@v4
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: pgadmin4-python-build-output
|
||||
if-no-files-found: error
|
||||
path: dist/*
|
||||
@@ -3,8 +3,16 @@ name: Check Python style
|
||||
on:
|
||||
push:
|
||||
branches: [ "master" ]
|
||||
paths:
|
||||
- '**.py'
|
||||
- '.pycodestyle'
|
||||
- '.github/workflows/check-python-style.yml'
|
||||
pull_request:
|
||||
branches: [ "master" ]
|
||||
paths:
|
||||
- '**.py'
|
||||
- '.pycodestyle'
|
||||
- '.github/workflows/check-python-style.yml'
|
||||
|
||||
workflow_dispatch:
|
||||
|
||||
@@ -17,7 +25,7 @@ jobs:
|
||||
runs-on: ubuntu-22.04
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v7
|
||||
|
||||
- name: Install Python dependencies
|
||||
run: |
|
||||
|
||||
@@ -0,0 +1,173 @@
|
||||
# Replaces the pgadmin4-rpm-qa Jenkins job, as check-deb-build.yml replaces the
|
||||
# DEB one: a container per distribution on a hosted runner, rather than a
|
||||
# dedicated EC2 VM per distribution.
|
||||
#
|
||||
# Unlike the DEB build, the PGDG repository is not optional here.
|
||||
# pkg/redhat/build.sh opens with `export PATH=$(ls -d /usr/pgsql-1* | ...)` and
|
||||
# runs under `set -e`, so the build dies immediately without a PGDG PostgreSQL
|
||||
# installed.
|
||||
#
|
||||
# The QA job does not sign anything; `rpmsign --resign` is a separate step in
|
||||
# the release and snapshot jobs.
|
||||
|
||||
name: Check RPM builds
|
||||
|
||||
on:
|
||||
# Callable from snapshot.yml, so that what gets published is the
|
||||
# same build that runs on every push rather than a second
|
||||
# definition of it that can drift.
|
||||
workflow_call:
|
||||
|
||||
push:
|
||||
branches: [ "master" ]
|
||||
paths:
|
||||
- 'pkg/redhat/**'
|
||||
- 'pkg/linux/**'
|
||||
- 'web/**'
|
||||
# Not the message catalogues: a .po change cannot break a build or a
|
||||
# test, and check-translations already compiles them, which is the
|
||||
# check that would catch a malformed one.
|
||||
- '!web/pgadmin/translations/**'
|
||||
- 'docs/**'
|
||||
- 'requirements.txt'
|
||||
- 'Makefile'
|
||||
- 'tools/setup-python-env.sh'
|
||||
- '.github/workflows/check-rpm-build.yml'
|
||||
|
||||
workflow_dispatch:
|
||||
|
||||
concurrency:
|
||||
# The workflow's own name, spelled out rather than taken from
|
||||
# github.workflow, because in a reusable workflow that expression is
|
||||
# the CALLER's name. Every workflow Snapshot calls would otherwise
|
||||
# share one group and, with cancel-in-progress, cancel each other.
|
||||
# github.event_name is in the key because a called workflow inherits the
|
||||
# caller's ref: once this is on master, the nightly Snapshot's deb build
|
||||
# and an ordinary push's deb build would otherwise share a group and,
|
||||
# with cancel-in-progress, kill each other. A push landing mid-nightly
|
||||
# would cancel the snapshot's build and skip the publish entirely.
|
||||
group: 'check-rpm-build @ ${{ github.ref }} @ ${{ github.event_name }}'
|
||||
cancel-in-progress: true
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
build-rpm:
|
||||
runs-on: ubuntu-24.04
|
||||
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
include:
|
||||
# "name" exists only because artifact names may not contain a colon.
|
||||
# "repo" selects the PGDG repository RPM, which is built per
|
||||
# distribution family and major version.
|
||||
- name: 'el-9'
|
||||
image: 'almalinux:9'
|
||||
repo: 'https://download.postgresql.org/pub/repos/yum/reporpms/EL-9-x86_64/pgdg-redhat-repo-latest.noarch.rpm'
|
||||
- name: 'el-10'
|
||||
image: 'almalinux:10'
|
||||
repo: 'https://download.postgresql.org/pub/repos/yum/reporpms/EL-10-x86_64/pgdg-redhat-repo-latest.noarch.rpm'
|
||||
- name: 'fedora-43'
|
||||
image: 'fedora:43'
|
||||
repo: 'https://download.postgresql.org/pub/repos/yum/reporpms/F-43-x86_64/pgdg-fedora-repo-latest.noarch.rpm'
|
||||
- name: 'fedora-44'
|
||||
image: 'fedora:44'
|
||||
repo: 'https://download.postgresql.org/pub/repos/yum/reporpms/F-44-x86_64/pgdg-fedora-repo-latest.noarch.rpm'
|
||||
|
||||
container: ${{ matrix.image }}
|
||||
|
||||
steps:
|
||||
# Before checkout, so the action has git available.
|
||||
- name: Install the build environment
|
||||
env:
|
||||
PGDG_REPO: ${{ matrix.repo }}
|
||||
run: |
|
||||
dnf install -y dnf-plugins-core
|
||||
|
||||
# EL needs EPEL and CRB for the build dependencies; Fedora has
|
||||
# everything in the main repositories.
|
||||
if [ ! -f /etc/fedora-release ]; then
|
||||
dnf install -y epel-release
|
||||
dnf config-manager --set-enabled crb
|
||||
fi
|
||||
|
||||
# The distro's own PostgreSQL module would shadow PGDG's packages.
|
||||
dnf -qy module disable postgresql || true
|
||||
|
||||
dnf install -y --nogpgcheck "${PGDG_REPO}"
|
||||
|
||||
# The compiler toolchain is not optional: psycopg-c and gssapi are
|
||||
# both built from source (pkg/linux/build-functions.sh passes
|
||||
# --no-binary psycopg), and rpmbuild needs redhat-rpm-config. The
|
||||
# buildfarm gets these from the @development-tools group; they are
|
||||
# listed individually here so the container stays small.
|
||||
dnf install -y \
|
||||
diffutils \
|
||||
expect \
|
||||
fakeroot \
|
||||
file \
|
||||
findutils \
|
||||
gcc \
|
||||
gcc-c++ \
|
||||
git \
|
||||
krb5-devel \
|
||||
libpq5-devel \
|
||||
make \
|
||||
openssl-devel \
|
||||
patch \
|
||||
postgresql18-devel \
|
||||
python3-devel \
|
||||
python3-pip \
|
||||
python3-sphinx \
|
||||
redhat-rpm-config \
|
||||
rpm-build \
|
||||
rpm-sign \
|
||||
unzip \
|
||||
wget \
|
||||
yum-utils \
|
||||
zlib-devel
|
||||
|
||||
# pkg/redhat/build.sh:229 shells out to yumdownloader, and a missing
|
||||
# one surfaces as a bare exit 127 twelve minutes into the build,
|
||||
# after the docs and the webpack bundle. Fail here instead, where the
|
||||
# message is obvious. dnf5 distributions (EL10, Fedora 41+) get
|
||||
# yumdownloader through yum-utils' compatibility shims rather than
|
||||
# from yum itself, so it is worth checking rather than assuming.
|
||||
command -v yumdownloader >/dev/null || {
|
||||
echo "ERROR: yumdownloader is not on PATH; pkg/redhat/build.sh needs it." >&2
|
||||
exit 1
|
||||
}
|
||||
|
||||
- name: Install the Sphinx extensions
|
||||
run: python3 -m pip install --break-system-packages sphinxcontrib-youtube || python3 -m pip install sphinxcontrib-youtube
|
||||
|
||||
- name: Install syft
|
||||
run: curl -sSfL https://raw.githubusercontent.com/anchore/syft/v1.52.0/install.sh | sh -s -- -b /usr/local/bin v1.52.0
|
||||
|
||||
- uses: actions/setup-node@v7
|
||||
with:
|
||||
node-version: '20.x'
|
||||
|
||||
- uses: actions/checkout@v7
|
||||
|
||||
# The container runs as root while the workspace is owned by the runner
|
||||
# user, so git refuses to operate on it ("detected dubious ownership").
|
||||
# The build shells out to git for the commit hash during `yarn run
|
||||
# bundle`, which fails with exit 128 without this.
|
||||
- name: Mark the workspace as a safe git directory
|
||||
run: git config --global --add safe.directory "${GITHUB_WORKSPACE}"
|
||||
|
||||
- name: Enable Corepack
|
||||
run: corepack enable
|
||||
|
||||
- name: Check the RPM build
|
||||
run: make redhat
|
||||
|
||||
- name: Archive the packages
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: pgadmin4-rpm-build-output-${{ matrix.name }}
|
||||
if-no-files-found: error
|
||||
path: dist/*.rpm
|
||||
@@ -1,15 +1,57 @@
|
||||
name: Check tarball build
|
||||
|
||||
on:
|
||||
# Callable from snapshot.yml, so that what gets published is the
|
||||
# same build that runs on every push rather than a second
|
||||
# definition of it that can drift.
|
||||
workflow_call:
|
||||
|
||||
push:
|
||||
branches: [ "master" ]
|
||||
paths:
|
||||
- 'web/**'
|
||||
# Not the message catalogues: a .po change cannot break a build or a
|
||||
# test, and check-translations already compiles them, which is the
|
||||
# check that would catch a malformed one.
|
||||
- '!web/pgadmin/translations/**'
|
||||
- 'docs/**'
|
||||
- 'pkg/src/**'
|
||||
- 'requirements.txt'
|
||||
- 'Makefile'
|
||||
- 'LICENSE'
|
||||
- 'README.md'
|
||||
- 'tools/setup-python-env.sh'
|
||||
- '.github/workflows/check-tarball-build.yml'
|
||||
pull_request:
|
||||
branches: [ "master" ]
|
||||
paths:
|
||||
- 'web/**'
|
||||
# Not the message catalogues: a .po change cannot break a build or a
|
||||
# test, and check-translations already compiles them, which is the
|
||||
# check that would catch a malformed one.
|
||||
- '!web/pgadmin/translations/**'
|
||||
- 'docs/**'
|
||||
- 'pkg/src/**'
|
||||
- 'requirements.txt'
|
||||
- 'Makefile'
|
||||
- 'LICENSE'
|
||||
- 'README.md'
|
||||
- 'tools/setup-python-env.sh'
|
||||
- '.github/workflows/check-tarball-build.yml'
|
||||
|
||||
workflow_dispatch:
|
||||
|
||||
concurrency:
|
||||
group: '${{ github.workflow }} @ ${{ github.event.pull_request.head.label || github.head_ref || github.ref }}'
|
||||
# The workflow's own name, spelled out rather than taken from
|
||||
# github.workflow, because in a reusable workflow that expression is
|
||||
# the CALLER's name. Every workflow Snapshot calls would otherwise
|
||||
# share one group and, with cancel-in-progress, cancel each other.
|
||||
# github.event_name is in the key because a called workflow inherits the
|
||||
# caller's ref: once this is on master, the nightly Snapshot's deb build
|
||||
# and an ordinary push's deb build would otherwise share a group and,
|
||||
# with cancel-in-progress, kill each other. A push landing mid-nightly
|
||||
# would cancel the snapshot's build and skip the publish entirely.
|
||||
group: 'check-tarball-build @ ${{ github.event.pull_request.head.label || github.head_ref || github.ref }} @ ${{ github.event_name }}'
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
@@ -17,7 +59,7 @@ jobs:
|
||||
runs-on: ubuntu-22.04
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v7
|
||||
|
||||
- name: Install platform dependencies
|
||||
run: |
|
||||
@@ -33,7 +75,8 @@ jobs:
|
||||
make src
|
||||
|
||||
- name: Archive source tarball
|
||||
uses: actions/upload-artifact@v4
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: pgadmin4-tarball-build-output
|
||||
if-no-files-found: error
|
||||
path: dist/*
|
||||
@@ -3,8 +3,16 @@ name: Check translations
|
||||
on:
|
||||
push:
|
||||
branches: [ "master" ]
|
||||
paths:
|
||||
- 'web/**'
|
||||
- 'Makefile'
|
||||
- '.github/workflows/check-translations.yml'
|
||||
pull_request:
|
||||
branches: [ "master" ]
|
||||
paths:
|
||||
- 'web/**'
|
||||
- 'Makefile'
|
||||
- '.github/workflows/check-translations.yml'
|
||||
|
||||
workflow_dispatch:
|
||||
|
||||
@@ -17,7 +25,7 @@ jobs:
|
||||
runs-on: ubuntu-22.04
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v7
|
||||
|
||||
- name: Install platform dependencies
|
||||
run: |
|
||||
|
||||
@@ -0,0 +1,149 @@
|
||||
# Replaces the pgadmin4-windows-x64-qa Jenkins job.
|
||||
#
|
||||
# This runs on a hosted runner, not a self-hosted one. Code signing in Make.bat
|
||||
# is conditional on PGADMIN_WINDOWS_CSC being set (see :SIGN_FILES,
|
||||
# :CREATE_INSTALLER and :VERIFY_SIGNATURE in Make.bat), so an unsigned build
|
||||
# check needs neither the Certum hardware token nor the machine it is plugged
|
||||
# into. Only the release and snapshot builds, which do sign, need the
|
||||
# self-hosted runner.
|
||||
#
|
||||
# Dependencies come from dpage/winpgbuild, which is exactly where the
|
||||
# buildfarm's winpgbuild-update job gets them, so the toolchain matches what
|
||||
# the release build uses rather than being an approximation of it.
|
||||
#
|
||||
# UNTESTED as written: unlike the macOS equivalent this has not yet had a
|
||||
# successful run, so expect the first few attempts to need adjustment.
|
||||
|
||||
name: Check Windows build
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ "master" ]
|
||||
paths:
|
||||
- 'Make.bat'
|
||||
- 'pkg/win32/**'
|
||||
- 'pkg/python-version.txt'
|
||||
- '.github/actions/install-pgbuild-deps/action.yml'
|
||||
# Which build of each dependency this compiles against, so a
|
||||
# change to it changes what the installer contains.
|
||||
- 'pkg/pgbuild-deps.lock'
|
||||
- 'web/**'
|
||||
# Not the message catalogues: a .po change cannot break a build or a
|
||||
# test, and check-translations already compiles them, which is the
|
||||
# check that would catch a malformed one.
|
||||
- '!web/pgadmin/translations/**'
|
||||
- 'docs/**'
|
||||
- 'requirements.txt'
|
||||
- '.github/workflows/check-windows-build.yml'
|
||||
|
||||
workflow_dispatch:
|
||||
|
||||
concurrency:
|
||||
# The workflow's own name, spelled out rather than taken from
|
||||
# github.workflow, because in a reusable workflow that expression is
|
||||
# the CALLER's name. Every workflow Snapshot calls would otherwise
|
||||
# share one group and, with cancel-in-progress, cancel each other.
|
||||
group: 'check-windows-build @ ${{ github.ref }}'
|
||||
cancel-in-progress: true
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
build-installer:
|
||||
runs-on: windows-2025
|
||||
timeout-minutes: 120
|
||||
|
||||
env:
|
||||
# The major version of the PostgreSQL release to pull from pgbuild,
|
||||
# matching DEFAULT_POSTGRESQL_VERSION on the buildfarm.
|
||||
POSTGRES_MAJOR: '18'
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v7
|
||||
|
||||
- uses: actions/setup-node@v7
|
||||
with:
|
||||
node-version: '20.x'
|
||||
|
||||
- name: Enable Corepack
|
||||
run: corepack enable
|
||||
|
||||
# None of these are on the hosted image. wget matters most: Make.bat's
|
||||
# Electron download is an unbounded retry loop (Make.bat:370), so without
|
||||
# it the build spins on error 9009 until the job times out rather than
|
||||
# failing. syft generates the SBOM, and its absence is silent, since
|
||||
# :GENERATE_SBOM redirects into sbom.json and returns 0 regardless. The
|
||||
# buildfarm's Windows worker installs all three via Chocolatey too.
|
||||
- name: Install Inno Setup, syft and wget
|
||||
run: choco install innosetup syft wget -y --no-progress
|
||||
|
||||
# From pgadmin-org/pgbuild, which supersedes dpage/winpgbuild and is
|
||||
# where the macOS dependencies come from too, so both platforms build
|
||||
# against one manifest of pinned versions. The archives unpack to
|
||||
# <DEPS_DIR>\postgresql and <DEPS_DIR>\krb5.
|
||||
# Unpacked under the runner's own temp directory rather than a fixed
|
||||
# path. C:\build64 was the obvious choice and the wrong one: it is what
|
||||
# the buildfarm's winpgbuild-update job manages on the Windows worker,
|
||||
# deleting and re-extracting postgresql and krb5 inside it. Since the
|
||||
# signing builds will run on that same machine as a self-hosted runner,
|
||||
# a shared path would have the two systems overwriting each other's
|
||||
# dependencies, and the resulting Jenkins failures would look nothing
|
||||
# like their cause.
|
||||
- name: Install the PostgreSQL and Kerberos dependencies
|
||||
id: deps
|
||||
uses: ./.github/actions/install-pgbuild-deps
|
||||
with:
|
||||
arch: x86_64
|
||||
postgres-major: ${{ env.POSTGRES_MAJOR }}
|
||||
|
||||
- name: Record the dependency location
|
||||
shell: pwsh
|
||||
run: '"DEPS_DIR=${{ steps.deps.outputs.prefix }}" | Out-File -FilePath $env:GITHUB_ENV -Append'
|
||||
|
||||
# The hosted image ships Visual Studio Enterprise at a path that differs
|
||||
# from the Community install Make.bat defaults to, and the redistributable
|
||||
# version directory changes with every VS update, so both are discovered
|
||||
# rather than hardcoded.
|
||||
- name: Locate the Visual Studio toolchain
|
||||
shell: pwsh
|
||||
run: |
|
||||
$vswhere = "${env:ProgramFiles(x86)}\Microsoft Visual Studio\Installer\vswhere.exe"
|
||||
$vs = & $vswhere -latest -products * -property installationPath
|
||||
if (-not $vs) { throw "Could not locate a Visual Studio installation" }
|
||||
Write-Output "Visual Studio: $vs"
|
||||
|
||||
$redist = Get-ChildItem "$vs\VC\Redist\MSVC" -Directory |
|
||||
Where-Object { Test-Path "$($_.FullName)\vc_redist.x64.exe" } |
|
||||
Sort-Object Name -Descending |
|
||||
Select-Object -First 1
|
||||
if (-not $redist) { throw "Could not locate vc_redist.x64.exe under $vs" }
|
||||
Write-Output "VC++ redistributable: $($redist.FullName)"
|
||||
|
||||
"VS_INSTALL_DIR=$vs" | Out-File -FilePath $env:GITHUB_ENV -Append
|
||||
"PGADMIN_VCREDIST_DIR=$($redist.FullName)" | Out-File -FilePath $env:GITHUB_ENV -Append
|
||||
|
||||
# psycopg[c] compiles against libpq, so the build needs the MSVC
|
||||
# environment. vcvars64.bat only sets it for its own process, hence
|
||||
# invoking it and Make.bat in a single cmd session, as the Jenkins job
|
||||
# does.
|
||||
#
|
||||
# No Python is set up for this job: Make.bat downloads the version named
|
||||
# in pkg/python-version.txt, so the build gets the same interpreter here
|
||||
# as it does on the signing host and on a developer's machine.
|
||||
- name: Check the installer build
|
||||
shell: cmd
|
||||
run: |
|
||||
SET "PGADMIN_POSTGRES_DIR=%DEPS_DIR%\postgresql"
|
||||
SET "PGADMIN_KRB5_DIR=%DEPS_DIR%\krb5"
|
||||
SET "PGADMIN_INNOTOOL_DIR=C:\Program Files (x86)\Inno Setup 6"
|
||||
|
||||
CALL "%VS_INSTALL_DIR%\VC\Auxiliary\Build\vcvars64.bat" || EXIT /b 1
|
||||
CALL Make.bat || EXIT /b 1
|
||||
|
||||
- name: Archive the installer
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: pgadmin4-windows-build-output
|
||||
if-no-files-found: error
|
||||
path: dist/*.exe
|
||||
@@ -0,0 +1,454 @@
|
||||
# Promote a tested release candidate to the download server.
|
||||
#
|
||||
# This is the irreversible one. Everything before it produces something that
|
||||
# can be rebuilt or deleted without anybody noticing; this puts packages in
|
||||
# front of every pgAdmin user, and within the hour they are on the PostgreSQL
|
||||
# mirror network and in the S3 archive, which has no --delete and therefore no
|
||||
# undo.
|
||||
#
|
||||
# So it builds nothing. It takes a candidate that already exists on the
|
||||
# development server, having been built, signed, indexed and installed from by
|
||||
# the acceptance test, and moves it across. If a candidate needs changing, the
|
||||
# answer is a new candidate rather than a repair applied here.
|
||||
#
|
||||
# The version is separate from the location deliberately: 2026-09-21-1 is a
|
||||
# build, 9.18 is what users will call it, and conflating them is how somebody
|
||||
# publishes the wrong tree under the right name.
|
||||
|
||||
name: Promote release
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
version:
|
||||
description: 'Version to publish, for example 9.18'
|
||||
required: true
|
||||
type: string
|
||||
location:
|
||||
description: 'Candidate to promote, for example 2026-09-21-1'
|
||||
required: true
|
||||
type: string
|
||||
confirm:
|
||||
description: 'Type the version again to confirm'
|
||||
required: true
|
||||
type: string
|
||||
container-tags:
|
||||
description: >-
|
||||
Space separated tags for the container, for example
|
||||
"9 9.18 9.18.0 latest". Required when scope is everything.
|
||||
required: false
|
||||
type: string
|
||||
default: ''
|
||||
scope:
|
||||
description: >-
|
||||
How much to do. "everything" is a real promotion. "files" stops
|
||||
before the website and the S3 archive, neither of which can be
|
||||
undone. "release-directory" stops before the package
|
||||
repositories as well, touching only v<VERSION>, which is the
|
||||
only setting that leaves the live repositories alone and so the
|
||||
only one safe to rehearse the download server with.
|
||||
"registries" is the other half: it skips the download server
|
||||
entirely and runs only the container re-tag and the chart push,
|
||||
so those can be exercised against a candidate tag without a
|
||||
release. It does not upload to PyPI, so that a rehearsal cannot
|
||||
publish a candidate's wheel. "pypi" runs only the PyPI upload,
|
||||
for finishing a promotion whose upload failed.
|
||||
type: choice
|
||||
required: true
|
||||
default: release-directory
|
||||
options:
|
||||
- release-directory
|
||||
- files
|
||||
- everything
|
||||
- registries
|
||||
- pypi
|
||||
|
||||
concurrency:
|
||||
group: 'promote-release'
|
||||
cancel-in-progress: false
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
# Every input is checked here, before either of the jobs that act on one
|
||||
# runs, because the checks used to live in promote and the registries scope
|
||||
# skips that job: under that scope the confirmation gate did not apply to
|
||||
# the step that moves the latest tag.
|
||||
check:
|
||||
runs-on: ubuntu-24.04
|
||||
|
||||
steps:
|
||||
- name: Check the request
|
||||
env:
|
||||
VERSION: ${{ inputs.version }}
|
||||
LOCATION: ${{ inputs.location }}
|
||||
CONFIRM: ${{ inputs.confirm }}
|
||||
TAGS: ${{ inputs.container-tags }}
|
||||
SCOPE: ${{ inputs.scope }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
|
||||
# Typed twice, because the cost of getting this wrong is a version
|
||||
# published from the wrong tree and no way to unpublish it.
|
||||
if [ "${VERSION}" != "${CONFIRM}" ]; then
|
||||
echo "The confirmation does not match the version." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# The same shapes the wrapper enforces, checked here so a mistake
|
||||
# costs a second rather than a round trip.
|
||||
printf '%s' "${VERSION}" | grep -Eq '^[0-9]{1,2}\.[0-9]{1,2}(\.[0-9]{1,2})?$' || {
|
||||
echo "Not a version: ${VERSION}" >&2; exit 1; }
|
||||
printf '%s' "${LOCATION}" | grep -Eq '^[0-9]{4}-[0-9]{2}-[0-9]{2}(-[1-9][0-9]?)?$' || {
|
||||
echo "Not a build location: ${LOCATION}" >&2; exit 1; }
|
||||
|
||||
# Only the scopes that publish containers need tags, but when they
|
||||
# are given they are checked whatever the scope, so a bad value is
|
||||
# not discovered halfway through a promotion.
|
||||
if [ "${SCOPE}" = "everything" ] || [ "${SCOPE}" = "registries" ]; then
|
||||
[ -n "${TAGS}" ] || {
|
||||
echo "container-tags is required for scope ${SCOPE}." >&2
|
||||
exit 1; }
|
||||
fi
|
||||
# -f, because an unquoted expansion is also a glob: a tag of * would
|
||||
# otherwise be replaced by whatever is in the working directory.
|
||||
set -f
|
||||
for tag in ${TAGS}; do
|
||||
printf '%s' "${tag}" | grep -Eq '^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$' || {
|
||||
echo "Not a container tag: ${tag}" >&2; exit 1; }
|
||||
done
|
||||
set +f
|
||||
|
||||
echo "Request accepted: ${VERSION} from ${LOCATION}, scope ${SCOPE}."
|
||||
|
||||
promote:
|
||||
needs: check
|
||||
# Skipped entirely when only the registries are being exercised, so a
|
||||
# rehearsal of those never touches the download server.
|
||||
if: ${{ inputs.scope != 'registries' && inputs.scope != 'pypi' }}
|
||||
runs-on: [self-hosted, publish]
|
||||
environment: publishing
|
||||
|
||||
steps:
|
||||
# The inputs are checked in the job above; this is about the runner.
|
||||
- name: Check the runner
|
||||
env:
|
||||
DOWNLOAD: ${{ vars.DOWNLOAD_SSH_TARGET }}
|
||||
PUBLISH_KEY: ${{ vars.PUBLISH_SSH_KEY || '~/.ssh/pga-publish' }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
|
||||
[ -n "${DOWNLOAD}" ] || { echo "DOWNLOAD_SSH_TARGET is not set." >&2; exit 1; }
|
||||
|
||||
# Both variables reach option positions: the target is the first
|
||||
# positional argument to every ssh below, and the key path follows
|
||||
# -i, so a value beginning with a hyphen would be read as an option
|
||||
# rather than as the host or the path it is meant to be. Only an
|
||||
# administrator can set a repository variable, which makes this
|
||||
# defence in depth rather than a live hole, but it is checked here,
|
||||
# once, before anything is published; the -- at each call site
|
||||
# closes the same gap from the other end.
|
||||
TARGET_RE='^[A-Za-z0-9][A-Za-z0-9._-]*(@[A-Za-z0-9][A-Za-z0-9.-]*)?$'
|
||||
printf '%s' "${DOWNLOAD}" | grep -Eq "${TARGET_RE}" || {
|
||||
echo "DOWNLOAD_SSH_TARGET is not a [user@]host." >&2; exit 1; }
|
||||
|
||||
KEY="${PUBLISH_KEY/#\~/$HOME}"
|
||||
printf '%s' "${KEY}" | grep -Eq '^/[A-Za-z0-9._/-]+$' || {
|
||||
echo "PUBLISH_SSH_KEY is not a plain absolute path." >&2
|
||||
exit 1; }
|
||||
[ -r "${KEY}" ] || { echo "Missing SSH key: ${KEY}" >&2; exit 1; }
|
||||
if head -c 4 "${KEY}" | grep -q '^ssh-'; then
|
||||
echo "${KEY} is a public key; -i needs the private one." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Checked before anything is created, so that a version published by
|
||||
# mistake is caught here rather than by release-create failing halfway
|
||||
# through and leaving a directory behind.
|
||||
- name: Refuse to republish
|
||||
env:
|
||||
VERSION: ${{ inputs.version }}
|
||||
DOWNLOAD: ${{ vars.DOWNLOAD_SSH_TARGET }}
|
||||
PUBLISH_KEY: ${{ vars.PUBLISH_SSH_KEY || '~/.ssh/pga-publish' }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
publish() {
|
||||
ssh -i "${PUBLISH_KEY/#\~/$HOME}" -o BatchMode=yes \
|
||||
-o StrictHostKeyChecking=yes -- "${DOWNLOAD}" "$@"
|
||||
}
|
||||
if publish release-exists "${VERSION}" 2>/dev/null; then
|
||||
echo "Version ${VERSION} is already published." >&2
|
||||
echo "Publishing is single-shot; a correction needs a new version." >&2
|
||||
exit 1
|
||||
fi
|
||||
echo "Version ${VERSION} is not yet published."
|
||||
|
||||
- name: Publish the release directory
|
||||
env:
|
||||
VERSION: ${{ inputs.version }}
|
||||
LOCATION: ${{ inputs.location }}
|
||||
DOWNLOAD: ${{ vars.DOWNLOAD_SSH_TARGET }}
|
||||
PUBLISH_KEY: ${{ vars.PUBLISH_SSH_KEY || '~/.ssh/pga-publish' }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
publish() {
|
||||
echo "+ $*"
|
||||
ssh -i "${PUBLISH_KEY/#\~/$HOME}" -o BatchMode=yes \
|
||||
-o StrictHostKeyChecking=yes -- "${DOWNLOAD}" "$@"
|
||||
}
|
||||
|
||||
# The downloads first: the directory, then the files sorted into it,
|
||||
# then the packages into the live repository trees. Nothing is
|
||||
# visible to a package manager until the indices are rebuilt below,
|
||||
# so an interruption here leaves packages that nothing points at
|
||||
# rather than a repository describing packages that are not there.
|
||||
publish release-create "${VERSION}"
|
||||
publish release-fetch "${VERSION}" "${LOCATION}"
|
||||
|
||||
# Separate from the step above because this is where a promotion stops
|
||||
# being reversible in practice. The step above creates one directory that
|
||||
# can simply be deleted; this copies packages into the live repositories
|
||||
# and re-signs the indices every client reads.
|
||||
- name: Publish the packages
|
||||
if: ${{ inputs.scope != 'release-directory' }}
|
||||
env:
|
||||
VERSION: ${{ inputs.version }}
|
||||
LOCATION: ${{ inputs.location }}
|
||||
DOWNLOAD: ${{ vars.DOWNLOAD_SSH_TARGET }}
|
||||
PUBLISH_KEY: ${{ vars.PUBLISH_SSH_KEY || '~/.ssh/pga-publish' }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
publish() {
|
||||
echo "+ $*"
|
||||
ssh -i "${PUBLISH_KEY/#\~/$HOME}" -o BatchMode=yes \
|
||||
-o StrictHostKeyChecking=yes -- "${DOWNLOAD}" "$@"
|
||||
}
|
||||
|
||||
# The auto-updater archives, which release-fetch deliberately left
|
||||
# behind because they belong outside the release directory.
|
||||
publish autoupdate-publish "${VERSION}"
|
||||
|
||||
publish packages-fetch "${LOCATION}"
|
||||
|
||||
# The indices, which is the point at which the new packages become
|
||||
# installable.
|
||||
for codename in bookworm trixie jammy noble resolute; do
|
||||
publish rebuild-apt "${codename}"
|
||||
done
|
||||
publish rebuild-yum redhat rhel 9 x86_64
|
||||
publish rebuild-yum redhat rhel 10 x86_64
|
||||
publish rebuild-yum fedora fedora 43 x86_64
|
||||
publish rebuild-yum fedora fedora 44 x86_64
|
||||
|
||||
# The platform tables carry the release history, so they change with
|
||||
# every release rather than only when a platform is added.
|
||||
publish rebuild-readme apt
|
||||
publish rebuild-readme yum
|
||||
|
||||
- name: Update the website
|
||||
if: ${{ inputs.scope == 'everything' }}
|
||||
env:
|
||||
VERSION: ${{ inputs.version }}
|
||||
DOWNLOAD: ${{ vars.DOWNLOAD_SSH_TARGET }}
|
||||
PUBLISH_KEY: ${{ vars.PUBLISH_SSH_KEY || '~/.ssh/pga-publish' }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
publish() {
|
||||
echo "+ $*"
|
||||
ssh -i "${PUBLISH_KEY/#\~/$HOME}" -o BatchMode=yes \
|
||||
-o StrictHostKeyChecking=yes -- "${DOWNLOAD}" "$@"
|
||||
}
|
||||
|
||||
# Separate from the packages, and after them, because the website
|
||||
# announcing a release whose files are not there yet is worse than
|
||||
# files sitting there unannounced for a minute.
|
||||
publish create-release "${VERSION}"
|
||||
publish load-docs "${VERSION}"
|
||||
publish purge-cache
|
||||
|
||||
- name: Archive to S3
|
||||
if: ${{ inputs.scope == 'everything' }}
|
||||
env:
|
||||
DOWNLOAD: ${{ vars.DOWNLOAD_SSH_TARGET }}
|
||||
PUBLISH_KEY: ${{ vars.PUBLISH_SSH_KEY || '~/.ssh/pga-publish' }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
# Last, and on its own, because it is the slowest step and the one
|
||||
# with no undo: the sync has no --delete, so anything it copies is
|
||||
# in the archive permanently.
|
||||
echo "+ sync-s3"
|
||||
ssh -i "${PUBLISH_KEY/#\~/$HOME}" -o BatchMode=yes \
|
||||
-o StrictHostKeyChecking=yes -- "${DOWNLOAD}" sync-s3
|
||||
|
||||
- name: Note what was skipped
|
||||
if: ${{ inputs.scope != 'everything' }}
|
||||
env:
|
||||
SCOPE: ${{ inputs.scope }}
|
||||
VERSION: ${{ inputs.version }}
|
||||
run: |
|
||||
echo "scope was ${SCOPE}, so this was not a full promotion."
|
||||
if [ "${SCOPE}" = "release-directory" ]; then
|
||||
echo "Only /var/ftp/pgadmin4/v${VERSION} was written. The package"
|
||||
echo "repositories, the website and the S3 archive are untouched,"
|
||||
echo "so removing that directory undoes this entirely."
|
||||
else
|
||||
echo "The website was not updated and nothing was archived to S3."
|
||||
echo "The packages are live and will reach the mirrors at the next"
|
||||
echo "hourly sync."
|
||||
fi
|
||||
|
||||
# The registries, which need none of the publishing keys and no self-hosted
|
||||
# runner:
|
||||
# nothing here touches our servers. Separate so that a Docker Hub outage
|
||||
# cannot leave a half-finished promotion on the download server, and so the
|
||||
# runner holding the SSH keys never handles registry credentials.
|
||||
registries:
|
||||
needs: [check, promote]
|
||||
# always(), because under the registries scope promote is skipped rather
|
||||
# than run, and a plain needs would skip this with it. check is never
|
||||
# skipped, so requiring its success is what keeps the inputs validated on
|
||||
# both paths.
|
||||
if: >-
|
||||
${{ always()
|
||||
&& needs.check.result == 'success'
|
||||
&& (inputs.scope == 'everything' || inputs.scope == 'registries'
|
||||
|| inputs.scope == 'pypi')
|
||||
&& (needs.promote.result == 'success'
|
||||
|| needs.promote.result == 'skipped') }}
|
||||
runs-on: ubuntu-24.04
|
||||
environment: publishing
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
# For PyPI trusted publishing, which exchanges this for a short-lived
|
||||
# upload credential rather than using a stored API token.
|
||||
id-token: write
|
||||
# For GHCR, which needs no stored credential at all.
|
||||
packages: write
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v7
|
||||
|
||||
# By commit rather than by tag, since this script is piped into a shell
|
||||
# in the same job that then logs in to two registries, and a tag can be
|
||||
# moved. bec5b06 is v4.3.0.
|
||||
- name: Install Helm
|
||||
run: |
|
||||
set -euo pipefail
|
||||
curl -fsSL https://raw.githubusercontent.com/helm/helm/bec5b06ed841fe5269972d864d5177944fd5970f/scripts/get-helm-3 -o get-helm-3
|
||||
chmod +x get-helm-3
|
||||
./get-helm-3
|
||||
|
||||
# Re-tagged rather than rebuilt, so what ships is the manifest the
|
||||
# acceptance test ran against. imagetools works registry-side, so
|
||||
# nothing is pulled or pushed through this runner.
|
||||
- name: Publish the container
|
||||
if: ${{ inputs.scope != 'pypi' }}
|
||||
env:
|
||||
LOCATION: ${{ inputs.location }}
|
||||
TAGS: ${{ inputs.container-tags }}
|
||||
DOCKERHUB_USERNAME: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
DOCKERHUB_TOKEN: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
GITHUB_ACTOR: ${{ github.actor }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
# Validated by the check job. -f so that the word splitting below
|
||||
# does not also glob against the workspace.
|
||||
set -f
|
||||
|
||||
printf '%s' "${DOCKERHUB_TOKEN}" \
|
||||
| docker login -u "${DOCKERHUB_USERNAME}" --password-stdin
|
||||
printf '%s' "${GITHUB_TOKEN}" \
|
||||
| docker login ghcr.io -u "${GITHUB_ACTOR}" --password-stdin
|
||||
|
||||
# Docker Hub stays canonical, because every tutorial, compose file
|
||||
# and manifest in the world names dpage/pgadmin4. GHCR is published
|
||||
# alongside it: organisation owned, no stored credential, and not
|
||||
# subject to Docker Hub's anonymous pull limits.
|
||||
for tag in ${TAGS}; do
|
||||
echo "+ ${LOCATION} -> :${tag}"
|
||||
docker buildx imagetools create \
|
||||
--tag "dpage/pgadmin4:${tag}" \
|
||||
--tag "ghcr.io/pgadmin-org/pgadmin4:${tag}" \
|
||||
"dpage/pgadmin4:${LOCATION}"
|
||||
done
|
||||
docker logout ghcr.io
|
||||
docker logout
|
||||
|
||||
# The chart carries the release version from pkg/helm/Chart.yaml, so
|
||||
# unlike the container there is no candidate copy to re-tag: it is
|
||||
# packaged and pushed once, here.
|
||||
#
|
||||
# To GHCR rather than Docker Hub. The chart's install base is a small
|
||||
# fraction of the container's, so moving it breaks far less than moving
|
||||
# the image would, oci://docker.io/dpage was an odd home for a chart in
|
||||
# the first place, and it needs no stored credential here.
|
||||
- name: Publish the Helm chart
|
||||
if: ${{ inputs.scope != 'pypi' }}
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
GITHUB_ACTOR: ${{ github.actor }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
printf '%s' "${GITHUB_TOKEN}" \
|
||||
| helm registry login ghcr.io -u "${GITHUB_ACTOR}" --password-stdin
|
||||
helm package pkg/helm
|
||||
helm push pgadmin4-helm-*.tgz oci://ghcr.io/pgadmin-org
|
||||
helm registry logout ghcr.io
|
||||
|
||||
# Fetched from the candidate rather than rebuilt, so what reaches PyPI
|
||||
# is the file the acceptance test installed. Not from the mirrors: they
|
||||
# sync from the download server hourly, so the release is not there
|
||||
# yet when this runs.
|
||||
- name: Collect the wheel
|
||||
if: ${{ inputs.scope == 'everything' || inputs.scope == 'pypi' }}
|
||||
env:
|
||||
VERSION: ${{ inputs.version }}
|
||||
LOCATION: ${{ inputs.location }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
mkdir -p pypi-dist
|
||||
BASE="https://developer.pgadmin.org/builds/${LOCATION}"
|
||||
WHEEL="pgadmin4-${VERSION}-py3-none-any.whl"
|
||||
curl -fsS -o "pypi-dist/${WHEEL}" "${BASE}/${WHEEL}"
|
||||
# Beside the wheel rather than in pypi-dist, because everything in
|
||||
# that directory is uploaded and PyPI has not accepted detached
|
||||
# signatures since 2023.
|
||||
curl -fsS -o "${WHEEL}.asc" "${BASE}/${WHEEL}.asc"
|
||||
ls -l pypi-dist
|
||||
|
||||
# The wheel arrives over TLS, which says which server sent it and
|
||||
# nothing about how it got there, and the signature that has been
|
||||
# sitting beside the file all along is the only thing that ties these
|
||||
# bytes back to the signing key. Checking it
|
||||
# costs a second and turns the last hop of a release into something an
|
||||
# attacker cannot quietly alter.
|
||||
- name: Verify the wheel before uploading it
|
||||
if: ${{ inputs.scope == 'everything' || inputs.scope == 'pypi' }}
|
||||
env:
|
||||
VERSION: ${{ inputs.version }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
curl -fsS -o packages_pgadmin_org.pub \
|
||||
https://www.pgadmin.org/static/packages_pgadmin_org.pub
|
||||
|
||||
# A keyring of its own, holding that one key and nothing the runner
|
||||
# image happened to trust already, so a signature made by any other
|
||||
# key fails for want of a public key rather than passing on the
|
||||
# strength of somebody else's.
|
||||
GNUPGHOME=$(mktemp -d)
|
||||
export GNUPGHOME
|
||||
gpg --batch --import packages_pgadmin_org.pub
|
||||
gpg --batch --verify "pgadmin4-${VERSION}-py3-none-any.whl.asc" \
|
||||
"pypi-dist/pgadmin4-${VERSION}-py3-none-any.whl"
|
||||
echo "Signature verified; the upload may proceed."
|
||||
|
||||
# Trusted publishing: PyPI verifies this workflow's OIDC identity, so
|
||||
# there is no API token to hold, rotate or leak.
|
||||
- name: Upload to PyPI
|
||||
if: ${{ inputs.scope == 'everything' || inputs.scope == 'pypi' }}
|
||||
uses: pypa/gh-action-pypi-publish@dc37677b2e1c63e2034f94d8a5b11f265b73ba33 # v1.14.2
|
||||
with:
|
||||
packages-dir: pypi-dist
|
||||
@@ -0,0 +1,54 @@
|
||||
# Purge the cache in front of the website.
|
||||
#
|
||||
# Promotion purges it once, but the website edits that follow a release, the
|
||||
# downloads being activated, the current version record and the news article,
|
||||
# all happen afterwards and are all cached. This is the button for that, and
|
||||
# for the times somebody edits the site outside a release.
|
||||
#
|
||||
# It takes no arguments: the verb behind it clears everything, and there is
|
||||
# nothing for a caller to choose.
|
||||
|
||||
name: Purge website cache
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
|
||||
concurrency:
|
||||
group: 'purge-website-cache'
|
||||
cancel-in-progress: true
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
purge:
|
||||
runs-on: [self-hosted, publish]
|
||||
environment: publishing
|
||||
|
||||
steps:
|
||||
- name: Purge
|
||||
env:
|
||||
DOWNLOAD: ${{ vars.DOWNLOAD_SSH_TARGET }}
|
||||
PUBLISH_KEY: ${{ vars.PUBLISH_SSH_KEY || '~/.ssh/pga-publish' }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
[ -n "${DOWNLOAD}" ] || { echo "DOWNLOAD_SSH_TARGET is not set." >&2; exit 1; }
|
||||
|
||||
# The target is the first positional argument to ssh and the key
|
||||
# path follows -i, so a value beginning with a hyphen would be read
|
||||
# as an option rather than as what it is meant to be. Only an
|
||||
# administrator can set a repository variable, so this is defence in
|
||||
# depth rather than a live hole, but it costs a line and rules out a
|
||||
# whole shape of mistake; the -- below does the same job from the
|
||||
# other direction.
|
||||
TARGET_RE='^[A-Za-z0-9][A-Za-z0-9._-]*(@[A-Za-z0-9][A-Za-z0-9.-]*)?$'
|
||||
printf '%s' "${DOWNLOAD}" | grep -Eq "${TARGET_RE}" || {
|
||||
echo "DOWNLOAD_SSH_TARGET is not a [user@]host." >&2; exit 1; }
|
||||
|
||||
KEY="${PUBLISH_KEY/#\~/$HOME}"
|
||||
printf '%s' "${KEY}" | grep -Eq '^/[A-Za-z0-9._/-]+$' || {
|
||||
echo "PUBLISH_SSH_KEY is not a plain absolute path." >&2
|
||||
exit 1; }
|
||||
|
||||
ssh -i "${KEY}" -o BatchMode=yes \
|
||||
-o StrictHostKeyChecking=yes -- "${DOWNLOAD}" purge-cache
|
||||
@@ -0,0 +1,439 @@
|
||||
# Release candidate builds, replacing the buildfarm's pgadmin4-*-build jobs.
|
||||
#
|
||||
# The same builds as the nightly snapshot, published to the development server
|
||||
# instead of the download server. That difference is the point: a snapshot is
|
||||
# finished when it is published, whereas a candidate is published in order to
|
||||
# be tested, and promoted to the download server only afterwards.
|
||||
#
|
||||
# developer.pgadmin.org serves this tree over HTTP as soon as it is written, so
|
||||
# a candidate's repositories can be added and installed from immediately,
|
||||
# rather than waiting on the hourly sync between the download server and the
|
||||
# mirrors.
|
||||
#
|
||||
# Never scheduled. A candidate exists because somebody decided to make one.
|
||||
|
||||
name: Release candidate
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
location:
|
||||
description: >-
|
||||
Build directory name, normally today's date. A second
|
||||
candidate on the same day needs a -N suffix, for example
|
||||
2026-09-17-2.
|
||||
required: false
|
||||
default: ''
|
||||
publish:
|
||||
description: Upload to the development server when the build succeeds
|
||||
type: boolean
|
||||
required: false
|
||||
default: false
|
||||
|
||||
|
||||
concurrency:
|
||||
# One candidate at a time, and never cancelled: a cancelled run can abandon a
|
||||
# notarisation already submitted to Apple, and leaves the signing token in an
|
||||
# unknown state.
|
||||
group: 'release-candidate'
|
||||
cancel-in-progress: false
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
# The directory name, decided once. Two jobs need it and they must agree:
|
||||
# the repo RPMs carry the candidate's own URL inside them, so if that job and
|
||||
# the assemble job each called date(1) a run straddling midnight would ship a
|
||||
# repository configuration pointing at a build that does not exist.
|
||||
location:
|
||||
runs-on: ubuntu-24.04
|
||||
outputs:
|
||||
name: ${{ steps.pick.outputs.name }}
|
||||
steps:
|
||||
- id: pick
|
||||
env:
|
||||
REQUESTED: ${{ inputs.location }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
NAME="${REQUESTED:-}"
|
||||
[ -n "${NAME}" ] || NAME=$(date +%Y-%m-%d)
|
||||
|
||||
# Validated here so that everything downstream, including a shell on
|
||||
# the machine holding the publishing keys, is working with a name
|
||||
# that cannot be anything but a date and an optional suffix. The
|
||||
# wrapper on the server applies its own rule to the same value; this
|
||||
# one exists so a bad input fails in five seconds rather than after
|
||||
# an hour of building.
|
||||
if ! printf '%s' "${NAME}" | grep -Eq '^[0-9]{4}-[0-9]{2}-[0-9]{2}(-[1-9][0-9]?)?$'; then
|
||||
echo "Not a valid build location: ${NAME}" >&2
|
||||
echo "Expected YYYY-MM-DD, optionally with a -N suffix." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "Build location: ${NAME}"
|
||||
echo "name=${NAME}" >> "${GITHUB_OUTPUT}"
|
||||
|
||||
deb:
|
||||
uses: ./.github/workflows/check-deb-build.yml
|
||||
rpm:
|
||||
uses: ./.github/workflows/check-rpm-build.yml
|
||||
tarball:
|
||||
uses: ./.github/workflows/check-tarball-build.yml
|
||||
wheel:
|
||||
uses: ./.github/workflows/check-python-build.yml
|
||||
docs:
|
||||
uses: ./.github/workflows/check-doc-builds.yml
|
||||
# The container, pushed to the registry under this candidate's location so
|
||||
# that it can be tested and then re-tagged at promotion. Not part of the
|
||||
# download tree, which is why it is not in assemble's needs.
|
||||
container:
|
||||
needs: location
|
||||
if: inputs.publish
|
||||
# The caller caps a called workflow's token: the top-level
|
||||
# 'contents: read' leaves 'packages: none', so the publish-container job's
|
||||
# 'packages: write' request must be granted here or the workflow is
|
||||
# rejected before it starts.
|
||||
permissions:
|
||||
contents: read
|
||||
packages: write
|
||||
uses: ./.github/workflows/check-container-build.yml
|
||||
with:
|
||||
publish: true
|
||||
location: ${{ needs.location.outputs.name }}
|
||||
secrets: inherit
|
||||
|
||||
# The Helm chart is built as a check but not published here. Its version
|
||||
# comes from pkg/helm/Chart.yaml, so pushing it would publish the release
|
||||
# version before the release, and the registry has no notion of a candidate
|
||||
# the way the download server does. It goes out at promotion.
|
||||
helm:
|
||||
uses: ./.github/workflows/check-helm-build.yml
|
||||
|
||||
# The repository configuration RPMs. These are not built by the QA
|
||||
# workflows, because they are not a build of pgAdmin: they are a one-file
|
||||
# package carrying a .repo pointing at the tree they ship in, which is why
|
||||
# the URL has to be known before they are built.
|
||||
repo-rpm:
|
||||
needs: location
|
||||
runs-on: ubuntu-24.04
|
||||
container: almalinux:9
|
||||
steps:
|
||||
- name: Install the build environment
|
||||
run: dnf install -y --setopt=install_weak_deps=False rpm-build python3 git
|
||||
|
||||
- uses: actions/checkout@v7
|
||||
|
||||
- name: Mark the workspace as a safe git directory
|
||||
run: git config --global --add safe.directory "${GITHUB_WORKSPACE}"
|
||||
|
||||
- name: Build the repository RPMs
|
||||
env:
|
||||
LOCATION: ${{ needs.location.outputs.name }}
|
||||
run: |
|
||||
# Through the environment rather than interpolated into the script.
|
||||
# The location is a dispatch input, so it is text somebody chose, and
|
||||
# workflow expression substitution happens before the shell ever sees the
|
||||
# line, so a crafted location would otherwise become shell text.
|
||||
export PGADMIN_REPO_DIR="https://developer.pgadmin.org/builds/${LOCATION}/yum"
|
||||
echo "Repositories will point at ${PGADMIN_REPO_DIR}"
|
||||
pkg/redhat/repo-rpm.sh
|
||||
|
||||
- name: Archive the repository RPMs
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: pgadmin4-repo-rpm-build-output
|
||||
if-no-files-found: error
|
||||
path: dist/*.noarch.rpm
|
||||
|
||||
# snapshot-macos.yml and snapshot-windows.yml are the signed-build
|
||||
# workflows rather than snapshot-only ones; the name predates there being
|
||||
# a second caller.
|
||||
macos:
|
||||
uses: ./.github/workflows/snapshot-macos.yml
|
||||
secrets: inherit
|
||||
|
||||
windows:
|
||||
uses: ./.github/workflows/snapshot-windows.yml
|
||||
secrets: inherit
|
||||
|
||||
assemble:
|
||||
needs: [location, deb, rpm, tarball, wheel, docs, repo-rpm, macos, windows]
|
||||
|
||||
# the runner, not a hosted runner. This job assembles a tree of several
|
||||
# hundred megabytes and then, when publishing, sends it to the development
|
||||
# server; splitting the two meant uploading that tree as an artifact and
|
||||
# immediately downloading it again on the runner, having first queued for a
|
||||
# hosted runner behind whatever else the organisation was building.
|
||||
runs-on: [self-hosted, publish]
|
||||
|
||||
# For the variables naming the keys and the target, which live here. The
|
||||
# environment carries no protection rules, so a run that is not publishing
|
||||
# is not gated by it.
|
||||
environment: publishing
|
||||
|
||||
# Everything must have built except Windows. That is the one runner which
|
||||
# can legitimately be offline, and a candidate without the installer is
|
||||
# still worth having; a candidate missing its documentation, source tarball,
|
||||
# wheel or repository RPMs is not, and the repository RPMs are worse than
|
||||
# missing, since install-repo-readme.py then fails after the packages have
|
||||
# been uploaded and signed, leaving half a candidate on the server.
|
||||
if: >-
|
||||
always()
|
||||
&& needs.location.result == 'success'
|
||||
&& needs.deb.result == 'success'
|
||||
&& needs.rpm.result == 'success'
|
||||
&& needs.tarball.result == 'success'
|
||||
&& needs.wheel.result == 'success'
|
||||
&& needs.docs.result == 'success'
|
||||
&& needs.repo-rpm.result == 'success'
|
||||
&& needs.macos.result == 'success'
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v7
|
||||
|
||||
# Only the build outputs, which are all named pgadmin4-*. The container
|
||||
# build also leaves a 'pgadmin-org~pgadmin4~*.dockerbuild' build-record
|
||||
# artifact that docker/build-push-action uploads; it is not a release
|
||||
# file, and it is not stored as a plain zip, so download-artifact fails
|
||||
# to extract it and, downloading everything, fails the whole step.
|
||||
- name: Collect the build artefacts
|
||||
uses: actions/download-artifact@v7
|
||||
with:
|
||||
path: artefacts
|
||||
pattern: pgadmin4-*
|
||||
|
||||
# The layout is the one the publishing scripts and the download site
|
||||
# expect, and is what the wrapper's stage-create verb creates on the
|
||||
# staging server.
|
||||
# The Debian architecture comes from the package filename rather than a
|
||||
# table, since dpkg already encodes it there; the codename does not, so
|
||||
# that mapping is explicit.
|
||||
- name: Assemble the staging tree
|
||||
env:
|
||||
LOCATION: ${{ needs.location.outputs.name }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
# Flat, beside apt/ and yum/, which is how both the staging server
|
||||
# and the snapshots tree are laid out. The docs/macos/pip/source/
|
||||
# windows subdivision belongs to a published release, and it is the
|
||||
# promotion step that sorts files into it on the way there.
|
||||
# Explicitly, rather than trusting the checkout to have cleaned it.
|
||||
# This runs on a self-hosted runner whose workspace persists between
|
||||
# jobs, and a previous run's tree left behind here is indistinguishable
|
||||
# from this one's until something downstream has to choose between
|
||||
# them.
|
||||
rm -rf stage
|
||||
|
||||
STAGE="stage/${LOCATION}"
|
||||
mkdir -p "${STAGE}/yum"
|
||||
|
||||
# An artefact can be legitimately absent: the Windows signing host is
|
||||
# allowed to be offline, and a failed matrix leg is reported by its
|
||||
# own job rather than by a confusing cp error here.
|
||||
copy_into() {
|
||||
DEST=$1; shift
|
||||
for SRC in "$@"; do
|
||||
[ -e "${SRC}" ] || continue
|
||||
mkdir -p "${DEST}"
|
||||
cp "${SRC}" "${DEST}/"
|
||||
done
|
||||
}
|
||||
|
||||
codename_for() {
|
||||
case "$1" in
|
||||
debian-12) echo bookworm ;;
|
||||
debian-13) echo trixie ;;
|
||||
ubuntu-2204) echo jammy ;;
|
||||
ubuntu-2404) echo noble ;;
|
||||
ubuntu-2604) echo resolute ;;
|
||||
*) echo "Unknown Debian build name: $1" >&2; exit 1 ;;
|
||||
esac
|
||||
}
|
||||
|
||||
for DIR in artefacts/pgadmin4-deb-build-output-*; do
|
||||
[ -d "${DIR}" ] || continue
|
||||
CODENAME=$(codename_for "${DIR##*-output-}")
|
||||
for DEB in "${DIR}"/*.deb; do
|
||||
ARCH=$(basename "${DEB}" .deb); ARCH=${ARCH##*_}
|
||||
TARGET="${STAGE}/apt/${CODENAME}/dists/pgadmin4/main/binary-${ARCH}"
|
||||
mkdir -p "${TARGET}"
|
||||
cp "${DEB}" "${TARGET}/"
|
||||
done
|
||||
done
|
||||
|
||||
# RPM trees are named for the distribution, which the build name
|
||||
# already carries: el-9 becomes redhat/rhel-9-x86_64.
|
||||
for DIR in artefacts/pgadmin4-rpm-build-output-*; do
|
||||
[ -d "${DIR}" ] || continue
|
||||
NAME=${DIR##*-output-}
|
||||
case "${NAME}" in
|
||||
el-*) FAMILY=redhat; TREE="rhel-${NAME#el-}" ;;
|
||||
fedora-*) FAMILY=fedora; TREE="${NAME}" ;;
|
||||
*) echo "Unknown RPM build name: ${NAME}" >&2; exit 1 ;;
|
||||
esac
|
||||
# Every RPM for a distribution goes in that distribution's
|
||||
# tree, including the noarch ones. Taking the directory from the
|
||||
# package's own architecture would file pgadmin4 and
|
||||
# pgadmin4-web, both noarch, under rhel-9-noarch, where no
|
||||
# client looks: dnf asks for <name>-<version>-$basearch. The
|
||||
# RPM matrix is x86_64 only for now, so this is a constant; when
|
||||
# aarch64 arrives it becomes a property of the build.
|
||||
TARGET="${STAGE}/yum/${FAMILY}/${TREE}-x86_64"
|
||||
mkdir -p "${TARGET}"
|
||||
cp "${DIR}"/*.rpm "${TARGET}/"
|
||||
done
|
||||
|
||||
copy_into "${STAGE}" artefacts/pgadmin4-tarball-build-output/*
|
||||
copy_into "${STAGE}" artefacts/pgadmin4-python-build-output/*
|
||||
# The version is read once here and used for the documentation
|
||||
# filenames, which the build leaves unversioned as pgadmin4.pdf and
|
||||
# pgAdmin4.epub whilst the download site has always carried
|
||||
# pgadmin4-<version>.pdf and .epub.
|
||||
VERSION=$(python3 -c "import sys; sys.path.insert(0, 'web'); import version; print(version.APP_VERSION)")
|
||||
|
||||
for SRC in artefacts/pgadmin4-pdf-build-output/*.pdf; do
|
||||
[ -e "${SRC}" ] && cp "${SRC}" "${STAGE}/pgadmin4-${VERSION}.pdf"
|
||||
done
|
||||
for SRC in artefacts/pgadmin4-epub-build-output/*.epub; do
|
||||
[ -e "${SRC}" ] && cp "${SRC}" "${STAGE}/pgadmin4-${VERSION}.epub"
|
||||
done
|
||||
|
||||
# The docs tarball is what load-docs.sh unpacks on the web server, so
|
||||
# it ships as a tarball rather than as the directory of files the
|
||||
# build produces.
|
||||
if [ -d artefacts/pgadmin4-html-build-output ]; then
|
||||
# Under a pgadmin4-<version>-docs/ directory, not flat. The
|
||||
# web server's load-docs.sh unpacks this into the docs root
|
||||
# and then hands docloader that directory name, so a flat
|
||||
# archive would scatter several hundred files across the root
|
||||
# and then fail on a directory that does not exist. Every
|
||||
# released tarball has carried that top level; the tar here
|
||||
# had not.
|
||||
DOCS_DIR="pgadmin4-${VERSION}-docs"
|
||||
rm -rf "artefacts/${DOCS_DIR}"
|
||||
mv artefacts/pgadmin4-html-build-output "artefacts/${DOCS_DIR}"
|
||||
tar -czf "${STAGE}/pgadmin4-${VERSION}-docs.tar.gz" \
|
||||
-C artefacts "${DOCS_DIR}"
|
||||
fi
|
||||
|
||||
# At the root of yum/, beside the family directories, which is
|
||||
# where the README instructions tell people to fetch them from.
|
||||
copy_into "${STAGE}/yum" artefacts/pgadmin4-repo-rpm-build-output/*
|
||||
|
||||
copy_into "${STAGE}" artefacts/pgadmin4-macos-snapshot-*/*
|
||||
copy_into "${STAGE}" artefacts/pgadmin4-windows-snapshot/*
|
||||
|
||||
find "${STAGE}" -type f | sort
|
||||
|
||||
- name: Check the keys are present
|
||||
if: inputs.publish
|
||||
env:
|
||||
STAGING: ${{ vars.STAGING_SSH_TARGET }}
|
||||
PUBLISH_KEY: ${{ vars.PUBLISH_SSH_KEY || '~/.ssh/pga-publish' }}
|
||||
UPLOAD_KEY: ${{ vars.UPLOAD_SSH_KEY || '~/.ssh/pga-upload' }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
for TOOL in ssh rsync; do
|
||||
command -v "${TOOL}" >/dev/null || {
|
||||
echo "This runner has no ${TOOL}." >&2; exit 1; }
|
||||
done
|
||||
|
||||
# These three variables reach option positions. The target is the
|
||||
# first positional argument to ssh, and the key paths are spliced
|
||||
# into the -e string that rsync splits on whitespace before exec'ing
|
||||
# it, so a value beginning with a hyphen, or one carrying a space,
|
||||
# would be read as an option or as a second word rather than as the
|
||||
# host or the path it is meant to be. Only an administrator can set
|
||||
# a repository variable, which makes this defence in depth rather
|
||||
# than a live hole, but it is checked here, once, rather than at
|
||||
# each point of use; the -- in the publish step below closes the
|
||||
# same gap from the other end.
|
||||
TARGET_RE='^[A-Za-z0-9][A-Za-z0-9._-]*(@[A-Za-z0-9][A-Za-z0-9.-]*)?$'
|
||||
printf '%s' "${STAGING}" | grep -Eq "${TARGET_RE}" || {
|
||||
echo "STAGING_SSH_TARGET is not a [user@]host." >&2; exit 1; }
|
||||
|
||||
for KEY in "${PUBLISH_KEY/#\~/$HOME}" "${UPLOAD_KEY/#\~/$HOME}"; do
|
||||
printf '%s' "${KEY}" | grep -Eq '^/[A-Za-z0-9._/-]+$' || {
|
||||
echo "Not a plain absolute path: ${KEY}" >&2; exit 1; }
|
||||
if [ ! -r "${KEY}" ]; then
|
||||
echo "Missing or unreadable SSH key: ${KEY}" >&2
|
||||
exit 1
|
||||
fi
|
||||
# A public key is readable too, and pointing -i at one fails
|
||||
# later inside ssh with a message that does not mention which
|
||||
# file was wrong. Both halves of a pair sit in the same
|
||||
# directory with names one suffix apart, so this is an easy
|
||||
# thing to configure wrongly and a cheap thing to catch.
|
||||
if head -c 4 "${KEY}" | grep -q '^ssh-'; then
|
||||
echo "${KEY} is a public key; -i needs the private one." >&2
|
||||
echo "Drop the .pub suffix from the variable that names it." >&2
|
||||
exit 1
|
||||
fi
|
||||
done
|
||||
if [ ! -r ~/.ssh/known_hosts ]; then
|
||||
echo "This runner has no ~/.ssh/known_hosts, so host key" >&2
|
||||
echo "checking would fail. Add the servers' host keys." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
- name: Publish the candidate
|
||||
if: inputs.publish
|
||||
env:
|
||||
STAGING: ${{ vars.STAGING_SSH_TARGET }}
|
||||
LOCATION: ${{ inputs.location }}
|
||||
PUBLISH_KEY: ${{ vars.PUBLISH_SSH_KEY || '~/.ssh/pga-publish' }}
|
||||
UPLOAD_KEY: ${{ vars.UPLOAD_SSH_KEY || '~/.ssh/pga-upload' }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
if [ -z "${STAGING}" ]; then
|
||||
echo "The STAGING_SSH_TARGET variable is not set." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# One directory came out of assemble, named for the date. An explicit
|
||||
# location overrides it so that a test run does not land on the
|
||||
# build the buildfarm is still writing on the same server.
|
||||
# assemble produces exactly one directory, named for the date.
|
||||
# Anything else means the artefact is not what this step thinks it
|
||||
# is, and guessing which one to publish is not a decision to make at
|
||||
# three in the morning.
|
||||
if [ "$(find stage -mindepth 1 -maxdepth 1 -type d | wc -l)" != "1" ]; then
|
||||
echo "Expected one directory in the build artefact:" >&2
|
||||
ls -l stage >&2
|
||||
exit 1
|
||||
fi
|
||||
BUILT=$(basename "$(find stage -mindepth 1 -maxdepth 1 -type d)")
|
||||
DEST=${LOCATION:-${BUILT}}
|
||||
echo "Publishing ${BUILT} as ${DEST}"
|
||||
|
||||
publish() {
|
||||
ssh -i "${PUBLISH_KEY/#\~/$HOME}" -o BatchMode=yes \
|
||||
-o StrictHostKeyChecking=yes -- "${STAGING}" "$@"
|
||||
}
|
||||
|
||||
publish stage-create "${DEST}"
|
||||
|
||||
# No --delete, deliberately: rrsync refuses it anyway, and a
|
||||
# publishing step that can remove things is a publishing step that
|
||||
# can remove the wrong things.
|
||||
rsync -rlt --safe-links --no-specials --no-devices \
|
||||
-e "ssh -i ${UPLOAD_KEY/#\~/$HOME} -o BatchMode=yes -o StrictHostKeyChecking=yes" \
|
||||
"stage/${BUILT}/" "${STAGING}:${DEST}/"
|
||||
|
||||
# Signing before indexing: the metadata has to describe the signed
|
||||
# packages rather than the unsigned ones just uploaded.
|
||||
publish stage-sign "${DEST}"
|
||||
publish stage-index-apt "${DEST}"
|
||||
publish stage-index-yum "${DEST}"
|
||||
|
||||
# Installing from the candidate, which is the only check that proves a user
|
||||
# could have. It gates nothing automatically yet, promotion still being a
|
||||
# human decision, but a red result here is a candidate that must not be
|
||||
# promoted.
|
||||
acceptance:
|
||||
needs: [location, assemble]
|
||||
if: inputs.publish
|
||||
uses: ./.github/workflows/acceptance-test.yml
|
||||
with:
|
||||
base-url: https://developer.pgadmin.org/builds/${{ needs.location.outputs.name }}
|
||||
@@ -3,8 +3,28 @@ name: Run feature tests on EPAS
|
||||
on:
|
||||
push:
|
||||
branches: [ "master" ]
|
||||
paths:
|
||||
- 'web/**'
|
||||
# Not the message catalogues: a .po change cannot break a build or a
|
||||
# test, and check-translations already compiles them, which is the
|
||||
# check that would catch a malformed one.
|
||||
- '!web/pgadmin/translations/**'
|
||||
- 'requirements.txt'
|
||||
- 'Makefile'
|
||||
- 'tools/setup-python-env.sh'
|
||||
- '.github/workflows/run-feature-tests-epas.yml'
|
||||
pull_request:
|
||||
branches: [ "master" ]
|
||||
paths:
|
||||
- 'web/**'
|
||||
# Not the message catalogues: a .po change cannot break a build or a
|
||||
# test, and check-translations already compiles them, which is the
|
||||
# check that would catch a malformed one.
|
||||
- '!web/pgadmin/translations/**'
|
||||
- 'requirements.txt'
|
||||
- 'Makefile'
|
||||
- 'tools/setup-python-env.sh'
|
||||
- '.github/workflows/run-feature-tests-epas.yml'
|
||||
|
||||
workflow_dispatch:
|
||||
|
||||
@@ -26,7 +46,7 @@ jobs:
|
||||
runs-on: ubuntu-22.04
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v7
|
||||
|
||||
- name: Setup the EDB APT repo on Linux
|
||||
run: |
|
||||
@@ -150,7 +170,7 @@ jobs:
|
||||
- name: Build the JS bundle
|
||||
run: |
|
||||
cd web
|
||||
yarn install
|
||||
yarn install --immutable
|
||||
yarn run bundle
|
||||
|
||||
- name: Run the tests
|
||||
@@ -163,21 +183,21 @@ jobs:
|
||||
|
||||
- name: Archive server log
|
||||
if: success() || failure()
|
||||
uses: actions/upload-artifact@v4
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: server-log-pg${{ matrix.pgver }}
|
||||
path: var/pgadmin4.log
|
||||
|
||||
- name: Archive regression log
|
||||
if: success() || failure()
|
||||
uses: actions/upload-artifact@v4
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: regression-log-pg${{ matrix.pgver }}
|
||||
path: web/regression/regression.log
|
||||
|
||||
- name: Archive screenshots
|
||||
if: success() || failure()
|
||||
uses: actions/upload-artifact@v4
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: screenshots-pg${{ matrix.pgver }}
|
||||
path: web/regression/screenshots
|
||||
|
||||
@@ -6,8 +6,28 @@ name: Run feature tests on PostgreSQL
|
||||
on:
|
||||
push:
|
||||
branches: [ "master" ]
|
||||
paths:
|
||||
- 'web/**'
|
||||
# Not the message catalogues: a .po change cannot break a build or a
|
||||
# test, and check-translations already compiles them, which is the
|
||||
# check that would catch a malformed one.
|
||||
- '!web/pgadmin/translations/**'
|
||||
- 'requirements.txt'
|
||||
- 'Makefile'
|
||||
- 'tools/setup-python-env.sh'
|
||||
- '.github/workflows/run-feature-tests-pg.yml'
|
||||
pull_request:
|
||||
branches: [ "master" ]
|
||||
paths:
|
||||
- 'web/**'
|
||||
# Not the message catalogues: a .po change cannot break a build or a
|
||||
# test, and check-translations already compiles them, which is the
|
||||
# check that would catch a malformed one.
|
||||
- '!web/pgadmin/translations/**'
|
||||
- 'requirements.txt'
|
||||
- 'Makefile'
|
||||
- 'tools/setup-python-env.sh'
|
||||
- '.github/workflows/run-feature-tests-pg.yml'
|
||||
|
||||
workflow_dispatch:
|
||||
|
||||
@@ -25,7 +45,7 @@ jobs:
|
||||
runs-on: ubuntu-22.04
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v7
|
||||
|
||||
- name: Setup the PGDG APT repo
|
||||
run: |
|
||||
@@ -160,7 +180,7 @@ jobs:
|
||||
- name: Build the JS bundle
|
||||
run: |
|
||||
cd web
|
||||
yarn install
|
||||
yarn install --immutable
|
||||
yarn run bundle
|
||||
|
||||
- name: Run the tests
|
||||
@@ -173,21 +193,21 @@ jobs:
|
||||
|
||||
- name: Archive server log
|
||||
if: success() || failure()
|
||||
uses: actions/upload-artifact@v4
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: server-log-pg${{ matrix.pgver }}
|
||||
path: var/pgadmin4.log
|
||||
|
||||
- name: Archive regression log
|
||||
if: success() || failure()
|
||||
uses: actions/upload-artifact@v4
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: regression-log-pg${{ matrix.pgver }}
|
||||
path: web/regression/regression.log
|
||||
|
||||
- name: Archive screenshots
|
||||
if: success() || failure()
|
||||
uses: actions/upload-artifact@v4
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: screenshots-pg${{ matrix.pgver }}
|
||||
path: web/regression/screenshots
|
||||
|
||||
@@ -3,8 +3,30 @@ name: Run Javascript tests
|
||||
on:
|
||||
push:
|
||||
branches: [ "master" ]
|
||||
paths:
|
||||
- 'web/**/*.js'
|
||||
- 'web/**/*.jsx'
|
||||
- 'web/package.json'
|
||||
- 'web/yarn.lock'
|
||||
- 'web/jest.config.js'
|
||||
- 'web/webpack.shim.js'
|
||||
- 'web/regression/javascript/**'
|
||||
- 'web/.yarnrc.yml'
|
||||
- 'web/babel.config.json'
|
||||
- '.github/workflows/run-javascript-tests.yml'
|
||||
pull_request:
|
||||
branches: [ "master" ]
|
||||
paths:
|
||||
- 'web/**/*.js'
|
||||
- 'web/**/*.jsx'
|
||||
- 'web/package.json'
|
||||
- 'web/yarn.lock'
|
||||
- 'web/jest.config.js'
|
||||
- 'web/webpack.shim.js'
|
||||
- 'web/regression/javascript/**'
|
||||
- 'web/.yarnrc.yml'
|
||||
- 'web/babel.config.json'
|
||||
- '.github/workflows/run-javascript-tests.yml'
|
||||
|
||||
workflow_dispatch:
|
||||
|
||||
@@ -22,11 +44,11 @@ jobs:
|
||||
runs-on: ${{ matrix.os }}
|
||||
|
||||
steps:
|
||||
- uses: actions/setup-node@v4
|
||||
- uses: actions/setup-node@v7
|
||||
with:
|
||||
node-version: '20.x'
|
||||
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v7
|
||||
|
||||
# Corepack reads packageManager from web/package.json and provisions
|
||||
# exactly that Yarn: a single pinned download, with nothing written
|
||||
@@ -37,7 +59,7 @@ jobs:
|
||||
- name: Install Node modules
|
||||
run: |
|
||||
cd web
|
||||
yarn install
|
||||
yarn install --immutable
|
||||
|
||||
- name: Run the tests
|
||||
run: |
|
||||
|
||||
@@ -13,8 +13,30 @@ name: Run Python tests on EPAS
|
||||
on:
|
||||
push:
|
||||
branches: [ "master" ]
|
||||
paths:
|
||||
- 'web/**'
|
||||
# Not the message catalogues: a .po change cannot break a build or a
|
||||
# test, and check-translations already compiles them, which is the
|
||||
# check that would catch a malformed one.
|
||||
- '!web/pgadmin/translations/**'
|
||||
- 'requirements.txt'
|
||||
- 'Makefile'
|
||||
- 'tools/setup-python-env.sh'
|
||||
- 'tools/get_sb_package.py'
|
||||
- '.github/workflows/run-python-tests-epas.yml'
|
||||
pull_request:
|
||||
branches: [ "master" ]
|
||||
paths:
|
||||
- 'web/**'
|
||||
# Not the message catalogues: a .po change cannot break a build or a
|
||||
# test, and check-translations already compiles them, which is the
|
||||
# check that would catch a malformed one.
|
||||
- '!web/pgadmin/translations/**'
|
||||
- 'requirements.txt'
|
||||
- 'Makefile'
|
||||
- 'tools/setup-python-env.sh'
|
||||
- 'tools/get_sb_package.py'
|
||||
- '.github/workflows/run-python-tests-epas.yml'
|
||||
|
||||
workflow_dispatch:
|
||||
|
||||
@@ -48,10 +70,10 @@ jobs:
|
||||
runs-on: ${{ matrix.os }}
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v7
|
||||
|
||||
- name: Update python version
|
||||
uses: actions/setup-python@v5
|
||||
uses: actions/setup-python@v7
|
||||
with:
|
||||
python-version: '3.10'
|
||||
|
||||
@@ -311,14 +333,14 @@ jobs:
|
||||
|
||||
- name: Archive server log
|
||||
if: success() || failure()
|
||||
uses: actions/upload-artifact@v4
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: server-log-epas-${{ matrix.os }}-pg${{ matrix.pgver }}
|
||||
path: var/pgadmin4.log
|
||||
|
||||
- name: Archive regression log
|
||||
if: success() || failure()
|
||||
uses: actions/upload-artifact@v4
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: regression-log-epas-${{ matrix.os }}-pg${{ matrix.pgver }}
|
||||
path: web/regression/regression.log
|
||||
@@ -6,10 +6,37 @@ name: Run Python tests on PostgreSQL
|
||||
on:
|
||||
push:
|
||||
branches: [ "master" ]
|
||||
paths:
|
||||
- 'web/**'
|
||||
# Not the message catalogues: a .po change cannot break a build or a
|
||||
# test, and check-translations already compiles them, which is the
|
||||
# check that would catch a malformed one.
|
||||
- '!web/pgadmin/translations/**'
|
||||
- 'requirements.txt'
|
||||
- 'Makefile'
|
||||
- 'tools/setup-python-env.sh'
|
||||
- 'tools/get_sb_package.py'
|
||||
- '.github/workflows/run-python-tests-pg.yml'
|
||||
pull_request:
|
||||
branches: [ "master" ]
|
||||
paths:
|
||||
- 'web/**'
|
||||
# Not the message catalogues: a .po change cannot break a build or a
|
||||
# test, and check-translations already compiles them, which is the
|
||||
# check that would catch a malformed one.
|
||||
- '!web/pgadmin/translations/**'
|
||||
- 'requirements.txt'
|
||||
- 'Makefile'
|
||||
- 'tools/setup-python-env.sh'
|
||||
- 'tools/get_sb_package.py'
|
||||
- '.github/workflows/run-python-tests-pg.yml'
|
||||
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
pg_timetable_version:
|
||||
description: 'pg_timetable version to install for the tests'
|
||||
required: false
|
||||
type: string
|
||||
|
||||
concurrency:
|
||||
group: '${{ github.workflow }} @ ${{ github.event.pull_request.head.label || github.head_ref || github.ref }}'
|
||||
@@ -17,6 +44,8 @@ concurrency:
|
||||
|
||||
jobs:
|
||||
run-python-tests-pg:
|
||||
env:
|
||||
PG_TIMETABLE_VERSION: ${{ inputs.pg_timetable_version || '6.3.0' }}
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
@@ -26,10 +55,10 @@ jobs:
|
||||
runs-on: ${{ matrix.os }}
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v7
|
||||
|
||||
- name: Update python version
|
||||
uses: actions/setup-python@v5
|
||||
uses: actions/setup-python@v7
|
||||
with:
|
||||
python-version: '3.10'
|
||||
|
||||
@@ -143,6 +172,38 @@ jobs:
|
||||
|
||||
psql postgres -p 59${{ matrix.pgver }} -c 'CREATE ROLE postgres SUPERUSER LOGIN;'
|
||||
|
||||
# pg_timetable tables are created by the agent and need the agent to test run_now.
|
||||
- name: Install and start pg_timetable on Linux
|
||||
if: ${{ matrix.os == 'ubuntu-22.04' }}
|
||||
run: |
|
||||
wget --quiet -O /tmp/pg_timetable.tar.gz https://github.com/cybertec-postgresql/pg_timetable/releases/download/v${{ env.PG_TIMETABLE_VERSION }}/pg_timetable_Linux_x86_64.tar.gz
|
||||
tar -xzf /tmp/pg_timetable.tar.gz -C /tmp --strip-components=1
|
||||
/tmp/pg_timetable --init --clientname=runner "host=/var/run/postgresql port=59${{ matrix.pgver }} dbname=postgres user=postgres"
|
||||
nohup /tmp/pg_timetable --clientname=runner "host=/var/run/postgresql port=59${{ matrix.pgver }} dbname=postgres user=postgres" > /tmp/pg_timetable.log 2>&1 &
|
||||
echo $! > /tmp/pg_timetable.pid
|
||||
sleep 2
|
||||
|
||||
- name: Install and start pg_timetable on macOS
|
||||
if: ${{ matrix.os == 'macos-latest' }}
|
||||
run: |
|
||||
curl -sSL -o /tmp/pg_timetable.tar.gz https://github.com/cybertec-postgresql/pg_timetable/releases/download/v${{ env.PG_TIMETABLE_VERSION }}/pg_timetable_Darwin_arm64.tar.gz
|
||||
tar -xzf /tmp/pg_timetable.tar.gz -C /tmp --strip-components=1
|
||||
/tmp/pg_timetable --init --clientname=runner "host=/tmp port=59${{ matrix.pgver }} dbname=postgres user=postgres"
|
||||
nohup /tmp/pg_timetable --clientname=runner "host=/tmp port=59${{ matrix.pgver }} dbname=postgres user=postgres" > /tmp/pg_timetable.log 2>&1 &
|
||||
echo $! > /tmp/pg_timetable.pid
|
||||
sleep 2
|
||||
|
||||
- name: Install and start pg_timetable on Windows
|
||||
if: ${{ matrix.os == 'windows-latest' }}
|
||||
shell: pwsh
|
||||
run: |
|
||||
$env:PGPASSWORD = 'postgres'
|
||||
curl.exe -sSL -o pg_timetable.zip https://github.com/cybertec-postgresql/pg_timetable/releases/download/v${{ env.PG_TIMETABLE_VERSION }}/pg_timetable_Windows_x86_64.zip
|
||||
Expand-Archive -Path pg_timetable.zip -DestinationPath .
|
||||
& .\pg_timetable_Windows_x86_64\pg_timetable.exe --init --clientname=runner "postgresql://postgres:postgres@127.0.0.1:59${{ matrix.pgver }}/postgres"
|
||||
Start-Process -FilePath .\pg_timetable_Windows_x86_64\pg_timetable.exe -ArgumentList @('--clientname=runner', "postgresql://postgres:postgres@127.0.0.1:59${{ matrix.pgver }}/postgres") -RedirectStandardError .\pg_timetable_Windows_x86_64\pg_timetable.err -NoNewWindow
|
||||
Start-Sleep -Seconds 3
|
||||
|
||||
- name: Install Python dependencies on Linux and macOS
|
||||
if: ${{ matrix.os == 'macos-latest' || matrix.os == 'ubuntu-22.04' }}
|
||||
run: make install-python-testing
|
||||
@@ -318,16 +379,29 @@ jobs:
|
||||
python web\regression\runtests.py --exclude feature_tests
|
||||
shell: cmd
|
||||
|
||||
- name: Stop pg_timetable on Linux and macOS
|
||||
if: always() && (matrix.os == 'macos-latest' || matrix.os == 'ubuntu-22.04')
|
||||
run: |
|
||||
if [ -f /tmp/pg_timetable.pid ]; then
|
||||
kill $(cat /tmp/pg_timetable.pid) 2>/dev/null || true
|
||||
fi
|
||||
|
||||
- name: Stop pg_timetable on Windows
|
||||
if: always() && matrix.os == 'windows-latest'
|
||||
shell: pwsh
|
||||
run: |
|
||||
Get-Process -Name pg_timetable -ErrorAction SilentlyContinue | Stop-Process -Force
|
||||
|
||||
- name: Archive server log
|
||||
if: success() || failure()
|
||||
uses: actions/upload-artifact@v4
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: server-log-${{ matrix.os }}-pg${{ matrix.pgver }}
|
||||
path: var/pgadmin4.log
|
||||
|
||||
- name: Archive regression log
|
||||
if: success() || failure()
|
||||
uses: actions/upload-artifact@v4
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: regression-log-${{ matrix.os }}-pg${{ matrix.pgver }}
|
||||
path: web/regression/regression.log
|
||||
|
||||
@@ -15,8 +15,28 @@ name: Run Python tests in server mode (data isolation)
|
||||
on:
|
||||
push:
|
||||
branches: [ "master" ]
|
||||
paths:
|
||||
- 'web/**'
|
||||
# Not the message catalogues: a .po change cannot break a build or a
|
||||
# test, and check-translations already compiles them, which is the
|
||||
# check that would catch a malformed one.
|
||||
- '!web/pgadmin/translations/**'
|
||||
- 'requirements.txt'
|
||||
- 'Makefile'
|
||||
- 'tools/setup-python-env.sh'
|
||||
- '.github/workflows/run-python-tests-server-mode.yml'
|
||||
pull_request:
|
||||
branches: [ "master" ]
|
||||
paths:
|
||||
- 'web/**'
|
||||
# Not the message catalogues: a .po change cannot break a build or a
|
||||
# test, and check-translations already compiles them, which is the
|
||||
# check that would catch a malformed one.
|
||||
- '!web/pgadmin/translations/**'
|
||||
- 'requirements.txt'
|
||||
- 'Makefile'
|
||||
- 'tools/setup-python-env.sh'
|
||||
- '.github/workflows/run-python-tests-server-mode.yml'
|
||||
|
||||
workflow_dispatch:
|
||||
|
||||
@@ -32,7 +52,7 @@ jobs:
|
||||
PG_VER: 18
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v7
|
||||
with:
|
||||
# We don't run any authenticated git operations after checkout
|
||||
# (no push, no submodule fetch, no comment back to the PR), so
|
||||
@@ -41,7 +61,7 @@ jobs:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Update python version
|
||||
uses: actions/setup-python@v5
|
||||
uses: actions/setup-python@v7
|
||||
with:
|
||||
python-version: '3.10'
|
||||
|
||||
@@ -191,14 +211,14 @@ jobs:
|
||||
|
||||
- name: Archive server log
|
||||
if: success() || failure()
|
||||
uses: actions/upload-artifact@v4
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: server-log-server-mode
|
||||
path: var/pgadmin4.log
|
||||
|
||||
- name: Archive regression log
|
||||
if: success() || failure()
|
||||
uses: actions/upload-artifact@v4
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: regression-log-server-mode
|
||||
path: web/regression/regression.log
|
||||
@@ -0,0 +1,179 @@
|
||||
# Signed, notarised macOS snapshot builds, replacing the buildfarm's
|
||||
# pgadmin4-appbundle-snapshot job.
|
||||
#
|
||||
# Unlike check-macos-build.yml this produces something distributable: the
|
||||
# bundle is signed with the Developer ID, notarised by Apple and stapled, and a
|
||||
# ZIP is built alongside the DMG because the Electron auto-updater consumes the
|
||||
# ZIP whilst the DMG is the primary download.
|
||||
#
|
||||
# Uploading is deliberately not wired up yet. This proves the signed build
|
||||
# works; publishing comes after.
|
||||
#
|
||||
# Triggered by hand for now rather than nightly, so it cannot start signing
|
||||
# things before we mean it to.
|
||||
|
||||
name: Snapshot - macOS
|
||||
|
||||
on:
|
||||
# Callable from snapshot.yml so that the orchestrator produces one
|
||||
# complete tree. Signing stays here rather than moving to the
|
||||
# servers like the GPG signing did, because the Apple and Certum
|
||||
# credentials cannot follow it.
|
||||
workflow_call:
|
||||
|
||||
workflow_dispatch:
|
||||
|
||||
concurrency:
|
||||
# Not cancel-in-progress, unlike the QA builds. Cancelling a signing build
|
||||
# partway through wastes forty minutes of work and, worse, can abandon a
|
||||
# notarisation already submitted to Apple, leaving a request sitting in their
|
||||
# queue for a build nobody will collect. Queue instead.
|
||||
# Spelled out rather than taken from github.workflow, which in a
|
||||
# reusable workflow is the CALLER's name, so every workflow Snapshot
|
||||
# calls would otherwise share one group.
|
||||
group: 'snapshot-macos @ ${{ github.ref }}'
|
||||
cancel-in-progress: false
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
appbundle:
|
||||
runs-on: ${{ matrix.runner }}
|
||||
|
||||
# The signing credentials live on an environment rather than loose on the
|
||||
# repository, so only the workflows that should be signing can reach them.
|
||||
environment: signing
|
||||
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
include:
|
||||
- { name: 'arm64', arch: 'arm64', runner: 'macos-15' }
|
||||
- { name: 'x64', arch: 'x86_64', runner: 'macos-26-intel' }
|
||||
|
||||
timeout-minutes: 180
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v7
|
||||
|
||||
- uses: actions/setup-node@v7
|
||||
with:
|
||||
node-version: '20.x'
|
||||
|
||||
- name: Enable Corepack
|
||||
run: corepack enable
|
||||
|
||||
- name: Install the build dependencies
|
||||
run: brew install syft wget
|
||||
|
||||
- name: Install the PostgreSQL build dependencies
|
||||
uses: ./.github/actions/install-pgbuild-deps
|
||||
with:
|
||||
arch: ${{ matrix.arch }}
|
||||
|
||||
# The buildfarm keeps the Developer ID in the worker's login keychain and
|
||||
# unlocks it with devkeys/unlock-csc.sh. A hosted runner has no such
|
||||
# keychain, so one is created for this job alone and destroyed after it,
|
||||
# which is also why the password is generated rather than configured: it
|
||||
# never outlives the job.
|
||||
- name: Import the signing certificate
|
||||
env:
|
||||
MACOS_CERTIFICATE: ${{ secrets.MACOS_CERTIFICATE }}
|
||||
MACOS_CERTIFICATE_PASSWORD: ${{ secrets.MACOS_CERTIFICATE_PASSWORD }}
|
||||
run: |
|
||||
KEYCHAIN="${RUNNER_TEMP}/build.keychain"
|
||||
KEYCHAIN_PASSWORD=$(uuidgen)
|
||||
echo "KEYCHAIN=${KEYCHAIN}" >> "${GITHUB_ENV}"
|
||||
|
||||
echo "${MACOS_CERTIFICATE}" | base64 --decode > "${RUNNER_TEMP}/cert.p12"
|
||||
|
||||
security create-keychain -p "${KEYCHAIN_PASSWORD}" "${KEYCHAIN}"
|
||||
security set-keychain-settings -lut 21600 "${KEYCHAIN}"
|
||||
security unlock-keychain -p "${KEYCHAIN_PASSWORD}" "${KEYCHAIN}"
|
||||
security import "${RUNNER_TEMP}/cert.p12" \
|
||||
-k "${KEYCHAIN}" \
|
||||
-P "${MACOS_CERTIFICATE_PASSWORD}" \
|
||||
-T /usr/bin/codesign
|
||||
# Without this, codesign prompts for permission to use the key, which
|
||||
# on a headless runner means hanging until the job times out.
|
||||
security set-key-partition-list -S apple-tool:,apple:,codesign: \
|
||||
-s -k "${KEYCHAIN_PASSWORD}" "${KEYCHAIN}" > /dev/null
|
||||
security list-keychain -d user -s "${KEYCHAIN}" login.keychain
|
||||
|
||||
rm -f "${RUNNER_TEMP}/cert.p12"
|
||||
echo "Identities available to codesign:"
|
||||
security find-identity -v -p codesigning "${KEYCHAIN}"
|
||||
|
||||
# build.sh decides whether to sign and notarise purely by whether these
|
||||
# files exist, which is what keeps the QA build credential-free.
|
||||
- name: Write the signing configuration
|
||||
env:
|
||||
MACOS_DEVELOPER_ID: ${{ secrets.MACOS_DEVELOPER_ID }}
|
||||
APPLE_ID: ${{ secrets.APPLE_ID }}
|
||||
APPLE_APP_SPECIFIC_PASSWORD: ${{ secrets.APPLE_APP_SPECIFIC_PASSWORD }}
|
||||
APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }}
|
||||
run: |
|
||||
printf 'DEVELOPER_ID="%s"\n' "${MACOS_DEVELOPER_ID}" > pkg/mac/codesign.conf
|
||||
{
|
||||
printf 'DEVELOPER_USER=%s\n' "${APPLE_ID}"
|
||||
printf 'DEVELOPER_ASP=%s\n' "${APPLE_APP_SPECIFIC_PASSWORD}"
|
||||
printf 'DEVELOPER_TEAM_ID=%s\n' "${APPLE_TEAM_ID}"
|
||||
} > pkg/mac/notarization.conf
|
||||
|
||||
# --zip as well as the DMG: the DMG is the download, the ZIP is what the
|
||||
# Electron auto-updater fetches. The buildfarm's snapshot job does the
|
||||
# same.
|
||||
- name: Build the signed appbundle
|
||||
run: |
|
||||
export PGADMIN_POSTGRES_DIR=/opt/pgbuild/postgresql
|
||||
export OPENSSL_DIR=/opt/pgbuild/openssl
|
||||
export OPENSSL_STATIC=1
|
||||
|
||||
make appbundle BUILD_OPTS="--zip"
|
||||
|
||||
- name: Verify the signature and notarisation
|
||||
run: |
|
||||
APP="mac-build/pgAdmin 4.app"
|
||||
echo "=== codesign ==="
|
||||
codesign --verify --deep --strict --verbose=2 "${APP}"
|
||||
codesign -dvvv "${APP}" 2>&1 | grep -E 'Authority|TeamIdentifier|Identifier='
|
||||
echo "=== gatekeeper assessment ==="
|
||||
spctl --assess --type execute --verbose=4 "${APP}"
|
||||
echo "=== stapled notarisation ==="
|
||||
# stapler looks the ticket up in Apple's CloudKit, and a dropped
|
||||
# connection there fails the step with error 68, so it is retried.
|
||||
# A genuinely broken ticket still fails all three attempts.
|
||||
validate() {
|
||||
for attempt in 1 2 3; do
|
||||
xcrun stapler validate "$1" && return 0
|
||||
[ "${attempt}" -lt 3 ] || return 1
|
||||
echo "stapler validate failed; retrying in $((attempt * 30))s"
|
||||
sleep $((attempt * 30))
|
||||
done
|
||||
}
|
||||
validate "${APP}"
|
||||
for F in dist/*.dmg; do
|
||||
echo "--- ${F} ---"
|
||||
validate "${F}"
|
||||
done
|
||||
|
||||
- name: Archive the disk image and zip
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: pgadmin4-macos-snapshot-${{ matrix.name }}
|
||||
if-no-files-found: error
|
||||
path: |
|
||||
dist/*.dmg
|
||||
dist/*.zip
|
||||
|
||||
# Runs even on failure: a keychain holding a Developer ID key must not
|
||||
# outlive the job that created it, and the conf files carry the Apple
|
||||
# credentials.
|
||||
- name: Clean up the credentials
|
||||
if: always()
|
||||
run: |
|
||||
rm -f pkg/mac/codesign.conf pkg/mac/notarization.conf
|
||||
if [ -n "${KEYCHAIN:-}" ] && [ -f "${KEYCHAIN}" ]; then
|
||||
security delete-keychain "${KEYCHAIN}"
|
||||
fi
|
||||
@@ -0,0 +1,180 @@
|
||||
# Signed Windows snapshot builds, replacing the buildfarm's
|
||||
# pgadmin4-windows-x64-snapshot job.
|
||||
#
|
||||
# This is the one workflow that cannot use a hosted runner. The certificate is
|
||||
# on a Certum hardware token plugged into a machine we host ourselves, reached
|
||||
# through the Windows machine certificate store, and a PIN handler answers the
|
||||
# prompt from the interactive desktop session. None of that can be moved to a
|
||||
# runner GitHub provides.
|
||||
#
|
||||
# Uploading is not wired up yet; this proves the signed build works.
|
||||
#
|
||||
# The runner must be running in the logged-in interactive session, not as a
|
||||
# Windows service. A service runs in session 0 with no desktop, so the PIN
|
||||
# prompt appears where the handler cannot see it and signing hangs rather than
|
||||
# failing.
|
||||
|
||||
name: Snapshot - Windows
|
||||
|
||||
on:
|
||||
# Callable from snapshot.yml so that the orchestrator produces one
|
||||
# complete tree. Signing stays here rather than moving to the
|
||||
# servers like the GPG signing did, because the Apple and Certum
|
||||
# credentials cannot follow it.
|
||||
workflow_call:
|
||||
|
||||
workflow_dispatch:
|
||||
|
||||
concurrency:
|
||||
# Serialised deliberately, and not cancel-in-progress. There is one hardware
|
||||
# token, and two builds signing through it at once is a good way to find out
|
||||
# what proCertum does under contention. Jenkins may also be using it.
|
||||
group: 'windows-signing'
|
||||
cancel-in-progress: false
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
installer:
|
||||
runs-on: [self-hosted, windows-x64]
|
||||
timeout-minutes: 120
|
||||
|
||||
environment: signing
|
||||
|
||||
env:
|
||||
POSTGRES_MAJOR: '18'
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v7
|
||||
|
||||
- uses: actions/setup-node@v7
|
||||
with:
|
||||
node-version: '20.x'
|
||||
|
||||
- name: Enable Corepack
|
||||
run: corepack enable
|
||||
|
||||
# No Python step, and none needed. actions/setup-python hangs on this
|
||||
# runner: it installs Python rather than unpacking it, and anything
|
||||
# raising a UAC prompt in the auto-logged-in session waits forever with
|
||||
# nobody to answer it, exactly as the card's PIN dialog would. The first
|
||||
# attempt sat on that step for thirty minutes against eleven seconds for
|
||||
# setup-node. Using whatever Python is on PATH avoids the hang and is
|
||||
# worse: this machine has 3.14 installed whilst the released installers
|
||||
# carry 3.13, so it would have quietly shipped a different interpreter
|
||||
# from the one the macOS build ships.
|
||||
#
|
||||
# Make.bat now downloads the version named in pkg/python-version.txt
|
||||
# itself, as a plain tarball with no installer and so no prompt, which
|
||||
# settles both problems and means this host needs no Python at all.
|
||||
|
||||
# Unlike the hosted build, nothing is installed here. This machine is
|
||||
# configured by hand rather than by Ansible, it is the only one in the
|
||||
# pipeline that cannot simply be rebuilt, and a CI job has no business
|
||||
# mutating it. So check, and stop with something useful to act on rather
|
||||
# than failing halfway through a build.
|
||||
- name: Verify the build prerequisites
|
||||
shell: powershell
|
||||
run: |
|
||||
$missing = @()
|
||||
if (-not (Test-Path "C:\Program Files (x86)\Inno Setup 6\ISCC.exe")) { $missing += "innosetup" }
|
||||
foreach ($tool in @('syft', 'wget')) {
|
||||
if (-not (Get-Command $tool -ErrorAction SilentlyContinue)) { $missing += $tool }
|
||||
}
|
||||
if ($missing.Count) {
|
||||
throw ("Missing build prerequisites on this runner: $($missing -join ', '). " +
|
||||
"Install with: choco install $($missing -join ' ') -y")
|
||||
}
|
||||
Write-Output "All prerequisites present."
|
||||
|
||||
- name: Install the PostgreSQL and Kerberos dependencies
|
||||
id: deps
|
||||
uses: ./.github/actions/install-pgbuild-deps
|
||||
with:
|
||||
arch: x86_64
|
||||
postgres-major: ${{ env.POSTGRES_MAJOR }}
|
||||
|
||||
- name: Record the dependency location
|
||||
shell: powershell
|
||||
run: '"DEPS_DIR=${{ steps.deps.outputs.prefix }}" | Out-File -FilePath $env:GITHUB_ENV -Encoding utf8 -Append'
|
||||
|
||||
# Discovered rather than hardcoded. The buildfarm pins these to specific
|
||||
# paths in its Jenkins node configuration, which needs editing every time
|
||||
# Visual Studio or the Windows SDK updates, and that configuration is
|
||||
# going away with the buildfarm.
|
||||
- name: Locate the toolchain
|
||||
shell: powershell
|
||||
run: |
|
||||
$vswhere = "${env:ProgramFiles(x86)}\Microsoft Visual Studio\Installer\vswhere.exe"
|
||||
$vs = & $vswhere -latest -products * -property installationPath
|
||||
if (-not $vs) { throw "Could not locate a Visual Studio installation" }
|
||||
|
||||
$redist = Get-ChildItem "$vs\VC\Redist\MSVC" -Directory |
|
||||
Where-Object { Test-Path "$($_.FullName)\vc_redist.x64.exe" } |
|
||||
Sort-Object Name -Descending | Select-Object -First 1
|
||||
if (-not $redist) { throw "Could not locate vc_redist.x64.exe under $vs" }
|
||||
|
||||
# signtool lives under the SDK's versioned bin directory, and also in
|
||||
# the App Certification Kit, which is where the buildfarm points at.
|
||||
# Take the newest SDK copy, falling back to the ACK one.
|
||||
$signtool = Get-ChildItem "${env:ProgramFiles(x86)}\Windows Kits\10\bin" `
|
||||
-Filter signtool.exe -Recurse -ErrorAction SilentlyContinue |
|
||||
Where-Object { $_.FullName -match '\\x64\\' } |
|
||||
Sort-Object FullName -Descending | Select-Object -First 1
|
||||
if (-not $signtool) {
|
||||
$ack = "${env:ProgramFiles(x86)}\Windows Kits\10\App Certification Kit\signtool.exe"
|
||||
if (Test-Path $ack) { $signtool = Get-Item $ack }
|
||||
}
|
||||
if (-not $signtool) { throw "Could not locate signtool.exe" }
|
||||
|
||||
Write-Output "Visual Studio: $vs"
|
||||
Write-Output "VC++ redist: $($redist.FullName)"
|
||||
Write-Output "signtool: $($signtool.DirectoryName)"
|
||||
|
||||
"VS_INSTALL_DIR=$vs" | Out-File -FilePath $env:GITHUB_ENV -Encoding utf8 -Append
|
||||
"PGADMIN_VCREDIST_DIR=$($redist.FullName)" | Out-File -FilePath $env:GITHUB_ENV -Encoding utf8 -Append
|
||||
"PGADMIN_SIGNTOOL_DIR=$($signtool.DirectoryName)" | Out-File -FilePath $env:GITHUB_ENV -Encoding utf8 -Append
|
||||
|
||||
# Setting PGADMIN_WINDOWS_CSC is what makes Make.bat sign: it selects the
|
||||
# certificate by subject name from the machine store, where proCertum
|
||||
# installed it. Kept in a repository variable rather than the tree, so a
|
||||
# certificate renewal is a settings change rather than a commit.
|
||||
- name: Build the signed installer
|
||||
shell: cmd
|
||||
env:
|
||||
PGADMIN_WINDOWS_CSC: ${{ vars.PGADMIN_WINDOWS_CSC }}
|
||||
run: |
|
||||
IF "%PGADMIN_WINDOWS_CSC%" == "" (
|
||||
ECHO ERROR: the PGADMIN_WINDOWS_CSC repository variable is not set,
|
||||
ECHO so Make.bat would silently produce an unsigned build.
|
||||
EXIT /b 1
|
||||
)
|
||||
|
||||
SET "PGADMIN_POSTGRES_DIR=%DEPS_DIR%\postgresql"
|
||||
SET "PGADMIN_KRB5_DIR=%DEPS_DIR%\krb5"
|
||||
SET "PGADMIN_INNOTOOL_DIR=C:\Program Files (x86)\Inno Setup 6"
|
||||
|
||||
CALL "%VS_INSTALL_DIR%\VC\Auxiliary\Build\vcvars64.bat" || EXIT /b 1
|
||||
CALL Make.bat || EXIT /b 1
|
||||
|
||||
# Make.bat now fails the build if either signature fails, the executable
|
||||
# inside the installer included, so this is a second opinion rather than
|
||||
# the only check. It verifies what is in dist/, which is the installer;
|
||||
# the executable within it is covered by Make.bat refusing to continue.
|
||||
- name: Verify the signatures
|
||||
shell: powershell
|
||||
run: |
|
||||
$signtool = Join-Path $env:PGADMIN_SIGNTOOL_DIR 'signtool.exe'
|
||||
Get-ChildItem dist\*.exe | ForEach-Object {
|
||||
Write-Output "--- $($_.Name) ---"
|
||||
& $signtool verify /pa /v $_.FullName
|
||||
if ($LASTEXITCODE -ne 0) { throw "Signature verification failed for $($_.Name)" }
|
||||
}
|
||||
|
||||
- name: Archive the installer
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: pgadmin4-windows-snapshot
|
||||
if-no-files-found: error
|
||||
path: dist/*.exe
|
||||
@@ -0,0 +1,465 @@
|
||||
# Nightly snapshot builds, replacing the buildfarm's pgadmin4-all-snapshot job
|
||||
# and the dozen per-artefact snapshot jobs it drives.
|
||||
#
|
||||
# Nothing here is a second definition of a build. Every artefact comes from the
|
||||
# same workflow that runs on each push, called rather than copied, so a
|
||||
# snapshot cannot quietly diverge from what QA tested. That was not true of the
|
||||
# buildfarm, where pgadmin4-deb-build and pgadmin4-deb-snapshot were separate
|
||||
# job definitions carrying the same commands.
|
||||
#
|
||||
# Nothing here signs anything either, beyond the macOS notarisation and the
|
||||
# Windows Authenticode signature, which have to happen where their credentials
|
||||
# are. The GPG key that signs the RPMs and the repository metadata stays on the
|
||||
# servers: this workflow assembles the tree and uploads it, and asks the
|
||||
# staging server to
|
||||
# index and sign, so a compromised workflow run can ask for a signature but
|
||||
# never holds the means to make one.
|
||||
|
||||
name: Snapshot
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
location:
|
||||
description: >-
|
||||
Snapshot directory name, normally today's date. The buildfarm
|
||||
writes <date> on the same server, so a test run wants a -N
|
||||
suffix, for example 2026-09-17-2.
|
||||
required: false
|
||||
default: ''
|
||||
publish:
|
||||
description: Upload to the download server when the build succeeds
|
||||
type: boolean
|
||||
required: false
|
||||
default: false
|
||||
|
||||
# Nightly, matching the buildfarm's pgadmin4-all-snapshot, which ran at
|
||||
# 01:01. GitHub's scheduler is best-effort and runs late under load, which
|
||||
# does not matter for a snapshot: the directory is named for the date the
|
||||
# run assembles it, not for the minute it started.
|
||||
schedule:
|
||||
- cron: '1 1 * * *'
|
||||
|
||||
concurrency:
|
||||
# One snapshot at a time, and never cancelled: a cancelled run can abandon a
|
||||
# notarisation already submitted to Apple, and leaves the signing token in an
|
||||
# unknown state.
|
||||
group: 'snapshot'
|
||||
cancel-in-progress: false
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
# The directory name, decided once. Two jobs need it and they must agree:
|
||||
# the repo RPMs carry the snapshot's own URL inside them, so if that job and
|
||||
# the assemble job each called date(1) a run straddling midnight would ship a
|
||||
# repository configuration pointing at a snapshot that does not exist.
|
||||
#
|
||||
# It is also where a scheduled run on a fork stops. A fork with Actions
|
||||
# enabled inherits the schedule but none of the signing secrets or
|
||||
# self-hosted runners, so the macOS jobs would fail at the certificate and
|
||||
# the Windows job would queue for a day. Every other job waits on this one,
|
||||
# so skipping it skips the run. A dispatch still runs anywhere.
|
||||
location:
|
||||
if: >-
|
||||
github.event_name != 'schedule'
|
||||
|| github.repository == 'pgadmin-org/pgadmin4'
|
||||
runs-on: ubuntu-24.04
|
||||
outputs:
|
||||
name: ${{ steps.pick.outputs.name }}
|
||||
# Whether a location was asked for by name, which decides both what the
|
||||
# published directory is called and whether retention runs. It travels
|
||||
# as a flag so that the steps downstream never need the raw input to
|
||||
# work out what was requested: this job is where that input is checked,
|
||||
# and a value that has not been through the check below has no business
|
||||
# reaching the machine holding the publishing keys.
|
||||
named: ${{ steps.pick.outputs.named }}
|
||||
steps:
|
||||
- id: pick
|
||||
env:
|
||||
REQUESTED: ${{ inputs.location }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
NAME="${REQUESTED:-}"
|
||||
if [ -n "${NAME}" ]; then NAMED=yes; else NAMED=no; fi
|
||||
[ -n "${NAME}" ] || NAME=$(date +%Y-%m-%d)
|
||||
|
||||
# Validated here so that everything downstream, including a shell on
|
||||
# the machine holding the publishing keys, is working with a name
|
||||
# that cannot be anything but a date and an optional suffix. The
|
||||
# wrapper on the server applies its own rule to the same value; this
|
||||
# one exists so a bad input fails in five seconds rather than after
|
||||
# an hour of building.
|
||||
if ! printf '%s' "${NAME}" | grep -Eq '^[0-9]{4}-[0-9]{2}-[0-9]{2}(-[1-9][0-9]?)?$'; then
|
||||
echo "Not a valid snapshot location: ${NAME}" >&2
|
||||
echo "Expected YYYY-MM-DD, optionally with a -N suffix." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "Snapshot location: ${NAME}"
|
||||
echo "name=${NAME}" >> "${GITHUB_OUTPUT}"
|
||||
echo "named=${NAMED}" >> "${GITHUB_OUTPUT}"
|
||||
|
||||
deb:
|
||||
needs: location
|
||||
uses: ./.github/workflows/check-deb-build.yml
|
||||
rpm:
|
||||
needs: location
|
||||
uses: ./.github/workflows/check-rpm-build.yml
|
||||
tarball:
|
||||
needs: location
|
||||
uses: ./.github/workflows/check-tarball-build.yml
|
||||
wheel:
|
||||
needs: location
|
||||
uses: ./.github/workflows/check-python-build.yml
|
||||
docs:
|
||||
needs: location
|
||||
uses: ./.github/workflows/check-doc-builds.yml
|
||||
# No helm job here. The chart is not part of the download tree: the
|
||||
# buildfarm pushed it to an OCI registry, which nothing in this workflow
|
||||
# does yet. Calling the build and then discarding its artefact, which is what
|
||||
# happened before, cost a runner and proved nothing that check-helm-build
|
||||
# does not already prove on every push.
|
||||
|
||||
# The repository configuration RPMs. These are not built by the QA
|
||||
# workflows, because they are not a build of pgAdmin: they are a one-file
|
||||
# package carrying a .repo pointing at the tree they ship in, which is why
|
||||
# the URL has to be known before they are built.
|
||||
repo-rpm:
|
||||
needs: location
|
||||
runs-on: ubuntu-24.04
|
||||
container: almalinux:9
|
||||
steps:
|
||||
- name: Install the build environment
|
||||
run: dnf install -y --setopt=install_weak_deps=False rpm-build python3 git
|
||||
|
||||
- uses: actions/checkout@v7
|
||||
|
||||
- name: Mark the workspace as a safe git directory
|
||||
run: git config --global --add safe.directory "${GITHUB_WORKSPACE}"
|
||||
|
||||
- name: Build the repository RPMs
|
||||
env:
|
||||
LOCATION: ${{ needs.location.outputs.name }}
|
||||
run: |
|
||||
# Through the environment rather than interpolated into the script.
|
||||
# The location is a dispatch input, so it is text somebody chose, and
|
||||
# workflow expression substitution happens before the shell ever sees the
|
||||
# line, so a crafted location would otherwise become shell text.
|
||||
export PGADMIN_REPO_DIR="https://ftp.postgresql.org/pub/pgadmin/pgadmin4/snapshots/${LOCATION}/yum"
|
||||
echo "Repositories will point at ${PGADMIN_REPO_DIR}"
|
||||
pkg/redhat/repo-rpm.sh
|
||||
|
||||
- name: Archive the repository RPMs
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: pgadmin4-repo-rpm-build-output
|
||||
if-no-files-found: error
|
||||
path: dist/*.noarch.rpm
|
||||
|
||||
macos:
|
||||
needs: location
|
||||
uses: ./.github/workflows/snapshot-macos.yml
|
||||
secrets: inherit
|
||||
|
||||
windows:
|
||||
needs: location
|
||||
uses: ./.github/workflows/snapshot-windows.yml
|
||||
secrets: inherit
|
||||
|
||||
assemble:
|
||||
needs: [location, deb, rpm, tarball, wheel, docs, repo-rpm, macos, windows]
|
||||
|
||||
# the runner, not a hosted runner. This job assembles a tree of several
|
||||
# hundred megabytes and then, when publishing, sends it to the download
|
||||
# server; splitting the two meant uploading that tree as an artifact and
|
||||
# immediately downloading it again on the runner, having first queued for a
|
||||
# hosted runner behind whatever else the organisation was building.
|
||||
runs-on: [self-hosted, publish]
|
||||
|
||||
# For the variables naming the keys and the target, which live here. The
|
||||
# environment carries no protection rules, so a run that is not publishing
|
||||
# is not gated by it.
|
||||
environment: publishing
|
||||
|
||||
# Everything must have built except Windows. That is the one runner which
|
||||
# can legitimately be offline, and a snapshot without the installer is
|
||||
# still worth having; a snapshot missing its documentation, source tarball,
|
||||
# wheel or repository RPMs is not, and the repository RPMs are worse than
|
||||
# missing, since install-repo-readme.py then fails after the packages have
|
||||
# been uploaded and signed, leaving half a snapshot on the download server.
|
||||
if: >-
|
||||
always()
|
||||
&& needs.location.result == 'success'
|
||||
&& needs.deb.result == 'success'
|
||||
&& needs.rpm.result == 'success'
|
||||
&& needs.tarball.result == 'success'
|
||||
&& needs.wheel.result == 'success'
|
||||
&& needs.docs.result == 'success'
|
||||
&& needs.repo-rpm.result == 'success'
|
||||
&& needs.macos.result == 'success'
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v7
|
||||
|
||||
- name: Collect the build artefacts
|
||||
uses: actions/download-artifact@v7
|
||||
with:
|
||||
path: artefacts
|
||||
|
||||
# The layout is the one the publishing scripts and the download site
|
||||
# expect, and is what the wrapper's stage-create verb creates on the
|
||||
# staging server.
|
||||
# The Debian architecture comes from the package filename rather than a
|
||||
# table, since dpkg already encodes it there; the codename does not, so
|
||||
# that mapping is explicit.
|
||||
- name: Assemble the staging tree
|
||||
env:
|
||||
LOCATION: ${{ needs.location.outputs.name }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
# Flat, beside apt/ and yum/, which is how both the staging server
|
||||
# and the snapshots tree are laid out. The docs/macos/pip/source/
|
||||
# windows subdivision belongs to a published release, and it is the
|
||||
# promotion step that sorts files into it on the way there.
|
||||
# Explicitly, rather than trusting the checkout to have cleaned it.
|
||||
# This runs on a self-hosted runner whose workspace persists between
|
||||
# jobs, and a previous run's tree left behind here is indistinguishable
|
||||
# from this one's until something downstream has to choose between
|
||||
# them.
|
||||
rm -rf stage
|
||||
|
||||
STAGE="stage/${LOCATION}"
|
||||
mkdir -p "${STAGE}/yum"
|
||||
|
||||
# An artefact can be legitimately absent: the Windows signing host is
|
||||
# allowed to be offline, and a failed matrix leg is reported by its
|
||||
# own job rather than by a confusing cp error here.
|
||||
copy_into() {
|
||||
DEST=$1; shift
|
||||
for SRC in "$@"; do
|
||||
[ -e "${SRC}" ] || continue
|
||||
mkdir -p "${DEST}"
|
||||
cp "${SRC}" "${DEST}/"
|
||||
done
|
||||
}
|
||||
|
||||
codename_for() {
|
||||
case "$1" in
|
||||
debian-12) echo bookworm ;;
|
||||
debian-13) echo trixie ;;
|
||||
ubuntu-2204) echo jammy ;;
|
||||
ubuntu-2404) echo noble ;;
|
||||
ubuntu-2604) echo resolute ;;
|
||||
*) echo "Unknown Debian build name: $1" >&2; exit 1 ;;
|
||||
esac
|
||||
}
|
||||
|
||||
for DIR in artefacts/pgadmin4-deb-build-output-*; do
|
||||
[ -d "${DIR}" ] || continue
|
||||
CODENAME=$(codename_for "${DIR##*-output-}")
|
||||
for DEB in "${DIR}"/*.deb; do
|
||||
ARCH=$(basename "${DEB}" .deb); ARCH=${ARCH##*_}
|
||||
TARGET="${STAGE}/apt/${CODENAME}/dists/pgadmin4/main/binary-${ARCH}"
|
||||
mkdir -p "${TARGET}"
|
||||
cp "${DEB}" "${TARGET}/"
|
||||
done
|
||||
done
|
||||
|
||||
# RPM trees are named for the distribution, which the build name
|
||||
# already carries: el-9 becomes redhat/rhel-9-x86_64.
|
||||
for DIR in artefacts/pgadmin4-rpm-build-output-*; do
|
||||
[ -d "${DIR}" ] || continue
|
||||
NAME=${DIR##*-output-}
|
||||
case "${NAME}" in
|
||||
el-*) FAMILY=redhat; TREE="rhel-${NAME#el-}" ;;
|
||||
fedora-*) FAMILY=fedora; TREE="${NAME}" ;;
|
||||
*) echo "Unknown RPM build name: ${NAME}" >&2; exit 1 ;;
|
||||
esac
|
||||
# Every RPM for a distribution goes in that distribution's
|
||||
# tree, including the noarch ones. Taking the directory from the
|
||||
# package's own architecture would file pgadmin4 and
|
||||
# pgadmin4-web, both noarch, under rhel-9-noarch, where no
|
||||
# client looks: dnf asks for <name>-<version>-$basearch. The
|
||||
# RPM matrix is x86_64 only for now, so this is a constant; when
|
||||
# aarch64 arrives it becomes a property of the build.
|
||||
TARGET="${STAGE}/yum/${FAMILY}/${TREE}-x86_64"
|
||||
mkdir -p "${TARGET}"
|
||||
cp "${DIR}"/*.rpm "${TARGET}/"
|
||||
done
|
||||
|
||||
copy_into "${STAGE}" artefacts/pgadmin4-tarball-build-output/*
|
||||
copy_into "${STAGE}" artefacts/pgadmin4-python-build-output/*
|
||||
# The version is read once here and used for the documentation
|
||||
# filenames, which the build leaves unversioned as pgadmin4.pdf and
|
||||
# pgAdmin4.epub whilst the download site has always carried
|
||||
# pgadmin4-<version>.pdf and .epub.
|
||||
VERSION=$(python3 -c "import sys; sys.path.insert(0, 'web'); import version; print(version.APP_VERSION)")
|
||||
|
||||
for SRC in artefacts/pgadmin4-pdf-build-output/*.pdf; do
|
||||
[ -e "${SRC}" ] && cp "${SRC}" "${STAGE}/pgadmin4-${VERSION}.pdf"
|
||||
done
|
||||
for SRC in artefacts/pgadmin4-epub-build-output/*.epub; do
|
||||
[ -e "${SRC}" ] && cp "${SRC}" "${STAGE}/pgadmin4-${VERSION}.epub"
|
||||
done
|
||||
|
||||
# The docs tarball is what load-docs.sh unpacks on the web server, so
|
||||
# it ships as a tarball rather than as the directory of files the
|
||||
# build produces.
|
||||
if [ -d artefacts/pgadmin4-html-build-output ]; then
|
||||
# Under a pgadmin4-<version>-docs/ directory, not flat. The
|
||||
# web server's load-docs.sh unpacks this into the docs root
|
||||
# and then hands docloader that directory name, so a flat
|
||||
# archive would scatter several hundred files across the root
|
||||
# and then fail on a directory that does not exist. Every
|
||||
# released tarball has carried that top level; the tar here
|
||||
# had not.
|
||||
DOCS_DIR="pgadmin4-${VERSION}-docs"
|
||||
rm -rf "artefacts/${DOCS_DIR}"
|
||||
mv artefacts/pgadmin4-html-build-output "artefacts/${DOCS_DIR}"
|
||||
tar -czf "${STAGE}/pgadmin4-${VERSION}-docs.tar.gz" \
|
||||
-C artefacts "${DOCS_DIR}"
|
||||
fi
|
||||
|
||||
# At the root of yum/, beside the family directories, which is
|
||||
# where the README instructions tell people to fetch them from.
|
||||
copy_into "${STAGE}/yum" artefacts/pgadmin4-repo-rpm-build-output/*
|
||||
|
||||
copy_into "${STAGE}" artefacts/pgadmin4-macos-snapshot-*/*
|
||||
copy_into "${STAGE}" artefacts/pgadmin4-windows-snapshot/*
|
||||
|
||||
find "${STAGE}" -type f | sort
|
||||
|
||||
- name: Check the keys are present
|
||||
# A scheduled run always publishes, that being the entire point of the
|
||||
# nightly; the input exists so that a dispatched test run does not.
|
||||
# It has to be spelled out because a schedule event carries no inputs
|
||||
# at all, so `inputs.publish` is null rather than false, and the two
|
||||
# publishing steps were quietly skipped on every nightly.
|
||||
if: ${{ github.event_name == 'schedule' || inputs.publish }}
|
||||
env:
|
||||
DOWNLOAD: ${{ vars.DOWNLOAD_SSH_TARGET }}
|
||||
PUBLISH_KEY: ${{ vars.PUBLISH_SSH_KEY || '~/.ssh/pga-publish' }}
|
||||
UPLOAD_KEY: ${{ vars.UPLOAD_SSH_KEY || '~/.ssh/pga-upload' }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
for TOOL in ssh rsync; do
|
||||
command -v "${TOOL}" >/dev/null || {
|
||||
echo "This runner has no ${TOOL}." >&2; exit 1; }
|
||||
done
|
||||
|
||||
# These three variables reach option positions. The target is the
|
||||
# first positional argument to ssh, and the key paths are spliced
|
||||
# into the -e string that rsync splits on whitespace before exec'ing
|
||||
# it, so a value beginning with a hyphen, or one carrying a space,
|
||||
# would be read as an option or as a second word rather than as the
|
||||
# host or the path it is meant to be. Only an administrator can set
|
||||
# a repository variable, which makes this defence in depth rather
|
||||
# than a live hole, but it is checked here, once, rather than at
|
||||
# each point of use; the -- in the publish step below closes the
|
||||
# same gap from the other end.
|
||||
TARGET_RE='^[A-Za-z0-9][A-Za-z0-9._-]*(@[A-Za-z0-9][A-Za-z0-9.-]*)?$'
|
||||
printf '%s' "${DOWNLOAD}" | grep -Eq "${TARGET_RE}" || {
|
||||
echo "DOWNLOAD_SSH_TARGET is not a [user@]host." >&2; exit 1; }
|
||||
|
||||
for KEY in "${PUBLISH_KEY/#\~/$HOME}" "${UPLOAD_KEY/#\~/$HOME}"; do
|
||||
printf '%s' "${KEY}" | grep -Eq '^/[A-Za-z0-9._/-]+$' || {
|
||||
echo "Not a plain absolute path: ${KEY}" >&2; exit 1; }
|
||||
if [ ! -r "${KEY}" ]; then
|
||||
echo "Missing or unreadable SSH key: ${KEY}" >&2
|
||||
exit 1
|
||||
fi
|
||||
# A public key is readable too, and pointing -i at one fails
|
||||
# later inside ssh with a message that does not mention which
|
||||
# file was wrong. Both halves of a pair sit in the same
|
||||
# directory with names one suffix apart, so this is an easy
|
||||
# thing to configure wrongly and a cheap thing to catch.
|
||||
if head -c 4 "${KEY}" | grep -q '^ssh-'; then
|
||||
echo "${KEY} is a public key; -i needs the private one." >&2
|
||||
echo "Drop the .pub suffix from the variable that names it." >&2
|
||||
exit 1
|
||||
fi
|
||||
done
|
||||
if [ ! -r ~/.ssh/known_hosts ]; then
|
||||
echo "This runner has no ~/.ssh/known_hosts, so host key" >&2
|
||||
echo "checking would fail. Add the servers' host keys." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
- name: Publish the snapshot
|
||||
if: ${{ github.event_name == 'schedule' || inputs.publish }}
|
||||
env:
|
||||
DOWNLOAD: ${{ vars.DOWNLOAD_SSH_TARGET }}
|
||||
# From the location job rather than from inputs.location, so the
|
||||
# name reaching the publishing key is the one that job checked the
|
||||
# shape of. The two agree today, which is exactly why it is worth
|
||||
# fixing: the validation sat upstream of this step by coincidence
|
||||
# rather than by construction.
|
||||
LOCATION: ${{ needs.location.outputs.name }}
|
||||
NAMED: ${{ needs.location.outputs.named }}
|
||||
PUBLISH_KEY: ${{ vars.PUBLISH_SSH_KEY || '~/.ssh/pga-publish' }}
|
||||
UPLOAD_KEY: ${{ vars.UPLOAD_SSH_KEY || '~/.ssh/pga-upload' }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
if [ -z "${DOWNLOAD}" ]; then
|
||||
echo "The DOWNLOAD_SSH_TARGET variable is not set." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# One directory came out of assemble, named for the date. An explicit
|
||||
# location overrides it so that a test run does not land on the
|
||||
# snapshot the buildfarm is still writing on the same server.
|
||||
# assemble produces exactly one directory, named for the date.
|
||||
# Anything else means the artefact is not what this step thinks it
|
||||
# is, and guessing which one to publish is not a decision to make at
|
||||
# three in the morning.
|
||||
if [ "$(find stage -mindepth 1 -maxdepth 1 -type d | wc -l)" != "1" ]; then
|
||||
echo "Expected one directory in the snapshot artefact:" >&2
|
||||
ls -l stage >&2
|
||||
exit 1
|
||||
fi
|
||||
BUILT=$(basename "$(find stage -mindepth 1 -maxdepth 1 -type d)")
|
||||
# An empty input still means "whatever assemble called it", which is
|
||||
# why this turns on the flag rather than on the name being empty:
|
||||
# the location job fills a blank input in with today's date, so the
|
||||
# name itself no longer says whether anybody asked for one.
|
||||
if [ "${NAMED}" = "yes" ]; then
|
||||
DEST=${LOCATION}
|
||||
else
|
||||
DEST=${BUILT}
|
||||
fi
|
||||
echo "Publishing ${BUILT} as ${DEST}"
|
||||
|
||||
publish() {
|
||||
ssh -i "${PUBLISH_KEY/#\~/$HOME}" -o BatchMode=yes \
|
||||
-o StrictHostKeyChecking=yes -- "${DOWNLOAD}" "$@"
|
||||
}
|
||||
|
||||
publish snapshot-create "${DEST}"
|
||||
|
||||
# No --delete, deliberately: rrsync refuses it anyway, and a
|
||||
# publishing step that can remove things is a publishing step that
|
||||
# can remove the wrong things.
|
||||
rsync -rlt --safe-links --no-specials --no-devices \
|
||||
-e "ssh -i ${UPLOAD_KEY/#\~/$HOME} -o BatchMode=yes -o StrictHostKeyChecking=yes" \
|
||||
"stage/${BUILT}/" "${DOWNLOAD}:${DEST}/"
|
||||
|
||||
# Signing before indexing: the metadata has to describe the signed
|
||||
# packages rather than the unsigned ones just uploaded.
|
||||
publish snapshot-sign "${DEST}"
|
||||
publish snapshot-index-apt "${DEST}"
|
||||
publish snapshot-index-yum "${DEST}"
|
||||
|
||||
# Retention, which the buildfarm did with ls | tail | xargs rm. The
|
||||
# verb takes no argument: it cannot be asked to remove a particular
|
||||
# snapshot, only to enforce the retention the server already has.
|
||||
#
|
||||
# Skipped when the directory was named by hand, because that is a
|
||||
# test run, and a test run has no business deciding which of the
|
||||
# real snapshots are old enough to delete.
|
||||
if [ "${NAMED}" = "yes" ]; then
|
||||
echo "Named location, so leaving retention alone."
|
||||
else
|
||||
publish snapshot-purge
|
||||
fi
|
||||
@@ -1,58 +0,0 @@
|
||||
# This workflow requires the following configuration in Github
|
||||
#
|
||||
# Variables:
|
||||
# SONARQUBE_PROJECT_KEY - The project key in SonarQube
|
||||
#
|
||||
# Secrets:
|
||||
# SONARQUBE_TOKEN - SonarQube API token
|
||||
# SONARQUBE_HOST - The URL of the SonarQube host
|
||||
|
||||
name: SonarQube scan
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ "master" ]
|
||||
|
||||
concurrency:
|
||||
group: sonarqube
|
||||
cancel-in-progress: false
|
||||
|
||||
jobs:
|
||||
run-sonarqube-scan:
|
||||
# Only run if the project key is set
|
||||
if: vars.SONARQUBE_PROJECT_KEY != null
|
||||
|
||||
runs-on: ubuntu-22.04
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Create the scan properties file
|
||||
run: |
|
||||
cat <<EOF > sonar-project.properties
|
||||
sonar.projectKey=${{ vars.SONARQUBE_PROJECT_KEY }}
|
||||
sonar.projectName=pgAdmin 4
|
||||
sonar.projectVersion=%VERSION%
|
||||
|
||||
# Ignore templates and SQL scripts as they confuse the scanner
|
||||
sonar.exclusions=**/templates/**/*, **/*.sql
|
||||
|
||||
# Let SonarQube know where tests can be found
|
||||
sonar.test.inclusions=**/tests/**, web/regression
|
||||
|
||||
# Python compatibility
|
||||
sonar.python.version=3.9, 3.10, 3.11, 3.12, 3.13, 3.14
|
||||
EOF
|
||||
|
||||
APP_RELEASE=`grep "^APP_RELEASE" web/config.py | cut -d"=" -f2 | sed 's/ //g'`
|
||||
APP_REVISION=`grep "^APP_REVISION" web/config.py | cut -d"=" -f2 | sed 's/ //g'`
|
||||
APP_LONG_VERSION=${APP_RELEASE}.${APP_REVISION}
|
||||
sed -i "s/%VERSION%/${APP_LONG_VERSION}/g" sonar-project.properties
|
||||
|
||||
- name: SonarQube Scan
|
||||
uses: sonarsource/sonarqube-scan-action@22918119ff8e1ca75a623e15c8296b6ea4fbe28f # v8.2.1
|
||||
env:
|
||||
SONAR_TOKEN: ${{ secrets.SONARQUBE_TOKEN }}
|
||||
SONAR_HOST_URL: ${{ secrets.SONARQUBE_HOST }}
|
||||
@@ -42,7 +42,7 @@ RUN --mount=type=bind,source=.git,target=/pgadmin4/.git \
|
||||
export CPPFLAGS="-DPNG_ARM_NEON_OPT=0" && \
|
||||
npm install -g corepack && \
|
||||
corepack enable && \
|
||||
yarn install && \
|
||||
yarn install --immutable && \
|
||||
yarn run bundle && \
|
||||
rm -rf yarn.lock \
|
||||
package.json \
|
||||
@@ -59,8 +59,15 @@ RUN --mount=type=bind,source=.git,target=/pgadmin4/.git \
|
||||
FROM python:3-alpine AS env-builder
|
||||
|
||||
# Install dependencies
|
||||
#
|
||||
# libpq is a runtime dependency of psycopg's C extension: psycopg[c] is built
|
||||
# from source against postgresql-dev, but that is removed with build-deps
|
||||
# below, so libpq.so.5 must be installed separately to survive. Without it,
|
||||
# importing psycopg (which the Sphinx docs build does, via config.py) fails
|
||||
# with "no pq wrapper available".
|
||||
RUN apk add --no-cache \
|
||||
make && \
|
||||
make \
|
||||
libpq && \
|
||||
apk add --no-cache --virtual build-deps \
|
||||
build-base \
|
||||
openssl-dev \
|
||||
@@ -77,6 +84,13 @@ RUN python3 -m venv --system-site-packages --without-pip /venv && \
|
||||
/venv/bin/python3 -m pip install --no-cache-dir -r requirements.txt && \
|
||||
apk del --no-cache build-deps
|
||||
|
||||
# Remove the files pgAdmin never uses. The script reads the language list from
|
||||
# web/config.py, so it is copied in separately to avoid a change to it
|
||||
# reinstalling the requirements above.
|
||||
COPY pkg/prune_site_packages.py /prune/pkg/
|
||||
COPY web/config.py /prune/web/
|
||||
RUN /venv/bin/python3 /prune/pkg/prune_site_packages.py /venv
|
||||
|
||||
#########################################################################
|
||||
# Now, create a documentation build container for the Sphinx docs
|
||||
#########################################################################
|
||||
|
||||
@@ -20,12 +20,18 @@ IF "%1" == "clean" (
|
||||
)
|
||||
|
||||
REM Main build sequence
|
||||
REM
|
||||
REM The clean runs first because :FETCH_PYTHON unpacks into the temp directory
|
||||
REM that it removes, and the fetch runs before :SET_ENVIRONMENT because the
|
||||
REM latter has no interpreter of its own to ask about the build.
|
||||
CALL :CLEAN || EXIT /B 1
|
||||
CALL :FETCH_PYTHON || EXIT /B 1
|
||||
CALL :SET_ENVIRONMENT
|
||||
CALL :VALIDATE_ENVIRONMENT || EXIT /B 1
|
||||
CALL :CLEAN || EXIT /B 1
|
||||
CALL :CREATE_VIRTUAL_ENV || EXIT /B 1
|
||||
CALL :CREATE_PYTHON_ENV || EXIT /B 1
|
||||
CALL :CREATE_RUNTIME_ENV || EXIT /B 1
|
||||
CALL :SIGN_COMPONENTS || EXIT /B 1
|
||||
CALL :GENERATE_SBOM || EXIT /B 1
|
||||
CALL :CREATE_INSTALLER || EXIT /B 1
|
||||
CALL :VERIFY_SIGNATURE || EXIT /B 1
|
||||
@@ -50,15 +56,78 @@ REM Main build sequence Ends
|
||||
EXIT /B 0
|
||||
|
||||
|
||||
:FETCH_PYTHON
|
||||
REM Fetch the Python this build uses, rather than using whatever happens to
|
||||
REM be installed on the machine running it. Which interpreter pgAdmin ships
|
||||
REM is a decision recorded in pkg\python-version.txt, not a property of the
|
||||
REM build agent, and this is the only place on Windows that reads it.
|
||||
REM
|
||||
REM The distribution comes from astral-sh/python-build-standalone: it is
|
||||
REM relocatable, published per exact CPython version, and is a plain tarball
|
||||
REM rather than an installer, so unpacking it raises no UAC prompt. That
|
||||
REM matters on the signing host, where the desktop session is logged in
|
||||
REM automatically and an elevation dialog waits forever for somebody who is
|
||||
REM not there.
|
||||
REM
|
||||
REM curl.exe and tar.exe have shipped in Windows since 1803, so this needs
|
||||
REM nothing installed to bootstrap itself, not even a Python.
|
||||
ECHO Reading the Python version...
|
||||
SET "PYTHON_VERSION="
|
||||
SET "PYTHON_BUILD_STANDALONE_TAG="
|
||||
FOR /F "eol=# usebackq tokens=1,2 delims==" %%a IN ("%WD%\pkg\python-version.txt") DO (
|
||||
IF "%%a" == "PYTHON_VERSION" SET "PYTHON_VERSION=%%b"
|
||||
IF "%%a" == "PYTHON_BUILD_STANDALONE_TAG" SET "PYTHON_BUILD_STANDALONE_TAG=%%b"
|
||||
)
|
||||
|
||||
IF "%PYTHON_VERSION%" == "" (
|
||||
ECHO PYTHON_VERSION is not set in pkg\python-version.txt.
|
||||
EXIT /B 1
|
||||
)
|
||||
IF "%PYTHON_BUILD_STANDALONE_TAG%" == "" (
|
||||
ECHO PYTHON_BUILD_STANDALONE_TAG is not set in pkg\python-version.txt.
|
||||
EXIT /B 1
|
||||
)
|
||||
|
||||
REM Split for the runtime build, where e.g. 3.13.15 becomes 313
|
||||
FOR /F "tokens=1,2,3 delims=." %%a IN ("%PYTHON_VERSION%") DO (
|
||||
SET "PYTHON_MAJOR=%%a"
|
||||
SET "PYTHON_MINOR=%%b"
|
||||
SET "PYTHON_REVISION=%%c"
|
||||
)
|
||||
|
||||
SET "PGADMIN_PYTHON_DIR=%TMPDIR%\python"
|
||||
SET "PYTHON_ARCHIVE=cpython-%PYTHON_VERSION%+%PYTHON_BUILD_STANDALONE_TAG%-x86_64-pc-windows-msvc-install_only.tar.gz"
|
||||
|
||||
IF NOT EXIST "%TMPDIR%" MKDIR "%TMPDIR%"
|
||||
|
||||
ECHO Downloading Python %PYTHON_VERSION%...
|
||||
curl.exe --fail --location --silent --show-error --output "%TMPDIR%\python.tar.gz" "https://github.com/astral-sh/python-build-standalone/releases/download/%PYTHON_BUILD_STANDALONE_TAG%/%PYTHON_ARCHIVE%" || EXIT /B 1
|
||||
|
||||
ECHO Unpacking Python...
|
||||
REM The archive unpacks to a single python\ directory.
|
||||
tar -x -f "%TMPDIR%\python.tar.gz" -C "%TMPDIR%" || EXIT /B 1
|
||||
DEL /q "%TMPDIR%\python.tar.gz" > nul 2>&1
|
||||
|
||||
"%PGADMIN_PYTHON_DIR%\python.exe" --version || EXIT /B 1
|
||||
|
||||
REM :CREATE_VIRTUAL_ENV needs virtualenv rather than the venv module, as it
|
||||
REM relocates the result. pip is bundled with the distribution, but ask
|
||||
REM ensurepip for it anyway so this does not quietly depend on that.
|
||||
ECHO Installing virtualenv...
|
||||
"%PGADMIN_PYTHON_DIR%\python.exe" -m ensurepip --upgrade || EXIT /B 1
|
||||
"%PGADMIN_PYTHON_DIR%\python.exe" -m pip install --upgrade pip virtualenv || EXIT /B 1
|
||||
|
||||
EXIT /B 0
|
||||
|
||||
|
||||
:SET_ENVIRONMENT
|
||||
ECHO Configuring the environment...
|
||||
IF "%PGADMIN_PYTHON_DIR%" == "" SET "PGADMIN_PYTHON_DIR=C:\Python314"
|
||||
IF "%PGADMIN_KRB5_DIR%" == "" SET "PGADMIN_KRB5_DIR=C:\Program Files\MIT\Kerberos"
|
||||
IF "%PGADMIN_POSTGRES_DIR%" == "" SET "PGADMIN_POSTGRES_DIR=C:\Program Files\PostgreSQL\17"
|
||||
IF "%PGADMIN_KRB5_DIR%" == "" SET "PGADMIN_KRB5_DIR=C:\build64\krb5"
|
||||
IF "%PGADMIN_POSTGRES_DIR%" == "" SET "PGADMIN_POSTGRES_DIR=C:\build64\postgresql"
|
||||
IF "%PGADMIN_INNOTOOL_DIR%" == "" SET "PGADMIN_INNOTOOL_DIR=C:\Program Files (x86)\Inno Setup 6"
|
||||
IF "%PGADMIN_VCREDIST_DIR%" == "" SET "PGADMIN_VCREDIST_DIR=C:\Program Files\Microsoft Visual Studio\2022\Community\VC\Redist\MSVC\14.40.33807"
|
||||
IF "%PGADMIN_VCREDIST_FILE%" == "" SET "PGADMIN_VCREDIST_FILE=vc_redist.x64.exe"
|
||||
IF "%PGADMIN_SIGNTOOL_DIR%" == "" SET "PGADMIN_SIGNTOOL_DIR=C:\Program Files (x86)\Windows Kits\10\bin\10.0.22621.0\x64"
|
||||
IF "%PGADMIN_SIGNTOOL_DIR%" == "" SET "PGADMIN_SIGNTOOL_DIR=C:\Program Files (x86)\Windows Kits\10\bin\10.0.26100.0\x64"
|
||||
IF "%PGADMIN_WINDOWS_CSC%" == "" SET "PGADMIN_WINDOWS_CSC="
|
||||
|
||||
REM Set additional variables we need
|
||||
@@ -74,11 +143,6 @@ REM Main build sequence Ends
|
||||
SET INSTALLERNAME=%APP_SHORTNAME%-%APP_MAJOR%.%APP_MINOR%-%APP_VERSION_SUFFIX%-x64.exe
|
||||
IF "%APP_VERSION_SUFFIX%" == "" SET INSTALLERNAME=%APP_SHORTNAME%-%APP_MAJOR%.%APP_MINOR%-x64.exe
|
||||
|
||||
REM get Python version for the runtime build ex. 3.9.2 will be 39
|
||||
FOR /f "tokens=1 DELims=." %%G IN ('%PGADMIN_PYTHON_DIR%/python.exe -c "import sys; print(sys.version.split(' ')[0])"') DO SET PYTHON_MAJOR=%%G
|
||||
FOR /f "tokens=2 DELims=." %%G IN ('%PGADMIN_PYTHON_DIR%/python.exe -c "import sys; print(sys.version.split(' ')[0])"') DO SET PYTHON_MINOR=%%G
|
||||
FOR /f "tokens=3 DELims=." %%G IN ('%PGADMIN_PYTHON_DIR%/python.exe -c "import sys; print(sys.version.split(' ')[0])"') DO SET PYTHON_REVISION=%%G
|
||||
|
||||
EXIT /B 0
|
||||
|
||||
|
||||
@@ -90,8 +154,8 @@ REM Main build sequence Ends
|
||||
ECHO Output directory: %DISTROOT%
|
||||
ECHO Installer name: %INSTALLERNAME%
|
||||
ECHO.
|
||||
ECHO Python directory: %PGADMIN_PYTHON_DIR%
|
||||
ECHO Python version: %PYTHON_MAJOR%.%PYTHON_MINOR%.%PYTHON_REVISION%
|
||||
ECHO Python directory: %PGADMIN_PYTHON_DIR%
|
||||
ECHO.
|
||||
ECHO KRB5 directory: %PGADMIN_KRB5_DIR%
|
||||
ECHO PostgreSQL directory: %PGADMIN_POSTGRES_DIR%
|
||||
@@ -113,41 +177,16 @@ REM Main build sequence Ends
|
||||
ECHO ****************************************************************
|
||||
|
||||
ECHO Checking the environment...
|
||||
IF NOT EXIST "%PGADMIN_INNOTOOL_DIR%" (
|
||||
ECHO !PGADMIN_INNOTOOL_DIR! does not exist
|
||||
ECHO Please install InnoTool and set the PGADMIN_INNOTOOL_DIR environment variable.
|
||||
EXIT /B 1
|
||||
)
|
||||
|
||||
IF NOT EXIST "%PGADMIN_VCREDIST_DIR%" (
|
||||
ECHO !PGADMIN_VCREDIST_DIR! does not exist
|
||||
ECHO Please install Microsoft Visual studio and set the PGADMIN_VCREDIST_DIR environment variable.
|
||||
EXIT /B 1
|
||||
)
|
||||
|
||||
IF NOT EXIST "%PGADMIN_KRB5_DIR%" (
|
||||
ECHO !PGADMIN_KRB5_DIR! does not exist.
|
||||
ECHO Please install MIT Kerberos for Windows and set the PGADMIN_KRB5_DIR environment variable.
|
||||
EXIT /B 1
|
||||
)
|
||||
|
||||
IF NOT EXIST "%PGADMIN_PYTHON_DIR%" (
|
||||
ECHO !PGADMIN_PYTHON_DIR! does not exist.
|
||||
ECHO Please install Python and set the PGADMIN_PYTHON_DIR environment variable.
|
||||
EXIT /B 1
|
||||
)
|
||||
|
||||
IF NOT EXIST "%PGADMIN_POSTGRES_DIR%" (
|
||||
ECHO !PGADMIN_POSTGRES_DIR! does not exist.
|
||||
ECHO Please install PostgreSQL and set the PGADMIN_POSTGRES_DIR environment variable.
|
||||
EXIT /B 1
|
||||
)
|
||||
|
||||
IF NOT EXIST "%PGADMIN_PYTHON_DIR%\Scripts\virtualenv.exe" (
|
||||
ECHO !PGADMIN_PYTHON_DIR!\Scripts\virtualenv.exe does not exist.
|
||||
ECHO Please install the virtualenv package in Python.
|
||||
EXIT /B 1
|
||||
)
|
||||
REM The checks are made by :REQUIRE_DIR rather than inline, because the
|
||||
REM message names the directory that was looked for, and a default such as
|
||||
REM "C:\Program Files (x86)\Inno Setup 6" carries a closing parenthesis.
|
||||
REM Inside a parenthesised block that parenthesis ends the block, as cmd
|
||||
REM expands the variable when it parses the block rather than when it runs it,
|
||||
REM so the block failed to parse whether or not the directory was there.
|
||||
CALL :REQUIRE_DIR "%PGADMIN_INNOTOOL_DIR%" "Please install InnoTool and set the PGADMIN_INNOTOOL_DIR environment variable." || EXIT /B 1
|
||||
CALL :REQUIRE_DIR "%PGADMIN_VCREDIST_DIR%" "Please install Microsoft Visual Studio and set the PGADMIN_VCREDIST_DIR environment variable." || EXIT /B 1
|
||||
CALL :REQUIRE_DIR "%PGADMIN_KRB5_DIR%" "Please install MIT Kerberos for Windows, from the winpgbuild project or elsewhere, and set the PGADMIN_KRB5_DIR environment variable." || EXIT /B 1
|
||||
CALL :REQUIRE_DIR "%PGADMIN_POSTGRES_DIR%" "Please install PostgreSQL, from the winpgbuild project or elsewhere, and set the PGADMIN_POSTGRES_DIR environment variable." || EXIT /B 1
|
||||
|
||||
SET "PATH=%PGADMIN_POSTGRES_DIR%\bin;%PATH%"
|
||||
|
||||
@@ -185,24 +224,29 @@ REM Main build sequence Ends
|
||||
ECHO Staging Python...
|
||||
MKDIR "%BUILDROOT%\python\Lib" || EXIT /B 1
|
||||
|
||||
ECHO Downloading embedded Python...
|
||||
REM Get the python embeddable and extract it to %BUILDROOT%\python
|
||||
CD "%TMPDIR%
|
||||
%PGADMIN_PYTHON_DIR%\python -c "import sys; from urllib.request import urlretrieve; urlretrieve('https://www.python.org/ftp/python/' + sys.version.split(' ')[0] + '/python-' + sys.version.split(' ')[0] + '-embed-amd64.zip', 'python-embedded.zip')" || EXIT /B 1
|
||||
%PGADMIN_PYTHON_DIR%\python -c "import zipfile; z = zipfile.ZipFile('python-embedded.zip', 'r'); z.extractall('../win-build/python/')" || EXIT /B 1
|
||||
ECHO Downloading embedded Python %PYTHON_VERSION%...
|
||||
REM What ships is the embeddable distribution from python.org, a different
|
||||
REM build from the one :FETCH_PYTHON downloaded, and it has to be the same
|
||||
REM CPython version: the extension modules in site-packages were compiled
|
||||
REM against the build Python and are about to be run under this one. Both
|
||||
REM therefore read pkg\python-version.txt, rather than either asking the
|
||||
REM other what it is.
|
||||
curl.exe --fail --location --silent --show-error --output "%TMPDIR%\python-embedded.zip" "https://www.python.org/ftp/python/%PYTHON_VERSION%/python-%PYTHON_VERSION%-embed-amd64.zip" || EXIT /B 1
|
||||
tar -x -f "%TMPDIR%\python-embedded.zip" -C "%BUILDROOT%\python" || EXIT /B 1
|
||||
|
||||
ECHO Copying site-packages...
|
||||
XCOPY /S /I /E /H /Y "%TMPDIR%\venv\Lib\site-packages" "%BUILDROOT%\python\Lib\site-packages" > nul || EXIT /B 1
|
||||
|
||||
REM This is done to the copy rather than the venv, which still needs pip to
|
||||
REM install Sphinx for the docs in :CREATE_RUNTIME_ENV.
|
||||
ECHO Removing unused files from the installed packages...
|
||||
"%TMPDIR%\venv\Scripts\python.exe" "%WD%\pkg\prune_site_packages.py" "%BUILDROOT%\python\Lib\site-packages" || EXIT /B 1
|
||||
|
||||
REM NOTE: There is intentionally no space after "site" in the line below, to prevent Python barfing if there's one in the file
|
||||
ECHO import site>> "%BUILDROOT%\python\python%PYTHON_MAJOR%%PYTHON_MINOR%._pth"
|
||||
|
||||
ECHO Staging Kerberos components...
|
||||
COPY "%PGADMIN_KRB5_DIR%\bin\kinit.exe" "%BUILDROOT%\python" > nul || EXIT /B 1
|
||||
COPY "%PGADMIN_KRB5_DIR%\bin\krb5_64.dll" "%BUILDROOT%\python" > nul || EXIT /B 1
|
||||
COPY "%PGADMIN_KRB5_DIR%\bin\comerr64.dll" "%BUILDROOT%\python" > nul || EXIT /B 1
|
||||
COPY "%PGADMIN_KRB5_DIR%\bin\k5sprt64.dll" "%BUILDROOT%\python" > nul || EXIT /B 1
|
||||
COPY "%PGADMIN_KRB5_DIR%\bin\gssapi64.dll" "%BUILDROOT%\python" > nul || EXIT /B 1
|
||||
REM NOTE: The Kerberos components are staged into the runtime directory
|
||||
REM alongside libpq.dll, not here. See :CREATE_RUNTIME_ENV.
|
||||
|
||||
ECHO Cleaning up unnecessary .pyc and .pyo files...
|
||||
FOR /R "%BUILDROOT%\python" %%f in (*.pyc *.pyo) do DEL /q "%%f" 1> nul 2>&1
|
||||
@@ -232,7 +276,7 @@ REM Main build sequence Ends
|
||||
EXIT /B 1
|
||||
)
|
||||
CALL yarn set version %YARN_VERSION% || EXIT /B 1
|
||||
CALL yarn install || EXIT /B 1
|
||||
CALL yarn install --immutable || EXIT /B 1
|
||||
CALL npm rebuild || EXIT /B 1
|
||||
|
||||
ECHO Bundling javascript...
|
||||
@@ -310,9 +354,33 @@ REM Main build sequence Ends
|
||||
ECHO Downloading Electron to %TMPDIR%...
|
||||
REM Get a fresh copy of electron.
|
||||
|
||||
REM WGET
|
||||
FOR /f "tokens=*" %%i IN ('npm info electron version') DO SET "ELECTRON_VERSION=%%i"
|
||||
REM Resolve the electron version from runtime\yarn.lock, NOT from the npm
|
||||
REM registry and NOT from the range in runtime\package.json. The registry's
|
||||
REM `latest` dist-tag lands any newly published electron release in shipped
|
||||
REM binaries without review, whilst the package.json range is only a lower
|
||||
REM bound, so yarn is free to resolve it to a build other than the one we
|
||||
REM ship. The lockfile is the single source of truth, and `yarn info`
|
||||
REM reports its resolution without touching the network or needing
|
||||
REM node_modules.
|
||||
REM
|
||||
REM It must run inside runtime\ though: outside a yarn project, `yarn info`
|
||||
REM silently falls back to querying the registry and still exits 0, so check
|
||||
REM the lockfile is present first and validate what comes back.
|
||||
IF NOT EXIST "%WD%\runtime\yarn.lock" (
|
||||
ECHO ERROR: "%WD%\runtime\yarn.lock" not found; cannot resolve the pinned Electron version.
|
||||
EXIT /B 1
|
||||
)
|
||||
|
||||
SET "ELECTRON_VERSION="
|
||||
PUSHD "%WD%\runtime" || EXIT /B 1
|
||||
FOR /f "delims=" %%i IN ('yarn info electron --json ^| node -e "const lines=require('fs').readFileSync(0,'utf8').split('\n').filter(Boolean);const pkg=lines.map(l=>{try{return JSON.parse(l);}catch(e){return null;}}).find(o=>o&&typeof o.value==='string'&&o.value.startsWith('electron@npm:'));const version=(pkg&&pkg.children&&pkg.children.Version)||'';process.stdout.write(/^[0-9]+[.][0-9]+[.][0-9]+(-[0-9A-Za-z.-]+)?$/.test(version)?version:'');"') DO SET "ELECTRON_VERSION=%%i"
|
||||
POPD
|
||||
IF "%ELECTRON_VERSION%"=="" (
|
||||
ECHO ERROR: Could not resolve the pinned Electron version from "%WD%\runtime\yarn.lock".
|
||||
EXIT /B 1
|
||||
)
|
||||
|
||||
REM WGET
|
||||
:GET_NW
|
||||
wget https://github.com/electron/electron/releases/download/v%ELECTRON_VERSION%/electron-v%ELECTRON_VERSION%-win32-x64.zip -O "%TMPDIR%\electron-v%ELECTRON_VERSION%-win32-x64.zip"
|
||||
IF %ERRORLEVEL% NEQ 0 GOTO GET_NW
|
||||
@@ -336,34 +404,49 @@ REM Main build sequence Ends
|
||||
%TMPDIR%\rcedit-x64.exe "%BUILDROOT%\runtime\pgAdmin4.exe" --set-version-string "ProductName" "%APP_NAME%"
|
||||
%TMPDIR%\rcedit-x64.exe "%BUILDROOT%\runtime\pgAdmin4.exe" --set-product-version "%APP_VERSION%"
|
||||
|
||||
IF NOT "%PGADMIN_WINDOWS_CSC%" == "" (
|
||||
ECHO Attempting to sign the pgAdmin4.exe...
|
||||
CALL "%PGADMIN_SIGNTOOL_DIR%\signtool.exe" sign /sm /n "%PGADMIN_WINDOWS_CSC%" /tr http://timestamp.digicert.com /td sha256 /fd sha1 /v "%BUILDROOT%\runtime\pgAdmin4.exe"
|
||||
IF %ERRORLEVEL% NEQ 0 (
|
||||
ECHO.
|
||||
ECHO ************************************************************
|
||||
ECHO * Failed to sign the pgAdmin4.exe
|
||||
ECHO ************************************************************
|
||||
PAUSE
|
||||
)
|
||||
) ELSE (
|
||||
ECHO Skipping code signing ^(PGADMIN_WINDOWS_CSC is not set^)...
|
||||
)
|
||||
|
||||
ECHO Staging PostgreSQL components...
|
||||
COPY "%PGADMIN_POSTGRES_DIR%\bin\libpq.dll" "%BUILDROOT%\runtime" > nul || EXIT /B 1
|
||||
COPY "%PGADMIN_POSTGRES_DIR%\bin\libcrypto-*-x64.dll" "%BUILDROOT%\runtime" > nul || EXIT /B 1
|
||||
COPY "%PGADMIN_POSTGRES_DIR%\bin\libssl-*-x64.dll" "%BUILDROOT%\runtime" > nul || EXIT /B 1
|
||||
IF EXIST "%PGADMIN_POSTGRES_DIR%\bin\libintl-*.dll" COPY "%PGADMIN_POSTGRES_DIR%\bin\libintl-*.dll" "%BUILDROOT%\runtime" > nul
|
||||
IF EXIST "%PGADMIN_POSTGRES_DIR%\bin\libiconv-*.dll" COPY "%PGADMIN_POSTGRES_DIR%\bin\libiconv-*.dll" "%BUILDROOT%\runtime" > nul
|
||||
IF EXIST "%PGADMIN_POSTGRES_DIR%\bin\liblz4.dll" COPY "%PGADMIN_POSTGRES_DIR%\bin\liblz4.dll" "%BUILDROOT%\runtime" > nul
|
||||
IF EXIST "%PGADMIN_POSTGRES_DIR%\bin\libzstd.dll" COPY "%PGADMIN_POSTGRES_DIR%\bin\libzstd.dll" "%BUILDROOT%\runtime" > nul
|
||||
IF EXIST "%PGADMIN_POSTGRES_DIR%\bin\libintl-*.dll" (
|
||||
COPY "%PGADMIN_POSTGRES_DIR%\bin\libintl-*.dll" "%BUILDROOT%\runtime" > nul || EXIT /B 1
|
||||
)
|
||||
IF EXIST "%PGADMIN_POSTGRES_DIR%\bin\libiconv-*.dll" (
|
||||
COPY "%PGADMIN_POSTGRES_DIR%\bin\libiconv-*.dll" "%BUILDROOT%\runtime" > nul || EXIT /B 1
|
||||
)
|
||||
IF EXIST "%PGADMIN_POSTGRES_DIR%\bin\liblz4.dll" (
|
||||
COPY "%PGADMIN_POSTGRES_DIR%\bin\liblz4.dll" "%BUILDROOT%\runtime" > nul || EXIT /B 1
|
||||
)
|
||||
IF EXIST "%PGADMIN_POSTGRES_DIR%\bin\libzstd.dll" (
|
||||
COPY "%PGADMIN_POSTGRES_DIR%\bin\libzstd.dll" "%BUILDROOT%\runtime" > nul || EXIT /B 1
|
||||
)
|
||||
COPY "%PGADMIN_POSTGRES_DIR%\bin\zlib1.dll" "%BUILDROOT%\runtime" > nul || EXIT /B 1
|
||||
COPY "%PGADMIN_POSTGRES_DIR%\bin\pg_dump.exe" "%BUILDROOT%\runtime" > nul || EXIT /B 1
|
||||
COPY "%PGADMIN_POSTGRES_DIR%\bin\pg_dumpall.exe" "%BUILDROOT%\runtime" > nul || EXIT /B 1L%
|
||||
COPY "%PGADMIN_POSTGRES_DIR%\bin\pg_dumpall.exe" "%BUILDROOT%\runtime" > nul || EXIT /B 1
|
||||
COPY "%PGADMIN_POSTGRES_DIR%\bin\pg_restore.exe" "%BUILDROOT%\runtime" > nul || EXIT /B 1
|
||||
COPY "%PGADMIN_POSTGRES_DIR%\bin\psql.exe" "%BUILDROOT%\runtime" > nul || EXIT /B 1
|
||||
|
||||
REM The Kerberos runtime belongs here, next to libpq.dll and the client
|
||||
REM binaries: PostgreSQL 18 and later are built with GSSAPI support, so
|
||||
REM libpq.dll has a load-time dependency on gssapi64.dll, and pg_dump.exe
|
||||
REM and friends resolve it from their own directory. The pgAdmin server
|
||||
REM process finds it here too, because the runtime prepends this directory
|
||||
REM to PATH before spawning Python.
|
||||
REM
|
||||
REM krb5_64.dll loads krbcc64.dll dynamically rather than importing it, but
|
||||
REM it is not optional: on Windows krb5 defaults the credential cache type
|
||||
REM to CCAPI, and the entry points for that are left null when the DLL
|
||||
REM cannot be loaded, so the first use of the default credential cache
|
||||
REM calls address zero. ccapiserver.exe is the process krbcc64.dll drives.
|
||||
ECHO Staging Kerberos components...
|
||||
COPY "%PGADMIN_KRB5_DIR%\bin\kinit.exe" "%BUILDROOT%\runtime" > nul || EXIT /B 1
|
||||
COPY "%PGADMIN_KRB5_DIR%\bin\krb5_64.dll" "%BUILDROOT%\runtime" > nul || EXIT /B 1
|
||||
COPY "%PGADMIN_KRB5_DIR%\bin\comerr64.dll" "%BUILDROOT%\runtime" > nul || EXIT /B 1
|
||||
COPY "%PGADMIN_KRB5_DIR%\bin\k5sprt64.dll" "%BUILDROOT%\runtime" > nul || EXIT /B 1
|
||||
COPY "%PGADMIN_KRB5_DIR%\bin\gssapi64.dll" "%BUILDROOT%\runtime" > nul || EXIT /B 1
|
||||
COPY "%PGADMIN_KRB5_DIR%\bin\krbcc64.dll" "%BUILDROOT%\runtime" > nul || EXIT /B 1
|
||||
COPY "%PGADMIN_KRB5_DIR%\bin\ccapiserver.exe" "%BUILDROOT%\runtime" > nul || EXIT /B 1
|
||||
|
||||
ECHO Staging VC++ runtime...
|
||||
MKDIR "%BUILDROOT%\installer" || EXIT /B 1
|
||||
COPY "%PGADMIN_VCREDIST_DIR%\%PGADMIN_VCREDIST_FILE%" "%BUILDROOT%\installer" > nul || EXIT /B 1
|
||||
@@ -390,8 +473,12 @@ REM Main build sequence Ends
|
||||
DEL /s "%WD%\pkg\win32\installer.iss.in_stage*" > nul
|
||||
|
||||
ECHO Creating windows installer using INNO tool...
|
||||
REM Inno signs the installer and its uninstaller itself, one file per call,
|
||||
REM through the same script as :SIGN_FILES. Inno replaces $q with a quote
|
||||
REM and $f with the already-quoted file name. The outer pair of quotes is
|
||||
REM for cmd /c, which strips the first and last quote of its command line.
|
||||
IF NOT "%PGADMIN_WINDOWS_CSC%" == "" (
|
||||
CALL "%PGADMIN_INNOTOOL_DIR%\ISCC.exe" "%WD%\pkg\win32\installer.iss" "/SpgAdminSigntool=%PGADMIN_SIGNTOOL_DIR%\signtool.exe sign /sm /n $q%PGADMIN_WINDOWS_CSC%$q /tr http://timestamp.digicert.com /td sha256 /fd sha1 /v $f" || EXIT /B 1
|
||||
CALL "%PGADMIN_INNOTOOL_DIR%\ISCC.exe" "%WD%\pkg\win32\installer.iss" "/DSIGNED" "/SpgAdminSigntool=%ComSpec% /c $q$q%WD%\pkg\win32\sign-files.bat$q $q%PGADMIN_WINDOWS_CSC%$q $f$q" || EXIT /B 1
|
||||
) ELSE (
|
||||
CALL "%PGADMIN_INNOTOOL_DIR%\ISCC.exe" "%WD%\pkg\win32\installer.iss" || EXIT /B 1
|
||||
)
|
||||
@@ -420,17 +507,86 @@ REM Main build sequence Ends
|
||||
ECHO Verifying the installer signature...
|
||||
|
||||
CALL "%PGADMIN_SIGNTOOL_DIR%\signtool.exe" verify /pa /v "%DISTROOT%\%INSTALLERNAME%"
|
||||
IF %ERRORLEVEL% NEQ 0 (
|
||||
REM PAUSE was the old handler here, which on the signing host either returns
|
||||
REM immediately, leaving the build to carry on with an unverified installer,
|
||||
REM or blocks until the job times out. There is nobody at that machine to
|
||||
REM press a key.
|
||||
IF ERRORLEVEL 1 (
|
||||
ECHO.
|
||||
ECHO ************************************************************
|
||||
ECHO * Failed to verify signature of the installer
|
||||
ECHO ************************************************************
|
||||
PAUSE
|
||||
EXIT /B 1
|
||||
)
|
||||
|
||||
EXIT /B 0
|
||||
|
||||
|
||||
REM Sign one or more files, passed as quoted arguments, with a SHA-256 file
|
||||
REM digest. pkg\win32\sign-files.bat does the work, in signtool's split
|
||||
REM digest, sign and ingest steps, because the one-step form cannot use a
|
||||
REM SHA-2 digest with the card's key; the script explains why. All the files
|
||||
REM go in one call because each call costs one PIN prompt, and their names
|
||||
REM must be unique, which the script checks.
|
||||
:SIGN_FILES
|
||||
IF "%PGADMIN_WINDOWS_CSC%" == "" EXIT /B 0
|
||||
|
||||
CALL "%WD%\pkg\win32\sign-files.bat" "%PGADMIN_WINDOWS_CSC%" %*
|
||||
REM IF ERRORLEVEL, not IF %ERRORLEVEL%: the latter would be expanded before
|
||||
REM the script had run were this ever moved inside a parenthesised block.
|
||||
IF ERRORLEVEL 1 (
|
||||
ECHO.
|
||||
ECHO ************************************************************
|
||||
ECHO * Failed to sign one or more files
|
||||
ECHO ************************************************************
|
||||
EXIT /B 1
|
||||
)
|
||||
|
||||
EXIT /B 0
|
||||
|
||||
|
||||
REM Sign the components that we build ourselves: the runtime executable, and
|
||||
REM the PostgreSQL and Kerberos utilities and libraries obtained from the
|
||||
REM winpgbuild project, all of which are staged into the runtime directory by
|
||||
REM :CREATE_RUNTIME_ENV. The Electron, Python and VC++ runtime components are
|
||||
REM deliberately left alone, as they are third party binaries that we do not
|
||||
REM build, and signing them would replace any signature of their own. Names
|
||||
REM are matched as patterns as some of the libraries include version numbers,
|
||||
REM and some of them are optional.
|
||||
:SIGN_COMPONENTS
|
||||
IF "%PGADMIN_WINDOWS_CSC%" == "" (
|
||||
ECHO Skipping code signing ^(PGADMIN_WINDOWS_CSC is not set^)...
|
||||
EXIT /B 0
|
||||
)
|
||||
|
||||
ECHO Attempting to sign the pgAdmin, PostgreSQL and Kerberos components...
|
||||
|
||||
SETLOCAL EnableDelayedExpansion
|
||||
SET "COMPONENTS="
|
||||
REM FOR expands an item containing a wildcard against the current directory
|
||||
REM and silently drops it when nothing matches there, so the loop must run
|
||||
REM in the runtime directory for the versioned libraries to be found at
|
||||
REM all. Plain names are passed through whether or not the file exists,
|
||||
REM hence the IF EXIST for the optional ones.
|
||||
PUSHD "%BUILDROOT%\runtime" || EXIT /B 1
|
||||
FOR %%f IN (pgAdmin4.exe libpq.dll libcrypto-*-x64.dll libssl-*-x64.dll libintl-*.dll libiconv-*.dll liblz4.dll libzstd.dll zlib1.dll pg_dump.exe pg_dumpall.exe pg_restore.exe psql.exe kinit.exe krb5_64.dll comerr64.dll k5sprt64.dll gssapi64.dll krbcc64.dll ccapiserver.exe) DO (
|
||||
IF EXIST "%%f" SET "COMPONENTS=!COMPONENTS! "%BUILDROOT%\runtime\%%f""
|
||||
)
|
||||
POPD
|
||||
IF "!COMPONENTS!" == "" (
|
||||
ECHO.
|
||||
ECHO ************************************************************
|
||||
ECHO * No components were found to sign
|
||||
ECHO ************************************************************
|
||||
ENDLOCAL
|
||||
EXIT /B 1
|
||||
)
|
||||
CALL :SIGN_FILES !COMPONENTS! || EXIT /B 1
|
||||
ENDLOCAL
|
||||
|
||||
EXIT /B 0
|
||||
|
||||
|
||||
:USAGE
|
||||
ECHO Invalid command line options.
|
||||
ECHO Usage: "Make.bat [clean]"
|
||||
@@ -441,3 +597,16 @@ REM Main build sequence Ends
|
||||
:CHECK_ROBOCOPY_ERROR
|
||||
IF %ERRORLEVEL% GEQ 8 EXIT /B %ERRORLEVEL%
|
||||
EXIT /B 0
|
||||
|
||||
|
||||
REM Check that a prerequisite directory exists, reporting the path that was
|
||||
REM looked for and how to correct it if it does not. The first argument is the
|
||||
REM directory and the second the hint; both are echoed here, outside any
|
||||
REM parenthesised block, so a parenthesis in either is just a character.
|
||||
:REQUIRE_DIR
|
||||
IF EXIST "%~1" EXIT /B 0
|
||||
|
||||
ECHO %~1 does not exist.
|
||||
ECHO %~2
|
||||
|
||||
EXIT /B 1
|
||||
@@ -25,7 +25,7 @@ appbundle:
|
||||
./pkg/mac/build.sh $(BUILD_OPTS)
|
||||
|
||||
install-node:
|
||||
cd web && yarn install
|
||||
cd web && yarn install --immutable
|
||||
|
||||
install-python:
|
||||
./tools/setup-python-env.sh
|
||||
|
||||
@@ -100,6 +100,10 @@ tab to provide options related to data or pgAdmin objects that correspond to *pg
|
||||
* Move the switch next to *Only schemas* to limit the back up to schema-level
|
||||
database objects.
|
||||
|
||||
* Move the switch next to *Only statistics* to the *Yes* position to limit
|
||||
the back up to optimizer statistics. **Note:** This option is visible only
|
||||
for database server greater than or equal to 18.
|
||||
|
||||
* Move the switch next to *Blobs* to the *No* position to exclude large
|
||||
objects in the backup.
|
||||
|
||||
@@ -143,6 +147,22 @@ tab to provide options related to data or pgAdmin objects that correspond to *pg
|
||||
Table access methods. **Note:** This option is visible only for
|
||||
database server greater than or equal to 15.
|
||||
|
||||
* Move the switch next to *Row security policies* to the *Yes* position to
|
||||
exclude row security policies. **Note:** This option is visible only for
|
||||
database server greater than or equal to 18.
|
||||
|
||||
* Move the switch next to *Data* to the *Yes* position to exclude data from
|
||||
the backup. **Note:** This option is visible only for database server
|
||||
greater than or equal to 18.
|
||||
|
||||
* Move the switch next to *Schema* to the *Yes* position to exclude schema
|
||||
(the data definitions) from the backup. **Note:** This option is visible
|
||||
only for database server greater than or equal to 18.
|
||||
|
||||
* Move the switch next to *Statistics* to the *Yes* position to exclude
|
||||
optimizer statistics. **Note:** This option is visible only for database
|
||||
server greater than or equal to 18.
|
||||
|
||||
.. image:: images/backup_queries.png
|
||||
:alt: Queries option on backup dialog
|
||||
:align: center
|
||||
@@ -259,6 +279,15 @@ tab to provide other backup options.
|
||||
position to include a statement that will use a SET SESSION AUTHORIZATION
|
||||
command to determine object ownership (instead of an ALTER OWNER command).
|
||||
|
||||
* Move the switch next to *Dump statistics* to the *Yes* position to include
|
||||
optimizer statistics in the backup. It cannot be combined with *Only
|
||||
data*, *Only schemas* or *Do not save* *Statistics*. **Note:** This option
|
||||
is visible only for database server greater than or equal to 18.
|
||||
|
||||
* Move the switch next to *Sequence data* to the *Yes* position to include
|
||||
sequence data that would normally be excluded. **Note:** This option is
|
||||
visible only for database server greater than or equal to 18.
|
||||
|
||||
* Use the *Exclude schema* field to not dump schemas whose name matches
|
||||
pattern.
|
||||
|
||||
|
||||
@@ -45,6 +45,10 @@ tab to provide options related to data or pgAdmin objects that correspond to *pg
|
||||
|
||||
* Move the switch next to *Only roles* to limit the back up to roles only.
|
||||
|
||||
* Move the switch next to *Only statistics* to the *Yes* position to limit
|
||||
the back up to optimizer statistics. **Note:** This option is visible only
|
||||
for database server greater than or equal to 18.
|
||||
|
||||
.. image:: images/backup_server_do_not_save.png
|
||||
:alt: Do not save option on backup server dialog
|
||||
:align: center
|
||||
@@ -88,6 +92,22 @@ tab to provide options related to data or pgAdmin objects that correspond to *pg
|
||||
Table access methods. **Note:** This option is visible only for
|
||||
database server greater than or equal to 15.
|
||||
|
||||
* Move the switch next to *Row security policies* to the *Yes* position to
|
||||
exclude row security policies. **Note:** This option is visible only for
|
||||
database server greater than or equal to 18.
|
||||
|
||||
* Move the switch next to *Data* to the *Yes* position to exclude data from
|
||||
the backup. **Note:** This option is visible only for database server
|
||||
greater than or equal to 18.
|
||||
|
||||
* Move the switch next to *Schema* to the *Yes* position to exclude schema
|
||||
(the data definitions) from the backup. **Note:** This option is visible
|
||||
only for database server greater than or equal to 18.
|
||||
|
||||
* Move the switch next to *Statistics* to the *Yes* position to exclude
|
||||
optimizer statistics. **Note:** This option is visible only for database
|
||||
server greater than or equal to 18.
|
||||
|
||||
.. image:: images/backup_server_queries.png
|
||||
:alt: Queries option on backup server dialog
|
||||
:align: center
|
||||
@@ -171,6 +191,15 @@ tab to provide other backup options.
|
||||
position to include a statement that will use a SET SESSION AUTHORIZATION
|
||||
command to determine object ownership (instead of an ALTER OWNER command).
|
||||
|
||||
* Move the switch next to *Dump statistics* to the *Yes* position to include
|
||||
optimizer statistics in the backup. It cannot be combined with *Only
|
||||
data*, *Only schemas* or *Do not save* *Statistics*. **Note:** This option
|
||||
is visible only for database server greater than or equal to 18.
|
||||
|
||||
* Move the switch next to *Sequence data* to the *Yes* position to include
|
||||
sequence data that would normally be excluded. **Note:** This option is
|
||||
visible only for database server greater than or equal to 18.
|
||||
|
||||
* Use the *Exclude database* field to not dump databases whose name matches
|
||||
pattern.
|
||||
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
.. _change_password_dialog:
|
||||
|
||||
*******************************
|
||||
`Change Password Dialog`:index:
|
||||
*******************************
|
||||
**************************************
|
||||
`Change Server Password Dialog`:index:
|
||||
**************************************
|
||||
|
||||
It is a good policy to routinely change your password to protect data, even in
|
||||
what you may consider a 'safe' environment. In the workplace, failure to apply
|
||||
@@ -30,7 +30,9 @@ comprehensive list and they **will not guarantee security**.
|
||||
:alt: Change database password dialog
|
||||
:align: center
|
||||
|
||||
Use the *Change Password* dialog to change your password:
|
||||
Use the *Change Server Password* dialog to change the password of the role that
|
||||
pgAdmin uses to connect to the database server, rather than the password you use
|
||||
to log in to pgAdmin itself:
|
||||
|
||||
* The name displayed in the *User* field is the role for which you are modifying
|
||||
the password; it is the role that is associated with the server connection
|
||||
@@ -39,7 +41,7 @@ Use the *Change Password* dialog to change your password:
|
||||
* Enter the desired password for in the *New Password* field.
|
||||
* Re-enter the new password in the *Confirm Password* field.
|
||||
|
||||
Click the *Save* button to change your password.
|
||||
Click the *Change* button to change your password.
|
||||
|
||||
Click the *Close* button to exit without changing your password.
|
||||
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
.. _change_user_password:
|
||||
|
||||
************************************
|
||||
`Change User Password Dialog`:index:
|
||||
************************************
|
||||
***************************************
|
||||
`Change pgAdmin Password Dialog`:index:
|
||||
***************************************
|
||||
|
||||
It is a good policy to routinely change your password to protect data, even in
|
||||
what you may consider a 'safe' environment. In the workplace, failure to apply
|
||||
@@ -30,11 +30,18 @@ comprehensive list and they **will not guarantee security**.
|
||||
:alt: Change current user password dialog
|
||||
:align: center
|
||||
|
||||
Use the *Change Password* dialog to change your password:
|
||||
Use the *Change pgAdmin Password* dialog to change the password you use to log
|
||||
in to pgAdmin itself, rather than the password of a database server connection:
|
||||
|
||||
* Enter your existing password in the *Current Password* field.
|
||||
* Enter the desired password for in the *New Password* field.
|
||||
* Re-enter the new password in the *Confirm Password* field.
|
||||
|
||||
Click the *Change Password* button to change your password; click *Close* to
|
||||
exit the dialog.
|
||||
Click the *Change* button to change your password; click *Close* to exit the
|
||||
dialog.
|
||||
|
||||
Changing your password signs out all of your other active sessions, so anyone
|
||||
using your account elsewhere must sign in again with the new password. The
|
||||
session you change it in stays signed in. When an administrator resets your
|
||||
password from the :ref:`User Management <user_management>` page, all of your
|
||||
sessions are signed out.
|
||||
@@ -5,8 +5,8 @@
|
||||
*************************************
|
||||
|
||||
A PostgreSQL server can be deployed on the Amazon, Azure, Google cloud
|
||||
using this module. In future more cloud provider options will be
|
||||
available.
|
||||
or pgEdge Starfleet using this module. In future more cloud provider
|
||||
options will be available.
|
||||
|
||||
To launch the *Cloud Deployment...* tool, right click on the *Server Group* or
|
||||
*Server* of the tree control, and select *Deploy a Cloud Instance* from the
|
||||
@@ -18,3 +18,4 @@ To launch the *Cloud Deployment...* tool, right click on the *Server Group* or
|
||||
cloud_aws_rds
|
||||
cloud_azure_database
|
||||
cloud_google_cloud_sql
|
||||
cloud_pgedge_starfleet
|
||||
@@ -0,0 +1,102 @@
|
||||
.. _cloud_pgedge_starfleet:
|
||||
|
||||
************************************************
|
||||
`pgEdge Starfleet Deployment`:index:
|
||||
************************************************
|
||||
|
||||
To deploy a PostgreSQL database on pgEdge Starfleet, follow the steps below.
|
||||
pgAdmin creates a *Managed* database, which Starfleet hosts for you.
|
||||
|
||||
.. image:: images/cloud_provider_for_postgresql.png
|
||||
:alt: Cloud Deployment Provider
|
||||
:align: center
|
||||
|
||||
Once you launch the tool, select the pgEdge Starfleet option and click on the
|
||||
*Next* button to proceed.
|
||||
|
||||
.. image:: images/cloud_starfleet_credentials.png
|
||||
:alt: pgEdge Starfleet credentials
|
||||
:align: center
|
||||
|
||||
In the Credentials step, enter the *Auth ID* and *Auth secret* of a Starfleet
|
||||
API client. pgAdmin checks them with Starfleet when you click *Next*.
|
||||
|
||||
.. note:: Create the API client in the pgEdge Starfleet console, under
|
||||
*Settings > API Clients*. The Auth secret is shown only when the client is
|
||||
created, so keep a copy of it. See the
|
||||
`pgEdge Starfleet documentation <https://docs.pgedge.com/pgedge-starfleet/>`_
|
||||
for details.
|
||||
|
||||
.. image:: images/cloud_starfleet_instance.png
|
||||
:alt: pgEdge Starfleet instance specification
|
||||
:align: center
|
||||
|
||||
Use the fields from the Instance Specification step to specify the database
|
||||
details.
|
||||
|
||||
* Use the *Database name* field to name the database. The name may contain
|
||||
lowercase letters and digits only, must start with a letter, and can be up
|
||||
to 50 characters long.
|
||||
|
||||
* Use the *Display name* field to optionally set a friendlier name, up to 25
|
||||
characters long.
|
||||
|
||||
* Select the location to deploy the database to from the *Region* field.
|
||||
|
||||
* Use the *Size* field to select the size of the database.
|
||||
|
||||
* Use the *PostgreSQL version* field to select the PostgreSQL version.
|
||||
|
||||
* Use the *Allowed IP addresses* field to list the IPv4 addresses or CIDR
|
||||
ranges that may connect to the database, separated by commas. The field is
|
||||
prefilled with your public IP address. At least one and at most 50 entries
|
||||
are required; IPv6 addresses are not accepted. Without a matching entry the
|
||||
database will not accept connections from pgAdmin.
|
||||
|
||||
* Use the *Connect as* field to choose whether pgAdmin connects as *admin*
|
||||
(the database administrator, which is not a superuser) or *app* (an
|
||||
application user).
|
||||
|
||||
.. image:: images/cloud_starfleet_database.png
|
||||
:alt: pgEdge Starfleet database details
|
||||
:align: center
|
||||
|
||||
In the Database Details step, use the *pgAdmin server group* field to select
|
||||
the server group the new server will be registered in.
|
||||
|
||||
.. image:: images/cloud_starfleet_review.png
|
||||
:alt: pgEdge Starfleet review
|
||||
:align: center
|
||||
|
||||
Finally, review the summary in the Review step and click *Finish* to start the
|
||||
deployment. The progress of the deployment is shown in the same way as for the
|
||||
other cloud providers, and the new server is added to the *Object Explorer*
|
||||
when the database is available. pgAdmin connects with *sslmode* set to
|
||||
*require* and *gssencmode* set to *disable*.
|
||||
|
||||
If the deployment fails after the database has been created, for example
|
||||
because pgEdge Starfleet reports it as failed or it does not become available
|
||||
within 30 minutes, pgAdmin removes the server from the *Object Explorer* but
|
||||
does not delete the database. Check the pgEdge Starfleet console, and delete
|
||||
the database there if it is not needed.
|
||||
|
||||
.. image:: images/cloud_starfleet_password.png
|
||||
:alt: pgEdge Starfleet database password
|
||||
:align: center
|
||||
|
||||
pgEdge Starfleet generates the database password. When the deployment
|
||||
finishes, pgAdmin shows it once in the *pgEdge Starfleet database password*
|
||||
dialog, together with the host and user name.
|
||||
|
||||
* Use the *Copy* button to copy the password to the clipboard.
|
||||
|
||||
* Use the *Save password* button to save the password with the server. This
|
||||
button is only shown if saving passwords is permitted by the
|
||||
:ref:`ALLOW_SAVE_PASSWORD <config_py>` setting.
|
||||
|
||||
The password is not shown again in pgAdmin after the dialog is closed. If you
|
||||
lose it, reset it from the pgEdge Starfleet console.
|
||||
|
||||
.. note:: pgAdmin contacts the Starfleet API at the address given by the
|
||||
*STARFLEET_API_URL* configuration setting, which defaults to
|
||||
*https://api.pgedge.com*. The address must use HTTPS.
|
||||
@@ -1,8 +1,8 @@
|
||||
.. _config_py:
|
||||
|
||||
***************************
|
||||
`The config.py File`:index:
|
||||
***************************
|
||||
*****************************
|
||||
`Configuration Files`:index:
|
||||
*****************************
|
||||
|
||||
There are multiple configuration files that are read at startup by pgAdmin. These
|
||||
files are used for configuration options that:
|
||||
@@ -32,6 +32,12 @@ The configuration files are as follows:
|
||||
and may be created by users in the same directory as ``config.py`` if
|
||||
needed.
|
||||
|
||||
.. note:: On macOS, ``config.py`` is located inside the application bundle
|
||||
(``pgAdmin 4.app/Contents/Resources/web/``), which is replaced in its
|
||||
entirety when pgAdmin is upgraded, so a ``config_local.py`` file created
|
||||
there will be lost. Use ``config_system.py`` instead, which is stored
|
||||
outside the application bundle.
|
||||
|
||||
* ``config_system.py``: This file is read after ``config_local.py`` and is
|
||||
intended for system administrators to include settings that are configured
|
||||
system-wide from a secure location that users cannot normally modify and that
|
||||
|
||||
@@ -157,8 +157,10 @@ of the `Preferences JSON file <https://www.pgadmin.org/docs/pgadmin4/latest/pref
|
||||
|
||||
Override the default file path for the pgadmin configurations file.This can be used while provisioning
|
||||
container with read only root file system to achieve a more secure pgadmin4 deployment for kubernetes.
|
||||
Note that if you are externally mapping this file, then environment variables passed using *PGADMIN_CONFIG_*
|
||||
suffix will be ignored.
|
||||
Note that if you are externally mapping this file, then environment variables
|
||||
passed using *PGADMIN_CONFIG_* suffix will be ignored only when that file
|
||||
exists and is non-empty. A missing or empty file is generated with those
|
||||
values.
|
||||
|
||||
**PGPASS_FILE**
|
||||
|
||||
@@ -167,6 +169,24 @@ Set this variable to the path of a pgpass file if you want to supply passwords
|
||||
for the servers added in pgAdmin. On startup, the file is copied into the
|
||||
container and used as the ``.pgpass`` file.
|
||||
|
||||
**PUID**
|
||||
|
||||
*Default: 5050*
|
||||
|
||||
The user ID that pgAdmin runs as. This is only used when the container is
|
||||
started as root (for example with ``--user root``), in which case the
|
||||
*pgadmin* user is given this UID, the ownership of */var/lib/pgadmin* is
|
||||
changed to match, and pgAdmin is then run as that user rather than as root.
|
||||
It is ignored when the container is started as any other user, which is the
|
||||
default. The value must be numeric, and may not be 0.
|
||||
|
||||
**PGID**
|
||||
|
||||
*Default: 0*
|
||||
|
||||
The group ID that pgAdmin runs as. As with *PUID*, this is only used when the
|
||||
container is started as root.
|
||||
|
||||
**GUNICORN_ACCESS_LOGFILE**
|
||||
|
||||
*Default: -* (stdout)
|
||||
@@ -195,20 +215,62 @@ where it may be increased.
|
||||
|
||||
This is a variable prefix that can be used to override any of the configuration
|
||||
options in pgAdmin's *config.py* file. Add the *PGADMIN_CONFIG_* prefix to any
|
||||
variable name from *config.py* and give the value in the format 'string value'
|
||||
for strings, True/False for booleans or 123 for numbers. See below for an
|
||||
example.
|
||||
variable name from *config.py* and give the value as a Python expression:
|
||||
``'string value'`` (including the quotes) for strings, ``True`` or ``False``
|
||||
for booleans, or ``123`` for numbers. See below for an example.
|
||||
|
||||
Settings are written to */pgadmin4/config_distro.py* within the container, which
|
||||
is read after */pgadmin4/config.py* and before */pgadmin4/config_local.py*.
|
||||
Any settings given will therefore override anything in config.py, but can be
|
||||
overridden by settings in config_local.py.
|
||||
*PGADMIN_DEFAULT_* variables, including *PGADMIN_DEFAULT_EMAIL* and
|
||||
*PGADMIN_DEFAULT_PASSWORD*, are ordinary text and take no Python literal
|
||||
quotes. *PGADMIN_CONFIG_* values are written as Python code into
|
||||
*/pgadmin4/config_distro.py*, or into the file named by
|
||||
*PGADMIN_CUSTOM_CONFIG_DISTRO_FILE* if that is set. The values ``true`` and
|
||||
``false``, in any letter case, are written as ``True`` and ``False``. A bare
|
||||
hostname is a Python name, so ``MAIL_SERVER = mail.example.com`` raises
|
||||
``NameError`` at startup.
|
||||
|
||||
Settings are only written to */pgadmin4/config_distro.py* once, typically on
|
||||
first launch of the container. If */pgadmin4/config_distro.py* contains one or
|
||||
more lines, then no changes are made; for example, if the container instance is
|
||||
restarted, or */pgadmin4/config_distro.py* is mapped to a file on persistent
|
||||
storage (not recommended - use */pgadmin4/config_local.py* instead)!
|
||||
Quoting added by the shell or by YAML sits outside that Python expression and
|
||||
is removed before the value is written. The *docker run* example below keeps
|
||||
the Python quotes inside shell quotes, for example
|
||||
``-e 'PGADMIN_CONFIG_LOGIN_BANNER="Authorised users only!"'``.
|
||||
|
||||
In Docker Compose map syntax, YAML consumes the quotes around a scalar. A
|
||||
YAML-only quoted hostname, ``PGADMIN_CONFIG_MAIL_SERVER: "mail.example.com"``
|
||||
or ``PGADMIN_CONFIG_MAIL_SERVER: 'mail.example.com'``, reaches the container
|
||||
as ``mail.example.com`` and cannot serve as a Python string. Nest the Python
|
||||
quotes inside the scalar:
|
||||
``PGADMIN_CONFIG_MAIL_SERVER: "'mail.example.com'"``. The container then
|
||||
writes ``MAIL_SERVER = 'mail.example.com'``. ``"587"`` and ``"True"`` are
|
||||
already valid Python, so they need no nested quotes. The list form keeps
|
||||
single quotes, as ``docker run -e`` does
|
||||
(``- PGADMIN_CONFIG_MAIL_SERVER='mail.example.com'``); nesting them there
|
||||
produces ``"'mail.example.com'"``, which is valid Python but puts stray
|
||||
quote characters into the value and fails silently rather than at startup.
|
||||
An ``env_file`` value strips one layer of quotes, so the same nesting as the
|
||||
map form applies there. The Compose example in the Examples section uses the
|
||||
map form for an SMTP server, username, password, and *SECURITY_EMAIL_SENDER*.
|
||||
|
||||
Compose substitutes ``$VAR`` and ``${VAR}`` in Compose file values, and in
|
||||
double-quoted ``env_file`` values, so write a literal dollar as ``$$``.
|
||||
``"'pa$word'"`` reaches the container as ``'pa'``, with only a warning about
|
||||
an unset variable. The entrypoint does not expand the value again, so ``$$``
|
||||
is the only escaping required.
|
||||
|
||||
Settings are written to */pgadmin4/config_distro.py* within the container (or
|
||||
to the file named by *PGADMIN_CUSTOM_CONFIG_DISTRO_FILE*), which is read after
|
||||
*/pgadmin4/config.py* and before */pgadmin4/config_local.py*. Any settings
|
||||
given will therefore override anything in config.py, but can be overridden by
|
||||
settings in config_local.py.
|
||||
|
||||
The file is generated only when it is missing or empty, typically on first
|
||||
launch, and a non-empty file is left unchanged, including when the container
|
||||
is restarted. To apply corrected *PGADMIN_CONFIG_* values, recreate the
|
||||
container (``docker compose up -d`` does this when the environment changes,
|
||||
whereas ``docker compose restart`` does not), keeping the */var/lib/pgadmin*
|
||||
mapping to retain your data. If the file is on persistent storage, either
|
||||
because it is mapped (not recommended; use */pgadmin4/config_local.py*
|
||||
instead) or because *PGADMIN_CUSTOM_CONFIG_DISTRO_FILE* points there,
|
||||
recreating the container is not enough: edit the file, or empty it so that it
|
||||
is regenerated.
|
||||
|
||||
See :ref:`config_py` for more information on the available configuration settings.
|
||||
|
||||
@@ -220,13 +282,37 @@ host machine to allow configuration to be customised and shared between
|
||||
instances.
|
||||
|
||||
.. warning:: Warning: pgAdmin runs as the *pgadmin* user (UID: 5050) in the
|
||||
*pgadmin* group (GID: 5050) in the container. You must ensure that all files
|
||||
*root* group (GID: 0) in the container. You must ensure that all files
|
||||
are readable, and where necessary (e.g. the working/session directory)
|
||||
writeable for this user on the host machine. For example:
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
sudo chown -R 5050:5050 <host_directory>
|
||||
sudo chown -R 5050:0 <host_directory>
|
||||
|
||||
When using rootless Podman, user IDs in the container are mapped to a
|
||||
range of subordinate IDs on the host, so changing the ownership of a host
|
||||
directory to 5050 as shown above does not make it writeable by the
|
||||
container. Instead, either add the *U* option to the volume, which has
|
||||
Podman recursively change the ownership of the directory and everything
|
||||
in it to match the container's user, for example:
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
-v /path/to/pgadmin-data:/var/lib/pgadmin:U
|
||||
|
||||
or start the container as root, which in a rootless container is your own
|
||||
user on the host. The container will change the ownership of
|
||||
*/var/lib/pgadmin* and then run pgAdmin as the *pgadmin* user; see *PUID*
|
||||
and *PGID* above. For example:
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
--user root
|
||||
|
||||
Neither option changes SELinux labels, so on a host with SELinux enforcing,
|
||||
the volume also needs the *z* (shared) or *Z* (private) option to relabel
|
||||
it, for example ``-v /path/to/pgadmin-data:/var/lib/pgadmin:Z,U``.
|
||||
|
||||
On some filesystems that do not support extended attributes, it may not be
|
||||
possible to run pgAdmin without specifying a value for *PGADMIN_LISTEN_PORT*
|
||||
@@ -304,6 +390,37 @@ Run a simple container over port 80, setting some configuration options:
|
||||
-e 'PGADMIN_CONFIG_CONSOLE_LOG_LEVEL=10' \
|
||||
-d dpage/pgadmin4
|
||||
|
||||
Run pgAdmin with Docker Compose, publishing port 80 and sending mail through
|
||||
an external SMTP server. *PGADMIN_DEFAULT_EMAIL* and
|
||||
*PGADMIN_DEFAULT_PASSWORD* are ordinary strings. Each *PGADMIN_CONFIG_*
|
||||
string below includes the Python quotes that YAML would otherwise remove.
|
||||
*PGADMIN_DISABLE_POSTFIX* may be any non-empty value; it is ordinary text,
|
||||
not a Python expression. The named volume keeps */var/lib/pgadmin* when the
|
||||
container is recreated:
|
||||
|
||||
.. code-block:: yaml
|
||||
|
||||
services:
|
||||
pgadmin:
|
||||
image: dpage/pgadmin4
|
||||
ports:
|
||||
- "80:80"
|
||||
environment:
|
||||
PGADMIN_DEFAULT_EMAIL: user@domain.com
|
||||
PGADMIN_DEFAULT_PASSWORD: SuperSecret
|
||||
PGADMIN_DISABLE_POSTFIX: "1"
|
||||
PGADMIN_CONFIG_MAIL_SERVER: "'mail.example.com'"
|
||||
PGADMIN_CONFIG_MAIL_PORT: "587"
|
||||
PGADMIN_CONFIG_MAIL_USE_TLS: "True"
|
||||
PGADMIN_CONFIG_MAIL_USERNAME: "'smtp-user'"
|
||||
PGADMIN_CONFIG_MAIL_PASSWORD: "'s3cret=value'"
|
||||
PGADMIN_CONFIG_SECURITY_EMAIL_SENDER: "'pgadmin@example.com'"
|
||||
volumes:
|
||||
- pgadmin-data:/var/lib/pgadmin
|
||||
|
||||
volumes:
|
||||
pgadmin-data:
|
||||
|
||||
Run a TLS secured container using a shared config/storage directory in
|
||||
/private/var/lib/pgadmin on the host, and servers pre-loaded from
|
||||
/tmp/servers.json on the host:
|
||||
|
||||
@@ -15,8 +15,15 @@ to specify the number of rows you would like to display in the editor panel.
|
||||
|
||||
To modify the content of a table, each row in the table must be uniquely
|
||||
identifiable. If the table definition does not include an OID or a primary key,
|
||||
the displayed data is read only. Note that views cannot be edited; updatable
|
||||
views (using rules) are not supported.
|
||||
the displayed data is read only. Simple, automatically-updatable views (a
|
||||
single base table, with no ``INSTEAD OF`` triggers or rewrite rules, whose
|
||||
base table's primary key columns are all exposed in the view under their
|
||||
original names) can also be edited; updating and deleting rows is supported,
|
||||
but inserting new rows through a view is not, so the *Add row* and *Paste*
|
||||
buttons are disabled when editing a view. Materialized views, views based
|
||||
on more than one table, views exposing only part of a composite primary key,
|
||||
and views relying on ``INSTEAD OF`` triggers or rules created with
|
||||
``CREATE RULE`` remain read only.
|
||||
|
||||
The editor features a toolbar that allows quick access to frequently used
|
||||
options, and a work environment divided into two panels:
|
||||
|
||||
@@ -39,6 +39,8 @@ Use the fields in the *Definition* tab to define the function:
|
||||
* Use the drop-down listbox next to *Return type* to select the data type
|
||||
returned by the function, if any. If *Custom return type?* is enabled this field
|
||||
will change to an input text field.
|
||||
* Move the *Returns a set?* switch to indicate if the function returns a set
|
||||
that includes multiple rows. The default is *No*.
|
||||
* Use the drop-down listbox next to *Language* to select the implementation
|
||||
language. The default is *sql*.
|
||||
* Use the drop-down listbox next to *Depends on extensions* to select the extension that this function
|
||||
@@ -88,8 +90,6 @@ function:
|
||||
* *IMMUTABLE* indicates that the function cannot modify the database and
|
||||
always returns the same result when given the same argument values.
|
||||
|
||||
* Move the *Returns a Set?* switch to indicate if the function returns a set
|
||||
that includes multiple rows. The default is *No*.
|
||||
* Move the *Strict?* switch to indicate if the function always returns NULL
|
||||
whenever any of its arguments are NULL. If *Yes*, the function is not executed
|
||||
when there are NULL arguments; instead a NULL result is assumed
|
||||
|
||||
|
Before Width: | Height: | Size: 86 KiB After Width: | Height: | Size: 62 KiB |
|
After Width: | Height: | Size: 56 KiB |
|
After Width: | Height: | Size: 62 KiB |
|
After Width: | Height: | Size: 65 KiB |
|
After Width: | Height: | Size: 70 KiB |
|
After Width: | Height: | Size: 48 KiB |
|
Before Width: | Height: | Size: 89 KiB After Width: | Height: | Size: 41 KiB |
|
Before Width: | Height: | Size: 56 KiB After Width: | Height: | Size: 34 KiB |
|
After Width: | Height: | Size: 56 KiB |
|
After Width: | Height: | Size: 29 KiB |
|
After Width: | Height: | Size: 20 KiB |
|
After Width: | Height: | Size: 15 KiB |
|
After Width: | Height: | Size: 47 KiB |
|
After Width: | Height: | Size: 18 KiB |
|
After Width: | Height: | Size: 18 KiB |
|
After Width: | Height: | Size: 48 KiB |
|
After Width: | Height: | Size: 87 KiB |
|
After Width: | Height: | Size: 51 KiB |
@@ -138,6 +138,15 @@ imported server.
|
||||
|
||||
Password fields cannot be imported or exported.
|
||||
|
||||
In desktop mode, ``PasswordExecCommand`` is the shell command to run, as shown
|
||||
in the example below. In server mode it must instead be the name of a command
|
||||
defined in the ``SERVER_PASSEXEC_COMMANDS`` setting (see :ref:`config_py`), and
|
||||
the same name is written when servers are exported. A server whose
|
||||
``PasswordExecCommand`` is not the name of a configured command is still
|
||||
imported, but without a password exec command, and a warning naming the server
|
||||
is printed. ``PasswordExecExpiration`` is imported only together with a valid
|
||||
command name.
|
||||
|
||||
The following example shows both a minimally defined and a fully defined server:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
@@ -26,6 +26,7 @@ of database objects.
|
||||
developer_tools
|
||||
processes
|
||||
pgagent
|
||||
pgtimetable_chains
|
||||
contributions
|
||||
release_notes
|
||||
licence
|
||||
|
||||
@@ -34,7 +34,7 @@ from *config.py* file and modify the values for the following parameters.
|
||||
through kerberos. If that authentication fails, then it will return back
|
||||
to the login page where you need to provide internal pgAdmin user
|
||||
credentials for authentication."
|
||||
"KERBEROS_AUTO_CREATE_USER", "Set the value to *True* if you want to
|
||||
"KRB_AUTO_CREATE_USER", "Set the value to *True* if you want to
|
||||
automatically create a pgAdmin user corresponding to a successfully
|
||||
authenticated Kerberos user. Please note that password is not stored in the
|
||||
pgAdmin database."
|
||||
@@ -126,6 +126,82 @@ PostgreSQL Server settings to configure Kerberos Authentication
|
||||
* Note that, you have to login into pgAdmin with Kerberos authentication to
|
||||
then connect to PostgreSQL using Kerberos.
|
||||
|
||||
* In server mode, the only Kerberos credentials used to connect to a
|
||||
PostgreSQL server are those delegated by a user who logged in to pgAdmin
|
||||
with Kerberos, and only for a server with "Kerberos authentication"
|
||||
enabled. The default credential cache and client keytab of the operating
|
||||
system account that pgAdmin runs as are never used, so a user cannot
|
||||
connect to a server with that account's Kerberos identity.
|
||||
|
||||
* To enforce this for Windows SSPI, which uses the logon of the account
|
||||
pgAdmin runs as, and for gssproxy, which environment variables do not
|
||||
stop, in server mode pgAdmin also tells libpq what to use and what to
|
||||
refuse.
|
||||
|
||||
* For a Kerberos-login user's connection to a server with Kerberos
|
||||
authentication enabled, pgAdmin forces ``gsslib=gssapi``, replacing any
|
||||
``gsslib`` the user set. Without it, a libpq built with both GSSAPI and
|
||||
SSPI answers the server's request with SSPI, which uses the Windows
|
||||
logon of the account pgAdmin runs as and ignores the delegated
|
||||
credentials, whereas ``gsslib=gssapi`` makes libpq use the delegated
|
||||
credential cache. This helps only with a Windows libpq build that
|
||||
includes both GSSAPI and SSPI, as pgAdmin's Windows packages do. With
|
||||
libpq 16 or later, SSPI is also refused with ``require_auth=!sspi``;
|
||||
with an older libpq it is not refused.
|
||||
|
||||
* Every other connection uses ``require_auth=!gss,!sspi`` and
|
||||
``gssencmode=disable``, merged with the server's own "Require
|
||||
authentication" (``require_auth``) parameter. A server whose
|
||||
``require_auth`` allows only ``gss`` can be used only by a
|
||||
Kerberos-login user connecting to a server with Kerberos authentication
|
||||
enabled, and one that allows only ``sspi`` cannot be used in server
|
||||
mode.
|
||||
|
||||
* The ``require_auth`` protection needs libpq 16 or later in pgAdmin.
|
||||
Without it only the ``KRB5CCNAME`` and ``KRB5_CLIENT_KTNAME`` isolation
|
||||
applies to connections that may not use Kerberos, which does not cover
|
||||
Windows SSPI or gssproxy. ``gssencmode`` needs libpq 12 or later;
|
||||
``gsslib`` is known to every libpq that pgAdmin supports.
|
||||
|
||||
* psql and the utilities (such as pg_dump) get no ``require_auth`` in their
|
||||
connection string unless the user set one for the server, which is then
|
||||
merged as above. Their protection is the ``PGREQUIREAUTH``,
|
||||
``PGGSSENCMODE`` and ``PGGSSLIB`` environment variables. Only a libpq
|
||||
16 or later in the utility honours ``PGREQUIREAUTH``; an older one
|
||||
ignores it silently.
|
||||
|
||||
* A service file entry's settings take precedence over these environment
|
||||
variables for psql and the utilities, whereas pgAdmin's own connections
|
||||
are not affected, because their connection string takes precedence over
|
||||
the service file. Only a service listed in
|
||||
``SERVER_MODE_ALLOWED_SERVICES`` can be used in server mode, so only an
|
||||
entry the administrator has written can do this. Do not set
|
||||
``require_auth``, ``gssencmode`` or ``gsslib`` in such an entry unless
|
||||
every pgAdmin user may use what it allows: the ``KRB5CCNAME`` and
|
||||
``KRB5_CLIENT_KTNAME`` isolation cannot be overridden by a service file,
|
||||
but ``require_auth`` and ``gsslib`` in a listed service can re-enable
|
||||
Windows SSPI or gssproxy for the utilities.
|
||||
|
||||
|
||||
Accounts
|
||||
========
|
||||
|
||||
A Kerberos user is only ever logged in to the Kerberos account whose username
|
||||
is their principal (for example *user@EXAMPLE.COM*), never to an ``INTERNAL``
|
||||
or other account with the same username. If there is no such Kerberos account
|
||||
and *KRB_AUTO_CREATE_USER* is *False*, the login fails.
|
||||
|
||||
The administrator account created when pgAdmin is first set up is an
|
||||
``INTERNAL`` account. If you use Kerberos authentication only, create a
|
||||
Kerberos administrator account and deactivate the initial one:
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
/path/to/python /path/to/setup.py add-external-user user@EXAMPLE.COM --auth-source kerberos --admin
|
||||
/path/to/python /path/to/setup.py update-user admin@example.com --inactive
|
||||
|
||||
See :ref:`Accounts from different authentication sources
|
||||
<auth_source_accounts>` for more information.
|
||||
|
||||
Master Password
|
||||
===============
|
||||
|
||||
@@ -15,47 +15,47 @@ When using main browser window, the following keyboard shortcuts are available:
|
||||
:class: longtable
|
||||
:widths: 2 2 3
|
||||
|
||||
+----------------------------+--------------------+------------------------------------+
|
||||
| Shortcut (Windows/Linux) | Shortcut (Mac) | Function |
|
||||
+============================+====================+====================================+
|
||||
| Shift + Ctrl + a | Shift + Ctrl + a | Add grid row |
|
||||
+----------------------------+--------------------+------------------------------------+
|
||||
| Shift + Alt + b | Shift + Option + b | Object explorer |
|
||||
+----------------------------+--------------------+------------------------------------+
|
||||
| Ctrl + Alt + b | Ctrl + Option + b | Toggle Object Explorer |
|
||||
+----------------------------+--------------------+------------------------------------+
|
||||
| Shift + Alt + n | Shift + Option + n | Create object |
|
||||
+----------------------------+--------------------+------------------------------------+
|
||||
| Shift + Alt + d | Shift + Option + d | Delete object |
|
||||
+----------------------------+--------------------+------------------------------------+
|
||||
| Shift + Alt + g | Shift + Option + g | Direct debugging |
|
||||
+----------------------------+--------------------+------------------------------------+
|
||||
| Shift + Alt + e | Shift + Option + e | Edit object properties |
|
||||
+----------------------------+--------------------+------------------------------------+
|
||||
| Shift + Alt + f | Shift + Option + f | File main menu |
|
||||
+----------------------------+--------------------+------------------------------------+
|
||||
| Shift + Alt + h | Shift + Option + h | Help main menu |
|
||||
+----------------------------+--------------------+------------------------------------+
|
||||
| Shift + Alt + o | Shift + Option + o | Object main menu |
|
||||
+----------------------------+--------------------+------------------------------------+
|
||||
| Shift + Alt + q | Shift + Option + q | Open query tool |
|
||||
+----------------------------+--------------------+------------------------------------+
|
||||
| Shift + Ctrl + f | Shift + Ctrl + f | Quick Search |
|
||||
+----------------------------+--------------------+------------------------------------+
|
||||
| F5 | F5 | Refresh object explorer |
|
||||
+----------------------------+--------------------+------------------------------------+
|
||||
| Shift + Alt + s | Shift + Option + s | Search objects |
|
||||
+----------------------------+--------------------+------------------------------------+
|
||||
| Shift + Alt + [ | Shift + Option + [ | Tabbed panel backward |
|
||||
+----------------------------+--------------------+------------------------------------+
|
||||
| Shift + Alt + ] | Shift + Option + ] | Tabbed panel forward |
|
||||
+----------------------------+--------------------+------------------------------------+
|
||||
| Shift + Alt + w | Shift + Ctrl + w | Close tab panel |
|
||||
+----------------------------+--------------------+------------------------------------+
|
||||
| Shift + Alt + l | Shift + Option + l | Tools main menu |
|
||||
+----------------------------+--------------------+------------------------------------+
|
||||
| Shift + Alt + v | Shift + Option + v | View data |
|
||||
+----------------------------+--------------------+------------------------------------+
|
||||
+----------------------------+---------------------------+------------------------------------+
|
||||
| Shortcut (Windows/Linux) | Shortcut (Mac) | Function |
|
||||
+============================+===========================+====================================+
|
||||
| Shift + Ctrl + a | Shift + Ctrl + a | Add grid row |
|
||||
+----------------------------+---------------------------+------------------------------------+
|
||||
| Shift + Alt + b | Shift + Option + b | Object explorer |
|
||||
+----------------------------+---------------------------+------------------------------------+
|
||||
| Ctrl + Alt + b | Ctrl + Option + b | Toggle Object Explorer |
|
||||
+----------------------------+---------------------------+------------------------------------+
|
||||
| Shift + Alt + n | Shift + Option + n | Create object |
|
||||
+----------------------------+---------------------------+------------------------------------+
|
||||
| Shift + Alt + d | Shift + Option + d | Delete object |
|
||||
+----------------------------+---------------------------+------------------------------------+
|
||||
| Shift + Alt + g | Shift + Option + g | Direct debugging |
|
||||
+----------------------------+---------------------------+------------------------------------+
|
||||
| Shift + Alt + e | Shift + Option + e | Edit object properties |
|
||||
+----------------------------+---------------------------+------------------------------------+
|
||||
| Shift + Alt + f | Shift + Option + f | File main menu |
|
||||
+----------------------------+---------------------------+------------------------------------+
|
||||
| Shift + Alt + h | Shift + Option + h | Help main menu |
|
||||
+----------------------------+---------------------------+------------------------------------+
|
||||
| Shift + Alt + o | Shift + Option + o | Object main menu |
|
||||
+----------------------------+---------------------------+------------------------------------+
|
||||
| Shift + Alt + q | Shift + Option + q | Open query tool |
|
||||
+----------------------------+---------------------------+------------------------------------+
|
||||
| Shift + Ctrl + f | Shift + Ctrl + f | Quick Search |
|
||||
+----------------------------+---------------------------+------------------------------------+
|
||||
| F5 | F5 | Refresh object explorer |
|
||||
+----------------------------+---------------------------+------------------------------------+
|
||||
| Shift + Alt + s | Shift + Option + s | Search objects |
|
||||
+----------------------------+---------------------------+------------------------------------+
|
||||
| Ctrl + Alt + Page Up | Ctrl + Option + Page Up | Tabbed panel backward |
|
||||
+----------------------------+---------------------------+------------------------------------+
|
||||
| Ctrl + Alt + Page Down | Ctrl + Option + Page Down | Tabbed panel forward |
|
||||
+----------------------------+---------------------------+------------------------------------+
|
||||
| Shift + Alt + w | Shift + Ctrl + w | Close tab panel |
|
||||
+----------------------------+---------------------------+------------------------------------+
|
||||
| Shift + Alt + l | Shift + Option + l | Tools main menu |
|
||||
+----------------------------+---------------------------+------------------------------------+
|
||||
| Shift + Alt + v | Shift + Option + v | View data |
|
||||
+----------------------------+---------------------------+------------------------------------+
|
||||
|
||||
Property Grid Controls
|
||||
**********************
|
||||
@@ -100,6 +100,8 @@ When using the syntax-highlighting SQL editors, the following shortcuts are avai
|
||||
+--------------------------+----------------------+-------------------------------------+
|
||||
| Ctrl + / | Cmd + / | Comment/Uncomment code (Block) |
|
||||
+--------------------------+----------------------+-------------------------------------+
|
||||
| Ctrl + Shift + d | Cmd + Shift + d | Duplicate current line/selection |
|
||||
+--------------------------+----------------------+-------------------------------------+
|
||||
| Ctrl + a | Cmd + a | Select all |
|
||||
+--------------------------+----------------------+-------------------------------------+
|
||||
| Ctrl + c | Cmd + c | Copy selected text to the clipboard |
|
||||
|
||||
@@ -133,5 +133,51 @@ credentials is the responsibility of:
|
||||
of pgAdmin and protects against attackers who would otherwise reach the
|
||||
LDAP server through pgAdmin.
|
||||
|
||||
See :ref:`Avoiding a bruteforce attack <login>` for the equivalent note
|
||||
on the login page.
|
||||
See :ref:`Avoiding a bruteforce attack <avoiding_bruteforce>` for the equivalent note
|
||||
on the login page.
|
||||
|
||||
Internal accounts
|
||||
*****************
|
||||
|
||||
pgAdmin treats accounts from different authentication sources as separate
|
||||
accounts, even when they share a username. An LDAP user is only ever logged
|
||||
in to the LDAP account with that username, and never to an ``INTERNAL``
|
||||
account with the same name, so each account keeps its own role and
|
||||
settings.
|
||||
|
||||
The administrator account created when pgAdmin is first set up always uses
|
||||
the ``INTERNAL`` authentication source. It remains in the pgAdmin database
|
||||
when *AUTHENTICATION_SOURCES* is set to *['ldap']*, although it can then no
|
||||
longer be used to log in. If you use LDAP authentication only, create an
|
||||
LDAP administrator account, for example:
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
/path/to/python /path/to/setup.py add-external-user ldapuser --auth-source ldap --admin
|
||||
|
||||
and then deactivate the initial ``INTERNAL`` administrator account so that
|
||||
it cannot be used if internal authentication is enabled again later:
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
/path/to/python /path/to/setup.py update-user admin@example.com --inactive
|
||||
|
||||
When both *internal* and *ldap* are listed in *AUTHENTICATION_SOURCES*, a
|
||||
username and password entered on the login page are tried against each source
|
||||
in turn, in the order they are listed, and the first source that accepts the
|
||||
password signs the user in; the sources listed after it are not tried. Every
|
||||
source listed before it is still tried, so the password is a failed attempt at
|
||||
each of those. If the same username exists as both an internal and an LDAP
|
||||
account, a wrong password sent to the directory is a failed LDAP bind that
|
||||
counts towards the directory's lockout, even when a source listed later (such
|
||||
as the internal account) then accepts the sign-in. pgAdmin's own lockout
|
||||
counter for the internal account increases only when the sign-in fails at every
|
||||
source. Avoid giving an internal and an LDAP account the same username.
|
||||
|
||||
When *LDAP_DN_CASE_SENSITIVE* is off, names that differ only in case are the
|
||||
same login. Do not create two accounts whose usernames differ only in case;
|
||||
pgAdmin cannot tell which one a sign-in is for.
|
||||
|
||||
See :ref:`Accounts from different authentication sources
|
||||
<auth_source_accounts>` and :ref:`User Management <user_management>` for more
|
||||
information on the ``setup.py`` user management commands.
|
||||
@@ -52,10 +52,17 @@ to launch a password recovery utility.
|
||||
If you have forgotten the email associated with your account, please contact
|
||||
your administrator.
|
||||
|
||||
Please note that your LDAP password cannot be recovered using this page. If
|
||||
you enter your LDAP username in the *Email Address/Username* field, and then
|
||||
enter your email to recover your password, an error message will be displayed
|
||||
asking you to contact the LDAP administrator to recover your LDAP password.
|
||||
Password recovery works only for accounts using the ``INTERNAL``
|
||||
authentication source, which are the only accounts whose password pgAdmin
|
||||
holds. Recovery looks up the email address you enter and acts on the ``INTERNAL``
|
||||
account that has it: if one exists, reset instructions are sent for that
|
||||
account. The password of an account authenticated against an external source
|
||||
(``LDAP``, ``OAUTH2``, ``KERBEROS`` or ``WEBSERVER``) cannot be recovered here,
|
||||
because it is held by that source; if the email belongs only to such an
|
||||
account, an error asks you to contact that source's administrator. See
|
||||
:ref:`Accounts from different authentication sources <auth_source_accounts>`.
|
||||
|
||||
.. _avoiding_bruteforce:
|
||||
|
||||
Avoiding a bruteforce attack
|
||||
****************************
|
||||
|
||||
@@ -39,6 +39,7 @@ node, and select *Create Cast...*
|
||||
publication_dialog
|
||||
schema_dialog
|
||||
sequence_dialog
|
||||
statistics_dialog
|
||||
subscription_dialog
|
||||
synonym_dialog
|
||||
trigger_function_dialog
|
||||
|
||||
@@ -72,7 +72,7 @@ following options (in alphabetical order):
|
||||
| | |
|
||||
| 2) *Deploy Cloud Instance*| Click to open the :ref:`Cloud Deployment <cloud_deployment>` dialog to deploy an cloud instance. |
|
||||
+-----------------------------+--------------------------------------------------------------------------------------------------------------------------+
|
||||
| *Change Password...* | Click to open the :ref:`Change Password... <change_password_dialog>` dialog to change your password. |
|
||||
| *Change Server Password...* | Click to open the :ref:`Change Server Password... <change_password_dialog>` dialog to change the server password. |
|
||||
+-----------------------------+--------------------------------------------------------------------------------------------------------------------------+
|
||||
| *Clear Saved Password* | If you have saved the database server password, click to clear the saved password. |
|
||||
| | Enable only when password is already saved. |
|
||||
|
||||
@@ -30,6 +30,9 @@ modify the values for the following parameters.
|
||||
"MFA_SUPPORTED_METHODS", "Set the authentication methods to be supported e.g. email, authenticator"
|
||||
"MFA_EMAIL_SUBJECT", "<APP_NAME> - Verification Code e.g. pgAdmin 4 -
|
||||
Verification Code"
|
||||
"MFA_EMAIL_CODE_MAX_ATTEMPTS", "The number of wrong entries allowed for one
|
||||
emailed verification code, after which the code is discarded and a new one
|
||||
must be sent. The default value is 5. Values below 1 are treated as 1."
|
||||
"MFA_FORCE_REGISTRATION", "Force the user to configure the authentication
|
||||
method on login (if no authentication is already configured)."
|
||||
|
||||
|
||||
@@ -88,11 +88,46 @@ and secure.
|
||||
"OAUTH2_AUTO_CREATE_USER", "Set the value to *True* if you want to automatically
|
||||
create a pgAdmin user corresponding to a successfully authenticated Oauth2 user.
|
||||
Please note that password is not stored in the pgAdmin database."
|
||||
"OAUTH2_AUTO_LOGIN", "Set the value to *True* to send users straight to the OAuth2 provider
|
||||
instead of showing the pgAdmin login page. This only takes effect when *oauth2* is the only
|
||||
value in AUTHENTICATION_SOURCES and exactly one provider is configured. The default is *False*.
|
||||
See :ref:`oauth2_auto_login` below."
|
||||
"OAUTH2_ADDITIONAL_CLAIMS", "If a dictionary is provided, pgAdmin will check for a matching key and value on the **ID token first** (for OIDC providers), then fall back to the userinfo endpoint response. In case there is no match with the provided config, the user will receive an authorization error. Useful for checking AzureAD_ *wids* or *groups*, GitLab_ *owner*, *maintainer* and *reporter* claims."
|
||||
"OAUTH2_SERVER_GROUP_CLAIM", "The name of a claim listing the server groups whose shared servers the user may access. If set, shared servers owned by other users are only visible and accessible if the name of their server group is in the claim. See :ref:`oauth2_server_group_claims` below."
|
||||
"OAUTH2_SERVER_GROUP_CLAIM_MAPPING", "An optional dictionary mapping values of the OAUTH2_SERVER_GROUP_CLAIM claim to a server group name or a list of server group names. Values without a mapping are used as server group names unchanged."
|
||||
"OAUTH2_SSL_CERT_VERIFICATION", "Set this variable to False to disable SSL certificate verification for OAuth2 provider.
|
||||
This may need to set False, in case of self-signed certificates."
|
||||
"OAUTH2_CHALLENGE_METHOD", "Enable PKCE workflow. PKCE method name, only *S256* is supported"
|
||||
"OAUTH2_RESPONSE_TYPE", "Enable PKCE workflow. Mandatory with OAUTH2_CHALLENGE_METHOD, must be set to *code*"
|
||||
"OAUTH2_LOGOUT_URL", "The provider's logout endpoint, used to end the user's session with the provider
|
||||
when they log out of pgAdmin. *{redirect_uri}* and *{id_token}* in the value are replaced with the pgAdmin
|
||||
URL and the user's ID token. Ex: *https://example.com/realms/master/protocol/openid-connect/logout?post_logout_redirect_uri={redirect_uri}&id_token_hint={id_token}* (for Keycloak)"
|
||||
|
||||
Accounts
|
||||
========
|
||||
|
||||
An OAuth2 user is only ever logged in to the OAuth2 account whose username is
|
||||
the one resolved from their identity. When *OAUTH2_USERNAME_CLAIM* is set, that
|
||||
claim is used, taken from the ID token if present and otherwise from the
|
||||
userinfo profile. When it is not set, an OIDC provider uses the first of
|
||||
``email``, ``preferred_username`` or ``sub`` that is present, and a non-OIDC
|
||||
provider uses the email address. The user is never logged in to an ``INTERNAL``
|
||||
or other account with the same username. If there is no such OAuth2 account and
|
||||
*OAUTH2_AUTO_CREATE_USER* is *False*, the login fails. When you provision an
|
||||
administrator account for an OAuth2-only deployment, give it the username your
|
||||
provider resolves to, which may not be the email address.
|
||||
|
||||
The administrator account created when pgAdmin is first set up is an
|
||||
``INTERNAL`` account. If you use OAuth2 authentication only, create an OAuth2
|
||||
administrator account and deactivate the initial one:
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
/path/to/python /path/to/setup.py add-external-user user@example.com --auth-source oauth2 --admin
|
||||
/path/to/python /path/to/setup.py update-user admin@example.com --inactive
|
||||
|
||||
See :ref:`Accounts from different authentication sources
|
||||
<auth_source_accounts>` for more information.
|
||||
|
||||
Redirect URL
|
||||
============
|
||||
@@ -119,6 +154,21 @@ After configuration, on restart, you can see the login page with the Oauth2 logi
|
||||
:alt: Oauth2 login
|
||||
:align: center
|
||||
|
||||
.. _oauth2_auto_login:
|
||||
|
||||
Skipping the Login Page
|
||||
-----------------------
|
||||
|
||||
If OAuth2 is the only authentication source and a single provider is configured, set
|
||||
OAUTH2_AUTO_LOGIN to *True* to send users straight to the provider rather than showing them the
|
||||
login page with a single button. Note that any LOGIN_BANNER will not be seen in this case.
|
||||
|
||||
The login page is still shown after a login fails, so that the error can be read, and after the
|
||||
user logs out, so that they are not logged straight back in by the provider. Setting
|
||||
OAUTH2_LOGOUT_URL for the provider is recommended, so that logging out of pgAdmin also ends the
|
||||
user's session with the provider. To show the login page at any other time, add *?no_auto_login*
|
||||
to its URL, for example *https://pgadmin.example.com/login?no_auto_login*.
|
||||
|
||||
PKCE Workflow
|
||||
=============
|
||||
|
||||
@@ -342,6 +392,49 @@ Example:
|
||||
# then userinfo endpoint if not found
|
||||
}]
|
||||
|
||||
.. _oauth2_server_group_claims:
|
||||
|
||||
Restricting Shared Servers with a Claim
|
||||
---------------------------------------
|
||||
|
||||
**OAUTH2_SERVER_GROUP_CLAIM** limits which shared servers an OAuth2 user can
|
||||
see, based on a claim from the identity provider such as a list of roles or
|
||||
groups. When it is set:
|
||||
|
||||
* A shared server owned by another user is only visible, and can only be
|
||||
connected to, if the name of the server group it is in appears in the claim.
|
||||
* If the claim is missing for a user, they see no shared servers owned by
|
||||
other users.
|
||||
* Servers and server groups that the user owns themselves are not affected.
|
||||
* Users who log in by other methods are not affected.
|
||||
|
||||
The claim is read from the ID token first, then from the userinfo endpoint
|
||||
response. Its value may be a string or a list of strings; values of any other
|
||||
type (for example, group objects returned by some providers) are ignored.
|
||||
Server group names are matched exactly, with no trimming or case folding.
|
||||
|
||||
**OAUTH2_SERVER_GROUP_CLAIM_MAPPING** can be used to translate claim values
|
||||
into server group names. A value may map to a single name or a list of names,
|
||||
and values with no mapping are used as server group names unchanged.
|
||||
|
||||
The claim is only read when the user logs in, so changes made at the identity
|
||||
provider take effect at the user's next login.
|
||||
|
||||
Example:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
OAUTH2_CONFIG = [{
|
||||
# ... other config ...
|
||||
'OAUTH2_SERVER_GROUP_CLAIM': 'roles',
|
||||
'OAUTH2_SERVER_GROUP_CLAIM_MAPPING': {
|
||||
'dba': ['Production', 'Staging'],
|
||||
'developer': 'Staging'
|
||||
},
|
||||
# A user with the roles ['developer', 'Reporting'] can see shared
|
||||
# servers in the 'Staging' and 'Reporting' server groups.
|
||||
}]
|
||||
|
||||
Legacy OAuth2 Configuration (Without OIDC)
|
||||
-------------------------------------------
|
||||
|
||||
|
||||
@@ -0,0 +1,239 @@
|
||||
.. _pgtimetable_chains:
|
||||
|
||||
|
||||
***************************************
|
||||
`pgTimeTable Chains`:index:
|
||||
***************************************
|
||||
|
||||
**pg_timetable** is a PostgreSQL-based scheduling agent that runs and manages what are called `Chains`.
|
||||
Each chain is composed of a set of Tasks to be executed in the order they should run.
|
||||
A chain is assigned a single schedule using an extended cron-style format.
|
||||
This UI has been tested with pg_timetable 6.3.0 and above.
|
||||
|
||||
A task may be a series of *SQL* statements, a *BUILTIN* pg_timetable command (such as sending email)
|
||||
or an operating system *PROGRAM* program/batch/shell script.
|
||||
|
||||
Switches on the *pgTimeTable Chain* dialog (accessed
|
||||
through the *Properties* context menu) allow you to modify a chain or disable the whole chain,
|
||||
as well as modify tasks within the chain.
|
||||
|
||||
When you highlight the name of a defined chain in the pgAdmin tree control, the
|
||||
*Properties* tab of the main pgAdmin window will display details about the chain,
|
||||
and the *Statistics* tab will display details about the chain's execution.
|
||||
There are also statistics about each task.
|
||||
|
||||
pg_timetable supports the ability to have more than one job agent running at a time.
|
||||
Job agents can be deployed on various platforms and binaries are readily available to
|
||||
deploy on Linux, Windows, and Mac.
|
||||
|
||||
For complete installation instructions, configuration options, and command-line
|
||||
usage, please refer to the
|
||||
`pg_timetable documentation <https://cybertec-postgresql.github.io/pg_timetable/>`_.
|
||||
|
||||
If you are migrating from pgAgent or another PostgreSQL based scheduler, pg_timetable provides
|
||||
migration SQL queries to help in that migration. See the
|
||||
`migration tools <https://github.com/cybertec-postgresql/pg_timetable/tree/master/extras>`_
|
||||
on GitHub for details.
|
||||
|
||||
Viewing pgTimeTable
|
||||
********************
|
||||
To enable viewing of the *pgTimeTable Chains* node, enable it on the File -> Preferences -> Nodes.
|
||||
It will only show if the schema *timetable* and associated tables exist in your maintenance database.
|
||||
|
||||
Creating a Chain
|
||||
****************
|
||||
|
||||
Note that pg_timetable is not installed via `CREATE EXTENSION`.
|
||||
Instead you use a pgTimeTable agent to install the necessary tables.
|
||||
To create or manage a chain, use the pgAdmin tree control to browse to the
|
||||
server on which the timetable schema exists. The tree control will
|
||||
display a *pgTimeTable Chains* node, under which currently defined chains are
|
||||
displayed.
|
||||
|
||||
To add a new chain, right click on the *pgTimeTable Chains* node,
|
||||
and select *Create pgTimeTable Chain...* from the context menu.
|
||||
|
||||
When the chain dialog opens, use the tabs on the dialog to define the chain
|
||||
and its tasks.
|
||||
|
||||
.. image:: images/pgtimetable_chain_general.png
|
||||
:alt: pgTimeTable chain dialog general tab
|
||||
:align: center
|
||||
|
||||
Use the fields on the *General* tab to provide general information about the
|
||||
chain:
|
||||
|
||||
* Provide a name for the chain in the *Name* field.
|
||||
* Move the *Enabled* switch to the *Yes* position to enable the chain, or
|
||||
*No* to disable it.
|
||||
* Use the *Max instances* field to specify the number of parallel instances
|
||||
(clients) that may execute this chain simultaneously. Leave the field blank
|
||||
to allow unlimited parallel execution.
|
||||
* Use the *Timeout (ms)* field to specify the maximum number of milliseconds
|
||||
the chain may run before it is aborted. A value of *0* means no timeout.
|
||||
* Move the *Self-destruct* switch to *Yes* to have the chain delete itself
|
||||
after a successful run.
|
||||
* Move the *Exclusive execution* switch to *Yes* to pause all other parallel
|
||||
chains while this chain is executing.
|
||||
* Use the *Host agent* field to specify the client name of the agent running
|
||||
pgTimeTable that should execute this chain. Leave the field blank to allow
|
||||
any available host to run the chain.
|
||||
|
||||
.. note:: You can query the ``timetable.active_session`` view to see the
|
||||
client names reported by running pgTimeTable agents:
|
||||
|
||||
.. code-block:: sql
|
||||
|
||||
SELECT DISTINCT client_name FROM timetable.active_session
|
||||
|
||||
Use the client name exactly as reported by the query in the *Host agent*
|
||||
field.
|
||||
|
||||
* Use the *On error* field to specify the behavior when an error occurs.
|
||||
Accepted values are:
|
||||
|
||||
* *NULL* - Use the default error handling.
|
||||
* *LOG* - Log the error and continue execution.
|
||||
* *ABORT* - Abort the chain on error.
|
||||
|
||||
* Use the *Schedule* field to specify a cron-style schedule for the chain.
|
||||
The format consists of a single field with 5 slots. As with regular cron,
|
||||
you can use a comma (`,`) for multiple values and an asterisk (`*`) as a wildcard for all.
|
||||
E.g. `20 1,5 * * *` would run at 1:20 and 5:20 AM.
|
||||
The timezone is always the timezone of the server the pgTimeTable database resides on.
|
||||
|
||||
.. code-block:: text
|
||||
|
||||
* * * * *
|
||||
| | | | |
|
||||
| | | | +---- day of the week (0 - 7) (0 and 7 = Sunday)
|
||||
| | | +------ month (1 - 12)
|
||||
| | +-------- day of the month (1 - 31)
|
||||
| +---------- hour (0 - 23)
|
||||
+------------ minute (0 - 59)
|
||||
|
||||
If you leave the field blank it runs every minute
|
||||
(the effective schedule is `* * * * *`).
|
||||
|
||||
In addition to cron syntax, pgTimeTable supports additional special case formats.
|
||||
Namely `@after`, `@every`, `@reboot`.
|
||||
|
||||
The `@every` and `@after` should be followed with additional arguments.
|
||||
For example `@every 2 hours`.
|
||||
|
||||
|
||||
.. image:: images/pgtimetable_chain_tasks.png
|
||||
:alt: pgTimeTable chain dialog tasks tab
|
||||
:align: center
|
||||
|
||||
Use the *Tasks* tab to define and manage the tasks that the chain will
|
||||
execute. Click the Add icon (+) to add a new task. Tasks are executed in
|
||||
order of their *Order* value.
|
||||
|
||||
Click the compose icon (located at the left side of the header) to open the
|
||||
task definition dialog. See `Chain Task Dialog`_ below for details on
|
||||
defining a task.
|
||||
|
||||
When you have finished defining the chain, you can use the *SQL* tab to review
|
||||
the SQL that will create or modify your chain.
|
||||
|
||||
Click the *Save* button to save the chain, or *Close* to exit without saving.
|
||||
Use the *Reset* button to remove your unsaved entries from the dialog.
|
||||
|
||||
After saving a chain, the chain will be listed under the *pgTimeTable Chains*
|
||||
node of the pgAdmin tree control. The *Properties* tab in the main pgAdmin
|
||||
window will display a high-level overview of the selected chain, and the
|
||||
*Statistics* tab will show the details of each execution run.
|
||||
|
||||
.. image:: images/pgtimetable_chain_properties.png
|
||||
:alt: pgTimeTable chain properties
|
||||
:align: center
|
||||
|
||||
To modify an existing chain or to review detailed information about a chain,
|
||||
right-click on a chain name, and select *Properties* from the context menu.
|
||||
|
||||
|
||||
Chain Task Dialog
|
||||
*****************
|
||||
|
||||
A chain task defines a single unit of work to be performed as part of a chain.
|
||||
Each chain can contain one or more tasks, which are executed sequentially in
|
||||
order.
|
||||
|
||||
.. image:: images/pgtimetable_task_general.png
|
||||
:alt: pgTimeTable task dialog general tab
|
||||
:align: center
|
||||
|
||||
Use the fields on the *General* tab to define the task:
|
||||
|
||||
* Provide a name for the task in the *Name* field.
|
||||
* Use the *Order* field to specify the execution order of the task within the
|
||||
chain. Tasks are executed in ascending order by this value.
|
||||
* Use the *Kind* drop-down to select the type of task:
|
||||
|
||||
* *SQL* - The task executes SQL statements.
|
||||
* *PROGRAM* - The task executes an operating system command.
|
||||
* *BUILTIN* - The task executes one of the built-in pgTimeTable commands.
|
||||
|
||||
* When *Kind* is set to *SQL*, use the *Connection string* field to optionally
|
||||
specify a libpq-style connection string for a remote database. Leave the
|
||||
field blank to execute against the pgTimeTable database.
|
||||
|
||||
.. note:: The *Connection string* field is only available when the
|
||||
pgTimeTable extension version supports it (version 5.x or later).
|
||||
It is ignored for *PROGRAM* and *BUILTIN* task kinds.
|
||||
|
||||
* Move the *Ignore error* switch to *Yes* to continue chain execution even if
|
||||
this task fails. Set to *No* to stop the chain when an error occurs.
|
||||
|
||||
.. image:: images/pgtimetable_task_code.png
|
||||
:alt: pgTimeTable task dialog code tab
|
||||
:align: center
|
||||
|
||||
Use the *Code* tab to provide the code or command for the task. The content
|
||||
of the *Code* tab depends on the selected *Kind*:
|
||||
|
||||
* If *Kind* is *SQL*, provide one or more SQL statements in the *SQL* field.
|
||||
Use ``$1``, ``$2``, etc. as parameter placeholders that will be substituted
|
||||
from the *Parameters* collection.
|
||||
* If *Kind* is *PROGRAM*, provide the full path and arguments of the program
|
||||
to execute in the *Program* field.
|
||||
* If *Kind* is *BUILTIN*, select one of the following internal commands from
|
||||
the *Internal Command* drop-down:
|
||||
|
||||
* *NoOp* - No operation; the task completes immediately.
|
||||
* *Sleep* - Pause execution for a duration specified by a parameter.
|
||||
* *Log* - Write a message to the pgTimeTable log.
|
||||
* *SendMail* - Send an email (requires parameters for recipient, subject,
|
||||
and body).
|
||||
* *Download* - Download a file from a URL.
|
||||
* *CopyFromFile* - Copy data from a file into a database table.
|
||||
* *CopyToFile* - Copy data from a database table to a file.
|
||||
* *Shutdown* - Shut down the pgTimeTable service.
|
||||
|
||||
.. image:: images/pgtimetable_task_parameters.png
|
||||
:alt: pgTimeTable task dialog parameters
|
||||
:align: center
|
||||
|
||||
Use the *Parameters* sub-collection to define parameter values that will be
|
||||
substituted into the task command. Click the Add icon (+) to add a new
|
||||
parameter row:
|
||||
|
||||
* Use the *Order* column to specify the parameter position (``$1``, ``$2``,
|
||||
etc.).
|
||||
* Use the *Value* column to provide the parameter value. The value may be a
|
||||
plain string or a JSON value.
|
||||
|
||||
Click the compose icon to close the task definition dialog, then click *Save*
|
||||
on the chain dialog to save all changes.
|
||||
|
||||
Refer to `pg_timetable samples <https://cybertec-postgresql.github.io/pg_timetable/latest/samples/>`_
|
||||
for the kind of parameters expected for each type.
|
||||
|
||||
|
||||
Both the Chain and Tasks have a statistics tab, which show the results of past runs of tasks.
|
||||
|
||||
|
||||
.. image:: images/pgtimetable_task_statistics.png
|
||||
:alt: pgTimeTable task statistics
|
||||
:align: center
|
||||
@@ -589,6 +589,15 @@ Use the fields on the *CSV/TXT Output* panel to control the CSV/TXT output.
|
||||
quoted in the CSV/TXT output; select *Strings*, *All*, or *None*.
|
||||
* Use the *Replace null values with* option to replace null values with
|
||||
specified string in the output file. Default is set to 'NULL'.
|
||||
* Use the *Output file encoding* drop-down listbox to specify the character
|
||||
encoding used when saving query results to a file. The default is utf-8; an
|
||||
encoding that is not listed can also be typed in. Any character that the
|
||||
chosen encoding cannot represent is written as a question mark (?).
|
||||
* Use the *Add byte order mark (BOM)?* switch to add a byte order mark at the
|
||||
start of the saved file when a UTF encoding is used. This helps applications
|
||||
such as Microsoft Excel detect the encoding correctly. This applies to the
|
||||
CSV/TXT output only. The utf-16, utf-32 and utf-8-sig encodings always
|
||||
include a BOM, whatever this is set to.
|
||||
|
||||
.. image:: images/preferences_sql_display.png
|
||||
:alt: Preferences sqleditor display options
|
||||
@@ -754,6 +763,9 @@ preferences for copied data.
|
||||
character for copied data.
|
||||
* Use the *Result copy quoting* drop-down listbox to select which type of fields
|
||||
require quoting; select *All*, *None*, or *Strings*.
|
||||
* When the *Copy with headers?* switch is set to true, the column headers are
|
||||
included by default when copying data from the results grid. This can still
|
||||
be toggled per-copy from the results grid copy options menu.
|
||||
* When the *Striped rows?* switch is set to true, the result grid will display
|
||||
rows with alternating background colors.
|
||||
|
||||
|
||||
@@ -210,10 +210,12 @@ Data Editing Options
|
||||
| *Save Data Changes* | Click the *Save Data Changes* icon to save data changes (insert, update, or delete) in the Data | F6 |
|
||||
| | Output Panel to the server. | |
|
||||
+----------------------+---------------------------------------------------------------------------------------------------+----------------+
|
||||
| *Save results to* | Click the Save results to file icon to save the result set of the current query as a delimited | F8 |
|
||||
| *file* | text file (CSV, if the field separator is set to a comma). This button will only be enabled when | |
|
||||
| | a query has been executed and there are results in the data grid. You can specify the CSV/TXT | |
|
||||
| | settings in the Preference Dialogue under SQL Editor -> CSV/TXT output. | |
|
||||
| *Save results to* | Click the Save results to file icon to save the result set of the current query. By | F8 |
|
||||
| *file* | default it is saved as a delimited text file (CSV, if the field separator is set to a | |
|
||||
| | comma). Use the adjacent drop-down list to instead save the results as JSON or XML. | |
|
||||
| | This button is only enabled when a query has been executed and there are results in | |
|
||||
| | the data grid. You can specify the CSV/TXT settings (including the output file encoding | |
|
||||
| | and byte order mark) in the Preferences dialog under Query Tool -> CSV/TXT Output. | |
|
||||
+----------------------+---------------------------------------------------------------------------------------------------+----------------+
|
||||
| Graph Visualiser | Use the Graph Visualiser button to generate graphs of the query results. | |
|
||||
+----------------------+---------------------------------------------------------------------------------------------------+----------------+
|
||||
|
||||
@@ -12,6 +12,7 @@ notes for it.
|
||||
:maxdepth: 1
|
||||
|
||||
|
||||
release_notes_9_19
|
||||
release_notes_9_18
|
||||
release_notes_9_17
|
||||
release_notes_9_16
|
||||
|
||||
@@ -0,0 +1,196 @@
|
||||
************
|
||||
Version 9.19
|
||||
************
|
||||
|
||||
Release date: 2026-10-29
|
||||
|
||||
This release contains a number of bug fixes and new features since the release of pgAdmin 4 v9.18.
|
||||
|
||||
.. warning:: In server mode, free-text Password Exec Commands are no longer
|
||||
run, and ``ENABLE_SERVER_PASS_EXEC_CMD`` is deprecated and ignored.
|
||||
Administrators now define the permitted commands by name in
|
||||
``SERVER_PASSEXEC_COMMANDS``, and users choose one of those names in the
|
||||
server properties. Configure ``SERVER_PASSEXEC_COMMANDS`` **before** the
|
||||
first start after upgrading: on that start, each existing command that
|
||||
exactly matches a configured entry is converted to its name, and every
|
||||
other command is cleared, which cannot be undone. If the setting is empty,
|
||||
the conversion is postponed and a warning is logged at each start. See
|
||||
``config.py`` and the server dialog documentation for details.
|
||||
|
||||
.. note:: Users who do not own a shared server never inherit the owner's
|
||||
password file; it is treated like the owner's password, SSH key and SSL
|
||||
client files. Each user can set their own password file on their copy of
|
||||
the server, and a file that everyone should use can be placed in a
|
||||
``SHARED_STORAGE`` directory and referenced as ``Name:/.pgpass``.
|
||||
|
||||
.. note:: In desktop mode, when an SSH tunnel is configured with an identity
|
||||
file or a password, pgAdmin no longer also offers keys from the SSH agent;
|
||||
the agent is still used when neither is configured. In server mode, SSH
|
||||
tunnels never use the SSH agent, or the keys and configuration in the
|
||||
``~/.ssh`` directory of the account pgAdmin runs as, so a tunnel that
|
||||
relied on them must be given an identity file or password on the server.
|
||||
|
||||
.. note:: On Python 3.10 and later, pgAdmin now requires Flask-Security-Too
|
||||
5.8.3 or later, which fixes an open redirect (GHSA-ccgp-pv8r-95wm). This
|
||||
matters to anyone installing pgAdmin from the Python wheel into an
|
||||
existing environment. Python 3.9 installations remain on the
|
||||
Flask-Security-Too 5.6 series.
|
||||
|
||||
Supported Database Servers
|
||||
**************************
|
||||
**PostgreSQL**: 14, 15, 16, 17 and 18
|
||||
|
||||
**EDB Advanced Server**: 14, 15, 16, 17 and 18
|
||||
|
||||
Bundled PostgreSQL Utilities
|
||||
****************************
|
||||
**psql**, **pg_dump**, **pg_dumpall**, **pg_restore**: 18.6
|
||||
|
||||
|
||||
New features
|
||||
************
|
||||
|
||||
| `Issue #2018 <https://github.com/pgadmin-org/pgadmin4/issues/2018>`_ - Add a Statistics node for creating and managing extended statistics.
|
||||
| `Issue #2363 <https://github.com/pgadmin-org/pgadmin4/issues/2363>`_ - Allow data to be edited through simple auto-updatable views in View/Edit Data.
|
||||
| `Issue #3205 <https://github.com/pgadmin-org/pgadmin4/issues/3205>`_ - Allow Query Tool results to be saved as JSON or XML, as well as CSV.
|
||||
| `Issue #3834 <https://github.com/pgadmin-org/pgadmin4/issues/3834>`_ - Add a keyboard shortcut to duplicate the current line or selection in the SQL editor.
|
||||
| `Issue #4128 <https://github.com/pgadmin-org/pgadmin4/issues/4128>`_ - Allow the encoding of Query Tool result files to be chosen.
|
||||
| `Issue #4129 <https://github.com/pgadmin-org/pgadmin4/issues/4129>`_ - Add a 'copy with headers' option for Query Tool results.
|
||||
| `Issue #5691 <https://github.com/pgadmin-org/pgadmin4/issues/5691>`_ - Close dialogs with the Escape key.
|
||||
| `Issue #6695 <https://github.com/pgadmin-org/pgadmin4/issues/6695>`_ - Allow a byte order mark to be added to Query Tool result files.
|
||||
| `Issue #7167 <https://github.com/pgadmin-org/pgadmin4/issues/7167>`_ - Allow dialogs such as Sort/Filter to be saved and closed with the Enter key.
|
||||
| `Issue #8647 <https://github.com/pgadmin-org/pgadmin4/issues/8647>`_ - Allow shift + click to extend the cell range selection in the Query Tool results grid.
|
||||
| `Issue #9057 <https://github.com/pgadmin-org/pgadmin4/issues/9057>`_ - Support UNLOGGED partitioned tables correctly, which PostgreSQL 18 no longer allows to be created or altered.
|
||||
| `Issue #9064 <https://github.com/pgadmin-org/pgadmin4/issues/9064>`_ - Add the PostgreSQL 18 pg_dump options to the Backup and Backup Server dialogs.
|
||||
| `Issue #9066 <https://github.com/pgadmin-org/pgadmin4/issues/9066>`_ - Add the PostgreSQL 18 pg_restore options to the Restore dialog.
|
||||
| `Issue #9212 <https://github.com/pgadmin-org/pgadmin4/issues/9212>`_ - Show fill factor, TOAST tuple target and parallel workers in the table Properties panel.
|
||||
| `Issue #9264 <https://github.com/pgadmin-org/pgadmin4/issues/9264>`_ - Move the 'Returns a set?' option to the Definition tab in the Function dialog.
|
||||
| `Issue #9704 <https://github.com/pgadmin-org/pgadmin4/issues/9704>`_ - Allow an OAuth2 claim to restrict which shared servers a user can see, configured through ``OAUTH2_SERVER_GROUP_CLAIM`` and ``OAUTH2_SERVER_GROUP_CLAIM_MAPPING``.
|
||||
| `Issue #9975 <https://github.com/pgadmin-org/pgadmin4/issues/9975>`_ - Allow the login page to be skipped when OAuth2 is the only authentication source, using ``OAUTH2_AUTO_LOGIN`` and ``OAUTH2_LOGOUT_URL``.
|
||||
| `Issue #10148 <https://github.com/pgadmin-org/pgadmin4/issues/10148>`_ - Add support for managing pg_timetable chains and tasks.
|
||||
| `Issue #10249 <https://github.com/pgadmin-org/pgadmin4/issues/10249>`_ - Replace free-text Password Exec Commands in server mode with an administrator-defined allowlist, ``SERVER_PASSEXEC_COMMANDS``. Commands run without a shell, with a reduced environment, and receive the pgAdmin user's identity in ``PGADMIN_PASSEXEC_*`` variables; users of a shared server inherit the owner's command by default.
|
||||
| `Issue #10473 <https://github.com/pgadmin-org/pgadmin4/issues/10473>`_ - Add a macOS 26 layered app icon with dark, clear and tinted variants.
|
||||
| `Issue #10499 <https://github.com/pgadmin-org/pgadmin4/issues/10499>`_ - Add pgEdge Starfleet to the Deploy Cloud Instance wizard, for creating Starfleet Managed databases.
|
||||
| `Issue #10503 <https://github.com/pgadmin-org/pgadmin4/issues/10503>`_ - Add numpad zoom shortcuts to the Desktop View menu.
|
||||
|
||||
Housekeeping
|
||||
************
|
||||
|
||||
| `Issue #5369 <https://github.com/pgadmin-org/pgadmin4/issues/5369>`_ - Clarify the configuration files documentation.
|
||||
| `Issue #9230 <https://github.com/pgadmin-org/pgadmin4/issues/9230>`_ - Rename the server 'Change Password' option to 'Change Server Password', and the user menu option to 'Change pgAdmin Password', so the two can no longer be confused.
|
||||
| `Issue #9331 <https://github.com/pgadmin-org/pgadmin4/issues/9331>`_ - Remove unused Google API documents, Babel locale data and packaging tools from the packages.
|
||||
| `Issue #9524 <https://github.com/pgadmin-org/pgadmin4/issues/9524>`_ - Document running the container with rootless Podman, and the ``PUID`` and ``PGID`` environment variables.
|
||||
| `Issue #10363 <https://github.com/pgadmin-org/pgadmin4/issues/10363>`_ - Enforce ``--immutable`` on every yarn install, in CI and the release builds, to harden the supply chain.
|
||||
| `Issue #10546 <https://github.com/pgadmin-org/pgadmin4/issues/10546>`_ - Fix the flaky ``test_windows_pty_failures`` test counting psql output from other tests.
|
||||
| `Issue #10552 <https://github.com/pgadmin-org/pgadmin4/issues/10552>`_ - Add a test for Reset discarding pending SQL editor values in dialogs.
|
||||
| `Issue #10553 <https://github.com/pgadmin-org/pgadmin4/issues/10553>`_ - Make the server tests restore the shared test fixture after themselves.
|
||||
| `Issue #10601 <https://github.com/pgadmin-org/pgadmin4/issues/10601>`_ - Remove the dead tree ``loadfail`` handler from ``browser.js``.
|
||||
| `Issue #10602 <https://github.com/pgadmin-org/pgadmin4/issues/10602>`_ - Fix the Jest ``addNewDatagridRow`` helper so schema UI specs actually add a row.
|
||||
|
||||
Bug fixes
|
||||
*********
|
||||
|
||||
| `Issue #5940 <https://github.com/pgadmin-org/pgadmin4/issues/5940>`_ - Fix View/Edit Data failing on SQL_ASCII databases when a name contains a backslash followed by 'u' or 'U'.
|
||||
| `Issue #6090 <https://github.com/pgadmin-org/pgadmin4/issues/6090>`_ - Refresh a stale server connection manager when a server id is reused, so one user's session can no longer pick up settings from another server.
|
||||
| `Issue #6231 <https://github.com/pgadmin-org/pgadmin4/issues/6231>`_ - Generate a single GRANT statement for roles with identical privileges.
|
||||
| `Issue #6481 <https://github.com/pgadmin-org/pgadmin4/issues/6481>`_ - Fix index and constraint columns not being shown when their names require quoting.
|
||||
| `Issue #6652 <https://github.com/pgadmin-org/pgadmin4/issues/6652>`_ - Fix Schema Diff treating functions that differ only in OUT arguments or return type as unrelated objects.
|
||||
| `Issue #6671 <https://github.com/pgadmin-org/pgadmin4/issues/6671>`_ - Allow the emailed MFA code to be re-entered after an invalid code is entered.
|
||||
| `Issue #7232 <https://github.com/pgadmin-org/pgadmin4/issues/7232>`_ - Fix the tab switching shortcuts not working whilst focus is inside the PSQL, Query, ERD or Schema Diff tools.
|
||||
| `Issue #7612 <https://github.com/pgadmin-org/pgadmin4/issues/7612>`_ - Fix the server context menu showing database options after the tree state is restored.
|
||||
| `Issue #7619 <https://github.com/pgadmin-org/pgadmin4/issues/7619>`_ - Fix Query Tool autocomplete running as the wrong user, or failing, after changing the connection to a different user.
|
||||
| `Issue #8015 <https://github.com/pgadmin-org/pgadmin4/issues/8015>`_ - Re-sort renamed server groups and other nodes in the Object Explorer instead of waiting for a reload.
|
||||
| `Issue #8144 <https://github.com/pgadmin-org/pgadmin4/issues/8144>`_ - Connect the server when changing the Query Tool connection after it was disconnected.
|
||||
| `Issue #8529 <https://github.com/pgadmin-org/pgadmin4/issues/8529>`_ - Rewrite the container entrypoint using bash features, and pass values to the embedded Python safely, so an email address containing an apostrophe works and a validation error stops the container rather than passing silently.
|
||||
| `Issue #8664 <https://github.com/pgadmin-org/pgadmin4/issues/8664>`_ - Fix debugger messages from successive steps running together on one line.
|
||||
| `Issue #8991 <https://github.com/pgadmin-org/pgadmin4/issues/8991>`_ - Fix BEGIN, COMMIT and ROLLBACK silently failing in the Query Tool under server cursor mode.
|
||||
| `Issue #9130 <https://github.com/pgadmin-org/pgadmin4/issues/9130>`_ - Select the adjacent tab when the active tab is closed, rather than the left-most.
|
||||
| `Issue #9450 <https://github.com/pgadmin-org/pgadmin4/issues/9450>`_ - Allow roles holding ADMIN OPTION to manage Group Role membership.
|
||||
| `Issue #9603 <https://github.com/pgadmin-org/pgadmin4/issues/9603>`_ - Restore the per-tab toolbar options when Query Tool tabs are restored on relaunch.
|
||||
| `Issue #9624 <https://github.com/pgadmin-org/pgadmin4/issues/9624>`_ - Fix a database's label staying at '[Disconnecting...]' when disconnecting it fails.
|
||||
| `Issue #9630 <https://github.com/pgadmin-org/pgadmin4/issues/9630>`_ - Fix the parameter value dropdown keeping stale options when the parameter name is changed.
|
||||
| `Issue #9672 <https://github.com/pgadmin-org/pgadmin4/issues/9672>`_ - Fix View/Edit Data not handling generated columns properly.
|
||||
| `Issue #9784 <https://github.com/pgadmin-org/pgadmin4/issues/9784>`_ - Fix modal dialogs losing keyboard focus to a tool iframe loading in the background.
|
||||
| `Issue #9798 <https://github.com/pgadmin-org/pgadmin4/issues/9798>`_ - Fix the collation and ctype query error for databases with a non-default LC_COLLATE.
|
||||
| `Issue #9814 <https://github.com/pgadmin-org/pgadmin4/issues/9814>`_ - Disable the SSH agent when an SSH tunnel identity file or password is provided.
|
||||
| `Issue #9818 <https://github.com/pgadmin-org/pgadmin4/issues/9818>`_ - Keep the AI Assistant's intermediate responses instead of replacing them with placeholder messages.
|
||||
| `Issue #9836 <https://github.com/pgadmin-org/pgadmin4/issues/9836>`_ - Fix the CREATE TABLE script ignoring the UNLOGGED attribute of a partition.
|
||||
| `Issue #9838 <https://github.com/pgadmin-org/pgadmin4/issues/9838>`_ - Fix formatting a view's SQL being treated as a column change that drops and re-creates the view.
|
||||
| `Issue #9865 <https://github.com/pgadmin-org/pgadmin4/issues/9865>`_ - Fix modal dialogs being hidden from assistive technology by ``aria-hidden`` on the application root.
|
||||
| `Issue #9890 <https://github.com/pgadmin-org/pgadmin4/issues/9890>`_ - Fix ERD tables becoming immovable after a Shift-drag selection.
|
||||
| `Issue #9921 <https://github.com/pgadmin-org/pgadmin4/issues/9921>`_ - Fix Azure and Google authentication in the cloud deployment wizard.
|
||||
| `Issue #9984 <https://github.com/pgadmin-org/pgadmin4/issues/9984>`_ - Report an unreachable external configuration database, or a malformed ``CONFIG_DATABASE_URI``, as a failed check rather than an unhandled exception.
|
||||
| `Issue #10031 <https://github.com/pgadmin-org/pgadmin4/issues/10031>`_ - Fix Query Tool tabs running their query automatically after a session restore.
|
||||
| `Issue #10103 <https://github.com/pgadmin-org/pgadmin4/issues/10103>`_ - Exclude non-editable alias and expression columns when saving edited rows in the Query Tool.
|
||||
| `Issue #10136 <https://github.com/pgadmin-org/pgadmin4/issues/10136>`_ - Restore tags for standard users on shared servers, and never inherit the owner's password file.
|
||||
| `Issue #10143 <https://github.com/pgadmin-org/pgadmin4/issues/10143>`_ - Fix switching back to a dialog tab containing a data grid being slow, by skipping virtualisation for small grids.
|
||||
| `Issue #10226 <https://github.com/pgadmin-org/pgadmin4/issues/10226>`_ - Restore remembering the last visited directory in the desktop file dialogs.
|
||||
| `Issue #10251 <https://github.com/pgadmin-org/pgadmin4/issues/10251>`_ - Fix invalid SQL from the Maintenance dialog for REINDEX ... CONCURRENTLY.
|
||||
| `Issue #10254 <https://github.com/pgadmin-org/pgadmin4/issues/10254>`_ - Fix the Object Explorer shortcut not moving keyboard focus into the tree.
|
||||
| `Issue #10273 <https://github.com/pgadmin-org/pgadmin4/issues/10273>`_ - Fix the dashboard transactions per second graph being wrong for refresh rates other than one second.
|
||||
| `Issue #10292 <https://github.com/pgadmin-org/pgadmin4/issues/10292>`_ - Fix Schema Diff scripts that convert a column to or from SERIAL.
|
||||
| `Issue #10301 <https://github.com/pgadmin-org/pgadmin4/issues/10301>`_ - Drop the scaffolding default partition after Schema Diff rebuilds a partitioned table.
|
||||
| `Issue #10310 <https://github.com/pgadmin-org/pgadmin4/issues/10310>`_ - Fix the Statistics node failing to list its children in Object Explorer.
|
||||
| `Issue #10334 <https://github.com/pgadmin-org/pgadmin4/issues/10334>`_ - Add Caddy reverse proxy configuration examples to the documentation.
|
||||
| `Issue #10347 <https://github.com/pgadmin-org/pgadmin4/issues/10347>`_ - Fix the selected UI language not persisting beyond the request that set it in server mode.
|
||||
| `Issue #10348 <https://github.com/pgadmin-org/pgadmin4/issues/10348>`_ - Fix OpenAI streaming tool calls being split into two entries in the AI Assistant.
|
||||
| `Issue #10349 <https://github.com/pgadmin-org/pgadmin4/issues/10349>`_ - Fix the OAuth2 userinfo request crashing when ``OAUTH2_USERINFO_ENDPOINT`` is set to None.
|
||||
| `Issue #10350 <https://github.com/pgadmin-org/pgadmin4/issues/10350>`_ - Fix the MAINTAIN privilege being silently dropped for views and materialised views on PostgreSQL 17 and later.
|
||||
| `Issue #10357 <https://github.com/pgadmin-org/pgadmin4/issues/10357>`_ - Fix the cursor jumping to the start of SQL fields in dialogs when keys are pressed together.
|
||||
| `Issue #10364 <https://github.com/pgadmin-org/pgadmin4/issues/10364>`_ - Explain Docker Compose quoting for ``PGADMIN_CONFIG_*`` values in the container documentation.
|
||||
| `Issue #10386 <https://github.com/pgadmin-org/pgadmin4/issues/10386>`_ - Stop the Query Tool spinner, and its idle CPU use, whilst a password prompt is open.
|
||||
| `Issue #10401 <https://github.com/pgadmin-org/pgadmin4/issues/10401>`_ - Show default privileges in the Database properties panel.
|
||||
| `Issue #10409 <https://github.com/pgadmin-org/pgadmin4/issues/10409>`_ - Include the authenticated user in the access log on logout.
|
||||
| `Issue #10414 <https://github.com/pgadmin-org/pgadmin4/issues/10414>`_ - Fix the Linux packages shipping a stray copy of Python as ``venv/bin/python``, which stopped the DEB package installing on Ubuntu 26.04.
|
||||
| `Issue #10415 <https://github.com/pgadmin-org/pgadmin4/issues/10415>`_ - Prefer the user's email address as the Gravatar identifier.
|
||||
| `Issue #10418 <https://github.com/pgadmin-org/pgadmin4/issues/10418>`_ - Remove the dead ``usr`` parameter from Query Tool connection URLs.
|
||||
| `Issue #10419 <https://github.com/pgadmin-org/pgadmin4/issues/10419>`_ - Fix Kerberos login failures returning a JSON 500 response, and first-time automatically created Kerberos logins being rejected.
|
||||
| `Issue #10422 <https://github.com/pgadmin-org/pgadmin4/issues/10422>`_ - Fix Schema Diff repeating unchanged column comments and identity options.
|
||||
| `Issue #10424 <https://github.com/pgadmin-org/pgadmin4/issues/10424>`_ - Fix Reset Layout dropping Query Tool tabs from the saved application state.
|
||||
| `Issue #10428 <https://github.com/pgadmin-org/pgadmin4/issues/10428>`_ - Fix connections crashing on Windows with an access violation, and the utilities failing to find ``gssapi64.dll``, by staging the Kerberos runtime next to libpq.
|
||||
| `Issue #10446 <https://github.com/pgadmin-org/pgadmin4/issues/10446>`_ - Fix Schema Diff reporting identical tables as different when a foreign key's referenced table has a dropped column.
|
||||
| `Issue #10449 <https://github.com/pgadmin-org/pgadmin4/issues/10449>`_ - Honour the password file in the Change Server Password dialog, rather than requiring the current password.
|
||||
| `Issue #10462 <https://github.com/pgadmin-org/pgadmin4/issues/10462>`_ - Fix duplicated content when opening large non-UTF-8 files in the Query Tool.
|
||||
| `Issue #10463 <https://github.com/pgadmin-org/pgadmin4/issues/10463>`_ - Pass the target database, and every supported connection parameter, to the utilities, so a database set in a service file no longer overrides the one selected.
|
||||
| `Issue #10470 <https://github.com/pgadmin-org/pgadmin4/issues/10470>`_ - Remove inherited columns from the Columns tab when their parent table is removed after reopening a table.
|
||||
| `Issue #10482 <https://github.com/pgadmin-org/pgadmin4/issues/10482>`_ - Allow saving files through symbolic links in desktop mode, and stop forcing 0600 permissions on them there.
|
||||
| `Issue #10485 <https://github.com/pgadmin-org/pgadmin4/issues/10485>`_ - Fix 'Cannot have two HTML5 backends at the same time' errors in dialogs containing a data grid.
|
||||
| `Issue #10511 <https://github.com/pgadmin-org/pgadmin4/issues/10511>`_ - Fix the Dashboard Logs tab failing on multi-byte characters split across read chunks.
|
||||
| `Issue #10527 <https://github.com/pgadmin-org/pgadmin4/issues/10527>`_ - Write the cloud deployment wizard's AWS, Azure and Google credentials through to the session store.
|
||||
| `Issue #10531 <https://github.com/pgadmin-org/pgadmin4/issues/10531>`_ - Fix the Query Tool saving files that were loaded as latin-1 back as UTF-8.
|
||||
| `Issue #10534 <https://github.com/pgadmin-org/pgadmin4/issues/10534>`_ - Fix the hardcoded table OID in the column properties query, which broke inherited column details.
|
||||
| `Issue #10547 <https://github.com/pgadmin-org/pgadmin4/issues/10547>`_ - Fix valid escapes below U+0100 being rewritten in SQL_ASCII data and names.
|
||||
| `Issue #10548 <https://github.com/pgadmin-org/pgadmin4/issues/10548>`_ - Fix negative identity column options being dropped from the generated SQL.
|
||||
| `Issue #10549 <https://github.com/pgadmin-org/pgadmin4/issues/10549>`_ - Fix Schema Diff not removing a column comment that exists only in the target.
|
||||
| `Issue #10550 <https://github.com/pgadmin-org/pgadmin4/issues/10550>`_ - Fix changing an identity column's START value also restarting its sequence.
|
||||
| `Issue #10551 <https://github.com/pgadmin-org/pgadmin4/issues/10551>`_ - Fix the cursor jumping to the start of SQL fields in dialogs after a quick edit and undo.
|
||||
| `Issue #10554 <https://github.com/pgadmin-org/pgadmin4/issues/10554>`_ - Fix column properties reporting an arbitrary parent for columns merged from several parents.
|
||||
| `Issue #10573 <https://github.com/pgadmin-org/pgadmin4/issues/10573>`_ - Fix Query Tool state save/restore failing on installations with a legacy ``SECRET_KEY``.
|
||||
| `Issue #10584 <https://github.com/pgadmin-org/pgadmin4/issues/10584>`_ - Preserve explicit 24-hour mode in date-time picker popups.
|
||||
| `Issue #10597 <https://github.com/pgadmin-org/pgadmin4/issues/10597>`_ - Fix Schema Diff treating a function whose input argument was renamed as two unrelated functions.
|
||||
| `Issue #10598 <https://github.com/pgadmin-org/pgadmin4/issues/10598>`_ - Keep the Query Tool execute options when its transaction is re-created.
|
||||
| `Issue #10600 <https://github.com/pgadmin-org/pgadmin4/issues/10600>`_ - Revert the Query Tool execute option checkboxes when changing an option fails.
|
||||
| `Issue #10603 <https://github.com/pgadmin-org/pgadmin4/issues/10603>`_ - Fix the Query Tool not re-running the query after reconnecting at a password prompt.
|
||||
| `Issue #10604 <https://github.com/pgadmin-org/pgadmin4/issues/10604>`_ - Report a failed autocomplete reconnect in the Query Tool instead of ignoring it.
|
||||
| `Issue #10607 <https://github.com/pgadmin-org/pgadmin4/issues/10607>`_ - Fix Query Tool autocomplete objects being shared between sessions that use the same transaction id.
|
||||
| `Issue #10612 <https://github.com/pgadmin-org/pgadmin4/issues/10612>`_ - Free Query Tool autocomplete objects when an idle session's connections are released.
|
||||
| `Issue #10613 <https://github.com/pgadmin-org/pgadmin4/issues/10613>`_ - Reconnect a lost Query Tool connection as the user and role it was changed to.
|
||||
| `Issue #10614 <https://github.com/pgadmin-org/pgadmin4/issues/10614>`_ - Keep external pages out of the desktop runtime's privileged windows and confine non-prompted downloads to the downloads directory.
|
||||
| `Issue #10618 <https://github.com/pgadmin-org/pgadmin4/issues/10618>`_ - Keep Query Tool execution disabled until a restored tab's execute options reach the server.
|
||||
| `Issue #10645 <https://github.com/pgadmin-org/pgadmin4/issues/10645>`_ - Fix connection redirection in the materialized view refresh, where the target database's name was passed straight to psql's ``--dbname``: because libpq expands a value containing an equals sign into a full connection string, a database named like ``host=... port=...``, which any role with only CREATEDB can create, redirected psql to a server of the creator's choosing and leaked the connecting user's saved password (CVE-2026-116252). The name is now passed through the same connection-information helper as the Backup, Restore, Maintenance and Import/Export tools, so libpq treats it strictly as a database name. Reported by Bae Seung-won (@baeseungwon1010).
|
||||
| `Issue #10646 <https://github.com/pgadmin-org/pgadmin4/issues/10646>`_ - Fix a user's other sessions surviving a password change, which left a session an attacker already held valid after the password was changed or reset (CVE-2026-116251). A self-service change now rotates the user's ``fs_uniquifier``, logging out every other session while the changing session stays signed in with its database connections, and saved passwords, including those of shared servers, are re-encrypted under the new key, with any that cannot be decrypted cleared, and all of them cleared if re-encryption fails. An administrator reset (including ``setup.py update-user``) also rotates ``fs_uniquifier``, ending all of that user's sessions, but does not re-encrypt their saved passwords. Reported by Zohaib.
|
||||
| `Issue #10647 <https://github.com/pgadmin-org/pgadmin4/issues/10647>`_ - Fix time-of-check to time-of-use symlink races in Import/Export Servers and the File Manager's upload, save and file-open paths, where a path validated against the user's storage directory was then opened by name, so a symbolic link swapped in at the file or any parent directory in between let an export overwrite, or an import read, a file outside the storage directory (CVE-2026-116250). In server mode on POSIX the file is now opened by walking down from the storage directory one component at a time with ``O_NOFOLLOW``; new files are created with mode 0600 and only regular files are read, opened non-blocking so a FIFO cannot hang the request. Symbolic links inside a storage directory are now refused, even when they point elsewhere inside it. Windows and desktop mode are unchanged, so the race remains there. Reported by MUTED CYBERSECURITY S.L.
|
||||
| `Issue #10648 <https://github.com/pgadmin-org/pgadmin4/issues/10648>`_ - Fix the same symlink races in the files handed to Backup, Restore and Import/Export Data, where the validated path was passed to pg_dump, pg_dumpall, pg_restore or psql, which opened it by name later, extending the fix for the vulnerability tracked in Issue #10647. In server mode on POSIX pgAdmin now opens the file itself with the descriptor-relative ``O_NOFOLLOW`` walk, accepts only a regular file and hands the utility ``/dev/fd/N``, and new backup and export files are created with mode 0600. This applies on Linux only, and not to directory-format Backup and Restore, where the utility opens the files inside the directory by name; on macOS and the BSDs the file is checked safely but the utility still reopens it by name.
|
||||
| `Issue #10649 <https://github.com/pgadmin-org/pgadmin4/issues/10649>`_ - Refuse hard links and FIFOs in the symlink-race fixes of Issue #10647 and Issue #10648, since ``O_NOFOLLOW`` stops only symbolic links: in server mode on POSIX, a file with more than one link is refused, for reading as well as before it is truncated for writing, and a FIFO can no longer hang an upload, save or export; Windows is not covered. The Import/Export Servers, save, Backup, Restore and Import/Export Data handlers also return the reason for a failure instead of text containing the absolute storage path.
|
||||
| `Issue #10650 <https://github.com/pgadmin-org/pgadmin4/issues/10650>`_ - Fix connection file paths escaping the user's storage directory, where a password file, SSL certificate, key or CRL, or SSH tunnel identity file such as ``../<another user>/.pgpass`` was joined to the storage directory after stripping only leading slashes, so a user could connect with another user's credentials or key, and where ``sslkeylogfile`` was copied into the connection string unchanged, letting libpq create or append a file as the pgAdmin service account (CVE-2026-116249). In server mode the resolved path, with symbolic links followed, must now stay inside the user's storage directory or the named shared storage directory, and ``sslkeylogfile`` is confined the same way. Connection parameter keys are also validated against the libpq keyword list, since a crafted key could smuggle its own ``keyword=value`` pairs past these checks. Servers whose file paths reach outside the storage directory, through ``..`` or a symbolic link, stop connecting until the files are moved into it or into a ``SHARED_STORAGE`` directory.
|
||||
| `Issue #10651 <https://github.com/pgadmin-org/pgadmin4/issues/10651>`_ - Fix any user able to register a server being able to share it with every other user in server mode, because the create and update endpoints stored whatever ``shared`` value they were sent, and the servers.json import removed a non-administrator's shared entries only after validation, so an early error left them in place (CVE-2026-116248). Such requests are now refused with a 403, and the import prunes and skips a non-administrator's shared entries before validating. The restriction is not retroactive: servers shared by non-administrators before the upgrade stay shared until the owner un-shares them or an administrator clears the ``shared`` column in the configuration database, and pgAdmin only logs a warning about each of them at startup, so administrators should review those servers and un-share any that should not be shared. ``Shared`` in a servers.json import must now be the JSON value ``true``, and a file that fails validation is refused as a whole.
|
||||
| `Issue #10652 <https://github.com/pgadmin-org/pgadmin4/issues/10652>`_ - Stop libpq using the pgAdmin service account's own client files in server mode: when a server had no password file, client certificate or client key configured, libpq fell back to the service account's ``~/.pgpass``, ``~/.postgresql/postgresql.crt`` and ``~/.postgresql/postgresql.key`` (or ``PGPASSFILE``, ``PGSSLCERT`` and ``PGSSLKEY``), letting any user connect with the service account's saved passwords or client certificate (CVE-2026-116247). In server mode each of these parameters is now always given a value, the user's own file or a path that can never be opened, in the connection string and for the utilities. Server mode deployments relying on the service account's files, or on ``passfile``, ``sslcert`` or ``sslkey`` in a connection service entry, must configure them on each server instead, and a server with a client certificate must also have a client key.
|
||||
| `Issue #10653 <https://github.com/pgadmin-org/pgadmin4/issues/10653>`_ - Fix server passwords being left in pgAdmin's own process environment, where the password handed to psql and to the utilities was written into ``os.environ`` and never removed, so another user's later psql session or utility inherited it and libpq used it for any connection made without a password of its own (CVE-2026-116246). The password now goes only into the environment of the process that needs it.
|
||||
| `Issue #10654 <https://github.com/pgadmin-org/pgadmin4/issues/10654>`_ - Restrict connection services in server mode, where any user could set a server's service, or a ``service`` connection parameter, to an entry in the service account's connection service file and connect with that entry's credentials, and where a ``PGPASSWORD`` or ``PGSERVICE`` variable set for that account was used for every connection (CVE-2026-116245). A service may now only be used if the administrator lists it in the new ``SERVER_MODE_ALLOWED_SERVICES`` setting, which is empty by default; the ``servicefile`` parameter is refused, and ``PGPASSWORD`` and ``PGSERVICE`` are removed from pgAdmin's environment at startup. ``setup.py load-servers`` also refuses an unlisted service. In server mode, servers that use a service stop connecting until the administrator lists it in ``SERVER_MODE_ALLOWED_SERVICES`` in ``config_local.py``, ideally before upgrading. Listing a service shares its credentials with every pgAdmin user, so list only services intended for everyone.
|
||||
| `Issue #10655 <https://github.com/pgadmin-org/pgadmin4/issues/10655>`_ - Stop libpq using the pgAdmin service account's Kerberos credentials in server mode, where removing ``KRB5CCNAME`` let the Kerberos library fall back to the service account's default credential cache or client keytab (or its logon, through SSPI on Windows), so any user could answer a server's GSSAPI request as the service account's principal (CVE-2026-116244). In server mode ``KRB5CCNAME`` and ``KRB5_CLIENT_KTNAME`` now point at a file that cannot exist, except that a user who logged in with Kerberos gets their own delegated ``KRB5CCNAME`` when connecting to a server with Kerberos authentication enabled. Every other connection also gets ``gssencmode=disable`` and, with libpq 16 or later, ``require_auth=!gss,!sspi``; without ``require_auth``, SSPI and gssproxy are not stopped, so upgrade the libpq used by pgAdmin and its utilities to 16 or later. A server whose ``require_auth`` allows only GSSAPI or SSPI is refused unless the user logged in with Kerberos. A service listed in ``SERVER_MODE_ALLOWED_SERVICES`` takes precedence for psql and the utilities, so it must not set ``require_auth``, ``gssencmode`` or ``gsslib``.
|
||||
| `Issue #10656 <https://github.com/pgadmin-org/pgadmin4/issues/10656>`_ - Fix the utility binary-path guard accepting a path under ``SHARED_STORAGE``, which ordinary users can write to, so a user could point a server type's binary path at a file placed there and have pgAdmin execute it (CVE-2026-116243). Every ``SHARED_STORAGE`` entry is now rejected alongside ``STORAGE_DIR``, after expanding ``$DIR`` and ``..`` and resolving symbolic links, so an existing binary path under a shared storage directory stops working. An empty ``STORAGE_DIR`` is not covered. Reported by Harsh Singh.
|
||||
| `Issue #10662 <https://github.com/pgadmin-org/pgadmin4/issues/10662>`_ - Stop SSH tunnels using the pgAdmin service account's own SSH identity, where sshtunnel always loaded the unencrypted private keys in the service account's ``~/.ssh``, read its ``~/.ssh/config`` and could use its ssh-agent, so any user could configure a tunnel that authenticated with the service account's key (CVE-2026-116242). In server mode the tunnel no longer reads those keys or that configuration, or uses the agent, and an identity file that cannot be found in the user's storage is reported. Tunnels relying on the service account's key, config or agent must have an identity file in the user's storage or a password configured on the server.
|
||||
| `Issue #10663 <https://github.com/pgadmin-org/pgadmin4/issues/10663>`_ - Fix OS command injection in the Import/Export Data tool, where a query-based export could pass a crafted query past the ``\copy (...)`` parenthesis-balance guard through a backslash inside a double-quoted identifier, which psql treats as an escape when ``standard_conforming_strings`` is off, exposing a live ``TO PROGRAM`` clause that runs an arbitrary command on the pgAdmin host, an incomplete fix for CVE-2026-17566 (CVE-2026-116241). A backslash inside a double-quoted identifier is now rejected, and so is a backslash or null byte in the schema, table or column names of an export or import, which reached the same ``\copy`` command through ``qtIdent()``. Reported by Yonghwa Lee (Xint by Theori).
|
||||
| `Issue #10674 <https://github.com/pgadmin-org/pgadmin4/issues/10674>`_ - Fix the File Manager and the storage-path tools acting on a different path from the one that was checked (CVE-2026-116240): opening, saving and listing files, download, delete, rename, create-folder and upload could name files outside the user's storage directory, or in another user's directory whose name begins with the same characters. Every File Manager and storage-path caller now acts on the absolute path that was checked, and in server mode on POSIX files are opened relative to the storage directory without following symbolic links, so symbolic links to directories and hard-linked files inside it can no longer be opened, downloaded or renamed through. The storage directory itself can no longer be deleted.
|
||||
| `Issue #10675 <https://github.com/pgadmin-org/pgadmin4/issues/10675>`_ - Fix LDAP and Kerberos authentication logging a user in to a different account that shared their username: Kerberos and LDAP looked the account up by username alone, or accepted the account Flask-Security's internal validation had matched by the email typed, so a Kerberos or LDAP user could be logged in as an Internal account, including the initial administrator, or as another user (CVE-2026-116239). Accounts are now looked up by the authenticated username and the source that authenticated them, and only internal authentication logs in an account from its own form. Webserver users whose username matches an Internal account, and LDAP users with capitals in their username, now log in correctly, and ``setup.py`` can update a user without a role and add one without ``--role``. Users who relied on being logged in to an Internal account of the same name now need an account of their own source. Reported by Yves Soete of Blacksight LLC (@yssoe).
|
||||
| `Issue #10676 <https://github.com/pgadmin-org/pgadmin4/issues/10676>`_ - Fix bypasses of the failed-login limit and of pgAdmin's own login, password change and password reset checks, where a username in another case or a wrong internal password with LDAP or Webserver authentication also configured was never counted, and where Flask-Security's own ``/login``, ``/verify``, ``/change``, ``/reset`` and ``/reset/<token>`` views accepted submissions that skipped the failed-login count and lock, the change password permission, the re-encryption of saved passwords, and the internal-account check on resets (CVE-2026-116238). Every failed internal login is now counted (when ``MAX_LOGIN_ATTEMPTS`` is set) and only internal authentication is locked, so a locked Internal account no longer blocks other sources. A login form that posts a password is no longer passed to Kerberos, OAuth2 or Webserver authentication, submissions to Flask-Security's views are refused with 405, and password resets act only on the internal account.
|
||||
@@ -77,6 +77,10 @@ tab to provide options related to data or pgAdmin objects that correspond to *pg
|
||||
* Move the switch next to *Only schema* to limit the restoration to
|
||||
schema-level database objects.
|
||||
|
||||
* Move the switch next to *Only statistics* to the *Yes* position to limit
|
||||
the restoration to optimizer statistics. **Note:** This option is visible
|
||||
only for database server greater than or equal to 18.
|
||||
|
||||
.. image:: images/restore_do_not_save.png
|
||||
:alt: Restore dialog do not save section
|
||||
:align: center
|
||||
@@ -110,6 +114,22 @@ tab to provide options related to data or pgAdmin objects that correspond to *pg
|
||||
Table access methods. **Note:** This option is visible only for
|
||||
database server greater than or equal to 15.
|
||||
|
||||
* Move the switch next to *Row security policies* to the *Yes* position to
|
||||
exclude row security policies. **Note:** This option is visible only for
|
||||
database server greater than or equal to 18.
|
||||
|
||||
* Move the switch next to *Data* to the *Yes* position to exclude data from
|
||||
the restore. **Note:** This option is visible only for database server
|
||||
greater than or equal to 18.
|
||||
|
||||
* Move the switch next to *Schema* to the *Yes* position to exclude schema
|
||||
(the data definitions) from the restore. **Note:** This option is visible
|
||||
only for database server greater than or equal to 18.
|
||||
|
||||
* Move the switch next to *Statistics* to the *Yes* position to exclude
|
||||
optimizer statistics from the restore. **Note:** This option is visible
|
||||
only for database server greater than or equal to 18.
|
||||
|
||||
.. image:: images/restore_queries.png
|
||||
:alt: Restore dialog queries section
|
||||
:align: center
|
||||
|
||||
@@ -195,7 +195,7 @@ the Python Wheel (you may need to adjust the path to suit your installation):
|
||||
gunicorn --bind 0.0.0.0:80 \
|
||||
--workers=1 \
|
||||
--threads=25 \
|
||||
--chdir /usr/lib/python3.7/dist-packages/pgadmin4 \
|
||||
--chdir /usr/lib/python3.13/dist-packages/pgadmin4 \
|
||||
pgAdmin4:app
|
||||
|
||||
Standalone uWSGI Configuration
|
||||
@@ -210,9 +210,48 @@ the Python Wheel (you may need to adjust the path to suit your installation):
|
||||
uwsgi --http-socket 0.0.0.0:80 \
|
||||
--processes 1 \
|
||||
--threads 25 \
|
||||
--chdir /usr/lib/python3.7/dist-packages/pgadmin4/ \
|
||||
--chdir /usr/lib/python3.13/dist-packages/pgadmin4/ \
|
||||
--mount /=pgAdmin4:app
|
||||
|
||||
.. _sub_directory_hosting:
|
||||
|
||||
Hosting in a Sub-Directory
|
||||
--------------------------
|
||||
|
||||
Any of the reverse proxy configurations described below can host pgAdmin in a
|
||||
sub-directory of the server (``/pgadmin4`` in each of the examples) rather than
|
||||
at its root, but doing so requires that the ``SCRIPT_NAME`` environment
|
||||
variable is set for the Gunicorn or uWSGI process, whichever is in use.
|
||||
|
||||
pgAdmin reads ``SCRIPT_NAME`` from the process environment once, at startup,
|
||||
and uses it to set ``APPLICATION_ROOT`` and ``SESSION_COOKIE_PATH``, so that
|
||||
URLs, redirects (including the one issued after a successful login) and
|
||||
session cookies are all generated relative to the sub-directory rather than to
|
||||
the root of the server. Without it, a successful login redirects to the root of
|
||||
the server, which typically presents to the user as a redirect loop.
|
||||
|
||||
This is a separate mechanism from the per-request ``X-Script-Name`` header that
|
||||
the NGINX Gunicorn example below sets, and neither substitutes for the other.
|
||||
It is also separate from uWSGI's ``--manage-script-name``, which only affects
|
||||
the per-request WSGI environment that uWSGI builds; the uWSGI examples below
|
||||
set ``SCRIPT_NAME`` alongside it so that both mechanisms agree, and so that the
|
||||
session cookie is confined to the sub-directory rather than being issued for
|
||||
the root of the server.
|
||||
|
||||
.. _proxy_headers:
|
||||
|
||||
Reverse Proxy Headers
|
||||
---------------------
|
||||
|
||||
pgAdmin honours the ``X-Script-Name`` and ``X-Scheme`` request headers
|
||||
unconditionally, unlike the ``X-Forwarded-*`` headers, which are trusted only
|
||||
to the depth configured by the ``PROXY_X_*_COUNT`` settings. A client that
|
||||
supplied its own ``X-Script-Name`` could therefore change every URL pgAdmin
|
||||
generates, so the reverse proxy must always either overwrite both headers with
|
||||
values of its own or remove them outright, rather than passing on whatever the
|
||||
client sent. Each of the examples below does one or the other, and a proxy
|
||||
configuration not derived from them should do the same.
|
||||
|
||||
NGINX Configuration with Gunicorn
|
||||
---------------------------------
|
||||
|
||||
@@ -228,7 +267,7 @@ command similar to:
|
||||
gunicorn --bind unix:/tmp/pgadmin4.sock \
|
||||
--workers=1 \
|
||||
--threads=25 \
|
||||
--chdir /usr/lib/python3.7/dist-packages/pgadmin4 \
|
||||
--chdir /usr/lib/python3.13/dist-packages/pgadmin4 \
|
||||
pgAdmin4:app
|
||||
|
||||
And configure NGINX:
|
||||
@@ -238,11 +277,24 @@ And configure NGINX:
|
||||
location / {
|
||||
include proxy_params;
|
||||
proxy_pass http://unix:/tmp/pgadmin4.sock;
|
||||
proxy_set_header X-Script-Name "";
|
||||
proxy_set_header X-Scheme "";
|
||||
}
|
||||
|
||||
Alternatively, pgAdmin can be hosted in a sub-directory (/pgadmin4 in this case)
|
||||
on the server. Start Gunicorn as when using the root directory, but configure
|
||||
NGINX as follows:
|
||||
on the server. Start Gunicorn as when using the root directory, but also set the
|
||||
``SCRIPT_NAME`` environment variable for the Gunicorn process, as described in
|
||||
:ref:`sub_directory_hosting`:
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
SCRIPT_NAME=/pgadmin4 gunicorn --bind unix:/tmp/pgadmin4.sock \
|
||||
--workers=1 \
|
||||
--threads=25 \
|
||||
--chdir /usr/lib/python3.13/dist-packages/pgadmin4 \
|
||||
pgAdmin4:app
|
||||
|
||||
Then configure NGINX:
|
||||
|
||||
.. code-block:: nginx
|
||||
|
||||
@@ -250,6 +302,7 @@ NGINX as follows:
|
||||
include proxy_params;
|
||||
proxy_pass http://unix:/tmp/pgadmin4.sock;
|
||||
proxy_set_header X-Script-Name /pgadmin4;
|
||||
proxy_set_header X-Scheme "";
|
||||
}
|
||||
|
||||
NGINX Configuration with uWSGI
|
||||
@@ -267,7 +320,7 @@ command similar to:
|
||||
uwsgi --socket /tmp/pgadmin4.sock \
|
||||
--processes 1 \
|
||||
--threads 25 \
|
||||
--chdir /usr/lib/python3.7/dist-packages/pgadmin4/ \
|
||||
--chdir /usr/lib/python3.13/dist-packages/pgadmin4/ \
|
||||
--manage-script-name \
|
||||
--mount /=pgAdmin4:app
|
||||
|
||||
@@ -279,20 +332,23 @@ And configure NGINX:
|
||||
location @pgadmin4 {
|
||||
include uwsgi_params;
|
||||
uwsgi_pass unix:/tmp/pgadmin4.sock;
|
||||
uwsgi_param HTTP_X_SCRIPT_NAME "";
|
||||
uwsgi_param HTTP_X_SCHEME "";
|
||||
}
|
||||
|
||||
Alternatively, pgAdmin can be hosted in a sub-directory (/pgadmin4 in this case)
|
||||
on the server. Start uWSGI, noting that the directory name is specified in the
|
||||
``mount`` parameter:
|
||||
``mount`` parameter, and that the ``SCRIPT_NAME`` environment variable should
|
||||
be set alongside it, as described in :ref:`sub_directory_hosting`:
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
uwsgi --socket /tmp/pgadmin4.sock \
|
||||
--processes 1 \
|
||||
--threads 25 \
|
||||
--chdir /usr/lib/python3.7/dist-packages/pgadmin4/ \
|
||||
--manage-script-name \
|
||||
--mount /pgadmin4=pgAdmin4:app
|
||||
SCRIPT_NAME=/pgadmin4 uwsgi --socket /tmp/pgadmin4.sock \
|
||||
--processes 1 \
|
||||
--threads 25 \
|
||||
--chdir /usr/lib/python3.13/dist-packages/pgadmin4/ \
|
||||
--manage-script-name \
|
||||
--mount /pgadmin4=pgAdmin4:app
|
||||
|
||||
Then, configure NGINX:
|
||||
|
||||
@@ -303,6 +359,161 @@ Then, configure NGINX:
|
||||
location @pgadmin4 {
|
||||
include uwsgi_params;
|
||||
uwsgi_pass unix:/tmp/pgadmin4.sock;
|
||||
uwsgi_param HTTP_X_SCRIPT_NAME "";
|
||||
uwsgi_param HTTP_X_SCHEME "";
|
||||
}
|
||||
|
||||
Caddy Configuration with Gunicorn
|
||||
----------------------------------
|
||||
|
||||
pgAdmin can be hosted by Gunicorn, with Caddy in front of it as a reverse
|
||||
proxy. Note that these examples assume pgAdmin was installed using the Python
|
||||
Wheel (you may need to adjust the path to suit your installation).
|
||||
|
||||
To run with pgAdmin in the root directory of the server, start Gunicorn using
|
||||
a command similar to:
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
gunicorn --bind unix:/run/pgadmin4/pgadmin4.sock \
|
||||
--workers=1 \
|
||||
--threads=25 \
|
||||
--chdir /usr/lib/python3.13/dist-packages/pgadmin4 \
|
||||
pgAdmin4:app
|
||||
|
||||
And configure Caddy:
|
||||
|
||||
.. code-block:: text
|
||||
|
||||
pgadmin.example.com {
|
||||
reverse_proxy unix//run/pgadmin4/pgadmin4.sock {
|
||||
header_up -X-Script-Name
|
||||
header_up -X-Scheme
|
||||
}
|
||||
}
|
||||
|
||||
.. _caddy_socket_location:
|
||||
|
||||
.. note:: The examples deliberately avoid ``/tmp`` for the socket. The systemd
|
||||
unit shipped with Caddy sets ``PrivateTmp=true``, which gives the Caddy
|
||||
process a private ``/tmp`` directory of its own, so a socket created by
|
||||
Gunicorn or uWSGI in the system ``/tmp`` is invisible to Caddy, which will
|
||||
report a 502 error for every request. Any directory outside ``/tmp`` that
|
||||
both processes can access will do, provided the user Caddy runs as has
|
||||
permission to read and write the socket; alternatively, have Gunicorn or
|
||||
uWSGI listen on a TCP port such as ``127.0.0.1:5050`` and proxy to that
|
||||
instead. Whichever directory is used, it must exist and be writable by the
|
||||
user Gunicorn or uWSGI runs as before the socket can be bound; under
|
||||
systemd, ``RuntimeDirectory=pgadmin4`` in the unit that starts Gunicorn or
|
||||
uWSGI will create ``/run/pgadmin4`` on start and remove it on stop. The
|
||||
same applies to the uWSGI examples below.
|
||||
|
||||
.. _caddy_header_stripping:
|
||||
|
||||
.. note:: The two ``header_up`` directives remove any ``X-Script-Name`` and
|
||||
``X-Scheme`` headers that arrive from the client, for the reasons given in
|
||||
:ref:`proxy_headers`. Each of the Caddy examples removes both, except that
|
||||
the Gunicorn sub-directory example sets ``X-Script-Name`` to the
|
||||
sub-directory instead, which likewise replaces whatever the client sent.
|
||||
|
||||
Alternatively, pgAdmin can be hosted in a sub-directory (/pgadmin4 in this
|
||||
case) on the server. Start Gunicorn as when using the root directory, but
|
||||
also set the ``SCRIPT_NAME`` environment variable for the Gunicorn process, as
|
||||
described in :ref:`sub_directory_hosting`:
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
SCRIPT_NAME=/pgadmin4 gunicorn --bind unix:/run/pgadmin4/pgadmin4.sock \
|
||||
--workers=1 \
|
||||
--threads=25 \
|
||||
--chdir /usr/lib/python3.13/dist-packages/pgadmin4 \
|
||||
pgAdmin4:app
|
||||
|
||||
Then configure Caddy. The request path must be passed through to Gunicorn
|
||||
unchanged, so do not strip the /pgadmin4 prefix; the ``X-Script-Name`` header
|
||||
is set to the sub-directory, both to tell pgAdmin which requests are made
|
||||
under it and to replace any value the client may have sent; and the path
|
||||
matcher must include both ``/pgadmin4`` and ``/pgadmin4/*``, so that pgAdmin's
|
||||
own redirect to the bare sub-directory path (with no trailing slash) is also
|
||||
proxied, rather than falling through to Caddy's default (and rather unhelpful)
|
||||
empty response for an unmatched path:
|
||||
|
||||
.. code-block:: text
|
||||
|
||||
pgadmin.example.com {
|
||||
@pgadmin path /pgadmin4 /pgadmin4/*
|
||||
handle @pgadmin {
|
||||
reverse_proxy unix//run/pgadmin4/pgadmin4.sock {
|
||||
header_up X-Script-Name /pgadmin4
|
||||
header_up -X-Scheme
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Caddy Configuration with uWSGI
|
||||
-------------------------------
|
||||
|
||||
Caddy does not speak the native uwsgi protocol used by NGINX's
|
||||
``uwsgi_pass``, so uWSGI must be run in HTTP mode instead, using
|
||||
``--http-socket`` rather than ``--socket``. Note that these examples assume
|
||||
pgAdmin was installed using the Python Wheel (you may need to adjust the
|
||||
path to suit your installation).
|
||||
|
||||
To run with pgAdmin in the root directory of the server, start uWSGI using a
|
||||
command similar to:
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
uwsgi --http-socket /run/pgadmin4/pgadmin4.sock \
|
||||
--processes 1 \
|
||||
--threads 25 \
|
||||
--chdir /usr/lib/python3.13/dist-packages/pgadmin4/ \
|
||||
--mount /=pgAdmin4:app
|
||||
|
||||
And configure Caddy, again keeping the socket out of ``/tmp`` (see
|
||||
:ref:`the note above <caddy_socket_location>`) and stripping the headers
|
||||
pgAdmin trusts unconditionally (see :ref:`the note above
|
||||
<caddy_header_stripping>`):
|
||||
|
||||
.. code-block:: text
|
||||
|
||||
pgadmin.example.com {
|
||||
reverse_proxy unix//run/pgadmin4/pgadmin4.sock {
|
||||
header_up -X-Script-Name
|
||||
header_up -X-Scheme
|
||||
}
|
||||
}
|
||||
|
||||
Alternatively, pgAdmin can be hosted in a sub-directory (/pgadmin4 in this
|
||||
case) on the server. Start uWSGI, noting that the directory name is specified
|
||||
in the ``mount`` parameter, and that the ``SCRIPT_NAME`` environment variable
|
||||
should still be set alongside it, as described in
|
||||
:ref:`sub_directory_hosting`:
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
SCRIPT_NAME=/pgadmin4 uwsgi --http-socket /run/pgadmin4/pgadmin4.sock \
|
||||
--processes 1 \
|
||||
--threads 25 \
|
||||
--chdir /usr/lib/python3.13/dist-packages/pgadmin4/ \
|
||||
--manage-script-name \
|
||||
--mount /pgadmin4=pgAdmin4:app
|
||||
|
||||
Then configure Caddy as for the Gunicorn sub-directory example above, except
|
||||
that no ``X-Script-Name`` header needs to be set: ``--manage-script-name`` has
|
||||
uWSGI report the mount point to pgAdmin with every request, as in the NGINX
|
||||
uWSGI example above, so the header is only removed:
|
||||
|
||||
.. code-block:: text
|
||||
|
||||
pgadmin.example.com {
|
||||
@pgadmin path /pgadmin4 /pgadmin4/*
|
||||
handle @pgadmin {
|
||||
reverse_proxy unix//run/pgadmin4/pgadmin4.sock {
|
||||
header_up -X-Script-Name
|
||||
header_up -X-Scheme
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Additional Information
|
||||
|
||||
@@ -33,6 +33,18 @@ Use the fields in the *General* tab to identify the server:
|
||||
* If the *Shared?* switch is moved to *Yes* then that server can be
|
||||
shared with all the other users. This option is available only to admin users.
|
||||
For more information on users see :ref:`User Management Dialog <user_management>`.
|
||||
Sharing a server, whether in this dialog, through the API or by importing a
|
||||
servers.json file, is restricted to users with the *Administrator* role.
|
||||
Servers that were shared by non-administrator users before this restriction
|
||||
was introduced remain shared until they are un-shared, so administrators
|
||||
should audit them: look for servers with the shared flag set in the
|
||||
configuration database whose owner does not have the *Administrator* role.
|
||||
pgAdmin logs a warning for each such server when it starts in server mode.
|
||||
Such a server can be un-shared by its owner through the server API (a PUT
|
||||
request on the server with ``"shared": false``), or by an administrator
|
||||
changing the ``shared`` column of the server record in the pgAdmin
|
||||
configuration database. The *Shared?* switch in this dialog is shown only
|
||||
to administrators, and only the owner of a server can change it.
|
||||
Users can access the shared servers with some restrictions - the following
|
||||
operations on shared servers are not permitted:
|
||||
|
||||
@@ -41,6 +53,17 @@ Use the fields in the *General* tab to identify the server:
|
||||
* Rename the group server
|
||||
* Change of host, port, and maintenance database
|
||||
|
||||
Each user of a shared server has their own copy of some of its settings.
|
||||
Tags, colours, the role and the service name are copied from the owner
|
||||
the first time a user opens the server, and each user can then change
|
||||
their copy. The host, port, maintenance database and the general
|
||||
connection parameters (such as *connect_timeout* or *sslmode*) always
|
||||
come from the owner's server definition, so a change the owner makes
|
||||
to them is seen by every user. The owner's password, SSH tunnel
|
||||
credentials, SSL certificate, key and CRL paths, and password file
|
||||
(*passfile*) are never shared: each user must supply their own in the
|
||||
server dialog if they need them.
|
||||
|
||||
Please note that once the server is shared, it's icon is changed in the
|
||||
object explorer.
|
||||
|
||||
@@ -85,6 +108,17 @@ Use the fields in the *Connection* tab to configure a connection:
|
||||
see
|
||||
`Section 33.16 of the Postgres documentation <https://www.postgresql.org/docs/current/libpq-pgservice.html>`_.
|
||||
|
||||
In server mode, services are read from the connection service file of the
|
||||
account that pgAdmin runs as, so a server can use only a service that the
|
||||
administrator has listed in the ``SERVER_MODE_ALLOWED_SERVICES`` setting
|
||||
(see :ref:`config_py`), which is empty by default. A server that uses any
|
||||
other service cannot be saved, and an existing one fails to connect with an
|
||||
error naming the setting. Listing a service shares its credentials, such as
|
||||
a password in its entry, with every pgAdmin user. The ``servicefile``
|
||||
connection parameter cannot be used in server mode, and pgAdmin ignores any
|
||||
``PGPASSWORD`` or ``PGSERVICE`` environment variable set for the account it
|
||||
runs as.
|
||||
|
||||
Click the *Parameters* tab to continue.
|
||||
|
||||
.. image:: images/server_parameters.png
|
||||
@@ -107,6 +141,10 @@ Click on the *+* button to add a new parameter. Some of the parameters are:
|
||||
in seconds. Zero or not specified means wait indefinitely. It is not
|
||||
recommended to use a timeout of less than 2 seconds. By default it is set to
|
||||
10 seconds.
|
||||
* *Require authentication* (*require_auth*) field to specify the
|
||||
authentication methods the server may use. In server mode, pgAdmin
|
||||
refuses the GSSAPI and SSPI methods for a connection that may not use
|
||||
Kerberos, as described in :ref:`kerberos`.
|
||||
* *SSL mode* field to select the type of SSL connection the server should use.
|
||||
For more information about using SSL encryption, see
|
||||
`Section 33.18 of the Postgres documentation <https://www.postgresql.org/docs/current/libpq-ssl.html>`_.
|
||||
@@ -145,6 +183,20 @@ icon that is located to the right of each of the following fields.
|
||||
means that administrators of the server may be able to access those files;
|
||||
appropriate caution should be taken before choosing to use this feature.
|
||||
|
||||
.. note:: In Server mode, every connection file path (the *Password file*,
|
||||
client certificate and key, root certificate, certificate revocation list,
|
||||
CRL directory and SSL key log file) must resolve inside your storage
|
||||
directory, or inside a *SHARED_STORAGE* entry (see :ref:`config_py`)
|
||||
written as *Name:/path*. A path that leaves it through *..* or a symbolic
|
||||
link is refused with an "Access denied (...): the path resolves outside
|
||||
your storage directory..." message when connecting or starting a utility.
|
||||
If you used a symbolic link in your storage directory to reach, for
|
||||
example, */etc/pgadmin/certs*, configure that directory as a
|
||||
*SHARED_STORAGE* entry instead (see :ref:`storage_manager`). Every pgAdmin
|
||||
user can use a file in shared storage, so it should hold only shareable
|
||||
files such as a root certificate or CRL, never a private key or a
|
||||
*.pgpass* file.
|
||||
|
||||
Click the *SSH Tunnel* tab to continue.
|
||||
|
||||
.. image:: images/server_ssh_tunnel.png
|
||||
@@ -175,6 +227,9 @@ not be able to connect directly.
|
||||
|
||||
* If the SSH host is expecting a private key file for authentication, use the
|
||||
*Identity file* field to specify the location of the key file.
|
||||
In server mode, pgAdmin does not use the private keys, ssh-agent or
|
||||
``~/.ssh/config`` of the account pgAdmin runs as; the identity file must be
|
||||
located inside your storage directory.
|
||||
* If the SSH host is expecting a password of the user name or an identity file
|
||||
if being used, use the *Password* field to specify the password.
|
||||
* Check the box next to *Prompt for password?* to to have pgAdmin prompt for
|
||||
@@ -211,7 +266,7 @@ Use the fields in the *Advanced* tab to configure a connection:
|
||||
that will be used against the pg_database table to limit the databases that you see.
|
||||
For example, you might enter: *live_db test_db* so that only live_db and test_db
|
||||
are shown in the pgAdmin object explorer.
|
||||
* Use the *Password exec command* field to specify a shell command to be executed
|
||||
* In desktop mode, use the *Password exec command* field to specify a shell command to be executed
|
||||
to retrieve a password to be used for SQL authentication. The ``stdout`` of the
|
||||
command will be used as the SQL password. This may be useful when the password
|
||||
should be generated as a transient authorization token instead of providing a
|
||||
@@ -219,8 +274,36 @@ Use the fields in the *Advanced* tab to configure a connection:
|
||||
You can pass server hostname, port and DB username to the password exec command as variable by providing placeholders
|
||||
like ``%HOSTNAME%``, ``%PORT%`` and ``%USERNAME%`` which will be replace with the server connection information.
|
||||
Example: ``/path/to/script --hostname %HOSTNAME% --port %PORT% --username %USERNAME%``
|
||||
* In server mode, use the *Password exec command* drop-down list to select one
|
||||
of the commands that the administrator has defined in the
|
||||
``SERVER_PASSEXEC_COMMANDS`` setting (see :ref:`config_py`). Users cannot
|
||||
enter their own command. The list offers *None* and each configured command
|
||||
name, and is hidden if no commands have been configured. On a shared server,
|
||||
users who do not own the server also see *Inherit from owner*, which is the
|
||||
default for them and uses whatever command the owner has selected; selecting
|
||||
*None* or a named command overrides it for that user only. If a selected
|
||||
command is later removed from the configuration, the server is treated as
|
||||
having no command and a warning is logged.
|
||||
|
||||
The command is run directly, without a shell, and does not receive the full
|
||||
environment of the pgAdmin process. Only ``PATH``, ``HOME``, ``LANG``,
|
||||
``LANGUAGE``, ``TZ``, ``TMPDIR``, ``TMP``, ``TEMP``, ``USER``, ``LOGNAME``
|
||||
and any ``LC_*`` variable are passed through (and, on Windows,
|
||||
``SYSTEMROOT``, ``SYSTEMDRIVE``, ``PATHEXT``, ``COMSPEC`` and ``WINDIR``),
|
||||
together with any variables the administrator has named in the
|
||||
``SERVER_PASSEXEC_ENV_PASSTHROUGH`` setting. The command also receives the
|
||||
following variables describing the connection, which take precedence over
|
||||
any variable of the same name:
|
||||
``PGADMIN_PASSEXEC_HOST``, ``PGADMIN_PASSEXEC_PORT``,
|
||||
``PGADMIN_PASSEXEC_USERNAME`` (the username that connection uses, so a
|
||||
non-owner's own username on a shared server), ``PGADMIN_PASSEXEC_DATABASE``
|
||||
(the maintenance database), ``PGADMIN_PASSEXEC_PGADMIN_USER`` (the pgAdmin
|
||||
user making the connection) and ``PGADMIN_PASSEXEC_AUTH_SOURCE`` (how that
|
||||
user authenticated to pgAdmin). The ``stdout`` of the command is used as the
|
||||
SQL password.
|
||||
* Use the *Password exec expiration* field to specify a maximum age, in seconds,
|
||||
of the password generated with a *Password exec command*. If not specified,
|
||||
of the password generated with a *Password exec command*. In server mode, this field is
|
||||
enabled only when a named command is selected. If not specified,
|
||||
the password will not expire until your pgAdmin session does.
|
||||
Zero means the command will be executed for each new connection or reconnection that is made.
|
||||
If the generated password is not valid indefinitely, set this value to slightly before it will expire.
|
||||
@@ -234,7 +317,34 @@ Use the fields in the *Advanced* tab to configure a connection:
|
||||
.. note:: The password file option is only supported when pgAdmin is using libpq
|
||||
v10.0 or later to connect to the server.
|
||||
|
||||
.. note:: The Password exec option is only supported when pgAdmin is run in desktop mode.
|
||||
.. note:: In server mode, the *Password exec command* setting is available
|
||||
only when the administrator has defined at least one command in
|
||||
``SERVER_PASSEXEC_COMMANDS``. Free-text commands are never run in server
|
||||
mode, and when pgAdmin is upgraded, an existing free-text command is kept
|
||||
only if it exactly matches a configured command (its arguments joined by
|
||||
single spaces), in which case it is converted to that command's name; all
|
||||
others are cleared and a warning is logged. This conversion cannot be
|
||||
undone, so ``SERVER_PASSEXEC_COMMANDS`` must be configured before the
|
||||
first start after upgrading. If it is still empty at that point, the
|
||||
conversion is postponed: existing commands are left in the configuration
|
||||
database unused, a warning giving the number of affected servers is
|
||||
logged at each start, and they are converted or cleared on the first
|
||||
start at which at least one command is configured. Placeholders such as
|
||||
``%HOSTNAME%``, ``%PORT%`` and ``%USERNAME%`` are not substituted in
|
||||
server mode, so a command containing them is converted only if the
|
||||
configured arguments contain the same literal text. On a shared server, a
|
||||
user who does not own the server and whose own command is cleared will
|
||||
then inherit the owner's command, if the owner has one. The
|
||||
``ENABLE_SERVER_PASS_EXEC_CMD`` setting is deprecated and ignored.
|
||||
|
||||
.. warning:: In server mode, only ``PGADMIN_PASSEXEC_PGADMIN_USER`` and
|
||||
``PGADMIN_PASSEXEC_AUTH_SOURCE`` identify the pgAdmin user on whose behalf
|
||||
the command is run. ``PGADMIN_PASSEXEC_HOST``, ``PGADMIN_PASSEXEC_PORT``,
|
||||
``PGADMIN_PASSEXEC_USERNAME`` and ``PGADMIN_PASSEXEC_DATABASE`` are set by
|
||||
whoever edits the server definition; on a shared server, a user who does
|
||||
not own the server sets their own username. Commands should therefore
|
||||
decide whether to issue a password based on the pgAdmin identity, and
|
||||
treat the other four variables as untrusted input.
|
||||
|
||||
Click the *Post Connection SQL* tab to continue.
|
||||
|
||||
|
||||
@@ -0,0 +1,93 @@
|
||||
.. _statistics_dialog:
|
||||
|
||||
**************************
|
||||
`Statistics Dialog`:index:
|
||||
**************************
|
||||
|
||||
Use the *Statistics* dialog to define extended statistics on one or more columns
|
||||
(or expressions) of a table. Extended statistics let PostgreSQL collect
|
||||
correlation data across columns that can significantly improve query-plan
|
||||
estimates for queries that filter or group by multiple columns.
|
||||
|
||||
PostgreSQL introduced ``CREATE STATISTICS`` in PostgreSQL 10, and added
|
||||
expression statistics in PostgreSQL 14. This dialog is available for
|
||||
PostgreSQL 14 and later, which covers all currently supported PostgreSQL
|
||||
versions.
|
||||
|
||||
The *Statistics* dialog organizes options across the *General* and *Definition*
|
||||
tabs. The *SQL* tab displays the SQL command generated by your selections.
|
||||
|
||||
.. image:: images/statistics_general.png
|
||||
:alt: Statistics dialog general tab
|
||||
:align: center
|
||||
|
||||
Use the fields in the *General* tab to describe the statistics object:
|
||||
|
||||
* Use the *Name* field to enter a descriptive name. On PostgreSQL 16 and later
|
||||
the name is optional, and the server will generate one from the table and the
|
||||
columns or expressions if you leave it blank.
|
||||
* Use the *Owner* field to select the role that will own the statistics object.
|
||||
* Use the *Schema* field to select the schema in which the statistics object
|
||||
will reside.
|
||||
* Use the *Table* field to select the table on which the statistics will be
|
||||
collected. The list is filtered to tables in the selected schema.
|
||||
* Use the *Columns* field to select two or more columns. Hold *Ctrl* (or *Cmd*
|
||||
on macOS) to select multiple columns. At least two columns are required when
|
||||
collecting column based statistics, although a single column is enough when
|
||||
it is combined with an expression.
|
||||
* Use the *Statistics types* field to choose which kinds of extended statistics
|
||||
to collect:
|
||||
|
||||
* *N-distinct*, which estimates the number of distinct value combinations
|
||||
across the selected columns or expressions.
|
||||
* *Dependencies*, which detects functional dependencies between columns,
|
||||
improving estimates for queries with correlated ``WHERE`` clauses.
|
||||
* *MCV (Most Common Values)*, which records the most common combinations of
|
||||
values.
|
||||
|
||||
If you leave the field empty, PostgreSQL collects every kind it supports.
|
||||
Leave it empty for statistics on a single expression, which do not accept
|
||||
a choice of kinds.
|
||||
|
||||
* Use the *Comment* field to store an optional note about the statistics object.
|
||||
|
||||
Click the *Definition* tab to continue.
|
||||
|
||||
.. image:: images/statistics_definition.png
|
||||
:alt: Statistics dialog definition tab
|
||||
:align: center
|
||||
|
||||
* Use the *Expressions* field to enter one or more SQL expressions separated by
|
||||
commas (for example ``lower(col1), (col1 + col2)``). Each expression must be
|
||||
enclosed in parentheses unless it is a function call, and the list is passed
|
||||
to the server exactly as you enter it, so an expression may itself contain
|
||||
commas. Expressions may be given instead of columns, or alongside them when
|
||||
you want statistics over a mixture of the two.
|
||||
|
||||
When you open the dialog on an existing statistics object, the *Properties*
|
||||
view also reports the statistics target and, for roles with access, the values
|
||||
that ``ANALYZE`` has collected. Those values are read through the
|
||||
``pg_catalog.pg_stats_ext`` view, which only shows them to roles the server
|
||||
allows to see them (the table's owners, on current PostgreSQL releases), so
|
||||
the *Computed Statistics* group is hidden when the current role lacks access.
|
||||
|
||||
Click the *SQL* tab to continue.
|
||||
|
||||
|
||||
Your entries in the *Statistics* dialog generate a SQL command (see an example
|
||||
below). Use the *SQL* tab for review; revisit or switch tabs to make any changes.
|
||||
|
||||
Example
|
||||
*******
|
||||
|
||||
The following is an example of the SQL command generated by user selections in
|
||||
the *Statistics* dialog:
|
||||
|
||||
.. image:: images/statistics_sql.png
|
||||
:alt: Statistics dialog SQL tab
|
||||
:align: center
|
||||
|
||||
* Click the *Info* button (i) to access online help.
|
||||
* Click the *Save* button to save work.
|
||||
* Click the *Close* button to exit without saving work.
|
||||
* Click the *Reset* button to restore configuration parameters.
|
||||
@@ -60,7 +60,7 @@ Provide information about the new pgAdmin role in the row:
|
||||
specified in the *Email* field. This field is disabled if you select any
|
||||
authentication source except *internal*.
|
||||
* Re-enter the password in the *Confirm password* field. This field is disabled
|
||||
if you select *ldap* as authentication source.
|
||||
if you select any authentication source except *internal*.
|
||||
* *Locked* switch cannot be changed if is turned off. It can only be changed
|
||||
when the user is locked by trying unsuccessful login attempts. Turn off the
|
||||
switch if you want to unlock the account.
|
||||
@@ -132,6 +132,56 @@ a role.
|
||||
|
||||
|
||||
|
||||
.. _auth_source_accounts:
|
||||
|
||||
Accounts from different authentication sources
|
||||
##############################################
|
||||
|
||||
pgAdmin treats accounts from different authentication sources as separate
|
||||
accounts, even when they share a username or an email address. Each has its
|
||||
own role, settings and servers:
|
||||
|
||||
* A user authenticated by LDAP, Kerberos, OAuth2 or webserver authentication
|
||||
is only ever logged in to the account with their username *and* that
|
||||
authentication source, never to an account from another source with the
|
||||
same username. If there is no such account, and the source does not create
|
||||
one automatically, the login fails; pgAdmin does not log the user in to an
|
||||
account from another source instead.
|
||||
* An internal login is checked against, and logs in, only the ``INTERNAL``
|
||||
account.
|
||||
* Only an ``INTERNAL`` account has a password, so *Forgotten your password?*
|
||||
resets only the ``INTERNAL`` account with the email address given.
|
||||
|
||||
The account-lockout setting *MAX_LOGIN_ATTEMPTS* applies only to ``INTERNAL``
|
||||
accounts. pgAdmin never locks an account from another authentication source;
|
||||
brute-force protection for those is the responsibility of the directory,
|
||||
identity provider or reverse proxy. When ``INTERNAL`` and another source are
|
||||
both enabled, a sign-in that fails overall and was tried against an
|
||||
``INTERNAL`` account with that username counts towards its lockout; the attempt
|
||||
that reaches the limit reports that the account is now locked. A locked
|
||||
``INTERNAL`` account stops only internal sign-in: a user of another source with
|
||||
the same username can still sign in. On a later sign-in with that username,
|
||||
pgAdmin discloses that the ``INTERNAL`` account is locked only when internal is
|
||||
the only source able to check the password; while another password source such
|
||||
as LDAP is also eligible, it shows a generic failure instead.
|
||||
|
||||
The administrator account created when pgAdmin is first set up always uses the
|
||||
``INTERNAL`` authentication source. It remains in the pgAdmin database when
|
||||
*AUTHENTICATION_SOURCES* does not include *internal*, although it can then no
|
||||
longer be used to log in. If you use external authentication only, create an
|
||||
administrator account for that source, for example for LDAP:
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
/path/to/python /path/to/setup.py add-external-user ldapuser --auth-source ldap --admin
|
||||
|
||||
and then deactivate the initial ``INTERNAL`` administrator account, so that it
|
||||
cannot be used if internal authentication is enabled again later:
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
/path/to/python /path/to/setup.py update-user admin@example.com --inactive
|
||||
|
||||
Using 'setup.py' command line script
|
||||
####################################
|
||||
|
||||
@@ -146,7 +196,7 @@ Using 'setup.py' command line script
|
||||
without Python interpreter.
|
||||
|
||||
Some of the examples:
|
||||
pgadmin4-cli add-user user1@gmail.com password --role 1
|
||||
pgadmin4-cli add-user user1@gmail.com password --role User
|
||||
pgadmin4-cli get-prefs
|
||||
|
||||
Manage Users
|
||||
@@ -156,7 +206,8 @@ Add User
|
||||
*********
|
||||
|
||||
To add user, invoke ``setup.py`` with ``add-user`` command line option, followed by
|
||||
email and password. role and active will be optional fields.
|
||||
email and password. role and active will be optional fields. When no role is
|
||||
given, the user is created with the *User* role.
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
@@ -166,7 +217,7 @@ email and password. role and active will be optional fields.
|
||||
# role using --role. If both are provided --admin will be used:
|
||||
|
||||
/path/to/python /path/to/setup.py add-user user1@gmail.com password --admin
|
||||
/path/to/python /path/to/setup.py add-user user1@gmail.com password --role Users
|
||||
/path/to/python /path/to/setup.py add-user user1@gmail.com password --role User
|
||||
|
||||
# to specify user's status
|
||||
|
||||
@@ -177,26 +228,28 @@ Add External User
|
||||
*****************
|
||||
|
||||
To add external authentication user, invoke ``setup.py`` with ``add-external-user`` command line option,
|
||||
followed by email, password and authentication source. email, role and status will be optional fields.
|
||||
followed by username, and the authentication source with ``--auth-source`` (*oauth2* if it is not
|
||||
given). email, role and status will be optional fields. When no role is given,
|
||||
the user is created with the *User* role.
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
/path/to/python /path/to/setup.py add-external-user user1@gmail.com ldap
|
||||
/path/to/python /path/to/setup.py add-external-user user1@gmail.com --auth-source ldap
|
||||
|
||||
# to specify an email:
|
||||
|
||||
/path/to/python /path/to/setup.py add-external-user ldapuser ldap --email user1@gmail.com
|
||||
/path/to/python /path/to/setup.py add-external-user ldapuser --auth-source ldap --email user1@gmail.com
|
||||
|
||||
# to specify a role, either you can use --admin for Administrator role or provide the
|
||||
# role using --role. If both are provided --admin will be used:
|
||||
|
||||
/path/to/python /path/to/setup.py add-external-user ldapuser ldap --admin
|
||||
/path/to/python /path/to/setup.py add-external-user ldapuser ldap --role Users
|
||||
/path/to/python /path/to/setup.py add-external-user ldapuser --auth-source ldap --admin
|
||||
/path/to/python /path/to/setup.py add-external-user ldapuser --auth-source ldap --role User
|
||||
|
||||
# to specify user's status
|
||||
|
||||
/path/to/python /path/to/setup.py add-external-user user1@gmail.com ldap --active
|
||||
/path/to/python /path/to/setup.py add-external-user user1@gmail.com ldap --inactive
|
||||
/path/to/python /path/to/setup.py add-external-user user1@gmail.com --auth-source ldap --active
|
||||
/path/to/python /path/to/setup.py add-external-user user1@gmail.com --auth-source ldap --inactive
|
||||
|
||||
Update User
|
||||
***********
|
||||
@@ -211,19 +264,20 @@ email address. password, role and active are updatable fields.
|
||||
# to specify a role, either you can use --admin for Administrator role or provide the
|
||||
# role using --role. If both are provided --admin will be used:
|
||||
|
||||
/path/to/python /path/to/setup.py update-user user1@gmail.com password --admin
|
||||
/path/to/python /path/to/setup.py update-user user1@gmail.com password --role Users
|
||||
/path/to/python /path/to/setup.py update-user user1@gmail.com --admin
|
||||
/path/to/python /path/to/setup.py update-user user1@gmail.com --role User
|
||||
|
||||
# to specify user's status
|
||||
|
||||
/path/to/python /path/to/setup.py update-user user1@gmail.com password --active
|
||||
/path/to/python /path/to/setup.py update-user user1@gmail.com password --inactive
|
||||
/path/to/python /path/to/setup.py update-user user1@gmail.com --active
|
||||
/path/to/python /path/to/setup.py update-user user1@gmail.com --inactive
|
||||
|
||||
Update External User
|
||||
********************
|
||||
|
||||
To update the external user, invoke ``setup.py`` with ``update-external-user`` command line option,
|
||||
followed by username and auth source. email, password, role and active are updatable fields.
|
||||
followed by username, and the authentication source with ``--auth-source`` (*oauth2* if it is not
|
||||
given). email, role and active are updatable fields.
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
@@ -234,19 +288,19 @@ followed by username and auth source. email, password, role and active are updat
|
||||
# to specify a role, either you can use --admin for Administrator role or provide the
|
||||
# role using --role. If both are provided --admin will be used:
|
||||
|
||||
/path/to/python /path/to/setup.py update-external-user user1@gmail.com password --role --admin
|
||||
/path/to/python /path/to/setup.py update-external-user user1@gmail.com password --role --role Users
|
||||
/path/to/python /path/to/setup.py update-external-user ldapuser --auth-source ldap --admin
|
||||
/path/to/python /path/to/setup.py update-external-user ldapuser --auth-source ldap --role User
|
||||
|
||||
# to change user's status
|
||||
|
||||
/path/to/python /path/to/setup.py update-user ldapuser --auth-source ldap --active
|
||||
/path/to/python /path/to/setup.py update-user ldapuser --auth-source ldap --inactive
|
||||
/path/to/python /path/to/setup.py update-external-user ldapuser --auth-source ldap --active
|
||||
/path/to/python /path/to/setup.py update-external-user ldapuser --auth-source ldap --inactive
|
||||
|
||||
Delete User
|
||||
***********
|
||||
|
||||
To delete the user, invoke ``setup.py`` with ``delete-user`` command line option, followed by
|
||||
username and auth_source. For Internal users, email adress will be used instead of username. the option ``--yes`` is available to skip the confirmation prompt before deleting the user.
|
||||
username and auth_source. For Internal users, email address will be used instead of username. the option ``--yes`` is available to skip the confirmation prompt before deleting the user.
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
|
||||
@@ -20,7 +20,7 @@ and modify the values for the following parameters:
|
||||
:widths: 35, 55
|
||||
|
||||
"AUTHENTICATION_SOURCES", "The default value for this parameter is *internal*.
|
||||
To enable OAUTH2 authentication, you must include *webserver* in the list of values
|
||||
To enable Webserver authentication, you must include *webserver* in the list of values
|
||||
for this parameter. you can modify the value as follows:
|
||||
|
||||
* [‘webserver’]: pgAdmin will use only Webserver authentication.
|
||||
@@ -88,6 +88,9 @@ and modify the values for the following parameters:
|
||||
with the internal administrator's password, fix the affected users'
|
||||
*Authentication source*, then remove *internal* again.
|
||||
|
||||
See :ref:`Accounts from different authentication sources
|
||||
<auth_source_accounts>` for more information.
|
||||
|
||||
Master Password
|
||||
===============
|
||||
|
||||
|
||||
@@ -5,8 +5,9 @@ Debian and Ubuntu.
|
||||
|
||||
## Supported platforms
|
||||
|
||||
* Debian 11, 12 & 13
|
||||
* Ubuntu 22.04, 24.04 & 25.04
|
||||
The platforms built are the ones in the matrix in
|
||||
`.github/workflows/check-deb-build.yml`; see `pkg/linux/PLATFORMS.md` for how
|
||||
to add or retire one.
|
||||
|
||||
## Build configuration
|
||||
|
||||
|
||||
@@ -16,26 +16,24 @@ PUID=${PUID:-5050}
|
||||
PGID=${PGID:-0}
|
||||
|
||||
# Validate PUID/PGID are numeric and in acceptable range
|
||||
if ! echo "$PUID" | grep -qE '^[0-9]+$'; then
|
||||
if [[ ! $PUID =~ ^[0-9]+$ ]]; then
|
||||
echo "ERROR: PUID must be a numeric value, got '$PUID'"
|
||||
exit 1
|
||||
fi
|
||||
if ! echo "$PGID" | grep -qE '^[0-9]+$'; then
|
||||
if [[ ! $PGID =~ ^[0-9]+$ ]]; then
|
||||
echo "ERROR: PGID must be a numeric value, got '$PGID'"
|
||||
exit 1
|
||||
fi
|
||||
if [ "$PUID" -eq 0 ]; then
|
||||
if (( 10#$PUID == 0 )); then
|
||||
echo "ERROR: PUID=0 (root) is not allowed. Use a non-root UID."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [ "$(id -u)" = "0" ]; then
|
||||
if (( EUID == 0 )); then
|
||||
# Ensure a group with the target GID exists
|
||||
if ! getent group "$PGID" > /dev/null 2>&1; then
|
||||
if ! addgroup -g "$PGID" pggroup; then
|
||||
echo "ERROR: Failed to create group with GID=$PGID"
|
||||
exit 1
|
||||
fi
|
||||
if ! getent group "$PGID" &>/dev/null && ! addgroup -g "$PGID" pggroup; then
|
||||
echo "ERROR: Failed to create group with GID=$PGID"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Reassign the pgadmin user to the desired UID/GID
|
||||
@@ -46,17 +44,17 @@ if [ "$(id -u)" = "0" ]; then
|
||||
|
||||
# Fix ownership of runtime directories BEFORE any initialization
|
||||
for dir in /run/pgadmin /var/lib/pgadmin; do
|
||||
if [ -d "$dir" ]; then
|
||||
if [[ -d $dir ]]; then
|
||||
chown -R "$PUID:$PGID" "$dir"
|
||||
fi
|
||||
done
|
||||
|
||||
# Fix ownership of individual files (no -R needed)
|
||||
if [ -e /pgadmin4/config_distro.py ]; then
|
||||
if [[ -e /pgadmin4/config_distro.py ]]; then
|
||||
chown "$PUID:$PGID" /pgadmin4/config_distro.py
|
||||
fi
|
||||
|
||||
if [ -d /certs ]; then
|
||||
if [[ -d /certs ]]; then
|
||||
chown -R "$PUID:$PGID" /certs
|
||||
fi
|
||||
|
||||
@@ -66,12 +64,8 @@ else
|
||||
SU_EXEC=""
|
||||
|
||||
# Fixup the passwd file, in case we're on OpenShift
|
||||
if ! whoami > /dev/null 2>&1; then
|
||||
if [ "$(id -u)" -ne 5050 ]; then
|
||||
if [ -w /etc/passwd ]; then
|
||||
echo "${USER_NAME:-pgadminr}:x:$(id -u):0:${USER_NAME:-pgadminr} user:${HOME}:/sbin/nologin" >> /etc/passwd
|
||||
fi
|
||||
fi
|
||||
if ! whoami &>/dev/null && (( EUID != 5050 )) && [[ -w /etc/passwd ]]; then
|
||||
echo "${USER_NAME:-pgadminr}:x:${EUID}:0:${USER_NAME:-pgadminr} user:${HOME}:/sbin/nologin" >>/etc/passwd
|
||||
fi
|
||||
fi
|
||||
|
||||
@@ -103,22 +97,22 @@ if grep -q '^NoNewPrivs:[[:space:]]*1' /proc/self/status 2>/dev/null; then
|
||||
restricted=1
|
||||
fi
|
||||
|
||||
if [ "$restricted" = "0" ]; then
|
||||
if (( restricted == 0 )); then
|
||||
cap_bnd=$(awk '/^CapBnd:/ { print $2 }' /proc/self/status 2>/dev/null)
|
||||
if [ -n "$cap_bnd" ] && [ "$(( 0x${cap_bnd} & 0x400 ))" -eq 0 ]; then
|
||||
if [[ -n $cap_bnd ]] && (( (0x$cap_bnd & 0x400) == 0 )); then
|
||||
restricted=1
|
||||
fi
|
||||
fi
|
||||
|
||||
if [ "$restricted" = "1" ] || [ ! -x /venv/bin/python3-cap ]; then
|
||||
if (( restricted == 1 )) || [[ ! -x /venv/bin/python3-cap ]]; then
|
||||
PYTHON_BIN=/venv/bin/python3
|
||||
if [ -z "${PGADMIN_LISTEN_PORT}" ]; then
|
||||
if [ -n "${PGADMIN_ENABLE_TLS}" ]; then
|
||||
if [[ -z $PGADMIN_LISTEN_PORT ]]; then
|
||||
if [[ -n $PGADMIN_ENABLE_TLS ]]; then
|
||||
export PGADMIN_LISTEN_PORT=8443
|
||||
else
|
||||
export PGADMIN_LISTEN_PORT=8080
|
||||
fi
|
||||
echo "Restricted security context detected; defaulting PGADMIN_LISTEN_PORT to ${PGADMIN_LISTEN_PORT}."
|
||||
echo "Restricted security context detected; defaulting PGADMIN_LISTEN_PORT to $PGADMIN_LISTEN_PORT."
|
||||
fi
|
||||
fi
|
||||
|
||||
@@ -126,30 +120,30 @@ fi
|
||||
# (will allow for "$XYZ_DB_PASSWORD_FILE" to fill in the value of
|
||||
# "$XYZ_DB_PASSWORD" from a file, for Docker's secrets feature)
|
||||
function file_env() {
|
||||
local var="$1"
|
||||
local fileVar="${var}_FILE"
|
||||
local def="${2:-}"
|
||||
if [ "${!var:-}" ] && [ "${!fileVar:-}" ]; then
|
||||
printf >&2 'error: both %s and %s are set (but are exclusive)\n' "$var" "$fileVar"
|
||||
exit 1
|
||||
fi
|
||||
local val="$def"
|
||||
if [ "${!var:-}" ]; then
|
||||
val="${!var}"
|
||||
elif [ "${!fileVar:-}" ] && [ ! -r "${!fileVar}" ]; then
|
||||
printf >&2 'error: %s is set to "%s" but the file does not exist or is not readable\n' \
|
||||
"$fileVar" "${!fileVar}"
|
||||
exit 1
|
||||
elif [ "${!fileVar:-}" ]; then
|
||||
val="$(< "${!fileVar}")"
|
||||
fi
|
||||
export "$var"="$val"
|
||||
unset "$fileVar"
|
||||
local var="$1"
|
||||
local fileVar="${var}_FILE"
|
||||
local def="${2:-}"
|
||||
if [[ -n ${!var:-} && -n ${!fileVar:-} ]]; then
|
||||
printf >&2 'error: both %s and %s are set (but are exclusive)\n' "$var" "$fileVar"
|
||||
exit 1
|
||||
fi
|
||||
local val="$def"
|
||||
if [[ -n ${!var:-} ]]; then
|
||||
val="${!var}"
|
||||
elif [[ -n ${!fileVar:-} && ! -r ${!fileVar} ]]; then
|
||||
printf >&2 'error: %s is set to "%s" but the file does not exist or is not readable\n' \
|
||||
"$fileVar" "${!fileVar}"
|
||||
exit 1
|
||||
elif [[ -n ${!fileVar:-} ]]; then
|
||||
val="$(<"${!fileVar}")"
|
||||
fi
|
||||
export "$var"="$val"
|
||||
unset "$fileVar"
|
||||
}
|
||||
|
||||
# Set values for config variables that can be passed using secrets
|
||||
if [ -n "${PGADMIN_CONFIG_CONFIG_DATABASE_URI_FILE}" ]; then
|
||||
file_env PGADMIN_CONFIG_CONFIG_DATABASE_URI
|
||||
if [[ -n $PGADMIN_CONFIG_CONFIG_DATABASE_URI_FILE ]]; then
|
||||
file_env PGADMIN_CONFIG_CONFIG_DATABASE_URI
|
||||
fi
|
||||
file_env PGADMIN_DEFAULT_PASSWORD
|
||||
|
||||
@@ -159,8 +153,8 @@ export CONFIG_DISTRO_FILE_PATH="${PGADMIN_CUSTOM_CONFIG_DISTRO_FILE:-/pgadmin4/c
|
||||
# provided by the user through the PGADMIN_CONFIG_* environment variables.
|
||||
# Only update the file on first launch. The empty file is created only in default path during the
|
||||
# container build so it can have the required ownership.
|
||||
if [ ! -e "${CONFIG_DISTRO_FILE_PATH}" ] || [ "$(wc -m "${CONFIG_DISTRO_FILE_PATH}" 2>/dev/null | awk '{ print $1 }')" = "0" ]; then
|
||||
cat << EOF > "${CONFIG_DISTRO_FILE_PATH}"
|
||||
if [[ ! -e $CONFIG_DISTRO_FILE_PATH || "$(wc -m "$CONFIG_DISTRO_FILE_PATH" 2>/dev/null | awk '{ print $1 }')" == 0 ]]; then
|
||||
cat <<EOF >"$CONFIG_DISTRO_FILE_PATH"
|
||||
CA_FILE = '/etc/ssl/certs/ca-certificates.crt'
|
||||
LOG_FILE = '/dev/null'
|
||||
HELP_PATH = '../../docs'
|
||||
@@ -175,22 +169,21 @@ DEFAULT_BINARY_PATHS = {
|
||||
}
|
||||
EOF
|
||||
|
||||
# This is a bit kludgy, but necessary as the container uses BusyBox/ash as
|
||||
# it's shell and not bash which would allow a much cleaner implementation
|
||||
for var in $(env | grep "^PGADMIN_CONFIG_" | cut -d "=" -f 1); do
|
||||
# Iterate over PGADMIN_CONFIG_* environment variables and writes them to the config file
|
||||
for var in "${!PGADMIN_CONFIG_@}"; do
|
||||
# Get the raw value
|
||||
val=$(eval "echo \"\$$var\"")
|
||||
val="${!var}"
|
||||
# This normalization step is what makes 'true', 'True'
|
||||
case "$(echo "$val" | tr '[:upper:]' '[:lower:]')" in
|
||||
true) val="True" ;;
|
||||
false) val="False" ;;
|
||||
esac
|
||||
echo "${var#PGADMIN_CONFIG_} = $val" >> "${CONFIG_DISTRO_FILE_PATH}"
|
||||
if [[ "${val,,}" =~ ^(true|false)$ ]]; then
|
||||
val="${val,,}"
|
||||
val="${val^}"
|
||||
fi
|
||||
echo "${var#PGADMIN_CONFIG_} = $val" >>"$CONFIG_DISTRO_FILE_PATH"
|
||||
done
|
||||
|
||||
# If running as root with custom config distro path, fix ownership
|
||||
if [ "$(id -u)" = "0" ] && [ "${CONFIG_DISTRO_FILE_PATH}" != "/pgadmin4/config_distro.py" ]; then
|
||||
chown "$PUID:$PGID" "${CONFIG_DISTRO_FILE_PATH}"
|
||||
if (( EUID == 0 )) && [[ $CONFIG_DISTRO_FILE_PATH != "/pgadmin4/config_distro.py" ]]; then
|
||||
chown "$PUID:$PGID" "$CONFIG_DISTRO_FILE_PATH"
|
||||
fi
|
||||
fi
|
||||
|
||||
@@ -203,7 +196,7 @@ fi
|
||||
# "False" default below is preserved so first-launch user setup still
|
||||
# runs (see #9984).
|
||||
external_config_db_exists="False"
|
||||
if [ -n "${PGADMIN_CONFIG_CONFIG_DATABASE_URI}" ]; then
|
||||
if [[ -n $PGADMIN_CONFIG_CONFIG_DATABASE_URI ]]; then
|
||||
result=$(cd /pgadmin4/pgadmin/utils && $SU_EXEC /venv/bin/python3 -c "
|
||||
import os, ast
|
||||
from check_external_config_db import check_external_config_db
|
||||
@@ -214,7 +207,7 @@ except (ValueError, SyntaxError):
|
||||
uri = raw
|
||||
print(check_external_config_db(uri))
|
||||
" 2>/dev/null)
|
||||
if [ -n "$result" ]; then
|
||||
if [[ -n $result ]]; then
|
||||
external_config_db_exists="$result"
|
||||
fi
|
||||
fi
|
||||
@@ -225,55 +218,70 @@ function load_server_json_file() {
|
||||
|
||||
EXTRA_ARGS=""
|
||||
|
||||
if [ "${PGADMIN_REPLACE_SERVERS_ON_STARTUP}" = "True" ]; then
|
||||
if [[ $PGADMIN_REPLACE_SERVERS_ON_STARTUP == "True" ]]; then
|
||||
EXTRA_ARGS="--replace"
|
||||
fi
|
||||
|
||||
if [ -f "${PGADMIN_SERVER_JSON_FILE}" ]; then
|
||||
if [[ -f $PGADMIN_SERVER_JSON_FILE ]]; then
|
||||
# When running in Desktop mode, no user is created
|
||||
# so we have to import servers anonymously
|
||||
if [ "${PGADMIN_CONFIG_SERVER_MODE}" = "False" ]; then
|
||||
$SU_EXEC /venv/bin/python3 /pgadmin4/setup.py load-servers "${PGADMIN_SERVER_JSON_FILE}" ${EXTRA_ARGS}
|
||||
if [[ $PGADMIN_CONFIG_SERVER_MODE == "False" ]]; then
|
||||
$SU_EXEC /venv/bin/python3 /pgadmin4/setup.py load-servers "$PGADMIN_SERVER_JSON_FILE" $EXTRA_ARGS
|
||||
else
|
||||
$SU_EXEC /venv/bin/python3 /pgadmin4/setup.py load-servers "${PGADMIN_SERVER_JSON_FILE}" --user "${PGADMIN_DEFAULT_EMAIL}" ${EXTRA_ARGS}
|
||||
$SU_EXEC /venv/bin/python3 /pgadmin4/setup.py load-servers "$PGADMIN_SERVER_JSON_FILE" --user "$PGADMIN_DEFAULT_EMAIL" $EXTRA_ARGS
|
||||
fi
|
||||
fi
|
||||
}
|
||||
|
||||
if [ ! -f /var/lib/pgadmin/pgadmin4.db ] && [ "${external_config_db_exists}" = "False" ]; then
|
||||
if [ -z "${PGADMIN_DEFAULT_EMAIL}" ] || { [ -z "${PGADMIN_DEFAULT_PASSWORD}" ] && [ -z "${PGADMIN_DEFAULT_PASSWORD_FILE}" ]; }; then
|
||||
if [[ ! -f /var/lib/pgadmin/pgadmin4.db && $external_config_db_exists == "False" ]]; then
|
||||
if [[ -z $PGADMIN_DEFAULT_EMAIL || (-z $PGADMIN_DEFAULT_PASSWORD && -z $PGADMIN_DEFAULT_PASSWORD_FILE) ]]; then
|
||||
echo 'You need to define the PGADMIN_DEFAULT_EMAIL and PGADMIN_DEFAULT_PASSWORD or PGADMIN_DEFAULT_PASSWORD_FILE environment variables.'
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Validate PGADMIN_DEFAULT_EMAIL
|
||||
CHECK_EMAIL_DELIVERABILITY="False"
|
||||
if [ -n "${PGADMIN_CONFIG_CHECK_EMAIL_DELIVERABILITY}" ]; then
|
||||
CHECK_EMAIL_DELIVERABILITY=${PGADMIN_CONFIG_CHECK_EMAIL_DELIVERABILITY}
|
||||
#
|
||||
# The settings and the email address are read inside Python via
|
||||
# os.environ rather than interpolated into the Python source. Boolean
|
||||
# settings are normalised in the same way as config_distro.py above, so
|
||||
# 'true' and 'false' in any case are accepted.
|
||||
is_valid_email=$(cd /pgadmin4/pgadmin/utils && $SU_EXEC /venv/bin/python3 -c "
|
||||
import ast, os, sys
|
||||
from validation_utils import validate_email
|
||||
|
||||
|
||||
def setting(name, default):
|
||||
raw = os.environ.get('PGADMIN_CONFIG_' + name, '')
|
||||
if not raw:
|
||||
return default
|
||||
if raw.lower() in ('true', 'false'):
|
||||
return raw.lower() == 'true'
|
||||
try:
|
||||
return ast.literal_eval(raw)
|
||||
except (ValueError, SyntaxError):
|
||||
return raw
|
||||
|
||||
|
||||
email_config = {
|
||||
'CHECK_EMAIL_DELIVERABILITY': setting('CHECK_EMAIL_DELIVERABILITY', False),
|
||||
'ALLOW_SPECIAL_EMAIL_DOMAINS': setting('ALLOW_SPECIAL_EMAIL_DOMAINS', []),
|
||||
'GLOBALLY_DELIVERABLE': setting('GLOBALLY_DELIVERABLE', True),
|
||||
}
|
||||
print(f'email config is {email_config}', file=sys.stderr)
|
||||
print(validate_email(os.environ['PGADMIN_DEFAULT_EMAIL'], email_config))
|
||||
") || exit 1
|
||||
if [[ $is_valid_email == *False* ]]; then
|
||||
echo "'$PGADMIN_DEFAULT_EMAIL' does not appear to be a valid email address. Please reset the PGADMIN_DEFAULT_EMAIL environment variable and try again."
|
||||
echo "Validation output: $is_valid_email"
|
||||
exit 1
|
||||
fi
|
||||
ALLOW_SPECIAL_EMAIL_DOMAINS="[]"
|
||||
if [ -n "${PGADMIN_CONFIG_ALLOW_SPECIAL_EMAIL_DOMAINS}" ]; then
|
||||
ALLOW_SPECIAL_EMAIL_DOMAINS=${PGADMIN_CONFIG_ALLOW_SPECIAL_EMAIL_DOMAINS}
|
||||
fi
|
||||
GLOBALLY_DELIVERABLE="True"
|
||||
if [ -n "${PGADMIN_CONFIG_GLOBALLY_DELIVERABLE}" ]; then
|
||||
GLOBALLY_DELIVERABLE=${PGADMIN_CONFIG_GLOBALLY_DELIVERABLE}
|
||||
fi
|
||||
email_config="{'CHECK_EMAIL_DELIVERABILITY': ${CHECK_EMAIL_DELIVERABILITY}, 'ALLOW_SPECIAL_EMAIL_DOMAINS': ${ALLOW_SPECIAL_EMAIL_DOMAINS}, 'GLOBALLY_DELIVERABLE': ${GLOBALLY_DELIVERABLE}}"
|
||||
echo "email config is ${email_config}"
|
||||
is_valid_email=$(cd /pgadmin4/pgadmin/utils && $SU_EXEC /venv/bin/python3 -c "from validation_utils import validate_email; val = validate_email('${PGADMIN_DEFAULT_EMAIL}', ${email_config}); print(val)")
|
||||
if echo "${is_valid_email}" | grep "False" > /dev/null; then
|
||||
echo "'${PGADMIN_DEFAULT_EMAIL}' does not appear to be a valid email address. Please reset the PGADMIN_DEFAULT_EMAIL environment variable and try again."
|
||||
echo "Validation output: ${is_valid_email}"
|
||||
exit 1
|
||||
fi
|
||||
# Switch back to root directory for further process
|
||||
cd /pgadmin4
|
||||
|
||||
# Set the default username and password in a
|
||||
# backwards compatible way
|
||||
export PGADMIN_SETUP_EMAIL="${PGADMIN_DEFAULT_EMAIL}"
|
||||
export PGADMIN_SETUP_PASSWORD="${PGADMIN_DEFAULT_PASSWORD}"
|
||||
export PGADMIN_SETUP_EMAIL="$PGADMIN_DEFAULT_EMAIL"
|
||||
export PGADMIN_SETUP_PASSWORD="$PGADMIN_DEFAULT_PASSWORD"
|
||||
|
||||
# Initialize DB before starting Gunicorn
|
||||
# Importing pgadmin4 (from this script) is enough
|
||||
@@ -285,41 +293,41 @@ if [ ! -f /var/lib/pgadmin/pgadmin4.db ] && [ "${external_config_db_exists}" = "
|
||||
load_server_json_file
|
||||
|
||||
# Pre-load any required preferences
|
||||
if [ -f "${PGADMIN_PREFERENCES_JSON_FILE}" ]; then
|
||||
if [ "${PGADMIN_CONFIG_SERVER_MODE}" = "False" ]; then
|
||||
if [[ -f $PGADMIN_PREFERENCES_JSON_FILE ]]; then
|
||||
if [[ $PGADMIN_CONFIG_SERVER_MODE == "False" ]]; then
|
||||
DESKTOP_USER=$(cd /pgadmin4 && $SU_EXEC /venv/bin/python3 -c 'import config; print(config.DESKTOP_USER)')
|
||||
$SU_EXEC /venv/bin/python3 /pgadmin4/setup.py set-prefs "${DESKTOP_USER}" --input-file "${PGADMIN_PREFERENCES_JSON_FILE}"
|
||||
$SU_EXEC /venv/bin/python3 /pgadmin4/setup.py set-prefs "$DESKTOP_USER" --input-file "$PGADMIN_PREFERENCES_JSON_FILE"
|
||||
else
|
||||
$SU_EXEC /venv/bin/python3 /pgadmin4/setup.py set-prefs "${PGADMIN_DEFAULT_EMAIL}" --input-file "${PGADMIN_PREFERENCES_JSON_FILE}"
|
||||
$SU_EXEC /venv/bin/python3 /pgadmin4/setup.py set-prefs "$PGADMIN_DEFAULT_EMAIL" --input-file "$PGADMIN_PREFERENCES_JSON_FILE"
|
||||
fi
|
||||
fi
|
||||
# Copy the pgpass file passed using secrets
|
||||
if [ -n "${PGPASS_FILE}" ] && [ -f "${PGPASS_FILE}" ]; then
|
||||
if [ "${PGADMIN_CONFIG_SERVER_MODE}" = "False" ]; then
|
||||
cp "${PGPASS_FILE}" /var/lib/pgadmin/.pgpass
|
||||
if [[ -n $PGPASS_FILE && -f $PGPASS_FILE ]]; then
|
||||
if [[ $PGADMIN_CONFIG_SERVER_MODE == "False" ]]; then
|
||||
cp "$PGPASS_FILE" /var/lib/pgadmin/.pgpass
|
||||
chmod 600 /var/lib/pgadmin/.pgpass
|
||||
# Fix ownership when running as root
|
||||
if [ "$(id -u)" = "0" ]; then
|
||||
if (( EUID == 0 )); then
|
||||
chown "$PUID:$PGID" /var/lib/pgadmin/.pgpass
|
||||
fi
|
||||
else
|
||||
PGADMIN_USER_CONFIG_DIR=$(echo "${PGADMIN_DEFAULT_EMAIL}" | sed 's/@/_/g')
|
||||
mkdir -p "/var/lib/pgadmin/storage/${PGADMIN_USER_CONFIG_DIR}"
|
||||
cp "${PGPASS_FILE}" "/var/lib/pgadmin/storage/${PGADMIN_USER_CONFIG_DIR}/.pgpass"
|
||||
chmod 600 "/var/lib/pgadmin/storage/${PGADMIN_USER_CONFIG_DIR}/.pgpass"
|
||||
PGADMIN_USER_CONFIG_DIR="${PGADMIN_DEFAULT_EMAIL//@/_}"
|
||||
mkdir -p "/var/lib/pgadmin/storage/$PGADMIN_USER_CONFIG_DIR"
|
||||
cp "$PGPASS_FILE" "/var/lib/pgadmin/storage/$PGADMIN_USER_CONFIG_DIR/.pgpass"
|
||||
chmod 600 "/var/lib/pgadmin/storage/$PGADMIN_USER_CONFIG_DIR/.pgpass"
|
||||
# Fix ownership when running as root
|
||||
if [ "$(id -u)" = "0" ]; then
|
||||
chown -R "$PUID:$PGID" "/var/lib/pgadmin/storage/${PGADMIN_USER_CONFIG_DIR}"
|
||||
if (( EUID == 0 )); then
|
||||
chown -R "$PUID:$PGID" "/var/lib/pgadmin/storage/$PGADMIN_USER_CONFIG_DIR"
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
# If already initialised and PGADMIN_REPLACE_SERVERS_ON_STARTUP is set to true, then load the server json file.
|
||||
elif [ "${PGADMIN_REPLACE_SERVERS_ON_STARTUP}" = "True" ]; then
|
||||
elif [[ $PGADMIN_REPLACE_SERVERS_ON_STARTUP == "True" ]]; then
|
||||
load_server_json_file
|
||||
fi
|
||||
|
||||
# Start Postfix to handle password resets etc.
|
||||
if [ -z "${PGADMIN_DISABLE_POSTFIX}" ]; then
|
||||
if [[ -z $PGADMIN_DISABLE_POSTFIX ]]; then
|
||||
sudo /usr/sbin/postfix start
|
||||
fi
|
||||
|
||||
@@ -330,18 +338,16 @@ TIMEOUT=$(cd /pgadmin4 && $SU_EXEC /venv/bin/python3 -c 'import config; print(co
|
||||
# NOTE: currently pgadmin can run only with 1 worker due to sessions implementation
|
||||
# Using --threads to have multi-threaded single-process worker
|
||||
|
||||
if [ -n "${PGADMIN_ENABLE_SOCK}" ]; then
|
||||
if [[ -n $PGADMIN_ENABLE_SOCK ]]; then
|
||||
BIND_ADDRESS="unix:/run/pgadmin/pgadmin.sock"
|
||||
elif [[ -n $PGADMIN_ENABLE_TLS ]]; then
|
||||
BIND_ADDRESS="${PGADMIN_LISTEN_ADDRESS:-[::]}:${PGADMIN_LISTEN_PORT:-443}"
|
||||
else
|
||||
if [ -n "${PGADMIN_ENABLE_TLS}" ]; then
|
||||
BIND_ADDRESS="${PGADMIN_LISTEN_ADDRESS:-[::]}:${PGADMIN_LISTEN_PORT:-443}"
|
||||
else
|
||||
BIND_ADDRESS="${PGADMIN_LISTEN_ADDRESS:-[::]}:${PGADMIN_LISTEN_PORT:-80}"
|
||||
fi
|
||||
BIND_ADDRESS="${PGADMIN_LISTEN_ADDRESS:-[::]}:${PGADMIN_LISTEN_PORT:-80}"
|
||||
fi
|
||||
|
||||
if [ -n "${PGADMIN_ENABLE_TLS}" ]; then
|
||||
exec $SU_EXEC "${PYTHON_BIN}" /venv/bin/gunicorn --limit-request-line "${GUNICORN_LIMIT_REQUEST_LINE:-8190}" --timeout "${TIMEOUT}" --bind "${BIND_ADDRESS}" -w 1 --threads "${GUNICORN_THREADS:-25}" --access-logfile "${GUNICORN_ACCESS_LOGFILE:--}" --keyfile /certs/server.key --certfile /certs/server.cert -c gunicorn_config.py run_pgadmin:app
|
||||
if [[ -n $PGADMIN_ENABLE_TLS ]]; then
|
||||
exec $SU_EXEC "$PYTHON_BIN" /venv/bin/gunicorn --limit-request-line "${GUNICORN_LIMIT_REQUEST_LINE:-8190}" --timeout "$TIMEOUT" --bind "$BIND_ADDRESS" -w 1 --threads "${GUNICORN_THREADS:-25}" --access-logfile "${GUNICORN_ACCESS_LOGFILE:--}" --keyfile /certs/server.key --certfile /certs/server.cert -c gunicorn_config.py run_pgadmin:app
|
||||
else
|
||||
exec $SU_EXEC "${PYTHON_BIN}" /venv/bin/gunicorn --limit-request-line "${GUNICORN_LIMIT_REQUEST_LINE:-8190}" --limit-request-fields "${GUNICORN_LIMIT_REQUEST_FIELDS:-100}" --limit-request-field_size "${GUNICORN_LIMIT_REQUEST_FIELD_SIZE:-8190}" --timeout "${TIMEOUT}" --bind "${BIND_ADDRESS}" -w 1 --threads "${GUNICORN_THREADS:-25}" --access-logfile "${GUNICORN_ACCESS_LOGFILE:--}" -c gunicorn_config.py run_pgadmin:app
|
||||
exec $SU_EXEC "$PYTHON_BIN" /venv/bin/gunicorn --limit-request-line "${GUNICORN_LIMIT_REQUEST_LINE:-8190}" --limit-request-fields "${GUNICORN_LIMIT_REQUEST_FIELDS:-100}" --limit-request-field_size "${GUNICORN_LIMIT_REQUEST_FIELD_SIZE:-8190}" --timeout "$TIMEOUT" --bind "$BIND_ADDRESS" -w 1 --threads "${GUNICORN_THREADS:-25}" --access-logfile "${GUNICORN_ACCESS_LOGFILE:--}" -c gunicorn_config.py run_pgadmin:app
|
||||
fi
|
||||
@@ -1,7 +1,7 @@
|
||||
apiVersion: v2
|
||||
name: pgadmin4-helm
|
||||
version: 9.18.0
|
||||
appVersion: 9.18.0
|
||||
version: 9.19.0
|
||||
appVersion: 9.19.0
|
||||
description: pgAdmin is the most popular and feature rich Open Source administration and development platform for PostgreSQL, the most advanced Open Source database in the world.
|
||||
home: https://pgadmin.org
|
||||
sources: ["https://github.com/pgadmin-org/pgadmin4/tree/master/helm"]
|
||||
|
||||
@@ -0,0 +1,174 @@
|
||||
# Adding and retiring Linux platforms
|
||||
|
||||
Debian, Ubuntu, Fedora and EL releases come and go several times a year, and
|
||||
each one is named in more places than is obvious: the build matrices, the
|
||||
staging and release workflows, the acceptance tests and the publishing wrapper
|
||||
on the servers all carry their own list. Missing one does not always fail
|
||||
loudly (a platform the wrapper does not know about is built, uploaded and then
|
||||
quietly left unindexed), so work through the whole of the relevant section
|
||||
below rather than stopping when CI goes green.
|
||||
|
||||
Architectures are a different change, and are not covered here; see the
|
||||
comments on the RPM tree in `.github/workflows/snapshot.yml`, `YUM_TARGETS` in
|
||||
`pkg/publish/pga-publish` and `Architectures` in `pkg/publish/aptftp.conf`.
|
||||
|
||||
## Where platforms are named
|
||||
|
||||
| File | What it holds | Naming |
|
||||
|---|---|---|
|
||||
| `.github/workflows/check-deb-build.yml` | The DEB build matrix, which the snapshot and release candidate workflows call | `debian-13`, `ubuntu-2604`, plus the container image |
|
||||
| `.github/workflows/check-rpm-build.yml` | The RPM build matrix, likewise | `el-10`, `fedora-44`, plus the image and the PGDG repository RPM URL |
|
||||
| `.github/workflows/snapshot.yml` | `codename_for()`, mapping DEB build names to codenames; the `case` mapping RPM build names to repository trees | `ubuntu-2604` to `resolute`; `el-10` to `redhat/rhel-10` |
|
||||
| `.github/workflows/release-candidate.yml` | The same two mappings, duplicated | as above |
|
||||
| `.github/workflows/acceptance-test.yml` | The install test matrices, which add the published repositories and install from them | `resolute`; `rhel-10`, `fedora-44` |
|
||||
| `.github/workflows/promote-release.yml` | The `rebuild-apt` and `rebuild-yum` calls in "Publish the packages" | `resolute`; `redhat rhel 10 x86_64` |
|
||||
| `pkg/publish/pga-publish` | `APT_CODENAMES` and `YUM_TARGETS`, which decide what the servers create, index and promote | as `promote-release.yml` |
|
||||
|
||||
These do not need changing for a new release of an existing distribution:
|
||||
|
||||
* The repository configuration RPMs (`pkg/redhat/repo-rpm.sh`), which use
|
||||
`$releasever` rather than naming versions.
|
||||
* The repository READMEs, whose platform tables `install-repo-readme.py`
|
||||
derives from the trees on disk and the archive bucket.
|
||||
* The DEB and RPM build scripts, unless the new release needs something
|
||||
different; see 'Version-specific code' below.
|
||||
|
||||
## Adding a platform
|
||||
|
||||
### Before starting
|
||||
|
||||
Check that the pieces the build depends on exist for the new release:
|
||||
|
||||
* The container image on Docker Hub (`ubuntu:26.04`, `fedora:45`,
|
||||
`almalinux:11`). New Ubuntu releases usually have a tag some weeks before
|
||||
release; building against a pre-release image is fine for proving the build,
|
||||
but do not publish packages from one.
|
||||
* For RPM platforms, the PGDG repository RPM
|
||||
(`https://download.postgresql.org/pub/repos/yum/reporpms/`), since the build
|
||||
takes `libpq5-devel` and `postgresql18-devel` from PGDG. On EL, EPEL must
|
||||
also exist for the new version. DEB builds use the distribution's own
|
||||
`libpq-dev` and have no equivalent dependency.
|
||||
* The codename, for DEB platforms: it is what `lsb_release -cs` prints and
|
||||
what the repository directory is called, so take it from the release itself
|
||||
rather than from the version number.
|
||||
|
||||
### In this repository
|
||||
|
||||
All of this goes in one pull request. The DEB and RPM checks do not run on
|
||||
pull requests, only on pushes to `master` and on dispatch, so prove the new
|
||||
matrix leg before merging by dispatching `check-deb-build.yml` or
|
||||
`check-rpm-build.yml` against the branch in a fork with Actions enabled
|
||||
(`gh workflow run check-deb-build.yml --ref <branch>`).
|
||||
|
||||
1. Add the matrix leg to `check-deb-build.yml` or `check-rpm-build.yml`,
|
||||
copying the neighbouring entry's form exactly. The `name` becomes part of
|
||||
the artifact name, and the mappings below parse it back out.
|
||||
2. Add the mapping for the new name to `snapshot.yml` and to
|
||||
`release-candidate.yml`. A DEB name with no `codename_for()` entry fails
|
||||
the assemble job with `Unknown Debian build name`; an RPM name that is not
|
||||
`el-*` or `fedora-*` fails with `Unknown RPM build name`.
|
||||
3. Add the platform to both matrices in `acceptance-test.yml`.
|
||||
4. Add the `rebuild-apt` codename, or the `rebuild-yum` line, to "Publish the
|
||||
packages" in `promote-release.yml`.
|
||||
5. Add the codename to `APT_CODENAMES`, or the tuple to `YUM_TARGETS`, in
|
||||
`pkg/publish/pga-publish`. The wrapper refuses to index anything not in
|
||||
these tables, and the snapshot and staging indexers only look for the trees
|
||||
they name, so without this the new packages are uploaded but never indexed,
|
||||
and `packages-fetch` never carries them into the live repository.
|
||||
|
||||
If the build needs a platform-specific change (a package renamed, a new
|
||||
AppArmor rule, a different Python), make it in the same pull request, so that
|
||||
the new matrix leg proves it.
|
||||
|
||||
### On the servers
|
||||
|
||||
`pga-publish` and the scripts beside it are installed by hand on both the
|
||||
staging and the download servers, as described under "Installing" in
|
||||
`pkg/publish/README.md`. Reinstall them on both once the pull request is
|
||||
merged, and before the next snapshot, since the snapshot and release candidate
|
||||
workflows build the new platform from then on and the servers will skip what
|
||||
their installed copy of the tables does not name.
|
||||
|
||||
### Proving it
|
||||
|
||||
1. Dispatch a publishing snapshot (`gh workflow run snapshot.yml -f
|
||||
publish=true`), and check that the new platform's tree in the snapshot has
|
||||
signed indices (`InRelease` for APT, `repodata/repomd.xml.asc` for YUM), and that
|
||||
the snapshot's README lists it.
|
||||
2. Install from the snapshot on the new platform, following that README.
|
||||
3. The first release candidate's acceptance test installs it from the staging
|
||||
repository, which is the check that matters before promotion.
|
||||
|
||||
### The first release that carries it
|
||||
|
||||
`rebuild-readme` builds the production README's history from the archive
|
||||
bucket, and exits with `<platform> is published but absent from the archive`
|
||||
when a platform in the live tree has never been copied there. Promotion runs
|
||||
`rebuild-readme` in "Publish the packages", before `sync-s3` copies the
|
||||
release to the archive in "Update the website", so the first promotion after
|
||||
adding a platform fails at that point, with the packages already live but the
|
||||
website not updated. Until that ordering is changed, expect it: run
|
||||
`pga-publish sync-s3` by hand on the download server, then rerun
|
||||
`pga-publish rebuild-readme` for the affected kind and finish the website
|
||||
steps.
|
||||
|
||||
### Elsewhere
|
||||
|
||||
The download pages on www.pgadmin.org list the supported platforms, and live
|
||||
outside this repository. Open an issue for the platform, too, so that the
|
||||
pre-release pass picks it up for the release notes, as with `Issue #9263`
|
||||
("Added Ubuntu 25.10 and Fedora 43") in `release_notes_9_11.rst`.
|
||||
|
||||
## Retiring a platform
|
||||
|
||||
A platform is retired when it stops receiving builds. Its repository keeps
|
||||
serving the last packages it had until somebody removes the tree, and the
|
||||
archive bucket keeps them after that, since `sync-s3` never deletes.
|
||||
|
||||
### In this repository
|
||||
|
||||
Reverse steps 1 to 5 of adding a platform, in a single pull request: the
|
||||
matrix leg, both mappings, both acceptance test entries, the promotion
|
||||
`rebuild-*` line and the wrapper table entry. Then remove any version-specific
|
||||
code that only existed for that platform (see below).
|
||||
|
||||
Keep the steps together. A platform removed from the wrapper tables but still
|
||||
in the build matrices is built and uploaded, then silently left out of the
|
||||
indices and the promotion.
|
||||
|
||||
Reinstall the wrapper on both servers once it is merged, as when adding one.
|
||||
|
||||
### On the download server
|
||||
|
||||
Nothing is removed automatically, and the production README will go on
|
||||
listing the platform as supported for as long as its tree holds packages,
|
||||
since it takes the supported list from the trees that exist. To move it to the
|
||||
"no longer supported" table:
|
||||
|
||||
1. Make sure the archive bucket has the platform's last release, which it will
|
||||
if `sync-s3` has run since that release was promoted.
|
||||
2. Remove the tree from the live repository: `apt/<codename>` for APT, or
|
||||
`yum/<family>/<name>-<version>-<arch>` and its `Server`, `Workstation` and
|
||||
`Client` compatibility links for YUM.
|
||||
3. Run `pga-publish rebuild-readme apt` or `yum`.
|
||||
|
||||
When to do this is a policy decision rather than a mechanical one: users who
|
||||
have not upgraded their distribution lose `apt update`/`dnf update` access to
|
||||
pgAdmin the moment the tree goes.
|
||||
|
||||
## Version-specific code
|
||||
|
||||
The build scripts branch on the distribution version in a few places. Check
|
||||
them when adding a platform, and delete the dead branches when retiring one:
|
||||
|
||||
* `pkg/debian/build.sh`: the AppArmor profile and `postinst` for Ubuntu 24 and
|
||||
later.
|
||||
* `pkg/linux/build-functions.sh`: the EL 8 Python selection, in `_setup_env`
|
||||
and `_copy_code`.
|
||||
* `pkg/redhat/setup.sh` and `pkg/debian/setup.sh`: prerequisites for building
|
||||
by hand. CI does not run these (the workflows install their own), so they
|
||||
are the likeliest to be out of date; `pkg/redhat/setup.sh`, for example,
|
||||
only knows EL 8 and 9.
|
||||
* The comments in `check-deb-build.yml` and `check-rpm-build.yml`, which name
|
||||
the releases their workarounds apply to (PEP 668, dnf5, the distribution's
|
||||
Node.js on Ubuntu 26.04).
|
||||
@@ -5,3 +5,6 @@ various Linux distributions.
|
||||
|
||||
You should not use them directly, but the functionality provided can be used to
|
||||
simplify your own build scripts (typically by sourcing *build-functions.sh*).
|
||||
|
||||
Adding or retiring a Linux platform touches more than the build scripts; see
|
||||
*PLATFORMS.md* for everywhere a platform is named.
|
||||
@@ -102,6 +102,16 @@ _create_python_virtualenv() {
|
||||
# file inside the venv's own site-packages (which we do own), added
|
||||
# once pip/wheel are installed below.
|
||||
"${SYSTEM_PYTHON_PATH}" -m venv venv
|
||||
|
||||
# Python 3.14's venv module adds a novelty "𝜋thon" alias (U+1D70B, see
|
||||
# CPython gh-119535) alongside python/python3/python3.X. Nothing uses it,
|
||||
# and the "sed -i" hash bang fixup below turns the symlink into a full
|
||||
# copy of the interpreter which, unlike the python* links, is never put
|
||||
# back. dpkg on Ubuntu 26.04 then fails to unpack it with EPERM (#10414),
|
||||
# so remove it. The name is given as raw UTF-8 bytes so that it matches
|
||||
# whatever the build shell's locale is.
|
||||
rm -f "venv/bin/"$'\360\235\234\213'"thon"
|
||||
|
||||
# shellcheck disable=SC1091
|
||||
. venv/bin/activate
|
||||
|
||||
@@ -137,6 +147,11 @@ _create_python_virtualenv() {
|
||||
--no-binary psycopg -r "${SOURCEDIR}/requirements.txt"
|
||||
fi
|
||||
|
||||
# Remove the files in the installed packages that we never use, and the
|
||||
# packaging tools (pip, setuptools and wheel) that are only needed to
|
||||
# build the environment
|
||||
python3 "${SOURCEDIR}/pkg/prune_site_packages.py" venv || exit 1
|
||||
|
||||
# Fixup the paths in the venv activation scripts
|
||||
sed -i 's/VIRTUAL_ENV=.*/VIRTUAL_ENV="\/usr\/pgadmin4\/venv"/g' venv/bin/activate
|
||||
sed -i 's/setenv VIRTUAL_ENV .*/setenv VIRTUAL_ENV "\/usr\/pgadmin4\/venv"/g' venv/bin/activate.csh
|
||||
@@ -202,14 +217,32 @@ _build_runtime() {
|
||||
ELECTRON_ARCH="arm64"
|
||||
fi
|
||||
|
||||
# Resolve the electron version from runtime/package.json, NOT from
|
||||
# `npm info electron version`. The latter fetches whatever currently
|
||||
# carries the `latest` dist-tag on the npm registry, which means any
|
||||
# newly published electron release lands in shipped binaries without
|
||||
# review. Keep the build deterministic and pinned.
|
||||
ELECTRON_VERSION=$(sed -nE 's/.*"electron":[[:space:]]*"\^?([0-9.]+)".*/\1/p' "${SOURCEDIR}/runtime/package.json" | head -1)
|
||||
# Resolve the electron version from runtime/yarn.lock, NOT from the npm
|
||||
# registry and NOT from the range in runtime/package.json. The registry's
|
||||
# `latest` dist-tag lands any newly published electron release in shipped
|
||||
# binaries without review, whilst the package.json range is only a lower
|
||||
# bound, so yarn is free to resolve it to a build other than the one we
|
||||
# ship. The lockfile is the single source of truth, and `yarn info` reports
|
||||
# its resolution without touching the network or needing node_modules.
|
||||
#
|
||||
# It must run inside runtime/ though: outside a yarn project, `yarn info`
|
||||
# silently falls back to querying the registry and still exits 0, so check
|
||||
# the lockfile is present first and validate what comes back.
|
||||
if [ ! -f "${SOURCEDIR}/runtime/yarn.lock" ]; then
|
||||
echo "ERROR: ${SOURCEDIR}/runtime/yarn.lock not found; cannot resolve the pinned electron version" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
ELECTRON_VERSION=$(cd "${SOURCEDIR}/runtime" && yarn info electron --json | node -e "
|
||||
const lines = require('fs').readFileSync(0, 'utf8').split('\n').filter(Boolean);
|
||||
const pkg = lines.map(l => { try { return JSON.parse(l); } catch (e) { return null; } })
|
||||
.find(o => o && typeof o.value === 'string' && o.value.startsWith('electron@npm:'));
|
||||
const version = (pkg && pkg.children && pkg.children.Version) || '';
|
||||
process.stdout.write(/^[0-9]+[.][0-9]+[.][0-9]+(-[0-9A-Za-z.-]+)?$/.test(version) ? version : '');
|
||||
")
|
||||
|
||||
if [ -z "${ELECTRON_VERSION}" ]; then
|
||||
echo "ERROR: could not resolve electron version from runtime/package.json" >&2
|
||||
echo "ERROR: could not resolve the pinned electron version from ${SOURCEDIR}/runtime/yarn.lock" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
@@ -304,7 +337,7 @@ _copy_code() {
|
||||
exit 1
|
||||
fi
|
||||
yarn set version "${YARN_VERSION}"
|
||||
yarn install
|
||||
yarn install --immutable
|
||||
yarn run bundle
|
||||
popd > /dev/null || exit
|
||||
|
||||
|
||||
@@ -12,6 +12,8 @@
|
||||
<string>%APPNAME%</string>
|
||||
<key>CFBundleIconFile</key>
|
||||
<string>app.icns</string>
|
||||
<key>CFBundleIconName</key>
|
||||
<string>pgAdmin4</string>
|
||||
<key>CFBundleIdentifier</key>
|
||||
<string>%APPID%</string>
|
||||
<key>CFBundleInfoDictionaryVersion</key>
|
||||
|
||||
@@ -6,17 +6,96 @@ Either build the sources or get them from macports or similar:
|
||||
|
||||
1. Yarn & NodeJS
|
||||
|
||||
2. PostgreSQL 12 or above from http://www.postgresql.org/
|
||||
2. PostgreSQL 12 or above from http://www.postgresql.org/, or the pre-built
|
||||
dependencies from the
|
||||
[pgbuild project](https://github.com/pgadmin-org/pgbuild), which is what the
|
||||
release builds use. Five are needed, openssl, krb5, zstd, lz4 and
|
||||
postgresql-18, each published as a release of its own:
|
||||
|
||||
https://github.com/pgadmin-org/pgbuild/releases/download/<tag>/<tag>.tar.gz
|
||||
|
||||
where <tag> is the release tag named in *pkg/pgbuild-deps.lock*, on the
|
||||
*<pkg>-macos-<arch>* line, and <arch> is arm64 or x86_64. Use those rather
|
||||
than the rolling *-latest* tags, so that what you build locally is what CI
|
||||
builds and what we ship. They unpack into subdirectories of a common prefix,
|
||||
/opt/pgbuild in CI, which is then given to the build as
|
||||
*PGADMIN_POSTGRES_DIR=/opt/pgbuild/postgresql*.
|
||||
|
||||
These matter beyond convenience: they are built against each other with
|
||||
*--with-zstd* and *--with-lz4* and a consistent deployment target, which
|
||||
Homebrew's PostgreSQL is not. The build checks in *.github/workflows* fetch
|
||||
exactly these through the *install-pgbuild-deps* action, so a local build
|
||||
and a CI build work from the same binaries.
|
||||
|
||||
3. Python 3.9+ (required for running the build; the version that gets bundled
|
||||
is a separate thing, see below). The build environment should run this
|
||||
version of python in response to the *python* command.
|
||||
|
||||
4. syft, to generate the software bill of materials, and wget, which fetches
|
||||
Electron:
|
||||
|
||||
brew install syft wget
|
||||
|
||||
|
||||
## Pinned dependencies
|
||||
|
||||
*pkg/pgbuild-deps.lock* records, for every dependency the
|
||||
*install-pgbuild-deps* action can fetch on either platform, the pgbuild
|
||||
release tag to download and the SHA-256 the archive must hash to. The action
|
||||
checks each download against it and refuses to unpack anything that does not
|
||||
match, which matters because a macOS appbundle is Developer ID signed and
|
||||
notarised and a Windows installer Authenticode signed, so without the lock
|
||||
write access to pgbuild would be write access to what pgAdmin ships, and two
|
||||
builds of the same commit would not contain the same binaries.
|
||||
|
||||
When pgbuild publishes a new build of something, the action says so as a
|
||||
warning and carries on with the pinned version, since a dependency that
|
||||
appeared overnight is not a reason to fail a build. To take the new one, run
|
||||
the refresh tool, naming the package so that nothing else is disturbed, then
|
||||
read the diff and commit it:
|
||||
|
||||
tools/refresh_pgbuild_lock.py openssl
|
||||
|
||||
That diff, one line per dependency with the version visible in the tag, is
|
||||
the record of a change to what we sign, and is the reason to bump
|
||||
dependencies through the tool rather than by editing the lock by hand.
|
||||
|
||||
A checksum mismatch fails the build, and the message names the archive, the
|
||||
expected digest and the one that arrived. Most such tags are per-version and
|
||||
so never change, the exception being PostgreSQL, whose tag carries only the
|
||||
major and is therefore rebuilt in place on every minor release. Satisfy
|
||||
yourself that a rebuild is what happened before re-running, since this is also
|
||||
what would catch a tampered dependency on its way into a signed build. An
|
||||
archive with no entry in the lock fails the same way, rather than being
|
||||
unpacked unverified.
|
||||
|
||||
pgbuild publishes a *.sha256* beside each archive. That is a convenience for
|
||||
people, not a source of trust, since whoever rebuilds an archive rebuilds its
|
||||
checksum too, so the action verifies against the lock instead.
|
||||
|
||||
|
||||
## Application icon
|
||||
|
||||
*pgAdmin4.icon* is an Icon Composer document holding the layered icon that
|
||||
macOS 26 and later use for the light, dark, clear and tinted appearances.
|
||||
It is compiled ahead of time into *Assets.car*, which is committed so that the
|
||||
build does not need Xcode 26; *pgAdmin4.icns* remains the icon for older
|
||||
releases. After editing *pgAdmin4.icon*, regenerate *Assets.car* from this
|
||||
directory with:
|
||||
|
||||
mkdir -p /tmp/pgadmin-icon
|
||||
xcrun actool pgAdmin4.icon --compile /tmp/pgadmin-icon \
|
||||
--platform macosx --minimum-deployment-target 10.13 \
|
||||
--app-icon pgAdmin4 --output-partial-info-plist /tmp/pgadmin-icon/partial.plist
|
||||
cp /tmp/pgadmin-icon/Assets.car .
|
||||
|
||||
|
||||
3. Python 3.6+ (required for building). The build environment should run this
|
||||
version of python in response to the *python* command.
|
||||
|
||||
## Building
|
||||
|
||||
1. To bundle a different version of Python from the default of 3.14.7, set the
|
||||
*PGADMIN_PYTHON_VERSION* environment variable, e.g:
|
||||
|
||||
export PGADMIN_PYTHON_VERSION=3.13.11
|
||||
1. The version of Python that gets bundled is read from
|
||||
*pkg/python-version.txt*, which the Windows packaging reads as well so that
|
||||
one commit cannot ship different interpreters on the two platforms that
|
||||
bundle one. To bundle a different version, edit that file.
|
||||
|
||||
2. If a path different from the default of /usr/local/pgsql for the PostgreSQL
|
||||
installation has been used, set the *PGADMIN_POSTGRES_DIR* environment variable
|
||||
|
||||
@@ -33,14 +33,32 @@ _build_runtime() {
|
||||
|
||||
test -d "${BUILD_ROOT}" || mkdir "${BUILD_ROOT}"
|
||||
# Get a fresh copy of electron
|
||||
# Resolve the electron version from runtime/package.json, NOT from
|
||||
# `npm info electron version`. The latter fetches whatever currently
|
||||
# carries the `latest` dist-tag on the npm registry, which means any
|
||||
# newly published electron release lands in shipped binaries without
|
||||
# review. Keep the build deterministic and pinned.
|
||||
ELECTRON_VERSION=$(sed -nE 's/.*"electron":[[:space:]]*"\^?([0-9.]+)".*/\1/p' "${SOURCE_DIR}/runtime/package.json" | head -1)
|
||||
# Resolve the electron version from runtime/yarn.lock, NOT from the npm
|
||||
# registry and NOT from the range in runtime/package.json. The registry's
|
||||
# `latest` dist-tag lands any newly published electron release in shipped
|
||||
# binaries without review, whilst the package.json range is only a lower
|
||||
# bound, so yarn is free to resolve it to a build other than the one we
|
||||
# ship. The lockfile is the single source of truth, and `yarn info` reports
|
||||
# its resolution without touching the network or needing node_modules.
|
||||
#
|
||||
# It must run inside runtime/ though: outside a yarn project, `yarn info`
|
||||
# silently falls back to querying the registry and still exits 0, so check
|
||||
# the lockfile is present first and validate what comes back.
|
||||
if [ ! -f "${SOURCE_DIR}/runtime/yarn.lock" ]; then
|
||||
echo "ERROR: ${SOURCE_DIR}/runtime/yarn.lock not found; cannot resolve the pinned electron version" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
ELECTRON_VERSION=$(cd "${SOURCE_DIR}/runtime" && yarn info electron --json | node -e "
|
||||
const lines = require('fs').readFileSync(0, 'utf8').split('\n').filter(Boolean);
|
||||
const pkg = lines.map(l => { try { return JSON.parse(l); } catch (e) { return null; } })
|
||||
.find(o => o && typeof o.value === 'string' && o.value.startsWith('electron@npm:'));
|
||||
const version = (pkg && pkg.children && pkg.children.Version) || '';
|
||||
process.stdout.write(/^[0-9]+[.][0-9]+[.][0-9]+(-[0-9A-Za-z.-]+)?$/.test(version) ? version : '');
|
||||
")
|
||||
|
||||
if [ -z "${ELECTRON_VERSION}" ]; then
|
||||
echo "ERROR: could not resolve electron version from runtime/package.json" >&2
|
||||
echo "ERROR: could not resolve the pinned electron version from ${SOURCE_DIR}/runtime/yarn.lock" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
@@ -105,6 +123,11 @@ _create_python_env() {
|
||||
"${BUNDLE_DIR}/Contents/Frameworks/Python.framework/Versions/Current/bin/python3" -m ensurepip --upgrade || exit 1
|
||||
"${BUNDLE_DIR}/Contents/Frameworks/Python.framework/Versions/Current/bin/pip3" install --no-cache-dir -r "${SOURCE_DIR}/requirements.txt" || exit 1
|
||||
|
||||
# Remove the files in the installed packages that we never use, and the
|
||||
# packaging tools (pip, setuptools and wheel) that are only needed to
|
||||
# build the environment
|
||||
"${BUNDLE_DIR}/Contents/Frameworks/Python.framework/Versions/Current/bin/python3" "${SOURCE_DIR}/pkg/prune_site_packages.py" "${BUNDLE_DIR}/Contents/Frameworks/Python.framework" || exit 1
|
||||
|
||||
# Make sure all the .so's in the Python env have the executable bit set
|
||||
# so they get properly signed later
|
||||
OLD_IFS=${IFS}
|
||||
@@ -145,13 +168,24 @@ _create_python_env() {
|
||||
|
||||
_build_docs() {
|
||||
echo "Building the docs..."
|
||||
# Create a temporary venv for the doc build, so we don't contaminate the one
|
||||
# that we're going to ship.
|
||||
"${BUNDLE_DIR}/Contents/Frameworks/Python.framework/Versions/Current/bin/python3" -m venv "${BUILD_ROOT}/venv"
|
||||
# The doc build imports pgAdmin itself: docs/en_US/build_code_snippet.py
|
||||
# pulls in config, pgadmin.utils, pgadmin.browser.utils and the driver
|
||||
# abstractions, so the whole of requirements.txt has to be importable. It
|
||||
# already is, in the framework we just populated, so the venv inherits
|
||||
# those rather than installing all hundred-odd packages a second time.
|
||||
# Sphinx still goes in the venv rather than the framework, which is the
|
||||
# point of having one: it must not end up in the shipped bundle.
|
||||
#
|
||||
# --system-site-packages is safe here in a way it is not in the Linux
|
||||
# build, which deliberately avoids it (see the long comment in
|
||||
# pkg/linux/build-functions.sh). There the parent is a shared system Python
|
||||
# that may carry other packages' stale .pth files; here it is a framework
|
||||
# we built ourselves moments ago and which contains only our dependencies.
|
||||
"${BUNDLE_DIR}/Contents/Frameworks/Python.framework/Versions/Current/bin/python3" \
|
||||
-m venv --system-site-packages "${BUILD_ROOT}/venv"
|
||||
# shellcheck disable=SC1091
|
||||
source "${BUILD_ROOT}/venv/bin/activate"
|
||||
pip3 install --upgrade pip
|
||||
pip3 install --no-cache-dir -r "${SOURCE_DIR}/requirements.txt"
|
||||
pip3 install sphinx==7.4.7
|
||||
pip3 install sphinxcontrib-youtube
|
||||
|
||||
@@ -288,8 +322,11 @@ _complete_bundle() {
|
||||
# PkgInfo
|
||||
echo APPLPGA4 > "${BUNDLE_DIR}/Contents/PkgInfo"
|
||||
|
||||
# Icon
|
||||
# Icon. Assets.car is compiled from pgAdmin4.icon (see README.md) and
|
||||
# gives macOS 26+ the dark, clear and tinted variants; older releases
|
||||
# fall back to app.icns.
|
||||
cp pgAdmin4.icns "${BUNDLE_DIR}/Contents/Resources/app.icns"
|
||||
cp Assets.car "${BUNDLE_DIR}/Contents/Resources/Assets.car"
|
||||
|
||||
# Rename the executable
|
||||
mv "${BUNDLE_DIR}/Contents/MacOS/Electron" "${BUNDLE_DIR}/Contents/MacOS/${APP_NAME}"
|
||||
@@ -308,7 +345,7 @@ _complete_bundle() {
|
||||
exit 1
|
||||
fi
|
||||
yarn set version "${YARN_VERSION}"
|
||||
yarn install 2>&1
|
||||
yarn install --immutable 2>&1
|
||||
|
||||
# Record the source commit hash before the heavy lint/webpack
|
||||
# steps. `yarn run` needs node_modules so this runs after install,
|
||||
@@ -497,7 +534,11 @@ _verify_bundle_linkage() {
|
||||
# Build-host prefixes that must never appear in a shipped bundle. SLAVE_HOME
|
||||
# (the Jenkins workspace root, under which the self-built OpenSSL and
|
||||
# PostgreSQL live) is only added when set, i.e. on the CI builders.
|
||||
local PREFIXES='/usr/local|/opt/homebrew|/opt/local'
|
||||
# /opt/pgbuild is where the PostgreSQL, OpenSSL, Kerberos, zstd and lz4
|
||||
# builds from pgadmin-org/pgbuild are unpacked. Their install names are
|
||||
# absolute, so an unrewritten reference to one is exactly the kind of
|
||||
# build-host path this check exists to catch, no different from Homebrew's.
|
||||
local PREFIXES='/usr/local|/opt/homebrew|/opt/local|/opt/pgbuild'
|
||||
if [ -n "${SLAVE_HOME}" ]; then
|
||||
PREFIXES="${PREFIXES}|${SLAVE_HOME}"
|
||||
fi
|
||||
@@ -535,15 +576,32 @@ _generate_sbom() {
|
||||
syft "${BUNDLE_DIR}/Contents/" -o cyclonedx-json > "${BUNDLE_DIR}/Contents/sbom.json"
|
||||
}
|
||||
|
||||
_codesign_binaries() {
|
||||
_set_codesign_args() {
|
||||
# Populates CODESIGN_ARGS with the arguments shared by every codesign call.
|
||||
#
|
||||
# Without a Developer ID we sign ad-hoc rather than not signing at all.
|
||||
# That is emphatically not a substitute for a real signature: an ad-hoc
|
||||
# signed bundle is still refused by Gatekeeper and cannot be notarised. It
|
||||
# exists because _fixup_imports has just rewritten install names across
|
||||
# several hundred binaries, which invalidates whatever signatures they
|
||||
# arrived with, Electron's included. On Apple Silicon the kernel refuses to
|
||||
# execute a binary whose signature is broken or absent, so without this an
|
||||
# unsigned bundle cannot be launched at all on arm64, even once the
|
||||
# quarantine attribute has been removed. On Intel it merely leaves every
|
||||
# binary in the bundle carrying a signature that no longer matches its
|
||||
# contents.
|
||||
if [ "${CODESIGN}" -eq 0 ]; then
|
||||
echo "Signing ad-hoc: the result will run locally once allowed, but"
|
||||
echo "cannot be distributed. Provide pkg/mac/codesign.conf to sign"
|
||||
echo "properly."
|
||||
# --deep for the same reason the Developer ID path uses it: these are
|
||||
# bundles with nested code, and without it codesign refuses to replace
|
||||
# the signature on one whose nested code is unsigned, reporting "code
|
||||
# object is not signed at all" against a subcomponent.
|
||||
CODESIGN_ARGS=(--deep --force --sign -)
|
||||
return
|
||||
fi
|
||||
|
||||
echo "Purging build-machine pollution (pycache) before signing..."
|
||||
find "${BUNDLE_DIR}" -name "__pycache__" -type d -exec rm -rf {} +
|
||||
find "${BUNDLE_DIR}" -name "*.pyc" -delete
|
||||
|
||||
if [ -z "${DEVELOPER_ID}" ] ; then
|
||||
echo "Developer ID Application not found in codesign.conf" >&2
|
||||
exit 1
|
||||
@@ -554,6 +612,20 @@ _codesign_binaries() {
|
||||
TEAM_ID=$(echo "${DEVELOPER_ID}" | awk -F"[()]" '{print $2}')
|
||||
sed -i '' "s/%TEAMID%/${TEAM_ID}/g" "${BUILD_ROOT}/entitlements.plist"
|
||||
|
||||
CODESIGN_ARGS=(--deep --force --verify --verbose --timestamp
|
||||
--options runtime
|
||||
--entitlements "${BUILD_ROOT}/entitlements.plist"
|
||||
-i org.pgadmin.pgadmin4
|
||||
--sign "${DEVELOPER_ID}")
|
||||
}
|
||||
|
||||
_codesign_binaries() {
|
||||
echo "Purging build-machine pollution (pycache) before signing..."
|
||||
find "${BUNDLE_DIR}" -name "__pycache__" -type d -exec rm -rf {} +
|
||||
find "${BUNDLE_DIR}" -name "*.pyc" -delete
|
||||
|
||||
_set_codesign_args
|
||||
|
||||
echo Signing "${BUNDLE_DIR}" binaries...
|
||||
IFS=$'\n'
|
||||
for i in $(find "${BUNDLE_DIR}" -type f -perm +111 -exec file "{}" \; | \
|
||||
@@ -563,37 +635,21 @@ _codesign_binaries() {
|
||||
awk -F":" '{print $1}' | \
|
||||
uniq)
|
||||
do
|
||||
codesign --deep --force --verify --verbose --timestamp \
|
||||
--options runtime \
|
||||
--entitlements "${BUILD_ROOT}/entitlements.plist" \
|
||||
-i org.pgadmin.pgadmin4 \
|
||||
--sign "${DEVELOPER_ID}" \
|
||||
"$i"
|
||||
codesign "${CODESIGN_ARGS[@]}" "$i"
|
||||
done
|
||||
unset IFS
|
||||
|
||||
echo Signing "${BUNDLE_DIR}" libraries...
|
||||
find "${BUNDLE_DIR}" -type f -name "*.dylib*" -exec \
|
||||
codesign --deep --force --verify --verbose --timestamp \
|
||||
--options runtime \
|
||||
--entitlements "${BUILD_ROOT}/entitlements.plist" \
|
||||
-i org.pgadmin.pgadmin4 \
|
||||
--sign "${DEVELOPER_ID}" \
|
||||
{} \;
|
||||
while IFS= read -r lib; do
|
||||
codesign "${CODESIGN_ARGS[@]}" "${lib}"
|
||||
done < <(find "${BUNDLE_DIR}" -type f -name "*.dylib*")
|
||||
}
|
||||
|
||||
_codesign_bundle() {
|
||||
if [ "${CODESIGN}" -eq 0 ]; then
|
||||
return
|
||||
fi
|
||||
|
||||
# Sign the .app
|
||||
# CODESIGN_ARGS is set by _codesign_binaries, which always runs first and
|
||||
# signs ad-hoc when there is no Developer ID; see _set_codesign_args.
|
||||
echo Signing "${BUNDLE_DIR}"...
|
||||
codesign --deep --force --verify --verbose --timestamp \
|
||||
--options runtime \
|
||||
--entitlements "${BUILD_ROOT}/entitlements.plist" \
|
||||
-i org.pgadmin.pgadmin4 \
|
||||
--sign "${DEVELOPER_ID}" \
|
||||
"${BUNDLE_DIR}"
|
||||
codesign "${CODESIGN_ARGS[@]}" "${BUNDLE_DIR}"
|
||||
|
||||
echo "Verifying the signature from bundle dir..."
|
||||
codesign --verify --deep --verbose=4 "${BUNDLE_DIR}"
|
||||
@@ -614,32 +670,116 @@ _create_zip() {
|
||||
echo "Successfully created ZIP file: ${ZIP_NAME}"
|
||||
}
|
||||
|
||||
_attach_dmg_eula() {
|
||||
# The licence shown when the image is opened is a resource attached to the
|
||||
# image, not a file inside it. Lifted from create-dmg, which in turn took
|
||||
# the udifrez approach from https://developer.apple.com/forums/thread/668084
|
||||
# after the older flatten/rez/unflatten route stopped working. The template
|
||||
# is vendored alongside this script rather than cloned, so the build does
|
||||
# not depend on another repository for one XML file.
|
||||
echo "Attaching the licence agreement..."
|
||||
|
||||
local template="${SCRIPT_DIR}/eula-resources-template.xml"
|
||||
local plist="${TEMP_DIR}/eula-resources.xml"
|
||||
local licence="${SCRIPT_DIR}/licence.rtf"
|
||||
local format
|
||||
|
||||
test -d "${TEMP_DIR}" || mkdir -p "${TEMP_DIR}"
|
||||
|
||||
case "$(file -b "${licence}")" in
|
||||
'Rich Text Format data'*) format='RTF ' ;;
|
||||
*) format='TEXT' ;;
|
||||
esac
|
||||
|
||||
# The payload goes into a plist <data> element, so it is base64 wrapped at
|
||||
# 52 columns and indented to sit inside the element.
|
||||
EULA_FORMAT="${format}" \
|
||||
EULA_DATA="$(openssl base64 -in "${licence}" | tr -d '\n' \
|
||||
| awk '{gsub(/.{52}/, "&\n")}1' | sed $'s/^\\(.*\\)$/\t\t\t\\1/')" \
|
||||
python3 -c 'import os, sys
|
||||
tpl = open(sys.argv[1]).read()
|
||||
tpl = tpl.replace("${EULA_FORMAT}", os.environ["EULA_FORMAT"])
|
||||
tpl = tpl.replace("${EULA_DATA}", os.environ["EULA_DATA"])
|
||||
open(sys.argv[2], "w").write(tpl)' "${template}" "${plist}"
|
||||
|
||||
hdiutil udifrez -xml "${plist}" '' -quiet "${DMG_NAME}" || {
|
||||
echo 'ERROR: Failed to attach the licence agreement.'
|
||||
exit 1
|
||||
}
|
||||
}
|
||||
|
||||
_create_dmg() {
|
||||
# move to the directory where we want to create the DMG
|
||||
# Build the image straight from a staged directory, rather than mounting a
|
||||
# read/write volume, populating it and unmounting again.
|
||||
#
|
||||
# The unmount was the problem. hdiutil waits on DiskArbitration with a
|
||||
# fixed 120 second timeout that nothing exposes a way to raise, and the
|
||||
# cost of unmounting scales with the bundle: measured on GitHub's hosted
|
||||
# runners with an identical payload it takes about 20 seconds on arm64, 41
|
||||
# on macos-26-intel and 90 on macos-15-intel, and with the real bundle it
|
||||
# exceeds 120 on both Intel images every time. `hdiutil detach -force` is
|
||||
# no help, contrary to how it is usually described: it waits on
|
||||
# DiskArbitration too, times out identically, and the conversion then fails
|
||||
# with "Resource temporarily unavailable" because the image is still
|
||||
# attached. `hdiutil create -srcfolder` never mounts anything, so none of
|
||||
# that can arise.
|
||||
#
|
||||
# This also replaces create-dmg, which was cloned unpinned from its default
|
||||
# branch on every build and run against the signed bundle. Everything it
|
||||
# did for us was staging, because we passed --skip-jenkins and so none of
|
||||
# its Finder automation ran: the volume name, the Applications symlink, the
|
||||
# background directory, the pre-built .DS_Store carrying the window and
|
||||
# icon layout, and the hidden bundle extension. The one part that is not a
|
||||
# file in the volume is the licence agreement, attached afterwards.
|
||||
test -d "${DIST_ROOT}" || mkdir "${DIST_ROOT}"
|
||||
|
||||
echo "Checking out create-dmg..."
|
||||
git clone https://github.com/create-dmg/create-dmg.git "${BUILD_ROOT}/create-dmg"
|
||||
local stage="${BUILD_ROOT}/dmg-staging"
|
||||
|
||||
"${BUILD_ROOT}/create-dmg/create-dmg" \
|
||||
--volname "${APP_NAME}" \
|
||||
--volicon "${SCRIPT_DIR}/dmg-icon.icns" \
|
||||
--eula "${SCRIPT_DIR}/licence.rtf" \
|
||||
--background "${SCRIPT_DIR}/dmg-background.png" \
|
||||
--app-drop-link 600 220 \
|
||||
--icon "${APP_NAME}.app" 200 220 \
|
||||
--window-pos 200 120 \
|
||||
--window-size 800 400 \
|
||||
--hide-extension "${APP_NAME}.app" \
|
||||
--add-file .DS_Store "${SCRIPT_DIR}/dmg.DS_Store" 5 5 \
|
||||
--format UDBZ \
|
||||
--skip-jenkins \
|
||||
--no-internet-enable \
|
||||
"${DMG_NAME}" \
|
||||
"${BUNDLE_DIR}"
|
||||
echo "Staging the disk image contents..."
|
||||
rm -rf "${stage}"
|
||||
mkdir -p "${stage}/.background"
|
||||
|
||||
# -c clones rather than copies on APFS, which makes staging a bundle of
|
||||
# this size effectively free; fall back for any filesystem that cannot.
|
||||
cp -Rc "${BUNDLE_DIR}" "${stage}/" 2>/dev/null || cp -R "${BUNDLE_DIR}" "${stage}/"
|
||||
cp "${SCRIPT_DIR}/dmg-background.png" "${stage}/.background/"
|
||||
cp "${SCRIPT_DIR}/dmg.DS_Store" "${stage}/.DS_Store"
|
||||
cp "${SCRIPT_DIR}/dmg-icon.icns" "${stage}/.VolumeIcon.icns"
|
||||
|
||||
# hdiutil preserves this as a symlink rather than following it, so the
|
||||
# volume gets the usual drag-to-install target without carrying a copy of
|
||||
# /Applications.
|
||||
ln -s /Applications "${stage}/Applications"
|
||||
|
||||
# C marks the volume root as having a custom icon, so .VolumeIcon.icns is
|
||||
# used; E hides the bundle's extension in Finder. These are what create-dmg
|
||||
# set via --volicon and --hide-extension.
|
||||
SetFile -a C "${stage}" || echo "WARNING: could not set the custom icon attribute"
|
||||
SetFile -a E "${stage}/${APP_NAME}.app" || echo "WARNING: could not hide the bundle extension"
|
||||
|
||||
echo "Creating the disk image..."
|
||||
rm -f "${DMG_NAME}"
|
||||
hdiutil create \
|
||||
-srcfolder "${stage}" \
|
||||
-volname "${APP_NAME}" \
|
||||
-fs HFS+ \
|
||||
-format UDBZ \
|
||||
-ov \
|
||||
"${DMG_NAME}" || { echo 'ERROR: Failed to create the disk image.'; exit 1; }
|
||||
|
||||
_attach_dmg_eula
|
||||
|
||||
hdiutil verify "${DMG_NAME}" || { echo 'ERROR: The disk image did not verify.'; exit 1; }
|
||||
|
||||
rm -rf "${stage}"
|
||||
}
|
||||
|
||||
_codesign_dmg() {
|
||||
# Unlike the bundle, there is no point signing the image ad-hoc: the
|
||||
# signature on a disk image is about distribution, and an ad-hoc one
|
||||
# satisfies nothing that a missing one does not. What matters for running
|
||||
# the build locally is that the app inside it is signed, which
|
||||
# _codesign_binaries and _codesign_bundle have already seen to.
|
||||
if [ "${CODESIGN}" -eq 0 ]; then
|
||||
return
|
||||
fi
|
||||
@@ -653,6 +793,74 @@ _codesign_dmg() {
|
||||
"${DMG_NAME}"
|
||||
}
|
||||
|
||||
# The submission's status, or an empty string if Apple could not be reached.
|
||||
# Separate from the waiting below because both need it and because a failure
|
||||
# to ask is not the same thing as an answer.
|
||||
_notarize_status() {
|
||||
local SUBMISSION_ID="$1"
|
||||
|
||||
xcrun notarytool info "${SUBMISSION_ID}" \
|
||||
--team-id "${DEVELOPER_TEAM_ID}" \
|
||||
--apple-id "${DEVELOPER_USER}" \
|
||||
--password "${DEVELOPER_ASP}" 2>&1 | \
|
||||
awk -F ': ' '/status:/ { print $2; }'
|
||||
}
|
||||
|
||||
# Wait for a submission Apple has already accepted for processing.
|
||||
#
|
||||
# notarytool wait polls Apple for as long as the notarisation takes, which is
|
||||
# minutes at best and has been much longer, and it exits non-zero if any one of
|
||||
# those polls fails. A build farm runner losing its route for a few seconds
|
||||
# therefore failed a notarisation that was proceeding perfectly well on Apple's
|
||||
# side, which is what cost the 22nd September snapshot: the DMG had been
|
||||
# submitted, the ID was in hand, and the wait died with "The Internet
|
||||
# connection appears to be offline" twenty six minutes in.
|
||||
#
|
||||
# So a failed wait is retried rather than being taken as the answer. The
|
||||
# submission is not resubmitted, since it is still queued under the same ID and
|
||||
# a second copy would only be a second thing to wait for. Each failure asks for
|
||||
# the status directly, because notarytool wait also exits non-zero when the
|
||||
# submission genuinely finishes as Invalid or Rejected, and retrying that would
|
||||
# be waiting for an answer already given: any terminal status ends the loop and
|
||||
# leaves the caller's own check to decide what it means.
|
||||
_notarize_wait() {
|
||||
local SUBMISSION_ID="$1"
|
||||
local ATTEMPT=1
|
||||
local ATTEMPTS=10
|
||||
local DELAY=60
|
||||
local STATUS
|
||||
|
||||
while [ "${ATTEMPT}" -le "${ATTEMPTS}" ]; do
|
||||
if xcrun notarytool wait "${SUBMISSION_ID}" \
|
||||
--team-id "${DEVELOPER_TEAM_ID}" \
|
||||
--apple-id "${DEVELOPER_USER}" \
|
||||
--password "${DEVELOPER_ASP}"; then
|
||||
return 0
|
||||
fi
|
||||
|
||||
STATUS=$(_notarize_status "${SUBMISSION_ID}")
|
||||
case "${STATUS}" in
|
||||
Accepted|Invalid|Rejected)
|
||||
echo "Notarization finished whilst waiting, with status: ${STATUS}"
|
||||
return 0
|
||||
;;
|
||||
esac
|
||||
|
||||
if [ "${ATTEMPT}" -eq "${ATTEMPTS}" ]; then
|
||||
break
|
||||
fi
|
||||
|
||||
echo "Could not wait on submission ${SUBMISSION_ID} (attempt ${ATTEMPT} of ${ATTEMPTS})."
|
||||
echo "The submission is still with Apple; retrying in ${DELAY} seconds."
|
||||
sleep "${DELAY}"
|
||||
ATTEMPT=$((ATTEMPT + 1))
|
||||
done
|
||||
|
||||
echo "Gave up waiting for submission ${SUBMISSION_ID} after ${ATTEMPTS} attempts."
|
||||
echo "Check it by hand with: xcrun notarytool info ${SUBMISSION_ID} ..."
|
||||
return 1
|
||||
}
|
||||
|
||||
_notarize_pkg() {
|
||||
local FILE_NAME="$1"
|
||||
local STAPLE_TARGET="$2"
|
||||
@@ -675,17 +883,10 @@ _notarize_pkg() {
|
||||
echo "Notarization submission ID: ${SUBMISSION_ID}"
|
||||
|
||||
echo "Waiting for Notarization to be completed ..."
|
||||
xcrun notarytool wait "${SUBMISSION_ID}" \
|
||||
--team-id "${DEVELOPER_TEAM_ID}" \
|
||||
--apple-id "${DEVELOPER_USER}" \
|
||||
--password "${DEVELOPER_ASP}"
|
||||
_notarize_wait "${SUBMISSION_ID}"
|
||||
|
||||
# Print status information
|
||||
REQUEST_STATUS=$(xcrun notarytool info "${SUBMISSION_ID}" \
|
||||
--team-id "${DEVELOPER_TEAM_ID}" \
|
||||
--apple-id "${DEVELOPER_USER}" \
|
||||
--password "${DEVELOPER_ASP}" 2>&1 | \
|
||||
awk -F ': ' '/status:/ { print $2; }')
|
||||
REQUEST_STATUS=$(_notarize_status "${SUBMISSION_ID}")
|
||||
|
||||
if [[ "${REQUEST_STATUS}" != "Accepted" ]]; then
|
||||
echo "Notarization failed with status: ${REQUEST_STATUS}"
|
||||
|
||||
@@ -52,10 +52,24 @@ if [ "${PGADMIN_POSTGRES_DIR}" == "" ]; then
|
||||
export PGADMIN_POSTGRES_DIR=/usr/local/pgsql
|
||||
fi
|
||||
|
||||
if [ "${PGADMIN_PYTHON_VERSION}" == "" ]; then
|
||||
echo "PGADMIN_PYTHON_VERSION not set. Setting it to the default: 3.14.7"
|
||||
export PGADMIN_PYTHON_VERSION=3.14.7
|
||||
# Which Python gets bundled is a property of this commit rather than of the
|
||||
# machine or the environment the build happens to run in, so it is read from
|
||||
# pkg/python-version.txt and cannot be overridden from outside. The Windows
|
||||
# build reads the same file.
|
||||
#
|
||||
# Unlike Windows, this still builds the framework with relocatable-python,
|
||||
# which downloads from python.org itself and so only needs the version.
|
||||
# python-build-standalone's relocatable distributions are not framework-shaped,
|
||||
# and shipping one instead would mean reworking both the codesigning and the
|
||||
# @loader_path rewriting in _fixup_imports, which is a job of its own.
|
||||
PYTHON_VERSION_FILE="${SOURCE_DIR}/pkg/python-version.txt"
|
||||
PGADMIN_PYTHON_VERSION=$(sed -nE 's/^PYTHON_VERSION=([0-9.]+)[[:space:]]*$/\1/p' "${PYTHON_VERSION_FILE}")
|
||||
if [ -z "${PGADMIN_PYTHON_VERSION}" ]; then
|
||||
echo "ERROR: could not read PYTHON_VERSION from ${PYTHON_VERSION_FILE}" >&2
|
||||
exit 1
|
||||
fi
|
||||
export PGADMIN_PYTHON_VERSION
|
||||
echo "Bundling Python ${PGADMIN_PYTHON_VERSION}, per ${PYTHON_VERSION_FILE}."
|
||||
|
||||
# Initialize variables
|
||||
CREATE_ZIP=0
|
||||
|
||||
@@ -0,0 +1,105 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>LPic</key>
|
||||
<array>
|
||||
<dict>
|
||||
<key>Attributes</key>
|
||||
<string>0x0000</string>
|
||||
<key>Data</key>
|
||||
<data>
|
||||
AAAAAgAAAAAAAAAAAAQAAA==
|
||||
</data>
|
||||
<key>ID</key>
|
||||
<string>5000</string>
|
||||
<key>Name</key>
|
||||
<string></string>
|
||||
</dict>
|
||||
</array>
|
||||
<key>STR#</key>
|
||||
<array>
|
||||
<dict>
|
||||
<key>Attributes</key>
|
||||
<string>0x0000</string>
|
||||
<key>Data</key>
|
||||
<data>
|
||||
AAYNRW5nbGlzaCB0ZXN0MQVBZ3JlZQhEaXNhZ3JlZQVQcmludAdT
|
||||
YXZlLi4ueklmIHlvdSBhZ3JlZSB3aXRoIHRoZSB0ZXJtcyBvZiB0
|
||||
aGlzIGxpY2Vuc2UsIGNsaWNrICJBZ3JlZSIgdG8gYWNjZXNzIHRo
|
||||
ZSBzb2Z0d2FyZS4gSWYgeW91IGRvIG5vdCBhZ3JlZSwgY2xpY2sg
|
||||
IkRpc2FncmVlIi4=
|
||||
</data>
|
||||
<key>ID</key>
|
||||
<string>5000</string>
|
||||
<key>Name</key>
|
||||
<string>English buttons</string>
|
||||
</dict>
|
||||
<dict>
|
||||
<key>Attributes</key>
|
||||
<string>0x0000</string>
|
||||
<key>Data</key>
|
||||
<data>
|
||||
AAYHRW5nbGlzaAVBZ3JlZQhEaXNhZ3JlZQVQcmludAdTYXZlLi4u
|
||||
e0lmIHlvdSBhZ3JlZSB3aXRoIHRoZSB0ZXJtcyBvZiB0aGlzIGxp
|
||||
Y2Vuc2UsIHByZXNzICJBZ3JlZSIgdG8gaW5zdGFsbCB0aGUgc29m
|
||||
dHdhcmUuIElmIHlvdSBkbyBub3QgYWdyZWUsIGNsaWNrICJEaXNh
|
||||
Z3JlZSIu
|
||||
</data>
|
||||
<key>ID</key>
|
||||
<string>5002</string>
|
||||
<key>Name</key>
|
||||
<string>English</string>
|
||||
</dict>
|
||||
</array>
|
||||
<key>${EULA_FORMAT}</key>
|
||||
<array>
|
||||
<dict>
|
||||
<key>Attributes</key>
|
||||
<string>0x0000</string>
|
||||
<key>Data</key>
|
||||
<data>
|
||||
${EULA_DATA}
|
||||
</data>
|
||||
<key>ID</key>
|
||||
<string>5000</string>
|
||||
<key>Name</key>
|
||||
<string>English</string>
|
||||
</dict>
|
||||
</array>
|
||||
<key>TMPL</key>
|
||||
<array>
|
||||
<dict>
|
||||
<key>Attributes</key>
|
||||
<string>0x0000</string>
|
||||
<key>Data</key>
|
||||
<data>
|
||||
E0RlZmF1bHQgTGFuZ3VhZ2UgSUREV1JEBUNvdW50T0NOVAQqKioq
|
||||
TFNUQwtzeXMgbGFuZyBJRERXUkQebG9jYWwgcmVzIElEIChvZmZz
|
||||
ZXQgZnJvbSA1MDAwRFdSRBAyLWJ5dGUgbGFuZ3VhZ2U/RFdSRAQq
|
||||
KioqTFNURQ==
|
||||
</data>
|
||||
<key>ID</key>
|
||||
<string>128</string>
|
||||
<key>Name</key>
|
||||
<string>LPic</string>
|
||||
</dict>
|
||||
</array>
|
||||
<key>styl</key>
|
||||
<array>
|
||||
<dict>
|
||||
<key>Attributes</key>
|
||||
<string>0x0000</string>
|
||||
<key>Data</key>
|
||||
<data>
|
||||
AAMAAAAAAAwACQAUAAAAAAAAAAAAAAAAACcADAAJABQBAAAAAAAA
|
||||
AAAAAAAAKgAMAAkAFAAAAAAAAAAAAAA=
|
||||
</data>
|
||||
<key>ID</key>
|
||||
<string>5000</string>
|
||||
<key>Name</key>
|
||||
<string>English</string>
|
||||
</dict>
|
||||
</array>
|
||||
</dict>
|
||||
</plist>
|
||||
@@ -0,0 +1,4 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg width="432" height="446" viewBox="0 0 432.071 445.383" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill="#336791" d="M402.395,271.23c-50.302,10.376-53.76-6.655-53.76-6.655c53.111-78.808,75.313-178.843,56.153-203.326c-52.27-66.785-142.752-35.2-144.262-34.38l-0.486,0.087c-9.938-2.063-21.06-3.292-33.56-3.496c-22.761-0.373-40.026,5.967-53.127,15.902 c0,0-161.411-66.495-153.904,83.63c1.597,31.938,45.776,241.657,98.471,178.312c19.26-23.163,37.869-42.748,37.869-42.748c9.243,6.14,20.308,9.272,31.908,8.147l0.901-0.765c-0.28,2.876-0.152,5.689,0.361,9.019c-13.575,15.167-9.586,17.83-36.723,23.416 c-27.459,5.659-11.328,15.734-0.796,18.367c12.768,3.193,42.307,7.716,62.266-20.224l-0.796,3.188c5.319,4.26,9.054,27.711,8.428,48.969c-0.626,21.259-1.044,35.854,3.147,47.254c4.191,11.4,8.368,37.05,44.042,29.406c29.809-6.388,45.256-22.942,47.405-50.555 c1.525-19.631,4.976-16.729,5.194-34.28l2.768-8.309c3.192-26.611,0.507-35.196,18.872-31.203l4.463,0.392c13.517,0.615,31.208-2.174,41.591-7c22.358-10.376,35.618-27.7,13.573-23.148z"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.1 KiB |
@@ -0,0 +1,14 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg width="432" height="446" viewBox="0 0 432.071 445.383" xmlns="http://www.w3.org/2000/svg">
|
||||
<g fill="none" stroke="#FFFFFF" stroke-width="8.3" stroke-linecap="round" stroke-linejoin="round" stroke-miterlimit="4">
|
||||
<path d="M215.866,286.484c-1.385,49.516,0.348,99.377,5.193,111.495c4.848,12.118,15.223,35.688,50.9,28.045c29.806-6.39,40.651-18.756,45.357-46.051c3.466-20.082,10.148-75.854,11.005-87.281"/>
|
||||
<path d="M173.104,38.256c0,0-161.521-66.016-154.012,84.109c1.597,31.938,45.779,241.664,98.473,178.316c19.256-23.166,36.671-41.335,36.671-41.335"/>
|
||||
<path d="M260.349,26.207c-5.591,1.753,89.848-34.889,144.087,34.417c19.159,24.484-3.043,124.519-56.153,203.329"/>
|
||||
<path style="stroke-linejoin:bevel;" d="M348.282,263.953c0,0,3.461,17.036,53.764,6.653c22.04-4.552,8.776,12.774-13.577,23.155c-18.345,8.514-59.474,10.696-60.146-1.069c-1.729-30.355,21.647-21.133,19.96-28.739c-1.525-6.85-11.979-13.573-18.894-30.338 c-6.037-14.633-82.796-126.849,21.287-110.183c3.813-0.789-27.146-99.002-124.553-100.599c-97.385-1.597-94.19,119.762-94.19,119.762"/>
|
||||
<path d="M188.604,274.334c-13.577,15.166-9.584,17.829-36.723,23.417c-27.459,5.66-11.326,15.733-0.797,18.365c12.768,3.195,42.307,7.718,62.266-20.229c6.078-8.509-0.036-22.086-8.385-25.547c-4.034-1.671-9.428-3.765-16.361,3.994z"/>
|
||||
<path d="M187.715,274.069c-1.368-8.917,2.93-19.528,7.536-31.942c6.922-18.626,22.893-37.255,10.117-96.339c-9.523-44.029-73.396-9.163-73.436-3.193c-0.039,5.968,2.889,30.26-1.067,58.548c-5.162,36.913,23.488,68.132,56.479,64.938"/>
|
||||
<path style="fill:#FFFFFF;stroke-width:4.155;stroke-linecap:butt;stroke-linejoin:miter;" d="M172.517,141.7c-0.288,2.039,3.733,7.48,8.976,8.207c5.234,0.73,9.714-3.522,9.998-5.559c0.284-2.039-3.732-4.285-8.977-5.015c-5.237-0.731-9.719,0.333-9.996,2.367z"/>
|
||||
<path style="fill:#FFFFFF;stroke-width:2.0775;stroke-linecap:butt;stroke-linejoin:miter;" d="M331.941,137.543c0.284,2.039-3.732,7.48-8.976,8.207c-5.238,0.73-9.718-3.522-10.005-5.559c-0.277-2.039,3.74-4.285,8.979-5.015c5.239-0.73,9.718,0.333,10.002,2.368z"/>
|
||||
<path d="M350.676,123.432c0.863,15.994-3.445,26.888-3.988,43.914c-0.804,24.748,11.799,53.074-7.191,81.435"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 2.1 KiB |
@@ -0,0 +1,72 @@
|
||||
{
|
||||
"fill-specializations" : [
|
||||
{
|
||||
"value" : {
|
||||
"linear-gradient" : [
|
||||
"srgb:0.92549,0.94118,0.95686,1.00000",
|
||||
"srgb:0.76078,0.81961,0.87059,1.00000"
|
||||
],
|
||||
"orientation" : {
|
||||
"start" : {
|
||||
"x" : 0.5,
|
||||
"y" : 0
|
||||
},
|
||||
"stop" : {
|
||||
"x" : 0.5,
|
||||
"y" : 1
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"appearance" : "dark",
|
||||
"value" : "system-dark"
|
||||
}
|
||||
],
|
||||
"groups" : [
|
||||
{
|
||||
"layers" : [
|
||||
{
|
||||
"glass" : false,
|
||||
"image-name" : "Lines.svg",
|
||||
"name" : "Lines",
|
||||
"position" : {
|
||||
"scale" : 1.85,
|
||||
"translation-in-points" : [
|
||||
0,
|
||||
30
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"glass" : false,
|
||||
"image-name" : "Body.svg",
|
||||
"name" : "Body",
|
||||
"position" : {
|
||||
"scale" : 1.85,
|
||||
"translation-in-points" : [
|
||||
0,
|
||||
30
|
||||
]
|
||||
}
|
||||
}
|
||||
],
|
||||
"lighting" : "individual",
|
||||
"name" : "Slonik",
|
||||
"shadow" : {
|
||||
"kind" : "layer-color",
|
||||
"opacity" : 0.25
|
||||
},
|
||||
"specular" : true,
|
||||
"translucency" : {
|
||||
"enabled" : false,
|
||||
"value" : 0.5
|
||||
}
|
||||
}
|
||||
],
|
||||
"supported-platforms" : {
|
||||
"squares" : [
|
||||
"macOS"
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
# pgbuild dependency lock
|
||||
#
|
||||
# Which build of each dependency .github/actions/install-pgbuild-deps
|
||||
# downloads from pgadmin-org/pgbuild, and what it must hash to. The action
|
||||
# fetches the tag named here and refuses to unpack an archive whose SHA-256
|
||||
# differs, so these lines decide which PostgreSQL, OpenSSL and Kerberos
|
||||
# binaries end up inside an Authenticode signed installer and a notarised
|
||||
# appbundle.
|
||||
#
|
||||
# It matters because pgbuild also publishes rolling tags ending in -latest,
|
||||
# whose assets are replaced in place on every rebuild. Building from those
|
||||
# means signing whatever they happened to point at that morning, write
|
||||
# access to pgbuild is write access to what pgAdmin ships, and no two
|
||||
# builds of the same commit contain the same binaries. Pinning the
|
||||
# per-version tag instead gives a build that can be reproduced and a change
|
||||
# that can be reviewed. pgbuild publishes a .sha256 beside each archive,
|
||||
# which is a convenience for people rather than a source of trust, since
|
||||
# whoever rebuilds the archive rebuilds that too; this file is the only
|
||||
# thing that actually pins anything, which is why the action checks against
|
||||
# it and not against the published checksum.
|
||||
#
|
||||
# Three whitespace separated columns, one dependency per line, with # for a
|
||||
# comment. Kept dull on purpose so that bash, PowerShell and a reviewer can
|
||||
# all read it.
|
||||
#
|
||||
# 1. the dependency, as the action names it: package, platform and
|
||||
# architecture, with the PostgreSQL major where there is one
|
||||
# 2. the pgbuild release tag to download from, which carries the upstream
|
||||
# version, so the second column is where a bump shows up in the diff
|
||||
# 3. the SHA-256 of the archive in that release, in lowercase hex
|
||||
#
|
||||
# A build that fails on a mismatch is telling you that the bytes behind a
|
||||
# pinned tag are not the bytes that were reviewed. A build that merely
|
||||
# warns is telling you pgbuild has published something newer, which is
|
||||
# routine and is not an error: the build carries on with the pinned
|
||||
# version. Either way the fix is to refresh, read the diff and commit it:
|
||||
#
|
||||
# tools/refresh_pgbuild_lock.py <package>
|
||||
#
|
||||
# This file is generated by that script, so edit it through the script
|
||||
# rather than by hand.
|
||||
|
||||
# macOS, arm64
|
||||
krb5-macos-arm64 krb5-1.22.2-macos-arm64 d61ec76f7adc79b915bb88f55bc4755db2edb5f215431aff01959e0fe38214ea
|
||||
lz4-macos-arm64 lz4-1.10.0-macos-arm64 50824e82cf864469b7d5e01d46896db07a48c4b143d7d4edb4bd7355ea5ebe29
|
||||
openssl-macos-arm64 openssl-3.5.8-macos-arm64 e319fda192735b7b4f7b5742388b9b2264c6e94078f79a9dbb856d2cadcb0c4c
|
||||
postgresql-18-macos-arm64 postgresql-18.6-macos-arm64 b21232335b76b3cb828839f98572da4298e19cad4a69b31f7b234d218e149c28
|
||||
zstd-macos-arm64 zstd-1.5.7-macos-arm64 6703acb18b9db260df484828d0e823017742be82c87cdd3d42fde85efdd232ad
|
||||
|
||||
# macOS, x86_64
|
||||
krb5-macos-x86_64 krb5-1.22.2-macos-x86_64 40e123c1a85dba06b1d808cecf6c1e5ac2818fb8a722122400a1ffac9494651f
|
||||
lz4-macos-x86_64 lz4-1.10.0-macos-x86_64 ff1e23e0150533abf0f81c1341e7dab8e7e18dd51158084ae4ac9b847be6df7c
|
||||
openssl-macos-x86_64 openssl-3.5.8-macos-x86_64 e5a947e1ddecd9a75c9c0f8f3b92d7de74958d6a3bbcea99c509fb4900dad431
|
||||
postgresql-18-macos-x86_64 postgresql-18.6-macos-x86_64 b36aa6afe7515806aa9724ae6ceebed214b1fec9c2849609598b19c4ecae893e
|
||||
zstd-macos-x86_64 zstd-1.5.7-macos-x86_64 77634c1dafe07882d63ace5e13e4f59f583b4763e04076611515335fe1168a41
|
||||
|
||||
# Windows, x86_64
|
||||
krb5-windows-x86_64 krb5-1.22.2-windows-x86_64 7557cf16faf8af1ee450ef211ac8f74a513914d722c776a3645d996e7cd08dfc
|
||||
postgresql-18-windows-x86_64 postgresql-18.6-windows-x86_64 4eba1f86561238348a39040809f7b1d08f434ef110c2ffe37e291d63c0407d67
|
||||
@@ -62,7 +62,7 @@ if [ -z "${YARN_VERSION}" ]; then
|
||||
exit 1
|
||||
fi
|
||||
yarn set version "${YARN_VERSION}"
|
||||
yarn install
|
||||
yarn install --immutable
|
||||
yarn run bundle
|
||||
|
||||
# Copy the commit_hash file, it doesn't show up in git ls-files
|
||||
|
||||
@@ -0,0 +1,215 @@
|
||||
##########################################################################
|
||||
#
|
||||
# pgAdmin 4 - PostgreSQL Tools
|
||||
#
|
||||
# Copyright (C) 2013 - 2026, The pgAdmin Development Team
|
||||
# This software is released under the PostgreSQL Licence
|
||||
#
|
||||
##########################################################################
|
||||
|
||||
"""Remove files that pgAdmin never uses from an installed Python
|
||||
environment, to reduce the size of the packages.
|
||||
|
||||
Usage: python3 prune_site_packages.py <directory>
|
||||
|
||||
The directory is searched recursively, so it may be a venv, a Python
|
||||
framework or a site-packages directory. The build fails if a file that is
|
||||
kept cannot be found, rather than shipping a package that cannot work.
|
||||
"""
|
||||
|
||||
import ast
|
||||
import fnmatch
|
||||
import os
|
||||
import re
|
||||
import shutil
|
||||
import sys
|
||||
|
||||
CONFIG_PY = os.path.join(os.path.dirname(os.path.abspath(__file__)), '..',
|
||||
'web', 'config.py')
|
||||
|
||||
|
||||
def languages():
|
||||
"""Return the language codes of the translations pgAdmin has, from the
|
||||
keys of LANGUAGES in web/config.py, without the territory or script
|
||||
(e.g. 'zh' for 'zh_Hans_CN'). The file is parsed rather than imported,
|
||||
as importing it needs pgAdmin's own modules."""
|
||||
with open(CONFIG_PY, encoding='utf-8') as f:
|
||||
tree = ast.parse(f.read(), CONFIG_PY)
|
||||
|
||||
for node in tree.body:
|
||||
if isinstance(node, ast.Assign) and \
|
||||
any(isinstance(t, ast.Name) and t.id == 'LANGUAGES'
|
||||
for t in node.targets):
|
||||
return sorted({code.split('_')[0]
|
||||
for code in ast.literal_eval(node.value)})
|
||||
|
||||
sys.exit('ERROR: LANGUAGES was not found in {}'.format(CONFIG_PY))
|
||||
|
||||
|
||||
# Each entry maps a directory to the files in it that may be removed, and
|
||||
# the patterns for the ones to keep. Every keep pattern must match at least
|
||||
# one file.
|
||||
PRUNE = {
|
||||
# google-api-python-client bundles the discovery document of every
|
||||
# Google API (600 or so files, over 100MB), and discovery.build() reads
|
||||
# them from there rather than fetching them. pgAdmin only builds the
|
||||
# services below; see the *_api_version attributes in
|
||||
# web/pgadmin/misc/cloud/google/__init__.py and the discovery.build()
|
||||
# calls in web/pgacloud/providers/google.py, which must be kept in step
|
||||
# with this list.
|
||||
os.path.join('googleapiclient', 'discovery_cache', 'documents'): (
|
||||
'*.json', [
|
||||
'cloudresourcemanager.v1.json',
|
||||
'compute.v1.json',
|
||||
'sqladmin.v1.json',
|
||||
'sqladmin.v1beta4.json',
|
||||
]),
|
||||
|
||||
# Babel carries CLDR data for over 1,000 locales (about 29MB), and only
|
||||
# loads the user's locale and its parents, down to root. Keeping every
|
||||
# territory and script variant of each language covers the parents.
|
||||
os.path.join('babel', 'locale-data'): (
|
||||
'*.dat',
|
||||
['root.dat'] +
|
||||
['{}{}.dat'.format(lang, suffix)
|
||||
for lang in languages() for suffix in ('', '_*')]),
|
||||
}
|
||||
|
||||
# Packaging tools that are needed to build the environment but not to run
|
||||
# pgAdmin. Each is removed from any site-packages directory it is found in.
|
||||
DISTRIBUTIONS = ['pip', 'setuptools', 'wheel']
|
||||
|
||||
# A module-level import of pkg_resources, which setuptools provides. Imports
|
||||
# inside a function or a try block are indented, and are either lazy or
|
||||
# guarded against it being missing.
|
||||
PKG_RESOURCES_IMPORT = re.compile(r'^(?:import|from)[ \t]+pkg_resources\b',
|
||||
re.MULTILINE)
|
||||
|
||||
|
||||
def pkg_resources_users(site_packages):
|
||||
"""Return the modules in site_packages, other than the packaging tools'
|
||||
own and tests, that import pkg_resources when they are loaded. On Python
|
||||
3.9, passlib 1.7.4 does, so setuptools must be kept there (#9829)."""
|
||||
own = {'pip', 'setuptools', 'pkg_resources', '_distutils_hack', 'wheel'}
|
||||
users = []
|
||||
|
||||
for dirpath, dirnames, filenames in os.walk(site_packages):
|
||||
if dirpath == site_packages:
|
||||
dirnames[:] = [d for d in dirnames if d not in own]
|
||||
dirnames[:] = [d for d in dirnames if d not in ('test', 'tests')]
|
||||
for filename in fnmatch.filter(filenames, '*.py'):
|
||||
path = os.path.join(dirpath, filename)
|
||||
with open(path, encoding='utf-8', errors='replace') as f:
|
||||
if PKG_RESOURCES_IMPORT.search(f.read()):
|
||||
users.append(os.path.relpath(path, site_packages))
|
||||
|
||||
return sorted(users)
|
||||
|
||||
|
||||
def prune_dir(dirpath, removable, keep):
|
||||
"""Remove the files in dirpath matching removable but none of the keep
|
||||
patterns, and return the number of bytes freed."""
|
||||
filenames = os.listdir(dirpath)
|
||||
|
||||
missing = [pattern for pattern in keep
|
||||
if not fnmatch.filter(filenames, pattern)]
|
||||
if missing:
|
||||
sys.exit('ERROR: {} is missing from {}'.format(
|
||||
', '.join(missing), dirpath))
|
||||
|
||||
saved = 0
|
||||
for filename in fnmatch.filter(filenames, removable):
|
||||
if any(fnmatch.fnmatch(filename, pattern) for pattern in keep):
|
||||
continue
|
||||
path = os.path.join(dirpath, filename)
|
||||
saved += os.path.getsize(path)
|
||||
os.remove(path)
|
||||
|
||||
return saved
|
||||
|
||||
|
||||
def _tree_size(path):
|
||||
return sum(os.path.getsize(os.path.join(dirpath, filename))
|
||||
for dirpath, _, filenames in os.walk(path)
|
||||
for filename in filenames)
|
||||
|
||||
|
||||
def uninstall(site_packages, dist_info, root):
|
||||
"""Remove the files listed in a distribution's RECORD, as pip uninstall
|
||||
would, and return the number of bytes freed. Files outside root, such as
|
||||
the scripts in a Windows venv when only site-packages is being pruned,
|
||||
are left alone."""
|
||||
root = os.path.abspath(root)
|
||||
saved = 0
|
||||
packages = set()
|
||||
|
||||
# Read the whole of RECORD first, as it lists itself, and Windows cannot
|
||||
# remove a file that is still open.
|
||||
with open(os.path.join(site_packages, dist_info, 'RECORD'),
|
||||
encoding='utf-8') as f:
|
||||
lines = f.readlines()
|
||||
|
||||
for line in lines:
|
||||
relpath = line.rsplit(',', 2)[0]
|
||||
if not relpath:
|
||||
continue
|
||||
path = os.path.abspath(os.path.join(site_packages, relpath))
|
||||
if os.path.commonpath([root, path]) != root:
|
||||
continue
|
||||
if os.sep in os.path.normpath(relpath) and \
|
||||
not relpath.startswith('..'):
|
||||
packages.add(os.path.normpath(relpath).split(os.sep)[0])
|
||||
if os.path.isfile(path) or os.path.islink(path):
|
||||
saved += os.path.getsize(path)
|
||||
os.remove(path)
|
||||
|
||||
# Remove the package directories too, including any bytecode compiled
|
||||
# after installation, which RECORD does not list.
|
||||
for package in packages:
|
||||
path = os.path.join(site_packages, package)
|
||||
if os.path.isdir(path):
|
||||
saved += _tree_size(path)
|
||||
shutil.rmtree(path)
|
||||
|
||||
return saved
|
||||
|
||||
|
||||
def prune(root):
|
||||
found = set()
|
||||
saved = 0
|
||||
removed = []
|
||||
|
||||
for dirpath, dirnames, _ in os.walk(root):
|
||||
for subdir, (removable, keep) in PRUNE.items():
|
||||
if dirpath.endswith(os.sep + subdir):
|
||||
found.add(subdir)
|
||||
saved += prune_dir(dirpath, removable, keep)
|
||||
|
||||
if os.path.basename(dirpath) == 'site-packages':
|
||||
users = pkg_resources_users(dirpath)
|
||||
for name in DISTRIBUTIONS:
|
||||
if name == 'setuptools' and users:
|
||||
print('Keeping setuptools, as pkg_resources is imported '
|
||||
'by {}'.format(', '.join(users)))
|
||||
continue
|
||||
for dist_info in fnmatch.filter(dirnames,
|
||||
name + '-*.dist-info'):
|
||||
saved += uninstall(dirpath, dist_info, root)
|
||||
removed.append(dist_info[:-len('.dist-info')])
|
||||
dirnames[:] = [d for d in dirnames
|
||||
if os.path.isdir(os.path.join(dirpath, d))]
|
||||
|
||||
for subdir in set(PRUNE) - found:
|
||||
sys.exit('ERROR: {} was not found under {}'.format(subdir, root))
|
||||
|
||||
if removed:
|
||||
print('Removed {}'.format(', '.join(sorted(removed))))
|
||||
print('Pruned {:.1f}MB of unused files from {}'.format(
|
||||
saved / 1024 / 1024, root))
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
if len(sys.argv) != 2 or not os.path.isdir(sys.argv[1]):
|
||||
sys.exit('Usage: {} <directory>'.format(sys.argv[0]))
|
||||
|
||||
prune(sys.argv[1])
|
||||
@@ -0,0 +1,3 @@
|
||||
Support: pgadmin-support@lists.postgresql.org
|
||||
Website: https://www.pgadmin.org/
|
||||
Tracker: https://github.com/pgadmin-org/pgadmin4/issues
|
||||
@@ -0,0 +1,41 @@
|
||||
pgAdmin 4: APT repository
|
||||
|
||||
Supported platform matrix:
|
||||
|
||||
@SUPPORTED_TABLE@
|
||||
|
||||
@ARCHIVE_SECTION_START@
|
||||
Platforms that are no longer supported. Packages for these remain available
|
||||
from https://pgadmin-archive.postgresql.org/, from where they can be manually
|
||||
downloaded; the archive site cannot be used as a repository.
|
||||
|
||||
@ARCHIVED_TABLE@
|
||||
@ARCHIVE_SECTION_END@
|
||||
|
||||
To use this repository, run the following commands:
|
||||
|
||||
#
|
||||
# Setup the repository
|
||||
#
|
||||
|
||||
# Install the public key for the repository (if not done previously):
|
||||
curl -fsS https://www.pgadmin.org/static/packages_pgadmin_org.pub | sudo gpg --dearmor -o /usr/share/keyrings/packages-pgadmin-org.gpg
|
||||
|
||||
# Create the repository configuration file:
|
||||
sudo sh -c 'echo "deb [signed-by=/usr/share/keyrings/packages-pgadmin-org.gpg] @BASE_URL@/apt/$(lsb_release -cs) pgadmin4 main" > /etc/apt/sources.list.d/pgadmin4.list && apt update'
|
||||
|
||||
#
|
||||
# Install pgAdmin
|
||||
#
|
||||
|
||||
# Install for both desktop and web modes:
|
||||
sudo apt install pgadmin4
|
||||
|
||||
# Install for desktop mode only:
|
||||
sudo apt install pgadmin4-desktop
|
||||
|
||||
# Install for web mode only:
|
||||
sudo apt install pgadmin4-web
|
||||
|
||||
# Configure the webserver, if you installed pgadmin4-web:
|
||||
sudo /usr/pgadmin4/bin/setup-web.sh
|
||||