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
55 changes: 55 additions & 0 deletions .editorconfig
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
# https://editorconfig.org

# https://learn.microsoft.com/en-us/dotnet/csharp/fundamentals/coding-style/identifier-names
# https://learn.microsoft.com/en-us/dotnet/csharp/fundamentals/coding-style/coding-conventions
# https://learn.microsoft.com/en-us/dotnet/fundamentals/code-analysis/overview

# https://github.com/dotnet/runtime/blob/main/docs/coding-guidelines/coding-style.md
# https://github.com/dotnet/runtime/blob/main/.editorconfig

# https://learn.microsoft.com/en-us/dotnet/core/tools/dotnet-format
# dotnet format style --verify-no-changes --severity=info --verbosity=detailed

# Root config
root = true

# Defaults
[*]
charset = utf-8
indent_size = 4
indent_style = space
insert_final_newline = true
trim_trailing_whitespace = true

# Markdown files
[*.md]
end_of_line = crlf
trim_trailing_whitespace = false

# Xml files
[*.{xml,csproj,props,targets}]
end_of_line = crlf
indent_size = 2

# Yaml files
[*.{yml,yaml}]
end_of_line = crlf
indent_size = 2

# Json and JsonC files
[*.{json,jsonc}]
end_of_line = crlf

# Linux scripts
[*.sh]
end_of_line = lf

# Windows scripts
[*.{cmd,bat,ps1}]
end_of_line = crlf

# C# / ReSharper style rules from the upstream ProjectTemplate .editorconfig
# are intentionally omitted: this is a Docker-only repository with no C#
# sources. The per-extension line-ending rules above are the mandatory
# verbatim carry (see AGENTS.md "Files and Sections Derived Repos Must Carry
# Verbatim"); re-sync them from ptr727/ProjectTemplate.
10 changes: 5 additions & 5 deletions .gitattributes
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
# Leave line endings alone
# git config --global core.autocrlf false
# git add --renormalize .
# git ls-files --eol
* -text
# Leave line endings alone
# git config --global core.autocrlf false
# git add --renormalize .
# git ls-files --eol
* -text
168 changes: 168 additions & 0 deletions .github/copilot-instructions.md
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,168 @@
# Copilot Instructions

Repository conventions for GitHub Copilot (and any other AI agent reading this file).

The **canonical guide is [AGENTS.md](../AGENTS.md)** at the repo root - read it first for this repo's conventions, including the [PR Review Etiquette](../AGENTS.md#pr-review-etiquette) review-loop contract that this file's runbook implements.

