fix(scheduler): reject 6-field cron in Scheduled Operations (#1435)

* fix(scheduler): reject 6-field cron in Scheduled Operations

Create and edit validation parsed cron with cron-parser, which accepts both
5- and 6-field expressions, while the form, presets, and docs all describe a
5-field cron. Because the scheduler ticks once per minute, a leading seconds
field can never improve precision, so a 6-field expression was silently
accepted but never honored on its stated schedule.

Add a field-count guard on both sides: the API rejects 6-field input at
create and edit with a clear message, and the form surfaces the same error
inline and disables save. Cron nicknames such as @daily still pass. Document
the five-field requirement in the cron reference.

* chore: merge main into scheduled cron validation

* fix: avoid logging policy bypass actor in debug output
This commit is contained in:
Anso
2026-06-24 23:00:56 -04:00
committed by GitHub
parent bc8c051962
commit db8bb70b7d
10 changed files with 715 additions and 472 deletions
+11 -1
View File
@@ -332,6 +332,10 @@ export class SchedulerService {
case 'auto_start':
output = await this.executeAutoStart(task);
break;
default: {
const unhandledAction: never = task.action;
throw new Error(`Unsupported scheduled action: ${unhandledAction}`);
}
}
if (isDebugEnabled()) console.log(`[SchedulerService:debug] Task ${task.id} action completed in ${Date.now() - actionStart}ms`);
@@ -673,6 +677,7 @@ export class SchedulerService {
: [...allTargets];
const labelFilter = task.prune_label_filter || undefined;
const results: string[] = [];
const failures: string[] = [];
for (const target of targets) {
try {
@@ -680,11 +685,16 @@ export class SchedulerService {
results.push(`${target}: ${result.reclaimedBytes ?? 0} bytes reclaimed`);
} catch (error: unknown) {
const msg = error instanceof Error ? error.message : String(error);
results.push(`${target}: failed (${msg})`);
const failure = `${target}: failed (${msg})`;
results.push(failure);
failures.push(failure);
}
}
const filterSuffix = labelFilter ? ` (label: ${labelFilter})` : '';
if (failures.length > 0) {
throw new Error(`System prune failed${filterSuffix}: ${results.join('; ')}`);
}
return `System prune completed${filterSuffix}: ${results.join('; ')}`;
}