Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion .github/release-process.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
29 changes: 18 additions & 11 deletions .github/skills/verify-release/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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

Expand Down
79 changes: 46 additions & 33 deletions .github/workflows/docs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -6,19 +6,23 @@ 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:
release:
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:
Expand Down Expand Up @@ -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"
Expand All @@ -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"
Expand All @@ -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:"
Expand Down