Skip to content

Test Extensions

Test Extensions #50

name: Test Extensions
on:
workflow_dispatch:
inputs:
debug:
description: "If 'true', test a small sample (up to 5 per extension type)"
default: "false"
batch_size:
description: "Number of extensions per batch"
default: "25"
schedule:
- cron: "0 14 15 * *"
concurrency:
group: test-extensions
cancel-in-progress: false
permissions:
contents: read
jobs:
build-matrix:
runs-on: ubuntu-latest
outputs:
matrix: ${{ steps.matrix.outputs.matrix }}
skipped: ${{ steps.matrix.outputs.skipped }}
steps:
- name: Checkout scripts
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
with:
sparse-checkout: .github/scripts/test-extensions
sparse-checkout-cone-mode: false
path: _scripts
- name: Checkout quarto-wizard branch
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
with:
ref: quarto-wizard
path: _data
- name: Create GitHub App token
uses: actions/create-github-app-token@f8d387b68d61c58ab83c6c016672934102569859 # v3
id: app-token
with:
app-id: ${{ vars.APP_ID }}
private-key: ${{ secrets.APP_KEY }}
permission-contents: read
permission-metadata: read
- name: Build test matrix
id: matrix
env:
GH_TOKEN: ${{ steps.app-token.outputs.token }}
DEBUG: ${{ inputs.debug || 'false' }}
BATCH_SIZE: ${{ inputs.batch_size || '25' }}
REPO_OWNER: ${{ github.repository_owner }}
shell: bash
working-directory: _data
run: bash "${GITHUB_WORKSPACE}/_scripts/.github/scripts/test-extensions/build-matrix.sh"
test-extensions:
name: test-extensions (batch ${{ matrix.batch_index }}, ${{ matrix.quarto_channel }})
needs: build-matrix
runs-on: ubuntu-latest
strategy:
fail-fast: false
max-parallel: 10
matrix: ${{ fromJSON(needs.build-matrix.outputs.matrix) }}
timeout-minutes: 90
defaults:
run:
shell: bash
env:
IMAGE: ${{ matrix.image_ref }}
IMAGE_READY: ${{ matrix.image_ready }}
QUARTO_CHANNEL: ${{ matrix.quarto_channel }}
steps:
- name: Checkout scripts
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
with:
sparse-checkout: .github/scripts/test-extensions
sparse-checkout-cone-mode: false
- name: Write extensions batch to file
env:
EXTENSIONS_JSON: ${{ toJSON(matrix.extensions) }}
run: printf '%s\n' "${EXTENSIONS_JSON}" > extensions-batch.json
- name: Inspect batch
id: inspect-batch
run: |
set -euo pipefail
ext_count=$(jq 'length' extensions-batch.json)
echo "count=${ext_count}" >> "${GITHUB_OUTPUT}"
if [[ "${ext_count}" -eq 0 ]]; then
echo "::notice::No extensions in this batch."
echo '[]' > results.json
mkdir -p logs
fi
- name: Prepare render image
if: steps.inspect-batch.outputs.count != '0'
run: |
set -euo pipefail
if [[ ! "${IMAGE}" =~ @sha256:[0-9a-f]{64}$ ]]; then
echo "::error::Image reference '${IMAGE}' is not a valid digest-pinned reference."
exit 1
fi
if [[ "${IMAGE_READY}" == "true" ]]; then
docker pull "${IMAGE}"
docker tag "${IMAGE}" render-image
else
echo "::notice::IMAGE_READY=false for ${QUARTO_CHANNEL}. Rebuilding from inline Dockerfile (~15 min overhead)."
docker build \
--build-arg BASE_IMAGE="${IMAGE}" \
--build-arg HOST_UID="$(id -u)" \
--build-arg HOST_GID="$(id -g)" \
-t render-image - <<'DOCKERFILE'
ARG BASE_IMAGE
ARG HOST_UID=1000
ARG HOST_GID=1000
FROM ${BASE_IMAGE}
USER root
RUN apt-get update -qq && apt-get install -y -qq --no-install-recommends \
jq libcurl4-openssl-dev libssl-dev libxml2-dev libfontconfig1-dev \
libharfbuzz-dev libfribidi-dev libfreetype6-dev libpng-dev \
libtiff5-dev libjpeg-dev libgit2-dev zlib1g-dev cmake \
ghostscript \
&& rm -rf /var/lib/apt/lists/*
RUN Rscript -e 'install.packages(c("rmarkdown", "knitr", "reticulate", "ggplot2", "dplyr", "tidyverse", "kableExtra", "palmerpenguins", "gt", "tibble", "ragg"), repos = "https://cloud.r-project.org", lib = .Library)'
ARG HOST_UID
ARG HOST_GID
RUN if [ -d /opt/tinytex ]; then chown -R ${HOST_UID}:${HOST_GID} /opt/tinytex; fi
USER vscode
ENV UV_LINK_MODE=copy
RUN uv venv /home/vscode/.venv \
&& . /home/vscode/.venv/bin/activate \
&& uv pip install jupyter papermill matplotlib shinylive
ENV VIRTUAL_ENV=/home/vscode/.venv
ENV PATH="/home/vscode/.venv/bin:${PATH}"
DOCKERFILE
fi
- name: Clone extensions
if: steps.inspect-batch.outputs.count != '0'
run: bash .github/scripts/test-extensions/clone-extensions.sh
- name: Render extensions
if: steps.inspect-batch.outputs.count != '0'
run: bash .github/scripts/test-extensions/render-extensions.sh
- name: Upload results
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7
if: always()
with:
name: results-batch-${{ matrix.batch_index }}-${{ matrix.quarto_channel }}
path: results.json
retention-days: 7
- name: Enforce log limits
id: enforce-log-limits
if: always()
env:
MAX_LOG_FILE_BYTES: "10485760"
MAX_LOG_FILES: "300"
MAX_LOG_TOTAL_BYTES: "104857600"
run: bash .github/scripts/test-extensions/enforce-log-limits.sh
- name: Upload logs
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7
if: always() && steps.enforce-log-limits.outcome == 'success'
with:
name: logs-batch-${{ matrix.batch_index }}-${{ matrix.quarto_channel }}
path: logs/
retention-days: 7
summarise-and-publish:
needs: [build-matrix, test-extensions]
if: always() && needs.build-matrix.result == 'success'
runs-on: ubuntu-latest
steps:
- name: Create GitHub App token
uses: actions/create-github-app-token@f8d387b68d61c58ab83c6c016672934102569859 # v3
id: app-token
with:
app-id: ${{ vars.APP_ID }}
private-key: ${{ secrets.APP_KEY }}
permission-contents: write
permission-metadata: read
- name: Setup git and check branch
id: setup
env:
GH_TOKEN: ${{ steps.app-token.outputs.token }}
APP_SLUG: ${{ steps.app-token.outputs.app-slug }}
shell: bash
run: |
set -euo pipefail
if [[ ! "${APP_SLUG}" =~ ^[a-z0-9-]+$ ]]; then
echo "::error::Invalid app slug '${APP_SLUG}'."
exit 1
fi
user_id=$(gh api "/users/${APP_SLUG}[bot]" --jq .id)
git config --global user.name "${APP_SLUG}[bot]"
git config --global user.email "${user_id}+${APP_SLUG}[bot]@users.noreply.github.com"
if gh api "repos/${GITHUB_REPOSITORY}/branches/quarto-tests" >/dev/null 2>&1; then
echo "branch-exists=true" >> "${GITHUB_OUTPUT}"
else
echo "branch-exists=false" >> "${GITHUB_OUTPUT}"
fi
- name: Checkout quarto-tests branch
if: steps.setup.outputs.branch-exists == 'true'
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
with:
ref: quarto-tests
token: ${{ steps.app-token.outputs.token }}
- name: Initialise if branch does not exist
if: steps.setup.outputs.branch-exists == 'false'
env:
GH_TOKEN: ${{ steps.app-token.outputs.token }}
shell: bash
run: |
set -euo pipefail
gh auth setup-git
git init
git remote add origin "https://github.com/${GITHUB_REPOSITORY}.git"
git checkout --orphan quarto-tests
echo '{}' > test-results.json
git add test-results.json
git commit -m "ci: initialise quarto-tests branch"
if ! git push -u origin quarto-tests; then
echo "::warning::Branch initialisation raced with another run. Fetching existing branch."
git fetch origin quarto-tests
git checkout -B quarto-tests origin/quarto-tests
fi
- name: Download all result artefacts
uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7
with:
path: artefacts
pattern: results-batch-*
- name: Download all log artefacts
uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7
with:
path: logs
pattern: logs-batch-*
merge-multiple: true
- name: Checkout scripts
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
with:
sparse-checkout: .github/scripts/test-extensions
sparse-checkout-cone-mode: false
path: _scripts
- name: Publish results
env:
SKIPPED: ${{ needs.build-matrix.outputs.skipped }}
shell: bash
run: bash "${GITHUB_WORKSPACE}/_scripts/.github/scripts/test-extensions/publish-results.sh"