diff --git a/.github/workflows/deploy-docs.yml b/.github/workflows/deploy-docs.yml index 4e33a3ce..e4e2c07f 100644 --- a/.github/workflows/deploy-docs.yml +++ b/.github/workflows/deploy-docs.yml @@ -28,6 +28,8 @@ jobs: steps: - uses: actions/checkout@v7 with: + # Tag pushes use maintained tooling; manual runs keep the selected commit. + ref: ${{ github.event_name == 'push' && 'refs/heads/develop' || github.sha }} fetch-depth: 0 fetch-tags: true diff --git a/tools/jsdoc/README.md b/tools/jsdoc/README.md index e8475b59..cffb1175 100644 --- a/tools/jsdoc/README.md +++ b/tools/jsdoc/README.md @@ -7,9 +7,11 @@ GitHub Pages. The published version set is curated in [`published-versions.json`](./published-versions.json). That manifest is the source of truth for which versions appear on the live docs site. Each staging -run rebuilds the whole site from scratch: every listed version is rebuilt from -its Git tag. The staged tree is therefore a pure function of the manifest plus -the tags — there is no `gh-pages` branch state to maintain or drift out of sync. +run rebuilds the whole site from scratch, preferring `docs-` for each +listed version and falling back to the `` release tag when no matching +documentation tag exists. The staged tree is determined by the manifest, selected +tag commits and current documentation tooling — there is no `gh-pages` branch +state to maintain. ## Commands @@ -32,11 +34,13 @@ Stage the full publishable docs tree under `build/published-docs/`. Behavior: - reads the published version set from `published-versions.json` -- rebuilds every listed version from its Git tag +- selects `docs-` when available, otherwise `` +- checks that the selected source's package version matches the manifest entry +- rebuilds the selected sources in temporary worktrees without rewriting them - rebuilds the staged landing page index -This is the normal command to use for a new release, and it is fully -deterministic: deleting `build/` and rerunning reproduces the identical tree. +This is the normal command to use for a new release. Rebuilds use the same inputs +when the manifest, selected tag commits and documentation tooling are unchanged. This does **not** rebuild docs for every historical `rclnodejs` tag — only the curated subset listed in the manifest. To change which versions are published, @@ -48,6 +52,33 @@ for testing: - `--manifest ` — use a different version manifest - `--out ` — stage into a different directory +### Documentation Source Tags + +For `docs-*` pushes, the workflow checks out `refs/heads/develop` for the +generator, version manifest and template. Manual dispatch uses the selected +ref's commit (`github.sha`), so tooling changes can be validated before merging. +The generator selects documentation sources independently for every version +in that manifest: + +1. Use the exact `docs-` tag if it exists. +2. Otherwise use the exact `` release tag. +3. Fail if neither tag exists or the selected source declares a different + version in `package.json`. + +For example, `docs-2.3.0` can contain documentation corrections while `2.3.0` +remains unchanged. Older versions without documentation tags continue to use +their release tags. Logs identify the source selected for each version. +There are no release-specific source patches, and JSDoc errors remain fatal: +a broken documentation tag does not silently fall back to a release tag. + +Keep release tags immutable and ensure documentation-tag commits remain +API-compatible with the associated release. Commit documentation corrections, +then create or update the matching documentation tag. The local generator must +see that tag to validate its source; selecting a fixed branch alone is not enough. +Pushing a `docs-*` tag triggers a real deployment, so validate locally first. +For a remote dry run, use manual dispatch with `dry_run: true` on a branch +containing the new tooling, with the desired source tags already available. + ## New Release Example For a new release such as `1.9.0`: @@ -56,15 +87,16 @@ For a new release such as `1.9.0`: 2. Add `1.9.0` to the `versions` array in `published-versions.json`. 3. Run `npm run docs` to preview the new version locally (`docs/1.9.0/` and `docs/index.html`). -4. Cut and push the `1.9.0` release tag so `npm run docs:publish` can rebuild - it from Git. +4. Cut and push the `1.9.0` release tag. If using a separate corrected + documentation source, create a local `docs-1.9.0` tag at that commit. 5. Run `npm run docs:publish`. 6. Verify the staged output in: - `build/published-docs/docs/1.9.0/` - `build/published-docs/docs/index.html` - `build/published-docs/.nojekyll` -7. Publish the contents of `build/published-docs/` to GitHub Pages (the - `deploy-docs.yml` workflow does this automatically on a `docs-*` tag). +7. Merge tooling and manifest updates into `develop` before pushing a `docs-*` + tag for automatic deployment. Alternatively, manually dispatch the workflow + from the validated tooling branch to publish the site. ## GitHub Actions Deployment @@ -73,9 +105,16 @@ building and deploying docs to GitHub Pages. ### Triggers -- **Tag push** matching `docs-*` (e.g. `git tag docs-1.9.0 && git push origin docs-1.9.0`) — builds and deploys automatically. +- **Tag push** matching `docs-*` (e.g. `git tag docs-1.9.0 && git push origin docs-1.9.0`) — builds and deploys automatically using tooling and the full version manifest from `develop`. - **Manual dispatch** from the Actions tab — includes a `dry_run` toggle - (defaults to `true`). Set it to `false` to deploy. + (defaults to `true`). Uses tooling from the selected commit, not automatically + from `develop`. Set `dry_run` to `false` to deploy. + +GitHub loads the workflow definition itself from the triggering ref before +checkout runs. A documentation tag must therefore contain this checkout rule +for automatic deployments to use it. For historical tags carrying an older +workflow, manually dispatch from a branch containing the updated workflow +instead of relying on the historical tag's deployment behavior. These workflow runs only execute in the upstream `RobotWebTools/rclnodejs` repository. If you trigger the workflow from a fork, the build job is skipped, @@ -83,16 +122,17 @@ so manual dispatches and `docs-*` tag pushes there will not run the docs build. ### What it does -1. Full checkout with all tags. +1. Check out deployment tooling from `develop` for tag pushes, or the selected + commit for manual dispatch, with full history and all source tags. 2. Runs `npm run docs:publish` to stage the docs tree. This reads - `published-versions.json` and rebuilds every listed version from its tag. + `published-versions.json` and rebuilds every listed version from its + documentation tag or release-tag fallback. 3. Uploads the staged output as a Pages artifact. 4. Deploys to GitHub Pages (skipped when `dry_run` is `true`). The workflow only needs `contents: read` and never writes to any branch. -Because the manifest plus the Git tags fully describe the published set, the -live site is always reconstructed from a fresh build — nothing can silently -drop off. +The manifest, Git tags and current documentation tooling reconstruct the full +site from a fresh build — no prior deployment output is required. ### Pull Request and Push Validation @@ -140,4 +180,4 @@ rm -rf build/published-docs/.tmp - `.nojekyll` must remain in the staged output because the published docs tree uses an underscore-prefixed directory. - `published-versions.json` is the source of truth for which versions are - published; the Git tags are the content source. + published; the selected documentation/release tags are the content source. diff --git a/tools/jsdoc/published-versions.json b/tools/jsdoc/published-versions.json index 6dad3c95..e5b3a155 100644 --- a/tools/jsdoc/published-versions.json +++ b/tools/jsdoc/published-versions.json @@ -1,5 +1,5 @@ { - "$comment": "Curated list of rclnodejs versions whose JSDoc is published to GitHub Pages. This file is the source of truth for the published set: add a version here when cutting its release. Each deploy rebuilds every listed version from its Git tag, plus the current package.json version from the workspace, so the live site is a pure function of this manifest and the tags.", + "$comment": "Curated list of rclnodejs versions whose JSDoc is published to GitHub Pages. Add a version here when cutting its release. For each listed version, the generator prefers docs- and otherwise uses the release tag. The selected source must declare that package version. Unlisted workspace versions are not added automatically.", "versions": [ "0.12.0", "0.13.0", diff --git a/tools/jsdoc/regenerate-published-docs.js b/tools/jsdoc/regenerate-published-docs.js index 0ff7481a..9e5e9478 100644 --- a/tools/jsdoc/regenerate-published-docs.js +++ b/tools/jsdoc/regenerate-published-docs.js @@ -132,24 +132,32 @@ function readManifestVersions(manifestPath) { return Array.from(new Set(versions)).sort(compareVersionsAsc); } -function getTaggedReleaseVersions() { +function getGitTags() { return runCommand('git', ['tag', '--list']) .split(/\r?\n/) .map((entry) => entry.trim()) - .filter((entry) => semverDirectoryPattern.test(entry)) - .sort(compareVersionsAsc); + .filter(Boolean); } -function assertTagsExist(versions) { - const tags = new Set(getTaggedReleaseVersions()); - - const missing = versions.filter((version) => !tags.has(version)); +function resolveDocumentationSources(versions, availableTags) { + const tags = new Set(availableTags); + const missing = []; + const sources = versions.map((version) => { + const docsTag = `docs-${version}`; + const tag = tags.has(docsTag) ? docsTag : version; + if (!tags.has(tag)) { + missing.push(`${version} (expected ${docsTag} or ${version})`); + } + return { version, tag }; + }); if (missing.length) { throw new Error( - `Missing Git tags for requested versions: ${missing.join(', ')}` + `Missing Git tags for requested documentation versions: ${missing.join(', ')}` ); } + + return sources; } function ensureCleanOutput(outputRoot) { @@ -249,10 +257,18 @@ function writeIndexPackage(packageJsonPath, outputRoot) { return destinationPath; } -function buildVersionDocs(version, outputDocsRoot, tempRoot) { - const worktreePath = addWorktree(version, tempRoot, version); +function buildVersionDocs(version, sourceTag, outputDocsRoot, tempRoot) { + const worktreePath = addWorktree(`refs/tags/${sourceTag}`, tempRoot, version); try { + const packageJsonPath = path.join(worktreePath, 'package.json'); + const packageInfo = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8')); + if (packageInfo.version !== version) { + throw new Error( + `Documentation source ${sourceTag} declares package version ${packageInfo.version}; expected ${version}` + ); + } + const inputs = getJsdocInputs(worktreePath); runCommand( @@ -260,7 +276,7 @@ function buildVersionDocs(version, outputDocsRoot, tempRoot) { [ 'jsdoc', '--package', - path.join(worktreePath, 'package.json'), + packageJsonPath, ...inputs, '-t', __dirname, @@ -272,7 +288,7 @@ function buildVersionDocs(version, outputDocsRoot, tempRoot) { return { worktreePath, - packageJsonPath: path.join(worktreePath, 'package.json'), + packageJsonPath, }; } catch (error) { removeWorktree(worktreePath); @@ -362,10 +378,8 @@ function main() { const latestVersion = versions[versions.length - 1]; - // The staged site is a pure function of the manifest and the Git tags: every - // listed version is rebuilt from its release tag, so a fresh build always - // reproduces the full site with no branch state to drift. - assertTagsExist(versions); + // Prefer documentation tags without changing immutable release tags. + const sources = resolveDocumentationSources(versions, getGitTags()); ensureCleanOutput(options.outputRoot); fs.writeFileSync(path.join(options.outputRoot, '.nojekyll'), '', 'utf8'); @@ -376,10 +390,10 @@ function main() { try { console.log(`Building ${versions.length} documentation versions.`); - versions.forEach((version) => { - console.log(`- ${version}`); + sources.forEach(({ version, tag }) => { + console.log(`- ${version} (source: ${tag})`); - const buildResult = buildVersionDocs(version, docsRoot, tempRoot); + const buildResult = buildVersionDocs(version, tag, docsRoot, tempRoot); if (version === latestVersion) { latestPackageJsonPath = writeIndexPackage( @@ -401,4 +415,8 @@ function main() { } } -main(); +if (require.main === module) { + main(); +} + +module.exports = { resolveDocumentationSources };