diff --git a/.github/workflows/merge-bot-pull-request.yml b/.github/workflows/merge-bot-pull-request.yml index 2c274a13..cb0ed8df 100644 --- a/.github/workflows/merge-bot-pull-request.yml +++ b/.github/workflows/merge-bot-pull-request.yml @@ -77,9 +77,9 @@ jobs: - name: Generate GitHub App token step id: app-token - uses: actions/create-github-app-token@d72941d797fd3113feb6b93fd0dec494b13a2547 # v1.12.0 + uses: actions/create-github-app-token@bcd2ba49218906704ab6c1aa796996da409d3eb1 # v3.2.0 with: - app-id: ${{ secrets.CODEGEN_APP_ID }} + client-id: ${{ secrets.CODEGEN_APP_CLIENT_ID }} private-key: ${{ secrets.CODEGEN_APP_PRIVATE_KEY }} - name: Get dependabot metadata step @@ -140,9 +140,9 @@ jobs: - name: Generate GitHub App token step id: app-token - uses: actions/create-github-app-token@d72941d797fd3113feb6b93fd0dec494b13a2547 # v1.12.0 + uses: actions/create-github-app-token@bcd2ba49218906704ab6c1aa796996da409d3eb1 # v3.2.0 with: - app-id: ${{ secrets.CODEGEN_APP_ID }} + client-id: ${{ secrets.CODEGEN_APP_CLIENT_ID }} private-key: ${{ secrets.CODEGEN_APP_PRIVATE_KEY }} - name: Merge pull request step @@ -193,9 +193,9 @@ jobs: # origin, not by event actor), and the restricted GITHUB_TOKEN # is read-only. Same App token pattern as the other merge jobs. id: app-token - uses: actions/create-github-app-token@d72941d797fd3113feb6b93fd0dec494b13a2547 # v1.12.0 + uses: actions/create-github-app-token@bcd2ba49218906704ab6c1aa796996da409d3eb1 # v3.2.0 with: - app-id: ${{ secrets.CODEGEN_APP_ID }} + client-id: ${{ secrets.CODEGEN_APP_CLIENT_ID }} private-key: ${{ secrets.CODEGEN_APP_PRIVATE_KEY }} - name: Disable auto-merge step diff --git a/.github/workflows/run-codegen-pull-request-task.yml b/.github/workflows/run-codegen-pull-request-task.yml index daf3d675..f7785d99 100644 --- a/.github/workflows/run-codegen-pull-request-task.yml +++ b/.github/workflows/run-codegen-pull-request-task.yml @@ -13,7 +13,7 @@ on: workflow_call: secrets: # GitHub App credentials to generate an installation token - CODEGEN_APP_ID: + CODEGEN_APP_CLIENT_ID: required: true CODEGEN_APP_PRIVATE_KEY: required: true @@ -48,9 +48,9 @@ jobs: # guard), which previously required a close/reopen dance under a PAT # to nudge the auto-merge workflow — that dance is gone. id: app-token - uses: actions/create-github-app-token@d72941d797fd3113feb6b93fd0dec494b13a2547 # v1.12.0 + uses: actions/create-github-app-token@bcd2ba49218906704ab6c1aa796996da409d3eb1 # v3.2.0 with: - app-id: ${{ secrets.CODEGEN_APP_ID }} + client-id: ${{ secrets.CODEGEN_APP_CLIENT_ID }} private-key: ${{ secrets.CODEGEN_APP_PRIVATE_KEY }} - name: Setup .NET SDK step @@ -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/.github/workflows/test-pull-request.yml b/.github/workflows/test-pull-request.yml index dd87da69..53e1d628 100644 --- a/.github/workflows/test-pull-request.yml +++ b/.github/workflows/test-pull-request.yml @@ -2,7 +2,7 @@ name: Test pull request action on: pull_request: - branches: [ main, develop, codegen ] + branches: [ main, develop ] workflow_dispatch: concurrency: diff --git a/AGENTS.md b/AGENTS.md index 4f001e4a..4f7ffd17 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -9,12 +9,10 @@ Treat this file as authoritative for everything else; don't restate its rules el ## Git and Commit Rules -**These rules are absolute — no exceptions:** - -- **Never make git commits.** AI coding agents cannot produce cryptographically signed commits. All commits must be signed (SSH/GPG) and must be made by the developer. Stage changes with `git add` and leave the commit to the developer. +- **Default to staging, not committing.** Stage changes with `git add` and leave `git commit` to the developer unless the developer has explicitly authorized the agent to commit for the current ask ("commit this", "open a PR", etc.). Authorization is scope-bound — it covers the commits needed for that specific task, not a blanket commit license for the rest of the session. +- **All commits must be cryptographically signed (SSH or GPG).** Branch protection enforces this on both branches; unsigned commits are rejected on push. Signing depends on environment configuration — `git config commit.gpgsign true`, a configured `user.signingkey`, and a working signing agent (loaded `ssh-agent` for SSH, or `gpg-agent` for GPG). If signing is not configured in the environment, **do not commit** — surface the missing config to the developer and stop at `git add`. Verify before any agent-authored commit (`git config --get commit.gpgsign && ssh-add -L` or the GPG equivalent). - **Never force push.** Do not run `git push --force` or `git push --force-with-lease` under any circumstances. Force pushing rewrites shared history and can cause data loss. - **Never run destructive git commands** (`git reset --hard`, `git checkout .`, `git restore .`, `git clean -f`) without explicit developer instruction. -- **Staging is the limit.** Prepare and stage file changes; the developer runs `git commit` in their own environment where signing keys are available. ## Branching Model @@ -29,6 +27,8 @@ 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 a4d051e2..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-04-30T03:07:13.4355622Z"; + 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; } diff --git a/README.md b/README.md index 5ea67df0..bac4755f 100644 --- a/README.md +++ b/README.md @@ -447,9 +447,9 @@ Licensed under the [MIT License][license-link]\ - Contents: Read & write — push commits to the `codegen` branch and merge bot PRs. - Pull requests: Read & write — open, update, and merge pull requests. - Metadata: Read-only (auto-required). - - Note the App ID from the app's settings page; generate a private key (downloads a `.pem` file). + - Note the **Client ID** from the app's settings page (labeled "Client ID" directly under the App name on the General tab — it looks like `Iv23li...`; **not** the numeric App ID shown above it). `actions/create-github-app-token` deprecated the numeric `app-id` input in v3.0.0 in favor of `client-id`. Also generate a private key (downloads a `.pem` file). - [Install the app](https://github.com/settings/apps) on your account and grant it access to the repository. The app must be both created **and** installed — creating it alone is not sufficient (`actions/create-github-app-token` fails with `Not Found` if the app isn't installed on the repository). - - Save the App ID as `CODEGEN_APP_ID` and the private key contents as `CODEGEN_APP_PRIVATE_KEY` in **both** of: + - Save the Client ID as `CODEGEN_APP_CLIENT_ID` and the private key contents as `CODEGEN_APP_PRIVATE_KEY` in **both** of: - GitHub project security Settings / Secrets / Actions — for the codegen workflow and the codegen merge job. - GitHub project security Settings / Secrets / Dependabot — **required** because Dependabot-triggered `pull_request` workflow runs use a separate, restricted secret context that doesn't see Actions secrets. Without the App secrets in the Dependabot store, the `merge-dependabot` job in `merge-bot-pull-request.yml` can't mint an App token and the PR will never auto-merge. - If the codegen workflows require additional secrets (e.g. third-party API keys), register them in the Actions store; if a Dependabot-triggered workflow ever needs them, register them in the Dependabot store too. @@ -465,7 +465,7 @@ Licensed under the [MIT License][license-link]\ **Codegen workflow schedule**: -- `run-periodic-codegen-pull-request.yml` runs every **Monday** at 02:00 UTC, plus on-demand via `workflow_dispatch`. It uses the App token (`CODEGEN_APP_ID` + `CODEGEN_APP_PRIVATE_KEY`) to commit, open the PR as `ptr727-codegen[bot]`, and let the merge-bot auto-merge once CI passes. No PAT, no close/reopen dance. +- `run-periodic-codegen-pull-request.yml` runs every **Monday** at 02:00 UTC, plus on-demand via `workflow_dispatch`. It uses the App token (`CODEGEN_APP_CLIENT_ID` + `CODEGEN_APP_PRIVATE_KEY`) to commit, open the PR as `ptr727-codegen[bot]`, and let the merge-bot auto-merge once CI passes. No PAT, no close/reopen dance. **GitHub project settings**: