diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 7f26e5dd..0c20ff88 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -71,8 +71,7 @@ jobs: - docs/** - scripts/generate-docs.sh - .github/workflows/ci.yml - - .github/workflows/generate-docs.yml - - .github/workflows/publish-docs.yml + - .github/workflows/docs.yml cpp_tools: - src/** - include/** @@ -123,11 +122,15 @@ jobs: if: ${{ needs.changes.outputs.cpp_tools == 'true' || github.event_name == 'workflow_dispatch' }} uses: ./.github/workflows/cpp-tools.yml - generate-docs: - name: Generate Docs + docs: + name: Docs needs: changes - if: ${{ needs.changes.outputs.docs == 'true' || github.event_name == 'workflow_dispatch' }} - uses: ./.github/workflows/generate-docs.yml + uses: ./.github/workflows/docs.yml + with: + build_docs: ${{ needs.changes.outputs.docs == 'true' || github.event_name == 'workflow_dispatch' }} + # Deploy credentials are deliberately withheld from pull requests. + check_credentials: ${{ github.event_name == 'push' && github.ref == 'refs/heads/main' }} + secrets: inherit # Only runs on a client-sdk-rust submodule bump. Runs in parallel and is not a # dependency of builds/tests, so developer iteration against an unreleased @@ -150,7 +153,7 @@ jobs: - tests - license-check - cpp-tools - - generate-docs + - docs - link-check - rust-release-check if: always() diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml new file mode 100644 index 00000000..856aadf7 --- /dev/null +++ b/.github/workflows/docs.yml @@ -0,0 +1,225 @@ +name: Docs + +on: + release: + # A stable release publishes docs. A prerelease still builds the docs and + # checks the deploy credentials, but does not publish. "released" also + # catches promotion of an existing prerelease to stable. + types: [published, released] + workflow_call: + inputs: + build_docs: + description: "Build and verify the documentation" + required: false + type: boolean + default: true + check_credentials: + description: "Check the docs deploy AWS credentials" + required: false + type: boolean + default: false + version: + description: "Documentation version (e.g. v0.1.0)" + required: false + type: string + upload_artifact: + description: "Upload the generated docs as a workflow artifact" + required: false + type: boolean + default: true + artifact_name: + description: "Name of the uploaded docs artifact" + required: false + type: string + default: livekit-cpp-docs + artifact_retention_days: + description: "Artifact retention in days" + required: false + type: number + default: 7 + secrets: + DOCS_DEPLOY_AWS_ACCESS_KEY: + required: false + DOCS_DEPLOY_AWS_API_SECRET: + required: false + outputs: + project_number: + description: "Doxygen PROJECT_NUMBER used for the build" + value: ${{ jobs.build.outputs.project_number }} + artifact_name: + description: "Uploaded docs artifact name" + value: ${{ jobs.build.outputs.artifact_name }} + +permissions: + contents: read + actions: read + +jobs: + build: + name: Generate and verify docs + if: github.event_name == 'release' || inputs.build_docs + runs-on: ubuntu-latest + outputs: + project_number: ${{ steps.build_docs.outputs.project_number }} + artifact_name: ${{ steps.artifact_meta.outputs.name }} + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + # Required so git describe in generate-docs.sh can resolve a version. + fetch-depth: 0 + + - name: Install Doxygen + run: | + sudo apt-get update + sudo apt-get install -y doxygen graphviz + + - name: Generate docs + id: build_docs + shell: bash + env: + INPUT_VERSION: ${{ github.event_name == 'release' && github.event.release.tag_name || inputs.version || '' }} + run: | + set -euo pipefail + + args=() + if [[ -n "$INPUT_VERSION" ]]; then + args+=(--version "$INPUT_VERSION") + elif [[ "${{ github.ref_type }}" == "tag" ]]; then + args+=(--version "${{ github.ref_name }}") + fi + + ./scripts/generate-docs.sh "${args[@]}" + + - name: Print docs version + shell: bash + run: | + set -euo pipefail + + PROJECT_NUMBER="${{ steps.build_docs.outputs.project_number }}" + if [[ -z "$PROJECT_NUMBER" ]]; then + echo "ERROR: build_docs step did not emit a project_number output." + exit 1 + fi + + echo "Docs version: ${PROJECT_NUMBER}" + { + echo "Docs version: \`${PROJECT_NUMBER}\`" + echo "" + echo "> Note: On a non-tag/release run, the version resolves to:" + echo " \`--\`" + } >>"$GITHUB_STEP_SUMMARY" + + - name: Verify docs were generated + shell: bash + run: | + set -euo pipefail + if [[ ! -f docs/doxygen/html/index.html ]]; then + echo "ERROR: Expected docs at docs/doxygen/html/index.html but file not found." + exit 1 + fi + + - name: Resolve artifact metadata + id: artifact_meta + if: github.event_name == 'release' || inputs.upload_artifact + shell: bash + env: + INPUT_NAME: ${{ inputs.artifact_name || format('livekit-cpp-docs-{0}', github.run_id) }} + INPUT_RETENTION: ${{ inputs.artifact_retention_days || 7 }} + run: | + set -euo pipefail + if [[ -z "$INPUT_NAME" ]]; then + echo "ERROR: Artifact name resolved to empty." + exit 1 + fi + echo "name=${INPUT_NAME}" >>"$GITHUB_OUTPUT" + echo "retention=${INPUT_RETENTION}" >>"$GITHUB_OUTPUT" + + - name: Upload docs artifact + if: steps.artifact_meta.outputs.name != '' + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + with: + name: ${{ steps.artifact_meta.outputs.name }} + path: docs/doxygen/html/ + retention-days: ${{ steps.artifact_meta.outputs.retention }} + if-no-files-found: error + + - name: Re-download artifact + if: steps.artifact_meta.outputs.name != '' + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 + with: + name: ${{ steps.artifact_meta.outputs.name }} + path: html + + - name: Verify downloaded artifact + if: steps.artifact_meta.outputs.name != '' + shell: bash + run: | + set -euo pipefail + + TOTAL=$(find html -type f | wc -l | tr -d ' ') + echo "Total files: ${TOTAL}" + if [[ ! -f html/index.html ]]; then + echo "ERROR: html/index.html is missing; the publish artifact layout regressed." + exit 1 + fi + + credentials: + name: Check AWS credentials + # CI enables this only for trusted main pushes. Release events always + # check credentials before publishing. PR jobs never receive deploy keys. + if: github.event_name == 'release' || inputs.check_credentials + runs-on: ubuntu-latest + steps: + - name: Verify docs deploy credentials + env: + AWS_ACCESS_KEY_ID: ${{ secrets.DOCS_DEPLOY_AWS_ACCESS_KEY }} + AWS_SECRET_ACCESS_KEY: ${{ secrets.DOCS_DEPLOY_AWS_API_SECRET }} + AWS_DEFAULT_REGION: "us-east-1" + run: | + set -euo pipefail + if [[ -z "${AWS_ACCESS_KEY_ID}" || -z "${AWS_SECRET_ACCESS_KEY}" ]]; then + echo "Docs deploy AWS credentials are not available to this workflow." + exit 1 + fi + aws sts get-caller-identity --query Arn --output text + + publish: + name: Publish (S3 + CloudFront) + needs: [build, credentials] + if: github.event_name == 'release' && github.event.release.prerelease == false + runs-on: ubuntu-latest + steps: + - name: Download docs artifact + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 + with: + name: ${{ needs.build.outputs.artifact_name }} + path: html + + - name: S3 Upload + env: + AWS_ACCESS_KEY_ID: ${{ secrets.DOCS_DEPLOY_AWS_ACCESS_KEY }} + AWS_SECRET_ACCESS_KEY: ${{ secrets.DOCS_DEPLOY_AWS_API_SECRET }} + AWS_DEFAULT_REGION: "us-east-1" + run: | + set -euo pipefail + + if [[ ! -f html/index.html ]]; then + echo "Expected docs at html/index.html but file not found." + exit 1 + fi + + VERSIONED_PREFIX="s3://livekit-docs/client-sdk-cpp/${{ needs.build.outputs.project_number }}" + LATEST_PREFIX="s3://livekit-docs/client-sdk-cpp" + + aws s3 cp html/ "$VERSIONED_PREFIX" --recursive + aws s3 cp html/ "$LATEST_PREFIX" --recursive + + - name: Invalidate CloudFront cache + env: + AWS_ACCESS_KEY_ID: ${{ secrets.DOCS_DEPLOY_AWS_ACCESS_KEY }} + AWS_SECRET_ACCESS_KEY: ${{ secrets.DOCS_DEPLOY_AWS_API_SECRET }} + AWS_DEFAULT_REGION: "us-east-1" + run: | + aws cloudfront create-invalidation \ + --distribution-id EJJ40KLJ3TRY9 \ + --paths "/client-sdk-cpp/*" diff --git a/.github/workflows/generate-docs.yml b/.github/workflows/generate-docs.yml deleted file mode 100644 index d7bea1cf..00000000 --- a/.github/workflows/generate-docs.yml +++ /dev/null @@ -1,170 +0,0 @@ -name: Generate docs -permissions: - contents: read - -# Generates Doxygen docs and uploads them as a workflow artifact. - -# This workflow is used to validate documentation before merge, for example catching -# things like broken @ref tags, missing @param tags, etc. - -# The artifact is attached to the workflow run so reviewers can download a preview, -# but also to verify the documentation artifact is valid for subsequent release runs -# that will publish them to the LiveKit docs web page: https://docs.livekit.io/reference/client-sdk-cpp/ -# Called by top-level ci.yml on PRs. PR trigger lives in the parent -# so a single "CI" check gates every PR regardless of paths. -on: - workflow_call: - inputs: - version: - description: 'Documentation version (e.g. v0.1.0)' - required: false - type: string - upload_artifact: - description: 'Upload the generated docs folder as a workflow artifact' - required: false - type: boolean - default: true - artifact_name: - description: 'Name to use for the uploaded docs artifact' - required: false - type: string - default: livekit-cpp-docs - artifact_retention_days: - description: 'Retention (days) for the uploaded docs artifact' - required: false - type: number - default: 7 - outputs: - project_number: - description: 'Doxygen PROJECT_NUMBER used for the build (e.g., v1.2.3)' - value: ${{ jobs.validate.outputs.project_number }} - artifact_name: - description: 'Name of the uploaded docs artifact (empty if upload was skipped via upload_artifact: false)' - value: ${{ jobs.validate.outputs.artifact_name }} - -jobs: - validate: - name: Generate and verify docs - runs-on: ubuntu-latest - outputs: - project_number: ${{ steps.build_docs.outputs.project_number }} - artifact_name: ${{ steps.artifact_meta.outputs.name }} - steps: - - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - with: - # fetch-depth: 0 is required so `git describe --tags` in - # scripts/generate-docs.sh resolves the right version from the git history. - fetch-depth: 0 - - # Doxygen is available on ubuntu-latest, but we install explicitly for stability - - name: Install Doxygen - run: | - sudo apt-get update - sudo apt-get install -y doxygen graphviz - - - name: Generate docs (Doxygen) - id: build_docs - shell: bash - env: - INPUT_VERSION: ${{ inputs.version || '' }} - run: | - set -euo pipefail - - args=() - if [[ -n "$INPUT_VERSION" ]]; then - args+=(--version "$INPUT_VERSION") - elif [[ "${{ github.ref_type }}" == "tag" ]]; then - args+=(--version "${{ github.ref_name }}") - fi - - ./scripts/generate-docs.sh "${args[@]}" - - - name: Print docs version - shell: bash - run: | - set -euo pipefail - - PROJECT_NUMBER="${{ steps.build_docs.outputs.project_number }}" - - if [[ -z "$PROJECT_NUMBER" ]]; then - echo "ERROR: build_docs step did not emit a project_number output." - exit 1 - fi - - echo "Docs version: ${PROJECT_NUMBER}" - - { - echo "Docs version: \`${PROJECT_NUMBER}\`" - echo "" - echo "> Note: On a non-tag/release run, the version will resolve to:" - echo " \`--\`" - } >>"$GITHUB_STEP_SUMMARY" - - - name: Verify docs were generated - shell: bash - run: | - set -euo pipefail - if [[ ! -f docs/doxygen/html/index.html ]]; then - echo "ERROR: Expected docs at docs/doxygen/html/index.html but file not found." - exit 1 - fi - - # `inputs.*` is only populated on `workflow_call`. On a direct - # `pull_request` trigger every `inputs.*` lookup is the empty string, so - # we OR-fold to the documented defaults to keep PR runs behaving the same - # as the workflow_call default of `upload_artifact: true`. - - name: Resolve artifact metadata - id: artifact_meta - if: inputs.upload_artifact || github.event_name == 'pull_request' - shell: bash - env: - INPUT_NAME: ${{ inputs.artifact_name || 'livekit-cpp-docs' }} - INPUT_RETENTION: ${{ inputs.artifact_retention_days || '7' }} - run: | - set -euo pipefail - if [[ -z "$INPUT_NAME" ]]; then - echo "ERROR: artifact name resolved to empty." - exit 1 - fi - echo "name=${INPUT_NAME}" >>"$GITHUB_OUTPUT" - echo "retention=${INPUT_RETENTION}" >>"$GITHUB_OUTPUT" - - - name: Upload docs artifact - if: steps.artifact_meta.outputs.name != '' - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 - with: - name: ${{ steps.artifact_meta.outputs.name }} - path: docs/doxygen/html/ - retention-days: ${{ steps.artifact_meta.outputs.retention }} - if-no-files-found: error - - # Simple check to make sure the documentation artifact is valid. - - name: Re-download artifact (publish workflow pre-validation) - if: steps.artifact_meta.outputs.name != '' - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 - with: - name: ${{ steps.artifact_meta.outputs.name }} - path: html - - - name: Inspect downloaded artifact - if: steps.artifact_meta.outputs.name != '' - shell: bash - run: | - set -euo pipefail - - NAME="${{ steps.artifact_meta.outputs.name }}" - ROOT="html" - - echo "Artifact '${NAME}' extracted into '${ROOT}/'. Top-level contents:" - # `sed -n '1,Np'` reads the whole pipe before truncating its output, - # so the producer (ls) never hits SIGPIPE under `pipefail`. - ls -la "$ROOT" | sed -n '1,40p' - - TOTAL=$(find "$ROOT" -type f | wc -l | tr -d ' ') - echo "Total files: ${TOTAL}" - - if [[ ! -f "${ROOT}/index.html" ]]; then - echo "ERROR: ${ROOT}/index.html is missing -- artifact layout regressed." - echo " publish-docs.yml expects html/index.html." - exit 1 - fi diff --git a/.github/workflows/make-release.yml b/.github/workflows/make-release.yml index cdf5583d..51c81d7a 100644 --- a/.github/workflows/make-release.yml +++ b/.github/workflows/make-release.yml @@ -461,13 +461,13 @@ jobs: # Build docs for validation and retain them as a workflow artifact. Publishing # is triggered separately by the GitHub release's authoritative state. - generate-docs: + docs: name: Generate Documentation needs: release permissions: contents: read actions: read - uses: ./.github/workflows/generate-docs.yml + uses: ./.github/workflows/docs.yml with: version: ${{ needs.release.outputs.version }} upload_artifact: true diff --git a/.github/workflows/publish-docs.yml b/.github/workflows/publish-docs.yml deleted file mode 100644 index 04d087ca..00000000 --- a/.github/workflows/publish-docs.yml +++ /dev/null @@ -1,63 +0,0 @@ -name: Publish docs - -on: - release: - # "published" covers newly published stable releases and prereleases. - # "released" also catches promotion of an existing prerelease to stable. - types: [published, released] - -permissions: - contents: read - actions: read - -jobs: - validate: - name: Validate (build docs) - uses: ./.github/workflows/generate-docs.yml - with: - version: ${{ github.event.release.tag_name }} - upload_artifact: true - # Suffix with run_id so concurrent publish runs cannot collide on the - # artifact namespace within the same repository. - artifact_name: livekit-cpp-docs-${{ github.run_id }} - - publish: - name: Publish (S3 + CloudFront) - needs: validate - # The GitHub release is the source of truth. Drafts do not emit these - # events until published; prereleases validate docs but never deploy them. - if: github.event.release.draft == false && github.event.release.prerelease == false - runs-on: ubuntu-latest - steps: - - name: Download docs artifact - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 - with: - name: ${{ needs.validate.outputs.artifact_name }} - path: html - - - name: S3 Upload - run: | - DOCS_DIR="html" - if [[ ! -d "$DOCS_DIR" ]]; then - echo "Expected docs at $DOCS_DIR but directory not found." - exit 1 - fi - - VERSIONED_PREFIX="s3://livekit-docs/client-sdk-cpp/${{ needs.validate.outputs.project_number }}" - LATEST_PREFIX="s3://livekit-docs/client-sdk-cpp" - - # Upload immutable versioned docs, then refresh rolling latest. - aws s3 cp "$DOCS_DIR"/ "$VERSIONED_PREFIX" --recursive - aws s3 cp "$DOCS_DIR"/ "$LATEST_PREFIX" --recursive - env: - AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }} - AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }} - AWS_DEFAULT_REGION: "us-east-1" - - - name: Invalidate cloudfront cache - run: | - aws cloudfront create-invalidation --distribution-id EJJ40KLJ3TRY9 --paths "/client-sdk-cpp/*" - env: - AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }} - AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }} - AWS_DEFAULT_REGION: "us-east-1" diff --git a/AGENTS.md b/AGENTS.md index 45c875eb..58985b83 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -258,7 +258,7 @@ with the same library loaded elsewhere in the host process. ### Public API Documentation (Doxygen) The public API (`include/livekit/*.h`) is what consumers read first and is also -published as a Doxygen site (`docs/doxygen/Doxyfile`, `.github/workflows/publish-docs.yml`). +published as a Doxygen site (`docs/doxygen/Doxyfile`, `.github/workflows/docs.yml`). Every doc comment in `include/livekit/` must use the rules below, and PRs that add or modify public symbols are gated on these rules during review. @@ -392,7 +392,8 @@ all filtered stages; normal pull requests and pushes use the path filters. - `.github/workflows/cpp-tools.yml` — Reusable SDK-specific `clang-format` and `clang-tidy` workflow. It prepares the build environment and invokes the project wrappers backed by the shared `cpp-tools` scripts. -- `.github/workflows/generate-docs.yml` — Reusable Doxygen docs validation. +- `.github/workflows/docs.yml` — Reusable Doxygen docs validation, deploy + credential check on trusted `main` pushes, and stable-release publishing. - `.github/workflows/rust-release-check.yml` — Reusable check that the pinned `client-sdk-rust` submodule commit maps to a published release. Gated by the `rust_submodule` path filter so it only runs on a submodule bump, runs in