Skip to content

Commit d973dde

Browse files
committed
refactor: final package cleanup and documentation update
1 parent e7a583b commit d973dde

File tree

14 files changed

+68
-58
lines changed

14 files changed

+68
-58
lines changed

README.md

-132 Bytes

GHAWatch — GitHub Actions Workflow Monitor

GHAWatch is a lightweight command-line tool that monitors GitHub Actions workflow runs, jobs, and steps for a given repository and prints updates in real time.

It reports:

  • workflow run started / completed
  • job started / completed
  • step started / completed

One event is printed per line to stdout.
The tool is fault-tolerant, handles GitHub API rate limits, and resumes from where it stopped using a small local JSON state file.


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.

Usage

Build

mvn clean package--

Run

java -jar target/GHAWatch-1.0-SNAPSHOT-jar-with-dependencies.jar <owner/repo>[options]

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

Options

--interval N Poll interval in seconds (default 10) --token VALUE GitHub token (or use GITHUB_TOKEN env var) --verbose Enable debug logging --state PATH Custom state file path


CLI Options--token--interval

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

Event Format

2025-112025-12-26T1201T14:59:25.099487+01:3400 | JOB_STARTED | repo=owner/repo | run=119823269680 | job=1256790344506 | step=- | branch=main | sha=abc1234c42a342 | status=in_progresscompleted | 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 with public or repo read access

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

pom.xml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -90,7 +90,7 @@
9090
</descriptorRefs>
9191
<archive>
9292
<manifest>
93-
<mainClass>org.example.Main</mainClass>
93+
<mainClass>org.example.cli.Main</mainClass>
9494
</manifest>
9595
</archive>
9696
</configuration>
Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
1-
package org.example;
1+
package org.example.cli;
22

