Skip to content

Commit ed6caf1

Browse files
committed
feat: job metrics report
1 parent 4447855 commit ed6caf1

File tree

6 files changed

+582
-33
lines changed

6 files changed

+582
-33
lines changed

.semaphore/semaphore.yml

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,18 @@ agent:
44
machine:
55
type: e2-standard-2
66
os_image: ubuntu2004
7+
8+
global_job_config:
9+
prologue:
10+
commands:
11+
- artifact pull project bin/test-results.v0.8.0-rc.0 -d /usr/local/bin/test-results --force
12+
- sudo chmod +x /usr/local/bin/test-results
13+
epilogue:
14+
always:
15+
commands:
16+
- test-results command-metrics report.md
17+
- test-results job-metrics report.md
18+
- artifact push job -d .semaphore/REPORT.md report.md
719
blocks:
820
- name: Unit tests
921
dependencies: []
@@ -67,6 +79,7 @@ blocks:
6779
commands:
6880
- make build
6981
- artifact push workflow bin/test-results -d bin/test-results
82+
- artifact push project bin/test-results -d bin/test-results.v0.8.0-rc.0 --force
7083
after_pipeline:
7184
task:
7285
jobs:

cmd/command-metrics.go

Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
package cmd
2+
3+
import (
4+
"encoding/json"
5+
"fmt"
6+
"os"
7+
"path/filepath"
8+
"strings"
9+
10+
"github.com/spf13/cobra"
11+
)
12+
13+
var commandMetricsCmd = &cobra.Command{
14+
Use: "command-metrics",
15+
Short: "Generates a command summary markdown report from agent metrics",
16+
Long: `Generates a command summary markdown report from agent metrics`,
17+
Args: cobra.ExactArgs(1),
18+
RunE: func(cmd *cobra.Command, args []string) error {
19+
out := ""
20+
21+
srcFile, err := cmd.Flags().GetString("src")
22+
if err != nil {
23+
return fmt.Errorf("src cannot be parsed: %w", err)
24+
}
25+
26+
matches, err := filepath.Glob(srcFile)
27+
if err != nil || len(matches) == 0 {
28+
return fmt.Errorf("failed to find job log file: %w", err)
29+
}
30+
31+
type CmdFinished struct {
32+
Event string `json:"event"`
33+
Directive string `json:"directive"`
34+
StartedAt int64 `json:"started_at"`
35+
FinishedAt int64 `json:"finished_at"`
36+
}
37+
38+
lines, err := os.ReadFile(matches[0])
39+
if err != nil {
40+
return fmt.Errorf("could not read job log: %w", err)
41+
}
42+
43+
var flowNodes []CmdFinished
44+
for _, raw := range strings.Split(string(lines), "\n") {
45+
if strings.TrimSpace(raw) == "" {
46+
continue
47+
}
48+
var entry CmdFinished
49+
if err := json.Unmarshal([]byte(raw), &entry); err != nil {
50+
continue
51+
}
52+
if entry.Event == "cmd_finished" {
53+
flowNodes = append(flowNodes, entry)
54+
}
55+
}
56+
57+
out += "## 🧭 Job Timeline\n\n```mermaid\ngantt\n title Job Command Timeline\n dateFormat X\n axisFormat %X\n"
58+
for i, node := range flowNodes {
59+
duration := node.FinishedAt - node.StartedAt
60+
if duration < 1 {
61+
duration = 1
62+
}
63+
out += fmt.Sprintf(" %s[%ds] :step%d, %d, %ds\n", node.Directive, duration, i, node.StartedAt, duration)
64+
}
65+
out += "```\n"
66+
67+
f, err := os.OpenFile(args[0], os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0600)
68+
if err != nil {
69+
return fmt.Errorf("failed to open output file: %w", err)
70+
}
71+
defer f.Close()
72+
73+
if _, err := f.WriteString(out); err != nil {
74+
return fmt.Errorf("failed to append to output file: %w", err)
75+
}
76+
return nil
77+
},
78+
}
79+
80+
func init() {
81+
commandMetricsCmd.Flags().String("src", "/tmp/job_log_*.json", "source file to read system metrics from")
82+
rootCmd.AddCommand(commandMetricsCmd)
83+
}

cmd/job-metrics.go

