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
15 changes: 14 additions & 1 deletion .github/CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ This repository is part of the Codebelt .NET library estate. The instructions be
- `test/` contains xUnit v3 test projects.
- `Cuemon.slnx` is the solution used for local development.
- `.github/workflows/pr.yml` owns the PR test matrix and blocking quality gates.
- `.github/workflows/prerelease.yml` publishes tagged NuGet prereleases from long-lived .NET branches through a manual run.
- `.github/workflows/release.yml` publishes packages from a version-tagged commit in `main` history; post-release assurance and DocFX production run after NuGet publication.
- `.github/workflows/deploy.yml` promotes the published DocFX image without rebuilding it. See [Release and deployment](#release-and-deployment) for the CI/CD handoff.
- `testenvironments.json` declares the supported `WSL-Ubuntu` and `Docker-Ubuntu` test environments.
Expand Down Expand Up @@ -73,7 +74,19 @@ Package-specific release notes live under `.nuget/<ProjectName>/PackageReleaseNo

## Release and deployment

After PR validation and merge, a maintainer creates and pushes a `vX.Y.Z` tag (or `vX.Y.Z-prerelease`, without build metadata) for the intended commit in `main` history. The tag push starts `release.yml`, which checks the tag identity and ancestry, builds signed Release packages from that commit, validates their versions and existing NuGet content, and sends the validated package artifact to the protected `Production` publication job.
### Prereleases from long-lived .NET branches

Use **.NET Prerelease Flow** (`prerelease.yml`) for Preview and Release Candidate packages developed on long-lived branches, such as `v11.0.0/double-digits` and future .NET 12 branches. Keep this workflow and its shared verification inputs/outputs on `main` and in each development branch. GitHub requires the workflow's manual trigger on the default branch before it appears in the Actions UI. The existing `Production` environment's deployment branch rules must also allow the intended development branches.

Tag the intended branch tip and push the branch and tag, for example `v11.0.0-rc.2`. In **Actions → .NET Prerelease Flow → Run workflow**, select that development branch. The workflow checks out the triggering commit, requires an existing SemVer prerelease tag at that commit, and requires MinVer's calculated version to match the tag. The same flow accepts future versions such as `v12.0.0-preview.1`; no workflow version changes are required.

Shared verification builds signed Debug and Release packages, runs the Linux and Windows test matrices and SQL integration tests, and requires SonarCloud and Codecov success. macOS is optional. CodeQL follows the shared workflow's explicit support policy. Analysis retains the selected development branch identity rather than reporting canonical main. After verification, package validation checks the existing `Verification-NuGet-Release` artifact before the protected `Production` job publishes that same artifact to NuGet. No packages are rebuilt for publication.

This workflow does not create tags, GitHub Releases, DocFX release assets, or deployments. Prerelease tag pushes do not start the main-only release flow. If a NuGet push partially succeeds, rerun its failed publication job to reuse the validated artifact; do not move the tag or regenerate replacement packages.

### Releases from main

After PR validation and merge, a maintainer creates and pushes a `vX.Y.Z` tag for the intended commit in `main` history. The tag push starts `release.yml`, which checks the tag identity and ancestry, builds signed Release packages from that commit, validates their versions and existing NuGet content, and sends the validated package artifact to the protected `Production` publication job.

After NuGet publication, the workflow runs post-release tests and analysis and builds the multi-platform DocFX OCI image from the same commit. The verified archive and SHA-256 checksum are attached to a draft GitHub Release using its numeric release ID. The workflow leaves the release as a draft. A maintainer reviews/edits the release and assurance results, then presses **Publish** to declare it deployable. Post-release assurance failures do not roll back published packages; inspect the release summary and resolve failures against its recorded commit.

Expand Down
155 changes: 155 additions & 0 deletions .github/workflows/prerelease.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,155 @@
name: .NET Prerelease Flow
on:
workflow_dispatch:
inputs:
run_mac_tests:
type: boolean
description: Run the macOS test matrix despite the additional cost and runtime.
default: false

permissions:
contents: read

concurrency:
group: cuemon-prerelease-${{ github.ref }}
cancel-in-progress: false

jobs:
prerelease_preflight:
name: Validate tagged prerelease branch
runs-on: ubuntu-26.04
timeout-minutes: 5
outputs:
version: ${{ steps.validate.outputs.version }}
tag: ${{ steps.validate.outputs.tag }}
sha: ${{ steps.validate.outputs.sha }}
steps:
- name: Checkout the selected branch's triggering commit
uses: codebeltnet/git-checkout@v1
with:
ref: ${{ github.sha }}

- id: validate
name: Validate the existing prerelease tag
shell: bash
env:
SOURCE_REF: ${{ github.ref }}
SOURCE_SHA: ${{ github.sha }}
run: |
set -euo pipefail
if [[ "$GITHUB_EVENT_NAME" != "workflow_dispatch" || "$SOURCE_REF" != refs/heads/* || "$SOURCE_REF" == "refs/heads/main" ]]; then
echo '::error::Select the long-lived .NET branch when manually running this workflow; main uses release.yml.'
exit 1
fi
if [[ "$(git rev-parse HEAD)" != "$SOURCE_SHA" ]]; then
echo '::error::Checkout does not match the selected branch commit.'
exit 1
fi
if ! tag="$(git describe --tags --exact-match --match 'v*-*' HEAD)"; then
echo '::error::Tag and push the selected branch tip first, for example v11.0.0-rc.2 or v12.0.0-preview.1.'
exit 1
fi
version="${tag#v}"
prerelease_regex='^(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)-((0|[1-9][0-9]*|[0-9A-Za-z-]*[A-Za-z-][0-9A-Za-z-]*)(\.(0|[1-9][0-9]*|[0-9A-Za-z-]*[A-Za-z-][0-9A-Za-z-]*))*)$'
if [[ ! "$version" =~ $prerelease_regex ]]; then
echo "::error::Tag '$tag' must be a SemVer prerelease without build metadata."
exit 1
fi
{
echo "version=$version"
echo "tag=$tag"
echo "sha=$SOURCE_SHA"
} >> "$GITHUB_OUTPUT"

verification:
name: Verify prerelease packages
needs: [prerelease_preflight]
uses: ./.github/workflows/verify.yml
with:
ref: ${{ needs.prerelease_preflight.outputs.sha }}
version: ${{ needs.prerelease_preflight.outputs.version }}
prerelease: true
run-mac-tests: ${{ inputs.run_mac_tests }}
permissions:
contents: read
security-events: write
secrets:
GCP_TOKEN: ${{ secrets.GCP_TOKEN }}
GCP_BUCKETNAME: ${{ secrets.GCP_BUCKETNAME }}
SA_PASSWORD: ${{ secrets.SA_PASSWORD }}
DB_ADVENTUREWORKS: ${{ secrets.DB_ADVENTUREWORKS }}
SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}

validate_packages:
name: Validate the verified Release package artifact
needs: [prerelease_preflight, verification]
runs-on: ubuntu-26.04
timeout-minutes: 15
steps:
- name: Install .NET including prerelease SDKs
uses: codebeltnet/install-dotnet@v3
with:
includePreview: true

- name: Download verified Release packages
uses: actions/download-artifact@v8
with:
name: Verification-NuGet-Release
path: ${{ runner.temp }}/.nuget

- name: Validate package versions and existing NuGet content
uses: codebeltnet/nuget-release-validate@v1
with:
package-directory: ${{ runner.temp }}/.nuget
version: ${{ needs.prerelease_preflight.outputs.version }}

publish_nuget:
name: Publish NuGet v${{ needs.prerelease_preflight.outputs.version }}
needs: [prerelease_preflight, verification, validate_packages]
uses: codebeltnet/jobs-nuget-push/.github/workflows/default.yml@v3
with:
version: ${{ needs.prerelease_preflight.outputs.version }}
environment: Production
configuration: Release
download-build-artifact-name: Verification-NuGet-Release
permissions:
contents: read
packages: write
secrets:
NUGET_TOKEN: ${{ secrets.NUGET_TOKEN }}

prerelease_summary:
if: ${{ always() }}
name: Prerelease publication status
needs: [prerelease_preflight, verification, validate_packages, publish_nuget]
runs-on: ubuntu-26.04
timeout-minutes: 5
steps:
- name: Report prerelease results
shell: bash
env:
SOURCE_BRANCH: ${{ github.ref_name }}
SOURCE_SHA: ${{ needs.prerelease_preflight.outputs.sha }}
RELEASE_TAG: ${{ needs.prerelease_preflight.outputs.tag }}
RELEASE_VERSION: ${{ needs.verification.outputs.version }}
PREFLIGHT_RESULT: ${{ needs.prerelease_preflight.result }}
VERIFICATION_RESULT: ${{ needs.verification.result }}
VALIDATION_RESULT: ${{ needs.validate_packages.result }}
PUBLICATION_RESULT: ${{ needs.publish_nuget.result }}
run: |
set -euo pipefail
{
echo '## .NET prerelease publication'
printf -- '- Branch: `%s`\n' "$SOURCE_BRANCH"
printf -- '- Tag: `%s`; commit: `%s`\n' "$RELEASE_TAG" "$SOURCE_SHA"
printf -- '- MinVer version: `%s`\n' "$RELEASE_VERSION"
printf -- '- Preflight: `%s`; verification: `%s`; package validation: `%s`; NuGet publication: `%s`\n' \
"$PREFLIGHT_RESULT" "$VERIFICATION_RESULT" "$VALIDATION_RESULT" "$PUBLICATION_RESULT"
echo '- Shared verification reports analysis coverage and any CodeQL exception.'
} >> "$GITHUB_STEP_SUMMARY"
if [[ "$PREFLIGHT_RESULT" != "success" || "$VERIFICATION_RESULT" != "success" ||
"$VALIDATION_RESULT" != "success" || "$PUBLICATION_RESULT" != "success" ]]; then
echo '::error::Prerelease publication did not complete. If a NuGet push partially succeeded, rerun the failed publication job to reuse its validated artifact.'
exit 1
fi
1 change: 1 addition & 0 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ on:
push:
tags:
- 'v*'
- '!v*-*' # Branch prereleases are published manually through prerelease.yml.
workflow_dispatch:
inputs:
tag:
Expand Down
34 changes: 29 additions & 5 deletions .github/workflows/verify.yml
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,12 @@ on:
default: ''
version:
type: string
description: Released SemVer for canonical main; empty uses the build's MinVer version.
description: Expected SemVer for release verification; empty uses the build's MinVer version.
default: ''
prerelease:
type: boolean
description: Verify signed packages for manual prerelease publication from a long-lived branch.
default: false
canonical-main:
type: boolean
description: Report main health at ref rather than the triggering PR identity.
Expand All @@ -18,6 +22,10 @@ on:
type: boolean
description: Opt in to the macOS matrix, consistently for PR and assurance replay.
default: false
outputs:
version:
description: The MinVer version of the verified packages.
value: ${{ jobs.build.outputs.version }}
secrets:
GCP_TOKEN:
required: false
Expand All @@ -35,7 +43,7 @@ on:
permissions:
contents: read

# One verification definition: production packages and immutable assets are caller-owned.
# One verification definition; prerelease publication reuses its verified Release package artifact.
jobs:
init:
name: initialize
Expand All @@ -58,8 +66,23 @@ jobs:
CANONICAL_MAIN: ${{ inputs.canonical-main }}
SOURCE_REF: ${{ inputs.ref }}
RELEASE_VERSION: ${{ inputs.version }}
PRERELEASE: ${{ inputs.prerelease }}
WORKFLOW_SHA: ${{ github.sha }}
run: |
set -euo pipefail
if [[ "$PRERELEASE" == "true" ]]; then
if [[ "$EVENT_NAME" != "workflow_dispatch" || "$WORKFLOW_REF" != refs/heads/* ||
"$WORKFLOW_REF" == "refs/heads/main" || "$SOURCE_REF" != "$WORKFLOW_SHA" ||
"$CANONICAL_MAIN" == "true" || -z "$RELEASE_VERSION" ]]; then
echo '::error::Prerelease verification requires a manual non-main branch run at its triggering SHA and an expected version.'
exit 1
fi
# The branch name is passed as a scanner command-line parameter below.
if [[ ! "${WORKFLOW_REF#refs/heads/}" =~ ^[0-9A-Za-z._/-]+$ ]]; then
echo '::error::Prerelease analysis branch names may contain letters, digits, dots, underscores, slashes and hyphens.'
exit 1
fi
fi
if [[ "$CANONICAL_MAIN" == "true" ]]; then
if [[ ! "$SOURCE_REF" =~ ^[0-9a-f]{40}$ || -z "$RELEASE_VERSION" ||
( "$EVENT_NAME" != "push" && "$EVENT_NAME" != "workflow_dispatch" ) ||
Expand All @@ -86,7 +109,7 @@ jobs:
echo "build-switches=" >> "$GITHUB_OUTPUT"
fi

if [[ "$EVENT_NAME" == "workflow_dispatch" && "$WORKFLOW_REF" != "refs/heads/main" ]]; then
if [[ "$EVENT_NAME" == "workflow_dispatch" && "$WORKFLOW_REF" != "refs/heads/main" && "$PRERELEASE" != "true" ]]; then
echo "run-privileged-jobs=false" >> "$GITHUB_OUTPUT"
echo "strong-name-key-filename=" >> "$GITHUB_OUTPUT"
echo "build-switches=-p:SkipSignAssembly=true" >> "$GITHUB_OUTPUT"
Expand Down Expand Up @@ -424,6 +447,7 @@ jobs:
parameters: >-
-d:sonar.exclusions='**/obj/**,**/bin/**'
${{ inputs.canonical-main && format('-d:sonar.branch.name=main -d:sonar.scm.revision={0}', inputs.ref) || '' }}
${{ inputs.prerelease && format('-d:sonar.branch.name={0} -d:sonar.scm.revision={1}', github.ref_name, inputs.ref) || '' }}
secrets:
SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}

Expand All @@ -436,8 +460,8 @@ jobs:
repository: codebeltnet/cuemon
configuration: .github/codecov.yml
ref: ${{ inputs.ref }}
branch: ${{ inputs.canonical-main && 'main' || '' }}
commit: ${{ inputs.canonical-main && inputs.ref || '' }}
branch: ${{ inputs.canonical-main && 'main' || inputs.prerelease && github.ref_name || '' }}
commit: ${{ (inputs.canonical-main || inputs.prerelease) && inputs.ref || '' }}
fail-on-error: true
secrets:
CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}
Expand Down
Loading