From d7d4854205c1e446a0cf38f20322dc5d1c8a25bd Mon Sep 17 00:00:00 2001 From: Rodrigo de Oliveira Date: Thu, 1 Oct 2026 10:00:33 -0300 Subject: [PATCH 01/20] fix: decouple NuGet indexing from release --- .github/workflows/release.yml | 173 +--------------------------------- 1 file changed, 1 insertion(+), 172 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 472cd28..b83d93b 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -247,7 +247,7 @@ jobs: '~~~' \ "" \ "The attached .nupkg files are the exact validated payload published to NuGet.org." \ - "Repo2C4.Mcp registry metadata is versioned in server.json and is published to the Official MCP Registry after NuGet consumer verification." \ + "Repo2C4.Mcp registry metadata is versioned in server.json and is published separately to the Official MCP Registry after the matching NuGet package becomes publicly consumable." \ "Verify downloads with SHA256SUMS. See docs/distribution.md for runtime requirements, MCP configuration, registry publication and security guidance." \ > "$notes_file" gh release create "$tag" \ @@ -335,174 +335,3 @@ jobs: echo "::error::Published release assets differ from the verified payload." exit 1 fi - - - post-publish: - name: Verify public consumer installation - needs: publish - if: ${{ needs.publish.result == 'success' }} - runs-on: ubuntu-latest - permissions: - contents: read - steps: - - name: Set up .NET 10 - uses: actions/setup-dotnet@a98b56852c35b8e3190ac28c8c2271da59106c68 # v6 - with: - dotnet-version: '10.0.x' - - - name: Install and exercise the published NuGet tools - shell: bash - env: - VERSION: ${{ inputs.version }} - run: | - set -euo pipefail - work="$(mktemp -d)" - trap 'rm -rf "$work"' EXIT - - install_with_retry() { - local package="$1" - local path="$2" - local attempt - for attempt in {1..12}; do - if dotnet tool install --tool-path "$path" "$package" --version "$VERSION" \ - --add-source https://api.nuget.org/v3/index.json \ - --ignore-failed-sources; then - return 0 - fi - if [ "$attempt" -eq 12 ]; then - echo "::error::$package $VERSION was not consumable from NuGet.org after publication." - return 1 - fi - sleep 10 - done - } - - install_with_retry Repo2C4.Cli "$work/cli" - install_with_retry Repo2C4.Mcp "$work/mcp" - install_with_retry Repo2C4.Agent "$work/agent" - - "$work/cli/repo2c4" --help | grep -Fq 'Repo2C4 CLI' - if ! "$work/mcp/repo2c4-mcp" --help >"$work/mcp.stdout" 2>"$work/mcp.stderr"; then - cat "$work/mcp.stderr" >&2 - exit 1 - fi - test ! -s "$work/mcp.stdout" - grep -Fq 'stdout is reserved exclusively for MCP protocol messages' "$work/mcp.stderr" - "$work/agent/repo2c4-agent" --help >"$work/agent.stdout" 2>"$work/agent.stderr" - test ! -s "$work/agent.stdout" - grep -Fq 'Repo2C4 Agent host' "$work/agent.stderr" - - echo "Published Repo2C4 CLI, MCP and Agent $VERSION installed and executed successfully from NuGet.org." - - - publish-mcp-registry: - name: Publish Repo2C4 MCP to Official Registry - needs: post-publish - if: ${{ needs.post-publish.result == 'success' }} - runs-on: ubuntu-latest - permissions: - contents: read - id-token: write - steps: - - name: Checkout the exact released commit - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7 - with: - ref: ${{ github.sha }} - persist-credentials: false - - - name: Set up .NET 10 - uses: actions/setup-dotnet@a98b56852c35b8e3190ac28c8c2271da59106c68 # v6 - with: - dotnet-version: '10.0.x' - - - name: Verify registry metadata and NuGet availability - shell: bash - env: - VERSION: ${{ inputs.version }} - run: | - set -euo pipefail - test "$(jq -r '.name' server.json)" = "io.github.rodri-oliveira-dev/repo2c4-mcp" - test "$(jq -r '.version' server.json)" = "$VERSION" - test "$(jq -r '.packages | length' server.json)" -eq 1 - test "$(jq -r '.packages[0].registryType' server.json)" = "nuget" - test "$(jq -r '.packages[0].identifier' server.json)" = "Repo2C4.Mcp" - test "$(jq -r '.packages[0].version' server.json)" = "$VERSION" - test "$(jq -r '.packages[0].runtimeHint' server.json)" = "dnx" - test "$(jq -r '.packages[0].transport.type' server.json)" = "stdio" - grep -Fq '' src/Repo2C4.Mcp/README.md - - work="$(mktemp -d)" - trap 'rm -rf "$work"' EXIT - for attempt in {1..12}; do - if dotnet tool install --tool-path "$work/tool" Repo2C4.Mcp --version "$VERSION" \ - --add-source https://api.nuget.org/v3/index.json \ - --ignore-failed-sources >/dev/null 2>&1; then - break - fi - if [ "$attempt" -eq 12 ]; then - echo "::error::Repo2C4.Mcp $VERSION is not yet consumable from NuGet.org." - exit 1 - fi - sleep 10 - done - "$work/tool/repo2c4-mcp" --help >"$work/stdout" 2>"$work/stderr" - test ! -s "$work/stdout" - grep -Fq 'stdout is reserved exclusively for MCP protocol messages' "$work/stderr" - - - name: Install pinned mcp-publisher - shell: bash - env: - MCP_PUBLISHER_VERSION: '1.8.1' - MCP_PUBLISHER_SHA256: 'a06c9096dcb9727c13555b6be26c7effa707b01f06a4c561ba7a3635443cf2cc' - run: | - set -euo pipefail - archive="$RUNNER_TEMP/mcp-publisher.tar.gz" - curl --fail --location --silent --show-error \ - "https://github.com/modelcontextprotocol/registry/releases/download/v$MCP_PUBLISHER_VERSION/mcp-publisher_linux_amd64.tar.gz" \ - --output "$archive" - echo "$MCP_PUBLISHER_SHA256 $archive" | sha256sum --check - - tar -xzf "$archive" -C "$RUNNER_TEMP" mcp-publisher - chmod +x "$RUNNER_TEMP/mcp-publisher" - "$RUNNER_TEMP/mcp-publisher" --help >/dev/null - - - name: Validate and publish MCP registry metadata - shell: bash - env: - VERSION: ${{ inputs.version }} - run: | - set -euo pipefail - publisher="$RUNNER_TEMP/mcp-publisher" - registry="https://registry.modelcontextprotocol.io" - server_name="io.github.rodri-oliveira-dev%2Frepo2c4-mcp" - detail_url="$registry/v0.1/servers/$server_name/versions/$VERSION" - - "$publisher" validate server.json - - status="$(curl --silent --show-error --output "$RUNNER_TEMP/registry-existing.json" \ - --write-out '%{http_code}' "$detail_url")" - if [ "$status" = "200" ]; then - echo "Repo2C4 MCP $VERSION is already present in the Official MCP Registry." - exit 0 - fi - if [ "$status" != "404" ]; then - cat "$RUNNER_TEMP/registry-existing.json" >&2 || true - echo "::error::Unexpected MCP Registry lookup status: $status" - exit 1 - fi - - "$publisher" login github-oidc - "$publisher" publish server.json - - for attempt in {1..12}; do - status="$(curl --silent --show-error --output "$RUNNER_TEMP/registry-published.json" \ - --write-out '%{http_code}' "$detail_url")" - if [ "$status" = "200" ]; then - echo "Published Repo2C4 MCP $VERSION to the Official MCP Registry." - exit 0 - fi - sleep 5 - done - - cat "$RUNNER_TEMP/registry-published.json" >&2 || true - echo "::error::MCP Registry publication was not observable after publishing." - exit 1 From 6ffc2d3dc5d3610e9d292c212636f11d1e4f904d Mon Sep 17 00:00:00 2001 From: Rodrigo de Oliveira Date: Thu, 1 Oct 2026 10:01:36 -0300 Subject: [PATCH 02/20] feat: add standalone MCP registry publication workflow --- .github/workflows/publish-mcp-registry.yml | 191 +++++++++++++++++++++ 1 file changed, 191 insertions(+) create mode 100644 .github/workflows/publish-mcp-registry.yml diff --git a/.github/workflows/publish-mcp-registry.yml b/.github/workflows/publish-mcp-registry.yml new file mode 100644 index 0000000..b2ddcfa --- /dev/null +++ b/.github/workflows/publish-mcp-registry.yml @@ -0,0 +1,191 @@ +name: Publish Repo2C4 MCP Registry metadata + +run-name: "Publish Repo2C4 MCP ${{ inputs.version }} to Official Registry" + +on: + workflow_dispatch: + inputs: + version: + description: "Released semantic version without the v prefix, for example 1.0.0." + required: true + type: string + confirmation: + description: "Type PUBLISH-MCP to publish the released MCP metadata." + required: true + type: string + +permissions: + contents: read + +concurrency: + group: "mcp-registry-${{ inputs.version }}" + cancel-in-progress: false + +jobs: + publish-mcp-registry: + name: Publish Repo2C4 MCP to Official Registry + runs-on: ubuntu-latest + environment: release + permissions: + contents: read + id-token: write + + steps: + - name: Validate request and resolve released commit + id: release + shell: bash + env: + GH_TOKEN: ${{ github.token }} + VERSION: ${{ inputs.version }} + CONFIRMATION: ${{ inputs.confirmation }} + run: | + set -euo pipefail + + test "$GITHUB_REF" = refs/heads/main + + if [[ ! "$VERSION" =~ ^(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)(-[0-9A-Za-z-]+(\.[0-9A-Za-z-]+)*)?$ ]]; then + echo "::error::Invalid semantic version '$VERSION'." + exit 1 + fi + + if [ "$CONFIRMATION" != "PUBLISH-MCP" ]; then + echo "::error::Confirmation must be PUBLISH-MCP." + exit 1 + fi + + tag="v$VERSION" + tag_json="$(gh api "repos/$GITHUB_REPOSITORY/git/ref/tags/$tag")" + tag_type="$(jq -r '.object.type' <<<"$tag_json")" + tag_sha="$(jq -r '.object.sha' <<<"$tag_json")" + + while [ "$tag_type" = "tag" ]; do + tag_object_json="$(gh api "repos/$GITHUB_REPOSITORY/git/tags/$tag_sha")" + tag_type="$(jq -r '.object.type' <<<"$tag_object_json")" + tag_sha="$(jq -r '.object.sha' <<<"$tag_object_json")" + done + + if [ "$tag_type" != "commit" ]; then + echo "::error::Release tag $tag does not resolve to a commit." + exit 1 + fi + + release_json="$(gh release view "$tag" --repo "$GITHUB_REPOSITORY" --json isDraft,tagName)" + if [ "$(jq -r '.isDraft' <<<"$release_json")" != "false" ]; then + echo "::error::GitHub Release $tag is still a draft." + exit 1 + fi + if [ "$(jq -r '.tagName' <<<"$release_json")" != "$tag" ]; then + echo "::error::GitHub Release tag does not match $tag." + exit 1 + fi + + echo "commit=$tag_sha" >> "$GITHUB_OUTPUT" + echo "Released commit: $tag_sha" + + - name: Checkout the exact released commit + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7 + with: + ref: ${{ steps.release.outputs.commit }} + persist-credentials: false + show-progress: false + + - name: Set up .NET 10 + uses: actions/setup-dotnet@a98b56852c35b8e3190ac28c8c2271da59106c68 # v6 + with: + dotnet-version: '10.0.x' + + - name: Verify registry metadata and NuGet availability + shell: bash + env: + VERSION: ${{ inputs.version }} + run: | + set -euo pipefail + test "$(jq -r '.name' server.json)" = "io.github.rodri-oliveira-dev/repo2c4-mcp" + test "$(jq -r '.version' server.json)" = "$VERSION" + test "$(jq -r '.packages | length' server.json)" -eq 1 + test "$(jq -r '.packages[0].registryType' server.json)" = "nuget" + test "$(jq -r '.packages[0].identifier' server.json)" = "Repo2C4.Mcp" + test "$(jq -r '.packages[0].version' server.json)" = "$VERSION" + test "$(jq -r '.packages[0].runtimeHint' server.json)" = "dnx" + test "$(jq -r '.packages[0].transport.type' server.json)" = "stdio" + grep -Fq '' src/Repo2C4.Mcp/README.md + + work="$(mktemp -d)" + trap 'rm -rf "$work"' EXIT + for attempt in {1..12}; do + if dotnet tool install --tool-path "$work/tool" Repo2C4.Mcp --version "$VERSION" \ + --add-source https://api.nuget.org/v3/index.json \ + --ignore-failed-sources >/dev/null 2>&1; then + break + fi + if [ "$attempt" -eq 12 ]; then + echo "::error::Repo2C4.Mcp $VERSION is not yet consumable from NuGet.org. The release is already complete; rerun this registry workflow after NuGet indexing finishes." + exit 1 + fi + sleep 10 + done + + "$work/tool/repo2c4-mcp" --help >"$work/stdout" 2>"$work/stderr" + test ! -s "$work/stdout" + grep -Fq 'stdout is reserved exclusively for MCP protocol messages' "$work/stderr" + + - name: Install pinned mcp-publisher + shell: bash + env: + MCP_PUBLISHER_VERSION: '1.8.1' + MCP_PUBLISHER_SHA256: 'a06c9096dcb9727c13555b6be26c7effa707b01f06a4c561ba7a3635443cf2cc' + run: | + set -euo pipefail + archive="$RUNNER_TEMP/mcp-publisher.tar.gz" + curl --fail --location --silent --show-error \ + --proto '=https' --proto-redir '=https' \ + "https://github.com/modelcontextprotocol/registry/releases/download/v$MCP_PUBLISHER_VERSION/mcp-publisher_linux_amd64.tar.gz" \ + --output "$archive" + echo "$MCP_PUBLISHER_SHA256 $archive" | sha256sum --check - + tar -xzf "$archive" -C "$RUNNER_TEMP" mcp-publisher + chmod +x "$RUNNER_TEMP/mcp-publisher" + "$RUNNER_TEMP/mcp-publisher" --help >/dev/null + + - name: Validate and publish MCP registry metadata + shell: bash + env: + VERSION: ${{ inputs.version }} + run: | + set -euo pipefail + publisher="$RUNNER_TEMP/mcp-publisher" + registry="https://registry.modelcontextprotocol.io" + server_name="io.github.rodri-oliveira-dev%2Frepo2c4-mcp" + detail_url="$registry/v0.1/servers/$server_name/versions/$VERSION" + + "$publisher" validate server.json + + status="$(curl --silent --show-error --proto '=https' --proto-redir '=https' \ + --output "$RUNNER_TEMP/registry-existing.json" \ + --write-out '%{http_code}' "$detail_url")" + if [ "$status" = "200" ]; then + echo "Repo2C4 MCP $VERSION is already present in the Official MCP Registry." + exit 0 + fi + if [ "$status" != "404" ]; then + cat "$RUNNER_TEMP/registry-existing.json" >&2 || true + echo "::error::Unexpected MCP Registry lookup status: $status" + exit 1 + fi + + "$publisher" login github-oidc + "$publisher" publish server.json + + for attempt in {1..12}; do + status="$(curl --silent --show-error --proto '=https' --proto-redir '=https' \ + --output "$RUNNER_TEMP/registry-published.json" \ + --write-out '%{http_code}' "$detail_url")" + if [ "$status" = "200" ]; then + echo "Published Repo2C4 MCP $VERSION to the Official MCP Registry." + exit 0 + fi + sleep 5 + done + + cat "$RUNNER_TEMP/registry-published.json" >&2 || true + echo "::error::MCP Registry publication was not observable after publishing." + exit 1 From 376356c622267c1dd3616bc0d60c40b43936a606 Mon Sep 17 00:00:00 2001 From: Rodrigo de Oliveira Date: Thu, 1 Oct 2026 10:01:38 -0300 Subject: [PATCH 03/20] docs: clarify asynchronous NuGet indexing --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 34b922e..a8f9e1a 100644 --- a/README.md +++ b/README.md @@ -325,7 +325,7 @@ CLI help is written to stdout. MCP help and diagnostics are written **only to st `.github/workflows/ci.yml` validates locked restore, formatting, Release build, tests, coverage, pinned LikeC4 integration, the complete offline CLI cycle (`inspect -> reviewed model -> generate -> validate`) and the full MCP protocol-client C1/C2 flow without paid AI or a proprietary client. CodeQL, Dependency Review and optional SonarQube Cloud checks remain available; [Sonar setup](docs/sonarqube-cloud.md) requires `SONAR_TOKEN`. -**Distribution:** the three versioned .NET tools, `Repo2C4.Cli`, `Repo2C4.Mcp` and `Repo2C4.Agent`, are clean-install tested. Public release remains manually gated; an authorized release publishes the validated packages to NuGet.org and GitHub Release, then verifies consumer installation. `Repo2C4.Mcp` also includes versioned Official MCP Registry metadata in [`server.json`](server.json); registry publication happens only after the matching NuGet package is public. `Repo2C4.Agent` remains a local .NET Tool/MCP client rather than a marketplace-specific agent package. See the [installation, security and release guide](docs/distribution.md) or [Português](docs/distribution.pt-BR.md). +**Distribution:** the three versioned .NET tools, `Repo2C4.Cli`, `Repo2C4.Mcp` and `Repo2C4.Agent`, are clean-install tested. Public release remains manually gated; an authorized release publishes the validated packages to NuGet.org and GitHub Release. NuGet indexing is asynchronous and is not used as a post-publication release gate. `Repo2C4.Mcp` also includes versioned Official MCP Registry metadata in [`server.json`](server.json); registry publication is a separate manual workflow run only after the matching NuGet package is publicly consumable. `Repo2C4.Agent` remains a local .NET Tool/MCP client rather than a marketplace-specific agent package. See the [installation, security and release guide](docs/distribution.md) or [Português](docs/distribution.pt-BR.md). From 99de847f897be52c30a00f8307b5fdd99e1b8e9a Mon Sep 17 00:00:00 2001 From: Rodrigo de Oliveira Date: Thu, 1 Oct 2026 10:01:40 -0300 Subject: [PATCH 04/20] docs: clarify asynchronous NuGet indexing in Portuguese --- README.pt-BR.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.pt-BR.md b/README.pt-BR.md index 66d4267..809680b 100644 --- a/README.pt-BR.md +++ b/README.pt-BR.md @@ -249,4 +249,4 @@ O [CI](.github/workflows/ci.yml) verifica restore locked, políticas de build, f ### Distribuição e automação -As ferramentas públicas são `Repo2C4.Cli` (comando `repo2c4`), `Repo2C4.Mcp` (comando `repo2c4-mcp`) e `Repo2C4.Agent` (comando `repo2c4-agent`). O LikeC4 continua uma dependência independente. A release pública é manual/protegida e publica os três pacotes no NuGet.org e GitHub Release. O `Repo2C4.Mcp` também possui metadata versionada para o Official MCP Registry em [`server.json`](server.json), publicada somente depois que a mesma versão estiver disponível no NuGet. O `Repo2C4.Agent` permanece uma .NET Tool local/cliente MCP, sem acoplamento a marketplace específico. Consulte [distribuição](docs/distribution.pt-BR.md) e [workflow de PR arquitetural](docs/architecture-pr.pt-BR.md). +As ferramentas públicas são `Repo2C4.Cli` (comando `repo2c4`), `Repo2C4.Mcp` (comando `repo2c4-mcp`) e `Repo2C4.Agent` (comando `repo2c4-agent`). O LikeC4 continua uma dependência independente. A release pública é manual/protegida e publica os três pacotes no NuGet.org e GitHub Release. A indexação do NuGet é assíncrona e não é usada como gate pós-publicação da release. O `Repo2C4.Mcp` também possui metadata versionada para o Official MCP Registry em [`server.json`](server.json), publicada por um workflow manual separado somente depois que a mesma versão estiver publicamente consumível no NuGet. O `Repo2C4.Agent` permanece uma .NET Tool local/cliente MCP, sem acoplamento a marketplace específico. Consulte [distribuição](docs/distribution.pt-BR.md) e [workflow de PR arquitetural](docs/architecture-pr.pt-BR.md). From 9503790426dec7f33fdac9543753dd8780361af6 Mon Sep 17 00:00:00 2001 From: Rodrigo de Oliveira Date: Thu, 1 Oct 2026 10:01:43 -0300 Subject: [PATCH 05/20] docs: document standalone MCP registry publishing --- docs/distribution.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/distribution.md b/docs/distribution.md index 4e8958d..74a9d96 100644 --- a/docs/distribution.md +++ b/docs/distribution.md @@ -47,7 +47,7 @@ NuGet ownership verification depends on the package README containing this exact The marker is packaged from `src/Repo2C4.Mcp/README.md`. The distribution smoke verifies the registry name, package/version binding, required root argument and ownership marker before a release can proceed. -The Official MCP Registry stores metadata rather than the NuGet artifact, so the matching NuGet package must be public first. The protected release workflow enforces that ordering: it publishes GitHub Release/NuGet, verifies that the public `Repo2C4.Mcp` package can be installed, and only then runs the registry publication job. +The Official MCP Registry stores metadata rather than the NuGet artifact, so the matching NuGet package must be public first. NuGet indexing is asynchronous, therefore public installation is deliberately not a gate of the immutable release. The protected release workflow finishes after the validated NuGet packages and GitHub Release are published. A separate manual `Publish Repo2C4 MCP Registry metadata` workflow verifies that the released `Repo2C4.Mcp` version is publicly consumable and only then publishes the matching Registry metadata. Registry authentication uses the Official MCP Registry's GitHub Actions OIDC flow. The registry job receives only `contents: read` and `id-token: write`; it does not require a PAT or long-lived MCP Registry secret. The job downloads a pinned `mcp-publisher` release, verifies its SHA-256, validates `server.json`, checks whether that exact server/version already exists, publishes only when absent, and verifies that the version becomes readable through the registry API. @@ -59,7 +59,7 @@ mcp-publisher login github mcp-publisher publish server.json ``` -A duplicate immutable version must not be republished. If the registry job fails after NuGet/GitHub publication, rerun the failed registry job from the same released commit; do not rebuild or change `server.json` for that version. +A duplicate immutable version must not be republished. If NuGet indexing has not completed yet, the registry workflow may fail without affecting the already completed release; rerun that registry workflow later for the same version. Do not rebuild or change `server.json` for that released version. When preparing a later Repo2C4 release, update `Directory.Build.props` and both version fields in `server.json` to the same exact SemVer before running distribution/release verification. From a8e3da321afd4626974a6146820eb46819eff12e Mon Sep 17 00:00:00 2001 From: Rodrigo de Oliveira Date: Thu, 1 Oct 2026 10:01:45 -0300 Subject: [PATCH 06/20] docs: document standalone MCP registry publishing in Portuguese --- docs/distribution.pt-BR.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/distribution.pt-BR.md b/docs/distribution.pt-BR.md index 97e0922..b783872 100644 --- a/docs/distribution.pt-BR.md +++ b/docs/distribution.pt-BR.md @@ -47,7 +47,7 @@ A validação de ownership do pacote NuGet depende deste marcador exato no READM O marcador vem de `src/Repo2C4.Mcp/README.md`. O smoke de distribuição valida nome do Registry, vínculo package/version, argumento obrigatório da raiz e marcador de ownership antes da release. -O Official MCP Registry armazena metadata, não o pacote NuGet, então o pacote correspondente precisa estar público primeiro. O workflow protegido de release garante essa ordem: publica GitHub Release/NuGet, verifica que o `Repo2C4.Mcp` público pode ser instalado e só então executa o job de publicação no Registry. +O Official MCP Registry armazena metadata, não o pacote NuGet, então o pacote correspondente precisa estar público primeiro. Como a indexação do NuGet é assíncrona, a instalação pública não é usada como gate da release imutável. O workflow protegido de release termina depois de publicar os pacotes NuGet validados e a GitHub Release. Um workflow manual separado, `Publish Repo2C4 MCP Registry metadata`, verifica que a versão publicada de `Repo2C4.Mcp` já está publicamente consumível e só então publica a metadata correspondente no Registry. A autenticação usa o fluxo oficial GitHub Actions OIDC. O job recebe somente `contents: read` e `id-token: write`; não exige PAT nem secret persistente específico do MCP Registry. O workflow baixa uma versão fixada do `mcp-publisher`, confere seu SHA-256, valida `server.json`, verifica se aquela versão exata já existe, publica apenas quando ausente e confirma a leitura da versão pela API do Registry. @@ -59,7 +59,7 @@ mcp-publisher login github mcp-publisher publish server.json ``` -Uma versão imutável duplicada não deve ser republicada. Se o job do Registry falhar depois que NuGet/GitHub já estiverem públicos, repita somente o job que falhou a partir do mesmo commit da release; não reconstrua nem altere `server.json` para aquela versão. +Uma versão imutável duplicada não deve ser republicada. Se a indexação do NuGet ainda não tiver terminado, o workflow do Registry pode falhar sem afetar a release já concluída; execute novamente esse workflow mais tarde para a mesma versão. Não reconstrua nem altere `server.json` para essa versão publicada. Ao preparar uma release futura, atualize `Directory.Build.props` e os dois campos de versão de `server.json` para o mesmo SemVer exato antes de executar a verificação de distribuição/release. From 6ca648d35a970360d614ae7c49c27cac1395763f Mon Sep 17 00:00:00 2001 From: Rodrigo de Oliveira Date: Thu, 1 Oct 2026 10:08:46 -0300 Subject: [PATCH 07/20] fix: shorten MCP registry description --- server.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server.json b/server.json index 67c0169..0fc7056 100644 --- a/server.json +++ b/server.json @@ -2,7 +2,7 @@ "$schema": "https://static.modelcontextprotocol.io/schemas/2025-12-11/server.schema.json", "name": "io.github.rodri-oliveira-dev/repo2c4-mcp", "title": "Repo2C4 MCP", - "description": "Evidence-first MCP server for generating reviewable C4 architecture documentation from authorized .NET repositories.", + "description": "Evidence-first MCP server for reviewable C4 architecture documentation from .NET repositories.", "version": "1.0.0", "websiteUrl": "https://github.com/rodri-oliveira-dev/Repo2C4", "repository": { From 4ae99324848e319b28e4a9c8bb9fc604c52c6a32 Mon Sep 17 00:00:00 2001 From: Rodrigo de Oliveira Date: Thu, 1 Oct 2026 10:10:01 -0300 Subject: [PATCH 08/20] fix: allow registry metadata correction after release --- .github/workflows/publish-mcp-registry.yml | 35 ++++++++++++++-------- 1 file changed, 23 insertions(+), 12 deletions(-) diff --git a/.github/workflows/publish-mcp-registry.yml b/.github/workflows/publish-mcp-registry.yml index b2ddcfa..6918817 100644 --- a/.github/workflows/publish-mcp-registry.yml +++ b/.github/workflows/publish-mcp-registry.yml @@ -82,10 +82,19 @@ jobs: echo "commit=$tag_sha" >> "$GITHUB_OUTPUT" echo "Released commit: $tag_sha" - - name: Checkout the exact released commit + - name: Checkout released package source uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7 with: ref: ${{ steps.release.outputs.commit }} + path: released-source + persist-credentials: false + show-progress: false + + - name: Checkout current registry metadata + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7 + with: + ref: ${{ github.sha }} + path: registry-metadata persist-credentials: false show-progress: false @@ -100,15 +109,16 @@ jobs: VERSION: ${{ inputs.version }} run: | set -euo pipefail - test "$(jq -r '.name' server.json)" = "io.github.rodri-oliveira-dev/repo2c4-mcp" - test "$(jq -r '.version' server.json)" = "$VERSION" - test "$(jq -r '.packages | length' server.json)" -eq 1 - test "$(jq -r '.packages[0].registryType' server.json)" = "nuget" - test "$(jq -r '.packages[0].identifier' server.json)" = "Repo2C4.Mcp" - test "$(jq -r '.packages[0].version' server.json)" = "$VERSION" - test "$(jq -r '.packages[0].runtimeHint' server.json)" = "dnx" - test "$(jq -r '.packages[0].transport.type' server.json)" = "stdio" - grep -Fq '' src/Repo2C4.Mcp/README.md + metadata="registry-metadata/server.json" + test "$(jq -r '.name' "$metadata")" = "io.github.rodri-oliveira-dev/repo2c4-mcp" + test "$(jq -r '.version' "$metadata")" = "$VERSION" + test "$(jq -r '.packages | length' "$metadata")" -eq 1 + test "$(jq -r '.packages[0].registryType' "$metadata")" = "nuget" + test "$(jq -r '.packages[0].identifier' "$metadata")" = "Repo2C4.Mcp" + test "$(jq -r '.packages[0].version' "$metadata")" = "$VERSION" + test "$(jq -r '.packages[0].runtimeHint' "$metadata")" = "dnx" + test "$(jq -r '.packages[0].transport.type' "$metadata")" = "stdio" + grep -Fq '' released-source/src/Repo2C4.Mcp/README.md work="$(mktemp -d)" trap 'rm -rf "$work"' EXIT @@ -157,7 +167,8 @@ jobs: server_name="io.github.rodri-oliveira-dev%2Frepo2c4-mcp" detail_url="$registry/v0.1/servers/$server_name/versions/$VERSION" - "$publisher" validate server.json + metadata="registry-metadata/server.json" + "$publisher" validate "$metadata" status="$(curl --silent --show-error --proto '=https' --proto-redir '=https' \ --output "$RUNNER_TEMP/registry-existing.json" \ @@ -173,7 +184,7 @@ jobs: fi "$publisher" login github-oidc - "$publisher" publish server.json + "$publisher" publish "$metadata" for attempt in {1..12}; do status="$(curl --silent --show-error --proto '=https' --proto-redir '=https' \ From f59816c4ae3163dbf6cddd43dc8e67e19f40d9c4 Mon Sep 17 00:00:00 2001 From: Rodrigo de Oliveira Date: Thu, 1 Oct 2026 10:10:24 -0300 Subject: [PATCH 09/20] docs: allow registry metadata-only corrections --- docs/distribution.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/distribution.md b/docs/distribution.md index 74a9d96..dd9a235 100644 --- a/docs/distribution.md +++ b/docs/distribution.md @@ -59,7 +59,7 @@ mcp-publisher login github mcp-publisher publish server.json ``` -A duplicate immutable version must not be republished. If NuGet indexing has not completed yet, the registry workflow may fail without affecting the already completed release; rerun that registry workflow later for the same version. Do not rebuild or change `server.json` for that released version. +A duplicate immutable package version must not be republished. If NuGet indexing has not completed yet, the registry workflow may fail without affecting the already completed release; rerun that registry workflow later for the same version. Registry-only metadata corrections in `server.json` are allowed after release when needed to satisfy Registry validation, provided the package identifier and version remain bound to the already-published immutable NuGet release. When preparing a later Repo2C4 release, update `Directory.Build.props` and both version fields in `server.json` to the same exact SemVer before running distribution/release verification. From 1e36534939c39abd3a17dccee8352b5bfa15aa80 Mon Sep 17 00:00:00 2001 From: Rodrigo de Oliveira Date: Thu, 1 Oct 2026 10:10:27 -0300 Subject: [PATCH 10/20] docs: allow registry metadata-only corrections in Portuguese --- docs/distribution.pt-BR.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/distribution.pt-BR.md b/docs/distribution.pt-BR.md index b783872..912a365 100644 --- a/docs/distribution.pt-BR.md +++ b/docs/distribution.pt-BR.md @@ -59,7 +59,7 @@ mcp-publisher login github mcp-publisher publish server.json ``` -Uma versão imutável duplicada não deve ser republicada. Se a indexação do NuGet ainda não tiver terminado, o workflow do Registry pode falhar sem afetar a release já concluída; execute novamente esse workflow mais tarde para a mesma versão. Não reconstrua nem altere `server.json` para essa versão publicada. +Uma versão imutável duplicada do pacote não deve ser republicada. Se a indexação do NuGet ainda não tiver terminado, o workflow do Registry pode falhar sem afetar a release já concluída; execute novamente esse workflow mais tarde para a mesma versão. Correções apenas de metadata em `server.json` são permitidas após a release quando necessárias para atender à validação do Registry, desde que o identificador e a versão do pacote continuem vinculados à release NuGet imutável já publicada. Ao preparar uma release futura, atualize `Directory.Build.props` e os dois campos de versão de `server.json` para o mesmo SemVer exato antes de executar a verificação de distribuição/release. From 269040a3aa5e54a008245afe177223cdd20ef9c2 Mon Sep 17 00:00:00 2001 From: Rodrigo de Oliveira Date: Thu, 1 Oct 2026 10:17:54 -0300 Subject: [PATCH 11/20] refactor: split release into governed stages --- .github/workflows/release.yml | 575 ++++++++++++++++++++++------------ 1 file changed, 370 insertions(+), 205 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index b83d93b..560fa28 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -1,43 +1,68 @@ -name: Verify and optionally release Repo2C4 tools +name: Release + +run-name: "Release ${{ inputs.version }} from ${{ github.ref_name }}" on: workflow_dispatch: inputs: version: - description: 'Exact version already declared in Directory.Build.props (no implicit version override)' + description: "Exact version already declared in Directory.Build.props." type: string required: true - default: '1.0.0' + default: "1.0.0" publish_release: - description: 'Explicitly publish GitHub Release and all three .NET tools to NuGet.org; MCP Registry metadata follows after public verification' + description: "Publish the validated tools to NuGet.org, GitHub Packages and GitHub Releases." type: boolean required: true default: false publication_confirmation: - description: 'Type PUBLISH only when deliberately publishing a public GitHub Release; otherwise leave NO' + description: "Type PUBLISH only when deliberately publishing the release; otherwise leave NO." type: string required: true - default: 'NO' + default: "NO" permissions: contents: read concurrency: - group: repo2c4-manual-release + group: repo2c4-release cancel-in-progress: false +env: + DOTNET_CLI_TELEMETRY_OPTOUT: "true" + DOTNET_SKIP_FIRST_TIME_EXPERIENCE: "true" + DOTNET_NOLOGO: "true" + CI: "true" + jobs: - dry-run: - name: Verify packages and clean install + build-and-pack: + name: Validate, build, test and pack if: ${{ github.ref == 'refs/heads/main' && github.event.repository.fork == false }} runs-on: ubuntu-latest + timeout-minutes: 60 permissions: contents: read outputs: - publishing: ${{ steps.inputs.outputs.publishing }} + publishing: ${{ steps.release.outputs.publishing }} + release-tag: ${{ steps.release.outputs.release_tag }} + is-prerelease: ${{ steps.release.outputs.is_prerelease }} + steps: - - name: Check explicit version and release mode - id: inputs + - name: Checkout selected main commit + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7 + with: + ref: ${{ github.sha }} + fetch-depth: 0 + persist-credentials: false + show-progress: false + + - name: Set up .NET 10 + uses: actions/setup-dotnet@a98b56852c35b8e3190ac28c8c2271da59106c68 # v6 + with: + dotnet-version: "10.0.x" + + - name: Validate release request and version + id: release shell: bash env: VERSION: ${{ inputs.version }} @@ -45,45 +70,107 @@ jobs: CONFIRMATION: ${{ inputs.publication_confirmation }} run: | set -euo pipefail - if [[ ! "$VERSION" =~ ^[0-9]+\.[0-9]+\.[0-9]+(-[0-9A-Za-z.-]+)?$ ]]; then - echo "::error::Version must be a valid project SemVer identifier." + + if [ "$GITHUB_REF" != "refs/heads/main" ]; then + echo "::error::Releases must be started from main. Current ref: $GITHUB_REF" + exit 1 + fi + + if [[ ! "$VERSION" =~ ^(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)(-[0-9A-Za-z-]+(\.[0-9A-Za-z-]+)*)?$ ]]; then + echo "::error::Version must be a valid semantic version without the v prefix." exit 1 fi - if [[ "$PUBLISH_RELEASE" == true && "$CONFIRMATION" != PUBLISH ]]; then + + if [[ "$PUBLISH_RELEASE" == "true" && "$CONFIRMATION" != "PUBLISH" ]]; then echo "::error::Public release requires publication_confirmation=PUBLISH." exit 1 fi - if [[ "$PUBLISH_RELEASE" != true && "$CONFIRMATION" == PUBLISH ]]; then - echo "::error::Publication confirmation was provided without opt-in." + + if [[ "$PUBLISH_RELEASE" != "true" && "$CONFIRMATION" == "PUBLISH" ]]; then + echo "::error::Publication confirmation was provided without publication opt-in." exit 1 fi - echo "publishing=$PUBLISH_RELEASE" >> "$GITHUB_OUTPUT" - - name: Checkout selected main commit without persisted credentials - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7 - with: - ref: ${{ github.sha }} - persist-credentials: false + tag="v$VERSION" + is_prerelease=false + if [[ "$VERSION" == *-* ]]; then + is_prerelease=true + fi - - name: Set up .NET 10 - uses: actions/setup-dotnet@a98b56852c35b8e3190ac28c8c2271da59106c68 # v6 - with: - dotnet-version: '10.0.x' + git fetch --tags --force + tag_exists=false + if git rev-parse -q --verify "refs/tags/$tag" >/dev/null; then + tag_exists=true + tag_sha="$(git rev-parse "refs/tags/$tag^{commit}")" + if [ "$tag_sha" != "$GITHUB_SHA" ]; then + echo "::error::Existing tag $tag points to $tag_sha instead of validated commit $GITHUB_SHA." + exit 1 + fi + echo "Tag $tag already points to the validated commit." + else + echo "Tag $tag does not currently exist." + fi + + if [[ "$PUBLISH_RELEASE" == "true" ]]; then + existing_nuget=0 + for package in Repo2C4.Cli Repo2C4.Mcp Repo2C4.Agent; do + package_lower="$(printf '%s' "$package" | tr '[:upper:]' '[:lower:]')" + version_lower="$(printf '%s' "$VERSION" | tr '[:upper:]' '[:lower:]')" + package_url="https://api.nuget.org/v3-flatcontainer/$package_lower/$version_lower/$package_lower.$version_lower.nupkg" + status="$(curl --silent --show-error --location \ + --proto '=https' --proto-redir '=https' \ + --retry 3 --retry-delay 2 --retry-all-errors \ + --connect-timeout 10 --max-time 30 \ + --output /dev/null --write-out '%{http_code}' "$package_url")" - - name: Confirm project version and product package identities + case "$status" in + 200) + existing_nuget=$((existing_nuget + 1)) + echo "NuGet.org already contains $package $VERSION." + ;; + 404) + echo "NuGet.org does not currently contain $package $VERSION." + ;; + *) + echo "::error::Unexpected NuGet.org HTTP $status while checking $package $VERSION." + exit 1 + ;; + esac + done + + if [ "$existing_nuget" -gt 0 ]; then + if [[ "$tag_exists" == "true" ]]; then + echo "::error::Version $VERSION already has published NuGet package state on the validated tag. Re-run failed jobs from the original workflow run instead of starting a new release run." + else + echo "::error::NuGet.org already contains package state for $VERSION while tag $tag is absent. This is a partial prior publication and must not be replayed from a new release run." + fi + exit 1 + fi + fi + + echo "publishing=$PUBLISH_RELEASE" >> "$GITHUB_OUTPUT" + echo "release_tag=$tag" >> "$GITHUB_OUTPUT" + echo "is_prerelease=$is_prerelease" >> "$GITHUB_OUTPUT" + + - name: Confirm project version and package identities shell: bash env: VERSION: ${{ inputs.version }} run: | set -euo pipefail - declared="$(dotnet msbuild src/Repo2C4.Cli/Repo2C4.Cli.csproj -nologo -getProperty:Version | tr -d '\r' | tail -n 1)" - test "$declared" = "$VERSION" + + test "$(dotnet msbuild src/Repo2C4.Cli/Repo2C4.Cli.csproj -nologo -getProperty:Version | tr -d '\r' | tail -n 1)" = "$VERSION" test "$(dotnet msbuild src/Repo2C4.Mcp/Repo2C4.Mcp.csproj -nologo -getProperty:Version | tr -d '\r' | tail -n 1)" = "$VERSION" test "$(dotnet msbuild src/Repo2C4.Agent/Repo2C4.Agent.csproj -nologo -getProperty:Version | tr -d '\r' | tail -n 1)" = "$VERSION" - test "$(dotnet msbuild src/Repo2C4.Cli/Repo2C4.Cli.csproj -nologo -getProperty:PackageId | tr -d '\r' | tail -n 1)" = Repo2C4.Cli - test "$(dotnet msbuild src/Repo2C4.Mcp/Repo2C4.Mcp.csproj -nologo -getProperty:PackageId | tr -d '\r' | tail -n 1)" = Repo2C4.Mcp - test "$(dotnet msbuild src/Repo2C4.Agent/Repo2C4.Agent.csproj -nologo -getProperty:PackageId | tr -d '\r' | tail -n 1)" = Repo2C4.Agent - test "$(dotnet msbuild src/Repo2C4.Core/Repo2C4.Core.csproj -nologo -getProperty:IsPackable | tr -d '\r' | tail -n 1)" = false + + test "$(dotnet msbuild src/Repo2C4.Cli/Repo2C4.Cli.csproj -nologo -getProperty:PackageId | tr -d '\r' | tail -n 1)" = "Repo2C4.Cli" + test "$(dotnet msbuild src/Repo2C4.Mcp/Repo2C4.Mcp.csproj -nologo -getProperty:PackageId | tr -d '\r' | tail -n 1)" = "Repo2C4.Mcp" + test "$(dotnet msbuild src/Repo2C4.Agent/Repo2C4.Agent.csproj -nologo -getProperty:PackageId | tr -d '\r' | tail -n 1)" = "Repo2C4.Agent" + test "$(dotnet msbuild src/Repo2C4.Core/Repo2C4.Core.csproj -nologo -getProperty:IsPackable | tr -d '\r' | tail -n 1)" = "false" + + test "$(jq -r '.version' server.json)" = "$VERSION" + test "$(jq -r '.packages[0].identifier' server.json)" = "Repo2C4.Mcp" + test "$(jq -r '.packages[0].version' server.json)" = "$VERSION" - name: Restore, format, build and test shell: bash @@ -98,7 +185,7 @@ jobs: - name: Set up pinned Node.js and LikeC4 CLI uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 with: - node-version: '22.23.3' + node-version: "22.23.3" package-manager-cache: false - name: Install official LikeC4 validator @@ -108,7 +195,7 @@ jobs: npm install --global --no-audit --no-fund likec4@1.59.4 likec4 --version - - name: Dry-run package build and isolated consumer installation + - name: Pack and verify isolated consumer installation shell: bash env: VERSION: ${{ inputs.version }} @@ -116,162 +203,106 @@ jobs: set -euo pipefail bash scripts/verify-distribution.sh artifacts/distribution "$VERSION" cd artifacts/distribution - sha256sum Repo2C4.Cli."$VERSION".nupkg Repo2C4.Mcp."$VERSION".nupkg Repo2C4.Agent."$VERSION".nupkg > SHA256SUMS + sha256sum "Repo2C4.Cli.$VERSION.nupkg" "Repo2C4.Mcp.$VERSION.nupkg" "Repo2C4.Agent.$VERSION.nupkg" > SHA256SUMS sha256sum --check SHA256SUMS - echo "All three versioned tools are installable and runnable from a clean local feed; no publication in the dry-run job." + echo "All three tools are installable and runnable from the exact local release payload." - - name: Retain validated release payload only after explicit opt-in - if: ${{ steps.inputs.outputs.publishing == 'true' }} + - name: Upload validated release payload + if: ${{ steps.release.outputs.publishing == 'true' }} uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7 with: name: repo2c4-release-payload path: artifacts/distribution/ if-no-files-found: error - retention-days: 1 + retention-days: 14 - publish: - name: Opt-in GitHub Release and NuGet.org - needs: dry-run - if: ${{ needs.dry-run.outputs.publishing == 'true' && needs.dry-run.result == 'success' }} + ensure-release-tag: + name: Create release tag + needs: build-and-pack + if: ${{ needs.build-and-pack.result == 'success' && needs.build-and-pack.outputs.publishing == 'true' }} runs-on: ubuntu-latest permissions: - actions: read contents: write - id-token: write - environment: release - steps: - - name: Download only this run's verified release payload - shell: bash - env: - GH_TOKEN: ${{ github.token }} - VERSION: ${{ inputs.version }} - run: | - set -euo pipefail - mkdir -p "$RUNNER_TEMP/payload" - gh run download "$GITHUB_RUN_ID" --repo "$GITHUB_REPOSITORY" \ - --name repo2c4-release-payload --dir "$RUNNER_TEMP/payload" - cd "$RUNNER_TEMP/payload" - test "$(find . -maxdepth 1 -type f | wc -l)" -eq 4 - test -s "Repo2C4.Cli.$VERSION.nupkg" - test -s "Repo2C4.Mcp.$VERSION.nupkg" - test -s "Repo2C4.Agent.$VERSION.nupkg" - sha256sum --check SHA256SUMS + outputs: + tag-created: ${{ steps.tag.outputs.tag_created }} - - name: Verify main has not moved and create release at validated SHA + steps: + - name: Verify main has not moved and create or verify tag + id: tag shell: bash env: GH_TOKEN: ${{ github.token }} - VERSION: ${{ inputs.version }} - CONFIRMATION: ${{ inputs.publication_confirmation }} - PUBLISH_RELEASE: ${{ inputs.publish_release }} + RELEASE_TAG: ${{ needs.build-and-pack.outputs.release-tag }} + VALIDATED_SHA: ${{ github.sha }} run: | set -euo pipefail - test "$GITHUB_REF" = refs/heads/main - test "$PUBLISH_RELEASE" = true - test "$CONFIRMATION" = PUBLISH - test "$(gh api "repos/$GITHUB_REPOSITORY/git/ref/heads/main" --jq .object.sha)" = "$GITHUB_SHA" - tag="v$VERSION" - expected_assets=( - "Repo2C4.Cli.$VERSION.nupkg" - "Repo2C4.Mcp.$VERSION.nupkg" - "Repo2C4.Agent.$VERSION.nupkg" - "SHA256SUMS" - ) - - tag_response_file="$RUNNER_TEMP/tag-ref.response" - tag_error_file="$RUNNER_TEMP/tag-ref.err" - if gh api --include "repos/$GITHUB_REPOSITORY/git/ref/tags/$tag" \ - >"$tag_response_file" 2>"$tag_error_file"; then - tag_status="$(awk '$1 ~ /^HTTP\// { print $2; exit }' "$tag_response_file")" - if [ "$tag_status" != "200" ]; then - echo "::error::Failed to query tag $tag (HTTP ${tag_status:-unknown}): $(cat "$tag_error_file")" - exit 1 - fi - tag_ref_json="$(awk 'body { print } /^[[:space:]\r]*$/ { body = 1 }' "$tag_response_file")" - existing_tag_type="$(jq -r '.object.type' <<<"$tag_ref_json")" - existing_tag_sha="$(jq -r '.object.sha' <<<"$tag_ref_json")" + current_main="$(gh api "repos/$GITHUB_REPOSITORY/git/ref/heads/main" --jq '.object.sha')" + if [ "$current_main" != "$VALIDATED_SHA" ]; then + echo "::error::main moved from validated commit $VALIDATED_SHA to $current_main. Start a new release validation." + exit 1 + fi - while [ "$existing_tag_type" = "tag" ]; do - tag_object_json="$(gh api "repos/$GITHUB_REPOSITORY/git/tags/$existing_tag_sha")" - existing_tag_type="$(jq -r '.object.type' <<<"$tag_object_json")" - existing_tag_sha="$(jq -r '.object.sha' <<<"$tag_object_json")" + tag_status="$(gh api --silent --include "repos/$GITHUB_REPOSITORY/git/ref/tags/$RELEASE_TAG" 2>"$RUNNER_TEMP/tag.err" | awk '$1 ~ /^HTTP\// { print $2; exit }' || true)" + if [ "$tag_status" = "200" ]; then + tag_json="$(gh api "repos/$GITHUB_REPOSITORY/git/ref/tags/$RELEASE_TAG")" + tag_type="$(jq -r '.object.type' <<<"$tag_json")" + tag_sha="$(jq -r '.object.sha' <<<"$tag_json")" + while [ "$tag_type" = "tag" ]; do + tag_object="$(gh api "repos/$GITHUB_REPOSITORY/git/tags/$tag_sha")" + tag_type="$(jq -r '.object.type' <<<"$tag_object")" + tag_sha="$(jq -r '.object.sha' <<<"$tag_object")" done - - if [ "$existing_tag_type" != "commit" ]; then - echo "::error::Existing tag $tag does not resolve directly to a commit; refusing publication." - exit 1 - fi - - if [ "$existing_tag_sha" != "$GITHUB_SHA" ]; then - echo "::error::Existing tag $tag points to a different commit; refusing publication." + if [ "$tag_type" != "commit" ] || [ "$tag_sha" != "$VALIDATED_SHA" ]; then + echo "::error::Tag $RELEASE_TAG does not resolve to validated commit $VALIDATED_SHA." exit 1 fi - else - tag_status="$(awk '$1 ~ /^HTTP\// { print $2; exit }' "$tag_response_file")" - if [ "$tag_status" != "404" ]; then - echo "::error::Failed to query tag $tag (HTTP ${tag_status:-unknown}): $(cat "$tag_error_file")" - exit 1 - fi - - gh api --method POST "repos/$GITHUB_REPOSITORY/git/refs" \ - -f ref="refs/tags/$tag" -f sha="$GITHUB_SHA" >/dev/null + echo "Tag $RELEASE_TAG already exists on the validated commit." + echo "tag_created=false" >> "$GITHUB_OUTPUT" + exit 0 fi - release_json="" - if release_json="$(gh release view "$tag" --repo "$GITHUB_REPOSITORY" --json isDraft,assets 2>/dev/null)"; then - if [ "$(jq -r '.isDraft' <<<"$release_json")" != "true" ]; then - expected="$(printf '%s\n' "${expected_assets[@]}" | sort)" - actual="$(jq -r '.assets[].name' <<<"$release_json" | sort)" - if [ "$actual" != "$expected" ]; then - echo "::error::Published release exists but does not contain exactly the expected assets." - exit 1 - fi - echo "Release $tag is already published with the expected assets at the validated commit." - exit 0 - fi - else - notes_file="$RUNNER_TEMP/release-notes.md" - printf '%s\n' \ - "Repo2C4 $tag" \ - "" \ - "Install the published .NET tools from NuGet.org:" \ - "" \ - '~~~bash' \ - "dotnet tool install --global Repo2C4.Cli --version $VERSION" \ - "dotnet tool install --global Repo2C4.Mcp --version $VERSION" \ - "dotnet tool install --global Repo2C4.Agent --version $VERSION" \ - "repo2c4 --help" \ - "repo2c4-mcp --help" \ - "repo2c4-agent --help" \ - '~~~' \ - "" \ - "The attached .nupkg files are the exact validated payload published to NuGet.org." \ - "Repo2C4.Mcp registry metadata is versioned in server.json and is published separately to the Official MCP Registry after the matching NuGet package becomes publicly consumable." \ - "Verify downloads with SHA256SUMS. See docs/distribution.md for runtime requirements, MCP configuration, registry publication and security guidance." \ - > "$notes_file" - gh release create "$tag" \ - --repo "$GITHUB_REPOSITORY" --verify-tag --target "$GITHUB_SHA" --draft \ - --title "Repo2C4 $tag" \ - --notes-file "$notes_file" + if [ "$tag_status" != "404" ]; then + echo "::error::Unable to query tag $RELEASE_TAG (HTTP ${tag_status:-unknown})." + cat "$RUNNER_TEMP/tag.err" >&2 || true + exit 1 fi - for asset in "${expected_assets[@]}"; do - gh release upload "$tag" "$RUNNER_TEMP/payload/$asset" \ - --repo "$GITHUB_REPOSITORY" --clobber - done + gh api --method POST "repos/$GITHUB_REPOSITORY/git/refs" \ + -f ref="refs/tags/$RELEASE_TAG" \ + -f sha="$VALIDATED_SHA" >/dev/null + echo "tag_created=true" >> "$GITHUB_OUTPUT" - release_json="$(gh release view "$tag" --repo "$GITHUB_REPOSITORY" --json isDraft,assets)" - if [ "$(jq -r '.isDraft' <<<"$release_json")" != "true" ]; then - echo "::error::Release changed state before the verified publication step." - exit 1 - fi - expected="$(printf '%s\n' "${expected_assets[@]}" | sort)" - actual="$(jq -r '.assets[].name' <<<"$release_json" | sort)" - if [ "$actual" != "$expected" ]; then - echo "::error::Draft release does not contain exactly the expected verified assets." - exit 1 - fi + publish-nuget: + name: Publish to NuGet.org + needs: + - build-and-pack + - ensure-release-tag + if: ${{ needs.build-and-pack.outputs.publishing == 'true' && needs.ensure-release-tag.result == 'success' }} + runs-on: ubuntu-latest + environment: release + permissions: + contents: read + id-token: write + + steps: + - name: Set up .NET 10 + uses: actions/setup-dotnet@a98b56852c35b8e3190ac28c8c2271da59106c68 # v6 + with: + dotnet-version: "10.0.x" + + - name: Download validated release payload + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 + with: + name: repo2c4-release-payload + path: ./artifacts/distribution + + - name: Verify release checksums + shell: bash + run: | + set -euo pipefail + cd artifacts/distribution + sha256sum --check SHA256SUMS - name: Authenticate to NuGet.org with Trusted Publishing id: nuget-login @@ -279,59 +310,193 @@ jobs: with: user: rodri-oliveira-dev - - name: Publish validated tools to NuGet.org + - name: Publish to NuGet.org shell: bash env: - GH_TOKEN: ${{ github.token }} NUGET_API_KEY: ${{ steps.nuget-login.outputs.NUGET_API_KEY }} VERSION: ${{ inputs.version }} run: | set -euo pipefail - tag="v$VERSION" - expected="$(printf '%s\n' "Repo2C4.Cli.$VERSION.nupkg" "Repo2C4.Mcp.$VERSION.nupkg" "Repo2C4.Agent.$VERSION.nupkg" "SHA256SUMS" | sort)" + if [ -z "${NUGET_API_KEY:-}" ]; then echo "::error::NuGet Trusted Publishing did not return a temporary API key." exit 1 fi - push_verified() { - local package_id="$1" - local package_file="$2" - local expected_hash - expected_hash="$(awk -v file="$(basename "$package_file")" '$2 == file { print $1 }' "$RUNNER_TEMP/payload/SHA256SUMS")" - if dotnet nuget push "$package_file" \ + + for package in Repo2C4.Cli Repo2C4.Mcp Repo2C4.Agent; do + dotnet nuget push "artifacts/distribution/$package.$VERSION.nupkg" \ --api-key "$NUGET_API_KEY" \ - --source https://api.nuget.org/v3/index.json; then - return 0 - fi + --source https://api.nuget.org/v3/index.json \ + --skip-duplicate + done - local work="$RUNNER_TEMP/nuget-verify-$package_id" - mkdir -p "$work" - if ! dotnet tool install --tool-path "$work" "$package_id" --version "$VERSION" \ - --add-source https://api.nuget.org/v3/index.json \ - --ignore-failed-sources >/dev/null 2>&1; then - echo "::error::$package_id $VERSION could not be verified after a duplicate/publication failure." - return 1 - fi - local public_package - public_package="$(find "$HOME/.nuget/packages/$(echo "$package_id" | tr '[:upper:]' '[:lower:]')/$VERSION" -maxdepth 1 -name '*.nupkg' -print -quit)" - if [ -z "$public_package" ]; then - echo "::error::Unable to locate the public package payload for $package_id $VERSION." - return 1 - fi - test "$(sha256sum "$public_package" | awk '{print $1}')" = "$expected_hash" - } + echo "NuGet.org accepted the validated release payload. Public indexing is asynchronous and is not a release gate." - push_verified Repo2C4.Cli "$RUNNER_TEMP/payload/Repo2C4.Cli.$VERSION.nupkg" - push_verified Repo2C4.Mcp "$RUNNER_TEMP/payload/Repo2C4.Mcp.$VERSION.nupkg" - push_verified Repo2C4.Agent "$RUNNER_TEMP/payload/Repo2C4.Agent.$VERSION.nupkg" + publish-github-packages: + name: Publish to GitHub Packages + needs: + - build-and-pack + - ensure-release-tag + if: ${{ needs.build-and-pack.outputs.publishing == 'true' && needs.ensure-release-tag.result == 'success' }} + runs-on: ubuntu-latest + environment: release + permissions: + contents: read + packages: write - gh release edit "$tag" --repo "$GITHUB_REPOSITORY" --draft=false - final_json="$(gh release view "$tag" --repo "$GITHUB_REPOSITORY" --json isDraft,assets)" - if [ "$(jq -r '.isDraft' <<<"$final_json")" != "false" ]; then - echo "::error::Release was not published successfully." - exit 1 + steps: + - name: Set up .NET 10 + uses: actions/setup-dotnet@a98b56852c35b8e3190ac28c8c2271da59106c68 # v6 + with: + dotnet-version: "10.0.x" + + - name: Download validated release payload + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 + with: + name: repo2c4-release-payload + path: ./artifacts/distribution + + - name: Verify release checksums + shell: bash + run: | + set -euo pipefail + cd artifacts/distribution + sha256sum --check SHA256SUMS + + - name: Publish to GitHub Packages + shell: bash + env: + GITHUB_PACKAGE_TOKEN: ${{ github.token }} + GITHUB_PACKAGE_SOURCE: https://nuget.pkg.github.com/${{ github.repository_owner }}/index.json + VERSION: ${{ inputs.version }} + run: | + set -euo pipefail + + for package in Repo2C4.Cli Repo2C4.Mcp Repo2C4.Agent; do + dotnet nuget push "artifacts/distribution/$package.$VERSION.nupkg" \ + --api-key "$GITHUB_PACKAGE_TOKEN" \ + --source "$GITHUB_PACKAGE_SOURCE" \ + --skip-duplicate + done + + github-release: + name: Create GitHub Release + needs: + - build-and-pack + - ensure-release-tag + - publish-nuget + - publish-github-packages + if: ${{ needs.build-and-pack.outputs.publishing == 'true' && needs.publish-nuget.result == 'success' && needs.publish-github-packages.result == 'success' }} + runs-on: ubuntu-latest + environment: release + permissions: + contents: write + + steps: + - name: Download validated release payload + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 + with: + name: repo2c4-release-payload + path: ./artifacts/distribution + + - name: Verify release checksums + shell: bash + run: | + set -euo pipefail + cd artifacts/distribution + sha256sum --check SHA256SUMS + + - name: Create or update GitHub Release + shell: bash + env: + GH_TOKEN: ${{ github.token }} + RELEASE_TAG: ${{ needs.build-and-pack.outputs.release-tag }} + IS_PRERELEASE: ${{ needs.build-and-pack.outputs.is-prerelease }} + VERSION: ${{ inputs.version }} + run: | + set -euo pipefail + + assets=( + "artifacts/distribution/Repo2C4.Cli.$VERSION.nupkg" + "artifacts/distribution/Repo2C4.Mcp.$VERSION.nupkg" + "artifacts/distribution/Repo2C4.Agent.$VERSION.nupkg" + "artifacts/distribution/SHA256SUMS" + ) + + notes="$RUNNER_TEMP/release-notes.md" + cat > "$notes" </dev/null 2>&1; then + echo "GitHub Release $RELEASE_TAG already exists; refreshing validated assets." + gh release upload "$RELEASE_TAG" "${assets[@]}" \ + --repo "$GITHUB_REPOSITORY" --clobber + exit 0 fi - if [ "$(jq -r '.assets[].name' <<<"$final_json" | sort)" != "$expected" ]; then - echo "::error::Published release assets differ from the verified payload." - exit 1 + + release_args=( + --repo "$GITHUB_REPOSITORY" + --verify-tag + --target "$GITHUB_SHA" + --title "Repo2C4 $RELEASE_TAG" + --notes-file "$notes" + ) + if [ "$IS_PRERELEASE" = "true" ]; then + release_args+=(--prerelease) fi + + gh release create "$RELEASE_TAG" "${assets[@]}" "${release_args[@]}" + + rollback: + name: Roll back failed release + if: >- + ${{ always() && + needs.build-and-pack.result == 'success' && + needs.build-and-pack.outputs.publishing == 'true' && + needs.ensure-release-tag.result == 'success' && + needs.ensure-release-tag.outputs.tag-created == 'true' && + (needs.publish-nuget.result == 'failure' || + needs.publish-github-packages.result == 'failure' || + needs.github-release.result == 'failure') }} + needs: + - build-and-pack + - ensure-release-tag + - publish-nuget + - publish-github-packages + - github-release + runs-on: ubuntu-latest + environment: release + permissions: + contents: write + + steps: + - name: Compensate failed release + shell: bash + env: + GH_TOKEN: ${{ github.token }} + RELEASE_TAG: ${{ needs.build-and-pack.outputs.release-tag }} + run: | + set -euo pipefail + + if gh release view "$RELEASE_TAG" --repo "$GITHUB_REPOSITORY" >/dev/null 2>&1; then + echo "Removing partial GitHub Release $RELEASE_TAG." + gh release delete "$RELEASE_TAG" --repo "$GITHUB_REPOSITORY" --yes + else + echo "No partial GitHub Release exists for $RELEASE_TAG." + fi + + echo "Release tag $RELEASE_TAG is intentionally retained so retries remain bound to the validated commit." + echo "Published NuGet.org and GitHub Packages versions are immutable and are never deleted by rollback." From 5d5c935de7bde75a3ff10fd428e46fe6005ac206 Mon Sep 17 00:00:00 2001 From: Rodrigo de Oliveira Date: Thu, 1 Oct 2026 10:19:39 -0300 Subject: [PATCH 12/20] fix: harden staged release tag verification --- .github/workflows/release.yml | 21 +++++++++++++++++---- 1 file changed, 17 insertions(+), 4 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 560fa28..7f12c2b 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -243,28 +243,41 @@ jobs: exit 1 fi - tag_status="$(gh api --silent --include "repos/$GITHUB_REPOSITORY/git/ref/tags/$RELEASE_TAG" 2>"$RUNNER_TEMP/tag.err" | awk '$1 ~ /^HTTP\// { print $2; exit }' || true)" - if [ "$tag_status" = "200" ]; then - tag_json="$(gh api "repos/$GITHUB_REPOSITORY/git/ref/tags/$RELEASE_TAG")" + tag_response="$RUNNER_TEMP/tag-ref.response" + tag_error="$RUNNER_TEMP/tag-ref.err" + + if gh api --include "repos/$GITHUB_REPOSITORY/git/ref/tags/$RELEASE_TAG" >"$tag_response" 2>"$tag_error"; then + tag_status="$(awk '$1 ~ /^HTTP\// { print $2; exit }' "$tag_response")" + if [ "$tag_status" != "200" ]; then + echo "::error::Unable to query tag $RELEASE_TAG (HTTP ${tag_status:-unknown})." + cat "$tag_error" >&2 || true + exit 1 + fi + + tag_json="$(awk 'body { print } /^[[:space:]\r]*$/ { body = 1 }' "$tag_response")" tag_type="$(jq -r '.object.type' <<<"$tag_json")" tag_sha="$(jq -r '.object.sha' <<<"$tag_json")" + while [ "$tag_type" = "tag" ]; do tag_object="$(gh api "repos/$GITHUB_REPOSITORY/git/tags/$tag_sha")" tag_type="$(jq -r '.object.type' <<<"$tag_object")" tag_sha="$(jq -r '.object.sha' <<<"$tag_object")" done + if [ "$tag_type" != "commit" ] || [ "$tag_sha" != "$VALIDATED_SHA" ]; then echo "::error::Tag $RELEASE_TAG does not resolve to validated commit $VALIDATED_SHA." exit 1 fi + echo "Tag $RELEASE_TAG already exists on the validated commit." echo "tag_created=false" >> "$GITHUB_OUTPUT" exit 0 fi + tag_status="$(awk '$1 ~ /^HTTP\// { print $2; exit }' "$tag_response")" if [ "$tag_status" != "404" ]; then echo "::error::Unable to query tag $RELEASE_TAG (HTTP ${tag_status:-unknown})." - cat "$RUNNER_TEMP/tag.err" >&2 || true + cat "$tag_error" >&2 || true exit 1 fi From ce2c53f1c604b90c5346a1fc238e52530a8215f7 Mon Sep 17 00:00:00 2001 From: Rodrigo de Oliveira Date: Thu, 1 Oct 2026 10:20:01 -0300 Subject: [PATCH 13/20] docs: document staged package publication --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index a8f9e1a..6e1225a 100644 --- a/README.md +++ b/README.md @@ -325,7 +325,7 @@ CLI help is written to stdout. MCP help and diagnostics are written **only to st `.github/workflows/ci.yml` validates locked restore, formatting, Release build, tests, coverage, pinned LikeC4 integration, the complete offline CLI cycle (`inspect -> reviewed model -> generate -> validate`) and the full MCP protocol-client C1/C2 flow without paid AI or a proprietary client. CodeQL, Dependency Review and optional SonarQube Cloud checks remain available; [Sonar setup](docs/sonarqube-cloud.md) requires `SONAR_TOKEN`. -**Distribution:** the three versioned .NET tools, `Repo2C4.Cli`, `Repo2C4.Mcp` and `Repo2C4.Agent`, are clean-install tested. Public release remains manually gated; an authorized release publishes the validated packages to NuGet.org and GitHub Release. NuGet indexing is asynchronous and is not used as a post-publication release gate. `Repo2C4.Mcp` also includes versioned Official MCP Registry metadata in [`server.json`](server.json); registry publication is a separate manual workflow run only after the matching NuGet package is publicly consumable. `Repo2C4.Agent` remains a local .NET Tool/MCP client rather than a marketplace-specific agent package. See the [installation, security and release guide](docs/distribution.md) or [Português](docs/distribution.pt-BR.md). +**Distribution:** the three versioned .NET tools, `Repo2C4.Cli`, `Repo2C4.Mcp` and `Repo2C4.Agent`, are clean-install tested. Public release remains manually gated; an authorized release publishes the exact validated payload to NuGet.org and GitHub Packages in parallel, then creates the GitHub Release only after both package registries succeed. NuGet indexing is asynchronous and is not used as a post-publication release gate. `Repo2C4.Mcp` also includes versioned Official MCP Registry metadata in [`server.json`](server.json); registry publication is a separate manual workflow run only after the matching NuGet package is publicly consumable. `Repo2C4.Agent` remains a local .NET Tool/MCP client rather than a marketplace-specific agent package. See the [installation, security and release guide](docs/distribution.md) or [Português](docs/distribution.pt-BR.md). From 963136932ee8c605b601011be772f4161c5b4b20 Mon Sep 17 00:00:00 2001 From: Rodrigo de Oliveira Date: Thu, 1 Oct 2026 10:20:04 -0300 Subject: [PATCH 14/20] docs: document staged package publication in Portuguese --- README.pt-BR.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.pt-BR.md b/README.pt-BR.md index 809680b..9f35962 100644 --- a/README.pt-BR.md +++ b/README.pt-BR.md @@ -249,4 +249,4 @@ O [CI](.github/workflows/ci.yml) verifica restore locked, políticas de build, f ### Distribuição e automação -As ferramentas públicas são `Repo2C4.Cli` (comando `repo2c4`), `Repo2C4.Mcp` (comando `repo2c4-mcp`) e `Repo2C4.Agent` (comando `repo2c4-agent`). O LikeC4 continua uma dependência independente. A release pública é manual/protegida e publica os três pacotes no NuGet.org e GitHub Release. A indexação do NuGet é assíncrona e não é usada como gate pós-publicação da release. O `Repo2C4.Mcp` também possui metadata versionada para o Official MCP Registry em [`server.json`](server.json), publicada por um workflow manual separado somente depois que a mesma versão estiver publicamente consumível no NuGet. O `Repo2C4.Agent` permanece uma .NET Tool local/cliente MCP, sem acoplamento a marketplace específico. Consulte [distribuição](docs/distribution.pt-BR.md) e [workflow de PR arquitetural](docs/architecture-pr.pt-BR.md). +As ferramentas públicas são `Repo2C4.Cli` (comando `repo2c4`), `Repo2C4.Mcp` (comando `repo2c4-mcp`) e `Repo2C4.Agent` (comando `repo2c4-agent`). O LikeC4 continua uma dependência independente. A release pública é manual/protegida e publica o payload validado no NuGet.org e GitHub Packages em paralelo, criando a GitHub Release somente depois que ambos os registries concluírem com sucesso. A indexação do NuGet é assíncrona e não é usada como gate pós-publicação da release. O `Repo2C4.Mcp` também possui metadata versionada para o Official MCP Registry em [`server.json`](server.json), publicada por um workflow manual separado somente depois que a mesma versão estiver publicamente consumível no NuGet. O `Repo2C4.Agent` permanece uma .NET Tool local/cliente MCP, sem acoplamento a marketplace específico. Consulte [distribuição](docs/distribution.pt-BR.md) e [workflow de PR arquitetural](docs/architecture-pr.pt-BR.md). From da84a61912c410e65b462faba3e109094ee89138 Mon Sep 17 00:00:00 2001 From: Rodrigo de Oliveira Date: Thu, 1 Oct 2026 10:20:06 -0300 Subject: [PATCH 15/20] docs: include GitHub Packages in release flow --- docs/distribution.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/distribution.md b/docs/distribution.md index dd9a235..628b3a6 100644 --- a/docs/distribution.md +++ b/docs/distribution.md @@ -23,9 +23,9 @@ The release uses different channels for artifact hosting and product discovery: | Product | Artifact distribution | Discovery | | --- | --- | --- | -| `Repo2C4.Cli` | NuGet.org + GitHub Release | GitHub/NuGet | -| `Repo2C4.Mcp` | NuGet.org + GitHub Release | Official MCP Registry metadata in `server.json` | -| `Repo2C4.Agent` | NuGet.org + GitHub Release | GitHub/NuGet | +| `Repo2C4.Cli` | NuGet.org + GitHub Packages + GitHub Release | GitHub/NuGet | +| `Repo2C4.Mcp` | NuGet.org + GitHub Packages + GitHub Release | Official MCP Registry metadata in `server.json` | +| `Repo2C4.Agent` | NuGet.org + GitHub Packages + GitHub Release | GitHub/NuGet | The Agent is intentionally not published as an MCP server or platform-specific agent package. It remains a local .NET Tool and an independent MCP client. @@ -47,7 +47,7 @@ NuGet ownership verification depends on the package README containing this exact The marker is packaged from `src/Repo2C4.Mcp/README.md`. The distribution smoke verifies the registry name, package/version binding, required root argument and ownership marker before a release can proceed. -The Official MCP Registry stores metadata rather than the NuGet artifact, so the matching NuGet package must be public first. NuGet indexing is asynchronous, therefore public installation is deliberately not a gate of the immutable release. The protected release workflow finishes after the validated NuGet packages and GitHub Release are published. A separate manual `Publish Repo2C4 MCP Registry metadata` workflow verifies that the released `Repo2C4.Mcp` version is publicly consumable and only then publishes the matching Registry metadata. +The Official MCP Registry stores metadata rather than the NuGet artifact, so the matching NuGet package must be public first. NuGet indexing is asynchronous, therefore public installation is deliberately not a gate of the immutable release. The protected release workflow validates and packs once, creates the release tag, publishes the exact payload to NuGet.org and GitHub Packages in parallel, and creates the GitHub Release only after both package registries succeed. A separate manual `Publish Repo2C4 MCP Registry metadata` workflow verifies that the released `Repo2C4.Mcp` version is publicly consumable and only then publishes the matching Registry metadata. Registry authentication uses the Official MCP Registry's GitHub Actions OIDC flow. The registry job receives only `contents: read` and `id-token: write`; it does not require a PAT or long-lived MCP Registry secret. The job downloads a pinned `mcp-publisher` release, verifies its SHA-256, validates `server.json`, checks whether that exact server/version already exists, publishes only when absent, and verifies that the version becomes readable through the registry API. From d6c4fe670b92e3886ae69c8349e23e42e0e85760 Mon Sep 17 00:00:00 2001 From: Rodrigo de Oliveira Date: Thu, 1 Oct 2026 10:20:08 -0300 Subject: [PATCH 16/20] docs: include GitHub Packages in release flow in Portuguese --- docs/distribution.pt-BR.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/distribution.pt-BR.md b/docs/distribution.pt-BR.md index 912a365..ebee770 100644 --- a/docs/distribution.pt-BR.md +++ b/docs/distribution.pt-BR.md @@ -23,9 +23,9 @@ A release usa canais diferentes para hospedagem dos artefatos e descoberta dos p | Produto | Distribuição do artefato | Descoberta | | --- | --- | --- | -| `Repo2C4.Cli` | NuGet.org + GitHub Release | GitHub/NuGet | -| `Repo2C4.Mcp` | NuGet.org + GitHub Release | metadata no Official MCP Registry via `server.json` | -| `Repo2C4.Agent` | NuGet.org + GitHub Release | GitHub/NuGet | +| `Repo2C4.Cli` | NuGet.org + GitHub Packages + GitHub Release | GitHub/NuGet | +| `Repo2C4.Mcp` | NuGet.org + GitHub Packages + GitHub Release | metadata no Official MCP Registry via `server.json` | +| `Repo2C4.Agent` | NuGet.org + GitHub Packages + GitHub Release | GitHub/NuGet | O Agent não é publicado artificialmente como servidor MCP nem como pacote de um marketplace específico. Ele permanece uma .NET Tool local e um cliente MCP independente. @@ -47,7 +47,7 @@ A validação de ownership do pacote NuGet depende deste marcador exato no READM O marcador vem de `src/Repo2C4.Mcp/README.md`. O smoke de distribuição valida nome do Registry, vínculo package/version, argumento obrigatório da raiz e marcador de ownership antes da release. -O Official MCP Registry armazena metadata, não o pacote NuGet, então o pacote correspondente precisa estar público primeiro. Como a indexação do NuGet é assíncrona, a instalação pública não é usada como gate da release imutável. O workflow protegido de release termina depois de publicar os pacotes NuGet validados e a GitHub Release. Um workflow manual separado, `Publish Repo2C4 MCP Registry metadata`, verifica que a versão publicada de `Repo2C4.Mcp` já está publicamente consumível e só então publica a metadata correspondente no Registry. +O Official MCP Registry armazena metadata, não o pacote NuGet, então o pacote correspondente precisa estar público primeiro. Como a indexação do NuGet é assíncrona, a instalação pública não é usada como gate da release imutável. O workflow protegido de release valida e empacota uma única vez, cria a tag, publica o mesmo payload no NuGet.org e GitHub Packages em paralelo e cria a GitHub Release somente depois que ambos os registries concluírem com sucesso. Um workflow manual separado, `Publish Repo2C4 MCP Registry metadata`, verifica que a versão publicada de `Repo2C4.Mcp` já está publicamente consumível e só então publica a metadata correspondente no Registry. A autenticação usa o fluxo oficial GitHub Actions OIDC. O job recebe somente `contents: read` e `id-token: write`; não exige PAT nem secret persistente específico do MCP Registry. O workflow baixa uma versão fixada do `mcp-publisher`, confere seu SHA-256, valida `server.json`, verifica se aquela versão exata já existe, publica apenas quando ausente e confirma a leitura da versão pela API do Registry. From 86dd5eee79bb0fced6ca8430459186408edac6ea Mon Sep 17 00:00:00 2001 From: Rodrigo de Oliveira Date: Thu, 1 Oct 2026 10:21:56 -0300 Subject: [PATCH 17/20] docs: align detailed release walkthrough --- docs/distribution.md | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/docs/distribution.md b/docs/distribution.md index 628b3a6..72c8724 100644 --- a/docs/distribution.md +++ b/docs/distribution.md @@ -128,7 +128,13 @@ MCP uses protocol messages on stdout and diagnostics on stderr; do not pipe bann ## Versioned release: verification first, publication separately authorized -[Verify and optionally release Repo2C4 tools](../.github/workflows/release.yml) runs **only via workflow_dispatch on trusted `main`**. It requires a SemVer input matching the version already declared in MSBuild, runs restore/format/build/test, pins and installs official LikeC4, packs the three tools and tests isolated installation/execution. A default dry-run creates **no tag, GitHub Release, NuGet publication or public artifact**. The release job is skipped by default. To publish deliberately, select `publish_release=true` **and** enter `publication_confirmation=PUBLISH`. The write-scoped job runs inside the protected `release` environment, rechecks that `main` still points at the validated commit, validates the exact four release assets, creates the versioned GitHub Release as a draft, then authenticates to NuGet.org with Trusted Publishing. `NuGet/login` exchanges the GitHub Actions OIDC token for a short-lived `NUGET_API_KEY`; no long-lived NuGet API key is stored as a repository or environment secret. The NuGet.org Trusted Publishing policy must authorize repository `rodri-oliveira-dev/Repo2C4`, workflow `release.yml` and the protected `release` environment. Only `Repo2C4.Cli`, `Repo2C4.Mcp` and `Repo2C4.Agent` are pushed, and the GitHub Release is made public only after those pushes succeed. A final read-only consumer job installs that exact version from NuGet.org and executes all three public commands. After that succeeds, a separate OIDC-scoped job publishes the matching `Repo2C4.Mcp` metadata to the Official MCP Registry and verifies the exact version through the registry API. Failed publication or propagation is reported without printing the API key. NuGet publication is not transactional across the three package IDs: if a later push fails after an earlier package is accepted, rerun only from the same protected `main` commit and the exact validated payload. The workflow's hash verification treats an already-published package with the same SHA-256 as satisfied and rejects a same-version package with different bytes; never rebuild or replace one package of a partially published version. +[Release](../.github/workflows/release.yml) runs **only via workflow_dispatch on trusted `main`**. The requested SemVer must already match the versions declared by the three product projects and `server.json`. Every run validates restore/format/build/test, installs the pinned official LikeC4 validator, packs `Repo2C4.Cli`, `Repo2C4.Mcp` and `Repo2C4.Agent`, and proves clean installation/execution from the local release payload. With the default `publish_release=false`, the workflow performs validation only and creates no tag or public release. + +To publish deliberately, set `publish_release=true` and `publication_confirmation=PUBLISH`. After validation, the workflow rechecks that `main` still points at the validated SHA and creates the release tag. The exact uploaded artifact is then consumed by two parallel protected jobs: one authenticates to NuGet.org through Trusted Publishing/OIDC and publishes the three packages, while the other publishes the same packages to GitHub Packages using the scoped `GITHUB_TOKEN`. Both jobs verify `SHA256SUMS` before pushing. Public NuGet indexing is intentionally **not** a post-publication release gate. + +The GitHub Release job runs only after both package registries succeed. It attaches the three validated `.nupkg` files and `SHA256SUMS` to the already-created tag. If a downstream publication step fails after the workflow created the tag, the compensating rollback job removes only a partial GitHub Release when present and intentionally keeps the tag so retries remain bound to the validated commit. Immutable NuGet.org and GitHub Packages versions are never deleted. If a package push partially succeeds, use **Re-run failed jobs** on the same workflow run so the original validated artifact is reused; do not start a new release run for the same partially published version. + +Official MCP Registry publication is outside the immutable release boundary. After `Repo2C4.Mcp` becomes publicly consumable from NuGet.org, manually dispatch [Publish Repo2C4 MCP Registry metadata](../.github/workflows/publish-mcp-registry.yml) for the released version. That workflow verifies the existing release/tag, validates the current registry metadata, publishes only the MCP Registry entry, and can be retried independently without rebuilding or republishing the NuGet package. The regular [CI](../.github/workflows/ci.yml) performs the same pack/clean-install smoke on push and PR without any AI key or release write permission. [CodeQL](../.github/workflows/codeql.yml), [Dependency Review](../.github/workflows/dependency-review.yml) and security/audit checks must remain green before the Phase 5 PR is merged. The dedicated [documentation PR workflow](architecture-pr.md) is a separate manual process, not release publication. From a21145e54d26c8855421c5655c4d75680d3ae21b Mon Sep 17 00:00:00 2001 From: Rodrigo de Oliveira Date: Thu, 1 Oct 2026 10:21:58 -0300 Subject: [PATCH 18/20] docs: align detailed release walkthrough in Portuguese --- docs/distribution.pt-BR.md | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/docs/distribution.pt-BR.md b/docs/distribution.pt-BR.md index ebee770..10c6255 100644 --- a/docs/distribution.pt-BR.md +++ b/docs/distribution.pt-BR.md @@ -123,7 +123,13 @@ O servidor rejeita raízes inexistentes, links simbólicos/junções e acessos f ## Release manual e validação -O workflow [Verify and optionally release Repo2C4 tools](../.github/workflows/release.yml) roda somente por `workflow_dispatch` na `main` confiável. A versão SemVer deve coincidir com a versão MSBuild já declarada. Sempre executa restore, formatação, build, testes, validação LikeC4, empacotamento e instalação isolada das três ferramentas. **Por padrão, apenas valida: não cria tag, release, pacote público ou publicação NuGet.** Para publicar deliberadamente, habilite `publish_release=true` **e** digite `publication_confirmation=PUBLISH`. O job com escrita roda no environment protegido `release`, confirma que a `main` não mudou, valida os quatro artefatos esperados, cria a GitHub Release versionada como draft e então autentica no NuGet.org com Trusted Publishing. O `NuGet/login` troca o token OIDC do GitHub Actions por uma `NUGET_API_KEY` temporária; nenhuma API key NuGet de longa duração é armazenada como secret do repositório ou do environment. A policy de Trusted Publishing no NuGet.org deve autorizar o repositório `rodri-oliveira-dev/Repo2C4`, o workflow `release.yml` e o environment protegido `release`. Somente `Repo2C4.Cli`, `Repo2C4.Mcp` e `Repo2C4.Agent` são enviados, e a GitHub Release só fica pública depois que esses pushes concluírem com sucesso. Depois, um job somente leitura instala essa mesma versão diretamente do NuGet.org e executa os três comandos como consumidor externo. Quando esse smoke público passa, um job separado com OIDC publica a metadata correspondente do `Repo2C4.Mcp` no Official MCP Registry e confirma a versão exata pela API do Registry. Falhas de publicação ou propagação são reportadas sem expor a credencial. A publicação NuGet não é transacional entre os três IDs de pacote: se um push posterior falhar depois que um pacote anterior já foi aceito, repita somente a partir do mesmo commit protegido da `main` e usando exatamente o payload validado. A verificação de hash considera satisfeito um pacote já publicado com o mesmo SHA-256 e rejeita bytes diferentes para a mesma versão; nunca reconstrua nem substitua apenas um pacote de uma versão parcialmente publicada. +O workflow [Release](../.github/workflows/release.yml) roda **somente por `workflow_dispatch` na `main` confiável**. A versão SemVer informada precisa coincidir com as versões declaradas pelos três projetos de produto e por `server.json`. Toda execução faz restore, formatação, build, testes, instala o LikeC4 oficial pinado, empacota `Repo2C4.Cli`, `Repo2C4.Mcp` e `Repo2C4.Agent` e comprova instalação/execução limpa a partir do payload local da release. Com `publish_release=false`, que é o padrão, o workflow apenas valida e não cria tag nem publicação pública. + +Para publicar deliberadamente, habilite `publish_release=true` e informe `publication_confirmation=PUBLISH`. Depois da validação, o workflow confirma novamente que a `main` ainda aponta para o SHA validado e cria a tag da release. O mesmo artefato validado é então consumido por dois jobs protegidos em paralelo: um autentica no NuGet.org por Trusted Publishing/OIDC e publica os três pacotes; o outro publica os mesmos pacotes no GitHub Packages usando o `GITHUB_TOKEN` com escopo restrito. Ambos conferem `SHA256SUMS` antes da publicação. A indexação pública do NuGet é propositalmente **fora** do gate pós-publicação da release. + +A GitHub Release só é criada depois que ambos os registries de pacotes concluem com sucesso. Ela anexa os três arquivos `.nupkg` validados e o `SHA256SUMS` à tag já criada. Se uma etapa posterior falhar depois que o workflow criou a tag, o job compensatório de rollback remove apenas uma GitHub Release parcial, quando existir, e mantém a tag para que qualquer retry continue preso ao commit validado. Versões imutáveis já publicadas no NuGet.org e GitHub Packages nunca são removidas. Se um push de pacote concluir apenas parcialmente, use **Re-run failed jobs** na mesma execução para reutilizar o artefato validado original; não inicie uma nova release para a mesma versão parcialmente publicada. + +A publicação no Official MCP Registry fica fora da fronteira imutável da release. Depois que `Repo2C4.Mcp` estiver publicamente consumível no NuGet.org, execute manualmente [Publish Repo2C4 MCP Registry metadata](../.github/workflows/publish-mcp-registry.yml) para a versão já lançada. Esse workflow valida a release/tag existente, valida a metadata atual do Registry, publica somente a entrada MCP e pode ser repetido de forma independente sem reconstruir nem republicar o pacote NuGet. O CI regular testa empacotamento e instalação sem credencial cloud nem permissão de release. CodeQL, Dependency Review e auditoria de dependências continuam obrigatórios. O workflow de [PR revisável de documentação](architecture-pr.pt-BR.md) é um fluxo manual distinto, sem merge automático. From 65376f06f189fae09a8d1dca7d2ed6862c7153ce Mon Sep 17 00:00:00 2001 From: Rodrigo de Oliveira Date: Thu, 1 Oct 2026 10:31:41 -0300 Subject: [PATCH 19/20] docs: explain registry metadata source --- .github/workflows/publish-mcp-registry.yml | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/.github/workflows/publish-mcp-registry.yml b/.github/workflows/publish-mcp-registry.yml index 6918817..d731efe 100644 --- a/.github/workflows/publish-mcp-registry.yml +++ b/.github/workflows/publish-mcp-registry.yml @@ -90,7 +90,11 @@ jobs: persist-credentials: false show-progress: false - - name: Checkout current registry metadata + # Package code is pinned to the released tag above. Registry metadata intentionally + # comes from the current trusted main commit so metadata-only fixes can be published + # after release without rebuilding or republishing the immutable NuGet package. + # The workflow still requires main and validates name/version/package binding below. + - name: Checkout current registry metadata from main uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7 with: ref: ${{ github.sha }} From 2fe0eafb0dbad04ce475f539356b9a50123fea86 Mon Sep 17 00:00:00 2001 From: Rodrigo de Oliveira Date: Thu, 1 Oct 2026 10:31:44 -0300 Subject: [PATCH 20/20] fix: generate release notes without command substitution --- .github/workflows/release.yml | 30 +++++++++++++++--------------- 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 7f12c2b..e6ff161 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -437,21 +437,21 @@ jobs: ) notes="$RUNNER_TEMP/release-notes.md" - cat > "$notes" < "$notes" if gh release view "$RELEASE_TAG" --repo "$GITHUB_REPOSITORY" >/dev/null 2>&1; then echo "GitHub Release $RELEASE_TAG already exists; refreshing validated assets."