Skip to content

feat: add cdn-deploy action for single-step site deployments#17

Open
LautaroPetaccio wants to merge 9 commits into
mainfrom
feat/cdn-deploy-action
Open

feat: add cdn-deploy action for single-step site deployments#17
LautaroPetaccio wants to merge 9 commits into
mainfrom
feat/cdn-deploy-action

Conversation

@LautaroPetaccio

@LautaroPetaccio LautaroPetaccio commented Jun 5, 2026

Copy link
Copy Markdown
Contributor

what

A new GitHub Action (cdn-deploy) + reusable workflow that deploy a Decentraland static site to the CDN in one step: upload the built folder straight to S3 (auth via GitHub OIDC) and patch the Cloudflare KV the worker reads — replacing the three-stage relay (oddish-action npm publish → static-sites-pipeline gitlab S3 upload → set-rollout-actionwebhooks-receiver). No npm publish, no npm re-download, no gitlab hop, no webhooks-receiver call.

Lives in this monorepo: a composite action under cdn-deploy/, a reusable workflow .github/workflows/cdn-deploy.yml, a release workflow, and Dependabot.

state-aware (no explicit modes)

The action infers the S3 work from state — it HEADs <package>/<version>/index.html and:

  • absent + folderupload the build
  • absent + release (target version ≠ commit version) → copy the commit's already-uploaded build (no rebuild)
  • already present → skip (just repoint)
  • force → redo; deployment-environments: '[]'stage (S3 only, no KV)

The version is commit-deterministic (<base>-commit-<sha>, no run id), so a later run can reconstruct and reuse a build. deployment-environments repoints several envs from one upload. A manual run can target a build by version or commit (promote dev→stg).

OIDC everywhere possible

concern auth
AWS S3 GitHub OIDC assume-role (aws-actions/configure-aws-credentials, SHA-pinned) — no static keys
GitHub deployment/commit status built-in GITHUB_TOKEN
npm removed entirely
Cloudflare KV scoped API token — Cloudflare has no GitHub-OIDC federation, the single remaining secret

runtime contract preserved

The CF worker still serves https://cdn.decentraland.org/<prefix>/<version>/… and selects the version from { records: { <name>: RolloutRecord[] } }. S3 key stays <packageName>/<version>/…, prefix === packageName, and the KV value is merged with patchRollouts from @well-known-components/rollouts-lib — the same function webhooks-receiver used.

slim per-site config