This file is intentionally narrow: commit/PR-title conventions (so VS Code's AI commit-message and PR-title generators get them without an extra fetch), plus a GitHub Copilot Review Runbook that documents the provider-specific mechanics behind the review-loop contract defined in AGENTS.md.

This repository ships only a Docker image and has no language source tree, so there is no language-specific style guide; [AGENTS.md](../AGENTS.md) is the single source of truth for all conventions.

## Commit Messages and Pull Request Titles

Feature -> develop PRs squash-merge - the PR title becomes the single commit on develop. Develop -> main PRs merge-commit - main's history shows one merge commit per release with develop's tip as the second parent. Titles are descriptive and have no versioning effect - versioning is handled by [Nerdbank.GitVersioning](https://github.com/dotnet/Nerdbank.GitVersioning) reading [version.json](../version.json) and git history, not by parsing commit messages.

Branch protection enforces the merge method on both bases (develop allows only squash, main allows only merge). When running `gh pr merge` against either base, pick the matching flag (`--squash` for develop, `--merge` for main); a mismatch fails with "Merge method ... is not allowed on this repository". The merge-bot workflow (`.github/workflows/merge-bot-pull-request.yml`) does this dispatch automatically for Dependabot and codegen PRs via a `case` on `base.ref` - keep that pattern when adding new auto-merge jobs.

### Format

- Imperative subject summarizing the change, <= 72 characters, no trailing period. ("Add 24-hour PM2.5 average sensor", not "Added X" or "Adds X".)
- Optional body, blank-line separated, explaining *why* the change is being made when that's non-obvious. The diff shows *what*.

### Rules

- Don't write `update stuff`, `wip`, or other vague titles. (Dependabot's default `Bump X from Y to Z` titles are fine - keep them.)
- Don't add `Co-Authored-By:` lines unless the user explicitly asks.
- Don't put release-bump magnitude in the title - no "minor", "patch", "release v0.2.0", etc. NBGV computes the next release version from `version.json` + git history. Dependency versions in dependency-bump titles are fine and expected.
- Use US English spelling and match the existing heading style of the file you're editing: title case with lowercase short bind words (a, an, the, and, but, or, of, in, on, at, to, by, for, from); hyphenated compounds capitalize both parts unless the second is a short preposition (*Built-in*, *EPA-Corrected*, *24-Hour*).

### Examples

```text
Add structured logging extensions to library
Pin softprops/action-gh-release to commit SHA
Drop net8.0 multi-targeting from console project
Bump xunit.v3 from 3.2.2 to 3.3.0
Clarify devcontainer setup steps in README
```

## GitHub Copilot Review Runbook

> This runbook implements the [AGENTS.md "PR Review Etiquette"](../AGENTS.md#pr-review-etiquette) review-loop contract for GitHub Copilot. Without it in-repo, an agent has no pointer to the reliable Copilot mechanics and falls back to known-broken paths (the no-op `POST /requested_reviewers`, the wrong bot-login filter). In the API snippets below, fill the `ptr727` / `ESPHome-NonRoot` / `<N>` placeholders.

Use this section for provider-specific mechanics. The expected review loop *contract* (request review on every push, verify head-SHA coverage, triage findings, reply + resolve, escalate when stuck) is defined in [AGENTS.md -> PR Review Etiquette](../AGENTS.md#pr-review-etiquette). This section only describes how to make GitHub Copilot reliably execute it.

### Triggering and Polling

Auto-review on push is configured (via the branch ruleset's `copilot_code_review` rule with `review_on_push: true`) but fires inconsistently in practice - treat it as best-effort, not guaranteed. After every push, **re-request a review programmatically** via the GraphQL `requestReviews` mutation, passing the Copilot reviewer's bot node id in `botIds`. This now works reliably (it previously did not - a maintainer had to click "re-request review" in the UI; the agent can now drive the loop end-to-end without that hand-off).

> **The reviewer login differs by API - this is intentional, not a typo.** In **GraphQL** (`gh api graphql` and `gh pr view --json reviews`, which is GraphQL-backed) the `Bot.login` is `copilot-pull-request-reviewer` - **no `[bot]` suffix**. In the **REST** API (`gh api repos/.../issues|pulls/...`) the same account's `user.login` is `copilot-pull-request-reviewer[bot]` - **with** the suffix. Each query below uses the correct form for its API; match the API, not a single spelling, when adapting them.

```sh
# 1. PR node id + the Copilot reviewer's bot node id (read from any existing
# Copilot review; the reviewer login is `copilot-pull-request-reviewer`).
PR_NODE=$(gh pr view <N> --json id --jq '.id')
BOT_ID=$(gh api graphql -f query='
{
repository(owner: "ptr727", name: "ESPHome-NonRoot") {
pullRequest(number: <N>) {
reviews(first: 50) { nodes { author { __typename login ... on Bot { id } } } }
}
}
}' --jq '[.data.repository.pullRequest.reviews.nodes[]
| select(.author.login == "copilot-pull-request-reviewer")
| .author.id] | first')

# 2. Re-request a Copilot review on the current head.
gh api graphql -f query='
mutation($pr: ID!, $bot: ID!) {
requestReviews(input: { pullRequestId: $pr, botIds: [$bot], union: true }) {
pullRequest { id }
}
}' -F pr="$PR_NODE" -F bot="$BOT_ID"
```

The bot node id is read from an existing Copilot review, so step 1 needs at least one prior review on the PR - the auto-review-on-open normally supplies the first one. If no Copilot review exists yet and auto-review didn't fire, request `Copilot` once through the GitHub PR UI to seed it, then use the mutation for every subsequent re-request.

**Do NOT post `@Copilot review` as a PR comment.** That comment triggers the Copilot *coding agent* (`copilot-swe-agent[bot]`), which makes code changes rather than posting a review.

Known non-working request paths (don't rely on them - use the `requestReviews` mutation above instead):

- `POST /requested_reviewers` with `reviewers=[Copilot]` can return 200 but no-op.
- `copilot-pull-request-reviewer` as a requested reviewer slug returns 422.

### Verify Review Covered Current Head

Before merging, confirm Copilot reviewed the current PR head SHA. Copilot may respond as either a formal review (carries an exact commit SHA) or an issue comment (no SHA - use the most recent Copilot comment for manual confirmation). Check both.

```sh
PR_HEAD=$(gh pr view <N> --json headRefOid --jq '.headRefOid')

# 1. Formal review - exact SHA match.
gh pr view <N> --json reviews --jq \
'.reviews[] | select(.author.login=="copilot-pull-request-reviewer") | .commit.oid' \
| grep -q "$PR_HEAD" && echo "covered via formal review"

# 2. Issue comment - show the most recent Copilot comment for manual
# confirmation. This is the REST API, so the login carries the `[bot]` suffix.
gh api repos/ptr727/ESPHome-NonRoot/issues/<N>/comments --jq \
'[.[] | select(.user.login=="copilot-pull-request-reviewer[bot]")] | last | {created_at, body: .body[:200]}'
```

Coverage is confirmed when (1) exits 0. For issue comments (path 2), body content is the only reliable signal - `created_at` is not: `git log -1 --format=%cI` is the **commit** timestamp, not the push timestamp, so amended or rebased commits can have an earlier timestamp and an older Copilot comment could satisfy a time check even though Copilot never saw the current head. Treat path (2) as confirmed only when the comment body explicitly refers to the current changes.

### Bounded Retry Workflow

If a review did not run on the current head, retry:

1. Wait briefly and check head-SHA coverage (see above).
1. Re-request the review via the `requestReviews` mutation (see "Triggering and Polling"); fall back to the GitHub PR UI only if the mutation no-ops.
1. Retry up to two more times (three total).
1. If still missing, mark review as blocked and escalate to the user/maintainer with what was attempted.

### Reply and Thread Resolution Workflow

List unresolved threads. Use `first: 100` with cursor-based pagination; if `hasNextPage` is true, re-run with `after: "<endCursor>"` to retrieve the next page:

```sh
gh api graphql -f query='
{
repository(owner: "ptr727", name: "ESPHome-NonRoot") {
pullRequest(number: <N>) {
reviewThreads(first: 100) {
nodes {
id isResolved path
comments(first: 1) { nodes { author { login } body } }
}
pageInfo { hasNextPage endCursor }
}
}
}
}' | jq '
.data.repository.pullRequest.reviewThreads |
(.pageInfo | "hasNextPage=\(.hasNextPage) endCursor=\(.endCursor)"),
(.nodes[] | select(.isResolved == false))
'
```

Reply on a thread, then resolve it:

```sh
gh api graphql -f query='
mutation($threadId: ID!, $body: String!) {
addPullRequestReviewThreadReply(input: { pullRequestReviewThreadId: $threadId, body: $body }) {
comment { id }
}
}' -F threadId="PRRT_..." -F body="Fixed in <SHA>: <one-line summary>."

gh api graphql -f query='
mutation($threadId: ID!) {
resolveReviewThread(input: { threadId: $threadId }) { thread { id isResolved } }
}' -F threadId="PRRT_..."
```

Issue-level Copilot comments (those in `issues/<N>/comments`) have no resolution action - GitHub provides no API or UI to resolve them. Reply if the finding warrants it; no resolution step is needed or possible.

Reply-body conventions:

- Accepted bug/style fix: include fixing commit SHA and a one-line summary.
- Declined style comment: cite the rule (AGENTS.md) and the existing-tree precedent.
- Declined architecture proposal: one-sentence rationale.

After the final push, sweep-resolve stale older threads for removed code paths.

## When in Doubt

Read [AGENTS.md](../AGENTS.md) for this repo's conventions. Don't restate its rules in commit bodies or PR descriptions - keep those focused on the change itself.

**In a derived repo:** if you find a discrepancy that should be fixed in the template itself (this file or AGENTS.md is out of date, a rule is missing, something bit this repo and would bite the next), open an issue upstream in [`ptr727/ProjectTemplate`](https://github.com/ptr727/ProjectTemplate) rather than only fixing it locally - see the template's [AGENTS.md "Staying in Sync and Reporting Drift Upstream"](https://github.com/ptr727/ProjectTemplate/blob/main/AGENTS.md#staying-in-sync-and-reporting-drift-upstream).
30 changes: 29 additions & 1 deletion .github/dependabot.yml
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,13 @@
# https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates
# https://containers.dev/guide/dependabot
#
# Each ecosystem targets both main and develop so neither branch goes stale
# between releases.

version: 2
updates:

# ----- github-actions -----

# main
- package-ecosystem: "github-actions"
target-branch: "main"
Expand All@@ -25,3 +29,27 @@ updates:
actions-deps:
patterns:
- "*"

# ----- docker (base image in Docker/Dockerfile) -----

# main
- package-ecosystem: "docker"
target-branch: "main"
directory: "/Docker"
schedule:
interval: "daily"
groups:
docker-deps:
patterns:
- "*"

# develop
- package-ecosystem: "docker"
target-branch: "develop"
directory: "/Docker"
schedule:
interval: "daily"
groups:
docker-deps:
patterns:
- "*"
Loading
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { // Add copy buttons to all
 blocks
(function() {
function addCopyButtons() {
document.querySelectorAll('pre code').forEach(function(codeBlock) {
if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;
codeBlock.parentElement.setAttribute('data-copy-added', 'true');
var btn = document.createElement('button');
btn.textContent = 'Copy';
btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';
btn.onmouseover = function() { this.style.opacity = '1'; };
btn.onmouseout = function() { this.style.opacity = '0.7'; };
btn.onclick = function() {
navigator.clipboard.writeText(codeBlock.textContent).then(function() {
btn.textContent = 'Copied!';
setTimeout(function() { btn.textContent = 'Copy'; }, 1500);
});
};
codeBlock.parentElement.style.position = 'relative';
codeBlock.parentElement.appendChild(btn);
});
}
addCopyButtons();
// Re-run on dynamic content
var observer = new MutationObserver(addCopyButtons);
observer.observe(document.body, { childList: true, subtree: true });
})();
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
Adopt ProjectTemplate Docker release pipeline and conventions by ptr727 · Pull Request #61 · ptr727/ESPHome-NonRoot · GitHub
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
55 changes: 55 additions & 0 deletions .editorconfig
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
# https://editorconfig.org

# https://learn.microsoft.com/en-us/dotnet/csharp/fundamentals/coding-style/identifier-names
# https://learn.microsoft.com/en-us/dotnet/csharp/fundamentals/coding-style/coding-conventions
# https://learn.microsoft.com/en-us/dotnet/fundamentals/code-analysis/overview

# https://github.com/dotnet/runtime/blob/main/docs/coding-guidelines/coding-style.md
# https://github.com/dotnet/runtime/blob/main/.editorconfig

# https://learn.microsoft.com/en-us/dotnet/core/tools/dotnet-format
# dotnet format style --verify-no-changes --severity=info --verbosity=detailed

# Root config
root = true

# Defaults
[*]
charset = utf-8
indent_size = 4
indent_style = space
insert_final_newline = true
trim_trailing_whitespace = true

# Markdown files
[*.md]
end_of_line = crlf
trim_trailing_whitespace = false

# Xml files
[*.{xml,csproj,props,targets}]
end_of_line = crlf
indent_size = 2

# Yaml files
[*.{yml,yaml}]
end_of_line = crlf
indent_size = 2

# Json and JsonC files
[*.{json,jsonc}]
end_of_line = crlf

# Linux scripts
[*.sh]
end_of_line = lf

# Windows scripts
[*.{cmd,bat,ps1}]
end_of_line = crlf

# C# / ReSharper style rules from the upstream ProjectTemplate .editorconfig
# are intentionally omitted: this is a Docker-only repository with no C#
# sources. The per-extension line-ending rules above are the mandatory
# verbatim carry (see AGENTS.md "Files and Sections Derived Repos Must Carry
# Verbatim"); re-sync them from ptr727/ProjectTemplate.
10 changes: 5 additions & 5 deletions .gitattributes
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
# Leave line endings alone
# git config --global core.autocrlf false
# git add --renormalize .
# git ls-files --eol
* -text
# Leave line endings alone
# git config --global core.autocrlf false
# git add --renormalize .
# git ls-files --eol
* -text
168 changes: 168 additions & 0 deletions .github/copilot-instructions.md
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,168 @@
# Copilot Instructions

Repository conventions for GitHub Copilot (and any other AI agent reading this file).

The **canonical guide is [AGENTS.md](../AGENTS.md)** at the repo root - read it first for this repo's conventions, including the [PR Review Etiquette](../AGENTS.md#pr-review-etiquette) review-loop contract that this file's runbook implements.

This file is intentionally narrow: commit/PR-title conventions (so VS Code's AI commit-message and PR-title generators get them without an extra fetch), plus a GitHub Copilot Review Runbook that documents the provider-specific mechanics behind the review-loop contract defined in AGENTS.md.

This repository ships only a Docker image and has no language source tree, so there is no language-specific style guide; [AGENTS.md](../AGENTS.md) is the single source of truth for all conventions.

## Commit Messages and Pull Request Titles

Feature -> develop PRs squash-merge - the PR title becomes the single commit on develop. Develop -> main PRs merge-commit - main's history shows one merge commit per release with develop's tip as the second parent. Titles are descriptive and have no versioning effect - versioning is handled by [Nerdbank.GitVersioning](https://github.com/dotnet/Nerdbank.GitVersioning) reading [version.json](../version.json) and git history, not by parsing commit messages.

Branch protection enforces the merge method on both bases (develop allows only squash, main allows only merge). When running `gh pr merge` against either base, pick the matching flag (`--squash` for develop, `--merge` for main); a mismatch fails with "Merge method ... is not allowed on this repository". The merge-bot workflow (`.github/workflows/merge-bot-pull-request.yml`) does this dispatch automatically for Dependabot and codegen PRs via a `case` on `base.ref` - keep that pattern when adding new auto-merge jobs.

### Format

- Imperative subject summarizing the change, <= 72 characters, no trailing period. ("Add 24-hour PM2.5 average sensor", not "Added X" or "Adds X".)
- Optional body, blank-line separated, explaining *why* the change is being made when that's non-obvious. The diff shows *what*.

### Rules

- Don't write `update stuff`, `wip`, or other vague titles. (Dependabot's default `Bump X from Y to Z` titles are fine - keep them.)
- Don't add `Co-Authored-By:` lines unless the user explicitly asks.
- Don't put release-bump magnitude in the title - no "minor", "patch", "release v0.2.0", etc. NBGV computes the next release version from `version.json` + git history. Dependency versions in dependency-bump titles are fine and expected.
- Use US English spelling and match the existing heading style of the file you're editing: title case with lowercase short bind words (a, an, the, and, but, or, of, in, on, at, to, by, for, from); hyphenated compounds capitalize both parts unless the second is a short preposition (*Built-in*, *EPA-Corrected*, *24-Hour*).

### Examples

```text
Add structured logging extensions to library
Pin softprops/action-gh-release to commit SHA
Drop net8.0 multi-targeting from console project
Bump xunit.v3 from 3.2.2 to 3.3.0
Clarify devcontainer setup steps in README
```

## GitHub Copilot Review Runbook

> This runbook implements the [AGENTS.md "PR Review Etiquette"](../AGENTS.md#pr-review-etiquette) review-loop contract for GitHub Copilot. Without it in-repo, an agent has no pointer to the reliable Copilot mechanics and falls back to known-broken paths (the no-op `POST /requested_reviewers`, the wrong bot-login filter). In the API snippets below, fill the `ptr727` / `ESPHome-NonRoot` / `<N>` placeholders.

Use this section for provider-specific mechanics. The expected review loop *contract* (request review on every push, verify head-SHA coverage, triage findings, reply + resolve, escalate when stuck) is defined in [AGENTS.md -> PR Review Etiquette](../AGENTS.md#pr-review-etiquette). This section only describes how to make GitHub Copilot reliably execute it.

### Triggering and Polling

Auto-review on push is configured (via the branch ruleset's `copilot_code_review` rule with `review_on_push: true`) but fires inconsistently in practice - treat it as best-effort, not guaranteed. After every push, **re-request a review programmatically** via the GraphQL `requestReviews` mutation, passing the Copilot reviewer's bot node id in `botIds`. This now works reliably (it previously did not - a maintainer had to click "re-request review" in the UI; the agent can now drive the loop end-to-end without that hand-off).

> **The reviewer login differs by API - this is intentional, not a typo.** In **GraphQL** (`gh api graphql` and `gh pr view --json reviews`, which is GraphQL-backed) the `Bot.login` is `copilot-pull-request-reviewer` - **no `[bot]` suffix**. In the **REST** API (`gh api repos/.../issues|pulls/...`) the same account's `user.login` is `copilot-pull-request-reviewer[bot]` - **with** the suffix. Each query below uses the correct form for its API; match the API, not a single spelling, when adapting them.

```sh
# 1. PR node id + the Copilot reviewer's bot node id (read from any existing
# Copilot review; the reviewer login is `copilot-pull-request-reviewer`).
PR_NODE=$(gh pr view <N> --json id --jq '.id')
BOT_ID=$(gh api graphql -f query='
{
repository(owner: "ptr727", name: "ESPHome-NonRoot") {
pullRequest(number: <N>) {
reviews(first: 50) { nodes { author { __typename login ... on Bot { id } } } }
}
}
}' --jq '[.data.repository.pullRequest.reviews.nodes[]
| select(.author.login == "copilot-pull-request-reviewer")
| .author.id] | first')

# 2. Re-request a Copilot review on the current head.
gh api graphql -f query='
mutation($pr: ID!, $bot: ID!) {
requestReviews(input: { pullRequestId: $pr, botIds: [$bot], union: true }) {
pullRequest { id }
}
}' -F pr="$PR_NODE" -F bot="$BOT_ID"
```

The bot node id is read from an existing Copilot review, so step 1 needs at least one prior review on the PR - the auto-review-on-open normally supplies the first one. If no Copilot review exists yet and auto-review didn't fire, request `Copilot` once through the GitHub PR UI to seed it, then use the mutation for every subsequent re-request.

**Do NOT post `@Copilot review` as a PR comment.** That comment triggers the Copilot *coding agent* (`copilot-swe-agent[bot]`), which makes code changes rather than posting a review.

Known non-working request paths (don't rely on them - use the `requestReviews` mutation above instead):

- `POST /requested_reviewers` with `reviewers=[Copilot]` can return 200 but no-op.
- `copilot-pull-request-reviewer` as a requested reviewer slug returns 422.

### Verify Review Covered Current Head

Before merging, confirm Copilot reviewed the current PR head SHA. Copilot may respond as either a formal review (carries an exact commit SHA) or an issue comment (no SHA - use the most recent Copilot comment for manual confirmation). Check both.

```sh
PR_HEAD=$(gh pr view <N> --json headRefOid --jq '.headRefOid')

# 1. Formal review - exact SHA match.
gh pr view <N> --json reviews --jq \
'.reviews[] | select(.author.login=="copilot-pull-request-reviewer") | .commit.oid' \
| grep -q "$PR_HEAD" && echo "covered via formal review"

# 2. Issue comment - show the most recent Copilot comment for manual
# confirmation. This is the REST API, so the login carries the `[bot]` suffix.
gh api repos/ptr727/ESPHome-NonRoot/issues/<N>/comments --jq \
'[.[] | select(.user.login=="copilot-pull-request-reviewer[bot]")] | last | {created_at, body: .body[:200]}'
```

Coverage is confirmed when (1) exits 0. For issue comments (path 2), body content is the only reliable signal - `created_at` is not: `git log -1 --format=%cI` is the **commit** timestamp, not the push timestamp, so amended or rebased commits can have an earlier timestamp and an older Copilot comment could satisfy a time check even though Copilot never saw the current head. Treat path (2) as confirmed only when the comment body explicitly refers to the current changes.

### Bounded Retry Workflow

If a review did not run on the current head, retry:

1. Wait briefly and check head-SHA coverage (see above).
1. Re-request the review via the `requestReviews` mutation (see "Triggering and Polling"); fall back to the GitHub PR UI only if the mutation no-ops.
1. Retry up to two more times (three total).
1. If still missing, mark review as blocked and escalate to the user/maintainer with what was attempted.

### Reply and Thread Resolution Workflow

List unresolved threads. Use `first: 100` with cursor-based pagination; if `hasNextPage` is true, re-run with `after: "<endCursor>"` to retrieve the next page:

```sh
gh api graphql -f query='
{
repository(owner: "ptr727", name: "ESPHome-NonRoot") {
pullRequest(number: <N>) {
reviewThreads(first: 100) {
nodes {
id isResolved path
comments(first: 1) { nodes { author { login } body } }
}
pageInfo { hasNextPage endCursor }
}
}
}
}' | jq '
.data.repository.pullRequest.reviewThreads |
(.pageInfo | "hasNextPage=\(.hasNextPage) endCursor=\(.endCursor)"),
(.nodes[] | select(.isResolved == false))
'
```

Reply on a thread, then resolve it:

```sh
gh api graphql -f query='
mutation($threadId: ID!, $body: String!) {
addPullRequestReviewThreadReply(input: { pullRequestReviewThreadId: $threadId, body: $body }) {
comment { id }
}
}' -F threadId="PRRT_..." -F body="Fixed in <SHA>: <one-line summary>."

gh api graphql -f query='
mutation($threadId: ID!) {
resolveReviewThread(input: { threadId: $threadId }) { thread { id isResolved } }
}' -F threadId="PRRT_..."
```

Issue-level Copilot comments (those in `issues/<N>/comments`) have no resolution action - GitHub provides no API or UI to resolve them. Reply if the finding warrants it; no resolution step is needed or possible.

Reply-body conventions:

- Accepted bug/style fix: include fixing commit SHA and a one-line summary.
- Declined style comment: cite the rule (AGENTS.md) and the existing-tree precedent.
- Declined architecture proposal: one-sentence rationale.

After the final push, sweep-resolve stale older threads for removed code paths.

## When in Doubt

Read [AGENTS.md](../AGENTS.md) for this repo's conventions. Don't restate its rules in commit bodies or PR descriptions - keep those focused on the change itself.

**In a derived repo:** if you find a discrepancy that should be fixed in the template itself (this file or AGENTS.md is out of date, a rule is missing, something bit this repo and would bite the next), open an issue upstream in [`ptr727/ProjectTemplate`](https://github.com/ptr727/ProjectTemplate) rather than only fixing it locally - see the template's [AGENTS.md "Staying in Sync and Reporting Drift Upstream"](https://github.com/ptr727/ProjectTemplate/blob/main/AGENTS.md#staying-in-sync-and-reporting-drift-upstream).
30 changes: 29 additions & 1 deletion .github/dependabot.yml
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,13 @@
# https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates
# https://containers.dev/guide/dependabot
#
# Each ecosystem targets both main and develop so neither branch goes stale
# between releases.

version: 2
updates:

# ----- github-actions -----

# main
- package-ecosystem: "github-actions"
target-branch: "main"
Expand All@@ -25,3 +29,27 @@ updates:
actions-deps:
patterns:
- "*"

# ----- docker (base image in Docker/Dockerfile) -----

# main
- package-ecosystem: "docker"
target-branch: "main"
directory: "/Docker"
schedule:
interval: "daily"
groups:
docker-deps:
patterns:
- "*"

# develop
- package-ecosystem: "docker"
target-branch: "develop"
directory: "/Docker"
schedule:
interval: "daily"
groups:
docker-deps:
patterns:
- "*"
Loading
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { // Force GitHub README to respect dark mode (function() { var style = document.createElement('style'); style.textContent = ' .markdown-body { color-scheme: dark light; } .markdown-body pre { background: #161b22 !important; } .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; } .markdown-body table th, .markdown-body table td { border-color: #30363d !important; } .markdown-body img { background: #0d1117; } .markdown-body blockquote { border-left-color: #8b949e; } .markdown-body hr { border-color: #30363d; } '; document.head.appendChild(style); })(); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' Adopt ProjectTemplate Docker release pipeline and conventions by ptr727 · Pull Request #61 · ptr727/ESPHome-NonRoot · GitHub
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
55 changes: 55 additions & 0 deletions .editorconfig
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
# https://editorconfig.org

# https://learn.microsoft.com/en-us/dotnet/csharp/fundamentals/coding-style/identifier-names
# https://learn.microsoft.com/en-us/dotnet/csharp/fundamentals/coding-style/coding-conventions
# https://learn.microsoft.com/en-us/dotnet/fundamentals/code-analysis/overview

# https://github.com/dotnet/runtime/blob/main/docs/coding-guidelines/coding-style.md
# https://github.com/dotnet/runtime/blob/main/.editorconfig

# https://learn.microsoft.com/en-us/dotnet/core/tools/dotnet-format
# dotnet format style --verify-no-changes --severity=info --verbosity=detailed

# Root config
root = true

# Defaults
[*]
charset = utf-8
indent_size = 4
indent_style = space
insert_final_newline = true
trim_trailing_whitespace = true

# Markdown files
[*.md]
end_of_line = crlf
trim_trailing_whitespace = false

# Xml files
[*.{xml,csproj,props,targets}]
end_of_line = crlf
indent_size = 2

# Yaml files
[*.{yml,yaml}]
end_of_line = crlf
indent_size = 2

# Json and JsonC files
[*.{json,jsonc}]
end_of_line = crlf

# Linux scripts
[*.sh]
end_of_line = lf

# Windows scripts
[*.{cmd,bat,ps1}]
end_of_line = crlf

# C# / ReSharper style rules from the upstream ProjectTemplate .editorconfig
# are intentionally omitted: this is a Docker-only repository with no C#
# sources. The per-extension line-ending rules above are the mandatory
# verbatim carry (see AGENTS.md "Files and Sections Derived Repos Must Carry
# Verbatim"); re-sync them from ptr727/ProjectTemplate.
10 changes: 5 additions & 5 deletions .gitattributes
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
# Leave line endings alone
# git config --global core.autocrlf false
# git add --renormalize .
# git ls-files --eol
* -text
# Leave line endings alone
# git config --global core.autocrlf false
# git add --renormalize .
# git ls-files --eol
* -text
168 changes: 168 additions & 0 deletions .github/copilot-instructions.md
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,168 @@
# Copilot Instructions

Repository conventions for GitHub Copilot (and any other AI agent reading this file).

The **canonical guide is [AGENTS.md](../AGENTS.md)** at the repo root - read it first for this repo's conventions, including the [PR Review Etiquette](../AGENTS.md#pr-review-etiquette) review-loop contract that this file's runbook implements.

This file is intentionally narrow: commit/PR-title conventions (so VS Code's AI commit-message and PR-title generators get them without an extra fetch), plus a GitHub Copilot Review Runbook that documents the provider-specific mechanics behind the review-loop contract defined in AGENTS.md.

This repository ships only a Docker image and has no language source tree, so there is no language-specific style guide; [AGENTS.md](../AGENTS.md) is the single source of truth for all conventions.

## Commit Messages and Pull Request Titles

Feature -> develop PRs squash-merge - the PR title becomes the single commit on develop. Develop -> main PRs merge-commit - main's history shows one merge commit per release with develop's tip as the second parent. Titles are descriptive and have no versioning effect - versioning is handled by [Nerdbank.GitVersioning](https://github.com/dotnet/Nerdbank.GitVersioning) reading [version.json](../version.json) and git history, not by parsing commit messages.

Branch protection enforces the merge method on both bases (develop allows only squash, main allows only merge). When running `gh pr merge` against either base, pick the matching flag (`--squash` for develop, `--merge` for main); a mismatch fails with "Merge method ... is not allowed on this repository". The merge-bot workflow (`.github/workflows/merge-bot-pull-request.yml`) does this dispatch automatically for Dependabot and codegen PRs via a `case` on `base.ref` - keep that pattern when adding new auto-merge jobs.

### Format

- Imperative subject summarizing the change, <= 72 characters, no trailing period. ("Add 24-hour PM2.5 average sensor", not "Added X" or "Adds X".)
- Optional body, blank-line separated, explaining *why* the change is being made when that's non-obvious. The diff shows *what*.

### Rules

- Don't write `update stuff`, `wip`, or other vague titles. (Dependabot's default `Bump X from Y to Z` titles are fine - keep them.)
- Don't add `Co-Authored-By:` lines unless the user explicitly asks.
- Don't put release-bump magnitude in the title - no "minor", "patch", "release v0.2.0", etc. NBGV computes the next release version from `version.json` + git history. Dependency versions in dependency-bump titles are fine and expected.
- Use US English spelling and match the existing heading style of the file you're editing: title case with lowercase short bind words (a, an, the, and, but, or, of, in, on, at, to, by, for, from); hyphenated compounds capitalize both parts unless the second is a short preposition (*Built-in*, *EPA-Corrected*, *24-Hour*).

### Examples

```text
Add structured logging extensions to library
Pin softprops/action-gh-release to commit SHA
Drop net8.0 multi-targeting from console project
Bump xunit.v3 from 3.2.2 to 3.3.0
Clarify devcontainer setup steps in README
```

## GitHub Copilot Review Runbook

> This runbook implements the [AGENTS.md "PR Review Etiquette"](../AGENTS.md#pr-review-etiquette) review-loop contract for GitHub Copilot. Without it in-repo, an agent has no pointer to the reliable Copilot mechanics and falls back to known-broken paths (the no-op `POST /requested_reviewers`, the wrong bot-login filter). In the API snippets below, fill the `ptr727` / `ESPHome-NonRoot` / `<N>` placeholders.

Use this section for provider-specific mechanics. The expected review loop *contract* (request review on every push, verify head-SHA coverage, triage findings, reply + resolve, escalate when stuck) is defined in [AGENTS.md -> PR Review Etiquette](../AGENTS.md#pr-review-etiquette). This section only describes how to make GitHub Copilot reliably execute it.

### Triggering and Polling

Auto-review on push is configured (via the branch ruleset's `copilot_code_review` rule with `review_on_push: true`) but fires inconsistently in practice - treat it as best-effort, not guaranteed. After every push, **re-request a review programmatically** via the GraphQL `requestReviews` mutation, passing the Copilot reviewer's bot node id in `botIds`. This now works reliably (it previously did not - a maintainer had to click "re-request review" in the UI; the agent can now drive the loop end-to-end without that hand-off).

> **The reviewer login differs by API - this is intentional, not a typo.** In **GraphQL** (`gh api graphql` and `gh pr view --json reviews`, which is GraphQL-backed) the `Bot.login` is `copilot-pull-request-reviewer` - **no `[bot]` suffix**. In the **REST** API (`gh api repos/.../issues|pulls/...`) the same account's `user.login` is `copilot-pull-request-reviewer[bot]` - **with** the suffix. Each query below uses the correct form for its API; match the API, not a single spelling, when adapting them.

```sh
# 1. PR node id + the Copilot reviewer's bot node id (read from any existing
# Copilot review; the reviewer login is `copilot-pull-request-reviewer`).
PR_NODE=$(gh pr view <N> --json id --jq '.id')
BOT_ID=$(gh api graphql -f query='
{
repository(owner: "ptr727", name: "ESPHome-NonRoot") {
pullRequest(number: <N>) {
reviews(first: 50) { nodes { author { __typename login ... on Bot { id } } } }
}
}
}' --jq '[.data.repository.pullRequest.reviews.nodes[]
| select(.author.login == "copilot-pull-request-reviewer")
| .author.id] | first')

# 2. Re-request a Copilot review on the current head.
gh api graphql -f query='
mutation($pr: ID!, $bot: ID!) {
requestReviews(input: { pullRequestId: $pr, botIds: [$bot], union: true }) {
pullRequest { id }
}
}' -F pr="$PR_NODE" -F bot="$BOT_ID"
```

The bot node id is read from an existing Copilot review, so step 1 needs at least one prior review on the PR - the auto-review-on-open normally supplies the first one. If no Copilot review exists yet and auto-review didn't fire, request `Copilot` once through the GitHub PR UI to seed it, then use the mutation for every subsequent re-request.

**Do NOT post `@Copilot review` as a PR comment.** That comment triggers the Copilot *coding agent* (`copilot-swe-agent[bot]`), which makes code changes rather than posting a review.

Known non-working request paths (don't rely on them - use the `requestReviews` mutation above instead):

- `POST /requested_reviewers` with `reviewers=[Copilot]` can return 200 but no-op.
- `copilot-pull-request-reviewer` as a requested reviewer slug returns 422.

### Verify Review Covered Current Head

Before merging, confirm Copilot reviewed the current PR head SHA. Copilot may respond as either a formal review (carries an exact commit SHA) or an issue comment (no SHA - use the most recent Copilot comment for manual confirmation). Check both.

```sh
PR_HEAD=$(gh pr view <N> --json headRefOid --jq '.headRefOid')

# 1. Formal review - exact SHA match.
gh pr view <N> --json reviews --jq \
'.reviews[] | select(.author.login=="copilot-pull-request-reviewer") | .commit.oid' \
| grep -q "$PR_HEAD" && echo "covered via formal review"

# 2. Issue comment - show the most recent Copilot comment for manual
# confirmation. This is the REST API, so the login carries the `[bot]` suffix.
gh api repos/ptr727/ESPHome-NonRoot/issues/<N>/comments --jq \
'[.[] | select(.user.login=="copilot-pull-request-reviewer[bot]")] | last | {created_at, body: .body[:200]}'
```

Coverage is confirmed when (1) exits 0. For issue comments (path 2), body content is the only reliable signal - `created_at` is not: `git log -1 --format=%cI` is the **commit** timestamp, not the push timestamp, so amended or rebased commits can have an earlier timestamp and an older Copilot comment could satisfy a time check even though Copilot never saw the current head. Treat path (2) as confirmed only when the comment body explicitly refers to the current changes.

### Bounded Retry Workflow

If a review did not run on the current head, retry:

1. Wait briefly and check head-SHA coverage (see above).
1. Re-request the review via the `requestReviews` mutation (see "Triggering and Polling"); fall back to the GitHub PR UI only if the mutation no-ops.
1. Retry up to two more times (three total).
1. If still missing, mark review as blocked and escalate to the user/maintainer with what was attempted.

### Reply and Thread Resolution Workflow

List unresolved threads. Use `first: 100` with cursor-based pagination; if `hasNextPage` is true, re-run with `after: "<endCursor>"` to retrieve the next page:

```sh
gh api graphql -f query='
{
repository(owner: "ptr727", name: "ESPHome-NonRoot") {
pullRequest(number: <N>) {
reviewThreads(first: 100) {
nodes {
id isResolved path
comments(first: 1) { nodes { author { login } body } }
}
pageInfo { hasNextPage endCursor }
}
}
}
}' | jq '
.data.repository.pullRequest.reviewThreads |
(.pageInfo | "hasNextPage=\(.hasNextPage) endCursor=\(.endCursor)"),
(.nodes[] | select(.isResolved == false))
'
```

Reply on a thread, then resolve it:

```sh
gh api graphql -f query='
mutation($threadId: ID!, $body: String!) {
addPullRequestReviewThreadReply(input: { pullRequestReviewThreadId: $threadId, body: $body }) {
comment { id }
}
}' -F threadId="PRRT_..." -F body="Fixed in <SHA>: <one-line summary>."

gh api graphql -f query='
mutation($threadId: ID!) {
resolveReviewThread(input: { threadId: $threadId }) { thread { id isResolved } }
}' -F threadId="PRRT_..."
```

Issue-level Copilot comments (those in `issues/<N>/comments`) have no resolution action - GitHub provides no API or UI to resolve them. Reply if the finding warrants it; no resolution step is needed or possible.

Reply-body conventions:

- Accepted bug/style fix: include fixing commit SHA and a one-line summary.
- Declined style comment: cite the rule (AGENTS.md) and the existing-tree precedent.
- Declined architecture proposal: one-sentence rationale.

After the final push, sweep-resolve stale older threads for removed code paths.

## When in Doubt

Read [AGENTS.md](../AGENTS.md) for this repo's conventions. Don't restate its rules in commit bodies or PR descriptions - keep those focused on the change itself.

**In a derived repo:** if you find a discrepancy that should be fixed in the template itself (this file or AGENTS.md is out of date, a rule is missing, something bit this repo and would bite the next), open an issue upstream in [`ptr727/ProjectTemplate`](https://github.com/ptr727/ProjectTemplate) rather than only fixing it locally - see the template's [AGENTS.md "Staying in Sync and Reporting Drift Upstream"](https://github.com/ptr727/ProjectTemplate/blob/main/AGENTS.md#staying-in-sync-and-reporting-drift-upstream).
30 changes: 29 additions & 1 deletion .github/dependabot.yml
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,13 @@
# https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates
# https://containers.dev/guide/dependabot
#
# Each ecosystem targets both main and develop so neither branch goes stale
# between releases.

version: 2
updates:

# ----- github-actions -----

# main
- package-ecosystem: "github-actions"
target-branch: "main"
Expand All@@ -25,3 +29,27 @@ updates:
actions-deps:
patterns:
- "*"

# ----- docker (base image in Docker/Dockerfile) -----

# main
- package-ecosystem: "docker"
target-branch: "main"
directory: "/Docker"
schedule:
interval: "daily"
groups:
docker-deps:
patterns:
- "*"

# develop
- package-ecosystem: "docker"
target-branch: "develop"
directory: "/Docker"
schedule:
interval: "daily"
groups:
docker-deps:
patterns:
- "*"
Loading
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { // Highlight search terms from Google/DuckDuckGo/Bing referrer (function() { var ref = document.referrer; var terms = []; if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) { var url = new URL(ref); var q = url.searchParams.get('q') || url.searchParams.get('p'); if (q) { terms = q.split(/\s+/).filter(function(t) { return t.length > 2; }); } } if (terms.length === 0) return; var style = document.createElement('style'); style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }'; document.head.appendChild(style); function highlight(node) { if (node.nodeType === 3) { // text node var text = node.textContent; var found = false; terms.forEach(function(term) { var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\]\\]/g, '\\') + ')', 'gi'); if (regex.test(text)) { found = true; var frag = document.createDocumentFragment(); var parts = text.split(regex); parts.forEach(function(part, i) { if (i % 2 === 0) { frag.appendChild(document.createTextNode(part)); } else { var span = document.createElement('span'); span.className = 'userscript-highlight'; span.textContent = part; frag.appendChild(span); } }); node.parentNode.replaceChild(frag, node); } }); } else if (node.nodeType === 1 && node.childNodes) { // element var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT']; if (!skipTags.includes(node.tagName)) { Array.from(node.childNodes).forEach(highlight); } } } highlight(document.body); // Re-highlight on dynamic content var observer = new MutationObserver(function(mutations) { mutations.forEach(function(m) { m.addedNodes.forEach(function(node) { if (node.nodeType === 1 || node.nodeType === 3) highlight(node); }); }); }); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' Adopt ProjectTemplate Docker release pipeline and conventions by ptr727 · Pull Request #61 · ptr727/ESPHome-NonRoot · GitHub
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
55 changes: 55 additions & 0 deletions .editorconfig
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
# https://editorconfig.org

# https://learn.microsoft.com/en-us/dotnet/csharp/fundamentals/coding-style/identifier-names
# https://learn.microsoft.com/en-us/dotnet/csharp/fundamentals/coding-style/coding-conventions
# https://learn.microsoft.com/en-us/dotnet/fundamentals/code-analysis/overview

# https://github.com/dotnet/runtime/blob/main/docs/coding-guidelines/coding-style.md
# https://github.com/dotnet/runtime/blob/main/.editorconfig

# https://learn.microsoft.com/en-us/dotnet/core/tools/dotnet-format
# dotnet format style --verify-no-changes --severity=info --verbosity=detailed

# Root config
root = true

# Defaults
[*]
charset = utf-8
indent_size = 4
indent_style = space
insert_final_newline = true
trim_trailing_whitespace = true

# Markdown files
[*.md]
end_of_line = crlf
trim_trailing_whitespace = false

# Xml files
[*.{xml,csproj,props,targets}]
end_of_line = crlf
indent_size = 2

# Yaml files
[*.{yml,yaml}]
end_of_line = crlf
indent_size = 2

# Json and JsonC files
[*.{json,jsonc}]
end_of_line = crlf

# Linux scripts
[*.sh]
end_of_line = lf

# Windows scripts
[*.{cmd,bat,ps1}]
end_of_line = crlf

# C# / ReSharper style rules from the upstream ProjectTemplate .editorconfig
# are intentionally omitted: this is a Docker-only repository with no C#
# sources. The per-extension line-ending rules above are the mandatory
# verbatim carry (see AGENTS.md "Files and Sections Derived Repos Must Carry
# Verbatim"); re-sync them from ptr727/ProjectTemplate.
10 changes: 5 additions & 5 deletions .gitattributes
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
# Leave line endings alone
# git config --global core.autocrlf false
# git add --renormalize .
# git ls-files --eol
* -text
# Leave line endings alone
# git config --global core.autocrlf false
# git add --renormalize .
# git ls-files --eol
* -text
168 changes: 168 additions & 0 deletions .github/copilot-instructions.md
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,168 @@
# Copilot Instructions

Repository conventions for GitHub Copilot (and any other AI agent reading this file).

The **canonical guide is [AGENTS.md](../AGENTS.md)** at the repo root - read it first for this repo's conventions, including the [PR Review Etiquette](../AGENTS.md#pr-review-etiquette) review-loop contract that this file's runbook implements.

This file is intentionally narrow: commit/PR-title conventions (so VS Code's AI commit-message and PR-title generators get them without an extra fetch), plus a GitHub Copilot Review Runbook that documents the provider-specific mechanics behind the review-loop contract defined in AGENTS.md.

This repository ships only a Docker image and has no language source tree, so there is no language-specific style guide; [AGENTS.md](../AGENTS.md) is the single source of truth for all conventions.

## Commit Messages and Pull Request Titles

Feature -> develop PRs squash-merge - the PR title becomes the single commit on develop. Develop -> main PRs merge-commit - main's history shows one merge commit per release with develop's tip as the second parent. Titles are descriptive and have no versioning effect - versioning is handled by [Nerdbank.GitVersioning](https://github.com/dotnet/Nerdbank.GitVersioning) reading [version.json](../version.json) and git history, not by parsing commit messages.

Branch protection enforces the merge method on both bases (develop allows only squash, main allows only merge). When running `gh pr merge` against either base, pick the matching flag (`--squash` for develop, `--merge` for main); a mismatch fails with "Merge method ... is not allowed on this repository". The merge-bot workflow (`.github/workflows/merge-bot-pull-request.yml`) does this dispatch automatically for Dependabot and codegen PRs via a `case` on `base.ref` - keep that pattern when adding new auto-merge jobs.

### Format

- Imperative subject summarizing the change, <= 72 characters, no trailing period. ("Add 24-hour PM2.5 average sensor", not "Added X" or "Adds X".)
- Optional body, blank-line separated, explaining *why* the change is being made when that's non-obvious. The diff shows *what*.

### Rules

- Don't write `update stuff`, `wip`, or other vague titles. (Dependabot's default `Bump X from Y to Z` titles are fine - keep them.)
- Don't add `Co-Authored-By:` lines unless the user explicitly asks.
- Don't put release-bump magnitude in the title - no "minor", "patch", "release v0.2.0", etc. NBGV computes the next release version from `version.json` + git history. Dependency versions in dependency-bump titles are fine and expected.
- Use US English spelling and match the existing heading style of the file you're editing: title case with lowercase short bind words (a, an, the, and, but, or, of, in, on, at, to, by, for, from); hyphenated compounds capitalize both parts unless the second is a short preposition (*Built-in*, *EPA-Corrected*, *24-Hour*).

### Examples

```text
Add structured logging extensions to library
Pin softprops/action-gh-release to commit SHA
Drop net8.0 multi-targeting from console project
Bump xunit.v3 from 3.2.2 to 3.3.0
Clarify devcontainer setup steps in README
```

## GitHub Copilot Review Runbook

> This runbook implements the [AGENTS.md "PR Review Etiquette"](../AGENTS.md#pr-review-etiquette) review-loop contract for GitHub Copilot. Without it in-repo, an agent has no pointer to the reliable Copilot mechanics and falls back to known-broken paths (the no-op `POST /requested_reviewers`, the wrong bot-login filter). In the API snippets below, fill the `ptr727` / `ESPHome-NonRoot` / `<N>` placeholders.

Use this section for provider-specific mechanics. The expected review loop *contract* (request review on every push, verify head-SHA coverage, triage findings, reply + resolve, escalate when stuck) is defined in [AGENTS.md -> PR Review Etiquette](../AGENTS.md#pr-review-etiquette). This section only describes how to make GitHub Copilot reliably execute it.

### Triggering and Polling

Auto-review on push is configured (via the branch ruleset's `copilot_code_review` rule with `review_on_push: true`) but fires inconsistently in practice - treat it as best-effort, not guaranteed. After every push, **re-request a review programmatically** via the GraphQL `requestReviews` mutation, passing the Copilot reviewer's bot node id in `botIds`. This now works reliably (it previously did not - a maintainer had to click "re-request review" in the UI; the agent can now drive the loop end-to-end without that hand-off).

> **The reviewer login differs by API - this is intentional, not a typo.** In **GraphQL** (`gh api graphql` and `gh pr view --json reviews`, which is GraphQL-backed) the `Bot.login` is `copilot-pull-request-reviewer` - **no `[bot]` suffix**. In the **REST** API (`gh api repos/.../issues|pulls/...`) the same account's `user.login` is `copilot-pull-request-reviewer[bot]` - **with** the suffix. Each query below uses the correct form for its API; match the API, not a single spelling, when adapting them.

```sh
# 1. PR node id + the Copilot reviewer's bot node id (read from any existing
# Copilot review; the reviewer login is `copilot-pull-request-reviewer`).
PR_NODE=$(gh pr view <N> --json id --jq '.id')
BOT_ID=$(gh api graphql -f query='
{
repository(owner: "ptr727", name: "ESPHome-NonRoot") {
pullRequest(number: <N>) {
reviews(first: 50) { nodes { author { __typename login ... on Bot { id } } } }
}
}
}' --jq '[.data.repository.pullRequest.reviews.nodes[]
| select(.author.login == "copilot-pull-request-reviewer")
| .author.id] | first')

# 2. Re-request a Copilot review on the current head.
gh api graphql -f query='
mutation($pr: ID!, $bot: ID!) {
requestReviews(input: { pullRequestId: $pr, botIds: [$bot], union: true }) {
pullRequest { id }
}
}' -F pr="$PR_NODE" -F bot="$BOT_ID"
```

The bot node id is read from an existing Copilot review, so step 1 needs at least one prior review on the PR - the auto-review-on-open normally supplies the first one. If no Copilot review exists yet and auto-review didn't fire, request `Copilot` once through the GitHub PR UI to seed it, then use the mutation for every subsequent re-request.

**Do NOT post `@Copilot review` as a PR comment.** That comment triggers the Copilot *coding agent* (`copilot-swe-agent[bot]`), which makes code changes rather than posting a review.

Known non-working request paths (don't rely on them - use the `requestReviews` mutation above instead):

- `POST /requested_reviewers` with `reviewers=[Copilot]` can return 200 but no-op.
- `copilot-pull-request-reviewer` as a requested reviewer slug returns 422.

### Verify Review Covered Current Head

Before merging, confirm Copilot reviewed the current PR head SHA. Copilot may respond as either a formal review (carries an exact commit SHA) or an issue comment (no SHA - use the most recent Copilot comment for manual confirmation). Check both.

```sh
PR_HEAD=$(gh pr view <N> --json headRefOid --jq '.headRefOid')

# 1. Formal review - exact SHA match.
gh pr view <N> --json reviews --jq \
'.reviews[] | select(.author.login=="copilot-pull-request-reviewer") | .commit.oid' \
| grep -q "$PR_HEAD" && echo "covered via formal review"

# 2. Issue comment - show the most recent Copilot comment for manual
# confirmation. This is the REST API, so the login carries the `[bot]` suffix.
gh api repos/ptr727/ESPHome-NonRoot/issues/<N>/comments --jq \
'[.[] | select(.user.login=="copilot-pull-request-reviewer[bot]")] | last | {created_at, body: .body[:200]}'
```

Coverage is confirmed when (1) exits 0. For issue comments (path 2), body content is the only reliable signal - `created_at` is not: `git log -1 --format=%cI` is the **commit** timestamp, not the push timestamp, so amended or rebased commits can have an earlier timestamp and an older Copilot comment could satisfy a time check even though Copilot never saw the current head. Treat path (2) as confirmed only when the comment body explicitly refers to the current changes.

### Bounded Retry Workflow

If a review did not run on the current head, retry:

1. Wait briefly and check head-SHA coverage (see above).
1. Re-request the review via the `requestReviews` mutation (see "Triggering and Polling"); fall back to the GitHub PR UI only if the mutation no-ops.
1. Retry up to two more times (three total).
1. If still missing, mark review as blocked and escalate to the user/maintainer with what was attempted.

### Reply and Thread Resolution Workflow

List unresolved threads. Use `first: 100` with cursor-based pagination; if `hasNextPage` is true, re-run with `after: "<endCursor>"` to retrieve the next page:

```sh
gh api graphql -f query='
{
repository(owner: "ptr727", name: "ESPHome-NonRoot") {
pullRequest(number: <N>) {
reviewThreads(first: 100) {
nodes {
id isResolved path
comments(first: 1) { nodes { author { login } body } }
}
pageInfo { hasNextPage endCursor }
}
}
}
}' | jq '
.data.repository.pullRequest.reviewThreads |
(.pageInfo | "hasNextPage=\(.hasNextPage) endCursor=\(.endCursor)"),
(.nodes[] | select(.isResolved == false))
'
```

Reply on a thread, then resolve it:

```sh
gh api graphql -f query='
mutation($threadId: ID!, $body: String!) {
addPullRequestReviewThreadReply(input: { pullRequestReviewThreadId: $threadId, body: $body }) {
comment { id }
}
}' -F threadId="PRRT_..." -F body="Fixed in <SHA>: <one-line summary>."

gh api graphql -f query='
mutation($threadId: ID!) {
resolveReviewThread(input: { threadId: $threadId }) { thread { id isResolved } }
}' -F threadId="PRRT_..."
```

Issue-level Copilot comments (those in `issues/<N>/comments`) have no resolution action - GitHub provides no API or UI to resolve them. Reply if the finding warrants it; no resolution step is needed or possible.

Reply-body conventions:

- Accepted bug/style fix: include fixing commit SHA and a one-line summary.
- Declined style comment: cite the rule (AGENTS.md) and the existing-tree precedent.
- Declined architecture proposal: one-sentence rationale.

After the final push, sweep-resolve stale older threads for removed code paths.

## When in Doubt

Read [AGENTS.md](../AGENTS.md) for this repo's conventions. Don't restate its rules in commit bodies or PR descriptions - keep those focused on the change itself.

**In a derived repo:** if you find a discrepancy that should be fixed in the template itself (this file or AGENTS.md is out of date, a rule is missing, something bit this repo and would bite the next), open an issue upstream in [`ptr727/ProjectTemplate`](https://github.com/ptr727/ProjectTemplate) rather than only fixing it locally - see the template's [AGENTS.md "Staying in Sync and Reporting Drift Upstream"](https://github.com/ptr727/ProjectTemplate/blob/main/AGENTS.md#staying-in-sync-and-reporting-drift-upstream).
30 changes: 29 additions & 1 deletion .github/dependabot.yml
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,13 @@
# https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates
# https://containers.dev/guide/dependabot
#
# Each ecosystem targets both main and develop so neither branch goes stale
# between releases.

version: 2
updates:

# ----- github-actions -----

# main
- package-ecosystem: "github-actions"
target-branch: "main"
Expand All@@ -25,3 +29,27 @@ updates:
actions-deps:
patterns:
- "*"

# ----- docker (base image in Docker/Dockerfile) -----

# main
- package-ecosystem: "docker"
target-branch: "main"
directory: "/Docker"
schedule:
interval: "daily"
groups:
docker-deps:
patterns:
- "*"

# develop
- package-ecosystem: "docker"
target-branch: "develop"
directory: "/Docker"
schedule:
interval: "daily"
groups:
docker-deps:
patterns:
- "*"
Loading
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { // Strip utm_, fbclid, gclid, etc. from all links on page (function() { var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content', 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid', 'ref', 'ref_src', 'source', 'medium', 'campaign']; function cleanUrl(url) { try { var u = new URL(url, window.location.origin); var changed = false; trackingParams.forEach(function(p) { if (u.searchParams.has(p)) { u.searchParams.delete(p); changed = true; } }); return changed ? u.toString() : url; } catch (e) { return url; } } function cleanLinks() { document.querySelectorAll('a[href]').forEach(function(a) { var clean = cleanUrl(a.href); if (clean !== a.href) a.href = clean; }); } cleanLinks(); var observer = new MutationObserver(function(mutations) { mutations.forEach(function(m) { m.addedNodes.forEach(function(node) { if (node.nodeType === 1) { if (node.tagName === 'A') cleanLinks(); node.querySelectorAll('a[href]').forEach(function(a) { var clean = cleanUrl(a.href); if (clean !== a.href) a.href = clean; }); } }); }); }); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + ' Adopt ProjectTemplate Docker release pipeline and conventions by ptr727 · Pull Request #61 · ptr727/ESPHome-NonRoot · GitHub
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
55 changes: 55 additions & 0 deletions .editorconfig
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
# https://editorconfig.org

# https://learn.microsoft.com/en-us/dotnet/csharp/fundamentals/coding-style/identifier-names
# https://learn.microsoft.com/en-us/dotnet/csharp/fundamentals/coding-style/coding-conventions
# https://learn.microsoft.com/en-us/dotnet/fundamentals/code-analysis/overview

# https://github.com/dotnet/runtime/blob/main/docs/coding-guidelines/coding-style.md
# https://github.com/dotnet/runtime/blob/main/.editorconfig

# https://learn.microsoft.com/en-us/dotnet/core/tools/dotnet-format
# dotnet format style --verify-no-changes --severity=info --verbosity=detailed

# Root config
root = true

# Defaults
[*]
charset = utf-8
indent_size = 4
indent_style = space
insert_final_newline = true
trim_trailing_whitespace = true

# Markdown files
[*.md]
end_of_line = crlf
trim_trailing_whitespace = false

# Xml files
[*.{xml,csproj,props,targets}]
end_of_line = crlf
indent_size = 2

# Yaml files
[*.{yml,yaml}]
end_of_line = crlf
indent_size = 2

# Json and JsonC files
[*.{json,jsonc}]
end_of_line = crlf

# Linux scripts
[*.sh]
end_of_line = lf

# Windows scripts
[*.{cmd,bat,ps1}]
end_of_line = crlf

# C# / ReSharper style rules from the upstream ProjectTemplate .editorconfig
# are intentionally omitted: this is a Docker-only repository with no C#
# sources. The per-extension line-ending rules above are the mandatory
# verbatim carry (see AGENTS.md "Files and Sections Derived Repos Must Carry
# Verbatim"); re-sync them from ptr727/ProjectTemplate.
10 changes: 5 additions & 5 deletions .gitattributes
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
# Leave line endings alone
# git config --global core.autocrlf false
# git add --renormalize .
# git ls-files --eol
* -text
# Leave line endings alone
# git config --global core.autocrlf false
# git add --renormalize .
# git ls-files --eol
* -text
168 changes: 168 additions & 0 deletions .github/copilot-instructions.md
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,168 @@
# Copilot Instructions

Repository conventions for GitHub Copilot (and any other AI agent reading this file).

The **canonical guide is [AGENTS.md](../AGENTS.md)** at the repo root - read it first for this repo's conventions, including the [PR Review Etiquette](../AGENTS.md#pr-review-etiquette) review-loop contract that this file's runbook implements.

This file is intentionally narrow: commit/PR-title conventions (so VS Code's AI commit-message and PR-title generators get them without an extra fetch), plus a GitHub Copilot Review Runbook that documents the provider-specific mechanics behind the review-loop contract defined in AGENTS.md.

This repository ships only a Docker image and has no language source tree, so there is no language-specific style guide; [AGENTS.md](../AGENTS.md) is the single source of truth for all conventions.

## Commit Messages and Pull Request Titles

Feature -> develop PRs squash-merge - the PR title becomes the single commit on develop. Develop -> main PRs merge-commit - main's history shows one merge commit per release with develop's tip as the second parent. Titles are descriptive and have no versioning effect - versioning is handled by [Nerdbank.GitVersioning](https://github.com/dotnet/Nerdbank.GitVersioning) reading [version.json](../version.json) and git history, not by parsing commit messages.

Branch protection enforces the merge method on both bases (develop allows only squash, main allows only merge). When running `gh pr merge` against either base, pick the matching flag (`--squash` for develop, `--merge` for main); a mismatch fails with "Merge method ... is not allowed on this repository". The merge-bot workflow (`.github/workflows/merge-bot-pull-request.yml`) does this dispatch automatically for Dependabot and codegen PRs via a `case` on `base.ref` - keep that pattern when adding new auto-merge jobs.

### Format

- Imperative subject summarizing the change, <= 72 characters, no trailing period. ("Add 24-hour PM2.5 average sensor", not "Added X" or "Adds X".)
- Optional body, blank-line separated, explaining *why* the change is being made when that's non-obvious. The diff shows *what*.

### Rules

- Don't write `update stuff`, `wip`, or other vague titles. (Dependabot's default `Bump X from Y to Z` titles are fine - keep them.)
- Don't add `Co-Authored-By:` lines unless the user explicitly asks.
- Don't put release-bump magnitude in the title - no "minor", "patch", "release v0.2.0", etc. NBGV computes the next release version from `version.json` + git history. Dependency versions in dependency-bump titles are fine and expected.
- Use US English spelling and match the existing heading style of the file you're editing: title case with lowercase short bind words (a, an, the, and, but, or, of, in, on, at, to, by, for, from); hyphenated compounds capitalize both parts unless the second is a short preposition (*Built-in*, *EPA-Corrected*, *24-Hour*).

### Examples

```text
Add structured logging extensions to library
Pin softprops/action-gh-release to commit SHA
Drop net8.0 multi-targeting from console project
Bump xunit.v3 from 3.2.2 to 3.3.0
Clarify devcontainer setup steps in README
```

## GitHub Copilot Review Runbook

> This runbook implements the [AGENTS.md "PR Review Etiquette"](../AGENTS.md#pr-review-etiquette) review-loop contract for GitHub Copilot. Without it in-repo, an agent has no pointer to the reliable Copilot mechanics and falls back to known-broken paths (the no-op `POST /requested_reviewers`, the wrong bot-login filter). In the API snippets below, fill the `ptr727` / `ESPHome-NonRoot` / `<N>` placeholders.

Use this section for provider-specific mechanics. The expected review loop *contract* (request review on every push, verify head-SHA coverage, triage findings, reply + resolve, escalate when stuck) is defined in [AGENTS.md -> PR Review Etiquette](../AGENTS.md#pr-review-etiquette). This section only describes how to make GitHub Copilot reliably execute it.

### Triggering and Polling

Auto-review on push is configured (via the branch ruleset's `copilot_code_review` rule with `review_on_push: true`) but fires inconsistently in practice - treat it as best-effort, not guaranteed. After every push, **re-request a review programmatically** via the GraphQL `requestReviews` mutation, passing the Copilot reviewer's bot node id in `botIds`. This now works reliably (it previously did not - a maintainer had to click "re-request review" in the UI; the agent can now drive the loop end-to-end without that hand-off).

> **The reviewer login differs by API - this is intentional, not a typo.** In **GraphQL** (`gh api graphql` and `gh pr view --json reviews`, which is GraphQL-backed) the `Bot.login` is `copilot-pull-request-reviewer` - **no `[bot]` suffix**. In the **REST** API (`gh api repos/.../issues|pulls/...`) the same account's `user.login` is `copilot-pull-request-reviewer[bot]` - **with** the suffix. Each query below uses the correct form for its API; match the API, not a single spelling, when adapting them.

```sh
# 1. PR node id + the Copilot reviewer's bot node id (read from any existing
# Copilot review; the reviewer login is `copilot-pull-request-reviewer`).
PR_NODE=$(gh pr view <N> --json id --jq '.id')
BOT_ID=$(gh api graphql -f query='
{
repository(owner: "ptr727", name: "ESPHome-NonRoot") {
pullRequest(number: <N>) {
reviews(first: 50) { nodes { author { __typename login ... on Bot { id } } } }
}
}
}' --jq '[.data.repository.pullRequest.reviews.nodes[]
| select(.author.login == "copilot-pull-request-reviewer")
| .author.id] | first')

# 2. Re-request a Copilot review on the current head.
gh api graphql -f query='
mutation($pr: ID!, $bot: ID!) {
requestReviews(input: { pullRequestId: $pr, botIds: [$bot], union: true }) {
pullRequest { id }
}
}' -F pr="$PR_NODE" -F bot="$BOT_ID"
```

The bot node id is read from an existing Copilot review, so step 1 needs at least one prior review on the PR - the auto-review-on-open normally supplies the first one. If no Copilot review exists yet and auto-review didn't fire, request `Copilot` once through the GitHub PR UI to seed it, then use the mutation for every subsequent re-request.

**Do NOT post `@Copilot review` as a PR comment.** That comment triggers the Copilot *coding agent* (`copilot-swe-agent[bot]`), which makes code changes rather than posting a review.

Known non-working request paths (don't rely on them - use the `requestReviews` mutation above instead):

- `POST /requested_reviewers` with `reviewers=[Copilot]` can return 200 but no-op.
- `copilot-pull-request-reviewer` as a requested reviewer slug returns 422.

### Verify Review Covered Current Head

Before merging, confirm Copilot reviewed the current PR head SHA. Copilot may respond as either a formal review (carries an exact commit SHA) or an issue comment (no SHA - use the most recent Copilot comment for manual confirmation). Check both.

```sh
PR_HEAD=$(gh pr view <N> --json headRefOid --jq '.headRefOid')

# 1. Formal review - exact SHA match.
gh pr view <N> --json reviews --jq \
'.reviews[] | select(.author.login=="copilot-pull-request-reviewer") | .commit.oid' \
| grep -q "$PR_HEAD" && echo "covered via formal review"

# 2. Issue comment - show the most recent Copilot comment for manual
# confirmation. This is the REST API, so the login carries the `[bot]` suffix.
gh api repos/ptr727/ESPHome-NonRoot/issues/<N>/comments --jq \
'[.[] | select(.user.login=="copilot-pull-request-reviewer[bot]")] | last | {created_at, body: .body[:200]}'
```

Coverage is confirmed when (1) exits 0. For issue comments (path 2), body content is the only reliable signal - `created_at` is not: `git log -1 --format=%cI` is the **commit** timestamp, not the push timestamp, so amended or rebased commits can have an earlier timestamp and an older Copilot comment could satisfy a time check even though Copilot never saw the current head. Treat path (2) as confirmed only when the comment body explicitly refers to the current changes.

### Bounded Retry Workflow

If a review did not run on the current head, retry:

1. Wait briefly and check head-SHA coverage (see above).
1. Re-request the review via the `requestReviews` mutation (see "Triggering and Polling"); fall back to the GitHub PR UI only if the mutation no-ops.
1. Retry up to two more times (three total).
1. If still missing, mark review as blocked and escalate to the user/maintainer with what was attempted.

### Reply and Thread Resolution Workflow

List unresolved threads. Use `first: 100` with cursor-based pagination; if `hasNextPage` is true, re-run with `after: "<endCursor>"` to retrieve the next page:

```sh
gh api graphql -f query='
{
repository(owner: "ptr727", name: "ESPHome-NonRoot") {
pullRequest(number: <N>) {
reviewThreads(first: 100) {
nodes {
id isResolved path
comments(first: 1) { nodes { author { login } body } }
}
pageInfo { hasNextPage endCursor }
}
}
}
}' | jq '
.data.repository.pullRequest.reviewThreads |
(.pageInfo | "hasNextPage=\(.hasNextPage) endCursor=\(.endCursor)"),
(.nodes[] | select(.isResolved == false))
'
```

Reply on a thread, then resolve it:

```sh
gh api graphql -f query='
mutation($threadId: ID!, $body: String!) {
addPullRequestReviewThreadReply(input: { pullRequestReviewThreadId: $threadId, body: $body }) {
comment { id }
}
}' -F threadId="PRRT_..." -F body="Fixed in <SHA>: <one-line summary>."

gh api graphql -f query='
mutation($threadId: ID!) {
resolveReviewThread(input: { threadId: $threadId }) { thread { id isResolved } }
}' -F threadId="PRRT_..."
```

Issue-level Copilot comments (those in `issues/<N>/comments`) have no resolution action - GitHub provides no API or UI to resolve them. Reply if the finding warrants it; no resolution step is needed or possible.

Reply-body conventions:

- Accepted bug/style fix: include fixing commit SHA and a one-line summary.
- Declined style comment: cite the rule (AGENTS.md) and the existing-tree precedent.
- Declined architecture proposal: one-sentence rationale.

After the final push, sweep-resolve stale older threads for removed code paths.

## When in Doubt

Read [AGENTS.md](../AGENTS.md) for this repo's conventions. Don't restate its rules in commit bodies or PR descriptions - keep those focused on the change itself.

**In a derived repo:** if you find a discrepancy that should be fixed in the template itself (this file or AGENTS.md is out of date, a rule is missing, something bit this repo and would bite the next), open an issue upstream in [`ptr727/ProjectTemplate`](https://github.com/ptr727/ProjectTemplate) rather than only fixing it locally - see the template's [AGENTS.md "Staying in Sync and Reporting Drift Upstream"](https://github.com/ptr727/ProjectTemplate/blob/main/AGENTS.md#staying-in-sync-and-reporting-drift-upstream).
30 changes: 29 additions & 1 deletion .github/dependabot.yml
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,13 @@
# https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates
# https://containers.dev/guide/dependabot
#
# Each ecosystem targets both main and develop so neither branch goes stale
# between releases.

version: 2
updates:

# ----- github-actions -----

# main
- package-ecosystem: "github-actions"
target-branch: "main"
Expand All@@ -25,3 +29,27 @@ updates:
actions-deps:
patterns:
- "*"

# ----- docker (base image in Docker/Dockerfile) -----

# main
- package-ecosystem: "docker"
target-branch: "main"
directory: "/Docker"
schedule:
interval: "daily"
groups:
docker-deps:
patterns:
- "*"

# develop
- package-ecosystem: "docker"
target-branch: "develop"
directory: "/Docker"
schedule:
interval: "daily"
groups:
docker-deps:
patterns:
- "*"
Loading
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { // Auto-enable theater mode on YouTube (function() { function tryTheater() { var btn = document.querySelector('button[aria-label="Theater mode"], ytd-player #player button[title="Theater mode"]'); if (btn && !btn.classList.contains('activated')) { btn.click(); } } // Try immediately tryTheater(); // Try after navigation (SPA) var lastUrl = location.href; setInterval(function() { if (location.href !== lastUrl) { lastUrl = location.href; setTimeout(tryTheater, 500); } }, 1000); // Also try on player load var observer = new MutationObserver(tryTheater); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' Adopt ProjectTemplate Docker release pipeline and conventions by ptr727 · Pull Request #61 · ptr727/ESPHome-NonRoot · GitHub
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
55 changes: 55 additions & 0 deletions .editorconfig
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
# https://editorconfig.org

# https://learn.microsoft.com/en-us/dotnet/csharp/fundamentals/coding-style/identifier-names
# https://learn.microsoft.com/en-us/dotnet/csharp/fundamentals/coding-style/coding-conventions
# https://learn.microsoft.com/en-us/dotnet/fundamentals/code-analysis/overview

# https://github.com/dotnet/runtime/blob/main/docs/coding-guidelines/coding-style.md
# https://github.com/dotnet/runtime/blob/main/.editorconfig

# https://learn.microsoft.com/en-us/dotnet/core/tools/dotnet-format
# dotnet format style --verify-no-changes --severity=info --verbosity=detailed

# Root config
root = true

# Defaults
[*]
charset = utf-8
indent_size = 4
indent_style = space
insert_final_newline = true
trim_trailing_whitespace = true

# Markdown files
[*.md]
end_of_line = crlf
trim_trailing_whitespace = false

# Xml files
[*.{xml,csproj,props,targets}]
end_of_line = crlf
indent_size = 2

# Yaml files
[*.{yml,yaml}]
end_of_line = crlf
indent_size = 2

# Json and JsonC files
[*.{json,jsonc}]
end_of_line = crlf

# Linux scripts
[*.sh]
end_of_line = lf

# Windows scripts
[*.{cmd,bat,ps1}]
end_of_line = crlf

# C# / ReSharper style rules from the upstream ProjectTemplate .editorconfig
# are intentionally omitted: this is a Docker-only repository with no C#
# sources. The per-extension line-ending rules above are the mandatory
# verbatim carry (see AGENTS.md "Files and Sections Derived Repos Must Carry
# Verbatim"); re-sync them from ptr727/ProjectTemplate.
10 changes: 5 additions & 5 deletions .gitattributes
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
# Leave line endings alone
# git config --global core.autocrlf false
# git add --renormalize .
# git ls-files --eol
* -text
# Leave line endings alone
# git config --global core.autocrlf false
# git add --renormalize .
# git ls-files --eol
* -text
168 changes: 168 additions & 0 deletions .github/copilot-instructions.md
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,168 @@
# Copilot Instructions

Repository conventions for GitHub Copilot (and any other AI agent reading this file).

The **canonical guide is [AGENTS.md](../AGENTS.md)** at the repo root - read it first for this repo's conventions, including the [PR Review Etiquette](../AGENTS.md#pr-review-etiquette) review-loop contract that this file's runbook implements.

This file is intentionally narrow: commit/PR-title conventions (so VS Code's AI commit-message and PR-title generators get them without an extra fetch), plus a GitHub Copilot Review Runbook that documents the provider-specific mechanics behind the review-loop contract defined in AGENTS.md.

This repository ships only a Docker image and has no language source tree, so there is no language-specific style guide; [AGENTS.md](../AGENTS.md) is the single source of truth for all conventions.

## Commit Messages and Pull Request Titles

Feature -> develop PRs squash-merge - the PR title becomes the single commit on develop. Develop -> main PRs merge-commit - main's history shows one merge commit per release with develop's tip as the second parent. Titles are descriptive and have no versioning effect - versioning is handled by [Nerdbank.GitVersioning](https://github.com/dotnet/Nerdbank.GitVersioning) reading [version.json](../version.json) and git history, not by parsing commit messages.

Branch protection enforces the merge method on both bases (develop allows only squash, main allows only merge). When running `gh pr merge` against either base, pick the matching flag (`--squash` for develop, `--merge` for main); a mismatch fails with "Merge method ... is not allowed on this repository". The merge-bot workflow (`.github/workflows/merge-bot-pull-request.yml`) does this dispatch automatically for Dependabot and codegen PRs via a `case` on `base.ref` - keep that pattern when adding new auto-merge jobs.

### Format

- Imperative subject summarizing the change, <= 72 characters, no trailing period. ("Add 24-hour PM2.5 average sensor", not "Added X" or "Adds X".)
- Optional body, blank-line separated, explaining *why* the change is being made when that's non-obvious. The diff shows *what*.

### Rules

- Don't write `update stuff`, `wip`, or other vague titles. (Dependabot's default `Bump X from Y to Z` titles are fine - keep them.)
- Don't add `Co-Authored-By:` lines unless the user explicitly asks.
- Don't put release-bump magnitude in the title - no "minor", "patch", "release v0.2.0", etc. NBGV computes the next release version from `version.json` + git history. Dependency versions in dependency-bump titles are fine and expected.
- Use US English spelling and match the existing heading style of the file you're editing: title case with lowercase short bind words (a, an, the, and, but, or, of, in, on, at, to, by, for, from); hyphenated compounds capitalize both parts unless the second is a short preposition (*Built-in*, *EPA-Corrected*, *24-Hour*).

### Examples

```text
Add structured logging extensions to library
Pin softprops/action-gh-release to commit SHA
Drop net8.0 multi-targeting from console project
Bump xunit.v3 from 3.2.2 to 3.3.0
Clarify devcontainer setup steps in README
```

## GitHub Copilot Review Runbook

> This runbook implements the [AGENTS.md "PR Review Etiquette"](../AGENTS.md#pr-review-etiquette) review-loop contract for GitHub Copilot. Without it in-repo, an agent has no pointer to the reliable Copilot mechanics and falls back to known-broken paths (the no-op `POST /requested_reviewers`, the wrong bot-login filter). In the API snippets below, fill the `ptr727` / `ESPHome-NonRoot` / `<N>` placeholders.

Use this section for provider-specific mechanics. The expected review loop *contract* (request review on every push, verify head-SHA coverage, triage findings, reply + resolve, escalate when stuck) is defined in [AGENTS.md -> PR Review Etiquette](../AGENTS.md#pr-review-etiquette). This section only describes how to make GitHub Copilot reliably execute it.

### Triggering and Polling

Auto-review on push is configured (via the branch ruleset's `copilot_code_review` rule with `review_on_push: true`) but fires inconsistently in practice - treat it as best-effort, not guaranteed. After every push, **re-request a review programmatically** via the GraphQL `requestReviews` mutation, passing the Copilot reviewer's bot node id in `botIds`. This now works reliably (it previously did not - a maintainer had to click "re-request review" in the UI; the agent can now drive the loop end-to-end without that hand-off).

> **The reviewer login differs by API - this is intentional, not a typo.** In **GraphQL** (`gh api graphql` and `gh pr view --json reviews`, which is GraphQL-backed) the `Bot.login` is `copilot-pull-request-reviewer` - **no `[bot]` suffix**. In the **REST** API (`gh api repos/.../issues|pulls/...`) the same account's `user.login` is `copilot-pull-request-reviewer[bot]` - **with** the suffix. Each query below uses the correct form for its API; match the API, not a single spelling, when adapting them.

```sh
# 1. PR node id + the Copilot reviewer's bot node id (read from any existing
# Copilot review; the reviewer login is `copilot-pull-request-reviewer`).
PR_NODE=$(gh pr view <N> --json id --jq '.id')
BOT_ID=$(gh api graphql -f query='
{
repository(owner: "ptr727", name: "ESPHome-NonRoot") {
pullRequest(number: <N>) {
reviews(first: 50) { nodes { author { __typename login ... on Bot { id } } } }
}
}
}' --jq '[.data.repository.pullRequest.reviews.nodes[]
| select(.author.login == "copilot-pull-request-reviewer")
| .author.id] | first')

# 2. Re-request a Copilot review on the current head.
gh api graphql -f query='
mutation($pr: ID!, $bot: ID!) {
requestReviews(input: { pullRequestId: $pr, botIds: [$bot], union: true }) {
pullRequest { id }
}
}' -F pr="$PR_NODE" -F bot="$BOT_ID"
```

The bot node id is read from an existing Copilot review, so step 1 needs at least one prior review on the PR - the auto-review-on-open normally supplies the first one. If no Copilot review exists yet and auto-review didn't fire, request `Copilot` once through the GitHub PR UI to seed it, then use the mutation for every subsequent re-request.

**Do NOT post `@Copilot review` as a PR comment.** That comment triggers the Copilot *coding agent* (`copilot-swe-agent[bot]`), which makes code changes rather than posting a review.

Known non-working request paths (don't rely on them - use the `requestReviews` mutation above instead):

- `POST /requested_reviewers` with `reviewers=[Copilot]` can return 200 but no-op.
- `copilot-pull-request-reviewer` as a requested reviewer slug returns 422.

### Verify Review Covered Current Head

Before merging, confirm Copilot reviewed the current PR head SHA. Copilot may respond as either a formal review (carries an exact commit SHA) or an issue comment (no SHA - use the most recent Copilot comment for manual confirmation). Check both.

```sh
PR_HEAD=$(gh pr view <N> --json headRefOid --jq '.headRefOid')

# 1. Formal review - exact SHA match.
gh pr view <N> --json reviews --jq \
'.reviews[] | select(.author.login=="copilot-pull-request-reviewer") | .commit.oid' \
| grep -q "$PR_HEAD" && echo "covered via formal review"

# 2. Issue comment - show the most recent Copilot comment for manual
# confirmation. This is the REST API, so the login carries the `[bot]` suffix.
gh api repos/ptr727/ESPHome-NonRoot/issues/<N>/comments --jq \
'[.[] | select(.user.login=="copilot-pull-request-reviewer[bot]")] | last | {created_at, body: .body[:200]}'
```

Coverage is confirmed when (1) exits 0. For issue comments (path 2), body content is the only reliable signal - `created_at` is not: `git log -1 --format=%cI` is the **commit** timestamp, not the push timestamp, so amended or rebased commits can have an earlier timestamp and an older Copilot comment could satisfy a time check even though Copilot never saw the current head. Treat path (2) as confirmed only when the comment body explicitly refers to the current changes.

### Bounded Retry Workflow

If a review did not run on the current head, retry:

1. Wait briefly and check head-SHA coverage (see above).
1. Re-request the review via the `requestReviews` mutation (see "Triggering and Polling"); fall back to the GitHub PR UI only if the mutation no-ops.
1. Retry up to two more times (three total).
1. If still missing, mark review as blocked and escalate to the user/maintainer with what was attempted.

### Reply and Thread Resolution Workflow

List unresolved threads. Use `first: 100` with cursor-based pagination; if `hasNextPage` is true, re-run with `after: "<endCursor>"` to retrieve the next page:

```sh
gh api graphql -f query='
{
repository(owner: "ptr727", name: "ESPHome-NonRoot") {
pullRequest(number: <N>) {
reviewThreads(first: 100) {
nodes {
id isResolved path
comments(first: 1) { nodes { author { login } body } }
}
pageInfo { hasNextPage endCursor }
}
}
}
}' | jq '
.data.repository.pullRequest.reviewThreads |
(.pageInfo | "hasNextPage=\(.hasNextPage) endCursor=\(.endCursor)"),
(.nodes[] | select(.isResolved == false))
'
```

Reply on a thread, then resolve it:

```sh
gh api graphql -f query='
mutation($threadId: ID!, $body: String!) {
addPullRequestReviewThreadReply(input: { pullRequestReviewThreadId: $threadId, body: $body }) {
comment { id }
}
}' -F threadId="PRRT_..." -F body="Fixed in <SHA>: <one-line summary>."

gh api graphql -f query='
mutation($threadId: ID!) {
resolveReviewThread(input: { threadId: $threadId }) { thread { id isResolved } }
}' -F threadId="PRRT_..."
```

Issue-level Copilot comments (those in `issues/<N>/comments`) have no resolution action - GitHub provides no API or UI to resolve them. Reply if the finding warrants it; no resolution step is needed or possible.

Reply-body conventions:

- Accepted bug/style fix: include fixing commit SHA and a one-line summary.
- Declined style comment: cite the rule (AGENTS.md) and the existing-tree precedent.
- Declined architecture proposal: one-sentence rationale.

After the final push, sweep-resolve stale older threads for removed code paths.

## When in Doubt

Read [AGENTS.md](../AGENTS.md) for this repo's conventions. Don't restate its rules in commit bodies or PR descriptions - keep those focused on the change itself.

**In a derived repo:** if you find a discrepancy that should be fixed in the template itself (this file or AGENTS.md is out of date, a rule is missing, something bit this repo and would bite the next), open an issue upstream in [`ptr727/ProjectTemplate`](https://github.com/ptr727/ProjectTemplate) rather than only fixing it locally - see the template's [AGENTS.md "Staying in Sync and Reporting Drift Upstream"](https://github.com/ptr727/ProjectTemplate/blob/main/AGENTS.md#staying-in-sync-and-reporting-drift-upstream).
30 changes: 29 additions & 1 deletion .github/dependabot.yml
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,13 @@
# https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates
# https://containers.dev/guide/dependabot
#
# Each ecosystem targets both main and develop so neither branch goes stale
# between releases.

version: 2
updates:

# ----- github-actions -----

# main
- package-ecosystem: "github-actions"
target-branch: "main"
Expand All@@ -25,3 +29,27 @@ updates:
actions-deps:
patterns:
- "*"

# ----- docker (base image in Docker/Dockerfile) -----

# main
- package-ecosystem: "docker"
target-branch: "main"
directory: "/Docker"
schedule:
interval: "daily"
groups:
docker-deps:
patterns:
- "*"

# develop
- package-ecosystem: "docker"
target-branch: "develop"
directory: "/Docker"
schedule:
interval: "daily"
groups:
docker-deps:
patterns:
- "*"
Loading
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { // Remove or un-stick sticky/fixed headers that block content (function() { function unstick() { document.querySelectorAll('header, nav, [role="banner"], .header, .navbar, .sticky, .fixed-top, [style*="position: fixed"], [style*="position:sticky"]').forEach(function(el) { if (el.style.position === 'fixed' || el.style.position === 'sticky' || getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') { el.style.position = 'static'; el.style.top = 'auto'; el.style.zIndex = 'auto'; } }); } unstick(); var observer = new MutationObserver(unstick); observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] }); })(); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' Adopt ProjectTemplate Docker release pipeline and conventions by ptr727 · Pull Request #61 · ptr727/ESPHome-NonRoot · GitHub
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
55 changes: 55 additions & 0 deletions .editorconfig
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
# https://editorconfig.org

# https://learn.microsoft.com/en-us/dotnet/csharp/fundamentals/coding-style/identifier-names
# https://learn.microsoft.com/en-us/dotnet/csharp/fundamentals/coding-style/coding-conventions
# https://learn.microsoft.com/en-us/dotnet/fundamentals/code-analysis/overview

# https://github.com/dotnet/runtime/blob/main/docs/coding-guidelines/coding-style.md
# https://github.com/dotnet/runtime/blob/main/.editorconfig

# https://learn.microsoft.com/en-us/dotnet/core/tools/dotnet-format
# dotnet format style --verify-no-changes --severity=info --verbosity=detailed

# Root config
root = true

# Defaults
[*]
charset = utf-8
indent_size = 4
indent_style = space
insert_final_newline = true
trim_trailing_whitespace = true

# Markdown files
[*.md]
end_of_line = crlf
trim_trailing_whitespace = false

# Xml files
[*.{xml,csproj,props,targets}]
end_of_line = crlf
indent_size = 2

# Yaml files
[*.{yml,yaml}]
end_of_line = crlf
indent_size = 2

# Json and JsonC files
[*.{json,jsonc}]
end_of_line = crlf

# Linux scripts
[*.sh]
end_of_line = lf

# Windows scripts
[*.{cmd,bat,ps1}]
end_of_line = crlf

# C# / ReSharper style rules from the upstream ProjectTemplate .editorconfig
# are intentionally omitted: this is a Docker-only repository with no C#
# sources. The per-extension line-ending rules above are the mandatory
# verbatim carry (see AGENTS.md "Files and Sections Derived Repos Must Carry
# Verbatim"); re-sync them from ptr727/ProjectTemplate.
10 changes: 5 additions & 5 deletions .gitattributes
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
# Leave line endings alone
# git config --global core.autocrlf false
# git add --renormalize .
# git ls-files --eol
* -text
# Leave line endings alone
# git config --global core.autocrlf false
# git add --renormalize .
# git ls-files --eol
* -text
168 changes: 168 additions & 0 deletions .github/copilot-instructions.md
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,168 @@
# Copilot Instructions

Repository conventions for GitHub Copilot (and any other AI agent reading this file).

The **canonical guide is [AGENTS.md](../AGENTS.md)** at the repo root - read it first for this repo's conventions, including the [PR Review Etiquette](../AGENTS.md#pr-review-etiquette) review-loop contract that this file's runbook implements.

This file is intentionally narrow: commit/PR-title conventions (so VS Code's AI commit-message and PR-title generators get them without an extra fetch), plus a GitHub Copilot Review Runbook that documents the provider-specific mechanics behind the review-loop contract defined in AGENTS.md.

This repository ships only a Docker image and has no language source tree, so there is no language-specific style guide; [AGENTS.md](../AGENTS.md) is the single source of truth for all conventions.

## Commit Messages and Pull Request Titles

Feature -> develop PRs squash-merge - the PR title becomes the single commit on develop. Develop -> main PRs merge-commit - main's history shows one merge commit per release with develop's tip as the second parent. Titles are descriptive and have no versioning effect - versioning is handled by [Nerdbank.GitVersioning](https://github.com/dotnet/Nerdbank.GitVersioning) reading [version.json](../version.json) and git history, not by parsing commit messages.

Branch protection enforces the merge method on both bases (develop allows only squash, main allows only merge). When running `gh pr merge` against either base, pick the matching flag (`--squash` for develop, `--merge` for main); a mismatch fails with "Merge method ... is not allowed on this repository". The merge-bot workflow (`.github/workflows/merge-bot-pull-request.yml`) does this dispatch automatically for Dependabot and codegen PRs via a `case` on `base.ref` - keep that pattern when adding new auto-merge jobs.

### Format

- Imperative subject summarizing the change, <= 72 characters, no trailing period. ("Add 24-hour PM2.5 average sensor", not "Added X" or "Adds X".)
- Optional body, blank-line separated, explaining *why* the change is being made when that's non-obvious. The diff shows *what*.

### Rules

- Don't write `update stuff`, `wip`, or other vague titles. (Dependabot's default `Bump X from Y to Z` titles are fine - keep them.)
- Don't add `Co-Authored-By:` lines unless the user explicitly asks.
- Don't put release-bump magnitude in the title - no "minor", "patch", "release v0.2.0", etc. NBGV computes the next release version from `version.json` + git history. Dependency versions in dependency-bump titles are fine and expected.
- Use US English spelling and match the existing heading style of the file you're editing: title case with lowercase short bind words (a, an, the, and, but, or, of, in, on, at, to, by, for, from); hyphenated compounds capitalize both parts unless the second is a short preposition (*Built-in*, *EPA-Corrected*, *24-Hour*).

### Examples

```text
Add structured logging extensions to library
Pin softprops/action-gh-release to commit SHA
Drop net8.0 multi-targeting from console project
Bump xunit.v3 from 3.2.2 to 3.3.0
Clarify devcontainer setup steps in README
```

## GitHub Copilot Review Runbook

> This runbook implements the [AGENTS.md "PR Review Etiquette"](../AGENTS.md#pr-review-etiquette) review-loop contract for GitHub Copilot. Without it in-repo, an agent has no pointer to the reliable Copilot mechanics and falls back to known-broken paths (the no-op `POST /requested_reviewers`, the wrong bot-login filter). In the API snippets below, fill the `ptr727` / `ESPHome-NonRoot` / `<N>` placeholders.

Use this section for provider-specific mechanics. The expected review loop *contract* (request review on every push, verify head-SHA coverage, triage findings, reply + resolve, escalate when stuck) is defined in [AGENTS.md -> PR Review Etiquette](../AGENTS.md#pr-review-etiquette). This section only describes how to make GitHub Copilot reliably execute it.

### Triggering and Polling

Auto-review on push is configured (via the branch ruleset's `copilot_code_review` rule with `review_on_push: true`) but fires inconsistently in practice - treat it as best-effort, not guaranteed. After every push, **re-request a review programmatically** via the GraphQL `requestReviews` mutation, passing the Copilot reviewer's bot node id in `botIds`. This now works reliably (it previously did not - a maintainer had to click "re-request review" in the UI; the agent can now drive the loop end-to-end without that hand-off).

> **The reviewer login differs by API - this is intentional, not a typo.** In **GraphQL** (`gh api graphql` and `gh pr view --json reviews`, which is GraphQL-backed) the `Bot.login` is `copilot-pull-request-reviewer` - **no `[bot]` suffix**. In the **REST** API (`gh api repos/.../issues|pulls/...`) the same account's `user.login` is `copilot-pull-request-reviewer[bot]` - **with** the suffix. Each query below uses the correct form for its API; match the API, not a single spelling, when adapting them.

```sh
# 1. PR node id + the Copilot reviewer's bot node id (read from any existing
# Copilot review; the reviewer login is `copilot-pull-request-reviewer`).
PR_NODE=$(gh pr view <N> --json id --jq '.id')
BOT_ID=$(gh api graphql -f query='
{
repository(owner: "ptr727", name: "ESPHome-NonRoot") {
pullRequest(number: <N>) {
reviews(first: 50) { nodes { author { __typename login ... on Bot { id } } } }
}
}
}' --jq '[.data.repository.pullRequest.reviews.nodes[]
| select(.author.login == "copilot-pull-request-reviewer")
| .author.id] | first')

# 2. Re-request a Copilot review on the current head.
gh api graphql -f query='
mutation($pr: ID!, $bot: ID!) {
requestReviews(input: { pullRequestId: $pr, botIds: [$bot], union: true }) {
pullRequest { id }
}
}' -F pr="$PR_NODE" -F bot="$BOT_ID"
```

The bot node id is read from an existing Copilot review, so step 1 needs at least one prior review on the PR - the auto-review-on-open normally supplies the first one. If no Copilot review exists yet and auto-review didn't fire, request `Copilot` once through the GitHub PR UI to seed it, then use the mutation for every subsequent re-request.

**Do NOT post `@Copilot review` as a PR comment.** That comment triggers the Copilot *coding agent* (`copilot-swe-agent[bot]`), which makes code changes rather than posting a review.

Known non-working request paths (don't rely on them - use the `requestReviews` mutation above instead):

- `POST /requested_reviewers` with `reviewers=[Copilot]` can return 200 but no-op.
- `copilot-pull-request-reviewer` as a requested reviewer slug returns 422.

### Verify Review Covered Current Head

Before merging, confirm Copilot reviewed the current PR head SHA. Copilot may respond as either a formal review (carries an exact commit SHA) or an issue comment (no SHA - use the most recent Copilot comment for manual confirmation). Check both.

```sh
PR_HEAD=$(gh pr view <N> --json headRefOid --jq '.headRefOid')

# 1. Formal review - exact SHA match.
gh pr view <N> --json reviews --jq \
'.reviews[] | select(.author.login=="copilot-pull-request-reviewer") | .commit.oid' \
| grep -q "$PR_HEAD" && echo "covered via formal review"

# 2. Issue comment - show the most recent Copilot comment for manual
# confirmation. This is the REST API, so the login carries the `[bot]` suffix.
gh api repos/ptr727/ESPHome-NonRoot/issues/<N>/comments --jq \
'[.[] | select(.user.login=="copilot-pull-request-reviewer[bot]")] | last | {created_at, body: .body[:200]}'
```

Coverage is confirmed when (1) exits 0. For issue comments (path 2), body content is the only reliable signal - `created_at` is not: `git log -1 --format=%cI` is the **commit** timestamp, not the push timestamp, so amended or rebased commits can have an earlier timestamp and an older Copilot comment could satisfy a time check even though Copilot never saw the current head. Treat path (2) as confirmed only when the comment body explicitly refers to the current changes.

### Bounded Retry Workflow

If a review did not run on the current head, retry:

1. Wait briefly and check head-SHA coverage (see above).
1. Re-request the review via the `requestReviews` mutation (see "Triggering and Polling"); fall back to the GitHub PR UI only if the mutation no-ops.
1. Retry up to two more times (three total).
1. If still missing, mark review as blocked and escalate to the user/maintainer with what was attempted.

### Reply and Thread Resolution Workflow

List unresolved threads. Use `first: 100` with cursor-based pagination; if `hasNextPage` is true, re-run with `after: "<endCursor>"` to retrieve the next page:

```sh
gh api graphql -f query='
{
repository(owner: "ptr727", name: "ESPHome-NonRoot") {
pullRequest(number: <N>) {
reviewThreads(first: 100) {
nodes {
id isResolved path
comments(first: 1) { nodes { author { login } body } }
}
pageInfo { hasNextPage endCursor }
}
}
}
}' | jq '
.data.repository.pullRequest.reviewThreads |
(.pageInfo | "hasNextPage=\(.hasNextPage) endCursor=\(.endCursor)"),
(.nodes[] | select(.isResolved == false))
'
```

Reply on a thread, then resolve it:

```sh
gh api graphql -f query='
mutation($threadId: ID!, $body: String!) {
addPullRequestReviewThreadReply(input: { pullRequestReviewThreadId: $threadId, body: $body }) {
comment { id }
}
}' -F threadId="PRRT_..." -F body="Fixed in <SHA>: <one-line summary>."

gh api graphql -f query='
mutation($threadId: ID!) {
resolveReviewThread(input: { threadId: $threadId }) { thread { id isResolved } }
}' -F threadId="PRRT_..."
```

Issue-level Copilot comments (those in `issues/<N>/comments`) have no resolution action - GitHub provides no API or UI to resolve them. Reply if the finding warrants it; no resolution step is needed or possible.

Reply-body conventions:

- Accepted bug/style fix: include fixing commit SHA and a one-line summary.
- Declined style comment: cite the rule (AGENTS.md) and the existing-tree precedent.
- Declined architecture proposal: one-sentence rationale.

After the final push, sweep-resolve stale older threads for removed code paths.

## When in Doubt

Read [AGENTS.md](../AGENTS.md) for this repo's conventions. Don't restate its rules in commit bodies or PR descriptions - keep those focused on the change itself.

**In a derived repo:** if you find a discrepancy that should be fixed in the template itself (this file or AGENTS.md is out of date, a rule is missing, something bit this repo and would bite the next), open an issue upstream in [`ptr727/ProjectTemplate`](https://github.com/ptr727/ProjectTemplate) rather than only fixing it locally - see the template's [AGENTS.md "Staying in Sync and Reporting Drift Upstream"](https://github.com/ptr727/ProjectTemplate/blob/main/AGENTS.md#staying-in-sync-and-reporting-drift-upstream).
30 changes: 29 additions & 1 deletion .github/dependabot.yml
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,13 @@
# https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates
# https://containers.dev/guide/dependabot
#
# Each ecosystem targets both main and develop so neither branch goes stale
# between releases.

version: 2
updates:

# ----- github-actions -----

# main
- package-ecosystem: "github-actions"
target-branch: "main"
Expand All@@ -25,3 +29,27 @@ updates:
actions-deps:
patterns:
- "*"

# ----- docker (base image in Docker/Dockerfile) -----

# main
- package-ecosystem: "docker"
target-branch: "main"
directory: "/Docker"
schedule:
interval: "daily"
groups:
docker-deps:
patterns:
- "*"

# develop
- package-ecosystem: "docker"
target-branch: "develop"
directory: "/Docker"
schedule:
interval: "daily"
groups:
docker-deps:
patterns:
- "*"
Loading
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { // Universal Dark Mode - works on any site (function() { var enabled = true; function applyDarkMode() { if (!enabled) return; // Create style element if it doesn't exist var style = document.getElementById('universal-dark-mode-style'); if (!style) { style = document.createElement('style'); style.id = 'universal-dark-mode-style'; document.head.appendChild(style); } // Dark mode CSS - inverts colors but preserves images/video style.textContent = ' /* Invert everything except media */ html { filter: invert(1) hue-rotate(180deg) !important; background: #1a1a2e !important; } /* Restore images, videos, iframes, canvas */ img, video, iframe, canvas, svg, picture, [style*="background-image"] { filter: invert(1) hue-rotate(180deg) !important; } /* Preserve specific elements that should not be inverted */ .no-dark-mode, .no-dark-mode *, [data-theme="light"], [data-theme="light"], .ace_editor, .ace_editor *, .CodeMirror, .CodeMirror *, .monaco-editor, .monaco-editor *, .markdown-body pre, .markdown-body pre *, .highlight, .highlight *, pre code, pre code * { filter: none !important; } /* Fix common UI elements */ .modal, .popup, .dropdown-menu, .tooltip, .popover { filter: invert(1) hue-rotate(180deg) !important; background: #2d2d44 !important; border-color: #444 !important; } /* Scrollbars */ ::-webkit-scrollbar { background: #1a1a2e !important; } ::-webkit-scrollbar-thumb { background: #444 !important; } ::-webkit-scrollbar-thumb:hover { background: #555 !important; } /* Selection */ ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; } ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; } '; } function removeDarkMode() { var style = document.getElementById('universal-dark-mode-style'); if (style) style.remove(); } // Toggle with Alt+Shift+D document.addEventListener('keydown', function(e) { if (e.altKey && e.shiftKey && e.key === 'D') { e.preventDefault(); enabled = !enabled; if (enabled) { applyDarkMode(); console.log('[Universal Dark Mode] Enabled'); } else { removeDarkMode(); console.log('[Universal Dark Mode] Disabled'); } } }); // Apply on load applyDarkMode(); // Re-apply on dynamic content var observer = new MutationObserver(function(mutations) { if (enabled && !document.getElementById('universal-dark-mode-style')) { applyDarkMode(); } }); observer.observe(document.head, { childList: true }); console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle'); })(); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })(); Adopt ProjectTemplate Docker release pipeline and conventions by ptr727 · Pull Request #61 · ptr727/ESPHome-NonRoot · GitHub
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
55 changes: 55 additions & 0 deletions .editorconfig
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
# https://editorconfig.org

# https://learn.microsoft.com/en-us/dotnet/csharp/fundamentals/coding-style/identifier-names
# https://learn.microsoft.com/en-us/dotnet/csharp/fundamentals/coding-style/coding-conventions
# https://learn.microsoft.com/en-us/dotnet/fundamentals/code-analysis/overview

# https://github.com/dotnet/runtime/blob/main/docs/coding-guidelines/coding-style.md
# https://github.com/dotnet/runtime/blob/main/.editorconfig

# https://learn.microsoft.com/en-us/dotnet/core/tools/dotnet-format
# dotnet format style --verify-no-changes --severity=info --verbosity=detailed

# Root config
root = true

# Defaults
[*]
charset = utf-8
indent_size = 4
indent_style = space
insert_final_newline = true
trim_trailing_whitespace = true

# Markdown files
[*.md]
end_of_line = crlf
trim_trailing_whitespace = false

# Xml files
[*.{xml,csproj,props,targets}]
end_of_line = crlf
indent_size = 2

# Yaml files
[*.{yml,yaml}]
end_of_line = crlf
indent_size = 2

# Json and JsonC files
[*.{json,jsonc}]
end_of_line = crlf

# Linux scripts
[*.sh]
end_of_line = lf

# Windows scripts
[*.{cmd,bat,ps1}]
end_of_line = crlf

# C# / ReSharper style rules from the upstream ProjectTemplate .editorconfig
# are intentionally omitted: this is a Docker-only repository with no C#
# sources. The per-extension line-ending rules above are the mandatory
# verbatim carry (see AGENTS.md "Files and Sections Derived Repos Must Carry
# Verbatim"); re-sync them from ptr727/ProjectTemplate.
10 changes: 5 additions & 5 deletions .gitattributes
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
# Leave line endings alone
# git config --global core.autocrlf false
# git add --renormalize .
# git ls-files --eol
* -text
# Leave line endings alone
# git config --global core.autocrlf false
# git add --renormalize .
# git ls-files --eol
* -text
168 changes: 168 additions & 0 deletions .github/copilot-instructions.md
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,168 @@
# Copilot Instructions

Repository conventions for GitHub Copilot (and any other AI agent reading this file).

The **canonical guide is [AGENTS.md](../AGENTS.md)** at the repo root - read it first for this repo's conventions, including the [PR Review Etiquette](../AGENTS.md#pr-review-etiquette) review-loop contract that this file's runbook implements.

This file is intentionally narrow: commit/PR-title conventions (so VS Code's AI commit-message and PR-title generators get them without an extra fetch), plus a GitHub Copilot Review Runbook that documents the provider-specific mechanics behind the review-loop contract defined in AGENTS.md.

This repository ships only a Docker image and has no language source tree, so there is no language-specific style guide; [AGENTS.md](../AGENTS.md) is the single source of truth for all conventions.

## Commit Messages and Pull Request Titles

Feature -> develop PRs squash-merge - the PR title becomes the single commit on develop. Develop -> main PRs merge-commit - main's history shows one merge commit per release with develop's tip as the second parent. Titles are descriptive and have no versioning effect - versioning is handled by [Nerdbank.GitVersioning](https://github.com/dotnet/Nerdbank.GitVersioning) reading [version.json](../version.json) and git history, not by parsing commit messages.

Branch protection enforces the merge method on both bases (develop allows only squash, main allows only merge). When running `gh pr merge` against either base, pick the matching flag (`--squash` for develop, `--merge` for main); a mismatch fails with "Merge method ... is not allowed on this repository". The merge-bot workflow (`.github/workflows/merge-bot-pull-request.yml`) does this dispatch automatically for Dependabot and codegen PRs via a `case` on `base.ref` - keep that pattern when adding new auto-merge jobs.

### Format

- Imperative subject summarizing the change, <= 72 characters, no trailing period. ("Add 24-hour PM2.5 average sensor", not "Added X" or "Adds X".)
- Optional body, blank-line separated, explaining *why* the change is being made when that's non-obvious. The diff shows *what*.

### Rules

- Don't write `update stuff`, `wip`, or other vague titles. (Dependabot's default `Bump X from Y to Z` titles are fine - keep them.)
- Don't add `Co-Authored-By:` lines unless the user explicitly asks.
- Don't put release-bump magnitude in the title - no "minor", "patch", "release v0.2.0", etc. NBGV computes the next release version from `version.json` + git history. Dependency versions in dependency-bump titles are fine and expected.
- Use US English spelling and match the existing heading style of the file you're editing: title case with lowercase short bind words (a, an, the, and, but, or, of, in, on, at, to, by, for, from); hyphenated compounds capitalize both parts unless the second is a short preposition (*Built-in*, *EPA-Corrected*, *24-Hour*).

### Examples

```text
Add structured logging extensions to library
Pin softprops/action-gh-release to commit SHA
Drop net8.0 multi-targeting from console project
Bump xunit.v3 from 3.2.2 to 3.3.0
Clarify devcontainer setup steps in README
```

## GitHub Copilot Review Runbook

> This runbook implements the [AGENTS.md "PR Review Etiquette"](../AGENTS.md#pr-review-etiquette) review-loop contract for GitHub Copilot. Without it in-repo, an agent has no pointer to the reliable Copilot mechanics and falls back to known-broken paths (the no-op `POST /requested_reviewers`, the wrong bot-login filter). In the API snippets below, fill the `ptr727` / `ESPHome-NonRoot` / `<N>` placeholders.

Use this section for provider-specific mechanics. The expected review loop *contract* (request review on every push, verify head-SHA coverage, triage findings, reply + resolve, escalate when stuck) is defined in [AGENTS.md -> PR Review Etiquette](../AGENTS.md#pr-review-etiquette). This section only describes how to make GitHub Copilot reliably execute it.

### Triggering and Polling

Auto-review on push is configured (via the branch ruleset's `copilot_code_review` rule with `review_on_push: true`) but fires inconsistently in practice - treat it as best-effort, not guaranteed. After every push, **re-request a review programmatically** via the GraphQL `requestReviews` mutation, passing the Copilot reviewer's bot node id in `botIds`. This now works reliably (it previously did not - a maintainer had to click "re-request review" in the UI; the agent can now drive the loop end-to-end without that hand-off).

> **The reviewer login differs by API - this is intentional, not a typo.** In **GraphQL** (`gh api graphql` and `gh pr view --json reviews`, which is GraphQL-backed) the `Bot.login` is `copilot-pull-request-reviewer` - **no `[bot]` suffix**. In the **REST** API (`gh api repos/.../issues|pulls/...`) the same account's `user.login` is `copilot-pull-request-reviewer[bot]` - **with** the suffix. Each query below uses the correct form for its API; match the API, not a single spelling, when adapting them.

```sh
# 1. PR node id + the Copilot reviewer's bot node id (read from any existing
# Copilot review; the reviewer login is `copilot-pull-request-reviewer`).
PR_NODE=$(gh pr view <N> --json id --jq '.id')
BOT_ID=$(gh api graphql -f query='
{
repository(owner: "ptr727", name: "ESPHome-NonRoot") {
pullRequest(number: <N>) {
reviews(first: 50) { nodes { author { __typename login ... on Bot { id } } } }
}
}
}' --jq '[.data.repository.pullRequest.reviews.nodes[]
| select(.author.login == "copilot-pull-request-reviewer")
| .author.id] | first')

# 2. Re-request a Copilot review on the current head.
gh api graphql -f query='
mutation($pr: ID!, $bot: ID!) {
requestReviews(input: { pullRequestId: $pr, botIds: [$bot], union: true }) {
pullRequest { id }
}
}' -F pr="$PR_NODE" -F bot="$BOT_ID"
```

The bot node id is read from an existing Copilot review, so step 1 needs at least one prior review on the PR - the auto-review-on-open normally supplies the first one. If no Copilot review exists yet and auto-review didn't fire, request `Copilot` once through the GitHub PR UI to seed it, then use the mutation for every subsequent re-request.

**Do NOT post `@Copilot review` as a PR comment.** That comment triggers the Copilot *coding agent* (`copilot-swe-agent[bot]`), which makes code changes rather than posting a review.

Known non-working request paths (don't rely on them - use the `requestReviews` mutation above instead):

- `POST /requested_reviewers` with `reviewers=[Copilot]` can return 200 but no-op.
- `copilot-pull-request-reviewer` as a requested reviewer slug returns 422.

### Verify Review Covered Current Head

Before merging, confirm Copilot reviewed the current PR head SHA. Copilot may respond as either a formal review (carries an exact commit SHA) or an issue comment (no SHA - use the most recent Copilot comment for manual confirmation). Check both.

```sh
PR_HEAD=$(gh pr view <N> --json headRefOid --jq '.headRefOid')

# 1. Formal review - exact SHA match.
gh pr view <N> --json reviews --jq \
'.reviews[] | select(.author.login=="copilot-pull-request-reviewer") | .commit.oid' \
| grep -q "$PR_HEAD" && echo "covered via formal review"

# 2. Issue comment - show the most recent Copilot comment for manual
# confirmation. This is the REST API, so the login carries the `[bot]` suffix.
gh api repos/ptr727/ESPHome-NonRoot/issues/<N>/comments --jq \
'[.[] | select(.user.login=="copilot-pull-request-reviewer[bot]")] | last | {created_at, body: .body[:200]}'
```

Coverage is confirmed when (1) exits 0. For issue comments (path 2), body content is the only reliable signal - `created_at` is not: `git log -1 --format=%cI` is the **commit** timestamp, not the push timestamp, so amended or rebased commits can have an earlier timestamp and an older Copilot comment could satisfy a time check even though Copilot never saw the current head. Treat path (2) as confirmed only when the comment body explicitly refers to the current changes.

### Bounded Retry Workflow

If a review did not run on the current head, retry:

1. Wait briefly and check head-SHA coverage (see above).
1. Re-request the review via the `requestReviews` mutation (see "Triggering and Polling"); fall back to the GitHub PR UI only if the mutation no-ops.
1. Retry up to two more times (three total).
1. If still missing, mark review as blocked and escalate to the user/maintainer with what was attempted.

### Reply and Thread Resolution Workflow

List unresolved threads. Use `first: 100` with cursor-based pagination; if `hasNextPage` is true, re-run with `after: "<endCursor>"` to retrieve the next page:

```sh
gh api graphql -f query='
{
repository(owner: "ptr727", name: "ESPHome-NonRoot") {
pullRequest(number: <N>) {
reviewThreads(first: 100) {
nodes {
id isResolved path
comments(first: 1) { nodes { author { login } body } }
}
pageInfo { hasNextPage endCursor }
}
}
}
}' | jq '
.data.repository.pullRequest.reviewThreads |
(.pageInfo | "hasNextPage=\(.hasNextPage) endCursor=\(.endCursor)"),
(.nodes[] | select(.isResolved == false))
'
```

Reply on a thread, then resolve it:

```sh
gh api graphql -f query='
mutation($threadId: ID!, $body: String!) {
addPullRequestReviewThreadReply(input: { pullRequestReviewThreadId: $threadId, body: $body }) {
comment { id }
}
}' -F threadId="PRRT_..." -F body="Fixed in <SHA>: <one-line summary>."

gh api graphql -f query='
mutation($threadId: ID!) {
resolveReviewThread(input: { threadId: $threadId }) { thread { id isResolved } }
}' -F threadId="PRRT_..."
```

Issue-level Copilot comments (those in `issues/<N>/comments`) have no resolution action - GitHub provides no API or UI to resolve them. Reply if the finding warrants it; no resolution step is needed or possible.

Reply-body conventions:

- Accepted bug/style fix: include fixing commit SHA and a one-line summary.
- Declined style comment: cite the rule (AGENTS.md) and the existing-tree precedent.
- Declined architecture proposal: one-sentence rationale.

After the final push, sweep-resolve stale older threads for removed code paths.

## When in Doubt

Read [AGENTS.md](../AGENTS.md) for this repo's conventions. Don't restate its rules in commit bodies or PR descriptions - keep those focused on the change itself.

**In a derived repo:** if you find a discrepancy that should be fixed in the template itself (this file or AGENTS.md is out of date, a rule is missing, something bit this repo and would bite the next), open an issue upstream in [`ptr727/ProjectTemplate`](https://github.com/ptr727/ProjectTemplate) rather than only fixing it locally - see the template's [AGENTS.md "Staying in Sync and Reporting Drift Upstream"](https://github.com/ptr727/ProjectTemplate/blob/main/AGENTS.md#staying-in-sync-and-reporting-drift-upstream).
30 changes: 29 additions & 1 deletion .github/dependabot.yml
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,13 @@
# https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates
# https://containers.dev/guide/dependabot
#
# Each ecosystem targets both main and develop so neither branch goes stale
# between releases.

version: 2
updates:

# ----- github-actions -----

# main
- package-ecosystem: "github-actions"
target-branch: "main"
Expand All@@ -25,3 +29,27 @@ updates:
actions-deps:
patterns:
- "*"

# ----- docker (base image in Docker/Dockerfile) -----

# main
- package-ecosystem: "docker"
target-branch: "main"
directory: "/Docker"
schedule:
interval: "daily"
groups:
docker-deps:
patterns:
- "*"

# develop
- package-ecosystem: "docker"
target-branch: "develop"
directory: "/Docker"
schedule:
interval: "daily"
groups:
docker-deps:
patterns:
- "*"
Loading
Loading