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
4 changes: 4 additions & 0 deletions .editorconfig
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,10 @@ end_of_line = crlf
[*.sh]
end_of_line = lf

# Dockerfiles - CRLF breaks RUN heredocs and line continuations
[{Dockerfile,*.Dockerfile}]
end_of_line = lf

# Windows scripts
[*.{cmd,bat,ps1}]
end_of_line = crlf
Expand Down
27 changes: 22 additions & 5 deletions .gitattributes
Original file line number Diff line number Diff line change
@@ -1,5 +1,22 @@
# Leave line endings alone
# git config --global core.autocrlf false
# git add --renormalize .
# git ls-files --eol
* -text
# Default: do not normalize line endings (`* -text`); .editorconfig end_of_line rules guide what the editor writes.
# The exception pins below are git's own enforcement - they force LF for execution-sensitive classes regardless of editor.
# git config --global core.autocrlf false
# git add --renormalize .
# git ls-files --eol
* -text

# Exception: scripts must stay LF regardless of the `* -text` default - a CRLF shebang breaks execution. `.editorconfig`
# covers `*.sh`, but extensionless executables (s6 service scripts, hooks) match no extension rule, so pin them here so
# git enforces LF on checkout and `--renormalize`. A repo shipping extensionless scripts adds an explicit path rule,
# e.g. for s6-overlay init: `Docker/s6-overlay/** text eol=lf`.
*.sh text eol=lf

# Dockerfiles must be LF - a CRLF breaks RUN heredocs and line continuations.
Dockerfile text eol=lf
*.Dockerfile text eol=lf

# Extensionless executables must stay LF - a CRLF shebang breaks execution. The Husky.Net git hook matches no extension rule.
.husky/pre-commit text eol=lf

# LanguageData/ holds downloaded source data the parser reads byte-for-byte; never normalize it. The `* -text` default
# above preserves it exactly as downloaded - do NOT add a `text`/`eol=` rule here.
18 changes: 5 additions & 13 deletions .github/workflows/build-datebadge-task.yml
Original file line number Diff line number Diff line change
@@ -1,15 +1,10 @@
name: Build BYOB date badge task

# Caller-gated: the publisher invokes this only when main is published - the badge has no per-branch context, it tracks
# the last main build.

on:
workflow_call:
inputs:
# Logical branch this badge run is for. The badge only updates on
# `main`; the publisher passes the branch explicitly so a scheduled
# run building `develop` doesn't try to write the main badge. Required
# (no `github.ref_name` fallback) so the gate can't silently misfire.
branch:
required: true
type: string

jobs:

Expand All @@ -21,13 +16,10 @@ jobs:

- name: Get current date step
id: date
run: |
set -euo pipefail
echo "date=$(date)" >> $GITHUB_OUTPUT
run: echo "date=$(date)" >> "$GITHUB_OUTPUT"

- name: Build BYOB date badge step
if: ${{ inputs.branch == 'main' }}
uses: RubbaBoy/BYOB@a4919104bc0ec7cfd7f113e42c405cc45246f2a4 # v1
uses: RubbaBoy/BYOB@24f464284c1fd32028524b59607d417a2e36fee7 # v1.3.0
with:
name: lastbuild
label: "Last Build"
Expand Down
3 changes: 2 additions & 1 deletion .github/workflows/build-nugetlibrary-task.yml
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ jobs:
secrets: inherit
with:
ref: ${{ inputs.ref }}
branch: ${{ inputs.branch }}

build-nugetlibrary:
name: Build NuGet library project job
Expand All @@ -46,7 +47,7 @@ jobs:
steps:

- name: Setup .NET SDK step
uses: actions/setup-dotnet@9a946fdbd5fb07b82b2f5a4466058b876ab72bb2 # v5.3.0
uses: actions/setup-dotnet@26b0ec14cb23fa6904739307f278c14f94c95bf1 # v5.4.0
with:
dotnet-version: 10.x

