Skip to content

Validate workflow and task handler/mail event names #6838

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 4 commits into from
Jul 22, 2025
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
1 change: 1 addition & 0 deletions changes.d/6838.fix.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Workflow and task `handler events` and `mail events` names are now validated. Outdated Cylc 7 workflow event names are automatically upgraded.
73 changes: 52 additions & 21 deletions cylc/flow/cfgspec/globalcfg.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,39 +15,55 @@
# along with this program. If not, see <http://www.gnu.org/licenses/>.
"""Cylc site and user configuration file spec."""

from contextlib import suppress
import os
from pathlib import Path
from sys import stderr
from textwrap import dedent, indent
from typing import List, Optional, Tuple, Any, Union
from textwrap import (
dedent,
indent,
)
from typing import (
Any,
Dict,
List,
Optional,
Tuple,
Union,
)

from contextlib import suppress
from packaging.version import Version

from cylc.flow import LOG
from cylc.flow import __version__ as CYLC_VERSION
from cylc.flow.platforms import validate_platforms
from cylc.flow import (
LOG,
__version__ as CYLC_VERSION,
)
from cylc.flow.exceptions import GlobalConfigError
from cylc.flow.hostuserutil import get_user_home
from cylc.flow.network.client_factory import CommsMeth
from cylc.flow.pathutil import SYMLINKABLE_LOCATIONS
from cylc.flow.parsec.config import (
ConfigNode as Conf,
ParsecConfig,
)
from cylc.flow.parsec.exceptions import (
ParsecError,
ItemNotFoundError,
ParsecError,
ValidationError,
)
from cylc.flow.parsec.upgrade import upgrader
from cylc.flow.parsec.util import printcfg, expand_many_section
from cylc.flow.parsec.util import (
expand_many_section,
printcfg,
)
from cylc.flow.parsec.validate import (
CylcConfigValidator as VDR,
DurationFloat,
Range,
cylc_config_validate,
)
from cylc.flow.pathutil import SYMLINKABLE_LOCATIONS
from cylc.flow.platforms import validate_platforms
from cylc.flow.workflow_events import WorkflowEventHandler


PLATFORM_REGEX_TEXT = '''
Expand Down Expand Up @@ -150,7 +166,7 @@
Configure the workflow event handling system.
'''

