Skip to content

Add dependabot grouping/auto-merge, unit-test CI gate, and status endpoint#382

Open
sallyom wants to merge 1 commit into
containers:mainfrom
sallyom:improvements/ci-and-status
Open

Add dependabot grouping/auto-merge, unit-test CI gate, and status endpoint#382
sallyom wants to merge 1 commit into
containers:mainfrom
sallyom:improvements/ci-and-status

Conversation

@sallyom

@sallyom sallyom commented Jun 13, 2026

Copy link
Copy Markdown
Collaborator
  • .github/dependabot.yml: group minor/patch Go bumps into one weekly PR, track github-actions; keep major bumps separate for review
  • dependabot-auto-merge.yml: auto-merge minor/patch dependabot PRs once CI passes
  • test.yml: fast go vet + unit test gate (build tags + CGO deps)
  • status.go: opt-in HTTP server (FETCHIT_STATUS_ADDR) serving /healthz and /status (uptime, per-method schedule/run-count/last-run); wired into RunTargets without changing the Method interface

Summary by Sourcery

Add an opt-in HTTP health and status endpoint for FetchIt and introduce CI and dependency management automation.

New Features:

  • Expose an optional HTTP server controlled by FETCHIT_STATUS_ADDR that serves /healthz and a JSON /status endpoint with uptime and per-method schedule/run metadata.

Enhancements:

  • Track reconciliation schedules and run statistics per method and surface them through the new status endpoint.
  • Wire the status tracking into the target scheduling loop without changing the existing Method interface.

CI:

  • Add a fast GitHub Actions workflow that runs go vet and unit tests with the required build tags and CGO dependencies on pushes and pull requests to main.
  • Configure Dependabot to group weekly minor and patch Go module and GitHub Actions updates while keeping majors separate, and add a workflow to auto-merge eligible Dependabot PRs after checks pass.

Documentation:

  • Extend the README with usage documentation and examples for enabling and querying the new health and status HTTP endpoint.

@sourcery-ai

sourcery-ai Bot commented Jun 13, 2026

Copy link
Copy Markdown

Reviewer's Guide

Adds an opt-in HTTP health/status endpoint integrated with method scheduling, introduces a fast Go vet + unit-test CI workflow, and configures Dependabot with grouped updates and auto-merge for minor/patch changes, along with README documentation for the new status server.

Sequence diagram for the new HTTP health and status endpoints

sequenceDiagram
    actor User
    participant Fetchit as Fetchit.RunTargets
    participant Status as statusRegistry
    participant Scheduler as Scheduler
    participant Method as Method
    participant HTTP as HTTPServer

    Note over Fetchit: Startup and scheduling
    Fetchit->>Fetchit: RunTargets
    Fetchit->>Fetchit: startStatusServer()
    alt [FETCHIT_STATUS_ADDR set]
        Fetchit->>HTTP: http.ListenAndServe(addr, mux)
    else [FETCHIT_STATUS_ADDR unset]
        Fetchit--xHTTP: no-op
    end

    loop for each method
        Fetchit->>Status: status.register(method, schedule)
        Fetchit->>Scheduler: Cron(schedule).Do(wrapper)
    end

    Note over Scheduler,Method: Scheduled execution
    Scheduler->>Method: wrapper(ctx, conn, skew)
    Method->>Status: status.recordRun(method)
    Method->>Method: Process(ctx, conn, skew)

    Note over User,HTTP: Status query
    User->>HTTP: GET /status
    HTTP->>Status: status.writeJSON(w)
    Status-->>HTTP: JSON with uptime and methods
    HTTP-->>User: 200 OK + status JSON

    Note over User,HTTP: Health check
    User->>HTTP: GET /healthz
    HTTP-->>User: 200 OK
Loading

File-Level Changes

Change Details Files
Wire an opt-in HTTP health/status server into the engine and method scheduler.
  • Introduce a status registry that tracks start time and per-method schedule/run metadata and exposes it via JSON
  • Start the status server from RunTargets only when FETCHIT_STATUS_ADDR is set, serving /healthz and /status
  • Wrap scheduled method executions to record runs before calling the underlying Method.Process
