Skip to content

Deployment setup

Deploy both sites to Cloudflare

English is authoritative; 中文操作教程 . This walkthrough uses the repository’s existing GitHub Actions → Cloudflare Workers Static Assets pipeline. Token and workflow-rerun references were checked on 2026-09-18. Account configuration and live deployment must still be verified when following the steps.

The order is: prepare accounts and settings → publish through GitHub → inspect the workers.dev URLs → connect custom domains → check the live sites. Keep the existing host available until cutover succeeds.

1. Prepare accounts and choose names

You need repository admin access on GitHub, permission to deploy Workers and manage the domain in the intended Cloudflare account, and access to the domain registrar if nameservers must change. The GitHub repository must already contain an approved main and .github/workflows/ci.yml. If it has no main, establish the repository baseline with the owner before proceeding; do not bypass the PR policy with an improvised main push.

Choose and record these values. The following are worked examples, not verified live assignments. Replace them consistently if you use different names or domains.

PurposeExampleUsed by
Portfolio Workerdonnyguo-portfolioGitHub PORTFOLIO_WORKER_NAME
Log Workerdonnyguo-logsGitHub LOG_WORKER_NAME
Portfolio originhttps://donnyguo.comGitHub PORTFOLIO_SITE_URL
Log originhttps://logs.donnyguo.comGitHub LOG_SITE_URL
Optional portfolio aliaswww.donnyguo.comA second Custom Domain on the portfolio Worker

On a narrow screen, swipe wide tables horizontally to see all columns.

Use two distinct Worker names, 1–63 lowercase letters/digits/hyphens, with a letter or digit at each end. Use HTTPS origins without a path, query or fragment for site URLs. The production script rejects localhost and reserved example domains such as example.com.

The portfolio builds into dist/portfolio/; Hugo builds the engineering log into dist/logs/. Both have English at / and Chinese at /zh/. They are separate Workers, not subdirectories of one deployment. The log site publishes the repository guides and change records. Review those public contents before uploading.

This guide uses Workers Static Assets, as configured in wrangler.portfolio.json and wrangler.logs.json. Do not create a Pages project or connect a second Git-based Cloudflare build for the same release. See Static Assets .

Ready when: you have the required access, an existing main workflow, and one consistent set of names and URLs.

2. Check the release locally

Local verification catches build/content failures before uploading. It generates the same kind of static files that CI will publish; a successful local run does not change the live site.

Work from the repository directory containing package.json. Use the development guide to select the pinned Node.js, pnpm, Go and Hugo versions; use a feature worktree for any edits. This step needs no Cloudflare token.

node --version
pnpm --version
go version
pnpm install --frozen-lockfile
pnpm tools:hugo
pnpm exec playwright install chromium

Expected versions are Node.js 26.8.2, pnpm 12.4.1, Go 1.27.1 and Hugo Extended 0.166.0. On Linux, install Chromium with pnpm exec playwright install --with-deps chromium.

Set the intended build URLs in this terminal, replacing the examples, then verify:

export PORTFOLIO_SITE_URL='https://donnyguo.com'
export LOG_SITE_URL='https://logs.donnyguo.com'
pnpm verify
pnpm deploy:dry-run

pnpm verify must pass type/content tests, both builds, output checks and browser tests. Both dry runs must exit successfully. The output must include dist/portfolio/index.html and dist/logs/index.html. Dry run does not upload or test credentials, DNS or HTTPS. Local exports or .env values do not configure GitHub; step 6 does that separately. For local servers and their stop commands, follow the development guide.

Ready when: verification and both dry runs succeed with the intended origins. Keep these results separate from later live checks.

3. Keep deployment off during setup

In the GitHub repository, open Settings → Actions → General and confirm Actions is enabled. Then:

  1. Open Settings → Secrets and variables → Actions → Variables.
  2. Select New repository variable. Enter DEPLOY_ENABLED in Name and false in Value, then select Add variable. If it exists, use its edit button and save false.
  3. Leave it at false until the credentials and variables in steps 5–6 are ready. Checks can still run while deployment is off.

Use values without surrounding quotes. Changing this variable does not start a workflow. For code or document changes, follow the PR workflow : work on a feature branch, open a PR, and merge only after its latest CI / checks passes.

