From 124ba910f21fb525f61f14d8ad579f89cd34da24 Mon Sep 17 00:00:00 2001 From: Josh Day Date: Fri, 13 Feb 2026 13:37:43 -0500 Subject: [PATCH 1/3] Apply JuliaPackageTemplate infrastructure --- .github/dependabot.yml | 21 ++++ .github/workflows/CI.yml | 37 +++++++ .github/workflows/CompatHelper.yml | 24 ----- .github/workflows/Docs.yml | 152 +++++++++++++++++++++++++++++ .github/workflows/TagBot.yml | 15 --- .gitignore | 17 +++- CHANGELOG.md | 1 + CLAUDE.md | 40 ++++++++ README.md | 7 +- docs/.gitignore | 2 + docs/Project.toml | 2 +- docs/_quarto.yml | 37 +++++++ docs/api.qmd | 54 ++++++++++ docs/changelog.qmd | 5 + docs/coverage.qmd | 28 ++++++ docs/getting-started.qmd | 11 +++ docs/index.qmd | 23 +++++ docs/make.jl | 17 ---- docs/src/algorithms.md | 46 --------- docs/src/index.md | 27 ----- docs/src/usage.md | 43 -------- docs/styles.css | 7 ++ 22 files changed, 439 insertions(+), 177 deletions(-) create mode 100644 .github/dependabot.yml create mode 100644 .github/workflows/CI.yml delete mode 100644 .github/workflows/CompatHelper.yml create mode 100644 .github/workflows/Docs.yml delete mode 100644 .github/workflows/TagBot.yml create mode 100644 CHANGELOG.md create mode 100644 CLAUDE.md create mode 100644 docs/.gitignore create mode 100644 docs/_quarto.yml create mode 100644 docs/api.qmd create mode 100644 docs/changelog.qmd create mode 100644 docs/coverage.qmd create mode 100644 docs/getting-started.qmd create mode 100644 docs/index.qmd delete mode 100644 docs/make.jl delete mode 100644 docs/src/algorithms.md delete mode 100644 docs/src/index.md delete mode 100644 docs/src/usage.md create mode 100644 docs/styles.css diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000..0134ffc --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,21 @@ +version: 2 +updates: + # Keep GitHub Actions up to date + - package-ecosystem: "github-actions" + directory: "/" + schedule: + interval: "weekly" + groups: + github-actions: + patterns: + - "*" + + # Keep Julia dependencies up to date + - package-ecosystem: "julia" + directory: "/" + schedule: + interval: "weekly" + groups: + julia-deps: + patterns: + - "*" diff --git a/.github/workflows/CI.yml b/.github/workflows/CI.yml new file mode 100644 index 0000000..8c5e990 --- /dev/null +++ b/.github/workflows/CI.yml @@ -0,0 +1,37 @@ +name: CI +on: + push: + branches: + - main + - master + tags: ['*'] + pull_request: + workflow_dispatch: + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: ${{ startsWith(github.ref, 'refs/pull/') }} + +jobs: + test: + name: Julia ${{ matrix.os }} + runs-on: ${{ matrix.os }} + timeout-minutes: 60 + permissions: + actions: write + contents: read + strategy: + fail-fast: false + matrix: + os: + - ubuntu-latest + - windows-latest + - macOS-latest + steps: + - uses: actions/checkout@v6 + - uses: julia-actions/setup-julia@v2 + with: + version: '1' + - uses: julia-actions/cache@v2 + - uses: julia-actions/julia-buildpkg@v1 + - uses: julia-actions/julia-runtest@v1 diff --git a/.github/workflows/CompatHelper.yml b/.github/workflows/CompatHelper.yml deleted file mode 100644 index 6992ce6..0000000 --- a/.github/workflows/CompatHelper.yml +++ /dev/null @@ -1,24 +0,0 @@ -name: CompatHelper - -on: - schedule: - - cron: '00 00 * * *' - -jobs: - CompatHelper: - runs-on: ${{ matrix.os }} - strategy: - matrix: - julia-version: [1.2.0] - julia-arch: [x86] - os: [ubuntu-latest] - steps: - - uses: julia-actions/setup-julia@latest - with: - version: ${{ matrix.julia-version }} - - name: Pkg.add("CompatHelper") - run: julia -e 'using Pkg; Pkg.add("CompatHelper")' - - name: CompatHelper.main() - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - run: julia -e 'using CompatHelper; CompatHelper.main()' diff --git a/.github/workflows/Docs.yml b/.github/workflows/Docs.yml new file mode 100644 index 0000000..7aad8c2 --- /dev/null +++ b/.github/workflows/Docs.yml @@ -0,0 +1,152 @@ +name: Docs + +on: + push: + branches: + - main + tags: + - 'v*' + workflow_dispatch: + +permissions: + contents: write + +concurrency: + group: pages + cancel-in-progress: false + +jobs: + docs: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v6 + + # Determine deploy target from ref + - name: Set deploy version + id: version + run: | + if [[ "$GITHUB_REF" == refs/tags/v* ]]; then + VERSION="${GITHUB_REF#refs/tags/}" + echo "version=$VERSION" >> "$GITHUB_OUTPUT" + echo "is_release=true" >> "$GITHUB_OUTPUT" + else + echo "version=dev" >> "$GITHUB_OUTPUT" + echo "is_release=false" >> "$GITHUB_OUTPUT" + fi + + # Build docs + - uses: julia-actions/setup-julia@v2 + with: + version: '1' + - uses: julia-actions/cache@v2 + - name: Install docs dependencies + run: julia --project=docs -e 'using Pkg; Pkg.develop(path="."); Pkg.instantiate()' + - name: Install lcov + run: sudo apt-get install -y lcov + - uses: quarto-dev/quarto-actions/setup@v2 + - uses: quarto-dev/quarto-actions/render@v2 + with: + path: docs + + # Deploy to gh-pages branch + - name: Deploy to gh-pages + env: + VERSION: ${{ steps.version.outputs.version }} + IS_RELEASE: ${{ steps.version.outputs.is_release }} + REPO_NAME: ${{ github.event.repository.name }} + run: | + git config --global user.name "github-actions[bot]" + git config --global user.email "github-actions[bot]@users.noreply.github.com" + + # Clone gh-pages branch or create orphan + if git ls-remote --exit-code origin gh-pages; then + git clone --branch gh-pages --single-branch "https://x-access-token:${{ github.token }}@github.com/${{ github.repository }}.git" gh-pages-deploy + else + mkdir gh-pages-deploy + cd gh-pages-deploy + git init + git checkout --orphan gh-pages + git commit --allow-empty -m "Initialize gh-pages" + cd .. + fi + + # Copy built docs to version directory + rm -rf "gh-pages-deploy/${VERSION}" + cp -r docs/_site "gh-pages-deploy/${VERSION}" + + cd gh-pages-deploy + + # If release tag: update versions.json and stable redirect + if [ "$IS_RELEASE" = "true" ]; then + # Update versions.json + if [ -f versions.json ]; then + # Add new version and sort by semver descending + python3 -c " + import json, re + with open('versions.json') as f: + versions = json.load(f) + ver = '${VERSION}' + if ver not in versions: + versions.append(ver) + def semver_key(v): + nums = re.findall(r'\d+', v) + return tuple(int(n) for n in nums) + versions.sort(key=semver_key, reverse=True) + with open('versions.json', 'w') as f: + json.dump(versions, f, indent=2) + " + else + echo "[\"${VERSION}\"]" > versions.json + fi + + # Update stable redirect + mkdir -p stable + cat > stable/index.html << STABLE_EOF + + + + Redirecting to latest stable... + + STABLE_EOF + fi + + # Create stable placeholder if no releases yet + if [ ! -d stable ]; then + mkdir -p stable + cat > stable/index.html << PLACEHOLDER_EOF + + + + + + No Stable Release Yet + + + +
+