pkg/engine/status.go
pkg/engine/fetchit.go
Document the new health and status endpoints for containerized deployment.
  • Add README section describing FETCHIT_STATUS_ADDR configuration and example podman run command
  • Document /healthz and /status semantics and sample JSON response payload
README.md
Add a fast Go vet + unit-test GitHub Actions workflow as a CI gate.
  • Create a Unit tests workflow that runs on pushes and PRs to main
  • Configure Go with required build tags and install CGO-based system dependencies
  • Run go vet and go test against cmd, pkg, and tests/unit packages
.github/workflows/test.yml
Configure Dependabot to group dependency updates and keep GitHub Actions current, and auto-merge safe updates.
  • Add dependabot configuration to group minor/patch Go module updates and GitHub Actions updates into weekly PRs
  • Set labels and limits for dependency PRs to manage noise
  • Create an auto-merge workflow that enables auto-merge for Dependabot minor/patch PRs once required checks pass
.github/dependabot.yml
.github/workflows/dependabot-auto-merge.yml

Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

@sourcery-ai sourcery-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey - I've found 14 issues, and left some high level feedback:

  • In statusRegistry.writeJSON, if no methods have been registered yet r.started remains the zero time, so time.Since(started) will produce a large negative duration and odd uptime/startedAt values; consider guarding for started.IsZero() and returning a sensible default (e.g., uptime 0 and omitting/zeroing startedAt) in that case.
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- In `statusRegistry.writeJSON`, if no methods have been registered yet `r.started` remains the zero time, so `time.Since(started)` will produce a large negative duration and odd uptime/startedAt values; consider guarding for `started.IsZero()` and returning a sensible default (e.g., uptime 0 and omitting/zeroing `startedAt`) in that case.

## Individual Comments