Lines changed: 199 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,199 @@
1+
package cmd
2+
3+
import (
4+
"bufio"
5+
"fmt"
6+
"os"
7+
"path/filepath"
8+
"regexp"
9+
"strings"
10+
"time"
11+
12+
"github.com/spf13/cobra"
13+
)
14+
15+
var jobMetricsCmd = &cobra.Command{
16+
Use: "job-metrics",
17+
Short: "Generates a job resource utilization summary markdown report from agent metrics",
18+
Long: `Generates a job resource utilization summary markdown report from agent metrics`,
19+
Args: cobra.ExactArgs(1),
20+
RunE: func(cmd *cobra.Command, args []string) error {
21+
type JobMetric struct {
22+
Timestamp string
23+
CPU float64
24+
Memory float64
25+
SystemDisk float64
26+
DockerDisk float64
27+
}
28+
29+
srcFile, err := cmd.Flags().GetString("src")
30+
if err != nil {
31+
return fmt.Errorf("src cannot be parsed: %w", err)
32+
}
33+
34+
file, err := os.Open(filepath.Clean(srcFile))
35+
if err != nil {
36+
return fmt.Errorf("failed to open log file: %w", err)
37+
}
38+
defer file.Close()
39+
40+
metricLineRegex := regexp.MustCompile(`^(.*?) \| cpu:(.*)%, mem:\s*(.*)%, system_disk:\s*(.*)%, docker_disk:\s*(.*)%,(.*)$`)
41+
42+
var metrics []JobMetric
43+
scanner := bufio.NewScanner(file)
44+
for scanner.Scan() {
45+
line := scanner.Text()
46+
matches := metricLineRegex.FindStringSubmatch(line)
47+
if len(matches) != 7 {
48+
continue
49+
}
50+
var m JobMetric
51+
m.Timestamp = matches[1]
52+
_, err := fmt.Sscanf(matches[2], "%f", &m.CPU)
53+
if err != nil {
54+
continue
55+
}
56+
_, err = fmt.Sscanf(matches[3], "%f", &m.Memory)
57+
if err != nil {
58+
continue
59+
}
60+
_, err = fmt.Sscanf(matches[4], "%f", &m.SystemDisk)
61+
if err != nil {
62+
continue
63+
}
64+
_, err = fmt.Sscanf(matches[5], "%f", &m.DockerDisk)
65+
if err != nil {
66+
continue
67+
}
68+
metrics = append(metrics, m)
69+
}
70+
71+
if err := scanner.Err(); err != nil {
72+
return fmt.Errorf("error reading file: %w", err)
73+
}
74+
75+
if len(metrics) == 0 {
76+
return fmt.Errorf("no valid data found")
77+
}
78+
79+
step := 1
80+
if len(metrics) > 100 {
81+
step = len(metrics) / 100
82+
}
83+
84+
var (
85+
xLabels []string
86+
cpuSeries []string
87+
memSeries []string
88+
sysDiskSeries []string
89+
dockerDiskSeries []string
90+
)
91+
92+
layout := "Mon 02 Jan 2006 03:04:05 PM MST"
93+
startTime, err := time.Parse(layout, metrics[0].Timestamp)
94+
if err != nil {
95+
return fmt.Errorf("failed to parse start time: %w", err)
96+
}
97+
98+
min := func(f1, f2 float64) float64 {
99+
if f1 < f2 {
100+
return f1
101+
}
102+
return f2
103+
}
104+
105+
max := func(f1, f2 float64) float64 {
106+
if f1 > f2 {
107+
return f1
108+
}
109+
return f2
110+
}
111+
112+
cpuMin, cpuMax := metrics[0].CPU, metrics[0].CPU
113+
memMin, memMax := metrics[0].Memory, metrics[0].Memory
114+
diskMin, diskMax := metrics[0].SystemDisk, metrics[0].SystemDisk
115+
dockerMin, dockerMax := metrics[0].DockerDisk, metrics[0].DockerDisk
116+
117+
for i := 0; i < len(metrics); i += step {
118+
m := metrics[i]
119+
cpuMin = min(cpuMin, m.CPU)
120+
cpuMax = max(cpuMax, m.CPU)
121+
memMin = min(memMin, m.Memory)
122+
memMax = max(memMax, m.Memory)
123+
diskMin = min(diskMin, m.SystemDisk)
124+
diskMax = max(diskMax, m.SystemDisk)
125+
dockerMin = min(dockerMin, m.DockerDisk)
126+
dockerMax = max(dockerMax, m.DockerDisk)
127+
128+
t, err := time.Parse(layout, m.Timestamp)
129+
if err != nil {
130+
xLabels = append(xLabels, "\"??:??\"")
131+
} else {
132+
duration := t.Sub(startTime)
133+
seconds := int(duration.Seconds())
134+
xLabels = append(xLabels, fmt.Sprintf("\"%02d:%02d\"", seconds/60, seconds%60))
135+
}
136+
cpuSeries = append(cpuSeries, fmt.Sprintf("%.2f", m.CPU))
137+
memSeries = append(memSeries, fmt.Sprintf("%.2f", m.Memory))
138+
sysDiskSeries = append(sysDiskSeries, fmt.Sprintf("%.2f", m.SystemDisk))
139+
dockerDiskSeries = append(dockerDiskSeries, fmt.Sprintf("%.2f", m.DockerDisk))
140+
}
141+
142+
out := "## 🎯 System Metrics Summary\n\n"
143+
out += fmt.Sprintf("**Total datapoints:** `%d` \n", len(metrics))
144+
out += fmt.Sprintf("**🕒 Time Range:** `%s` → `%s` \n\n", metrics[0].Timestamp, metrics[len(metrics)-1].Timestamp)
145+
out += fmt.Sprintf("- **🔥 CPU:** `min: %.2f%%`, `max: %.2f%%` \n", cpuMin, cpuMax)
146+
out += fmt.Sprintf("- **🧠 Memory:** `min: %.2f%%`, `max: %.2f%%` \n", memMin, memMax)
147+
out += fmt.Sprintf("- **💽 System Disk:** `min: %.2f%%`, `max: %.2f%%` \n", diskMin, diskMax)
148+
out += fmt.Sprintf("- **🐳 Docker Disk:** `min: %.2f%%`, `max: %.2f%%`\n\n", dockerMin, dockerMax)
149+
out += "---\n\n"
150+
151+
out += "```mermaid\n"
152+
out += "xychart-beta\n"
153+
out += "title \"CPU Usage\"\n"
154+
out += fmt.Sprintf("x-axis [%s]\n", strings.Join(xLabels, ", "))
155+
out += "y-axis \"Usage (%)\"\n"
156+
out += fmt.Sprintf("line [%s]\n", strings.Join(cpuSeries, ", "))
157+
out += "```\n\n"
158+
159+
out += "```mermaid\n"
160+
out += "xychart-beta\n"
161+
out += "title \"Memory Usage\"\n"
162+
out += fmt.Sprintf("x-axis [%s]\n", strings.Join(xLabels, ", "))
163+
out += "y-axis \"Usage (%)\"\n"
164+
out += fmt.Sprintf("line [%s]\n", strings.Join(memSeries, ", "))
165+
out += "```\n\n"
166+
167+
out += "```mermaid\n"
168+
out += "xychart-beta\n"
169+
out += "title \"System Disk Usage\"\n"
170+
out += fmt.Sprintf("x-axis [%s]\n", strings.Join(xLabels, ", "))
171+
out += "y-axis \"Disk Usage (%)\"\n"
172+
out += fmt.Sprintf("line [%s]\n", strings.Join(sysDiskSeries, ", "))
173+
out += "```\n"
174+
175+
out += "```mermaid\n"
176+
out += "xychart-beta\n"
177+
out += "title \"Docker Disk Usage\"\n"
178+
out += fmt.Sprintf("x-axis [%s]\n", strings.Join(xLabels, ", "))
179+
out += "y-axis \"Disk Usage (%)\"\n"
180+
out += fmt.Sprintf("line [%s]\n", strings.Join(dockerDiskSeries, ", "))
181+
out += "```\n"
182+
183+
f, err := os.OpenFile(args[0], os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0600)
184+
if err != nil {
185+
return fmt.Errorf("failed to open output file: %w", err)
186+
}
187+
defer f.Close()
188+
189+
if _, err := f.WriteString(out); err != nil {
190+
return fmt.Errorf("failed to append to output file: %w", err)
191+
}
192+
return nil
193+
},
194+
}
195+
196+
func init() {
197+
jobMetricsCmd.Flags().String("src", "/tmp/system-metrics", "source file to read system metrics from")
198+
rootCmd.AddCommand(jobMetricsCmd)
199+
}

