Commit 0842022

Eric Bower  ·  2026-06-13 09:57:41 -0400 EDT
parent 21a5703
feat: producer can set job_id

This allows users to set a deterministic job id like git commit sha
3 files changed,  +94, -7
+5, -4
......@@ -63,13 +63,14 @@ while read -r old_sha new_sha ref; do
6363
6464 workspace="${upload_host}:${tar_path}"
6565 artifact_dest="${upload_host}:${upload_dir}"
66+ job_id="$short_sha"
6667
6768 if [ "$event_type" = "git.tag" ]; then
68- event=$(printf '{"type":"%s","name":"%s","tag":"%s","commit":"%s","workspace":"%s","artifact_dest":"%s"}' \
69- "$event_type" "$repo" "$ref_name" "$new_sha" "$workspace" "$artifact_dest")
69+ event=$(printf '{"type":"%s","name":"%s","job_id":"%s","tag":"%s","commit":"%s","workspace":"%s","artifact_dest":"%s"}' \
70+ "$event_type" "$repo" "$job_id" "$ref_name" "$new_sha" "$workspace" "$artifact_dest")
7071 else
71- event=$(printf '{"type":"%s","name":"%s","branch":"%s","commit":"%s","workspace":"%s","artifact_dest":"%s"}' \
72- "$event_type" "$repo" "$ref_name" "$new_sha" "$workspace" "$artifact_dest")
72+ event=$(printf '{"type":"%s","name":"%s","job_id":"%s","branch":"%s","commit":"%s","workspace":"%s","artifact_dest":"%s"}' \
73+ "$event_type" "$repo" "$job_id" "$ref_name" "$new_sha" "$workspace" "$artifact_dest")
7374 fi
7475
7576 log "publish: $event"
+37, -3
......@@ -65,6 +65,7 @@ type Cfg struct {
6565 type Event struct {
6666 Type string `json:"type"`
6767 Name string `json:"name"`
68+ JobID string `json:"job_id,omitempty"` // optional producer-set ID
6869 Workspace string `json:"workspace"`
6970 Branch string `json:"branch"`
7071 Tag string `json:"tag"`
......@@ -580,11 +581,21 @@ func (eng *JobEngine) FindManifest() (string, error) {
580581 func eventHandler(cfg *Cfg, eventData *Event) error {
581582 log := cfg.Logger.With("repo", eventData.Name, "type", eventData.Type)
582583
584+ jobID := resolveJobID(eventData.Name, eventData.Workspace, eventData.JobID)
585+ log = log.With("job_id", jobID)
586+
583587 // Cancel any existing job for this repo before starting a new one
584588 cancelRunningJobs(cfg, log, eventData.Name)
585589
586- jobID := generateJobID(eventData.Name, eventData.Workspace)
587- log = log.With("job_id", jobID)
590+ // Clean up any artifacts from a previous run with the same job ID
591+ // (e.g. duplicate event, re-trigger of the same commit).
592+ eventDir := filepath.Join(cfg.ArtifactDir, eventData.Name, jobID)
593+ if _, err := os.Stat(eventDir); err == nil {
594+ if err := os.RemoveAll(eventDir); err != nil {
595+ log.Warn("failed to clean old job directory", "err", err, "dir", eventDir)
596+ }
597+ }
598+
588599 eventBytes, _ := json.Marshal(eventData)
589600 fmt.Fprintf(os.Stdout, "🚀 starting job ci.%s.%s\n", eventData.Name, jobID) //nolint:errcheck
590601 fmt.Fprintf(os.Stdout, " event: type=%s name=%s workspace=%s\n", eventData.Type, eventData.Name, eventData.Workspace) //nolint:errcheck
......@@ -611,7 +622,6 @@ func eventHandler(cfg *Cfg, eventData *Event) error {
611622 fmt.Fprintf(os.Stdout, "✅ workspace ready %s\n", eng.Wk.GetDir()) //nolint:errcheck
612623
613624 // Store the event in the artifact directory so the monitor can access it
614- eventDir := filepath.Join(cfg.ArtifactDir, eventData.Name, jobID)
615625 artifactsDir := filepath.Join(eventDir, "artifacts")
616626 if err := os.MkdirAll(artifactsDir, 0755); err != nil {
617627 log.Error("create artifacts dir", "err", err)
......@@ -1358,6 +1368,30 @@ func generateJobID(name, workspace string) string {
13581368 return jobIDFor(name, workspace, time.Now().UnixNano())
13591369 }
13601370
1371+// resolveJobID returns the producer-provided job ID if set and valid,
1372+// otherwise generates one from name + workspace + timestamp.
1373+func resolveJobID(name, workspace, providedID string) string {
1374+ if providedID != "" && validateJobID(providedID) {
1375+ return providedID
1376+ }
1377+ return generateJobID(name, workspace)
1378+}
1379+
1380+// validateJobID checks that the ID is safe to use in session names and paths.
1381+// Constraints: alphanumeric + hyphens only, no dots (breaks parseJobPrefix),
1382+// 1-16 chars to keep session names reasonable.
1383+func validateJobID(id string) bool {
1384+ if len(id) == 0 || len(id) > 16 {
1385+ return false
1386+ }
1387+ for _, r := range id {
1388+ if !((r >= 'a' && r <= 'z') || (r >= 'A' && r <= 'Z') || (r >= '0' && r <= '9') || r == '-') {
1389+ return false
1390+ }
1391+ }
1392+ return true
1393+}
1394+
13611395 func runtimeOS() string {
13621396 return runtime.GOOS
13631397 }
+52, -0
......@@ -416,6 +416,58 @@ func TestGenerateJobID(t *testing.T) {
416416 }
417417 }
418418
419+func TestValidateJobID(t *testing.T) {
420+ tests := []struct {
421+ id string
422+ want bool
423+ }{
424+ {"a3f2b8c1", true}, // 8-char hex (commit short SHA)
425+ {"abc-123", true}, // alphanumeric + hyphen
426+ {"PR42", true}, // uppercase + digits
427+ {"a", true}, // single char
428+ {"abcdefghij123456", true}, // 16 chars (max)
429+ {"", false}, // empty
430+ {"abcdefghij1234567", false}, // 17 chars (too long)
431+ {"abc.def", false}, // contains dot (breaks parseJobPrefix)
432+ {"abc/def", false}, // contains slash
433+ {"abc def", false}, // contains space
434+ {"abc_def", false}, // contains underscore
435+ }
436+
437+ for _, tt := range tests {
438+ got := validateJobID(tt.id)
439+ if got != tt.want {
440+ t.Errorf("validateJobID(%q) = %v, want %v", tt.id, got, tt.want)
441+ }
442+ }
443+}
444+
445+func TestResolveJobID(t *testing.T) {
446+ // Valid producer ID is used as-is
447+ id := resolveJobID("myrepo", "/workspace", "a3f2b8c1")
448+ if id != "a3f2b8c1" {
449+ t.Errorf("resolveJobID with valid ID = %q, want %q", id, "a3f2b8c1")
450+ }
451+
452+ // Empty producer ID falls back to generated
453+ id = resolveJobID("myrepo", "/workspace", "")
454+ if id == "" || len(id) != 8 {
455+ t.Errorf("resolveJobID with empty ID = %q, want 8-char generated ID", id)
456+ }
457+
458+ // Invalid producer ID (contains dot) falls back to generated
459+ id = resolveJobID("myrepo", "/workspace", "1.0.0")
460+ if id == "1.0.0" || len(id) != 8 {
461+ t.Errorf("resolveJobID with invalid ID = %q, want 8-char generated ID", id)
462+ }
463+
464+ // Invalid producer ID (too long) falls back to generated
465+ id = resolveJobID("myrepo", "/workspace", "this_is_way_too_long_for_a_job_id")
466+ if len(id) != 8 {
467+ t.Errorf("resolveJobID with too-long ID = %q, want 8-char generated ID", id)
468+ }
469+}
470+
419471 func TestAllCompleted(t *testing.T) {
420472 tests := []struct {
421473 name string