Ready when: Actions is enabled and repository variable DEPLOY_ENABLED is false.

4. Prepare the Cloudflare account and DNS

Account and temporary URLs

Select the intended account in Cloudflare and open Workers & Pages. Complete any initial Workers setup and ensure the account has a workers.dev subdomain. Note the account subdomain shown in the overview; if it is already configured, retain it. Each Worker will have a URL shaped like https://WORKER_NAME.ACCOUNT_SUBDOMAIN.workers.dev. These URLs are public. See workers.dev setup .

You do not need to pre-create empty Workers: the first CI upload uses the configured names and creates them. If names already exist, verify those Workers belong to these sites before deploying, because the upload updates them. Keep custom domains on the existing host until the new assets have passed step 7.

Domain zone

Nameservers determine who answers DNS lookups for the domain. Pointing them to Cloudflare makes Cloudflare responsible for those answers; it does not yet select the new website. Step 8 attaches each hostname to its Worker. That is why DNS onboarding and website cutover are separate steps.

If the domain is already Active in this Cloudflare account, retain its nameservers and skip the transfer steps below. Otherwise:

  1. Save/export the current DNS records and record the old nameservers and hosting target privately. Include existing blog subdomains and MX/TXT email records.
  2. Add the domain to Cloudflare from the account’s domain onboarding flow. Review/import the DNS records; automatic scanning can miss records. Preserve the old hosting targets at this stage.
  3. If DNSSEC is active at the registrar, follow the official migration instructions before changing nameservers. For a normal full setup, disable the old DNSSEC delegation first, then enable Cloudflare DNSSEC after activation.
  4. At the registrar, replace the authoritative nameservers with the exact pair assigned by Cloudflare. Wait for the zone to become Active. Check dig +short NS donnyguo.com using your actual domain; results should match the assigned nameservers after propagation.

This changes DNS hosting; it does not require transferring the domain registration. Preserve mail and other services throughout. See Cloudflare domain onboarding . If account ownership or a conflicting hostname is unclear, resolve it before changing live DNS.

Ready when: the account’s workers.dev subdomain exists, the domain is Active in the same account, and you have a private copy of the old routing/DNS settings.

5. Save the Cloudflare credentials in GitHub

Create or check the token

  1. In Cloudflare, select the target account and copy its Account ID from Workers & Pages → Account Details. Use Account ID, not Zone ID; see where to find it .
  2. Open Manage Account → Account API Tokens → Create Token. Creating an account token requires an account Super Administrator. Under Permission policies, select Custom → Edit Cloudflare Workers and name it github-static-sites.
  3. Scope the token to the deployment account and the intended zone if the template asks for zone resources. Keep the template’s Worker permissions; check that Account → Workers Scripts → Edit and Account → Account Settings → Read are present. A token that only grants Pages or DNS access is insufficient.
  4. Select Continue to summary → Create Token, then copy the token directly into the GitHub secret below. Keep it out of source files, ordinary variables, logs and screenshots.

If the policy editor shows Specified Domains → donnyguo.com, that policy grants zone permissions only. Select Add policy and set the new policy’s resource scope to the deployment account. Use Search for permission groups to find the two account permissions below; select only the indicated access level, then save the policy. They belong in the account policy, not the domain policy. Select all is unnecessary.

Search forAccessPurpose
Workers ScriptsEdit (called Write in the API permission list)Upload and deploy Workers
Account SettingsReadRead account settings

This follows Cloudflare’s CI authentication instructions and account-token setup . An existing valid deployment token can be reused.

If you use a personal token from My Profile → API Tokens, use the Edit Cloudflare Workers template there. In addition to its account permissions, retain User → User Details → Read and User → Memberships → Read for Wrangler’s user/account diagnostics. Those user permissions apply to personal tokens; do not look for them in an account-token form. See the personal-token template permissions . For the Unable to get membership roles message, follow step 11 before changing permissions.

Add or update the two secrets

In GitHub, open Settings → Secrets and variables → Actions → Secrets. For each row, select New repository secret, fill Name and Secret, and select Add secret.

