Skip to content
Open
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
54 changes: 42 additions & 12 deletions .github/workflows/promote-stable-docs.yml
Original file line number Diff line number Diff line change
@@ -1,22 +1,27 @@
# Advances docs-stable to the current stable HEAD (or a chosen SHA).
#
# Auto path (workflow_run): only release-superdoc.yml triggers a promotion,
# and only when a real v* tag actually appeared on stable. The CLI/SDK/MCP
# tooling bundle and the React/esign/template-builder/vscode-ext wrappers do
# not advance docs-stable - docs-stable represents the documentation for
# the stable SuperDoc release.
# Auto path (workflow_run): release-stable.yml is the orchestrator that
# releases superdoc on stable, so we trigger off its completion 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. the first stable bundle run that ships CLI/SDK/MCP without a SuperDoc
# release, or a docs-only refresh between SuperDoc versions. Defaults to
# pushing the current origin/stable head; an optional `sha` input promotes
# a specific commit instead.
# 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 superdoc"
- "📦 Release stable tooling (CLI/SDK/MCP)"
types:
- completed
workflow_dispatch:
Expand All @@ -37,7 +42,10 @@ jobs:
promote:
if: |
github.event_name == 'workflow_dispatch' ||
(github.event.workflow_run.conclusion == 'success' && github.event.workflow_run.head_branch == 'stable')
(
github.event.workflow_run.head_branch == 'stable' &&
(github.event.workflow_run.conclusion == 'success' || github.event.workflow_run.conclusion == 'failure')
Comment thread
caio-pizzol marked this conversation as resolved.
)
runs-on: ubuntu-24.04
steps:
- name: Generate token
Expand All @@ -55,6 +63,14 @@ jobs:
# 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
Expand All @@ -72,8 +88,22 @@ jobs:
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) detected: $(echo "${new_tags}" | tr '\n' ' ')"
echo "New SuperDoc tag(s) verified on npm: $(echo "${new_tags}" | tr '\n' ' ')"
fi

- name: Push docs-stable (auto)
Expand Down
24 changes: 14 additions & 10 deletions .github/workflows/release-stable.yml
Original file line number Diff line number Diff line change
@@ -1,10 +1,13 @@
# Stable tooling mini-bundle: CLI -> SDK -> MCP, in order, on the same runner.
# These three share artifacts (SDK packages CLI native binaries; MCP imports
# SDK + engine code), so they release together with one queue slot and one
# permission context (including PyPI OIDC).
# Stable release orchestrator. Drives `release-local-stable.mjs`, which
# releases each package in the right chain on one runner, with one queue
# slot and one permission context (including PyPI OIDC).
#
# Out of scope: superdoc, react, esign, template-builder, vscode-ext. Each of
# those has its own per-package stable workflow.
# Tools chain (CLI -> SDK -> MCP) ships together because SDK packages
# CLI native binaries and MCP imports SDK + engine code. Core chain
# (currently superdoc) ships independently of the tools chain.
#
# Out of scope: esign, template-builder. Each has its own stable workflow
# until brought into the orchestrator.
name: 📦 Release stable tooling (CLI/SDK/MCP)

on:
Expand Down Expand Up @@ -103,7 +106,7 @@ jobs:
- name: Build packages
run: pnpm run build

- name: Release stable tooling (CLI -> SDK -> MCP)
- name: Release stable packages (orchestrator)
id: stable_release
env:
GITHUB_TOKEN: ${{ steps.generate_token.outputs.token }}
Expand Down Expand Up @@ -152,6 +155,7 @@ jobs:
# with:
# packages-dir: packages/sdk/langs/python/dist/
# skip-existing: true
# Docs promotion lives in promote-stable-docs.yml, which triggers on the
# SuperDoc workflow specifically. Tooling-bundle releases (CLI/SDK/MCP)
# do not advance docs-stable.
# Docs promotion lives in promote-stable-docs.yml. It triggers off this
# workflow's completion and gates on whether a real superdoc v* tag was
# created during the run, so tools-only releases leave docs-stable
# unchanged automatically.
9 changes: 5 additions & 4 deletions .github/workflows/release-superdoc.yml
Original file line number Diff line number Diff line change
@@ -1,14 +1,15 @@
# Auto-releases on push to main (@next) and stable (@latest).
# docs-stable is advanced by promote-stable-docs.yml when this workflow
# completes a real release on stable (not a no-op or PR preview).
# Auto-releases on push to main (@next).
# Stable releases are orchestrated centrally by release-stable.yml so that
# every stable release shares one concurrency slot and one git push lane.
# docs-stable is advanced by promote-stable-docs.yml when release-stable.yml
# produces a real superdoc v* tag (no-op runs do not advance docs-stable).
# Manual PR preview: dispatch with pr_number to publish @pr-<number>
name: 📦 Release superdoc

on:
push:
branches:
- main
- stable
paths:
- 'packages/superdoc/**'
- 'packages/layout-engine/**'
Expand Down
48 changes: 35 additions & 13 deletions scripts/__tests__/release-local.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -176,19 +176,18 @@ test('stable orchestrator prunes before snapshot and reports would-release previ
);
});

test('stable tooling bundle releases CLI, SDK, then MCP in order', async () => {
test('stable orchestrator releases tools chain (CLI, SDK, MCP) and core chain (superdoc) in order', async () => {
const content = await readRepoFile('scripts/release-local-stable.mjs');
assertOrder(content, "name: 'cli'", "name: 'sdk'", 'scripts/release-local-stable.mjs (cli before sdk)');
assertOrder(content, "name: 'sdk'", "name: 'mcp'", 'scripts/release-local-stable.mjs (sdk before mcp)');
assert.equal(
assert.ok(
content.includes("name: 'superdoc'"),
false,
'scripts/release-local-stable.mjs: superdoc has its own per-package stable workflow, not this bundle',
'scripts/release-local-stable.mjs: orchestrator must release superdoc so the v* tag drives docs-stable promotion in the same workflow',
);
assert.equal(
content.includes("name: 'esign'") || content.includes("name: 'react'") || content.includes("name: 'template-builder'") || content.includes("name: 'vscode-ext'"),
false,
'scripts/release-local-stable.mjs: only CLI/SDK/MCP belong in the tooling bundle',
'scripts/release-local-stable.mjs: react, vscode-ext, esign, and template-builder are added in follow-up PRs',
);
});

Expand Down Expand Up @@ -258,8 +257,11 @@ test('stable release workflows serialize on the shared release-stable concurrenc
'.github/workflows/release-stable.yml: skip-ci writeback runs must still no-op when they start',
);

// Per-package workflows that still auto-fire on stable directly.
// superdoc is excluded because release-stable.yml drives its stable
// releases now. The remaining workflows have not yet been brought into
// the orchestrator.
const perPackageStableWorkflows = [
'.github/workflows/release-superdoc.yml',
'.github/workflows/release-react.yml',
'.github/workflows/release-esign.yml',
'.github/workflows/release-template-builder.yml',
Expand All @@ -272,6 +274,15 @@ test('stable release workflows serialize on the shared release-stable concurrenc
`${file}: must trigger on push to both main and stable`,
);
}

// superdoc no longer auto-fires on stable - the orchestrator is its
// single stable release path.
const superdocWorkflow = await readRepoFile('.github/workflows/release-superdoc.yml');
assert.equal(
/branches:\s*\n\s*-\s*main\s*\n\s*-\s*stable/.test(superdocWorkflow),
false,
'.github/workflows/release-superdoc.yml: stable releases are driven by release-stable.yml; this workflow only fires on main',
);
});

test('MCP releaserc builds the package before publish so the tarball ships dist/', async () => {
Expand Down Expand Up @@ -319,24 +330,27 @@ test('release-state probes wrap fetch in bounded retry to absorb transient blips
);
});

test('docs promotion is keyed to SuperDoc only', async () => {
test('docs promotion is keyed to a real superdoc tag from the orchestrator run', async () => {
const promoteWorkflow = await readRepoFile('.github/workflows/promote-stable-docs.yml');
assert.ok(
promoteWorkflow.includes('workflow_run:'),
'.github/workflows/promote-stable-docs.yml: must trigger on workflow_run completion',
);
assert.ok(
/workflows:\s*\n\s*-\s*"📦 Release superdoc"/.test(promoteWorkflow),
'.github/workflows/promote-stable-docs.yml: must trigger only on the SuperDoc release workflow',
/workflows:\s*\n\s*-\s*"📦 Release stable tooling \(CLI\/SDK\/MCP\)"/.test(promoteWorkflow),
'.github/workflows/promote-stable-docs.yml: must trigger off the stable orchestrator workflow',
);
assert.equal(
/Release CLI|Release SDK|Release MCP|Release react|Release esign|Release template-builder|Release vscode-ext/.test(promoteWorkflow),
/"📦 Release CLI"|"📦 Release SDK"|"📦 Release MCP"|"📦 Release react"|"📦 Release esign"|"📦 Release template-builder"|"📦 Release vscode-ext"/.test(promoteWorkflow),
false,
'.github/workflows/promote-stable-docs.yml: docs-stable tracks SuperDoc only, not other packages',
'.github/workflows/promote-stable-docs.yml: must trigger only off the orchestrator, not per-package workflows',
);
// Chain-independent failures (e.g. tools fail, superdoc releases) must
// still promote docs. The git-tag detection is the source of truth.
assert.ok(
promoteWorkflow.includes("github.event.workflow_run.conclusion == 'success'"),
'.github/workflows/promote-stable-docs.yml: must only promote on successful SuperDoc runs',
promoteWorkflow.includes("github.event.workflow_run.conclusion == 'success'") &&
promoteWorkflow.includes("github.event.workflow_run.conclusion == 'failure'"),
'.github/workflows/promote-stable-docs.yml: must accept both success and failure conclusions so a tools-chain failure does not block superdoc-driven docs promotion',
);
assert.ok(
promoteWorkflow.includes("github.event.workflow_run.head_branch == 'stable'"),
Expand All @@ -347,6 +361,14 @@ test('docs promotion is keyed to SuperDoc only', async () => {
promoteWorkflow.includes('git tag --merged "${HEAD_SHA}" --list'),
'.github/workflows/promote-stable-docs.yml: must detect a real SuperDoc release (not a no-op) before pushing docs-stable',
);
// semantic-release pushes the v* tag during prepare, before publish runs.
// A failed publish leaves the tag on origin without the npm tarball, so
// tag presence alone is not sufficient evidence that the release shipped.
assert.ok(
promoteWorkflow.includes('npm view "superdoc@${version}"') &&
promoteWorkflow.includes('npm view "@harbour-enterprises/superdoc@${version}"'),
'.github/workflows/promote-stable-docs.yml: must verify npm publish completed for both superdoc and @harbour-enterprises/superdoc before promoting docs-stable, otherwise a tag-without-publish failure would advance docs to an unshipped version',
);
assert.ok(
promoteWorkflow.includes('refs/heads/docs-stable'),
'.github/workflows/promote-stable-docs.yml: must push to docs-stable',
Expand Down
Loading
Loading