### Comment 1
<location path="pkg/engine/status.go" line_range="69-78" />
<code_context>
+
+func (r *statusRegistry) writeJSON(w http.ResponseWriter) {
+	r.mu.Lock()
+	ms := make([]*methodStatus, 0, len(r.methods))
+	for _, s := range r.methods {
+		ms = append(ms, s)
+	}
+	started := r.started
+	r.mu.Unlock()
+
+	sort.Slice(ms, func(i, j int) bool {
+		if ms[i].Kind != ms[j].Kind {
+			return ms[i].Kind < ms[j].Kind
+		}
+		return ms[i].Name < ms[j].Name
+	})
+
+	resp := struct {
+		Status        string          `json:"status"`
+		StartedAt     time.Time       `json:"startedAt"`
</code_context>
<issue_to_address>
**issue (bug_risk):** Avoid data races between status mutation and JSON encoding

`writeJSON` collects `*methodStatus` under the mutex, then unlocks before sorting/encoding. While encoding, concurrent `register`/`recordRun` calls can mutate those structs, causing a data race. Either hold the lock through sorting and encoding, or copy each `methodStatus` to value types under the lock and encode the copies after unlocking.
</issue_to_address>

### Comment 2
<location path="pkg/engine/status.go" line_range="80-89" />
<code_context>
+		Methods       []*methodStatus `json:"methods"`
+	}{
+		Status:        "running",
+		StartedAt:     started,
+		UptimeSeconds: int64(time.Since(started).Seconds()),
+		Methods:       ms,
+	}
</code_context>
<issue_to_address>
**suggestion:** Handle the case where `started` is still zero in the status response

If `/status` is called before any method is registered, `r.started` will be the zero `time.Time`, so `time.Since(started)` will produce a huge duration (since year 1) and misleading `startedAt`/`uptimeSeconds` values. Please handle `started.IsZero()` explicitly (e.g., treat as "initializing" with zero/omitted uptime fields) until the first method is registered.

```suggestion
		return ms[i].Name < ms[j].Name
	})

	status := "running"
	var uptimeSeconds int64
	if started.IsZero() {
		status = "initializing"
		uptimeSeconds = 0
	} else {
		uptimeSeconds = int64(time.Since(started).Seconds())
	}

	resp := struct {
		Status        string          `json:"status"`
		StartedAt     time.Time       `json:"startedAt"`
		UptimeSeconds int64           `json:"uptimeSeconds"`
		Methods       []*methodStatus `json:"methods"`
	}{
		Status:        status,
		StartedAt:     started,
		UptimeSeconds: uptimeSeconds,
		Methods:       ms,
```
</issue_to_address>

### Comment 3
<location path="pkg/engine/status.go" line_range="104" />
<code_context>
+// startStatusServer launches the status/health HTTP server in a goroutine if
+// FETCHIT_STATUS_ADDR is set. It is a no-op otherwise.
+func startStatusServer() {
+	addr := os.Getenv(statusAddrEnv)
+	if addr == "" {
+		return
</code_context>
<issue_to_address>
**suggestion (review_instructions):** The status server may unintentionally listen on all interfaces (e.g. with `:8080`), which is risky on multi-tenant or shared hosts; consider defaulting to localhost unless an explicit host is provided.

Because `FETCHIT_STATUS_ADDR` is passed directly to `http.ListenAndServe`, a common configuration like `:8080` will bind to all interfaces. On shared hosts or container platforms, this can expose `/status` and `/healthz` beyond the local machine, which is unnecessary for a liveness/status endpoint and can be considered a security hardening issue.

One way to make this safer by default while keeping the current flexibility is to treat a host-less address as localhost-only, and require an explicit host if an operator truly wants 0.0.0.0:

```go
func startStatusServer() {
    addr := os.Getenv(statusAddrEnv)
    if addr == "" {
        return
    }

    // Hardening: default host-less addresses to localhost only.
    if strings.HasPrefix(addr, ":") {
        addr = "127.0.0.1" + addr
    }

    mux := http.NewServeMux()
    mux.HandleFunc("/healthz", func(w http.ResponseWriter, _ *http.Request) {
        w.WriteHeader(http.StatusOK)
        _, _ = w.Write([]byte("ok\n"))
    })
    mux.HandleFunc("/status", func(w http.ResponseWriter, _ *http.Request) {
        status.writeJSON(w)
    })
    go func() {
        logger.Infof("Status server listening on %s (/healthz, /status)", addr)
        if err := http.ListenAndServe(addr, mux); err != nil {
            logger.Errorf("status server error: %v", err)
        }
    }()
}
```

This preserves the current behavior when a full host:port is provided (e.g. `0.0.0.0:8080`), but avoids unintentionally exposing the status endpoints when users follow the `:8080` pattern from the README. If you prefer to keep the existing behavior, an alternative is to at least document that `:8080` will bind on all interfaces and recommend `127.0.0.1:8080` for local-only use.

<details>
<summary>Review instructions:</summary>

**Path patterns:** `*`

**Instructions:**
Include comments that identify potential security risks and provide a safer alternative.

</details>
</issue_to_address>

### Comment 4
<location path="pkg/engine/status.go" line_range="96" />
<code_context>
+	}
+
+	w.Header().Set("Content-Type", "application/json")
+	enc := json.NewEncoder(w)
+	enc.SetIndent("", "  ")
+	_ = enc.Encode(resp)
</code_context>
<issue_to_address>
**issue (review_instructions):** The error from enc.Encode(resp) is explicitly discarded, which can mask failures when writing the JSON response.

Currently the result of `enc.Encode(resp)` is ignored via `_ = enc.Encode(resp)`. This is similar in spirit to catching all exceptions and then continuing, because any failure to encode or write the response will be silently dropped.

To make the behavior clearer and safer, consider checking the returned error and either:
- logging it (at least at warning/error level), and/or
- returning an appropriate HTTP status code (e.g., 500) to the client.

That way, operational issues (e.g., a broken writer or encoding problem) are surfaced instead of being silently ignored.

<details>
<summary>Review instructions:</summary>

**Path patterns:** `*`

**Instructions:**
Include comments that identify potential bug risks, such as catching all exceptions, which can mask important errors.

</details>
</issue_to_address>

### Comment 5
<location path="pkg/engine/status.go" line_range="109" />
<code_context>
+		return
+	}
+	mux := http.NewServeMux()
+	mux.HandleFunc("/healthz", func(w http.ResponseWriter, _ *http.Request) {
+		w.WriteHeader(http.StatusOK)
+		_, _ = w.Write([]byte("ok\n"))
</code_context>
<issue_to_address>
**issue (review_instructions):** The writes in the /healthz handler ignore write errors, which can silently hide I/O problems.

In the `/healthz` handler, the call to `w.Write` uses `_, _ = w.Write([]byte("ok\n"))`, which discards any error from writing the response. While health checks often are simple, silently ignoring I/O errors can hide underlying problems (e.g., client disconnects or broken connections), similar to catching all exceptions and continuing.

Consider at least logging the error from `w.Write` so that unexpected failures are visible. Alternatively, if you want to keep the handler minimal, a brief comment explaining why the error is intentionally ignored would clarify that this is a conscious choice rather than an oversight.

<details>
<summary>Review instructions:</summary>

**Path patterns:** `*`

**Instructions:**
Include comments that identify potential bug risks, such as catching all exceptions, which can mask important errors.

</details>
</issue_to_address>

### Comment 6
<location path="pkg/engine/status.go" line_range="91" />
<code_context>
+	}{
+		Status:        "running",
+		StartedAt:     started,
+		UptimeSeconds: int64(time.Since(started).Seconds()),
+		Methods:       ms,
+	}
</code_context>
<issue_to_address>
**issue (review_instructions):** `UptimeSeconds` is computed from `started` even when `started` is the zero time, so hitting `/status` before any methods are registered will return a huge, incorrect uptime value.

Because `started` is only set in `register`, if `/status` is called before any methods are registered then `r.started` is still the zero `time.Time`. In that case `time.Since(started)` is the duration since year 0, so `uptimeSeconds` will be an enormous, meaningless value rather than something close to zero.

Example: right after startup with `FETCHIT_STATUS_ADDR` set but before any method registration, a user hitting `/status` would see `uptimeSeconds` on the order of billions of seconds. You could guard against this by initializing `started` to `time.Now()` in `startStatusServer`, or by having `writeJSON` set `started` to `time.Now()` when it finds it zero before computing `UptimeSeconds`.

<details>
<summary>Review instructions:</summary>

**Path patterns:** `*`

**Instructions:**
Include comments that identify potential bugs or risks in the code.

</details>
</issue_to_address>

### Comment 7
<location path="README.md" line_range="182" />
<code_context>
+    -p 8080:8080 \
+    -v fetchit-volume:/opt \
+    -v $HOME/.fetchit:/opt/mount \
+    -v /run/user/$(id -u)/podman//podman.sock:/run/podman/podman.sock \
+    --security-opt label=disable \
+    quay.io/fetchit/fetchit:latest
</code_context>
<issue_to_address>
**suggestion (review_instructions):** The double slash in `podman//podman.sock` looks like a typographical error in the command and may confuse readers.

The path `podman//podman.sock` includes a double slash, which is unusual and likely a typo rather than intentional.

To make the example clearer and avoid confusion, consider correcting it to use a single slash:

```sh
-    -v /run/user/$(id -u)/podman//podman.sock:/run/podman/podman.sock \
+    -v /run/user/$(id -u)/podman/podman.sock:/run/podman/podman.sock \
```

This keeps the path syntactically clean while preserving the intended mount point.

<details>
<summary>Review instructions:</summary>

**Path patterns:** `*`

**Instructions:**
Identify and point out typographical errors in code, especially in commands or scripts.

</details>
</issue_to_address>

### Comment 8
<location path="pkg/engine/status.go" line_range="98" />
<code_context>
+	w.Header().Set("Content-Type", "application/json")
+	enc := json.NewEncoder(w)
+	enc.SetIndent("", "  ")
+	_ = enc.Encode(resp)
+}
+
</code_context>
<issue_to_address>
**suggestion (review_instructions):** The JSON encoding error is being ignored here, which weakens error handling and makes the status endpoint less robust.

Right now the status handler ignores any error from `json.Encoder.Encode`, which means failures to serialize the response (e.g., due to a write error on the `http.ResponseWriter`) will be silently dropped. For a status/health endpoint, it’s usually better to surface a clear HTTP error so callers know something went wrong.

You could tighten the error handling like this:

```go
w.Header().Set("Content-Type", "application/json")
enc := json.NewEncoder(w)
enc.SetIndent("", "  ")
if err := enc.Encode(resp); err != nil {
    logger.Errorf("status: failed to encode response: %v", err)
    http.Error(w, "failed to encode status", http.StatusInternalServerError)
    return
}
```

This keeps the implementation simple while making the status endpoint more robust and observable when write/encode errors occur.

<details>
<summary>Review instructions:</summary>

**Path patterns:** `*`

**Instructions:**
- The comment should address potential issues such as error handling or code robustness.
- Include specific code examples or modifications to illustrate the suggestion.
- Ensure the comment provides a clear suggestion for improvement or enhancement.
- The comment should be relevant to the context of the code and provide actionable advice.

</details>
</issue_to_address>

### Comment 9
<location path="pkg/engine/status.go" line_range="98" />
<code_context>
+	w.Header().Set("Content-Type", "application/json")
+	enc := json.NewEncoder(w)
+	enc.SetIndent("", "  ")
+	_ = enc.Encode(resp)
+}
+
</code_context>
<issue_to_address>
**suggestion (review_instructions):** Silently discarding JSON encoding errors can hide real issues; consider handling specific error types instead of ignoring them.

