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
2 changes: 2 additions & 0 deletions .github/workflows/deploy-docs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
76 changes: 58 additions & 18 deletions tools/jsdoc/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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-<version>` for each
listed version and falling back to the `<version>` 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

Expand All @@ -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-<version>` when available, otherwise `<version>`
- 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,
Expand All @@ -48,6 +52,33 @@ for testing:
- `--manifest <path>` — use a different version manifest
- `--out <dir>` — 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-<version>` tag if it exists.
2. Otherwise use the exact `<version>` 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`:
Expand All @@ -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

Expand All @@ -73,26 +105,34 @@ 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,
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

Expand Down Expand Up @@ -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.
2 changes: 1 addition & 1 deletion tools/jsdoc/published-versions.json
Original file line number Diff line number Diff line change
@@ -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-<version> and otherwise uses the <version> release tag. The selected source must declare that package version. Unlisted workspace versions are not added automatically.",
"versions": [
"0.12.0",
"0.13.0",
Expand Down
58 changes: 38 additions & 20 deletions tools/jsdoc/regenerate-published-docs.js
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down Expand Up @@ -249,18 +257,26 @@ 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(
'npx',
[
'jsdoc',
'--package',
path.join(worktreePath, 'package.json'),
packageJsonPath,
...inputs,
'-t',
__dirname,
Expand All @@ -272,7 +288,7 @@ function buildVersionDocs(version, outputDocsRoot, tempRoot) {

return {
worktreePath,
packageJsonPath: path.join(worktreePath, 'package.json'),
packageJsonPath,
};
} catch (error) {
removeWorktree(worktreePath);
Expand Down Expand Up @@ -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');

Expand All @@ -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(
Expand All @@ -401,4 +415,8 @@ function main() {
}
}

main();
if (require.main === module) {
main();
}

module.exports = { resolveDocumentationSources };
Loading