From 8e5289149cd4febcc2659968452dadbfe4b24c84 Mon Sep 17 00:00:00 2001 From: bircni Date: Sun, 11 Oct 2026 23:19:06 +0200 Subject: [PATCH] fix(deadline): reject day/month dates that read differently by locale --deadline 03/04/2026 was silently parsed as March 4. Numeric dates such as 03/04/2026 or 15.01.2028 are now refused with "the order of day and month is ambiguous; use YYYY-MM-DD", and the error for other invalid values no longer suggests clearing on issue create. Signed-off-by: bircni --- internal/app/deadline.go | 11 ++++++++++- internal/app/deadline_test.go | 11 ++++++++++- 2 files changed, 20 insertions(+), 2 deletions(-) diff --git a/internal/app/deadline.go b/internal/app/deadline.go index 3ff7004a..fa9e641b 100644 --- a/internal/app/deadline.go +++ b/internal/app/deadline.go @@ -5,6 +5,8 @@ package app import ( "errors" + "fmt" + "regexp" "strconv" "time" @@ -12,18 +14,25 @@ import ( "github.com/araddon/dateparse" ) +// numericDate matches dates such as 03/04/2026, which read as March 4 in the US and April 3 +// elsewhere. +var numericDate = regexp.MustCompile(`^\d{1,2}[/.]\d{1,2}[/.]\d{2,4}$`) + // parseDeadline parses a --deadline value; an empty value requests clearing. // Supplied timezone offsets are kept and Unix timestamps are UTC. func parseDeadline(value string) (*time.Time, *bool, error) { if value == "" { return nil, new(true), nil } + if numericDate.MatchString(value) { + return nil, nil, clierr.Wrap(clierr.Usage, fmt.Errorf("invalid --deadline %q: the order of day and month is ambiguous; use YYYY-MM-DD", value)) + } date, err := dateparse.ParseAny(value) if _, numericError := strconv.ParseInt(value, 10, 64); numericError == nil { date = date.UTC() } if err != nil || date.IsZero() || date.Unix() == 0 || date.Year() < 1 || date.Year() > 9999 { - return nil, nil, clierr.Wrap(clierr.Usage, errors.New("invalid --deadline; use YYYY-MM-DD, RFC3339, a Unix timestamp, or an empty value to clear")) + return nil, nil, clierr.Wrap(clierr.Usage, errors.New("invalid --deadline; use YYYY-MM-DD, RFC3339 or a Unix timestamp")) } return &date, nil, nil } diff --git a/internal/app/deadline_test.go b/internal/app/deadline_test.go index 28ae887d..6299cabe 100644 --- a/internal/app/deadline_test.go +++ b/internal/app/deadline_test.go @@ -5,6 +5,7 @@ package app import ( "context" + "fmt" "net/http" "testing" @@ -16,7 +17,7 @@ func TestDeadlineInputAndExplicitClearing(t *testing.T) { for _, tc := range []struct{ value, payload string }{ {"2028-01-15", `{"due_date":"2028-01-15T00:00:00Z"}`}, {"2028-01-15T00:30:00+02:00", `{"due_date":"2028-01-15T00:30:00+02:00"}`}, - {"01/15/2028", `{"due_date":"2028-01-15T00:00:00Z"}`}, + {"Jan 15 2028", `{"due_date":"2028-01-15T00:00:00Z"}`}, {"1704067200000", `{"due_date":"2024-01-01T00:00:00Z"}`}, {"1704067200", `{"due_date":"2024-01-01T00:00:00Z"}`}, {"", `{"unset_due_date":true}`}, @@ -41,6 +42,14 @@ func TestDeadlineInputAndExplicitClearing(t *testing.T) { } } +func TestAmbiguousNumericDeadlinesAreRejected(t *testing.T) { + for _, value := range []string{"03/04/2026", "04/03/2026", "01/15/2028", "15.01.2028", "3/4/26"} { + for _, command := range [][]string{{"issue", "edit", "1"}, {"issue", "create", "--title", "x"}, {"milestone", "create", "--title", "x"}} { + requireUsageBeforeRequests(t, fmt.Sprintf("invalid --deadline %q: the order of day and month is ambiguous; use YYYY-MM-DD", value), append(command, "--deadline="+value)...) + } + } +} + func TestInvalidDeadlineBeforeInputCollectionOrNetwork(t *testing.T) { for _, value := range []string{"bad", "2026-02-30", "0", "not-a-timestamp", "999999999999999999999999999999999999"} { for _, command := range [][]string{{"issue", "edit", "1"}, {"pr", "edit", "1", "--add-reviewers=alice"}, {"issue", "create"}, {"pr", "create"}} {