From 8a961300c0ab273c9cd45a0000d0bf1167d648c5 Mon Sep 17 00:00:00 2001 From: Matthew Helmke Date: Wed, 30 Sep 2026 10:22:44 -0500 Subject: [PATCH 1/2] Drop the unused httpx pin from the MCP server requirements httpx was pinned for the registry client behind check_image_freshness, which edu#4099 removed. Nothing imports it now, and mcp 2.2 depends on httpx2, a separate package, so the pin only installed an unused library. The unit tests pass in a fresh environment without it. Refs DOCS-138 Co-Authored-By: Claude Opus 5.5 --- scripts/mcp-requirements.txt | 1 - 1 file changed, 1 deletion(-) diff --git a/scripts/mcp-requirements.txt b/scripts/mcp-requirements.txt index b8702aa210..a69e866181 100644 --- a/scripts/mcp-requirements.txt +++ b/scripts/mcp-requirements.txt @@ -3,4 +3,3 @@ # (see DOCS-89 for the decision, DOCS-90 for the port). Cap below 3.0 to guard # against the next major-version break. mcp>=2.2.0,<3 -httpx>=0.28.1 From a688f6e05a0e6b047fcbc4bebfd9dd7630e5d18f Mon Sep 17 00:00:00 2001 From: Matthew Helmke Date: Wed, 30 Sep 2026 10:22:49 -0500 Subject: [PATCH 2/2] Remove the obsolete export templates and update the pipeline guide The templates in .github/workflows/templates/ were blueprints for the images-private and courses exports to GCS. Since edu#4099 the AI docs bundle compiles from this repository alone, so nothing reads those exports, and Actions never ran the templates. docs/automated-docs-setup.md described four sources, the templates, GCS downloads, and a local test that needed placeholder sibling directories. It now describes the two sources, the scripts/ trigger, the package catalog, and a local test that runs from a plain checkout. Every command in it was run as written. Refs DOCS-138 Co-Authored-By: Claude Opus 5.5 --- .../templates/export-courses-docs-to-gcs.yaml | 156 ------------- .../templates/export-docs-to-gcs.yaml | 219 ------------------ .../templates/export-images-docs-to-gcs.yaml | 126 ---------- docs/automated-docs-setup.md | 76 +++--- 4 files changed, 39 insertions(+), 538 deletions(-) delete mode 100644 .github/workflows/templates/export-courses-docs-to-gcs.yaml delete mode 100644 .github/workflows/templates/export-docs-to-gcs.yaml delete mode 100644 .github/workflows/templates/export-images-docs-to-gcs.yaml diff --git a/.github/workflows/templates/export-courses-docs-to-gcs.yaml b/.github/workflows/templates/export-courses-docs-to-gcs.yaml deleted file mode 100644 index 7cb2c19acf..0000000000 --- a/.github/workflows/templates/export-courses-docs-to-gcs.yaml +++ /dev/null @@ -1,156 +0,0 @@ -# Template workflow for chainguard-dev/courses repository -# Copy this to .github/workflows/export-docs-to-gcs.yaml in the courses repo - -name: Export Courses Documentation to GCS for AI Bundle - -on: - push: - branches: [ main ] - paths: - - '*/lessons/*/content-*.html' - - schedule: - - cron: '30 1 * * 0' # Weekly on Sundays at 1:30 AM - - workflow_dispatch: - -permissions: - contents: read - id-token: write # Required for workload identity federation - -jobs: - export-docs: - runs-on: ubuntu-latest - - steps: - - name: Harden Runner - uses: step-security/harden-runner@8d3c67de8e2fe68ef647c8db1e6a09f647780f40 # v2.19.0 - with: - egress-policy: audit - - - name: Checkout repository - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - with: - persist-credentials: false - - - name: Authenticate to Google Cloud - uses: step-security/google-github-auth@57c51210cb4d85d8a5d39dc4c576c79bd693f914 # v3.0.1 - with: - workload_identity_provider: "projects/456977358484/locations/global/workloadIdentityPools/chainguard-academy/providers/chainguard-edu" - service_account: "github-chainguard-academy@chainguard-academy.iam.gserviceaccount.com" - - - name: Set up Cloud SDK - uses: google-github-actions/setup-gcloud@aa5489c8933f4cc7a4f7d45035b3b1440c9c10db # v3.0.1 - - - name: Prepare courses documentation export - run: | - set -euo pipefail - - EXPORT_DIR=$(mktemp -d) - trap "rm -rf $EXPORT_DIR" EXIT - - echo "Processing courses repository HTML content..." - - # Install pandoc for HTML to Markdown conversion - sudo apt-get update -qq && sudo apt-get install -y -qq pandoc - - course_count=0 - lesson_count=0 - - for course_dir in [A-Z]*; do - # Skip internal/private courses - if [ "$course_dir" = "Build-Your-First-Chainguard-Container" ]; then - echo "Skipping internal course: $course_dir" - continue - fi - - if [ -d "$course_dir/lessons" ]; then - echo "Processing course: $course_dir" - course_count=$((course_count + 1)) - mkdir -p "$EXPORT_DIR/$course_dir/lessons" - - for lesson_dir in "$course_dir/lessons"/*; do - if [ -d "$lesson_dir" ]; then - lesson_name=$(basename "$lesson_dir") - mkdir -p "$EXPORT_DIR/$course_dir/lessons/$lesson_name" - - for html_file in "$lesson_dir"/content-*.html; do - if [ -f "$html_file" ]; then - base_name=$(basename "$html_file" .html) - output_file="$EXPORT_DIR/$course_dir/lessons/$lesson_name/${base_name}.md" - - pandoc -f html -t markdown_strict "$html_file" | \ - sed -E 's///g' | \ - sed '/^[[:space:]]*$/N;/\n[[:space:]]*$/d' > "$output_file" - - lesson_count=$((lesson_count + 1)) - fi - done - fi - done - fi - done - - echo "Processed $course_count courses with $lesson_count lesson files" - - # Create metadata file - cat > "$EXPORT_DIR/metadata.json" << EOF - { - "repository": "$GITHUB_REPOSITORY", - "export_time": "$(date -u +"%Y-%m-%dT%H:%M:%SZ")", - "commit": "$GITHUB_SHA", - "ref": "$GITHUB_REF", - "files_count": $(find "$EXPORT_DIR" -name "*.md" -type f | wc -l) - } - EOF - - python3 -m json.tool "$EXPORT_DIR/metadata.json" > /dev/null - - cd "$(dirname "$EXPORT_DIR")" - tar --owner=0 --group=0 --mode='u+rwX,go+rX,go-w' \ - -czf /tmp/docs-export.tar.gz "$(basename "$EXPORT_DIR")" - - echo "Documentation export created:" - ls -lh /tmp/docs-export.tar.gz - - - name: Upload to GCS - run: | - set -euo pipefail - - echo "Uploading to gs://academy-all-docs/courses/" - - gcloud storage cp /tmp/docs-export.tar.gz \ - "gs://academy-all-docs/courses/docs-export.tar.gz" \ - --project=chainguard-academy \ - --content-type="application/gzip" \ - --cache-control="no-cache" - - # Extract for metadata upload - EXPORT_DIR=$(mktemp -d) - trap "rm -rf $EXPORT_DIR" EXIT - tar -xzf /tmp/docs-export.tar.gz -C "$EXPORT_DIR" --strip-components=1 - - gcloud storage cp "$EXPORT_DIR/metadata.json" \ - "gs://academy-all-docs/courses/metadata.json" \ - --project=chainguard-academy \ - --content-type="application/json" \ - --cache-control="no-cache" - - echo "Successfully uploaded courses documentation to GCS" - - - name: Trigger edu repo workflow - if: github.event_name != 'workflow_dispatch' - uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0 - with: - github-token: ${{ secrets.GITHUB_TOKEN }} - script: | - await github.rest.repos.createDispatchEvent({ - owner: 'chainguard-dev', - repo: 'edu', - event_type: 'ai-docs-source-updated', - client_payload: { - repository: '${{ github.repository }}', - commit: '${{ github.sha }}', - repo_path: 'courses' - } - }); diff --git a/.github/workflows/templates/export-docs-to-gcs.yaml b/.github/workflows/templates/export-docs-to-gcs.yaml deleted file mode 100644 index 3cf2b91cea..0000000000 --- a/.github/workflows/templates/export-docs-to-gcs.yaml +++ /dev/null @@ -1,219 +0,0 @@ -# Template workflow for images-private and courses repositories -# Copy this to .github/workflows/export-docs-to-gcs.yaml in each repo - -name: Export Documentation to GCS for AI Bundle - -on: - push: - branches: [ main ] - paths: - - 'content/**' - - 'docs/**' - - '**.md' - - schedule: - - cron: '30 1 * * 0' # Weekly on Sundays at 1:30 AM - - workflow_dispatch: - -permissions: - contents: read - id-token: write # Required for workload identity federation - -jobs: - export-docs: - runs-on: ubuntu-latest - - steps: - - name: Harden Runner - uses: step-security/harden-runner@8d3c67de8e2fe68ef647c8db1e6a09f647780f40 # v2.19.0 - with: - egress-policy: audit - - - name: Checkout repository - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - with: - persist-credentials: false - - - name: Authenticate to Google Cloud - uses: step-security/google-github-auth@57c51210cb4d85d8a5d39dc4c576c79bd693f914 # v3.0.1 - with: - workload_identity_provider: "projects/456977358484/locations/global/workloadIdentityPools/chainguard-academy/providers/chainguard-edu" - service_account: "github-chainguard-academy@chainguard-academy.iam.gserviceaccount.com" - - - name: Set up Cloud SDK - uses: google-github-actions/setup-gcloud@aa5489c8933f4cc7a4f7d45035b3b1440c9c10db # v3.0.1 - - - name: Prepare documentation export - run: | - set -euo pipefail # Exit on error, undefined variable, or pipe failure - - # Use mktemp for secure temp directory - EXPORT_DIR=$(mktemp -d) - trap "rm -rf $EXPORT_DIR" EXIT # Clean up on exit - - # Copy all markdown content (adjust paths as needed for each repo) - echo "Collecting documentation files..." - - # For images-private repo - if [[ "${{ github.repository }}" == *"images-private"* ]]; then - echo "Collecting README.md files from image directories..." - - # Create images directory structure - mkdir -p "$EXPORT_DIR/images" - - # Find all README.md files in images/*/README.md pattern - # This captures only the main README for each image, not nested subdirectories - find images -mindepth 2 -maxdepth 2 -name "README.md" -type f | while read file; do - # Get the image directory name (e.g., adoptium-jdk-fips from images/adoptium-jdk-fips/README.md) - image_dir=$(dirname "$file") - - # Create the directory structure in export - mkdir -p "$EXPORT_DIR/$image_dir" - - # Copy and clean the README.md file - # Remove HTML comments like , , etc. - sed -E 's///g' "$file" | \ - sed '/^[[:space:]]*$/N;/\n[[:space:]]*$/d' > "$EXPORT_DIR/$file" - - echo " ✓ Processed $file" - done - - echo "Total image READMEs collected: $(find "$EXPORT_DIR/images" -name "README.md" | wc -l)" - fi - - # For courses repo - if [[ "${{ github.repository }}" == *"courses"* ]]; then - echo "Processing courses repository HTML content..." - - # Install pandoc for HTML to Markdown conversion - echo "Installing pandoc for HTML conversion..." - sudo apt-get update && sudo apt-get install -y pandoc - - # Find and process HTML files in course structure - # Pattern: /CourseDirectory/lessons/*/content-*.html - course_count=0 - lesson_count=0 - - # Look for course directories (they typically start with capital letters) - for course_dir in [A-Z]*; do - # Skip internal/private courses - if [ "$course_dir" = "Build-Your-First-Chainguard-Container" ]; then - echo "Skipping internal course: $course_dir" - continue - fi - - if [ -d "$course_dir/lessons" ]; then - echo "Processing course: $course_dir" - course_count=$((course_count + 1)) - - # Create course directory in export - mkdir -p "$EXPORT_DIR/$course_dir/lessons" - - # Process each lesson directory - for lesson_dir in "$course_dir/lessons"/*; do - if [ -d "$lesson_dir" ]; then - lesson_name=$(basename "$lesson_dir") - echo " Processing lesson: $lesson_name" - - # Create lesson directory - mkdir -p "$EXPORT_DIR/$course_dir/lessons/$lesson_name" - - # Convert HTML files to markdown - for html_file in "$lesson_dir"/content-*.html; do - if [ -f "$html_file" ]; then - base_name=$(basename "$html_file" .html) - output_file="$EXPORT_DIR/$course_dir/lessons/$lesson_name/${base_name}.md" - - # Convert HTML to Markdown using pandoc, then clean up - pandoc -f html -t markdown_strict "$html_file" | \ - sed -E 's///g' | \ - sed '/^[[:space:]]*$/N;/\n[[:space:]]*$/d' > "$output_file" - - lesson_count=$((lesson_count + 1)) - echo " ✓ Converted $(basename "$html_file")" - fi - done - fi - done - fi - done - - echo "Processed $course_count courses with $lesson_count lesson files" - fi - - # Create metadata file - cat > "$EXPORT_DIR/metadata.json" << EOF - { - "repository": "$GITHUB_REPOSITORY", - "export_time": "$(date -u +"%Y-%m-%dT%H:%M:%SZ")", - "commit": "$GITHUB_SHA", - "ref": "$GITHUB_REF", - "files_count": $(find "$EXPORT_DIR" \( -name "*.md" -o -name "*.html" \) -type f | wc -l) - } - EOF - - # Validate JSON - python3 -m json.tool "$EXPORT_DIR/metadata.json" > /dev/null - - # Create tarball with restricted permissions - cd "$(dirname "$EXPORT_DIR")" - tar --owner=0 --group=0 --mode='u+rwX,go+rX,go-w' \ - -czf /tmp/docs-export.tar.gz "$(basename "$EXPORT_DIR")" - - echo "Documentation export created:" - ls -lh /tmp/docs-export.tar.gz - echo "Files included: $(tar -tzf /tmp/docs-export.tar.gz | wc -l)" - - - name: Upload to GCS - run: | - set -euo pipefail - - # Determine repo name for path - if [[ "${{ github.repository }}" == *"images-private"* ]]; then - REPO_PATH="images-private" - elif [[ "${{ github.repository }}" == *"courses"* ]]; then - REPO_PATH="courses" - else - REPO_PATH=$(echo "${{ github.repository }}" | cut -d'/' -f2) - fi - - # Upload to GCS with specific content types - echo "Uploading to gs://academy-all-docs/${REPO_PATH}/" - - gcloud storage cp /tmp/docs-export.tar.gz \ - "gs://academy-all-docs/${REPO_PATH}/docs-export.tar.gz" \ - --project=chainguard-academy \ - --content-type="application/gzip" \ - --cache-control="no-cache" - - # Extract for metadata upload - EXPORT_DIR=$(mktemp -d) - trap "rm -rf $EXPORT_DIR" EXIT - tar -xzf /tmp/docs-export.tar.gz -C "$EXPORT_DIR" --strip-components=1 - - # Upload metadata - gcloud storage cp "$EXPORT_DIR/metadata.json" \ - "gs://academy-all-docs/${REPO_PATH}/metadata.json" \ - --project=chainguard-academy \ - --content-type="application/json" \ - --cache-control="no-cache" - - echo "Successfully uploaded documentation to GCS" - - - name: Trigger edu repo workflow - if: github.event_name != 'workflow_dispatch' # Only trigger on automatic runs - uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0 - with: - github-token: ${{ secrets.GITHUB_TOKEN }} # Or use a PAT if cross-repo - script: | - await github.rest.repos.createDispatchEvent({ - owner: 'chainguard-dev', - repo: 'edu', - event_type: 'ai-docs-source-updated', - client_payload: { - repository: '${{ github.repository }}', - commit: '${{ github.sha }}', - repo_path: process.env.REPO_PATH - } - }); diff --git a/.github/workflows/templates/export-images-docs-to-gcs.yaml b/.github/workflows/templates/export-images-docs-to-gcs.yaml deleted file mode 100644 index 34de448311..0000000000 --- a/.github/workflows/templates/export-images-docs-to-gcs.yaml +++ /dev/null @@ -1,126 +0,0 @@ -# Template workflow for chainguard-dev/images-private repository -# Copy this to .github/workflows/export-docs-to-gcs.yaml in the images-private repo - -name: Export Image Documentation to GCS for AI Bundle - -on: - push: - branches: [ main ] - paths: - - 'images/*/README.md' - - schedule: - - cron: '30 1 * * 0' # Weekly on Sundays at 1:30 AM - - workflow_dispatch: - -permissions: - contents: read - -jobs: - export-docs: - runs-on: ubuntu-latest - - steps: - - name: Harden Runner - uses: step-security/harden-runner@8d3c67de8e2fe68ef647c8db1e6a09f647780f40 # v2.19.0 - with: - egress-policy: audit - - - name: Checkout repository - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - with: - persist-credentials: false - - - name: Authenticate to Google Cloud - env: - GCS_SA_KEY: ${{ secrets.GCS_SA_KEY }} - run: | - KEY_FILE=$(mktemp) - trap "rm -f $KEY_FILE" EXIT - echo "$GCS_SA_KEY" > "$KEY_FILE" - gcloud auth activate-service-account --key-file="$KEY_FILE" - - - name: Prepare image documentation export - run: | - set -euo pipefail - - EXPORT_DIR=$(mktemp -d) - trap "rm -rf $EXPORT_DIR" EXIT - - echo "Collecting README.md files from image directories..." - mkdir -p "$EXPORT_DIR/images" - - # Find all README.md files in images/*/README.md pattern - find images -mindepth 2 -maxdepth 2 -name "README.md" -type f | while read file; do - image_dir=$(dirname "$file") - mkdir -p "$EXPORT_DIR/$image_dir" - - # Remove HTML comments and clean up empty lines - sed -E 's///g' "$file" | \ - sed '/^[[:space:]]*$/N;/\n[[:space:]]*$/d' > "$EXPORT_DIR/$file" - done - - echo "Total image READMEs collected: $(find "$EXPORT_DIR/images" -name "README.md" | wc -l)" - - # Create metadata file - cat > "$EXPORT_DIR/metadata.json" << EOF - { - "repository": "$GITHUB_REPOSITORY", - "export_time": "$(date -u +"%Y-%m-%dT%H:%M:%SZ")", - "commit": "$GITHUB_SHA", - "ref": "$GITHUB_REF", - "files_count": $(find "$EXPORT_DIR" -name "*.md" -type f | wc -l) - } - EOF - - python3 -m json.tool "$EXPORT_DIR/metadata.json" > /dev/null - - cd "$(dirname "$EXPORT_DIR")" - tar --owner=0 --group=0 --mode='u+rwX,go+rX,go-w' \ - -czf /tmp/docs-export.tar.gz "$(basename "$EXPORT_DIR")" - - echo "Documentation export created:" - ls -lh /tmp/docs-export.tar.gz - - - name: Upload to GCS - run: | - set -euo pipefail - - echo "Uploading to gs://academy-all-docs/images-private/" - - gcloud storage cp /tmp/docs-export.tar.gz \ - "gs://academy-all-docs/images-private/docs-export.tar.gz" \ - --project=chainguard-academy \ - --content-type="application/gzip" \ - --cache-control="no-cache" - - # Extract for metadata upload - EXPORT_DIR=$(mktemp -d) - trap "rm -rf $EXPORT_DIR" EXIT - tar -xzf /tmp/docs-export.tar.gz -C "$EXPORT_DIR" --strip-components=1 - - gcloud storage cp "$EXPORT_DIR/metadata.json" \ - "gs://academy-all-docs/images-private/metadata.json" \ - --project=chainguard-academy \ - --content-type="application/json" \ - --cache-control="no-cache" - - echo "Successfully uploaded images-private documentation to GCS" - - - name: Trigger edu repo workflow - if: github.event_name != 'workflow_dispatch' - uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0 - with: - github-token: ${{ secrets.GITHUB_TOKEN }} - script: | - await github.rest.repos.createDispatchEvent({ - owner: 'chainguard-dev', - repo: 'edu', - event_type: 'ai-docs-source-updated', - client_payload: { - repository: '${{ github.repository }}', - commit: '${{ github.sha }}', - repo_path: 'images-private' - } - }); diff --git a/docs/automated-docs-setup.md b/docs/automated-docs-setup.md index 71453ea328..2af5aa4cec 100644 --- a/docs/automated-docs-setup.md +++ b/docs/automated-docs-setup.md @@ -5,18 +5,23 @@ and refreshed. ## Overview -The bundle combines documentation from four sources: +The bundle combines two sources, both from this repository: -- edu content, from `content/` in this repository -- Container image READMEs, from the images-private repository -- Course content, from the courses repository -- Dockerfile Converter package mappings, from the dfc repository +- edu content, from `content/` +- Dockerfile Converter (dfc) package and image mappings, from + `data/package-mappings.yaml` -Each source repository exports a tarball to the `academy-all-docs` bucket in -the `chainguard-academy` GCP project, then sends an `ai-docs-source-updated` -repository dispatch event to this repository. The -`.github/workflows/compile-ai-docs-from-gcs.yaml` workflow compiles the -sources into a single markdown file and publishes it. +The `.github/workflows/compile-ai-docs-from-gcs.yaml` workflow compiles them +into a single Markdown file and publishes it. `scripts/compile_docs.py` writes +the bundle, and `scripts/generate_package_catalog.py` writes +`package-mappings.json`, the catalog behind the MCP server's +`find_package_equivalent` and `find_image_equivalent` tools. The build also +ships the catalog as `image-catalog.json`, its earlier name. + +The nightly `autodocs-platform.yaml` workflow keeps +`data/package-mappings.yaml` current: it downloads dfc's `builtin-mappings.yaml` +and includes any change in its pull request. To change a mapping, change it in +[chainguard-dev/dfc](https://github.com/chainguard-dev/dfc), not here. ## Publishing targets @@ -45,29 +50,20 @@ nobody merged. The workflow runs on: -- An `ai-docs-source-updated` repository dispatch event from any source - repository +- An `ai-docs-source-updated` repository dispatch event, which + `export-edu-docs-to-gcs.yaml` sends after a push to `main` that changes + content +- A push to `main` that changes `scripts/` or the workflow itself, so a server + fix deploys without waiting for the nightly run - The nightly schedule, at 02:00 UTC - A manual run from the **Actions** tab Only the scheduled and manual runs open a pull request. -## Export workflows - -Each source repository needs an export workflow. Use the templates in -`.github/workflows/templates/`: - -- `export-docs-to-gcs.yaml` -- `export-images-docs-to-gcs.yaml` -- `export-courses-docs-to-gcs.yaml` - -The templates authenticate with workload identity federation against the -`chainguard-academy` pool. They don't use personal access tokens. - ## Authentication -The compile job federates a GCP token through workload identity to read from -and write to the bucket. The publish job federates a separate GitHub token +The compile job federates a GCP token through workload identity to write to +the bucket and to deploy. The publish job federates a separate GitHub token through octo-sts, using the trust policy in `.github/chainguard/ai-docs.sts.yaml`, which grants only `contents: write` and `pull_requests: write`. @@ -77,17 +73,24 @@ images never holds a git-write credential. ## Test the compilation locally -The compile script reads the three external sources from sibling directories. -To compile edu content alone, create empty placeholders: +Compile the bundle and the catalog from a checkout of this repository: ```bash -mkdir -p ../courses ../images-private ../dfc python3 scripts/compile_docs.py +python3 scripts/generate_package_catalog.py \ + --mappings data/package-mappings.yaml \ + --output /tmp/package-mappings.json ``` -The result lands in `static/downloads/chainguard-complete-docs.md`. Expect a -much smaller file than the published bundle, because it contains no image -READMEs. Don't commit that file. +The bundle lands in `static/downloads/chainguard-complete-docs.md`. Don't +commit that file; the nightly pull request owns it. + +To run the MCP server's unit tests: + +```bash +pip install -r scripts/mcp-requirements.txt pytest pyyaml +pytest scripts/test_mcp_server.py scripts/test_mcp_transport.py scripts/test_compile_docs.py +``` ## Troubleshoot @@ -95,13 +98,12 @@ READMEs. Don't commit that file. documentation bundle` pull request. If none exists, check whether the nightly run succeeded. -**The bundle is missing image documentation.** The images-private export is -failing, so the compile job is rebuilding from an old tarball. Read the -`Metadata information:` block in the `Download documentation from GCS` step, -which prints the `export_time` for each source. +**The bundle has no DFC mappings page.** The compile log shows `WARNING: +data/package-mappings.yaml not found` instead of `DFC mappings added`. Confirm +the file exists on `main`. **The compile job fails on size.** The bundle has a 50 MB ceiling. Something -in a source export has grown unexpectedly. +in `content/` has grown unexpectedly. **The compile job fails on the credential scan.** The bundle carries a pattern that looks like a real key. Find it in the workflow log, then fix the source