@@ -30,6 +30,13 @@ func Save(ctx context.Context, database *sql.DB, result *ParseResult, rawContent
3030 return 0 , fmt .Errorf ("checking latest hash: %w" , err )
3131 }
3232 if latestHash == hash {
33+ // Content unchanged, so no new version — but still backfill triggers
34+ // if the table drifted from the definition (e.g. a workflow applied
35+ // before trigger registration existed). Otherwise upgrading and
36+ // re-applying would leave triggers inert until a byte-level change.
37+ if err := reconcileTriggersIfDrifted (ctx , database , teamID , name , result .Workflow .Triggers ); err != nil {
38+ return 0 , err
39+ }
3340 return 0 , nil
3441 }
3542
@@ -44,14 +51,32 @@ func Save(ctx context.Context, database *sql.DB, result *ParseResult, rawContent
4451 return 0 , fmt .Errorf ("marshaling workflow: %w" , err )
4552 }
4653
47- _ , err = database .ExecContext (ctx ,
54+ tx , err := database .BeginTx (ctx , nil )
55+ if err != nil {
56+ return 0 , fmt .Errorf ("starting transaction: %w" , err )
57+ }
58+ defer tx .Rollback () //nolint:errcheck
59+
60+ _ , err = tx .ExecContext (ctx ,
4861 `INSERT INTO workflow_definitions (name, version, content, content_hash, team_id) VALUES ($1, $2, $3, $4, $5)` ,
4962 name , newVersion , content , hash , teamID ,
5063 )
5164 if err != nil {
5265 return 0 , fmt .Errorf ("inserting workflow definition: %w" , err )
5366 }
5467
68+ // Register the workflow's declared triggers so the cron/webhook/email
69+ // schedulers actually fire it. Done in the same transaction as the
70+ // definition insert so version and triggers commit atomically. A new
71+ // version is always active, so its triggers are enabled.
72+ if err := syncTriggersTx (ctx , tx , teamID , name , newVersion , result .Workflow .Triggers , true ); err != nil {
73+ return 0 , err
74+ }
75+
76+ if err := tx .Commit (); err != nil {
77+ return 0 , fmt .Errorf ("committing workflow save: %w" , err )
78+ }
79+
5580 return newVersion , nil
5681}
5782
@@ -194,7 +219,13 @@ func GetVersion(ctx context.Context, database *sql.DB, name string, version int)
194219// speculatively during a GitOps prune pass).
195220func Disable (ctx context.Context , database * sql.DB , name string ) error {
196221 teamID := auth .TeamIDFromContext (ctx )
197- _ , err := database .ExecContext (ctx ,
222+ tx , err := database .BeginTx (ctx , nil )
223+ if err != nil {
224+ return fmt .Errorf ("disabling workflow %q: %w" , name , err )
225+ }
226+ defer tx .Rollback () //nolint:errcheck
227+
228+ if _ , err := tx .ExecContext (ctx ,
198229 `UPDATE workflow_definitions
199230 SET disabled_at = NOW()
200231 WHERE id = (
@@ -203,8 +234,23 @@ func Disable(ctx context.Context, database *sql.DB, name string) error {
203234 ORDER BY version DESC LIMIT 1
204235 )` ,
205236 name , teamID ,
206- )
207- if err != nil {
237+ ); err != nil {
238+ return fmt .Errorf ("disabling workflow %q: %w" , name , err )
239+ }
240+
241+ // Disable the workflow's triggers too — the schedulers key off
242+ // workflow_triggers.enabled, not the definition's disabled_at, so a
243+ // pruned workflow would otherwise keep firing. Guarded on enabled = true
244+ // so repeated GitOps passes don't churn updated_at.
245+ if _ , err := tx .ExecContext (ctx ,
246+ `UPDATE workflow_triggers SET enabled = false, updated_at = NOW()
247+ WHERE workflow_name = $1 AND team_id = $2 AND enabled = true` ,
248+ name , teamID ,
249+ ); err != nil {
250+ return fmt .Errorf ("disabling triggers for %q: %w" , name , err )
251+ }
252+
253+ if err := tx .Commit (); err != nil {
208254 return fmt .Errorf ("disabling workflow %q: %w" , name , err )
209255 }
210256 return nil
@@ -214,7 +260,13 @@ func Disable(ctx context.Context, database *sql.DB, name string) error {
214260// workflow. Safe to call when already enabled — becomes a no-op.
215261func Reenable (ctx context.Context , database * sql.DB , name string ) error {
216262 teamID := auth .TeamIDFromContext (ctx )
217- _ , err := database .ExecContext (ctx ,
263+ tx , err := database .BeginTx (ctx , nil )
264+ if err != nil {
265+ return fmt .Errorf ("reenabling workflow %q: %w" , name , err )
266+ }
267+ defer tx .Rollback () //nolint:errcheck
268+
269+ if _ , err := tx .ExecContext (ctx ,
218270 `UPDATE workflow_definitions
219271 SET disabled_at = NULL
220272 WHERE id = (
@@ -223,8 +275,24 @@ func Reenable(ctx context.Context, database *sql.DB, name string) error {
223275 ORDER BY version DESC LIMIT 1
224276 )` ,
225277 name , teamID ,
226- )
227- if err != nil {
278+ ); err != nil {
279+ return fmt .Errorf ("reenabling workflow %q: %w" , name , err )
280+ }
281+
282+ // Restore the workflow's triggers. This covers the GitOps case where a
283+ // workflow was pruned (triggers disabled) and later reappears with
284+ // byte-identical content: Save is a no-op so it won't re-register them,
285+ // but Reenable must. Guarded on enabled = false to stay a true no-op on
286+ // the common already-enabled path.
287+ if _ , err := tx .ExecContext (ctx ,
288+ `UPDATE workflow_triggers SET enabled = true, updated_at = NOW()
289+ WHERE workflow_name = $1 AND team_id = $2 AND enabled = false` ,
290+ name , teamID ,
291+ ); err != nil {
292+ return fmt .Errorf ("reenabling triggers for %q: %w" , name , err )
293+ }
294+
295+ if err := tx .Commit (); err != nil {
228296 return fmt .Errorf ("reenabling workflow %q: %w" , name , err )
229297 }
230298 return nil
0 commit comments