Run Flutter widget code on Codename One #11488
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| name: Build Hugo Website | |
| on: | |
| pull_request: | |
| paths: | |
| - 'docs/website/**' | |
| - 'docs/developer-guide/**' | |
| - 'scripts/cn1playground/**' | |
| - 'scripts/initializr/**' | |
| - 'scripts/website/**' | |
| - 'scripts/skindesigner/**' | |
| # The Initializr/playground/skindesigner JS bundles are built from | |
| # JavaScriptPort sources and the ParparVM translator at workflow | |
| # time, so a change in either also needs to redeploy the website. | |
| # Without these paths a JS-port bug fix (e.g. dialog rendering | |
| # in be3bc6dcd) sits in the branch with no Cloudflare preview | |
| # refresh until an unrelated docs change happens to trigger | |
| # the workflow. | |
| - 'Ports/JavaScriptPort/**' | |
| - 'vm/ByteCodeTranslator/**' | |
| - 'vm/JavaAPI/**' | |
| - 'CodenameOne/src/**' | |
| # The other root build_javadocs.sh generates from, and the other one | |
| # check-guide-links.py indexes. Without it, deleting a linked CLDC type | |
| # merges unvalidated and the next scheduled build is where it surfaces. | |
| - 'Ports/CLDC11/src/**' | |
| # The developer guide this site renders includes a build hint table that is | |
| # generated rather than committed, so a change to the catalog or to the | |
| # renderer changes the published page without touching docs/. The | |
| # annotations it is also rendered from are covered by CodenameOne/src/** | |
| # above. Same gap codex reported on developer-guide-docs.yml; this is the | |
| # other workflow that renders the table. | |
| - 'maven/build-hint-catalog/**' | |
| - 'maven/build-hint-tools/**' | |
| - 'scripts/gen-build-hint-table.sh' | |
| # The Hugo build embeds JavaDocs produced by this script, so a change | |
| # to how the docs are generated must redeploy the website too. | |
| - '.github/scripts/build_javadocs.sh' | |
| # The doclet that renders the API pages themselves. It is outside the | |
| # maven reactor and built only by this workflow, so nothing else would | |
| # catch a change to it. | |
| - 'maven/javadoc-hugo-doclet/**' | |
| - '.github/workflows/website-docs.yml' | |
| push: | |
| branches: [main, master] | |
| paths: | |
| - 'docs/website/**' | |
| - 'docs/developer-guide/**' | |
| - 'scripts/cn1playground/**' | |
| - 'scripts/initializr/**' | |
| - 'scripts/website/**' | |
| - 'scripts/skindesigner/**' | |
| - 'Ports/JavaScriptPort/**' | |
| - 'vm/ByteCodeTranslator/**' | |
| - 'vm/JavaAPI/**' | |
| - 'CodenameOne/src/**' | |
| # The other root build_javadocs.sh generates from, and the other one | |
| # check-guide-links.py indexes. Without it, deleting a linked CLDC type | |
| # merges unvalidated and the next scheduled build is where it surfaces. | |
| - 'Ports/CLDC11/src/**' | |
| # As above: the build hint table is generated, not committed. | |
| - 'maven/build-hint-catalog/**' | |
| - 'maven/build-hint-tools/**' | |
| - 'scripts/gen-build-hint-table.sh' | |
| # The Hugo build embeds JavaDocs produced by this script, so a change | |
| # to how the docs are generated must redeploy the website too. | |
| - '.github/scripts/build_javadocs.sh' | |
| # The doclet that renders the API pages themselves. It is outside the | |
| # maven reactor and built only by this workflow, so nothing else would | |
| # catch a change to it. | |
| - 'maven/javadoc-hugo-doclet/**' | |
| - '.github/workflows/website-docs.yml' | |
| workflow_dispatch: | |
| inputs: | |
| deploy_production: | |
| description: 'Deploy this master build to production' | |
| required: false | |
| default: false | |
| type: boolean | |
| # Daily production rebuild so future-dated blog posts (the daily release | |
| # follow-ups) publish on their own date with no manual push. Production | |
| # builds keep HUGO_BUILD_FUTURE=false, so a post only appears once its date | |
| # has arrived (UTC; hugo.toml sets no timeZone). Runs at 00:15 UTC; GitHub | |
| # may delay scheduled runs under load, but the post still surfaces same-day. | |
| schedule: | |
| - cron: '15 0 * * *' | |
| permissions: | |
| contents: read | |
| pull-requests: write | |
| jobs: | |
| build: | |
| runs-on: ubuntu-latest | |
| # A ceiling, because there was not one: this job hung in apt and ran for six | |
| # hours before GitHub's own limit stopped it. | |
| timeout-minutes: 90 | |
| env: | |
| CLOUDFLARE_TOKEN: ${{ secrets.CLOUDFLARE_TOKEN || secrets.CF_API_TOKEN }} | |
| CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_ACCOUNT_ID || secrets.CF_ACCOUNT_ID }} | |
| CF_PAGES_PROJECT_NAME: ${{ vars.CLOUDFLARE_PAGES_PROJECT_NAME || 'codenameone' }} | |
| CF_PAGES_PRODUCTION_BRANCH: ${{ vars.CLOUDFLARE_PAGES_PRODUCTION_BRANCH || 'main' }} | |
| WRANGLER_VERSION: '4.129.0' | |
| # `npx` installs Wrangler on a clean runner. An unavailable npm audit | |
| # endpoint must not keep that install alive while the Worker test waits | |
| # for a process that has not actually started yet. | |
| npm_config_audit: 'false' | |
| npm_config_fund: 'false' | |
| steps: | |
| - name: Check out repository | |
| uses: actions/checkout@v6 | |
| - name: Set up Hugo | |
| uses: peaceiris/actions-hugo@v3 | |
| with: | |
| hugo-version: 'latest' | |
| extended: true | |
| - name: Set up Java 8 for Initializr build | |
| uses: actions/setup-java@v5 | |
| with: | |
| distribution: temurin | |
| java-version: '8' | |
| # Restored once for the whole job, and saved with whatever the later Java 25 steps | |
| # pulled as well -- both use the same ~/.m2. Without it every run of this workflow | |
| # re-fetched the world from Maven Central, which answered one of them with HTTP 429 | |
| # and failed the build on the pom rather than on anything in the change. Every other | |
| # Java workflow here already caches this way. | |
| cache: 'maven' | |
| - name: Set up Ruby | |
| uses: ruby/setup-ruby@v1 | |
| with: | |
| ruby-version: '3.3' | |
| - name: Set up Node.js | |
| uses: actions/setup-node@v6 | |
| with: | |
| node-version: '22' | |
| - name: Prepare Wrangler | |
| timeout-minutes: 6 | |
| run: | | |
| set -euo pipefail | |
| for attempt in 1 2 3; do | |
| if timeout 90s npx --yes "wrangler@${WRANGLER_VERSION}" --version; then | |
| exit 0 | |
| fi | |
| echo "::warning::Wrangler bootstrap attempt ${attempt} failed; retrying" | |
| done | |
| exit 1 | |
| - name: Install Asciidoctor tooling | |
| run: | | |
| set -euo pipefail | |
| gem install --no-document asciidoctor rouge | |
| - name: Download latest OTA skins | |
| run: | | |
| set -euo pipefail | |
| scripts/website/fetch_ota_skins.sh | |
| env: | |
| GITHUB_TOKEN: ${{ github.token }} | |
| - name: Check redirect rule ordering | |
| run: python3 scripts/website/test_redirect_order.py | |
| - name: Test developer guide chapter generation | |
| run: ruby scripts/website/test_generate_developer_guide.rb | |
| - name: Test developer guide PDF redirect resolver | |
| run: scripts/website/test_update_developer_guide_redirect.sh | |
| - name: Update developer guide PDF redirect | |
| run: | | |
| set -euo pipefail | |
| scripts/website/update_developer_guide_redirect.sh | |
| env: | |
| GITHUB_TOKEN: ${{ github.token }} | |
| # The Initializr/Playground/Skin Designer JavaScript bundles are built by | |
| # scripts/website/build.sh through xvfb-run: the Codename One Maven | |
| # plugin's CSS/theme compile instantiates JavaSEPort, which needs a | |
| # display on the headless runner. (xvfb formerly also served the Skin | |
| # Designer screenshot generation step, which has been removed -- the | |
| # screenshots are now committed under docs/developer-guide/img/skin-designer.) | |
| - name: Set up xvfb for headless JavaScript bundle builds | |
| run: | | |
| set -euo pipefail | |
| bash scripts/ci/apt-get-update.sh | |
| sudo apt-get install -y --no-install-recommends xvfb | |
| - name: Set up Java 25 for website build | |
| uses: actions/setup-java@v5 | |
| with: | |
| distribution: temurin | |
| java-version: '25' | |
| - name: Build website | |
| run: | | |
| set -euo pipefail | |
| scripts/website/build.sh | |
| env: | |
| WEBSITE_INCLUDE_JAVADOCS: "true" | |
| WEBSITE_INCLUDE_DEVGUIDE: "true" | |
| WEBSITE_INCLUDE_INITIALIZR: "true" | |
| WEBSITE_INCLUDE_PLAYGROUND: "true" | |
| # The Initializr's JavaScript app is built with the local ParparVM | |
| # target, whose builder lives in the repo (8.0-SNAPSHOT) plugin rather | |
| # than the pinned release. Bootstrap the local snapshot artifacts so | |
| # the initializr (and the other site apps) build against repo HEAD. | |
| WEBSITE_BOOTSTRAP_CN1_SNAPSHOTS: "true" | |
| # Resolve the latest persisted master result for each port before | |
| # Hugo writes the static status table. Checked-in reports remain the | |
| # fallback if the data branch is temporarily unavailable. | |
| WEBSITE_REFRESH_PORT_STATUS: "true" | |
| # PR previews build with future-dated posts visible so reviewers | |
| # can read posts staged for later in the week. Production deploys | |
| # (push to master) keep the default so future posts only appear | |
| # on their actual date. | |
| HUGO_BUILD_FUTURE: ${{ github.event_name == 'pull_request' && 'true' || 'false' }} | |
| - name: Validate API reference output | |
| run: | | |
| set -euo pipefail | |
| test -f docs/website/public/javadoc/index.html | |
| # Pages are published at the directory URL, because Cloudflare Pages | |
| # will not serve a .html one: see check-javadoc-urls.sh. | |
| test -f docs/website/public/javadoc/com/codename1/ui/Component/index.html | |
| test -f docs/website/public/javadoc/com/codename1/ui/package-summary/index.html | |
| test -f docs/website/public/javadoc-search.json | |
| # The fragment encoding is the compatibility surface, so assert one of | |
| # the awkward spellings rather than only that the page exists. This is | |
| # a canary for the parity gate rather than a duplicate of it: the gate | |
| # compares two trees, and a gate that ends up comparing nothing reports | |
| # nothing missing. | |
| # | |
| # Quotes are optional because the site is built with --minify and the | |
| # minifier drops them wherever HTML allows: this page really does carry | |
| # id=createImage(byte[],int,int) with no quotes at all. | |
| grep -Eq 'id="?createImage\(byte\[\],int,int\)"?' \ | |
| docs/website/public/javadoc/com/codename1/ui/Image/index.html | |
| # The old integration copied the standard doclet's tree into static/ | |
| # and scoped its stylesheet with a script. Nothing should reintroduce | |
| # either: they are what broke dark mode. | |
| test ! -e docs/website/static/javadoc | |
| test ! -e docs/website/assets/js/cn1-javadoc.js | |
| - name: Validate API URLs on the Pages runtime | |
| run: | | |
| set -euo pipefail | |
| # Asks the server rather than the filesystem. Everything else was green | |
| # on a build where clicking any class left the site: Pages redirects | |
| # /x.html to /x before an asset is considered, so the page published at | |
| # the extension was one nobody could reach. | |
| scripts/website/check-javadoc-urls.sh docs/website/public 8789 | |
| - name: Validate Port Status page output | |
| run: | | |
| set -euo pipefail | |
| node scripts/website/validate_port_status.mjs docs/website/public | |
| - name: Validate comparison page claims | |
| run: | | |
| set -euo pipefail | |
| node scripts/website/validate_compare_page.mjs docs/website/public | |
| - name: Validate Initializr page output | |
| run: | | |
| set -euo pipefail | |
| test -f docs/website/public/initializr/index.html | |
| - name: Validate Playground page output | |
| run: | | |
| set -euo pipefail | |
| test -f docs/website/public/playground/index.html | |
| - name: Validate Initializr JS bundle output | |
| run: | | |
| set -euo pipefail | |
| test -f docs/website/public/initializr-app/index.html | |
| grep -Fq 'downloadProject' docs/website/public/initializr-app/translated_app.js | |
| grep -Fq 'cn1-initializr-project-downloaded' \ | |
| docs/website/public/initializr-app/com_codename1_initializr_WebsiteThemeNative.js | |
| grep -Fq 'cn1-initializr-project-downloaded' docs/website/public/initializr/index.html | |
| node docs/website/scripts/test-initializr-download-bridge.mjs \ | |
| docs/website/public/initializr-app/com_codename1_initializr_WebsiteThemeNative.js | |
| - name: Validate website conversion telemetry | |
| run: | | |
| set -euo pipefail | |
| node docs/website/scripts/test-signup-funnel.mjs docs/website/public | |
| node docs/website/scripts/test-cn1-crisp-events.mjs | |
| - name: Validate Playground JS bundle output | |
| run: test -f docs/website/public/playground-app/index.html | |
| - name: Validate redirects against local Pages runtime | |
| run: | | |
| set -euo pipefail | |
| PORT=8788 | |
| cd docs/website | |
| npx --yes "wrangler@${WRANGLER_VERSION}" pages dev public \ | |
| --port "${PORT}" \ | |
| --ip 127.0.0.1 \ | |
| --compatibility-date=2026-02-18 > /tmp/wrangler-pages-dev.log 2>&1 & | |
| WRANGLER_PID=$! | |
| cleanup() { | |
| kill "${WRANGLER_PID}" >/dev/null 2>&1 || true | |
| } | |
| trap cleanup EXIT | |
| print_log_on_error() { | |
| echo "wrangler pages dev log (tail):" >&2 | |
| tail -n 200 /tmp/wrangler-pages-dev.log >&2 || true | |
| } | |
| trap print_log_on_error ERR | |
| for i in $(seq 1 60); do | |
| if curl -sSf "http://127.0.0.1:${PORT}/" >/dev/null; then | |
| break | |
| fi | |
| sleep 1 | |
| done | |
| if ! curl -sSf "http://127.0.0.1:${PORT}/" >/dev/null; then | |
| echo "wrangler pages dev did not become ready on port ${PORT}" >&2 | |
| exit 1 | |
| fi | |
| cd ../.. | |
| python3 scripts/website/test_redirects.py \ | |
| --base-url "http://127.0.0.1:${PORT}" \ | |
| --redirects-file docs/website/public/_redirects \ | |
| --skip-auto-cases | |
| - name: Validate internal links and images | |
| uses: lycheeverse/lychee-action@v2 | |
| with: | |
| # Pin to v0.23.0. lychee v0.24.0 renamed its release assets | |
| # from `lychee-<arch>-unknown-linux-gnu.tar.gz` to | |
| # `lychee-lychee-v0.24.0-<arch>-unknown-linux-gnu.tar.gz`, | |
| # and `lychee-action@v2` still constructs the old URL, so | |
| # `lycheeVersion: latest` 404s and breaks this job with | |
| # curl exit code 22. Bump once the action catches up. | |
| lycheeVersion: v0.23.0 | |
| workingDirectory: docs/website | |
| args: >- | |
| --offline | |
| --no-progress | |
| --root-dir public | |
| public/**/*.html | |
| # The developer guide links into this site, so a page renamed, deleted or | |
| # re-slugged here breaks the guide. The check lives in THIS workflow rather | |
| # than in developer-guide-docs.yml because it is the route data that | |
| # changed: widening that workflow's paths to docs/website/** would run the | |
| # full guide build -- maven install, demo build, screenshot generation -- | |
| # on every blog post the daily publisher pushes, to run one Python script. | |
| # This job already triggers on docs/website/**, and the check needs nothing | |
| # but a checkout. | |
| - name: Check developer guide links against the routes this site serves | |
| run: python3 scripts/developer-guide/check-guide-links.py | |
| - name: Reject absolute codenameone.com links | |
| run: | | |
| set -euo pipefail | |
| report="docs/website/reports/disallowed-codenameone-links.txt" | |
| mkdir -p "$(dirname "$report")" | |
| rg -n --no-heading -S '\]\((https?:)?//(www\.)?codenameone\.com([/:?#)]|$)|<https?://(www\.)?codenameone\.com([/:?#>]|$)|(href|src)=["'"'"']https?://(www\.)?codenameone\.com([/:?#]|$)' \ | |
| docs/website/content docs/website/layouts docs/website/static > "$report" || true | |
| if [ -s "$report" ]; then | |
| echo "Disallowed absolute codenameone.com links found (use relative URLs or non-www subdomains):" >&2 | |
| sed -n '1,200p' "$report" >&2 | |
| exit 1 | |
| fi | |
| - name: Upload codenameone link policy report | |
| if: ${{ always() }} | |
| uses: actions/upload-artifact@v7 | |
| with: | |
| name: codenameone-link-policy-report | |
| path: docs/website/reports/disallowed-codenameone-links.txt | |
| if-no-files-found: ignore | |
| - name: Validate OTA skin output | |
| run: | | |
| set -euo pipefail | |
| test -f docs/website/public/OTA/Skins.xml | |
| - name: Validate developer guide redirect output | |
| run: | | |
| set -euo pipefail | |
| test -f docs/website/public/_redirects | |
| test -f docs/website/public/developer-guide/index.html | |
| python3 scripts/website/check_developer_guide.py docs/website/public | |
| grep -Eq '^/files/developer-guide\.pdf https://github\.com/codenameone/CodenameOne/releases/download/.+/developer-guide\.pdf 302$' docs/website/public/_redirects | |
| grep -Eq '^/manual /developer-guide/ 301$' docs/website/public/_redirects | |
| grep -Eq '^/manual/ /developer-guide/ 301$' docs/website/public/_redirects | |
| grep -Eq '^/developer-guide /developer-guide/ 301$' docs/website/public/_redirects | |
| - name: Validate RSS output and alias | |
| run: | | |
| set -euo pipefail | |
| test -f docs/website/public/blog/index.xml | |
| test ! -f docs/website/public/index.xml | |
| grep -Eq '^/feed\.xml /blog/index\.xml 302$' docs/website/public/_redirects | |
| - name: Upload built site artifact | |
| uses: actions/upload-artifact@v7 | |
| with: | |
| name: website-preview | |
| path: docs/website/public | |
| if-no-files-found: error | |
| - name: Check Cloudflare preview deploy credentials | |
| if: ${{ github.event_name == 'pull_request' && env.CLOUDFLARE_TOKEN == '' }} | |
| run: | | |
| echo "::warning::Skipping Cloudflare Pages preview deploy because no API token secret is configured. Set CLOUDFLARE_TOKEN (preferred) or CF_API_TOKEN." | |
| - name: Deploy PR preview to Cloudflare Pages | |
| id: cf_preview | |
| if: ${{ github.event_name == 'pull_request' && env.CLOUDFLARE_TOKEN != '' }} | |
| env: | |
| CLOUDFLARE_API_TOKEN: ${{ env.CLOUDFLARE_TOKEN }} | |
| PREVIEW_BRANCH: pr-${{ github.event.pull_request.number }}-website-preview | |
| run: | | |
| set -uo pipefail | |
| # Stream wrangler output to the job log (via tee) while still | |
| # capturing it so we can pull the *.pages.dev preview URL out. The | |
| # previous `deploy_output=$(... 2>&1)` form hid every line — when | |
| # wrangler died without any stdout we had nothing to debug with. | |
| # -e is intentionally off for the wrangler invocation so we can | |
| # report its exit status explicitly instead of exiting opaquely. | |
| deploy_log="$(mktemp)" | |
| npx --yes "wrangler@${WRANGLER_VERSION}" pages deploy docs/website/public \ | |
| --project-name "${CF_PAGES_PROJECT_NAME}" \ | |
| --branch "${PREVIEW_BRANCH}" 2>&1 | tee "${deploy_log}" | |
| wrangler_status="${PIPESTATUS[0]}" | |
| if [ "${wrangler_status}" -ne 0 ]; then | |
| echo "wrangler pages deploy exited with status ${wrangler_status}" >&2 | |
| exit "${wrangler_status}" | |
| fi | |
| deployment_url="$(grep 'Deployment complete!' "${deploy_log}" | grep -Eo 'https://[A-Za-z0-9._-]+\.pages\.dev' | tail -n1 || true)" | |
| preview_url="$(grep 'Deployment alias URL:' "${deploy_log}" | grep -Eo 'https://[A-Za-z0-9._-]+\.pages\.dev' | tail -n1 || true)" | |
| if [ -z "${deployment_url}" ]; then | |
| echo "wrangler reported success but printed no immutable deployment URL." >&2 | |
| exit 1 | |
| fi | |
| if [ -z "${preview_url}" ]; then | |
| # Cloudflare does not always echo "Deployment alias URL", even though the branch | |
| # alias is created either way -- one deploy of this branch printed it and the next | |
| # did not. The upload, the alias and the deployment had all succeeded, so failing | |
| # here reported a working preview as a broken build. | |
| # | |
| # The alias is derived from the branch name, and PREVIEW_BRANCH is built from a PR | |
| # number so it is always lowercase, hyphenated and well inside the 28-character | |
| # limit Cloudflare truncates at -- no sanitising to replicate. Rebuild it, and only | |
| # accept it if it actually serves. | |
| candidate="https://${PREVIEW_BRANCH}.${CF_PAGES_PROJECT_NAME}.pages.dev" | |
| if curl -fsS -o /dev/null --max-time 30 "${candidate}"; then | |
| preview_url="${candidate}" | |
| echo "No alias line in the deploy output; recovered it from the branch name: ${preview_url}" | |
| else | |
| preview_url="${deployment_url}" | |
| echo "::warning::No alias line in the deploy output and ${candidate} did not respond; using the immutable deployment URL as the preview link." | |
| fi | |
| fi | |
| echo "deployment_url=${deployment_url}" >> "${GITHUB_OUTPUT}" | |
| echo "preview_url=${preview_url}" >> "${GITHUB_OUTPUT}" | |
| echo "preview_branch=${PREVIEW_BRANCH}" >> "${GITHUB_OUTPUT}" | |
| - name: Validate deployed comparison page | |
| if: ${{ steps.cf_preview.outputs.deployment_url != '' }} | |
| run: | | |
| set -euo pipefail | |
| deployed_compare="$(mktemp)" | |
| curl --fail --silent --show-error \ | |
| --retry 6 \ | |
| --retry-delay 2 \ | |
| --retry-all-errors \ | |
| "${{ steps.cf_preview.outputs.deployment_url }}/compare/" \ | |
| --output "${deployed_compare}" | |
| node scripts/website/validate_compare_page.mjs "${deployed_compare}" | |
| - name: Publish Cloudflare preview link in CI summary | |
| if: ${{ steps.cf_preview.outputs.preview_url != '' }} | |
| run: | | |
| { | |
| echo "## Cloudflare Preview" | |
| echo | |
| echo "- URL: ${{ steps.cf_preview.outputs.preview_url }}" | |
| echo "- Branch: \`${{ steps.cf_preview.outputs.preview_branch }}\`" | |
| } >> "${GITHUB_STEP_SUMMARY}" | |
| - name: Comment Cloudflare preview link on PR | |
| if: ${{ github.event_name == 'pull_request' && steps.cf_preview.outputs.preview_url != '' }} | |
| uses: actions/github-script@v9 | |
| with: | |
| script: | | |
| const marker = '<!-- cn1-cloudflare-preview -->'; | |
| const body = [ | |
| marker, | |
| '## Cloudflare Preview', | |
| '', | |
| `- URL: ${{ steps.cf_preview.outputs.preview_url }}`, | |
| `- Branch: \`${{ steps.cf_preview.outputs.preview_branch }}\``, | |
| ].join('\n'); | |
| const { owner, repo } = context.repo; | |
| const issue_number = context.issue.number; | |
| const comments = await github.paginate(github.rest.issues.listComments, { | |
| owner, | |
| repo, | |
| issue_number, | |
| per_page: 100, | |
| }); | |
| const existing = comments.find((comment) => | |
| comment.user?.type === 'Bot' && 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, | |
| }); | |
| } | |
| - name: Prune older Cloudflare preview deployments for this PR branch | |
| if: ${{ steps.cf_preview.outputs.preview_url != '' }} | |
| env: | |
| CLOUDFLARE_API_TOKEN: ${{ env.CLOUDFLARE_TOKEN }} | |
| PREVIEW_BRANCH: ${{ steps.cf_preview.outputs.preview_branch }} | |
| DEPLOYMENT_URL: ${{ steps.cf_preview.outputs.deployment_url }} | |
| run: | | |
| set -euo pipefail | |
| api_base="https://api.cloudflare.com/client/v4/accounts/${CLOUDFLARE_ACCOUNT_ID}/pages/projects/${CF_PAGES_PROJECT_NAME}/deployments" | |
| tmp_json="$(mktemp)" | |
| curl -sS \ | |
| -H "Authorization: Bearer ${CLOUDFLARE_API_TOKEN}" \ | |
| -H "Content-Type: application/json" \ | |
| "${api_base}" > "${tmp_json}" | |
| cat > /tmp/cf_preview_cleanup.py <<'PY' | |
| import json | |
| import sys | |
| path, preview_branch, deployment_url = sys.argv[1:] | |
| data = json.load(open(path, encoding="utf-8")) | |
| result = data.get("result", []) or [] | |
| def normalized(value): | |
| return (value or "").rstrip("/") | |
| def branch_of(dep): | |
| trig = dep.get("deployment_trigger") or {} | |
| meta = trig.get("metadata") or {} | |
| return meta.get("branch") | |
| current_id = None | |
| for dep in result: | |
| if normalized(dep.get("url")) == normalized(deployment_url): | |
| current_id = dep.get("id") | |
| break | |
| if not current_id: | |
| raise SystemExit( | |
| f"Could not identify current Cloudflare deployment for {deployment_url}; refusing to prune" | |
| ) | |
| for dep in result: | |
| dep_id = dep.get("id") | |
| if not dep_id or dep_id == current_id: | |
| continue | |
| if branch_of(dep) == preview_branch: | |
| print(dep_id) | |
| PY | |
| python3 /tmp/cf_preview_cleanup.py "${tmp_json}" "${PREVIEW_BRANCH}" "${DEPLOYMENT_URL}" > /tmp/cf_deployments_to_delete.txt | |
| while IFS= read -r dep_id; do | |
| [ -z "${dep_id}" ] && continue | |
| echo "Deleting old preview deployment: ${dep_id}" | |
| curl -sS -X DELETE \ | |
| -H "Authorization: Bearer ${CLOUDFLARE_API_TOKEN}" \ | |
| -H "Content-Type: application/json" \ | |
| "${api_base}/${dep_id}" >/dev/null | |
| done < /tmp/cf_deployments_to_delete.txt | |
| - name: Check Cloudflare deploy credentials | |
| if: ${{ ((github.event_name == 'push' && (github.ref_name == 'main' || github.ref_name == 'master')) || github.event_name == 'schedule' || (github.event_name == 'workflow_dispatch' && inputs.deploy_production && (github.ref_name == 'main' || github.ref_name == 'master'))) && env.CLOUDFLARE_TOKEN == '' }} | |
| run: | | |
| echo "::warning::Skipping Cloudflare Pages deploy because no API token secret is configured. Set CLOUDFLARE_TOKEN (preferred) or CF_API_TOKEN." | |
| - name: Deploy to Cloudflare Pages | |
| if: ${{ ((github.event_name == 'push' && (github.ref_name == 'main' || github.ref_name == 'master')) || github.event_name == 'schedule' || (github.event_name == 'workflow_dispatch' && inputs.deploy_production && (github.ref_name == 'main' || github.ref_name == 'master'))) && env.CLOUDFLARE_TOKEN != '' }} | |
| uses: cloudflare/wrangler-action@v3 | |
| env: | |
| # Keep these env vars explicit so Wrangler can authenticate in non-interactive CI. | |
| CLOUDFLARE_API_TOKEN: ${{ env.CLOUDFLARE_TOKEN }} | |
| CLOUDFLARE_ACCOUNT_ID: ${{ env.CLOUDFLARE_ACCOUNT_ID }} | |
| with: | |
| # Also pass through action inputs for compatibility with wrangler-action versions. | |
| apiToken: ${{ env.CLOUDFLARE_TOKEN }} | |
| accountId: ${{ env.CLOUDFLARE_ACCOUNT_ID }} | |
| command: >- | |
| pages deploy docs/website/public | |
| --project-name=${{ env.CF_PAGES_PROJECT_NAME }} | |
| --branch=${{ env.CF_PAGES_PRODUCTION_BRANCH }} | |
| # touched to retrigger Hugo workflow (path filter matches this file) |