Skip to content
Merged
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
6 changes: 6 additions & 0 deletions .sampo/changesets/debug-thaw-answers-thawed.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
cargo/devlanes: patch
cargo/devlanes-daemon: patch
---

Fixed `devlanes debug thaw` sometimes saying a frozen lane was not throttled or frozen after it had thawed it.
11 changes: 11 additions & 0 deletions .sampo/changesets/lane-project-variable.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
---
cargo/devlanes: minor
cargo/devlanes-cli: minor
cargo/devlanes-daemon: minor
cargo/devlanes-proto: minor
cargo/devlanes-resolve: minor
---

Changed lanes to export their project as `DEVLANES_LANE_PROJECT` instead of `DEVLANES_PROJECT`, so a lane's variables no longer pin devlanes commands in another project's folder or checkout to that project.

`DEVLANES_PROJECT` is now only a setting you export yourself, and it wins over the current folder like `-p`. Scripts that read the lane's project from its environment use `DEVLANES_LANE_PROJECT`.
6 changes: 6 additions & 0 deletions .sampo/changesets/proxy-port-after-restart.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
cargo/devlanes: patch
cargo/devlanes-proxy: patch
---

Fixed the proxy moving to its fallback port (8443 or 8080) on macOS and Linux when the daemon restarted shortly after the proxy had closed connections, such as a browser's after `devlanes daemon restart`.
7 changes: 7 additions & 0 deletions .sampo/changesets/pty-leader-exits-at-once.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
cargo/devlanes: patch
cargo/devlanes-runner-pty: patch
cargo/devlanes-sys: patch
---

Fixed `devlanes start` on macOS sometimes waiting until its timeout, and `devlanes daemon stop` stalling after it, when a service printed something and exited right after it started, instead of reporting it as failed with its exit code and output.
2 changes: 1 addition & 1 deletion agent-plugin/skills/devlanes/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ A unit is a service, task, `exec` command or `infra.external` entry, referenced

Output prints lanes as `acme@2 (feat/billing)`. Paste references back unchanged. A reference that matches nothing or several lanes fails with exit 3 and lists the candidates.

Commands use the project of the nearest `devlanes.yaml` above the current directory, up to the repository root. A repository can hold more projects: a `devlanes.yaml` in a subdirectory, or another file such as `devlanes.prod.yaml`, which is named once with `--file devlanes.prod.yaml` (`devlanes --file devlanes.prod.yaml start`) and afterwards reached with `-p PROJECT`. `-p` also reads `DEVLANES_PROJECT`, which devlanes sets in every process it starts, so devlanes commands run from a service or task stay in its project. Renaming `project:` makes a new project with new lanes, ports and hostnames; the old project's lanes stay listed until `devlanes lane gc --yes`, which you run only when the user agrees.
Commands use the project of the nearest `devlanes.yaml` above the current directory, up to the repository root. A repository can hold more projects: a `devlanes.yaml` in a subdirectory, or another file such as `devlanes.prod.yaml`, which is named once with `--file devlanes.prod.yaml` (`devlanes --file devlanes.prod.yaml start`) and afterwards reached with `-p PROJECT`. `-p` also reads `DEVLANES_PROJECT` when the user exports it. devlanes sets `DEVLANES_LANE_PROJECT` in every process a lane starts, so devlanes commands run from a service or task stay in its project; in another project's folder or checkout that folder's project applies. Renaming `project:` makes a new project with new lanes, ports and hostnames; the old project's lanes stay listed until `devlanes lane gc --yes`, which you run only when the user agrees.

## Commands

Expand Down
2 changes: 1 addition & 1 deletion agent-plugin/skills/devlanes/references/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -142,7 +142,7 @@ Values may contain `${{ ... }}` (MiniJinja), evaluated per lane. Undefined names
| `lanes.main.services.<name>` | services of lane 0 |
| `projects.<project>.services.<name>` | services of another devlanes project |

