From 65228f62c78869f45509ab97a1ea51413c60761f Mon Sep 17 00:00:00 2001 From: Yuma Takino <36617106+tappe9@users.noreply.github.com> Date: Tue, 1 Sep 2026 14:29:11 +0900 Subject: [PATCH 1/9] test: add release readiness contract --- scripts/test-release-readiness.py | 235 ++++++++++++++++++++++++++++++ 1 file changed, 235 insertions(+) create mode 100644 scripts/test-release-readiness.py diff --git a/scripts/test-release-readiness.py b/scripts/test-release-readiness.py new file mode 100644 index 0000000..c53bb81 --- /dev/null +++ b/scripts/test-release-readiness.py @@ -0,0 +1,235 @@ +#!/usr/bin/env python3 +"""Verify the durable package and publication contract for pgdumpx 0.2.0.""" + +from __future__ import annotations + +import sys +import tomllib +from pathlib import Path +from typing import Any + +ROOT = Path(__file__).resolve().parents[1] +VERSION = "0.2.0" +REPOSITORY = "https://github.com/tappe9/pgdumpx" + + +def load_toml(path: Path) -> dict[str, Any]: + with path.open("rb") as handle: + return tomllib.load(handle) + + +def require_equal( + errors: list[str], label: str, actual: Any, expected: Any +) -> None: + if actual != expected: + errors.append(f"{label}={actual!r}, expected {expected!r}") + + +def require_file(errors: list[str], relative: str) -> Path | None: + path = ROOT / relative + if not path.is_file(): + errors.append(f"missing required release file: {relative}") + return None + return path + + +def require_snippets( + errors: list[str], relative: str, snippets: list[str] +) -> None: + path = require_file(errors, relative) + if path is None: + return + text = path.read_text(encoding="utf-8") + for snippet in snippets: + if snippet not in text: + errors.append(f"{relative}: missing required text {snippet!r}") + + +def verify_manifest_metadata(errors: list[str]) -> None: + workspace = load_toml(ROOT / "Cargo.toml") + workspace_package = workspace.get("workspace", {}).get("package", {}) + require_equal( + errors, + "workspace.package.version", + workspace_package.get("version"), + VERSION, + ) + require_equal( + errors, + "workspace.package.homepage", + workspace_package.get("homepage"), + REPOSITORY, + ) + + expected = { + "pgdumpx": { + "description": ( + "Read-only, bounded inspection, extraction, and row scanning for " + "PostgreSQL custom-format dumps" + ), + "documentation": "https://docs.rs/pgdumpx", + "keywords": ["postgresql", "pg-dump", "backup", "parser", "forensics"], + "categories": ["database", "parser-implementations"], + }, + "pgdumpx-cli": { + "description": ( + "Command-line inspection, extraction, and row scanning for PostgreSQL " + "custom-format dumps" + ), + "documentation": "https://docs.rs/pgdumpx-cli", + "keywords": ["postgresql", "pg-dump", "backup", "cli", "forensics"], + "categories": ["database", "command-line-utilities"], + }, + } + + for package_name, metadata in expected.items(): + manifest_path = ROOT / "crates" / package_name / "Cargo.toml" + manifest = load_toml(manifest_path) + package = manifest.get("package", {}) + require_equal( + errors, + f"{package_name}.package.version.workspace", + package.get("version", {}).get("workspace") + if isinstance(package.get("version"), dict) + else None, + True, + ) + require_equal( + errors, + f"{package_name}.package.homepage.workspace", + package.get("homepage", {}).get("workspace") + if isinstance(package.get("homepage"), dict) + else None, + True, + ) + require_equal( + errors, + f"{package_name}.package.publish", + package.get("publish"), + ["crates-io"], + ) + for key, expected_value in metadata.items(): + require_equal( + errors, + f"{package_name}.package.{key}", + package.get(key), + expected_value, + ) + + cli = load_toml(ROOT / "crates" / "pgdumpx-cli" / "Cargo.toml") + dependency = cli.get("dependencies", {}).get("pgdumpx", {}) + require_equal(errors, "pgdumpx-cli dependency path", dependency.get("path"), "../pgdumpx") + require_equal(errors, "pgdumpx-cli dependency version", dependency.get("version"), VERSION) + require_equal( + errors, + "pgdumpx-cli dependency default-features", + dependency.get("default-features"), + False, + ) + + +def verify_lockfile(errors: list[str]) -> None: + lockfile = load_toml(ROOT / "Cargo.lock") + packages = lockfile.get("package", []) + for name in ("pgdumpx", "pgdumpx-cli"): + matches = [ + package + for package in packages + if package.get("name") == name and package.get("source") is None + ] + if len(matches) != 1: + errors.append(f"Cargo.lock: expected one local package named {name}") + continue + require_equal( + errors, + f"Cargo.lock {name} version", + matches[0].get("version"), + VERSION, + ) + + +def verify_release_documents(errors: list[str]) -> None: + require_snippets( + errors, + "CHANGELOG.md", + [ + "# Changelog", + "## [0.2.0] - Unreleased", + "first public release", + ], + ) + require_snippets( + errors, + "docs/RELEASING.md", + [ + "pgdumpx 0.2.0", + "cargo publish --dry-run -p pgdumpx --locked", + "cargo publish -p pgdumpx --locked", + "cargo info pgdumpx@0.2.0", + "cargo publish --dry-run -p pgdumpx-cli --locked", + "cargo publish -p pgdumpx-cli --locked", + "cargo info pgdumpx-cli@0.2.0", + "git tag -a v0.2.0", + "gh release create v0.2.0", + "Do not create the tag or GitHub Release until both package versions", + "If the library publishes but the CLI fails", + ], + ) + require_snippets( + errors, + "docs/PACKAGING.md", + [ + "0.2.0", + "cargo package --workspace --locked", + "cargo publish --dry-run -p pgdumpx --locked", + "pgdumpx-cli", + ], + ) + require_snippets( + errors, + "docs/release-notes/0.2.0.md", + [ + "# pgdumpx 0.2.0", + "first public release", + "PostgreSQL custom-format dumps", + "Archive versions 1.14 through 1.16", + ], + ) + + +def verify_packaging_script(errors: list[str]) -> None: + require_snippets( + errors, + "scripts/verify-release-packaging.py", + [ + 'VERSION = "0.2.0"', + 'run("cargo", "publish", "--dry-run", "-p", "pgdumpx", "--locked")', + '"cargo",\n "test",\n "-p",\n "pgdumpx-cli",', + '"cargo",\n "install",\n "--path",\n "crates/pgdumpx-cli",', + "MAX_PACKAGE_BYTES", + ], + ) + + +def main() -> int: + errors: list[str] = [] + try: + verify_manifest_metadata(errors) + verify_lockfile(errors) + verify_release_documents(errors) + verify_packaging_script(errors) + except (OSError, tomllib.TOMLDecodeError) as error: + errors.append(str(error)) + + if errors: + print("release readiness contract failed:", file=sys.stderr) + for error in errors: + print(f" - {error}", file=sys.stderr) + return 1 + + print(f"release readiness contract passed for pgdumpx {VERSION}") + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) From 8e77a345c4055cdd8f2e941c11ba38cdeed84cdd Mon Sep 17 00:00:00 2001 From: Yuma Takino <36617106+tappe9@users.noreply.github.com> Date: Tue, 1 Sep 2026 14:29:40 +0900 Subject: [PATCH 2/9] test: run release readiness contract --- .github/workflows/ci.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 9e39c80..0213746 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -46,6 +46,9 @@ jobs: - name: Workflow policy contract run: python3 scripts/test-workflow-policy.py + - name: Release readiness contract + run: python3 scripts/test-release-readiness.py + - name: Clippy run: cargo clippy --workspace --all-targets --all-features -- -D warnings From adf3f3b8d39ced4310b72f8b172801ae288a922b Mon Sep 17 00:00:00 2001 From: Yuma Takino <36617106+tappe9@users.noreply.github.com> Date: Tue, 1 Sep 2026 14:35:43 +0900 Subject: [PATCH 3/9] chore(release): prepare pgdumpx 0.2.0 --- .github/workflows/ci.yml | 2 +- CHANGELOG.md | 28 +++++ Cargo.lock | 4 +- Cargo.toml | 3 +- crates/pgdumpx-cli/Cargo.toml | 9 +- crates/pgdumpx/Cargo.toml | 7 +- docs/PACKAGING.md | 105 +++++++---------- docs/RELEASING.md | 116 +++++++++++++++++++ docs/release-notes/0.2.0.md | 41 +++++++ scripts/test-release-readiness.py | 2 +- scripts/verify-release-packaging.py | 173 ++++++++++++++++++++++++---- 11 files changed, 395 insertions(+), 95 deletions(-) create mode 100644 CHANGELOG.md create mode 100644 docs/RELEASING.md create mode 100644 docs/release-notes/0.2.0.md mode change 100644 => 100755 scripts/test-release-readiness.py mode change 100644 => 100755 scripts/verify-release-packaging.py diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 0213746..f36cf01 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -85,7 +85,7 @@ jobs: - name: Test workspace with all compression backends run: cargo test --workspace --all-features - - name: Verify default CLI supports all v0.1 compression backends + - name: Verify default CLI supports all compression backends run: cargo test -p pgdumpx-cli --test compression_defaults feature-matrix: diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..2b3bd74 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,28 @@ +# Changelog + +This changelog records user-visible behavior, compatibility boundaries, and release-engineering changes for published versions of `pgdumpx`. + +## [0.2.0] + +This is the first public release of the `pgdumpx` library and `pgdumpx-cli` package. It includes the completed archive-reading foundation and the subsequent reusable extraction, filtering, scan-budget, security, and maintenance work. + +### Added + +- Read-only parsing of PostgreSQL custom-format archive metadata for archive versions 1.14 through 1.16. +- Bounded selected-entry streaming for none, gzip, LZ4, and Zstandard compression. +- Byte-oriented PostgreSQL COPY text row parsing with explicit NULL handling and exact named-column equality helpers. +- File-oriented archive helpers, owned table selectors, reusable extraction plans, sequential multi-table extraction, and metadata filtering. +- `pgdumpx` CLI commands for `inspect`, `list`, `extract`, and `find`, with finite default structural, raw-output, row, and decompressed-byte budgets. +- Reproducible official fixture provenance, compatibility differential checks, benchmark harnesses, fuzz targets, scheduled fuzz campaigns, and dependency advisory checks. + +### Compatibility + +- Rust 1.85.0 is the minimum supported Rust version. +- Linux, macOS, and Windows are exercised by the stable CI matrix. +- Row-aware APIs support PostgreSQL COPY text table data. Binary COPY and INSERT-based row decoding are intentionally unsupported; readable INSERT entries remain available through raw extraction. + +### Security and reliability + +- Project-authored Rust code forbids `unsafe`. +- Structural parsing, raw extraction, and row scanning expose separate typed resource limits. +- GitHub workflows use immutable action revisions, read-only permissions, disabled checkout credential persistence, concurrency cancellation, and finite job timeouts. diff --git a/Cargo.lock b/Cargo.lock index 6ce6f0e..b396439 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -136,7 +136,7 @@ dependencies = [ [[package]] name = "pgdumpx" -version = "0.1.0" +version = "0.2.0" dependencies = [ "flate2", "lz4_flex", @@ -148,7 +148,7 @@ dependencies = [ [[package]] name = "pgdumpx-cli" -version = "0.1.0" +version = "0.2.0" dependencies = [ "pgdumpx", ] diff --git a/Cargo.toml b/Cargo.toml index 9f8c7ef..dcd0738 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -7,11 +7,12 @@ exclude = ["fuzz"] resolver = "3" [workspace.package] -version = "0.1.0" +version = "0.2.0" edition = "2024" rust-version = "1.85.0" license = "MIT OR Apache-2.0" repository = "https://github.com/tappe9/pgdumpx" +homepage = "https://github.com/tappe9/pgdumpx" readme = "README.md" [workspace.lints.rust] diff --git a/crates/pgdumpx-cli/Cargo.toml b/crates/pgdumpx-cli/Cargo.toml index 07b1bee..3f5789a 100644 --- a/crates/pgdumpx-cli/Cargo.toml +++ b/crates/pgdumpx-cli/Cargo.toml @@ -1,12 +1,17 @@ [package] name = "pgdumpx-cli" -description = "Command-line interface for pgdumpx" +description = "Command-line inspection, extraction, and row scanning for PostgreSQL custom-format dumps" version.workspace = true edition.workspace = true rust-version.workspace = true license.workspace = true repository.workspace = true +homepage.workspace = true readme.workspace = true +documentation = "https://docs.rs/pgdumpx-cli" +keywords = ["postgresql", "pg-dump", "backup", "cli", "forensics"] +categories = ["database", "command-line-utilities"] +publish = ["crates-io"] [[bin]] name = "pgdumpx" @@ -19,7 +24,7 @@ lz4 = ["pgdumpx/lz4"] zstd = ["pgdumpx/zstd"] [dependencies] -pgdumpx = { path = "../pgdumpx", version = "0.1.0", default-features = false } +pgdumpx = { path = "../pgdumpx", version = "0.2.0", default-features = false } [lints] workspace = true diff --git a/crates/pgdumpx/Cargo.toml b/crates/pgdumpx/Cargo.toml index 228071e..86a67df 100644 --- a/crates/pgdumpx/Cargo.toml +++ b/crates/pgdumpx/Cargo.toml @@ -1,12 +1,17 @@ [package] name = "pgdumpx" -description = "A bounded, byte-oriented row scanner for PostgreSQL custom-format dumps" +description = "Read-only, bounded inspection, extraction, and row scanning for PostgreSQL custom-format dumps" version.workspace = true edition.workspace = true rust-version.workspace = true license.workspace = true repository.workspace = true +homepage.workspace = true readme.workspace = true +documentation = "https://docs.rs/pgdumpx" +keywords = ["postgresql", "pg-dump", "backup", "parser", "forensics"] +categories = ["database", "parser-implementations"] +publish = ["crates-io"] [features] default = ["lz4", "zstd"] diff --git a/docs/PACKAGING.md b/docs/PACKAGING.md index 08b1cfc..a13b006 100644 --- a/docs/PACKAGING.md +++ b/docs/PACKAGING.md @@ -1,75 +1,50 @@ # Packaging and dependency constraints -Status: **v0.1 package, license, dependency, and runtime boundary verified** +Status: **0.2.0 publication preflight; registry publication pending credentials** -This document records the v0.1 publish-time package and dependency boundary verified by Issue #35 / PR #54. The final cross-document Definition of Done mapping is recorded separately in `V0.1-RELEASE-AUDIT.md`. +This document records the package, dependency, and staged-verification boundary for the first public release. -## Intended package set +## Package set and versioning -The workspace contains two intended crates.io packages; neither is published by this audit: +The workspace publishes two crates in dependency order: -- `pgdumpx` — the reusable library; -- `pgdumpx-cli` — the `pgdumpx` command-line binary, depending on `pgdumpx = "0.1.0"` with library default features disabled and CLI features forwarded explicitly. +- `pgdumpx 0.2.0` — the reusable library; +- `pgdumpx-cli 0.2.0` — the `pgdumpx` command-line binary. -Both packages inherit the workspace version, edition, Rust version, license, repository, and README metadata. For v0.1 these are: +Both packages inherit edition 2024, Rust 1.85.0, the `MIT OR Apache-2.0` license expression, repository, homepage, and root README metadata. Each package declares crates.io discovery metadata and is restricted to the `crates-io` registry. The CLI dependency is: -- version: `0.1.0`; -- edition: `2024`; -- MSRV: Rust `1.85.0`; -- license expression: `MIT OR Apache-2.0`; -- repository: `https://github.com/tappe9/pgdumpx`; -- README: the workspace `README.md`. - -The package file list contains `README.md`, `LICENSE-MIT`, and `LICENSE-APACHE`. The package-local license copies exactly match the repository-root license texts. Repository fixtures, generated dump archives, and benchmark datasets are not distribution inputs and do not appear in either `.crate` package. - -## Compression and feature boundary - -The v0.1 default CLI supports all four archive compression modes required by the compatibility contract: - -| Archive compression | Implementation | Feature behavior | Native/runtime constraint | -| --- | --- | --- | --- | -| none | pgdumpx framing/streaming path | always available | none | -| gzip | `flate2` with `default-features = false`, `rust_backend` | always available | no system zlib requirement | -| LZ4 | `lz4_flex` with default features disabled | CLI default feature `lz4`; library optional feature | no Cargo native `links` dependency in the accepted default graph | -| Zstandard | `ruzstd = 0.8.1` | CLI default feature `zstd`; library optional feature | no Cargo native `links` dependency in the accepted default graph | - -The project does not use “Pure Rust” as a blanket promise for all future transitive dependencies. Instead, the release audit rejects a new default-runtime Cargo package with a non-empty `links` field until the build/distribution constraint is explicitly reviewed and documented. - -The library remains coherent with: - -```text -default features ---no-default-features ---no-default-features --features lz4 ---no-default-features --features zstd +```toml +pgdumpx = { path = "../pgdumpx", version = "0.2.0", default-features = false } ``` -`flate2`/gzip remains part of the base library dependency set; the feature switches control the optional LZ4 and Zstandard decoders. +Cargo uses the path while developing in the workspace and the matching version requirement in the published package. -## PostgreSQL runtime boundary +## Package contents -The production library and CLI read the custom archive format directly. The default runtime dependency graph does not contain PostgreSQL client/runtime packages such as `libpq`, `pq-sys`, `postgres`, or `tokio-postgres`. +The package audit checks each `cargo package --list` result. Every package must include `README.md`, `LICENSE-MIT`, and `LICENSE-APACHE`; package-local license copies must match the repository-root texts byte-for-byte. -A running PostgreSQL server, `libpq`, `pg_restore`, `pg_dump`, `psql`, or another PostgreSQL executable is not required to run the v0.1 library or CLI. PostgreSQL tools may still be used by repository-only fixture generation or differential-test scripts; those development/test tools are not runtime dependencies of the packaged crates. +Package archives must not include fixture directories, dump archives, benchmark datasets, workflow files, repository scripts, credentials, local environment files, or planning artifacts. The resulting `.crate` files are inspected after `cargo package --workspace --locked`, must be non-empty, and must remain below the registry package-size boundary enforced by the audit. -## Dependency license policy +## Compression and feature boundary -Release packaging audits the normal runtime dependency closure of the default `pgdumpx-cli` build using `cargo metadata --locked`. Every runtime package must provide license metadata and its SPDX expression must have a distributable branch composed only of the accepted permissive identifiers enforced by `scripts/verify-release-packaging.py`. +The default CLI supports the four verified archive compression modes: -The initial allow-set covers the permissive licenses used by the accepted dependency closure, including MIT, Apache-2.0, BSD, ISC, Zlib, Unicode-3.0, 0BSD/MIT-0, and Unlicense forms. An SPDX `WITH` exception or a dependency outside that set fails the audit and requires an explicit review rather than being silently accepted. +| Archive compression | Implementation | Feature behavior | Runtime constraint | +| --- | --- | --- | --- | +| none | archive framing and streaming path | always available | none | +| gzip | `flate2` with `rust_backend` | always available | no system zlib requirement | +| LZ4 | `lz4_flex` frame decoder | CLI default feature `lz4`; library optional feature | no accepted Cargo `links` dependency | +| Zstandard | `ruzstd 0.8.1` | CLI default feature `zstd`; library optional feature | no accepted Cargo `links` dependency | -This is a distribution-compatibility gate, not legal advice. `Cargo.lock` is the version/checksum record for the exact audited dependency resolution. +The library remains coherent with default features, no optional compression, LZ4 only, and Zstandard only. Disabled optional backends remain recognizable in metadata and return the typed unsupported-compression error when entry decoding is requested. -## Project-authored unsafe policy +## Runtime dependency boundary -The workspace sets: +The library and CLI read the archive format directly. The accepted runtime dependency graph contains no PostgreSQL client package, `libpq`, native compression library, running server, or PostgreSQL executable requirement. PostgreSQL tools are used only by repository fixture generation and compatibility differential checks. -```toml -[workspace.lints.rust] -unsafe_code = "forbid" -``` +The audit traverses the default CLI normal-runtime dependency graph through locked Cargo metadata. Every runtime dependency must provide an accepted distributable SPDX branch. A new license exception, native `links` package, or PostgreSQL runtime dependency fails the preflight until explicitly reviewed and documented. -Both release crates inherit workspace lints. The packaging audit verifies that this remains true; normal all-target/all-feature CI compilation then enforces the policy across project-authored Rust code. +The workspace forbids project-authored `unsafe`, and both release crates inherit the workspace lint policy. ## Publish-preflight verification @@ -79,18 +54,22 @@ Run from a clean checkout: python3 scripts/verify-release-packaging.py ``` -The audit performs all of the following without publishing: +The audit performs all of the following without uploading a package: + +1. validates version, package metadata, the CLI path-plus-version dependency, licenses, and workspace lint inheritance; +2. audits the locked default runtime dependency closure and accepted license expressions; +3. checks package file lists for required and forbidden content; +4. exercises default, no-optional, LZ4-only, and Zstandard-only library builds; +5. runs the complete `pgdumpx-cli` test suite, including command, exit-code, limit, and compression behavior; +6. installs the CLI into an isolated temporary root and verifies `--version`, `--help`, and command discovery; +7. runs `cargo package --workspace --locked` and inspects both `.crate` sizes; +8. runs `cargo publish --dry-run -p pgdumpx --locked`; +9. verifies that the source tree remains unchanged. -1. loads locked Cargo metadata and verifies package metadata; -2. verifies both package-local license files are byte-for-byte copies of the repository-root licenses; -3. traverses the default CLI normal-runtime dependency graph; -4. checks dependency license expressions, PostgreSQL runtime exclusions, and native `links` constraints; -5. checks `cargo package --list` for both intended packages; -6. verifies required README/license files and rejects fixture/benchmark data in package contents; -7. checks the default and reduced-feature library builds plus the default CLI compression contract; -8. runs `cargo package --workspace --locked` so current stable Cargo assembles and verifies both interdependent release packages together before `pgdumpx 0.1.0` exists on crates.io; -9. verifies that the source tree remains unchanged by the preflight. +The CLI package is assembled and build-verified through workspace packaging before the library exists in crates.io. Its final publication dry-run is intentionally staged after `pgdumpx 0.2.0` is visible in the registry: -Release packaging CI intentionally follows the repository's stable toolchain so it can use current Cargo's multi-package packaging support. The v0.1 MSRV remains Rust 1.85.0 and is enforced separately by the normal workspace CI. +```bash +cargo publish --dry-run -p pgdumpx-cli --locked +``` -Publishing crates, creating tags, and creating release artifacts remain outside this preflight and outside the Issue #36 audit. +See `RELEASING.md` for the complete publication order, registry verification, tag creation, and recovery procedure. The preflight never uploads packages, creates tags, or creates a GitHub Release. diff --git a/docs/RELEASING.md b/docs/RELEASING.md new file mode 100644 index 0000000..ea5d5df --- /dev/null +++ b/docs/RELEASING.md @@ -0,0 +1,116 @@ +# Release process + +This document defines the staged publication procedure for `pgdumpx 0.2.0`, the first public release of both workspace packages. + +## Release identity + +| Item | Value | +| --- | --- | +| Library package | `pgdumpx 0.2.0` | +| CLI package | `pgdumpx-cli 0.2.0` | +| Publication order | library, registry confirmation, CLI | +| Git tag | `v0.2.0` | +| GitHub Release name | `pgdumpx 0.2.0` | +| Release notes | `docs/release-notes/0.2.0.md` | +| Changelog range | repository inception through the release commit | + +Both packages use the same version for the initial release train. The CLI retains a path dependency for workspace development and a matching `0.2.0` version requirement for published packages. + +## Preconditions + +Use a clean checkout of the exact release commit on `main`. Confirm that there are no release-blocking Issues or pull requests and that CI, dependency advisories, scheduled-fuzz policy, and Release Packaging are green for that commit. + +Crates.io publication requires a verified crates.io account and a scoped API token. Store it with `cargo login`; never commit the token or place it in command history, logs, Issues, or pull requests. + +Record the release commit before running the gates: + +```bash +git switch main +git pull --ff-only +RELEASE_SHA="$(git rev-parse HEAD)" +git status --short --branch +python3 scripts/test-release-readiness.py +``` + +The status output must show a clean `main` synchronized with `origin/main`. + +## Local release gates + +Run every gate from the same clean commit: + +```bash +cargo fmt --check +cargo clippy --workspace --all-targets --all-features -- -D warnings +cargo test --workspace --all-features +RUSTDOCFLAGS="-D warnings" cargo doc --workspace --all-features --no-deps +cargo +1.85.0 check --workspace --all-targets --all-features +python3 scripts/verify-release-packaging.py +``` + +The packaging preflight verifies metadata, licenses, dependency boundaries, package contents and sizes, all CLI tests, an installed CLI smoke test, workspace packaging, and the library publication dry-run. Review the emitted package file lists before continuing. + +## Stage 1: publish the library + +Re-run the library dry-run immediately before upload: + +```bash +cargo publish --dry-run -p pgdumpx --locked +cargo publish -p pgdumpx --locked +cargo info pgdumpx@0.2.0 +``` + +If `cargo publish` times out while waiting for the index, do not retry the upload blindly. Run `cargo info pgdumpx@0.2.0` until the registry either confirms the version or clearly shows that it was not accepted. + +## Stage 2: publish the CLI + +Only continue after `cargo info pgdumpx@0.2.0` resolves the exact library version from crates.io: + +```bash +cargo publish --dry-run -p pgdumpx-cli --locked +cargo publish -p pgdumpx-cli --locked +cargo info pgdumpx-cli@0.2.0 +``` + +The pre-publication repository gate uses `cargo package --workspace --locked`, the complete CLI test suite, and `cargo install --path` because the published CLI dry-run cannot resolve its registry dependency until the library version exists in the crates.io index. + +## Stage 3: create the source release + +Do not create the tag or GitHub Release until both package versions are independently visible in crates.io. + +Verify that the working tree and release commit have not changed, then create an annotated tag and a Release from the committed notes: + +```bash +test "$(git rev-parse HEAD)" = "$RELEASE_SHA" +git status --porcelain +git tag -a v0.2.0 -m "pgdumpx 0.2.0" "$RELEASE_SHA" +git push origin v0.2.0 +gh release create v0.2.0 \ + --verify-tag \ + --title "pgdumpx 0.2.0" \ + --notes-file docs/release-notes/0.2.0.md +``` + +Verify the final state: + +```bash +git rev-list -n 1 v0.2.0 +gh release view v0.2.0 +cargo info pgdumpx@0.2.0 +cargo info pgdumpx-cli@0.2.0 +``` + +The tag must resolve to `RELEASE_SHA`. docs.rs builds are asynchronous; verify the `pgdumpx 0.2.0` documentation after the registry has accepted the package. The CLI package intentionally has no public Rust library target, so its primary usage documentation remains the repository README and CLI help. + +## Partial publication recovery + +### Library published, CLI not published + +If the library publishes but the CLI fails, do not create `v0.2.0` or a GitHub Release and do not attempt to overwrite `pgdumpx 0.2.0`. + +- If the CLI package itself is the only problem and `pgdumpx-cli 0.2.0` was never accepted, correct the CLI or release metadata, re-run every gate, and publish the same still-unpublished CLI version against `pgdumpx 0.2.0`. +- If the correction requires changing the published library API or package, prepare `0.2.1` for both packages and repeat the library-first sequence. +- Yank `pgdumpx 0.2.0` only for a severe correctness or security defect that justifies excluding it from new dependency resolution. Yanking does not delete the published archive. + +### Packages published, tag or Release creation failed + +Published crate versions are immutable. Verify both registry entries, restore the exact release commit locally, and retry only the failed Git tag or GitHub Release operation. Do not publish replacement crate versions merely because the source-release operation failed. diff --git a/docs/release-notes/0.2.0.md b/docs/release-notes/0.2.0.md new file mode 100644 index 0000000..d30f505 --- /dev/null +++ b/docs/release-notes/0.2.0.md @@ -0,0 +1,41 @@ +# pgdumpx 0.2.0 + +`pgdumpx 0.2.0` is the first public release of the read-only Rust library and CLI for bounded inspection, extraction, and row scanning of PostgreSQL custom-format dumps. + +## Highlights + +- Inspect archive metadata and table entries without restoring into a PostgreSQL server. +- Stream selected raw entry bodies with explicit decompressed-byte limits and typed partial-output failures. +- Parse PostgreSQL COPY text rows as bytes, distinguish NULL from non-NULL values, and scan exact named-column matches with finite default budgets. +- Reuse owned selectors, metadata filters, extraction plans, and sequential multi-table extraction from library code. +- Use `inspect`, `list`, `extract`, and `find` from the `pgdumpx` CLI. + +## Supported boundaries + +- Archive versions 1.14 through 1.16 are fixture- and differential-verified. +- none, gzip, LZ4, and Zstandard selected-entry decompression is supported. +- COPY text row access is supported. Binary COPY and INSERT-based row decoding are not supported; readable INSERT entries can still be extracted as raw bytes. +- Rust 1.85.0 is the MSRV. +- Linux, macOS, and Windows are exercised by CI. + +## Reliability and maintenance + +The release includes separate structural, raw-output, row, and decompressed-byte limits; warning-denying tests and documentation; official fixture provenance; compatibility differential checks; fuzz smoke and scheduled coverage-guided campaigns; dependency advisory checks; package-content audits; and immutable, least-privilege workflow dependencies. + +## Installation + +After both crates.io packages are published: + +```bash +cargo install pgdumpx-cli --version 0.2.0 --locked +pgdumpx --help +``` + +Library consumers can add: + +```toml +[dependencies] +pgdumpx = "0.2.0" +``` + +See `README.md`, `docs/COMPATIBILITY.md`, `docs/PACKAGING.md`, and `SECURITY.md` for complete support and safety boundaries. diff --git a/scripts/test-release-readiness.py b/scripts/test-release-readiness.py old mode 100644 new mode 100755 index c53bb81..a5742d7 --- a/scripts/test-release-readiness.py +++ b/scripts/test-release-readiness.py @@ -154,7 +154,7 @@ def verify_release_documents(errors: list[str]) -> None: "CHANGELOG.md", [ "# Changelog", - "## [0.2.0] - Unreleased", + "## [0.2.0]", "first public release", ], ) diff --git a/scripts/verify-release-packaging.py b/scripts/verify-release-packaging.py old mode 100644 new mode 100755 index 5fecc95..13aa247 --- a/scripts/verify-release-packaging.py +++ b/scripts/verify-release-packaging.py @@ -1,30 +1,70 @@ #!/usr/bin/env python3 -"""Verify v0.1 package contents, dependency licenses, and runtime boundaries.""" +"""Verify pgdumpx 0.2.0 package contents and publication boundaries.""" from __future__ import annotations import json +import os import re import subprocess import sys from pathlib import Path +from tempfile import TemporaryDirectory ROOT = Path(__file__).resolve().parents[1] -VERSION = "0.1.0" +VERSION = "0.2.0" +REPOSITORY = "https://github.com/tappe9/pgdumpx" +MAX_PACKAGE_BYTES = 10_000_000 PROJECT_PACKAGES = {"pgdumpx", "pgdumpx-cli"} PACKAGE_DIRS = { "pgdumpx": ROOT / "crates" / "pgdumpx", "pgdumpx-cli": ROOT / "crates" / "pgdumpx-cli", } +PACKAGE_EXPECTATIONS = { + "pgdumpx": { + "description": ( + "Read-only, bounded inspection, extraction, and row scanning for " + "PostgreSQL custom-format dumps" + ), + "documentation": "https://docs.rs/pgdumpx", + "keywords": ["postgresql", "pg-dump", "backup", "parser", "forensics"], + "categories": ["database", "parser-implementations"], + }, + "pgdumpx-cli": { + "description": ( + "Command-line inspection, extraction, and row scanning for PostgreSQL " + "custom-format dumps" + ), + "documentation": "https://docs.rs/pgdumpx-cli", + "keywords": ["postgresql", "pg-dump", "backup", "cli", "forensics"], + "categories": ["database", "command-line-utilities"], + }, +} REQUIRED_METADATA = { "version": VERSION, "edition": "2024", "license": "MIT OR Apache-2.0", - "repository": "https://github.com/tappe9/pgdumpx", + "repository": REPOSITORY, + "homepage": REPOSITORY, } LICENSE_FILES = {"LICENSE-APACHE", "LICENSE-MIT"} REQUIRED_PACKAGE_FILES = {"README.md", *LICENSE_FILES} -FORBIDDEN_PACKAGE_PARTS = {"fixtures", "benchmarks", "benchmark-data"} +FORBIDDEN_PACKAGE_PARTS = { + ".github", + ".plan", + "benchmark-data", + "benchmarks", + "fixtures", + "scripts", +} +FORBIDDEN_FILE_NAMES = { + ".env", + ".npmrc", + "credentials", + "credentials.toml", + "id_rsa", + "id_ed25519", +} FORBIDDEN_RUNTIME_PACKAGES = { "libpq", "libpq-sys", @@ -120,14 +160,39 @@ def verify_project_metadata(metadata: dict) -> None: actual = package.get(key) if actual != expected: raise AuditError(f"{name}: metadata {key}={actual!r}, expected {expected!r}") - if not package.get("description"): - raise AuditError(f"{name}: package description is missing") + for key, expected in PACKAGE_EXPECTATIONS[name].items(): + actual = package.get(key) + if actual != expected: + raise AuditError(f"{name}: metadata {key}={actual!r}, expected {expected!r}") if not package.get("readme"): raise AuditError(f"{name}: package readme metadata is missing") if package.get("rust_version") != "1.85.0": raise AuditError( f"{name}: rust-version={package.get('rust_version')!r}, expected '1.85.0'" ) + if package.get("publish") != ["crates-io"]: + raise AuditError( + f"{name}: publish={package.get('publish')!r}, expected ['crates-io']" + ) + + cli = package_by_name(metadata, "pgdumpx-cli") + dependencies = [ + dependency + for dependency in cli.get("dependencies", []) + if dependency.get("name") == "pgdumpx" and dependency.get("kind") is None + ] + if len(dependencies) != 1: + raise AuditError("pgdumpx-cli: expected one normal pgdumpx dependency") + dependency = dependencies[0] + if dependency.get("req") != "^0.2.0": + raise AuditError( + f"pgdumpx-cli: pgdumpx version requirement={dependency.get('req')!r}, " + "expected '^0.2.0'" + ) + if not dependency.get("path"): + raise AuditError("pgdumpx-cli: pgdumpx workspace dependency must retain its path") + if dependency.get("uses_default_features"): + raise AuditError("pgdumpx-cli: pgdumpx dependency must disable default features") def verify_license_copies() -> None: @@ -219,9 +284,6 @@ def parse_factor() -> bool: index += 1 value = token in ALLOWED_LICENSE_IDS if index < len(tokens) and tokens[index] == "WITH": - # No current runtime dependency needs a license exception. If one is - # introduced, document and explicitly add support instead of silently - # treating it as equivalent to the base license. index += 1 if index >= len(tokens): raise AuditError(f"truncated SPDX WITH expression: {expression!r}") @@ -302,12 +364,19 @@ def verify_package_contents(package: str) -> None: rejected = [] for path in sorted(files): - parts = set(Path(path).parts) - if parts & FORBIDDEN_PACKAGE_PARTS or path.endswith(".dump"): + parts = Path(path).parts + lowered_parts = {part.lower() for part in parts} + if lowered_parts & FORBIDDEN_PACKAGE_PARTS: + rejected.append(path) + continue + if Path(path).name.lower() in FORBIDDEN_FILE_NAMES: + rejected.append(path) + continue + if path.endswith((".dump", ".pem", ".key")): rejected.append(path) if rejected: raise AuditError( - f"{package}: package contains non-distribution fixture/benchmark data:\n " + f"{package}: package contains forbidden repository or sensitive files:\n " + "\n ".join(rejected) ) @@ -317,12 +386,25 @@ def verify_package_contents(package: str) -> None: def verify_package_builds() -> None: - # Package the two interdependent release crates together. Current stable - # Cargo verifies workspace members through its temporary registry overlay, - # so this works before pgdumpx 0.1.0 exists on crates.io. run("cargo", "package", "--workspace", "--locked") +def verify_package_archives() -> None: + for package in sorted(PROJECT_PACKAGES): + archive = ROOT / "target" / "package" / f"{package}-{VERSION}.crate" + if not archive.is_file(): + raise AuditError(f"{package}: expected package archive is missing: {archive}") + size = archive.stat().st_size + if size <= 0: + raise AuditError(f"{package}: package archive is empty") + if size > MAX_PACKAGE_BYTES: + raise AuditError( + f"{package}: package archive is {size} bytes, exceeding " + f"{MAX_PACKAGE_BYTES} bytes" + ) + print(f"{package}: package archive size {size} bytes") + + def verify_feature_builds() -> None: commands = [ ("cargo", "check", "-p", "pgdumpx", "--locked", "--no-default-features"), @@ -348,20 +430,60 @@ def verify_feature_builds() -> None: ), ("cargo", "check", "-p", "pgdumpx", "--locked"), ("cargo", "check", "-p", "pgdumpx-cli", "--locked"), - ( - "cargo", - "test", - "-p", - "pgdumpx-cli", - "--locked", - "--test", - "compression_defaults", - ), ] for command in commands: run(*command) +def verify_cli_package_behavior() -> None: + run( + "cargo", + "test", + "-p", + "pgdumpx-cli", + "--locked", + ) + + with TemporaryDirectory(prefix="pgdumpx-install-") as temporary_root: + install_root = Path(temporary_root) + run( + "cargo", + "install", + "--path", + "crates/pgdumpx-cli", + "--locked", + "--root", + str(install_root), + "--force", + ) + executable = "pgdumpx.exe" if os.name == "nt" else "pgdumpx" + binary = install_root / "bin" / executable + if not binary.is_file(): + raise AuditError(f"installed CLI binary is missing: {binary}") + + version = run(str(binary), "--version", display_output=False).stdout.strip() + if version != f"pgdumpx {VERSION}": + raise AuditError( + f"installed CLI version output={version!r}, expected 'pgdumpx {VERSION}'" + ) + + help_output = run(str(binary), "--help", display_output=False).stdout + missing_commands = [ + command + for command in ("inspect", "list", "extract", "find") + if command not in help_output + ] + if missing_commands: + raise AuditError( + "installed CLI help is missing commands: " + ", ".join(missing_commands) + ) + print(f"installed CLI smoke passed: {version}") + + +def verify_library_publish_dry_run() -> None: + run("cargo", "publish", "--dry-run", "-p", "pgdumpx", "--locked") + + def verify_unsafe_policy() -> None: workspace_manifest = (ROOT / "Cargo.toml").read_text(encoding="utf-8") if 'unsafe_code = "forbid"' not in workspace_manifest: @@ -386,7 +508,10 @@ def main() -> int: for package in sorted(PROJECT_PACKAGES): verify_package_contents(package) verify_feature_builds() + verify_cli_package_behavior() verify_package_builds() + verify_package_archives() + verify_library_publish_dry_run() after = clean_status() if after: From e06260e25167457762ed0bbf266a87c22c46d983 Mon Sep 17 00:00:00 2001 From: Yuma Takino <36617106+tappe9@users.noreply.github.com> Date: Tue, 1 Sep 2026 14:37:48 +0900 Subject: [PATCH 4/9] test: define global CLI metadata options --- crates/pgdumpx-cli/tests/global_options.rs | 32 ++++++++++++++++++++++ 1 file changed, 32 insertions(+) create mode 100644 crates/pgdumpx-cli/tests/global_options.rs diff --git a/crates/pgdumpx-cli/tests/global_options.rs b/crates/pgdumpx-cli/tests/global_options.rs new file mode 100644 index 0000000..34354da --- /dev/null +++ b/crates/pgdumpx-cli/tests/global_options.rs @@ -0,0 +1,32 @@ +use std::process::{Command, Output}; + +fn run(arguments: &[&str]) -> Output { + Command::new(env!("CARGO_BIN_EXE_pgdumpx")) + .args(arguments) + .output() + .expect("run pgdumpx") +} + +#[test] +fn version_reports_the_package_version_on_stdout() { + let output = run(&["--version"]); + + assert!(output.status.success(), "status: {:?}", output.status.code()); + assert_eq!(output.stdout, b"pgdumpx 0.2.0\n"); + assert!(output.stderr.is_empty()); +} + +#[test] +fn help_reports_global_usage_on_stdout() { + let output = run(&["--help"]); + + assert!(output.status.success(), "status: {:?}", output.status.code()); + assert!(output.stderr.is_empty()); + + let stdout = String::from_utf8(output.stdout).expect("help is UTF-8"); + assert!(stdout.starts_with("usage:\n")); + for command in ["inspect", "list", "extract", "find"] { + assert!(stdout.contains(command), "missing command {command:?}: {stdout}"); + } + assert!(stdout.ends_with('\n')); +} From 44d8f51d7de13a5a6d50bf341614e0d070c0562f Mon Sep 17 00:00:00 2001 From: Yuma Takino <36617106+tappe9@users.noreply.github.com> Date: Tue, 1 Sep 2026 14:38:38 +0900 Subject: [PATCH 5/9] test: format global CLI metadata options --- crates/pgdumpx-cli/tests/global_options.rs | 17 ++++++++++++++--- 1 file changed, 14 insertions(+), 3 deletions(-) diff --git a/crates/pgdumpx-cli/tests/global_options.rs b/crates/pgdumpx-cli/tests/global_options.rs index 34354da..e0920b1 100644 --- a/crates/pgdumpx-cli/tests/global_options.rs +++ b/crates/pgdumpx-cli/tests/global_options.rs @@ -11,7 +11,11 @@ fn run(arguments: &[&str]) -> Output { fn version_reports_the_package_version_on_stdout() { let output = run(&["--version"]); - assert!(output.status.success(), "status: {:?}", output.status.code()); + assert!( + output.status.success(), + "status: {:?}", + output.status.code() + ); assert_eq!(output.stdout, b"pgdumpx 0.2.0\n"); assert!(output.stderr.is_empty()); } @@ -20,13 +24,20 @@ fn version_reports_the_package_version_on_stdout() { fn help_reports_global_usage_on_stdout() { let output = run(&["--help"]); - assert!(output.status.success(), "status: {:?}", output.status.code()); + assert!( + output.status.success(), + "status: {:?}", + output.status.code() + ); assert!(output.stderr.is_empty()); let stdout = String::from_utf8(output.stdout).expect("help is UTF-8"); assert!(stdout.starts_with("usage:\n")); for command in ["inspect", "list", "extract", "find"] { - assert!(stdout.contains(command), "missing command {command:?}: {stdout}"); + assert!( + stdout.contains(command), + "missing command {command:?}: {stdout}" + ); } assert!(stdout.ends_with('\n')); } From d2ee9ebe31ad5a475658c620f7c67dd85955f69c Mon Sep 17 00:00:00 2001 From: Yuma Takino <36617106+tappe9@users.noreply.github.com> Date: Tue, 1 Sep 2026 14:43:30 +0900 Subject: [PATCH 6/9] feat(cli): add global help and version options --- crates/pgdumpx-cli/Cargo.toml | 2 +- crates/pgdumpx-cli/src/entrypoint.rs | 93 ++++++++++++++++++++++++++++ 2 files changed, 94 insertions(+), 1 deletion(-) create mode 100644 crates/pgdumpx-cli/src/entrypoint.rs diff --git a/crates/pgdumpx-cli/Cargo.toml b/crates/pgdumpx-cli/Cargo.toml index 3f5789a..f16bd53 100644 --- a/crates/pgdumpx-cli/Cargo.toml +++ b/crates/pgdumpx-cli/Cargo.toml @@ -15,7 +15,7 @@ publish = ["crates-io"] [[bin]] name = "pgdumpx" -path = "src/main.rs" +path = "src/entrypoint.rs" doc = false [features] diff --git a/crates/pgdumpx-cli/src/entrypoint.rs b/crates/pgdumpx-cli/src/entrypoint.rs new file mode 100644 index 0000000..337dc98 --- /dev/null +++ b/crates/pgdumpx-cli/src/entrypoint.rs @@ -0,0 +1,93 @@ +use std::{ + env, + ffi::{OsStr, OsString}, + io::{self, BufWriter, Write}, + process::ExitCode, +}; + +const FAILURE_EXIT: u8 = 2; +const VERSION: &str = concat!("pgdumpx ", env!("CARGO_PKG_VERSION")); + +fn main() -> ExitCode { + match global_option(env::args_os()) { + Some(GlobalOption::Help) => write_global_output(application::usage()), + Some(GlobalOption::Version) => write_global_output(VERSION), + None => application::launch(), + } +} + +fn global_option(arguments: I) -> Option +where + I: IntoIterator, +{ + let mut arguments = arguments.into_iter(); + let _program = arguments.next(); + let option = arguments.next()?; + if arguments.next().is_some() { + return None; + } + + match option.as_os_str() { + value if value == OsStr::new("--help") || value == OsStr::new("-h") => { + Some(GlobalOption::Help) + } + value if value == OsStr::new("--version") || value == OsStr::new("-V") => { + Some(GlobalOption::Version) + } + _ => None, + } +} + +fn write_global_output(value: &str) -> ExitCode { + let stdout = io::stdout(); + let mut stdout = BufWriter::new(stdout.lock()); + if let Err(source) = writeln!(stdout, "{value}").and_then(|()| stdout.flush()) { + eprintln!("pgdumpx: stdout error: {source}"); + return ExitCode::from(FAILURE_EXIT); + } + ExitCode::SUCCESS +} + +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +enum GlobalOption { + Help, + Version, +} + +mod application { + include!("main.rs"); + + pub(super) fn launch() -> ExitCode { + main() + } + + pub(super) fn usage() -> &'static str { + USAGE + } +} + +#[cfg(test)] +mod tests { + use super::{GlobalOption, global_option}; + use std::ffi::OsString; + + #[test] + fn global_options_require_exactly_one_option_argument() { + assert_eq!(parse(&["--help"]), Some(GlobalOption::Help)); + assert_eq!(parse(&["-h"]), Some(GlobalOption::Help)); + assert_eq!(parse(&["--version"]), Some(GlobalOption::Version)); + assert_eq!(parse(&["-V"]), Some(GlobalOption::Version)); + assert_eq!(parse(&[]), None); + assert_eq!(parse(&["--help", "unexpected"]), None); + assert_eq!(parse(&["inspect"]), None); + } + + fn parse(arguments: &[&str]) -> Option { + global_option( + ["pgdumpx"] + .into_iter() + .chain(arguments.iter().copied()) + .map(OsString::from), + ) + } +} From 6eb78fcb8c5efd715770e9af8820ef97bd183b98 Mon Sep 17 00:00:00 2001 From: Yuma Takino <36617106+tappe9@users.noreply.github.com> Date: Tue, 1 Sep 2026 14:45:57 +0900 Subject: [PATCH 7/9] test: require a single published CLI binary --- scripts/test-release-readiness.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/scripts/test-release-readiness.py b/scripts/test-release-readiness.py index a5742d7..899efdb 100755 --- a/scripts/test-release-readiness.py +++ b/scripts/test-release-readiness.py @@ -117,6 +117,18 @@ def verify_manifest_metadata(errors: list[str]) -> None: ) cli = load_toml(ROOT / "crates" / "pgdumpx-cli" / "Cargo.toml") + require_equal( + errors, + "pgdumpx-cli.package.autobins", + cli.get("package", {}).get("autobins"), + False, + ) + require_equal( + errors, + "pgdumpx-cli binary targets", + cli.get("bin"), + [{"name": "pgdumpx", "path": "src/entrypoint.rs", "doc": False}], + ) dependency = cli.get("dependencies", {}).get("pgdumpx", {}) require_equal(errors, "pgdumpx-cli dependency path", dependency.get("path"), "../pgdumpx") require_equal(errors, "pgdumpx-cli dependency version", dependency.get("version"), VERSION) From e022c9ca8fd1a9117dbe17f2d653abb03c3b2a10 Mon Sep 17 00:00:00 2001 From: Yuma Takino <36617106+tappe9@users.noreply.github.com> Date: Tue, 1 Sep 2026 14:47:50 +0900 Subject: [PATCH 8/9] fix(cli): publish only the pgdumpx binary --- crates/pgdumpx-cli/Cargo.toml | 1 + docs/PACKAGING.md | 6 +-- scripts/test-release-readiness.py | 65 ++++++++++++++++++++++++++++++- 3 files changed, 68 insertions(+), 4 deletions(-) diff --git a/crates/pgdumpx-cli/Cargo.toml b/crates/pgdumpx-cli/Cargo.toml index f16bd53..84be6e2 100644 --- a/crates/pgdumpx-cli/Cargo.toml +++ b/crates/pgdumpx-cli/Cargo.toml @@ -1,6 +1,7 @@ [package] name = "pgdumpx-cli" description = "Command-line inspection, extraction, and row scanning for PostgreSQL custom-format dumps" +autobins = false version.workspace = true edition.workspace = true rust-version.workspace = true diff --git a/docs/PACKAGING.md b/docs/PACKAGING.md index a13b006..af2e995 100644 --- a/docs/PACKAGING.md +++ b/docs/PACKAGING.md @@ -9,9 +9,9 @@ This document records the package, dependency, and staged-verification boundary The workspace publishes two crates in dependency order: - `pgdumpx 0.2.0` — the reusable library; -- `pgdumpx-cli 0.2.0` — the `pgdumpx` command-line binary. +- `pgdumpx-cli 0.2.0` — the package that installs one executable named `pgdumpx`. -Both packages inherit edition 2024, Rust 1.85.0, the `MIT OR Apache-2.0` license expression, repository, homepage, and root README metadata. Each package declares crates.io discovery metadata and is restricted to the `crates-io` registry. The CLI dependency is: +Both packages inherit edition 2024, Rust 1.85.0, the `MIT OR Apache-2.0` license expression, repository, homepage, and root README metadata. Each package declares crates.io discovery metadata and is restricted to the `crates-io` registry. The CLI disables automatic binary discovery so `src/main.rs` remains an internal implementation module rather than a second published executable. Its library dependency is: ```toml pgdumpx = { path = "../pgdumpx", version = "0.2.0", default-features = false } @@ -56,7 +56,7 @@ python3 scripts/verify-release-packaging.py The audit performs all of the following without uploading a package: -1. validates version, package metadata, the CLI path-plus-version dependency, licenses, and workspace lint inheritance; +1. validates version, package metadata, the single CLI binary target, the CLI path-plus-version dependency, licenses, and workspace lint inheritance; 2. audits the locked default runtime dependency closure and accepted license expressions; 3. checks package file lists for required and forbidden content; 4. exercises default, no-optional, LZ4-only, and Zstandard-only library builds; diff --git a/scripts/test-release-readiness.py b/scripts/test-release-readiness.py index 899efdb..befba14 100755 --- a/scripts/test-release-readiness.py +++ b/scripts/test-release-readiness.py @@ -3,6 +3,8 @@ from __future__ import annotations +import json +import subprocess import sys import tomllib from pathlib import Path @@ -140,6 +142,65 @@ def verify_manifest_metadata(errors: list[str]) -> None: ) +def verify_cargo_binary_targets(errors: list[str]) -> None: + result = subprocess.run( + [ + "cargo", + "metadata", + "--format-version", + "1", + "--no-deps", + "--locked", + ], + cwd=ROOT, + check=False, + text=True, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + ) + if result.returncode != 0: + errors.append( + "cargo metadata failed while verifying CLI targets: " + + result.stderr.strip() + ) + return + + metadata = json.loads(result.stdout) + packages = [ + package + for package in metadata.get("packages", []) + if package.get("name") == "pgdumpx-cli" + ] + if len(packages) != 1: + errors.append( + f"cargo metadata returned {len(packages)} pgdumpx-cli packages, expected 1" + ) + return + + targets = [ + { + "name": target.get("name"), + "src_path": Path(target.get("src_path", "")) + .resolve() + .relative_to(ROOT) + .as_posix(), + } + for target in packages[0].get("targets", []) + if "bin" in target.get("kind", []) + ] + require_equal( + errors, + "pgdumpx-cli Cargo binary targets", + targets, + [ + { + "name": "pgdumpx", + "src_path": "crates/pgdumpx-cli/src/entrypoint.rs", + } + ], + ) + + def verify_lockfile(errors: list[str]) -> None: lockfile = load_toml(ROOT / "Cargo.lock") packages = lockfile.get("package", []) @@ -195,6 +256,7 @@ def verify_release_documents(errors: list[str]) -> None: "cargo package --workspace --locked", "cargo publish --dry-run -p pgdumpx --locked", "pgdumpx-cli", + "one executable named `pgdumpx`", ], ) require_snippets( @@ -227,10 +289,11 @@ def main() -> int: errors: list[str] = [] try: verify_manifest_metadata(errors) + verify_cargo_binary_targets(errors) verify_lockfile(errors) verify_release_documents(errors) verify_packaging_script(errors) - except (OSError, tomllib.TOMLDecodeError) as error: + except (json.JSONDecodeError, OSError, subprocess.SubprocessError, tomllib.TOMLDecodeError) as error: errors.append(str(error)) if errors: From a5c9bb8a53778e9659e23a9882064d962f926a23 Mon Sep 17 00:00:00 2001 From: Yuma Takino <36617106+tappe9@users.noreply.github.com> Date: Tue, 1 Sep 2026 14:58:50 +0900 Subject: [PATCH 9/9] fix(cli): keep wrapper items before included tests --- crates/pgdumpx-cli/src/entrypoint.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/crates/pgdumpx-cli/src/entrypoint.rs b/crates/pgdumpx-cli/src/entrypoint.rs index 337dc98..79cbb93 100644 --- a/crates/pgdumpx-cli/src/entrypoint.rs +++ b/crates/pgdumpx-cli/src/entrypoint.rs @@ -55,8 +55,6 @@ enum GlobalOption { } mod application { - include!("main.rs"); - pub(super) fn launch() -> ExitCode { main() } @@ -64,6 +62,8 @@ mod application { pub(super) fn usage() -> &'static str { USAGE } + + include!("main.rs"); } #[cfg(test)]