Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 9 additions & 1 deletion .github/workflows/run-codegen-pull-request-task.yml
Original file line numberDiff line numberDiff line change
Expand Up@@ -65,11 +65,19 @@ jobs:
token: ${{ steps.app-token.outputs.token }}

- name: Run codegen step
# `--runtime` is template-internal hygiene: passing the workflow's
# `run_started_at` to both matrix legs (main and develop) makes them
# produce byte-identical CodeGen.cs, so develop->main release merges
# don't conflict on this demo file every release. Derived projects'
# real codegen should not copy this pattern — if your generator's
# per-run state is intentional, design it not to land on multiple
# release branches simultaneously, or absorb the merge cost.
run: |
set -euo pipefail
dotnet run --project ./CodeGen/CodeGen.csproj -- \
--codepath ./CodeGen \
--apikey "${{ secrets.NINJA_API_KEY }}"
--apikey "${{ secrets.NINJA_API_KEY }}" \
--runtime "${{ github.run_started_at }}"

- name: Format code step
run: |
Expand Down
1 change: 1 addition & 0 deletions AGENTS.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -27,6 +27,7 @@ Treat this file as authoritative for everything else; don't restate its rules el
- **Bots (Dependabot and codegen) target both `main` and `develop` in parallel.** [`.github/dependabot.yml`](./.github/dependabot.yml) duplicates every ecosystem entry (one per branch) and [`.github/workflows/run-codegen-pull-request-task.yml`](./.github/workflows/run-codegen-pull-request-task.yml) runs as a matrix over both branches with branch names `codegen-main` and `codegen-develop`. Each branch absorbs its own bot PRs independently, so neither falls behind, and the forward-only rule still holds (nothing is back-merged from main to develop — both branches receive their updates directly). The merge-bot ([`.github/workflows/merge-bot-pull-request.yml`](./.github/workflows/merge-bot-pull-request.yml)) dispatches `--squash` or `--merge` from each PR's base ref via a `case` statement so the form matches the ruleset on either base. Dependabot **security** PRs (CVE-driven) always open against the repo default branch (`main`) regardless of `target-branch` — the same `case` statement covers them.
- **Maintainer-pushed commits on a bot PR auto-disable auto-merge.** The merge-bot's `merge-dependabot` and `merge-codegen` jobs only fire on `opened` / `reopened` events (auto-merge is enabled exactly once per PR). When a maintainer pushes commits to a bot's branch (a `synchronize` event with an actor that isn't the same bot), the merge-bot's `disable-auto-merge-on-maintainer-push` job fires and calls `gh pr merge --disable-auto`. The maintainer's commits stay in the PR but won't auto-merge with the bot's content; re-enable auto-merge manually (`gh pr merge --auto <PR>` or the GitHub UI) when ready.
- **Why parallel dual-target rather than develop-only with eventual flow-through:** push-distribution channels (HACS for Home Assistant integrations, Linux distros that vendor from `main`, etc.) consume `main` directly. A develop-only model would leave `main` running stale code during long-running develop features. Codegen content can also be production-critical (live API-derived data, language lists, build catalogs) rather than just sample/demo content, so both branches need fresh codegen on their own cadence.
- **Dual-target codegen + per-run state = merge conflicts.** If a generator embeds per-invocation state (timestamps, GUIDs, build IDs) and runs independently on `main` and `develop`, the two branches' outputs diverge and every `develop → main` release conflicts on the generated file. This template's `CodeGen/CodeGen.cs` demo embeds a timestamp; the codegen workflow passes `--runtime "${{ github.run_started_at }}"` to both matrix legs so they produce byte-identical output. **That `--runtime` plumbing is template hygiene only — not a codegen pattern derived projects should reproduce.** Your real generators should either be deterministic given the same inputs (preferred), or not run on both release branches simultaneously, or absorb the per-release merge cost.
- **App-token workflows use Client ID, not App ID.** `actions/create-github-app-token` deprecated the numeric `app-id` input in v3.0.0; the template uses `client-id: ${{ secrets.CODEGEN_APP_CLIENT_ID }}`. When adding new App-token call sites, use the same form — do not reintroduce `app-id` / `CODEGEN_APP_ID`. See [README "Template - GitHub Setup"](./README.md#template---github-setup) for the secret-setup procedure.

## Pull Request Title and Commit Message Conventions
Expand Down
2 changes: 1 addition & 1 deletion CodeGen/CodeGen.cs
Original file line numberDiff line numberDiff line change
Expand Up@@ -8,7 +8,7 @@ internal static class CodeGen

internal static void Quote()
{
const string dateTime = "2026-05-18T03:01:36.2172197Z";
const string dateTime = "2026-05-18T03:01:38.0586119Z";
Console.WriteLine($"{dateTime} : {QuoteOfTheDay}");
Log.Logger.Information("Quote of the Day: {DateTime} : {Quote}", dateTime, QuoteOfTheDay);
}
Expand Down
15 changes: 13 additions & 2 deletions CodeGen/CodeGenBuilder.cs
Original file line numberDiff line numberDiff line change
Expand Up@@ -4,8 +4,19 @@ namespace ptr727.ProjectTemplate.CodeGen;

internal sealed class CodeGenBuilder(string outputPath, CancellationToken cancellationToken)
{
internal async Task CodeGenAsync(string quote)
// `runtime` is a template-internal hook: when the dual-target codegen
// matrix passes the same value to both main and develop legs, they
// produce byte-identical CodeGen.cs and develop->main merges don't
// conflict on this file. Empty -> use DateTime.UtcNow.ToString("o")
// (the original demo behavior, kept for local `dotnet run` use).
// This plumbing exists for template hygiene only — derived projects'
// real codegen should not copy this pattern.
internal async Task CodeGenAsync(string quote, string runtime)
{
string dateTime = string.IsNullOrEmpty(runtime)
? DateTime.UtcNow.ToString("o", CultureInfo.InvariantCulture)
: runtime;

// Codegen example
string codeGen = $$"""
namespace ptr727.ProjectTemplate.CodeGen;
Expand All@@ -17,7 +28,7 @@ internal static class CodeGen

internal static void Quote()
{
const string dateTime = "{{DateTime.UtcNow:o}}";
const string dateTime = {{ToCSharpStringLiteral(dateTime)}};
Console.WriteLine($"{dateTime} : {QuoteOfTheDay}");
Log.Logger.Information("Quote of the Day: {DateTime} : {Quote}", dateTime, QuoteOfTheDay);
}
Expand Down
25 changes: 24 additions & 1 deletion CodeGen/CommandLine.cs
Original file line numberDiff line numberDiff line change
Expand Up@@ -7,6 +7,7 @@ internal sealed class CommandLine
{
private readonly Option<DirectoryInfo> _codePathOption = CreateCodePathOption();
private readonly Option<string> _apiKeyOption = CreateApiKeyOption();
private readonly Option<string> _runtimeOption = CreateRuntimeOption();

private static readonly FrozenSet<string> s_cliBypassList = FrozenSet.Create(
StringComparer.OrdinalIgnoreCase,
Expand All@@ -25,7 +26,12 @@ internal CommandLine(string[] args)

internal RootCommand CreateRootCommand()
{
RootCommand rootCommand = new("C# .NET codegen project") { _codePathOption, _apiKeyOption };
RootCommand rootCommand = new("C# .NET codegen project")
{
_codePathOption,
_apiKeyOption,
_runtimeOption,
};
rootCommand.SetAction(
(parseResult, cancellationToken) =>
{
Expand All@@ -42,6 +48,7 @@ internal Options CreateOptions(ParseResult parseResult) =>
{
CodePath = parseResult.GetValue(_codePathOption)!,
ApiKey = parseResult.GetValue(_apiKeyOption) ?? string.Empty,
Runtime = parseResult.GetValue(_runtimeOption) ?? string.Empty,
};

private static Option<DirectoryInfo> CreateCodePathOption()
Expand All@@ -57,6 +64,21 @@ private static Option<DirectoryInfo> CreateCodePathOption()
private static Option<string> CreateApiKeyOption() =>
new("--apikey", "-a") { Description = "The API key to use (optional).", Required = false };

// Template-internal: deterministic timestamp injection so the dual-target
// codegen matrix produces byte-identical CodeGen.cs on main and develop
// (eliminates merge conflicts on every develop->main release). Derived
// projects: do NOT replicate this plumbing for production codegen — if
// your generator's per-run timestamp is intentional, accept the conflicts
// or redesign the generator. See README "Template - GitHub Setup".
private static Option<string> CreateRuntimeOption() =>
new("--runtime", "-r")
{
Description =
"Override the timestamp embedded in generated content "
+ "(ISO 8601; defaults to DateTime.UtcNow).",
Required = false,
};

internal static bool BypassStartup(ParseResult parseResult) =>
parseResult.Errors.Count > 0
|| parseResult.CommandResult.Children.Any(symbolResult =>
Expand All@@ -68,5 +90,6 @@ internal sealed class Options
{
internal required DirectoryInfo CodePath { get; init; }
internal required string ApiKey { get; init; }
internal required string Runtime { get; init; }
}
}
4 changes: 3 additions & 1 deletion CodeGen/Program.cs
Original file line numberDiff line numberDiff line change
Expand Up@@ -63,7 +63,9 @@ internal async Task<int> ExecuteAsync()
string outputPath = Path.Combine(commandLineOptions.CodePath.FullName, "CodeGen.cs");
Log.Information("Writing quote to {OutputPath}", outputPath);
CodeGenBuilder codegenBuilder = new(outputPath, cancellationToken);
await codegenBuilder.CodeGenAsync(quoteoftheday).ConfigureAwait(false);
await codegenBuilder
.CodeGenAsync(quoteoftheday, commandLineOptions.Runtime)
.ConfigureAwait(false);

return 0;
}
Expand Down
Loading