NameSecret
CLOUDFLARE_API_TOKENThe Cloudflare API token value
CLOUDFLARE_ACCOUNT_IDThe Account ID of that same deployment account

If a secret already exists, use its edit button, paste the current value and select Update secret. GitHub does not reveal the saved value. After replacing a token, verify a successful upload before revoking the old token, and first check whether other integrations use it. See GitHub repository secrets .

Ready when: both names appear under Repository secrets, and the token’s account scope matches the saved Account ID. Actual permission validation happens during upload. If an update has no effect, check the secret precedence in step 11.

6. Set repository variables

Go to Settings → Secrets and variables → Actions → Variables → New repository variable. Add/save one entry per row; edit the existing deployment switch rather than creating another name.

NameExample valueMeaning
PORTFOLIO_WORKER_NAMEdonnyguo-portfolioWorker receiving dist/portfolio/
LOG_WORKER_NAMEdonnyguo-logsWorker receiving dist/logs/
PORTFOLIO_SITE_URLhttps://donnyguo.comPortfolio canonical build URL
LOG_SITE_URLhttps://logs.donnyguo.comHugo production base URL
DEPLOY_ENABLEDfalse for nowOnly the exact string true enables deployment

Enter raw values without surrounding quotes. Save all five as repository variables so both the build and deploy jobs can read them. See GitHub configuration variables .

The worker-name variables override the sample names in the Wrangler files. The URL variables affect built HTML and Hugo links; they do not create DNS records or bind domains. Changing an origin later requires a new build, followed by separate domain configuration.

Ready when: five repository variables match your chosen values and both repository secrets exist.

7. Start the deployment and confirm both uploads

After steps 5–6, publish the approved contents of current main using the steps below. Both sites, including the engineering log, will become public.

Enable deployment

  1. Open Settings → Secrets and variables → Actions → Variables.
  2. Find DEPLOY_ENABLED, select its edit button, change Value to true, and select Save variable.

Find and rerun the current main workflow

  1. Open the repository’s Code tab, select branch main, and open its latest commit. Note the commit SHA.
  2. Open Actions, select CI in the left sidebar, and filter Branch → main and Event → push. Open the run whose commit SHA matches the one from Code. A PR run cannot publish.
  3. If that run is still running, wait for it to finish. For a completed run, select Re-run all jobs at the top right. If it has failed, first open Re-run jobs → Re-run all jobs. Confirm with Re-run jobs in the dialog. This rebuilds and checks both sites before uploading. See GitHub’s rerun controls .
  4. If no matching push run is available to rerun, open Pull requests, select the approved feature PR, confirm its latest Checks → CI / checks passed, then select Merge pull request and confirm the merge. Return to Actions → CI and open the new main / push run. If there is no approved PR, prepare one using the workflow guide ; do not push directly to main just to trigger deployment.

Changing the switch or a secret alone starts nothing. There is no Run workflow button because this workflow has no manual trigger. A rerun keeps its original commit: if main has advanced, select the new main run instead.

Read the deployment result

  1. In the run’s left sidebar under Jobs, open checks. Wait for it to pass; its Upload checked static sites step saves the tested static-sites artifact.
  2. Return to the run summary, open deploy, then expand Publish checked main artifacts. The job downloads the checked artifact before this step; the step uploads portfolio first, logs second.
  3. Confirm that the log ends with Published checked revision to: portfolio, logs. A green checks job, a skipped deploy, or Skipped: this revision is no longer main. does not confirm publication. For a red upload step, use step 11.
  4. In Cloudflare, open Workers & Pages → donnyguo-portfolio → Settings → Domains & Routes and open the listed workers.dev URL. Repeat for donnyguo-logs, replacing names with your step 6 values. If workers.dev is disabled, enable it there before checking.
  5. On the portfolio’s workers.dev host, visit /, /zh/ and /projects/cloud-storage/. On the log host, visit /, /zh/ and /docs/deployment/. Keep the workers.dev host when entering these paths: links built with the final domain are checked after step 8.

Ready when: the current main run reports both uploads complete and both workers.dev sites show the expected pages. Continue to step 8 to attach the final domains.

8. Connect the custom domains

