From 78d2e2ce4cd650e17c791e4cfafebfac87e2b4e7 Mon Sep 17 00:00:00 2001 From: Ziad Ashraf Date: Tue, 17 Mar 2026 12:36:21 +0200 Subject: [PATCH 1/3] Feat(Text Vertical Align): Added a new feature to enable the vertical text alignment --- .../src/messages/frontend/frontend_message.rs | 4 ++- .../document/graph_operation/utility_types.rs | 1 + .../node_graph/document_node_definitions.rs | 7 +++++ .../document/overlays/utility_functions.rs | 1 + .../document/overlays/utility_types_native.rs | 1 + .../messages/portfolio/document_migration.rs | 27 +++++++++++++++++-- .../graph_modification_utils.rs | 4 +++ .../messages/tool/tool_messages/text_tool.rs | 24 ++++++++++++++++- .../src/components/panels/Document.svelte | 25 +++++++++++++++++ node-graph/graph-craft/src/document/value.rs | 1 + node-graph/libraries/core-types/src/text.rs | 14 ++++++++++ node-graph/nodes/gstd/src/text.rs | 4 +++ node-graph/nodes/text/src/lib.rs | 14 ++++++++++ node-graph/nodes/text/src/path_builder.rs | 4 +-- node-graph/nodes/text/src/text_context.rs | 19 +++++++++++-- 15 files changed, 142 insertions(+), 8 deletions(-) diff --git a/editor/src/messages/frontend/frontend_message.rs b/editor/src/messages/frontend/frontend_message.rs index 743d6ed2c2..933cffd2ff 100644 --- a/editor/src/messages/frontend/frontend_message.rs +++ b/editor/src/messages/frontend/frontend_message.rs @@ -14,7 +14,7 @@ use crate::messages::tool::tool_messages::eyedropper_tool::PrimarySecondary; use graph_craft::document::NodeId; use graphene_std::raster::Image; use graphene_std::raster::color::Color; -use graphene_std::text::{Font, TextAlign}; +use graphene_std::text::{Font, TextAlign, VerticalAlign}; use std::path::PathBuf; #[cfg(not(target_family = "wasm"))] @@ -50,6 +50,8 @@ pub enum FrontendMessage { #[serde(rename = "maxHeight")] max_height: Option, align: TextAlign, + #[serde(rename = "verticalAlign")] + vertical_align: VerticalAlign, }, DisplayEditableTextboxUpdateFontData { #[serde(rename = "fontData")] diff --git a/editor/src/messages/portfolio/document/graph_operation/utility_types.rs b/editor/src/messages/portfolio/document/graph_operation/utility_types.rs index f74fb67c15..d84640c05c 100644 --- a/editor/src/messages/portfolio/document/graph_operation/utility_types.rs +++ b/editor/src/messages/portfolio/document/graph_operation/utility_types.rs @@ -205,6 +205,7 @@ impl<'a> ModifyInputsContext<'a> { Some(NodeInput::value(TaggedValue::F64(typesetting.max_height.unwrap_or(100.)), false)), Some(NodeInput::value(TaggedValue::F64(typesetting.tilt), false)), Some(NodeInput::value(TaggedValue::TextAlign(typesetting.align), false)), + Some(NodeInput::value(TaggedValue::VerticalAlign(typesetting.vertical_align), false)), ]); let transform = resolve_network_node_type("Transform").expect("Transform node does not exist").default_node_template(); let stroke = resolve_proto_node_type(graphene_std::vector_nodes::stroke::IDENTIFIER) diff --git a/editor/src/messages/portfolio/document/node_graph/document_node_definitions.rs b/editor/src/messages/portfolio/document/node_graph/document_node_definitions.rs index b7db96ec65..ada7be0c5a 100644 --- a/editor/src/messages/portfolio/document/node_graph/document_node_definitions.rs +++ b/editor/src/messages/portfolio/document/node_graph/document_node_definitions.rs @@ -2586,6 +2586,13 @@ fn static_input_properties() -> InputProperties { Ok(vec![choices]) }), ); + map.insert( + "text_vertical_align".to_string(), + Box::new(|node_id, index, context| { + let choices = enum_choice::().for_socket(ParameterWidgetsInfo::new(node_id, index, true, context)).property_row(); + Ok(vec![choices]) + }), + ); map } diff --git a/editor/src/messages/portfolio/document/overlays/utility_functions.rs b/editor/src/messages/portfolio/document/overlays/utility_functions.rs index dec2fda58e..d59a797f1c 100644 --- a/editor/src/messages/portfolio/document/overlays/utility_functions.rs +++ b/editor/src/messages/portfolio/document/overlays/utility_functions.rs @@ -239,6 +239,7 @@ pub fn text_width(text: &str, font_size: f64) -> f64 { max_height: None, tilt: 0.0, align: TextAlign::Left, + ..Default::default() }; // Load Source Sans Pro font data diff --git a/editor/src/messages/portfolio/document/overlays/utility_types_native.rs b/editor/src/messages/portfolio/document/overlays/utility_types_native.rs index 9991d39c57..f979f7e30f 100644 --- a/editor/src/messages/portfolio/document/overlays/utility_types_native.rs +++ b/editor/src/messages/portfolio/document/overlays/utility_types_native.rs @@ -1111,6 +1111,7 @@ impl OverlayContextInternal { max_height: None, tilt: 0., align: TextAlign::Left, // We'll handle alignment manually via pivot + ..Default::default() }; // Load Source Sans Pro font data diff --git a/editor/src/messages/portfolio/document_migration.rs b/editor/src/messages/portfolio/document_migration.rs index ac5ec052c0..cc1e158701 100644 --- a/editor/src/messages/portfolio/document_migration.rs +++ b/editor/src/messages/portfolio/document_migration.rs @@ -1281,11 +1281,34 @@ fn migrate_node(node_id: &NodeId, node: &DocumentNode, network_path: &[NodeId], network_path, ); - // Copy over old inputs + // Copy over old inputs (tilt, align) #[allow(clippy::needless_range_loop)] - for i in 10..=12 { + for i in 10..=11 { document.network_interface.set_input(&InputConnector::node(*node_id, i), old_inputs[i - 2].clone(), network_path); } + + // vertical_align at index 12 gets its default from the template + + // Copy over separate_glyph_elements from old index 10 to new index 13 + document.network_interface.set_input(&InputConnector::node(*node_id, 13), old_inputs[10].clone(), network_path); + } + + // Insert vertical_align parameter between align and separate_glyph_elements: + // https://github.com/GraphiteEditor/Graphite/issues/3883 + if reference == DefinitionIdentifier::ProtoNode(graphene_std::text::text::IDENTIFIER) && inputs_count == 13 { + let mut template: NodeTemplate = resolve_document_node_type(&reference)?.default_node_template(); + document.network_interface.replace_implementation(node_id, network_path, &mut template); + let old_inputs = document.network_interface.replace_inputs(node_id, network_path, &mut template)?; + + #[allow(clippy::needless_range_loop)] + for i in 0..=11 { + document.network_interface.set_input(&InputConnector::node(*node_id, i), old_inputs[i].clone(), network_path); + } + + // vertical_align at index 12 gets its default from the template + + // Shift separate_glyph_elements from old index 12 to new index 13 + document.network_interface.set_input(&InputConnector::node(*node_id, 13), old_inputs[12].clone(), network_path); } // Upgrade Sine, Cosine, and Tangent nodes to include a boolean input for whether the output should be in radians, which was previously the only option but is now not the default diff --git a/editor/src/messages/tool/common_functionality/graph_modification_utils.rs b/editor/src/messages/tool/common_functionality/graph_modification_utils.rs index c71f199078..e527480f48 100644 --- a/editor/src/messages/tool/common_functionality/graph_modification_utils.rs +++ b/editor/src/messages/tool/common_functionality/graph_modification_utils.rs @@ -421,6 +421,9 @@ pub fn get_text(layer: LayerNodeIdentifier, network_interface: &NodeNetworkInter let Some(&TaggedValue::TextAlign(align)) = inputs[graphene_std::text::text::AlignInput::INDEX].as_value() else { return None; }; + let Some(&TaggedValue::VerticalAlign(vertical_align)) = inputs[graphene_std::text::text::VerticalAlignInput::INDEX].as_value() else { + return None; + }; let Some(&TaggedValue::Bool(per_glyph_instances)) = inputs[graphene_std::text::text::SeparateGlyphElementsInput::INDEX].as_value() else { return None; }; @@ -433,6 +436,7 @@ pub fn get_text(layer: LayerNodeIdentifier, network_interface: &NodeNetworkInter character_spacing, tilt, align, + vertical_align, }; Some((text, font, typesetting, per_glyph_instances)) } diff --git a/editor/src/messages/tool/tool_messages/text_tool.rs b/editor/src/messages/tool/tool_messages/text_tool.rs index c5471f0bff..72228add0d 100644 --- a/editor/src/messages/tool/tool_messages/text_tool.rs +++ b/editor/src/messages/tool/tool_messages/text_tool.rs @@ -19,7 +19,7 @@ use crate::messages::tool::utility_types::ToolRefreshOptions; use graph_craft::document::value::TaggedValue; use graph_craft::document::{NodeId, NodeInput}; use graphene_std::renderer::Quad; -use graphene_std::text::{Font, FontCache, TextAlign, TypesettingConfig, lines_clipping}; +use graphene_std::text::{Font, FontCache, TextAlign, TypesettingConfig, VerticalAlign, lines_clipping}; use graphene_std::vector::style::Fill; use graphene_std::{Color, NodeInputDecleration}; @@ -38,6 +38,7 @@ pub struct TextOptions { fill: ToolColorOptions, tilt: f64, align: TextAlign, + vertical_align: VerticalAlign, } impl Default for TextOptions { @@ -50,6 +51,7 @@ impl Default for TextOptions { fill: ToolColorOptions::new_primary(), tilt: 0., align: TextAlign::default(), + vertical_align: VerticalAlign::default(), } } } @@ -85,6 +87,7 @@ pub enum TextOptionsUpdate { FontSize(f64), LineHeightRatio(f64), Align(TextAlign), + VerticalAlign(VerticalAlign), WorkingColors(Option, Option), } @@ -216,6 +219,20 @@ fn create_text_widgets(tool: &TextTool, font_catalog: &FontCatalog) -> Vec = [VerticalAlign::Top, VerticalAlign::Center, VerticalAlign::Bottom] + .into_iter() + .map(|va| { + RadioEntryData::new(format!("{va:?}")).label(va.to_string()).on_update(move |_| { + TextToolMessage::UpdateOptions { + options: TextOptionsUpdate::VerticalAlign(va), + } + .into() + }) + }) + .collect(); + let vertical_align = RadioInput::new(vertical_align_entries).selected_index(Some(tool.options.vertical_align as u32)).widget_instance(); + vec![ font, Separator::new(SeparatorStyle::Related).widget_instance(), @@ -226,6 +243,8 @@ fn create_text_widgets(tool: &TextTool, font_catalog: &FontCatalog) -> Vec MessageHandler> for Text TextOptionsUpdate::FontSize(font_size) => self.options.font_size = font_size, TextOptionsUpdate::LineHeightRatio(line_height_ratio) => self.options.line_height_ratio = line_height_ratio, TextOptionsUpdate::Align(align) => self.options.align = align, + TextOptionsUpdate::VerticalAlign(vertical_align) => self.options.vertical_align = vertical_align, TextOptionsUpdate::FillColor(color) => { self.options.fill.custom_color = color; self.options.fill.color_type = ToolColorType::Custom; @@ -420,6 +440,7 @@ impl TextToolData { max_width: editing_text.typesetting.max_width, max_height: editing_text.typesetting.max_height, align: editing_text.typesetting.align, + vertical_align: editing_text.typesetting.vertical_align, }); } else { // Check if DisplayRemoveEditableTextbox is already in the responses queue @@ -904,6 +925,7 @@ impl Fsm for TextToolFsmState { max_height: constraint_size.map(|size| size.y), tilt: tool_options.tilt, align: tool_options.align, + vertical_align: tool_options.vertical_align, }, font: Font::new(tool_options.font.font_family.clone(), tool_options.font.font_style.clone()), color: tool_options.fill.active_color(), diff --git a/frontend/src/components/panels/Document.svelte b/frontend/src/components/panels/Document.svelte index 1767c18e6c..5c47a366b4 100644 --- a/frontend/src/components/panels/Document.svelte +++ b/frontend/src/components/panels/Document.svelte @@ -366,6 +366,31 @@ textInput.style.color = data.color; textInput.style.textAlign = data.align; + // Apply vertical alignment using padding-top to offset text within the container + if (data.maxHeight !== undefined) { + if (data.verticalAlign === "Center" || data.verticalAlign === "Bottom") { + // Reset any existing padding first + textInput.style.paddingTop = "0px"; + + // Wait for layout to settle so we can measure the content height + await tick(); + + const contentHeight = textInput.scrollHeight; + const containerHeight = Math.floor(data.maxHeight / (data.lineHeightRatio * data.fontSize)) * (data.lineHeightRatio * data.fontSize); + const freeSpace = Math.max(containerHeight - contentHeight, 0); + + if (data.verticalAlign === "Center") { + textInput.style.paddingTop = `${freeSpace / 2}px`; + } else { + textInput.style.paddingTop = `${freeSpace}px`; + } + } else { + textInput.style.paddingTop = "0px"; + } + } else { + textInput.style.paddingTop = "0px"; + } + textInput.oninput = () => { if (!textInput) return; editor.handle.updateBounds(textInputCleanup(textInput.innerText)); diff --git a/node-graph/graph-craft/src/document/value.rs b/node-graph/graph-craft/src/document/value.rs index 91b6831809..8cebc5b4c7 100644 --- a/node-graph/graph-craft/src/document/value.rs +++ b/node-graph/graph-craft/src/document/value.rs @@ -269,6 +269,7 @@ tagged_value! { CentroidType(vector::misc::CentroidType), BooleanOperation(vector::misc::BooleanOperation), TextAlign(text_nodes::TextAlign), + VerticalAlign(text_nodes::VerticalAlign), } impl TaggedValue { diff --git a/node-graph/libraries/core-types/src/text.rs b/node-graph/libraries/core-types/src/text.rs index 29917694b6..960481d3b8 100644 --- a/node-graph/libraries/core-types/src/text.rs +++ b/node-graph/libraries/core-types/src/text.rs @@ -34,6 +34,18 @@ impl From for parley::Alignment { } } +/// Vertical alignment of text within its bounding box. +#[repr(C)] +#[cfg_attr(feature = "wasm", derive(tsify::Tsify))] +#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, serde::Serialize, serde::Deserialize, Hash, DynAny, node_macro::ChoiceType)] +#[widget(Radio)] +pub enum VerticalAlign { + #[default] + Top, + Center, + Bottom, +} + #[derive(PartialEq, Clone, Copy, Debug, serde::Serialize, serde::Deserialize)] pub struct TypesettingConfig { pub font_size: f64, @@ -43,6 +55,7 @@ pub struct TypesettingConfig { pub max_height: Option, pub tilt: f64, pub align: TextAlign, + pub vertical_align: VerticalAlign, } impl Default for TypesettingConfig { @@ -55,6 +68,7 @@ impl Default for TypesettingConfig { max_height: None, tilt: 0., align: TextAlign::default(), + vertical_align: VerticalAlign::default(), } } } diff --git a/node-graph/nodes/gstd/src/text.rs b/node-graph/nodes/gstd/src/text.rs index 4cb280ba30..1b6f059972 100644 --- a/node-graph/nodes/gstd/src/text.rs +++ b/node-graph/nodes/gstd/src/text.rs @@ -59,6 +59,9 @@ fn text<'i: 'n>( /// To have an effect on a single line of text, *Max Width* must be set. #[widget(ParsedWidgetOverride::Custom = "text_align")] align: TextAlign, + /// The vertical alignment of text within the text box. Requires *Max Height* to be set. + #[widget(ParsedWidgetOverride::Custom = "text_vertical_align")] + vertical_align: VerticalAlign, /// Whether to split every letterform into its own vector path element. Otherwise, a single compound path is produced. separate_glyph_elements: bool, ) -> Table { @@ -70,6 +73,7 @@ fn text<'i: 'n>( max_height: has_max_height.then_some(max_height), tilt, align, + vertical_align, }; to_path(&text, &font, &editor_resources.font_cache, typesetting, separate_glyph_elements) diff --git a/node-graph/nodes/text/src/lib.rs b/node-graph/nodes/text/src/lib.rs index ca4738ffa1..2efc0ef3cf 100644 --- a/node-graph/nodes/text/src/lib.rs +++ b/node-graph/nodes/text/src/lib.rs @@ -38,6 +38,18 @@ impl From for parley::Alignment { } } +/// Vertical alignment of text within its bounding box. +#[repr(C)] +#[cfg_attr(feature = "wasm", derive(tsify::Tsify))] +#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, serde::Serialize, serde::Deserialize, Hash, DynAny, node_macro::ChoiceType)] +#[widget(Radio)] +pub enum VerticalAlign { + #[default] + Top, + Center, + Bottom, +} + #[derive(PartialEq, Clone, Copy, Debug, serde::Serialize, serde::Deserialize)] pub struct TypesettingConfig { pub font_size: f64, @@ -47,6 +59,7 @@ pub struct TypesettingConfig { pub max_height: Option, pub tilt: f64, pub align: TextAlign, + pub vertical_align: VerticalAlign, } impl Default for TypesettingConfig { @@ -59,6 +72,7 @@ impl Default for TypesettingConfig { max_height: None, tilt: 0., align: TextAlign::default(), + vertical_align: VerticalAlign::default(), } } } diff --git a/node-graph/nodes/text/src/path_builder.rs b/node-graph/nodes/text/src/path_builder.rs index c5ba250409..a5eb5cd2ea 100644 --- a/node-graph/nodes/text/src/path_builder.rs +++ b/node-graph/nodes/text/src/path_builder.rs @@ -64,7 +64,7 @@ impl PathBuilder { } } - pub fn render_glyph_run(&mut self, glyph_run: &GlyphRun<'_, ()>, tilt: f64, per_glyph_instances: bool) { + pub fn render_glyph_run(&mut self, glyph_run: &GlyphRun<'_, ()>, tilt: f64, per_glyph_instances: bool, vertical_offset: f64) { let mut run_x = glyph_run.offset(); let run_y = glyph_run.baseline(); @@ -105,7 +105,7 @@ impl PathBuilder { let outlines = font_ref.outline_glyphs(); for glyph in glyph_run.glyphs() { - let glyph_offset = DVec2::new((run_x + glyph.x) as f64, (run_y - glyph.y) as f64); + let glyph_offset = DVec2::new((run_x + glyph.x) as f64, (run_y - glyph.y) as f64 + vertical_offset); run_x += glyph.advance; let glyph_id = GlyphId::from(glyph.id); diff --git a/node-graph/nodes/text/src/text_context.rs b/node-graph/nodes/text/src/text_context.rs index 7934feb885..90b7cf506f 100644 --- a/node-graph/nodes/text/src/text_context.rs +++ b/node-graph/nodes/text/src/text_context.rs @@ -1,4 +1,4 @@ -use super::{Font, FontCache, TypesettingConfig}; +use super::{Font, FontCache, TypesettingConfig, VerticalAlign}; use core::cell::RefCell; use core_types::table::Table; use glam::DVec2; @@ -94,12 +94,27 @@ impl TextContext { let mut path_builder = PathBuilder::new(per_glyph_instances, layout.scale() as f64); + let vertical_offset = match typesetting.vertical_align { + VerticalAlign::Top => 0., + VerticalAlign::Center | VerticalAlign::Bottom => { + let layout_height = layout.height() as f64; + let container_height = typesetting.max_height.unwrap_or(layout_height); + let free_space = (container_height - layout_height).max(0.); + + match typesetting.vertical_align { + VerticalAlign::Center => free_space / 2., + VerticalAlign::Bottom => free_space, + _ => unreachable!(), + } + } + }; + for line in layout.lines() { for item in line.items() { if let PositionedLayoutItem::GlyphRun(glyph_run) = item && typesetting.max_height.filter(|&max_height| glyph_run.baseline() > max_height as f32).is_none() { - path_builder.render_glyph_run(&glyph_run, typesetting.tilt, per_glyph_instances); + path_builder.render_glyph_run(&glyph_run, typesetting.tilt, per_glyph_instances, vertical_offset); } } } From adcf4b40f8463ea69c06bf8dbe31df7f4345d531 Mon Sep 17 00:00:00 2001 From: Ziad Ashraf Date: Tue, 17 Mar 2026 14:22:57 +0200 Subject: [PATCH 2/3] Fix: add serde(default) for vertical_align, simplify offset calculation, and match frontend container height to backend --- .../src/components/panels/Document.svelte | 2 +- node-graph/libraries/core-types/src/text.rs | 1 + node-graph/nodes/text/src/lib.rs | 1 + node-graph/nodes/text/src/text_context.rs | 22 +++++++++---------- 4 files changed, 13 insertions(+), 13 deletions(-) diff --git a/frontend/src/components/panels/Document.svelte b/frontend/src/components/panels/Document.svelte index 5c47a366b4..2909fa5440 100644 --- a/frontend/src/components/panels/Document.svelte +++ b/frontend/src/components/panels/Document.svelte @@ -376,7 +376,7 @@ await tick(); const contentHeight = textInput.scrollHeight; - const containerHeight = Math.floor(data.maxHeight / (data.lineHeightRatio * data.fontSize)) * (data.lineHeightRatio * data.fontSize); + const containerHeight = data.maxHeight; const freeSpace = Math.max(containerHeight - contentHeight, 0); if (data.verticalAlign === "Center") { diff --git a/node-graph/libraries/core-types/src/text.rs b/node-graph/libraries/core-types/src/text.rs index 960481d3b8..f5de96c30d 100644 --- a/node-graph/libraries/core-types/src/text.rs +++ b/node-graph/libraries/core-types/src/text.rs @@ -55,6 +55,7 @@ pub struct TypesettingConfig { pub max_height: Option, pub tilt: f64, pub align: TextAlign, + #[serde(default)] pub vertical_align: VerticalAlign, } diff --git a/node-graph/nodes/text/src/lib.rs b/node-graph/nodes/text/src/lib.rs index 2efc0ef3cf..68853d72d5 100644 --- a/node-graph/nodes/text/src/lib.rs +++ b/node-graph/nodes/text/src/lib.rs @@ -59,6 +59,7 @@ pub struct TypesettingConfig { pub max_height: Option, pub tilt: f64, pub align: TextAlign, + #[serde(default)] pub vertical_align: VerticalAlign, } diff --git a/node-graph/nodes/text/src/text_context.rs b/node-graph/nodes/text/src/text_context.rs index 90b7cf506f..5118c2da90 100644 --- a/node-graph/nodes/text/src/text_context.rs +++ b/node-graph/nodes/text/src/text_context.rs @@ -94,19 +94,17 @@ impl TextContext { let mut path_builder = PathBuilder::new(per_glyph_instances, layout.scale() as f64); - let vertical_offset = match typesetting.vertical_align { - VerticalAlign::Top => 0., - VerticalAlign::Center | VerticalAlign::Bottom => { - let layout_height = layout.height() as f64; - let container_height = typesetting.max_height.unwrap_or(layout_height); - let free_space = (container_height - layout_height).max(0.); - - match typesetting.vertical_align { - VerticalAlign::Center => free_space / 2., - VerticalAlign::Bottom => free_space, - _ => unreachable!(), - } + let vertical_offset = if let Some(container_height) = typesetting.max_height { + let layout_height = layout.height() as f64; + let free_space = (container_height - layout_height).max(0.); + + match typesetting.vertical_align { + VerticalAlign::Top => 0., + VerticalAlign::Center => free_space / 2., + VerticalAlign::Bottom => free_space, } + } else { + 0. }; for line in layout.lines() { From 9b97ab77579c6f7722a403e5b1e46c746817e20f Mon Sep 17 00:00:00 2001 From: Keavon Chambers Date: Wed, 18 Mar 2026 23:08:30 -0700 Subject: [PATCH 3/3] Consolidate CI web builds into the build workflow and restore deployments to GitHub environments Restore the CI "View deployment" button in PRs after building Consolidate release.yml functionality into build.yml Move build from ci.yml to a delegated run in build.yml --- .github/workflows/build.yml | 109 +++++++++++++++++++++++++++------- .github/workflows/ci.yml | 77 ++---------------------- .github/workflows/release.yml | 72 ---------------------- 3 files changed, 92 insertions(+), 166 deletions(-) delete mode 100644 .github/workflows/release.yml diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 58467c78ec..df3f4e8b53 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -4,6 +4,8 @@ on: push: branches: - master + tags: + - latest-stable workflow_dispatch: inputs: web: @@ -59,8 +61,6 @@ jobs: RUSTC_WRAPPER: /usr/bin/sccache CARGO_INCREMENTAL: 0 SCCACHE_DIR: /var/lib/github-actions/.cache - INDEX_HTML_HEAD_REPLACEMENT: - steps: - name: 📥 Clone repository uses: actions/checkout@v6 @@ -90,9 +90,22 @@ jobs: rustflags: "" target: wasm32-unknown-unknown + - name: 🔀 Choose production deployment environment + id: production-env + if: github.event_name == 'push' + run: | + if [[ "${{ github.ref }}" == "refs/tags/latest-stable" ]]; then + echo "cf_project=graphite-editor" >> $GITHUB_OUTPUT + DOMAIN="editor.graphite.art" + else + echo "cf_project=graphite-dev" >> $GITHUB_OUTPUT + DOMAIN="dev.graphite.art" + fi + echo "head_replacement=" >> $GITHUB_OUTPUT + - name: ✂ Replace template in of index.html if: github.event_name == 'push' - run: sed -i "s||$INDEX_HTML_HEAD_REPLACEMENT|" frontend/index.html + run: sed -i "s||${{ steps.production-env.outputs.head_replacement }}|" frontend/index.html - name: 🌐 Build Graphite web code env: @@ -105,27 +118,68 @@ jobs: CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }} CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }} run: | - MAX_ATTEMPTS=5 - DELAY=30 + if [ -z "$CLOUDFLARE_API_TOKEN" ]; then + echo "No Cloudflare API token available (fork PR), skipping deploy." + exit 0 + fi + MAX_ATTEMPTS=8 + DELAY=15 for ATTEMPT in $(seq 1 $MAX_ATTEMPTS); do echo "Attempt $ATTEMPT of $MAX_ATTEMPTS..." - if OUTPUT=$(npx wrangler@3 pages deploy "frontend/dist" --project-name="graphite-dev" --commit-dirty=true 2>&1); then - URL=$(echo "$OUTPUT" | grep -oP 'https://[^\s]+\.pages\.dev' | head -1) + npx wrangler@3 pages deploy "frontend/dist" --project-name="${{ steps.production-env.outputs.cf_project || 'graphite-dev' }}" --commit-dirty=true 2>&1 | tee /tmp/wrangler_output + if [ ${PIPESTATUS[0]} -eq 0 ]; then + URL=$(grep -oP 'https://[^\s]+\.pages\.dev' /tmp/wrangler_output | head -1) echo "url=$URL" >> "$GITHUB_OUTPUT" echo "Published successfully: $URL" exit 0 fi - echo "Attempt $ATTEMPT failed:" - echo "$OUTPUT" + echo "Attempt $ATTEMPT failed." if [ "$ATTEMPT" -lt "$MAX_ATTEMPTS" ]; then echo "Retrying in ${DELAY}s..." sleep $DELAY - DELAY=$((DELAY * 3)) + DELAY=$((DELAY * 2)) fi done echo "All $MAX_ATTEMPTS Cloudflare Pages publish attempts failed." exit 1 + - name: 🚀 Create GitHub Deployment for "View deployment" button + if: inputs.checkout_repo == '' || inputs.checkout_repo == github.repository + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + CF_URL: ${{ steps.cloudflare.outputs.url }} + run: | + if [ -z "$CF_URL" ]; then + echo "No Cloudflare URL available, skipping deployment." + exit 0 + fi + if [ "${{ github.ref }}" = "refs/tags/latest-stable" ]; then + REF="latest-stable" + ENVIRONMENT="graphite-editor (Production)" + elif [ "${{ github.event_name }}" = "push" ]; then + REF="master" + ENVIRONMENT="graphite-dev (Production)" + else + REF="${{ inputs.checkout_ref || github.head_ref || github.ref_name }}" + ENVIRONMENT="graphite-dev (Preview)" + fi + DEPLOY_ID=$(gh api \ + -X POST \ + -H "Accept: application/vnd.github+json" \ + repos/${{ github.repository }}/deployments \ + --input - \ + --jq '.id' < - - steps: - - name: 📥 Clone repository - uses: actions/checkout@v6 - - - name: 🗑 Clear wasm-bindgen cache - run: rm -r ~/.cache/.wasm-pack - - - name: 🟢 Install Node.js - uses: actions/setup-node@v6 - with: - node-version-file: .nvmrc - - - name: 🚧 Install build dependencies - run: | - cd frontend - npm run setup - - - name: 🦀 Install Rust - uses: actions-rust-lang/setup-rust-toolchain@v1 - with: - toolchain: stable - override: true - cache: false - rustflags: "" - target: wasm32-unknown-unknown - - - name: ✂ Replace template in of index.html - run: sed -i "s||$INDEX_HTML_HEAD_REPLACEMENT|" frontend/index.html - - - name: 🌐 Build Graphite web code - env: - NODE_ENV: production - run: mold -run cargo run build web - - - name: 📤 Publish to Cloudflare Pages - env: - CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }} - CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }} - run: npx wrangler@3 pages deploy "frontend/dist" --project-name="graphite-editor" --branch="master" --commit-dirty=true - - - name: 📦 Upload assets to GitHub release - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - run: | - DATE=$(git log -1 --format=%cd --date=format:%Y-%m-%d) - cd frontend - sed -i "s|$INDEX_HTML_HEAD_REPLACEMENT||" dist/index.html - mv dist "graphite-$DATE" - zip -r "graphite-self-hosted-build.zip" "graphite-$DATE" - gh release upload latest-stable "graphite-self-hosted-build.zip" --clobber