Assigning and discarding the result of `enc.Encode` means any encoding or write error is completely ignored, which can make diagnosing real failures difficult. Handling specific error types explicitly would make this safer.

For example:

```go
import (
    "encoding/json"
    "errors"
    "net"
    "net/http"
)

// ... inside writeJSON
w.Header().Set("Content-Type", "application/json")
enc := json.NewEncoder(w)
enc.SetIndent("", "  ")
if err := enc.Encode(resp); err != nil {
    // Treat transient network/client issues differently from internal bugs.
    var netErr net.Error
    if errors.As(err, &netErr) {
        // likely client disconnect / timeout; optionally log at debug level
        logger.Debugf("client disconnected while writing status response: %v", netErr)
    } else {
        // unexpected serialization or IO error
        logger.Errorf("failed to encode status response: %v", err)
    }
}
```

This keeps the handler resilient but adds more specific error-type handling instead of ignoring all failures. It requires adding `errors` and `net` to the imports.

<details>
<summary>Review instructions:</summary>

**Path patterns:** `*`

**Instructions:**
The comment should suggest using more specific exception types for better error handling.

</details>
</issue_to_address>

### Comment 10
<location path="pkg/engine/status.go" line_range="103" />
<code_context>
+
+// startStatusServer launches the status/health HTTP server in a goroutine if
+// FETCHIT_STATUS_ADDR is set. It is a no-op otherwise.
+func startStatusServer() {
+	addr := os.Getenv(statusAddrEnv)
+	if addr == "" {
</code_context>
<issue_to_address>
**issue (review_instructions):** If the status server is enabled but no methods are ever registered, `started` remains zero so `/status` reports a `StartedAt` of year 0001 and a huge uptime, which is likely unintended behavior.

As written, `status.started` is only initialized in `register`, so if `FETCHIT_STATUS_ADDR` is set but no methods are scheduled or registered (for example in a minimal configuration, or if scheduling fails before registration), then `writeJSON` will compute:

```go
started := r.started              // time.Time{}
UptimeSeconds: int64(time.Since(started).Seconds()),
```

`time.Since(time.Time{})` is the duration since the zero time (year 0001), so `/status` will emit a `StartedAt` of `0001-01-01T00:00:00Z` and an extremely large `uptimeSeconds`. That’s misleading for operators and monitoring systems.

A concrete improvement is to ensure `started` is initialized when the status server starts, so the status endpoint always reports a sane start time even if no methods are registered:

```go
func startStatusServer() {
    addr := os.Getenv(statusAddrEnv)
    if addr == "" {
        return
    }

    // Ensure the registry has a reasonable start time even if no methods
    status.mu.Lock()
    if status.started.IsZero() {
        status.started = time.Now()
    }
    status.mu.Unlock()

    mux := http.NewServeMux()
    mux.HandleFunc("/healthz", func(w http.ResponseWriter, _ *http.Request) {
        w.WriteHeader(http.StatusOK)
        _, _ = w.Write([]byte("ok\n"))
    })
    mux.HandleFunc("/status", func(w http.ResponseWriter, _ *http.Request) {
        status.writeJSON(w)
    })
    go func() {
        logger.Infof("Status server listening on %s (/healthz, /status)", addr)
        if err := http.ListenAndServe(addr, mux); err != nil {
            logger.Errorf("status server error: %v", err)
        }
    }()
}
```

This keeps the behavior consistent across configurations and avoids confusing monitoring data, which is particularly helpful in heterogeneous or multi-environment deployments.

<details>
<summary>Review instructions:</summary>

**Path patterns:** `*`

**Instructions:**
Ensure the comment identifies a potential bug risk or issue in the code.

</details>
</issue_to_address>

### Comment 11
<location path="pkg/engine/status.go" line_range="67" />
<code_context>
+	s.LastRun = &now
+}
+
+func (r *statusRegistry) writeJSON(w http.ResponseWriter) {
+	r.mu.Lock()
+	ms := make([]*methodStatus, 0, len(r.methods))
</code_context>
<issue_to_address>
**issue (review_instructions):** If /status is hit before any method is registered, uptimeSeconds will be computed from a zero time, yielding a huge, misleading uptime value.

In `writeJSON`, `started` is copied from `r.started`, and then `uptimeSeconds` is calculated as:

```go
started := r.started
...
UptimeSeconds: int64(time.Since(started).Seconds()),
```

If the status server is started and `/status` is requested before any method has been registered via `register`, `r.started` will still be the zero time (`time.Time{}`). In that case, `time.Since(started)` will be the duration since year 0001, resulting in an enormous `uptimeSeconds` value that does not reflect the actual process uptime.

A simple fix is to ensure `started` is initialized to `time.Now()` if it is zero before computing uptime, for example:

```go
func (r *statusRegistry) writeJSON(w http.ResponseWriter) {
    r.mu.Lock()
    ms := make([]*methodStatus, 0, len(r.methods))
    for _, s := range r.methods {
        ms = append(ms, s)
    }
    started := r.started
    if started.IsZero() {
        started = time.Now()
    }
    r.mu.Unlock()

    ...

    resp := struct {
        // ...
        UptimeSeconds int64 `json:"uptimeSeconds"`
    }{
        Status:        "running",
        StartedAt:     started,
        UptimeSeconds: int64(time.Since(started).Seconds()),
        Methods:       ms,
    }
    ...
}
```

Alternatively, you could initialize `r.started` when the status server is started, but the key is to avoid using a zero `time.Time` to compute uptime to prevent misleading values.

<details>
<summary>Review instructions:</summary>

**Path patterns:** `*`

**Instructions:**
Include comments that identify potential bugs or runtime errors and provide a clear solution.

</details>
</issue_to_address>

### Comment 12
<location path="pkg/engine/fetchit.go" line_range="372" />
<code_context>
 		logger.Infof("Processing git target: %s Method: %s Name: %s", method.GetTarget().url, mt, method.GetName())
-		s.Cron(schedInfo.schedule).Tag(mt).Do(method.Process, ctx, f.conn, skew)
+		status.register(method, schedInfo.schedule)
+		m := method
+		s.Cron(schedInfo.schedule).Tag(mt).Do(func(ctx, conn context.Context, skew int) {
+			status.recordRun(m)
</code_context>
<issue_to_address>
**issue (review_instructions):** The anonymous function passed to `Do` has a parameter type mismatch with the arguments (`ctx` and `f.conn`), which will cause a compile-time error and incorrect types passed to `m.Process`.

The line below defines the function as `func(ctx, conn context.Context, skew int)`, but it's invoked with `ctx` (a `context.Context`) and `f.conn` (likely a `*grpc.ClientConn`). That won't compile because `conn` is declared as `context.Context` while the second argument is `*grpc.ClientConn`, and `m.Process` expects the connection argument, not another context.

You probably want the second parameter to be the connection, matching the original `method.Process` signature. For example:

```go
m := method
s.Cron(schedInfo.schedule).Tag(mt).Do(func(ctx context.Context, conn *grpc.ClientConn, skew int) {
    status.recordRun(m)
    m.Process(ctx, conn, skew)
}, ctx, f.conn, skew)
```

This preserves the previous behavior while adding the status tracking without introducing a type mismatch.

<details>
<summary>Review instructions:</summary>

**Path patterns:** `*`

**Instructions:**
Identify and highlight potential bugs or risks in the code, such as missing syntax elements that could lead to unintended behavior.

</details>
</issue_to_address>

### Comment 13
<location path="pkg/engine/status.go" line_range="67" />
<code_context>
+	s.LastRun = &now
+}
+
+func (r *statusRegistry) writeJSON(w http.ResponseWriter) {
+	r.mu.Lock()
+	ms := make([]*methodStatus, 0, len(r.methods))
</code_context>
<issue_to_address>
**issue (review_instructions):** If /status is hit before any methods are registered, `started` remains zero, so `StartedAt` will be the zero time and `UptimeSeconds` will be an enormous value, which is likely unintended behavior.

Currently `r.started` is only set in `register`, so if the status server is enabled but no methods have been scheduled (or `/status` is hit very early), `writeJSON` will compute `time.Since(started)` where `started` is the zero value. That yields a huge uptime and a `0001-01-01...` `StartedAt`, which is misleading for operators.

A concrete fix would be to ensure `started` is initialized when the server is started or before computing the response, for example:

```go
func (r *statusRegistry) writeJSON(w http.ResponseWriter) {
    r.mu.Lock()
    if r.started.IsZero() {
        r.started = time.Now()
    }
    ms := make([]*methodStatus, 0, len(r.methods))
    for _, s := range r.methods {
        ms = append(ms, s)
    }
    started := r.started
    r.mu.Unlock()
    // ... rest unchanged
}
```

Alternatively, you could special-case the zero-time case by setting `UptimeSeconds` to 0 and omitting/zeroing `StartedAt` until there is at least one registered method.

<details>
<summary>Review instructions:</summary>

**Path patterns:** `*`

**Instructions:**
Identify potential bugs or risks in the code and provide a clear explanation of the issue.

</details>
</issue_to_address>

### Comment 14
<location path="pkg/engine/status.go" line_range="118" />
<code_context>
+	})
+	go func() {
+		logger.Infof("Status server listening on %s (/healthz, /status)", addr)
+		if err := http.ListenAndServe(addr, mux); err != nil {
+			logger.Errorf("status server error: %v", err)
+		}
</code_context>
<issue_to_address>
**suggestion (review_instructions):** The HTTP status server uses http.ListenAndServe with default settings, which lacks timeouts and can be vulnerable to slow-client/DoS-style attacks; consider using an http.Server with sensible timeouts.

Right now the status server is created via `http.ListenAndServe(addr, mux)` with all default settings. The default `http.Server` has no read/write/idle timeouts, which can make it susceptible to resource exhaustion from slow clients (e.g. Slowloris-style behavior), even on an internal status port.

A more defensive pattern is to construct an `http.Server` with explicit timeouts, for example:

```go
srv := &http.Server{
    Addr:         addr,
    Handler:      mux,
    ReadTimeout:  5 * time.Second,
    WriteTimeout: 10 * time.Second,
    IdleTimeout:  60 * time.Second,
}

if err := srv.ListenAndServe(); err != nil {
    logger.Errorf("status server error: %v", err)
}
```

If you expect this endpoint to ever be exposed beyond localhost, these timeouts help reduce the risk of malicious or misbehaving clients tying up connections indefinitely.

<details>
<summary>Review instructions:</summary>

**Path patterns:** `*`

**Instructions:**
Include comments that suggest security improvements, such as input validation, especially when they prevent potential runtime failures or malicious use.

</details>
</issue_to_address>

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

Comment thread pkg/engine/status.go Outdated
Comment thread pkg/engine/status.go Outdated
Comment thread pkg/engine/status.go
Comment thread pkg/engine/status.go
Comment thread pkg/engine/status.go
return
}
mux := http.NewServeMux()
mux.HandleFunc("/healthz", func(w http.ResponseWriter, _ *http.Request) {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

issue (review_instructions): The writes in the /healthz handler ignore write errors, which can silently hide I/O problems.

In the /healthz handler, the call to w.Write uses _, _ = w.Write([]byte("ok\n")), which discards any error from writing the response. While health checks often are simple, silently ignoring I/O errors can hide underlying problems (e.g., client disconnects or broken connections), similar to catching all exceptions and continuing.

Consider at least logging the error from w.Write so that unexpected failures are visible. Alternatively, if you want to keep the handler minimal, a brief comment explaining why the error is intentionally ignored would clarify that this is a conscious choice rather than an oversight.

Review instructions:

Path patterns: *

Instructions:
Include comments that identify potential bug risks, such as catching all exceptions, which can mask important errors.

Comment thread pkg/engine/status.go
Comment thread pkg/engine/status.go
Comment thread pkg/engine/fetchit.go
logger.Infof("Processing git target: %s Method: %s Name: %s", method.GetTarget().url, mt, method.GetName())
s.Cron(schedInfo.schedule).Tag(mt).Do(method.Process, ctx, f.conn, skew)
status.register(method, schedInfo.schedule)
m := method

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

issue (review_instructions): The anonymous function passed to Do has a parameter type mismatch with the arguments (ctx and f.conn), which will cause a compile-time error and incorrect types passed to m.Process.

The line below defines the function as func(ctx, conn context.Context, skew int), but it's invoked with ctx (a context.Context) and f.conn (likely a *grpc.ClientConn). That won't compile because conn is declared as context.Context while the second argument is *grpc.ClientConn, and m.Process expects the connection argument, not another context.

You probably want the second parameter to be the connection, matching the original method.Process signature. For example:

m := method
s.Cron(schedInfo.schedule).Tag(mt).Do(func(ctx context.Context, conn *grpc.ClientConn, skew int) {
    status.recordRun(m)
    m.Process(ctx, conn, skew)
}, ctx, f.conn, skew)

This preserves the previous behavior while adding the status tracking without introducing a type mismatch.

Review instructions:

Path patterns: *

Instructions:
Identify and highlight potential bugs or risks in the code, such as missing syntax elements that could lead to unintended behavior.

Comment thread pkg/engine/status.go
Comment thread pkg/engine/status.go
})
go func() {
logger.Infof("Status server listening on %s (/healthz, /status)", addr)
if err := http.ListenAndServe(addr, mux); err != nil {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

suggestion (review_instructions): The HTTP status server uses http.ListenAndServe with default settings, which lacks timeouts and can be vulnerable to slow-client/DoS-style attacks; consider using an http.Server with sensible timeouts.

Right now the status server is created via http.ListenAndServe(addr, mux) with all default settings. The default http.Server has no read/write/idle timeouts, which can make it susceptible to resource exhaustion from slow clients (e.g. Slowloris-style behavior), even on an internal status port.

A more defensive pattern is to construct an http.Server with explicit timeouts, for example:

srv := &http.Server{
    Addr:         addr,
    Handler:      mux,
    ReadTimeout:  5 * time.Second,
    WriteTimeout: 10 * time.Second,
    IdleTimeout:  60 * time.Second,
}

if err := srv.ListenAndServe(); err != nil {
    logger.Errorf("status server error: %v", err)
}

If you expect this endpoint to ever be exposed beyond localhost, these timeouts help reduce the risk of malicious or misbehaving clients tying up connections indefinitely.

Review instructions:

Path patterns: *

Instructions:
Include comments that suggest security improvements, such as input validation, especially when they prevent potential runtime failures or malicious use.

@sallyom sallyom self-assigned this Jun 13, 2026
…point

- .github/dependabot.yml: group minor/patch Go bumps into one weekly PR,
  track github-actions; keep major bumps separate for review
- dependabot-auto-merge.yml: auto-merge minor/patch dependabot PRs once CI passes
- test.yml: fast go vet + unit test gate (build tags + CGO deps)
- status.go: opt-in HTTP server (FETCHIT_STATUS_ADDR) serving /healthz and
  /status (uptime, per-method schedule/run-count/last-run); wired into
  RunTargets without changing the Method interface

Signed-off-by: sallyom <somalley@redhat.com>
@sallyom sallyom force-pushed the improvements/ci-and-status branch from 86ce2df to a68c5c3 Compare June 14, 2026 13:36
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant