Skip to content

Add Cloudflare Workers deployment method#2205

Open
rjocoleman wants to merge 1 commit intoGetPublii:masterfrom
rjocoleman:cloudflare-workers
Open

Add Cloudflare Workers deployment method#2205
rjocoleman wants to merge 1 commit intoGetPublii:masterfrom
rjocoleman:cloudflare-workers

Conversation

@rjocoleman
Copy link
Copy Markdown

@rjocoleman rjocoleman commented May 28, 2025

This PR adds Cloudflare Workers Static Assets as a new deployment method.
Is is a direct API integration, and could replace the to the existing Cloudflare Pages deployment method via Github.

Overview

This implementation uses Cloudflare Workers' static asset hosting capabilities to deploy sites directly to Cloudflare's edge network. It's strongly recommended by Cloudflare that new projects use this rather than CloudFlare Pages.

Key Features & Changes

Core Functionality:
• New CloudflareWorkers deployment class (cloudflare-workers.js) with full upload/deployment pipeline
• Direct API integration with Cloudflare Workers Static Assets API
• Automatic file hashing and deduplication for efficient uploads
• Batch file upload system using form-data multipart uploads
• Auto-generated worker script that serves static assets with proper routing

Configuration & UI:
• New deployment option in server settings with Cloudflare logo
• Three required configuration fields: Account ID, API Token, and Worker Name
• Secure credential storage via keytar
• Connection testing functionality to validate API access
• Form validation and error handling

Technical Implementation:
• Follows existing Publii deployment patterns and idioms
• Integrated into main deployment system (deployment.js)
• Support for 404s via the existing 404.html in worker configuration
• Auto-trailing-slash HTML handling for clean URLs
• Progress tracking and status reporting during deployment

Setup Requirements:
• Cloudflare account with Workers plan (free is fine)
• API token with Workers edit permissions
• Account ID from Cloudflare dashboard
• Worker name (can be created via one of the Cloudflare onboarding starters, gets overwritten)
• Optional: Custom domain can be later added via Cloudflare Workers Web UI

Refs: #2049 #996 #1174

Copy link
Copy Markdown

@YasharF YasharF left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  • What testing and verification have you done for this?

  • Can you add the proper required API permissions to UI/docs so users create a token with the proper scopes?

  • I think uploadFiles() and deployWorker() build a FormData body but do not merge form.getHeaders() into the fetch request headers. This will omit the required Content-Type: multipart/form-data; boundary=... and break uploads/deploys. Fix: set headers to { ...form.getHeaders(), Authorization: 'Bearer ...' }.

  • If fetch(...).json() fails because response isn't JSON (e.g., upload failed due to missing multipart headers), the error will surface but logs may be unclear. Consider logging response.status and raw text on non-OK responses for easier debugging.

validateCloudflareWorkers () {
let fields = ['cloudflareWorkers_accountId', 'cloudflareWorkers_apiToken', 'cloudflareWorkers_scriptName'];
return this.validateFields(fields);
},
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cloudflare Workers validation errors won’t map to the fields in the template. validateFields() will push error keys like cloudflareWorkers-accountId (underscore replaced with hyphen), but the inputs are keyed off cf-account-id / cf-api-token / cf-script-name, so the invalid styling and cleanError() won’t work and users won’t see which fields are failing validation. Align the input ids / error checks with the keys generated by validateFields(), or handle these fields with custom error keys instead of validateFields().

Suggested change
},
// Custom validation for Cloudflare Workers to ensure error keys
// match the template input IDs: cf-account-id, cf-api-token, cf-script-name
const workers = (this.deploymentSettings && this.deploymentSettings.cloudflareWorkers) || {};
// Clear any previous errors for these specific fields
if (typeof this.cleanError === 'function') {
this.cleanError('cf-account-id');
this.cleanError('cf-api-token');
this.cleanError('cf-script-name');
}
// Ensure there is an errors object to store field-level errors
if (!this.errors) {
this.$set(this, 'errors', {});
}
let isValid = true;
if (!workers.accountId) {
this.$set(this.errors, 'cf-account-id', true);
isValid = false;
}
if (!workers.apiToken) {
this.$set(this.errors, 'cf-api-token', true);
isValid = false;
}
if (!workers.scriptName) {
this.$set(this.errors, 'cf-script-name', true);
isValid = false;
}
return isValid;

