diff --git a/.bot/README.md b/.bot/README.md index 2cfca899..3f81cdf0 100644 --- a/.bot/README.md +++ b/.bot/README.md @@ -7,4 +7,4 @@ This folder is reserved for local-only AI working material such as: - design alternatives - temporary agent state -Keep this folder out of source control. Move only finalized, non-confidential guidance into `AGENTS.md` or `.github/copilot-instructions.md`. +The `.bot/README.md` file itself is intentionally tracked in source control. All other `.bot/` working material remains local-only and excluded from source control. Move only finalized, non-confidential guidance into `AGENTS.md` or `.github/copilot-instructions.md`. diff --git a/.github/CONTRIBUTING.md b/.github/CONTRIBUTING.md index 6756c67b..c5f96e2f 100644 --- a/.github/CONTRIBUTING.md +++ b/.github/CONTRIBUTING.md @@ -75,9 +75,27 @@ Package-specific release notes live under `.nuget//PackageReleaseNo 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. -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 before that release is published. Post-release assurance failures do not roll back published packages; inspect the release summary and resolve failures against its recorded commit. +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. -A published GitHub Release starts `deploy.yml`. To retry deployment, dispatch that workflow from `main` with the existing published release tag. Deployment requires the versioned OCI archive and checksum, resolves the tag to its source commit, and promotes the verified image to JCR through `Production` without rebuilding it. The workflow reports the immutable image digest for a Kubernetes handoff; this repository does not perform the Kubernetes rollout. +The Git tag identifies the release; `main` identifies repository health; the released SHA ties them together. Release assurance calls the same reusable workflows as `pr.yml`: `jobs-sonarcloud@v3`, `jobs-codecov@v1`, and `jobs-codeql@v3`. Assurance always checks out the exact released tag's commit using each workflow's `ref` input, even if `main` has advanced. SonarCloud explicitly reports `sonar.branch.name=main`, the released SemVer as project version, and the released SHA as SCM revision through the existing `parameters` input, retaining the scanner's default exclusions. Codecov explicitly reports `branch: main` and `commit: ` independently of its checkout `ref`. CodeQL uses `analysis-ref: refs/heads/main` and `analysis-sha: `, forwarded to the finalize action's `ref` and `sha` inputs. SonarCloud and CodeQL request Release builds; callers that omit configuration retain Debug builds. Release tags must never become analysis branches. + +Before accepting assurance, verify service records rather than relying on successful Actions jobs: SonarCloud's `api/project_analyses/search?project=Cuemon&branch=main` must contain the released `revision` and `projectVersion`; Codecov's `api/v2/github/codebeltnet/repos/cuemon/commits//` must report `branch: main` and the expected `commitid`; GitHub's `repos/codebeltnet/cuemon/code-scanning/analyses?tool_name=CodeQL&ref=refs%2Fheads%2Fmain` must contain the expected `commit_sha`. For `v10.8.0`, every check must identify `33e6e756984e5d21a4d0e6b9171d417a58a779f1` on the canonical main branch. Any assurance replay must use these same identities and the exact released SHA; rerunning a historical job does not pick up corrected workflow parameters. + +If OCI asset attachment fails after NuGet publication and the OCI build succeeded, dispatch `release.yml` from **main** with `recovery: assets` (the default), the existing `tag` and original `source_run_id`. This mode never rebuilds packages/images, invokes NuGet publication, reruns assurance, creates a replacement release, or publishes the draft. Missing, expired or unverifiable artifacts cause failure with diagnostics rather than replacement bytes. + +To correct or replay repository-health telemetry, dispatch the corrected workflow from **main** with `recovery: assurance`, the existing release `tag`, and its original tag-push `source_run_id`. Preflight confirms the source run's successful tag validation and NuGet publication, resolves the unchanged tag, and checks its membership in main history. This mode regenerates tests/coverage and all three analyses from that exact released SHA. It does not require the original OCI artifacts or alter NuGet packages, OCI assets, the GitHub Release, or deployment. A service-record verification job waits for processing, then fails unless SonarCloud reports main with the released SHA and SemVer, Codecov reports main with that SHA, and CodeQL records refs/heads/main with that SHA. Assurance recovery fails when tests, analysis, or service verification fail. + +Publish the backward-compatible `codecov-scan@v1` and `codeql-scan-finalize@v1` extensions, then `jobs-codecov@v1`, `jobs-sonarcloud@v3`, and `jobs-codeql@v3`, before publishing the Cuemon workflow that uses their new inputs. For the existing release, the recovery request is: + +```powershell +gh workflow run release.yml --repo codebeltnet/cuemon --ref main -f recovery=assurance -f tag=v10.8.0 -f source_run_id=37153698543 +``` + +This command requires the corrected workflow and wrappers to be available remotely. Checkout and all service verification must identify `main @ 33e6e756984e5d21a4d0e6b9171d417a58a779f1`; the current tip of main is only the orchestration source. + +Recovery verifies the artifact's download digest, archive checksum, OCI version/revision and `linux/amd64`/`linux/arm64` coverage before attachment. Exact existing release assets are accepted; conflicting bytes fail before mutation, and only missing assets are uploaded. Recovery can be dispatched again after a partial upload. Fix orchestration on main and resume from the last durable successful boundary; do not replay immutable publication because later finalization failed. Actions artifacts are retained for 30 days, so recover while the source artifact is available. + +A human-published GitHub Release starts `deploy.yml`. To retry deployment, dispatch that workflow from `main` with the existing published release tag. Deployment requires the versioned OCI archive and checksum, resolves the tag to its source commit, and promotes the verified image to JCR through `Production` without rebuilding it. Publishing the GitHub Release and approving the Production environment are separate human decisions. The workflow reports the immutable image digest for a Kubernetes handoff; this repository does not perform the Kubernetes rollout. If publication fails, inspect the job results before retrying. A partially completed NuGet push may already have published some packages; rerun the failed publication job to reuse its validated artifact. Keep release tags fixed: publication rechecks the live tag against the built commit and rejects a mismatch. diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 6e5789ce..4a4ee7f3 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -3,12 +3,30 @@ on: push: tags: - 'v*' + workflow_dispatch: + inputs: + tag: + type: string + description: Existing release tag to recover, e.g. v10.8.0. + required: true + source_run_id: + type: string + description: GitHub Actions release run containing the already-built release artifacts. + required: true + recovery: + type: choice + description: Recover OCI asset attachment or replay assurance against main at the released SHA. + required: true + default: assets + options: + - assets + - assurance permissions: contents: read concurrency: - group: cuemon-release-${{ github.ref }} + group: cuemon-release-${{ inputs.tag || github.ref_name }} cancel-in-progress: false jobs: @@ -17,10 +35,13 @@ jobs: runs-on: ubuntu-26.04 permissions: contents: read + actions: read outputs: version: ${{ steps.validate.outputs.version }} tag: ${{ steps.validate.outputs.tag }} sha: ${{ steps.validate.outputs.sha }} + source_run_id: ${{ steps.validate.outputs.source_run_id }} + artifact_id: ${{ steps.validate.outputs.artifact_id }} steps: # git-checkout fetches full history and tags for MinVer and ancestry validation. - name: Checkout the triggering SHA @@ -34,12 +55,19 @@ jobs: env: RELEASE_REF: ${{ github.ref }} RELEASE_SHA: ${{ github.sha }} - RELEASE_TAG: ${{ github.ref_name }} + RELEASE_TAG: ${{ inputs.tag || github.ref_name }} + SOURCE_RUN_ID: ${{ inputs.source_run_id }} + RECOVERY_MODE: ${{ inputs.recovery || 'assets' }} GH_TOKEN: ${{ github.token }} run: | set -euo pipefail - if [[ "$RELEASE_REF" != "refs/tags/$RELEASE_TAG" ]]; then + if [[ "$GITHUB_EVENT_NAME" == "workflow_dispatch" ]]; then + if [[ "$RELEASE_REF" != "refs/heads/main" || ! "$SOURCE_RUN_ID" =~ ^[1-9][0-9]*$ ]]; then + echo "::error::Recovery must run from main with an existing numeric source_run_id." + exit 1 + fi + elif [[ "$GITHUB_EVENT_NAME" != "push" || "$RELEASE_REF" != "refs/tags/$RELEASE_TAG" ]]; then echo "::error::Release must be triggered by a Git tag; received '$RELEASE_REF'." exit 1 fi @@ -59,14 +87,16 @@ jobs: echo "::error::Triggering tag '$RELEASE_TAG' is missing or does not resolve to a Git commit." exit 1 fi - if ! event_sha="$(git rev-parse --verify "$RELEASE_SHA^{commit}" 2>/dev/null)"; then - echo "::error::Triggering SHA '$RELEASE_SHA' does not resolve to a Git commit." - exit 1 - fi - checked_out_sha="$(git rev-parse HEAD)" - if [[ "$tagged_sha" != "$event_sha" || "$checked_out_sha" != "$event_sha" ]]; then - echo "::error::Tag '$RELEASE_TAG' resolves to '$tagged_sha', checkout to '$checked_out_sha', and triggering commit to '$event_sha'. The tag may have moved; restore the original tag before retrying." - exit 1 + if [[ "$GITHUB_EVENT_NAME" == "push" ]]; then + if ! event_sha="$(git rev-parse --verify "$RELEASE_SHA^{commit}" 2>/dev/null)"; then + echo "::error::Triggering SHA '$RELEASE_SHA' does not resolve to a Git commit." + exit 1 + fi + checked_out_sha="$(git rev-parse HEAD)" + if [[ "$tagged_sha" != "$event_sha" || "$checked_out_sha" != "$event_sha" ]]; then + echo "::error::Tag '$RELEASE_TAG' resolves to '$tagged_sha', checkout to '$checked_out_sha', and triggering commit to '$event_sha'. The tag may have moved; restore the original tag before retrying." + exit 1 + fi fi RELEASE_SHA="$tagged_sha" @@ -79,23 +109,51 @@ jobs: exit 1 fi - set +e - release_json="$(gh api "repos/$GITHUB_REPOSITORY/releases/tags/$RELEASE_TAG" 2>&1)" - release_status=$? - set -e - if [[ "$release_status" -eq 0 ]]; then - expected_prerelease=false - if [[ "$RELEASE_VERSION" == *-* ]]; then expected_prerelease=true; fi - if ! jq -e --arg tag "$RELEASE_TAG" --argjson prerelease "$expected_prerelease" \ - '.tag_name == $tag and .prerelease == $prerelease and (.draft | type == "boolean")' <<< "$release_json" > /dev/null; then - echo "::error::Existing GitHub Release conflicts with tag '$RELEASE_TAG' or its prerelease identity. Review the existing release before retrying." + if [[ "$GITHUB_EVENT_NAME" == "workflow_dispatch" ]]; then + source_run="$(gh api "repos/$GITHUB_REPOSITORY/actions/runs/$SOURCE_RUN_ID")" + if ! jq -e --arg repo "$GITHUB_REPOSITORY" --arg tag "$RELEASE_TAG" --arg sha "$RELEASE_SHA" \ + '.repository.full_name == $repo and .head_repository.full_name == $repo and + .event == "push" and .path == ".github/workflows/release.yml" and + .head_branch == $tag and .head_sha == $sha' <<< "$source_run" >/dev/null; then + echo "::error::Source run '$SOURCE_RUN_ID' must be this repository's tag-push release.yml run for '$RELEASE_TAG' at '$RELEASE_SHA'. No artifacts will be consumed." exit 1 fi - echo "Compatible GitHub Release '$RELEASE_TAG' already exists; retaining its draft/published state." - elif [[ "$release_json" != *"HTTP 404"* ]]; then - echo "::error::Could not check GitHub Release '$RELEASE_TAG'." - echo "$release_json" - exit 1 + + source_jobs="$(gh api --paginate --slurp "repos/$GITHUB_REPOSITORY/actions/runs/$SOURCE_RUN_ID/jobs?per_page=100")" + if ! jq -e --arg prefix "Publish NuGet v$RELEASE_VERSION /" ' + [.[].jobs[]] as $jobs | + ([$jobs[] | select(.name | startswith($prefix))] | any(.conclusion == "success")) and + ([$jobs[] | select(.name | startswith($prefix))] | all(.conclusion == "success" or .conclusion == "skipped")) and + ($jobs | any(.name == "Validate release tag and authoritative main history" and .conclusion == "success")) + ' <<< "$source_jobs" >/dev/null; then + echo "::error::Source run '$SOURCE_RUN_ID' has no confirmed successful tag validation and NuGet publication. Inspect the source jobs; do not republish NuGet." + exit 1 + fi + + if [[ "$RECOVERY_MODE" == "assets" ]]; then + if ! jq -e '[.[].jobs[]] | any(.name == "Build multi-platform DocFX OCI artifact once" and .conclusion == "success")' <<< "$source_jobs" >/dev/null; then + echo "::error::Source run '$SOURCE_RUN_ID' has no confirmed successful DocFX OCI build. Asset recovery cannot rebuild replacement bytes." + exit 1 + fi + source_artifacts="$(gh api --paginate --slurp "repos/$GITHUB_REPOSITORY/actions/runs/$SOURCE_RUN_ID/artifacts?per_page=100")" + artifact_json="$(jq -c --arg name "DocFX-OCI-$RELEASE_VERSION" '[.[].artifacts[] | select(.name == $name)]' <<< "$source_artifacts")" + if ! jq -e --arg sha "$RELEASE_SHA" --arg tag "$RELEASE_TAG" --argjson run "$SOURCE_RUN_ID" \ + --argjson repo "$(jq '.repository.id' <<< "$source_run")" ' + length == 1 and (.[0] | .expired == false and + (.expires_at | fromdateiso8601) > now and (.digest | test("^sha256:[0-9a-f]{64}$")) and + .workflow_run.id == $run and .workflow_run.repository_id == $repo and + .workflow_run.head_repository_id == $repo and .workflow_run.head_branch == $tag and + .workflow_run.head_sha == $sha)' <<< "$artifact_json" >/dev/null; then + echo "::error::Expected unique, unexpired, SHA-256 identified DocFX-OCI-$RELEASE_VERSION artifact from run '$SOURCE_RUN_ID' for '$RELEASE_SHA'. Restore access to that exact artifact; recovery will not rebuild replacement bytes." + exit 1 + fi + echo "artifact_id=$(jq -r '.[0].id' <<< "$artifact_json")" >> "$GITHUB_OUTPUT" + elif [[ "$RECOVERY_MODE" != "assurance" ]]; then + echo "::error::Unknown recovery mode '$RECOVERY_MODE'. Select assets or assurance." + exit 1 + fi + echo "source_run_id=$SOURCE_RUN_ID" >> "$GITHUB_OUTPUT" + echo "NuGet v$RELEASE_VERSION: already published (source run confirmed); recovery will not invoke publication." fi { @@ -114,6 +172,7 @@ jobs: } >> "$GITHUB_STEP_SUMMARY" release_packages: + if: ${{ github.event_name == 'push' }} name: Build and validate Release packages needs: [release_preflight] runs-on: ubuntu-26.04 @@ -174,6 +233,7 @@ jobs: retention-days: 30 publish_nuget: + if: ${{ github.event_name == 'push' }} name: Publish NuGet v${{ needs.release_preflight.outputs.version }} needs: [release_preflight, release_packages] uses: codebeltnet/jobs-nuget-push/.github/workflows/default.yml@v3 @@ -189,70 +249,80 @@ jobs: NUGET_TOKEN: ${{ secrets.NUGET_TOKEN }} draft_github_release: - name: Create draft GitHub Release for the existing tag - needs: [release_preflight, release_packages, publish_nuget] + if: ${{ !cancelled() && needs.release_preflight.result == 'success' && ((github.event_name == 'workflow_dispatch' && inputs.recovery == 'assets') || (github.event_name == 'push' && needs.publish_nuget.result == 'success')) }} + name: Resolve draft GitHub Release for the existing tag + needs: [release_preflight, publish_nuget] runs-on: ubuntu-26.04 permissions: contents: write + outputs: + release_id: ${{ steps.draft.outputs.release_id }} steps: - - name: Checkout the released SHA - uses: codebeltnet/git-checkout@v1 - with: - ref: ${{ needs.release_preflight.outputs.sha }} - - name: Verify the existing release tag still identifies the released SHA shell: bash env: + GH_TOKEN: ${{ github.token }} RELEASE_TAG: ${{ needs.release_preflight.outputs.tag }} RELEASE_SHA: ${{ needs.release_preflight.outputs.sha }} run: | set -euo pipefail - if ! tagged_sha="$(git rev-parse --verify "refs/tags/$RELEASE_TAG^{commit}" 2>/dev/null)"; then - echo "::error::Release tag '$RELEASE_TAG' is missing or does not resolve to a commit; restore the original tag before retrying." - exit 1 - fi - if [[ "$tagged_sha" != "$RELEASE_SHA" ]]; then - echo "::error::Tag '$RELEASE_TAG' points to '$tagged_sha', not released SHA '$RELEASE_SHA'. Restore the original tag before retrying." + ref_json="$(gh api "repos/$GITHUB_REPOSITORY/git/ref/tags/$RELEASE_TAG")" + object_type="$(jq -r '.object.type' <<< "$ref_json")" + object_sha="$(jq -r '.object.sha' <<< "$ref_json")" + while [[ "$object_type" == "tag" ]]; do + tag_json="$(gh api "repos/$GITHUB_REPOSITORY/git/tags/$object_sha")" + object_type="$(jq -r '.object.type' <<< "$tag_json")" + object_sha="$(jq -r '.object.sha' <<< "$tag_json")" + done + if [[ "$object_type" != "commit" || "$object_sha" != "$RELEASE_SHA" ]]; then + echo "::error::Live tag '$RELEASE_TAG' does not identify released SHA '$RELEASE_SHA'. Restore the original tag before retrying." exit 1 fi - - name: Create or verify the draft GitHub Release + - id: draft + name: Create or resolve the draft GitHub Release ID shell: bash env: GH_TOKEN: ${{ github.token }} RELEASE_VERSION: ${{ needs.release_preflight.outputs.version }} RELEASE_TAG: ${{ needs.release_preflight.outputs.tag }} + RELEASE_SHA: ${{ needs.release_preflight.outputs.sha }} run: | set -euo pipefail - set +e - release_json="$(gh api "repos/$GITHUB_REPOSITORY/releases/tags/$RELEASE_TAG" 2>&1)" - release_status=$? - set -e - - if [[ "$release_status" -eq 0 ]]; then - release_tag="$(jq -r '.tag_name' <<< "$release_json")" - if [[ "$release_tag" != "$RELEASE_TAG" ]]; then - echo "::error::GitHub returned release '$release_tag' for requested tag '$RELEASE_TAG'." - exit 1 - fi - echo "GitHub Release '$RELEASE_TAG' already exists; retaining its current draft/published state." - exit 0 + # Listing with contents:write includes drafts; tag-oriented lookup can return 404 for them. + releases="$(gh api --paginate --slurp "repos/$GITHUB_REPOSITORY/releases?per_page=100")" + matches="$(jq -c --arg tag "$RELEASE_TAG" '[.[][] | select(.tag_name == $tag)]' <<< "$releases")" + count="$(jq 'length' <<< "$matches")" + if [[ "$count" -gt 1 ]]; then + echo "::error::Multiple GitHub Releases identify '$RELEASE_TAG'; resolve the ambiguity before recovery." + exit 1 fi - if [[ "$release_json" != *"HTTP 404"* ]]; then - echo "::error::Could not check GitHub Release '$RELEASE_TAG'." - echo "$release_json" - exit 1 + expected_prerelease=false + if [[ "$RELEASE_VERSION" == *-* ]]; then expected_prerelease=true; fi + if [[ "$count" -eq 1 ]]; then + release_json="$(jq -c '.[0]' <<< "$matches")" + else + notes="$(gh api --method POST "repos/$GITHUB_REPOSITORY/releases/generate-notes" \ + -f tag_name="$RELEASE_TAG" -f target_commitish="$RELEASE_SHA" --jq '.body')" + # 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 draft=true -F prerelease="$expected_prerelease")" fi - release_args=(--draft --verify-tag --title "Cuemon $RELEASE_TAG" --generate-notes) - if [[ "$RELEASE_VERSION" == *-* ]]; then - release_args+=(--prerelease) + if ! jq -e --arg tag "$RELEASE_TAG" --argjson prerelease "$expected_prerelease" \ + '.tag_name == $tag and .draft == true and .prerelease == $prerelease and + (.id | type == "number" and . > 0)' <<< "$release_json" >/dev/null; then + echo "::error::GitHub Release must be a draft with tag '$RELEASE_TAG' and its expected prerelease identity. Published releases cannot be finalized by recovery." + exit 1 fi - gh release create "$RELEASE_TAG" "${release_args[@]}" + echo "release_id=$(jq -r '.id' <<< "$release_json")" >> "$GITHUB_OUTPUT" docfx_oci_build: + if: ${{ github.event_name == 'push' }} name: Build multi-platform DocFX OCI artifact once needs: [release_preflight, publish_nuget] runs-on: ubuntu-26.04 @@ -305,18 +375,32 @@ jobs: retention-days: 30 upload_docfx_release_asset: + if: ${{ !cancelled() && needs.release_preflight.result == 'success' && needs.draft_github_release.result == 'success' && ((github.event_name == 'workflow_dispatch' && inputs.recovery == 'assets') || (github.event_name == 'push' && needs.docfx_oci_build.result == 'success')) }} name: Attach the built DocFX OCI artifact to its release needs: [release_preflight, draft_github_release, docfx_oci_build] runs-on: ubuntu-26.04 permissions: contents: write + actions: read steps: - name: Download the already-built OCI artifact + if: ${{ github.event_name == 'push' }} uses: actions/download-artifact@v8 with: name: DocFX-OCI-${{ needs.release_preflight.outputs.version }} path: ${{ runner.temp }}/docfx-release-asset + - name: Recover the exact OCI artifact from the source release run + if: ${{ github.event_name == 'workflow_dispatch' }} + uses: actions/download-artifact@v8 + with: + artifact-ids: ${{ needs.release_preflight.outputs.artifact_id }} + run-id: ${{ needs.release_preflight.outputs.source_run_id }} + repository: ${{ github.repository }} + github-token: ${{ github.token }} + digest-mismatch: error + path: ${{ runner.temp }}/docfx-release-asset + - name: Revalidate transferred OCI artifact uses: codebeltnet/oci-artifact-verify@v1 with: @@ -331,76 +415,22 @@ jobs: GH_TOKEN: ${{ github.token }} RELEASE_VERSION: ${{ needs.release_preflight.outputs.version }} RELEASE_TAG: ${{ needs.release_preflight.outputs.tag }} + RELEASE_SHA: ${{ needs.release_preflight.outputs.sha }} + RELEASE_ID: ${{ needs.draft_github_release.outputs.release_id }} run: | set -euo pipefail archive_name="cuemon-docfx-$RELEASE_VERSION.oci.tar" checksum_name="$archive_name.sha256" - archive="$RUNNER_TEMP/docfx-release-asset/$archive_name" - checksum="$RUNNER_TEMP/docfx-release-asset/$checksum_name" existing_dir="$RUNNER_TEMP/existing-docfx-assets" mkdir -p "$existing_dir" - asset_names="$(gh api "repos/$GITHUB_REPOSITORY/releases/tags/$RELEASE_TAG" --jq '.assets[].name')" - has_archive=false - has_checksum=false - if grep -Fxq "$archive_name" <<< "$asset_names"; then has_archive=true; fi - if grep -Fxq "$checksum_name" <<< "$asset_names"; then has_checksum=true; fi - - if [[ "$has_archive" == "true" ]]; then - gh release download "$RELEASE_TAG" --repo "$GITHUB_REPOSITORY" --pattern "$archive_name" --dir "$existing_dir" - if ! cmp -s "$archive" "$existing_dir/$archive_name"; then - echo "::error::Release asset '$archive_name' already exists with different bytes; refusing to overwrite it." - exit 1 - fi - fi - - if [[ "$has_checksum" == "true" ]]; then - gh release download "$RELEASE_TAG" --repo "$GITHUB_REPOSITORY" --pattern "$checksum_name" --dir "$existing_dir" - if ! cmp -s "$checksum" "$existing_dir/$checksum_name"; then - echo "::error::Release asset '$checksum_name' already exists with different bytes; refusing to overwrite it." - exit 1 - fi - fi - - if [[ "$has_archive" == "false" && "$has_checksum" == "false" ]]; then - gh release upload "$RELEASE_TAG" "$archive" "$checksum" --repo "$GITHUB_REPOSITORY" - elif [[ "$has_archive" == "true" && "$has_checksum" == "false" ]]; then - gh release upload "$RELEASE_TAG" "$checksum" --repo "$GITHUB_REPOSITORY" - elif [[ "$has_archive" == "false" && "$has_checksum" == "true" ]]; then - gh release upload "$RELEASE_TAG" "$archive" --repo "$GITHUB_REPOSITORY" - else - echo "The exact versioned OCI archive and checksum are already attached to '$RELEASE_TAG'." - fi - - publish_github_release: - name: Publish GitHub Release after the OCI asset is attached - needs: [release_preflight, upload_docfx_release_asset] - runs-on: ubuntu-26.04 - permissions: - contents: write - steps: - - name: Publish the versioned GitHub Release - shell: bash - env: - GH_TOKEN: ${{ github.token }} - RELEASE_TAG: ${{ needs.release_preflight.outputs.tag }} - RELEASE_SHA: ${{ needs.release_preflight.outputs.sha }} - run: | - set -euo pipefail - release_json="$(gh api "repos/$GITHUB_REPOSITORY/releases/tags/$RELEASE_TAG")" - release_id="$(jq -r '.id' <<< "$release_json")" - is_draft="$(jq -r '.draft' <<< "$release_json")" - if [[ "$is_draft" == "false" ]]; then - echo "GitHub Release '$RELEASE_TAG' is already published." - exit 0 - fi - if [[ "$is_draft" != "true" || ! "$release_id" =~ ^[0-9]+$ ]]; then - echo "::error::GitHub Release '$RELEASE_TAG' is not in a publishable draft state." + if [[ ! "$RELEASE_ID" =~ ^[1-9][0-9]*$ ]]; then + echo "::error::Missing immutable GitHub Release ID for '$RELEASE_TAG'." exit 1 fi - # Resolve the live remote tag immediately before publication, not the earlier checkout. + # Resolve the live tag before mutation; main's dispatch SHA is never product identity. ref_json="$(gh api "repos/$GITHUB_REPOSITORY/git/ref/tags/$RELEASE_TAG")" object_type="$(jq -r '.object.type' <<< "$ref_json")" object_sha="$(jq -r '.object.sha' <<< "$ref_json")" @@ -410,13 +440,49 @@ jobs: object_sha="$(jq -r '.object.sha' <<< "$tag_json")" done if [[ "$object_type" != "commit" || ! "$object_sha" =~ ^[0-9a-fA-F]{40}$ || "$object_sha" != "$RELEASE_SHA" ]]; then - echo "::error::Release tag '$RELEASE_TAG' no longer identifies built commit '$RELEASE_SHA'; refusing to publish the draft." + echo "::error::Release tag '$RELEASE_TAG' no longer identifies built commit '$RELEASE_SHA'; refusing to attach assets." exit 1 fi - gh api --method PATCH "repos/$GITHUB_REPOSITORY/releases/$release_id" -F draft=false + release_json="$(gh api "repos/$GITHUB_REPOSITORY/releases/$RELEASE_ID")" + if ! jq -e --arg tag "$RELEASE_TAG" --argjson id "$RELEASE_ID" \ + '.id == $id and .tag_name == $tag and .draft == true' <<< "$release_json" >/dev/null; then + echo "::error::Release ID '$RELEASE_ID' must still identify the draft for '$RELEASE_TAG'." + exit 1 + fi + assets="$(gh api --paginate --slurp "repos/$GITHUB_REPOSITORY/releases/$RELEASE_ID/assets?per_page=100")" + missing_assets=() + # Validate every existing asset before uploading anything. Never overwrite different bytes. + for asset_name in "$archive_name" "$checksum_name"; do + matches="$(jq -c --arg name "$asset_name" '[.[][] | select(.name == $name)]' <<< "$assets")" + count="$(jq 'length' <<< "$matches")" + if [[ "$count" -gt 1 ]]; then + echo "::error::Multiple assets named '$asset_name' exist on release ID '$RELEASE_ID'." + exit 1 + elif [[ "$count" -eq 1 ]]; then + asset_id="$(jq -r '.[0].id' <<< "$matches")" + gh api -H 'Accept: application/octet-stream' \ + "repos/$GITHUB_REPOSITORY/releases/assets/$asset_id" > "$existing_dir/$asset_name" + if ! cmp -s "$RUNNER_TEMP/docfx-release-asset/$asset_name" "$existing_dir/$asset_name"; then + echo "::error::Release asset '$asset_name' already exists with different bytes; refusing to overwrite it." + exit 1 + fi + echo "Exact asset '$asset_name' already attached to release ID '$RELEASE_ID'." + else + missing_assets+=("$asset_name") + fi + done + + for asset_name in "${missing_assets[@]}"; do + gh api --method POST \ + "https://uploads.github.com/repos/$GITHUB_REPOSITORY/releases/$RELEASE_ID/assets?name=$asset_name" \ + -H 'Content-Type: application/octet-stream' \ + --input "$RUNNER_TEMP/docfx-release-asset/$asset_name" > /dev/null + 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 @@ -443,6 +509,7 @@ jobs: 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: @@ -485,6 +552,7 @@ jobs: 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 @@ -539,49 +607,29 @@ jobs: command: down post_release_sonarcloud: - if: ${{ always() && needs.publish_nuget.result == 'success' }} + 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] - runs-on: ubuntu-26.04 - timeout-minutes: 45 permissions: contents: read - steps: - - name: Checkout the released SHA as main analysis - uses: codebeltnet/git-checkout@v1 - with: - ref: ${{ needs.release_preflight.outputs.sha }} - - - name: Install .NET - uses: codebeltnet/install-dotnet@v3 - - - name: Install .NET Tool - Sonar Scanner - uses: codebeltnet/dotnet-tool-install-sonarscanner@v3 - - - name: Restore released source dependencies - uses: codebeltnet/dotnet-restore@v3 - - - name: Begin SonarCloud analysis for the released version - uses: codebeltnet/sonarcloud-scan@v2 - with: - token: ${{ secrets.SONAR_TOKEN }} - organization: geekle - projectKey: Cuemon - version: ${{ needs.release_preflight.outputs.version }} - - - name: Build released source for SonarCloud - uses: codebeltnet/dotnet-build@v4 - with: - configuration: Release - build-switches: -p:SkipSignAssembly=true - - - name: Finalize SonarCloud analysis - uses: codebeltnet/sonarcloud-scan-finalize@v1 - with: - token: ${{ secrets.SONAR_TOKEN }} + uses: codebeltnet/jobs-sonarcloud/.github/workflows/default.yml@v3 + 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: ${{ always() && needs.publish_nuget.result == 'success' }} + 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] permissions: @@ -591,41 +639,86 @@ jobs: repository: codebeltnet/cuemon configuration: .github/codecov.yml ref: ${{ needs.release_preflight.outputs.sha }} + branch: main + commit: ${{ needs.release_preflight.outputs.sha }} secrets: CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} post_release_codeql: - if: ${{ always() && needs.publish_nuget.result == 'success' }} + 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] - runs-on: ubuntu-26.04 - timeout-minutes: 45 permissions: contents: read security-events: write - steps: - - name: Checkout the released SHA as main analysis - uses: codebeltnet/git-checkout@v1 - with: - ref: ${{ needs.release_preflight.outputs.sha }} - - - name: Install .NET - uses: codebeltnet/install-dotnet@v3 - - - name: Restore released source dependencies - uses: codebeltnet/dotnet-restore@v3 - - - name: Begin CodeQL analysis - uses: codebeltnet/codeql-scan@v1 - - - name: Build released source for CodeQL - uses: codebeltnet/dotnet-build@v4 - with: - configuration: Release - build-switches: -p:SkipSignAssembly=true + 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 - - name: Finalize CodeQL analysis - uses: codebeltnet/codeql-scan-finalize@v1 + 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() }} @@ -637,13 +730,13 @@ jobs: - draft_github_release - docfx_oci_build - upload_docfx_release_asset - - publish_github_release - prepare_release_tests - post_release_tests - post_release_integration_test - post_release_sonarcloud - post_release_codecov - post_release_codeql + - verify_assurance_identity runs-on: ubuntu-26.04 permissions: contents: read @@ -655,39 +748,101 @@ jobs: RELEASE_VERSION: ${{ needs.release_preflight.outputs.version }} RELEASE_SHA: ${{ needs.release_preflight.outputs.sha }} RELEASE_TAG: ${{ needs.release_preflight.outputs.tag }} + RELEASE_ID: ${{ needs.draft_github_release.outputs.release_id }} + SOURCE_RUN_ID: ${{ inputs.source_run_id }} + REQUESTED_TAG: ${{ inputs.tag }} + RECOVERY_MODE: ${{ inputs.recovery || 'assets' }} 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 }} - GITHUB_RELEASE_RESULT: ${{ needs.publish_github_release.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 }} run: | set -euo pipefail + if [[ "$GITHUB_EVENT_NAME" == "workflow_dispatch" ]]; then + if [[ "$RECOVERY_MODE" == "assurance" ]]; then + { + echo '## Post-release assurance recovery status' + echo "- Tag: \`${RELEASE_TAG:-$REQUESTED_TAG}\`" + echo "- Released SHA: \`${RELEASE_SHA:-not validated}\`" + 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 '- 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.' + exit 1 + fi + exit 0 + fi + { + echo "## Partial-release recovery status" + echo + echo "- Requested tag: \`${RELEASE_TAG:-$REQUESTED_TAG}\`" + echo "- Authoritative SHA: \`${RELEASE_SHA:-not validated}\`" + echo "- Source run: \`$SOURCE_RUN_ID\`" + echo "- Recovery validation: \`$PREFLIGHT_RESULT\`" + if [[ "$PREFLIGHT_RESULT" == "success" ]]; then + echo "- Git tag $RELEASE_TAG: valid" + echo "- NuGet v$RELEASE_VERSION: already published (source run confirmed); publication not invoked" + else + echo "- Existing product publication: not changed; source identity/publication not confirmed" + fi + if [[ "$DRAFT_RELEASE_RESULT" == "success" ]]; then + echo "- GitHub draft release: recovered (ID \`$RELEASE_ID\`)" + else + echo "- GitHub draft resolution: \`$DRAFT_RELEASE_RESULT\`" + fi + if [[ "$DOCFX_ASSET_RESULT" == "success" ]]; then + echo "- DocFX OCI artifact: recovered and verified from run $SOURCE_RUN_ID" + echo '- OCI assets: attached (exact existing assets accepted)' + echo '- Recovery: complete' + else + echo "- OCI recovery/attachment orchestration: \`$DOCFX_ASSET_RESULT\`" + echo '- Recovery incomplete; inspect the failed step. No immutable products were rebuilt or republished.' + fi + echo '- GitHub Release: manual publication required; recovery never publishes it' + echo '- Deployment: not started by this workflow' + } >> "$GITHUB_STEP_SUMMARY" + exit 0 + fi + { echo "## Cuemon release status" echo echo "- Tag: \`${RELEASE_TAG:-not validated}\`" 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 "- 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 "- Published GitHub Release: \`$GITHUB_RELEASE_RESULT\`" + echo "- GitHub Release ID: \`${RELEASE_ID:-not resolved}\`" + echo '- GitHub Release: awaiting manual publication once draft assets and assurance are reviewed' + echo '- Deployment: not started by this workflow' echo } >> "$GITHUB_STEP_SUMMARY" if [[ "$PREFLIGHT_RESULT" != "success" ]]; then { - echo "No release was attempted because the triggering tag, SemVer, main ancestry or existing release identity failed preflight validation." + echo "No release was attempted because the triggering tag, SemVer or main ancestry failed preflight validation." } >> "$GITHUB_STEP_SUMMARY" elif [[ "$PACKAGE_BUILD_RESULT" != "success" ]]; then { @@ -707,24 +862,25 @@ jobs: } >> "$GITHUB_STEP_SUMMARY" elif [[ "$DOCFX_BUILD_RESULT" != "success" || "$DOCFX_ASSET_RESULT" != "success" ]]; then { - echo "NuGet packages were published for the existing source tag, but the DocFX OCI artifact or release asset upload failed. GitHub Release publication waits for the verified OCI archive and checksum, so the draft remains unpublished." + echo "NuGet packages were published for the existing source tag, but DocFX finalization did not complete. If the DocFX-OCI-$RELEASE_VERSION Actions artifact was persisted, dispatch recovery from main with tag '$RELEASE_TAG' and source_run_id '$GITHUB_RUN_ID' to reuse those exact bytes. Do not replay NuGet publication. The GitHub Release awaits finalization and manual publication." } >> "$GITHUB_STEP_SUMMARY" - elif [[ "$GITHUB_RELEASE_RESULT" != "success" ]]; then + else { - echo "NuGet packages, the source tag and the OCI release assets are ready, but GitHub Release publication failed or could not be confirmed. Check whether the release is still a draft before retrying the idempotent publish job." + echo "NuGet packages are published and the verified OCI assets are attached to draft release ID '$RELEASE_ID'. Review/edit the draft and assurance results, then publish it manually to declare it deployable. Production environment approval remains a separate deployment decision." } >> "$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" ) ]]; then - echo "::warning::Product release succeeded; post-release assurance failed." + "$CODECOV_RESULT" != "success" || "$CODEQL_RESULT" != "success" || \ + "$IDENTITY_RESULT" != "success" ) ]]; then + echo "::warning::NuGet publication succeeded; post-release assurance did not complete successfully." { echo - echo "**Product release succeeded; post-release assurance failed.**" + 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\`." + 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." } >> "$GITHUB_STEP_SUMMARY" fi diff --git a/AGENTS.md b/AGENTS.md index eb904b59..8eb3b28a 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -285,7 +285,7 @@ Preserve manual documentation edits. Prefer additive changes, but correct stale Preserve working Markdown links, `Related:` references, and historical URL citations during prose rewrites. Remove or replace a URL only after directly verifying that the current destination returns HTTP 404. Timeouts, 403s, rate limits, DNS failures, and other lookup problems are not removal evidence. -Interim scratch artifacts do not belong in the repository working tree. Store assessment queues, project manifests, review reports, captured validator output, progress notes, and one-off helper scripts in temp or session storage instead. New working-tree files are only legitimate when they are the managed `AGENTS.md` block, the active `docfx.json`, the deterministic `skip-compile-allowlist.json` waiver file when one is truly required, or DocFX-authored namespace/type Markdown that maps to a real public namespace or type. Everything else is blocking cleanup work, not a documentation deliverable. The validator auto-detects generic-arity type families (such as `MutableTuple`1`..`MutableTuple`N`) and skips redundant sibling examples from the public API surface alone, so no family-skip manifest is ever written into the repository. +The following restrictions on new working-tree files apply only to files created as part of the DocFX documentation-maintenance process, not to the repository in general. Interim scratch artifacts from this process do not belong in the repository working tree. Store assessment queues, project manifests, review reports, captured validator output, progress notes, and one-off helper scripts in temp or session storage instead. New working-tree files are only legitimate when they are the managed `AGENTS.md` block, the active `docfx.json`, the deterministic `skip-compile-allowlist.json` waiver file when one is truly required, or DocFX-authored namespace/type Markdown that maps to a real public namespace or type. Everything else is blocking cleanup work, not a documentation deliverable. The validator auto-detects generic-arity type families (such as `MutableTuple`1`..`MutableTuple`N`) and skips redundant sibling examples from the public API surface alone, so no family-skip manifest is ever written into the repository. Skip markers are waivers, not fixes. A skip marker only suppresses compilation when it both existed before the current run and matches an entry in `.docfx/skip-compile-allowlist.json`. Each allowlist entry must include `diagnosticCode`, `filePath`, `uid` or `symbol`, `reason`, `approval`, and `lifetime` (`temporary` or `permanent`). Newly introduced or unallowlisted skip markers remain fail-level diagnostics and do not permit a completion claim.