priv/job-metrics/job_log.json

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
{"event":"job_started","timestamp":1747001724}
2+
{"event":"cmd_started","timestamp":1747001724,"directive":"Exporting environment variables"}
3+
{"event":"cmd_output","timestamp":1747001724,"output":"Exporting CI\n"}
4+
{"event":"cmd_output","timestamp":1747001724,"output":"Exporting DISPLAY\n"}
5+
{"event":"cmd_output","timestamp":1747001724,"output":"Exporting PAGER\n"}
6+
{"event":"cmd_output","timestamp":1747001724,"output":"Exporting SEMAPHORE\n"}
7+
{"event":"cmd_output","timestamp":1747001724,"output":"Exporting SEMAPHORE_AGENT_MACHINE_ENVIRONMENT_TYPE\n"}
8+
{"event":"cmd_output","timestamp":1747001724,"output":"Exporting SEMAPHORE_AGENT_MACHINE_OS_IMAGE\n"}
9+
{"event":"cmd_output","timestamp":1747001724,"output":"Exporting SEMAPHORE_AGENT_MACHINE_TYPE\n"}
10+
{"event":"cmd_output","timestamp":1747001724,"output":"Exporting SEMAPHORE_ARTIFACT_TOKEN\n"}
11+
{"event":"cmd_output","timestamp":1747001724,"output":"Exporting SEMAPHORE_CACHE_ARCHIVE_METHOD\n"}
12+
{"event":"cmd_output","timestamp":1747001724,"output":"Exporting SEMAPHORE_CACHE_BACKEND\n"}
13+
{"event":"cmd_output","timestamp":1747001724,"output":"Exporting SEMAPHORE_CACHE_PRIVATE_KEY_PATH\n"}
14+
{"event":"cmd_output","timestamp":1747001724,"output":"Exporting SEMAPHORE_CACHE_URL\n"}
15+
{"event":"cmd_output","timestamp":1747001724,"output":"Exporting SEMAPHORE_CACHE_USERNAME\n"}
16+
{"event":"cmd_output","timestamp":1747001724,"output":"Exporting SEMAPHORE_GIT_BRANCH\n"}
17+
{"event":"cmd_output","timestamp":1747001724,"output":"Exporting SEMAPHORE_GIT_COMMITTER\n"}
18+
{"event":"cmd_output","timestamp":1747001724,"output":"Exporting SEMAPHORE_GIT_COMMIT_AUTHOR\n"}
19+
{"event":"cmd_output","timestamp":1747001724,"output":"Exporting SEMAPHORE_GIT_COMMIT_RANGE\n"}
20+
{"event":"cmd_output","timestamp":1747001724,"output":"Exporting SEMAPHORE_GIT_DIR\n"}
21+
{"event":"cmd_output","timestamp":1747001724,"output":"Exporting SEMAPHORE_GIT_PROVIDER\n"}
22+
{"event":"cmd_output","timestamp":1747001724,"output":"Exporting SEMAPHORE_GIT_REF\n"}
23+
{"event":"cmd_output","timestamp":1747001724,"output":"Exporting SEMAPHORE_GIT_REF_TYPE\n"}
24+
{"event":"cmd_output","timestamp":1747001724,"output":"Exporting SEMAPHORE_GIT_REPO_NAME\n"}
25+
{"event":"cmd_output","timestamp":1747001724,"output":"Exporting SEMAPHORE_GIT_REPO_SLUG\n"}
26+
{"event":"cmd_output","timestamp":1747001724,"output":"Exporting SEMAPHORE_GIT_SHA\n"}
27+
{"event":"cmd_output","timestamp":1747001724,"output":"Exporting SEMAPHORE_GIT_URL\n"}
28+
{"event":"cmd_output","timestamp":1747001724,"output":"Exporting SEMAPHORE_GIT_WORKING_BRANCH\n"}
29+
{"event":"cmd_output","timestamp":1747001724,"output":"Exporting SEMAPHORE_JOB_CREATION_TIME\n"}
30+
{"event":"cmd_output","timestamp":1747001724,"output":"Exporting SEMAPHORE_JOB_ID\n"}
31+
{"event":"cmd_output","timestamp":1747001724,"output":"Exporting SEMAPHORE_JOB_NAME\n"}
32+
{"event":"cmd_output","timestamp":1747001724,"output":"Exporting SEMAPHORE_JOB_TYPE\n"}
33+
{"event":"cmd_output","timestamp":1747001724,"output":"Exporting SEMAPHORE_OIDC_TOKEN\n"}
34+
{"event":"cmd_output","timestamp":1747001724,"output":"Exporting SEMAPHORE_ORGANIZATION_URL\n"}
35+
{"event":"cmd_output","timestamp":1747001724,"output":"Exporting SEMAPHORE_PIPELINE_0_ARTEFACT_ID\n"}
36+
{"event":"cmd_output","timestamp":1747001724,"output":"Exporting SEMAPHORE_PIPELINE_ARTEFACT_ID\n"}
37+
{"event":"cmd_output","timestamp":1747001724,"output":"Exporting SEMAPHORE_PIPELINE_ID\n"}
38+
{"event":"cmd_output","timestamp":1747001724,"output":"Exporting SEMAPHORE_PIPELINE_PROMOTED_BY\n"}
39+
{"event":"cmd_output","timestamp":1747001724,"output":"Exporting SEMAPHORE_PIPELINE_PROMOTION\n"}
40+
{"event":"cmd_output","timestamp":1747001724,"output":"Exporting SEMAPHORE_PIPELINE_RERUN\n"}
41+
{"event":"cmd_output","timestamp":1747001724,"output":"Exporting SEMAPHORE_PROJECT_ID\n"}
42+
{"event":"cmd_output","timestamp":1747001724,"output":"Exporting SEMAPHORE_PROJECT_NAME\n"}
43+
{"event":"cmd_output","timestamp":1747001724,"output":"Exporting SEMAPHORE_TOOLBOX_METRICS_ENABLED\n"}
44+
{"event":"cmd_output","timestamp":1747001724,"output":"Exporting SEMAPHORE_WORKFLOW_HOOK_SOURCE\n"}
45+
{"event":"cmd_output","timestamp":1747001724,"output":"Exporting SEMAPHORE_WORKFLOW_ID\n"}
46+
{"event":"cmd_output","timestamp":1747001724,"output":"Exporting SEMAPHORE_WORKFLOW_NUMBER\n"}
47+
{"event":"cmd_output","timestamp":1747001724,"output":"Exporting SEMAPHORE_WORKFLOW_RERUN\n"}
48+
{"event":"cmd_output","timestamp":1747001724,"output":"Exporting SEMAPHORE_WORKFLOW_TRIGGERED_BY\n"}
49+
{"event":"cmd_output","timestamp":1747001724,"output":"Exporting SEMAPHORE_WORKFLOW_TRIGGERED_BY_API\n"}
50+
{"event":"cmd_output","timestamp":1747001724,"output":"Exporting SEMAPHORE_WORKFLOW_TRIGGERED_BY_HOOK\n"}
51+
{"event":"cmd_output","timestamp":1747001724,"output":"Exporting SEMAPHORE_WORKFLOW_TRIGGERED_BY_MANUAL_RUN\n"}
52+
{"event":"cmd_output","timestamp":1747001724,"output":"Exporting SEMAPHORE_WORKFLOW_TRIGGERED_BY_SCHEDULE\n"}
53+
{"event":"cmd_output","timestamp":1747001724,"output":"Exporting SSH_PRIVATE_KEY_PATH\n"}
54+
{"event":"cmd_output","timestamp":1747001724,"output":"Exporting TERM\n"}
55+
{"event":"cmd_finished","timestamp":1747001724,"directive":"Exporting environment variables","exit_code":0,"started_at":1747001724,"finished_at":1747001724}
56+
{"event":"cmd_started","timestamp":1747001724,"directive":"Injecting Files"}
57+
{"event":"cmd_output","timestamp":1747001724,"output":"Injecting /home/semaphore/.ssh/semaphore_cache_key with file mode 0600\n"}
58+
{"event":"cmd_output","timestamp":1747001724,"output":"Injecting /home/semaphore/.ssh/id_rsa with file mode 0600\n"}
59+
{"event":"cmd_output","timestamp":1747001724,"output":"Injecting /home/semaphore/commands.sh with file mode 0644\n"}
60+
{"event":"cmd_finished","timestamp":1747001724,"directive":"Injecting Files","exit_code":0,"started_at":1747001724,"finished_at":1747001724}
61+
{"event":"cmd_started","timestamp":1747001724,"directive":"Running the pre-job hook configured in the agent"}
62+
{"event":"cmd_output","timestamp":1747001724,"output":"Running: source /opt/semaphoreci/prejob/pre_job.sh\n"}
63+
{"event":"cmd_output","timestamp":1747001724,"output":"Warning: The agent is configured to fail the job if the pre-job hook fails.\n"}
64+
{"event":"cmd_output","timestamp":1747001724,"output":"Installing toolbox in pre-job-hook\nToolbox download URL: http://packages.semaphoreci.com/toolbox/re"}
65+
{"event":"cmd_output","timestamp":1747001724,"output":"leases/download/v1.23.1/linux.tar\nTar file: linux.tar\n"}
66+
{"event":"cmd_output","timestamp":1747001726,"output":"Attempt 5\ntoolbox/\ntoolbox/sem-dockerize\ntoolbox/toolbox\ntoolbox/retry\ntoolbox/enetwork\ntoolbo"}
67+
{"event":"cmd_output","timestamp":1747001726,"output":"x/artifact\ntoolbox/install-toolbox\ntoolbox/system-metrics-collector\ntoolbox/libcheckout\ntoolbox/"}
68+
{"event":"cmd_output","timestamp":1747001726,"output":"cache\ntoolbox/when_otp_24\ntoolbox/spc\ntoolbox/libchecksum\ntoolbox/when_otp_26\ntoolbox/when_otp_"}
69+
{"event":"cmd_output","timestamp":1747001726,"output":"25\ntoolbox/install-package\ntoolbox/README.md\ntoolbox/sem-context\ntoolbox/test-results\ntoolbox/s"}
70+
{"event":"cmd_output","timestamp":1747001726,"output":"em-version\ntoolbox/ssh-session-cli\ntoolbox/sem-service\ntoolbox/sem-install\ntoolbox/sem-service-c"}
71+
{"event":"cmd_output","timestamp":1747001726,"output":"heck-params\ntoolbox/Dockerfile.dev\ntoolbox/sem-semantic-release\nretry installed\nssh-session-cli "}
72+
{"event":"cmd_output","timestamp":1747001726,"output":"installed\ncache installed\nsem-context installed\nsem-service installed\nsem-dockerize installed\ns"}
73+
{"event":"cmd_output","timestamp":1747001726,"output":"em-service-check-params installed\nsem-semantic-release installed\nInstalling the artifacts CLI\nart"}
74+
{"event":"cmd_output","timestamp":1747001726,"output":"ifacts installed\nInstalling the test results CLI\ntest-results installed\nInstalling the install-pa"}
75+
{"event":"cmd_output","timestamp":1747001726,"output":"ckage for linux\ninstall-package installed\nInstalling the SPC CLI\nspc installed\nInstalling the wh"}
76+
{"event":"cmd_output","timestamp":1747001726,"output":"en CLI\nwhen installed\nInstalling the enetwork CLI\nenetwork installed\nStarting to collect System "}
77+
{"event":"cmd_output","timestamp":1747001726,"output":"Metrics in /tmp/system-metrics\nStarting virtual X frame buffer: Xvfb.\nToolbox installed\nStarting "}
78+
{"event":"cmd_output","timestamp":1747001726,"output":"ssh-agent\nAgent pid 2326\nssh-agent started\nConnecting to cache\n# 94.130.123.200:29920 SSH-2.0-Op"}
79+
{"event":"cmd_output","timestamp":1747001726,"output":"enSSH_8.2p1 Ubuntu-4ubuntu0.11\n# 94.130.123.200:29920 SSH-2.0-OpenSSH_8.2p1 Ubuntu-4ubuntu0.11\n# 94.130.123.200:29920 SSH-2.0-OpenSSH_8.2p1 Ubuntu-4ubuntu0.11\n# 94.130.123.200:29920 SSH-2.0-OpenSSH_8.2p1 Ubuntu-4ubuntu0.11\n# 94.130.123.200:29920 SSH-2.0-OpenSSH_8.2p1 Ubuntu-4ubuntu0.11\nIdentity added: /home/semaphore/.ssh/semaphore_cache_key (/home/semaphore/.ssh/semaphore_cache_key)\n"}
80+
{"event":"cmd_finished","timestamp":1747001726,"directive":"Running the pre-job hook configured in the agent","exit_code":0,"started_at":1747001724,"finished_at":1747001726}
81+
{"event":"cmd_started","timestamp":1747001726,"directive":"sleep 3600"}

0 commit comments

Comments
 (0)