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
6 changes: 3 additions & 3 deletions .github/workflows/merge-bot-pull-request.yml
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ jobs:
# Auto-merge every tier, semver-major included: the required checks are the gate, not the bump magnitude.
- name: Merge pull request step
run: |
set -euo pipefail
set -Eeuo pipefail
case "${{ github.event.pull_request.base.ref }}" in
develop) method=--squash ;;
main) method=--merge ;;
Expand Down Expand Up @@ -84,7 +84,7 @@ jobs:

- name: Merge pull request step
run: |
set -euo pipefail
set -Eeuo pipefail
case "${{ github.event.pull_request.base.ref }}" in
develop) method=--squash ;;
main) method=--merge ;;
Expand Down Expand Up @@ -126,7 +126,7 @@ jobs:

- name: Merge pull request step
run: |
set -euo pipefail
set -Eeuo pipefail
case "${{ github.event.pull_request.base.ref }}" in
develop) method=--squash ;;
main) method=--merge ;;
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/publish-release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ jobs:

- name: Assert dispatch ref step
run: |
set -euo pipefail
set -Eeuo pipefail
if [ "${{ github.ref_name }}" != "main" ] && [ "${{ github.ref_name }}" != "develop" ]; then
echo "::error::Dispatch publish-release from main (release) or develop (prerelease); got ${{ github.ref_name }}."
exit 1
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/test-pull-request.yml
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ jobs:
steps:
- name: Check workflow results step
run: |
set -euo pipefail
set -Eeuo pipefail
if [[ "${{ needs.validate.result }}" != "success" ]]; then
echo "Job 'validate' did not succeed (${{ needs.validate.result }}); refusing to pass."
exit 1
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/validate-task.yml
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ jobs:

- name: Validate registry and spec step
run: |
set -euo pipefail
set -Eeuo pipefail
for f in registry/*.json spec/*.json repo-config/*.json; do
jq empty "$f"
done
Expand Down
2 changes: 1 addition & 1 deletion AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -227,7 +227,7 @@ These conventions describe the target state. New and modified workflows must res
- **Workflow `name:`** (the top-level `name:` field): reusable workflow names end in **"task"** (e.g. `Build PyPI library task`); entry-point workflow names end in **"action"** (e.g. `Publish project release action`, `Test pull request action`). The displayed action name in the GitHub Actions UI tells you at a glance whether you're looking at an orchestrator or a callee.
- **Job and step `name:` suffixes**: every job's `name:` ends in **"job"**; every step's `name:` ends in **"step"** - including the PR-gate aggregator, whose `name:` is a required-status-check `context:` in a branch ruleset (`Check pull request workflow status job` in `test-pull-request.yml`). A ruleset-bound job's `name:` and its ruleset `context:` are the **same string**: rename them **together** - update the live ruleset and `repo-config/{develop,main}.json` in lockstep with the job `name:`, never one without the other, or required-status-check enforcement silently breaks. There is no un-suffixed exception.
- **Concurrency**: top-level workflows declare `concurrency: { group: '${{ github.workflow }}-${{ github.ref }}', cancel-in-progress: true }` so a fresh push supersedes an in-flight run on the same ref. **Documented exceptions** (both record the rationale inline in their header comment): (1) [`merge-bot-pull-request.yml`](./.github/workflows/merge-bot-pull-request.yml) uses `cancel-in-progress: false` because its three-job model (enable-auto-merge on opened, disable-auto-merge on maintainer-pushed synchronize, with method dispatched by base) requires each event to run to completion in arrival order - cancellation would leave auto-merge in an inconsistent state. (2) [`publish-release.yml`](./.github/workflows/publish-release.yml) uses both a **global, ref-independent group** (`group: ${{ github.workflow }}`, dropping the usual `-${{ github.ref }}`) and `cancel-in-progress: false`. It publishes shared ref-independent artifacts (both branches' Docker tags/caches and GitHub releases) on schedule/dispatch 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; and cancelling a publish mid-flight can leave a partially pushed tag set or a half-created release. The global group + queueing serializes every publish run to completion.
- **Shells**: multi-line `run:` blocks with bash start with `set -euo pipefail` - fail fast, fail on undefined vars, fail on a failed pipe segment.
- **Shells**: every bash surface - a multi-line `run:` block and every committed `.sh` script alike - starts with `set -Eeuo pipefail`: fail fast, fail on undefined vars, fail on a failed pipe segment, and let an `ERR` trap inherit into functions, subshells, and command substitutions (`-E`). The `-E` is defense in depth: the fleet ships no `ERR` trap today, so a script that later adds one inherits the behavior instead of silently losing it.
Comment thread
ptr727 marked this conversation as resolved.
- **Conditionals**: multi-line `if:` uses folded scalar `if: >-` so YAML preserves whitespace correctly. Literal block (`if: |`) is wrong because it embeds newlines inside the boolean expression.
- **Boolean inputs**: workflows triggered both via `workflow_call` and `workflow_dispatch` must declare each boolean input in *both* trigger blocks - one definition does not propagate to the other. `workflow_call` delivers booleans as actual booleans; `workflow_dispatch` delivers them as the *strings* `"true"`/`"false"`. Any `if:` consuming a boolean input must compare against both forms - `if: ${{ inputs.foo == true || inputs.foo == 'true' }}`.
- **Validate input/state consistency at entry, fail fast**: when a workflow's inputs must satisfy a cross-input or input-versus-derived-state invariant (e.g. the release branch must match the computed version's prerelease status, or two inputs are mutually exclusive), assert it **once** in a dedicated entry validation step/job that the downstream jobs `needs:`, before any expensive build or publish work - not as partial checks scattered deep in later jobs. One gate that fails fast with a clear `::error::` beats a late or one-directional check. Examples: [`build-release-task.yml`](./catalog/snippets/workflows/build-release-task.yml)'s `validate-release` job (branch-versus-prerelease, both directions) and [`publish-docker-readme-task.yml`](./catalog/snippets/workflows/publish-docker-readme-task.yml)'s "Validate inputs step".
Expand Down
4 changes: 2 additions & 2 deletions WORKFLOW.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ Prescriptive style/legibility rules. Cheap to check, necessary but not sufficien
- **Workflow `name:`.** Reusable names end in **"task"**; entry-point names end in **"action"**.
- **Job and step `name:`.** Every job ends in **"job"**, every step in **"step"** - including a ruleset-bound required-check job, whose `name:` and the ruleset `context:` are one string renamed together (never independently).
- **Concurrency.** Top-level workflows declare `concurrency: { group: '${{ github.workflow }}-${{ github.ref }}', cancel-in-progress: true }`. Document exceptions inline (D7).
- **Shells.** Every multi-line bash `run:` starts `set -euo pipefail`.
- **Shells.** Every multi-line bash `run:` - and every committed `.sh` script - starts `set -Eeuo pipefail`.
- **Conditionals.** Multi-line `if:` uses the folded scalar `if: >-`.
- **Boolean inputs.** A boolean used by both `workflow_call` and `workflow_dispatch` is declared in **both** trigger blocks; `workflow_dispatch` delivers the **string** `"true"`/`"false"`, so any `if:` compares both forms: `${{ inputs.foo == true || inputs.foo == 'true' }}`.
- **Reusable-workflow permissions.** Job-level `permissions:` are validated **before** `if:`, so even a skipped job needs valid permissions. Grant least privilege; a reusable callee's extra scope (e.g. `actions: write` for cleanup) is granted by the **caller**.
Expand Down Expand Up @@ -201,7 +201,7 @@ The required behaviors, organized by domain. Each is a **MUST**, stated as input

- **D9.1** Every action SHA-pinned with a version comment (sole exception: the documented lagging-tag tool).
- **D9.2** File/workflow/job/step names follow the suffix rules; a ruleset-bound job's `name:` equals its ruleset `context:` (renamed together).
- **D9.3** Bash `run:` blocks start `set -euo pipefail`; multi-line `if:` uses `>-`.
- **D9.3** Bash `run:` blocks start `set -Eeuo pipefail`; multi-line `if:` uses `>-`.
- **D9.4** Docker layer cache targets a registry tag, not `type=gha`; `cache-to` writes only the built branch's `buildcache-<branch>` and only on push, while `cache-from` reads both branches; multi-image repos use a per-image cache tag.
- **D9.5** Line endings follow `.editorconfig`.

Expand Down
2 changes: 1 addition & 1 deletion catalog/snippets/devcontainer/dotnet/post-create.sh
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
#!/usr/bin/env bash
set -euo pipefail
set -Eeuo pipefail

# Restore the .NET local-tool manifest (csharpier, dotnet-outdated).
dotnet tool restore
2 changes: 1 addition & 1 deletion catalog/snippets/devcontainer/python/post-create.sh
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
#!/usr/bin/env bash
set -euo pipefail
set -Eeuo pipefail

# Install uv (Astral) for the Python project. Idempotent - re-running
# overwrites in place. The installer drops the binary in $HOME/.local/bin and
Expand Down
1 change: 1 addition & 0 deletions catalog/snippets/workflows/build-executable-task.yml
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ jobs:

- name: Build executable project step
run: |
set -Eeuo pipefail
dotnet publish ./Console/Console.csproj \
--runtime ${{ matrix.runtime }} \
-property:PublishDir=${{ runner.temp }}/publish/${{ matrix.runtime }}/ \
Expand Down
4 changes: 2 additions & 2 deletions catalog/snippets/workflows/build-nugetlibrary-task.yml
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ jobs:

- name: Build NuGet library project step
run: |
set -euo pipefail
set -Eeuo pipefail
dotnet build ./NuGetLibrary/NuGetLibrary.csproj \
-property:OutputPath=${{ runner.temp }}/publish/ \
-property:PackageOutputPath=${{ runner.temp }}/publish/ \
Expand All @@ -66,7 +66,7 @@ jobs:
- name: Publish to NuGet.org step
if: ${{ inputs.push }}
run: |
set -euo pipefail
set -Eeuo pipefail
dotnet nuget push ${{ runner.temp }}/publish/*.nupkg \
--source https://api.nuget.org/v3/index.json \
--api-key ${{ secrets.NUGET_API_KEY }} \
Expand Down
4 changes: 2 additions & 2 deletions catalog/snippets/workflows/build-pypilibrary-task.yml
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,7 @@ jobs:
- name: Compute PyPI version step
id: pypiver
run: |
set -euo pipefail
set -Eeuo pipefail
if [[ "$BRANCH" == "develop" ]]; then
version="${AFV}.dev0"
else
Expand All @@ -100,7 +100,7 @@ jobs:
# Done after tests so the test asserting __version__ is non-empty isn't affected.
- name: Write version into _version.py step
run: |
set -euo pipefail
set -Eeuo pipefail
sed -i 's/^__version__ = .*/__version__ = "'"$VERSION"'"/' src/ptr727_projecttemplate_library/_version.py
env:
VERSION: ${{ steps.pypiver.outputs.version }}
Expand Down
6 changes: 3 additions & 3 deletions catalog/snippets/workflows/build-release-task.yml
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,7 @@ jobs:
BRANCH: ${{ inputs.branch }}
SMOKE: ${{ inputs.smoke }}
run: |
set -euo pipefail
set -Eeuo pipefail
# Smoke builds never publish and always version as prerelease (detached PR HEAD), which would trip the main arm.
if [[ "$SMOKE" == "true" ]]; then
echo "Smoke build; skipping release version validation."
Expand Down Expand Up @@ -193,7 +193,7 @@ jobs:
GH_TOKEN: ${{ github.token }}
TAG: ${{ needs.get-version.outputs.SemVer2 }}
run: |
set -euo pipefail
set -Eeuo pipefail
if gh release view "$TAG" --repo "$GITHUB_REPOSITORY" >/dev/null 2>&1; then
echo "exists=true" >> "$GITHUB_OUTPUT"
if [[ "${{ github.event_name }}" == "workflow_dispatch" ]]; then
Expand Down Expand Up @@ -240,7 +240,7 @@ jobs:
env:
GH_TOKEN: ${{ github.token }}
run: |
set -euo pipefail
set -Eeuo pipefail
if ! ids=$(gh api "repos/$GITHUB_REPOSITORY/actions/runs/${{ github.run_id }}/artifacts" --paginate \
--jq ".artifacts[] | select(.name | startswith(\"release-asset-${{ inputs.branch }}-\")) | .id"); then
echo "::warning::Could not list run artifacts; retention-days backstop will reap them."
Expand Down
2 changes: 1 addition & 1 deletion catalog/snippets/workflows/check-upstream-version-task.yml
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,7 @@ jobs:
RESOLVER_COMMAND: ${{ inputs.resolver-command }}
STATE_FILE: ${{ inputs.state-file }}
run: |
set -euo pipefail
set -Eeuo pipefail

