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
1@@ -63,13 +63,14 @@ while read -r old_sha new_sha ref; do
2
3 workspace="${upload_host}:${tar_path}"
4 artifact_dest="${upload_host}:${upload_dir}"
5+ job_id="$short_sha"
6
7 if [ "$event_type" = "git.tag" ]; then
8- event=$(printf '{"type":"%s","name":"%s","tag":"%s","commit":"%s","workspace":"%s","artifact_dest":"%s"}' \
9- "$event_type" "$repo" "$ref_name" "$new_sha" "$workspace" "$artifact_dest")
10+ event=$(printf '{"type":"%s","name":"%s","job_id":"%s","tag":"%s","commit":"%s","workspace":"%s","artifact_dest":"%s"}' \
11+ "$event_type" "$repo" "$job_id" "$ref_name" "$new_sha" "$workspace" "$artifact_dest")
12 else
13- event=$(printf '{"type":"%s","name":"%s","branch":"%s","commit":"%s","workspace":"%s","artifact_dest":"%s"}' \
14- "$event_type" "$repo" "$ref_name" "$new_sha" "$workspace" "$artifact_dest")
15+ event=$(printf '{"type":"%s","name":"%s","job_id":"%s","branch":"%s","commit":"%s","workspace":"%s","artifact_dest":"%s"}' \
16+ "$event_type" "$repo" "$job_id" "$ref_name" "$new_sha" "$workspace" "$artifact_dest")
17 fi
18
19 log "publish: $event"
M
main.go
+37,
-3
1@@ -65,6 +65,7 @@ type Cfg struct {
2 type Event struct {
3 Type string `json:"type"`
4 Name string `json:"name"`
5+ JobID string `json:"job_id,omitempty"` // optional producer-set ID
6 Workspace string `json:"workspace"`
7 Branch string `json:"branch"`
8 Tag string `json:"tag"`
9@@ -580,11 +581,21 @@ func (eng *JobEngine) FindManifest() (string, error) {
10 func eventHandler(cfg *Cfg, eventData *Event) error {
11 log := cfg.Logger.With("repo", eventData.Name, "type", eventData.Type)
12
13+ jobID := resolveJobID(eventData.Name, eventData.Workspace, eventData.JobID)
14+ log = log.With("job_id", jobID)
15+
16 // Cancel any existing job for this repo before starting a new one
17 cancelRunningJobs(cfg, log, eventData.Name)
18
19- jobID := generateJobID(eventData.Name, eventData.Workspace)
20- log = log.With("job_id", jobID)
21+ // Clean up any artifacts from a previous run with the same job ID
22+ // (e.g. duplicate event, re-trigger of the same commit).
23+ eventDir := filepath.Join(cfg.ArtifactDir, eventData.Name, jobID)
24+ if _, err := os.Stat(eventDir); err == nil {
25+ if err := os.RemoveAll(eventDir); err != nil {
26+ log.Warn("failed to clean old job directory", "err", err, "dir", eventDir)
27+ }
28+ }
29+
30 eventBytes, _ := json.Marshal(eventData)
31 fmt.Fprintf(os.Stdout, "🚀 starting job ci.%s.%s\n", eventData.Name, jobID) //nolint:errcheck
32 fmt.Fprintf(os.Stdout, " event: type=%s name=%s workspace=%s\n", eventData.Type, eventData.Name, eventData.Workspace) //nolint:errcheck
33@@ -611,7 +622,6 @@ func eventHandler(cfg *Cfg, eventData *Event) error {
34 fmt.Fprintf(os.Stdout, "✅ workspace ready %s\n", eng.Wk.GetDir()) //nolint:errcheck
35
36 // Store the event in the artifact directory so the monitor can access it
37- eventDir := filepath.Join(cfg.ArtifactDir, eventData.Name, jobID)
38 artifactsDir := filepath.Join(eventDir, "artifacts")
39 if err := os.MkdirAll(artifactsDir, 0755); err != nil {
40 log.Error("create artifacts dir", "err", err)
41@@ -1358,6 +1368,30 @@ func generateJobID(name, workspace string) string {
42 return jobIDFor(name, workspace, time.Now().UnixNano())
43 }
44
45+// resolveJobID returns the producer-provided job ID if set and valid,
46+// otherwise generates one from name + workspace + timestamp.
47+func resolveJobID(name, workspace, providedID string) string {
48+ if providedID != "" && validateJobID(providedID) {
49+ return providedID
50+ }
51+ return generateJobID(name, workspace)
52+}
53+
54+// validateJobID checks that the ID is safe to use in session names and paths.
55+// Constraints: alphanumeric + hyphens only, no dots (breaks parseJobPrefix),
56+// 1-16 chars to keep session names reasonable.
57+func validateJobID(id string) bool {
58+ if len(id) == 0 || len(id) > 16 {
59+ return false
60+ }
61+ for _, r := range id {
62+ if !((r >= 'a' && r <= 'z') || (r >= 'A' && r <= 'Z') || (r >= '0' && r <= '9') || r == '-') {
63+ return false
64+ }
65+ }
66+ return true
67+}
68+
69 func runtimeOS() string {
70 return runtime.GOOS
71 }
+52,
-0
1@@ -416,6 +416,58 @@ func TestGenerateJobID(t *testing.T) {
2 }
3 }
4
5+func TestValidateJobID(t *testing.T) {
6+ tests := []struct {
7+ id string
8+ want bool
9+ }{
10+ {"a3f2b8c1", true}, // 8-char hex (commit short SHA)
11+ {"abc-123", true}, // alphanumeric + hyphen
12+ {"PR42", true}, // uppercase + digits
13+ {"a", true}, // single char
14+ {"abcdefghij123456", true}, // 16 chars (max)
15+ {"", false}, // empty
16+ {"abcdefghij1234567", false}, // 17 chars (too long)
17+ {"abc.def", false}, // contains dot (breaks parseJobPrefix)
18+ {"abc/def", false}, // contains slash
19+ {"abc def", false}, // contains space
20+ {"abc_def", false}, // contains underscore
21+ }
22+
23+ for _, tt := range tests {
24+ got := validateJobID(tt.id)
25+ if got != tt.want {
26+ t.Errorf("validateJobID(%q) = %v, want %v", tt.id, got, tt.want)
27+ }
28+ }
29+}
30+
31+func TestResolveJobID(t *testing.T) {
32+ // Valid producer ID is used as-is
33+ id := resolveJobID("myrepo", "/workspace", "a3f2b8c1")
34+ if id != "a3f2b8c1" {
35+ t.Errorf("resolveJobID with valid ID = %q, want %q", id, "a3f2b8c1")
36+ }
37+
38+ // Empty producer ID falls back to generated
39+ id = resolveJobID("myrepo", "/workspace", "")
40+ if id == "" || len(id) != 8 {
41+ t.Errorf("resolveJobID with empty ID = %q, want 8-char generated ID", id)
42+ }
43+
44+ // Invalid producer ID (contains dot) falls back to generated
45+ id = resolveJobID("myrepo", "/workspace", "1.0.0")
46+ if id == "1.0.0" || len(id) != 8 {
47+ t.Errorf("resolveJobID with invalid ID = %q, want 8-char generated ID", id)
48+ }
49+
50+ // Invalid producer ID (too long) falls back to generated
51+ id = resolveJobID("myrepo", "/workspace", "this_is_way_too_long_for_a_job_id")
52+ if len(id) != 8 {
53+ t.Errorf("resolveJobID with too-long ID = %q, want 8-char generated ID", id)
54+ }
55+}
56+
57 func TestAllCompleted(t *testing.T) {
58 tests := []struct {
59 name string