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
19 changes: 9 additions & 10 deletions .github/workflows/build-executable-task.yml
Original file line number Diff line number Diff line change
Expand Up @@ -21,10 +21,6 @@ on:
required: false
type: boolean
default: false
outputs:
# Output of the uploaded artifact id
artifact-id:
value: ${{ jobs.upload-build-artifacts.outputs.artifact-id }}

jobs:

Expand Down Expand Up @@ -87,13 +83,11 @@ jobs:

# Smoke builds only need the per-runtime compile to succeed (fast PR
# feedback) — the zipped, downloadable artifact is a release concern, so
# skip the aggregation entirely on smoke. The `artifact-id` output is then
# empty, which is fine because the GitHub release job never runs on smoke.
# skip the aggregation entirely on smoke. The GitHub release job never runs
# on smoke, so no `release-asset-*` artifact is needed then.
upload-build-artifacts:
name: Upload matrix build artifacts job
if: ${{ !inputs.smoke }}
outputs:
artifact-id: ${{ steps.artifact-upload-step.outputs.artifact-id }}
runs-on: ubuntu-latest
needs: [ build-executable-matrix ]

Expand All @@ -109,9 +103,14 @@ jobs:
- name: Zip build output step
run: 7z a -t7z ${{ runner.temp }}/Console.7z ${{ runner.temp }}/publish/*

# GitHub-release asset, uploaded under the `release-asset-<branch>-*`
# convention that the `github-release` job in build-release-task.yml
# collects by pattern (it never names this job) — so a derived project
# swaps its release contents by replacing this leaf task, not the
# orchestrator. Branch-suffixed so the publisher's branch matrix can
# build both branches in one run without colliding on the artifact name.
- name: Upload build artifacts step
id: artifact-upload-step
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0
with:
name: executable-build-${{ inputs.branch }}
name: release-asset-${{ inputs.branch }}-executable
path: ${{ runner.temp }}/Console.7z
17 changes: 7 additions & 10 deletions .github/workflows/build-nugetlibrary-task.yml
Original file line number Diff line number Diff line change
Expand Up @@ -19,10 +19,6 @@ on:
branch:
required: true
type: string
outputs:
# Output of the uploaded artifact id
artifact-id:
value: ${{ jobs.build-nugetlibrary.outputs.artifact-id }}

jobs:

Expand All @@ -35,8 +31,6 @@ jobs:
build-nugetlibrary:
name: Build NuGet library project job
runs-on: ubuntu-latest
outputs:
artifact-id: ${{ steps.artifact-upload-step.outputs.artifact-id }}
needs: [get-version]

steps:
Expand Down Expand Up @@ -76,11 +70,14 @@ jobs:
- name: Zip output step
run: 7z a -t7z ${{ runner.temp }}/NuGetLibrary.7z ${{ runner.temp }}/publish/*

# Branch-suffixed so the publisher's branch matrix can build both
# branches in one run without colliding on the artifact name.
# GitHub-release asset, uploaded under the `release-asset-<branch>-*`
# convention that the `github-release` job in build-release-task.yml
# collects by pattern (it never names this job) — so a derived project
# swaps its release contents by replacing this leaf task, not the
# orchestrator. Branch-suffixed so the publisher's branch matrix can
# build both branches in one run without colliding on the artifact name.
- name: Upload build artifacts step
id: artifact-upload-step
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0
with:
name: nugetlibrary-build-${{ inputs.branch }}
name: release-asset-${{ inputs.branch }}-nugetlibrary
path: ${{ runner.temp }}/NuGetLibrary.7z
63 changes: 48 additions & 15 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,21 +136,36 @@ 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 }}

- name: Download NuGet library build artifacts step
uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7.0.0
with:
artifact-ids: ${{ needs.build-nugetlibrary.outputs.artifact-id }}
path: ./Publish
ref: ${{ needs.get-version.outputs.GitCommitId }}

- name: Download executable build artifacts step
# Collect every release asset by the `release-asset-<branch>-*` artifact
# convention rather than naming individual build jobs, so this download
# step never changes when a derived project subsets targets: dropping a
# target (delete its job + this job's `needs` entry, per AGENTS.md
# "Per-target subsetting") just leaves one fewer `release-asset-*` to
# match, and a downstream ships different files by adding/replacing a
# leaf `build-*-task.yml` that uploads under this pattern — without
# editing this step. `merge-multiple` flattens the matched artifacts into
# one `./Publish` dir, exactly as the previous per-target downloads did;
# download-artifact@v7 succeeds (downloads nothing) when the pattern
# matches zero artifacts, so a file-less release (tag + LICENSE/README
# only) still works.
# NOTE: subset releases by *deleting* the target, not by `enable_*:
# false`. `enable_*` is for smoke subsetting only — it skips a build job,
# and because `github-release` `needs` those jobs, a skipped one would
# skip this release job too (GitHub Actions: a skipped `needs` job skips
# its dependents).
- name: Download release asset artifacts step
uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7.0.0
with:
artifact-ids: ${{ needs.build-executable.outputs.artifact-id }}
pattern: release-asset-${{ inputs.branch }}-*
merge-multiple: true
path: ./Publish

# The weekly publisher re-runs even when a branch has no new commits, so
Expand All @@ -159,7 +185,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 +204,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
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
Loading