Expand Down
77 changes: 73 additions & 4 deletions .github/workflows/build-release-task.yml
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,12 @@ name: Build project release task
on:
workflow_call:
inputs:
# Input to control whether to create a GitHub release
# Whether to create a GitHub release.
github:
required: false
type: boolean
default: false
# Input to control whether to push the library to NuGet.org
# Whether to push the library to NuGet.org.
nuget:
required: false
type: boolean
Expand All @@ -35,6 +35,12 @@ on:
required: false
type: boolean
default: true
# Set false for a repo that produces no release-asset-* files (e.g. Docker-only): the release is then just the
# tag + source zip + README + LICENSE; the artifact download is skipped and the unmatched-files guard relaxes.
expect_release_assets:
required: false
type: boolean
default: true

jobs:

Expand All @@ -44,11 +50,38 @@ jobs:
secrets: inherit
with:
ref: ${{ inputs.ref }}
branch: ${{ inputs.branch }}

# Entry gate: validate branch<->version consistency once, before the build jobs, so an NBGV mis-classification fails
# fast instead of after building and publishing. main must be a public release (no prerelease '-'); every other branch
# must carry a prerelease '-' (guards a develop leg being classified public and published as stable). Strip
# '+buildmetadata' first; a '-' there is legitimate, only a '-' in the core/prerelease segment marks a prerelease.
validate-release:
name: Validate release version job
needs: [get-version]
runs-on: ubuntu-latest
steps:
- name: Validate branch and version consistency step
env:
SEMVER2: ${{ needs.get-version.outputs.SemVer2 }}
BRANCH: ${{ inputs.branch }}
run: |
set -euo pipefail
CORE_AND_PRE="${SEMVER2%%+*}"
if [[ "$BRANCH" == "main" ]]; then
if [[ "$CORE_AND_PRE" == *-* ]]; then
echo "::error::Public (main) release version '$SEMVER2' carries a prerelease suffix; refusing to publish."
exit 1
fi
elif [[ "$CORE_AND_PRE" != *-* ]]; then
echo "::error::Prerelease ($BRANCH) version '$SEMVER2' has no prerelease suffix (NBGV classified it public); refusing to publish."
exit 1
fi

build-nugetlibrary:
name: Build NuGet library job
if: ${{ inputs.enable_nuget }}
needs: [get-version]
needs: [get-version, validate-release]
uses: ./.github/workflows/build-nugetlibrary-task.yml
secrets: inherit
with:
Expand All @@ -66,7 +99,7 @@ jobs:
# `github: true` still can't create a release.
if: ${{ inputs.github && !inputs.smoke }}
runs-on: ubuntu-latest
needs: [get-version, build-nugetlibrary]
needs: [get-version, validate-release, build-nugetlibrary]

steps:

Expand All @@ -76,7 +109,13 @@ jobs:
with:
ref: ${{ needs.get-version.outputs.GitCommitId }}

