diff --git a/.github/workflows/cloudflare-pages.yml b/.github/workflows/cloudflare-pages.yml new file mode 100644 index 0000000..57cc656 --- /dev/null +++ b/.github/workflows/cloudflare-pages.yml @@ -0,0 +1,38 @@ +name: Deploy to Cloudflare Pages + +on: + push: + branches: [main] + workflow_dispatch: + +permissions: + contents: read + +concurrency: + group: cloudflare-pages + cancel-in-progress: true + +jobs: + deploy: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - uses: ruby/setup-ruby@v1 + with: + ruby-version: '3.4' + bundler-cache: true + + # Layers _config.cloudflare.yml on top, swapping the GitHub Pages subpath + # for the pages.dev root. + - name: Build site + run: bundle exec jekyll build --config _config.yml,_config.cloudflare.yml + env: + JEKYLL_ENV: production + + # Project name and output directory come from wrangler.jsonc. + - uses: cloudflare/wrangler-action@v4 + with: + apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }} + accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }} + command: pages deploy diff --git a/.github/workflows/github-pages.yml b/.github/workflows/github-pages.yml new file mode 100644 index 0000000..8ebea2e --- /dev/null +++ b/.github/workflows/github-pages.yml @@ -0,0 +1,49 @@ +name: Deploy to GitHub Pages + +on: + push: + branches: [main] + workflow_dispatch: + +permissions: + contents: read + pages: write + id-token: write + +# Let a running deploy finish rather than cancelling it mid-publish. +concurrency: + group: github-pages + cancel-in-progress: false + +jobs: + build: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - uses: ruby/setup-ruby@v1 + with: + ruby-version: '3.4' + bundler-cache: true + + # Uses _config.yml only, which carries the tiennm99.github.io/penny-pincher-provider + # url and baseurl. actions/configure-pages is deliberately not used — it would + # overwrite that baseurl with its own guess. + - name: Build site + run: bundle exec jekyll build + env: + JEKYLL_ENV: production + + - uses: actions/upload-pages-artifact@v3 + with: + path: _site + + deploy: + needs: build + runs-on: ubuntu-latest + environment: + name: github-pages + url: ${{ steps.deployment.outputs.page_url }} + steps: + - id: deployment + uses: actions/deploy-pages@v4 diff --git a/.gitignore b/.gitignore index 160c0d4..6dd2a4b 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,13 @@ session-state settings.local.json + +# Jekyll build output +_site/ +.jekyll-cache/ +.jekyll-metadata + +# Project-local gems (bundler path config + installed gems) +.bundle/ + +# Wrangler local state +.wrangler/ diff --git a/404.html b/404.html new file mode 100644 index 0000000..fc768e6 --- /dev/null +++ b/404.html @@ -0,0 +1,13 @@ +--- +layout: default +title: Page not found +permalink: /404.html +--- + +

Page not found

+ +

That page does not exist. The provider list lives on the +home page.

+ +

If a link here is broken, +open an issue.