const crypto = require('crypto');
const mime = require('mime');
const FormData = require('form-data');
const fetch = require('node-fetch');
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

'node-fetch' as a npm module is no longer maintained. Remove this line and use the built-in fetch().

]
};

console.log(`[${new Date().toUTCString()}] Cloudflare Workers metadata:`, JSON.stringify(metadata, null, 2));
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This console.log prints the full deployment metadata including the assets upload JWT (metadata.assets.jwt). That token can be used to authorize asset operations and shouldn’t be written to logs. I would remove this log line or use conditional debug prints while redacting sensitive fields before logging (and ideally gate behind an explicit debug flag).

Suggested change
console.log(`[${new Date().toUTCString()}] Cloudflare Workers metadata:`, JSON.stringify(metadata, null, 2));
if (process.env.CF_WORKERS_DEBUG === 'true') {
const safeMetadata = {
...metadata,
assets: {
...metadata.assets,
jwt: '[REDACTED]'
}
};
console.log(
`[${new Date().toUTCString()}] Cloudflare Workers metadata:`,
JSON.stringify(safeMetadata, null, 2)
);
}

}
}
return null;
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

findFileByHash() does an O(n) scan over fileMetadata for every hash in every bucket, which can easily become O(n^2) for larger sites. Build a reverse index (e.g., hash -> relativePath) once when gathering metadata so uploads can locate files in O(1).

this.deployment.operationsCounter = files.length + 3; // +3 for upload session, worker deploy, and completion
this.deployment.progressPerFile = 90.0 / this.deployment.operationsCounter;
this.deployment.currentOperationNumber = 0;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

operationsCounter is set based on the number of files, but updateProgress() is only called once for the upload session, once per bucket upload, and once for worker deploy (not once per file). This makes the (current/total) operations display and progress step calculation inaccurate. Make operationsCounter match the number of progress updates you actually emit, or call updateProgress() per uploaded file instead of per bucket.

        // Track progress based on high-level operations (upload session, worker deploy, completion)
        this.deployment.operationsCounter = 3;

config.settings.deployment.cloudflareWorkers &&
config.settings.deployment.cloudflareWorkers.accountId !== '' &&
config.settings.deployment.cloudflareWorkers.apiToken !== '' &&
config.settings.deployment.cloudflareWorkers.accountId !== 'publii-cf-account-id ' + siteID
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When deciding whether to store Cloudflare Workers credentials in the keychain, the condition checks whether accountId is already the placeholder value, but it doesn’t check the same for apiToken. For consistency with other credential blocks (and to avoid trying to re-store placeholder values), add a placeholder check for cloudflareWorkers.apiToken as well.

Suggested change
config.settings.deployment.cloudflareWorkers.accountId !== 'publii-cf-account-id ' + siteID
config.settings.deployment.cloudflareWorkers.accountId !== 'publii-cf-account-id ' + siteID &&
config.settings.deployment.cloudflareWorkers.apiToken !== 'publii-cf-api-token ' + siteID

const path = require('path');
const crypto = require('crypto');
const mime = require('mime');
const FormData = require('form-data');
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe I missed it, but I didn't see form-data as a direct dependency in package.json

@rjocoleman
Copy link
Copy Markdown
Author

  • What testing and verification have you done for this?

It works for me - uploads/deploys. I won't pretend to have encountered every edge case but it's dependable and without visible errors in my workflow.

  • Can you add the proper required API permissions to UI/docs so users create a token with the proper scopes?
  • I think uploadFiles() and deployWorker() build a FormData body but do not merge form.getHeaders() into the fetch request headers. This will omit the required Content-Type: multipart/form-data; boundary=... and break uploads/deploys. Fix: set headers to { ...form.getHeaders(), Authorization: 'Bearer ...' }.
  • If fetch(...).json() fails because response isn't JSON (e.g., upload failed due to missing multipart headers), the error will surface but logs may be unclear. Consider logging response.status and raw text on non-OK responses for easier debugging.

Thanks - I will look at the balance of this feedback if I get a chance.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants