mirror of
https://github.com/taigrr/github-to-signal.git
synced 2026-04-02 03:09:09 -07:00
update truncation
This commit is contained in:
132
AGENTS.md
Normal file
132
AGENTS.md
Normal file
@@ -0,0 +1,132 @@
|
|||||||
|
# Agent Guide for github-to-signal
|
||||||
|
|
||||||
|
HTTP server that receives GitHub webhook events and forwards them as Signal messages via signal-cli.
|
||||||
|
|
||||||
|
## Commands
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Build
|
||||||
|
GOWORK=off go build -o github-to-signal .
|
||||||
|
|
||||||
|
# Test
|
||||||
|
GOWORK=off go test .
|
||||||
|
|
||||||
|
# Run (requires config.toml)
|
||||||
|
./github-to-signal
|
||||||
|
```
|
||||||
|
|
||||||
|
**Note**: This repo may not be in a parent `go.work` workspace. Use `GOWORK=off` to ensure commands work standalone.
|
||||||
|
|
||||||
|
## Project Structure
|
||||||
|
|
||||||
|
```
|
||||||
|
.
|
||||||
|
├── main.go # Entry point, HTTP server, event handlers
|
||||||
|
├── config.go # Configuration loading (TOML + env vars)
|
||||||
|
├── filter.go # Event filtering logic
|
||||||
|
├── filter_test.go # Tests for event filtering
|
||||||
|
├── format.go # Message formatting for each event type
|
||||||
|
├── config.example.toml
|
||||||
|
└── deploy/ # Systemd services and nginx config
|
||||||
|
```
|
||||||
|
|
||||||
|
All source files are in the root directory (single `main` package, no subdirectories).
|
||||||
|
|
||||||
|
## Configuration
|
||||||
|
|
||||||
|
Configuration via `config.toml` or environment variables with `GH2SIG_` prefix:
|
||||||
|
|
||||||
|
| Config Key | Env Variable | Description |
|
||||||
|
|------------|--------------|-------------|
|
||||||
|
| `webhook_secret` | `GH2SIG_WEBHOOK_SECRET` | GitHub webhook secret |
|
||||||
|
| `listen_addr` | `GH2SIG_LISTEN_ADDR` | Server address (default `:9900`) |
|
||||||
|
| `signal_url` | `GH2SIG_SIGNAL_URL` | signal-cli JSON-RPC endpoint |
|
||||||
|
| `signal_account` | `GH2SIG_SIGNAL_ACCOUNT` | Phone number for signal-cli |
|
||||||
|
| `signal_recipient` | `GH2SIG_SIGNAL_RECIPIENT` | Recipient UUID for DMs |
|
||||||
|
| `signal_group_id` | `GH2SIG_SIGNAL_GROUP_ID` | Group ID (overrides recipient) |
|
||||||
|
| `events` | `GH2SIG_EVENTS` | Comma-separated event filter |
|
||||||
|
|
||||||
|
Configuration is loaded using [jety](https://github.com/taigrr/jety) library.
|
||||||
|
|
||||||
|
## Code Patterns
|
||||||
|
|
||||||
|
### Event Handlers
|
||||||
|
|
||||||
|
Each GitHub event type has:
|
||||||
|
1. A handler method on `notifier` struct in `main.go`
|
||||||
|
2. A `format*` function in `format.go` that returns the Signal message
|
||||||
|
|
||||||
|
Handler pattern:
|
||||||
|
```go
|
||||||
|
func (n *notifier) onEventName(ctx context.Context, _ string, _ string, event *github.EventType) error {
|
||||||
|
if !n.filter.Allowed("event_name", event.GetAction()) {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
n.send(ctx, formatEventName(event))
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Event Filtering
|
||||||
|
|
||||||
|
`EventFilter` in `filter.go` supports:
|
||||||
|
- Empty filter = allow all events
|
||||||
|
- `"event"` = all actions of that event type
|
||||||
|
- `"event:action"` = specific action only
|
||||||
|
|
||||||
|
Two-level check:
|
||||||
|
1. `EventEnabled(event)` — used at registration time to skip registering handlers
|
||||||
|
2. `Allowed(event, action)` — checked at runtime for action-level filtering
|
||||||
|
|
||||||
|
### Message Formatting
|
||||||
|
|
||||||
|
All `format*` functions in `format.go`:
|
||||||
|
- Return a string (empty string = no message sent)
|
||||||
|
- Use `[repo] user action ...` prefix format
|
||||||
|
- Include relevant URLs
|
||||||
|
- Truncate bodies with `truncate()` helper
|
||||||
|
|
||||||
|
### Dependencies
|
||||||
|
|
||||||
|
- `cbrgm/githubevents` — GitHub webhook event parsing and routing
|
||||||
|
- `google/go-github` — GitHub API types
|
||||||
|
- `taigrr/signalcli` — signal-cli JSON-RPC client
|
||||||
|
- `taigrr/jety` — Configuration (TOML/JSON/YAML/env)
|
||||||
|
|
||||||
|
## Adding New Event Types
|
||||||
|
|
||||||
|
1. Add handler method in `main.go` following existing pattern
|
||||||
|
2. Register handler in `main()` with `EventEnabled` check
|
||||||
|
3. Add `format*` function in `format.go`
|
||||||
|
4. Add event name to filter docs in `config.example.toml`
|
||||||
|
|
||||||
|
## Testing
|
||||||
|
|
||||||
|
Only `filter_test.go` exists — table-driven tests for `EventFilter`.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
GOWORK=off go test -v .
|
||||||
|
```
|
||||||
|
|
||||||
|
## HTTP Endpoints
|
||||||
|
|
||||||
|
| Path | Method | Description |
|
||||||
|
|------|--------|-------------|
|
||||||
|
| `/webhook` | POST | GitHub webhook receiver |
|
||||||
|
| `/health` | GET | Health check (returns `ok`) |
|
||||||
|
|
||||||
|
## Deployment
|
||||||
|
|
||||||
|
Systemd services in `deploy/`:
|
||||||
|
- `signal-cli-bot.service` — runs signal-cli daemon
|
||||||
|
- `github-to-signal.service` — runs this server (depends on signal-cli)
|
||||||
|
- `github-to-signal.nginx.conf` — nginx reverse proxy config
|
||||||
|
|
||||||
|
The server expects signal-cli to be running on `127.0.0.1:8081`.
|
||||||
|
|
||||||
|
## Gotchas
|
||||||
|
|
||||||
|
- **GOWORK**: May need `GOWORK=off` if a parent `go.work` exists
|
||||||
|
- **signal-cli port**: Default in code is `8080`, but deployment uses `8081` to avoid conflicts
|
||||||
|
- **Workflow runs**: Only notifies on `completed` action, ignores `requested`/`in_progress`
|
||||||
|
- **Empty message**: Returning `""` from a formatter skips sending (used by workflow_run filter)
|
||||||
10
format.go
10
format.go
@@ -44,7 +44,7 @@ func formatIssue(event *github.IssuesEvent) string {
|
|||||||
repo, sender, action, issue.GetNumber(), issue.GetTitle(), issue.GetHTMLURL())
|
repo, sender, action, issue.GetNumber(), issue.GetTitle(), issue.GetHTMLURL())
|
||||||
|
|
||||||
if action == "opened" && issue.GetBody() != "" {
|
if action == "opened" && issue.GetBody() != "" {
|
||||||
body := truncate(issue.GetBody(), 200)
|
body := truncate(issue.GetBody(), 2000)
|
||||||
msg += "\n\n" + body
|
msg += "\n\n" + body
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -58,7 +58,7 @@ func formatIssueComment(event *github.IssueCommentEvent) string {
|
|||||||
issue := event.GetIssue()
|
issue := event.GetIssue()
|
||||||
comment := event.GetComment()
|
comment := event.GetComment()
|
||||||
|
|
||||||
body := truncate(comment.GetBody(), 300)
|
body := truncate(comment.GetBody(), 2000)
|
||||||
|
|
||||||
return fmt.Sprintf("[%s] %s commented on #%d (%s):\n%s\n%s",
|
return fmt.Sprintf("[%s] %s commented on #%d (%s):\n%s\n%s",
|
||||||
repo, sender, issue.GetNumber(), issue.GetTitle(), body, comment.GetHTMLURL())
|
repo, sender, issue.GetNumber(), issue.GetTitle(), body, comment.GetHTMLURL())
|
||||||
@@ -75,7 +75,7 @@ func formatPR(event *github.PullRequestEvent) string {
|
|||||||
repo, sender, action, pr.GetNumber(), pr.GetTitle(), pr.GetHTMLURL())
|
repo, sender, action, pr.GetNumber(), pr.GetTitle(), pr.GetHTMLURL())
|
||||||
|
|
||||||
if action == "opened" && pr.GetBody() != "" {
|
if action == "opened" && pr.GetBody() != "" {
|
||||||
body := truncate(pr.GetBody(), 200)
|
body := truncate(pr.GetBody(), 2000)
|
||||||
msg += "\n\n" + body
|
msg += "\n\n" + body
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -90,7 +90,7 @@ func formatPRReview(event *github.PullRequestReviewEvent) string {
|
|||||||
review := event.GetReview()
|
review := event.GetReview()
|
||||||
|
|
||||||
state := review.GetState()
|
state := review.GetState()
|
||||||
body := truncate(review.GetBody(), 200)
|
body := truncate(review.GetBody(), 2000)
|
||||||
|
|
||||||
msg := fmt.Sprintf("[%s] %s %s PR #%d: %s\n%s",
|
msg := fmt.Sprintf("[%s] %s %s PR #%d: %s\n%s",
|
||||||
repo, sender, state, pr.GetNumber(), pr.GetTitle(), review.GetHTMLURL())
|
repo, sender, state, pr.GetNumber(), pr.GetTitle(), review.GetHTMLURL())
|
||||||
@@ -109,7 +109,7 @@ func formatPRReviewComment(event *github.PullRequestReviewCommentEvent) string {
|
|||||||
pr := event.GetPullRequest()
|
pr := event.GetPullRequest()
|
||||||
comment := event.GetComment()
|
comment := event.GetComment()
|
||||||
|
|
||||||
body := truncate(comment.GetBody(), 300)
|
body := truncate(comment.GetBody(), 2000)
|
||||||
|
|
||||||
return fmt.Sprintf("[%s] %s commented on PR #%d (%s):\n%s\n%s",
|
return fmt.Sprintf("[%s] %s commented on PR #%d (%s):\n%s\n%s",
|
||||||
repo, sender, pr.GetNumber(), pr.GetTitle(), body, comment.GetHTMLURL())
|
repo, sender, pr.GetNumber(), pr.GetTitle(), body, comment.GetHTMLURL())
|
||||||
|
|||||||
Reference in New Issue
Block a user