Layers, lowest first: the client's environment, contributors (mise, direnv, Node, Python), devlanes' variables (`DEVLANES=1`, `DEVLANES_PROJECT`, `DEVLANES_LANE`, `DEVLANES_LANE_NAME`, `DEVLANES_LANE_DATA`, `DEVLANES_CA_CERT`, `DEVLANES_CA_BUNDLE`, and for services `PORT`, `HOST`, `DEVLANES_SERVICE`, `DEVLANES_ORIGINS`), `global_env`, the service's `env`, the profile's `env`, `--env` on the command line.
Layers, lowest first: the client's environment, contributors (mise, direnv, Node, Python), devlanes' variables (`DEVLANES=1`, `DEVLANES_LANE_PROJECT`, `DEVLANES_LANE`, `DEVLANES_LANE_NAME`, `DEVLANES_LANE_DATA`, `DEVLANES_CA_CERT`, `DEVLANES_CA_BUNDLE`, and for services `PORT`, `HOST`, `DEVLANES_SERVICE`, `DEVLANES_ORIGINS`), `global_env`, the service's `env`, the profile's `env`, `--env` on the command line.

`.env` is never injected into processes; it only feeds `${{ env.X }}`. Name what a process needs (`DB_PASSWORD: ${{ env.DB_PASSWORD }}`) or let the app load its own dotenv. mise `[env]` and an allowed `.envrc` are applied automatically, so do not copy them.

