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 | |
| 63 | 63 | ||
| 64 | 64 | workspace="${upload_host}:${tar_path}" | |
| 65 | 65 | artifact_dest="${upload_host}:${upload_dir}" | |
| 66 | + | job_id="$short_sha" | |
| 66 | 67 | ||
| 67 | 68 | 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") | |
| 70 | 71 | 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") | |
| 73 | 74 | fi | |
| 74 | 75 | ||
| 75 | 76 | log "publish: $event" |
M
main.go
+37,
-3
| ... | ... | @@ -65,6 +65,7 @@ type Cfg struct { | |
| 65 | 65 | type Event struct { | |
| 66 | 66 | Type string `json:"type"` | |
| 67 | 67 | Name string `json:"name"` | |
| 68 | + | JobID string `json:"job_id,omitempty"` // optional producer-set ID | |
| 68 | 69 | Workspace string `json:"workspace"` | |
| 69 | 70 | Branch string `json:"branch"` | |
| 70 | 71 | Tag string `json:"tag"` |
| ... | ... | @@ -580,11 +581,21 @@ func (eng *JobEngine) FindManifest() (string, error) { | |
| 580 | 581 | func eventHandler(cfg *Cfg, eventData *Event) error { | |
| 581 | 582 | log := cfg.Logger.With("repo", eventData.Name, "type", eventData.Type) | |
| 582 | 583 | ||
| 584 | + | jobID := resolveJobID(eventData.Name, eventData.Workspace, eventData.JobID) | |
| 585 | + | log = log.With("job_id", jobID) | |
| 586 | + | ||
| 583 | 587 | // Cancel any existing job for this repo before starting a new one | |
| 584 | 588 | cancelRunningJobs(cfg, log, eventData.Name) | |
| 585 | 589 | ||
| 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 | + | ||
| 588 | 599 | eventBytes, _ := json.Marshal(eventData) | |
| 589 | 600 | fmt.Fprintf(os.Stdout, "🚀 starting job ci.%s.%s\n", eventData.Name, jobID) //nolint:errcheck | |
| 590 | 601 | 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 { | |
| 611 | 622 | fmt.Fprintf(os.Stdout, "✅ workspace ready %s\n", eng.Wk.GetDir()) //nolint:errcheck | |
| 612 | 623 | ||
| 613 | 624 | // Store the event in the artifact directory so the monitor can access it | |
| 614 | - | eventDir := filepath.Join(cfg.ArtifactDir, eventData.Name, jobID) | |
| 615 | 625 | artifactsDir := filepath.Join(eventDir, "artifacts") | |
| 616 | 626 | if err := os.MkdirAll(artifactsDir, 0755); err != nil { | |
| 617 | 627 | log.Error("create artifacts dir", "err", err) |
| ... | ... | @@ -1358,6 +1368,30 @@ func generateJobID(name, workspace string) string { | |
| 1358 | 1368 | return jobIDFor(name, workspace, time.Now().UnixNano()) | |
| 1359 | 1369 | } | |
| 1360 | 1370 | ||
| 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 | + | ||
| 1361 | 1395 | func runtimeOS() string { | |
| 1362 | 1396 | return runtime.GOOS | |
| 1363 | 1397 | } |
+52,
-0
| ... | ... | @@ -416,6 +416,58 @@ func TestGenerateJobID(t *testing.T) { | |
| 416 | 416 | } | |
| 417 | 417 | } | |
| 418 | 418 | ||
| 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 | + | ||
| 419 | 471 | func TestAllCompleted(t *testing.T) { | |
| 420 | 472 | tests := []struct { | |
| 421 | 473 | name string |