feat(wfe-core): human-friendly workflow names

Add a `name` field to both `WorkflowDefinition` (optional display name
declared in YAML, e.g. "Continuous Integration") and `WorkflowInstance`
(required, unique alongside the UUID primary key). Instance names are
auto-assigned as `{definition_id}-{N}` via a per-definition monotonic
counter so the 42nd run of `ci` becomes `ci-42`.

Persistence trait gains two methods:

* `get_workflow_instance_by_name` — name-based lookup for Get/Cancel/
  Suspend/Resume/Watch/Logs RPCs so callers can address instances
  interchangeably as either UUID or human name.
* `next_definition_sequence` — atomic per-definition counter used by
  the host at start time to allocate the next N.

This commit wires the in-memory test provider and touches the deno
bridge test helper; the real postgres/sqlite impls follow in the next
commit. UUIDs remain the primary key throughout — names are a second
unique index, never a replacement.
This commit is contained in:
2026-04-07 18:58:12 +01:00
parent 883471181d
commit d9b9c5651e
5 changed files with 92 additions and 28 deletions

View File

@@ -5,9 +5,7 @@ use async_trait::async_trait;
use chrono::{DateTime, Utc};
use tokio::sync::RwLock;
use crate::models::{
Event, EventSubscription, ExecutionError, ScheduledCommand, WorkflowInstance,
};
use crate::models::{Event, EventSubscription, ExecutionError, ScheduledCommand, WorkflowInstance};
use crate::traits::{
EventRepository, PersistenceProvider, ScheduledCommandRepository, SubscriptionRepository,
WorkflowRepository,
@@ -22,6 +20,9 @@ pub struct InMemoryPersistenceProvider {
subscriptions: Arc<RwLock<HashMap<String, EventSubscription>>>,
errors: Arc<RwLock<Vec<ExecutionError>>>,
scheduled_commands: Arc<RwLock<Vec<ScheduledCommand>>>,
/// Per-definition monotonic counter used to generate human-friendly
/// workflow instance names of the form `{definition_id}-{N}`.
sequences: Arc<RwLock<HashMap<String, u64>>>,
}
impl InMemoryPersistenceProvider {
@@ -32,6 +33,7 @@ impl InMemoryPersistenceProvider {
subscriptions: Arc::new(RwLock::new(HashMap::new())),
errors: Arc::new(RwLock::new(Vec::new())),
scheduled_commands: Arc::new(RwLock::new(Vec::new())),
sequences: Arc::new(RwLock::new(HashMap::new())),
}
}
@@ -107,6 +109,23 @@ impl WorkflowRepository for InMemoryPersistenceProvider {
.ok_or_else(|| WfeError::WorkflowNotFound(id.to_string()))
}
async fn get_workflow_instance_by_name(&self, name: &str) -> Result<WorkflowInstance> {
self.workflows
.read()
.await
.values()
.find(|w| w.name == name)
.cloned()
.ok_or_else(|| WfeError::WorkflowNotFound(name.to_string()))
}
async fn next_definition_sequence(&self, definition_id: &str) -> Result<u64> {
let mut seqs = self.sequences.write().await;
let next = seqs.get(definition_id).copied().unwrap_or(0) + 1;
seqs.insert(definition_id.to_string(), next);
Ok(next)
}
async fn get_workflow_instances(&self, ids: &[String]) -> Result<Vec<WorkflowInstance>> {
let workflows = self.workflows.read().await;
let mut result = Vec::new();
@@ -121,10 +140,7 @@ impl WorkflowRepository for InMemoryPersistenceProvider {
#[async_trait]
impl SubscriptionRepository for InMemoryPersistenceProvider {
async fn create_event_subscription(
&self,
subscription: &EventSubscription,
) -> Result<String> {
async fn create_event_subscription(&self, subscription: &EventSubscription) -> Result<String> {
let id = if subscription.id.is_empty() {
uuid::Uuid::new_v4().to_string()
} else {
@@ -217,11 +233,7 @@ impl SubscriptionRepository for InMemoryPersistenceProvider {
}
}
async fn clear_subscription_token(
&self,
subscription_id: &str,
token: &str,
) -> Result<()> {
async fn clear_subscription_token(&self, subscription_id: &str, token: &str) -> Result<()> {
let mut subs = self.subscriptions.write().await;
match subs.get_mut(subscription_id) {
Some(sub) => {
@@ -282,7 +294,9 @@ impl EventRepository for InMemoryPersistenceProvider {
let events = self.events.read().await;
let ids = events
.values()
.filter(|e| e.event_name == event_name && e.event_key == event_key && e.event_time <= as_of)
.filter(|e| {
e.event_name == event_name && e.event_key == event_key && e.event_time <= as_of
})
.map(|e| e.id.clone())
.collect();
Ok(ids)
@@ -325,9 +339,14 @@ impl ScheduledCommandRepository for InMemoryPersistenceProvider {
async fn process_commands(
&self,
as_of: DateTime<Utc>,
handler: &(dyn Fn(ScheduledCommand) -> std::pin::Pin<Box<dyn std::future::Future<Output = Result<()>> + Send>>
+ Send
+ Sync),
handler: &(
dyn Fn(
ScheduledCommand,
)
-> std::pin::Pin<Box<dyn std::future::Future<Output = Result<()>> + Send>>
+ Send
+ Sync
),
) -> Result<()> {
let as_of_millis = as_of.timestamp_millis();
let due: Vec<ScheduledCommand> = {
@@ -360,7 +379,7 @@ impl PersistenceProvider for InMemoryPersistenceProvider {
#[cfg(test)]
mod tests {
use super::*;
use crate::models::{Event, EventSubscription, ExecutionError, ScheduledCommand, CommandName};
use crate::models::{CommandName, Event, EventSubscription, ExecutionError, ScheduledCommand};
use crate::traits::{
EventRepository, PersistenceProvider, ScheduledCommandRepository, SubscriptionRepository,
WorkflowRepository,