From c5901d15f108ba443567471507b47d015b9ca839 Mon Sep 17 00:00:00 2001 From: aicia-bot Date: Thu, 15 Oct 2026 19:35:45 +0200 Subject: [PATCH 1/2] =?UTF-8?q?=F0=9F=91=B7=20add=20manual=20prerelease=20?= =?UTF-8?q?publishing=20workflow?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Long-lived .NET branches need signed NuGet prereleases without a main-only release tag. The new prerelease flow verifies the tagged branch commit and publishes the same validated Release artifact, while release.yml ignores prerelease tags. --- .github/workflows/prerelease.yml | 155 +++++++++++++++++++++++++++++++ .github/workflows/release.yml | 1 + .github/workflows/verify.yml | 34 ++++++- 3 files changed, 185 insertions(+), 5 deletions(-) create mode 100644 .github/workflows/prerelease.yml diff --git a/.github/workflows/prerelease.yml b/.github/workflows/prerelease.yml new file mode 100644 index 00000000..3a47dc26 --- /dev/null +++ b/.github/workflows/prerelease.yml @@ -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 diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 62b2f589..43f6029d 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -3,6 +3,7 @@ on: push: tags: - 'v*' + - '!v*-*' # Branch prereleases are published manually through prerelease.yml. workflow_dispatch: inputs: tag: diff --git a/.github/workflows/verify.yml b/.github/workflows/verify.yml index 8c51eb76..b79fb2a5 100644 --- a/.github/workflows/verify.yml +++ b/.github/workflows/verify.yml @@ -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. @@ -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 @@ -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 @@ -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" ) || @@ -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" @@ -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 }} @@ -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 }} From 87ee3a37da8eadf1b6e616e65bc15bf5f2ecc31b Mon Sep 17 00:00:00 2001 From: aicia-bot Date: Thu, 15 Oct 2026 19:35:46 +0200 Subject: [PATCH 2/2] =?UTF-8?q?=F0=9F=93=9D=20document=20branch=20prerelea?= =?UTF-8?q?se=20publication?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Maintainers need to know when to use the prerelease flow and which Production deployment rules it depends on, so the release section now separates prerelease and main release steps. --- .github/CONTRIBUTING.md | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/.github/CONTRIBUTING.md b/.github/CONTRIBUTING.md index c5f96e2f..6a356f9b 100644 --- a/.github/CONTRIBUTING.md +++ b/.github/CONTRIBUTING.md @@ -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. @@ -73,7 +74,19 @@ Package-specific release notes live under `.nuget//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.