Skip to content

New deploy service#244

Closed
purplecabbage wants to merge 25 commits into
masterfrom
NewDeployService
Closed

New deploy service#244
purplecabbage wants to merge 25 commits into
masterfrom
NewDeployService

Conversation

@purplecabbage
Copy link
Copy Markdown
Member

@purplecabbage purplecabbage commented May 19, 2026

Summary

Migrates static web deploy and undeploy from direct AWS S3 access (TVM / BYO credentials) to the App Builder deploy-service CDN API, using the user’s bearer token instead of short-lived S3 keys.

Storage layer (RemoteStorage)

  • Constructor now takes an Authorization header value instead of AWS credential objects.
  • List / delete / upload call deploy-service REST endpoints under /cdn-api/namespaces/{namespace}/files via fetch, with the service base URL chosen from CLI environment (@adobe/aio-lib-env: prod vs stage).
  • Upload payload matches the CDN API shape: JSON body with base64 file content, contentType, cacheControl, customHeaders, and a relative file.name (namespace is applied server-side).
  • Cache control and response headers behavior is preserved; adp-cache-control can still override manifest cache settings.
  • Batch uploads still walk the dist directory with klaw and upload in batches of 50.

Deploy / undeploy entry points

  • deploy-web: Requires config.ow.auth_handler.getAuthHeader(); validates hostname and namespace (required + [a-zA-Z0-9.-]+); clears any existing namespace deployment before upload; returns the same https://{namespace}.{hostname}/index.html URL.
  • undeploy-web: Same auth requirement; checks that files exist, deletes via deploy-service, and fails clearly if the delete request does not succeed.

Removed / dependencies

  • Removes lib/getS3Creds.js and its tests (no more TVM S3 credential fetch or BYO config.s3.creds path in this library).
  • Drops @aws-sdk/client-s3; adds @adobe/aio-lib-env.
  • Bumps version to 7.2.0 and minimum Node to >= 20.

Tests

  • remote-storage, deploy-web, and undeploy-web tests rewritten around mocked fetch and the new API contract.
  • Adds coverage for auth failures, deploy-service error paths, hostname/namespace validation, and “clear before deploy” behavior.
  • Jest setup gains shared fakeAppConfig / namespace helpers for the new config shape.

Motivation

App Builder static hosting is moving behind deploy-service so clients no longer need S3 credentials or direct bucket access. This aligns aio-lib-web with that model and simplifies credential handling to a single OAuth/bearer flow the CLI already provides.

Breaking changes

  • Callers must supply config.ow.auth_handler.getAuthHeader(); deploy/undeploy will fail without it.
  • RemoteStorage API changed: constructor signature, and folderExists / emptyFolder now require appConfig (namespace-driven) rather than S3 prefix + bucket creds.
  • BYO S3 credentials via getS3Creds are no longer supported in this package.

  • Bug fix (non-breaking change which fixes an issue)
  • New feature (non-breaking change which adds functionality)
  • Breaking change (fix or feature that would cause existing functionality to change)

Checklist:

  • I have signed the Adobe Open Source CLA.
  • My code follows the code style of this project.
  • My change requires a change to the documentation.
  • I have updated the documentation accordingly.
  • I have read the CONTRIBUTING document.
  • I have added tests to cover my changes.
  • All new and existing tests passed.

Copy link
Copy Markdown

@github-actions github-actions Bot left a comment

Choose a reason for hiding this comment

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

🤖 PR Reviewer

This diff migrates the web deployment infrastructure from AWS S3 (via TVM credentials) to a new deploy-service REST API using bearer token authentication. The code is generally well-structured with good error handling, input validation, and comprehensive test coverage. A few issues remain around hardcoded URLs, leftover commented-out code, package.json inconsistency, and a minor security concern with base64 content encoding.

📝 8 suggestion(s) - Please review inline comments below.

⚠️ Diff exceeded 100 000 chars and was truncated — some files may not have been reviewed.


💡 How to re-trigger

Comment /review or /pr-reviewer on this PR

Comment thread lib/remote-storage.js
Comment thread lib/remote-storage.js Outdated
Comment thread lib/remote-storage.js
Comment thread lib/remote-storage.js Outdated
Comment thread package.json Outdated
Comment thread src/deploy-web.js Outdated
Comment thread src/deploy-web.js Outdated
Comment thread lib/remote-storage.js Outdated
@codecov
Copy link
Copy Markdown

