feat(deploy): publish to GitHub Pages and Cloudflare Pages

Build the site from one source and deploy it to both hosts from GitHub
Actions, so neither depends on a host's default Jekyll toolchain.

- Gemfile/Gemfile.lock pin Jekyll 4.4.1 on Ruby 3.4; both workflows use
  them, which keeps the two sites on an identical toolchain
- _config.yml carries the GitHub Pages url and baseurl, fixing asset
  paths that previously resolved off-site; _config.cloudflare.yml layers
  over it for the pages.dev root
- wrangler.jsonc declares the Pages project name and output directory
- 404.html gives both hosts a page for unmatched routes
- exclude README.md from the output and stop shipping CSS sourcemaps
This commit is contained in:
tiennm99 committed 2026-08-28 15:35:34 +07:00
1 parent 8d52ed0dd0
commit 64987877a9
11 files changed
+532

No files matched your search

+38
View File
@@ -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
+49
View File
@@ -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
+11
View File
@@ -1,2 +1,13 @@
session-state session-state
settings.local.json settings.local.json
# Jekyll build output
_site/
.jekyll-cache/
.jekyll-metadata
# Project-local gems (bundler path config + installed gems)
.bundle/
# Wrangler local state
.wrangler/
+13
View File
@@ -0,0 +1,13 @@
---
layout: default
title: Page not found
permalink: /404.html
---
<h1>Page not found</h1>
<p>That page does not exist. The provider list lives on the
<a href="/">home page</a>.</p>
<p>If a link here is broken,
<a href="https://github.com/tiennm99/penny-pincher-provider/issues/new">open an issue</a>.</p>
+10
View File
@@ -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"
+176
View File
@@ -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
+6
View File
@@ -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: ""
+22
View File
@@ -2,3 +2,25 @@ title: Penny-Pincher Provider
description: A curated list of affordable (or almost free) LLM providers description: A curated list of affordable (or almost free) LLM providers
theme: minima theme: minima
show_downloads: false 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
+88
View File
@@ -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 | <https://tiennm99.github.io/penny-pincher-provider> | `.github/workflows/github-pages.yml` |
| Cloudflare Pages | <https://penny-pincher-provider.pages.dev> | `.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.
@@ -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.
+9
View File
@@ -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"
}