Skip to content

Commit f0a5047

Browse files
cotticursoragent
andauthored
changelog: switch prepare-artifact bool CLI params to bool? (#3410)
* changelog: switch prepare-artifact bool CLI params to bool? The `changelog prepare-artifact` command exposed `isFork`, `canCommit`, and `maintainerCanModify` as plain `bool` parameters. The Argh CLI framework treats `bool` as a presence-only switch — `--flag` sets true, omission leaves false. There is no `--flag <value>` form. When `elastic/docs-actions` shelled `--can-commit "$CAN_COMMIT"` into this command (where `$CAN_COMMIT` was the string "true" or "false"), the parser took the bare `--can-commit` as "true" and silently dropped the literal "false" as a stray positional. Every fork PR ended up with `can_commit: true` in the artifact metadata, which then routed the apply step into the commit-and-push branch where it failed on a detached-HEAD push. This commit changes the three boolean parameters to `bool?` so: - Argh now generates `--[no-]flag` pairs for each one. `--can-commit` sets true, `--no-can-commit` sets false, omission leaves null. The `--help` output makes this explicit. - Callers that want to express "explicitly false" have a correct syntax to do so instead of the silently-broken `--flag false`. - The service coerces `null` → `false` when writing `ChangelogArtifactMetadata`, so an omitted flag never grants commit permission to the downstream apply step (fail closed). Behaviour summary at the CLI: Before After ------------------- ------------------------- --can-commit --can-commit → true (omitted) --no-can-commit → false --can-commit false (omitted) → false (null → false) The companion workflow fix in elastic/docs-actions#172 stops emitting `--flag <value>` patterns in the first place and only appends the affirmative flag when the upstream string equals "true". Tests: - New PrepareArtifact_NullableBoolsUnspecified_CoerceToFalseInMetadata asserts the fail-closed null → false coercion, which is the contract the apply step relies on. - Existing fork-fields tests still cover the explicit true/false paths (bool → bool? is an implicit conversion at the call sites). Verified with the AOT binary against a fixture staging dir: `docs-builder changelog prepare-artifact --is-fork --no-can-commit --maintainer-can-modify ...` writes `is_fork: true, can_commit: false, maintainer_can_modify: true` as expected. Co-authored-by: Cursor <cursoragent@cursor.com> * changelog: regenerate cli-schema.json after bool? switch The previous commit moved the bool→bool? rationale into a regular `//` comment block sitting between the XML `<param>` lines and the method declaration. That terminated the XML doc comment group for the schema source-generator, which then dropped every param summary for this command and broke the `Check CLI schema is up to date` CI step. - Inline the explanation into the existing `<remarks>` block as a `<para>`, restoring the unbroken XML doc group above the method. - Regenerate `docs/cli-schema.json` so it reflects the bool? CLI surface: the three flags now serialize with the updated summaries and `defaultValue: "default"` (nullable default) instead of `"false"`. No behaviour change beyond the previous commit; this just makes the schema check pass again. Co-authored-by: Cursor <cursoragent@cursor.com> --------- Co-authored-by: Cursor <cursoragent@cursor.com>
1 parent a7cf57b commit f0a5047

5 files changed

Lines changed: 68 additions & 19 deletions

File tree

docs/cli-schema.json

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -3394,7 +3394,7 @@
33943394
],
33953395
"name": "prepare-artifact",
33963396
"summary": "(CI) Package changelog artifact for cross-workflow transfer.",
3397-
"notes": "Resolves final status from evaluate-pr \u002B changelog add outcomes, copies generated YAML,\nwrites metadata.json, and sets GitHub Actions outputs. Always succeeds (exit 0) so the upload step runs.",
3397+
"notes": "Resolves final status from evaluate-pr \u002B changelog add outcomes, copies generated YAML,\nwrites metadata.json, and sets GitHub Actions outputs. Always succeeds (exit 0) so the upload step runs.\n\n\nThe isFork, canCommit and maintainerCanModify parameters are declared\nas bool? so the generated CLI emits both --flag and --no-flag pairs\n(Argh convention). A plain bool would expose presence-only switches: passing\n--can-commit \u0022false\u0022 would set canCommit = true (the flag is present) and\nsilently discard the literal \u0022false\u0022 as a stray positional. Callers that forward a\ndynamic value (--can-commit \u0022$VAR\u0022) would then misroute fork PRs into the\ncommit-and-push branch and die on a detached-HEAD push. See elastic/docs-actions#172\nfor the workflow-side fix.",
33983398
"usage": "docs-builder changelog prepare-artifact --staging-dir \u003Cstring\u003E --output-dir \u003Cstring\u003E --evaluate-status \u003Cstring\u003E --generate-outcome \u003Cstring\u003E --pr-number \u003Cint\u003E --head-ref \u003Cstring\u003E --head-sha \u003Cstring\u003E [options]",
33993399
"examples": [],
34003400
"parameters": [
@@ -3452,24 +3452,24 @@
34523452
"name": "is-fork",
34533453
"type": "boolean",
34543454
"required": false,
3455-
"summary": "Whether the PR is from a fork",
3456-
"defaultValue": "false"
3455+
"summary": "Whether the PR is from a fork (pass --is-fork / --no-is-fork; omit to leave null which is treated as false)",
3456+
"defaultValue": "default"
34573457
},
34583458
{
34593459
"role": "flag",
34603460
"name": "can-commit",
34613461
"type": "boolean",
34623462
"required": false,
3463-
"summary": "Whether the commit strategy allows committing",
3464-
"defaultValue": "false"
3463+
"summary": "Whether the commit strategy allows committing (pass --can-commit / --no-can-commit; omit to leave null which is treated as false)",
3464+
"defaultValue": "default"
34653465
},
34663466
{
34673467
"role": "flag",
34683468
"name": "maintainer-can-modify",
34693469
"type": "boolean",
34703470
"required": false,
3471-
"summary": "Whether the fork PR allows maintainer edits",
3472-
"defaultValue": "false"
3471+
"summary": "Whether the fork PR allows maintainer edits (pass --maintainer-can-modify / --no-maintainer-can-modify; omit to leave null which is treated as false)",
3472+
"defaultValue": "default"
34733473
},
34743474
{
34753475
"role": "flag",

src/services/Elastic.Changelog/Evaluation/ChangelogPrepareArtifactService.cs

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -79,16 +79,20 @@ public async Task<bool> PrepareArtifact(IDiagnosticsCollector collector, Prepare
7979
var changelogDir = config?.Bundle?.Directory ?? "docs/changelog";
8080

8181
var statusString = status.ToStringFast(true);
82+
// Null-coalesce the nullable bool inputs into the metadata's concrete
83+
// `bool` fields. Treating "unspecified" as `false` keeps downstream
84+
// consumers (apply step) failing closed: an unrecognised or omitted
85+
// CLI flag never grants commit permission.
8286
var metadata = new ChangelogArtifactMetadata
8387
{
8488
PrNumber = input.PrNumber,
8589
HeadRef = input.HeadRef,
8690
HeadSha = input.HeadSha,
8791
Status = statusString,
88-
IsFork = input.IsFork,
92+
IsFork = input.IsFork ?? false,
8993
HeadRepo = input.HeadRepo,
90-
CanCommit = input.CanCommit,
91-
MaintainerCanModify = input.MaintainerCanModify,
94+
CanCommit = input.CanCommit ?? false,
95+
MaintainerCanModify = input.MaintainerCanModify ?? false,
9296
LabelTable = input.LabelTable,
9397
ProductLabelTable = input.ProductLabelTable,
9498
SkipLabels = input.SkipLabels,

src/services/Elastic.Changelog/Evaluation/PrepareArtifactArguments.cs

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -14,10 +14,16 @@ public record PrepareArtifactArguments
1414
public required int PrNumber { get; init; }
1515
public required string HeadRef { get; init; }
1616
public required string HeadSha { get; init; }
17-
public bool IsFork { get; init; }
17+
// Nullable bool mirrors the CLI surface: null = "flag not specified",
18+
// normalized to false when serialized into ChangelogArtifactMetadata.
19+
// Using non-nullable bool here would force every call site to choose
20+
// between true/false and lose the "not specified" signal, which is what
21+
// allowed --can-commit "false" to silently set CanCommit = true at the
22+
// CLI before this change.
23+
public bool? IsFork { get; init; }
1824
public string? HeadRepo { get; init; }
19-
public bool CanCommit { get; init; }
20-
public bool MaintainerCanModify { get; init; }
25+
public bool? CanCommit { get; init; }
26+
public bool? MaintainerCanModify { get; init; }
2127
public string? LabelTable { get; init; }
2228
public string? ProductLabelTable { get; init; }
2329
public string? SkipLabels { get; init; }

src/tooling/docs-builder/Commands/ChangelogCommand.cs

Lines changed: 17 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1401,6 +1401,17 @@ async static (s, collector, state, ctx) => await s.EvaluatePr(collector, state,
14011401
/// <remarks>
14021402
/// Resolves final status from evaluate-pr + changelog add outcomes, copies generated YAML,
14031403
/// writes metadata.json, and sets GitHub Actions outputs. Always succeeds (exit 0) so the upload step runs.
1404+
///
1405+
/// <para>
1406+
/// The <c>isFork</c>, <c>canCommit</c> and <c>maintainerCanModify</c> parameters are declared
1407+
/// as <c>bool?</c> so the generated CLI emits both <c>--flag</c> and <c>--no-flag</c> pairs
1408+
/// (Argh convention). A plain <c>bool</c> would expose presence-only switches: passing
1409+
/// <c>--can-commit "false"</c> would set <c>canCommit = true</c> (the flag is present) and
1410+
/// silently discard the literal <c>"false"</c> as a stray positional. Callers that forward a
1411+
/// dynamic value (<c>--can-commit "$VAR"</c>) would then misroute fork PRs into the
1412+
/// commit-and-push branch and die on a detached-HEAD push. See elastic/docs-actions#172
1413+
/// for the workflow-side fix.
1414+
/// </para>
14041415
/// </remarks>
14051416
/// <param name="stagingDir">Directory where changelog add wrote the generated YAML</param>
14061417
/// <param name="outputDir">Directory to write the artifact (metadata.json + YAML)</param>
@@ -1409,9 +1420,9 @@ async static (s, collector, state, ctx) => await s.EvaluatePr(collector, state,
14091420
/// <param name="prNumber">Pull request number</param>
14101421
/// <param name="headRef">PR head branch ref</param>
14111422
/// <param name="headSha">PR head commit SHA</param>
1412-
/// <param name="isFork">Whether the PR is from a fork</param>
1413-
/// <param name="canCommit">Whether the commit strategy allows committing</param>
1414-
/// <param name="maintainerCanModify">Whether the fork PR allows maintainer edits</param>
1423+
/// <param name="isFork">Whether the PR is from a fork (pass --is-fork / --no-is-fork; omit to leave null which is treated as false)</param>
1424+
/// <param name="canCommit">Whether the commit strategy allows committing (pass --can-commit / --no-can-commit; omit to leave null which is treated as false)</param>
1425+
/// <param name="maintainerCanModify">Whether the fork PR allows maintainer edits (pass --maintainer-can-modify / --no-maintainer-can-modify; omit to leave null which is treated as false)</param>
14151426
/// <param name="headRepo">Fork repository full name (owner/repo)</param>
14161427
/// <param name="labelTable">Optional: markdown label table from evaluate-pr</param>
14171428
/// <param name="productLabelTable">Optional: markdown product label table from evaluate-pr</param>
@@ -1427,9 +1438,9 @@ public async Task<int> PrepareArtifact(
14271438
int prNumber,
14281439
string headRef,
14291440
string headSha,
1430-
bool isFork = false,
1431-
bool canCommit = false,
1432-
bool maintainerCanModify = false,
1441+
bool? isFork = null,
1442+
bool? canCommit = null,
1443+
bool? maintainerCanModify = null,
14331444
string? headRepo = null,
14341445
string? labelTable = null,
14351446
string? productLabelTable = null,

tests/Elastic.Changelog.Tests/Evaluation/ChangelogPrepareArtifactServiceTests.cs

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -149,6 +149,34 @@ public async Task PrepareArtifact_ForkNoMaintainerEdits_CanCommitFalse()
149149
metadata.MaintainerCanModify.Should().BeFalse();
150150
}
151151

152+
// Regression for the docs-actions / docs-builder fork-PR detached-HEAD failure
153+
// (see elastic/docs-actions#172). The bool? CLI parameters mean an *omitted*
154+
// --can-commit flag arrives at the service as null. The service must coerce
155+
// that to false in the persisted metadata so the downstream apply step does
156+
// NOT attempt a commit+push. Failing closed on "unspecified" is the whole
157+
// point of moving these flags to nullable bool.
158+
[Fact]
159+
public async Task PrepareArtifact_NullableBoolsUnspecified_CoerceToFalseInMetadata()
160+
{
161+
await SetupStagingYaml();
162+
await SetupConfig();
163+
var service = CreateService();
164+
var args = DefaultArgs() with
165+
{
166+
IsFork = null,
167+
CanCommit = null,
168+
MaintainerCanModify = null,
169+
HeadRepo = null
170+
};
171+
172+
await service.PrepareArtifact(Collector, args, CancellationToken.None);
173+
174+
var metadata = ReadMetadata();
175+
metadata.IsFork.Should().BeFalse("an omitted --is-fork flag must not be treated as 'fork'");
176+
metadata.CanCommit.Should().BeFalse("an omitted --can-commit flag must never grant commit permission to the apply step");
177+
metadata.MaintainerCanModify.Should().BeFalse("an omitted --maintainer-can-modify flag must not be treated as granted");
178+
}
179+
152180
[Fact]
153181
public async Task PrepareArtifact_ProductLabelTableAndSkipLabels_PersistedInMetadata()
154182
{

0 commit comments

Comments
 (0)