Skip to content

fix(generator): preserve repeatable GitHub CLI attachments - #4855

Merged
thomhurst merged 5 commits into
mainfrom
issue-4796-gh-attachments
Sep 13, 2026
Merged

thomhurst merged 5 commits into
mainfrom
issue-4796-gh-attachments

Conversation

@thomhurst

@thomhurst thomhurst commented Sep 13, 2026

Copy link
Copy Markdown
Owner

GitHub CLI accepts multiple attachments, but its scalar file help hint made the generator emit a single string. Model --attach as a repeatable GitHub CLI option and regenerate all six issue/PR create, edit, and comment options with IEnumerable<string>? Attach. Each value produces its own --attach=... argument, preserving spaces, commas, and # alt text.

The rule belongs in the existing GitHub adapter: the flag row does not express cardinality, and inferring repeatability from arbitrary shell examples would risk treating quoted body text or unrelated command examples as flags. Six captured gh 2.100.0 help fixtures also verify that --body-file remains scalar. Serialization tests exercise the actual generated classes for all six commands.

Regeneration uses the current gh 2.100.0 binary and includes its new gh stack command and gh issue develop --worktree option. Command coverage increases from 221 to 222 commands with no removals. API baselines, CLI documentation, and generator provenance are regenerated; generated options were not edited manually.

The shared service template now emits ConfigureAwait(false), and all 222 GitHub wrappers are regenerated from that template. Four root/nested and required/optional generation regressions cover this rule.

Validation:

  • Reproduced all six attachment cardinality failures before the fix; all 26 ZeroOutputScraperTests pass afterward.
  • All 37 GitHub integration unit tests pass, including six attachment serialization cases.
  • Release builds of the OptionsGenerator and GitHub solutions pass.
  • Formatted the affected solutions for the edited source/test files.
  • API baseline synchronization records 6,737 current entries.
  • Node 24.14.1: yarn install and yarn build pass.
  • git diff --check passes. Every local .NET command uses Invoke-AgentDotNet.ps1 with the default 10-minute/2-GB limits.

Latest follow-up: four generator regressions and all 37 GitHub integration tests pass; Release builds and scoped formatting pass. The full generator test attempt hit the default 2 GB guard (2,198 MB), so broader validation is deferred to CI. An unrelated HTTP test setup race in the previous Ubuntu run is tracked as #4864.

Closes #4796

@coderabbitai

coderabbitai Bot commented Sep 13, 2026

Copy link
Copy Markdown

Warning

Review limit reached

Next included review available in 1 minute.

Check out review usage here.

View limit details

Limit details: You’ve used all 4 included reviews currently available.

You've used all free OSS reviews for now. Wait for the free limit to reset to keep reviewing this public repository.

Learn how review limits work.

Review configuration:

⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Advanced

Run ID: 4dc992b5-448d-4128-9acc-6b5f77e5636b

📥 Commits

Reviewing files that changed from the base of the PR and between 34a5536 and 43c097f.

