From 736f1e9c7b0ff3958912980a96985cc03803b9c7 Mon Sep 17 00:00:00 2001 From: VisruthSK Date: Thu, 8 Oct 2026 19:45:27 -0700 Subject: [PATCH 01/27] Genreated roadmap implementation --- .github/workflows/pkgdown.yaml | 133 +------------------------------ DESCRIPTION | 5 ++ NAMESPACE | 6 +- R/Hello.R | 13 --- R/build_site.R | 39 +++++++++ R/roadmap.R | 141 +++++++++++++++++++++++++++++++++ R/use_pkgdown_gha.R | 17 ++++ inst/pkgdown/_pkgdown.yml | 9 +++ inst/pkgdown/extra.scss | 26 +++++- inst/workflows/pkgdown.yaml | 128 ++++++++++++++++++++++++++++++ man/build_site.Rd | 24 ++++++ man/hello.Rd | 21 ----- man/roadmap.Rd | 21 +++++ man/roadmap_override.Rd | 19 +++++ man/use_pkgdown_gha.Rd | 17 ++++ man/use_roadmap.Rd | 20 +++++ vignettes/Setup.qmd | 15 +++- 17 files changed, 485 insertions(+), 169 deletions(-) mode change 100644 => 120000 .github/workflows/pkgdown.yaml delete mode 100644 R/Hello.R create mode 100644 R/build_site.R create mode 100644 R/roadmap.R create mode 100644 R/use_pkgdown_gha.R create mode 100644 inst/workflows/pkgdown.yaml create mode 100644 man/build_site.Rd delete mode 100644 man/hello.Rd create mode 100644 man/roadmap.Rd create mode 100644 man/roadmap_override.Rd create mode 100644 man/use_pkgdown_gha.Rd create mode 100644 man/use_roadmap.Rd diff --git a/.github/workflows/pkgdown.yaml b/.github/workflows/pkgdown.yaml deleted file mode 100644 index 5778f5f..0000000 --- a/.github/workflows/pkgdown.yaml +++ /dev/null @@ -1,132 +0,0 @@ -# Workflow derived from https://github.com/r-lib/actions/tree/v2/examples -# Need help debugging build failures? Start at https://github.com/r-lib/actions#where-to-find-help -on: - # build dev site on merged pushes - push: - branches: [main, master] - pull_request: - types: [opened, reopened, synchronize, labeled, closed] - # build full site on releases - release: - types: [published] - workflow_dispatch: - -name: pkgdown.yaml - -jobs: - pkgdown: - # For PRs, only run when the PR has the build-website label. - if: >- - github.event_name != 'pull_request' || - ( - github.event.action != 'closed' && - contains(github.event.pull_request.labels.*.name, 'build-website') && - (github.event.action != 'labeled' || github.event.label.name == 'build-website') - ) - runs-on: ubuntu-latest - concurrency: - group: pkgdown-${{ github.event_name == 'pull_request' && github.event.pull_request.number || 'production' }} - cancel-in-progress: true - env: - GITHUB_PAT: ${{ secrets.GITHUB_TOKEN }} - NOT_CRAN: "true" - permissions: - contents: write - steps: - - uses: actions/checkout@v7 - - - uses: r-lib/actions/setup-pandoc@v2 - - - uses: r-lib/actions/setup-r@v2 - with: - use-public-rspm: true - - - uses: r-lib/actions/setup-r-dependencies@v2 - with: - extra-packages: any::pkgdown, local::., stan-dev/pkgdown-config - - - name: Build site - env: - PR_NUMBER: ${{ github.event.pull_request.number }} - run: | - override <- list() - - pr_number <- Sys.getenv("PR_NUMBER") - if (nzchar(pr_number)) { - pkg <- pkgdown::as_pkgdown(".") - - if (pkg$development$mode == "devel") { - Sys.setenv(PKGDOWN_DEV_MODE = "unreleased") - } - - override <- list( - url = paste0(pkg$meta$url, "/pr/", pr_number), - template = list( - params = list(noindex = TRUE) - ) - ) - } - - pkgdown::build_site_github_pages( - lazy = FALSE, - run_dont_run = TRUE, - new_process = TRUE, - override = override - ) - shell: Rscript {0} - - - name: Deploy PR preview - # Forks cannot deploy the site - if: > - github.event_name == 'pull_request' && - github.event.pull_request.head.repo.full_name == github.repository - uses: JamesIves/github-pages-deploy-action@v4 - with: - branch: gh-pages - folder: docs - target-folder: pr/${{ github.event.pull_request.number }} - clean: true - force: false - - - name: Deploy site - if: github.event_name != 'pull_request' - uses: JamesIves/github-pages-deploy-action@v4 - with: - branch: gh-pages - folder: docs - clean: false - force: false - cleanup-preview: - if: >- - github.event_name == 'pull_request' && github.event.action == 'closed' - && github.event.pull_request.head.repo.full_name == github.repository - runs-on: ubuntu-latest - - concurrency: - group: pkgdown-${{ github.event.pull_request.number }} - cancel-in-progress: true - - permissions: - contents: write - - steps: - - uses: actions/checkout@v7 - with: - ref: gh-pages - - - name: Remove PR preview - env: - PR_NUMBER: ${{ github.event.pull_request.number }} - run: | - git config user.name "github-actions[bot]" - git config user.email "41898282+github-actions[bot]@users.noreply.github.com" - - git rm -r --ignore-unmatch "pr/$PR_NUMBER" - - if git diff --cached --quiet; then - exit 0 - fi - - git commit -m "Remove pkgdown preview for PR #$PR_NUMBER" - git pull --rebase origin gh-pages - git push origin HEAD:gh-pages diff --git a/.github/workflows/pkgdown.yaml b/.github/workflows/pkgdown.yaml new file mode 120000 index 0000000..53f5a1e --- /dev/null +++ b/.github/workflows/pkgdown.yaml @@ -0,0 +1 @@ +../../inst/workflows/pkgdown.yaml \ No newline at end of file diff --git a/DESCRIPTION b/DESCRIPTION index e5d22d1..f247681 100644 --- a/DESCRIPTION +++ b/DESCRIPTION @@ -11,6 +11,11 @@ License: BSD_3_clause + file LICENSE URL: https://mc-stan.org/pkgdown-config, https://mc-stan.org/pkgdown-config BugReports: https://github.com/stan-dev/pkgdown-config/issues/ +Depends: + pkgdown (>= 2.1.0) +Imports: + gh, + utils Suggests: knitr, quarto diff --git a/NAMESPACE b/NAMESPACE index 01843dc..eeff9a3 100644 --- a/NAMESPACE +++ b/NAMESPACE @@ -1,3 +1,7 @@ # Generated by roxygen2: do not edit by hand -export(hello) +export(build_site) +export(roadmap) +export(roadmap_override) +export(use_pkgdown_gha) +export(use_roadmap) diff --git a/R/Hello.R b/R/Hello.R deleted file mode 100644 index 1f64a84..0000000 --- a/R/Hello.R +++ /dev/null @@ -1,13 +0,0 @@ -#' Hello! -#' -#' Trivial example for documentation. -#' -#' @param name Character to greet. Defaults to `world`. -#' @return Character greeting. -#' @examples -#' hello() -#' hello("Stan") -#' @export -hello <- function(name = "world") { - paste0("Hello, ", name, "!") -} diff --git a/R/build_site.R b/R/build_site.R new file mode 100644 index 0000000..82ddd89 --- /dev/null +++ b/R/build_site.R @@ -0,0 +1,39 @@ +#' Build a Stan pkgdown site +#' +#' Writes the roadmap article and adds the roadmap to the navbar when +#' `roadmap: enabled: true` is set, then calls [pkgdown::build_site()] with all +#' other arguments. +#' +#' @inheritParams pkgdown::build_site +#' @param ... Passed on to [pkgdown::build_site()]. +#' @param override Passed on to [pkgdown::build_site()], merged over the +#' navbar override from `roadmap_override()`. +#' @return The result of [pkgdown::build_site()], invisibly. +#' @export +build_site <- function(pkg = ".", ..., override = list()) { + config <- pkgdown::as_pkgdown(pkg) + use_roadmap(config) + pkgdown::build_site( + pkg, + ..., + override = utils::modifyList(roadmap_override(config), override) + ) +} + +#' Navbar override for the roadmap +#' +#' Inserts `roadmap` into the navbar after `news` when the roadmap is enabled. +#' Used by [build_site()] and the GitHub Action. +#' +#' @inheritParams use_roadmap +#' @return A pkgdown override list, empty when the roadmap is disabled. +#' @export +roadmap_override <- function(pkg = ".") { + pkg <- pkgdown::as_pkgdown(pkg) + if (!isTRUE(pkg$meta$roadmap$enabled)) { + return(list()) + } + left <- pkg$meta$navbar$structure$left + after <- match("news", left, nomatch = length(left)) + list(navbar = list(structure = list(left = append(left, "roadmap", after)))) +} diff --git a/R/roadmap.R b/R/roadmap.R new file mode 100644 index 0000000..86dae75 --- /dev/null +++ b/R/roadmap.R @@ -0,0 +1,141 @@ +#' Print the roadmap +#' +#' Renders the issues of the open GitHub milestones of the package as pandoc +#' markdown, one section per milestone. This is the body of the article written +#' by [use_roadmap()]. +#' +#' @param pkg Path to the package, or a pkgdown object from +#' [pkgdown::as_pkgdown()]. The default is two levels up, which is the working +#' directory pkgdown renders articles from. +#' @return `NULL`, invisibly. Called for its printed output. +#' @export +roadmap <- function(pkg = "../..") { + pkg <- pkgdown::as_pkgdown(pkg) + config <- pkg$meta$roadmap + repo <- gsub("^https://github.com/|/$", "", pkg$repo$url$home) + issues <- gh::gh( + "/repos/{repo}/issues", + repo = repo, + milestone = "*", + state = "all", + .limit = Inf + ) + issues <- Filter( + function(issue) { + is.null(issue$pull_request) && + issue$milestone$state == "open" && + !roadmap_excluded(issue, config$exclude) + }, + issues + ) + closed <- vapply(issues, function(issue) issue$state == "closed", NA) + numbers <- vapply(issues, `[[`, 0L, "number") + issues <- issues[order(closed, numbers)] + milestones <- vapply(issues, function(issue) issue$milestone$number, 0L) + + sections <- lapply(split(issues, milestones), roadmap_milestone, config) + cat(unlist(sections), sep = "\n") +} + +#' Write the roadmap article +#' +#' Writes `vignettes/articles/roadmap.Rmd`, which calls [roadmap()], when +#' `roadmap: enabled: true` is set in `_pkgdown.yml`. +#' +#' @param pkg Path to the package, or a pkgdown object from +#' [pkgdown::as_pkgdown()]. +#' @return The path of the article, invisibly, or `NULL` invisibly if the +#' roadmap is disabled. +#' @export +use_roadmap <- function(pkg = ".") { + pkg <- pkgdown::as_pkgdown(pkg) + config <- pkg$meta$roadmap + if (!isTRUE(config$enabled)) { + return(invisible()) + } + if (!identical(config$format, "rmd")) { + stop("Only `roadmap: format: rmd` is supported.", call. = FALSE) + } + path <- file.path(pkg$src_path, "vignettes", "articles", "roadmap.Rmd") + dir.create(dirname(path), recursive = TRUE, showWarnings = FALSE) + writeLines( + c( + "---", + "title: Roadmap", + "---", + "", + "```{r, echo=FALSE, results='asis'}", + "pkgdownconfig::roadmap()", + "```" + ), + path + ) + invisible(path) +} + +#' Markdown for one milestone +#' +#' @param issues The issues of a single milestone, in display order. +#' @param config The `roadmap` section of the merged pkgdown config. +#' @return A character vector of markdown lines. +#' @noRd +roadmap_milestone <- function(issues, config) { + milestone <- issues[[1]]$milestone + override <- config$overrides$milestones[[as.character(milestone$number)]] + c( + paste("##", override$title %||% milestone$title), + "", + trimws(override$description %||% milestone$description), + "", + "::::: {.rm-cards}", + unlist(lapply(issues, roadmap_card, config$overrides$issues)), + ":::::", + "" + ) +} + +#' Whether an issue is excluded from the roadmap +#' +#' @param issue An issue as returned by the GitHub API. +#' @param exclude The `exclude` section of the roadmap config. +#' @return `TRUE` or `FALSE`. +#' @noRd +roadmap_excluded <- function(issue, exclude) { + labels <- vapply(issue$labels, `[[`, "", "name") + patterns <- utils::glob2rx(unlist(exclude$labels)) + as.character(issue$number) %in% unlist(exclude$issues) || + any(vapply(patterns, function(p) any(grepl(p, labels)), NA)) +} + +#' Markdown for one issue card +#' +#' @param issue An issue as returned by the GitHub API. +#' @param overrides The `overrides.issues` section of the roadmap config. +#' @return A character vector of markdown lines. +#' @noRd +roadmap_card <- function(issue, overrides) { + closed <- issue$state == "closed" + c( + if (closed) ":::: {.rm-card .rm-closed}" else ":::: {.rm-card}", + "", + sprintf( + "#### [%s%s](%s)", + if (closed) "✓ " else "", + issue$title, + issue$html_url + ), + "", + trimws(overrides[[as.character(issue$number)]]$description), + "", + "::::", + "" + ) +} + +#' Fall back to a default when `x` is `NULL` +#' +#' @param x A value. +#' @param y The default. +#' @return `x` if not `NULL`, otherwise `y`. +#' @noRd +`%||%` <- function(x, y) if (is.null(x)) y else x diff --git a/R/use_pkgdown_gha.R b/R/use_pkgdown_gha.R new file mode 100644 index 0000000..9547780 --- /dev/null +++ b/R/use_pkgdown_gha.R @@ -0,0 +1,17 @@ +#' Copy the Stan pkgdown GitHub Action +#' +#' Copies this package's `pkgdown.yaml` workflow into `.github/workflows`. +#' +#' @param pkg Path to the package. +#' @return The path of the workflow, invisibly. +#' @export +use_pkgdown_gha <- function(pkg = ".") { + path <- file.path(pkg, ".github", "workflows", "pkgdown.yaml") + dir.create(dirname(path), recursive = TRUE, showWarnings = FALSE) + file.copy( + system.file("workflows", "pkgdown.yaml", package = "pkgdownconfig"), + path, + overwrite = TRUE + ) + invisible(path) +} diff --git a/inst/pkgdown/_pkgdown.yml b/inst/pkgdown/_pkgdown.yml index 778df54..7e8131c 100644 --- a/inst/pkgdown/_pkgdown.yml +++ b/inst/pkgdown/_pkgdown.yml @@ -86,6 +86,9 @@ navbar: href: https://mc-stan.org/rstantools - text: shinystan href: https://mc-stan.org/shinystan + roadmap: + text: Roadmap + href: articles/roadmap.html stan: text: About Stan href: https://mc-stan.org/about/ @@ -102,3 +105,9 @@ home: links: - text: Ask a question href: http://discourse.mc-stan.org/ + +roadmap: + enabled: false + format: rmd + exclude: + labels: [internal*, wontfix, invalid] diff --git a/inst/pkgdown/extra.scss b/inst/pkgdown/extra.scss index 681f004..701d3de 100644 --- a/inst/pkgdown/extra.scss +++ b/inst/pkgdown/extra.scss @@ -52,4 +52,28 @@ pre { margin-left: .4rem; line-height: 1; font-weight: 800; -} \ No newline at end of file +} + +.rm-cards { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(16rem, 1fr)); + gap: 1rem; + margin: 1rem 0 2rem; +} + +.rm-card { + background: var(--bs-tertiary-bg); + border: 1px solid var(--bs-border-color); + border-left: 4px solid var(--bs-primary); + border-radius: .4rem; + padding: .9rem 1rem .6rem; + + h4 { + margin: 0 0 .4rem; + font-size: 1.05em; + } + + &.rm-closed { + opacity: .65; + } +} diff --git a/inst/workflows/pkgdown.yaml b/inst/workflows/pkgdown.yaml new file mode 100644 index 0000000..babb0ff --- /dev/null +++ b/inst/workflows/pkgdown.yaml @@ -0,0 +1,128 @@ +# Workflow derived from https://github.com/r-lib/actions/tree/v2/examples +# Need help debugging build failures? Start at https://github.com/r-lib/actions#where-to-find-help +on: + # build dev site on merged pushes + push: + branches: [main, master] + pull_request: + types: [opened, reopened, synchronize, labeled, closed] + # build full site on releases + release: + types: [published] + workflow_dispatch: + +name: pkgdown.yaml + +jobs: + pkgdown: + # For PRs, only run when the PR has the build-website label. + if: >- + github.event_name != 'pull_request' || + ( + github.event.action != 'closed' && + contains(github.event.pull_request.labels.*.name, 'build-website') && + (github.event.action != 'labeled' || github.event.label.name == 'build-website') + ) + runs-on: ubuntu-latest + concurrency: + group: pkgdown-${{ github.event_name == 'pull_request' && github.event.pull_request.number || 'production' }} + cancel-in-progress: true + env: + GITHUB_PAT: ${{ secrets.GITHUB_TOKEN }} + NOT_CRAN: "true" + permissions: + contents: write + steps: + - uses: actions/checkout@v7 + + - uses: r-lib/actions/setup-pandoc@v2 + + - uses: r-lib/actions/setup-r@v2 + with: + use-public-rspm: true + + - uses: r-lib/actions/setup-r-dependencies@v2 + with: + extra-packages: any::pkgdown, local::., stan-dev/pkgdown-config + + - name: Build site + env: + PR_NUMBER: ${{ github.event.pull_request.number }} + run: | + pkg <- pkgdown::as_pkgdown(".") + pkgdownconfig::use_roadmap(pkg) + override <- pkgdownconfig::roadmap_override(pkg) + + pr_number <- Sys.getenv("PR_NUMBER") + if (nzchar(pr_number)) { + if (pkg$development$mode == "devel") { + Sys.setenv(PKGDOWN_DEV_MODE = "unreleased") + } + + override$url <- paste0(pkg$meta$url, "/pr/", pr_number) + override$template$params$noindex <- TRUE + } + + pkgdown::build_site_github_pages( + lazy = FALSE, + run_dont_run = TRUE, + new_process = TRUE, + override = override + ) + shell: Rscript {0} + + - name: Deploy PR preview + # Forks cannot deploy the site + if: > + github.event_name == 'pull_request' && + github.event.pull_request.head.repo.full_name == github.repository + uses: JamesIves/github-pages-deploy-action@v4 + with: + branch: gh-pages + folder: docs + target-folder: pr/${{ github.event.pull_request.number }} + clean: true + force: false + + - name: Deploy site + if: github.event_name != 'pull_request' + uses: JamesIves/github-pages-deploy-action@v4 + with: + branch: gh-pages + folder: docs + clean: false + force: false + cleanup-preview: + if: >- + github.event_name == 'pull_request' && github.event.action == 'closed' + && github.event.pull_request.head.repo.full_name == github.repository + runs-on: ubuntu-latest + + concurrency: + group: pkgdown-${{ github.event.pull_request.number }} + cancel-in-progress: true + + permissions: + contents: write + + steps: + - uses: actions/checkout@v7 + with: + ref: gh-pages + + - name: Remove PR preview + env: + PR_NUMBER: ${{ github.event.pull_request.number }} + run: | + git config user.name "github-actions[bot]" + git config user.email "41898282+github-actions[bot]@users.noreply.github.com" + + git rm -r --ignore-unmatch "pr/$PR_NUMBER" + + if git diff --cached --quiet; then + exit 0 + fi + + git commit -m "Remove pkgdown preview for PR #$PR_NUMBER" + git pull --rebase origin gh-pages + git push origin HEAD:gh-pages diff --git a/man/build_site.Rd b/man/build_site.Rd new file mode 100644 index 0000000..79a24bc --- /dev/null +++ b/man/build_site.Rd @@ -0,0 +1,24 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/build_site.R +\name{build_site} +\alias{build_site} +\title{Build a Stan pkgdown site} +\usage{ +build_site(pkg = ".", ..., override = list()) +} +\arguments{ +\item{pkg}{Path to package.} + +\item{...}{Passed on to \code{\link[pkgdown:build_site]{pkgdown::build_site()}}.} + +\item{override}{Passed on to \code{\link[pkgdown:build_site]{pkgdown::build_site()}}, merged over the +navbar override from \code{roadmap_override()}.} +} +\value{ +The result of \code{\link[pkgdown:build_site]{pkgdown::build_site()}}, invisibly. +} +\description{ +Writes the roadmap article and adds the roadmap to the navbar when +\code{roadmap: enabled: true} is set, then calls \code{\link[pkgdown:build_site]{pkgdown::build_site()}} with all +other arguments. +} diff --git a/man/hello.Rd b/man/hello.Rd deleted file mode 100644 index 8c41a23..0000000 --- a/man/hello.Rd +++ /dev/null @@ -1,21 +0,0 @@ -% Generated by roxygen2: do not edit by hand -% Please edit documentation in R/Hello.R -\name{hello} -\alias{hello} -\title{Hello!} -\usage{ -hello(name = "world") -} -\arguments{ -\item{name}{Character to greet. Defaults to \code{world}.} -} -\value{ -Character greeting. -} -\description{ -Trivial example for documentation. -} -\examples{ -hello() -hello("Stan") -} diff --git a/man/roadmap.Rd b/man/roadmap.Rd new file mode 100644 index 0000000..a93b05c --- /dev/null +++ b/man/roadmap.Rd @@ -0,0 +1,21 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/roadmap.R +\name{roadmap} +\alias{roadmap} +\title{Print the roadmap} +\usage{ +roadmap(pkg = "../..") +} +\arguments{ +\item{pkg}{Path to the package, or a pkgdown object from +\code{\link[pkgdown:as_pkgdown]{pkgdown::as_pkgdown()}}. The default is two levels up, which is the working +directory pkgdown renders articles from.} +} +\value{ +\code{NULL}, invisibly. Called for its printed output. +} +\description{ +Renders the issues of the open GitHub milestones of the package as pandoc +markdown, one section per milestone. This is the body of the article written +by \code{\link[=use_roadmap]{use_roadmap()}}. +} diff --git a/man/roadmap_override.Rd b/man/roadmap_override.Rd new file mode 100644 index 0000000..eff65fa --- /dev/null +++ b/man/roadmap_override.Rd @@ -0,0 +1,19 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/build_site.R +\name{roadmap_override} +\alias{roadmap_override} +\title{Navbar override for the roadmap} +\usage{ +roadmap_override(pkg = ".") +} +\arguments{ +\item{pkg}{Path to the package, or a pkgdown object from +\code{\link[pkgdown:as_pkgdown]{pkgdown::as_pkgdown()}}.} +} +\value{ +A pkgdown override list, empty when the roadmap is disabled. +} +\description{ +Inserts \code{roadmap} into the navbar after \code{news} when the roadmap is enabled. +Used by \code{\link[=build_site]{build_site()}} and the GitHub Action. +} diff --git a/man/use_pkgdown_gha.Rd b/man/use_pkgdown_gha.Rd new file mode 100644 index 0000000..f4fcba4 --- /dev/null +++ b/man/use_pkgdown_gha.Rd @@ -0,0 +1,17 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/use_pkgdown_gha.R +\name{use_pkgdown_gha} +\alias{use_pkgdown_gha} +\title{Copy the Stan pkgdown GitHub Action} +\usage{ +use_pkgdown_gha(pkg = ".") +} +\arguments{ +\item{pkg}{Path to the package.} +} +\value{ +The path of the workflow, invisibly. +} +\description{ +Copies this package's \code{pkgdown.yaml} workflow into \code{.github/workflows}. +} diff --git a/man/use_roadmap.Rd b/man/use_roadmap.Rd new file mode 100644 index 0000000..d0f1bea --- /dev/null +++ b/man/use_roadmap.Rd @@ -0,0 +1,20 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/roadmap.R +\name{use_roadmap} +\alias{use_roadmap} +\title{Write the roadmap article} +\usage{ +use_roadmap(pkg = ".") +} +\arguments{ +\item{pkg}{Path to the package, or a pkgdown object from +\code{\link[pkgdown:as_pkgdown]{pkgdown::as_pkgdown()}}.} +} +\value{ +The path of the article, invisibly, or \code{NULL} invisibly if the +roadmap is disabled. +} +\description{ +Writes \code{vignettes/articles/roadmap.Rmd}, which calls \code{\link[=roadmap]{roadmap()}}, when +\code{roadmap: enabled: true} is set in \verb{_pkgdown.yml}. +} diff --git a/vignettes/Setup.qmd b/vignettes/Setup.qmd index 13e363e..7de9446 100644 --- a/vignettes/Setup.qmd +++ b/vignettes/Setup.qmd @@ -84,9 +84,22 @@ reference: ... ``` +## Roadmap + +Opt in to a roadmap page built from your open GitHub milestones and issues (PRs, closed milestones and issues labelled `internal*`, `wontfix` or `invalid` are skipped; closed issues go last). + +```yaml +roadmap: + enabled: true +``` + +Customize with `roadmap.overrides.milestones` / `roadmap.overrides.issues` (keyed by milestone/issue number, with `title` and `description`) and `roadmap.exclude.labels` / `roadmap.exclude.issues`. See the defaults in [this package's config](https://github.com/stan-dev/pkgdown-config/blob/main/inst/pkgdown/_pkgdown.yml). + +The GHA generates the roadmap article (`vignettes/articles/roadmap.Rmd`) and adds the navbar link for you. The article is rewritten on every build, so add it to `.gitignore`. For local builds use `pkgdownconfig::build_site()` in place of `pkgdown::build_site()`; it does the same and forwards all arguments. + ## GHA -You can use the default GHA, or you can copy [this package's GHA](https://github.com/stan-dev/pkgdown-config/blob/main/.github/workflows/pkgdown.yaml). This GHA deploys `pkgdown` sites on (non-fork[^1]) PRs to unique URLs (`/prs/$PR-NUMBER`). This means that PRs would have preview sites, `/dev` would track `main`, and the main site would track releases. +You can use the default GHA, or you can run `pkgdownconfig::use_pkgdown_gha()` to copy [this package's GHA](https://github.com/stan-dev/pkgdown-config/blob/main/inst/workflows/pkgdown.yaml). This GHA deploys `pkgdown` sites on (non-fork[^1]) PRs to unique URLs (`/prs/$PR-NUMBER`). This means that PRs would have preview sites, `/dev` would track `main`, and the main site would track releases. You could also configure the `pkgdown` GHA to only run when [vignettes are modified](https://docs.github.com/en/actions/how-tos/write-workflows/choose-when-workflows-run/trigger-a-workflow#using-filters-to-target-specific-paths-for-pull-request-or-push-events), or only have the `workflow_dispatch` trigger so that you can build PR's `pkgdown` sites as needed. From 17622d8f991ce2c78ac56fc6ba58db12fc344340 Mon Sep 17 00:00:00 2001 From: VisruthSK Date: Thu, 8 Oct 2026 19:48:19 -0700 Subject: [PATCH 02/27] Replace workflow symlink with a real file GitHub Actions does not follow symlinks for workflow files and parsed the link target as YAML, so the workflow was invalid. --- .github/workflows/pkgdown.yaml | 129 ++++++++++++++++++++++++++++++++- 1 file changed, 128 insertions(+), 1 deletion(-) mode change 120000 => 100644 .github/workflows/pkgdown.yaml diff --git a/.github/workflows/pkgdown.yaml b/.github/workflows/pkgdown.yaml deleted file mode 120000 index 53f5a1e..0000000 --- a/.github/workflows/pkgdown.yaml +++ /dev/null @@ -1 +0,0 @@ -../../inst/workflows/pkgdown.yaml \ No newline at end of file diff --git a/.github/workflows/pkgdown.yaml b/.github/workflows/pkgdown.yaml new file mode 100644 index 0000000..babb0ff --- /dev/null +++ b/.github/workflows/pkgdown.yaml @@ -0,0 +1,128 @@ +# Workflow derived from https://github.com/r-lib/actions/tree/v2/examples +# Need help debugging build failures? Start at https://github.com/r-lib/actions#where-to-find-help +on: + # build dev site on merged pushes + push: + branches: [main, master] + pull_request: + types: [opened, reopened, synchronize, labeled, closed] + # build full site on releases + release: + types: [published] + workflow_dispatch: + +name: pkgdown.yaml + +jobs: + pkgdown: + # For PRs, only run when the PR has the build-website label. + if: >- + github.event_name != 'pull_request' || + ( + github.event.action != 'closed' && + contains(github.event.pull_request.labels.*.name, 'build-website') && + (github.event.action != 'labeled' || github.event.label.name == 'build-website') + ) + runs-on: ubuntu-latest + concurrency: + group: pkgdown-${{ github.event_name == 'pull_request' && github.event.pull_request.number || 'production' }} + cancel-in-progress: true + env: + GITHUB_PAT: ${{ secrets.GITHUB_TOKEN }} + NOT_CRAN: "true" + permissions: + contents: write + steps: + - uses: actions/checkout@v7 + + - uses: r-lib/actions/setup-pandoc@v2 + + - uses: r-lib/actions/setup-r@v2 + with: + use-public-rspm: true + + - uses: r-lib/actions/setup-r-dependencies@v2 + with: + extra-packages: any::pkgdown, local::., stan-dev/pkgdown-config + + - name: Build site + env: + PR_NUMBER: ${{ github.event.pull_request.number }} + run: | + pkg <- pkgdown::as_pkgdown(".") + pkgdownconfig::use_roadmap(pkg) + override <- pkgdownconfig::roadmap_override(pkg) + + pr_number <- Sys.getenv("PR_NUMBER") + if (nzchar(pr_number)) { + if (pkg$development$mode == "devel") { + Sys.setenv(PKGDOWN_DEV_MODE = "unreleased") + } + + override$url <- paste0(pkg$meta$url, "/pr/", pr_number) + override$template$params$noindex <- TRUE + } + + pkgdown::build_site_github_pages( + lazy = FALSE, + run_dont_run = TRUE, + new_process = TRUE, + override = override + ) + shell: Rscript {0} + + - name: Deploy PR preview + # Forks cannot deploy the site + if: > + github.event_name == 'pull_request' && + github.event.pull_request.head.repo.full_name == github.repository + uses: JamesIves/github-pages-deploy-action@v4 + with: + branch: gh-pages + folder: docs + target-folder: pr/${{ github.event.pull_request.number }} + clean: true + force: false + + - name: Deploy site + if: github.event_name != 'pull_request' + uses: JamesIves/github-pages-deploy-action@v4 + with: + branch: gh-pages + folder: docs + clean: false + force: false + cleanup-preview: + if: >- + github.event_name == 'pull_request' && github.event.action == 'closed' + && github.event.pull_request.head.repo.full_name == github.repository + runs-on: ubuntu-latest + + concurrency: + group: pkgdown-${{ github.event.pull_request.number }} + cancel-in-progress: true + + permissions: + contents: write + + steps: + - uses: actions/checkout@v7 + with: + ref: gh-pages + + - name: Remove PR preview + env: + PR_NUMBER: ${{ github.event.pull_request.number }} + run: | + git config user.name "github-actions[bot]" + git config user.email "41898282+github-actions[bot]@users.noreply.github.com" + + git rm -r --ignore-unmatch "pr/$PR_NUMBER" + + if git diff --cached --quiet; then + exit 0 + fi + + git commit -m "Remove pkgdown preview for PR #$PR_NUMBER" + git pull --rebase origin gh-pages + git push origin HEAD:gh-pages From dd35b84be97327403725ceb5b95f8cfaac4112fa Mon Sep 17 00:00:00 2001 From: VisruthSK Date: Thu, 8 Oct 2026 19:51:25 -0700 Subject: [PATCH 03/27] Added some tests --- DESCRIPTION | 2 ++ tests/testthat.R | 4 ++++ tests/testthat/helper-package.R | 20 ++++++++++++++++++++ tests/testthat/test-roadmap.R | 28 ++++++++++++++++++++++++++++ tests/testthat/test-workflow.R | 15 +++++++++++++++ 5 files changed, 69 insertions(+) create mode 100644 tests/testthat.R create mode 100644 tests/testthat/helper-package.R create mode 100644 tests/testthat/test-roadmap.R create mode 100644 tests/testthat/test-workflow.R diff --git a/DESCRIPTION b/DESCRIPTION index f247681..77cf54e 100644 --- a/DESCRIPTION +++ b/DESCRIPTION @@ -17,10 +17,12 @@ Imports: gh, utils Suggests: + testthat (>= 3.0.0), knitr, quarto VignetteBuilder: quarto +Config/testthat/edition: 3 Encoding: UTF-8 Roxygen: list(markdown = TRUE) Config/roxygen2/version: 8.1.0 diff --git a/tests/testthat.R b/tests/testthat.R new file mode 100644 index 0000000..33ab0d4 --- /dev/null +++ b/tests/testthat.R @@ -0,0 +1,4 @@ +library(testthat) +library(pkgdownconfig) + +test_check("pkgdownconfig") diff --git a/tests/testthat/helper-package.R b/tests/testthat/helper-package.R new file mode 100644 index 0000000..526a935 --- /dev/null +++ b/tests/testthat/helper-package.R @@ -0,0 +1,20 @@ +fake_package <- function(config) { + dir <- tempfile() + dir.create(dir) + writeLines( + c( + "Package: fake", + "Version: 1.0", + "Title: Fake", + "Description: Fake.", + "License: MIT" + ), + file.path(dir, "DESCRIPTION") + ) + writeLines(config, file.path(dir, "_pkgdown.yml")) + dir +} + +article_path <- function(dir) { + file.path(dir, "vignettes", "articles", "roadmap.Rmd") +} diff --git a/tests/testthat/test-roadmap.R b/tests/testthat/test-roadmap.R new file mode 100644 index 0000000..f9f20a8 --- /dev/null +++ b/tests/testthat/test-roadmap.R @@ -0,0 +1,28 @@ +test_that("use_roadmap() writes the article only when enabled", { + disabled <- fake_package(c("roadmap:", " enabled: false")) + enabled <- fake_package(c("roadmap:", " enabled: true", " format: rmd")) + + use_roadmap(disabled) + use_roadmap(enabled) + + expect_false(file.exists(article_path(disabled))) + expect_true(file.exists(article_path(enabled))) +}) + +test_that("use_roadmap() rejects formats other than rmd", { + pkg <- fake_package(c("roadmap:", " enabled: true", " format: qmd")) + + expect_error(use_roadmap(pkg), "rmd") +}) + +test_that("roadmap_override() adds the navbar link only when enabled", { + navbar <- c("navbar:", " structure:", " left: [home, news, stan]") + enabled <- fake_package(c(navbar, "roadmap:", " enabled: true")) + disabled <- fake_package(c(navbar, "roadmap:", " enabled: false")) + + expect_equal( + roadmap_override(enabled)$navbar$structure$left, + c("home", "news", "roadmap", "stan") + ) + expect_equal(roadmap_override(disabled), list()) +}) diff --git a/tests/testthat/test-workflow.R b/tests/testthat/test-workflow.R new file mode 100644 index 0000000..dd73c74 --- /dev/null +++ b/tests/testthat/test-workflow.R @@ -0,0 +1,15 @@ +test_that("the repository workflow matches the one shipped to consumers", { + repository <- test_path("..", "..", ".github", "workflows", "pkgdown.yaml") + skip_if_not(file.exists(repository)) + shipped <- system.file("workflows", "pkgdown.yaml", package = "pkgdownconfig") + + expect_identical(readLines(repository), readLines(shipped)) +}) + +test_that("use_pkgdown_gha() copies the workflow into the package", { + pkg <- tempfile() + + use_pkgdown_gha(pkg) + + expect_true(file.exists(file.path(pkg, ".github", "workflows", "pkgdown.yaml"))) +}) From 7382f702994e806417a078970f3dd752491a9cb8 Mon Sep 17 00:00:00 2001 From: VisruthSK Date: Thu, 8 Oct 2026 19:55:28 -0700 Subject: [PATCH 04/27] Enable the roadmap on this site --- .gitignore | 1 + _pkgdown.yml | 3 +++ 2 files changed, 4 insertions(+) diff --git a/.gitignore b/.gitignore index be97e2a..ca76a5e 100644 --- a/.gitignore +++ b/.gitignore @@ -5,3 +5,4 @@ docs inst/doc **/.quarto/ +vignettes/articles/roadmap.Rmd diff --git a/_pkgdown.yml b/_pkgdown.yml index e04dc10..8b450c7 100644 --- a/_pkgdown.yml +++ b/_pkgdown.yml @@ -5,3 +5,6 @@ template: development: mode: auto + +roadmap: + enabled: true From 79348da753966a1f15e3d004618c4ed4aed52448 Mon Sep 17 00:00:00 2001 From: VisruthSK Date: Thu, 8 Oct 2026 20:17:39 -0700 Subject: [PATCH 05/27] Fix roadmap rendering and clean up head template Silence gh progress output leaking into the roadmap page, restyle the cards to follow the theme, drop KaTeX, update MathJax to 4.1.3 and fix the malformed SVG favicon link. --- R/roadmap.R | 2 +- inst/pkgdown/extra.scss | 17 ++++++++++++++--- inst/pkgdown/templates/head.html | 18 +++--------------- 3 files changed, 18 insertions(+), 19 deletions(-) diff --git a/R/roadmap.R b/R/roadmap.R index 86dae75..2a6acf5 100644 --- a/R/roadmap.R +++ b/R/roadmap.R @@ -64,7 +64,7 @@ use_roadmap <- function(pkg = ".") { "title: Roadmap", "---", "", - "```{r, echo=FALSE, results='asis'}", + "```{r, echo=FALSE, message=FALSE, results='asis'}", "pkgdownconfig::roadmap()", "```" ), diff --git a/inst/pkgdown/extra.scss b/inst/pkgdown/extra.scss index 701d3de..774aa53 100644 --- a/inst/pkgdown/extra.scss +++ b/inst/pkgdown/extra.scss @@ -62,15 +62,26 @@ pre { } .rm-card { - background: var(--bs-tertiary-bg); border: 1px solid var(--bs-border-color); border-left: 4px solid var(--bs-primary); border-radius: .4rem; - padding: .9rem 1rem .6rem; + padding: .75rem 1rem; h4 { - margin: 0 0 .4rem; + margin: 0; font-size: 1.05em; + + a { + text-decoration: none; + + &:hover { + text-decoration: underline; + } + } + } + + p { + margin: .4rem 0 0; } &.rm-closed { diff --git a/inst/pkgdown/templates/head.html b/inst/pkgdown/templates/head.html index a3c80d8..0e2ff3f 100644 --- a/inst/pkgdown/templates/head.html +++ b/inst/pkgdown/templates/head.html @@ -8,27 +8,15 @@ - + -{{#uses_katex}} - - - - - -{{/uses_katex}} - {{#uses_mathjax}} - {{/uses_mathjax}} From 7e467039f539fe8eef550c0992d5622b7b28b165 Mon Sep 17 00:00:00 2001 From: VisruthSK Date: Thu, 8 Oct 2026 20:25:37 -0700 Subject: [PATCH 06/27] Keep navbar links on one line --- inst/pkgdown/extra.scss | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/inst/pkgdown/extra.scss b/inst/pkgdown/extra.scss index 774aa53..e89026f 100644 --- a/inst/pkgdown/extra.scss +++ b/inst/pkgdown/extra.scss @@ -33,6 +33,10 @@ pre { color: var(--bs-body-color); } +.navbar .nav-link { + white-space: nowrap; +} + .navbar .navbar-brand img.stan-logo { height: 25px !important; width: auto; From bda2f7f3f882546d6ea19ed5256be0864354c730 Mon Sep 17 00:00:00 2001 From: VisruthSK Date: Thu, 8 Oct 2026 20:31:23 -0700 Subject: [PATCH 07/27] Revert "Keep navbar links on one line" This reverts commit 7e467039f539fe8eef550c0992d5622b7b28b165. --- inst/pkgdown/extra.scss | 4 ---- 1 file changed, 4 deletions(-) diff --git a/inst/pkgdown/extra.scss b/inst/pkgdown/extra.scss index e89026f..774aa53 100644 --- a/inst/pkgdown/extra.scss +++ b/inst/pkgdown/extra.scss @@ -33,10 +33,6 @@ pre { color: var(--bs-body-color); } -.navbar .nav-link { - white-space: nowrap; -} - .navbar .navbar-brand img.stan-logo { height: 25px !important; width: auto; From f4a87e53ce98e823bc04b427b37dd6b93d0946ea Mon Sep 17 00:00:00 2001 From: VisruthSK Date: Thu, 8 Oct 2026 20:33:24 -0700 Subject: [PATCH 08/27] Fall back to the Stan logo in the navbar and document rate limits --- inst/pkgdown/assets/logo.svg | 1 + vignettes/Setup.qmd | 4 ++-- 2 files changed, 3 insertions(+), 2 deletions(-) create mode 100644 inst/pkgdown/assets/logo.svg diff --git a/inst/pkgdown/assets/logo.svg b/inst/pkgdown/assets/logo.svg new file mode 100644 index 0000000..496f040 --- /dev/null +++ b/inst/pkgdown/assets/logo.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/vignettes/Setup.qmd b/vignettes/Setup.qmd index 7de9446..7a0153a 100644 --- a/vignettes/Setup.qmd +++ b/vignettes/Setup.qmd @@ -95,7 +95,7 @@ roadmap: Customize with `roadmap.overrides.milestones` / `roadmap.overrides.issues` (keyed by milestone/issue number, with `title` and `description`) and `roadmap.exclude.labels` / `roadmap.exclude.issues`. See the defaults in [this package's config](https://github.com/stan-dev/pkgdown-config/blob/main/inst/pkgdown/_pkgdown.yml). -The GHA generates the roadmap article (`vignettes/articles/roadmap.Rmd`) and adds the navbar link for you. The article is rewritten on every build, so add it to `.gitignore`. For local builds use `pkgdownconfig::build_site()` in place of `pkgdown::build_site()`; it does the same and forwards all arguments. +The GHA generates the roadmap article (`vignettes/articles/roadmap.Rmd`) and adds the navbar link for you. The article is rewritten on every build, so add it to `.gitignore`. For local builds use `pkgdownconfig::build_site()` in place of `pkgdown::build_site()`; it does the same and forwards all arguments. The roadmap is fetched from the GitHub API at build time. The GHA uses its own token; for local builds set a `GITHUB_PAT`, otherwise GitHub's unauthenticated limit of 60 requests per hour applies and the build can fail. ## GHA @@ -107,7 +107,7 @@ You could also configure the `pkgdown` GHA to only run when [vignettes are modif ## Common Issues -If for some reason the new favicons don't get copied over, check if you are defining favicons in `pkgdown/favicons`. In most cases you can delete everything in that folder--just delete the logo and favicons if you are worried. The template will hook in the correct favicon and logo. If its not working, download [logo.svg](https://github.com/stan-dev/logos/blob/master/logo.svg) to `/man/figures/logo.svg` and run `pkgdown::build_favicons()` once to build the favicons. +If for some reason the new favicons don't get copied over, check if you are defining favicons in `pkgdown/favicons`. In most cases you can delete everything in that folder--just delete the logo and favicons if you are worried. The template will hook in the correct favicon and logo. If its not working, download [logo.svg](https://github.com/stan-dev/logos/blob/master/logo.svg) to `/man/figures/logo.svg` and run `pkgdown::build_favicons()` once to build the favicons. The navbar logo falls back to the Stan logo shipped with the template, so a package without `man/figures/logo.svg` still builds; a logo in your package takes precedence. From 62716e2740e4dbe109f2e76018503529e146bb46 Mon Sep 17 00:00:00 2001 From: VisruthSK Date: Sat, 10 Oct 2026 11:43:47 -0700 Subject: [PATCH 09/27] More in line with Florence's roadmap --- R/roadmap.R | 200 +++++++++++++++++++++++++++++++++----- inst/pkgdown/_pkgdown.yml | 42 ++++++++ inst/pkgdown/extra.scss | 105 +++++++++++++++++++- man/roadmap.Rd | 8 +- vignettes/Setup.qmd | 2 +- 5 files changed, 326 insertions(+), 31 deletions(-) diff --git a/R/roadmap.R b/R/roadmap.R index 2a6acf5..5bf0c34 100644 --- a/R/roadmap.R +++ b/R/roadmap.R @@ -1,8 +1,10 @@ #' Print the roadmap #' -#' Renders the issues of the open GitHub milestones of the package as pandoc -#' markdown, one section per milestone. This is the body of the article written -#' by [use_roadmap()]. +#' Renders the roadmap as pandoc markdown: an introduction, a timeline of the +#' latest release and the open milestones, one section per milestone with its +#' issues, and a closing section. Milestones are ordered by version, next +#' release first, and the text comes from the `roadmap` section of +#' `_pkgdown.yml`. This is the body of the article written by [use_roadmap()]. #' #' @param pkg Path to the package, or a pkgdown object from #' [pkgdown::as_pkgdown()]. The default is two levels up, which is the working @@ -28,13 +30,38 @@ roadmap <- function(pkg = "../..") { }, issues ) - closed <- vapply(issues, function(issue) issue$state == "closed", NA) - numbers <- vapply(issues, `[[`, 0L, "number") - issues <- issues[order(closed, numbers)] - milestones <- vapply(issues, function(issue) issue$milestone$number, 0L) + milestones <- split( + issues, + vapply(issues, function(issue) issue$milestone$number, 0L) + ) + titles <- vapply(milestones, function(m) m[[1]]$milestone$title, "") + versions <- numeric_version(sub("^v", "", titles), strict = FALSE) + versioned <- !is.na(versions) + release <- tryCatch( + gh::gh("/repos/{repo}/releases/latest", repo = repo), + error = function(e) NULL + ) - sections <- lapply(split(issues, milestones), roadmap_milestone, config) - cat(unlist(sections), sep = "\n") + fill <- function(text) roadmap_fill(text, pkg$package, repo) + cat( + sprintf("[Last updated: %s]{.rm-updated}", format(Sys.Date(), "%B %Y")), + "", + fill(config$intro), + if (any(versioned)) { + roadmap_timeline( + milestones[versioned][order(versions[versioned])], + release, + repo + ) + }, + unlist(lapply( + milestones[order(is.na(versions), versions)], + roadmap_milestone, + config + )), + fill(config$outro), + sep = "\n" + ) } #' Write the roadmap article @@ -73,22 +100,153 @@ use_roadmap <- function(pkg = ".") { invisible(path) } +#' Substitute the package placeholders in roadmap text +#' +#' @param text Text from the roadmap config. +#' @param package The package name, for `{package}`. +#' @param repo The repository as `owner/name`, for `{repo}`. +#' @return `text` with the placeholders replaced. +#' @noRd +roadmap_fill <- function(text, package, repo) { + text <- gsub("{package}", package, text, fixed = TRUE) + gsub("{repo}", repo, text, fixed = TRUE) +} + +#' Markdown for the release timeline +#' +#' @param milestones The issues grouped by milestone, in ascending version +#' order. +#' @param release The latest release as returned by the GitHub API, or `NULL`. +#' @param repo The repository as `owner/name`. +#' @return A character vector of markdown lines. +#' @noRd +roadmap_timeline <- function(milestones, release, repo) { + current <- if (!is.null(release)) { + numeric_version(sub("^v", "", release$tag_name), strict = FALSE) + } + released <- if (!is.null(release)) { + list(roadmap_station( + release$tag_name, + format(as.Date(substr(release$published_at, 1, 10)), "Released %B %Y"), + "What's new", + "../news/index.html", + "rm-past" + )) + } + upcoming <- lapply(milestones, function(m) { + milestone <- m[[1]]$milestone + version <- numeric_version(sub("^v", "", milestone$title), strict = FALSE) + roadmap_station( + milestone$title, + roadmap_kind(version, current), + "Milestone", + milestone$html_url + ) + }) + beyond <- list(roadmap_station( + paste("Beyond", milestones[[length(milestones)]][[1]]$milestone$title), + "Planned improvements & your ideas are welcome", + "Open an issue", + sprintf("https://github.com/%s/issues/new", repo) + )) + stations <- c(released, unname(upcoming), beyond) + + c( + "", + sprintf("::::: {.rm-timeline style=\"--rm-stations:%d\"}", length(stations)), + unlist(stations), + ":::::", + "" + ) +} + +#' Markdown for one timeline station +#' +#' @param title The station title. +#' @param subtitle The line below the title. +#' @param label The link text. +#' @param href The link target. +#' @param class Extra class for the station. +#' @return A character vector of markdown lines. +#' @noRd +roadmap_station <- function(title, subtitle, label, href, class = NULL) { + c( + paste0(":::: {", paste(c(".rm-station", class), collapse = " ."), "}"), + sprintf("[%s]{.rm-station-title}", title), + "", + sprintf("[%s]{.rm-station-sub}", subtitle), + "", + sprintf("[%s](%s){.rm-station-sub}", label, href), + "::::", + "" + ) +} + +#' Describe how a version follows the current release +#' +#' @param version The milestone version. +#' @param current The latest released version, or `NULL`. +#' @return `"Next major release"`, `"Next minor release"`, +#' `"Next patch release"` or `"Upcoming release"`. +#' @noRd +roadmap_kind <- function(version, current) { + if (is.null(current) || is.na(version) || is.na(current)) { + return("Upcoming release") + } + next_version <- c(unlist(version), 0, 0) + current <- c(unlist(current), 0, 0) + if (next_version[1] > current[1]) { + "Next major release" + } else if (next_version[2] > current[2]) { + "Next minor release" + } else { + "Next patch release" + } +} + #' Markdown for one milestone #' -#' @param issues The issues of a single milestone, in display order. +#' Cards are the issues followed by the custom `cards` of the milestone +#' override, ordered by `order` (the issue number by default), closed last. +#' +#' @param issues The issues of a single milestone. #' @param config The `roadmap` section of the merged pkgdown config. #' @return A character vector of markdown lines. #' @noRd roadmap_milestone <- function(issues, config) { milestone <- issues[[1]]$milestone override <- config$overrides$milestones[[as.character(milestone$number)]] + cards <- c( + lapply(issues, function(issue) { + card <- config$overrides$issues[[as.character(issue$number)]] + list( + title = card$title %||% issue$title, + description = card$description, + url = issue$html_url, + closed = issue$state == "closed", + order = card$order %||% issue$number + ) + }), + lapply(override$cards, function(card) { + list( + title = card$title, + description = card$description, + url = card$url, + closed = FALSE, + order = card$order %||% Inf + ) + }) + ) + closed <- vapply(cards, function(card) card$closed, NA) + position <- vapply(cards, function(card) card$order, 0) + c( paste("##", override$title %||% milestone$title), "", trimws(override$description %||% milestone$description), "", "::::: {.rm-cards}", - unlist(lapply(issues, roadmap_card, config$overrides$issues)), + unlist(lapply(cards[order(closed, position)], roadmap_card)), ":::::", "" ) @@ -107,25 +265,19 @@ roadmap_excluded <- function(issue, exclude) { any(vapply(patterns, function(p) any(grepl(p, labels)), NA)) } -#' Markdown for one issue card +#' Markdown for one card #' -#' @param issue An issue as returned by the GitHub API. -#' @param overrides The `overrides.issues` section of the roadmap config. +#' @param card A list with `title`, `description`, `url` and `closed`. #' @return A character vector of markdown lines. #' @noRd -roadmap_card <- function(issue, overrides) { - closed <- issue$state == "closed" +roadmap_card <- function(card) { + title <- paste0(if (card$closed) "✓ ", card$title) c( - if (closed) ":::: {.rm-card .rm-closed}" else ":::: {.rm-card}", + if (card$closed) ":::: {.rm-card .rm-closed}" else ":::: {.rm-card}", "", - sprintf( - "#### [%s%s](%s)", - if (closed) "✓ " else "", - issue$title, - issue$html_url - ), + paste("####", if (is.null(card$url)) title else sprintf("[%s](%s)", title, card$url)), "", - trimws(overrides[[as.character(issue$number)]]$description), + trimws(card$description), "", "::::", "" diff --git a/inst/pkgdown/_pkgdown.yml b/inst/pkgdown/_pkgdown.yml index 7e8131c..865e254 100644 --- a/inst/pkgdown/_pkgdown.yml +++ b/inst/pkgdown/_pkgdown.yml @@ -111,3 +111,45 @@ roadmap: format: rmd exclude: labels: [internal*, wontfix, invalid] + intro: | + Welcome! This page shows what the core developers of **{package}** are + working on right now and what we have planned for the coming releases. It + highlights a selection of our main goals, not a complete list of everything + we work on. + + We hope the roadmap is useful to you in a few ways: + + + **If you use {package}**, it shows what we currently prioritize and what + changes are on their way, so you can plan ahead. + + **If you would like to contribute**, it shows the topics we are actively + thinking about. If one of them interests you, we would love to hear from + you. Leave a comment on the linked issue, or reach out on the + [Stan Forums](https://discourse.mc-stan.org/). + + **If you think something important is missing**, or should get more + attention, please tell us. Your input helps us decide what to work on + next. + + ::: {.rm-note} + **A note on plans.** This roadmap shows our strategic plans for the medium + and long term. Alongside this work, we continue to respond to bug reports, + questions, and requests as they come in. The roadmap describes our current + intentions, not fixed commitments. Priorities can shift, and releases have + no fixed dates. We update this page as our plans change. + ::: + outro: | + ## Get involved + + Contributions are welcome! Here are a few ways to join in: + + + **Try the development version** with + `remotes::install_github("{repo}")`, and let us know what works well and + what does not. + + **Share your view on priorities.** Comment on an issue that matters to + you, or let us know about features you are missing. + + **Interested in contributing?** Browse our + [open issues](https://github.com/{repo}/issues) and leave a comment on + the one you would like to work on, or reach out on the + [Stan Forums](https://discourse.mc-stan.org/). + + **Read the + [contributing guide](https://github.com/{repo}/blob/HEAD/.github/CONTRIBUTING.md)** + for how we work. diff --git a/inst/pkgdown/extra.scss b/inst/pkgdown/extra.scss index 774aa53..a9e902a 100644 --- a/inst/pkgdown/extra.scss +++ b/inst/pkgdown/extra.scss @@ -54,6 +54,74 @@ pre { font-weight: 800; } +.rm-updated { + color: var(--bs-secondary-color); + font-size: .9em; +} + +.rm-note { + border: 1px solid var(--bs-border-color); + border-radius: .4rem; + padding: .75rem 1rem; + margin: 1rem 0; +} + +.rm-timeline { + display: grid; + grid-template-columns: repeat(var(--rm-stations), minmax(0, 1fr)); + margin: 2rem 0 2.5rem; + position: relative; + + &::before { + content: ""; + position: absolute; + top: .7rem; + left: calc(50% / var(--rm-stations)); + right: calc(50% / var(--rm-stations)); + border-top: 3px solid var(--bs-border-color); + } +} + +.rm-station { + text-align: center; + position: relative; + padding: 0 .5rem; + + &::before { + content: ""; + display: block; + width: 1.4rem; + height: 1.4rem; + margin: 0 auto .75rem; + border-radius: 50%; + border: 3px solid var(--bs-primary); + background: var(--bs-body-bg); + position: relative; + } + + &.rm-past::before { + background: var(--bs-primary); + } + + p { + margin: 0; + } + + a { + text-decoration: none; + } +} + +.rm-station-title { + font-weight: 700; + font-size: 1.1em; +} + +.rm-station-sub { + color: var(--bs-secondary-color); + font-size: .9em; +} + .rm-cards { display: grid; grid-template-columns: repeat(auto-fill, minmax(16rem, 1fr)); @@ -62,16 +130,18 @@ pre { } .rm-card { + background: var(--bs-tertiary-bg); border: 1px solid var(--bs-border-color); border-left: 4px solid var(--bs-primary); border-radius: .4rem; - padding: .75rem 1rem; + padding: .9rem 1rem .6rem; h4 { - margin: 0; + margin: .4rem 0; font-size: 1.05em; a { + color: inherit; text-decoration: none; &:hover { @@ -81,10 +151,39 @@ pre { } p { - margin: .4rem 0 0; + margin: 0 0 .5rem; + font-size: .95em; } &.rm-closed { opacity: .65; } } + +@media (max-width: 600px) { + .rm-timeline { + grid-template-columns: 1fr; + gap: 1.25rem; + + &::before { + top: .7rem; + bottom: .7rem; + left: .7rem; + right: auto; + border-top: 0; + border-left: 3px solid var(--bs-border-color); + } + } + + .rm-station { + text-align: left; + padding-left: 2.5rem; + + &::before { + position: absolute; + left: 0; + top: 0; + margin: 0; + } + } +} diff --git a/man/roadmap.Rd b/man/roadmap.Rd index a93b05c..9508154 100644 --- a/man/roadmap.Rd +++ b/man/roadmap.Rd @@ -15,7 +15,9 @@ directory pkgdown renders articles from.} \code{NULL}, invisibly. Called for its printed output. } \description{ -Renders the issues of the open GitHub milestones of the package as pandoc -markdown, one section per milestone. This is the body of the article written -by \code{\link[=use_roadmap]{use_roadmap()}}. +Renders the roadmap as pandoc markdown: an introduction, a timeline of the +latest release and the open milestones, one section per milestone with its +issues, and a closing section. Milestones are ordered by version, next +release first, and the text comes from the \code{roadmap} section of +\verb{_pkgdown.yml}. This is the body of the article written by \code{\link[=use_roadmap]{use_roadmap()}}. } diff --git a/vignettes/Setup.qmd b/vignettes/Setup.qmd index 7a0153a..1a8b8ba 100644 --- a/vignettes/Setup.qmd +++ b/vignettes/Setup.qmd @@ -93,7 +93,7 @@ roadmap: enabled: true ``` -Customize with `roadmap.overrides.milestones` / `roadmap.overrides.issues` (keyed by milestone/issue number, with `title` and `description`) and `roadmap.exclude.labels` / `roadmap.exclude.issues`. See the defaults in [this package's config](https://github.com/stan-dev/pkgdown-config/blob/main/inst/pkgdown/_pkgdown.yml). +Customize with `roadmap.overrides.milestones` / `roadmap.overrides.issues` (keyed by milestone/issue number; both take `title` and `description`, issues also take `order`, and milestones can add your own `cards`, each with a `title`, `description`, optional `url` and optional `order`). Cards are ordered by issue number with closed issues last, unless you set `order`; custom cards come last unless they set it and `roadmap.exclude.labels` / `roadmap.exclude.issues`. See the defaults in [this package's config](https://github.com/stan-dev/pkgdown-config/blob/main/inst/pkgdown/_pkgdown.yml). The introduction (`roadmap.intro`) and the closing section (`roadmap.outro`) are Markdown with `{package}` and `{repo}` placeholders, and you can replace them. The timeline is generated from the latest GitHub release and the open milestones, and milestones are listed next release first, ordered by version (milestones whose title is not a version come last). The GHA generates the roadmap article (`vignettes/articles/roadmap.Rmd`) and adds the navbar link for you. The article is rewritten on every build, so add it to `.gitignore`. For local builds use `pkgdownconfig::build_site()` in place of `pkgdown::build_site()`; it does the same and forwards all arguments. The roadmap is fetched from the GitHub API at build time. The GHA uses its own token; for local builds set a `GITHUB_PAT`, otherwise GitHub's unauthenticated limit of 60 requests per hour applies and the build can fail. From 4eb7b2c5d6287d1e76b21663ceb3af4c52c8f594 Mon Sep 17 00:00:00 2001 From: VisruthSK Date: Sat, 10 Oct 2026 12:13:26 -0700 Subject: [PATCH 10/27] Keep the roadmap from breaking sites with their own articles Hide the roadmap article from the Articles index by listing it in an internal section, and set the navbar and sections on the parsed package instead of through the override. Add the roadmap issue description to this site's config. --- .github/workflows/pkgdown.yaml | 6 ++--- NAMESPACE | 2 +- R/build_site.R | 49 ++++++++++++++++++++-------------- _pkgdown.yml | 4 +++ inst/workflows/pkgdown.yaml | 6 ++--- man/build_site.Rd | 8 +++--- man/roadmap_override.Rd | 19 ------------- man/roadmap_site.Rd | 20 ++++++++++++++ tests/testthat/test-roadmap.R | 12 ++++++--- vignettes/Setup.qmd | 2 +- 10 files changed, 72 insertions(+), 56 deletions(-) delete mode 100644 man/roadmap_override.Rd create mode 100644 man/roadmap_site.Rd diff --git a/.github/workflows/pkgdown.yaml b/.github/workflows/pkgdown.yaml index babb0ff..87b721d 100644 --- a/.github/workflows/pkgdown.yaml +++ b/.github/workflows/pkgdown.yaml @@ -49,9 +49,9 @@ jobs: env: PR_NUMBER: ${{ github.event.pull_request.number }} run: | - pkg <- pkgdown::as_pkgdown(".") - pkgdownconfig::use_roadmap(pkg) - override <- pkgdownconfig::roadmap_override(pkg) + pkgdownconfig::use_roadmap(".") + pkg <- pkgdownconfig::roadmap_site(".") + override <- list() pr_number <- Sys.getenv("PR_NUMBER") if (nzchar(pr_number)) { diff --git a/NAMESPACE b/NAMESPACE index eeff9a3..7d69cd3 100644 --- a/NAMESPACE +++ b/NAMESPACE @@ -2,6 +2,6 @@ export(build_site) export(roadmap) -export(roadmap_override) +export(roadmap_site) export(use_pkgdown_gha) export(use_roadmap) diff --git a/R/build_site.R b/R/build_site.R index 82ddd89..d26f926 100644 --- a/R/build_site.R +++ b/R/build_site.R @@ -1,39 +1,48 @@ #' Build a Stan pkgdown site #' -#' Writes the roadmap article and adds the roadmap to the navbar when -#' `roadmap: enabled: true` is set, then calls [pkgdown::build_site()] with all -#' other arguments. +#' Writes the roadmap article when `roadmap: enabled: true` is set, then calls +#' [pkgdown::build_site()] with all other arguments. #' #' @inheritParams pkgdown::build_site #' @param ... Passed on to [pkgdown::build_site()]. -#' @param override Passed on to [pkgdown::build_site()], merged over the -#' navbar override from `roadmap_override()`. +#' @param override Passed on to [pkgdown::build_site()]. #' @return The result of [pkgdown::build_site()], invisibly. #' @export build_site <- function(pkg = ".", ..., override = list()) { - config <- pkgdown::as_pkgdown(pkg) - use_roadmap(config) - pkgdown::build_site( - pkg, - ..., - override = utils::modifyList(roadmap_override(config), override) - ) + use_roadmap(pkg) + pkgdown::build_site(roadmap_site(pkg), ..., override = override) } -#' Navbar override for the roadmap +#' Add the roadmap to a pkgdown site #' -#' Inserts `roadmap` into the navbar after `news` when the roadmap is enabled. -#' Used by [build_site()] and the GitHub Action. +#' Adds `roadmap` to the navbar after `news`, and hides the roadmap article from +#' the Articles index while keeping it built. Call it after [use_roadmap()] so +#' the article is parsed. Used by [build_site()] and the GitHub Action. #' #' @inheritParams use_roadmap -#' @return A pkgdown override list, empty when the roadmap is disabled. +#' @return A pkgdown object, unchanged when the roadmap is disabled. #' @export -roadmap_override <- function(pkg = ".") { +roadmap_site <- function(pkg = ".") { pkg <- pkgdown::as_pkgdown(pkg) if (!isTRUE(pkg$meta$roadmap$enabled)) { - return(list()) + return(pkg) } left <- pkg$meta$navbar$structure$left - after <- match("news", left, nomatch = length(left)) - list(navbar = list(structure = list(left = append(left, "roadmap", after)))) + pkg$meta$navbar$structure$left <- append( + left, + "roadmap", + match("news", left, nomatch = length(left)) + ) + sections <- pkg$meta$articles %||% list( + list(title = "All vignettes", contents = list("starts_with(\"\")")) + ) + sections <- lapply(sections, function(section) { + section$contents <- c(section$contents, "-starts_with('articles/roadmap')") + section + }) + pkg$meta$articles <- c( + sections, + list(list(title = "internal", contents = list("articles/roadmap"))) + ) + pkg } diff --git a/_pkgdown.yml b/_pkgdown.yml index 8b450c7..5810791 100644 --- a/_pkgdown.yml +++ b/_pkgdown.yml @@ -8,3 +8,7 @@ development: roadmap: enabled: true + overrides: + issues: + "16": + description: Support declaratively building a Roadmap page from milestones and issues. diff --git a/inst/workflows/pkgdown.yaml b/inst/workflows/pkgdown.yaml index babb0ff..87b721d 100644 --- a/inst/workflows/pkgdown.yaml +++ b/inst/workflows/pkgdown.yaml @@ -49,9 +49,9 @@ jobs: env: PR_NUMBER: ${{ github.event.pull_request.number }} run: | - pkg <- pkgdown::as_pkgdown(".") - pkgdownconfig::use_roadmap(pkg) - override <- pkgdownconfig::roadmap_override(pkg) + pkgdownconfig::use_roadmap(".") + pkg <- pkgdownconfig::roadmap_site(".") + override <- list() pr_number <- Sys.getenv("PR_NUMBER") if (nzchar(pr_number)) { diff --git a/man/build_site.Rd b/man/build_site.Rd index 79a24bc..d8cdaf6 100644 --- a/man/build_site.Rd +++ b/man/build_site.Rd @@ -11,14 +11,12 @@ build_site(pkg = ".", ..., override = list()) \item{...}{Passed on to \code{\link[pkgdown:build_site]{pkgdown::build_site()}}.} -\item{override}{Passed on to \code{\link[pkgdown:build_site]{pkgdown::build_site()}}, merged over the -navbar override from \code{roadmap_override()}.} +\item{override}{Passed on to \code{\link[pkgdown:build_site]{pkgdown::build_site()}}.} } \value{ The result of \code{\link[pkgdown:build_site]{pkgdown::build_site()}}, invisibly. } \description{ -Writes the roadmap article and adds the roadmap to the navbar when -\code{roadmap: enabled: true} is set, then calls \code{\link[pkgdown:build_site]{pkgdown::build_site()}} with all -other arguments. +Writes the roadmap article when \code{roadmap: enabled: true} is set, then calls +\code{\link[pkgdown:build_site]{pkgdown::build_site()}} with all other arguments. } diff --git a/man/roadmap_override.Rd b/man/roadmap_override.Rd deleted file mode 100644 index eff65fa..0000000 --- a/man/roadmap_override.Rd +++ /dev/null @@ -1,19 +0,0 @@ -% Generated by roxygen2: do not edit by hand -% Please edit documentation in R/build_site.R -\name{roadmap_override} -\alias{roadmap_override} -\title{Navbar override for the roadmap} -\usage{ -roadmap_override(pkg = ".") -} -\arguments{ -\item{pkg}{Path to the package, or a pkgdown object from -\code{\link[pkgdown:as_pkgdown]{pkgdown::as_pkgdown()}}.} -} -\value{ -A pkgdown override list, empty when the roadmap is disabled. -} -\description{ -Inserts \code{roadmap} into the navbar after \code{news} when the roadmap is enabled. -Used by \code{\link[=build_site]{build_site()}} and the GitHub Action. -} diff --git a/man/roadmap_site.Rd b/man/roadmap_site.Rd new file mode 100644 index 0000000..7a4233a --- /dev/null +++ b/man/roadmap_site.Rd @@ -0,0 +1,20 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/build_site.R +\name{roadmap_site} +\alias{roadmap_site} +\title{Add the roadmap to a pkgdown site} +\usage{ +roadmap_site(pkg = ".") +} +\arguments{ +\item{pkg}{Path to the package, or a pkgdown object from +\code{\link[pkgdown:as_pkgdown]{pkgdown::as_pkgdown()}}.} +} +\value{ +A pkgdown object, unchanged when the roadmap is disabled. +} +\description{ +Adds \code{roadmap} to the navbar after \code{news}, and hides the roadmap article from +the Articles index while keeping it built. Call it after \code{\link[=use_roadmap]{use_roadmap()}} so +the article is parsed. Used by \code{\link[=build_site]{build_site()}} and the GitHub Action. +} diff --git a/tests/testthat/test-roadmap.R b/tests/testthat/test-roadmap.R index f9f20a8..e0a2ac8 100644 --- a/tests/testthat/test-roadmap.R +++ b/tests/testthat/test-roadmap.R @@ -15,14 +15,18 @@ test_that("use_roadmap() rejects formats other than rmd", { expect_error(use_roadmap(pkg), "rmd") }) -test_that("roadmap_override() adds the navbar link only when enabled", { +test_that("roadmap_site() adds the navbar link only when enabled", { navbar <- c("navbar:", " structure:", " left: [home, news, stan]") - enabled <- fake_package(c(navbar, "roadmap:", " enabled: true")) + enabled <- fake_package(c(navbar, "roadmap:", " enabled: true", " format: rmd")) disabled <- fake_package(c(navbar, "roadmap:", " enabled: false")) + use_roadmap(enabled) expect_equal( - roadmap_override(enabled)$navbar$structure$left, + roadmap_site(enabled)$meta$navbar$structure$left, c("home", "news", "roadmap", "stan") ) - expect_equal(roadmap_override(disabled), list()) + expect_equal( + roadmap_site(disabled)$meta$navbar$structure$left, + c("home", "news", "stan") + ) }) diff --git a/vignettes/Setup.qmd b/vignettes/Setup.qmd index 1a8b8ba..5d53a2d 100644 --- a/vignettes/Setup.qmd +++ b/vignettes/Setup.qmd @@ -95,7 +95,7 @@ roadmap: Customize with `roadmap.overrides.milestones` / `roadmap.overrides.issues` (keyed by milestone/issue number; both take `title` and `description`, issues also take `order`, and milestones can add your own `cards`, each with a `title`, `description`, optional `url` and optional `order`). Cards are ordered by issue number with closed issues last, unless you set `order`; custom cards come last unless they set it and `roadmap.exclude.labels` / `roadmap.exclude.issues`. See the defaults in [this package's config](https://github.com/stan-dev/pkgdown-config/blob/main/inst/pkgdown/_pkgdown.yml). The introduction (`roadmap.intro`) and the closing section (`roadmap.outro`) are Markdown with `{package}` and `{repo}` placeholders, and you can replace them. The timeline is generated from the latest GitHub release and the open milestones, and milestones are listed next release first, ordered by version (milestones whose title is not a version come last). -The GHA generates the roadmap article (`vignettes/articles/roadmap.Rmd`) and adds the navbar link for you. The article is rewritten on every build, so add it to `.gitignore`. For local builds use `pkgdownconfig::build_site()` in place of `pkgdown::build_site()`; it does the same and forwards all arguments. The roadmap is fetched from the GitHub API at build time. The GHA uses its own token; for local builds set a `GITHUB_PAT`, otherwise GitHub's unauthenticated limit of 60 requests per hour applies and the build can fail. +The GHA generates the roadmap article (`vignettes/articles/roadmap.Rmd`) and adds the navbar link for you. The article is rewritten on every build, so add it to `.gitignore`. For local builds use `pkgdownconfig::build_site()` in place of `pkgdown::build_site()`; it does the same and forwards all arguments. Plain `pkgdown::build_site()` does not add the navbar link or hide the article, so with the roadmap enabled it fails on sites that define their own `articles` sections. The roadmap is fetched from the GitHub API at build time. The GHA uses its own token; for local builds set a `GITHUB_PAT`, otherwise GitHub's unauthenticated limit of 60 requests per hour applies and the build can fail. ## GHA From e5b24ed639aa408e52290f21271b3bbeb9f0619e Mon Sep 17 00:00:00 2001 From: VisruthSK Date: Sat, 10 Oct 2026 13:09:45 -0700 Subject: [PATCH 11/27] Build the PR site in the same process so the roadmap navbar and article index apply --- .github/workflows/pkgdown.yaml | 2 +- inst/workflows/pkgdown.yaml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/pkgdown.yaml b/.github/workflows/pkgdown.yaml index 87b721d..b9efedc 100644 --- a/.github/workflows/pkgdown.yaml +++ b/.github/workflows/pkgdown.yaml @@ -66,7 +66,7 @@ jobs: pkgdown::build_site_github_pages( lazy = FALSE, run_dont_run = TRUE, - new_process = TRUE, + new_process = FALSE, override = override ) shell: Rscript {0} diff --git a/inst/workflows/pkgdown.yaml b/inst/workflows/pkgdown.yaml index 87b721d..b9efedc 100644 --- a/inst/workflows/pkgdown.yaml +++ b/inst/workflows/pkgdown.yaml @@ -66,7 +66,7 @@ jobs: pkgdown::build_site_github_pages( lazy = FALSE, run_dont_run = TRUE, - new_process = TRUE, + new_process = FALSE, override = override ) shell: Rscript {0} From dd923da8ebac6699e3d0440243712dd7fdaab770 Mon Sep 17 00:00:00 2001 From: VisruthSK Date: Sat, 10 Oct 2026 13:10:11 -0700 Subject: [PATCH 12/27] Pass the prepared package to build_site_github_pages --- .github/workflows/pkgdown.yaml | 1 + inst/workflows/pkgdown.yaml | 1 + 2 files changed, 2 insertions(+) diff --git a/.github/workflows/pkgdown.yaml b/.github/workflows/pkgdown.yaml index b9efedc..cd43ecb 100644 --- a/.github/workflows/pkgdown.yaml +++ b/.github/workflows/pkgdown.yaml @@ -64,6 +64,7 @@ jobs: } pkgdown::build_site_github_pages( + pkg = pkg, lazy = FALSE, run_dont_run = TRUE, new_process = FALSE, diff --git a/inst/workflows/pkgdown.yaml b/inst/workflows/pkgdown.yaml index b9efedc..cd43ecb 100644 --- a/inst/workflows/pkgdown.yaml +++ b/inst/workflows/pkgdown.yaml @@ -64,6 +64,7 @@ jobs: } pkgdown::build_site_github_pages( + pkg = pkg, lazy = FALSE, run_dont_run = TRUE, new_process = FALSE, From 1d6e38afab26d0b28c6367cd6582eee981a0bc42 Mon Sep 17 00:00:00 2001 From: VisruthSK Date: Sat, 10 Oct 2026 13:11:00 -0700 Subject: [PATCH 13/27] Restore new_process in the PR build --- .github/workflows/pkgdown.yaml | 2 +- inst/workflows/pkgdown.yaml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/pkgdown.yaml b/.github/workflows/pkgdown.yaml index cd43ecb..d7473d0 100644 --- a/.github/workflows/pkgdown.yaml +++ b/.github/workflows/pkgdown.yaml @@ -67,7 +67,7 @@ jobs: pkg = pkg, lazy = FALSE, run_dont_run = TRUE, - new_process = FALSE, + new_process = TRUE, override = override ) shell: Rscript {0} diff --git a/inst/workflows/pkgdown.yaml b/inst/workflows/pkgdown.yaml index cd43ecb..d7473d0 100644 --- a/inst/workflows/pkgdown.yaml +++ b/inst/workflows/pkgdown.yaml @@ -67,7 +67,7 @@ jobs: pkg = pkg, lazy = FALSE, run_dont_run = TRUE, - new_process = FALSE, + new_process = TRUE, override = override ) shell: Rscript {0} From 3bf2589d43b0013e3e8c53a042d30a93944d0ad7 Mon Sep 17 00:00:00 2001 From: VisruthSK Date: Sat, 10 Oct 2026 13:24:34 -0700 Subject: [PATCH 14/27] Retitle issue 19 on the roadmap and describe it --- _pkgdown.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/_pkgdown.yml b/_pkgdown.yml index 5810791..7ea5312 100644 --- a/_pkgdown.yml +++ b/_pkgdown.yml @@ -12,3 +12,6 @@ roadmap: issues: "16": description: Support declaratively building a Roadmap page from milestones and issues. + "19": + title: Function to Write Default `pkgdown` GitHub Actions + description: Writes this package's `pkgdown` GitHub Action into a Stan package, so every site builds PR previews the same way. From a780180fa6701d963632f000b05fd3c80267fd33 Mon Sep 17 00:00:00 2001 From: VisruthSK Date: Sat, 10 Oct 2026 13:46:41 -0700 Subject: [PATCH 15/27] Generate the roadmap article from the build roadmap_site() now writes the roadmap markdown into the article, so pkgdown renders plain Markdown and no R code runs during the render. The article is written as .Rmd by default, or .qmd with `roadmap: format: qmd`. Only build_site() and use_pkgdown_gha() are exported. The GitHub Action calls roadmap_site() through `:::`. The roadmap article is hidden from the Articles index and linked from the navbar. Imports now includes pkgdown instead of Depends. Issue titles from GitHub pass through as Markdown, so backticks still render as code. --- .github/workflows/pkgdown.yaml | 3 +- .gitignore | 3 +- DESCRIPTION | 3 +- NAMESPACE | 3 -- R/build_site.R | 28 ++++++----- R/roadmap.R | 86 +++++++++++++++------------------ inst/workflows/pkgdown.yaml | 3 +- man/build_site.Rd | 2 +- man/roadmap.Rd | 23 --------- man/roadmap_site.Rd | 20 -------- man/use_roadmap.Rd | 20 -------- tests/testthat/helper-package.R | 4 +- tests/testthat/test-roadmap.R | 38 ++++++++++----- vignettes/Setup.qmd | 2 +- 14 files changed, 88 insertions(+), 150 deletions(-) delete mode 100644 man/roadmap.Rd delete mode 100644 man/roadmap_site.Rd delete mode 100644 man/use_roadmap.Rd diff --git a/.github/workflows/pkgdown.yaml b/.github/workflows/pkgdown.yaml index d7473d0..bbbea1f 100644 --- a/.github/workflows/pkgdown.yaml +++ b/.github/workflows/pkgdown.yaml @@ -49,8 +49,7 @@ jobs: env: PR_NUMBER: ${{ github.event.pull_request.number }} run: | - pkgdownconfig::use_roadmap(".") - pkg <- pkgdownconfig::roadmap_site(".") + pkg <- pkgdownconfig:::roadmap_site(".") override <- list() pr_number <- Sys.getenv("PR_NUMBER") diff --git a/.gitignore b/.gitignore index ca76a5e..f897d58 100644 --- a/.gitignore +++ b/.gitignore @@ -5,4 +5,5 @@ docs inst/doc **/.quarto/ -vignettes/articles/roadmap.Rmd +vignettes/articles/roadmap.* +vignettes/articles/roadmap_files/ diff --git a/DESCRIPTION b/DESCRIPTION index 77cf54e..99c8613 100644 --- a/DESCRIPTION +++ b/DESCRIPTION @@ -11,9 +11,8 @@ License: BSD_3_clause + file LICENSE URL: https://mc-stan.org/pkgdown-config, https://mc-stan.org/pkgdown-config BugReports: https://github.com/stan-dev/pkgdown-config/issues/ -Depends: - pkgdown (>= 2.1.0) Imports: + pkgdown (>= 2.1.0), gh, utils Suggests: diff --git a/NAMESPACE b/NAMESPACE index 7d69cd3..7c8f416 100644 --- a/NAMESPACE +++ b/NAMESPACE @@ -1,7 +1,4 @@ # Generated by roxygen2: do not edit by hand export(build_site) -export(roadmap) -export(roadmap_site) export(use_pkgdown_gha) -export(use_roadmap) diff --git a/R/build_site.R b/R/build_site.R index d26f926..e02af4e 100644 --- a/R/build_site.R +++ b/R/build_site.R @@ -1,6 +1,6 @@ #' Build a Stan pkgdown site #' -#' Writes the roadmap article when `roadmap: enabled: true` is set, then calls +#' Adds the roadmap when `roadmap: enabled: true` is set, then calls #' [pkgdown::build_site()] with all other arguments. #' #' @inheritParams pkgdown::build_site @@ -9,33 +9,35 @@ #' @return The result of [pkgdown::build_site()], invisibly. #' @export build_site <- function(pkg = ".", ..., override = list()) { - use_roadmap(pkg) pkgdown::build_site(roadmap_site(pkg), ..., override = override) } -#' Add the roadmap to a pkgdown site +#' Prepare a pkgdown site for the roadmap #' -#' Adds `roadmap` to the navbar after `news`, and hides the roadmap article from -#' the Articles index while keeping it built. Call it after [use_roadmap()] so -#' the article is parsed. Used by [build_site()] and the GitHub Action. +#' Writes the roadmap article, adds `roadmap` to the navbar after `news`, and +#' hides the article from the Articles index while keeping it built. Used by +#' [build_site()] and the GitHub Action. #' -#' @inheritParams use_roadmap +#' @param path Path to the package. #' @return A pkgdown object, unchanged when the roadmap is disabled. -#' @export -roadmap_site <- function(pkg = ".") { - pkg <- pkgdown::as_pkgdown(pkg) +#' @noRd +roadmap_site <- function(path = ".") { + pkg <- pkgdown::as_pkgdown(path) if (!isTRUE(pkg$meta$roadmap$enabled)) { return(pkg) } + write_roadmap(pkg) + pkg <- pkgdown::as_pkgdown(path) left <- pkg$meta$navbar$structure$left pkg$meta$navbar$structure$left <- append( left, "roadmap", match("news", left, nomatch = length(left)) ) - sections <- pkg$meta$articles %||% list( - list(title = "All vignettes", contents = list("starts_with(\"\")")) - ) + sections <- pkg$meta$articles %||% + list( + list(title = "All vignettes", contents = list("starts_with(\"\")")) + ) sections <- lapply(sections, function(section) { section$contents <- c(section$contents, "-starts_with('articles/roadmap')") section diff --git a/R/roadmap.R b/R/roadmap.R index 5bf0c34..b6d7dfb 100644 --- a/R/roadmap.R +++ b/R/roadmap.R @@ -1,18 +1,13 @@ -#' Print the roadmap +#' Roadmap markdown #' -#' Renders the roadmap as pandoc markdown: an introduction, a timeline of the -#' latest release and the open milestones, one section per milestone with its -#' issues, and a closing section. Milestones are ordered by version, next -#' release first, and the text comes from the `roadmap` section of -#' `_pkgdown.yml`. This is the body of the article written by [use_roadmap()]. +#' Renders the roadmap as markdown: an introduction, a timeline of the latest +#' release and the open milestones, one section per milestone with its issues, +#' and a closing section. #' -#' @param pkg Path to the package, or a pkgdown object from -#' [pkgdown::as_pkgdown()]. The default is two levels up, which is the working -#' directory pkgdown renders articles from. -#' @return `NULL`, invisibly. Called for its printed output. -#' @export -roadmap <- function(pkg = "../..") { - pkg <- pkgdown::as_pkgdown(pkg) +#' @param pkg A pkgdown object from [pkgdown::as_pkgdown()]. +#' @return A character vector of markdown lines. +#' @noRd +roadmap <- function(pkg) { config <- pkg$meta$roadmap repo <- gsub("^https://github.com/|/$", "", pkg$repo$url$home) issues <- gh::gh( @@ -43,7 +38,7 @@ roadmap <- function(pkg = "../..") { ) fill <- function(text) roadmap_fill(text, pkg$package, repo) - cat( + c( sprintf("[Last updated: %s]{.rm-updated}", format(Sys.Date(), "%B %Y")), "", fill(config$intro), @@ -59,44 +54,32 @@ roadmap <- function(pkg = "../..") { roadmap_milestone, config )), - fill(config$outro), - sep = "\n" + fill(config$outro) ) } #' Write the roadmap article #' -#' Writes `vignettes/articles/roadmap.Rmd`, which calls [roadmap()], when -#' `roadmap: enabled: true` is set in `_pkgdown.yml`. +#' Writes `vignettes/articles/roadmap.Rmd`, or `roadmap.qmd` when `format` is +#' `qmd`, containing the roadmap markdown. Removes the other format's file. #' -#' @param pkg Path to the package, or a pkgdown object from -#' [pkgdown::as_pkgdown()]. -#' @return The path of the article, invisibly, or `NULL` invisibly if the -#' roadmap is disabled. -#' @export -use_roadmap <- function(pkg = ".") { - pkg <- pkgdown::as_pkgdown(pkg) - config <- pkg$meta$roadmap - if (!isTRUE(config$enabled)) { - return(invisible()) - } - if (!identical(config$format, "rmd")) { - stop("Only `roadmap: format: rmd` is supported.", call. = FALSE) +#' @param pkg A pkgdown object from [pkgdown::as_pkgdown()]. +#' @return The path of the article, invisibly. +#' @noRd +write_roadmap <- function(pkg) { + format <- pkg$meta$roadmap$format %||% "rmd" + if (!format %in% c("rmd", "qmd")) { + stop("`roadmap: format` must be `rmd` or `qmd`.", call. = FALSE) } - path <- file.path(pkg$src_path, "vignettes", "articles", "roadmap.Rmd") - dir.create(dirname(path), recursive = TRUE, showWarnings = FALSE) - writeLines( - c( - "---", - "title: Roadmap", - "---", - "", - "```{r, echo=FALSE, message=FALSE, results='asis'}", - "pkgdownconfig::roadmap()", - "```" - ), - path - ) + dir <- file.path(pkg$src_path, "vignettes", "articles") + dir.create(dir, recursive = TRUE, showWarnings = FALSE) + extensions <- c(rmd = "Rmd", qmd = "qmd") + unlink(file.path( + dir, + paste0("roadmap.", extensions[names(extensions) != format]) + )) + path <- file.path(dir, paste0("roadmap.", extensions[[format]])) + writeLines(c("---", "title: Roadmap", "---", "", roadmap(pkg)), path) invisible(path) } @@ -153,7 +136,10 @@ roadmap_timeline <- function(milestones, release, repo) { c( "", - sprintf("::::: {.rm-timeline style=\"--rm-stations:%d\"}", length(stations)), + sprintf( + "::::: {.rm-timeline style=\"--rm-stations:%d\"}", + length(stations) + ), unlist(stations), ":::::", "" @@ -261,7 +247,8 @@ roadmap_milestone <- function(issues, config) { roadmap_excluded <- function(issue, exclude) { labels <- vapply(issue$labels, `[[`, "", "name") patterns <- utils::glob2rx(unlist(exclude$labels)) - as.character(issue$number) %in% unlist(exclude$issues) || + as.character(issue$number) %in% + unlist(exclude$issues) || any(vapply(patterns, function(p) any(grepl(p, labels)), NA)) } @@ -275,7 +262,10 @@ roadmap_card <- function(card) { c( if (card$closed) ":::: {.rm-card .rm-closed}" else ":::: {.rm-card}", "", - paste("####", if (is.null(card$url)) title else sprintf("[%s](%s)", title, card$url)), + paste( + "####", + if (is.null(card$url)) title else sprintf("[%s](%s)", title, card$url) + ), "", trimws(card$description), "", diff --git a/inst/workflows/pkgdown.yaml b/inst/workflows/pkgdown.yaml index d7473d0..bbbea1f 100644 --- a/inst/workflows/pkgdown.yaml +++ b/inst/workflows/pkgdown.yaml @@ -49,8 +49,7 @@ jobs: env: PR_NUMBER: ${{ github.event.pull_request.number }} run: | - pkgdownconfig::use_roadmap(".") - pkg <- pkgdownconfig::roadmap_site(".") + pkg <- pkgdownconfig:::roadmap_site(".") override <- list() pr_number <- Sys.getenv("PR_NUMBER") diff --git a/man/build_site.Rd b/man/build_site.Rd index d8cdaf6..cc739c1 100644 --- a/man/build_site.Rd +++ b/man/build_site.Rd @@ -17,6 +17,6 @@ build_site(pkg = ".", ..., override = list()) The result of \code{\link[pkgdown:build_site]{pkgdown::build_site()}}, invisibly. } \description{ -Writes the roadmap article when \code{roadmap: enabled: true} is set, then calls +Adds the roadmap when \code{roadmap: enabled: true} is set, then calls \code{\link[pkgdown:build_site]{pkgdown::build_site()}} with all other arguments. } diff --git a/man/roadmap.Rd b/man/roadmap.Rd deleted file mode 100644 index 9508154..0000000 --- a/man/roadmap.Rd +++ /dev/null @@ -1,23 +0,0 @@ -% Generated by roxygen2: do not edit by hand -% Please edit documentation in R/roadmap.R -\name{roadmap} -\alias{roadmap} -\title{Print the roadmap} -\usage{ -roadmap(pkg = "../..") -} -\arguments{ -\item{pkg}{Path to the package, or a pkgdown object from -\code{\link[pkgdown:as_pkgdown]{pkgdown::as_pkgdown()}}. The default is two levels up, which is the working -directory pkgdown renders articles from.} -} -\value{ -\code{NULL}, invisibly. Called for its printed output. -} -\description{ -Renders the roadmap as pandoc markdown: an introduction, a timeline of the -latest release and the open milestones, one section per milestone with its -issues, and a closing section. Milestones are ordered by version, next -release first, and the text comes from the \code{roadmap} section of -\verb{_pkgdown.yml}. This is the body of the article written by \code{\link[=use_roadmap]{use_roadmap()}}. -} diff --git a/man/roadmap_site.Rd b/man/roadmap_site.Rd deleted file mode 100644 index 7a4233a..0000000 --- a/man/roadmap_site.Rd +++ /dev/null @@ -1,20 +0,0 @@ -% Generated by roxygen2: do not edit by hand -% Please edit documentation in R/build_site.R -\name{roadmap_site} -\alias{roadmap_site} -\title{Add the roadmap to a pkgdown site} -\usage{ -roadmap_site(pkg = ".") -} -\arguments{ -\item{pkg}{Path to the package, or a pkgdown object from -\code{\link[pkgdown:as_pkgdown]{pkgdown::as_pkgdown()}}.} -} -\value{ -A pkgdown object, unchanged when the roadmap is disabled. -} -\description{ -Adds \code{roadmap} to the navbar after \code{news}, and hides the roadmap article from -the Articles index while keeping it built. Call it after \code{\link[=use_roadmap]{use_roadmap()}} so -the article is parsed. Used by \code{\link[=build_site]{build_site()}} and the GitHub Action. -} diff --git a/man/use_roadmap.Rd b/man/use_roadmap.Rd deleted file mode 100644 index d0f1bea..0000000 --- a/man/use_roadmap.Rd +++ /dev/null @@ -1,20 +0,0 @@ -% Generated by roxygen2: do not edit by hand -% Please edit documentation in R/roadmap.R -\name{use_roadmap} -\alias{use_roadmap} -\title{Write the roadmap article} -\usage{ -use_roadmap(pkg = ".") -} -\arguments{ -\item{pkg}{Path to the package, or a pkgdown object from -\code{\link[pkgdown:as_pkgdown]{pkgdown::as_pkgdown()}}.} -} -\value{ -The path of the article, invisibly, or \code{NULL} invisibly if the -roadmap is disabled. -} -\description{ -Writes \code{vignettes/articles/roadmap.Rmd}, which calls \code{\link[=roadmap]{roadmap()}}, when -\code{roadmap: enabled: true} is set in \verb{_pkgdown.yml}. -} diff --git a/tests/testthat/helper-package.R b/tests/testthat/helper-package.R index 526a935..a36859a 100644 --- a/tests/testthat/helper-package.R +++ b/tests/testthat/helper-package.R @@ -15,6 +15,6 @@ fake_package <- function(config) { dir } -article_path <- function(dir) { - file.path(dir, "vignettes", "articles", "roadmap.Rmd") +article_path <- function(dir, ext) { + file.path(dir, "vignettes", "articles", paste0("roadmap.", ext)) } diff --git a/tests/testthat/test-roadmap.R b/tests/testthat/test-roadmap.R index e0a2ac8..dc5a700 100644 --- a/tests/testthat/test-roadmap.R +++ b/tests/testthat/test-roadmap.R @@ -1,25 +1,39 @@ -test_that("use_roadmap() writes the article only when enabled", { - disabled <- fake_package(c("roadmap:", " enabled: false")) - enabled <- fake_package(c("roadmap:", " enabled: true", " format: rmd")) +test_that("roadmap_site() writes the article in the configured format", { + local_mocked_bindings(roadmap = function(pkg) "## Stub") + rmd <- fake_package(c("roadmap:", " enabled: true")) + qmd <- fake_package(c("roadmap:", " enabled: true", " format: qmd")) - use_roadmap(disabled) - use_roadmap(enabled) + roadmap_site(rmd) + roadmap_site(qmd) - expect_false(file.exists(article_path(disabled))) - expect_true(file.exists(article_path(enabled))) + expect_true(file.exists(article_path(rmd, "Rmd"))) + expect_false(file.exists(article_path(rmd, "qmd"))) + expect_true(file.exists(article_path(qmd, "qmd"))) + expect_false(file.exists(article_path(qmd, "Rmd"))) + expect_match(readLines(article_path(qmd, "qmd")), "## Stub", all = FALSE) }) -test_that("use_roadmap() rejects formats other than rmd", { - pkg <- fake_package(c("roadmap:", " enabled: true", " format: qmd")) +test_that("roadmap_site() writes nothing when disabled", { + local_mocked_bindings(roadmap = function(pkg) stop("should not run")) + pkg <- fake_package(c("roadmap:", " enabled: false")) - expect_error(use_roadmap(pkg), "rmd") + roadmap_site(pkg) + + expect_false(file.exists(article_path(pkg, "Rmd"))) +}) + +test_that("roadmap_site() rejects formats other than rmd and qmd", { + local_mocked_bindings(roadmap = function(pkg) "## Stub") + pkg <- fake_package(c("roadmap:", " enabled: true", " format: html")) + + expect_error(roadmap_site(pkg), "rmd") }) test_that("roadmap_site() adds the navbar link only when enabled", { + local_mocked_bindings(roadmap = function(pkg) "## Stub") navbar <- c("navbar:", " structure:", " left: [home, news, stan]") - enabled <- fake_package(c(navbar, "roadmap:", " enabled: true", " format: rmd")) + enabled <- fake_package(c(navbar, "roadmap:", " enabled: true")) disabled <- fake_package(c(navbar, "roadmap:", " enabled: false")) - use_roadmap(enabled) expect_equal( roadmap_site(enabled)$meta$navbar$structure$left, diff --git a/vignettes/Setup.qmd b/vignettes/Setup.qmd index 5d53a2d..b6020be 100644 --- a/vignettes/Setup.qmd +++ b/vignettes/Setup.qmd @@ -95,7 +95,7 @@ roadmap: Customize with `roadmap.overrides.milestones` / `roadmap.overrides.issues` (keyed by milestone/issue number; both take `title` and `description`, issues also take `order`, and milestones can add your own `cards`, each with a `title`, `description`, optional `url` and optional `order`). Cards are ordered by issue number with closed issues last, unless you set `order`; custom cards come last unless they set it and `roadmap.exclude.labels` / `roadmap.exclude.issues`. See the defaults in [this package's config](https://github.com/stan-dev/pkgdown-config/blob/main/inst/pkgdown/_pkgdown.yml). The introduction (`roadmap.intro`) and the closing section (`roadmap.outro`) are Markdown with `{package}` and `{repo}` placeholders, and you can replace them. The timeline is generated from the latest GitHub release and the open milestones, and milestones are listed next release first, ordered by version (milestones whose title is not a version come last). -The GHA generates the roadmap article (`vignettes/articles/roadmap.Rmd`) and adds the navbar link for you. The article is rewritten on every build, so add it to `.gitignore`. For local builds use `pkgdownconfig::build_site()` in place of `pkgdown::build_site()`; it does the same and forwards all arguments. Plain `pkgdown::build_site()` does not add the navbar link or hide the article, so with the roadmap enabled it fails on sites that define their own `articles` sections. The roadmap is fetched from the GitHub API at build time. The GHA uses its own token; for local builds set a `GITHUB_PAT`, otherwise GitHub's unauthenticated limit of 60 requests per hour applies and the build can fail. +The GHA generates the roadmap article (`vignettes/articles/roadmap.Rmd`, or `roadmap.qmd` with `format: qmd`) and adds the navbar link for you. The article is rewritten on every build, so add it to `.gitignore`. For local builds use `pkgdownconfig::build_site()` in place of `pkgdown::build_site()`; it does the same and forwards all arguments. Plain `pkgdown::build_site()` does not add the navbar link or hide the article, so with the roadmap enabled it fails on sites that define their own `articles` sections. The roadmap is fetched from the GitHub API at build time. The GHA uses its own token; for local builds set a `GITHUB_PAT`, otherwise GitHub's unauthenticated limit of 60 requests per hour applies and the build can fail. ## GHA From 486609b6f49b611fd0432900665e126180c4c8d4 Mon Sep 17 00:00:00 2001 From: VisruthSK Date: Sat, 10 Oct 2026 13:57:44 -0700 Subject: [PATCH 16/27] Build the GitHub site through one helper build_gha_site() prepares the roadmap and builds the site for GitHub Pages, including the pull request preview overrides. The workflow calls it in one line. gha_override() builds the PR overrides and is tested directly. --- .github/workflows/pkgdown.yaml | 22 +------------------- R/build_site.R | 38 ++++++++++++++++++++++++++++++++++ inst/workflows/pkgdown.yaml | 22 +------------------- tests/testthat/test-gha.R | 17 +++++++++++++++ 4 files changed, 57 insertions(+), 42 deletions(-) create mode 100644 tests/testthat/test-gha.R diff --git a/.github/workflows/pkgdown.yaml b/.github/workflows/pkgdown.yaml index bbbea1f..b674102 100644 --- a/.github/workflows/pkgdown.yaml +++ b/.github/workflows/pkgdown.yaml @@ -48,27 +48,7 @@ jobs: - name: Build site env: PR_NUMBER: ${{ github.event.pull_request.number }} - run: | - pkg <- pkgdownconfig:::roadmap_site(".") - override <- list() - - pr_number <- Sys.getenv("PR_NUMBER") - if (nzchar(pr_number)) { - if (pkg$development$mode == "devel") { - Sys.setenv(PKGDOWN_DEV_MODE = "unreleased") - } - - override$url <- paste0(pkg$meta$url, "/pr/", pr_number) - override$template$params$noindex <- TRUE - } - - pkgdown::build_site_github_pages( - pkg = pkg, - lazy = FALSE, - run_dont_run = TRUE, - new_process = TRUE, - override = override - ) + run: pkgdownconfig:::build_gha_site() shell: Rscript {0} - name: Deploy PR preview diff --git a/R/build_site.R b/R/build_site.R index e02af4e..2b35947 100644 --- a/R/build_site.R +++ b/R/build_site.R @@ -48,3 +48,41 @@ roadmap_site <- function(path = ".") { ) pkg } + +#' Build the site for GitHub Pages +#' +#' Used by the GitHub Action. Prepares the roadmap, then builds the site. With a +#' pull request number, the site is built under `/pr/` and not indexed. +#' +#' @param pr_number The pull request number, or an empty string. +#' @return The result of [pkgdown::build_site_github_pages()], invisibly. +#' @noRd +build_gha_site <- function(pr_number = Sys.getenv("PR_NUMBER")) { + pkg <- roadmap_site(".") + if (nzchar(pr_number) && pkg$development$mode == "devel") { + Sys.setenv(PKGDOWN_DEV_MODE = "unreleased") + } + pkgdown::build_site_github_pages( + pkg = pkg, + lazy = FALSE, + run_dont_run = TRUE, + new_process = TRUE, + override = gha_override(pkg, pr_number) + ) +} + +#' Build overrides for a pull request preview +#' +#' @param pkg A pkgdown object. +#' @param pr_number The pull request number, or an empty string. +#' @return A pkgdown override list, empty when there is no pull request. +#' @noRd +gha_override <- function(pkg, pr_number) { + if (!nzchar(pr_number)) { + return(list()) + } + list( + url = paste0(pkg$meta$url, "/pr/", pr_number), + template = list(params = list(noindex = TRUE)) + ) +} diff --git a/inst/workflows/pkgdown.yaml b/inst/workflows/pkgdown.yaml index bbbea1f..b674102 100644 --- a/inst/workflows/pkgdown.yaml +++ b/inst/workflows/pkgdown.yaml @@ -48,27 +48,7 @@ jobs: - name: Build site env: PR_NUMBER: ${{ github.event.pull_request.number }} - run: | - pkg <- pkgdownconfig:::roadmap_site(".") - override <- list() - - pr_number <- Sys.getenv("PR_NUMBER") - if (nzchar(pr_number)) { - if (pkg$development$mode == "devel") { - Sys.setenv(PKGDOWN_DEV_MODE = "unreleased") - } - - override$url <- paste0(pkg$meta$url, "/pr/", pr_number) - override$template$params$noindex <- TRUE - } - - pkgdown::build_site_github_pages( - pkg = pkg, - lazy = FALSE, - run_dont_run = TRUE, - new_process = TRUE, - override = override - ) + run: pkgdownconfig:::build_gha_site() shell: Rscript {0} - name: Deploy PR preview diff --git a/tests/testthat/test-gha.R b/tests/testthat/test-gha.R new file mode 100644 index 0000000..f8af58b --- /dev/null +++ b/tests/testthat/test-gha.R @@ -0,0 +1,17 @@ +test_that("gha_override() points a pull request preview at its own URL, unindexed", { + pkg <- pkgdown::as_pkgdown(fake_package(c("url: https://example.org"))) + + site <- pkgdown::as_pkgdown( + fake_package(c("url: https://example.org")), + override = gha_override(pkg, "18") + ) + + expect_equal(site$meta$url, "https://example.org/pr/18") + expect_true(site$meta$template$params$noindex) +}) + +test_that("gha_override() leaves the site alone without a pull request", { + pkg <- pkgdown::as_pkgdown(fake_package(c("url: https://example.org"))) + + expect_equal(gha_override(pkg, ""), list()) +}) From 66ebcdc4dac6660da4dabbbfba4f4b44146e71b9 Mon Sep 17 00:00:00 2001 From: VisruthSK Date: Sat, 10 Oct 2026 14:10:29 -0700 Subject: [PATCH 17/27] Rename roadmap() to roadmap_markdown() and add roadmap tests roadmap_markdown() returns the markdown, so the name says what it does. Add fixture tests for filtering, ordering and overrides with GitHub mocked, and an end-to-end build of a site with its own articles section. --- R/roadmap.R | 10 ++-- tests/testthat/helper-github.R | 28 ++++++++++ tests/testthat/helper-package.R | 4 +- tests/testthat/test-roadmap-markdown.R | 76 ++++++++++++++++++++++++++ tests/testthat/test-roadmap.R | 8 +-- tests/testthat/test-site.R | 29 ++++++++++ 6 files changed, 145 insertions(+), 10 deletions(-) create mode 100644 tests/testthat/helper-github.R create mode 100644 tests/testthat/test-roadmap-markdown.R create mode 100644 tests/testthat/test-site.R diff --git a/R/roadmap.R b/R/roadmap.R index b6d7dfb..0c0059b 100644 --- a/R/roadmap.R +++ b/R/roadmap.R @@ -7,7 +7,7 @@ #' @param pkg A pkgdown object from [pkgdown::as_pkgdown()]. #' @return A character vector of markdown lines. #' @noRd -roadmap <- function(pkg) { +roadmap_markdown <- function(pkg) { config <- pkg$meta$roadmap repo <- gsub("^https://github.com/|/$", "", pkg$repo$url$home) issues <- gh::gh( @@ -27,7 +27,7 @@ roadmap <- function(pkg) { ) milestones <- split( issues, - vapply(issues, function(issue) issue$milestone$number, 0L) + vapply(issues, function(issue) issue$milestone$number, numeric(1)) ) titles <- vapply(milestones, function(m) m[[1]]$milestone$title, "") versions <- numeric_version(sub("^v", "", titles), strict = FALSE) @@ -49,11 +49,11 @@ roadmap <- function(pkg) { repo ) }, - unlist(lapply( + unname(unlist(lapply( milestones[order(is.na(versions), versions)], roadmap_milestone, config - )), + ))), fill(config$outro) ) } @@ -79,7 +79,7 @@ write_roadmap <- function(pkg) { paste0("roadmap.", extensions[names(extensions) != format]) )) path <- file.path(dir, paste0("roadmap.", extensions[[format]])) - writeLines(c("---", "title: Roadmap", "---", "", roadmap(pkg)), path) + writeLines(c("---", "title: Roadmap", "---", "", roadmap_markdown(pkg)), path) invisible(path) } diff --git a/tests/testthat/helper-github.R b/tests/testthat/helper-github.R new file mode 100644 index 0000000..05668ff --- /dev/null +++ b/tests/testthat/helper-github.R @@ -0,0 +1,28 @@ +fake_milestone <- function(number, title, state = "open") { + list( + number = number, + title = title, + state = state, + description = paste(title, "description"), + html_url = paste0("https://github.com/stan-dev/fake/milestone/", number) + ) +} + +fake_issue <- function(number, title, milestone, state = "open", + labels = character(), pull_request = FALSE) { + list( + number = number, + title = title, + html_url = paste0("https://github.com/stan-dev/fake/issues/", number), + state = state, + labels = lapply(labels, function(label) list(name = label)), + pull_request = if (pull_request) list(url = "https://example.org"), + milestone = milestone + ) +} + +fake_gh <- function(issues, release = list(tag_name = "v2.10.1")) { + function(endpoint, ...) { + if (grepl("releases", endpoint)) release else issues + } +} diff --git a/tests/testthat/helper-package.R b/tests/testthat/helper-package.R index a36859a..a4e2777 100644 --- a/tests/testthat/helper-package.R +++ b/tests/testthat/helper-package.R @@ -7,7 +7,9 @@ fake_package <- function(config) { "Version: 1.0", "Title: Fake", "Description: Fake.", - "License: MIT" + "License: MIT", + "URL: https://github.com/stan-dev/fake", + "Authors@R: person(\"A\", \"B\", role = c(\"aut\", \"cre\"), email = \"a@b.c\")" ), file.path(dir, "DESCRIPTION") ) diff --git a/tests/testthat/test-roadmap-markdown.R b/tests/testthat/test-roadmap-markdown.R new file mode 100644 index 0000000..dbadbd5 --- /dev/null +++ b/tests/testthat/test-roadmap-markdown.R @@ -0,0 +1,76 @@ +test_that("roadmap_markdown() leaves out pull requests, closed milestones and excluded issues", { + issues <- list( + fake_issue(1, "Kept", fake_milestone(1, "v3.0.0")), + fake_issue(2, "Pull request", fake_milestone(1, "v3.0.0"), pull_request = TRUE), + fake_issue(3, "Internal work", fake_milestone(1, "v3.0.0"), labels = "internal-tooling"), + fake_issue(4, "Excluded by number", fake_milestone(1, "v3.0.0")), + fake_issue(5, "Old milestone", fake_milestone(2, "v2.0.0", state = "closed")) + ) + local_mocked_bindings(gh = fake_gh(issues), .package = "gh") + pkg <- pkgdown::as_pkgdown(fake_package(c( + "roadmap:", + " enabled: true", + " exclude:", + " labels: [internal*]", + " issues: [4]" + ))) + + markdown <- paste(roadmap_markdown(pkg), collapse = "\n") + + expect_match(markdown, "Kept") + expect_no_match(markdown, "Pull request|Internal work|Excluded by number|Old milestone") +}) + +test_that("roadmap_markdown() orders milestones by version and cards by number, open first", { + issues <- list( + fake_issue(9, "Open late", fake_milestone(11, "v2.11.0")), + fake_issue(3, "Closed early", fake_milestone(11, "v2.11.0"), state = "closed"), + fake_issue(7, "Older version", fake_milestone(9, "v2.9.0")), + fake_issue(8, "Backlog item", fake_milestone(12, "Backlog")), + fake_issue(1, "Newest version", fake_milestone(13, "v3.0.0")) + ) + local_mocked_bindings(gh = fake_gh(issues), .package = "gh") + pkg <- pkgdown::as_pkgdown(fake_package(c("roadmap:", " enabled: true"))) + + headings <- grep("^## ", roadmap_markdown(pkg), value = TRUE) + cards <- grep("^#### ", roadmap_markdown(pkg), value = TRUE) + + expect_equal(headings, c("## v2.9.0", "## v2.11.0", "## v3.0.0", "## Backlog")) + expect_equal(grep("Open late|Closed early", cards, value = TRUE), c( + "#### [Open late](https://github.com/stan-dev/fake/issues/9)", + "#### [✓ Closed early](https://github.com/stan-dev/fake/issues/3)" + )) +}) + +test_that("roadmap_markdown() applies milestone and issue overrides and custom cards", { + issues <- list( + fake_issue(9, "Original title", fake_milestone(3, "v3.0.0")), + fake_issue(5, "Second", fake_milestone(3, "v3.0.0")) + ) + local_mocked_bindings(gh = fake_gh(issues), .package = "gh") + pkg <- pkgdown::as_pkgdown(fake_package(c( + "roadmap:", + " enabled: true", + " overrides:", + " milestones:", + " \"3\":", + " title: Version three", + " description: Custom description.", + " cards:", + " - title: Your ideas", + " description: Tell us.", + " order: 0", + " issues:", + " \"9\":", + " title: Renamed", + " description: Custom card text.", + " order: 1" + ))) + + markdown <- roadmap_markdown(pkg) + + expect_true("## Version three" %in% markdown) + expect_true("Custom description." %in% markdown) + expect_match(paste(markdown, collapse = "\n"), "Your ideas.*Renamed.*Second") + expect_match(paste(markdown, collapse = "\n"), "Custom card text.") +}) diff --git a/tests/testthat/test-roadmap.R b/tests/testthat/test-roadmap.R index dc5a700..42063f0 100644 --- a/tests/testthat/test-roadmap.R +++ b/tests/testthat/test-roadmap.R @@ -1,5 +1,5 @@ test_that("roadmap_site() writes the article in the configured format", { - local_mocked_bindings(roadmap = function(pkg) "## Stub") + local_mocked_bindings(roadmap_markdown = function(pkg) "## Stub") rmd <- fake_package(c("roadmap:", " enabled: true")) qmd <- fake_package(c("roadmap:", " enabled: true", " format: qmd")) @@ -14,7 +14,7 @@ test_that("roadmap_site() writes the article in the configured format", { }) test_that("roadmap_site() writes nothing when disabled", { - local_mocked_bindings(roadmap = function(pkg) stop("should not run")) + local_mocked_bindings(roadmap_markdown = function(pkg) stop("should not run")) pkg <- fake_package(c("roadmap:", " enabled: false")) roadmap_site(pkg) @@ -23,14 +23,14 @@ test_that("roadmap_site() writes nothing when disabled", { }) test_that("roadmap_site() rejects formats other than rmd and qmd", { - local_mocked_bindings(roadmap = function(pkg) "## Stub") + local_mocked_bindings(roadmap_markdown = function(pkg) "## Stub") pkg <- fake_package(c("roadmap:", " enabled: true", " format: html")) expect_error(roadmap_site(pkg), "rmd") }) test_that("roadmap_site() adds the navbar link only when enabled", { - local_mocked_bindings(roadmap = function(pkg) "## Stub") + local_mocked_bindings(roadmap_markdown = function(pkg) "## Stub") navbar <- c("navbar:", " structure:", " left: [home, news, stan]") enabled <- fake_package(c(navbar, "roadmap:", " enabled: true")) disabled <- fake_package(c(navbar, "roadmap:", " enabled: false")) diff --git a/tests/testthat/test-site.R b/tests/testthat/test-site.R new file mode 100644 index 0000000..b9acba0 --- /dev/null +++ b/tests/testthat/test-site.R @@ -0,0 +1,29 @@ +test_that("build_site() builds the roadmap beside a site with its own articles", { + skip_on_cran() + skip_if_not_installed("pkgdownconfig") + issues <- list(fake_issue(1, "Roadmap item", fake_milestone(1, "v3.0.0"))) + local_mocked_bindings(gh = fake_gh(issues), .package = "gh") + withr::local_envvar(CI = "true") + dir <- fake_package(c( + "url: https://example.org", + "template:", + " package: pkgdownconfig", + "roadmap:", + " enabled: true", + "articles:", + " - title: Guides", + " contents:", + " - articles/guide" + )) + dir.create(file.path(dir, "vignettes", "articles"), recursive = TRUE) + writeLines(c("---", "title: Guide", "---", "", "Text."), file.path(dir, "vignettes", "articles", "guide.Rmd")) + + pkgdownconfig::build_site(dir, preview = FALSE, new_process = FALSE, quiet = TRUE) + + docs <- file.path(dir, "docs") + index <- readLines(file.path(docs, "articles", "index.html")) + expect_true(file.exists(file.path(docs, "articles", "roadmap.html"))) + expect_true(any(grepl('href="guide.html"', index))) + expect_false(any(grepl('href="roadmap.html"', index))) + expect_true(any(grepl('href="articles/roadmap.html"', readLines(file.path(docs, "index.html"))))) +}) From a8858f7d4486f1c7c5ccecec88a0fab8c5cdeb44 Mon Sep 17 00:00:00 2001 From: VisruthSK Date: Sat, 10 Oct 2026 14:11:55 -0700 Subject: [PATCH 18/27] Refuse to write a roadmap beside one in the other format Writing the article no longer deletes an existing roadmap file in the other format. It stops with an error instead, so the choice stays with the user. --- R/roadmap.R | 11 +++++++++-- tests/testthat/test-roadmap.R | 11 +++++++++++ 2 files changed, 20 insertions(+), 2 deletions(-) diff --git a/R/roadmap.R b/R/roadmap.R index 0c0059b..fca45e3 100644 --- a/R/roadmap.R +++ b/R/roadmap.R @@ -74,10 +74,17 @@ write_roadmap <- function(pkg) { dir <- file.path(pkg$src_path, "vignettes", "articles") dir.create(dir, recursive = TRUE, showWarnings = FALSE) extensions <- c(rmd = "Rmd", qmd = "qmd") - unlink(file.path( + other <- file.path( dir, paste0("roadmap.", extensions[names(extensions) != format]) - )) + ) + if (file.exists(other)) { + stop( + "`", other, "` exists alongside the ", format, " article. ", + "Remove one of them.", + call. = FALSE + ) + } path <- file.path(dir, paste0("roadmap.", extensions[[format]])) writeLines(c("---", "title: Roadmap", "---", "", roadmap_markdown(pkg)), path) invisible(path) diff --git a/tests/testthat/test-roadmap.R b/tests/testthat/test-roadmap.R index 42063f0..864e0cb 100644 --- a/tests/testthat/test-roadmap.R +++ b/tests/testthat/test-roadmap.R @@ -44,3 +44,14 @@ test_that("roadmap_site() adds the navbar link only when enabled", { c("home", "news", "stan") ) }) + +test_that("roadmap_site() refuses to write beside an article in the other format", { + local_mocked_bindings(roadmap_markdown = function(pkg) "## Stub") + pkg <- fake_package(c("roadmap:", " enabled: true", " format: qmd")) + dir.create(file.path(pkg, "vignettes", "articles"), recursive = TRUE) + writeLines("kept", article_path(pkg, "Rmd")) + + expect_error(roadmap_site(pkg), "Remove one of them") + expect_equal(readLines(article_path(pkg, "Rmd")), "kept") + expect_false(file.exists(article_path(pkg, "qmd"))) +}) From 641e4f38432d355e9c58c62cdd065885734cb0ec Mon Sep 17 00:00:00 2001 From: VisruthSK Date: Sat, 10 Oct 2026 14:12:48 -0700 Subject: [PATCH 19/27] Export build_gha_site() and call it from the workflow --- .github/workflows/pkgdown.yaml | 2 +- NAMESPACE | 1 + R/build_site.R | 10 +++------- inst/workflows/pkgdown.yaml | 2 +- man/build_gha_site.Rd | 14 ++++++++++++++ 5 files changed, 20 insertions(+), 9 deletions(-) create mode 100644 man/build_gha_site.Rd diff --git a/.github/workflows/pkgdown.yaml b/.github/workflows/pkgdown.yaml index b674102..c9a2f2b 100644 --- a/.github/workflows/pkgdown.yaml +++ b/.github/workflows/pkgdown.yaml @@ -48,7 +48,7 @@ jobs: - name: Build site env: PR_NUMBER: ${{ github.event.pull_request.number }} - run: pkgdownconfig:::build_gha_site() + run: pkgdownconfig::build_gha_site() shell: Rscript {0} - name: Deploy PR preview diff --git a/NAMESPACE b/NAMESPACE index 7c8f416..7248269 100644 --- a/NAMESPACE +++ b/NAMESPACE @@ -1,4 +1,5 @@ # Generated by roxygen2: do not edit by hand +export(build_gha_site) export(build_site) export(use_pkgdown_gha) diff --git a/R/build_site.R b/R/build_site.R index 2b35947..8d803a4 100644 --- a/R/build_site.R +++ b/R/build_site.R @@ -49,14 +49,10 @@ roadmap_site <- function(path = ".") { pkg } -#' Build the site for GitHub Pages +#' Build the GitHub Pages site #' -#' Used by the GitHub Action. Prepares the roadmap, then builds the site. With a -#' pull request number, the site is built under `/pr/` and not indexed. -#' -#' @param pr_number The pull request number, or an empty string. -#' @return The result of [pkgdown::build_site_github_pages()], invisibly. -#' @noRd +#' @param pr_number Pull request number. Empty builds the main site. +#' @export build_gha_site <- function(pr_number = Sys.getenv("PR_NUMBER")) { pkg <- roadmap_site(".") if (nzchar(pr_number) && pkg$development$mode == "devel") { diff --git a/inst/workflows/pkgdown.yaml b/inst/workflows/pkgdown.yaml index b674102..c9a2f2b 100644 --- a/inst/workflows/pkgdown.yaml +++ b/inst/workflows/pkgdown.yaml @@ -48,7 +48,7 @@ jobs: - name: Build site env: PR_NUMBER: ${{ github.event.pull_request.number }} - run: pkgdownconfig:::build_gha_site() + run: pkgdownconfig::build_gha_site() shell: Rscript {0} - name: Deploy PR preview diff --git a/man/build_gha_site.Rd b/man/build_gha_site.Rd new file mode 100644 index 0000000..8fc0bde --- /dev/null +++ b/man/build_gha_site.Rd @@ -0,0 +1,14 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/build_site.R +\name{build_gha_site} +\alias{build_gha_site} +\title{Build the GitHub Pages site} +\usage{ +build_gha_site(pr_number = Sys.getenv("PR_NUMBER")) +} +\arguments{ +\item{pr_number}{Pull request number. Empty builds the main site.} +} +\description{ +Build the GitHub Pages site +} From 16b4bac10d9eb50638f6529f13a5b0e3f15dcf72 Mon Sep 17 00:00:00 2001 From: VisruthSK Date: Sat, 10 Oct 2026 14:13:53 -0700 Subject: [PATCH 20/27] Formatted --- R/roadmap.R | 6 +++- tests/testthat/helper-github.R | 10 ++++-- tests/testthat/test-roadmap-markdown.R | 48 ++++++++++++++++++++------ tests/testthat/test-site.R | 17 +++++++-- tests/testthat/test-workflow.R | 7 +++- 5 files changed, 71 insertions(+), 17 deletions(-) diff --git a/R/roadmap.R b/R/roadmap.R index fca45e3..bf8222a 100644 --- a/R/roadmap.R +++ b/R/roadmap.R @@ -80,7 +80,11 @@ write_roadmap <- function(pkg) { ) if (file.exists(other)) { stop( - "`", other, "` exists alongside the ", format, " article. ", + "`", + other, + "` exists alongside the ", + format, + " article. ", "Remove one of them.", call. = FALSE ) diff --git a/tests/testthat/helper-github.R b/tests/testthat/helper-github.R index 05668ff..9af67bf 100644 --- a/tests/testthat/helper-github.R +++ b/tests/testthat/helper-github.R @@ -8,8 +8,14 @@ fake_milestone <- function(number, title, state = "open") { ) } -fake_issue <- function(number, title, milestone, state = "open", - labels = character(), pull_request = FALSE) { +fake_issue <- function( + number, + title, + milestone, + state = "open", + labels = character(), + pull_request = FALSE +) { list( number = number, title = title, diff --git a/tests/testthat/test-roadmap-markdown.R b/tests/testthat/test-roadmap-markdown.R index dbadbd5..d1c7a3d 100644 --- a/tests/testthat/test-roadmap-markdown.R +++ b/tests/testthat/test-roadmap-markdown.R @@ -1,10 +1,24 @@ test_that("roadmap_markdown() leaves out pull requests, closed milestones and excluded issues", { issues <- list( fake_issue(1, "Kept", fake_milestone(1, "v3.0.0")), - fake_issue(2, "Pull request", fake_milestone(1, "v3.0.0"), pull_request = TRUE), - fake_issue(3, "Internal work", fake_milestone(1, "v3.0.0"), labels = "internal-tooling"), + fake_issue( + 2, + "Pull request", + fake_milestone(1, "v3.0.0"), + pull_request = TRUE + ), + fake_issue( + 3, + "Internal work", + fake_milestone(1, "v3.0.0"), + labels = "internal-tooling" + ), fake_issue(4, "Excluded by number", fake_milestone(1, "v3.0.0")), - fake_issue(5, "Old milestone", fake_milestone(2, "v2.0.0", state = "closed")) + fake_issue( + 5, + "Old milestone", + fake_milestone(2, "v2.0.0", state = "closed") + ) ) local_mocked_bindings(gh = fake_gh(issues), .package = "gh") pkg <- pkgdown::as_pkgdown(fake_package(c( @@ -18,13 +32,21 @@ test_that("roadmap_markdown() leaves out pull requests, closed milestones and ex markdown <- paste(roadmap_markdown(pkg), collapse = "\n") expect_match(markdown, "Kept") - expect_no_match(markdown, "Pull request|Internal work|Excluded by number|Old milestone") + expect_no_match( + markdown, + "Pull request|Internal work|Excluded by number|Old milestone" + ) }) test_that("roadmap_markdown() orders milestones by version and cards by number, open first", { issues <- list( fake_issue(9, "Open late", fake_milestone(11, "v2.11.0")), - fake_issue(3, "Closed early", fake_milestone(11, "v2.11.0"), state = "closed"), + fake_issue( + 3, + "Closed early", + fake_milestone(11, "v2.11.0"), + state = "closed" + ), fake_issue(7, "Older version", fake_milestone(9, "v2.9.0")), fake_issue(8, "Backlog item", fake_milestone(12, "Backlog")), fake_issue(1, "Newest version", fake_milestone(13, "v3.0.0")) @@ -35,11 +57,17 @@ test_that("roadmap_markdown() orders milestones by version and cards by number, headings <- grep("^## ", roadmap_markdown(pkg), value = TRUE) cards <- grep("^#### ", roadmap_markdown(pkg), value = TRUE) - expect_equal(headings, c("## v2.9.0", "## v2.11.0", "## v3.0.0", "## Backlog")) - expect_equal(grep("Open late|Closed early", cards, value = TRUE), c( - "#### [Open late](https://github.com/stan-dev/fake/issues/9)", - "#### [✓ Closed early](https://github.com/stan-dev/fake/issues/3)" - )) + expect_equal( + headings, + c("## v2.9.0", "## v2.11.0", "## v3.0.0", "## Backlog") + ) + expect_equal( + grep("Open late|Closed early", cards, value = TRUE), + c( + "#### [Open late](https://github.com/stan-dev/fake/issues/9)", + "#### [✓ Closed early](https://github.com/stan-dev/fake/issues/3)" + ) + ) }) test_that("roadmap_markdown() applies milestone and issue overrides and custom cards", { diff --git a/tests/testthat/test-site.R b/tests/testthat/test-site.R index b9acba0..5225622 100644 --- a/tests/testthat/test-site.R +++ b/tests/testthat/test-site.R @@ -16,14 +16,25 @@ test_that("build_site() builds the roadmap beside a site with its own articles", " - articles/guide" )) dir.create(file.path(dir, "vignettes", "articles"), recursive = TRUE) - writeLines(c("---", "title: Guide", "---", "", "Text."), file.path(dir, "vignettes", "articles", "guide.Rmd")) + writeLines( + c("---", "title: Guide", "---", "", "Text."), + file.path(dir, "vignettes", "articles", "guide.Rmd") + ) - pkgdownconfig::build_site(dir, preview = FALSE, new_process = FALSE, quiet = TRUE) + pkgdownconfig::build_site( + dir, + preview = FALSE, + new_process = FALSE, + quiet = TRUE + ) docs <- file.path(dir, "docs") index <- readLines(file.path(docs, "articles", "index.html")) expect_true(file.exists(file.path(docs, "articles", "roadmap.html"))) expect_true(any(grepl('href="guide.html"', index))) expect_false(any(grepl('href="roadmap.html"', index))) - expect_true(any(grepl('href="articles/roadmap.html"', readLines(file.path(docs, "index.html"))))) + expect_true(any(grepl( + 'href="articles/roadmap.html"', + readLines(file.path(docs, "index.html")) + ))) }) diff --git a/tests/testthat/test-workflow.R b/tests/testthat/test-workflow.R index dd73c74..e2d47b3 100644 --- a/tests/testthat/test-workflow.R +++ b/tests/testthat/test-workflow.R @@ -11,5 +11,10 @@ test_that("use_pkgdown_gha() copies the workflow into the package", { use_pkgdown_gha(pkg) - expect_true(file.exists(file.path(pkg, ".github", "workflows", "pkgdown.yaml"))) + expect_true(file.exists(file.path( + pkg, + ".github", + "workflows", + "pkgdown.yaml" + ))) }) From 62515d2dfab517bf17a2f68bdaf5c36ed4f0e1c2 Mon Sep 17 00:00:00 2001 From: VisruthSK Date: Sat, 10 Oct 2026 14:27:17 -0700 Subject: [PATCH 21/27] Tidy the roadmap code Inline the helpers that had a single caller, drop the version comparison in favour of plain labels, and return only for a 404 when reading the latest release. Classes are joined with a plain space. roadmap() is now roadmap_markdown(). Add tests for the release errors, the overrides of build_gha_site(), and a Quarto build. --- R/build_site.R | 53 ++++++----- R/roadmap.R | 123 +++++-------------------- tests/testthat/test-gha.R | 29 +++--- tests/testthat/test-roadmap-markdown.R | 31 +++++++ tests/testthat/test-site.R | 39 +++++--- 5 files changed, 129 insertions(+), 146 deletions(-) diff --git a/R/build_site.R b/R/build_site.R index 8d803a4..6c81c61 100644 --- a/R/build_site.R +++ b/R/build_site.R @@ -23,10 +23,32 @@ build_site <- function(pkg = ".", ..., override = list()) { #' @noRd roadmap_site <- function(path = ".") { pkg <- pkgdown::as_pkgdown(path) - if (!isTRUE(pkg$meta$roadmap$enabled)) { + config <- pkg$meta$roadmap + if (!isTRUE(config$enabled)) { return(pkg) } - write_roadmap(pkg) + + article_format <- config$format %||% "rmd" + if (!article_format %in% c("rmd", "qmd")) { + stop("`roadmap: format` must be `rmd` or `qmd`.", call. = FALSE) + } + extensions <- c(rmd = "Rmd", qmd = "qmd") + dir <- file.path(pkg$src_path, "vignettes", "articles") + other_format <- setdiff(names(extensions), article_format) + other <- file.path(dir, paste0("roadmap.", extensions[[other_format]])) + if (file.exists(other)) { + stop( + "`", other, "` exists alongside the ", article_format, " article. ", + "Remove one of them.", + call. = FALSE + ) + } + dir.create(dir, recursive = TRUE, showWarnings = FALSE) + writeLines( + c("---", "title: Roadmap", "---", "", roadmap_markdown(pkg)), + file.path(dir, paste0("roadmap.", extensions[[article_format]])) + ) + pkg <- pkgdown::as_pkgdown(path) left <- pkg$meta$navbar$structure$left pkg$meta$navbar$structure$left <- append( @@ -55,30 +77,19 @@ roadmap_site <- function(path = ".") { #' @export build_gha_site <- function(pr_number = Sys.getenv("PR_NUMBER")) { pkg <- roadmap_site(".") - if (nzchar(pr_number) && pkg$development$mode == "devel") { - Sys.setenv(PKGDOWN_DEV_MODE = "unreleased") + override <- list() + if (nzchar(pr_number)) { + if (pkg$development$mode == "devel") { + Sys.setenv(PKGDOWN_DEV_MODE = "unreleased") + } + override$url <- paste0(pkg$meta$url, "/pr/", pr_number) + override$template$params$noindex <- TRUE } pkgdown::build_site_github_pages( pkg = pkg, lazy = FALSE, run_dont_run = TRUE, new_process = TRUE, - override = gha_override(pkg, pr_number) - ) -} - -#' Build overrides for a pull request preview -#' -#' @param pkg A pkgdown object. -#' @param pr_number The pull request number, or an empty string. -#' @return A pkgdown override list, empty when there is no pull request. -#' @noRd -gha_override <- function(pkg, pr_number) { - if (!nzchar(pr_number)) { - return(list()) - } - list( - url = paste0(pkg$meta$url, "/pr/", pr_number), - template = list(params = list(noindex = TRUE)) + override = override ) } diff --git a/R/roadmap.R b/R/roadmap.R index bf8222a..2c8ff9f 100644 --- a/R/roadmap.R +++ b/R/roadmap.R @@ -10,6 +10,11 @@ roadmap_markdown <- function(pkg) { config <- pkg$meta$roadmap repo <- gsub("^https://github.com/|/$", "", pkg$repo$url$home) + fill <- function(text) { + text <- gsub("{package}", pkg$package, text, fixed = TRUE) + gsub("{repo}", repo, text, fixed = TRUE) + } + issues <- gh::gh( "/repos/{repo}/issues", repo = repo, @@ -17,34 +22,37 @@ roadmap_markdown <- function(pkg) { state = "all", .limit = Inf ) + excluded_labels <- utils::glob2rx(unlist(config$exclude$labels)) + excluded_issues <- unlist(config$exclude$issues) issues <- Filter( function(issue) { + labels <- vapply(issue$labels, `[[`, "", "name") is.null(issue$pull_request) && issue$milestone$state == "open" && - !roadmap_excluded(issue, config$exclude) + !(as.character(issue$number) %in% excluded_issues) && + !any(vapply(excluded_labels, function(p) any(grepl(p, labels)), NA)) }, issues ) + milestones <- split( issues, vapply(issues, function(issue) issue$milestone$number, numeric(1)) ) titles <- vapply(milestones, function(m) m[[1]]$milestone$title, "") versions <- numeric_version(sub("^v", "", titles), strict = FALSE) - versioned <- !is.na(versions) release <- tryCatch( gh::gh("/repos/{repo}/releases/latest", repo = repo), - error = function(e) NULL + http_error_404 = function(e) NULL ) - fill <- function(text) roadmap_fill(text, pkg$package, repo) c( sprintf("[Last updated: %s]{.rm-updated}", format(Sys.Date(), "%B %Y")), "", fill(config$intro), - if (any(versioned)) { + if (any(!is.na(versions))) { roadmap_timeline( - milestones[versioned][order(versions[versioned])], + milestones[!is.na(versions)][order(versions[!is.na(versions)])], release, repo ) @@ -58,66 +66,15 @@ roadmap_markdown <- function(pkg) { ) } -#' Write the roadmap article -#' -#' Writes `vignettes/articles/roadmap.Rmd`, or `roadmap.qmd` when `format` is -#' `qmd`, containing the roadmap markdown. Removes the other format's file. -#' -#' @param pkg A pkgdown object from [pkgdown::as_pkgdown()]. -#' @return The path of the article, invisibly. -#' @noRd -write_roadmap <- function(pkg) { - format <- pkg$meta$roadmap$format %||% "rmd" - if (!format %in% c("rmd", "qmd")) { - stop("`roadmap: format` must be `rmd` or `qmd`.", call. = FALSE) - } - dir <- file.path(pkg$src_path, "vignettes", "articles") - dir.create(dir, recursive = TRUE, showWarnings = FALSE) - extensions <- c(rmd = "Rmd", qmd = "qmd") - other <- file.path( - dir, - paste0("roadmap.", extensions[names(extensions) != format]) - ) - if (file.exists(other)) { - stop( - "`", - other, - "` exists alongside the ", - format, - " article. ", - "Remove one of them.", - call. = FALSE - ) - } - path <- file.path(dir, paste0("roadmap.", extensions[[format]])) - writeLines(c("---", "title: Roadmap", "---", "", roadmap_markdown(pkg)), path) - invisible(path) -} - -#' Substitute the package placeholders in roadmap text -#' -#' @param text Text from the roadmap config. -#' @param package The package name, for `{package}`. -#' @param repo The repository as `owner/name`, for `{repo}`. -#' @return `text` with the placeholders replaced. -#' @noRd -roadmap_fill <- function(text, package, repo) { - text <- gsub("{package}", package, text, fixed = TRUE) - gsub("{repo}", repo, text, fixed = TRUE) -} - #' Markdown for the release timeline #' #' @param milestones The issues grouped by milestone, in ascending version #' order. -#' @param release The latest release as returned by the GitHub API, or `NULL`. +#' @param release The latest release from the GitHub API, or `NULL`. #' @param repo The repository as `owner/name`. #' @return A character vector of markdown lines. #' @noRd roadmap_timeline <- function(milestones, release, repo) { - current <- if (!is.null(release)) { - numeric_version(sub("^v", "", release$tag_name), strict = FALSE) - } released <- if (!is.null(release)) { list(roadmap_station( release$tag_name, @@ -127,12 +84,11 @@ roadmap_timeline <- function(milestones, release, repo) { "rm-past" )) } - upcoming <- lapply(milestones, function(m) { - milestone <- m[[1]]$milestone - version <- numeric_version(sub("^v", "", milestone$title), strict = FALSE) + upcoming <- lapply(seq_along(milestones), function(i) { + milestone <- milestones[[i]][[1]]$milestone roadmap_station( milestone$title, - roadmap_kind(version, current), + if (i == 1) "Next release" else "Upcoming release", "Milestone", milestone$html_url ) @@ -143,7 +99,7 @@ roadmap_timeline <- function(milestones, release, repo) { "Open an issue", sprintf("https://github.com/%s/issues/new", repo) )) - stations <- c(released, unname(upcoming), beyond) + stations <- c(released, upcoming, beyond) c( "", @@ -167,8 +123,9 @@ roadmap_timeline <- function(milestones, release, repo) { #' @return A character vector of markdown lines. #' @noRd roadmap_station <- function(title, subtitle, label, href, class = NULL) { + classes <- paste0(".", c("rm-station", class), collapse = " ") c( - paste0(":::: {", paste(c(".rm-station", class), collapse = " ."), "}"), + sprintf(":::: {%s}", classes), sprintf("[%s]{.rm-station-title}", title), "", sprintf("[%s]{.rm-station-sub}", subtitle), @@ -179,28 +136,6 @@ roadmap_station <- function(title, subtitle, label, href, class = NULL) { ) } -#' Describe how a version follows the current release -#' -#' @param version The milestone version. -#' @param current The latest released version, or `NULL`. -#' @return `"Next major release"`, `"Next minor release"`, -#' `"Next patch release"` or `"Upcoming release"`. -#' @noRd -roadmap_kind <- function(version, current) { - if (is.null(current) || is.na(version) || is.na(current)) { - return("Upcoming release") - } - next_version <- c(unlist(version), 0, 0) - current <- c(unlist(current), 0, 0) - if (next_version[1] > current[1]) { - "Next major release" - } else if (next_version[2] > current[2]) { - "Next minor release" - } else { - "Next patch release" - } -} - #' Markdown for one milestone #' #' Cards are the issues followed by the custom `cards` of the milestone @@ -235,7 +170,7 @@ roadmap_milestone <- function(issues, config) { }) ) closed <- vapply(cards, function(card) card$closed, NA) - position <- vapply(cards, function(card) card$order, 0) + position <- vapply(cards, function(card) card$order, numeric(1)) c( paste("##", override$title %||% milestone$title), @@ -249,20 +184,6 @@ roadmap_milestone <- function(issues, config) { ) } -#' Whether an issue is excluded from the roadmap -#' -#' @param issue An issue as returned by the GitHub API. -#' @param exclude The `exclude` section of the roadmap config. -#' @return `TRUE` or `FALSE`. -#' @noRd -roadmap_excluded <- function(issue, exclude) { - labels <- vapply(issue$labels, `[[`, "", "name") - patterns <- utils::glob2rx(unlist(exclude$labels)) - as.character(issue$number) %in% - unlist(exclude$issues) || - any(vapply(patterns, function(p) any(grepl(p, labels)), NA)) -} - #' Markdown for one card #' #' @param card A list with `title`, `description`, `url` and `closed`. diff --git a/tests/testthat/test-gha.R b/tests/testthat/test-gha.R index f8af58b..ee2b7f2 100644 --- a/tests/testthat/test-gha.R +++ b/tests/testthat/test-gha.R @@ -1,17 +1,24 @@ -test_that("gha_override() points a pull request preview at its own URL, unindexed", { - pkg <- pkgdown::as_pkgdown(fake_package(c("url: https://example.org"))) - - site <- pkgdown::as_pkgdown( - fake_package(c("url: https://example.org")), - override = gha_override(pkg, "18") +test_that("build_gha_site() builds a pull request preview under its own URL, unindexed", { + dir <- fake_package(c("url: https://example.org")) + withr::local_dir(dir) + local_mocked_bindings( + build_site_github_pages = function(...) list(...), + .package = "pkgdown" ) - expect_equal(site$meta$url, "https://example.org/pr/18") - expect_true(site$meta$template$params$noindex) + args <- build_gha_site("18") + + expect_equal(args$override$url, "https://example.org/pr/18") + expect_true(args$override$template$params$noindex) }) -test_that("gha_override() leaves the site alone without a pull request", { - pkg <- pkgdown::as_pkgdown(fake_package(c("url: https://example.org"))) +test_that("build_gha_site() adds no overrides without a pull request", { + dir <- fake_package(c("url: https://example.org")) + withr::local_dir(dir) + local_mocked_bindings( + build_site_github_pages = function(...) list(...), + .package = "pkgdown" + ) - expect_equal(gha_override(pkg, ""), list()) + expect_equal(build_gha_site("")$override, list()) }) diff --git a/tests/testthat/test-roadmap-markdown.R b/tests/testthat/test-roadmap-markdown.R index d1c7a3d..b214136 100644 --- a/tests/testthat/test-roadmap-markdown.R +++ b/tests/testthat/test-roadmap-markdown.R @@ -102,3 +102,34 @@ test_that("roadmap_markdown() applies milestone and issue overrides and custom c expect_match(paste(markdown, collapse = "\n"), "Your ideas.*Renamed.*Second") expect_match(paste(markdown, collapse = "\n"), "Custom card text.") }) + +test_that("roadmap_markdown() leaves out the released station when there is no release", { + issues <- list(fake_issue(1, "Kept", fake_milestone(1, "v3.0.0"))) + local_mocked_bindings( + gh = function(endpoint, ...) { + if (grepl("releases", endpoint)) { + rlang::abort("Not Found", class = c("github_error", "http_error_404", "error")) + } + issues + }, + .package = "gh" + ) + pkg <- pkgdown::as_pkgdown(fake_package(c("roadmap:", " enabled: true"))) + + expect_no_match(paste(roadmap_markdown(pkg), collapse = "\n"), "rm-past") +}) + +test_that("roadmap_markdown() reports other release errors", { + local_mocked_bindings( + gh = function(endpoint, ...) { + if (grepl("releases", endpoint)) { + rlang::abort("Rate limited", class = c("github_error", "http_error_403", "error")) + } + list() + }, + .package = "gh" + ) + pkg <- pkgdown::as_pkgdown(fake_package(c("roadmap:", " enabled: true"))) + + expect_error(roadmap_markdown(pkg), "Rate limited") +}) diff --git a/tests/testthat/test-site.R b/tests/testthat/test-site.R index 5225622..c3bc79a 100644 --- a/tests/testthat/test-site.R +++ b/tests/testthat/test-site.R @@ -1,6 +1,4 @@ -test_that("build_site() builds the roadmap beside a site with its own articles", { - skip_on_cran() - skip_if_not_installed("pkgdownconfig") +build_roadmap_site <- function(format) { issues <- list(fake_issue(1, "Roadmap item", fake_milestone(1, "v3.0.0"))) local_mocked_bindings(gh = fake_gh(issues), .package = "gh") withr::local_envvar(CI = "true") @@ -10,6 +8,7 @@ test_that("build_site() builds the roadmap beside a site with its own articles", " package: pkgdownconfig", "roadmap:", " enabled: true", + paste0(" format: ", format), "articles:", " - title: Guides", " contents:", @@ -21,20 +20,34 @@ test_that("build_site() builds the roadmap beside a site with its own articles", file.path(dir, "vignettes", "articles", "guide.Rmd") ) - pkgdownconfig::build_site( - dir, - preview = FALSE, - new_process = FALSE, - quiet = TRUE - ) + pkgdownconfig::build_site(dir, preview = FALSE, new_process = FALSE, quiet = TRUE) + dir +} + +test_that("build_site() builds the roadmap beside a site with its own articles", { + skip_on_cran() + skip_if_not_installed("pkgdownconfig") + dir <- build_roadmap_site("rmd") docs <- file.path(dir, "docs") index <- readLines(file.path(docs, "articles", "index.html")) + expect_true(file.exists(file.path(dir, "vignettes", "articles", "roadmap.Rmd"))) expect_true(file.exists(file.path(docs, "articles", "roadmap.html"))) expect_true(any(grepl('href="guide.html"', index))) expect_false(any(grepl('href="roadmap.html"', index))) - expect_true(any(grepl( - 'href="articles/roadmap.html"', - readLines(file.path(docs, "index.html")) - ))) + expect_true(any(grepl('href="articles/roadmap.html"', readLines(file.path(docs, "index.html"))))) +}) + +test_that("build_site() builds a Quarto roadmap when the format is qmd", { + skip_on_cran() + skip_if_not_installed("pkgdownconfig") + skip_if(Sys.which("quarto") == "", "Quarto is not installed") + dir <- build_roadmap_site("qmd") + + docs <- file.path(dir, "docs") + index <- readLines(file.path(docs, "articles", "index.html")) + expect_true(file.exists(file.path(dir, "vignettes", "articles", "roadmap.qmd"))) + expect_false(file.exists(file.path(dir, "vignettes", "articles", "roadmap.Rmd"))) + expect_true(file.exists(file.path(docs, "articles", "roadmap.html"))) + expect_false(any(grepl('href="roadmap.html"', index))) }) From 93fa6659a5d8dfe007e50689960176abcedb0f37 Mon Sep 17 00:00:00 2001 From: VisruthSK Date: Sat, 10 Oct 2026 14:38:37 -0700 Subject: [PATCH 22/27] Set up formatting --- .Rbuildignore | 3 ++ .github/workflows/format-suggest.yaml | 55 +++++++++++++++++++++++++++ .vscode/extensions.json | 6 +++ .vscode/settings.json | 6 +++ air.toml | 0 jarl.toml | 2 + 6 files changed, 72 insertions(+) create mode 100644 .github/workflows/format-suggest.yaml create mode 100644 .vscode/extensions.json create mode 100644 .vscode/settings.json create mode 100644 air.toml create mode 100644 jarl.toml diff --git a/.Rbuildignore b/.Rbuildignore index 3a0e4c0..9c97ef8 100644 --- a/.Rbuildignore +++ b/.Rbuildignore @@ -5,3 +5,6 @@ ^vignettes/\.quarto$ ^vignettes/*_files$ ^LICENSE\.md$ +^jarl\.toml$ +^[.]?air[.]toml$ +^\.vscode$ diff --git a/.github/workflows/format-suggest.yaml b/.github/workflows/format-suggest.yaml new file mode 100644 index 0000000..f342139 --- /dev/null +++ b/.github/workflows/format-suggest.yaml @@ -0,0 +1,55 @@ +# Workflow derived from https://github.com/posit-dev/setup-air/tree/main/examples + +on: + # Using `pull_request_target` over `pull_request` for elevated `GITHUB_TOKEN` + # privileges, otherwise we can't set `pull-requests: write` when the pull + # request comes from a fork, which is our main use case (external contributors). + # + # `pull_request_target` runs in the context of the target branch (`main`, usually), + # rather than in the context of the pull request like `pull_request` does. Due + # to this, we must explicitly checkout `ref: ${{ github.event.pull_request.head.sha }}`. + # This is typically frowned upon by GitHub, as it exposes you to potentially running + # untrusted code in a context where you have elevated privileges, but they explicitly + # call out the use case of reformatting and committing back / commenting on the PR + # as a situation that should be safe (because we aren't actually running the untrusted + # code, we are just treating it as passive data). + # https://securitylab.github.com/resources/github-actions-preventing-pwn-requests/ + pull_request_target: + +name: format-suggest.yaml + +jobs: + format-suggest: + name: format-suggest + runs-on: ubuntu-latest + + permissions: + contents: read + checks: write + issues: write + pull-requests: write + + steps: + - uses: actions/checkout@v7 + with: + ref: ${{ github.event.pull_request.head.sha }} + allow-unsafe-pr-checkout: true + persist-credentials: false + + - name: Install + uses: posit-dev/setup-air@v1 + + - name: Jarl format + uses: etiennebacher/setup-jarl@v0.1.1 + with: + args: "check --fix" + + - name: Format + run: air format . + + - name: Suggest + uses: reviewdog/action-suggester@v1 + with: + level: error + fail_level: error + tool_name: air+jarl diff --git a/.vscode/extensions.json b/.vscode/extensions.json new file mode 100644 index 0000000..15a1ec9 --- /dev/null +++ b/.vscode/extensions.json @@ -0,0 +1,6 @@ +{ + "recommendations": [ + "Posit.air-vscode", + "etiennebacher.jarl-vscode" + ] +} diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..f2d0b79 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,6 @@ +{ + "[r]": { + "editor.formatOnSave": true, + "editor.defaultFormatter": "Posit.air-vscode" + } +} diff --git a/air.toml b/air.toml new file mode 100644 index 0000000..e69de29 diff --git a/jarl.toml b/jarl.toml new file mode 100644 index 0000000..bc01a84 --- /dev/null +++ b/jarl.toml @@ -0,0 +1,2 @@ +[lint] +extend-select = ["TESTTHAT"] From 0a7371ee6feefe65f03bedaf046bdbe99550b148 Mon Sep 17 00:00:00 2001 From: VisruthSK Date: Sat, 10 Oct 2026 14:47:31 -0700 Subject: [PATCH 23/27] Warn about open milestones with no issues Milestones with no open issues were dropped silently. The build now warns and continues. The logo fallback note moves to the v3.0 Setup vignette. --- R/roadmap.R | 14 ++++++++++++++ tests/testthat/helper-github.R | 10 ++++++++-- tests/testthat/test-roadmap-markdown.R | 10 ++++++++++ vignettes/Setup.qmd | 3 +-- 4 files changed, 33 insertions(+), 4 deletions(-) diff --git a/R/roadmap.R b/R/roadmap.R index 2c8ff9f..80a2cac 100644 --- a/R/roadmap.R +++ b/R/roadmap.R @@ -39,6 +39,20 @@ roadmap_markdown <- function(pkg) { issues, vapply(issues, function(issue) issue$milestone$number, numeric(1)) ) + open_milestones <- gh::gh( + "/repos/{repo}/milestones", + repo = repo, + state = "open", + .limit = Inf + ) + for (milestone in open_milestones) { + if (!as.character(milestone$number) %in% names(milestones)) { + warning( + "Milestone `", milestone$title, "` has no open issues and is left out of the roadmap.", + call. = FALSE + ) + } + } titles <- vapply(milestones, function(m) m[[1]]$milestone$title, "") versions <- numeric_version(sub("^v", "", titles), strict = FALSE) release <- tryCatch( diff --git a/tests/testthat/helper-github.R b/tests/testthat/helper-github.R index 9af67bf..461853b 100644 --- a/tests/testthat/helper-github.R +++ b/tests/testthat/helper-github.R @@ -27,8 +27,14 @@ fake_issue <- function( ) } -fake_gh <- function(issues, release = list(tag_name = "v2.10.1")) { +fake_gh <- function(issues, release = list(tag_name = "v2.10.1"), milestones = list()) { function(endpoint, ...) { - if (grepl("releases", endpoint)) release else issues + if (grepl("releases", endpoint)) { + release + } else if (grepl("milestones", endpoint)) { + milestones + } else { + issues + } } } diff --git a/tests/testthat/test-roadmap-markdown.R b/tests/testthat/test-roadmap-markdown.R index b214136..0d2936a 100644 --- a/tests/testthat/test-roadmap-markdown.R +++ b/tests/testthat/test-roadmap-markdown.R @@ -133,3 +133,13 @@ test_that("roadmap_markdown() reports other release errors", { expect_error(roadmap_markdown(pkg), "Rate limited") }) + +test_that("roadmap_markdown() warns about an open milestone with no issues and keeps building", { + issues <- list(fake_issue(1, "Kept", fake_milestone(1, "v3.0.0"))) + milestones <- list(fake_milestone(1, "v3.0.0"), fake_milestone(4, "v4.0.0")) + local_mocked_bindings(gh = fake_gh(issues, milestones = milestones), .package = "gh") + pkg <- pkgdown::as_pkgdown(fake_package(c("roadmap:", " enabled: true"))) + + expect_warning(markdown <- roadmap_markdown(pkg), "v4.0.0") + expect_match(paste(markdown, collapse = "\n"), "Kept") +}) diff --git a/vignettes/Setup.qmd b/vignettes/Setup.qmd index b6020be..df1e694 100644 --- a/vignettes/Setup.qmd +++ b/vignettes/Setup.qmd @@ -107,8 +107,7 @@ You could also configure the `pkgdown` GHA to only run when [vignettes are modif ## Common Issues -If for some reason the new favicons don't get copied over, check if you are defining favicons in `pkgdown/favicons`. In most cases you can delete everything in that folder--just delete the logo and favicons if you are worried. The template will hook in the correct favicon and logo. If its not working, download [logo.svg](https://github.com/stan-dev/logos/blob/master/logo.svg) to `/man/figures/logo.svg` and run `pkgdown::build_favicons()` once to build the favicons. The navbar logo falls back to the Stan logo shipped with the template, so a package without `man/figures/logo.svg` still builds; a logo in your package takes precedence. - +If for some reason the new favicons don't get copied over, check if you are defining favicons in `pkgdown/favicons`. In most cases you can delete everything in that folder--just delete the logo and favicons if you are worried. The template will hook in the correct favicon and logo. If its not working, download [logo.svg](https://github.com/stan-dev/logos/blob/master/logo.svg) to `/man/figures/logo.svg` and run `pkgdown::build_favicons()` once to build the favicons. If you want the hex in your README (or if it isn't working), make sure to edit the `README.md` or however you generate it. You can take a look at this package's to get an idea of what you need to do (repeated below): From 459a3739a093c4082a5bd6abca8cd866ee2efb97 Mon Sep 17 00:00:00 2001 From: VisruthSK Date: Sat, 10 Oct 2026 14:48:39 -0700 Subject: [PATCH 24/27] Revert "Set up formatting" This reverts commit 93fa6659a5d8dfe007e50689960176abcedb0f37. --- .Rbuildignore | 3 -- .github/workflows/format-suggest.yaml | 55 --------------------------- .vscode/extensions.json | 6 --- .vscode/settings.json | 6 --- air.toml | 0 jarl.toml | 2 - 6 files changed, 72 deletions(-) delete mode 100644 .github/workflows/format-suggest.yaml delete mode 100644 .vscode/extensions.json delete mode 100644 .vscode/settings.json delete mode 100644 air.toml delete mode 100644 jarl.toml diff --git a/.Rbuildignore b/.Rbuildignore index 9c97ef8..3a0e4c0 100644 --- a/.Rbuildignore +++ b/.Rbuildignore @@ -5,6 +5,3 @@ ^vignettes/\.quarto$ ^vignettes/*_files$ ^LICENSE\.md$ -^jarl\.toml$ -^[.]?air[.]toml$ -^\.vscode$ diff --git a/.github/workflows/format-suggest.yaml b/.github/workflows/format-suggest.yaml deleted file mode 100644 index f342139..0000000 --- a/.github/workflows/format-suggest.yaml +++ /dev/null @@ -1,55 +0,0 @@ -# Workflow derived from https://github.com/posit-dev/setup-air/tree/main/examples - -on: - # Using `pull_request_target` over `pull_request` for elevated `GITHUB_TOKEN` - # privileges, otherwise we can't set `pull-requests: write` when the pull - # request comes from a fork, which is our main use case (external contributors). - # - # `pull_request_target` runs in the context of the target branch (`main`, usually), - # rather than in the context of the pull request like `pull_request` does. Due - # to this, we must explicitly checkout `ref: ${{ github.event.pull_request.head.sha }}`. - # This is typically frowned upon by GitHub, as it exposes you to potentially running - # untrusted code in a context where you have elevated privileges, but they explicitly - # call out the use case of reformatting and committing back / commenting on the PR - # as a situation that should be safe (because we aren't actually running the untrusted - # code, we are just treating it as passive data). - # https://securitylab.github.com/resources/github-actions-preventing-pwn-requests/ - pull_request_target: - -name: format-suggest.yaml - -jobs: - format-suggest: - name: format-suggest - runs-on: ubuntu-latest - - permissions: - contents: read - checks: write - issues: write - pull-requests: write - - steps: - - uses: actions/checkout@v7 - with: - ref: ${{ github.event.pull_request.head.sha }} - allow-unsafe-pr-checkout: true - persist-credentials: false - - - name: Install - uses: posit-dev/setup-air@v1 - - - name: Jarl format - uses: etiennebacher/setup-jarl@v0.1.1 - with: - args: "check --fix" - - - name: Format - run: air format . - - - name: Suggest - uses: reviewdog/action-suggester@v1 - with: - level: error - fail_level: error - tool_name: air+jarl diff --git a/.vscode/extensions.json b/.vscode/extensions.json deleted file mode 100644 index 15a1ec9..0000000 --- a/.vscode/extensions.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "recommendations": [ - "Posit.air-vscode", - "etiennebacher.jarl-vscode" - ] -} diff --git a/.vscode/settings.json b/.vscode/settings.json deleted file mode 100644 index f2d0b79..0000000 --- a/.vscode/settings.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "[r]": { - "editor.formatOnSave": true, - "editor.defaultFormatter": "Posit.air-vscode" - } -} diff --git a/air.toml b/air.toml deleted file mode 100644 index e69de29..0000000 diff --git a/jarl.toml b/jarl.toml deleted file mode 100644 index bc01a84..0000000 --- a/jarl.toml +++ /dev/null @@ -1,2 +0,0 @@ -[lint] -extend-select = ["TESTTHAT"] From 7a5a743eb5743d58d8f1e434db9c1289b7b23202 Mon Sep 17 00:00:00 2001 From: VisruthSK Date: Sat, 10 Oct 2026 15:23:05 -0700 Subject: [PATCH 25/27] Pass extra arguments through build_gha_site() and comment the preview link build_gha_site() forwards ... to pkgdown::build_site_github_pages(), with run_dont_run = TRUE as the default. It also writes PREVIEW_URL for the sticky PR comment, which the workflow posts with the commit SHA. --- .github/workflows/pkgdown.yaml | 11 +++++++++++ R/build_site.R | 24 ++++++++++++++++-------- inst/workflows/pkgdown.yaml | 11 +++++++++++ man/build_gha_site.Rd | 4 +++- tests/testthat/test-gha.R | 14 ++++++++++++++ 5 files changed, 55 insertions(+), 9 deletions(-) diff --git a/.github/workflows/pkgdown.yaml b/.github/workflows/pkgdown.yaml index c9a2f2b..6e1687e 100644 --- a/.github/workflows/pkgdown.yaml +++ b/.github/workflows/pkgdown.yaml @@ -32,6 +32,7 @@ jobs: NOT_CRAN: "true" permissions: contents: write + pull-requests: write steps: - uses: actions/checkout@v7 @@ -64,6 +65,16 @@ jobs: clean: true force: false + - name: Comment preview link + if: > + github.event_name == 'pull_request' && + github.event.pull_request.head.repo.full_name == github.repository + uses: marocchino/sticky-pull-request-comment@v2 + with: + message: | + Preview: ${{ env.PREVIEW_URL }} + Built from ${{ github.event.pull_request.head.sha }} + - name: Deploy site if: github.event_name != 'pull_request' uses: JamesIves/github-pages-deploy-action@v4 diff --git a/R/build_site.R b/R/build_site.R index 6c81c61..79adc4c 100644 --- a/R/build_site.R +++ b/R/build_site.R @@ -74,22 +74,30 @@ roadmap_site <- function(path = ".") { #' Build the GitHub Pages site #' #' @param pr_number Pull request number. Empty builds the main site. +#' @param ... Passed on to [pkgdown::build_site_github_pages()], overriding its defaults. #' @export -build_gha_site <- function(pr_number = Sys.getenv("PR_NUMBER")) { +build_gha_site <- function(pr_number = Sys.getenv("PR_NUMBER"), ...) { pkg <- roadmap_site(".") override <- list() if (nzchar(pr_number)) { if (pkg$development$mode == "devel") { Sys.setenv(PKGDOWN_DEV_MODE = "unreleased") } - override$url <- paste0(pkg$meta$url, "/pr/", pr_number) + preview <- paste0(pkg$meta$url, "/pr/", pr_number) + if (nzchar(Sys.getenv("GITHUB_ENV"))) { + cat( + paste0("PREVIEW_URL=", preview, "\n"), + file = Sys.getenv("GITHUB_ENV"), + append = TRUE + ) + } + override$url <- preview override$template$params$noindex <- TRUE } - pkgdown::build_site_github_pages( - pkg = pkg, - lazy = FALSE, - run_dont_run = TRUE, - new_process = TRUE, - override = override + defaults <- list(lazy = FALSE, run_dont_run = TRUE, new_process = TRUE) + args <- utils::modifyList(defaults, list(...)) + do.call( + pkgdown::build_site_github_pages, + c(list(pkg = pkg, override = override), args) ) } diff --git a/inst/workflows/pkgdown.yaml b/inst/workflows/pkgdown.yaml index c9a2f2b..6e1687e 100644 --- a/inst/workflows/pkgdown.yaml +++ b/inst/workflows/pkgdown.yaml @@ -32,6 +32,7 @@ jobs: NOT_CRAN: "true" permissions: contents: write + pull-requests: write steps: - uses: actions/checkout@v7 @@ -64,6 +65,16 @@ jobs: clean: true force: false + - name: Comment preview link + if: > + github.event_name == 'pull_request' && + github.event.pull_request.head.repo.full_name == github.repository + uses: marocchino/sticky-pull-request-comment@v2 + with: + message: | + Preview: ${{ env.PREVIEW_URL }} + Built from ${{ github.event.pull_request.head.sha }} + - name: Deploy site if: github.event_name != 'pull_request' uses: JamesIves/github-pages-deploy-action@v4 diff --git a/man/build_gha_site.Rd b/man/build_gha_site.Rd index 8fc0bde..db812d0 100644 --- a/man/build_gha_site.Rd +++ b/man/build_gha_site.Rd @@ -4,10 +4,12 @@ \alias{build_gha_site} \title{Build the GitHub Pages site} \usage{ -build_gha_site(pr_number = Sys.getenv("PR_NUMBER")) +build_gha_site(pr_number = Sys.getenv("PR_NUMBER"), ...) } \arguments{ \item{pr_number}{Pull request number. Empty builds the main site.} + +\item{...}{Passed on to \code{\link[pkgdown:build_site_github_pages]{pkgdown::build_site_github_pages()}}, overriding its defaults.} } \description{ Build the GitHub Pages site diff --git a/tests/testthat/test-gha.R b/tests/testthat/test-gha.R index ee2b7f2..027898f 100644 --- a/tests/testthat/test-gha.R +++ b/tests/testthat/test-gha.R @@ -22,3 +22,17 @@ test_that("build_gha_site() adds no overrides without a pull request", { expect_equal(build_gha_site("")$override, list()) }) + +test_that("build_gha_site() passes other arguments through to pkgdown", { + dir <- fake_package(c("url: https://example.org")) + withr::local_dir(dir) + local_mocked_bindings( + build_site_github_pages = function(...) list(...), + .package = "pkgdown" + ) + + args <- build_gha_site("", run_dont_run = FALSE, quiet = TRUE) + + expect_false(args$run_dont_run) + expect_true(args$quiet) +}) From dc2df7bb784305dbc675f0bddc376c169088be6e Mon Sep 17 00:00:00 2001 From: VisruthSK Date: Mon, 12 Oct 2026 12:08:24 -0700 Subject: [PATCH 26/27] Log how many issues the roadmap fetched --- R/roadmap.R | 1 + 1 file changed, 1 insertion(+) diff --git a/R/roadmap.R b/R/roadmap.R index 80a2cac..a3244e4 100644 --- a/R/roadmap.R +++ b/R/roadmap.R @@ -22,6 +22,7 @@ roadmap_markdown <- function(pkg) { state = "all", .limit = Inf ) + message(length(issues), " issues returned for ", repo) excluded_labels <- utils::glob2rx(unlist(config$exclude$labels)) excluded_issues <- unlist(config$exclude$issues) issues <- Filter( From 107851f5410c2c24444cc30d718bd7fbd4759816 Mon Sep 17 00:00:00 2001 From: VisruthSK Date: Mon, 12 Oct 2026 13:54:47 -0700 Subject: [PATCH 27/27] Give the PR build read access to issues and milestones --- .github/workflows/pkgdown.yaml | 1 + inst/workflows/pkgdown.yaml | 1 + 2 files changed, 2 insertions(+) diff --git a/.github/workflows/pkgdown.yaml b/.github/workflows/pkgdown.yaml index 6e1687e..2b415c1 100644 --- a/.github/workflows/pkgdown.yaml +++ b/.github/workflows/pkgdown.yaml @@ -33,6 +33,7 @@ jobs: permissions: contents: write pull-requests: write + issues: read steps: - uses: actions/checkout@v7 diff --git a/inst/workflows/pkgdown.yaml b/inst/workflows/pkgdown.yaml index 6e1687e..2b415c1 100644 --- a/inst/workflows/pkgdown.yaml +++ b/inst/workflows/pkgdown.yaml @@ -33,6 +33,7 @@ jobs: permissions: contents: write pull-requests: write + issues: read steps: - uses: actions/checkout@v7