Skip to content

Deploy service cdn#245

Open
purplecabbage wants to merge 25 commits into
masterfrom
DeployServiceCDN
Open

Deploy service cdn#245
purplecabbage wants to merge 25 commits into
masterfrom
DeployServiceCDN

Conversation

@purplecabbage
Copy link
Copy Markdown
Member

@purplecabbage purplecabbage commented May 29, 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 PR refactors the remote storage layer from direct AWS S3 SDK usage to a new deploy-service HTTP API, replacing TVM credentials with bearer token auth. The code is generally clean and well-structured with good test coverage. A few issues remain around the module-level singleton pattern for the fetch dispatcher, error message detail, and minor security/robustness concerns.

📝 5 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
return aioLibEnv.getCliEnv() === aioLibEnv.PROD_ENV
? 'https://deploy-service.app-builder.adp.adobe.io'
: 'https://deploy-service.stg.app-builder.corp.adp.adobe.io'
}
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 module-level singleton is never reset between tests or process restarts. If EnvHttpProxyAgent initialization fails or the proxy environment changes at runtime, there's no way to refresh it. Additionally, this pattern makes the module harder to test in isolation since jest module resets won't clear it. Consider using a lazy factory pattern that at minimum allows resetting, or instantiate it per-request if the overhead is acceptable.

Suggested change
}
let fetchDispatcher
const getFetchDispatcher = () => {
if (!fetchDispatcher) {
fetchDispatcher = new EnvHttpProxyAgent()
}
return fetchDispatcher
}
// Exported only for testing purposes
module.exports._resetFetchDispatcher = () => { fetchDispatcher = undefined }

Comment thread lib/remote-storage.js
const deploymentServiceUrl = getDeploymentServiceUrl()
const url = prefix === '/'
? `${deploymentServiceUrl}/cdn-api/namespaces/${appConfig.ow.namespace}/files/`
: `${deploymentServiceUrl}/cdn-api/namespaces/${appConfig.ow.namespace}/files/${prefix}`
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 for a failed upload doesn't include the HTTP status code, only the status text. This makes debugging harder. Include response.status in the message, consistent with the folderExists error at line 84.

Suggested change
: `${deploymentServiceUrl}/cdn-api/namespaces/${appConfig.ow.namespace}/files/${prefix}`
throw new Error(`Failed to upload file: ${response.status} ${response.statusText}`)

Comment thread lib/remote-storage.js
this.s3 = new S3(s3Config)
let fetchDispatcher
const getFetchDispatcher = () => {
if (!fetchDispatcher) {
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 deployServiceFetch function spreads init and then adds dispatcher, which means a caller-supplied dispatcher in init would be silently overridden. The spread order should put dispatcher before the spread, or document that it always overrides.

Suggested change
if (!fetchDispatcher) {
const deployServiceFetch = (url, init) => {
return fetch(url, { dispatcher: getFetchDispatcher(), ...init })
}

Comment thread src/deploy-web.js
@@ -30,19 +34,25 @@ const deployWeb = async (config, log) => {
throw new Error(`missing files in ${dist}, maybe you forgot to build your UI ?`)
}

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 config?.ow?.auth_handler?.getAuthHeader() call could theoretically return a non-string truthy value or an empty string that passes the truthiness check but is an invalid bearer token. Consider validating that the result is a non-empty string.

Suggested change
const bearerToken = await config?.ow?.auth_handler?.getAuthHeader()
if (!bearerToken || typeof bearerToken !== 'string') {
throw new Error('cannot deploy web, Authorization is required')
}

Comment thread lib/remote-storage.js
method: 'GET',
headers: {
Authorization: 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 URL construction for emptyFolder when prefix is / results in a trailing slash (/files/). While this is intentional per the comment, it's fragile—if the server normalizes the URL and strips the trailing slash, the behavior would silently change to "delete nothing" or "delete a file named empty string". This design should be explicitly documented and ideally replaced with a dedicated query parameter or a separate endpoint path like /files?deleteAll=true to make the intent unambiguous.

Suggested change
}
// Use explicit query param to signal full namespace deletion instead of relying on trailing slash
const url = prefix === '/'
? `${deploymentServiceUrl}/cdn-api/namespaces/${appConfig.ow.namespace}/files?deleteAll=true`
: `${deploymentServiceUrl}/cdn-api/namespaces/${appConfig.ow.namespace}/files/${prefix}`

Copy link
Copy Markdown
Contributor

@chsrimanaswi chsrimanaswi 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 SDK with a custom deploy-service HTTP API using undici's fetch. The migration is clean and well-tested. Two previously raised issues remain unaddressed (missing status code in upload error message, and the dispatcher override order in deployServiceFetch), and the trailing slash URL fragility concern for emptyFolder is still present. The bearerToken type validation suggestion was partially addressed with a null check but not the non-string type guard.

🔄 3 re-raised suggestion(s) from previous review


💡 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 src/deploy-web.js
@chsrimanaswi chsrimanaswi dismissed stale reviews from github-actions[bot] and themself May 29, 2026 05:41

Superseded by new review

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