ashita-planning/laravel-error-monitor-github

GitHub Issue adapter for ashita-planning/laravel-error-monitor.

Maintainers

Package info

github.com/ashita-planning/laravel-error-monitor-github

pkg:composer/ashita-planning/laravel-error-monitor-github

Transparency log

Statistics

Installs: 10

Dependents: 0

Suggesters: 0

Stars: 0

Open Issues: 0

v1.1.0 2026-08-05 10:26 UTC

This package is auto-updated.

Last update: 2026-08-05 10:26:18 UTC


README

ashita-planning/laravel-error-monitor-github files the failures ashita-planning/laravel-error-monitor finds as GitHub issues.

It contains no log parsing, no fingerprinting and no masking — those belong to the core. This package knows about GitHub, and the core knows nothing about GitHub.

ashita-planning/laravel-error-monitor           log analysis, aggregation, contracts
ashita-planning/laravel-error-monitor-xserver   XServer stored log retrieval
ashita-planning/laravel-error-monitor-github    GitHub Issue publishing   ← this one

Requirements

  • PHP 8.2 or newer
  • Laravel 10, 11, or 12
  • ashita-planning/laravel-error-monitor

Installation

composer require ashita-planning/laravel-error-monitor-github
php artisan vendor:publish --provider="Apkk\LaravelErrorMonitorGithub\GithubErrorMonitorServiceProvider" --tag=error-monitor-github-config
ERROR_MONITOR_GITHUB_ENABLED=true
ERROR_MONITOR_GITHUB_REPOSITORY=acme/shop
ERROR_MONITOR_GITHUB_TOKEN=github_pat_...

Check the wiring before relying on it — this makes no network request:

php artisan error-monitor:github-status

And once, to confirm the token and repository actually work:

php artisan error-monitor:github-status --check-connection

From then on the core's daily command publishes as part of its run:

php artisan error-monitor:run

Token permissions

A fine-grained personal access token needs exactly one repository permission:

Permission Access Why
Issues Read and write Search, create, comment, reopen, label

Nothing else. No repo scope, no code access, no workflow permission. If you are using a classic token, public_repo is enough for a public repository and repo is the narrowest option GitHub offers for a private one — which is one more reason to prefer a fine-grained token.

What happens, and when

Situation Action action
Failure not tracked anywhere Open an issue created
Already reported today, unchanged Nothing at all skipped
Open issue, new day (or the day got worse) Add a comment commented
Closed issue, failure returned Reopen, label, comment reopened
Could not be settled safely Nothing recorded failed

A day "gets worse" when its occurrence count or last occurrence moves, which changes the report hash and makes it worth saying again.

How duplicates are prevented

Two independent records, because either one alone has a blind spot.

The database link (error_monitor_issues, owned by the core) is checked first. It is cheap and usually right, but it can be behind: a previous run may have created an issue and then lost the response before recording anything.

HTML markers in the issue itself are the fallback, and the final authority:

<!-- error-monitor-fingerprint:{fingerprint} -->
<!-- error-monitor-environment:{environment} -->
<!-- error-monitor-provider:github -->
<!-- error-monitor-target:{owner/repository} -->

and on every daily comment:

<!-- error-monitor-event:{fingerprint}:{date}:{report_hash} -->

They are invisible to readers, survive edits to the visible text, and are matched exactly — a body containing the fingerprint as a substring is not a match.

The order is always:

database link → GitHub search → GitHub issue list → create

Search API vs. issue list

Both are used, for different jobs.

The Search API is fast and is tried first, but it is eventually consistent: an issue created seconds ago may not be indexed yet.

The repository issue list (state=all, newest first, bounded to three pages) reads the repository directly and sees an issue the moment it exists.

That distinction matters most after a lost write, where "not indexed yet" and "never created" would look identical through the search index — which is exactly the confusion that produces a duplicate. Recovery therefore never uses the search index at all.

Lost writes

A write whose response never arrived may well have succeeded, so it is never simply sent again:

Write On ConnectionException
Create issue Scan the issue list for the fingerprint marker (up to 3 looks, short waits between). Found → treat as created. Not found → retry the POST, within the attempt limit.
Add comment Re-read the comments and look for the daily event marker. Found → treat as commented. Not found → retry.
Reopen Re-read the issue state. Already open → treat as reopened. Still closed → retry the PATCH.
Add label Re-read the issue labels. Already present → done.

Concurrency is handled separately. GitHub's creation endpoint takes no idempotency key, so two workers that both look, both find nothing and both create would produce two issues — no amount of searching closes a gap that exists between the search and the write. Looking and creating therefore happen inside a distributed cache lock keyed on error-monitor-github:{repository}:{environment}:{fingerprint}. A worker that cannot take the lock does nothing and makes no request at all.

If two issues carry the same fingerprint marker, the publication fails rather than choosing one — picking either would quietly orphan the other.

Retrying

The rule is whether GitHub answered.

Reads (GET) are retried on 429, 5xx, a secondary-rate-limit 403, and connection failures.

Writes (POST, PATCH) are retried by the client only when GitHub actually answered — a 429 or a 5xx means the request was processed and refused. A connection failure is handed back to the caller instead, which checks the side effect first.

