Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
2 changes: 0 additions & 2 deletions pkg/cli/audit.go
Original file line number Diff line number Diff line change
Expand Up @@ -286,8 +286,6 @@ func runAuditMulti(ctx context.Context, args []string, repoFlag, outputDir strin
// isPermissionErrorStr checks if a string contains any known permission/authentication error marker.
// This is the canonical union of all auth-error substrings used across the codebase; update here
// rather than adding new inline strings.Contains checks in callers.
//
//nolint:errstringmatch // gh auth and gh api permission failures are intentionally classified from gh CLI text here.
func isPermissionErrorStr(s string) bool {
return strings.Contains(s, "authentication required") ||
strings.Contains(s, "exit status 4") ||
Expand Down
14 changes: 10 additions & 4 deletions pkg/parser/schedule_parser.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,12 @@ import (

var scheduleLog = logger.New("parser:schedule_parser")

// ErrUnsupportedSyntax is returned by ParseSchedule when the input uses a
// recognised schedule syntax that is explicitly not supported (e.g. "daily at
// 2pm"). Callers that need to distinguish this from a completely unrecognised
// input should test with errors.Is.
var ErrUnsupportedSyntax = errors.New("unsupported schedule syntax")

// durationPattern matches short duration format: number followed by unit (pre-compiled for performance)
var durationPattern = regexp.MustCompile(`^(\d+)([hdwm]|mo)$`)

Expand Down Expand Up @@ -354,7 +360,7 @@ func (p *ScheduleParser) parseDailyBase(hasWeekdaysSuffix bool) (string, error)
case "around":
return p.parseDailyAround(hasWeekdaysSuffix)
default:
return "", errors.New("'daily at <time>' syntax is not supported. Use fuzzy schedules like 'daily' (scattered), 'daily around <time>', or 'daily between <start> and <end>' for load distribution. For fixed times, use standard cron syntax (e.g., '0 14 * * *')")
return "", fmt.Errorf("'daily at <time>' syntax is not supported. Use fuzzy schedules like 'daily' (scattered), 'daily around <time>', or 'daily between <start> and <end>' for load distribution. For fixed times, use standard cron syntax (e.g., '0 14 * * *'): %w", ErrUnsupportedSyntax)
}
}

Expand Down Expand Up @@ -439,7 +445,7 @@ func (p *ScheduleParser) parseWeeklyBase() (string, error) {
return fmt.Sprintf("FUZZY:WEEKLY:%s * * *", weekday), nil
}
if p.tokens[3] != "around" {
return "", fmt.Errorf("'weekly on <weekday> at <time>' syntax is not supported. Use fuzzy schedules like 'weekly on %s' (scattered), 'weekly on %s around <time>', or standard cron syntax (e.g., '30 6 * * %s')", weekdayStr, weekdayStr, weekday)
return "", fmt.Errorf("'weekly on <weekday> at <time>' syntax is not supported. Use fuzzy schedules like 'weekly on %s' (scattered), 'weekly on %s around <time>', or standard cron syntax (e.g., '30 6 * * %s'): %w", weekdayStr, weekdayStr, weekday, ErrUnsupportedSyntax)
}

timeStr, err := p.extractTime(4)
Expand Down Expand Up @@ -469,9 +475,9 @@ func (p *ScheduleParser) parseMonthlyBase() (string, error) {
return "", fmt.Errorf("invalid day of month '%s', must be 1-31", day)
}
if len(p.tokens) > 3 {
return "", fmt.Errorf("'monthly on <day> at <time>' syntax is not supported. Use standard cron syntax for monthly schedules (e.g., '0 9 %s * *' for the %sth at 9am)", day, day)
return "", fmt.Errorf("'monthly on <day> at <time>' syntax is not supported. Use standard cron syntax for monthly schedules (e.g., '0 9 %s * *' for the %sth at 9am): %w", day, day, ErrUnsupportedSyntax)
}
return "", fmt.Errorf("'monthly on <day>' syntax is not supported. Use standard cron syntax for monthly schedules (e.g., '0 0 %s * *' for the %sth at midnight)", day, day)
return "", fmt.Errorf("'monthly on <day>' syntax is not supported. Use standard cron syntax for monthly schedules (e.g., '0 0 %s * *' for the %sth at midnight): %w", day, day, ErrUnsupportedSyntax)
}

// extractTime extracts the time specification from tokens starting at startPos
Expand Down
36 changes: 36 additions & 0 deletions pkg/parser/schedule_parser_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1095,3 +1095,39 @@ func TestParseSchedule(t *testing.T) {
})
}
}

// TestErrUnsupportedSyntaxIs verifies that all four "explicitly unsupported
// syntax" branches wrap ErrUnsupportedSyntax so that errors.Is works, and
// that an ordinary parse error (unrecognised keyword) does not.
func TestErrUnsupportedSyntaxIs(t *testing.T) {
tests := []struct {
name string
input string
wantUnsupported bool
}{
// daily at <time> — first unsupported-syntax branch
{name: "daily at time", input: "daily at 2pm", wantUnsupported: true},
// weekly on <weekday> at <time> — second unsupported-syntax branch
{name: "weekly on weekday at time", input: "weekly on friday at 5pm", wantUnsupported: true},
// monthly on <day> — third unsupported-syntax branch
{name: "monthly on day", input: "monthly on 15", wantUnsupported: true},
// monthly on <day> at <time> — fourth unsupported-syntax branch
{name: "monthly on day at time", input: "monthly on 15 at 9am", wantUnsupported: true},
// ordinary parse error: unrecognised schedule keyword
{name: "ordinary parse error (yearly)", input: "yearly", wantUnsupported: false},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
_, _, err := ParseSchedule(tt.input)
require.Error(t, err)
if tt.wantUnsupported {
assert.ErrorIs(t, err, ErrUnsupportedSyntax,
"expected errors.Is(err, ErrUnsupportedSyntax) for input %q; got: %v", tt.input, err)
} else {
assert.NotErrorIs(t, err, ErrUnsupportedSyntax,
"expected errors.Is(err, ErrUnsupportedSyntax)==false for input %q; got: %v", tt.input, err)
}
})
}
}
3 changes: 1 addition & 2 deletions pkg/workflow/schedule_preprocessing.go
Original file line number Diff line number Diff line change
Expand Up @@ -190,8 +190,7 @@ func (c *Compiler) preprocessScheduleFields(frontmatter map[string]any, markdown
if err != nil {
// Check if this is an explicit rejection of unsupported syntax
// vs. just not being a valid schedule at all
//nolint:errstringmatch // gronx currently exposes unsupported cron syntax only as plain error text.
if strings.Contains(err.Error(), "syntax is not supported") {
if errors.Is(err, parser.ErrUnsupportedSyntax) {
// This is an explicit rejection - return the error
return err
}
Expand Down
Loading