diff --git a/.github/release-process.md b/.github/release-process.md index 79a91d12f..d7418b0b2 100644 --- a/.github/release-process.md +++ b/.github/release-process.md @@ -49,4 +49,7 @@ monitor both and confirm their published outputs: published release tags and deploys it to [csharp.sdk.modelcontextprotocol.io](https://csharp.sdk.modelcontextprotocol.io). Verify the new version appears in the version picker and that its major-version path serves the updated content. - A content-only docs refresh can be run later via manual dispatch with the `docs_ref` input. + A content-only docs refresh can be run later via manual dispatch. Its `docs_source` input defaults + to `release-tags` to mirror a release deployment; select `latest-branches` to refresh every + published major from its current `release/{MAJOR}.x` branch, using `main` for its + matching major. diff --git a/.github/skills/verify-release/SKILL.md b/.github/skills/verify-release/SKILL.md index 1f8a706b2..ad5477694 100644 --- a/.github/skills/verify-release/SKILL.md +++ b/.github/skills/verify-release/SKILL.md @@ -103,11 +103,14 @@ Report the run's conclusion, accounting for these docs-specific behaviors: - **Version discovery reads published releases.** For each major version >= 1, the workflow takes the most recently published non-draft release tagged `v{MAJOR}.*`. A draft release contributes nothing. -- **Every major is rebuilt.** Each major's docs are built from that major's latest release tag into - its own path (`/v1/`, `/v2/`). A new MAJOR adds a new path; the site root redirects to the newest - release, prereleases included. +- **Every major is rebuilt.** A release-triggered run builds each major from its latest release tag + into its own path (`/v1/`, `/v2/`). A manual dispatch defaults to the same release-tag content, + or can build every major from `release/{MAJOR}.x`, using `main` for its matching major, + by selecting `docs_source=latest-branches`. A new MAJOR adds a new path; the site root redirects + to the newest release, prereleases included. - **Orchestration comes from `main`.** The scripts and picker assets are always checked out from - `main`, while each version's content comes from its release tag. A docs fix that lives only in a + `main`. Release-triggered and default manual runs use release-tag content; a manual + `docs_source=latest-branches` run uses source-branch content. A docs fix that lives only in a release branch will not affect orchestration. If it failed, summarize the failing step. Common causes are a docs build failure in one version's @@ -164,17 +167,21 @@ Both remediations require explicit user confirmation. gh run rerun {run-id} --failed ``` -**Refresh the docs without a new release** — when documentation content needs correcting after the -release, the docs workflow accepts a manual dispatch that rebuilds one major version's content from -an arbitrary ref, without minting a product release: +**Rebuild the released docs** — use the default `release-tags` source to retry a failed Pages +deployment after a release, without minting a product release: ``` -gh workflow run docs.yml --field docs_ref={branch-tag-or-commit} +gh workflow run docs.yml --field docs_source=release-tags ``` -The ref's major version, read from `src/Directory.Build.props`, must have a published release; the -workflow fails fast if it does not. This replaces only the matching major's HTML — orchestration -and all other versions are unaffected. +**Refresh docs without a new release** — select `latest-branches` to rebuild every published major +from its current source branch (`release/{MAJOR}.x`, using `main` for its matching major): + +``` +gh workflow run docs.yml --field docs_source=latest-branches +``` + +The workflow fails instead of silently using a release tag when any major has no matching branch. ## Edge Cases diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index 1e187ef5f..458e3fc1f 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -6,8 +6,8 @@ name: Publish Docs # latest release, by date, for each major >= 1), builds each release tag into # its own sub-path (e.g. /v1/ and /v2/) with make generate-docs, injects a # version-picker widget into each page, adds a root redirect to the default -# version, and deploys the combined site. A manual dispatch can also rebuild -# the matching major-version path from a specified branch, tag, or commit. +# version, and deploys the combined site. A manual dispatch can build every +# major-version path from release tags or current source branches. # Triggers on release publish and manual dispatch. on: @@ -15,10 +15,14 @@ on: types: [published] workflow_dispatch: inputs: - docs_ref: - description: Branch, tag, or commit whose docs should refresh its matching major version - required: false - type: string + docs_source: + description: Source for every published major-version path + required: true + default: release-tags + type: choice + options: + - release-tags + - latest-branches # Sets permissions of the GITHUB_TOKEN to allow deployment to GitHub Pages permissions: @@ -86,7 +90,7 @@ jobs: - name: Build all versions shell: bash env: - CONTENT_REF: ${{ inputs.docs_ref }} + USE_SOURCE_BRANCHES: ${{ inputs.docs_source == 'latest-branches' }} run: | set -euo pipefail ROOT="$PWD" @@ -96,28 +100,43 @@ jobs: # Orchestration (discovered docs-versions.json, scripts, picker assets) # always comes from THIS branch. Normally each version's HTML is produced - # by its release tag. A manual docs_ref replaces the matching major's - # HTML with content built from that ref, allowing content-only refreshes - # without minting a product release. - git fetch --tags --force origin - CONTENT_REF="${CONTENT_REF:-}" - CONTENT_WORKTREE="" - CONTENT_SLUG="" - if [[ -n "$CONTENT_REF" ]]; then - CONTENT_WORKTREE="$ROOT/../work-content" - git worktree add --force --detach "$CONTENT_WORKTREE" "$CONTENT_REF" - CONTENT_SLUG="$(node "$ROOT/scripts/get-docs-version-slug.mjs" "$CONTENT_WORKTREE/src/Directory.Build.props")" - if ! node "$ROOT/scripts/list-versions.mjs" | cut -f1 | grep -Fxq "$CONTENT_SLUG"; then - echo "::error::The docs ref '$CONTENT_REF' has major '$CONTENT_SLUG', which has no published release." - exit 1 - fi - echo "Refreshing $CONTENT_SLUG docs from $CONTENT_REF" + # by its release tag. A manual dispatch can instead build every major from + # its current source branch. + git fetch --tags --force --prune origin "+refs/heads/*:refs/remotes/origin/*" + USE_SOURCE_BRANCHES="${USE_SOURCE_BRANCHES:-false}" + + MAIN_SLUG="" + if [[ "$USE_SOURCE_BRANCHES" == "true" ]]; then + MAIN_PROPS="$(mktemp)" + git show refs/remotes/origin/main:src/Directory.Build.props > "$MAIN_PROPS" + MAIN_SLUG="$(node "$ROOT/scripts/get-docs-version-slug.mjs" "$MAIN_PROPS")" + rm -f "$MAIN_PROPS" + echo "Refreshing all published docs from source branches (main serves $MAIN_SLUG)" + else + echo "Refreshing all published docs from release tags" fi while IFS=$'\t' read -r slug ref; do - if [[ "$slug" == "$CONTENT_SLUG" ]]; then - echo "::group::Build $slug (from ref $CONTENT_REF)" - wt="$CONTENT_WORKTREE" + if [[ "$USE_SOURCE_BRANCHES" == "true" ]]; then + source_branch="release/${slug#v}.x" + if ! git show-ref --verify --quiet "refs/remotes/origin/$source_branch"; then + if [[ "$slug" == "$MAIN_SLUG" ]]; then + source_branch="main" + else + echo "::error::No source branch found for $slug. Expected release/${slug#v}.x, or main with VersionPrefix for $slug." + exit 1 + fi + fi + + echo "::group::Build $slug (from branch $source_branch)" + wt="$ROOT/../work-$slug" + git worktree add --force --detach "$wt" "refs/remotes/origin/$source_branch" + source_slug="$(node "$ROOT/scripts/get-docs-version-slug.mjs" "$wt/src/Directory.Build.props")" + if [[ "$source_slug" != "$slug" ]]; then + git worktree remove --force "$wt" + echo "::error::Source branch '$source_branch' has major '$source_slug', not '$slug'." + exit 1 + fi else echo "::group::Build $slug (from tag $ref)" wt="$ROOT/../work-$slug" @@ -130,16 +149,10 @@ jobs: cp -a "$wt/artifacts/_site/." "$COMBINED/$slug/" node "$ROOT/scripts/inject-version-picker.mjs" "$COMBINED/$slug" "$slug" --base / - if [[ "$wt" != "$CONTENT_WORKTREE" ]]; then - git worktree remove --force "$wt" - fi + git worktree remove --force "$wt" echo "::endgroup::" done < <(node "$ROOT/scripts/list-versions.mjs") - if [[ -n "$CONTENT_WORKTREE" ]]; then - git worktree remove --force "$CONTENT_WORKTREE" - fi - node "$ROOT/scripts/finalize-docs-site.mjs" "$COMBINED" echo "Combined site contents:"