From 8415de0bad4372c03465f592205cadc24fd04c8b Mon Sep 17 00:00:00 2001 From: "Dylan M. Taylor" Date: Wed, 11 Nov 2026 12:11:16 -0500 Subject: [PATCH 01/14] ci: run Harden-Runner on every job (#2258) Adds step-security/harden-runner (audit, pinned to v2.22.4 like craft-common-actions) as the first step of each job that did not have it, so every workflow in the org's public repos reports its egress to StepSecurity. Audit, not block: block-mode allow lists come from each job's observed traffic. --- .github/workflows/packaging-lint.yml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.github/workflows/packaging-lint.yml b/.github/workflows/packaging-lint.yml index da85536c4..0a91cc050 100644 --- a/.github/workflows/packaging-lint.yml +++ b/.github/workflows/packaging-lint.yml @@ -29,6 +29,10 @@ jobs: name: Windows installation policy (no app build) runs-on: windows-latest steps: + - name: Harden runner + uses: step-security/harden-runner@1d88384fd173af6ecb012dfd0264a306b711241a # v2.22.4 + with: + egress-policy: audit - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false From 2446e8718b116144ac5e3b47cd09db8c5a02a40a Mon Sep 17 00:00:00 2001 From: "Dylan M. Taylor" Date: Wed, 11 Nov 2026 12:44:16 -0500 Subject: [PATCH 02/14] Updates: take craft-update from craft-libs main at 6e03f65bf --- Cargo.lock | 3 ++- Cargo.toml | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 8ca91ac04..73d4d60fb 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1134,7 +1134,7 @@ dependencies = [ [[package]] name = "craft-update" version = "0.1.0" -source = "git+https://github.com/storytold/craft-libs?rev=1b34eba41e62138fc2438d2734523b302840b45d#1b34eba41e62138fc2438d2734523b302840b45d" +source = "git+https://github.com/storytold/craft-libs?rev=6e03f65bf45181db77ba6229716e6624db9eb25b#6e03f65bf45181db77ba6229716e6624db9eb25b" dependencies = [ "chrono", "dunce", @@ -1144,6 +1144,7 @@ dependencies = [ "image", "log", "plist", + "rustix 1.1.5", "semver", "serde", "serde_json", diff --git a/Cargo.toml b/Cargo.toml index af0dcaa8b..21833c3ec 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -43,7 +43,7 @@ craft-docpath = { version = "0.1.0", git = "https://github.com/storytold/craft-l # pull a second egui family version). craft-window = { git = "https://github.com/storytold/craft-libs", rev = "e11f9d9aebea5f6b8e74643c1ea1ed172ba5f545" } # craft-update: app-specific pretend versions apply to startup and manual checks. -craft-update = { git = "https://github.com/storytold/craft-libs", version = "=0.1.0", rev = "1b34eba41e62138fc2438d2734523b302840b45d", features = ["egui"] } +craft-update = { git = "https://github.com/storytold/craft-libs", version = "=0.1.0", rev = "6e03f65bf45181db77ba6229716e6624db9eb25b", features = ["egui"] } # The suite's font store: exact lookup of a missing font in the pinned Google Fonts and Noto # indexes, and checksum-verified installs into the one folder every Craft app reads (craft-libs). craft-font-store = { git = "https://github.com/storytold/craft-libs", rev = "e11f9d9aebea5f6b8e74643c1ea1ed172ba5f545" } From 74698fa89fb1219d219a9e1bbd102f1154f4cedd Mon Sep 17 00:00:00 2001 From: Oleksii Sukholita <72558888+aleksei-suholita@users.noreply.github.com> Date: Wed, 11 Nov 2026 20:23:45 +0100 Subject: [PATCH 03/14] M7: links open in the browser in the desktop app, What's new on the update prompt among them (#2216) The desktop app built eframe without its `links` feature, so egui-winit dropped every `ctx.open_url` with only a log warning: the update prompt's What's new link and Download button, the About and Credits links, Terms in Live and the link in the Dev Mode panel did nothing on click. The feature adds no crate: `webbrowser` is already a dependency of the app. DeckCraft's desktop app builds eframe with it too. `links_in_the_interface_open_in_the_browser` fails without the feature. Co-authored-by: Oleksii Sukholita --- Cargo.lock | 1 + apps/figcraft/Cargo.toml | 4 +++- apps/figcraft/src/services_tests.rs | 10 ++++++++++ 3 files changed, 14 insertions(+), 1 deletion(-) diff --git a/Cargo.lock b/Cargo.lock index 73d4d60fb..6714be11b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1547,6 +1547,7 @@ dependencies = [ "raw-window-handle", "smithay-clipboard", "web-time", + "webbrowser", "windows-sys 0.61.2", "winit", ] diff --git a/apps/figcraft/Cargo.toml b/apps/figcraft/Cargo.toml index f2ea73ba8..386cb1f33 100644 --- a/apps/figcraft/Cargo.toml +++ b/apps/figcraft/Cargo.toml @@ -23,7 +23,9 @@ craft-window = { workspace = true } # The suite's settings folder: `FIGCRAFT_CONFIG_DIR`, a portable marker, then the platform folder. craft-settings = { workspace = true } egui = { workspace = true } -eframe = { workspace = true, features = ["default_fonts", "wgpu", "accesskit", "clipboard", "wayland", "x11"] } +# `links`: without it eframe drops every `ctx.open_url` and hyperlink click (the update prompt's +# What's new and Download, About, Credits) with only a log warning. +eframe = { workspace = true, features = ["default_fonts", "wgpu", "accesskit", "clipboard", "links", "wayland", "x11"] } serde_json = { workspace = true } log = { workspace = true } rfd = { workspace = true } diff --git a/apps/figcraft/src/services_tests.rs b/apps/figcraft/src/services_tests.rs index 970f1729e..0d197f5ce 100644 --- a/apps/figcraft/src/services_tests.rs +++ b/apps/figcraft/src/services_tests.rs @@ -142,3 +142,13 @@ fn the_next_dialog_starts_in_the_folder_of_the_last_pick() { let _ = std::fs::remove_dir_all(&dir); assert_eq!(start_folder(Some(dir)), None, "a folder deleted since: the system's own"); } + +/// The update prompt's What's new and Download, and the About and Credits links, open through +/// `ctx.open_url`. eframe drops those with only a log warning unless it is built with `links`. +#[test] +fn links_in_the_interface_open_in_the_browser() { + let manifest = include_str!("../Cargo.toml"); + let eframe: Vec<&str> = manifest.lines().filter(|l| l.trim_start().starts_with("eframe =")).collect(); + assert!(!eframe.is_empty(), "the app depends on eframe"); + assert!(eframe.iter().any(|l| l.contains("\"links\"")), "eframe is built with `links`: {eframe:?}"); +} From 6bc8bbaef1718ff32fd322eeb585e053e9448f3e Mon Sep 17 00:00:00 2001 From: "Dylan M. Taylor" Date: Wed, 11 Nov 2026 14:48:38 -0500 Subject: [PATCH 04/14] inspect: work out a grid's cells once per inspection (#2260) --- crates/engine/src/automation/inspect.rs | 27 +++++++++++++++++-------- crates/engine/src/cmd/layout.rs | 7 +++++++ 2 files changed, 26 insertions(+), 8 deletions(-) diff --git a/crates/engine/src/automation/inspect.rs b/crates/engine/src/automation/inspect.rs index 324635071..931280dba 100644 --- a/crates/engine/src/automation/inspect.rs +++ b/crates/engine/src/automation/inspect.rs @@ -165,6 +165,14 @@ pub fn node_json(doc: &Document, id: NodeId, depth: usize) -> Value { /// A node's selected properties, applied at every level of its subtree. pub fn node_json_with_options(doc: &Document, id: NodeId, depth: usize, options: &InspectOptions) -> Value { + node_json_in(doc, id, depth, options, &GridCells::default()) +} + +/// The cells of the grid frames met so far in one inspection, so a grid's geometry is worked out +/// once, not once per child. +type GridCells = std::cell::RefCell>>; + +fn node_json_in(doc: &Document, id: NodeId, depth: usize, options: &InspectOptions, grids: &GridCells) -> Value { let Some(n) = doc.node(id) else { return Value::Null }; let mut v = json!({"id": n.id}); let Some(o) = v.as_object_mut() else { return v }; @@ -176,7 +184,7 @@ pub fn node_json_with_options(doc: &Document, id: NodeId, depth: usize, options: if !n.is_page() { base_props(doc, n, id, o, options); paint_props(n, o, options); - layout_props(doc, n, o, options); + layout_props(doc, n, o, options, grids); dev_props(doc, n, o, options); } // Layer data written with `node.set {props: {pluginData}}` (pages too). @@ -189,7 +197,7 @@ pub fn node_json_with_options(doc: &Document, id: NodeId, depth: usize, options: } if !n.children.is_empty() { if depth > 0 { - o.insert("children".into(), Value::Array(n.children.iter().map(|c| node_json_with_options(doc, *c, depth - 1, options)).collect())); + o.insert("children".into(), Value::Array(n.children.iter().map(|c| node_json_in(doc, *c, depth - 1, options, grids)).collect())); } else { o.insert("childCount".into(), json!(n.children.len())); } @@ -286,7 +294,7 @@ fn paint_props(n: &Node, o: &mut serde_json::Map, options: &Inspe } } -fn layout_props(doc: &Document, n: &Node, o: &mut serde_json::Map, options: &InspectOptions) { +fn layout_props(doc: &Document, n: &Node, o: &mut serde_json::Map, options: &InspectOptions, grids: &GridCells) { if let Some(l) = &n.layout { insert_prop(o, options, "layout", || camel(serde_json::to_value(l).unwrap_or(Value::Null))); } @@ -303,11 +311,14 @@ fn layout_props(doc: &Document, n: &Node, o: &mut serde_json::Map insert_prop(o, options, k, || json!(v)); } } - if let Some(cell) = (parent_al && options.includes("gridCell")) - .then(|| n.parent.and_then(|p| figcraft_layout::grid_geometry(doc, p))) - .flatten() - .and_then(|g| g.cells.into_iter().find(|c| c.id == n.id)) - { + let cell = (parent_al && options.includes("gridCell")).then_some(n.parent).flatten().and_then(|p| { + let mut grids = grids.borrow_mut(); + let cells = grids + .entry(p) + .or_insert_with(|| figcraft_layout::grid_geometry(doc, p).map(|g| g.cells.into_iter().map(|c| (c.id, c)).collect()).unwrap_or_default()); + cells.get(&n.id).copied() + }); + if let Some(cell) = cell { insert_prop(o, options, "gridCell", || { json!({"column": cell.column, "row": cell.row, "columnSpan": cell.column_span, "rowSpan": cell.row_span, "anchored": cl.grid_column.is_some() || cl.grid_row.is_some(), "alignH": cl.grid_align_h, "alignV": cl.grid_align_v}) diff --git a/crates/engine/src/cmd/layout.rs b/crates/engine/src/cmd/layout.rs index fc40cee88..6403dfd4b 100644 --- a/crates/engine/src/cmd/layout.rs +++ b/crates/engine/src/cmd/layout.rs @@ -270,6 +270,13 @@ mod tests { assert_eq!(info["gridCell"]["column"], json!(2)); assert_eq!(info["gridCell"]["row"], json!(1)); assert_eq!(info["gridCell"]["anchored"], json!(true)); + // Inspected together with the frame, each child still gets its own cell. + let all = crate::inspect::node_json(s.doc().unwrap(), f, 1); + let cells: Vec<(Value, Value)> = + all["children"].as_array().unwrap().iter().map(|c| (c["gridCell"]["column"].clone(), c["gridCell"]["row"].clone())).collect(); + assert_eq!(cells[3], (json!(2), json!(1))); + assert_eq!(cells[0].0, json!(0)); + assert!(cells.iter().all(|(c, r)| c.is_number() && r.is_number()), "{cells:?}"); ex(&mut s, "node.set", json!({"id": kids[3], "props": {"gridColumn": "auto", "gridRow": null}})); assert_eq!(node(&s, kids[3]).child_layout.grid_column, None); // Fewer columns drop the extra tracks. From 6e9582c0a0f395eca36376dcc26a2ea4b58dfccb Mon Sep 17 00:00:00 2001 From: "Dylan M. Taylor" Date: Wed, 11 Nov 2026 14:48:52 -0500 Subject: [PATCH 05/14] codeConnect.import: index components by name and describe each once (#2265) --- crates/engine/src/code_connect_tests.rs | 13 +++++++++++++ crates/engine/src/dev/code_connect.rs | 13 +++++++++---- 2 files changed, 22 insertions(+), 4 deletions(-) diff --git a/crates/engine/src/code_connect_tests.rs b/crates/engine/src/code_connect_tests.rs index 69f655ec6..4474fe03a 100644 --- a/crates/engine/src/code_connect_tests.rs +++ b/crates/engine/src/code_connect_tests.rs @@ -285,3 +285,16 @@ fn vue_layer_trees_import_connected_components_in_script_setup() { assert!(code.starts_with(want), "{code}"); assert_eq!(code.matches("import Card").count(), 1, "{code}"); } + +#[test] +fn many_entries_for_one_component_each_link() { + let mut s = Session::new(); + let (_, hover) = button(&mut s); + let links: Vec = (0..crate::code_connect::MAX_FILE_LINKS) + .map(|i| json!({"component": "Button", "language": if i % 2 == 0 { "react" } else { "vue" }, "name": "Button", "template": "