Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
7a309aa
Add full Cronitor API support via 'cronitor api' command
claude Jan 28, 2026
52574c8
API - Refactor to cleaner flag-based pattern
claude Jan 28, 2026
2bf032d
API - Remove incidents and components (not separate resources)
claude Jan 28, 2026
d4a40c1
Redesign API commands as top-level resource commands with subcommands
claude Jan 28, 2026
1251ce3
Add integration tests, shared test utilities, and API docs links
aflanagan Feb 1, 2026
72120ba
Add Go test step to CI workflow
aflanagan Feb 1, 2026
c8a2b4f
Fix update commands and list table parsing for production API compati…
aflanagan Feb 1, 2026
25184d7
README - Document API resource commands
claude Feb 4, 2026
15ad30f
README - Update API docs to reflect enhanced commands
claude Feb 4, 2026
9b9b6f6
Remove --all flag from issues, notifications, environments, and statu…
claude Feb 5, 2026
803b30d
Remove --all flag from monitors and clean up integration tests
claude Feb 5, 2026
92d4f95
Fix three failing Windows test issues
claude Feb 3, 2026
dee0b9b
Revert TimeTrigger description change, remove incorrect test case
claude Feb 4, 2026
4012301
Add standalone TimeTrigger test asserting no RRULE and no description
claude Feb 4, 2026
05a8bff
Add monitor/group export commands, remove --all flag everywhere
claude Feb 5, 2026
8757441
Remove group export command (monitor export --group covers this)
claude Feb 5, 2026
f1a7f09
Skip API integration tests unconditionally in CI
claude Feb 5, 2026
d6580d8
Pass API key explicitly in integration tests
claude Feb 5, 2026
7759069
Match integration test pattern with other bats tests
claude Feb 5, 2026
2e77a12
Skip API integration tests unconditionally in CI
claude Feb 5, 2026
8906692
Add separate documentation URLs for humans and agents
claude Feb 5, 2026
813706a
Skip status integration test that has platform-specific output
claude Feb 5, 2026
5246723
Add panic recovery for Windows Task Scheduler connection
claude Feb 5, 2026
e3eebce
Fix flaky duration test on Windows
claude Feb 5, 2026
4b99b62
Revert panic recovery for Windows Task Scheduler
claude Feb 5, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions .github/workflows/tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,10 @@ jobs:
shell: bash
run: go build -o cronitor main.go

- name: Run Go tests
shell: bash
run: go test ./...

- name: Run tests
working-directory: tests
shell: bash
Expand Down Expand Up @@ -93,6 +97,9 @@ jobs:
- name: Build binary
run: go build -o cronitor main.go

- name: Run Go tests
run: go test ./...

- name: Run tests
working-directory: tests
env:
Expand Down
449 changes: 449 additions & 0 deletions Plan.md

Large diffs are not rendered by default.

147 changes: 114 additions & 33 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,41 +18,122 @@ For the latest installation details, see https://cronitor.io/docs/using-cronitor
## Usage

```
CronitorCLI version 31.4

Command line tools for Cronitor.io. See https://cronitor.io/docs/using-cronitor-cli for details.

Usage:
cronitor [command]

Available Commands:
completion generate the autocompletion script for the specified shell
configure Save configuration variables to the config file
dash Start the web dashboard
exec Execute a command with monitoring
help Help about any command
list Search for and list all cron jobs
ping Send a telemetry ping to Cronitor
shell Run commands from a cron-like shell
signup Sign up for a Cronitor account
status View monitor status
sync Add monitoring to new cron jobs and sync changes to existing jobs
update Update to the latest version

Flags:
-k, --api-key string Cronitor API Key
-c, --config string Config file
--env string Cronitor Environment
-h, --help help for cronitor
-n, --hostname string A unique identifier for this host (default: system hostname)
-l, --log string Write debug logs to supplied file
-p, --ping-api-key string Ping API Key
-u, --users string Comma-separated list of users whose crontabs to include (default: current user only)
-v, --verbose Verbose output

Use "cronitor [command] --help" for more information about a command.
cronitor [command]
```

### Cron Management
| Command | Description |
|---------|-------------|
| `cronitor sync` | Sync cron jobs to Cronitor |
| `cronitor exec <key> <cmd>` | Run a command with monitoring |
| `cronitor list` | List all cron jobs |
| `cronitor status` | View monitor status |
| `cronitor dash` | Start the web dashboard |

### API Resources

Manage Cronitor resources directly from the command line.

#### Monitors

```bash
cronitor monitor list # List all monitors
cronitor monitor list --type job --state failing # Filter by type and state
cronitor monitor list --tag critical --env production # Filter by tag and environment
cronitor monitor export -o monitors.yaml # Export full YAML config
cronitor monitor export --type job # Export only jobs
cronitor monitor search "backup" # Search monitors
cronitor monitor get <key> # Get monitor details
cronitor monitor get <key> --with-events # Include latest events
cronitor monitor create -d '{"key":"my-job","type":"job"}'
cronitor monitor create --file monitors.yaml # Create from YAML
cronitor monitor update <key> -d '{"name":"New Name"}'
cronitor monitor delete <key> # Delete one
cronitor monitor delete key1 key2 key3 # Delete many
cronitor monitor clone <key> --name "Copy" # Clone a monitor
cronitor monitor pause <key> # Pause indefinitely
cronitor monitor pause <key> --hours 24 # Pause for 24 hours
cronitor monitor unpause <key>
```

#### Status Pages

```bash
cronitor statuspage list
cronitor statuspage list --with-status # Include current status
cronitor statuspage get <key> --with-components # Include components
cronitor statuspage create -d '{"name":"My Status Page","subdomain":"my-status"}'
cronitor statuspage update <key> -d '{"name":"Updated"}'
cronitor statuspage delete <key>

# Components (nested under statuspage)
cronitor statuspage component list --statuspage my-page
cronitor statuspage component create -d '{"statuspage":"my-page","monitor":"api-health"}'
cronitor statuspage component update <key> -d '{"name":"New Name"}'
cronitor statuspage component delete <key>
```

#### Issues

```bash
cronitor issue list # List all issues
cronitor issue list --state unresolved --severity outage # Filter
cronitor issue list --monitor my-job --time 24h # By monitor, time range
cronitor issue list --search "database" # Search issues
cronitor issue get <key>
cronitor issue create -d '{"name":"DB issues","severity":"outage"}'
cronitor issue update <key> -d '{"state":"investigating"}'
cronitor issue resolve <key> # Shorthand for resolving
cronitor issue delete <key>
cronitor issue bulk --action delete --issues KEY1,KEY2 # Bulk actions
```

#### Notifications

```bash
cronitor notification list
cronitor notification get <key>
cronitor notification create -d '{"name":"DevOps","notifications":{"emails":["team@co.com"]}}'
cronitor notification update <key> -d '{"name":"Updated"}'
cronitor notification delete <key>
```

#### Groups

```bash
cronitor group list
cronitor group list --with-status # Include group status
cronitor group get <key>
cronitor group create -d '{"name":"Production Jobs"}'
cronitor group update <key> -d '{"monitors":["job1","job2"]}'
cronitor group delete <key>
cronitor group pause <key> 24 # Pause all monitors for 24 hours
cronitor group resume <key> # Resume all monitors
```

#### Environments

```bash
cronitor environment list
cronitor environment get <key>
cronitor environment create -d '{"key":"staging","name":"Staging"}'
cronitor environment update <key> -d '{"name":"Updated"}'
cronitor environment delete <key>
```

**Aliases:** `cronitor env` → `environment`, `cronitor notifications` → `notification`

### Common Flags

| Flag | Description |
|------|-------------|
| `--format json\|table\|yaml` | Output format (default: `table` for list, `json` for get) |
| `-o, --output <file>` | Write output to a file |
| `--page <n>` | Page number for paginated results |
| `-d, --data <json>` | JSON data for create/update |
| `-f, --file <path>` | Read JSON or YAML from a file |
| `-k, --api-key <key>` | Cronitor API key |

## Crontab Guru Dashboard

The Cronitor CLI bundles the [Crontab Guru Dashboard](https://crontab.guru/dashboard.html), a self‑hosted web UI to manage your cron jobs, including a one‑click “run now” and "suspend", a local console for testing jobs, and a built in MCP server for configuring jobs and checking the health/status of existing ones.
Expand Down
9 changes: 9 additions & 0 deletions cmd/configure.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ type ConfigFile struct {
AllowedIPs string `json:"CRONITOR_ALLOWED_IPS"`
CorsAllowedOrigins string `json:"CRONITOR_CORS_ALLOWED_ORIGINS"`
Users string `json:"CRONITOR_USERS"`
ApiVersion string `json:"CRONITOR_API_VERSION,omitempty"`
MCPEnabled bool `json:"CRONITOR_MCP_ENABLED,omitempty"`
MCPInstances map[string]MCPInstanceConfig `json:"mcp_instances,omitempty"`
}
Expand Down Expand Up @@ -76,6 +77,7 @@ Example setting common exclude text for use with 'cronitor discover':
configData.AllowedIPs = viper.GetString(varAllowedIPs)
configData.CorsAllowedOrigins = viper.GetString("CRONITOR_CORS_ALLOWED_ORIGINS")
configData.Users = viper.GetString(varUsers)
configData.ApiVersion = viper.GetString(varApiVersion)
configData.MCPEnabled = viper.GetBool(varMCPEnabled)

// Load MCP instances if configured
Expand Down Expand Up @@ -169,6 +171,13 @@ Example setting common exclude text for use with 'cronitor discover':
fmt.Println(configData.Users)
}

fmt.Println("\nAPI Version:")
if configData.ApiVersion == "" {
fmt.Println("Not Set (API default)")
} else {
fmt.Println(configData.ApiVersion)
}

fmt.Println("\nMCP Enabled:")
fmt.Println(configData.MCPEnabled)

Expand Down
79 changes: 79 additions & 0 deletions cmd/discover.go
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
package cmd

import (
"encoding/json"
"errors"
"fmt"
"os"
"os/user"
"path/filepath"
"runtime"
"strings"

Expand All @@ -16,6 +18,7 @@ import (
"github.com/fatih/color"
"github.com/spf13/cobra"
"github.com/spf13/viper"
"gopkg.in/yaml.v3"
)

// getJobLabel returns the appropriate term for a job based on the platform
Expand Down Expand Up @@ -109,6 +112,7 @@ var notificationList string
var existingMonitors = ExistingMonitors{}
var processingMultipleCrontabs = false
var userAbortedSync = false // Set to true when user presses Ctrl+D to abort sync entirely
var syncFile string // Path to YAML/JSON file for bulk monitor import

// To deprecate this feature we are hijacking this flag that will trigger removal of auto-discover lines from existing user's crontabs.
var noAutoDiscover = true
Expand Down Expand Up @@ -173,6 +177,12 @@ Example where you perform a dry-run without any crontab modifications:
lipgloss.NewStyle().Italic(true).Render("cronitor configure --api-key <key>")), 1)
}

// Handle --file flag for bulk monitor import from YAML/JSON file
if syncFile != "" {
importMonitorsFromFile(syncFile)
return
}

var username string
if u, err := user.Current(); err == nil {
username = u.Username
Expand Down Expand Up @@ -275,6 +285,74 @@ func processDirectory(username, directory string) {
}
}

// importMonitorsFromFile imports monitors from a YAML or JSON file
// YAML files are sent with Content-Type: application/yaml
// JSON files are sent with Content-Type: application/json
func importMonitorsFromFile(filePath string) {
printSuccessText(fmt.Sprintf("Importing monitors from %s...", filePath), false)

// Read the file
data, err := os.ReadFile(filePath)
if err != nil {
fatal(fmt.Sprintf("Failed to read file: %s", err.Error()), 1)
}

// Determine content type based on file extension
ext := strings.ToLower(filepath.Ext(filePath))
var contentType string

switch ext {
case ".yaml", ".yml":
contentType = "application/yaml"
// Basic validation - try to parse YAML
var yamlData interface{}
if err := yaml.Unmarshal(data, &yamlData); err != nil {
fatal(fmt.Sprintf("Failed to parse YAML: %s", err.Error()), 1)
}
case ".json":
contentType = "application/json"
// Basic validation - try to parse JSON
var jsonData interface{}
if err := json.Unmarshal(data, &jsonData); err != nil {
fatal(fmt.Sprintf("Failed to parse JSON: %s", err.Error()), 1)
}
default:
fatal(fmt.Sprintf("Unsupported file format: %s (use .yaml, .yml, or .json)", ext), 1)
}

// Send to the API with the appropriate content type
printDoneText("Sending to Cronitor...", false)

response, err := getCronitorApi().PutRawMonitors(data, contentType)
if err != nil {
fatal(fmt.Sprintf("API error: %s", err.Error()), 1)
}

// Try to parse response to show results
// Response format may vary based on input format
var result struct {
Monitors []struct {
Key string `json:"key"`
Name string `json:"name"`
} `json:"monitors"`
}
if err := json.Unmarshal(response, &result); err == nil && len(result.Monitors) > 0 {
printDoneText(fmt.Sprintf("Successfully synced %d monitor(s)", len(result.Monitors)), false)
for _, m := range result.Monitors {
name := m.Name
if name == "" {
name = m.Key
}
printSuccessText(fmt.Sprintf(" • %s", name), false)
}
} else {
// For YAML responses or other formats, just show success
printDoneText("Monitors synced successfully", false)
}

printSuccessText("View your dashboard: https://cronitor.io/app/dashboard", false)
}

func processCrontab(crontab *lib.Crontab) bool {
defer printLn()

Expand Down Expand Up @@ -792,6 +870,7 @@ func init() {
discoverCmd.Flags().BoolVar(&noStdoutPassthru, "no-stdout", noStdoutPassthru, "Do not send cron job output to Cronitor when your job completes.")
discoverCmd.Flags().StringVar(&notificationList, "notification-list", notificationList, "Use the provided notification list when creating or updating monitors, or \"default\" list if omitted.")
discoverCmd.Flags().BoolVar(&isAutoDiscover, "auto", isAutoDiscover, "Do not use an interactive shell. Write updated crontab to stdout.")
discoverCmd.Flags().StringVar(&syncFile, "file", "", "Path to YAML or JSON file containing monitor definitions for bulk import")

discoverCmd.Flags().BoolVar(&isSilent, "silent", isSilent, "")
discoverCmd.Flags().MarkHidden("silent")
Expand Down
57 changes: 33 additions & 24 deletions cmd/discover_logic_windows.go
Original file line number Diff line number Diff line change
Expand Up @@ -155,30 +155,39 @@ func convertMonthDays(days taskmaster.DayOfMonth) string {
func convertWeekOfMonth(weeks taskmaster.Week, days taskmaster.DayOfWeek) string {
var dayList []string

// Convert days
dayStrings := make(map[taskmaster.DayOfWeek]string)
dayStrings[taskmaster.Monday] = "MO"
dayStrings[taskmaster.Tuesday] = "TU"
dayStrings[taskmaster.Wednesday] = "WE"
dayStrings[taskmaster.Thursday] = "TH"
dayStrings[taskmaster.Friday] = "FR"
dayStrings[taskmaster.Saturday] = "SA"
dayStrings[taskmaster.Sunday] = "SU"

// Convert weeks
weekNumbers := make(map[taskmaster.Week]string)
weekNumbers[taskmaster.First] = "1"
weekNumbers[taskmaster.Second] = "2"
weekNumbers[taskmaster.Third] = "3"
weekNumbers[taskmaster.Fourth] = "4"
weekNumbers[taskmaster.LastWeek] = "-1"

// Build combinations
for week, weekNum := range weekNumbers {
if weeks&week != 0 {
for day, dayStr := range dayStrings {
if days&day != 0 {
dayList = append(dayList, weekNum+dayStr)
// Use ordered slices instead of maps to ensure deterministic output
type weekEntry struct {
week taskmaster.Week
prefix string
}
weekOrder := []weekEntry{
{taskmaster.First, "1"},
{taskmaster.Second, "2"},
{taskmaster.Third, "3"},
{taskmaster.Fourth, "4"},
{taskmaster.LastWeek, "-1"},
}

type dayEntry struct {
day taskmaster.DayOfWeek
suffix string
}
dayOrder := []dayEntry{
{taskmaster.Monday, "MO"},
{taskmaster.Tuesday, "TU"},
{taskmaster.Wednesday, "WE"},
{taskmaster.Thursday, "TH"},
{taskmaster.Friday, "FR"},
{taskmaster.Saturday, "SA"},
{taskmaster.Sunday, "SU"},
}

// Build combinations in deterministic order
for _, w := range weekOrder {
if weeks&w.week != 0 {
for _, d := range dayOrder {
if days&d.day != 0 {
dayList = append(dayList, w.prefix+d.suffix)
}
}
}
Expand Down
Loading