From 5edcc6d24c9680b3ee49c9c6aa53cb7018ec6252 Mon Sep 17 00:00:00 2001 From: Amy Lin Date: Fri, 24 Jul 2026 13:43:22 -0500 Subject: [PATCH] ci: enforce quarto extensions render only what they serve --- .github/actions/lint-extension/action.yml | 62 +++++++++++++++++++ .github/workflows/extensions.yml | 1 + CONTRIBUTING.md | 21 +++++++ extensions/pqr/CHANGELOG.md | 6 ++ extensions/pqr/_quarto.yml | 6 ++ extensions/pqr/manifest.json | 5 +- .../quarto-document/connect-extension.qmd | 8 --- .../quarto-stock-report-python/CHANGELOG.md | 7 +++ .../quarto-stock-report-python/_quarto.yml | 5 ++ .../connect-extension.qmd | 8 --- .../quarto-stock-report-python/manifest.json | 4 +- extensions/quarto-stock-report-r/CHANGELOG.md | 6 ++ extensions/quarto-stock-report-r/_quarto.yml | 5 ++ .../quarto-stock-report-r/manifest.json | 6 +- 14 files changed, 128 insertions(+), 22 deletions(-) create mode 100644 extensions/pqr/_quarto.yml delete mode 100644 extensions/quarto-document/connect-extension.qmd delete mode 100644 extensions/quarto-stock-report-python/connect-extension.qmd diff --git a/.github/actions/lint-extension/action.yml b/.github/actions/lint-extension/action.yml index 4492d16c..d29bfa06 100644 --- a/.github/actions/lint-extension/action.yml +++ b/.github/actions/lint-extension/action.yml @@ -150,3 +150,65 @@ runs: exit 1 fi shell: bash + + # The render-set guard below only applies to Quarto content. + - name: Detect Quarto appmode + id: quarto-appmode + run: | + APPMODE=$(jq -r '.metadata.appmode // ""' ./extensions/${{ inputs.extension-name }}/manifest.json) + if [[ "$APPMODE" == quarto-* ]]; then + echo "is-quarto=true" >> "$GITHUB_OUTPUT" + else + echo "is-quarto=false" >> "$GITHUB_OUTPUT" + fi + shell: bash + + - name: Set up Quarto + if: steps.quarto-appmode.outputs.is-quarto == 'true' + uses: quarto-dev/quarto-actions/setup@v2 + + # Guards against the served-document bug (#392): Quarto renders every sibling + # Markdown file (e.g. CHANGELOG.md) as its own page, and with no pinned + # primary the default served document can silently flip to that page (e.g. + # CHANGELOG.html) instead of the entrypoint. Every build-time check stays + # green, so nothing else catches it. The standard is that a Quarto extension + # renders only what it serves: single-document content pins project.render to + # its entrypoint, while websites render their own pages. So this fails when a + # non-website extension renders a sibling Markdown page. + - name: Check Quarto render set + if: steps.quarto-appmode.outputs.is-quarto == 'true' + run: | + set -euo pipefail + EXT_DIR=./extensions/${{ inputs.extension-name }} + + # Without a _quarto.yml the directory is not a project, so Connect renders + # only the entrypoint and no sibling Markdown becomes a page. + if ! INSPECT=$(quarto inspect "$EXT_DIR" 2>/dev/null); then + echo "Not a Quarto project; only the entrypoint renders. OK." + exit 0 + fi + + # Websites legitimately render many pages and serve index.html by convention. + PROJECT_TYPE=$(jq -r '.config.project.type // "default"' <<< "$INSPECT") + if [ "$PROJECT_TYPE" == "website" ]; then + echo "Website project; many pages are expected. OK." + exit 0 + fi + + # Rendered input basenames. + INPUTS=$(jq -r '.files.input[] | split("/") | last' <<< "$INSPECT") + COUNT=$(grep -c . <<< "$INPUTS") + + # A lone document is unambiguously the served document. Otherwise a + # rendered sibling Markdown file (Quarto ignores README.md) is a stray + # page that could become the served document. + STRAY_MD=$(grep -iE '\.md$' <<< "$INPUTS" | grep -viE '^README\.md$' || true) + if [ "$COUNT" -gt 1 ] && [ -n "$STRAY_MD" ]; then + echo "Error: Quarto extension '${{ inputs.extension-name }}' renders sibling Markdown page(s):" + echo "$STRAY_MD" + echo "Pin the entrypoint in _quarto.yml (project.render) so only it renders and becomes the served document." + exit 1 + fi + + echo "No stray Markdown pages render. OK." + shell: bash diff --git a/.github/workflows/extensions.yml b/.github/workflows/extensions.yml index 782414cc..75e22929 100644 --- a/.github/workflows/extensions.yml +++ b/.github/workflows/extensions.yml @@ -35,6 +35,7 @@ jobs: _infra: - integration/** - .github/actions/connect-integration-test/** + - .github/actions/lint-extension/** - .github/workflows/connect-integration-tests.yml - .github/workflows/extensions.yml quarto-stock-report-python: extensions/quarto-stock-report-python/** diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 4a71e5c0..546edc1b 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -442,3 +442,24 @@ to document changes made on each release. A recommended format is the [keep a changelog](https://keepachangelog.com/en/1.1.0/) format and ahering to the [Semantic Versioning](https://semver.org/) guidelines. + +### Quarto: render only what you serve + +Quarto renders every sibling Markdown file as its own page. It ignores +`README.md`, but not `CHANGELOG.md`. So a `CHANGELOG.md` in a Quarto extension +is rendered into a `CHANGELOG.html` page, and with no pinned primary the default +served document can silently flip to it instead of your entrypoint, with every +build-time check still passing. + +A Quarto extension should render only the document it serves. Pin the entrypoint +in `_quarto.yml` so nothing else renders: + +```yaml +project: + render: + - index.qmd # or your real entrypoint, e.g. script.py or script.R +``` + +Websites are the exception: they render many pages and serve `index.html` by +convention. Linting fails any non-website extension that renders a sibling +Markdown page like a `CHANGELOG`. diff --git a/extensions/pqr/CHANGELOG.md b/extensions/pqr/CHANGELOG.md index 88834fc6..1b331f7f 100644 --- a/extensions/pqr/CHANGELOG.md +++ b/extensions/pqr/CHANGELOG.md @@ -5,6 +5,12 @@ All notable changes to the Quarto Document with Python and R extension will be d The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [0.1.4] - 2026-07-24 + +### Changed + +- Added a `_quarto.yml` pinning `project.render` to `index.qmd` so only the document renders, preventing a sibling `CHANGELOG.md` from rendering as a stray page. (#442) + ## [0.1.3] - 2026-06-22 ### Fixed diff --git a/extensions/pqr/_quarto.yml b/extensions/pqr/_quarto.yml new file mode 100644 index 00000000..7a04aaa4 --- /dev/null +++ b/extensions/pqr/_quarto.yml @@ -0,0 +1,6 @@ +project: + # Render only the document so it is the unambiguous primary document. + # Without this, Quarto also renders sibling Markdown (e.g. CHANGELOG.md) + # and the default served document can become that file instead. + render: + - index.qmd diff --git a/extensions/pqr/manifest.json b/extensions/pqr/manifest.json index bc80737b..a6004511 100644 --- a/extensions/pqr/manifest.json +++ b/extensions/pqr/manifest.json @@ -1264,6 +1264,9 @@ ".Rprofile": { "checksum": "57f9047cb06fdbef821b61f49e21406e" }, + "_quarto.yml": { + "checksum": "6087acf57476d9f95b353c3db3ae0fb2" + }, "index.qmd": { "checksum": "e3c9e1907f7e77e8d7da0492e92d9def" }, @@ -1289,6 +1292,6 @@ "category": "example", "tags": ["python", "quarto", "r"], "minimumConnectVersion": "2025.04.0", - "version": "0.1.3" + "version": "0.1.4" } } diff --git a/extensions/quarto-document/connect-extension.qmd b/extensions/quarto-document/connect-extension.qmd deleted file mode 100644 index 959cbf2f..00000000 --- a/extensions/quarto-document/connect-extension.qmd +++ /dev/null @@ -1,8 +0,0 @@ ---- -title: Quarto Document -categories: - - quarto ---- - -This Quarto example shows how a basic document can present diagrams and -interactivity. diff --git a/extensions/quarto-stock-report-python/CHANGELOG.md b/extensions/quarto-stock-report-python/CHANGELOG.md index 82f3ec9b..b6d26416 100644 --- a/extensions/quarto-stock-report-python/CHANGELOG.md +++ b/extensions/quarto-stock-report-python/CHANGELOG.md @@ -5,6 +5,13 @@ All notable changes to the Quarto Stock Report using Python extension will be do The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [1.0.6] - 2026-07-24 + +### Changed + +- Pinned the Quarto `project.render` list to `index.qmd` so only the report renders, preventing a sibling `CHANGELOG.md` from rendering as a stray page. (#442) +- Removed `connect-extension.qmd`, a superseded legacy gallery-metadata file that is no longer used. (#442) + ## [1.0.5] - 2026-06-15 ### Changed diff --git a/extensions/quarto-stock-report-python/_quarto.yml b/extensions/quarto-stock-report-python/_quarto.yml index e84f266b..a7e1ce01 100644 --- a/extensions/quarto-stock-report-python/_quarto.yml +++ b/extensions/quarto-stock-report-python/_quarto.yml @@ -1,2 +1,7 @@ project: title: Stock Report + # Render only the report so it is the unambiguous primary document. + # Without this, Quarto also renders sibling Markdown (e.g. CHANGELOG.md) + # and the default served document can become that file instead. + render: + - index.qmd diff --git a/extensions/quarto-stock-report-python/connect-extension.qmd b/extensions/quarto-stock-report-python/connect-extension.qmd deleted file mode 100644 index a1e12c88..00000000 --- a/extensions/quarto-stock-report-python/connect-extension.qmd +++ /dev/null @@ -1,8 +0,0 @@ ---- -title: Quarto Stock Report using Python -categories: - - python - - quarto ---- - -This stock report is generated using Quarto and Python. It is an example of how you might automate regular updates using a data source. The report also generates a custom email, sharing the results directly to stakeholders. diff --git a/extensions/quarto-stock-report-python/manifest.json b/extensions/quarto-stock-report-python/manifest.json index d2c109fa..a8a398df 100644 --- a/extensions/quarto-stock-report-python/manifest.json +++ b/extensions/quarto-stock-report-python/manifest.json @@ -12,7 +12,7 @@ "minimumConnectVersion": "2025.04.0", "category": "example", "tags": ["quarto", "python"], - "version": "1.0.5" + "version": "1.0.6" }, "environment": { "python": { @@ -38,7 +38,7 @@ }, "files": { "_quarto.yml": { - "checksum": "35b4c4c58cce875b126683c525ad40f4" + "checksum": "c18e19d86e99c788e4ccfabf68179aa2" }, "index.qmd": { "checksum": "7eeaeddb32075ffea02ef77ada5b719b" diff --git a/extensions/quarto-stock-report-r/CHANGELOG.md b/extensions/quarto-stock-report-r/CHANGELOG.md index 3f745534..9fd2eaf8 100644 --- a/extensions/quarto-stock-report-r/CHANGELOG.md +++ b/extensions/quarto-stock-report-r/CHANGELOG.md @@ -5,6 +5,12 @@ All notable changes to the Quarto Stock Report using R extension will be documen The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [1.0.5] - 2026-07-24 + +### Changed + +- Pinned the Quarto `project.render` list to `index.qmd` so only the report renders, preventing a sibling `CHANGELOG.md` from rendering as a stray page. (#442) + ## [1.0.4] - 2026-06-11 ### Changed diff --git a/extensions/quarto-stock-report-r/_quarto.yml b/extensions/quarto-stock-report-r/_quarto.yml index e84f266b..a7e1ce01 100644 --- a/extensions/quarto-stock-report-r/_quarto.yml +++ b/extensions/quarto-stock-report-r/_quarto.yml @@ -1,2 +1,7 @@ project: title: Stock Report + # Render only the report so it is the unambiguous primary document. + # Without this, Quarto also renders sibling Markdown (e.g. CHANGELOG.md) + # and the default served document can become that file instead. + render: + - index.qmd diff --git a/extensions/quarto-stock-report-r/manifest.json b/extensions/quarto-stock-report-r/manifest.json index f2c55802..22d80f76 100644 --- a/extensions/quarto-stock-report-r/manifest.json +++ b/extensions/quarto-stock-report-r/manifest.json @@ -32,7 +32,7 @@ "category": "example", "minimumConnectVersion": "2025.04.0", "tags": ["quarto", "r"], - "version": "1.0.4" + "version": "1.0.5" }, "packages": { "DT": { @@ -2419,13 +2419,13 @@ }, "files": { "_quarto.yml": { - "checksum": "35b4c4c58cce875b126683c525ad40f4" + "checksum": "c18e19d86e99c788e4ccfabf68179aa2" }, ".output_metadata.json": { "checksum": "a335b9151c7b39bed18fd0f56928ce68" }, "CHANGELOG.md": { - "checksum": "6adb848a545277816a79a36437af87db" + "checksum": "659505e735500a6cdd3480f30d401d8d" }, "data.csv": { "checksum": "d64607673ef065841a9d355317ec8d7b"