codecov Bot commented May 19, 2026

Codecov Report

✅ All modified and coverable lines are covered by tests.

📢 Thoughts on this report? Let us know!

Copy link
Copy Markdown

@github-actions github-actions Bot left a comment

Choose a reason for hiding this comment

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

🤖 PR Reviewer

The diff represents a significant architectural change from AWS S3/TVM-based storage to a custom deployment service API using bearer token auth. Most previous suggestions have been partially addressed (e.g., AIO_DEPLOYMENT_SERVICE_URL env var is now supported, proxy support is restored via undici). However, several issues remain: commented-out code is still present, the warning for overwriting deployments was removed without re-adding it, and the namespace validation regex still uses the same pattern for both fields despite different valid character sets.

📝 6 suggestion(s) (1 new, 5 re-raised)

⚠️ Diff exceeded 100 000 chars and was truncated — some files may not have been reviewed.


💡 How to re-trigger

Comment /review or /pr-reviewer on this PR

Comment thread lib/remote-storage.js Outdated
Comment thread lib/remote-storage.js
let relativeDir = path.dirname(path.relative(dir, file))
// path.relative returns '.' for files in the root directory, normalize to empty string
relativeDir = relativeDir === '.' ? '' : relativeDir

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

[Re-raised] Commented-out sleep/rate-limiting code should be removed or replaced with a real implementation if rate limiting is a concern.