This is the traffic cutover. Verify the old host/DNS backup from step 4 before replacing any record. In Cloudflare, open Workers & Pages → portfolio Worker → Settings → Domains & Routes → Add → Custom Domain. Enter the hostname only, such as donnyguo.com, and confirm. Repeat on the log Worker for logs.donnyguo.com.

Cloudflare creates the routing DNS record and HTTPS certificate for each Custom Domain. Wait until the domain/certificate is ready, then open its HTTPS URL. Do not enter https://, a path or /* in the domain field. An existing CNAME on the hostname prevents creation; after confirming the old target and backup, remove that conflicting record and immediately add the Custom Domain. Review any other conflict/replacement prompt against the saved target. Do not remove unrelated email or blog records. See Custom Domains .

If you want www.donnyguo.com to serve the portfolio too, repeat the same operation on the portfolio Worker for that hostname. This creates a second working hostname, not an automatic redirect. Choose one canonical origin in PORTFOLIO_SITE_URL; a www-to-root redirect requires a separate redirect rule if desired.

For this setup, manage domain attachments in the dashboard. The current Wrangler files contain no routes; do not add a partial route list casually in a later change, because Wrangler-managed routes can override dashboard settings. Treat any move to configuration-managed domains as an explicit change and recheck both domains after every release. See Wrangler routing configuration .

Ready when: both final HTTPS hostnames serve their correct sites with valid certificates; the optional www hostname also works if configured.

9. Verify the public result

Use your final origins in these read-only checks:

curl -sS -o /dev/null -w '%{http_code}\n' https://donnyguo.com/
curl -sS -o /dev/null -w '%{http_code}\n' https://donnyguo.com/zh/
curl -sS -o /dev/null -w '%{http_code}\n' https://logs.donnyguo.com/
curl -sS -o /dev/null -w '%{http_code}\n' https://logs.donnyguo.com/zh/
curl -sS -o /dev/null -w '%{http_code}\n' https://donnyguo.com/not-a-page/
curl -sS -o /dev/null -w '%{http_code}\n' https://logs.donnyguo.com/not-a-page/

Expected: the first four print 200; the last two print 404. A TLS error or status 000 is a connection failure. Inspect unexpected redirects with curl -I URL before accepting them; a branded error page returning 200 is not a passing 404 check.

In a browser, check portfolio Projects, a project detail, Experience, EN/ZH switching, images and mobile width. On the log site, open a guide and change article in each language, follow internal links, and search for a term visible on those pages. Confirm the intended canonical hostname in the HTML and that existing blog/email services still work after DNS migration. Check that draft/test content, raw resumes and private acceptance files are absent from published assets.

Record the actual main SHA, workflow run, public URLs and results in the relevant bilingual change log. Keep tokens, account identifiers, DNS backups and private screenshots out of public records. Mark unperformed checks as unverified.

10. Later releases, retries and rollback

For normal releases, merge a passing PR; the main workflow rebuilds and publishes automatically. The production lock serializes deployments. Before upload, the script checks main’s current SHA; an old run prints Skipped: this revision is no longer main. and uploads nothing. Success status with that message is not a new release.

For a credential or transient upload failure, fix the setting, confirm that the failed run still matches current main, then use Actions → CI → that run → Re-run jobs → Re-run failed jobs. The artifact is retained for 7 days. If it is missing, or build URLs changed, use Re-run all jobs to rebuild and recheck current main; if the run is no longer eligible for rerun, merge an authorized corrective PR. Never rerun a PR event expecting deployment. GitHub retains the original event/ref/SHA for reruns; see rerun instructions .

If main has advanced, use its current run. The two uploads are not atomic: Release failed at logs. Already published: portfolio. means the main site changed while logs did not. Resolve the failure and rerun current main; expect both sites to be uploaded again.

To pause future automatic releases, set DEPLOY_ENABLED=false. This does not unpublish the current sites, cancel an already-started upload or restore an earlier version. Check running/queued jobs before attempting recovery.

For a bad content/code release:

  1. Identify the last known-good source and the offending change from the main history and CI runs.
  2. Create a rollback worktree/branch from current main using the workflow guide. For one ordinary offending commit, git revert --no-commit BAD_COMMIT_SHA prepares a reversible diff; replace the placeholder. A merge commit needs a deliberately selected mainline parent (typically -m 1), and multiple dependent changes need a reviewed rollback scope.
  3. Inspect the result, run pnpm verify and pnpm deploy:dry-run, then follow the required staged-diff confirmation and PR process. Do not use destructive resets or an old workflow rerun as rollback.
  4. Re-enable deployment if paused before merging the approved revert PR. Inspect its current-main deployment and repeat step 9 for both sites.

For a domain cutover failure, preserve the new Workers while restoring the previous host: remove the affected Custom Domain attachment, restore that hostname’s saved DNS record(s) in Cloudflare, and verify the old HTTPS site and email/blog behavior. The old host must still be available. Keep working Cloudflare nameservers in place unless a separately planned DNS-provider rollback is necessary; nameserver/DNSSEC changes have their own propagation and validation requirements. Previously downloaded public content cannot be recalled by reverting or changing DNS.

11. Diagnose a failed deployment

Authentication error 10000 on /accounts/…/workers/subdomain

This request reads the account’s workers.dev subdomain. The endpoint accepts Workers Scripts Read or Write . If the token summary already lists Workers Scripts Write, adding more permission names is not the next step: verify that CI is using that token for the correct account.

  1. In the target Cloudflare account, copy Account ID again and update GitHub’s CLOUDFLARE_ACCOUNT_ID secret. Do not use Zone ID or the token’s ID.
  2. Confirm that CLOUDFLARE_API_TOKEN contains the full value of the token whose permissions you checked, without quotes or a Bearer prefix. Its resource scope must include the same account. If you cannot recover its value, create a replacement with the same intended scope and update the secret; retain the old token until validation succeeds.
  3. Check which secrets the job actually uses. If the deploy job declares an environment, open Settings → Environments → that environment → Environment secrets and check for the same two names. Same-named environment secrets override repository secrets, so update the effective values there too if present. See GitHub secret precedence . If no environment secrets exist, use the repository values from step 5.
  4. Rerun current main as in step 7. If the same error remains, check the token’s status, resource scope and any restrictions against the target account; retain the original API operation and error code for further diagnosis. A successful upload is the validation, not the permission summary alone.

Error 10000 establishes an authentication failure for this request; it does not by itself distinguish a wrong Account ID, wrong saved token or token scope/restriction. It is not evidence of a DNS fault or a missing workers.dev registration.

Unable to get membership roles

This is a later account-membership diagnostic. With a personal token, missing User → Memberships → Read can cause it; it can also appear while diagnosing an account token. The subdomain endpoint above does not require that user permission. Wrangler 4.131.1 prints authentication errors before running these diagnostics; see its error handler and membership diagnostic .

In deploy → Publish checked main artifacts, scroll up to the first Cloudflare ERROR above the membership message. Diagnose that operation; redact account identifiers when sharing an excerpt. scripts/release.ts:12 is the repository’s upload-failure wrapper. Its Release failed at … Already published: … message identifies the failed site and completed uploads, not the Cloudflare cause.

Other common failures

SymptomNext action
No run after editing a settingUse step 7 to rerun current main. A variable or secret edit triggers nothing.
deploy is skippedCheck the run is main / push, checks passed, and repository variable DEPLOY_ENABLED is exactly true.
Missing deployment setting: ...Match the exact secret/variable name and location in steps 5–6.
Invalid Worker name / invalid HTTPS originUse step 1’s format and remove quotes, spaces or example hostnames.
workers.dev setup errorComplete account subdomain setup in Workers & Pages, then rerun current main.
Unable to verify main before upload / old revision skippedRestore GitHub API access or select current main as appropriate; retain the SHA guard.
static-sites artifact missing / Hugo links use an old hostnameConfirm URL variables, then Re-run all jobs for current main to rebuild.
Only one site changedRead Already published and follow the partial-release recovery in step 10.
Custom Domain conflict, DNS error or certificate pendingCheck step 8’s attachment, active zone, saved DNS targets and certificate status; compare with workers.dev.
Local tool or Chromium errorFollow the pinned setup and troubleshooting in the development guide .