From d0c025c4a46b1566f2a5b233e529781a70e33bb6 Mon Sep 17 00:00:00 2001 From: aicia-bot Date: Sun, 4 Oct 2026 19:30:46 +0200 Subject: [PATCH 1/4] =?UTF-8?q?=F0=9F=91=B7=20extract=20shared=20verificat?= =?UTF-8?q?ion=20workflow=20for=20pr=20and=20release?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Consolidate duplicated build, test and analysis jobs into a reusable verify workflow so PR and release flows share one verification definition while keeping authoritative release products caller-owned. --- .github/workflows/pr.yml | 438 ++-------------------------- .github/workflows/release.yml | 393 ++++++------------------- .github/workflows/verify.yml | 522 ++++++++++++++++++++++++++++++++++ 3 files changed, 619 insertions(+), 734 deletions(-) create mode 100644 .github/workflows/verify.yml diff --git a/.github/workflows/pr.yml b/.github/workflows/pr.yml index 653fd1d4..a02da09a 100644 --- a/.github/workflows/pr.yml +++ b/.github/workflows/pr.yml @@ -13,441 +13,35 @@ permissions: contents: read jobs: - init: - name: initialize - runs-on: ubuntu-26.04 - outputs: - run-privileged-jobs: ${{ steps.vars.outputs.run-privileged-jobs }} - run-mac-tests: ${{ steps.vars.outputs.run-mac-tests }} - strong-name-key-filename: ${{ steps.vars.outputs.strong-name-key-filename }} - build-switches: ${{ steps.vars.outputs.build-switches }} - steps: - - id: vars - name: calculate workflow variables - shell: bash - env: - EVENT_NAME: ${{ github.event_name }} - HEAD_REPOSITORY: ${{ github.event.pull_request.head.repo.full_name }} - REPOSITORY: ${{ github.repository }} - WORKFLOW_REF: ${{ github.ref }} - RUN_MAC_TESTS: ${{ inputs.run_mac_tests }} - run: | - if [[ "$EVENT_NAME" == "workflow_dispatch" && "$RUN_MAC_TESTS" == "true" ]]; then - echo "run-mac-tests=true" >> "$GITHUB_OUTPUT" - else - echo "run-mac-tests=false" >> "$GITHUB_OUTPUT" - fi - - if [[ "$EVENT_NAME" == "pull_request" && "$HEAD_REPOSITORY" != "$REPOSITORY" ]]; then - echo "run-privileged-jobs=false" >> "$GITHUB_OUTPUT" - echo "strong-name-key-filename=" >> "$GITHUB_OUTPUT" - echo "build-switches=-p:SkipSignAssembly=true" >> "$GITHUB_OUTPUT" - else - echo "run-privileged-jobs=true" >> "$GITHUB_OUTPUT" - echo "strong-name-key-filename=cuemon.snk" >> "$GITHUB_OUTPUT" - echo "build-switches=" >> "$GITHUB_OUTPUT" - fi - - if [[ "$EVENT_NAME" == "workflow_dispatch" && "$WORKFLOW_REF" != "refs/heads/main" ]]; then - echo "run-privileged-jobs=false" >> "$GITHUB_OUTPUT" - echo "strong-name-key-filename=" >> "$GITHUB_OUTPUT" - echo "build-switches=-p:SkipSignAssembly=true" >> "$GITHUB_OUTPUT" - fi - - prepare_test: - name: 📜 Prepare Test - runs-on: ubuntu-26.04 - timeout-minutes: 5 - outputs: - json: ${{ steps.test-projects.outputs.result }} - steps: - - name: Checkout - uses: codebeltnet/git-checkout@v1 - - - id: test-projects - name: Generate matrix for test projects - uses: codebeltnet/shell-globbing@v2 - with: - pattern: | - test/**/*.csproj - !test/**/Cuemon.Data.SqlClient.Tests.csproj - - - name: JSON output - run: echo "${{ steps.test-projects.outputs.result }}" - - build: - name: call-build - needs: [init] - strategy: - matrix: - arch: [X64, ARM64] - configuration: [Debug, Release] - uses: codebeltnet/jobs-dotnet-build/.github/workflows/default.yml@v3 + verification: + name: PR proof + uses: ./.github/workflows/verify.yml with: - configuration: ${{ matrix.configuration }} - strong-name-key-filename: ${{ needs.init.outputs.strong-name-key-filename }} - build-switches: ${{ needs.init.outputs.build-switches }} - runs-on: ${{ matrix.arch == 'ARM64' && 'ubuntu-26.04-arm' || 'ubuntu-26.04' }} - upload-build-artifact-name: build-${{ matrix.configuration }}-${{ matrix.arch }} + run-mac-tests: ${{ github.event_name == 'workflow_dispatch' && inputs.run_mac_tests }} + permissions: + contents: read + security-events: write secrets: GCP_TOKEN: ${{ secrets.GCP_TOKEN }} GCP_BUCKETNAME: ${{ secrets.GCP_BUCKETNAME }} - - pack: - name: call-pack - needs: [build] - strategy: - matrix: - configuration: [Debug, Release] - uses: codebeltnet/jobs-dotnet-pack/.github/workflows/default.yml@v3 - with: - configuration: ${{ matrix.configuration }} - version: ${{ needs.build.outputs.version }} - download-build-artifact-pattern: build-${{ matrix.configuration }}-X64 - - - test_linux: - name: call-test-linux - needs: [build, prepare_test] - strategy: - fail-fast: false - matrix: - configuration: [Debug, Release] - project: ${{ fromJson(needs.prepare_test.outputs.json) }} - arch: [X64, ARM64] - uses: codebeltnet/jobs-dotnet-test/.github/workflows/default.yml@v3 - with: - runs-on: ${{ matrix.arch == 'ARM64' && 'ubuntu-26.04-arm' || 'ubuntu-26.04' }} - configuration: ${{ matrix.configuration }} - build-switches: -p:SkipSignAssembly=true - projects: ${{ matrix.project }} - build: true # we need to build due to xUnitv3 - restore: true # we need to restore since we disabled caching - download-pattern: build-${{ matrix.configuration }}-${{ matrix.arch }} - - test_windows: - name: call-test-windows - needs: [build, prepare_test] - strategy: - fail-fast: false - matrix: - arch: [X64, ARM64] - configuration: [Debug, Release] - project: ${{ fromJson(needs.prepare_test.outputs.json) }} - uses: codebeltnet/jobs-dotnet-test/.github/workflows/default.yml@v3 - with: - runs-on: ${{ matrix.arch == 'ARM64' && 'windows-11-arm' || 'windows-2025' }} - configuration: ${{ matrix.configuration }} - build-switches: -p:SkipSignAssembly=true - projects: ${{ matrix.project }} - build: true # we need to build for .net48 - restore: true # apparently we need to restore for .net48 - download-pattern: build-${{ matrix.configuration }}-${{ matrix.arch }} - - test_mac: - if: ${{ needs.init.outputs.run-mac-tests == 'true' }} - name: call-test-mac - needs: [init, build, prepare_test] - strategy: - fail-fast: false - matrix: - arch: [X64, ARM64] - configuration: [Debug, Release] - project: ${{ fromJson(needs.prepare_test.outputs.json) }} - uses: codebeltnet/jobs-dotnet-test/.github/workflows/default.yml@v3 - with: - runs-on: ${{ matrix.arch == 'ARM64' && 'macos-26' || 'macos-26-intel' }} - configuration: ${{ matrix.configuration }} - build-switches: -p:SkipSignAssembly=true - projects: ${{ matrix.project }} - build: true # we need to build due to xUnitv3 - restore: true # we need to restore since we disabled caching - download-pattern: build-${{ matrix.configuration }}-${{ matrix.arch }} - - integration_test: - if: ${{ needs.init.outputs.run-privileged-jobs == 'true' }} - name: ⚗️ Integration Test - needs: [init, build] - strategy: - fail-fast: false - matrix: - configuration: [Debug, Release] - project: [ test/**/Cuemon.Data.SqlClient.Tests.csproj ] - runs-on: ubuntu-26.04 - timeout-minutes: 15 - steps: - - name: Checkout - uses: codebeltnet/git-checkout@v1 - - - name: Install .NET - uses: codebeltnet/install-dotnet@v3 - - - name: Install .NET Tool - Report Generator - uses: codebeltnet/dotnet-tool-install-reportgenerator@v1 - - - name: Spin up SQL Server test dependency for ${{ matrix.configuration }} build - uses: codebeltnet/docker-compose@v1 - with: - command: up - options: --wait - env: - SA_PASSWORD: ${{ secrets.SA_PASSWORD }} - - - name: Download Build Artifacts - uses: actions/download-artifact@v8 - with: - pattern: build-${{ matrix.configuration }}-X64 - merge-multiple: true - - - name: Fix Linux test apphost permissions - run: | - set -euo pipefail - - echo "=== Context ===" - echo "Runner: $RUNNER_OS / $RUNNER_ARCH" - echo "Configuration: ${{ matrix.configuration }}" - echo "Workspace: $GITHUB_WORKSPACE" - echo "PWD: $(pwd)" - echo "Event: $GITHUB_EVENT_NAME" - echo "Ref: $GITHUB_REF" - echo "SHA: $GITHUB_SHA" - echo - - echo "=== .NET info ===" - dotnet --info || true - echo - - echo "=== Git state ===" - git rev-parse HEAD || true - git status --porcelain || true - git rev-parse --is-shallow-repository || true - echo - - # Paths we care about - BIN_GLOB="*/bin/*/net*/*" - OBJ_GLOB="*/obj/*/net*/*" - - echo "=== Brute-force chmod (bin + obj) ===" - find . -type f \( -path "$BIN_GLOB" -o -path "$OBJ_GLOB" \) -exec chmod a+x {} + 2>/dev/null || true - echo "chmod completed (errors ignored)." - echo - - echo "=== Mount options (look for noexec) ===" - # If binaries live on a noexec mount, chmod won't help. - mount | sed -n '1,200p' || true - echo - - echo "=== Candidate executables (top 200) ===" - # Show what we might execute; exclude obvious managed files - find . -type f -path "$BIN_GLOB" \ - ! -name "*.dll" ! -name "*.pdb" ! -name "*.json" ! -name "*.xml" \ - -printf "%m %u:%g %s %p\n" | head -n 200 || true - echo - - echo "=== Likely xUnit / test hosts (if present) ===" - # These names vary; do not rely on just *Tests* - find . -type f -path "$BIN_GLOB" \( \ - -name "testhost*" -o \ - -name "*xunit*" -o \ - -name "*Tests*" -o \ - -name "*.runsettings" \ - \) -printf "%m %u:%g %s %p\n" | head -n 200 || true - echo - - echo "=== Deep diagnostics for any 'testhost' or apphost candidates ===" - # For each likely executable, show the facts that explain 'permission denied' vs 'exec format error' - while IFS= read -r f; do - echo "--- $f ---" - ls -la "$f" || true - - # Identify file type and architecture - file -L "$f" || true - - # If it's an ELF binary, show its dynamic interpreter and linked libs (exec format errors often show up here) - if file -L "$f" | grep -q "ELF"; then - echo "readelf -l (interpreter):" - readelf -l "$f" 2>/dev/null | sed -n '1,80p' || true - echo "ldd (dependencies):" - ldd "$f" 2>/dev/null || true - fi - - # If it's a script, CRLF in the shebang can cause 'Exec format error' - if head -c 2 "$f" 2>/dev/null | grep -q "#!"; then - echo "shebang:" - head -n 1 "$f" | cat -A || true - fi - - echo - done < <( - find . -type f -path "$BIN_GLOB" \( \ - -name "testhost*" -o \ - -name "*xunit*" -o \ - -name "*Tests*" \ - \) | head -n 50 - ) || true - - echo "=== Done diagnostics step ===" - shell: bash - - - name: Test with ${{ matrix.configuration }} build - uses: codebeltnet/dotnet-test@v4 - with: - projects: ${{ matrix.project }} - configuration: ${{ matrix.configuration }} - build: true # apparently we need to due to xUnitv3 - restore: true # we need to restore since we disabled caching - env: - CONNECTIONSTRINGS__ADVENTUREWORKS: ${{ secrets.DB_ADVENTUREWORKS }} - - - name: Upload Integration Test Results - if: always() - uses: actions/upload-artifact@v7 - with: - name: IntegrationTestResults-${{ matrix.configuration }} - path: ${{ runner.temp }}/TestResults - - - name: Take down SQL Server test dependency for ${{ matrix.configuration }} build - if: always() - uses: codebeltnet/docker-compose@v1 - with: - command: down - - test_qualitygate: - if: ${{ always() }} - name: test-qualitygate - needs: [init, test_linux, test_windows, test_mac, integration_test] - runs-on: ubuntu-26.04 - steps: - - name: Evaluate test results - shell: bash - env: - RUN_MAC_TESTS: ${{ needs.init.outputs.run-mac-tests }} - RUN_PRIVILEGED_JOBS: ${{ needs.init.outputs.run-privileged-jobs }} - TEST_LINUX_RESULT: ${{ needs.test_linux.result }} - TEST_WINDOWS_RESULT: ${{ needs.test_windows.result }} - TEST_MAC_RESULT: ${{ needs.test_mac.result }} - INTEGRATION_TEST_RESULT: ${{ needs.integration_test.result }} - run: | - require_success() { - local job_name="$1" - local job_result="$2" - - if [[ "$job_result" != "success" ]]; then - echo "::error::$job_name finished with '$job_result'." - exit 1 - fi - } - - require_success_or_skip() { - local job_name="$1" - local job_enabled="$2" - local job_result="$3" - - if [[ "$job_enabled" == "true" ]]; then - require_success "$job_name" "$job_result" - return - fi - - if [[ "$job_result" != "success" && "$job_result" != "skipped" ]]; then - echo "::error::$job_name finished with '$job_result' while disabled." - exit 1 - fi - } - - require_success "test_linux" "$TEST_LINUX_RESULT" - require_success "test_windows" "$TEST_WINDOWS_RESULT" - require_success_or_skip "test_mac" "$RUN_MAC_TESTS" "$TEST_MAC_RESULT" - require_success_or_skip "integration_test" "$RUN_PRIVILEGED_JOBS" "$INTEGRATION_TEST_RESULT" - - sonarcloud: - if: ${{always() && needs.init.outputs.run-privileged-jobs == 'true' && needs.build.result == 'success' && needs.test_qualitygate.result == 'success'}} - name: call-sonarcloud - needs: [init, build, test_qualitygate] - uses: codebeltnet/jobs-sonarcloud/.github/workflows/default.yml@v3 - with: - organization: geekle - projectKey: Cuemon - version: ${{ needs.build.outputs.version }} - secrets: + SA_PASSWORD: ${{ secrets.SA_PASSWORD }} + DB_ADVENTUREWORKS: ${{ secrets.DB_ADVENTUREWORKS }} SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} - - codecov: - if: ${{always() && needs.init.outputs.run-privileged-jobs == 'true' && needs.build.result == 'success' && needs.test_qualitygate.result == 'success'}} - name: call-codecov - needs: [init, build, test_qualitygate] - uses: codebeltnet/jobs-codecov/.github/workflows/default.yml@v1 - with: - repository: codebeltnet/cuemon - configuration: .github/codecov.yml - secrets: CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} - codeql: - if: ${{always() && needs.init.outputs.run-privileged-jobs == 'true' && needs.build.result == 'success' && needs.test_qualitygate.result == 'success'}} - name: call-codeql - needs: [init, build, test_qualitygate] - uses: codebeltnet/jobs-codeql/.github/workflows/default.yml@v3 - with: - timeout-minutes: 30 - permissions: - contents: read - security-events: write - + # Keep the existing aggregate required-check name for branch protection. pr_quality_gate: if: ${{ always() }} name: PR quality gate - needs: [init, build, pack, test_qualitygate, sonarcloud, codecov, codeql] + needs: [verification] runs-on: ubuntu-26.04 steps: - - name: Evaluate PR proof results + - name: Evaluate PR proof shell: bash env: - RUN_PRIVILEGED_JOBS: ${{ needs.init.outputs.run-privileged-jobs }} - BUILD_RESULT: ${{ needs.build.result }} - PACK_RESULT: ${{ needs.pack.result }} - TEST_RESULT: ${{ needs.test_qualitygate.result }} - SONAR_RESULT: ${{ needs.sonarcloud.result }} - CODECOV_RESULT: ${{ needs.codecov.result }} - CODEQL_RESULT: ${{ needs.codeql.result }} + VERIFICATION_RESULT: ${{ needs.verification.result }} run: | - set -euo pipefail - - require_success() { - local job_name="$1" - local job_result="$2" - - if [[ "$job_result" != "success" ]]; then - echo "::error::$job_name finished with '$job_result'." - exit 1 - fi - } - - require_skipped() { - local job_name="$1" - local job_result="$2" - - if [[ "$job_result" != "skipped" ]]; then - echo "::error::$job_name finished with '$job_result' for a run that is not authorized to use privileged integrations." - exit 1 - fi - } - - require_success "build" "$BUILD_RESULT" - require_success "pack" "$PACK_RESULT" - require_success "test_qualitygate" "$TEST_RESULT" - - if [[ "$RUN_PRIVILEGED_JOBS" == "true" ]]; then - require_success "sonarcloud" "$SONAR_RESULT" - require_success "codecov" "$CODECOV_RESULT" - require_success "codeql" "$CODEQL_RESULT" - else - require_skipped "sonarcloud" "$SONAR_RESULT" - require_skipped "codecov" "$CODECOV_RESULT" - require_skipped "codeql" "$CODEQL_RESULT" + if [[ "$VERIFICATION_RESULT" != "success" ]]; then + echo "::error::PR verification finished with '$VERIFICATION_RESULT'." + exit 1 fi - - { - echo "## PR proof passed" - echo - echo "Build and package validation passed. Required Linux and Windows tests passed; optional macOS and trusted-repository checks followed the workflow inputs and fork policy." - } >> "$GITHUB_STEP_SUMMARY" diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 4a4ee7f3..33098fac 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -11,7 +11,7 @@ on: required: true source_run_id: type: string - description: GitHub Actions release run containing the already-built release artifacts. + description: Original tag-push release run anchoring the already-published SHA (and OCI artifact for assets). required: true recovery: type: choice @@ -21,6 +21,10 @@ on: options: - assets - assurance + run_mac_tests: + type: boolean + description: Opt in to the same macOS verification matrix as PR (assurance replay only). + default: false permissions: contents: read @@ -171,66 +175,68 @@ jobs: printf -- '- Released SHA: `%s`\n' "$RELEASE_SHA" } >> "$GITHUB_STEP_SUMMARY" - release_packages: + # Release produces authoritative products before reproducing PR proof. + release_build: if: ${{ github.event_name == 'push' }} - name: Build and validate Release packages + name: Build authoritative Release products needs: [release_preflight] - runs-on: ubuntu-26.04 - timeout-minutes: 45 - permissions: - contents: read - steps: - - name: Checkout the released SHA - uses: codebeltnet/git-checkout@v1 - with: - ref: ${{ needs.release_preflight.outputs.sha }} - - - name: Install .NET - uses: codebeltnet/install-dotnet@v3 + uses: codebeltnet/jobs-dotnet-build/.github/workflows/default.yml@v3 + with: + ref: ${{ needs.release_preflight.outputs.sha }} + configuration: Release + runs-on: ubuntu-26.04 + strong-name-key-filename: cuemon.snk + upload-build-artifact-name: release-build-Release-X64 + timeout-minutes: 45 + secrets: + GCP_TOKEN: ${{ secrets.GCP_TOKEN }} + GCP_BUCKETNAME: ${{ secrets.GCP_BUCKETNAME }} - - name: Download the strong-name signing key - uses: codebeltnet/gcp-download-file@v1 - with: - serviceAccountKey: ${{ secrets.GCP_TOKEN }} - bucketName: ${{ secrets.GCP_BUCKETNAME }} - objectName: cuemon.snk + release_pack: + if: ${{ github.event_name == 'push' }} + name: Pack authoritative Release products + needs: [release_preflight, release_build] + uses: codebeltnet/jobs-dotnet-pack/.github/workflows/default.yml@v3 + with: + ref: ${{ needs.release_preflight.outputs.sha }} + configuration: Release + version: ${{ needs.release_build.outputs.version }} + download-build-artifact-pattern: release-build-Release-X64 + upload-packed-artifact-name: NuGet-Release + timeout-minutes: 45 - - name: Verify signing key is present + release_packages: + if: ${{ github.event_name == 'push' }} + name: Validate authoritative Release packages + needs: [release_preflight, release_build, release_pack] + runs-on: ubuntu-26.04 + timeout-minutes: 15 + steps: + - name: Verify MinVer agrees with the existing tag shell: bash + env: + RELEASE_VERSION: ${{ needs.release_preflight.outputs.version }} + BUILD_VERSION: ${{ needs.release_build.outputs.version }} run: | - set -euo pipefail - if [[ ! -s cuemon.snk ]]; then - echo "::error::The strong-name key 'cuemon.snk' was not downloaded." + if [[ "$BUILD_VERSION" != "$RELEASE_VERSION" ]]; then + echo "::error::MinVer '$BUILD_VERSION' does not match release tag version '$RELEASE_VERSION'." exit 1 fi - - name: Restore Release dependencies - uses: codebeltnet/dotnet-restore@v3 - - - name: Build Release packages from the exact SHA - uses: codebeltnet/dotnet-build@v4 - with: - configuration: Release + - name: Install .NET for package validation + uses: codebeltnet/install-dotnet@v3 - - name: Pack Release packages - uses: codebeltnet/dotnet-pack@v3 + - name: Download authoritative Release packages + uses: actions/download-artifact@v8 with: - configuration: Release + name: 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.release_preflight.outputs.version }} - install-dotnet: 'false' - - name: Persist validated NuGet packages for protected publication - uses: actions/upload-artifact@v7 - with: - name: NuGet-Release - path: ${{ runner.temp }}/.nuget - if-no-files-found: error - include-hidden-files: true - retention-days: 30 publish_nuget: if: ${{ github.event_name == 'push' }} @@ -309,7 +315,7 @@ jobs: # Capture the ID directly from creation, never rediscover the new draft by tag. release_json="$(gh api --method POST "repos/$GITHUB_REPOSITORY/releases" \ -f tag_name="$RELEASE_TAG" -f target_commitish="$RELEASE_SHA" \ - -f name="Cuemon $RELEASE_TAG" -f body="$notes" \ + -f name="$RELEASE_TAG" -f body="$notes" \ -F draft=true -F prerelease="$expected_prerelease")" fi @@ -481,262 +487,41 @@ jobs: done echo "OCI archive and checksum attached to draft release ID '$RELEASE_ID'; awaiting manual publication." - prepare_release_tests: - if: ${{ !cancelled() && needs.release_preflight.result == 'success' && ((github.event_name == 'push' && needs.publish_nuget.result == 'success') || (github.event_name == 'workflow_dispatch' && inputs.recovery == 'assurance')) }} - name: Discover post-release test projects - needs: [release_preflight, publish_nuget] - runs-on: ubuntu-26.04 - timeout-minutes: 5 - permissions: - contents: read - outputs: - json: ${{ steps.test-projects.outputs.result }} - steps: - - name: Checkout the released SHA - uses: codebeltnet/git-checkout@v1 - with: - ref: ${{ needs.release_preflight.outputs.sha }} - - - id: test-projects - name: Generate matrix for test projects - uses: codebeltnet/shell-globbing@v2 - with: - pattern: | - test/**/*.csproj - !test/**/Cuemon.Data.SqlClient.Tests.csproj - - - name: JSON output - run: echo "${{ steps.test-projects.outputs.result }}" - - post_release_tests: - if: ${{ !cancelled() && needs.release_preflight.result == 'success' && needs.prepare_release_tests.result == 'success' && ((github.event_name == 'push' && needs.publish_nuget.result == 'success') || (github.event_name == 'workflow_dispatch' && inputs.recovery == 'assurance')) }} - name: Post-release Release tests - ${{ matrix.project }} - needs: [release_preflight, prepare_release_tests, publish_nuget] - strategy: - fail-fast: false - matrix: - project: ${{ fromJson(needs.prepare_release_tests.outputs.json) }} - runs-on: ubuntu-26.04 - timeout-minutes: 30 - permissions: - contents: read - steps: - - name: Checkout the released SHA - uses: codebeltnet/git-checkout@v1 - with: - ref: ${{ needs.release_preflight.outputs.sha }} - - - name: Install .NET - uses: codebeltnet/install-dotnet@v3 - - - name: Install .NET Tool - Report Generator - uses: codebeltnet/dotnet-tool-install-reportgenerator@v1 - - - name: Test Release project with xUnit v3 and coverage - uses: codebeltnet/dotnet-test@v4 - with: - projects: ${{ matrix.project }} - configuration: Release - build: true - restore: true - build-switches: -p:SkipSignAssembly=true - - - name: Upload post-release test results and coverage - if: always() - uses: actions/upload-artifact@v7 - with: - name: TestResults-Release-${{ strategy.job-index }} - path: ${{ runner.temp }}/TestResults - if-no-files-found: warn - include-hidden-files: true - retention-days: 30 - - post_release_integration_test: - if: ${{ !cancelled() && needs.release_preflight.result == 'success' && ((github.event_name == 'push' && needs.publish_nuget.result == 'success') || (github.event_name == 'workflow_dispatch' && inputs.recovery == 'assurance')) }} - name: Post-release SQL Server integration test - needs: [release_preflight, publish_nuget] - runs-on: ubuntu-26.04 - timeout-minutes: 30 - permissions: - contents: read - steps: - - name: Checkout the released SHA - uses: codebeltnet/git-checkout@v1 - with: - ref: ${{ needs.release_preflight.outputs.sha }} - - - name: Install .NET - uses: codebeltnet/install-dotnet@v3 - - - name: Install .NET Tool - Report Generator - uses: codebeltnet/dotnet-tool-install-reportgenerator@v1 - - - name: Spin up SQL Server test dependency - uses: codebeltnet/docker-compose@v1 - with: - command: up - options: --wait - env: - SA_PASSWORD: ${{ secrets.SA_PASSWORD }} - - - name: Run SQL Server integration tests from the released SHA - uses: codebeltnet/dotnet-test@v4 - with: - projects: test/**/Cuemon.Data.SqlClient.Tests.csproj - configuration: Release - build: true - restore: true - build-switches: -p:SkipSignAssembly=true - env: - CONNECTIONSTRINGS__ADVENTUREWORKS: ${{ secrets.DB_ADVENTUREWORKS }} - - - name: Upload SQL Server test results and coverage - if: always() - uses: actions/upload-artifact@v7 - with: - name: TestResults-Release-SqlServer - path: ${{ runner.temp }}/TestResults - if-no-files-found: warn - include-hidden-files: true - retention-days: 30 - - - name: Take down SQL Server test dependency - if: always() - uses: codebeltnet/docker-compose@v1 - with: - command: down - - post_release_sonarcloud: - if: ${{ !cancelled() && needs.release_preflight.result == 'success' && ((github.event_name == 'push' && needs.publish_nuget.result == 'success') || (github.event_name == 'workflow_dispatch' && inputs.recovery == 'assurance')) }} - name: Post-release SonarCloud analysis - needs: [release_preflight, publish_nuget, prepare_release_tests, post_release_tests, post_release_integration_test] - permissions: - contents: read - uses: codebeltnet/jobs-sonarcloud/.github/workflows/default.yml@v3 + # Normal release waits for draft + immutable OCI attachment. Replay invokes only proof. + post_release_verification: + if: ${{ !cancelled() && needs.release_preflight.result == 'success' && ((github.event_name == 'push' && needs.publish_nuget.result == 'success' && needs.upload_docfx_release_asset.result == 'success') || (github.event_name == 'workflow_dispatch' && inputs.recovery == 'assurance')) }} + name: Reproduce PR proof on canonical main + needs: [release_preflight, publish_nuget, upload_docfx_release_asset] + uses: ./.github/workflows/verify.yml with: - organization: geekle - projectKey: Cuemon - version: ${{ needs.release_preflight.outputs.version }} ref: ${{ needs.release_preflight.outputs.sha }} - configuration: Release - timeout-minutes: 45 - # Custom parameters replace the workflow defaults; retain its exclusions. - parameters: >- - -d:sonar.exclusions='**/obj/**,**/bin/**' - -d:sonar.branch.name=main - -d:sonar.scm.revision=${{ needs.release_preflight.outputs.sha }} - secrets: - SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} - - post_release_codecov: - if: ${{ !cancelled() && needs.release_preflight.result == 'success' && ((github.event_name == 'push' && needs.publish_nuget.result == 'success') || (github.event_name == 'workflow_dispatch' && inputs.recovery == 'assurance')) }} - name: Post-release Codecov upload - needs: [release_preflight, publish_nuget, prepare_release_tests, post_release_tests, post_release_integration_test] + version: ${{ needs.release_preflight.outputs.version }} + canonical-main: true + run-mac-tests: ${{ github.event_name == 'workflow_dispatch' && inputs.run_mac_tests }} permissions: contents: read - uses: codebeltnet/jobs-codecov/.github/workflows/default.yml@v1 - with: - repository: codebeltnet/cuemon - configuration: .github/codecov.yml - ref: ${{ needs.release_preflight.outputs.sha }} - branch: main - commit: ${{ needs.release_preflight.outputs.sha }} + 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 }} - post_release_codeql: - if: ${{ !cancelled() && needs.release_preflight.result == 'success' && ((github.event_name == 'push' && needs.publish_nuget.result == 'success') || (github.event_name == 'workflow_dispatch' && inputs.recovery == 'assurance')) }} - name: Post-release CodeQL analysis - needs: [release_preflight, publish_nuget, prepare_release_tests, post_release_tests, post_release_integration_test] - permissions: - contents: read - security-events: write - uses: codebeltnet/jobs-codeql/.github/workflows/default.yml@v3 - with: - ref: ${{ needs.release_preflight.outputs.sha }} - analysis-ref: refs/heads/main - analysis-sha: ${{ needs.release_preflight.outputs.sha }} - configuration: Release - timeout-minutes: 45 - - verify_assurance_identity: - if: ${{ !cancelled() && needs.release_preflight.result == 'success' && needs.post_release_sonarcloud.result == 'success' && needs.post_release_codecov.result == 'success' && needs.post_release_codeql.result == 'success' }} - name: Verify released SHA on canonical main in quality services - needs: [release_preflight, post_release_sonarcloud, post_release_codecov, post_release_codeql] - runs-on: ubuntu-26.04 - timeout-minutes: 10 - permissions: - contents: read - security-events: read - steps: - - name: Verify service records for main at the released SHA - shell: bash - env: - GH_TOKEN: ${{ github.token }} - RELEASE_SHA: ${{ needs.release_preflight.outputs.sha }} - RELEASE_VERSION: ${{ needs.release_preflight.outputs.version }} - run: | - set -euo pipefail - # Upload completion can precede service-side processing. Bound the wait. - for attempt in {1..20}; do - sonar_ok=false - codecov_ok=false - codeql_ok=false - # Newer main analyses must not hide the released revision, including on later pages. - sonar_page=1 - while sonar="$(curl --fail --silent --show-error "https://sonarcloud.io/api/project_analyses/search?project=Cuemon&branch=main&ps=100&p=$sonar_page")"; do - if jq -e --arg sha "$RELEASE_SHA" --arg version "$RELEASE_VERSION" \ - 'any(.analyses[]; .revision == $sha and .projectVersion == $version)' <<< "$sonar" >/dev/null; then - sonar_ok=true - break - fi - if ! jq -e '.paging.pageIndex * .paging.pageSize < .paging.total' <<< "$sonar" >/dev/null; then - break - fi - sonar_page=$((sonar_page + 1)) - done - if codecov="$(curl --fail --silent --show-error "https://api.codecov.io/api/v2/github/codebeltnet/repos/cuemon/commits/$RELEASE_SHA/")" && - jq -e --arg sha "$RELEASE_SHA" \ - '.branch == "main" and .commitid == $sha and .state == "complete"' <<< "$codecov" >/dev/null; then - codecov_ok=true - fi - if codeql="$(gh api "repos/$GITHUB_REPOSITORY/code-scanning/analyses?tool_name=CodeQL&ref=refs%2Fheads%2Fmain&per_page=100")" && - jq -e --arg sha "$RELEASE_SHA" \ - 'any(.[]; .tool.name == "CodeQL" and .ref == "refs/heads/main" and .commit_sha == $sha and .error == "")' <<< "$codeql" >/dev/null; then - codeql_ok=true - fi - echo "Service identity check $attempt/20 for $RELEASE_SHA: SonarCloud=$sonar_ok; Codecov=$codecov_ok; CodeQL=$codeql_ok" - if [[ "$sonar_ok" == true && "$codecov_ok" == true && "$codeql_ok" == true ]]; then - { - echo '## Post-release service identity verified' - echo "- SonarCloud: main; SCM revision \`$RELEASE_SHA\`; project version \`$RELEASE_VERSION\`" - echo "- Codecov: main; commit \`$RELEASE_SHA\`" - echo "- CodeQL: refs/heads/main; commit \`$RELEASE_SHA\`" - } >> "$GITHUB_STEP_SUMMARY" - exit 0 - fi - if [[ "$attempt" -lt 20 ]]; then sleep 15; fi - done - echo "::error::Services did not confirm canonical main at released SHA '$RELEASE_SHA' (SonarCloud=$sonar_ok; Codecov=$codecov_ok; CodeQL=$codeql_ok). Inspect service records; successful uploads alone do not establish assurance." - exit 1 - release_summary: if: ${{ always() }} name: Release and post-release status needs: - release_preflight + - release_build + - release_pack - release_packages - publish_nuget - draft_github_release - docfx_oci_build - upload_docfx_release_asset - - prepare_release_tests - - post_release_tests - - post_release_integration_test - - post_release_sonarcloud - - post_release_codecov - - post_release_codeql - - verify_assurance_identity + - post_release_verification runs-on: ubuntu-26.04 permissions: contents: read @@ -752,18 +537,14 @@ jobs: SOURCE_RUN_ID: ${{ inputs.source_run_id }} REQUESTED_TAG: ${{ inputs.tag }} RECOVERY_MODE: ${{ inputs.recovery || 'assets' }} + PRODUCT_BUILD_RESULT: ${{ needs.release_build.result }} + PRODUCT_PACK_RESULT: ${{ needs.release_pack.result }} PACKAGE_BUILD_RESULT: ${{ needs.release_packages.result }} NUGET_RESULT: ${{ needs.publish_nuget.result }} DRAFT_RELEASE_RESULT: ${{ needs.draft_github_release.result }} DOCFX_BUILD_RESULT: ${{ needs.docfx_oci_build.result }} DOCFX_ASSET_RESULT: ${{ needs.upload_docfx_release_asset.result }} - TEST_DISCOVERY_RESULT: ${{ needs.prepare_release_tests.result }} - TEST_RESULT: ${{ needs.post_release_tests.result }} - INTEGRATION_RESULT: ${{ needs.post_release_integration_test.result }} - SONAR_RESULT: ${{ needs.post_release_sonarcloud.result }} - CODECOV_RESULT: ${{ needs.post_release_codecov.result }} - CODEQL_RESULT: ${{ needs.post_release_codeql.result }} - IDENTITY_RESULT: ${{ needs.verify_assurance_identity.result }} + VERIFICATION_RESULT: ${{ needs.post_release_verification.result }} run: | set -euo pipefail @@ -776,16 +557,11 @@ jobs: echo "- Source run: \`$SOURCE_RUN_ID\`" echo "- Recovery validation: \`$PREFLIGHT_RESULT\`" echo '- Reporting identity: SonarCloud/Codecov main; CodeQL refs/heads/main at the released SHA' - echo "- Test discovery: \`$TEST_DISCOVERY_RESULT\`; test matrix: \`$TEST_RESULT\`; SQL Server integration: \`$INTEGRATION_RESULT\`" - echo "- SonarCloud: \`$SONAR_RESULT\`; Codecov: \`$CODECOV_RESULT\`; CodeQL: \`$CODEQL_RESULT\`" - echo "- Service identity verification: \`$IDENTITY_RESULT\`" + echo "- Shared PR verification (tests, coverage and analysis): \`$VERIFICATION_RESULT\`" echo '- NuGet publication, OCI build/attachment, GitHub Release and deployment: not invoked' } >> "$GITHUB_STEP_SUMMARY" - if [[ "$PREFLIGHT_RESULT" != "success" || "$TEST_DISCOVERY_RESULT" != "success" || - "$TEST_RESULT" != "success" || "$INTEGRATION_RESULT" != "success" || - "$SONAR_RESULT" != "success" || "$CODECOV_RESULT" != "success" || - "$CODEQL_RESULT" != "success" || "$IDENTITY_RESULT" != "success" ]]; then - echo '::error::Assurance recovery did not complete. Inspect the failed tests, analysis or service identity check.' + if [[ "$PREFLIGHT_RESULT" != "success" || "$VERIFICATION_RESULT" != "success" ]]; then + echo '::error::Assurance replay did not complete. Inspect the shared verification jobs.' exit 1 fi exit 0 @@ -829,13 +605,14 @@ jobs: echo "- Version: \`${RELEASE_VERSION:-not validated}\`" echo "- Released SHA: \`${RELEASE_SHA:-not validated}\`" echo '- Repository health identity: SonarCloud/Codecov main; CodeQL refs/heads/main at the released SHA' - echo "- Release package build: \`$PACKAGE_BUILD_RESULT\`" + echo "- Release product build: \`$PRODUCT_BUILD_RESULT\`; pack: \`$PRODUCT_PACK_RESULT\`; package validation: \`$PACKAGE_BUILD_RESULT\`" echo "- NuGet publication: \`$NUGET_RESULT\`" echo "- Draft GitHub Release: \`$DRAFT_RELEASE_RESULT\`" echo "- DocFX OCI build: \`$DOCFX_BUILD_RESULT\`" echo "- DocFX release asset: \`$DOCFX_ASSET_RESULT\`" echo "- GitHub Release ID: \`${RELEASE_ID:-not resolved}\`" echo '- GitHub Release: awaiting manual publication once draft assets and assurance are reviewed' + echo "- Shared PR verification: \`$VERIFICATION_RESULT\`" echo '- Deployment: not started by this workflow' echo } >> "$GITHUB_STEP_SUMMARY" @@ -870,19 +647,11 @@ jobs: } >> "$GITHUB_STEP_SUMMARY" fi - if [[ "$NUGET_RESULT" == "success" && \ - ( "$TEST_DISCOVERY_RESULT" != "success" || "$TEST_RESULT" != "success" || \ - "$INTEGRATION_RESULT" != "success" || "$SONAR_RESULT" != "success" || \ - "$CODECOV_RESULT" != "success" || "$CODEQL_RESULT" != "success" || \ - "$IDENTITY_RESULT" != "success" ) ]]; then - echo "::warning::NuGet publication succeeded; post-release assurance did not complete successfully." + if [[ "$NUGET_RESULT" == "success" && "$VERIFICATION_RESULT" != "success" ]]; then + echo "::warning::NuGet publication succeeded; post-release verification did not complete successfully." { echo - echo "**NuGet publication succeeded; post-release assurance did not complete successfully.**" - echo - echo "Post-release test discovery: \`$TEST_DISCOVERY_RESULT\`; test matrix: \`$TEST_RESULT\`; SQL Server integration: \`$INTEGRATION_RESULT\`; SonarCloud: \`$SONAR_RESULT\`; Codecov: \`$CODECOV_RESULT\`; CodeQL: \`$CODEQL_RESULT\`; service identity: \`$IDENTITY_RESULT\`." - echo "The package and tag remain released. These findings do not roll back publication; resolve them against the recorded SHA." + echo "**NuGet publication succeeded; post-release verification: \`$VERIFICATION_RESULT\`.**" + echo "The package and tag remain released. Resolve findings against the recorded SHA; dispatch assurance replay from main with source_run_id '$GITHUB_RUN_ID' and tag '$RELEASE_TAG'." } >> "$GITHUB_STEP_SUMMARY" fi - - diff --git a/.github/workflows/verify.yml b/.github/workflows/verify.yml new file mode 100644 index 00000000..3054c8db --- /dev/null +++ b/.github/workflows/verify.yml @@ -0,0 +1,522 @@ +name: Shared Verification +on: + workflow_call: + inputs: + ref: + type: string + description: Exact source SHA for release assurance; empty retains PR checkout behavior. + default: '' + version: + type: string + description: Released SemVer for canonical main; empty uses the build's MinVer version. + default: '' + canonical-main: + type: boolean + description: Report main health at ref rather than the triggering PR identity. + default: false + run-mac-tests: + type: boolean + description: Opt in to the macOS matrix, consistently for PR and assurance replay. + default: false + secrets: + GCP_TOKEN: + required: false + GCP_BUCKETNAME: + required: false + SA_PASSWORD: + required: false + DB_ADVENTUREWORKS: + required: false + SONAR_TOKEN: + required: false + CODECOV_TOKEN: + required: false + +permissions: + contents: read + +# One verification definition: production packages and immutable assets are caller-owned. +jobs: + init: + name: initialize + runs-on: ubuntu-26.04 + outputs: + run-privileged-jobs: ${{ steps.vars.outputs.run-privileged-jobs }} + run-mac-tests: ${{ steps.vars.outputs.run-mac-tests }} + strong-name-key-filename: ${{ steps.vars.outputs.strong-name-key-filename }} + build-switches: ${{ steps.vars.outputs.build-switches }} + steps: + - id: vars + name: calculate workflow variables + shell: bash + env: + EVENT_NAME: ${{ github.event_name }} + HEAD_REPOSITORY: ${{ github.event.pull_request.head.repo.full_name }} + REPOSITORY: ${{ github.repository }} + WORKFLOW_REF: ${{ github.ref }} + RUN_MAC_TESTS: ${{ inputs.run-mac-tests }} + CANONICAL_MAIN: ${{ inputs.canonical-main }} + SOURCE_REF: ${{ inputs.ref }} + RELEASE_VERSION: ${{ inputs.version }} + run: | + set -euo pipefail + if [[ "$CANONICAL_MAIN" == "true" ]]; then + if [[ ! "$SOURCE_REF" =~ ^[0-9a-f]{40}$ || -z "$RELEASE_VERSION" || + ( "$EVENT_NAME" != "push" && "$EVENT_NAME" != "workflow_dispatch" ) || + ( "$EVENT_NAME" == "push" && "$WORKFLOW_REF" != refs/tags/v* ) || + ( "$EVENT_NAME" == "workflow_dispatch" && "$WORKFLOW_REF" != "refs/heads/main" ) ]]; then + echo '::error::Canonical main assurance requires a validated release SHA/version and a tag push or main dispatch.' + exit 1 + fi + fi + + if [[ "$EVENT_NAME" == "workflow_dispatch" && "$RUN_MAC_TESTS" == "true" ]]; then + echo "run-mac-tests=true" >> "$GITHUB_OUTPUT" + else + echo "run-mac-tests=false" >> "$GITHUB_OUTPUT" + fi + + if [[ "$EVENT_NAME" == "pull_request" && "$HEAD_REPOSITORY" != "$REPOSITORY" ]]; then + echo "run-privileged-jobs=false" >> "$GITHUB_OUTPUT" + echo "strong-name-key-filename=" >> "$GITHUB_OUTPUT" + echo "build-switches=-p:SkipSignAssembly=true" >> "$GITHUB_OUTPUT" + else + echo "run-privileged-jobs=true" >> "$GITHUB_OUTPUT" + echo "strong-name-key-filename=cuemon.snk" >> "$GITHUB_OUTPUT" + echo "build-switches=" >> "$GITHUB_OUTPUT" + fi + + if [[ "$EVENT_NAME" == "workflow_dispatch" && "$WORKFLOW_REF" != "refs/heads/main" ]]; then + echo "run-privileged-jobs=false" >> "$GITHUB_OUTPUT" + echo "strong-name-key-filename=" >> "$GITHUB_OUTPUT" + echo "build-switches=-p:SkipSignAssembly=true" >> "$GITHUB_OUTPUT" + fi + + prepare_test: + name: 📜 Prepare Test + needs: [init] + runs-on: ubuntu-26.04 + timeout-minutes: 5 + outputs: + json: ${{ steps.test-projects.outputs.result }} + steps: + - name: Checkout + uses: codebeltnet/git-checkout@v1 + with: + ref: ${{ inputs.ref || github.ref }} + + - id: test-projects + name: Generate matrix for test projects + uses: codebeltnet/shell-globbing@v2 + with: + pattern: | + test/**/*.csproj + !test/**/Cuemon.Data.SqlClient.Tests.csproj + + - name: JSON output + run: echo "${{ steps.test-projects.outputs.result }}" + + build: + name: call-build + needs: [init] + strategy: + matrix: + arch: [X64, ARM64] + configuration: [Debug, Release] + uses: codebeltnet/jobs-dotnet-build/.github/workflows/default.yml@v3 + with: + ref: ${{ inputs.ref }} + configuration: ${{ matrix.configuration }} + strong-name-key-filename: ${{ needs.init.outputs.strong-name-key-filename }} + build-switches: ${{ needs.init.outputs.build-switches }} + runs-on: ${{ matrix.arch == 'ARM64' && 'ubuntu-26.04-arm' || 'ubuntu-26.04' }} + upload-build-artifact-name: build-${{ matrix.configuration }}-${{ matrix.arch }} + secrets: + GCP_TOKEN: ${{ secrets.GCP_TOKEN }} + GCP_BUCKETNAME: ${{ secrets.GCP_BUCKETNAME }} + + pack: + name: call-pack + needs: [build] + strategy: + matrix: + configuration: [Debug, Release] + uses: codebeltnet/jobs-dotnet-pack/.github/workflows/default.yml@v3 + with: + ref: ${{ inputs.ref }} + configuration: ${{ matrix.configuration }} + version: ${{ needs.build.outputs.version }} + download-build-artifact-pattern: build-${{ matrix.configuration }}-X64 + # Keep verification packages separate from authoritative release products in the same run. + upload-packed-artifact-name: Verification-NuGet-${{ matrix.configuration }} + + + test_linux: + name: call-test-linux + needs: [build, prepare_test] + strategy: + fail-fast: false + matrix: + configuration: [Debug, Release] + project: ${{ fromJson(needs.prepare_test.outputs.json) }} + arch: [X64, ARM64] + uses: codebeltnet/jobs-dotnet-test/.github/workflows/default.yml@v3 + with: + ref: ${{ inputs.ref }} + runs-on: ${{ matrix.arch == 'ARM64' && 'ubuntu-26.04-arm' || 'ubuntu-26.04' }} + configuration: ${{ matrix.configuration }} + build-switches: -p:SkipSignAssembly=true + projects: ${{ matrix.project }} + build: true # we need to build due to xUnitv3 + restore: true # we need to restore since we disabled caching + download-pattern: build-${{ matrix.configuration }}-${{ matrix.arch }} + + test_windows: + name: call-test-windows + needs: [build, prepare_test] + strategy: + fail-fast: false + matrix: + arch: [X64, ARM64] + configuration: [Debug, Release] + project: ${{ fromJson(needs.prepare_test.outputs.json) }} + uses: codebeltnet/jobs-dotnet-test/.github/workflows/default.yml@v3 + with: + ref: ${{ inputs.ref }} + runs-on: ${{ matrix.arch == 'ARM64' && 'windows-11-arm' || 'windows-2025' }} + configuration: ${{ matrix.configuration }} + build-switches: -p:SkipSignAssembly=true + projects: ${{ matrix.project }} + build: true # we need to build for .net48 + restore: true # apparently we need to restore for .net48 + download-pattern: build-${{ matrix.configuration }}-${{ matrix.arch }} + + test_mac: + if: ${{ needs.init.outputs.run-mac-tests == 'true' }} + name: call-test-mac + needs: [init, build, prepare_test] + strategy: + fail-fast: false + matrix: + arch: [X64, ARM64] + configuration: [Debug, Release] + project: ${{ fromJson(needs.prepare_test.outputs.json) }} + uses: codebeltnet/jobs-dotnet-test/.github/workflows/default.yml@v3 + with: + ref: ${{ inputs.ref }} + runs-on: ${{ matrix.arch == 'ARM64' && 'macos-26' || 'macos-26-intel' }} + configuration: ${{ matrix.configuration }} + build-switches: -p:SkipSignAssembly=true + projects: ${{ matrix.project }} + build: true # we need to build due to xUnitv3 + restore: true # we need to restore since we disabled caching + download-pattern: build-${{ matrix.configuration }}-${{ matrix.arch }} + + integration_test: + if: ${{ needs.init.outputs.run-privileged-jobs == 'true' }} + name: ⚗️ Integration Test + needs: [init, build] + strategy: + fail-fast: false + matrix: + configuration: [Debug, Release] + project: [ test/**/Cuemon.Data.SqlClient.Tests.csproj ] + runs-on: ubuntu-26.04 + timeout-minutes: 15 + steps: + - name: Checkout + uses: codebeltnet/git-checkout@v1 + with: + ref: ${{ inputs.ref || github.ref }} + + - name: Install .NET + uses: codebeltnet/install-dotnet@v3 + + - name: Install .NET Tool - Report Generator + uses: codebeltnet/dotnet-tool-install-reportgenerator@v1 + + - name: Spin up SQL Server test dependency for ${{ matrix.configuration }} build + uses: codebeltnet/docker-compose@v1 + with: + command: up + options: --wait + env: + SA_PASSWORD: ${{ secrets.SA_PASSWORD }} + + - name: Download Build Artifacts + uses: actions/download-artifact@v8 + with: + pattern: build-${{ matrix.configuration }}-X64 + merge-multiple: true + + - name: Fix Linux test apphost permissions + run: | + set -euo pipefail + + echo "=== Context ===" + echo "Runner: $RUNNER_OS / $RUNNER_ARCH" + echo "Configuration: ${{ matrix.configuration }}" + echo "Workspace: $GITHUB_WORKSPACE" + echo "PWD: $(pwd)" + echo "Event: $GITHUB_EVENT_NAME" + echo "Ref: $GITHUB_REF" + echo "SHA: $GITHUB_SHA" + echo + + echo "=== .NET info ===" + dotnet --info || true + echo + + echo "=== Git state ===" + git rev-parse HEAD || true + git status --porcelain || true + git rev-parse --is-shallow-repository || true + echo + + # Paths we care about + BIN_GLOB="*/bin/*/net*/*" + OBJ_GLOB="*/obj/*/net*/*" + + echo "=== Brute-force chmod (bin + obj) ===" + find . -type f \( -path "$BIN_GLOB" -o -path "$OBJ_GLOB" \) -exec chmod a+x {} + 2>/dev/null || true + echo "chmod completed (errors ignored)." + echo + + echo "=== Mount options (look for noexec) ===" + # If binaries live on a noexec mount, chmod won't help. + mount | sed -n '1,200p' || true + echo + + echo "=== Candidate executables (top 200) ===" + # Show what we might execute; exclude obvious managed files + find . -type f -path "$BIN_GLOB" \ + ! -name "*.dll" ! -name "*.pdb" ! -name "*.json" ! -name "*.xml" \ + -printf "%m %u:%g %s %p\n" | head -n 200 || true + echo + + echo "=== Likely xUnit / test hosts (if present) ===" + # These names vary; do not rely on just *Tests* + find . -type f -path "$BIN_GLOB" \( \ + -name "testhost*" -o \ + -name "*xunit*" -o \ + -name "*Tests*" -o \ + -name "*.runsettings" \ + \) -printf "%m %u:%g %s %p\n" | head -n 200 || true + echo + + echo "=== Deep diagnostics for any 'testhost' or apphost candidates ===" + # For each likely executable, show the facts that explain 'permission denied' vs 'exec format error' + while IFS= read -r f; do + echo "--- $f ---" + ls -la "$f" || true + + # Identify file type and architecture + file -L "$f" || true + + # If it's an ELF binary, show its dynamic interpreter and linked libs (exec format errors often show up here) + if file -L "$f" | grep -q "ELF"; then + echo "readelf -l (interpreter):" + readelf -l "$f" 2>/dev/null | sed -n '1,80p' || true + echo "ldd (dependencies):" + ldd "$f" 2>/dev/null || true + fi + + # If it's a script, CRLF in the shebang can cause 'Exec format error' + if head -c 2 "$f" 2>/dev/null | grep -q "#!"; then + echo "shebang:" + head -n 1 "$f" | cat -A || true + fi + + echo + done < <( + find . -type f -path "$BIN_GLOB" \( \ + -name "testhost*" -o \ + -name "*xunit*" -o \ + -name "*Tests*" \ + \) | head -n 50 + ) || true + + echo "=== Done diagnostics step ===" + shell: bash + + - name: Test with ${{ matrix.configuration }} build + uses: codebeltnet/dotnet-test@v4 + with: + projects: ${{ matrix.project }} + configuration: ${{ matrix.configuration }} + build: true # apparently we need to due to xUnitv3 + restore: true # we need to restore since we disabled caching + env: + CONNECTIONSTRINGS__ADVENTUREWORKS: ${{ secrets.DB_ADVENTUREWORKS }} + + - name: Upload Integration Test Results + if: always() + uses: actions/upload-artifact@v7 + with: + name: TestResults-Integration-${{ matrix.configuration }} + path: ${{ runner.temp }}/TestResults + + - name: Take down SQL Server test dependency for ${{ matrix.configuration }} build + if: always() + uses: codebeltnet/docker-compose@v1 + with: + command: down + + test_qualitygate: + if: ${{ always() }} + name: test-qualitygate + needs: [init, test_linux, test_windows, test_mac, integration_test] + runs-on: ubuntu-26.04 + steps: + - name: Evaluate test results + shell: bash + env: + RUN_MAC_TESTS: ${{ needs.init.outputs.run-mac-tests }} + RUN_PRIVILEGED_JOBS: ${{ needs.init.outputs.run-privileged-jobs }} + TEST_LINUX_RESULT: ${{ needs.test_linux.result }} + TEST_WINDOWS_RESULT: ${{ needs.test_windows.result }} + TEST_MAC_RESULT: ${{ needs.test_mac.result }} + INTEGRATION_TEST_RESULT: ${{ needs.integration_test.result }} + run: | + require_success() { + local job_name="$1" + local job_result="$2" + + if [[ "$job_result" != "success" ]]; then + echo "::error::$job_name finished with '$job_result'." + exit 1 + fi + } + + require_success_or_skip() { + local job_name="$1" + local job_enabled="$2" + local job_result="$3" + + if [[ "$job_enabled" == "true" ]]; then + require_success "$job_name" "$job_result" + return + fi + + if [[ "$job_result" != "success" && "$job_result" != "skipped" ]]; then + echo "::error::$job_name finished with '$job_result' while disabled." + exit 1 + fi + } + + require_success "test_linux" "$TEST_LINUX_RESULT" + require_success "test_windows" "$TEST_WINDOWS_RESULT" + require_success_or_skip "test_mac" "$RUN_MAC_TESTS" "$TEST_MAC_RESULT" + require_success_or_skip "integration_test" "$RUN_PRIVILEGED_JOBS" "$INTEGRATION_TEST_RESULT" + + sonarcloud: + if: ${{always() && needs.init.outputs.run-privileged-jobs == 'true' && needs.build.result == 'success' && needs.test_qualitygate.result == 'success'}} + name: call-sonarcloud + needs: [init, build, test_qualitygate] + uses: codebeltnet/jobs-sonarcloud/.github/workflows/default.yml@v3 + with: + organization: geekle + projectKey: Cuemon + version: ${{ inputs.version || needs.build.outputs.version }} + ref: ${{ inputs.ref }} + configuration: Debug # Preserve PR's proven analysis build configuration for both callers. + parameters: >- + -d:sonar.exclusions='**/obj/**,**/bin/**' + ${{ inputs.canonical-main && format('-d:sonar.branch.name=main -d:sonar.scm.revision={0}', inputs.ref) || '' }} + secrets: + SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} + + codecov: + if: ${{always() && needs.init.outputs.run-privileged-jobs == 'true' && needs.build.result == 'success' && needs.test_qualitygate.result == 'success'}} + name: call-codecov + needs: [init, build, test_qualitygate] + uses: codebeltnet/jobs-codecov/.github/workflows/default.yml@v1 + with: + repository: codebeltnet/cuemon + configuration: .github/codecov.yml + ref: ${{ inputs.ref }} + branch: ${{ inputs.canonical-main && 'main' || '' }} + commit: ${{ inputs.canonical-main && inputs.ref || '' }} + fail-on-error: true + secrets: + CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} + + codeql: + if: ${{always() && needs.init.outputs.run-privileged-jobs == 'true' && needs.build.result == 'success' && needs.test_qualitygate.result == 'success'}} + name: call-codeql + needs: [init, build, test_qualitygate] + uses: codebeltnet/jobs-codeql/.github/workflows/default.yml@v3 + with: + ref: ${{ inputs.ref }} + analysis-ref: ${{ inputs.canonical-main && 'refs/heads/main' || '' }} + analysis-sha: ${{ inputs.canonical-main && inputs.ref || '' }} + configuration: Debug # Same analysis build as SonarCloud and the original PR flow. + timeout-minutes: 30 + permissions: + contents: read + security-events: write + + verification_quality_gate: + if: ${{ always() }} + name: Verification quality gate + needs: [init, build, pack, test_qualitygate, sonarcloud, codecov, codeql] + runs-on: ubuntu-26.04 + steps: + - name: Evaluate verification results + shell: bash + env: + RUN_PRIVILEGED_JOBS: ${{ needs.init.outputs.run-privileged-jobs }} + BUILD_RESULT: ${{ needs.build.result }} + EXPECTED_VERSION: ${{ inputs.version }} + BUILD_VERSION: ${{ needs.build.outputs.version }} + PACK_RESULT: ${{ needs.pack.result }} + TEST_RESULT: ${{ needs.test_qualitygate.result }} + SONAR_RESULT: ${{ needs.sonarcloud.result }} + CODECOV_RESULT: ${{ needs.codecov.result }} + CODEQL_RESULT: ${{ needs.codeql.result }} + run: | + set -euo pipefail + + require_success() { + local job_name="$1" + local job_result="$2" + + if [[ "$job_result" != "success" ]]; then + echo "::error::$job_name finished with '$job_result'." + exit 1 + fi + } + + require_skipped() { + local job_name="$1" + local job_result="$2" + + if [[ "$job_result" != "skipped" ]]; then + echo "::error::$job_name finished with '$job_result' for a run that is not authorized to use privileged integrations." + exit 1 + fi + } + + require_success "build" "$BUILD_RESULT" + require_success "pack" "$PACK_RESULT" + if [[ -n "$EXPECTED_VERSION" && "$BUILD_VERSION" != "$EXPECTED_VERSION" ]]; then + echo "::error::Verification MinVer '$BUILD_VERSION' does not match released SemVer '$EXPECTED_VERSION'." + exit 1 + fi + require_success "test_qualitygate" "$TEST_RESULT" + + if [[ "$RUN_PRIVILEGED_JOBS" == "true" ]]; then + require_success "sonarcloud" "$SONAR_RESULT" + require_success "codecov" "$CODECOV_RESULT" + require_success "codeql" "$CODEQL_RESULT" + else + require_skipped "sonarcloud" "$SONAR_RESULT" + require_skipped "codecov" "$CODECOV_RESULT" + require_skipped "codeql" "$CODEQL_RESULT" + fi + + { + echo "## Shared verification passed" + echo + echo "Build and package validation passed. Required Linux and Windows tests passed; optional macOS and trusted-repository checks followed the workflow inputs and fork policy." + } >> "$GITHUB_STEP_SUMMARY" From 5f50b4e886932261f6358f7ab7b5d666dd67fd20 Mon Sep 17 00:00:00 2001 From: aicia-bot Date: Sun, 4 Oct 2026 20:37:52 +0200 Subject: [PATCH 2/4] =?UTF-8?q?=F0=9F=91=B7=20add=20concurrency=20to=20can?= =?UTF-8?q?cel=20superseded=20pr=20runs?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add a concurrency group keyed by pull request so new pushes cancel outdated PR verification runs and save CI capacity. --- .github/workflows/pr.yml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.github/workflows/pr.yml b/.github/workflows/pr.yml index a02da09a..e84f86e7 100644 --- a/.github/workflows/pr.yml +++ b/.github/workflows/pr.yml @@ -12,6 +12,10 @@ on: permissions: contents: read +concurrency: + group: pr-${{ github.event.pull_request.number || github.ref }} + cancel-in-progress: true + jobs: verification: name: PR proof From 522de81fccbf26e7f2a054426f8d8785e920d028 Mon Sep 17 00:00:00 2001 From: aicia-bot Date: Sun, 4 Oct 2026 21:30:19 +0200 Subject: [PATCH 3/4] =?UTF-8?q?=F0=9F=90=9B=20require=20canonical=20servic?= =?UTF-8?q?e=20records=20before=20accepting=20assurance?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Successful uploads do not prove that quality services recorded the released SHA on main. Require matching processed service records before release or replay assurance can pass. Generated with Codebuff 🤖 Co-Authored-By: Codebuff --- .github/workflows/verify.yml | 59 ++++++++++++++++++++++++++++++++++++ 1 file changed, 59 insertions(+) diff --git a/.github/workflows/verify.yml b/.github/workflows/verify.yml index 3054c8db..1adfd0a9 100644 --- a/.github/workflows/verify.yml +++ b/.github/workflows/verify.yml @@ -461,6 +461,10 @@ jobs: name: Verification quality gate needs: [init, build, pack, test_qualitygate, sonarcloud, codecov, codeql] runs-on: ubuntu-26.04 + timeout-minutes: 10 + permissions: + contents: read + security-events: read steps: - name: Evaluate verification results shell: bash @@ -515,6 +519,61 @@ jobs: require_skipped "codeql" "$CODEQL_RESULT" fi + - name: Verify service records for main at the released SHA + if: ${{ inputs.canonical-main }} + shell: bash + env: + GH_TOKEN: ${{ github.token }} + RELEASE_SHA: ${{ inputs.ref }} + RELEASE_VERSION: ${{ inputs.version }} + run: | + set -euo pipefail + # Upload completion can precede service-side processing. Bound the wait. + for attempt in {1..20}; do + sonar_ok=false + codecov_ok=false + codeql_ok=false + # Newer main analyses must not hide the released revision, including on later pages. + sonar_page=1 + while sonar="$(curl --fail --silent --show-error --connect-timeout 10 --max-time 30 "https://sonarcloud.io/api/project_analyses/search?project=Cuemon&branch=main&ps=100&p=$sonar_page")"; do + if jq -e --arg sha "$RELEASE_SHA" --arg version "$RELEASE_VERSION" \ + 'any(.analyses[]; .revision == $sha and .projectVersion == $version)' <<< "$sonar" >/dev/null; then + sonar_ok=true + break + fi + if ! jq -e '.paging.pageIndex * .paging.pageSize < .paging.total' <<< "$sonar" >/dev/null; then + break + fi + sonar_page=$((sonar_page + 1)) + done + if codecov="$(curl --fail --silent --show-error --connect-timeout 10 --max-time 30 "https://api.codecov.io/api/v2/github/codebeltnet/repos/cuemon/commits/$RELEASE_SHA/")" && + jq -e --arg sha "$RELEASE_SHA" \ + '.branch == "main" and .commitid == $sha and .state == "complete"' <<< "$codecov" >/dev/null; then + codecov_ok=true + fi + if codeql="$(gh api --paginate --slurp "repos/$GITHUB_REPOSITORY/code-scanning/analyses?tool_name=CodeQL&ref=refs%2Fheads%2Fmain&per_page=100")" && + jq -e --arg sha "$RELEASE_SHA" \ + 'any(.[][]; .tool.name == "CodeQL" and .ref == "refs/heads/main" and .commit_sha == $sha and .error == "")' <<< "$codeql" >/dev/null; then + codeql_ok=true + fi + echo "Service identity check $attempt/20 for $RELEASE_SHA: SonarCloud=$sonar_ok; Codecov=$codecov_ok; CodeQL=$codeql_ok" + if [[ "$sonar_ok" == true && "$codecov_ok" == true && "$codeql_ok" == true ]]; then + { + echo '## Post-release service identity verified' + echo "- SonarCloud: main; SCM revision \`$RELEASE_SHA\`; project version \`$RELEASE_VERSION\`" + echo "- Codecov: main; commit \`$RELEASE_SHA\`" + echo "- CodeQL: refs/heads/main; commit \`$RELEASE_SHA\`" + } >> "$GITHUB_STEP_SUMMARY" + exit 0 + fi + if [[ "$attempt" -lt 20 ]]; then sleep 15; fi + done + echo "::error::Services did not confirm canonical main at released SHA '$RELEASE_SHA' (SonarCloud=$sonar_ok; Codecov=$codecov_ok; CodeQL=$codeql_ok). Inspect service records; successful uploads alone do not establish assurance." + exit 1 + + - name: Report verified success + shell: bash + run: | { echo "## Shared verification passed" echo From 6eec298ea14f9f0ffe77cb87d43b66a1e8c9cddd Mon Sep 17 00:00:00 2001 From: aicia-bot Date: Sun, 4 Oct 2026 21:30:42 +0200 Subject: [PATCH 4/4] =?UTF-8?q?=F0=9F=90=9B=20verify=20published=20package?= =?UTF-8?q?s=20independently=20of=20OCI=20finalization?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Published NuGet packages require automatic verification even when DocFX finalization fails. Start canonical-main assurance at the successful publication boundary rather than waiting for OCI attachment. Generated with Codebuff 🤖 Co-Authored-By: Codebuff --- .github/workflows/release.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 33098fac..62b2f589 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -487,11 +487,11 @@ jobs: done echo "OCI archive and checksum attached to draft release ID '$RELEASE_ID'; awaiting manual publication." - # Normal release waits for draft + immutable OCI attachment. Replay invokes only proof. + # Published packages require proof independently of OCI finalization. Replay invokes only proof. post_release_verification: - if: ${{ !cancelled() && needs.release_preflight.result == 'success' && ((github.event_name == 'push' && needs.publish_nuget.result == 'success' && needs.upload_docfx_release_asset.result == 'success') || (github.event_name == 'workflow_dispatch' && inputs.recovery == 'assurance')) }} + if: ${{ !cancelled() && needs.release_preflight.result == 'success' && ((github.event_name == 'push' && needs.publish_nuget.result == 'success') || (github.event_name == 'workflow_dispatch' && inputs.recovery == 'assurance')) }} name: Reproduce PR proof on canonical main - needs: [release_preflight, publish_nuget, upload_docfx_release_asset] + needs: [release_preflight, publish_nuget] uses: ./.github/workflows/verify.yml with: ref: ${{ needs.release_preflight.outputs.sha }}