Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -248,8 +248,11 @@ async function readConfig(
allowNodeModules: boolean,
debug: boolean,
): Promise<Config> {
// Only `--config <file>` is parsed as a config file; a positional root is not.
const fromConfig = Boolean(maybeConfigPath);
const config = resolve_config(
resolve(maybeConfigPath || rootPath),
fromConfig,
ignorePaths,
allowNodeModules,
debug,
Expand Down
6 changes: 4 additions & 2 deletions deploy/create/mod.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ import {

import { publish, waitForRevision } from "../publish.ts";
import { resolve } from "@std/path";
import { error, writeJsonResult } from "../../util.ts";
import { deployRootDir, error, writeJsonResult } from "../../util.ts";
import { green } from "@std/fmt/colors";

export const createCommand = new Command<GlobalContext>()
Expand Down Expand Up @@ -180,7 +180,9 @@ export const createCommand = new Command<GlobalContext>()
},
)
.arguments("[root-path:string]")
.action(actionHandler(async (config, options, rootPath = Deno.cwd()) => {
.action(actionHandler(async (config, options, rawRootPath = Deno.cwd()) => {
// A positional file argument (e.g. `main.ts`) deploys its directory.
const rootPath = deployRootDir(rawRootPath);
await getAuth(options);
let data;
if (
Expand Down
5 changes: 4 additions & 1 deletion deploy/mod.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { Command, ValidationError } from "@cliffy/command";
import { green, red, setColorEnabled, yellow } from "@std/fmt/colors";
import {
deployRootDir,
error,
renderTemporalTimestamp,
tablePrinter,
Expand Down Expand Up @@ -402,7 +403,9 @@ for the full reference.`)
})
.action(
actionHandler(
async (config, options, rootPath = Deno.cwd()) => {
async (config, options, rawRootPath = Deno.cwd()) => {
// A positional file argument (e.g. `main.ts`) deploys its directory.
const rootPath = deployRootDir(rawRootPath);
const org = await getOrg(options, config, options.org);
const { app, created } = await getApp(
options,
Expand Down
123 changes: 114 additions & 9 deletions rs_lib/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -25,40 +25,54 @@ pub struct ConfigLookup {
#[wasm_bindgen]
pub fn resolve_config(
root_path: String,
from_config: bool,
ignore_paths: Vec<String>,
allow_node_modules: bool,
debug: bool,
) -> Result<JsValue, JsValue> {
let result =
inner_resolve_config(root_path, ignore_paths, allow_node_modules, debug);
let result = inner_resolve_config(
root_path,
from_config,
ignore_paths,
allow_node_modules,
debug,
);
result
.map_err(|err| create_js_error(&err))
.map(|val| serde_wasm_bindgen::to_value(&val).unwrap())
}

fn inner_resolve_config(
root_path: String,
from_config: bool,
ignore_paths: Vec<String>,
allow_node_modules: bool,
debug: bool,
) -> Result<ConfigLookup, anyhow::Error> {
debug_log(
debug,
&format!(
"resolve_config(root_path={:?}, ignore_paths={:?}, allow_node_modules={})",
root_path, ignore_paths, allow_node_modules
"resolve_config(root_path={:?}, from_config={}, ignore_paths={:?}, allow_node_modules={})",
root_path, from_config, ignore_paths, allow_node_modules
),
);

let real_sys = sys_traits::impls::RealSys;
let root_path = resolve_absolute_path(root_path)?;
debug_log(debug, &format!("resolved absolute root_path={:?}", root_path));

// When --config points to a file (not a directory), use ConfigFile
// discovery so non-standard filenames like deno-staging.json work.
let is_config_file = real_sys.fs_is_file(&root_path).unwrap_or(false);
debug_log(debug, &format!("is_config_file={}", is_config_file));
let dir_path = if is_config_file {
let path_is_file = real_sys.fs_is_file(&root_path).unwrap_or(false);
// Only `--config <file>` is parsed as a config file; a positional file root
// is a deploy target whose config is discovered from its parent directory.
let is_config_file = from_config && path_is_file;
debug_log(
debug,
&format!(
"path_is_file={} is_config_file={}",
path_is_file, is_config_file
),
);
let dir_path = if path_is_file {
root_path.parent().unwrap().to_path_buf()
} else {
root_path.clone()
Expand Down Expand Up @@ -284,6 +298,7 @@ mod tests {

let result = inner_resolve_config(
root.to_string_lossy().into_owned(),
false,
Vec::new(),
false,
false,
Expand All @@ -301,4 +316,94 @@ mod tests {
result.files,
);
}

// Regression test for denoland/deploy-cli#107: a positional file root (e.g.
// `deno deploy main.ts`) must not be deserialized as a config file. Config is
// discovered from the file's parent directory and the file itself is part of
// the upload manifest. Before the fix this errored with "Failed deserializing
// config file" because any file path was treated as a config file.
#[test]
fn positional_file_root_discovers_parent_config() {
let temp = TempDir::new().unwrap();
let root = temp.path();
write_file(
root,
"deno.json",
r#"{ "deploy": { "org": "myorg", "app": "myapp" } }"#,
);
write_file(root, "main.ts", "Deno.serve(() => new Response('hello'));");

let entry = root.join("main.ts");
let result = inner_resolve_config(
entry.to_string_lossy().into_owned(),
false,
Vec::new(),
false,
false,
)
.unwrap();

let config_path = result
.path
.as_deref()
.expect("expected a discovered config path");
assert!(
config_path.ends_with("deno.json"),
"expected parent deno.json as config; got {}",
config_path,
);
assert!(
result
.files
.iter()
.any(|f| Path::new(f) == entry.as_path()),
"expected {} in deploy files; got {:?}",
entry.display(),
result.files,
);
}

// A non-standard config filename passed via `--config` must still use
// ConfigFile discovery so it is loaded directly regardless of its name.
#[test]
fn explicit_config_flag_uses_named_config_file() {
let temp = TempDir::new().unwrap();
let root = temp.path();
write_file(
root,
"deno-staging.json",
r#"{ "deploy": { "org": "myorg", "app": "staging" } }"#,
);
write_file(root, "main.ts", "Deno.serve(() => new Response('hello'));");

let config = root.join("deno-staging.json");
let result = inner_resolve_config(
config.to_string_lossy().into_owned(),
true,
Vec::new(),
false,
false,
)
.unwrap();

let config_path = result
.path
.as_deref()
.expect("expected a discovered config path");
assert!(
config_path.ends_with("deno-staging.json"),
"expected deno-staging.json as config; got {}",
config_path,
);
let entry = root.join("main.ts");
assert!(
result
.files
.iter()
.any(|f| Path::new(f) == entry.as_path()),
"expected {} in deploy files; got {:?}",
entry.display(),
result.files,
);
}
}
10 changes: 10 additions & 0 deletions util.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,18 @@
import { red, stripAnsiCode } from "@std/fmt/colors";
import { dirname } from "@std/path";
import { Temporal } from "temporal-polyfill";

import type { GlobalContext } from "./main.ts";

/** Resolve a deploy root to a directory: a file argument maps to its parent. */
export function deployRootDir(path: string): string {
try {
return Deno.statSync(path).isFile ? dirname(path) : path;
} catch {
return path;
}
}

/**
* Exit codes returned by the CLI. Agents pattern-match on these before parsing
* stderr. Keep this list small and stable — new categories require a docs bump.
Expand Down
Loading