Skip to content

Commit 3947632

Browse files
committed
Merge branch 'dev'
# Conflicts: # src/main/java/org/example/Main.java
2 parents ede1e4d + d973dde commit 3947632

25 files changed

+1138
-9
lines changed

.idea/uiDesigner.xml

Lines changed: 124 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

README.md

3.32 KB

��# GHAWatch

GHAWatch — GitHub Actions Workflow Monitor

GHAWatch is a lightweight Java CLI tool that monitors GitHub Actions workflow runs, jobs, and steps for a given repository, printing real-time updates to stdout — one line per event. It supports:- Workflow run started / completed- Job started / completed- Step started / completed- Includes timestamps, run/job IDs, step number, branch, SHA, status, and message The tool is fault-tolerant, handles GitHub API rate limits, and resumes from where it stopped using a local JSON state file (~/.gha-watch///state.json).--

Tech Stack

  • Java 17 — stable LTS.
  • Maven — simple build/dependency management.
  • OkHttp — lightweight HTTP client for GitHub API calls.
  • Jackson — fast JSON parsing and DTO mapping.
  • SLF4J — clean logging with optional verbose mode.

Build

mvn clean package--

Run

java -jar target/GHAWatch-1.0-SNAPSHOT-jar-with-dependencies.jar [options]

Example: export GITHUB_TOKEN=ghp_xxx java -jar target/GHAWatch-1.0-SNAPSHOT-jar-with-dependencies.jar Anvarjon7/learning-platfrom--interval 5 --verbose --token ghp_xxx--

CLI Options--token--interval

--state--since-seconds--verbose--

Event Format

2025-12-01T14:59:25.099487+01:00 | JOB_STARTED | run=19823269680 | job=56790344506 | step=- | branch=main | sha=c42a342 | status=completed | msg="Job started: build"--

Architecture

  • Main — CLI entry point
  • GitHubClient — GitHub API wrapper with rate-limit handling
  • MonitorEngine — polling loop and event detection
  • StateStore — load/save JSON state
  • EventEmitter — prints events to stdout

Requirements

Java 17+ Maven GitHub token (repo or public_repo or actions:read)

pom.xml

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,13 @@
4646
<version>${jackson.version}</version>
4747
</dependency>
4848

49+
<dependency>
50+
<groupId>org.projectlombok</groupId>
51+
<artifactId>lombok</artifactId>
52+
<version>1.18.42</version>
53+
<scope>provided</scope>
54+
</dependency>
55+
4956
<dependency>
5057
<groupId>com.fasterxml.jackson.datatype</groupId>
5158
<artifactId>jackson-datatype-jsr310</artifactId>
@@ -83,7 +90,7 @@
8390
</descriptorRefs>
8491
<archive>
8592
<manifest>
86-
<mainClass>com.ghmonitor.cli.Main</mainClass>
93+
<mainClass>org.example.cli.Main</mainClass>
8794
</manifest>
8895
</archive>
8996
</configuration>
@@ -98,6 +105,7 @@
98105
</executions>
99106
</plugin>
100107

108+
101109
<!-- Java Compiler -->
102110
<plugin>
103111
<groupId>org.apache.maven.plugins</groupId>

src/main/java/org/example/Main.java

Lines changed: 0 additions & 8 deletions
This file was deleted.
Lines changed: 133 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,133 @@
1+
package org.example.cli;
2+
3+
import org.example.event.EventEmitter;
4+
import org.example.github.GitHubClient;
5+
import org.example.monitor.MonitorEngine;
6+
import org.example.state.MonitorState;
7+
import org.example.state.StateStore;
8+
import org.slf4j.Logger;
9+
import org.slf4j.LoggerFactory;
10+
import picocli.CommandLine;
11+
12+
import java.nio.file.Path;
13+
import java.nio.file.Paths;
14+
import java.util.concurrent.Callable;
15+
16+
@CommandLine.Command(
17+
name = "gha-watch",
18+
version = "GHAWatch 1.0.0",
19+
description = "Monitor GithubActions workflow runs and print one-line events to stdout."
20+
)
21+
public class Main implements Callable<Integer> {
22+
23+
private static final Logger log = LoggerFactory.getLogger(Main.class);
24+
25+
@CommandLine.Parameters(index = "0", paramLabel = "<owner/repo>",
26+
description = "Repository in the form owner/repo")
27+
private String repoArg;
28+
29+
@CommandLine.Option(names = {"--token"}, description = "Github personal access token (overrides GITHUB_TOKEN env)")
30+
private String tokenOpt;
31+
32+
@CommandLine.Option(names = {"--state"}, description = "Path to the state file (default: ~/.gha-watch/<owner>/<repo>/state.json)")
33+
private String stateFileOpt;
34+
@CommandLine.Option(names = {"--interval"}, description = "Polling interval in seconds (default: ${DEFAULT-VALUE})", defaultValue = "10")
35+
36+
private int intervalSeconds;
37+
@CommandLine.Option(names = {"--verbose"}, description = "Enable verbose (DEBUG) logging")
38+
private boolean verbose;
39+
@CommandLine.Option(names = {"--since-seconds"}, description = "When first run: look back this many seconds to emit recent completion events (default: 0)", defaultValue = "0")
40+
private long sinceSeconds;
41+
42+
public static void main(String[] args) {
43+
44+
int exit = new CommandLine(new Main()).execute(args);
45+
System.exit(exit);
46+
}
47+
48+
@Override
49+
public Integer call() throws Exception {
50+
51+
configureLogging(verbose);
52+
53+
System.out.println("GHAWatch v1.0.0 - Github Actions Monitor");
54+
System.out.println("JETBRAINS");
55+
System.out.println("-----------------------------------------");
56+
57+
if (repoArg == null || !repoArg.contains("/")) {
58+
System.err.println("Repository must be specified in the form: owner/repo");
59+
return 2;
60+
}
61+
62+
String[] parts = repoArg.split("/", 2);
63+
String owner = parts[0];
64+
String repo = parts[1];
65+
66+
if (owner.isEmpty() || repo.isEmpty()) {
67+
System.err.println("Repository must be specified in the form: owner/repo");
68+
return 2;
69+
}
70+
71+
String token = resolveToken(tokenOpt);
72+
if (token == null || token.isBlank()) {
73+
System.err.println("ERROR: GitHub token not provided. Use --token or set GITHUB_TOKEN environment variable.");
74+
return 3;
75+
}
76+
77+
Path statePath = resolveStateFilePath(stateFileOpt, owner, repo);
78+
log.info("Using state file: {}", statePath.toAbsolutePath());
79+
80+
GitHubClient client = new GitHubClient(token);
81+
StateStore store = new StateStore(statePath.toString());
82+
MonitorState state = store.load();
83+
84+
if (sinceSeconds > 0 && state.getLastProcessedRunId() == 0) {
85+
// We won't fetch by timestamp; this flag will be used in Phase 6 to decide lookback;
86+
log.info("First run lookback requested: {} seconds — (will be applied if implemented)", sinceSeconds);
87+
}
88+
89+
EventEmitter emitter = new EventEmitter();
90+
MonitorEngine engine = new MonitorEngine(
91+
client, store, emitter, owner, repo, intervalSeconds * 1000L
92+
);
93+
94+
try {
95+
engine.start();
96+
return 0;
97+
} catch (Exception e) {
98+
log.error("Fatal error: {}", e.getMessage(), e);
99+
System.err.println("Fatal error: " + e.getMessage());
100+
return 1;
101+
}
102+
}
103+
104+
private void configureLogging(boolean verbose) {
105+
if (verbose) {
106+
System.setProperty("org.slf4j.simpleLogger.defaultLogLevel", "debug");
107+
System.out.println("[verbose] debug logging enabled");
108+
} else {
109+
System.setProperty("org.slf4j.simpleLogger.defaultLogLevel", "info");
110+
}
111+
112+
System.setProperty("org.slf4j.simpleLogger.showDateTime", "true");
113+
System.setProperty("org.slf4j.simpleLogger.dateTimeFormat", "yyyy-MM-dd'T'HH:mm:ss");
114+
}
115+
116+
private String resolveToken(String tokenOpt) {
117+
if (tokenOpt != null && !tokenOpt.isBlank()) return tokenOpt;
118+
String env = System.getenv("GITHUB_TOKEN");
119+
if (env != null && !env.isBlank()) return env;
120+
String env2 = System.getenv("GH_TOKEN");
121+
if (env2 != null && !env2.isBlank()) return env2;
122+
return null;
123+
}
124+
125+
private Path resolveStateFilePath(String explicit, String owner, String repo) {
126+
if (explicit != null && !explicit.isBlank()) {
127+
return Paths.get(explicit);
128+
}
129+
130+
String home = System.getProperty("user.home");
131+
return Paths.get(home, ".gha-watch", owner, repo, "state.json");
132+
}
133+
}
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
package org.example.event;
2+
3+
import java.time.format.DateTimeFormatter;
4+
import java.util.StringJoiner;
5+
6+
public class EventEmitter {
7+
8+
private static final DateTimeFormatter ISO = DateTimeFormatter.ISO_OFFSET_DATE_TIME;
9+
10+
public synchronized void emit(WorkflowEvent event) {
11+
12+
StringJoiner sj = new StringJoiner(" | ");
13+
14+
sj.add(ISO.format(event.getTimeStamp()));
15+
16+
sj.add(event.getEventType().name());
17+
18+
sj.add("run=" + event.getRunId());
19+
20+
event.getJobId().ifPresentOrElse(
21+
id -> sj.add("job=" + id),
22+
() -> sj.add("job=-")
23+
);
24+
25+
event.getStepNumber().ifPresentOrElse(
26+
step -> sj.add("step=" + step),
27+
() -> sj.add("step=-")
28+
);
29+
30+
event.getBranch().ifPresentOrElse(
31+
b -> sj.add("branch=" + b),
32+
() -> sj.add("branch=-")
33+
);
34+
35+
event.getShaShort().ifPresentOrElse(
36+
sha -> sj.add("sha=" + sha),
37+
() -> sj.add("sha=-")
38+
);
39+
40+
event.getStatus().ifPresentOrElse(
41+
st -> sj.add("status=" + st),
42+
() -> sj.add("status=-")
43+
);
44+
45+
event.getMessage().ifPresentOrElse(
46+
msg -> sj.add("msg=\"" + escape(msg) + "\""),
47+
() -> sj.add("msg=-")
48+
);
49+
50+
System.out.println(sj.toString());
51+
52+
}
53+
54+
private String escape(String s) {
55+
return s.replace("\"", "\\\"");
56+
}
57+
}
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
package org.example.event;
2+
3+
public enum EventType {
4+
5+
WORKFLOW_QUEUED,
6+
WORKFLOW_STARTED,
7+
WORKFLOW_COMPLETED,
8+
JOB_STARTED,
9+
JOB_COMPLETED,
10+
STEP_STARTED,
11+
STEP_COMPLETED
12+
}

0 commit comments

Comments
 (0)