From 51031d64b817d34e2afee2e088ebdc6d67b8a841 Mon Sep 17 00:00:00 2001 From: Keavon Chambers Date: Tue, 17 Mar 2026 01:41:14 -0700 Subject: [PATCH 1/8] Remove the crate dependency graph website generation intermediate generation step --- .github/workflows/build.yml | 2 +- .github/workflows/website.yml | 5 +- tools/crate-hierarchy-viz/src/main.rs | 65 ++++++++++++++++++- .../generate-crate-hierarchy.ts | 19 ------ website/package-lock.json | 8 --- website/package.json | 2 - website/templates/macros/replacements.html | 4 +- 7 files changed, 67 insertions(+), 38 deletions(-) delete mode 100644 website/.build-scripts/generate-crate-hierarchy.ts diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 784bd919e6..60e8920372 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -176,7 +176,7 @@ jobs: if: github.event_name == 'push' run: | mkdir -p website/generated-new - cargo run -p crate-hierarchy-viz -- website/generated-new/crate_hierarchy.dot + cargo run -p crate-hierarchy-viz -- website/generated-new/crate_hierarchy.svg cargo run -p editor-message-tree -- website/generated-new/hierarchical_message_system_tree.txt - name: šŸ’æ Obtain cache of auto-generated code docs artifacts, to check if they've changed diff --git a/.github/workflows/website.yml b/.github/workflows/website.yml index fafa349598..fd6b5d2a01 100644 --- a/.github/workflows/website.yml +++ b/.github/workflows/website.yml @@ -64,15 +64,14 @@ jobs: rustup update stable echo "šŸ¦€ Latest updated version of Rust:" rustc --version - cargo run -p crate-hierarchy-viz -- website/generated/crate_hierarchy.dot + cargo run -p crate-hierarchy-viz -- website/generated/crate_hierarchy.svg cargo run -p editor-message-tree -- website/generated/hierarchical_message_system_tree.txt - - name: šŸ”§ Build auto-generated code docs artifacts into HTML/SVG + - name: šŸ”§ Build auto-generated code docs artifacts into HTML run: | cd website npm ci npm run generate-editor-structure - npm run generate-crate-hierarchy - name: šŸ“ƒ Generate node catalog documentation run: cargo run -p node-docs -- website/content/learn/node-catalog diff --git a/tools/crate-hierarchy-viz/src/main.rs b/tools/crate-hierarchy-viz/src/main.rs index 837b38768d..7b43b7b872 100644 --- a/tools/crate-hierarchy-viz/src/main.rs +++ b/tools/crate-hierarchy-viz/src/main.rs @@ -2,7 +2,9 @@ use anyhow::{Context, Result}; use serde::Deserialize; use std::collections::{HashMap, HashSet}; use std::fs; +use std::io::Write; use std::path::PathBuf; +use std::process::Command; #[derive(Debug, Deserialize)] struct WorkspaceToml { @@ -173,17 +175,76 @@ fn main() -> Result<()> { remove_transitive_dependencies(&mut crates); - // Generate DOT format and write to output file + // Generate DOT format, convert to SVG, and write to output file let dot_content = generate_dot(&crates); + let svg_content = dot_to_svg(&dot_content)?; if let Some(parent) = output_path.parent() { fs::create_dir_all(parent).with_context(|| format!("Failed to create directory {:?}", parent))?; } - fs::write(&output_path, &dot_content).with_context(|| format!("Failed to write to {:?}", output_path))?; + fs::write(&output_path, &svg_content).with_context(|| format!("Failed to write to {:?}", output_path))?; Ok(()) } +/// Convert a DOT graph string to SVG by shelling out to @viz-js/viz via Node.js +fn dot_to_svg(dot: &str) -> Result { + let temp_dir = std::env::temp_dir().join("crate-hierarchy-viz"); + fs::create_dir_all(&temp_dir).with_context(|| "Failed to create temp directory")?; + + // Install @viz-js/viz into the temp directory if not already present + let node_modules = temp_dir.join("node_modules").join("@viz-js"); + if !node_modules.exists() { + let npm = if cfg!(target_os = "windows") { "npm.cmd" } else { "npm" }; + let status = Command::new(npm) + .args(["install", "--prefix", &temp_dir.to_string_lossy(), "@viz-js/viz"]) + .stdout(std::process::Stdio::null()) + .stderr(std::process::Stdio::piped()) + .status() + .with_context(|| "Failed to run `npm install`. Is Node.js installed?")?; + if !status.success() { + anyhow::bail!("Executing `npm install @viz-js/viz` failed"); + } + } + + // Write a small script that reads DOT from stdin and outputs SVG + let script_path = temp_dir.join("convert.mjs"); + fs::write( + &script_path, + r#" + import { instance } from "@viz-js/viz"; + let dot = ""; + for await (const chunk of process.stdin) dot += chunk; + const viz = await instance(); + process.stdout.write(viz.renderString(dot, { format: "svg" })); + "# + .trim(), + )?; + + let mut child = Command::new("node") + .arg(&script_path) + .stdin(std::process::Stdio::piped()) + .stdout(std::process::Stdio::piped()) + .stderr(std::process::Stdio::piped()) + .spawn() + .with_context(|| "Failed to spawn `node`. Is Node.js installed?")?; + + // Write DOT content to stdin then close the pipe + child.stdin.take().unwrap().write_all(dot.as_bytes()).with_context(|| "Failed to write DOT content to stdin")?; + + let output = child.wait_with_output().with_context(|| "Failed to wait for `node` process")?; + + // Clean up the temp script (node_modules is intentionally kept as a cache) + let _ = fs::remove_file(&script_path); + + if !output.status.success() { + let stderr = String::from_utf8_lossy(&output.stderr); + anyhow::bail!("DOT to SVG conversion failed (exit code {:?}):\n{}", output.status.code(), stderr); + } + + String::from_utf8(output.stdout).with_context(|| "SVG output was not valid UTF-8") +} + fn generate_dot(crates: &[CrateInfo]) -> String { let mut out = String::new(); out.push_str("digraph CrateHierarchy {\n"); diff --git a/website/.build-scripts/generate-crate-hierarchy.ts b/website/.build-scripts/generate-crate-hierarchy.ts deleted file mode 100644 index aadc853e07..0000000000 --- a/website/.build-scripts/generate-crate-hierarchy.ts +++ /dev/null @@ -1,19 +0,0 @@ -/* eslint-disable no-console */ - -import fs from "fs"; - -import { instance } from "@viz-js/viz"; - -const [inputFile, outputFile] = process.argv.slice(2); -if (!inputFile || !outputFile) { - console.error("Usage: node generate-crate-hierarchy.ts "); - process.exit(1); -} - -const dot = fs.readFileSync(inputFile, "utf-8"); - -const viz = await instance(); -const svg = viz.renderString(dot, { format: "svg" }); - -fs.writeFileSync(outputFile, svg); -console.log(`SVG output written to: ${outputFile}`); diff --git a/website/package-lock.json b/website/package-lock.json index 59a81e65ec..c038887f85 100644 --- a/website/package-lock.json +++ b/website/package-lock.json @@ -16,7 +16,6 @@ "@eslint/eslintrc": "^3.3.3", "@eslint/js": "^9.39.2", "@types/node": "^25.0.9", - "@viz-js/viz": "^3.25.0", "eslint": "^9.39.2", "eslint-config-prettier": "^10.1.8", "eslint-import-resolver-typescript": "^4.4.4", @@ -1233,13 +1232,6 @@ "win32" ] }, - "node_modules/@viz-js/viz": { - "version": "3.25.0", - "resolved": "https://registry.npmjs.org/@viz-js/viz/-/viz-3.25.0.tgz", - "integrity": "sha512-dM7zAYMdf7mcRz5Kdb+YJb6+qv5Rjk0rPZ18gROdpMrP/3S7RFOp8uxybeiz5RypHrE1zo1vccA8Twh4mIcLZw==", - "dev": true, - "license": "MIT" - }, "node_modules/acorn": { "version": "8.15.0", "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", diff --git a/website/package.json b/website/package.json index d06c6a1752..0423d2cf91 100644 --- a/website/package.json +++ b/website/package.json @@ -13,7 +13,6 @@ "scripts": { "postinstall": "node .build-scripts/install.ts", "generate-editor-structure": "node .build-scripts/generate-editor-structure.ts generated/hierarchical_message_system_tree.txt generated/hierarchical_message_system_tree.html", - "generate-crate-hierarchy": "node .build-scripts/generate-crate-hierarchy.ts generated/crate_hierarchy.dot generated/crate_hierarchy.svg", "check": "tsc --noEmit && eslint", "fix": "eslint --fix" }, @@ -22,7 +21,6 @@ "@eslint/eslintrc": "^3.3.3", "@eslint/js": "^9.39.2", "@types/node": "^25.0.9", - "@viz-js/viz": "^3.25.0", "eslint": "^9.39.2", "eslint-config-prettier": "^10.1.8", "eslint-import-resolver-typescript": "^4.4.4", diff --git a/website/templates/macros/replacements.html b/website/templates/macros/replacements.html index e201d41460..3e62d40d94 100644 --- a/website/templates/macros/replacements.html +++ b/website/templates/macros/replacements.html @@ -57,8 +57,6 @@

{{ article.title } TO TEST IT LOCALLY, FROM THE ROOT OF THE PROJECT, RUN: -cargo run -p crate-hierarchy-viz -- website/generated/crate_hierarchy.dot -cd website -npm run generate-crate-hierarchy" -%} +cargo run -p crate-hierarchy-viz -- website/generated/crate_hierarchy.svg" -%} {{ content | default(value = fallback) | safe }} {% endmacro crate_hierarchy %} From bfcb7d526ced84c01f306c80f81e4b2ad1693a08 Mon Sep 17 00:00:00 2001 From: Keavon Chambers Date: Tue, 17 Mar 2026 04:17:14 -0700 Subject: [PATCH 2/8] Remove the message system tree website generation intermediate generation step --- .github/workflows/build.yml | 5 +- .github/workflows/website.yml | 7 +- tools/crate-hierarchy-viz/src/main.rs | 9 +- tools/editor-message-tree/src/main.rs | 236 +++++++++++++++--- .../generate-editor-structure.ts | 122 --------- .../codebase-overview/editor-structure.md | 2 +- website/package.json | 1 - website/templates/macros/replacements.html | 10 +- 8 files changed, 220 insertions(+), 172 deletions(-) delete mode 100644 website/.build-scripts/generate-editor-structure.ts diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 60e8920372..254cac2a88 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -175,9 +175,8 @@ jobs: - name: šŸ“ƒ Generate code documentation info for website if: github.event_name == 'push' run: | - mkdir -p website/generated-new - cargo run -p crate-hierarchy-viz -- website/generated-new/crate_hierarchy.svg - cargo run -p editor-message-tree -- website/generated-new/hierarchical_message_system_tree.txt + cargo run -p crate-hierarchy-viz -- website/generated-new + cargo run -p editor-message-tree -- website/generated-new - name: šŸ’æ Obtain cache of auto-generated code docs artifacts, to check if they've changed if: github.event_name == 'push' diff --git a/.github/workflows/website.yml b/.github/workflows/website.yml index fd6b5d2a01..65107cde2c 100644 --- a/.github/workflows/website.yml +++ b/.github/workflows/website.yml @@ -64,14 +64,13 @@ jobs: rustup update stable echo "šŸ¦€ Latest updated version of Rust:" rustc --version - cargo run -p crate-hierarchy-viz -- website/generated/crate_hierarchy.svg - cargo run -p editor-message-tree -- website/generated/hierarchical_message_system_tree.txt + cargo run -p crate-hierarchy-viz -- website/generated + cargo run -p editor-message-tree -- website/generated - - name: šŸ”§ Build auto-generated code docs artifacts into HTML + - name: šŸ”§ Install website npm dependencies run: | cd website npm ci - npm run generate-editor-structure - name: šŸ“ƒ Generate node catalog documentation run: cargo run -p node-docs -- website/content/learn/node-catalog diff --git a/tools/crate-hierarchy-viz/src/main.rs b/tools/crate-hierarchy-viz/src/main.rs index 7b43b7b872..c0a47a0b21 100644 --- a/tools/crate-hierarchy-viz/src/main.rs +++ b/tools/crate-hierarchy-viz/src/main.rs @@ -85,10 +85,11 @@ fn collect_all_dependencies(crate_name: &str, dep_map: &HashMap Result<()> { - let output_path = std::env::args_os() + let output_dir = std::env::args_os() .nth(1) .map(PathBuf::from) - .ok_or_else(|| anyhow::anyhow!("Usage: crate-hierarchy-viz "))?; + .ok_or_else(|| anyhow::anyhow!("Usage: crate-hierarchy-viz "))?; + let output_path = output_dir.join("crate-hierarchy.svg"); let workspace_root = std::env::current_dir().unwrap(); let workspace_toml_path = workspace_root.join("Cargo.toml"); @@ -179,9 +180,7 @@ fn main() -> Result<()> { let dot_content = generate_dot(&crates); let svg_content = dot_to_svg(&dot_content)?; - if let Some(parent) = output_path.parent() { - fs::create_dir_all(parent).with_context(|| format!("Failed to create directory {:?}", parent))?; - } + fs::create_dir_all(&output_dir).with_context(|| format!("Failed to create directory {:?}", output_dir))?; fs::write(&output_path, &svg_content).with_context(|| format!("Failed to write to {:?}", output_path))?; Ok(()) diff --git a/tools/editor-message-tree/src/main.rs b/tools/editor-message-tree/src/main.rs index 8b9108f895..bb072caa7e 100644 --- a/tools/editor-message-tree/src/main.rs +++ b/tools/editor-message-tree/src/main.rs @@ -4,27 +4,45 @@ use std::io::Write; use std::path::PathBuf; fn main() -> Result<(), Box> { - let output_path = std::env::args_os().nth(1).map(PathBuf::from).ok_or("Usage: editor-message-tree ")?; + let output_dir = std::env::args_os().nth(1).map(PathBuf::from).ok_or("Usage: editor-message-tree ")?; + std::fs::create_dir_all(&output_dir)?; - if let Some(parent) = output_path.parent() { - std::fs::create_dir_all(parent).unwrap(); + let tree = Message::message_tree(); + + // Write the .txt file (plain text tree outline, served as a static download) + let static_dir = output_dir.join("../static/volunteer/guide/codebase-overview"); + std::fs::create_dir_all(&static_dir)?; + let mut txt_file = std::fs::File::create(static_dir.join("hierarchical-message-system-tree.txt"))?; + write_tree_txt(&tree, &mut txt_file); + + // Write the .html file (structured HTML embedded in the website page) + let mut html = String::new(); + write_tree_html(&tree, &mut html); + std::fs::write(output_dir.join("hierarchical-message-system-tree.html"), &html)?; + + Ok(()) +} + +// ================= +// PLAIN TEXT OUTPUT +// ================= + +fn write_tree_txt(tree: &DebugMessageTree, file: &mut std::fs::File) { + if tree.path().is_empty() { + let _ = file.write_all(format!("{}\n", tree.name()).as_bytes()); + } else { + let _ = file.write_all(format!("{} `{}#L{}`\n", tree.name(), tree.path(), tree.line_number()).as_bytes()); } - let result = Message::message_tree(); - let mut file = std::fs::File::create(&output_path).unwrap(); - file.write_all(format!("{} `{}#L{}`\n", result.name(), result.path(), result.line_number()).as_bytes()).unwrap(); - if let Some(variants) = result.variants() { + if let Some(variants) = tree.variants() { for (i, variant) in variants.iter().enumerate() { let is_last = i == variants.len() - 1; - print_tree_node(variant, "", is_last, &mut file); + write_tree_txt_node(variant, "", is_last, file); } } - - Ok(()) } -fn print_tree_node(tree: &DebugMessageTree, prefix: &str, is_last: bool, file: &mut std::fs::File) { - // Print the current node +fn write_tree_txt_node(tree: &DebugMessageTree, prefix: &str, is_last: bool, file: &mut std::fs::File) { let (branch, child_prefix) = if tree.message_handler_data_fields().is_some() || tree.message_handler_fields().is_some() { ("ā”œā”€ā”€ ", format!("{prefix}│ ")) } else if is_last { @@ -34,33 +52,28 @@ fn print_tree_node(tree: &DebugMessageTree, prefix: &str, is_last: bool, file: & }; if tree.path().is_empty() { - file.write_all(format!("{}{}{}\n", prefix, branch, tree.name()).as_bytes()).unwrap(); + let _ = file.write_all(format!("{}{}{}\n", prefix, branch, tree.name()).as_bytes()); } else { - file.write_all(format!("{}{}{} `{}#L{}`\n", prefix, branch, tree.name(), tree.path(), tree.line_number()).as_bytes()) - .unwrap(); + let _ = file.write_all(format!("{}{}{} `{}#L{}`\n", prefix, branch, tree.name(), tree.path(), tree.line_number()).as_bytes()); } - // Print children if any if let Some(variants) = tree.variants() { let len = variants.len(); for (i, variant) in variants.iter().enumerate() { let is_last_child = i == len - 1; - print_tree_node(variant, &child_prefix, is_last_child, file); + write_tree_txt_node(variant, &child_prefix, is_last_child, file); } } - // Print message field if any if let Some(fields) = tree.fields() { let len = fields.len(); for (i, field) in fields.iter().enumerate() { let is_last_field = i == len - 1; let branch = if is_last_field { "└── " } else { "ā”œā”€ā”€ " }; - - file.write_all(format!("{child_prefix}{branch}{field}\n").as_bytes()).unwrap(); + let _ = file.write_all(format!("{child_prefix}{branch}{field}\n").as_bytes()); } } - // Print handler field if any if let Some(data) = tree.message_handler_fields() { let len = data.fields().len(); let (branch, child_prefix) = if tree.message_handler_data_fields().is_some() { @@ -73,32 +86,195 @@ fn print_tree_node(tree: &DebugMessageTree, prefix: &str, is_last: bool, file: & if data.name().is_empty() && tree.name() != FRONTEND_MESSAGE_STR { panic!("{}'s MessageHandler is missing #[message_handler_data]", tree.name()); } else if tree.name() != FRONTEND_MESSAGE_STR { - file.write_all(format!("{}{}{} `{}#L{}`\n", prefix, branch, data.name(), data.path(), data.line_number()).as_bytes()) - .unwrap(); + let _ = file.write_all(format!("{}{}{} `{}#L{}`\n", prefix, branch, data.name(), data.path(), data.line_number()).as_bytes()); for (i, field) in data.fields().iter().enumerate() { let is_last_field = i == len - 1; let branch = if is_last_field { "└── " } else { "ā”œā”€ā”€ " }; - - file.write_all(format!("{}{}{}\n", child_prefix, branch, field.0).as_bytes()).unwrap(); + let _ = file.write_all(format!("{}{}{}\n", child_prefix, branch, field.0).as_bytes()); } } } - // Print data field if any if let Some(data) = tree.message_handler_data_fields() { let len = data.fields().len(); if data.path().is_empty() { - file.write_all(format!("{}{}{}\n", prefix, "└── ", data.name()).as_bytes()).unwrap(); + let _ = file.write_all(format!("{}{}{}\n", prefix, "└── ", data.name()).as_bytes()); } else { - file.write_all(format!("{}{}{} `{}#L{}`\n", prefix, "└── ", data.name(), data.path(), data.line_number()).as_bytes()) - .unwrap(); + let _ = file.write_all(format!("{}{}{} `{}#L{}`\n", prefix, "└── ", data.name(), data.path(), data.line_number()).as_bytes()); } for (i, field) in data.fields().iter().enumerate() { let is_last_field = i == len - 1; let branch = if is_last_field { "└── " } else { "ā”œā”€ā”€ " }; let field = &field.0; - file.write_all(format!("{prefix} {branch}{field}\n").as_bytes()).unwrap(); + let _ = file.write_all(format!("{prefix} {branch}{field}\n").as_bytes()); + } + } +} + +// =========== +// HTML OUTPUT +// =========== + +const GITHUB_BASE: &str = "https://github.com/GraphiteEditor/Graphite/blob/master/"; +const NAMING_SUFFIXES: &[&str] = &["Message", "MessageHandler", "MessageContext"]; + +fn escape_html(s: &str) -> String { + s.replace('&', "&").replace('<', "<").replace('>', ">") +} + +fn github_link(path: &str, line: usize) -> String { + let path = path.replace('\\', "/"); + let filename = path.rsplit('/').next().unwrap_or(&path); + format!(r#"{filename}:{line}"#) +} + +fn naming_convention_warning(name: &str) -> &'static str { + // Strip generic parameters for the check (e.g. `Foo` -> `Foo`) + let base_name = name.split('<').next().unwrap_or(name); + if NAMING_SUFFIXES.iter().any(|suffix| base_name.ends_with(suffix)) { + "" + } else { + r#"(violates naming convention — should end with 'Message', 'MessageHandler', or 'MessageContext')"# + } +} + +fn write_tree_html(tree: &DebugMessageTree, out: &mut String) { + // Root node + let link = if !tree.path().is_empty() { github_link(tree.path(), tree.line_number()) } else { String::new() }; + let escaped_name = escape_html(tree.name()); + + out.push_str("
    \n"); + out.push_str(&format!(r#"
  • {escaped_name}{link}"#)); + + if let Some(variants) = tree.variants() { + out.push_str(r#"
    "#); + write_tree_html_children(variants, out); + out.push_str("
    "); + } + + out.push_str("
  • \n
\n"); +} + +fn write_tree_html_children(variants: &[DebugMessageTree], out: &mut String) { + out.push_str("
    \n"); + for variant in variants { + write_tree_html_node(variant, out); + } + out.push_str("
\n"); +} + +fn write_tree_html_node(tree: &DebugMessageTree, out: &mut String) { + let has_link = !tree.path().is_empty(); + let link = if has_link { github_link(tree.path(), tree.line_number()) } else { String::new() }; + let escaped_name = escape_html(tree.name()); + + enum HtmlChild<'a> { + Subtree(&'a DebugMessageTree), + Field(String), + HandlerFields(String, String, usize, Vec), + DataFields(String, String, usize, Vec), + } + + // Collect all child entries for this node + let mut children: Vec = Vec::new(); + + if let Some(variants) = tree.variants() { + for variant in variants { + children.push(HtmlChild::Subtree(variant)); + } + } + + if let Some(fields) = tree.fields() { + for field in fields { + children.push(HtmlChild::Field(field.to_string())); + } + } + + const FRONTEND_MESSAGE_STR: &str = "FrontendMessage"; + if let Some(data) = tree.message_handler_fields() + && (!data.name().is_empty() || tree.name() == FRONTEND_MESSAGE_STR) + && tree.name() != FRONTEND_MESSAGE_STR + { + children.push(HtmlChild::HandlerFields( + data.name().to_string(), + data.path().to_string(), + data.line_number(), + data.fields().iter().map(|f| f.0.clone()).collect(), + )); + } + + if let Some(data) = tree.message_handler_data_fields() { + children.push(HtmlChild::DataFields( + data.name().to_string(), + data.path().to_string(), + data.line_number(), + data.fields().iter().map(|f| f.0.clone()).collect(), + )); + } + + let has_children = !children.is_empty(); + let has_deeper_children = children.iter().any(|child| matches!(child, HtmlChild::Subtree(t) if t.variants().is_some() || t.fields().is_some())); + + // Determine role + let role = if has_link { + "subsystem" + } else if has_deeper_children { + "submessage" + } else { + "message" + }; + + // Naming convention warning (only for linked/subsystem nodes) + let warning = if has_link { naming_convention_warning(tree.name()) } else { "" }; + + if has_children { + out.push_str(&format!(r#"
  • {escaped_name}{link}{warning}"#)); + out.push_str(r#"
      "#); + out.push('\n'); + + for child in &children { + match child { + HtmlChild::Subtree(subtree) => write_tree_html_node(subtree, out), + HtmlChild::Field(field) => write_field_html(field, out), + HtmlChild::HandlerFields(name, path, line, fields) => write_handler_or_data_html(name, path, *line, fields, out), + HtmlChild::DataFields(name, path, line, fields) => write_handler_or_data_html(name, path, *line, fields, out), + } + } + + out.push_str("
    \n
  • \n"); + } else { + out.push_str(&format!(r#"
  • {escaped_name}{link}{warning}
  • "#)); + out.push('\n'); + } +} + +fn write_field_html(field: &str, out: &mut String) { + if let Some((name, ty)) = field.split_once(':') { + let name = escape_html(name.trim()); + let ty = escape_html(ty.trim()); + out.push_str(&format!(r#"
  • {name}: {ty}
  • "#)); + } else { + let escaped = escape_html(field); + out.push_str(&format!(r#"
  • {escaped}
  • "#)); + } + out.push('\n'); +} + +fn write_handler_or_data_html(name: &str, path: &str, line: usize, fields: &[String], out: &mut String) { + let escaped_name = escape_html(name); + let link = if !path.is_empty() { github_link(path, line) } else { String::new() }; + let warning = if !path.is_empty() { naming_convention_warning(name) } else { "" }; + + if fields.is_empty() { + out.push_str(&format!(r#"
  • {escaped_name}{link}{warning}
  • "#)); + } else { + out.push_str(&format!(r#"
  • {escaped_name}{link}{warning}"#)); + out.push_str(r#"
      "#); + out.push('\n'); + for field in fields { + write_field_html(field, out); } + out.push_str("
    \n
  • \n"); } } diff --git a/website/.build-scripts/generate-editor-structure.ts b/website/.build-scripts/generate-editor-structure.ts deleted file mode 100644 index 221a5b9292..0000000000 --- a/website/.build-scripts/generate-editor-structure.ts +++ /dev/null @@ -1,122 +0,0 @@ -// TODO: Port this script to Rust as part of `tools/editor-message-tree/src/main.rs` - -/* eslint-disable no-console */ - -import fs from "fs"; -import path from "path"; - -type Entry = { level: number; text: string; link: string | undefined }; - -/// Parses a single line of the input text. -function parseLine(line: string) { - const linkRegex = /`([^`]+)`$/; - const linkMatch = line.match(linkRegex); - let link = undefined; - - if (linkMatch) { - const filePath = linkMatch[1].replace(/\\/g, "/"); - link = `https://github.com/GraphiteEditor/Graphite/blob/master/${filePath}`; - } - - const textContent = line - .replace(/^[\sā”‚ā”œā””ā”€]*/, "") - .replace(linkRegex, "") - .trim(); - const indentation = line.indexOf(textContent); - // Each level of indentation is 4 characters. - const level = Math.floor(indentation / 4); - - return { level, text: textContent, link }; -} - -/// Recursively builds the HTML list from the parsed nodes. -function buildHtmlList(nodes: Entry[], currentIndex: number, currentLevel: number) { - if (currentIndex >= nodes.length) { - return { html: "", nextIndex: currentIndex }; - } - - let html = "
      \n"; - let i = currentIndex; - - while (i < nodes.length && nodes[i].level >= currentLevel) { - const node = nodes[i]; - - if (node.level > currentLevel) { - // This case handles malformed input, skip to next valid line - i++; - continue; - } - - const hasDirectChildren = i + 1 < nodes.length && nodes[i + 1].level > node.level; - const hasDeeperChildren = hasDirectChildren && i + 2 < nodes.length && nodes[i + 2].level > nodes[i + 1].level; - - const linkHtml = node.link ? `${path.basename(node.link.split("#L").join(":"))}` : ""; - const fieldPieces = node.text.match(/([^:]*):(.*)/); - let escapedText; - if (fieldPieces && fieldPieces.length === 3) { - escapedText = [escapeHtml(fieldPieces[1].trim()), escapeHtml(fieldPieces[2].trim())]; - } else { - escapedText = [escapeHtml(node.text)]; - } - - let role = "message"; - if (node.link) role = "subsystem"; - else if (hasDeeperChildren) role = "submessage"; - else if (escapedText.length === 2) role = "field"; - - const partOfMessageFromNamingConvention = ["Message", "MessageHandler", "MessageContext"].some((suffix) => node.text.replace(/(.*)<.*>/g, "$1").endsWith(suffix)); - const partOfMessageViolatesNamingConvention = node.link && !partOfMessageFromNamingConvention; - const violatesNamingConvention = partOfMessageViolatesNamingConvention - ? "(violates naming convention — should end with 'Message', 'MessageHandler', or 'MessageContext')" - : ""; - - if (hasDirectChildren) { - html += `
    • ${escapedText}${linkHtml}${violatesNamingConvention}`; - const childResult = buildHtmlList(nodes, i + 1, node.level + 1); - html += `
      ${childResult.html}
    • \n`; - i = childResult.nextIndex; - } else if (role === "field") { - html += `
    • ${escapedText[0]}: ${escapedText[1]}${linkHtml}
    • \n`; - i++; - } else { - html += `
    • ${escapedText[0]}${linkHtml}${violatesNamingConvention}
    • \n`; - i++; - } - } - - html += "
    \n"; - return { html, nextIndex: i }; -} - -function escapeHtml(text: string) { - return text.replace(//g, ">"); -} - -const inputFile = process.argv[2]; -const outputFile = process.argv[3]; - -if (!inputFile || !outputFile) { - console.error("Error: Please provide the input text and output HTML file paths as arguments."); - console.log("Usage: node generate-editor-structure.ts "); - process.exit(1); -} - -if (!fs.existsSync(inputFile)) { - console.error(`Error: File not found at "${inputFile}"`); - process.exit(1); -} - -try { - const fileContent = fs.readFileSync(inputFile, "utf-8"); - const lines = fileContent.split(/\r?\n/).filter((line) => line.trim() !== "" && !line.startsWith("// filepath:")); - const parsedNodes = lines.map(parseLine); - - const { html } = buildHtmlList(parsedNodes, 0, 0); - - fs.writeFileSync(outputFile, html, "utf-8"); - - console.log(`Successfully generated HTML outline at: ${outputFile}`); -} catch (error) { - console.error("An error occurred during processing:", error); - process.exit(1); -} diff --git a/website/content/volunteer/guide/codebase-overview/editor-structure.md b/website/content/volunteer/guide/codebase-overview/editor-structure.md index e7bb07aeb9..9177b047b6 100644 --- a/website/content/volunteer/guide/codebase-overview/editor-structure.md +++ b/website/content/volunteer/guide/codebase-overview/editor-structure.md @@ -22,7 +22,7 @@ The dispatcher lives at the root of the editor hierarchy and acts as the owner o cargo run explore editor ``` -Click to explore the outline of the editor subsystem hierarchy which forms the structure of the editor's subsystems, state, and interactions. +Click to explore the outline of the editor subsystem hierarchy which forms the structure of the editor's subsystems, state, and interactions. Also available as a searchable plain text file.
    diff --git a/website/package.json b/website/package.json index 0423d2cf91..7423113691 100644 --- a/website/package.json +++ b/website/package.json @@ -12,7 +12,6 @@ "type": "module", "scripts": { "postinstall": "node .build-scripts/install.ts", - "generate-editor-structure": "node .build-scripts/generate-editor-structure.ts generated/hierarchical_message_system_tree.txt generated/hierarchical_message_system_tree.html", "check": "tsc --noEmit && eslint", "fix": "eslint --fix" }, diff --git a/website/templates/macros/replacements.html b/website/templates/macros/replacements.html index 3e62d40d94..bace183903 100644 --- a/website/templates/macros/replacements.html +++ b/website/templates/macros/replacements.html @@ -40,23 +40,21 @@

    {{ article.title } {% endmacro text_balancer %} {% macro hierarchical_message_system_tree() %} -{%- set content = load_data(path = "../generated/hierarchical_message_system_tree.html", format = "plain", required = false) -%} +{%- set content = load_data(path = "../generated/hierarchical-message-system-tree.html", format = "plain", required = false) -%} {%- set fallback = "
    THIS CONTENT IS FILLED IN WHEN CI BUILDS THE WEBSITE.
     
     TO TEST IT LOCALLY, FROM THE ROOT OF THE PROJECT, RUN:
     
    -cargo run -p editor-message-tree -- website/generated/hierarchical_message_system_tree.txt
    -cd website
    -npm run generate-editor-structure
    " -%} +cargo run -p editor-message-tree -- website/generated" -%} {{ content | default(value = fallback) | safe }} {% endmacro hierarchical_message_system_tree %} {% macro crate_hierarchy() %} -{%- set content = load_data(path = "../generated/crate_hierarchy.svg", format = "plain", required = false) -%} +{%- set content = load_data(path = "../generated/crate-hierarchy.svg", format = "plain", required = false) -%} {%- set fallback = "
    THIS CONTENT IS FILLED IN WHEN CI BUILDS THE WEBSITE.
     
     TO TEST IT LOCALLY, FROM THE ROOT OF THE PROJECT, RUN:
     
    -cargo run -p crate-hierarchy-viz -- website/generated/crate_hierarchy.svg
    " -%} +cargo run -p crate-hierarchy-viz -- website/generated" -%} {{ content | default(value = fallback) | safe }} {% endmacro crate_hierarchy %} From 00ac388d859d5d586bab52c06abb2e6b3cda6bd4 Mon Sep 17 00:00:00 2001 From: Keavon Chambers Date: Tue, 17 Mar 2026 04:37:33 -0700 Subject: [PATCH 3/8] Code cleanup --- tools/crate-hierarchy-viz/src/main.rs | 13 ++++++--- tools/editor-message-tree/src/main.rs | 38 +++++++++++++++------------ 2 files changed, 30 insertions(+), 21 deletions(-) diff --git a/tools/crate-hierarchy-viz/src/main.rs b/tools/crate-hierarchy-viz/src/main.rs index c0a47a0b21..a53eff48c1 100644 --- a/tools/crate-hierarchy-viz/src/main.rs +++ b/tools/crate-hierarchy-viz/src/main.rs @@ -91,7 +91,7 @@ fn main() -> Result<()> { .ok_or_else(|| anyhow::anyhow!("Usage: crate-hierarchy-viz "))?; let output_path = output_dir.join("crate-hierarchy.svg"); - let workspace_root = std::env::current_dir().unwrap(); + let workspace_root = std::env::current_dir()?; let workspace_toml_path = workspace_root.join("Cargo.toml"); // Parse workspace Cargo.toml @@ -192,8 +192,8 @@ fn dot_to_svg(dot: &str) -> Result { fs::create_dir_all(&temp_dir).with_context(|| "Failed to create temp directory")?; // Install @viz-js/viz into the temp directory if not already present - let node_modules = temp_dir.join("node_modules").join("@viz-js"); - if !node_modules.exists() { + let viz_package = temp_dir.join("node_modules").join("@viz-js").join("viz"); + if !viz_package.exists() { let npm = if cfg!(target_os = "windows") { "npm.cmd" } else { "npm" }; let status = Command::new(npm) .args(["install", "--prefix", &temp_dir.to_string_lossy(), "@viz-js/viz"]) @@ -229,7 +229,12 @@ fn dot_to_svg(dot: &str) -> Result { .with_context(|| "Failed to spawn `node`. Is Node.js installed?")?; // Write DOT content to stdin then close the pipe - child.stdin.take().unwrap().write_all(dot.as_bytes()).with_context(|| "Failed to write DOT content to stdin")?; + child + .stdin + .take() + .context("Failed to get stdin handle for node process")? + .write_all(dot.as_bytes()) + .with_context(|| "Failed to write DOT content to stdin")?; let output = child.wait_with_output().with_context(|| "Failed to wait for `node` process")?; diff --git a/tools/editor-message-tree/src/main.rs b/tools/editor-message-tree/src/main.rs index bb072caa7e..58e5c4c0ec 100644 --- a/tools/editor-message-tree/src/main.rs +++ b/tools/editor-message-tree/src/main.rs @@ -3,6 +3,8 @@ use editor::utility_types::DebugMessageTree; use std::io::Write; use std::path::PathBuf; +const FRONTEND_MESSAGE_STR: &str = "FrontendMessage"; + fn main() -> Result<(), Box> { let output_dir = std::env::args_os().nth(1).map(PathBuf::from).ok_or("Usage: editor-message-tree ")?; std::fs::create_dir_all(&output_dir)?; @@ -13,7 +15,7 @@ fn main() -> Result<(), Box> { let static_dir = output_dir.join("../static/volunteer/guide/codebase-overview"); std::fs::create_dir_all(&static_dir)?; let mut txt_file = std::fs::File::create(static_dir.join("hierarchical-message-system-tree.txt"))?; - write_tree_txt(&tree, &mut txt_file); + write_tree_txt(&tree, &mut txt_file)?; // Write the .html file (structured HTML embedded in the website page) let mut html = String::new(); @@ -27,22 +29,24 @@ fn main() -> Result<(), Box> { // PLAIN TEXT OUTPUT // ================= -fn write_tree_txt(tree: &DebugMessageTree, file: &mut std::fs::File) { +fn write_tree_txt(tree: &DebugMessageTree, file: &mut std::fs::File) -> std::io::Result<()> { if tree.path().is_empty() { - let _ = file.write_all(format!("{}\n", tree.name()).as_bytes()); + file.write_all(format!("{}\n", tree.name()).as_bytes())?; } else { - let _ = file.write_all(format!("{} `{}#L{}`\n", tree.name(), tree.path(), tree.line_number()).as_bytes()); + file.write_all(format!("{} `{}#L{}`\n", tree.name(), tree.path(), tree.line_number()).as_bytes())?; } if let Some(variants) = tree.variants() { for (i, variant) in variants.iter().enumerate() { let is_last = i == variants.len() - 1; - write_tree_txt_node(variant, "", is_last, file); + write_tree_txt_node(variant, "", is_last, file)?; } } + + Ok(()) } -fn write_tree_txt_node(tree: &DebugMessageTree, prefix: &str, is_last: bool, file: &mut std::fs::File) { +fn write_tree_txt_node(tree: &DebugMessageTree, prefix: &str, is_last: bool, file: &mut std::fs::File) -> std::io::Result<()> { let (branch, child_prefix) = if tree.message_handler_data_fields().is_some() || tree.message_handler_fields().is_some() { ("ā”œā”€ā”€ ", format!("{prefix}│ ")) } else if is_last { @@ -52,16 +56,16 @@ fn write_tree_txt_node(tree: &DebugMessageTree, prefix: &str, is_last: bool, fil }; if tree.path().is_empty() { - let _ = file.write_all(format!("{}{}{}\n", prefix, branch, tree.name()).as_bytes()); + file.write_all(format!("{}{}{}\n", prefix, branch, tree.name()).as_bytes())?; } else { - let _ = file.write_all(format!("{}{}{} `{}#L{}`\n", prefix, branch, tree.name(), tree.path(), tree.line_number()).as_bytes()); + file.write_all(format!("{}{}{} `{}#L{}`\n", prefix, branch, tree.name(), tree.path(), tree.line_number()).as_bytes())?; } if let Some(variants) = tree.variants() { let len = variants.len(); for (i, variant) in variants.iter().enumerate() { let is_last_child = i == len - 1; - write_tree_txt_node(variant, &child_prefix, is_last_child, file); + write_tree_txt_node(variant, &child_prefix, is_last_child, file)?; } } @@ -70,7 +74,7 @@ fn write_tree_txt_node(tree: &DebugMessageTree, prefix: &str, is_last: bool, fil for (i, field) in fields.iter().enumerate() { let is_last_field = i == len - 1; let branch = if is_last_field { "└── " } else { "ā”œā”€ā”€ " }; - let _ = file.write_all(format!("{child_prefix}{branch}{field}\n").as_bytes()); + file.write_all(format!("{child_prefix}{branch}{field}\n").as_bytes())?; } } @@ -82,16 +86,15 @@ fn write_tree_txt_node(tree: &DebugMessageTree, prefix: &str, is_last: bool, fil ("└── ", format!("{prefix} ")) }; - const FRONTEND_MESSAGE_STR: &str = "FrontendMessage"; if data.name().is_empty() && tree.name() != FRONTEND_MESSAGE_STR { panic!("{}'s MessageHandler is missing #[message_handler_data]", tree.name()); } else if tree.name() != FRONTEND_MESSAGE_STR { - let _ = file.write_all(format!("{}{}{} `{}#L{}`\n", prefix, branch, data.name(), data.path(), data.line_number()).as_bytes()); + file.write_all(format!("{}{}{} `{}#L{}`\n", prefix, branch, data.name(), data.path(), data.line_number()).as_bytes())?; for (i, field) in data.fields().iter().enumerate() { let is_last_field = i == len - 1; let branch = if is_last_field { "└── " } else { "ā”œā”€ā”€ " }; - let _ = file.write_all(format!("{}{}{}\n", child_prefix, branch, field.0).as_bytes()); + file.write_all(format!("{}{}{}\n", child_prefix, branch, field.0).as_bytes())?; } } } @@ -99,17 +102,19 @@ fn write_tree_txt_node(tree: &DebugMessageTree, prefix: &str, is_last: bool, fil if let Some(data) = tree.message_handler_data_fields() { let len = data.fields().len(); if data.path().is_empty() { - let _ = file.write_all(format!("{}{}{}\n", prefix, "└── ", data.name()).as_bytes()); + file.write_all(format!("{}{}{}\n", prefix, "└── ", data.name()).as_bytes())?; } else { - let _ = file.write_all(format!("{}{}{} `{}#L{}`\n", prefix, "└── ", data.name(), data.path(), data.line_number()).as_bytes()); + file.write_all(format!("{}{}{} `{}#L{}`\n", prefix, "└── ", data.name(), data.path(), data.line_number()).as_bytes())?; } for (i, field) in data.fields().iter().enumerate() { let is_last_field = i == len - 1; let branch = if is_last_field { "└── " } else { "ā”œā”€ā”€ " }; let field = &field.0; - let _ = file.write_all(format!("{prefix} {branch}{field}\n").as_bytes()); + file.write_all(format!("{prefix} {branch}{field}\n").as_bytes())?; } } + + Ok(()) } // =========== @@ -191,7 +196,6 @@ fn write_tree_html_node(tree: &DebugMessageTree, out: &mut String) { } } - const FRONTEND_MESSAGE_STR: &str = "FrontendMessage"; if let Some(data) = tree.message_handler_fields() && (!data.name().is_empty() || tree.name() == FRONTEND_MESSAGE_STR) && tree.name() != FRONTEND_MESSAGE_STR From 179937e9dbe059861a623f240d437774a799f6d0 Mon Sep 17 00:00:00 2001 From: Keavon Chambers Date: Tue, 17 Mar 2026 04:57:04 -0700 Subject: [PATCH 4/8] Remove cache system --- .github/workflows/build.yml | 38 +++++++---------------------------- .github/workflows/website.yml | 14 +------------ 2 files changed, 8 insertions(+), 44 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 254cac2a88..2c7dbaa7e7 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -172,40 +172,16 @@ jobs: name: graphite-web-bundle path: frontend/dist - - name: šŸ“ƒ Generate code documentation info for website + - name: šŸ“ƒ Trigger website rebuild if auto-generated code docs are stale if: github.event_name == 'push' - run: | - cargo run -p crate-hierarchy-viz -- website/generated-new - cargo run -p editor-message-tree -- website/generated-new - - - name: šŸ’æ Obtain cache of auto-generated code docs artifacts, to check if they've changed - if: github.event_name == 'push' - id: cache-website-code-docs - uses: actions/cache/restore@v5 - with: - path: website/generated - key: website-code-docs - - - name: šŸ” Check if auto-generated code docs artifacts changed - if: github.event_name == 'push' - id: website-code-docs-changed - run: | - diff --brief --recursive website/generated-new website/generated || echo "changed=true" >> $GITHUB_OUTPUT - rm -rf website/generated - mv website/generated-new website/generated - - - name: šŸ’¾ Save cache of auto-generated code docs artifacts - if: github.event_name == 'push' && steps.website-code-docs-changed.outputs.changed == 'true' - uses: actions/cache/save@v5 - with: - path: website/generated - key: ${{ steps.cache-website-code-docs.outputs.cache-primary-key }} - - - name: ā™»ļø Trigger website rebuild if the auto-generated code docs artifacts have changed - if: github.event_name == 'push' && steps.website-code-docs-changed.outputs.changed == 'true' env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} - run: gh workflow run website.yml --ref master + run: | + cargo run -p editor-message-tree -- website/generated + TREE=volunteer/guide/codebase-overview/hierarchical-message-system-tree + curl -sf "https://graphite.art/$TREE.txt" -o "website/static/$TREE.live.txt" \ + && diff -q "website/static/$TREE.txt" "website/static/$TREE.live.txt" > /dev/null \ + || gh workflow run website.yml --ref master windows: if: github.event_name == 'push' || inputs.windows diff --git a/.github/workflows/website.yml b/.github/workflows/website.yml index 65107cde2c..b9c2af737b 100644 --- a/.github/workflows/website.yml +++ b/.github/workflows/website.yml @@ -49,21 +49,9 @@ jobs: # Remove the INDEX_HTML_HEAD_INCLUSION environment variable for build links (not master deploys) git rev-parse --abbrev-ref HEAD | grep master > /dev/null || export INDEX_HTML_HEAD_INCLUSION="" - - name: šŸ’æ Obtain cache of auto-generated code docs artifacts - id: cache-website-code-docs - uses: actions/cache/restore@v5 - with: - path: website/generated - key: website-code-docs - - - name: šŸ“ Fallback in case auto-generated code docs artifacts weren't cached - if: steps.cache-website-code-docs.outputs.cache-hit != 'true' + - name: šŸ¦€ Produce auto-generated code docs data run: | - echo "šŸ¦€ Initial system version of Rust:" - rustc --version rustup update stable - echo "šŸ¦€ Latest updated version of Rust:" - rustc --version cargo run -p crate-hierarchy-viz -- website/generated cargo run -p editor-message-tree -- website/generated From fb41b4454ec2c0bd32c97c37def1892d32964492 Mon Sep 17 00:00:00 2001 From: Keavon Chambers Date: Tue, 17 Mar 2026 13:20:10 -0700 Subject: [PATCH 5/8] Fix Windows comment URL error --- .github/workflows/build.yml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 2c7dbaa7e7..181d6d45aa 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -145,7 +145,7 @@ jobs: gh api \ -X POST \ -H "Accept: application/vnd.github+json" \ - /repos/${{ github.repository }}/commits/$(git rev-parse HEAD)/comments \ + repos/${{ github.repository }}/commits/$(git rev-parse HEAD)/comments \ -f body="$COMMENT_BODY" else # Comment on the PR (use provided PR number from !build, or look it up by branch name) @@ -277,7 +277,7 @@ jobs: env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} run: | - ARTIFACT_URL=$(gh api /repos/${{ github.repository }}/actions/runs/${{ github.run_id }}/artifacts --jq '.artifacts[] | select(.name == "graphite-windows-bundle") | .archive_download_url') + ARTIFACT_URL=$(gh api repos/${{ github.repository }}/actions/runs/${{ github.run_id }}/artifacts --jq '.artifacts[] | select(.name == "graphite-windows-bundle") | .archive_download_url') PR_NUMBER="${{ inputs.pr_number }}" if [ -z "$PR_NUMBER" ]; then BRANCH=$(git rev-parse --abbrev-ref HEAD) @@ -463,7 +463,7 @@ jobs: env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} run: | - ARTIFACT_URL=$(gh api /repos/${{ github.repository }}/actions/runs/${{ github.run_id }}/artifacts --jq '.artifacts[] | select(.name == "graphite-mac-bundle") | .archive_download_url') + ARTIFACT_URL=$(gh api repos/${{ github.repository }}/actions/runs/${{ github.run_id }}/artifacts --jq '.artifacts[] | select(.name == "graphite-mac-bundle") | .archive_download_url') PR_NUMBER="${{ inputs.pr_number }}" if [ -z "$PR_NUMBER" ]; then BRANCH=$(git rev-parse --abbrev-ref HEAD) @@ -595,7 +595,7 @@ jobs: env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} run: | - ARTIFACT_URL=$(gh api /repos/${{ github.repository }}/actions/runs/${{ github.run_id }}/artifacts --jq '.artifacts[] | select(.name == "graphite-linux-bundle") | .archive_download_url') + ARTIFACT_URL=$(gh api repos/${{ github.repository }}/actions/runs/${{ github.run_id }}/artifacts --jq '.artifacts[] | select(.name == "graphite-linux-bundle") | .archive_download_url') PR_NUMBER="${{ inputs.pr_number }}" if [ -z "$PR_NUMBER" ]; then BRANCH=$(git rev-parse --abbrev-ref HEAD) From 076c59b98f752010b686fc18ed7c6045df836f40 Mon Sep 17 00:00:00 2001 From: Keavon Chambers Date: Tue, 17 Mar 2026 13:20:36 -0700 Subject: [PATCH 6/8] Fix incorrect artifact download URLs --- .github/workflows/build.yml | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 181d6d45aa..71d0f55693 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -277,13 +277,14 @@ jobs: env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} run: | - ARTIFACT_URL=$(gh api repos/${{ github.repository }}/actions/runs/${{ github.run_id }}/artifacts --jq '.artifacts[] | select(.name == "graphite-windows-bundle") | .archive_download_url') + ARTIFACT_ID=$(gh api repos/${{ github.repository }}/actions/runs/${{ github.run_id }}/artifacts --jq '.artifacts[] | select(.name == "graphite-windows-bundle") | .id') + ARTIFACT_URL="https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}/artifacts/$ARTIFACT_ID" PR_NUMBER="${{ inputs.pr_number }}" if [ -z "$PR_NUMBER" ]; then BRANCH=$(git rev-parse --abbrev-ref HEAD) PR_NUMBER=$(gh pr list --repo ${{ github.repository }} --head "$BRANCH" --json number --jq '.[0].number' 2>/dev/null || true) fi - if [ -n "$PR_NUMBER" ] && [ -n "$ARTIFACT_URL" ]; then + if [ -n "$PR_NUMBER" ] && [ -n "$ARTIFACT_ID" ]; then gh pr comment "$PR_NUMBER" --repo ${{ github.repository }} --body "| šŸ“¦ **Windows Build Complete for** $(git rev-parse HEAD) | |-| | [Download artifact]($ARTIFACT_URL) |" @@ -463,13 +464,14 @@ jobs: env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} run: | - ARTIFACT_URL=$(gh api repos/${{ github.repository }}/actions/runs/${{ github.run_id }}/artifacts --jq '.artifacts[] | select(.name == "graphite-mac-bundle") | .archive_download_url') + ARTIFACT_ID=$(gh api repos/${{ github.repository }}/actions/runs/${{ github.run_id }}/artifacts --jq '.artifacts[] | select(.name == "graphite-mac-bundle") | .id') + ARTIFACT_URL="https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}/artifacts/$ARTIFACT_ID" PR_NUMBER="${{ inputs.pr_number }}" if [ -z "$PR_NUMBER" ]; then BRANCH=$(git rev-parse --abbrev-ref HEAD) PR_NUMBER=$(gh pr list --repo ${{ github.repository }} --head "$BRANCH" --json number --jq '.[0].number' 2>/dev/null || true) fi - if [ -n "$PR_NUMBER" ] && [ -n "$ARTIFACT_URL" ]; then + if [ -n "$PR_NUMBER" ] && [ -n "$ARTIFACT_ID" ]; then gh pr comment "$PR_NUMBER" --repo ${{ github.repository }} --body "| šŸ“¦ **Mac Build Complete for** $(git rev-parse HEAD) | |-| | [Download artifact]($ARTIFACT_URL) |" @@ -595,13 +597,14 @@ jobs: env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} run: | - ARTIFACT_URL=$(gh api repos/${{ github.repository }}/actions/runs/${{ github.run_id }}/artifacts --jq '.artifacts[] | select(.name == "graphite-linux-bundle") | .archive_download_url') + ARTIFACT_ID=$(gh api repos/${{ github.repository }}/actions/runs/${{ github.run_id }}/artifacts --jq '.artifacts[] | select(.name == "graphite-linux-bundle") | .id') + ARTIFACT_URL="https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}/artifacts/$ARTIFACT_ID" PR_NUMBER="${{ inputs.pr_number }}" if [ -z "$PR_NUMBER" ]; then BRANCH=$(git rev-parse --abbrev-ref HEAD) PR_NUMBER=$(gh pr list --repo ${{ github.repository }} --head "$BRANCH" --json number --jq '.[0].number' 2>/dev/null || true) fi - if [ -n "$PR_NUMBER" ] && [ -n "$ARTIFACT_URL" ]; then + if [ -n "$PR_NUMBER" ] && [ -n "$ARTIFACT_ID" ]; then gh pr comment "$PR_NUMBER" --repo ${{ github.repository }} --body "| šŸ“¦ **Linux Build Complete for** $(git rev-parse HEAD) | |-| | [Download artifact]($ARTIFACT_URL) |" From 50a1f7fee04c4ae457424f073cb7bc6b35919071 Mon Sep 17 00:00:00 2001 From: Keavon Chambers Date: Tue, 17 Mar 2026 13:52:58 -0700 Subject: [PATCH 7/8] Add Flatpak to comment --- .github/workflows/build.yml | 38 ++++++++++++++++++++++++++++--------- 1 file changed, 29 insertions(+), 9 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 71d0f55693..0d95845173 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -285,9 +285,10 @@ jobs: PR_NUMBER=$(gh pr list --repo ${{ github.repository }} --head "$BRANCH" --json number --jq '.[0].number' 2>/dev/null || true) fi if [ -n "$PR_NUMBER" ] && [ -n "$ARTIFACT_ID" ]; then - gh pr comment "$PR_NUMBER" --repo ${{ github.repository }} --body "| šŸ“¦ **Windows Build Complete for** $(git rev-parse HEAD) | - |-| - | [Download artifact]($ARTIFACT_URL) |" + BODY="| šŸ“¦ **Windows Build Complete for** $(git rev-parse HEAD) |"$'\n' + BODY+="|-|"$'\n' + BODY+="| [Download binary]($ARTIFACT_URL) |" + gh pr comment "$PR_NUMBER" --repo ${{ github.repository }} --body "$BODY" fi - name: šŸ”‘ Azure login @@ -472,9 +473,10 @@ jobs: PR_NUMBER=$(gh pr list --repo ${{ github.repository }} --head "$BRANCH" --json number --jq '.[0].number' 2>/dev/null || true) fi if [ -n "$PR_NUMBER" ] && [ -n "$ARTIFACT_ID" ]; then - gh pr comment "$PR_NUMBER" --repo ${{ github.repository }} --body "| šŸ“¦ **Mac Build Complete for** $(git rev-parse HEAD) | - |-| - | [Download artifact]($ARTIFACT_URL) |" + BODY="| šŸ“¦ **Mac Build Complete for** $(git rev-parse HEAD) |"$'\n' + BODY+="|-|"$'\n' + BODY+="| [Download binary]($ARTIFACT_URL) |" + gh pr comment "$PR_NUMBER" --repo ${{ github.repository }} --body "$BODY" fi - name: šŸ” Sign and notarize (preparation) @@ -593,6 +595,7 @@ jobs: compression-level: 0 - name: šŸ’¬ Comment artifact link on PR + id: linux-comment if: github.event_name != 'push' env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} @@ -605,9 +608,11 @@ jobs: PR_NUMBER=$(gh pr list --repo ${{ github.repository }} --head "$BRANCH" --json number --jq '.[0].number' 2>/dev/null || true) fi if [ -n "$PR_NUMBER" ] && [ -n "$ARTIFACT_ID" ]; then - gh pr comment "$PR_NUMBER" --repo ${{ github.repository }} --body "| šŸ“¦ **Linux Build Complete for** $(git rev-parse HEAD) | - |-| - | [Download artifact]($ARTIFACT_URL) |" + BODY="| šŸ“¦ **Linux Build Complete for** $(git rev-parse HEAD) |"$'\n' + BODY+="|-|"$'\n' + BODY+="| [Download binary]($ARTIFACT_URL) |" + COMMENT_ID=$(gh api repos/${{ github.repository }}/issues/$PR_NUMBER/comments -f body="$BODY" --jq '.id') + echo "comment_id=$COMMENT_ID" >> "$GITHUB_OUTPUT" fi - name: šŸ”§ Install Flatpak tooling @@ -638,3 +643,18 @@ jobs: name: graphite-flatpak path: .flatpak/Graphite.flatpak compression-level: 0 + + - name: šŸ’¬ Update PR comment with Flatpak artifact link + if: github.event_name != 'push' && steps.linux-comment.outputs.comment_id + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + ARTIFACT_ID=$(gh api repos/${{ github.repository }}/actions/runs/${{ github.run_id }}/artifacts --jq '.artifacts[] | select(.name == "graphite-flatpak") | .id') + ARTIFACT_URL="https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}/artifacts/$ARTIFACT_ID" + COMMENT_ID="${{ steps.linux-comment.outputs.comment_id }}" + if [ -n "$ARTIFACT_ID" ]; then + EXISTING_BODY=$(gh api repos/${{ github.repository }}/issues/comments/$COMMENT_ID --jq '.body') + BODY="$EXISTING_BODY"$'\n' + BODY+="| [Download Flatpak]($ARTIFACT_URL) |" + gh api repos/${{ github.repository }}/issues/comments/$COMMENT_ID -X PATCH -f body="$BODY" + fi From 44bc1a073bdbe53b4e6bbf2e2d08c7a1790eee5f Mon Sep 17 00:00:00 2001 From: Keavon Chambers Date: Tue, 17 Mar 2026 14:17:39 -0700 Subject: [PATCH 8/8] Make Flatpak use debug/release mode choice instead of always release mode --- .github/workflows/build.yml | 2 +- .nix/default.nix | 3 ++- .nix/pkgs/graphite-flatpak-manifest.nix | 5 ++++- 3 files changed, 7 insertions(+), 3 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 0d95845173..58467c78ec 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -623,7 +623,7 @@ jobs: - name: šŸ— Build Flatpak run: | - nix build .#graphite-flatpak-manifest + nix build .#graphite${{ inputs.debug && '-dev' || '' }}-flatpak-manifest rm -rf .flatpak mkdir -p .flatpak diff --git a/.nix/default.nix b/.nix/default.nix index 20e26a962b..1fa50b0015 100644 --- a/.nix/default.nix +++ b/.nix/default.nix @@ -61,7 +61,8 @@ in graphite-branding = lib.call ./pkgs/graphite-branding.nix; graphite-bundle = (lib.call ./pkgs/graphite-bundle.nix) { }; graphite-dev-bundle = (lib.call ./pkgs/graphite-bundle.nix) { graphite = graphite-dev; }; - graphite-flatpak-manifest = lib.call ./pkgs/graphite-flatpak-manifest.nix; + graphite-flatpak-manifest = (lib.call ./pkgs/graphite-flatpak-manifest.nix) { }; + graphite-dev-flatpak-manifest = (lib.call ./pkgs/graphite-flatpak-manifest.nix) { graphite-bundle = graphite-dev-bundle; }; # TODO: graphene-cli = lib.call ./pkgs/graphene-cli.nix; diff --git a/.nix/pkgs/graphite-flatpak-manifest.nix b/.nix/pkgs/graphite-flatpak-manifest.nix index fee6603119..a01bde88c3 100644 --- a/.nix/pkgs/graphite-flatpak-manifest.nix +++ b/.nix/pkgs/graphite-flatpak-manifest.nix @@ -4,6 +4,9 @@ system, ... }: +{ + graphite-bundle ? self.packages.${system}.graphite-bundle, +}: (pkgs.formats.json { }).generate "art.graphite.Graphite.json" { app-id = "art.graphite.Graphite"; @@ -30,7 +33,7 @@ sources = [ { type = "archive"; - path = self.packages.${system}.graphite-bundle.tar; + path = graphite-bundle.tar; strip-components = 0; } ];