Skip to content

feat(sites): list bunny sites in help as a public preview - #243

Merged
jamie-at-bunny merged 1 commit into
mainfrom
sites-public-preview
Oct 7, 2026
Merged

jamie-at-bunny merged 1 commit into
mainfrom
sites-public-preview

Conversation

@jamie-at-bunny

@jamie-at-bunny jamie-at-bunny commented Oct 7, 2026 •

Copy link
Copy Markdown
Member

Lists bunny sites in bunny --help as a public preview.

@changeset-bot

changeset-bot Bot commented Oct 7, 2026 •

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 212586d

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 6 packages
Name Type
@bunny.net/cli Minor
@bunny.net/cli-darwin-arm64 Minor
@bunny.net/cli-darwin-x64 Minor
@bunny.net/cli-linux-arm64 Minor
@bunny.net/cli-linux-x64 Minor
@bunny.net/cli-windows-x64 Minor

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@jamie-at-bunny
jamie-at-bunny marked this pull request as ready for review October 7, 2026 15:44
@bunnynet-devops

Copy link
Copy Markdown

@codex review

@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Oct 7, 2026 •

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review ✅ Completed 2026-10-07T15:51:49.175811Z d06088b Manual request
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: d06088ba02

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +94 to +95
if (err instanceof UserError && !(err instanceof ApiError)) throw err;
throw new UserError(

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Preserve API and unexpected error classifications

Re-throw ApiError and unexpected exceptions instead of wrapping both as a plain UserError. When provisioning receives a 401, this catch strips its status so defineCommand() cannot produce the credential-source/login hint; it also turns programming or runtime failures into exit code 1 without the verbose stack trace. The new resume hint should only augment errors whose classification and context can be preserved.

AGENTS.md reference: AGENTS.md:L269-L282

Useful? React with 👍 / 👎.

Comment on lines +273 to +275
// CI deploys a nested project's output from the repo root, so fall back to the bunny.jsonc above the deploy directory.
const siteConfig =
loadSiteConfig() ?? (args.dir ? loadSiteConfig(args.dir) : 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.

P2 Badge Use the nested config name when selecting the site

When a deploy is launched from a repository root with a positional nested output directory, this fallback finds that project's bunny.jsonc, but selectSite() later performs its own loadSiteConfig() from the current working directory and never sees siteConfig.config.name. Consequently, a non-interactive command such as bunny sites deploy packages/web/dist --output json still fails with “No site specified” even though packages/web/bunny.jsonc names the site; pass the already loaded name into site selection.

Useful? React with 👍 / 👎.

Comment on lines +90 to +92
const lockDir = [projectRoot, root].find((dir) =>
LOCKFILES.some((name) => existsSync(join(dir, name))),
);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Do not treat an unrelated root lockfile as the project's

For a nested project without its own lockfile, this unconditionally treats any repository-root lockfile as governing the project. If the nested package is not actually a workspace member—for example, an independent npm app under a repository whose root has package-lock.json—the generated job runs from the nested directory but emits npm ci; npm requires an existing lockfile for that project and fails there. Verify workspace membership before falling back to the root lockfile, otherwise generate the unlocked install path.

Useful? React with 👍 / 👎.

Comment on lines +33 to +35
"refs/remotes/origin/HEAD",
]);
return ref?.replace(/^origin\//, "") || "main";

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Resolve the remote default branch before assuming main

When refs/remotes/origin/HEAD is absent locally, this emits main even if the GitHub repository's default branch is master, trunk, or another name. That ref is commonly missing after git init plus git remote add, so the generated workflow listens on the wrong branch and ordinary pushes never deploy despite the command reporting that pushes to main go live. Query the remote HEAD or otherwise obtain the repository default before using the fallback.

Useful? React with 👍 / 👎.

Comment on lines +46 to +49
// statSync follows symlinks, so linked files and dirs ship as their targets.
const stat = entry.isSymbolicLink()
? statSync(entryAbs, { throwIfNoEntry: false })
: entry;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Keep symlink targets inside the deploy boundary

Following symlinks without checking the resolved target allows a harmless-looking entry inside the deploy directory to upload files from anywhere on the machine. For example, public/config -> ../.env or public/vendor -> ../node_modules bypasses the documented dotfile and node_modules exclusions and can publish credentials or unrelated files to the CDN. Resolve each target and reject targets outside the deploy root, or require an explicit opt-in before dereferencing them.

Useful? React with 👍 / 👎.

Comment thread packages/cli/src/commands/sites/api.ts Outdated
Comment on lines +530 to +531
// An imported site keeps its original zone names, so the name-pattern scan above can't see it.
if ((await fetchSites(coreClient)).some((s) => s.state.name === name)) {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Abort duplicate-name checks when site discovery is incomplete

This uses fetchSites() as an authoritative uniqueness check even though that helper catches per-zone read failures and returns a partial list. If the existing imported site's storage read fails transiently, creation proceeds with the same logical name; subsequent name-based resolution then fails because multiple sites match. The create path should fail closed when any candidate was unreadable rather than merely printing the partial-list warning.

Useful? React with 👍 / 👎.

Comment thread packages/cli/src/commands/sites/api.ts Outdated
Comment on lines +855 to +858
if (elapsed < SETTLE_FLOOR_MS) {
await promoteVerification.wait(SETTLE_FLOOR_MS - elapsed);
}
return confirmed;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Handle unconfirmed propagation at every promotion call site

The new boolean result is ignored by the unchanged-content deploy path and by migrateSite() (deploy.ts:473-475 and api.ts:992-995). When the edge does not confirm within the deadline, those commands still report an already-live or successfully migrated site without the warning emitted by the normal deploy and publish paths. Propagate the result through those callers and emit the same warning before reporting success.

Useful? React with 👍 / 👎.

@chatgpt-codex-connector

Copy link
Copy Markdown

💡 Codex Review

if (err instanceof UserError && !(err instanceof ApiError)) throw err;
throw new UserError(

P2 Badge Preserve API and unexpected error classifications

Re-throw ApiError and unexpected exceptions instead of wrapping both as a plain UserError. When provisioning receives a 401, this catch strips its status so defineCommand() cannot produce the credential-source/login hint; it also turns programming or runtime failures into exit code 1 without the verbose stack trace. The new resume hint should only augment errors whose classification and context can be preserved.

AGENTS.md reference: AGENTS.md:L269-L282


// CI deploys a nested project's output from the repo root, so fall back to the bunny.jsonc above the deploy directory.
const siteConfig =
loadSiteConfig() ?? (args.dir ? loadSiteConfig(args.dir) : null);

P2 Badge Use the nested config name when selecting the site

When a deploy is launched from a repository root with a positional nested output directory, this fallback finds that project's bunny.jsonc, but selectSite() later performs its own loadSiteConfig() from the current working directory and never sees siteConfig.config.name. Consequently, a non-interactive command such as bunny sites deploy packages/web/dist --output json still fails with “No site specified” even though packages/web/bunny.jsonc names the site; pass the already loaded name into site selection.


const lockDir = [projectRoot, root].find((dir) =>
LOCKFILES.some((name) => existsSync(join(dir, name))),
);

P2 Badge Do not treat an unrelated root lockfile as the project's

For a nested project without its own lockfile, this unconditionally treats any repository-root lockfile as governing the project. If the nested package is not actually a workspace member—for example, an independent npm app under a repository whose root has package-lock.json—the generated job runs from the nested directory but emits npm ci; npm requires an existing lockfile for that project and fails there. Verify workspace membership before falling back to the root lockfile, otherwise generate the unlocked install path.


"refs/remotes/origin/HEAD",
]);
return ref?.replace(/^origin\//, "") || "main";

P2 Badge Resolve the remote default branch before assuming main

When refs/remotes/origin/HEAD is absent locally, this emits main even if the GitHub repository's default branch is master, trunk, or another name. That ref is commonly missing after git init plus git remote add, so the generated workflow listens on the wrong branch and ordinary pushes never deploy despite the command reporting that pushes to main go live. Query the remote HEAD or otherwise obtain the repository default before using the fallback.


// statSync follows symlinks, so linked files and dirs ship as their targets.
const stat = entry.isSymbolicLink()
? statSync(entryAbs, { throwIfNoEntry: false })
: entry;

P2 Badge Keep symlink targets inside the deploy boundary

Following symlinks without checking the resolved target allows a harmless-looking entry inside the deploy directory to upload files from anywhere on the machine. For example, public/config -> ../.env or public/vendor -> ../node_modules bypasses the documented dotfile and node_modules exclusions and can publish credentials or unrelated files to the CDN. Resolve each target and reject targets outside the deploy root, or require an explicit opt-in before dereferencing them.


// An imported site keeps its original zone names, so the name-pattern scan above can't see it.
if ((await fetchSites(coreClient)).some((s) => s.state.name === name)) {

P2 Badge Abort duplicate-name checks when site discovery is incomplete

This uses fetchSites() as an authoritative uniqueness check even though that helper catches per-zone read failures and returns a partial list. If the existing imported site's storage read fails transiently, creation proceeds with the same logical name; subsequent name-based resolution then fails because multiple sites match. The create path should fail closed when any candidate was unreadable rather than merely printing the partial-list warning.


// Let the rule reach every node before the follow-up purge, so re-promotes don't re-cache the outgoing deploy's files.
const elapsed = Date.now() - start;
if (elapsed < SETTLE_FLOOR_MS) {
await promoteVerification.wait(SETTLE_FLOOR_MS - elapsed);
}
return confirmed;

P2 Badge Handle unconfirmed propagation at every promotion call site

The new boolean result is ignored by the unchanged-content deploy path and by migrateSite() (deploy.ts:473-475 and api.ts:992-995). When the edge does not confirm within the deadline, those commands still report an already-live or successfully migrated site without the warning emitted by the normal deploy and publish paths. Propagate the result through those callers and emit the same warning before reporting success.

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

@greptile-apps

greptile-apps Bot commented Oct 7, 2026 •

Copy link
Copy Markdown

RetriggerConfidence Score: 3/5

[Medium risk] Moves sites command from experimental to public preview.

The visibility changes look safe, but the earlier deployment-replacement and nested npm install issues still block merging.

Fix All in Claude CodeFindings

  1. P1 Fallback IDs erase older deploys ▶
  2. P1 Nested CI installs fail ▶

Summary

Makes bunny sites discoverable as a public preview.

  • Shows the namespace in root help and the landing page.
  • Restores the static-site deployment example.
  • Updates the preview notice and adds a minor release changeset.
  • No new findings were accepted.

Reviews (3) · Last reviewed commit: "feat(sites): list bunny sites in help as..." · Reviewed by Greptile

Comment thread packages/cli/src/commands/sites/uploader.ts Outdated
Comment on lines +154 to +155
const deployId =
alreadyUploaded?.id ?? (shaTaken ? identity.contentHash : identity.id);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Fallback IDs erase older deploys

If an older custom deploy ID equals the new content hash, holds different bytes, and is neither live nor previous, shaTaken selects that ID. The conflict check only asks before replacement when customId was passed on this run. The handler then deletes the older files without asking, so that ID can no longer restore its original deployment. Check fallback IDs for conflicts too, and choose another ID or require confirmation before replacing one.

Fix in Claude Code

Comment on lines +90 to +96
const lockDir = [projectRoot, root].find((dir) =>
LOCKFILES.some((name) => existsSync(join(dir, name))),
);
const packageManager = await detectPackageManager(lockDir ?? projectRoot);
return {
packageManager,
lockfile: lockDir !== undefined,

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Nested CI installs fail

If a standalone project under apps/web has no lockfile, an unrelated root package-lock.json makes this return lockfile: true. The generated job still runs in apps/web, so it emits npm ci where no usable lockfile exists and fails before building. Only reuse the root lockfile when the nested project belongs to its workspace; otherwise generate a plain install.

Fix in Claude Code

Comment thread packages/cli/src/commands/sites/provision.ts Outdated
Comment thread packages/cli/src/commands/sites/deploy.ts Outdated
Comment thread packages/cli/src/commands/sites/ci/scaffold.ts Outdated
@jamie-at-bunny
jamie-at-bunny merged commit eb4e0fc into main Oct 7, 2026
5 checks passed
@jamie-at-bunny
jamie-at-bunny deleted the sites-public-preview branch October 7, 2026 16:14
@github-actions github-actions Bot mentioned this pull request Oct 7, 2026
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.

3 participants