Skip to content

Commit 6367299

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

File tree

6 files changed

+570
-36
lines changed

6 files changed

+570
-36
lines changed

.semaphore/semaphore.yml

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

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)