Suggested change
const res = await Promise.all(fileBatch.map(async file => {

Comment thread lib/remote-storage.js
if (Object.keys(responseHeaders).length > 0) {
uploadParams.Metadata = responseHeaders
// server expected body is: { contentType, cacheControl, customHeaders: {}, file: { name, content } }
// file.name is the path relative to namespace (e.g., 'images/photo.jpg' or 'index.html')
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

[Re-raised] File content is sent as base64-encoded string in a JSON body. For large static assets this doubles memory usage (Buffer + base64 string) and increases payload size by ~33%. Consider using multipart/form-data or a streaming upload approach for files above a certain size threshold.

Comment thread src/deploy-web.js Outdated
Comment thread src/deploy-web.js
Comment thread package.json Outdated
@github-actions github-actions Bot dismissed their stale review May 20, 2026 01:09

Superseded by new review

Copy link
Copy Markdown

@github-actions github-actions Bot left a comment

Choose a reason for hiding this comment

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

🤖 PR Reviewer

The diff represents a significant architectural change from AWS S3/TVM-based storage to a custom CDN deploy-service API using bearer token auth. Most previous suggestions have been addressed (namespace regex now uses /^[a-zA-Z0-9_-]+$/, warning log is re-added, separate hostname/namespace validation). However, several previously flagged issues remain: commented-out code in remote-storage.js, the commented-out sleep/rate-limiting code, the @adobe/aio-lib-core-tvm unused dependency, and the base64 upload concern. The test coverage is thorough and the refactor is generally clean.

📝 5 suggestion(s) (1 new, 4 re-raised)

⚠️ Diff exceeded 100 000 chars and was truncated — some files may not have been reviewed.


💡 How to re-trigger

Comment /review or /pr-reviewer on this PR

Comment thread lib/remote-storage.js Outdated
Comment thread lib/remote-storage.js Outdated
const allResults = []
if (!this._authToken) {
throw new Error('cannot upload files, Authorization is required')
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

[Re-raised] Commented-out sleep/rate-limiting code should be removed. If rate limiting is a concern, implement it properly or remove the comment entirely.

Suggested change
}
const res = await Promise.all(fileBatch.map(async file => {

Comment thread package.json Outdated
Comment thread lib/remote-storage.js
Comment thread src/deploy-web.js Outdated
log('warning: an existing deployment will be overwritten')
}
await remoteStorage.emptyFolder(config.s3.folder + '/')
await remoteStorage.emptyFolder('/', config)
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

uploadDir is called with config.s3.folder which may be undefined if s3 config is absent. Add a guard or default value to prevent passing undefined as basePath, which would cause uploadDir to throw a confusing error.

Suggested change
await remoteStorage.emptyFolder('/', config)
const s3Folder = config.s3?.folder ?? ''
await remoteStorage.uploadDir(dist, s3Folder, config, _log)

@github-actions github-actions Bot dismissed their stale review May 20, 2026 17:00

Superseded by new review

Copy link
Copy Markdown

@github-actions github-actions Bot left a comment

Choose a reason for hiding this comment

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

🤖 PR Reviewer

The diff represents a significant architectural migration from AWS S3/TVM-based file storage to a new deploy-service REST API using bearer token authentication. The code is generally well-structured and the tests are comprehensive. A few issues remain: commented-out code, potential undefined config access, and the joi package remains in dependencies even though it's no longer used in remote-storage.js.

📝 5 suggestion(s) (2 new, 3 re-raised)

⚠️ Diff exceeded 100 000 chars and was truncated — some files may not have been reviewed.


💡 How to re-trigger

Comment /review or /pr-reviewer on this PR

Comment thread lib/remote-storage.js
Comment thread lib/remote-storage.js
Comment thread package.json
"joi": "^17.2.1",
"klaw": "^4",
"lodash.clonedeep": "^4.5.0",
"mime-types": "^2.1.24",
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

joi is still listed as a dependency but is no longer used in remote-storage.js (the Joi validation in the constructor was removed). Remove it to keep the dependency list clean.

Suggested change
"mime-types": "^2.1.24",
"lodash.clonedeep": "^4.5.0",

Comment thread src/deploy-web.js Outdated
await remoteStorage.emptyFolder('/', config)
}
const _log = log ? (f) => log(`deploying ${path.relative(dist, f)}`) : null
await remoteStorage.uploadDir(dist, config.s3.folder, config, _log)
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

[Re-raised] config.s3?.folder may be undefined if the s3 config is absent, which would pass undefined as basePath to uploadDir, causing a confusing 'basePath must be a valid string' error. Add a guard or default.

Suggested change
await remoteStorage.uploadDir(dist, config.s3.folder, config, _log)
const s3Folder = config.s3?.folder ?? ''
await remoteStorage.uploadDir(dist, s3Folder, config, _log)

Comment thread src/undeploy-web.js
if (!(await remoteStorage.folderExists('/', config))) {
throw new Error(`cannot undeploy static files, there is no deployment for ${config.s3.folder}`)
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

The error message still references config.s3.folder which may be undefined under the new API. Update the error message to reference the namespace instead, or guard against undefined.

Suggested change
throw new Error(`cannot undeploy static files, there is no deployment for namespace ${config.ow.namespace}`)

@github-actions github-actions Bot dismissed their stale review May 20, 2026 19:17

Superseded by new review

@purplecabbage
Copy link
Copy Markdown
Member Author

/review

Copy link
Copy Markdown

@github-actions github-actions Bot left a comment

Choose a reason for hiding this comment

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

🤖 PR Reviewer

This is a well-structured refactor replacing AWS S3 direct SDK usage with a deploy-service HTTP API. The code is generally clean, tests are comprehensive, and the migration is logical. However, several previously raised issues remain unaddressed, and a few new concerns exist in the updated files.

📝 6 suggestion(s) (2 new, 4 re-raised)

⚠️ Diff exceeded 100 000 chars and was truncated — some files may not have been reviewed.


💡 How to re-trigger

Comment /review or /pr-reviewer on this PR

Comment thread lib/remote-storage.js
const { NodeHttpHandler } = require('@smithy/node-http-handler')
const { ProxyAgent } = require('proxy-agent')
const { EnvHttpProxyAgent } = require('undici')
const { codes, logAndThrow } = require('./StorageError')
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

[Re-raised] Commented-out code and stale comments ('// dev deploy => ...', '// run local => ...') should be removed before merging.

Suggested change
const { codes, logAndThrow } = require('./StorageError')
const deploymentServiceUrl = process.env.AIO_DEPLOYMENT_SERVICE_URL ||
(getCliEnv() === PROD_ENV
? 'https://deploy-service.app-builder.adp.adobe.io'
: 'https://deploy-service.stg.app-builder.corp.adp.adobe.io')

Comment thread lib/remote-storage.js
if (!this._authToken) {
throw new Error('cannot upload files, Authorization is required')
}
while (fileBatch.length > 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.

[Re-raised] Commented-out sleep/rate-limiting code should be removed. If rate limiting is needed, implement it properly or open a tracked issue.

Suggested change
while (fileBatch.length > 0) {
const res = await Promise.all(fileBatch.map(async file => {

Comment thread package.json
"fs-extra": "^11",
"joi": "^17.2.1",
"klaw": "^4",
"lodash.clonedeep": "^4.5.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.

joi is still listed as a dependency but is no longer used in remote-storage.js. Remove it to keep the dependency list clean.

Suggested change
"lodash.clonedeep": "^4.5.0",
"lodash.clonedeep": "^4.5.0",

Comment thread src/deploy-web.js Outdated
await remoteStorage.emptyFolder('/', config)
}
const _log = log ? (f) => log(`deploying ${path.relative(dist, f)}`) : null
await remoteStorage.uploadDir(dist, config.s3.folder, config, _log)
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

[Re-raised] config.s3?.folder may be undefined if the s3 config is absent, causing a confusing 'basePath must be a valid string' error downstream in uploadDir. Add a guard or default.

Suggested change
await remoteStorage.uploadDir(dist, config.s3.folder, config, _log)
const s3Folder = config.s3?.folder ?? ''
await remoteStorage.uploadDir(dist, s3Folder, config, _log)

Comment thread src/undeploy-web.js

const remoteStorage = new RemoteStorage(creds)
const remoteStorage = new RemoteStorage(bearerToken)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

[Re-raised] Error message still references config.s3.folder which may be undefined under the new API. Update to reference the namespace instead.

Suggested change
throw new Error(`cannot undeploy static files, there is no deployment for namespace ${config.ow.namespace}`)

Comment thread lib/remote-storage.js
@github-actions github-actions Bot dismissed their stale review May 20, 2026 19:25

Superseded by new review

Copy link
Copy Markdown

@github-actions github-actions Bot left a comment

Choose a reason for hiding this comment

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

🤖 PR Reviewer

This PR is a significant refactor that replaces the AWS S3 SDK with a REST-based deploy-service API and bearer token authentication. The code is generally well-structured and tested, but several previously raised issues remain (stale comments in remote-storage.js and missing joi removal from package.json), and a few new issues exist around error messages, the config.s3?.folder usage in deploy-web.js, and the module-level side effect for URL initialization.

📝 8 suggestion(s) (1 new, 7 re-raised)

⚠️ Diff exceeded 100 000 chars and was truncated — some files may not have been reviewed.


💡 How to re-trigger

Comment /review or /pr-reviewer on this PR

⚠️ Inline comments could not be attached (lines not in diff). See summary above.

@github-actions github-actions Bot dismissed their stale review May 28, 2026 23:12

Superseded by new review

Copy link
Copy Markdown

@github-actions github-actions Bot left a comment

Choose a reason for hiding this comment

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

🤖 PR Reviewer

This is a significant refactoring that replaces AWS S3 direct SDK usage with a new deploy-service API using fetch/undici. The overall approach is clean and well-tested. A few issues need attention: the module-level deploymentServiceUrl computation prevents environment mocking in tests without jest.resetModules(), there's a potential security concern with auth token storage, and some minor issues with error message consistency and the joi dependency remaining in package.json despite no longer being needed for validation.

📝 8 suggestion(s) - Please review inline comments below.

⚠️ Diff exceeded 100 000 chars and was truncated — some files may not have been reviewed.


💡 How to re-trigger

Comment /review or /pr-reviewer on this PR

Comment thread lib/remote-storage.js Outdated
throw res.error
}
// First check AIO_DEPLOYMENT_SERVICE_URL environment variable
// If not set, use the default value based on the environment
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

The deploymentServiceUrl and fetchDispatcher are module-level singletons. This is fine for production but makes testing environment changes harder. This is already worked around in tests with jest.resetModules(), but it's worth noting the fetchDispatcher singleton is never reset between module reloads (since it's a closure), which is fine since jest.resetModules() discards the whole module.

Suggested change
// If not set, use the default value based on the environment
// No code change needed, but consider documenting the singleton behavior

Comment thread lib/remote-storage.js
Comment thread lib/remote-storage.js
this._authToken = authToken
}

/**
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

The JSDoc comment for the constructor parameter should note that the token is stored in memory. For security, consider not storing the token as an instance variable directly if it could be logged or serialized, though this is a minor concern for a server-side Node.js module.

Suggested change
/**
/**
* Constructor for RemoteStorage
* @param {string} authToken - The authorization token (e.g. 'Bearer <token>') to use for the remote storage
* @private This token is stored in memory only and not serialized
*/

Comment thread lib/remote-storage.js
headers: {
Authorization: this._authToken
}
})
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

The emptyFolder method returns response.ok (a boolean) but the caller in undeploy-web.js checks if (!deleted) and throws. This is fine, but the method's JSDoc says it returns Promise<boolean> - however when auth check throws, it doesn't return false, it throws. The behavior is inconsistent: auth failure throws, but HTTP failure returns false. Consider making these consistent by throwing on HTTP failure too, or documenting the distinction.

Suggested change
})
if (!response.ok) {
throw new Error(`cannot empty folder, request failed: ${response.status} ${response.statusText}`)
}
return true

Comment thread src/deploy-web.js Outdated
await remoteStorage.emptyFolder('/', config)
}
const _log = log ? (f) => log(`deploying ${path.relative(dist, f)}`) : null
await remoteStorage.uploadDir(dist, config.s3.folder, config, _log)
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

The folderExists call passes '/' as prefix but the implementation ignores the prefix parameter entirely (it lists all files for the namespace). The prefix parameter in the new implementation is kept only for API compatibility but is misleading. Consider documenting this or simplifying the call.

Suggested change
await remoteStorage.uploadDir(dist, config.s3.folder, config, _log)
if (await remoteStorage.folderExists('/' /* prefix unused in new API */, config)) {

Comment thread src/deploy-web.js
throw new Error('config.app.hostname and config.ow.namespace are required')
}
if (!config.app.hostname.match(/^[a-zA-Z0-9.-]+$/)) {
throw new Error('config.app.hostname is invalid')
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

The hostname validation regex /^[a-zA-Z0-9.-]+$/ is applied before the auth check and dist directory check. If the hostname or namespace validation is expensive or if order matters for UX, consider consolidating all config validation at the top. Also, this regex allows hostnames starting/ending with dots or hyphens which are technically invalid hostnames.

Suggested change
throw new Error('config.app.hostname is invalid')
if (!config.app.hostname.match(/^[a-zA-Z0-9]([a-zA-Z0-9.-]*[a-zA-Z0-9])?$/)) {
throw new Error('config.app.hostname is invalid')
}

Comment thread package.json
"proxy-agent": "^6.3.0",
"regenerator-runtime": "^0.13.7"
"regenerator-runtime": "^0.13.7",
"undici": "^7.25.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.

The joi package is still listed as a dependency in package.json but validation using joi was removed from remote-storage.js constructor. Check if joi is used elsewhere; if not, it should be removed to reduce bundle size.

Suggested change
"undici": "^7.25.0"
// Remove 'joi' from dependencies if not used elsewhere

Comment thread lib/remote-storage.js
// ideally we deprecate cache control config in favor of response headers directly
if (responseHeaders?.['adp-cache-control']) {
uploadParams.CacheControl = responseHeaders['adp-cache-control']
cacheControlString = responseHeaders['adp-cache-control']
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

File content is read with fs.readFile which returns a Buffer, then Buffer.from(content).toString('base64') is called. Since content is already a Buffer, Buffer.from(content) creates an unnecessary copy. Use content.toString('base64') directly.

Suggested change
cacheControlString = responseHeaders['adp-cache-control']
content: content.toString('base64')

@github-actions github-actions Bot dismissed their stale review May 28, 2026 23:27

Superseded by new review

Copy link
Copy Markdown

@github-actions github-actions Bot left a comment

Choose a reason for hiding this comment

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

🤖 PR Reviewer

This is a substantial refactor replacing AWS S3/TVM-based storage with a new deploy-service HTTP API. The migration is well-structured with good test coverage. Several previously raised issues remain unaddressed, and a few new issues are present including the Buffer.from(content) redundancy and the hostname regex allowing invalid formats.

📝 6 suggestion(s) (1 new, 5 re-raised)

⚠️ Diff exceeded 100 000 chars and was truncated — some files may not have been reviewed.


💡 How to re-trigger

Comment /review or /pr-reviewer on this PR

Comment thread lib/remote-storage.js
httpsAgent: agent
})
}
const deployServiceFetch = (url, init) => {
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

[Re-raised] The deployServiceFetch function accesses the global fetch. If fetch is not available in the Node.js version being used, this will fail silently at runtime. Consider adding a guard or using undici's fetch directly.

Suggested change
const deployServiceFetch = (url, init) => {
const deployServiceFetch = (url, init) => {
const fetchFn = typeof fetch !== 'undefined' ? fetch : require('undici').fetch
return fetchFn(url, { ...init, dispatcher: getFetchDispatcher() })
}

Comment thread lib/remote-storage.js
const response = await deployServiceFetch(url, {
method: 'PUT',
body: JSON.stringify(data),
headers: {
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

[Re-raised] Buffer.from(content) creates an unnecessary copy since content is already a Buffer returned by fs.readFile. Use content.toString('base64') directly.

Suggested change
headers: {
content: content.toString('base64')

Comment thread lib/remote-storage.js
method: 'GET',
headers: {
Authorization: this._authToken
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

[Re-raised] emptyFolder returns response.ok (boolean false) on HTTP failure, but throws on auth failure. The behavior is inconsistent. Consider throwing on HTTP failure too for uniform error handling, since the caller in undeploy-web.js explicitly checks the return value and throws anyway.

Suggested change
}
if (!response.ok) {
throw new Error(`cannot empty folder, request failed: ${response.status} ${response.statusText}`)
}
return true

Comment thread src/deploy-web.js

if (exists) {
const remoteStorage = new RemoteStorage(bearerToken)
// validate config.app.hostname and config.ow.namespace are not empty and are valid and do not contain invalid characters
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

[Re-raised] The hostname regex /^[a-zA-Z0-9.-]+$/ allows hostnames starting or ending with dots or hyphens (e.g. -bad.host, bad.host.), which are technically invalid. Use a stricter regex.

Suggested change
// validate config.app.hostname and config.ow.namespace are not empty and are valid and do not contain invalid characters
if (!config.app.hostname.match(/^[a-zA-Z0-9]([a-zA-Z0-9.-]*[a-zA-Z0-9])?$/)) {
throw new Error('config.app.hostname is invalid')
}

Comment thread src/deploy-web.js
await remoteStorage.emptyFolder(config.s3.folder + '/')
await remoteStorage.emptyFolder('/', config)
}
const _log = log ? (f) => log(`deploying ${path.relative(dist, f)}`) : 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.

[Re-raised] folderExists is called with '/' but the implementation ignores the prefix parameter entirely. Consider documenting this explicitly to avoid future confusion.

Suggested change
const _log = log ? (f) => log(`deploying ${path.relative(dist, f)}`) : null
if (await remoteStorage.folderExists('/' /* prefix unused, kept for API compatibility */, config)) {

Comment thread lib/remote-storage.js Outdated
if (!this._authToken) {
throw new Error('cannot upload file, Authorization is required')
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

The uploadFile method silently passes false as contentType to the server when the MIME type is unknown (mime-types returns false for unknown extensions). The server may not handle false gracefully. Consider omitting the field or using a fallback like 'application/octet-stream'.

Suggested change
contentType: mimeType || 'application/octet-stream',

@github-actions github-actions Bot dismissed their stale review May 28, 2026 23:52

Superseded by new review

Copy link
Copy Markdown

@github-actions github-actions Bot left a comment

Choose a reason for hiding this comment

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

🤖 PR Reviewer

This is a significant refactor replacing AWS S3/TVM-based deployment with a new deploy-service HTTP API using undici's fetch. The code is generally well-structured with good test coverage. Several previously raised issues remain unaddressed, and a few new concerns are present in the diff.

📝 7 suggestion(s) (2 new, 5 re-raised)

⚠️ Diff exceeded 100 000 chars and was truncated — some files may not have been reviewed.


💡 How to re-trigger

Comment /review or /pr-reviewer on this PR

Comment thread lib/remote-storage.js
Prefix: prefix
// Call the list files endpoint (GET /files) - there is no GET /files/:key route
const response = await deployServiceFetch(`${getDeploymentServiceUrl()}/cdn-api/namespaces/${appConfig.ow.namespace}/files`, {
method: 'GET',
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

[Re-raised] The deployServiceFetch function accesses the global fetch. If fetch is not available in the Node.js version being used, this will fail silently at runtime. Consider adding a guard or using undici's fetch directly.

Suggested change
method: 'GET',
const deployServiceFetch = (url, init) => {
const fetchFn = typeof fetch !== 'undefined' ? fetch : require('undici').fetch
return fetchFn(url, { ...init, dispatcher: getFetchDispatcher() })
}

Comment thread lib/remote-storage.js
}).catch(error => {
console.error('Error uploading file:', file)
throw error
})
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

[Re-raised] Buffer.from(content) creates an unnecessary copy since content is already a Buffer returned by fs.readFile. Use content.toString('base64') directly.

Suggested change
})
content: content.toString('base64')

Comment thread lib/remote-storage.js
* This becomes file.name in the API request. The server will prepend the namespace.
* @param {Object} appConfig - application config
* @param {string} distRoot - Distribution root dir
* @param {string} distRoot - Distribution root dir (used for header matching)
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

[Re-raised] emptyFolder returns response.ok (boolean false) on HTTP failure, but throws on other errors. The behavior is inconsistent. Consider throwing on HTTP failure too for uniform error handling, since the caller in undeploy-web.js explicitly checks the return value and throws anyway.

Suggested change
* @param {string} distRoot - Distribution root dir (used for header matching)
if (!response.ok) {
throw new Error(`cannot empty folder, request failed: ${response.status} ${response.statusText}`)
}
return true

Comment thread src/deploy-web.js
if (exists) {
const remoteStorage = new RemoteStorage()
// validate config.app.hostname and config.ow.namespace are not empty and are valid and do not contain invalid characters
if (!config.app.hostname || !config.ow.namespace) {
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

[Re-raised] The hostname regex /^[a-zA-Z0-9.-]+$/ allows hostnames starting or ending with dots or hyphens (e.g. -bad.host, bad.host.), which are technically invalid. Use a stricter regex.

Suggested change
if (!config.app.hostname || !config.ow.namespace) {
if (!config.app.hostname.match(/^[a-zA-Z0-9]([a-zA-Z0-9.-]*[a-zA-Z0-9])?$/)) {
throw new Error('config.app.hostname is invalid')
}

Comment thread lib/remote-storage.js
}).catch(error => {
console.error('Error uploading file:', file)
throw error
})
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

[Re-raised] uploadFile silently passes false as contentType to the server when the MIME type is unknown (mime-types returns false for unknown extensions). The server may not handle false gracefully. Consider using a fallback like 'application/octet-stream'.

Suggested change
})
contentType: mimeType || 'application/octet-stream',

Comment thread lib/remote-storage.js
* @param {string} prefix - unused, kept for API compatibility
* @param {Object} appConfig - application config
* @returns {Promise<boolean>} true if files exist, false otherwise
*/
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

The fetchDispatcher is a module-level singleton. This means it persists across tests and potentially across different runtime configurations if the module is cached. While lazy initialization is fine, consider documenting that fetchDispatcher is intentionally a singleton and cannot be reset without clearing the module cache.

Suggested change
*/
// Module-level singleton: intentionally shared across all requests for connection reuse.
// To reset (e.g., in tests), clear the require cache for this module.
let fetchDispatcher

EnvHttpProxyAgent: jest.fn(() => mockProxyDispatcher)
}))

const { EnvHttpProxyAgent } = require('undici')
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

The EnvHttpProxyAgent singleton test may be flaky across test suites because fetchDispatcher is a module-level variable that persists between test files. The test asserts toHaveBeenCalledTimes(1) but if a previous test in another file already triggered initialization, the count would be wrong. Consider resetting the module between tests with jest.resetModules() or isolating the module.

@github-actions github-actions Bot dismissed their stale review May 29, 2026 00:05

Superseded by new review

@purplecabbage
Copy link
Copy Markdown
Member Author

closing in favor of new clean pr

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.

1 participant