diff --git a/.docmode b/.docmode new file mode 100644 index 0000000..8e695ec --- /dev/null +++ b/.docmode @@ -0,0 +1 @@ +doc diff --git a/.github/dependabot.yml b/.github/dependabot.yml index 34d92e4..7cc2a76 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -6,3 +6,23 @@ updates: directory: / schedule: interval: daily + + # Local Antora preview tooling (package.json / package-lock.json). These exist + # to mirror what the central playbook (antora.riscv.org) runs in production, so + # major bumps are opt-in: taking one here without the central playbook moving + # first makes the local preview stop predicting the published site. + - package-ecosystem: npm + directory: / + schedule: + interval: weekly + ignore: + # asciidoctor-kroki 1.x needs a newer Asciidoctor.js than Antora 3.1.x + # bundles and dies with `block.$!= is not a function`. The central playbook + # pins 0.18.1 -- stay there until Antora itself moves. See ANTORA.md Phase 5. + - dependency-name: asciidoctor-kroki + update-types: [version-update:semver-major] + # Antora majors must follow the central playbook, not lead it. + - dependency-name: '@antora/cli' + update-types: [version-update:semver-major] + - dependency-name: '@antora/site-generator' + update-types: [version-update:semver-major] diff --git a/.github/workflows/build-pdf.yml b/.github/workflows/build-pdf.yml index 4f16b9d..c46433a 100644 --- a/.github/workflows/build-pdf.yml +++ b/.github/workflows/build-pdf.yml @@ -1,90 +1,345 @@ --- name: Create Specification Document -# The workflow is triggered by pull request, push to main, and manual dispatch. +# Cutting a release goes through version-bot.yml, which owns the version +# arithmetic, the monotonic guard, SPEC_STATE.md and the milestone PR. It calls +# this workflow directly rather than leaning on its tag push to trigger one, so +# the release chain runs on the built-in GITHUB_TOKEN and needs no PAT. on: - workflow_dispatch: + workflow_call: inputs: - version: - description: 'Release version, e.g. X.Y.Z:' - required: true - type: string - revision_mark: - description: 'Set revision mark as Draft, Release or Stable:' + release_version: + description: Exact version to build and release (e.g. v0.62). Must already be tagged. required: true type: string - default: Draft - prerelease: - description: Tag as a pre-release? - required: false - type: boolean - default: true draft: - description: Create release as a draft? + description: Create the GitHub Release as a draft. required: false type: boolean default: false - # pull_request: - # push: - # branches: - # - main + + # Preview only: builds the PDF and HTML for the current ref and publishes + # nothing. This used to accept release_version/target_phase/draft and cut a + # release of its own -- a second door that bypassed version-bot, so a milestone + # cut here got a tag and a release but no SPEC_STATE transition and no + # milestone PR. Releasing now has exactly one entry point. + workflow_dispatch: + + pull_request: + push: + branches: + - main + # A tag pushed by hand still builds and releases. version-bot's own tag push + # deliberately uses GITHUB_TOKEN -- which GitHub refuses to let start a + # workflow run -- so this cannot double-fire alongside the workflow_call + # above and re-release the same version. + tags: + - v* jobs: build: runs-on: ubuntu-latest + + # Expose the resolved version/date so the stamp-site-version job can keep + # antora.yml (the HTML site version) in exact lockstep with this PDF build. + outputs: + version: ${{ steps.set_build_version.outputs.version }} + date: ${{ steps.set_build_version.outputs.date }} + releasable: ${{ steps.classify.outputs.releasable }} + steps: + # Checkout the repository - name: Checkout repository uses: actions/checkout@v6 with: + # When version-bot calls us, build the TAGGED commit, not whatever main + # happens to point at by the time this job starts. + ref: ${{ inputs.release_version || github.ref }} + fetch-depth: 0 submodules: recursive - - name: Normalize version input - id: ver + # Pull the latest RISC-V Docs container image + - name: Pull Container + run: docker pull riscvintl/riscv-docs-base-container-image:latest + + # Set VERSION and DATE for this build. + # + # RELEASE builds are a version-bot call (inputs.release_version) or a + # hand-pushed tag. Both name an existing tag, so DATE comes from the TAGGED + # COMMIT rather than "today": rebuilding a tag then reproduces an identical + # PDF filename and an identical antora.yml stamp, instead of churning a + # stamp PR whose entire diff is page-revdate. + # + # Everything else is a DEVELOPMENT build (pull request, push to main, + # preview dispatch) and resolves to - via release-info.sh + # -- byte-for-byte what a local `make` produces. Development builds must NOT + # compute `next`: a preview stamped with the next release number spends a + # version that has not been cut, so the eventual real release ships a + # second, different document under the same name. Nothing is released from + # them; see RELEASABLE below. + - name: Set build version + id: set_build_version run: | - v='${{ github.event.inputs.version }}' - # Trim leading/trailing whitespace - v="$(echo "$v" | awk '{$1=$1; print}')" - # Drop leading 'v' or 'V' - v="${v#[Vv]}" - # Remove any internal spaces - v="${v//[[:space:]]/}" - echo "plain=$v" >> "$GITHUB_OUTPUT" - echo "tag=v$v" >> "$GITHUB_OUTPUT" - - - name: Validate version format + set -euo pipefail + + # Empty unless a tag pins it; filled in below. + date="" + tag="" + + if [ -n "${{ inputs.release_version }}" ]; then + tag="$(./scripts/release-info.sh normalize "${{ inputs.release_version }}")" + elif [ "${{ github.ref_type }}" = "tag" ]; then + tag="$(./scripts/release-info.sh normalize "${{ github.ref_name }}")" + fi + + if [ -n "$tag" ]; then + version="$tag" + release_build=true + if ! date="$(git log -1 --format=%cs "$tag" 2>/dev/null)"; then + echo "::error::$tag is not a tag in this repository; a release build must name an existing tag." + exit 1 + fi + else + version="$(./scripts/release-info.sh version)" + # NOT derivable from the version string: release-info.sh returns a + # bare vX.YY (no -) whenever HEAD happens to carry a tag, so a + # push to main or a preview dispatch landing on a freshly tagged + # commit would otherwise look releasable and re-cut that release. + release_build=false + fi + + # Resolve the date ONCE so the PDF and the Antora version stamp share it + # exactly (no midnight-boundary drift between separate steps). + date="${date:-$(date +%Y-%m-%d)}" + + echo "VERSION=$version" >> "$GITHUB_ENV" + echo "DATE=$date" >> "$GITHUB_ENV" + echo "RELEASE_BUILD=$release_build" >> "$GITHUB_ENV" + echo "version=$version" >> "$GITHUB_OUTPUT" + echo "date=$date" >> "$GITHUB_OUTPUT" + + - name: Classify release type + id: classify run: | - v='${{ steps.ver.outputs.plain }}' - if ! [[ "$v" =~ ^[0-9]+(\.[0-9]+){1,2}(-[0-9A-Za-z.-]+)?$ ]]; then - echo "Invalid version '$v'. Use formats like 0.2, 0.2.0, or 0.2.0-rc1" - exit 1 + set -euo pipefail + + # Two independent conditions, both required, and neither of them the + # event name -- under workflow_call `github.event_name` describes the + # CALLER, so an event test would misclassify every version-bot release. + # + # RELEASE_BUILD this run was asked for a specific tag + # no - the resolved version is not a development version + # + # The second alone is not enough (a preview can land on a tagged commit + # and resolve to a bare version); the first alone is not enough either + # (it is the last line of defence against ever releasing a - + # version). Every release step and the stamp job gate on the result. + if [ "$RELEASE_BUILD" = "true" ] && [ "${VERSION#*-}" = "${VERSION}" ]; then + releasable=true + else + releasable=false + echo "$VERSION is a development build; this run publishes nothing." fi + echo "RELEASABLE=$releasable" >> "$GITHUB_ENV" + echo "releasable=$releasable" >> "$GITHUB_OUTPUT" - - name: Pull Container - run: docker pull riscvintl/riscv-docs-base-container-image:latest + # Milestone-gate tags (v0.6, v0.8, v0.9, v0.99, v1.0) get a + # non-prerelease GitHub Release. release-info.sh is the single source of + # truth for which versions are milestones. + if ./scripts/release-info.sh is-milestone "${VERSION}"; then + echo "OFFICIAL_RELEASE=true" >> "$GITHUB_ENV" + else + echo "OFFICIAL_RELEASE=false" >> "$GITHUB_ENV" + fi + # Build Files. VERSION and DATE are exported so the Makefile and + # release-info.sh produce ARC-compliant PDF names: -v-.pdf - name: Build Files - run: make - env: - VERSION: ${{ steps.ver.outputs.tag }} # e.g., v0.2.0 - REVMARK: ${{ github.event.inputs.revision_mark }} + run: | + set -euo pipefail + export VERSION="${VERSION}" + export DATE="${DATE}" + echo "Building ${VERSION} (${DATE})" + make + # Upload the built documents as a single artifact. + # + # The HTML is rendered on every run whether or not we keep it -- build-docs + # depends on $(DOCS_HTML) as well as $(DOCS_PDF) -- so retaining it costs no + # extra build time, and reviewers get the rendered document instead of only + # the PDF. It travels as one file: the spec sets :data-uri:, so images and + # the stylesheet are inlined and nothing sidecar has to ship with it. + # + # `*` does not cross directory separators, so this cannot sweep up an Antora + # build/site/ or build/pages-site/ tree if one ever lands in this job. + # + # Release assets stay PDF-only on purpose (see the steps below): the ARC + # submission artifact is the PDF, and the published HTML is the Antora site. - name: Upload Build Artifacts + id: upload uses: actions/upload-artifact@v6 with: name: Build Artifacts - path: ${{ github.workspace }}/build/*.pdf + path: | + ${{ github.workspace }}/build/*.pdf + ${{ github.workspace }}/build/*.html retention-days: 30 - - name: Create Release - if: github.event_name == 'workflow_dispatch' + # Put a direct link in the run summary, so reaching the build output is one + # click from the Checks tab rather than a hunt down the summary page. Same + # reasoning as validate-content-source.yml: needs no permissions and works + # on fork PRs, unlike a PR comment. + - name: Link the build artifacts in the run summary + if: steps.upload.outputs.artifact-url != '' + env: + ARTIFACT_URL: ${{ steps.upload.outputs.artifact-url }} + run: | + set -euo pipefail + { + echo "### Built document" + echo + echo "[Download the build artifacts]($ARTIFACT_URL) — \`${VERSION}\` (${DATE})." + echo + echo "- Downloading requires being signed in to GitHub (an Actions limitation)." + echo "- Retained for 30 days." + } >> "$GITHUB_STEP_SUMMARY" + + # Publish the GitHub Release. + # + # There used to be four of these -- official/prerelease x tag-push/manual -- + # because a manual dispatch could cut its own release. Releasing now has one + # entry point (a tag, whether version-bot pushed it or a maintainer did), so + # the only remaining axis is milestone vs interim. RELEASABLE, not the event + # name, decides whether a release happens at all. + - name: Create Release (Official) + uses: softprops/action-gh-release@v2 + with: + files: ${{ github.workspace }}/build/*.pdf + tag_name: ${{ env.VERSION }} + name: Release ${{ env.VERSION }} + draft: ${{ inputs.draft || false }} + prerelease: false + env: + GITHUB_TOKEN: ${{ secrets.GHTOKEN || secrets.GITHUB_TOKEN }} + if: env.RELEASABLE == 'true' && env.OFFICIAL_RELEASE == 'true' + + - name: Create Release (Pre-release) uses: softprops/action-gh-release@v2 with: files: ${{ github.workspace }}/build/*.pdf - tag_name: ${{ steps.ver.outputs.tag }} # e.g., v0.2.0 - name: Release ${{ steps.ver.outputs.plain }} - draft: ${{ github.event.inputs.draft }} - prerelease: ${{ github.event.inputs.prerelease }} - target_commitish: ${{ github.sha }} + tag_name: ${{ env.VERSION }} + name: Release ${{ env.VERSION }} + draft: ${{ inputs.draft || false }} + prerelease: true env: - # Prefer the default token unless you have a reason to use a PAT. - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + GITHUB_TOKEN: ${{ secrets.GHTOKEN || secrets.GITHUB_TOKEN }} + if: env.RELEASABLE == 'true' && env.OFFICIAL_RELEASE != 'true' + + # Keep the Antora HTML site version in EXACT lockstep with the PDF this run just + # built: stamp the SAME resolved version/date into antora.yml on main, so the + # central playbook publishes the version the PDF carries. The PDF and this stamp + # come from one build job (same source, same VERSION/DATE) -- they cannot drift. + # + # Runs only for real releases: not PR previews (nothing to publish) and not + # drafts (not public yet). Never moves main's version backwards, so rebuilding + # an older tag will not regress the site. + # + # The stamp lands as a REVIEW PR, not a direct push to `main`. This template is + # seeded into many spec repos whose branch protection we do not control, and a + # rejected push would leave the release green while the site version silently + # lagged the PDF -- the exact divergence this job exists to prevent. Opening a + # PR works under any protection setting and matches the milestone-PR idiom in + # version-bot.yml. Cost: a maintainer must merge it for the site to catch up, + # so treat that merge as part of cutting a release (see MIGRATION.md Step 14). + stamp-site-version: + needs: build + runs-on: ubuntu-latest + # + # Gated purely on the build job's own verdict: under workflow_call + # `github.event_name` and `github.ref_type` describe the CALLER, so testing + # them here would stamp on preview runs and skip real releases. + if: >- + needs.build.outputs.releasable == 'true' && + inputs.draft != true + permissions: + contents: write + pull-requests: write + steps: + - name: Checkout main + uses: actions/checkout@v6 + with: + ref: main + fetch-depth: 0 + token: ${{ secrets.GHTOKEN || secrets.GITHUB_TOKEN }} + # Do not persist the token as a git extraheader. create-pull-request + # injects its own auth header; a persisted one here makes git send two + # Authorization headers -> GitHub 400 "Duplicate header: Authorization". + persist-credentials: false + + - name: Stamp antora.yml to match the released PDF version + id: stamp + env: + VERSION: ${{ needs.build.outputs.version }} + DATE: ${{ needs.build.outputs.date }} + run: | + set -euo pipefail + + current="$(grep -E '^version:' antora.yml | awk '{print $2}')" + echo "main antora.yml version: ${current:-}; released version: $VERSION" + + # Monotonic guard: never stamp main backwards (e.g. when an old tag is + # rebuilt). Compare by DECIMAL order via release-info.sh -- `sort -V` + # would rank v0.8 below v0.61 and stamp the site backwards. + # + # Skip at >=, not just at >. At an EQUAL version the only field that can + # still differ is page-revdate, and a PR that moves a released version's + # date forward is pure churn that also misdates the release. (Rebuilding + # v0.01 a day later opened exactly such a PR; the tag-derived DATE in the + # build job closes the other half of that hole.) + if [ -n "$current" ] && [ "$current" != "~" ]; then + if [ "$(./scripts/release-info.sh compare "$current" "$VERSION")" != "-1" ]; then + echo "main is already at $current (>= $VERSION); skipping stamp." + echo "changed=false" >> "$GITHUB_OUTPUT" + exit 0 + fi + fi + + # Leaves antora.yml modified in the worktree; create-pull-request below + # is what commits it. stamp-antora fails loudly if it cannot stamp, so a + # no-op here means "already correct", never "silently did nothing". + make stamp-antora VERSION="$VERSION" DATE="$DATE" + + if git diff --quiet -- antora.yml; then + echo "antora.yml already matches $VERSION; nothing to commit." + echo "changed=false" >> "$GITHUB_OUTPUT" + exit 0 + fi + echo "changed=true" >> "$GITHUB_OUTPUT" + + - name: Open site version stamp PR + if: steps.stamp.outputs.changed == 'true' + uses: peter-evans/create-pull-request@v6 + with: + token: ${{ secrets.GHTOKEN || secrets.GITHUB_TOKEN }} + base: main + commit-message: 'chore(site): stamp Antora version ${{ needs.build.outputs.version }} to match PDF' + branch: gitbot/stamp-site-${{ needs.build.outputs.version }} + delete-branch: true + title: Stamp Antora site version ${{ needs.build.outputs.version }} to match released PDF + body: | + This PR was opened automatically by the `${{ needs.build.outputs.version }}` release build. + + The ARC Author Guide requires the published HTML site version to match + the submission PDF exactly. Antora reads a static `version:` from the + committed `antora.yml`, so a release has to stamp that file. + + Released PDF version: `${{ needs.build.outputs.version }}` (${{ needs.build.outputs.date }}) + + **Until this merges, the site version lags the released PDF.** The + values come from `scripts/release-info.sh` -- the same source the PDF + build uses -- so this needs a merge, not an edit. + labels: | + site-version + automated + add-paths: | + antora.yml diff --git a/.github/workflows/pre-commit.yml b/.github/workflows/pre-commit.yml index 179cf94..cbbb4d6 100644 --- a/.github/workflows/pre-commit.yml +++ b/.github/workflows/pre-commit.yml @@ -11,5 +11,5 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v6 - - uses: actions/setup-python@v3 + - uses: actions/setup-python@v5 - uses: pre-commit/action@v3.0.0 diff --git a/.github/workflows/deploy-website.yml b/.github/workflows/publish-site.yml similarity index 100% rename from .github/workflows/deploy-website.yml rename to .github/workflows/publish-site.yml diff --git a/.github/workflows/static.yml b/.github/workflows/static.yml deleted file mode 100644 index ce8b323..0000000 --- a/.github/workflows/static.yml +++ /dev/null @@ -1,89 +0,0 @@ -# Builds the Antora site for this repository and deploys it to GitHub Pages at -# https://riscv.github.io/docs-dev-guide/. -# -# This is a standalone preview of THIS repository. The authoritative RISC-V -# documentation site is assembled by the central playbook at -# riscv-admin/antora.riscv.org, which consumes this repo as a content source. -name: Deploy Antora site to Pages - -on: - # Runs on pushes targeting the default branch - push: - branches: ["main"] - - # Allows you to run this workflow manually from the Actions tab - workflow_dispatch: - -# Sets permissions of the GITHUB_TOKEN to allow deployment to GitHub Pages -permissions: - contents: read - pages: write - id-token: write - -# Allow only one concurrent deployment, skipping runs queued between the run in-progress and latest queued. -# However, do NOT cancel in-progress runs as we want to allow these production deployments to complete. -concurrency: - group: "pages" - cancel-in-progress: false - -jobs: - build: - runs-on: ubuntu-latest - steps: - - name: Checkout - uses: actions/checkout@v6 - - - name: Setup Node - uses: actions/setup-node@v4 - with: - node-version: '20' - cache: npm - - - name: Install Antora - run: npm ci - - # antora-playbook.yml is the local preview playbook: it points site.url at - # the repo and Kroki at the docker-compose server on localhost. Neither is - # reachable from a runner, so override both here rather than editing the - # playbook and breaking local preview. - - name: Build site - run: | - ./node_modules/.bin/antora --fetch antora-playbook.yml \ - --url https://riscv.github.io/docs-dev-guide \ - --attribute kroki-server-url=https://kroki.io - - # The shared RISC-V UI bundle hardcodes links to /home/index.html -- the - # landing page of the central site's `home` component, which a standalone - # preview does not build. Emit a redirect there so the header logo and the - # "RISC-V Specifications" nav link resolve instead of 404ing. - - name: Add home redirect for the shared UI bundle - run: | - mkdir -p build/site/home - cat > build/site/home/index.html <<'HTML' - - - - - - Redirect Notice -

The RISC-V Authoring and Editing Guide is here.

- HTML - - - name: Setup Pages - uses: actions/configure-pages@v5 - - - name: Upload artifact - uses: actions/upload-pages-artifact@v3 - with: - path: build/site - - deploy: - needs: build - environment: - name: github-pages - url: ${{ steps.deployment.outputs.page_url }} - runs-on: ubuntu-latest - steps: - - name: Deploy to GitHub Pages - id: deployment - uses: actions/deploy-pages@v4 diff --git a/.github/workflows/vale-linting.yml b/.github/workflows/vale-linting.yml new file mode 100644 index 0000000..08cd975 --- /dev/null +++ b/.github/workflows/vale-linting.yml @@ -0,0 +1,11 @@ +--- +name: Linting with Vale using Reusable Config +on: + push: + branches: [main] + pull_request: + branches: [main] + +jobs: + call-reusable-vale-workflow-in-riscv-vale-repo: + uses: riscv-admin/riscv-vale/.github/workflows/vale-reusable.yml@main diff --git a/.github/workflows/vale-using-reusable.yml b/.github/workflows/vale-using-reusable.yml deleted file mode 100644 index 224c410..0000000 --- a/.github/workflows/vale-using-reusable.yml +++ /dev/null @@ -1,10 +0,0 @@ -name: Linting with Vale using Reusable Config -on: - push: - branches: [main] - pull_request: - branches: [main] - -jobs: - call-reusable-vale-workflow-in-riscv-vale-repo: - uses: riscv-admin/riscv-vale/.github/workflows/vale-reusable.yml@main diff --git a/.github/workflows/validate-content-source.yml b/.github/workflows/validate-content-source.yml new file mode 100644 index 0000000..be8cf9b --- /dev/null +++ b/.github/workflows/validate-content-source.yml @@ -0,0 +1,194 @@ +--- +name: Validate Antora content source + +# This repo is ONE content source. The real site is assembled elsewhere by the +# central playbook (github.com/riscv-admin/antora.riscv.org) from 20+ sources -- +# this job does NOT build or deploy that site. +# +# What it does: build THIS component in isolation via the local +# antora-playbook.yml (same extensions/attributes as production) as a PR gate, so +# broken AsciiDoc, unresolved intra-component xrefs/includes, or extension errors +# fail here instead of stalling the shared central library build once the branch +# is consumed. +# +# What it CANNOT check: cross-component references (e.g. `common::risc-v_logo.svg` +# or xrefs into other specs) resolve only in the central multi-source build, so +# they are expected to be unresolved here and are exception-listed below. +# +# The release-time version stamp (make stamp-antora -> antora.yml) is NOT part of +# this gate; it runs from build-pdf.yml on release. See ANTORA.md Phase 4/6. +# +# The gate's build output is also KEPT, as a downloadable artifact, so reviewers +# can see a content change rendered instead of reading the AsciiDoc diff. The site +# was already being built here on every PR and thrown away; retaining it costs no +# build time. This is a HEAD-only preview -- no version selector, no release +# history -- and it is not a deployment: see the issue trail on why a real per-PR +# preview host is out of scope for this template (the RISC-V org refuses Pages +# site creation to the workflow token, and fork PRs cannot hold deploy secrets). + +on: + pull_request: + push: + branches: + - main + workflow_dispatch: + +# Read-only: this job builds and inspects the component, it never writes to the +# repo or publishes anything. +permissions: + contents: read + +jobs: + validate: + runs-on: ubuntu-latest + + # Local Kroki server for asciidoctor-kroki (wavedrom etc.). Matches the + # kroki-server-url: http://localhost:9870 attribute in antora-playbook.yml. + # Pinned: a floating `latest` can change diagram output (or break) between + # runs with no change in this repo. Keep in sync with docker-compose.yml. + services: + kroki: + image: yuzutech/kroki:0.31.1 + ports: + - 9870:8000 + + steps: + # submodules -- docs-resources supplies the cover logo staged below. The + # gate itself does not need it; the preview artifact does. + - name: Checkout repository + uses: actions/checkout@v6 + with: + fetch-depth: 0 + submodules: recursive + + - name: Set up Node + uses: actions/setup-node@v4 + with: + node-version: 20 + cache: npm + + - name: Install preview tooling + run: npm ci + + # The service container is "started" before it can serve; without this the + # Antora build can race Kroki's JVM startup and fail on diagram rendering. + # Polled from the runner (which has curl) rather than via a container + # healthcheck, since the Kroki image ships no HTTP client to probe itself. + - name: Wait for Kroki + run: | + set -euo pipefail + for i in $(seq 1 30); do + if curl -fsS http://localhost:9870/health >/dev/null 2>&1; then + echo "Kroki is up after ${i} attempt(s)." + exit 0 + fi + sleep 2 + done + echo "::error::Kroki did not become healthy within 60s" + exit 1 + + # antora.yml sets `cover-logo: common::risc-v_logo.svg` -- a shared asset + # from the central `common` component, which does not exist in a + # single-component build, so index.adoc (the start page) renders a broken + # image. Tolerable when this job was only a pass/fail gate; not tolerable + # now that its output is published as a preview, because index.adoc is the + # FIRST page a reviewer opens. Vendor the SVG and hard-set the attribute, + # exactly as scripts/build-pages-site.sh does for the Pages build. + # modules/ROOT/images/risc-v_logo.svg is gitignored; this only ever touches + # the runner's worktree. + - name: Stage cover logo + id: cover + run: | + set -euo pipefail + src=docs-resources/images/risc-v_logo.svg + if [ -f "$src" ]; then + mkdir -p modules/ROOT/images + cp "$src" modules/ROOT/images/risc-v_logo.svg + echo "cover_logo=risc-v_logo.svg" >> "$GITHUB_OUTPUT" + echo "Staged cover logo from docs-resources." + else + echo "::warning::$src not found (submodule not initialized?) -- the" + echo "::warning::preview cover image will be unresolved." + echo "cover_logo=" >> "$GITHUB_OUTPUT" + fi + + - name: Build component (isolated) + env: + COVER_LOGO: ${{ steps.cover.outputs.cover_logo }} + run: | + # Hard-set (playbook-level) so it overrides the antora.yml default. + # Omitted entirely if staging failed, leaving the pre-existing + # unresolved-`common::` behaviour that the exception below covers. + attrs=() + if [ -n "$COVER_LOGO" ]; then + attrs+=(--attribute "cover-logo=$COVER_LOGO") + fi + + # Run the pinned local binary rather than `npx antora`, matching + # scripts/build-pages-site.sh. npx routes argv through npm's own + # option parser, which claims bare `key=value` arguments -- it warns + # `"cover-logo=..." is being parsed as a normal command line argument` + # -- so the attribute above is safer passed straight to the CLI. + antora_bin=./node_modules/.bin/antora + + set +e + "$antora_bin" --fetch --log-format=json "${attrs[@]}" antora-playbook.yml 2>&1 | tee antora-build.log + status=${PIPESTATUS[0]} + set -e + + if [ "$status" -ne 0 ]; then + echo "::error::Antora exited with status $status" + exit "$status" + fi + + # Antora logs unresolved xrefs/includes/images at error level without + # failing the build. Treat those as failures -- EXCEPT cross-component + # references that only resolve in the central multi-source build. + # + # The cover-logo exception is now the FALLBACK path only: with the logo + # staged above the reference resolves and this filter matches nothing, + # so the gate actually validates the image macro (it previously could + # not -- see modules/ROOT/pages/index.adoc). It stays so an unavailable + # submodule degrades to a warning instead of a red gate. + unexpected="$(grep -E '"level":"(error|fatal)"' antora-build.log \ + | grep -v 'common::risc-v_logo.svg' || true)" + if [ -n "$unexpected" ]; then + echo "::error::Antora reported unexpected errors:" + echo "$unexpected" + exit 1 + fi + echo "Component builds clean (ignoring expected cross-component refs)." + + # The site the gate just built, kept instead of discarded (~4 MB, ~70 + # files). Uploaded only on a clean gate: a site built from a tree that + # failed validation would misrepresent the change. + - name: Upload rendered site + id: upload + uses: actions/upload-artifact@v6 + with: + name: Rendered site (Antora) + path: build/site + retention-days: 14 + + # Put a direct link in the run summary. `artifact-url` is an output of + # upload-artifact -- GitHub documents it precisely for this. This is the + # cheap half of discoverability: it needs no permissions and works on fork + # PRs, where the pull_request GITHUB_TOKEN is read-only and a step here + # could not post a PR comment. A comment in the PR conversation needs a + # separate workflow_run-triggered workflow; that is deliberately not in + # this change. + - name: Link the rendered site in the run summary + if: steps.upload.outputs.artifact-url != '' + env: + ARTIFACT_URL: ${{ steps.upload.outputs.artifact-url }} + run: | + set -euo pipefail + { + echo "### Rendered site (preview)" + echo + echo "[Download the rendered Antora site]($ARTIFACT_URL) — unzip, open \`index.html\`." + echo + echo "- Preview of this branch's HEAD only: no version selector, no release history." + echo "- Downloading requires being signed in to GitHub (an Actions limitation)." + echo "- Cross-component references into other specs still resolve only on the central site." + } >> "$GITHUB_STEP_SUMMARY" diff --git a/.github/workflows/version-bot.yml b/.github/workflows/version-bot.yml new file mode 100644 index 0000000..a829117 --- /dev/null +++ b/.github/workflows/version-bot.yml @@ -0,0 +1,267 @@ +--- +name: Version Bot + +on: + workflow_dispatch: + inputs: + target_phase: + description: Force-jump to the milestone floor version for this phase + required: false + type: choice + default: auto-next + options: + - auto-next + - draft-and-development + - development-complete + - stabilized + - frozen + - ratification-ready + - ratified + release_version: + description: Explicit version override (e.g., v0.9). If set, target_phase is ignored. + required: false + type: string + allow_non_monotonic: + description: Allow jumps lower than the latest existing version tag + required: false + type: boolean + default: false + draft: + description: Create the GitHub Release as a draft + required: false + type: boolean + default: false + +permissions: + contents: write + pull-requests: write + +jobs: + tag-version: + runs-on: ubuntu-latest + outputs: + previous_version: ${{ steps.version.outputs.previous_version }} + next_version: ${{ steps.version.outputs.next_version }} + previous_phase: ${{ steps.version.outputs.previous_phase }} + next_phase: ${{ steps.version.outputs.next_phase }} + milestone_transition: ${{ steps.version.outputs.milestone_transition }} + mode: ${{ steps.version.outputs.mode }} + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + with: + fetch-depth: 0 + # Deliberately the built-in GITHUB_TOKEN, even when GHTOKEN exists. + # + # A tag pushed by GITHUB_TOKEN cannot start a `push: tags: v*` run. + # That used to be the problem -- the tag landed and nothing built, so + # the whole release chain silently required a PAT. It is now the + # mechanism: the release-pdf and publish-site jobs below invoke those + # workflows directly, and an inert tag push is what keeps them from + # ALSO firing on the tag and cutting the same release twice. + # + # Unlike milestone-pr, this job does not use create-pull-request, so + # persisting credentials here is safe. + token: ${{ secrets.GITHUB_TOKEN }} + + # The release chain no longer depends on GHTOKEN: this workflow calls + # build-pdf.yml and publish-site.yml directly rather than hoping its tag + # push triggers them. What still depends on it is the CHECKS on the PRs + # this template's bots open (the milestone PR, the site version stamp PR): + # GitHub refuses to let a GITHUB_TOKEN-authored PR start workflow runs, so + # those checks sit in 'action_required' until a maintainer approves them. + # Warn rather than fail -- the tag, the release and the PRs are all still + # correct. + - name: Check for bot token + env: + GHTOKEN: ${{ secrets.GHTOKEN }} + run: | + set -euo pipefail + + if [[ -z "$GHTOKEN" ]]; then + echo "::warning::GHTOKEN is not set. The release itself will proceed, but checks on any PR this workflow opens will sit in 'action_required' until approved by hand. See README.adoc, 'Recommended repository secret'." + fi + + - name: Compute and tag next version + id: version + env: + EVENT_NAME: ${{ github.event_name }} + TARGET_PHASE: ${{ github.event.inputs.target_phase }} + RELEASE_VERSION: ${{ github.event.inputs.release_version }} + ALLOW_NON_MONOTONIC: ${{ github.event.inputs.allow_non_monotonic }} + REF_NAME: ${{ github.ref_name }} + run: | + set -euo pipefail + + git fetch --tags --force + + # Highest existing tag by DECIMAL order. Never use `git ... --sort` or + # `sort -V` here: they order the fractional part component-wise and rank + # v0.8 below v0.61. release-info.sh compares by centi-value. + latest="$(./scripts/release-info.sh latest)" + + # Auto-increment (`next`) refuses at a milestone gate with rc=10: the + # 0.01 band before the next manual milestone is exhausted. That is not + # an error -- it means "a maintainer must cut the milestone" -- so we + # emit a notice and finish without tagging. Sets the global `next`; + # returns non-zero (band exhausted) to signal "stop, no tag". Kept out + # of $(...) so the ::notice:: reaches the workflow log. + compute_next() { + local base="$1" out rc + # Capture rc from the substitution itself (a bare `if ...; fi` with no + # else would leave $? as the if-statement's 0, masking rc=10). + out="$(./scripts/release-info.sh next "$base" 2>/tmp/next.err)" && rc=0 || rc=$? + if [[ "$rc" -eq 0 ]]; then + next="$out" + return 0 + fi + if [[ "$rc" -eq 10 ]]; then + echo "::notice::$(cat /tmp/next.err) No tag created." + return 1 + fi + cat /tmp/next.err >&2 + exit "$rc" + } + + if [[ "$EVENT_NAME" == "workflow_dispatch" ]]; then + if [[ "$REF_NAME" != "main" ]]; then + echo "Manual version jump must run against main." >&2 + exit 1 + fi + + if [[ -n "${RELEASE_VERSION:-}" ]]; then + next="$(./scripts/release-info.sh normalize "$RELEASE_VERSION")" + elif [[ -n "${TARGET_PHASE:-}" && "$TARGET_PHASE" != "auto-next" ]]; then + next="$(./scripts/release-info.sh phase-floor-version "$TARGET_PHASE")" + else + compute_next "$latest" || exit 0 + fi + else + existing="$(git tag --points-at HEAD --list 'v*' | head -n1 || true)" + if [[ -n "$existing" ]]; then + next="$(./scripts/release-info.sh normalize "$existing")" + else + compute_next "$latest" || exit 0 + fi + fi + + if [[ "${ALLOW_NON_MONOTONIC:-false}" != "true" ]]; then + if [[ "$(./scripts/release-info.sh compare "$next" "$latest")" == "-1" ]]; then + echo "Refusing non-monotonic jump from $latest to $next. Set allow_non_monotonic=true to override." >&2 + exit 1 + fi + fi + + if git rev-parse -q --verify "refs/tags/$next" >/dev/null; then + tag_sha="$(git rev-list -n1 "$next")" + head_sha="$(git rev-parse HEAD)" + if [[ "$tag_sha" != "$head_sha" ]]; then + echo "Tag $next already exists on another commit ($tag_sha)." >&2 + exit 1 + fi + echo "Tag $next already exists on HEAD; reusing." + else + git config user.name "riscv-gitbot" + git config user.email "actions@users.noreply.github.com" + git tag -a "$next" -m "Automated version tag for ${GITHUB_SHA}" + git push origin "$next" + fi + + prev_phase="$(./scripts/release-info.sh phase "$latest")" + next_phase="$(./scripts/release-info.sh phase "$next")" + + milestone_transition=false + if [[ "$prev_phase" != "$next_phase" ]]; then + milestone_transition=true + fi + + echo "previous_version=$latest" >> "$GITHUB_OUTPUT" + echo "next_version=$next" >> "$GITHUB_OUTPUT" + echo "previous_phase=$prev_phase" >> "$GITHUB_OUTPUT" + echo "next_phase=$next_phase" >> "$GITHUB_OUTPUT" + echo "milestone_transition=$milestone_transition" >> "$GITHUB_OUTPUT" + echo "mode=$(./scripts/release-info.sh mode)" >> "$GITHUB_OUTPUT" + + # Build and release the PDF for the tag just cut, then publish the Antora site + # for it. These run as workflow_call rather than waiting on the tag push, which + # is inert by design (see the checkout comment above) -- so the release chain + # completes on the built-in GITHUB_TOKEN, with no PAT to provision in every + # repository seeded from this template. + # + # `if` guards the milestone-gate case: compute_next exits the job without + # setting next_version when the 0.01 band is exhausted, and there is then no + # tag to build. + release-pdf: + needs: tag-version + if: needs.tag-version.outputs.next_version != '' + uses: ./.github/workflows/build-pdf.yml + permissions: + contents: write + pull-requests: write + secrets: inherit + with: + release_version: ${{ needs.tag-version.outputs.next_version }} + draft: ${{ inputs.draft }} + + # Sequenced after release-pdf so a failed build does not publish a site for a + # version that has no PDF. + publish-site: + needs: [tag-version, release-pdf] + if: needs.tag-version.outputs.next_version != '' + uses: ./.github/workflows/publish-site.yml + permissions: + contents: read + pages: write + id-token: write + secrets: inherit + with: + release_version: ${{ needs.tag-version.outputs.next_version }} + + milestone-pr: + runs-on: ubuntu-latest + needs: tag-version + # SPEC_STATE.md is a ratification-layer artifact (milestone/phase labels), + # meaningless for a doc-mode repo, which has no milestones. See + # scripts/release-info.sh's .docmode / "mode" output above. + if: needs.tag-version.outputs.milestone_transition == 'true' && needs.tag-version.outputs.mode == 'spec' + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + with: + fetch-depth: 0 + # See build-pdf.yml: create-pull-request adds its own auth header, so a + # persisted token here yields a duplicate Authorization header (400). + persist-credentials: false + + - name: Update specification state + run: | + set -euo pipefail + ./scripts/update-spec-state.sh "${{ needs.tag-version.outputs.next_version }}" "$(date +%Y-%m-%d)" + + - name: Create milestone review PR + uses: peter-evans/create-pull-request@v6 + with: + token: ${{ secrets.GHTOKEN || secrets.GITHUB_TOKEN }} + commit-message: 'docs: milestone transition to ${{ needs.tag-version.outputs.next_phase }}' + branch: gitbot/milestone-${{ needs.tag-version.outputs.next_version }} + delete-branch: true + title: 'Milestone reached: ${{ needs.tag-version.outputs.next_phase }} (${{ needs.tag-version.outputs.next_version }})' + body: | + This PR was opened automatically after a version milestone transition. + + Previous version/state: `${{ needs.tag-version.outputs.previous_version }}` / `${{ needs.tag-version.outputs.previous_phase }}` + New version/state: `${{ needs.tag-version.outputs.next_version }}` / `${{ needs.tag-version.outputs.next_phase }}` + + Please review and apply any governance/process updates required for this state transition. + + Suggested maintainer checklist: + - Confirm the state transition is intentional. + - Confirm change-control policy for the new state is enforced. + - Confirm communication to contributors and implementers. + labels: | + milestone + spec-state + add-paths: | + SPEC_STATE.md diff --git a/.gitmodules b/.gitmodules index 78a8f9c..cde1eb5 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,3 +1,3 @@ [submodule "docs-resources"] path = docs-resources - url = https://github.com/riscv/docs-resources + url = https://github.com/riscv/docs-resources.git diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index a5b74b9..37b1a9a 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -7,10 +7,13 @@ repos: - id: check-symlinks - id: check-yaml - id: end-of-file-fixer - exclude: \.svg$ + exclude: \.patch$ - id: trailing-whitespace args: [--markdown-linebreak-ext=md] - exclude: \.svg$ + # Patch files are byte-sensitive: context lines carry a leading space and + # blank context lines are a single space. Reformatting them can corrupt + # the patch. + exclude: \.patch$ - repo: local hooks: @@ -26,3 +29,8 @@ repos: hooks: - id: yamlfmt args: [--mapping, '2', --sequence, '4', --offset, '2'] + # antora.yml is rewritten at release time by scripts/stamp-antora-version.sh, + # which replaces values line by line. yamlfmt line-wraps long plain scalars + # (e.g. page-phase-notice), which splits a value across two lines and makes + # the stamp emit invalid YAML. Leave this file alone. + exclude: ^antora\.yml$ diff --git a/.template-version b/.template-version new file mode 100644 index 0000000..1493d10 --- /dev/null +++ b/.template-version @@ -0,0 +1,12 @@ +# Records which docs-spec-template release this repository's tooling came from. +# Nothing reads this file. You update it manually at the end of every upgrade, +# in UPGRADING.md step 2.8. This is not a document version -- those are the +# v* git tags created by the version-bot.yml workflow. +# +# Baseline is template main rather than a release tag: doc mode (the posture +# this repo runs in, see .docmode) landed in template PR #150 and is not in any +# tagged release yet. The template's tag namespace also mixes real releases with +# version-bot test tags, so the SHA is the authoritative record. +template_ref: main +template_sha: 74566374080a11fd7abe482b1246a87a12158f8b +upgraded_on: 2026-09-09 diff --git a/ANTORA.md b/ANTORA.md new file mode 100644 index 0000000..b24fa29 --- /dev/null +++ b/ANTORA.md @@ -0,0 +1,450 @@ +# Antora Migration Guide + +How `docs-spec-template` produces **both** the ARC-compliant submission PDF and +the Antora static-site HTML from a **single source tree**, and the phased plan +to finish that migration. + +## TL;DR for authors + +- Chapter content lives once, in `modules/ROOT/pages/*.adoc`, as standalone + Antora pages (each starts with a level-0 `= Title`). +- `src/spec-sample.adoc` is a thin **PDF assembler**: it includes those pages + with `leveloffset=+1` so their level-0 titles become chapters in the PDF, + reproducing the historical section numbering. +- Build the PDF: `make` → `build/-v-.pdf`. +- Build the site locally: `antora antora-playbook.yml` → `build/site/`. + +## The dual-source technique (why it works) + +The PDF wants one master document; Antora wants one file per navigable page. +`leveloffset=+1` reconciles them: + +``` +modules/ROOT/pages/intro.adoc src/spec-sample.adoc (PDF assembler) +----------------------------- ----------------------------------- += Introduction (level 0) -> include::...intro.adoc[leveloffset=+1] +== Sub Section (level 1) => renders as "== Introduction" (Ch.1) + and "=== Sub Section" (1.1) +``` + +So the pages are the single content source; the assembler is PDF-only glue. +PDF-only constructs stay in the assembler and never appear in a page: +- `include::../docs-resources/global-config.adoc[]` (cross-repo relative + include — would break under Antora, so it lives only here) +- the back-of-book `[index] == Index` macro (Antora generates no index) +- `:title-logo-image:`, `:pdf-theme:`, `:pdf-fontsdir:`, `:doctype: book`, etc. + +## Repository layout + +``` +antora.yml # component descriptor (keep MINIMAL — see below) +antora-playbook.yml # LOCAL preview playbook (mirrors production UI) +modules/ROOT/ + nav.adoc # site navigation + pages/ # single source of chapter content (Antora pages) + index.adoc # site landing page (Antora start_page; NOT in PDF) + intro.adoc chapter2.adoc contributors.adoc bibliography.adoc + examples/example.bib # bibliography database +src/spec-sample.adoc # PDF assembler (Makefile DOCS target) +Makefile # asciidoctor-pdf/html via Docker; ARC PDF naming +docs-resources/ # submodule: PDF fonts/themes/logo + global-config +``` + +## Modes: spec vs. doc + +Everything above (the PDF, the site, the dual-source technique) is common to +every repo built from this template. What's *not* common is ratification: +this template also assumes, by default, that every consumer is a +specification headed for ARC ratification, and bundles that assumption in as +a "ratification layer" — the Document State preface, the phase/milestone +attributes, `SPEC_STATE.md` tracking. + +A repo that is documentation rather than a spec (e.g. `riscv/docs-dev-guide`) +needs the Antora-ready layer above but not the ratification layer. It opts +out by committing a `.docmode` file at the repo root containing `doc` (absent, +empty, or any other value ⇒ `spec`, today's behavior, byte-for-byte). Version +identity — semver git tags + build-date stamping — is identical in both modes; +only the phase/milestone surface changes: + +- `scripts/release-info.sh mode` resolves `.docmode` and is the single source + of truth every other consumer reads it from (never re-parse the file + yourself). In `doc` mode, `phase`, `phase-floor-version`, `display`, + `milestone`, `notice`, and `revremark` all resolve to `""` — `version` and + the rest of the version-arithmetic commands are unaffected. +- The Makefile passes `-a doc-mode='doc'` (instead of the phase/milestone `-a` + flags) when in doc mode, which `src/spec-sample.adoc` and + `modules/ROOT/pages/index.adoc` key their `ifdef::doc-mode[]` / + `ifndef::doc-mode[]` guards on to drop the Document State preface, the + title-page revremark, and the cover-page phase banner entirely. +- `scripts/stamp-antora-version.sh` requires only `version`/`revnumber`/ + `revdate` in `antora.yml` in doc mode (not `display`/`notice`/`phase`) — see + "Version stamping" below. +- `.github/workflows/version-bot.yml`'s `milestone-pr` job (which regenerates + `SPEC_STATE.md`) only runs in spec mode. + +See `MIGRATION.md`'s "Doc mode" section for how to adopt this in a derived +repo. + +## Production model (important) + +The canonical site is built **elsewhere**, by the central playbook at +`github.com/riscv-admin/antora.riscv.org` (dev mirror: +`github.com/riscv-admin/antora-dev.riscv.org`). This repo is just a **content +source** consumed by that playbook. + +The central playbook supplies, uniformly to every spec: +- **Extensions**: `asciidoctor-kroki` (diagrams, incl. wavedrom), + `@djencks/asciidoctor-mathjax` (math), an ASAM extension for + `cite:`/`bibliography::[]`, plus section/nav numbering extensions. +- **Shared AsciiDoc attributes**: `doctype: book`, `icons: font`, `xrefstyle`, + `source-highlighter: highlight.js`, kroki config, math entities, etc. +- **UI**: `github.com/riscv-admin/riscv-antora-only-ui` release bundle. + +Consequence — **keep `antora.yml` minimal** (name/title/version/nav). Component +attributes override the playbook, so setting rendering attributes here (notably +`sectnums`, which the central section-numbering extension controls) desyncs this +spec from the rest of the library. Component *identity/grouping* keys are fine; +rendering attributes are not. Put preview-only rendering config in +`antora-playbook.yml` instead. + +### Registering this spec on the dev site + +Add to `content.sources:` in the dev-site `antora-playbook.yml` (push the branch +first — Antora fetches from GitHub, not the worktree): + +```yaml + # docs-spec-template example spec (dev preview) — component: spec-sample + - url: https://github.com/riscv/docs-spec-template.git + branches: antora-setup + start_page: ROOT::index.adoc + start_path: / +``` + +No `nav:` key needed (declared in `antora.yml`). Renders at `/spec-sample/`. + +## Publishing to GitHub Pages + +Separate from the central site, this repo also publishes a **standalone copy** of +the same content to its own GitHub Pages site, next to the release PDF. For a +repo seeded from this template that is not part of the RISC-V central library, +this is the primary HTML output; for specs that *are* consumed centrally, +`docs.riscv.org` stays canonical and this is a convenience. + +- Workflow: `.github/workflows/publish-site.yml` — runs on `v*` tag pushes and on + manual dispatch, and deploys via `actions/deploy-pages`. +- Build: `scripts/build-pages-site.sh` — the whole build, runnable locally. +- Setup in a seeded repo: **one manual step**, best done before the first push + to `main` — the workflow runs on every push to `main`, not only on tags, so + until Pages exists each push leaves a failed run behind. + `actions/configure-pages` is configured with `enablement: true` and will turn + Pages on by itself wherever it is allowed to, but the workflow's `GITHUB_TOKEN` + is refused with `Resource not accessible by integration`: creating a Pages site + is an admin-level operation that the Actions app cannot perform on a repo that + has none, regardless of the `pages: write` grant the job holds. This is a + limitation of the Actions token, **not** an organization policy — `riscv` has + `members_can_create_pages: true`, so there is no org setting to change and no + point hunting for one. A repository admin must set *Settings → Pages → Source* + to "GitHub Actions" once. Equivalent API call, with an admin token: + `gh api -X POST repos///pages -f build_type=workflow`. Once the + site exists, `enablement: true` is a no-op and releases publish unattended. + The job skips itself on private repos, where Pages needs Team/Enterprise. +- Set that source **directly** to "GitHub Actions". If a repo is ever left on + "Deploy from a branch" first, GitHub queues a run of its built-in + `pages-build-deployment` workflow that survives the switch, and it can deploy + *after* the first Actions deployment — overwriting the site with the repository + root, which has no `index.html`. The symptom is a live 404 from a + `publish-site.yml` run that reported success; check the `github-pages` + deployment list for a `pages-build-deployment` entry newer than yours. Re-run + `publish-site.yml` once the stray build has finished; it does not recur. + +### How the playbook is derived + +The Pages build does **not** get its own committed playbook. `antora-playbook.yml` +carries a hand-maintained mirror of the central playbook's rendering attributes, +and a second copy of that block would silently drift. Instead +`scripts/gen-pages-playbook.js` loads it and overrides four things into a +generated, gitignored `antora-pages-playbook.yml`: + +| Key | Why | +| --- | --- | +| `site.url` | Project Pages sites live at `https://.github.io//`, not at a domain root; Antora needs the base path for the sitemap and canonical links. | +| `content.sources` | Which versions to publish (below). | +| `output.dir` | `build/pages-site`, so a publish never clobbers the `build/site` preview output. | +| `cover-logo` | Resolves the cover logo locally instead of from `common::`. | + +### The cover logo + +`index.adoc` renders the logo through the `cover-logo` attribute rather than a +literal resource ID. It defaults in `antora.yml` to `common::risc-v_logo.svg`, +the shared asset the central build uses. A single-repo build has no `common` +component, so the Pages build stages the copy from the `docs-resources` submodule +into `modules/ROOT/images/` (gitignored) and hard-sets the attribute to it. The +central build is unaffected, and the local-preview behaviour is unchanged — a +bare `npm run preview` still logs the one expected unresolved `common::` image. + +`validate-content-source.yml` stages the logo the same way, for a different +reason: it publishes its render as a PR preview artifact, and `index.adoc` is the +start page, so an unresolved cover would be the first thing a reviewer sees. A +side effect is that the gate now actually *validates* the image macro, which it +could not while it was merely exception-listing the error. The exception stays, +but only covers the degraded path where the submodule is unavailable — there it +keeps a missing submodule a warning rather than a red gate. + +### Versions, and why only one is published today + +### Untagged builds publish as `dev` + +A build with no release tag to name it — the first manual publish in a freshly +seeded repo, or any `workflow_dispatch` before the first tag — gets +`release-info.sh`'s dev version, `vX.YY--`. That string is published +under a short fixed label (`dev`, override with `DEV_VERSION_LABEL`) instead of +verbatim, for two reasons: + +1. **It breaks the navigation.** The RISC-V UI floats the version selector + beside the nav tree (`.version-box{float:right}` plus + `.nav-version-group{overflow:hidden}`, which establishes a block formatting + context), so the nav only gets the width left over next to the selector, and + the selector is as wide as its longest version string. At 22 characters the + nav collapses to about three characters per line. Release tags are short + (`vX.Y`) and render correctly, so this never affects a real release. +2. **It churns URLs.** Every dispatch would otherwise mint a + `/spec-sample//` path that the next publish orphans, since each deploy + replaces the whole site. `/spec-sample/dev/` stays linkable. + +The full dev version is not lost: it is still stamped into `page-revnumber` and +shown on the cover page, so a published dev site names the exact commit it came +from. + +### Version stamping + +The version stamp is applied to the **working tree** at build time from +`scripts/release-info.sh` — the same source the PDF uses — so the published +version matches the PDF even though the tagged commit's committed `antora.yml` +has not been stamped yet. Nothing is committed back; getting the stamp onto +`main` stays `build-pdf.yml`'s job, which opens a review PR for it. + +`scripts/stamp-antora-version.sh` asserts each key it stamps was found in +`antora.yml` exactly once — see "Modes" above. In spec mode that's all six of +`version`/`page-revnumber`/`page-revdate`/`page-phase-display`/ +`page-phase-notice`/`page-phase`; in doc mode, whose `antora.yml` carries no +`page-phase*` keys, it's only the first three. + +The build also scans release tags and publishes any that carry a correctly +stamped descriptor, which is what would give the site a multi-version dropdown. A +tag qualifies only if it contains an `antora.yml` whose `version:` equals the tag +itself. That is deliberately strict, for two reasons: + +1. Antora **aborts the entire build** on any ref that has no `antora.yml`, so + tags predating the Antora layout must be filtered out. +2. A tag stamped with a different version would publish under a wrong or + duplicate version path. + +**Today no tag qualifies**, so the site publishes exactly one version. The reason +is ordering: a release tag is pushed *first*, and `build-pdf.yml` stamps +`antora.yml` on `main` *afterwards* — so a tag never contains its own version. +If the release flow is ever changed to stamp and commit **before** cutting the +tag, past releases begin appearing in the version dropdown automatically, with no +change to this script. + +## Section numbering + +Chapter/section numbering is applied by the **central playbook**, not this repo. +The `nav_numbering` and `section_numbering` extensions read a per-component rule +from the playbook's `numbering_rules` anchor. That is *why* `antora.yml` sets no +`sectnums` (design rule under "Production model"): the central extension owns it, +and a local override would desync this spec. A generic template can't ship a rule +— chapter counts differ per spec — but the bundled example spec can. + +The important subtlety: a rule's `chapters: {start, end}` are **line numbers in +`modules/ROOT/nav.adoc`**, not chapter numbers. The extension scans nav lines; +each line in `[start, end]` matching `^*+ xref:…[` becomes a chapter, numbered +sequentially from 1. For this example spec the nav xrefs currently sit at: + +| `nav.adoc` line | page | numbering | +|---|---|---| +| 12 | `index.adoc` | landing/cover — not a chapter | +| 13 | `intro.adoc` | **Chapter 1** | +| 14 | `chapter2.adoc` | **Chapter 2** | +| 15 | `contributors.adoc` | unnumbered (front matter) | +| 16 | `bibliography.adoc` | unnumbered (`[bibliography]`) | + +so the entry to add to `numbering_rules` in the central playbook +(`riscv-admin/antora-dev.riscv.org`, `antora/antora-playbook.yml`) is: + +```yaml +- component: spec-sample + module: ROOT + branches: ['antora-setup'] # match the content-source branch/tag + chapters: {start: 13, end: 14} # nav lines 13–14 = intro, chapter2 +``` + +One entry covers both extensions (they share the `&numbering_rules` anchor). +`section_numbering` ignores `appendices`, so the bibliography stays unnumbered, +matching the PDF (where contributors is a `[preface]` and the bibliography is +unnumbered back matter). + +> ⚠ **This rule is line-coupled to `nav.adoc`.** Adding, removing, or reordering +> entries — or editing the nav header comments — shifts the line numbers, so the +> central rule must be updated in lockstep or site numbering silently drifts. +> (Adding the warning comment to `nav.adoc` already moved these from lines 7–8 to +> 13–14.) Also update `branches:`/`tags:` to match wherever the spec is consumed +> (dev = `antora-setup`; later `main` or release tags). + +## Phased plan + +- [x] **Phase 1 — Dual-source layout.** Chapters → `modules/ROOT/pages/`; + `src/spec-sample.adoc` → assembler with `leveloffset=+1`; `antora.yml` + minimal; nav/start_page repointed; UI bundle fixed to the RISC-V production + bundle. Verified: `make` → 14pp ARC PDF; `antora antora-playbook.yml` → 5 + pages + landing. (commit `19211be`) +- [x] **Phase 2 — docs-resources cross-repo include.** Verified the boundary + holds: the `include::../docs-resources/global-config.adoc[]` appears only in + `src/spec-sample.adoc`; zero `docs-resources` references under `modules/`. + `global-config.adoc` defines three attributes — `company`, `url-riscv`, + `doctype: book`. Decision: `company`/`url-riscv` are unreferenced in content, + so they do **not** need to reach the site; `doctype: book` is already supplied + uniformly by the central playbook, so it must **not** be duplicated in + `antora.yml` (would override the playbook — see design rule above). Local + `antora antora-playbook.yml` build passes (exit 0, 5 pages) with no unresolved + includes or cross-repo artifacts in the rendered HTML. ++ + **Dev-site fixes (found on antora-dev.riscv.org after publishing):** two + site-rendering bugs the local build could not surface (both need the central + extensions/assets): + * *Bibliography rendered raw.* `cite:`/`bibliography::[]` came through as + literal text. The central ASAM bibliography extension no-ops unless the + component declares `asamBibliography: ''` in `antora.yml`. + Fix: moved the bib to the canonical `modules/ROOT/resources/riscv-spec.bib` + (was `modules/ROOT/examples/example.bib`; `examples/` is a reserved Antora + family and no real spec puts a bib there), set + `asamBibliography: 'ROOT:resources/riscv-spec.bib'` in `antora.yml`, and + repointed the PDF assembler's `:bibtex-file:` to the new path. `make` PDF + build re-verified green. + * *No cover page.* The landing page was placeholder prose. Rebuilt + `modules/ROOT/pages/index.adoc` on the ratified-spec cover pattern + (`image::common::risc-v_logo.svg` + title heading + version line + phase + banner linking to riscv.org/spec-state). The `common::` logo resolves on the + site build; it warns in bare local preview (no `common` component) until + Phase 5. +- [x] **Phase 3 — Attribute split.** Added an `asciidoc.attributes` block to the + LOCAL `antora-playbook.yml`, mirroring the central playbook's shared rendering + attributes (doctype, icons, xrefstyle, source-highlighter, toclevels, math/char + entities, kroki config, image-path helpers) so `antora antora-playbook.yml` + preview renders like production. Deliberately omitted: ISA-manual-specific + content shortcuts (`csrname`, `i`/`u`/`ra`/`reg_list`), UI PDF-button plumbing + (`pdf_url`, `multiple_pdfs`, `pdf_list`), and section-numbering keys (`sectnums` + etc. — commented out in central because the section-numbering *extension* + controls them, per rule #2). PDF-only attributes stay in the assembler/Makefile; + `antora.yml` stays minimal (only `page-group` + the spec-specific + `asamBibliography`). Local build passes (exit 0, 5 pages). The extensions these + attributes configure (kroki, mathjax, ASAM bib) land in Phase 5, so the + attributes are inert in bare local preview until then. +- [x] **Phase 4 — Version bridge.** The ARC Author Guide requires the artifact's + identity to be the `vX.Y` release tag; the HTML site version must match the + PDF version exactly. Because Antora reads a STATIC `version:` from the committed + `antora.yml` (the central playbook just fetches the branch — no build step runs + `release-info.sh` on the spec side), the bridge *stamps* that file, the Antora + analogue of how `scripts/update-spec-state.sh` stamps `SPEC_STATE.md`: + * `scripts/stamp-antora-version.sh [version] [date]` writes `version:` and the + cover attributes (`page-revnumber`, `page-revdate`, `page-phase`, + `page-phase-display`, `page-phase-notice`) from `release-info.sh` — the same + source the PDF uses. Idempotent; preserves comments/nav. `make stamp-antora` + wraps it with the Makefile's `VERSION`/`DATE`, so site and PDF cannot diverge. + * `antora.yml` `version:` is now the exact tag (site path `/spec-sample//`, + a distinct citable folder per release — matching ARC's citability goal). + * `index.adoc` cover renders the PDF title-page revision line verbatim: + `Version {page-revnumber}, {page-revdate}: {page-phase-display}`, plus a phase + banner mirroring the PDF "Document State" preface. No hardcoded version. + * Verified: stamping `v0.8` yields `/spec-sample/v0.8/` with cover + "Version v0.8, 2026-06-12: Stabilized"; restamped to the repo's real state. + * **Release step:** automated in `build-pdf.yml` (see Phase 6) — the same run + that builds the PDF stamps the matching version and opens a review PR against + `main`; merging it is part of cutting a release. Manual fallback for + local/offline releases: `make stamp-antora VERSION=vX.Y` then commit + `antora.yml`. + * **Hardening:** the stamp script asserts that each key was substituted exactly + once and that the result still parses as YAML, then fails loudly. It + previously exited 0 having silently produced invalid YAML when a formatter + line-wrapped `page-phase-notice` into a plain multi-line scalar (caught in + review of #105); it now also drops such wrapped continuations. `antora.yml` + is excluded from yamlfmt (#106) so the wrap cannot return. +- [x] **Phase 5 — Extensions + preview.** Wired the central playbook's *asciidoc* + extensions into the LOCAL preview so `antora antora-playbook.yml` renders like + production: + * `package.json` pins Antora + `asciidoctor-kroki` + `@djencks/asciidoctor-mathjax` + as devDependencies (reproducible preview, not a global `antora`). **Version + note:** `asciidoctor-kroki` must be `0.18.1` (what the central playbook uses); + `1.0.0` needs a newer Asciidoctor.js than Antora 3.1.x bundles and dies with + `block.$!= is not a function`. + * `antora-playbook.yml` gained an `asciidoc.extensions` list (kroki + mathjax). + * `docker-compose.yml` runs a local Kroki on `localhost:9870` (matching the + `kroki-server-url` attribute), so wavedrom renders without shipping source to + a public service. Preview flow: `npm install` → `docker compose up -d kroki` + → `npm run preview`. + * Added a small `latexmath` demo to `chapter2.adoc` (the example spec now + demonstrates math like it already did wavedrom/citations). + * Verified: wavedrom → inline `` (kroki), `latexmath` → MathJax SVG + (`data-mml-node` markers), and `make` PDF still builds clean with the new math. + * **Bibliography deliberately NOT mirrored locally.** The central `cite:` / + `bibliography::[]` support is a custom, multi-file ASAM extension vendored in + the central playbook (not on npm); replicating it here would be heavy and a + divergence risk. Citations render on the central site (ASAM) and in the `make` + PDF/HTML (asciidoctor-bibtex gem), so authors have two ways to check them + without it living in this repo. Revisit only if local ASAM parity is needed. +- [x] **Phase 6 — CI (content-source validation).** This repo is ONE content + source; the real site is built centrally from 20+ sources, so there is no site + to build here. Added `.github/workflows/validate-content-source.yml`: on PR / + push-to-main / dispatch it builds THIS component in isolation via + `antora-playbook.yml` (npm ci → Kroki service on :9870 → `npx antora`), as a + gate so broken AsciiDoc / unresolved intra-component xrefs/includes / extension + errors fail here instead of stalling the shared central build. It does NOT + deploy. Cross-component refs (xrefs into other specs) resolve only centrally, + so they are exception-listed. Verified: clean tree passes; an injected broken + xref fails the gate. + * **PR preview artifact.** The gate's render is uploaded as *Rendered site + (Antora)* (~4 MB, 14-day retention) instead of being discarded, so a reviewer + can see a content change rendered rather than reading the AsciiDoc diff — the + build was already happening on every PR, so this costs no build time. The + cover logo is staged first (see "The cover logo") so the start page is not + broken. Both this job and `build-pdf.yml` write a direct artifact link into + `$GITHUB_STEP_SUMMARY` via `upload-artifact`'s `artifact-url` output; that + needs no permissions and works on fork PRs, where the `pull_request` token is + read-only. It is a HEAD-only preview, not a deployment: a real per-PR preview + host was rejected because the RISC-V org refuses Pages site creation to the + workflow token (see `publish-site.yml`) and fork PRs cannot hold deploy + secrets. Artifact download still requires a signed-in GitHub account. + * **Version-stamp automation (Phase 4 lockstep, now wired).** `build-pdf.yml` + resolves `VERSION`/`DATE` once (shared by the PDF build and the stamp, so no + date drift) and a `stamp-site-version` job stamps the matching `antora.yml` + and opens a review PR against `main`. It runs only for real releases (skips + PR previews and drafts) and is monotonic (never stamps `main` backwards when + an older tag is rebuilt). Chosen over `version-bot.yml` because + `build-pdf.yml` is the run the author actually triggers to cut a release *and* + the run that builds the PDF, so both come from the same source/version/run by + construction. + * **Why a PR, not a direct push** (changed in review of #105): the original job + ran `git push origin HEAD:main`, assuming a seeded repo's `main` is + unprotected. That is a bad bet for a file copied into 20+ spec repos whose + settings we do not control — and a rejected push fails *after* the Release is + published, leaving a green release with a silently stale site. A PR works + under any protection setting and matches the milestone-PR idiom already in + `version-bot.yml`. Trade-off: lockstep is no longer merge-free — an unmerged + stamp PR means the site lags, so treat it as part of the release checklist. + +## Build commands + +```bash +make # ARC PDF (+ HTML) via Docker; VERSION/DATE overridable +make stamp-antora VERSION=vX.Y # stamp antora.yml to match the PDF release (commit the result) + +# Local Antora preview (renders like production: diagrams + math): +npm install # one-time: Antora + kroki/mathjax extensions +docker compose up -d kroki # local Kroki server on :9870 (for wavedrom/diagrams) +npm run preview # antora --fetch antora-playbook.yml -> build/site/ +docker compose down # stop Kroki when done + +# GitHub Pages site, exactly as CI builds it (needs Kroki up, as above). +# NOTE: this stamps antora.yml in your worktree -- pass NO_STAMP=1 to leave it be. +NO_STAMP=1 ./scripts/build-pages-site.sh # -> build/pages-site/ +./scripts/build-pages-site.sh https://org.github.io/repo # with the real base URL +``` diff --git a/ARC_SUBMISSION.md b/ARC_SUBMISSION.md new file mode 100644 index 0000000..e6fdfd7 --- /dev/null +++ b/ARC_SUBMISSION.md @@ -0,0 +1,226 @@ +# ARC Submission Requirements for ISA and non-ISA Specifications + +**Audience:** Maintainers, editors, and chairs of any specification repository +under `github.com/riscv/*` (ISA) and `github.com/riscv-non-isa/*` (non-ISA) +that submits work to the Architecture Review Committee (ARC) for review. + +**Effective:** Immediately. ARC will **reject submissions that do not meet +these requirements**. + +--- + +## 0. Notice from ARC (verbatim) + +> ARC has been having issues with submitted specifications not being in the +> form of a clearly versioned file that has been released in github with a +> unique and monotonic version tag. Future review requests of specifications +> will be rejected unless the specifications are of the following form: +> +> * Link to a tag on github, which contains the compiled pdf. +> +> * The compiled pdf file name has to contain a version string matching the +> tag and also a date stamp (e.g, `Zifoo-v0.8-20260520.pdf`). +> +> * The title page of the document must match the above information. + +The rest of this document operationalizes that notice — specifying the +exact tag/filename/title-page conventions, the version→milestone mapping +that goes on the title page, and how the `docs-spec-template` toolchain +produces compliant artifacts automatically. + +--- + +## 1. Why this exists + +ARC has been receiving spec submissions in inconsistent forms — uncompiled +sources, draft PDFs without identifiers, or files whose name/title/version do +not match. This makes review unreproducible: ARC cannot be sure two reviewers +are looking at the same artifact, and the reviewed artifact cannot be cited +later. The rules below make every reviewable artifact uniquely identifiable. + +## 2. What ARC requires + +Every specification submitted to ARC for review MUST be presented as: + +1. **A link to a Git tag on GitHub** in the spec's canonical repository + (`riscv/` or `riscv-non-isa/`). The tag MUST point at the exact + commit the PDF was built from. + +2. **The compiled PDF MUST be published with the tag** — either as an asset on + a GitHub Release attached to that tag, or committed at the tagged commit. + A bare tag with no PDF is not a valid submission. + +3. **The PDF filename MUST contain**: + - the spec short name, + - the version string matching the tag, and + - a date stamp in `YYYYMMDD` form. + + Pattern: `-v.-.pdf` + +4. **The PDF title page MUST display the same information** — spec name, + version string, milestone ID, and date — and they MUST match the filename + and the tag. + +## 3. Conventions + +### 3.1 Version → Milestone mapping + +Tags use **`vMAJOR.MINOR`** form, where the version is a fixed-point decimal to +hundredths: `v0.6` = 0.60, `v0.72` = 0.72, `v1.0` = 1.00. Ordering is therefore +**decimal, not semver** — `v0.8` (0.80) is greater than `v0.61`. The milestone +gates are the reserved values below; between them, merges to `main` +auto-advance the revision by `0.01` (`v0.61`, `v0.62`, …). The milestone ID is +the single canonical label that identifies what state the document is in. + +| Milestone ID | Tag at milestone gate | Revisions within phase | +| ----------------------- | --------------------- | -------------------------- | +| `development-complete` | `v0.6` | `v0.61`, `v0.62`, … `v0.79` | +| `stabilized` | `v0.8` | `v0.81`, `v0.82`, … `v0.89` | +| `frozen` | `v0.9` | `v0.91`, `v0.92`, … `v0.98` | +| `ratification-ready` | `v0.99` | (no revisions; single tag) | +| `ratified` | `v1.0` | (no revisions; ratified) | + +Milestone gates are cut **manually** by a maintainer; the `v0.NN` revisions +between them are created automatically on merge to `main`. `v0.0` is the +inception version. + +**Tags MUST be monotonically increasing** (by decimal value). Never delete or +rewrite a published tag — create a new one. + +A revision tag carries the milestone label of the **most recent gate +passed**. Example: `v0.83` is still labeled `stabilized` until the spec is +re-tagged at `v0.9` (`frozen`). + +### 3.2 PDF filename + +``` +-v.-.pdf +``` + +Worked examples — one per milestone: + +| Filename | Milestone | +| --------------------------------------- | -------------------- | +| `Zifoo-v0.6-20260520.pdf` | development-complete | +| `Zifoo-v0.8-20260612.pdf` | stabilized | +| `RHTI-v0.9-20260408.pdf` | frozen | +| `Server-Platform-v0.99-20260415.pdf` | ratification-ready | +| `Zifoo-v1.0-20260901.pdf` | ratified | + +Use the **short name** registered for the spec, not the long title. Use +hyphens, not spaces. + +### 3.3 Title page + +The first page of the PDF MUST show, prominently: + +- Spec long title and short name +- Version string matching the tag and filename (e.g. `v0.8`) +- Date in human-readable form matching the `YYYYMMDD` in the filename +- **Milestone label**, the title-case display form of one of the canonical + milestone IDs: + + | Canonical ID (filenames, scripts) | Display label (title page) | + | --------------------------------- | -------------------------- | + | `draft-and-development` | Draft and Development | + | `development-complete` | Development Complete | + | `stabilized` | Stabilized | + | `frozen` | Frozen | + | `ratification-ready` | Ratification-Ready | + | `ratified` | Ratified | + +The canonical ID is what scripts, filenames, and CI consumers reference. +The display label is what humans see on the title page. + +Repos derived from `docs-spec-template` get this layout automatically: + +- **Page 1 (title page):** title, authors, and a single revision line of the + form `Version vX.Y, YYYY-MM-DD: ` — for + example `Version v1.0, 2026-05-24: Ratified`. This is asciidoctor-pdf's + default rendering of `revnumber, revdate: revremark`, with `revremark` + set to the title-case display label. + +- **Page 2 (Document State preface):** a single `Note:` paragraph + containing the phase notice — e.g., + > **Note:** This document is ratified. No changes are allowed; use a + > follow-on extension for updates. + +- **Page 3 onward:** TOC, list-of prefaces, and body content. + +The `Document State` preface is created in the spec source with: + +```adoc +[preface] +== Document State +*Note:* {phase_notice} + +toc::[] +``` + +`{phase_notice}` is set automatically by the Makefile via +`scripts/release-info.sh notice `. + +## 4. How to comply + +### 4.1 Release flow (recommended) + +1. Freeze the source at the commit you intend to submit. +2. Run the `Create Specification Document` GitHub Action with + `target_phase` set to the milestone you're cutting (e.g. `stabilized`) + — OR push a tag of the form `v0.8`. +3. The workflow builds the PDF with `VERSION` and `DATE` plumbed through + the Makefile, produces `build/-v.-.pdf`, creates + a GitHub Release, and attaches the PDF. +4. Verify the title page shows the version, milestone ID, and date. +5. Use the **GitHub Release URL** when scheduling the ARC slot. + +### 4.2 Building locally + +```bash +make VERSION=v0.8 DATE=2026-06-12 +ls build/ +# → -v0.8-20260612.pdf +``` + +The Makefile passes the ARC-compliant filename to AsciiDoctor's `-o` on every +build — there is no separate "ARC build" target, and no post-build rename that +could fail and leave a non-compliant PDF behind; every PDF this repo emits is +ARC-compliant by construction. + +### 4.3 Multi-document repos + +If your repo builds more than one specification, list each in `DOCS` and +the `%.pdf` rule names each PDF from its own basename. If two docs in one +repo are submitted to ARC independently, ensure their basenames are the +ARC short names. + +## 5. Pre-submission checklist + +Before requesting an ARC review slot, confirm: + +- [ ] Tag `v.` is pushed to the canonical repo +- [ ] Tag is on a commit that builds reproducibly +- [ ] PDF is published (release asset preferred) with filename + `-v.-.pdf` +- [ ] PDF title page shows the same short name, version, milestone ID, and + date +- [ ] The version and date in filename, tag, title page, and milestone ID + are mutually consistent (no `v0.85` filename with a `frozen` + milestone, etc.) +- [ ] Tag version is monotonically greater than the previous tag (decimal + order: `v0.8` > `v0.79`) + +ARC reviewers reference the **tag link** (or release URL), not a draft or +branch. Submissions that don't satisfy the checklist will be rejected and +rescheduled. + +## 6. Migration + +If your repo was forked from `docs-spec-template` before this policy was +introduced, see `MIGRATION.md` in this template repo for a step-by-step +checklist to bring the toolchain in line. + +## 7. Where to ask for help + +- Policy questions: `help@riscv.org` +- Tooling / build issues: open an issue against `riscv/docs-spec-template` diff --git a/MIGRATION.md b/MIGRATION.md new file mode 100644 index 0000000..7fd771d --- /dev/null +++ b/MIGRATION.md @@ -0,0 +1,693 @@ +# Migration Guide — ARC PDF + Antora Site + +**Who this is for:** Maintainers of any RISC-V specification repository under +`github.com/riscv/*` (ISA) or `github.com/riscv-non-isa/*` (non-ISA) that was +forked from `docs-spec-template` or shares its toolchain (Makefile + +`scripts/release-info.sh` + `.github/workflows/build-pdf.yml`). + +**Why this exists:** the template now produces **two** artifacts from **one** +source tree, and a spec author needs both: + +1. **The ARC submission PDF.** ARC rejects submissions that do not emit a PDF + named `-v.-.pdf` whose title page matches, + using the canonical milestone IDs (`development-complete`, `stabilized`, + `frozen`, `ratification-ready`, `ratified`). +2. **The Antora HTML site.** Chapter content is consumed as a *content source* + by the RISC-V central playbook and published on antora.riscv.org. The ARC + Author Guide requires the site version to match the PDF version **exactly**. + Optionally, the same content can also be published to your own repo's GitHub + Pages site (Step 13a) — a standalone copy, not a replacement for the central + site. + +Parts A and C below are the PDF; Part B is the site. **Do not stop after Part +A** — a repo that does gets a compliant PDF and no site. + +**Related docs:** `ARC_SUBMISSION.md` is the ARC policy itself. `ANTORA.md` +explains *how* the dual build works and *why* it is designed this way — read its +"Dual-source technique" and "Production model" sections before Part B. This guide +is the sequence of actions; ANTORA.md is the rationale. + +## Pre-flight + +- [ ] Read `ARC_SUBMISSION.md` to understand what your repo must emit. +- [ ] Skim `ANTORA.md` §"The dual-source technique" and §"Production model". +- [ ] Confirm your repo's spec short name (e.g. `Zifoo`, `Server-Platform`, + `RHTI`). This is the `` that appears in PDF filenames. +- [ ] Check the latest tag in your repo (`./scripts/release-info.sh latest`). + Your next tag MUST be monotonically greater (decimal order: `v0.8` > `v0.79`). + Do NOT use `git ... --sort=version:refname` — it ranks `v0.8` below `v0.61`. +- [ ] **Do NOT rewrite or delete existing tags.** The policy is + forward-looking; old artifacts stay as-is. + +> **Scope warning.** This is no longer a "sync a few files" job. Part B moves +> your chapter sources on disk. Do it on a branch, and expect to touch +> `antora.yml`, `antora-playbook.yml`, `modules/ROOT/nav.adoc`, `package.json`, +> `docker-compose.yml`, `scripts/stamp-antora-version.sh`, and +> `.github/workflows/validate-content-source.yml` in addition to the PDF files. +> Adopting the optional GitHub Pages publish (Step 13a) adds +> `.github/workflows/publish-site.yml`, `scripts/build-pages-site.sh`, and +> `scripts/gen-pages-playbook.js`. + +## Doc mode (non-ratified documentation) + +Skip this section unless your repo is **documentation**, not a specification +headed for ARC ratification (the worked example is `riscv/docs-dev-guide`). +Doc mode keeps everything above — the PDF target, the Makefile `%.html` +target, the Antora site, version identity from semver git tags + build-date +stamping — and strips only the ratification layer: the Document State +preface, the phase/milestone attributes, and `SPEC_STATE.md` tracking. + +- [ ] Commit a `.docmode` file at your repo root containing exactly `doc` + (no trailing content beyond the first line). Absent, empty, or any + other value means `spec` — today's behavior, unaffected. +- [ ] Do Steps 1–2 and 7–13 below as written — they're the shared + Antora-ready layer. `scripts/release-info.sh mode` is the single + source of truth every one of them reads `.docmode` through; you never + parse the file yourself. +- [ ] In Step 1, ignore the phase-ID/milestone changes — `release-info.sh`'s + `phase`/`display`/`notice`/`revremark`/`milestone`/ + `phase-floor-version` all resolve to `""` in doc mode automatically. +- [ ] In Step 3/4's `build-pdf.yml`/`version-bot.yml`, the `target_phase` + enum and milestone gating stay as-is (they're pure version arithmetic, + not ratification), but a doc-mode `version-bot.yml`'s `milestone-pr` + job (which regenerates `SPEC_STATE.md`) should be gated the same way + upstream's is: add `&& needs.tag-version.outputs.mode == 'spec'` to its + `if:` (and add a `mode` output to `tag-version`, from + `release-info.sh mode`). +- [ ] Skip Step 5's Document State preface and phase-attribute defaults + entirely — a doc-mode top-level `.adoc` needs neither; the upstream + file already guards them behind `ifndef::doc-mode[]`. +- [ ] Skip `SPEC_STATE.md` and `scripts/update-spec-state.sh` altogether — + there's no milestone state to track. +- [ ] In Step 12's `antora.yml`, keep only `version`, `page-revnumber`, + `page-revdate`, and add `doc-mode: 'true'` — drop `page-phase`, + `page-phase-display`, `page-phase-notice`. `stamp-antora-version.sh` + requires exactly those three keys (not six) in doc mode. +- [ ] Verify the same way as Step 6, minus the phase assertions: the PDF + still has the ARC filename shape (`-v-.pdf`) and a + title page, but no "Document State" preface page and no phase label + on the revision line; `modules/ROOT/pages/index.adoc` renders with no + phase banner. + +--- + +# Part A — ARC PDF compliance + +## Step 1 — Sync `scripts/release-info.sh` + +Replace your script with the upstream version, or apply the following changes: + +- [ ] Phase IDs switched from `Developed`/`Stable`/`Frozen`/`Ratification-Ready`/`Ratified` + to the canonical (lowercase, hyphenated) IDs + `development-complete`/`stabilized`/`frozen`/`ratification-ready`/`ratified`. +- [ ] Versions are two-digit `vMAJOR.MINOR` as a fixed-point decimal + (`v0.6` = 0.60, `v1.0` = 1.00). Comparison is decimal, not semver, so the + script compares by centi-value — never `sort -V`/`git ... --sort`, which + rank `v0.8` below `v0.61`. Use the `compare`/`max`/`latest` subcommands. +- [ ] Milestone gates are `v0.6`, `v0.8`, `v0.9`, `v0.99`, `v1.0`. `next` + advances by `0.01` and REFUSES (exit 10) when the next step would land on + a gate — gates are cut manually via `workflow_dispatch`. +- [ ] New helper `phase_display_for_phase` returns the title-case display + label (e.g. `Stabilized`, `Ratification-Ready`) used on the title page. +- [ ] `revremark_for_phase` is now a thin wrapper that returns just the + display label (e.g. `"Ratified"`), so asciidoctor-pdf renders + `Version vX.Y, YYYY-MM-DD: Print -1 / 0 / 1 for ab (decimal order). + max Print the greater of two versions. + is-milestone Exit 0 if v is a manual milestone gate. + mode Resolved .docmode: "spec" (default) or "doc". + phase [v] Lifecycle phase (state) for a version. Empty in doc mode. + phase-floor-version

Version at the gate of phase p. Empty in doc mode. + display [v] Title-case display label for a version's phase. Empty in doc mode. + milestone [v] " " milestone label. Empty in doc mode. + notice [v] Change-control notice text for a version's phase. Empty in doc mode. + revremark [v] Revision remark (display label) for a version. Empty in doc mode. + all Emit all of the above as KEY=VALUE lines. +USAGE +} + +# .docmode selects the repo's ratification posture: "spec" (default -- today's +# behavior, byte-for-byte) keeps the full phase/milestone surface below; "doc" +# is for non-ratified documentation repos (e.g. docs-dev-guide) and neutralizes +# it -- see phase/display/milestone/notice/revremark/phase-floor-version below. +# Version resolution (get_version and everything derived from it) is UNCHANGED +# by mode: identity still comes from semver git tags + build-date stamping. +docmode() { + local f="$repo_root/.docmode" v + if [[ -f "$f" ]]; then + v="$(head -n1 "$f" | tr -d '[:space:]')" + else + v="" + fi + case "$v" in + ""|spec) echo "spec" ;; + doc) echo "doc" ;; + *) + echo "release-info: unrecognized .docmode value '$v'; defaulting to spec" >&2 + echo "spec" + ;; + esac +} + +normalize_prefix() { + local v="$1" + v="${v##*/}" + if [[ "$v" != v* ]]; then + v="v${v}" + fi + echo "$v" +} + +base_version() { + local v="$1" + v="${v#v}" + v="${v%%+*}" + v="${v%%-*}" + echo "$v" +} + +version_valid() { + local v + v="$(base_version "$1")" + [[ "$v" =~ ^[0-9]+\.[0-9]+$ ]] +} + +# Centi-value: MAJOR*100 + fractional-hundredths. The fractional part is padded +# to two digits so v0.6 == v0.60 == 60. v1.0 -> 100, v0.0 -> 0. +centi_of() { + local v major frac + v="$(base_version "$1")" + IFS='.' read -r major frac <<<"$v" + frac="${frac:-0}" + frac="${frac}00" + frac="${frac:0:2}" + echo $(( 10#$major * 100 + 10#$frac )) +} + +is_milestone_centi() { + case "$1" in + 60|80|90|99|100) return 0 ;; + *) return 1 ;; + esac +} + +# Canonical short (policy) form for a milestone centi-value. +milestone_string_for_centi() { + case "$1" in + 0) echo "v0.0" ;; + 60) echo "v0.6" ;; + 80) echo "v0.8" ;; + 90) echo "v0.9" ;; + 99) echo "v0.99" ;; + 100) echo "v1.0" ;; + *) return 1 ;; + esac +} + +# Format an arbitrary centi-value as a version string. Milestone values use the +# short policy form (60 -> v0.6); non-milestone auto values use the two-digit +# fractional form (70 -> v0.70, 5 -> v0.05). +format_centi() { + local c="$1" major frac + major=$(( c / 100 )) + frac=$(( c % 100 )) + if milestone_string_for_centi "$c" >/dev/null 2>&1; then + milestone_string_for_centi "$c" + elif (( frac == 0 )); then + printf 'v%d.0\n' "$major" + else + printf 'v%d.%02d\n' "$major" "$frac" + fi +} + +canonical_version() { + local v="$1" suffix="" + if [[ "$v" == *-* ]]; then + suffix="-${v#*-}" + v="${v%%-*}" + fi + echo "$(format_centi "$(centi_of "$v")")${suffix}" +} + +version_ge() { + local a b + a="$(centi_of "$1")" + b="$(centi_of "$2")" + (( a >= b )) +} + +compare_versions() { + local a b + a="$(centi_of "$1")" + b="$(centi_of "$2")" + if (( a < b )); then + echo -1 + elif (( a > b )); then + echo 1 + else + echo 0 + fi +} + +max_version() { + if version_ge "$1" "$2"; then + canonical_version "$1" + else + canonical_version "$2" + fi +} + +next_version() { + local c n + c="$(centi_of "$1")" + + if (( c >= 100 )); then + echo "release-info: $1 is at or past v1.0 (ratified); no automatic successor." >&2 + exit 2 + fi + + n=$(( c + 1 )) + if is_milestone_centi "$n"; then + local ms + ms="$(milestone_string_for_centi "$n")" + echo "release-info: next step $ms is a manual milestone gate; cut it via workflow_dispatch (target_phase or release_version)." >&2 + exit "$NEXT_AT_MILESTONE_RC" + fi + + format_centi "$n" +} + +# Highest valid v* tag by decimal (centi) order -- git's version sort cannot be +# trusted for this scheme (it would rank v0.8 below v0.61). +latest_tag() { + local best="" bestc=-1 t tc + if command -v git >/dev/null 2>&1; then + while IFS= read -r t; do + [[ -n "$t" ]] || continue + version_valid "$t" || continue + tc="$(centi_of "$t")" + if (( tc > bestc )); then + bestc="$tc" + best="$t" + fi + done < <(git tag --list 'v*' 2>/dev/null || true) + fi + if [[ -n "$best" ]]; then + canonical_version "$best" + else + echo "$DEFAULT_VERSION" + fi +} + +get_version() { + local v="" + + if [[ -n "${VERSION:-}" ]]; then + v="$VERSION" + elif [[ -n "${RELEASE_VERSION:-}" ]]; then + v="$RELEASE_VERSION" + elif [[ -n "${GITHUB_REF_NAME:-}" ]]; then + if version_valid "$GITHUB_REF_NAME"; then + v="$GITHUB_REF_NAME" + fi + elif [[ -n "${GITHUB_REF:-}" ]]; then + local ref="${GITHUB_REF##*/}" + if version_valid "$ref"; then + v="$ref" + fi + fi + + if [[ -n "$v" ]] && version_valid "$v"; then + canonical_version "$v" + return 0 + fi + + if command -v git >/dev/null 2>&1 && git rev-parse --git-dir >/dev/null 2>&1; then + local exact_tag + exact_tag="$(git tag --points-at HEAD --list 'v*' 2>/dev/null | head -n1 || true)" + if [[ -n "$exact_tag" ]] && version_valid "$exact_tag"; then + canonical_version "$exact_tag" + return 0 + fi + + # Untagged: -, with NO date folded in. The build date + # already reaches every artifact independently -- the Makefile passes it as + # revdate and appends DATE_STAMP to the ARC filename -- so embedding it here + # too stamped local PDFs with the date twice (spec-v0.6-abc-20260812-20260812 + # .pdf), which is not ARC-compliant. The sha alone keeps the build uniquely + # identifiable and marks it as not built from a tag. + local latest short_sha + latest="$(latest_tag)" + short_sha="$(git rev-parse --short HEAD 2>/dev/null || echo "unknown")" + echo "${latest}-${short_sha}" + return 0 + fi + + latest_tag +} + +phase_display_for_phase() { + case "$1" in + "draft-and-development") echo "Draft and Development" ;; + "development-complete") echo "Development Complete" ;; + "stabilized") echo "Stabilized" ;; + "frozen") echo "Frozen" ;; + "ratification-ready") echo "Ratification-Ready" ;; + "ratified") echo "Ratified" ;; + *) echo "Draft" ;; + esac +} + +phase_for_version() { + local v="$1" c + + if ! version_valid "$v"; then + echo "$DEFAULT_PHASE" + return 0 + fi + + c="$(centi_of "$v")" + + if (( c >= 100 )); then + echo "ratified" + elif (( c >= 99 )); then + echo "ratification-ready" + elif (( c >= 90 )); then + echo "frozen" + elif (( c >= 80 )); then + echo "stabilized" + elif (( c >= 60 )); then + echo "development-complete" + else + echo "draft-and-development" + fi +} + +milestone_for_phase() { + case "$1" in + "development-complete") echo "v0.6 development-complete" ;; + "stabilized") echo "v0.8 stabilized" ;; + "frozen") echo "v0.9 frozen" ;; + "ratification-ready") echo "v0.99 ratification-ready" ;; + "ratified") echo "v1.0 ratified" ;; + *) echo "draft-and-development" ;; + esac +} + +phase_floor_version() { + case "$1" in + "draft-and-development") echo "v0.0" ;; + "development-complete") echo "v0.6" ;; + "stabilized") echo "v0.8" ;; + "frozen") echo "v0.9" ;; + "ratification-ready") echo "v0.99" ;; + "ratified") echo "v1.0" ;; + *) + echo "Unknown phase '$1'" >&2 + return 2 + ;; + esac +} + +notice_for_phase() { + case "$1" in + "draft-and-development") + echo "Assume everything is subject to change. At this stage, ideas, structures, and content are still evolving. Feedback and iteration are encouraged as nothing is final, and adjustments may be frequent." + ;; + "development-complete") + echo "Assume everything is subject to change. At this stage, ideas, structures, and content are still evolving. Feedback and iteration are encouraged as nothing is final, and adjustments may be frequent." + ;; + "stabilized") + echo "Changes may still occur, but they should be limited in scope. The core structure and content are mostly settled, with only refinements or necessary adjustments expected. Any modifications should be carefully considered to maintain stability." + ;; + "frozen") + echo "Changes are highly unlikely. A high threshold will be applied, and modifications will only be made in response to critical issues. Any other proposed changes should be addressed through a follow-on extension." + ;; + "ratification-ready") + echo "The specification is preparing for ratification. Only critical, ratification-blocking issues should be considered for change." + ;; + "ratified") + echo "No changes are allowed. Any necessary or desired modifications must be addressed through a follow-on extension. Ratified extensions are never revised." + ;; + *) + echo "Assume everything is subject to change until a formal milestone is reached." + ;; + esac +} + +revremark_for_phase() { + phase_display_for_phase "$1" +} + +phase_from_input() { + local input="${1:-}" + if [[ -z "$input" ]]; then + phase_for_version "$(get_version)" + elif version_valid "$input"; then + phase_for_version "$input" + else + echo "$input" + fi +} + +command="${1:-all}" +value="${2:-}" + +# Neutralize the phase surface in doc mode: these commands short-circuit to an +# empty value before touching any version/phase machinery, so every consumer +# (Makefile, stamp-antora-version.sh) sees "" rather than a spec-mode label. +case "$command" in + phase|phase-floor-version|display|milestone|notice|revremark) + if [[ "$(docmode)" == "doc" ]]; then + echo "" + exit 0 + fi + ;; +esac + +case "$command" in + version) + get_version + ;; + latest) + latest_tag + ;; + mode) + docmode + ;; + normalize) + if [[ -z "$value" ]]; then + echo "normalize requires a version value" >&2 + exit 2 + fi + if ! version_valid "$value"; then + echo "invalid version: $value" >&2 + exit 2 + fi + canonical_version "$value" + ;; + next) + if [[ -z "$value" ]]; then + value="$(get_version)" + fi + if ! version_valid "$value"; then + echo "invalid version: $value" >&2 + exit 2 + fi + next_version "$value" + ;; + compare) + if [[ -z "$value" || -z "${3:-}" ]]; then + echo "compare requires two version values" >&2 + exit 2 + fi + if ! version_valid "$value" || ! version_valid "$3"; then + echo "invalid version(s): $value $3" >&2 + exit 2 + fi + compare_versions "$value" "$3" + ;; + max) + if [[ -z "$value" || -z "${3:-}" ]]; then + echo "max requires two version values" >&2 + exit 2 + fi + if ! version_valid "$value" || ! version_valid "$3"; then + echo "invalid version(s): $value $3" >&2 + exit 2 + fi + max_version "$value" "$3" + ;; + is-milestone) + if [[ -z "$value" ]] || ! version_valid "$value" || [[ "$value" == *-* ]]; then + exit 1 + fi + is_milestone_centi "$(centi_of "$value")" + ;; + phase) + if [[ -z "$value" ]]; then + value="$(get_version)" + fi + phase_for_version "$value" + ;; + phase-floor-version) + if [[ -z "$value" ]]; then + value="$(phase_for_version "$(get_version)")" + fi + phase_floor_version "$value" + ;; + display) + if [[ -z "$value" ]]; then + value="$(get_version)" + fi + phase_display_for_phase "$(phase_for_version "$value")" + ;; + milestone) + phase="$(phase_from_input "$value")" + milestone_for_phase "$phase" + ;; + notice) + phase="$(phase_from_input "$value")" + notice_for_phase "$phase" + ;; + revremark) + phase="$(phase_from_input "$value")" + revremark_for_phase "$phase" + ;; + all|"") + mode="$(docmode)" + version="$(get_version)" + if [[ "$mode" == "doc" ]]; then + phase="" display="" milestone="" notice="" revremark="" + else + phase="$(phase_for_version "$version")" + display="$(phase_display_for_phase "$phase")" + milestone="$(milestone_for_phase "$phase")" + notice="$(notice_for_phase "$phase")" + revremark="$(revremark_for_phase "$phase")" + fi + printf 'MODE=%s\nVERSION=%s\nPHASE=%s\nPHASE_DISPLAY=%s\nMILESTONE=%s\nPHASE_NOTICE=%s\nREVMARK=%s\n' \ + "$mode" "$version" "$phase" "$display" "$milestone" "$notice" "$revremark" + ;; + -h|--help|help) + usage + ;; + *) + usage + exit 2 + ;; +esac diff --git a/scripts/stamp-antora-version.sh b/scripts/stamp-antora-version.sh new file mode 100755 index 0000000..6ed2fd4 --- /dev/null +++ b/scripts/stamp-antora-version.sh @@ -0,0 +1,120 @@ +#!/usr/bin/env bash +# Stamp the Antora component descriptor (antora.yml) with the current release +# version and phase so the HTML site version stays in EXACT lockstep with the +# ARC submission PDF, per the ARC Author Guide (version = the vX.Y tag; the +# title-page revision line is "Version , : "). +# +# The PDF derives its version dynamically at build time from the git tag via +# scripts/release-info.sh. Antora, by contrast, reads a STATIC version from the +# committed antora.yml, so a release must stamp that file. This is the Antora +# analogue of scripts/update-spec-state.sh (which stamps SPEC_STATE.md). +# +# Usage: scripts/stamp-antora-version.sh [version] [date] +# version vX.Y (default: scripts/release-info.sh version -> latest tag) +# date YYYY-MM-DD (default: today) +# +# Idempotent: it replaces the values of existing keys in antora.yml in place and +# leaves comments, nav, and other attributes untouched. +set -euo pipefail + +here="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +repo_root="$(cd "$here/.." && pwd)" +antora_yml="$repo_root/antora.yml" + +version="${1:-$("$here/release-info.sh" version)}" +date="${2:-$(date +%Y-%m-%d)}" + +# Normalise and validate the version (vX.Y) through the shared helper. +version="$("$here/release-info.sh" normalize "$version")" + +# .docmode ("spec" default | "doc") -- see scripts/release-info.sh. In doc mode +# phase/display/notice all resolve to "" (the phase surface is neutralized), so +# the page-phase* keys below are dropped from the required set rather than +# stamped with empty values. +mode="$("$here/release-info.sh" mode)" + +phase="$("$here/release-info.sh" phase "$version")" +phase_display="$("$here/release-info.sh" display "$version")" +phase_notice="$("$here/release-info.sh" notice "$version")" + +# YAML single-quote escaping (double any embedded single quote). +yq_squote() { printf "'%s'" "${1//\'/\'\'}"; } + +export A_VERSION="$version" +export A_REVDATE="$date" +export A_PHASE="$phase" +export A_PHASE_DISPLAY="$(yq_squote "$phase_display")" +export A_PHASE_NOTICE="$(yq_squote "$phase_notice")" + +tmp="$(mktemp)" +# Replace values by unique key. The trailing ':' in each pattern anchors the key +# so e.g. `page-phase:` never matches `page-phase-display:`. +# +# Every value we write is a single-line quoted scalar. If a formatter or a hand +# edit has line-wrapped one of these into a plain MULTI-line scalar, replacing +# just the key's first line would orphan the continuation after a now-closed +# quoted scalar and silently produce invalid YAML. So after replacing a key we +# also drop any following lines that are indented deeper than it -- those are +# the remains of its wrapped value. `cont` holds the indent of the key just +# written, or -1 when we are not inside a replaced value. +# +# Each key must be found EXACTLY once; otherwise we would exit 0 having stamped +# nothing (or having stamped twice), and the release workflow's `git diff` check +# would read that as "already up to date". Fail loudly instead -- see END. +awk -v mode="$mode" ' + BEGIN { + cont = -1 + if (mode == "doc") { + nreq = split("version revnumber revdate", req, " ") + } else { + nreq = split("version revnumber revdate display notice phase", req, " ") + } + } + + cont >= 0 { + if ($0 ~ /^[[:space:]]*$/ || $0 ~ /^[[:space:]]*#/) { + cont = -1 + } else { + match($0, /^[[:space:]]*/) + if (RLENGTH > cont) next + cont = -1 + } + } + + /^version:/ { print "version: " ENVIRON["A_VERSION"]; seen["version"]++; cont = 0; next } + /^ page-revnumber:/ { print " page-revnumber: " ENVIRON["A_VERSION"]; seen["revnumber"]++; cont = 4; next } + /^ page-revdate:/ { print " page-revdate: " ENVIRON["A_REVDATE"]; seen["revdate"]++; cont = 4; next } + /^ page-phase-display:/ { print " page-phase-display: " ENVIRON["A_PHASE_DISPLAY"]; seen["display"]++; cont = 4; next } + /^ page-phase-notice:/ { print " page-phase-notice: " ENVIRON["A_PHASE_NOTICE"]; seen["notice"]++; cont = 4; next } + /^ page-phase:/ { print " page-phase: " ENVIRON["A_PHASE"]; seen["phase"]++; cont = 4; next } + { print } + + END { + bad = "" + for (i = 1; i <= nreq; i++) + if (seen[req[i]] != 1) + bad = bad sprintf(" %-12s found %d time(s), expected 1\n", req[i], seen[req[i]] + 0) + if (bad != "") { + printf "stamp-antora-version: antora.yml does not have the expected keys:\n%s", bad > "/dev/stderr" + exit 3 + } + } +' "$antora_yml" > "$tmp" +mv "$tmp" "$antora_yml" + +# Assert the result still parses. Without this the release workflow would commit +# a corrupt antora.yml to main and the site version would stop tracking the PDF. +if python3 -c 'import yaml' 2>/dev/null; then + python3 -c 'import sys, yaml; yaml.safe_load(open(sys.argv[1]))' "$antora_yml" || { + echo "stamp-antora-version: stamped antora.yml is not valid YAML (see above)" >&2 + exit 4 + } +else + echo "stamp-antora-version: WARNING: python3 + PyYAML unavailable; skipped YAML validation" >&2 +fi + +if [[ "$mode" == "doc" ]]; then + echo "Stamped antora.yml: version=$version date=$date (doc mode: phase surface not stamped)" +else + echo "Stamped antora.yml: version=$version phase=$phase ($phase_display) date=$date" +fi diff --git a/src/docs-dev-guide.adoc b/src/docs-dev-guide.adoc index 5a009d4..1f5bcad 100755 --- a/src/docs-dev-guide.adoc +++ b/src/docs-dev-guide.adoc @@ -1,5 +1,14 @@ [[riscv-authoring]] = Authoring and Editing RISC-V Specifications +// Fallbacks for a bare `asciidoctor` invocation. The Makefile always passes +// -a revnumber/-a revdate (from scripts/release-info.sh, the same source that +// stamps antora.yml), so the PDF title page and the site cover cannot diverge. +// This repo runs in doc mode (.docmode), so there are no phase/milestone +// attribute defaults to declare -- see MIGRATION.md "Doc mode". +ifndef::revnumber[:revnumber: v0.0] +ifndef::revdate[:revdate: 1970-01-01] +ifndef::spec_short[:spec_short: docs-dev-guide] +:revinfo: //:title-logo-image: image:risc-v_logo.png[pdfwidth=3.25in,align=center] :description: A working template for documenting RISC-V architecture in asciidoc :company: RISC-V.org diff --git a/tests/release-info-test.sh b/tests/release-info-test.sh new file mode 100755 index 0000000..de53cb1 --- /dev/null +++ b/tests/release-info-test.sh @@ -0,0 +1,186 @@ +#!/usr/bin/env bash +# +# Tests for scripts/release-info.sh -- the 2-digit (fixed-point decimal) release +# metadata helper. These lock the subtle bits: DECIMAL ordering (v0.8 > v0.61, +# which sort -V gets wrong), the +0.01 auto-increment, the milestone guard that +# refuses to auto-mint a manual gate, phase detection, and normalization. +# +# Run: tests/release-info-test.sh (exit 0 = all passed) +set -uo pipefail + +here="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +RI="$here/../scripts/release-info.sh" + +pass=0 +fail=0 + +# ok +ok() { + local desc="$1" expected="$2" actual="$3" + if [[ "$actual" == "$expected" ]]; then + pass=$(( pass + 1 )) + else + fail=$(( fail + 1 )) + printf 'FAIL: %s\n expected: %q\n actual: %q\n' "$desc" "$expected" "$actual" + fi +} + +# rc +rc() { + local desc="$1" expected="$2"; shift 2 + "$@" >/dev/null 2>&1 + local actual=$? + if [[ "$actual" == "$expected" ]]; then + pass=$(( pass + 1 )) + else + fail=$(( fail + 1 )) + printf 'FAIL: %s\n expected rc: %s\n actual rc: %s\n' "$desc" "$expected" "$actual" + fi +} + +# --- decimal ordering (the sort -V / git version-sort trap) ----------------- +ok "compare v0.8 > v0.61" 1 "$("$RI" compare v0.8 v0.61)" +ok "compare v0.61 < v0.8" -1 "$("$RI" compare v0.61 v0.8)" +ok "compare v0.6 == v0.60" 0 "$("$RI" compare v0.6 v0.60)" +ok "compare v0.99 > v0.9" 1 "$("$RI" compare v0.99 v0.9)" +ok "compare v1.0 > v0.99" 1 "$("$RI" compare v1.0 v0.99)" +ok "compare v0.9 > v0.89" 1 "$("$RI" compare v0.9 v0.89)" +ok "max v0.8 v0.61 = v0.8" v0.8 "$("$RI" max v0.8 v0.61)" +ok "max v0.70 v0.9 = v0.9" v0.9 "$("$RI" max v0.70 v0.9)" + +# --- normalization / canonical form ----------------------------------------- +ok "normalize v0.60 -> v0.6" v0.6 "$("$RI" normalize v0.60)" +ok "normalize v0.6 -> v0.6" v0.6 "$("$RI" normalize v0.6)" +ok "normalize v0.7 -> v0.70" v0.70 "$("$RI" normalize v0.7)" +ok "normalize 0.99 -> v0.99" v0.99 "$("$RI" normalize 0.99)" +ok "normalize v1.0 -> v1.0" v1.0 "$("$RI" normalize v1.0)" +ok "normalize v0.0 -> v0.0" v0.0 "$("$RI" normalize v0.0)" + +# --- auto-increment (+0.01) -------------------------------------------------- +ok "next v0.6 -> v0.61" v0.61 "$("$RI" next v0.6)" +ok "next v0.61 -> v0.62" v0.62 "$("$RI" next v0.61)" +ok "next v0.69 -> v0.70" v0.70 "$("$RI" next v0.69)" +ok "next v0.05 -> v0.06" v0.06 "$("$RI" next v0.05)" +ok "next v0.0 -> v0.01" v0.01 "$("$RI" next v0.0)" +ok "next v0.9 -> v0.91" v0.91 "$("$RI" next v0.9)" + +# --- milestone guard: auto-increment must never mint a manual gate ---------- +rc "next v0.59 refuses v0.6" 10 "$RI" next v0.59 +rc "next v0.79 refuses v0.8" 10 "$RI" next v0.79 +rc "next v0.89 refuses v0.9" 10 "$RI" next v0.89 +rc "next v0.98 refuses v0.99" 10 "$RI" next v0.98 +rc "next v0.99 refuses v1.0" 10 "$RI" next v0.99 +rc "next v1.0 has no successor" 2 "$RI" next v1.0 + +# --- is-milestone ------------------------------------------------------------ +rc "v0.6 is a milestone" 0 "$RI" is-milestone v0.6 +rc "v0.8 is a milestone" 0 "$RI" is-milestone v0.8 +rc "v0.9 is a milestone" 0 "$RI" is-milestone v0.9 +rc "v0.99 is a milestone" 0 "$RI" is-milestone v0.99 +rc "v1.0 is a milestone" 0 "$RI" is-milestone v1.0 +rc "v0.61 not a milestone" 1 "$RI" is-milestone v0.61 +rc "v0.7 not a milestone" 1 "$RI" is-milestone v0.7 + +# --- phase detection (state achieved) --------------------------------------- +ok "phase v0.0" draft-and-development "$("$RI" phase v0.0)" +ok "phase v0.3" draft-and-development "$("$RI" phase v0.3)" +ok "phase v0.59" draft-and-development "$("$RI" phase v0.59)" +ok "phase v0.6" development-complete "$("$RI" phase v0.6)" +ok "phase v0.72" development-complete "$("$RI" phase v0.72)" +ok "phase v0.8" stabilized "$("$RI" phase v0.8)" +ok "phase v0.85" stabilized "$("$RI" phase v0.85)" +ok "phase v0.9" frozen "$("$RI" phase v0.9)" +ok "phase v0.98" frozen "$("$RI" phase v0.98)" +ok "phase v0.99" ratification-ready "$("$RI" phase v0.99)" +ok "phase v1.0" ratified "$("$RI" phase v1.0)" + +# --- phase floors round-trip ------------------------------------------------- +ok "floor draft-and-development" v0.0 "$("$RI" phase-floor-version draft-and-development)" +ok "floor development-complete" v0.6 "$("$RI" phase-floor-version development-complete)" +ok "floor stabilized" v0.8 "$("$RI" phase-floor-version stabilized)" +ok "floor frozen" v0.9 "$("$RI" phase-floor-version frozen)" +ok "floor ratification-ready" v0.99 "$("$RI" phase-floor-version ratification-ready)" +ok "floor ratified" v1.0 "$("$RI" phase-floor-version ratified)" + +# --- display labels ---------------------------------------------------------- +ok "display v0.8" Stabilized "$("$RI" display v0.8)" +ok "display v1.0" Ratified "$("$RI" display v1.0)" + +# --- publication milestone is gone ------------------------------------------ +rc "publication is not a valid phase floor" 2 "$RI" phase-floor-version publication + +# --- interim build non-tagged versions --------------------------------------- +ok "normalize v0.60-a1b2c3d" "v0.6-a1b2c3d" "$("$RI" normalize v0.60-a1b2c3d)" +rc "v0.6-a1b2c3d not a milestone" 1 "$RI" is-milestone v0.6-a1b2c3d +ok "phase v0.6-a1b2c3d" development-complete "$("$RI" phase v0.6-a1b2c3d)" +# Suffix handling stays generic, incl. the legacy date-bearing dev form. +ok "normalize v0.60-a1b2c3d-20260730" "v0.6-a1b2c3d-20260730" "$("$RI" normalize v0.60-a1b2c3d-20260730)" +ok "phase v0.6-a1b2c3d-20260730" development-complete "$("$RI" phase v0.6-a1b2c3d-20260730)" + +# --- dev version carries NO date (#131) -------------------------------------- +# The Makefile appends DATE_STAMP to build the ARC filename +# -v-.pdf. A dev version that embedded its own build +# date therefore stamped the date TWICE on every local build. Assert the emitted +# shape is - so the concatenation stays ARC-shaped. +devrepo="$(mktemp -d)" +( + cd "$devrepo" || exit 1 + git init -q . + git -c user.email=t@example.com -c user.name=t commit -q --allow-empty -m init + git tag v0.6 + git -c user.email=t@example.com -c user.name=t commit -q --allow-empty -m untagged +) >/dev/null 2>&1 +ri_clean() { (cd "$devrepo" && env -u VERSION -u RELEASE_VERSION -u GITHUB_REF_NAME -u GITHUB_REF "$RI" "$@"); } +dev_sha="$(cd "$devrepo" && git rev-parse --short HEAD)" +dev_version="$(ri_clean version)" +ok "untagged build version is -" "v0.6-$dev_sha" "$dev_version" +ok "untagged build version has no date stamp" "" "$(printf '%s' "$dev_version" | grep -oE '[0-9]{8}' || true)" +ok "untagged build phase still resolves" development-complete "$(ri_clean phase "$dev_version")" +(cd "$devrepo" && git checkout -q v0.6) +ok "tagged build version is the bare tag" v0.6 "$(ri_clean version)" +rm -rf "$devrepo" + +# --- 3-digit input is rejected ---------------------------------------------- +rc "normalize rejects v0.6.0" 2 "$RI" normalize v0.6.0 +rc "normalize rejects v0.99.1" 2 "$RI" normalize v0.99.1 + +# --- .docmode (issue #110) ---------------------------------------------------- +# .docmode is resolved relative to release-info.sh's OWN location (repo_root = +# $here/..), not the caller's cwd, so exercising it needs a scratch repo that +# carries its own copy of the script under scripts/ -- same as $RI itself. +modrepo="$(mktemp -d)" +mkdir -p "$modrepo/scripts" +cp "$RI" "$modrepo/scripts/release-info.sh" +( + cd "$modrepo" || exit 1 + git init -q . + git -c user.email=t@example.com -c user.name=t commit -q --allow-empty -m init + git tag v0.8 +) >/dev/null 2>&1 +mod_ri() { (cd "$modrepo" && env -u VERSION -u RELEASE_VERSION -u GITHUB_REF_NAME -u GITHUB_REF ./scripts/release-info.sh "$@"); } + +ok "mode defaults to spec (no .docmode)" spec "$(mod_ri mode)" +ok "phase unaffected in spec mode" stabilized "$(mod_ri phase v0.8)" + +echo doc > "$modrepo/.docmode" +ok "mode is doc when .docmode contains doc" doc "$(mod_ri mode)" +ok "phase is empty in doc mode" "" "$(mod_ri phase v0.8)" +ok "phase-floor-version is empty in doc mode" "" "$(mod_ri phase-floor-version stabilized)" +ok "display is empty in doc mode" "" "$(mod_ri display v0.8)" +ok "milestone is empty in doc mode" "" "$(mod_ri milestone v0.8)" +ok "notice is empty in doc mode" "" "$(mod_ri notice v0.8)" +ok "revremark is empty in doc mode" "" "$(mod_ri revremark v0.8)" +ok "version is unaffected in doc mode" v0.8 "$(mod_ri version)" +ok "compare is unaffected in doc mode" 1 "$(mod_ri compare v0.8 v0.6)" +ok "is-milestone is unaffected in doc mode" 0 "$(mod_ri is-milestone v0.8; echo $?)" +ok "all reports MODE=doc and blank phase fields" \ + "$(printf 'MODE=doc\nVERSION=v0.8\nPHASE=\nPHASE_DISPLAY=\nMILESTONE=\nPHASE_NOTICE=\nREVMARK=')" \ + "$(mod_ri all)" + +echo bogus > "$modrepo/.docmode" +ok "unrecognized .docmode value falls back to spec" spec "$(mod_ri mode 2>/dev/null)" + +rm -rf "$modrepo" + +printf '\n%d passed, %d failed\n' "$pass" "$fail" +[[ "$fail" -eq 0 ]]