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
7 changes: 5 additions & 2 deletions .github/copilot-instructions.md
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,8 @@ Use this section for provider-specific mechanics. The expected review loop *cont

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`).
Expand Down Expand Up @@ -94,9 +96,10 @@ 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.
# 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/<owner>/<repo>/issues/<N>/comments --jq \
'[.[] | select(.user.login=="copilot-pull-request-reviewer")] | last | {created_at, body: .body[:200]}'
'[.[] | 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.
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/build-docker-task.yml
Original file line number Diff line number Diff line change
Expand Up @@ -100,7 +100,7 @@ jobs:
cache-from: |
type=registry,ref=docker.io/ptr727/projecttemplate:buildcache-main
type=registry,ref=docker.io/ptr727/projecttemplate:buildcache-develop
cache-to: ${{ inputs.push && format('type=registry,ref=docker.io/ptr727/projecttemplate:buildcache-{0},mode=max', inputs.branch) || '' }}
cache-to: ${{ inputs.push && format('type=registry,ref=docker.io/ptr727/projecttemplate:buildcache-{0},mode=max,ignore-error=true', inputs.branch) || '' }}
build-args: |
LABEL_VERSION=${{ needs.get-version.outputs.SemVer2 }}
BUILD_CONFIGURATION=${{ inputs.branch == 'main' && 'Release' || 'Debug' }}
Expand Down
35 changes: 28 additions & 7 deletions .github/workflows/build-release-task.yml
Original file line number Diff line number Diff line change
Expand Up @@ -71,10 +71,15 @@ jobs:
build-nugetlibrary:
name: Build NuGet library job
if: ${{ inputs.enable_nuget }}
needs: [get-version]
uses: ./.github/workflows/build-nugetlibrary-task.yml
secrets: inherit
with:
ref: ${{ inputs.ref }}
# Pin to the exact commit get-version resolved (immutable), not the
# possibly-moving branch ref: the publisher passes a branch name, and a
# commit landing mid-run could otherwise build artifacts from a different
# commit than the one the release tag (also GitCommitId) points at.
ref: ${{ needs.get-version.outputs.GitCommitId }}
branch: ${{ inputs.branch }}
# Conditional push to NuGet.org — never on a smoke build.
push: ${{ inputs.nuget && !inputs.smoke }}
Expand All @@ -86,29 +91,35 @@ jobs:
build-pypilibrary:
name: Build PyPI library job
if: ${{ inputs.enable_pypi }}
needs: [get-version]
uses: ./.github/workflows/build-pypilibrary-task.yml
secrets: inherit
with:
ref: ${{ inputs.ref }}
# Pin to the resolved commit (see build-nugetlibrary).
ref: ${{ needs.get-version.outputs.GitCommitId }}
branch: ${{ inputs.branch }}

build-executable:
name: Build executable job
if: ${{ inputs.enable_executable }}
needs: [get-version]
uses: ./.github/workflows/build-executable-task.yml
secrets: inherit
with:
ref: ${{ inputs.ref }}
# Pin to the resolved commit (see build-nugetlibrary).
ref: ${{ needs.get-version.outputs.GitCommitId }}
branch: ${{ inputs.branch }}
smoke: ${{ inputs.smoke }}

build-docker:
name: Build Docker job
if: ${{ inputs.enable_docker }}
needs: [get-version]
uses: ./.github/workflows/build-docker-task.yml
secrets: inherit
with:
ref: ${{ inputs.ref }}
# Pin to the resolved commit (see build-nugetlibrary).
ref: ${{ needs.get-version.outputs.GitCommitId }}
branch: ${{ inputs.branch }}
smoke: ${{ inputs.smoke }}
# Conditional push to Docker Hub — never on a smoke build.
Expand All @@ -125,10 +136,13 @@ jobs:

steps:

# Check out the exact built commit (NBGV `GitCommitId`), not the
# possibly-moving `inputs.ref` branch, so the uploaded release files
# match the tag even if the branch advances mid-run.
- name: Checkout code step
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
ref: ${{ inputs.ref }}
ref: ${{ needs.get-version.outputs.GitCommitId }}

- name: Download NuGet library build artifacts step
uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7.0.0
Expand Down Expand Up @@ -159,7 +173,11 @@ jobs:
set -euo pipefail
if gh release view "$TAG" --repo "$GITHUB_REPOSITORY" >/dev/null 2>&1; then
echo "exists=true" >> "$GITHUB_OUTPUT"
echo "Release $TAG already exists; skipping release creation (no-op republish)."
if [[ "${{ github.event_name }}" == "workflow_dispatch" ]]; then
echo "Release $TAG already exists; workflow_dispatch will refresh it."
else
echo "Release $TAG already exists; skipping release creation (no-op republish)."
fi
else
echo "exists=false" >> "$GITHUB_OUTPUT"
fi
Expand All @@ -174,8 +192,11 @@ jobs:
# could tag the release on a newer commit than the one that was built).
# The exact SHA is immutable, on the right branch, and consistent with
# both the SemVer2 tag and the uploaded artifacts.
# Skip the no-op weekly republish when the tag already exists, but always
# allow a manual `workflow_dispatch` through so it can repair/refresh a
# partially-created release for the same tag.
- name: Create GitHub release step
if: ${{ steps.release-exists.outputs.exists == 'false' }}
if: ${{ steps.release-exists.outputs.exists == 'false' || github.event_name == 'workflow_dispatch' }}
uses: softprops/action-gh-release@3bb12739c298aeb8a4eeaf626c5b8d85266b0e65 # v2.6.2
Comment thread
ptr727 marked this conversation as resolved.
with:
generate_release_notes: true
Expand Down
16 changes: 9 additions & 7 deletions .github/workflows/publish-release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -11,14 +11,16 @@ on:
# (also refreshing the Docker base image, e.g. `ubuntu:rolling`).
- cron: '0 2 * * MON'

# Single GLOBAL group (not ref-scoped): this workflow publishes shared,
# ref-independent artifacts — it pushes both branches' Docker tags + caches
# and creates GitHub releases for both — on schedule/dispatch regardless of
# the triggering ref. A ref-scoped group would let a scheduled run (ref=main)
# and a manual dispatch (ref=develop) run concurrently and double-push. The
# global group serializes every publish run.
# Real publishes (schedule, dispatch, or push when PUBLISH_ON_MERGE is set)
# share a single GLOBAL, ref-independent group so they serialize: they push
# both branches' shared Docker tags + caches and create GitHub releases for
# both regardless of the triggering ref, so a ref-scoped group would let a
# scheduled run (ref=main) and a manual dispatch (ref=develop) run concurrently
# and double-push. Non-publishing `push` runs (the two-phase default) get a
# unique per-run group so they don't queue behind — or delay — a real publish;
# they only execute the no-op `setup` job and skip everything else.
concurrency:
group: ${{ github.workflow }}
group: ${{ (github.event_name == 'schedule' || github.event_name == 'workflow_dispatch' || vars.PUBLISH_ON_MERGE == 'true') && github.workflow || format('{0}-noop-{1}', github.workflow, github.run_id) }}
# Documented exception to the standard `cancel-in-progress: true` (see
# AGENTS.md "Workflow YAML Conventions"): cancelling a publish mid-flight can
# leave a partially pushed multi-arch tag set or a half-created GitHub
Expand Down
6 changes: 5 additions & 1 deletion .github/workflows/test-pull-request.yml
Original file line number Diff line number Diff line change
Expand Up @@ -101,7 +101,11 @@ jobs:
# docs-only PR) — unit tests still run.
smoke-build:
name: Smoke build changed targets job
needs: [changes]
# Also gate on unit-test: the smoke build includes a Docker image build, so
# don't spend it when unit tests are already failing. A failed unit-test
# leaves this job skipped (needs unsatisfied) and the aggregator blocks on
# the unit-test failure directly.
needs: [changes, unit-test]
if: >-
needs.changes.outputs.docker == 'true' ||
needs.changes.outputs.nuget == 'true' ||
Expand Down
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -536,7 +536,7 @@ Which to pick: two-phase suits projects whose consumers are **pushed** updates (
[commits-link]: https://github.com/ptr727/ProjectTemplate/commits/main
[discussions-link]: https://github.com/ptr727/ProjectTemplate/discussions
[docker-link]: https://hub.docker.com/r/ptr727/projecttemplate
[dockerbuildstatus-shield]: https://img.shields.io/github/actions/workflow/status/ptr727/ProjectTemplate/publish-release.yml?logo=github&label=Docker%20Build
[dockerbuildstatus-shield]: https://img.shields.io/github/actions/workflow/status/ptr727/ProjectTemplate/publish-release.yml?event=schedule&logo=github&label=Docker%20Build
[dockerdevelopversion-shield]: https://img.shields.io/docker/v/ptr727/projecttemplate/develop?label=Docker%20Develop&logo=docker&color=orange
[dockerlatestversion-shield]: https://img.shields.io/docker/v/ptr727/projecttemplate/latest?label=Docker%20Latest&logo=docker
[github-link]: https://github.com/ptr727/ProjectTemplate
Expand All @@ -550,7 +550,7 @@ Which to pick: two-phase suits projects whose consumers are **pushed** updates (
[prereleaseversion-shield]: https://img.shields.io/github/v/release/ptr727/ProjectTemplate?include_prereleases&filter=*-g*&label=GitHub%20Pre-Release&logo=github
[pypi-link]: https://pypi.org/project/ptr727-projecttemplate-library/
[pypireleaseversion-shield]: https://img.shields.io/pypi/v/ptr727-projecttemplate-library?logo=pypi&label=PyPI%20Release
[releasebuildstatus-shield]: https://img.shields.io/github/actions/workflow/status/ptr727/ProjectTemplate/publish-release.yml?logo=github&label=Releases%20Build
[releasebuildstatus-shield]: https://img.shields.io/github/actions/workflow/status/ptr727/ProjectTemplate/publish-release.yml?event=schedule&logo=github&label=Releases%20Build
[releases-link]: https://github.com/ptr727/ProjectTemplate/releases
[releaseversion-shield]: https://img.shields.io/github/v/release/ptr727/ProjectTemplate?logo=github&label=GitHub%20Release

Expand Down