ESPHome-NonRoot builds and publishes a single multi-arch Docker image (ptr727/esphome-nonroot) that runs ESPHome plus the esphome-device-builder dashboard as a non-root user, layered onto a python:3.14-slim base. It ships a single release target: the Docker image; consumers pull from Docker Hub on their own cadence. There is no application source in this repo - the Dockerfile is the only build input.
This file is the canonical reference for cross-cutting AI-agent rules. The CI/CD workflow contract and conventions live in WORKFLOW.md; code-style conventions live in CODESTYLE.md. Copilot review mechanics are owned by .github/copilot-instructions.md - this file delegates them there explicitly (see "PR Review Etiquette" below). High-level summaries in other docs (e.g. README's Contributing section) are allowed when they link back here; don't duplicate the rules themselves. The project's project-specific conventions also live here, not in .github/copilot-instructions.md - that file targets GitHub Copilot / VS Code specifically, while this file is the agent-agnostic one every coding agent reads, so any rule a reviewer must honor has to live here to be provider-independent.
Where rules live. A durable project, code, or style rule belongs in this file (or WORKFLOW.md / CODESTYLE.md as appropriate), so it is versioned and read by every session and every agent. An agent's own session memory or scratch state is private and lost on restart, so it is never the system of record for a rule: when you learn or are corrected on a rule, write it into the right doc in the same change. Memory may also note it, but the committed docs are the source of truth.
- Default to staging, not committing. Stage changes with
git addand leavegit committo the developer unless the developer has explicitly authorized the agent to commit for the current ask ("commit this", "open a PR", etc.). Authorization is scope-bound - it covers the commits needed for that specific task, not a blanket commit license for the rest of the session. - All commits must be cryptographically signed (SSH or GPG). Branch protection enforces this on both branches; unsigned commits are rejected on push. Signing depends on environment configuration -
git config commit.gpgsign true, a configureduser.signingkey, and a working signing agent (loadedssh-agentfor SSH, orgpg-agentfor GPG). If signing is not configured in the environment, do not commit - surface the missing config to the developer and stop atgit add. Verify before any agent-authored commit (git config --get commit.gpgsign && ssh-add -Lor the GPG equivalent). Signing must be live before the first commit, not retrofitted. Turning onRequire signed commitsagainst a branch that already has unsigned commits forces a rewrite of that entire history to re-sign it - changing every commit SHA and making whoever does the rewrite the committer and signer of every commit (a rebase preserves theauthorfield but not the original signatures; you cannot sign another contributor's commits for them). During new-repo setup, never create commits until signing is verified. - Commit under the committing account's own GitHub
noreplyidentity - never a private, personal, or invented address. Theauthorandcommitteron every agent-authored commit are the GitHubnoreplyaddress of the account whose key signs the commit (above) - GitHub issues these in ausername@users.noreply.github.comorID+username@users.noreply.github.comform, and for this single-maintainer fleet it is the owner'sptr727@users.noreply.github.com. Do not setuser.name/user.emailto a fabricated persona, bot name, or product name, and do not commit under whatever identity the environment happens to carry: verifygit config --get user.emailis that GitHubnoreplyaddress before committing, and fix it if not. A wrong identity is not cosmetic - a private email trips GitHub's email-privacy push protection (GH007), and an unrecognized or invented author pollutes history. Identity is separate from signing: a wrong author does not by itself fail the signature rule, but the ad-hoc identities that produce it are typically also unsigned, which the signing rule above then rejects on push. - Never force push. Do not run
git push --forceorgit push --force-with-leaseunder any circumstances. Force pushing rewrites shared history and can cause data loss. - Never run destructive git commands (
git reset --hard,git checkout .,git restore .,git clean -f) without explicit developer instruction.
- The
develop -> mainrelease merge is maintainer-only. Drivefeature -> developPRs end-to-end when authorized (commit, push, Copilot review loop, squash-merge), but never self-merge a release tomain.
developis the integration branch. Feature branches ->developis squash-only; develop is kept linear.develop->mainis merge-commit only (no squash, no rebase). Merge commits preserve develop's commit list as a real second-parent reference on main, which lets the release model attribute releases to the develop commits that produced them (relevant to the weekly publish - see "Release Model" below). Branch protection enforces this: the develop ruleset allows onlysquash, the main ruleset allows onlymerge.- All commits on both branches must be cryptographically signed (SSH or GPG). Squash and merge commits created via the GitHub UI are signed by GitHub's web-flow key.
developis forward-only - nomain -> developback-merges. The develop ruleset's squash-only setting physically blocks merge commits on develop. Historical back-merge commits visible ingit logpredate this rule and must not be repeated.- Both rulesets intentionally omit "Require branches to be up to date before merging" (
strict_required_status_checks_policy: false), for two distinct reasons:- Main - the check is graph-based; it asks whether main's tip commit is reachable from develop, not whether the two branches have the same content. After any develop -> main release, main's tip is a brand-new merge commit that develop's history doesn't contain. Forward-only develop never adds it (no back-merge of main into develop), so the check would fail on every subsequent release.
- Develop - bot auto-merge incompatibility. When two bot PRs against develop land in the same minute (e.g. two grouped Dependabot PRs from the same daily run, or the upstream-version bump alongside a Dependabot bump), the first to merge pushes the second into
mergeStateStatus: BEHIND. GitHub's auto-merge will not fire while the strict flag is on, and nothing in the workflow set auto-updates a bot branch in that window - the merge-bot enables auto-merge viagh pr merge --autobut never rebases a stalled branch onto base (seemerge-bot-pull-request.yml). Real file-level conflicts are still caught textually (mergeable: CONFLICTINGblocks merge regardless); semantic-but-not-textual conflicts that combine cleanly are caught by the post-merge develop CI run rather than pre-merge. Do not reintroduce the strict flag on develop thinking it's hygiene - it breaks bot auto-merge.
- Dependabot and the upstream-version tracker target both
mainanddevelopin parallel..github/dependabot.ymlduplicates every ecosystem entry (one per branch), and.github/workflows/check-upstream-version.ymlcallscheck-upstream-version-task.yml, which matrixes over both branches, opening rolling bump PRs on branchesupstream-version-mainandupstream-version-develop. Each branch absorbs its own bot PRs independently, so neither falls behind, and the forward-only rule still holds (nothing is back-merged from main to develop - both branches receive their updates directly). Parallel auto-merge across same-batch bot PRs is race-proof only because both rulesets have the strict "up to date" flag off (see bullet above). The merge-bot (.github/workflows/merge-bot-pull-request.yml) dispatches--squashor--mergefrom each PR's base ref via acasestatement so the form matches the ruleset on either base. Dependabot security PRs (CVE-driven) always open against the repo default branch (main) regardless oftarget-branch- the samecasestatement covers them. - Dual-target tracker output is deterministic, so it does not conflict. The upstream-version tracker writes only the resolved upstream versions into
upstream-version.jsonas a sorted JSON object (esphome+device_builder) - no per-invocation state (timestamps, GUIDs). Both matrix legs resolving the same PyPI versions produce byte-identical content, so independentmain/developbumps never diverge into adevelop -> mainmerge conflict. - Maintainer-pushed commits on a bot PR auto-disable auto-merge. The merge-bot's
merge-dependabotandmerge-upstream-versionjobs only fire onopened/reopenedevents (auto-merge is enabled exactly once per PR, for bot-authored PRs that originate from this repository, not forks). When a maintainer pushes commits to a bot's branch (asynchronizeevent with a non-bot actor), thedisable-auto-merge-on-maintainer-pushjob fires and callsgh pr merge --disable-auto; the maintainer's commits stay in the PR but won't auto-merge with the bot's content. Re-enable manually (gh pr merge --auto <PR>) when ready. The merge-bot is onpull_request_targetwith per-PR concurrency; it carriesmerge-dependabot,merge-upstream-version, anddisable-auto-merge-on-maintainer-push. - App-token workflows use Client ID, not App ID.
actions/create-github-app-tokendeprecated the numericapp-idinput in v3.0.0; the merge-bot and the tracker useclient-id: ${{ secrets.CODEGEN_APP_CLIENT_ID }}(withprivate-key: ${{ secrets.CODEGEN_APP_PRIVATE_KEY }}). The App token - notGITHUB_TOKEN- is required so the merge push is committed by the App and fires downstream workflows (GITHUB_TOKENpushes are blocked from triggering further runs by GitHub's recursion guard). When adding new App-token call sites, use the same form - do not reintroduceapp-id. - Why parallel dual-target rather than develop-only with eventual flow-through: consumers pull the Docker image from
maindirectly. A develop-only model would leavemainrunning a stale ESPHome version during long-running develop features, so both branches receive their own bot updates on their own cadence and each stays current. The upstream ESPHome version is the shipped content (it sets the image tag and the installed package), so it must stay fresh on both branches. - Mirror to
developany change that lands onmainoutside the feature -> develop -> main flow. "Mirror" means landing the same fix directly ondevelopvia a follow-up PR targetingdevelop- never amain -> developback-merge, which the forward-only rule forbids. A reconciliation-branch fix made to resolve adevelop -> mainpromotion conflict, or a security PR that merges only tomain, leavesdevelopbehind on that content - and forward-onlydevelopnever back-merges to catch up (the same parallel-target principle as the bots). Before basing new work ondevelop, or diagnosing a defect from it, compare content and not commit history: rungit diff origin/main origin/developand inspect its-lines - themain-side of each difference, to check for staleness. A-/+pair within one hunk is usually justdevelopmodifying that code as normal unpromoted work (occasionallydevelopis reworking amain-side fix differently - worth a glance). The stronger staleness signal is a deletion-only hunk (-lines, no+lines): content onmainthatdeveloplacks entirely, i.e. amain-only fixdevelopnever received, so the defect may already be fixed onmain. Prefer this over a commit-log check likegit log origin/develop..origin/main, which is noisy here because it also lists routine promotion merges and themain-direct bot commits whose contentdevelopalready carries via its own parallel bot PRs. - Put issue-closing keywords (
Closes #N) where they fire on merge to the default branch (main). GitHub closes an issue from a PR description only when that PR merges tomain, so aCloses #Nin a PR that targetsdevelopnever fires - put it in thedevelop -> mainpromotion PR instead. A closing keyword in a commit message does close the issue once that commit reachesmainvia promotion, but that is fragile across squash-merges, so prefer the promotion PR's description or close the issue manually once the fix lands onmain.
The publish behavior - the triggered-Docker publisher (one branch per run: the weekly schedule rebuilds main, a path-scoped push publishes main when upstream-version.json changes, and a dispatch publishes the branch it is started from - a multi-arch Docker image + a GitHub release that anchors the version), branch-scoped versioning (main = stable / latest, develop = prerelease / develop), and the rule that ordinary merges do not publish (only a pin change, a schedule, or a dispatch does; code changes accumulate and ship in the next scheduled run, which also refreshes the python:3.14-slim base) - is specified in WORKFLOW.md, the canonical CI/CD guide. Do not duplicate those rules here.
Two release rules are human processes, not workflow outcomes, so they live here:
- The
version(major.minor) inversion.jsonis the version floor; NBGV appends the git height as the SemVer patch.main(the public release ref,publicReleaseRefSpec = ^refs/heads/main$) builds a stableX.Y.<height>;developbuilds a prereleaseX.Y.<height>-g<sha>. NBGV needs onlyversion.jsonand git history, so it versions correctly although the repo builds no .NET assembly. The NBGV version drives the GitHub-release tag and theLABEL_VERSIONbuild-arg; it is independent of theesphomeimage tag, which comes fromupstream-version.json. The maintainer editsversion.json; routine dependency bumps, CI/workflow fixes, doc edits, and upstream-version bumps leave it untouched. - Bump
version.jsononly by maintainer instruction, for a functional change (a new feature, a behavior change, a breaking change) or a significant one-time overhaul of the build/release process (such as a CI/CD migration), in the PR that introduces it (typically ondevelop). Do not bump on a cadence, for routine CI/workflow or dependency or doc or upstream-version edits, or mechanically after a release. - No post-release bump; no develop-ahead requirement. NBGV advances the patch on every commit, so a release always gets a fresh build version with no
version.jsonedit and there is nobump-version-X.YPR after a release. Adevelop -> mainpromotion carries whateverversion.jsonis current. upstream-version.jsonis the shipped upstream pin, bot-maintained. The tracker rewrites the whole file each run (sortedesphome+device_builder), so it carries no inline comment and is bot-maintained, not hand-edited. A new pin landing onmainis what fires the publisher; the build reads it for the image tag and theESPHOME_VERSION/DEVICE_BUILDER_VERSIONbuild-args.dotnet/nbgvis consumed via@master, never SHA-pinned. Its tag stream lagsmastersuch that Dependabot tag-tracking would only propose downgrades to stale tags; this is the sole WORKFLOW.md D9.1 exception (rationale inline in the workflow). Do not SHA-pin it.
- 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.
- Don't write
update stuff,wip, or other vague titles. (Dependabot's defaultBump X from Y to Ztitles are fine - keep them.) - Don't add
Co-Authored-By:lines unless the developer explicitly asks. - Don't put release-bump magnitude in the title - no "minor", "patch", "release v0.2.0", etc. Nerdbank.GitVersioning 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).
Run the dashboard and the ESPHome CLI as a non-root user
Pin softprops/action-gh-release to commit SHA
Refresh the python:3.14-slim base image
Bump docker/build-push-action from 6.19.2 to 7.2.0
Clarify container usage in README
- Use reference-style links for any URL referenced more than once or appearing in lists; alphabetize the reference definitions block.
- Inline single-use relative links (e.g.
[CODESTYLE.md](./CODESTYLE.md)) are fine. - One logical paragraph per line; no hard-wrap line-length limit. For an intentional hard line break within a block - stacked badges, status, or license lines - end the line with a trailing backslash (
\); this explicit form is preferred over trailing whitespace and is not treated as a paragraph split. - Headings follow the title-case-with-short-bind-words rule from the PR-title section.
- Write docs in the current state, not as a change from a prior one. The reader has no memory of the previous behavior, so describe what is: "X does Y", never "X now does Y", "X no longer does Z", or "changed/switched/restored to Y". Before/after framing belongs in changelogs, commit messages, and PR descriptions - not in
README.mdor other living docs.
Applies to code and workflow (#) comments alike.
- Comment only when the code is non-obvious or important. Self-evident code needs no comment.
- Judge "obvious" in context, not line by line. A note that reads as redundant on its own line can be essential in the larger flow - a comment marking a workflow step's exit condition, for example, even though the line itself plainly does a
returnorexit. - State the non-obvious why, not what the code already shows. No cross-project references (do not name other repos), no historic or design narrative, no rule citations - governance lives in this file, not echoed inline.
- One line if it fits in ~120 columns. Do not wrap a comment at 75-80 columns; a short two-line comment that would fit on one line looks sloppy - collapse it. Go multi-line only when the content genuinely exceeds ~120, filling each line rather than narrow-wrapping. For a multi-point comment, prefer short structured lines or
-bullets over one prose paragraph. - Workflows: prefer one short summary description at the top of the file over scattering rationale across steps; comment an individual step only when its purpose is non-obvious.
- Do not accumulate comments. When you change code or a comment, rewrite the whole comment fresh; never bolt a new comment onto an existing one or layer explanations across edits. Comment volume should stay flat or shrink over time, not grow.
- Leave human-authored comments and emojis exactly as written - do not reword, trim, reflow, or "clean" them, even if they seem to bend a rule. Revise only agent-authored comments, and match the surrounding voice when you do.
- Write ASCII in all agent-authored text - documentation, code, comments, commit messages, and PR descriptions. The agent does not introduce non-ASCII characters. Replace typographic Unicode with its ASCII equivalent on sight:
- em dash (U+2014) and en dash (U+2013) -> hyphen
-(use a spaced-for an em-dash-style clause break) - right arrow (U+2192) ->
->; double arrow (U+21D2) ->=> - less-than-or-equal (U+2264) ->
<=; greater-than-or-equal (U+2265) ->>= - curly quotes (U+2018/U+2019/U+201C/U+201D) -> straight
'and"; ellipsis (U+2026) ->...
- em dash (U+2014) and en dash (U+2013) -> hyphen
- Allowed non-ASCII (two narrow exceptions):
- Scientific or technical symbols with no clean ASCII equivalent - e.g. ohm, micro, degree, pi. Keep the symbol; do not approximate it away.
- Unicode the developer deliberately typed - emoji used for emphasis or as callout markers (for example the warning/info markers a maintainer placed in
README.md). Preserve it; never strip the developer's own characters. This carve-out is for developer-authored text, not a license for the agent to add emoji.
.editorconfigdefines the correct ending per file type (CRLF for.md, XML/.csproj/.props, non-workflow.yml/.yaml,.json,.cmd/.bat/.ps1; LF for.sh,Dockerfile, and workflow YAML under.github/workflows/), and.gitattributes(* -text) stops git from normalizing. Carry both files whole even where a rule covers a file type this repo does not ship (the[*.cs]block is inert without.csfiles) so a future re-sync stays a clean whole-file overwrite.- Editing an existing file: preserve its current line endings - do not reflow them as a side effect of a content change, even if the file is already non-compliant. After any programmatic edit, verify with
git diff --stat(only changed lines) andfile <path>(expected ending). Bring a non-compliant file to its.editorconfigending only as a deliberate, isolated EOL-only change. - Workflow YAML (
.github/workflows/*.{yml,yaml}) is pinned LF in.editorconfig- Dependabot and Actions rewrite it with LF, so declaring LF keeps it consistent instead of mixed on every bump. This is not a.gitattributespin (git keeps* -textand does not normalize); CI'seditorconfig-checker(EOL-only) catches a mismatch instead. Non-workflow YAML stays CRLF.
- Any quantitative claim in
README.md(counts, sizes, version floors, supported platforms) must be verified against current code. If a doc number is derived from a code constant, mark the dependency in a source-code comment so the next editor knows to update both.
This "PR Review Etiquette" section is the provider-agnostic review-loop contract; the
.github/copilot-instructions.md"GitHub Copilot Review Runbook" implements its mechanics. Without both in-repo, an agent has no pointer to the reliable Copilot mechanics and falls back to ad-hoc (and known-broken) behavior.
The repo runs a review loop on every PR: local agent iteration plus remote automated review (GitHub Copilot is the configured reviewer). Treat this as a contract regardless of which local agent authored the changes.
Do not merge - and do not enable auto-merge - unless ALL of these hold:
- Required status checks are green (
mergeStateStatus: CLEAN), and - A Copilot review is confirmed on the current head SHA (not an earlier push), and
- Every Copilot finding on that head SHA is closed out - all review threads resolved, and any issue-level Copilot comments (which have no resolve action) triaged and replied to - so zero outstanding findings remain, and
- The maintainer has given explicit permission to merge.
mergeStateStatus: CLEAN reflects only required statuses - it never reflects open bot review comments, so CLEAN alone is never sufficient to merge. A green/CLEAN PR with an unresolved Copilot finding fails this gate; treat it as "not mergeable" no matter what the merge-state field says. The agent never merges on its own (consistent with "default to staging"; merging is maintainer-authorized).
Merging is not releasing. A merge to a release branch does not by itself publish; publishing is a separate, explicitly configured step in the repo's release pipeline (e.g. a scheduled run, a manual dispatch, or an opted-in publish-on-merge trigger), not an automatic consequence of merging. Never describe a merge as cutting a release, and never trigger a publish without explicit maintainer instruction.
- Push changes to the PR branch.
- Re-request a review for the current head SHA. Auto-trigger is unreliable, so request it explicitly via the
requestReviewsGraphQL mutation (now reliable end-to-end - see the runbook); the UI is only a fallback. - Wait for review activity on that head. A completed review that raises no findings is a valid terminal outcome for that head - proceed; do not re-trigger it or treat the absence of comments as a missing review.
- Triage findings.
- Apply fixes or write a rationale for declines.
- Reply to each thread and resolve what was addressed.
- Re-run the loop after every fix push until no actionable findings remain.
Drive the loop to green - review confirmed on the latest head SHA and every actionable finding closed - then stop and apply the Merge Gate above: all four preconditions must hold, and mergeStateStatus: CLEAN alone never satisfies it.
For provider-specific mechanics (how to request review, query review state, post replies, resolve threads), see the GitHub Copilot Review Runbook in .github/copilot-instructions.md. This file owns the contract; that file owns the mechanics.
For each comment, classify before responding:
- Bug - wrong behavior, missing test coverage, or a real divergence between code and docs. Fix it. Reply with the fixing commit SHA when done.
- Style/convention - the comment cites a rule from this file or a language-specific style guide. Two cases:
- The cited rule matches what the existing codebase already does -> fix the offending code.
- The cited rule contradicts what's in the tree, or industry norm -> update the rule instead of the code. The rule is wrong, not the code. Bouncing the same code across rounds is the symptom of a wrong rule. Heuristic: three rounds on the same style category means the rule needs adjusting and the user should authorize the rule change.
- Architectural opinion - the comment proposes a different design ("constrain this to disabled-by-default", "move it elsewhere", "add a runtime guardrail"). This is judgment, not a bug. Surface it to the user with a recommendation; don't apply unilaterally.
Reply inline with either the fixing commit SHA (for accepted issues) or a concise rationale (for declines). Resolve review threads when addressed or intentionally declined with rationale. Issue-level comments (those at repos/.../issues/<N>/comments rather than tied to a specific line) have no resolution action - acknowledge with a reply if needed and move on.
After the final push on a PR, sweep older threads from earlier rounds whose code paths no longer exist; otherwise stale unresolved markers remain in the review UI.
Bring the user in when:
- Genuine design trade-off surfaces (fail-open vs fail-closed, narrow vs broad refactor scope, "should we add a guardrail or trust the docstring"). Triage, recommend, ask.
- Repeated friction across rounds without convergence - that's the rule-needs-updating signal. Stop, summarize the pattern, and let the user authorize the rule change.
- Architectural redesign is requested rather than a bug fix. Surface with a recommendation; never apply unilaterally.
Anti-pattern: don't keep flipping the code on the same style point. Flip the rule once and stick to the rule.
The conventions for everything under .github/workflows/ - action pinning (every action SHA-pinned with a version comment, the sole exception being dotnet/nbgv@master), file/workflow/job/step naming, concurrency, shells, conditionals, boolean inputs, permissions, artifact handling, Docker layer cache, and release tagging - are specified in WORKFLOW.md, the canonical CI/CD guide. New and modified workflows must respect it; do not duplicate those rules here.
Pin every action - first-party (actions/*) and third-party - to a commit SHA with a trailing # vX.Y.Z comment, so Dependabot can still bump it but a tag swap can't change the executed code. Use # vX (major-only) only when the upstream's floating major tag doesn't correspond to a specific patch/minor release SHA. Documented exception (no SHA pin at all): dotnet/nbgv is consumed via @master because the upstream tag stream lags master substantially and Dependabot's tag-tracking would only propose downgrades to stale tags - the rationale is documented inline in that workflow. Do not convert nbgv@master to a SHA, and do not edit that human-authored rationale comment during a terse-comment pass.
- Config files.
.editorconfig(per-file-type EOL plus the inert C# / ReSharper style block),.gitattributes(* -text, with the*.shandDockerfileLF pins),.markdownlint-cli2.jsonc, andCODESTYLE.mdhold the repo's formatting, linting, and code-style rules. Keep.github/copilot-instructions.mdnarrow (Copilot / VS Code review mechanics plus the commit/PR-title summary); project-specific conventions live in this file. - Spell check. The cspell word list and path exclusions live in
cspell.json, the single source shared by the editor and CI. Do not keep a parallel word list in the.code-workspacefile. - Release notes. Keep a short summary in
README.mdand the full history inHISTORY.md; update both when cutting a release.
The repo ships a single .devcontainer/devcontainer.json that builds and runs the project's own Docker/Dockerfile - it is an ESPHome runtime environment (the dashboard, the esphome CLI, PlatformIO) for developing and testing ESPHome configs against the same image the repo ships, not a language toolchain. Open ESPHome-NonRoot.code-workspace and pick Reopen in Container. The container's customizations.vscode.extensions mirrors the recommendations array in the workspace file - when you add an extension to one, add it to the other.
Commit signing inside any container relies on the forwarded ssh-agent socket and the host SSH signing key's public half; the private key never enters the container.
This is a Docker-only repo - there is no language source tree.
Docker/- the multi-arch container build:Dockerfile(multi-stage,python:3.14-slimbase, builds ESPHome wheels and runs as non-root),entrypoint/scripts,Compose.yml, and the staticREADME.mdpublished to Docker Hub..github/workflows/- the branch-scoped CI/CD pipeline (seeWORKFLOW.md) plus the upstream-version tracker;repo-config/- the branch rulesets, settings, and secret audit as code..devcontainer/- the single ESPHome runtime devcontainer.- Docs -
README.md(also the GitHub-side overview),Docker/README.md(the Docker Hub overview),HISTORY.md, this file,WORKFLOW.md,CODESTYLE.md, and.github/copilot-instructions.md. - Config -
.editorconfig,.gitattributes,.markdownlint-cli2.jsonc,cspell.json, andESPHome-NonRoot.code-workspace. version.json- NBGV version floor (drives the GitHub release tag andLABEL_VERSION; independent of the ESPHome image tag).upstream-version.json- the committed upstream version pins (esphome+device_builder) as a sorted JSON object, maintained by the tracker and read by the Docker build for the image tag + theESPHOME_VERSION/DEVICE_BUILDER_VERSIONbuild-args. Sits besideversion.json(both are root-level version sources the build reads), not under.github/(which holds GitHub-platform config).
This repo is derived from ptr727/ProjectTemplate, but its CI/CD is the branch-scoped, one-branch-per-run model specified in WORKFLOW.md, not the template's older two-phase model. The cross-cutting contract carried from the template - this AGENTS.md (the "PR Review Etiquette" section especially), .github/copilot-instructions.md, .markdownlint-cli2.jsonc, .editorconfig, and .gitattributes - is shared byte-for-byte with the sibling Docker repos and the canonical sources; keep it converged rather than re-inventing it. The build-docker-task.yml leaf and the upstream-version tracker entry-point (check-upstream-version.yml, with the ESPHome/device-builder resolver) are owned here; everything else follows WORKFLOW.md.