diff --git a/Gemfile b/Gemfile new file mode 100644 index 0000000..e4b9be6 --- /dev/null +++ b/Gemfile @@ -0,0 +1,10 @@ +source "https://rubygems.org" + +gem "jekyll", "~> 4.3" +gem "minima", "~> 2.5" + +# Ruby 3.x no longer bundles these as default gems. +gem "webrick", "~> 1.8" +gem "csv", "~> 3.3" +gem "base64", "~> 0.2" +gem "bigdecimal", "~> 3.1" diff --git a/Gemfile.lock b/Gemfile.lock new file mode 100644 index 0000000..e3aafaf --- /dev/null +++ b/Gemfile.lock @@ -0,0 +1,176 @@ +GEM + remote: https://rubygems.org/ + specs: + addressable (2.9.0) + public_suffix (>= 2.0.2, < 8.0) + base64 (0.3.0) + bigdecimal (3.3.1) + colorator (1.1.0) + concurrent-ruby (1.3.8) + csv (3.3.6) + em-websocket (0.5.3) + eventmachine (>= 0.12.9) + http_parser.rb (~> 0) + eventmachine (1.2.7) + ffi (1.17.4) + ffi (1.17.4-aarch64-linux-gnu) + ffi (1.17.4-aarch64-linux-musl) + ffi (1.17.4-arm-linux-gnu) + ffi (1.17.4-arm-linux-musl) + ffi (1.17.4-arm64-darwin) + ffi (1.17.4-x86-linux-gnu) + ffi (1.17.4-x86-linux-musl) + ffi (1.17.4-x86_64-darwin) + ffi (1.17.4-x86_64-linux-gnu) + ffi (1.17.4-x86_64-linux-musl) + forwardable-extended (2.6.0) + google-protobuf (4.36.0) + bigdecimal + rake (~> 13.3) + google-protobuf (4.36.0-aarch64-linux-gnu) + bigdecimal + rake (~> 13.3) + google-protobuf (4.36.0-aarch64-linux-musl) + bigdecimal + rake (~> 13.3) + google-protobuf (4.36.0-arm64-darwin) + bigdecimal + rake (~> 13.3) + google-protobuf (4.36.0-x86-linux-gnu) + bigdecimal + rake (~> 13.3) + google-protobuf (4.36.0-x86-linux-musl) + bigdecimal + rake (~> 13.3) + google-protobuf (4.36.0-x86_64-darwin) + bigdecimal + rake (~> 13.3) + google-protobuf (4.36.0-x86_64-linux-gnu) + bigdecimal + rake (~> 13.3) + google-protobuf (4.36.0-x86_64-linux-musl) + bigdecimal + rake (~> 13.3) + http_parser.rb (0.8.1) + i18n (1.15.2) + concurrent-ruby (~> 1.0) + jekyll (4.4.1) + addressable (~> 2.4) + base64 (~> 0.2) + colorator (~> 1.0) + csv (~> 3.0) + em-websocket (~> 0.5) + i18n (~> 1.0) + jekyll-sass-converter (>= 2.0, < 4.0) + jekyll-watch (~> 2.0) + json (~> 2.6) + kramdown (~> 2.3, >= 2.3.1) + kramdown-parser-gfm (~> 1.0) + liquid (~> 4.0) + mercenary (~> 0.3, >= 0.3.6) + pathutil (~> 0.9) + rouge (>= 3.0, < 5.0) + safe_yaml (~> 1.0) + terminal-table (>= 1.8, < 4.0) + webrick (~> 1.7) + jekyll-feed (0.17.0) + jekyll (>= 3.7, < 5.0) + jekyll-sass-converter (3.1.0) + sass-embedded (~> 1.75) + jekyll-seo-tag (2.9.0) + jekyll (>= 3.8, < 5.0) + jekyll-watch (2.2.1) + listen (~> 3.0) + json (2.21.2) + kramdown (2.5.2) + rexml (>= 3.4.4) + kramdown-parser-gfm (1.1.0) + kramdown (~> 2.0) + liquid (4.0.4) + listen (3.10.0) + logger + rb-fsevent (~> 0.10, >= 0.10.3) + rb-inotify (~> 0.9, >= 0.9.10) + logger (1.7.0) + mercenary (0.4.0) + minima (2.5.2) + jekyll (>= 3.5, < 5.0) + jekyll-feed (~> 0.9) + jekyll-seo-tag (~> 2.1) + pathutil (0.16.2) + forwardable-extended (~> 2.6) + public_suffix (7.0.5) + rake (13.4.2) + rb-fsevent (0.11.2) + rb-inotify (0.11.1) + ffi (~> 1.0) + rexml (3.4.4) + rouge (4.7.0) + safe_yaml (1.0.5) + sass-embedded (1.103.1) + google-protobuf (~> 4.31) + rake (>= 13) + sass-embedded (1.103.1-aarch64-linux-android) + google-protobuf (~> 4.31) + sass-embedded (1.103.1-aarch64-linux-gnu) + google-protobuf (~> 4.31) + sass-embedded (1.103.1-aarch64-linux-musl) + google-protobuf (~> 4.31) + sass-embedded (1.103.1-arm-linux-androideabi) + google-protobuf (~> 4.31) + sass-embedded (1.103.1-arm-linux-gnueabihf) + google-protobuf (~> 4.31) + sass-embedded (1.103.1-arm-linux-musleabihf) + google-protobuf (~> 4.31) + sass-embedded (1.103.1-arm64-darwin) + google-protobuf (~> 4.31) + sass-embedded (1.103.1-riscv64-linux-android) + google-protobuf (~> 4.31) + sass-embedded (1.103.1-riscv64-linux-gnu) + google-protobuf (~> 4.31) + sass-embedded (1.103.1-riscv64-linux-musl) + google-protobuf (~> 4.31) + sass-embedded (1.103.1-x86_64-darwin) + google-protobuf (~> 4.31) + sass-embedded (1.103.1-x86_64-linux-android) + google-protobuf (~> 4.31) + sass-embedded (1.103.1-x86_64-linux-gnu) + google-protobuf (~> 4.31) + sass-embedded (1.103.1-x86_64-linux-musl) + google-protobuf (~> 4.31) + terminal-table (3.0.2) + unicode-display_width (>= 1.1.1, < 3) + unicode-display_width (2.6.0) + webrick (1.9.2) + +PLATFORMS + aarch64-linux-android + aarch64-linux-gnu + aarch64-linux-musl + arm-linux-androideabi + arm-linux-gnu + arm-linux-gnueabihf + arm-linux-musl + arm-linux-musleabihf + arm64-darwin + riscv64-linux-android + riscv64-linux-gnu + riscv64-linux-musl + ruby + x86-linux-gnu + x86-linux-musl + x86_64-darwin + x86_64-linux-android + x86_64-linux-gnu + x86_64-linux-musl + +DEPENDENCIES + base64 (~> 0.2) + bigdecimal (~> 3.1) + csv (~> 3.3) + jekyll (~> 4.3) + minima (~> 2.5) + webrick (~> 1.8) + +BUNDLED WITH + 2.6.9 diff --git a/_config.cloudflare.yml b/_config.cloudflare.yml new file mode 100644 index 0000000..e233ed7 --- /dev/null +++ b/_config.cloudflare.yml @@ -0,0 +1,6 @@ +# Cloudflare Pages overrides, layered on top of _config.yml: +# bundle exec jekyll build --config _config.yml,_config.cloudflare.yml +# +# The site sits at the domain root here, unlike the GitHub Pages subpath. +url: https://penny-pincher-provider.pages.dev +baseurl: "" diff --git a/_config.yml b/_config.yml index 32b043f..e606f98 100644 --- a/_config.yml +++ b/_config.yml @@ -2,3 +2,25 @@ title: Penny-Pincher Provider description: A curated list of affordable (or almost free) LLM providers theme: minima show_downloads: false + +# GitHub Pages target. It only ever reads this file, so its values live here and +# Cloudflare overrides them via _config.cloudflare.yml. +url: https://tiennm99.github.io +baseurl: /penny-pincher-provider + +# README.md is the site content, pulled into index.md via include_relative. +# Excluding it stops the raw markdown shipping as a duplicate page. +exclude: + - CLAUDE.md + - LICENSE + - README.md + - Gemfile + - Gemfile.lock + - wrangler.jsonc + - _config.cloudflare.yml + - .claude/ + - docs/ + - plans/ + +sass: + sourcemap: never diff --git a/docs/deployment.md b/docs/deployment.md new file mode 100644 index 0000000..b3ba61e --- /dev/null +++ b/docs/deployment.md @@ -0,0 +1,88 @@ +# Deployment + +The site is a Jekyll build of `README.md` (via `index.md`), published to two hosts +from the same source. Both deploy from GitHub Actions on every push to `main`. + +| Target | URL | Workflow | +| --- | --- | --- | +| GitHub Pages | | `.github/workflows/github-pages.yml` | +| Cloudflare Pages | | `.github/workflows/cloudflare-pages.yml` | + +Both workflows build with this repo's `Gemfile` (Jekyll 4.4.1 on Ruby 3.4), so the +two sites cannot drift apart on toolchain. + +## Config layering + +The only difference between the targets is where the site sits on its domain. + +| File | Role | +| --- | --- | +| `_config.yml` | Base config **plus GitHub Pages values** — `url: https://tiennm99.github.io`, `baseurl: /penny-pincher-provider`. | +| `_config.cloudflare.yml` | Overrides for the pages.dev root — `baseurl: ""`. Layered on top, never used alone. | + +```sh +bundle exec jekyll build # GitHub Pages +bundle exec jekyll build --config _config.yml,_config.cloudflare.yml # Cloudflare +``` + +Order matters: later files win. `_config.cloudflare.yml` is excluded from the site +output. + +## GitHub Pages setup + +One repo setting, once: **Settings → Pages → Source → GitHub Actions**. Without it +the workflow's deploy step fails. + +The workflow deliberately does not use `actions/configure-pages` — that action +rewrites `baseurl` with its own guess, which would fight the explicit value in +`_config.yml`. + +## Cloudflare Pages setup + +The project must be **Direct Upload**, not Git-connected. A Git-connected project +would build on every push as well, deploying twice and racing the workflow. + +1. Create the project once, either in the dashboard (**Workers & Pages → Create → + Pages → Upload assets**, named `penny-pincher-provider`) or locally: + + ```sh + npx wrangler pages project create penny-pincher-provider --production-branch main + ``` + +2. Add two repository secrets under **Settings → Secrets and variables → Actions**: + + | Secret | Where to get it | + | --- | --- | + | `CLOUDFLARE_API_TOKEN` | My Profile → API Tokens → Create Token, with the **Cloudflare Pages: Edit** permission. | + | `CLOUDFLARE_ACCOUNT_ID` | Workers & Pages overview sidebar, or `npx wrangler whoami`. | + +`wrangler.jsonc` supplies the project name and `pages_build_output_dir`, so the +workflow's `pages deploy` command needs no arguments. + +## Building locally + +Requires Ruby 3.4.x with Bundler (3.4.8 is what this project was developed +against). Jekyll does not yet support Ruby 4.0 — do not upgrade past the 3.4 branch. + +```sh +bundle config set --local path .bundle/gems # first time only +bundle install +JEKYLL_ENV=production bundle exec jekyll build +``` + +Gems install into the gitignored `.bundle/` inside the repo rather than the shared +rbenv gem home, keeping the project self-contained. To deploy to Cloudflare by hand, +build with the Cloudflare config and run `npx wrangler pages deploy` after +`npx wrangler login`. + +## Notes + +- `Gemfile.lock` is committed. It was generated on aarch64 but its `PLATFORMS` list + includes `x86_64-linux-gnu`, which is what GitHub runners use — without that entry + CI installs would fail. Re-check it after any `bundle update`. +- `_config.yml` excludes `README.md` from the output. It is still the site's + content, pulled into `index.md` by `include_relative`, which reads excluded files + fine — the exclusion only stops the raw markdown shipping as a duplicate page. +- `404.html` is served automatically by both hosts for unmatched routes. +- Ruby is pinned to the `3.4` series in both workflows rather than an exact patch. + Cloudflare's own build image is irrelevant here, since Actions does the building. diff --git a/plans/reports/research-260828-1318-ruby-version-selection.md b/plans/reports/research-260828-1318-ruby-version-selection.md new file mode 100644 index 0000000..1beca03 --- /dev/null +++ b/plans/reports/research-260828-1318-ruby-version-selection.md @@ -0,0 +1,110 @@ +# Research Report: Stable Ruby Version for 1–2 Year Horizon + +Conducted 2026-08-28. Context: local rbenv install + Cloudflare Workers build for this Jekyll site. + +## Executive Summary + +**Install Ruby 3.4.4.** Not 4.0, not 3.3, and not the 3.2.2 recommended earlier in this session +(3.2 hit EOL 2026-04-01). + +Two constraints decide it, both external: + +1. **Jekyll has no Ruby 4.0 support.** Jekyll 4.4.1 (Jan 2025, still latest) declares `>= 2.7.0` + and recommends 3.2+. Theme gems constrained to `~> 3.1` fail version solving under 4.0. +2. **Cloudflare's build image defaults to Ruby 3.4.4.** Pinning anything else risks a build + image that cannot supply it — and there is conflicting evidence that recent 3.4.x patches + are unavailable (see Conflict below). + +3.4 is the only branch in normal (non-security-only) maintenance that Jekyll actually supports. +Picking the exact patch Cloudflare defaults to gives local/CI parity for free. + +## Methodology + +- Sources: 5 (ruby-lang.org branches page, Cloudflare Workers build-image docs, 3 web searches) +- Date range: 2024-12 (Ruby 3.4.0) → 2026-07 (Ruby 4.0.6) +- Terms: ruby maintenance branches EOL, ruby 3.5/4.0 stable, jekyll ruby 4.0 compatibility, + cloudflare workers build image RUBY_VERSION + +## Key Findings + +### 1. Ruby 3.5 does not exist — it shipped as 4.0 + +The version after 3.4 was renumbered. Ruby 4.0.0 released 2025-12-25; latest patch 4.0.6 +(2026-07-14). Anyone searching for "Ruby 3.5 stable" finds only the April 2025 preview. + +### 2. Branch status as of 2026-08-28 + +| Branch | Status | Released | EOL | Verdict | +| --- | --- | --- | --- | --- | +| 4.0 | Normal maintenance | 2025-12-25 | TBD (~2029-03) | Too early — Jekyll unsupported | +| 3.4 | Normal maintenance | 2024-12-25 | TBD (~2028-03) | **Pick this** | +| 3.3 | Security maintenance only | 2023-12-25 | 2027-03-31 | <1 yr left, no bug fixes | +| 3.2 | **EOL** | 2022-12-25 | 2026-04-01 | Dead — no security patches | + +EOL dates for 3.4/4.0 are unpublished. Ruby's historical cadence is ~2 yrs normal + ~1 yr +security, so 3.4 EOL lands ~March 2028 — comfortably past a 2-year horizon. Treat as +convention, not commitment. + +### 3. Jekyll compatibility + +Jekyll 4.4.0 (Jan 2025) added Ruby 3.4 support; 4.4.1 is current. No Jekyll release has +declared Ruby 4.0 support. Community reports (chirpy theme issue #2640, Jan 2026) show +dependency resolution failing on 4.0. Ruby 3.4 moved `csv`, `base64`, `bigdecimal` to bundled +gems — this repo's `Gemfile` already declares them explicitly, so it is 3.4-ready as written. + +### 4. Cloudflare build image + +- Default Ruby **3.4.4**; OS Ubuntu 24.04; x86_64; Node 24.18.0. +- Override via `RUBY_VERSION` env var or `.ruby-version` file. +- Policy: minor versions may auto-update without notice; pin to avoid drift. + +**Conflict:** the docs claim "all versions are available for override," but open issue +cloudflare-docs#27779 ("Recent Ruby versions aren't yet supported") reports 3.4.5–3.4.8 +unavailable. Unresolved. Pinning 3.4.4 sidesteps it entirely — it is the default, so it is +guaranteed present. + +### 5. Security + +3.2 is EOL: no further CVE patches. Anything still on it should move. 3.3 gets security fixes +only until 2027-03-31 — inside the requested horizon, so it fails the 1–2 year test. 3.4 is the +oldest branch still receiving ordinary bug fixes. + +## Recommendation + +```sh +rbenv install 3.4.4 +cd /config/workspace/tiennm99/penny-pincher-provider +rbenv local 3.4.4 # writes .ruby-version; Cloudflare reads the same file +``` + +Requires `libyaml-dev` present first (psych failure seen earlier this session). + +Horizon: safe to ~March 2028 on 3.4. Revisit when Jekyll declares Ruby 4.0 support, or by +early 2028, whichever is first. + +### Correction required in existing docs + +`docs/cloudflare-deployment.md` (written earlier this session) states the build image ships +Ruby 3.2 and suggests `RUBY_VERSION=3.2.2`. Both wrong — default is 3.4.4. With a committed +`.ruby-version` the env-var fallback becomes unnecessary. + +### Pitfalls + +- Don't pin a 3.4.x newer than 3.4.4 until #27779 resolves. +- Don't rely on Cloudflare's default staying 3.4.4 — minors update without notice; the + `.ruby-version` file is the pin. +- Ruby 4.0 local + Jekyll = dependency resolution failure, not a runtime error. Looks confusing. + +## Sources + +- [Ruby maintenance branches](https://www.ruby-lang.org/en/downloads/branches/) +- [Cloudflare Workers build image](https://developers.cloudflare.com/workers/ci-cd/builds/build-image/) +- [cloudflare-docs#27779 — recent Ruby versions unsupported](https://github.com/cloudflare/cloudflare-docs/issues/27779) +- [Jekyll 4.4.0 release notes](https://jekyllrb.com/news/2025/01/27/jekyll-4-4-0-released/) +- [chirpy#2640 — Add Ruby 4 support](https://github.com/cotes2020/jekyll-theme-chirpy/issues/2640) + +## Unresolved + +1. Exact EOL dates for 3.4 and 4.0 — unpublished; ~2028-03 / ~2029-03 inferred from cadence. +2. Whether Cloudflare actually rejects 3.4.5+ (docs vs. issue conflict). Untested. +3. Jekyll's Ruby 4.0 timeline — no roadmap statement found. diff --git a/wrangler.jsonc b/wrangler.jsonc new file mode 100644 index 0000000..158e0ef --- /dev/null +++ b/wrangler.jsonc @@ -0,0 +1,9 @@ +{ + // Cloudflare Pages project. The site is fully static — no Pages Functions — + // so this file only declares where Jekyll writes its output. + "name": "penny-pincher-provider", + "compatibility_date": "2026-08-28", + + // Produced by `bundle exec jekyll build`; see docs/cloudflare-deployment.md + "pages_build_output_dir": "./_site" +}