From 8095d0e5913789693a94600517ab62ebcc02909f Mon Sep 17 00:00:00 2001 From: Mohamed Abdulalim Date: Thu, 12 Nov 2026 21:17:52 +0200 Subject: [PATCH 1/2] Prepare 1.0.0-rc.1 and verify release archives --- .github/workflows/ci.yml | 20 +- CHANGELOG.md | 8 +- CONTRIBUTING.md | 3 +- PHP_PERSISTENCE_PACKAGE_REFERENCE.md | 6 +- README.md | 10 +- SECURITY.md | 2 +- composer.json | 3 +- docs/architecture/CI_AND_INTEGRATION.md | 3 +- docs/guides/CONSUMER_VERIFICATION.md | 2 +- docs/guides/RELEASE_ARTIFACT_VERIFICATION.md | 15 ++ docs/guides/USAGE_GUIDE.md | 2 +- llms.txt | 2 +- scripts/release-artifact.php | 183 ++++++++++++++++++ .../IntegrationInfrastructureTest.php | 16 +- 14 files changed, 258 insertions(+), 17 deletions(-) create mode 100644 docs/guides/RELEASE_ARTIFACT_VERIFICATION.md create mode 100644 scripts/release-artifact.php diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 8c543f3..c59d7b9 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -224,6 +224,24 @@ jobs: test -z "$(docker ps -aq --filter 'label=com.docker.compose.project')" test -z "$(docker volume ls -q --filter 'label=com.docker.compose.project')" + release-artifact: + name: Release artifact verification + runs-on: ubuntu-24.04 + timeout-minutes: 10 + steps: + # actions/checkout v7.0.1 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 + with: + persist-credentials: false + ref: ${{ github.event.pull_request.head.sha || github.sha }} + - uses: ./.github/actions/setup-php-composer + with: + php-version: "8.4" + - name: Verify the exact committed artifact + env: + ARTIFACT_SHA: ${{ github.event.pull_request.head.sha || github.sha }} + run: composer release:verify -- --target=1.0.0-rc.1 --sha="$ARTIFACT_SHA" + exploratory: name: Exploratory MySQL Innovation (not required) if: github.event_name == 'workflow_dispatch' && inputs.exploratory @@ -246,7 +264,7 @@ jobs: ci-gate: name: CI gate if: always() - needs: [workflow-lint, static, compat, integration, examples-smoke, consumer-verification] + needs: [workflow-lint, static, compat, integration, examples-smoke, consumer-verification, release-artifact] runs-on: ubuntu-24.04 timeout-minutes: 5 steps: diff --git a/CHANGELOG.md b/CHANGELOG.md index 0d9cf46..29dc2f3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,9 +2,11 @@ All notable changes to this project are documented in this file. -The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/). Versioning will follow [Semantic Versioning](https://semver.org/) once a version is published. **Nothing in this file has been published, tagged, or released.** See [README.md](README.md) and [SECURITY.md](SECURITY.md) for the current Development/Unpublished status. +The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/) and [Semantic Versioning](https://semver.org/). -## [Unreleased] +## [1.0.0-rc.1] + +First release candidate target. Publication state recorded during preparation: **Unpublished**. ### Added @@ -13,6 +15,8 @@ The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/). Ver - Pagination capability: `PdoPaginator`, returning a validated `PageResultDTO`, with whitelist-sorted, bounded, parameterized pagination and a documented non-single-snapshot caveat for its count queries. - Ordering capability: `ScopedOrderingManager` and `ScopedOrderingConfig`, with global/scoped/nullable-scoped integer ordering, a plain next-position read, a locking next-position read, and a locked, clamped, savepoint-protected move. - `PHP_PERSISTENCE_PACKAGE_REFERENCE.md` as the canonical Public Runtime API, behavior, and exception reference; `docs/guides/USAGE_GUIDE.md`; runnable, Public-API-only `examples/`; and `llms.txt`. +- External Composer consumer verification and legacy coexistence checks on real MySQL, including snapshot, savepoint, and deadlock replay evidence. +- Release Artifact Verification for the exact Git and Composer archives of this target. - Composer manifest, PHPStan at level `max`, PER-CS 3.1 code-style verification, and a real MySQL/MariaDB Integration test infrastructure (`scripts/integration.php`, `compose.integration.yaml`, `docker/integration-matrix.json`). No database version range is declared as supported; see [PHP_PERSISTENCE_PACKAGE_REFERENCE.md §8](PHP_PERSISTENCE_PACKAGE_REFERENCE.md#8-database-engine-support). Durable decisions governing the changes above are indexed in [docs/decisions/DECISIONS_INDEX.md](docs/decisions/DECISIONS_INDEX.md). diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 36885c8..900c1ce 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -38,6 +38,7 @@ composer test:regression # no Docker needed composer test:integration # needs Docker; starts a disposable MySQL/MariaDB service composer examples:smoke # needs Docker; smoke-executes examples/ against a real engine composer test:consumer # needs Docker; two fresh external Composer verification runs +composer release:verify -- --target=1.0.0-rc.1 --sha="$(git rev-parse HEAD)" actionlint .github/workflows/*.yml ``` @@ -57,7 +58,7 @@ This package does not commit `composer.lock`. CI resolves both `latest` and `low - Keep a pull request to one coherent change; include its own tests and any directly affected documentation in the same change rather than splitting them across separate pull requests. - Describe the change, its rationale, and which local commands above you ran. -- All applicable CI gates (workflow lint, static checks, compatibility matrix, Integration, example smoke, and consumer verification) must pass before a merge is considered; CI gating and Git/merge permissions follow [AGENTS.md](AGENTS.md) and the pinned `AI_COLLABORATION_WORKFLOW_AR.md`/`GITHUB_PHASE_STACK_WORKFLOW_AR.md` — in particular, merges require explicit Owner authorization. +- All applicable CI gates (workflow lint, static checks, compatibility matrix, Integration, example smoke, consumer verification, and release artifact verification) must pass before a merge is considered; CI gating and Git/merge permissions follow [AGENTS.md](AGENTS.md) and the pinned `AI_COLLABORATION_WORKFLOW_AR.md`/`GITHUB_PHASE_STACK_WORKFLOW_AR.md` — in particular, merges require explicit Owner authorization. ## Reporting a security issue diff --git a/PHP_PERSISTENCE_PACKAGE_REFERENCE.md b/PHP_PERSISTENCE_PACKAGE_REFERENCE.md index c118b9c..365f02d 100644 --- a/PHP_PERSISTENCE_PACKAGE_REFERENCE.md +++ b/PHP_PERSISTENCE_PACKAGE_REFERENCE.md @@ -2,7 +2,7 @@ `maatify/php-persistence` — canonical stable public/runtime/behavioral contract. -Status: **Development / Unpublished.** Nothing described here is published, installable from Packagist, or tagged. This document is the single canonical Package Reference required by the pinned [Package Building Standard](docs/php-engineering-standards/standards/packages/PACKAGE_BUILDING_STANDARD.md) §3. It owns the stable public Runtime API inventory, behavior and exception guarantees, and package boundaries. It supersedes [docs/architecture/CONTRACT_MATRIX.md](docs/architecture/CONTRACT_MATRIX.md) and [docs/architecture/BEHAVIOR_CONTRACT.md](docs/architecture/BEHAVIOR_CONTRACT.md) as the *current* public-contract surface; those documents remain as dated design/evidence records (legacy mapping and the test evidence recorded per behavior) and are not a second competing contract. +Artifact target: **Release Candidate `1.0.0-rc.1`**. Publication state recorded during preparation: **Unpublished**. This document is the single canonical Package Reference required by the pinned [Package Building Standard](docs/php-engineering-standards/standards/packages/PACKAGE_BUILDING_STANDARD.md) §3. It owns the stable public Runtime API inventory, behavior and exception guarantees, and package boundaries. It supersedes [docs/architecture/CONTRACT_MATRIX.md](docs/architecture/CONTRACT_MATRIX.md) and [docs/architecture/BEHAVIOR_CONTRACT.md](docs/architecture/BEHAVIOR_CONTRACT.md) as the *current* public-contract surface; those documents remain as dated design/evidence records (legacy mapping and the test evidence recorded per behavior) and are not a second competing contract. Composer package identity, PHP/extension requirements, dependencies, autoloading, configuration and distribution are owned exclusively by [`composer.json`](composer.json) under the pinned [Composer Package Standard](docs/php-engineering-standards/standards/packages/COMPOSER_PACKAGE_STANDARD.md). This document explains runtime implications of that manifest; it does not restate or override it. @@ -43,9 +43,9 @@ This package is a separate Composer package from the published `maatify/persiste - Published legacy `v1.4.0` and the unpublished legacy `main` used as source provenance are distinct baselines ([DEC-007](docs/decisions/DEC-007-legacy-source-baseline-and-deprecated-retirement.md)). This package has its own namespace and public API. - Consumer migration from `maatify/persistence` to this package is explicitly **not** addressed by this document and needs its own separate authorization ([DEC-006](docs/decisions/DEC-006-successor-intent-and-standards-ownership-boundary.md)). -### 1.4 Development / Unpublished state +### 1.4 Release Candidate target -No version of this package is published, resolvable, or installable through Packagist or any other Composer distribution channel, and no Git tag exists. `composer.json` governs the declared package identity, PHP/extension requirements (`php ^8.4`, `ext-pdo`, `ext-pdo_mysql`), and dependency (`maatify/exceptions ^1.0`); see that file for the authoritative, current declaration. +The [README](README.md#installation) records the exact installation target and the condition for external Composer resolution. `composer.json` governs the declared package identity, PHP/extension requirements (`php ^8.4`, `ext-pdo`, `ext-pdo_mysql`), and dependency (`maatify/exceptions ^1.0`); see that file for the authoritative, current declaration. --- diff --git a/README.md b/README.md index 6d9d096..8454901 100644 --- a/README.md +++ b/README.md @@ -28,7 +28,7 @@ Direct-PDO Transaction, Pagination, and Ordering utilities for MySQL and MariaDB `maatify/php-persistence` is a small, direct-PDO toolkit with three independent capabilities over a **caller-supplied** `PDO` connection: running callbacks inside a transaction or a caller's savepoint (with optional bounded deadlock replay), parameterized whitelist-sorted bounded pagination, and locking next-position/move operations for integer display-order columns. It is developed as a separate package from the earlier, published `maatify/persistence` ([coexistence rules](docs/decisions/DEC-005-coexistence-with-legacy-package.md)), not a drop-in replacement. -This repository is in **Development**. Nothing described here is published, tagged, or installable through Packagist or any other Composer distribution channel. +This tree prepares the **Release Candidate artifact `1.0.0-rc.1`**. The live project is in **Development** while the target is unpublished. The publication state recorded for this preparation is **Unpublished**. ## Key Features @@ -49,7 +49,13 @@ These are declared runtime requirements, not a statement that any specific datab ## Installation -This package is **not published**. It has no Packagist listing, no Git tag, and no installable version; `composer require maatify/php-persistence` does not resolve anything today. The intended Composer identity is `maatify/php-persistence` once a release exists; see [CHANGELOG.md](CHANGELOG.md) for the current `[Unreleased]` state. +The exact Composer installation target for this artifact is: + +```bash +composer require maatify/php-persistence:1.0.0-rc.1 +``` + +This command resolves through the approved Composer distribution source only after that exact version is published. See the [target changelog](CHANGELOG.md#100-rc1) and [security policy](SECURITY.md) for lifecycle and support details. ## Usage diff --git a/SECURITY.md b/SECURITY.md index 83352d0..48fce2e 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -5,7 +5,7 @@ ## Supported Versions -This package is in **Development** and has never published a Stable release or a SemVer Release Candidate. There is currently no supported Stable release line. +This artifact represents Release Candidate `1.0.0-rc.1`. The publication state recorded during preparation was **Unpublished**. No Stable release line is supported, and this release candidate establishes no Stable support commitment. ## Reporting a Vulnerability diff --git a/composer.json b/composer.json index 7616f85..7680dfa 100644 --- a/composer.json +++ b/composer.json @@ -62,7 +62,8 @@ "test:regression": "phpunit --testsuite regression", "test:integration": "@php scripts/integration.php", "examples:smoke": "@php scripts/examples-smoke.php", - "test:consumer": "@php scripts/consumer-verification.php" + "test:consumer": "@php scripts/consumer-verification.php", + "release:verify": "@php scripts/release-artifact.php" }, "config": { "optimize-autoloader": true, diff --git a/docs/architecture/CI_AND_INTEGRATION.md b/docs/architecture/CI_AND_INTEGRATION.md index 99b6496..25951a3 100644 --- a/docs/architecture/CI_AND_INTEGRATION.md +++ b/docs/architecture/CI_AND_INTEGRATION.md @@ -22,9 +22,10 @@ Every CI gate runs a repository-owned command that a developer can run locally w | Integration suite (real database) | `composer test:integration` | Integration | | Standalone examples (real database) | `composer examples:smoke` | Examples smoke | | Consumer verification and coexistence | `composer test:consumer` | Consumer verification and coexistence | +| Exact-target release archives | [`composer release:verify`](../guides/RELEASE_ARTIFACT_VERIFICATION.md) | Release artifact verification | | Workflow lint | `actionlint` over `.github/workflows` | Workflow lint | -Unit and Regression need no Docker. `composer test` runs Unit, Regression, and Integration locally. The required `CI gate` aggregates Workflow lint, Static checks, Compat, Integration, Examples smoke, and Consumer verification and coexistence. The exploratory cell is optional. +Unit and Regression need no Docker. `composer test` runs Unit, Regression, and Integration locally. The required `CI gate` aggregates Workflow lint, Static checks, Compat, Integration, Examples smoke, Consumer verification and coexistence, and Release artifact verification. The exploratory cell is optional. ## 2. Integration lifecycle diff --git a/docs/guides/CONSUMER_VERIFICATION.md b/docs/guides/CONSUMER_VERIFICATION.md index 261af93..5373ad3 100644 --- a/docs/guides/CONSUMER_VERIFICATION.md +++ b/docs/guides/CONSUMER_VERIFICATION.md @@ -24,4 +24,4 @@ The [behavior contract](../architecture/BEHAVIOR_CONTRACT.md) labels published l The orchestrator is checked by the package's PHPStan configuration. The independent consumer fixture is syntax-checked and validated through execution in the two production Composer roots, where its legacy imports resolve. -The harness is additional to Unit, Regression, Integration and Examples Smoke. This evidence concerns an unpublished candidate, with no production host migration or support-range claim. DEC-009 remains deferred, and DEC-013 records test versions. +The harness is additional to Unit, Regression, Integration and Examples Smoke. This evidence concerns a candidate tree before publication. It provides neither production host migration evidence nor a support-range claim. DEC-009 remains deferred, and DEC-013 records test versions. diff --git a/docs/guides/RELEASE_ARTIFACT_VERIFICATION.md b/docs/guides/RELEASE_ARTIFACT_VERIFICATION.md new file mode 100644 index 0000000..0a72a74 --- /dev/null +++ b/docs/guides/RELEASE_ARTIFACT_VERIFICATION.md @@ -0,0 +1,15 @@ +# Release Artifact Verification + +The preparation target for this tree is `maatify/php-persistence:1.0.0-rc.1`. Its publication state during preparation is Unpublished. + +Run the same archive checks as CI from a checkout containing the exact committed candidate. The command needs Git, tar, PHP and Composer. + +```sh +composer release:verify -- --target=1.0.0-rc.1 --sha="$(git rev-parse HEAD)" +``` + +The verifier creates an isolated checkout of the selected commit. It generates a VCS archive with `git archive` and a Composer archive with `composer archive`, independently of each other. The verification checks the actual entries and bytes, including required consumer files, the full runtime source, manifest identity and production PSR-4 mapping, and repository-only or sensitive paths. The log records the target, commit, tree, and a file inventory and SHA-256 for each generated archive. The temporary workspace is removed when verification ends. The sensitive-path and private-key checks are structural. The Lead reviews release claims, changelog allocation and the contents for sensitive data in context. + +In pull-request CI the archive job checks out and verifies the exact PR HEAD. On a push it verifies the exact pushed commit. Qualification requires Full Applicable CI and Release Artifact Verification on the same actual, Owner-authorized `main` commit and exact target. The [Consumer Verification Harness](CONSUMER_VERIFICATION.md) separately exercises runtime consumption. + +After separately authorized publication, the approved Composer channel provides the version and distribution evidence for Published Artifact Verification. Its archive and install mode must be observed there. This preparation step makes no assumption about a source-only distribution mode. diff --git a/docs/guides/USAGE_GUIDE.md b/docs/guides/USAGE_GUIDE.md index c8cf72f..d27a49c 100644 --- a/docs/guides/USAGE_GUIDE.md +++ b/docs/guides/USAGE_GUIDE.md @@ -2,7 +2,7 @@ Consumer-facing integration guidance for `maatify/php-persistence`. This guide explains how to use the three capabilities; it does not define a technical contract of its own — every normative rule here is a presentation of what [PHP_PERSISTENCE_PACKAGE_REFERENCE.md](../../PHP_PERSISTENCE_PACKAGE_REFERENCE.md) already defines. Where the two disagree, the Package Reference governs. -**Status:** this package is unpublished (Development). Nothing in this guide implies a published, installable version. +**Artifact target:** Release Candidate `1.0.0-rc.1`. See the [README](../../README.md#installation) for the exact installation target and when it resolves. ## Before you start diff --git a/llms.txt b/llms.txt index 5fef887..5c596d2 100644 --- a/llms.txt +++ b/llms.txt @@ -1,6 +1,6 @@ # PHP Persistence -> Direct-PDO Transaction, Pagination, and Ordering utilities for MySQL and MariaDB, over a caller-supplied `PDO` connection. Development/Unpublished: not installable from Packagist today. +> Direct-PDO Transaction, Pagination, and Ordering utilities for MySQL and MariaDB, over a caller-supplied `PDO` connection. Release Candidate artifact target: 1.0.0-rc.1. See README for the conditional Composer installation target. This map is for package consumers. Use the Package Reference for the public runtime contract and the guide and examples for usage. Repository workflow and governance are documented separately for contributors in CONTRIBUTING.md. diff --git a/scripts/release-artifact.php b/scripts/release-artifact.php new file mode 100644 index 0000000..102a8c3 --- /dev/null +++ b/scripts/release-artifact.php @@ -0,0 +1,183 @@ + $arguments + * @param array $environment + */ +function releaseCommand(array $arguments, string $directory, array $environment = []): string +{ + [$status, $output, $errors] = execute($arguments, $environment, $directory, 90); + if ($status !== 0) { + throw new RuntimeException(sprintf('%s failed (%d): %s', $arguments[0], $status, trim($errors))); + } + + return trim($output); +} + +function releaseFile(string $path): string +{ + $contents = file_get_contents($path); + if ($contents === false) { + throw new RuntimeException('Cannot read release file: ' . basename($path)); + } + + return $contents; +} + +function verifyReleasePresentation(string $directory, string $target): void +{ + $manifest = json_decode(releaseFile($directory . '/composer.json'), true, 512, JSON_THROW_ON_ERROR); + $autoload = is_array($manifest) ? ($manifest['autoload'] ?? null) : null; + if (!is_array($manifest) || ($manifest['name'] ?? null) !== RELEASE_PACKAGE + || ($manifest['type'] ?? null) !== 'library' || ($manifest['license'] ?? null) !== 'proprietary' + || array_key_exists('version', $manifest) || !is_array($autoload) + || ($autoload['psr-4'] ?? null) !== ['Maatify\\PhpPersistence\\' => 'src/'] + ) { + throw new RuntimeException('Unexpected archived Composer identity or production autoload'); + } + + $readme = releaseFile($directory . '/README.md'); + $changelog = releaseFile($directory . '/CHANGELOG.md'); + $security = releaseFile($directory . '/SECURITY.md'); + $targetHeading = preg_quote('## [' . $target . ']', '/'); + if (!str_contains($readme, 'composer require ' . RELEASE_PACKAGE . ':' . $target) + || !str_contains($readme, 'Release Candidate artifact `' . $target . '`') + || preg_match('/^' . $targetHeading . '$/m', $changelog) !== 1 + || preg_match('/^' . $targetHeading . ' - /m', $changelog) !== 0 + || !str_contains($security, 'Release Candidate `' . $target . '`') + ) { + throw new RuntimeException('Archived presentation is missing an exact, undated release target'); + } +} + +/** + * @param non-empty-list $requiredFiles + * @return list + */ +function verifyReleaseArchive(string $archive, string $workspace, string $source, string $label, string $target, array $requiredFiles): array +{ + $files = []; + foreach (explode("\n", releaseCommand(['tar', '-tf', $archive], $workspace)) as $entry) { + $path = rtrim($entry, '/'); + if ($path === '' || preg_match('~^(?:src|docs|examples)(?:/|$)|^(?:' . implode('|', array_map( + static fn(string $file): string => preg_quote($file, '~'), + [...RELEASE_FILES, 'AGENTS.md', 'CODE_OF_CONDUCT.md', 'CONTRIBUTING.md'], + )) . ')$~D', $path) !== 1 + || preg_match('~(?:^|/)(?:[.]\.?|[.]env[^/]*|[.]git[^/]*|[.]idea|[.]vscode|vendor|tests|harness|composer[.]lock|auth[.]json)(?:/|$)|[.](?:key|pem|pfx|p12)$~iD', $path) !== 0 + ) { + throw new RuntimeException($label . ' contains an unexpected archive path: ' . $path); + } + if (!str_ends_with($entry, '/')) { + $files[] = $path; + } + } + if (count($files) !== count(array_unique($files)) || array_diff($requiredFiles, $files) !== []) { + throw new RuntimeException($label . ' omits required files or contains duplicate paths'); + } + + $directory = $workspace . '/' . $label; + if (!mkdir($directory, 0700)) { + throw new RuntimeException('Cannot create archive inspection directory'); + } + releaseCommand(['tar', '-xf', $archive, '-C', $directory], $workspace); + foreach ($files as $path) { + if (is_link($directory . '/' . $path) || !is_file($source . '/' . $path)) { + throw new RuntimeException($label . ' contains an unexpected file type or untracked file: ' . $path); + } + $contents = releaseFile($directory . '/' . $path); + if ($contents !== releaseFile($source . '/' . $path)) { + throw new RuntimeException($label . ' contents do not match the selected commit: ' . $path); + } + if (preg_match('/-----BEGIN (?:[A-Z0-9]+ )*PRIVATE KEY-----/', $contents) === 1) { + throw new RuntimeException($label . ' contains private key material: ' . $path); + } + } + verifyReleasePresentation($directory, $target); + sort($files); + + return $files; +} + +$root = dirname(__DIR__); +$arguments = $_SERVER['argv'] ?? null; +if (!is_array($arguments) || count($arguments) !== 3 || !is_string($arguments[1]) || !is_string($arguments[2]) + || $arguments[1] !== '--target=' . RELEASE_TARGET || preg_match('/^--sha=([0-9a-f]{40})$/D', $arguments[2], $matches) !== 1 +) { + fail(EXIT_USAGE, 'Usage: composer release:verify -- --target=' . RELEASE_TARGET . ' --sha='); +} + +$sha = $matches[1]; +$target = RELEASE_TARGET; +$workspace = sys_get_temp_dir() . '/pp-release-' . bin2hex(random_bytes(12)); +if (!mkdir($workspace, 0700)) { + fail(EXIT_INFRASTRUCTURE, 'Cannot create release verification workspace'); +} + +$status = EXIT_TEST_FAILURE; +$tree = ''; +try { + if (releaseCommand(['git', 'rev-parse', $sha . '^{commit}'], $root) !== $sha) { + throw new RuntimeException('Release SHA must name the exact selected commit'); + } + $tree = releaseCommand(['git', 'rev-parse', $sha . '^{tree}'], $root); + $objects = releaseCommand(['git', 'ls-tree', '-r', $sha], $root); + if (preg_match('/^(?:120000|160000) /m', $objects) !== 0) { + throw new RuntimeException('Release source contains symbolic links or submodules'); + } + echo "RELEASE ARTIFACT candidate target={$target} sha={$sha} tree={$tree}\n"; + + $source = $workspace . '/source'; + releaseCommand(['git', 'clone', '--quiet', '--shared', '--no-checkout', $root, $source], $workspace); + releaseCommand(['git', '-c', 'advice.detachedHead=false', 'checkout', '--detach', $sha], $source); + $runtime = releaseCommand(['git', 'ls-tree', '-r', '--name-only', $sha, '--', 'src'], $source); + if ($runtime === '') { + throw new RuntimeException('Release source has no runtime files'); + } + $requiredFiles = [...RELEASE_FILES, ...explode("\n", $runtime)]; + + releaseCommand(['git', 'archive', '--format=tar', '--output=' . $workspace . '/git.tar', $sha], $source); + releaseCommand(['composer', 'archive', '--no-interaction', '--no-plugins', '--no-scripts', '--format=tar', '--dir=' . $workspace, '--file=composer'], $source, [ + 'COMPOSER' => $source . '/composer.json', + 'COMPOSER_HOME' => $workspace . '/composer-home', + ]); + foreach (['git', 'composer'] as $label) { + $archive = $workspace . '/' . $label . '.tar'; + $files = verifyReleaseArchive($archive, $workspace, $source, $label, $target, $requiredFiles); + echo 'RELEASE INVENTORY ' . json_encode(['archive' => $label, 'sha256' => hash_file('sha256', $archive), 'files' => $files], JSON_THROW_ON_ERROR) . "\n"; + } + $status = 0; +} catch (Throwable $exception) { + fwrite(STDERR, 'Release artifact verification failed: ' . $exception->getMessage() . "\n"); +} finally { + [$cleanup] = execute(['rm', '-rf', '--', $workspace]); + if ($cleanup !== 0 || file_exists($workspace)) { + fwrite(STDERR, "Release verification workspace cleanup failed\n"); + $status = EXIT_INFRASTRUCTURE; + } +} + +if ($status === 0) { + echo "RELEASE ARTIFACT VERIFICATION OK target={$target} sha={$sha} tree={$tree}\n"; +} +exit($status); diff --git a/tests/Regression/IntegrationInfrastructureTest.php b/tests/Regression/IntegrationInfrastructureTest.php index 27ae578..6e2fede 100644 --- a/tests/Regression/IntegrationInfrastructureTest.php +++ b/tests/Regression/IntegrationInfrastructureTest.php @@ -181,7 +181,7 @@ public function testWorkflowFollowsTheHardeningRules(): void $names[] = $chunks[$index]; self::assertStringContainsString('timeout-minutes:', $chunks[$index + 1], $chunks[$index] . ' needs a timeout'); } - self::assertSame(['workflow-lint', 'static', 'compat', 'integration', 'examples-smoke', 'consumer-verification', 'exploratory', 'ci-gate'], $names); + self::assertSame(['workflow-lint', 'static', 'compat', 'integration', 'examples-smoke', 'consumer-verification', 'release-artifact', 'exploratory', 'ci-gate'], $names); } private static function constantSeconds(string $source, string $name): int @@ -246,7 +246,7 @@ public function testGateInspectsEveryRequiredJob(): void [, $gate] = array_pad(explode("\n ci-gate:\n", $workflow, 2), 2, ''); self::assertMatchesRegularExpression( - '/^ needs: \[workflow-lint, static, compat, integration, examples-smoke, consumer-verification\]$/m', + '/^ needs: \[workflow-lint, static, compat, integration, examples-smoke, consumer-verification, release-artifact\]$/m', $gate, ); self::assertStringNotContainsString('exploratory', $gate, 'the exploratory job is not required'); @@ -257,6 +257,18 @@ public function testGateInspectsEveryRequiredJob(): void self::assertStringContainsString('echo "$RESULTS" | jq -e \'all(.[]; .result == "success")\' > /dev/null', $gate); } + public function testReleaseArtifactGateVerifiesTheExactWorkflowSha(): void + { + $workflow = self::read('.github/workflows/ci.yml'); + [, $following] = array_pad(explode("\n release-artifact:\n", $workflow, 2), 2, ''); + [$job] = explode("\n exploratory:\n", $following, 2); + self::assertStringContainsString('ref: ${{ github.event.pull_request.head.sha || github.sha }}', $job); + self::assertStringContainsString('ARTIFACT_SHA: ${{ github.event.pull_request.head.sha || github.sha }}', $job); + self::assertStringContainsString('composer release:verify -- --target=1.0.0-rc.1 --sha="$ARTIFACT_SHA"', $job); + self::assertStringContainsString('persist-credentials: false', $job); + self::assertStringContainsString('"release:verify": "@php scripts/release-artifact.php"', self::read('composer.json')); + } + public function testComposerAndToolingAreProvisionedDeterministically(): void { $action = self::read('.github/actions/setup-php-composer/action.yml'); From 2387100c7715866768ecc51dd047e2fc35a2cb3b Mon Sep 17 00:00:00 2001 From: Mohamed Abdulalim Date: Thu, 12 Nov 2026 21:24:05 +0200 Subject: [PATCH 2/2] Make release artifact presentation durable and validate changelog target --- README.md | 4 ++-- SECURITY.md | 2 +- composer.json | 2 +- docs/architecture/ARCHITECTURE.md | 4 +++- scripts/release-artifact.php | 7 ++++--- 5 files changed, 11 insertions(+), 8 deletions(-) diff --git a/README.md b/README.md index 8454901..9838c74 100644 --- a/README.md +++ b/README.md @@ -4,10 +4,10 @@ ![Maatify.dev](https://www.maatify.dev/assets/img/img/maatify_logo_white.svg) -[![Status](https://img.shields.io/badge/Status-Development-blue)](README.md) +[![Status at preparation](https://img.shields.io/badge/Status%20at%20preparation-Development-blue)](README.md#overview) [![PHP](https://img.shields.io/badge/PHP-%5E8.4-8892BF)](composer.json) [![License](https://img.shields.io/badge/License-Proprietary-green)](LICENSE) -[![PHPStan](https://img.shields.io/badge/PHPStan-Level%20Max-brightgreen)](phpstan.neon) +[![PHPStan](https://img.shields.io/badge/PHPStan-Level%20Max-brightgreen)](README.md#quality-status) [![Maatify Ecosystem](https://img.shields.io/badge/Maatify-Ecosystem-blueviolet)](https://github.com/Maatify) diff --git a/SECURITY.md b/SECURITY.md index 48fce2e..2b57761 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -5,7 +5,7 @@ ## Supported Versions -This artifact represents Release Candidate `1.0.0-rc.1`. The publication state recorded during preparation was **Unpublished**. No Stable release line is supported, and this release candidate establishes no Stable support commitment. +This artifact represents Release Candidate `1.0.0-rc.1`. At preparation, the target was **Unpublished** and the package had no supported Stable release line. This release candidate establishes no Stable support commitment. ## Reporting a Vulnerability diff --git a/composer.json b/composer.json index 7680dfa..747f76c 100644 --- a/composer.json +++ b/composer.json @@ -1,6 +1,6 @@ { "name": "maatify/php-persistence", - "description": "PDO persistence utilities for MySQL and MariaDB, in development.", + "description": "Direct-PDO transaction, pagination, and ordering utilities for MySQL and MariaDB.", "keywords": [ "persistence", "pdo", diff --git a/docs/architecture/ARCHITECTURE.md b/docs/architecture/ARCHITECTURE.md index 76f1d44..83b0cf4 100644 --- a/docs/architecture/ARCHITECTURE.md +++ b/docs/architecture/ARCHITECTURE.md @@ -2,7 +2,9 @@ `maatify/php-persistence` — direct-PDO transaction, pagination and ordering utilities, designed for MySQL and MariaDB. -Status: **Development**. This document describes the intended architecture decided in WU-3. Since WU-4 the repository contains a Composer manifest and quality tooling; its CI (WU-5) runs the quality gates and real-database infrastructure tests. WU-6 implemented the Transaction capability (`PdoTransactionRunner`, `PdoSavepointTransactionRunner`, their exceptions, and the `@internal` executor and savepoint helper). WU-7 implemented the Pagination capability (`PdoPaginator`, `PageRequest`, `PaginationConfig`, `PdoPaginationQueryDescriptor`, `PageResultDTO`, `SortWhitelist`, `SortDirectionEnum`, and their exceptions). WU-8 implemented the Ordering capability (`ScopedOrderingConfig`, `ScopedOrderingManager`, and their exceptions), reusing the Transaction capability's Gen2 `PdoTransactionRunner` and `@internal` `SavepointScope` rather than a second savepoint helper, per [DEC-011](../decisions/DEC-011-responsibility-placement-and-savepoint-helper.md). WU-9 delivered the canonical [Package Reference](../../PHP_PERSISTENCE_PACKAGE_REFERENCE.md), the consumer-facing README, a Usage Guide, runnable examples, `llms.txt`, and the remaining release-facing files, verified against the current source and tests rather than against this design record. Nothing here is released or installable. Statements about database engine support are design targets, not claims ([DATABASE_SUPPORT_AND_TEST_POLICY.md](./DATABASE_SUPPORT_AND_TEST_POLICY.md)): the Transaction capability's own tests, including real deadlocks, the Pagination capability's own tests, including a deterministic concurrent-writer snapshot test (B-08a), and the Ordering capability's own tests, including real stale-snapshot, lock-wait and deadlock cases, pass on named MySQL and MariaDB versions, which is evidence for those versions, not a declared supported range. +This document records the architecture decided in WU-3. WU-4 and WU-5 established Composer packaging, quality tooling, and real-database CI. WU-6 through WU-8 implemented Transaction, Pagination, and Ordering. Ordering reuses the Transaction capability's Gen2 runner and its `@internal` savepoint helper under [DEC-011](../decisions/DEC-011-responsibility-placement-and-savepoint-helper.md). WU-9 delivered the canonical [Package Reference](../../PHP_PERSISTENCE_PACKAGE_REFERENCE.md), usage guidance, and runnable examples. + +WU-10 added [consumer verification and coexistence](../guides/CONSUMER_VERIFICATION.md). WU-11 prepares the `1.0.0-rc.1` artifact and its [archive verification](../guides/RELEASE_ARTIFACT_VERIFICATION.md). See the [README](../../README.md#installation) for installation and the publication state recorded for this artifact. The [database policy](DATABASE_SUPPORT_AND_TEST_POLICY.md) records test evidence for named MySQL and MariaDB versions, including deadlocks, pagination snapshots, and ordering concurrency. That evidence is limited to the tested versions. ## 1. Document map diff --git a/scripts/release-artifact.php b/scripts/release-artifact.php index 102a8c3..2876274 100644 --- a/scripts/release-artifact.php +++ b/scripts/release-artifact.php @@ -60,11 +60,12 @@ function verifyReleasePresentation(string $directory, string $target): void $readme = releaseFile($directory . '/README.md'); $changelog = releaseFile($directory . '/CHANGELOG.md'); $security = releaseFile($directory . '/SECURITY.md'); - $targetHeading = preg_quote('## [' . $target . ']', '/'); + $targetHeading = '## [' . $target . ']'; + $targetLabel = preg_quote('[' . $target . ']', '/'); if (!str_contains($readme, 'composer require ' . RELEASE_PACKAGE . ':' . $target) || !str_contains($readme, 'Release Candidate artifact `' . $target . '`') - || preg_match('/^' . $targetHeading . '$/m', $changelog) !== 1 - || preg_match('/^' . $targetHeading . ' - /m', $changelog) !== 0 + || preg_match_all('/^[ \t]{0,3}##[ \t]+' . $targetLabel . '[^\r\n]*$/m', $changelog, $headings) !== 1 + || $headings[0] !== [$targetHeading] || !str_contains($security, 'Release Candidate `' . $target . '`') ) { throw new RuntimeException('Archived presentation is missing an exact, undated release target');