Commit Graph
8545 Commits
Author SHA1 Message Date
Dave Page dd0d8ac533 Revert Windows code signing to SHA-1 file digests
#10355 moved signtool to /fd sha256 on the assumption that the key
provider repair described in pkg/win32/SIGNING.md made SHA-256 possible.
It does not: that repair binds the certificate to the Certum card's
legacy CryptoAPI provider, crypto3 CSP, which fails a SHA-256 file digest
in SignerSign() with 0xc0000225 before the PIN is requested, whilst
/fd sha1 signs and verifies. SHA-1 is what every previous release has
shipped with, so go back to it for both the component and the installer
signatures, and document why.

The timestamp digest stays at SHA-256, which works. Rebinding the
certificate to Certum's CNG provider is the likely route to SHA-256 file
digests, but is untested and is left for later.
2026-09-23 10:26:37 +01:00
Dave Page d0315c58ec Fix the Windows environment check, which cmd could not parse
The messages that report a missing prerequisite were changed from !VAR!
to %VAR% in commit afa0c773a, since the file does not enable delayed
expansion and they printed the variable name rather than the path. That
made them correct and unparseable at the same time: cmd expands %VAR%
when it parses a parenthesised block rather than when it runs it, and
PGADMIN_INNOTOOL_DIR defaults to "C:\Program Files (x86)\Inno Setup 6",
whose closing parenthesis then ended the IF block early. Every Windows
build has failed at "Checking the environment..." since, with "\Inno was
unexpected at this time", whether or not Inno Setup was installed, which
took the nightly snapshot with it.

The four checks move into a :REQUIRE_DIR subroutine that takes the
directory and the hint, and echoes both outside any block, where a
parenthesis is just a character. Enabling delayed expansion would also
have worked, but only by making ! a metacharacter for the whole file.
The other uses of these two paths are all inside double quotes, which
the parser does respect, so they were never affected.
2026-09-23 09:01:54 +01:00
Dave Page 43d8b3fabf Rename the server 'Change Password' option to 'Change Server Password' (#10035)
The 'Change Password...' option on a server node was ambiguous, because it
changes the password of the PostgreSQL role that pgAdmin connects with, yet
read as though it might change the pgAdmin login password instead. The menu
option and its dialog are now called 'Change Server Password', with the
connected role name in the dialog title, and the equivalent option in the
user menu becomes 'Change pgAdmin Password', so that the two can no longer
be confused with one another.

The dialog's own 'User' field was always blank, since the userName prop that
showChangeServerPassword passes was never accepted by ChangePasswordContent;
it is now wired through, which also makes the existing documentation of that
field accurate. The documentation for both dialogs has been updated to match
the new names, along with a couple of button names that had drifted.

Fixes #9230.
2026-09-22 17:15:12 +01:00
dev-hari-prasad e78c7fe020 Fix Query Tool tabs auto-running their query after a session restore (#10055)
Typing in the editor of a View/Edit Data tab promotes it to a Query Tool, but
that promotion only ever updated the component's own state: the tab's saved
metadata kept the original tool URL, with is_query_tool=false in its query
string. On the next start-up the tab was therefore restored as a View/Edit Data
tab, and View/Edit Data tabs execute their query as soon as the connection is
made, so whatever SQL the user had left in the editor ran unprompted. A
restored DELETE or UPDATE running by itself is not a pleasant way to start the
day.

toggleQueryTool now rewrites is_query_tool in the panel's stored toolUrl and
saves the layout, so a promoted tab comes back as a Query Tool and no longer
takes the auto-execute path. The URL is kept relative, because in Desktop mode
the runtime picks a fresh port on every launch and an absolute URL would point
the restored tab at a dead origin.

is_query_tool and restore are also normalised to real booleans up front rather
than compared against the string 'true' in each place they are read, and a
failed restore no longer leaves the editor disabled with nothing logged.

Fixes #10031
2026-09-22 17:11:02 +01:00
Dave Page 2ba68de777 Add Caddy reverse proxy configuration examples (#10344)
Adds Caddy examples alongside the existing NGINX ones for hosting pgAdmin with
Gunicorn or uWSGI, both at the root of a server and under a sub-directory,
mirroring the structure of the NGINX sections. Caddy does not speak the native
uwsgi protocol, so its uWSGI examples run uWSGI in HTTP mode instead.

Documents the two things that must line up for a sub-directory deployment
behind any reverse proxy: the proxy forwarding the request path unmodified
with an X-Script-Name header, and a SCRIPT_NAME environment variable on the
pgAdmin process itself, which is read at startup to set APPLICATION_ROOT and
SESSION_COOKIE_PATH. The missing environment variable is what caused the
"login succeeds then bounces back to the login page" report in #10334.

Also notes that pgAdmin trusts X-Script-Name and X-Scheme unconditionally,
unlike the X-Forwarded-* headers, so every example now either overwrites or
clears both rather than passing on whatever the client sent; warns that the
systemd unit shipped with Caddy sets PrivateTmp=true, which hides a socket
created in the system /tmp; and refreshes the stale python3.7 paths.
2026-09-22 17:03:47 +01:00
Dave Page afa0c773ad Sign the bundled PostgreSQL binaries in the Windows build (#10355)
The PostgreSQL and Kerberos utilities and libraries we ship in the
Windows package are not signed, whilst the installer, the uninstaller
and pgAdmin4.exe are. Nothing verifies an Authenticode signature when
these run, so this is not a security fix, but it does make life easier
for organisations running application allow-listing under WDAC or
AppLocker, where publisher rules are simpler and more durable than path
or hash based ones, and it cuts down on antivirus false positives.

The signtool invocation moves into a :SIGN_FILES subroutine, and a new
:SIGN_COMPONENTS step signs everything we build ourselves once
:CREATE_RUNTIME_ENV has staged it. Names are matched through DIR /B so
the optional libraries are skipped when absent, and the whole thing is a
no-op when PGADMIN_WINDOWS_CSC is unset, so unsigned developer builds
are unaffected. The Electron, Python and VC++ runtime components are
deliberately left alone, being third party binaries whose own signatures
ours would replace. Everything goes through a single signtool call,
since the hardware token prompts for a PIN on the first signature of a
session.

The file digest moves from SHA-1 to SHA-256 for both the runtime and the
installer. SHA-1 was never a deliberate choice: it dates from the move
to the Certum token, at a point where the certificate in the machine
store had no key provider link, which limits signing to SHA-1. The
buildfarm now repairs that link with certutil when it provisions the
host.

The four optional PostgreSQL libraries were copied without checking the
result; they now abort the build like the others do.

PGADMIN_POSTGRES_DIR and PGADMIN_KRB5_DIR defaulted to an EDB PostgreSQL
installation and an MIT Kerberos for Windows installation respectively,
whereas both sets of binaries have come from winpgbuild for some time
and the buildfarm points at C:\build64\postgresql and C:\build64\krb5.
The four "does not exist" messages that report a missing prerequisite
used !VAR!, but the SETLOCAL at the top of the file does not enable
delayed expansion, so they printed the variable name rather than the
path that was looked for.

Finally, the acceptance test now verifies the signatures on everything
:SIGN_COMPONENTS signs, walking the same list of names where the files
land: none of them exist outside the installer until the installer has
run, so that is the only place a published build can be checked. The
expected publisher is taken from the installer itself rather than hard
coded, so a component signed by somebody else is caught without the
workflow needing an edit when the certificate is renewed.
2026-09-22 17:02:38 +01:00
dev-hari-prasad 351376af7f Enforce --immutable on every yarn install, in CI and the release builds (#10389)
In the four GitHub Actions workflows the flag is documentation only, since
Yarn's enableImmutableInstalls already defaults to true whenever CI is set in
the environment, but making it explicit means the intent is visible in the
workflow rather than implied by the runner.

The six installs that run outside CI are where this actually changes
behaviour: the Makefile, the Dockerfile, and the pip, Linux, macOS and Windows
packaging builds all run with yarn.lock beside them, and until now any of them
would quietly rewrite the lockfile rather than fail. A release build that
rewrites the lockfile is shipping something other than what we reviewed, so
failing loudly there is the point of the exercise.
2026-09-22 16:50:39 +01:00
Domenico Sgarbossa 91e7747226 Updated message catalogs for v9.18 (#10448) 2026-09-22 16:43:37 +01:00
Dave Page 84a76baa8e Pin against pgbuild's immutable releases, now that they exist
The lock was written against the rolling tags because the per-version
releases did not exist when it was generated, and the refresh tool assumed
PostgreSQL was tagged by major, which it is not: the release that never
changes is postgresql-18.6, whilst only the rolling one says 18. The tool
now resolves a major to the version pgbuild's manifest names, and a tag that
does not exist is an error rather than something to fall back from, since a
lock recording an archive nobody can fetch is worse than no lock at all.
2026-09-22 16:17:05 +01:00
Dave Page c603393827 Run the Windows and macOS build checks when the dependency lock changes
A lock change decides which PostgreSQL, OpenSSL and Kerberos binaries end up
inside the installer, so it is exactly the kind of change those checks exist
for, and without this it was the one packaging input that could move without
either of them running.
2026-09-22 14:31:45 +01:00
Dave Page ce6c049dbc Pin and verify the pgbuild build dependencies
The install-pgbuild-deps action fetched PostgreSQL, OpenSSL, Kerberos, zstd
and lz4 from pgbuild release tags ending in -latest, whose assets are
replaced in place on every rebuild, and unpacked them with no checksum or
signature check anywhere in between. Since the resulting build is
Authenticode signed on Windows and Developer ID signed and notarised on
macOS, that made write access to pgbuild equivalent to write access to what
pgAdmin ships, and it meant a build was not reproducible either, the same
commit producing different binaries depending on the day it was built. This
is pgadmin-org/pgbuild issue 1.

pkg/pgbuild-deps.lock now records, for every dependency the action can
fetch on either macOS architecture and on Windows, the per-version release
tag to download from and the SHA-256 its archive must hash to. The action
fetches that tag, checks the digest before unpacking, and fails on a
mismatch or on a dependency the lock does not mention, since an unpinned
dependency reaching a signing build is the thing this is meant to prevent.
pgbuild publishes a .sha256 beside each archive, which is deliberately not
consulted: whoever rebuilds an archive rebuilds that too, so the lock is
the only thing here that pins anything.

Being out of date is a different matter from being wrong, so once an
archive is verified the action asks pgbuild what it has published since,
and warns rather than fails when the pinned build is no longer the newest.
That check is best effort and never fails a build whose downloads have
already been verified.

tools/refresh_pgbuild_lock.py rewrites the lock, taking the version to pin
from pgbuild's own manifest.json and working one package at a time so that
bumping OpenSSL does not restate every other digest. The diff it produces,
with the upstream version visible in the tag column, is the audit trail the
previous arrangement had none of.

The digests were taken from the archives pgbuild publishes today, which are
the same archives its per-version releases will carry, but those releases
do not exist yet, so the tags in the lock cannot be fetched until pgbuild's
own change lands.
2026-09-22 14:30:43 +01:00
Dave Page bccef24bf7 Drop from= from the authorized_keys templates, which do not carry it
The deployed files are generated rather than hand written, and that option is
not one we can set there, so the templates described a restriction production
does not have. What a key may do is still bounded by restrict and the forced
command, which is the part that matters; what is lost is the second answer to
where it may be used from.
2026-09-22 13:07:19 +01:00
Dave Page e543aaa6f9 Bring install-repo-readme.py up to the project's pep8 settings
It has been failing check-python-style since it landed: five lines over the
limit, and two breaks before a binary operator, which count here because
.pycodestyle sets an explicit ignore list and so loses pycodestyle's own
default exemption for W503.
2026-09-22 11:40:50 +01:00
Dave Page 720dd3714f Ship the docs tarball with its versioned directory, as releases always have
The web server unpacks this into the documentation root and then hands
docloader the directory name pgadmin4-<version>-docs, so a flat archive would
have scattered several hundred HTML files across that root and then failed on
a directory that was never created. Every published tarball carries that top
level; the one assembled here did not, and the first release to notice would
have been the next one.
2026-09-22 11:25:32 +01:00
Dave Page af5fd2acc3 Move the archive sync and release purge scripts into the publishing tooling
sync-ftp-to-s3.py and purge_pgadmin_releases.sh have been living in the
pgaweb checkout under tools/, which was never where they belonged: neither
touches Django, the site database or Varnish, and both work on the download
tree at /var/ftp/pgadmin4, so they are part of publishing rather than part of
the website. They move here, renamed to match the hyphenated names of the
scripts already in this directory, and the README gains them in the file
table, in the install line and in the prose that explains what lives where.

The sync-s3 verb now runs sync-ftp-to-s3.py out of PUBLISH_DIR instead of
TOOLS_DIR, which makes this a deployment-ordering change: the wrapper and the
copy of the script it names must both be installed on the download server
before the pgaweb side deletes its own copy, and the comment on the verb says
so. The three verbs that remain genuinely website operations, create-release,
load-docs and purge-cache, still reach into TOOLS_DIR and are untouched.

purge-pgadmin-releases.sh deliberately gets no verb. It is a by-hand
operation that deletes published content, it defaults to a dry run that
somebody is expected to read before re-running with --live, and the rebuild
scripts it pairs with are already documented as standalone; handing a
compromised runner the ability to delete releases would undo most of what the
wrapper is for.

Both scripts are otherwise unchanged apart from the filename in the purge
script's own header comment, and four lines in the Python rewrapped to 79
columns because pkg/ is covered by check-pep8 and the originals were not.
2026-09-22 11:19:39 +01:00
Dave Page 2343f60f04 Select the publishing runner by role rather than by its hostname
The runner has carried both labels all along, so this is the last of the
machine names out of the tree; roadie can stay on the runner until these have
run green, and then go.
2026-09-22 11:06:25 +01:00
Dave Page 5f102aa36c Do not claim the PIN helper was shipped as a compiled executable 2026-09-22 11:03:40 +01:00
Dave Page 1b392caa0f Say how the PIN helper is installed and started
The section named the script and left the reader to work out the rest: where
AutoHotkey comes from, where the script should live given the runner deletes
its own workspace, how it starts in the interactive session alongside the
runner, and how to tell whether it is running at all.
2026-09-22 10:46:59 +01:00
Dave Page 8bca7e0c57 Retry the notarisation wait rather than failing the build
xcrun notarytool wait polls Apple for as long as the notarisation takes, and
exits non-zero if any single poll fails, so a runner losing its route for a few
seconds failed a notarisation that was proceeding perfectly well: the 22nd
September snapshot died twenty six minutes into waiting for a DMG Apple had
already accepted, with "The Internet connection appears to be offline".

The wait is now retried ten times at a minute apart, without resubmitting,
since the submission is still queued under the same ID. Each failure asks for
the status directly, because the wait also exits non-zero when a submission
genuinely ends as Invalid or Rejected, and any terminal status ends the loop
and leaves the existing check to decide what it means.
2026-09-22 10:40:08 +01:00
Dave Page 475b864e7f Stop the publishing pipeline overwriting what it has already published
Every one of these is a way that a verb meant only to add something would
quietly change something already published, which is the failure the wrapper
exists to prevent, so they are worth having together.

Snapshot retention kept the newest five directories, and a directory is
something either key can create: five dated today with high suffixes filled
the keep set and every genuine snapshot was deleted. It now keeps every
directory whose date is among the newest five that are not in the future, so
retention is counted in days, and the intended consequence is that a day with
several builds keeps all of them.

release-fetch required only that v<VERSION> existed, and shutil.move onto an
existing file replaces it, so a release published months ago could be
overwritten and autoupdate-publish and sync-s3 would then carry the
replacement to the auto-updater and into an archive that has no undo. The
single-shot guarantee that release-create made for the first verb now covers
the whole sequence: it refuses before moving anything if .incoming exists or
if a content directory holds more than its maintainer marker, and refuses each
individual move whose destination is already there.

packages-fetch pulled with no --ignore-existing, so a staging package whose
filename matched a published one replaced it and the following rebuild signed
the new bytes. It now leaves the live copy alone and reports what it skipped,
since a collision during a real promotion means two builds have produced one
filename.

autoupdate-publish deleted the live archives before moving the new ones in,
which left a window with nothing there at all; the order is reversed and only
archives this release did not replace are removed, and once every move has
succeeded. --delay-updates leaves .~tmp~ behind in the live trees when a
transfer is interrupted, and both indexers descend into it, so the holding
directories are swept immediately before indexing rather than moved out of the
tree with --partial-dir, which would make the final rename a copy across
filesystems. DATESTAMP_RE and VERSION_RE gain re.ASCII, because \d matches any
Unicode decimal digit and int() parses those, so a fullwidth datestamp on the
filesystem read back as an ordinary date.

In the workflows, the repository variables naming the SSH target and the key
paths reached option positions, as the first positional argument to ssh and
inside the -e string rsync splits on whitespace. Only an administrator can set
those, so this is defence in depth: they are validated against a conservative
pattern in the step that already checks the keys are present, and the target
is passed after --. snapshot.yml passed the raw inputs.location to its publish
step rather than the validated output of the location job, which agreed only
by coincidence, and it now carries a flag for whether a location was named so
that an empty input still means the assembled directory name.
check-container-build.yml gains the top-level permissions: contents: read the
other workflows have, leaving the publish job's packages: write alone, and
promote-release.yml verifies the wheel's detached signature against the
published signing key before uploading it to PyPI, the mirror having been
trusted on TLS alone until now.

The README and the download server's key annotations are updated to match: the
guarantees they stated about single-shot publication and about retention were
overstated, and are now true.
2026-09-22 10:31:40 +01:00
Dave Page d04e39cc65 Describe the signing host without naming whose rack it is in 2026-09-22 10:24:53 +01:00
Dave Page b7789be4af Bring the Certum PIN handler into the repository
The script that answers the signing card's PIN prompt lived only in the
buildfarm repository, as an Ansible template with the PIN itself interpolated
into it. That repository is going away, and the template could not come with it
as it stood, so this is the same script with the secret taken out and the two
faults that cost a build this morning fixed.

The PIN now comes from a file in the build user's local application data, read
once at startup, so neither this repository nor the compiled executable carries
it; the helper says so and exits if the file is missing or empty, rather than
sitting in the tray looking healthy whilst answering nothing.

The build was lost to a race. The original waited for the dialog and then
activated it as two separate operations, and when the activate lost that race
and threw "Target window not found", AutoHotkey put up its default error dialog
on a machine with nobody to dismiss it, so one transient fault became an
indefinite hang. So the work now happens on a timer rather than in a loop that
can end, activation is retried for a couple of seconds and addressed by window
handle rather than by title, a window that vanishes mid-operation is a log line
and a return rather than an error, and an OnError handler makes it impossible
for anything to put a dialog on the screen at all.

Whilst there, the PIN is sent with SendText rather than Send, which would have
read {, +, ^, ! and # in it as modifiers, and it is only sent once WinActive
confirms the card's own dialog has the focus, because a blind send on a build
machine is a good way to type a PIN into a console whose output gets uploaded.
The title is matched on its leading unaccented substring, which keeps the file
to plain ASCII and survives proCertum wording changes.

SIGNING.md gains the operator's half of that: where the PIN file goes, the
icacls to lock it down, where the log is, and what each thing the log can say
means when a signed build hangs.
2026-09-22 10:24:02 +01:00
Dave Page 97a7e55b96 Close the review's remaining findings in the workflows
The promotion validated its inputs in the promote job, which the registries
scope skips, so under that scope the typed-twice confirmation did not cover
the step that moves the latest tag; validation moves to a job both paths
depend on. The container tag loops no longer glob against the workspace, and
check-container-build validates the location it interpolates into a push tag
in a job holding the Docker Hub credentials.

The acceptance test built its apt sources line with sh -c, which re-parsed the
expanded base URL as source: the one place in the pipeline where a
caller-supplied value reached a shell. The outer shell writes the file now,
and the URL is checked once, against a host allowlist, before any job
downloads and installs what that host serves as root.

The actions in jobs holding registry or trusted-publishing credentials are
pinned by commit rather than by a movable tag, as the SonarQube action already
was, and so is the helm installer piped into a shell beside them.

Also: the README understated the signing verbs, which select by extension
across the tree rather than by anything tied to a build.
2026-09-22 10:00:17 +01:00
Dave Page ff9819e222 Make snapshot retention safe, and stop packages-fetch importing staging's indices
snapshot-purge sorted directory names and kept the last five, so a name that
merely sorted late survived and everything real fell off the end: five
snapshot-create calls with a future datestamp, which the verb accepts up to
two days ahead, were enough to have the next purge delete every genuine
snapshot. Anything dated after today is now left out of the retention set
entirely, and the ordering is by date and then by suffix as a number, which
also fixes 2026-09-22-10 sorting before 2026-09-22-2 and so a day with more
than nine builds keeping the older ones.

packages-fetch pulled staging's Packages files and repodata into the live
tree, whilst the signed Release, Release.gpg and InRelease they are checked
against live a directory further up and were left as they were. Every live
apt repository therefore failed on a hash sum mismatch from the moment the
fetch landed until that codename's rebuild caught up, which with five
sequential rebuilds each taking the lock is minutes. The docstring already
claimed the indices were left behind; now they are.

The pull key is renamed to pga-pull to match the other two.
2026-09-22 09:58:02 +01:00
Dave Page 355d6d099f Name the publishing hosts by role rather than by hostname
The machine names were only ever in comments and two filenames, but the
repository is public and the buildfarm never was, so there is no reason to hand
a reader the mapping from internal name to public service. The two
authorized_keys templates become .staging and .download, and the prose refers
to the staging server, the download server and the runner.

The pull key the download server uses is renamed to match, so it needs moving
on that host: ~pgaupload/.ssh/id_ed25519_paxsor_pull -> id_ed25519_pull. The
runner's own key paths come from repository variables, so the defaults here
changing affects nothing.
2026-09-22 09:49:07 +01:00
Dave Page da0bc3ecd9 Say which platforms read pkg/python-version.txt
Only the Windows and macOS packaging does, those being the two that ship an
interpreter; the Linux packages use the distribution's Python.
2026-09-22 09:38:49 +01:00
Dave Page db2030e9c0 Describe where aptftp.conf lives rather than telling the reader to move it 2026-09-22 09:23:31 +01:00
Dave Page 34cb0be9a2 Make a scheduled snapshot actually publish
Both publishing steps were gated on inputs.publish, which exists so that a
dispatched test run builds without uploading. A schedule event carries no
inputs, so that expression was null rather than false and the nightly built
everything, assembled the tree and threw it away, reporting success. Publish
unconditionally on a schedule, and keep the input for dispatches.
2026-09-22 09:06:35 +01:00
Dave Page ca275cbc8d Let the registry half of a promotion be rehearsed on its own
The container re-tag and the chart push need no download server and touch
nothing that cannot be deleted, but they were reachable only through a real
promotion, which is the one thing we cannot practise. A "registries" scope
skips the promote job entirely and runs just those two, leaving the PyPI
upload out since a rehearsal has no published wheel to upload.
2026-09-21 21:49:13 +01:00
Dave Page c28362013f Give the container publish its own job, on the publishing environment
The Docker Hub credentials are environment secrets, and an environment
secret is not available to a job that does not name the environment.
secrets: inherit passes repository secrets, so the login failed with
'Username and password required' and would have done the same in a real
candidate run.

Splitting it also keeps the credentials away from the build check, which
runs on every pull request and has no business being able to push.
2026-09-21 16:26:02 +01:00
Dave Page b2d1362b8a Let the container build be dispatched with its publish inputs
They were only on workflow_call, so the push path could not be exercised
without running an entire release candidate to reach it.
2026-09-21 16:24:51 +01:00
Dave Page 24a1f24e48 Add a button for purging the website cache
The last thing in the release process still needing Jenkins. Promotion
purges the cache, but the website edits that follow a release happen
afterwards and are all cached, so there has to be a way to purge it on
its own.

No arguments: the verb behind it clears everything, and there is nothing
for a caller to choose.
2026-09-21 16:11:13 +01:00
Dave Page e1915c7dc0 Publish the container, chart and wheel from Actions
The last three things still running from Jenkins, and the reason the
buildfarm could not be switched off.

The container is built for both architectures and pushed under the
candidate's location, then re-tagged at promotion with buildx imagetools
rather than rebuilt, so what ships is the manifest the acceptance test
ran against. It goes to Docker Hub and to GHCR: Docker Hub stays
canonical because every tutorial and compose file in the world names
dpage/pgadmin4, whilst GHCR is organisation owned, needs no stored
credential and is not subject to Docker Hub's anonymous pull limits.

The Helm chart moves to GHCR outright. Its version comes from Chart.yaml
so there is no candidate copy to re-tag, its install base is a fraction
of the container's, and oci://docker.io/dpage was an odd home for a
chart.

The wheel is fetched from the published release rather than rebuilt, so
what reaches PyPI is the file the acceptance test installed, and it goes
up by trusted publishing, which exchanges the workflow's own identity for
a short-lived credential rather than holding an API token.

All three run in their own job on a hosted runner. They need none of the
publishing keys, so the machine that holds those never handles registry
credentials, and a registry outage cannot leave a half-finished
promotion on the download server.
2026-09-21 16:10:13 +01:00
Dave Page 89bb92dc44 Read the staging server's name from the server
It was the last SITE value the repository had to get right, and it was a
fully qualified internal hostname in a public tree.

It is read here rather than accepted from the caller, which is the part
that matters: the client names a verb and a datestamp and never a host,
a path, a binary or a signing key, and that is what stops a compromised
runner asking for a signature over something it fetched from a source of
its own choosing. A workflow variable would put it on the wrong side of
that line. It is validated as a hostname on the way in, since it becomes
part of an rsync remote specification.
2026-09-21 15:35:59 +01:00
Dave Page 42d2e55bcd Write the maintainer marker into published directories
Every directory under a release carries a CURRENT_MAINTAINER naming the
support address, which the PostgreSQL mirror network expects and which
the buildfarm wrote with an echo in each of nine job definitions. The
rehearsal produced a release directory without them.

It is a file beside the scripts now, so changing the support address is
one reviewed commit.
2026-09-21 15:30:49 +01:00
Dave Page f57e46c7c4 Fetch the release downloads, and keep the rehearsal confined
Two faults the first working rehearsal exposed.

rsync without -r reports 'skipping directory .' and transfers nothing,
so release-fetch created the directory, sorted no files and reported
success. The comment claiming -r was omitted on purpose was simply
wrong; the trees are excluded by pattern instead.

The auto-updater archives were being moved by release-fetch, which
writes them outside v<VERSION>. That contradicted the whole point of the
release-directory scope, which is that deleting one directory undoes it.
They are now autoupdate-publish's job, and it runs with the packages,
where the promotion stops being reversible anyway.
2026-09-21 15:28:58 +01:00
Dave Page 9a68105260 Name the staging server in full
PULL_HOST carried the short name, one of the SITE values never confirmed
against the servers, and the first real release-fetch failed on it. The
firewall now permits the pull and the host key is recorded on the
download server under both names; the fully qualified one is used here so
that what this file names and what known_hosts records cannot drift.
2026-09-21 15:27:06 +01:00
Dave Page 299a767b1f Make the promotion scope an explicit choice
Three settings rather than a boolean, because the steps differ in how
undoable they are and that is what somebody needs to choose between.

release-directory writes only /var/ftp/pgadmin4/v<VERSION>, so deleting
that directory undoes it completely; it is the only setting that leaves
the live repositories alone, and so the only one that can be rehearsed.
files goes on to copy packages into the production trees and re-sign
every index clients read. everything adds the website and the S3
archive, neither of which can be undone.

The default is the safe one, on the grounds that a full promotion should
be a deliberate choice rather than what happens if nobody reads the
form.
2026-09-21 15:09:03 +01:00
Dave Page fc6815ae70 Allow a promotion to stop after the files
The website update and the S3 archive are the two steps with no undo:
one writes to the website database, the other copies into a bucket whose
sync carries no --delete. files-only skips both, so a promotion can be
rehearsed against a throwaway version and the files deleted again before
the hourly sync carries them to the mirrors.
2026-09-21 15:06:24 +01:00
Dave Page 456b2ddefa Promote a tested candidate to the download server
The last piece, replacing pgadmin4-all-publish-qa-to-prod and the deb
and rpm promotion jobs, plus update-download-archive and
pgadmin4-website-create-release.

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: the release directory,
the downloads sorted into it, the packages into the live repository
trees, then the indices, the READMEs, the website and finally the S3
archive. The order is deliberate, since an interruption before the
indices leaves packages nothing points at, which is recoverable, whereas
the reverse is a repository describing packages that are not there.

Two changes to the wrapper it depends on.

release-fetch assumed the staging build was divided into docs, macos,
pip, source and windows. It is not: a staging build is flat, and it is
promotion that sorts the downloads into those directories, which the
buildfarm did with a series of scp commands carrying shell globs. That
sorting is now a table, the destination comes from each file's own name,
and an unrecognised file stops the publication rather than being dropped
silently. It also handles the auto-updater ZIPs, which replace the
previous release's rather than accumulating beside them, and which
nothing here had carried across at all.

rebuild-readme is new, and closes the gap left when the README generator
went in: nothing regenerated the production platform tables, only the
staging and snapshot ones. Those carry the release history, so they
change with every release.
2026-09-21 15:04:21 +01:00
Dave Page 9039065ee0 Check Gatekeeper before codesign --strict on the disk image
The macOS jobs failed on 'Disallowed xattr com.apple.FinderInfo', which
is not a signature problem: _create_dmg runs SetFile -a E on the bundle
to hide the .app extension in the disk image window, and that writes the
attribute --strict refuses to see.

What decides whether a user can open the application is Gatekeeper, so
spctl and stapler run first and are the pass or fail. --strict still
runs, and reports rather than fails, so that a future signature problem
is still visible and distinguishable from the attribute we put there
deliberately.
2026-09-21 14:53:08 +01:00
Dave Page 620f7b8064 Check the Windows, macOS and direct downloads too
The apt and yum halves proved a user can install from the repositories.
These cover everything else a user can download.

Windows is the most valuable, because it closes a gap rather than
confirming something already checked: nothing had ever run the installer,
and nothing verified the Authenticode signature on the pgAdmin4.exe
inside it. Make.bat's own check tested a stale %ERRORLEVEL%, and the
workflow step that claimed to cover it re-verified the installer instead.
That binary does not exist outside the installer until the installer has
run, so this is the only place it can be verified.

macOS mounts the published disk image rather than inspecting the bundle
that was just built, with the quarantine attribute a browser would set so
Gatekeeper is actually consulted. It also runs otool over the bundled
client binaries and fails if any still points at /opt/pgbuild, Homebrew
or /usr/local, which is what a dependency that was linked but not
relocated looks like.

The rest is deliberately shallow: the wheel is installed into a venv and
imported, the tarballs are checked for a couple of known paths, the PDF
and ePub are checked to be those formats and not truncated, and every
detached signature is verified from the published URL rather than on the
server, since that is the copy a user actually fetches.
2026-09-21 14:48:54 +01:00
Dave Page 3cd04b405e Do not use grep -q under pipefail
Both halves installed correctly and then failed the assertion that they
had: grep -q exits on the first match, dpkg -L and rpm -ql die of
SIGPIPE, and pipefail reports the pipeline as failed. The packages do
ship files under /usr/pgadmin4, as dpkg-deb -c on the published .deb
confirms.
2026-09-21 14:43:08 +01:00
Dave Page 8057624259 Correct two faults in the acceptance test itself
setup-web.sh ships in pgadmin4-web, not pgadmin4-server, so the apt jobs
installed correctly and then failed on an assertion about a file that
package never contained. The check is now that the package delivered
files under /usr/pgadmin4, which cannot be wrong about which package
owns what.

The yum jobs never got as far as installing: dnf install curl conflicts
with the curl-minimal already in those images, and curl was not needed
in the first place.
2026-09-21 14:40:47 +01:00
Dave Page 750d0630e0 Run the acceptance test steps under bash
The containers default to sh, which has no pipefail, so every step died
on its first line with 'Illegal option -o pipefail' before testing
anything.
2026-09-21 14:38:16 +01:00
Dave Page c23172af37 Install from the candidate, as a user would
Everything else checks that files exist, that indexes parse and that
signatures validate. All three can be true of a repository apt still
refuses, and both bugs the migration turned up were that shape: packages
one directory too deep, and noarch RPMs filed where no dnf client looks.
Neither failed a build.

So this inspects nothing. It follows the instructions in the repository
README, in a clean container, on all five apt platforms and all four yum
ones, and installs the package. No --allow-unauthenticated, no
--nogpgcheck, no pinning, so a wrong signature or an architecture missing
from Release fails the job.

It runs against a candidate rather than a snapshot because the
development server publishes its tree immediately, whilst the download
server sits behind an hourly sync to the mirrors that would otherwise be
most of what the test measured.

Promotion stays a human decision, so this gates nothing automatically; a
red result is simply a candidate that must not be promoted.
2026-09-21 14:36:09 +01:00
Dave Page 458b24b5f8 Do not write a literal ${{ }} in a comment
GitHub evaluates workflow expressions inside run blocks before the shell
sees them, comments included, so an empty one is a parse error: the
whole file is rejected with 'An expression was expected' and the
workflow cannot be dispatched or scheduled at all.

snapshot.yml has carried this since the review fixes landed, which would
have failed tonight's nightly, and release-candidate.yml inherited it.
2026-09-21 14:32:45 +01:00
Dave Page 4dc7a7bd7b Add release candidate builds, publishing to the development server
Phase one of moving the release pipeline off the buildfarm, replacing
the pgadmin4-*-build jobs. The same builds as the nightly snapshot,
calling the same workflows, but published to procyon through the
wrapper's staging verbs rather than to the download server.

The difference is what the tree is for. A snapshot is finished when it
is published; a candidate is published so that it can be tested, and
promoted afterwards. developer.pgadmin.org serves the staging tree over
HTTP the moment it is written, so a candidate's repositories can be
added and installed from immediately, whereas the download server sits
behind an hourly sync to the mirrors.

Never scheduled, and publishing is off unless asked for, because a
candidate exists only because somebody decided to make one.

Needs a STAGING_SSH_TARGET variable on the publishing environment; the
keys and the wrapper are already deployed on procyon.
2026-09-21 14:32:45 +01:00
Regina Obe 6312baa34e Fix dynamic type function not receiving correct state for collection row fields (#10398)
When a schema field uses a dynamic type function (type as a function)
and is rendered inside a collection (e.g. chain tasks), the function
always received the top-level schema data instead of the row-level
data. This caused the field to fall through to its default type,
ignoring the actual field values in the row.

Resolve the type callback's state against the field's own container
instead, mirroring what cell() already gets via its row argument: for a
field inside a collection row that is the row, and for a top-level
field the parent path is empty, so the whole schema data comes back as
before. Nested tabs and fieldsets carry no id, so they do not extend
the access path and are unaffected.

As well as the pg_timetable case that prompted this, it repairs the
expanded-row Definition forms that were already relying on row state
and silently getting the table's: the foreign key 'Referencing'
column list, the partition name control's attach/create switch, and
the column dialog's identity/generated constraint options.
2026-09-21 13:15:33 +01:00
Hari Prasad 5f56805162 Stop the Query Tool spinner whilst a password prompt is open (#10436)
When restoring saved Query Tool tabs, or switching to a connection with no
saved password, the backend replies with HTTP 428 and the 'Connect to server'
dialog opens, but obtaining_conn was left set to true whilst waiting for the
user. ConnectionBar renders MUI's CircularProgress from that flag, so every
affected tab kept two infinite CSS animations running at the display refresh
rate, costing around 30-35% CPU whilst completely idle.

obtaining_conn is now cleared when the 428 arrives and set again only whilst a
connection request is actually in flight, with a promptCallback threaded
through connectServer() so that password retries behave the same way. Along the
way this also fixes cancellation leaving obtaining_conn stuck on with the
Promise never settling, the New Connection dialog closing itself on a failed
connection rather than reporting the error, and duplicate entries being added
to the connection list when a role was an empty string rather than null.

Fixes #10386
2026-09-21 13:15:03 +01:00