3+
import org.example.event.EventEmitter;
34
import org.example.github.GitHubClient;
4-
import org.example.monitor.EventEmitter;
55
import org.example.monitor.MonitorEngine;
66
import org.example.state.MonitorState;
77
import org.example.state.StateStore;
@@ -28,9 +28,11 @@ public class Main implements Callable<Integer> {
2828

2929
@CommandLine.Option(names = {"--token"}, description = "Github personal access token (overrides GITHUB_TOKEN env)")
3030
private String tokenOpt;
31-
@CommandLine.Option(names = {"--interval"}, description = "Polling interval in seconds (default: ${DEFAULT-VALUE})", defaultValue = "10")
32-
private String stateFileOpt;
31+
3332
@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+
3436
private int intervalSeconds;
3537
@CommandLine.Option(names = {"--verbose"}, description = "Enable verbose (DEBUG) logging")
3638
private boolean verbose;
@@ -113,7 +115,7 @@ private void configureLogging(boolean verbose) {
113115

114116
private String resolveToken(String tokenOpt) {
115117
if (tokenOpt != null && !tokenOpt.isBlank()) return tokenOpt;
116-
String env = System.getenv("GITHUB TOKEN");
118+
String env = System.getenv("GITHUB_TOKEN");
117119
if (env != null && !env.isBlank()) return env;
118120
String env2 = System.getenv("GH_TOKEN");
119121
if (env2 != null && !env2.isBlank()) return env2;

src/main/java/org/example/monitor/EventEmitter.java renamed to src/main/java/org/example/event/EventEmitter.java

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
package org.example.monitor;
1+
package org.example.event;
22

33
import java.time.format.DateTimeFormatter;
44
import java.util.StringJoiner;
@@ -7,7 +7,7 @@ public class EventEmitter {
77

88
private static final DateTimeFormatter ISO = DateTimeFormatter.ISO_OFFSET_DATE_TIME;
99

10-
public synchronized void emit(WorkflowEvent event){
10+
public synchronized void emit(WorkflowEvent event) {
1111

1212
StringJoiner sj = new StringJoiner(" | ");
1313

@@ -23,7 +23,7 @@ public synchronized void emit(WorkflowEvent event){
2323
);
2424

2525
event.getStepNumber().ifPresentOrElse(
26-
step -> sj.add("step=" +step),
26+
step -> sj.add("step=" + step),
2727
() -> sj.add("step=-")
2828
);
2929

@@ -33,7 +33,7 @@ public synchronized void emit(WorkflowEvent event){
3333
);
3434

3535
event.getShaShort().ifPresentOrElse(
36-
sha -> sj.add("sha=" +sha),
36+
sha -> sj.add("sha=" + sha),
3737
() -> sj.add("sha=-")
3838
);
3939

src/main/java/org/example/monitor/EventType.java renamed to src/main/java/org/example/event/EventType.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
package org.example.monitor;
1+
package org.example.event;
22

33
public enum EventType {
44

src/main/java/org/example/monitor/WorkflowEvent.java renamed to src/main/java/org/example/event/WorkflowEvent.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
package org.example.monitor;
1+
package org.example.event;
22

33
import lombok.AllArgsConstructor;
44

src/main/java/org/example/github/GitHubClient.java

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,10 @@
33
import com.fasterxml.jackson.databind.DeserializationFeature;
44
import com.fasterxml.jackson.databind.ObjectMapper;
55
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
6-
import okhttp3.*;
6+
import okhttp3.OkHttpClient;
7+
import okhttp3.Request;
8+
import okhttp3.Response;
9+
import okhttp3.ResponseBody;
710
import org.example.github.exception.GithubApiException;
811
import org.example.github.exception.RateLimitException;
912
import org.example.github.model.JobsResponse;

src/main/java/org/example/github/exception/RateLimitException.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
11
package org.example.github.exception;
22

3-
public class RateLimitException extends RuntimeException{
3+
public class RateLimitException extends RuntimeException {
44

55
private final long retryAfterMillis;
66

7-
public RateLimitException(long retryAfterMillis){
7+
public RateLimitException(long retryAfterMillis) {
88
super("Github API rete limit exceeded. Retry after: " + retryAfterMillis);
99
this.retryAfterMillis = retryAfterMillis;
1010
}

src/main/java/org/example/monitor/MonitorEngine.java

Lines changed: 24 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,23 @@
11
package org.example.monitor;
22

3-
import lombok.AllArgsConstructor;
3+
import org.example.event.EventEmitter;
4+
import org.example.event.EventType;
5+
import org.example.event.WorkflowEvent;
46
import org.example.github.GitHubClient;
57
import org.example.github.exception.GithubApiException;
68
import org.example.github.exception.RateLimitException;
79
import org.example.github.model.*;
810
import org.example.monitor.backoff.BackOffStrategy;
9-
import org.example.monitor.state.JobSnapshot;
10-
import org.example.monitor.state.RunSnapshot;
11-
import org.example.monitor.state.StepSnapshot;
1211
import org.example.state.MonitorState;
1312
import org.example.state.StateStore;
13+
import org.example.state.snapshot.JobSnapshot;
14+
import org.example.state.snapshot.RunSnapshot;
15+
import org.example.state.snapshot.StepSnapshot;
1416
import org.slf4j.Logger;
1517
import org.slf4j.LoggerFactory;
1618

1719
import java.io.IOException;
1820
import java.time.OffsetDateTime;
19-
import java.util.Comparator;
2021
import java.util.HashMap;
2122
import java.util.List;
2223
import java.util.Map;
@@ -72,22 +73,22 @@ public void start() {
7273

7374
} catch (RateLimitException rle) {
7475
long waitMs = rle.getRetryAfterMillis();
75-
log.warn("Rate limited by Github. Sleeping {} ms." +waitMs);
76+
log.warn("Rate limited by Github. Sleeping {} ms." + waitMs);
7677
sleepInterruptibly(waitMs);
7778
} catch (GithubApiException ghe) {
7879
int delaySec = backOff.nextDelay();
79-
log.warn("Github APi error: {}. Backing off {}s and retrying.", ghe.getMessage(),delaySec);
80+
log.warn("Github APi error: {}. Backing off {}s and retrying.", ghe.getMessage(), delaySec);
8081
sleepInterruptibly(delaySec + 1000L);
81-
}catch (Exception e){
82+
} catch (Exception e) {
8283
int delaySec = backOff.nextDelay();
83-
log.error("Unexpected error during polling: {}. Backing off {}s.", e.getMessage(),delaySec);
84+
log.error("Unexpected error during polling: {}. Backing off {}s.", e.getMessage(), delaySec);
8485
}
8586
}
8687

8788
try {
8889
stateStore.save(state);
8990
log.info("Final state saved.");
90-
}catch (Exception e){
91+
} catch (Exception e) {
9192
log.error("Failed to save final state: {}", e.getMessage());
9293
}
9394

@@ -104,7 +105,7 @@ private void pollOnce(MonitorState state) throws IOException {
104105
return;
105106
}
106107

107-
runs.sort((a,b) -> Long.compare(a.getId(),b.getId()));
108+
runs.sort((a, b) -> Long.compare(a.getId(), b.getId()));
108109

109110
long maxSeenRunId = state.getLastProcessedRunId();
110111

@@ -116,14 +117,14 @@ private void pollOnce(MonitorState state) throws IOException {
116117
}
117118

118119
try {
119-
processRunWithSnapshot(run,state);
120-
}catch (RateLimitException | GithubApiException e){
120+
processRunWithSnapshot(run, state);
121+
} catch (RateLimitException | GithubApiException e) {
121122
throw e;
122-
}catch (Exception e){
123+
} catch (Exception e) {
123124
log.error("Error processing run {}: {}", runId, e.getMessage());
124125
}
125126

126-
if (runId > maxSeenRunId){
127+
if (runId > maxSeenRunId) {
127128
maxSeenRunId = runId;
128129
}
129130

@@ -193,7 +194,7 @@ private void processRunWithSnapshot(WorkflowRun run, MonitorState state) throws
193194
List<Job> jobs = jobsResponse == null ? null : jobsResponse.getJobs();
194195
if (jobs == null) jobs = List.of();
195196

196-
Map<Long,JobSnapshot> currentJobSnapshots = new HashMap<>();
197+
Map<Long, JobSnapshot> currentJobSnapshots = new HashMap<>();
197198

198199
for (Job job : jobs) {
199200
long jobId = job.getId();
@@ -308,20 +309,20 @@ private String shorten(String sha) {
308309
return sha != null && sha.length() > 7 ? sha.substring(0, 7) : sha;
309310
}
310311

311-
private String safeString(String s){
312+
private String safeString(String s) {
312313
return s == null ? "-" : s;
313314
}
314315

315-
private void sleepInterruptibly(long millis){
316-
try{
316+
private void sleepInterruptibly(long millis) {
317+
try {
317318
long slept = 0;
318319
final long chunk = 1000L;
319-
while (running.get() && slept < millis){
320-
long toSleep =Math.min(chunk,millis-slept);
320+
while (running.get() && slept < millis) {
321+
long toSleep = Math.min(chunk, millis - slept);
321322
Thread.sleep(toSleep);
322-
slept+=toSleep;
323+
slept += toSleep;
323324
}
324-
}catch (InterruptedException e){
325+
} catch (InterruptedException e) {
325326
Thread.currentThread().interrupt();
326327
running.set(false);
327328
}

src/main/java/org/example/state/MonitorState.java

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,8 @@
55
import lombok.Getter;
66
import lombok.NoArgsConstructor;
77
import lombok.Setter;
8-
import org.example.monitor.state.RunSnapshot;
8+
import org.example.state.snapshot.RunSnapshot;
99

10-
import java.util.HashMap;
1110
import java.util.Map;
1211

1312
@NoArgsConstructor
@@ -25,7 +24,7 @@ public MonitorState(long lastProcessedRunId) {
2524
this.lastProcessedRunId = lastProcessedRunId;
2625
}
2726

28-
public void updateSnapshot(long runId, RunSnapshot snapshot){
29-
runSnapshots.put(runId,snapshot);
30-
}
27+
public void updateSnapshot(long runId, RunSnapshot snapshot) {
28+
runSnapshots.put(runId, snapshot);
29+
}
3130
}

0 commit comments

Comments
 (0)