No Stable Release Yet

+

This package has not published a release.
Check out the development docs instead.

+
+ + + PLACEHOLDER_EOF + fi + + # Always update root redirect to stable + cat > index.html << ROOT_EOF + + + + Redirecting to stable docs... + + ROOT_EOF + + touch .nojekyll + + git add -A + if git diff --cached --quiet; then + echo "No changes to deploy" + else + git commit -m "Deploy docs: ${VERSION}" + git push "https://x-access-token:${{ github.token }}@github.com/${{ github.repository }}.git" HEAD:gh-pages + fi diff --git a/.github/workflows/TagBot.yml b/.github/workflows/TagBot.yml deleted file mode 100644 index f49313b..0000000 --- a/.github/workflows/TagBot.yml +++ /dev/null @@ -1,15 +0,0 @@ -name: TagBot -on: - issue_comment: - types: - - created - workflow_dispatch: -jobs: - TagBot: - if: github.event_name == 'workflow_dispatch' || github.actor == 'JuliaTagBot' - runs-on: ubuntu-latest - steps: - - uses: JuliaRegistries/TagBot@v1 - with: - token: ${{ secrets.GITHUB_TOKEN }} - ssh: ${{ secrets.DOCUMENTER_KEY }} diff --git a/.gitignore b/.gitignore index 9f56f2c..e912af6 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,17 @@ *.jl.cov *.jl.*.cov *.jl.mem -docs/build -docs/site -*Manifest.toml \ No newline at end of file +coverage/ +Manifest.toml +docs/Manifest.toml +docs/build/ +docs/_site +*.claude +*.ipynb +*.quarto +docs/site_libs +docs/.quarto/ +docs/*_files/ +docs/coverage/ +**/*.quarto_ipynb +*.html diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..268a01b --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1 @@ +## Unreleased diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..17d15c7 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,40 @@ +# Package Setup + +- Replace all instances of `SparseRegression` to the name set by the user +- Use Julia to create a new UUID + +# Development + +- Run tests: `julia --project -e 'using Pkg; Pkg.test()'` +- Build docs: `quarto render docs` +- `docs/` has its own Project.toml for doc-specific dependencies. +- Each .qmd file in the docs should have `engine: julia` in the YAML frontmatter +- Never edit Project.toml or Manifest.toml manually — use Pkg + +# Style + +- 4-space indentation +- Docstrings on all exports +- Use `### Examples` for inline docs examples +- Segment code sections with: "#" * repeat('-', 80) * "# " * "$section_title" + +# Releases + +- Preflight: tests must pass and git status must be clean +- If current version has no git tag, release it as-is (don't bump) +- If current version is already tagged, bump based on commit log: + - **Major**: major rewrites (ask user if major bump is ok) + - **Minor**: new features, exports, or API additions + - **Patch**: fixes, docs, refactoring, dependency updates (default) +- Commit message: `bump version for new release: {x} to {y}` +- Generate release notes from commits since last tag (group by features, fixes, etc.) +- For major/minor bumps, release notes must include "breaking changes" section +- Update CHANGELOG.md with each release (prepend new entry under `# Unreleased` or version heading) +- Register via: + ``` + gh api repos/{owner}/{repo}/commits/{sha}/comments -f body='@JuliaRegistrator register + + Release notes: + + ' + ``` diff --git a/README.md b/README.md index a777ccc..329a0c8 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,8 @@ +[![CI](https://github.com/joshday/SparseRegression.jl/actions/workflows/CI.yml/badge.svg)](https://github.com/joshday/SparseRegression.jl/actions/workflows/CI.yml) +[![Docs Build](https://github.com/joshday/SparseRegression.jl/actions/workflows/Docs.yml/badge.svg)](https://github.com/joshday/SparseRegression.jl/actions/workflows/Docs.yml) +[![Stable Docs](https://img.shields.io/badge/docs-stable-blue)](https://joshday.github.io/SparseRegression.jl/stable/) +[![Dev Docs](https://img.shields.io/badge/docs-dev-blue)](https://joshday.github.io/SparseRegression.jl/dev/) + # SparseRegression | Documentation | Master Build | Test Coverage | @@ -20,4 +25,4 @@ y = x * range(-1, stop=1, length=50) + randn(10_000) s = SModel(x, y, L2DistLoss(), L2Penalty()) @time learn!(s) s -``` \ No newline at end of file +``` diff --git a/docs/.gitignore b/docs/.gitignore new file mode 100644 index 0000000..ad29309 --- /dev/null +++ b/docs/.gitignore @@ -0,0 +1,2 @@ +/.quarto/ +**/*.quarto_ipynb diff --git a/docs/Project.toml b/docs/Project.toml index d2ca36b..83eba56 100644 --- a/docs/Project.toml +++ b/docs/Project.toml @@ -1,3 +1,3 @@ [deps] -Documenter = "e30172f5-a6a5-5a46-863b-614d45cd2de4" SparseRegression = "ca6142a6-a6a7-57f5-b674-4a8484b22e92" +LocalCoverage = "5f6e1e16-694c-5876-87ef-16b5274f298e" diff --git a/docs/_quarto.yml b/docs/_quarto.yml new file mode 100644 index 0000000..2e58a5f --- /dev/null +++ b/docs/_quarto.yml @@ -0,0 +1,37 @@ +project: + type: book + output-dir: _site + +engine: julia +julia: + project: "." + +book: + title: "SparseRegression.jl" + navbar: + right: + - icon: star + href: https://github.com/joshday/SparseRegression.jl/stargazers + text: "Star" + - icon: github + href: https://github.com/joshday/SparseRegression.jl + chapters: + - part: "Docs" + chapters: + - index.qmd + - getting-started.qmd + - api.qmd + - part: "Reference" + chapters: + - changelog.qmd + - coverage.qmd + +format: + html: + css: styles.css + number-sections: false + theme: + light: flatly + dark: darkly + include-after-body: + - file: _version-selector.html diff --git a/docs/api.qmd b/docs/api.qmd new file mode 100644 index 0000000..f739fca --- /dev/null +++ b/docs/api.qmd @@ -0,0 +1,54 @@ +--- +title: "API" +engine: julia +--- + +```{julia} +#| echo: false +#| output: false +using SparseRegression +``` + +```{julia} +#| echo: false +#| output: asis +export_names = filter(!=(:SparseRegression), names(SparseRegression)) + +println(""" + + +""") + +# Table of contents +for name in export_names + binding = getfield(SparseRegression, name) + kind = binding isa Type ? "type" : binding isa Function ? "function" : "constant" + println("- [`$name`](#$(lowercase(string(name)))) *($(kind))*") +end +println() + +# Detailed sections +for name in export_names + binding = getfield(SparseRegression, name) + kind = binding isa Type ? "type" : binding isa Function ? "function" : "constant" + doc = string(Docs.doc(binding)) + println(""" + ::: {.callout-note collapse="true" appearance="minimal"} + ## `$name` [*($(kind))*]{.text-muted} {#$(lowercase(string(name)))} + + $doc + ::: + """) +end +``` diff --git a/docs/changelog.qmd b/docs/changelog.qmd new file mode 100644 index 0000000..f9b3878 --- /dev/null +++ b/docs/changelog.qmd @@ -0,0 +1,5 @@ +--- +title: "Changelog" +--- + +{{< include ../CHANGELOG.md >}} diff --git a/docs/coverage.qmd b/docs/coverage.qmd new file mode 100644 index 0000000..aecc4b1 --- /dev/null +++ b/docs/coverage.qmd @@ -0,0 +1,28 @@ +--- +title: "Coverage" +engine: julia +--- + +```{julia} +#| echo: false +#| output: false +using LocalCoverage +coverage = generate_coverage("SparseRegression"; run_test=true) +html_coverage(coverage; dir=joinpath(@__DIR__, "_site", "coverage"), open=false) +rm(joinpath(dirname(@__DIR__), "coverage"); recursive=true, force=true) +``` + + + + diff --git a/docs/getting-started.qmd b/docs/getting-started.qmd new file mode 100644 index 0000000..fbc28fd --- /dev/null +++ b/docs/getting-started.qmd @@ -0,0 +1,11 @@ +# Getting Started {.unnumbered} + +## Basic Usage + +```julia +using SparseRegression +``` + +## Examples + +*TODO: Add examples here.* diff --git a/docs/index.qmd b/docs/index.qmd new file mode 100644 index 0000000..4a2f247 --- /dev/null +++ b/docs/index.qmd @@ -0,0 +1,23 @@ +--- +title: "SparseRegression.jl" +engine: julia +--- + +Welcome to the documentation for **SparseRegression.jl**. + +## Overview + +SparseRegression.jl is a Julia package that ... + +## Installation + +```julia +using Pkg +Pkg.add("SparseRegression") +``` + +## Quickstart + +```{julia} +println("Hello, World!") +``` diff --git a/docs/make.jl b/docs/make.jl deleted file mode 100644 index 9b8f74c..0000000 --- a/docs/make.jl +++ /dev/null @@ -1,17 +0,0 @@ -using Documenter, SparseRegression - -makedocs( - format = Documenter.HTML(), - sitename = "SparseRegression.jl", - authors = "Josh Day", - clean = true, - pages = [ - "index.md", - "usage.md", - "algorithms.md" - ] -) - -deploydocs( - repo = "github.com/joshday/SparseRegression.jl.git", -) diff --git a/docs/src/algorithms.md b/docs/src/algorithms.md deleted file mode 100644 index e5b53de..0000000 --- a/docs/src/algorithms.md +++ /dev/null @@ -1,46 +0,0 @@ -# Algorithms - -The first argument of an `Algorithm`'s constructor is an `SModel`. This is to ensure -storage buffers are the correct size. - -## ProxGrad - -```@docs -ProxGrad -``` - -## Fista - -```@docs -Fista -``` - -## AdaptiveProxGrad - -```@docs -AdaptiveProxGrad -``` - -## GradientDescent - -```@docs -GradientDescent -``` - -## Sweep - -```@docs -Sweep -``` - -## LinRegCholesky - -```@docs -LinRegCholesky -``` - -## LineSearch - -```@docs -LineSearch -``` \ No newline at end of file diff --git a/docs/src/index.md b/docs/src/index.md deleted file mode 100644 index 759fd82..0000000 --- a/docs/src/index.md +++ /dev/null @@ -1,27 +0,0 @@ -# Introduction - -**SparseRegression** is a Julia package which combines [JuliaML](https://github.com/JuliaML) primitives to implement high-performance algorithms for fitting linear models. - -## Objective Function - -The objective function that SparseRegression can solve takes the form: - -```math -\frac{1}{n}\sum_{i=1}^n w_i f(y_i, x_i^T\beta) + \sum_{j=1}^p \lambda_j J(\beta_j), -``` - -where $f$ is a loss function, $J$ is a penalty or regularization function, the $w_i$'s are nonnegative observation weights and the $\lambda_j$'s are nonnegative element-wise regularization parameters. Many models take this form: - -| Model | $f(y_i, x_i^T\beta)$ | $g(\beta_j)$ | -|------------------|------------------------|---------------| -| Lasso Regression | $\frac{1}{2}(y_i - x_i^T\beta)^2$ | $\|\beta_j\|$ | -| Ridge Regression | $\frac{1}{2}(y_i - x_i^T\beta)^2$ | $\beta_j^2$ | -| SVM | $max(0, 1 - y_i x_i^T\beta)$ | $\beta_j^2$ | - -## [JuliaML](https://github.com/JuliaML) - -The three core JuliaML packages that SparseRegression brings together are: - -- [LossFunctions](https://github.com/JuliaML/LossFunctions.jl) -- [PenaltyFunctions](https://github.com/JuliaML/PenaltyFunctions.jl) -- [LearningStrategies](https://github.com/JuliaML/LearningStrategies.jl) diff --git a/docs/src/usage.md b/docs/src/usage.md deleted file mode 100644 index 306d9ff..0000000 --- a/docs/src/usage.md +++ /dev/null @@ -1,43 +0,0 @@ -# Usage - -1. Create a model -1. Fit the model - -## SModel - -The model type used by SparseRegression is `SModel`. An `SModel` holds onto the sufficient -information for generating a solution fo the SparseRegression objective. - -!!! note - Constructing an `SModel` does not create a fitted model. It must be `learn!`-ed. - -```@docs -SModel -``` - - -## [LearningStrategies](https://github.com/JuliaML/LearningStrategies.jl) - -An `SModel` can be learned with the default learning strategy with `learn!(model)`. You -can provide more control over the learning process by providing your own LearningStrategy. - -SparseRegression implements several `Algorithm <: LearningStrategy` types to do `SModel` -fitting. An `Algorithm` must be constructed with an `SModel` to ensure storage buffers -are the correct size. - -```julia -using SparseRegression - -# Make some fake data -x = randn(1000, 10) -y = x * range(-1, stop=1, length=10) + randn(1000) - -# Create an SModel -s = SModel(x, y) - -# All of the following are valid ways to calculate a solution -learn!(s) -learn!(s, strategy(ProxGrad(s), MaxIter(25), TimeLimit(.5))) -learn!(s, Sweep(s)) -learn!(s, LinRegCholesky(s)) -``` \ No newline at end of file diff --git a/docs/styles.css b/docs/styles.css new file mode 100644 index 0000000..23d1a3f --- /dev/null +++ b/docs/styles.css @@ -0,0 +1,7 @@ +.sidebar-item-section { + margin-top: 2rem !important; +} + +.sidebar-item-section > .sidebar-item-container > .sidebar-item-text { + font-weight: bold; +} From 7458f7c87ad654455605022795f1243d4f1040a2 Mon Sep 17 00:00:00 2001 From: Josh Day Date: Fri, 13 Feb 2026 16:53:45 -0500 Subject: [PATCH 2/3] Add DocsBackfill workflow and build date footer --- .github/workflows/Docs.yml | 2 + .github/workflows/DocsBackfill.yml | 182 +++++++++++++++++++++++++++++ docs/_quarto.yml | 3 + 3 files changed, 187 insertions(+) create mode 100644 .github/workflows/DocsBackfill.yml diff --git a/.github/workflows/Docs.yml b/.github/workflows/Docs.yml index 7aad8c2..326d452 100644 --- a/.github/workflows/Docs.yml +++ b/.github/workflows/Docs.yml @@ -44,6 +44,8 @@ jobs: - name: Install lcov run: sudo apt-get install -y lcov - uses: quarto-dev/quarto-actions/setup@v2 + - name: Set build date + run: sed -i "s/__BUILD_DATE__/$(date -u +'%Y-%m-%d')/" docs/_quarto.yml - uses: quarto-dev/quarto-actions/render@v2 with: path: docs diff --git a/.github/workflows/DocsBackfill.yml b/.github/workflows/DocsBackfill.yml new file mode 100644 index 0000000..3881896 --- /dev/null +++ b/.github/workflows/DocsBackfill.yml @@ -0,0 +1,182 @@ +name: Backfill Docs + +on: + workflow_dispatch: + +permissions: + contents: write + +concurrency: + group: pages + cancel-in-progress: false + +jobs: + backfill: + runs-on: ubuntu-latest + timeout-minutes: 120 + steps: + - uses: actions/checkout@v6 + with: + fetch-depth: 0 + + # Save docs infrastructure from main (without coverage — too slow for backfill) + - name: Save docs infrastructure + run: | + cp -r docs /tmp/docs-infrastructure + rm -f /tmp/docs-infrastructure/coverage.qmd + + # Remove coverage chapter from _quarto.yml + python3 -c " + import re + with open('/tmp/docs-infrastructure/_quarto.yml') as f: + content = f.read() + content = re.sub(r'\n\s*- coverage\.qmd', '', content) + with open('/tmp/docs-infrastructure/_quarto.yml', 'w') as f: + f.write(content) + " + + # Remove LocalCoverage from docs/Project.toml + sed -i '/LocalCoverage/d' /tmp/docs-infrastructure/Project.toml + + - uses: julia-actions/setup-julia@v2 + with: + version: '1' + - uses: julia-actions/cache@v2 + - uses: quarto-dev/quarto-actions/setup@v2 + + # Prepare gh-pages + - name: Setup gh-pages + run: | + git config --global user.name "github-actions[bot]" + git config --global user.email "github-actions[bot]@users.noreply.github.com" + if git ls-remote --exit-code origin gh-pages; then + git clone --branch gh-pages --single-branch \ + "https://x-access-token:${{ github.token }}@github.com/${{ github.repository }}.git" \ + gh-pages-deploy + else + mkdir gh-pages-deploy + cd gh-pages-deploy + git init + git checkout --orphan gh-pages + git commit --allow-empty -m "Initialize gh-pages" + fi + + # Build docs for every version tag + - name: Build all versions + env: + REPO_NAME: ${{ github.event.repository.name }} + run: | + TAGS=$(git tag -l 'v*' | sort -V) + if [ -z "$TAGS" ]; then + echo "No version tags found, nothing to backfill." + exit 0 + fi + + BUILT=() + SKIPPED=() + + for TAG in $TAGS; do + echo "" + echo "==========================================" + echo "Building docs for $TAG" + echo "==========================================" + + # Skip if already deployed + if [ -d "gh-pages-deploy/$TAG" ]; then + echo "Already deployed, skipping." + BUILT+=("$TAG") + continue + fi + + # Checkout tag (force to discard overlaid docs from previous iteration) + git checkout -f "$TAG" + + # Overlay Quarto docs infrastructure from main + rm -rf docs + cp -r /tmp/docs-infrastructure docs + + # Install and build + if ! julia --project=docs -e 'using Pkg; Pkg.develop(path="."); Pkg.instantiate()'; then + echo "WARNING: Pkg setup failed for $TAG, skipping." + SKIPPED+=("$TAG") + continue + fi + + # Stamp build date + sed -i "s/__BUILD_DATE__/$(date -u +'%Y-%m-%d')/" docs/_quarto.yml + + if ! quarto render docs; then + echo "WARNING: Quarto render failed for $TAG, skipping." + SKIPPED+=("$TAG") + continue + fi + + cp -r docs/_site "gh-pages-deploy/$TAG" + BUILT+=("$TAG") + echo "Success: $TAG" + done + + # Return to main + git checkout -f main + + # --- Update gh-pages metadata --- + cd gh-pages-deploy + + # Rebuild versions.json from deployed version directories + python3 -c " + import json, os, re + versions = [d for d in os.listdir('.') + if re.match(r'^v\d', d) and os.path.isdir(d)] + def semver_key(v): + return tuple(int(n) for n in re.findall(r'\d+', v)) + versions.sort(key=semver_key, reverse=True) + with open('versions.json', 'w') as f: + json.dump(versions, f, indent=2) + print('versions.json:', versions) + " + + # Stable redirect → latest version + LATEST=$(python3 -c " + import json + with open('versions.json') as f: + vs = json.load(f) + print(vs[0] if vs else '') + ") + + if [ -n "$LATEST" ]; then + mkdir -p stable + cat > stable/index.html << STABLE_EOF + + + + Redirecting to latest stable... + + STABLE_EOF + fi + + # Root redirect → stable + cat > index.html << ROOT_EOF + + + + Redirecting to stable docs... + + ROOT_EOF + + touch .nojekyll + + git add -A + if git diff --cached --quiet; then + echo "No changes to deploy" + else + git commit -m "Backfill docs for all versions" + git push "https://x-access-token:${{ github.token }}@github.com/${{ github.repository }}.git" HEAD:gh-pages + fi + + # Summary + echo "" + echo "==========================================" + echo "SUMMARY" + echo "==========================================" + echo "Built: ${BUILT[*]}" + echo "Skipped: ${SKIPPED[*]}" diff --git a/docs/_quarto.yml b/docs/_quarto.yml index 2e58a5f..338cfec 100644 --- a/docs/_quarto.yml +++ b/docs/_quarto.yml @@ -26,6 +26,9 @@ book: - changelog.qmd - coverage.qmd + page-footer: + center: "Built on __BUILD_DATE__" + format: html: css: styles.css From 123336bd0a19c64eb940585a4ff0989831054f06 Mon Sep 17 00:00:00 2001 From: Josh Day Date: Tue, 17 Feb 2026 06:55:28 -0500 Subject: [PATCH 3/3] Add TagBot workflow --- .github/workflows/TagBot.yml | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) create mode 100644 .github/workflows/TagBot.yml diff --git a/.github/workflows/TagBot.yml b/.github/workflows/TagBot.yml new file mode 100644 index 0000000..2f6ac12 --- /dev/null +++ b/.github/workflows/TagBot.yml @@ -0,0 +1,19 @@ +name: TagBot +on: + issue_comment: + types: + - created + workflow_dispatch: + inputs: + lookback: + description: "[DEPRECATED] No longer has any effect" + default: "3" +jobs: + TagBot: + if: github.event_name == 'workflow_dispatch' || github.actor == 'JuliaTagBot' + runs-on: ubuntu-slim + steps: + - uses: JuliaRegistries/TagBot@v1 + with: + token: ${{ secrets.GITHUB_TOKEN }} + ssh: ${{ secrets.DOCUMENTER_KEY }}