Skip to content

Make the generated action references configurable#535

Open
lahma wants to merge 2 commits into
Fallout-build:mainfrom
lahma:configurable-actions
Open

Make the generated action references configurable#535
lahma wants to merge 2 commits into
Fallout-build:mainfrom
lahma:configurable-actions

Conversation

@lahma

@lahma lahma commented Jul 23, 2026

Copy link
Copy Markdown
Contributor

The generator hard-coded the four actions it emits, so a consumer who wants a newer major — or a SHA pin — has to subclass GitHubActionsAttribute and reimplement step emission. #533 bumped three of them; this makes all four overridable so lagging a release behind never blocks anyone.

Closes #534.

Attribute properties

CheckoutAction, CacheAction, SetupDotNetAction, UploadArtifactAction.

A value containing an @ is a complete reference, emitted as-is; anything else is a bare ref appended to the default action:

CheckoutAction = "v8",                                             // actions/checkout@v8
CacheAction = "0c907a75c2c80ebcb7f088228285e798b750cf8f # v4.2.4", // SHA pin, version as comment
UploadArtifactAction = "my-org/upload-artifact@v7"                 // fork or drop-in replacement

The trailing # comment is split off before the value is classified, so a slash or an @ inside it changes nothing — that's what makes the SHA-pinning idiom work. A ref that itself contains a / (a branch like releases/v1) reads exactly like an owner/repo, so it takes a leading @"@releases/v1" — and the ambiguous bare form is rejected rather than guessed at. Null or whitespace restores the default.

The reference token must carry no whitespace: emitted into an unquoted uses: scalar, a stray : would otherwise corrupt the whole workflow file. All four are resolved up-front in GetConfiguration, so an override is validated even when its step isn't emitted (caching off, artifacts disabled) — errors name the property and the workflow.

Resolution lives in the step setters, so subclasses and hand-built steps get the same shorthand and validation. GitHubActionsCustomStep.Uses has no default to resolve against, so it now must name its action in full instead of silently emitting an unusable uses: v8.

GitHubActionsDefaults is internal — the properties take a plain string, so nothing a consumer writes needs the type.

actions/cache v4 → v6 (first commit)

Cache was never in #533's scope and sat two majors behind. Separate commit; the diff touches only actions/cache@v4@v6 lines.

  • v5.0.0 — node24 runtime, requires a self-hosted runner ≥ 2.327.1
  • v6.0.0 — dependency update + ESM migration

The path:/key: inputs are unchanged. Consumers on older self-hosted runners set CacheAction = "v4" — the feature demonstrating itself.

Verification

  • dotnet build fallout.slnx clean; dotnet test tests/Fallout.Common.Specs — 168 passed, 0 failed
  • The pre-existing Verify snapshots are untouched by the override commit — that's the assertion that the default path is byte-identical
  • New snapshot action-overrides covers all four properties in one workflow, one per accepted form
  • GitHubActionsActionReferenceSpecs covers the resolver: fallback, bare ref, @-prefixed slash-bearing ref, SHA + comment, comment containing a URL, complete reference, fork, local/container refs, the ambiguous form, comment-only, and YAML-corrupting values — plus validation of a skipped step's override
  • Confirmed the transition-shim generator no longer mirrors GitHubActionsDefaults into Nuke.* as an empty class (it forwards static methods, never consts)
  • Dogfooded: set CheckoutAction = "v8" and a CacheAction override on this repo's build workflow, regenerated, confirmed both lines changed, reverted — regeneration with no overrides leaves .github/workflows untouched

Not in scope

ITaluone's original "heads-up when a new action release ships" — worth a follow-up issue; this removes the urgency but not the need.

🤖 Generated with Claude Code

https://claude.ai/code/session_01Vn8JQZRwdoovPuctD21Wjx

@lahma
lahma marked this pull request as ready for review July 23, 2026 16:29
@lahma
lahma requested a review from a team as a code owner July 23, 2026 16:29

@ITaluone ITaluone left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

In general: LGTM

Just this one question, tho

@ITaluone
ITaluone requested a review from avidenic July 24, 2026 07:56
@ChrisonSimtian

Copy link
Copy Markdown
Collaborator

v5+ runs on node24 and requires self-hosted runners ≥ 2.327.1
might be worth dropping this in the release notes somewhere. But then, its really up to people to pick their own version

@ChrisonSimtian ChrisonSimtian left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

I love it, thanks for throwing this in "last minute", I think this is a good addition for the upcoming 10.4 release

{
GitHubActionsActionReference.Resolve(defaultAction, value);
}
catch (ArgumentException exception)

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

🤔 When is this exception thrown? GitHubActionsActionReference.Resolve doesn't seem to throw it.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Indirectly — Assert.True throws ArgumentException (src/Fallout.Utilities/Assert.cs:34), so Resolve did throw it, just never visibly. Fair to call that out: catching a type a helper only throws transitively is fragile, and if Assert ever changed its exception the property name would silently vanish from the message.

Removed the try/catch. Resolve now takes an origin describing what carries the value, so the message is built correctly the first time instead of being caught and re-wrapped:

GitHubActionsActionReference.Resolve(defaultAction, value, $"'{property}' in workflow '{name}'");

'CacheAction' in workflow 'build': action reference 'actions/cache' is ambiguous — write 'owner/repo@ref' …

attribute.GetConfiguration(relevantTargets);
}

private static string Resolve(string value)

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

🔧 (requires change) Keep this inline as using this private method obscures the test.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Done — inlined. Each test now passes the default explicitly, which is the part the helper was hiding (the assertions are all about resolving against actions/checkout):

var reference = GitHubActionsActionReference.Resolve(GitHubActionsDefaults.CheckoutAction, value, Origin);

reference.Should().Be("actions/checkout@v8");

act.Should().NotThrow();
}

private static void Generate(Action<TestGitHubActionsAttribute> configure)

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

🔧 This method should either get a much better name, replaced with a Test Data Builder or just copy-pasted in each test. Right now, it makes the tests hard to udnerstand.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Renamed to GenerateWorkflow, with the parameter as configureAttribute and the lambda arg spelled out, so the call sites now say what they do:

var act = () => GenerateWorkflow(attribute => attribute.CheckoutAction = "releases/v1");

Added a summary explaining why it exists — generation is what triggers validation, which is the whole point of these particular specs. Kept it as a method rather than a builder: it takes one argument and every spec uses it the same way, so a builder would be ceremony without a second axis to vary.

@dennisdoomen dennisdoomen left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

🔧 You now have 4 commits, of which four are related and caused by rework and one is completely unrelated. Either squash the thre so we can keep the final two for a clean source control history, or split the PR into two please.

/// <summary>
/// Resolves a user-supplied action override against the version the generator pins by default.
/// </summary>
internal static class GitHubActionsActionReference

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

⛏️Please reformat the file so it aligns with our new standards

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Found it: the three files I created had LF endings, while .editorconfig sets end_of_line = crlf. Files I edited in place kept CRLF, so only the new ones were off — which is why the whole file reads as non-conforming. Converted all three.

dotnet format --verify-no-changes over CI/GitHubActions/ now reports zero ENDOFLINE errors (was 123).

Deliberately did not apply the rest of what dotnet format wants: it also flags 78 WHITESPACE errors in this folder, but untouched folders (CI/AzurePipelines, CI/AppVeyor) have 85 of the same, and they're all the aligned object-initializer style the repo uses everywhere (resharper_wrap_object_and_collection_initializer_style = chop_always). Reformatting to dotnet format defaults would make these files inconsistent with the rest of the repo, so I matched the surrounding code instead. Say the word if the new standard is in fact dotnet format and you'd like it applied repo-wide in a separate PR.

return comment == null ? reference : $"{reference} {comment}";
}

private static string ResolveActionReference(string defaultAction, string reference)

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