⛔ Files ignored due to path filters (43)
  • src/ModularPipelines.GitHub/Generated/Gh.CommandCoverage.json is excluded by !**/generated/**
  • src/ModularPipelines.GitHub/Generated/Gh.Generation.json is excluded by !**/generated/**
  • src/ModularPipelines.GitHub/Options/GhIssueCommentOptions.Generated.cs is excluded by !**/*.generated.*
  • src/ModularPipelines.GitHub/Options/GhIssueCreateOptions.Generated.cs is excluded by !**/*.generated.*
  • src/ModularPipelines.GitHub/Options/GhIssueDevelopOptions.Generated.cs is excluded by !**/*.generated.*
  • src/ModularPipelines.GitHub/Options/GhIssueEditOptions.Generated.cs is excluded by !**/*.generated.*
  • src/ModularPipelines.GitHub/Options/GhPrCommentOptions.Generated.cs is excluded by !**/*.generated.*
  • src/ModularPipelines.GitHub/Options/GhPrCreateOptions.Generated.cs is excluded by !**/*.generated.*
  • src/ModularPipelines.GitHub/Options/GhPrEditOptions.Generated.cs is excluded by !**/*.generated.*
  • src/ModularPipelines.GitHub/Options/GhStackOptions.Generated.cs is excluded by !**/*.generated.*
  • src/ModularPipelines.GitHub/Services/Gh.Generated.cs is excluded by !**/*.generated.*
  • src/ModularPipelines.GitHub/Services/GhAgentTask.Generated.cs is excluded by !**/*.generated.*
  • src/ModularPipelines.GitHub/Services/GhAttestation.Generated.cs is excluded by !**/*.generated.*
  • src/ModularPipelines.GitHub/Services/GhAuth.Generated.cs is excluded by !**/*.generated.*
  • src/ModularPipelines.GitHub/Services/GhCache.Generated.cs is excluded by !**/*.generated.*
  • src/ModularPipelines.GitHub/Services/GhCodespace.Generated.cs is excluded by !**/*.generated.*
  • src/ModularPipelines.GitHub/Services/GhCodespacePorts.Generated.cs is excluded by !**/*.generated.*
  • src/ModularPipelines.GitHub/Services/GhConfig.Generated.cs is excluded by !**/*.generated.*
  • src/ModularPipelines.GitHub/Services/GhDiscussion.Generated.cs is excluded by !**/*.generated.*
  • src/ModularPipelines.GitHub/Services/GhExtension.Generated.cs is excluded by !**/*.generated.*
  • src/ModularPipelines.GitHub/Services/GhGist.Generated.cs is excluded by !**/*.generated.*
  • src/ModularPipelines.GitHub/Services/GhGpgKey.Generated.cs is excluded by !**/*.generated.*
  • src/ModularPipelines.GitHub/Services/GhIssue.Generated.cs is excluded by !**/*.generated.*
  • src/ModularPipelines.GitHub/Services/GhLabel.Generated.cs is excluded by !**/*.generated.*
  • src/ModularPipelines.GitHub/Services/GhOrg.Generated.cs is excluded by !**/*.generated.*
  • src/ModularPipelines.GitHub/Services/GhPr.Generated.cs is excluded by !**/*.generated.*
  • src/ModularPipelines.GitHub/Services/GhPreview.Generated.cs is excluded by !**/*.generated.*
  • src/ModularPipelines.GitHub/Services/GhProject.Generated.cs is excluded by !**/*.generated.*
  • src/ModularPipelines.GitHub/Services/GhRelease.Generated.cs is excluded by !**/*.generated.*
  • src/ModularPipelines.GitHub/Services/GhRepo.Generated.cs is excluded by !**/*.generated.*
  • src/ModularPipelines.GitHub/Services/GhRepoAutolink.Generated.cs is excluded by !**/*.generated.*
  • src/ModularPipelines.GitHub/Services/GhRepoDeployKey.Generated.cs is excluded by !**/*.generated.*
  • src/ModularPipelines.GitHub/Services/GhRepoGitignore.Generated.cs is excluded by !**/*.generated.*
  • src/ModularPipelines.GitHub/Services/GhRepoLicense.Generated.cs is excluded by !**/*.generated.*
  • src/ModularPipelines.GitHub/Services/GhRuleset.Generated.cs is excluded by !**/*.generated.*
  • src/ModularPipelines.GitHub/Services/GhRun.Generated.cs is excluded by !**/*.generated.*
  • src/ModularPipelines.GitHub/Services/GhSearch.Generated.cs is excluded by !**/*.generated.*
  • src/ModularPipelines.GitHub/Services/GhSecret.Generated.cs is excluded by !**/*.generated.*
  • src/ModularPipelines.GitHub/Services/GhSkill.Generated.cs is excluded by !**/*.generated.*
  • src/ModularPipelines.GitHub/Services/GhSshKey.Generated.cs is excluded by !**/*.generated.*
  • src/ModularPipelines.GitHub/Services/GhVariable.Generated.cs is excluded by !**/*.generated.*
  • src/ModularPipelines.GitHub/Services/GhWorkflow.Generated.cs is excluded by !**/*.generated.*
  • src/ModularPipelines.GitHub/Services/IGh.Generated.cs is excluded by !**/*.generated.*
📒 Files selected for processing (13)
  • docs/docs/mp-packages/cli/gh.md
  • src/ModularPipelines.GitHub/PublicAPI.Unshipped.txt
  • test/ModularPipelines.GitHub.UnitTests/GhAttachmentOptionsTests.cs
  • tools/ModularPipelines.OptionsGenerator/src/ModularPipelines.OptionsGenerator.Tests/Fixtures/Gh/issue-comment.txt
  • tools/ModularPipelines.OptionsGenerator/src/ModularPipelines.OptionsGenerator.Tests/Fixtures/Gh/issue-create.txt
  • tools/ModularPipelines.OptionsGenerator/src/ModularPipelines.OptionsGenerator.Tests/Fixtures/Gh/issue-edit.txt
  • tools/ModularPipelines.OptionsGenerator/src/ModularPipelines.OptionsGenerator.Tests/Fixtures/Gh/pr-comment.txt
  • tools/ModularPipelines.OptionsGenerator/src/ModularPipelines.OptionsGenerator.Tests/Fixtures/Gh/pr-create.txt
  • tools/ModularPipelines.OptionsGenerator/src/ModularPipelines.OptionsGenerator.Tests/Fixtures/Gh/pr-edit.txt
  • tools/ModularPipelines.OptionsGenerator/src/ModularPipelines.OptionsGenerator.Tests/Generators/ServiceAwaitGenerationTests.cs
  • tools/ModularPipelines.OptionsGenerator/src/ModularPipelines.OptionsGenerator.Tests/Scrapers/ZeroOutputScraperTests.cs
  • tools/ModularPipelines.OptionsGenerator/src/ModularPipelines.OptionsGenerator/Generators/GeneratorUtils.cs
  • tools/ModularPipelines.OptionsGenerator/src/ModularPipelines.OptionsGenerator/Scrapers/Cli/GhCliScraper.cs

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Sep 13, 2026

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review Completed 2026-09-13T16:35:25.560562Z 43c097f New commits
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@greptile-apps

greptile-apps Bot commented Sep 13, 2026

Copy link
Copy Markdown

Greptile Summary

This PR updates GitHub CLI generation so all six issue and pull-request create, edit, and comment commands represent --attach as a repeatable option while preserving scalar handling for unrelated file options. It also refreshes the generated API for GitHub CLI 2.100.0 and updates generated service awaits to comply with the repository's asynchronous-library convention.

  • Emits one --attach=... argument per attachment without splitting spaces, commas, or alt text.
  • Scopes the repeatability override to the six supported issue/PR commands.
  • Adds serialization and scraper regression coverage.
  • Regenerates command coverage, public API records, documentation, and the new gh stack facade.
  • Emits ConfigureAwait(false) from the generator for top-level and nested service methods.

Confidence Score: 5/5

The PR appears safe to merge, with both previous findings resolved and no new actionable defects identified.

The attachment override is now limited to the intended issue and pull-request commands, generated serialization preserves each value as a separate argument, and the generator now emits ConfigureAwait(false) consistently across the refreshed GitHub service surface. The previous attachment-scoping thread was resolved after its targeted fix, and the previous ConfigureAwait requirement was manually resolved after the generator and generated services were updated.

Important Files Changed

Filename Overview
tools/ModularPipelines.OptionsGenerator/src/ModularPipelines.OptionsGenerator/Scrapers/Cli/GhCliScraper.cs Scopes repeatable attachment inference to the six GitHub issue and pull-request commands while retaining existing inference elsewhere.
tools/ModularPipelines.OptionsGenerator/src/ModularPipelines.OptionsGenerator/Generators/GeneratorUtils.cs Updates generated service awaits to use ConfigureAwait(false) for both required and optional option parameters.
tools/ModularPipelines.OptionsGenerator/src/ModularPipelines.OptionsGenerator.Tests/Generators/ServiceAwaitGenerationTests.cs Covers top-level and nested service generation with required and optional options.
tools/ModularPipelines.OptionsGenerator/src/ModularPipelines.OptionsGenerator.Tests/Scrapers/ZeroOutputScraperTests.cs Verifies attachment cardinality, scalar body-file behavior, and isolation from unrelated commands.
test/ModularPipelines.GitHub.UnitTests/GhAttachmentOptionsTests.cs Exercises the actual generated option classes and confirms each attachment is serialized separately.
src/ModularPipelines.GitHub/Services/Gh.Generated.cs Regenerates the root GitHub service with the new stack command and context-safe awaits.

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart LR
    H[GitHub CLI help fixtures] --> S[GhCliScraper]
    S --> C{Command is issue/pr create, edit, or comment?}
    C -->|Yes, --attach| R[Repeatable IEnumerable<string> option]
    C -->|No| N[Normal scalar inference]
    R --> G[Generated GitHub options]
    N --> G
    G --> B[Command-line builder]
    B --> A[One --attach=value argument per attachment]
Loading

Reviews (3): Last reviewed commit: "chore(github): regenerate wrappers with ..." | Re-trigger Greptile

@github-actions

Copy link
Copy Markdown
Contributor

Code review

Thanks for the regeneration work here — the attachment fixture/test coverage across the six issue/PR commands is thorough, and the generator/scraper changes for --attach look otherwise well targeted.

Blocking issue

RepeatableOptions collection initializer will not compiletools/ModularPipelines.OptionsGenerator/src/ModularPipelines.OptionsGenerator/Scrapers/Cli/GhCliScraper.cs#L52-L60:

private static readonly HashSet<string> RepeatableOptions =
[
    with(StringComparer.OrdinalIgnoreCase),
    "--field",
    "--raw-field",
    "--header",
    // gh documents multiple attachments in command prose/examples, but prints a scalar file hint.
    "--attach",
];

with(StringComparer.OrdinalIgnoreCase) is not valid C#. Collection expressions ([...]) don't support passing a constructor argument like an IEqualityComparer<T> this way — with is only a contextual keyword for non-destructive record mutation (instance with { ... }), not a way to invoke a constructor overload inside a collection literal. This won't parse, so GhCliScraper.cs — and therefore the whole ModularPipelines.OptionsGenerator project — will fail to build.

This also silently drops the case-insensitive comparer the old new(StringComparer.OrdinalIgnoreCase) { ... } initializer used for RepeatableOptions.Contains(switchName) (GhCliScraper.cs#L87), so even a corrected version needs to preserve that behavior explicitly, e.g.:

private static readonly HashSet<string> RepeatableOptions = new(StringComparer.OrdinalIgnoreCase)
{
    "--field",
    "--raw-field",
    "--header",
    // gh documents multiple attachments in command prose/examples, but prints a scalar file hint.
    "--attach",
};

The PR description states "Release builds of the OptionsGenerator and GitHub solutions pass," so it's worth double-checking whether this was introduced after that validation step, or in a local formatting/cleanup pass that wasn't rebuilt afterward.

@thomhurst

Copy link
Copy Markdown
Owner Author

The compiler concern in review comment 5654051678 does not apply to this repository. I built the unchanged 7e534dc generator solution using the pinned SDK through Invoke-AgentDotNet.ps1: zero warnings/errors. Directory.Build.props selects LangVersion=preview. Collection expression arguments explicitly support this comparer syntax: https://learn.microsoft.com/en-us/dotnet/csharp/language-reference/proposals/collection-expression-arguments. The comparer remains intact.

Separately, db2cc76 scopes --attach to the six established commands and adds two failing-before/passing-after regressions. All 1,355 generator tests and 37 GitHub tests pass. GitHub Release build passes with 19 existing style/nullability warnings; scoped formatting passes. Regeneration with gh 2.100.0 preserves all 222 commands and the generated API, with updated provenance. Waiting for the new review/CI cycle before resolving the remaining thread.

Comment thread src/ModularPipelines.GitHub/Services/Gh.Generated.cs Outdated
@github-actions

Copy link
Copy Markdown
Contributor

Code review — PR #4855 (db2cc768)

Reviewed the --attach cardinality fix, the gh 2.100.0 regeneration, and the generator/scraper changes.

Summary

The fix is well-scoped and matches the repo's generator-first constraints:

  • GhCliScraper.IsRepeatableOption now overrides cardinality only for --attach on the six affected commands (issue/pr x create/edit/comment) via a commandParts pattern match, rather than a blanket rule on the switch name. That correctly avoids misclassifying an unrelated --attach flag on other commands (verified by the added Gh_Attachment_Override_Does_Not_Apply_To_Unrelated_Commands test).
  • The generated IEnumerable<string>? Attach properties, PublicAPI.Unshipped.txt entries, and the new GhAttachmentOptionsTests (which asserts each attachment renders as its own --attach=... argument) all line up with the scraper change.
  • New gh stack command and gh issue develop --worktree option are plain regeneration output, consistent with existing patterns (no hand-editing of generated files, per repo convention).
  • Fixture-based scraper tests (issue-create.txt, pr-edit.txt, etc.) capture real gh 2.100.0 help text and pin both the repeatable --attach behavior and the scalar --body-file behavior side by side, which is good regression coverage for the actual bug (cardinality misdetection), not just the happy path.

Addressing prior feedback

A previous automated review flagged RepeatableOptions = [with(StringComparer.OrdinalIgnoreCase), "--field", ...] in GhCliScraper.cs as a compile error. That collection-expression-with-comparer syntax is valid under the Collection Expression Arguments preview feature (https://learn.microsoft.com/en-us/dotnet/csharp/language-reference/proposals/collection-expression-arguments, LangVersion=preview per Directory.Build.props), and the same pattern is already used elsewhere on main (e.g. ModuleConfigurationBuilder.cs), so it compiles today. That thread was correctly resolved by the author's rebuttal — no action needed here.

The current HEAD also no longer touches the static RepeatableOptions set for --attach at all; it uses the narrower commandParts-scoped override instead, which is the more defensible design given six specific commands need it rather than a global switch-name rule.

Non-blocking observation (out of scope for this PR)

Greptile noted the new StackAsync method in Gh.Generated.cs omits ConfigureAwait(false), which CLAUDE.md requires for library code under src/. Checking the surrounding file: none of the other five hand-generated methods in Gh.Generated.cs (LicensesAsync, StatusAsync, etc.) use ConfigureAwait(false) either, on main or in this PR — it's an existing gap in the code-generation template, not something this PR introduced or regressed. Per CLAUDE.md, generated output shouldn't be hand-edited; if this is worth fixing, it belongs in the generator template as a follow-up, not a manual patch to this file in this PR.

Verdict

No actionable findings against this PR's own changes. Nice job scoping the fix narrowly and backing it with both scraper-level and serialization-level tests.

@thomhurst

Copy link
Copy Markdown
Owner Author

The ConfigureAwait(false) finding is fixed in a0c84e6 and 43c097f using the shared generator change already developed in #4713, followed by gh 2.100.0 regeneration. All 222 wrappers now suppress context capture, with no API or command-coverage change. Four generator regressions and all 37 GitHub integration tests pass. Release builds pass (the GitHub dependency chain retains 19 existing warnings), and scoped formatting passes.

The full generator test attempt reached the default 2 GB process-tree limit (2,198 MB) and was not retried; the four focused tests ran successfully from the completed build. Broader testing remains with CI.

The previous Ubuntu failure was unrelated: SendAsync_CustomClientKeepsTimeoutOutsideLoggedReplayContent hit its 100 ms timeout during HttpContentPreviewReader.ReadAsync, before acquiring the stream. Tracked separately as #4864 with the failing run and deterministic-test acceptance criteria. No rerun-for-green was requested.

@github-actions

Copy link
Copy Markdown
Contributor

Code review

Reviewed the current head (43c097fbaad766870ab16a0b0e0dc131019934d2) against the prior review threads on this PR.

Prior findings — verified resolved:

  • The RepeatableOptions = [with(StringComparer.OrdinalIgnoreCase), ...] compile-error concern does not hold: this collection-expression-arguments syntax already exists on main (src/ModularPipelines/ModuleConfigurationBuilder.cs, src/ModularPipelines/Engine/ModuleTypeIdentifier.cs), and LangVersion is preview repo-wide (Directory.Build.props). Confirmed by direct inspection, not just the author's rebuttal.
  • --attach cardinality is scoped correctly: GhCliScraper.IsRepeatableOption (tools/ModularPipelines.OptionsGenerator/src/ModularPipelines.OptionsGenerator/Scrapers/Cli/GhCliScraper.cs:79-89) only overrides for commandParts is ["issue" or "pr", "create" or "edit" or "comment"], matching the existing IsBooleanValueOption pattern used elsewhere in the same file. Gh_Attachment_Override_Does_Not_Apply_To_Unrelated_Commands backs this with repo create/issue view cases.
  • The previously-flagged missing ConfigureAwait(false) on StackAsync (and the rest of the GitHub surface) is fixed: grepping every regenerated src/ModularPipelines.GitHub/Services/*.cs file shows zero ExecuteCommandLineToolAsync(...) calls without .ConfigureAwait(false). The generator template change in GeneratorUtils.cs (both the options-parameter and no-options-parameter branches) is covered by the new ServiceAwaitGenerationTests (root + nested, required + optional options).

Other checks:

  • GhAttachmentOptionsTests.Renders_Each_Attachment_As_A_Separate_Option exercises the actual generated Attach properties on all six option records and confirms spaces/commas/# alt text survive as separate --attach= arguments.
  • No generated files were hand-edited outside of what regeneration would produce (options, services, PublicAPI.Unshipped.txt, docs, Gh.Generation.json/Gh.CommandCoverage.json all move together consistently with the gh 2.100.0 bump, 221 → 222 commands, no removals).
  • The scraper/test class refactors to primary constructors (TestAwsCliScraper, TestYarnCliScraper, TestGhCliScraper) and collection-expression cleanups are mechanical and don't change behavior.

No actionable findings against the current head.

@thomhurst
thomhurst deployed to Pull Requests September 13, 2026 17:03 — with GitHub Actions Active
@thomhurst
thomhurst merged commit f2eaf0d into main Sep 13, 2026
18 checks passed
@thomhurst
thomhurst deleted the issue-4796-gh-attachments branch September 13, 2026 19:46
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Generator: preserve repeatable GitHub CLI --attach options

1 participant