From 577934bd027a2412b013d65c380538412500005d Mon Sep 17 00:00:00 2001 From: Tony Hegyes Date: Thu, 29 Oct 2026 13:51:04 +0100 Subject: [PATCH 1/5] Check the plugin's own floor and phpunit.dist.xml at release The release compared the entry's Requires PHP with the require.php of the production packages in vendor/ and the scoped ones in vendor-prefixed/, but never with the project's own composer.json. A plugin that raised its own floor for its own syntax and forgot its header shipped, and with Composer's platform check off the header is the only gate, so sites between the two passed it and failed to parse the plugin. The baseline .distignore and the archive's forbidden-entry check both matched phpunit.xml* only, which misses phpunit.dist.xml, PHPUnit's default name. The header check now reads the project's composer.json beside the shipped packages, and both exclusions match phpunit*.xml*. A row whose root alone requires the higher floor and a phpunit.dist.xml archive row fail without the change. Assisted-by: Claude Code:claude-opus-5-5 --- .distignore | 2 +- .github/workflows/reusable-release.yml | 8 +++++-- docs/workflows.md | 2 +- tests/Unit/ReleaseWorkflowTest.php | 33 ++++++++++++++++++++++++++ 4 files changed, 41 insertions(+), 4 deletions(-) diff --git a/.distignore b/.distignore index 0ff15c2..2776436 100644 --- a/.distignore +++ b/.distignore @@ -20,7 +20,7 @@ package-lock.json phpcs*.xml* .phpstan.neon phpstan.dist.neon -phpunit.xml* +phpunit*.xml* infection.json* eslint.config.* stylelint.config.* diff --git a/.github/workflows/reusable-release.yml b/.github/workflows/reusable-release.yml index e7d0418..c0b0fba 100644 --- a/.github/workflows/reusable-release.yml +++ b/.github/workflows/reusable-release.yml @@ -310,8 +310,12 @@ jobs: return $lowest; }; - // Scoped packages ship from vendor-prefixed/ although Composer counts them as development packages. + // The project's own manifest states the floor of the plugin's code, and scoped packages ship from vendor-prefixed/ although Composer counts them as development packages. $packages = []; + $project = is_file('composer.json') ? json_decode((string) file_get_contents('composer.json'), true) : null; + if (is_array($project)) { + $packages[] = $project; + } if (is_file('vendor/composer/installed.json')) { $installed = json_decode((string) file_get_contents('vendor/composer/installed.json'), true); foreach ($installed['packages'] ?? [] as $package) { @@ -444,7 +448,7 @@ jobs: echo "::error::Archive must not contain $entry." status=1 ;; - "$PLUGIN_SLUG/phpunit.xml"|"$PLUGIN_SLUG/phpunit.xml."*|"$PLUGIN_SLUG/infection.json"|"$PLUGIN_SLUG/infection.json."*) + "$PLUGIN_SLUG/phpunit"*.xml*|"$PLUGIN_SLUG/infection.json"|"$PLUGIN_SLUG/infection.json."*) echo "::error::Archive must not contain $entry." status=1 ;; diff --git a/docs/workflows.md b/docs/workflows.md index cb55828..909f274 100644 --- a/docs/workflows.md +++ b/docs/workflows.md @@ -231,7 +231,7 @@ The `build` job fails when: - the entry file's `Version:` header differs from the tag, or from the `Stable tag:` of `readme.txt` when that file exists; a wp.org release requires `readme.txt`; - the entry file's `Text Domain:` header, or `extra.text-domain` in `composer.json` when declared, differs from `plugin-slug`; - `vendor-prefixed/` carries strings in the plugin's text domain but the regenerated POT has none of them; -- `Requires PHP` is below the PHP floor of a shipped package, the highest lower bound of the `require.php` constraints of the production packages in `vendor/` and the scoped packages in `vendor-prefixed/`; +- `Requires PHP` is below the PHP floor of a shipped package, the highest lower bound of the `require.php` constraints of the project's own `composer.json`, the production packages in `vendor/` and the scoped packages in `vendor-prefixed/`; - `Requires at least` is below the `extra.requires-wp` version a shipped package declares, or `Requires Plugins` lacks a slug listed in a shipped package's `extra.requires-plugins`; - the archive misses the entry file, `readme.txt` for a wp.org release, or, when `composer.json` declares `extra.scoping-prefix`, `vendor-prefixed/scoper-autoload.php` and the files of every scoped package; - the archive holds a development or secret file, such as a `phpcs*.xml*` ruleset, `composer.json` or `.env`. diff --git a/tests/Unit/ReleaseWorkflowTest.php b/tests/Unit/ReleaseWorkflowTest.php index 54432f4..7ce37e0 100644 --- a/tests/Unit/ReleaseWorkflowTest.php +++ b/tests/Unit/ReleaseWorkflowTest.php @@ -241,6 +241,38 @@ public function test_headers_must_cover_the_shipped_packages( array $headers, ar } } + /** + * @return array + */ + public static function provide_project_floors(): array { + return array( + 'header-meets-the-project-floor' => array( '8.5', 0 ), + 'header-below-the-project-floor' => array( '7.4', 1 ), + ); + } + + #[DataProvider( 'provide_project_floors' )] + public function test_headers_must_cover_the_project_s_own_floor( string $requires_php, int $expected_status ): void { + $this->write_project( + array( + 'main.php' => self::plugin_header( array( 'Requires PHP' => $requires_php ) ), + 'composer.json' => (string) \json_encode( + array( + 'name' => 'acme/plugin', + 'require' => array( 'php' => '>=8.5' ), + ) + ), + ) + ); + + $result = $this->run_build_step( 'Verify the headers cover the shipped packages', array() ); + + self::assertSame( $expected_status, $result['status'], $result['output'] ); + if ( 1 === $expected_status ) { + self::assertMatchesRegularExpression( '/^::error::.*acme\/plugin/m', $result['output'] ); + } + } + /** * @return array */ @@ -250,6 +282,7 @@ public static function provide_archive_entries(): array { 'tests-ruleset' => array( 'phpcs.tests.dist.xml', 1 ), 'hidden-tests-ruleset' => array( '.phpcs.tests.xml', 1 ), 'dist-suffixed-ruleset' => array( 'phpcs.xml.dist', 1 ), + 'dist-named-phpunit' => array( 'phpunit.dist.xml', 1 ), ); } From 7d9944b468827e736d7ee82121711dc5342816fd Mon Sep 17 00:00:00 2001 From: Tony Hegyes Date: Thu, 29 Oct 2026 13:51:04 +0100 Subject: [PATCH 2/5] Test the release's scoped-package, provenance and notes steps AGENTS.md says the unit tests run the release's assertion steps, but "Assert scoped packages bundled", "Verify required workflows passed", the release-notes extraction and the opt-in phpstan.dist.neon.php path discovery had no row, so a change to their logic would pass. They fail closed, but nothing pinned what they accept. Rows now run each step: a complete scoped archive, a missing autoloader or package and a build whose scoping never ran; every, one or no required workflow among the push runs, a malformed list and an unavailable API; a dated, a bracketed and an absent changelog section, a heading whose dots are not wildcards and no changelog at all; and the path discovery over a plugin-shaped directory. Assisted-by: Claude Code:claude-opus-5-5 --- tests/Unit/PhpstanConfigTest.php | 29 +++++++ tests/Unit/ReleaseWorkflowTest.php | 131 +++++++++++++++++++++++++++++ 2 files changed, 160 insertions(+) diff --git a/tests/Unit/PhpstanConfigTest.php b/tests/Unit/PhpstanConfigTest.php index 9143e5d..06a16fb 100644 --- a/tests/Unit/PhpstanConfigTest.php +++ b/tests/Unit/PhpstanConfigTest.php @@ -20,6 +20,35 @@ public function test_global_statements_take_the_types_their_function_documents() self::assertSame( array( 'method.nonObject' ), $this->analyse( 'globals.php' ) ); } + public function test_the_path_discovery_adds_a_plugin_s_conventional_files_and_scans_its_scoped_dependencies(): void { + $project = ( \realpath( \sys_get_temp_dir() ) ?: \sys_get_temp_dir() ) . '/wp-configs-phpstan-' . \uniqid(); + foreach ( array( 'src', 'includes', 'blocks', 'vendor-prefixed/acme/library', 'docs' ) as $dir ) { + \mkdir( "$project/$dir", 0777, true ); + } + foreach ( array( 'functions.php', 'uninstall.php', 'footprint.php' ) as $file ) { + \file_put_contents( "$project/$file", " array( + 'paths' => array( "$project/functions.php", "$project/uninstall.php", "$project/src", "$project/includes", "$project/blocks" ), + 'scanDirectories' => array( "$project/vendor-prefixed" ), + ), + ), + $config + ); + } + /** * @return list */ diff --git a/tests/Unit/ReleaseWorkflowTest.php b/tests/Unit/ReleaseWorkflowTest.php index 7ce37e0..2401178 100644 --- a/tests/Unit/ReleaseWorkflowTest.php +++ b/tests/Unit/ReleaseWorkflowTest.php @@ -300,6 +300,137 @@ public function test_archive_must_not_ship_development_files( string $entry, int self::assertSame( $expected_status, $result['status'], $result['output'] ); } + /** + * @return array, bool, int, string}> + */ + public static function provide_scoped_archives(): array { + $complete = array( 'acme-plugin.php', 'vendor-prefixed/scoper-autoload.php', 'vendor-prefixed/acme/library/src/Client.php' ); + + return array( + 'every-scoped-file-shipped' => array( $complete, true, 0, '' ), + 'autoloader-missing' => array( array( 'acme-plugin.php', 'vendor-prefixed/acme/library/src/Client.php' ), true, 1, 'scoper-autoload.php' ), + 'scoped-package-missing' => array( array( 'acme-plugin.php', 'vendor-prefixed/scoper-autoload.php' ), true, 1, 'vendor-prefixed/acme/library/' ), + 'scoping-never-ran' => array( $complete, false, 1, 'did not run' ), + ); + } + + /** + * @param list $archived + */ + #[DataProvider( 'provide_scoped_archives' )] + public function test_the_archive_must_ship_every_scoped_package( array $archived, bool $scoped, int $expected_status, string $named ): void { + $stage = $this->work_dir . '/stage/acme-plugin'; + foreach ( $archived as $file ) { + if ( ! \is_dir( \dirname( "$stage/$file" ) ) ) { + \mkdir( \dirname( "$stage/$file" ), 0777, true ); + } + \file_put_contents( "$stage/$file", "work_dir . '/project/acme-plugin.zip' ) ), $output, $zip_status ); + self::assertSame( 0, $zip_status ); + if ( $scoped ) { + $this->write_project( array( 'vendor-prefixed/acme/library/composer.json' => '{"name": "acme/library"}' ) ); + } + + $result = $this->run_build_step( 'Assert scoped packages bundled', array() ); + + self::assertSame( $expected_status, $result['status'], $result['output'] ); + if ( '' !== $named ) { + self::assertMatchesRegularExpression( '/^::error::.*' . \preg_quote( $named, '/' ) . '/m', $result['output'] ); + } + } + + /** + * @return array + */ + public static function provide_provenance_runs(): array { + $required = '[".github/workflows/tests.yml", ".github/workflows/quality.yml"]'; + + return array( + 'every-required-workflow-passed' => array( $required, ".github/workflows/quality.yml\n.github/workflows/tests.yml\n", 0, '' ), + 'one-required-workflow-missing' => array( $required, ".github/workflows/tests.yml\n", 1, '.github/workflows/quality.yml' ), + 'required-workflows-not-a-list' => array( '".github/workflows/tests.yml"', '', 1, 'non-empty JSON array' ), + 'actions-api-unavailable' => array( $required, 'FAIL', 1, 'Unable to list' ), + ); + } + + #[DataProvider( 'provide_provenance_runs' )] + public function test_a_release_needs_a_successful_push_run_of_every_required_workflow( string $required, string $runs, int $expected_status, string $named ): void { + $this->write_executable( 'gh', "#!/usr/bin/env bash\nif [ \"\$(cat \"\$HOME/runs\")\" = FAIL ]; then exit 1; fi\ncat \"\$HOME/runs\"\n" ); + \file_put_contents( $this->work_dir . '/runs', $runs ); + + $result = $this->run_step( + 'reusable-release.yml', + 'provenance', + 'Verify required workflows passed', + array( + 'GH_TOKEN' => 'token', + 'REPOSITORY' => 'acme/acme-plugin', + 'SHA' => 'abc123', + 'REQUIRED_WORKFLOWS' => $required, + ) + ); + + self::assertSame( $expected_status, $result['status'], $result['output'] ); + if ( '' !== $named ) { + self::assertMatchesRegularExpression( '/^::error::.*' . \preg_quote( $named, '/' ) . '/m', $result['output'] ); + } + } + + /** + * @return array + */ + public static function provide_changelogs(): array { + return array( + 'dated-section' => array( "# Changelog\n\n## 1.2.0 - 2026-10-29\n\n- Fixed a thing.\n\n## 1.1.0 - 2026-10-01\n\n- An older thing.\n", 0, "\n- Fixed a thing.\n\n" ), + 'bracketed-section' => array( "## [1.2.0] - 2026-10-29\n\n- Fixed a thing.\n", 0, "\n- Fixed a thing.\n" ), + 'dots-are-not-wildcards' => array( "## 1a2a0 - 2026-10-29\n\n- Not this version.\n", 1, '' ), + 'no-section-for-the-version' => array( "## 1.1.0 - 2026-10-01\n\n- An older thing.\n", 1, '' ), + 'no-changelog' => array( null, 0, '' ), + ); + } + + #[DataProvider( 'provide_changelogs' )] + public function test_the_release_notes_are_the_version_s_changelog_section( ?string $changelog, int $expected_status, string $notes ): void { + $this->write_executable( 'gh', "#!/usr/bin/env bash\nprintf '%s\\n' \"\$@\" > \"\$HOME/gh-arguments\"\n" ); + if ( ! \is_null( $changelog ) ) { + $this->write_project( array( 'CHANGELOG.md' => $changelog ) ); + } + \mkdir( $this->work_dir . '/runner-temp' ); + + $result = $this->run_step( + 'reusable-release.yml', + 'release', + 'Create the GitHub release', + array( + 'GH_TOKEN' => 'token', + 'CHANGELOG' => 'CHANGELOG.md', + 'VERSION' => '1.2.0', + 'RUNNER_TEMP' => $this->work_dir . '/runner-temp', + 'GITHUB_REPOSITORY' => 'acme/acme-plugin', + 'GITHUB_REF_NAME' => 'v1.2.0', + 'PLUGIN_SLUG' => 'acme-plugin', + ) + ); + + self::assertSame( $expected_status, $result['status'], $result['output'] ); + if ( 1 === $expected_status ) { + self::assertMatchesRegularExpression( '/^::error::.*no section for 1\.2\.0/m', $result['output'] ); + self::assertFileDoesNotExist( $this->work_dir . '/gh-arguments' ); + return; + } + + $arguments = \file( $this->work_dir . '/gh-arguments', \FILE_IGNORE_NEW_LINES ); + self::assertIsArray( $arguments ); + if ( \is_null( $changelog ) ) { + self::assertContains( '--generate-notes', $arguments ); + self::assertNotContains( '--notes-file', $arguments ); + } else { + self::assertContains( '--notes-file', $arguments ); + self::assertSame( $notes, \file_get_contents( $this->work_dir . '/runner-temp/release-notes.md' ) ); + } + } + public function test_required_files_are_found_in_a_large_archive(): void { $stage = $this->work_dir . '/stage/acme-plugin'; \mkdir( "$stage/assets", 0777, true ); From db942479630a1b658a3d3bb9c7bb9b848d56510e Mon Sep 17 00:00:00 2001 From: Tony Hegyes Date: Thu, 29 Oct 2026 13:54:44 +0100 Subject: [PATCH 3/5] Declare TypeScript as a peer of the Node baselines node/tsconfig.base.json carries only deltas from TypeScript 6's defaults, strict mode among them, but the package declared no typescript peer, so a consumer resolving TypeScript 5 turned strict off without a word. The package lists typescript >=6.0.0 beside its other tool peers, the major it is tested against (6.0.3), and the README names it. Assisted-by: Claude Code:claude-opus-5-5 --- README.md | 2 +- package-lock.json | 4 ++-- package.json | 3 ++- 3 files changed, 5 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 62f38b4..0961b53 100644 --- a/README.md +++ b/README.md @@ -217,7 +217,7 @@ A recipe returns php-scoper settings for one package, with paths relative to it: ## Node baselines -`node/` holds one baseline per tool, which a project extends and overrides. The package declares the tools behind them as `>=` peer dependencies, floored at the versions it is tested against: `@wordpress/scripts`, `@wordpress/eslint-plugin`, `@wordpress/stylelint-config`, `@wordpress/postcss-plugins-preset` and `@playwright/test`. +`node/` holds one baseline per tool, which a project extends and overrides. The package declares the tools behind them as `>=` peer dependencies, floored at the versions it is tested against: `@wordpress/scripts`, `@wordpress/eslint-plugin`, `@wordpress/stylelint-config`, `@wordpress/postcss-plugins-preset`, `@playwright/test` and `typescript`, whose defaults `node/tsconfig.base.json` builds on. | Baseline | Holds | | --- | --- | diff --git a/package-lock.json b/package-lock.json index 68c808b..0987b56 100644 --- a/package-lock.json +++ b/package-lock.json @@ -26,7 +26,8 @@ "@wordpress/eslint-plugin": ">=28.0.0", "@wordpress/postcss-plugins-preset": ">=6.0.0", "@wordpress/scripts": ">=37.0.0", - "@wordpress/stylelint-config": ">=27.0.0" + "@wordpress/stylelint-config": ">=27.0.0", + "typescript": ">=6.0.0" } }, "node_modules/@babel/code-frame": { @@ -18313,7 +18314,6 @@ "version": "6.0.3", "resolved": "https://registry.npmjs.org/typescript/-/typescript-6.0.3.tgz", "integrity": "sha512-y2TvuxSZPDyQakkFRPZHKFm+KKVqIisdg9/CZwm9ftvKXLP8NRWj38/ODjNbr43SsoXqNuAisEf1GdCxqWcdBw==", - "dev": true, "license": "Apache-2.0", "peer": true, "bin": { diff --git a/package.json b/package.json index 9ee5fe6..e66a88f 100644 --- a/package.json +++ b/package.json @@ -30,7 +30,8 @@ "@wordpress/eslint-plugin": ">=28.0.0", "@wordpress/postcss-plugins-preset": ">=6.0.0", "@wordpress/scripts": ">=37.0.0", - "@wordpress/stylelint-config": ">=27.0.0" + "@wordpress/stylelint-config": ">=27.0.0", + "typescript": ">=6.0.0" }, "dependencies": { "prettier": "npm:wp-prettier@^3.0.3" From ad195496390473630b4a28731fc6c87535ae63f3 Mon Sep 17 00:00:00 2001 From: Tony Hegyes Date: Thu, 29 Oct 2026 13:54:44 +0100 Subject: [PATCH 4/5] Name the configs floors by the keys that hold them The README's Requirements gave PHP 8.5, Node 26, npm 11 and WordPress 7.1 as numbers that composer.json, package.json and the PHPCS and PHPStan profiles own, and AGENTS.md required the copies, so a floor change had to edit prose in step with the manifests. The style guide allows no number another file owns, as the framework's READMEs follow. The README names each file and key that holds a floor, and AGENTS.md says so instead. Assisted-by: Claude Code:claude-opus-5-5 --- AGENTS.md | 2 +- README.md | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index 5880e2e..5fcb5f8 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -45,7 +45,7 @@ zizmor --config .github/zizmor.yml .github/ - Every reusable-workflow input has a `description:`; shipped input names, types and defaults change only as a coordinated migration (CONTRIBUTING.md). - Self-CI calls the reusable workflows through `./`, and `.github/zizmor.yml` turns off zizmor's `self-repository` audit until actionlint accepts `$/`. - The README is an index; workflow inputs live in `docs/workflows.md`, script and file contracts in `docs/scripts-contract.md`, test tiers in `tests/README.md`. A behavior change updates them in the same change. -- The README's Requirements state the PHP, WordPress, Node and npm floors; a floor change updates them in the same commit as `composer.json`, `package.json`, `php/quality-assurance/phpcs.base.dist.xml` and `php/quality-assurance/phpstan.dist.neon`. +- The README names each floor by the file and key that hold it (`composer.json`'s `require.php`, `package.json`'s `engines`, `phpcs.base.dist.xml`'s `testVersion` and `minimum_wp_version`, `phpstan.dist.neon`'s `requiresAtLeast`), never by its number, so a floor change edits only those files. - Class and method docblocks open with one third-person sentence naming the role, such as "Returns …", and add a second paragraph only for a non-obvious why; property and constant docblocks open with "The …". A function or method docblock ends with `@return`, `@return void` included, unless it documents a constructor or opens with `{@inheritDoc}`. Each `@param` stays on one line, `@throws` reads "Thrown when …", members are `protected` rather than `private`, and class bodies outside `tests/` group members in `// region NAME` blocks. - Inline comments state a non-obvious why in one capitalized sentence, and every file describes the present, never the change. - Markdown keeps one line per paragraph, sentence-case headings and an impersonal, present-tense voice. diff --git a/README.md b/README.md index 0961b53..bb080e6 100644 --- a/README.md +++ b/README.md @@ -4,10 +4,10 @@ Shared PHPCS, PHPStan and php-scoper configurations, Node tool baselines and reu ## Requirements -- PHP 8.5 or later, and Composer 2. -- For the Node baselines only, Node 26 or later and npm 11 or later. +- PHP at the floor `composer.json`'s `require.php` names, and Composer 2. +- For the Node baselines only, the Node and npm versions `package.json`'s `engines` names, and the tools its `peerDependencies` list. -The shared PHPCS profiles check code against PHP 8.5 or later and WordPress 7.1 or later, and the PHPStan profile against WordPress 7.1 or later; a project with lower floors lowers them as [the scripts contract](docs/scripts-contract.md#lowering-the-floors) shows for PHPCS and [its PHPStan section](docs/scripts-contract.md#phpstan) for PHPStan. +The shared PHPCS profiles check code against the PHP and WordPress floors that `php/quality-assurance/phpcs.base.dist.xml` sets in `testVersion` and `minimum_wp_version`, and the PHPStan profile against the WordPress floor that `php/quality-assurance/phpstan.dist.neon` sets in `requiresAtLeast`; a project with lower floors lowers them as [the scripts contract](docs/scripts-contract.md#lowering-the-floors) shows for PHPCS and [its PHPStan section](docs/scripts-contract.md#phpstan) for PHPStan. ## Installation From 9f5d435edd290b3848b64a15e198ff3decba2a01 Mon Sep 17 00:00:00 2001 From: Tony Hegyes Date: Thu, 29 Oct 2026 13:54:45 +0100 Subject: [PATCH 5/5] Keep CI comments to one sentence of why Six comments in the workflows and dependabot.yml ran to several sentences, one with an all-caps MUST, against the rule that an inline comment states a non-obvious why in one sentence. Each now states its why once. Assisted-by: Claude Code:claude-opus-5-5 --- .github/dependabot.yml | 3 +-- .github/workflows/reusable-release.yml | 5 +---- .../workflows/reusable-workflow-checks.yml | 21 +++---------------- .github/workflows/workflow-checks.yml | 4 +--- 4 files changed, 6 insertions(+), 27 deletions(-) diff --git a/.github/dependabot.yml b/.github/dependabot.yml index ecc52cb..4287759 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -40,8 +40,7 @@ updates: open-pull-requests-limit: 5 cooldown: default-days: 7 - # This source repo keeps action bumps grouped. Consumer repos split policy at review time: - # unprivileged build-side reusable bumps can automerge; credentialed deploy bumps need review. + # Action bumps stay grouped here, because consumers decide per bump which ones may automerge. groups: github-actions: patterns: diff --git a/.github/workflows/reusable-release.yml b/.github/workflows/reusable-release.yml index c0b0fba..4bfe0e8 100644 --- a/.github/workflows/reusable-release.yml +++ b/.github/workflows/reusable-release.yml @@ -460,10 +460,7 @@ jobs: echo "::error::Archive must not contain $entry." status=1 ;; - # Bundled CA/cert files under vendor/ or vendor-prefixed/ are legitimate - # runtime resources, not secrets. This exempt arm MUST stay ahead of the broad - # *.pem/*.key secret arm below — `case` stops at the first match, so reordering or - # merging the two would start flagging bundled certs (or suppress the secret check). + # Bundled certificates are runtime resources, and this arm stays ahead of the secret arm below because case stops at the first match. "$PLUGIN_SLUG/vendor/"*.pem|"$PLUGIN_SLUG/"*"/vendor/"*.pem|"$PLUGIN_SLUG/vendor/"*.key|"$PLUGIN_SLUG/"*"/vendor/"*.key \ |"$PLUGIN_SLUG/vendor-prefixed/"*.pem|"$PLUGIN_SLUG/vendor-prefixed/"*.key) ;; diff --git a/.github/workflows/reusable-workflow-checks.yml b/.github/workflows/reusable-workflow-checks.yml index ea7b818..9f86c5b 100644 --- a/.github/workflows/reusable-workflow-checks.yml +++ b/.github/workflows/reusable-workflow-checks.yml @@ -36,16 +36,7 @@ jobs: GH_TOKEN: ${{ github.token }} REPO: ${{ github.repository }} run: | - # code-scanning/alerts is GHAS-gated: a 200 (even an empty list) means the feature is - # available; a 403 naming Advanced Security is the exact failure the SARIF upload below - # would hit on a private repo without the feature, so probing this surfaces it first. A - # confirmed-absent 403 is trusted immediately. Everything else — a curl error, a transient - # 5xx, an unrecognized 403 body — is ambiguous and gets retried up to max_attempts times - # before the loop gives up. - # - # An unresolved ambiguity resolves to "unavailable": the native-mode zizmor branch below - # still scans and fails the job on any finding, so guessing wrong only costs the - # Security-tab upload — a safe degradation. + # The code-scanning alerts endpoint answers 403 naming Advanced Security exactly where the SARIF upload would fail, so an unclear answer is retried and then read as unavailable, which only skips the upload. probe_body="${RUNNER_TEMP}/ghas-probe.json" max_attempts=3 retry_sleep_seconds=5 @@ -106,10 +97,7 @@ jobs: with: version: ${{ env.ZIZMOR_VERSION }} - # No-GHAS path: the same pinned action in native mode. advanced-security:false skips the - # Security-tab upload and drops --format=sarif, so zizmor runs in plain mode and its own - # nonzero exit on any finding fails the job directly — no count gate on this branch. - # annotations:true surfaces findings inline on the PR. + # Without Advanced Security, zizmor runs in plain mode, where its own nonzero exit on a finding fails the job. - name: Run zizmor without Advanced Security if: needs.detect-ghas.outputs.ghas-available == 'false' uses: zizmorcore/zizmor-action@cc914d7f3750a2d13d75c7f184a1060aa0e9d482 # v0.6.4 @@ -123,10 +111,7 @@ jobs: env: SARIF: ${{ steps.zizmor.outputs.output-file }} run: | - # Fail closed: only a well-formed SARIF whose runs[].results count is a single integer is - # trusted. A missing/empty file, unparseable JSON, parseable-but-non-SARIF JSON ({}, null, - # {"runs":{}}), or a multi-document count is an unknown scan result and must not pass — an - # empty/non-integer $count would otherwise make `[ -ne 0 ]` error and the `if` fall through. + # Only a SARIF whose result count is one integer is trusted, because a non-integer count would make the comparison error and the check fall through. if [ ! -s "$SARIF" ]; then echo "::error::zizmor produced no SARIF at '$SARIF' — cannot confirm a clean scan." exit 1 diff --git a/.github/workflows/workflow-checks.yml b/.github/workflows/workflow-checks.yml index f2b27c7..5f422c1 100644 --- a/.github/workflows/workflow-checks.yml +++ b/.github/workflows/workflow-checks.yml @@ -7,9 +7,7 @@ on: pull_request: paths: ['.github/workflows/**'] schedule: - # Weekly Monday 06:17 UTC — off the top of the hour, where GitHub sheds scheduled runs, and - # staggered against the other weekly crons. Keeps zizmor's online audits (e.g. - # known-vulnerable-actions) fresh between workflow changes. + # A weekly run off the hour keeps zizmor's online audits current between workflow changes. - cron: '17 6 * * 1' workflow_dispatch: