GitHub Actions schedule events (cron triggers) have a known issue where delays of over an hour can occur. ghacron is a Go service that reads annotations from workflow files and fires workflow_dispatch events on time.
- Add annotations like
# ghacron: "0 8 * * *"to your workflow files - The service scans repositories every 5 minutes and detects annotations
- Fires
workflow_dispatchaccording to the cron expression - State is persisted via GitHub Actions Variables (no PVC required)
on:
# ghacron: "0 8 * * *"
workflow_dispatch:workflow_dispatch:must be included underon:- Multiple annotations per file are supported
- Cron expressions use the standard 5-field format (minute hour day month weekday)
- Per-workflow timezone override via
CRON_TZ=orTZ=prefix:
on:
# ghacron: "CRON_TZ=Asia/Tokyo 0 8 * * *"
workflow_dispatch:When specified, the prefix overrides the global GHACRON_TIMEZONE setting for that job. The value must be a valid IANA timezone name.
- Go 1.25 or later
- GitHub App (App ID + Private Key)
- Required permissions:
contents: read,actions: write,variables: write,metadata: read
- Required permissions:
./ghacron [options]| Flag | Description |
|---|---|
-version |
Show version and exit |
# Binary
GHACRON_APP_ID=123456 GHACRON_APP_PRIVATE_KEY="$(cat key.pem)" ./ghacron
# Docker
docker run \
-e GHACRON_APP_ID=123456 \
-e GHACRON_APP_PRIVATE_KEY="$(cat key.pem)" \
ghcr.io/korosuke613/ghacronAll configuration is done via GHACRON_* environment variables.
| Environment Variable | Type | Default | Required | Description |
|---|---|---|---|---|
GHACRON_APP_ID |
int64 | — | Yes | GitHub App ID |
GHACRON_APP_PRIVATE_KEY |
string | — | Yes* | GitHub App Private Key (PEM) |
GHACRON_APP_PRIVATE_KEY_PATH |
string | — | Yes* | Private Key file path |
GHACRON_RECONCILE_INTERVAL_MINUTES |
int | 5 |
No | Reconcile loop interval in minutes |
GHACRON_RECONCILE_DUPLICATE_GUARD_SECONDS |
int | 60 |
No | Duplicate dispatch guard in seconds |
GHACRON_DRY_RUN |
bool | false |
No | Dry-run mode |
GHACRON_TIMEZONE |
string | UTC |
No | IANA timezone for cron schedule evaluation |
GHACRON_LOG_LEVEL |
string | info |
No | Log level (debug/info/warn/error) |
GHACRON_LOG_FORMAT |
string | json |
No | Log format (json/text) |
GHACRON_WEBAPI_ENABLED |
bool | true |
No | Enable/disable web API server |
GHACRON_WEBAPI_HOST |
string | 0.0.0.0 |
No | Web API listen host |
GHACRON_WEBAPI_PORT |
int | 8080 |
No | Web API listen port |
*Either GHACRON_APP_PRIVATE_KEY or GHACRON_APP_PRIVATE_KEY_PATH is required. When both are set, GHACRON_APP_PRIVATE_KEY takes priority.
The web API server is enabled by default on port 8080. All responses are JSON.
Health check for liveness probes.
{"status": "ok"}Service status including uptime and reconciliation state.
{
"uptime_seconds": 3600.5,
"registered_jobs": 3,
"last_reconcile": "2026-02-24T09:00:00Z"
}Registered cron jobs and annotations that failed validation.
{
"registered": [
{
"owner": "myorg",
"repo": "myrepo",
"workflow_file": "ci.yml",
"cron_expr": "0 8 * * *",
"next_run": "2026-02-25T08:00:00Z"
}
],
"skipped": [
{
"owner": "myorg",
"repo": "myrepo",
"workflow_file": "deploy.yml",
"cron_expr": "CRON_TZ=Asis/Tokyo 0 8 * * *",
"reason": "provided bad location Asis/Tokyo: unknown time zone Asis/Tokyo"
}
]
}Public configuration (credentials are not exposed).
{
"app_id": 123456,
"reconcile_interval_minutes": 5,
"reconcile_duplicate_guard_seconds": 60,
"dry_run": false,
"timezone": "UTC",
"log_level": "info",
"log_format": "json",
"webapi_enabled": true,
"webapi_host": "0.0.0.0",
"webapi_port": 8080
}# Build
docker build -t ghacron .
# Run
docker run \
-e GHACRON_APP_ID=123456 \
-e GHACRON_APP_PRIVATE_KEY="$(cat key.pem)" \
ghacron
# Run with all options
docker run \
-e GHACRON_APP_ID=123456 \
-e GHACRON_APP_PRIVATE_KEY="$(cat key.pem)" \
-e GHACRON_TIMEZONE=Asia/Tokyo \
-e GHACRON_DRY_RUN=true \
ghacroncontainers:
- name: ghacron
image: ghcr.io/korosuke613/ghacron:latest
env:
- name: GHACRON_APP_ID
value: "123456"
- name: GHACRON_APP_PRIVATE_KEY
valueFrom:
secretKeyRef:
name: ghacron-secrets
key: private-key
- name: GHACRON_TIMEZONE
value: "Asia/Tokyo"# Build
go build -ldflags="-s -w -X main.version=$(git describe --tags --always)" -o ghacron main.go
# Run (dry-run)
GHACRON_APP_ID=123456 GHACRON_APP_PRIVATE_KEY="$(cat key.pem)" GHACRON_DRY_RUN=true ./ghacron
# Test
go test ./...Releases are triggered exclusively by GitHub Release events — pushing to main does not publish any artifacts.
- Create a GitHub Release (manually or via
/generate-releaseskill in Claude Code) - The
release.ymlworkflow automatically:- Builds Go binaries for linux/amd64 and linux/arm64 via GoReleaser
- Builds and pushes multi-arch Docker images to
ghcr.io/korosuke613/ghacron
Tagging rules:
| Release type | Example tag | Docker tags | latest |
|---|---|---|---|
| Stable | v1.0.0 |
1.0.0, 1.0, latest |
Yes |
| Release candidate | v1.0.0-rc.1 |
1.0.0-rc.1 |
No |
ghacron/
├── main.go # Entry point
├── config/ # Configuration management
├── github/ # GitHub App authentication & API client
├── scanner/ # Workflow scanning & annotation parsing
├── scheduler/ # Cron job management & reconciliation
├── api/ # Health/status API
├── Dockerfile
└── README.md
- cronium — A prior implementation with a similar approach
MIT License