diff --git a/.github/workflows/preview-smoke.yml b/.github/workflows/preview-smoke.yml index 1e8ba2ae1..6869cbcae 100644 --- a/.github/workflows/preview-smoke.yml +++ b/.github/workflows/preview-smoke.yml @@ -38,13 +38,16 @@ jobs: # preview clones ~616 MiB, checks out ~625 MB and uploads ~697 MB of # output. The build script itself runs in about seven seconds. # Fork heads need a maintainer to mirror the exact SHA into a preview - # branch here; waiting longer cannot create a missing deployment. Only - # install dependencies and browsers once there is a preview to test. + # branch here; waiting longer cannot create a missing deployment. Limit + # their missing-check grace window, but let same-repository checks use + # the full deployment window: Pages can queue before publishing a check. + # Install dependencies and browsers only once there is a preview to test. - name: Wait for the Cloudflare Pages preview of this commit id: preview env: GH_TOKEN: ${{ github.token }} SHA: ${{ github.event.pull_request.head.sha }} + HEAD_REPOSITORY: ${{ github.event.pull_request.head.repo.full_name }} WAIT_MINUTES: "45" CHECK_WAIT_MINUTES: "5" run: | @@ -56,20 +59,18 @@ jobs: --jq '.check_runs | map(select(.name=="Cloudflare Pages")) | max_by(.id) | if . == null then empty else {status, conclusion, summary: .output.summary} end') STATUS=$(printf '%s' "$RUN" | jq -r '.status // empty' 2>/dev/null | head -1) if [ -n "$STATUS" ]; then SEEN_CHECK=true; fi - if [ "$SEEN_CHECK" = false ] && [ "$i" -ge "$CHECK_ATTEMPTS" ]; then + if [ "$SEEN_CHECK" = false ] && [ "$HEAD_REPOSITORY" != "$GITHUB_REPOSITORY" ] && [ "$i" -ge "$CHECK_ATTEMPTS" ]; then echo "::error::Cloudflare Pages check never appeared for $SHA within $CHECK_WAIT_MINUTES minutes. For a fork PR, a maintainer must push the exact head SHA to a preview branch in this repository, then rerun this workflow. For same-repository PRs, check Pages branch deployment controls and the GitHub integration." exit 1 fi if [ "$STATUS" = "completed" ]; then CONCLUSION=$(printf '%s' "$RUN" | jq -r '.conclusion' | head -1) if [ "$CONCLUSION" != "success" ]; then - # Cloudflare builds this repository close to its limits, so a - # failure here is not automatically a defect in the commit. Check - # whether `E2E (Cloudflare Pages semantics)` passed: it runs the - # same bin/build.sh, so green there and red here means the build - # is fine and Cloudflare fell over. There is no API to retry a - # Git-integration deployment -- pushing again is the way. - echo "::error::Cloudflare Pages preview deployment concluded: $CONCLUSION. If the 'E2E (Cloudflare Pages semantics)' checks are green, the build itself is fine (they run the same bin/build.sh) and this was Cloudflare-side; push again to trigger a new deployment. See docs/explorations/2026-08-21-cloudflare-preview-flake.md" + # The Pages-semantics suite uses the same build script, but its + # success cannot diagnose a failed provider deployment. Inspect + # the actual Pages logs to distinguish clone, build and upload + # failures. Pushing again triggers a new Git-integration deploy. + echo "::error::Cloudflare Pages preview deployment concluded: $CONCLUSION. Inspect the Pages deployment logs and compare the 'E2E (Cloudflare Pages semantics)' checks to identify the failed stage. For a transient failure, retry in the Pages dashboard or push again to trigger a new deployment. See docs/explorations/2026-08-21-cloudflare-preview-flake.md" exit 1 fi URL=$(printf '%s' "$RUN" | jq -r '.summary' | grep -oE 'https://[a-z0-9-]+\.document-[a-z0-9]+\.pages\.dev' | head -1) diff --git a/bin/build-social-image.mjs b/bin/build-social-image.mjs new file mode 100644 index 000000000..f66c77ccd --- /dev/null +++ b/bin/build-social-image.mjs @@ -0,0 +1,42 @@ +/** Render the product sharing artwork without a runtime dependency. + * npm install --prefix --no-audit --no-fund @resvg/resvg-js@2.6.2 + * node bin/build-social-image.mjs /package.json + */ +import { createRequire } from 'node:module'; +import { mkdirSync, readFileSync, writeFileSync } from 'node:fs'; +import { resolve } from 'node:path'; + +if (!process.argv[2]) throw new Error('Pass the package.json path of an installation of @resvg/resvg-js.'); +const { Resvg } = createRequire(resolve(process.argv[2]))('@resvg/resvg-js'); +const brand = readFileSync(new URL('../public/icons/document-light.svg', import.meta.url), 'utf8').match( + /([\s\S]*?)<\/g>/, +)[1]; + +for (const [name, width, height] of [ + ['document-editor-v1', 1200, 630], + ['document-editor-github-v1', 1280, 640], +]) { + const svg = ` + Online Document Editor + Open-source document editing in your browser. Word, Excel, PowerPoint and PDF. Local editing, no signup, optional AI (Experimental Beta), WebMCP and PWA. + + + ${brand} + + Online Document Editor + Your documents. + Right in your browser. + Word · Excel · PowerPoint · PDF + Local editing. Open source. No signup. + + AI (Experimental Beta) · WebMCP · PWA + edit.chaxus.com + github.com/ranuts/document + + +`; + const folder = new URL('../public/social/', import.meta.url); + mkdirSync(folder, { recursive: true }); + writeFileSync(new URL(`${name}.svg`, folder), svg); + writeFileSync(new URL(`${name}.png`, folder), new Resvg(svg).render().asPng()); +} diff --git a/bin/pages/render-home.mjs b/bin/pages/render-home.mjs index 749326a1a..1341d74e7 100644 --- a/bin/pages/render-home.mjs +++ b/bin/pages/render-home.mjs @@ -11,6 +11,7 @@ import { DEFAULT_LOCALE, LOCALES } from './locales.mjs'; import { langMenu } from './chrome.mjs'; import { escapeHtml } from './markdown.mjs'; import { UI } from './ui.mjs'; +import { socialImageTags } from './social-image.mjs'; /** * The homepage. @@ -177,11 +178,11 @@ ${alternates} ${ogAlternates} - + - - +${socialImageTags()} + + + + + + + + + + + + + + + + + diff --git a/history.html b/history.html index e7b02cd1e..46151f689 100644 --- a/history.html +++ b/history.html @@ -38,6 +38,34 @@ + + + + + + + + + + + + + + + + diff --git a/index.html b/index.html index d72878d60..3a42ecceb 100644 --- a/index.html +++ b/index.html @@ -66,12 +66,19 @@ - - + + - - - + + + + + + + + + +