Skip to content

fix(keys-sync): report GPG execution failures - #1634

Merged
potiuk merged 2 commits into
apache:mainfrom
MakerFlyFly:fix/keys-sync-gpg-errors-upstream
Oct 16, 2026
Merged

potiuk merged 2 commits into
apache:mainfrom
MakerFlyFly:fix/keys-sync-gpg-errors-upstream

Conversation

@MakerFlyFly

@MakerFlyFly MakerFlyFly commented Oct 15, 2026 •

Copy link
Copy Markdown
Contributor

Summary

  • Check GPG import and listing return codes so execution failures produce the existing error JSON and helper exit 2 instead of key_found: false.
  • Validate and read regular input completely, then import those same bytes through stdin. Unreadable inputs, directories and failed declared public-key armor are errors, including truncated armor whose GPG statuses resemble ordinary no-data.
  • Preserve the bounded ordinary no-data exception, normal ten-field JSON, business-verdict exit 0, key-assessment policy and temporary keyring cleanup.

Type of change

  • Other: deterministic Python helper and sibling tests shipped with keys-sync.

Test plan

  • The import-failure and failed-listing regressions fail against unchanged upstream helper 0a1f044e and pass with the repair. The old-helper replay honours its text=True subprocess interface.
  • Real-GPG directory and truncated-armor regressions fail against the first repair (e1c2e10a) and pass with the reviewed follow-up.
  • Linux, Python 3.12.12 / GPG 2.2.40, UID 65534: 61 keys-sync tests, 60 passed and the Windows-only test skipped.
  • Windows, Python 3.12.13 / Git/MSYS GPG 2.4.8: 61 keys-sync tests, 52 passed and nine compatible-native-homedir tests skipped; the production MSYS error regression runs and passes.
  • Linux plugins: Ruff, formatting and mypy pass; pytest reports 422 passed, one skipped.
  • Windows plugins: Ruff, formatting and mypy pass; pytest reports 230 passed, ten skipped with GPG absent from the default PATH.
  • Real-GPG boundary probes: 17 Linux cases and 14 Windows control cases cover valid armored/binary keys, ordinary English/Unicode text, empty and mismatched inputs, malformed armor, CRC errors, partial imports, unreadable input and keyring/listing failures. Temporary keyrings are removed.
  • Production Windows/MSYS CLI comparison changes the original false missing-key result/exit 0 into an import error/exit 2. Control-only homedir translation is not production functionality.
  • The nine existing eval prompts render byte-identically to the audited run; saved author answers pass an exact/structural compatibility replay. This is not a fresh or independent model evaluation.
  • Full prek run --all-files, including lychee, passes on this HEAD in the Linux checkout; both changed source files match the Windows checkout byte for byte. No tracked files were changed by the checks.
  • Magpie pre-first-pr-check against upstream main: all five categories pass, ready with zero blocking or advisory items.
  • Upstream CI on 4c0bae57: required prek, zizmor and tests-ok pass, including the Ready-event reruns. CodeQL and RAT also pass. No old-head success is reused.

RFC-AI-0004 compliance

The helper continues to handle public information in a disposable keyring. It adds no signing operation, network operation or outbound message. Release signing remains with the RM.

Linked issues

Fixes #1633

Notes for reviewers

This branch is based on upstream main 0a1f044e and contains only the two reviewed helper/test changes from fork Draft PR #12. The source files match reviewed fork HEAD 06056d2c byte for byte. The fork issue and PR remain audit references.

Suggested labels: bug, family:release-management, capability:fix.

Coordination with #1625: that pending PR removes both files changed here and replaces the GPG entry point with release-verify list-keys. Its proposed reader still ignores import/listing return codes. This PR fixes current main; it does not claim conflict-free integration with that migration. If this repair merges first, #1625 must retain/migrate the error handling and regressions. If #1625 merges first, this PR must be adapted to its replacement entry point. No changes to the migration or shared signature-verification functions are bundled here.

No key-strength, signing-subkey admission, signer-selection, signing or signature-verification policy changes. This remains independent of issue #1628, including Option 2.

Windows/MSYS homedir compatibility is still unresolved; production acceptance is correct error reporting with exit 2. Native Windows GPG was unavailable for separate verification.

No skill prompt or eval fixture changes. Existing Step 1 model fixtures have no helper-error case, so model-layer error forwarding has not been independently evaluated. Fork PR #12's last HEAD had no reported remote CI; this PR requires checks on its new HEAD.

Was generative AI tooling used to co-author this PR?
  • Yes — OpenAI Codex (GPT-6) assisted with code, tests and text. The contributor approved the reviewed repair and upstream submission. Commits carry Generated-by:. Upstream maintainer review is pending.

@github-actions github-actions Bot added capability:resolve Close-out: invalidate, dedupe, CVE allocate, announcement family:release-management release-* skills labels Oct 15, 2026
@MakerFlyFly
MakerFlyFly marked this pull request as ready for review October 15, 2026 04:39
Check import and listing return codes before assessing a fetched public
key. Preserve missing-key results only for a confirmed no-data import
followed by a successful empty listing.

Include the GPG stage and diagnostics in the existing error response,
and cover both operational failures and the compatible no-data cases.

Generated-by: OpenAI Codex (GPT-6)
Read regular key files completely before considering an empty input,
and import the same bytes through stdin. Reject failed imports of a
public-key block even when GPG reports NODATA with zero import counts.

Cover directories, truncated armor, readable binary keys and input
changes with real-GPG regressions while preserving plain no-key text.

Generated-by: OpenAI Codex (GPT-6)
@potiuk
potiuk force-pushed the fix/keys-sync-gpg-errors-upstream branch from 4c0bae5 to 39e771d Compare October 16, 2026 01:46
@potiuk

potiuk commented Oct 16, 2026

Copy link
Copy Markdown
Member

Rebased onto current main and pushed a fix (maintainer edit).

#1635 added a signers field to the helper output, so test_completed_assessments_preserve_the_ten_field_contract failed on the merged result (CI on the old head predated that merge). The test now expects signers and is renamed ..._preserve_the_field_contract. All 72 keys-sync tests pass locally with real GPG.

Note for the author, not changed here: a roborev pass suggested that GnuPG 2.4.1+ may need an empty common.conf in the temporary home when --no-autostart is used, as release-verify does. The tests pass on this machine's GPG, and the flag predates this PR, so I left it.

Still open for maintainers: ordering against #1625, which replaces this file.

@potiuk potiuk left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Rebased and fixed the field-contract test after #1635; verified locally (72 keys-sync tests pass).

@potiuk
potiuk merged commit d09de64 into apache:main Oct 16, 2026
10 checks passed
potiuk pushed a commit to potiuk/magpie that referenced this pull request Oct 16, 2026
* fix(keys-sync): report GPG execution failures

Check import and listing return codes before assessing a fetched public
key. Preserve missing-key results only for a confirmed no-data import
followed by a successful empty listing.

Include the GPG stage and diagnostics in the existing error response,
and cover both operational failures and the compatible no-data cases.

Generated-by: OpenAI Codex (GPT-6)

* fix(keys-sync): reject malformed key input

Read regular key files completely before considering an empty input,
and import the same bytes through stdin. Reject failed imports of a
public-key block even when GPG reports NODATA with zero import counts.

Cover directories, truncated armor, readable binary keys and input
changes with real-GPG regressions while preserving plain no-key text.

Generated-by: OpenAI Codex (GPT-6)
potiuk pushed a commit to Kaap10/magpie that referenced this pull request Oct 16, 2026
* fix(keys-sync): report GPG execution failures

Check import and listing return codes before assessing a fetched public
key. Preserve missing-key results only for a confirmed no-data import
followed by a successful empty listing.

Include the GPG stage and diagnostics in the existing error response,
and cover both operational failures and the compatible no-data cases.

Generated-by: OpenAI Codex (GPT-6)

* fix(keys-sync): reject malformed key input

Read regular key files completely before considering an empty input,
and import the same bytes through stdin. Reject failed imports of a
public-key block even when GPG reports NODATA with zero import counts.

Cover directories, truncated armor, readable binary keys and input
changes with real-GPG regressions while preserving plain no-key text.

Generated-by: OpenAI Codex (GPT-6)
potiuk added a commit that referenced this pull request Oct 16, 2026
…er fixes into the tool and read S3 and version paths right

Port of the key-checker fixes main landed on the script this series moves
into tools/release-management (#1634, #1635, #1646):

- A key passes only with a signing-capable primary or subkey that itself
  meets the strength floor and is not revoked, invalid, disabled or
  expired; a certify-only primary with no such subkey is blocked
  (`signers` in the check output).
- GPG failures are reported, never read as "key not found": unreadable or
  non-file input, a failed import or listing, and gpg failing to start are
  errors naming the stage and exit code, with the "no key data" exception
  ported as is. release-verify list-keys, which runs gpg for keys-sync,
  now also reports each key's capabilities and subkeys.
- Every test of main's test_check_key.py has a ported equivalent.

Adversarial-review findings:

- archive-sweep reads S3 URLs with promote's whole-host path-style rule,
  so a virtual-hosted bucket whose name starts with "s3" keeps its key.
- A release_dist_url_template with a literal around <version> (v<version>,
  foo-<version>-bin) gives the right release directory and archive URLs.
- keys-sync reads a GitHub KEYS URL as branch (the first segment after
  blob/) plus a path that may include subdirectories, so KEYS can live
  in e.g. dist/; a new optional keys_file_branch names a branch that
  contains '/' (release/1.x). Without it, a subdirectory reading is
  warned; a URL whose file is not KEYS is refused.
- The keyserver's UIDs are compared with KEYS as a set, so a reordered
  keyserver listing is no longer reported as a rolled key.

Generated-by: Claude Opus 5
potiuk added a commit that referenced this pull request Oct 16, 2026
…er fixes into the tool and read S3 and version paths right

Port of the key-checker fixes main landed on the script this series moves
into tools/release-management (#1634, #1635, #1646):

- A key passes only with a signing-capable primary or subkey that itself
  meets the strength floor and is not revoked, invalid, disabled or
  expired; a certify-only primary with no such subkey is blocked
  (`signers` in the check output).
- GPG failures are reported, never read as "key not found": unreadable or
  non-file input, a failed import or listing, and gpg failing to start are
  errors naming the stage and exit code, with the "no key data" exception
  ported as is. release-verify list-keys, which runs gpg for keys-sync,
  now also reports each key's capabilities and subkeys.
- Every test of main's test_check_key.py has a ported equivalent.

Adversarial-review findings:

- archive-sweep reads S3 URLs with promote's whole-host path-style rule,
  so a virtual-hosted bucket whose name starts with "s3" keeps its key.
- A release_dist_url_template with a literal around <version> (v<version>,
  foo-<version>-bin) gives the right release directory and archive URLs.
- keys-sync reads a GitHub KEYS URL as branch (the first segment after
  blob/) plus a path that may include subdirectories, so KEYS can live
  in e.g. dist/; a new optional keys_file_branch names a branch that
  contains '/' (release/1.x). Without it, a subdirectory reading is
  warned; a URL whose file is not KEYS is refused.
- The keyserver's UIDs are compared with KEYS as a set, so a reordered
  keyserver listing is no longer reported as a rolled key.

Generated-by: Claude Opus 5
potiuk added a commit that referenced this pull request Oct 16, 2026
…er fixes into the tool and read S3 and version paths right

Port of the key-checker fixes main landed on the script this series moves
into tools/release-management (#1634, #1635, #1646):

- A key passes only with a signing-capable primary or subkey that itself
  meets the strength floor and is not revoked, invalid, disabled or
  expired; a certify-only primary with no such subkey is blocked
  (`signers` in the check output).
- GPG failures are reported, never read as "key not found": unreadable or
  non-file input, a failed import or listing, and gpg failing to start are
  errors naming the stage and exit code, with the "no key data" exception
  ported as is. release-verify list-keys, which runs gpg for keys-sync,
  now also reports each key's capabilities and subkeys.
- Every test of main's test_check_key.py has a ported equivalent.

Adversarial-review findings:

- archive-sweep reads S3 URLs with promote's whole-host path-style rule,
  so a virtual-hosted bucket whose name starts with "s3" keeps its key.
- A release_dist_url_template with a literal around <version> (v<version>,
  foo-<version>-bin) gives the right release directory and archive URLs.
- keys-sync reads a GitHub KEYS URL as branch (the first segment after
  blob/) plus a path that may include subdirectories, so KEYS can live
  in e.g. dist/; a new optional keys_file_branch names a branch that
  contains '/' (release/1.x). Without it, a subdirectory reading is
  warned; a URL whose file is not KEYS is refused.
- The keyserver's UIDs are compared with KEYS as a set, so a reordered
  keyserver listing is no longer reported as a rolled key.

Generated-by: Claude Opus 5
potiuk added a commit that referenced this pull request Oct 16, 2026
…er fixes into the tool and read S3 and version paths right

Port of the key-checker fixes main landed on the script this series moves
into tools/release-management (#1634, #1635, #1646):

- A key passes only with a signing-capable primary or subkey that itself
  meets the strength floor and is not revoked, invalid, disabled or
  expired; a certify-only primary with no such subkey is blocked
  (`signers` in the check output).
- GPG failures are reported, never read as "key not found": unreadable or
  non-file input, a failed import or listing, and gpg failing to start are
  errors naming the stage and exit code, with the "no key data" exception
  ported as is. release-verify list-keys, which runs gpg for keys-sync,
  now also reports each key's capabilities and subkeys.
- Every test of main's test_check_key.py has a ported equivalent.

Adversarial-review findings:

- archive-sweep reads S3 URLs with promote's whole-host path-style rule,
  so a virtual-hosted bucket whose name starts with "s3" keeps its key.
- A release_dist_url_template with a literal around <version> (v<version>,
  foo-<version>-bin) gives the right release directory and archive URLs.
- keys-sync reads a GitHub KEYS URL as branch (the first segment after
  blob/) plus a path that may include subdirectories, so KEYS can live
  in e.g. dist/; a new optional keys_file_branch names a branch that
  contains '/' (release/1.x). Without it, a subdirectory reading is
  warned; a URL whose file is not KEYS is refused.
- The keyserver's UIDs are compared with KEYS as a set, so a reordered
  keyserver listing is no longer reported as a rolled key.

Generated-by: Claude Opus 5
potiuk added a commit that referenced this pull request Oct 16, 2026
…er fixes into the tool and read S3 and version paths right

Port of the key-checker fixes main landed on the script this series moves
into tools/release-management (#1634, #1635, #1646):

- A key passes only with a signing-capable primary or subkey that itself
  meets the strength floor and is not revoked, invalid, disabled or
  expired; a certify-only primary with no such subkey is blocked
  (`signers` in the check output).
- GPG failures are reported, never read as "key not found": unreadable or
  non-file input, a failed import or listing, and gpg failing to start are
  errors naming the stage and exit code, with the "no key data" exception
  ported as is. release-verify list-keys, which runs gpg for keys-sync,
  now also reports each key's capabilities and subkeys.
- Every test of main's test_check_key.py has a ported equivalent.

Adversarial-review findings:

- archive-sweep reads S3 URLs with promote's whole-host path-style rule,
  so a virtual-hosted bucket whose name starts with "s3" keeps its key.
- A release_dist_url_template with a literal around <version> (v<version>,
  foo-<version>-bin) gives the right release directory and archive URLs.
- keys-sync reads a GitHub KEYS URL as branch (the first segment after
  blob/) plus a path that may include subdirectories, so KEYS can live
  in e.g. dist/; a new optional keys_file_branch names a branch that
  contains '/' (release/1.x). Without it, a subdirectory reading is
  warned; a URL whose file is not KEYS is refused.
- The keyserver's UIDs are compared with KEYS as a set, so a reordered
  keyserver listing is no longer reported as a rolled key.

Generated-by: Claude Opus 5
potiuk added a commit that referenced this pull request Oct 16, 2026
…er fixes into the tool and read S3 and version paths right

Port of the key-checker fixes main landed on the script this series moves
into tools/release-management (#1634, #1635, #1646):

- A key passes only with a signing-capable primary or subkey that itself
  meets the strength floor and is not revoked, invalid, disabled or
  expired; a certify-only primary with no such subkey is blocked
  (`signers` in the check output).
- GPG failures are reported, never read as "key not found": unreadable or
  non-file input, a failed import or listing, and gpg failing to start are
  errors naming the stage and exit code, with the "no key data" exception
  ported as is. release-verify list-keys, which runs gpg for keys-sync,
  now also reports each key's capabilities and subkeys.
- Every test of main's test_check_key.py has a ported equivalent.

Adversarial-review findings:

- archive-sweep reads S3 URLs with promote's whole-host path-style rule,
  so a virtual-hosted bucket whose name starts with "s3" keeps its key.
- A release_dist_url_template with a literal around <version> (v<version>,
  foo-<version>-bin) gives the right release directory and archive URLs.
- keys-sync reads a GitHub KEYS URL as branch (the first segment after
  blob/) plus a path that may include subdirectories, so KEYS can live
  in e.g. dist/; a new optional keys_file_branch names a branch that
  contains '/' (release/1.x). Without it, a subdirectory reading is
  warned; a URL whose file is not KEYS is refused.
- The keyserver's UIDs are compared with KEYS as a set, so a reordered
  keyserver listing is no longer reported as a rolled key.

Generated-by: Claude Opus 5
potiuk added a commit that referenced this pull request Oct 16, 2026
…er fixes into the tool and read S3 and version paths right

Port of the key-checker fixes main landed on the script this series moves
into tools/release-management (#1634, #1635, #1646):

- A key passes only with a signing-capable primary or subkey that itself
  meets the strength floor and is not revoked, invalid, disabled or
  expired; a certify-only primary with no such subkey is blocked
  (`signers` in the check output).
- GPG failures are reported, never read as "key not found": unreadable or
  non-file input, a failed import or listing, and gpg failing to start are
  errors naming the stage and exit code, with the "no key data" exception
  ported as is. release-verify list-keys, which runs gpg for keys-sync,
  now also reports each key's capabilities and subkeys.
- Every test of main's test_check_key.py has a ported equivalent.

Adversarial-review findings:

- archive-sweep reads S3 URLs with promote's whole-host path-style rule,
  so a virtual-hosted bucket whose name starts with "s3" keeps its key.
- A release_dist_url_template with a literal around <version> (v<version>,
  foo-<version>-bin) gives the right release directory and archive URLs.
- keys-sync reads a GitHub KEYS URL as branch (the first segment after
  blob/) plus a path that may include subdirectories, so KEYS can live
  in e.g. dist/; a new optional keys_file_branch names a branch that
  contains '/' (release/1.x). Without it, a subdirectory reading is
  warned; a URL whose file is not KEYS is refused.
- The keyserver's UIDs are compared with KEYS as a set, so a reordered
  keyserver listing is no longer reported as a rolled key.

Generated-by: Claude Opus 5
potiuk added a commit that referenced this pull request Oct 16, 2026
… decide and render as code (#1625)

* perf(release-audit-report, release-archive-sweep, release-keys-sync): decide and render as code

The last three release skills each had one rule in a skill-local script
and the rest in prose. The scripts move unchanged into
tools/release-management (render_record → audit_report.record,
retention → archive_sweep, check_key → keys_sync), and new commands do
what the agent did by hand, printing JSON and running nothing:

- audit-report: `verdict` (planning issue found, `org/repo#n`
  normalised to the canonical URL), `gather` (PonyMail subjects and
  queries, the `svn info` revision command, the required fields; the
  search itself stays with the agent), `voters` (the [RESULT] voters
  mapped to roster handles with vote-tally's binding resolution,
  unresolved ones listed, never cited), `check` (Step 1 missing /
  redacted fields, schema violations, the thread-permalink shape),
  `record`, and `pr`.
- archive-sweep: `verdict`, `listing`, `trains` (strict `N.x` lines
  become patterns; a free-text retention rule leaves `keep` to the
  agent), `retention`, and `render` for svn mv, gh release delete (with
  the permanent-deletion warning), aws s3 mv, self-hosted, and atr
  (no command).
- keys-sync: whether the fingerprint is in KEYS and its UID matches the
  keyserver's is no longer a text scan. A new read-only
  `release-verify list-keys` lists fingerprints and UIDs in the
  existing throwaway GNUPGHOME (no agent, no keyring, no network,
  private-key material refused), and `keys-sync verdict` classifies it
  as proceed / noop / blocked, failing closed when it cannot compare.
  `render` draws the `# <uid>` KEYS block, the svn / git / generic
  sequence and the upload reminder with the expiry advisory.

Every rendered command quotes its values with the shared shell helpers.
Each backend and outcome gets its own file; audit-report's PR step
moves, byte-identical with its generated adversarial block, to
audit-log-pr.md at the skill root. Golden rules, Hard rules and the
eval headings are unchanged. audit-report 6,657 → 5,136 tokens,
archive-sweep 4,573 → 4,165, keys-sync 4,871 → 4,584.

Evals: the scripted cases become pytest cases named after each former
case (expected records byte-identical); the judgement and injection
cases stay, their fixtures now carrying the real tool output.

Generated-by: Claude Opus 5
Claude-Session: https://claude.ai/code/session_01GTPsZ5aEE5bUVYqp47Dpvv

* fix(release-archive-sweep, release-keys-sync, release-audit-report): refuse what the rules forbid instead of defaulting

Review findings on the three tools:

- archive-sweep: `retention` refuses a trains file whose retention rule
  was not decided (keep_decided false) unless every train names its keep,
  so a free-text rule no longer falls back to keep=1 and archives what
  the rule keeps.
- archive-sweep: `render` refuses a retention output without
  mapping_complete, and any latest-of-line, orphan or pre-release in
  past_retention (Golden rule 2).
- archive-sweep: a trailing -incubating qualifier is a release, not a
  pre-release, so podlings archive their older releases.
- keys-sync: the Step 0 verdict normalises the fingerprint as Step 1
  does (a 0x prefix no longer turns the no-op into a duplicate append).
- keys-sync: Step 2 requires --key-listing and appends only an armour
  holding exactly the key Step 1 checked.
- audit-report: `pr --base` defaults to <default-branch>, not main.

Generated-by: Claude Opus 5

* fix(release-keys-sync, release-archive-sweep): carry main's key-checker fixes into the tool and read S3 and version paths right

Port of the key-checker fixes main landed on the script this series moves
into tools/release-management (#1634, #1635, #1646):

- A key passes only with a signing-capable primary or subkey that itself
  meets the strength floor and is not revoked, invalid, disabled or
  expired; a certify-only primary with no such subkey is blocked
  (`signers` in the check output).
- GPG failures are reported, never read as "key not found": unreadable or
  non-file input, a failed import or listing, and gpg failing to start are
  errors naming the stage and exit code, with the "no key data" exception
  ported as is. release-verify list-keys, which runs gpg for keys-sync,
  now also reports each key's capabilities and subkeys.
- Every test of main's test_check_key.py has a ported equivalent.

Adversarial-review findings:

- archive-sweep reads S3 URLs with promote's whole-host path-style rule,
  so a virtual-hosted bucket whose name starts with "s3" keeps its key.
- A release_dist_url_template with a literal around <version> (v<version>,
  foo-<version>-bin) gives the right release directory and archive URLs.
- keys-sync reads a GitHub KEYS URL as branch (the first segment after
  blob/) plus a path that may include subdirectories, so KEYS can live
  in e.g. dist/; a new optional keys_file_branch names a branch that
  contains '/' (release/1.x). Without it, a subdirectory reading is
  warned; a URL whose file is not KEYS is refused.
- The keyserver's UIDs are compared with KEYS as a set, so a reordered
  keyserver listing is no longer reported as a rolled key.

Generated-by: Claude Opus 5
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

capability:resolve Close-out: invalidate, dedupe, CVE allocate, announcement family:release-management release-* skills

Projects

None yet

Development

Successfully merging this pull request may close these issues.

keys-sync reports GPG execution failures as missing keys

2 participants