# Require a non-empty JSON object of single-line name -> version strings; a CR/LF would corrupt the
# single-line GITHUB_OUTPUT, so reject it here instead of committing unconsumable state.
Expand Down
6 changes: 3 additions & 3 deletions catalog/snippets/workflows/publish-docker-readme-task.yml
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,7 @@ jobs:
MANIFEST: ${{ inputs.manifest }}
MANIFEST_JQ: ${{ inputs.manifest-jq }}
run: |
set -euo pipefail
set -Eeuo pipefail
if [ -n "$REPOSITORIES" ] && [ -n "$MANIFEST" ]; then
echo "::error::Pass either 'repositories' or 'manifest', not both." >&2
exit 1
Expand All @@ -94,7 +94,7 @@ jobs:
MANIFEST: ${{ inputs.manifest }}
MANIFEST_JQ: ${{ inputs.manifest-jq }}
run: |
set -euo pipefail
set -Eeuo pipefail
# Inputs validated above: at most one of repositories / manifest is set, and manifest implies manifest-jq.
if [ -n "$REPOSITORIES" ]; then
echo "repositories=$REPOSITORIES" >> "$GITHUB_OUTPUT"
Expand Down Expand Up @@ -133,7 +133,7 @@ jobs:
- name: Generate readme step
if: ${{ inputs.transform-run != '' }}
run: |
set -euo pipefail
set -Eeuo pipefail
${{ inputs.transform-run }}