A site's deploy is a few with: lines + secrets: inherit. Shared config is org-level, set once:

  • org secrets: CF_KV_API_TOKEN, ROLLOUTS_SLACK_WEBHOOK, and the KV namespace ids CF_NS_ZONE/CF_NS_TODAY/CF_NS_ORG (these live in the private sites-deployer repo, so they're not hardcoded here)
  • org variables: CF_ACCOUNT_ID, CDN_DEPLOY_ROLE_ARN
  • bucket / region / CDN url defaulted in the action; package-name + deployment-path derived from package.json

also in here

  • Reusable workflow (cdn-deploy.yml) — checkout + Node 24 + build + deploy; one deploy at a time per repo (concurrency).
  • Release workflow — pushing cdn-deploy-v*.*.* moves the floating cdn-deploy-v1 major tag for pinning.
  • Dependabot (npm + github-actions, chore: prefix); SHA-pinned third-party actions on Node 24.
  • Guards/observability: fail-fast if the build has no index.html; a core.summary run table; GitHub deployment + cdn-rollout/upload commit status.
  • 74 unit tests; committed dist/index.js (ncc bundle).

not in this PR

Infra prereqs (one-time): an IAM role with GitHub-OIDC trust + s3:PutObject on the CDN bucket, a scoped Cloudflare Workers-KV-Edit token, and the org secrets/variables above. A worked, inert adoption example: decentraland/sites#565.

Note for reviewers: the branch was force-pushed once to scrub the KV namespace ids that were briefly hardcoded — they're now org secrets (see commit 2d7194d).

new composite action that replaces the three-stage site deploy relay
(oddish-action npm publish -> static-sites-pipeline gitlab upload ->
set-rollout-action -> webhooks-receiver) with a single in-job step:

- uploads the pre-built folder straight to S3 (auth via github OIDC,
  no static aws keys) using @dcl/cdn-uploader with the same
  { immutable, concurrency: 10 } config and <packageName>/<version> key
- patches the cloudflare KV rollout record inline via patchRollouts from
  @well-known-components/rollouts-lib, keyed by path/domain with the
  environment selecting the namespace
- keeps slack and github deployment/commit status for observability

no npm publish, no npm re-download, no gitlab hop, no webhooks-receiver
call. cloudflare KV still uses a scoped api token (no OIDC federation
available there); everything else uses OIDC or the ephemeral GITHUB_TOKEN.
adds a no-rebuild "promote" path alongside the existing deploy:

- `source-version` triggers redeploy: server-side copy of every object
  under <packageName>/<sourceVersion>/ to <packageName>/<version>/ in the
  same bucket (MetadataDirective COPY + public-read ACL, paginated, so
  the uncompressed/.gzip/.br variant objects and their metadata are
  reproduced exactly), then the KV is pointed at the target version
- `version` alone (no folder, no source-version) is a pure repoint:
  KV pointer only, no S3 op and no aws creds needed
- mode is resolved from the inputs (deploy | redeploy | repoint) and
  exposed as a `mode` output

`folder` and `aws-role-to-assume` become optional; the aws credentials
step is gated so a repoint needs no OIDC. use case: cutting a release
over a commit already on the cdn copies the bytes to the release tag and
points prod at it, no second build.
@LautaroPetaccio

Copy link
Copy Markdown
Contributor Author

update: added redeploy / promote mode (no rebuild)

the action now picks one of three modes from its inputs:

mode trigger s3 action kv
deploy folder upload built folder point at new version
redeploy source-version copy …/<source-version>/…/<version>/ point at new version
repoint version only none point at an already-uploaded version

redeploy is the "promote without rebuilding" path: cutting a release over a commit already on the cdn copies those exact bytes to the release-tagged prefix and points prod at it — no second build. it copies every object under the source prefix with MetadataDirective: COPY + public-read ACL, so the file / file.gzip / file.br variants and their metadata are reproduced exactly (and the copy is skipped when source == target). ordering stays copy-then-kv, so kv never points at missing bytes.

folder and aws-role-to-assume are now optional; a pure repoint writes only the kv pointer and needs no aws creds (the credentials step is gated with if:). new mode output reports what ran.

63 unit tests pass (added plan.spec.ts for mode resolution + s3 copy tests), tsc clean, bundle rebuilt. readme has a release-promote example.

- aws-actions/configure-aws-credentials -> e7f100c # v6.2.0
- actions/checkout -> df4cb1c # v6.0.3 (readme example)
- actions/setup-node -> 48b55a0 # v6.4.0 (readme example)
- readme examples set up node 24
@LautaroPetaccio
LautaroPetaccio force-pushed the feat/cdn-deploy-action branch from a0e128b to 433af6e Compare June 7, 2026 13:50
- deploy mode fails fast when the folder has no index.html at its root
  (new `require-index` input, default true) so an empty/broken build is
  caught before it reaches the cdn and the kv pointer
- write a github job summary table (mode, version, env, cdn url, ...)
- pin dev node via .nvmrc (24) and declare engines (node >=20)
- reusable workflow .github/workflows/cdn-deploy.yml wraps checkout +
  node 24 + build + the cdn-deploy action so a site's deploy is a few
  lines of `with:` inputs
- release workflow moves a floating major tag (cdn-deploy-v1) on each
  cdn-deploy-v*.*.* tag so consumers can pin @cdn-deploy-v1
- dependabot keeps the npm deps and the sha-pinned actions current
  (chore: prefix so its PR titles pass validate-pr-title)
infer the S3 work from state instead of an explicit mode:
- HEAD <package>/<version>/index.html; already there -> skip and just
  repoint; absent -> upload the folder, or copy the commit's build for a
  release; `force` redoes it
- version is now commit-deterministic (<base>-commit-<sha>, no run id) so a
  release run can locate and copy the build the last master commit uploaded
- `deployment-environments` repoints several envs from one upload; an empty
  list stages bytes in S3 with no KV change (release staging)

cut what each site configures:
- default the bucket + region in the action; the KV namespace ids come from
  org secrets CF_NS_ZONE/TODAY/ORG (they live in a private repo — never
  hardcoded here)
- reusable workflow defaults account-id/role to org vars and reads the org
  CF_KV_API_TOKEN / ROLLOUTS_SLACK_WEBHOOK / CF_NS_* via `secrets: inherit`
- derive deployment-path from the package name; default envs to zone+today
- reusable workflow serializes one deploy at a time per repo (concurrency)
@LautaroPetaccio
LautaroPetaccio force-pushed the feat/cdn-deploy-action branch from e1a3834 to 2d7194d Compare June 17, 2026 20:37
add a `commit` input: the action computes the version from it (overriding
the workflow sha) so you can promote a specific build by commit — e.g.
deploy to stg what you set on dev. The reusable workflow checks the repo
out at that commit so the base version is read from its package.json, and
reads package.json from the repo root when there's no upload folder.

- `commit` participates in the S3 "already deployed?" check (errors clearly
  if that commit's build isn't on the CDN)
- version still takes precedence when both are given
P0 — the release/promote flow was broken: stage-release didn't check out
(build-command empty, no commit), so the action read no package.json and
baseVersion silently became "0.0.0". The copy source `<pkg>/0.0.0-commit-<sha>`
never matched the dev deploy's `<pkg>/<realbase>-commit-<sha>`, so the copy
found no objects and failed. Fixes:
- read packageName + baseVersion from the repo-root package.json (not the
  upload folder, which for a Vite build has no package.json), so every flow
  computes the same version for a commit
- the reusable workflow now always checks out (ref: commit || triggering ref)
  so the base version is available for release/repoint, not just builds

Also:
- multi-env KV writes attempt every environment and, on failure, throw an
  aggregate naming which envs were already updated vs failed (was: abort on
  first failure, hiding the partial write); helper renamed to
  patchRolloutInEnvironments
- parsePercentage rejects non-integers (KV stored a truncated value while
  logs/Slack reported the fractional one); dropped the `| 0` truncation
- warn when no GitHub deployment id is returned (e.g. 202) instead of silently
  leaving the deployment without status
- removed dead rolloutHasVersion; typed the `mode` output as S3Action

Security hardening:
- cloudflare.ts uses encodeURIComponent for the KV key (single path segment)
- reusable workflow runs build-command via `bash -ec "$BUILD_COMMAND"` from env
  instead of interpolating it into the run line
…e workflow

the build belonged to the site, not a shared workflow. the action already
took a pre-built folder, so:

- rename the `folder` input to `dist-path` (the caller builds its own artifact
  and hands over the path); build + deploy run in the same job, no artifact
  round-trip
- remove the reusable workflow whose only extra job was running the build
  (the thing we're moving out). sites call the composite action directly,
  setting their own concurrency group and wiring the org secrets/vars
- README rewritten for the direct-action, one-job model

the release/manual flows are unchanged (version/commit/source-version still
resolve against the repo-root package.json, which the deploy job checks out).
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