From 7015e2f890fcdfb3cf350c170cedce10dc98d85a Mon Sep 17 00:00:00 2001 From: Pieter Viljoen Date: Mon, 13 Apr 2026 13:03:53 -0700 Subject: [PATCH 1/3] Enhance GitHub workflows for codegen and auto-merge processes - Refactor conditions in merge-bot workflows to improve security and prevent abuse. - Introduce new workflow for running codegen app and creating pull requests. - Add periodic workflow for running codegen app weekly. - Update README with detailed instructions on using GitHub App for codegen. - Modify date in CodeGen class for accurate quote generation. Signed-off-by: Pieter Viljoen --- .github/workflows/merge-bot-pull-request.yml | 38 ++++++++++- .../run-codegen-app-pull-request-task.yml | 65 +++++++++++++++++++ .../run-codegen-pull-request-task.yml | 3 + .../run-periodic-codegen-app-pull-request.yml | 21 ++++++ .../run-periodic-codegen-pull-request.yml | 2 +- CodeGen/CodeGen.cs | 2 +- CodeGen/HttpClientFactory.cs | 17 +++-- README.md | 32 ++++++++- 8 files changed, 167 insertions(+), 13 deletions(-) create mode 100644 .github/workflows/run-codegen-app-pull-request-task.yml create mode 100644 .github/workflows/run-periodic-codegen-app-pull-request.yml diff --git a/.github/workflows/merge-bot-pull-request.yml b/.github/workflows/merge-bot-pull-request.yml index 39c1a658..fc836bc5 100644 --- a/.github/workflows/merge-bot-pull-request.yml +++ b/.github/workflows/merge-bot-pull-request.yml @@ -13,7 +13,10 @@ jobs: merge-dependabot: name: Merge dependabot pull request job runs-on: ubuntu-latest - if: github.actor == 'dependabot[bot]' && github.event.pull_request.head.repo.full_name == github.repository + # Must come from dependabot, and the PR must be from the same repository to prevent abuse + if: >- + github.actor == 'dependabot[bot]' && + github.event.pull_request.head.repo.full_name == github.repository permissions: contents: write pull-requests: write @@ -36,7 +39,38 @@ jobs: merge-codegen: name: Merge codegen pull request job runs-on: ubuntu-latest - if: github.event.pull_request.user.login == 'github-actions[bot]' && github.event.pull_request.head.ref == 'codegen' && github.event.pull_request.base.ref == 'main' && github.event.pull_request.head.repo.full_name == github.repository && ((github.event.action == 'reopened' && github.actor == 'ptr727') || (github.event.action != 'reopened' && github.actor == 'github-actions[bot]')) + # Must come from the codegen workflow, and the PR must be from the same repository to prevent abuse + if: >- + github.event.pull_request.user.login == 'github-actions[bot]' && + github.event.pull_request.head.ref == 'codegen' && + github.event.pull_request.base.ref == 'main' && + github.event.pull_request.head.repo.full_name == github.repository && + ( + (github.event.action == 'reopened' && github.actor == github.repository_owner) || + (github.event.action != 'reopened' && github.actor == 'github-actions[bot]') + ) + permissions: + contents: write + pull-requests: write + + steps: + + - name: Merge pull request step + run: gh pr merge --auto --squash "$PR_URL" + env: + PR_URL: ${{github.event.pull_request.html_url}} + GH_TOKEN: ${{secrets.GITHUB_TOKEN}} + + merge-codegen-app: + name: Merge codegen app pull request job + runs-on: ubuntu-latest + # Must come from the codegen app workflow, and the PR must be from the same repository to prevent abuse + if: >- + github.actor == 'ptr727-codegen[bot]' && + github.event.pull_request.user.login == 'ptr727-codegen[bot]' && + github.event.pull_request.head.ref == 'codegen' && + github.event.pull_request.base.ref == 'main' && + github.event.pull_request.head.repo.full_name == github.repository permissions: contents: write pull-requests: write diff --git a/.github/workflows/run-codegen-app-pull-request-task.yml b/.github/workflows/run-codegen-app-pull-request-task.yml new file mode 100644 index 00000000..060d8a0f --- /dev/null +++ b/.github/workflows/run-codegen-app-pull-request-task.yml @@ -0,0 +1,65 @@ +name: Run codegen app and pull request task + +on: + workflow_call: + secrets: + # GitHub App credentials to generate an installation token + CODEGEN_APP_ID: + required: true + CODEGEN_APP_PRIVATE_KEY: + required: true + +jobs: + + codegen: + name: Run codegen app and pull request job + runs-on: ubuntu-latest + permissions: + contents: write + pull-requests: write + + steps: + + - name: Generate GitHub App token step + uses: actions/create-github-app-token@v1 + id: app-token + with: + app-id: ${{ secrets.CODEGEN_APP_ID }} + private-key: ${{ secrets.CODEGEN_APP_PRIVATE_KEY }} + + - name: Setup .NET SDK step + uses: actions/setup-dotnet@v5 + with: + dotnet-version: 10.x + + - name: Checkout code step + uses: actions/checkout@v6 + with: + ref: main + token: ${{ steps.app-token.outputs.token }} + + - name: Run codegen step + run: | + dotnet run --project ./CodeGen/CodeGen.csproj -- \ + --codepath ./CodeGen + + - name: Format code step + run: | + dotnet tool restore + dotnet husky install + dotnet csharpier format --log-level=debug . + git status + + - name: Create pull request step + uses: peter-evans/create-pull-request@v8 + id: cpr + with: + # Use app token: triggers pull_request workflow events directly, creates verified commits as the app + token: ${{ steps.app-token.outputs.token }} + base: main + branch: codegen + title: 'Update codegen files' + body: 'This PR updates the codegen files.' + commit-message: 'Update codegen files' + delete-branch: true + sign-commits: true diff --git a/.github/workflows/run-codegen-pull-request-task.yml b/.github/workflows/run-codegen-pull-request-task.yml index fdcd8dfc..730d274b 100644 --- a/.github/workflows/run-codegen-pull-request-task.yml +++ b/.github/workflows/run-codegen-pull-request-task.yml @@ -3,6 +3,7 @@ name: Run codegen and pull request task on: workflow_call: secrets: + # Use PAT to trigger workflows WORKFLOW_PAT: required: true @@ -43,6 +44,7 @@ jobs: uses: peter-evans/create-pull-request@v8 id: cpr with: + # Use GITHUB_TOKEN to sign the commit, but will not trigger workflows token: ${{ secrets.GITHUB_TOKEN }} base: main branch: codegen @@ -59,4 +61,5 @@ jobs: gh pr close "$PR" gh pr reopen "$PR" env: + # Use PAT to trigger workflows GH_TOKEN: ${{ secrets.WORKFLOW_PAT }} diff --git a/.github/workflows/run-periodic-codegen-app-pull-request.yml b/.github/workflows/run-periodic-codegen-app-pull-request.yml new file mode 100644 index 00000000..5ecfcfcc --- /dev/null +++ b/.github/workflows/run-periodic-codegen-app-pull-request.yml @@ -0,0 +1,21 @@ +name: Run weekly CodeGen App and Pull Request action + +on: + workflow_dispatch: + schedule: + # Run weekly on Thursdays at 02:00 UTC (PAT workflow runs on Mondays) + - cron: '0 2 * * THU' + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +jobs: + + run-codegen-app: + name: Run codegen app and pull request job + uses: ./.github/workflows/run-codegen-app-pull-request-task.yml + secrets: inherit + permissions: + contents: write + pull-requests: write diff --git a/.github/workflows/run-periodic-codegen-pull-request.yml b/.github/workflows/run-periodic-codegen-pull-request.yml index cb16fe8c..af1193ff 100644 --- a/.github/workflows/run-periodic-codegen-pull-request.yml +++ b/.github/workflows/run-periodic-codegen-pull-request.yml @@ -3,7 +3,7 @@ name: Run weekly CodeGen and Pull Request action on: workflow_dispatch: schedule: - # Run weekly on Mondays at 02:00 UTC + # Run weekly on Mondays at 02:00 UTC (PAT workflow; app workflow runs on Thursdays) - cron: '0 2 * * MON' concurrency: diff --git a/CodeGen/CodeGen.cs b/CodeGen/CodeGen.cs index 7c6f39df..a283a0e3 100644 --- a/CodeGen/CodeGen.cs +++ b/CodeGen/CodeGen.cs @@ -7,7 +7,7 @@ internal static class CodeGen internal static void Quote() { - const string dateTime = "2026-03-30T03:00:04.9630307Z"; + const string dateTime = "2026-01-30T22:28:14.6290903Z"; Console.WriteLine($"{dateTime} : {QuoteOfTheDay}"); Log.Logger.Information("Quote of the Day: {DateTime} : {Quote}", dateTime, QuoteOfTheDay); } diff --git a/CodeGen/HttpClientFactory.cs b/CodeGen/HttpClientFactory.cs index 121098f8..c3b37f64 100644 --- a/CodeGen/HttpClientFactory.cs +++ b/CodeGen/HttpClientFactory.cs @@ -27,14 +27,10 @@ internal static class HttpClientFactory private static readonly TimeSpan s_httpClientTimeout = TimeSpan.FromSeconds(120); private static readonly Lazy s_httpClient = new(CreateHttpClient); - private static readonly Lazy s_resilienceHandler = new( - CreateResilienceHandler - ); + // Returns the shared singleton HttpClient; all callers share the connection pool and circuit breaker state. internal static HttpClient GetHttpClient() => s_httpClient.Value; - private static ResilienceHandler GetResilienceHandler() => s_resilienceHandler.Value; - private static ResilienceHandler CreateResilienceHandler() => new( new ResiliencePipelineBuilder() @@ -106,9 +102,16 @@ outcome.Exception is not null ? outcome.Exception is not (OperationCanceledException or BrokenCircuitException) : outcome.Result is not null && (int)outcome.Result.StatusCode is 408 or 429 or >= 500; - private static HttpClient CreateHttpClient() + // Creates a new HttpClient instance; each caller gets an independent resilience handler + // and circuit breaker state. Callers should store and reuse the returned instance. + [System.Diagnostics.CodeAnalysis.SuppressMessage( + "Reliability", + "CA2000:Dispose objects before losing scope", + Justification = "HttpClient takes ownership of the handler and disposes it when the client is disposed." + )] + internal static HttpClient CreateHttpClient() { - HttpClient httpClient = new(GetResilienceHandler()) { Timeout = s_httpClientTimeout }; + HttpClient httpClient = new(CreateResilienceHandler()) { Timeout = s_httpClientTimeout }; httpClient.DefaultRequestHeaders.UserAgent.Add( new ProductInfoHeaderValue(AssemblyInfo.AppName, AssemblyInfo.ReleaseVersion) ); diff --git a/README.md b/README.md index da04ae17..bda991e2 100644 --- a/README.md +++ b/README.md @@ -388,16 +388,44 @@ Licensed under the [MIT License][license-link]\ - Pull requests: Read & write — to close and reopen the PR, triggering `pull_request` workflow events under the PAT owner's identity - Workflows: Read & write — required for the PAT to trigger `pull_request` events in other workflows - Metadata: Read-only (auto-required) - - The codegen workflow uses `GITHUB_TOKEN` to create a signed commit and open the PR as `github-actions[bot]`. It then uses `WORKFLOW_PAT` to close and reopen the PR so the `pull_request` event fires under the PAT owner's identity (`ptr727`), which triggers the auto-merge workflow. PRs created or updated by `GITHUB_TOKEN` alone do not trigger other workflows, hence the close/reopen step. + - The codegen workflow uses `GITHUB_TOKEN` to create a signed commit and open the PR as `github-actions[bot]`. It then uses `WORKFLOW_PAT` to close and reopen the PR so the `pull_request` event fires under the repository owner's identity (`github.repository_owner`), which triggers the auto-merge workflow. PRs created or updated by `GITHUB_TOKEN` alone do not trigger other workflows, hence the close/reopen step. - The auto-merge condition in `merge-bot-pull-request.yml` requires all of the following to be true: - `github.event.pull_request.user.login == 'github-actions[bot]'` — the PR was created by the Actions bot (via `GITHUB_TOKEN`) - `github.event.pull_request.head.ref == 'codegen'` — the source branch is `codegen` - `github.event.pull_request.base.ref == 'main'` — the PR targets `main` - `github.event.pull_request.head.repo.full_name == github.repository` — the PR is from the same repository (not a fork) - - For `reopened` events: `github.actor == 'ptr727'` — the reopen was triggered by the PAT owner + - For `reopened` events: `github.actor == github.repository_owner` — the reopen was triggered by the repository owner account - For all other events: `github.actor == 'github-actions[bot]'` — triggered by normal workflow activity - Save the PAT as `WORKFLOW_PAT` in: - GitHub project security Settings / Secrets / Actions. +- Create a [GitHub App](https://github.com/settings/apps) as an alternative to the PAT workflow. + - App name: `ptr727-codegen` + - The app bot user will be `ptr727-codegen[bot]`. + - Permissions required: + - Repository permissions: + - Contents: Read & write — to push commits to the `codegen` branch + - Pull requests: Read & write — to open and update pull requests + - Metadata: Read-only (auto-required) + - Install the app on the repository. + - Note the App ID from the app's settings page. + - Generate a private key (downloads a `.pem` file). + - Save the App ID as `CODEGEN_APP_ID` and the private key contents as `CODEGEN_APP_PRIVATE_KEY` in: + - GitHub project security Settings / Secrets / Actions. + - Unlike the PAT workflow, the GitHub App token triggers `pull_request` workflow events directly when opening a PR. No close/reopen step is required. + - The auto-merge condition in `merge-bot-pull-request.yml` for the app workflow requires all of the following to be true: + - `github.actor == 'ptr727-codegen[bot]'` — the event was triggered by the app + - `github.event.pull_request.user.login == 'ptr727-codegen[bot]'` — the PR was created by the app + - `github.event.pull_request.head.ref == 'codegen'` — the source branch is `codegen` + - `github.event.pull_request.base.ref == 'main'` — the PR targets `main` + - `github.event.pull_request.head.repo.full_name == github.repository` — the PR is from the same repository (not a fork) + +**Codegen workflow schedule**: + +- The PAT-based codegen workflow (`run-periodic-codegen-pull-request.yml`) runs every **Monday** at 02:00 UTC. + - Uses `WORKFLOW_PAT` to close and reopen the PR after creation so that the `reopened` `pull_request` event is triggered by the repository owner account, which is required by the auto-merge condition (`github.actor == github.repository_owner`). Therefore, `WORKFLOW_PAT` must belong to the repository owner account. +- The App-based codegen workflow (`run-periodic-codegen-app-pull-request.yml`) runs every **Thursday** at 02:00 UTC. + - Uses `CODEGEN_APP_ID` and `CODEGEN_APP_PRIVATE_KEY` to generate a GitHub App installation token. The app token triggers `pull_request` events directly when the PR is opened — no close/reopen step needed. +- The two workflows alternate through the week as independent verification that both authentication paths continue to work. **GitHub project settings**: From c46d21735e547ae39ae00c6fbfa2522f05c90efe Mon Sep 17 00:00:00 2001 From: Pieter Viljoen Date: Mon, 13 Apr 2026 13:11:02 -0700 Subject: [PATCH 2/3] Add Git and commit rules to documentation for AI coding agents Signed-off-by: Pieter Viljoen --- .github/copilot-instructions.md | 9 +++++++++ AGENTS.md | 9 +++++++++ 2 files changed, 18 insertions(+) diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md index 8f4c0def..ce63fad2 100644 --- a/.github/copilot-instructions.md +++ b/.github/copilot-instructions.md @@ -309,6 +309,15 @@ The project includes comprehensive `.editorconfig` settings that enforce: **Always respect the .editorconfig settings** - these are verified by the build process. +## Git and Commit Rules + +**These rules are absolute — no exceptions:** + +- **Never make git commits.** All commits must be cryptographically signed (SSH/GPG). AI coding agents cannot produce signed commits. Stage changes with `git add` and leave `git commit` to the developer, who must run it in their own environment where signing keys are available. +- **Never force push.** Do not run `git push --force` or `git push --force-with-lease`. Force pushing rewrites shared branch history and is blocked by branch protection rules. +- **Never run destructive git commands** (`git reset --hard`, `git checkout .`, `git restore .`, `git clean -f`) without explicit developer instruction. +- **Staging is the limit.** Prepare changes and stage files; the developer handles all commits and pushes. + ## Workflow 1. **Before coding**: Run `dotnet tool restore` to ensure tools are installed diff --git a/AGENTS.md b/AGENTS.md index 7b31ff94..b06d7ba4 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -4,6 +4,15 @@ For comprehensive coding standards and detailed conventions, refer to [`.github/copilot-instructions.md`](./.github/copilot-instructions.md) and [`CODESTYLE.md`](./CODESTYLE.md). +## Git and Commit Rules + +**These rules are absolute — no exceptions:** + +- **Never make git commits.** Claude Code 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. +- **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. + ## Key Requirements for All Projects Derived from This Template ### Build & Quality Standards From 6b0f4a9043516090ac2c792f905aaa0f27b3a223 Mon Sep 17 00:00:00 2001 From: Pieter Viljoen Date: Mon, 13 Apr 2026 13:20:20 -0700 Subject: [PATCH 3/3] Refactor concurrency group naming and update AI commit guidelines in documentation Signed-off-by: Pieter Viljoen --- .github/workflows/run-periodic-codegen-app-pull-request.yml | 6 ++++-- .github/workflows/run-periodic-codegen-pull-request.yml | 2 +- AGENTS.md | 2 +- 3 files changed, 6 insertions(+), 4 deletions(-) diff --git a/.github/workflows/run-periodic-codegen-app-pull-request.yml b/.github/workflows/run-periodic-codegen-app-pull-request.yml index 5ecfcfcc..fcd687f1 100644 --- a/.github/workflows/run-periodic-codegen-app-pull-request.yml +++ b/.github/workflows/run-periodic-codegen-app-pull-request.yml @@ -7,7 +7,7 @@ on: - cron: '0 2 * * THU' concurrency: - group: ${{ github.workflow }}-${{ github.ref }} + group: codegen-${{ github.ref }} cancel-in-progress: true jobs: @@ -15,7 +15,9 @@ jobs: run-codegen-app: name: Run codegen app and pull request job uses: ./.github/workflows/run-codegen-app-pull-request-task.yml - secrets: inherit + secrets: + CODEGEN_APP_ID: ${{ secrets.CODEGEN_APP_ID }} + CODEGEN_APP_PRIVATE_KEY: ${{ secrets.CODEGEN_APP_PRIVATE_KEY }} permissions: contents: write pull-requests: write diff --git a/.github/workflows/run-periodic-codegen-pull-request.yml b/.github/workflows/run-periodic-codegen-pull-request.yml index af1193ff..701a7294 100644 --- a/.github/workflows/run-periodic-codegen-pull-request.yml +++ b/.github/workflows/run-periodic-codegen-pull-request.yml @@ -7,7 +7,7 @@ on: - cron: '0 2 * * MON' concurrency: - group: ${{ github.workflow }}-${{ github.ref }} + group: codegen-${{ github.ref }} cancel-in-progress: true jobs: diff --git a/AGENTS.md b/AGENTS.md index b06d7ba4..e5283834 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -8,7 +8,7 @@ For comprehensive coding standards and detailed conventions, refer to [`.github/ **These rules are absolute — no exceptions:** -- **Never make git commits.** Claude Code 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. +- **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. - **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.