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
9 changes: 9 additions & 0 deletions .github/copilot-instructions.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -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
Expand Down
38 changes: 36 additions & 2 deletions .github/workflows/merge-bot-pull-request.yml
Original file line numberDiff line numberDiff line change
Expand Up@@ -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
Expand All@@ -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
Expand Down
65 changes: 65 additions & 0 deletions .github/workflows/run-codegen-app-pull-request-task.yml
Original file line numberDiff line numberDiff line change
@@ -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
3 changes: 3 additions & 0 deletions .github/workflows/run-codegen-pull-request-task.yml
Original file line numberDiff line numberDiff line change
Expand Up@@ -3,6 +3,7 @@ name: Run codegen and pull request task
on:
workflow_call:
secrets:
# Use PAT to trigger workflows
WORKFLOW_PAT:
required: true

Expand DownExpand Up@@ -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
Expand All@@ -59,4 +61,5 @@ jobs:
gh pr close "$PR"
gh pr reopen "$PR"
env:
# Use PAT to trigger workflows
GH_TOKEN: ${{ secrets.WORKFLOW_PAT }}
23 changes: 23 additions & 0 deletions .github/workflows/run-periodic-codegen-app-pull-request.yml
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
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: codegen-${{ 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:
CODEGEN_APP_ID: ${{ secrets.CODEGEN_APP_ID }}
CODEGEN_APP_PRIVATE_KEY: ${{ secrets.CODEGEN_APP_PRIVATE_KEY }}
permissions:
contents: write
pull-requests: write
4 changes: 2 additions & 2 deletions .github/workflows/run-periodic-codegen-pull-request.yml
Original file line numberDiff line numberDiff line change
Expand Up@@ -3,11 +3,11 @@ 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:
group: ${{ github.workflow }}-${{ github.ref }}
group: codegen-${{ github.ref }}
cancel-in-progress: true

jobs:
Expand Down
9 changes: 9 additions & 0 deletions AGENTS.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -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.** 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.

## Key Requirements for All Projects Derived from This Template

### Build & Quality Standards
Expand Down
2 changes: 1 addition & 1 deletion CodeGen/CodeGen.cs
Original file line numberDiff line numberDiff line change
Expand Up@@ -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);
}
Expand Down
17 changes: 10 additions & 7 deletions CodeGen/HttpClientFactory.cs
Original file line numberDiff line numberDiff line change
Expand Up@@ -27,14 +27,10 @@ internal static class HttpClientFactory
private static readonly TimeSpan s_httpClientTimeout = TimeSpan.FromSeconds(120);

private static readonly Lazy<HttpClient> s_httpClient = new(CreateHttpClient);
private static readonly Lazy<ResilienceHandler> 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<HttpResponseMessage>()
Expand DownExpand Up@@ -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)
);
Expand Down
32 changes: 30 additions & 2 deletions README.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -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**:

Expand Down
Loading