NOTE TO PACKAGERS: A new distribution tarball is available which simplifies the build process by providing pregenerated HTML documentation, man page, and code. The tarball is attached to each release as an asset and is named pgbackrest-{version}.tar.gz. See the README.md in the tarball for details. Please use the new distribution tarball to avoid the additional build tooling required to generate code in future releases.
NOTE TO PACKAGERS: There is a new optional dependency on libsystemd.
IMPORTANT NOTE: Starting with this release, only the restore command may be run as root by default. Use allow-root to run other commands as root (though this is not recommended).
Bug Fixes:
* Fix expire removing a backup in progress on another host. (Reviewed by Douglas J Hunley. Reported by Dmitrii.)
* Fix archive-push-queue-max not enforced when a WAL file errors. (Reviewed by Stefan Fercot. Reported by vut12.)
* Fix async archive-get failure during recovery across a timeline switch. (Reviewed by Douglas J Hunley. Reported by Jimmy Yih.)
* Fix potential buffer overrun in error module. (Fixed by Christophe Pettus. Reviewed by David Steele.)
* Fix use-after-free of parallel protocol client. (Reviewed by Douglas J Hunley. Reported by Georgy Shelkovy.)
* Fix heap overflow when a pg option is set without pg1. (Reviewed by Douglas J Hunley. Reported by Naveen.)
Features:
* PostgreSQL 19beta2 support. (Reviewed by Stefan Fercot.)
* Add archive-expire-before option to clean up WAL archive. (Contributed by Stefan Fercot. Reviewed by David Steele.)
* Add batch delete for Azure storage. (Reviewed by Douglas J Hunley, Crispy.)
* Add support for S3 Outposts. (Contributed by Shiva Kumar Ambigi. Reviewed by David Steele, Roberto Mello.)
* Add S3 process authentication. (Reviewed by Andrew Charlton. Suggested by Andrew Charlton.)
Improvements:
* Reconnect SFTP storage after the server drops an idle connection. (Reviewed by Stefan Fercot. Suggested by mustafa0x, tisserat.)
* Add user/group caching for faster manifest build. (Contributed by Gunnar Lindholm. Reviewed by David Steele.)
* Add per-repo backup progress to info command output. (Contributed by Will Morland. Reviewed by David Steele, Stefan Fercot.)
* Add backup.info checks to verify command. (Contributed by Denis Garsh. Reviewed by David Steele, Douglas J Hunley.)
* Allow the S3 STS endpoint to be configured. (Contributed by Simon Gratton. Reviewed by David Steele.)
* Add archive-push-batch-size to limit WAL pushed per asynchronous run. (Reviewed by Stefan Fercot.)
* Exit async archive-push on first error. (Reviewed by Stefan Fercot. Suggested by Lardière Sébastien.)
* Harden HTTP chunked response parsing. (Contributed by Shubham. Reviewed by David Steele.)
* Error when running as root unless allow-root is enabled. (Reviewed by Douglas J Hunley.)
* Fewer binary searches when building manifest. (Contributed by Gunnar Lindholm. Reviewed by David Steele.)
* Improve seek performance during block incremental delta restore. (Reviewed by David Christensen, Douglas J Hunley.)
* Bundle backup files in order rather than scanning for a better fit. (Reviewed by Stefan Fercot.)
* Report the underlying error when a query fails to complete. (Reviewed by Andrew Pogrebnoi. Suggested by Marco Fontana.)
* Report the actual error when an S3 credential request fails. (Reviewed by Douglas J Hunley. Suggested by Mitchell Grice.)
* Report archive-push spool path errors to the PostgreSQL log. (Reviewed by Douglas J Hunley. Suggested by Don Seiler.)
* Hint that pgBackRest may be out of date when a version is unsupported.
* Add systemd notify integration. (Contributed by Andrew Jackson. Reviewed by David Steele.)
* Suppress unused parameter errors in meson compiler probes. (Contributed by Jörg Plate. Reviewed by David Steele.)
* C11 is now the minimum C standard. (Reviewed by Mohammad Ali Nazir Kosar.)
Documentation Improvements:
* Make stanza option internal for the repo-* commands. (Reviewed by Stefan Fercot.)
* Document that info reads encryption settings from global section. (Reviewed by Stefan Fercot. Suggested by Ron Johnson.)
* Document that expire-auto uses the backup command configuration. (Reviewed by Stefan Fercot. Suggested by Lardière Sébastien.)
* Document adjusting the logrotate su directive for a dedicated user. (Reviewed by Douglas J Hunley. Suggested by lkanbus.)
* Document that end-of-line comments are not supported in config files. (Reviewed by Douglas J Hunley. Suggested by Alex Richman.)
Test Suite Improvements:
* Fix Alpine group conflicts in CI containers. (Contributed by Artur Zakirov. Reviewed by David Steele.)
A markdown link that specified both a page and a section failed to render when the target page was itself rendered as markdown, because markdown cannot reliably anchor into another generated file.
Resolve these links to the website URL with the section anchor, matching how links to pages that are not rendered as markdown already behave, so a markdown document can link to a section of another.
The site now serves over HTTPS at the apex domain, so switch from http://www.pgbackrest.org to https://pgbackrest.org.
Change the backrest-url-base variable that generates the documentation links and the URL hardcoded in the man page SEE ALSO section, then regenerate README.md and CONTRIBUTING.md and update the doc build test to match.
NEWS.md is the release announcement copied to a postgresql.org news item. It was maintained by hand for each release, but that content now lives in news.xml and is rendered to NEWS.md by the documentation build.
Remove the committed file and add it to .gitignore so it is treated as generated output and regenerated when an announcement is needed.
The distribution tarball ships the generated source and the rendered documentation so it builds without the code generation or documentation tooling that a git checkout requires. Add a workflow that exercises this contract: build the tarball, then on a range of distributions build and smoke test it in a fresh container that has only the required packages, which is both a test and a record of what a packager needs on each.
The tarball is built once, since it is distribution-independent by design, and uploaded as an artifact; a matrix job then consumes it per distribution and drives the built binary through a full backup, restore, and verify cycle against that distribution's native PostgreSQL. The containers are run with docker directly rather than as job containers because the runner's glibc Node.js cannot execute the checkout and artifact actions inside a musl container, which keeps one path for glibc and musl alike.
Debian, Ubuntu, Rocky, Fedora, and Alpine are covered, and the tarball is built with -Dwerror so a compiler warning on any of them fails the build. A separate check confirms the tarball ships exactly the expected generated and rendered files and that none of them is empty, catching a broken code generation or documentation render before it is uploaded.
Also remove the smoke job from the misc workflow, since building and smoke testing the tarball now covers the check a packager would run, and move its format check for smoke.py, the only Python file, into the code-format job with the other formatting checks.
The bsd test group cannot run the integration tests because there are no containers available, but the smoke test drives a backup/restore cycle against each installed PostgreSQL and so provides basic coverage on these platforms. Teach the smoke test the Homebrew and FreeBSD bin paths so it can find the installations there.
Discover the PostgreSQL versions to test at install time rather than naming them so the tests do not break when a version becomes unavailable. Read the supported versions from the generated header, intersect them with what the platform offers, and test the oldest and newest of the result.
On MacOS only keg-only formulae that are neither deprecated nor disabled are considered since the binaries in a formula that is not keg-only load libpq from the linked prefix and fail unless the formula is linked.
On FreeBSD the ports all install into the same prefix and conflict, so only one version can be installed at a time and the oldest goes on the older OS while the newest goes on the newer OS. Install the versioned client and server packages instead of postgresql-libpqxx, which depends on the default PostgreSQL version.
Now that a persistent ccache has made compiling under emulation affordable, broaden the tests run there to better check that checksums are correct on big-endian. Add the command restore, archive-get, archive-push, and verify tests alongside backup, and the common type-pack, crypto, and compress tests, which exercise the serialization, checksum, and compression code that a byte-order bug would break.
Also add a smoke test that drives a real PostgreSQL through a full backup, restore, and verify cycle, checking checksums end-to-end rather than against synthetic harness data. This needs a PostgreSQL server, so install a single version in the base image for these architectures; the full version matrix is skipped to keep the emulated build fast.
Consolidate the per-vm and per-vm-index ccache directories into a single cache shared by the host and every container, passed with CCACHE_DIR instead of mounted at ~/.ccache. A file compiled in one place is then a hit in the others, e.g. test-pgbackrest is built both on the host and in the build container, and since ccache is safe for concurrent access there is no need to split the cache. It stays in the test path so --clean removes it.
Store and restore that cache in GHCR via a new composite action. Unlike actions/cache a registry is not branch-scoped, so a short-lived branch starts warm from the last run on the fork rather than cold, which matters most for the emulated architecture jobs where compiling is slow. The action pulls the fork's own cache first and falls back to the canonical pgbackrest cache for a fork that has not run yet; the push is best effort so pull requests from forks that cannot write just use what they built.
The smoke test now reads the ELF header of the pgbackrest binary being tested and prints its machine architecture and endianness, e.g. "s390x (64-bit big-endian)", right after the user it runs as. This makes it easy to confirm which architecture is actually being exercised, which matters when a binary built for a foreign architecture is run under emulation.
test.yml had grown large enough to be awkward to navigate, but splitting it into independent workflows would have produced a separate run in the Actions UI for each file and required the push/pull_request trigger to be duplicated in every one.
Instead test.yml becomes a dispatcher that calls reusable workflows, so it remains a readable index of what runs while still producing a single run. Only test.yml has a trigger and the called workflows are all on: workflow_call. Permissions are granted in the dispatcher since a called workflow can only maintain or reduce the permissions of the token it is given, never elevate them.
The jobs are grouped by platform into linux, arch, and bsd, with doc for the documentation builds and misc for the checks that do not fit any of them.
The documentation builds are worth separating because they have little in common with the tests they were mixed in with. They do not use the test containers, so they need no GHCR login or packages: write permission to maintain the container cache, and they generate no coverage, so there is no report to upload on failure. The build-max parameter was already ignored for doc since ci.pl only passes parameters through on the test path, so it is not carried over.
Job names gain the calling job as a prefix, e.g. linux / test, which is the cost of getting a single run.
Also move symbol.out into a data directory since it is not a workflow and GitHub only scans the top level of .github/workflows.
These instructions are long out of date and it does not seem worth updating them since packagers should now be using the pre-generated documentation in the distribution tarball introduced in 8fcfef5d.
The user guide build section demonstrates building from the distribution tarball but the download command still points at the GitHub source archive url for the release tag. That tarball is just the repository contents, so it has neither the generated files nor the dist marker, and building it requires the code generation tooling that the documented dependencies do not include. It also unpacks to pgbackrest-release-<version> rather than the pgbackrest-<version> path that build-br-path has used since the distribution tarball was introduced.
Point github-url-release at the distribution tarball attached to the release instead. The release tag contains a slash, e.g. release/2.59.0, which must be percent-encoded as %2F in the asset download path.
The url renders on a single line of 110 characters, which is why the download command carries a cmd-line-len override.
This was missed in 8fcfef5d.
The documentation build errors when a rendered command has a line longer than cmd-line-len, which is configured per document and defaults to 80. The check keeps commands readable in the rendered output, but a command containing a single token longer than the limit cannot satisfy it no matter where the line is broken, e.g. a url.
Add a cmd-line-len parameter to execute that overrides the document setting for a single command so the limit continues to apply to every other command.
curl is already used elsewhere in the documentation and is preinstalled on the container images, while wget is not installed on any of them by default. Standardize on curl.
Update the systemctl replacement and release tarball downloads in the user guide to use curl -fsSL. This is equivalent to the wget flags it replaces, except that -f makes curl fail on an HTTP error rather than piping the error page into tar.
Remove wget from the RHEL documentation container instead of replacing it with curl, since curl is already provided by curl-minimal. Installing the curl package there would fail because it conflicts with curl-minimal.
Remove wget from the test containers entirely. Nothing has invoked it since the lcov, nodejs, pip, minio, and s3 server downloads that needed it were removed. The PGDG repository keys it once fetched are now retrieved by rpm/dnf directly on RHEL, and on Debian the key is shipped with postgresql-common.
Move to the last release of the 1.5 branch to pick up a year of fixes.
The 1.7 branch is where upstream development continues, but it lowered the default log level from ERROR to NOTE, which surfaces a warning about postgresql-{[pg-version]}.service using /bin/kill for ExecReload. The warning is not new, it was simply below the log level before, and 1.7 offers no way to suppress it since --no-warn is accepted but ignored and -c is applied after the log level is set.
The runner image comes with a large amount of preinstalled software that cannot be controlled, which makes it a poor stand-in for the environment a packager builds in. Run the job in a minimal ubuntu:24.04 container instead so the smoke test runs with nothing but the build environment that the job explicitly installs.
Install the build environment before checkout since the container does not include git, which checkout requires in order to clone rather than download the code via the GitHub API. Also add build-essential for the compiler and ca-certificates/curl/gnupg/lsb-release for the PGDG script, all of which were previously provided by the runner image.
Since the container runs as root, the smoke test now drops to the postgres user, which exercises a path that was not covered when the job ran as the runner user.
The GitHub runner image adds taps such as aws/tap but never trusts them, so Homebrew 6.0 tap trust emits a warning on every brew command that auto-updates. Untap everything except the official Homebrew taps since no formula used in test comes from a third-party tap.
Uninstall the packages provided by a tap before untapping it. The runner image installs packer from hashicorp/tap and bicep from azure/bicep, and untapping a tap that still has packages installed warns just as the trust check does.
The run parameter of cross-platform-actions/action is deprecated in favor of starting the VM in one step and running commands in subsequent steps with the cpa.sh custom shell.
Bump the u22 revision on x86_64 and aarch64 so the base images are rebuilt and pick up the new beta from apt.postgresql.org. Key the revision by architecture rather than by VM so the ppc64le and s390x images, which do not install PostgreSQL 19, are not rebuilt under emulation for no reason.
Since there are no code changes beta1 continues to work.
Both tests only report the major version of PostgreSQL, which is not enough to identify the build. The integration test takes the major version from the test matrix and the smoke test parses it out of pg_ctl during discovery, so neither shows the minor version. This matters most during the beta period, where a rebuild of a container or a package update can move the installed version from one beta to the next without anything in the test output changing.
Log the output of pg_ctl --version in both tests, e.g. "pg_ctl (PostgreSQL) 19beta2". The integration test logs it after the hosts are built, so the version is reported even when cluster creation fails. The smoke test logs it under the header for each version it tests.
Use pg_ctl rather than postgres since the tests already require pg_ctl to run a cluster, so no error handling is needed when it cannot be found. The postmaster symlink is not an option because it was removed in PostgreSQL 16.
A base image is built per VM and architecture but a revision could only be keyed by VM, so bumping one rebuilt every architecture. This is expensive for u22, which is built for x86_64, aarch64, ppc64le, and s390x, since the last two are built under emulation in CI and are very slow. A new PostgreSQL beta packaged for only some architectures does not change the others, so those rebuilds just reproduce identical images under a new tag.
Allow a revision to be keyed by VM and architecture, e.g. u22-x86_64. A container uses the revision for its VM and architecture if it has one, else the revision for its VM, else the required "all" revision. Keying by VM alone is unchanged and still rebuilds every architecture.
Validate the revision keys. Only the presence of "all" was checked before, so a key with a misspelled VM or architecture, or with no value at all, silently fell back to a less specific revision and the expected rebuild never happened. Add VM_ARCH_PPC64LE, VM_ARCH_S390X, and a list of valid architectures to support the check, since these architectures were previously named only in the CI workflow.
The test suite is too complex and time-consuming for packagers to run, so provide a basic smoke test that exercises major functionality to show that a build works. test/smoke.py searches the paths used by the test harness for each PostgreSQL installation and runs the built pgbackrest through a full cycle for every supported version found: initdb, stanza-create, check, full backup, incremental backup, restore, and verification that the restored data matches. Each version runs in a temporary directory with its own repository and a private unix socket, so a running cluster is not disturbed.
Run the test with meson test --suite smoke. It uses only the Python standard library since meson already requires Python. The test fails rather than skips when there is no PostgreSQL to test against, since a test that silently does nothing does not verify the build. PostgreSQL will not run as root, so running as root drops to the postgres user and fails when that user does not exist.
The supported versions are read from the generated src/postgres/version.auto.h so that versions pgBackRest does not support are skipped rather than failing. The header is used rather than src/build/postgres/postgres.yaml because the latter is not shipped in the distribution.
The test is defined in test/meson.build rather than the root meson.build because unit test builds comment out all subdirs, which excludes the smoke test from them. .gitattributes ships test/smoke.py and test/meson.build in the distribution tarball, and DIST.md and the user guide document how to run it.
Add a CI job that builds on Ubuntu 24.04 and runs the smoke test against every supported version, installed from PGDG with a component added for versions that are not released yet. The version list comes from postgres.yaml so it does not need to be maintained in the workflow. The job also checks that smoke.py is formatted with black.
The message read "expected mode for '0644' for 'src/file.c' but found '0755'", where the extra "for" made the expected mode read as a file name. Drop it so the message reads "expected mode '0644' for 'src/file.c' but found '0755'".
On failure the coverage report was written to the job log with cat, which meant copying the HTML out of the log and into a file before it could be viewed. Upload it as an artifact instead so it can be downloaded directly from a failed run.
The upload ignores a missing report (most matrix jobs run with no coverage) and skips the zip archive since the report is a single HTML file.
meson dist produces the tarball via src/build/dist.sh, which regenerates help.auto.c.inc and interface.auto.c.inc, copies the man page and HTML into doc, and writes a dist marker to the root. When the marker is present the build uses the shipped files and skips build-code, libyaml, and the doc and test tools, so the tarball compiles and installs without the code generation or documentation tooling.
.gitattributes export-ignore rules limit the tarball to what is needed to build: src (except the src/build code generation sources), meson.build, meson_options.txt, and LICENSE, plus the generated files, docs, marker, and a README copied from doc/DIST.md.
RELEASE.md documents building the tarball, attaching it to a draft release, verifying it, and publishing. The user guides demonstrate building from the tarball.
Add the logo, news, and sponsor document variables, all defaulting to y so the rendered documentation is unchanged. Setting a variable to n omits the corresponding content: logo omits the project logo file and its og:image link, news omits the news section, and sponsor omits the sponsor sections and their logos.
This allows the documentation to be rendered without the logo, news, and sponsor content that is only relevant to the website.
The harness modules were the only files in test/src/common and each was named with a harness prefix, e.g. common/harnessLog.c. Move them to test/src/harness and drop the prefix so the file name matches the module it supports, e.g. harness/log.c.
Renaming in place would leave harness headers such as log.h, lock.h, and time.h at the same include path (common/) as their real src/common counterparts. Because test/src precedes src on the include path, the harness header would shadow the real one, so the harness must live under a distinct harness/ path.
Adjust the harness path construction in command/test/build.c, normalize the include guards to a TEST_HARNESS_ prefix (fixing blockIncr.h, which incorrectly used BLOCK_DELTA), and update the harness file references in the contributing documentation.
Write the compressed help data as a single C string literal rather than a list of byte values so it is much more compact, which will matter if help.auto.c.inc is checked in as an asset. Printable bytes are written directly, bytes with a short named escape are mapped through a lookup table, and the rest use three-digit octal escapes.
Physical lines are broken at a fixed width with a backslash-newline continuation, which the preprocessor removes before the string is tokenized. This lets an escape span a line break so every line stays the same width.
The data is not NUL-terminated, so the array is declared VR_NON_STRING and sized to exactly match the data.
Several test modules defined the harness key more than once in a single test, either a bare name alongside a map or multiple maps, which relies on the YAML parser accepting duplicate mapping keys. Combine these into a single YAML list per test so a test can declare multiple harnesses without duplicate keys, which a stricter parser rejects. Modules with a single bare name or map are left unchanged.
Update the define parser to accept a harness sequence in addition to the existing bare name and single map forms; the parsed data structures are unchanged. Only the C parser consumes the harness and shim keys, so no other parsing is affected. Update the harness test fixture to the list form to cover the new sequence branch.
The code counts were informational only and required ongoing upkeep of file-type.yaml, so remove the --code-count option, the CodeCountTest module, the generated file-type.yaml, and the related release steps. This can be restored later if it proves useful.
During recovery PostgreSQL calls archive-get serially, once per WAL segment and each invocation with its own exec id. When async archive-get needs to fill the spool queue it takes the archive lock, releases it, and forks a detached async process that reacquires the lock at startup. The lock is released before the forked process has taken it.
A main process returns to PostgreSQL as soon as it finds its segment in the spool, which happens immediately when a prior async process has already fetched it, so PostgreSQL starts the next archive-get before the just-forked async process has reacquired the lock. That next main process finds the lock free and forks an async process of its own. The two async processes race for the lock; one takes it and the other, holding a different exec id, fails to acquire it and dies before it can fetch the requested segment.
The main process that forked the failed async process waits out archive-timeout and returns an error to PostgreSQL. This is especially damaging on a timeline switch because the failed async process was the only one that would have fetched the requested segment; the surviving async process is filling a different timeline. A false timeout on a segment that actually exists can end recovery prematurely.
When the requested segment is not in the spool queue and the archive lock is not held by our own async process, the segment will not be delivered: either a foreign async process holds the lock (spawned by a different main process, e.g. filling the queue for another timeline) or no async process is running because ours failed. In that case fetch the segment synchronously rather than waiting. The main process now waits for an async process only while its own async process holds the lock.
Add RHEL 9 and RHEL 10 (Rocky Linux) as test VMs that run the unit and integration tests, and switch RHEL 8 to the older PostgreSQL that ships natively with the distribution via an application stream. The native packages install to /usr/bin rather than the PGDG path, which the psql-bin setting and the integration host harness now handle.
RHEL 10 required several adjustments: its rpm-sequoia OpenPGP backend rejects the SHA-1 signature on the PGDG key (and the crypto policy that would allow it was removed), so the PGDG repository is installed without gpg checks; it dropped dnf modularity, so there is no postgresql module to disable; and its zlib-ng build trips a valgrind "possibly lost" false positive that is suppressed.
Base images were pulled from Docker Hub by looking up a hash of the generated Dockerfile in container.yaml, which required manually building the image, pushing it, and committing the new hash whenever the Dockerfile changed. Instead cache the base images in GHCR keyed by a hash of the Dockerfile and a revision, pulling on a hit and pushing after a build so the manual steps are no longer needed.
Images are pulled from the pgbackrest cache first, then from the repository owner's own cache when running on a fork, and built locally if neither has them. A build is pushed back to the fork's own cache on a fork, otherwise to the pgbackrest cache, but the push is best effort and does nothing without write access, as when building locally or on a pull request from a fork. The image is always tagged in the pgbackrest namespace locally so the rest of the build and the integration harness reference a single name regardless of which cache supplied it.
The revision comes from container.yaml and forces a rebuild when the Dockerfile is unchanged but the upstream packages have changed, e.g. a new PostgreSQL beta. The required "all" revision applies to every container that does not set its own revision, so bumping it rebuilds those containers together, while a per-VM revision overrides "all" for that container and is bumped independently. The revision is also included in the image tag for reference.
Grant package write permission and add a GHCR login to the CI jobs that build containers, and update the integration harness to reference the GHCR image name.
Security scanners flag strcpy() as unsafe because it performs an unbounded write and silently overflows the destination when the source is longer than expected. Every remaining use already knows the exact length being copied, so replace each with a bounded memcpy() (or snprintf() in the test harness, matching the adjacent idiom).
Poison strcpy() and strcat() in build.h so any future use is a compile-time error that will be caught by CI. There are no strcat() uses to remove, but ban it alongside strcpy() as it has the same unbounded-write hazard. string.h is included and the macros are undefined first for the same reason as strncpy(): fortify defines them as macros, and system headers must be processed before the identifier is poisoned.
Security scanners flag sprintf() as unsafe because it performs an unbounded write and silently overflows the destination when the formatted output exceeds it. There is a single use left, in the log test, so remove it to avoid the noise.
The stderr check builds an expected string into a fixed 4096-byte buffer; use snprintf() with sizeof(buffer), which is bounded and always null-terminates, matching the idiom already used elsewhere in the tests.
Poison sprintf() in build.h so any future use is a compile-time error that will be caught by CI. stdio.h is included and the macro is undefined first for the same reason as strncpy(): fortify defines sprintf() as a macro, and system headers must be processed before the identifier is poisoned.
yamlEventPeekIs() replaces the yamlEventPeek(yaml).type == X test and yamlMapEmpty() replaces the map-begin/map-end pair used to consume an empty map value. Apply them in the config, error, and postgres build parsers and the test define parser.
The value was assigned the result of strLower(strDup()), a non-NULL String pointer, into the boolean parameterAllowed field, so it was always true whenever the key was present regardless of the value. The expression was copied from the adjacent log-level-default definition (which is a string) and never changed to yamlBoolParse().
This does not change the generated output since config.yaml always sets parameter-allowed to true, but parameter-allowed of false would previously have been parsed as true.
The config, error, and postgres build parsers walked the libyaml event stream by hand, repeating the same begin/next/do-while scaffolding and empty-collection special cases in nearly every function. Reuse the YAML_MAP_BEGIN/YAML_SEQ_BEGIN iteration macros (already used by the test define parser) so each parser reads more like the structure it parses.
Rework the macros to test for the end event with a peek at the top of the loop rather than a do/while at the bottom. This makes empty maps and sequences zero-iteration and removes the manual empty-collection handling the config parser required, since config.yaml contains many empty {} maps while the other build YAML never does.
Also drop the braces around single-statement if/else branches in the parser dispatch chains, which did not match the surrounding style where braces are used only for multi-line bodies.
End-of-line comments have never been supported by the INI parser, though they may appear to work for some option types by coincidence. Clarify in the documentation that only full-line comments are supported.
Packages may install a logrotate configuration that rotates the pgBackRest logs as a specific user (e.g. postgres) via the su directive. When a dedicated user owns the repository and logs, the su directive must be updated to match that user or logrotate will fail with a permission error.
Running as the root user for commands other than restore can create files (e.g. backup.info in the repository) owned by root that the PostgreSQL user cannot access, causing later commands to fail.
Add the allow-root option to gate this. It defaults to false for all commands except restore, which is designed to run as root and manages file ownership carefully. The check applies to all command roles so a non-root command cannot run as root on a remote, and is skipped for help/version.
When a client's job callback returned no more work, protocolParallelProcess() freed the client with protocolHelperFree() but left the now-dangling pointer in clientList and did not mark the slot done. Since process() keeps being called while other clients are still running, the same slot was revisited on a later call and protocolHelperFree() was invoked again on the freed client. At debug/trace log level the entry parameter log formatted the freed client via protocolClientToLog(), reading freed memory and segfaulting (reported during a parallel backup run with --log-level-file=trace).
This was introduced in 4a94b6b, which replaced protocolLocalFree(clientIdx + 1) (logging a scalar process id) with protocolHelperFree(client) (logging a dereferenced client pointer), turning the pre-existing repeated free into a use-after-free.
Clear the client reference after freeing so the exhausted client is neither queried nor freed a second time.
In async mode the spool out path was created only by the async process. If it could not be created (e.g. permission denied) then the async process also could not write its error status file to the spool, so the foreground process never saw the error and failed only with a generic archive timeout that pointed at the async log. The operator then had to cross-reference a second log file to find the real cause.
Create the spool out path in the foreground before forking the async process, as archive-get already does. A permission error is now thrown in the process PostgreSQL invokes and appears in the PostgreSQL log immediately. Since the foreground always creates the path before the async process runs, the redundant create in archivePushProcessList() is removed.
Web identity (web-id) and pod identity (pod-id) credential requests parsed the response as if it had succeeded, so an error from AWS was reported as a confusing failure to find AssumeRoleWithWebIdentityResult (web-id) or missing JSON keys (pod-id) rather than the actual error.
Report the error returned by AWS using httpRequestError(), as the main request path already does. Redact the web identity token in the query and the pod identity token in the authorization header so they are not exposed when the request is logged.
Rocky Linux 8 is getting old so move the RHEL user guide and documentation CI to Rocky Linux 9, updating the base image and the PGDG repository to EL-9.
Install awscli from the awscli2 package and azure-cli from EPEL instead of pulling awscli through pip and azure-cli from the Microsoft package repository. This simplifies the build and drops the pip and Microsoft repository dependencies.
Remove the RHEL 8 PowerTools repository since CRB is already enabled via crb on Rocky 9.
The repo-target-time feature allows a repository on versioned object storage to be read at a point-in-time, but this protection against accidental, malware, or ransomware damage was not mentioned in the feature list.
Invisible, bidirectional, and homoglyph characters can hide code that looks benign under review, e.g. Trojan Source and Glassworm, and binary files cannot be reviewed at all. The linter now walks every file in the repo and requires it to be 7-bit ASCII text with tab and newline as the only permitted control characters, which catches the entire class at once. A short skip list exempts the two committed binary files and the generated git-history.cache.
The StringId check now also runs on hand-written .c.inc files, excluding generated .auto.c.inc and vendored .vendor.c.inc.
Intentional non-ASCII test data is written with \xNN escapes, an accented contributor id is normalized to ASCII, and smart quotes in meson.build are replaced.
When a unit test failed under valgrind (or otherwise exited before flushing coverage data) the real error was hidden and replaced by a gcov error about the missing coverage file.
The test command pipes output to tee so stderr can be copied to both stderr and stdout, but that made the pipeline return tee's exit status rather than the test's. The masked (zero) status caused the harness to attempt coverage generation, which then failed on the missing coverage data and reported that failure instead of the actual test error.
Save the test exit status to a file and exit with it explicitly so the real status is preserved through the pipe. PIPESTATUS/pipefail cannot be used here because the vm=none path runs through dash.
When expire runs automatically after a successful backup it uses the configuration loaded for the backup command, so options set only in an expire command section are not applied. Note this behavior in the expire-auto option help and point users to disabling it and running expire separately when expire-specific configuration is needed.
Add a new command-line option archive-expire-before to the expire command. It removes WAL segments earlier than a given segment from the archive, while never removing WAL that a retained backup still requires.
A typical use is to drive archive clean-up from a standby's restartpoint (using PostgreSQL's archive_cleanup_command) without having to fall back to pg_archivecleanup and lose pgBackRest's retention safeguards.