From bad37ddc7da83437790fd581fa285b9c560d16db Mon Sep 17 00:00:00 2001 From: Hare Date: Wed, 2 Sep 2026 15:24:12 +0900 Subject: [PATCH] feat: centralize workspace profile DTOs --- crates/workspace-api/src/lib.rs | 186 ++++++++++++++++++ .../workspace-server/src/profile_settings.rs | 85 +------- crates/workspace-server/src/server.rs | 54 ++--- .../src/lib/generated/workspace-api.ts | 58 ++++++ 4 files changed, 282 insertions(+), 101 deletions(-) diff --git a/crates/workspace-api/src/lib.rs b/crates/workspace-api/src/lib.rs index 8570dd26..79153639 100644 --- a/crates/workspace-api/src/lib.rs +++ b/crates/workspace-api/src/lib.rs @@ -222,6 +222,100 @@ pub struct WorkspaceResponse { pub extension_points: WorkspaceExtensionPoints, } +/// Workspace identity metadata exposed by the current settings resource. +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] +#[cfg_attr(feature = "typescript", derive(ts_rs::TS))] +#[serde(deny_unknown_fields)] +pub struct WorkspaceMetadataSettingsResponse { + pub workspace_id: String, + pub display_name: String, + pub created_at: String, + pub revision: String, + pub source: String, + pub diagnostics: Vec, +} + +/// Compare-and-swap update for Workspace identity display metadata. +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] +#[cfg_attr(feature = "typescript", derive(ts_rs::TS))] +#[serde(deny_unknown_fields)] +pub struct UpdateWorkspaceMetadataRequest { + pub display_name: String, + pub revision: String, +} + +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] +#[cfg_attr(feature = "typescript", derive(ts_rs::TS))] +#[serde(deny_unknown_fields)] +pub struct WorkspaceMetadataMutationResponse { + pub workspace: WorkspaceMetadataSettingsResponse, + pub diagnostics: Vec, +} + +/// Read-only Profile catalog projected from one active Workspace config revision. +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] +#[cfg_attr(feature = "typescript", derive(ts_rs::TS))] +#[cfg_attr(feature = "typescript", ts(optional_fields = nullable))] +#[serde(deny_unknown_fields)] +pub struct ProfileSettingsResponse { + pub workspace_id: String, + pub registry_revision: String, + #[serde(default, skip_serializing_if = "Option::is_none")] + #[cfg_attr(feature = "typescript", ts(optional, type = "number | null"))] + pub config_revision: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub tree_digest: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub projection_digest: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub default_profile: Option, + pub profiles: Vec, + pub sources: Vec, + pub diagnostics: Vec, +} + +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] +#[cfg_attr(feature = "typescript", derive(ts_rs::TS))] +#[cfg_attr(feature = "typescript", ts(optional_fields = nullable))] +#[serde(deny_unknown_fields)] +pub struct WorkspaceProfileSummary { + pub profile_id: String, + pub selector: String, + pub label: String, + pub source_kind: String, + #[serde(skip_serializing_if = "Option::is_none")] + pub profile_source_id: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub description: Option, + pub editable: bool, + pub is_default: bool, + pub diagnostics: Vec, +} + +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] +#[cfg_attr(feature = "typescript", derive(ts_rs::TS))] +#[serde(deny_unknown_fields)] +pub struct WorkspaceProfileSourceSummary { + pub profile_source_id: String, + pub display_path: String, + pub kind: String, + pub content_type: String, + pub content_digest: String, + pub provenance: WorkspaceProfileSourceProvenance, + pub editable: bool, + pub revision: String, + #[cfg_attr(feature = "typescript", ts(type = "number"))] + pub size_bytes: u64, + pub diagnostics: Vec, +} + +#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)] +#[cfg_attr(feature = "typescript", derive(ts_rs::TS))] +#[serde(rename_all = "snake_case")] +pub enum WorkspaceProfileSourceProvenance { + ProjectProfileSourceTree, +} + #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] #[cfg_attr(feature = "typescript", derive(ts_rs::TS))] #[serde(deny_unknown_fields)] @@ -1124,6 +1218,13 @@ pub fn catalog_typescript() -> String { WorkspaceExtensionPointState::decl(&config), WorkspaceExtensionPoints::decl(&config), WorkspaceResponse::decl(&config), + WorkspaceMetadataSettingsResponse::decl(&config), + UpdateWorkspaceMetadataRequest::decl(&config), + WorkspaceMetadataMutationResponse::decl(&config), + ProfileSettingsResponse::decl(&config), + WorkspaceProfileSummary::decl(&config), + WorkspaceProfileSourceSummary::decl(&config), + WorkspaceProfileSourceProvenance::decl(&config), RepositorySourceKind::decl(&config), RepositorySource::decl(&config), RepositoryObservedStatus::decl(&config), @@ -1322,6 +1423,14 @@ mod tests { assert!(output.contains("export type RepositoryListResponse =")); assert!(output.contains("items: Array")); assert!(output.contains("observed_at?: string | null")); + assert!(output.contains("export type WorkspaceMetadataSettingsResponse =")); + assert!(output.contains("export type WorkspaceMetadataMutationResponse =")); + assert!(output.contains("export type ProfileSettingsResponse =")); + assert!(output.contains("config_revision?: number | null")); + assert!(output.contains("provenance: WorkspaceProfileSourceProvenance")); + assert!(output.contains( + "export type WorkspaceProfileSourceProvenance = \"project_profile_source_tree\"" + )); assert!(!output.contains("repository_id: string, display_name")); } @@ -1354,6 +1463,83 @@ mod tests { assert_eq!(decoded, value); } + #[test] + fn workspace_metadata_and_profile_projection_fixtures_round_trip() { + let diagnostic = Diagnostic { + code: "profile_projection_warning".to_string(), + severity: DiagnosticSeverity::Warning, + message: "projected from the active config revision".to_string(), + }; + let metadata = WorkspaceMetadataSettingsResponse { + workspace_id: "workspace-test".to_string(), + display_name: "Test".to_string(), + created_at: "2026-01-01T00:00:00Z".to_string(), + revision: "sha256:metadata".to_string(), + source: "workspace-config".to_string(), + diagnostics: vec![diagnostic.clone()], + }; + round_trip(metadata.clone()); + round_trip(UpdateWorkspaceMetadataRequest { + display_name: "Renamed".to_string(), + revision: metadata.revision.clone(), + }); + round_trip(WorkspaceMetadataMutationResponse { + workspace: metadata, + diagnostics: vec![], + }); + + round_trip(ProfileSettingsResponse { + workspace_id: "workspace-test".to_string(), + registry_revision: "config-source:7:sha256:tree:sha256:projection".to_string(), + config_revision: Some(7), + tree_digest: Some("sha256:tree".to_string()), + projection_digest: Some("sha256:projection".to_string()), + default_profile: Some("workspace:coder".to_string()), + profiles: vec![WorkspaceProfileSummary { + profile_id: "workspace:coder".to_string(), + selector: "workspace:coder".to_string(), + label: "Coder".to_string(), + source_kind: "project".to_string(), + profile_source_id: Some("profile-source-1".to_string()), + description: None, + editable: true, + is_default: true, + diagnostics: vec![diagnostic.clone()], + }], + sources: vec![WorkspaceProfileSourceSummary { + profile_source_id: "profile-source-1".to_string(), + display_path: "profiles/coder.dcdl".to_string(), + kind: "profile".to_string(), + content_type: "text/x-decodal".to_string(), + content_digest: "sha256:source".to_string(), + provenance: WorkspaceProfileSourceProvenance::ProjectProfileSourceTree, + editable: false, + revision: "config-source:7".to_string(), + size_bytes: 128, + diagnostics: vec![], + }], + diagnostics: vec![diagnostic], + }); + + let absent_optional_fields = serde_json::json!({ + "workspace_id": "workspace-test", + "registry_revision": "builtin", + "profiles": [], + "sources": [], + "diagnostics": [] + }); + let decoded: ProfileSettingsResponse = + serde_json::from_value(absent_optional_fields.clone()).unwrap(); + assert_eq!(decoded.config_revision, None); + assert_eq!(decoded.tree_digest, None); + assert_eq!(decoded.projection_digest, None); + assert_eq!(decoded.default_profile, None); + assert_eq!( + serde_json::to_value(decoded).unwrap(), + absent_optional_fields + ); + } + fn companion_worker() -> WorkspaceWorkerDiscoveryItem { WorkspaceWorkerDiscoveryItem { subject: WorkspaceWorkerSubject::RuntimeWorker { diff --git a/crates/workspace-server/src/profile_settings.rs b/crates/workspace-server/src/profile_settings.rs index f76aff77..e1888f8f 100644 --- a/crates/workspace-server/src/profile_settings.rs +++ b/crates/workspace-server/src/profile_settings.rs @@ -12,11 +12,15 @@ use worker_runtime::config_bundle::{ ConfigBundle, ConfigBundleMetadata, ConfigBundleProvenance, ConfigProfileDescriptor, }; use worker_runtime::profile_archive::{ProfileSourceArchive, ProfileSourceArchiveInput}; +use workspace_api::{ + Diagnostic, DiagnosticSeverity, ProfileSettingsResponse, UpdateWorkspaceMetadataRequest, + WorkspaceMetadataSettingsResponse, WorkspaceProfileSourceProvenance, + WorkspaceProfileSourceSummary, WorkspaceProfileSummary, +}; use crate::config_source::{ WorkspaceConfigSchemaProvider, WorkspaceConfigState, evaluate_workspace_config_state, }; -use crate::hosts::{DiagnosticSeverity, RuntimeDiagnostic}; use crate::{Error, Result}; const PROFILE_SCHEMA_SOURCE: &str = r#"{ @@ -427,81 +431,6 @@ fn build_virtual_profile_archive( .map_err(|error| profile_validation_error("profile_source_archive_invalid", &error.to_string())) } -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct WorkspaceMetadataSettingsResponse { - pub workspace_id: String, - pub display_name: String, - pub created_at: String, - pub revision: String, - pub source: String, - pub diagnostics: Vec, -} - -#[derive(Debug, Clone, Deserialize)] -#[serde(deny_unknown_fields)] -pub struct UpdateWorkspaceMetadataRequest { - pub display_name: String, - pub revision: String, -} - -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct WorkspaceMetadataMutationResponse { - pub workspace: WorkspaceMetadataSettingsResponse, - pub diagnostics: Vec, -} - -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct ProfileSettingsResponse { - pub workspace_id: String, - pub registry_revision: String, - #[serde(default, skip_serializing_if = "Option::is_none")] - pub config_revision: Option, - #[serde(default, skip_serializing_if = "Option::is_none")] - pub tree_digest: Option, - #[serde(default, skip_serializing_if = "Option::is_none")] - pub projection_digest: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub default_profile: Option, - pub profiles: Vec, - pub sources: Vec, - pub diagnostics: Vec, -} - -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct WorkspaceProfileSummary { - pub profile_id: String, - pub selector: String, - pub label: String, - pub source_kind: String, - #[serde(skip_serializing_if = "Option::is_none")] - pub profile_source_id: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub description: Option, - pub editable: bool, - pub is_default: bool, - pub diagnostics: Vec, -} - -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct WorkspaceProfileSourceSummary { - pub profile_source_id: String, - pub display_path: String, - pub kind: String, - pub content_type: String, - pub content_digest: String, - pub provenance: WorkspaceProfileSourceProvenance, - pub editable: bool, - pub revision: String, - pub size_bytes: u64, - pub diagnostics: Vec, -} - -#[derive(Debug, Clone, Serialize, Deserialize)] -#[serde(rename_all = "snake_case")] -pub enum WorkspaceProfileSourceProvenance { - ProjectProfileSourceTree, -} - #[derive(Debug, Clone, Serialize, Deserialize)] #[serde(deny_unknown_fields)] struct WorkspaceIdentityFile { @@ -804,8 +733,8 @@ fn diagnostic( code: impl Into, severity: DiagnosticSeverity, message: impl Into, -) -> RuntimeDiagnostic { - RuntimeDiagnostic { +) -> Diagnostic { + Diagnostic { code: code.into(), severity, message: message.into(), diff --git a/crates/workspace-server/src/server.rs b/crates/workspace-server/src/server.rs index 26bf67cb..1343205d 100644 --- a/crates/workspace-server/src/server.rs +++ b/crates/workspace-server/src/server.rs @@ -61,20 +61,22 @@ use workspace_api::{ CreateWorkspaceRepositoryRequest, CreateWorkspaceRepositoryResponse, DeleteRepositorySshCredentialRequest, DeleteRepositorySshHostTrustRequest, ObjectiveCreateRequest, ObjectiveEditRequest, ObjectiveLinkTicketRequest, - ObjectiveStateRequest, PutRepositorySshHostTrustRequest, RepositoryAccessProjection, - RepositoryDetailResponse, RepositoryListResponse, RepositoryLogResponse, - RepositorySshCredential, RepositorySshHostTrust, RotateRepositorySshCredentialRequest, - RuntimeConnectionTestResponse, RuntimeManagementSummary, TICKET_ORCHESTRATION_PLANS_QUERY_PATH, - TICKET_RELATIONS_QUERY_PATH, + ObjectiveStateRequest, ProfileSettingsResponse, PutRepositorySshHostTrustRequest, + RepositoryAccessProjection, RepositoryDetailResponse, RepositoryListResponse, + RepositoryLogResponse, RepositorySshCredential, RepositorySshHostTrust, + RotateRepositorySshCredentialRequest, RuntimeConnectionTestResponse, RuntimeManagementSummary, + TICKET_ORCHESTRATION_PLANS_QUERY_PATH, TICKET_RELATIONS_QUERY_PATH, + UpdateWorkspaceMetadataRequest, WorkingDirectoryCreateRequest as BrowserWorkingDirectoryCreateRequest, WorkingDirectoryCreateResponse as BrowserWorkingDirectoryCreateResponse, WorkingDirectoryDetailResponse as BrowserWorkingDirectoryDetailResponse, WorkingDirectoryListResponse as BrowserWorkingDirectoryListResponse, WorkingDirectoryRemovalDisposition, WorkingDirectoryRemovalRequest, WorkingDirectoryRemovalResponse, WorkspaceCatalogListResponse, WorkspaceCreateResponse, - WorkspaceExtensionPointState, WorkspaceExtensionPoints, WorkspacePermissionSummary, - WorkspaceRepositoryRecord, WorkspaceResponse, WorkspaceRuntimeResource, WorkspaceSummary, - WorkspaceWorkerDiscoveryItem, WorkspaceWorkerDiscoveryPage, WorkspaceWorkerSubject, + WorkspaceExtensionPointState, WorkspaceExtensionPoints, WorkspaceMetadataMutationResponse, + WorkspaceMetadataSettingsResponse, WorkspacePermissionSummary, WorkspaceRepositoryRecord, + WorkspaceResponse, WorkspaceRuntimeResource, WorkspaceSummary, WorkspaceWorkerDiscoveryItem, + WorkspaceWorkerDiscoveryPage, WorkspaceWorkerSubject, }; use crate::auth::{ @@ -114,7 +116,6 @@ use crate::observation::{ BackendObservationProxy, ObservationProxyError, RuntimeObservationClient, RuntimeObservationSource, RuntimeObservationSourceConfig, }; -use crate::profile_settings::UpdateWorkspaceMetadataRequest; use crate::records::{ MergeRequestListItem, MergeRequestListResponse, ObjectiveDetail, ObjectiveQueryRequest, ObjectiveQueryResponse, ObjectiveShowRequest, ProjectRecordList, TicketDetail, @@ -3360,7 +3361,7 @@ async fn scoped_get_workspace( async fn scoped_get_workspace_settings( State(api): State, AxumPath(path): AxumPath, -) -> ApiResult> { +) -> ApiResult> { validate_workspace_scope(&api, &path.workspace_id)?; Ok(Json(crate::profile_settings::workspace_metadata_settings( &api.config.workspace_root, @@ -3374,20 +3375,18 @@ async fn scoped_update_workspace_settings( State(api): State, AxumPath(path): AxumPath, Json(request): Json, -) -> ApiResult> { +) -> ApiResult> { validate_workspace_scope(&api, &path.workspace_id)?; let workspace = crate::profile_settings::update_workspace_metadata(&api.config.workspace_root, request)?; - Ok(Json( - crate::profile_settings::WorkspaceMetadataMutationResponse { - workspace, - diagnostics: vec![RuntimeDiagnostic { - code: "workspace_metadata_updated".to_string(), - severity: DiagnosticSeverity::Info, - message: "Workspace display metadata was updated.".to_string(), - }], - }, - )) + Ok(Json(WorkspaceMetadataMutationResponse { + workspace, + diagnostics: vec![workspace_api::Diagnostic { + code: "workspace_metadata_updated".to_string(), + severity: workspace_api::DiagnosticSeverity::Info, + message: "Workspace display metadata was updated.".to_string(), + }], + })) } #[derive(Debug, Deserialize)] @@ -3763,7 +3762,7 @@ async fn scoped_commit_workspace_config_tree( async fn scoped_get_profile_settings( State(api): State, AxumPath(path): AxumPath, -) -> ApiResult> { +) -> ApiResult> { validate_workspace_scope(&api, &path.workspace_id)?; let state = api .config_store @@ -14353,7 +14352,7 @@ fn worker_launch_options_response(api: &WorkspaceApi) -> ApiResult; +}; + +export type UpdateWorkspaceMetadataRequest = { + display_name: string; + revision: string; +}; + +export type WorkspaceMetadataMutationResponse = { + workspace: WorkspaceMetadataSettingsResponse; + diagnostics: Array; +}; + +export type ProfileSettingsResponse = { + workspace_id: string; + registry_revision: string; + config_revision?: number | null; + tree_digest?: string | null; + projection_digest?: string | null; + default_profile?: string | null; + profiles: Array; + sources: Array; + diagnostics: Array; +}; + +export type WorkspaceProfileSummary = { + profile_id: string; + selector: string; + label: string; + source_kind: string; + profile_source_id?: string | null; + description?: string | null; + editable: boolean; + is_default: boolean; + diagnostics: Array; +}; + +export type WorkspaceProfileSourceSummary = { + profile_source_id: string; + display_path: string; + kind: string; + content_type: string; + content_digest: string; + provenance: WorkspaceProfileSourceProvenance; + editable: boolean; + revision: string; + size_bytes: number; + diagnostics: Array; +}; + +export type WorkspaceProfileSourceProvenance = "project_profile_source_tree"; + export type RepositorySourceKind = | "local_path" | "file"