Skip to content

πŸš€ Promote stable docs #133

πŸš€ Promote stable docs

πŸš€ Promote stable docs #133

# Advances docs-stable to the current stable HEAD (or a chosen SHA).
#
# Auto path (workflow_run): release-stable.yml is the orchestrator that
# releases superdoc on stable, so we trigger off its completion, wait for the
# shared stable release lane to settle, and gate on whether a real v* tag
# appeared between the triggering run's head_sha and origin/stable. Tools-only
# runs (CLI/SDK/MCP without a superdoc release) leave docs-stable unchanged -
# no new v* tag, no push.
#
# We accept conclusion: failure as well as success because the orchestrator
# runs chains independently. A tools-chain failure that follows a successful
# superdoc release should still promote docs - the v* tag is the source of
# truth, not the workflow's overall conclusion. Cancelled and skipped runs
# are excluded since they may not have reached semantic-release at all.
#
# Manual path (workflow_dispatch): for cases the auto path cannot cover,
# e.g. a docs-only refresh between SuperDoc versions, or recovering from
# a missed promotion. Defaults to pushing the current origin/stable head;
# an optional `sha` input promotes a specific commit instead.
name: πŸš€ Promote stable docs
on:
workflow_run:
workflows:
- "πŸ“¦ Release stable tooling (CLI/SDK/MCP)"
types:
- completed
workflow_dispatch:
inputs:
sha:
description: 'Commit SHA to promote to docs-stable. Leave empty to promote the current origin/stable head.'
required: false
type: string
permissions:
contents: write
concurrency:
group: promote-stable-docs
cancel-in-progress: false
jobs:
promote:
if: |
github.event_name == 'workflow_dispatch' ||
(
github.event.workflow_run.head_branch == 'stable' &&
(github.event.workflow_run.conclusion == 'success' || github.event.workflow_run.conclusion == 'failure')
)
runs-on: ubuntu-24.04
steps:
- name: Generate token
id: generate_token
uses: actions/create-github-app-token@v2
with:
app-id: ${{ secrets.APP_ID }}
private-key: ${{ secrets.APP_PRIVATE_KEY }}
- uses: actions/checkout@v6
with:
fetch-depth: 0
token: ${{ steps.generate_token.outputs.token }}
- name: Wait for stable release lane to drain
if: github.event_name == 'workflow_run'
env:
GH_TOKEN: ${{ steps.generate_token.outputs.token }}
REPO: ${{ github.repository }}
run: |
set -euo pipefail
deadline=$((SECONDS + 1800))
while true; do
active_runs=$(gh run list \
--repo "$REPO" \
--branch stable \
--limit 100 \
--json databaseId,name,status,url \
--jq '[.[] | select((.name == "πŸ“¦ Release stable tooling (CLI/SDK/MCP)" or .name == "πŸ“¦ Release esign" or .name == "πŸ“¦ Release template-builder") and .status != "completed")] | length')
if [ "$active_runs" -eq 0 ]; then
echo "Stable release lane is idle."
break
fi
if [ "${SECONDS}" -ge "${deadline}" ]; then
echo "Timed out waiting for stable release lane to drain."
gh run list \
--repo "$REPO" \
--branch stable \
--limit 100 \
--json databaseId,name,status,url \
--jq '.[] | select((.name == "πŸ“¦ Release stable tooling (CLI/SDK/MCP)" or .name == "πŸ“¦ Release esign" or .name == "πŸ“¦ Release template-builder") and .status != "completed") | "\(.databaseId)\t\(.name)\t\(.status)\t\(.url)"'
exit 1
fi
echo "Waiting for ${active_runs} stable release run(s) to finish..."
sleep 30
done
# Auto path: gate on a real SuperDoc release between the triggering
# run's head_sha and origin/stable. A no-op semantic-release run must
# not advance docs-stable.
#
# The tag alone is not sufficient. @semantic-release/git pushes the v*
# tag during its prepare phase, before publish-superdoc.cjs runs. If
# publish fails and the orchestrator's recovery does not republish, the
# tag exists on origin without corresponding npm tarballs. In that case
# docs-stable would point at code that consumers cannot install. Verify
# both unscoped (`superdoc`) and scoped (`@harbour-enterprises/superdoc`)
# publishes are present at the released version before promoting.
- name: Detect SuperDoc release
if: github.event_name == 'workflow_run'
id: detect
env:
HEAD_SHA: ${{ github.event.workflow_run.head_sha }}
run: |
set -euo pipefail
git fetch origin stable --tags --force
tags_at_stable=$(git tag --merged origin/stable --list 'v[0-9]*' | sort -u)
tags_at_head=$(git tag --merged "${HEAD_SHA}" --list 'v[0-9]*' | sort -u)
new_tags=$(comm -23 <(echo "${tags_at_stable}") <(echo "${tags_at_head}"))
if [ -z "${new_tags}" ]; then
echo "released=false" >> "${GITHUB_OUTPUT}"
echo "No new v* tag between ${HEAD_SHA} and origin/stable β€” release-superdoc was a no-op."
else
for tag in ${new_tags}; do
version="${tag#v}"
if ! npm view "superdoc@${version}" version >/dev/null 2>&1; then
echo "released=false" >> "${GITHUB_OUTPUT}"
echo "Tag ${tag} exists but superdoc@${version} is not on npm β€” orchestrator publish did not complete; docs-stable will not advance."
exit 0
fi
if ! npm view "@harbour-enterprises/superdoc@${version}" version >/dev/null 2>&1; then
echo "released=false" >> "${GITHUB_OUTPUT}"
echo "Tag ${tag} exists but @harbour-enterprises/superdoc@${version} is not on npm β€” orchestrator publish did not complete; docs-stable will not advance."
exit 0
fi
done
echo "released=true" >> "${GITHUB_OUTPUT}"
echo "New SuperDoc tag(s) verified on npm: $(echo "${new_tags}" | tr '\n' ' ')"
fi
- name: Push docs-stable (auto)
if: github.event_name == 'workflow_run' && steps.detect.outputs.released == 'true'
run: |
set -euo pipefail
git fetch origin stable docs-stable --tags --force
docs_only_commits=$(git log --oneline origin/stable..origin/docs-stable -- apps/docs/ || true)
if [ -n "${docs_only_commits}" ]; then
echo "docs-stable has docs changes that are not on stable; refusing to overwrite:"
echo "${docs_only_commits}"
exit 1
fi
target=$(git rev-parse origin/stable)
expected=$(git rev-parse origin/docs-stable)
echo "Promoting ${target} to docs-stable with lease ${expected}."
git push --force-with-lease=refs/heads/docs-stable:"${expected}" origin "${target}:refs/heads/docs-stable"
# Manual path: trust the operator. Promote either the requested SHA
# or the current origin/stable head, while still using a lease so we
# never overwrite a concurrently updated docs-stable branch.
- name: Push docs-stable (manual)
if: github.event_name == 'workflow_dispatch'
env:
REQUESTED_SHA: ${{ inputs.sha }}
run: |
set -euo pipefail
git fetch origin stable docs-stable --tags --force
if [ -n "${REQUESTED_SHA}" ]; then
target="${REQUESTED_SHA}"
else
target=$(git rev-parse origin/stable)
fi
docs_only_commits=$(git log --oneline "${target}"..origin/docs-stable -- apps/docs/ || true)
if [ -n "${docs_only_commits}" ]; then
echo "docs-stable has docs changes that are not on ${target}; refusing to overwrite:"
echo "${docs_only_commits}"
exit 1
fi
expected=$(git rev-parse origin/docs-stable)
echo "Promoting ${target} to docs-stable with lease ${expected}."
git push --force-with-lease=refs/heads/docs-stable:"${expected}" origin "${target}:refs/heads/docs-stable"