🔧 I like to see some docs on all private methods explaining their purpose.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Added <summary> docs to all three privates — ResolveMarketplaceReference (also renamed from ResolveActionReference, which said nothing the type name didn't), GetActionName, and SplitComment. Each says what it's for rather than restating the code; the inline // notes that explain why a rule exists are kept alongside.


public class GitHubActionsArtifactStep : GitHubActionsStep
{
private string uses = GitHubActionsDefaults.UploadArtifactAction;

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

♻️ You can use the new field keyword. My IDE directly told me about that. Same for the other new steps.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Done, on all four steps — the explicit backing fields are gone:

public string Uses
{
    get;
    set => field = GitHubActionsActionReference.Resolve(
        GitHubActionsDefaults.UploadArtifactAction, value, $"{nameof(GitHubActionsArtifactStep)}.{nameof(Uses)}");
} = GitHubActionsDefaults.UploadArtifactAction;

LangVersion is preview so it compiles clean, and the property initializer replaces the field initializer that carried the default.

lahma and others added 2 commits July 25, 2026 14:32
The generator's cache pin sat at v4 while Fallout-build#533 moved checkout, setup-dotnet,
and upload-artifact to their current majors — cache was never in that PR's
scope. v6.1.0 is current; v4 is two majors behind.

- v5.0.0 moved to the node24 runtime and requires a self-hosted runner
  >= 2.327.1
- v6.0.0 updated dependencies and migrated to ESM

The `path:` and `key:` inputs the generator emits are unchanged across both,
so the emitted step needs no other edits.

Covers the generated workflows, the hand-written publish-packages-* ones, the
14 Verify snapshots with a cache step, and the docs sample.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Vn8JQZRwdoovPuctD21Wjx
The generator hard-coded the four actions it emits, so a consumer who wanted a
newer major — or a SHA pin — had to subclass GitHubActionsAttribute and
reimplement step emission. Two repos migrating off NUKE carried custom step
classes for exactly that (Fallout-build#533).

Each emitting step now carries a normalizing property, forwarded from four new
attribute properties:

    CheckoutAction, CacheAction, SetupDotNetAction, UploadArtifactAction

A trailing '# comment' is split off before the value is classified, so a '/' or
'@' inside it never sways the decision — that is what makes the SHA-pinning
idiom work. What remains is a complete reference when it contains an '@',
emitted verbatim; otherwise a bare ref appended to the pinned action, so "v8"
becomes actions/checkout@v8. A ref that itself contains a '/' reads exactly
like an owner/repo, so it takes a leading '@' — "@releases/v1" — and the
ambiguous bare form is rejected rather than guessed at. Null or whitespace
restores the default.

The reference token must carry no whitespace: it is emitted into an unquoted
YAML scalar, where a stray ': ' would corrupt the whole workflow file.

All four are resolved up-front in GetConfiguration. The cache and artifact
steps are conditional, so their overrides would otherwise go unvalidated — and
silently ignored — whenever caching or publishing is off, surfacing the typo
only when the step was re-enabled. Rejections name the property and workflow
that carry the value, so the message points at the declaration to fix.

Resolution lives in the step setters, so subclasses and hand-built steps get
the same shorthand and validation. GitHubActionsCustomStep.Uses has no default
to resolve against and is emitted verbatim, so it must now name its action in
full — otherwise the shorthand documented for the built-in steps silently
produced an unusable 'uses: v8', and a multi-line value could inject arbitrary
keys into the generated workflow.

GitHubActionsDefaults is internal: the properties take a plain string, so
nothing a consumer writes needs the type. That also keeps it out of the Nuke.*
transition shims, which mirror public types but cannot carry const members and
would have exposed an empty class.

Closes Fallout-build#534

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Vn8JQZRwdoovPuctD21Wjx
@lahma
lahma force-pushed the configurable-actions branch from 6b8f93f to a1959d1 Compare July 25, 2026 11:33
@lahma

lahma commented Jul 25, 2026

Copy link
Copy Markdown
Contributor Author

All six comments addressed, and the history is restructured. Rebased onto latest main.

Two commits now, as asked — the three rework commits are squashed into one, leaving the unrelated bump on its own:

  • Bump the generated cache step to actions/cache@v6 — touches only actions/cache@v4@v6 lines
  • Make the generated action references configurable — the feature

Happy to split these into two PRs instead if you'd rather; they're independent, the bump just happened to surface while writing the feature.

Changes since review

  • Exception flow — dropped the try/catch-and-rewrap. Resolve now takes an origin so the message is built right the first time, instead of relying on Assert.True throwing ArgumentException transitively.
  • Line endings — the three new files were LF against .editorconfig's crlf. Converted; dotnet format now reports zero ENDOFLINE errors here. See the thread for why I left the WHITESPACE ones alone.
  • field keyword — all four steps, backing fields gone.
  • Private-method docs — added to all three, plus ResolveActionReferenceResolveMarketplaceReference.
  • TestsResolve helper inlined; GenerateGenerateWorkflow with a documented purpose.

dotnet build fallout.slnx clean, 169 specs pass, and regenerating this repo's own workflows leaves them byte-identical.

One note for whoever merges: the PR still has no labels — I don't have permission to add them, so it needs target/vCurrent + a changelog category.

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.

Should we setup some sort of "heads-up tests"? Meaning: something that notifies us, when a new release is out..

4 participants