Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
70 changes: 64 additions & 6 deletions .github/workflows/deploy-preview.yml
Original file line number Diff line number Diff line change
Expand Up @@ -59,13 +59,23 @@ permissions:
contents: read
pull-requests: write # to post the preview URL comment
id-token: write
# GitHub Deployments registration so Decap CMS's "Deploy Preview Links"
# spinner (https://decapcms.org/docs/deploy-preview-links/) can find
# the preview URL for each PR. Without this, the editor toolbar shows
# a perpetual "looking for preview" spinner — Decap polls
# /repos/.../deployments?ref=<pr-head>, never finds a deployment with
# state=success + a target_url, gives up.
# GitHub Deployments registration. admin/deploy-status-pill.js polls
# /repos/.../deployments?environment=preview-pr-<N> to flip its
# in-flight spinner; the per-PR GH Environment row also gives the
# Environments UI something to show. (NB: this is NOT what Decap's
# own "Deploy Preview Links" feature reads — see `statuses: write`.)
deployments: write
# Commit Statuses. Decap CMS's github backend implements
# `getDeployPreview` as GET /repos/.../commits/<pr-head-sha>/status
# and surfaces the first status whose `context` matches
# `backend.preview_context` (admin/config*.yml pins this to
# `deploy/preview`). With no such status the editor's
# deploy-preview-links button is stuck on "Check for Preview"
# forever even though the preview is live (decap-cms 3.12.2; the
# docs' "polls /deployments?ref=" describes the *Netlify* backend,
# not github). The "Set Decap deploy-preview commit status" step
# publishes that status; this scope lets it.
statuses: write

jobs:
# ────────────────────────────────────────────────────────────
Expand Down Expand Up @@ -301,6 +311,54 @@ jobs:

core.notice(`Registered deployment ${deployment.id} → ${url}`);

- name: Set Decap deploy-preview commit status
# THIS is the status Decap CMS's editor actually reads. Its
# github backend implements getDeployPreview as
# GET /repos/.../commits/<pr-head-sha>/status
# then picks the first status whose `context` matches
# `backend.preview_context` (admin/config*.yml → `deploy/preview`;
# decap-cms 3.12.2 falls back to "context includes 'deploy'"
# when unset). Without a matching commit status the editor's
# deploy-preview button is pinned to "Check for Preview"
# forever even though the preview is already serving — the
# GitHub Deployment registered above is invisible to this code
# path (that polling-/deployments?ref= behaviour is the Netlify
# backend, not github). Decap then runs the status `target_url`
# through its preview_path builder, so the site root here
# becomes https://preview-pr<N>.adamdaniel.ai/blog/<slug>/ in
# the editor's "View Preview" link.
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 (2026-04-09)
env:
PR_NUMBER: ${{ github.event.pull_request.number }}
PR_HEAD_SHA: ${{ github.event.pull_request.head.sha }}
with:
script: |
const pr = process.env.PR_NUMBER;
const sha = process.env.PR_HEAD_SHA;
const apex = process.env.APEX_DOMAIN;
const cfId = process.env.CLOUDFRONT_DISTRIBUTION_ID;
const bucket = process.env.PREVIEW_BUCKET;
const region = process.env.AWS_REGION;

// Site root (no /blog/<slug>/): Decap appends the entry's
// preview_path itself. Mirrors the deployment registration
// URL above so both surfaces agree.
const url = cfId
? `https://preview-pr${pr}.${apex}/`
: `http://${bucket}.s3-website-${region}.amazonaws.com/pr-${pr}/`;

await github.rest.repos.createCommitStatus({
owner: context.repo.owner,
repo: context.repo.repo,
sha,
state: 'success',
context: 'deploy/preview',
target_url: url,
description: `Preview live at ${url}`,
});

core.notice(`Set deploy/preview commit status on ${sha} → ${url}`);

- name: Register GitHub Deployment for the per-CMS-slug preview
if: steps.cms_slug.outputs.slug != ''
# Mirror of the registration above, scoped to the cms-<slug>
Expand Down
9 changes: 7 additions & 2 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -113,8 +113,8 @@ Editors get a WYSIWYG preview of the page they're editing without publishing. Th

Two admin affordances live alongside the preview, both loaded (deferred) from all three `admin/index*.html` shells in the order **`live-url-derive.js` → `live-url-banner.js` → `native-preview-href.js` → `posts-list-enhance.js`** (`live-url-derive.js` exposes `window.LiveURL.compute()` and MUST precede its consumer; this order is locked by `cms-posts-list-enhance.spec.js` and `cms-permalink-contract.spec.js`):

- **`admin/live-url-banner.js`** — the "View page on site:" banner above the entry form. A past change (#184) deleted it, leaving the editor with no link to the post; issue #1042 restored it byte-for-byte. It renders one anchor (`data-testid="cms-live-url-banner-link"`) at the live URL `window.LiveURL.compute()` derives, or a placeholder when unpublished / no slug. `cms-live-url-banner-link` is in `native-preview-href.js`'s `EXCLUDE_IDS` so the native-anchor hide can't swallow it (the banner is in the form pane, not the toolbar, so it wouldn't match anyway — the exclusion is the original pre-#184 contract, kept defensively).
- **`admin/posts-list-enhance.js`** — turns Decap's bare Posts list into a dashboard (issue #1042). It **augments in place** — it never replaces Decap's `<a href="#/collections/posts/entries/…">` cards, so every existing e2e selector keeps resolving — adding per-row status / published-link / last-edited / preview-PR columns. Remote data (last-edited via one batched GitHub GraphQL query, the production deployment, and open editorial PRs) is fetched in **three calls total regardless of post count**, cached in sessionStorage, and refreshed both on a ↻ button and whenever the user returns to the list from an entry. Auth reuses the operator's Decap token at `localStorage["decap-cms-user"].token` (same pattern as `deploy-status-pill.js`); with no token / on any API error it degrades to the local-only columns. It also CSS-hides only the "E2E Canary" Quick-add menu item (the `_e2e` collection is `create: true` and test-locked, so it can't be dropped from config; the `#/collections/e2e/new` route is untouched and `canary-content.test.js` stays green). See the **Automated-test fixtures** note under *Content model* for the default-hide behaviour.
- **`admin/live-url-banner.js`** — the "View page on site:" banner above the entry form. A past change (#184) deleted it, leaving the editor with no link to the post; issue #1042 restored it. It renders one anchor (`data-testid="cms-live-url-banner-link"`) at the live URL `window.LiveURL.compute()` derives, or a placeholder when unpublished / no slug. **Preview-aware origin:** a post edited through Decap's editorial workflow lives on a `cms/<col>/<file-slug>` PR branch and is NOT on production until that PR merges, so linking the prod URL 404s for the whole draft lifecycle. When the open entry has an open editorial-workflow PR the banner swaps the host to that PR's preview env (`preview-pr<N>.adamdaniel.ai`), exactly the URL `posts-list-enhance.js` surfaces in the list; with no open PR it stays at the current origin. The open-PR map is read from `posts-list-enhance.js`'s shared sessionStorage cache when warm, else one `pulls?state=open` REST call (operator's Decap token, same auth as `deploy-status-pill.js`); no token / API error degrades to the current origin. `cms-live-url-banner-link` is in `native-preview-href.js`'s `EXCLUDE_IDS` so the native-anchor hide can't swallow it (the banner is in the form pane, not the toolbar, so it wouldn't match anyway — the exclusion is the original pre-#184 contract, kept defensively).
- **`admin/posts-list-enhance.js`** — turns Decap's bare Posts list into a dashboard (issue #1042). It **augments in place** — it never replaces Decap's `<a href="#/collections/posts/entries/…">` cards, so every existing e2e selector keeps resolving — adding per-row status / published-link / last-edited columns plus, per state: **"view published changes"** (the merged PR's GitHub `/files` diff — shown only when the post is actually live on `main`), **"preview draft ↗"** (the open editorial-workflow PR's `preview-pr<N>.adamdaniel.ai/blog/<slug>/` env), and **"view draft changes"** (that open PR's `/files` diff). "view published changes" renders before "preview draft" when both are present; an unpublished draft (no merged PR on `main`) shows neither published-changes nor a `published ↗` link. Remote data — last-edited **and the PR that last commit was merged in** (`history` + `associatedPullRequests` in one batched GitHub GraphQL query), the production deployment, and open editorial PRs — is fetched in **three calls total regardless of post count**, cached in sessionStorage, and refreshed both on a ↻ button and whenever the user returns to the list from an entry. Auth reuses the operator's Decap token at `localStorage["decap-cms-user"].token` (same pattern as `deploy-status-pill.js`); with no token / on any API error it degrades to the local-only columns. It also CSS-hides only the "E2E Canary" Quick-add menu item (the `_e2e` collection is `create: true` and test-locked, so it can't be dropped from config; the `#/collections/e2e/new` route is untouched and `canary-content.test.js` stays green). See the **Automated-test fixtures** note under *Content model* for the default-hide behaviour.

### Embedding HTML / Widgets

Expand Down Expand Up @@ -250,6 +250,11 @@ URL shown in comment:
- With `PREVIEW_CLOUDFRONT_ID`: `https://preview-pr{N}.adamdaniel.ai/`
- Without: `http://adamdaniel-ai-previews.s3-website-us-east-1.amazonaws.com/pr-{N}/` (HTTP fallback — Decap CMS won't work over this)

**Two independent "preview is ready" signals, read by different consumers — keep both:**

- **GitHub Deployment** (`environment: preview-pr-<N>`, `state: success`, `environment_url`). Polled by `admin/deploy-status-pill.js` to flip its in-flight spinner, and surfaces the per-PR row in the Environments UI. NOT read by Decap's editor.
- **`deploy/preview` commit status** (`createCommitStatus` on the PR head SHA, `state: success`, `target_url` = preview root; needs `statuses: write`). THIS is what Decap CMS's editor reads: decap-cms 3.12.2's github backend implements `getDeployPreview` as `GET /repos/.../commits/<pr-head-sha>/status` and surfaces the first status whose `context` matches `backend.preview_context` (pinned to `deploy/preview` in all three `admin/config*.yml`; `patch-preview-config.sh` doesn't touch it). Without this status the editor's deploy-preview button is stuck on **"Check for Preview"** forever even though the preview is live — the GitHub Deployment is invisible to this code path (the decapcms.org "polls `/deployments?ref=`" docs describe the *Netlify* backend, not github). Decap runs the status `target_url` through its `preview_path` builder, so the editor's "View Preview" link becomes `https://preview-pr<N>.adamdaniel.ai/blog/<slug>/`. The workflow↔config contract is locked by `e2e/cms-posts-list-enhance.spec.js` (which `select-specs.js` runs on `deploy-preview.yml` changes). An existing open PR only gets the status on its next push (the step runs per preview deploy; it isn't backfilled).

#### Job: `teardown-preview` (when action == `closed`)

1. AWS OIDC auth
Expand Down
6 changes: 6 additions & 0 deletions admin/config-local.yml
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,12 @@ backend:
name: github
repo: Adam-S-Daniel/adamdaniel.ai
branch: main
# Mirrors admin/config.yml — Decap's github-backend deploy-preview
# detection matches a `deploy/preview` commit status. Inert under
# local_backend (decap-server is simple-mode, no editorial workflow),
# kept so the three configs stay in lockstep and one static spec can
# assert the contract everywhere.
preview_context: deploy/preview

site_url: http://localhost:4000
display_url: http://localhost:4000
Expand Down
4 changes: 4 additions & 0 deletions admin/config-test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,10 @@

backend:
name: test-repo
# Mirrors admin/config.yml — inert for the in-browser test-repo
# backend (its getDeployPreview is a stub), kept so the three configs
# stay in lockstep and one static spec asserts the contract everywhere.
preview_context: deploy/preview

site_url: http://localhost:4000
display_url: http://localhost:4000
Expand Down
12 changes: 12 additions & 0 deletions admin/config.yml
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,18 @@ backend:
# OAuth proxy endpoint
base_url: https://sq8d4876v8.execute-api.us-east-1.amazonaws.com
auth_endpoint: prod/auth
# Decap's deploy-preview-links: the github backend reads
# GET /commits/<pr-head-sha>/status and surfaces the first status
# whose `context` equals this. .github/workflows/deploy-preview.yml's
# "Set Decap deploy-preview commit status" step publishes exactly
# `deploy/preview` once the per-PR preview is live. Without this the
# editor's button is stuck on "Check for Preview" forever (decap-cms
# 3.12.2: unset → loose "context includes 'deploy'" match; pinning it
# is the explicit, auditable half of the contract — see the static
# lock in e2e/cms-posts-list-enhance.spec.js). patch-preview-config.sh
# only rewrites site_url/display_url/branch, so this survives the
# per-PR preview rebuild unchanged.
preview_context: deploy/preview

# Editorial workflow: every Save opens a PR on its own branch instead of
# committing straight to main. The PR drives the draft → review → ready →
Expand Down
Loading
Loading