Scope sync to campaigns changed since a given commit
CI / test (push) Successful in 8s

Add SyncChanged (and the ChangedSlugs git-diff helper it's built on) as
an opt-in narrower version of SyncDir: pass the push's "before" SHA and
only the campaign directories that actually changed get looked at at
all, instead of every campaign getting resolved and diffed against
listmonk on every push. An empty or unresolvable since (new branch's
first push, a shallow clone missing that commit, any git failure) always
falls back to a full scan -- this can only ever cause more work than
strictly necessary, never a silently-skipped campaign. SyncDir itself is
now a thin wrapper (SyncChanged with since=""), so every existing caller
and test is unaffected.

campaigns sync PATH now accepts an optional trailing SINCE_SHA.
This commit is contained in:
2026-08-13 13:40:10 -04:00
parent 34d61c2e69
commit c375916f05
3 changed files with 245 additions and 10 deletions
+13 -4
View File
@@ -29,10 +29,14 @@ func main() {
switch os.Args[1] {
case "sync":
if len(os.Args) < 3 {
fmt.Fprintln(os.Stderr, "usage: campaigns sync PATH")
fmt.Fprintln(os.Stderr, "usage: campaigns sync PATH [SINCE_SHA]")
os.Exit(1)
}
runSync(lm, os.Args[2])
since := ""
if len(os.Args) >= 4 {
since = os.Args[3]
}
runSync(lm, os.Args[2], since)
case "send":
if len(os.Args) < 3 {
fmt.Fprintln(os.Stderr, "usage: campaigns send SLUG")
@@ -65,7 +69,11 @@ func usage() {
// (comma-separated), used for any campaign that doesn't set its own
// preview_emails in frontmatter. Optional — sync still works with none set,
// it just won't auto-preview campaigns that don't specify their own.
func runSync(lm *listmonk.Client, path string) {
//
// since is optional — the commit to diff against (typically the push
// event's "before" SHA) to scope this run to just the campaigns that
// changed, per campaign.SyncChanged. Pass "" to always do a full scan.
func runSync(lm *listmonk.Client, path, since string) {
var defaultPreview []string
if raw := os.Getenv("CAMPAIGNS_PREVIEW_EMAIL"); raw != "" {
for _, addr := range strings.Split(raw, ",") {
@@ -75,10 +83,11 @@ func runSync(lm *listmonk.Client, path string) {
}
}
result, err := campaign.SyncDir(lm, path, defaultPreview)
result, err := campaign.SyncChanged(lm, path, defaultPreview, since)
if err != nil {
log.Fatalf("sync: %v", err)
}
log.Printf("mode: %s", result.Mode)
log.Printf("synced: %v", result.Synced)
log.Printf("unchanged: %v", result.Unchanged)
if len(result.Skipped) > 0 {