Repository: meltano/meltano

Issue:
bug: `meltano schedule set` does not validate interval
### Meltano Version

2.19.1

### Python Version

3.10

### Bug scope

CLI (options, error messages, logging, etc.)

### Operating System

N/A

### Description

If you pass an invalid interval to `meltano schedule add`, it will error:
```bash
(meltano-dev-3.10) braedon ~/meltano/projects/test-project $ meltano schedule add csv-to-jsonl --interval fakeinterval --extractor tap-csv --loader target-jsonl
Invalid Cron expression or alias: 'fakeinterval'. Please use a valid cron expression.
```

The same is not true for `meltano schedule set`:
```bash
(meltano-dev-3.10) braedon ~/meltano/projects/test-project $ meltano schedule set csv-to-jsonl --interval fakeinterval
Updated schedule 'csv-to-jsonl'
(meltano-dev-3.10) braedon ~/meltano/projects/test-project $ meltano schedule list
[fakeinterval] elt csv-to-jsonl: tap-csv → target-jsonl x transforms
```

The same validation should be done when setting the interval with `set` and `add`.

### Code

_No response_
bug: `meltano schedule set` does not validate interval
### Meltano Version

2.19.1

### Python Version

3.10

### Bug scope

CLI (options, error messages, logging, etc.)

### Operating System

N/A

### Description

If you pass an invalid interval to `meltano schedule add`, it will error:
```bash
(meltano-dev-3.10) braedon ~/meltano/projects/test-project $ meltano schedule add csv-to-jsonl --interval fakeinterval --extractor tap-csv --loader target-jsonl
Invalid Cron expression or alias: 'fakeinterval'. Please use a valid cron expression.
```

The same is not true for `meltano schedule set`:
```bash
(meltano-dev-3.10) braedon ~/meltano/projects/test-project $ meltano schedule set csv-to-jsonl --interval fakeinterval
Updated schedule 'csv-to-jsonl'
(meltano-dev-3.10) braedon ~/meltano/projects/test-project $ meltano schedule list
[fakeinterval] elt csv-to-jsonl: tap-csv → target-jsonl x transforms
```

The same validation should be done when setting the interval with `set` and `add`.

### Code

_No response_


Discussion:
Good find! This would be a great quick fix and we came across this with a cloud customer.
@BraedonLeonard Can I work on this?
Good find! This would be a great quick fix and we came across this with a cloud customer.
@BraedonLeonard Can I work on this?

Resolving patch:
diff --git a/src/meltano/cli/schedule.py b/src/meltano/cli/schedule.py
--- a/src/meltano/cli/schedule.py
+++ b/src/meltano/cli/schedule.py
@@ -7,6 +7,7 @@
 import typing as t
 
 import click
+from croniter import croniter
 
 from meltano.cli.params import pass_project
 from meltano.cli.utils import (
@@ -16,7 +17,12 @@
 )
 from meltano.core.db import project_engine
 from meltano.core.job.stale_job_failer import fail_stale_jobs
-from meltano.core.schedule_service import ScheduleAlreadyExistsError, ScheduleService
+from meltano.core.schedule import CRON_INTERVALS
+from meltano.core.schedule_service import (
+    BadCronError,
+    ScheduleAlreadyExistsError,
+    ScheduleService,
+)
 from meltano.core.task_sets_service import TaskSetsService
 from meltano.core.utils import coerce_datetime
 
@@ -350,13 +356,30 @@ def _update_elt_schedule(
     return candidate
 
 
+class CronParam(click.ParamType):
+    """Custom type definition for cron prameter."""
+
+    name = "cron"
+
+    def convert(self, value, *_):
+        """Validate and con interval."""
+        if value not in CRON_INTERVALS and not croniter.is_valid(value):
+            raise BadCronError(value)
+
+        return value
+
+
 @schedule.command(
     cls=PartialInstrumentedCmd,
     name="set",
     short_help="Update a schedule.",
 )
 @click.argument("name", required=True)
-@click.option("--interval", help="Update the interval of the schedule.")
+@click.option(
+    "--interval",
+    help="Update the interval of the schedule.",
+    type=CronParam(),
+)
 @click.option("--job", help="Update the name of the job to run a scheduled job.")
 @click.option("--extractor", help="Update the extractor for an elt schedule.")
 @click.option("--loader", help="Updated the loader for an elt schedule.")


Test patch:
diff --git a/tests/meltano/cli/test_schedule.py b/tests/meltano/cli/test_schedule.py
--- a/tests/meltano/cli/test_schedule.py
+++ b/tests/meltano/cli/test_schedule.py
@@ -5,6 +5,7 @@
 
 from asserts import assert_cli_runner
 from meltano.cli import cli
+from meltano.core.schedule_service import BadCronError
 from meltano.core.utils import iso8601_datetime
 
 
@@ -213,6 +214,14 @@ def test_schedule_set(
                 == "0 * * * *"
             )
 
+            # Should raise exception for invalid cron interval
+            res = cli_runner.invoke(
+                cli,
+                ["schedule", "set", elt_schedule.name, "--interval", "@hourl"],
+            )
+            assert res.exit_code != 0
+            assert isinstance(res.exception, BadCronError)
+
             res = cli_runner.invoke(
                 cli,
                 ["schedule", "set", elt_schedule.name, "--interval", "@hourly"],