EVENTS_SETTINGS = { # workflow events
EVENTS_SETTINGS: Dict[str, Union[str, Dict[str, Any]]] = { # workflow events
'handlers': '''
Configure :term:`event handlers` that run when certain workflow
events occur.
Expand All @@ -171,17 +187,26 @@

:ref:`user_guide.scheduler.workflow_events`
''',
'handler events': '''
Specify the events for which workflow event handlers should be invoked.
'handler events': {
'desc': '''
Specify the events for which workflow event handlers should be
invoked.

.. seealso::
.. seealso::

:ref:`user_guide.scheduler.workflow_events`
''',
'mail events': '''
Specify the workflow events for which notification emails should
be sent.
''',
:ref:`user_guide.scheduler.workflow_events`
''',
'options': WorkflowEventHandler.EVENTS.copy(),
'depr_options': WorkflowEventHandler.EVENTS_DEPRECATED.copy(),
},
'mail events': {
'desc': '''
Specify the workflow events for which notification emails should
be sent.
''',
'options': WorkflowEventHandler.EVENTS.copy(),
'depr_options': WorkflowEventHandler.EVENTS_DEPRECATED.copy(),
},
'startup handlers': f'''
Handlers to run at scheduler startup.

Expand Down Expand Up @@ -1038,7 +1063,13 @@ def default_for(

with Conf('events',
desc=default_for(EVENTS_DESCR, '[scheduler][events]')):
for item, desc in EVENTS_SETTINGS.items():
for item, val in EVENTS_SETTINGS.items():
if isinstance(val, dict):
val = val.copy()
desc: str = val.pop('desc')
else:
desc = val
val = {}
desc = default_for(desc, f"[scheduler][events]{item}")
vdr_type = VDR.V_STRING_LIST
default: Any = Conf.UNSET
Expand All @@ -1058,7 +1089,7 @@ def default_for(
default = DurationFloat(300)
else:
default = None
Conf(item, vdr_type, default, desc=desc)
Conf(item, vdr_type, default, desc=desc, **val)

with Conf('mail', desc=(
default_for(MAIL_DESCR, "[scheduler][mail]", section=True)
Expand Down
74 changes: 49 additions & 25 deletions cylc/flow/cfgspec/workflow.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,18 +18,23 @@
import contextlib
import re
from textwrap import dedent
from typing import Any, Dict, Optional, Set
from typing import (
Any,
Dict,
Optional,
Set,
)

from metomi.isodatetime.data import Calendar

from cylc.flow import LOG
from cylc.flow.cfgspec.globalcfg import (
DIRECTIVES_DESCR,
DIRECTIVES_ITEM_DESCR,
LOG_RETR_SETTINGS,
EVENTS_DESCR,
EVENTS_SETTINGS,
EXECUTION_POLL_DESCR,
LOG_RETR_SETTINGS,
MAIL_DESCR,
MAIL_FOOTER_DESCR,
MAIL_FROM_DESCR,
Expand All @@ -47,18 +52,31 @@
UTC_MODE_DESCR,
)
import cylc.flow.flags
from cylc.flow.parsec.exceptions import UpgradeError
from cylc.flow.parsec.config import ParsecConfig, ConfigNode as Conf
from cylc.flow.parsec.OrderedDict import OrderedDictWithDefaults
from cylc.flow.parsec.upgrade import upgrader, converter
from cylc.flow.parsec.config import (
ConfigNode as Conf,
ParsecConfig,
)
from cylc.flow.parsec.exceptions import UpgradeError
from cylc.flow.parsec.upgrade import (
converter,
upgrader,
)
from cylc.flow.parsec.validate import (
DurationFloat, CylcConfigValidator as VDR, cylc_config_validate)
CylcConfigValidator as VDR,
DurationFloat,
cylc_config_validate,
)
from cylc.flow.platforms import (
fail_if_platform_and_host_conflict, get_platform_deprecated_settings,
is_platform_definition_subshell)
from cylc.flow.run_modes import RunMode
fail_if_platform_and_host_conflict,
get_platform_deprecated_settings,
is_platform_definition_subshell,
)
from cylc.flow.run_modes import (
TASK_CONFIG_RUN_MODES,
RunMode,
)
from cylc.flow.task_events_mgr import EventData
from cylc.flow.run_modes import TASK_CONFIG_RUN_MODES


# Regex to check whether a string is a command
Expand Down Expand Up @@ -393,9 +411,16 @@ def get_script_common_text(this: str, example: Optional[str] = None):
)
))

with Conf('events',
desc=global_default(EVENTS_DESCR, '[scheduler][events]')):
for item, desc in EVENTS_SETTINGS.items():
with Conf(
'events', desc=global_default(EVENTS_DESCR, '[scheduler][events]')
):
for item, val in EVENTS_SETTINGS.items():
if isinstance(val, dict):
val = val.copy()
desc: str = val.pop('desc')
else:
desc = val
val = {}
desc = global_default(desc, f"[scheduler][events]{item}")
vdr_type = VDR.V_STRING_LIST
default: Any = Conf.UNSET
Expand Down Expand Up @@ -426,7 +451,7 @@ def get_script_common_text(this: str, example: Optional[str] = None):
vdr_type = VDR.V_BOOLEAN
elif item.endswith("timeout"):
vdr_type = VDR.V_INTERVAL
Conf(item, vdr_type, default, desc=desc)
Conf(item, vdr_type, default, desc=desc, **val)

Conf('expected task failures', VDR.V_STRING_LIST, desc='''
(For Cylc developers writing a functional tests only)
Expand Down Expand Up @@ -2190,13 +2215,15 @@ def upg(cfg, descr):
silent=cylc.flow.flags.cylc7_back_compat,
)

u.obsolete('8.0.0', ['cylc', 'events', 'abort on stalled'])
u.obsolete('8.0.0', ['cylc', 'events', 'abort if startup handler fails'])
u.obsolete('8.0.0', ['cylc', 'events', 'abort if shutdown handler fails'])
u.obsolete('8.0.0', ['cylc', 'events', 'abort if timeout handler fails'])
u.obsolete('8.0.0', ['cylc', 'events',
'abort if inactivity handler fails'])
u.obsolete('8.0.0', ['cylc', 'events', 'abort if stalled handler fails'])
for old in [
'abort on stalled',
'abort if startup handler fails',
'abort if shutdown handler fails',
'abort if timeout handler fails',
'abort if inactivity handler fails',
'abort if stalled handler fails',
]:
u.obsolete('8.0.0', ['cylc', 'events', old])

u.deprecate(
'8.0.0',
Expand Down Expand Up @@ -2279,10 +2306,7 @@ def upgrade_param_env_templates(cfg, descr):
continue
if not cylc.flow.flags.cylc7_back_compat:
if first_warn:
LOG.warning(
'deprecated items automatically upgraded in '
f'"{descr}":'
)
LOG.warning(upgrader.DEPR_MSG)
first_warn = False
LOG.warning(
f' * (8.0.0) {dep % task_name} contents prepended to '
Expand Down
Loading
Loading