Response Behaviour
401 Fails immediately. A rejected token stays rejected.
403 (permission) Fails immediately.
403 (secondary rate limit) Waits at least 60 seconds, then retries. Detected from the message, since GitHub does not always send Retry-After or zero the remaining count.
403 with x-ratelimit-remaining: 0 Waits until x-ratelimit-reset.
404 Fails immediately — the repository or issue is not visible to this token.
422 Fails immediately. The same body would be rejected the same way.
429 Honours Retry-After, in seconds or as an HTTP-date.
500 / 502 / 503 / 504 Exponential backoff — 500ms, 1s, 2s… capped by retry.max_delay_ms.

retry.max_wait_ms (default 60s) is a hard ceiling on every wait, including Retry-After. Asked to wait longer, this package stops and reports a failure instead of sleeping: a scheduled run that appears hung is harder to reason about than one that failed, and the next run is minutes away.

An unparseable Retry-After falls back to the bounded backoff rather than becoming an unbounded wait.

The regression label

Reopening the issue and posting the recurrence comment are required for a publication to succeed. The regression label is supporting information: if a transient GitHub API failure prevents it from being added, the recurrence comment is still posted. The outcome is recorded in the publication metadata (regression_label_added) and as a warning in the log.

This is deliberate. Failing the whole publication over a label would leave the state worse rather than better: the next run would find an issue that is already open with the day's comment already posted, and would never return to the labelling path.

Configuration

Key Environment variable Default
enabled ERROR_MONITOR_GITHUB_ENABLED false
repository ERROR_MONITOR_GITHUB_REPOSITORY
token ERROR_MONITOR_GITHUB_TOKEN
api_url ERROR_MONITOR_GITHUB_API_URL https://api.github.com
labels.default ERROR_MONITOR_GITHUB_LABELS laravel-500,auto-detected,ai-fix
labels.regression ERROR_MONITOR_GITHUB_REGRESSION_LABEL regression
retry.attempts ERROR_MONITOR_GITHUB_RETRY_ATTEMPTS 3
retry.base_delay_ms ERROR_MONITOR_GITHUB_RETRY_BASE_DELAY_MS 500
retry.max_delay_ms ERROR_MONITOR_GITHUB_RETRY_MAX_DELAY_MS 5000
retry.max_wait_ms ERROR_MONITOR_GITHUB_RETRY_MAX_WAIT_MS 60000
timeouts.connect_seconds ERROR_MONITOR_GITHUB_CONNECT_TIMEOUT 10
timeouts.request_seconds ERROR_MONITOR_GITHUB_REQUEST_TIMEOUT 30
lock_seconds ERROR_MONITOR_GITHUB_LOCK_SECONDS 60
issue.title_prefix ERROR_MONITOR_GITHUB_TITLE_PREFIX [Laravel Error]
issue.include_context ERROR_MONITOR_GITHUB_INCLUDE_CONTEXT false
issue.include_metadata ERROR_MONITOR_GITHUB_INCLUDE_METADATA true

include_context is off by default. The context is masked, but an issue is read by more people than a database is, and none of them need the request detail to know what broke.

GitHub Enterprise Server

Point api_url at your instance's API root:

ERROR_MONITOR_GITHUB_API_URL=https://github.example.com/api/v3

Nothing else changes. The adapter uses only the REST endpoints that Enterprise Server provides.

One repository per environment

The link is unique on (provider, environment, fingerprint, target), so the same failure in production and staging gets its own issue, and the same failure filed at two repositories gets one per repository.

Do not point two applications at one repository unless you want their issues merged. Two applications sharing a repository and an environment name will share issues for any failure whose fingerprint matches — which is occasionally what you want, and usually is not. Give them different environment values, or different repositories.

Scheduling

The core's daily command does the publishing:

use Illuminate\Support\Facades\Schedule;

Schedule::command('error-monitor:run')
    ->dailyAt('05:00')
    ->onOneServer()
    ->withoutOverlapping();

Two things are worth knowing:

  • Use a shared cache store (Redis, Memcached, or the database driver) if more than one machine can run the schedule. The publication lock is a cache lock; a per-machine store cannot serialise across machines. Without a lock provider the package still publishes — it does not refuse to work — but the create-race protection is gone.
  • --skip-github suppresses publishing for a run without touching the analysis, which is useful when backfilling.

Testing

composer update
composer check   # Pint, PHPStan and PHPUnit

Every test runs against Http::fake(). Nothing in the suite reaches GitHub, and CI has no token.

Troubleshooting

Symptom Likely cause
Nothing is published, no errors enabled is false, or no token. Run error-monitor:github-status.
GitHub answered 404 The repository does not exist, or the token cannot see it. Check repository and the token's repository access.
GitHub refused the request (403) The token lacks Issues: read and write on that repository.
Several GitHub issues carry this fingerprint Two issues have the same fingerprint marker. Close or edit one; this is deliberately not resolved automatically.
Publication fails with a rate limit A Retry-After longer than retry.max_wait_ms. Raise it, or let the next run handle it.
Issues appear twice Check that the cache store provides locks across all machines running the schedule.

Current limitations

  • Issues are searched within a bounded window: the search index, then the three most recent pages of the issue list. A fingerprint whose issue is older than that and missing from the search index would not be found.
  • One repository per application per environment. Cross-repository routing — filing by source, or by domain — is not implemented.
  • Pull request linking (recordPullRequest on the core's repository) is not driven by this package yet.
  • Closing an issue when a failure stops occurring is not implemented; issues are reopened but never closed automatically.

License

MIT.