From 83ccb6add5f6e3576a5d7a44cd0511332711f8be Mon Sep 17 00:00:00 2001 From: Pieter Viljoen Date: Sun, 24 May 2026 13:20:11 -0700 Subject: [PATCH] Pass shared run timestamp into CodeGen to prevent dual-target merge conflicts MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The codegen workflow runs as a matrix on main and develop. Each leg embedded `DateTime.UtcNow:o` directly into `CodeGen/CodeGen.cs`, so even within a single workflow invocation the two legs produced different file contents (timestamps ~2 seconds apart). Every develop->main release after a codegen sync then conflicted textually on that one line, and the conflict blocked the merge ref from computing, which in turn prevented `pull_request` workflows from firing on the develop->main PR. Fix: add a `--runtime` CLI flag to the CodeGen project that overrides the embedded timestamp; the workflow passes `${{ github.run_started_at }}` to both matrix legs so they emit byte-identical output. When `--runtime` is omitted (local `dotnet run`), behavior is unchanged (DateTime.UtcNow). Also resolves the existing CodeGen.cs divergence on develop by pinning to main's already-released timestamp, so the next develop->main release won't conflict on this file. Note on scope: per-run-unique demo output is *expected* for codegen in general — the `--runtime` plumbing is template-internal hygiene to keep the dual-target template from generating release-blocking conflicts on every cycle. Derived projects' real generators should not copy this pattern; design generators to be deterministic given the same inputs, run them on a single release branch, or absorb the per-release merge cost. --- .../run-codegen-pull-request-task.yml | 10 +++++++- AGENTS.md | 1 + CodeGen/CodeGen.cs | 2 +- CodeGen/CodeGenBuilder.cs | 15 +++++++++-- CodeGen/CommandLine.cs | 25 ++++++++++++++++++- CodeGen/Program.cs | 4 ++- 6 files changed, 51 insertions(+), 6 deletions(-) diff --git a/.github/workflows/run-codegen-pull-request-task.yml b/.github/workflows/run-codegen-pull-request-task.yml index 9c139445..f7785d99 100644 --- a/.github/workflows/run-codegen-pull-request-task.yml +++ b/.github/workflows/run-codegen-pull-request-task.yml @@ -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: | diff --git a/AGENTS.md b/AGENTS.md index b753db3b..4f7ffd17 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -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 ` 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 diff --git a/CodeGen/CodeGen.cs b/CodeGen/CodeGen.cs index d811c5f9..22a57664 100644 --- a/CodeGen/CodeGen.cs +++ b/CodeGen/CodeGen.cs @@ -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); } diff --git a/CodeGen/CodeGenBuilder.cs b/CodeGen/CodeGenBuilder.cs index e0be3675..a4e19d70 100644 --- a/CodeGen/CodeGenBuilder.cs +++ b/CodeGen/CodeGenBuilder.cs @@ -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; @@ -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); } diff --git a/CodeGen/CommandLine.cs b/CodeGen/CommandLine.cs index 3e0bc6ed..daa32d4b 100644 --- a/CodeGen/CommandLine.cs +++ b/CodeGen/CommandLine.cs @@ -7,6 +7,7 @@ internal sealed class CommandLine { private readonly Option _codePathOption = CreateCodePathOption(); private readonly Option _apiKeyOption = CreateApiKeyOption(); + private readonly Option _runtimeOption = CreateRuntimeOption(); private static readonly FrozenSet s_cliBypassList = FrozenSet.Create( StringComparer.OrdinalIgnoreCase, @@ -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) => { @@ -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 CreateCodePathOption() @@ -57,6 +64,21 @@ private static Option CreateCodePathOption() private static Option 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 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 => @@ -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; } } } diff --git a/CodeGen/Program.cs b/CodeGen/Program.cs index 15f68ec2..9ea4efe3 100644 --- a/CodeGen/Program.cs +++ b/CodeGen/Program.cs @@ -63,7 +63,9 @@ internal async Task 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; }