- name: Publish Docker Hub readme step
Expand Down
2 changes: 1 addition & 1 deletion catalog/snippets/workflows/publish-plan-task.yml
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ jobs:
ACTOR: ${{ inputs.actor }}
REF: ${{ inputs.ref_name }}
run: |
set -euo pipefail
set -Eeuo pipefail
publish=false
case "$EVENT" in
workflow_dispatch)
Expand Down
4 changes: 2 additions & 2 deletions catalog/snippets/workflows/run-codegen-pull-request-task.yml
Original file line number Diff line number Diff line change
Expand Up @@ -54,14 +54,14 @@ jobs:

- name: Run codegen step
run: |
set -euo pipefail
set -Eeuo pipefail
dotnet run --project ./CodeGen/CodeGen.csproj -- \
--codepath ./CodeGen \
--apikey "${{ secrets.NINJA_API_KEY }}"

- name: Format code step
run: |
set -euo pipefail
set -Eeuo pipefail
dotnet tool restore
dotnet csharpier format --log-level=debug .
git status
Expand Down
2 changes: 1 addition & 1 deletion repo-config/configure.sh
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
# Usage: repo-config/configure.sh [owner/repo] [release|operational] (repo defaults to the current repo via gh;
# model defaults to the registry lookup, else payload inference). The model may also be passed as the sole
# argument: repo-config/configure.sh operational
set -euo pipefail
set -Eeuo pipefail

repo_arg="${1:-}"
model="${2:-}"
Expand Down