Expand Down
9 changes: 6 additions & 3 deletions bins/devlanes/src/choose.rs
Original file line number Diff line number Diff line change
Expand Up @@ -42,9 +42,12 @@ pub async fn choose(
args: &[String],
) -> Result<Chosen, RpcError> {
let cwd = context.cwd.display().to_string();
// Without --lane, --project or --file the lane comes from the directory; with no
// devlanes.yaml above it there is nothing to ask the daemon.
let scoped = context.lane.is_some() || context.project.is_some() || context.file.is_some();
// Without --lane, --project, --file or a lane's project the lane comes from the
// directory; with no devlanes.yaml above it there is nothing to ask the daemon.
let scoped = context.lane.is_some()
|| context.project.is_some()
|| context.lane_project.is_some()
|| context.file.is_some();
if choice.kind != ChoiceKind::Lane && !scoped && !has_config(&context.cwd).await {
emit(&no_options(choice, &NoOptions::NoConfig { cwd }, runtime));
return Ok(Chosen::Exit(devlanes_cli::exit::NOT_FOUND));
Expand Down
17 changes: 11 additions & 6 deletions crates/cli/src/args.rs
Original file line number Diff line number Diff line change
Expand Up @@ -73,9 +73,14 @@ pub struct Cli {
/// Output mode [default: agent when an AI agent runs devlanes, plain in CI or without a terminal, else human]
#[arg(long, global = true, value_enum, value_name = "MODE", help_heading = GLOBAL)]
pub output: Option<OutputMode>,
/// Use this project instead of the one containing the current directory [env: DEVLANES_PROJECT, which devlanes sets in the processes it starts]
/// Use this project instead of the one containing the current directory [env: DEVLANES_PROJECT]
#[arg(short = 'p', long, global = true, env = "DEVLANES_PROJECT", hide_env = true, value_name = "PROJECT", help_heading = GLOBAL)]
pub project: Option<String>,
/// `DEVLANES_LANE_PROJECT`, which lanes set in the processes they start. Unlike
/// `DEVLANES_PROJECT` it is only a fallback: it also reaches later CI steps and
/// `devlanes with` shells, which may run in another project's folder.
#[arg(skip = std::env::var("DEVLANES_LANE_PROJECT").ok().filter(|name| !name.is_empty()))]
pub lane_project: Option<String>,
/// Use this project file instead of the nearest devlanes.yaml (for example devlanes.prod.yaml); after the first use, -p <name> finds its project
// No `-f`: `logs -f` and the other `--follow` flags already take it.
#[arg(long, global = true, value_name = "FILE", help_heading = GLOBAL)]
Expand Down Expand Up @@ -1040,18 +1045,18 @@ impl Cli {
// The daemon resolves the file, so a relative one names a path below the cwd.
let file = self.file.as_ref().map(|file| cwd.join(file));
// The file names its project. `-p` is dropped rather than refused with clap's
// `conflicts_with`, which counts `DEVLANES_PROJECT` as given: inside a lane that
// env var names the lane's project and would point lookups at it instead.
let project = if file.is_some() {
None
// `conflicts_with`, which counts an exported `DEVLANES_PROJECT` as given.
let (project, lane_project) = if file.is_some() {
(None, None)
} else {
self.project.clone()
(self.project.clone(), self.lane_project.clone())
};
let mut context = Context {
cwd,
file,
actor: runtime.actor.clone(),
project,
lane_project,
lane: self.lane.clone(),
yes: self.yes,
env: None,
Expand Down
22 changes: 19 additions & 3 deletions crates/cli/tests/it/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -75,8 +75,8 @@ fn project_file_and_short_project_flags() {
}

#[test]
/// The file names its project, so `-p` and an inherited `DEVLANES_PROJECT` (which
/// clap reads into the same field) are left out instead of refused.
/// The file names its project, so `-p`, an exported `DEVLANES_PROJECT` (which clap
/// reads into the same field) and the lane's project are left out instead of refused.
fn project_file_sends_only_the_file() {
let mut cli = Cli::parse_from([
"devlanes",
Expand All @@ -89,11 +89,27 @@ fn project_file_sends_only_the_file() {
let req = request(action(&cli, &runtime(&cli, OutputMode::Agent)));
assert_eq!(req.context.project, None);
assert!(req.context.file.is_some());
// What `DEVLANES_PROJECT` of the lane this command runs in would set.
cli = Cli::parse_from(["devlanes", "--file", "devlanes.prod.yaml", "start"]);
cli.project = Some("acme".into());
cli.lane_project = Some("acme".into());
let req = request(action(&cli, &runtime(&cli, OutputMode::Agent)));
assert_eq!(req.context.project, None);
assert_eq!(req.context.lane_project, None);
}

#[test]
/// `-p` and `DEVLANES_PROJECT` name the project; the lane's `DEVLANES_LANE_PROJECT`
/// travels separately, so the daemon uses it only where the cwd can't decide.
fn lane_project_is_sent_apart_from_the_project() {
let mut cli = Cli::parse_from(["devlanes", "status"]);
cli.lane_project = Some("acme".into());
let req = request(action(&cli, &runtime(&cli, OutputMode::Agent)));
assert_eq!(req.context.project, None);
assert_eq!(req.context.lane_project.as_deref(), Some("acme"));
let mut cli = Cli::parse_from(["devlanes", "-p", "acme-prod", "status"]);
cli.lane_project = Some("acme".into());
let req = request(action(&cli, &runtime(&cli, OutputMode::Agent)));
assert_eq!(req.context.project.as_deref(), Some("acme-prod"));
}

#[test]
Expand Down
2 changes: 1 addition & 1 deletion crates/cli/tests/it/snapshots/it__help__help_config.snap
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ Global options:
- json: The daemon's data as JSON, or NDJSON for streams

-p, --project <PROJECT>
Use this project instead of the one containing the current directory [env: DEVLANES_PROJECT, which devlanes sets in the processes it starts]
Use this project instead of the one containing the current directory [env: DEVLANES_PROJECT]

--file <FILE>
Use this project file instead of the nearest devlanes.yaml (for example devlanes.prod.yaml); after the first use, -p <name> finds its project
Expand Down
2 changes: 1 addition & 1 deletion crates/cli/tests/it/snapshots/it__help__help_daemon.snap
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ Global options:
- json: The daemon's data as JSON, or NDJSON for streams

-p, --project <PROJECT>
Use this project instead of the one containing the current directory [env: DEVLANES_PROJECT, which devlanes sets in the processes it starts]
Use this project instead of the one containing the current directory [env: DEVLANES_PROJECT]

--file <FILE>
Use this project file instead of the nearest devlanes.yaml (for example devlanes.prod.yaml); after the first use, -p <name> finds its project
Expand Down
2 changes: 1 addition & 1 deletion crates/cli/tests/it/snapshots/it__help__help_debug.snap
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ Global options:
- json: The daemon's data as JSON, or NDJSON for streams

-p, --project <PROJECT>
Use this project instead of the one containing the current directory [env: DEVLANES_PROJECT, which devlanes sets in the processes it starts]
Use this project instead of the one containing the current directory [env: DEVLANES_PROJECT]

--file <FILE>
Use this project file instead of the nearest devlanes.yaml (for example devlanes.prod.yaml); after the first use, -p <name> finds its project
Expand Down
2 changes: 1 addition & 1 deletion crates/cli/tests/it/snapshots/it__help__help_devlanes.snap
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ Global options:
- json: The daemon's data as JSON, or NDJSON for streams

-p, --project <PROJECT>
Use this project instead of the one containing the current directory [env: DEVLANES_PROJECT, which devlanes sets in the processes it starts]
Use this project instead of the one containing the current directory [env: DEVLANES_PROJECT]

--file <FILE>
Use this project file instead of the nearest devlanes.yaml (for example devlanes.prod.yaml); after the first use, -p <name> finds its project
Expand Down
2 changes: 1 addition & 1 deletion crates/cli/tests/it/snapshots/it__help__help_env.snap
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ Global options:
- json: The daemon's data as JSON, or NDJSON for streams

-p, --project <PROJECT>
Use this project instead of the one containing the current directory [env: DEVLANES_PROJECT, which devlanes sets in the processes it starts]
Use this project instead of the one containing the current directory [env: DEVLANES_PROJECT]

--file <FILE>
Use this project file instead of the nearest devlanes.yaml (for example devlanes.prod.yaml); after the first use, -p <name> finds its project
Expand Down
2 changes: 1 addition & 1 deletion crates/cli/tests/it/snapshots/it__help__help_infra.snap
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ Global options:
- json: The daemon's data as JSON, or NDJSON for streams

-p, --project <PROJECT>
Use this project instead of the one containing the current directory [env: DEVLANES_PROJECT, which devlanes sets in the processes it starts]
Use this project instead of the one containing the current directory [env: DEVLANES_PROJECT]

--file <FILE>
Use this project file instead of the nearest devlanes.yaml (for example devlanes.prod.yaml); after the first use, -p <name> finds its project
Expand Down
2 changes: 1 addition & 1 deletion crates/cli/tests/it/snapshots/it__help__help_lane.snap
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ Global options:
- json: The daemon's data as JSON, or NDJSON for streams

-p, --project <PROJECT>
Use this project instead of the one containing the current directory [env: DEVLANES_PROJECT, which devlanes sets in the processes it starts]
Use this project instead of the one containing the current directory [env: DEVLANES_PROJECT]

--file <FILE>
Use this project file instead of the nearest devlanes.yaml (for example devlanes.prod.yaml); after the first use, -p <name> finds its project
Expand Down
2 changes: 1 addition & 1 deletion crates/cli/tests/it/snapshots/it__help__help_logs.snap
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@ Global options:
- json: The daemon's data as JSON, or NDJSON for streams

-p, --project <PROJECT>
Use this project instead of the one containing the current directory [env: DEVLANES_PROJECT, which devlanes sets in the processes it starts]
Use this project instead of the one containing the current directory [env: DEVLANES_PROJECT]

--file <FILE>
Use this project file instead of the nearest devlanes.yaml (for example devlanes.prod.yaml); after the first use, -p <name> finds its project
Expand Down
2 changes: 1 addition & 1 deletion crates/cli/tests/it/snapshots/it__help__help_remote.snap
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ Global options:
- json: The daemon's data as JSON, or NDJSON for streams

-p, --project <PROJECT>
Use this project instead of the one containing the current directory [env: DEVLANES_PROJECT, which devlanes sets in the processes it starts]
Use this project instead of the one containing the current directory [env: DEVLANES_PROJECT]

--file <FILE>
Use this project file instead of the nearest devlanes.yaml (for example devlanes.prod.yaml); after the first use, -p <name> finds its project
Expand Down
2 changes: 1 addition & 1 deletion crates/cli/tests/it/snapshots/it__help__help_setup.snap
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ Global options:
- json: The daemon's data as JSON, or NDJSON for streams

-p, --project <PROJECT>
Use this project instead of the one containing the current directory [env: DEVLANES_PROJECT, which devlanes sets in the processes it starts]
Use this project instead of the one containing the current directory [env: DEVLANES_PROJECT]

--file <FILE>
Use this project file instead of the nearest devlanes.yaml (for example devlanes.prod.yaml); after the first use, -p <name> finds its project
Expand Down
2 changes: 1 addition & 1 deletion crates/cli/tests/it/snapshots/it__help__help_start.snap
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ Global options:
- json: The daemon's data as JSON, or NDJSON for streams

-p, --project <PROJECT>
Use this project instead of the one containing the current directory [env: DEVLANES_PROJECT, which devlanes sets in the processes it starts]
Use this project instead of the one containing the current directory [env: DEVLANES_PROJECT]

--file <FILE>
Use this project file instead of the nearest devlanes.yaml (for example devlanes.prod.yaml); after the first use, -p <name> finds its project
Expand Down
2 changes: 1 addition & 1 deletion crates/cli/tests/it/snapshots/it__help__help_url.snap
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ Global options:
- json: The daemon's data as JSON, or NDJSON for streams

-p, --project <PROJECT>
Use this project instead of the one containing the current directory [env: DEVLANES_PROJECT, which devlanes sets in the processes it starts]
Use this project instead of the one containing the current directory [env: DEVLANES_PROJECT]

--file <FILE>
Use this project file instead of the nearest devlanes.yaml (for example devlanes.prod.yaml); after the first use, -p <name> finds its project
Expand Down
2 changes: 1 addition & 1 deletion crates/cli/tests/it/snapshots/it__help__help_with.snap
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ Global options:
- json: The daemon's data as JSON, or NDJSON for streams

-p, --project <PROJECT>
Use this project instead of the one containing the current directory [env: DEVLANES_PROJECT, which devlanes sets in the processes it starts]
Use this project instead of the one containing the current directory [env: DEVLANES_PROJECT]

--file <FILE>
Use this project file instead of the nearest devlanes.yaml (for example devlanes.prod.yaml); after the first use, -p <name> finds its project
Expand Down
4 changes: 2 additions & 2 deletions crates/config/schema.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion crates/config/src/model.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ pub struct ProjectConfig<E: Extensions = OpaqueExtensions> {
/// Project name, the same in every worktree of the repository. It builds each lane's
/// default domain (`<project>.localhost` for lane 0, `<project><N>.localhost` for lane N),
/// names lanes in commands (`acme@2`), the containers devlanes creates and
/// `DEVLANES_PROJECT`, and other projects reach this one as `projects.<project>`.
/// `DEVLANES_LANE_PROJECT`, and other projects reach this one as `projects.<project>`.
/// Lowercase letters, digits, dots, underscores and dashes.
#[schemars(extend("examples" = ["acme"]))]
pub project: String,
Expand Down
5 changes: 4 additions & 1 deletion crates/daemon/src/idle.rs
Original file line number Diff line number Diff line change
Expand Up @@ -698,7 +698,10 @@ impl Inner {
let board = &self.opts.idle;
let reference = lane.st().reference(&project.name());
if let Some(activity) = activity {
board.activity(&reference, activity);
// Without queueing a wake: this call thaws the lane itself, and a
// queued wake could take the lock first and leave this call
// answering that the lane was not idle.
board.record(&board.slot(&reference), activity);
}
// Always under the lock: a freeze in progress holds it until the lane's
// state and the board agree, and the board is only written under it.
Expand Down
Loading
Loading