Repository navigation
fix(sites): make ci init workflows deploy and install correctly #246
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,6 @@ | ||
| --- | ||
| "@bunny.net/cli": patch | ||
| "@bunny.net/framework-detector": patch | ||
| --- | ||
|
|
||
| `bunny sites ci init` workflows deploy again (deploy-site 0.1.1 running this CLI's version), follow the default branch, and install correctly in pnpm, lockfile-less and Python projects |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -4,6 +4,7 @@ import { UserError } from "@/core/errors.ts"; | |
| import { runGit } from "@/core/git.ts"; | ||
| import { logger } from "@/core/logger.ts"; | ||
| import { confirm, prompts } from "@/core/ui.ts"; | ||
| import { VERSION } from "@/core/version.ts"; | ||
| import { | ||
| detectFramework, | ||
| detectPackageManager, | ||
|
|
@@ -24,6 +25,19 @@ export async function gitTopLevel(cwd: string): Promise<string | null> { | |
| return runGit(cwd, ["rev-parse", "--show-toplevel"]); | ||
| } | ||
|
|
||
| /** The remote's default branch (from `origin/HEAD`), or main when it isn't known locally; never the checked-out branch, which may be a feature branch. */ | ||
| export async function defaultBranch(root: string): Promise<string> { | ||
| const ref = await runGit(root, [ | ||
| "symbolic-ref", | ||
| "--short", | ||
| "refs/remotes/origin/HEAD", | ||
| ]); | ||
| return ref?.replace(/^origin\//, "") || "main"; | ||
|
Comment on lines
+29
to
+35
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
If |
||
| } | ||
|
|
||
| // The action runs this CLI's minor line, so CI deploys sites the same way the local command does. | ||
| const CLI_VERSION_RANGE = VERSION.split(".").slice(0, 2).join("."); | ||
|
|
||
| /** The host of a git remote URL, handling both scp-style (`git@host:path`) and URL forms; null when neither parses. */ | ||
| export function remoteHost(url: string): string | null { | ||
| const scp = url.match(/^(?:[^@/]+@)?([^/:]+):(?!\/)/); | ||
|
|
@@ -49,6 +63,8 @@ interface ScaffoldResult { | |
| packageManager: PackageManager; | ||
| /** Directory the workflow deploys, relative to the repo root: `sites.dir` when configured, else the preset's, prefixed when the project sits below the root. */ | ||
| dir: string; | ||
| /** The branch whose pushes go live. */ | ||
| branch: string; | ||
| } | ||
|
|
||
| // Lockfiles that decide the package manager; a nested project may carry its own, in which case setup-node needs to be pointed at it. | ||
|
|
@@ -60,6 +76,55 @@ const LOCKFILES = [ | |
| "package-lock.json", | ||
| ]; | ||
|
|
||
| interface JsInstallSettings { | ||
| packageManager: PackageManager; | ||
| lockfile: boolean; | ||
| pnpmVersion?: string; | ||
| } | ||
|
|
||
| async function jsInstallSettings( | ||
| root: string, | ||
| projectRoot: string, | ||
| ): Promise<JsInstallSettings> { | ||
| const lockDir = LOCKFILES.some((name) => existsSync(join(projectRoot, name))) | ||
| ? projectRoot | ||
| : undefined; | ||
| const packageManager = await detectPackageManager(lockDir ?? projectRoot); | ||
| return { | ||
| packageManager, | ||
| lockfile: lockDir !== undefined, | ||
| pnpmVersion: | ||
| packageManager === "pnpm" | ||
| ? await pnpmVersion(root, projectRoot, lockDir) | ||
|
Comment on lines
+92
to
+98
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
When Useful? React with 👍 / 👎.
Comment on lines
+92
to
+98
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
The lockfile-free path still chooses npm for pnpm projects. Knowledge Base Used: Framework detection |
||
| : undefined, | ||
| }; | ||
| } | ||
|
|
||
| // pnpm/action-setup reads only the root package.json's `packageManager`; without one it needs an explicit version, taken from the project's own field or the lockfile format. | ||
| async function pnpmVersion( | ||
| root: string, | ||
| projectRoot: string, | ||
| lockDir: string | undefined, | ||
| ): Promise<string | undefined> { | ||
| const pinned = (dir: string) => | ||
| readPackageJson(dir).then((pkg) => { | ||
| const field = pkg?.packageManager; | ||
| return typeof field === "string" && field.startsWith("pnpm@") | ||
| ? field.slice("pnpm@".length).split("+")[0] | ||
| : undefined; | ||
| }); | ||
| if (await pinned(root)) return undefined; | ||
| const own = await pinned(projectRoot); | ||
| if (own) return own; | ||
| const lock = lockDir | ||
| ? await Bun.file(join(lockDir, "pnpm-lock.yaml")) | ||
| .text() | ||
| .catch(() => "") | ||
| : ""; | ||
| const format = lock.match(/^lockfileVersion:\s*['"]?(\d+)/m)?.[1]; | ||
| return format === "5" ? "7" : format === "6" ? "8" : "10"; | ||
| } | ||
|
|
||
| // The git top level and the bunny.jsonc directory can reach the same place by different paths (macOS /tmp -> /private/tmp), which would read as "outside the repo". | ||
| function realOrSelf(path: string): string { | ||
| try { | ||
|
|
@@ -190,16 +255,24 @@ export async function scaffoldSitesWorkflow(opts: { | |
| opts.interactive, | ||
| settings.dir, | ||
| ); | ||
| const packageManager = await detectPackageManager(settings.projectRoot); | ||
| const { packageManager, lockfile, pnpmVersion } = await jsInstallSettings( | ||
| opts.root, | ||
| settings.projectRoot, | ||
| ); | ||
| const branch = await defaultBranch(opts.root); | ||
| const content = renderSitesWorkflow({ | ||
| site: opts.site, | ||
| preset, | ||
| packageManager, | ||
| lockfile, | ||
| pnpmVersion, | ||
| dir: settings.dir, | ||
| build: settings.build, | ||
| workingDirectory: settings.prefix || undefined, | ||
| cacheDependencyPath: settings.cacheDependencyPath, | ||
| installDeps: await needsJsInstall(preset, settings), | ||
| branch, | ||
| cliVersion: CLI_VERSION_RANGE, | ||
| }); | ||
|
|
||
| const target = join(opts.root, SITES_WORKFLOW_PATH); | ||
|
|
@@ -227,6 +300,7 @@ export async function scaffoldSitesWorkflow(opts: { | |
| preset, | ||
| packageManager, | ||
| dir: workflowPath(settings.prefix, settings.dir ?? preset.dir), | ||
| branch, | ||
| }; | ||
| } | ||
|
|
||
|
|
@@ -245,7 +319,10 @@ export async function printWorkflowInstructions( | |
| const preset = | ||
| (await detectFramework(settings.projectRoot)) ?? findPreset("static"); | ||
| if (!preset) return; | ||
| const packageManager = await detectPackageManager(settings.projectRoot); | ||
| const { packageManager, lockfile, pnpmVersion } = await jsInstallSettings( | ||
| root, | ||
| settings.projectRoot, | ||
| ); | ||
| logger.log(); | ||
| logger.log(`To deploy from GitHub later, add ${SITES_WORKFLOW_PATH}:`); | ||
| logger.log(); | ||
|
|
@@ -254,11 +331,15 @@ export async function printWorkflowInstructions( | |
| site, | ||
| preset, | ||
| packageManager, | ||
| lockfile, | ||
| pnpmVersion, | ||
| dir: settings.dir, | ||
| build: settings.build, | ||
| workingDirectory: settings.prefix || undefined, | ||
| cacheDependencyPath: settings.cacheDependencyPath, | ||
| installDeps: await needsJsInstall(preset, settings), | ||
| branch: await defaultBranch(root), | ||
| cliVersion: CLI_VERSION_RANGE, | ||
| }), | ||
| ); | ||
| printSecretHint(); | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
refs/remotes/origin/HEADis only a locally cached symbolic ref, not necessarily the remote's current default branch. I reproduced with Git 2.43.0 that after changing a bare remote frommastertomainand fetching normally, this command still returnsorigin/master; Git exposesgit remote set-head <name> --autoas a separate operation. Runningsites ci initfrom such a common post-rename clone therefore generates a workflow that listens to the obsolete branch, so pushes to the documented default branch never deploy; resolve the remote HEAD or refresh the cached symref first.AGENTS.md reference: AGENTS.md:L3-L5
Useful? React with 👍 / 👎.