From 59ca08dd6265788503a7beaf2c8f50f2cec8ea52 Mon Sep 17 00:00:00 2001 From: Oscar Levin Date: Fri, 22 May 2026 09:27:26 -0600 Subject: [PATCH] Add multiple author support and preview workflow --- .github/workflows/pr-preview.yml | 200 +++++++++++++++++++++++++++++++ .vscode/settings.json | 5 + README.md | 51 ++++++++ _includes/author-profile.html | 4 +- _includes/sidebar.html | 12 +- 5 files changed, 268 insertions(+), 4 deletions(-) create mode 100644 .github/workflows/pr-preview.yml create mode 100644 .vscode/settings.json diff --git a/.github/workflows/pr-preview.yml b/.github/workflows/pr-preview.yml new file mode 100644 index 0000000..540825b --- /dev/null +++ b/.github/workflows/pr-preview.yml @@ -0,0 +1,200 @@ +name: PR Preview + +on: + pull_request: + types: [opened, synchronize, reopened, ready_for_review] + +permissions: + contents: read + issues: write + pull-requests: write + +concurrency: + group: pr-preview-${{ github.event.pull_request.number }} + cancel-in-progress: true + +jobs: + build-preview: + runs-on: ubuntu-latest + outputs: + artifact_name: ${{ steps.meta.outputs.artifact_name }} + screenshot_name: ${{ steps.meta.outputs.screenshot_name }} + post_path: ${{ steps.changed_post.outputs.post_path }} + post_url: ${{ steps.changed_post.outputs.post_url }} + steps: + - name: Set artifact name + id: meta + run: | + echo "artifact_name=site-preview-pr-${{ github.event.number }}" >> "$GITHUB_OUTPUT" + echo "screenshot_name=site-screenshots-pr-${{ github.event.number }}" >> "$GITHUB_OUTPUT" + + - name: Checkout + uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Set up Ruby + uses: ruby/setup-ruby@v1 + with: + bundler-cache: true + + - name: Build site + run: bundle exec jekyll build + + - name: Detect changed post + id: changed_post + env: + BASE_SHA: ${{ github.event.pull_request.base.sha }} + HEAD_SHA: ${{ github.sha }} + run: | + mapfile -t posts < <(git diff --name-only "$BASE_SHA" "$HEAD_SHA" -- '_posts/*.md') + if [[ ${#posts[@]} -eq 0 ]]; then + echo "post_path=" >> "$GITHUB_OUTPUT" + echo "post_url=/" >> "$GITHUB_OUTPUT" + exit 0 + fi + + post="${posts[0]}" + slug="$(basename "$post" .md)" + slug="${slug#????-??-??-}" + echo "post_path=$post" >> "$GITHUB_OUTPUT" + echo "post_url=/$slug/" >> "$GITHUB_OUTPUT" + + - name: Upload site artifact + uses: actions/upload-artifact@v4 + with: + name: ${{ steps.meta.outputs.artifact_name }} + path: _site + retention-days: 7 + + - name: Install Playwright Chromium + run: npx -y playwright@1.53.0 install --with-deps chromium + + - name: Capture preview screenshot + env: + POST_URL: ${{ steps.changed_post.outputs.post_url }} + run: | + python3 -m http.server --directory _site 4173 > /tmp/pr-preview-server.log 2>&1 & + server_pid=$! + trap 'kill "$server_pid"' EXIT + + for i in {1..20}; do + if curl -fsS "http://127.0.0.1:4173${POST_URL}" >/dev/null; then + break + fi + sleep 0.5 + done + + npx -y playwright@1.53.0 screenshot --browser=chromium --full-page "http://127.0.0.1:4173${POST_URL}" preview-long.png + + - name: Upload screenshot artifact + uses: actions/upload-artifact@v4 + with: + name: ${{ steps.meta.outputs.screenshot_name }} + path: preview-long.png + retention-days: 7 + + deploy-cloudflare-preview: + runs-on: ubuntu-latest + needs: build-preview + if: ${{ !github.event.pull_request.head.repo.fork && vars.CLOUDFLARE_PAGES_PROJECT != '' && vars.CLOUDFLARE_ACCOUNT_ID != '' && secrets.CLOUDFLARE_API_TOKEN != '' }} + permissions: + contents: read + deployments: write + outputs: + preview_url: ${{ steps.cf.outputs.alias }} + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Set up Ruby + uses: ruby/setup-ruby@v1 + with: + bundler-cache: true + + - name: Build site + run: bundle exec jekyll build + + - name: Publish to Cloudflare Pages + id: cf + uses: cloudflare/pages-action@v1 + with: + apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }} + accountId: ${{ vars.CLOUDFLARE_ACCOUNT_ID }} + projectName: ${{ vars.CLOUDFLARE_PAGES_PROJECT }} + directory: _site + gitHubToken: ${{ secrets.GITHUB_TOKEN }} + branch: pr-${{ github.event.number }} + wranglerVersion: '3' + + comment-preview: + runs-on: ubuntu-latest + needs: [build-preview, deploy-cloudflare-preview] + if: ${{ always() && !github.event.pull_request.head.repo.fork && needs.build-preview.result == 'success' && (needs.deploy-cloudflare-preview.result == 'success' || needs.deploy-cloudflare-preview.result == 'skipped') }} + steps: + - name: Add or update PR preview comment + uses: actions/github-script@v7 + env: + RUN_URL: https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }} + ARTIFACT_NAME: ${{ needs.build-preview.outputs.artifact_name }} + SCREENSHOT_NAME: ${{ needs.build-preview.outputs.screenshot_name }} + POST_PATH: ${{ needs.build-preview.outputs.post_path }} + POST_URL: ${{ needs.build-preview.outputs.post_url }} + CLOUDFLARE_URL: ${{ needs.deploy-cloudflare-preview.outputs.preview_url }} + CLOUDFLARE_ENABLED: ${{ vars.CLOUDFLARE_PAGES_PROJECT != '' && vars.CLOUDFLARE_ACCOUNT_ID != '' && secrets.CLOUDFLARE_API_TOKEN != '' }} + CLOUDFLARE_RESULT: ${{ needs.deploy-cloudflare-preview.result }} + with: + script: | + const marker = ""; + const postLine = process.env.POST_PATH + ? `- Changed post detected: \`${process.env.POST_PATH}\`\n- Suggested file to open after extracting: \`_site${process.env.POST_URL}index.html\`` + : "- No post markdown file changed in this PR; preview starts at `_site/index.html`."; + + const cloudflareLine = process.env.CLOUDFLARE_ENABLED === "true" + ? (process.env.CLOUDFLARE_RESULT === "success" + ? (process.env.CLOUDFLARE_URL + ? `- Hosted preview (Cloudflare Pages): [Open preview](${process.env.CLOUDFLARE_URL})` + : "- Hosted preview (Cloudflare Pages): deployed, but URL output was empty. Check deployment details.") + : "- Hosted preview (Cloudflare Pages): deployment did not succeed; check workflow logs.") + : "- Hosted preview (Cloudflare Pages): not configured (set repository vars/secrets; see README)."; + + const body = `${marker} + ## PR Preview Ready + + A static preview was built for this PR. + + - Download artifact: **${process.env.ARTIFACT_NAME}** from [this workflow run](${process.env.RUN_URL}) + - Download long screenshot artifact: **${process.env.SCREENSHOT_NAME}** from [this workflow run](${process.env.RUN_URL}) + ${postLine} + ${cloudflareLine} + - Quick local preview: + - Extract artifact zip + - Run: \`python3 -m http.server --directory _site 4173\` + - Open: \`http://localhost:4173${process.env.POST_URL}\` + `; + + const { owner, repo } = context.repo; + const issue_number = context.issue.number; + const comments = await github.rest.issues.listComments({ + owner, + repo, + issue_number, + per_page: 100, + }); + + const existing = comments.data.find((comment) => comment.body && comment.body.includes(marker)); + if (existing) { + await github.rest.issues.updateComment({ + owner, + repo, + comment_id: existing.id, + body, + }); + } else { + await github.rest.issues.createComment({ + owner, + repo, + issue_number, + body, + }); + } diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..b336f0a --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,5 @@ +{ + "chat.tools.terminal.autoApprove": { + "bundle": true + } +} \ No newline at end of file diff --git a/README.md b/README.md index fe8f235..2a74b44 100644 --- a/README.md +++ b/README.md @@ -8,8 +8,59 @@ The site is generated by Jekyll, based on the [Minimal Mistakes](https://mmistak To submit a blog entry, create a `md` file in the `_posts` directory. Look for examples of posts there for formatting help. +Author bios in the sidebar are enabled for posts by default. For a single author, use: + +```yaml +author: Oscar Levin +``` + +For multiple authors, use: + +```yaml +authors: + - Oscar Levin + - Steven Clontz +``` + +Each name must match an entry in `_data/authors.yml`. + ## Building the site When the site is pushed to github, it will automatically rebuild. To preview locally, run `bundle exec jekyll serve`. Note, some changes to `_config.yml` require stopping and starting this, while other changes to the site should be automatically incorporated. See the [Minimal Mistakes Documentation](https://mmistakes.github.io/minimal-mistakes/docs/quick-start-guide/) for more. + +## PR previews + +This repo now includes a PR preview workflow at `.github/workflows/pr-preview.yml`. + +For each pull request update, it: + +1. Builds the site with Jekyll. +2. Uploads the full `_site` output as a workflow artifact. +3. Captures a full-page screenshot artifact for quick visual review. +4. Optionally deploys to Cloudflare Pages for a hosted preview URL. +5. Posts or updates a sticky PR comment with links and instructions. + +This gives reviewers a consistent way to preview rendered pages without running Jekyll locally. + +### Cloudflare Pages setup (optional) + +If you want hosted preview links directly in PRs, set up Cloudflare Pages once and add repo settings. + +1. In Cloudflare, create a Pages project (or use an existing one). + - Project name should match `CLOUDFLARE_PAGES_PROJECT` exactly. +2. In GitHub repo settings, add **Repository variables**: + - `CLOUDFLARE_ACCOUNT_ID`: your Cloudflare account ID + - `CLOUDFLARE_PAGES_PROJECT`: the Cloudflare Pages project name +3. In GitHub repo settings, add **Repository secret**: + - `CLOUDFLARE_API_TOKEN`: Cloudflare API token with `Account -> Cloudflare Pages -> Edit` +4. In Cloudflare, create the API token at **My Profile -> API Tokens -> Create Token -> Custom token** and scope it to the specific account. +5. Open or update a PR. The workflow will deploy `_site` and include preview status in the PR comment. +6. Open the PR **Deployments** panel to click through to the hosted Cloudflare preview URL. + +Notes: + +- Cloudflare deploy is skipped automatically if these vars/secrets are missing. +- Cloudflare deploy is also skipped for forked PRs for security. +- Artifact and screenshot previews still run even when Cloudflare is skipped. diff --git a/_includes/author-profile.html b/_includes/author-profile.html index e256ec7..0f122a0 100644 --- a/_includes/author-profile.html +++ b/_includes/author-profile.html @@ -1,5 +1,5 @@ -{% assign author = page.author | default: page.authors[0] | default: site.author %} -{% assign author = site.data.authors[author] | default: author %} +{% assign author_key = include.author_key | default: page.author | default: page.authors[0] %} +{% assign author = site.data.authors[author_key] | default: author_key | default: site.author %}
diff --git a/_includes/sidebar.html b/_includes/sidebar.html index a4ca1ca..283ee8a 100644 --- a/_includes/sidebar.html +++ b/_includes/sidebar.html @@ -1,6 +1,14 @@ {% if page.author_profile or layout.author_profile or page.sidebar %} -