# Collect assets by the `release-asset-<branch>-*` pattern so this step is target-agnostic: subset releases by
# deleting the target, not `enable_*: false` (a skipped `needs` job would skip this release job too). The release
# step guards `fail_on_unmatched_files: true`, so at least one `release-asset-*` must match; a repo that drops
# every file-producing target (e.g. a Docker-only repo, whose release carries only source zip + README + LICENSE)
# relaxes that guard.
- name: Download release asset artifacts step
if: ${{ inputs.expect_release_assets }}
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
with:
pattern: release-asset-${{ inputs.branch }}-*
Expand Down Expand Up @@ -106,6 +145,10 @@ jobs:
# `target_commitish` must be set explicitly: otherwise GitHub's REST API tags the release on the default branch.
# Pin it to `GitCommitId` so the tag is on the exact built commit, consistent with the SemVer2 tag and artifacts.
# Skip when the release already exists, but always let a manual `workflow_dispatch` through to refresh it.
# Every release (any branch, any target) is a tag on the built commit plus the auto-attached source zip, README,
# and LICENSE; targets amend it by uploading `release-asset-*` files (binaries/packages) or pushing elsewhere
# (image/registry). `fail_on_unmatched_files: true` fails loudly if a promised `release-asset-*` is missing or
# misnamed; a no-file-target repo relaxes it (see download step).
- name: Create GitHub release step
if: ${{ steps.release-exists.outputs.exists == 'false' || github.event_name == 'workflow_dispatch' }}
uses: softprops/action-gh-release@718ea10b132b3b2eba29c1007bb80653f286566b # v3.0.1
Expand All @@ -114,7 +157,33 @@ jobs:
tag_name: ${{ needs.get-version.outputs.SemVer2 }}
target_commitish: ${{ needs.get-version.outputs.GitCommitId }}
prerelease: ${{ inputs.branch != 'main' }}
fail_on_unmatched_files: ${{ inputs.expect_release_assets }}
files: |
LICENSE
README.md
./Publish/*

# Surgical cleanup at the point of consumption: the release-asset-<branch>-* transfer artifacts now have durable
# copies on the release, so delete them by exact pattern to free the storage quota - scoped to this branch's
# assets, leaving diagnostics and any other artifacts. Gated to the same condition as the create step so it only
# deletes when a release was actually created/refreshed this run; on a skipped create (existing tag, no new
# commits) the fresh artifacts stay for the run, reaped by the retention-days: 1 backstop. Needs the caller to
# grant `actions: write` (publish-release's publish job does).
- name: Delete consumed release asset artifacts step
if: ${{ inputs.expect_release_assets && (steps.release-exists.outputs.exists == 'false' || github.event_name == 'workflow_dispatch') }}
# Best-effort: the release is already published, so a listing/delete hiccup must never red the job; the
# retention-days: 1 backstop reaps anything missed. Deletes every matching id (a rerun can upload duplicates).
continue-on-error: true
env:
GH_TOKEN: ${{ github.token }}
run: |
set -euo 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."
ids=""
fi
for id in $ids; do
gh api --method DELETE "repos/$GITHUB_REPOSITORY/actions/artifacts/$id" \
|| echo "::warning::Failed to delete artifact $id; retention-days backstop will reap it."
done
14 changes: 12 additions & 2 deletions .github/workflows/get-version-task.yml
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,13 @@ on:
required: false
type: string
default: ''
# Logical branch NBGV classifies against. Pins PublicRelease to this branch instead of the runner's GITHUB_REF,
# which on a publish dispatched from the default branch is that branch for every matrix leg. Empty keeps GITHUB_REF.
branch:
required: false
type: string
default: ''
outputs:
# Version information outputs
SemVer2:
value: ${{ jobs.get-version.outputs.SemVer2 }}
AssemblyVersion:
Expand Down Expand Up @@ -38,7 +43,7 @@ jobs:
steps:

- name: Setup .NET SDK step
uses: actions/setup-dotnet@9a946fdbd5fb07b82b2f5a4466058b876ab72bb2 # v5.3.0
uses: actions/setup-dotnet@26b0ec14cb23fa6904739307f278c14f94c95bf1 # v5.4.0
with:
dotnet-version: 10.x

Expand All @@ -53,3 +58,8 @@ jobs:
- name: Run Nerdbank.GitVersioning tool step
id: nbgv
uses: dotnet/nbgv@master
env:
# NBGV reads the branch from GITHUB_REF; pin it to the leg being versioned so a publish dispatched from the
# default branch doesn't classify every leg as the public ref and strip its prerelease suffix.
GITHUB_REF: ${{ inputs.branch != '' && format('refs/heads/{0}', inputs.branch) || github.ref }}
GITHUB_REF_NAME: ${{ inputs.branch != '' && inputs.branch || github.ref_name }}
7 changes: 4 additions & 3 deletions .github/workflows/merge-bot-pull-request.yml
Original file line number Diff line number Diff line change
Expand Up @@ -13,10 +13,11 @@ on:
pull_request_target:
types: [opened, reopened, synchronize]

# `cancel-in-progress: false` is required so events process to completion in arrival order: a follow-up
# synchronize must not cancel an in-flight `opened` run before it enables auto-merge.
# Per-PR group: under `pull_request_target` `github.ref` is the base branch, which would serialize every bot PR
# against that base; key on the PR number so each PR's events queue independently. `cancel-in-progress: false` so a
# follow-up synchronize doesn't cancel an in-flight `opened` run before it enables auto-merge.
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
group: ${{ github.workflow }}-${{ github.event.pull_request.number }}
cancel-in-progress: false

jobs:
Expand Down
38 changes: 5 additions & 33 deletions .github/workflows/publish-release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,8 @@ jobs:
secrets: inherit
permissions:
contents: write
# actions:write lets the github-release job delete the release-asset-* artifacts it consumes (surgical cleanup).
actions: write
with:
ref: ${{ matrix.branch }}
branch: ${{ matrix.branch }}
Expand All @@ -86,43 +88,13 @@ jobs:
github: true
nuget: true

# Caller-gated to main: the badge has no per-branch context, so it updates only when main is among the published
# branches (a develop-only push skips it). One invocation, not a per-branch matrix leg.
date-badge:
name: Create BYOB date badge job
needs: [setup, publish]
if: ${{ needs.setup.outputs.publish == 'true' }}
strategy:
matrix:
branch: ${{ fromJSON(needs.setup.outputs.branches) }}
if: ${{ needs.setup.outputs.publish == 'true' && contains(fromJSON(needs.setup.outputs.branches), 'main') }}
uses: ./.github/workflows/build-datebadge-task.yml
secrets: inherit
permissions:
contents: write
with:
# The badge task self-gates to `main`; the develop leg is a no-op.
branch: ${{ matrix.branch }}

# Delete the run's artifacts (durable copies live on the GitHub release) to keep them off the account storage quota.
cleanup-artifacts:
name: Delete workflow artifacts job
needs: [setup, publish, date-badge]
if: ${{ always() && needs.setup.outputs.publish == 'true' }}
runs-on: ubuntu-latest
permissions:
actions: write
steps:
- name: Delete workflow artifacts step
# Best-effort housekeeping must never fail the run.
continue-on-error: true
env:
GH_TOKEN: ${{ github.token }}
run: |
set -euo pipefail
if ! ids=$(gh api repos/${{ github.repository }}/actions/runs/${{ github.run_id }}/artifacts --paginate \
--jq '.artifacts[].id'); then
echo "::warning::Could not list run artifacts; skipping cleanup."
ids=""
fi
for artifact_id in $ids; do
gh api --method DELETE "repos/${{ github.repository }}/actions/artifacts/$artifact_id" \
|| echo "::warning::Failed to delete artifact $artifact_id; continuing."
done
2 changes: 1 addition & 1 deletion .github/workflows/run-codegen-pull-request-task.yml
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ jobs:
private-key: ${{ secrets.CODEGEN_APP_PRIVATE_KEY }}

- name: Setup .NET SDK step
uses: actions/setup-dotnet@9a946fdbd5fb07b82b2f5a4466058b876ab72bb2 # v5.3.0
uses: actions/setup-dotnet@26b0ec14cb23fa6904739307f278c14f94c95bf1 # v5.4.0
with:
dotnet-version: 10.x

Expand Down
29 changes: 1 addition & 28 deletions .github/workflows/test-pull-request.yml
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ jobs:
steps:

- name: Setup .NET SDK step
uses: actions/setup-dotnet@9a946fdbd5fb07b82b2f5a4466058b876ab72bb2 # v5.3.0
uses: actions/setup-dotnet@26b0ec14cb23fa6904739307f278c14f94c95bf1 # v5.4.0
with:
dotnet-version: 10.x

Expand Down Expand Up @@ -115,30 +115,3 @@ jobs:
# smoke-build may be legitimately skipped (library unchanged); only failure/cancelled blocks.
exit_on_result "unit-test" "${{ needs.unit-test.result }}"
exit_on_result "smoke-build" "${{ needs.smoke-build.result }}"

# Delete any incidental artifacts a build step emitted to keep them off the account storage quota. Kept out of
# `check-workflow-status`'s needs so housekeeping never gates the required merge check.
cleanup-artifacts:
name: Delete workflow artifacts job
needs: [smoke-build]
if: always()
runs-on: ubuntu-latest
permissions:
actions: write
steps:
- name: Delete workflow artifacts step
# Best-effort housekeeping must never fail the run.
continue-on-error: true
env:
GH_TOKEN: ${{ github.token }}
run: |
set -euo pipefail
if ! ids=$(gh api repos/${{ github.repository }}/actions/runs/${{ github.run_id }}/artifacts --paginate \
--jq '.artifacts[].id'); then
echo "::warning::Could not list run artifacts; skipping cleanup."
ids=""
fi
for artifact_id in $ids; do
gh api --method DELETE "repos/${{ github.repository }}/actions/artifacts/$artifact_id" \
|| echo "::warning::Failed to delete artifact $artifact_id; continuing."
done
Loading