workdir: report current selector and ref
This commit is contained in:
@@ -119,13 +119,15 @@ pub struct BackendWorkingDirectorySummary {
|
|||||||
pub working_directory_id: String,
|
pub working_directory_id: String,
|
||||||
pub repository_id: String,
|
pub repository_id: String,
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub requested_selector: Option<String>,
|
pub creation_selector: Option<String>,
|
||||||
|
#[serde(default)]
|
||||||
|
pub creation_ref: Option<String>,
|
||||||
|
#[serde(default)]
|
||||||
|
pub current_selector: Option<String>,
|
||||||
|
#[serde(default)]
|
||||||
|
pub current_ref: Option<String>,
|
||||||
pub materializer_kind: String,
|
pub materializer_kind: String,
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub resolved_commit: Option<String>,
|
|
||||||
#[serde(default)]
|
|
||||||
pub resolved_tree: Option<String>,
|
|
||||||
#[serde(default)]
|
|
||||||
pub cleanup_target: Option<BackendWorkingDirectoryCleanupTarget>,
|
pub cleanup_target: Option<BackendWorkingDirectoryCleanupTarget>,
|
||||||
pub status: String,
|
pub status: String,
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
|
|||||||
@@ -143,14 +143,20 @@ pub struct WorkingDirectoryOccupancy {
|
|||||||
pub struct WorkingDirectorySummary {
|
pub struct WorkingDirectorySummary {
|
||||||
pub working_directory_id: String,
|
pub working_directory_id: String,
|
||||||
pub repository_id: String,
|
pub repository_id: String,
|
||||||
|
/// Selector used to create this Workdir, retained as immutable materialization evidence.
|
||||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||||
pub requested_selector: Option<String>,
|
pub creation_selector: Option<String>,
|
||||||
|
/// Provider-specific immutable ref resolved when this Workdir was created.
|
||||||
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||||
|
pub creation_ref: Option<String>,
|
||||||
|
/// Selector currently observed from the materialized Workdir, when one exists.
|
||||||
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||||
|
pub current_selector: Option<String>,
|
||||||
|
/// Provider-specific immutable ref currently observed from the materialized Workdir.
|
||||||
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||||
|
pub current_ref: Option<String>,
|
||||||
pub materializer_kind: MaterializerKind,
|
pub materializer_kind: MaterializerKind,
|
||||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||||
pub resolved_commit: Option<String>,
|
|
||||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
|
||||||
pub resolved_tree: Option<String>,
|
|
||||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
|
||||||
pub cleanup_target: Option<WorkingDirectoryCleanupTarget>,
|
pub cleanup_target: Option<WorkingDirectoryCleanupTarget>,
|
||||||
pub status: WorkingDirectoryStatusKind,
|
pub status: WorkingDirectoryStatusKind,
|
||||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||||
|
|||||||
@@ -40,10 +40,11 @@ impl WorkingDirectory {
|
|||||||
WorkingDirectorySummary {
|
WorkingDirectorySummary {
|
||||||
working_directory_id: self.id.clone(),
|
working_directory_id: self.id.clone(),
|
||||||
repository_id: self.repository_id.clone(),
|
repository_id: self.repository_id.clone(),
|
||||||
requested_selector: self.evidence.requested_selector.clone(),
|
creation_selector: self.evidence.requested_selector.clone(),
|
||||||
|
creation_ref: Some(self.evidence.resolved_commit.clone()),
|
||||||
|
current_selector: None,
|
||||||
|
current_ref: None,
|
||||||
materializer_kind: self.materializer_kind.clone(),
|
materializer_kind: self.materializer_kind.clone(),
|
||||||
resolved_commit: Some(self.evidence.resolved_commit.clone()),
|
|
||||||
resolved_tree: self.evidence.resolved_tree.clone(),
|
|
||||||
cleanup_target: Some(self.cleanup_target.clone()),
|
cleanup_target: Some(self.cleanup_target.clone()),
|
||||||
status: self.status.clone(),
|
status: self.status.clone(),
|
||||||
cleanliness: None,
|
cleanliness: None,
|
||||||
@@ -88,6 +89,9 @@ impl WorkingDirectoryBinding {
|
|||||||
}
|
}
|
||||||
let mut summary = working_directory.status_summary();
|
let mut summary = working_directory.status_summary();
|
||||||
summary.cleanliness = if summary.status == WorkingDirectoryStatusKind::Active {
|
summary.cleanliness = if summary.status == WorkingDirectoryStatusKind::Active {
|
||||||
|
let (current_selector, current_ref) = binding_current_revision(self);
|
||||||
|
summary.current_selector = current_selector;
|
||||||
|
summary.current_ref = current_ref;
|
||||||
Some(binding_cleanliness(self))
|
Some(binding_cleanliness(self))
|
||||||
} else {
|
} else {
|
||||||
Some("unknown".to_string())
|
Some("unknown".to_string())
|
||||||
@@ -171,6 +175,22 @@ fn binding_paths_are_available(binding: &WorkingDirectoryBinding) -> bool {
|
|||||||
source_repository_path.is_dir()
|
source_repository_path.is_dir()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn binding_current_revision(binding: &WorkingDirectoryBinding) -> (Option<String>, Option<String>) {
|
||||||
|
let current_ref = git_stdout(binding.root(), ["rev-parse", "HEAD"])
|
||||||
|
.ok()
|
||||||
|
.filter(|value| !value.is_empty());
|
||||||
|
if current_ref.is_none() {
|
||||||
|
return (None, None);
|
||||||
|
}
|
||||||
|
let current_selector = git_stdout(
|
||||||
|
binding.root(),
|
||||||
|
["symbolic-ref", "--short", "--quiet", "HEAD"],
|
||||||
|
)
|
||||||
|
.ok()
|
||||||
|
.filter(|value| !value.is_empty());
|
||||||
|
(current_selector, current_ref)
|
||||||
|
}
|
||||||
|
|
||||||
fn binding_cleanliness(binding: &WorkingDirectoryBinding) -> String {
|
fn binding_cleanliness(binding: &WorkingDirectoryBinding) -> String {
|
||||||
match git_stdout(binding.root(), ["status", "--porcelain"]) {
|
match git_stdout(binding.root(), ["status", "--porcelain"]) {
|
||||||
Ok(output) if output.is_empty() => "clean".to_string(),
|
Ok(output) if output.is_empty() => "clean".to_string(),
|
||||||
@@ -208,10 +228,11 @@ impl LocalGitWorktreeMaterializer {
|
|||||||
summary: WorkingDirectorySummary {
|
summary: WorkingDirectorySummary {
|
||||||
working_directory_id: working_directory_id.to_string(),
|
working_directory_id: working_directory_id.to_string(),
|
||||||
repository_id: "unknown".to_string(),
|
repository_id: "unknown".to_string(),
|
||||||
requested_selector: None,
|
creation_selector: None,
|
||||||
|
creation_ref: None,
|
||||||
|
current_selector: None,
|
||||||
|
current_ref: None,
|
||||||
materializer_kind: MaterializerKind::LocalGitWorktree,
|
materializer_kind: MaterializerKind::LocalGitWorktree,
|
||||||
resolved_commit: None,
|
|
||||||
resolved_tree: None,
|
|
||||||
cleanup_target: Some(WorkingDirectoryCleanupTarget {
|
cleanup_target: Some(WorkingDirectoryCleanupTarget {
|
||||||
kind: "local_git_worktree".to_string(),
|
kind: "local_git_worktree".to_string(),
|
||||||
working_directory_id: working_directory_id.to_string(),
|
working_directory_id: working_directory_id.to_string(),
|
||||||
@@ -930,12 +951,39 @@ mod tests {
|
|||||||
listed[0].summary.working_directory_id,
|
listed[0].summary.working_directory_id,
|
||||||
working_directory.working_directory.id
|
working_directory.working_directory.id
|
||||||
);
|
);
|
||||||
|
assert_eq!(listed[0].summary.creation_selector.as_deref(), Some("HEAD"));
|
||||||
|
assert_eq!(listed[0].summary.current_selector, None);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
listed[0].summary.requested_selector.as_deref(),
|
listed[0].summary.current_ref,
|
||||||
Some("HEAD")
|
listed[0].summary.creation_ref
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn working_directory_observes_current_selector_and_ref_without_changing_creation_evidence() {
|
||||||
|
let repo = create_clean_repo();
|
||||||
|
let runtime_root = tempfile::tempdir().unwrap();
|
||||||
|
let materializer = LocalGitWorktreeMaterializer::new(runtime_root.path());
|
||||||
|
let working_directory = materializer.create(&request(repo.path())).unwrap();
|
||||||
|
let bound = materializer
|
||||||
|
.bind_working_directory(&working_directory.working_directory.id, None)
|
||||||
|
.unwrap();
|
||||||
|
let initial_ref = bound.status().summary.creation_ref.expect("creation ref");
|
||||||
|
|
||||||
|
git(&bound.root, &["switch", "-c", "observed-branch"]);
|
||||||
|
fs::write(bound.root.join("observed.txt"), "observed\n").unwrap();
|
||||||
|
git(&bound.root, &["add", "observed.txt"]);
|
||||||
|
git(&bound.root, &["commit", "-m", "advance workdir"]);
|
||||||
|
|
||||||
|
let summary = materializer.list_working_directories().unwrap()[0]
|
||||||
|
.summary
|
||||||
|
.clone();
|
||||||
|
assert_eq!(summary.creation_selector.as_deref(), Some("HEAD"));
|
||||||
|
assert_eq!(summary.creation_ref.as_deref(), Some(initial_ref.as_str()));
|
||||||
|
assert_eq!(summary.current_selector.as_deref(), Some("observed-branch"));
|
||||||
|
assert_ne!(summary.current_ref.as_deref(), Some(initial_ref.as_str()));
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn relative_cwd_rejects_absolute_parent_nonexistent_file_and_symlink_escape() {
|
fn relative_cwd_rejects_absolute_parent_nonexistent_file_and_symlink_escape() {
|
||||||
let repo = create_clean_repo();
|
let repo = create_clean_repo();
|
||||||
|
|||||||
@@ -2381,12 +2381,14 @@ fn create_working_directory_for_runtime(
|
|||||||
workdir_id: workdir_id.clone(),
|
workdir_id: workdir_id.clone(),
|
||||||
runtime_id: runtime_id.clone(),
|
runtime_id: runtime_id.clone(),
|
||||||
repository_id: working_directory_request.repository.id.clone(),
|
repository_id: working_directory_request.repository.id.clone(),
|
||||||
selector: working_directory_request
|
creation_selector: working_directory_request
|
||||||
.repository
|
.repository
|
||||||
.selector
|
.selector
|
||||||
.as_ref()
|
.as_ref()
|
||||||
.map(|selector| selector.as_ref().to_string()),
|
.map(|selector| selector.as_ref().to_string()),
|
||||||
resolved_commit: None,
|
creation_ref: None,
|
||||||
|
current_selector: None,
|
||||||
|
current_ref: None,
|
||||||
materialization_status: "pending".to_string(),
|
materialization_status: "pending".to_string(),
|
||||||
cleanliness: "unknown".to_string(),
|
cleanliness: "unknown".to_string(),
|
||||||
created_at: now_registry_timestamp(),
|
created_at: now_registry_timestamp(),
|
||||||
@@ -6264,12 +6266,14 @@ fn upsert_pending_backend_workdir(
|
|||||||
workdir_id: workdir_id.clone(),
|
workdir_id: workdir_id.clone(),
|
||||||
runtime_id: runtime_id.to_string(),
|
runtime_id: runtime_id.to_string(),
|
||||||
repository_id: request.repository.id.clone(),
|
repository_id: request.repository.id.clone(),
|
||||||
selector: request
|
creation_selector: request
|
||||||
.repository
|
.repository
|
||||||
.selector
|
.selector
|
||||||
.as_ref()
|
.as_ref()
|
||||||
.map(|selector| selector.as_ref().to_string()),
|
.map(|selector| selector.as_ref().to_string()),
|
||||||
resolved_commit: None,
|
creation_ref: None,
|
||||||
|
current_selector: None,
|
||||||
|
current_ref: None,
|
||||||
materialization_status: "pending".to_string(),
|
materialization_status: "pending".to_string(),
|
||||||
cleanliness: "unknown".to_string(),
|
cleanliness: "unknown".to_string(),
|
||||||
created_at: timestamp.clone(),
|
created_at: timestamp.clone(),
|
||||||
@@ -6417,8 +6421,10 @@ fn workdir_record_from_summary(
|
|||||||
workdir_id: summary.working_directory_id.clone(),
|
workdir_id: summary.working_directory_id.clone(),
|
||||||
runtime_id: runtime_id.to_string(),
|
runtime_id: runtime_id.to_string(),
|
||||||
repository_id: summary.repository_id.clone(),
|
repository_id: summary.repository_id.clone(),
|
||||||
selector: summary.requested_selector.clone(),
|
creation_selector: summary.creation_selector.clone(),
|
||||||
resolved_commit: summary.resolved_commit.clone(),
|
creation_ref: summary.creation_ref.clone(),
|
||||||
|
current_selector: summary.current_selector.clone(),
|
||||||
|
current_ref: summary.current_ref.clone(),
|
||||||
materialization_status: match summary.status {
|
materialization_status: match summary.status {
|
||||||
WorkingDirectoryStatusKind::Active => "present",
|
WorkingDirectoryStatusKind::Active => "present",
|
||||||
WorkingDirectoryStatusKind::CleanupPending => "pending",
|
WorkingDirectoryStatusKind::CleanupPending => "pending",
|
||||||
@@ -6449,11 +6455,11 @@ fn preserve_workdir_identity_for_corrupted_summary(
|
|||||||
if record.repository_id == "unknown" {
|
if record.repository_id == "unknown" {
|
||||||
record.repository_id = existing.repository_id.clone();
|
record.repository_id = existing.repository_id.clone();
|
||||||
}
|
}
|
||||||
if record.selector.is_none() {
|
if record.creation_selector.is_none() {
|
||||||
record.selector = existing.selector.clone();
|
record.creation_selector = existing.creation_selector.clone();
|
||||||
}
|
}
|
||||||
if record.resolved_commit.is_none() {
|
if record.creation_ref.is_none() {
|
||||||
record.resolved_commit = existing.resolved_commit.clone();
|
record.creation_ref = existing.creation_ref.clone();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -6469,10 +6475,11 @@ fn workdir_summary_from_record(record: &WorkdirRegistryRecord) -> WorkingDirecto
|
|||||||
WorkingDirectorySummary {
|
WorkingDirectorySummary {
|
||||||
working_directory_id: record.workdir_id.clone(),
|
working_directory_id: record.workdir_id.clone(),
|
||||||
repository_id: record.repository_id.clone(),
|
repository_id: record.repository_id.clone(),
|
||||||
requested_selector: record.selector.clone(),
|
creation_selector: record.creation_selector.clone(),
|
||||||
|
creation_ref: record.creation_ref.clone(),
|
||||||
|
current_selector: record.current_selector.clone(),
|
||||||
|
current_ref: record.current_ref.clone(),
|
||||||
materializer_kind: MaterializerKind::LocalGitWorktree,
|
materializer_kind: MaterializerKind::LocalGitWorktree,
|
||||||
resolved_commit: record.resolved_commit.clone(),
|
|
||||||
resolved_tree: None,
|
|
||||||
cleanup_target: Some(worker_runtime::catalog::WorkingDirectoryCleanupTarget {
|
cleanup_target: Some(worker_runtime::catalog::WorkingDirectoryCleanupTarget {
|
||||||
kind: "local_git_worktree".to_string(),
|
kind: "local_git_worktree".to_string(),
|
||||||
working_directory_id: record.workdir_id.clone(),
|
working_directory_id: record.workdir_id.clone(),
|
||||||
@@ -7110,8 +7117,10 @@ mod tests {
|
|||||||
workdir_id: "0000019a00000000000".to_string(),
|
workdir_id: "0000019a00000000000".to_string(),
|
||||||
runtime_id: "embedded".to_string(),
|
runtime_id: "embedded".to_string(),
|
||||||
repository_id: "repo".to_string(),
|
repository_id: "repo".to_string(),
|
||||||
selector: Some("develop".to_string()),
|
creation_selector: Some("develop".to_string()),
|
||||||
resolved_commit: Some("abcdef".to_string()),
|
creation_ref: Some("abcdef".to_string()),
|
||||||
|
current_selector: None,
|
||||||
|
current_ref: Some("fedcba".to_string()),
|
||||||
materialization_status: "missing".to_string(),
|
materialization_status: "missing".to_string(),
|
||||||
cleanliness: "clean".to_string(),
|
cleanliness: "clean".to_string(),
|
||||||
created_at: "1".to_string(),
|
created_at: "1".to_string(),
|
||||||
@@ -7135,6 +7144,13 @@ mod tests {
|
|||||||
working_directory.status,
|
working_directory.status,
|
||||||
WorkingDirectoryStatusKind::NotFound
|
WorkingDirectoryStatusKind::NotFound
|
||||||
);
|
);
|
||||||
|
assert_eq!(
|
||||||
|
working_directory.creation_selector.as_deref(),
|
||||||
|
Some("develop")
|
||||||
|
);
|
||||||
|
assert_eq!(working_directory.creation_ref.as_deref(), Some("abcdef"));
|
||||||
|
assert_eq!(working_directory.current_selector, None);
|
||||||
|
assert_eq!(working_directory.current_ref.as_deref(), Some("fedcba"));
|
||||||
let occupied_by = working_directory.occupied_by.as_ref().unwrap();
|
let occupied_by = working_directory.occupied_by.as_ref().unwrap();
|
||||||
assert_eq!(occupied_by.runtime_id, "embedded");
|
assert_eq!(occupied_by.runtime_id, "embedded");
|
||||||
assert_eq!(occupied_by.runtime_worker_id, 1);
|
assert_eq!(occupied_by.runtime_worker_id, 1);
|
||||||
@@ -7155,8 +7171,10 @@ mod tests {
|
|||||||
workdir_id: "managed".to_string(),
|
workdir_id: "managed".to_string(),
|
||||||
runtime_id: EMBEDDED_WORKER_RUNTIME_ID.to_string(),
|
runtime_id: EMBEDDED_WORKER_RUNTIME_ID.to_string(),
|
||||||
repository_id: "repo".to_string(),
|
repository_id: "repo".to_string(),
|
||||||
selector: None,
|
creation_selector: None,
|
||||||
resolved_commit: None,
|
creation_ref: None,
|
||||||
|
current_selector: None,
|
||||||
|
current_ref: None,
|
||||||
materialization_status: "present".to_string(),
|
materialization_status: "present".to_string(),
|
||||||
cleanliness: "clean".to_string(),
|
cleanliness: "clean".to_string(),
|
||||||
created_at: "1".to_string(),
|
created_at: "1".to_string(),
|
||||||
@@ -7169,8 +7187,10 @@ mod tests {
|
|||||||
workdir_id: "runtime-direct".to_string(),
|
workdir_id: "runtime-direct".to_string(),
|
||||||
runtime_id: EMBEDDED_WORKER_RUNTIME_ID.to_string(),
|
runtime_id: EMBEDDED_WORKER_RUNTIME_ID.to_string(),
|
||||||
repository_id: "repo".to_string(),
|
repository_id: "repo".to_string(),
|
||||||
selector: None,
|
creation_selector: None,
|
||||||
resolved_commit: None,
|
creation_ref: None,
|
||||||
|
current_selector: None,
|
||||||
|
current_ref: None,
|
||||||
materialization_status: "present".to_string(),
|
materialization_status: "present".to_string(),
|
||||||
cleanliness: "unknown".to_string(),
|
cleanliness: "unknown".to_string(),
|
||||||
created_at: "1".to_string(),
|
created_at: "1".to_string(),
|
||||||
@@ -7240,8 +7260,10 @@ mod tests {
|
|||||||
workdir_id: "runtime-direct".to_string(),
|
workdir_id: "runtime-direct".to_string(),
|
||||||
runtime_id: "embedded".to_string(),
|
runtime_id: "embedded".to_string(),
|
||||||
repository_id: "repo".to_string(),
|
repository_id: "repo".to_string(),
|
||||||
selector: None,
|
creation_selector: None,
|
||||||
resolved_commit: None,
|
creation_ref: None,
|
||||||
|
current_selector: None,
|
||||||
|
current_ref: None,
|
||||||
materialization_status: "present".to_string(),
|
materialization_status: "present".to_string(),
|
||||||
cleanliness: "unknown".to_string(),
|
cleanliness: "unknown".to_string(),
|
||||||
created_at: "1".to_string(),
|
created_at: "1".to_string(),
|
||||||
@@ -8236,8 +8258,10 @@ mod tests {
|
|||||||
workdir_id: workdir_id.to_string(),
|
workdir_id: workdir_id.to_string(),
|
||||||
runtime_id: "runtime-test".to_string(),
|
runtime_id: "runtime-test".to_string(),
|
||||||
repository_id: "repo-test".to_string(),
|
repository_id: "repo-test".to_string(),
|
||||||
selector: Some("HEAD".to_string()),
|
creation_selector: Some("HEAD".to_string()),
|
||||||
resolved_commit: None,
|
creation_ref: None,
|
||||||
|
current_selector: None,
|
||||||
|
current_ref: None,
|
||||||
materialization_status: status.to_string(),
|
materialization_status: status.to_string(),
|
||||||
cleanliness: cleanliness.to_string(),
|
cleanliness: cleanliness.to_string(),
|
||||||
created_at: now.clone(),
|
created_at: now.clone(),
|
||||||
|
|||||||
@@ -87,6 +87,11 @@ const MIGRATIONS: &[Migration] = &[
|
|||||||
name: "remove unused control-plane Ticket tables",
|
name: "remove unused control-plane Ticket tables",
|
||||||
apply: remove_unused_control_plane_ticket_tables,
|
apply: remove_unused_control_plane_ticket_tables,
|
||||||
},
|
},
|
||||||
|
Migration {
|
||||||
|
version: 15,
|
||||||
|
name: "separate workdir creation evidence from current revision observation",
|
||||||
|
apply: add_workdir_revision_observations,
|
||||||
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
struct Migration {
|
struct Migration {
|
||||||
@@ -238,8 +243,10 @@ pub struct WorkdirRegistryRecord {
|
|||||||
pub workdir_id: String,
|
pub workdir_id: String,
|
||||||
pub runtime_id: String,
|
pub runtime_id: String,
|
||||||
pub repository_id: String,
|
pub repository_id: String,
|
||||||
pub selector: Option<String>,
|
pub creation_selector: Option<String>,
|
||||||
pub resolved_commit: Option<String>,
|
pub creation_ref: Option<String>,
|
||||||
|
pub current_selector: Option<String>,
|
||||||
|
pub current_ref: Option<String>,
|
||||||
pub materialization_status: String,
|
pub materialization_status: String,
|
||||||
pub cleanliness: String,
|
pub cleanliness: String,
|
||||||
pub created_at: String,
|
pub created_at: String,
|
||||||
@@ -1570,14 +1577,17 @@ impl ControlPlaneStore for SqliteWorkspaceStore {
|
|||||||
self.with_conn(|conn| {
|
self.with_conn(|conn| {
|
||||||
conn.execute(
|
conn.execute(
|
||||||
r#"INSERT INTO workdir_registry (
|
r#"INSERT INTO workdir_registry (
|
||||||
workspace_id, workdir_id, runtime_id, repository_id, selector, resolved_commit,
|
workspace_id, workdir_id, runtime_id, repository_id,
|
||||||
|
creation_selector, creation_ref, current_selector, current_ref,
|
||||||
materialization_status, cleanliness, created_at, updated_at
|
materialization_status, cleanliness, created_at, updated_at
|
||||||
) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10)
|
) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11, ?12)
|
||||||
ON CONFLICT(workspace_id, workdir_id) DO UPDATE SET
|
ON CONFLICT(workspace_id, workdir_id) DO UPDATE SET
|
||||||
runtime_id = excluded.runtime_id,
|
runtime_id = excluded.runtime_id,
|
||||||
repository_id = excluded.repository_id,
|
repository_id = excluded.repository_id,
|
||||||
selector = excluded.selector,
|
creation_selector = excluded.creation_selector,
|
||||||
resolved_commit = excluded.resolved_commit,
|
creation_ref = excluded.creation_ref,
|
||||||
|
current_selector = excluded.current_selector,
|
||||||
|
current_ref = excluded.current_ref,
|
||||||
materialization_status = excluded.materialization_status,
|
materialization_status = excluded.materialization_status,
|
||||||
cleanliness = excluded.cleanliness,
|
cleanliness = excluded.cleanliness,
|
||||||
updated_at = excluded.updated_at"#,
|
updated_at = excluded.updated_at"#,
|
||||||
@@ -1586,8 +1596,10 @@ impl ControlPlaneStore for SqliteWorkspaceStore {
|
|||||||
record.workdir_id,
|
record.workdir_id,
|
||||||
record.runtime_id,
|
record.runtime_id,
|
||||||
record.repository_id,
|
record.repository_id,
|
||||||
record.selector,
|
record.creation_selector,
|
||||||
record.resolved_commit,
|
record.creation_ref,
|
||||||
|
record.current_selector,
|
||||||
|
record.current_ref,
|
||||||
record.materialization_status,
|
record.materialization_status,
|
||||||
record.cleanliness,
|
record.cleanliness,
|
||||||
record.created_at,
|
record.created_at,
|
||||||
@@ -1998,7 +2010,8 @@ fn read_worker_registry_record(row: &rusqlite::Row<'_>) -> rusqlite::Result<Work
|
|||||||
|
|
||||||
fn workdir_registry_select_sql(where_clause: &str) -> String {
|
fn workdir_registry_select_sql(where_clause: &str) -> String {
|
||||||
format!(
|
format!(
|
||||||
"SELECT workspace_id, workdir_id, runtime_id, repository_id, selector, resolved_commit, \
|
"SELECT workspace_id, workdir_id, runtime_id, repository_id, \
|
||||||
|
creation_selector, creation_ref, current_selector, current_ref, \
|
||||||
materialization_status, cleanliness, created_at, updated_at \
|
materialization_status, cleanliness, created_at, updated_at \
|
||||||
FROM workdir_registry {where_clause}"
|
FROM workdir_registry {where_clause}"
|
||||||
)
|
)
|
||||||
@@ -2012,12 +2025,14 @@ fn read_workdir_registry_record(
|
|||||||
workdir_id: row.get(1)?,
|
workdir_id: row.get(1)?,
|
||||||
runtime_id: row.get(2)?,
|
runtime_id: row.get(2)?,
|
||||||
repository_id: row.get(3)?,
|
repository_id: row.get(3)?,
|
||||||
selector: row.get(4)?,
|
creation_selector: row.get(4)?,
|
||||||
resolved_commit: row.get(5)?,
|
creation_ref: row.get(5)?,
|
||||||
materialization_status: row.get(6)?,
|
current_selector: row.get(6)?,
|
||||||
cleanliness: row.get(7)?,
|
current_ref: row.get(7)?,
|
||||||
created_at: row.get(8)?,
|
materialization_status: row.get(8)?,
|
||||||
updated_at: row.get(9)?,
|
cleanliness: row.get(9)?,
|
||||||
|
created_at: row.get(10)?,
|
||||||
|
updated_at: row.get(11)?,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2175,6 +2190,18 @@ DROP TABLE IF EXISTS tickets;
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn add_workdir_revision_observations(conn: &Connection) -> Result<()> {
|
||||||
|
conn.execute_batch(
|
||||||
|
r#"
|
||||||
|
ALTER TABLE workdir_registry RENAME COLUMN selector TO creation_selector;
|
||||||
|
ALTER TABLE workdir_registry RENAME COLUMN resolved_commit TO creation_ref;
|
||||||
|
ALTER TABLE workdir_registry ADD COLUMN current_selector TEXT;
|
||||||
|
ALTER TABLE workdir_registry ADD COLUMN current_ref TEXT;
|
||||||
|
"#,
|
||||||
|
)?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
fn create_objective_event_tables(conn: &Connection) -> Result<()> {
|
fn create_objective_event_tables(conn: &Connection) -> Result<()> {
|
||||||
conn.execute_batch(
|
conn.execute_batch(
|
||||||
r#"
|
r#"
|
||||||
@@ -2857,7 +2884,7 @@ CREATE TABLE ticket_worker_links (ticket_id TEXT, worker_ref_key TEXT);
|
|||||||
let db = dir.path().join("control-plane.sqlite");
|
let db = dir.path().join("control-plane.sqlite");
|
||||||
let store = SqliteWorkspaceStore::open(&db).unwrap();
|
let store = SqliteWorkspaceStore::open(&db).unwrap();
|
||||||
|
|
||||||
assert_eq!(store.schema_version().await.unwrap(), 14);
|
assert_eq!(store.schema_version().await.unwrap(), 15);
|
||||||
|
|
||||||
let record = WorkspaceRecord {
|
let record = WorkspaceRecord {
|
||||||
workspace_id: "local-dev".to_string(),
|
workspace_id: "local-dev".to_string(),
|
||||||
@@ -2870,7 +2897,7 @@ CREATE TABLE ticket_worker_links (ticket_id TEXT, worker_ref_key TEXT);
|
|||||||
store.upsert_workspace(&record).await.unwrap();
|
store.upsert_workspace(&record).await.unwrap();
|
||||||
|
|
||||||
let reopened = SqliteWorkspaceStore::open(&db).unwrap();
|
let reopened = SqliteWorkspaceStore::open(&db).unwrap();
|
||||||
assert_eq!(reopened.schema_version().await.unwrap(), 14);
|
assert_eq!(reopened.schema_version().await.unwrap(), 15);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
reopened.get_workspace("local-dev").await.unwrap(),
|
reopened.get_workspace("local-dev").await.unwrap(),
|
||||||
Some(record)
|
Some(record)
|
||||||
@@ -2982,6 +3009,24 @@ CREATE TABLE ticket_worker_links (ticket_id TEXT, worker_ref_key TEXT);
|
|||||||
"updated_at",
|
"updated_at",
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
|
assert_columns(
|
||||||
|
&conn,
|
||||||
|
"workdir_registry",
|
||||||
|
[
|
||||||
|
"workspace_id",
|
||||||
|
"workdir_id",
|
||||||
|
"runtime_id",
|
||||||
|
"repository_id",
|
||||||
|
"creation_selector",
|
||||||
|
"creation_ref",
|
||||||
|
"materialization_status",
|
||||||
|
"cleanliness",
|
||||||
|
"created_at",
|
||||||
|
"updated_at",
|
||||||
|
"current_selector",
|
||||||
|
"current_ref",
|
||||||
|
],
|
||||||
|
);
|
||||||
assert_columns(
|
assert_columns(
|
||||||
&conn,
|
&conn,
|
||||||
"artifacts",
|
"artifacts",
|
||||||
@@ -3058,7 +3103,7 @@ CREATE TABLE ticket_worker_links (ticket_id TEXT, worker_ref_key TEXT);
|
|||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
let store = SqliteWorkspaceStore::from_connection(conn).unwrap();
|
let store = SqliteWorkspaceStore::from_connection(conn).unwrap();
|
||||||
assert_eq!(store.schema_version().await.unwrap(), 14);
|
assert_eq!(store.schema_version().await.unwrap(), 15);
|
||||||
|
|
||||||
store
|
store
|
||||||
.with_conn(|conn| {
|
.with_conn(|conn| {
|
||||||
@@ -3158,10 +3203,66 @@ CREATE TABLE ticket_worker_links (ticket_id TEXT, worker_ref_key TEXT);
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn workdir_revision_migration_preserves_creation_evidence_and_leaves_observation_unknown() {
|
||||||
|
let conn = Connection::open_in_memory().unwrap();
|
||||||
|
conn.execute_batch(
|
||||||
|
r#"
|
||||||
|
CREATE TABLE workdir_registry (
|
||||||
|
workspace_id TEXT NOT NULL,
|
||||||
|
workdir_id TEXT NOT NULL,
|
||||||
|
runtime_id TEXT NOT NULL,
|
||||||
|
repository_id TEXT NOT NULL,
|
||||||
|
selector TEXT,
|
||||||
|
resolved_commit TEXT,
|
||||||
|
materialization_status TEXT NOT NULL,
|
||||||
|
cleanliness TEXT NOT NULL,
|
||||||
|
created_at TEXT NOT NULL,
|
||||||
|
updated_at TEXT NOT NULL,
|
||||||
|
PRIMARY KEY (workspace_id, workdir_id)
|
||||||
|
);
|
||||||
|
INSERT INTO workdir_registry (
|
||||||
|
workspace_id, workdir_id, runtime_id, repository_id, selector, resolved_commit,
|
||||||
|
materialization_status, cleanliness, created_at, updated_at
|
||||||
|
) VALUES (
|
||||||
|
'workspace', 'workdir', 'runtime', 'repository', 'develop', 'abcdef',
|
||||||
|
'present', 'clean', '1', '2'
|
||||||
|
);
|
||||||
|
"#,
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
add_workdir_revision_observations(&conn).unwrap();
|
||||||
|
|
||||||
|
let values = conn
|
||||||
|
.query_row(
|
||||||
|
"SELECT creation_selector, creation_ref, current_selector, current_ref FROM workdir_registry",
|
||||||
|
[],
|
||||||
|
|row| {
|
||||||
|
Ok((
|
||||||
|
row.get::<_, Option<String>>(0)?,
|
||||||
|
row.get::<_, Option<String>>(1)?,
|
||||||
|
row.get::<_, Option<String>>(2)?,
|
||||||
|
row.get::<_, Option<String>>(3)?,
|
||||||
|
))
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
assert_eq!(
|
||||||
|
values,
|
||||||
|
(
|
||||||
|
Some("develop".to_string()),
|
||||||
|
Some("abcdef".to_string()),
|
||||||
|
None,
|
||||||
|
None,
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn repository_records_round_trip() {
|
async fn repository_records_round_trip() {
|
||||||
let store = SqliteWorkspaceStore::in_memory().unwrap();
|
let store = SqliteWorkspaceStore::in_memory().unwrap();
|
||||||
assert_eq!(store.schema_version().await.unwrap(), 14);
|
assert_eq!(store.schema_version().await.unwrap(), 15);
|
||||||
let workspace = WorkspaceRecord {
|
let workspace = WorkspaceRecord {
|
||||||
workspace_id: "local-dev".to_string(),
|
workspace_id: "local-dev".to_string(),
|
||||||
owner_account_id: None,
|
owner_account_id: None,
|
||||||
@@ -3199,7 +3300,7 @@ CREATE TABLE ticket_worker_links (ticket_id TEXT, worker_ref_key TEXT);
|
|||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn memory_authority_records_round_trip_and_close_staging() {
|
async fn memory_authority_records_round_trip_and_close_staging() {
|
||||||
let store = SqliteWorkspaceStore::in_memory().unwrap();
|
let store = SqliteWorkspaceStore::in_memory().unwrap();
|
||||||
assert_eq!(store.schema_version().await.unwrap(), 14);
|
assert_eq!(store.schema_version().await.unwrap(), 15);
|
||||||
let workspace = WorkspaceRecord {
|
let workspace = WorkspaceRecord {
|
||||||
workspace_id: "local-dev".to_string(),
|
workspace_id: "local-dev".to_string(),
|
||||||
owner_account_id: None,
|
owner_account_id: None,
|
||||||
@@ -3313,8 +3414,10 @@ CREATE TABLE ticket_worker_links (ticket_id TEXT, worker_ref_key TEXT);
|
|||||||
workdir_id: "0000019a00000000001".to_string(),
|
workdir_id: "0000019a00000000001".to_string(),
|
||||||
runtime_id: "embedded".to_string(),
|
runtime_id: "embedded".to_string(),
|
||||||
repository_id: "repo".to_string(),
|
repository_id: "repo".to_string(),
|
||||||
selector: Some("develop".to_string()),
|
creation_selector: Some("develop".to_string()),
|
||||||
resolved_commit: Some("abcdef".to_string()),
|
creation_ref: Some("abcdef".to_string()),
|
||||||
|
current_selector: None,
|
||||||
|
current_ref: Some("abcdef".to_string()),
|
||||||
materialization_status: "not_found".to_string(),
|
materialization_status: "not_found".to_string(),
|
||||||
cleanliness: "clean".to_string(),
|
cleanliness: "clean".to_string(),
|
||||||
created_at: "2".to_string(),
|
created_at: "2".to_string(),
|
||||||
@@ -3326,8 +3429,10 @@ CREATE TABLE ticket_worker_links (ticket_id TEXT, worker_ref_key TEXT);
|
|||||||
workdir_id: "runtime-direct".to_string(),
|
workdir_id: "runtime-direct".to_string(),
|
||||||
runtime_id: "embedded".to_string(),
|
runtime_id: "embedded".to_string(),
|
||||||
repository_id: "repo".to_string(),
|
repository_id: "repo".to_string(),
|
||||||
selector: Some("feature".to_string()),
|
creation_selector: Some("feature".to_string()),
|
||||||
resolved_commit: Some("123456".to_string()),
|
creation_ref: Some("123456".to_string()),
|
||||||
|
current_selector: Some("feature".to_string()),
|
||||||
|
current_ref: Some("123456".to_string()),
|
||||||
materialization_status: "present".to_string(),
|
materialization_status: "present".to_string(),
|
||||||
cleanliness: "unknown".to_string(),
|
cleanliness: "unknown".to_string(),
|
||||||
created_at: "3".to_string(),
|
created_at: "3".to_string(),
|
||||||
@@ -3373,7 +3478,7 @@ CREATE TABLE ticket_worker_links (ticket_id TEXT, worker_ref_key TEXT);
|
|||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn account_and_login_records_round_trip() {
|
async fn account_and_login_records_round_trip() {
|
||||||
let store = SqliteWorkspaceStore::in_memory().unwrap();
|
let store = SqliteWorkspaceStore::in_memory().unwrap();
|
||||||
assert_eq!(store.schema_version().await.unwrap(), 14);
|
assert_eq!(store.schema_version().await.unwrap(), 15);
|
||||||
let now = "2026-07-22T00:00:00Z".to_string();
|
let now = "2026-07-22T00:00:00Z".to_string();
|
||||||
let account = AccountRecord {
|
let account = AccountRecord {
|
||||||
account_id: "acct-user-alice".to_string(),
|
account_id: "acct-user-alice".to_string(),
|
||||||
|
|||||||
@@ -0,0 +1,28 @@
|
|||||||
|
# SpawnWorker が current `yoi-runtime` CLI と不整合で起動できない
|
||||||
|
|
||||||
|
Date: 2026-07-30
|
||||||
|
|
||||||
|
## 発生状況
|
||||||
|
|
||||||
|
Workdir selector/ref変更の未コミット差分をreviewer Workerへ委譲するため、`SpawnWorker`をread-only scopeで呼び出した。
|
||||||
|
|
||||||
|
## 結果
|
||||||
|
|
||||||
|
Worker socketが作成されず、child stderrには以下が記録された。
|
||||||
|
|
||||||
|
```text
|
||||||
|
yoi-runtime: unexpected positional argument `worker`
|
||||||
|
|
||||||
|
Usage: yoi-runtime [OPTIONS]
|
||||||
|
```
|
||||||
|
|
||||||
|
現在の`yoi-runtime`はRuntime REST serverの直接起動CLIであり、`worker` positional subcommandを受け付けない。SpawnWorker側のlauncherが旧CLI契約を使っている可能性がある。
|
||||||
|
|
||||||
|
## 影響
|
||||||
|
|
||||||
|
- reviewer/coder Workerをspawnできず、今回の差分は親Worker内で実装・検証した。
|
||||||
|
- scope delegationやreviewer profile以前にprocess起動で失敗するため、Worker orchestration機能が利用できない。
|
||||||
|
|
||||||
|
## 改善案
|
||||||
|
|
||||||
|
SpawnWorker launcherがcurrent Worker/Runtime起動contractを使用しているかを確認し、CLI rename後のdirect executable契約と同期する。失敗時には実際に組み立てたargvと解決したexecutable pathもbounded diagnosticとして返すと、installed binary/worktree binaryの取り違えを判別しやすい。
|
||||||
@@ -0,0 +1,61 @@
|
|||||||
|
import type { WorkingDirectorySummary } from "../sidebar/types.ts";
|
||||||
|
import { formatCurrentWorkdirRevision } from "./workdir-revision.ts";
|
||||||
|
|
||||||
|
declare const Deno: {
|
||||||
|
test(name: string, fn: () => Promise<void> | void): void;
|
||||||
|
};
|
||||||
|
|
||||||
|
function assertEquals<T>(actual: T, expected: T): void {
|
||||||
|
if (actual !== expected) {
|
||||||
|
throw new Error(`Expected ${String(expected)}, got ${String(actual)}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function workdir(
|
||||||
|
current_selector: string | null,
|
||||||
|
current_ref: string | null,
|
||||||
|
): WorkingDirectorySummary {
|
||||||
|
return {
|
||||||
|
working_directory_id: "workdir-1",
|
||||||
|
repository_id: "repository-1",
|
||||||
|
current_selector,
|
||||||
|
current_ref,
|
||||||
|
materializer_kind: "local_git_worktree",
|
||||||
|
status: "active",
|
||||||
|
cleanup_target: {
|
||||||
|
kind: "local_git_worktree",
|
||||||
|
working_directory_id: "workdir-1",
|
||||||
|
repository_id: "repository-1",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
Deno.test("Git detached Workdir shows only its current ref", () => {
|
||||||
|
assertEquals(
|
||||||
|
formatCurrentWorkdirRevision(
|
||||||
|
workdir(null, "0123456789abcdef0123456789abcdef01234567"),
|
||||||
|
"git",
|
||||||
|
),
|
||||||
|
"0123456789ab",
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
Deno.test("Git Workdir with a selector shows selector at current ref", () => {
|
||||||
|
assertEquals(
|
||||||
|
formatCurrentWorkdirRevision(
|
||||||
|
workdir("feature/current", "fedcba9876543210fedcba9876543210fedcba98"),
|
||||||
|
"git",
|
||||||
|
),
|
||||||
|
"feature/current@fedcba987654",
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
Deno.test("non-Git Workdir does not receive Git hash formatting", () => {
|
||||||
|
assertEquals(
|
||||||
|
formatCurrentWorkdirRevision(
|
||||||
|
workdir("snapshot", "revision-value"),
|
||||||
|
"archive",
|
||||||
|
),
|
||||||
|
"snapshot · revision-value",
|
||||||
|
);
|
||||||
|
});
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
import type { WorkingDirectorySummary } from "../sidebar/types.ts";
|
||||||
|
|
||||||
|
export function formatCurrentWorkdirRevision(
|
||||||
|
workdir: WorkingDirectorySummary,
|
||||||
|
repositoryProvider: string | null | undefined,
|
||||||
|
): string {
|
||||||
|
const selector = workdir.current_selector?.trim() || null;
|
||||||
|
const reference = workdir.current_ref?.trim() || null;
|
||||||
|
|
||||||
|
if (repositoryProvider?.toLowerCase() === "git") {
|
||||||
|
const hash = reference ? shortGitHash(reference) : null;
|
||||||
|
if (selector && hash) return `${selector}@${hash}`;
|
||||||
|
return selector ?? hash ?? "—";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (selector && reference) return `${selector} · ${reference}`;
|
||||||
|
return selector ?? reference ?? "—";
|
||||||
|
}
|
||||||
|
|
||||||
|
function shortGitHash(reference: string): string {
|
||||||
|
return reference.length > 12 ? reference.slice(0, 12) : reference;
|
||||||
|
}
|
||||||
@@ -110,7 +110,7 @@
|
|||||||
</span>
|
</span>
|
||||||
<span class="item-meta">
|
<span class="item-meta">
|
||||||
worker {worker.worker_id} · {worker.profile ? `${worker.profile} · ` : ''}{worker.state} · 🖥 {worker.host_id}
|
worker {worker.worker_id} · {worker.profile ? `${worker.profile} · ` : ''}{worker.state} · 🖥 {worker.host_id}
|
||||||
{worker.working_directory ? ` · wd:${worker.working_directory.repository_id}@${worker.working_directory.resolved_commit.slice(0, 8)}` : ''}
|
{worker.working_directory?.current_ref ? ` · wd:${worker.working_directory.repository_id}@${worker.working_directory.current_ref.slice(0, 8)}` : ''}
|
||||||
</span>
|
</span>
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
|
|||||||
@@ -128,10 +128,11 @@ export type WorkingDirectoryOccupancy = {
|
|||||||
export type WorkingDirectorySummary = {
|
export type WorkingDirectorySummary = {
|
||||||
working_directory_id: string;
|
working_directory_id: string;
|
||||||
repository_id: string;
|
repository_id: string;
|
||||||
requested_selector?: string | null;
|
creation_selector?: string | null;
|
||||||
|
creation_ref?: string | null;
|
||||||
|
current_selector?: string | null;
|
||||||
|
current_ref?: string | null;
|
||||||
materializer_kind: string;
|
materializer_kind: string;
|
||||||
resolved_commit: string;
|
|
||||||
resolved_tree?: string | null;
|
|
||||||
status: string;
|
status: string;
|
||||||
cleanliness?: string | null;
|
cleanliness?: string | null;
|
||||||
primary_worker_id?: number | null;
|
primary_worker_id?: number | null;
|
||||||
|
|||||||
@@ -50,9 +50,11 @@ const options: WorkerLaunchOptionsResponse = {
|
|||||||
{
|
{
|
||||||
working_directory_id: "wd-1-repo",
|
working_directory_id: "wd-1-repo",
|
||||||
repository_id: "repo",
|
repository_id: "repo",
|
||||||
requested_selector: "HEAD",
|
creation_selector: "HEAD",
|
||||||
|
creation_ref: "0123456789abcdef",
|
||||||
|
current_selector: null,
|
||||||
|
current_ref: "0123456789abcdef",
|
||||||
materializer_kind: "local_git_worktree",
|
materializer_kind: "local_git_worktree",
|
||||||
resolved_commit: "0123456789abcdef",
|
|
||||||
status: "active",
|
status: "active",
|
||||||
cleanliness: "clean",
|
cleanliness: "clean",
|
||||||
primary_worker_id: null,
|
primary_worker_id: null,
|
||||||
@@ -159,7 +161,7 @@ Deno.test("defaultWorkerLaunchForm preserves a Ticket repository target", () =>
|
|||||||
...options.working_directories[0],
|
...options.working_directories[0],
|
||||||
working_directory_id: "ticket-workdir",
|
working_directory_id: "ticket-workdir",
|
||||||
repository_id: "ticket-repo",
|
repository_id: "ticket-repo",
|
||||||
requested_selector: "work/ticket",
|
creation_selector: "work/ticket",
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -55,7 +55,8 @@ export function defaultWorkerLaunchForm(
|
|||||||
Boolean(current.working_directory_repository_id) &&
|
Boolean(current.working_directory_repository_id) &&
|
||||||
directory.repository_id === current.working_directory_repository_id &&
|
directory.repository_id === current.working_directory_repository_id &&
|
||||||
(!current.working_directory_selector ||
|
(!current.working_directory_selector ||
|
||||||
directory.requested_selector === current.working_directory_selector)
|
(directory.current_selector ?? directory.creation_selector) ===
|
||||||
|
current.working_directory_selector)
|
||||||
) ?? availableWorkingDirectories.find((directory) =>
|
) ?? availableWorkingDirectories.find((directory) =>
|
||||||
Boolean(current.working_directory_repository_id) &&
|
Boolean(current.working_directory_repository_id) &&
|
||||||
directory.repository_id === current.working_directory_repository_id
|
directory.repository_id === current.working_directory_repository_id
|
||||||
|
|||||||
+8
-8
@@ -1,6 +1,7 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { pushWorkspaceAlert } from '$lib/workspace/alerts/store';
|
import { pushWorkspaceAlert } from '$lib/workspace/alerts/store';
|
||||||
import { workspaceApiPath } from '$lib/workspace/api/http';
|
import { workspaceApiPath } from '$lib/workspace/api/http';
|
||||||
|
import { formatCurrentWorkdirRevision } from '$lib/workspace/settings/workdir-revision';
|
||||||
import type {
|
import type {
|
||||||
CleanupWorkdirCandidate,
|
CleanupWorkdirCandidate,
|
||||||
RuntimeCleanupExecutionResponse,
|
RuntimeCleanupExecutionResponse,
|
||||||
@@ -23,12 +24,13 @@
|
|||||||
workdirs = data.workdirs?.items ?? [];
|
workdirs = data.workdirs?.items ?? [];
|
||||||
});
|
});
|
||||||
|
|
||||||
function commitLabel(workdir: WorkingDirectorySummary): string {
|
function repositoryProvider(workdir: WorkingDirectorySummary): string | null {
|
||||||
return workdir.resolved_commit ? workdir.resolved_commit.slice(0, 12) : '—';
|
return data.repositories?.items.find((repository) => repository.id === workdir.repository_id)
|
||||||
|
?.provider ?? null;
|
||||||
}
|
}
|
||||||
|
|
||||||
function selectorLabel(workdir: WorkingDirectorySummary): string {
|
function currentRevision(workdir: WorkingDirectorySummary): string {
|
||||||
return workdir.requested_selector ?? 'HEAD';
|
return formatCurrentWorkdirRevision(workdir, repositoryProvider(workdir));
|
||||||
}
|
}
|
||||||
|
|
||||||
function cleanupCandidate(workdir: WorkingDirectorySummary): CleanupWorkdirCandidate | undefined {
|
function cleanupCandidate(workdir: WorkingDirectorySummary): CleanupWorkdirCandidate | undefined {
|
||||||
@@ -125,8 +127,7 @@
|
|||||||
<tr>
|
<tr>
|
||||||
<th>Workdir</th>
|
<th>Workdir</th>
|
||||||
<th>Repository</th>
|
<th>Repository</th>
|
||||||
<th>Selector</th>
|
<th>Revision</th>
|
||||||
<th>Commit</th>
|
|
||||||
<th>Status</th>
|
<th>Status</th>
|
||||||
<th>Cleanliness</th>
|
<th>Cleanliness</th>
|
||||||
<th>Occupied by</th>
|
<th>Occupied by</th>
|
||||||
@@ -139,8 +140,7 @@
|
|||||||
<tr>
|
<tr>
|
||||||
<td><code>{workdir.working_directory_id}</code></td>
|
<td><code>{workdir.working_directory_id}</code></td>
|
||||||
<td>{workdir.repository_id}</td>
|
<td>{workdir.repository_id}</td>
|
||||||
<td>{selectorLabel(workdir)}</td>
|
<td><code>{currentRevision(workdir)}</code></td>
|
||||||
<td><code>{commitLabel(workdir)}</code></td>
|
|
||||||
<td>{workdir.status}</td>
|
<td>{workdir.status}</td>
|
||||||
<td>{workdir.cleanliness ?? 'unknown'}</td>
|
<td>{workdir.cleanliness ?? 'unknown'}</td>
|
||||||
<td>
|
<td>
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
import { pushWorkspaceAlert } from '$lib/workspace/alerts/store';
|
import { pushWorkspaceAlert } from '$lib/workspace/alerts/store';
|
||||||
import { workspaceApiPath } from '$lib/workspace/api/http';
|
import { workspaceApiPath } from '$lib/workspace/api/http';
|
||||||
import { workerConsoleHref } from '$lib/workspace/console/model';
|
import { workerConsoleHref } from '$lib/workspace/console/model';
|
||||||
|
import { formatCurrentWorkdirRevision } from '$lib/workspace/settings/workdir-revision';
|
||||||
import { canOpenWorkerConsole } from '$lib/workspace/sidebar/workers';
|
import { canOpenWorkerConsole } from '$lib/workspace/sidebar/workers';
|
||||||
import type { CleanupWorkerCandidate, RuntimeCleanupExecutionResponse, RuntimeCleanupPlanResponse, Worker } from '$lib/workspace/sidebar/types';
|
import type { CleanupWorkerCandidate, RuntimeCleanupExecutionResponse, RuntimeCleanupPlanResponse, Worker } from '$lib/workspace/sidebar/types';
|
||||||
import type { PageProps } from './$types';
|
import type { PageProps } from './$types';
|
||||||
@@ -145,9 +146,9 @@
|
|||||||
function workerDirectory(worker: Worker): string {
|
function workerDirectory(worker: Worker): string {
|
||||||
const directory = worker.working_directory;
|
const directory = worker.working_directory;
|
||||||
if (!directory) return '—';
|
if (!directory) return '—';
|
||||||
const selector = directory.requested_selector ?? 'HEAD';
|
const provider = data.repositories?.items.find((repository) => repository.id === directory.repository_id)
|
||||||
const commit = directory.resolved_commit ? directory.resolved_commit.slice(0, 12) : null;
|
?.provider;
|
||||||
return commit ? `${directory.repository_id} · ${selector} · ${commit}` : `${directory.repository_id} · ${selector}`;
|
return `${directory.repository_id} · ${formatCurrentWorkdirRevision(directory, provider)}`;
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|||||||
@@ -2,12 +2,14 @@
|
|||||||
import { goto } from '$app/navigation';
|
import { goto } from '$app/navigation';
|
||||||
import { untrack } from 'svelte';
|
import { untrack } from 'svelte';
|
||||||
import { workspaceApiPath } from '$lib/workspace/api/http';
|
import { workspaceApiPath } from '$lib/workspace/api/http';
|
||||||
|
import { formatCurrentWorkdirRevision } from '$lib/workspace/settings/workdir-revision';
|
||||||
import { buildBrowserCreateWorkerRequest, defaultWorkerLaunchForm } from '$lib/workspace/sidebar/worker-launch';
|
import { buildBrowserCreateWorkerRequest, defaultWorkerLaunchForm } from '$lib/workspace/sidebar/worker-launch';
|
||||||
import type {
|
import type {
|
||||||
BrowserCreateWorkerResponse,
|
BrowserCreateWorkerResponse,
|
||||||
BrowserWorkingDirectoryCreateResponse,
|
BrowserWorkingDirectoryCreateResponse,
|
||||||
Diagnostic,
|
Diagnostic,
|
||||||
WorkerLaunchOptionsResponse,
|
WorkerLaunchOptionsResponse,
|
||||||
|
WorkingDirectorySummary,
|
||||||
} from '$lib/workspace/sidebar/types';
|
} from '$lib/workspace/sidebar/types';
|
||||||
import type { PageProps } from './$types';
|
import type { PageProps } from './$types';
|
||||||
|
|
||||||
@@ -22,6 +24,12 @@
|
|||||||
diagnostics?: Diagnostic[];
|
diagnostics?: Diagnostic[];
|
||||||
};
|
};
|
||||||
|
|
||||||
|
function workdirOptionLabel(directory: WorkingDirectorySummary): string {
|
||||||
|
const provider = data.repositories?.items.find((repository) => repository.id === directory.repository_id)
|
||||||
|
?.provider;
|
||||||
|
return `${directory.repository_id} · ${formatCurrentWorkdirRevision(directory, provider)}`;
|
||||||
|
}
|
||||||
|
|
||||||
let { data }: PageProps = $props();
|
let { data }: PageProps = $props();
|
||||||
let workspaceId = $derived(data.workspaceId);
|
let workspaceId = $derived(data.workspaceId);
|
||||||
const ticketContext = untrack(() => data.ticketContext);
|
const ticketContext = untrack(() => data.ticketContext);
|
||||||
@@ -296,7 +304,7 @@
|
|||||||
<option value="" disabled>Select workdir</option>
|
<option value="" disabled>Select workdir</option>
|
||||||
{#each availableWorkingDirectories as directory}
|
{#each availableWorkingDirectories as directory}
|
||||||
<option value={directory.working_directory_id}>
|
<option value={directory.working_directory_id}>
|
||||||
{directory.repository_id} · {directory.requested_selector ?? 'HEAD'}
|
{workdirOptionLabel(directory)}
|
||||||
</option>
|
</option>
|
||||||
{/each}
|
{/each}
|
||||||
<option value={NEW_WORKING_DIRECTORY_VALUE}>New workdir…</option>
|
<option value={NEW_WORKING_DIRECTORY_VALUE}>New workdir…</option>
|
||||||
|
|||||||
Reference in New Issue
Block a user