diff --git a/orchestrator/dispatch/main.go b/orchestrator/dispatch/main.go index 4e3b11e..9a5e006 100644 --- a/orchestrator/dispatch/main.go +++ b/orchestrator/dispatch/main.go @@ -18,6 +18,7 @@ import ( "fmt" "log" "net/http" + "regexp" "os" "strings" "time" @@ -233,6 +234,10 @@ func handler(ctx context.Context, request events.APIGatewayProxyRequest) (events push.Repository.FullName, branch, push.Pusher.Login, len(push.Commits)) changedFiles := collectChangedFiles(push.Commits) + var commitMessages []string + for _, c := range push.Commits { + commitMessages = append(commitMessages, c.Message) + } workflows, err := fetchWorkflows(c.GiteaURL, c.GiteaToken, push.Repository.FullName, branch) if err != nil || len(workflows) == 0 { @@ -247,7 +252,7 @@ func handler(ctx context.Context, request events.APIGatewayProxyRequest) (events continue } - if !shouldTrigger(wf, branch, changedFiles) { + if !shouldTrigger(wf, content, branch, changedFiles, commitMessages) { log.Printf("Skipping %s (no matching trigger)", name) continue } @@ -633,7 +638,7 @@ func fetchRawFile(baseURL, token, repoFullName, path, branch string) (string, er // --- Trigger evaluation --- -func shouldTrigger(wf Workflow, branch string, changedFiles []string) bool { +func shouldTrigger(wf Workflow, wfContent string, branch string, changedFiles []string, commitMessages []string) bool { onMap, ok := wf.On.(map[string]interface{}) if !ok { return true @@ -701,9 +706,43 @@ func shouldTrigger(wf Workflow, branch string, changedFiles []string) bool { } } + // Opt-in commit-marker gate (Ozan 2026-09-22): a `# tinqs-marker: ` comment in the + // workflow makes push triggers require a pushed commit whose MESSAGE matches the regex + // (case-insensitive). No match → no runner, nothing spawned. workflow_dispatch events + // never reach this function, so button/CLI publishes are unaffected. + if mr := markerRegex(wfContent); mr != nil { + matched := false + for _, msg := range commitMessages { + if mr.MatchString(msg) { + matched = true + break + } + } + if !matched { + log.Printf("Marker gate: no pushed commit matches %s — not spawning", mr) + return false + } + } + return true } +// markerRegex extracts the opt-in marker regex from a `# tinqs-marker: ` comment +// in the workflow YAML. Empty (no comment) = no marker gating — behaviour unchanged. +func markerRegex(content string) *regexp.Regexp { + re := regexp.MustCompile(`(?im)^\s*#\s*tinqs-marker:\s*(.+)$`) + m := re.FindStringSubmatch(content) + if len(m) < 2 { + return nil + } + cr, err := regexp.Compile(strings.TrimSpace(m[1])) + if err != nil { + log.Printf("Bad tinqs-marker regex %q: %v", m[1], err) + return nil + } + return cr +} + func matchPath(pattern, file string) bool { if strings.HasSuffix(pattern, "/**") { prefix := strings.TrimSuffix(pattern, "/**")