diff --git a/github-actions/build/.gitattributes b/github-actions/build/.gitattributes new file mode 100644 index 000000000..0b0e893a8 --- /dev/null +++ b/github-actions/build/.gitattributes @@ -0,0 +1,2 @@ +# Preserve the action commit in source archives downloaded by GitHub Actions. +source-commit.txt export-subst diff --git a/github-actions/build/README.md b/github-actions/build/README.md new file mode 100644 index 000000000..411201438 --- /dev/null +++ b/github-actions/build/README.md @@ -0,0 +1,150 @@ +# docs.rs build action + +A composite GitHub Action that runs the docs.rs build with nearly 100% parity +with our production systems. + +It uses: + +- `cargo package --allow-dirty --no-verify`, so this checks the files that would + be published. +- the [docs.rs sandbox image][linux-image] and +- the same sandbox CPU / memory / disk limits, and +- the crate's [docs.rs metadata settings](https://docs.rs/about/metadata). + +## Why test your crate with this action? + +A successful local `cargo doc` build can still fail after publishing to docs.rs. +Adding this action to your crate's pull-request CI catches differences in the +published package, [documentation settings][docsrs-metadata], and build +environment before you release a version with broken hosted documentation. + +It helps catch common failures described in the +[docs.rs build documentation](https://docs.rs/about/builds): + +- **[docs.rs metadata][docsrs-metadata]:** The build uses + [`[package.metadata.docs.rs]`][docsrs-metadata], including selected features, + disabled default features, targets, and rustdoc options. This exercises + configurations that your normal tests or `cargo doc` command might never + build. +- **Missing system dependencies:** A native library or executable installed on + your development machine might be absent from the docs.rs image. Building in + that image reveals those dependencies; use the default + [full image][linux-image] for the closest match to the libraries available on + docs.rs. +- [**Read-only filesystem:**](https://docs.rs/about/builds#read-only-directories) + The sandbox mounts the crate's source directory read-only. Build scripts that + generate files in the source tree can fail here even when they work locally; + generated build files should go in `OUT_DIR`. +- **Out-of-memory failures:** The sandbox applies docs.rs's default memory + limit, helping expose documentation builds that succeed on a larger + development machine but exceed the hosted build's budget. The CLI reports peak + sandbox memory usage alongside build results. + +The build also starts from a packaged crate, which can reveal files accidentally +excluded from publication. This is a useful rehearsal for publishing, though +nightly toolchain changes and crate-specific resource limits on docs.rs can +still affect the eventual hosted build. + +## Alternative: cargo-docs-rs + +[`cargo-docs-rs`](https://github.com/syphar/cargo-docs-rs) is a lighter-weight +alternative for local development or CI. It runs `cargo rustdoc` with +docs.rs-style options derived from +[`[package.metadata.docs.rs]`][docsrs-metadata], making it useful for checking +feature selection and documentation flags without downloading the sandbox image. + +It runs in your existing environment, so it does not reproduce the docs.rs +image's system dependencies, read-only source mount, or sandbox memory limit. +Use this action when you also want to test those environment constraints. + +## Usage + +This action lives at `github-actions/build` in the docs.rs repository. In the +beta-phase, we're referencing using `main`. We'll do version numbers when we +fully release the tool. + +```yaml +name: test docs.rs documentation build +on: [push, pull_request] +permissions: + contents: read + +jobs: + docs-rs: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v7 + - uses: rust-lang/docs.rs/github-actions/build@main + with: + package: my-crate + strict: "true" +``` + +The caller checks out the crate before invoking the action. Use a Linux host +with Bash, rustup, Cargo, and a running Docker daemon accessible to the runner +user. GitHub-hosted Ubuntu runners provide these prerequisites. Run directly on +the host: a job-level `container:` or Docker action controlling sibling +containers is not supported by `docs_rs_build`. + +The action installs stable Rust for compiling the runner without changing the +caller's default toolchain. Documentation uses the CLI's default nightly +toolchain. + +The `docs_rs_build` CLI is installed from the same repository snapshot as the +action using `GITHUB_ACTION_PATH`. Selecting `@main`, a tag, or a commit +therefore selects both the action and runner together, without a second Git +checkout or a separate runner revision input. + +## Inputs + +| Input | Default | Meaning | +| ------------- | ------- | --------------------------------------------------------------------------------------------------------------------------------- | +| `path` | `.` | Crate or workspace directory relative to the checkout root. | +| `package` | empty | Cargo package specification; required for virtual workspaces. If omitted, Cargo must select exactly one package. | +| `small-image` | `false` | Use the smaller [linux-micro image][linux-micro-image] instead of the full [linux image][linux-image]. Accepts `true` or `false`. | +| `strict` | `false` | Also fail for JSON, coverage, or additional-target failures. Accepts `true` or `false`. | + +Default failure behavior matches the runner: setup, packaging, default-target +HTML, and missing library documentation fail the action. `strict: 'true'` +additionally makes auxiliary build failures fatal. + +To select a workspace member, run from the workspace root (the default +`path: .`) and specify its package name: + +```yaml +- uses: rust-lang/docs.rs/github-actions/build@main + with: + package: my-crate +``` + +For faster image downloads, when the smaller image provides the native +dependencies your crate needs: + +```yaml +- uses: rust-lang/docs.rs/github-actions/build@main + with: + small-image: "true" +``` + +The [full image][linux-image] provides closer parity with docs.rs. The +[micro image][linux-micro-image] has fewer native libraries. + +## Artifacts + +The `workspace` output gives the absolute path containing rustwide state and +generated documentation. The runner prints artifact locations in its summary; +the action does not upload documentation artifacts automatically. + +Each invocation builds one package. Use separate matrix jobs to check multiple +crates concurrently. Host toolchain installation and Docker image downloads +require network access. + +## References + +- [docs_rs_build documentation](../../crates/bin/docs_rs_build/README.md) +- [Swatinem/rust-cache](https://github.com/Swatinem/rust-cache) +- [GitHub composite action metadata](https://docs.github.com/en/actions/reference/workflows-and-actions/metadata-syntax#runs-for-composite-actions) + +[linux-image]: https://github.com/rust-lang/crates-build-env/tree/master/linux +[linux-micro-image]: https://github.com/rust-lang/crates-build-env/tree/master/linux-micro +[docsrs-metadata]: https://docs.rs/about/metadata diff --git a/github-actions/build/action.yml b/github-actions/build/action.yml new file mode 100644 index 000000000..e40a9af70 --- /dev/null +++ b/github-actions/build/action.yml @@ -0,0 +1,145 @@ +name: docs.rs build +description: Build a packaged Rust crate with the docs.rs build runner and sandbox. + +inputs: + path: + description: Package or workspace directory, relative to the repository root. + default: '.' + package: + description: Cargo package specification. Required for virtual workspaces. + default: '' + small-image: + description: Use the smaller sandbox image with fewer native libraries. Accepts true or false. + default: 'false' + strict: + description: Also fail on JSON, coverage, or additional-target build failures. + default: 'false' + +outputs: + workspace: + description: Absolute rustwide workspace path containing build state and artifacts. + value: ${{ steps.prepare.outputs.workspace }} + +runs: + using: composite + steps: + - name: Prepare build workspace + id: prepare + shell: bash + working-directory: ${{ github.workspace }} + env: + CRATE_PATH: ${{ inputs.path }} + STRICT: ${{ inputs.strict }} + SMALL_IMAGE: ${{ inputs.small-image }} + run: | + if [[ "$RUNNER_OS" != Linux ]]; then + echo '::error::docs.rs builds require a Linux host with Docker.' + exit 1 + fi + case "$STRICT" in + true|false) ;; + *) echo '::error::strict must be true or false'; exit 1 ;; + esac + case "$SMALL_IMAGE" in + true|false) ;; + *) echo '::error::small-image must be true or false'; exit 1 ;; + esac + if [[ ! -f "$CRATE_PATH/Cargo.toml" ]]; then + echo '::error::No Cargo.toml found at the requested path.' + exit 1 + fi + docker info > /dev/null + echo "workspace=$RUNNER_TEMP/docsrs-build" >> "$GITHUB_OUTPUT" + + # Install the CLI from the action's source archive so both use the same + # commit, even if the requested branch/tag moves while this job runs. + # Archives have no .git directory. export-subst embeds their exact commit + # in source-commit.txt; github.sha would identify the calling crate instead. + # This SHA supplies the CLI's build metadata and keys its binary cache. + - name: Read the bundled runner revision + id: runner-source + shell: bash + run: | + # Use this repository's .git explicitly so an archived action nested + # in a caller's checkout cannot pick up the caller's commit by accident. + source_sha=$(git --git-dir="$GITHUB_ACTION_PATH/../../.git" rev-parse HEAD 2>/dev/null || + cat "$GITHUB_ACTION_PATH/source-commit.txt") + echo "sha=$source_sha" >> "$GITHUB_OUTPUT" + echo "Building docs_rs_build from commit $source_sha" + + - name: Install Rust stable for the build runner + shell: bash + run: rustup toolchain install --profile minimal --no-self-update stable + + - name: Cache the docs.rs build runner + id: runner-cache + uses: Swatinem/rust-cache@v2 + with: + key: docs-rs-build-runner-commit-v1-${{ steps.runner-source.outputs.sha }} + add-rust-environment-hash-key: 'false' + cache-targets: 'false' + cache-on-failure: 'true' + + - name: Restore the rustwide workspace + id: workspace-cache + uses: actions/cache/restore@v5 + with: + path: ${{ steps.prepare.outputs.workspace }} + # Rustwide manages changes to packages, images, and toolchains itself. + # Bump the version only when the workspace layout becomes incompatible. + key: docs-rs-workspace-v1-${{ runner.os }}-${{ runner.arch }} + + # Keep image download time separate from CLI installation and doc builds. + - name: Pull the docs.rs Docker image + shell: bash + env: + SMALL_IMAGE: ${{ inputs.small-image }} + run: | + # Keep these names aligned with the CLI's default and --small-image. + image=ghcr.io/rust-lang/crates-build-env/linux + if [[ "$SMALL_IMAGE" == true ]]; then + image=ghcr.io/rust-lang/crates-build-env/linux-micro + fi + docker pull "$image" + + - name: Install docs.rs build runner + # Swatinem/rust-cache restores the binary and Cargo installation metadata, + # but cargo install --path always rebuilds even when those were restored. + # Skip installation on an exact commit-cache hit to reuse the binary; + # otherwise build from the same source archive that provided this action. + if: steps.runner-cache.outputs.cache-hit != 'true' + shell: bash + env: + CARGO_TARGET_DIR: ${{ runner.temp }}/docsrs-runner-target + GIT_SHA: ${{ steps.runner-source.outputs.sha }} + run: cargo +stable install --path "$GITHUB_ACTION_PATH/../../crates/bin/docs_rs_build" --locked --force + + - name: Build documentation as docs.rs + id: build + shell: bash + working-directory: ${{ github.workspace }} + env: + CRATE_PATH: ${{ inputs.path }} + PACKAGE: ${{ inputs.package }} + STRICT: ${{ inputs.strict }} + SMALL_IMAGE: ${{ inputs.small-image }} + BUILD_WORKSPACE: ${{ steps.prepare.outputs.workspace }} + run: | + args=(--workspace "$BUILD_WORKSPACE") + if [[ -n "$PACKAGE" ]]; then + args+=(--package "$PACKAGE") + fi + if [[ "$SMALL_IMAGE" == true ]]; then + args+=(--small-image) + fi + if [[ "$STRICT" == true ]]; then + args+=(--strict) + fi + docs_rs_build "${args[@]}" -- "$CRATE_PATH" + + - name: Save the rustwide workspace + if: ${{ !cancelled() && steps.build.outcome != 'skipped' && steps.build.outcome != '' && steps.workspace-cache.outputs.cache-hit != 'true' }} + uses: actions/cache/save@v5 + with: + path: ${{ steps.prepare.outputs.workspace }} + key: ${{ steps.workspace-cache.outputs.cache-primary-key }} diff --git a/github-actions/build/source-commit.txt b/github-actions/build/source-commit.txt new file mode 100644 index 000000000..6828f88dc --- /dev/null +++ b/github-actions/build/source-commit.txt @@ -0,0 +1 @@ +$Format:%H$