From 76e53aef5b0c1552a4dcb5edcbe6d09a538e349d Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Wed, 1 Jul 2026 14:30:05 +0800 Subject: [PATCH 001/297] [v3-3-test] Fix drifting data intervals for monthly/yearly schedules with catchup disabled (#69143) (#69189) Co-authored-by: Shahar Epstein <60007259+shahar1@users.noreply.github.com> --- airflow-core/newsfragments/69143.bugfix.rst | 1 + .../src/airflow/timetables/interval.py | 26 +++++- .../timetables/test_interval_timetable.py | 84 +++++++++++++++++++ 3 files changed, 107 insertions(+), 4 deletions(-) create mode 100644 airflow-core/newsfragments/69143.bugfix.rst diff --git a/airflow-core/newsfragments/69143.bugfix.rst b/airflow-core/newsfragments/69143.bugfix.rst new file mode 100644 index 0000000000000..f6a2b7880d94c --- /dev/null +++ b/airflow-core/newsfragments/69143.bugfix.rst @@ -0,0 +1 @@ +Fix drifting, mis-aligned data intervals for monthly and yearly (``relativedelta``) schedules when ``catchup`` is disabled. The first interval is now anchored to the Dag's ``start_date`` instead of a fixed 30-day/365-day grid, matching the boundaries produced with ``catchup=True``. diff --git a/airflow-core/src/airflow/timetables/interval.py b/airflow-core/src/airflow/timetables/interval.py index 5064ee9be508f..30a7f5033faac 100644 --- a/airflow-core/src/airflow/timetables/interval.py +++ b/airflow-core/src/airflow/timetables/interval.py @@ -224,8 +224,26 @@ def _relativedelta_in_seconds(delta: relativedelta) -> int: + delta.seconds ) - def _round(self, dt: DateTime) -> DateTime: - """Round the given time to the nearest interval.""" + def _round(self, dt: DateTime, anchor: DateTime) -> DateTime: + """ + Floor ``dt`` to the latest schedule boundary at or before it. + + Months/years have no fixed second count, so the epoch-grid rounding + used for fixed deltas would drift. For them we anchor on ``anchor`` + (the start_date) and advance one period at a time, so boundaries match + the catchup=True grid -- relativedelta day-clamping is path-dependent + (e.g. Jan 31 -> Feb 28 -> Mar 28), so a multiplied jump would land + elsewhere. Fixed deltas keep the historical epoch rounding and ignore + ``anchor``. + + ``anchor`` must be at or before ``dt``; otherwise the forward stepping + cannot reach ``dt`` and the result is meaningless. + """ + if isinstance(self._delta, relativedelta) and (self._delta.months or self._delta.years): + boundary = anchor + while self._get_next(boundary) <= dt: + boundary = self._get_next(boundary) + return boundary if isinstance(self._delta, datetime.timedelta): delta_in_seconds = self._delta.total_seconds() else: @@ -243,8 +261,8 @@ def _skip_to_latest(self, earliest: DateTime | None) -> DateTime: This is slightly different from the cron version at terminal values. """ - round_current_time = self._round(coerce_datetime(utcnow())) - new_start = self._get_prev(round_current_time) + now = coerce_datetime(utcnow()) + new_start = self._get_prev(self._round(now, earliest or now)) if earliest is None: return new_start return max(new_start, earliest) diff --git a/airflow-core/tests/unit/timetables/test_interval_timetable.py b/airflow-core/tests/unit/timetables/test_interval_timetable.py index d8ad62131a8a9..dbe27c6784f8a 100644 --- a/airflow-core/tests/unit/timetables/test_interval_timetable.py +++ b/airflow-core/tests/unit/timetables/test_interval_timetable.py @@ -43,6 +43,8 @@ HOURLY_CRON_TIMETABLE = CronDataIntervalTimetable("@hourly", utc) HOURLY_TIMEDELTA_TIMETABLE = DeltaDataIntervalTimetable(datetime.timedelta(hours=1)) HOURLY_RELATIVEDELTA_TIMETABLE = DeltaDataIntervalTimetable(dateutil.relativedelta.relativedelta(hours=1)) +MONTHLY_RELATIVEDELTA_TIMETABLE = DeltaDataIntervalTimetable(dateutil.relativedelta.relativedelta(months=1)) +YEARLY_RELATIVEDELTA_TIMETABLE = DeltaDataIntervalTimetable(dateutil.relativedelta.relativedelta(years=1)) CRON_TIMETABLE = CronDataIntervalTimetable("30 16 * * *", utc) DELTA_FROM_MIDNIGHT = datetime.timedelta(minutes=30, hours=16) @@ -139,6 +141,88 @@ def test_no_catchup_next_info_starts_at_current_time( assert next_info == DagRunInfo.interval(start=expected_start, end=CURRENT_TIME) +@pytest.mark.parametrize( + "last_automated_data_interval", + [ + pytest.param(None, id="first-run"), + pytest.param( + DataInterval( + pendulum.DateTime(2020, 1, 1, tzinfo=utc), + pendulum.DateTime(2020, 2, 1, tzinfo=utc), + ), + id="subsequent", + ), + ], +) +@pytest.mark.parametrize( + ("timetable", "start_date", "expected_start", "expected_end"), + [ + pytest.param( + MONTHLY_RELATIVEDELTA_TIMETABLE, + pendulum.DateTime(2025, 1, 15, tzinfo=utc), + pendulum.DateTime(2026, 5, 15, tzinfo=utc), + pendulum.DateTime(2026, 6, 15, tzinfo=utc), + id="monthly", + ), + pytest.param( + YEARLY_RELATIVEDELTA_TIMETABLE, + pendulum.DateTime(2020, 3, 10, tzinfo=utc), + pendulum.DateTime(2025, 3, 10, tzinfo=utc), + pendulum.DateTime(2026, 3, 10, tzinfo=utc), + id="yearly", + ), + ], +) +@time_machine.travel(pendulum.DateTime(2026, 6, 29, tzinfo=utc)) +def test_no_catchup_calendar_delta_aligns_to_start_date( + timetable: Timetable, + start_date: pendulum.DateTime, + expected_start: pendulum.DateTime, + expected_end: pendulum.DateTime, + last_automated_data_interval: DataInterval | None, +) -> None: + """``catchup=False`` with a relativedelta in months/years must stay aligned + to ``start_date`` and not drift onto a fixed 30-day/365-day epoch grid.""" + next_info = timetable.next_dagrun_info( + last_automated_data_interval=last_automated_data_interval, + restriction=TimeRestriction(earliest=start_date, latest=None, catchup=False), + ) + assert next_info == DagRunInfo.interval(start=expected_start, end=expected_end) + + +@time_machine.travel(pendulum.DateTime(2026, 6, 29, 12, tzinfo=utc)) +def test_no_catchup_calendar_delta_without_start_date_ends_now() -> None: + """With no ``start_date`` to anchor on, the interval simply ends at now.""" + next_info = MONTHLY_RELATIVEDELTA_TIMETABLE.next_dagrun_info( + last_automated_data_interval=None, + restriction=TimeRestriction(earliest=None, latest=None, catchup=False), + ) + assert next_info == DagRunInfo.interval( + start=pendulum.DateTime(2026, 5, 29, 12, tzinfo=utc), + end=pendulum.DateTime(2026, 6, 29, 12, tzinfo=utc), + ) + + +@time_machine.travel(pendulum.DateTime(2026, 6, 29, tzinfo=utc)) +def test_no_catchup_calendar_delta_uses_one_period_at_a_time_clamping() -> None: + """Boundaries advance one relativedelta period at a time, so day-clamping is + path-dependent: Jan 31 + 1 month clamps to Feb 28, and because each step starts + from the previous boundary (not from Jan 31), the 28 then sticks -- + Feb 28 -> Mar 28 -> ... -> May 28 -> Jun 28. A single multiplied jump off + ``start_date`` would instead re-clamp from the 31st and land elsewhere + (Jan 31 + 5 months -> Jun 30).""" + next_info = MONTHLY_RELATIVEDELTA_TIMETABLE.next_dagrun_info( + last_automated_data_interval=None, + restriction=TimeRestriction( + earliest=pendulum.DateTime(2025, 1, 31, tzinfo=utc), latest=None, catchup=False + ), + ) + assert next_info == DagRunInfo.interval( + start=pendulum.DateTime(2026, 5, 28, tzinfo=utc), + end=pendulum.DateTime(2026, 6, 28, tzinfo=utc), + ) + + @pytest.mark.parametrize( "timetable", [ From 92babfd196e4501534e5084673f65429ddc6a45c Mon Sep 17 00:00:00 2001 From: Henry Chen Date: Thu, 2 Jul 2026 16:53:59 +0800 Subject: [PATCH 002/297] [v3-3-test] Mark providers commands for airflowctl(#68525) (#69232) The airflowctl migration direction now keeps existing airflow CLI behavior unchanged and only records commands that should no longer receive new development. (cherry picked from commit 17aa5415c7c36555b7fc645b9b27c21e2965c6d6) --- airflow-core/src/airflow/cli/commands/provider_command.py | 3 +++ .../tests/unit/cli/commands/test_command_deprecations.py | 4 +++- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/airflow-core/src/airflow/cli/commands/provider_command.py b/airflow-core/src/airflow/cli/commands/provider_command.py index 645618fd852cc..fa24b8bb8b884 100644 --- a/airflow-core/src/airflow/cli/commands/provider_command.py +++ b/airflow-core/src/airflow/cli/commands/provider_command.py @@ -22,6 +22,7 @@ import sys from airflow.cli.simple_table import AirflowConsole +from airflow.cli.utils import deprecated_for_airflowctl from airflow.providers_manager import ProvidersManager from airflow.utils.cli import suppress_logs_and_warning from airflow.utils.providers_configuration_loader import providers_configuration_loaded @@ -33,6 +34,7 @@ def _remove_rst_syntax(value: str) -> str: return re.sub("[`_<>]", "", value.strip(" \n.")) +@deprecated_for_airflowctl("airflowctl providers get") @suppress_logs_and_warning @providers_configuration_loaded def provider_get(args): @@ -55,6 +57,7 @@ def provider_get(args): raise SystemExit(f"No such provider installed: {args.provider_name}") +@deprecated_for_airflowctl("airflowctl providers list") @suppress_logs_and_warning @providers_configuration_loaded def providers_list(args): diff --git a/airflow-core/tests/unit/cli/commands/test_command_deprecations.py b/airflow-core/tests/unit/cli/commands/test_command_deprecations.py index 9300219fe5a1e..3f082c48b4708 100644 --- a/airflow-core/tests/unit/cli/commands/test_command_deprecations.py +++ b/airflow-core/tests/unit/cli/commands/test_command_deprecations.py @@ -30,7 +30,7 @@ import pytest -from airflow.cli.commands import asset_command, dag_command, pool_command +from airflow.cli.commands import asset_command, dag_command, pool_command, provider_command # (command callable, expected airflowctl replacement recorded by the decorator) MIGRATED_CLI_COMMANDS = [ @@ -43,6 +43,8 @@ (pool_command.pool_import, "airflowctl pools import"), (pool_command.pool_export, "airflowctl pools export"), (asset_command.asset_materialize, "airflowctl assets materialize"), + (provider_command.provider_get, "airflowctl providers get"), + (provider_command.providers_list, "airflowctl providers list"), ] From b2292dad6e3bffa86841bdaa40d5776920bc12c6 Mon Sep 17 00:00:00 2001 From: Henry Chen Date: Fri, 3 Jul 2026 00:26:01 +0800 Subject: [PATCH 003/297] [v3-3-test] [AIP-94] Mark config CLI commands as migrated to airflowctl (#68958) (#69258) Under AIP-94 the remote config CLI commands are frozen so new development goes to airflowctl. Following the marker-only direction from #68726 , record their airflowctl counterparts for maintainers without emitting any user-facing deprecation warning. (cherry picked from commit 6571dce244edbfc7560474cfc97e9b21e514427b) Co-authored-by: PoAn Yang --- .../src/airflow/cli/commands/config_command.py | 3 +++ .../unit/cli/commands/test_command_deprecations.py | 10 +++++++++- 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/airflow-core/src/airflow/cli/commands/config_command.py b/airflow-core/src/airflow/cli/commands/config_command.py index 21f1dcfc4a9fa..548a2a21e056b 100644 --- a/airflow-core/src/airflow/cli/commands/config_command.py +++ b/airflow-core/src/airflow/cli/commands/config_command.py @@ -28,6 +28,7 @@ from pygments.lexers.configs import IniLexer from airflow.cli.simple_table import AirflowConsole +from airflow.cli.utils import deprecated_for_airflowctl from airflow.configuration import AIRFLOW_CONFIG, ConfigModifications, conf from airflow.exceptions import AirflowConfigException from airflow.utils.cli import should_use_colors @@ -35,6 +36,7 @@ from airflow.utils.providers_configuration_loader import providers_configuration_loaded +@deprecated_for_airflowctl("airflowctl config list") @providers_configuration_loaded def show_config(args): """Show current application configuration.""" @@ -63,6 +65,7 @@ def show_config(args): print(code) +@deprecated_for_airflowctl("airflowctl config get") @providers_configuration_loaded def get_value(args): """Get one value from configuration.""" diff --git a/airflow-core/tests/unit/cli/commands/test_command_deprecations.py b/airflow-core/tests/unit/cli/commands/test_command_deprecations.py index 3f082c48b4708..bf9c181779c81 100644 --- a/airflow-core/tests/unit/cli/commands/test_command_deprecations.py +++ b/airflow-core/tests/unit/cli/commands/test_command_deprecations.py @@ -30,7 +30,13 @@ import pytest -from airflow.cli.commands import asset_command, dag_command, pool_command, provider_command +from airflow.cli.commands import ( + asset_command, + config_command, + dag_command, + pool_command, + provider_command, +) # (command callable, expected airflowctl replacement recorded by the decorator) MIGRATED_CLI_COMMANDS = [ @@ -45,6 +51,8 @@ (asset_command.asset_materialize, "airflowctl assets materialize"), (provider_command.provider_get, "airflowctl providers get"), (provider_command.providers_list, "airflowctl providers list"), + (config_command.get_value, "airflowctl config get"), + (config_command.show_config, "airflowctl config list"), ] From c69d665df905926c0dbe91e40914c5001741157a Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Fri, 3 Jul 2026 17:49:35 +0800 Subject: [PATCH 004/297] [v3-3-test] Fix OTel integration test after task.execute span addition (#69236) (#69246) --- airflow-core/tests/integration/otel/test_otel.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/airflow-core/tests/integration/otel/test_otel.py b/airflow-core/tests/integration/otel/test_otel.py index d19756094b6ce..6826d6cc877a7 100644 --- a/airflow-core/tests/integration/otel/test_otel.py +++ b/airflow-core/tests/integration/otel/test_otel.py @@ -467,9 +467,10 @@ def test_export_metrics_during_process_shutdown(self, capfd): "_validate_task_inlets_and_outlets": "_prepare", "_prepare": "run", "_execute_task": "run", + "task.execute": "_execute_task", "finalize": "worker.task1", "run": "worker.task1", - "sub_span1": "_execute_task", + "sub_span1": "task.execute", "dag_run.otel_test_dag": None, "task_run.task1": "dag_run.otel_test_dag", "worker.task1": "task_run.task1", From 0668801685c08839a2f9ad2c456c161c9ae31693 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Fri, 3 Jul 2026 15:46:28 +0200 Subject: [PATCH 005/297] [v3-3-test] Add auto area labels for ts-sdk and java-sdk PRs (#69325) (#69331) (cherry picked from commit 15272fbac82f491b5622ca0b3595289e27fe87cf) Co-authored-by: Guan-Ming Chiu <105915352+guan404ming@users.noreply.github.com> --- .github/boring-cyborg.yml | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/.github/boring-cyborg.yml b/.github/boring-cyborg.yml index f83c78cb39ee2..dfec60d504a38 100644 --- a/.github/boring-cyborg.yml +++ b/.github/boring-cyborg.yml @@ -554,6 +554,18 @@ labelPRBasedOnFilePath: area:go-sdk: - go-sdk/**/* + area:java-sdk: + - java-sdk/**/* + + area:ts-sdk: + - ts-sdk/**/* + + "AIP-108: Coordinator": + - task-sdk/src/airflow/sdk/coordinators/**/* + - task-sdk/src/airflow/sdk/execution_time/coordinator.py + - task-sdk/tests/task_sdk/coordinators/**/* + - task-sdk/tests/task_sdk/execution_time/test_coordinator.py + area:db-migrations: - airflow-core/src/airflow/migrations/versions/* From c9cd25ffa5b784f19560081c7c248ae4db8e54f7 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Fri, 3 Jul 2026 15:47:15 +0200 Subject: [PATCH 006/297] [v3-3-test] Update upload-artifact gh action (#69256) (#69268) (cherry picked from commit b71135bdbb3d9a8355d6815ab371829b9ad47734) Co-authored-by: Kacper Muda --- .github/actions/post_tests_failure/action.yml | 6 +++--- .github/actions/post_tests_success/action.yml | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/actions/post_tests_failure/action.yml b/.github/actions/post_tests_failure/action.yml index e6bbf03ad9416..275339a54457a 100644 --- a/.github/actions/post_tests_failure/action.yml +++ b/.github/actions/post_tests_failure/action.yml @@ -22,21 +22,21 @@ runs: using: "composite" steps: - name: "Upload airflow logs" - uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 with: name: airflow-logs-${{env.JOB_ID}} path: './files/airflow_logs*' retention-days: 7 if-no-files-found: ignore - name: "Upload container logs" - uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 with: name: container-logs-${{env.JOB_ID}} path: "./files/container_logs*" retention-days: 7 if-no-files-found: ignore - name: "Upload other logs" - uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 with: name: container-logs-${{env.JOB_ID}} path: "./files/other_logs*" diff --git a/.github/actions/post_tests_success/action.yml b/.github/actions/post_tests_success/action.yml index 7298fadaf7cb5..4c41a9331ecb9 100644 --- a/.github/actions/post_tests_success/action.yml +++ b/.github/actions/post_tests_success/action.yml @@ -31,7 +31,7 @@ runs: using: "composite" steps: - name: "Upload artifact for warnings" - uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 with: name: test-warnings-${{ env.JOB_ID }} path: ./files/warnings-*.txt From 450161af5d038f84c479ce334799f0e34e404d59 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Fri, 3 Jul 2026 15:47:36 +0200 Subject: [PATCH 007/297] [v3-3-test] Gate provider release on artifact completeness check before vote (#69141) (#69180) A recent ad-hoc provider release reached the vote thread missing the all-providers -source.tar.gz tarball, drawing a -1. The completeness check that catches this already exists as a PMC-reviewer step but the release-manager flow never ran it, so the gap only surfaced after the vote email went out. Run the same check right after the SVN commit so a missing artifact fails for the release manager first. (cherry picked from commit b176ec91dc543338079d92b6697eb59a1520e69b) Co-authored-by: Shahar Epstein <60007259+shahar1@users.noreply.github.com> --- dev/README_RELEASE_PROVIDERS.md | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/dev/README_RELEASE_PROVIDERS.md b/dev/README_RELEASE_PROVIDERS.md index d958fc68a0857..7a95c062dd1e9 100644 --- a/dev/README_RELEASE_PROVIDERS.md +++ b/dev/README_RELEASE_PROVIDERS.md @@ -592,6 +592,20 @@ svn commit -m "Add artifacts for Airflow Providers ${RELEASE_DATE}" cd "$AIRFLOW_REPO_ROOT" ``` +* Before sending the vote email, gate on the same completeness check the PMC verifiers run, so a + missing artifact (e.g. the `-source.tar.gz` tarball) fails here instead of in the vote thread. + Put the package list from the upcoming vote email into `dev/packages.txt`, then run: + +```shell script +cd "$AIRFLOW_REPO_ROOT" +breeze release-management check-release-files providers --release-date "${RELEASE_DATE}" \ + --packages-file ./dev/packages.txt \ + --path-to-airflow-svn "$(cd ../asf-dist/dev/airflow && pwd -P)" +``` + + It exits non-zero and lists every missing file (including `.asc`/`.sha512` variants) if anything is + absent. Only proceed to the vote once it prints `All expected files are present!`. + Verify that the files are available in the ${RELEASE_DATE} folder under [providers](https://dist.apache.org/repos/dist/dev/airflow/providers/) From aa33d518902d2715a443d97bbef243d9b9a42556 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Fri, 3 Jul 2026 15:47:55 +0200 Subject: [PATCH 008/297] [v3-3-test] Auto-allow breeze in the Claude Code isolated agent setup (#69094) (#69129) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit breeze is the mandated entrypoint for all local development in this repo (the contributor instructions forbid running pytest/python/airflow directly on the host), so contributors using Claude Code's isolated/secure agent setup get a permission prompt on every breeze invocation. This adds a Magpie adopter override so the setup-isolated-setup-install skill seeds a per-machine breeze allow baseline (including the uvx/uv-run dev/breeze shim forms and read-only docker ps/info) into .claude/settings.local.json at install time — no shared committed settings file to maintain, and only non-destructive commands. (cherry picked from commit 5cf831b4a608a3589b00ee1dcc1853528032d225) Co-authored-by: Jarek Potiuk --- .../setup-isolated-setup-install.md | 81 +++++++++++++++++++ 1 file changed, 81 insertions(+) create mode 100644 .apache-magpie-overrides/setup-isolated-setup-install.md diff --git a/.apache-magpie-overrides/setup-isolated-setup-install.md b/.apache-magpie-overrides/setup-isolated-setup-install.md new file mode 100644 index 0000000000000..0874037043d0e --- /dev/null +++ b/.apache-magpie-overrides/setup-isolated-setup-install.md @@ -0,0 +1,81 @@ + + +# Override: setup-isolated-setup-install — seed the breeze permission baseline + +## What this overrides + +Augments the **settings merge** the `setup-isolated-setup-install` skill +performs (its "Golden rules → Do not overwrite an existing settings file +silently" desired-merge, and the project-local settings write in **Step P**). +When wiring the isolated / secure agent setup for this repo, add the +project-specific Bash permission allow-list below to the adopter's +**project-local** settings file (`/.claude/settings.local.json`) — +merged alongside the sandbox `allowRead` / `allowWrite` entries Step P already +writes there, surfaced in the same diff, and applied with the same approval. + +This seeds a per-machine baseline at install time; it does **not** introduce a +committed project-scope `.claude/settings.json` (that path stays gitignored in +this repo). + +## Permissions to add + +Add each of these to `permissions.allow` (create the key if absent). Never +remove or reorder pre-existing entries — this is an additive merge, and an +entry already present is a no-op: + +```text +Bash(breeze *) +Bash(ANSWER=yes breeze *) +Bash(SKIP_BREEZE_SELF_UPGRADE_CHECK=1 breeze *) +Bash(SKIP_BREEZE_SELF_UPGRADE_CHECK=true breeze *) +Bash(SKIP_BREEZE_SELF_UPGRADE_CHECK= breeze *) +Bash(uvx *dev/breeze*) +Bash(uv run *dev/breeze*) +Bash(docker ps *) +Bash(docker info *) +``` + +## Why + +`breeze` is the mandated entrypoint for all local development in Apache +Airflow — the contributor instructions forbid running `pytest` / `python` / +`airflow` directly on the host (see this repo's `AGENTS.md` / `CLAUDE.md`). +Without these allows, every breeze invocation prompts for Bash permission, +which is pure friction for a command contributors run constantly. + +- The `uvx` / `uv run *dev/breeze*` forms cover the breeze shim that runs + breeze from `dev/breeze` (see [ADR 0017](../dev/breeze/doc/adr/0017-use-uvx-to-run-breeze-from-local-sources.md)). +- The env-prefixed forms cover the common `ANSWER=yes` and + `SKIP_BREEZE_SELF_UPGRADE_CHECK` wrappers used throughout the dev/CI scripts. + +Only **non-destructive** commands are allowed: breeze itself (which drives +docker internally, so its docker usage is covered by the single breeze call) +plus read-only `docker ps` / `docker info`. No `docker rm`, no +`docker network prune`, nothing that writes outside the breeze workflow — +those stay prompt-gated. + +## Scope notes + +- These land in the gitignored, per-machine `.claude/settings.local.json`, + applied when a contributor sets up the isolated build — there is no shared + committed settings file to review or maintain. +- A contributor's own additional per-machine allows live in the same + `settings.local.json`; this override only seeds the breeze baseline and + leaves everything else untouched. From d198fa8ae4700a0dc103e3eb89d64a4432204519 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Fri, 3 Jul 2026 16:03:07 +0200 Subject: [PATCH 009/297] [v3-3-test] Add docker stack docs example for venv scene (#69088) (#69112) * [v3-3-test] Add docker stack docs example for venv scene (#69088) (cherry picked from commit 899ce98b5c2730abeef19b15c840e537cd2b6227) Co-authored-by: Andrew Chang <69671930+Andrushika@users.noreply.github.com> * Use 3.3.0 base image in the venv Dockerfile example on v3-3-test The example was backported verbatim from main, where the base image is apache/airflow:3.4.0. On the v3-3-test branch the example should reference the matching release line, 3.3.0. --------- Co-authored-by: Andrew Chang <69671930+Andrushika@users.noreply.github.com> Co-authored-by: Jarek Potiuk --- docker-stack-docs/build.rst | 22 +++++++++++++++++ .../extending/add-python-venv/Dockerfile | 24 +++++++++++++++++++ .../add-python-venv/requirements.txt | 1 + 3 files changed, 47 insertions(+) create mode 100644 docker-stack-docs/docker-examples/extending/add-python-venv/Dockerfile create mode 100644 docker-stack-docs/docker-examples/extending/add-python-venv/requirements.txt diff --git a/docker-stack-docs/build.rst b/docker-stack-docs/build.rst index dd3cf7e01f7ef..79ffe2bd7b22c 100644 --- a/docker-stack-docs/build.rst +++ b/docker-stack-docs/build.rst @@ -477,6 +477,28 @@ Note that similarly when adding individual packages, you need to use the ``airfl :language: text +Example of adding a Python venv for ``ExternalPythonOperator`` +.............................................................. + +The following example creates a separate Python virtualenv inside the image and installs packages +from ``requirements.txt`` into it, leaving Airflow's own Python environment untouched. This is the +typical setup for the +:ref:`ExternalPythonOperator ` +(or the ``@task.external_python`` decorator), which executes tasks in a pre-existing, immutable +Python environment. Use ``/opt/airflow/venv/bin/python`` as the operator's ``python`` argument +to run tasks in this venv. + +The venv is created without ``--system-site-packages``, so the packages installed inside it +are isolated from Airflow's own dependencies. + +.. exampleinclude:: docker-examples/extending/add-python-venv/Dockerfile + :language: Dockerfile + :start-after: [START Dockerfile] + :end-before: [END Dockerfile] + +.. exampleinclude:: docker-examples/extending/add-python-venv/requirements.txt + :language: text + Example when writable directory is needed ......................................... diff --git a/docker-stack-docs/docker-examples/extending/add-python-venv/Dockerfile b/docker-stack-docs/docker-examples/extending/add-python-venv/Dockerfile new file mode 100644 index 0000000000000..13fe643797f2a --- /dev/null +++ b/docker-stack-docs/docker-examples/extending/add-python-venv/Dockerfile @@ -0,0 +1,24 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. + +# This is an example Dockerfile. It is not intended for PRODUCTION use +# [START Dockerfile] +FROM apache/airflow:3.3.0 +COPY requirements.txt / +RUN python -m venv /opt/airflow/venv \ + && /opt/airflow/venv/bin/pip install --no-cache-dir -r /requirements.txt +# [END Dockerfile] diff --git a/docker-stack-docs/docker-examples/extending/add-python-venv/requirements.txt b/docker-stack-docs/docker-examples/extending/add-python-venv/requirements.txt new file mode 100644 index 0000000000000..e8710b5a474db --- /dev/null +++ b/docker-stack-docs/docker-examples/extending/add-python-venv/requirements.txt @@ -0,0 +1 @@ +colorama==0.4.0 From d21627282766df9933af7b132015e66210644371 Mon Sep 17 00:00:00 2001 From: Jarek Potiuk Date: Fri, 3 Jul 2026 17:39:51 +0200 Subject: [PATCH 010/297] Remove unused ts-sdk label rule from Boring Cyborg config (#69340) The area:ts-sdk label rule globs ts-sdk/**/*, but the ts-sdk directory does not exist on this release branch (it was added to main after 3.3 was cut). The check-boring-cyborg-configuration static check flags any pattern that matches zero files, so the rule made the check fail on every PR targeting this branch. Remove the stale rule. --- .github/boring-cyborg.yml | 3 --- 1 file changed, 3 deletions(-) diff --git a/.github/boring-cyborg.yml b/.github/boring-cyborg.yml index dfec60d504a38..0a9acdd51e2f6 100644 --- a/.github/boring-cyborg.yml +++ b/.github/boring-cyborg.yml @@ -557,9 +557,6 @@ labelPRBasedOnFilePath: area:java-sdk: - java-sdk/**/* - area:ts-sdk: - - ts-sdk/**/* - "AIP-108: Coordinator": - task-sdk/src/airflow/sdk/coordinators/**/* - task-sdk/src/airflow/sdk/execution_time/coordinator.py From 4a3c2412b84f64687d9dbdb8b446c378391b521d Mon Sep 17 00:00:00 2001 From: Jarek Potiuk Date: Fri, 3 Jul 2026 18:08:58 +0200 Subject: [PATCH 011/297] Reduce noise in the daily CI duration trend alert (#69113) (#69337) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The duration monitor flagged jobs by comparing a single nightly canary run against the median of the preceding runs, so any one slow run — slow PyPI, runner queue pressure, a cold cache — tripped the alert. Because a different run was "latest" each day, a different set of jobs was flagged each day, and network-bound constraint-resolution jobs that legitimately swing tens of minutes dominated nearly every alert. The result was a near-daily alert whose contents swung wildly and carried little signal. Compare the median of the last few nightly runs against the baseline so the two sides are symmetric and one unlucky run no longer trips it, and require a larger absolute jump before flagging individual jobs. Pin the monitor to successful (green) canary runs only. A failed or cancelled canary stops partway, so its truncated wall-clock and per-job durations would skew the baseline downwards and mask real regressions. The script already defaults to this, but the guarantee is now explicit at the call site so it cannot be silently changed. (cherry picked from commit e99daee1c154b77d807f45c0e3b00a6660980238) --- .github/workflows/ci-duration-monitor.yml | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/.github/workflows/ci-duration-monitor.yml b/.github/workflows/ci-duration-monitor.yml index 2f3e1398d29ab..08e6b6b265787 100644 --- a/.github/workflows/ci-duration-monitor.yml +++ b/.github/workflows/ci-duration-monitor.yml @@ -49,7 +49,20 @@ jobs: # main coverage comes from the scheduled canary runs of the AMD workflow. WORKFLOW_NAME: "ci-amd.yml" BRANCH: "main" + # Only successful (green) canary runs feed the baseline — a failed or cancelled + # canary stops partway, so its truncated wall-clock and per-job durations would + # skew the trend downwards and mask real regressions. Set explicitly so the + # green-only guarantee is visible at the call site and can't be silently changed. + ONLY_SUCCESSFUL: "true" MAX_RUNS: "25" + # Compare the median of the last few nightly runs (not a single run) against the + # baseline so one unlucky run — slow PyPI, runner queue pressure, cold cache — does + # not trip the alert. With LATEST_RUNS=1 both sides were asymmetric (raw point vs + # median) and the alert fired most nights on whichever jobs happened to be slow. + LATEST_RUNS: "3" + # Network-bound jobs (constraint resolution, provider installs) legitimately swing + # tens of minutes run-to-run; require a larger sustained jump before flagging them. + JOB_MIN_ABS_INCREASE_MINUTES: "6" OUTPUT_FILE: "slack-message.json" - name: "Post duration alert to Slack" From 8b0bfe823c34cad22f3428d7d603da14232a53cd Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Fri, 3 Jul 2026 18:12:32 +0200 Subject: [PATCH 012/297] [v3-3-test] Update local Otel Collector and Prometheus Versions to support Exponential Histograms. (#69040) (#69056) (cherry picked from commit e9ef38b9e0004a4d13cf650a749463109489bbc3) Co-authored-by: Ei Sandi Aung <150097294+Ei-Sandi@users.noreply.github.com> --- .../logging-monitoring/metrics.rst | 36 +++++++++++++++++++ .../ci/docker-compose/integration-otel.yml | 7 ++-- .../docker-compose/otel-collector-config.yml | 7 ++-- 3 files changed, 45 insertions(+), 5 deletions(-) diff --git a/airflow-core/docs/administration-and-deployment/logging-monitoring/metrics.rst b/airflow-core/docs/administration-and-deployment/logging-monitoring/metrics.rst index 49f37ab18207f..d79849c3d29c6 100644 --- a/airflow-core/docs/administration-and-deployment/logging-monitoring/metrics.rst +++ b/airflow-core/docs/administration-and-deployment/logging-monitoring/metrics.rst @@ -115,6 +115,42 @@ You need to configure the SSL certificate and key within the OpenTelemetry colle cert_file: "/path/to/cert/cert.crt" key_file: "/path/to/key/key.pem" +Histogram Metrics and Backend Requirements +------------------------------------------ + +Airflow's timing metrics (``timing()`` / ``timer()``) are emitted as OpenTelemetry +histograms aggregated with +`exponential bucket histograms `_, +so bucket boundaries adapt automatically to the observed range and you do not have to +hand-tune explicit buckets for metrics that span very different scales (milliseconds to +hours). + +To ingest these correctly end-to-end, the metrics backend you connect to must support +OpenTelemetry exponential histograms and (for Prometheus) their conversion to native +histograms: + +* **OpenTelemetry Collector** — use ``opentelemetry-collector-contrib`` version 0.115.0 + or above. Older versions do not translate OTLP exponential histograms into Prometheus + native histograms. +* **Prometheus** — native histograms must be enabled explicitly, and how you do that + depends on the Prometheus version: + + * **2.40 to 3.8** — start Prometheus with the ``--enable-feature=native-histograms`` + flag. + * **3.8 and above** — set ``scrape_native_histograms: true`` in the scrape + configuration (this option was added in 3.8, and from 3.9 the feature flag is a + no-op so the config setting is required): + + .. code-block:: yaml + + global: + scrape_native_histograms: true + +If the backend does not support native histograms, exponential-histogram data points may +be dropped or rendered incorrectly. A reference stack (Collector, Prometheus, and Grafana) +wired up for local development is available via ``breeze start-airflow --integration otel``; +see the contributor docs for details. + Allow/Block Lists ----------------- diff --git a/scripts/ci/docker-compose/integration-otel.yml b/scripts/ci/docker-compose/integration-otel.yml index 8b98699fccbfd..fd7f252c3f1a3 100644 --- a/scripts/ci/docker-compose/integration-otel.yml +++ b/scripts/ci/docker-compose/integration-otel.yml @@ -17,7 +17,7 @@ --- services: otel-collector: - image: otel/opentelemetry-collector-contrib:0.70.0 + image: otel/opentelemetry-collector-contrib:0.155.0 labels: breeze.description: "Integration required for OTEL/opentelemetry hooks." container_name: "breeze-otel-collector" @@ -29,9 +29,12 @@ services: - "28889:8889" # Prometheus exporter metrics prometheus: - image: prom/prometheus + image: prom/prometheus:v3.5.4 container_name: "breeze-prometheus" user: "0" + command: + - "--config.file=/etc/prometheus/prometheus.yml" + - "--enable-feature=native-histograms" ports: - "29090:9090" volumes: diff --git a/scripts/ci/docker-compose/otel-collector-config.yml b/scripts/ci/docker-compose/otel-collector-config.yml index 9c6ede5f50ca6..b22c07f736214 100644 --- a/scripts/ci/docker-compose/otel-collector-config.yml +++ b/scripts/ci/docker-compose/otel-collector-config.yml @@ -22,6 +22,7 @@ receivers: otlp: protocols: http: + endpoint: 0.0.0.0:4318 processors: batch: @@ -32,7 +33,7 @@ exporters: tls: insecure: true - logging: + debug: verbosity: detailed prometheus: endpoint: 0.0.0.0:8889 @@ -44,9 +45,9 @@ service: traces: receivers: [otlp] processors: [batch] - exporters: [logging, otlp/jaeger] + exporters: [debug, otlp/jaeger] metrics: receivers: [otlp] processors: [batch] - exporters: [logging, prometheus] + exporters: [debug, prometheus] From 9335cb5766ae57789097743c90a96bca76db248e Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Fri, 3 Jul 2026 19:21:12 +0200 Subject: [PATCH 013/297] [v3-3-test] Honor catchup for historical asset events in asset-triggered Dags (#68749) (#69224) (cherry picked from commit ff10b2e3e19c9a6c1a262ebd32ef65b968b39424) Co-authored-by: Shahar Epstein <60007259+shahar1@users.noreply.github.com> Co-authored-by: Jarek Potiuk --- airflow-core/newsfragments/68749.bugfix.rst | 1 + .../src/airflow/jobs/scheduler_job_runner.py | 14 +- .../tests/unit/jobs/test_scheduler_job.py | 123 ++++++++++++++---- 3 files changed, 113 insertions(+), 25 deletions(-) create mode 100644 airflow-core/newsfragments/68749.bugfix.rst diff --git a/airflow-core/newsfragments/68749.bugfix.rst b/airflow-core/newsfragments/68749.bugfix.rst new file mode 100644 index 0000000000000..592b875b2f54b --- /dev/null +++ b/airflow-core/newsfragments/68749.bugfix.rst @@ -0,0 +1 @@ +Asset-triggered Dags now honor ``catchup`` for historical asset events. With ``catchup`` off (the default), a newly added asset-triggered Dag no longer consumes events recorded before it started scheduling on those assets; its first run is bounded at the moment the schedule reference was created instead of reprocessing the entire backlog. With ``catchup`` on, the backlog is still replayed (#39456). diff --git a/airflow-core/src/airflow/jobs/scheduler_job_runner.py b/airflow-core/src/airflow/jobs/scheduler_job_runner.py index 2f94d480eb6d1..355f693db0a02 100644 --- a/airflow-core/src/airflow/jobs/scheduler_job_runner.py +++ b/airflow-core/src/airflow/jobs/scheduler_job_runner.py @@ -2581,6 +2581,18 @@ def _create_dag_runs_asset_triggered( .cte() ) + # A first asset-triggered run has no previous run to floor the event window. With + # catchup off, floor it at when the Dag started scheduling on its assets so the + # backlog is skipped; with catchup on, only date.min applies and the backlog replays. + event_window_floor: list[Any] = [cte.c.previous_dag_run_run_after] + if not dag.catchup: + event_window_floor.append( + select(func.min(DagScheduleAssetReference.created_at)) + .where(DagScheduleAssetReference.dag_id == dag.dag_id) + .scalar_subquery() + ) + event_window_floor.append(date.min) + asset_events = list( session.scalars( select(AssetEvent) @@ -2598,7 +2610,7 @@ def _create_dag_runs_asset_triggered( ), ), AssetEvent.timestamp <= triggered_date, - AssetEvent.timestamp > func.coalesce(cte.c.previous_dag_run_run_after, date.min), + AssetEvent.timestamp > func.coalesce(*event_window_floor), ) .order_by(AssetEvent.timestamp.asc(), AssetEvent.id.asc()) ) diff --git a/airflow-core/tests/unit/jobs/test_scheduler_job.py b/airflow-core/tests/unit/jobs/test_scheduler_job.py index e2d5977eb86f7..d7b08e662b19b 100644 --- a/airflow-core/tests/unit/jobs/test_scheduler_job.py +++ b/airflow-core/tests/unit/jobs/test_scheduler_job.py @@ -65,6 +65,7 @@ AssetEvent, AssetModel, AssetPartitionDagRun, + DagScheduleAssetReference, PartitionedAssetKeyLog, ) from airflow.models.backfill import Backfill, BackfillDagRun, ReprocessBehavior, _create_backfill @@ -5501,51 +5502,59 @@ def test_create_dag_runs_assets(self, session, dag_maker): with dag_maker(dag_id="assets-1", start_date=timezone.utcnow(), session=session): BashOperator(task_id="task", bash_command="echo 1", outlets=[asset1]) - dr = dag_maker.create_dagrun( + dr1 = dag_maker.create_dagrun( run_id="run1", logical_date=(DEFAULT_DATE + timedelta(days=100)), data_interval=(DEFAULT_DATE + timedelta(days=10), DEFAULT_DATE + timedelta(days=11)), ) + dr2 = dag_maker.create_dagrun( + run_id="run2", + logical_date=(DEFAULT_DATE + timedelta(days=101)), + data_interval=(DEFAULT_DATE + timedelta(days=5), DEFAULT_DATE + timedelta(days=6)), + ) asset1_id = session.scalar(select(AssetModel.id).where(AssetModel.uri == asset1.uri)) + # Consumer Dags are created before the events, so the events fall within their window. + with dag_maker(dag_id="assets-consumer-multiple", schedule=[asset1, asset2]): + pass + dag2 = dag_maker.dag + with dag_maker(dag_id="assets-consumer-single", schedule=[asset1]): + pass + dag3 = dag_maker.dag + + base = session.scalar( + select(DagScheduleAssetReference.created_at).where( + DagScheduleAssetReference.dag_id == dag3.dag_id + ) + ) event1 = AssetEvent( asset_id=asset1_id, source_task_id="task", - source_dag_id=dr.dag_id, - source_run_id=dr.run_id, + source_dag_id=dr1.dag_id, + source_run_id=dr1.run_id, source_map_index=-1, + timestamp=base + timedelta(seconds=1), ) - session.add(event1) - - # Create a second event, creation time is more recent, but data interval is older - dr = dag_maker.create_dagrun( - run_id="run2", - logical_date=(DEFAULT_DATE + timedelta(days=101)), - data_interval=(DEFAULT_DATE + timedelta(days=5), DEFAULT_DATE + timedelta(days=6)), - ) - event2 = AssetEvent( asset_id=asset1_id, source_task_id="task", - source_dag_id=dr.dag_id, - source_run_id=dr.run_id, + source_dag_id=dr2.dag_id, + source_run_id=dr2.run_id, source_map_index=-1, + timestamp=base + timedelta(seconds=2), ) - session.add(event2) - - with dag_maker(dag_id="assets-consumer-multiple", schedule=[asset1, asset2]): - pass - dag2 = dag_maker.dag - with dag_maker(dag_id="assets-consumer-single", schedule=[asset1]): - pass - dag3 = dag_maker.dag + session.add_all([event1, event2]) session = dag_maker.session session.add_all( [ - AssetDagRunQueue(asset_id=asset1_id, target_dag_id=dag2.dag_id), - AssetDagRunQueue(asset_id=asset1_id, target_dag_id=dag3.dag_id), + AssetDagRunQueue( + asset_id=asset1_id, target_dag_id=dag2.dag_id, created_at=base + timedelta(hours=1) + ), + AssetDagRunQueue( + asset_id=asset1_id, target_dag_id=dag3.dag_id, created_at=base + timedelta(hours=1) + ), ] ) session.flush() @@ -5591,6 +5600,72 @@ def dict_from_obj(obj): assert created_run.creating_job_id == scheduler_job.id + @pytest.mark.need_serialized_dag + @pytest.mark.parametrize( + ("catchup", "expects_old_event"), + [ + pytest.param(False, False, id="catchup-off-ignores-backlog"), + pytest.param(True, True, id="catchup-on-consumes-backlog"), + ], + ) + def test_new_asset_triggered_dag_backlog_gated_by_catchup( + self, catchup, expects_old_event, session, dag_maker + ): + """Reproduces #39456: catchup gates whether a new asset-triggered Dag replays the + pre-creation backlog. With catchup off (the default) it only consumes events after it + started scheduling on the asset; with catchup on it replays the full history.""" + asset = Asset(uri="test://asset-historical", name="hist_asset", group="test_group") + + # Producer Dag + run that the asset events are sourced from. + with dag_maker(dag_id="historical-producer", start_date=timezone.utcnow(), session=session): + BashOperator(task_id="task", bash_command="echo 1", outlets=[asset]) + producer_run = dag_maker.create_dagrun(run_id="producer-run") + + asset_id = session.scalar(select(AssetModel.id).where(AssetModel.uri == asset.uri)) + + # Consumer Dag created now; its schedule reference's created_at is the cut-off. + with dag_maker(dag_id="historical-consumer", schedule=[asset], catchup=catchup): + pass + consumer_dag = dag_maker.dag + reference_created_at = session.scalar( + select(DagScheduleAssetReference.created_at).where( + DagScheduleAssetReference.dag_id == consumer_dag.dag_id + ) + ) + + def _make_event(timestamp): + return AssetEvent( + asset_id=asset_id, + source_task_id="task", + source_dag_id=producer_run.dag_id, + source_run_id=producer_run.run_id, + source_map_index=-1, + timestamp=timestamp, + ) + + old_event = _make_event(reference_created_at - timedelta(days=1)) + new_event = _make_event(reference_created_at + timedelta(seconds=1)) + session.add_all([old_event, new_event]) + # Trigger time after both events so neither is excluded by the upper bound. + session.add( + AssetDagRunQueue( + asset_id=asset_id, + target_dag_id=consumer_dag.dag_id, + created_at=reference_created_at + timedelta(hours=1), + ) + ) + session.flush() + + scheduler_job = Job() + self.job_runner = SchedulerJobRunner(job=scheduler_job, executors=[self.null_exec]) + with create_session() as session: + self.job_runner._create_dagruns_for_dags(session, session) + + created_run = session.scalars(select(DagRun).where(DagRun.dag_id == consumer_dag.dag_id)).one() + assert created_run.state == State.QUEUED + expected = {new_event.id} | ({old_event.id} if expects_old_event else set()) + assert {e.id for e in created_run.consumed_asset_events} == expected + @pytest.mark.need_serialized_dag def test_create_dag_runs_asset_alias_with_asset_event_attached(self, session, dag_maker): """ From 5a9caee89cc7d25dc6cb7231fee453d4a2949f45 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Fri, 3 Jul 2026 19:21:19 +0200 Subject: [PATCH 014/297] [v3-3-test] Mark dags details for airflowctl (#68529) (#69222) The airflowctl migration direction keeps the existing airflow CLI behavior unchanged and only records commands that should no longer receive new development. (cherry picked from commit 98f89a0497674d07281b535db3b6c90e77f56bbd) Co-authored-by: Henry Chen Co-authored-by: Jarek Potiuk --- airflow-core/src/airflow/cli/commands/dag_command.py | 1 + .../tests/unit/cli/commands/test_command_deprecations.py | 1 + 2 files changed, 2 insertions(+) diff --git a/airflow-core/src/airflow/cli/commands/dag_command.py b/airflow-core/src/airflow/cli/commands/dag_command.py index 756048af14d5e..6a5b9b3a7cffc 100644 --- a/airflow-core/src/airflow/cli/commands/dag_command.py +++ b/airflow-core/src/airflow/cli/commands/dag_command.py @@ -614,6 +614,7 @@ def filter_dags_by_bundle(dags: Iterable[DAG], bundle_names: list[str] | None) - ) +@deprecated_for_airflowctl("airflowctl dags get-details") @cli_utils.action_cli @suppress_logs_and_warning @providers_configuration_loaded diff --git a/airflow-core/tests/unit/cli/commands/test_command_deprecations.py b/airflow-core/tests/unit/cli/commands/test_command_deprecations.py index bf9c181779c81..44f23c111196c 100644 --- a/airflow-core/tests/unit/cli/commands/test_command_deprecations.py +++ b/airflow-core/tests/unit/cli/commands/test_command_deprecations.py @@ -42,6 +42,7 @@ MIGRATED_CLI_COMMANDS = [ (dag_command.dag_trigger, "airflowctl dags trigger"), (dag_command.dag_delete, "airflowctl dags delete"), + (dag_command.dag_details, "airflowctl dags get-details"), (pool_command.pool_list, "airflowctl pools list"), (pool_command.pool_get, "airflowctl pools get"), (pool_command.pool_set, "airflowctl pools create"), From f58e517900f5dcbf9a2259a55ad804f1e8f420ca Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Fri, 3 Jul 2026 19:21:27 +0200 Subject: [PATCH 015/297] [v3-3-test] [AIP-94] Mark variables CLI commands as migrated to airflowctl (#68932) (#69127) (cherry picked from commit b74521b54885092613439fa5dbf1e50472dcd9e9) Signed-off-by: PoAn Yang Co-authored-by: PoAn Yang --- airflow-core/src/airflow/cli/commands/variable_command.py | 7 ++++++- .../tests/unit/cli/commands/test_command_deprecations.py | 6 ++++++ 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/airflow-core/src/airflow/cli/commands/variable_command.py b/airflow-core/src/airflow/cli/commands/variable_command.py index 5216aabb446c6..194b02b529a97 100644 --- a/airflow-core/src/airflow/cli/commands/variable_command.py +++ b/airflow-core/src/airflow/cli/commands/variable_command.py @@ -26,7 +26,7 @@ from sqlalchemy import select from airflow.cli.simple_table import AirflowConsole -from airflow.cli.utils import SENSITIVE_PLACEHOLDER, print_export_output +from airflow.cli.utils import SENSITIVE_PLACEHOLDER, deprecated_for_airflowctl, print_export_output from airflow.exceptions import ( AirflowFileParseException, AirflowUnsupportedFileTypeException, @@ -63,6 +63,7 @@ def with_values(var, hide_sensitive: bool = False) -> dict[str, str]: return {"key": key, "val": val} +@deprecated_for_airflowctl("airflowctl variables list") @suppress_logs_and_warning @providers_configuration_loaded def variables_list(args): @@ -92,6 +93,7 @@ def _mapper(var): AirflowConsole().print_as(data=variables, output=args.output, mapper=None) +@deprecated_for_airflowctl("airflowctl variables get") @suppress_logs_and_warning @providers_configuration_loaded def variables_get(args): @@ -108,6 +110,7 @@ def variables_get(args): @cli_utils.action_cli +@deprecated_for_airflowctl("airflowctl variables create") @providers_configuration_loaded def variables_set(args): """Create new variable with a given name, value and description.""" @@ -116,6 +119,7 @@ def variables_set(args): @cli_utils.action_cli +@deprecated_for_airflowctl("airflowctl variables delete") @providers_configuration_loaded def variables_delete(args): """Delete variable by a given name.""" @@ -124,6 +128,7 @@ def variables_delete(args): @cli_utils.action_cli +@deprecated_for_airflowctl("airflowctl variables import") @providers_configuration_loaded @provide_session def variables_import(args, *, session: Session = NEW_SESSION): diff --git a/airflow-core/tests/unit/cli/commands/test_command_deprecations.py b/airflow-core/tests/unit/cli/commands/test_command_deprecations.py index 44f23c111196c..411cb6a84f14e 100644 --- a/airflow-core/tests/unit/cli/commands/test_command_deprecations.py +++ b/airflow-core/tests/unit/cli/commands/test_command_deprecations.py @@ -36,6 +36,7 @@ dag_command, pool_command, provider_command, + variable_command, ) # (command callable, expected airflowctl replacement recorded by the decorator) @@ -49,6 +50,11 @@ (pool_command.pool_delete, "airflowctl pools delete"), (pool_command.pool_import, "airflowctl pools import"), (pool_command.pool_export, "airflowctl pools export"), + (variable_command.variables_list, "airflowctl variables list"), + (variable_command.variables_get, "airflowctl variables get"), + (variable_command.variables_set, "airflowctl variables create"), + (variable_command.variables_delete, "airflowctl variables delete"), + (variable_command.variables_import, "airflowctl variables import"), (asset_command.asset_materialize, "airflowctl assets materialize"), (provider_command.provider_get, "airflowctl providers get"), (provider_command.providers_list, "airflowctl providers list"), From 1d195637b11aad614bff694d35ebb0436646a935 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Fri, 3 Jul 2026 20:16:21 +0200 Subject: [PATCH 016/297] [v3-3-test] Automate stable REST API permission reference doc generation (#67606) (#69334) * Generate stable REST API permission reference docs automatically * Fix mypy typing issue in permission extractor * chore: rerun flaky CI * Add prek hook and sort generated API permission docs * Potential fix for pull request finding * Handle positional DAG access entity extraction * Include public API endpoints in permission reference docs * Update generated REST API permission reference * Fix multi-router path extraction in permission docs --------- (cherry picked from commit 63b98266c9e1e80c173983d52d8504aca3b3a5f1) Co-authored-by: Durgaprasad M L Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- .pre-commit-config.yaml | 11 + .../docs/security/api_permissions_ref.rst | 576 ++++++++++++ scripts/ci/prek/extract_permissions.py | 550 ++++++++++++ .../tests/ci/prek/test_extract_permissions.py | 832 ++++++++++++++++++ 4 files changed, 1969 insertions(+) create mode 100644 airflow-core/docs/security/api_permissions_ref.rst create mode 100644 scripts/ci/prek/extract_permissions.py create mode 100644 scripts/tests/ci/prek/test_extract_permissions.py diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index f3461ed417cd4..95bb77f184c62 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -644,6 +644,17 @@ repos: language: python files: ^airflow-core/docs/installation/supported-versions\.rst$|^scripts/ci/prek/supported_versions\.py$|^README\.md$ pass_filenames: false + - id: generate-api-permissions-doc + name: Generate REST API permission reference documentation + entry: ./scripts/ci/prek/extract_permissions.py + language: python + files: > + (?x) + ^airflow-core/src/airflow/api_fastapi/core_api/routes/public/.*\.py$| + ^airflow-core/src/airflow/api_fastapi/core_api/security\.py$| + ^scripts/ci/prek/extract_permissions\.py$| + ^airflow-core/docs/security/api_permissions_ref\.rst$ + pass_filenames: false - id: check-revision-heads-map name: Check that the REVISION_HEADS_MAP is up-to-date language: python diff --git a/airflow-core/docs/security/api_permissions_ref.rst b/airflow-core/docs/security/api_permissions_ref.rst new file mode 100644 index 0000000000000..11097ad5a422e --- /dev/null +++ b/airflow-core/docs/security/api_permissions_ref.rst @@ -0,0 +1,576 @@ + .. Licensed to the Apache Software Foundation (ASF) under one + or more contributor license agreements. See the NOTICE file + distributed with this work for additional information + regarding copyright ownership. The ASF licenses this file + to you under the Apache License, Version 2.0 (the + "License"); you may not use this file except in compliance + with the License. You may obtain a copy of the License at + + .. http://www.apache.org/licenses/LICENSE-2.0 + + .. Unless required by applicable law or agreed to in writing, + software distributed under the License is distributed on an + "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + KIND, either express or implied. See the License for the + specific language governing permissions and limitations + under the License. + +.. THIS FILE IS AUTO-GENERATED. DO NOT EDIT MANUALLY. + Regenerate with: python scripts/ci/prek/extract_permissions.py + Trigger: prek run generate-api-permissions-doc --all-files + +API Endpoint Permission Reference +================================== + +This page lists the required permission for every endpoint in the stable +Airflow REST API (``/api/v2``). It is generated automatically from the +source code so it stays up to date as endpoints are added or changed. + +.. seealso:: + + :doc:`/security/api` — for authentication instructions (JWT tokens). + +.. note:: + + Permissions are enforced by the configured **auth manager**. The + :class:`~airflow.api_fastapi.auth.managers.base_auth_manager.BaseAuthManager` + interface defines the contract; individual auth manager implementations + (e.g. the Simple Auth Manager, or the FAB provider) translate these + resource/method tuples into their own role/permission models. + +.. list-table:: Stable REST API endpoint permissions + :header-rows: 1 + :widths: 7 50 20 13 + + * - Method + - Endpoint path + - Resource + - Required permission + * - ``GET`` + - ``/api/v2/assets`` + - ``Asset`` + - ``GET`` + * - ``GET`` + - ``/api/v2/assets`` + - ``AssetAlias`` + - ``GET`` + * - ``GET`` + - ``/api/v2/assets/aliases`` + - ``AssetAlias`` + - ``GET`` + * - ``GET`` + - ``/api/v2/assets/aliases/{asset_alias_id}`` + - ``AssetAlias`` + - ``GET`` + * - ``GET`` + - ``/api/v2/assets/events`` + - ``Asset`` + - ``GET`` + * - ``POST`` + - ``/api/v2/assets/events`` + - ``Asset`` + - ``POST`` + * - ``GET`` + - ``/api/v2/assets/{asset_id}`` + - ``Asset`` + - ``GET`` + * - ``GET`` + - ``/api/v2/assets/{asset_id}`` + - ``AssetAlias`` + - ``GET`` + * - ``POST`` + - ``/api/v2/assets/{asset_id}/materialize`` + - ``Asset`` + - ``POST`` + * - ``DELETE`` + - ``/api/v2/assets/{asset_id}/queuedEvents`` + - ``Asset`` + - ``DELETE`` + * - ``DELETE`` + - ``/api/v2/assets/{asset_id}/queuedEvents`` + - ``DAG`` + - ``GET`` + * - ``GET`` + - ``/api/v2/assets/{asset_id}/queuedEvents`` + - ``Asset`` + - ``GET`` + * - ``DELETE`` + - ``/api/v2/assets/{asset_id}/state-store`` + - ``Asset`` + - ``DELETE`` + * - ``GET`` + - ``/api/v2/assets/{asset_id}/state-store`` + - ``Asset`` + - ``GET`` + * - ``DELETE`` + - ``/api/v2/assets/{asset_id}/state-store/{key:path}`` + - ``Asset`` + - ``DELETE`` + * - ``GET`` + - ``/api/v2/assets/{asset_id}/state-store/{key:path}`` + - ``Asset`` + - ``GET`` + * - ``PUT`` + - ``/api/v2/assets/{asset_id}/state-store/{key:path}`` + - ``Asset`` + - ``PUT`` + * - ``GET`` + - ``/api/v2/auth/login`` + - ``Public`` + - ``No Airflow permission required`` + * - ``GET`` + - ``/api/v2/auth/logout`` + - ``Public`` + - ``No Airflow permission required`` + * - ``GET`` + - ``/api/v2/backfills`` + - ``DAG.RUN`` + - ``GET`` + * - ``POST`` + - ``/api/v2/backfills`` + - ``DAG.RUN`` + - ``POST`` + * - ``PUT`` + - ``/api/v2/backfills`` + - ``DAG.RUN`` + - ``PUT`` + * - ``GET`` + - ``/api/v2/config`` + - ``Configuration`` + - ``GET`` + * - ``GET`` + - ``/api/v2/config/section/{section}/option/{option}`` + - ``Configuration`` + - ``GET`` + * - ``GET`` + - ``/api/v2/connections`` + - ``Connection`` + - ``GET`` + * - ``PATCH`` + - ``/api/v2/connections`` + - ``Connection`` + - ``multi`` + * - ``POST`` + - ``/api/v2/connections`` + - ``Connection`` + - ``POST`` + * - ``POST`` + - ``/api/v2/connections/defaults`` + - ``Connection`` + - ``POST`` + * - ``GET`` + - ``/api/v2/connections/enqueue-test`` + - ``Public`` + - ``No Airflow permission required`` + * - ``POST`` + - ``/api/v2/connections/enqueue-test`` + - ``Public`` + - ``No Airflow permission required`` + * - ``POST`` + - ``/api/v2/connections/test`` + - ``Connection`` + - ``POST`` + * - ``DELETE`` + - ``/api/v2/connections/{connection_id}`` + - ``Connection`` + - ``DELETE`` + * - ``GET`` + - ``/api/v2/connections/{connection_id}`` + - ``Connection`` + - ``GET`` + * - ``PATCH`` + - ``/api/v2/connections/{connection_id}`` + - ``Connection`` + - ``PUT`` + * - ``GET`` + - ``/api/v2/dagSources/{dag_id}`` + - ``DAG.CODE`` + - ``GET`` + * - ``GET`` + - ``/api/v2/dagStats`` + - ``DAG.RUN`` + - ``GET`` + * - ``GET`` + - ``/api/v2/dagTags`` + - ``DAG`` + - ``GET`` + * - ``GET`` + - ``/api/v2/dagWarnings`` + - ``DAG.WARNING`` + - ``GET`` + * - ``GET`` + - ``/api/v2/dags`` + - ``DAG`` + - ``GET`` + * - ``PATCH`` + - ``/api/v2/dags`` + - ``DAG`` + - ``PUT`` + * - ``DELETE`` + - ``/api/v2/dags/{dag_id}`` + - ``DAG`` + - ``DELETE`` + * - ``GET`` + - ``/api/v2/dags/{dag_id}`` + - ``DAG`` + - ``GET`` + * - ``PATCH`` + - ``/api/v2/dags/{dag_id}`` + - ``DAG`` + - ``PUT`` + * - ``DELETE`` + - ``/api/v2/dags/{dag_id}/assets/queuedEvents`` + - ``Asset`` + - ``DELETE`` + * - ``DELETE`` + - ``/api/v2/dags/{dag_id}/assets/queuedEvents`` + - ``DAG`` + - ``GET`` + * - ``GET`` + - ``/api/v2/dags/{dag_id}/assets/queuedEvents`` + - ``Asset`` + - ``GET`` + * - ``GET`` + - ``/api/v2/dags/{dag_id}/assets/queuedEvents`` + - ``DAG`` + - ``GET`` + * - ``DELETE`` + - ``/api/v2/dags/{dag_id}/assets/{asset_id}/queuedEvents`` + - ``Asset`` + - ``DELETE`` + * - ``DELETE`` + - ``/api/v2/dags/{dag_id}/assets/{asset_id}/queuedEvents`` + - ``DAG`` + - ``GET`` + * - ``GET`` + - ``/api/v2/dags/{dag_id}/assets/{asset_id}/queuedEvents`` + - ``Asset`` + - ``GET`` + * - ``GET`` + - ``/api/v2/dags/{dag_id}/assets/{asset_id}/queuedEvents`` + - ``DAG`` + - ``GET`` + * - ``POST`` + - ``/api/v2/dags/{dag_id}/clearDagRuns`` + - ``DAG.RUN`` + - ``multi`` + * - ``POST`` + - ``/api/v2/dags/{dag_id}/clearPartitions`` + - ``DAG.RUN`` + - ``PUT`` + * - ``POST`` + - ``/api/v2/dags/{dag_id}/clearTaskInstances`` + - ``DAG.TASK_INSTANCE`` + - ``PUT`` + * - ``GET`` + - ``/api/v2/dags/{dag_id}/dagRuns`` + - ``DAG.RUN`` + - ``GET`` + * - ``PATCH`` + - ``/api/v2/dags/{dag_id}/dagRuns`` + - ``DAG.RUN`` + - ``multi`` + * - ``POST`` + - ``/api/v2/dags/{dag_id}/dagRuns`` + - ``DAG.RUN`` + - ``POST`` + * - ``POST`` + - ``/api/v2/dags/{dag_id}/dagRuns/list`` + - ``DAG.RUN`` + - ``GET`` + * - ``DELETE`` + - ``/api/v2/dags/{dag_id}/dagRuns/{dag_run_id}`` + - ``DAG.RUN`` + - ``DELETE`` + * - ``GET`` + - ``/api/v2/dags/{dag_id}/dagRuns/{dag_run_id}`` + - ``DAG.RUN`` + - ``GET`` + * - ``PATCH`` + - ``/api/v2/dags/{dag_id}/dagRuns/{dag_run_id}`` + - ``DAG.RUN`` + - ``PUT`` + * - ``POST`` + - ``/api/v2/dags/{dag_id}/dagRuns/{dag_run_id}/clear`` + - ``DAG.RUN`` + - ``PUT`` + * - ``GET`` + - ``/api/v2/dags/{dag_id}/dagRuns/{dag_run_id}/hitlDetails`` + - ``DAG.HITL_DETAIL`` + - ``GET`` + * - ``PATCH`` + - ``/api/v2/dags/{dag_id}/dagRuns/{dag_run_id}/taskGroupInstances/{group_id}`` + - ``DAG.TASK_INSTANCE`` + - ``PUT`` + * - ``PATCH`` + - ``/api/v2/dags/{dag_id}/dagRuns/{dag_run_id}/taskGroupInstances/{group_id}/dry_run`` + - ``DAG.TASK_INSTANCE`` + - ``PUT`` + * - ``GET`` + - ``/api/v2/dags/{dag_id}/dagRuns/{dag_run_id}/taskInstances`` + - ``DAG.TASK_INSTANCE`` + - ``GET`` + * - ``PATCH`` + - ``/api/v2/dags/{dag_id}/dagRuns/{dag_run_id}/taskInstances`` + - ``DAG.TASK_INSTANCE`` + - ``PUT`` + * - ``POST`` + - ``/api/v2/dags/{dag_id}/dagRuns/{dag_run_id}/taskInstances/list`` + - ``DAG.TASK_INSTANCE`` + - ``GET`` + * - ``DELETE`` + - ``/api/v2/dags/{dag_id}/dagRuns/{dag_run_id}/taskInstances/{task_id}`` + - ``DAG.TASK_INSTANCE`` + - ``DELETE`` + * - ``GET`` + - ``/api/v2/dags/{dag_id}/dagRuns/{dag_run_id}/taskInstances/{task_id}`` + - ``DAG.TASK_INSTANCE`` + - ``GET`` + * - ``PATCH`` + - ``/api/v2/dags/{dag_id}/dagRuns/{dag_run_id}/taskInstances/{task_id}`` + - ``DAG.TASK_INSTANCE`` + - ``PUT`` + * - ``GET`` + - ``/api/v2/dags/{dag_id}/dagRuns/{dag_run_id}/taskInstances/{task_id}/dependencies`` + - ``DAG.TASK_INSTANCE`` + - ``GET`` + * - ``PATCH`` + - ``/api/v2/dags/{dag_id}/dagRuns/{dag_run_id}/taskInstances/{task_id}/dry_run`` + - ``DAG.TASK_INSTANCE`` + - ``PUT`` + * - ``GET`` + - ``/api/v2/dags/{dag_id}/dagRuns/{dag_run_id}/taskInstances/{task_id}/externalLogUrl/{try_number}`` + - ``DAG.TASK_LOGS`` + - ``GET`` + * - ``GET`` + - ``/api/v2/dags/{dag_id}/dagRuns/{dag_run_id}/taskInstances/{task_id}/links`` + - ``DAG.TASK_INSTANCE`` + - ``GET`` + * - ``GET`` + - ``/api/v2/dags/{dag_id}/dagRuns/{dag_run_id}/taskInstances/{task_id}/listMapped`` + - ``DAG.TASK_INSTANCE`` + - ``GET`` + * - ``GET`` + - ``/api/v2/dags/{dag_id}/dagRuns/{dag_run_id}/taskInstances/{task_id}/logs/{try_number}`` + - ``DAG.TASK_LOGS`` + - ``GET`` + * - ``DELETE`` + - ``/api/v2/dags/{dag_id}/dagRuns/{dag_run_id}/taskInstances/{task_id}/state-store`` + - ``DAG.TASK_INSTANCE`` + - ``DELETE`` + * - ``GET`` + - ``/api/v2/dags/{dag_id}/dagRuns/{dag_run_id}/taskInstances/{task_id}/state-store`` + - ``DAG.TASK_INSTANCE`` + - ``GET`` + * - ``DELETE`` + - ``/api/v2/dags/{dag_id}/dagRuns/{dag_run_id}/taskInstances/{task_id}/state-store/{key:path}`` + - ``DAG.TASK_INSTANCE`` + - ``DELETE`` + * - ``GET`` + - ``/api/v2/dags/{dag_id}/dagRuns/{dag_run_id}/taskInstances/{task_id}/state-store/{key:path}`` + - ``DAG.TASK_INSTANCE`` + - ``GET`` + * - ``PATCH`` + - ``/api/v2/dags/{dag_id}/dagRuns/{dag_run_id}/taskInstances/{task_id}/state-store/{key:path}`` + - ``DAG.TASK_INSTANCE`` + - ``PUT`` + * - ``PUT`` + - ``/api/v2/dags/{dag_id}/dagRuns/{dag_run_id}/taskInstances/{task_id}/state-store/{key:path}`` + - ``DAG.TASK_INSTANCE`` + - ``PUT`` + * - ``GET`` + - ``/api/v2/dags/{dag_id}/dagRuns/{dag_run_id}/taskInstances/{task_id}/tries`` + - ``DAG.TASK_INSTANCE`` + - ``GET`` + * - ``GET`` + - ``/api/v2/dags/{dag_id}/dagRuns/{dag_run_id}/taskInstances/{task_id}/tries/{task_try_number}`` + - ``DAG.TASK_INSTANCE`` + - ``GET`` + * - ``GET`` + - ``/api/v2/dags/{dag_id}/dagRuns/{dag_run_id}/taskInstances/{task_id}/xcomEntries`` + - ``DAG.XCOM`` + - ``GET`` + * - ``POST`` + - ``/api/v2/dags/{dag_id}/dagRuns/{dag_run_id}/taskInstances/{task_id}/xcomEntries`` + - ``DAG.XCOM`` + - ``POST`` + * - ``DELETE`` + - ``/api/v2/dags/{dag_id}/dagRuns/{dag_run_id}/taskInstances/{task_id}/xcomEntries/{xcom_key:path}`` + - ``DAG.XCOM`` + - ``DELETE`` + * - ``GET`` + - ``/api/v2/dags/{dag_id}/dagRuns/{dag_run_id}/taskInstances/{task_id}/xcomEntries/{xcom_key:path}`` + - ``DAG.XCOM`` + - ``GET`` + * - ``PATCH`` + - ``/api/v2/dags/{dag_id}/dagRuns/{dag_run_id}/taskInstances/{task_id}/xcomEntries/{xcom_key:path}`` + - ``DAG.XCOM`` + - ``PUT`` + * - ``GET`` + - ``/api/v2/dags/{dag_id}/dagRuns/{dag_run_id}/taskInstances/{task_id}/{map_index}`` + - ``DAG.TASK_INSTANCE`` + - ``GET`` + * - ``PATCH`` + - ``/api/v2/dags/{dag_id}/dagRuns/{dag_run_id}/taskInstances/{task_id}/{map_index}`` + - ``DAG.TASK_INSTANCE`` + - ``PUT`` + * - ``GET`` + - ``/api/v2/dags/{dag_id}/dagRuns/{dag_run_id}/taskInstances/{task_id}/{map_index}/dependencies`` + - ``DAG.TASK_INSTANCE`` + - ``GET`` + * - ``PATCH`` + - ``/api/v2/dags/{dag_id}/dagRuns/{dag_run_id}/taskInstances/{task_id}/{map_index}/dry_run`` + - ``DAG.TASK_INSTANCE`` + - ``PUT`` + * - ``GET`` + - ``/api/v2/dags/{dag_id}/dagRuns/{dag_run_id}/taskInstances/{task_id}/{map_index}/hitlDetails`` + - ``DAG.HITL_DETAIL`` + - ``GET`` + * - ``PATCH`` + - ``/api/v2/dags/{dag_id}/dagRuns/{dag_run_id}/taskInstances/{task_id}/{map_index}/hitlDetails`` + - ``DAG.HITL_DETAIL`` + - ``PUT`` + * - ``GET`` + - ``/api/v2/dags/{dag_id}/dagRuns/{dag_run_id}/taskInstances/{task_id}/{map_index}/hitlDetails/tries/{try_number}`` + - ``DAG.HITL_DETAIL`` + - ``GET`` + * - ``GET`` + - ``/api/v2/dags/{dag_id}/dagRuns/{dag_run_id}/taskInstances/{task_id}/{map_index}/tries`` + - ``DAG.TASK_INSTANCE`` + - ``GET`` + * - ``GET`` + - ``/api/v2/dags/{dag_id}/dagRuns/{dag_run_id}/taskInstances/{task_id}/{map_index}/tries/{task_try_number}`` + - ``DAG.TASK_INSTANCE`` + - ``GET`` + * - ``GET`` + - ``/api/v2/dags/{dag_id}/dagRuns/{dag_run_id}/upstreamAssetEvents`` + - ``Asset`` + - ``GET`` + * - ``GET`` + - ``/api/v2/dags/{dag_id}/dagRuns/{dag_run_id}/upstreamAssetEvents`` + - ``DAG.RUN`` + - ``GET`` + * - ``GET`` + - ``/api/v2/dags/{dag_id}/dagRuns/{dag_run_id}/wait`` + - ``DAG.RUN`` + - ``GET`` + * - ``GET`` + - ``/api/v2/dags/{dag_id}/dagVersions`` + - ``DAG.VERSION`` + - ``GET`` + * - ``GET`` + - ``/api/v2/dags/{dag_id}/dagVersions/{version_number}`` + - ``DAG.VERSION`` + - ``GET`` + * - ``GET`` + - ``/api/v2/dags/{dag_id}/details`` + - ``DAG`` + - ``GET`` + * - ``POST`` + - ``/api/v2/dags/{dag_id}/favorite`` + - ``DAG`` + - ``GET`` + * - ``GET`` + - ``/api/v2/dags/{dag_id}/tasks`` + - ``DAG.TASK`` + - ``GET`` + * - ``GET`` + - ``/api/v2/dags/{dag_id}/tasks/{task_id}`` + - ``DAG.TASK`` + - ``GET`` + * - ``POST`` + - ``/api/v2/dags/{dag_id}/unfavorite`` + - ``DAG`` + - ``GET`` + * - ``GET`` + - ``/api/v2/eventLogs`` + - ``DAG.AUDIT_LOG`` + - ``GET`` + * - ``GET`` + - ``/api/v2/eventLogs/{event_log_id}`` + - ``DAG.AUDIT_LOG`` + - ``GET`` + * - ``GET`` + - ``/api/v2/importErrors`` + - ``View.IMPORT_ERRORS`` + - ``IMPORT_ERRORS`` + * - ``GET`` + - ``/api/v2/importErrors/{import_error_id}`` + - ``View.IMPORT_ERRORS`` + - ``IMPORT_ERRORS`` + * - ``GET`` + - ``/api/v2/jobs`` + - ``View.JOBS`` + - ``JOBS`` + * - ``GET`` + - ``/api/v2/monitor/health`` + - ``Public`` + - ``No Airflow permission required`` + * - ``PUT`` + - ``/api/v2/parseDagFile/{file_token}`` + - ``DAG`` + - ``PUT`` + * - ``GET`` + - ``/api/v2/plugins`` + - ``View.PLUGINS`` + - ``PLUGINS`` + * - ``GET`` + - ``/api/v2/plugins/importErrors`` + - ``View.PLUGINS`` + - ``PLUGINS`` + * - ``GET`` + - ``/api/v2/pools`` + - ``Pool`` + - ``GET`` + * - ``PATCH`` + - ``/api/v2/pools`` + - ``Pool`` + - ``multi`` + * - ``POST`` + - ``/api/v2/pools`` + - ``Pool`` + - ``POST`` + * - ``DELETE`` + - ``/api/v2/pools/{pool_name:path}`` + - ``Pool`` + - ``DELETE`` + * - ``GET`` + - ``/api/v2/pools/{pool_name:path}`` + - ``Pool`` + - ``GET`` + * - ``PATCH`` + - ``/api/v2/pools/{pool_name:path}`` + - ``Pool`` + - ``PUT`` + * - ``GET`` + - ``/api/v2/providers`` + - ``View.PROVIDERS`` + - ``PROVIDERS`` + * - ``GET`` + - ``/api/v2/variables`` + - ``Variable`` + - ``GET`` + * - ``PATCH`` + - ``/api/v2/variables`` + - ``Variable`` + - ``multi`` + * - ``POST`` + - ``/api/v2/variables`` + - ``Variable`` + - ``POST`` + * - ``DELETE`` + - ``/api/v2/variables/{variable_key:path}`` + - ``Variable`` + - ``DELETE`` + * - ``GET`` + - ``/api/v2/variables/{variable_key:path}`` + - ``Variable`` + - ``GET`` + * - ``PATCH`` + - ``/api/v2/variables/{variable_key:path}`` + - ``Variable`` + - ``PUT`` + * - ``GET`` + - ``/api/v2/version`` + - ``Public`` + - ``No Airflow permission required`` diff --git a/scripts/ci/prek/extract_permissions.py b/scripts/ci/prek/extract_permissions.py new file mode 100644 index 0000000000000..e9e454fe29907 --- /dev/null +++ b/scripts/ci/prek/extract_permissions.py @@ -0,0 +1,550 @@ +#!/usr/bin/env python +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. +""" +Extract permission requirements from FastAPI routes in Airflow REST API. + +This script statically parses FastAPI route files under airflow-core's public REST API +routes to extract required permissions for each endpoint. It generates a reference +RST documentation file for security/api_permissions_ref.rst. + +It runs completely statically using Python's built-in AST parser, requiring no runtime +Airflow imports or active execution environment, making it suitable for CI checks. +""" + +from __future__ import annotations + +import ast +import pathlib +import sys +from dataclasses import dataclass + +# --------------------------------------------------------------------------- +# Paths (all relative to the repo root, resolved from this file's location) +# --------------------------------------------------------------------------- +REPO_ROOT = pathlib.Path(__file__).resolve().parents[3] +PUBLIC_ROUTES_DIR = REPO_ROOT / "airflow-core/src/airflow/api_fastapi/core_api/routes/public" +OUTPUT_RST = REPO_ROOT / "airflow-core/docs/security/api_permissions_ref.rst" + +# The global /api/v2 prefix comes from public_router in __init__.py +API_PREFIX = "/api/v2" + + +# --------------------------------------------------------------------------- +# Data model +# --------------------------------------------------------------------------- +@dataclass(frozen=True, order=True) +class PermissionEntry: + """One HTTP operation's permission requirement.""" + + full_path: str # full route path, e.g. /api/v2/dags/{dag_id} + http_method: str # GET / POST / PATCH / PUT / DELETE + tag: str # OpenAPI tag, e.g. "DAG", "Variable" + resource: str # e.g. "DAG", "DAG.RUN", "Variable", "View" + required_permission: str # e.g. "GET", "POST", "DELETE", "multi", "PLUGINS" + source_file: str # route file basename for traceability + + +# --------------------------------------------------------------------------- +# Per-file AST helpers +# --------------------------------------------------------------------------- + + +def _resolve_string_node(node: ast.expr, module_consts: dict[str, str]) -> str: + """ + Convert an AST expression to a string. + + Handles: + - ast.Constant → direct string + - ast.BinOp(+) → resolve left and right recursively (string concat) + - ast.Name → look up in module_consts if available + """ + if isinstance(node, ast.Constant) and isinstance(node.value, str): + return node.value + if isinstance(node, ast.BinOp) and isinstance(node.op, ast.Add): + left = _resolve_string_node(node.left, module_consts) + right = _resolve_string_node(node.right, module_consts) + return left + right + if isinstance(node, ast.Name) and node.id in module_consts: + return module_consts[node.id] + # Give up — return an unresolvable marker (will surface in tests) + return f"" + + +def _extract_module_string_constants(tree: ast.Module) -> dict[str, str]: + """ + Walk top-level assignments and collect simple string assignments. + + e.g. task_instances_prefix = "/dagRuns/{dag_run_id}/taskInstances" + → {"task_instances_prefix": "/dagRuns/{dag_run_id}/taskInstances"} + """ + consts: dict[str, str] = {} + for node in tree.body: + if ( + isinstance(node, ast.Assign) + and len(node.targets) == 1 + and isinstance(node.targets[0], ast.Name) + and isinstance(node.value, ast.Constant) + and isinstance(node.value.value, str) + ): + consts[node.targets[0].id] = node.value.value + return consts + + +def _extract_routers(tree: ast.Module) -> dict[str, str]: + """ + Find all assignments like some_router = AirflowRouter(...) at module level. + + Returns a mapping of router variable name to its prefix. + """ + routers: dict[str, str] = {} + for node in tree.body: + if not ( + isinstance(node, ast.Assign) + and len(node.targets) == 1 + and isinstance(node.targets[0], ast.Name) + and isinstance(node.value, ast.Call) + ): + continue + call = node.value + call_name = ( + call.func.id + if isinstance(call.func, ast.Name) + else call.func.attr + if isinstance(call.func, ast.Attribute) + else "" + ) + if call_name != "AirflowRouter": + continue + + target_name = node.targets[0].id + prefix = "" + for kw in call.keywords: + if kw.arg == "prefix" and isinstance(kw.value, ast.Constant) and isinstance(kw.value.value, str): + prefix = kw.value.value + elif kw.arg == "dependencies" and isinstance(kw.value, ast.List): + for dep_item in kw.value.elts: + for subnode in ast.walk(dep_item): + if isinstance(subnode, ast.Call): + fn_name = _get_requires_access_call_name(subnode) + if fn_name is not None: + raise ValueError( + f"Unsupported extraction semantics: Router-level permission dependency '{fn_name}' " + f"on router '{target_name}' is not supported by the static permission extractor." + ) + routers[target_name] = prefix + return routers + + +def _get_requires_access_call_name(call_node: ast.Call) -> str | None: + """Extract the function name from a requires_access_*() call node.""" + fn = call_node.func + if isinstance(fn, ast.Name) and fn.id.startswith("requires_access"): + return fn.id + if isinstance(fn, ast.Attribute) and fn.attr.startswith("requires_access"): + return fn.attr + return None + + +def _extract_method_arg(call_node: ast.Call) -> str: + """ + Extract the HTTP method from a requires_access_*(...) call. + + Two calling conventions exist in the codebase: + requires_access_dag("GET", ...) ← positional + requires_access_dag(method="GET", ...) ← keyword + + Returns the method string (GET/POST/PUT/DELETE) or "multi" + for bulk functions that carry no method. + """ + # Positional first arg + if call_node.args: + first = call_node.args[0] + if isinstance(first, ast.Constant) and isinstance(first.value, str): + return first.value.upper() + return ast.unparse(first).strip("\"'").upper() + + # Keyword method= + for kw in call_node.keywords: + if kw.arg == "method": + val = kw.value + if isinstance(val, ast.Constant) and isinstance(val.value, str): + return val.value.upper() + return ast.unparse(val).strip("\"'").upper() + + # bulk functions: no method arg + return "multi" + + +def _extract_entity_arg(call_node: ast.Call) -> str | None: + """ + Extract the access_entity or first positional (for requires_access_view). + + Returns e.g. "TASK_INSTANCE", "PLUGINS", or None. + """ + fn_name = _get_requires_access_call_name(call_node) or "" + + # For requires_access_view the entity IS the first positional arg + if fn_name == "requires_access_view": + for kw in call_node.keywords: + if kw.arg == "access_view": + return ast.unparse(kw.value).split(".")[-1] # AccessView.PLUGINS → "PLUGINS" + if call_node.args: + return ast.unparse(call_node.args[0]).split(".")[-1] + return None + + # For requires_access_dag the entity is the access_entity keyword + # or second positional argument + if fn_name == "requires_access_dag": + for kw in call_node.keywords: + if kw.arg == "access_entity": + return ast.unparse(kw.value).split(".")[-1] # DagAccessEntity.RUN → "RUN" + + if len(call_node.args) >= 2: + return ast.unparse(call_node.args[1]).split(".")[-1] + + return None + + return None + + +# Map from requires_access_* function name → (resource base name, forced entity or None) +_FN_TO_RESOURCE_INFO: dict[str, tuple[str, str | None]] = { + "requires_access_dag": ("DAG", None), + "requires_access_backfill": ("DAG", "RUN"), # backfill is a DAG.RUN alias + "requires_access_dag_run_bulk": ("DAG", "RUN"), # dag_run bulk is a DAG.RUN alias + "requires_access_dag_run_clear_bulk": ("DAG", "RUN"), # dag_run clear bulk is a DAG.RUN alias + "requires_access_event_log": ("DAG", "AUDIT_LOG"), # event log is a DAG.AUDIT_LOG alias + "requires_access_pool": ("Pool", None), + "requires_access_pool_bulk": ("Pool", None), + "requires_access_connection": ("Connection", None), + "requires_access_connection_bulk": ("Connection", None), + "requires_access_configuration": ("Configuration", None), + "requires_access_variable": ("Variable", None), + "requires_access_variable_bulk": ("Variable", None), + "requires_access_asset": ("Asset", None), + "requires_access_asset_alias": ("AssetAlias", None), + "requires_access_view": ("View", None), +} + + +def _build_resource_label(fn_name: str, entity: str | None) -> str: + """Convert fn_name + entity into a human-readable resource label.""" + if fn_name in _FN_TO_RESOURCE_INFO: + base, forced_entity = _FN_TO_RESOURCE_INFO[fn_name] + entity_to_use = forced_entity or entity + if entity_to_use: + return f"{base}.{entity_to_use}" + return base + return fn_name + + +def _extract_tag_from_decorator(decorator: ast.Call) -> str: + """Get the OpenAPI tag from @router.get(tags=["Tag"]) if present.""" + for kw in decorator.keywords: + if kw.arg == "tags" and isinstance(kw.value, ast.List): + for elt in kw.value.elts: + if isinstance(elt, ast.Constant): + return str(elt.value) + return "?" + + +# --------------------------------------------------------------------------- +# Core extraction per file +# --------------------------------------------------------------------------- + + +def extract_from_file(path: pathlib.Path) -> list[PermissionEntry]: + """Parse one route file and return all PermissionEntry objects.""" + try: + source = path.read_text(encoding="utf-8") + tree = ast.parse(source) + except (OSError, SyntaxError) as exc: + print(f"[WARN] Could not parse {path.name}: {exc}", file=sys.stderr) + return [] + + # Build lookup tables for this file + module_consts = _extract_module_string_constants(tree) + routers = _extract_routers(tree) + + results: list[PermissionEntry] = [] + + for node in ast.walk(tree): + if not isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef)): + continue + + for decorator in node.decorator_list: + if not isinstance(decorator, ast.Call): + continue + + # Determine HTTP method from decorator attribute: @router.GET / .get / .post … + if not isinstance(decorator.func, ast.Attribute): + continue + http_verb = decorator.func.attr.upper() + if http_verb not in {"GET", "POST", "PATCH", "PUT", "DELETE", "HEAD"}: + continue + + # Resolve the route path + route_suffix = "" + if decorator.args: + route_suffix = _resolve_string_node(decorator.args[0], module_consts) + + # Resolve the prefix based on the router variable used in the decorator + router_prefix = "" + if isinstance(decorator.func.value, ast.Name): + router_var = decorator.func.value.id + router_prefix = routers.get(router_var, "") + + full_path = API_PREFIX + router_prefix + route_suffix + + # Extract tag (for grouping in the RST table) + tag = _extract_tag_from_decorator(decorator) + + # Find dependencies=[...] kwarg + deps_kwarg = next( + (kw for kw in decorator.keywords if kw.arg == "dependencies"), + None, + ) + + has_permission_dependency = False + if deps_kwarg is not None and isinstance(deps_kwarg.value, ast.List): + # Walk the dependency list + for dep_item in deps_kwarg.value.elts: + if not isinstance(dep_item, ast.Call): + continue + # Must be Depends(...) + dep_name = ( + dep_item.func.id + if isinstance(dep_item.func, ast.Name) + else getattr(dep_item.func, "attr", "") + ) + if dep_name != "Depends" or not dep_item.args: + continue + + inner = dep_item.args[0] + if not isinstance(inner, ast.Call): + continue + + fn_name = _get_requires_access_call_name(inner) + if fn_name is None: + continue + + method = _extract_method_arg(inner) + entity = _extract_entity_arg(inner) + resource = _build_resource_label(fn_name, entity) + permission = entity if fn_name == "requires_access_view" else method + if not isinstance(permission, str): + raise ValueError( + f"Could not resolve required permission for {fn_name} in {path.name}" + ) + + results.append( + PermissionEntry( + http_method=http_verb, + full_path=full_path, + tag=tag, + resource=resource, + required_permission=permission, + source_file=path.name, + ) + ) + has_permission_dependency = True + + if not has_permission_dependency: + results.append( + PermissionEntry( + http_method=http_verb, + full_path=full_path, + tag=tag, + resource="Public", + required_permission="No Airflow permission required", + source_file=path.name, + ) + ) + + return results + + +# --------------------------------------------------------------------------- +# Main extraction entry point +# --------------------------------------------------------------------------- + + +def extract_all_permissions(routes_dir: pathlib.Path) -> list[PermissionEntry]: + """ + Walk all public route files and return a sorted, deduplicated list + of PermissionEntry objects. + """ + all_entries: list[PermissionEntry] = [] + for route_file in sorted(routes_dir.glob("*.py")): + if route_file.name == "__init__.py": + continue + all_entries.extend(extract_from_file(route_file)) + + # Deduplicate (same path+method+resource can appear from multiple deps) + seen: set[PermissionEntry] = set() + deduped: list[PermissionEntry] = [] + sorted_entries = sorted( + all_entries, + key=lambda e: ( + e.full_path, + e.http_method, + e.resource, + e.required_permission, + ), + ) + for entry in sorted_entries: + if entry not in seen: + seen.add(entry) + deduped.append(entry) + + return deduped + + +# --------------------------------------------------------------------------- +# RST generation +# --------------------------------------------------------------------------- + +RST_HEADER = """\ + .. Licensed to the Apache Software Foundation (ASF) under one + or more contributor license agreements. See the NOTICE file + distributed with this work for additional information + regarding copyright ownership. The ASF licenses this file + to you under the Apache License, Version 2.0 (the + "License"); you may not use this file except in compliance + with the License. You may obtain a copy of the License at + + .. http://www.apache.org/licenses/LICENSE-2.0 + + .. Unless required by applicable law or agreed to in writing, + software distributed under the License is distributed on an + "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + KIND, either express or implied. See the License for the + specific language governing permissions and limitations + under the License. + +.. THIS FILE IS AUTO-GENERATED. DO NOT EDIT MANUALLY. + Regenerate with: python scripts/ci/prek/extract_permissions.py + Trigger: prek run generate-api-permissions-doc --all-files + +API Endpoint Permission Reference +================================== + +This page lists the required permission for every endpoint in the stable +Airflow REST API (``/api/v2``). It is generated automatically from the +source code so it stays up to date as endpoints are added or changed. + +.. seealso:: + + :doc:`/security/api` — for authentication instructions (JWT tokens). + +.. note:: + + Permissions are enforced by the configured **auth manager**. The + :class:`~airflow.api_fastapi.auth.managers.base_auth_manager.BaseAuthManager` + interface defines the contract; individual auth manager implementations + (e.g. the Simple Auth Manager, or the FAB provider) translate these + resource/method tuples into their own role/permission models. + +""" + +RST_TABLE_HEADER = """\ +.. list-table:: Stable REST API endpoint permissions + :header-rows: 1 + :widths: 7 50 20 13 + + * - Method + - Endpoint path + - Resource + - Required permission +""" + + +def _rst_table_row(entry: PermissionEntry) -> str: + return ( + f" * - ``{entry.http_method}``\n" + f" - ``{entry.full_path}``\n" + f" - ``{entry.resource}``\n" + f" - ``{entry.required_permission}``\n" + ) + + +def render_rst(entries: list[PermissionEntry]) -> str: + """Render the full RST document from the list of PermissionEntry objects.""" + rows = "".join(_rst_table_row(e) for e in entries) + return RST_HEADER + RST_TABLE_HEADER + rows + + +# --------------------------------------------------------------------------- +# CLI +# --------------------------------------------------------------------------- + + +def main(argv: list[str] | None = None) -> int: + import argparse + + parser = argparse.ArgumentParser(description="Extract API permissions and write RST reference doc.") + parser.add_argument( + "--check", + action="store_true", + help=( + "Check mode: exit 1 if the generated content differs from " + f"what is on disk at {OUTPUT_RST}. " + "Use in CI to detect stale documentation." + ), + ) + parser.add_argument( + "--print", + dest="print_only", + action="store_true", + help="Print the generated RST to stdout instead of writing to disk.", + ) + args = parser.parse_args(argv) + + entries = extract_all_permissions(PUBLIC_ROUTES_DIR) + content = render_rst(entries) + + if args.print_only: + print(content) + return 0 + + if args.check: + if not OUTPUT_RST.exists(): + print( + f"[FAIL] {OUTPUT_RST} does not exist. Run: python scripts/ci/prek/extract_permissions.py", + file=sys.stderr, + ) + return 1 + existing = OUTPUT_RST.read_text(encoding="utf-8") + if existing != content: + print( + f"[FAIL] {OUTPUT_RST} is stale. Run: python scripts/ci/prek/extract_permissions.py", + file=sys.stderr, + ) + return 1 + print(f"[OK] {OUTPUT_RST} is up to date.") + return 0 + + # Write mode (default) + OUTPUT_RST.parent.mkdir(parents=True, exist_ok=True) + OUTPUT_RST.write_text(content, encoding="utf-8") + print(f"[OK] Written {len(entries)} entries to {OUTPUT_RST}") + return 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/scripts/tests/ci/prek/test_extract_permissions.py b/scripts/tests/ci/prek/test_extract_permissions.py new file mode 100644 index 0000000000000..40be37dff0d28 --- /dev/null +++ b/scripts/tests/ci/prek/test_extract_permissions.py @@ -0,0 +1,832 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. +""" +Tests for scripts/ci/prek/extract_permissions.py. + +Test strategy: + - Unit tests parse small synthetic code strings, never real route files. + This makes tests fast, self-contained, and immune to unrelated route changes. + - Integration tests call extract_all_permissions() against the real + routes/public directory to guard against regressions when routes change. + - No snapshot tests: we assert on invariants (no unresolved markers, + no duplicates, count ≥ known_minimum) rather than exact string equality. + +Run with (no Airflow env needed — extractor is stdlib-only): + uv run --project scripts pytest scripts/tests/ci/prek/test_extract_permissions.py -xvs +""" + +from __future__ import annotations + +import ast +import sys +import textwrap +from pathlib import Path + +import pytest + +# --------------------------------------------------------------------------- +# Add scripts/ci/prek to sys.path so we can import the extractor directly. +# --------------------------------------------------------------------------- +REPO_ROOT = Path(__file__).resolve().parents[4] # scripts/tests/ci/prek → repo root +PREK_DIR = REPO_ROOT / "scripts/ci/prek" + +if str(PREK_DIR) not in sys.path: + sys.path.insert(0, str(PREK_DIR)) + +from extract_permissions import ( # noqa: E402 + _FN_TO_RESOURCE_INFO, + PermissionEntry, + _build_resource_label, + _extract_method_arg, + _extract_module_string_constants, + _extract_routers, + _resolve_string_node, + extract_all_permissions, + extract_from_file, + render_rst, +) + +PUBLIC_ROUTES_DIR = REPO_ROOT / "airflow-core/src/airflow/api_fastapi/core_api/routes/public" + + +# --------------------------------------------------------------------------- +# Helpers +# --------------------------------------------------------------------------- + + +def parse_expr(code: str) -> ast.expr: + """Parse a single expression string into an AST node.""" + return ast.parse(code, mode="eval").body + + +def parse_module(code: str) -> ast.Module: + """Parse a dedented code block into a module AST.""" + return ast.parse(textwrap.dedent(code)) + + +def _make_route_file(tmp_path: Path, code: str) -> Path: + """Write synthetic route code to a temp .py file.""" + f = tmp_path / "test_route.py" + f.write_text(textwrap.dedent(code)) + return f + + +# =========================================================================== +# Unit tests: _resolve_string_node +# =========================================================================== + + +class TestResolveStringNode: + def test_plain_string_constant(self): + node = parse_expr("'/dags'") + assert _resolve_string_node(node, {}) == "/dags" + + def test_name_lookup_in_module_consts(self): + node = parse_expr("my_prefix") + assert _resolve_string_node(node, {"my_prefix": "/taskInstances"}) == "/taskInstances" + + def test_binop_concatenation(self): + # Mirrors task_instances.py: task_instances_prefix + "/{task_id}" + node = parse_expr("task_instances_prefix + '/{task_id}'") + consts = {"task_instances_prefix": "/dagRuns/{dag_run_id}/taskInstances"} + result = _resolve_string_node(node, consts) + assert result == "/dagRuns/{dag_run_id}/taskInstances/{task_id}" + + def test_nested_binop(self): + # a + b + c → (a + b) + c (left-associative) + node = parse_expr("a + b + c") + consts = {"a": "/dags", "b": "/{dag_id}", "c": "/runs"} + assert _resolve_string_node(node, consts) == "/dags/{dag_id}/runs" + + def test_unresolvable_name_returns_marker(self): + node = parse_expr("unknown_var") + result = _resolve_string_node(node, {}) + assert result.startswith(" ast.Call: + return ast.parse(code, mode="eval").body # type: ignore[return-value] + + def test_keyword_method(self): + call = self._call("requires_access_dag(method='GET', access_entity=DagAccessEntity.RUN)") + assert _extract_method_arg(call) == "GET" + + def test_positional_method(self): + # e.g. requires_access_variable('DELETE') + call = self._call("requires_access_variable('DELETE')") + assert _extract_method_arg(call) == "DELETE" + + def test_positional_method_is_uppercased(self): + call = self._call("requires_access_pool('get')") + assert _extract_method_arg(call) == "GET" + + def test_bulk_function_returns_multi(self): + # requires_access_pool_bulk() has no method argument + call = self._call("requires_access_pool_bulk()") + assert _extract_method_arg(call) == "multi" + + def test_keyword_method_is_uppercased(self): + call = self._call("requires_access_dag(method='put')") + assert _extract_method_arg(call) == "PUT" + + def test_post_keyword(self): + call = self._call("requires_access_connection(method='POST')") + assert _extract_method_arg(call) == "POST" + + +# =========================================================================== +# Unit tests: _build_resource_label +# =========================================================================== + + +class TestBuildResourceLabel: + def test_simple_resource_no_entity(self): + assert _build_resource_label("requires_access_pool", None) == "Pool" + + def test_dag_with_entity(self): + assert _build_resource_label("requires_access_dag", "RUN") == "DAG.RUN" + + def test_dag_with_task_instance_entity(self): + assert _build_resource_label("requires_access_dag", "TASK_INSTANCE") == "DAG.TASK_INSTANCE" + + def test_alias_backfill_forces_run_entity(self): + # requires_access_backfill → DAG.RUN regardless of entity arg + assert _build_resource_label("requires_access_backfill", None) == "DAG.RUN" + + def test_alias_dag_run_bulk_forces_run_entity(self): + assert _build_resource_label("requires_access_dag_run_bulk", None) == "DAG.RUN" + + def test_alias_event_log_forces_audit_log(self): + assert _build_resource_label("requires_access_event_log", None) == "DAG.AUDIT_LOG" + + def test_view_with_no_entity_returns_base(self): + assert _build_resource_label("requires_access_view", None) == "View" + + def test_view_with_entity(self): + assert _build_resource_label("requires_access_view", "PLUGINS") == "View.PLUGINS" + + def test_unknown_function_falls_back_to_fn_name(self): + # If a new requires_access_* is added but not yet in the map, the + # function name is used. Tests will catch it via the coverage test. + assert _build_resource_label("requires_access_new_thing", None) == "requires_access_new_thing" + + +# =========================================================================== +# Unit tests: extract_from_file (synthetic route files) +# =========================================================================== + + +class TestExtractFromFile: + def test_basic_get_with_keyword_method(self, tmp_path): + f = _make_route_file( + tmp_path, + """ + from fastapi import Depends + dags_router = AirflowRouter(tags=["DAG"], prefix="/dags") + + @dags_router.get( + "/{dag_id}", + dependencies=[Depends(requires_access_dag(method="GET"))], + ) + def get_dag(dag_id: str): ... + """, + ) + entries = extract_from_file(f) + assert len(entries) == 1 + e = entries[0] + assert e.http_method == "GET" + assert e.full_path == "/api/v2/dags/{dag_id}" + assert e.resource == "DAG" + assert e.required_permission == "GET" + + def test_multiple_routers_in_same_file(self, tmp_path): + f = _make_route_file( + tmp_path, + """ + from fastapi import Depends + dag_run_router = AirflowRouter(prefix="/dags/{dag_id}/dagRuns") + dag_run_at_dag_router = AirflowRouter(prefix="/dags/{dag_id}") + + @dag_run_router.post( + "/clear", + dependencies=[Depends(requires_access_dag(method="POST", access_entity=DagAccessEntity.RUN))], + ) + def clear_dag_runs(dag_id: str): ... + + @dag_run_at_dag_router.post( + "/clearDagRuns", + dependencies=[Depends(requires_access_dag(method="POST", access_entity=DagAccessEntity.RUN))], + ) + def clear_dag_runs_at_dag(dag_id: str): ... + """, + ) + entries = extract_from_file(f) + assert len(entries) == 2 + # Sort by full_path to be deterministic in assertions + entries_sorted = sorted(entries, key=lambda e: e.full_path) + + # /api/v2/dags/{dag_id}/clearDagRuns (from dag_run_at_dag_router) + assert entries_sorted[0].full_path == "/api/v2/dags/{dag_id}/clearDagRuns" + assert entries_sorted[0].http_method == "POST" + + # /api/v2/dags/{dag_id}/dagRuns/clear (from dag_run_router) + assert entries_sorted[1].full_path == "/api/v2/dags/{dag_id}/dagRuns/clear" + assert entries_sorted[1].http_method == "POST" + + def test_positional_method_arg(self, tmp_path): + f = _make_route_file( + tmp_path, + """ + from fastapi import Depends + variables_router = AirflowRouter(prefix="/variables") + + @variables_router.delete( + "/{variable_key:path}", + dependencies=[Depends(requires_access_variable("DELETE"))], + ) + def delete_variable(variable_key: str): ... + """, + ) + entries = extract_from_file(f) + assert len(entries) == 1 + assert entries[0].http_method == "DELETE" + assert entries[0].required_permission == "DELETE" + assert entries[0].resource == "Variable" + + def test_dag_access_entity_positional(self, tmp_path): + """The second positional arg to requires_access_dag is the access_entity.""" + f = _make_route_file( + tmp_path, + """ + from fastapi import Depends + dag_router = AirflowRouter(tags=["DAG"], prefix="/dags/{dag_id}") + + @dag_router.get( + "/taskLogs", + dependencies=[Depends(requires_access_dag("GET", DagAccessEntity.TASK_LOGS))], + ) + def get_task_logs(dag_id: str): ... + """, + ) + entries = extract_from_file(f) + assert len(entries) == 1 + e = entries[0] + assert e.resource == "DAG.TASK_LOGS" + assert e.required_permission == "GET" + + def test_binop_path_resolved(self, tmp_path): + # Mirrors the task_instances.py pattern exactly + f = _make_route_file( + tmp_path, + """ + from fastapi import Depends + task_instances_router = AirflowRouter(prefix="/dags/{dag_id}") + task_instances_prefix = "/dagRuns/{dag_run_id}/taskInstances" + + @task_instances_router.get( + task_instances_prefix + "/{task_id}", + dependencies=[Depends(requires_access_dag(method="GET", access_entity=DagAccessEntity.TASK_INSTANCE))], + ) + def get_task_instance(dag_id: str, dag_run_id: str, task_id: str): ... + """, + ) + entries = extract_from_file(f) + assert len(entries) == 1 + e = entries[0] + assert e.full_path == "/api/v2/dags/{dag_id}/dagRuns/{dag_run_id}/taskInstances/{task_id}" + assert e.resource == "DAG.TASK_INSTANCE" + assert e.http_method == "GET" + + def test_bulk_function_no_method_arg(self, tmp_path): + f = _make_route_file( + tmp_path, + """ + from fastapi import Depends + pools_router = AirflowRouter(prefix="/pools") + + @pools_router.patch( + "", + dependencies=[Depends(requires_access_pool_bulk())], + ) + def bulk_pools(): ... + """, + ) + entries = extract_from_file(f) + assert len(entries) == 1 + assert entries[0].required_permission == "multi" + assert entries[0].resource == "Pool" + + def test_view_access_positional(self, tmp_path): + f = _make_route_file( + tmp_path, + """ + from fastapi import Depends + plugins_router = AirflowRouter(tags=["Plugin"], prefix="/plugins") + + @plugins_router.get( + "", + dependencies=[Depends(requires_access_view(AccessView.PLUGINS))], + ) + def get_plugins(): ... + """, + ) + entries = extract_from_file(f) + assert len(entries) == 1 + e = entries[0] + assert e.resource == "View.PLUGINS" + assert e.required_permission == "PLUGINS" + + def test_no_dependencies_kwarg_extracted_as_public(self, tmp_path): + f = _make_route_file( + tmp_path, + """ + from fastapi import Depends + router = AirflowRouter(prefix="/version") + + @router.get("") + def get_version(): ... + """, + ) + entries = extract_from_file(f) + assert len(entries) == 1 + e = entries[0] + assert e.http_method == "GET" + assert e.full_path == "/api/v2/version" + assert e.resource == "Public" + assert e.required_permission == "No Airflow permission required" + + def test_only_unrelated_dependencies_extracted_as_public(self, tmp_path): + f = _make_route_file( + tmp_path, + """ + from fastapi import Depends + router = AirflowRouter(prefix="/version") + + @router.get("", dependencies=[Depends(action_logging())]) + def get_version(): ... + """, + ) + entries = extract_from_file(f) + assert len(entries) == 1 + e = entries[0] + assert e.http_method == "GET" + assert e.full_path == "/api/v2/version" + assert e.resource == "Public" + assert e.required_permission == "No Airflow permission required" + + def test_multiple_deps_on_same_route_produces_multiple_entries(self, tmp_path): + f = _make_route_file( + tmp_path, + """ + from fastapi import Depends + dag_run_router = AirflowRouter(prefix="/dags/{dag_id}/dagRuns") + + @dag_run_router.get( + "/{dag_run_id}/upstreamAssetEvents", + dependencies=[ + Depends(requires_access_asset(method="GET")), + Depends(requires_access_dag(method="GET", access_entity=DagAccessEntity.RUN)), + ], + ) + def get_upstream_events(): ... + """, + ) + entries = extract_from_file(f) + assert len(entries) == 2 + resources = {e.resource for e in entries} + assert "Asset" in resources + assert "DAG.RUN" in resources + + def test_non_requires_access_dep_is_ignored(self, tmp_path): + # action_logging() is a dep that should not produce a permission entry + f = _make_route_file( + tmp_path, + """ + from fastapi import Depends + router = AirflowRouter(prefix="/dags") + + @router.post( + "", + dependencies=[Depends(action_logging()), Depends(requires_access_dag(method="POST"))], + ) + def post_dag(): ... + """, + ) + entries = extract_from_file(f) + assert len(entries) == 1 + assert entries[0].resource == "DAG" + + def test_syntax_error_returns_empty_list(self, tmp_path, capsys): + f = tmp_path / "broken.py" + f.write_text("def broken(:\n") + entries = extract_from_file(f) + assert entries == [] + # Should print a warning, not raise + captured = capsys.readouterr() + assert "WARN" in captured.err + + def test_source_file_is_basename(self, tmp_path): + f = _make_route_file( + tmp_path, + """ + router = AirflowRouter(prefix="/pools") + + @router.get("", dependencies=[Depends(requires_access_pool(method="GET"))]) + def get_pools(): ... + """, + ) + entries = extract_from_file(f) + assert entries[0].source_file == "test_route.py" + + +# =========================================================================== +# Unit tests: _FN_TO_RESOURCE coverage invariant +# =========================================================================== + + +class TestResourceMapCoverage: + """Guard against _FN_TO_RESOURCE_INFO going stale as new requires_access_* functions are added.""" + + def _get_all_imported_security_fns(self) -> set[str]: + """ + Find all requires_access_* names imported in public route files. + This is the ground truth of what the extractor must know about. + """ + imported: set[str] = set() + for route_file in PUBLIC_ROUTES_DIR.glob("*.py"): + if route_file.name == "__init__.py": + continue + try: + tree = ast.parse(route_file.read_text()) + except SyntaxError: + continue + for node in ast.walk(tree): + if isinstance(node, ast.ImportFrom) and node.module and "security" in node.module: + for alias in node.names: + if alias.name.startswith("requires_access"): + imported.add(alias.name) + return imported + + def test_all_imported_functions_are_in_resource_map(self): + """ + If a new requires_access_* function is added to security.py and used in + a route, it must also be added to _FN_TO_RESOURCE_INFO in the extractor. + + Failure here means: a new endpoint has no documented permission. + """ + imported = self._get_all_imported_security_fns() + unmapped = imported - set(_FN_TO_RESOURCE_INFO.keys()) + assert not unmapped, ( + "These requires_access_* functions are used in route files but are not " + "in _FN_TO_RESOURCE_INFO in extract_permissions.py:\n" + + "\n".join(f" - {fn}" for fn in sorted(unmapped)) + ) + + def test_no_dead_entries_in_resource_map(self): + """ + Every entry in _FN_TO_RESOURCE_INFO should correspond to a function actually + used in route files. Dead entries suggest the function was removed or renamed. + + This is a WARNING-level test: it identifies map entries that should be cleaned up. + """ + imported = self._get_all_imported_security_fns() + dead = set(_FN_TO_RESOURCE_INFO.keys()) - imported + assert not dead, ( + "These entries in _FN_TO_RESOURCE_INFO are not used by any route file " + "and should be removed:\n" + "\n".join(f" - {fn}" for fn in sorted(dead)) + ) + + +# =========================================================================== +# Integration tests: extract_all_permissions against real routes +# =========================================================================== + + +class TestExtractAllPermissions: + """ + Integration tests against the real route files. + Uses invariants, not snapshots, so they survive unrelated route additions. + """ + + @pytest.fixture(scope="class") + def all_entries(self) -> list[PermissionEntry]: + return extract_all_permissions(PUBLIC_ROUTES_DIR) + + def test_extracts_non_empty_result(self, all_entries): + assert len(all_entries) > 0 + + def test_minimum_known_entry_count(self, all_entries): + """ + Guard against the extractor silently returning fewer results. + The exact number will grow; 100 is a floor well below current 123. + """ + assert len(all_entries) >= 100, f"Expected ≥100 entries, got {len(all_entries)}" + + def test_output_is_sorted(self, all_entries): + expected = sorted( + all_entries, + key=lambda e: ( + e.full_path, + e.http_method, + e.resource, + e.required_permission, + ), + ) + assert all_entries == expected + + def test_public_endpoints_coverage(self, all_entries): + """Verify that known public endpoints are extracted as Public.""" + public_paths = { + "/api/v2/monitor/health": "GET", + "/api/v2/version": "GET", + "/api/v2/auth/login": "GET", + "/api/v2/auth/logout": "GET", + } + for path, method in public_paths.items(): + matches = [e for e in all_entries if e.full_path == path and e.http_method == method] + assert len(matches) == 1, f"Expected exactly one match for public endpoint {method} {path}" + e = matches[0] + assert e.resource == "Public" + assert e.required_permission == "No Airflow permission required" + + def test_no_duplicate_entries(self, all_entries): + seen: set[PermissionEntry] = set() + for e in all_entries: + assert e not in seen, f"Duplicate entry: {e}" + seen.add(e) + + def test_no_unresolved_path_markers(self, all_entries): + unresolved = [e for e in all_entries if "= 1 + assert any(e.resource == "DAG" and e.required_permission == "GET" for e in matches) + + def test_variable_delete_permission(self, all_entries): + matches = [ + e for e in all_entries if "/api/v2/variables/" in e.full_path and e.http_method == "DELETE" + ] + assert len(matches) >= 1 + assert all(e.resource == "Variable" and e.required_permission == "DELETE" for e in matches) + + def test_task_instance_path_resolved(self, all_entries): + """The BinOp path in task_instances.py must be fully resolved.""" + ti_entries = [e for e in all_entries if "/taskInstances/" in e.full_path] + assert len(ti_entries) > 0 + for e in ti_entries: + assert "= 1 + assert all(e.resource == "DAG.RUN" for e in bulk) + + def test_view_permissions_use_entity_as_permission(self, all_entries): + view_entries = [e for e in all_entries if e.resource.startswith("View.")] + assert len(view_entries) > 0 + for e in view_entries: + # For view permissions, required_permission == the view name (e.g. "PLUGINS") + assert e.required_permission == e.resource.split(".")[-1] + + def test_event_log_mapped_to_dag_audit_log(self, all_entries): + el_entries = [e for e in all_entries if e.source_file == "event_logs.py"] + assert len(el_entries) > 0 + assert all(e.resource == "DAG.AUDIT_LOG" for e in el_entries) + + def test_backfill_mapped_to_dag_run(self, all_entries): + bf_entries = [e for e in all_entries if e.source_file == "backfills.py"] + assert len(bf_entries) > 0 + assert all(e.resource == "DAG.RUN" for e in bf_entries) + + def test_clear_dag_runs_endpoint_prefix(self, all_entries): + """Verify that the clearDagRuns endpoint resolves to the correct path prefix.""" + matches = [e for e in all_entries if e.full_path == "/api/v2/dags/{dag_id}/clearDagRuns"] + assert len(matches) == 1 + e = matches[0] + assert e.http_method == "POST" + assert e.resource == "DAG.RUN" + assert e.required_permission == "multi" + + # Also verify that other dag runs endpoints still resolve with the longer prefix /api/v2/dags/{dag_id}/dagRuns + dag_runs_list = [ + e + for e in all_entries + if e.full_path == "/api/v2/dags/{dag_id}/dagRuns" and e.http_method == "GET" + ] + assert len(dag_runs_list) >= 1 + + +# =========================================================================== +# Integration tests: render_rst +# =========================================================================== + + +class TestRenderRst: + @pytest.fixture(scope="class") + def rst_content(self) -> str: + entries = extract_all_permissions(PUBLIC_ROUTES_DIR) + return render_rst(entries) + + def test_rst_contains_auto_generated_marker(self, rst_content): + assert "AUTO-GENERATED" in rst_content + + def test_rst_contains_list_table_directive(self, rst_content): + assert ".. list-table::" in rst_content + + def test_rst_contains_api_v2_paths(self, rst_content): + assert "/api/v2/" in rst_content + + def test_rst_contains_no_unresolved_markers(self, rst_content): + assert " Date: Sat, 4 Jul 2026 02:30:03 +0800 Subject: [PATCH 017/297] Fix asset materialization dropping partition date on partitioned Dag runs (#69314) (#69339) --- .../core_api/routes/public/assets.py | 1 + .../core_api/routes/public/test_assets.py | 36 +++++++++++++++++++ 2 files changed, 37 insertions(+) diff --git a/airflow-core/src/airflow/api_fastapi/core_api/routes/public/assets.py b/airflow-core/src/airflow/api_fastapi/core_api/routes/public/assets.py index 15af36445dd95..a19f4c7da7797 100644 --- a/airflow-core/src/airflow/api_fastapi/core_api/routes/public/assets.py +++ b/airflow-core/src/airflow/api_fastapi/core_api/routes/public/assets.py @@ -464,6 +464,7 @@ def materialize_asset( triggering_user_name=user.get_name(), state=DagRunState.QUEUED, partition_key=params["partition_key"], + partition_date=params["partition_date"], note=params["note"], session=session, ) diff --git a/airflow-core/tests/unit/api_fastapi/core_api/routes/public/test_assets.py b/airflow-core/tests/unit/api_fastapi/core_api/routes/public/test_assets.py index 33c43785a55a8..0470b0b1e4e51 100644 --- a/airflow-core/tests/unit/api_fastapi/core_api/routes/public/test_assets.py +++ b/airflow-core/tests/unit/api_fastapi/core_api/routes/public/test_assets.py @@ -41,7 +41,9 @@ from airflow.models.serialized_dag import SerializedDagModel from airflow.models.trigger import Trigger from airflow.providers.standard.operators.empty import EmptyOperator +from airflow.sdk import Asset from airflow.timetables.simple import PartitionedAtRuntime +from airflow.timetables.trigger import CronPartitionTimetable from airflow.utils.session import provide_session from airflow.utils.state import DagRunState from airflow.utils.types import DagRunType @@ -1684,6 +1686,40 @@ def test_should_respond_400_on_invalid_dag_run_id(self, test_client): assert response.status_code == 400 assert "must not contain '..'" in response.json()["detail"] + @pytest.mark.usefixtures("configure_git_connection_for_dag_bundle") + def test_should_respond_200_with_partition_date_for_partitioned_dag( + self, test_client, dag_maker, session + ): + """Materializing a Dag with a real partitioned timetable must populate partition_date. + + Regression guard: before this fix, `partition_date` resolved by `validate_context` was + dropped when creating the run, unlike the sibling `/dags/{dag_id}/dagRuns` trigger route. + """ + partitioned_dag_id = "test_materialize_populates_partition_date" + asset = Asset(name="materialize_partition_date_asset", uri="s3://bucket/materialize-partition-date") + with dag_maker( + dag_id=partitioned_dag_id, + schedule=CronPartitionTimetable("0 0 * * *", timezone="UTC"), + start_date=DEFAULT_DATE, + session=session, + serialized=True, + ): + EmptyOperator(task_id="task", outlets=[asset]) + session.commit() + + asset_id = session.scalar(select(AssetModel.id).where(AssetModel.uri == asset.uri)) + + response = test_client.post( + f"/assets/{asset_id}/materialize", + json={"partition_key": "2025-06-01T00:00:00"}, + ) + assert response.status_code == 200 + + dag_run = session.scalar(select(DagRun).where(DagRun.dag_id == partitioned_dag_id)) + assert dag_run is not None + assert dag_run.partition_key == "2025-06-01T00:00:00" + assert dag_run.partition_date == timezone.datetime(2025, 6, 1) + class TestGetAssetQueuedEvents(TestQueuedEventEndpoint): @pytest.mark.usefixtures("time_freezer") From c8112817be7e8d7fae0ae5cdf4b4826c3c0d1983 Mon Sep 17 00:00:00 2001 From: Jarek Potiuk Date: Fri, 3 Jul 2026 21:22:41 +0200 Subject: [PATCH 018/297] Add task.execute detail span around task execute callable (#67877) (#69359) When task span detail level is greater than 1, the actual execute call was not separately traced, making it hard to see how much of a task's runtime was spent in the operator's own work versus the surrounding setup. Wrapping the execute call in its own span gives that finer-grained breakdown. The contextvars context the callable runs in is snapshotted inside the new helper, after the span is current, so spans the operator emits during execute nest under it rather than alongside it. (cherry picked from commit b006a978d204ad72f88b9c0633418facd2c00330) Co-authored-by: Daniel Standish <15932138+dstandish@users.noreply.github.com> --- .../airflow/sdk/execution_time/task_runner.py | 59 +++++--- .../execution_time/test_task_runner.py | 140 ++++++++++++++++++ 2 files changed, 178 insertions(+), 21 deletions(-) diff --git a/task-sdk/src/airflow/sdk/execution_time/task_runner.py b/task-sdk/src/airflow/sdk/execution_time/task_runner.py index f3fee689928a0..7a77ed9ad23c5 100644 --- a/task-sdk/src/airflow/sdk/execution_time/task_runner.py +++ b/task-sdk/src/airflow/sdk/execution_time/task_runner.py @@ -2064,6 +2064,43 @@ def _send_error_email_notification( log.exception("Failed to send email notification") +@detail_span("task.execute") +def _run_execute_callable( + context: Context, + execute: Callable[..., Any] | functools.partial[Any], + task: BaseOperator, +) -> Any: + """ + Run the task's execute callable, applying the execution timeout if one is set. + + The contextvars snapshot is taken here, after the ``task.execute`` span is + current, so spans the operator emits during ``execute`` nest under it rather + than under the caller. ``ExecutorSafeguard``'s tracker is set into that copy + so the operator's ``execute`` passes the safeguard check, while the copy keeps + the change from leaking into the surrounding context. + """ + ctx = contextvars.copy_context() + ctx.run(ExecutorSafeguard.tracker.set, task) + if task.execution_timeout: + from airflow.sdk.execution_time.timeout import timeout + + # TODO: handle timeout in case of deferral + timeout_seconds = task.execution_timeout.total_seconds() + try: + # It's possible we're already timed out, so fast-fail if true + if timeout_seconds <= 0: + raise AirflowTaskTimeout() + # Run task in timeout wrapper + with timeout(timeout_seconds): + result = ctx.run(execute, context=context) + except AirflowTaskTimeout: + task.on_kill() + raise + else: + result = ctx.run(execute, context=context) + return result + + @detail_span("_execute_task") def _execute_task(context: Context, ti: RuntimeTaskInstance, log: Logger): """Execute Task (optionally with a Timeout) and push Xcom results.""" @@ -2087,10 +2124,6 @@ def _execute_task(context: Context, ti: RuntimeTaskInstance, log: Logger): assert isinstance(kwargs, dict) execute = functools.partial(task.resume_execution, next_method=next_method, next_kwargs=kwargs) - ctx = contextvars.copy_context() - # Populate the context var so ExecutorSafeguard doesn't complain - ctx.run(ExecutorSafeguard.tracker.set, task) - # Export context in os.environ to make it available for operators to use. airflow_context_vars = context_to_airflow_vars(context, in_env_var_format=True) os.environ.update(airflow_context_vars) @@ -2106,23 +2139,7 @@ def _execute_task(context: Context, ti: RuntimeTaskInstance, log: Logger): log.info("::endgroup::") - if task.execution_timeout: - from airflow.sdk.execution_time.timeout import timeout - - # TODO: handle timeout in case of deferral - timeout_seconds = task.execution_timeout.total_seconds() - try: - # It's possible we're already timed out, so fast-fail if true - if timeout_seconds <= 0: - raise AirflowTaskTimeout() - # Run task in timeout wrapper - with timeout(timeout_seconds): - result = ctx.run(execute, context=context) - except AirflowTaskTimeout: - task.on_kill() - raise - else: - result = ctx.run(execute, context=context) + result = _run_execute_callable(context, execute, task) if (post_execute_hook := task._post_execute_hook) is not None: create_executable_runner(post_execute_hook, outlet_events, logger=log).run(context, result) diff --git a/task-sdk/tests/task_sdk/execution_time/test_task_runner.py b/task-sdk/tests/task_sdk/execution_time/test_task_runner.py index 64493f6305f3c..1aad1a02d5eab 100644 --- a/task-sdk/tests/task_sdk/execution_time/test_task_runner.py +++ b/task-sdk/tests/task_sdk/execution_time/test_task_runner.py @@ -18,6 +18,7 @@ from __future__ import annotations import contextlib +import contextvars import functools import json import os @@ -73,6 +74,7 @@ TaskInstanceState, TIRunContext, ) +from airflow.sdk.bases.operator import ExecutorSafeguard from airflow.sdk.bases.xcom import BaseXCom from airflow.sdk.definitions._internal.types import NOTSET, SET_DURING_EXECUTION, is_arg_set from airflow.sdk.definitions.asset import Asset, AssetAlias, AssetUniqueKey, AssetUriRef, Dataset, Model @@ -167,6 +169,7 @@ _make_task_span, _push_xcom_if_needed, _register_deserialization_allowed_classes, + _run_execute_callable, _serialize_outlet_events, _xcom_push, detail_span, @@ -5597,6 +5600,143 @@ def test_exception_in_context_manager_propagates(self): raise ValueError("boom") +class TestRunExecuteCallable: + """Tests for ``_run_execute_callable``. + + It runs the task's execute callable inside an isolated contextvars copy (with + the ExecutorSafeguard tracker set), applies the execution timeout when one is + configured, and wraps the call in a ``task.execute`` detail span. + """ + + @pytest.fixture(autouse=True) + def _sampled_carrier_provider(self): + """Make new_dagrun_trace_carrier produce a SAMPLED carrier (see TestDetailSpan).""" + provider = TracerProvider() + with mock.patch( + "airflow._shared.observability.traces.trace.get_tracer_provider", + return_value=provider, + ): + yield + + @staticmethod + def _make_task(execution_timeout=None): + task = mock.MagicMock(spec=BaseOperator) + task.execution_timeout = execution_timeout + return task + + def test_runs_in_isolated_context_with_safeguard_tracker_set(self): + """The callable runs in an internal context copy that has the safeguard tracker set and does not leak.""" + var = contextvars.ContextVar("marker") + var.set("outer") + task = self._make_task() + seen = {} + + def execute(context): + var.set("inner") + seen["tracker"] = ExecutorSafeguard.tracker.get(None) + return context["value"] * 2 + + result = _run_execute_callable(context={"value": 21}, execute=execute, task=task) + + assert result == 42 + # The safeguard tracker is set to the task inside the copy used to run execute. + assert seen["tracker"] is task + # The mutation happened inside the copy, so it does not leak to the caller's context. + assert var.get() == "outer" + # The .set was confined to the copy, so the tracker never leaked to the caller's context. + assert ExecutorSafeguard.tracker.get(None) is not task + task.on_kill.assert_not_called() + + def test_applies_execution_timeout(self): + """When a timeout is set and the callable overruns, AirflowTaskTimeout is raised and on_kill is called.""" + task = self._make_task(execution_timeout=timedelta(milliseconds=10)) + + def execute(context): + time.sleep(2) + + with pytest.raises(AirflowTaskTimeout): + _run_execute_callable(context={}, execute=execute, task=task) + + task.on_kill.assert_called_once() + + def test_fast_fails_when_timeout_already_elapsed(self): + """A non-positive timeout fast-fails before running the callable and still calls on_kill.""" + task = self._make_task(execution_timeout=timedelta(seconds=-1)) + execute = mock.MagicMock() + + with pytest.raises(AirflowTaskTimeout): + _run_execute_callable(context={}, execute=execute, task=task) + + execute.assert_not_called() + task.on_kill.assert_called_once() + + def test_emits_task_execute_span_at_detail_level_2(self): + """At detail level 2, running the callable produces a recorded ``task.execute`` span.""" + exporter = InMemorySpanExporter() + provider = TracerProvider() + provider.add_span_processor(SimpleSpanProcessor(exporter)) + t = provider.get_tracer("test") + carrier = new_dagrun_trace_carrier(task_span_detail_level=2) + parent_ctx = TraceContextTextMapPropagator().extract(carrier) + + task = self._make_task() + + with mock.patch("airflow.sdk.execution_time.task_runner.tracer", t): + with t.start_as_current_span("parent", context=parent_ctx): + result = _run_execute_callable(context={}, execute=lambda context: "ok", task=task) + + assert result == "ok" + names = [s.name for s in exporter.get_finished_spans()] + assert "task.execute" in names + + def test_operator_child_spans_nest_under_task_execute(self): + """Spans the operator emits during execute nest under ``task.execute``, not its caller. + + The contextvars snapshot is taken inside ``_run_execute_callable`` after the + ``task.execute`` span is current, so a span started during execute parents to + ``task.execute`` rather than to the surrounding span. + """ + exporter = InMemorySpanExporter() + provider = TracerProvider() + provider.add_span_processor(SimpleSpanProcessor(exporter)) + t = provider.get_tracer("test") + carrier = new_dagrun_trace_carrier(task_span_detail_level=2) + parent_ctx = TraceContextTextMapPropagator().extract(carrier) + + task = self._make_task() + + def execute(context): + with t.start_as_current_span("operator_child"): + return "ok" + + with mock.patch("airflow.sdk.execution_time.task_runner.tracer", t): + with t.start_as_current_span("parent", context=parent_ctx): + result = _run_execute_callable(context={}, execute=execute, task=task) + + assert result == "ok" + spans = {s.name: s for s in exporter.get_finished_spans()} + assert spans["operator_child"].parent.span_id == spans["task.execute"].context.span_id + + def test_no_task_execute_span_at_detail_level_1(self): + """At detail level 1, no ``task.execute`` span is recorded but the callable still runs.""" + exporter = InMemorySpanExporter() + provider = TracerProvider() + provider.add_span_processor(SimpleSpanProcessor(exporter)) + t = provider.get_tracer("test") + carrier = new_dagrun_trace_carrier(task_span_detail_level=1) + parent_ctx = TraceContextTextMapPropagator().extract(carrier) + + task = self._make_task() + + with mock.patch("airflow.sdk.execution_time.task_runner.tracer", t): + with t.start_as_current_span("parent", context=parent_ctx): + result = _run_execute_callable(context={}, execute=lambda context: "ok", task=task) + + assert result == "ok" + names = [s.name for s in exporter.get_finished_spans()] + assert "task.execute" not in names + + def test_dag_add_result(create_runtime_ti, mock_supervisor_comms): with DAG(dag_id="test_dag_add_result") as dag: task = PythonOperator(task_id="t", python_callable=lambda: 123) From a3e4c2593708a03fd1823a149a6aa0ffc3af48ee Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Fri, 3 Jul 2026 22:31:26 +0200 Subject: [PATCH 019/297] [v3-3-test] Run OTel integration tests when span-emitting task runner code changes (#69250) (#69285) The otel core integration was only triggered by observability sources, so PRs changing the spans the task runner emits (like #67877) or the otel integration tests themselves passed CI without running the tests that assert the span hierarchy, and breakage surfaced only in canary builds. (cherry picked from commit 529843127c79e4c8a496275e4344f655b6661468) Co-authored-by: Jason(Zhe-You) Liu <68415893+jason810496@users.noreply.github.com> Co-authored-by: Jarek Potiuk --- dev/breeze/doc/ci/04_selective_checks.md | 1 + dev/breeze/src/airflow_breeze/utils/selective_checks.py | 4 ++++ dev/breeze/tests/test_selective_checks.py | 2 ++ 3 files changed, 7 insertions(+) diff --git a/dev/breeze/doc/ci/04_selective_checks.md b/dev/breeze/doc/ci/04_selective_checks.md index 5dbaf289f9eb0..67374350dedd0 100644 --- a/dev/breeze/doc/ci/04_selective_checks.md +++ b/dev/breeze/doc/ci/04_selective_checks.md @@ -244,6 +244,7 @@ representative examples (file → effect): | `scripts/ci/prek/check_*.py` (static-check hook) | CI image + static checks, **no full matrix** | prek hooks are static checks → `Prek files` carve-out | | the generated OpenAPI spec | **full matrix** | the API *contract* ripples to UI codegen + every client | | `chart/templates/...yaml` (on `main`) | `run_helm_tests` (+ PROD image) | matches `HELM_FILES`; Helm tests only on `main` | +| `task-sdk/.../task_runner.py` or `airflow-core/tests/integration/otel/...` | the `otel` core integration | matches `OTEL_FILES`; the otel integration tests assert the span hierarchy task_runner emits | | `airflow-core/src/airflow/ui/...tsx` only | `run_ui_tests`, **no** unit tests | "only new-UI files" short-circuit skips Python unit tests | The "complexity" you feel reading the code is just *many* such rules stacked up — each one on its own diff --git a/dev/breeze/src/airflow_breeze/utils/selective_checks.py b/dev/breeze/src/airflow_breeze/utils/selective_checks.py index fcaf8ae5c0a27..93cd502df0478 100644 --- a/dev/breeze/src/airflow_breeze/utils/selective_checks.py +++ b/dev/breeze/src/airflow_breeze/utils/selective_checks.py @@ -464,6 +464,10 @@ def __hash__(self): r"^airflow-core/src/airflow/observability/.*", r"^shared/observability/src/airflow_shared/observability/.*", r"^airflow-core/src/airflow/utils/span_status\.py$", + # The otel integration tests assert the exact span hierarchy that + # task_runner emits, so changes to either must exercise the integration. + r"^airflow-core/tests/integration/otel/.*", + r"^task-sdk/src/airflow/sdk/execution_time/task_runner\.py$", ], FileGroupForCi.CELERY_FILES: [ # Core executor sources - redis is celery's broker/result backend, so the diff --git a/dev/breeze/tests/test_selective_checks.py b/dev/breeze/tests/test_selective_checks.py index 7b2848a6f467f..552e0db77ed6e 100644 --- a/dev/breeze/tests/test_selective_checks.py +++ b/dev/breeze/tests/test_selective_checks.py @@ -3419,6 +3419,8 @@ def test_testable_providers_integrations_excludes_arm_disabled_on_arm(): [ pytest.param("airflow-core/src/airflow/security/kerberos.py", "kerberos", id="kerberos-source"), pytest.param("airflow-core/src/airflow/observability/stats.py", "otel", id="otel-source"), + pytest.param("airflow-core/tests/integration/otel/test_otel.py", "otel", id="otel-integration-tests"), + pytest.param("task-sdk/src/airflow/sdk/execution_time/task_runner.py", "otel", id="otel-task-runner"), pytest.param("airflow-core/src/airflow/executors/executor_loader.py", "redis", id="celery-source"), ], ) From f57c064ed7fdd727dd62b6c6faae5c5b2f56ed46 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Sat, 4 Jul 2026 00:59:16 +0300 Subject: [PATCH 020/297] [v3-3-test] Auto-generate sorted packages.txt during providers PyPI upload (#69356) (#69357) Release managers previously copied the PyPI URLs printed by twine out of the terminal by hand, sorted them, and saved them off to the side for the vote email and announcement. The completeness-check step also referenced packages.txt before it existed, and the twine upload used dist/* globs that swept in the ASF source tarball and the .asc/.sha512 files, which are SVN-only and make twine fail with 'Unknown distribution format'. The upload now writes the log and a sorted, de-duplicated packages.txt into the git-ignored files/ directory, uploads only the provider wheels and sdists, and the completeness gate runs after the file is generated, right before the vote email is sent. (cherry picked from commit bc2628e4f3455d76d4db90cb4650d1b55d6e27b3) Co-authored-by: Shahar Epstein <60007259+shahar1@users.noreply.github.com> --- dev/README_RELEASE_PROVIDERS.md | 79 +++++++++++++++++++++------------ 1 file changed, 50 insertions(+), 29 deletions(-) diff --git a/dev/README_RELEASE_PROVIDERS.md b/dev/README_RELEASE_PROVIDERS.md index 7a95c062dd1e9..7f4c1ab0350b8 100644 --- a/dev/README_RELEASE_PROVIDERS.md +++ b/dev/README_RELEASE_PROVIDERS.md @@ -592,20 +592,6 @@ svn commit -m "Add artifacts for Airflow Providers ${RELEASE_DATE}" cd "$AIRFLOW_REPO_ROOT" ``` -* Before sending the vote email, gate on the same completeness check the PMC verifiers run, so a - missing artifact (e.g. the `-source.tar.gz` tarball) fails here instead of in the vote thread. - Put the package list from the upcoming vote email into `dev/packages.txt`, then run: - -```shell script -cd "$AIRFLOW_REPO_ROOT" -breeze release-management check-release-files providers --release-date "${RELEASE_DATE}" \ - --packages-file ./dev/packages.txt \ - --path-to-airflow-svn "$(cd ../asf-dist/dev/airflow && pwd -P)" -``` - - It exits non-zero and lists every missing file (including `.asc`/`.sha512` variants) if anything is - absent. Only proceed to the vote once it prints `All expected files are present!`. - Verify that the files are available in the ${RELEASE_DATE} folder under [providers](https://dist.apache.org/repos/dist/dev/airflow/providers/) @@ -692,15 +678,27 @@ twine check ${AIRFLOW_REPO_ROOT}/dist/* This is a defence-in-depth practice: the RM machine becomes a one-time release vehicle, not a persistent point of compromise. -* Upload the package to PyPI: +* Upload the packages to PyPI. Only the provider wheels and sdists are uploaded — the ASF + `-source.tar.gz` tarball and the `.asc`/`.sha512` files are SVN-only and must not go to PyPI. + The output is teed to a log and the PyPI URLs twine prints are extracted (sorted, de-duplicated) + into `files/packages.txt`. That file is what you paste into the vote email, and what the + completeness gate ("Prepare voting email" section below) and the PMC verifiers consume. Both files + land in `files/` (git-ignored), so they are never accidentally committed: ```shell script -twine upload -r pypi ${AIRFLOW_REPO_ROOT}/dist/* +mkdir -p "${AIRFLOW_REPO_ROOT}/files" +# COLUMNS=200 stops rich (twine's printer) from wrapping long URLs when stdout is a pipe. +COLUMNS=200 twine upload -r pypi ${AIRFLOW_REPO_ROOT}/dist/*.whl \ + $(ls ${AIRFLOW_REPO_ROOT}/dist/*.tar.gz | grep -v -- '-source.tar.gz') 2>&1 \ + | tee "${AIRFLOW_REPO_ROOT}/files/twine-upload.log" +# Only trust packages.txt if the upload above succeeded (PIPESTATUS[0] == 0): +grep -oE 'https://pypi\.org/project/[^[:space:]]+' "${AIRFLOW_REPO_ROOT}/files/twine-upload.log" \ + | sort -u > "${AIRFLOW_REPO_ROOT}/files/packages.txt" ``` -* Confirm that the packages are available under the links printed and look good. - -* Save these links for later, you'll need to paste them in the email you'll send to dev@airflow.apache.org +* Confirm that the packages are available under the links printed and look good. The same links are + now saved, sorted, in `files/packages.txt` — you'll paste them into the vote email you send to + dev@airflow.apache.org. ## Push the RC tags @@ -856,6 +854,21 @@ gh issue create --repo apache/airflow \ Make sure the packages are in https://dist.apache.org/repos/dist/dev/airflow/providers/ +* Before sending the vote email, gate on the same completeness check the PMC verifiers run, so a + missing artifact (e.g. the `-source.tar.gz` tarball) fails here instead of in the vote thread. + `files/packages.txt` was already generated by the PyPI upload step ("Publish the Regular + distributions to PyPI" above); run the check against it: + +```shell script +cd "$AIRFLOW_REPO_ROOT" +breeze release-management check-release-files providers --release-date "${RELEASE_DATE}" \ + --packages-file ./files/packages.txt \ + --path-to-airflow-svn "$(cd ../asf-dist/dev/airflow && pwd -P)" +``` + + It exits non-zero and lists every missing file (including `.asc`/`.sha512` variants) if anything is + absent. Only proceed to the vote once it prints `All expected files are present!`. + Send out a vote to the dev@airflow.apache.org mailing list. Here you can prepare text of the email. @@ -1004,13 +1017,14 @@ to verify that all expected files are present in SVN. This command will produce may help with verifying installation of the packages. Once you have cloned/updated the SVN repository, copy the PyPi URLs shared -in the email to a file called `packages.txt` in the $AIRFLOW_REPO_ROOT/dev -directory. +in the email to a file called `packages.txt` in the $AIRFLOW_REPO_ROOT/files +directory (git-ignored, so it won't be accidentally committed). ```shell script cd "$AIRFLOW_REPO_ROOT" +mkdir -p files # Copy packages.txt extracted from the mail sent by the release manager here -breeze release-management check-release-files providers --release-date "${RELEASE_DATE}" --packages-file ./dev/packages.txt --path-to-airflow-svn "${PATH_TO_AIRFLOW_SVN}" +breeze release-management check-release-files providers --release-date "${RELEASE_DATE}" --packages-file ./files/packages.txt --path-to-airflow-svn "${PATH_TO_AIRFLOW_SVN}" ``` After the above command completes you can build `Dockerfile.pmc` to trigger an installation of each provider @@ -1566,17 +1580,24 @@ twine check ${AIRFLOW_REPO_ROOT}/dist/*.whl ${AIRFLOW_REPO_ROOT}/dist/*.tar.gz This is a defence-in-depth practice: the RM machine becomes a one-time release vehicle, not a persistent point of compromise. -* Upload the package to PyPI: +* Upload the packages to PyPI. Only the provider wheels and sdists are uploaded — the ASF + `-source.tar.gz` tarball and the `.asc`/`.sha512` files are SVN-only. The PyPI URLs twine prints + are extracted (sorted, de-duplicated) into `files/packages.txt` for the announcement message; both + files land in `files/` (git-ignored): ```shell script -twine upload -r pypi ${AIRFLOW_REPO_ROOT}/dist/*.whl ${AIRFLOW_REPO_ROOT}/dist/*.tar.gz +mkdir -p "${AIRFLOW_REPO_ROOT}/files" +# COLUMNS=200 stops rich (twine's printer) from wrapping long URLs when stdout is a pipe. +COLUMNS=200 twine upload -r pypi ${AIRFLOW_REPO_ROOT}/dist/*.whl \ + $(ls ${AIRFLOW_REPO_ROOT}/dist/*.tar.gz | grep -v -- '-source.tar.gz') 2>&1 \ + | tee "${AIRFLOW_REPO_ROOT}/files/twine-upload.log" +# Only trust packages.txt if the upload above succeeded (PIPESTATUS[0] == 0): +grep -oE 'https://pypi\.org/project/[^[:space:]]+' "${AIRFLOW_REPO_ROOT}/files/twine-upload.log" \ + | sort -u > "${AIRFLOW_REPO_ROOT}/files/packages.txt" ``` -* Verify that the packages are available under the links printed. - -Copy links to updated packages, sort it alphabetically and save it on the side. You will need it for the announcement message. - -* Again, confirm that the packages are available under the links printed. +* Confirm that the packages are available under the links printed. The sorted list is saved in + `files/packages.txt` for the announcement message. ## Add the final release tag in git From 2cadf44ecba4064e0b1885e747b59c4cabf964d7 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sat, 4 Jul 2026 08:34:12 +0300 Subject: [PATCH 021/297] Bump the github-actions-updates group with 4 updates (#69353) Bumps the github-actions-updates group with 4 updates: [actions/setup-java](https://github.com/actions/setup-java), [actions/setup-go](https://github.com/actions/setup-go), [actions/setup-python](https://github.com/actions/setup-python) and [aws-actions/configure-aws-credentials](https://github.com/aws-actions/configure-aws-credentials). Updates `actions/setup-java` from 5.3.0 to 5.4.0 - [Release notes](https://github.com/actions/setup-java/releases) - [Commits](https://github.com/actions/setup-java/compare/ad2b38190b15e4d6bdf0c97fb4fca8412226d287...1bcf9fb12cf4aa7d266a90ae39939e61372fe520) Updates `actions/setup-go` from 6.4.0 to 6.5.0 - [Release notes](https://github.com/actions/setup-go/releases) - [Commits](https://github.com/actions/setup-go/compare/4a3601121dd01d1626a1e23e37211e3254c1c06c...924ae3a1cded613372ab5595356fb5720e22ba16) Updates `actions/setup-python` from 6.2.0 to 6.3.0 - [Release notes](https://github.com/actions/setup-python/releases) - [Commits](https://github.com/actions/setup-python/compare/a309ff8b426b58ec0e2a45f0f869d46889d02405...ece7cb06caefa5fff74198d8649806c4678c61a1) Updates `aws-actions/configure-aws-credentials` from 6.2.0 to 6.2.1 - [Release notes](https://github.com/aws-actions/configure-aws-credentials/releases) - [Changelog](https://github.com/aws-actions/configure-aws-credentials/blob/main/CHANGELOG.md) - [Commits](https://github.com/aws-actions/configure-aws-credentials/compare/e7f100cf4c008499ea8adda475de1042d6975c7b...254c19bd240aabef8777f48595e9d2d7b972184b) --- updated-dependencies: - dependency-name: actions/setup-java dependency-version: 5.4.0 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: github-actions-updates - dependency-name: actions/setup-go dependency-version: 6.5.0 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: github-actions-updates - dependency-name: actions/setup-python dependency-version: 6.3.0 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: github-actions-updates - dependency-name: aws-actions/configure-aws-credentials dependency-version: 6.2.1 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: github-actions-updates ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/basic-tests.yml | 2 +- .github/workflows/ci-amd.yml | 6 +++--- .github/workflows/ci-arm.yml | 6 +++--- .github/workflows/ci-image-checks.yml | 2 +- .github/workflows/codeql-analysis.yml | 2 +- .github/workflows/publish-docs-to-s3.yml | 2 +- .github/workflows/registry-backfill.yml | 4 ++-- .github/workflows/registry-build.yml | 2 +- 8 files changed, 13 insertions(+), 13 deletions(-) diff --git a/.github/workflows/basic-tests.yml b/.github/workflows/basic-tests.yml index 714aef1139ca4..0912b0722ec59 100644 --- a/.github/workflows/basic-tests.yml +++ b/.github/workflows/basic-tests.yml @@ -115,7 +115,7 @@ jobs: - name: "Install SVN" run: sudo apt-get update && sudo apt-get install -y subversion - name: "Install Java (for Apache RAT)" - uses: actions/setup-java@ad2b38190b15e4d6bdf0c97fb4fca8412226d287 # v5.3.0 + uses: actions/setup-java@1bcf9fb12cf4aa7d266a90ae39939e61372fe520 # v5.4.0 with: distribution: 'temurin' java-version: '17' diff --git a/.github/workflows/ci-amd.yml b/.github/workflows/ci-amd.yml index a1e07e1f16c15..e0a4b5ab788e0 100644 --- a/.github/workflows/ci-amd.yml +++ b/.github/workflows/ci-amd.yml @@ -945,7 +945,7 @@ jobs: persist-credentials: false # keep this in sync with go.mod in go-sdk/ - name: Setup Go - uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6.4.0 + uses: actions/setup-go@924ae3a1cded613372ab5595356fb5720e22ba16 # v6.5.0 with: go-version: 1.24 cache-dependency-path: go-sdk/go.sum @@ -981,7 +981,7 @@ jobs: with: persist-credentials: false - name: Setup Java - uses: actions/setup-java@ad2b38190b15e4d6bdf0c97fb4fca8412226d287 # v5.3.0 + uses: actions/setup-java@1bcf9fb12cf4aa7d266a90ae39939e61372fe520 # v5.4.0 with: distribution: 'temurin' java-version: ${{ env.JAVA_VERSION }} @@ -1235,7 +1235,7 @@ jobs: path: ./artifacts pattern: test-warnings-* - name: "Setup python" - uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0 + uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6.3.0 with: python-version: "${{ inputs.default-python-version }}" - name: "Summarize all warnings" diff --git a/.github/workflows/ci-arm.yml b/.github/workflows/ci-arm.yml index 71847cc44b5c7..e1552cde6b493 100644 --- a/.github/workflows/ci-arm.yml +++ b/.github/workflows/ci-arm.yml @@ -938,7 +938,7 @@ jobs: persist-credentials: false # keep this in sync with go.mod in go-sdk/ - name: Setup Go - uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6.4.0 + uses: actions/setup-go@924ae3a1cded613372ab5595356fb5720e22ba16 # v6.5.0 with: go-version: 1.24 cache-dependency-path: go-sdk/go.sum @@ -974,7 +974,7 @@ jobs: with: persist-credentials: false - name: Setup Java - uses: actions/setup-java@ad2b38190b15e4d6bdf0c97fb4fca8412226d287 # v5.3.0 + uses: actions/setup-java@1bcf9fb12cf4aa7d266a90ae39939e61372fe520 # v5.4.0 with: distribution: 'temurin' java-version: ${{ env.JAVA_VERSION }} @@ -1228,7 +1228,7 @@ jobs: path: ./artifacts pattern: test-warnings-* - name: "Setup python" - uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0 + uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6.3.0 with: python-version: "${{ inputs.default-python-version }}" - name: "Summarize all warnings" diff --git a/.github/workflows/ci-image-checks.yml b/.github/workflows/ci-image-checks.yml index 5eba03f950cae..9f845ebb0d5ee 100644 --- a/.github/workflows/ci-image-checks.yml +++ b/.github/workflows/ci-image-checks.yml @@ -441,7 +441,7 @@ jobs: inputs.canary-run == 'true' && (github.event_name == 'schedule' || github.event_name == 'workflow_dispatch') - name: Configure AWS credentials - uses: aws-actions/configure-aws-credentials@e7f100cf4c008499ea8adda475de1042d6975c7b # v6.2.0 + uses: aws-actions/configure-aws-credentials@254c19bd240aabef8777f48595e9d2d7b972184b # v6.2.1 with: aws-access-key-id: ${{ secrets.DOCS_AWS_ACCESS_KEY_ID }} aws-secret-access-key: ${{ secrets.DOCS_AWS_SECRET_ACCESS_KEY }} diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml index 20dbac2c97e7d..ec2d9e6fd8feb 100644 --- a/.github/workflows/codeql-analysis.yml +++ b/.github/workflows/codeql-analysis.yml @@ -116,7 +116,7 @@ jobs: - name: Setup Java if: matrix.language == 'java' - uses: actions/setup-java@ad2b38190b15e4d6bdf0c97fb4fca8412226d287 # v5.3.0 + uses: actions/setup-java@1bcf9fb12cf4aa7d266a90ae39939e61372fe520 # v5.4.0 with: distribution: 'temurin' java-version: '11' diff --git a/.github/workflows/publish-docs-to-s3.yml b/.github/workflows/publish-docs-to-s3.yml index 739be11a17379..e89b1cc9db290 100644 --- a/.github/workflows/publish-docs-to-s3.yml +++ b/.github/workflows/publish-docs-to-s3.yml @@ -507,7 +507,7 @@ jobs: sudo /tmp/aws/install --update rm -rf /tmp/aws/ - name: Configure AWS credentials - uses: aws-actions/configure-aws-credentials@e7f100cf4c008499ea8adda475de1042d6975c7b # v6.2.0 + uses: aws-actions/configure-aws-credentials@254c19bd240aabef8777f48595e9d2d7b972184b # v6.2.1 with: aws-access-key-id: ${{ secrets.DOCS_AWS_ACCESS_KEY_ID }} aws-secret-access-key: ${{ secrets.DOCS_AWS_SECRET_ACCESS_KEY }} diff --git a/.github/workflows/registry-backfill.yml b/.github/workflows/registry-backfill.yml index 6bd6493a09ee7..69ae19e840195 100644 --- a/.github/workflows/registry-backfill.yml +++ b/.github/workflows/registry-backfill.yml @@ -162,7 +162,7 @@ jobs: rm -rf /tmp/aws/ - name: "Configure AWS credentials" - uses: aws-actions/configure-aws-credentials@e7f100cf4c008499ea8adda475de1042d6975c7b # v6.2.0 + uses: aws-actions/configure-aws-credentials@254c19bd240aabef8777f48595e9d2d7b972184b # v6.2.1 with: aws-access-key-id: ${{ secrets.DOCS_AWS_ACCESS_KEY_ID }} aws-secret-access-key: ${{ secrets.DOCS_AWS_SECRET_ACCESS_KEY }} @@ -304,7 +304,7 @@ jobs: rm -rf /tmp/aws/ - name: "Configure AWS credentials" - uses: aws-actions/configure-aws-credentials@e7f100cf4c008499ea8adda475de1042d6975c7b # v6.2.0 + uses: aws-actions/configure-aws-credentials@254c19bd240aabef8777f48595e9d2d7b972184b # v6.2.1 with: aws-access-key-id: ${{ secrets.DOCS_AWS_ACCESS_KEY_ID }} aws-secret-access-key: ${{ secrets.DOCS_AWS_SECRET_ACCESS_KEY }} diff --git a/.github/workflows/registry-build.yml b/.github/workflows/registry-build.yml index 81847483ea448..a6f138bd4449e 100644 --- a/.github/workflows/registry-build.yml +++ b/.github/workflows/registry-build.yml @@ -138,7 +138,7 @@ jobs: rm -rf /tmp/aws/ - name: "Configure AWS credentials" - uses: aws-actions/configure-aws-credentials@e7f100cf4c008499ea8adda475de1042d6975c7b # v6.2.0 + uses: aws-actions/configure-aws-credentials@254c19bd240aabef8777f48595e9d2d7b972184b # v6.2.1 with: aws-access-key-id: ${{ secrets.DOCS_AWS_ACCESS_KEY_ID }} aws-secret-access-key: ${{ secrets.DOCS_AWS_SECRET_ACCESS_KEY }} From 9f2e915fff24442aed95497edce6767daf594de1 Mon Sep 17 00:00:00 2001 From: Wei Lee Date: Sat, 4 Jul 2026 18:23:48 +0800 Subject: [PATCH 022/297] [v3-3-test] Return 422 for empty backfill window and stop leaving orphan rows (#68883) (#69367) --- .../core_api/routes/public/backfills.py | 2 + .../airflow/cli/commands/backfill_command.py | 48 +++++----- airflow-core/src/airflow/models/backfill.py | 31 +++++-- .../core_api/routes/public/test_backfills.py | 58 +++++++++++- .../cli/commands/test_backfill_command.py | 61 +++++++++++- .../tests/unit/models/test_backfill.py | 92 ++++++++++++++++++- 6 files changed, 255 insertions(+), 37 deletions(-) diff --git a/airflow-core/src/airflow/api_fastapi/core_api/routes/public/backfills.py b/airflow-core/src/airflow/api_fastapi/core_api/routes/public/backfills.py index 0dba6086e0aee..70c353fb7a22c 100644 --- a/airflow-core/src/airflow/api_fastapi/core_api/routes/public/backfills.py +++ b/airflow-core/src/airflow/api_fastapi/core_api/routes/public/backfills.py @@ -56,6 +56,7 @@ InvalidBackfillDateRange, InvalidBackfillDirection, InvalidReprocessBehavior, + NoBackfillRunsToCreate, _create_backfill, _do_dry_run, ) @@ -276,6 +277,7 @@ def create_backfill( InvalidBackfillDate, InvalidBackfillDateRange, InvalidBackfillConf, + NoBackfillRunsToCreate, ) as e: raise RequestValidationError(str(e)) diff --git a/airflow-core/src/airflow/cli/commands/backfill_command.py b/airflow-core/src/airflow/cli/commands/backfill_command.py index d76d435a391a4..75ba6b03ce4af 100644 --- a/airflow-core/src/airflow/cli/commands/backfill_command.py +++ b/airflow-core/src/airflow/cli/commands/backfill_command.py @@ -27,7 +27,7 @@ from airflow.api_fastapi.common.dagbag import resolve_run_on_latest_version from airflow.cli.simple_table import AirflowConsole from airflow.exceptions import AirflowConfigException -from airflow.models.backfill import ReprocessBehavior, _create_backfill, _do_dry_run +from airflow.models.backfill import NoBackfillRunsToCreate, ReprocessBehavior, _create_backfill, _do_dry_run from airflow.utils import cli as cli_utils from airflow.utils.cli import sigint_handler from airflow.utils.platform import getuser @@ -50,6 +50,13 @@ def create_backfill(args) -> None: else: reprocess_behavior = None + dag_run_conf = None + if args.dag_run_conf: + try: + dag_run_conf = json.loads(args.dag_run_conf) + except json.JSONDecodeError as e: + raise ValueError(f"Invalid JSON in --dag-run-conf: {e}") + with create_session() as session: resolved_run_on_latest = resolve_run_on_latest_version( args.run_on_latest_version, @@ -67,7 +74,7 @@ def create_backfill(args) -> None: to_date=args.to_date, max_active_runs=args.max_active_runs, reverse=args.run_backwards, - dag_run_conf=args.dag_run_conf, + dag_run_conf=dag_run_conf, reprocess_behavior=reprocess_behavior, run_on_latest_version=resolved_run_on_latest, ) @@ -79,7 +86,8 @@ def create_backfill(args) -> None: from_date=args.from_date, to_date=args.to_date, reverse=args.run_backwards, - reprocess_behavior=args.reprocess_behavior, + reprocess_behavior=reprocess_behavior or ReprocessBehavior.NONE, + dag_run_conf=dag_run_conf, session=session, ) console.print("Runs to be attempted:") @@ -97,22 +105,18 @@ def create_backfill(args) -> None: log.warning("Failed to get user name from os: %s, not setting the triggering user", e) user = None - # Parse dag_run_conf if provided - dag_run_conf = None - if args.dag_run_conf: - try: - dag_run_conf = json.loads(args.dag_run_conf) - except json.JSONDecodeError as e: - raise ValueError(f"Invalid JSON in --dag-run-conf: {e}") - - _create_backfill( - dag_id=args.dag_id, - from_date=args.from_date, - to_date=args.to_date, - max_active_runs=args.max_active_runs, - reverse=args.run_backwards, - dag_run_conf=dag_run_conf, - triggering_user_name=user, - reprocess_behavior=reprocess_behavior, - run_on_latest_version=resolved_run_on_latest, - ) + try: + _create_backfill( + dag_id=args.dag_id, + from_date=args.from_date, + to_date=args.to_date, + max_active_runs=args.max_active_runs, + reverse=args.run_backwards, + dag_run_conf=dag_run_conf, + triggering_user_name=user, + reprocess_behavior=reprocess_behavior, + run_on_latest_version=resolved_run_on_latest, + ) + except NoBackfillRunsToCreate as e: + console.print(f"[yellow]Warning:[/yellow] {e}") + raise SystemExit(1) diff --git a/airflow-core/src/airflow/models/backfill.py b/airflow-core/src/airflow/models/backfill.py index 52019fa5d984b..1f507de94bb92 100644 --- a/airflow-core/src/airflow/models/backfill.py +++ b/airflow-core/src/airflow/models/backfill.py @@ -123,6 +123,17 @@ class InvalidBackfillConf(AirflowException): """ +class NoBackfillRunsToCreate(ValueError): + """ + Raised when a backfill request yields no Dag runs for the given date range. + + This happens when the from/to date range falls entirely outside the Dag's + scheduled intervals (e.g. the range predates the first partition boundary). + + :meta private: + """ + + class UnknownActiveBackfills(AirflowException): """ Raised when the quantity of active backfills cannot be determined. @@ -665,6 +676,17 @@ def _create_backfill( dag_run_conf, ) + dagrun_info_list = _get_info_list( + from_date=from_date, + to_date=to_date, + reverse=reverse, + dag=dag, + ) + if not dagrun_info_list: + raise NoBackfillRunsToCreate( + f"No runs to create for Dag {dag_id} in the range [{from_date}, {to_date}]" + ) + br = Backfill( dag_id=dag_id, from_date=from_date, @@ -680,15 +702,6 @@ def _create_backfill( session.scalars(select(DagModel).where(DagModel.dag_id == dag_id)).one() - dagrun_info_list = _get_info_list( - from_date=from_date, - to_date=to_date, - reverse=reverse, - dag=dag, - ) - if not dagrun_info_list: - raise RuntimeError(f"No runs to create for Dag {dag_id}") - first_info = dagrun_info_list[0] if first_info.partition_key: _create_runs_partitioned( diff --git a/airflow-core/tests/unit/api_fastapi/core_api/routes/public/test_backfills.py b/airflow-core/tests/unit/api_fastapi/core_api/routes/public/test_backfills.py index 42b48fc0c4931..2380f73b9bf7b 100644 --- a/airflow-core/tests/unit/api_fastapi/core_api/routes/public/test_backfills.py +++ b/airflow-core/tests/unit/api_fastapi/core_api/routes/public/test_backfills.py @@ -27,7 +27,13 @@ from airflow._shared.timezones import timezone from airflow.dag_processing.dagbag import DagBag from airflow.models import DagModel, DagRun -from airflow.models.backfill import Backfill, BackfillDagRun, ReprocessBehavior, _create_backfill +from airflow.models.backfill import ( + Backfill, + BackfillDagRun, + NoBackfillRunsToCreate, + ReprocessBehavior, + _create_backfill, +) from airflow.models.dag import DAG from airflow.models.dagbundle import DagBundleModel from airflow.providers.standard.operators.empty import EmptyOperator @@ -965,6 +971,56 @@ def test_partitioned_dag_from_date_after_to_date_returns_422(self, session, dag_ response = test_client.post(url=url, json=data) assert response.status_code == 422 + @mock.patch("airflow.api_fastapi.core_api.routes.public.backfills._create_backfill", autospec=True) + def test_empty_window_create_returns_422(self, mock_create, session, dag_maker, test_client): + """POST /backfills with an empty window raises NoBackfillRunsToCreate → 422.""" + mock_create.side_effect = NoBackfillRunsToCreate( + "No runs to create for Dag TEST_PARTITIONED_DAG in the range [...]" + ) + with dag_maker( + session=session, + dag_id="TEST_PARTITIONED_DAG", + schedule=CronPartitionTimetable("0 0 * * *", timezone="Asia/Taipei"), + ): + EmptyOperator(task_id="mytask") + session.commit() + + data = { + "dag_id": "TEST_PARTITIONED_DAG", + "from_date": "2026-02-18T00:00:00+00:00", + "to_date": "2026-02-18T00:00:00+00:00", + "max_active_runs": 5, + "run_backwards": False, + } + response = test_client.post(url="/backfills", json=data) + assert response.status_code == 422 + + @mock.patch("airflow.api_fastapi.core_api.routes.public.backfills._do_dry_run", autospec=True) + def test_empty_window_dry_run_returns_200_with_zero_entries( + self, mock_dry_run, session, dag_maker, test_client + ): + """POST /backfills/dry_run with an empty window returns 200 with total_entries == 0.""" + mock_dry_run.return_value = iter([]) + with dag_maker( + session=session, + dag_id="TEST_PARTITIONED_DAG", + schedule=CronPartitionTimetable("0 0 * * *", timezone="Asia/Taipei"), + ): + EmptyOperator(task_id="mytask") + session.commit() + + data = { + "dag_id": "TEST_PARTITIONED_DAG", + "from_date": "2026-02-18T00:00:00+00:00", + "to_date": "2026-02-18T00:00:00+00:00", + "max_active_runs": 5, + "run_backwards": False, + } + response = test_client.post(url="/backfills/dry_run", json=data) + assert response.status_code == 200 + assert response.json()["total_entries"] == 0 + assert response.json()["backfills"] == [] + class TestCancelBackfill(TestBackfillEndpoint): def test_cancel_backfill(self, session, test_client): diff --git a/airflow-core/tests/unit/cli/commands/test_backfill_command.py b/airflow-core/tests/unit/cli/commands/test_backfill_command.py index c897fa204439f..5d5743349f717 100644 --- a/airflow-core/tests/unit/cli/commands/test_backfill_command.py +++ b/airflow-core/tests/unit/cli/commands/test_backfill_command.py @@ -164,7 +164,8 @@ def test_backfill_dry_run(self, mock_dry_run, reverse): from_date=DEFAULT_DATE.replace(tzinfo=timezone.utc), to_date=DEFAULT_DATE.replace(tzinfo=timezone.utc), reverse=reverse, - reprocess_behavior="none", + reprocess_behavior=ReprocessBehavior.NONE, + dag_run_conf=None, session=mock.ANY, ) @@ -240,9 +241,10 @@ def test_backfill_create_missing_to_date_raises(self): with pytest.raises(SystemExit): self.parser.parse_args(args) + @mock.patch("airflow.cli.commands.backfill_command.getuser", return_value="test_user") @mock.patch("airflow.cli.commands.backfill_command._create_backfill") - def test_backfill_with_empty_dag_run_conf(self, mock_create): - """Test that empty dag_run_conf is properly parsed.""" + def test_backfill_with_empty_dag_run_conf(self, mock_create, mock_getuser): + """Test that empty dag_run_conf ({}) is parsed as an empty dict, not None.""" args = [ "backfill", "create", @@ -265,6 +267,57 @@ def test_backfill_with_empty_dag_run_conf(self, mock_create): reverse=False, dag_run_conf={}, reprocess_behavior=None, - triggering_user_name="root", + triggering_user_name="test_user", run_on_latest_version=True, ) + + @mock.patch("airflow.cli.commands.backfill_command._do_dry_run", autospec=True) + def test_backfill_dry_run_passes_dag_run_conf(self, mock_dry_run): + """dry-run path forwards parsed dag_run_conf dict (not raw string) to _do_dry_run.""" + mock_dry_run.return_value = iter([]) + args = [ + "backfill", + "create", + "--dag-id", + "example_bash_operator", + "--from-date", + DEFAULT_DATE.isoformat(), + "--to-date", + DEFAULT_DATE.isoformat(), + "--dry-run", + "--reprocess-behavior", + "failed", + "--dag-run-conf", + '{"key": "val"}', + ] + airflow.cli.commands.backfill_command.create_backfill(self.parser.parse_args(args)) + + mock_dry_run.assert_called_once_with( + dag_id="example_bash_operator", + from_date=DEFAULT_DATE.replace(tzinfo=timezone.utc), + to_date=DEFAULT_DATE.replace(tzinfo=timezone.utc), + reverse=False, + reprocess_behavior=ReprocessBehavior.FAILED, + dag_run_conf={"key": "val"}, + session=mock.ANY, + ) + + @mock.patch("airflow.cli.commands.backfill_command._create_backfill", autospec=True) + def test_backfill_empty_window_shows_friendly_message(self, mock_create): + """Empty-window backfill exits with code 1 and prints a message, no traceback.""" + from airflow.models.backfill import NoBackfillRunsToCreate + + mock_create.side_effect = NoBackfillRunsToCreate("No runs to create for Dag example_bash_operator") + args = [ + "backfill", + "create", + "--dag-id", + "example_bash_operator", + "--from-date", + DEFAULT_DATE.isoformat(), + "--to-date", + DEFAULT_DATE.isoformat(), + ] + with pytest.raises(SystemExit) as exc_info: + airflow.cli.commands.backfill_command.create_backfill(self.parser.parse_args(args)) + assert exc_info.value.code == 1 diff --git a/airflow-core/tests/unit/models/test_backfill.py b/airflow-core/tests/unit/models/test_backfill.py index 69a4ebd5dc26a..ea621be84d6c1 100644 --- a/airflow-core/tests/unit/models/test_backfill.py +++ b/airflow-core/tests/unit/models/test_backfill.py @@ -20,10 +20,11 @@ from contextlib import nullcontext from datetime import datetime, timedelta from typing import TYPE_CHECKING +from unittest import mock import pendulum import pytest -from sqlalchemy import select +from sqlalchemy import func, select from airflow._shared.timezones import timezone from airflow.models import DagModel, DagRun, TaskInstance @@ -37,6 +38,7 @@ InvalidBackfillDateRange, InvalidBackfillDirection, InvalidReprocessBehavior, + NoBackfillRunsToCreate, ReprocessBehavior, _create_backfill, _do_dry_run, @@ -1384,6 +1386,94 @@ def test_partitioned_backfill_reprocess_failed(dag_maker, session): assert bdr.partition_key == info.partition_key +@mock.patch("airflow.models.backfill._get_info_list", autospec=True, return_value=[]) +def test_create_backfill_empty_window_raises_no_runs_to_create(mock_get_info_list, dag_maker, session): + """_create_backfill raises NoBackfillRunsToCreate when _get_info_list returns an empty list.""" + with dag_maker(schedule=CronPartitionTimetable("0 0 * * *", timezone="Asia/Taipei")) as dag: + PythonOperator(task_id="hi", python_callable=print) + session.commit() + + with pytest.raises(NoBackfillRunsToCreate, match=dag.dag_id): + _create_backfill( + dag_id=dag.dag_id, + from_date=pendulum.parse("2026-02-18"), + to_date=pendulum.parse("2026-02-18"), + max_active_runs=2, + reverse=False, + triggering_user_name="pytest", + dag_run_conf={}, + ) + + +@pytest.mark.parametrize("dag_run_conf", [None, {}]) +@mock.patch("airflow.models.backfill._get_info_list", autospec=True, return_value=[]) +def test_do_dry_run_empty_window_returns_empty_iterable(mock_get_info_list, dag_run_conf, dag_maker, session): + """_do_dry_run on an empty window yields nothing (does not raise), with or without dag_run_conf.""" + with dag_maker(schedule=CronPartitionTimetable("0 0 * * *", timezone="Asia/Taipei")) as dag: + PythonOperator(task_id="hi", python_callable=print) + session.commit() + + infos = list( + _do_dry_run( + dag_id=dag.dag_id, + from_date=pendulum.parse("2026-02-18"), + to_date=pendulum.parse("2026-02-18"), + reverse=False, + reprocess_behavior=ReprocessBehavior.NONE, + dag_run_conf=dag_run_conf, + session=session, + ) + ) + assert infos == [] + + +def test_create_backfill_real_empty_window_no_orphan(dag_maker, session): + """_create_backfill with a real empty window raises NoBackfillRunsToCreate without leaving an orphan. + + Uses a weekly-Monday timetable; 2026-02-18 is a Wednesday so _get_info_list returns [] for real. + After the raise, no incomplete Backfill row must exist — proving #1 (orphan fix) holds. + A second call for the same dag_id must succeed (not be blocked by AlreadyRunningBackfill). + """ + with dag_maker(schedule=CronPartitionTimetable("0 0 * * 1", timezone="UTC")) as dag: + PythonOperator(task_id="hi", python_callable=print) + session.commit() + + wednesday = pendulum.datetime(2026, 2, 18, tz="UTC") # not a Monday + + with pytest.raises(NoBackfillRunsToCreate, match=dag.dag_id): + _create_backfill( + dag_id=dag.dag_id, + from_date=wednesday, + to_date=wednesday, + max_active_runs=2, + reverse=False, + triggering_user_name="pytest", + dag_run_conf=None, + ) + + # No orphan: zero incomplete Backfill rows for this dag + orphan_count = session.scalar( + select(func.count()).where( + Backfill.dag_id == dag.dag_id, + Backfill.completed_at.is_(None), + ) + ) + assert orphan_count == 0, "An orphan Backfill row was left behind after NoBackfillRunsToCreate" + + # A valid (Monday) window must not be blocked by AlreadyRunningBackfill + monday = pendulum.datetime(2026, 2, 23, tz="UTC") # a Monday + br = _create_backfill( + dag_id=dag.dag_id, + from_date=monday, + to_date=monday, + max_active_runs=2, + reverse=False, + triggering_user_name="pytest", + dag_run_conf=None, + ) + assert br is not None + + def test_handle_clear_run_preserves_partition_key(dag_maker, session): """BackfillDagRun created via the clear/reprocess path carries partition_key from info.""" From 02ef6df00beacdadfd9e07c705c69233edcf95f6 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sun, 5 Jul 2026 14:59:58 +0200 Subject: [PATCH 023/297] Bump the 3-3-auth-ui-package-updates group across 1 directory with 11 updates (#69350) Bumps the 3-3-auth-ui-package-updates group with 11 updates in the /airflow-core/src/airflow/api_fastapi/auth/managers/simple/ui directory: | Package | From | To | | --- | --- | --- | | [@hey-api/openapi-ts](https://github.com/hey-api/hey-api/tree/HEAD/packages/openapi-ts) | `0.98.2` | `0.99.0` | | [@tanstack/react-query](https://github.com/TanStack/query/tree/HEAD/packages/react-query) | `5.101.0` | `5.101.2` | | [axios](https://github.com/axios/axios) | `1.18.0` | `1.18.1` | | [react-hook-form](https://github.com/react-hook-form/react-hook-form) | `7.79.0` | `7.80.0` | | [@typescript-eslint/eslint-plugin](https://github.com/typescript-eslint/typescript-eslint/tree/HEAD/packages/eslint-plugin) | `8.61.1` | `8.62.0` | | [@typescript-eslint/parser](https://github.com/typescript-eslint/typescript-eslint/tree/HEAD/packages/parser) | `8.61.1` | `8.62.0` | | [@typescript-eslint/utils](https://github.com/typescript-eslint/typescript-eslint/tree/HEAD/packages/utils) | `8.61.1` | `8.62.0` | | [eslint](https://github.com/eslint/eslint) | `10.5.0` | `10.6.0` | | [prettier](https://github.com/prettier/prettier) | `3.8.4` | `3.9.3` | | [typescript-eslint](https://github.com/typescript-eslint/typescript-eslint/tree/HEAD/packages/typescript-eslint) | `8.61.1` | `8.62.0` | | [vite](https://github.com/vitejs/vite/tree/HEAD/packages/vite) | `8.0.16` | `8.1.0` | Updates `@hey-api/openapi-ts` from 0.98.2 to 0.99.0 - [Release notes](https://github.com/hey-api/hey-api/releases) - [Changelog](https://github.com/hey-api/hey-api/blob/main/packages/openapi-ts/CHANGELOG.md) - [Commits](https://github.com/hey-api/hey-api/commits/@hey-api/openapi-ts@0.99.0/packages/openapi-ts) Updates `@tanstack/react-query` from 5.101.0 to 5.101.2 - [Release notes](https://github.com/TanStack/query/releases) - [Changelog](https://github.com/TanStack/query/blob/main/packages/react-query/CHANGELOG.md) - [Commits](https://github.com/TanStack/query/commits/@tanstack/react-query@5.101.2/packages/react-query) Updates `axios` from 1.18.0 to 1.18.1 - [Release notes](https://github.com/axios/axios/releases) - [Changelog](https://github.com/axios/axios/blob/v1.x/CHANGELOG.md) - [Commits](https://github.com/axios/axios/compare/v1.18.0...v1.18.1) Updates `react-hook-form` from 7.79.0 to 7.80.0 - [Release notes](https://github.com/react-hook-form/react-hook-form/releases) - [Changelog](https://github.com/react-hook-form/react-hook-form/blob/master/CHANGELOG.md) - [Commits](https://github.com/react-hook-form/react-hook-form/compare/v7.79.0...v7.80.0) Updates `@typescript-eslint/eslint-plugin` from 8.61.1 to 8.62.0 - [Release notes](https://github.com/typescript-eslint/typescript-eslint/releases) - [Changelog](https://github.com/typescript-eslint/typescript-eslint/blob/main/packages/eslint-plugin/CHANGELOG.md) - [Commits](https://github.com/typescript-eslint/typescript-eslint/commits/v8.62.0/packages/eslint-plugin) Updates `@typescript-eslint/parser` from 8.61.1 to 8.62.0 - [Release notes](https://github.com/typescript-eslint/typescript-eslint/releases) - [Changelog](https://github.com/typescript-eslint/typescript-eslint/blob/main/packages/parser/CHANGELOG.md) - [Commits](https://github.com/typescript-eslint/typescript-eslint/commits/v8.62.0/packages/parser) Updates `@typescript-eslint/utils` from 8.61.1 to 8.62.0 - [Release notes](https://github.com/typescript-eslint/typescript-eslint/releases) - [Changelog](https://github.com/typescript-eslint/typescript-eslint/blob/main/packages/utils/CHANGELOG.md) - [Commits](https://github.com/typescript-eslint/typescript-eslint/commits/v8.62.0/packages/utils) Updates `eslint` from 10.5.0 to 10.6.0 - [Release notes](https://github.com/eslint/eslint/releases) - [Commits](https://github.com/eslint/eslint/compare/v10.5.0...v10.6.0) Updates `prettier` from 3.8.4 to 3.9.3 - [Release notes](https://github.com/prettier/prettier/releases) - [Changelog](https://github.com/prettier/prettier/blob/main/CHANGELOG.md) - [Commits](https://github.com/prettier/prettier/compare/3.8.4...3.9.3) Updates `typescript-eslint` from 8.61.1 to 8.62.0 - [Release notes](https://github.com/typescript-eslint/typescript-eslint/releases) - [Changelog](https://github.com/typescript-eslint/typescript-eslint/blob/main/packages/typescript-eslint/CHANGELOG.md) - [Commits](https://github.com/typescript-eslint/typescript-eslint/commits/v8.62.0/packages/typescript-eslint) Updates `vite` from 8.0.16 to 8.1.0 - [Release notes](https://github.com/vitejs/vite/releases) - [Changelog](https://github.com/vitejs/vite/blob/main/packages/vite/CHANGELOG.md) - [Commits](https://github.com/vitejs/vite/commits/create-vite@8.1.0/packages/vite) --- updated-dependencies: - dependency-name: "@hey-api/openapi-ts" dependency-version: 0.99.0 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: 3-3-auth-ui-package-updates - dependency-name: "@tanstack/react-query" dependency-version: 5.101.2 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: 3-3-auth-ui-package-updates - dependency-name: axios dependency-version: 1.18.1 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: 3-3-auth-ui-package-updates - dependency-name: react-hook-form dependency-version: 7.80.0 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: 3-3-auth-ui-package-updates - dependency-name: "@typescript-eslint/eslint-plugin" dependency-version: 8.62.0 dependency-type: direct:development update-type: version-update:semver-minor dependency-group: 3-3-auth-ui-package-updates - dependency-name: "@typescript-eslint/parser" dependency-version: 8.62.0 dependency-type: direct:development update-type: version-update:semver-minor dependency-group: 3-3-auth-ui-package-updates - dependency-name: "@typescript-eslint/utils" dependency-version: 8.62.0 dependency-type: direct:development update-type: version-update:semver-minor dependency-group: 3-3-auth-ui-package-updates - dependency-name: eslint dependency-version: 10.6.0 dependency-type: direct:development update-type: version-update:semver-minor dependency-group: 3-3-auth-ui-package-updates - dependency-name: prettier dependency-version: 3.9.3 dependency-type: direct:development update-type: version-update:semver-minor dependency-group: 3-3-auth-ui-package-updates - dependency-name: typescript-eslint dependency-version: 8.62.0 dependency-type: direct:development update-type: version-update:semver-minor dependency-group: 3-3-auth-ui-package-updates - dependency-name: vite dependency-version: 8.1.0 dependency-type: direct:development update-type: version-update:semver-minor dependency-group: 3-3-auth-ui-package-updates ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .../auth/managers/simple/ui/package.json | 22 +- .../auth/managers/simple/ui/pnpm-lock.yaml | 629 +++++++++--------- 2 files changed, 335 insertions(+), 316 deletions(-) diff --git a/airflow-core/src/airflow/api_fastapi/auth/managers/simple/ui/package.json b/airflow-core/src/airflow/api_fastapi/auth/managers/simple/ui/package.json index a00c95abbcabd..b2241c8eb1c6c 100644 --- a/airflow-core/src/airflow/api_fastapi/auth/managers/simple/ui/package.json +++ b/airflow-core/src/airflow/api_fastapi/auth/managers/simple/ui/package.json @@ -20,14 +20,14 @@ "dependencies": { "@chakra-ui/react": "^3.36.0", "@hey-api/client-axios": "^0.9.1", - "@hey-api/openapi-ts": "^0.98.2", - "@tanstack/react-query": "^5.101.0", - "axios": "^1.18.0", + "@hey-api/openapi-ts": "^0.99.0", + "@tanstack/react-query": "^5.101.2", + "axios": "^1.18.1", "next-themes": "^0.4.6", "react": "^19.2.7", "react-cookie": "^8.1.2", "react-dom": "^19.2.7", - "react-hook-form": "^7.79.0", + "react-hook-form": "^7.80.0", "react-router-dom": "^7.18.0" }, "devDependencies": { @@ -40,12 +40,12 @@ "@trivago/prettier-plugin-sort-imports": "^6.0.2", "@types/react": "^19.2.17", "@types/react-dom": "^19.2.3", - "@typescript-eslint/eslint-plugin": "8.61.1", - "@typescript-eslint/parser": "8.61.1", - "@typescript-eslint/utils": "^8.61.1", + "@typescript-eslint/eslint-plugin": "8.62.0", + "@typescript-eslint/parser": "8.62.0", + "@typescript-eslint/utils": "^8.62.0", "@vitejs/plugin-react-swc": "^4.3.1", "@vitest/coverage-v8": "^4.1.9", - "eslint": "^10.5.0", + "eslint": "^10.6.0", "eslint-config-prettier": "^10.1.8", "eslint-plugin-jsx-a11y": "^6.10.2", "eslint-plugin-perfectionist": "^5.9.1", @@ -55,11 +55,11 @@ "eslint-plugin-react-refresh": "^0.5.3", "eslint-plugin-unicorn": "^67.0.0", "happy-dom": "^20.10.6", - "prettier": "^3.8.4", + "prettier": "^3.9.3", "ts-morph": "^28.0.0", "typescript": "~6.0.3", - "typescript-eslint": "^8.61.1", - "vite": "^8.0.16", + "typescript-eslint": "^8.62.0", + "vite": "^8.1.0", "vite-plugin-css-injected-by-js": "^5.0.1", "vitest": "^4.1.9" }, diff --git a/airflow-core/src/airflow/api_fastapi/auth/managers/simple/ui/pnpm-lock.yaml b/airflow-core/src/airflow/api_fastapi/auth/managers/simple/ui/pnpm-lock.yaml index 3c11980f1244e..a2c6c1c2f09cf 100644 --- a/airflow-core/src/airflow/api_fastapi/auth/managers/simple/ui/pnpm-lock.yaml +++ b/airflow-core/src/airflow/api_fastapi/auth/managers/simple/ui/pnpm-lock.yaml @@ -35,16 +35,16 @@ importers: version: 3.36.0(@emotion/react@11.14.0(@types/react@19.2.17)(react@19.2.7))(react-dom@19.2.7(react@19.2.7))(react@19.2.7) '@hey-api/client-axios': specifier: ^0.9.1 - version: 0.9.1(@hey-api/openapi-ts@0.98.2(magicast@0.5.3)(typescript@6.0.3))(axios@1.18.0) + version: 0.9.1(@hey-api/openapi-ts@0.99.0(magicast@0.5.3)(typescript@6.0.3))(axios@1.18.1) '@hey-api/openapi-ts': - specifier: ^0.98.2 - version: 0.98.2(magicast@0.5.3)(typescript@6.0.3) + specifier: ^0.99.0 + version: 0.99.0(magicast@0.5.3)(typescript@6.0.3) '@tanstack/react-query': - specifier: ^5.101.0 - version: 5.101.0(react@19.2.7) + specifier: ^5.101.2 + version: 5.101.2(react@19.2.7) axios: - specifier: ^1.18.0 - version: 1.18.0 + specifier: ^1.18.1 + version: 1.18.1 next-themes: specifier: ^0.4.6 version: 0.4.6(react-dom@19.2.7(react@19.2.7))(react@19.2.7) @@ -58,8 +58,8 @@ importers: specifier: ^19.2.7 version: 19.2.7(react@19.2.7) react-hook-form: - specifier: ^7.79.0 - version: 7.79.0(react@19.2.7) + specifier: ^7.80.0 + version: 7.80.0(react@19.2.7) react-router-dom: specifier: ^7.18.0 version: 7.18.0(react-dom@19.2.7(react@19.2.7))(react@19.2.7) @@ -69,13 +69,13 @@ importers: version: 2.1.0(commander@15.0.0)(magicast@0.5.3)(ts-morph@28.0.0)(typescript@6.0.3) '@eslint/compat': specifier: ^2.1.0 - version: 2.1.0(eslint@10.5.0(jiti@2.7.0)) + version: 2.1.0(eslint@10.6.0(jiti@2.7.0)) '@eslint/js': specifier: ^10.0.1 - version: 10.0.1(eslint@10.5.0(jiti@2.7.0)) + version: 10.0.1(eslint@10.6.0(jiti@2.7.0)) '@stylistic/eslint-plugin': specifier: ^5.10.0 - version: 5.10.0(eslint@10.5.0(jiti@2.7.0)) + version: 5.10.0(eslint@10.6.0(jiti@2.7.0)) '@testing-library/jest-dom': specifier: ^6.9.1 version: 6.9.1 @@ -84,7 +84,7 @@ importers: version: 16.3.2(@testing-library/dom@10.4.0)(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) '@trivago/prettier-plugin-sort-imports': specifier: ^6.0.2 - version: 6.0.2(prettier@3.8.4) + version: 6.0.2(prettier@3.9.3) '@types/react': specifier: ^19.2.17 version: 19.2.17 @@ -92,53 +92,53 @@ importers: specifier: ^19.2.3 version: 19.2.3(@types/react@19.2.17) '@typescript-eslint/eslint-plugin': - specifier: 8.61.1 - version: 8.61.1(@typescript-eslint/parser@8.61.1(eslint@10.5.0(jiti@2.7.0))(typescript@6.0.3))(eslint@10.5.0(jiti@2.7.0))(typescript@6.0.3) + specifier: 8.62.0 + version: 8.62.0(@typescript-eslint/parser@8.62.0(eslint@10.6.0(jiti@2.7.0))(typescript@6.0.3))(eslint@10.6.0(jiti@2.7.0))(typescript@6.0.3) '@typescript-eslint/parser': - specifier: 8.61.1 - version: 8.61.1(eslint@10.5.0(jiti@2.7.0))(typescript@6.0.3) + specifier: 8.62.0 + version: 8.62.0(eslint@10.6.0(jiti@2.7.0))(typescript@6.0.3) '@typescript-eslint/utils': - specifier: ^8.61.1 - version: 8.61.1(eslint@10.5.0(jiti@2.7.0))(typescript@6.0.3) + specifier: ^8.62.0 + version: 8.62.0(eslint@10.6.0(jiti@2.7.0))(typescript@6.0.3) '@vitejs/plugin-react-swc': specifier: ^4.3.1 - version: 4.3.1(@swc/helpers@0.5.23)(vite@8.0.16(@types/node@26.0.0)(jiti@2.7.0)(yaml@2.8.3)) + version: 4.3.1(@swc/helpers@0.5.23)(vite@8.1.0(@types/node@26.0.0)(jiti@2.7.0)(yaml@2.8.3)) '@vitest/coverage-v8': specifier: ^4.1.9 version: 4.1.9(vitest@4.1.9) eslint: - specifier: ^10.5.0 - version: 10.5.0(jiti@2.7.0) + specifier: ^10.6.0 + version: 10.6.0(jiti@2.7.0) eslint-config-prettier: specifier: ^10.1.8 - version: 10.1.8(eslint@10.5.0(jiti@2.7.0)) + version: 10.1.8(eslint@10.6.0(jiti@2.7.0)) eslint-plugin-jsx-a11y: specifier: ^6.10.2 - version: 6.10.2(eslint@10.5.0(jiti@2.7.0)) + version: 6.10.2(eslint@10.6.0(jiti@2.7.0)) eslint-plugin-perfectionist: specifier: ^5.9.1 - version: 5.9.1(eslint@10.5.0(jiti@2.7.0))(typescript@6.0.3) + version: 5.9.1(eslint@10.6.0(jiti@2.7.0))(typescript@6.0.3) eslint-plugin-prettier: specifier: ^5.5.6 - version: 5.5.6(eslint-config-prettier@10.1.8(eslint@10.5.0(jiti@2.7.0)))(eslint@10.5.0(jiti@2.7.0))(prettier@3.8.4) + version: 5.5.6(eslint-config-prettier@10.1.8(eslint@10.6.0(jiti@2.7.0)))(eslint@10.6.0(jiti@2.7.0))(prettier@3.9.3) eslint-plugin-react: specifier: ^7.37.5 - version: 7.37.5(eslint@10.5.0(jiti@2.7.0)) + version: 7.37.5(eslint@10.6.0(jiti@2.7.0)) eslint-plugin-react-hooks: specifier: ^7.1.1 - version: 7.1.1(eslint@10.5.0(jiti@2.7.0)) + version: 7.1.1(eslint@10.6.0(jiti@2.7.0)) eslint-plugin-react-refresh: specifier: ^0.5.3 - version: 0.5.3(eslint@10.5.0(jiti@2.7.0)) + version: 0.5.3(eslint@10.6.0(jiti@2.7.0)) eslint-plugin-unicorn: specifier: ^67.0.0 - version: 67.0.0(eslint@10.5.0(jiti@2.7.0)) + version: 67.0.0(eslint@10.6.0(jiti@2.7.0)) happy-dom: specifier: ^20.10.6 version: 20.10.6 prettier: - specifier: ^3.8.4 - version: 3.8.4 + specifier: ^3.9.3 + version: 3.9.3 ts-morph: specifier: ^28.0.0 version: 28.0.0 @@ -146,17 +146,17 @@ importers: specifier: ~6.0.3 version: 6.0.3 typescript-eslint: - specifier: ^8.61.1 - version: 8.61.1(eslint@10.5.0(jiti@2.7.0))(typescript@6.0.3) + specifier: ^8.62.0 + version: 8.62.0(eslint@10.6.0(jiti@2.7.0))(typescript@6.0.3) vite: - specifier: ^8.0.16 - version: 8.0.16(@types/node@26.0.0)(jiti@2.7.0)(yaml@2.8.3) + specifier: ^8.1.0 + version: 8.1.0(@types/node@26.0.0)(jiti@2.7.0)(yaml@2.8.3) vite-plugin-css-injected-by-js: specifier: ^5.0.1 - version: 5.0.1(vite@8.0.16(@types/node@26.0.0)(jiti@2.7.0)(yaml@2.8.3)) + version: 5.0.1(vite@8.1.0(@types/node@26.0.0)(jiti@2.7.0)(yaml@2.8.3)) vitest: specifier: ^4.1.9 - version: 4.1.9(@types/node@26.0.0)(@vitest/coverage-v8@4.1.9)(happy-dom@20.10.6)(vite@8.0.16(@types/node@26.0.0)(jiti@2.7.0)(yaml@2.8.3)) + version: 4.1.9(@types/node@26.0.0)(@vitest/coverage-v8@4.1.9)(happy-dom@20.10.6)(vite@8.1.0(@types/node@26.0.0)(jiti@2.7.0)(yaml@2.8.3)) packages: @@ -322,14 +322,14 @@ packages: react: '>=18' react-dom: '>=18' - '@emnapi/core@1.10.0': - resolution: {integrity: sha512-yq6OkJ4p82CAfPl0u9mQebQHKPJkY7WrIuk205cTYnYe+k2Z8YBh11FrbRG/H6ihirqcacOgl2BIO8oyMQLeXw==} + '@emnapi/core@1.11.1': + resolution: {integrity: sha512-RSvbQmHzdKzNsLYa/wHrbc3KN4sYLKAdPZxqiM2HATqv/SBk2/ENSHpvXGaLOMcsAyz0poEGqkmmKYG3OWiJEQ==} - '@emnapi/runtime@1.10.0': - resolution: {integrity: sha512-ewvYlk86xUoGI0zQRNq/mC+16R1QeDlKQy21Ki3oSYXNgLb45GV1P6A0M+/s6nyCuNDqe5VpaY84BzXGwVbwFA==} + '@emnapi/runtime@1.11.1': + resolution: {integrity: sha512-vgj7R3y3Wgx24IQaGPA/R6YFXLHVMOZ0uVEyIQPaWs+rd1AzfEMXlAC22FYwO1XkKR6NPsq7mUandH8oIRdZFw==} - '@emnapi/wasi-threads@1.2.1': - resolution: {integrity: sha512-uTII7OYF+/Mes/MrcIOYp5yOtSMLBWSIoLPpcgwipoiKbli6k322tcoFsxoIIxPDqW01SQGAgko4EzZi2BNv2w==} + '@emnapi/wasi-threads@1.2.2': + resolution: {integrity: sha512-c95qOXkHdydNKhscBTebqEC1CVAZpyqOfVfBzQ1qgzyl3gfeldUjIggDbIZgDKsHLgnsM+igH7TJ/eAasaVuMA==} '@emotion/babel-plugin@11.13.5': resolution: {integrity: sha512-pxHCpT2ex+0q+HH91/zsdHkw/lXd468DIN2zvfvLtPKLLMo6gQj7oLObq8PhkrxOZb/gGCq03S3Z7PDhS8pduQ==} @@ -445,16 +445,16 @@ packages: peerDependencies: typescript: '>=5.5.3' - '@hey-api/codegen-core@0.9.0': - resolution: {integrity: sha512-OK9/R8WuujwgvnrDIPnEiIf6WnfUOi3GaEr6kIngqoI5FUQwYbeDKHE/frTVUl2A76ZQPCrMknHtPx6Gqtwf8Q==} + '@hey-api/codegen-core@0.9.1': + resolution: {integrity: sha512-s97jL1dgTMuiMHv2BZ1X4Tgd99Mf9GOvGdNqNcGwIMmnR+PgYNoraj4Zvp134MKsNCap/m7k0r0vKKnl56pj4w==} engines: {node: '>=22.18.0'} '@hey-api/json-schema-ref-parser@1.2.4': resolution: {integrity: sha512-uuOaZ6tStUgRJFUqnX3Xdbs792++ezxOLI5NMxuikVklpbFWk2wcvIZbeX+qTWDv6kiS1Ik2EVKQgeQFWHML4A==} engines: {node: '>= 16'} - '@hey-api/json-schema-ref-parser@1.4.3': - resolution: {integrity: sha512-UzGSDzh3QUhrnwl4atnHc2YqDO6KemYVEOwl1Ynowm/tcr0XlpdHOpyWr5UaWIJfiXTXdYRIC9k2Yxm19pcPzQ==} + '@hey-api/json-schema-ref-parser@1.4.4': + resolution: {integrity: sha512-otmd+zCxbYVBIp/mlMTnGkvlNYLkVKgs3VOIq0kSnenhB1+fRwLPQIeSwyWM6E51oXhUedkYjVsVpkVexeuJOA==} engines: {node: '>=22.18.0'} '@hey-api/openapi-ts@0.92.3': @@ -464,8 +464,8 @@ packages: peerDependencies: typescript: '>=5.5.3' - '@hey-api/openapi-ts@0.98.2': - resolution: {integrity: sha512-2nVJXH8tpFPGTBOhxyjEd1Jw0hsRqJqeTQW3kltAjVdSU4YWxeu97x5sgNOmsbsfeg6Dqz7Wfzs26walBOuswA==} + '@hey-api/openapi-ts@0.99.0': + resolution: {integrity: sha512-SePU/5oEWWkvUBYmvzdYRctseoLuskyhs4ET0RvLIcmzc8yLQoA2R+KtBIQ8bPsoSUB0m4E5SmBnl6aGSA0szQ==} engines: {node: '>=22.18.0'} hasBin: true peerDependencies: @@ -477,8 +477,8 @@ packages: peerDependencies: typescript: '>=5.5.3' - '@hey-api/shared@0.4.8': - resolution: {integrity: sha512-29Pg2FB0UW20pplYgcfiQn1hQYpbZ9D2gdDJc7nDK3xh3pvHOTGP0v3R2ueFpFnw9GN1SRhIdhiVuAYWMDimjA==} + '@hey-api/shared@0.5.0': + resolution: {integrity: sha512-JN/j4Ebh4cJGYIQ5cwWuqe7GeSUyQoz7oC51WqyhKOcrejK6DKZMDkshc5d1eKTRuRL+rjozuRcoUaZZn2DGPw==} engines: {node: '>=22.18.0'} '@hey-api/spec-types@0.2.0': @@ -541,14 +541,14 @@ packages: resolution: {integrity: sha512-9I2Zn6+NJLfaGoz9jN3lpwDgAYvfGeNYdbAIjJOqzs4Tpc+VU3Jqq4IofSUBKajiDS8k9fZIg18/z13mpk1bsA==} engines: {node: '>=8'} - '@napi-rs/wasm-runtime@1.1.4': - resolution: {integrity: sha512-3NQNNgA1YSlJb/kMH1ildASP9HW7/7kYnRI2szWJaofaS1hWmbGI4H+d3+22aGzXXN9IJ+n+GiFVcGipJP18ow==} + '@napi-rs/wasm-runtime@1.1.6': + resolution: {integrity: sha512-ZLv/JdUfkvOy9eCnnBaGfiO+XimbjebAeO+MRQqD/B+FR1tnRN0tpKSJHRbE8sFfS6aqsXZ67TQjfwfsxULVbg==} peerDependencies: '@emnapi/core': ^1.7.1 '@emnapi/runtime': ^1.7.1 - '@oxc-project/types@0.133.0': - resolution: {integrity: sha512-KzkdCd6Uxqnf6l3HOw1xfatAlUURA0g14cvBYFyJ5SaNOQbOUvBr9PKArcPcrNIeRsBdgcUzOGrhKveVpvOIGA==} + '@oxc-project/types@0.138.0': + resolution: {integrity: sha512-1a7ZKmrRTCoN1XMZ4L0PyyqrMnrNlLyPuOkdSX2MZg7IiIGRUyurNhAm73ptDOraoBcIordsIGKNPKUzy3ZmfA==} '@pandacss/is-valid-prop@1.11.3': resolution: {integrity: sha512-YaHK+p5DaN8AUpsRx5OqqGxaZzn8uNIdVhP+K1cjvjv3+Qa9D/75/A1dPyLmfKrSRJc8UoR9WN9fxQX0rVzhzQ==} @@ -558,91 +558,91 @@ packages: resolution: {integrity: sha512-SEeaJLb3qBNF/OaXnaR1NmmBbFYk1zC0ZH/52fATcRPLFg/p791YrcyFFy44Bo9sLaGuSuLp5Q6axbb/O+v/RA==} engines: {node: ^14.18.0 || >=16.0.0} - '@rolldown/binding-android-arm64@1.0.3': - resolution: {integrity: sha512-454rs7jHngixp/NMxd5srYD57OnzSlZ/eFTETjORQHLwJG1lRtmNOJcBerZlfu4GjKqeq8aCCIQrMdHyhI51Hw==} + '@rolldown/binding-android-arm64@1.1.4': + resolution: {integrity: sha512-EZLpf/8y7GXkkra90ML47kzik/GMP3EMcE9bPyHmRfxLC6z9+aW5A8poCsoxjrT5GfEcNAAvWwUHjvP1pUQkfw==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [android] - '@rolldown/binding-darwin-arm64@1.0.3': - resolution: {integrity: sha512-PcAhP+ynjURNyy8SKGl5DQP94aGuB/7JrXJb/t7P+hanXvQVMWzUvRRhBAcg/lNRadBhoUPqSoP4xw5tR/KBEA==} + '@rolldown/binding-darwin-arm64@1.1.4': + resolution: {integrity: sha512-aUi+HBvmYb7j8krl1+qJgkG8C17fO79gk3c+jPw4S8glRFc1DTija9S3EyaTSQUm5GJXYKDAsugBEhFHH2vYiQ==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [darwin] - '@rolldown/binding-darwin-x64@1.0.3': - resolution: {integrity: sha512-9YpfeUvSE2RS7wysJ81uOZkXJz7f7Q55H2Gvp3VEw/EsahqDtrphrZ0EwDLK5vvKOzaCrBsjF8JmnMLcUt78Gg==} + '@rolldown/binding-darwin-x64@1.1.4': + resolution: {integrity: sha512-F7hHC3gwY11+vByKPRWqwGbeXWVgKmL+pTGCinaEhdihzBV2aQ0fvZOch9cXYUOKuKKq429HeYXOqQLc7wFCEg==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [darwin] - '@rolldown/binding-freebsd-x64@1.0.3': - resolution: {integrity: sha512-yB1IlAsSNHncV6SCTL27/MVGR5htvQsoGxIv5KMGXALp+Ll1wYsn+x98M9MW7qa+NdSbvrrY7ANI4wLJ0n1e6g==} + '@rolldown/binding-freebsd-x64@1.1.4': + resolution: {integrity: sha512-sI5yw+7s92SK6odiEhD5lKCBlWcpjHS5qyqpVQbZAJ0fIzEUXrmbl3DH2ybR3PZogulNJF+COLtmA8hUfvkCCQ==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [freebsd] - '@rolldown/binding-linux-arm-gnueabihf@1.0.3': - resolution: {integrity: sha512-Yi30IVAAfLUCy2MseFjbB1jAMDl1VMCAas5StnYp8da9+CKvMd2H2cbEjWcw5NPaPqzvYkVIaF1nNUG+b7u/sw==} + '@rolldown/binding-linux-arm-gnueabihf@1.1.4': + resolution: {integrity: sha512-mCi0OKgEieFircrtVYmQAFGszRtMnZ6fpZAXrxanXAu7lqZcsK1E1RAaZNG0uKAnxox3B1f4EyQNnoyMfN1vAA==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm] os: [linux] - '@rolldown/binding-linux-arm64-gnu@1.0.3': - resolution: {integrity: sha512-jsO7R8To+AdlYgUmN5sHSCZbfhtMBkO0WUx8iORQnPcMMdgr7qM2DQmMwgabs3GhNztdmoKkMKQFHD6DTMCIQw==} + '@rolldown/binding-linux-arm64-gnu@1.1.4': + resolution: {integrity: sha512-B9Ial3Kv5sh0SHnB1g/QWcUQCEvCF6QKGAl4zXypYj65mVI+B4AhFBwPtSN7pDrJeIx8Z7zdy4ntx+wQABom7w==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [linux] - '@rolldown/binding-linux-arm64-musl@1.0.3': - resolution: {integrity: sha512-VWkUHwWriDciit80wleYwKILoR/KMvxh/IdwS/paX+ZgpuRpCrKLUdadJbc0NpBEiyhpYawsJ73j9aCvOH+f7Q==} + '@rolldown/binding-linux-arm64-musl@1.1.4': + resolution: {integrity: sha512-lZVym0PuHE1KZ22gmFTC15lAkrg9iTszR617oYRB/iPY1A56ywoJzVKOJBKaot5RiikCObmur6pogpse3gRcng==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [linux] - '@rolldown/binding-linux-ppc64-gnu@1.0.3': - resolution: {integrity: sha512-5f1laC0SlIR0yDbFCd8acUhvJIag6N3zC5P7oUPN6wX0aOma+uKJ0wBDH5aq7I1PVI2ttTlhJwzwRIBnLiSGEg==} + '@rolldown/binding-linux-ppc64-gnu@1.1.4': + resolution: {integrity: sha512-t2DNiLJWNTbnEHyUzTumldML6ET4/g16467LZoDDJ3tSxGvguL5/NyC2lCsNKuyRycg9XeDQF5SSv+TNOhQEXg==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [ppc64] os: [linux] - '@rolldown/binding-linux-s390x-gnu@1.0.3': - resolution: {integrity: sha512-Iq4ko0r4XsgbrF/LunNgHtAGLRRVE2kXonAXQ/MV0mC6jQpMOhW1SvtZja2EhC/kd05++bP78dsqBeIQyYJ6Yg==} + '@rolldown/binding-linux-s390x-gnu@1.1.4': + resolution: {integrity: sha512-0WIRnL1Uw4BvTZRLQt+PVgo6ZKTJadlC2btP+/EOXv2f/DWbY0rEgl+y834mIVwP1FkTlWVTrGGJXf12lru7EQ==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [s390x] os: [linux] - '@rolldown/binding-linux-x64-gnu@1.0.3': - resolution: {integrity: sha512-B8m6tD5+/N5FeNQFbKlLA/2yVq9ycQP1SeedyEYYKWBNR3ZQbkvIUcNnDNM03lO1l5F2roiiFJGgvoLLyZXtSg==} + '@rolldown/binding-linux-x64-gnu@1.1.4': + resolution: {integrity: sha512-JWtGshGfX+oENAKonoNkqEJX+7hC8yfhi9GUyPX1VX4mdh1y5r+ZiJLR5XzAB0aoP6s/PcILsGjKq8O0mm24bw==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [linux] - '@rolldown/binding-linux-x64-musl@1.0.3': - resolution: {integrity: sha512-pSdpdUJHkuCxun9LE7jvgUB9qsRgaiyNNCX7m/AvHTcq67AiT/Yhoxvw5zPfhrM8k/BfP8ce/hMOpthKDpEUow==} + '@rolldown/binding-linux-x64-musl@1.1.4': + resolution: {integrity: sha512-rT6yQcxUuXs4CnbofqwHRRV0iem349rLMYpTjkgQGLjrY4ado/eDzwPZPTCgTOlF6Nkp8NEv70yLMTn6qkWxsQ==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [linux] - '@rolldown/binding-openharmony-arm64@1.0.3': - resolution: {integrity: sha512-OXXS3RKJgX2uLwM+gYyuH5omcH8fL1LJs96pZGgtetVCahON57+d4SJHzTgZiOjxgGkSnpXpOsWuPDGAKAigEg==} + '@rolldown/binding-openharmony-arm64@1.1.4': + resolution: {integrity: sha512-KXMGoboq5cyaCQjDA4GLuRiOwBQ0EyFnJoVViLeZ45/3rFItRODEr+NdsBcVpll40hhNArlm/speWGRvj08LzA==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [openharmony] - '@rolldown/binding-wasm32-wasi@1.0.3': - resolution: {integrity: sha512-JTtb8BWFynicNSoPrehsCzBtOKjZ6jhMiPFEmOiuXg1Fl8dn2KHQob+GuPSGR0dryQa1PQJbzjF3dqO/whhjLg==} + '@rolldown/binding-wasm32-wasi@1.1.4': + resolution: {integrity: sha512-5K83rb36oJiY7BCyE9zLZtGcPV4g5wvq+xwdO0XPIwDVZI8cyB/AUjkNXGb92/rnmezEkjMOpgY61rtwjQtFwg==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [wasm32] - '@rolldown/binding-win32-arm64-msvc@1.0.3': - resolution: {integrity: sha512-gEdFFEN70A/jxb2svrWsN3aDL7OUtmvlOy+6fa2jxG8K0wQ1ZbdeLGnidov6Yu5/733dI5ySfzFlQ/cb0bSz1g==} + '@rolldown/binding-win32-arm64-msvc@1.1.4': + resolution: {integrity: sha512-PnWBtw3TV5KOg69HQQDR0mnQuyCmSGR2pAB4DC1rPF808fgKeTUMj2EOEyKATpgiuxuR5APQmiDO7PDgEjTFSA==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [win32] - '@rolldown/binding-win32-x64-msvc@1.0.3': - resolution: {integrity: sha512-eXB7CHuaQdqmJcc3koCNtNPmT/bj2gc999kUFgBxG8Ac0NdgXc4rkCHhqrgrhN3zddvvvrgzj1e90SuSfmyIXA==} + '@rolldown/binding-win32-x64-msvc@1.1.4': + resolution: {integrity: sha512-M1lpniBePobTfsa7Ks9a199e1akxsXn+GYBUKsEzv3YFzOm1HJAMNwKI3qr0Zq+mxwx9gOZoTdP1yXRYsZUocQ==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [win32] @@ -749,11 +749,11 @@ packages: '@swc/types@0.1.26': resolution: {integrity: sha512-lyMwd7WGgG79RS7EERZV3T8wMdmPq3xwyg+1nmAM64kIhx5yl+juO2PYIHb7vTiPgPCj8LYjsNV2T5wiQHUEaw==} - '@tanstack/query-core@5.101.0': - resolution: {integrity: sha512-cQetA74EB+seWySv1TTKr828TnP0u39m6LykwDXIo84SNortpDkp30TMEjkqtYCNP9c40uT/iwl6MLiufEt0Ow==} + '@tanstack/query-core@5.101.2': + resolution: {integrity: sha512-hH5MLoJhF7KaIGd7q3xTXGXvslI+GYlM1Z/35aSHHWaCJWB7XvTSHYuV3eM7tw+aE0mT/xMro4M4Q9rCGHT0lw==} - '@tanstack/react-query@5.101.0': - resolution: {integrity: sha512-rLlJXSpkqfizLWgkR5+eLeIk0MvTx/meEIR7LRjxic+qxiQP8zVjq7BqQkiCMNLQBlLfuOLqqr6KO5GtrDlmSg==} + '@tanstack/react-query@5.101.2': + resolution: {integrity: sha512-seDkr6kzGzX1okaaTtZPtgA688CDPlXUz1C6xSg0ESqn04Vuc8tlrYms1s3de+znBqhPVxFRfpAfUf+6XvfPWg==} peerDependencies: react: ^18 || ^19 @@ -802,8 +802,8 @@ packages: '@ts-morph/common@0.29.0': resolution: {integrity: sha512-35oUmphHbJvQ/+UTwFNme/t2p3FoKiGJ5auTjjpNTop2dyREspirjMy82PLSC1pnDJ8ah1GU98hwpVt64YXQsg==} - '@tybys/wasm-util@0.10.2': - resolution: {integrity: sha512-RoBvJ2X0wuKlWFIjrwffGw1IqZHKQqzIchKaadZZfnNpsAYp2mM0h36JtPCjNDAHGgYez/15uMBpfGwchhiMgg==} + '@tybys/wasm-util@0.10.3': + resolution: {integrity: sha512-F3fo1MYrRJYL3zER0OUOmkutjr1Vp23m7OsSgp7nq4SP6OqX6C/56XFIPAl5bt3zaBRjmW7SGz3u/6LwFpYcOg==} '@types/aria-query@5.0.4': resolution: {integrity: sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw==} @@ -848,39 +848,39 @@ packages: '@types/ws@8.18.1': resolution: {integrity: sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg==} - '@typescript-eslint/eslint-plugin@8.61.1': - resolution: {integrity: sha512-ZPlVl3PB3et/59Ne0fv/sci6ZXz4T4Hp4nTJ56i/Y0gR89ARb+KphojTq6j+56E5PIezmOIOOWyY+aWQFd+IkQ==} + '@typescript-eslint/eslint-plugin@8.62.0': + resolution: {integrity: sha512-o+mpz7EYiMzXoySXiKmzlabIvTVqUuK5yLrAedRPRDA0IpPFMUV1IXt6OqljIxX/kumN6EjUYp41Hqelh6p/Dw==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: - '@typescript-eslint/parser': ^8.61.1 + '@typescript-eslint/parser': ^8.62.0 eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 typescript: '>=4.8.4 <6.1.0' - '@typescript-eslint/parser@8.61.1': - resolution: {integrity: sha512-PJ5vePq5/ognBbrIcoC5+SHO5dfpeLPzP9FpLkzWrguoYQEeeSjlJpVwOpo1JRSTEi7dRcwNy4h4dzV70PqHcg==} + '@typescript-eslint/parser@8.62.0': + resolution: {integrity: sha512-dzHeT2gySzZtLDsuqxU9AkYgIsQoHAHtRBpOqM+Ofzx1Bwrd2RcCjQJ+6iQbsHOIR6NS33bF2W1k3blN1zLDrA==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 typescript: '>=4.8.4 <6.1.0' - '@typescript-eslint/project-service@8.61.1': - resolution: {integrity: sha512-PrC4JYGmR241lYnfhmKGTXkFqv8+ymbTFgSAY0fVXpY82/QkMw5TZPl+vGzuDDU2QYJk9fIDOBTntF+yDv9LEA==} + '@typescript-eslint/project-service@8.62.0': + resolution: {integrity: sha512-wexnCqiTg7BOGtbLDftYpRWlmLq4xfoMd7BKFR6Y75sZS3QmRKLdN3yWLhmIYgqMmP/OXWpj3H8odkb5nGURCQ==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: typescript: '>=4.8.4 <6.1.0' - '@typescript-eslint/scope-manager@8.61.1': - resolution: {integrity: sha512-L2bdIeoQS8FlKAvONAr20w6OcLXeB+qiDKbAooS9A0Ben+iSIkBef0FxqwKWYqt5sa0i4KJtxVyVmhMylKzF5w==} + '@typescript-eslint/scope-manager@8.62.0': + resolution: {integrity: sha512-1lX38kNxXIRb8mEc3lbq5mdHq1Pf2+U0nFU65KfT18mtPxxl0fvjuEE92mHuXPuCtElJhOrddOpyMlM3Z0umEA==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - '@typescript-eslint/tsconfig-utils@8.61.1': - resolution: {integrity: sha512-UN/H4di+OO7EWx2ovME+8t31YO+KVnK0RRKEHR3kOt21/Ay8BOq3M1OMvWs5vNiqcFCYGYoxK3MXPZzmMUE+yg==} + '@typescript-eslint/tsconfig-utils@8.62.0': + resolution: {integrity: sha512-y2GAdB6ykaXUvuspbYnizQc4oDDz0Tz/Yc7iWrXf9mx8vm/L/0vLHCe0tS2boG96Zy+DivnVDQ9ZUEWoHqqx1g==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: typescript: '>=4.8.4 <6.1.0' - '@typescript-eslint/type-utils@8.61.1': - resolution: {integrity: sha512-GYRicKmVK0C4fsKgaACaknOUAq9Oa2kwsjnpFhFcS/5p4Ht5IP9OVLbgIgcK4SRk92nVHFluurg1lumD9dBcLw==} + '@typescript-eslint/type-utils@8.62.0': + resolution: {integrity: sha512-+g5O3j0w2ldzC86Pv6fvbO/xhAonbJFIdf/MKQ1d30gndlsVzUOE83ldfSE15Qrl9fhFjK6AovHs5Wpp6vx86w==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 @@ -890,25 +890,25 @@ packages: resolution: {integrity: sha512-dTLI8PEXhjUC7B9Kre+u0XznO696BhXcTlOn0/6kf1fHaQW8+VjJAVHJ3eTI14ZapTxdkOmc80HblPQLaEeJdg==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - '@typescript-eslint/types@8.61.1': - resolution: {integrity: sha512-G+CRlPqLv7Bz1IZVs03x5K59F1veqL0EJUROAdGhKsEq8qOiRiZbI+HUojPq5l0fEGOKModD9br6lObhB8zkoA==} + '@typescript-eslint/types@8.62.0': + resolution: {integrity: sha512-KvAclkktORPvM54TgLgA4z9HIV1M8zOgw9ZVNXl9f/8dLYfXYX1wkMXP7qmabpijQRV5bHJLOmoyGQbLMaUYeg==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - '@typescript-eslint/typescript-estree@8.61.1': - resolution: {integrity: sha512-u+oQD3BqYWPc8YV9Zab4vaJElJuwOLPRc10Jm1o/qS+6Qwen14HCWwx0Seo4LnSn2wxea2Ik8DxPt2/FHmuhrg==} + '@typescript-eslint/typescript-estree@8.62.0': + resolution: {integrity: sha512-+hVbNxtW64pIcZWDPGbyaKF7vp2IBTVY5ma1blwwksrjdsbdqqEKvJWMGbBofei4F6Dovx1M0RJgoFeNu2279A==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: typescript: '>=4.8.4 <6.1.0' - '@typescript-eslint/utils@8.61.1': - resolution: {integrity: sha512-1+P/3Dj6jvtybE1q0HQ6yBt/gq+oKJyLdEv4HdnqasaEXRSYCAsD59mXEVQnM/ULNdQxbX77tdG4jPRjIS6knA==} + '@typescript-eslint/utils@8.62.0': + resolution: {integrity: sha512-82r66fi9zYwZ+mTq3vKgwjbZ1PVk/DJzrXFLpG6RnBbdvH8TEGVHIs9H4d2drhkOzf0syZuD/OZvvlu6GDbP4g==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 typescript: '>=4.8.4 <6.1.0' - '@typescript-eslint/visitor-keys@8.61.1': - resolution: {integrity: sha512-6fJ9MHWtK14C1DSkiMlHUSOmrVebL7150xZJBlJiL62jjhIA4JmOq6flwBgDxIdBKKdoiZRel+dfPD5MLfny3w==} + '@typescript-eslint/visitor-keys@8.62.0': + resolution: {integrity: sha512-CY3uyFSRbcQv3nnSv8S0+lDftMVz6P963PoRlxrV7ew/Md564g9ut60PYzdLM5qW4jFn93GBF+Soi90ISAN+GQ==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} '@vitejs/plugin-react-swc@4.3.1': @@ -1290,8 +1290,8 @@ packages: resolution: {integrity: sha512-Xm7bpRXnDSX2YE2YFfBk2FnF0ep6tmG7xPh8iHee8MIcrgq762Nkce856dYtJYLkuIoYZvGfTs/PbZhideTcEg==} engines: {node: '>=4'} - axios@1.18.0: - resolution: {integrity: sha512-E32NzpYKp++W7XRe52rHiXV2ehxmh3wbdgO7MHeFM+vqxLBYHzt0ElkiImtOBxtOmyp0yoC8C6uESVV84Y2/hw==} + axios@1.18.1: + resolution: {integrity: sha512-3nTvFlvpn9Zu/RkHUqtc7/+al4UpRW5az71ap5zccp6e8RAYEzhMTecX8Dz1wWDYrPpUoB1HAQEGEAEvUr7S9g==} axobject-query@4.1.0: resolution: {integrity: sha512-qIj0G9wZbMGNLjLmg1PT6v2mE9AH2zlnADJD/2tC6E00hgmhUOfEB6greHPAfLRSufHqROIUTkw6E+M3lH0PTQ==} @@ -1319,8 +1319,8 @@ packages: brace-expansion@2.0.3: resolution: {integrity: sha512-MCV/fYJEbqx68aE58kv2cA/kiky1G8vux3OR6/jbS+jIMe/6fJWa0DTzJU7dqijOWYwHi1t29FlfYI9uytqlpA==} - brace-expansion@5.0.6: - resolution: {integrity: sha512-kLpxurY4Z4r9sgMsyG0Z9uzsBlgiU/EFKhj/h91/8yHu0edo7XuixOIH3VcJ8kkxs6/jPzoI6U9Vj3WqbMQ94g==} + brace-expansion@5.0.7: + resolution: {integrity: sha512-7oFy703dxfY3/NLxC1fh2SUCQ0H9rmAY+5EpDVfXjUTTs+HEwR2nYaqLv+GWcTsumwxPfiz6CzCNkwXwBUwqCA==} engines: {node: 18 || 20 || >=22} browserslist@4.28.2: @@ -1677,8 +1677,8 @@ packages: resolution: {integrity: sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==} engines: {node: ^20.19.0 || ^22.13.0 || >=24} - eslint@10.5.0: - resolution: {integrity: sha512-1y+7C+vi12bUK1IpZeaV3gsH9fHLBmPvYmPx42pvT/E9yG0IC8g3PUZZgp0+JLJl7ZDK0flc2gc+Aw9dpCvIsQ==} + eslint@10.6.0: + resolution: {integrity: sha512-6lVbcqSodALYo+4ELD0heG6lFiFxnLMuLkiMi2qV8LMp54N8tE8FT1GMH+ev4Ti00nFjNze2+Su6DsV5OQW3Dg==} engines: {node: ^20.19.0 || ^22.13.0 || >=24} hasBin: true peerDependencies: @@ -1721,6 +1721,9 @@ packages: exsolve@1.0.8: resolution: {integrity: sha512-LmDxfWXwcTArk8fUEnOfSZpHOJ6zOMUJKOtFLFqJLoKJetuQG874Uc7/Kki7zFLzYybmZhp1M7+98pfMqeX8yA==} + exsolve@1.1.0: + resolution: {integrity: sha512-D+42+T12DdIlJM3uepa55qGiL3sYdLBOxIl2ifQCzCHz4c7eiolaHsi3BIqEr7JxBzxv2pYZQX9kw16ziMcEmw==} + fast-deep-equal@3.1.3: resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==} @@ -2082,6 +2085,10 @@ packages: resolution: {integrity: sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==} hasBin: true + js-yaml@4.2.0: + resolution: {integrity: sha512-ePWsvanv0DWuDRsW8dnt+R4jQ31SCRCQ7hhNcPXZPsoBZiemuZNYGf7adZdqX2D86j6rvKp3RpCxVTSb8WQlOw==} + hasBin: true + jsesc@3.1.0: resolution: {integrity: sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==} engines: {node: '>=6'} @@ -2256,8 +2263,8 @@ packages: ms@2.1.3: resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} - nanoid@3.3.12: - resolution: {integrity: sha512-ZB9RH/39qpq5Vu6Y+NmUaFhQR6pp+M2Xt76XBnEwDaGcVAqhlvxrl3B2bKS5D3NH3QR76v3aSrKaF/Kiy7lEtQ==} + nanoid@3.3.15: + resolution: {integrity: sha512-y7Wygv/7mEOvxTuEQDB8StXdMRBWf1kR/tlhAzBRUFkB2jfcLOAxO/SHmOO2zgz1pVgK29/kyupn059/bCHdjA==} engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} hasBin: true @@ -2389,6 +2396,10 @@ packages: resolution: {integrity: sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==} engines: {node: '>=12'} + picomatch@4.0.5: + resolution: {integrity: sha512-RvwwcruNjI1ncT5xRakeyS9Lf8lcItv34KD+aif+VH9kduAyfYBipGh12274xtenIPZ119/R9BdTBa8gAwSh0A==} + engines: {node: '>=12'} + pkg-types@2.3.0: resolution: {integrity: sha512-SIqCzDRg0s9npO5XQ3tNZioRY1uK06lA41ynBC1YmFTmnY6FjUjVt6s4LoADmwoig1qqD0oK8h1p/8mlMx8Oig==} @@ -2403,8 +2414,8 @@ packages: resolution: {integrity: sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg==} engines: {node: '>= 0.4'} - postcss@8.5.15: - resolution: {integrity: sha512-FfR8sjd4em2T6fb3I2MwAJU7HWVMr9zba+enmQeeWFfCbm+UOC/0X4DS8XtpUTMwWMGbjKYP7xjfNekzyGmB3A==} + postcss@8.5.16: + resolution: {integrity: sha512-vuwillviilfKZsg0VGj5R/YwwcHx4SLsIOI/7K6mQkWx+l5cUHTjj5g0AasTBcyXsbfTgrwsUNmVUb5xVwyPwg==} engines: {node: ^10 || ^12 || >=14} powershell-utils@0.1.0: @@ -2419,8 +2430,8 @@ packages: resolution: {integrity: sha512-SxToR7P8Y2lWmv/kTzVLC1t/GDI2WGjMwNhLLE9qtH8Q13C+aEmuRlzDst4Up4s0Wc8sF2M+J57iB3cMLqftfg==} engines: {node: '>=6.0.0'} - prettier@3.8.4: - resolution: {integrity: sha512-N2MylSdi48+5N/6S5j+maeHbUSIzzZ5uOcX5Hm4QpV8Dkb1HFjfAKTKX6yNPJQD9AhcT3ifHNB66tWTTJDi11Q==} + prettier@3.9.3: + resolution: {integrity: sha512-HWmu+K+zvHNpaMfSnYeqdqrDbR16cuIXaPx8WoHaviQkDJh1/0BNtOZmHVQI5jc3wXv0H1yXc9wjvFdXh+n3hQ==} engines: {node: '>=14'} hasBin: true @@ -2461,8 +2472,8 @@ packages: peerDependencies: react: ^19.2.7 - react-hook-form@7.79.0: - resolution: {integrity: sha512-mhYp/MTmXvzYX6AJcJVko0rktoIhhmRnEouObj4wF5i/tCttgJvnp1+9wRkpITZjDTqpo4IOSJqu0dBlPlV/Lw==} + react-hook-form@7.80.0: + resolution: {integrity: sha512-4P+fk6oXsxY+6xSj7Euhc2sumQD8zQqCuVHoJwoyp9EchP+IUW9OESB7uHFJOKsIBQ4MQqYE84INJFqUCYNoOg==} engines: {node: '>=18.0.0'} peerDependencies: react: ^16.8.0 || ^17 || ^18 || ^19 @@ -2530,8 +2541,8 @@ packages: resolution: {integrity: sha512-U7WjGVG9sH8tvjW5SmGbQuui75FiyjAX72HX15DwBBwF9dNiQZRQAg9nnPhYy+TUnE0+VcrttuvNI8oSxZcocA==} hasBin: true - rolldown@1.0.3: - resolution: {integrity: sha512-i00lAJ2ks1BYr7rjNjKC7BcqAS7nVfiT3QX1SI5aY+AFHblCmaUf9OE9dbdzDvW6dJxbi2ZCZiy9v3CcwOiX3g==} + rolldown@1.1.4: + resolution: {integrity: sha512-IjZYiLxZwpnhwhdBH2ugdTGVSdhCQUmLxLoqyjiL0JxYjyRst+5a0P3xfrTxJ5F638j4Mvvw5FAX5XE6eHpXbA==} engines: {node: ^20.19.0 || >=22.12.0} hasBin: true @@ -2563,8 +2574,8 @@ packages: engines: {node: '>=10'} hasBin: true - semver@7.8.2: - resolution: {integrity: sha512-c8jsqUZm3omBOI66G90z1Dyw5z622G8oLG+omfsHBJf3CWQTlOcwOjvOG6wtiNfW6anKm/eA39LMwMtMez2TiQ==} + semver@7.8.4: + resolution: {integrity: sha512-rUCObTnP32Q08R2uuIrt7r9PlEonuTmtuXYcW6s5kjdlj3xbnwe+21yXptAUYcMAABLkYYTtnmzb3w3EDZfueA==} engines: {node: '>=10'} hasBin: true @@ -2722,8 +2733,8 @@ packages: resolution: {integrity: sha512-3KS2b+kL7fsuk/eJZ7EQdnEmQoaho/r6KUef7hxvltNA5DR8NAUM+8wJMbJyZ4G9/7i3v5zPBIMN5aybAh2/Jg==} engines: {node: '>= 0.4'} - typescript-eslint@8.61.1: - resolution: {integrity: sha512-V7PayAfJokV3pEHgN7/v03D1SpujhRfQtYLbLIiBfDDncdg4PAiRBfoS4cnCANK4jmAPncczi59QO3afiXUlNw==} + typescript-eslint@8.62.0: + resolution: {integrity: sha512-8QxXi+ZACKX0kaqO4gY8kn0RSD9gFfaHDWwjqtEN48aWCBkX4MJaufWN+c3BzlrXLOxfywDL8CaoqUwcRq4j4Q==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 @@ -2761,13 +2772,13 @@ packages: peerDependencies: vite: '>8.0.0-0' - vite@8.0.16: - resolution: {integrity: sha512-h9bXPmJichP5fLmVQo3PyaGSDE2n3aPuomeAlVRm0JLmt4rY6zmPKd59HYI4LNW8oTK7tlTsuC7l/m7awx9Jcw==} + vite@8.1.0: + resolution: {integrity: sha512-BuJcQK/56NQTWDGn4ABea3q4SSBdNPWwNZKTkkUpcMPnLoquSYH8llRtSUIgoL1KSCpHt5eghLShn50mH36y7Q==} engines: {node: ^20.19.0 || >=22.12.0} hasBin: true peerDependencies: '@types/node': ^20.19.0 || >=22.12.0 - '@vitejs/devtools': ^0.1.18 + '@vitejs/devtools': ^0.3.0 esbuild: '>=0.28.1' jiti: '>=1.21.0' less: ^4.0.0 @@ -3212,18 +3223,18 @@ snapshots: react: 19.2.7 react-dom: 19.2.7(react@19.2.7) - '@emnapi/core@1.10.0': + '@emnapi/core@1.11.1': dependencies: - '@emnapi/wasi-threads': 1.2.1 + '@emnapi/wasi-threads': 1.2.2 tslib: 2.8.1 optional: true - '@emnapi/runtime@1.10.0': + '@emnapi/runtime@1.11.1': dependencies: tslib: 2.8.1 optional: true - '@emnapi/wasi-threads@1.2.1': + '@emnapi/wasi-threads@1.2.2': dependencies: tslib: 2.8.1 optional: true @@ -3296,18 +3307,18 @@ snapshots: '@emotion/weak-memoize@0.4.0': {} - '@eslint-community/eslint-utils@4.9.1(eslint@10.5.0(jiti@2.7.0))': + '@eslint-community/eslint-utils@4.9.1(eslint@10.6.0(jiti@2.7.0))': dependencies: - eslint: 10.5.0(jiti@2.7.0) + eslint: 10.6.0(jiti@2.7.0) eslint-visitor-keys: 3.4.3 '@eslint-community/regexpp@4.12.2': {} - '@eslint/compat@2.1.0(eslint@10.5.0(jiti@2.7.0))': + '@eslint/compat@2.1.0(eslint@10.6.0(jiti@2.7.0))': dependencies: '@eslint/core': 1.2.1 optionalDependencies: - eslint: 10.5.0(jiti@2.7.0) + eslint: 10.6.0(jiti@2.7.0) '@eslint/config-array@0.23.5': dependencies: @@ -3325,9 +3336,9 @@ snapshots: dependencies: '@types/json-schema': 7.0.15 - '@eslint/js@10.0.1(eslint@10.5.0(jiti@2.7.0))': + '@eslint/js@10.0.1(eslint@10.6.0(jiti@2.7.0))': optionalDependencies: - eslint: 10.5.0(jiti@2.7.0) + eslint: 10.6.0(jiti@2.7.0) '@eslint/object-schema@3.0.5': {} @@ -3347,10 +3358,10 @@ snapshots: '@floating-ui/utils@0.2.11': {} - '@hey-api/client-axios@0.9.1(@hey-api/openapi-ts@0.98.2(magicast@0.5.3)(typescript@6.0.3))(axios@1.18.0)': + '@hey-api/client-axios@0.9.1(@hey-api/openapi-ts@0.99.0(magicast@0.5.3)(typescript@6.0.3))(axios@1.18.1)': dependencies: - '@hey-api/openapi-ts': 0.98.2(magicast@0.5.3)(typescript@6.0.3) - axios: 1.18.0 + '@hey-api/openapi-ts': 0.99.0(magicast@0.5.3)(typescript@6.0.3) + axios: 1.18.1 '@hey-api/codegen-core@0.7.0(magicast@0.5.3)(typescript@6.0.3)': dependencies: @@ -3362,7 +3373,7 @@ snapshots: transitivePeerDependencies: - magicast - '@hey-api/codegen-core@0.9.0(magicast@0.5.3)': + '@hey-api/codegen-core@0.9.1(magicast@0.5.3)': dependencies: '@hey-api/types': 0.1.4 ansi-colors: 4.1.3 @@ -3378,11 +3389,11 @@ snapshots: js-yaml: 4.1.1 lodash: 4.18.1 - '@hey-api/json-schema-ref-parser@1.4.3': + '@hey-api/json-schema-ref-parser@1.4.4': dependencies: '@jsdevtools/ono': 7.1.3 '@types/json-schema': 7.0.15 - js-yaml: 4.1.1 + js-yaml: 4.2.0 '@hey-api/openapi-ts@0.92.3(magicast@0.5.3)(typescript@6.0.3)': dependencies: @@ -3397,11 +3408,11 @@ snapshots: transitivePeerDependencies: - magicast - '@hey-api/openapi-ts@0.98.2(magicast@0.5.3)(typescript@6.0.3)': + '@hey-api/openapi-ts@0.99.0(magicast@0.5.3)(typescript@6.0.3)': dependencies: - '@hey-api/codegen-core': 0.9.0(magicast@0.5.3) - '@hey-api/json-schema-ref-parser': 1.4.3 - '@hey-api/shared': 0.4.8(magicast@0.5.3) + '@hey-api/codegen-core': 0.9.1(magicast@0.5.3) + '@hey-api/json-schema-ref-parser': 1.4.4 + '@hey-api/shared': 0.5.0(magicast@0.5.3) '@hey-api/spec-types': 0.2.0 '@hey-api/types': 0.1.4 '@lukeed/ms': 2.0.2 @@ -3426,16 +3437,16 @@ snapshots: transitivePeerDependencies: - magicast - '@hey-api/shared@0.4.8(magicast@0.5.3)': + '@hey-api/shared@0.5.0(magicast@0.5.3)': dependencies: - '@hey-api/codegen-core': 0.9.0(magicast@0.5.3) - '@hey-api/json-schema-ref-parser': 1.4.3 + '@hey-api/codegen-core': 0.9.1(magicast@0.5.3) + '@hey-api/json-schema-ref-parser': 1.4.4 '@hey-api/spec-types': 0.2.0 '@hey-api/types': 0.1.4 ansi-colors: 4.1.3 cross-spawn: 7.0.6 open: 11.0.0 - semver: 7.8.2 + semver: 7.8.4 transitivePeerDependencies: - magicast @@ -3496,77 +3507,77 @@ snapshots: '@lukeed/ms@2.0.2': {} - '@napi-rs/wasm-runtime@1.1.4(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)': + '@napi-rs/wasm-runtime@1.1.6(@emnapi/core@1.11.1)(@emnapi/runtime@1.11.1)': dependencies: - '@emnapi/core': 1.10.0 - '@emnapi/runtime': 1.10.0 - '@tybys/wasm-util': 0.10.2 + '@emnapi/core': 1.11.1 + '@emnapi/runtime': 1.11.1 + '@tybys/wasm-util': 0.10.3 optional: true - '@oxc-project/types@0.133.0': {} + '@oxc-project/types@0.138.0': {} '@pandacss/is-valid-prop@1.11.3': {} '@pkgr/core@0.3.6': {} - '@rolldown/binding-android-arm64@1.0.3': + '@rolldown/binding-android-arm64@1.1.4': optional: true - '@rolldown/binding-darwin-arm64@1.0.3': + '@rolldown/binding-darwin-arm64@1.1.4': optional: true - '@rolldown/binding-darwin-x64@1.0.3': + '@rolldown/binding-darwin-x64@1.1.4': optional: true - '@rolldown/binding-freebsd-x64@1.0.3': + '@rolldown/binding-freebsd-x64@1.1.4': optional: true - '@rolldown/binding-linux-arm-gnueabihf@1.0.3': + '@rolldown/binding-linux-arm-gnueabihf@1.1.4': optional: true - '@rolldown/binding-linux-arm64-gnu@1.0.3': + '@rolldown/binding-linux-arm64-gnu@1.1.4': optional: true - '@rolldown/binding-linux-arm64-musl@1.0.3': + '@rolldown/binding-linux-arm64-musl@1.1.4': optional: true - '@rolldown/binding-linux-ppc64-gnu@1.0.3': + '@rolldown/binding-linux-ppc64-gnu@1.1.4': optional: true - '@rolldown/binding-linux-s390x-gnu@1.0.3': + '@rolldown/binding-linux-s390x-gnu@1.1.4': optional: true - '@rolldown/binding-linux-x64-gnu@1.0.3': + '@rolldown/binding-linux-x64-gnu@1.1.4': optional: true - '@rolldown/binding-linux-x64-musl@1.0.3': + '@rolldown/binding-linux-x64-musl@1.1.4': optional: true - '@rolldown/binding-openharmony-arm64@1.0.3': + '@rolldown/binding-openharmony-arm64@1.1.4': optional: true - '@rolldown/binding-wasm32-wasi@1.0.3': + '@rolldown/binding-wasm32-wasi@1.1.4': dependencies: - '@emnapi/core': 1.10.0 - '@emnapi/runtime': 1.10.0 - '@napi-rs/wasm-runtime': 1.1.4(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0) + '@emnapi/core': 1.11.1 + '@emnapi/runtime': 1.11.1 + '@napi-rs/wasm-runtime': 1.1.6(@emnapi/core@1.11.1)(@emnapi/runtime@1.11.1) optional: true - '@rolldown/binding-win32-arm64-msvc@1.0.3': + '@rolldown/binding-win32-arm64-msvc@1.1.4': optional: true - '@rolldown/binding-win32-x64-msvc@1.0.3': + '@rolldown/binding-win32-x64-msvc@1.1.4': optional: true '@rolldown/pluginutils@1.0.1': {} '@standard-schema/spec@1.1.0': {} - '@stylistic/eslint-plugin@5.10.0(eslint@10.5.0(jiti@2.7.0))': + '@stylistic/eslint-plugin@5.10.0(eslint@10.6.0(jiti@2.7.0))': dependencies: - '@eslint-community/eslint-utils': 4.9.1(eslint@10.5.0(jiti@2.7.0)) + '@eslint-community/eslint-utils': 4.9.1(eslint@10.6.0(jiti@2.7.0)) '@typescript-eslint/types': 8.57.0 - eslint: 10.5.0(jiti@2.7.0) + eslint: 10.6.0(jiti@2.7.0) eslint-visitor-keys: 4.2.1 espree: 10.4.0 estraverse: 5.3.0 @@ -3637,11 +3648,11 @@ snapshots: dependencies: '@swc/counter': 0.1.3 - '@tanstack/query-core@5.101.0': {} + '@tanstack/query-core@5.101.2': {} - '@tanstack/react-query@5.101.0(react@19.2.7)': + '@tanstack/react-query@5.101.2(react@19.2.7)': dependencies: - '@tanstack/query-core': 5.101.0 + '@tanstack/query-core': 5.101.2 react: 19.2.7 '@testing-library/dom@10.4.0': @@ -3674,7 +3685,7 @@ snapshots: '@types/react': 19.2.17 '@types/react-dom': 19.2.3(@types/react@19.2.17) - '@trivago/prettier-plugin-sort-imports@6.0.2(prettier@3.8.4)': + '@trivago/prettier-plugin-sort-imports@6.0.2(prettier@3.9.3)': dependencies: '@babel/generator': 7.28.6 '@babel/parser': 7.28.6 @@ -3684,7 +3695,7 @@ snapshots: lodash-es: 4.18.1 minimatch: 9.0.9 parse-imports-exports: 0.2.4 - prettier: 3.8.4 + prettier: 3.9.3 transitivePeerDependencies: - supports-color @@ -3694,7 +3705,7 @@ snapshots: path-browserify: 1.0.1 tinyglobby: 0.2.17 - '@tybys/wasm-util@0.10.2': + '@tybys/wasm-util@0.10.3': dependencies: tslib: 2.8.1 optional: true @@ -3739,15 +3750,15 @@ snapshots: dependencies: '@types/node': 26.0.0 - '@typescript-eslint/eslint-plugin@8.61.1(@typescript-eslint/parser@8.61.1(eslint@10.5.0(jiti@2.7.0))(typescript@6.0.3))(eslint@10.5.0(jiti@2.7.0))(typescript@6.0.3)': + '@typescript-eslint/eslint-plugin@8.62.0(@typescript-eslint/parser@8.62.0(eslint@10.6.0(jiti@2.7.0))(typescript@6.0.3))(eslint@10.6.0(jiti@2.7.0))(typescript@6.0.3)': dependencies: '@eslint-community/regexpp': 4.12.2 - '@typescript-eslint/parser': 8.61.1(eslint@10.5.0(jiti@2.7.0))(typescript@6.0.3) - '@typescript-eslint/scope-manager': 8.61.1 - '@typescript-eslint/type-utils': 8.61.1(eslint@10.5.0(jiti@2.7.0))(typescript@6.0.3) - '@typescript-eslint/utils': 8.61.1(eslint@10.5.0(jiti@2.7.0))(typescript@6.0.3) - '@typescript-eslint/visitor-keys': 8.61.1 - eslint: 10.5.0(jiti@2.7.0) + '@typescript-eslint/parser': 8.62.0(eslint@10.6.0(jiti@2.7.0))(typescript@6.0.3) + '@typescript-eslint/scope-manager': 8.62.0 + '@typescript-eslint/type-utils': 8.62.0(eslint@10.6.0(jiti@2.7.0))(typescript@6.0.3) + '@typescript-eslint/utils': 8.62.0(eslint@10.6.0(jiti@2.7.0))(typescript@6.0.3) + '@typescript-eslint/visitor-keys': 8.62.0 + eslint: 10.6.0(jiti@2.7.0) ignore: 7.0.5 natural-compare: 1.4.0 ts-api-utils: 2.5.0(typescript@6.0.3) @@ -3755,43 +3766,43 @@ snapshots: transitivePeerDependencies: - supports-color - '@typescript-eslint/parser@8.61.1(eslint@10.5.0(jiti@2.7.0))(typescript@6.0.3)': + '@typescript-eslint/parser@8.62.0(eslint@10.6.0(jiti@2.7.0))(typescript@6.0.3)': dependencies: - '@typescript-eslint/scope-manager': 8.61.1 - '@typescript-eslint/types': 8.61.1 - '@typescript-eslint/typescript-estree': 8.61.1(typescript@6.0.3) - '@typescript-eslint/visitor-keys': 8.61.1 + '@typescript-eslint/scope-manager': 8.62.0 + '@typescript-eslint/types': 8.62.0 + '@typescript-eslint/typescript-estree': 8.62.0(typescript@6.0.3) + '@typescript-eslint/visitor-keys': 8.62.0 debug: 4.4.3 - eslint: 10.5.0(jiti@2.7.0) + eslint: 10.6.0(jiti@2.7.0) typescript: 6.0.3 transitivePeerDependencies: - supports-color - '@typescript-eslint/project-service@8.61.1(typescript@6.0.3)': + '@typescript-eslint/project-service@8.62.0(typescript@6.0.3)': dependencies: - '@typescript-eslint/tsconfig-utils': 8.61.1(typescript@6.0.3) - '@typescript-eslint/types': 8.61.1 + '@typescript-eslint/tsconfig-utils': 8.62.0(typescript@6.0.3) + '@typescript-eslint/types': 8.62.0 debug: 4.4.3 typescript: 6.0.3 transitivePeerDependencies: - supports-color - '@typescript-eslint/scope-manager@8.61.1': + '@typescript-eslint/scope-manager@8.62.0': dependencies: - '@typescript-eslint/types': 8.61.1 - '@typescript-eslint/visitor-keys': 8.61.1 + '@typescript-eslint/types': 8.62.0 + '@typescript-eslint/visitor-keys': 8.62.0 - '@typescript-eslint/tsconfig-utils@8.61.1(typescript@6.0.3)': + '@typescript-eslint/tsconfig-utils@8.62.0(typescript@6.0.3)': dependencies: typescript: 6.0.3 - '@typescript-eslint/type-utils@8.61.1(eslint@10.5.0(jiti@2.7.0))(typescript@6.0.3)': + '@typescript-eslint/type-utils@8.62.0(eslint@10.6.0(jiti@2.7.0))(typescript@6.0.3)': dependencies: - '@typescript-eslint/types': 8.61.1 - '@typescript-eslint/typescript-estree': 8.61.1(typescript@6.0.3) - '@typescript-eslint/utils': 8.61.1(eslint@10.5.0(jiti@2.7.0))(typescript@6.0.3) + '@typescript-eslint/types': 8.62.0 + '@typescript-eslint/typescript-estree': 8.62.0(typescript@6.0.3) + '@typescript-eslint/utils': 8.62.0(eslint@10.6.0(jiti@2.7.0))(typescript@6.0.3) debug: 4.4.3 - eslint: 10.5.0(jiti@2.7.0) + eslint: 10.6.0(jiti@2.7.0) ts-api-utils: 2.5.0(typescript@6.0.3) typescript: 6.0.3 transitivePeerDependencies: @@ -3799,14 +3810,14 @@ snapshots: '@typescript-eslint/types@8.57.0': {} - '@typescript-eslint/types@8.61.1': {} + '@typescript-eslint/types@8.62.0': {} - '@typescript-eslint/typescript-estree@8.61.1(typescript@6.0.3)': + '@typescript-eslint/typescript-estree@8.62.0(typescript@6.0.3)': dependencies: - '@typescript-eslint/project-service': 8.61.1(typescript@6.0.3) - '@typescript-eslint/tsconfig-utils': 8.61.1(typescript@6.0.3) - '@typescript-eslint/types': 8.61.1 - '@typescript-eslint/visitor-keys': 8.61.1 + '@typescript-eslint/project-service': 8.62.0(typescript@6.0.3) + '@typescript-eslint/tsconfig-utils': 8.62.0(typescript@6.0.3) + '@typescript-eslint/types': 8.62.0 + '@typescript-eslint/visitor-keys': 8.62.0 debug: 4.4.3 minimatch: 10.2.5 semver: 7.8.5 @@ -3816,27 +3827,27 @@ snapshots: transitivePeerDependencies: - supports-color - '@typescript-eslint/utils@8.61.1(eslint@10.5.0(jiti@2.7.0))(typescript@6.0.3)': + '@typescript-eslint/utils@8.62.0(eslint@10.6.0(jiti@2.7.0))(typescript@6.0.3)': dependencies: - '@eslint-community/eslint-utils': 4.9.1(eslint@10.5.0(jiti@2.7.0)) - '@typescript-eslint/scope-manager': 8.61.1 - '@typescript-eslint/types': 8.61.1 - '@typescript-eslint/typescript-estree': 8.61.1(typescript@6.0.3) - eslint: 10.5.0(jiti@2.7.0) + '@eslint-community/eslint-utils': 4.9.1(eslint@10.6.0(jiti@2.7.0)) + '@typescript-eslint/scope-manager': 8.62.0 + '@typescript-eslint/types': 8.62.0 + '@typescript-eslint/typescript-estree': 8.62.0(typescript@6.0.3) + eslint: 10.6.0(jiti@2.7.0) typescript: 6.0.3 transitivePeerDependencies: - supports-color - '@typescript-eslint/visitor-keys@8.61.1': + '@typescript-eslint/visitor-keys@8.62.0': dependencies: - '@typescript-eslint/types': 8.61.1 + '@typescript-eslint/types': 8.62.0 eslint-visitor-keys: 5.0.1 - '@vitejs/plugin-react-swc@4.3.1(@swc/helpers@0.5.23)(vite@8.0.16(@types/node@26.0.0)(jiti@2.7.0)(yaml@2.8.3))': + '@vitejs/plugin-react-swc@4.3.1(@swc/helpers@0.5.23)(vite@8.1.0(@types/node@26.0.0)(jiti@2.7.0)(yaml@2.8.3))': dependencies: '@rolldown/pluginutils': 1.0.1 '@swc/core': 1.15.40(@swc/helpers@0.5.23) - vite: 8.0.16(@types/node@26.0.0)(jiti@2.7.0)(yaml@2.8.3) + vite: 8.1.0(@types/node@26.0.0)(jiti@2.7.0)(yaml@2.8.3) transitivePeerDependencies: - '@swc/helpers' @@ -3852,7 +3863,7 @@ snapshots: obug: 2.1.3 std-env: 4.1.0 tinyrainbow: 3.1.0 - vitest: 4.1.9(@types/node@26.0.0)(@vitest/coverage-v8@4.1.9)(happy-dom@20.10.6)(vite@8.0.16(@types/node@26.0.0)(jiti@2.7.0)(yaml@2.8.3)) + vitest: 4.1.9(@types/node@26.0.0)(@vitest/coverage-v8@4.1.9)(happy-dom@20.10.6)(vite@8.1.0(@types/node@26.0.0)(jiti@2.7.0)(yaml@2.8.3)) '@vitest/expect@4.1.9': dependencies: @@ -3863,13 +3874,13 @@ snapshots: chai: 6.2.2 tinyrainbow: 3.1.0 - '@vitest/mocker@4.1.9(vite@8.0.16(@types/node@26.0.0)(jiti@2.7.0)(yaml@2.8.3))': + '@vitest/mocker@4.1.9(vite@8.1.0(@types/node@26.0.0)(jiti@2.7.0)(yaml@2.8.3))': dependencies: '@vitest/spy': 4.1.9 estree-walker: 3.0.3 magic-string: 0.30.21 optionalDependencies: - vite: 8.0.16(@types/node@26.0.0)(jiti@2.7.0)(yaml@2.8.3) + vite: 8.1.0(@types/node@26.0.0)(jiti@2.7.0)(yaml@2.8.3) '@vitest/pretty-format@4.1.9': dependencies: @@ -4580,7 +4591,7 @@ snapshots: axe-core@4.10.3: {} - axios@1.18.0: + axios@1.18.1: dependencies: follow-redirects: 1.16.0 form-data: 4.0.6 @@ -4613,7 +4624,7 @@ snapshots: dependencies: balanced-match: 1.0.2 - brace-expansion@5.0.6: + brace-expansion@5.0.7: dependencies: balanced-match: 4.0.4 @@ -4658,7 +4669,7 @@ snapshots: confbox: 0.2.4 defu: 6.1.7 dotenv: 17.4.2 - exsolve: 1.0.8 + exsolve: 1.1.0 giget: 3.3.0 jiti: 2.7.0 ohash: 2.0.11 @@ -4952,11 +4963,11 @@ snapshots: escape-string-regexp@4.0.0: {} - eslint-config-prettier@10.1.8(eslint@10.5.0(jiti@2.7.0)): + eslint-config-prettier@10.1.8(eslint@10.6.0(jiti@2.7.0)): dependencies: - eslint: 10.5.0(jiti@2.7.0) + eslint: 10.6.0(jiti@2.7.0) - eslint-plugin-jsx-a11y@6.10.2(eslint@10.5.0(jiti@2.7.0)): + eslint-plugin-jsx-a11y@6.10.2(eslint@10.6.0(jiti@2.7.0)): dependencies: aria-query: 5.3.2 array-includes: 3.1.8 @@ -4966,7 +4977,7 @@ snapshots: axobject-query: 4.1.0 damerau-levenshtein: 1.0.8 emoji-regex: 9.2.2 - eslint: 10.5.0(jiti@2.7.0) + eslint: 10.6.0(jiti@2.7.0) hasown: 2.0.2 jsx-ast-utils: 3.3.5 language-tags: 1.0.9 @@ -4975,40 +4986,40 @@ snapshots: safe-regex-test: 1.1.0 string.prototype.includes: 2.0.1 - eslint-plugin-perfectionist@5.9.1(eslint@10.5.0(jiti@2.7.0))(typescript@6.0.3): + eslint-plugin-perfectionist@5.9.1(eslint@10.6.0(jiti@2.7.0))(typescript@6.0.3): dependencies: - '@typescript-eslint/utils': 8.61.1(eslint@10.5.0(jiti@2.7.0))(typescript@6.0.3) - eslint: 10.5.0(jiti@2.7.0) + '@typescript-eslint/utils': 8.62.0(eslint@10.6.0(jiti@2.7.0))(typescript@6.0.3) + eslint: 10.6.0(jiti@2.7.0) natural-orderby: 5.0.0 transitivePeerDependencies: - supports-color - typescript - eslint-plugin-prettier@5.5.6(eslint-config-prettier@10.1.8(eslint@10.5.0(jiti@2.7.0)))(eslint@10.5.0(jiti@2.7.0))(prettier@3.8.4): + eslint-plugin-prettier@5.5.6(eslint-config-prettier@10.1.8(eslint@10.6.0(jiti@2.7.0)))(eslint@10.6.0(jiti@2.7.0))(prettier@3.9.3): dependencies: - eslint: 10.5.0(jiti@2.7.0) - prettier: 3.8.4 + eslint: 10.6.0(jiti@2.7.0) + prettier: 3.9.3 prettier-linter-helpers: 1.0.1 synckit: 0.11.13 optionalDependencies: - eslint-config-prettier: 10.1.8(eslint@10.5.0(jiti@2.7.0)) + eslint-config-prettier: 10.1.8(eslint@10.6.0(jiti@2.7.0)) - eslint-plugin-react-hooks@7.1.1(eslint@10.5.0(jiti@2.7.0)): + eslint-plugin-react-hooks@7.1.1(eslint@10.6.0(jiti@2.7.0)): dependencies: '@babel/core': 7.29.0 '@babel/parser': 7.29.2 - eslint: 10.5.0(jiti@2.7.0) + eslint: 10.6.0(jiti@2.7.0) hermes-parser: 0.25.1 zod: 4.3.6 zod-validation-error: 4.0.2(zod@4.3.6) transitivePeerDependencies: - supports-color - eslint-plugin-react-refresh@0.5.3(eslint@10.5.0(jiti@2.7.0)): + eslint-plugin-react-refresh@0.5.3(eslint@10.6.0(jiti@2.7.0)): dependencies: - eslint: 10.5.0(jiti@2.7.0) + eslint: 10.6.0(jiti@2.7.0) - eslint-plugin-react@7.37.5(eslint@10.5.0(jiti@2.7.0)): + eslint-plugin-react@7.37.5(eslint@10.6.0(jiti@2.7.0)): dependencies: array-includes: 3.1.8 array.prototype.findlast: 1.2.5 @@ -5016,7 +5027,7 @@ snapshots: array.prototype.tosorted: 1.1.4 doctrine: 2.1.0 es-iterator-helpers: 1.2.1 - eslint: 10.5.0(jiti@2.7.0) + eslint: 10.6.0(jiti@2.7.0) estraverse: 5.3.0 hasown: 2.0.2 jsx-ast-utils: 3.3.5 @@ -5030,16 +5041,16 @@ snapshots: string.prototype.matchall: 4.0.12 string.prototype.repeat: 1.0.0 - eslint-plugin-unicorn@67.0.0(eslint@10.5.0(jiti@2.7.0)): + eslint-plugin-unicorn@67.0.0(eslint@10.6.0(jiti@2.7.0)): dependencies: '@babel/helper-validator-identifier': 7.29.7 - '@eslint-community/eslint-utils': 4.9.1(eslint@10.5.0(jiti@2.7.0)) + '@eslint-community/eslint-utils': 4.9.1(eslint@10.6.0(jiti@2.7.0)) browserslist: 4.28.2 change-case: 5.4.4 ci-info: 4.4.0 core-js-compat: 3.49.0 detect-indent: 7.0.2 - eslint: 10.5.0(jiti@2.7.0) + eslint: 10.6.0(jiti@2.7.0) find-up-simple: 1.0.1 globals: 17.6.0 indent-string: 5.0.0 @@ -5063,9 +5074,9 @@ snapshots: eslint-visitor-keys@5.0.1: {} - eslint@10.5.0(jiti@2.7.0): + eslint@10.6.0(jiti@2.7.0): dependencies: - '@eslint-community/eslint-utils': 4.9.1(eslint@10.5.0(jiti@2.7.0)) + '@eslint-community/eslint-utils': 4.9.1(eslint@10.6.0(jiti@2.7.0)) '@eslint-community/regexpp': 4.12.2 '@eslint/config-array': 0.23.5 '@eslint/config-helpers': 0.6.0 @@ -5132,6 +5143,8 @@ snapshots: exsolve@1.0.8: {} + exsolve@1.1.0: {} + fast-deep-equal@3.1.3: {} fast-diff@1.3.0: {} @@ -5140,9 +5153,9 @@ snapshots: fast-levenshtein@2.0.6: {} - fdir@6.5.0(picomatch@4.0.4): + fdir@6.5.0(picomatch@4.0.5): optionalDependencies: - picomatch: 4.0.4 + picomatch: 4.0.5 file-entry-cache@8.0.0: dependencies: @@ -5492,6 +5505,10 @@ snapshots: dependencies: argparse: 2.0.1 + js-yaml@4.2.0: + dependencies: + argparse: 2.0.1 + jsesc@3.1.0: {} json-buffer@3.0.1: {} @@ -5621,7 +5638,7 @@ snapshots: minimatch@10.2.5: dependencies: - brace-expansion: 5.0.6 + brace-expansion: 5.0.7 minimatch@3.1.5: dependencies: @@ -5633,7 +5650,7 @@ snapshots: ms@2.1.3: {} - nanoid@3.3.12: {} + nanoid@3.3.15: {} natural-compare@1.4.0: {} @@ -5763,6 +5780,8 @@ snapshots: picomatch@4.0.4: {} + picomatch@4.0.5: {} + pkg-types@2.3.0: dependencies: confbox: 0.2.4 @@ -5772,16 +5791,16 @@ snapshots: pkg-types@2.3.1: dependencies: confbox: 0.2.4 - exsolve: 1.0.8 + exsolve: 1.1.0 pathe: 2.0.3 pluralize@8.0.0: {} possible-typed-array-names@1.1.0: {} - postcss@8.5.15: + postcss@8.5.16: dependencies: - nanoid: 3.3.12 + nanoid: 3.3.15 picocolors: 1.1.1 source-map-js: 1.2.1 @@ -5793,7 +5812,7 @@ snapshots: dependencies: fast-diff: 1.3.0 - prettier@3.8.4: {} + prettier@3.9.3: {} pretty-format@27.5.1: dependencies: @@ -5841,7 +5860,7 @@ snapshots: react: 19.2.7 scheduler: 0.27.0 - react-hook-form@7.79.0(react@19.2.7): + react-hook-form@7.80.0(react@19.2.7): dependencies: react: 19.2.7 @@ -5913,26 +5932,26 @@ snapshots: path-parse: 1.0.7 supports-preserve-symlinks-flag: 1.0.0 - rolldown@1.0.3: + rolldown@1.1.4: dependencies: - '@oxc-project/types': 0.133.0 + '@oxc-project/types': 0.138.0 '@rolldown/pluginutils': 1.0.1 optionalDependencies: - '@rolldown/binding-android-arm64': 1.0.3 - '@rolldown/binding-darwin-arm64': 1.0.3 - '@rolldown/binding-darwin-x64': 1.0.3 - '@rolldown/binding-freebsd-x64': 1.0.3 - '@rolldown/binding-linux-arm-gnueabihf': 1.0.3 - '@rolldown/binding-linux-arm64-gnu': 1.0.3 - '@rolldown/binding-linux-arm64-musl': 1.0.3 - '@rolldown/binding-linux-ppc64-gnu': 1.0.3 - '@rolldown/binding-linux-s390x-gnu': 1.0.3 - '@rolldown/binding-linux-x64-gnu': 1.0.3 - '@rolldown/binding-linux-x64-musl': 1.0.3 - '@rolldown/binding-openharmony-arm64': 1.0.3 - '@rolldown/binding-wasm32-wasi': 1.0.3 - '@rolldown/binding-win32-arm64-msvc': 1.0.3 - '@rolldown/binding-win32-x64-msvc': 1.0.3 + '@rolldown/binding-android-arm64': 1.1.4 + '@rolldown/binding-darwin-arm64': 1.1.4 + '@rolldown/binding-darwin-x64': 1.1.4 + '@rolldown/binding-freebsd-x64': 1.1.4 + '@rolldown/binding-linux-arm-gnueabihf': 1.1.4 + '@rolldown/binding-linux-arm64-gnu': 1.1.4 + '@rolldown/binding-linux-arm64-musl': 1.1.4 + '@rolldown/binding-linux-ppc64-gnu': 1.1.4 + '@rolldown/binding-linux-s390x-gnu': 1.1.4 + '@rolldown/binding-linux-x64-gnu': 1.1.4 + '@rolldown/binding-linux-x64-musl': 1.1.4 + '@rolldown/binding-openharmony-arm64': 1.1.4 + '@rolldown/binding-wasm32-wasi': 1.1.4 + '@rolldown/binding-win32-arm64-msvc': 1.1.4 + '@rolldown/binding-win32-x64-msvc': 1.1.4 run-applescript@7.1.0: {} @@ -5961,7 +5980,7 @@ snapshots: semver@7.7.3: {} - semver@7.8.2: {} + semver@7.8.4: {} semver@7.8.5: {} @@ -6107,8 +6126,8 @@ snapshots: tinyglobby@0.2.17: dependencies: - fdir: 6.5.0(picomatch@4.0.4) - picomatch: 4.0.4 + fdir: 6.5.0(picomatch@4.0.5) + picomatch: 4.0.5 tinyrainbow@3.1.0: {} @@ -6160,13 +6179,13 @@ snapshots: possible-typed-array-names: 1.1.0 reflect.getprototypeof: 1.0.10 - typescript-eslint@8.61.1(eslint@10.5.0(jiti@2.7.0))(typescript@6.0.3): + typescript-eslint@8.62.0(eslint@10.6.0(jiti@2.7.0))(typescript@6.0.3): dependencies: - '@typescript-eslint/eslint-plugin': 8.61.1(@typescript-eslint/parser@8.61.1(eslint@10.5.0(jiti@2.7.0))(typescript@6.0.3))(eslint@10.5.0(jiti@2.7.0))(typescript@6.0.3) - '@typescript-eslint/parser': 8.61.1(eslint@10.5.0(jiti@2.7.0))(typescript@6.0.3) - '@typescript-eslint/typescript-estree': 8.61.1(typescript@6.0.3) - '@typescript-eslint/utils': 8.61.1(eslint@10.5.0(jiti@2.7.0))(typescript@6.0.3) - eslint: 10.5.0(jiti@2.7.0) + '@typescript-eslint/eslint-plugin': 8.62.0(@typescript-eslint/parser@8.62.0(eslint@10.6.0(jiti@2.7.0))(typescript@6.0.3))(eslint@10.6.0(jiti@2.7.0))(typescript@6.0.3) + '@typescript-eslint/parser': 8.62.0(eslint@10.6.0(jiti@2.7.0))(typescript@6.0.3) + '@typescript-eslint/typescript-estree': 8.62.0(typescript@6.0.3) + '@typescript-eslint/utils': 8.62.0(eslint@10.6.0(jiti@2.7.0))(typescript@6.0.3) + eslint: 10.6.0(jiti@2.7.0) typescript: 6.0.3 transitivePeerDependencies: - supports-color @@ -6198,16 +6217,16 @@ snapshots: dependencies: punycode: 2.3.1 - vite-plugin-css-injected-by-js@5.0.1(vite@8.0.16(@types/node@26.0.0)(jiti@2.7.0)(yaml@2.8.3)): + vite-plugin-css-injected-by-js@5.0.1(vite@8.1.0(@types/node@26.0.0)(jiti@2.7.0)(yaml@2.8.3)): dependencies: - vite: 8.0.16(@types/node@26.0.0)(jiti@2.7.0)(yaml@2.8.3) + vite: 8.1.0(@types/node@26.0.0)(jiti@2.7.0)(yaml@2.8.3) - vite@8.0.16(@types/node@26.0.0)(jiti@2.7.0)(yaml@2.8.3): + vite@8.1.0(@types/node@26.0.0)(jiti@2.7.0)(yaml@2.8.3): dependencies: lightningcss: 1.32.0 - picomatch: 4.0.4 - postcss: 8.5.15 - rolldown: 1.0.3 + picomatch: 4.0.5 + postcss: 8.5.16 + rolldown: 1.1.4 tinyglobby: 0.2.17 optionalDependencies: '@types/node': 26.0.0 @@ -6215,10 +6234,10 @@ snapshots: jiti: 2.7.0 yaml: 2.8.3 - vitest@4.1.9(@types/node@26.0.0)(@vitest/coverage-v8@4.1.9)(happy-dom@20.10.6)(vite@8.0.16(@types/node@26.0.0)(jiti@2.7.0)(yaml@2.8.3)): + vitest@4.1.9(@types/node@26.0.0)(@vitest/coverage-v8@4.1.9)(happy-dom@20.10.6)(vite@8.1.0(@types/node@26.0.0)(jiti@2.7.0)(yaml@2.8.3)): dependencies: '@vitest/expect': 4.1.9 - '@vitest/mocker': 4.1.9(vite@8.0.16(@types/node@26.0.0)(jiti@2.7.0)(yaml@2.8.3)) + '@vitest/mocker': 4.1.9(vite@8.1.0(@types/node@26.0.0)(jiti@2.7.0)(yaml@2.8.3)) '@vitest/pretty-format': 4.1.9 '@vitest/runner': 4.1.9 '@vitest/snapshot': 4.1.9 @@ -6235,7 +6254,7 @@ snapshots: tinyexec: 1.2.4 tinyglobby: 0.2.17 tinyrainbow: 3.1.0 - vite: 8.0.16(@types/node@26.0.0)(jiti@2.7.0)(yaml@2.8.3) + vite: 8.1.0(@types/node@26.0.0)(jiti@2.7.0)(yaml@2.8.3) why-is-node-running: 2.3.0 optionalDependencies: '@types/node': 26.0.0 From 862cb269c5a8b874b8b406f08bbdce94b408b71d Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Sun, 5 Jul 2026 21:19:38 +0800 Subject: [PATCH 024/297] [v3-3-test] [AIP-94] Mark dags list-import-errors as migrated to airflowctl (#68602) (#69390) (cherry picked from commit 924f022d901acebaf75f8c1d9259bbcc024481ad) Co-authored-by: Yuseok Jo --- airflow-core/src/airflow/cli/commands/dag_command.py | 1 + .../tests/unit/cli/commands/test_command_deprecations.py | 1 + 2 files changed, 2 insertions(+) diff --git a/airflow-core/src/airflow/cli/commands/dag_command.py b/airflow-core/src/airflow/cli/commands/dag_command.py index 6a5b9b3a7cffc..41d1c8c6669a7 100644 --- a/airflow-core/src/airflow/cli/commands/dag_command.py +++ b/airflow-core/src/airflow/cli/commands/dag_command.py @@ -638,6 +638,7 @@ def dag_details(args, *, session: Session = NEW_SESSION): @cli_utils.action_cli +@deprecated_for_airflowctl("airflowctl dags list-import-errors") @suppress_logs_and_warning @providers_configuration_loaded @provide_session diff --git a/airflow-core/tests/unit/cli/commands/test_command_deprecations.py b/airflow-core/tests/unit/cli/commands/test_command_deprecations.py index 411cb6a84f14e..bcf7516dbd864 100644 --- a/airflow-core/tests/unit/cli/commands/test_command_deprecations.py +++ b/airflow-core/tests/unit/cli/commands/test_command_deprecations.py @@ -44,6 +44,7 @@ (dag_command.dag_trigger, "airflowctl dags trigger"), (dag_command.dag_delete, "airflowctl dags delete"), (dag_command.dag_details, "airflowctl dags get-details"), + (dag_command.dag_list_import_errors, "airflowctl dags list-import-errors"), (pool_command.pool_list, "airflowctl pools list"), (pool_command.pool_get, "airflowctl pools get"), (pool_command.pool_set, "airflowctl pools create"), From ea24516bfec54851a1d705cfa8886b0c118d0586 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Sun, 5 Jul 2026 15:52:17 +0200 Subject: [PATCH 025/297] [v3-3-test] [AIP-94] Mark connections commands as migrated to airflowctl (#68972) (#69410) (cherry picked from commit cf2806199fe07c5cab7c8287ab06dd1fbca83517) Co-authored-by: Yuseok Jo --- .../src/airflow/cli/commands/connection_command.py | 13 ++++++++++++- .../unit/cli/commands/test_command_deprecations.py | 7 +++++++ 2 files changed, 19 insertions(+), 1 deletion(-) diff --git a/airflow-core/src/airflow/cli/commands/connection_command.py b/airflow-core/src/airflow/cli/commands/connection_command.py index 8911bc80b99e2..ce87af7a9c374 100644 --- a/airflow-core/src/airflow/cli/commands/connection_command.py +++ b/airflow-core/src/airflow/cli/commands/connection_command.py @@ -30,7 +30,12 @@ from sqlalchemy.orm import exc from airflow.cli.simple_table import AirflowConsole -from airflow.cli.utils import SENSITIVE_PLACEHOLDER, is_stdout, print_export_output +from airflow.cli.utils import ( + SENSITIVE_PLACEHOLDER, + deprecated_for_airflowctl, + is_stdout, + print_export_output, +) from airflow.configuration import conf from airflow.exceptions import AirflowNotFoundException from airflow.models import Connection @@ -140,6 +145,7 @@ def connections_get(args): ) +@deprecated_for_airflowctl("airflowctl connections list") @suppress_logs_and_warning @providers_configuration_loaded def connections_list(args): @@ -186,6 +192,7 @@ def _connection_to_dict(conn: Connection) -> dict: } +@deprecated_for_airflowctl("airflowctl connections create-defaults") def create_default_connections(args): db_create_default_connections() @@ -283,6 +290,7 @@ def connections_export(args): @cli_utils.action_cli +@deprecated_for_airflowctl("airflowctl connections create") @providers_configuration_loaded def connections_add(args): """Add new connection.""" @@ -379,6 +387,7 @@ def connections_add(args): @cli_utils.action_cli +@deprecated_for_airflowctl("airflowctl connections delete") @providers_configuration_loaded def connections_delete(args): """Delete connection from DB.""" @@ -395,6 +404,7 @@ def connections_delete(args): @cli_utils.action_cli(check_db=False) +@deprecated_for_airflowctl("airflowctl connections import") @providers_configuration_loaded def connections_import(args): """Import connections from a file.""" @@ -433,6 +443,7 @@ def _import_helper(file_path: str, overwrite: bool) -> None: print(f"Imported connection {conn_id}") +@deprecated_for_airflowctl("airflowctl connections test") @suppress_logs_and_warning @providers_configuration_loaded def connections_test(args) -> None: diff --git a/airflow-core/tests/unit/cli/commands/test_command_deprecations.py b/airflow-core/tests/unit/cli/commands/test_command_deprecations.py index bcf7516dbd864..287b230ea3476 100644 --- a/airflow-core/tests/unit/cli/commands/test_command_deprecations.py +++ b/airflow-core/tests/unit/cli/commands/test_command_deprecations.py @@ -33,6 +33,7 @@ from airflow.cli.commands import ( asset_command, config_command, + connection_command, dag_command, pool_command, provider_command, @@ -41,6 +42,12 @@ # (command callable, expected airflowctl replacement recorded by the decorator) MIGRATED_CLI_COMMANDS = [ + (connection_command.connections_list, "airflowctl connections list"), + (connection_command.connections_add, "airflowctl connections create"), + (connection_command.connections_delete, "airflowctl connections delete"), + (connection_command.connections_import, "airflowctl connections import"), + (connection_command.connections_test, "airflowctl connections test"), + (connection_command.create_default_connections, "airflowctl connections create-defaults"), (dag_command.dag_trigger, "airflowctl dags trigger"), (dag_command.dag_delete, "airflowctl dags delete"), (dag_command.dag_details, "airflowctl dags get-details"), From 2d1892884915236752bb8e08d7e591f48b0f6b3f Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Sun, 5 Jul 2026 15:52:22 +0200 Subject: [PATCH 026/297] [v3-3-test] Document native template rendering type coercion (#34641) (#69152) (#69389) (cherry picked from commit b5a60ceae6b6bbe4434782d682aba275d4034e17) Co-authored-by: Deepak Jain --- airflow-core/docs/core-concepts/operators.rst | 7 +++++++ airflow-core/docs/core-concepts/params.rst | 5 +++++ 2 files changed, 12 insertions(+) diff --git a/airflow-core/docs/core-concepts/operators.rst b/airflow-core/docs/core-concepts/operators.rst index ac460183c2f7a..6bcf4a3c7140a 100644 --- a/airflow-core/docs/core-concepts/operators.rst +++ b/airflow-core/docs/core-concepts/operators.rst @@ -303,6 +303,13 @@ Alternatively, Jinja can also be instructed to render a native Python object. Th python_callable=transform, ) +.. note:: + + ``NativeEnvironment`` renders values according to Python literal rules. This is useful when a template + should produce a list, dict, number, or boolean, but it also means a string that looks like a number, + such as ``"42"``, can be rendered as the integer ``42``. Keep the default string rendering, use a + callable template field, or add explicit quoting if the task needs the value to stay a string. + .. _concepts:reserved-keywords: diff --git a/airflow-core/docs/core-concepts/params.rst b/airflow-core/docs/core-concepts/params.rst index ac5df462fac98..2f70611e88c29 100644 --- a/airflow-core/docs/core-concepts/params.rst +++ b/airflow-core/docs/core-concepts/params.rst @@ -145,6 +145,11 @@ This way, the :class:`~airflow.sdk.definitions.param.Param`'s type is respected ), ) +Because ``render_template_as_native_obj=True`` uses Jinja's native rendering, values that look like +Python literals can also be converted. For example, a string value of ``"42"`` may be rendered as the +integer ``42``. Leave native rendering disabled, use a callable template field, or quote the value +explicitly when the task must receive a string. + Another way to access your param is via a task's ``context`` kwarg. .. code-block:: From ec1d891fed62cae6f61643da6db3f4f712395dd7 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Sun, 5 Jul 2026 15:52:26 +0200 Subject: [PATCH 027/297] [v3-3-test] Update multi-node executor guidance (#48605) (#69154) (#69388) * Update multi-node executor guidance (#48605) * Use accepted spelling for executor trade-offs (cherry picked from commit 2c46d0b3c458f5592c8b736e36956fba49f2f0f8) Co-authored-by: Deepak Jain --- .../production-deployment.rst | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/airflow-core/docs/administration-and-deployment/production-deployment.rst b/airflow-core/docs/administration-and-deployment/production-deployment.rst index e88b94d94ba8b..8e1258bb9da49 100644 --- a/airflow-core/docs/administration-and-deployment/production-deployment.rst +++ b/airflow-core/docs/administration-and-deployment/production-deployment.rst @@ -57,8 +57,13 @@ Multi-Node Cluster ================== Airflow uses :class:`~airflow.executors.local_executor.LocalExecutor` by default. For a multi-node setup, -you should use the :doc:`Kubernetes executor ` or -the :doc:`Celery executor `. +choose a remote executor, or a multi-executor configuration, that matches where tasks should run. +Common choices include the :doc:`Celery executor `, +the :doc:`Kubernetes executor `, +Amazon provider executors such as :doc:`Batch ` +or :doc:`ECS `, and the +:doc:`Edge executor `. See the +:ref:`executor comparison ` for the current list and trade-offs. Once you have configured the executor, it is necessary to make sure that every node in the cluster contains From f0ad3617e899b5af5c40b82a09b63498e4c45750 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Sun, 5 Jul 2026 15:52:31 +0200 Subject: [PATCH 028/297] [v3-3-test] Clarify custom-time parameterized timetable logic (#34897) (#69151) (#69387) (cherry picked from commit a9f0cf504543d67b3d0f9c458956e41dd8a92ef0) Co-authored-by: Deepak Jain --- airflow-core/docs/howto/timetable.rst | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/airflow-core/docs/howto/timetable.rst b/airflow-core/docs/howto/timetable.rst index 90308ca42e52e..0fc6fa9ea5651 100644 --- a/airflow-core/docs/howto/timetable.rst +++ b/airflow-core/docs/howto/timetable.rst @@ -238,6 +238,13 @@ purpose, we'd want to do something like: run_after=DateTime.combine(end.date(), self._schedule_at).replace(tzinfo=UTC), ) +If you adapt the first-run logic from ``AfterWorkdayTimetable`` for a custom +``schedule_at`` value, compare the candidate time with ``self._schedule_at``. +The midnight-specific check in the earlier example is only correct when runs +are scheduled at ``00:00``. For example, an earliest time of ``06:00`` should +still allow an ``08:00`` same-day run, while an earliest time of ``09:00`` should +move to the next workday. + However, since the timetable is a part of the Dag, we need to tell Airflow how to serialize it with the context we provide in ``__init__``. This is done by implementing two additional methods on our timetable class: From 7dc0a21b883ed93d5b3f617ea76f29bcc8dcd268 Mon Sep 17 00:00:00 2001 From: Henry Chen Date: Sun, 5 Jul 2026 23:00:57 +0800 Subject: [PATCH 029/297] [AIP-94] Mark dags pause/unpause as migrated to airflowctl (#68650) (#69412) (cherry picked from commit 638c63523a2f07c6391dcadbca2deafe43b0e3ee) Co-authored-by: Yuseok Jo --- airflow-core/src/airflow/cli/commands/dag_command.py | 2 ++ .../tests/unit/cli/commands/test_command_deprecations.py | 2 ++ 2 files changed, 4 insertions(+) diff --git a/airflow-core/src/airflow/cli/commands/dag_command.py b/airflow-core/src/airflow/cli/commands/dag_command.py index 41d1c8c6669a7..ec5b62ca1d3f4 100644 --- a/airflow-core/src/airflow/cli/commands/dag_command.py +++ b/airflow-core/src/airflow/cli/commands/dag_command.py @@ -241,6 +241,7 @@ def _bulk_clear_runs( @cli_utils.action_cli +@deprecated_for_airflowctl("airflowctl dags pause") @providers_configuration_loaded def dag_pause(args) -> None: """Pauses a DAG.""" @@ -248,6 +249,7 @@ def dag_pause(args) -> None: @cli_utils.action_cli +@deprecated_for_airflowctl("airflowctl dags unpause") @providers_configuration_loaded def dag_unpause(args) -> None: """Unpauses a DAG.""" diff --git a/airflow-core/tests/unit/cli/commands/test_command_deprecations.py b/airflow-core/tests/unit/cli/commands/test_command_deprecations.py index 287b230ea3476..0308e5f7230ed 100644 --- a/airflow-core/tests/unit/cli/commands/test_command_deprecations.py +++ b/airflow-core/tests/unit/cli/commands/test_command_deprecations.py @@ -51,6 +51,8 @@ (dag_command.dag_trigger, "airflowctl dags trigger"), (dag_command.dag_delete, "airflowctl dags delete"), (dag_command.dag_details, "airflowctl dags get-details"), + (dag_command.dag_pause, "airflowctl dags pause"), + (dag_command.dag_unpause, "airflowctl dags unpause"), (dag_command.dag_list_import_errors, "airflowctl dags list-import-errors"), (pool_command.pool_list, "airflowctl pools list"), (pool_command.pool_get, "airflowctl pools get"), From 56706deb6fc5c54389741dff81d5913832f3b70f Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Mon, 6 Jul 2026 00:57:52 +0200 Subject: [PATCH 030/297] [v3-3-test] UI: Load Monaco codicon glyph styles via direct CSS import (#69419) (#69422) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit monaco-editor 0.53 removed the `codiconStyles` side-effect module that the local ESM Monaco setup imported to register the codicon glyph font (the folding arrows and find-widget icons). Import the two stylesheets that module pulled in — `codicon/codicon.css` and `codicon/codicon-modifiers.css` — directly instead, mirroring the existing dynamic-CSS-import pattern already used for Katex. Both files ship in the currently pinned 0.52.2 and in newer releases, so this is behaviour-neutral today and lets the editor keep rendering its glyphs once monaco-editor is bumped past 0.52, unblocking the pending dependency update. (cherry picked from commit 2629cf34ce27ee51430e62d968df46d034ea9e08) Co-authored-by: Jarek Potiuk --- .../ui/src/components/MonacoEditor/configureMonaco.ts | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/airflow-core/src/airflow/ui/src/components/MonacoEditor/configureMonaco.ts b/airflow-core/src/airflow/ui/src/components/MonacoEditor/configureMonaco.ts index 737de9e020cb1..97b6ae6be20e5 100644 --- a/airflow-core/src/airflow/ui/src/components/MonacoEditor/configureMonaco.ts +++ b/airflow-core/src/airflow/ui/src/components/MonacoEditor/configureMonaco.ts @@ -34,7 +34,11 @@ const loadMonacoModules = async () => { import("monaco-editor/esm/vs/editor/editor.api"), import("monaco-editor/esm/vs/editor/contrib/folding/browser/folding"), import("monaco-editor/esm/vs/editor/contrib/find/browser/findController"), - import("monaco-editor/esm/vs/base/browser/ui/codicons/codiconStyles"), + // monaco-editor 0.53 removed the `codiconStyles` side-effect module; import the two codicon + // stylesheets it used to pull in directly so folding/find glyphs still render. Both files + // ship in 0.52 and 0.55, so this resolves against the current pin and any newer bump. + import("monaco-editor/esm/vs/base/browser/ui/codicons/codicon/codicon.css"), + import("monaco-editor/esm/vs/base/browser/ui/codicons/codicon/codicon-modifiers.css"), ]).then(([api]) => api); // Resolve the bundled worker URLs (`?worker&url` runs the worker through Vite's worker From bf20180d9a6f6df4ce1aebbe4900ea487601b408 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Mon, 6 Jul 2026 16:58:38 +0900 Subject: [PATCH 031/297] [v3-3-test] Link pkg.go.dev API reference from the Go SDK docs (#69429) (#69440) (cherry picked from commit 603a712d6c9f19ab0eb3ce97b5faaa7c58007709) Co-authored-by: Jason(Zhe-You) Liu <68415893+jason810496@users.noreply.github.com> --- .../docs/authoring-and-scheduling/language-sdks/go.rst | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/airflow-core/docs/authoring-and-scheduling/language-sdks/go.rst b/airflow-core/docs/authoring-and-scheduling/language-sdks/go.rst index 722bd7549e5a3..158fff4ddeb2f 100644 --- a/airflow-core/docs/authoring-and-scheduling/language-sdks/go.rst +++ b/airflow-core/docs/authoring-and-scheduling/language-sdks/go.rst @@ -37,6 +37,12 @@ the bundle: one runnable file to ship, with no separate manifest or archive. The :local: :depth: 2 +API reference +------------- + +The generated API reference for the Go SDK module, and the list of its released versions, is available on +`pkg.go.dev `__. + Prerequisites ------------- From ea2a61095dfaf86ae6766732fc30b3316a449595 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Mon, 6 Jul 2026 17:51:02 +0800 Subject: [PATCH 032/297] [v3-3-test] Link the published Java SDK API reference from the Java SDK docs (#69433) (#69448) * Link the published Java SDK API reference from the Java SDK docs The interface-based API section pointed readers at the published JavaDoc without a link, with a TODO left from AIP-108 waiting for the docs site location to exist. The Javadoc publishing pipeline now targets https://airflow.apache.org/docs/java-sdk/stable/, so the placeholder can become a real link, matching how other SDK references are linked. * Stop calling the published Java SDK API reference Javadoc The site on airflow.apache.org is an HTML rendering of the API, not an actual Javadoc rendering; Javadoc is bundled with the released artifacts instead. The term also fails the docs spell check. (cherry picked from commit 66798f96d15263e6c7cfd4d3a5568bb6adcee019) Co-authored-by: Jason(Zhe-You) Liu <68415893+jason810496@users.noreply.github.com> --- .../authoring-and-scheduling/language-sdks/java.rst | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/airflow-core/docs/authoring-and-scheduling/language-sdks/java.rst b/airflow-core/docs/authoring-and-scheduling/language-sdks/java.rst index 55a80f9967fce..422b6a3985ef0 100644 --- a/airflow-core/docs/authoring-and-scheduling/language-sdks/java.rst +++ b/airflow-core/docs/authoring-and-scheduling/language-sdks/java.rst @@ -30,6 +30,12 @@ scheduling remain in Python; individual tasks delegate to a JVM subprocess that :local: :depth: 2 +API reference +------------- + +The generated API reference for the Java SDK is published with the Airflow documentation at +`Java SDK API Reference `__. + Prerequisites ------------- @@ -226,9 +232,7 @@ Register tasks manually in a ``BundleBuilder``: } } -See the Java SDK's published JavaDoc for more details. - -.. TODO: (AIP-108) Put a link here once we publish the JavaDoc. +See the `Java SDK API Reference `__ for more details. .. _java-sdk/logging: From b2d937c5f637c387a0caf6c92cfb4eaee22cfeb5 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Mon, 6 Jul 2026 10:14:51 -0400 Subject: [PATCH 033/297] [v3-3-test] Remove defensive bundle/team pre-cleaning from api_fastapi tests (#69416) (#69420) (cherry picked from commit 9ec996edc4f4dafc075cadf10752480941bc8799) Co-authored-by: Anish Giri <161533316+anishgirianish@users.noreply.github.com> --- airflow-core/tests/unit/api_fastapi/common/db/test_dags.py | 1 - .../unit/api_fastapi/core_api/routes/public/test_assets.py | 2 -- .../unit/api_fastapi/core_api/routes/public/test_backfills.py | 1 + .../unit/api_fastapi/core_api/routes/public/test_dag_run.py | 2 -- .../unit/api_fastapi/core_api/routes/public/test_dag_stats.py | 4 ---- .../unit/api_fastapi/core_api/routes/public/test_dag_tags.py | 2 -- .../api_fastapi/core_api/routes/public/test_dag_versions.py | 3 +-- .../api_fastapi/core_api/routes/public/test_import_error.py | 1 - .../unit/api_fastapi/core_api/routes/public/test_pools.py | 2 +- .../api_fastapi/core_api/routes/public/test_task_instances.py | 1 - .../core_api/routes/public/test_task_state_store.py | 1 - .../unit/api_fastapi/core_api/routes/public/test_tasks.py | 1 - .../unit/api_fastapi/core_api/routes/public/test_variables.py | 1 - .../unit/api_fastapi/core_api/routes/ui/test_backfills.py | 1 - 14 files changed, 3 insertions(+), 20 deletions(-) diff --git a/airflow-core/tests/unit/api_fastapi/common/db/test_dags.py b/airflow-core/tests/unit/api_fastapi/common/db/test_dags.py index a0747747670f5..1fe74438c84b6 100644 --- a/airflow-core/tests/unit/api_fastapi/common/db/test_dags.py +++ b/airflow-core/tests/unit/api_fastapi/common/db/test_dags.py @@ -45,7 +45,6 @@ def _clear_db(): @pytest.fixture(autouse=True) def setup_teardown(self): """Setup and teardown for each test.""" - self._clear_db() yield self._clear_db() diff --git a/airflow-core/tests/unit/api_fastapi/core_api/routes/public/test_assets.py b/airflow-core/tests/unit/api_fastapi/core_api/routes/public/test_assets.py index 0470b0b1e4e51..3487fa28598a2 100644 --- a/airflow-core/tests/unit/api_fastapi/core_api/routes/public/test_assets.py +++ b/airflow-core/tests/unit/api_fastapi/core_api/routes/public/test_assets.py @@ -253,7 +253,6 @@ def setup(self): clear_db_assets() clear_db_runs() clear_db_dags() - clear_db_dag_bundles() clear_db_logs() yield @@ -710,7 +709,6 @@ def setup(self) -> None: clear_db_assets() clear_db_runs() clear_db_dags() - clear_db_dag_bundles() def teardown_method(self) -> None: clear_db_assets() diff --git a/airflow-core/tests/unit/api_fastapi/core_api/routes/public/test_backfills.py b/airflow-core/tests/unit/api_fastapi/core_api/routes/public/test_backfills.py index 2380f73b9bf7b..d1bf2b517298d 100644 --- a/airflow-core/tests/unit/api_fastapi/core_api/routes/public/test_backfills.py +++ b/airflow-core/tests/unit/api_fastapi/core_api/routes/public/test_backfills.py @@ -73,6 +73,7 @@ def _clean_db(): @pytest.fixture(autouse=True) def clean_db(): + yield _clean_db() diff --git a/airflow-core/tests/unit/api_fastapi/core_api/routes/public/test_dag_run.py b/airflow-core/tests/unit/api_fastapi/core_api/routes/public/test_dag_run.py index 39a391f80f1b4..43a9e36c198f7 100644 --- a/airflow-core/tests/unit/api_fastapi/core_api/routes/public/test_dag_run.py +++ b/airflow-core/tests/unit/api_fastapi/core_api/routes/public/test_dag_run.py @@ -56,7 +56,6 @@ from tests_common.test_utils.db import ( clear_db_assets, clear_db_connections, - clear_db_dag_bundles, clear_db_dags, clear_db_logs, clear_db_runs, @@ -141,7 +140,6 @@ def setup(request, dag_maker, *, session=None): clear_db_connections() clear_db_runs() clear_db_dags() - clear_db_dag_bundles() clear_db_serialized_dags() clear_db_logs() clear_db_assets() diff --git a/airflow-core/tests/unit/api_fastapi/core_api/routes/public/test_dag_stats.py b/airflow-core/tests/unit/api_fastapi/core_api/routes/public/test_dag_stats.py index a3f8038fe2018..d807c363c4267 100644 --- a/airflow-core/tests/unit/api_fastapi/core_api/routes/public/test_dag_stats.py +++ b/airflow-core/tests/unit/api_fastapi/core_api/routes/public/test_dag_stats.py @@ -134,10 +134,6 @@ def _create_dag_and_runs(self, session=None): session.add_all(entities) session.commit() - @pytest.fixture(autouse=True) - def setup(self) -> None: - self._clear_db() - def teardown_method(self) -> None: self._clear_db() diff --git a/airflow-core/tests/unit/api_fastapi/core_api/routes/public/test_dag_tags.py b/airflow-core/tests/unit/api_fastapi/core_api/routes/public/test_dag_tags.py index 82f3d11ed4ffb..2692ea07ff148 100644 --- a/airflow-core/tests/unit/api_fastapi/core_api/routes/public/test_dag_tags.py +++ b/airflow-core/tests/unit/api_fastapi/core_api/routes/public/test_dag_tags.py @@ -106,8 +106,6 @@ def _create_dag_tags(self, session=None): @pytest.fixture(autouse=True) @provide_session def setup(self, dag_maker, *, session=None) -> None: - self._clear_db() - with dag_maker( DAG1_ID, dag_display_name=DAG1_DISPLAY_NAME, diff --git a/airflow-core/tests/unit/api_fastapi/core_api/routes/public/test_dag_versions.py b/airflow-core/tests/unit/api_fastapi/core_api/routes/public/test_dag_versions.py index e83721783c754..73051be51df5a 100644 --- a/airflow-core/tests/unit/api_fastapi/core_api/routes/public/test_dag_versions.py +++ b/airflow-core/tests/unit/api_fastapi/core_api/routes/public/test_dag_versions.py @@ -23,7 +23,7 @@ from airflow.providers.standard.operators.empty import EmptyOperator from tests_common.test_utils.asserts import assert_queries_count -from tests_common.test_utils.db import clear_db_dag_bundles, clear_db_dags, clear_db_serialized_dags +from tests_common.test_utils.db import clear_db_dags, clear_db_serialized_dags pytestmark = pytest.mark.db_test @@ -33,7 +33,6 @@ class TestDagVersionEndpoint: def setup(request, dag_maker, session): clear_db_dags() clear_db_serialized_dags() - clear_db_dag_bundles() with dag_maker( dag_id="ANOTHER_DAG_ID", bundle_version="some_commit_hash", bundle_name="another_bundle_name" diff --git a/airflow-core/tests/unit/api_fastapi/core_api/routes/public/test_import_error.py b/airflow-core/tests/unit/api_fastapi/core_api/routes/public/test_import_error.py index 0db0c07c5f79c..e7be4fa3219ea 100644 --- a/airflow-core/tests/unit/api_fastapi/core_api/routes/public/test_import_error.py +++ b/airflow-core/tests/unit/api_fastapi/core_api/routes/public/test_import_error.py @@ -102,7 +102,6 @@ def not_permitted_dag_model(testing_dag_bundle, *, session: Session = NEW_SESSIO def clear_db(): clear_db_import_errors() clear_db_dags() - clear_db_dag_bundles() yield diff --git a/airflow-core/tests/unit/api_fastapi/core_api/routes/public/test_pools.py b/airflow-core/tests/unit/api_fastapi/core_api/routes/public/test_pools.py index d779359d9ccf4..d56aceecfa0f2 100644 --- a/airflow-core/tests/unit/api_fastapi/core_api/routes/public/test_pools.py +++ b/airflow-core/tests/unit/api_fastapi/core_api/routes/public/test_pools.py @@ -73,10 +73,10 @@ class TestPoolsEndpoint: @pytest.fixture(autouse=True) def setup(self) -> None: clear_db_pools() - clear_db_teams() def teardown_method(self) -> None: clear_db_pools() + clear_db_teams() def create_pools(self): _create_team() diff --git a/airflow-core/tests/unit/api_fastapi/core_api/routes/public/test_task_instances.py b/airflow-core/tests/unit/api_fastapi/core_api/routes/public/test_task_instances.py index 40337bb9be71c..93569e594829e 100644 --- a/airflow-core/tests/unit/api_fastapi/core_api/routes/public/test_task_instances.py +++ b/airflow-core/tests/unit/api_fastapi/core_api/routes/public/test_task_instances.py @@ -6004,7 +6004,6 @@ class TestBulkTaskInstances(TestTaskInstanceEndpoint): @pytest.fixture(autouse=True) def clean_db(self, session): clear_db_runs() - clear_db_teams() yield clear_db_teams() clear_db_runs() diff --git a/airflow-core/tests/unit/api_fastapi/core_api/routes/public/test_task_state_store.py b/airflow-core/tests/unit/api_fastapi/core_api/routes/public/test_task_state_store.py index 4061106435649..650858656da5f 100644 --- a/airflow-core/tests/unit/api_fastapi/core_api/routes/public/test_task_state_store.py +++ b/airflow-core/tests/unit/api_fastapi/core_api/routes/public/test_task_state_store.py @@ -78,7 +78,6 @@ def clear_db(): @pytest.fixture(autouse=True) def setup(self, dag_maker, session): - self.clear_db() _create_dag_run(dag_maker, session) self.dag_run = session.scalar(select(DagRun).where(DagRun.run_id == RUN_ID)) self._session = session diff --git a/airflow-core/tests/unit/api_fastapi/core_api/routes/public/test_tasks.py b/airflow-core/tests/unit/api_fastapi/core_api/routes/public/test_tasks.py index 0dacd19397eae..9fc48a9bbb8c9 100644 --- a/airflow-core/tests/unit/api_fastapi/core_api/routes/public/test_tasks.py +++ b/airflow-core/tests/unit/api_fastapi/core_api/routes/public/test_tasks.py @@ -81,7 +81,6 @@ def clear_db(): @pytest.fixture(autouse=True) def setup(self, test_client) -> None: - self.clear_db() self.create_dags(test_client) def teardown_method(self) -> None: diff --git a/airflow-core/tests/unit/api_fastapi/core_api/routes/public/test_variables.py b/airflow-core/tests/unit/api_fastapi/core_api/routes/public/test_variables.py index ed480b036026e..234bbe6b1f777 100644 --- a/airflow-core/tests/unit/api_fastapi/core_api/routes/public/test_variables.py +++ b/airflow-core/tests/unit/api_fastapi/core_api/routes/public/test_variables.py @@ -141,7 +141,6 @@ class TestVariableEndpoint: @pytest.fixture(autouse=True) def setup(self): clear_db_variables() - clear_db_teams() with conf_vars({("core", "multi_team"): "True"}): yield diff --git a/airflow-core/tests/unit/api_fastapi/core_api/routes/ui/test_backfills.py b/airflow-core/tests/unit/api_fastapi/core_api/routes/ui/test_backfills.py index 21ae10d23b3a9..ebb23efff7d4a 100644 --- a/airflow-core/tests/unit/api_fastapi/core_api/routes/ui/test_backfills.py +++ b/airflow-core/tests/unit/api_fastapi/core_api/routes/ui/test_backfills.py @@ -53,7 +53,6 @@ def _clean_db(): @pytest.fixture(autouse=True) def clean_db(): - _clean_db() yield _clean_db() From ede00c37b91c2694148e5b2234a16bc7129df493 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Mon, 6 Jul 2026 16:41:51 +0200 Subject: [PATCH 034/297] [v3-3-test] Fix partition label casing and Taiwanese Mandarin ranslations (#69455) (#69470) (cherry picked from commit 8aabd056e8a5d6436cd066c34798459c5d343dca) Co-authored-by: Wei Lee --- .../src/airflow/ui/public/i18n/locales/en/common.json | 4 ++-- .../src/airflow/ui/public/i18n/locales/zh-TW/common.json | 8 ++++---- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/airflow-core/src/airflow/ui/public/i18n/locales/en/common.json b/airflow-core/src/airflow/ui/public/i18n/locales/en/common.json index 7b5f37c9cfd3b..88a348e1de54d 100644 --- a/airflow-core/src/airflow/ui/public/i18n/locales/en/common.json +++ b/airflow-core/src/airflow/ui/public/i18n/locales/en/common.json @@ -71,9 +71,9 @@ }, "expectedDuration": "Expected Duration", "lastSchedulingDecision": "Last Scheduling Decision", - "mappedPartitionKey": "Mapped Partition key", + "mappedPartitionKey": "Mapped Partition Key", "partitionDate": "Partition Date", - "partitionKey": "Partition key", + "partitionKey": "Partition Key", "queuedAt": "Queued At", "runAfter": "Run After", "runType": "Run Type", diff --git a/airflow-core/src/airflow/ui/public/i18n/locales/zh-TW/common.json b/airflow-core/src/airflow/ui/public/i18n/locales/zh-TW/common.json index a98d200635ce3..b2010475a3781 100644 --- a/airflow-core/src/airflow/ui/public/i18n/locales/zh-TW/common.json +++ b/airflow-core/src/airflow/ui/public/i18n/locales/zh-TW/common.json @@ -72,8 +72,8 @@ "expectedDuration": "預計時長", "lastSchedulingDecision": "最後排程決策", "mappedPartitionKey": "映射分區鍵", - "partitionDate": "資產分區日期", - "partitionKey": "資產分區鍵", + "partitionDate": "資源分區日期", + "partitionKey": "資源分區鍵", "queuedAt": "開始排隊時間", "runAfter": "最早可執行時間", "runType": "執行類型", @@ -193,8 +193,8 @@ "partitionedDagRunDetail": { "receivedAssetEvents": "收到的資源事件" }, - "pendingDagRun_one": "待執行的 Dag 執行", - "pendingDagRun_other": "待執行的 Dag 執行", + "pendingDagRun_one": "{{count}} 個待執行的 Dag 執行", + "pendingDagRun_other": "{{count}} 個待執行的 Dag 執行", "reset": "重置", "runId": "執行 ID", "runTypes": { From 35be7a600da8af561c1d0daf107d49debc41c121 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Mon, 6 Jul 2026 23:51:25 +0800 Subject: [PATCH 035/297] [v3-3-test] Archive worker-reported end date and rendered map index on task retry (#69248) (#69458) * Archive worker-reported end date and rendered map index on task retry Follow-up to #69235. The task_instance_history row for a retried try stamped archive-time utcnow() as end_date instead of the end date the worker reported, and missed the final rendered map index when the mid-run update was suppressed (e.g. template errors during failure handling). Snapshot both onto the TI before archiving, and let record_ti() respect a pre-set end_date so the audit trail reflects when the try actually ended. * Add test for record_ti fallback end_date stamping Cover the conditional branch where record_ti() archives a non-finished TI with end_date=None, verifying it gets stamped with utcnow() and duration is computed correctly. * Clarify record_ti comment covers pre-set duration too * Snapshot rendered_map_index in TIH when a retry explicitly clears it (cherry picked from commit 66b803d5efbbb743c357f2b4696f49a22aa662b8) Co-authored-by: Jason(Zhe-You) Liu <68415893+jason810496@users.noreply.github.com> --- .../execution_api/routes/task_instances.py | 23 ++++---- .../src/airflow/models/taskinstancehistory.py | 7 ++- .../versions/head/test_task_instances.py | 57 +++++++++++++++++-- .../tests/unit/models/test_taskinstance.py | 24 ++++++++ 4 files changed, 93 insertions(+), 18 deletions(-) diff --git a/airflow-core/src/airflow/api_fastapi/execution_api/routes/task_instances.py b/airflow-core/src/airflow/api_fastapi/execution_api/routes/task_instances.py index 3b142a4e6b1b7..b813c4c8f7e2e 100644 --- a/airflow-core/src/airflow/api_fastapi/execution_api/routes/task_instances.py +++ b/airflow-core/src/airflow/api_fastapi/execution_api/routes/task_instances.py @@ -643,21 +643,22 @@ def _create_ti_state_update_query_and_update_state( if ti is not None: _handle_fail_fast_for_dag(ti=ti, dag_id=dag_id, session=session, dag_bag=dag_bag) elif isinstance(ti_patch_payload, TIRetryStatePayload): + retry_delay_override = ti_patch_payload.retry_delay_seconds + retry_reason = ti_patch_payload.retry_reason[:500] if ti_patch_payload.retry_reason else None if ti is not None: - # Set the overrides on the TI *before* archiving so record_ti() - # snapshots them into task_instance_history (it copies attrs off - # the ti object). Otherwise the per-try audit trail is always NULL. - ti.retry_delay_override = ti_patch_payload.retry_delay_seconds - ti.retry_reason = ( - ti_patch_payload.retry_reason[:500] if ti_patch_payload.retry_reason else None - ) + # Snapshot the finished try onto the TI *before* archiving so record_ti() + # copies the values into task_instance_history (it reads attrs off the + # ti object and cannot see the live-row UPDATE built below). + ti.retry_delay_override = retry_delay_override + ti.retry_reason = retry_reason + ti.end_date = ti_patch_payload.end_date + ti.set_duration() + if "rendered_map_index" in ti_patch_payload.model_fields_set: + ti._rendered_map_index = ti_patch_payload.rendered_map_index ti.prepare_db_for_next_try(session=session) # Store retry policy overrides so next_retry_datetime() can read them. # These are cleared when the task enters RUNNING (ti_run). - query = query.values( - retry_delay_override=ti_patch_payload.retry_delay_seconds, - retry_reason=(ti_patch_payload.retry_reason[:500] if ti_patch_payload.retry_reason else None), - ) + query = query.values(retry_delay_override=retry_delay_override, retry_reason=retry_reason) elif isinstance(ti_patch_payload, TISuccessStatePayload): if ti is not None: TI.register_asset_changes_in_db( diff --git a/airflow-core/src/airflow/models/taskinstancehistory.py b/airflow-core/src/airflow/models/taskinstancehistory.py index b0d55114bb6b1..47cfdb68ae577 100644 --- a/airflow-core/src/airflow/models/taskinstancehistory.py +++ b/airflow-core/src/airflow/models/taskinstancehistory.py @@ -211,8 +211,11 @@ def record_ti(ti: TaskInstance, *, session: Session = NEW_SESSION) -> None: ti_history_state = ti.state if ti.state not in State.finished: ti_history_state = TaskInstanceState.FAILED - ti.end_date = timezone.utcnow() - ti.set_duration() + # Callers that know when the try actually ended (e.g. the Execution API + # retry path) pre-set end_date and duration; only stamp archive time when unset. + if ti.end_date is None: + ti.end_date = timezone.utcnow() + ti.set_duration() ti_history = TaskInstanceHistory(ti, state=ti_history_state) session.add(ti_history) diff --git a/airflow-core/tests/unit/api_fastapi/execution_api/versions/head/test_task_instances.py b/airflow-core/tests/unit/api_fastapi/execution_api/versions/head/test_task_instances.py index 4920fc001f9d8..da6ea74337df5 100644 --- a/airflow-core/tests/unit/api_fastapi/execution_api/versions/head/test_task_instances.py +++ b/airflow-core/tests/unit/api_fastapi/execution_api/versions/head/test_task_instances.py @@ -1891,17 +1891,17 @@ def test_ti_update_state_retry_with_policy_overrides(self, client, session, crea def test_ti_update_state_retry_policy_overrides_persisted_in_history( self, client, session, create_task_instance ): - """Retry policy override + reason must be archived to task_instance_history. + """The finished try's values must be archived to task_instance_history. - record_ti() snapshots columns off the TI object, so the overrides must be set on - the TI before prepare_db_for_next_try() archives it. When they were written only - to the live-row UPDATE, the per-try audit trail in task_instance_history was - always NULL even though the live row was correct. + record_ti() snapshots columns off the TI object, so the overrides, end_date, + and rendered_map_index must be set on the TI before prepare_db_for_next_try() + archives it; the live-row UPDATE is not visible to it. """ ti = create_task_instance( task_id="test_retry_policy_override_history", state=State.RUNNING, ) + ti.start_date = DEFAULT_START_DATE session.commit() response = client.patch( @@ -1909,6 +1909,7 @@ def test_ti_update_state_retry_policy_overrides_persisted_in_history( json={ "state": State.UP_FOR_RETRY, "end_date": DEFAULT_END_DATE.isoformat(), + "rendered_map_index": DEFAULT_RENDERED_MAP_INDEX, "retry_delay_seconds": 42.5, "retry_reason": "Rate limit: backing off", }, @@ -1925,6 +1926,52 @@ def test_ti_update_state_retry_policy_overrides_persisted_in_history( ).one() assert tih.retry_delay_override == 42.5 assert tih.retry_reason == "Rate limit: backing off" + assert tih.end_date == DEFAULT_END_DATE + assert tih.duration == (DEFAULT_END_DATE - DEFAULT_START_DATE).total_seconds() + assert tih.rendered_map_index == DEFAULT_RENDERED_MAP_INDEX + + def test_ti_update_state_retry_clears_rendered_map_index_in_history( + self, client, session, create_task_instance + ): + """An explicit ``rendered_map_index: null`` must clear the value archived to history too. + + The worker sends ``rendered_map_index`` on every retry, even when this try never + (re-)computed it (e.g. it failed before rendering). That must null out the archived + row along with the live one, not leave the history row snapshotting a stale value + left over from an earlier try. + """ + ti = create_task_instance( + task_id="test_retry_clears_rendered_map_index_history", + state=State.RUNNING, + ) + ti.start_date = DEFAULT_START_DATE + ti._rendered_map_index = DEFAULT_RENDERED_MAP_INDEX + session.commit() + + response = client.patch( + f"/execution/task-instances/{ti.id}/state", + json={ + "state": State.UP_FOR_RETRY, + "end_date": DEFAULT_END_DATE.isoformat(), + "rendered_map_index": None, + }, + ) + + assert response.status_code == 204 + + ti = session.scalars( + select(TaskInstance).filter_by(task_id=ti.task_id, run_id=ti.run_id, dag_id=ti.dag_id) + ).one() + assert ti.rendered_map_index is None + + tih = session.scalars( + select(TaskInstanceHistory).where( + TaskInstanceHistory.dag_id == ti.dag_id, + TaskInstanceHistory.task_id == ti.task_id, + TaskInstanceHistory.run_id == ti.run_id, + ) + ).one() + assert tih.rendered_map_index is None def test_ti_update_state_retry_without_policy_overrides(self, client, session, create_task_instance): """Without retry policy fields, the columns remain NULL.""" diff --git a/airflow-core/tests/unit/models/test_taskinstance.py b/airflow-core/tests/unit/models/test_taskinstance.py index 89ac6b12048e3..1221a96d9f85a 100644 --- a/airflow-core/tests/unit/models/test_taskinstance.py +++ b/airflow-core/tests/unit/models/test_taskinstance.py @@ -2680,6 +2680,30 @@ def test_task_instance_history_is_created_when_ti_goes_for_retry(self, dag_maker # the new try_id should be different from what's recorded in tih assert tih[0].task_instance_id == try_id + def test_record_ti_stamps_end_date_when_unset_for_non_finished_state(self, dag_maker, session): + """record_ti() must fill in end_date/duration when archiving a non-finished TI with end_date=None.""" + archive_time = pendulum.datetime(2024, 6, 15, 12, 0, 0, tz="UTC") + start = pendulum.datetime(2024, 6, 15, 11, 50, 0, tz="UTC") + + with dag_maker(serialized=True): + EmptyOperator(task_id="test_record_ti_fallback") + + dr = dag_maker.create_dagrun() + ti = dr.task_instances[0] + ti.state = TaskInstanceState.RUNNING + ti.start_date = start + ti.end_date = None + session.flush() + + with time_machine.travel(archive_time, tick=False): + TaskInstanceHistory.record_ti(ti, session=session) + session.flush() + + tih = session.scalars(select(TaskInstanceHistory)).one() + assert tih.state == str(TaskInstanceState.FAILED) + assert tih.end_date == archive_time + assert tih.duration == (archive_time - start).total_seconds() + @pytest.mark.parametrize( ("first_ti", "second_ti"), [ From 16ea81cdf2325735bc524258a6c2fba72a777d8d Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 6 Jul 2026 21:59:15 +0200 Subject: [PATCH 036/297] Bump the 3-3-core-ui-package-updates group across 1 directory with 47 updates (#69425) * Bump the 3-3-core-ui-package-updates group across 1 directory with 47 updates Bumps the 3-3-core-ui-package-updates group with 47 updates in the /airflow-core/src/airflow/ui directory: | Package | From | To | | --- | --- | --- | | [@chakra-ui/react](https://github.com/chakra-ui/chakra-ui/tree/HEAD/packages/react) | `3.34.0` | `3.36.0` | | [@guanmingchiu/sqlparser-ts](https://github.com/guan404ming/sqlparser-ts) | `0.61.1` | `0.62.0` | | [@tanstack/react-query](https://github.com/TanStack/query/tree/HEAD/packages/react-query) | `5.90.21` | `5.101.2` | | [@tanstack/react-virtual](https://github.com/TanStack/virtual/tree/HEAD/packages/react-virtual) | `3.13.21` | `3.14.5` | | [@xyflow/react](https://github.com/xyflow/xyflow/tree/HEAD/packages/react) | `12.10.1` | `12.11.1` | | [axios](https://github.com/axios/axios) | `1.16.1` | `1.18.1` | | [chakra-react-select](https://github.com/csandman/chakra-react-select) | `6.1.1` | `6.1.3` | | [dayjs](https://github.com/iamkun/dayjs) | `1.11.19` | `1.11.21` | | [i18next](https://github.com/i18next/i18next) | `25.8.16` | `25.10.10` | | [i18next-http-backend](https://github.com/i18next/i18next-http-backend) | `3.0.5` | `3.0.6` | | [monaco-editor](https://github.com/microsoft/monaco-editor) | `0.52.2` | `0.55.1` | | [react](https://github.com/facebook/react/tree/HEAD/packages/react) | `19.2.6` | `19.2.7` | | [@types/react](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/react) | `19.2.15` | `19.2.17` | | [react-dom](https://github.com/facebook/react/tree/HEAD/packages/react-dom) | `19.2.6` | `19.2.7` | | [react-hook-form](https://github.com/react-hook-form/react-hook-form) | `7.71.2` | `7.80.0` | | [react-hotkeys-hook](https://github.com/JohannesKlauss/react-keymap-hook) | `4.6.1` | `4.6.2` | | [react-i18next](https://github.com/i18next/react-i18next) | `16.6.5` | `16.6.6` | | [react-icons](https://github.com/react-icons/react-icons) | `5.6.0` | `5.7.0` | | [react-router-dom](https://github.com/remix-run/react-router/tree/HEAD/packages/react-router-dom) | `7.13.1` | `7.18.1` | | [react-syntax-highlighter](https://github.com/react-syntax-highlighter/react-syntax-highlighter) | `15.6.1` | `15.6.6` | | [use-debounce](https://github.com/xnimorz/use-debounce) | `10.1.0` | `10.1.1` | | [yaml](https://github.com/eemeli/yaml) | `2.8.3` | `2.9.0` | | [zustand](https://github.com/pmndrs/zustand) | `5.0.11` | `5.0.14` | | [@eslint/compat](https://github.com/eslint/rewrite/tree/HEAD/packages/compat) | `2.0.5` | `2.1.0` | | [@playwright/test](https://github.com/microsoft/playwright) | `1.60.0` | `1.61.1` | | [@rolldown/plugin-babel](https://github.com/rolldown/plugins/tree/HEAD/packages/babel) | `0.2.2` | `0.2.3` | | [@tanstack/eslint-plugin-query](https://github.com/TanStack/query/tree/HEAD/packages/eslint-plugin-query) | `5.91.4` | `5.101.2` | | [@types/node](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/node) | `24.10.3` | `24.13.2` | | [@typescript-eslint/eslint-plugin](https://github.com/typescript-eslint/typescript-eslint/tree/HEAD/packages/eslint-plugin) | `8.60.0` | `8.62.1` | | [@typescript-eslint/parser](https://github.com/typescript-eslint/typescript-eslint/tree/HEAD/packages/parser) | `8.60.0` | `8.62.1` | | [@typescript-eslint/utils](https://github.com/typescript-eslint/typescript-eslint/tree/HEAD/packages/utils) | `8.60.0` | `8.62.1` | | [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/tree/HEAD/packages/plugin-react) | `6.0.1` | `6.0.3` | | [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react/tree/HEAD/packages/plugin-react-swc) | `4.2.3` | `4.3.1` | | [@vitest/coverage-v8](https://github.com/vitest-dev/vitest/tree/HEAD/packages/coverage-v8) | `4.1.4` | `4.1.9` | | [eslint](https://github.com/eslint/eslint) | `10.3.0` | `10.6.0` | | [eslint-plugin-i18next](https://github.com/edvardchen/eslint-plugin-i18next) | `6.1.4` | `6.1.5` | | [eslint-plugin-jsonc](https://github.com/ota-meshi/eslint-plugin-jsonc) | `3.1.2` | `3.2.0` | | [eslint-plugin-perfectionist](https://github.com/azat-io/eslint-plugin-perfectionist) | `5.9.0` | `5.9.1` | | [eslint-plugin-prettier](https://github.com/prettier/eslint-plugin-prettier) | `5.5.5` | `5.5.6` | | [eslint-plugin-react-refresh](https://github.com/ArnaudBarre/eslint-plugin-react-refresh) | `0.5.2` | `0.5.3` | | [happy-dom](https://github.com/capricorn86/happy-dom) | `20.8.9` | `20.10.6` | | [jsonc-eslint-parser](https://github.com/ota-meshi/jsonc-eslint-parser) | `2.4.1` | `2.4.2` | | [msw](https://github.com/mswjs/msw) | `2.12.10` | `2.14.6` | | [prettier](https://github.com/prettier/prettier) | `3.8.1` | `3.9.4` | | [typescript-eslint](https://github.com/typescript-eslint/typescript-eslint/tree/HEAD/packages/typescript-eslint) | `8.60.0` | `8.62.1` | | [vite](https://github.com/vitejs/vite/tree/HEAD/packages/vite) | `8.0.16` | `8.1.2` | | [vitest](https://github.com/vitest-dev/vitest/tree/HEAD/packages/vitest) | `4.1.4` | `4.1.9` | Updates `@chakra-ui/react` from 3.34.0 to 3.36.0 - [Release notes](https://github.com/chakra-ui/chakra-ui/releases) - [Changelog](https://github.com/chakra-ui/chakra-ui/blob/main/packages/react/CHANGELOG.md) - [Commits](https://github.com/chakra-ui/chakra-ui/commits/@chakra-ui/react@3.36.0/packages/react) Updates `@guanmingchiu/sqlparser-ts` from 0.61.1 to 0.62.0 - [Release notes](https://github.com/guan404ming/sqlparser-ts/releases) - [Commits](https://github.com/guan404ming/sqlparser-ts/compare/v0.61.1...v0.62.0) Updates `@tanstack/react-query` from 5.90.21 to 5.101.2 - [Release notes](https://github.com/TanStack/query/releases) - [Changelog](https://github.com/TanStack/query/blob/main/packages/react-query/CHANGELOG.md) - [Commits](https://github.com/TanStack/query/commits/@tanstack/react-query@5.101.2/packages/react-query) Updates `@tanstack/react-virtual` from 3.13.21 to 3.14.5 - [Release notes](https://github.com/TanStack/virtual/releases) - [Changelog](https://github.com/TanStack/virtual/blob/main/packages/react-virtual/CHANGELOG.md) - [Commits](https://github.com/TanStack/virtual/commits/@tanstack/react-virtual@3.14.5/packages/react-virtual) Updates `@xyflow/react` from 12.10.1 to 12.11.1 - [Release notes](https://github.com/xyflow/xyflow/releases) - [Changelog](https://github.com/xyflow/xyflow/blob/main/packages/react/CHANGELOG.md) - [Commits](https://github.com/xyflow/xyflow/commits/@xyflow/react@12.11.1/packages/react) Updates `axios` from 1.16.1 to 1.18.1 - [Release notes](https://github.com/axios/axios/releases) - [Changelog](https://github.com/axios/axios/blob/v1.x/CHANGELOG.md) - [Commits](https://github.com/axios/axios/compare/v1.16.1...v1.18.1) Updates `chakra-react-select` from 6.1.1 to 6.1.3 - [Release notes](https://github.com/csandman/chakra-react-select/releases) - [Commits](https://github.com/csandman/chakra-react-select/compare/v6.1.1...v6.1.3) Updates `dayjs` from 1.11.19 to 1.11.21 - [Release notes](https://github.com/iamkun/dayjs/releases) - [Changelog](https://github.com/iamkun/dayjs/blob/dev/CHANGELOG.md) - [Commits](https://github.com/iamkun/dayjs/compare/v1.11.19...v1.11.21) Updates `i18next` from 25.8.16 to 25.10.10 - [Release notes](https://github.com/i18next/i18next/releases) - [Changelog](https://github.com/i18next/i18next/blob/master/CHANGELOG.md) - [Commits](https://github.com/i18next/i18next/compare/v25.8.16...v25.10.10) Updates `i18next-http-backend` from 3.0.5 to 3.0.6 - [Changelog](https://github.com/i18next/i18next-http-backend/blob/master/CHANGELOG.md) - [Commits](https://github.com/i18next/i18next-http-backend/compare/v3.0.5...v3.0.6) Updates `monaco-editor` from 0.52.2 to 0.55.1 - [Release notes](https://github.com/microsoft/monaco-editor/releases) - [Changelog](https://github.com/microsoft/monaco-editor/blob/main/CHANGELOG.md) - [Commits](https://github.com/microsoft/monaco-editor/compare/v0.52.2...v0.55.1) Updates `react` from 19.2.6 to 19.2.7 - [Release notes](https://github.com/facebook/react/releases) - [Changelog](https://github.com/react/react/blob/main/CHANGELOG.md) - [Commits](https://github.com/facebook/react/commits/v19.2.7/packages/react) Updates `@types/react` from 19.2.15 to 19.2.17 - [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases) - [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/react) Updates `react-dom` from 19.2.6 to 19.2.7 - [Release notes](https://github.com/facebook/react/releases) - [Changelog](https://github.com/react/react/blob/main/CHANGELOG.md) - [Commits](https://github.com/facebook/react/commits/v19.2.7/packages/react-dom) Updates `react-hook-form` from 7.71.2 to 7.80.0 - [Release notes](https://github.com/react-hook-form/react-hook-form/releases) - [Changelog](https://github.com/react-hook-form/react-hook-form/blob/master/CHANGELOG.md) - [Commits](https://github.com/react-hook-form/react-hook-form/compare/v7.71.2...v7.80.0) Updates `react-hotkeys-hook` from 4.6.1 to 4.6.2 - [Release notes](https://github.com/JohannesKlauss/react-keymap-hook/releases) - [Changelog](https://github.com/JohannesKlauss/react-hotkeys-hook/blob/main/CHANGELOG.md) - [Commits](https://github.com/JohannesKlauss/react-keymap-hook/compare/v4.6.1...v4.6.2) Updates `react-i18next` from 16.6.5 to 16.6.6 - [Changelog](https://github.com/i18next/react-i18next/blob/master/CHANGELOG.md) - [Commits](https://github.com/i18next/react-i18next/compare/v16.6.5...v16.6.6) Updates `react-icons` from 5.6.0 to 5.7.0 - [Release notes](https://github.com/react-icons/react-icons/releases) - [Commits](https://github.com/react-icons/react-icons/compare/v5.6.0...v5.7.0) Updates `react-router-dom` from 7.13.1 to 7.18.1 - [Release notes](https://github.com/remix-run/react-router/releases) - [Changelog](https://github.com/remix-run/react-router/blob/react-router-dom@7.18.1/packages/react-router-dom/CHANGELOG.md) - [Commits](https://github.com/remix-run/react-router/commits/react-router-dom@7.18.1/packages/react-router-dom) Updates `react-syntax-highlighter` from 15.6.1 to 15.6.6 - [Release notes](https://github.com/react-syntax-highlighter/react-syntax-highlighter/releases) - [Changelog](https://github.com/react-syntax-highlighter/react-syntax-highlighter/blob/master/CHANGELOG.MD) - [Commits](https://github.com/react-syntax-highlighter/react-syntax-highlighter/compare/v15.6.1...v15.6.6) Updates `use-debounce` from 10.1.0 to 10.1.1 - [Release notes](https://github.com/xnimorz/use-debounce/releases) - [Changelog](https://github.com/xnimorz/use-debounce/blob/master/CHANGELOG.md) - [Commits](https://github.com/xnimorz/use-debounce/commits) Updates `yaml` from 2.8.3 to 2.9.0 - [Release notes](https://github.com/eemeli/yaml/releases) - [Commits](https://github.com/eemeli/yaml/compare/v2.8.3...v2.9.0) Updates `zustand` from 5.0.11 to 5.0.14 - [Release notes](https://github.com/pmndrs/zustand/releases) - [Commits](https://github.com/pmndrs/zustand/compare/v5.0.11...v5.0.14) Updates `@eslint/compat` from 2.0.5 to 2.1.0 - [Release notes](https://github.com/eslint/rewrite/releases) - [Changelog](https://github.com/eslint/rewrite/blob/main/packages/compat/CHANGELOG.md) - [Commits](https://github.com/eslint/rewrite/commits/compat-v2.1.0/packages/compat) Updates `@playwright/test` from 1.60.0 to 1.61.1 - [Release notes](https://github.com/microsoft/playwright/releases) - [Commits](https://github.com/microsoft/playwright/compare/v1.60.0...v1.61.1) Updates `@rolldown/plugin-babel` from 0.2.2 to 0.2.3 - [Release notes](https://github.com/rolldown/plugins/releases) - [Changelog](https://github.com/rolldown/plugins/blob/main/packages/babel/CHANGELOG.md) - [Commits](https://github.com/rolldown/plugins/commits/plugin-babel@0.2.3/packages/babel) Updates `@tanstack/eslint-plugin-query` from 5.91.4 to 5.101.2 - [Release notes](https://github.com/TanStack/query/releases) - [Changelog](https://github.com/TanStack/query/blob/main/packages/eslint-plugin-query/CHANGELOG.md) - [Commits](https://github.com/TanStack/query/commits/@tanstack/eslint-plugin-query@5.101.2/packages/eslint-plugin-query) Updates `@types/node` from 24.10.3 to 24.13.2 - [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases) - [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/node) Updates `@types/react` from 19.2.15 to 19.2.17 - [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases) - [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/react) Updates `@typescript-eslint/eslint-plugin` from 8.60.0 to 8.62.1 - [Release notes](https://github.com/typescript-eslint/typescript-eslint/releases) - [Changelog](https://github.com/typescript-eslint/typescript-eslint/blob/main/packages/eslint-plugin/CHANGELOG.md) - [Commits](https://github.com/typescript-eslint/typescript-eslint/commits/v8.62.1/packages/eslint-plugin) Updates `@typescript-eslint/parser` from 8.60.0 to 8.62.1 - [Release notes](https://github.com/typescript-eslint/typescript-eslint/releases) - [Changelog](https://github.com/typescript-eslint/typescript-eslint/blob/main/packages/parser/CHANGELOG.md) - [Commits](https://github.com/typescript-eslint/typescript-eslint/commits/v8.62.1/packages/parser) Updates `@typescript-eslint/utils` from 8.60.0 to 8.62.1 - [Release notes](https://github.com/typescript-eslint/typescript-eslint/releases) - [Changelog](https://github.com/typescript-eslint/typescript-eslint/blob/main/packages/utils/CHANGELOG.md) - [Commits](https://github.com/typescript-eslint/typescript-eslint/commits/v8.62.1/packages/utils) Updates `@vitejs/plugin-react` from 6.0.1 to 6.0.3 - [Release notes](https://github.com/vitejs/vite-plugin-react/releases) - [Changelog](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react/CHANGELOG.md) - [Commits](https://github.com/vitejs/vite-plugin-react/commits/plugin-react@6.0.3/packages/plugin-react) Updates `@vitejs/plugin-react-swc` from 4.2.3 to 4.3.1 - [Release notes](https://github.com/vitejs/vite-plugin-react/releases) - [Changelog](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react-swc/CHANGELOG.md) - [Commits](https://github.com/vitejs/vite-plugin-react/commits/v4.3.1/packages/plugin-react-swc) Updates `@vitest/coverage-v8` from 4.1.4 to 4.1.9 - [Release notes](https://github.com/vitest-dev/vitest/releases) - [Changelog](https://github.com/vitest-dev/vitest/blob/main/docs/releases.md) - [Commits](https://github.com/vitest-dev/vitest/commits/v4.1.9/packages/coverage-v8) Updates `eslint` from 10.3.0 to 10.6.0 - [Release notes](https://github.com/eslint/eslint/releases) - [Commits](https://github.com/eslint/eslint/compare/v10.3.0...v10.6.0) Updates `eslint-plugin-i18next` from 6.1.4 to 6.1.5 - [Changelog](https://github.com/edvardchen/eslint-plugin-i18next/blob/main/CHANGELOG.md) - [Commits](https://github.com/edvardchen/eslint-plugin-i18next/compare/v6.1.4...v6.1.5) Updates `eslint-plugin-jsonc` from 3.1.2 to 3.2.0 - [Release notes](https://github.com/ota-meshi/eslint-plugin-jsonc/releases) - [Changelog](https://github.com/ota-meshi/eslint-plugin-jsonc/blob/master/CHANGELOG.md) - [Commits](https://github.com/ota-meshi/eslint-plugin-jsonc/compare/v3.1.2...v3.2.0) Updates `eslint-plugin-perfectionist` from 5.9.0 to 5.9.1 - [Release notes](https://github.com/azat-io/eslint-plugin-perfectionist/releases) - [Changelog](https://github.com/azat-io/eslint-plugin-perfectionist/blob/main/changelog.md) - [Commits](https://github.com/azat-io/eslint-plugin-perfectionist/compare/v5.9.0...v5.9.1) Updates `eslint-plugin-prettier` from 5.5.5 to 5.5.6 - [Release notes](https://github.com/prettier/eslint-plugin-prettier/releases) - [Changelog](https://github.com/prettier/eslint-plugin-prettier/blob/main/CHANGELOG.md) - [Commits](https://github.com/prettier/eslint-plugin-prettier/compare/v5.5.5...v5.5.6) Updates `eslint-plugin-react-refresh` from 0.5.2 to 0.5.3 - [Release notes](https://github.com/ArnaudBarre/eslint-plugin-react-refresh/releases) - [Changelog](https://github.com/ArnaudBarre/eslint-plugin-react-refresh/blob/main/CHANGELOG.md) - [Commits](https://github.com/ArnaudBarre/eslint-plugin-react-refresh/compare/v0.5.2...v0.5.3) Updates `happy-dom` from 20.8.9 to 20.10.6 - [Release notes](https://github.com/capricorn86/happy-dom/releases) - [Commits](https://github.com/capricorn86/happy-dom/compare/v20.8.9...v20.10.6) Updates `jsonc-eslint-parser` from 2.4.1 to 2.4.2 - [Release notes](https://github.com/ota-meshi/jsonc-eslint-parser/releases) - [Changelog](https://github.com/ota-meshi/jsonc-eslint-parser/blob/master/CHANGELOG.md) - [Commits](https://github.com/ota-meshi/jsonc-eslint-parser/compare/v2.4.1...v2.4.2) Updates `msw` from 2.12.10 to 2.14.6 - [Release notes](https://github.com/mswjs/msw/releases) - [Changelog](https://github.com/mswjs/msw/blob/main/CHANGELOG.md) - [Commits](https://github.com/mswjs/msw/compare/v2.12.10...v2.14.6) Updates `prettier` from 3.8.1 to 3.9.4 - [Release notes](https://github.com/prettier/prettier/releases) - [Changelog](https://github.com/prettier/prettier/blob/main/CHANGELOG.md) - [Commits](https://github.com/prettier/prettier/compare/3.8.1...3.9.4) Updates `typescript-eslint` from 8.60.0 to 8.62.1 - [Release notes](https://github.com/typescript-eslint/typescript-eslint/releases) - [Changelog](https://github.com/typescript-eslint/typescript-eslint/blob/main/packages/typescript-eslint/CHANGELOG.md) - [Commits](https://github.com/typescript-eslint/typescript-eslint/commits/v8.62.1/packages/typescript-eslint) Updates `vite` from 8.0.16 to 8.1.2 - [Release notes](https://github.com/vitejs/vite/releases) - [Changelog](https://github.com/vitejs/vite/blob/main/packages/vite/CHANGELOG.md) - [Commits](https://github.com/vitejs/vite/commits/v8.1.2/packages/vite) Updates `vitest` from 4.1.4 to 4.1.9 - [Release notes](https://github.com/vitest-dev/vitest/releases) - [Changelog](https://github.com/vitest-dev/vitest/blob/main/docs/releases.md) - [Commits](https://github.com/vitest-dev/vitest/commits/v4.1.9/packages/vitest) --- updated-dependencies: - dependency-name: "@chakra-ui/react" dependency-version: 3.36.0 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: 3-3-core-ui-package-updates - dependency-name: "@guanmingchiu/sqlparser-ts" dependency-version: 0.62.0 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: 3-3-core-ui-package-updates - dependency-name: "@tanstack/react-query" dependency-version: 5.101.2 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: 3-3-core-ui-package-updates - dependency-name: "@tanstack/react-virtual" dependency-version: 3.14.5 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: 3-3-core-ui-package-updates - dependency-name: "@xyflow/react" dependency-version: 12.11.1 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: 3-3-core-ui-package-updates - dependency-name: axios dependency-version: 1.18.1 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: 3-3-core-ui-package-updates - dependency-name: chakra-react-select dependency-version: 6.1.3 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: 3-3-core-ui-package-updates - dependency-name: dayjs dependency-version: 1.11.21 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: 3-3-core-ui-package-updates - dependency-name: i18next dependency-version: 25.10.10 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: 3-3-core-ui-package-updates - dependency-name: i18next-http-backend dependency-version: 3.0.6 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: 3-3-core-ui-package-updates - dependency-name: monaco-editor dependency-version: 0.55.1 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: 3-3-core-ui-package-updates - dependency-name: react dependency-version: 19.2.7 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: 3-3-core-ui-package-updates - dependency-name: "@types/react" dependency-version: 19.2.17 dependency-type: direct:development update-type: version-update:semver-patch dependency-group: 3-3-core-ui-package-updates - dependency-name: react-dom dependency-version: 19.2.7 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: 3-3-core-ui-package-updates - dependency-name: react-hook-form dependency-version: 7.80.0 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: 3-3-core-ui-package-updates - dependency-name: react-hotkeys-hook dependency-version: 4.6.2 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: 3-3-core-ui-package-updates - dependency-name: react-i18next dependency-version: 16.6.6 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: 3-3-core-ui-package-updates - dependency-name: react-icons dependency-version: 5.7.0 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: 3-3-core-ui-package-updates - dependency-name: react-router-dom dependency-version: 7.18.1 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: 3-3-core-ui-package-updates - dependency-name: react-syntax-highlighter dependency-version: 15.6.6 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: 3-3-core-ui-package-updates - dependency-name: use-debounce dependency-version: 10.1.1 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: 3-3-core-ui-package-updates - dependency-name: yaml dependency-version: 2.9.0 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: 3-3-core-ui-package-updates - dependency-name: zustand dependency-version: 5.0.14 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: 3-3-core-ui-package-updates - dependency-name: "@eslint/compat" dependency-version: 2.1.0 dependency-type: direct:development update-type: version-update:semver-minor dependency-group: 3-3-core-ui-package-updates - dependency-name: "@playwright/test" dependency-version: 1.61.1 dependency-type: direct:development update-type: version-update:semver-minor dependency-group: 3-3-core-ui-package-updates - dependency-name: "@rolldown/plugin-babel" dependency-version: 0.2.3 dependency-type: direct:development update-type: version-update:semver-patch dependency-group: 3-3-core-ui-package-updates - dependency-name: "@tanstack/eslint-plugin-query" dependency-version: 5.101.2 dependency-type: direct:development update-type: version-update:semver-minor dependency-group: 3-3-core-ui-package-updates - dependency-name: "@types/node" dependency-version: 24.13.2 dependency-type: direct:development update-type: version-update:semver-minor dependency-group: 3-3-core-ui-package-updates - dependency-name: "@types/react" dependency-version: 19.2.17 dependency-type: direct:development update-type: version-update:semver-patch dependency-group: 3-3-core-ui-package-updates - dependency-name: "@typescript-eslint/eslint-plugin" dependency-version: 8.62.1 dependency-type: direct:development update-type: version-update:semver-minor dependency-group: 3-3-core-ui-package-updates - dependency-name: "@typescript-eslint/parser" dependency-version: 8.62.1 dependency-type: direct:development update-type: version-update:semver-minor dependency-group: 3-3-core-ui-package-updates - dependency-name: "@typescript-eslint/utils" dependency-version: 8.62.1 dependency-type: direct:development update-type: version-update:semver-minor dependency-group: 3-3-core-ui-package-updates - dependency-name: "@vitejs/plugin-react" dependency-version: 6.0.3 dependency-type: direct:development update-type: version-update:semver-patch dependency-group: 3-3-core-ui-package-updates - dependency-name: "@vitejs/plugin-react-swc" dependency-version: 4.3.1 dependency-type: direct:development update-type: version-update:semver-minor dependency-group: 3-3-core-ui-package-updates - dependency-name: "@vitest/coverage-v8" dependency-version: 4.1.9 dependency-type: direct:development update-type: version-update:semver-patch dependency-group: 3-3-core-ui-package-updates - dependency-name: eslint dependency-version: 10.6.0 dependency-type: direct:development update-type: version-update:semver-minor dependency-group: 3-3-core-ui-package-updates - dependency-name: eslint-plugin-i18next dependency-version: 6.1.5 dependency-type: direct:development update-type: version-update:semver-patch dependency-group: 3-3-core-ui-package-updates - dependency-name: eslint-plugin-jsonc dependency-version: 3.2.0 dependency-type: direct:development update-type: version-update:semver-minor dependency-group: 3-3-core-ui-package-updates - dependency-name: eslint-plugin-perfectionist dependency-version: 5.9.1 dependency-type: direct:development update-type: version-update:semver-patch dependency-group: 3-3-core-ui-package-updates - dependency-name: eslint-plugin-prettier dependency-version: 5.5.6 dependency-type: direct:development update-type: version-update:semver-patch dependency-group: 3-3-core-ui-package-updates - dependency-name: eslint-plugin-react-refresh dependency-version: 0.5.3 dependency-type: direct:development update-type: version-update:semver-patch dependency-group: 3-3-core-ui-package-updates - dependency-name: happy-dom dependency-version: 20.10.6 dependency-type: direct:development update-type: version-update:semver-minor dependency-group: 3-3-core-ui-package-updates - dependency-name: jsonc-eslint-parser dependency-version: 2.4.2 dependency-type: direct:development update-type: version-update:semver-patch dependency-group: 3-3-core-ui-package-updates - dependency-name: msw dependency-version: 2.14.6 dependency-type: direct:development update-type: version-update:semver-minor dependency-group: 3-3-core-ui-package-updates - dependency-name: prettier dependency-version: 3.9.4 dependency-type: direct:development update-type: version-update:semver-minor dependency-group: 3-3-core-ui-package-updates - dependency-name: typescript-eslint dependency-version: 8.62.1 dependency-type: direct:development update-type: version-update:semver-minor dependency-group: 3-3-core-ui-package-updates - dependency-name: vite dependency-version: 8.1.2 dependency-type: direct:development update-type: version-update:semver-minor dependency-group: 3-3-core-ui-package-updates - dependency-name: vitest dependency-version: 4.1.9 dependency-type: direct:development update-type: version-update:semver-patch dependency-group: 3-3-core-ui-package-updates ... Signed-off-by: dependabot[bot] * UI: Adapt to monaco-editor 0.55, msw 2.14, and stricter eslint rules monaco-editor 0.55 added an `exports` map, so deep ESM subpath imports must carry the `.js` extension to resolve; add it to the side-effect and API imports, and derive the `Monaco` type from `useMonaco`'s return since @monaco-editor/react's bundled type now collapses to `any`. msw 2.14 returns `SetupServer` from `setupServer`. The eslint-plugin bumps in this group also flag pre-existing code: extract a named callback to satisfy max-nested-callbacks, and move the file-parsing status string into i18n to satisfy i18next/no-literal-string. --------- Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Jarek Potiuk --- airflow-core/src/airflow/ui/package.json | 94 +- airflow-core/src/airflow/ui/pnpm-lock.yaml | 3376 +++++++++-------- .../ui/public/i18n/locales/en/admin.json | 1 + .../MonacoEditor/configureMonaco.ts | 8 +- .../MonacoEditor/pythonFStrings.test.ts | 2 +- .../ui/src/components/TaskInstanceTooltip.tsx | 4 +- .../context/colorMode/useMonacoTheme.test.ts | 3 +- .../src/context/colorMode/useMonacoTheme.ts | 8 +- .../ui/src/pages/Dag/Calendar/Calendar.tsx | 6 +- .../ui/src/pages/Dag/DagHeader.test.tsx | 4 +- .../src/airflow/ui/src/pages/ReactPlugin.tsx | 3 +- .../pages/Variables/ImportVariablesForm.tsx | 3 +- .../ui/src/queries/useGridTISummaries.ts | 14 +- airflow-core/src/airflow/ui/testsSetup.ts | 4 +- 14 files changed, 1772 insertions(+), 1758 deletions(-) diff --git a/airflow-core/src/airflow/ui/package.json b/airflow-core/src/airflow/ui/package.json index 611013f07fb3b..b31305173fb2f 100644 --- a/airflow-core/src/airflow/ui/package.json +++ b/airflow-core/src/airflow/ui/package.json @@ -26,96 +26,96 @@ }, "dependencies": { "@chakra-ui/anatomy": "^2.3.4", - "@chakra-ui/react": "~3.34.0", + "@chakra-ui/react": "~3.36.0", "@emotion/react": "^11.14.0", - "@guanmingchiu/sqlparser-ts": "^0.61.1", + "@guanmingchiu/sqlparser-ts": "^0.62.0", "@lezer/highlight": "^1.2.3", "@monaco-editor/react": "^4.7.0", - "@tanstack/react-query": "^5.90.21", + "@tanstack/react-query": "^5.101.2", "@tanstack/react-table": "^8.21.3", - "@tanstack/react-virtual": "^3.13.21", + "@tanstack/react-virtual": "^3.14.5", "@visx/group": "^3.12.0", "@visx/shape": "^3.12.0", - "@xyflow/react": "^12.10.1", + "@xyflow/react": "^12.11.1", "anser": "^2.3.5", - "axios": "^1.16.0", - "chakra-react-select": "^6.1.1", + "axios": "^1.18.1", + "chakra-react-select": "^6.1.3", "chart.js": "^4.5.1", "chartjs-adapter-dayjs-4": "^1.0.4", "chartjs-plugin-annotation": "^3.1.0", "culori": "^4.0.2", - "dayjs": "^1.11.19", + "dayjs": "^1.11.21", "elkjs": "^0.11.1", "html-to-image": "^1.11.13", - "i18next": "^25.8.16", + "i18next": "^25.10.10", "i18next-browser-languagedetector": "^8.2.1", - "i18next-http-backend": "^3.0.5", - "monaco-editor": "^0.52.2", + "i18next-http-backend": "^3.0.6", + "monaco-editor": "^0.55.1", "next-themes": "^0.4.6", - "react": "^19.2.6", + "react": "^19.2.7", "react-chartjs-2": "^5.3.1", - "react-dom": "^19.2.6", - "react-hook-form": "^7.71.2", - "react-hotkeys-hook": "^4.6.1", - "react-i18next": "^16.6.5", - "react-icons": "^5.6.0", + "react-dom": "^19.2.7", + "react-hook-form": "^7.80.0", + "react-hotkeys-hook": "^4.6.2", + "react-i18next": "^16.6.6", + "react-icons": "^5.7.0", "react-innertext": "^1.1.5", "react-markdown": "^9.1.0", "react-resizable-panels": "^3.0.6", - "react-router-dom": "^7.13.1", - "react-syntax-highlighter": "^15.6.1", + "react-router-dom": "^7.18.1", + "react-syntax-highlighter": "^15.6.6", "remark-gfm": "^4.0.1", - "use-debounce": "^10.1.0", + "use-debounce": "^10.1.1", "usehooks-ts": "^3.1.1", - "yaml": "^2.8.2", - "zustand": "^5.0.11" + "yaml": "^2.9.0", + "zustand": "^5.0.14" }, "devDependencies": { "@7nohe/openapi-react-query-codegen": "^1.6.2", - "@eslint/compat": "^2.0.5", + "@eslint/compat": "^2.1.0", "@eslint/js": "^10.0.1", - "@playwright/test": "^1.60.0", - "@rolldown/plugin-babel": "^0.2.2", + "@playwright/test": "^1.61.1", + "@rolldown/plugin-babel": "^0.2.3", "@stylistic/eslint-plugin": "^2.13.0", - "@tanstack/eslint-plugin-query": "^5.91.4", + "@tanstack/eslint-plugin-query": "^5.101.2", "@testing-library/jest-dom": "^6.9.1", "@testing-library/react": "^16.3.2", "@trivago/prettier-plugin-sort-imports": "^4.3.0", "@types/culori": "^4.0.1", - "@types/node": "^24.10.1", - "@types/react": "^19.2.15", + "@types/node": "^24.13.2", + "@types/react": "^19.2.17", "@types/react-dom": "^19.2.3", "@types/react-syntax-highlighter": "^15.5.13", - "@typescript-eslint/eslint-plugin": "^8.60.0", - "@typescript-eslint/parser": "^8.60.0", - "@typescript-eslint/utils": "^8.60.0", - "@vitejs/plugin-react": "^6.0.1", - "@vitejs/plugin-react-swc": "^4.2.3", - "@vitest/coverage-v8": "^4.1.4", + "@typescript-eslint/eslint-plugin": "^8.62.1", + "@typescript-eslint/parser": "^8.62.1", + "@typescript-eslint/utils": "^8.62.1", + "@vitejs/plugin-react": "^6.0.3", + "@vitejs/plugin-react-swc": "^4.3.1", + "@vitest/coverage-v8": "^4.1.9", "babel-plugin-react-compiler": "^1.0.0", - "eslint": "^10.3.0", + "eslint": "^10.6.0", "eslint-config-prettier": "^10.1.8", - "eslint-plugin-i18next": "^6.1.4", - "eslint-plugin-jsonc": "^3.1.2", + "eslint-plugin-i18next": "^6.1.5", + "eslint-plugin-jsonc": "^3.2.0", "eslint-plugin-jsx-a11y": "^6.10.2", - "eslint-plugin-perfectionist": "^5.9.0", - "eslint-plugin-prettier": "^5.5.5", + "eslint-plugin-perfectionist": "^5.9.1", + "eslint-plugin-prettier": "^5.5.6", "eslint-plugin-react": "^7.37.5", "eslint-plugin-react-hooks": "^7.1.1", - "eslint-plugin-react-refresh": "^0.5.2", + "eslint-plugin-react-refresh": "^0.5.3", "eslint-plugin-unicorn": "^64.0.0", "globals": "^15.15.0", - "happy-dom": "^20.8.3", - "jsonc-eslint-parser": "^2.4.0", - "msw": "^2.12.10", + "happy-dom": "^20.10.6", + "jsonc-eslint-parser": "^2.4.2", + "msw": "^2.14.6", "openapi-merge-cli": "^1.3.2", - "prettier": "^3.8.1", + "prettier": "^3.9.4", "ts-morph": "^27.0.2", "typescript": "^6.0.3", - "typescript-eslint": "^8.60.0", - "vite": "^8.0.16", + "typescript-eslint": "^8.62.1", + "vite": "^8.1.2", "vite-plugin-css-injected-by-js": "^3.5.2", - "vitest": "^4.1.4", + "vitest": "^4.1.9", "web-worker": "^1.5.0" }, "pnpm": { diff --git a/airflow-core/src/airflow/ui/pnpm-lock.yaml b/airflow-core/src/airflow/ui/pnpm-lock.yaml index 256fa44174f5c..c6fd32a14cd1f 100644 --- a/airflow-core/src/airflow/ui/pnpm-lock.yaml +++ b/airflow-core/src/airflow/ui/pnpm-lock.yaml @@ -42,53 +42,53 @@ importers: specifier: ^2.3.4 version: 2.3.4 '@chakra-ui/react': - specifier: ~3.34.0 - version: 3.34.0(@emotion/react@11.14.0(@types/react@19.2.15)(react@19.2.6))(react-dom@19.2.6(react@19.2.6))(react@19.2.6) + specifier: ~3.36.0 + version: 3.36.0(@emotion/react@11.14.0(@types/react@19.2.17)(react@19.2.7))(react-dom@19.2.7(react@19.2.7))(react@19.2.7) '@emotion/react': specifier: ^11.14.0 - version: 11.14.0(@types/react@19.2.15)(react@19.2.6) + version: 11.14.0(@types/react@19.2.17)(react@19.2.7) '@guanmingchiu/sqlparser-ts': - specifier: ^0.61.1 - version: 0.61.1 + specifier: ^0.62.0 + version: 0.62.0 '@lezer/highlight': specifier: ^1.2.3 version: 1.2.3 '@monaco-editor/react': specifier: ^4.7.0 - version: 4.7.0(monaco-editor@0.52.2)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) + version: 4.7.0(monaco-editor@0.55.1)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) '@tanstack/react-query': - specifier: ^5.90.21 - version: 5.90.21(react@19.2.6) + specifier: ^5.101.2 + version: 5.101.2(react@19.2.7) '@tanstack/react-table': specifier: ^8.21.3 - version: 8.21.3(react-dom@19.2.6(react@19.2.6))(react@19.2.6) + version: 8.21.3(react-dom@19.2.7(react@19.2.7))(react@19.2.7) '@tanstack/react-virtual': - specifier: ^3.13.21 - version: 3.13.21(react-dom@19.2.6(react@19.2.6))(react@19.2.6) + specifier: ^3.14.5 + version: 3.14.5(react-dom@19.2.7(react@19.2.7))(react@19.2.7) '@visx/group': specifier: ^3.12.0 - version: 3.12.0(react@19.2.6) + version: 3.12.0(react@19.2.7) '@visx/shape': specifier: ^3.12.0 - version: 3.12.0(react@19.2.6) + version: 3.12.0(react@19.2.7) '@xyflow/react': - specifier: ^12.10.1 - version: 12.10.1(@types/react@19.2.15)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) + specifier: ^12.11.1 + version: 12.11.1(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) anser: specifier: ^2.3.5 version: 2.3.5 axios: - specifier: ^1.16.0 - version: 1.16.1 + specifier: ^1.18.1 + version: 1.18.1 chakra-react-select: - specifier: ^6.1.1 - version: 6.1.1(@chakra-ui/react@3.34.0(@emotion/react@11.14.0(@types/react@19.2.15)(react@19.2.6))(react-dom@19.2.6(react@19.2.6))(react@19.2.6))(@types/react@19.2.15)(next-themes@0.4.6(react-dom@19.2.6(react@19.2.6))(react@19.2.6))(react-dom@19.2.6(react@19.2.6))(react@19.2.6) + specifier: ^6.1.3 + version: 6.1.3(@chakra-ui/react@3.36.0(@emotion/react@11.14.0(@types/react@19.2.17)(react@19.2.7))(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(@types/react@19.2.17)(next-themes@0.4.6(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react-dom@19.2.7(react@19.2.7))(react@19.2.7) chart.js: specifier: ^4.5.1 version: 4.5.1 chartjs-adapter-dayjs-4: specifier: ^1.0.4 - version: 1.0.4(chart.js@4.5.1)(dayjs@1.11.19) + version: 1.0.4(chart.js@4.5.1)(dayjs@1.11.21) chartjs-plugin-annotation: specifier: ^3.1.0 version: 3.1.0(chart.js@4.5.1) @@ -96,8 +96,8 @@ importers: specifier: ^4.0.2 version: 4.0.2 dayjs: - specifier: ^1.11.19 - version: 1.11.19 + specifier: ^1.11.21 + version: 1.11.21 elkjs: specifier: ^0.11.1 version: 0.11.1 @@ -105,189 +105,189 @@ importers: specifier: ^1.11.13 version: 1.11.13 i18next: - specifier: ^25.8.16 - version: 25.8.16(typescript@6.0.3) + specifier: ^25.10.10 + version: 25.10.10(typescript@6.0.3) i18next-browser-languagedetector: specifier: ^8.2.1 version: 8.2.1 i18next-http-backend: - specifier: ^3.0.5 - version: 3.0.5 + specifier: ^3.0.6 + version: 3.0.6 monaco-editor: - specifier: ^0.52.2 - version: 0.52.2 + specifier: ^0.55.1 + version: 0.55.1 next-themes: specifier: ^0.4.6 - version: 0.4.6(react-dom@19.2.6(react@19.2.6))(react@19.2.6) + version: 0.4.6(react-dom@19.2.7(react@19.2.7))(react@19.2.7) react: - specifier: ^19.2.6 - version: 19.2.6 + specifier: ^19.2.7 + version: 19.2.7 react-chartjs-2: specifier: ^5.3.1 - version: 5.3.1(chart.js@4.5.1)(react@19.2.6) + version: 5.3.1(chart.js@4.5.1)(react@19.2.7) react-dom: - specifier: ^19.2.6 - version: 19.2.6(react@19.2.6) + specifier: ^19.2.7 + version: 19.2.7(react@19.2.7) react-hook-form: - specifier: ^7.71.2 - version: 7.71.2(react@19.2.6) + specifier: ^7.80.0 + version: 7.80.0(react@19.2.7) react-hotkeys-hook: - specifier: ^4.6.1 - version: 4.6.1(react-dom@19.2.6(react@19.2.6))(react@19.2.6) + specifier: ^4.6.2 + version: 4.6.2(react-dom@19.2.7(react@19.2.7))(react@19.2.7) react-i18next: - specifier: ^16.6.5 - version: 16.6.5(i18next@25.8.16(typescript@6.0.3))(react-dom@19.2.6(react@19.2.6))(react@19.2.6)(typescript@6.0.3) + specifier: ^16.6.6 + version: 16.6.6(i18next@25.10.10(typescript@6.0.3))(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(typescript@6.0.3) react-icons: - specifier: ^5.6.0 - version: 5.6.0(react@19.2.6) + specifier: ^5.7.0 + version: 5.7.0(react@19.2.7) react-innertext: specifier: ^1.1.5 - version: 1.1.5(@types/react@19.2.15)(react@19.2.6) + version: 1.1.5(@types/react@19.2.17)(react@19.2.7) react-markdown: specifier: ^9.1.0 - version: 9.1.0(@types/react@19.2.15)(react@19.2.6) + version: 9.1.0(@types/react@19.2.17)(react@19.2.7) react-resizable-panels: specifier: ^3.0.6 - version: 3.0.6(react-dom@19.2.6(react@19.2.6))(react@19.2.6) + version: 3.0.6(react-dom@19.2.7(react@19.2.7))(react@19.2.7) react-router-dom: - specifier: ^7.13.1 - version: 7.13.1(react-dom@19.2.6(react@19.2.6))(react@19.2.6) + specifier: ^7.18.1 + version: 7.18.1(react-dom@19.2.7(react@19.2.7))(react@19.2.7) react-syntax-highlighter: - specifier: ^15.6.1 - version: 15.6.1(react@19.2.6) + specifier: ^15.6.6 + version: 15.6.6(react@19.2.7) remark-gfm: specifier: ^4.0.1 version: 4.0.1 use-debounce: - specifier: ^10.1.0 - version: 10.1.0(react@19.2.6) + specifier: ^10.1.1 + version: 10.1.1(react@19.2.7) usehooks-ts: specifier: ^3.1.1 - version: 3.1.1(react@19.2.6) + version: 3.1.1(react@19.2.7) yaml: - specifier: '>=2.8.3' - version: 2.8.3 + specifier: ^2.9.0 + version: 2.9.0 zustand: - specifier: ^5.0.11 - version: 5.0.11(@types/react@19.2.15)(react@19.2.6)(use-sync-external-store@1.6.0(react@19.2.6)) + specifier: ^5.0.14 + version: 5.0.14(@types/react@19.2.17)(react@19.2.7)(use-sync-external-store@1.6.0(react@19.2.7)) devDependencies: '@7nohe/openapi-react-query-codegen': specifier: ^1.6.2 version: 1.6.2(commander@12.1.0)(glob@11.1.0)(magicast@0.3.5)(ts-morph@27.0.2)(typescript@6.0.3) '@eslint/compat': - specifier: ^2.0.5 - version: 2.0.5(eslint@10.3.0(jiti@1.21.7)) + specifier: ^2.1.0 + version: 2.1.0(eslint@10.6.0(jiti@1.21.7)) '@eslint/js': specifier: ^10.0.1 - version: 10.0.1(eslint@10.3.0(jiti@1.21.7)) + version: 10.0.1(eslint@10.6.0(jiti@1.21.7)) '@playwright/test': - specifier: ^1.60.0 - version: 1.60.0 + specifier: ^1.61.1 + version: 1.61.1 '@rolldown/plugin-babel': - specifier: ^0.2.2 - version: 0.2.2(@babel/core@7.29.0)(@babel/runtime@7.29.7)(rolldown@1.0.3)(vite@8.0.16(@types/node@24.10.3)(jiti@1.21.7)(yaml@2.8.3)) + specifier: ^0.2.3 + version: 0.2.3(@babel/core@7.29.0)(@babel/runtime@7.29.7)(rolldown@1.1.4)(vite@8.1.2(@types/node@24.13.2)(jiti@1.21.7)(yaml@2.9.0)) '@stylistic/eslint-plugin': specifier: ^2.13.0 - version: 2.13.0(eslint@10.3.0(jiti@1.21.7))(typescript@6.0.3) + version: 2.13.0(eslint@10.6.0(jiti@1.21.7))(typescript@6.0.3) '@tanstack/eslint-plugin-query': - specifier: ^5.91.4 - version: 5.91.4(eslint@10.3.0(jiti@1.21.7))(typescript@6.0.3) + specifier: ^5.101.2 + version: 5.101.2(eslint@10.6.0(jiti@1.21.7))(typescript@6.0.3) '@testing-library/jest-dom': specifier: ^6.9.1 version: 6.9.1 '@testing-library/react': specifier: ^16.3.2 - version: 16.3.2(@testing-library/dom@10.4.0)(@types/react-dom@19.2.3(@types/react@19.2.15))(@types/react@19.2.15)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) + version: 16.3.2(@testing-library/dom@10.4.0)(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) '@trivago/prettier-plugin-sort-imports': specifier: ^4.3.0 - version: 4.3.0(prettier@3.8.1) + version: 4.3.0(prettier@3.9.4) '@types/culori': specifier: ^4.0.1 version: 4.0.1 '@types/node': - specifier: ^24.10.1 - version: 24.10.3 + specifier: ^24.13.2 + version: 24.13.2 '@types/react': - specifier: ^19.2.15 - version: 19.2.15 + specifier: ^19.2.17 + version: 19.2.17 '@types/react-dom': specifier: ^19.2.3 - version: 19.2.3(@types/react@19.2.15) + version: 19.2.3(@types/react@19.2.17) '@types/react-syntax-highlighter': specifier: ^15.5.13 version: 15.5.13 '@typescript-eslint/eslint-plugin': - specifier: ^8.60.0 - version: 8.60.0(@typescript-eslint/parser@8.60.0(eslint@10.3.0(jiti@1.21.7))(typescript@6.0.3))(eslint@10.3.0(jiti@1.21.7))(typescript@6.0.3) + specifier: ^8.62.1 + version: 8.62.1(@typescript-eslint/parser@8.62.1(eslint@10.6.0(jiti@1.21.7))(typescript@6.0.3))(eslint@10.6.0(jiti@1.21.7))(typescript@6.0.3) '@typescript-eslint/parser': - specifier: ^8.60.0 - version: 8.60.0(eslint@10.3.0(jiti@1.21.7))(typescript@6.0.3) + specifier: ^8.62.1 + version: 8.62.1(eslint@10.6.0(jiti@1.21.7))(typescript@6.0.3) '@typescript-eslint/utils': - specifier: ^8.60.0 - version: 8.60.0(eslint@10.3.0(jiti@1.21.7))(typescript@6.0.3) + specifier: ^8.62.1 + version: 8.62.1(eslint@10.6.0(jiti@1.21.7))(typescript@6.0.3) '@vitejs/plugin-react': - specifier: ^6.0.1 - version: 6.0.1(@rolldown/plugin-babel@0.2.2(@babel/core@7.29.0)(@babel/runtime@7.29.7)(rolldown@1.0.3)(vite@8.0.16(@types/node@24.10.3)(jiti@1.21.7)(yaml@2.8.3)))(babel-plugin-react-compiler@1.0.0)(vite@8.0.16(@types/node@24.10.3)(jiti@1.21.7)(yaml@2.8.3)) + specifier: ^6.0.3 + version: 6.0.3(@rolldown/plugin-babel@0.2.3(@babel/core@7.29.0)(@babel/runtime@7.29.7)(rolldown@1.1.4)(vite@8.1.2(@types/node@24.13.2)(jiti@1.21.7)(yaml@2.9.0)))(babel-plugin-react-compiler@1.0.0)(vite@8.1.2(@types/node@24.13.2)(jiti@1.21.7)(yaml@2.9.0)) '@vitejs/plugin-react-swc': - specifier: ^4.2.3 - version: 4.2.3(@swc/helpers@0.5.19)(vite@8.0.16(@types/node@24.10.3)(jiti@1.21.7)(yaml@2.8.3)) + specifier: ^4.3.1 + version: 4.3.1(@swc/helpers@0.5.23)(vite@8.1.2(@types/node@24.13.2)(jiti@1.21.7)(yaml@2.9.0)) '@vitest/coverage-v8': - specifier: ^4.1.4 - version: 4.1.4(vitest@4.1.4) + specifier: ^4.1.9 + version: 4.1.9(vitest@4.1.9) babel-plugin-react-compiler: specifier: ^1.0.0 version: 1.0.0 eslint: - specifier: ^10.3.0 - version: 10.3.0(jiti@1.21.7) + specifier: ^10.6.0 + version: 10.6.0(jiti@1.21.7) eslint-config-prettier: specifier: ^10.1.8 - version: 10.1.8(eslint@10.3.0(jiti@1.21.7)) + version: 10.1.8(eslint@10.6.0(jiti@1.21.7)) eslint-plugin-i18next: - specifier: ^6.1.4 - version: 6.1.4 + specifier: ^6.1.5 + version: 6.1.5 eslint-plugin-jsonc: - specifier: ^3.1.2 - version: 3.1.2(eslint@10.3.0(jiti@1.21.7)) + specifier: ^3.2.0 + version: 3.2.0(eslint@10.6.0(jiti@1.21.7)) eslint-plugin-jsx-a11y: specifier: ^6.10.2 - version: 6.10.2(eslint@10.3.0(jiti@1.21.7)) + version: 6.10.2(eslint@10.6.0(jiti@1.21.7)) eslint-plugin-perfectionist: - specifier: ^5.9.0 - version: 5.9.0(eslint@10.3.0(jiti@1.21.7))(typescript@6.0.3) + specifier: ^5.9.1 + version: 5.9.1(eslint@10.6.0(jiti@1.21.7))(typescript@6.0.3) eslint-plugin-prettier: - specifier: ^5.5.5 - version: 5.5.5(eslint-config-prettier@10.1.8(eslint@10.3.0(jiti@1.21.7)))(eslint@10.3.0(jiti@1.21.7))(prettier@3.8.1) + specifier: ^5.5.6 + version: 5.5.6(eslint-config-prettier@10.1.8(eslint@10.6.0(jiti@1.21.7)))(eslint@10.6.0(jiti@1.21.7))(prettier@3.9.4) eslint-plugin-react: specifier: ^7.37.5 - version: 7.37.5(eslint@10.3.0(jiti@1.21.7)) + version: 7.37.5(eslint@10.6.0(jiti@1.21.7)) eslint-plugin-react-hooks: specifier: ^7.1.1 - version: 7.1.1(eslint@10.3.0(jiti@1.21.7)) + version: 7.1.1(eslint@10.6.0(jiti@1.21.7)) eslint-plugin-react-refresh: - specifier: ^0.5.2 - version: 0.5.2(eslint@10.3.0(jiti@1.21.7)) + specifier: ^0.5.3 + version: 0.5.3(eslint@10.6.0(jiti@1.21.7)) eslint-plugin-unicorn: specifier: ^64.0.0 - version: 64.0.0(eslint@10.3.0(jiti@1.21.7)) + version: 64.0.0(eslint@10.6.0(jiti@1.21.7)) globals: specifier: ^15.15.0 version: 15.15.0 happy-dom: - specifier: '>=20.8.8' - version: 20.8.9 + specifier: ^20.10.6 + version: 20.10.6 jsonc-eslint-parser: - specifier: ^2.4.0 - version: 2.4.1 + specifier: ^2.4.2 + version: 2.4.2 msw: - specifier: ^2.12.10 - version: 2.12.10(@types/node@24.10.3)(typescript@6.0.3) + specifier: ^2.14.6 + version: 2.14.6(@types/node@24.13.2)(typescript@6.0.3) openapi-merge-cli: specifier: ^1.3.2 version: 1.3.2 prettier: - specifier: ^3.8.1 - version: 3.8.1 + specifier: ^3.9.4 + version: 3.9.4 ts-morph: specifier: ^27.0.2 version: 27.0.2 @@ -295,17 +295,17 @@ importers: specifier: ^6.0.3 version: 6.0.3 typescript-eslint: - specifier: ^8.60.0 - version: 8.60.0(eslint@10.3.0(jiti@1.21.7))(typescript@6.0.3) + specifier: ^8.62.1 + version: 8.62.1(eslint@10.6.0(jiti@1.21.7))(typescript@6.0.3) vite: - specifier: ^8.0.16 - version: 8.0.16(@types/node@24.10.3)(jiti@1.21.7)(yaml@2.8.3) + specifier: ^8.1.2 + version: 8.1.2(@types/node@24.13.2)(jiti@1.21.7)(yaml@2.9.0) vite-plugin-css-injected-by-js: specifier: ^3.5.2 - version: 3.5.2(vite@8.0.16(@types/node@24.10.3)(jiti@1.21.7)(yaml@2.8.3)) + version: 3.5.2(vite@8.1.2(@types/node@24.13.2)(jiti@1.21.7)(yaml@2.9.0)) vitest: - specifier: ^4.1.4 - version: 4.1.4(@types/node@24.10.3)(@vitest/coverage-v8@4.1.4)(happy-dom@20.8.9)(msw@2.12.10(@types/node@24.10.3)(typescript@6.0.3))(vite@8.0.16(@types/node@24.10.3)(jiti@1.21.7)(yaml@2.8.3)) + specifier: ^4.1.9 + version: 4.1.9(@types/node@24.13.2)(@vitest/coverage-v8@4.1.9)(happy-dom@20.10.6)(msw@2.14.6(@types/node@24.13.2)(typescript@6.0.3))(vite@8.1.2(@types/node@24.13.2)(jiti@1.21.7)(yaml@2.9.0)) web-worker: specifier: ^1.5.0 version: 1.5.0 @@ -329,8 +329,8 @@ packages: resolution: {integrity: sha512-9K6xOqeevacvweLGik6LnZCb1fBtCOSIWQs8d096XGeqoLKC33UVMGz9+77Gw44KvbH4pKcQPWo4ZpxkXYj05w==} engines: {node: '>= 16'} - '@ark-ui/react@5.34.1': - resolution: {integrity: sha512-RJlXCvsHzbK9LVxUVtaSD5pyF1PL8IUR1rHHkf0H0Sa397l6kOFE4EH7MCSj3pDumj2NsmKDVeVgfkfG0KCuEw==} + '@ark-ui/react@5.37.2': + resolution: {integrity: sha512-Q0R2Ah50kUhup0Ljxg65zGJq5yBV52BLm1coRkjHHid40d1yclaDGfhPL48kcF/xtjAFlGLkL6SiENkGvfh+mw==} peerDependencies: react: '>=18.0.0' react-dom: '>=18.0.0' @@ -456,10 +456,6 @@ packages: resolution: {integrity: sha512-05WQkdpL9COIMz4LjTxGpPNCdlpyimKppYNoJ5Di5EUObifl8t4tuLuUBBZEpoLYOmfvIWrsp9fCl0HoPRVTdA==} engines: {node: '>=6.9.0'} - '@babel/runtime@7.29.2': - resolution: {integrity: sha512-JiDShH45zKHWyGe4ZNVRrCjBz8Nh9TMmZG1kh4QTK8hCBTWBi8Da+i7s1fJw7/lYpM4ccepSNfqzZ/QvABBi5g==} - engines: {node: '>=6.9.0'} - '@babel/runtime@7.29.7': resolution: {integrity: sha512-Nq8OhGWiZIZGV6hLHoyAKLLcJihP/xFeBMGJoUrxTX2psI8dCifzLhZISFb+VWS3wFMRDmCGw5R+dOySCqPLhw==} engines: {node: '>=6.9.0'} @@ -499,21 +495,21 @@ packages: '@chakra-ui/anatomy@2.3.4': resolution: {integrity: sha512-fFIYN7L276gw0Q7/ikMMlZxP7mvnjRaWJ7f3Jsf9VtDOi6eAYIBRrhQe6+SZ0PGmoOkRaBc7gSE5oeIbgFFyrw==} - '@chakra-ui/react@3.34.0': - resolution: {integrity: sha512-VLhpVwv5IVxhwajO10KnS1VQT4hDqQMQP/A796Ya+uVu8AdoSX+5HHyTLTkYIeXIDMe0xLqJfov04OBKbBchJA==} + '@chakra-ui/react@3.36.0': + resolution: {integrity: sha512-6AxUbJsC6yyTzPeYL8sxyAL07lflT0NA+S6tcPzEuwdMux+benRMFOpPktnkifWKl/Vq/JD7fhxDyMuDQ4M0gA==} peerDependencies: '@emotion/react': '>=11' react: '>=18' react-dom: '>=18' - '@emnapi/core@1.10.0': - resolution: {integrity: sha512-yq6OkJ4p82CAfPl0u9mQebQHKPJkY7WrIuk205cTYnYe+k2Z8YBh11FrbRG/H6ihirqcacOgl2BIO8oyMQLeXw==} + '@emnapi/core@1.11.1': + resolution: {integrity: sha512-RSvbQmHzdKzNsLYa/wHrbc3KN4sYLKAdPZxqiM2HATqv/SBk2/ENSHpvXGaLOMcsAyz0poEGqkmmKYG3OWiJEQ==} - '@emnapi/runtime@1.10.0': - resolution: {integrity: sha512-ewvYlk86xUoGI0zQRNq/mC+16R1QeDlKQy21Ki3oSYXNgLb45GV1P6A0M+/s6nyCuNDqe5VpaY84BzXGwVbwFA==} + '@emnapi/runtime@1.11.1': + resolution: {integrity: sha512-vgj7R3y3Wgx24IQaGPA/R6YFXLHVMOZ0uVEyIQPaWs+rd1AzfEMXlAC22FYwO1XkKR6NPsq7mUandH8oIRdZFw==} - '@emnapi/wasi-threads@1.2.1': - resolution: {integrity: sha512-uTII7OYF+/Mes/MrcIOYp5yOtSMLBWSIoLPpcgwipoiKbli6k322tcoFsxoIIxPDqW01SQGAgko4EzZi2BNv2w==} + '@emnapi/wasi-threads@1.2.2': + resolution: {integrity: sha512-c95qOXkHdydNKhscBTebqEC1CVAZpyqOfVfBzQ1qgzyl3gfeldUjIggDbIZgDKsHLgnsM+igH7TJ/eAasaVuMA==} '@emotion/babel-plugin@11.13.5': resolution: {integrity: sha512-pxHCpT2ex+0q+HH91/zsdHkw/lXd468DIN2zvfvLtPKLLMo6gQj7oLObq8PhkrxOZb/gGCq03S3Z7PDhS8pduQ==} @@ -569,8 +565,8 @@ packages: resolution: {integrity: sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==} engines: {node: ^12.0.0 || ^14.0.0 || >=16.0.0} - '@eslint/compat@2.0.5': - resolution: {integrity: sha512-IbHDbHJfkVNv6xjlET8AIVo/K1NQt7YT4Rp6ok/clyBGcpRx1l6gv0Rq3vBvYfPJIZt6ODf66Zq08FJNDpnzgg==} + '@eslint/compat@2.1.0': + resolution: {integrity: sha512-LgaSCymEpw7tF53xvDw9SNsraPb1IBHxpdABIOM0hW8UAlP8znrjYtuxfR58FSJ3L9BhwD+FaPRFQpZq84Nh6g==} engines: {node: ^20.19.0 || ^22.13.0 || >=24} peerDependencies: eslint: ^8.40 || 9 || 10 @@ -582,8 +578,8 @@ packages: resolution: {integrity: sha512-Y3kKLvC1dvTOT+oGlqNQ1XLqK6D1HU2YXPc52NmAlJZbMMWDzGYXMiPRJ8TYD39muD/OTjlZmNJ4ib7dvSrMBA==} engines: {node: ^20.19.0 || ^22.13.0 || >=24} - '@eslint/config-helpers@0.5.5': - resolution: {integrity: sha512-eIJYKTCECbP/nsKaaruF6LW967mtbQbsw4JTtSVkUQc9MneSkbrgPJAbKl9nWr0ZeowV8BfsarBmPpBzGelA2w==} + '@eslint/config-helpers@0.6.0': + resolution: {integrity: sha512-ii6Bw9jJ2zi2cWA2Z+9/QZ/+3DX6kwaV5Q986D/CdP3Lap3w/pgQZ373FV7byY/i7L4IRH/G43I5dz1ClsCbpA==} engines: {node: ^20.19.0 || ^22.13.0 || >=24} '@eslint/core@1.2.1': @@ -603,34 +599,21 @@ packages: resolution: {integrity: sha512-vqTaUEgxzm+YDSdElad6PiRoX4t8VGDjCtt05zn4nU810UIx/uNEV7/lZJ6KwFThKZOzOxzXy48da+No7HZaMw==} engines: {node: ^20.19.0 || ^22.13.0 || >=24} - '@eslint/plugin-kit@0.6.1': - resolution: {integrity: sha512-iH1B076HoAshH1mLpHMgwdGeTs0CYwL0SPMkGuSebZrwBp16v415e9NZXg2jtrqPVQjf6IANe2Vtlr5KswtcZQ==} - engines: {node: ^20.19.0 || ^22.13.0 || >=24} - - '@eslint/plugin-kit@0.7.1': - resolution: {integrity: sha512-rZAP3aVgB9ds9KOeUSL+zZ21hPmo8dh6fnIFwRQj5EAZl9gzR7wxYbYXYysAM8CTqGmUGyp2S4kUdV17MnGuWQ==} + '@eslint/plugin-kit@0.7.2': + resolution: {integrity: sha512-+CNAzxglkrpNf/kKywqQfk74QjtceuOE7Qm+AF8miRvPF/wmmK5+OJOgVh3AVTT3RP2mH3+FOaxlE5v72owk0A==} engines: {node: ^20.19.0 || ^22.13.0 || >=24} - '@floating-ui/core@1.7.1': - resolution: {integrity: sha512-azI0DrjMMfIug/ExbBaeDVJXcY0a7EPvPjb2xAJPa4HeimBX+Z18HK8QQR3jb6356SnDDdxx+hinMLcJEDdOjw==} - '@floating-ui/core@1.7.5': resolution: {integrity: sha512-1Ih4WTWyw0+lKyFMcBHGbb5U5FtuHJuujoyyr5zTaWS5EYMeT6Jb2AuDeftsCsEuchO+mM2ij5+q9crhydzLhQ==} - '@floating-ui/dom@1.7.1': - resolution: {integrity: sha512-cwsmW/zyw5ltYTUeeYJ60CnQuPqmGwuGVhG9w0PRaRKkAyi38BT5CKrpIbb+jtahSwUl04cWzSx9ZOIxeS6RsQ==} - '@floating-ui/dom@1.7.6': resolution: {integrity: sha512-9gZSAI5XM36880PPMm//9dfiEngYoC6Am2izES1FF406YFsjvyBMmeJ2g4SAju3xWwtuynNRFL2s9hgxpLI5SQ==} '@floating-ui/utils@0.2.11': resolution: {integrity: sha512-RiB/yIh78pcIxl6lLMG0CgBXAZ2Y0eVHqMPYugu+9U0AeT6YBeiJpf7lbdJNIugFP5SIjwNRgo4DhR1Qxi26Gg==} - '@floating-ui/utils@0.2.9': - resolution: {integrity: sha512-MDWhGtE+eHw5JW7lq4qhc5yRLS11ERl1c7Z6Xd0a58DozHES6EnNNwUWbMiG4J9Cgj053Bhk8zvlhFYKVhULwg==} - - '@guanmingchiu/sqlparser-ts@0.61.1': - resolution: {integrity: sha512-5RA05UHDkcm4cyhBNz2oJqU+8+fhCZvseSGtAvuVcwM1EEh2FfRaU1qdPygpriGqsQRT3Cn1PK5YShJffQjmXg==} + '@guanmingchiu/sqlparser-ts@0.62.0': + resolution: {integrity: sha512-m0YUe1oPzaixw0YX8Es2B8tA8UDPwT6uMIsKocTfyOcpb8tQIYWrP5Im70LT3p8tARLm718M6SObAxNYc0Wi3A==} engines: {node: '>=16.0.0'} '@hey-api/openapi-ts@0.52.0': @@ -660,46 +643,46 @@ packages: resolution: {integrity: sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==} engines: {node: '>=18.18'} - '@inquirer/ansi@1.0.2': - resolution: {integrity: sha512-S8qNSZiYzFd0wAcyG5AXCvUHC5Sr7xpZ9wZ2py9XR88jUz8wooStVx5M6dRzczbBWjic9NP7+rY0Xi7qqK/aMQ==} - engines: {node: '>=18'} + '@inquirer/ansi@2.0.7': + resolution: {integrity: sha512-3eTuUO1vH2cZm2ZKHeQxnOqlTi9EfZDGgIe3BL3I4u+rJHocr9Fz86M4fjYABPvFnQG/gGK551HqDiIcETwU6Q==} + engines: {node: '>=23.5.0 || ^22.13.0 || ^20.17.0'} - '@inquirer/confirm@5.1.21': - resolution: {integrity: sha512-KR8edRkIsUayMXV+o3Gv+q4jlhENF9nMYUZs9PA2HzrXeHI8M5uDag70U7RJn9yyiMZSbtF5/UexBtAVtZGSbQ==} - engines: {node: '>=18'} + '@inquirer/confirm@6.1.1': + resolution: {integrity: sha512-eb8DBZcz/2qHWQda4rk2JiQk5h9QV/cVHi1yjt0f69WFZMRFn0sJTye3EAP8icut8UDMjQPsaH5KbcOogefrFQ==} + engines: {node: '>=23.5.0 || ^22.13.0 || ^20.17.0'} peerDependencies: '@types/node': '>=18' peerDependenciesMeta: '@types/node': optional: true - '@inquirer/core@10.3.2': - resolution: {integrity: sha512-43RTuEbfP8MbKzedNqBrlhhNKVwoK//vUFNW3Q3vZ88BLcrs4kYpGg+B2mm5p2K/HfygoCxuKwJJiv8PbGmE0A==} - engines: {node: '>=18'} + '@inquirer/core@11.2.1': + resolution: {integrity: sha512-Qd6GJT1yVyrZZCfN8W2qKF5ApmqryXRhRKCuip8h01x2w/esJQ2XIYc6f9abMIHgKQdBfFTSOdbHRLAhuM09UA==} + engines: {node: '>=23.5.0 || ^22.13.0 || ^20.17.0'} peerDependencies: '@types/node': '>=18' peerDependenciesMeta: '@types/node': optional: true - '@inquirer/figures@1.0.15': - resolution: {integrity: sha512-t2IEY+unGHOzAaVM5Xx6DEWKeXlDDcNPeDyUpsRc6CUhBfU3VQOEl+Vssh7VNp1dR8MdUJBWhuObjXCsVpjN5g==} - engines: {node: '>=18'} + '@inquirer/figures@2.0.7': + resolution: {integrity: sha512-aJ8TBPOGB6f/2qziPfElISTCEd5XOYTFckA2SGjhNmiKzfK/u4ot3v0DUzGVdUnKjN10EqnnEPck36BkyfLnJw==} + engines: {node: '>=23.5.0 || ^22.13.0 || ^20.17.0'} - '@inquirer/type@3.0.10': - resolution: {integrity: sha512-BvziSRxfz5Ov8ch0z/n3oijRSEcEsHnhggm4xFZe93DHcUCTlutlq9Ox4SVENAfcRD22UQq7T/atg9Wr3k09eA==} - engines: {node: '>=18'} + '@inquirer/type@4.0.7': + resolution: {integrity: sha512-t28inv14nMQ1PhKpsJPY+kEs/c00qzeCOS2gTNRyTjG5d6qsVA2fItxW4hkvGZ5lvanGLdtCzVIx5dwdRpN1+g==} + engines: {node: '>=23.5.0 || ^22.13.0 || ^20.17.0'} peerDependencies: '@types/node': '>=18' peerDependenciesMeta: '@types/node': optional: true - '@internationalized/date@3.11.0': - resolution: {integrity: sha512-BOx5huLAWhicM9/ZFs84CzP+V3gBW6vlpM02yzsdYC7TGlZJX1OJiEEHcSayF00Z+3jLlm4w79amvSt6RqKN3Q==} + '@internationalized/date@3.12.2': + resolution: {integrity: sha512-FY1Y+H64NDs+HAF6omlnWxm3mEpfgaCSWtL5l551ZZfImA+kGjPFgrnJrGjH6lfmLL0g8Z/mBu1R3kufeCp6Jw==} - '@internationalized/number@3.6.5': - resolution: {integrity: sha512-6hY4Kl4HPBvtfS62asS/R22JzNNy8vi/Ssev7x6EobfCp+9QIB2hKvI2EtbdJ0VSQacxVNtqhE/NmF/NZ0gm6g==} + '@internationalized/number@3.6.6': + resolution: {integrity: sha512-iFgmQaXHE0vytNfpLZWOC2mEJCBRzcUxt53Xf/yCXG93lRvqas237i3r7X4RKMwO3txiyZD4mQjKAByFv6UGSQ==} '@isaacs/cliui@9.0.0': resolution: {integrity: sha512-AokJm4tuBHillT+FpMtxQ60n8ObyXBatq7jD2/JA9dxbDDokKQm8KMht5ibGzLVU9IJDIKK4TPKgMHEYMn3lMg==} @@ -727,15 +710,9 @@ packages: resolution: {integrity: sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A==} engines: {node: '>=6.0.0'} - '@jridgewell/sourcemap-codec@1.5.0': - resolution: {integrity: sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==} - '@jridgewell/sourcemap-codec@1.5.5': resolution: {integrity: sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==} - '@jridgewell/trace-mapping@0.3.25': - resolution: {integrity: sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==} - '@jridgewell/trace-mapping@0.3.31': resolution: {integrity: sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==} @@ -761,12 +738,12 @@ packages: react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 - '@mswjs/interceptors@0.41.3': - resolution: {integrity: sha512-cXu86tF4VQVfwz8W1SPbhoRyHJkti6mjH/XJIxp40jhO4j2k1m4KYrEykxqWPkFF3vrK4rgQppBh//AwyGSXPA==} + '@mswjs/interceptors@0.41.9': + resolution: {integrity: sha512-VVPPgHyQ6ShqnrmDWuxjmUIsO9gWyOZFmuOfLd9LfBGQJwZfy0gvv9pbHSJuoFNIYC7ZDX9aoFwowjcdSC4E8w==} engines: {node: '>=18'} - '@napi-rs/wasm-runtime@1.1.5': - resolution: {integrity: sha512-AWPoBRJ9tsnVhor4sjO7rkni+7p+2IAEFj6cx06UgP10jkQHqay/36uRV/bFkgrh18D9vb4cr8Q0Pthskgzy+Q==} + '@napi-rs/wasm-runtime@1.1.6': + resolution: {integrity: sha512-ZLv/JdUfkvOy9eCnnBaGfiO+XimbjebAeO+MRQqD/B+FR1tnRN0tpKSJHRbE8sFfS6aqsXZ67TQjfwfsxULVbg==} peerDependencies: '@emnapi/core': ^1.7.1 '@emnapi/runtime': ^1.7.1 @@ -774,6 +751,9 @@ packages: '@open-draft/deferred-promise@2.2.0': resolution: {integrity: sha512-CecwLWx3rhxVQF6V4bAgPS5t+So2sTbPgAzafKkVizyi7tlwpcFpdFqq+wqF2OwNBmqFuu6tOyouTuxgpMfzmA==} + '@open-draft/deferred-promise@3.0.0': + resolution: {integrity: sha512-XW375UK8/9SqUVNVa6M0yEy8+iTi4QN5VZ7aZuRFQmy76LRwI9wy5F4YIBU6T+eTe2/DNDo8tqu8RHlwLHM6RA==} + '@open-draft/logger@0.3.0': resolution: {integrity: sha512-X2g45fzhxH238HKO4xbSr7+wBS8Fvw6ixhTDuvLd5mqh6bJJCFAPwU9mPDxbcrRtfxv4u5IHCEH77BmxvXmmxQ==} @@ -784,118 +764,113 @@ packages: resolution: {integrity: sha512-XRO0zi2NIUKq2lUk3T1ecFSld1fMWRKE6naRFGkgkdeosx7IslyUKNv5Dcb5PJTja9tHJoFu0v/7yEpAkrkrTg==} engines: {node: ^20.19.0 || ^22.13.0 || >=24} - '@oxc-project/types@0.133.0': - resolution: {integrity: sha512-KzkdCd6Uxqnf6l3HOw1xfatAlUURA0g14cvBYFyJ5SaNOQbOUvBr9PKArcPcrNIeRsBdgcUzOGrhKveVpvOIGA==} + '@oxc-project/types@0.138.0': + resolution: {integrity: sha512-1a7ZKmrRTCoN1XMZ4L0PyyqrMnrNlLyPuOkdSX2MZg7IiIGRUyurNhAm73ptDOraoBcIordsIGKNPKUzy3ZmfA==} - '@pandacss/is-valid-prop@1.9.0': - resolution: {integrity: sha512-AZvpXWGyjbHc8TC+YVloQ31Z2c4j2xMvYj6UfVxuZdB5w4c9+4N8wy5R7I/XswNh8e4cfUlkvsEGDXjhJRgypw==} + '@pandacss/is-valid-prop@1.11.4': + resolution: {integrity: sha512-RWxInlS+lGgKiF0fB0HO76vsJFgRvbavm5Z25/GqqN8MPHXYA6n5rZnfdp4itEXy5DJkQ9vt3yrwa2IKiuhtrA==} + engines: {node: '>=20'} - '@pkgr/core@0.2.9': - resolution: {integrity: sha512-QNqXyfVS2wm9hweSYD2O7F0G06uurj9kZ96TRQE5Y9hU7+tgdZwIkbAKc5Ocy1HxEY2kuDQa6cQ1WRs/O5LFKA==} - engines: {node: ^12.20.0 || ^14.18.0 || >=16.0.0} + '@pkgr/core@0.3.6': + resolution: {integrity: sha512-SEeaJLb3qBNF/OaXnaR1NmmBbFYk1zC0ZH/52fATcRPLFg/p791YrcyFFy44Bo9sLaGuSuLp5Q6axbb/O+v/RA==} + engines: {node: ^14.18.0 || >=16.0.0} - '@playwright/test@1.60.0': - resolution: {integrity: sha512-O71yZIbAh/PxDMNGns37GHBIfrVkEVyn+AXyIa5dOTfb4/xNvRWV+Vv/NMbNCtODB/pO7vLlF2OTmMVLhmr7Ag==} + '@playwright/test@1.61.1': + resolution: {integrity: sha512-8nKv6+0RJSL9FE4jYOEGXnPeM/Hg12qZpmqzZjRh3qM0Y7c3z1mrOTfFLids72RDQYVh9WpLEfR5WdpNX4fkig==} engines: {node: '>=18'} hasBin: true - '@rolldown/binding-android-arm64@1.0.3': - resolution: {integrity: sha512-454rs7jHngixp/NMxd5srYD57OnzSlZ/eFTETjORQHLwJG1lRtmNOJcBerZlfu4GjKqeq8aCCIQrMdHyhI51Hw==} + '@rolldown/binding-android-arm64@1.1.4': + resolution: {integrity: sha512-EZLpf/8y7GXkkra90ML47kzik/GMP3EMcE9bPyHmRfxLC6z9+aW5A8poCsoxjrT5GfEcNAAvWwUHjvP1pUQkfw==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [android] - '@rolldown/binding-darwin-arm64@1.0.3': - resolution: {integrity: sha512-PcAhP+ynjURNyy8SKGl5DQP94aGuB/7JrXJb/t7P+hanXvQVMWzUvRRhBAcg/lNRadBhoUPqSoP4xw5tR/KBEA==} + '@rolldown/binding-darwin-arm64@1.1.4': + resolution: {integrity: sha512-aUi+HBvmYb7j8krl1+qJgkG8C17fO79gk3c+jPw4S8glRFc1DTija9S3EyaTSQUm5GJXYKDAsugBEhFHH2vYiQ==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [darwin] - '@rolldown/binding-darwin-x64@1.0.3': - resolution: {integrity: sha512-9YpfeUvSE2RS7wysJ81uOZkXJz7f7Q55H2Gvp3VEw/EsahqDtrphrZ0EwDLK5vvKOzaCrBsjF8JmnMLcUt78Gg==} + '@rolldown/binding-darwin-x64@1.1.4': + resolution: {integrity: sha512-F7hHC3gwY11+vByKPRWqwGbeXWVgKmL+pTGCinaEhdihzBV2aQ0fvZOch9cXYUOKuKKq429HeYXOqQLc7wFCEg==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [darwin] - '@rolldown/binding-freebsd-x64@1.0.3': - resolution: {integrity: sha512-yB1IlAsSNHncV6SCTL27/MVGR5htvQsoGxIv5KMGXALp+Ll1wYsn+x98M9MW7qa+NdSbvrrY7ANI4wLJ0n1e6g==} + '@rolldown/binding-freebsd-x64@1.1.4': + resolution: {integrity: sha512-sI5yw+7s92SK6odiEhD5lKCBlWcpjHS5qyqpVQbZAJ0fIzEUXrmbl3DH2ybR3PZogulNJF+COLtmA8hUfvkCCQ==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [freebsd] - '@rolldown/binding-linux-arm-gnueabihf@1.0.3': - resolution: {integrity: sha512-Yi30IVAAfLUCy2MseFjbB1jAMDl1VMCAas5StnYp8da9+CKvMd2H2cbEjWcw5NPaPqzvYkVIaF1nNUG+b7u/sw==} + '@rolldown/binding-linux-arm-gnueabihf@1.1.4': + resolution: {integrity: sha512-mCi0OKgEieFircrtVYmQAFGszRtMnZ6fpZAXrxanXAu7lqZcsK1E1RAaZNG0uKAnxox3B1f4EyQNnoyMfN1vAA==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm] os: [linux] - '@rolldown/binding-linux-arm64-gnu@1.0.3': - resolution: {integrity: sha512-jsO7R8To+AdlYgUmN5sHSCZbfhtMBkO0WUx8iORQnPcMMdgr7qM2DQmMwgabs3GhNztdmoKkMKQFHD6DTMCIQw==} + '@rolldown/binding-linux-arm64-gnu@1.1.4': + resolution: {integrity: sha512-B9Ial3Kv5sh0SHnB1g/QWcUQCEvCF6QKGAl4zXypYj65mVI+B4AhFBwPtSN7pDrJeIx8Z7zdy4ntx+wQABom7w==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [linux] - libc: [glibc] - '@rolldown/binding-linux-arm64-musl@1.0.3': - resolution: {integrity: sha512-VWkUHwWriDciit80wleYwKILoR/KMvxh/IdwS/paX+ZgpuRpCrKLUdadJbc0NpBEiyhpYawsJ73j9aCvOH+f7Q==} + '@rolldown/binding-linux-arm64-musl@1.1.4': + resolution: {integrity: sha512-lZVym0PuHE1KZ22gmFTC15lAkrg9iTszR617oYRB/iPY1A56ywoJzVKOJBKaot5RiikCObmur6pogpse3gRcng==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [linux] - libc: [musl] - '@rolldown/binding-linux-ppc64-gnu@1.0.3': - resolution: {integrity: sha512-5f1laC0SlIR0yDbFCd8acUhvJIag6N3zC5P7oUPN6wX0aOma+uKJ0wBDH5aq7I1PVI2ttTlhJwzwRIBnLiSGEg==} + '@rolldown/binding-linux-ppc64-gnu@1.1.4': + resolution: {integrity: sha512-t2DNiLJWNTbnEHyUzTumldML6ET4/g16467LZoDDJ3tSxGvguL5/NyC2lCsNKuyRycg9XeDQF5SSv+TNOhQEXg==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [ppc64] os: [linux] - libc: [glibc] - '@rolldown/binding-linux-s390x-gnu@1.0.3': - resolution: {integrity: sha512-Iq4ko0r4XsgbrF/LunNgHtAGLRRVE2kXonAXQ/MV0mC6jQpMOhW1SvtZja2EhC/kd05++bP78dsqBeIQyYJ6Yg==} + '@rolldown/binding-linux-s390x-gnu@1.1.4': + resolution: {integrity: sha512-0WIRnL1Uw4BvTZRLQt+PVgo6ZKTJadlC2btP+/EOXv2f/DWbY0rEgl+y834mIVwP1FkTlWVTrGGJXf12lru7EQ==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [s390x] os: [linux] - libc: [glibc] - '@rolldown/binding-linux-x64-gnu@1.0.3': - resolution: {integrity: sha512-B8m6tD5+/N5FeNQFbKlLA/2yVq9ycQP1SeedyEYYKWBNR3ZQbkvIUcNnDNM03lO1l5F2roiiFJGgvoLLyZXtSg==} + '@rolldown/binding-linux-x64-gnu@1.1.4': + resolution: {integrity: sha512-JWtGshGfX+oENAKonoNkqEJX+7hC8yfhi9GUyPX1VX4mdh1y5r+ZiJLR5XzAB0aoP6s/PcILsGjKq8O0mm24bw==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [linux] - libc: [glibc] - '@rolldown/binding-linux-x64-musl@1.0.3': - resolution: {integrity: sha512-pSdpdUJHkuCxun9LE7jvgUB9qsRgaiyNNCX7m/AvHTcq67AiT/Yhoxvw5zPfhrM8k/BfP8ce/hMOpthKDpEUow==} + '@rolldown/binding-linux-x64-musl@1.1.4': + resolution: {integrity: sha512-rT6yQcxUuXs4CnbofqwHRRV0iem349rLMYpTjkgQGLjrY4ado/eDzwPZPTCgTOlF6Nkp8NEv70yLMTn6qkWxsQ==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [linux] - libc: [musl] - '@rolldown/binding-openharmony-arm64@1.0.3': - resolution: {integrity: sha512-OXXS3RKJgX2uLwM+gYyuH5omcH8fL1LJs96pZGgtetVCahON57+d4SJHzTgZiOjxgGkSnpXpOsWuPDGAKAigEg==} + '@rolldown/binding-openharmony-arm64@1.1.4': + resolution: {integrity: sha512-KXMGoboq5cyaCQjDA4GLuRiOwBQ0EyFnJoVViLeZ45/3rFItRODEr+NdsBcVpll40hhNArlm/speWGRvj08LzA==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [openharmony] - '@rolldown/binding-wasm32-wasi@1.0.3': - resolution: {integrity: sha512-JTtb8BWFynicNSoPrehsCzBtOKjZ6jhMiPFEmOiuXg1Fl8dn2KHQob+GuPSGR0dryQa1PQJbzjF3dqO/whhjLg==} + '@rolldown/binding-wasm32-wasi@1.1.4': + resolution: {integrity: sha512-5K83rb36oJiY7BCyE9zLZtGcPV4g5wvq+xwdO0XPIwDVZI8cyB/AUjkNXGb92/rnmezEkjMOpgY61rtwjQtFwg==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [wasm32] - '@rolldown/binding-win32-arm64-msvc@1.0.3': - resolution: {integrity: sha512-gEdFFEN70A/jxb2svrWsN3aDL7OUtmvlOy+6fa2jxG8K0wQ1ZbdeLGnidov6Yu5/733dI5ySfzFlQ/cb0bSz1g==} + '@rolldown/binding-win32-arm64-msvc@1.1.4': + resolution: {integrity: sha512-PnWBtw3TV5KOg69HQQDR0mnQuyCmSGR2pAB4DC1rPF808fgKeTUMj2EOEyKATpgiuxuR5APQmiDO7PDgEjTFSA==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [win32] - '@rolldown/binding-win32-x64-msvc@1.0.3': - resolution: {integrity: sha512-eXB7CHuaQdqmJcc3koCNtNPmT/bj2gc999kUFgBxG8Ac0NdgXc4rkCHhqrgrhN3zddvvvrgzj1e90SuSfmyIXA==} + '@rolldown/binding-win32-x64-msvc@1.1.4': + resolution: {integrity: sha512-M1lpniBePobTfsa7Ks9a199e1akxsXn+GYBUKsEzv3YFzOm1HJAMNwKI3qr0Zq+mxwx9gOZoTdP1yXRYsZUocQ==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [win32] - '@rolldown/plugin-babel@0.2.2': - resolution: {integrity: sha512-q9pE8+47bQNHb5eWVcE6oXppA+JTSwvnrhH53m0ZuHuK5MLvwsLoWrWzBTFQqQ06BVxz1gp0HblLsch8o6pvZw==} + '@rolldown/plugin-babel@0.2.3': + resolution: {integrity: sha512-+zEk16yGlz1F9STiRr6uG9hmIXb6nprjLczV/htGptYuLoCuxb+itZ03RKCEeOhBpDDd1NU7qF6x1VLMUp62bw==} engines: {node: '>=22.12.0 || ^24.0.0'} peerDependencies: '@babel/core': ^7.29.0 || ^8.0.0-rc.1 @@ -911,12 +886,6 @@ packages: vite: optional: true - '@rolldown/pluginutils@1.0.0-rc.2': - resolution: {integrity: sha512-izyXV/v+cHiRfozX62W9htOAvwMo4/bXKDrQ+vom1L1qRuexPock/7VZDAhnpHCLNejd3NJ6hiab+tO0D44Rgw==} - - '@rolldown/pluginutils@1.0.0-rc.7': - resolution: {integrity: sha512-qujRfC8sFVInYSPPMLQByRh7zhwkGFS4+tyMQ83srV1qrxL4g8E2tyxVVyxd0+8QeBM1mIk9KbWxkegRr76XzA==} - '@rolldown/pluginutils@1.0.1': resolution: {integrity: sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw==} @@ -929,72 +898,80 @@ packages: peerDependencies: eslint: '>=8.40.0' - '@swc/core-darwin-arm64@1.15.18': - resolution: {integrity: sha512-+mIv7uBuSaywN3C9LNuWaX1jJJ3SKfiJuE6Lr3bd+/1Iv8oMU7oLBjYMluX1UrEPzwN2qCdY6Io0yVicABoCwQ==} + '@swc/core-darwin-arm64@1.15.43': + resolution: {integrity: sha512-v1aVuvXdo/BHxJzco9V2xpHrvwWmhfS8t6gziY5wJxd+Z2h8AeJRnAwPD8itCDaGXVBwJ/CaKfxEzTkG0Va0OA==} engines: {node: '>=10'} cpu: [arm64] os: [darwin] - '@swc/core-darwin-x64@1.15.18': - resolution: {integrity: sha512-wZle0eaQhnzxWX5V/2kEOI6Z9vl/lTFEC6V4EWcn+5pDjhemCpQv9e/TDJ0GIoiClX8EDWRvuZwh+Z3dhL1NAg==} + '@swc/core-darwin-x64@1.15.43': + resolution: {integrity: sha512-lp3d4Lamc8dt5huYdGLSR+9hLxmfr1jb0l+4XXG2zPqZwYWRN9R0U2qYoTrggiU2RWW0oV9VbWM3kBnqIc2kdQ==} engines: {node: '>=10'} cpu: [x64] os: [darwin] - '@swc/core-linux-arm-gnueabihf@1.15.18': - resolution: {integrity: sha512-ao61HGXVqrJFHAcPtF4/DegmwEkVCo4HApnotLU8ognfmU8x589z7+tcf3hU+qBiU1WOXV5fQX6W9Nzs6hjxDw==} + '@swc/core-linux-arm-gnueabihf@1.15.43': + resolution: {integrity: sha512-JWTQQELtsG5GgphDrr/XqqmM2pDN3cZqbMS0Mrg+iTiXL3F74sn/S2IyYE/5u4h2KLkTf9qQ7dXyxsbx7YzkeA==} engines: {node: '>=10'} cpu: [arm] os: [linux] - '@swc/core-linux-arm64-gnu@1.15.18': - resolution: {integrity: sha512-3xnctOBLIq3kj8PxOCgPrGjBLP/kNOddr6f5gukYt/1IZxsITQaU9TDyjeX6jG+FiCIHjCuWuffsyQDL5Ew1bg==} + '@swc/core-linux-arm64-gnu@1.15.43': + resolution: {integrity: sha512-B4otJRdPWIsmiSBf0uG7Z/+vMWmkufjz5MmYxubwKuZazDW14Zd3symga1N62QR4RT+kEFeHEgsXfZGyn/w0hw==} engines: {node: '>=10'} cpu: [arm64] os: [linux] - libc: [glibc] - '@swc/core-linux-arm64-musl@1.15.18': - resolution: {integrity: sha512-0a+Lix+FSSHBSBOA0XznCcHo5/1nA6oLLjcnocvzXeqtdjnPb+SvchItHI+lfeiuj1sClYPDvPMLSLyXFaiIKw==} + '@swc/core-linux-arm64-musl@1.15.43': + resolution: {integrity: sha512-6zB6OnpViBxYy4tgY3v2i6AZY9fwkcHZ032UOwtwUuW1d19sdT07qF0kZe6/3UR1tUaK6jjg2rmVcUIBCEYVjQ==} engines: {node: '>=10'} cpu: [arm64] os: [linux] - libc: [musl] - '@swc/core-linux-x64-gnu@1.15.18': - resolution: {integrity: sha512-wG9J8vReUlpaHz4KOD/5UE1AUgirimU4UFT9oZmupUDEofxJKYb1mTA/DrMj0s78bkBiNI+7Fo2EgPuvOJfuAA==} + '@swc/core-linux-ppc64-gnu@1.15.43': + resolution: {integrity: sha512-coxE1ZWdB3uSDVNoEtYNrRi/1epvckZx9cTJ8ICUxTMTxGk+yvQ/Twacp3ruZSaMPGCriUjP86C37VhaT6nyRg==} + engines: {node: '>=10'} + cpu: [ppc64] + os: [linux] + + '@swc/core-linux-s390x-gnu@1.15.43': + resolution: {integrity: sha512-lXfLhs+LpBsD5inuYx+YDH5WsPPBQ95KPUiy8P5wq9ob9xKDZFqwNfU2QW6bGO8NqRO/H9JQomTSt5Yyh+FGfA==} + engines: {node: '>=10'} + cpu: [s390x] + os: [linux] + + '@swc/core-linux-x64-gnu@1.15.43': + resolution: {integrity: sha512-07XnKwTmKy8TGOZG3D9fRnLWGynxPjwQnZLVmBFbo6F+7vHYzBIOuwXEhemrChBWb6yDNZsVCcMWCPX6FDD2xg==} engines: {node: '>=10'} cpu: [x64] os: [linux] - libc: [glibc] - '@swc/core-linux-x64-musl@1.15.18': - resolution: {integrity: sha512-4nwbVvCphKzicwNWRmvD5iBaZj8JYsRGa4xOxJmOyHlMDpsvvJ2OR2cODlvWyGFH6BYL1MfIAK3qph3hp0Az6g==} + '@swc/core-linux-x64-musl@1.15.43': + resolution: {integrity: sha512-TJc+bsSIaBh+hZvZ5GRtW/K1bw66TJ9vsUwvVIsZdiWxU5ObLwZvfcnZ3UpgVfMnFibRes9uriJrQNBHEEogRQ==} engines: {node: '>=10'} cpu: [x64] os: [linux] - libc: [musl] - '@swc/core-win32-arm64-msvc@1.15.18': - resolution: {integrity: sha512-zk0RYO+LjiBCat2RTMHzAWaMky0cra9loH4oRrLKLLNuL+jarxKLFDA8xTZWEkCPLjUTwlRN7d28eDLLMgtUcQ==} + '@swc/core-win32-arm64-msvc@1.15.43': + resolution: {integrity: sha512-jfd7s2/bUQYkOHLs+LWQNKZdmDa8+sufKLllhpWAhVQ2GDCwsHe3vR/j+OSiItZNtkzFuaawa3+SAKz9y5gYfw==} engines: {node: '>=10'} cpu: [arm64] os: [win32] - '@swc/core-win32-ia32-msvc@1.15.18': - resolution: {integrity: sha512-yVuTrZ0RccD5+PEkpcLOBAuPbYBXS6rslENvIXfvJGXSdX5QGi1ehC4BjAMl5FkKLiam4kJECUI0l7Hq7T1vwg==} + '@swc/core-win32-ia32-msvc@1.15.43': + resolution: {integrity: sha512-rLAE8JvucqEW1ZGohxPQrQWPBQeJG4+ypKbWfdlU/qmKScvCkxf9/Jxnzki1dkUQCQ7P5Enp13RlvqOlvx/32g==} engines: {node: '>=10'} cpu: [ia32] os: [win32] - '@swc/core-win32-x64-msvc@1.15.18': - resolution: {integrity: sha512-7NRmE4hmUQNCbYU3Hn9Tz57mK9Qq4c97ZS+YlamlK6qG9Fb5g/BB3gPDe0iLlJkns/sYv2VWSkm8c3NmbEGjbg==} + '@swc/core-win32-x64-msvc@1.15.43': + resolution: {integrity: sha512-h8MLDHZcfIukwQWj03rIJZx1I0E81AYj2X7J/nGErG4nz+QAv6G1Z+peotvinL3lqpbo32tLYSMFo32/ySzxKg==} engines: {node: '>=10'} cpu: [x64] os: [win32] - '@swc/core@1.15.18': - resolution: {integrity: sha512-z87aF9GphWp//fnkRsqvtY+inMVPgYW3zSlXH1kJFvRT5H/wiAn+G32qW5l3oEk63KSF1x3Ov0BfHCObAmT8RA==} + '@swc/core@1.15.43': + resolution: {integrity: sha512-1CuKjFkPxIgGdeHVuNbkxmBxkcbdc08u0aiI43pFq6yY1tTVKmXT9hFEooyyKs/sJ3xf1GPHyEwTtk9Xl8dvQw==} engines: {node: '>=10'} peerDependencies: '@swc/helpers': '>=0.5.17' @@ -1005,26 +982,26 @@ packages: '@swc/counter@0.1.3': resolution: {integrity: sha512-e2BR4lsJkkRlKZ/qCHPw9ZaSxc0MVUd7gtbtaB7aMvHeJVYe8sOB8DBZkP2DtISHGSku9sCK6T6cnY0CtXrOCQ==} - '@swc/helpers@0.5.19': - resolution: {integrity: sha512-QamiFeIK3txNjgUTNppE6MiG3p7TdninpZu0E0PbqVh1a9FNLT2FRhisaa4NcaX52XVhA5l7Pk58Ft7Sqi/2sA==} + '@swc/helpers@0.5.23': + resolution: {integrity: sha512-5lSsMOTXURePglDfvuAQUqkGek9Hg2kksOYay2m0+XR++b2NWYL/4sWyuvVBIs8oKnJaxkdi9whaL/sqN13afw==} - '@swc/types@0.1.25': - resolution: {integrity: sha512-iAoY/qRhNH8a/hBvm3zKj9qQ4oc2+3w1unPJa2XvTK3XjeLXtzcCingVPw/9e5mn1+0yPqxcBGp9Jf0pkfMb1g==} + '@swc/types@0.1.27': + resolution: {integrity: sha512-K6h3iUlqeM946U4sXFYeahefR1YBbXJvko+hv8WS8/0BNJ4OHiHRywMnQUJCqkR7Y9+hqQ1TvEpiKqUhz7NEFg==} - '@tanstack/eslint-plugin-query@5.91.4': - resolution: {integrity: sha512-8a+GAeR7oxJ5laNyYBQ6miPK09Hi18o5Oie/jx8zioXODv/AUFLZQecKabPdpQSLmuDXEBPKFh+W5DKbWlahjQ==} + '@tanstack/eslint-plugin-query@5.101.2': + resolution: {integrity: sha512-cPE99s3XZwlObfn8lCezT4j4JLj2CVzpIEywx0H4hzfPsX/o9QhdwaOwcDXxrQAqx2ds7TbvTinxhB8B/ywb6w==} peerDependencies: - eslint: ^8.57.0 || ^9.0.0 - typescript: ^5.0.0 + eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 + typescript: ^5.4.0 || ^6.0.0 peerDependenciesMeta: typescript: optional: true - '@tanstack/query-core@5.90.20': - resolution: {integrity: sha512-OMD2HLpNouXEfZJWcKeVKUgQ5n+n3A2JFmBaScpNDUqSrQSjiveC7dKMe53uJUg1nDG16ttFPz2xfilz6i2uVg==} + '@tanstack/query-core@5.101.2': + resolution: {integrity: sha512-hH5MLoJhF7KaIGd7q3xTXGXvslI+GYlM1Z/35aSHHWaCJWB7XvTSHYuV3eM7tw+aE0mT/xMro4M4Q9rCGHT0lw==} - '@tanstack/react-query@5.90.21': - resolution: {integrity: sha512-0Lu6y5t+tvlTJMTO7oh5NSpJfpg/5D41LlThfepTixPYkJ0sE2Jj0m0f6yYqujBwIXlId87e234+MxG3D3g7kg==} + '@tanstack/react-query@5.101.2': + resolution: {integrity: sha512-seDkr6kzGzX1okaaTtZPtgA688CDPlXUz1C6xSg0ESqn04Vuc8tlrYms1s3de+znBqhPVxFRfpAfUf+6XvfPWg==} peerDependencies: react: ^18 || ^19 @@ -1035,8 +1012,8 @@ packages: react: '>=16.8' react-dom: '>=16.8' - '@tanstack/react-virtual@3.13.21': - resolution: {integrity: sha512-SYXFrmrbPgXBvf+HsOsKhFgqSe4M6B29VHOsX9Jih9TlNkNkDWx0hWMiMLUghMEzyUz772ndzdEeCEBx+3GIZw==} + '@tanstack/react-virtual@3.14.5': + resolution: {integrity: sha512-4EKRXh7zBLkbKbFmG3AUVkircuHd+7OdT1pocJSepxtfBd3qnrJgJ5rtPkRYyo9fmyVb2+pI2xPy5oYvMLQy6A==} peerDependencies: react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 @@ -1045,8 +1022,8 @@ packages: resolution: {integrity: sha512-ldZXEhOBb8Is7xLs01fR3YEc3DERiz5silj8tnGkFZytt1abEvl/GhUmCE0PMLaMPTa3Jk4HbKmRlHmu+gCftg==} engines: {node: '>=12'} - '@tanstack/virtual-core@3.13.21': - resolution: {integrity: sha512-ww+fmLHyCbPSf7JNbWZP3g7wl6SdNo3ah5Aiw+0e9FDErkVHLKprYUrwTm7dF646FtEkN/KkAKPYezxpmvOjxw==} + '@tanstack/virtual-core@3.17.3': + resolution: {integrity: sha512-8Np/TFELpI0ySuJoVmjvOrQYXH/8sTX0Biv9szhFhY39xOdAAY+smrMxjxOum/ux3eM8MUJQsEJ0/R0UpvC8dw==} '@testing-library/dom@10.4.0': resolution: {integrity: sha512-pemlzrSESWbdAloYml3bAJMEfNh1Z7EduzqPKprCH5S341frlpYnUEW0H72dLxa6IsYr+mPno20GiSm+h9dEdQ==} @@ -1083,14 +1060,14 @@ packages: '@ts-morph/common@0.28.1': resolution: {integrity: sha512-W74iWf7ILp1ZKNYXY5qbddNaml7e9Sedv5lvU1V8lftlitkc9Pq1A+jlH23ltDgWYeZFFEqGCD1Ies9hqu3O+g==} - '@tybys/wasm-util@0.10.2': - resolution: {integrity: sha512-RoBvJ2X0wuKlWFIjrwffGw1IqZHKQqzIchKaadZZfnNpsAYp2mM0h36JtPCjNDAHGgYez/15uMBpfGwchhiMgg==} + '@tybys/wasm-util@0.10.3': + resolution: {integrity: sha512-F3fo1MYrRJYL3zER0OUOmkutjr1Vp23m7OsSgp7nq4SP6OqX6C/56XFIPAl5bt3zaBRjmW7SGz3u/6LwFpYcOg==} '@types/aria-query@5.0.4': resolution: {integrity: sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw==} - '@types/chai@5.2.2': - resolution: {integrity: sha512-8kB30R7Hwqf40JPiKhVzodJs2Qc1ZJ5zuT3uzw5Hq/dhNCl3G3l83jfpdI1e20BP348+fV7VIL/+FxaXkqBmWg==} + '@types/chai@5.2.3': + resolution: {integrity: sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==} '@types/culori@4.0.1': resolution: {integrity: sha512-43M51r/22CjhbOXyGT361GZ9vncSVQ39u62x5eJdBQFviI8zWp2X5jzqg7k4M6PVgDQAClpy2bUe2dtwEgEDVQ==} @@ -1182,8 +1159,8 @@ packages: '@types/ms@2.1.0': resolution: {integrity: sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==} - '@types/node@24.10.3': - resolution: {integrity: sha512-gqkrWUsS8hcm0r44yn7/xZeV1ERva/nLgrLxFRUGb7aoNMIJfZJ3AC261zDQuOAKC7MiXai1WCpYc48jAHoShQ==} + '@types/node@24.13.2': + resolution: {integrity: sha512-fRa09kZTgu8o71KFcDjUFuc7F+dEbZYZmkI0mg5YBTRs0yMKjYHsq/c0urDKeDb+D5qVgXOdFcuu+DZPKOITwA==} '@types/parse-json@4.0.2': resolution: {integrity: sha512-dISoDXWWQwUquiKsyZ4Ng+HX2KsPL7LyHKHQwgGFEA3IaKac4Obd+h2a/a6waisAoepJlBcx9paWqjA8/HVjCw==} @@ -1201,12 +1178,18 @@ packages: peerDependencies: '@types/react': '*' - '@types/react@19.2.15': - resolution: {integrity: sha512-eRwcGNHve+E8qtEQSSRl6urh+rFop4v8gm6O8rGv25CodbvFdLjA1vVQ1KkiFE0w0UPOnb8tDiFKL5lp0rtY5Q==} + '@types/react@19.2.17': + resolution: {integrity: sha512-MXfmqaVPEVgkBT/aY0aGCkRWWtByiYQXo3xdQ8r5RzuFrPiRn8Gar2tQdXSUQ2GKV3bkXckek89V8wQBY2Q/Aw==} + + '@types/set-cookie-parser@2.4.10': + resolution: {integrity: sha512-GGmQVGpQWUe5qglJozEjZV/5dyxbOOZ0LHe/lqyWssB88Y4svNfst0uqBVscdDeIKl5Jy5+aPSvy7mI9tYRguw==} '@types/statuses@2.0.6': resolution: {integrity: sha512-xMAgYwceFhRA2zY+XbEA7mxYbA093wdiW8Vu6gZPGWy9cmOyU9XesH1tNcEWsKFd5Vzrqx5T3D38PWx1FIIXkA==} + '@types/trusted-types@2.0.7': + resolution: {integrity: sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==} + '@types/unist@2.0.11': resolution: {integrity: sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA==} @@ -1219,63 +1202,63 @@ packages: '@types/ws@8.18.1': resolution: {integrity: sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg==} - '@typescript-eslint/eslint-plugin@8.60.0': - resolution: {integrity: sha512-QYb/sa74/s7OKMbACMjrYnGspj9Hs5YI5aaffSL65UfeBUzVzBJfVo3oWSpbzPurvm7yaCCo2Lk7lVj610HqKw==} + '@typescript-eslint/eslint-plugin@8.62.1': + resolution: {integrity: sha512-4EQM77WgVNxj7OkL/5b/D/xZsw00G577+UriYTC7JF5opcF3T2AuoeY7ueLaZgSVjSgCS6yOAJB5bRGLPSJUzA==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: - '@typescript-eslint/parser': ^8.60.0 + '@typescript-eslint/parser': ^8.62.1 eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 typescript: '>=4.8.4 <6.1.0' - '@typescript-eslint/parser@8.60.0': - resolution: {integrity: sha512-fcqpj/MyK4sxDPcbe7STNPbpQL4RLZOPWuaTmwZYuc+hJKzRf58yRxfhqGpc6PIq9ZyfSBpfHgmUHmHs0KwHwg==} + '@typescript-eslint/parser@8.62.1': + resolution: {integrity: sha512-sPhE4iHuJDSvoAiec+Ro8JyXw8f0ql13HFR82P99nCm9GwTEKG0KYLvDe6REk8BCXuit6vJAv/Yxg5ABaNS2rA==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 typescript: '>=4.8.4 <6.1.0' - '@typescript-eslint/project-service@8.60.0': - resolution: {integrity: sha512-aZu74NNKJeUWqCjDddzdiKaS82dgYgV/vmf+Ui3ZdZejmgfXR/q+pRumgobnQ2cCJTgGTWp4ypiwsuofFubavg==} + '@typescript-eslint/project-service@8.62.1': + resolution: {integrity: sha512-yQ3RgY5RkSBpsNS1Bx/JQEcA24FOSdfGktoyprAr5u18390UQdtVcfnEv4nIrIshNnavlVyZBKxQwT1fIAE6cg==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: typescript: '>=4.8.4 <6.1.0' - '@typescript-eslint/scope-manager@8.60.0': - resolution: {integrity: sha512-pFzqhllJMs+jghLQWzV00ds39xLzuyqPSev5pd8f4Ir0rtKR3ZLUB4/4dhjOFighWb9larvtfJvqL+4yKDI3Xw==} + '@typescript-eslint/scope-manager@8.62.1': + resolution: {integrity: sha512-r4d249KbQ1SFdpeStvob8Ih6aPPIzfqllPVOtvhve6ZcpuVcYo5/7zUWckKpHE7StASX4kTKZTLf0WQm/wPkcg==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - '@typescript-eslint/tsconfig-utils@8.60.0': - resolution: {integrity: sha512-BZPR3RGYlAXnly6ymAxfkVn5rCbZzQNou0rxv3GfWZ8cTQp+hhVd73khbGLAd8k1TlAPLISH337M+tAgAnaJDQ==} + '@typescript-eslint/tsconfig-utils@8.62.1': + resolution: {integrity: sha512-xadytJqX9vJVQ2fdQjkcIVigwaOJNWkpjdLt6cEQ+xPnrI1fkp+/jZE/I97k9KUjqtpd25i0HeyZf3T6dutv2g==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: typescript: '>=4.8.4 <6.1.0' - '@typescript-eslint/type-utils@8.60.0': - resolution: {integrity: sha512-SX46wEUtitCpq7AN38HkUU/+zvUpdKf7ephtWAFgckH8O7PQIyL5gvrhQgBLuEYgLfuKWOVvWVskMbuFHAz5xg==} + '@typescript-eslint/type-utils@8.62.1': + resolution: {integrity: sha512-aXM5xlqXiTxPibXB93cLAURfT3rlizf7uMXISCXy66Isr/9hISJx3yDsKl0L7lKa51b8JpFuNKby0/O0pEm9jg==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 typescript: '>=4.8.4 <6.1.0' - '@typescript-eslint/types@8.60.0': - resolution: {integrity: sha512-AsE7x2XaAK+CVbeih0Fvbn+r1qHxtpLDJ3XUuFcIinT318T90yHMJC+Zgv+jUuDjQQd06HKwxnDu6sz1IcTilA==} + '@typescript-eslint/types@8.62.1': + resolution: {integrity: sha512-ooCzJFaf+Hg+uG6fA3NRFGuFjlfNlDhBthbv4ZPU/0elCAFUfnyXUvf/WOpHz/jYwSmvU2GkR2LtyUfy1AxZ1Q==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - '@typescript-eslint/typescript-estree@8.60.0': - resolution: {integrity: sha512-3AcZNBGMClm6CXDyo8kYvVGT/sx29sS0oBsIb9oZI2gunA4Vm2M3YHzRLPvsUBBsl+yB5FPtltq7gGH0iTlp9g==} + '@typescript-eslint/typescript-estree@8.62.1': + resolution: {integrity: sha512-xMcW9oP9u7fAMXYs9A65CVmtLQe2r//oXINHfi8HV+oiqhih17sbLdhXr4540YWlgpDKQdY854OL5ZrdCiQsAA==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: typescript: '>=4.8.4 <6.1.0' - '@typescript-eslint/utils@8.60.0': - resolution: {integrity: sha512-HtXuPfrHTyBDkameWpl+vJb1Uevu2tznAyahM1Oc4AENidCLTPiZDWIo4GfcxNdC/RcfGcadzzkqbRG87dUrQA==} + '@typescript-eslint/utils@8.62.1': + resolution: {integrity: sha512-sHtbPfuKNZCG+ih8SyjjucqRntSVmp8XgL5u6o9mAhiSn8ds5o/M/XdM0abweme2Tln3szOstOrZ9OXitvPh0g==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 typescript: '>=4.8.4 <6.1.0' - '@typescript-eslint/visitor-keys@8.60.0': - resolution: {integrity: sha512-9WI52t8ZGLVGrPMBet25yAftqY/n95+zmoUUtJBBQTKDSKUu7OsPTroT2op7U9JatkoRccL0YkWDNMFfC4Sjxg==} + '@typescript-eslint/visitor-keys@8.62.1': + resolution: {integrity: sha512-4g3BLxfdTMy8iZG0MaBkadnlRrCJ74cQiFbyEVMrkwIoqdyaXXQM22cotDvrl4x28wgIZ9rEJRoM+mmhSJpJ1g==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} '@ungap/structured-clone@1.3.0': @@ -1301,14 +1284,14 @@ packages: '@visx/vendor@3.12.0': resolution: {integrity: sha512-SVO+G0xtnL9dsNpGDcjCgoiCnlB3iLSM9KLz1sLbSrV7RaVXwY3/BTm2X9OWN1jH2a9M+eHt6DJ6sE6CXm4cUg==} - '@vitejs/plugin-react-swc@4.2.3': - resolution: {integrity: sha512-QIluDil2prhY1gdA3GGwxZzTAmLdi8cQ2CcuMW4PB/Wu4e/1pzqrwhYWVd09LInCRlDUidQjd0B70QWbjWtLxA==} + '@vitejs/plugin-react-swc@4.3.1': + resolution: {integrity: sha512-PaeokKjAGraNN+s5SIApgsktnJprIyt3zgEIu7awnEdfn29QiB2crTcCzyi2XGpX9rUnTc0cKU07Wm0N0g7H2w==} engines: {node: ^20.19.0 || >=22.12.0} peerDependencies: - vite: ^4 || ^5 || ^6 || ^7 + vite: ^4 || ^5 || ^6 || ^7 || ^8 - '@vitejs/plugin-react@6.0.1': - resolution: {integrity: sha512-l9X/E3cDb+xY3SWzlG1MOGt2usfEHGMNIaegaUGFsLkb3RCn/k8/TOXBcab+OndDI4TBtktT8/9BwwW8Vi9KUQ==} + '@vitejs/plugin-react@6.0.3': + resolution: {integrity: sha512-vmFvco5/QuC2f9Oj+wTk0+9XeDFkHxSamwZKYc7MxYwKICfvUvlMhqKI0VuICPltGqh1neqBKDvO4kes1ya8vg==} engines: {node: ^20.19.0 || >=22.12.0} peerDependencies: '@rolldown/plugin-babel': ^0.1.7 || ^0.2.0 @@ -1320,20 +1303,20 @@ packages: babel-plugin-react-compiler: optional: true - '@vitest/coverage-v8@4.1.4': - resolution: {integrity: sha512-x7FptB5oDruxNPDNY2+S8tCh0pcq7ymCe1gTHcsp733jYjrJl8V1gMUlVysuCD9Kz46Xz9t1akkv08dPcYDs1w==} + '@vitest/coverage-v8@4.1.9': + resolution: {integrity: sha512-G9/lgqibheLVBDRuya45EbsEXTYcWoSG+TLg7i2axuzx0Eq62eXn+aWXyaVdV5vKvFSWd6ywcX8hA7la9Pvu8g==} peerDependencies: - '@vitest/browser': 4.1.4 - vitest: 4.1.4 + '@vitest/browser': 4.1.9 + vitest: 4.1.9 peerDependenciesMeta: '@vitest/browser': optional: true - '@vitest/expect@4.1.4': - resolution: {integrity: sha512-iPBpra+VDuXmBFI3FMKHSFXp3Gx5HfmSCE8X67Dn+bwephCnQCaB7qWK2ldHa+8ncN8hJU8VTMcxjPpyMkUjww==} + '@vitest/expect@4.1.9': + resolution: {integrity: sha512-vl/rYsUKcBr3SnQn166+XR5ZQcgMx3DQhFWdfli/cWpLnLUmbxZvyrJZotLFUryib+LtArYMSTJ5RbQ57ZqrlA==} - '@vitest/mocker@4.1.4': - resolution: {integrity: sha512-R9HTZBhW6yCSGbGQnDnH3QHfJxokKN4KB+Yvk9Q1le7eQNYwiCyKxmLmurSpFy6BzJanSLuEUDrD+j97Q+ZLPg==} + '@vitest/mocker@4.1.9': + resolution: {integrity: sha512-EVkXzBjrPGM+cK8/ANWgBrkUCfJfb38/EfTSO8h7pWvKkyPkpWxvR7BkD2MyItMF62C97zAEoqdpUixwR/e+Rw==} peerDependencies: msw: ^2.4.9 vite: ^6.0.0 || ^7.0.0 || ^8.0.0 @@ -1343,258 +1326,270 @@ packages: vite: optional: true - '@vitest/pretty-format@4.1.4': - resolution: {integrity: sha512-ddmDHU0gjEUyEVLxtZa7xamrpIefdEETu3nZjWtHeZX4QxqJ7tRxSteHVXJOcr8jhiLoGAhkK4WJ3WqBpjx42A==} + '@vitest/pretty-format@4.1.9': + resolution: {integrity: sha512-s0iufns3iIFitdgm+YR7g1whCAaGtXz459VS9/PqyKDEEFgYIhsHOQmXgIgDuYCt7DeQmiZT0Qe2OA2p4ZPu5A==} - '@vitest/runner@4.1.4': - resolution: {integrity: sha512-xTp7VZ5aXP5ZJrn15UtJUWlx6qXLnGtF6jNxHepdPHpMfz/aVPx+htHtgcAL2mDXJgKhpoo2e9/hVJsIeFbytQ==} + '@vitest/runner@4.1.9': + resolution: {integrity: sha512-KXLMDtc7oe70+3mJfGrPUWPesswH+3sTxAMAMl8DG7I8IUQT4XW718dY5ID3vPUcmlu27CcKfY4P3h3I29SLJg==} - '@vitest/snapshot@4.1.4': - resolution: {integrity: sha512-MCjCFgaS8aZz+m5nTcEcgk/xhWv0rEH4Yl53PPlMXOZ1/Ka2VcZU6CJ+MgYCZbcJvzGhQRjVrGQNZqkGPttIKw==} + '@vitest/snapshot@4.1.9': + resolution: {integrity: sha512-Jc7RKGNBo8Z28WYIm0Niej4xdSPByRf6mU58VpHQkd6Zh05rlnA+twjbK5HyeIGHxrzsc3mJgS43uM0CZKzaIA==} - '@vitest/spy@4.1.4': - resolution: {integrity: sha512-XxNdAsKW7C+FLydqFJLb5KhJtl3PGCMmYwFRfhvIgxJvLSXhhVI1zM8f1qD3Zg7RCjTSzDVyct6sghs9UEgBEQ==} + '@vitest/spy@4.1.9': + resolution: {integrity: sha512-fHpsS6mIi+PiEW+vcRVOMkX1oSaPKne3VOclSFICPcGOmfKgXPU5iAah+wcNcj2xPrCCmfq99IDGf+EojhhvhA==} - '@vitest/utils@4.1.4': - resolution: {integrity: sha512-13QMT+eysM5uVGa1rG4kegGYNp6cnQcsTc67ELFbhNLQO+vgsygtYJx2khvdt4gVQqSSpC/KT5FZZxUpP3Oatw==} + '@vitest/utils@4.1.9': + resolution: {integrity: sha512-A51o8ymO5PpqlWNnBP9ZHPXDIpuMtTLlGSjN7la4US+LJzoUMyhwjA5QXlm39JexgwHKW4Xjs8Z2d3dLCXOeuA==} - '@xyflow/react@12.10.1': - resolution: {integrity: sha512-5eSWtIK/+rkldOuFbOOz44CRgQRjtS9v5nufk77DV+XBnfCGL9HAQ8PG00o2ZYKqkEU/Ak6wrKC95Tu+2zuK3Q==} + '@xyflow/react@12.11.1': + resolution: {integrity: sha512-L+zBoLGSXham0MnlY8QqjfR7/C5JNw0zxkaey5aZ5XmCgJBAdH4+WRIu8CR40d3l/BdU635V6YbhBK1jMo8/6Q==} peerDependencies: + '@types/react': '>=17' + '@types/react-dom': '>=17' react: '>=17' react-dom: '>=17' + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@xyflow/system@0.0.78': + resolution: {integrity: sha512-lY0z2qP33fUhTva9Vaxrk0lqZta2pkbxB1trHAx1omnJqRtPvDlAQYV2r5fhS6AdpkulYmbNW0svy+A4/t4B/g==} - '@xyflow/system@0.0.75': - resolution: {integrity: sha512-iXs+AGFLi8w/VlAoc/iSxk+CxfT6o64Uw/k0CKASOPqjqz6E0rb5jFZgJtXGZCpfQI6OQpu5EnumP5fGxQheaQ==} + '@zag-js/accordion@1.41.2': + resolution: {integrity: sha512-7G//V7svGGT8k5avw7bbQvbRC0Q/9QtX51b4iyAB1alR9E5mFd6Ch8q4njwcXClMQ7xePS3jUfVnzVGiRInEiQ==} - '@zag-js/accordion@1.35.3': - resolution: {integrity: sha512-wmw6yo5Zr6ShiKGTc5ICEOJCurWAOSGubIpGISiHi3cZ4tlxKF/vpATIUT3eq8xzdB56YK57yKCujs/WmwqqoA==} + '@zag-js/anatomy@1.41.2': + resolution: {integrity: sha512-Fm9hqdrvaCzCsdcf19G8WZxYtHElKltkGHdhqMEt4XU+ULTr1DK7KbOtDDv9J27CuzqSLALUz5QfRjPftoKHwg==} - '@zag-js/anatomy@1.35.3': - resolution: {integrity: sha512-oqU9iLNNylrtJMBX5Xu4DsxnPNvtZLiobryv2oNtsDI1mi1Fca/XHghQC9K5aYT0qNsmHj1M3W5WAWTaOtPLkQ==} + '@zag-js/angle-slider@1.41.2': + resolution: {integrity: sha512-+7bZHAZx0MEbjTMr2tD+meFJ0EJwFfUEcTqmdLzFGr/ySAMCWlcadDBz+ZmrSn03aKLps8FxliVLzsFJNgUqIQ==} - '@zag-js/angle-slider@1.35.3': - resolution: {integrity: sha512-HXRlmsbNEJSBT53fq9XQKL/vwZWwJC3nprskI7s4f/jy8a4uXPTlv7N7zuBYjew+ScTMzZah6fLWzUztBehmSg==} + '@zag-js/aria-hidden@1.41.2': + resolution: {integrity: sha512-qEcYmwlQr3qjA0T/IZ5a/o7fRUxfQ14tXjAFhR3GXCtxBKaqS+wnq/LN09Xw4bin3QWTINU+Z0oXFs9spWtNwA==} - '@zag-js/aria-hidden@1.35.3': - resolution: {integrity: sha512-dk5POebn10WneQfLrEgbTzwolaXWpCSHL6F3jCTinW9IbOx7BXghzJD21iU5Iun+y9CorqJPW3p7LplYNUMO5Q==} + '@zag-js/async-list@1.41.2': + resolution: {integrity: sha512-NZZEIGFdeDp2uHjsLVegLAGJOYGwI9HPJI1V2c/P1TQmfmrfWyWELAvnnW4kWYVUKYD9TxKQkm6LvqpHrQzgfQ==} - '@zag-js/async-list@1.35.3': - resolution: {integrity: sha512-SXX3wGzLK/maKS1PJ3XfLIGWbu0022f/OhcFsT1PbiHnoFZTH7h2fBhirrCBfy2TYFQ6r5uxgjkhPUNkuaeYnA==} + '@zag-js/auto-resize@1.41.2': + resolution: {integrity: sha512-CYq+JQ1TTkEiK7OcNcMTS0f4wFvtmManvUltije5o50gDQ7vFYA81oQh4A9pAB1keUi9Zv06CNefFARaTcne5w==} - '@zag-js/auto-resize@1.35.3': - resolution: {integrity: sha512-ufG8HSqzLd9h5rnos8aumj8iORlRskeR/gbpJu1NHrnHBWIrpuXm6KJJR2oZhTFY1BUMMk8eYIBA2QkVuiJzWA==} + '@zag-js/avatar@1.41.2': + resolution: {integrity: sha512-+4K0tRtIQysMGuERh5JRmn46uq7gJ6IjJd5DKj74VBtVVY8T6HGk0D0DZxmOIgADHP1qSvowLDoOX8kUyBHarQ==} - '@zag-js/avatar@1.35.3': - resolution: {integrity: sha512-lbQ2Q4Va8AAScKULOHw2tCQez+0JRYGHSMFq6i+dJmeT3dlSgRanm69ra6K2po6hM9E4v6pRe+xOVE+9QMDnuA==} + '@zag-js/carousel@1.41.2': + resolution: {integrity: sha512-H6sDxyWIzkvtHN4M/BYvFy7pi8j+kLEtfPZvgNl2+gRnfAHVK9/XqpoUsjw1DAo5Fh25pPuaTnh52Kml9OK0iA==} - '@zag-js/carousel@1.35.3': - resolution: {integrity: sha512-F+b8HzUeZfB+xUkAkLG4r0Ubui8pj7pSgZhi26ZiWgsM7tsd7cD+xRMXkvPEITN5Fd5QCe3KlVBuE00w5byjmg==} + '@zag-js/cascade-select@1.41.2': + resolution: {integrity: sha512-dBByZmIAJU/B+YIAzczng05lCJXEwEm4df6GmUZtioKHZdeN+WEKUP4qzFDFZdytXympGfJCIBvtgf87gACYVQ==} - '@zag-js/cascade-select@1.35.3': - resolution: {integrity: sha512-Nifdx77hEuAdXqr1wpZSPjLXqygRhq/WvnPjGhCeSqFPpy62uT4JZ3avyjUZ4I0UhvIpkleUcXtFwQ3cSMh4ww==} + '@zag-js/checkbox@1.41.2': + resolution: {integrity: sha512-aQ5dZWHUHRIw4cbLtzrR+dc8M0N6wSiiCml/zbU3ciHOTBXSrs2rKnp/L99xhi97Lj2uCEhpIZ704Ou/MjGuCg==} - '@zag-js/checkbox@1.35.3': - resolution: {integrity: sha512-8XBt/Wg2zSQWqV2ZFqZBQUjYRkOYHA2O3IEi0VVYtds3S1n7Pu/HqkZT5qDw+E/SY2+X9Uyx4hO7h2XrlsiZQQ==} + '@zag-js/clipboard@1.41.2': + resolution: {integrity: sha512-FM+PNGEeY+YpF1dVr9d8kg3DQM66LRnkR4Mva1oprqnrCXTYWj+k7uig893ubSG2xw1GO5b/WJd27kSmNoKnmw==} - '@zag-js/clipboard@1.35.3': - resolution: {integrity: sha512-obTwynBpp6c17fLHe5tg//FQ497QsyCEry+K3bTdlrivWW200wvfHxZ6RKVbKwDAwhH+ye0bI1xkYAId8j7sdA==} + '@zag-js/collapsible@1.41.2': + resolution: {integrity: sha512-uMIp4rqd3iI6VFPMAOcbu9dh9WV4l85nNPYQiBtMKRHgbkfaZdfzF+E+NX3KquIeHW6JiBFUiIyCU0Sf2Cscdw==} - '@zag-js/collapsible@1.35.3': - resolution: {integrity: sha512-IweG8JOBCerJwLO6QzTZGEMlsYUmQfQSeD0jniFguMM8vcunvGVSrM+AaL8pDbmXd+snXokaGyJpGO3vzMW6Fw==} + '@zag-js/collection@1.41.2': + resolution: {integrity: sha512-ZZWuvfPZI8ccWd4aLpuU47k1jSc9eO+F3FM3iBJuvgegCH6g3+HDEwGN6wdePHnYfv7zyIKCGKr816zXcIWQbw==} - '@zag-js/collection@1.35.3': - resolution: {integrity: sha512-BYoWJ4b7ma2PgiuQbRSnP603f2DlK6se5JtViUHTamZScLLLWnWHuQ6zFa1KS5kiIkbb7CFM6/bJ3WNYLch8Ig==} + '@zag-js/color-picker@1.41.2': + resolution: {integrity: sha512-UynyJ/bTBeSlq6ziHr9GVrFycDjVxfLFIb8Aivu/XvihrAAvkhXUxwy+1ax+hj7peGlTY590xoQAvQixV+McBA==} - '@zag-js/color-picker@1.35.3': - resolution: {integrity: sha512-i9roSgtqeA1b4Q+jWqnxjXB//BQXMP5m1FQ4YcZVq/0yT14A53JIknchuqrh3wC3yPsJMXFqCoKg+NET2+OVig==} + '@zag-js/color-utils@1.41.2': + resolution: {integrity: sha512-Lsi5c2ztGZqud0oeDtAn3xFhgrYMaeaz1zi4p+mr0zXOuQNuZzfsrJ0stQ45s8L/xT8UJtGhYyEVy1+xjE4ARA==} - '@zag-js/color-utils@1.35.3': - resolution: {integrity: sha512-vxkEVgz4YdSbdaPvjiRI1VsJAdwzu/dUNvzqOaiVcPDrHr/FFgmUbv0SOFjnfSb2QWGI8EDEMn02RW9ym+BzGw==} + '@zag-js/combobox@1.41.2': + resolution: {integrity: sha512-V9jQteyQHs8ZNQx/FcA98P1NVZas/yjiW1V7s4L+h8MnRS3AK97+FAHAT83KCiZ34/vkpLF6ZEHI2zkcQpNXcg==} - '@zag-js/combobox@1.35.3': - resolution: {integrity: sha512-s1qmttTGJTMjlDakL+uvWSEggpafKr1vhOeZCh8j+N4eFt9bLAwaffjuh/1JzWBvzovw7WoMVkizdTXPlN8oYg==} + '@zag-js/core@1.41.2': + resolution: {integrity: sha512-xXTN3zKwOtMI4+5dG2cG+T1B4WR3X9alXiYaPJKaGd4N2eYRj9JEPte3Hv9gtFm+RM0b9VIwksHEg25rqE4apw==} - '@zag-js/core@1.35.3': - resolution: {integrity: sha512-fGAHyqOYSEFmo52t7wI4dvbFfLyJmUlyf7wknsiUlzUHlrn3yv5PAZYZ2TibpOD1hwXIp4AoCjbiIPPZBxirZw==} + '@zag-js/date-input@1.41.2': + resolution: {integrity: sha512-J8PdpEpM9TBxZBEE8/Nxi39LNJOZY7lilTbgcDABR5uiviZUk5++5GSdUTspcjFUIXx5SvqmdCk2RF7hsUEHVQ==} + peerDependencies: + '@internationalized/date': '>=3.0.0' - '@zag-js/date-picker@1.35.3': - resolution: {integrity: sha512-4G10h6pzzLbd84SE2CKtqi6Z9wEBhSyx4GRSxxy3tsf5wAxnz4anRFat9CGwn2YVUYcUJpD+umYgBMPt6zGDnA==} + '@zag-js/date-picker@1.41.2': + resolution: {integrity: sha512-j9LaznCV4QCfrq/y/mNAN9XgIRFFg+414TxGT4TK8mfWouIaFvxEoKdGP0l/LL4DFv1NRDaRdSBBcw3eXtMVnA==} peerDependencies: '@internationalized/date': '>=3.0.0' - '@zag-js/date-utils@1.35.3': - resolution: {integrity: sha512-1co0FPpZ6nO5dN8sZtECkMYaf+3E5zu0KSIJZpZiXb4TgsZMDyHu7K7IsiKFHk9qmhuF6AdPpNxBju91pSXMFg==} + '@zag-js/date-utils@1.41.2': + resolution: {integrity: sha512-jdcWLa5fYLTsvGWJkx4v/9Hzqf6UHdvJDeP6NxHpwoEmzYy7+AghYKBNSnRrJIBCQJgl1vM9OkpMvYrLNHr9jw==} peerDependencies: '@internationalized/date': '>=3.0.0' - '@zag-js/dialog@1.35.3': - resolution: {integrity: sha512-byosV+aBHH5LoFKnjEgC7WdqJid7bP9UhgWLSC7+IXbxrif9Czg1YVp6ZlQM6Nx6uD1vnty4touI3P7D7CTKcw==} + '@zag-js/dialog@1.41.2': + resolution: {integrity: sha512-t1N72snpFGiKj7cg2PivPdsy+X1YdgXPv7bzovpqjMLy0kN+gYTPoV1Iy/hQA+g021dz9XGgXzkDuU45sJqsFA==} - '@zag-js/dismissable@1.35.3': - resolution: {integrity: sha512-XPk+lqmsZp2Z1yMb5K1yj/e7Sobv4D7zK66B1GS97lk9Xzz8vuSgsimcLy0p7RXQl3KL6H5L69inSuQa2exybQ==} + '@zag-js/dismissable@1.41.2': + resolution: {integrity: sha512-hO/tFhRZ7S+LOOljGOQJIubbc3MXg41+iWR1yUXKl76cAenbxaCit1LZmUCwQPvRN0GndK6bDQo5ETjHZz/k7A==} - '@zag-js/dom-query@1.35.3': - resolution: {integrity: sha512-1RbFZoT4CjlHN9TUNse1++ZVOyKo45ktucTIT349o6HMsoWWKmTJDPvFkMBbmu/qY6XXn4dT+LJEp4bL3DR+Qw==} + '@zag-js/dom-query@1.41.2': + resolution: {integrity: sha512-+eBk1nlJA312mNmY/GSThLRwcCRqMIL+A1pLsWvTlQLQjmH1/UxoAuv6l2yvRCT33XmC8FBlBIKnXhOCpDvIZA==} - '@zag-js/drawer@1.35.3': - resolution: {integrity: sha512-DN5bwa7bDCDaUSbNzFxMc2U/WmbLcXvPSQjyOpKI6CC3VbW2kKaOnjJ5qQG+W5YBO0FpmJBtaxRV7lke4sZH2w==} + '@zag-js/drawer@1.41.2': + resolution: {integrity: sha512-aJql6L0cfHd1wXbemfLcNnjqTA/CbwVgQdWEVn5Qci6zhy4UJXPQeBBfxfgPgEOAbW60gL/gaaXG3d9Vx6+6oA==} - '@zag-js/editable@1.35.3': - resolution: {integrity: sha512-HcjeacS61vQXfNT9IalZj/+oS45yW5bIDO2NjJWV7zNe5AG29NCceUnvBhy+hrUKPnKcjfDocdW5rCL+Lvs/CQ==} + '@zag-js/editable@1.41.2': + resolution: {integrity: sha512-loTM1lrHBBqYfR8SJZrYayVdWHMpXCLVir+aDTQ8/d4bb/Gfl/L8CJNs3BRj3yt9zvLoWTLO+4LOY3hLyQSR2w==} - '@zag-js/file-upload@1.35.3': - resolution: {integrity: sha512-oIYwnDct4ERo2mfmcxsBIJnlmpzjrzYx82SQsXWD3NGKx3cgdh2lwBX+ebItaLH1jkgzBa3z0TWxc6rfvcUXbw==} + '@zag-js/file-upload@1.41.2': + resolution: {integrity: sha512-WFwIaKvHpCUjyYMvp42VoydT1WIP5DhDlpmG/nrF4i0ro7pLGb1A4dvyBrAfGcozCB88yR1y1iZKPDY1I9/uUw==} - '@zag-js/file-utils@1.35.3': - resolution: {integrity: sha512-Tb05RCzx4swc156hd4jLiO7z+Gxg/HQ+JCds03jgTbrFJAz2D56YaMeI7gSDc1m4Xre3nyqQpSo9AeX5nzbE/w==} + '@zag-js/file-utils@1.41.2': + resolution: {integrity: sha512-Ih+8ULbId0M+CFR4IsqG5y/0VLCk2l+1rgPH+21L40dlSB6z6qKSP2tG7W69Cj2/3vryZsn67ibn26iCPG/vOw==} - '@zag-js/floating-panel@1.35.3': - resolution: {integrity: sha512-nTZypcS0X46Oo1kpCQTnP5UlzjhypOAj3B4dq2z/3bAOC0TntYTnFkj8PbEJtExk7364xfMyxfgZOiv7Aqq01w==} + '@zag-js/floating-panel@1.41.2': + resolution: {integrity: sha512-nJP3oZ4YrJh+7H5YdwNccSzPGXFqjQN1ujZ/xGDhegjz4XtL48QMFnAasxlRI8VGjse9Tj8VXlQZxXPrASGzlA==} - '@zag-js/focus-trap@1.35.3': - resolution: {integrity: sha512-evErLlGFdDVCI8xipNS5k0rAvO+KFRA9g273bbfWAL1+mT54mcB/XHa85nC3QpPgMNrSh+6LUNq9fapyOGoyYg==} + '@zag-js/focus-trap@1.41.2': + resolution: {integrity: sha512-3QTtGUjFU2OLbyrDlyoYWvKZecCmtn/+bsfsHW159jJHiEGHVYK6CY8AI1ePsMk9gVay48bXh008j+lVli7gAw==} - '@zag-js/focus-visible@1.35.3': - resolution: {integrity: sha512-g4F8PRGIoFoKBrHiQ1HQh5AjCS7brFRXHvpbDNb9+T11FGlF5Turb+6OVRoNV8MmiuqMltO2I28l36YsGc//uQ==} + '@zag-js/focus-visible@1.41.2': + resolution: {integrity: sha512-oRjwtgafUdGVwLJUN6mKsnBQbez/CHYAPkPg1FxOnr5GFpEpr8oMTOZJ3wdPM2U1ynS9QnUUu/sXc3KQv/jX0Q==} - '@zag-js/highlight-word@1.35.3': - resolution: {integrity: sha512-K+mvEBbf3SUFjQeMeJQYb3cjri3x6sPaPhcKWayalelSLB/StWEGqcpmz+a6uUYrCUAK5kEi3Hn0YLGfn0GOig==} + '@zag-js/highlight-word@1.41.2': + resolution: {integrity: sha512-95zcZKqNrL7JlqAckfzHa+LRbnfoz1lj6skUhq/uHSnni1vH6+8fNWT8ruo9G7vpGopbyRmmcie5p41SbAwQjQ==} - '@zag-js/hover-card@1.35.3': - resolution: {integrity: sha512-xVoKOtvrnzhYzciZ1csgiV76IQ4DRtx1lsJeFSrfg5MH0kYWeC/pcmm3yCd2+Qh/45J7DbSXeZneqxpyiF5Vvw==} + '@zag-js/hover-card@1.41.2': + resolution: {integrity: sha512-Xn9RVzgTkKaVzyJTDdBJiXmCleNi1/hmW8z73tC0vOiQvSSvPejg/JkzqTOLFODvQHK3NOw54QHZvmjYp9Mubw==} - '@zag-js/i18n-utils@1.35.3': - resolution: {integrity: sha512-k7UcNxbnC2jvGwCoHYAkFD3ZaRSMQNVHfuy8TujZQ+ci3IJovwgWLveZoRfFbXHkTLfhmbpE2tFXBdpwOVZutg==} + '@zag-js/i18n-utils@1.41.2': + resolution: {integrity: sha512-f1xqaEY79awBxgUyjFso0UEpIoEHZq+zRvB0nUVFHJRW7Ds/QhaFHKSRnf2QBTlP/ObvCT225R+piNAAc17Aaw==} - '@zag-js/image-cropper@1.35.3': - resolution: {integrity: sha512-1PH6bg8JAQESHzNqjka2TJ0QGNBGBAO6rb7AZ+9CaCCLw0pIzbUJhqPMkwd9GhdWGKGP+e7wFitnjcT4W5Js8g==} + '@zag-js/image-cropper@1.41.2': + resolution: {integrity: sha512-750aT4U+J/TJw/Z1QVoLU9JG0luCtf9CyvShtJFIxeS+i25aUoBI9pOKgSFABsOutIqNJTPq4gYpqtuxFjSxRw==} - '@zag-js/interact-outside@1.35.3': - resolution: {integrity: sha512-tOcuo/IztzpU7UKXtjVrLZtXzzcbhP4n2WynKwDRkTkq3mRCp61xXJp1csIBycI3JHm/CMeAEcPdRIioxIT/Zw==} + '@zag-js/interact-outside@1.41.2': + resolution: {integrity: sha512-dM4Fn9iyqQeqkCMRYZP+bAgWEPKRVQRqMmcPsN0OXBhhFKC31Em15LTIZXaOtVKAjH+iwx+UvSYFRiWwEjkOEA==} - '@zag-js/json-tree-utils@1.35.3': - resolution: {integrity: sha512-nOv2dPJf+1mxsobYiSlYt96hR1MK7iHKG1iDLoO5wLggS6GQA3ix1BerHJK0zdehoEZ71R45el5ghCG1HB9VzQ==} + '@zag-js/json-tree-utils@1.41.2': + resolution: {integrity: sha512-gNaOzsbCwmTd2HM3/u0xQdWX5UDBfl8tCXFavzbamkFH0iYQOXJb7cqUXBVuI4KScIbHPCKwrzZjqA5Sg9qzAQ==} - '@zag-js/listbox@1.35.3': - resolution: {integrity: sha512-FE6FOuBr6aWtOb8U8oDvAvcUzD6JKLXAe8WngiLFG+b2yyW4nlaz2AcKRG1bjjB066UMxMo9/+2p4D0Kf5Id1Q==} + '@zag-js/listbox@1.41.2': + resolution: {integrity: sha512-iDGrZleP3ui2Q6Jgmr9RYlbd7njdJHs8Qb3IJrSIZBIeYyYmFvVUfAFjk0g/z/amjTx6uYxRASWSPy/RETd4ug==} - '@zag-js/live-region@1.35.3': - resolution: {integrity: sha512-64rWcfggYpyr2Fn4pdrB/lljMgm3quwn9is+vdDN85Vv3WShKWoz08T4njidm0hwcIbzas0bRqQYWDLLsAoSJQ==} + '@zag-js/live-region@1.41.2': + resolution: {integrity: sha512-7ubIW5AQt1wx9S/gFN+rU4TyvuFWJrL/DhnDWPNlH5g3luDVHSNeYGeeqf4d4tkcibtpYZa0pg9CbXxRxrfwVA==} - '@zag-js/marquee@1.35.3': - resolution: {integrity: sha512-bKZVpmAJWPDORP7WOWnS+65W5ZQBQmRs8zvV33ZfCpFbkXjhRiqKSzIj223/VOc2NEDjyWagz2vioAxrFYVzww==} + '@zag-js/marquee@1.41.2': + resolution: {integrity: sha512-cT77aMhrtAK3oe2O6+X3TLtQs8wupdPUtZgHMWVxCcQOwagrk7RUBEQwU3Cx7cTswpBdTKSuDYgUggfgCs96wg==} - '@zag-js/menu@1.35.3': - resolution: {integrity: sha512-KyY0EZXkIU57Mjt+Lg+pupiePk3LcnQcB3Gl05Vva61bNjBjdKV71qwCQru/OxPZEwYgPo46L7TDIb56kfK/VQ==} + '@zag-js/menu@1.41.2': + resolution: {integrity: sha512-wwix8hcAUSi0scpWXCiDppfdZV01Za8nN0gqLt9GdhCiVSlr0rs9pK1ROgPKJTyc43UZfyFPqtTWVHvEHMM70w==} - '@zag-js/navigation-menu@1.35.3': - resolution: {integrity: sha512-8cCHx0X/KjEpr2BaMOxJS5LiA6fs/CNqVTF/sTTgZAv7Dm+MH0yNuKm4kpPvcLaVeBpVE09bnyCHrNKzZes+Fw==} + '@zag-js/navigation-menu@1.41.2': + resolution: {integrity: sha512-aROB9CHzskZpnoFuGFkp7dbkZdsXvp2nxQgsgld02I1sDqiwcQd+YMdB0/6Ik0oz6X8I70RKdUuQM9WQFQfDnA==} - '@zag-js/number-input@1.35.3': - resolution: {integrity: sha512-uqawVybAcLcefVEHMVONuAA5kDSDPP5TsROr5PnAyFlhM1iD85+r3KAfCueoDX5w2X4ibbu9o2tdV6zTFKD/nQ==} + '@zag-js/number-input@1.41.2': + resolution: {integrity: sha512-QoQWCiVHO+ciUbq8uL8Kxhtk4o3UpwzYpJkfsOfitzsZoYpPc7V/A6+n5yABV2SOwpqBODwNASZdxiEa60kfow==} - '@zag-js/pagination@1.35.3': - resolution: {integrity: sha512-fKm4s5KAd12RiCI/EDmmGKjPQ+i2qS/UsJPdMe65yb/4mY5OibwV2zyHcVeFsOD4gBZpnU6kYlDAGSttmLWLlQ==} + '@zag-js/pagination@1.41.2': + resolution: {integrity: sha512-vZTxz4DrIDfedMcTjDeEkOc1iXD9wkP8eKuEcDieHOodnjSnNNwtmoFw5DCWv+yEa/TByIamXBZ8ZxHY144JgA==} - '@zag-js/password-input@1.35.3': - resolution: {integrity: sha512-etd0gm6ELAm3y+cFhPU+TYm8khm9cL5Mg5m2DcZxu1Mqpj7JY0LsXZ8SFOdCZgTIHuMEhKBiYfnuyMAd4CJztA==} + '@zag-js/password-input@1.41.2': + resolution: {integrity: sha512-OWKFl0S12Qnlf4R4WbMCJ/YU0kGfezm5tP0UiyubMO/Fixv6H0twDFaJSPg2F6POv5uomCcGubc1H7gO+fIhsQ==} - '@zag-js/pin-input@1.35.3': - resolution: {integrity: sha512-ZFt+WIHMdVlSg29BrQLFq5ijabiUO3tXMhoKhjjzTSe/tLqfNeu3UxFB6y/FYpn8+Cvn6xwvhu3lgnORYmI0zQ==} + '@zag-js/pin-input@1.41.2': + resolution: {integrity: sha512-Wrn3YDbmWL3qvUIzN4QyLO7PzEhunX7z8DwBpmrK04p7HxR9pniTLVIPk27xk2MzyAPYcl4mwd9/Mc88tBxHsg==} - '@zag-js/popover@1.35.3': - resolution: {integrity: sha512-+MIEENPsbKPxzoNuDI/C5d5ZN9uxnfZ+MBDc5C5XSgjjg9FcvMXClNq7IFM1aZi24peRXg9cMNf//lApVRT37w==} + '@zag-js/popover@1.41.2': + resolution: {integrity: sha512-h/LlVMIERM+NWzYV0ZHURlJuaqkT8XxwyEV96XfVzjknDRFNoPSl5IttVYtoaPoUqC0p/Y4oTSiee4mUZKOLHw==} - '@zag-js/popper@1.35.3': - resolution: {integrity: sha512-gpB7Xn9WtlfrUsIVbSgNQGDwgNOL/cSGt0Id3wEQKArmqVC704EWtPvXzOMMybBEdm8YW2hQrXuo+o66abI1Sg==} + '@zag-js/popper@1.41.2': + resolution: {integrity: sha512-Iz4D5YAIiIPn4IHGjhX3QatR/RyGaDt43lBSZv0RYcPQYtFg9sUuek3wizjW9qXgdvItevvNMqRdpl7f3es09g==} - '@zag-js/presence@1.35.3': - resolution: {integrity: sha512-ev5E7+U9IZAGvEaflpdVLHaZl8ZaQMhGB3ypd0yKhPwXeM51obV8w3+5HjzTqHPl8TKuoHWL31YaiUBd5EuS6w==} + '@zag-js/presence@1.41.2': + resolution: {integrity: sha512-OhOLPAf/DYPmgoEntrlrf3LOrkwA+Y0J3K03NXHXPnkRB7h8jyIbHqzHS0jRTb1pvsO2P/yowRyoYtptY2Zmog==} - '@zag-js/progress@1.35.3': - resolution: {integrity: sha512-u0GxQN1AfXMAgzYOUMxKQA12DyuAP0svh2S//KvOorTSv7d5hAa8nZXi2cEv5abYsyfKJ6/bc1Z56byzW1jVZw==} + '@zag-js/progress@1.41.2': + resolution: {integrity: sha512-SnzrqN+Z568NoO4rrgjrIc/S4EXMEne5CDgWwt2kQ8yq9VysdH8TtHAjyciFRIio7cdgEZNHKw9jSccpMWgRwA==} - '@zag-js/qr-code@1.35.3': - resolution: {integrity: sha512-t0Ehwogr49vTNtWyNdQU2tYex7uJyfAn7N/5LgD7FXw8aa+RBMWZWlqjCUvHqJ929tVMrn+LIrQnZCcwNunalA==} + '@zag-js/qr-code@1.41.2': + resolution: {integrity: sha512-+JLswCNnzf58aQTaX0SMUA9wRC8Hb/a/ZveJIXTz6853Siemay2HqOqB2WQIeF33HNldUFD/a4+4Q8ugg93ubA==} - '@zag-js/radio-group@1.35.3': - resolution: {integrity: sha512-kOzocjqWk3dXuRfyfsHwfw63Z99NHbc7rvVUutSsfXANXi+DFYZHuqdPUwMt+29LfaL15XTOfuGV+yUXDCgQHQ==} + '@zag-js/radio-group@1.41.2': + resolution: {integrity: sha512-EZjos61jKHZlNw8ez80vG2T9gUwpooxcVpYOKS2hyhuEXn3wEoefS5v1WFbmpoA/8TUpUQnYxisAeNuQfEQCuQ==} - '@zag-js/rating-group@1.35.3': - resolution: {integrity: sha512-BmhJZdbaTnd3nFWMY+nR+HF952UhWXfaXXxiBWptSLMBfAYImQTWBMrLgTHCSnVfmFATj4Gb7xQe79FQU8T5fA==} + '@zag-js/rating-group@1.41.2': + resolution: {integrity: sha512-SbJP4HiK5XRy/oC3xRowjZOCThq1n/QA2Z/XAkvKLJArVQCYjrH3MoWwWvMVNfNC5+ZJluborR2AGF7tlVLzxA==} - '@zag-js/react@1.35.3': - resolution: {integrity: sha512-x2PxYUCQ6OgOpUdmSkG5tbL9JWVqYRh42r4V2UeAdMh0MRwjAJtxjvAy50DZ8Sfia5o4UGdZMXJyDY2O7Pdhyw==} + '@zag-js/react@1.41.2': + resolution: {integrity: sha512-5Bx7mQAron4LFWI8Hhs/uw5kwQ19s2Tn30HhctozLqmCu4nnJSTSh7GRvX+uwRZnztGXBXoOrgBWIepU4RXFGg==} peerDependencies: react: '>=18.0.0' react-dom: '>=18.0.0' - '@zag-js/rect-utils@1.35.3': - resolution: {integrity: sha512-mt/oD3RXdyaX6ZPSd8BO13vvPBJ7QpVWieubE3O0WM3OPhU7ykDMRp/tR7cYMQrzUm04GlY9pbkmSSw2uABxlA==} + '@zag-js/rect-utils@1.41.2': + resolution: {integrity: sha512-GWBTamaMLMG1p7Fe6V0dsXeTEmk+tqG3ciovzmjxURCJ3Yq2EkAMRhS0v5DG0oo+PyrPEIzEWukGBQkh/XRgYQ==} - '@zag-js/remove-scroll@1.35.3': - resolution: {integrity: sha512-e59z9SbEpPiw0qwNQa2cB5/h30ZCLREaHsCw1TKTANFhwg7v85k9Lq1H/G/49li1CAjmiaOU9BNGlDvbzpNETQ==} + '@zag-js/remove-scroll@1.41.2': + resolution: {integrity: sha512-ieIrOgPKlCikAGEBIboQJoU7oxrL5BEY670tDOu7Eg7rNOdAwGXLEKPX2A/q+lREhOiVYjx0D3//vHTvdte80Q==} - '@zag-js/scroll-area@1.35.3': - resolution: {integrity: sha512-IQwdUws/AckRIHK1z/wHdHurnOeGd8h8Dmspfh3VT7NkwTnxeJ4SW9di9smuD+d25eXkJRuX5zGEDHAyx2IaPQ==} + '@zag-js/scroll-area@1.41.2': + resolution: {integrity: sha512-hJFAwfIFuS7XmNsS3nB5rPm9OWXfB4d98sID7fjurcaZtDH5LqFriqfXhceNvs7rz7K4f/u8rufXrb6tcvATIA==} - '@zag-js/scroll-snap@1.35.3': - resolution: {integrity: sha512-NVa2yRm2DQnF6hTV9k7Xz7l8YCZBagZTiqSwNvWKUulKD1csjt2fpBxvUt2cK+1iQnLOey2ydhs7MMsAnXPbJA==} + '@zag-js/scroll-snap@1.41.2': + resolution: {integrity: sha512-+70Al6LSASyEZtFyyffUJlDbE88KgYJDud05z8oZTqyEOLlTqnlSNkXq/P3siO7r3sNykg9H+TmAKn+/dVSKuw==} - '@zag-js/select@1.35.3': - resolution: {integrity: sha512-ztszGHWvlbBDE0YT5LYPH+sMd6VH1ct5pH/M9VSzIUO6C5PARkW0NwSVQ1rCQJMj4sfvSE1gC1/r7urRzqEcUQ==} + '@zag-js/select@1.41.2': + resolution: {integrity: sha512-3wGaKABILexoNBJ1bJiHqLLTctR/VMZaNA4cyKiqZeBEWtAkdMhgyY2xKobrP6KtUTqAeUFNVSTu4yCDrAQnUw==} - '@zag-js/signature-pad@1.35.3': - resolution: {integrity: sha512-jvtxxzAQ8fre11zWUh6HflG4Ycr5z83Wba4pONRJbUE/vNgkJQ7yJgfyUl1QTlkn8Arfg2Zwoxu9GIq80HLZWg==} + '@zag-js/signature-pad@1.41.2': + resolution: {integrity: sha512-iNrOxY4gtqhsZdXYvlhF7s+LiOvwV7/kBpNnq8tJ1oYhgTs8wvKtJHrP86/CR05irEXQTaLfmeAJZuBEys9iRA==} - '@zag-js/slider@1.35.3': - resolution: {integrity: sha512-Th142JO4Fqla5AWhGrTW6CQicwvTw87PdVpur/WotQ7brlZIww5HipzEMh5eQJSWfwpKD4PI2bYK9V/ZE/mpXA==} + '@zag-js/slider@1.41.2': + resolution: {integrity: sha512-mKK2BwoDbIGxAdkdKkPZJA1SHtEQt3lS9hJ6WghefYU2vyd0BXoIKvcDV3xJOzly5LXYhH5cJITn6JtGK8353A==} - '@zag-js/splitter@1.35.3': - resolution: {integrity: sha512-IsIbRwzjr5amGANEDsZDSToaSn8wHUWvS2l0XHmf3BiiguVApaZgQTlfqthVQC9hBHMOaGIXIW1CFUOrQYkvUQ==} + '@zag-js/splitter@1.41.2': + resolution: {integrity: sha512-Ubp4hkmzvVysU31jCINYbBXVqruu7rEPGqugWMzeXC5Hwda648aHpbg4Jix/wRtaGLjsyh6KOVEwAmoJU9NwhQ==} - '@zag-js/steps@1.35.3': - resolution: {integrity: sha512-TYIrqV+v9/ULhvrTRBtQFFvJQPPTWOmjFXxlIxDwozek5R4dCIyeUYt1/ChJEc2mNETocbfDVSTxRO1dwCFpwQ==} + '@zag-js/steps@1.41.2': + resolution: {integrity: sha512-m0t2r8+FWwa2b2aU5JiNrHVdYHyNZYHK0G3Tq9lCOSQoDeoJIkyta+sIVehLVSY+0Ba9kOlkRUmYLbsnfXaW+Q==} - '@zag-js/store@1.35.3': - resolution: {integrity: sha512-7kEV4T/20DU36UIfVMzuDlLhWSSEy/vabmpiB700tcdD9BBBODTiSg3ZeljW17dQbvE545vZOFEjVf/cQ5LVGA==} + '@zag-js/store@1.41.2': + resolution: {integrity: sha512-dVZF7E1ezXzynrKhMH3rfSr2rBbCfvTjvXbXz7//1PNULuq58UU5dG93V+9l834npCZxI2+PrpY45wZLJPTsIA==} - '@zag-js/switch@1.35.3': - resolution: {integrity: sha512-EP/2cJ46sd+6C5x5+89jn/9NOpM05CRESYB4RMhOnTe/WFtcS4IpiYtVHFhikdXkvJoibm67O2EHep2Pm/Xj4w==} + '@zag-js/switch@1.41.2': + resolution: {integrity: sha512-qHbQK95UUHN0tj+bf9LLphLcMo/uTg2Pvs0c1Gs03Zh4g3NtHf0uYIhMZKYlCH0hNVlKrmdzLKWgeoDxv9gySw==} - '@zag-js/tabs@1.35.3': - resolution: {integrity: sha512-lZKlDmxE25miCikj9QZCCnL02SVV2K14KZy5bn7+XDgrWlfSNTpNTj8r5E3zGlSgio5pkTGou57ASqS7WaPDWg==} + '@zag-js/tabs@1.41.2': + resolution: {integrity: sha512-7YVj2mCcxRbn1wMXP9anaTOVf+J0fa5uaPScr8e4+e2xc+/1WKzqN6V8IDeKS5wV/xzi1r3Ny307sX7Xz4ZJVQ==} - '@zag-js/tags-input@1.35.3': - resolution: {integrity: sha512-HqyoQ3DZFhByOGnDShFfxi6u0bIf7aSVTlwmAvcL+b2ZhyU6/wIMGc4WJE7BMx1NYWM/jNLHedvGExAI8R0kXQ==} + '@zag-js/tags-input@1.41.2': + resolution: {integrity: sha512-aIPEndSO+9LHxyoXLUr0Uttxw2cYMyDuii5w50wn/N7lFJD49U3Sj+6XaB/oJSbDAwn10WrsRDtb7Gs2kmU+Qw==} - '@zag-js/timer@1.35.3': - resolution: {integrity: sha512-edmgitbRgsq+msxvVB4wc17Q5d5k63zMWaLJnWjUdDGAgEtM6/HNxwGb3riv46S2U3RgYxaaHTNZ/M7EE5mvYw==} + '@zag-js/timer@1.41.2': + resolution: {integrity: sha512-PRYLWaip0+1FeVGEMNk5wMGAAIYgBIWwulZ4U5I+2Ayjohzp2NUAfwJ3sqoYvRrfjNYUNAPdU4eGu1zetC+oVQ==} - '@zag-js/toast@1.35.3': - resolution: {integrity: sha512-whlR791GHdnMD21nNPsl2Dbql8+qu1wBZl75QzwYrjR8FlKjp8bhr3gXKzQEddcBXe9GPEFGvUs4iCyXsuTbpg==} + '@zag-js/toast@1.41.2': + resolution: {integrity: sha512-+F3PsAo6EIz4rh73IOMCb/+FOUp7e3VjUY2q5sdU4IbfOzJBIbVSJxn0PQmHkuxkzWdCom0Lv0qSPFg/UTplnQ==} - '@zag-js/toggle-group@1.35.3': - resolution: {integrity: sha512-Gn6JHzkQ4tlttjZcE0ZjIdxYkFeVp9VHrcMVizjJTkGZRmQ+kPZ5G/wOsZhIrvLX3Dw6Y0NkuBcP+jDHz/o3TA==} + '@zag-js/toggle-group@1.41.2': + resolution: {integrity: sha512-C6wn3A89h24hTs0BN9ryEuKatfR493u7QqxS06TeK9oI/KZBvm5Rwm8FPHSUJvsUTkAkow4PsXC0Ra19duzEdQ==} - '@zag-js/toggle@1.35.3': - resolution: {integrity: sha512-aFfHKuR4sKzglhkmWLA+0RTNPs9dfeqwtc96qljawGYfAYWJXkEPYK9dFfVa+arZ7L84xBi24QSLiTg7LGSFLw==} + '@zag-js/toggle@1.41.2': + resolution: {integrity: sha512-EFB9pb3pEtwXt7RSivVLWXV64dKUF8gsn75tt8TbYBgfy+zW85MsRLu8U48TXZN5teQWAKKNujr9bxQsW/CWvQ==} - '@zag-js/tooltip@1.35.3': - resolution: {integrity: sha512-/pImDGYl79MfLdvEphj3rSvNdj2tLW4GwGEncgdLM/GKwQiEUjfi/9EJOfLYP23M4lOOnoW7orehJ9xeaXOAkA==} + '@zag-js/tooltip@1.41.2': + resolution: {integrity: sha512-68okWJCFXfW8r0h97kEcU2yKPkq6e0S6QkiYh09ifMXoYjrQw/shPol8PgrS1poqKJigUWtsKm+bw73abhMn3w==} - '@zag-js/tour@1.35.3': - resolution: {integrity: sha512-DI2aCXmZaE9KcPZDs9itc2BO7ixLApJ/yVRfM69pXwVOrucdSeDDNPFkfbhj5XwB+9VjjZEkqWFHKntRIyPl5g==} + '@zag-js/tour@1.41.2': + resolution: {integrity: sha512-Q7UsvuHYYBo1Cs4b4OS0e/D8lxd2GpSRII93s5BQPi5HTcBjaWhVysAykmCFbat6Z56z0NBmFHNdhZ8oVPls1A==} - '@zag-js/tree-view@1.35.3': - resolution: {integrity: sha512-DbHaLxSNa1goE3o3IsXxEdzp8P5dvmkk1rVWgNUUIhpA+44idEjSSNXJkHPl18Mk5blqSMVjK1EX91oqai01Vw==} + '@zag-js/tree-view@1.41.2': + resolution: {integrity: sha512-QNi0VpV+RyzF4NP72+kSleUpauF9SMAzOAe59nxvs8jAUHqV5diDCInnbQos4+cyFDXFzGq3lElot26A1yI+7Q==} - '@zag-js/types@1.35.3': - resolution: {integrity: sha512-Fnm3AMs1lfb55hlkip/eJeWHOjFB3gSi1JkZlkkdltG2l7y/zsHkumPSe6jIKy+DRRIFKRCyXVTatbPN27bO3w==} + '@zag-js/types@1.41.2': + resolution: {integrity: sha512-L6CNvK06lIVpy0X8eG3kbDIx8Uuv+3KHElxXYSzRXSJ7/OLCv1sTRgEvnxNtdIWOrksGgxF4JtT7PXtoClGqNQ==} - '@zag-js/utils@1.35.3': - resolution: {integrity: sha512-LHcC+9y6TFhDsIz9I3koYxONl2JFfx5yQDzc6ZEQO2cqzXedRcN0R9IPqNGCX7JuhGt14ctDkVCm1JWGP2J6Wg==} + '@zag-js/utils@1.41.2': + resolution: {integrity: sha512-Yj8FSrR7vGA6ahUhjrThfHAF+PM2Y1Yv2lkXkqZZd60mPBhixcot1+SHOfEMV63JimQcWrmQ8QbeYYMmF+ZpLQ==} acorn-jsx@5.3.2: resolution: {integrity: sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==} @@ -1606,13 +1601,8 @@ packages: engines: {node: '>=0.4.0'} hasBin: true - acorn@8.15.0: - resolution: {integrity: sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==} - engines: {node: '>=0.4.0'} - hasBin: true - - acorn@8.16.0: - resolution: {integrity: sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==} + acorn@8.17.0: + resolution: {integrity: sha512-xRQbDb9BnwDafYNn6Vwl839DYVjqXYb1XVGtWAZ1kcDc6iwAL4hg3B1dZlRiuENFeO2H53gFG3in621AdERVAg==} engines: {node: '>=0.4.0'} hasBin: true @@ -1686,11 +1676,15 @@ packages: resolution: {integrity: sha512-BNoCY6SXXPQ7gF2opIP4GBE+Xw7U+pHMYKuzjgCN3GwiaIR09UUeKfheyIry77QtrCBlC0KK0q5/TER/tYh3PQ==} engines: {node: '>= 0.4'} + assertion-error@2.0.1: + resolution: {integrity: sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==} + engines: {node: '>=12'} + ast-types-flow@0.0.8: resolution: {integrity: sha512-OH/2E5Fg20h2aPrbe+QL8JZQFko0YZaF+j4mnQ7BGhfavO7OpSLa8a0y9sBwomHdSbkhTS8TQNayBfnW5DwbvQ==} - ast-v8-to-istanbul@1.0.0: - resolution: {integrity: sha512-1fSfIwuDICFA4LKkCzRPO7F0hzFf0B7+Xqrl27ynQaa+Rh0e1Es0v6kWHPott3lU10AyAr7oKHa65OppjLn3Rg==} + ast-v8-to-istanbul@1.0.4: + resolution: {integrity: sha512-0bC0/4bTSrnwdhU3IsZDwEdojvuPrSg59OYZfKsLRtJZ0u8VBx9DebfqqG8bRdCC0I7vjgxmPi41P0lpkhJHtA==} async-function@1.0.0: resolution: {integrity: sha512-hsU18Ae8CDTR6Kgu9DYf0EbCr/a5iGL0rytQDobUcdpYOKokk8LEjVphnXkDkgpi0wYVsqrXuP0bZxJaTqdgoA==} @@ -1711,8 +1705,8 @@ packages: resolution: {integrity: sha512-Xm7bpRXnDSX2YE2YFfBk2FnF0ep6tmG7xPh8iHee8MIcrgq762Nkce856dYtJYLkuIoYZvGfTs/PbZhideTcEg==} engines: {node: '>=4'} - axios@1.16.1: - resolution: {integrity: sha512-caYkukvroVPO8KrzuJEb50Hm07KwfBZPEC3VeFHTsqWHvKTsy54hjJz9BS/cdaypROE2rH6xvm9mHX4fgWkr3A==} + axios@1.18.1: + resolution: {integrity: sha512-3nTvFlvpn9Zu/RkHUqtc7/+al4UpRW5az71ap5zccp6e8RAYEzhMTecX8Dz1wWDYrPpUoB1HAQEGEAEvUr7S9g==} axobject-query@4.1.0: resolution: {integrity: sha512-qIj0G9wZbMGNLjLmg1PT6v2mE9AH2zlnADJD/2tC6E00hgmhUOfEB6greHPAfLRSufHqROIUTkw6E+M3lH0PTQ==} @@ -1747,8 +1741,8 @@ packages: brace-expansion@1.1.13: resolution: {integrity: sha512-9ZLprWS6EENmhEOpjCYW2c8VkmOvckIJZfkr7rBW6dObmfgJ/L1GpSYW5Hpo9lDz4D1+n0Ckz8rU7FwHDQiG/w==} - brace-expansion@5.0.6: - resolution: {integrity: sha512-kLpxurY4Z4r9sgMsyG0Z9uzsBlgiU/EFKhj/h91/8yHu0edo7XuixOIH3VcJ8kkxs6/jPzoI6U9Vj3WqbMQ94g==} + brace-expansion@5.0.7: + resolution: {integrity: sha512-7oFy703dxfY3/NLxC1fh2SUCQ0H9rmAY+5EpDVfXjUTTs+HEwR2nYaqLv+GWcTsumwxPfiz6CzCNkwXwBUwqCA==} engines: {node: 18 || 20 || >=22} braces@3.0.3: @@ -1760,6 +1754,10 @@ packages: engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} hasBin: true + buffer-image-size@0.6.4: + resolution: {integrity: sha512-nEh+kZOPY1w+gcCMobZ6ETUp9WfibndnosbpwB1iJk/8Gt5ZF2bhS6+B6bPYz424KtwsR6Rflc3tCz1/ghX2dQ==} + engines: {node: '>=4.0'} + builtin-modules@5.1.0: resolution: {integrity: sha512-c5JxaDrzwRjq3WyJkI1AGR5xy6Gr6udlt7sQPbl09+3ckB+Zo2qqQ2KhCTBr7Q8dHB43bENGYEk4xddrFH/b7A==} engines: {node: '>=18.20'} @@ -1802,8 +1800,8 @@ packages: resolution: {integrity: sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg==} engines: {node: '>=18'} - chakra-react-select@6.1.1: - resolution: {integrity: sha512-ztKr8VIXtDsRDB3exXmAdBf2JG2yWMUsxyUDMH9mIVaNFo+hBIq1RMbKYu212MeYnLaoYHMekzgQeoZupfP4kQ==} + chakra-react-select@6.1.3: + resolution: {integrity: sha512-U02sJqqM4rEBdzje1IXUY3Of6W+u6r+hln+a1+hOhKsj9KWGwJOb6Yxlpdblsgp+K52WAoKW8PkrJIcm2p82dw==} peerDependencies: '@chakra-ui/react': 3.x next-themes: 0.x @@ -2045,8 +2043,8 @@ packages: resolution: {integrity: sha512-BS8PfmtDGnrgYdOonGZQdLZslWIeCGFP9tpan0hi1Co2Zr2NKADsvGYA8XxuG/4UWgJ6Cjtv+YJnB6MM69QGlQ==} engines: {node: '>= 0.4'} - dayjs@1.11.19: - resolution: {integrity: sha512-t5EcLVS6QPBNqM2z8fakk/NKel+Xzshgt8FFKAn+qwlD1pzZWxh0nVCrvFK7ZDb6XucZeF9z8C7CBWTRIVApAw==} + dayjs@1.11.21: + resolution: {integrity: sha512-98IT+HOahAisibz/yjKbzuOBwYcjJ7BCLPzARyHiyEBmRz4fatF+KPJszEHXsGYjUG234aH/cOjW1wwTbKUZlA==} debug@4.4.3: resolution: {integrity: sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==} @@ -2112,6 +2110,9 @@ packages: dom-helpers@5.2.1: resolution: {integrity: sha512-nRCa7CK3VTrM2NmGkIy4cbK7IZlgBE/PYMn55rrXefr5xXDP0LdtfPnblFDoVdcAfslJ7or6iqAUnx0CCGIWQA==} + dompurify@3.2.7: + resolution: {integrity: sha512-WhL/YuveyGXJaerVlMYGWhvQswa7myDG17P7Vu65EWC05o8vfeNbvNf4d/BOvH99+ZW+LlQsc1GDKMa1vNK6dw==} + dotenv@16.6.1: resolution: {integrity: sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow==} engines: {node: '>=12'} @@ -2155,8 +2156,8 @@ packages: resolution: {integrity: sha512-uDn+FE1yrDzyC0pCo961B2IHbdM8y/ACZsKD4dG6WqrjV53BADjwa7D+1aom2rsNVfLyDgU/eigvlJGJ08OQ4w==} engines: {node: '>= 0.4'} - es-module-lexer@2.0.0: - resolution: {integrity: sha512-5POEcUuZybH7IdmGsD8wlf0AI55wMecM9rVBTI/qEAy2c1kTOm3DjFYjrBdI2K3BaJjJYfYFeRtM0t9ssnRuxw==} + es-module-lexer@2.3.0: + resolution: {integrity: sha512-KLdwQm2NvGLDkQDCGvmiQrhkd0JbMzXthwQAUgWjQuQdBLFa3eiBP5arXZyA+f8x+x7OXgud6bq2rxjGtHV2tw==} es-object-atoms@1.1.1: resolution: {integrity: sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==} @@ -2210,12 +2211,12 @@ packages: '@eslint/json': optional: true - eslint-plugin-i18next@6.1.4: - resolution: {integrity: sha512-BekXQu1VaVFkREepQGsntBYoKuPsf89V16n/s2xpKMtsw0/lDseds1wBhj3RtO1dKnHsfTPaG+xul1vF0R7LEQ==} + eslint-plugin-i18next@6.1.5: + resolution: {integrity: sha512-xCTfstbK9ZpQ6UFT5S1s6zbZMhKn2o2jRSQYiU2UkV7wt8y1m3WjkUYGkGlipgRdFInYI9+LAqE+JQU/L73HmA==} engines: {node: '>=18.10.0'} - eslint-plugin-jsonc@3.1.2: - resolution: {integrity: sha512-dopTxdB22iuOkgKyJCupEC5IYBItUT4J/teq1H5ddUObcaYhOURxtJElZczdcYnnKCghNU/vccuyPkliy2Wxsg==} + eslint-plugin-jsonc@3.2.0: + resolution: {integrity: sha512-eQSxJypkpNycQAFE/ph/j+bDD2MiCcojxNb+7nugYzuQZvELYg4YO1Cv1y/8MbjPIEw5u3Lx0VPOTlqJJIhPPw==} engines: {node: ^20.19.0 || ^22.13.0 || >=24} peerDependencies: eslint: '>=9.38.0' @@ -2226,14 +2227,14 @@ packages: peerDependencies: eslint: ^3 || ^4 || ^5 || ^6 || ^7 || ^8 || ^9 - eslint-plugin-perfectionist@5.9.0: - resolution: {integrity: sha512-8TWzg02zmnBdZwCkWLi8jhzqXI+fE7Z/RwV8SL6xD45tJ8Bp3wGuYL2XtQgfe/Wd0eBqOUX+s6ey73IyszvKTA==} + eslint-plugin-perfectionist@5.9.1: + resolution: {integrity: sha512-30mHLNfEhzwaq5cquyWgnzrNXvT8AzwIwyeH5aj4U5ajhHSF2uiO6i09xpMDLv7koaZVTjLsvYF4m3gK/15tyA==} engines: {node: ^20.0.0 || >=22.0.0} peerDependencies: eslint: ^8.45.0 || ^9.0.0 || ^10.0.0 - eslint-plugin-prettier@5.5.5: - resolution: {integrity: sha512-hscXkbqUZ2sPithAuLm5MXL+Wph+U7wHngPBv9OMWwlP8iaflyxpjTYZkmdgB4/vPIhemRlBEoLrH7UC1n7aUw==} + eslint-plugin-prettier@5.5.6: + resolution: {integrity: sha512-ifetmTcxWfz+4qRW3pH/ujdTq2jQIj59AxJMIN26K5avYgU8dxycUETQonWiW+wPrYXA0j3Try0l1CnwVQtDqQ==} engines: {node: ^14.18.0 || >=16.0.0} peerDependencies: '@types/eslint': '>=8.0.0' @@ -2252,8 +2253,8 @@ packages: peerDependencies: eslint: ^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 || ^9.0.0 || ^10.0.0 - eslint-plugin-react-refresh@0.5.2: - resolution: {integrity: sha512-hmgTH57GfzoTFjVN0yBwTggnsVUF2tcqi7RJZHqi9lIezSs4eFyAMktA68YD4r5kNw1mxyY4dmkyoFDb3FIqrA==} + eslint-plugin-react-refresh@0.5.3: + resolution: {integrity: sha512-5EMmLCV98Pi4o/f/3DP/v/tNqLHMIc9I8LKClNDWhZ9JTho89/kQcitCXQBMG7sAfVRK0Ie3T2EDOzp1YXYiVA==} peerDependencies: eslint: ^9 || ^10 @@ -2285,8 +2286,8 @@ packages: resolution: {integrity: sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==} engines: {node: ^20.19.0 || ^22.13.0 || >=24} - eslint@10.3.0: - resolution: {integrity: sha512-XbEXaRva5cF0ZQB8w6MluHA0kZZfV2DuCMJ3ozyEOHLwDpZX2Lmm/7Pp0xdJmI0GL1W05VH5VwIFHEm1Vcw2gw==} + eslint@10.6.0: + resolution: {integrity: sha512-6lVbcqSodALYo+4ELD0heG6lFiFxnLMuLkiMi2qV8LMp54N8tE8FT1GMH+ev4Ti00nFjNze2+Su6DsV5OQW3Dg==} engines: {node: ^20.19.0 || ^22.13.0 || >=24} hasBin: true peerDependencies: @@ -2334,8 +2335,8 @@ packages: resolution: {integrity: sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==} engines: {node: '>=0.10.0'} - expect-type@1.3.0: - resolution: {integrity: sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA==} + expect-type@1.4.0: + resolution: {integrity: sha512-KfYbmpRm0VbLjEvVa9yGwCi9GI34xvi7A/HXYWQO65CSD2u3MczUJSuwXKFIxlGsgBQizV9q5J9NHj4VG0n+pA==} engines: {node: '>=12.0.0'} extend@3.0.2: @@ -2353,6 +2354,15 @@ packages: fast-levenshtein@2.0.6: resolution: {integrity: sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==} + fast-string-truncated-width@3.0.3: + resolution: {integrity: sha512-0jjjIEL6+0jag3l2XWWizO64/aZVtpiGE3t0Zgqxv0DPuxiMjvB3M24fCyhZUO4KomJQPj3LTSUnDP3GpdwC0g==} + + fast-string-width@3.0.2: + resolution: {integrity: sha512-gX8LrtNEI5hq8DVUfRQMbr5lpaS4nMIWV+7XEbXk2b8kiQIizgnlr12B4dA3ZEx3308ze0O4Q1R+cHts8kyUJg==} + + fast-wrap-ansi@0.2.2: + resolution: {integrity: sha512-7F2Fl+TjRSenLqlU3UjSH0iyqopqoZIu7eZVpEirP2g1GtWa2G/ecEmBdgz31+Mxr+ELclgg6sokpSFIQiZ02Q==} + fault@1.0.4: resolution: {integrity: sha512-CJ0HCB5tL5fYTEA7ToAq5+kTwd++Borf1/bifxd9iT70QcXr4MRrO3Llf8Ifs70q+SJcGHFtnIE/Nw6giCtECA==} @@ -2408,8 +2418,8 @@ packages: resolution: {integrity: sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==} engines: {node: '>=14'} - form-data@4.0.5: - resolution: {integrity: sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==} + form-data@4.0.6: + resolution: {integrity: sha512-vKatAh4SlVfgbv+YtmhiRjhEMJsYpsG1Y2rMQtR+SVSbytsSD1YGzDIcrAJmdFec88u/+VoGmxnl+80gL1tRCQ==} engines: {node: '>= 6'} format@0.2.2: @@ -2494,8 +2504,8 @@ packages: resolution: {integrity: sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==} engines: {node: '>= 0.4'} - graphql@16.13.1: - resolution: {integrity: sha512-gGgrVCoDKlIZ8fIqXBBb0pPKqDgki0Z/FSKNiQzSGj2uEYHr1tq5wmBegGwJx6QB5S5cM0khSBpi/JFHMCvsmQ==} + graphql@16.14.2: + resolution: {integrity: sha512-Chq1s4CY7jmh8gO2qvLIJyfCDIN+EHLFW/9iShnp1z8FjBQMoodWP1kDC36VAMXXIvAjj4ARa7ntfAV2BrjsbA==} engines: {node: ^12.22.0 || ^14.16.0 || ^16.0.0 || >=17.0.0} handlebars@4.7.9: @@ -2503,8 +2513,8 @@ packages: engines: {node: '>=0.4.7'} hasBin: true - happy-dom@20.8.9: - resolution: {integrity: sha512-Tz23LR9T9jOGVZm2x1EPdXqwA37G/owYMxRwU0E4miurAtFsPMQ1d2Jc2okUaSjZqAFz2oEn3FLXC5a0a+siyA==} + happy-dom@20.10.6: + resolution: {integrity: sha512-6QD0ilzDDt93tX44y8tbmZdAcdTRYDhUP+Asgi6pC8Pp5IA3cvaZGyoVN/EGtlq9ziT65iPuBBn3ASLr6hCgVw==} engines: {node: '>=20.0.0'} has-bigints@1.1.0: @@ -2534,8 +2544,8 @@ packages: resolution: {integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==} engines: {node: '>= 0.4'} - hasown@2.0.3: - resolution: {integrity: sha512-ej4AhfhfL2Q2zpMmLo7U1Uv9+PyhIZpgQLGT1F9miIGmiCJIoCgSmczFdrc97mWT4kVY72KA+WnnhJ5pghSvSg==} + hasown@2.0.4: + resolution: {integrity: sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A==} engines: {node: '>= 0.4'} hast-util-parse-selector@2.2.5: @@ -2550,8 +2560,8 @@ packages: hastscript@6.0.0: resolution: {integrity: sha512-nDM6bvd7lIqDUiYEiu5Sl/+6ReP0BMk/2f4U/Rooccxkj0P5nm+acM5PrGJ/t5I8qPGiqZSE6hVAwZEdZIvP4w==} - headers-polyfill@4.0.3: - resolution: {integrity: sha512-IScLbePpkvO846sIwOtOTDjutRMWdXdJmXdMvk6gCBHxFO8d+QKOQedyZSxFTTFYRSmlgSTDtXqqq4pcenBXLQ==} + headers-polyfill@5.0.1: + resolution: {integrity: sha512-1TJ6Fih/b8h5TIcv+1+Hw0PDQWJTKDKzFZzcKOiW1wJza3XoAQlkCuXLbymPYB8+ZQyw8mHvdw560e8zVFIWyA==} hermes-estree@0.25.1: resolution: {integrity: sha512-0wUoCcLp+5Ev5pDW2OriHC2MJCbwLwuRx+gAqMTOkGKJJiBCLjtrvy4PWUGn6MIVefecRpzoOZ/UV6iGdOr+Cw==} @@ -2587,13 +2597,13 @@ packages: i18next-browser-languagedetector@8.2.1: resolution: {integrity: sha512-bZg8+4bdmaOiApD7N7BPT9W8MLZG+nPTOFlLiJiT8uzKXFjhxw4v2ierCXOwB5sFDMtuA5G4kgYZ0AznZxQ/cw==} - i18next-http-backend@3.0.5: - resolution: {integrity: sha512-QaWHnsxieEDcqKe+vo/RFqpiIFRi/KBqlOSPcUlvinBaISCeiTRCbtrazHAjtHtsLC66oDsROAH8frWkQzfMMQ==} + i18next-http-backend@3.0.6: + resolution: {integrity: sha512-mBOqy8993jtqAoj6XaI1XeC/8/9v6EPS+681ziegrPvTB0DoaCY7PpTS0SpY56qLMoS4OI1TZEM2Zf59zNh05w==} - i18next@25.8.16: - resolution: {integrity: sha512-/4Xvgm8RiJNcB+sZwplylrFNJ27DVvubGX7y6uXn7hh7aSvbmXVSRIyIGx08fEn05SYwaSYWt753mIpJuPKo+Q==} + i18next@25.10.10: + resolution: {integrity: sha512-cqUW2Z3EkRx7NqSyywjkgCLK7KLCL6IFVFcONG7nVYIJ3ekZ1/N5jUsihHV6Bq37NfhgtczxJcxduELtjTwkuQ==} peerDependencies: - typescript: ^5 + typescript: ^5 || ^6 peerDependenciesMeta: typescript: optional: true @@ -2852,8 +2862,8 @@ packages: engines: {node: '>=6'} hasBin: true - jsonc-eslint-parser@2.4.1: - resolution: {integrity: sha512-uuPNLJkKN8NXAlZlQ6kmUF9qO+T6Kyd7oV4+/7yy8Jz6+MZNyhPq8EdLpdfnPVzUC8qSf1b4j1azKaGnFsjmsw==} + jsonc-eslint-parser@2.4.2: + resolution: {integrity: sha512-1e4qoRgnn448pRuMvKGsFFymUCquZV0mpGgOyIKNgD3JVDTsVJyRBGH/Fm0tBb8WsWGgmB1mDe6/yJMQM37DUA==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} jsonc-eslint-parser@3.1.0: @@ -2917,28 +2927,24 @@ packages: engines: {node: '>= 12.0.0'} cpu: [arm64] os: [linux] - libc: [glibc] lightningcss-linux-arm64-musl@1.32.0: resolution: {integrity: sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg==} engines: {node: '>= 12.0.0'} cpu: [arm64] os: [linux] - libc: [musl] lightningcss-linux-x64-gnu@1.32.0: resolution: {integrity: sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA==} engines: {node: '>= 12.0.0'} cpu: [x64] os: [linux] - libc: [glibc] lightningcss-linux-x64-musl@1.32.0: resolution: {integrity: sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg==} engines: {node: '>= 12.0.0'} cpu: [x64] os: [linux] - libc: [musl] lightningcss-win32-arm64-msvc@1.32.0: resolution: {integrity: sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw==} @@ -2996,8 +3002,8 @@ packages: magicast@0.3.5: resolution: {integrity: sha512-L0WhttDl+2BOsybvEOLK7fW3UA0OQ0IQ2d6Zl2x/a6vVRs3bAY0ECOSHHeL5jD+SbOpOCUEi0y1DgHEn9Qn1AQ==} - magicast@0.5.2: - resolution: {integrity: sha512-E3ZJh4J3S9KfwdjZhe2afj6R9lGIN5Pher1pF39UGrXRqq/VDaGVIGN13BjHd2u8B61hArAGOnso7nBOouW3TQ==} + magicast@0.5.3: + resolution: {integrity: sha512-pVKE4UdSQ7DvHzivsCIFx2BJn1mHG6KsyrFcaxFx6tONdneEuThrDx0Cj3AMg58KyN4pzYT+LHOotxDQDjNvkw==} make-dir@4.0.0: resolution: {integrity: sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==} @@ -3006,6 +3012,11 @@ packages: markdown-table@3.0.4: resolution: {integrity: sha512-wiYz4+JrLyb/DqW2hkFJxP7Vd7JuTDm77fvbM8VfEQdmSMqcImWeeRbHwZjBjIFki/VaMK2BhFi7oUUZeM5bqw==} + marked@14.0.0: + resolution: {integrity: sha512-uIj4+faQ+MgHgwUW1l2PsPglZLOLOT1uErt06dAPtx2kjteLAkbsd/0FiYg/MGS+i7ZKLb7w2WClxHkzOOuryQ==} + engines: {node: '>= 18'} + hasBin: true + math-intrinsics@1.1.0: resolution: {integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==} engines: {node: '>= 0.4'} @@ -3175,14 +3186,14 @@ packages: mlly@1.8.0: resolution: {integrity: sha512-l8D9ODSRWLe2KHJSifWGwBqpTZXIXTeo8mlKjY+E2HAakaTeNpqAyBZ8GSqLzHgw4XmHmC8whvpjJNMbFZN7/g==} - monaco-editor@0.52.2: - resolution: {integrity: sha512-GEQWEZmfkOGLdd3XK8ryrfWz3AIP8YymVXiPHEdewrUq7mh0qrKrfHLNCXcbB6sTnMLnOZ3ztSiKcciFUkIJwQ==} + monaco-editor@0.55.1: + resolution: {integrity: sha512-jz4x+TJNFHwHtwuV9vA9rMujcZRb0CEilTEwG2rRSpe/A7Jdkuj8xPKttCgOh+v/lkHy7HsZ64oj+q3xoAFl9A==} ms@2.1.3: resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} - msw@2.12.10: - resolution: {integrity: sha512-G3VUymSE0/iegFnuipujpwyTM2GuZAKXNeerUSrG2+Eg391wW63xFs5ixWsK9MWzr1AGoSkYGmyAzNgbR3+urw==} + msw@2.14.6: + resolution: {integrity: sha512-ALe+N10S72cyx94cMcy3Zs4HhXCj35sgeAL4c+WTvKi0zWnbd8/h0lcFqv0mb2P+aSgAdD7p9HzvA0DiUPxsyg==} engines: {node: '>=18'} hasBin: true peerDependencies: @@ -3191,12 +3202,12 @@ packages: typescript: optional: true - mute-stream@2.0.0: - resolution: {integrity: sha512-WWdIxpyjEn+FhQJQQv9aQAYlHoNVdzIzUySNV1gHUPDSdZJ3yZn7pAAbQcV7B56Mvu881q9FZV+0Vx2xC44VWA==} - engines: {node: ^18.17.0 || >=20.5.0} + mute-stream@3.0.0: + resolution: {integrity: sha512-dkEJPVvun4FryqBmZ5KhDo0K9iDXAwn08tMLDinNdRBNPcYEDiWYysLcc6k3mjTMlbP9KyylvRpd4wFtwrT9rw==} + engines: {node: ^20.17.0 || >=22.9.0} - nanoid@3.3.12: - resolution: {integrity: sha512-ZB9RH/39qpq5Vu6Y+NmUaFhQR6pp+M2Xt76XBnEwDaGcVAqhlvxrl3B2bKS5D3NH3QR76v3aSrKaF/Kiy7lEtQ==} + nanoid@3.3.15: + resolution: {integrity: sha512-y7Wygv/7mEOvxTuEQDB8StXdMRBWf1kR/tlhAzBRUFkB2jfcLOAxO/SHmOO2zgz1pVgK29/kyupn059/bCHdjA==} engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} hasBin: true @@ -3268,8 +3279,9 @@ packages: resolution: {integrity: sha512-gXah6aZrcUxjWg2zR2MwouP2eHlCBzdV4pygudehaKXSGW4v2AsRQUK+lwwXhii6KFZcunEnmSUoYp5CXibxtA==} engines: {node: '>= 0.4'} - obug@2.1.1: - resolution: {integrity: sha512-uTqF9MuPraAQ+IsnPf366RG4cP9RtUi7MLO1N3KEc+wb0a6yKpeL0lmk2IB1jY5KHPAlTc6T/JRdC/YqxHNwkQ==} + obug@2.1.3: + resolution: {integrity: sha512-9miFgM2OFba7hB+pRgvtV84pYTBaoTHohvmIgiRt6dRIzbwEOIaNaP+dIlGs2fNFoB0SeISs0Jz5WFVRid6Xyg==} + engines: {node: '>=12.20.0'} ohash@1.1.6: resolution: {integrity: sha512-TBu7PtV8YkAZn0tSxobKY2n2aAQva936lhRrj6957aDaCf9IEtqsKbgMzXE/F/sjqYOwmrukeORHNLe5glk7Cg==} @@ -3361,16 +3373,20 @@ packages: resolution: {integrity: sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==} engines: {node: '>=12'} + picomatch@4.0.5: + resolution: {integrity: sha512-RvwwcruNjI1ncT5xRakeyS9Lf8lcItv34KD+aif+VH9kduAyfYBipGh12274xtenIPZ119/R9BdTBa8gAwSh0A==} + engines: {node: '>=12'} + pkg-types@1.3.1: resolution: {integrity: sha512-/Jm5M4RvtBFVkKWRu2BLUTNP8/M2a+UwuAX+ae4770q1qVGtfjG+WTCupoZixokjmHiry8uI+dlY8KXYV5HVVQ==} - playwright-core@1.60.0: - resolution: {integrity: sha512-9bW6zvX/m0lEbgTKJ6YppOKx8H3VOPBMOCFh2irXFOT4BbHgrx5hPjwJYLT40Lu+4qtD36qKc/Hn56StUW57IA==} + playwright-core@1.61.1: + resolution: {integrity: sha512-h7Qlt6m4REp25qvIdvbDtVmD4LqVXfpRxhORv9L0jzETM05p4fuPJ3dKyuSXQxDSbXnmS79HAgi9589lGSpLkg==} engines: {node: '>=18'} hasBin: true - playwright@1.60.0: - resolution: {integrity: sha512-hheHdokM8cdqCb0lcE3s+zT4t4W+vvjpGxsZlDnikarzx8tSzMebh3UiFtgqwFwnTnjYQcsyMF8ei2mCO/tpeA==} + playwright@1.61.1: + resolution: {integrity: sha512-DWnY5o3YbLWK4GovuAVwpqL+1VwGNdUGrRr++8j8PtQQzvAVZUIMjKQ90fY689sEJZJBbZVw1rXaOKSTitkzPQ==} engines: {node: '>=18'} hasBin: true @@ -3382,8 +3398,8 @@ packages: resolution: {integrity: sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg==} engines: {node: '>= 0.4'} - postcss@8.5.15: - resolution: {integrity: sha512-FfR8sjd4em2T6fb3I2MwAJU7HWVMr9zba+enmQeeWFfCbm+UOC/0X4DS8XtpUTMwWMGbjKYP7xjfNekzyGmB3A==} + postcss@8.5.16: + resolution: {integrity: sha512-vuwillviilfKZsg0VGj5R/YwwcHx4SLsIOI/7K6mQkWx+l5cUHTjj5g0AasTBcyXsbfTgrwsUNmVUb5xVwyPwg==} engines: {node: ^10 || ^12 || >=14} prelude-ls@1.2.1: @@ -3394,8 +3410,8 @@ packages: resolution: {integrity: sha512-SxToR7P8Y2lWmv/kTzVLC1t/GDI2WGjMwNhLLE9qtH8Q13C+aEmuRlzDst4Up4s0Wc8sF2M+J57iB3cMLqftfg==} engines: {node: '>=6.0.0'} - prettier@3.8.1: - resolution: {integrity: sha512-UOnG6LftzbdaHZcKoPFtOcCKztrQ57WkHDeRD9t/PTQtmT0NHSeWWepj6pS0z/N7+08BHFDQVUrfmfMRcZwbMg==} + prettier@3.9.4: + resolution: {integrity: sha512-yWG/o/4oJfo036EKAfK6ACAoDOfHeRHx4tuxkfBZiauURiaSmYwlpOr5LQqKtIkRD2z1PLteme2WoxEnj4tHTg==} engines: {node: '>=14'} hasBin: true @@ -3439,27 +3455,27 @@ packages: chart.js: ^4.1.1 react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 - react-dom@19.2.6: - resolution: {integrity: sha512-0prMI+hvBbPjsWnxDLxlCGyM8PN6UuWjEUCYmZhO67xIV9Xasa/r/vDnq+Xyq4Lo27g8QSbO5YzARu0D1Sps3g==} + react-dom@19.2.7: + resolution: {integrity: sha512-t0BRVXvbiE/o20Hfw669rLbMCDWtYZLvmJigy2f0MxsXF+71pxhR3xOkspmsO8h3ZlNzyibAmtCa3l4lYKk6gQ==} peerDependencies: - react: ^19.2.6 + react: ^19.2.7 - react-hook-form@7.71.2: - resolution: {integrity: sha512-1CHvcDYzuRUNOflt4MOq3ZM46AronNJtQ1S7tnX6YN4y72qhgiUItpacZUAQ0TyWYci3yz1X+rXaSxiuEm86PA==} + react-hook-form@7.80.0: + resolution: {integrity: sha512-4P+fk6oXsxY+6xSj7Euhc2sumQD8zQqCuVHoJwoyp9EchP+IUW9OESB7uHFJOKsIBQ4MQqYE84INJFqUCYNoOg==} engines: {node: '>=18.0.0'} peerDependencies: react: ^16.8.0 || ^17 || ^18 || ^19 - react-hotkeys-hook@4.6.1: - resolution: {integrity: sha512-XlZpbKUj9tkfgPgT9gA+1p7Ey6vFIZHttUjPqpTdyT5nqQ8mHL7elxvSbaC+dpSiHUSmr21Ya1mDxBZG3aje4Q==} + react-hotkeys-hook@4.6.2: + resolution: {integrity: sha512-FmP+ZriY3EG59Ug/lxNfrObCnW9xQShgk7Nb83+CkpfkcCpfS95ydv+E9JuXA5cp8KtskU7LGlIARpkc92X22Q==} peerDependencies: react: '>=16.8.1' react-dom: '>=16.8.1' - react-i18next@16.6.5: - resolution: {integrity: sha512-bfdJhmyjQCXtU9CLcGMn3a1V5/jTeUX/x29cOhlS1Lolm/epRtm24gnYsltxArsc29ow3klSJEijjfYXc5kxjg==} + react-i18next@16.6.6: + resolution: {integrity: sha512-ZgL2HUoW34UKUkOV7uSQFE1CDnRPD+tCR3ywSuWH7u2iapnz86U8Bi3Vrs620qNDzCf1F47NxglCEkchCTDOHw==} peerDependencies: - i18next: '>= 25.6.2' + i18next: '>= 25.10.9' react: '>= 16.8.0' react-dom: '*' react-native: '*' @@ -3472,8 +3488,8 @@ packages: typescript: optional: true - react-icons@5.6.0: - resolution: {integrity: sha512-RH93p5ki6LfOiIt0UtDyNg/cee+HLVR6cHHtW3wALfo+eOHTp8RnU2kRkI6E+H19zMIs03DyxUG/GfZMOGvmiA==} + react-icons@5.7.0: + resolution: {integrity: sha512-LBLy340Rzqy6+/yVhZKT3B/QpP1BZaesGqasf09HPOBzRarcDIFH0WwXlXQfE7q7ipxK4MSiC5DIBWURCny6fw==} peerDependencies: react: '*' @@ -3501,15 +3517,15 @@ packages: react: ^16.14.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc react-dom: ^16.14.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc - react-router-dom@7.13.1: - resolution: {integrity: sha512-UJnV3Rxc5TgUPJt2KJpo1Jpy0OKQr0AjgbZzBFjaPJcFOb2Y8jA5H3LT8HUJAiRLlWrEXWHbF1Z4SCZaQjWDHw==} + react-router-dom@7.18.1: + resolution: {integrity: sha512-KaZh+X/6UtEp28x51AUYZDMg9NGoz2ja3dNHa+ta/tk40vCzKhQ/RypCWBMLbmDr6//E24Vv5uPsrqXFozdkAg==} engines: {node: '>=20.0.0'} peerDependencies: react: '>=18' react-dom: '>=18' - react-router@7.16.0: - resolution: {integrity: sha512-wArC8lVyJb3+jM9OpDyW6hLCizACWkvQR/sSGqSs+o5uEXEtGlqdZ4v8hENR3Jad6i+LRkK93q/+bQAcvl6V1A==} + react-router@7.18.1: + resolution: {integrity: sha512-GDLgg3i3uM0aeJO3Fm+TCS+sDQ7gu12T6x0qdTEzcwqEfleci7JwugVNIF3U//0FWKnJT7ptG+20B2jfDqnZAg==} engines: {node: '>=20.0.0'} peerDependencies: react: '>=18' @@ -3518,14 +3534,14 @@ packages: react-dom: optional: true - react-select@5.10.1: - resolution: {integrity: sha512-roPEZUL4aRZDx6DcsD+ZNreVl+fM8VsKn0Wtex1v4IazH60ILp5xhdlp464IsEAlJdXeD+BhDAFsBVMfvLQueA==} + react-select@5.10.2: + resolution: {integrity: sha512-Z33nHdEFWq9tfnfVXaiM12rbJmk+QjFEztWLtmXqQhz6Al4UZZ9xc0wiatmGtUOCCnHN0WizL3tCMYRENX4rVQ==} peerDependencies: react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 - react-syntax-highlighter@15.6.1: - resolution: {integrity: sha512-OqJ2/vL7lEeV5zTJyG7kmARppUjiB9h9udl4qHQjjgEos66z00Ia0OckwYfRxCSFrW8RJIBnsBwQsHZbVPspqg==} + react-syntax-highlighter@15.6.6: + resolution: {integrity: sha512-DgXrc+AZF47+HvAPEmn7Ua/1p10jNoVZVI/LoPiYdtY+OM+/nG5yefLHKJwdKqY1adMuHFbeyBaG9j64ML7vTw==} peerDependencies: react: '>= 0.14.0' @@ -3535,8 +3551,8 @@ packages: react: '>=16.6.0' react-dom: '>=16.6.0' - react@19.2.6: - resolution: {integrity: sha512-sfWGGfavi0xr8Pg0sVsyHMAOziVYKgPLNrS7ig+ivMNb3wbCBw3KxtflsGBAwD3gYQlE/AEZsTLgToRrSCjb0Q==} + react@19.2.7: + resolution: {integrity: sha512-HNe9WslTbXmFK8o8cmwgAeJFSBvt1bPdHCVKtaaV+WlAN36mpT4hcRpwbf3fY56ar2oIXzsBpOAiIRHAdY0OlQ==} engines: {node: '>=0.10.0'} readdirp@3.6.0: @@ -3602,14 +3618,14 @@ packages: resolution: {integrity: sha512-U7WjGVG9sH8tvjW5SmGbQuui75FiyjAX72HX15DwBBwF9dNiQZRQAg9nnPhYy+TUnE0+VcrttuvNI8oSxZcocA==} hasBin: true - rettime@0.10.1: - resolution: {integrity: sha512-uyDrIlUEH37cinabq0AX4QbgV4HbFZ/gqoiunWQ1UqBtRvTTytwhNYjE++pO/MjPTZL5KQCf2bEoJ/BJNVQ5Kw==} + rettime@0.11.11: + resolution: {integrity: sha512-ILJRqVWBCTlg9r42fFgwVZx1gnFAcQF8mRoMkbgQfIrjEDf9nbBFDFx00oloOa+Q869FUtaYDXZvEfnecQSCoQ==} robust-predicates@3.0.2: resolution: {integrity: sha512-IXgzBWvWQwE6PrDI05OvmXUIruQTcoMDzRsOd5CDvHCVLcLHMTSYvOK5Cm46kWqlV3yAbuSpBZdJ5oP5OUoStg==} - rolldown@1.0.3: - resolution: {integrity: sha512-i00lAJ2ks1BYr7rjNjKC7BcqAS7nVfiT3QX1SI5aY+AFHblCmaUf9OE9dbdzDvW6dJxbi2ZCZiy9v3CcwOiX3g==} + rolldown@1.1.4: + resolution: {integrity: sha512-IjZYiLxZwpnhwhdBH2ugdTGVSdhCQUmLxLoqyjiL0JxYjyRst+5a0P3xfrTxJ5F638j4Mvvw5FAX5XE6eHpXbA==} engines: {node: ^20.19.0 || >=22.12.0} hasBin: true @@ -3632,29 +3648,22 @@ packages: resolution: {integrity: sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==} hasBin: true - semver@7.7.1: - resolution: {integrity: sha512-hlq8tAfn0m/61p4BVRcPzIGr6LKiMwo4VM6dGi6pt4qcRkmNzTcWq6eCEjEh+qXjkMDvPlOFFSGwQjoEa6gyMA==} - engines: {node: '>=10'} - hasBin: true - semver@7.7.4: resolution: {integrity: sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==} engines: {node: '>=10'} hasBin: true - semver@7.8.0: - resolution: {integrity: sha512-AcM7dV/5ul4EekoQ29Agm5vri8JNqRyj39o0qpX6vDF2GZrtutZl5RwgD1XnZjiTAfncsJhMI48QQH3sN87YNA==} - engines: {node: '>=10'} - hasBin: true - - semver@7.8.1: - resolution: {integrity: sha512-rkVq3IXh+4FDGch+KwzX3aV9W3kO54GyEgpvBzSyctDA6Xtd7RJQV1xmXbeQp5v7+VzLOfVqiutSE6GICgPFvg==} + semver@7.8.5: + resolution: {integrity: sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==} engines: {node: '>=10'} hasBin: true set-cookie-parser@2.7.2: resolution: {integrity: sha512-oeM1lpU/UvhTxw+g3cIfxXHyJRc/uidd3yK1P242gzHds0udQBYzs3y8j4gCCW+ZJ7ad0yctld8RYO+bdurlvw==} + set-cookie-parser@3.1.1: + resolution: {integrity: sha512-vM9SUhjsUYs6UeJUmygc5Ofm5eQGe85riob5ju6XCgFGJI5PLV4nrDAQpQjd+LkFBpAkADn5BQQpZ9EUNkyLuA==} + set-function-length@1.2.2: resolution: {integrity: sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==} engines: {node: '>= 0.4'} @@ -3729,8 +3738,8 @@ packages: resolution: {integrity: sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==} engines: {node: '>= 0.8'} - std-env@4.0.0: - resolution: {integrity: sha512-zUMPtQ/HBY3/50VbpkupYHbRroTRZJPRLvreamgErJVys0ceuzMkD44J/QjqhHjOzK42GQ3QZIeFG1OYfOtKqQ==} + std-env@4.1.0: + resolution: {integrity: sha512-Rq7ybcX2RuC55r9oaPVEW7/xu3tj8u4GeBYHBWCychFtzMIr86A7e3PPEBPT37sHStKX3+TiX/Fr/ACmJLVlLQ==} strict-event-emitter@0.5.1: resolution: {integrity: sha512-vMgjE/GGEPEFnhFub6pa4FmJBRBVOLpIII2hvCZ8Kzb7K0hlHo7mQv6xYrBvCL2LtAIBwFUK8wvuJgTVSQ5MFQ==} @@ -3794,8 +3803,8 @@ packages: resolution: {integrity: sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==} engines: {node: '>= 0.4'} - synckit@0.11.12: - resolution: {integrity: sha512-Bh7QjT8/SuKUIfObSXNHNSK6WHo6J1tHCqJsuaFDP7gP0fkzSfTxI8y85JrppZ0h8l0maIgc2tfuZQ6/t3GtnQ==} + synckit@0.11.13: + resolution: {integrity: sha512-eNRKgb3z66Yp3D2CixVujOUvXLFUTij/zVnV8KRyvFdQwpz7I5DS8UfRkTeLzb64u+dkzDSdelE24izu+zSSUg==} engines: {node: ^14.18.0 || >=16.0.0} tagged-tag@1.0.0: @@ -3812,14 +3821,10 @@ packages: tinyexec@0.3.2: resolution: {integrity: sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA==} - tinyexec@1.1.1: - resolution: {integrity: sha512-VKS/ZaQhhkKFMANmAOhhXVoIfBXblQxGX1myCQ2faQrfmobMftXeJPcZGp0gS07ocvGJWDLZGyOZDadDBqYIJg==} + tinyexec@1.2.4: + resolution: {integrity: sha512-SHf/r48b7vOrjve9PxJo3MN5v5yuyjHvdUcrQffT3WXMUfnGmHDVbC4k3sHJaJTgZCwpUplIaAo5ANtMyp3YHg==} engines: {node: '>=18'} - tinyglobby@0.2.15: - resolution: {integrity: sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==} - engines: {node: '>=12.0.0'} - tinyglobby@0.2.17: resolution: {integrity: sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==} engines: {node: '>=12.0.0'} @@ -3828,11 +3833,11 @@ packages: resolution: {integrity: sha512-Bf+ILmBgretUrdJxzXM0SgXLZ3XfiaUuOj/IKQHuTXip+05Xn+uyEYdVg0kYDipTBcLrCVyUzAPz7QmArb0mmw==} engines: {node: '>=14.0.0'} - tldts-core@7.0.25: - resolution: {integrity: sha512-ZjCZK0rppSBu7rjHYDYsEaMOIbbT+nWF57hKkv4IUmZWBNrBWBOjIElc0mKRgLM8bm7x/BBlof6t2gi/Oq/Asw==} + tldts-core@7.4.6: + resolution: {integrity: sha512-TkQNGJIhlEphpHCjKodMTSe23egUZr/g+flI2qkLgiJ/maAzSgXypSLRTNH3nCmqgayEmtcJBiLcfODSAr1xoA==} - tldts@7.0.25: - resolution: {integrity: sha512-keinCnPbwXEUG3ilrWQZU+CqcTTzHq9m2HhoUP2l7Xmi8l1LuijAXLpAJ5zRW+ifKTNscs4NdCkfkDCBYm352w==} + tldts@7.4.6: + resolution: {integrity: sha512-rbP0Gyx8b3Ae9yO//CU2wbSnQNoQ66m1nJdSbSHmnwKwzkkz/u8mERYU8T2rmlmy+bJvRNn84yNCW8gYqox44Q==} hasBin: true to-fast-properties@2.0.0: @@ -3843,8 +3848,8 @@ packages: resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==} engines: {node: '>=8.0'} - tough-cookie@6.0.0: - resolution: {integrity: sha512-kXuRi1mtaKMrsLUxz3sQYvVl37B0Ns6MzfrtV5DvJceE9bPyspOqk9xxv7XbZWcfLWbFmm997vl83qUWVJA64w==} + tough-cookie@6.0.1: + resolution: {integrity: sha512-LktZQb3IeoUWB9lqR5EWTHgW/VTITCXg4D21M+lvybRVdylLrRMnqaIONLVb5mav8vM19m44HIcGq4qASeu2Qw==} engines: {node: '>=16'} tr46@0.0.3: @@ -3875,8 +3880,8 @@ packages: resolution: {integrity: sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==} engines: {node: '>= 0.8.0'} - type-fest@5.4.4: - resolution: {integrity: sha512-JnTrzGu+zPV3aXIUhnyWJj4z/wigMsdYajGLIYakqyOW1nPllzXEJee0QQbHj+CTIQtXGlAjuK0UY+2xTyjVAw==} + type-fest@5.8.0: + resolution: {integrity: sha512-YGYEVz3Fm5iy/AybuA0oyNFq7H4CgQNfRp/qfe8nurE1kuCeNm3/vfm9X4Mtl+qLyaKJUh5xrFZwogr41SMjYA==} engines: {node: '>=20'} typed-array-buffer@1.0.3: @@ -3895,8 +3900,8 @@ packages: resolution: {integrity: sha512-3KS2b+kL7fsuk/eJZ7EQdnEmQoaho/r6KUef7hxvltNA5DR8NAUM+8wJMbJyZ4G9/7i3v5zPBIMN5aybAh2/Jg==} engines: {node: '>= 0.4'} - typescript-eslint@8.60.0: - resolution: {integrity: sha512-9f65qWLZdAW9m1JaxBDUHcqRUfL8bkxxXL7XxEfI+F09q56PkBvIfCjLF3yInsDM/BBmwkqmCQdCZe/RYlIWEw==} + typescript-eslint@8.62.1: + resolution: {integrity: sha512-vymnnM5g0AKQDSAyfP12nMIBvgwgA42syg74kkuZ4x1VuTzwQKwc5h9rGxeShCjny5o+zWAb6OEoz7XLgrIkIw==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 @@ -3919,8 +3924,8 @@ packages: resolution: {integrity: sha512-nWJ91DjeOkej/TA8pXQ3myruKpKEYgqvpw9lz4OPHj/NWFNluYrjbz9j01CJ8yKQd2g4jFoOkINCTW2I5LEEyw==} engines: {node: '>= 0.4'} - undici-types@7.16.0: - resolution: {integrity: sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==} + undici-types@7.18.2: + resolution: {integrity: sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w==} unified@11.0.5: resolution: {integrity: sha512-xKvGhPWw3k84Qjh8bI3ZeJjqnyadK+GEFtazSfZv/rKeTkTjOJho6mFqh2SM96iIcZokxiOpg78GazTSg8+KHA==} @@ -3949,8 +3954,8 @@ packages: peerDependencies: browserslist: '>= 4.21.0' - uqr@0.1.2: - resolution: {integrity: sha512-MJu7ypHq6QasgF5YRTjqscSzQp/W11zoUk6kvmlH+fmWEs63Y0Eib13hYFwAzagRJcVY8WVnlV+eBDUGMJ5IbA==} + uqr@0.1.3: + resolution: {integrity: sha512-0rjE8iEJe4YmT9TOhwsZtqCMRLc5DXZUI2UEYUUg63ikBkqqE5EYWaI0etFe/5KUcmcYwLih2RND1kq+hrUJXA==} uri-js@4.4.1: resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==} @@ -3958,8 +3963,8 @@ packages: urijs@1.19.11: resolution: {integrity: sha512-HXgFDgDommxn5/bIv0cnQZsPhHDA90NPHD6+c/v21U5+Sx5hoP8+dP9IZXBU1gIfvdRfhG8cel9QNPeionfcCQ==} - use-debounce@10.1.0: - resolution: {integrity: sha512-lu87Za35V3n/MyMoEpD5zJv0k7hCn0p+V/fK2kWD+3k2u3kOCwO593UArbczg1fhfs2rqPEnHpULJ3KmGdDzvg==} + use-debounce@10.1.1: + resolution: {integrity: sha512-kvds8BHR2k28cFsxW8k3nc/tRga2rs1RHYCqmmGqb90MEeE++oALwzh2COiuBLO1/QXiOuShXoSN2ZpWnMmvuQ==} engines: {node: '>= 16.0.0'} peerDependencies: react: '*' @@ -3995,13 +4000,13 @@ packages: peerDependencies: vite: '>2.0.0-0' - vite@8.0.16: - resolution: {integrity: sha512-h9bXPmJichP5fLmVQo3PyaGSDE2n3aPuomeAlVRm0JLmt4rY6zmPKd59HYI4LNW8oTK7tlTsuC7l/m7awx9Jcw==} + vite@8.1.2: + resolution: {integrity: sha512-6YYPbRXTxx6bRXmOn7XdnQAy5DQNHhDgtjhDHI13oe4pY93kkcdGJWxpGwOm++/Wh0QpQhDrpIoVMrmrsI5AGQ==} engines: {node: ^20.19.0 || >=22.12.0} hasBin: true peerDependencies: '@types/node': ^20.19.0 || >=22.12.0 - '@vitejs/devtools': ^0.1.18 + '@vitejs/devtools': ^0.3.0 esbuild: '>=0.28.1' jiti: '>=1.21.0' less: ^4.0.0 @@ -4038,20 +4043,20 @@ packages: yaml: optional: true - vitest@4.1.4: - resolution: {integrity: sha512-tFuJqTxKb8AvfyqMfnavXdzfy3h3sWZRWwfluGbkeR7n0HUev+FmNgZ8SDrRBTVrVCjgH5cA21qGbCffMNtWvg==} + vitest@4.1.9: + resolution: {integrity: sha512-nE3/LEyc0z87uHYLZebqCUOaJr2hdtuPp7BQ4BosVFnfltxgAvMG08NyrSGlPpOUWvR27c5flSmYFTNr78L9GQ==} engines: {node: ^20.0.0 || ^22.0.0 || >=24.0.0} hasBin: true peerDependencies: '@edge-runtime/vm': '*' '@opentelemetry/api': ^1.9.0 '@types/node': ^20.0.0 || ^22.0.0 || >=24.0.0 - '@vitest/browser-playwright': 4.1.4 - '@vitest/browser-preview': 4.1.4 - '@vitest/browser-webdriverio': 4.1.4 - '@vitest/coverage-istanbul': 4.1.4 - '@vitest/coverage-v8': 4.1.4 - '@vitest/ui': 4.1.4 + '@vitest/browser-playwright': 4.1.9 + '@vitest/browser-preview': 4.1.9 + '@vitest/browser-webdriverio': 4.1.9 + '@vitest/coverage-istanbul': 4.1.9 + '@vitest/coverage-v8': 4.1.9 + '@vitest/ui': 4.1.9 happy-dom: '>=20.8.8' jsdom: '*' vite: ^6.0.0 || ^7.0.0 || ^8.0.0 @@ -4132,10 +4137,6 @@ packages: wordwrap@1.0.0: resolution: {integrity: sha512-gvVzJFlPycKc5dZN4yPkP8w7Dc37BtP1yczEneOb4uq34pXZcvrtRTmWV8W+Ume+XCxKgbjM+nevkyFPMybd4Q==} - wrap-ansi@6.2.0: - resolution: {integrity: sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==} - engines: {node: '>=8'} - wrap-ansi@7.0.0: resolution: {integrity: sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==} engines: {node: '>=10'} @@ -4167,8 +4168,8 @@ packages: resolution: {integrity: sha512-YgvUTfwqyc7UXVMrB+SImsVYSmTS8X/tSrtdNZMImM+n7+QTriRXyXim0mBrTXNeqzVF0KWGgHPeiyViFFrNDw==} engines: {node: '>=18'} - yaml@2.8.3: - resolution: {integrity: sha512-AvbaCLOO2Otw/lW5bmh9d/WEdcDFdQp2Z2ZUH3pX9U2ihyUY0nvLv7J6TrWowklRGPYbB/IuIMfYgxaCPg5Bpg==} + yaml@2.9.0: + resolution: {integrity: sha512-2AvhNX3mb8zd6Zy7INTtSpl1F15HW6Wnqj0srWlkKLcpYl/gMIMJiyuGq2KeI2YFxUPjdlB+3Lc10seMLtL4cA==} engines: {node: '>= 14.6'} hasBin: true @@ -4176,18 +4177,14 @@ packages: resolution: {integrity: sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==} engines: {node: '>=12'} - yargs@17.7.2: - resolution: {integrity: sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==} + yargs@17.7.3: + resolution: {integrity: sha512-GZtjxm/J/4TSxuL3FNYjCmLktBTnIw/rVmKSIyKeYAZpmJB2ig9VauCC5xsa82GNKVKDAqpOn3KVzNt0zmrU0g==} engines: {node: '>=12'} yocto-queue@0.1.0: resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==} engines: {node: '>=10'} - yoctocolors-cjs@2.1.3: - resolution: {integrity: sha512-U/PBtDf35ff0D8X8D0jfdzHYEPFxAI7jJlxZXwCSez5M3190m+QobIfh+sWDWSHMCWWJN2AWamkegn6vr6YBTw==} - engines: {node: '>=18'} - zod-validation-error@4.0.2: resolution: {integrity: sha512-Q6/nZLe6jxuU80qb/4uJ4t5v2VEZ44lzQjPDhYJNztRQ4wyWc6VF3D3Kb/fAuPetZQnhS3hnajCf9CsWesghLQ==} engines: {node: '>=18.0.0'} @@ -4212,8 +4209,8 @@ packages: react: optional: true - zustand@5.0.11: - resolution: {integrity: sha512-fdZY+dk7zn/vbWNCYmzZULHRrss0jx5pPFiOuMZ/5HJN6Yv3u+1Wswy/4MpZEkEGhtNH+pwxZB8OKgUBPzYAGg==} + zustand@5.0.14: + resolution: {integrity: sha512-/8tAspM5LMPr28b3fwLYrtdj77ECpfZviaP75CMTnwO8ISyaE4GDIG/9rDDYq/cH9D2Xw2A2RXglLInmVBQB/g==} engines: {node: '>=12.20.0'} peerDependencies: '@types/react': '>=18.0.0' @@ -4253,75 +4250,77 @@ snapshots: '@types/json-schema': 7.0.15 js-yaml: 4.1.1 - '@ark-ui/react@5.34.1(react-dom@19.2.6(react@19.2.6))(react@19.2.6)': - dependencies: - '@internationalized/date': 3.11.0 - '@zag-js/accordion': 1.35.3 - '@zag-js/anatomy': 1.35.3 - '@zag-js/angle-slider': 1.35.3 - '@zag-js/async-list': 1.35.3 - '@zag-js/auto-resize': 1.35.3 - '@zag-js/avatar': 1.35.3 - '@zag-js/carousel': 1.35.3 - '@zag-js/cascade-select': 1.35.3 - '@zag-js/checkbox': 1.35.3 - '@zag-js/clipboard': 1.35.3 - '@zag-js/collapsible': 1.35.3 - '@zag-js/collection': 1.35.3 - '@zag-js/color-picker': 1.35.3 - '@zag-js/color-utils': 1.35.3 - '@zag-js/combobox': 1.35.3 - '@zag-js/core': 1.35.3 - '@zag-js/date-picker': 1.35.3(@internationalized/date@3.11.0) - '@zag-js/date-utils': 1.35.3(@internationalized/date@3.11.0) - '@zag-js/dialog': 1.35.3 - '@zag-js/dom-query': 1.35.3 - '@zag-js/drawer': 1.35.3 - '@zag-js/editable': 1.35.3 - '@zag-js/file-upload': 1.35.3 - '@zag-js/file-utils': 1.35.3 - '@zag-js/floating-panel': 1.35.3 - '@zag-js/focus-trap': 1.35.3 - '@zag-js/highlight-word': 1.35.3 - '@zag-js/hover-card': 1.35.3 - '@zag-js/i18n-utils': 1.35.3 - '@zag-js/image-cropper': 1.35.3 - '@zag-js/json-tree-utils': 1.35.3 - '@zag-js/listbox': 1.35.3 - '@zag-js/marquee': 1.35.3 - '@zag-js/menu': 1.35.3 - '@zag-js/navigation-menu': 1.35.3 - '@zag-js/number-input': 1.35.3 - '@zag-js/pagination': 1.35.3 - '@zag-js/password-input': 1.35.3 - '@zag-js/pin-input': 1.35.3 - '@zag-js/popover': 1.35.3 - '@zag-js/presence': 1.35.3 - '@zag-js/progress': 1.35.3 - '@zag-js/qr-code': 1.35.3 - '@zag-js/radio-group': 1.35.3 - '@zag-js/rating-group': 1.35.3 - '@zag-js/react': 1.35.3(react-dom@19.2.6(react@19.2.6))(react@19.2.6) - '@zag-js/scroll-area': 1.35.3 - '@zag-js/select': 1.35.3 - '@zag-js/signature-pad': 1.35.3 - '@zag-js/slider': 1.35.3 - '@zag-js/splitter': 1.35.3 - '@zag-js/steps': 1.35.3 - '@zag-js/switch': 1.35.3 - '@zag-js/tabs': 1.35.3 - '@zag-js/tags-input': 1.35.3 - '@zag-js/timer': 1.35.3 - '@zag-js/toast': 1.35.3 - '@zag-js/toggle': 1.35.3 - '@zag-js/toggle-group': 1.35.3 - '@zag-js/tooltip': 1.35.3 - '@zag-js/tour': 1.35.3 - '@zag-js/tree-view': 1.35.3 - '@zag-js/types': 1.35.3 - '@zag-js/utils': 1.35.3 - react: 19.2.6 - react-dom: 19.2.6(react@19.2.6) + '@ark-ui/react@5.37.2(react-dom@19.2.7(react@19.2.7))(react@19.2.7)': + dependencies: + '@internationalized/date': 3.12.2 + '@zag-js/accordion': 1.41.2 + '@zag-js/anatomy': 1.41.2 + '@zag-js/angle-slider': 1.41.2 + '@zag-js/async-list': 1.41.2 + '@zag-js/auto-resize': 1.41.2 + '@zag-js/avatar': 1.41.2 + '@zag-js/carousel': 1.41.2 + '@zag-js/cascade-select': 1.41.2 + '@zag-js/checkbox': 1.41.2 + '@zag-js/clipboard': 1.41.2 + '@zag-js/collapsible': 1.41.2 + '@zag-js/collection': 1.41.2 + '@zag-js/color-picker': 1.41.2 + '@zag-js/color-utils': 1.41.2 + '@zag-js/combobox': 1.41.2 + '@zag-js/core': 1.41.2 + '@zag-js/date-input': 1.41.2(@internationalized/date@3.12.2) + '@zag-js/date-picker': 1.41.2(@internationalized/date@3.12.2) + '@zag-js/date-utils': 1.41.2(@internationalized/date@3.12.2) + '@zag-js/dialog': 1.41.2 + '@zag-js/dom-query': 1.41.2 + '@zag-js/drawer': 1.41.2 + '@zag-js/editable': 1.41.2 + '@zag-js/file-upload': 1.41.2 + '@zag-js/file-utils': 1.41.2 + '@zag-js/floating-panel': 1.41.2 + '@zag-js/focus-trap': 1.41.2 + '@zag-js/focus-visible': 1.41.2 + '@zag-js/highlight-word': 1.41.2 + '@zag-js/hover-card': 1.41.2 + '@zag-js/i18n-utils': 1.41.2 + '@zag-js/image-cropper': 1.41.2 + '@zag-js/json-tree-utils': 1.41.2 + '@zag-js/listbox': 1.41.2 + '@zag-js/marquee': 1.41.2 + '@zag-js/menu': 1.41.2 + '@zag-js/navigation-menu': 1.41.2 + '@zag-js/number-input': 1.41.2 + '@zag-js/pagination': 1.41.2 + '@zag-js/password-input': 1.41.2 + '@zag-js/pin-input': 1.41.2 + '@zag-js/popover': 1.41.2 + '@zag-js/presence': 1.41.2 + '@zag-js/progress': 1.41.2 + '@zag-js/qr-code': 1.41.2 + '@zag-js/radio-group': 1.41.2 + '@zag-js/rating-group': 1.41.2 + '@zag-js/react': 1.41.2(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@zag-js/scroll-area': 1.41.2 + '@zag-js/select': 1.41.2 + '@zag-js/signature-pad': 1.41.2 + '@zag-js/slider': 1.41.2 + '@zag-js/splitter': 1.41.2 + '@zag-js/steps': 1.41.2 + '@zag-js/switch': 1.41.2 + '@zag-js/tabs': 1.41.2 + '@zag-js/tags-input': 1.41.2 + '@zag-js/timer': 1.41.2 + '@zag-js/toast': 1.41.2 + '@zag-js/toggle': 1.41.2 + '@zag-js/toggle-group': 1.41.2 + '@zag-js/tooltip': 1.41.2 + '@zag-js/tour': 1.41.2 + '@zag-js/tree-view': 1.41.2 + '@zag-js/types': 1.41.2 + '@zag-js/utils': 1.41.2 + react: 19.2.7 + react-dom: 19.2.7(react@19.2.7) '@babel/code-frame@7.26.2': dependencies: @@ -4374,7 +4373,7 @@ snapshots: '@babel/parser': 7.26.10 '@babel/types': 7.26.10 '@jridgewell/gen-mapping': 0.3.8 - '@jridgewell/trace-mapping': 0.3.25 + '@jridgewell/trace-mapping': 0.3.31 jsesc: 3.1.0 '@babel/generator@7.29.1': @@ -4411,7 +4410,7 @@ snapshots: '@babel/helper-module-imports@7.25.9': dependencies: '@babel/traverse': 7.29.0 - '@babel/types': 7.29.0 + '@babel/types': 7.29.7 transitivePeerDependencies: - supports-color @@ -4439,8 +4438,7 @@ snapshots: '@babel/helper-string-parser@7.27.1': {} - '@babel/helper-string-parser@7.29.7': - optional: true + '@babel/helper-string-parser@7.29.7': {} '@babel/helper-validator-identifier@7.28.5': {} @@ -4464,7 +4462,6 @@ snapshots: '@babel/parser@7.29.7': dependencies: '@babel/types': 7.29.7 - optional: true '@babel/runtime@7.26.10': dependencies: @@ -4472,8 +4469,6 @@ snapshots: '@babel/runtime@7.28.6': {} - '@babel/runtime@7.29.2': {} - '@babel/runtime@7.29.7': {} '@babel/template@7.28.6': @@ -4528,37 +4523,36 @@ snapshots: dependencies: '@babel/helper-string-parser': 7.29.7 '@babel/helper-validator-identifier': 7.29.7 - optional: true '@bcoe/v8-coverage@1.0.2': {} '@chakra-ui/anatomy@2.3.4': {} - '@chakra-ui/react@3.34.0(@emotion/react@11.14.0(@types/react@19.2.15)(react@19.2.6))(react-dom@19.2.6(react@19.2.6))(react@19.2.6)': + '@chakra-ui/react@3.36.0(@emotion/react@11.14.0(@types/react@19.2.17)(react@19.2.7))(react-dom@19.2.7(react@19.2.7))(react@19.2.7)': dependencies: - '@ark-ui/react': 5.34.1(react-dom@19.2.6(react@19.2.6))(react@19.2.6) + '@ark-ui/react': 5.37.2(react-dom@19.2.7(react@19.2.7))(react@19.2.7) '@emotion/is-prop-valid': 1.4.0 - '@emotion/react': 11.14.0(@types/react@19.2.15)(react@19.2.6) + '@emotion/react': 11.14.0(@types/react@19.2.17)(react@19.2.7) '@emotion/serialize': 1.3.3 - '@emotion/use-insertion-effect-with-fallbacks': 1.2.0(react@19.2.6) + '@emotion/use-insertion-effect-with-fallbacks': 1.2.0(react@19.2.7) '@emotion/utils': 1.4.2 - '@pandacss/is-valid-prop': 1.9.0 + '@pandacss/is-valid-prop': 1.11.4 csstype: 3.2.3 - react: 19.2.6 - react-dom: 19.2.6(react@19.2.6) + react: 19.2.7 + react-dom: 19.2.7(react@19.2.7) - '@emnapi/core@1.10.0': + '@emnapi/core@1.11.1': dependencies: - '@emnapi/wasi-threads': 1.2.1 + '@emnapi/wasi-threads': 1.2.2 tslib: 2.8.1 optional: true - '@emnapi/runtime@1.10.0': + '@emnapi/runtime@1.11.1': dependencies: tslib: 2.8.1 optional: true - '@emnapi/wasi-threads@1.2.1': + '@emnapi/wasi-threads@1.2.2': dependencies: tslib: 2.8.1 optional: true @@ -4595,19 +4589,19 @@ snapshots: '@emotion/memoize@0.9.0': {} - '@emotion/react@11.14.0(@types/react@19.2.15)(react@19.2.6)': + '@emotion/react@11.14.0(@types/react@19.2.17)(react@19.2.7)': dependencies: '@babel/runtime': 7.26.10 '@emotion/babel-plugin': 11.13.5 '@emotion/cache': 11.14.0 '@emotion/serialize': 1.3.3 - '@emotion/use-insertion-effect-with-fallbacks': 1.2.0(react@19.2.6) + '@emotion/use-insertion-effect-with-fallbacks': 1.2.0(react@19.2.7) '@emotion/utils': 1.4.2 '@emotion/weak-memoize': 0.4.0 hoist-non-react-statics: 3.3.2 - react: 19.2.6 + react: 19.2.7 optionalDependencies: - '@types/react': 19.2.15 + '@types/react': 19.2.17 transitivePeerDependencies: - supports-color @@ -4623,26 +4617,26 @@ snapshots: '@emotion/unitless@0.10.0': {} - '@emotion/use-insertion-effect-with-fallbacks@1.2.0(react@19.2.6)': + '@emotion/use-insertion-effect-with-fallbacks@1.2.0(react@19.2.7)': dependencies: - react: 19.2.6 + react: 19.2.7 '@emotion/utils@1.4.2': {} '@emotion/weak-memoize@0.4.0': {} - '@eslint-community/eslint-utils@4.9.1(eslint@10.3.0(jiti@1.21.7))': + '@eslint-community/eslint-utils@4.9.1(eslint@10.6.0(jiti@1.21.7))': dependencies: - eslint: 10.3.0(jiti@1.21.7) + eslint: 10.6.0(jiti@1.21.7) eslint-visitor-keys: 3.4.3 '@eslint-community/regexpp@4.12.2': {} - '@eslint/compat@2.0.5(eslint@10.3.0(jiti@1.21.7))': + '@eslint/compat@2.1.0(eslint@10.6.0(jiti@1.21.7))': dependencies: '@eslint/core': 1.2.1 optionalDependencies: - eslint: 10.3.0(jiti@1.21.7) + eslint: 10.6.0(jiti@1.21.7) '@eslint/config-array@0.23.5': dependencies: @@ -4652,7 +4646,7 @@ snapshots: transitivePeerDependencies: - supports-color - '@eslint/config-helpers@0.5.5': + '@eslint/config-helpers@0.6.0': dependencies: '@eslint/core': 1.2.1 @@ -4660,35 +4654,21 @@ snapshots: dependencies: '@types/json-schema': 7.0.15 - '@eslint/js@10.0.1(eslint@10.3.0(jiti@1.21.7))': + '@eslint/js@10.0.1(eslint@10.6.0(jiti@1.21.7))': optionalDependencies: - eslint: 10.3.0(jiti@1.21.7) + eslint: 10.6.0(jiti@1.21.7) '@eslint/object-schema@3.0.5': {} - '@eslint/plugin-kit@0.6.1': - dependencies: - '@eslint/core': 1.2.1 - levn: 0.4.1 - - '@eslint/plugin-kit@0.7.1': + '@eslint/plugin-kit@0.7.2': dependencies: '@eslint/core': 1.2.1 levn: 0.4.1 - '@floating-ui/core@1.7.1': - dependencies: - '@floating-ui/utils': 0.2.9 - '@floating-ui/core@1.7.5': dependencies: '@floating-ui/utils': 0.2.11 - '@floating-ui/dom@1.7.1': - dependencies: - '@floating-ui/core': 1.7.1 - '@floating-ui/utils': 0.2.9 - '@floating-ui/dom@1.7.6': dependencies: '@floating-ui/core': 1.7.5 @@ -4696,9 +4676,7 @@ snapshots: '@floating-ui/utils@0.2.11': {} - '@floating-ui/utils@0.2.9': {} - - '@guanmingchiu/sqlparser-ts@0.61.1': {} + '@guanmingchiu/sqlparser-ts@0.62.0': {} '@hey-api/openapi-ts@0.52.0(magicast@0.3.5)(typescript@6.0.3)': dependencies: @@ -4727,41 +4705,40 @@ snapshots: '@humanwhocodes/retry@0.4.3': {} - '@inquirer/ansi@1.0.2': {} + '@inquirer/ansi@2.0.7': {} - '@inquirer/confirm@5.1.21(@types/node@24.10.3)': + '@inquirer/confirm@6.1.1(@types/node@24.13.2)': dependencies: - '@inquirer/core': 10.3.2(@types/node@24.10.3) - '@inquirer/type': 3.0.10(@types/node@24.10.3) + '@inquirer/core': 11.2.1(@types/node@24.13.2) + '@inquirer/type': 4.0.7(@types/node@24.13.2) optionalDependencies: - '@types/node': 24.10.3 + '@types/node': 24.13.2 - '@inquirer/core@10.3.2(@types/node@24.10.3)': + '@inquirer/core@11.2.1(@types/node@24.13.2)': dependencies: - '@inquirer/ansi': 1.0.2 - '@inquirer/figures': 1.0.15 - '@inquirer/type': 3.0.10(@types/node@24.10.3) + '@inquirer/ansi': 2.0.7 + '@inquirer/figures': 2.0.7 + '@inquirer/type': 4.0.7(@types/node@24.13.2) cli-width: 4.1.0 - mute-stream: 2.0.0 + fast-wrap-ansi: 0.2.2 + mute-stream: 3.0.0 signal-exit: 4.1.0 - wrap-ansi: 6.2.0 - yoctocolors-cjs: 2.1.3 optionalDependencies: - '@types/node': 24.10.3 + '@types/node': 24.13.2 - '@inquirer/figures@1.0.15': {} + '@inquirer/figures@2.0.7': {} - '@inquirer/type@3.0.10(@types/node@24.10.3)': + '@inquirer/type@4.0.7(@types/node@24.13.2)': optionalDependencies: - '@types/node': 24.10.3 + '@types/node': 24.13.2 - '@internationalized/date@3.11.0': + '@internationalized/date@3.12.2': dependencies: - '@swc/helpers': 0.5.19 + '@swc/helpers': 0.5.23 - '@internationalized/number@3.6.5': + '@internationalized/number@3.6.6': dependencies: - '@swc/helpers': 0.5.19 + '@swc/helpers': 0.5.23 '@isaacs/cliui@9.0.0': {} @@ -4777,8 +4754,8 @@ snapshots: '@jridgewell/gen-mapping@0.3.8': dependencies: '@jridgewell/set-array': 1.2.1 - '@jridgewell/sourcemap-codec': 1.5.0 - '@jridgewell/trace-mapping': 0.3.25 + '@jridgewell/sourcemap-codec': 1.5.5 + '@jridgewell/trace-mapping': 0.3.31 '@jridgewell/remapping@2.3.5': dependencies: @@ -4789,15 +4766,8 @@ snapshots: '@jridgewell/set-array@1.2.1': {} - '@jridgewell/sourcemap-codec@1.5.0': {} - '@jridgewell/sourcemap-codec@1.5.5': {} - '@jridgewell/trace-mapping@0.3.25': - dependencies: - '@jridgewell/resolve-uri': 3.1.2 - '@jridgewell/sourcemap-codec': 1.5.0 - '@jridgewell/trace-mapping@0.3.31': dependencies: '@jridgewell/resolve-uri': 3.1.2 @@ -4817,14 +4787,14 @@ snapshots: dependencies: state-local: 1.0.7 - '@monaco-editor/react@4.7.0(monaco-editor@0.52.2)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)': + '@monaco-editor/react@4.7.0(monaco-editor@0.55.1)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)': dependencies: '@monaco-editor/loader': 1.5.0 - monaco-editor: 0.52.2 - react: 19.2.6 - react-dom: 19.2.6(react@19.2.6) + monaco-editor: 0.55.1 + react: 19.2.7 + react-dom: 19.2.7(react@19.2.7) - '@mswjs/interceptors@0.41.3': + '@mswjs/interceptors@0.41.9': dependencies: '@open-draft/deferred-promise': 2.2.0 '@open-draft/logger': 0.3.0 @@ -4833,15 +4803,17 @@ snapshots: outvariant: 1.4.3 strict-event-emitter: 0.5.1 - '@napi-rs/wasm-runtime@1.1.5(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)': + '@napi-rs/wasm-runtime@1.1.6(@emnapi/core@1.11.1)(@emnapi/runtime@1.11.1)': dependencies: - '@emnapi/core': 1.10.0 - '@emnapi/runtime': 1.10.0 - '@tybys/wasm-util': 0.10.2 + '@emnapi/core': 1.11.1 + '@emnapi/runtime': 1.11.1 + '@tybys/wasm-util': 0.10.3 optional: true '@open-draft/deferred-promise@2.2.0': {} + '@open-draft/deferred-promise@3.0.0': {} + '@open-draft/logger@0.3.0': dependencies: is-node-process: 1.2.0 @@ -4851,86 +4823,82 @@ snapshots: '@ota-meshi/ast-token-store@0.3.0': {} - '@oxc-project/types@0.133.0': {} + '@oxc-project/types@0.138.0': {} - '@pandacss/is-valid-prop@1.9.0': {} + '@pandacss/is-valid-prop@1.11.4': {} - '@pkgr/core@0.2.9': {} + '@pkgr/core@0.3.6': {} - '@playwright/test@1.60.0': + '@playwright/test@1.61.1': dependencies: - playwright: 1.60.0 + playwright: 1.61.1 - '@rolldown/binding-android-arm64@1.0.3': + '@rolldown/binding-android-arm64@1.1.4': optional: true - '@rolldown/binding-darwin-arm64@1.0.3': + '@rolldown/binding-darwin-arm64@1.1.4': optional: true - '@rolldown/binding-darwin-x64@1.0.3': + '@rolldown/binding-darwin-x64@1.1.4': optional: true - '@rolldown/binding-freebsd-x64@1.0.3': + '@rolldown/binding-freebsd-x64@1.1.4': optional: true - '@rolldown/binding-linux-arm-gnueabihf@1.0.3': + '@rolldown/binding-linux-arm-gnueabihf@1.1.4': optional: true - '@rolldown/binding-linux-arm64-gnu@1.0.3': + '@rolldown/binding-linux-arm64-gnu@1.1.4': optional: true - '@rolldown/binding-linux-arm64-musl@1.0.3': + '@rolldown/binding-linux-arm64-musl@1.1.4': optional: true - '@rolldown/binding-linux-ppc64-gnu@1.0.3': + '@rolldown/binding-linux-ppc64-gnu@1.1.4': optional: true - '@rolldown/binding-linux-s390x-gnu@1.0.3': + '@rolldown/binding-linux-s390x-gnu@1.1.4': optional: true - '@rolldown/binding-linux-x64-gnu@1.0.3': + '@rolldown/binding-linux-x64-gnu@1.1.4': optional: true - '@rolldown/binding-linux-x64-musl@1.0.3': + '@rolldown/binding-linux-x64-musl@1.1.4': optional: true - '@rolldown/binding-openharmony-arm64@1.0.3': + '@rolldown/binding-openharmony-arm64@1.1.4': optional: true - '@rolldown/binding-wasm32-wasi@1.0.3': + '@rolldown/binding-wasm32-wasi@1.1.4': dependencies: - '@emnapi/core': 1.10.0 - '@emnapi/runtime': 1.10.0 - '@napi-rs/wasm-runtime': 1.1.5(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0) + '@emnapi/core': 1.11.1 + '@emnapi/runtime': 1.11.1 + '@napi-rs/wasm-runtime': 1.1.6(@emnapi/core@1.11.1)(@emnapi/runtime@1.11.1) optional: true - '@rolldown/binding-win32-arm64-msvc@1.0.3': + '@rolldown/binding-win32-arm64-msvc@1.1.4': optional: true - '@rolldown/binding-win32-x64-msvc@1.0.3': + '@rolldown/binding-win32-x64-msvc@1.1.4': optional: true - '@rolldown/plugin-babel@0.2.2(@babel/core@7.29.0)(@babel/runtime@7.29.7)(rolldown@1.0.3)(vite@8.0.16(@types/node@24.10.3)(jiti@1.21.7)(yaml@2.8.3))': + '@rolldown/plugin-babel@0.2.3(@babel/core@7.29.0)(@babel/runtime@7.29.7)(rolldown@1.1.4)(vite@8.1.2(@types/node@24.13.2)(jiti@1.21.7)(yaml@2.9.0))': dependencies: '@babel/core': 7.29.0 - picomatch: 4.0.4 - rolldown: 1.0.3 + picomatch: 4.0.5 + rolldown: 1.1.4 optionalDependencies: '@babel/runtime': 7.29.7 - vite: 8.0.16(@types/node@24.10.3)(jiti@1.21.7)(yaml@2.8.3) - - '@rolldown/pluginutils@1.0.0-rc.2': {} - - '@rolldown/pluginutils@1.0.0-rc.7': {} + vite: 8.1.2(@types/node@24.13.2)(jiti@1.21.7)(yaml@2.9.0) '@rolldown/pluginutils@1.0.1': {} '@standard-schema/spec@1.1.0': {} - '@stylistic/eslint-plugin@2.13.0(eslint@10.3.0(jiti@1.21.7))(typescript@6.0.3)': + '@stylistic/eslint-plugin@2.13.0(eslint@10.6.0(jiti@1.21.7))(typescript@6.0.3)': dependencies: - '@typescript-eslint/utils': 8.60.0(eslint@10.3.0(jiti@1.21.7))(typescript@6.0.3) - eslint: 10.3.0(jiti@1.21.7) + '@typescript-eslint/utils': 8.62.1(eslint@10.6.0(jiti@1.21.7))(typescript@6.0.3) + eslint: 10.6.0(jiti@1.21.7) eslint-visitor-keys: 4.2.0 espree: 10.3.0 estraverse: 5.3.0 @@ -4939,94 +4907,102 @@ snapshots: - supports-color - typescript - '@swc/core-darwin-arm64@1.15.18': + '@swc/core-darwin-arm64@1.15.43': + optional: true + + '@swc/core-darwin-x64@1.15.43': optional: true - '@swc/core-darwin-x64@1.15.18': + '@swc/core-linux-arm-gnueabihf@1.15.43': optional: true - '@swc/core-linux-arm-gnueabihf@1.15.18': + '@swc/core-linux-arm64-gnu@1.15.43': optional: true - '@swc/core-linux-arm64-gnu@1.15.18': + '@swc/core-linux-arm64-musl@1.15.43': optional: true - '@swc/core-linux-arm64-musl@1.15.18': + '@swc/core-linux-ppc64-gnu@1.15.43': optional: true - '@swc/core-linux-x64-gnu@1.15.18': + '@swc/core-linux-s390x-gnu@1.15.43': optional: true - '@swc/core-linux-x64-musl@1.15.18': + '@swc/core-linux-x64-gnu@1.15.43': optional: true - '@swc/core-win32-arm64-msvc@1.15.18': + '@swc/core-linux-x64-musl@1.15.43': optional: true - '@swc/core-win32-ia32-msvc@1.15.18': + '@swc/core-win32-arm64-msvc@1.15.43': optional: true - '@swc/core-win32-x64-msvc@1.15.18': + '@swc/core-win32-ia32-msvc@1.15.43': optional: true - '@swc/core@1.15.18(@swc/helpers@0.5.19)': + '@swc/core-win32-x64-msvc@1.15.43': + optional: true + + '@swc/core@1.15.43(@swc/helpers@0.5.23)': dependencies: '@swc/counter': 0.1.3 - '@swc/types': 0.1.25 + '@swc/types': 0.1.27 optionalDependencies: - '@swc/core-darwin-arm64': 1.15.18 - '@swc/core-darwin-x64': 1.15.18 - '@swc/core-linux-arm-gnueabihf': 1.15.18 - '@swc/core-linux-arm64-gnu': 1.15.18 - '@swc/core-linux-arm64-musl': 1.15.18 - '@swc/core-linux-x64-gnu': 1.15.18 - '@swc/core-linux-x64-musl': 1.15.18 - '@swc/core-win32-arm64-msvc': 1.15.18 - '@swc/core-win32-ia32-msvc': 1.15.18 - '@swc/core-win32-x64-msvc': 1.15.18 - '@swc/helpers': 0.5.19 + '@swc/core-darwin-arm64': 1.15.43 + '@swc/core-darwin-x64': 1.15.43 + '@swc/core-linux-arm-gnueabihf': 1.15.43 + '@swc/core-linux-arm64-gnu': 1.15.43 + '@swc/core-linux-arm64-musl': 1.15.43 + '@swc/core-linux-ppc64-gnu': 1.15.43 + '@swc/core-linux-s390x-gnu': 1.15.43 + '@swc/core-linux-x64-gnu': 1.15.43 + '@swc/core-linux-x64-musl': 1.15.43 + '@swc/core-win32-arm64-msvc': 1.15.43 + '@swc/core-win32-ia32-msvc': 1.15.43 + '@swc/core-win32-x64-msvc': 1.15.43 + '@swc/helpers': 0.5.23 '@swc/counter@0.1.3': {} - '@swc/helpers@0.5.19': + '@swc/helpers@0.5.23': dependencies: tslib: 2.8.1 - '@swc/types@0.1.25': + '@swc/types@0.1.27': dependencies: '@swc/counter': 0.1.3 - '@tanstack/eslint-plugin-query@5.91.4(eslint@10.3.0(jiti@1.21.7))(typescript@6.0.3)': + '@tanstack/eslint-plugin-query@5.101.2(eslint@10.6.0(jiti@1.21.7))(typescript@6.0.3)': dependencies: - '@typescript-eslint/utils': 8.60.0(eslint@10.3.0(jiti@1.21.7))(typescript@6.0.3) - eslint: 10.3.0(jiti@1.21.7) + '@typescript-eslint/utils': 8.62.1(eslint@10.6.0(jiti@1.21.7))(typescript@6.0.3) + eslint: 10.6.0(jiti@1.21.7) optionalDependencies: typescript: 6.0.3 transitivePeerDependencies: - supports-color - '@tanstack/query-core@5.90.20': {} + '@tanstack/query-core@5.101.2': {} - '@tanstack/react-query@5.90.21(react@19.2.6)': + '@tanstack/react-query@5.101.2(react@19.2.7)': dependencies: - '@tanstack/query-core': 5.90.20 - react: 19.2.6 + '@tanstack/query-core': 5.101.2 + react: 19.2.7 - '@tanstack/react-table@8.21.3(react-dom@19.2.6(react@19.2.6))(react@19.2.6)': + '@tanstack/react-table@8.21.3(react-dom@19.2.7(react@19.2.7))(react@19.2.7)': dependencies: '@tanstack/table-core': 8.21.3 - react: 19.2.6 - react-dom: 19.2.6(react@19.2.6) + react: 19.2.7 + react-dom: 19.2.7(react@19.2.7) - '@tanstack/react-virtual@3.13.21(react-dom@19.2.6(react@19.2.6))(react@19.2.6)': + '@tanstack/react-virtual@3.14.5(react-dom@19.2.7(react@19.2.7))(react@19.2.7)': dependencies: - '@tanstack/virtual-core': 3.13.21 - react: 19.2.6 - react-dom: 19.2.6(react@19.2.6) + '@tanstack/virtual-core': 3.17.3 + react: 19.2.7 + react-dom: 19.2.7(react@19.2.7) '@tanstack/table-core@8.21.3': {} - '@tanstack/virtual-core@3.13.21': {} + '@tanstack/virtual-core@3.17.3': {} '@testing-library/dom@10.4.0': dependencies: @@ -5048,17 +5024,17 @@ snapshots: picocolors: 1.1.1 redent: 3.0.0 - '@testing-library/react@16.3.2(@testing-library/dom@10.4.0)(@types/react-dom@19.2.3(@types/react@19.2.15))(@types/react@19.2.15)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)': + '@testing-library/react@16.3.2(@testing-library/dom@10.4.0)(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)': dependencies: '@babel/runtime': 7.28.6 '@testing-library/dom': 10.4.0 - react: 19.2.6 - react-dom: 19.2.6(react@19.2.6) + react: 19.2.7 + react-dom: 19.2.7(react@19.2.7) optionalDependencies: - '@types/react': 19.2.15 - '@types/react-dom': 19.2.3(@types/react@19.2.15) + '@types/react': 19.2.17 + '@types/react-dom': 19.2.3(@types/react@19.2.17) - '@trivago/prettier-plugin-sort-imports@4.3.0(prettier@3.8.1)': + '@trivago/prettier-plugin-sort-imports@4.3.0(prettier@3.9.4)': dependencies: '@babel/generator': 7.17.7 '@babel/parser': 7.26.10 @@ -5066,7 +5042,7 @@ snapshots: '@babel/types': 7.17.0 javascript-natural-sort: 0.7.1 lodash: 4.18.1 - prettier: 3.8.1 + prettier: 3.9.4 transitivePeerDependencies: - supports-color @@ -5076,16 +5052,17 @@ snapshots: path-browserify: 1.0.1 tinyglobby: 0.2.17 - '@tybys/wasm-util@0.10.2': + '@tybys/wasm-util@0.10.3': dependencies: tslib: 2.8.1 optional: true '@types/aria-query@5.0.4': {} - '@types/chai@5.2.2': + '@types/chai@5.2.3': dependencies: '@types/deep-eql': 4.0.2 + assertion-error: 2.0.1 '@types/culori@4.0.1': {} @@ -5174,30 +5151,37 @@ snapshots: '@types/ms@2.1.0': {} - '@types/node@24.10.3': + '@types/node@24.13.2': dependencies: - undici-types: 7.16.0 + undici-types: 7.18.2 '@types/parse-json@4.0.2': {} - '@types/react-dom@19.2.3(@types/react@19.2.15)': + '@types/react-dom@19.2.3(@types/react@19.2.17)': dependencies: - '@types/react': 19.2.15 + '@types/react': 19.2.17 '@types/react-syntax-highlighter@15.5.13': dependencies: - '@types/react': 19.2.15 + '@types/react': 19.2.17 - '@types/react-transition-group@4.4.12(@types/react@19.2.15)': + '@types/react-transition-group@4.4.12(@types/react@19.2.17)': dependencies: - '@types/react': 19.2.15 + '@types/react': 19.2.17 - '@types/react@19.2.15': + '@types/react@19.2.17': dependencies: csstype: 3.2.3 + '@types/set-cookie-parser@2.4.10': + dependencies: + '@types/node': 24.13.2 + '@types/statuses@2.0.6': {} + '@types/trusted-types@2.0.7': + optional: true + '@types/unist@2.0.11': {} '@types/unist@3.0.3': {} @@ -5206,17 +5190,17 @@ snapshots: '@types/ws@8.18.1': dependencies: - '@types/node': 24.10.3 + '@types/node': 24.13.2 - '@typescript-eslint/eslint-plugin@8.60.0(@typescript-eslint/parser@8.60.0(eslint@10.3.0(jiti@1.21.7))(typescript@6.0.3))(eslint@10.3.0(jiti@1.21.7))(typescript@6.0.3)': + '@typescript-eslint/eslint-plugin@8.62.1(@typescript-eslint/parser@8.62.1(eslint@10.6.0(jiti@1.21.7))(typescript@6.0.3))(eslint@10.6.0(jiti@1.21.7))(typescript@6.0.3)': dependencies: '@eslint-community/regexpp': 4.12.2 - '@typescript-eslint/parser': 8.60.0(eslint@10.3.0(jiti@1.21.7))(typescript@6.0.3) - '@typescript-eslint/scope-manager': 8.60.0 - '@typescript-eslint/type-utils': 8.60.0(eslint@10.3.0(jiti@1.21.7))(typescript@6.0.3) - '@typescript-eslint/utils': 8.60.0(eslint@10.3.0(jiti@1.21.7))(typescript@6.0.3) - '@typescript-eslint/visitor-keys': 8.60.0 - eslint: 10.3.0(jiti@1.21.7) + '@typescript-eslint/parser': 8.62.1(eslint@10.6.0(jiti@1.21.7))(typescript@6.0.3) + '@typescript-eslint/scope-manager': 8.62.1 + '@typescript-eslint/type-utils': 8.62.1(eslint@10.6.0(jiti@1.21.7))(typescript@6.0.3) + '@typescript-eslint/utils': 8.62.1(eslint@10.6.0(jiti@1.21.7))(typescript@6.0.3) + '@typescript-eslint/visitor-keys': 8.62.1 + eslint: 10.6.0(jiti@1.21.7) ignore: 7.0.5 natural-compare: 1.4.0 ts-api-utils: 2.5.0(typescript@6.0.3) @@ -5224,79 +5208,79 @@ snapshots: transitivePeerDependencies: - supports-color - '@typescript-eslint/parser@8.60.0(eslint@10.3.0(jiti@1.21.7))(typescript@6.0.3)': + '@typescript-eslint/parser@8.62.1(eslint@10.6.0(jiti@1.21.7))(typescript@6.0.3)': dependencies: - '@typescript-eslint/scope-manager': 8.60.0 - '@typescript-eslint/types': 8.60.0 - '@typescript-eslint/typescript-estree': 8.60.0(typescript@6.0.3) - '@typescript-eslint/visitor-keys': 8.60.0 + '@typescript-eslint/scope-manager': 8.62.1 + '@typescript-eslint/types': 8.62.1 + '@typescript-eslint/typescript-estree': 8.62.1(typescript@6.0.3) + '@typescript-eslint/visitor-keys': 8.62.1 debug: 4.4.3 - eslint: 10.3.0(jiti@1.21.7) + eslint: 10.6.0(jiti@1.21.7) typescript: 6.0.3 transitivePeerDependencies: - supports-color - '@typescript-eslint/project-service@8.60.0(typescript@6.0.3)': + '@typescript-eslint/project-service@8.62.1(typescript@6.0.3)': dependencies: - '@typescript-eslint/tsconfig-utils': 8.60.0(typescript@6.0.3) - '@typescript-eslint/types': 8.60.0 + '@typescript-eslint/tsconfig-utils': 8.62.1(typescript@6.0.3) + '@typescript-eslint/types': 8.62.1 debug: 4.4.3 typescript: 6.0.3 transitivePeerDependencies: - supports-color - '@typescript-eslint/scope-manager@8.60.0': + '@typescript-eslint/scope-manager@8.62.1': dependencies: - '@typescript-eslint/types': 8.60.0 - '@typescript-eslint/visitor-keys': 8.60.0 + '@typescript-eslint/types': 8.62.1 + '@typescript-eslint/visitor-keys': 8.62.1 - '@typescript-eslint/tsconfig-utils@8.60.0(typescript@6.0.3)': + '@typescript-eslint/tsconfig-utils@8.62.1(typescript@6.0.3)': dependencies: typescript: 6.0.3 - '@typescript-eslint/type-utils@8.60.0(eslint@10.3.0(jiti@1.21.7))(typescript@6.0.3)': + '@typescript-eslint/type-utils@8.62.1(eslint@10.6.0(jiti@1.21.7))(typescript@6.0.3)': dependencies: - '@typescript-eslint/types': 8.60.0 - '@typescript-eslint/typescript-estree': 8.60.0(typescript@6.0.3) - '@typescript-eslint/utils': 8.60.0(eslint@10.3.0(jiti@1.21.7))(typescript@6.0.3) + '@typescript-eslint/types': 8.62.1 + '@typescript-eslint/typescript-estree': 8.62.1(typescript@6.0.3) + '@typescript-eslint/utils': 8.62.1(eslint@10.6.0(jiti@1.21.7))(typescript@6.0.3) debug: 4.4.3 - eslint: 10.3.0(jiti@1.21.7) + eslint: 10.6.0(jiti@1.21.7) ts-api-utils: 2.5.0(typescript@6.0.3) typescript: 6.0.3 transitivePeerDependencies: - supports-color - '@typescript-eslint/types@8.60.0': {} + '@typescript-eslint/types@8.62.1': {} - '@typescript-eslint/typescript-estree@8.60.0(typescript@6.0.3)': + '@typescript-eslint/typescript-estree@8.62.1(typescript@6.0.3)': dependencies: - '@typescript-eslint/project-service': 8.60.0(typescript@6.0.3) - '@typescript-eslint/tsconfig-utils': 8.60.0(typescript@6.0.3) - '@typescript-eslint/types': 8.60.0 - '@typescript-eslint/visitor-keys': 8.60.0 + '@typescript-eslint/project-service': 8.62.1(typescript@6.0.3) + '@typescript-eslint/tsconfig-utils': 8.62.1(typescript@6.0.3) + '@typescript-eslint/types': 8.62.1 + '@typescript-eslint/visitor-keys': 8.62.1 debug: 4.4.3 minimatch: 10.2.5 - semver: 7.8.1 + semver: 7.8.5 tinyglobby: 0.2.17 ts-api-utils: 2.5.0(typescript@6.0.3) typescript: 6.0.3 transitivePeerDependencies: - supports-color - '@typescript-eslint/utils@8.60.0(eslint@10.3.0(jiti@1.21.7))(typescript@6.0.3)': + '@typescript-eslint/utils@8.62.1(eslint@10.6.0(jiti@1.21.7))(typescript@6.0.3)': dependencies: - '@eslint-community/eslint-utils': 4.9.1(eslint@10.3.0(jiti@1.21.7)) - '@typescript-eslint/scope-manager': 8.60.0 - '@typescript-eslint/types': 8.60.0 - '@typescript-eslint/typescript-estree': 8.60.0(typescript@6.0.3) - eslint: 10.3.0(jiti@1.21.7) + '@eslint-community/eslint-utils': 4.9.1(eslint@10.6.0(jiti@1.21.7)) + '@typescript-eslint/scope-manager': 8.62.1 + '@typescript-eslint/types': 8.62.1 + '@typescript-eslint/typescript-estree': 8.62.1(typescript@6.0.3) + eslint: 10.6.0(jiti@1.21.7) typescript: 6.0.3 transitivePeerDependencies: - supports-color - '@typescript-eslint/visitor-keys@8.60.0': + '@typescript-eslint/visitor-keys@8.62.1': dependencies: - '@typescript-eslint/types': 8.60.0 + '@typescript-eslint/types': 8.62.1 eslint-visitor-keys: 5.0.1 '@ungap/structured-clone@1.3.0': {} @@ -5306,32 +5290,32 @@ snapshots: '@types/d3-shape': 1.3.12 d3-shape: 1.3.7 - '@visx/group@3.12.0(react@19.2.6)': + '@visx/group@3.12.0(react@19.2.7)': dependencies: - '@types/react': 19.2.15 + '@types/react': 19.2.17 classnames: 2.5.1 prop-types: 15.8.1 - react: 19.2.6 + react: 19.2.7 '@visx/scale@3.12.0': dependencies: '@visx/vendor': 3.12.0 - '@visx/shape@3.12.0(react@19.2.6)': + '@visx/shape@3.12.0(react@19.2.7)': dependencies: '@types/d3-path': 1.0.11 '@types/d3-shape': 1.3.12 '@types/lodash': 4.17.20 - '@types/react': 19.2.15 + '@types/react': 19.2.17 '@visx/curve': 3.12.0 - '@visx/group': 3.12.0(react@19.2.6) + '@visx/group': 3.12.0(react@19.2.7) '@visx/scale': 3.12.0 classnames: 2.5.1 d3-path: 1.0.9 d3-shape: 1.3.7 lodash: 4.18.1 prop-types: 15.8.1 - react: 19.2.6 + react: 19.2.7 '@visx/vendor@3.12.0': dependencies: @@ -5355,90 +5339,92 @@ snapshots: d3-time-format: 4.1.0 internmap: 2.0.3 - '@vitejs/plugin-react-swc@4.2.3(@swc/helpers@0.5.19)(vite@8.0.16(@types/node@24.10.3)(jiti@1.21.7)(yaml@2.8.3))': + '@vitejs/plugin-react-swc@4.3.1(@swc/helpers@0.5.23)(vite@8.1.2(@types/node@24.13.2)(jiti@1.21.7)(yaml@2.9.0))': dependencies: - '@rolldown/pluginutils': 1.0.0-rc.2 - '@swc/core': 1.15.18(@swc/helpers@0.5.19) - vite: 8.0.16(@types/node@24.10.3)(jiti@1.21.7)(yaml@2.8.3) + '@rolldown/pluginutils': 1.0.1 + '@swc/core': 1.15.43(@swc/helpers@0.5.23) + vite: 8.1.2(@types/node@24.13.2)(jiti@1.21.7)(yaml@2.9.0) transitivePeerDependencies: - '@swc/helpers' - '@vitejs/plugin-react@6.0.1(@rolldown/plugin-babel@0.2.2(@babel/core@7.29.0)(@babel/runtime@7.29.7)(rolldown@1.0.3)(vite@8.0.16(@types/node@24.10.3)(jiti@1.21.7)(yaml@2.8.3)))(babel-plugin-react-compiler@1.0.0)(vite@8.0.16(@types/node@24.10.3)(jiti@1.21.7)(yaml@2.8.3))': + '@vitejs/plugin-react@6.0.3(@rolldown/plugin-babel@0.2.3(@babel/core@7.29.0)(@babel/runtime@7.29.7)(rolldown@1.1.4)(vite@8.1.2(@types/node@24.13.2)(jiti@1.21.7)(yaml@2.9.0)))(babel-plugin-react-compiler@1.0.0)(vite@8.1.2(@types/node@24.13.2)(jiti@1.21.7)(yaml@2.9.0))': dependencies: - '@rolldown/pluginutils': 1.0.0-rc.7 - vite: 8.0.16(@types/node@24.10.3)(jiti@1.21.7)(yaml@2.8.3) + '@rolldown/pluginutils': 1.0.1 + vite: 8.1.2(@types/node@24.13.2)(jiti@1.21.7)(yaml@2.9.0) optionalDependencies: - '@rolldown/plugin-babel': 0.2.2(@babel/core@7.29.0)(@babel/runtime@7.29.7)(rolldown@1.0.3)(vite@8.0.16(@types/node@24.10.3)(jiti@1.21.7)(yaml@2.8.3)) + '@rolldown/plugin-babel': 0.2.3(@babel/core@7.29.0)(@babel/runtime@7.29.7)(rolldown@1.1.4)(vite@8.1.2(@types/node@24.13.2)(jiti@1.21.7)(yaml@2.9.0)) babel-plugin-react-compiler: 1.0.0 - '@vitest/coverage-v8@4.1.4(vitest@4.1.4)': + '@vitest/coverage-v8@4.1.9(vitest@4.1.9)': dependencies: '@bcoe/v8-coverage': 1.0.2 - '@vitest/utils': 4.1.4 - ast-v8-to-istanbul: 1.0.0 + '@vitest/utils': 4.1.9 + ast-v8-to-istanbul: 1.0.4 istanbul-lib-coverage: 3.2.2 istanbul-lib-report: 3.0.1 istanbul-reports: 3.2.0 - magicast: 0.5.2 - obug: 2.1.1 - std-env: 4.0.0 + magicast: 0.5.3 + obug: 2.1.3 + std-env: 4.1.0 tinyrainbow: 3.1.0 - vitest: 4.1.4(@types/node@24.10.3)(@vitest/coverage-v8@4.1.4)(happy-dom@20.8.9)(msw@2.12.10(@types/node@24.10.3)(typescript@6.0.3))(vite@8.0.16(@types/node@24.10.3)(jiti@1.21.7)(yaml@2.8.3)) + vitest: 4.1.9(@types/node@24.13.2)(@vitest/coverage-v8@4.1.9)(happy-dom@20.10.6)(msw@2.14.6(@types/node@24.13.2)(typescript@6.0.3))(vite@8.1.2(@types/node@24.13.2)(jiti@1.21.7)(yaml@2.9.0)) - '@vitest/expect@4.1.4': + '@vitest/expect@4.1.9': dependencies: '@standard-schema/spec': 1.1.0 - '@types/chai': 5.2.2 - '@vitest/spy': 4.1.4 - '@vitest/utils': 4.1.4 + '@types/chai': 5.2.3 + '@vitest/spy': 4.1.9 + '@vitest/utils': 4.1.9 chai: 6.2.2 tinyrainbow: 3.1.0 - '@vitest/mocker@4.1.4(msw@2.12.10(@types/node@24.10.3)(typescript@6.0.3))(vite@8.0.16(@types/node@24.10.3)(jiti@1.21.7)(yaml@2.8.3))': + '@vitest/mocker@4.1.9(msw@2.14.6(@types/node@24.13.2)(typescript@6.0.3))(vite@8.1.2(@types/node@24.13.2)(jiti@1.21.7)(yaml@2.9.0))': dependencies: - '@vitest/spy': 4.1.4 + '@vitest/spy': 4.1.9 estree-walker: 3.0.3 magic-string: 0.30.21 optionalDependencies: - msw: 2.12.10(@types/node@24.10.3)(typescript@6.0.3) - vite: 8.0.16(@types/node@24.10.3)(jiti@1.21.7)(yaml@2.8.3) + msw: 2.14.6(@types/node@24.13.2)(typescript@6.0.3) + vite: 8.1.2(@types/node@24.13.2)(jiti@1.21.7)(yaml@2.9.0) - '@vitest/pretty-format@4.1.4': + '@vitest/pretty-format@4.1.9': dependencies: tinyrainbow: 3.1.0 - '@vitest/runner@4.1.4': + '@vitest/runner@4.1.9': dependencies: - '@vitest/utils': 4.1.4 + '@vitest/utils': 4.1.9 pathe: 2.0.3 - '@vitest/snapshot@4.1.4': + '@vitest/snapshot@4.1.9': dependencies: - '@vitest/pretty-format': 4.1.4 - '@vitest/utils': 4.1.4 + '@vitest/pretty-format': 4.1.9 + '@vitest/utils': 4.1.9 magic-string: 0.30.21 pathe: 2.0.3 - '@vitest/spy@4.1.4': {} + '@vitest/spy@4.1.9': {} - '@vitest/utils@4.1.4': + '@vitest/utils@4.1.9': dependencies: - '@vitest/pretty-format': 4.1.4 + '@vitest/pretty-format': 4.1.9 convert-source-map: 2.0.0 tinyrainbow: 3.1.0 - '@xyflow/react@12.10.1(@types/react@19.2.15)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)': + '@xyflow/react@12.11.1(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)': dependencies: - '@xyflow/system': 0.0.75 + '@xyflow/system': 0.0.78 classcat: 5.0.5 - react: 19.2.6 - react-dom: 19.2.6(react@19.2.6) - zustand: 4.5.7(@types/react@19.2.15)(react@19.2.6) + react: 19.2.7 + react-dom: 19.2.7(react@19.2.7) + zustand: 4.5.7(@types/react@19.2.17)(react@19.2.7) + optionalDependencies: + '@types/react': 19.2.17 + '@types/react-dom': 19.2.3(@types/react@19.2.17) transitivePeerDependencies: - - '@types/react' - immer - '@xyflow/system@0.0.75': + '@xyflow/system@0.0.78': dependencies: '@types/d3-drag': 3.0.7 '@types/d3-interpolate': 3.0.4 @@ -5450,579 +5436,584 @@ snapshots: d3-selection: 3.0.0 d3-zoom: 3.0.0 - '@zag-js/accordion@1.35.3': + '@zag-js/accordion@1.41.2': dependencies: - '@zag-js/anatomy': 1.35.3 - '@zag-js/core': 1.35.3 - '@zag-js/dom-query': 1.35.3 - '@zag-js/types': 1.35.3 - '@zag-js/utils': 1.35.3 + '@zag-js/anatomy': 1.41.2 + '@zag-js/core': 1.41.2 + '@zag-js/dom-query': 1.41.2 + '@zag-js/types': 1.41.2 + '@zag-js/utils': 1.41.2 - '@zag-js/anatomy@1.35.3': {} + '@zag-js/anatomy@1.41.2': {} - '@zag-js/angle-slider@1.35.3': + '@zag-js/angle-slider@1.41.2': dependencies: - '@zag-js/anatomy': 1.35.3 - '@zag-js/core': 1.35.3 - '@zag-js/dom-query': 1.35.3 - '@zag-js/rect-utils': 1.35.3 - '@zag-js/types': 1.35.3 - '@zag-js/utils': 1.35.3 + '@zag-js/anatomy': 1.41.2 + '@zag-js/core': 1.41.2 + '@zag-js/dom-query': 1.41.2 + '@zag-js/rect-utils': 1.41.2 + '@zag-js/types': 1.41.2 + '@zag-js/utils': 1.41.2 - '@zag-js/aria-hidden@1.35.3': + '@zag-js/aria-hidden@1.41.2': dependencies: - '@zag-js/dom-query': 1.35.3 + '@zag-js/dom-query': 1.41.2 - '@zag-js/async-list@1.35.3': + '@zag-js/async-list@1.41.2': dependencies: - '@zag-js/core': 1.35.3 - '@zag-js/utils': 1.35.3 + '@zag-js/core': 1.41.2 + '@zag-js/utils': 1.41.2 - '@zag-js/auto-resize@1.35.3': + '@zag-js/auto-resize@1.41.2': dependencies: - '@zag-js/dom-query': 1.35.3 + '@zag-js/dom-query': 1.41.2 - '@zag-js/avatar@1.35.3': + '@zag-js/avatar@1.41.2': dependencies: - '@zag-js/anatomy': 1.35.3 - '@zag-js/core': 1.35.3 - '@zag-js/dom-query': 1.35.3 - '@zag-js/types': 1.35.3 - '@zag-js/utils': 1.35.3 + '@zag-js/anatomy': 1.41.2 + '@zag-js/core': 1.41.2 + '@zag-js/dom-query': 1.41.2 + '@zag-js/types': 1.41.2 + '@zag-js/utils': 1.41.2 - '@zag-js/carousel@1.35.3': + '@zag-js/carousel@1.41.2': dependencies: - '@zag-js/anatomy': 1.35.3 - '@zag-js/core': 1.35.3 - '@zag-js/dom-query': 1.35.3 - '@zag-js/scroll-snap': 1.35.3 - '@zag-js/types': 1.35.3 - '@zag-js/utils': 1.35.3 + '@zag-js/anatomy': 1.41.2 + '@zag-js/core': 1.41.2 + '@zag-js/dom-query': 1.41.2 + '@zag-js/scroll-snap': 1.41.2 + '@zag-js/types': 1.41.2 + '@zag-js/utils': 1.41.2 - '@zag-js/cascade-select@1.35.3': + '@zag-js/cascade-select@1.41.2': dependencies: - '@zag-js/anatomy': 1.35.3 - '@zag-js/collection': 1.35.3 - '@zag-js/core': 1.35.3 - '@zag-js/dismissable': 1.35.3 - '@zag-js/dom-query': 1.35.3 - '@zag-js/focus-visible': 1.35.3 - '@zag-js/popper': 1.35.3 - '@zag-js/rect-utils': 1.35.3 - '@zag-js/types': 1.35.3 - '@zag-js/utils': 1.35.3 + '@zag-js/anatomy': 1.41.2 + '@zag-js/collection': 1.41.2 + '@zag-js/core': 1.41.2 + '@zag-js/dismissable': 1.41.2 + '@zag-js/dom-query': 1.41.2 + '@zag-js/focus-visible': 1.41.2 + '@zag-js/popper': 1.41.2 + '@zag-js/rect-utils': 1.41.2 + '@zag-js/types': 1.41.2 + '@zag-js/utils': 1.41.2 - '@zag-js/checkbox@1.35.3': + '@zag-js/checkbox@1.41.2': dependencies: - '@zag-js/anatomy': 1.35.3 - '@zag-js/core': 1.35.3 - '@zag-js/dom-query': 1.35.3 - '@zag-js/focus-visible': 1.35.3 - '@zag-js/types': 1.35.3 - '@zag-js/utils': 1.35.3 + '@zag-js/anatomy': 1.41.2 + '@zag-js/core': 1.41.2 + '@zag-js/dom-query': 1.41.2 + '@zag-js/focus-visible': 1.41.2 + '@zag-js/types': 1.41.2 + '@zag-js/utils': 1.41.2 - '@zag-js/clipboard@1.35.3': + '@zag-js/clipboard@1.41.2': dependencies: - '@zag-js/anatomy': 1.35.3 - '@zag-js/core': 1.35.3 - '@zag-js/dom-query': 1.35.3 - '@zag-js/types': 1.35.3 - '@zag-js/utils': 1.35.3 + '@zag-js/anatomy': 1.41.2 + '@zag-js/core': 1.41.2 + '@zag-js/dom-query': 1.41.2 + '@zag-js/types': 1.41.2 + '@zag-js/utils': 1.41.2 - '@zag-js/collapsible@1.35.3': + '@zag-js/collapsible@1.41.2': dependencies: - '@zag-js/anatomy': 1.35.3 - '@zag-js/core': 1.35.3 - '@zag-js/dom-query': 1.35.3 - '@zag-js/types': 1.35.3 - '@zag-js/utils': 1.35.3 + '@zag-js/anatomy': 1.41.2 + '@zag-js/core': 1.41.2 + '@zag-js/dom-query': 1.41.2 + '@zag-js/types': 1.41.2 + '@zag-js/utils': 1.41.2 - '@zag-js/collection@1.35.3': + '@zag-js/collection@1.41.2': dependencies: - '@zag-js/utils': 1.35.3 + '@zag-js/utils': 1.41.2 - '@zag-js/color-picker@1.35.3': + '@zag-js/color-picker@1.41.2': dependencies: - '@zag-js/anatomy': 1.35.3 - '@zag-js/color-utils': 1.35.3 - '@zag-js/core': 1.35.3 - '@zag-js/dismissable': 1.35.3 - '@zag-js/dom-query': 1.35.3 - '@zag-js/popper': 1.35.3 - '@zag-js/types': 1.35.3 - '@zag-js/utils': 1.35.3 + '@zag-js/anatomy': 1.41.2 + '@zag-js/color-utils': 1.41.2 + '@zag-js/core': 1.41.2 + '@zag-js/dismissable': 1.41.2 + '@zag-js/dom-query': 1.41.2 + '@zag-js/popper': 1.41.2 + '@zag-js/types': 1.41.2 + '@zag-js/utils': 1.41.2 - '@zag-js/color-utils@1.35.3': - dependencies: - '@zag-js/utils': 1.35.3 + '@zag-js/color-utils@1.41.2': + dependencies: + '@zag-js/utils': 1.41.2 - '@zag-js/combobox@1.35.3': + '@zag-js/combobox@1.41.2': dependencies: - '@zag-js/anatomy': 1.35.3 - '@zag-js/aria-hidden': 1.35.3 - '@zag-js/collection': 1.35.3 - '@zag-js/core': 1.35.3 - '@zag-js/dismissable': 1.35.3 - '@zag-js/dom-query': 1.35.3 - '@zag-js/focus-visible': 1.35.3 - '@zag-js/popper': 1.35.3 - '@zag-js/types': 1.35.3 - '@zag-js/utils': 1.35.3 + '@zag-js/anatomy': 1.41.2 + '@zag-js/collection': 1.41.2 + '@zag-js/core': 1.41.2 + '@zag-js/dismissable': 1.41.2 + '@zag-js/dom-query': 1.41.2 + '@zag-js/focus-visible': 1.41.2 + '@zag-js/live-region': 1.41.2 + '@zag-js/popper': 1.41.2 + '@zag-js/types': 1.41.2 + '@zag-js/utils': 1.41.2 - '@zag-js/core@1.35.3': + '@zag-js/core@1.41.2': dependencies: - '@zag-js/dom-query': 1.35.3 - '@zag-js/utils': 1.35.3 + '@zag-js/dom-query': 1.41.2 + '@zag-js/utils': 1.41.2 - '@zag-js/date-picker@1.35.3(@internationalized/date@3.11.0)': + '@zag-js/date-input@1.41.2(@internationalized/date@3.12.2)': dependencies: - '@internationalized/date': 3.11.0 - '@zag-js/anatomy': 1.35.3 - '@zag-js/core': 1.35.3 - '@zag-js/date-utils': 1.35.3(@internationalized/date@3.11.0) - '@zag-js/dismissable': 1.35.3 - '@zag-js/dom-query': 1.35.3 - '@zag-js/live-region': 1.35.3 - '@zag-js/popper': 1.35.3 - '@zag-js/types': 1.35.3 - '@zag-js/utils': 1.35.3 + '@internationalized/date': 3.12.2 + '@zag-js/anatomy': 1.41.2 + '@zag-js/core': 1.41.2 + '@zag-js/date-utils': 1.41.2(@internationalized/date@3.12.2) + '@zag-js/dom-query': 1.41.2 + '@zag-js/live-region': 1.41.2 + '@zag-js/types': 1.41.2 + '@zag-js/utils': 1.41.2 + + '@zag-js/date-picker@1.41.2(@internationalized/date@3.12.2)': + dependencies: + '@internationalized/date': 3.12.2 + '@zag-js/anatomy': 1.41.2 + '@zag-js/core': 1.41.2 + '@zag-js/date-utils': 1.41.2(@internationalized/date@3.12.2) + '@zag-js/dismissable': 1.41.2 + '@zag-js/dom-query': 1.41.2 + '@zag-js/live-region': 1.41.2 + '@zag-js/popper': 1.41.2 + '@zag-js/types': 1.41.2 + '@zag-js/utils': 1.41.2 - '@zag-js/date-utils@1.35.3(@internationalized/date@3.11.0)': + '@zag-js/date-utils@1.41.2(@internationalized/date@3.12.2)': dependencies: - '@internationalized/date': 3.11.0 + '@internationalized/date': 3.12.2 - '@zag-js/dialog@1.35.3': + '@zag-js/dialog@1.41.2': dependencies: - '@zag-js/anatomy': 1.35.3 - '@zag-js/aria-hidden': 1.35.3 - '@zag-js/core': 1.35.3 - '@zag-js/dismissable': 1.35.3 - '@zag-js/dom-query': 1.35.3 - '@zag-js/focus-trap': 1.35.3 - '@zag-js/remove-scroll': 1.35.3 - '@zag-js/types': 1.35.3 - '@zag-js/utils': 1.35.3 + '@zag-js/anatomy': 1.41.2 + '@zag-js/aria-hidden': 1.41.2 + '@zag-js/core': 1.41.2 + '@zag-js/dismissable': 1.41.2 + '@zag-js/dom-query': 1.41.2 + '@zag-js/focus-trap': 1.41.2 + '@zag-js/remove-scroll': 1.41.2 + '@zag-js/types': 1.41.2 + '@zag-js/utils': 1.41.2 - '@zag-js/dismissable@1.35.3': + '@zag-js/dismissable@1.41.2': dependencies: - '@zag-js/dom-query': 1.35.3 - '@zag-js/interact-outside': 1.35.3 - '@zag-js/utils': 1.35.3 + '@zag-js/dom-query': 1.41.2 + '@zag-js/interact-outside': 1.41.2 + '@zag-js/utils': 1.41.2 - '@zag-js/dom-query@1.35.3': + '@zag-js/dom-query@1.41.2': dependencies: - '@zag-js/types': 1.35.3 + '@zag-js/types': 1.41.2 - '@zag-js/drawer@1.35.3': + '@zag-js/drawer@1.41.2': dependencies: - '@zag-js/anatomy': 1.35.3 - '@zag-js/aria-hidden': 1.35.3 - '@zag-js/core': 1.35.3 - '@zag-js/dismissable': 1.35.3 - '@zag-js/dom-query': 1.35.3 - '@zag-js/focus-trap': 1.35.3 - '@zag-js/remove-scroll': 1.35.3 - '@zag-js/types': 1.35.3 - '@zag-js/utils': 1.35.3 + '@zag-js/anatomy': 1.41.2 + '@zag-js/aria-hidden': 1.41.2 + '@zag-js/core': 1.41.2 + '@zag-js/dismissable': 1.41.2 + '@zag-js/dom-query': 1.41.2 + '@zag-js/focus-trap': 1.41.2 + '@zag-js/remove-scroll': 1.41.2 + '@zag-js/types': 1.41.2 + '@zag-js/utils': 1.41.2 - '@zag-js/editable@1.35.3': + '@zag-js/editable@1.41.2': dependencies: - '@zag-js/anatomy': 1.35.3 - '@zag-js/core': 1.35.3 - '@zag-js/dom-query': 1.35.3 - '@zag-js/interact-outside': 1.35.3 - '@zag-js/types': 1.35.3 - '@zag-js/utils': 1.35.3 + '@zag-js/anatomy': 1.41.2 + '@zag-js/core': 1.41.2 + '@zag-js/dom-query': 1.41.2 + '@zag-js/interact-outside': 1.41.2 + '@zag-js/types': 1.41.2 + '@zag-js/utils': 1.41.2 - '@zag-js/file-upload@1.35.3': + '@zag-js/file-upload@1.41.2': dependencies: - '@zag-js/anatomy': 1.35.3 - '@zag-js/core': 1.35.3 - '@zag-js/dom-query': 1.35.3 - '@zag-js/file-utils': 1.35.3 - '@zag-js/i18n-utils': 1.35.3 - '@zag-js/types': 1.35.3 - '@zag-js/utils': 1.35.3 + '@zag-js/anatomy': 1.41.2 + '@zag-js/core': 1.41.2 + '@zag-js/dom-query': 1.41.2 + '@zag-js/file-utils': 1.41.2 + '@zag-js/i18n-utils': 1.41.2 + '@zag-js/types': 1.41.2 + '@zag-js/utils': 1.41.2 - '@zag-js/file-utils@1.35.3': + '@zag-js/file-utils@1.41.2': dependencies: - '@zag-js/i18n-utils': 1.35.3 + '@zag-js/i18n-utils': 1.41.2 - '@zag-js/floating-panel@1.35.3': + '@zag-js/floating-panel@1.41.2': dependencies: - '@zag-js/anatomy': 1.35.3 - '@zag-js/core': 1.35.3 - '@zag-js/dom-query': 1.35.3 - '@zag-js/popper': 1.35.3 - '@zag-js/rect-utils': 1.35.3 - '@zag-js/store': 1.35.3 - '@zag-js/types': 1.35.3 - '@zag-js/utils': 1.35.3 + '@zag-js/anatomy': 1.41.2 + '@zag-js/core': 1.41.2 + '@zag-js/dom-query': 1.41.2 + '@zag-js/popper': 1.41.2 + '@zag-js/rect-utils': 1.41.2 + '@zag-js/store': 1.41.2 + '@zag-js/types': 1.41.2 + '@zag-js/utils': 1.41.2 - '@zag-js/focus-trap@1.35.3': + '@zag-js/focus-trap@1.41.2': dependencies: - '@zag-js/dom-query': 1.35.3 + '@zag-js/dom-query': 1.41.2 - '@zag-js/focus-visible@1.35.3': + '@zag-js/focus-visible@1.41.2': dependencies: - '@zag-js/dom-query': 1.35.3 + '@zag-js/dom-query': 1.41.2 - '@zag-js/highlight-word@1.35.3': {} + '@zag-js/highlight-word@1.41.2': {} - '@zag-js/hover-card@1.35.3': + '@zag-js/hover-card@1.41.2': dependencies: - '@zag-js/anatomy': 1.35.3 - '@zag-js/core': 1.35.3 - '@zag-js/dismissable': 1.35.3 - '@zag-js/dom-query': 1.35.3 - '@zag-js/popper': 1.35.3 - '@zag-js/types': 1.35.3 - '@zag-js/utils': 1.35.3 + '@zag-js/anatomy': 1.41.2 + '@zag-js/core': 1.41.2 + '@zag-js/dismissable': 1.41.2 + '@zag-js/dom-query': 1.41.2 + '@zag-js/popper': 1.41.2 + '@zag-js/types': 1.41.2 + '@zag-js/utils': 1.41.2 - '@zag-js/i18n-utils@1.35.3': + '@zag-js/i18n-utils@1.41.2': dependencies: - '@zag-js/dom-query': 1.35.3 + '@zag-js/dom-query': 1.41.2 - '@zag-js/image-cropper@1.35.3': + '@zag-js/image-cropper@1.41.2': dependencies: - '@zag-js/anatomy': 1.35.3 - '@zag-js/core': 1.35.3 - '@zag-js/dom-query': 1.35.3 - '@zag-js/types': 1.35.3 - '@zag-js/utils': 1.35.3 + '@zag-js/anatomy': 1.41.2 + '@zag-js/core': 1.41.2 + '@zag-js/dom-query': 1.41.2 + '@zag-js/types': 1.41.2 + '@zag-js/utils': 1.41.2 - '@zag-js/interact-outside@1.35.3': + '@zag-js/interact-outside@1.41.2': dependencies: - '@zag-js/dom-query': 1.35.3 - '@zag-js/utils': 1.35.3 + '@zag-js/dom-query': 1.41.2 + '@zag-js/utils': 1.41.2 - '@zag-js/json-tree-utils@1.35.3': {} + '@zag-js/json-tree-utils@1.41.2': {} - '@zag-js/listbox@1.35.3': + '@zag-js/listbox@1.41.2': dependencies: - '@zag-js/anatomy': 1.35.3 - '@zag-js/collection': 1.35.3 - '@zag-js/core': 1.35.3 - '@zag-js/dom-query': 1.35.3 - '@zag-js/focus-visible': 1.35.3 - '@zag-js/types': 1.35.3 - '@zag-js/utils': 1.35.3 + '@zag-js/anatomy': 1.41.2 + '@zag-js/collection': 1.41.2 + '@zag-js/core': 1.41.2 + '@zag-js/dom-query': 1.41.2 + '@zag-js/focus-visible': 1.41.2 + '@zag-js/types': 1.41.2 + '@zag-js/utils': 1.41.2 - '@zag-js/live-region@1.35.3': {} + '@zag-js/live-region@1.41.2': {} - '@zag-js/marquee@1.35.3': + '@zag-js/marquee@1.41.2': dependencies: - '@zag-js/anatomy': 1.35.3 - '@zag-js/core': 1.35.3 - '@zag-js/dom-query': 1.35.3 - '@zag-js/types': 1.35.3 - '@zag-js/utils': 1.35.3 + '@zag-js/anatomy': 1.41.2 + '@zag-js/core': 1.41.2 + '@zag-js/dom-query': 1.41.2 + '@zag-js/types': 1.41.2 + '@zag-js/utils': 1.41.2 - '@zag-js/menu@1.35.3': + '@zag-js/menu@1.41.2': dependencies: - '@zag-js/anatomy': 1.35.3 - '@zag-js/core': 1.35.3 - '@zag-js/dismissable': 1.35.3 - '@zag-js/dom-query': 1.35.3 - '@zag-js/focus-visible': 1.35.3 - '@zag-js/popper': 1.35.3 - '@zag-js/rect-utils': 1.35.3 - '@zag-js/types': 1.35.3 - '@zag-js/utils': 1.35.3 + '@zag-js/anatomy': 1.41.2 + '@zag-js/core': 1.41.2 + '@zag-js/dismissable': 1.41.2 + '@zag-js/dom-query': 1.41.2 + '@zag-js/focus-visible': 1.41.2 + '@zag-js/popper': 1.41.2 + '@zag-js/rect-utils': 1.41.2 + '@zag-js/types': 1.41.2 + '@zag-js/utils': 1.41.2 - '@zag-js/navigation-menu@1.35.3': + '@zag-js/navigation-menu@1.41.2': dependencies: - '@zag-js/anatomy': 1.35.3 - '@zag-js/core': 1.35.3 - '@zag-js/dismissable': 1.35.3 - '@zag-js/dom-query': 1.35.3 - '@zag-js/types': 1.35.3 - '@zag-js/utils': 1.35.3 + '@zag-js/anatomy': 1.41.2 + '@zag-js/core': 1.41.2 + '@zag-js/dismissable': 1.41.2 + '@zag-js/dom-query': 1.41.2 + '@zag-js/types': 1.41.2 + '@zag-js/utils': 1.41.2 - '@zag-js/number-input@1.35.3': + '@zag-js/number-input@1.41.2': dependencies: - '@internationalized/number': 3.6.5 - '@zag-js/anatomy': 1.35.3 - '@zag-js/core': 1.35.3 - '@zag-js/dom-query': 1.35.3 - '@zag-js/types': 1.35.3 - '@zag-js/utils': 1.35.3 + '@internationalized/number': 3.6.6 + '@zag-js/anatomy': 1.41.2 + '@zag-js/core': 1.41.2 + '@zag-js/dom-query': 1.41.2 + '@zag-js/types': 1.41.2 + '@zag-js/utils': 1.41.2 - '@zag-js/pagination@1.35.3': + '@zag-js/pagination@1.41.2': dependencies: - '@zag-js/anatomy': 1.35.3 - '@zag-js/core': 1.35.3 - '@zag-js/dom-query': 1.35.3 - '@zag-js/types': 1.35.3 - '@zag-js/utils': 1.35.3 + '@zag-js/anatomy': 1.41.2 + '@zag-js/core': 1.41.2 + '@zag-js/dom-query': 1.41.2 + '@zag-js/types': 1.41.2 + '@zag-js/utils': 1.41.2 - '@zag-js/password-input@1.35.3': + '@zag-js/password-input@1.41.2': dependencies: - '@zag-js/anatomy': 1.35.3 - '@zag-js/core': 1.35.3 - '@zag-js/dom-query': 1.35.3 - '@zag-js/types': 1.35.3 - '@zag-js/utils': 1.35.3 + '@zag-js/anatomy': 1.41.2 + '@zag-js/core': 1.41.2 + '@zag-js/dom-query': 1.41.2 + '@zag-js/types': 1.41.2 + '@zag-js/utils': 1.41.2 - '@zag-js/pin-input@1.35.3': + '@zag-js/pin-input@1.41.2': dependencies: - '@zag-js/anatomy': 1.35.3 - '@zag-js/core': 1.35.3 - '@zag-js/dom-query': 1.35.3 - '@zag-js/types': 1.35.3 - '@zag-js/utils': 1.35.3 - - '@zag-js/popover@1.35.3': + '@zag-js/anatomy': 1.41.2 + '@zag-js/core': 1.41.2 + '@zag-js/dom-query': 1.41.2 + '@zag-js/types': 1.41.2 + '@zag-js/utils': 1.41.2 + + '@zag-js/popover@1.41.2': dependencies: - '@zag-js/anatomy': 1.35.3 - '@zag-js/aria-hidden': 1.35.3 - '@zag-js/core': 1.35.3 - '@zag-js/dismissable': 1.35.3 - '@zag-js/dom-query': 1.35.3 - '@zag-js/focus-trap': 1.35.3 - '@zag-js/popper': 1.35.3 - '@zag-js/remove-scroll': 1.35.3 - '@zag-js/types': 1.35.3 - '@zag-js/utils': 1.35.3 + '@zag-js/anatomy': 1.41.2 + '@zag-js/aria-hidden': 1.41.2 + '@zag-js/core': 1.41.2 + '@zag-js/dismissable': 1.41.2 + '@zag-js/dom-query': 1.41.2 + '@zag-js/focus-trap': 1.41.2 + '@zag-js/popper': 1.41.2 + '@zag-js/remove-scroll': 1.41.2 + '@zag-js/types': 1.41.2 + '@zag-js/utils': 1.41.2 - '@zag-js/popper@1.35.3': + '@zag-js/popper@1.41.2': dependencies: '@floating-ui/dom': 1.7.6 - '@zag-js/dom-query': 1.35.3 - '@zag-js/utils': 1.35.3 + '@zag-js/dom-query': 1.41.2 + '@zag-js/utils': 1.41.2 - '@zag-js/presence@1.35.3': + '@zag-js/presence@1.41.2': dependencies: - '@zag-js/core': 1.35.3 - '@zag-js/dom-query': 1.35.3 - '@zag-js/types': 1.35.3 + '@zag-js/core': 1.41.2 + '@zag-js/dom-query': 1.41.2 + '@zag-js/types': 1.41.2 - '@zag-js/progress@1.35.3': + '@zag-js/progress@1.41.2': dependencies: - '@zag-js/anatomy': 1.35.3 - '@zag-js/core': 1.35.3 - '@zag-js/dom-query': 1.35.3 - '@zag-js/types': 1.35.3 - '@zag-js/utils': 1.35.3 + '@zag-js/anatomy': 1.41.2 + '@zag-js/core': 1.41.2 + '@zag-js/dom-query': 1.41.2 + '@zag-js/types': 1.41.2 + '@zag-js/utils': 1.41.2 - '@zag-js/qr-code@1.35.3': + '@zag-js/qr-code@1.41.2': dependencies: - '@zag-js/anatomy': 1.35.3 - '@zag-js/core': 1.35.3 - '@zag-js/dom-query': 1.35.3 - '@zag-js/types': 1.35.3 - '@zag-js/utils': 1.35.3 + '@zag-js/anatomy': 1.41.2 + '@zag-js/core': 1.41.2 + '@zag-js/dom-query': 1.41.2 + '@zag-js/types': 1.41.2 + '@zag-js/utils': 1.41.2 proxy-memoize: 3.0.1 - uqr: 0.1.2 + uqr: 0.1.3 - '@zag-js/radio-group@1.35.3': + '@zag-js/radio-group@1.41.2': dependencies: - '@zag-js/anatomy': 1.35.3 - '@zag-js/core': 1.35.3 - '@zag-js/dom-query': 1.35.3 - '@zag-js/focus-visible': 1.35.3 - '@zag-js/types': 1.35.3 - '@zag-js/utils': 1.35.3 + '@zag-js/anatomy': 1.41.2 + '@zag-js/core': 1.41.2 + '@zag-js/dom-query': 1.41.2 + '@zag-js/focus-visible': 1.41.2 + '@zag-js/types': 1.41.2 + '@zag-js/utils': 1.41.2 - '@zag-js/rating-group@1.35.3': + '@zag-js/rating-group@1.41.2': dependencies: - '@zag-js/anatomy': 1.35.3 - '@zag-js/core': 1.35.3 - '@zag-js/dom-query': 1.35.3 - '@zag-js/types': 1.35.3 - '@zag-js/utils': 1.35.3 + '@zag-js/anatomy': 1.41.2 + '@zag-js/core': 1.41.2 + '@zag-js/dom-query': 1.41.2 + '@zag-js/types': 1.41.2 + '@zag-js/utils': 1.41.2 - '@zag-js/react@1.35.3(react-dom@19.2.6(react@19.2.6))(react@19.2.6)': + '@zag-js/react@1.41.2(react-dom@19.2.7(react@19.2.7))(react@19.2.7)': dependencies: - '@zag-js/core': 1.35.3 - '@zag-js/store': 1.35.3 - '@zag-js/types': 1.35.3 - '@zag-js/utils': 1.35.3 - react: 19.2.6 - react-dom: 19.2.6(react@19.2.6) + '@zag-js/core': 1.41.2 + '@zag-js/store': 1.41.2 + '@zag-js/types': 1.41.2 + '@zag-js/utils': 1.41.2 + react: 19.2.7 + react-dom: 19.2.7(react@19.2.7) - '@zag-js/rect-utils@1.35.3': {} + '@zag-js/rect-utils@1.41.2': {} - '@zag-js/remove-scroll@1.35.3': + '@zag-js/remove-scroll@1.41.2': dependencies: - '@zag-js/dom-query': 1.35.3 + '@zag-js/dom-query': 1.41.2 - '@zag-js/scroll-area@1.35.3': + '@zag-js/scroll-area@1.41.2': dependencies: - '@zag-js/anatomy': 1.35.3 - '@zag-js/core': 1.35.3 - '@zag-js/dom-query': 1.35.3 - '@zag-js/types': 1.35.3 - '@zag-js/utils': 1.35.3 + '@zag-js/anatomy': 1.41.2 + '@zag-js/core': 1.41.2 + '@zag-js/dom-query': 1.41.2 + '@zag-js/types': 1.41.2 + '@zag-js/utils': 1.41.2 - '@zag-js/scroll-snap@1.35.3': + '@zag-js/scroll-snap@1.41.2': dependencies: - '@zag-js/dom-query': 1.35.3 + '@zag-js/dom-query': 1.41.2 - '@zag-js/select@1.35.3': + '@zag-js/select@1.41.2': dependencies: - '@zag-js/anatomy': 1.35.3 - '@zag-js/collection': 1.35.3 - '@zag-js/core': 1.35.3 - '@zag-js/dismissable': 1.35.3 - '@zag-js/dom-query': 1.35.3 - '@zag-js/focus-visible': 1.35.3 - '@zag-js/popper': 1.35.3 - '@zag-js/types': 1.35.3 - '@zag-js/utils': 1.35.3 + '@zag-js/anatomy': 1.41.2 + '@zag-js/collection': 1.41.2 + '@zag-js/core': 1.41.2 + '@zag-js/dismissable': 1.41.2 + '@zag-js/dom-query': 1.41.2 + '@zag-js/focus-visible': 1.41.2 + '@zag-js/popper': 1.41.2 + '@zag-js/types': 1.41.2 + '@zag-js/utils': 1.41.2 - '@zag-js/signature-pad@1.35.3': + '@zag-js/signature-pad@1.41.2': dependencies: - '@zag-js/anatomy': 1.35.3 - '@zag-js/core': 1.35.3 - '@zag-js/dom-query': 1.35.3 - '@zag-js/types': 1.35.3 - '@zag-js/utils': 1.35.3 + '@zag-js/anatomy': 1.41.2 + '@zag-js/core': 1.41.2 + '@zag-js/dom-query': 1.41.2 + '@zag-js/types': 1.41.2 + '@zag-js/utils': 1.41.2 perfect-freehand: 1.2.3 - '@zag-js/slider@1.35.3': + '@zag-js/slider@1.41.2': dependencies: - '@zag-js/anatomy': 1.35.3 - '@zag-js/core': 1.35.3 - '@zag-js/dom-query': 1.35.3 - '@zag-js/types': 1.35.3 - '@zag-js/utils': 1.35.3 + '@zag-js/anatomy': 1.41.2 + '@zag-js/core': 1.41.2 + '@zag-js/dom-query': 1.41.2 + '@zag-js/types': 1.41.2 + '@zag-js/utils': 1.41.2 - '@zag-js/splitter@1.35.3': + '@zag-js/splitter@1.41.2': dependencies: - '@zag-js/anatomy': 1.35.3 - '@zag-js/core': 1.35.3 - '@zag-js/dom-query': 1.35.3 - '@zag-js/types': 1.35.3 - '@zag-js/utils': 1.35.3 + '@zag-js/anatomy': 1.41.2 + '@zag-js/core': 1.41.2 + '@zag-js/dom-query': 1.41.2 + '@zag-js/types': 1.41.2 + '@zag-js/utils': 1.41.2 - '@zag-js/steps@1.35.3': + '@zag-js/steps@1.41.2': dependencies: - '@zag-js/anatomy': 1.35.3 - '@zag-js/core': 1.35.3 - '@zag-js/dom-query': 1.35.3 - '@zag-js/types': 1.35.3 - '@zag-js/utils': 1.35.3 + '@zag-js/anatomy': 1.41.2 + '@zag-js/core': 1.41.2 + '@zag-js/dom-query': 1.41.2 + '@zag-js/types': 1.41.2 + '@zag-js/utils': 1.41.2 - '@zag-js/store@1.35.3': + '@zag-js/store@1.41.2': dependencies: proxy-compare: 3.0.1 - '@zag-js/switch@1.35.3': - dependencies: - '@zag-js/anatomy': 1.35.3 - '@zag-js/core': 1.35.3 - '@zag-js/dom-query': 1.35.3 - '@zag-js/focus-visible': 1.35.3 - '@zag-js/types': 1.35.3 - '@zag-js/utils': 1.35.3 - - '@zag-js/tabs@1.35.3': - dependencies: - '@zag-js/anatomy': 1.35.3 - '@zag-js/core': 1.35.3 - '@zag-js/dom-query': 1.35.3 - '@zag-js/types': 1.35.3 - '@zag-js/utils': 1.35.3 - - '@zag-js/tags-input@1.35.3': - dependencies: - '@zag-js/anatomy': 1.35.3 - '@zag-js/auto-resize': 1.35.3 - '@zag-js/core': 1.35.3 - '@zag-js/dom-query': 1.35.3 - '@zag-js/interact-outside': 1.35.3 - '@zag-js/live-region': 1.35.3 - '@zag-js/types': 1.35.3 - '@zag-js/utils': 1.35.3 - - '@zag-js/timer@1.35.3': - dependencies: - '@zag-js/anatomy': 1.35.3 - '@zag-js/core': 1.35.3 - '@zag-js/dom-query': 1.35.3 - '@zag-js/types': 1.35.3 - '@zag-js/utils': 1.35.3 - - '@zag-js/toast@1.35.3': - dependencies: - '@zag-js/anatomy': 1.35.3 - '@zag-js/core': 1.35.3 - '@zag-js/dismissable': 1.35.3 - '@zag-js/dom-query': 1.35.3 - '@zag-js/types': 1.35.3 - '@zag-js/utils': 1.35.3 - - '@zag-js/toggle-group@1.35.3': - dependencies: - '@zag-js/anatomy': 1.35.3 - '@zag-js/core': 1.35.3 - '@zag-js/dom-query': 1.35.3 - '@zag-js/types': 1.35.3 - '@zag-js/utils': 1.35.3 - - '@zag-js/toggle@1.35.3': - dependencies: - '@zag-js/anatomy': 1.35.3 - '@zag-js/core': 1.35.3 - '@zag-js/dom-query': 1.35.3 - '@zag-js/types': 1.35.3 - '@zag-js/utils': 1.35.3 - - '@zag-js/tooltip@1.35.3': - dependencies: - '@zag-js/anatomy': 1.35.3 - '@zag-js/core': 1.35.3 - '@zag-js/dom-query': 1.35.3 - '@zag-js/focus-visible': 1.35.3 - '@zag-js/popper': 1.35.3 - '@zag-js/types': 1.35.3 - '@zag-js/utils': 1.35.3 - - '@zag-js/tour@1.35.3': - dependencies: - '@zag-js/anatomy': 1.35.3 - '@zag-js/core': 1.35.3 - '@zag-js/dismissable': 1.35.3 - '@zag-js/dom-query': 1.35.3 - '@zag-js/focus-trap': 1.35.3 - '@zag-js/interact-outside': 1.35.3 - '@zag-js/popper': 1.35.3 - '@zag-js/types': 1.35.3 - '@zag-js/utils': 1.35.3 - - '@zag-js/tree-view@1.35.3': - dependencies: - '@zag-js/anatomy': 1.35.3 - '@zag-js/collection': 1.35.3 - '@zag-js/core': 1.35.3 - '@zag-js/dom-query': 1.35.3 - '@zag-js/types': 1.35.3 - '@zag-js/utils': 1.35.3 - - '@zag-js/types@1.35.3': + '@zag-js/switch@1.41.2': + dependencies: + '@zag-js/anatomy': 1.41.2 + '@zag-js/core': 1.41.2 + '@zag-js/dom-query': 1.41.2 + '@zag-js/focus-visible': 1.41.2 + '@zag-js/types': 1.41.2 + '@zag-js/utils': 1.41.2 + + '@zag-js/tabs@1.41.2': + dependencies: + '@zag-js/anatomy': 1.41.2 + '@zag-js/core': 1.41.2 + '@zag-js/dom-query': 1.41.2 + '@zag-js/types': 1.41.2 + '@zag-js/utils': 1.41.2 + + '@zag-js/tags-input@1.41.2': + dependencies: + '@zag-js/anatomy': 1.41.2 + '@zag-js/auto-resize': 1.41.2 + '@zag-js/core': 1.41.2 + '@zag-js/dom-query': 1.41.2 + '@zag-js/interact-outside': 1.41.2 + '@zag-js/live-region': 1.41.2 + '@zag-js/types': 1.41.2 + '@zag-js/utils': 1.41.2 + + '@zag-js/timer@1.41.2': + dependencies: + '@zag-js/anatomy': 1.41.2 + '@zag-js/core': 1.41.2 + '@zag-js/dom-query': 1.41.2 + '@zag-js/types': 1.41.2 + '@zag-js/utils': 1.41.2 + + '@zag-js/toast@1.41.2': + dependencies: + '@zag-js/anatomy': 1.41.2 + '@zag-js/core': 1.41.2 + '@zag-js/dismissable': 1.41.2 + '@zag-js/dom-query': 1.41.2 + '@zag-js/types': 1.41.2 + '@zag-js/utils': 1.41.2 + + '@zag-js/toggle-group@1.41.2': + dependencies: + '@zag-js/anatomy': 1.41.2 + '@zag-js/core': 1.41.2 + '@zag-js/dom-query': 1.41.2 + '@zag-js/types': 1.41.2 + '@zag-js/utils': 1.41.2 + + '@zag-js/toggle@1.41.2': + dependencies: + '@zag-js/anatomy': 1.41.2 + '@zag-js/core': 1.41.2 + '@zag-js/dom-query': 1.41.2 + '@zag-js/types': 1.41.2 + '@zag-js/utils': 1.41.2 + + '@zag-js/tooltip@1.41.2': + dependencies: + '@zag-js/anatomy': 1.41.2 + '@zag-js/core': 1.41.2 + '@zag-js/dom-query': 1.41.2 + '@zag-js/focus-visible': 1.41.2 + '@zag-js/popper': 1.41.2 + '@zag-js/types': 1.41.2 + '@zag-js/utils': 1.41.2 + + '@zag-js/tour@1.41.2': + dependencies: + '@zag-js/anatomy': 1.41.2 + '@zag-js/core': 1.41.2 + '@zag-js/dismissable': 1.41.2 + '@zag-js/dom-query': 1.41.2 + '@zag-js/focus-trap': 1.41.2 + '@zag-js/interact-outside': 1.41.2 + '@zag-js/popper': 1.41.2 + '@zag-js/types': 1.41.2 + '@zag-js/utils': 1.41.2 + + '@zag-js/tree-view@1.41.2': + dependencies: + '@zag-js/anatomy': 1.41.2 + '@zag-js/collection': 1.41.2 + '@zag-js/core': 1.41.2 + '@zag-js/dom-query': 1.41.2 + '@zag-js/types': 1.41.2 + '@zag-js/utils': 1.41.2 + + '@zag-js/types@1.41.2': dependencies: csstype: 3.2.3 - '@zag-js/utils@1.35.3': {} + '@zag-js/utils@1.41.2': {} acorn-jsx@5.3.2(acorn@8.14.1): dependencies: acorn: 8.14.1 - acorn-jsx@5.3.2(acorn@8.15.0): + acorn-jsx@5.3.2(acorn@8.17.0): dependencies: - acorn: 8.15.0 - - acorn-jsx@5.3.2(acorn@8.16.0): - dependencies: - acorn: 8.16.0 + acorn: 8.17.0 acorn@8.14.1: {} - acorn@8.15.0: {} - - acorn@8.16.0: {} + acorn@8.17.0: {} agent-base@6.0.2: dependencies: @@ -6057,7 +6048,7 @@ snapshots: anymatch@3.1.3: dependencies: normalize-path: 3.0.0 - picomatch: 4.0.4 + picomatch: 4.0.5 argparse@1.0.10: dependencies: @@ -6126,9 +6117,11 @@ snapshots: get-intrinsic: 1.3.0 is-array-buffer: 3.0.5 + assertion-error@2.0.1: {} + ast-types-flow@0.0.8: {} - ast-v8-to-istanbul@1.0.0: + ast-v8-to-istanbul@1.0.4: dependencies: '@jridgewell/trace-mapping': 0.3.31 estree-walker: 3.0.3 @@ -6149,10 +6142,10 @@ snapshots: axe-core@4.10.3: {} - axios@1.16.1: + axios@1.18.1: dependencies: follow-redirects: 1.16.0 - form-data: 4.0.5 + form-data: 4.0.6 https-proxy-agent: 5.0.1 proxy-from-env: 2.1.0 transitivePeerDependencies: @@ -6163,7 +6156,7 @@ snapshots: babel-plugin-macros@3.1.0: dependencies: - '@babel/runtime': 7.29.2 + '@babel/runtime': 7.26.10 cosmiconfig: 7.1.0 resolve: 1.22.10 @@ -6186,7 +6179,7 @@ snapshots: balanced-match: 1.0.2 concat-map: 0.0.1 - brace-expansion@5.0.6: + brace-expansion@5.0.7: dependencies: balanced-match: 4.0.4 @@ -6202,6 +6195,10 @@ snapshots: node-releases: 2.0.38 update-browserslist-db: 1.2.3(browserslist@4.28.2) + buffer-image-size@0.6.4: + dependencies: + '@types/node': 24.13.2 + builtin-modules@5.1.0: {} c12@1.11.1(magicast@0.3.5): @@ -6248,12 +6245,12 @@ snapshots: chai@6.2.2: {} - chakra-react-select@6.1.1(@chakra-ui/react@3.34.0(@emotion/react@11.14.0(@types/react@19.2.15)(react@19.2.6))(react-dom@19.2.6(react@19.2.6))(react@19.2.6))(@types/react@19.2.15)(next-themes@0.4.6(react-dom@19.2.6(react@19.2.6))(react@19.2.6))(react-dom@19.2.6(react@19.2.6))(react@19.2.6): + chakra-react-select@6.1.3(@chakra-ui/react@3.36.0(@emotion/react@11.14.0(@types/react@19.2.17)(react@19.2.7))(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(@types/react@19.2.17)(next-themes@0.4.6(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react-dom@19.2.7(react@19.2.7))(react@19.2.7): dependencies: - '@chakra-ui/react': 3.34.0(@emotion/react@11.14.0(@types/react@19.2.15)(react@19.2.6))(react-dom@19.2.6(react@19.2.6))(react@19.2.6) - next-themes: 0.4.6(react-dom@19.2.6(react@19.2.6))(react@19.2.6) - react: 19.2.6 - react-select: 5.10.1(@types/react@19.2.15)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) + '@chakra-ui/react': 3.36.0(@emotion/react@11.14.0(@types/react@19.2.17)(react@19.2.7))(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + next-themes: 0.4.6(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + react: 19.2.7 + react-select: 5.10.2(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) transitivePeerDependencies: - '@types/react' - react-dom @@ -6284,10 +6281,10 @@ snapshots: dependencies: '@kurkle/color': 0.3.4 - chartjs-adapter-dayjs-4@1.0.4(chart.js@4.5.1)(dayjs@1.11.19): + chartjs-adapter-dayjs-4@1.0.4(chart.js@4.5.1)(dayjs@1.11.21): dependencies: chart.js: 4.5.1 - dayjs: 1.11.19 + dayjs: 1.11.21 chartjs-plugin-annotation@3.1.0(chart.js@4.5.1): dependencies: @@ -6371,7 +6368,7 @@ snapshots: import-fresh: 3.3.1 parse-json: 5.2.0 path-type: 4.0.0 - yaml: 2.8.3 + yaml: 2.9.0 cross-fetch@4.1.0: dependencies: @@ -6483,7 +6480,7 @@ snapshots: es-errors: 1.3.0 is-data-view: 1.0.2 - dayjs@1.11.19: {} + dayjs@1.11.21: {} debug@4.4.3: dependencies: @@ -6537,9 +6534,13 @@ snapshots: dom-helpers@5.2.1: dependencies: - '@babel/runtime': 7.29.2 + '@babel/runtime': 7.29.7 csstype: 3.2.3 + dompurify@3.2.7: + optionalDependencies: + '@types/trusted-types': 2.0.7 + dotenv@16.6.1: {} dunder-proto@1.0.1: @@ -6586,7 +6587,7 @@ snapshots: has-property-descriptors: 1.0.2 has-proto: 1.2.0 has-symbols: 1.1.0 - hasown: 2.0.3 + hasown: 2.0.2 internal-slot: 1.1.0 is-array-buffer: 3.0.5 is-callable: 1.2.7 @@ -6639,7 +6640,7 @@ snapshots: iterator.prototype: 1.1.5 safe-array-concat: 1.1.3 - es-module-lexer@2.0.0: {} + es-module-lexer@2.3.0: {} es-object-atoms@1.1.1: dependencies: @@ -6650,11 +6651,11 @@ snapshots: es-errors: 1.3.0 get-intrinsic: 1.3.0 has-tostringtag: 1.0.2 - hasown: 2.0.3 + hasown: 2.0.4 es-shim-unscopables@1.1.0: dependencies: - hasown: 2.0.3 + hasown: 2.0.2 es-to-primitive@1.3.0: dependencies: @@ -6672,37 +6673,36 @@ snapshots: escape-string-regexp@5.0.0: {} - eslint-config-prettier@10.1.8(eslint@10.3.0(jiti@1.21.7)): + eslint-config-prettier@10.1.8(eslint@10.6.0(jiti@1.21.7)): dependencies: - eslint: 10.3.0(jiti@1.21.7) + eslint: 10.6.0(jiti@1.21.7) - eslint-json-compat-utils@0.2.3(eslint@10.3.0(jiti@1.21.7))(jsonc-eslint-parser@3.1.0): + eslint-json-compat-utils@0.2.3(eslint@10.6.0(jiti@1.21.7))(jsonc-eslint-parser@3.1.0): dependencies: - eslint: 10.3.0(jiti@1.21.7) + eslint: 10.6.0(jiti@1.21.7) esquery: 1.7.0 jsonc-eslint-parser: 3.1.0 - eslint-plugin-i18next@6.1.4: + eslint-plugin-i18next@6.1.5: dependencies: - lodash: 4.18.1 requireindex: 1.1.0 - eslint-plugin-jsonc@3.1.2(eslint@10.3.0(jiti@1.21.7)): + eslint-plugin-jsonc@3.2.0(eslint@10.6.0(jiti@1.21.7)): dependencies: - '@eslint-community/eslint-utils': 4.9.1(eslint@10.3.0(jiti@1.21.7)) + '@eslint-community/eslint-utils': 4.9.1(eslint@10.6.0(jiti@1.21.7)) '@eslint/core': 1.2.1 - '@eslint/plugin-kit': 0.6.1 + '@eslint/plugin-kit': 0.7.2 '@ota-meshi/ast-token-store': 0.3.0 diff-sequences: 29.6.3 - eslint: 10.3.0(jiti@1.21.7) - eslint-json-compat-utils: 0.2.3(eslint@10.3.0(jiti@1.21.7))(jsonc-eslint-parser@3.1.0) + eslint: 10.6.0(jiti@1.21.7) + eslint-json-compat-utils: 0.2.3(eslint@10.6.0(jiti@1.21.7))(jsonc-eslint-parser@3.1.0) jsonc-eslint-parser: 3.1.0 natural-compare: 1.4.0 - synckit: 0.11.12 + synckit: 0.11.13 transitivePeerDependencies: - '@eslint/json' - eslint-plugin-jsx-a11y@6.10.2(eslint@10.3.0(jiti@1.21.7)): + eslint-plugin-jsx-a11y@6.10.2(eslint@10.6.0(jiti@1.21.7)): dependencies: aria-query: 5.3.2 array-includes: 3.1.8 @@ -6712,7 +6712,7 @@ snapshots: axobject-query: 4.1.0 damerau-levenshtein: 1.0.8 emoji-regex: 9.2.2 - eslint: 10.3.0(jiti@1.21.7) + eslint: 10.6.0(jiti@1.21.7) hasown: 2.0.2 jsx-ast-utils: 3.3.5 language-tags: 1.0.9 @@ -6721,40 +6721,40 @@ snapshots: safe-regex-test: 1.1.0 string.prototype.includes: 2.0.1 - eslint-plugin-perfectionist@5.9.0(eslint@10.3.0(jiti@1.21.7))(typescript@6.0.3): + eslint-plugin-perfectionist@5.9.1(eslint@10.6.0(jiti@1.21.7))(typescript@6.0.3): dependencies: - '@typescript-eslint/utils': 8.60.0(eslint@10.3.0(jiti@1.21.7))(typescript@6.0.3) - eslint: 10.3.0(jiti@1.21.7) + '@typescript-eslint/utils': 8.62.1(eslint@10.6.0(jiti@1.21.7))(typescript@6.0.3) + eslint: 10.6.0(jiti@1.21.7) natural-orderby: 5.0.0 transitivePeerDependencies: - supports-color - typescript - eslint-plugin-prettier@5.5.5(eslint-config-prettier@10.1.8(eslint@10.3.0(jiti@1.21.7)))(eslint@10.3.0(jiti@1.21.7))(prettier@3.8.1): + eslint-plugin-prettier@5.5.6(eslint-config-prettier@10.1.8(eslint@10.6.0(jiti@1.21.7)))(eslint@10.6.0(jiti@1.21.7))(prettier@3.9.4): dependencies: - eslint: 10.3.0(jiti@1.21.7) - prettier: 3.8.1 + eslint: 10.6.0(jiti@1.21.7) + prettier: 3.9.4 prettier-linter-helpers: 1.0.1 - synckit: 0.11.12 + synckit: 0.11.13 optionalDependencies: - eslint-config-prettier: 10.1.8(eslint@10.3.0(jiti@1.21.7)) + eslint-config-prettier: 10.1.8(eslint@10.6.0(jiti@1.21.7)) - eslint-plugin-react-hooks@7.1.1(eslint@10.3.0(jiti@1.21.7)): + eslint-plugin-react-hooks@7.1.1(eslint@10.6.0(jiti@1.21.7)): dependencies: '@babel/core': 7.29.0 '@babel/parser': 7.29.2 - eslint: 10.3.0(jiti@1.21.7) + eslint: 10.6.0(jiti@1.21.7) hermes-parser: 0.25.1 zod: 4.3.6 zod-validation-error: 4.0.2(zod@4.3.6) transitivePeerDependencies: - supports-color - eslint-plugin-react-refresh@0.5.2(eslint@10.3.0(jiti@1.21.7)): + eslint-plugin-react-refresh@0.5.3(eslint@10.6.0(jiti@1.21.7)): dependencies: - eslint: 10.3.0(jiti@1.21.7) + eslint: 10.6.0(jiti@1.21.7) - eslint-plugin-react@7.37.5(eslint@10.3.0(jiti@1.21.7)): + eslint-plugin-react@7.37.5(eslint@10.6.0(jiti@1.21.7)): dependencies: array-includes: 3.1.8 array.prototype.findlast: 1.2.5 @@ -6762,7 +6762,7 @@ snapshots: array.prototype.tosorted: 1.1.4 doctrine: 2.1.0 es-iterator-helpers: 1.2.1 - eslint: 10.3.0(jiti@1.21.7) + eslint: 10.6.0(jiti@1.21.7) estraverse: 5.3.0 hasown: 2.0.2 jsx-ast-utils: 3.3.5 @@ -6776,15 +6776,15 @@ snapshots: string.prototype.matchall: 4.0.12 string.prototype.repeat: 1.0.0 - eslint-plugin-unicorn@64.0.0(eslint@10.3.0(jiti@1.21.7)): + eslint-plugin-unicorn@64.0.0(eslint@10.6.0(jiti@1.21.7)): dependencies: '@babel/helper-validator-identifier': 7.28.5 - '@eslint-community/eslint-utils': 4.9.1(eslint@10.3.0(jiti@1.21.7)) + '@eslint-community/eslint-utils': 4.9.1(eslint@10.6.0(jiti@1.21.7)) change-case: 5.4.4 ci-info: 4.4.0 clean-regexp: 1.0.0 core-js-compat: 3.49.0 - eslint: 10.3.0(jiti@1.21.7) + eslint: 10.6.0(jiti@1.21.7) find-up-simple: 1.0.1 globals: 17.5.0 indent-string: 5.0.0 @@ -6809,14 +6809,14 @@ snapshots: eslint-visitor-keys@5.0.1: {} - eslint@10.3.0(jiti@1.21.7): + eslint@10.6.0(jiti@1.21.7): dependencies: - '@eslint-community/eslint-utils': 4.9.1(eslint@10.3.0(jiti@1.21.7)) + '@eslint-community/eslint-utils': 4.9.1(eslint@10.6.0(jiti@1.21.7)) '@eslint-community/regexpp': 4.12.2 '@eslint/config-array': 0.23.5 - '@eslint/config-helpers': 0.5.5 + '@eslint/config-helpers': 0.6.0 '@eslint/core': 1.2.1 - '@eslint/plugin-kit': 0.7.1 + '@eslint/plugin-kit': 0.7.2 '@humanfs/node': 0.16.8 '@humanwhocodes/module-importer': 1.0.1 '@humanwhocodes/retry': 0.4.3 @@ -6854,14 +6854,14 @@ snapshots: espree@11.2.0: dependencies: - acorn: 8.16.0 - acorn-jsx: 5.3.2(acorn@8.16.0) + acorn: 8.17.0 + acorn-jsx: 5.3.2(acorn@8.17.0) eslint-visitor-keys: 5.0.1 espree@9.6.1: dependencies: - acorn: 8.15.0 - acorn-jsx: 5.3.2(acorn@8.15.0) + acorn: 8.17.0 + acorn-jsx: 5.3.2(acorn@8.17.0) eslint-visitor-keys: 3.4.3 esprima@4.0.1: {} @@ -6884,7 +6884,7 @@ snapshots: esutils@2.0.3: {} - expect-type@1.3.0: {} + expect-type@1.4.0: {} extend@3.0.2: {} @@ -6896,13 +6896,23 @@ snapshots: fast-levenshtein@2.0.6: {} + fast-string-truncated-width@3.0.3: {} + + fast-string-width@3.0.2: + dependencies: + fast-string-truncated-width: 3.0.3 + + fast-wrap-ansi@0.2.2: + dependencies: + fast-string-width: 3.0.2 + fault@1.0.4: dependencies: format: 0.2.2 - fdir@6.5.0(picomatch@4.0.4): + fdir@6.5.0(picomatch@4.0.5): optionalDependencies: - picomatch: 4.0.4 + picomatch: 4.0.5 file-entry-cache@8.0.0: dependencies: @@ -6939,12 +6949,12 @@ snapshots: cross-spawn: 7.0.6 signal-exit: 4.1.0 - form-data@4.0.5: + form-data@4.0.6: dependencies: asynckit: 0.4.0 combined-stream: 1.0.8 es-set-tostringtag: 2.1.0 - hasown: 2.0.3 + hasown: 2.0.4 mime-types: 2.1.35 format@0.2.2: {} @@ -6963,7 +6973,7 @@ snapshots: call-bound: 1.0.4 define-properties: 1.2.1 functions-have-names: 1.2.3 - hasown: 2.0.3 + hasown: 2.0.2 is-callable: 1.2.7 functions-have-names@1.2.3: {} @@ -6982,7 +6992,7 @@ snapshots: get-proto: 1.0.1 gopd: 1.2.0 has-symbols: 1.1.0 - hasown: 2.0.3 + hasown: 2.0.2 math-intrinsics: 1.1.0 get-proto@1.0.1: @@ -7036,7 +7046,7 @@ snapshots: gopd@1.2.0: {} - graphql@16.13.1: {} + graphql@16.14.2: {} handlebars@4.7.9: dependencies: @@ -7047,11 +7057,12 @@ snapshots: optionalDependencies: uglify-js: 3.19.3 - happy-dom@20.8.9: + happy-dom@20.10.6: dependencies: - '@types/node': 24.10.3 + '@types/node': 24.13.2 '@types/whatwg-mimetype': 3.0.2 '@types/ws': 8.18.1 + buffer-image-size: 0.6.4 entities: 7.0.1 whatwg-mimetype: 3.0.0 ws: 8.21.0 @@ -7081,7 +7092,7 @@ snapshots: dependencies: function-bind: 1.1.2 - hasown@2.0.3: + hasown@2.0.4: dependencies: function-bind: 1.1.2 @@ -7119,7 +7130,10 @@ snapshots: property-information: 5.6.0 space-separated-tokens: 1.1.5 - headers-polyfill@4.0.3: {} + headers-polyfill@5.0.1: + dependencies: + '@types/set-cookie-parser': 2.4.10 + set-cookie-parser: 3.1.1 hermes-estree@0.25.1: {} @@ -7156,15 +7170,15 @@ snapshots: dependencies: '@babel/runtime': 7.28.6 - i18next-http-backend@3.0.5: + i18next-http-backend@3.0.6: dependencies: cross-fetch: 4.1.0 transitivePeerDependencies: - encoding - i18next@25.8.16(typescript@6.0.3): + i18next@25.10.10(typescript@6.0.3): dependencies: - '@babel/runtime': 7.28.6 + '@babel/runtime': 7.29.7 optionalDependencies: typescript: 6.0.3 @@ -7188,7 +7202,7 @@ snapshots: internal-slot@1.1.0: dependencies: es-errors: 1.3.0 - hasown: 2.0.3 + hasown: 2.0.2 side-channel: 1.1.0 internmap@2.0.3: {} @@ -7244,7 +7258,7 @@ snapshots: is-core-module@2.16.1: dependencies: - hasown: 2.0.3 + hasown: 2.0.2 is-data-view@1.0.2: dependencies: @@ -7302,7 +7316,7 @@ snapshots: call-bound: 1.0.4 gopd: 1.2.0 has-tostringtag: 1.0.2 - hasown: 2.0.3 + hasown: 2.0.2 is-set@2.0.3: {} @@ -7404,18 +7418,18 @@ snapshots: json5@2.2.3: {} - jsonc-eslint-parser@2.4.1: + jsonc-eslint-parser@2.4.2: dependencies: - acorn: 8.15.0 + acorn: 8.17.0 eslint-visitor-keys: 3.4.3 espree: 9.6.1 - semver: 7.7.1 + semver: 7.8.5 jsonc-eslint-parser@3.1.0: dependencies: - acorn: 8.16.0 + acorn: 8.17.0 eslint-visitor-keys: 5.0.1 - semver: 7.7.4 + semver: 7.8.5 jsonpointer@5.0.1: {} @@ -7530,18 +7544,20 @@ snapshots: source-map-js: 1.2.1 optional: true - magicast@0.5.2: + magicast@0.5.3: dependencies: - '@babel/parser': 7.29.2 - '@babel/types': 7.29.0 + '@babel/parser': 7.29.7 + '@babel/types': 7.29.7 source-map-js: 1.2.1 make-dir@4.0.0: dependencies: - semver: 7.8.0 + semver: 7.8.5 markdown-table@3.0.4: {} + marked@14.0.0: {} + math-intrinsics@1.1.0: {} mdast-util-find-and-replace@3.0.2: @@ -7900,7 +7916,7 @@ snapshots: minimatch@10.2.5: dependencies: - brace-expansion: 5.0.6 + brace-expansion: 5.0.7 minimatch@3.1.5: dependencies: @@ -7916,43 +7932,46 @@ snapshots: mlly@1.8.0: dependencies: - acorn: 8.16.0 + acorn: 8.17.0 pathe: 2.0.3 pkg-types: 1.3.1 ufo: 1.6.1 - monaco-editor@0.52.2: {} + monaco-editor@0.55.1: + dependencies: + dompurify: 3.2.7 + marked: 14.0.0 ms@2.1.3: {} - msw@2.12.10(@types/node@24.10.3)(typescript@6.0.3): + msw@2.14.6(@types/node@24.13.2)(typescript@6.0.3): dependencies: - '@inquirer/confirm': 5.1.21(@types/node@24.10.3) - '@mswjs/interceptors': 0.41.3 - '@open-draft/deferred-promise': 2.2.0 + '@inquirer/confirm': 6.1.1(@types/node@24.13.2) + '@mswjs/interceptors': 0.41.9 + '@open-draft/deferred-promise': 3.0.0 '@types/statuses': 2.0.6 cookie: 1.1.1 - graphql: 16.13.1 - headers-polyfill: 4.0.3 + graphql: 16.14.2 + headers-polyfill: 5.0.1 is-node-process: 1.2.0 outvariant: 1.4.3 path-to-regexp: 6.3.0 picocolors: 1.1.1 - rettime: 0.10.1 + rettime: 0.11.11 statuses: 2.0.2 strict-event-emitter: 0.5.1 - tough-cookie: 6.0.0 - type-fest: 5.4.4 + tough-cookie: 6.0.1 + type-fest: 5.8.0 until-async: 3.0.2 - yargs: 17.7.2 + yargs: 17.7.3 optionalDependencies: typescript: 6.0.3 transitivePeerDependencies: - '@types/node' - mute-stream@2.0.0: {} + mute-stream@3.0.0: {} - nanoid@3.3.12: {} + nanoid@3.3.15: {} natural-compare@1.4.0: {} @@ -7960,10 +7979,10 @@ snapshots: neo-async@2.6.2: {} - next-themes@0.4.6(react-dom@19.2.6(react@19.2.6))(react@19.2.6): + next-themes@0.4.6(react-dom@19.2.7(react@19.2.7))(react@19.2.7): dependencies: - react: 19.2.6 - react-dom: 19.2.6(react@19.2.6) + react: 19.2.7 + react-dom: 19.2.7(react@19.2.7) node-fetch-native@1.6.7: {} @@ -8020,7 +8039,7 @@ snapshots: define-properties: 1.2.1 es-object-atoms: 1.1.1 - obug@2.1.1: {} + obug@2.1.3: {} ohash@1.1.6: {} @@ -8127,17 +8146,19 @@ snapshots: picomatch@4.0.4: {} + picomatch@4.0.5: {} + pkg-types@1.3.1: dependencies: confbox: 0.1.8 mlly: 1.8.0 pathe: 2.0.3 - playwright-core@1.60.0: {} + playwright-core@1.61.1: {} - playwright@1.60.0: + playwright@1.61.1: dependencies: - playwright-core: 1.60.0 + playwright-core: 1.61.1 optionalDependencies: fsevents: 2.3.2 @@ -8145,9 +8166,9 @@ snapshots: possible-typed-array-names@1.1.0: {} - postcss@8.5.15: + postcss@8.5.16: dependencies: - nanoid: 3.3.12 + nanoid: 3.3.15 picocolors: 1.1.1 source-map-js: 1.2.1 @@ -8157,7 +8178,7 @@ snapshots: dependencies: fast-diff: 1.3.0 - prettier@3.8.1: {} + prettier@3.9.4: {} pretty-format@27.5.1: dependencies: @@ -8194,59 +8215,59 @@ snapshots: defu: 6.1.6 destr: 2.0.5 - react-chartjs-2@5.3.1(chart.js@4.5.1)(react@19.2.6): + react-chartjs-2@5.3.1(chart.js@4.5.1)(react@19.2.7): dependencies: chart.js: 4.5.1 - react: 19.2.6 + react: 19.2.7 - react-dom@19.2.6(react@19.2.6): + react-dom@19.2.7(react@19.2.7): dependencies: - react: 19.2.6 + react: 19.2.7 scheduler: 0.27.0 - react-hook-form@7.71.2(react@19.2.6): + react-hook-form@7.80.0(react@19.2.7): dependencies: - react: 19.2.6 + react: 19.2.7 - react-hotkeys-hook@4.6.1(react-dom@19.2.6(react@19.2.6))(react@19.2.6): + react-hotkeys-hook@4.6.2(react-dom@19.2.7(react@19.2.7))(react@19.2.7): dependencies: - react: 19.2.6 - react-dom: 19.2.6(react@19.2.6) + react: 19.2.7 + react-dom: 19.2.7(react@19.2.7) - react-i18next@16.6.5(i18next@25.8.16(typescript@6.0.3))(react-dom@19.2.6(react@19.2.6))(react@19.2.6)(typescript@6.0.3): + react-i18next@16.6.6(i18next@25.10.10(typescript@6.0.3))(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(typescript@6.0.3): dependencies: - '@babel/runtime': 7.29.2 + '@babel/runtime': 7.29.7 html-parse-stringify: 3.0.1 - i18next: 25.8.16(typescript@6.0.3) - react: 19.2.6 - use-sync-external-store: 1.6.0(react@19.2.6) + i18next: 25.10.10(typescript@6.0.3) + react: 19.2.7 + use-sync-external-store: 1.6.0(react@19.2.7) optionalDependencies: - react-dom: 19.2.6(react@19.2.6) + react-dom: 19.2.7(react@19.2.7) typescript: 6.0.3 - react-icons@5.6.0(react@19.2.6): + react-icons@5.7.0(react@19.2.7): dependencies: - react: 19.2.6 + react: 19.2.7 - react-innertext@1.1.5(@types/react@19.2.15)(react@19.2.6): + react-innertext@1.1.5(@types/react@19.2.17)(react@19.2.7): dependencies: - '@types/react': 19.2.15 - react: 19.2.6 + '@types/react': 19.2.17 + react: 19.2.7 react-is@16.13.1: {} react-is@17.0.2: {} - react-markdown@9.1.0(@types/react@19.2.15)(react@19.2.6): + react-markdown@9.1.0(@types/react@19.2.17)(react@19.2.7): dependencies: '@types/hast': 3.0.4 '@types/mdast': 4.0.4 - '@types/react': 19.2.15 + '@types/react': 19.2.17 devlop: 1.1.0 hast-util-to-jsx-runtime: 2.3.6 html-url-attributes: 3.0.1 mdast-util-to-hast: 13.2.1 - react: 19.2.6 + react: 19.2.7 remark-parse: 11.0.0 remark-rehype: 11.1.1 unified: 11.0.5 @@ -8255,66 +8276,66 @@ snapshots: transitivePeerDependencies: - supports-color - react-resizable-panels@3.0.6(react-dom@19.2.6(react@19.2.6))(react@19.2.6): + react-resizable-panels@3.0.6(react-dom@19.2.7(react@19.2.7))(react@19.2.7): dependencies: - react: 19.2.6 - react-dom: 19.2.6(react@19.2.6) + react: 19.2.7 + react-dom: 19.2.7(react@19.2.7) - react-router-dom@7.13.1(react-dom@19.2.6(react@19.2.6))(react@19.2.6): + react-router-dom@7.18.1(react-dom@19.2.7(react@19.2.7))(react@19.2.7): dependencies: - react: 19.2.6 - react-dom: 19.2.6(react@19.2.6) - react-router: 7.16.0(react-dom@19.2.6(react@19.2.6))(react@19.2.6) + react: 19.2.7 + react-dom: 19.2.7(react@19.2.7) + react-router: 7.18.1(react-dom@19.2.7(react@19.2.7))(react@19.2.7) - react-router@7.16.0(react-dom@19.2.6(react@19.2.6))(react@19.2.6): + react-router@7.18.1(react-dom@19.2.7(react@19.2.7))(react@19.2.7): dependencies: cookie: 1.1.1 - react: 19.2.6 + react: 19.2.7 set-cookie-parser: 2.7.2 optionalDependencies: - react-dom: 19.2.6(react@19.2.6) + react-dom: 19.2.7(react@19.2.7) - react-select@5.10.1(@types/react@19.2.15)(react-dom@19.2.6(react@19.2.6))(react@19.2.6): + react-select@5.10.2(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7): dependencies: - '@babel/runtime': 7.29.2 + '@babel/runtime': 7.29.7 '@emotion/cache': 11.14.0 - '@emotion/react': 11.14.0(@types/react@19.2.15)(react@19.2.6) - '@floating-ui/dom': 1.7.1 - '@types/react-transition-group': 4.4.12(@types/react@19.2.15) + '@emotion/react': 11.14.0(@types/react@19.2.17)(react@19.2.7) + '@floating-ui/dom': 1.7.6 + '@types/react-transition-group': 4.4.12(@types/react@19.2.17) memoize-one: 6.0.0 prop-types: 15.8.1 - react: 19.2.6 - react-dom: 19.2.6(react@19.2.6) - react-transition-group: 4.4.5(react-dom@19.2.6(react@19.2.6))(react@19.2.6) - use-isomorphic-layout-effect: 1.2.1(@types/react@19.2.15)(react@19.2.6) + react: 19.2.7 + react-dom: 19.2.7(react@19.2.7) + react-transition-group: 4.4.5(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + use-isomorphic-layout-effect: 1.2.1(@types/react@19.2.17)(react@19.2.7) transitivePeerDependencies: - '@types/react' - supports-color - react-syntax-highlighter@15.6.1(react@19.2.6): + react-syntax-highlighter@15.6.6(react@19.2.7): dependencies: - '@babel/runtime': 7.26.10 + '@babel/runtime': 7.29.7 highlight.js: 10.7.3 highlightjs-vue: 1.0.0 lowlight: 1.20.0 prismjs: 1.30.0 - react: 19.2.6 + react: 19.2.7 refractor: 3.6.0 - react-transition-group@4.4.5(react-dom@19.2.6(react@19.2.6))(react@19.2.6): + react-transition-group@4.4.5(react-dom@19.2.7(react@19.2.7))(react@19.2.7): dependencies: - '@babel/runtime': 7.29.2 + '@babel/runtime': 7.29.7 dom-helpers: 5.2.1 loose-envify: 1.4.0 prop-types: 15.8.1 - react: 19.2.6 - react-dom: 19.2.6(react@19.2.6) + react: 19.2.7 + react-dom: 19.2.7(react@19.2.7) - react@19.2.6: {} + react@19.2.7: {} readdirp@3.6.0: dependencies: - picomatch: 4.0.4 + picomatch: 4.0.5 redent@3.0.0: dependencies: @@ -8407,30 +8428,30 @@ snapshots: path-parse: 1.0.7 supports-preserve-symlinks-flag: 1.0.0 - rettime@0.10.1: {} + rettime@0.11.11: {} robust-predicates@3.0.2: {} - rolldown@1.0.3: + rolldown@1.1.4: dependencies: - '@oxc-project/types': 0.133.0 + '@oxc-project/types': 0.138.0 '@rolldown/pluginutils': 1.0.1 optionalDependencies: - '@rolldown/binding-android-arm64': 1.0.3 - '@rolldown/binding-darwin-arm64': 1.0.3 - '@rolldown/binding-darwin-x64': 1.0.3 - '@rolldown/binding-freebsd-x64': 1.0.3 - '@rolldown/binding-linux-arm-gnueabihf': 1.0.3 - '@rolldown/binding-linux-arm64-gnu': 1.0.3 - '@rolldown/binding-linux-arm64-musl': 1.0.3 - '@rolldown/binding-linux-ppc64-gnu': 1.0.3 - '@rolldown/binding-linux-s390x-gnu': 1.0.3 - '@rolldown/binding-linux-x64-gnu': 1.0.3 - '@rolldown/binding-linux-x64-musl': 1.0.3 - '@rolldown/binding-openharmony-arm64': 1.0.3 - '@rolldown/binding-wasm32-wasi': 1.0.3 - '@rolldown/binding-win32-arm64-msvc': 1.0.3 - '@rolldown/binding-win32-x64-msvc': 1.0.3 + '@rolldown/binding-android-arm64': 1.1.4 + '@rolldown/binding-darwin-arm64': 1.1.4 + '@rolldown/binding-darwin-x64': 1.1.4 + '@rolldown/binding-freebsd-x64': 1.1.4 + '@rolldown/binding-linux-arm-gnueabihf': 1.1.4 + '@rolldown/binding-linux-arm64-gnu': 1.1.4 + '@rolldown/binding-linux-arm64-musl': 1.1.4 + '@rolldown/binding-linux-ppc64-gnu': 1.1.4 + '@rolldown/binding-linux-s390x-gnu': 1.1.4 + '@rolldown/binding-linux-x64-gnu': 1.1.4 + '@rolldown/binding-linux-x64-musl': 1.1.4 + '@rolldown/binding-openharmony-arm64': 1.1.4 + '@rolldown/binding-wasm32-wasi': 1.1.4 + '@rolldown/binding-win32-arm64-msvc': 1.1.4 + '@rolldown/binding-win32-x64-msvc': 1.1.4 safe-array-concat@1.1.3: dependencies: @@ -8455,16 +8476,14 @@ snapshots: semver@6.3.1: {} - semver@7.7.1: {} - semver@7.7.4: {} - semver@7.8.0: {} - - semver@7.8.1: {} + semver@7.8.5: {} set-cookie-parser@2.7.2: {} + set-cookie-parser@3.1.1: {} + set-function-length@1.2.2: dependencies: define-data-property: 1.1.4 @@ -8543,7 +8562,7 @@ snapshots: statuses@2.0.2: {} - std-env@4.0.0: {} + std-env@4.1.0: {} strict-event-emitter@0.5.1: {} @@ -8634,9 +8653,9 @@ snapshots: supports-preserve-symlinks-flag@1.0.0: {} - synckit@0.11.12: + synckit@0.11.13: dependencies: - '@pkgr/core': 0.2.9 + '@pkgr/core': 0.3.6 tagged-tag@1.0.0: {} @@ -8652,25 +8671,20 @@ snapshots: tinyexec@0.3.2: {} - tinyexec@1.1.1: {} - - tinyglobby@0.2.15: - dependencies: - fdir: 6.5.0(picomatch@4.0.4) - picomatch: 4.0.4 + tinyexec@1.2.4: {} tinyglobby@0.2.17: dependencies: - fdir: 6.5.0(picomatch@4.0.4) - picomatch: 4.0.4 + fdir: 6.5.0(picomatch@4.0.5) + picomatch: 4.0.5 tinyrainbow@3.1.0: {} - tldts-core@7.0.25: {} + tldts-core@7.4.6: {} - tldts@7.0.25: + tldts@7.4.6: dependencies: - tldts-core: 7.0.25 + tldts-core: 7.4.6 to-fast-properties@2.0.0: {} @@ -8678,9 +8692,9 @@ snapshots: dependencies: is-number: 7.0.0 - tough-cookie@6.0.0: + tough-cookie@6.0.1: dependencies: - tldts: 7.0.25 + tldts: 7.4.6 tr46@0.0.3: {} @@ -8705,7 +8719,7 @@ snapshots: dependencies: prelude-ls: 1.2.1 - type-fest@5.4.4: + type-fest@5.8.0: dependencies: tagged-tag: 1.0.0 @@ -8742,13 +8756,13 @@ snapshots: possible-typed-array-names: 1.1.0 reflect.getprototypeof: 1.0.10 - typescript-eslint@8.60.0(eslint@10.3.0(jiti@1.21.7))(typescript@6.0.3): + typescript-eslint@8.62.1(eslint@10.6.0(jiti@1.21.7))(typescript@6.0.3): dependencies: - '@typescript-eslint/eslint-plugin': 8.60.0(@typescript-eslint/parser@8.60.0(eslint@10.3.0(jiti@1.21.7))(typescript@6.0.3))(eslint@10.3.0(jiti@1.21.7))(typescript@6.0.3) - '@typescript-eslint/parser': 8.60.0(eslint@10.3.0(jiti@1.21.7))(typescript@6.0.3) - '@typescript-eslint/typescript-estree': 8.60.0(typescript@6.0.3) - '@typescript-eslint/utils': 8.60.0(eslint@10.3.0(jiti@1.21.7))(typescript@6.0.3) - eslint: 10.3.0(jiti@1.21.7) + '@typescript-eslint/eslint-plugin': 8.62.1(@typescript-eslint/parser@8.62.1(eslint@10.6.0(jiti@1.21.7))(typescript@6.0.3))(eslint@10.6.0(jiti@1.21.7))(typescript@6.0.3) + '@typescript-eslint/parser': 8.62.1(eslint@10.6.0(jiti@1.21.7))(typescript@6.0.3) + '@typescript-eslint/typescript-estree': 8.62.1(typescript@6.0.3) + '@typescript-eslint/utils': 8.62.1(eslint@10.6.0(jiti@1.21.7))(typescript@6.0.3) + eslint: 10.6.0(jiti@1.21.7) typescript: 6.0.3 transitivePeerDependencies: - supports-color @@ -8767,7 +8781,7 @@ snapshots: has-symbols: 1.1.0 which-boxed-primitive: 1.1.1 - undici-types@7.16.0: {} + undici-types@7.18.2: {} unified@11.0.5: dependencies: @@ -8810,7 +8824,7 @@ snapshots: escalade: 3.2.0 picocolors: 1.1.1 - uqr@0.1.2: {} + uqr@0.1.3: {} uri-js@4.4.1: dependencies: @@ -8818,24 +8832,24 @@ snapshots: urijs@1.19.11: {} - use-debounce@10.1.0(react@19.2.6): + use-debounce@10.1.1(react@19.2.7): dependencies: - react: 19.2.6 + react: 19.2.7 - use-isomorphic-layout-effect@1.2.1(@types/react@19.2.15)(react@19.2.6): + use-isomorphic-layout-effect@1.2.1(@types/react@19.2.17)(react@19.2.7): dependencies: - react: 19.2.6 + react: 19.2.7 optionalDependencies: - '@types/react': 19.2.15 + '@types/react': 19.2.17 - use-sync-external-store@1.6.0(react@19.2.6): + use-sync-external-store@1.6.0(react@19.2.7): dependencies: - react: 19.2.6 + react: 19.2.7 - usehooks-ts@3.1.1(react@19.2.6): + usehooks-ts@3.1.1(react@19.2.7): dependencies: lodash.debounce: 4.0.8 - react: 19.2.6 + react: 19.2.7 vfile-message@4.0.2: dependencies: @@ -8847,49 +8861,49 @@ snapshots: '@types/unist': 3.0.3 vfile-message: 4.0.2 - vite-plugin-css-injected-by-js@3.5.2(vite@8.0.16(@types/node@24.10.3)(jiti@1.21.7)(yaml@2.8.3)): + vite-plugin-css-injected-by-js@3.5.2(vite@8.1.2(@types/node@24.13.2)(jiti@1.21.7)(yaml@2.9.0)): dependencies: - vite: 8.0.16(@types/node@24.10.3)(jiti@1.21.7)(yaml@2.8.3) + vite: 8.1.2(@types/node@24.13.2)(jiti@1.21.7)(yaml@2.9.0) - vite@8.0.16(@types/node@24.10.3)(jiti@1.21.7)(yaml@2.8.3): + vite@8.1.2(@types/node@24.13.2)(jiti@1.21.7)(yaml@2.9.0): dependencies: lightningcss: 1.32.0 - picomatch: 4.0.4 - postcss: 8.5.15 - rolldown: 1.0.3 + picomatch: 4.0.5 + postcss: 8.5.16 + rolldown: 1.1.4 tinyglobby: 0.2.17 optionalDependencies: - '@types/node': 24.10.3 + '@types/node': 24.13.2 fsevents: 2.3.3 jiti: 1.21.7 - yaml: 2.8.3 - - vitest@4.1.4(@types/node@24.10.3)(@vitest/coverage-v8@4.1.4)(happy-dom@20.8.9)(msw@2.12.10(@types/node@24.10.3)(typescript@6.0.3))(vite@8.0.16(@types/node@24.10.3)(jiti@1.21.7)(yaml@2.8.3)): - dependencies: - '@vitest/expect': 4.1.4 - '@vitest/mocker': 4.1.4(msw@2.12.10(@types/node@24.10.3)(typescript@6.0.3))(vite@8.0.16(@types/node@24.10.3)(jiti@1.21.7)(yaml@2.8.3)) - '@vitest/pretty-format': 4.1.4 - '@vitest/runner': 4.1.4 - '@vitest/snapshot': 4.1.4 - '@vitest/spy': 4.1.4 - '@vitest/utils': 4.1.4 - es-module-lexer: 2.0.0 - expect-type: 1.3.0 + yaml: 2.9.0 + + vitest@4.1.9(@types/node@24.13.2)(@vitest/coverage-v8@4.1.9)(happy-dom@20.10.6)(msw@2.14.6(@types/node@24.13.2)(typescript@6.0.3))(vite@8.1.2(@types/node@24.13.2)(jiti@1.21.7)(yaml@2.9.0)): + dependencies: + '@vitest/expect': 4.1.9 + '@vitest/mocker': 4.1.9(msw@2.14.6(@types/node@24.13.2)(typescript@6.0.3))(vite@8.1.2(@types/node@24.13.2)(jiti@1.21.7)(yaml@2.9.0)) + '@vitest/pretty-format': 4.1.9 + '@vitest/runner': 4.1.9 + '@vitest/snapshot': 4.1.9 + '@vitest/spy': 4.1.9 + '@vitest/utils': 4.1.9 + es-module-lexer: 2.3.0 + expect-type: 1.4.0 magic-string: 0.30.21 - obug: 2.1.1 + obug: 2.1.3 pathe: 2.0.3 - picomatch: 4.0.4 - std-env: 4.0.0 + picomatch: 4.0.5 + std-env: 4.1.0 tinybench: 2.9.0 - tinyexec: 1.1.1 - tinyglobby: 0.2.15 + tinyexec: 1.2.4 + tinyglobby: 0.2.17 tinyrainbow: 3.1.0 - vite: 8.0.16(@types/node@24.10.3)(jiti@1.21.7)(yaml@2.8.3) + vite: 8.1.2(@types/node@24.13.2)(jiti@1.21.7)(yaml@2.9.0) why-is-node-running: 2.3.0 optionalDependencies: - '@types/node': 24.10.3 - '@vitest/coverage-v8': 4.1.4(vitest@4.1.4) - happy-dom: 20.8.9 + '@types/node': 24.13.2 + '@vitest/coverage-v8': 4.1.9(vitest@4.1.9) + happy-dom: 20.10.6 transitivePeerDependencies: - msw @@ -8962,12 +8976,6 @@ snapshots: wordwrap@1.0.0: {} - wrap-ansi@6.2.0: - dependencies: - ansi-styles: 4.3.0 - string-width: 4.2.3 - strip-ansi: 6.0.1 - wrap-ansi@7.0.0: dependencies: ansi-styles: 4.3.0 @@ -8984,11 +8992,11 @@ snapshots: yallist@5.0.0: {} - yaml@2.8.3: {} + yaml@2.9.0: {} yargs-parser@21.1.1: {} - yargs@17.7.2: + yargs@17.7.3: dependencies: cliui: 8.0.1 escalade: 3.2.0 @@ -9000,25 +9008,23 @@ snapshots: yocto-queue@0.1.0: {} - yoctocolors-cjs@2.1.3: {} - zod-validation-error@4.0.2(zod@4.3.6): dependencies: zod: 4.3.6 zod@4.3.6: {} - zustand@4.5.7(@types/react@19.2.15)(react@19.2.6): + zustand@4.5.7(@types/react@19.2.17)(react@19.2.7): dependencies: - use-sync-external-store: 1.6.0(react@19.2.6) + use-sync-external-store: 1.6.0(react@19.2.7) optionalDependencies: - '@types/react': 19.2.15 - react: 19.2.6 + '@types/react': 19.2.17 + react: 19.2.7 - zustand@5.0.11(@types/react@19.2.15)(react@19.2.6)(use-sync-external-store@1.6.0(react@19.2.6)): + zustand@5.0.14(@types/react@19.2.17)(react@19.2.7)(use-sync-external-store@1.6.0(react@19.2.7)): optionalDependencies: - '@types/react': 19.2.15 - react: 19.2.6 - use-sync-external-store: 1.6.0(react@19.2.6) + '@types/react': 19.2.17 + react: 19.2.7 + use-sync-external-store: 1.6.0(react@19.2.7) zwitch@2.0.4: {} diff --git a/airflow-core/src/airflow/ui/public/i18n/locales/en/admin.json b/airflow-core/src/airflow/ui/public/i18n/locales/en/admin.json index 1b02ee5b9b448..58319ddd81d5c 100644 --- a/airflow-core/src/airflow/ui/public/i18n/locales/en/admin.json +++ b/airflow-core/src/airflow/ui/public/i18n/locales/en/admin.json @@ -177,6 +177,7 @@ "title": "Skip" } }, + "parsingFile": "Parsing file...", "title": "Import Variables", "upload": "Upload a JSON File", "uploadPlaceholder": "Upload a JSON file containing variables (e.g., {\"key\": \"value\", ...})" diff --git a/airflow-core/src/airflow/ui/src/components/MonacoEditor/configureMonaco.ts b/airflow-core/src/airflow/ui/src/components/MonacoEditor/configureMonaco.ts index 97b6ae6be20e5..97ac79a8b36dd 100644 --- a/airflow-core/src/airflow/ui/src/components/MonacoEditor/configureMonaco.ts +++ b/airflow-core/src/airflow/ui/src/components/MonacoEditor/configureMonaco.ts @@ -31,9 +31,9 @@ const loadMonacoModules = async () => { // to register their actions and render their glyphs. The CDN bundle pulled these in // transitively; the local ESM build does not. const monacoApi = Promise.all([ - import("monaco-editor/esm/vs/editor/editor.api"), - import("monaco-editor/esm/vs/editor/contrib/folding/browser/folding"), - import("monaco-editor/esm/vs/editor/contrib/find/browser/findController"), + import("monaco-editor/esm/vs/editor/editor.api.js"), + import("monaco-editor/esm/vs/editor/contrib/folding/browser/folding.js"), + import("monaco-editor/esm/vs/editor/contrib/find/browser/findController.js"), // monaco-editor 0.53 removed the `codiconStyles` side-effect module; import the two codicon // stylesheets it used to pull in directly so folding/find glyphs still render. Both files // ship in 0.52 and 0.55, so this resolves against the current pin and any newer bump. @@ -61,7 +61,7 @@ const loadMonacoModules = async () => { // whose lazy tokens provider would overwrite our patched grammar on first use. // The grammar module is a private monaco internal (verified against monaco-editor // 0.52.2); the runtime guard below fails loudly if its export shape changes. - const jsonContribution = import("monaco-editor/esm/vs/language/json/monaco.contribution"); + const jsonContribution = import("monaco-editor/esm/vs/language/json/monaco.contribution.js"); const pythonGrammar = import("monaco-editor/esm/vs/basic-languages/python/python.js"); const [monaco, [editorWorkerUrl, jsonWorkerUrl], { conf: pythonConf, language: pythonLanguage }] = diff --git a/airflow-core/src/airflow/ui/src/components/MonacoEditor/pythonFStrings.test.ts b/airflow-core/src/airflow/ui/src/components/MonacoEditor/pythonFStrings.test.ts index 0fb4184a66029..1e69373e0282e 100644 --- a/airflow-core/src/airflow/ui/src/components/MonacoEditor/pythonFStrings.test.ts +++ b/airflow-core/src/airflow/ui/src/components/MonacoEditor/pythonFStrings.test.ts @@ -127,7 +127,7 @@ describe("patchPythonFStrings (tokenized)", () => { let singleLineTokens: Array<{ offset: number; type: string }> = []; beforeAll(async () => { - const monaco = await import("monaco-editor/esm/vs/editor/editor.api"); + const monaco = await import("monaco-editor/esm/vs/editor/editor.api.js"); const { conf, language } = await import("monaco-editor/esm/vs/basic-languages/python/python.js"); monaco.languages.register({ id: "python" }); diff --git a/airflow-core/src/airflow/ui/src/components/TaskInstanceTooltip.tsx b/airflow-core/src/airflow/ui/src/components/TaskInstanceTooltip.tsx index a0534f2c25fad..cb4ddee6cffb2 100644 --- a/airflow-core/src/airflow/ui/src/components/TaskInstanceTooltip.tsx +++ b/airflow-core/src/airflow/ui/src/components/TaskInstanceTooltip.tsx @@ -37,9 +37,7 @@ type LightGridTaskInstanceSummaryWithWhen = { type Props = { readonly runId?: string | null; readonly taskInstance?: - | LightGridTaskInstanceSummaryWithWhen - | TaskInstanceHistoryResponse - | TaskInstanceResponse; + LightGridTaskInstanceSummaryWithWhen | TaskInstanceHistoryResponse | TaskInstanceResponse; readonly tooltip?: string | null; } & Omit; diff --git a/airflow-core/src/airflow/ui/src/context/colorMode/useMonacoTheme.test.ts b/airflow-core/src/airflow/ui/src/context/colorMode/useMonacoTheme.test.ts index ff6812b9a0ec0..58fe517262c98 100644 --- a/airflow-core/src/airflow/ui/src/context/colorMode/useMonacoTheme.test.ts +++ b/airflow-core/src/airflow/ui/src/context/colorMode/useMonacoTheme.test.ts @@ -16,10 +16,11 @@ * specific language governing permissions and limitations * under the License. */ -import type { Monaco } from "@monaco-editor/react"; import { renderHook } from "@testing-library/react"; import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; +import type { Monaco } from "./useMonacoTheme"; + // `useColorMode` is the only dependency of the hook we want to test. We mock // it with a mutable return so individual tests can drive light/dark behaviour. const colorModeMock = vi.fn<() => { colorMode: "dark" | "light" | undefined }>(); diff --git a/airflow-core/src/airflow/ui/src/context/colorMode/useMonacoTheme.ts b/airflow-core/src/airflow/ui/src/context/colorMode/useMonacoTheme.ts index 6ecb1f8b89050..a3d0bb23b3ce8 100644 --- a/airflow-core/src/airflow/ui/src/context/colorMode/useMonacoTheme.ts +++ b/airflow-core/src/airflow/ui/src/context/colorMode/useMonacoTheme.ts @@ -16,11 +16,17 @@ * specific language governing permissions and limitations * under the License. */ -import type { Monaco } from "@monaco-editor/react"; +import type { useMonaco } from "@monaco-editor/react"; import { formatHex, parse } from "culori"; import { useColorMode } from "./useColorMode"; +// @monaco-editor/react's exported `Monaco` type aliases an extension-less deep +// monaco-editor import that monaco 0.55's package `exports` map no longer resolves, +// collapsing it to `any`. Derive it from `useMonaco`'s return type instead, which +// resolves through monaco's proper package entry point. +export type Monaco = NonNullable>; + const LIGHT_THEME_NAME = "airflow-light"; const DARK_THEME_NAME = "airflow-dark"; diff --git a/airflow-core/src/airflow/ui/src/pages/Dag/Calendar/Calendar.tsx b/airflow-core/src/airflow/ui/src/pages/Dag/Calendar/Calendar.tsx index c8ed293a62f58..993f980dc7c5e 100644 --- a/airflow-core/src/airflow/ui/src/pages/Dag/Calendar/Calendar.tsx +++ b/airflow-core/src/airflow/ui/src/pages/Dag/Calendar/Calendar.tsx @@ -164,9 +164,9 @@ export const Calendar = () => { fontWeight="bold" minWidth="120px" onClick={() => { - if ( - !(selectedDate.isSame(currentDate, "month") && selectedDate.isSame(currentDate, "year")) - ) { + if (!( + selectedDate.isSame(currentDate, "month") && selectedDate.isSame(currentDate, "year") + )) { setSelectedDate(currentDate.startOf("month")); } }} diff --git a/airflow-core/src/airflow/ui/src/pages/Dag/DagHeader.test.tsx b/airflow-core/src/airflow/ui/src/pages/Dag/DagHeader.test.tsx index faf1d1bc4ba8b..b1e9f1ecb486b 100644 --- a/airflow-core/src/airflow/ui/src/pages/Dag/DagHeader.test.tsx +++ b/airflow-core/src/airflow/ui/src/pages/Dag/DagHeader.test.tsx @@ -18,7 +18,7 @@ */ import "@testing-library/jest-dom"; import { render, screen, waitFor } from "@testing-library/react"; -import { setupServer, type SetupServerApi } from "msw/node"; +import { setupServer, type SetupServer } from "msw/node"; import { afterEach, describe, it, expect, beforeAll, afterAll } from "vitest"; import type { DAGDetailsResponse } from "openapi/requests/types.gen"; @@ -28,7 +28,7 @@ import { Wrapper } from "src/utils/Wrapper"; import { Header } from "./Header"; -let server: SetupServerApi; +let server: SetupServer; beforeAll(() => { server = setupServer(...handlers); diff --git a/airflow-core/src/airflow/ui/src/pages/ReactPlugin.tsx b/airflow-core/src/airflow/ui/src/pages/ReactPlugin.tsx index 25c33080b35f2..d78198be9ea81 100644 --- a/airflow-core/src/airflow/ui/src/pages/ReactPlugin.tsx +++ b/airflow-core/src/airflow/ui/src/pages/ReactPlugin.tsx @@ -40,8 +40,7 @@ const loadPlugin = (reactApp: ReactAppResponse): Promise<{ default: PluginCompon // Store components in globalThis[reactApp.name] to avoid conflicts with the shared globalThis.AirflowPlugin // global variable. let pluginComponent = (globalThis as Record)[reactApp.name] as - | PluginComponentType - | undefined; + PluginComponentType | undefined; if (pluginComponent === undefined) { pluginComponent = (globalThis as Record).AirflowPlugin as PluginComponentType; diff --git a/airflow-core/src/airflow/ui/src/pages/Variables/ImportVariablesForm.tsx b/airflow-core/src/airflow/ui/src/pages/Variables/ImportVariablesForm.tsx index 3f599ef619dbd..dbad73c788bda 100644 --- a/airflow-core/src/airflow/ui/src/pages/Variables/ImportVariablesForm.tsx +++ b/airflow-core/src/airflow/ui/src/pages/Variables/ImportVariablesForm.tsx @@ -156,7 +156,8 @@ const ImportVariablesForm = ({ onClose }: ImportVariablesFormProps) => { {isParsing ? (
- Parsing file... + {" "} + {translate("variables.import.parsingFile")}
) : undefined} diff --git a/airflow-core/src/airflow/ui/src/queries/useGridTISummaries.ts b/airflow-core/src/airflow/ui/src/queries/useGridTISummaries.ts index 7c350440e66a6..1ec67127b3f02 100644 --- a/airflow-core/src/airflow/ui/src/queries/useGridTISummaries.ts +++ b/airflow-core/src/airflow/ui/src/queries/useGridTISummaries.ts @@ -168,6 +168,13 @@ export const useGridTiSummariesStream = ({ setTimeout(cb, 0); }; + const runScheduledRefresh = () => { + if (isMounted) { + setRefreshTick((tick) => tick + 1); + } + scheduleScheduled = false; + }; + const unsubscribe = queryClient.getQueryCache().subscribe((event) => { const [firstKey] = event.query.queryKey as Array; @@ -180,12 +187,7 @@ export const useGridTiSummariesStream = ({ // Coalesce: multiple invalidations in the same execution tick only trigger one re-stream. if (!scheduleScheduled) { scheduleScheduled = true; - schedule(() => { - if (isMounted) { - setRefreshTick((tick) => tick + 1); - } - scheduleScheduled = false; - }); + schedule(runScheduledRefresh); } } }); diff --git a/airflow-core/src/airflow/ui/testsSetup.ts b/airflow-core/src/airflow/ui/testsSetup.ts index bbb226c7c91c4..32d59922d9963 100644 --- a/airflow-core/src/airflow/ui/testsSetup.ts +++ b/airflow-core/src/airflow/ui/testsSetup.ts @@ -19,7 +19,7 @@ import "@testing-library/jest-dom/vitest"; import { cleanup } from "@testing-library/react"; import type { HttpHandler } from "msw"; -import { setupServer, type SetupServerApi } from "msw/node"; +import { setupServer, type SetupServer } from "msw/node"; import { beforeEach, beforeAll, afterAll, afterEach, vi } from "vitest"; import { handlers } from "src/mocks/handlers"; @@ -71,7 +71,7 @@ vi.mock("chart.js", () => ({ Tooltip: vi.fn(), })); -let server: SetupServerApi; +let server: SetupServer; beforeAll(() => { server = setupServer(...(handlers as Array)); From 7d2cdceea5d5c4681e5c126dc66593e2ff11999d Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Mon, 6 Jul 2026 22:04:32 +0200 Subject: [PATCH 037/297] [v3-3-test] Allow manual triggering of constraints refresh workflow (#69457) (#69461) Constraints in the constraints-* branches only refresh automatically when uv.lock changes on a stable branch. After a providers release there is no such push, so refreshing constraints - for example to pick up newly released providers before promoting an RC - required running the breeze constraint-generation commands locally. Add a workflow_dispatch trigger to the update-constraints workflow. The manual run is launched from main and takes a `ref` input (branch, tag or commit hash) selecting the sources to refresh constraints from; the target constraints-X-Y branch is derived from that ref's branch_defaults.py. This avoids cherry-picking the workflow to vX-Y-test / vX-Y-stable, which matters because those branches diverge while RCs are being voted on. A toggle re-resolves to the newest matching dependencies from PyPI (default on). The ref is threaded through the reusable ci-image-build and generate-constraints workflows via a new optional checkout-ref input (backward compatible for existing callers). Because the RC constraints are frozen when the RC is cut, start-release now asks whether to base the final constraints- tag on the latest constraints-X-Y branch tip (when refreshed after the last RC) instead of the RC tag. The manual procedure and release-guide note are documented accordingly. (cherry picked from commit 271604fa772551eee6fa67f834908fddff325cbc) Co-authored-by: Jarek Potiuk --- .github/workflows/ci-image-build.yml | 6 ++ .github/workflows/generate-constraints.yml | 8 +- .../workflows/update-constraints-on-push.yml | 76 +++++++++++++++---- ..._GENERATING_IMAGE_CACHE_AND_CONSTRAINTS.md | 49 ++++++++++++ dev/README_RELEASE_AIRFLOW.md | 10 +++ .../commands/release_command.py | 21 ++++- dev/breeze/tests/test_release_command.py | 52 +++++++++++++ 7 files changed, 204 insertions(+), 18 deletions(-) diff --git a/.github/workflows/ci-image-build.yml b/.github/workflows/ci-image-build.yml index 7f1fa61fe609e..980c9c7a22399 100644 --- a/.github/workflows/ci-image-build.yml +++ b/.github/workflows/ci-image-build.yml @@ -29,6 +29,11 @@ on: # yamllint disable-line rule:truthy required: false default: "" type: string + checkout-ref: + description: "Commit-ish to checkout sources from (empty = workflow ref)." + required: false + default: "" + type: string pull-request-target: description: "Whether we are running this from pull-request-target workflow (true/false)" required: false @@ -120,6 +125,7 @@ jobs: - name: "Checkout target branch" uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 with: + ref: ${{ inputs.checkout-ref }} persist-credentials: false - name: "Free up disk space" shell: bash diff --git a/.github/workflows/generate-constraints.yml b/.github/workflows/generate-constraints.yml index 55604039f7734..2a8d8f4f72dd8 100644 --- a/.github/workflows/generate-constraints.yml +++ b/.github/workflows/generate-constraints.yml @@ -52,6 +52,11 @@ on: # yamllint disable-line rule:truthy description: "Whether to use uvloop (true/false)" required: true type: string + checkout-ref: + description: "Commit-ish to checkout sources from (empty = workflow ref)." + required: false + default: "" + type: string jobs: generate-constraints-matrix: permissions: @@ -76,9 +81,10 @@ jobs: - name: "Cleanup repo" shell: bash run: sudo rm -rf ${GITHUB_WORKSPACE}/* - - name: "Checkout ${{ github.ref }} ( ${{ github.sha }} )" + - name: "Checkout ${{ inputs.checkout-ref || github.ref }} ( ${{ github.sha }} )" uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 with: + ref: ${{ inputs.checkout-ref }} persist-credentials: false - name: "Install prek" uses: ./.github/actions/install-prek diff --git a/.github/workflows/update-constraints-on-push.yml b/.github/workflows/update-constraints-on-push.yml index 16460bd89cbb0..b65168c53dae1 100644 --- a/.github/workflows/update-constraints-on-push.yml +++ b/.github/workflows/update-constraints-on-push.yml @@ -16,7 +16,20 @@ # under the License. # --- -name: Update constraints on push for main (only when uv.lock changes) +name: Update constraints (on uv.lock push or manual dispatch) +# This workflow refreshes the pinned constraint files stored in the +# `constraints-*` branches. It runs automatically whenever `uv.lock` changes on +# `main` or a `vX-Y-test` branch, and can also be triggered manually via the +# "Run workflow" button (workflow_dispatch) - for example to pick up newly +# released providers/dependencies from PyPI just before promoting an RC. +# +# The manual run is always launched from `main` (so the workflow definition and +# `breeze` come from `main`), and the `ref` input selects the commit-ish +# (branch, tag or commit hash) whose sources the constraints are refreshed from. +# The `constraints-X-Y` branch to push to is derived from that ref's +# `dev/breeze/src/airflow_breeze/branch_defaults.py`, so no cherry-pick to the +# `vX-Y-test` / `vX-Y-stable` branch is needed. +# See dev/MANUALLY_GENERATING_IMAGE_CACHE_AND_CONSTRAINTS.md for details. on: # yamllint disable-line rule:truthy push: branches: @@ -24,11 +37,27 @@ on: # yamllint disable-line rule:truthy - v[0-9]+-[0-9]+-test paths: - 'uv.lock' + workflow_dispatch: + inputs: + ref: + description: >- + Commit-ish to refresh constraints from (branch, tag or commit hash), + e.g. `v3-3-test`, `v3-3-stable`, `constraints-3-3` or a tag/hash. + The matching `constraints-X-Y` branch is derived from that ref. + required: true + type: string + upgrade-to-newer-dependencies: + description: >- + Re-resolve to the newest matching dependencies (picks up newly + released providers/dependencies from PyPI). Leave enabled when + refreshing constraints before promoting an RC. + type: boolean + default: true permissions: contents: read concurrency: - group: ${{ github.workflow }}-${{ github.ref }} + group: ${{ github.workflow }}-${{ github.ref }}-${{ inputs.ref }} cancel-in-progress: true env: GITHUB_REPOSITORY: ${{ github.repository }} @@ -39,6 +68,25 @@ jobs: build-info: name: "Build info" runs-on: ["ubuntu-22.04"] + # Automatic uv.lock-push runs are unrestricted; manual (workflow_dispatch) + # runs are limited to release managers (same allowlist as the prod image + # release workflow) because they push to the protected constraints-* branches. + if: >- + github.event_name != 'workflow_dispatch' || + contains(fromJSON('[ + "ashb", + "bugraoz93", + "eladkal", + "ephraimbuddy", + "jedcunningham", + "jscheffl", + "kaxil", + "pierrejeambrun", + "potiuk", + "utkarsharma2", + "vincbeck", + "vatsrahul1001", + ]'), github.event.sender.login) outputs: default-branch: ${{ steps.selective-checks.outputs.default-branch }} default-constraints-branch: ${{ steps.selective-checks.outputs.default-constraints-branch }} @@ -49,14 +97,10 @@ jobs: - name: "Cleanup repo" shell: bash run: sudo rm -rf ${GITHUB_WORKSPACE}/* - - name: "Checkout ${{ github.ref }} ( ${{ github.sha }} )" - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 - with: - persist-credentials: false - - name: Fetch incoming commit ${{ github.sha }} with its parent + - name: "Checkout ${{ inputs.ref || github.ref }} ( ${{ inputs.ref || github.sha }} )" uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 with: - ref: ${{ github.sha }} + ref: ${{ inputs.ref || github.sha }} fetch-depth: 2 persist-credentials: false - name: "Install Breeze" @@ -75,7 +119,7 @@ jobs: id: selective-checks env: PR_LABELS: "[]" - COMMIT_REF: "${{ github.sha }}" + COMMIT_REF: "${{ inputs.ref || github.sha }}" VERBOSE: "false" GITHUB_CONTEXT_INPUT: "${{ runner.temp }}/github_context.json" run: breeze ci selective-check 2>> ${GITHUB_OUTPUT} @@ -96,8 +140,11 @@ jobs: python-versions: ${{ needs.build-info.outputs.python-versions }} branch: ${{ needs.build-info.outputs.default-branch }} constraints-branch: ${{ needs.build-info.outputs.default-constraints-branch }} + checkout-ref: ${{ inputs.ref }} use-uv: "true" - upgrade-to-newer-dependencies: "false" + upgrade-to-newer-dependencies: >- + ${{ github.event_name == 'workflow_dispatch' + && inputs.upgrade-to-newer-dependencies && 'true' || 'false' }} docker-cache: "registry" disable-airflow-repo-cache: "false" @@ -113,6 +160,7 @@ jobs: generate-pypi-constraints: "true" generate-no-providers-constraints: "true" debug-resources: "false" + checkout-ref: ${{ inputs.ref }} use-uv: "true" update-constraints: @@ -125,6 +173,7 @@ jobs: packages: read env: PYTHON_VERSIONS: ${{ needs.build-info.outputs.python-versions-list-as-string }} + CONSTRAINTS_BRANCH: ${{ needs.build-info.outputs.default-constraints-branch }} steps: - name: "Cleanup repo" shell: bash @@ -133,14 +182,11 @@ jobs: uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 with: persist-credentials: false - - name: "Set constraints branch name" - id: constraints-branch - run: ./scripts/ci/constraints/ci_branch_constraints.sh >> ${GITHUB_OUTPUT} - - name: Checkout ${{ steps.constraints-branch.outputs.branch }} + - name: Checkout ${{ env.CONSTRAINTS_BRANCH }} uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 with: path: "constraints" - ref: ${{ steps.constraints-branch.outputs.branch }} + ref: ${{ env.CONSTRAINTS_BRANCH }} persist-credentials: true fetch-depth: 0 - name: "Download constraints from the generate-constraints job" diff --git a/dev/MANUALLY_GENERATING_IMAGE_CACHE_AND_CONSTRAINTS.md b/dev/MANUALLY_GENERATING_IMAGE_CACHE_AND_CONSTRAINTS.md index 68104fee02d33..4a794333ab664 100644 --- a/dev/MANUALLY_GENERATING_IMAGE_CACHE_AND_CONSTRAINTS.md +++ b/dev/MANUALLY_GENERATING_IMAGE_CACHE_AND_CONSTRAINTS.md @@ -31,6 +31,7 @@ - [What the command does](#what-the-command-does) - [Manually generating constraint files](#manually-generating-constraint-files) - [Why we need to generate constraint files manually](#why-we-need-to-generate-constraint-files-manually) + - [How to refresh constraints via the CI workflow (recommended)](#how-to-refresh-constraints-via-the-ci-workflow-recommended) - [How to generate constraint files](#how-to-generate-constraint-files) - [Is it safe to generate constraints manually?](#is-it-safe-to-generate-constraints-manually) - [Manually updating already tagged constraint files](#manually-updating-already-tagged-constraint-files) @@ -213,6 +214,54 @@ with tests, but we KNOW that the tip of the branch is good and we want to releas we want to move the PRs of contributors to start using the new constraints. This should be done with caution and you need to be sure what you are doing, but you can always do it manually if you want. +## How to refresh constraints via the CI workflow (recommended) + +The easiest way to refresh the constraints - for example to pick up newly released +providers/dependencies from PyPI just before promoting an RC - is to trigger the +[`Update constraints`](../.github/workflows/update-constraints-on-push.yml) workflow manually +instead of running the `breeze` commands locally. The workflow runs exactly the same steps +that run automatically when `uv.lock` changes, builds the CI images, generates all constraint +flavours and commits/pushes them to the matching `constraints-*` branch. + +The manual run is always launched from `main` and takes a `ref` input that selects the +commit-ish (branch, tag or commit hash) whose sources the constraints are refreshed from. You +do **not** select the release branch in GitHub's branch dropdown, and you do **not** need to +cherry-pick anything to the `vX-Y-test` / `vX-Y-stable` branch first - the workflow definition +and `breeze` come from `main`, while the sources come from the `ref` you pass. The only +requirement is that the `breeze` constraint-generation commands work against that `ref` (they +are stable across recent branches). + +Manual runs are restricted to release managers (the same allowlist as the prod image release +workflow); the automatic `uv.lock`-push runs are not restricted. + +To run it: + +1. Go to the [`Update constraints`](https://github.com/apache/airflow/actions/workflows/update-constraints-on-push.yml) + workflow in the Actions tab. +2. Click **Run workflow** and keep the branch set to `main` (this is where the workflow runs + from - it is not the branch whose constraints get refreshed). +3. In the **ref** field, enter the commit-ish to refresh constraints from - for example + `v3-3-test`, `v3-3-stable`, `constraints-3-3`, an RC tag, or a commit hash. The matching + `constraints-X-Y` branch to push to is derived automatically from that ref's + `dev/breeze/src/airflow_breeze/branch_defaults.py`, so pointing at anything on the 3.3 line + refreshes `constraints-3-3`. +4. Keep **Re-resolve to the newest matching dependencies** enabled (the default) so the run + picks up the latest released providers/dependencies from PyPI. Disable it only if you want + to regenerate constraints strictly from that ref's `uv.lock` without upgrading. +5. Once the run finishes, verify the new commit on the matching constraints branch + (for example `constraints-3-3` for a 3.3 refresh): + + https://github.com/apache/airflow/commits/constraints--/ + +> [!NOTE] +> Because `v3-3-test` and `v3-3-stable` can diverge while RCs are being voted on (fixes are +> cherry-picked to `v3-3-test`), be deliberate about which `ref` you refresh from. Refresh from +> the ref that matches the artifacts you are about to promote - typically `v3-3-stable` (or the +> RC tag) for a release, not `v3-3-test`. + +If you cannot or do not want to use the workflow, you can still generate the constraints +locally with the `breeze` commands below. + ## How to generate constraint files ```bash diff --git a/dev/README_RELEASE_AIRFLOW.md b/dev/README_RELEASE_AIRFLOW.md index f7d8dc7961d55..7e6005e75bba0 100644 --- a/dev/README_RELEASE_AIRFLOW.md +++ b/dev/README_RELEASE_AIRFLOW.md @@ -1372,6 +1372,16 @@ breeze release-management start-release \ Note: The `--task-sdk-version` parameter is optional. If you are releasing Airflow without a corresponding Task SDK release, you can omit this parameter. +Note: When it reaches the constraints step, `start-release` asks whether to base the final +`constraints-${VERSION}` tag on the latest `constraints-X-Y` branch tip instead of the RC +constraints tag. The RC constraints are frozen when the RC is cut, so if any providers were +released (or constraints were otherwise refreshed - see +[MANUALLY_GENERATING_IMAGE_CACHE_AND_CONSTRAINTS.md](MANUALLY_GENERATING_IMAGE_CACHE_AND_CONSTRAINTS.md)) +after the last RC and you want the released constraints to reflect that, answer **yes** to tag the +`constraints-X-Y` branch tip. Otherwise (the default) the final tag matches the RC exactly. If you +do refresh, run the `Update constraints` workflow from `main` with `ref` set to the ref you are +releasing (typically `v3-*-stable`) **before** running `start-release`. + 4. Make sure to update Airflow version in ``v3-*-test`` branch after cherry-picking to X.Y.1 in ``airflow/__init__.py`` diff --git a/dev/breeze/src/airflow_breeze/commands/release_command.py b/dev/breeze/src/airflow_breeze/commands/release_command.py index 5b183e765d965..1b93f9c28ac43 100644 --- a/dev/breeze/src/airflow_breeze/commands/release_command.py +++ b/dev/breeze/src/airflow_breeze/commands/release_command.py @@ -343,10 +343,27 @@ def upload_to_pypi(version, task_sdk_version=None): ) +def get_constraints_branch_for_version(version: str) -> str: + major, minor = version.split(".")[:2] + return f"constraints-{major}-{minor}" + + def retag_constraints(release_candidate, version): - if confirm_action(f"Retag constraints for {release_candidate} as {version}?"): + # By default the final ``constraints-`` tag is created from the RC constraints tag. + # If the constraints were refreshed after the last RC (e.g. to pick up newly released + # providers - see dev/MANUALLY_GENERATING_IMAGE_CACHE_AND_CONSTRAINTS.md) the tip of the + # ``constraints-X-Y`` branch is newer than the RC tag and should be tagged instead. + constraints_branch = get_constraints_branch_for_version(version) + source_ref = f"constraints-{release_candidate}" + if confirm_action( + f"Base the final constraints on the latest '{constraints_branch}' branch tip instead of the " + f"'{source_ref}' tag? Choose yes if you refreshed constraints after {release_candidate}." + ): + run_command(["git", "fetch", "origin", constraints_branch], check=True) + source_ref = f"origin/{constraints_branch}" + if confirm_action(f"Retag constraints from {source_ref} as {version}?"): run_command( - ["git", "checkout", f"constraints-{release_candidate}"], + ["git", "checkout", source_ref], check=True, ) run_command( diff --git a/dev/breeze/tests/test_release_command.py b/dev/breeze/tests/test_release_command.py index 2745a8387eb05..1e023e6bf5bfd 100644 --- a/dev/breeze/tests/test_release_command.py +++ b/dev/breeze/tests/test_release_command.py @@ -538,3 +538,55 @@ def fake_getcwd() -> str: assert "task-sdk" not in " ".join(console_messages).lower() assert run_command_calls == [] assert chdir_calls == [svn_release_repo, "/original/dir"] + + +@pytest.mark.parametrize( + ("version", "expected_branch"), + [ + ("3.3.0", "constraints-3-3"), + ("3.0.5", "constraints-3-0"), + ("3.12.10", "constraints-3-12"), + ], +) +def test_get_constraints_branch_for_version(release_cmd, version, expected_branch): + assert release_cmd.get_constraints_branch_for_version(version) == expected_branch + + +def test_retag_constraints_from_rc_tag_by_default(monkeypatch, release_cmd): + run_command_calls: list[list[str]] = [] + + def fake_confirm_action(prompt: str, **_kwargs) -> bool: + # Decline basing on the branch tip; accept the retag and the push. + return not prompt.startswith("Base the final constraints on the latest") + + monkeypatch.setattr(release_cmd, "confirm_action", fake_confirm_action) + monkeypatch.setattr(release_cmd, "run_command", lambda cmd, **_kwargs: run_command_calls.append(cmd)) + + release_cmd.retag_constraints("3.3.0rc2", "3.3.0") + + assert ["git", "checkout", "constraints-3.3.0rc2"] in run_command_calls + assert ["git", "tag", "-s", "constraints-3.3.0", "-m", "Constraints for Apache Airflow 3.3.0"] in ( + run_command_calls + ) + assert ["git", "push", "origin", "tag", "constraints-3.3.0"] in run_command_calls + # The branch tip is not fetched when basing on the RC tag. + assert not any(cmd[:2] == ["git", "fetch"] for cmd in run_command_calls) + + +def test_retag_constraints_from_branch_tip_when_confirmed(monkeypatch, release_cmd): + run_command_calls: list[list[str]] = [] + + def fake_confirm_action(prompt: str, **_kwargs) -> bool: + return True + + monkeypatch.setattr(release_cmd, "confirm_action", fake_confirm_action) + monkeypatch.setattr(release_cmd, "run_command", lambda cmd, **_kwargs: run_command_calls.append(cmd)) + + release_cmd.retag_constraints("3.3.0rc2", "3.3.0") + + assert ["git", "fetch", "origin", "constraints-3-3"] in run_command_calls + assert ["git", "checkout", "origin/constraints-3-3"] in run_command_calls + assert ["git", "checkout", "constraints-3.3.0rc2"] not in run_command_calls + assert ["git", "tag", "-s", "constraints-3.3.0", "-m", "Constraints for Apache Airflow 3.3.0"] in ( + run_command_calls + ) From 78abd3044654d44d711eb61e475029fd1b1ccc23 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Mon, 6 Jul 2026 23:09:55 +0200 Subject: [PATCH 038/297] [v3-3-test] Reject reserved XCom serialization keys submitted as JSON string literals (#69378) (#69462) The XCom create/update payload validator _check_forbidden_xcom_keys recursively rejects reserved serialization keys (__classname__, __type, __var, ...) in dict/list values. Its _walk helper descended dicts, lists and tuples but not str, so a value submitted as a JSON string literal (e.g. json.dumps({"__classname__": ...})) was stored verbatim and re-parsed into a dict on a ?deserialize=true read, slipping past the filter. Extend _walk to json.loads a string value and inspect the decoded dict/list the same way the read path does. Strings that are not JSON, or that decode to non-container values, are unchanged and still accepted. (cherry picked from commit aae7c8f42789965940c0bf0fe9da25299d03521a) Generated-by: Claude Opus 4.8 (1M context) following the guidelines at https: //github.com/apache/airflow/blob/main/contributing-docs/05_pull_requests.rst#gen-ai-assisted-contributions Co-authored-by: Jarek Potiuk --- .../api_fastapi/core_api/datamodels/xcom.py | 14 +++++ .../core_api/routes/public/test_xcom.py | 59 +++++++++++++++++++ 2 files changed, 73 insertions(+) diff --git a/airflow-core/src/airflow/api_fastapi/core_api/datamodels/xcom.py b/airflow-core/src/airflow/api_fastapi/core_api/datamodels/xcom.py index b42cc176f01c7..e6987502ad321 100644 --- a/airflow-core/src/airflow/api_fastapi/core_api/datamodels/xcom.py +++ b/airflow-core/src/airflow/api_fastapi/core_api/datamodels/xcom.py @@ -16,6 +16,7 @@ # under the License. from __future__ import annotations +import json from collections.abc import Iterable from datetime import datetime from typing import Any @@ -88,6 +89,19 @@ def _check_forbidden_xcom_keys(value: Any) -> Any: from airflow._shared.serialization import FORBIDDEN_XCOM_KEYS def _walk(obj: Any, path: str = "value") -> None: + if isinstance(obj, str): + # A value submitted as a JSON string literal (e.g. ``json.dumps({...})``) + # is stored verbatim and re-parsed into a dict/list on a + # ``deserialize=true`` read, which would otherwise smuggle reserved keys + # past the dict/list checks below. Re-parse and inspect the decoded + # structure the same way the read path does. + try: + decoded = json.loads(obj) + except (ValueError, TypeError): + return + if isinstance(decoded, (dict, list)): + _walk(decoded, path) + return if isinstance(obj, dict): found = FORBIDDEN_XCOM_KEYS & obj.keys() if found: diff --git a/airflow-core/tests/unit/api_fastapi/core_api/routes/public/test_xcom.py b/airflow-core/tests/unit/api_fastapi/core_api/routes/public/test_xcom.py index 81723e05a8e42..64ce784775cf2 100644 --- a/airflow-core/tests/unit/api_fastapi/core_api/routes/public/test_xcom.py +++ b/airflow-core/tests/unit/api_fastapi/core_api/routes/public/test_xcom.py @@ -746,6 +746,52 @@ def test_create_xcom_entry_blocks_forbidden_keys(self, test_client, key, value): assert "reserved serialization keys" in detail assert key in detail + @pytest.mark.parametrize( + "value", + [ + pytest.param( + json.dumps({"__classname__": "airflow.sdk.definitions.connection.Connection"}), + id="classname-in-json-string", + ), + pytest.param( + json.dumps( + {"nested": {"__type": "airflow.sdk.definitions.connection.Connection", "__var": {}}} + ), + id="nested-forbidden-in-json-string", + ), + ], + ) + def test_create_xcom_entry_blocks_forbidden_keys_in_json_string(self, test_client, value): + """A forbidden payload submitted as a JSON string literal is blocked too. + + ``_check_forbidden_xcom_keys._walk`` previously descended dict/list/tuple but not + ``str``, so a value like ``json.dumps({"__classname__": ...})`` slipped past the + filter and was reconstructed into a dict on a ``deserialize=true`` read. + """ + response = test_client.post( + f"/dags/{TEST_DAG_ID}/dagRuns/{run_id}/taskInstances/{TEST_TASK_ID}/xcomEntries", + json={"key": "test_key", "value": value, "map_index": -1}, + ) + assert response.status_code == 422 + assert "reserved serialization keys" in str(response.json()["detail"]) + + @pytest.mark.parametrize( + "value", + [ + pytest.param("just a plain string", id="plain-string"), + pytest.param(json.dumps({"safe": "data", "count": 3}), id="benign-json-object-string"), + pytest.param(json.dumps(["a", "b"]), id="benign-json-array-string"), + pytest.param('{"not valid json', id="not-json"), + ], + ) + def test_create_xcom_entry_allows_benign_string_values(self, test_client, value): + """String values that do not decode to a reserved-key structure stay accepted.""" + response = test_client.post( + f"/dags/{TEST_DAG_ID}/dagRuns/{run_id}/taskInstances/{TEST_TASK_ID}/xcomEntries", + json={"key": "test_key", "value": value, "map_index": -1}, + ) + assert response.status_code != 422 + class TestDeleteXComEntry(TestXComEndpoint): def test_delete_xcom_entry(self, test_client, session): @@ -894,6 +940,19 @@ def test_patch_xcom_entry_blocks_forbidden_keys(self, test_client, key, value): assert "reserved serialization keys" in detail assert key in detail + def test_patch_xcom_entry_blocks_forbidden_keys_in_json_string(self, test_client): + """A forbidden payload submitted as a JSON string literal is blocked on PATCH too.""" + self._create_xcom(TEST_XCOM_KEY, TEST_XCOM_VALUE) + response = test_client.patch( + f"/dags/{TEST_DAG_ID}/dagRuns/{run_id}/taskInstances/{TEST_TASK_ID}/xcomEntries/{TEST_XCOM_KEY}", + json={ + "value": json.dumps({"__classname__": "airflow.sdk.definitions.connection.Connection"}), + "map_index": -1, + }, + ) + assert response.status_code == 422 + assert "reserved serialization keys" in str(response.json()["detail"]) + def test_patch_xcom_preserves_int_type(self, test_client, session): """Test scenario described in #59032: if existing XCom value type is int, after patching with different value, it should still be int in the API response. From de05b97a4bcfc855edff3ad4b4f7798d21757091 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Tue, 7 Jul 2026 00:28:46 +0200 Subject: [PATCH 039/297] [v3-3-test] Add prek hook to keep the Go toolchain version in sync (#69338) (#69498) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The Go toolchain version is pinned in eight places (go-sdk/go.mod, the go_example module, the setup-go step in both CI workflow copies, the golang alpine builder image used by the e2e conftest and by breeze, and the default_language_version.golang in the top-level and go-sdk prek configs). They have no cross-file include, so a dependency bump that raises the go directive in go-sdk/go.mod leaves the rest behind and the drift only surfaces as an opaque "go.mod requires go >= " failure deep in CI — as happened in PR #69214. Add a prek hook that treats go-sdk/go.mod as the single source of truth and fails when any other site disagrees at major.minor granularity, pointing directly at the drifting file. (cherry picked from commit 0a48feeb1d58f2cb608e9b2258f83c2df1e8b06a) Co-authored-by: Jarek Potiuk --- .pre-commit-config.yaml | 15 ++ scripts/ci/prek/check_go_version_in_sync.py | 197 ++++++++++++++++++ .../ci/prek/test_check_go_version_in_sync.py | 130 ++++++++++++ 3 files changed, 342 insertions(+) create mode 100755 scripts/ci/prek/check_go_version_in_sync.py create mode 100644 scripts/tests/ci/prek/test_check_go_version_in_sync.py diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 95bb77f184c62..3998ee80bb5b4 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -269,6 +269,21 @@ repos: ^Dockerfile\.ci$ pass_filenames: false require_serial: true + - id: check-go-version-in-sync + name: Check Go toolchain version is consistent across build files + entry: ./scripts/ci/prek/check_go_version_in_sync.py + language: python + files: > + (?x) + ^go-sdk/go\.mod$| + ^kubernetes-tests/lang_sdk/go_example/go\.mod$| + ^\.github/workflows/ci-(arm|amd)\.yml$| + ^airflow-e2e-tests/tests/airflow_e2e_tests/constants\.py$| + ^dev/breeze/src/airflow_breeze/commands/kubernetes_commands\.py$| + ^go-sdk/\.pre-commit-config\.yaml$| + ^\.pre-commit-config\.yaml$ + pass_filenames: false + require_serial: true - id: check-partition-mapper-defaults-in-sync name: Check partition-mapper core/SDK sync (FanOutMapper table + SegmentWindow/FixedKeyMapper) entry: ./scripts/ci/prek/check_partition_mapper_defaults_in_sync.py diff --git a/scripts/ci/prek/check_go_version_in_sync.py b/scripts/ci/prek/check_go_version_in_sync.py new file mode 100755 index 0000000000000..250bb2246b251 --- /dev/null +++ b/scripts/ci/prek/check_go_version_in_sync.py @@ -0,0 +1,197 @@ +#!/usr/bin/env python3 +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. +""" +Keep the Go toolchain version in sync across every file that pins it. + +``golang.org/x/net`` (and other ``golang.org/x`` modules) periodically raise +their minimum Go version. When that happens a dependency bump changes the +``go`` directive in ``go-sdk/go.mod`` but leaves the *other* places that pin +the toolchain untouched — and CI then fails deep inside static checks and the +Go SDK e2e bundle build with ``go.mod requires go >= `` (this is exactly +what happened in PR #69214). Those pins have no cross-file include, so nothing +catches the drift until CI does. + +The single source of truth is the ``go`` directive in ``go-sdk/go.mod``. Every +other site below must agree with it at ``major.minor`` granularity (the minor +version is what selects the toolchain and the builder image; the go.mod patch +suffix is not meaningful for those): + +- ``go-sdk/go.mod`` -> ``go `` (SOURCE OF TRUTH) +- ``kubernetes-tests/lang_sdk/go_example/go.mod`` -> ``go `` (resolves the SDK via ``replace``) +- ``.github/workflows/ci-amd.yml`` -> Setup Go ``go-version: `` +- ``.github/workflows/ci-arm.yml`` -> Setup Go ``go-version: `` +- ``airflow-e2e-tests/tests/airflow_e2e_tests/constants.py`` -> ``GO_BUILDER_IMAGE`` default ``golang:-alpine`` +- ``dev/breeze/src/airflow_breeze/commands/kubernetes_commands.py`` -> ``LANG_SDK_GO_BUILDER_IMAGE`` default ``golang:-alpine`` +- ``go-sdk/.pre-commit-config.yaml`` -> ``default_language_version.golang`` (the toolchain prek uses for the SDK's ``language: golang`` hooks — this is the pin that broke static checks in #69214, not the setup-go one) +- ``.pre-commit-config.yaml`` -> ``default_language_version.golang`` (top-level default for any ``language: golang`` hook) + +When they disagree, bump the drifting sites to the source-of-truth minor +version. If you add a new place that pins the Go version, register it here too. + +Run from the repo root: + + uv run --project scripts python scripts/ci/prek/check_go_version_in_sync.py + +Exits 0 if every site agrees with the source of truth, 1 otherwise. +""" + +from __future__ import annotations + +import dataclasses +import pathlib +import re +import sys + +REPO_ROOT = pathlib.Path(__file__).resolve().parents[3] + +# The ``go`` directive: ``go 1.25`` or ``go 1.25.0``. +GO_MOD_DIRECTIVE = re.compile(r"^go\s+(\d+\.\d+(?:\.\d+)?)\s*$", re.MULTILINE) +# Setup Go step: ``go-version: 1.25`` (optionally quoted). +WORKFLOW_GO_VERSION = re.compile(r"^\s*go-version:\s*[\"']?(\d+\.\d+(?:\.\d+)?)[\"']?\s*$", re.MULTILINE) +# Builder image default: ``golang:1.25-alpine``. +GOLANG_BUILDER_IMAGE = re.compile(r"golang:(\d+\.\d+(?:\.\d+)?)-alpine") +# prek ``default_language_version`` entry: `` golang: 1.25.0``. +PREK_GOLANG_VERSION = re.compile(r"^\s*golang:\s*(\d+\.\d+(?:\.\d+)?)\s*$", re.MULTILINE) + + +@dataclasses.dataclass +class VersionSite: + """A single location that pins the Go toolchain version.""" + + label: str + path: pathlib.Path + pattern: re.Pattern[str] + is_source_of_truth: bool = False + + def extract(self) -> str | None: + """Return the captured version string, or ``None`` if the pattern is missing.""" + if not self.path.exists(): + return None + if m := self.pattern.search(self.path.read_text()): + return m.group(1) + return None + + +def major_minor(version: str) -> str: + """Reduce ``1.25`` / ``1.25.0`` to its ``major.minor`` (``1.25``).""" + parts = version.split(".") + return ".".join(parts[:2]) + + +def build_sites(repo_root: pathlib.Path) -> list[VersionSite]: + """Build the list of Go-version pin sites, resolved against ``repo_root``.""" + workflows = repo_root / ".github" / "workflows" + return [ + VersionSite( + label="go-sdk/go.mod (go directive)", + path=repo_root / "go-sdk" / "go.mod", + pattern=GO_MOD_DIRECTIVE, + is_source_of_truth=True, + ), + VersionSite( + label="kubernetes-tests/lang_sdk/go_example/go.mod (go directive)", + path=repo_root / "kubernetes-tests" / "lang_sdk" / "go_example" / "go.mod", + pattern=GO_MOD_DIRECTIVE, + ), + VersionSite( + label=".github/workflows/ci-amd.yml (Setup Go go-version)", + path=workflows / "ci-amd.yml", + pattern=WORKFLOW_GO_VERSION, + ), + VersionSite( + label=".github/workflows/ci-arm.yml (Setup Go go-version)", + path=workflows / "ci-arm.yml", + pattern=WORKFLOW_GO_VERSION, + ), + VersionSite( + label="airflow-e2e-tests/.../constants.py (GO_BUILDER_IMAGE)", + path=repo_root / "airflow-e2e-tests" / "tests" / "airflow_e2e_tests" / "constants.py", + pattern=GOLANG_BUILDER_IMAGE, + ), + VersionSite( + label="dev/breeze/.../kubernetes_commands.py (LANG_SDK_GO_BUILDER_IMAGE)", + path=repo_root + / "dev" + / "breeze" + / "src" + / "airflow_breeze" + / "commands" + / "kubernetes_commands.py", + pattern=GOLANG_BUILDER_IMAGE, + ), + VersionSite( + label="go-sdk/.pre-commit-config.yaml (default_language_version.golang)", + path=repo_root / "go-sdk" / ".pre-commit-config.yaml", + pattern=PREK_GOLANG_VERSION, + ), + VersionSite( + label=".pre-commit-config.yaml (default_language_version.golang)", + path=repo_root / ".pre-commit-config.yaml", + pattern=PREK_GOLANG_VERSION, + ), + ] + + +def check_sync(sites: list[VersionSite], repo_root: pathlib.Path) -> tuple[int, str]: + """Compare every site against the source of truth. Returns ``(exit_code, report)``.""" + results = [(site, site.extract()) for site in sites] + + if missing := [site for site, version in results if version is None]: + lines = [f"ERROR: Go version pin not found in {site.path.relative_to(repo_root)}" for site in missing] + lines += [f" (expected pattern {site.pattern.pattern!r})" for site in missing] + return 1, "\n".join(lines) + + source = next(site for site, _ in results if site.is_source_of_truth) + expected = major_minor(source.extract()) # type: ignore[arg-type] + + drifted = [(site, version) for site, version in results if major_minor(version) != expected] # type: ignore[arg-type] + if not drifted: + return 0, f"OK: Go version is consistently {expected} across all {len(sites)} pin sites." + + col = max(len(site.label) for site, _ in results) + lines = [ + f"ERROR: Go version drifted from the source of truth ({source.label} = {expected}).", + "", + f" {'PIN SITE':<{col}} VERSION", + ] + for site, version in results: + marker = ( + " <- source of truth" + if site.is_source_of_truth + else ( + " <- DRIFT" if major_minor(version) != expected else "" # type: ignore[arg-type] + ) + ) + lines.append(f" {site.label:<{col}} {version}{marker}") + lines += [ + "", + f"Bump the drifting site(s) to {expected} to match {source.label},", + "or, if you intentionally changed the source of truth, update the other pins to match.", + "After changing a go.mod, run its `go mod tidy` so go.sum stays consistent.", + ] + return 1, "\n".join(lines) + + +def main() -> int: + exit_code, report = check_sync(build_sites(REPO_ROOT), REPO_ROOT) + print(report) + return exit_code + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/scripts/tests/ci/prek/test_check_go_version_in_sync.py b/scripts/tests/ci/prek/test_check_go_version_in_sync.py new file mode 100644 index 0000000000000..8092f17e034a4 --- /dev/null +++ b/scripts/tests/ci/prek/test_check_go_version_in_sync.py @@ -0,0 +1,130 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. +from __future__ import annotations + +from pathlib import Path + +import pytest +from check_go_version_in_sync import build_sites, check_sync, major_minor + +# Default (all-consistent) version at every pin site. Individual tests override +# one entry to simulate drift. go.mod carries the patch suffix; the workflow and +# image sites carry only major.minor — the check must treat these as equal. +DEFAULT_VERSIONS = { + "go_sdk_mod": "1.25.0", + "go_example_mod": "1.25.0", + "ci_amd": "1.25", + "ci_arm": "1.25", + "constants": "1.25", + "kubernetes_commands": "1.25", + "go_sdk_prek": "1.25.0", + "root_prek": "1.25.0", +} + + +def _write_tree(root: Path, versions: dict[str, str]) -> None: + """Materialise a minimal repo tree with a Go version pin at each of the eight sites.""" + + def write(rel: str, content: str) -> None: + path = root / rel + path.parent.mkdir(parents=True, exist_ok=True) + path.write_text(content) + + write("go-sdk/go.mod", f"module github.com/apache/airflow/go-sdk\n\ngo {versions['go_sdk_mod']}\n") + write( + "kubernetes-tests/lang_sdk/go_example/go.mod", + f"module github.com/apache/airflow/kubernetes-tests/lang_sdk/go_example\n\n" + f"go {versions['go_example_mod']}\n", + ) + for site, rel in (("ci_amd", "ci-amd.yml"), ("ci_arm", "ci-arm.yml")): + write( + f".github/workflows/{rel}", + f" - name: Setup Go\n with:\n go-version: {versions[site]}\n", + ) + write( + "airflow-e2e-tests/tests/airflow_e2e_tests/constants.py", + f'GO_BUILDER_IMAGE = os.environ.get("GO_BUILDER_IMAGE", "golang:{versions["constants"]}-alpine")\n', + ) + write( + "dev/breeze/src/airflow_breeze/commands/kubernetes_commands.py", + f'LANG_SDK_GO_BUILDER_IMAGE = os.environ.get("GO_BUILDER_IMAGE", ' + f'"golang:{versions["kubernetes_commands"]}-alpine")\n', + ) + for site, rel in ( + ("go_sdk_prek", "go-sdk/.pre-commit-config.yaml"), + ("root_prek", ".pre-commit-config.yaml"), + ): + write( + rel, + f"default_language_version:\n python: python3\n node: 22.19.0\n golang: {versions[site]}\n", + ) + + +@pytest.mark.parametrize( + ("version", "expected"), + [("1.25", "1.25"), ("1.25.0", "1.25"), ("1.24.6", "1.24")], +) +def test_major_minor(version: str, expected: str): + assert major_minor(version) == expected + + +def test_all_sites_consistent(tmp_path: Path): + _write_tree(tmp_path, DEFAULT_VERSIONS) + exit_code, report = check_sync(build_sites(tmp_path), tmp_path) + assert exit_code == 0 + assert "consistently 1.25" in report + + +def test_go_mod_patch_suffix_matches_major_minor_pins(tmp_path: Path): + """go.mod may pin 1.25.0 while the workflow/image sites pin 1.25 — that is in sync.""" + _write_tree(tmp_path, {**DEFAULT_VERSIONS, "go_sdk_mod": "1.25.0", "ci_amd": "1.25"}) + exit_code, _ = check_sync(build_sites(tmp_path), tmp_path) + assert exit_code == 0 + + +@pytest.mark.parametrize( + "drifted_site", + ["go_example_mod", "ci_amd", "ci_arm", "constants", "kubernetes_commands", "go_sdk_prek", "root_prek"], +) +def test_drift_in_any_derived_site_is_flagged(tmp_path: Path, drifted_site: str): + """A single derived site left on the old minor version must fail the check.""" + _write_tree( + tmp_path, {**DEFAULT_VERSIONS, drifted_site: "1.24" if "mod" not in drifted_site else "1.24.6"} + ) + exit_code, report = check_sync(build_sites(tmp_path), tmp_path) + assert exit_code == 1 + assert "drifted from the source of truth" in report + assert "<- DRIFT" in report + + +def test_source_of_truth_bump_flags_all_stale_sites(tmp_path: Path): + """Bumping only go-sdk/go.mod (the source of truth) flags every other site — the #69214 scenario.""" + _write_tree(tmp_path, {**DEFAULT_VERSIONS, "go_sdk_mod": "1.26.0"}) + exit_code, report = check_sync(build_sites(tmp_path), tmp_path) + assert exit_code == 1 + # All seven non-source sites are still on the old version -> all flagged. + assert report.count("<- DRIFT") == 7 + assert "<- source of truth" in report + + +def test_missing_pin_is_reported(tmp_path: Path): + _write_tree(tmp_path, DEFAULT_VERSIONS) + (tmp_path / "go-sdk" / "go.mod").write_text("module github.com/apache/airflow/go-sdk\n") + exit_code, report = check_sync(build_sites(tmp_path), tmp_path) + assert exit_code == 1 + assert "Go version pin not found" in report + assert "go-sdk/go.mod" in report From 1d3018e0cdd37e877d2a57ecbfef59e8da1882b3 Mon Sep 17 00:00:00 2001 From: Wei Lee Date: Tue, 7 Jul 2026 20:06:49 +0800 Subject: [PATCH 040/297] Decide pod_template and image based on Coordinator for lang-SDK tasks on KubernetesExecutor (#68713) (#69536) Co-authored-by: Jason(Zhe-You) Liu <68415893+jason810496@users.noreply.github.com> --- .../executors/kubernetes_executor.py | 82 +++++++- .../executors/kubernetes_executor_types.py | 1 + .../executors/kubernetes_executor_utils.py | 3 +- .../executors/test_kubernetes_executor.py | 198 +++++++++++++++++- 4 files changed, 278 insertions(+), 6 deletions(-) diff --git a/providers/cncf/kubernetes/src/airflow/providers/cncf/kubernetes/executors/kubernetes_executor.py b/providers/cncf/kubernetes/src/airflow/providers/cncf/kubernetes/executors/kubernetes_executor.py index 4ddb189390b3c..154024add261d 100644 --- a/providers/cncf/kubernetes/src/airflow/providers/cncf/kubernetes/executors/kubernetes_executor.py +++ b/providers/cncf/kubernetes/src/airflow/providers/cncf/kubernetes/executors/kubernetes_executor.py @@ -198,6 +198,60 @@ def start(self) -> None: scheduler_job_id=self.scheduler_job_id, ) + def _coordinator_extra(self, queue: str | None) -> dict[str, Any] | None: + """ + Return the ``extra`` mapping a coordinator declares for *queue*, if any. + + Read from the coordinator's declarative ``[sdk]`` config without importing + or instantiating the coordinator. The coordinator manager only exists on + Airflow 3.3+; on older Task SDKs the import fails and we fall back to no + extra. A malformed ``[sdk] coordinators`` / ``queue_to_coordinator`` config + must not crash the scheduler on this first lookup either, so an invalid + config also falls back to no extra. The exception types are imported from + ``airflow.sdk`` so they match whatever Task SDK actually raised them. + """ + if not queue: + return None + try: + from airflow.sdk.exceptions import AirflowConfigException + from airflow.sdk.execution_time.coordinator import get_coordinator_manager + except ImportError: + return None + try: + return get_coordinator_manager().extra_for_queue(queue) + except (AirflowConfigException, ValueError): + self.log.warning( + "Ignoring coordinator config for queue %s: invalid [sdk] coordinator config", + queue, + exc_info=True, + ) + return None + + def _coordinator_pod_template_file(self, extra: dict[str, Any]) -> str | None: + """ + Return the pod template declared in a coordinator's *extra* mapping, if any. + + Lets a queue routed to a non-Python coordinator (via ``[sdk] + queue_to_coordinator``) launch its worker pod from a coordinator-specific + template — for example an image carrying the JVM for a Java coordinator. + """ + return extra.get("pod_template_file") + + def _coordinator_kube_image(self, extra: dict[str, Any]) -> str | None: + """ + Return the worker base image declared in a coordinator's *extra* mapping, if any. + + The base container image is never taken from a pod template; it comes + from ``kube_image`` (``worker_container_repository:worker_container_tag``) + or a per-task ``pod_override``. A coordinator may declare its own + ``worker_container_repository`` and ``worker_container_tag`` in ``extra`` + (e.g. a JRE-bearing image for a Java coordinator); both are required to + compose an override, otherwise the executor default applies. + """ + if (repo := extra.get("worker_container_repository")) and (tag := extra.get("worker_container_tag")): + return f"{repo}:{tag}" + return None + def execute_async( self, key: TaskInstanceKey, @@ -225,8 +279,34 @@ def execute_async( pod_template_file = executor_config.get("pod_template_file", None) else: pod_template_file = None + + coordinator_kube_image: str | None = None + if (coordinator_extra := self._coordinator_extra(queue)) is not None: + # A coordinator-level pod_template wins (e.g. a JVM image for JavaCoordinator) + coordinator_pod_template_file = self._coordinator_pod_template_file(coordinator_extra) + if coordinator_pod_template_file is not None: + self.log.debug( + "Using coordinator-declared pod template %s for task %s in queue %s", + coordinator_pod_template_file, + key, + queue, + ) + pod_template_file = coordinator_pod_template_file + + # The base image is not carried by a pod template, so a coordinator routes + # its worker base image separately (e.g. a JRE image for a Java queue). + if (coordinator_kube_image := self._coordinator_kube_image(coordinator_extra)) is not None: + self.log.debug( + "Using coordinator-declared base image %s for task %s in queue %s", + coordinator_kube_image, + key, + queue, + ) + self.event_buffer[key] = (TaskInstanceState.QUEUED, self.scheduler_job_id) - self.task_queue.put(KubernetesJob(key, command, kube_executor_config, pod_template_file)) + self.task_queue.put( + KubernetesJob(key, command, kube_executor_config, pod_template_file, coordinator_kube_image) + ) def queue_workload(self, workload: workloads.All, session: Session | None) -> None: from airflow.executors import workloads diff --git a/providers/cncf/kubernetes/src/airflow/providers/cncf/kubernetes/executors/kubernetes_executor_types.py b/providers/cncf/kubernetes/src/airflow/providers/cncf/kubernetes/executors/kubernetes_executor_types.py index f8e03f1f04c93..45d2ea301f75b 100644 --- a/providers/cncf/kubernetes/src/airflow/providers/cncf/kubernetes/executors/kubernetes_executor_types.py +++ b/providers/cncf/kubernetes/src/airflow/providers/cncf/kubernetes/executors/kubernetes_executor_types.py @@ -75,6 +75,7 @@ class KubernetesJob(NamedTuple): command: Sequence[str] kube_executor_config: Any pod_template_file: str | None + kube_image: str | None = None ALL_NAMESPACES = "ALL_NAMESPACES" diff --git a/providers/cncf/kubernetes/src/airflow/providers/cncf/kubernetes/executors/kubernetes_executor_utils.py b/providers/cncf/kubernetes/src/airflow/providers/cncf/kubernetes/executors/kubernetes_executor_utils.py index af719ada9e4fc..787ea73237f16 100644 --- a/providers/cncf/kubernetes/src/airflow/providers/cncf/kubernetes/executors/kubernetes_executor_utils.py +++ b/providers/cncf/kubernetes/src/airflow/providers/cncf/kubernetes/executors/kubernetes_executor_utils.py @@ -558,6 +558,7 @@ def run_next(self, next_job: KubernetesJob) -> None: command = next_job.command kube_executor_config = next_job.kube_executor_config pod_template_file = next_job.pod_template_file + kube_image = next_job.kube_image or self.kube_config.kube_image dag_id, task_id, run_id, try_number, map_index = key if len(command) == 1: @@ -586,7 +587,7 @@ def run_next(self, next_job: KubernetesJob) -> None: pod_id=create_unique_id(dag_id, task_id), dag_id=dag_id, task_id=task_id, - kube_image=self.kube_config.kube_image, + kube_image=kube_image, try_number=try_number, map_index=map_index, date=None, diff --git a/providers/cncf/kubernetes/tests/unit/cncf/kubernetes/executors/test_kubernetes_executor.py b/providers/cncf/kubernetes/tests/unit/cncf/kubernetes/executors/test_kubernetes_executor.py index bc1c2a97f55c7..65f5820482b7c 100644 --- a/providers/cncf/kubernetes/tests/unit/cncf/kubernetes/executors/test_kubernetes_executor.py +++ b/providers/cncf/kubernetes/tests/unit/cncf/kubernetes/executors/test_kubernetes_executor.py @@ -39,6 +39,7 @@ ) from airflow.providers.cncf.kubernetes.executors.kubernetes_executor_types import ( ADOPTED, + KubernetesJob, KubernetesResults, KubernetesWatch, ) @@ -66,7 +67,11 @@ from airflow.utils.state import State, TaskInstanceState from tests_common.test_utils.config import conf_vars -from tests_common.test_utils.version_compat import AIRFLOW_V_3_0_PLUS, AIRFLOW_V_3_2_PLUS +from tests_common.test_utils.version_compat import ( + AIRFLOW_V_3_0_PLUS, + AIRFLOW_V_3_2_PLUS, + AIRFLOW_V_3_3_PLUS, +) try: # Check whether a module-level function from stats is importable. @@ -863,11 +868,10 @@ def test_pod_template_file_override_in_executor_config( assert not executor.task_queue.empty() task = executor.task_queue.get_nowait() - _, _, expected_executor_config, expected_pod_template_file = task executor.task_queue.task_done() # Test that the correct values have been put to queue - assert expected_executor_config.metadata.labels == {"release": "stable"} - assert expected_pod_template_file == executor_template_file + assert task.kube_executor_config.metadata.labels == {"release": "stable"} + assert task.pod_template_file == executor_template_file self.kubernetes_executor.kube_scheduler.run_next(task) mock_run_pod_async.assert_called_once_with( @@ -915,6 +919,192 @@ def test_pod_template_file_override_in_executor_config( finally: executor.end() + @pytest.mark.skipif(not AIRFLOW_V_3_3_PLUS, reason="The coordinator interface only support since 3.3+") + @pytest.mark.parametrize( + ("coordinator_extra", "executor_config", "expected_template"), + [ + pytest.param( + {"pod_template_file": "/coord/java.yaml"}, + None, + "/coord/java.yaml", + id="coordinator-template-used", + ), + pytest.param( + {"pod_template_file": "/coord/java.yaml"}, + {"pod_template_file": "/from/executor_config.yaml"}, + "/coord/java.yaml", + id="coordinator-template-wins", + ), + pytest.param( + None, + {"pod_template_file": "/from/executor_config.yaml"}, + "/from/executor_config.yaml", + id="executor-config-used-without-coordinator", + ), + ], + ) + @mock.patch("airflow.providers.cncf.kubernetes.executors.kubernetes_executor_utils.KubernetesJobWatcher") + @mock.patch("airflow.providers.cncf.kubernetes.kube_client.get_kube_client") + @mock.patch.object(KubernetesExecutor, "_coordinator_extra") + def test_coordinator_pod_template_file_used_for_queue( + self, + mock_coordinator_extra, + mock_get_kube_client, + mock_kubernetes_job_watcher, + coordinator_extra, + executor_config, + expected_template, + ): + """A queue coordinator's template overrides executor_config; without a coordinator, executor_config is used.""" + mock_coordinator_extra.return_value = coordinator_extra + executor = self.kubernetes_executor + executor.start() + try: + executor.execute_async( + key=TaskInstanceKey("dag", "task", "run_id", 1, -1), + queue="java", + command=["airflow", "tasks", "run", "true", "some_parameter"], + executor_config=executor_config, + ) + assert not executor.task_queue.empty() + queued_job = executor.task_queue.get_nowait() + executor.task_queue.task_done() + assert queued_job.pod_template_file == expected_template + finally: + executor.end() + + @pytest.mark.skipif(not AIRFLOW_V_3_3_PLUS, reason="The coordinator interface only support since 3.3+") + @pytest.mark.parametrize( + ("extra", "expected"), + [ + pytest.param({"pod_template_file": "/coord/go.yaml"}, "/coord/go.yaml", id="template-in-extra"), + pytest.param({"other": "value"}, None, id="extra-without-template"), + ], + ) + def test_coordinator_pod_template_file_reads_extra(self, extra, expected): + """The template is read straight from the coordinator ``extra`` mapping passed in.""" + assert self.kubernetes_executor._coordinator_pod_template_file(extra) == expected + + @pytest.mark.skipif(not AIRFLOW_V_3_3_PLUS, reason="The coordinator interface only support since 3.3+") + @mock.patch("airflow.providers.cncf.kubernetes.executors.kubernetes_executor_utils.KubernetesJobWatcher") + @mock.patch("airflow.providers.cncf.kubernetes.kube_client.get_kube_client") + @mock.patch.object( + KubernetesExecutor, + "_coordinator_extra", + return_value={"worker_container_repository": "repo/java", "worker_container_tag": "1"}, + ) + def test_coordinator_kube_image_carried_on_job( + self, + mock_coordinator_extra, + mock_get_kube_client, + mock_kubernetes_job_watcher, + ): + """A coordinator base image resolved by queue rides on the queued job.""" + executor = self.kubernetes_executor + executor.start() + try: + executor.execute_async( + key=TaskInstanceKey("dag", "task", "run_id", 1, -1), + queue="java", + command=["airflow", "tasks", "run", "true", "some_parameter"], + ) + queued_job = executor.task_queue.get_nowait() + executor.task_queue.task_done() + assert queued_job.kube_image == "repo/java:1" + finally: + executor.end() + + @pytest.mark.skipif(not AIRFLOW_V_3_3_PLUS, reason="The coordinator interface only support since 3.3+") + @pytest.mark.parametrize( + ("extra", "expected"), + [ + pytest.param( + {"worker_container_repository": "repo/java", "worker_container_tag": "17"}, + "repo/java:17", + id="repository-and-tag", + ), + pytest.param({"worker_container_repository": "repo/java"}, None, id="repository-only"), + pytest.param({"worker_container_tag": "17"}, None, id="tag-only"), + pytest.param({"other": "value"}, None, id="extra-without-image"), + ], + ) + def test_coordinator_kube_image_reads_extra(self, extra, expected): + """The base image is composed straight from the coordinator ``extra`` mapping passed in.""" + assert self.kubernetes_executor._coordinator_kube_image(extra) == expected + + @pytest.mark.skipif(not AIRFLOW_V_3_3_PLUS, reason="The coordinator interface only support since 3.3+") + def test_coordinator_extra_skips_lookup_without_queue(self): + """No queue means no coordinator lookup (and no Task SDK import).""" + with mock.patch("airflow.sdk.execution_time.coordinator.get_coordinator_manager") as mock_get_manager: + assert self.kubernetes_executor._coordinator_extra(None) is None + mock_get_manager.assert_not_called() + + @pytest.mark.skipif(not AIRFLOW_V_3_3_PLUS, reason="The coordinator interface only support since 3.3+") + def test_coordinator_extra_returns_none_on_old_task_sdk(self): + """Pre-3.3 Task SDKs lack get_coordinator_manager; the import error falls back to None.""" + with mock.patch.dict("sys.modules", {"airflow.sdk.execution_time.coordinator": None}): + assert self.kubernetes_executor._coordinator_extra("java") is None + + @pytest.mark.skipif(not AIRFLOW_V_3_3_PLUS, reason="The coordinator interface only support since 3.3+") + @pytest.mark.parametrize("exc_type", ["airflow_config_exception", "value_error"]) + def test_coordinator_extra_falls_back_on_invalid_config(self, exc_type): + """A malformed ``[sdk]`` coordinator config must degrade gracefully, not crash the scheduler.""" + if exc_type == "airflow_config_exception": + from airflow.sdk.exceptions import AirflowConfigException + + exc = AirflowConfigException("invalid json") + else: + exc = ValueError("invalid coordinator key") + with mock.patch("airflow.sdk.execution_time.coordinator.get_coordinator_manager") as mock_get_manager: + mock_get_manager.return_value.extra_for_queue.side_effect = exc + assert self.kubernetes_executor._coordinator_extra("java") is None + + @pytest.mark.skipif( + AirflowKubernetesScheduler is None, reason="kubernetes python package is not installed" + ) + @pytest.mark.parametrize( + ("job_image", "use_default"), + [ + pytest.param("repo/java:17", False, id="job-image-wins"), + pytest.param(None, True, id="falls-back-to-kube_config"), + ], + ) + @mock.patch( + "airflow.providers.cncf.kubernetes.executors.kubernetes_executor_utils.AirflowKubernetesScheduler.run_pod_async" + ) + @mock.patch("airflow.providers.cncf.kubernetes.executors.kubernetes_executor_utils.PodGenerator") + @mock.patch( + "airflow.providers.cncf.kubernetes.executors.kubernetes_executor_utils.get_base_pod_from_template" + ) + @mock.patch("airflow.providers.cncf.kubernetes.kube_client.get_kube_client") + def test_run_next_applies_job_kube_image( + self, + mock_get_kube_client, + mock_get_base_pod, + mock_pod_generator, + mock_run_pod_async, + job_image, + use_default, + ): + """``run_next`` uses the job's coordinator image, falling back to the kube_config default.""" + executor = self.kubernetes_executor + executor.start() + try: + scheduler = executor.kube_scheduler + scheduler.run_next( + KubernetesJob( + key=TaskInstanceKey("dag", "task", "run_id", 1, -1), + command=["airflow", "tasks", "run", "true", "some_parameter"], + kube_executor_config=None, + pod_template_file=None, + kube_image=job_image, + ) + ) + expected = scheduler.kube_config.kube_image if use_default else job_image + assert mock_pod_generator.construct_pod.call_args.kwargs["kube_image"] == expected + finally: + executor.end() + @pytest.mark.db_test @mock.patch("airflow.providers.cncf.kubernetes.executors.kubernetes_executor_utils.KubernetesJobWatcher") @mock.patch("airflow.providers.cncf.kubernetes.kube_client.get_kube_client") From 022c571f48780ab97c357a29366f6e966d154130 Mon Sep 17 00:00:00 2001 From: Wei Lee Date: Tue, 7 Jul 2026 21:24:06 +0800 Subject: [PATCH 041/297] [v3-3-test] Add Multi-Lang KubernetesExecutor system test (#68709) (#69532) Co-authored-by: Jason(Zhe-You) Liu <68415893+jason810496@users.noreply.github.com> --- .github/workflows/k8s-tests.yml | 3 + dev/breeze/doc/05_test_commands.rst | 17 + dev/breeze/doc/images/output-commands.svg | 2 +- dev/breeze/doc/images/output_k8s.svg | 38 +- dev/breeze/doc/images/output_k8s.txt | 2 +- .../images/output_k8s_run-complete-tests.svg | 72 ++- .../images/output_k8s_run-complete-tests.txt | 2 +- .../images/output_k8s_setup-lang-sdk-test.svg | 152 ++++++ .../images/output_k8s_setup-lang-sdk-test.txt | 1 + ...utput_setup_check-all-params-in-groups.svg | 4 +- ...utput_setup_check-all-params-in-groups.txt | 2 +- ...output_setup_regenerate-command-images.svg | 8 +- ...output_setup_regenerate-command-images.txt | 2 +- .../commands/kubernetes_commands.py | 498 +++++++++++++++++- .../commands/kubernetes_commands_config.py | 21 +- kubernetes-tests/lang_sdk/Dockerfile.java | 31 ++ kubernetes-tests/lang_sdk/README.md | 103 ++++ kubernetes-tests/lang_sdk/config/values.yaml | 74 +++ .../lang_sdk/dags/lang_sdk_combined.py | 80 +++ .../lang_sdk/go_example/.gitignore | 3 + kubernetes-tests/lang_sdk/go_example/go.mod | 55 ++ kubernetes-tests/lang_sdk/go_example/go.sum | 155 ++++++ kubernetes-tests/lang_sdk/go_example/main.go | 88 ++++ .../lang_sdk/java_example/.gitignore | 3 + .../lang_sdk/java_example/build.gradle | 50 ++ .../lang_sdk/java_example/gradle.properties | 19 + .../lang_sdk/java_example/settings.gradle | 33 ++ .../airflow/k8sexample/CombinedExample.java | 47 ++ .../airflow/k8sexample/K8sBundleBuilder.java | 39 ++ .../lang_sdk/manifests/localstack.yaml | 77 +++ .../pod_templates/lang_sdk_golang.yaml | 117 ++++ .../lang_sdk/pod_templates/lang_sdk_java.yaml | 115 ++++ kubernetes-tests/lang_sdk/stage_artifacts.py | 92 ++++ .../test_lang_sdk_coordinator_executor.py | 88 ++++ 34 files changed, 2042 insertions(+), 51 deletions(-) create mode 100644 dev/breeze/doc/images/output_k8s_setup-lang-sdk-test.svg create mode 100644 dev/breeze/doc/images/output_k8s_setup-lang-sdk-test.txt create mode 100644 kubernetes-tests/lang_sdk/Dockerfile.java create mode 100644 kubernetes-tests/lang_sdk/README.md create mode 100644 kubernetes-tests/lang_sdk/config/values.yaml create mode 100644 kubernetes-tests/lang_sdk/dags/lang_sdk_combined.py create mode 100644 kubernetes-tests/lang_sdk/go_example/.gitignore create mode 100644 kubernetes-tests/lang_sdk/go_example/go.mod create mode 100644 kubernetes-tests/lang_sdk/go_example/go.sum create mode 100644 kubernetes-tests/lang_sdk/go_example/main.go create mode 100644 kubernetes-tests/lang_sdk/java_example/.gitignore create mode 100644 kubernetes-tests/lang_sdk/java_example/build.gradle create mode 100644 kubernetes-tests/lang_sdk/java_example/gradle.properties create mode 100644 kubernetes-tests/lang_sdk/java_example/settings.gradle create mode 100644 kubernetes-tests/lang_sdk/java_example/src/java/org/apache/airflow/k8sexample/CombinedExample.java create mode 100644 kubernetes-tests/lang_sdk/java_example/src/java/org/apache/airflow/k8sexample/K8sBundleBuilder.java create mode 100644 kubernetes-tests/lang_sdk/manifests/localstack.yaml create mode 100644 kubernetes-tests/lang_sdk/pod_templates/lang_sdk_golang.yaml create mode 100644 kubernetes-tests/lang_sdk/pod_templates/lang_sdk_java.yaml create mode 100644 kubernetes-tests/lang_sdk/stage_artifacts.py create mode 100644 kubernetes-tests/tests/kubernetes_tests/test_lang_sdk_coordinator_executor.py diff --git a/.github/workflows/k8s-tests.yml b/.github/workflows/k8s-tests.yml index 04f587edf9640..9aa28cfaeade8 100644 --- a/.github/workflows/k8s-tests.yml +++ b/.github/workflows/k8s-tests.yml @@ -103,6 +103,9 @@ jobs: env: EXECUTOR: ${{ matrix.executor }} USE_STANDARD_NAMING: ${{ matrix.use-standard-naming }} + # Provision + run the lang-SDK coordinator system test in a single variant only + # (KubernetesExecutor, standard-naming off) to keep it off the other five k8s jobs. + RUN_LANG_SDK_K8S_TESTS: ${{ (matrix.executor == 'KubernetesExecutor' && matrix.use-standard-naming == false) && 'true' || 'false' }} # yamllint disable-line rule:line-length VERBOSE: "false" - name: "\ Print logs ${{ matrix.executor }}-${{ matrix.kubernetes-combo }}-\ diff --git a/dev/breeze/doc/05_test_commands.rst b/dev/breeze/doc/05_test_commands.rst index 9422918e9bfdb..ae165365164eb 100644 --- a/dev/breeze/doc/05_test_commands.rst +++ b/dev/breeze/doc/05_test_commands.rst @@ -565,6 +565,23 @@ All parameters of the command are here: :width: 100% :alt: Breeze k8s deploy-airflow +Setting up the lang-SDK coordinator system test +............................................... + +``breeze k8s setup-lang-sdk-test`` provisions a cluster for the lang-SDK coordinator +system test: it builds the Go and Java example bundles, deploys an in-cluster localstack +S3, uploads the artifacts and the Python stub Dag to their buckets, renders the +coordinator pod-template image placeholders, and installs the Helm release configured for +the ``golang`` and ``java`` queues. After it completes, run the test with +``breeze k8s tests``. + +All parameters of the command are here: + +.. image:: ./images/output_k8s_setup-lang-sdk-test.svg + :target: https://raw.githubusercontent.com/apache/airflow/main/dev/breeze/images/output_k8s_setup-lang-sdk-test.svg + :width: 100% + :alt: Breeze k8s setup-lang-sdk-test + Hot-reloading Dags and core sources ................................... diff --git a/dev/breeze/doc/images/output-commands.svg b/dev/breeze/doc/images/output-commands.svg index 4891c30ed5124..6dfe7e09c52c4 100644 --- a/dev/breeze/doc/images/output-commands.svg +++ b/dev/breeze/doc/images/output-commands.svg @@ -367,7 +367,7 @@ -Usage:breeze[OPTIONSCOMMAND [ARGS]... +Usage:breeze[OPTIONS] [COMMAND] [ARGS]... ╭─ Common options ─────────────────────────────────────────────────────────────────────────────────────────────────────╮ --answer -aForce answer to questions. (y | n | q | yes | no | quit) diff --git a/dev/breeze/doc/images/output_k8s.svg b/dev/breeze/doc/images/output_k8s.svg index fd164ea3575ff..33dfb7ca92762 100644 --- a/dev/breeze/doc/images/output_k8s.svg +++ b/dev/breeze/doc/images/output_k8s.svg @@ -1,4 +1,4 @@ - + logs    Dump k8s logs to ${TMP_DIR}/kind_logs_<cluster_name> directory (optionally all clusters).                  ╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭─ K8S testing commands ───────────────────────────────────────────────────────────────────────────────────────────────╮ -tests             Run tests against the current KinD cluster (optionally for all clusters in parallel).            -run-complete-testsRun complete k8s tests consisting of: creating cluster, building and uploading image, deploying  -airflow, running tests and deleting clusters (optionally for all clusters in parallel).          -shell             Run shell environment for the current KinD cluster.                                              -k9s               Run k9s tool. You can pass any k9s args as extra args.                                           -╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ +tests              Run tests against the current KinD cluster (optionally for all clusters in parallel).           +run-complete-tests Run complete k8s tests consisting of: creating cluster, building and uploading image, deploying +airflow, running tests and deleting clusters (optionally for all clusters in parallel).         +setup-lang-sdk-testProvision the lang-SDK (Go + Java) coordinator system test on an already-deployed               +KubernetesExecutor cluster: build artifacts, build + load the Java worker image, deploy         +localstack S3, upload artifacts + stub Dag, create config, and upgrade the Helm release. Run    +the test afterwards with `RUN_LANG_SDK_K8S_TESTS=true breeze k8s tests --executor +KubernetesExecutor -- -k test_lang_sdk_combined_dag_succeeds`.                                  +shell              Run shell environment for the current KinD cluster.                                             +k9s                Run k9s tool. You can pass any k9s args as extra args.                                          +╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ diff --git a/dev/breeze/doc/images/output_k8s.txt b/dev/breeze/doc/images/output_k8s.txt index 710fb9e9e5af2..9905cb9eb1454 100644 --- a/dev/breeze/doc/images/output_k8s.txt +++ b/dev/breeze/doc/images/output_k8s.txt @@ -1 +1 @@ -bbba8147c6480f8b333ae667f0ace4f5 +bbfb153dc7f511c65455bcf213bef665 diff --git a/dev/breeze/doc/images/output_k8s_run-complete-tests.svg b/dev/breeze/doc/images/output_k8s_run-complete-tests.svg index 7af3fc6182b40..6f378e470770d 100644 --- a/dev/breeze/doc/images/output_k8s_run-complete-tests.svg +++ b/dev/breeze/doc/images/output_k8s_run-complete-tests.svg @@ -1,4 +1,4 @@ - +
{prepare_breeze_timing}" lines.append( - f"| {reg['job']} | {format_duration(reg['baseline'])} | " + f"| {job} | {format_duration(reg['baseline'])} | " f"{format_duration(reg['latest'])} | +{round(reg['rel_increase'] * 100, 1)}% |" ) lines.append("") diff --git a/scripts/tests/ci/test_analyze_ci_job_durations.py b/scripts/tests/ci/test_analyze_ci_job_durations.py index 432fefe44a576..6bac585b60c1b 100644 --- a/scripts/tests/ci/test_analyze_ci_job_durations.py +++ b/scripts/tests/ci/test_analyze_ci_job_durations.py @@ -101,6 +101,14 @@ def test_zero_pads_seconds(self, durations_module): assert durations_module.format_duration(60 + 5) == "1m 05s" +class TestFormatDurationDelta: + def test_positive(self, durations_module): + assert durations_module.format_duration_delta(60 + 5) == "+1m 05s" + + def test_negative(self, durations_module): + assert durations_module.format_duration_delta(-(60 + 5)) == "-1m 05s" + + class TestDetectRegression: def test_flags_regression_above_both_thresholds(self, durations_module): # baseline median ~1800s (30m), latest 2700s (45m) -> +50%, +15m @@ -238,6 +246,13 @@ def test_parses_successful_jobs(self, durations_module): "conclusion": "success", "startedAt": "2026-06-10T13:00:00Z", "completedAt": "2026-06-10T13:20:00Z", + "steps": [ + { + "name": "Prepare breeze & CI image: 3.10", + "startedAt": "2026-06-10T13:00:00Z", + "completedAt": "2026-06-10T13:05:00Z", + } + ], }, { "name": "Skipped job", @@ -251,7 +266,58 @@ def test_parses_successful_jobs(self, durations_module): completed = subprocess.CompletedProcess(args=[], returncode=0, stdout=payload, stderr="") with patch.object(subprocess, "run", return_value=completed): jobs = durations_module.get_run_jobs("apache/airflow", 2) - assert jobs == {"Tests": 20 * 60} + assert jobs == {"Tests": {"duration": 20 * 60, "prepare_breeze_duration": 5 * 60}} + + def test_omits_prepare_breeze_duration_when_step_missing(self, durations_module): + payload = json.dumps( + { + "jobs": [ + { + "name": "Tests", + "conclusion": "success", + "startedAt": "2026-06-10T13:00:00Z", + "completedAt": "2026-06-10T13:20:00Z", + "steps": [], + } + ] + } + ) + completed = subprocess.CompletedProcess(args=[], returncode=0, stdout=payload, stderr="") + with patch.object(subprocess, "run", return_value=completed): + jobs = durations_module.get_run_jobs("apache/airflow", 2) + assert jobs == {"Tests": {"duration": 20 * 60, "prepare_breeze_duration": None}} + + def test_keeps_longest_duplicate_job_name(self, durations_module): + payload = json.dumps( + { + "jobs": [ + { + "name": "Tests", + "conclusion": "success", + "startedAt": "2026-06-10T13:00:00Z", + "completedAt": "2026-06-10T13:10:00Z", + "steps": [], + }, + { + "name": "Tests", + "conclusion": "success", + "startedAt": "2026-06-10T13:00:00Z", + "completedAt": "2026-06-10T13:20:00Z", + "steps": [ + { + "name": "Prepare breeze & CI image: 3.10", + "startedAt": "2026-06-10T13:00:00Z", + "completedAt": "2026-06-10T13:05:00Z", + } + ], + }, + ] + } + ) + completed = subprocess.CompletedProcess(args=[], returncode=0, stdout=payload, stderr="") + with patch.object(subprocess, "run", return_value=completed): + jobs = durations_module.get_run_jobs("apache/airflow", 2) + assert jobs == {"Tests": {"duration": 20 * 60, "prepare_breeze_duration": 5 * 60}} def test_empty_on_command_failure(self, durations_module): completed = subprocess.CompletedProcess(args=[], returncode=1, stdout="", stderr="boom") @@ -266,9 +332,16 @@ def test_reports_only_regressed_jobs_with_enough_baseline(self, durations_module def fake_jobs(_repo, run_id): if run_id == 100: - return {"slow-job": 2700, "stable-job": 600, "new-job": 999} + return { + "slow-job": {"duration": 2700, "prepare_breeze_duration": 900}, + "stable-job": {"duration": 600, "prepare_breeze_duration": None}, + "new-job": {"duration": 999, "prepare_breeze_duration": 300}, + } # baseline runs - return {"slow-job": 1800, "stable-job": 590} + return { + "slow-job": {"duration": 1800, "prepare_breeze_duration": 300}, + "stable-job": {"duration": 590, "prepare_breeze_duration": None}, + } with patch.object(durations_module, "get_run_jobs", side_effect=fake_jobs): regressions = durations_module.analyze_jobs( @@ -282,6 +355,7 @@ def fake_jobs(_repo, run_id): names = [r["job"] for r in regressions] # slow-job regressed; stable-job did not; new-job lacks baseline samples assert names == ["slow-job"] + assert regressions[0]["prepare_breeze"] == {"latest": 900, "baseline": 300, "increase": 600} class TestFormatSlackMessage: @@ -308,3 +382,42 @@ def test_includes_channel_and_blocks(self, durations_module): text_blob = json.dumps(msg) assert "Tests" in text_blob assert "main" in msg["text"] + + def test_includes_prepare_breeze_timing_when_available(self, durations_module): + msg = durations_module.format_slack_message( + repo="apache/airflow", + workflow="ci-amd.yml", + branch="main", + overall_regression=None, + job_regressions=[ + { + "job": "Tests", + "latest": 1500, + "baseline": 1000, + "increase": 500, + "rel_increase": 0.5, + "prepare_breeze": {"latest": 600, "baseline": 300, "increase": 300}, + } + ], + recent_runs=[{"run_number": 102, "html_url": "https://example/2", "duration": 2700}], + rel_threshold=0.25, + channel="internal-airflow-ci-cd", + ) + text_blob = json.dumps(msg) + assert "Prepare breeze & CI image: 5m 00s" in text_blob + assert "10m 00s" in text_blob + + def test_omits_prepare_breeze_timing_when_unavailable(self, durations_module): + msg = durations_module.format_slack_message( + repo="apache/airflow", + workflow="ci-amd.yml", + branch="main", + overall_regression=None, + job_regressions=[ + {"job": "Tests", "latest": 1500, "baseline": 1000, "increase": 500, "rel_increase": 0.5} + ], + recent_runs=[{"run_number": 102, "html_url": "https://example/2", "duration": 2700}], + rel_threshold=0.25, + channel="internal-airflow-ci-cd", + ) + assert "Prepare breeze" not in json.dumps(msg) From 6d10b8e2e3ad30b7760a4b10fcc9dabf65549da3 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Sat, 11 Jul 2026 17:09:32 +0300 Subject: [PATCH 067/297] [v3-3-test] Add upgrade-fab-provider skill and FAB contributing doc (#69729) (#69756) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Upgrading the pinned Flask-AppBuilder dependency is deceptively involved: the FAB provider vendors and subclasses large parts of FAB's security manager, so a bump must be reconciled against that vendored code, the alignment-test tripwire, and the generated provider README. Capturing the verified procedure as a skill — and a matching providers/fab/CONTRIBUTING.rst that points contributors at it and records past bumps — lets future FAB upgrades follow the same checklist instead of rediscovering it each time. The skill lives vendor-neutrally in .agents/skills with .claude and .github relays, matching the repository's existing skill layout. (cherry picked from commit 621192d76a9fec1f9f105d0597f9ddcd955c35b7) Co-authored-by: Jarek Potiuk --- .agents/skills/upgrade-fab-provider/SKILL.md | 270 +++++++++++++++++++ .claude/skills/upgrade-fab-provider | 1 + .github/skills/upgrade-fab-provider | 1 + .gitignore | 1 + providers/fab/CONTRIBUTING.rst | 56 ++++ 5 files changed, 329 insertions(+) create mode 100644 .agents/skills/upgrade-fab-provider/SKILL.md create mode 120000 .claude/skills/upgrade-fab-provider create mode 120000 .github/skills/upgrade-fab-provider create mode 100644 providers/fab/CONTRIBUTING.rst diff --git a/.agents/skills/upgrade-fab-provider/SKILL.md b/.agents/skills/upgrade-fab-provider/SKILL.md new file mode 100644 index 0000000000000..1b08f8aa22adb --- /dev/null +++ b/.agents/skills/upgrade-fab-provider/SKILL.md @@ -0,0 +1,270 @@ +--- +name: upgrade-fab-provider +description: > + Upgrade the pinned Flask-AppBuilder (FAB) dependency in the Apache Airflow + FAB provider (`providers/fab/`). Bumps the exact `flask-appbuilder==` pin and + its mirror constant, regenerates `uv.lock`, drives the `test_fab_alignment.py` + drift tripwire, reviews the vendored security-manager `override.py` against the + new upstream FAB, and conditionally re-vendors static assets / DB migrations. + Use when asked to "upgrade FAB", "bump flask-appbuilder", or move the FAB + provider to a newer Flask-AppBuilder release. +license: Apache-2.0 +--- + + + +# upgrade-fab-provider + +Airflow's FAB provider is **tightly coupled** to a specific Flask-AppBuilder +release because it vendors-in and subclasses large parts of FAB's security +manager. A version bump is therefore never "just change the pin" — it must be +reconciled against the vendored code, and that reconciliation is enforced by a +pytest **alignment test**, not a prek hook. + +The canonical reference for a real bump is PR **#66841** ("Bump flask-appbuilder +to 5.2.1 and mirror new auth event hooks") — commit `c72b6613fd`. Read its diff +first when in doubt: `git show c72b6613fd`. + +## The coupling — why this is not a one-line change + +- `providers/fab/pyproject.toml:75-80` explains it: Airflow vendored FAB's + security-manager code into + `providers/fab/src/airflow/providers/fab/auth_manager/security_manager/override.py` + (~2700 lines) as `FabAirflowSecurityManagerOverride`. Every bump must review + that class against upstream FAB for new / changed / removed methods. +- `test_fab_alignment.py` mechanically detects drift between the **installed** + FAB package and the vendored override, and **fails CI** until the developer + reconciles it. + +## Inputs + +- **Target version** — the FAB version to move to. If not given, use the latest + release on PyPI (`https://pypi.org/pypi/flask-appbuilder/json` → `info.version`). + Confirm the target with the user if it is a **major or minor** bump (higher + reconciliation risk); a **patch** bump can proceed. + +## The files a bump touches + +**Always:** + +1. `providers/fab/pyproject.toml` — line ~80, the `flask-appbuilder==X.Y.Z` pin + (the **only** real dependency pin in the repo). +2. `providers/fab/tests/unit/fab/auth_manager/security_manager/test_fab_alignment.py` + — `EXPECTED_FAB_VERSION = "X.Y.Z"` (line ~43). Must move in lockstep with the pin. +3. `providers/fab/docs/index.rst` — the dependency table row + ``` ``flask-appbuilder`` ``==X.Y.Z`` ``` (line ~114). +4. `providers/fab/README.rst` — the Requirements table row (line ~60). **Do + not hand-edit** — it is auto-generated. Regenerate it from the bumped + `pyproject.toml` with the `sync-provider-readme` prek hook (Step 8); the hook + re-renders the table whenever `pyproject.toml` changes. (Pre-existing bumps + that predate this hook left it to release-time regeneration; today the hook is + per-commit, so CI flags the drift — run it.) +5. `uv.lock` — regenerated (see Step 4 for the pinned-uv caveat). + +**Conditionally:** + +6. `providers/fab/src/airflow/providers/fab/auth_manager/security_manager/override.py` + — transplant any relevant upstream FAB changes (new auth hooks, changed + signatures, ported fixes). The reference bump added +37 lines here. **Note:** + a green alignment test does not prove the transplant is unnecessary — a + security fix may live inside a method Airflow vendors (see the 5.2.2 worked + example below). +7. `providers/fab/src/airflow/providers/fab/www/**` + `providers/fab/www-hash.txt` + — only if the new FAB ships changed static assets / templates that are + re-vendored (see Step 6). +8. `providers/fab/src/airflow/providers/fab/migrations/versions/**` — only if the + new FAB version ships DB migrations (see Step 7). + +**Never:** no newsfragment, and do **not** hand-edit `providers/fab/docs/changelog.rst` +— providers are released from `main` and the release manager regenerates the +changelog from `git log` (per `providers/AGENTS.md`). The commit subject is the +changelog entry. + +## Procedure + +### Step 1 — Determine the target version and current state + +- Read the current pin: `grep flask-appbuilder== providers/fab/pyproject.toml`. +- Resolve the target (PyPI latest, or the user's requested version). +- Classify the jump: patch / minor / major. For minor/major, skim the FAB + release notes (`https://github.com/dpgaspar/Flask-AppBuilder/releases`) for + security-manager / model / template changes before editing. + +### Step 2 — Bump the three source pins + +Edit in lockstep: + +- `providers/fab/pyproject.toml` — the `flask-appbuilder==X.Y.Z` line (keep the + trailing `# Whenever updating the version, run test_fab_alignment.py to verify.` + comment). +- `test_fab_alignment.py` — `EXPECTED_FAB_VERSION = "X.Y.Z"`. It is the tripwire; + editing it now is fine — you will keep re-running the test until it and the + other three tests pass. +- `providers/fab/docs/index.rst` — the dependency-table `==X.Y.Z` row. + +### Step 3 — Install the new FAB into the provider venv + +`uv --project providers/fab sync` installs the new pin. Confirm the installed +version: + +```bash +uv run --project providers/fab python -c \ + "import importlib.metadata as m; print(m.version('flask-appbuilder'))" +``` + +### Step 4 — Regenerate uv.lock (pinned uv!) + +The lock **must** be regenerated with the repo's pinned uv version, or it drifts +hundreds of unrelated marker lines: + +```bash +AIRFLOW_UV_VERSION=$(grep -oE 'AIRFLOW_UV_VERSION=[0-9.]+' Dockerfile.ci | head -1 | cut -d= -f2) +uvx --from uv==$AIRFLOW_UV_VERSION uv lock +``` + +If a conflict is irrecoverable, delete `uv.lock` and re-run `uv lock` with the +pinned version. Confirm the diff is limited to the FAB bump, not a wholesale +marker rewrite. + +### Step 5 — Run the alignment test and reconcile override.py + +```bash +uv run --project providers/fab pytest \ + providers/fab/tests/unit/fab/auth_manager/security_manager/test_fab_alignment.py -xvs +``` + +Under the host sandbox the test needs a writable `AIRFLOW_HOME` and the +rerun-failures socket disabled — if it errors on a socket `bind` or on +`~/airflow`, run it as: + +```bash +AIRFLOW_HOME="$TMPDIR/fab_home" uv run --project providers/fab pytest \ + providers/fab/tests/.../test_fab_alignment.py -q -p no:rerunfailures +``` + +The four tests and how to fix each: + +1. **`test_fab_version_matches_expected`** — trips on the version mismatch. It + passes once `EXPECTED_FAB_VERSION` == installed version, but only after you + have done the review below. +2. **`test_no_unaudited_fab_methods`** — a new FAB public method exists that is + neither implemented in `override.py` nor listed in `AUDITED_EXCLUSIONS`. + Fix: either implement/override it in `override.py`, or add it to + `AUDITED_EXCLUSIONS` **with a justification comment**. +3. **`test_no_stale_exclusions`** — `AUDITED_EXCLUSIONS` lists a method the new + FAB no longer has. Fix: remove that entry. +4. **`test_shared_method_signatures_compatible`** — FAB changed a method + signature (new required param). Fix: update the `override.py` signature, or + add to `KNOWN_SIGNATURE_DEVIATIONS` if the divergence is intentional. + +**The manual review that the test cannot fully automate:** diff the vendored +`override.py` against the new FAB's `flask_appbuilder/security/sqla/manager.py` +and `BaseSecurityManager` and **transplant behavioural changes** (bug fixes, new +auth event hooks), not just signatures — the test only checks method *presence* +and *required params*. Locate the installed source: + +```bash +uv run --project providers/fab python -c \ + "import flask_appbuilder.security.sqla.manager as m; print(m.__file__)" +``` + +### Step 6 — Re-vendor static assets / templates (only if changed) + +If the new FAB changed frontend assets that Airflow vendors under +`providers/fab/src/airflow/providers/fab/www/` (templates in +`templates/appbuilder/`, static JS/CSS), re-vendor them, then regenerate the +fingerprint: + +```bash +prek run compile-fab-assets --all-files +``` + +This runs `scripts/ci/prek/compile_provider_assets.py fab` (pnpm build over +`www/`) and rewrites `providers/fab/www-hash.txt`. A patch bump usually does +**not** touch assets — skip this step unless FAB's templates/static changed. +Commit the regenerated `www-hash.txt` if it changed. + +### Step 7 — DB migrations (only if FAB ships them) + +If the new FAB adds/changes security-model tables, add the corresponding +migration under `providers/fab/src/airflow/providers/fab/migrations/versions/` +and run: + +```bash +prek run update-migration-references-fab check-revision-heads-map-fab --all-files +``` + +Patch bumps normally have no migrations — skip unless the release notes mention +schema changes. + +### Step 8 — Static checks + tests + +```bash +prek run --from-ref main --stage pre-commit +uv run --project providers/fab pytest providers/fab/tests/unit/fab/auth_manager -xvs +``` + +The pre-commit stage runs `sync-provider-readme` (regenerating `README.rst`) and +other FAB hooks. Re-run the alignment test until all four tests pass. The full +provider suite is `breeze testing providers-tests --test-type "Providers[fab]"`. + +### Step 9 — Self-review and commit + +- `git diff main...HEAD` — verify only the intended files changed, and `uv.lock` + is a clean FAB-scoped diff. +- Commit subject in imperative mood, plain prose, **no** Conventional-Commits + prefix, e.g. `Bump flask-appbuilder to X.Y.Z in FAB provider`. Body explains + *why* (what upstream changes were mirrored), not what. +- No newsfragment, no changelog edit (provider release manager regenerates from + git log). +- Prepend your PR to the history list in `providers/fab/CONTRIBUTING.rst` so the + FAB-upgrade record stays current. +- Push to `origin` and open the PR per the repo's PR conventions. + +## Gotchas + +- **`EXPECTED_FAB_VERSION` is a second pin.** Forgetting it makes + `test_fab_alignment.py` fail even when everything else is correct. +- **`uv.lock` marker drift.** Always use the pinned uv (Step 4) — a bare + `uv lock` rewrites hundreds of environment-marker lines and buries the real diff. +- **The alignment test uses AST, not import**, to read FAB's `SecurityManager` + (to avoid SQLAlchemy model-registry collisions with Airflow's vendored models). + A green test proves *structural* alignment; it does **not** prove behavioural + parity — Step 5's manual transplant review is still required. +- **`docs/index.rst` dependency table** may look auto-generated but the reference + PR edited it by hand; if a docs-regen prek hook rewrites it, let the hook win. +- **`README.rst` is generated, but the sync hook is per-commit.** Don't hand-edit + it; run `prek run sync-provider-readme` (or the full pre-commit stage) after + bumping `pyproject.toml` — CI fails on the drift otherwise. +- **Don't touch `providers/fab/docs/upgrading.rst`** — that is end-user guidance + for upgrading the *provider package* in a deployment, not the developer bump + workflow. + +## Worked example — 5.2.1 to 5.2.2 (patch) + +A patch bump that needed **no `override.py` change**, but only after a real +behavioural review — the green alignment test alone was not sufficient evidence: + +- FAB 5.2.2 shipped three security-manager fixes: LDAP search-filter escaping, + OAuth email-allowlist regex anchoring (`email + "$"`), and API-login provider + validation. +- All three touch methods Airflow *vendors* (`_search_ldap`, `auth_user_ldap`, + `auth_user_oauth`) — so the alignment test passing did **not** mean "nothing to + do". Each had to be checked by hand: + - **LDAP escaping** — Airflow's vendored `_search_ldap` **already** escapes via + `ldap.filter.escape_filter_chars` (and adds filter-parenthesis validation); it + was ahead of FAB. No transplant. + - **OAuth allowlist anchoring** — the match lives in FAB's `AuthOAuthView` + (`views.py`), and Airflow's `CustomAuthOAuthView.oauth_authorized` delegates + via `super().oauth_authorized()`, so the fix is inherited from the installed + FAB 5.2.2. No transplant. + - **API-login validation / Azure-JWT warning / uuid4** — in FAB core Airflow + doesn't vendor. Inherited. +- Net change set: `pyproject.toml`, `test_fab_alignment.py`, `docs/index.rst`, + `README.rst` (via hook), `uv.lock`. Commit: `Bump flask-appbuilder to 5.2.2 in + FAB provider`. + +The lesson the skill encodes: **for every security/behavioural fix in the FAB +release notes, locate the method and check whether Airflow vendors it** — the +alignment test guards structure, you guard behaviour. diff --git a/.claude/skills/upgrade-fab-provider b/.claude/skills/upgrade-fab-provider new file mode 120000 index 0000000000000..d7c96f92a70e3 --- /dev/null +++ b/.claude/skills/upgrade-fab-provider @@ -0,0 +1 @@ +../../.agents/skills/upgrade-fab-provider \ No newline at end of file diff --git a/.github/skills/upgrade-fab-provider b/.github/skills/upgrade-fab-provider new file mode 120000 index 0000000000000..d7c96f92a70e3 --- /dev/null +++ b/.github/skills/upgrade-fab-provider @@ -0,0 +1 @@ +../../.agents/skills/upgrade-fab-provider \ No newline at end of file diff --git a/.gitignore b/.gitignore index a0ccf9fd3de7f..791680eeae553 100644 --- a/.gitignore +++ b/.gitignore @@ -139,6 +139,7 @@ ENV/ !.claude/skills/aip-user-stories !.claude/skills/airflow-translations !.claude/skills/prepare-providers-documentation +!.claude/skills/upgrade-fab-provider !.claude/skills/magpie-setup # Kiro diff --git a/providers/fab/CONTRIBUTING.rst b/providers/fab/CONTRIBUTING.rst new file mode 100644 index 0000000000000..091338ce34b25 --- /dev/null +++ b/providers/fab/CONTRIBUTING.rst @@ -0,0 +1,56 @@ + .. Licensed to the Apache Software Foundation (ASF) under one + or more contributor license agreements. See the NOTICE file + distributed with this work for additional information + regarding copyright ownership. The ASF licenses this file + to you under the Apache License, Version 2.0 (the + "License"); you may not use this file except in compliance + with the License. You may obtain a copy of the License at + + .. http://www.apache.org/licenses/LICENSE-2.0 + + .. Unless required by applicable law or agreed to in writing, + software distributed under the License is distributed on an + "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + KIND, either express or implied. See the License for the + specific language governing permissions and limitations + under the License. + +Contributing to the FAB provider +================================ + +This page collects developer notes for the ``apache-airflow-providers-fab`` +distribution. It is intentionally kept out of the published provider +documentation under ``docs/`` — it targets contributors editing the provider, +not deployment users. + +Upgrading Flask-AppBuilder +-------------------------- + +The FAB provider is **tightly coupled** to a specific Flask-AppBuilder (FAB) +release: it vendors and subclasses large parts of FAB's security manager in +``src/airflow/providers/fab/auth_manager/security_manager/override.py`` (and +vendors FAB's web assets/templates under ``src/airflow/providers/fab/www/``). +Bumping the pinned ``flask-appbuilder`` version is therefore never just a pin +change — the vendored code must be reconciled against the new release, which +``tests/unit/fab/auth_manager/security_manager/test_fab_alignment.py`` enforces +in CI. + +Use the ``upgrade-fab-provider`` agent skill +(``.agents/skills/upgrade-fab-provider/``) to perform a bump end-to-end. It +drives the pin change, the ``uv.lock`` regeneration, the alignment-test +tripwire, the manual review of the vendored ``override.py`` against upstream +FAB, and the generated-README sync — and documents the gotchas that a green +alignment test alone does not catch (a security fix can live inside a method +Airflow vendors). + +Past FAB version bumps, for reference (newest first): + +* ``5.2.1`` -> ``5.2.2``: https://github.com/apache/airflow/pull/69730 +* ``5.2.0`` -> ``5.2.1``: https://github.com/apache/airflow/pull/66841 +* ``5.0.1`` -> ``5.2.0``: https://github.com/apache/airflow/pull/62924 +* ``5.0.0`` -> ``5.0.1``: https://github.com/apache/airflow/pull/57170 +* ``4.6.3`` -> ``5.0.0``: https://github.com/apache/airflow/pull/50960 +* ``4.5.3`` -> ``4.6.3``: https://github.com/apache/airflow/pull/50513 + +When you complete a bump, prepend your PR to this list so the history stays +current. From 1373d0e4e77e3bf637324d0d90ad7a813fa740c5 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sun, 12 Jul 2026 18:09:46 +0200 Subject: [PATCH 068/297] Bump the github-actions-updates group with 4 updates (#69726) --- updated-dependencies: - dependency-name: github/codeql-action/init dependency-version: 4.36.3 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: github-actions-updates - dependency-name: github/codeql-action/autobuild dependency-version: 4.36.3 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: github-actions-updates - dependency-name: github/codeql-action/analyze dependency-version: 4.36.3 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: github-actions-updates - dependency-name: astral-sh/setup-uv dependency-version: 8.3.0 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: github-actions-updates ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/codeql-analysis.yml | 6 +++--- .github/workflows/registry-tests.yml | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml index ec2d9e6fd8feb..e35889da18cdc 100644 --- a/.github/workflows/codeql-analysis.yml +++ b/.github/workflows/codeql-analysis.yml @@ -122,13 +122,13 @@ jobs: java-version: '11' - name: Initialize CodeQL - uses: github/codeql-action/init@8aad20d150bbac5944a9f9d289da16a4b0d87c1e # v4.36.2 + uses: github/codeql-action/init@54f647b7e1bb85c95cddabcd46b0c578ec92bc1a # v4.36.3 with: languages: ${{ matrix.language }} - name: Autobuild if: matrix.language != 'java' - uses: github/codeql-action/autobuild@8aad20d150bbac5944a9f9d289da16a4b0d87c1e # v4.36.2 + uses: github/codeql-action/autobuild@54f647b7e1bb85c95cddabcd46b0c578ec92bc1a # v4.36.3 - name: Build Java SDK if: matrix.language == 'java' @@ -136,7 +136,7 @@ jobs: run: ./gradlew classes testClasses - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@8aad20d150bbac5944a9f9d289da16a4b0d87c1e # v4.36.2 + uses: github/codeql-action/analyze@54f647b7e1bb85c95cddabcd46b0c578ec92bc1a # v4.36.3 with: # Provide more context to the SARIF output (shows up in run.automationDetails.id field) category: "/language:${{matrix.language}}" diff --git a/.github/workflows/registry-tests.yml b/.github/workflows/registry-tests.yml index 7a0cb3c518eed..42ad21192b30a 100644 --- a/.github/workflows/registry-tests.yml +++ b/.github/workflows/registry-tests.yml @@ -50,7 +50,7 @@ jobs: persist-credentials: false - name: "Install uv" - uses: astral-sh/setup-uv@fac544c07dec837d0ccb6301d7b5580bf5edae39 # v8.2.0 + uses: astral-sh/setup-uv@d31148d669074a8d0a63714ba94f3201e7020bc3 # v8.3.0 with: python-version: "3.12" From f93c927eb5e5c159c173670397c9a732080c85dc Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sun, 12 Jul 2026 18:09:56 +0200 Subject: [PATCH 069/297] Bump the 3-3-core-ui-package-updates group across 1 directory with 5 updates (#69724) --- updated-dependencies: - dependency-name: "@xyflow/react" dependency-version: 12.11.2 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: 3-3-core-ui-package-updates - dependency-name: react-hook-form dependency-version: 7.81.0 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: 3-3-core-ui-package-updates - dependency-name: "@vitest/coverage-v8" dependency-version: 4.1.10 dependency-type: direct:development update-type: version-update:semver-patch dependency-group: 3-3-core-ui-package-updates - dependency-name: vite dependency-version: 8.1.3 dependency-type: direct:development update-type: version-update:semver-patch dependency-group: 3-3-core-ui-package-updates - dependency-name: vitest dependency-version: 4.1.10 dependency-type: direct:development update-type: version-update:semver-patch dependency-group: 3-3-core-ui-package-updates ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- airflow-core/src/airflow/ui/package.json | 10 +- airflow-core/src/airflow/ui/pnpm-lock.yaml | 336 ++++++++++----------- 2 files changed, 173 insertions(+), 173 deletions(-) diff --git a/airflow-core/src/airflow/ui/package.json b/airflow-core/src/airflow/ui/package.json index b31305173fb2f..371b8184c9b5e 100644 --- a/airflow-core/src/airflow/ui/package.json +++ b/airflow-core/src/airflow/ui/package.json @@ -36,7 +36,7 @@ "@tanstack/react-virtual": "^3.14.5", "@visx/group": "^3.12.0", "@visx/shape": "^3.12.0", - "@xyflow/react": "^12.11.1", + "@xyflow/react": "^12.11.2", "anser": "^2.3.5", "axios": "^1.18.1", "chakra-react-select": "^6.1.3", @@ -55,7 +55,7 @@ "react": "^19.2.7", "react-chartjs-2": "^5.3.1", "react-dom": "^19.2.7", - "react-hook-form": "^7.80.0", + "react-hook-form": "^7.81.0", "react-hotkeys-hook": "^4.6.2", "react-i18next": "^16.6.6", "react-icons": "^5.7.0", @@ -91,7 +91,7 @@ "@typescript-eslint/utils": "^8.62.1", "@vitejs/plugin-react": "^6.0.3", "@vitejs/plugin-react-swc": "^4.3.1", - "@vitest/coverage-v8": "^4.1.9", + "@vitest/coverage-v8": "^4.1.10", "babel-plugin-react-compiler": "^1.0.0", "eslint": "^10.6.0", "eslint-config-prettier": "^10.1.8", @@ -113,9 +113,9 @@ "ts-morph": "^27.0.2", "typescript": "^6.0.3", "typescript-eslint": "^8.62.1", - "vite": "^8.1.2", + "vite": "^8.1.3", "vite-plugin-css-injected-by-js": "^3.5.2", - "vitest": "^4.1.9", + "vitest": "^4.1.10", "web-worker": "^1.5.0" }, "pnpm": { diff --git a/airflow-core/src/airflow/ui/pnpm-lock.yaml b/airflow-core/src/airflow/ui/pnpm-lock.yaml index c6fd32a14cd1f..d13be47008ff1 100644 --- a/airflow-core/src/airflow/ui/pnpm-lock.yaml +++ b/airflow-core/src/airflow/ui/pnpm-lock.yaml @@ -72,8 +72,8 @@ importers: specifier: ^3.12.0 version: 3.12.0(react@19.2.7) '@xyflow/react': - specifier: ^12.11.1 - version: 12.11.1(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + specifier: ^12.11.2 + version: 12.11.2(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) anser: specifier: ^2.3.5 version: 2.3.5 @@ -129,8 +129,8 @@ importers: specifier: ^19.2.7 version: 19.2.7(react@19.2.7) react-hook-form: - specifier: ^7.80.0 - version: 7.80.0(react@19.2.7) + specifier: ^7.81.0 + version: 7.81.0(react@19.2.7) react-hotkeys-hook: specifier: ^4.6.2 version: 4.6.2(react-dom@19.2.7(react@19.2.7))(react@19.2.7) @@ -185,7 +185,7 @@ importers: version: 1.61.1 '@rolldown/plugin-babel': specifier: ^0.2.3 - version: 0.2.3(@babel/core@7.29.0)(@babel/runtime@7.29.7)(rolldown@1.1.4)(vite@8.1.2(@types/node@24.13.2)(jiti@1.21.7)(yaml@2.9.0)) + version: 0.2.3(@babel/core@7.29.0)(@babel/runtime@7.29.7)(rolldown@1.1.5)(vite@8.1.3(@types/node@24.13.2)(jiti@1.21.7)(yaml@2.9.0)) '@stylistic/eslint-plugin': specifier: ^2.13.0 version: 2.13.0(eslint@10.6.0(jiti@1.21.7))(typescript@6.0.3) @@ -227,13 +227,13 @@ importers: version: 8.62.1(eslint@10.6.0(jiti@1.21.7))(typescript@6.0.3) '@vitejs/plugin-react': specifier: ^6.0.3 - version: 6.0.3(@rolldown/plugin-babel@0.2.3(@babel/core@7.29.0)(@babel/runtime@7.29.7)(rolldown@1.1.4)(vite@8.1.2(@types/node@24.13.2)(jiti@1.21.7)(yaml@2.9.0)))(babel-plugin-react-compiler@1.0.0)(vite@8.1.2(@types/node@24.13.2)(jiti@1.21.7)(yaml@2.9.0)) + version: 6.0.3(@rolldown/plugin-babel@0.2.3(@babel/core@7.29.0)(@babel/runtime@7.29.7)(rolldown@1.1.5)(vite@8.1.3(@types/node@24.13.2)(jiti@1.21.7)(yaml@2.9.0)))(babel-plugin-react-compiler@1.0.0)(vite@8.1.3(@types/node@24.13.2)(jiti@1.21.7)(yaml@2.9.0)) '@vitejs/plugin-react-swc': specifier: ^4.3.1 - version: 4.3.1(@swc/helpers@0.5.23)(vite@8.1.2(@types/node@24.13.2)(jiti@1.21.7)(yaml@2.9.0)) + version: 4.3.1(@swc/helpers@0.5.23)(vite@8.1.3(@types/node@24.13.2)(jiti@1.21.7)(yaml@2.9.0)) '@vitest/coverage-v8': - specifier: ^4.1.9 - version: 4.1.9(vitest@4.1.9) + specifier: ^4.1.10 + version: 4.1.10(vitest@4.1.10) babel-plugin-react-compiler: specifier: ^1.0.0 version: 1.0.0 @@ -298,14 +298,14 @@ importers: specifier: ^8.62.1 version: 8.62.1(eslint@10.6.0(jiti@1.21.7))(typescript@6.0.3) vite: - specifier: ^8.1.2 - version: 8.1.2(@types/node@24.13.2)(jiti@1.21.7)(yaml@2.9.0) + specifier: ^8.1.3 + version: 8.1.3(@types/node@24.13.2)(jiti@1.21.7)(yaml@2.9.0) vite-plugin-css-injected-by-js: specifier: ^3.5.2 - version: 3.5.2(vite@8.1.2(@types/node@24.13.2)(jiti@1.21.7)(yaml@2.9.0)) + version: 3.5.2(vite@8.1.3(@types/node@24.13.2)(jiti@1.21.7)(yaml@2.9.0)) vitest: - specifier: ^4.1.9 - version: 4.1.9(@types/node@24.13.2)(@vitest/coverage-v8@4.1.9)(happy-dom@20.10.6)(msw@2.14.6(@types/node@24.13.2)(typescript@6.0.3))(vite@8.1.2(@types/node@24.13.2)(jiti@1.21.7)(yaml@2.9.0)) + specifier: ^4.1.10 + version: 4.1.10(@types/node@24.13.2)(@vitest/coverage-v8@4.1.10)(happy-dom@20.10.6)(msw@2.14.6(@types/node@24.13.2)(typescript@6.0.3))(vite@8.1.3(@types/node@24.13.2)(jiti@1.21.7)(yaml@2.9.0)) web-worker: specifier: ^1.5.0 version: 1.5.0 @@ -764,8 +764,8 @@ packages: resolution: {integrity: sha512-XRO0zi2NIUKq2lUk3T1ecFSld1fMWRKE6naRFGkgkdeosx7IslyUKNv5Dcb5PJTja9tHJoFu0v/7yEpAkrkrTg==} engines: {node: ^20.19.0 || ^22.13.0 || >=24} - '@oxc-project/types@0.138.0': - resolution: {integrity: sha512-1a7ZKmrRTCoN1XMZ4L0PyyqrMnrNlLyPuOkdSX2MZg7IiIGRUyurNhAm73ptDOraoBcIordsIGKNPKUzy3ZmfA==} + '@oxc-project/types@0.139.0': + resolution: {integrity: sha512-r9gHphtCs+1M7J0pw6Sn/hh/Wpa/iQrOOkrNAlVLF/gHq+/CJmHIWKKUUhdWjcD6CIa8idarspCsASiXCXvFUw==} '@pandacss/is-valid-prop@1.11.4': resolution: {integrity: sha512-RWxInlS+lGgKiF0fB0HO76vsJFgRvbavm5Z25/GqqN8MPHXYA6n5rZnfdp4itEXy5DJkQ9vt3yrwa2IKiuhtrA==} @@ -780,91 +780,91 @@ packages: engines: {node: '>=18'} hasBin: true - '@rolldown/binding-android-arm64@1.1.4': - resolution: {integrity: sha512-EZLpf/8y7GXkkra90ML47kzik/GMP3EMcE9bPyHmRfxLC6z9+aW5A8poCsoxjrT5GfEcNAAvWwUHjvP1pUQkfw==} + '@rolldown/binding-android-arm64@1.1.5': + resolution: {integrity: sha512-lZg8fqIv2v7FF237bwMgzGZEJvGL79/s5knJ/i6FmsGF4XXlzccZ4jb+TrFIxtSSxFtIpdsgrPZeMk1I9AFcyQ==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [android] - '@rolldown/binding-darwin-arm64@1.1.4': - resolution: {integrity: sha512-aUi+HBvmYb7j8krl1+qJgkG8C17fO79gk3c+jPw4S8glRFc1DTija9S3EyaTSQUm5GJXYKDAsugBEhFHH2vYiQ==} + '@rolldown/binding-darwin-arm64@1.1.5': + resolution: {integrity: sha512-51Bnx9pNiMRKSUNtBfySkNJ9vMU9Hh3I1ozDd6gyPPYzaXCfnptUcEZxXGYFn+ul2dtcMUiqGR1Yai2K10uoTw==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [darwin] - '@rolldown/binding-darwin-x64@1.1.4': - resolution: {integrity: sha512-F7hHC3gwY11+vByKPRWqwGbeXWVgKmL+pTGCinaEhdihzBV2aQ0fvZOch9cXYUOKuKKq429HeYXOqQLc7wFCEg==} + '@rolldown/binding-darwin-x64@1.1.5': + resolution: {integrity: sha512-Tm+gbfC0aHu1tBA/JvKQh32S0K6YgCHkiAF4/W6xX0K0RmNuc94VeK419dJoE65R5aRxmo+noZQSWrAMF6yb6g==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [darwin] - '@rolldown/binding-freebsd-x64@1.1.4': - resolution: {integrity: sha512-sI5yw+7s92SK6odiEhD5lKCBlWcpjHS5qyqpVQbZAJ0fIzEUXrmbl3DH2ybR3PZogulNJF+COLtmA8hUfvkCCQ==} + '@rolldown/binding-freebsd-x64@1.1.5': + resolution: {integrity: sha512-JMzDKCCXq93YccG5gz3hvOs1oXRKAf0XYpfOS88e+wZrC8Iugj6j68867vrYZkvpDDpKn/KoKORThmchMpF6TA==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [freebsd] - '@rolldown/binding-linux-arm-gnueabihf@1.1.4': - resolution: {integrity: sha512-mCi0OKgEieFircrtVYmQAFGszRtMnZ6fpZAXrxanXAu7lqZcsK1E1RAaZNG0uKAnxox3B1f4EyQNnoyMfN1vAA==} + '@rolldown/binding-linux-arm-gnueabihf@1.1.5': + resolution: {integrity: sha512-uML21j2K5TfPGutKxub+M+nLjZIrWjXQ5Grx4lCe/nimTj9B4L63zHpjXLl4y0L3mcm2htEQIb06oCG/szerNw==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm] os: [linux] - '@rolldown/binding-linux-arm64-gnu@1.1.4': - resolution: {integrity: sha512-B9Ial3Kv5sh0SHnB1g/QWcUQCEvCF6QKGAl4zXypYj65mVI+B4AhFBwPtSN7pDrJeIx8Z7zdy4ntx+wQABom7w==} + '@rolldown/binding-linux-arm64-gnu@1.1.5': + resolution: {integrity: sha512-navSiuTMogvnQoZoM/v+l3ZWo50/NTwSHSzheABx/RCnmUPaKwq9qSo4Br2OYRs21+Fz8uFqITZM3H4opOB0/Q==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [linux] - '@rolldown/binding-linux-arm64-musl@1.1.4': - resolution: {integrity: sha512-lZVym0PuHE1KZ22gmFTC15lAkrg9iTszR617oYRB/iPY1A56ywoJzVKOJBKaot5RiikCObmur6pogpse3gRcng==} + '@rolldown/binding-linux-arm64-musl@1.1.5': + resolution: {integrity: sha512-lAryqH7IteztmCXQXk0etKj4wBQ7Gx5S6LjKhsgp9zb8I5bsuvU/2llH1hDQcjsFeqIsovMVN339/8pUDDBXxA==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [linux] - '@rolldown/binding-linux-ppc64-gnu@1.1.4': - resolution: {integrity: sha512-t2DNiLJWNTbnEHyUzTumldML6ET4/g16467LZoDDJ3tSxGvguL5/NyC2lCsNKuyRycg9XeDQF5SSv+TNOhQEXg==} + '@rolldown/binding-linux-ppc64-gnu@1.1.5': + resolution: {integrity: sha512-fsK/sNBnxzBlL4O1JNrZakVQxPspqpED5dLtNsZS9oOKmtSpdNIzxH2kkol5HYTWJN47sE20ztMJPxfZ89qGOg==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [ppc64] os: [linux] - '@rolldown/binding-linux-s390x-gnu@1.1.4': - resolution: {integrity: sha512-0WIRnL1Uw4BvTZRLQt+PVgo6ZKTJadlC2btP+/EOXv2f/DWbY0rEgl+y834mIVwP1FkTlWVTrGGJXf12lru7EQ==} + '@rolldown/binding-linux-s390x-gnu@1.1.5': + resolution: {integrity: sha512-gLYb4BIadlfTOYT5gO503n8zQjXflgzpD0FcyKh0Mzx3rqCZKnHoJWV9xe1KXUJ5lx2JfcSHr/mhzS0PC/McAA==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [s390x] os: [linux] - '@rolldown/binding-linux-x64-gnu@1.1.4': - resolution: {integrity: sha512-JWtGshGfX+oENAKonoNkqEJX+7hC8yfhi9GUyPX1VX4mdh1y5r+ZiJLR5XzAB0aoP6s/PcILsGjKq8O0mm24bw==} + '@rolldown/binding-linux-x64-gnu@1.1.5': + resolution: {integrity: sha512-FjcpEKUyJygHgs1o50VYNvkt5+7Le/VEdYt0AkRpkL33MnyQfwr8l5mXwMmfmTbyMPr5vJLC+8/Gd9gXnwU1QQ==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [linux] - '@rolldown/binding-linux-x64-musl@1.1.4': - resolution: {integrity: sha512-rT6yQcxUuXs4CnbofqwHRRV0iem349rLMYpTjkgQGLjrY4ado/eDzwPZPTCgTOlF6Nkp8NEv70yLMTn6qkWxsQ==} + '@rolldown/binding-linux-x64-musl@1.1.5': + resolution: {integrity: sha512-Me+PfPI2TMeOQk0gYWfLQZtTktrmzbr8cDboqX83XKc7UrgAi55gF+2dUkWdxd19n55Essp2yeca+O9N5rBxHg==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [linux] - '@rolldown/binding-openharmony-arm64@1.1.4': - resolution: {integrity: sha512-KXMGoboq5cyaCQjDA4GLuRiOwBQ0EyFnJoVViLeZ45/3rFItRODEr+NdsBcVpll40hhNArlm/speWGRvj08LzA==} + '@rolldown/binding-openharmony-arm64@1.1.5': + resolution: {integrity: sha512-yc5WrLzXks6zCQfn9Oxr8pORKyl/pF+QjHmW/Qx3qu0oyrrNC+y2JLTU1E2rcWYAmzlnqngWXHQjy51VzW70Vw==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [openharmony] - '@rolldown/binding-wasm32-wasi@1.1.4': - resolution: {integrity: sha512-5K83rb36oJiY7BCyE9zLZtGcPV4g5wvq+xwdO0XPIwDVZI8cyB/AUjkNXGb92/rnmezEkjMOpgY61rtwjQtFwg==} + '@rolldown/binding-wasm32-wasi@1.1.5': + resolution: {integrity: sha512-VbQGPX2b4r48TAMIM2cjgluIM1HYutm4pcTEJsle7iEP7sB1dFqtPLBVbdLAZCxy1txCcPxf4QFf4v8uvltPqA==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [wasm32] - '@rolldown/binding-win32-arm64-msvc@1.1.4': - resolution: {integrity: sha512-PnWBtw3TV5KOg69HQQDR0mnQuyCmSGR2pAB4DC1rPF808fgKeTUMj2EOEyKATpgiuxuR5APQmiDO7PDgEjTFSA==} + '@rolldown/binding-win32-arm64-msvc@1.1.5': + resolution: {integrity: sha512-gHv82k63z4qpV5+Q1y/12KrK0ltWBukVDI8nZcbT7Tt/ZlOIVwppazneq0F93oDxTo3IgAMEDIoQh3E2n6mVsw==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [win32] - '@rolldown/binding-win32-x64-msvc@1.1.4': - resolution: {integrity: sha512-M1lpniBePobTfsa7Ks9a199e1akxsXn+GYBUKsEzv3YFzOm1HJAMNwKI3qr0Zq+mxwx9gOZoTdP1yXRYsZUocQ==} + '@rolldown/binding-win32-x64-msvc@1.1.5': + resolution: {integrity: sha512-tTZuDBPw85tEN5PQi1pnEBzDy0Z49HtScLAbD5t6hyeU92A95pRWaSMw1GZZi/RwgSgUIl0xrSlXIT/9QzvYSA==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [win32] @@ -1303,20 +1303,20 @@ packages: babel-plugin-react-compiler: optional: true - '@vitest/coverage-v8@4.1.9': - resolution: {integrity: sha512-G9/lgqibheLVBDRuya45EbsEXTYcWoSG+TLg7i2axuzx0Eq62eXn+aWXyaVdV5vKvFSWd6ywcX8hA7la9Pvu8g==} + '@vitest/coverage-v8@4.1.10': + resolution: {integrity: sha512-IM49HmthevbgAO4anp1hwtoT9wYe59w0LR00gr+eagHE+ZJ5lK4sLPeO0ubgoJcwLk6dehU3R24N+FbEEKDc8g==} peerDependencies: - '@vitest/browser': 4.1.9 - vitest: 4.1.9 + '@vitest/browser': 4.1.10 + vitest: 4.1.10 peerDependenciesMeta: '@vitest/browser': optional: true - '@vitest/expect@4.1.9': - resolution: {integrity: sha512-vl/rYsUKcBr3SnQn166+XR5ZQcgMx3DQhFWdfli/cWpLnLUmbxZvyrJZotLFUryib+LtArYMSTJ5RbQ57ZqrlA==} + '@vitest/expect@4.1.10': + resolution: {integrity: sha512-YsCn+qAk1GWjQOWFEsEcL2gNQ0zmVmQu3T03qP6UyjhtmdtwtbuI+DASn/7iQB3HGTXkdBwGddzxPlmiql5vlA==} - '@vitest/mocker@4.1.9': - resolution: {integrity: sha512-EVkXzBjrPGM+cK8/ANWgBrkUCfJfb38/EfTSO8h7pWvKkyPkpWxvR7BkD2MyItMF62C97zAEoqdpUixwR/e+Rw==} + '@vitest/mocker@4.1.10': + resolution: {integrity: sha512-v0xaezt+DKEmKfaxg133ldzADrwLGd7Ze1MfQQTYfvs8OqZIwbxyxaYURivwV7sWy5fqn3rH5uOrSp07bp44Ow==} peerDependencies: msw: ^2.4.9 vite: ^6.0.0 || ^7.0.0 || ^8.0.0 @@ -1326,23 +1326,23 @@ packages: vite: optional: true - '@vitest/pretty-format@4.1.9': - resolution: {integrity: sha512-s0iufns3iIFitdgm+YR7g1whCAaGtXz459VS9/PqyKDEEFgYIhsHOQmXgIgDuYCt7DeQmiZT0Qe2OA2p4ZPu5A==} + '@vitest/pretty-format@4.1.10': + resolution: {integrity: sha512-W1HsjSH4MXQ9YfmmhLAoIYf1HRfekQCGngeIgcei6MP5QQGWUe0gkopdZQaVCFO+JDJMrAJGwa5pRpNpvy4P8Q==} - '@vitest/runner@4.1.9': - resolution: {integrity: sha512-KXLMDtc7oe70+3mJfGrPUWPesswH+3sTxAMAMl8DG7I8IUQT4XW718dY5ID3vPUcmlu27CcKfY4P3h3I29SLJg==} + '@vitest/runner@4.1.10': + resolution: {integrity: sha512-IKI6kpIH+LmpROplyLwBBaCfMgOZOMsygVa6BARD6ahA04VRuJSa6OaVG7kRvSEMD870Vd91rSSw0eegtWyLGg==} - '@vitest/snapshot@4.1.9': - resolution: {integrity: sha512-Jc7RKGNBo8Z28WYIm0Niej4xdSPByRf6mU58VpHQkd6Zh05rlnA+twjbK5HyeIGHxrzsc3mJgS43uM0CZKzaIA==} + '@vitest/snapshot@4.1.10': + resolution: {integrity: sha512-xRkfOT1qpTAi/Ti4Y1LtfRc3kEuqxGw59eN2jN9pRWMtS/XDevekhcFSqvQqjUNGksfjMJu3Y+oJ+4Ypn2OaJw==} - '@vitest/spy@4.1.9': - resolution: {integrity: sha512-fHpsS6mIi+PiEW+vcRVOMkX1oSaPKne3VOclSFICPcGOmfKgXPU5iAah+wcNcj2xPrCCmfq99IDGf+EojhhvhA==} + '@vitest/spy@4.1.10': + resolution: {integrity: sha512-PLf/Ugvoq5wO/b4rwYCR1h2PSIdXz7wnkQFMiUpLdtM7l6pqVFcQIBEHyT1+l+cj7mNwAfZHzqXqDyjvOuwbDw==} - '@vitest/utils@4.1.9': - resolution: {integrity: sha512-A51o8ymO5PpqlWNnBP9ZHPXDIpuMtTLlGSjN7la4US+LJzoUMyhwjA5QXlm39JexgwHKW4Xjs8Z2d3dLCXOeuA==} + '@vitest/utils@4.1.10': + resolution: {integrity: sha512-fy9am/HWxbaGt/Sawrp90vt6Y6jQwf1RX77cz3uwoJwJVMli/e1IEwRPnMNJ7vKfPTwo0diXifkpPvwH9v7nGA==} - '@xyflow/react@12.11.1': - resolution: {integrity: sha512-L+zBoLGSXham0MnlY8QqjfR7/C5JNw0zxkaey5aZ5XmCgJBAdH4+WRIu8CR40d3l/BdU635V6YbhBK1jMo8/6Q==} + '@xyflow/react@12.11.2': + resolution: {integrity: sha512-eLAlDWJfWnQEhJwGMjlWdAXO9eYllKpliUmPQlAmOLxz6mExXuzMVDUKLMquixgkrtmMFFtug3jGKmYYld12cA==} peerDependencies: '@types/react': '>=17' '@types/react-dom': '>=17' @@ -1354,8 +1354,8 @@ packages: '@types/react-dom': optional: true - '@xyflow/system@0.0.78': - resolution: {integrity: sha512-lY0z2qP33fUhTva9Vaxrk0lqZta2pkbxB1trHAx1omnJqRtPvDlAQYV2r5fhS6AdpkulYmbNW0svy+A4/t4B/g==} + '@xyflow/system@0.0.79': + resolution: {integrity: sha512-czLyOh91NF0hIzbNzwi8I6GlqG23BHh2435OddfI6uiaLH3xdrdygO93gqgH1Bv9mhy8XPFQJOBn1FTq4LvEWA==} '@zag-js/accordion@1.41.2': resolution: {integrity: sha512-7G//V7svGGT8k5avw7bbQvbRC0Q/9QtX51b4iyAB1alR9E5mFd6Ch8q4njwcXClMQ7xePS3jUfVnzVGiRInEiQ==} @@ -2985,8 +2985,8 @@ packages: lowlight@1.20.0: resolution: {integrity: sha512-8Ktj+prEb1RoCPkEOrPMYUN/nCggB7qAWe3a7OpMjWQkh3l2RD5wKRQ+o8Q8YuI9RG/xs95waaI/E6ym/7NsTw==} - lru-cache@11.5.1: - resolution: {integrity: sha512-RPimw/7aMdv2oqRrxKwvZXcPfwBrn/JZ2xYcY9Hus/6LaS3VOAKVWKWgNLCFSiOm1ESXinjsDlidVU7JlnCN2A==} + lru-cache@11.5.2: + resolution: {integrity: sha512-4pfM1Ff0x50o0tQwb5ucw/RzNyD0/YJME6IVcStalZuMWxdt3sR3huStTtxz4PUmvZfRguvDejasvQ2kifR11g==} engines: {node: 20 || >=22} lru-cache@5.1.1: @@ -3460,8 +3460,8 @@ packages: peerDependencies: react: ^19.2.7 - react-hook-form@7.80.0: - resolution: {integrity: sha512-4P+fk6oXsxY+6xSj7Euhc2sumQD8zQqCuVHoJwoyp9EchP+IUW9OESB7uHFJOKsIBQ4MQqYE84INJFqUCYNoOg==} + react-hook-form@7.81.0: + resolution: {integrity: sha512-ocbmr2p5KBMoAfj4WCUvped33lVi1Kd5DuDUvQDnB6VEAacOjPI/jMbtDdbhco4y9ct4xUuCmMY0b/C9L0QHjw==} engines: {node: '>=18.0.0'} peerDependencies: react: ^16.8.0 || ^17 || ^18 || ^19 @@ -3624,8 +3624,8 @@ packages: robust-predicates@3.0.2: resolution: {integrity: sha512-IXgzBWvWQwE6PrDI05OvmXUIruQTcoMDzRsOd5CDvHCVLcLHMTSYvOK5Cm46kWqlV3yAbuSpBZdJ5oP5OUoStg==} - rolldown@1.1.4: - resolution: {integrity: sha512-IjZYiLxZwpnhwhdBH2ugdTGVSdhCQUmLxLoqyjiL0JxYjyRst+5a0P3xfrTxJ5F638j4Mvvw5FAX5XE6eHpXbA==} + rolldown@1.1.5: + resolution: {integrity: sha512-t9z29cJjXf/vxQ8dyhCSpt6H6aSwHTk8cT5I3iy6SMXuFpk5mB6PL6XfC8PCwrPTx93udwKUm9HRteAlTGBLiA==} engines: {node: ^20.19.0 || >=22.12.0} hasBin: true @@ -3738,8 +3738,8 @@ packages: resolution: {integrity: sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==} engines: {node: '>= 0.8'} - std-env@4.1.0: - resolution: {integrity: sha512-Rq7ybcX2RuC55r9oaPVEW7/xu3tj8u4GeBYHBWCychFtzMIr86A7e3PPEBPT37sHStKX3+TiX/Fr/ACmJLVlLQ==} + std-env@4.2.0: + resolution: {integrity: sha512-oCUKSupKTHX53EyjDtuZQ64pjLJ6yYCtpmEw0goYxtjG9KpbRe8KAsl2tBUGU9DyMcJ0RwJ8GqJAFzMXcXW1Rw==} strict-event-emitter@0.5.1: resolution: {integrity: sha512-vMgjE/GGEPEFnhFub6pa4FmJBRBVOLpIII2hvCZ8Kzb7K0hlHo7mQv6xYrBvCL2LtAIBwFUK8wvuJgTVSQ5MFQ==} @@ -4000,8 +4000,8 @@ packages: peerDependencies: vite: '>2.0.0-0' - vite@8.1.2: - resolution: {integrity: sha512-6YYPbRXTxx6bRXmOn7XdnQAy5DQNHhDgtjhDHI13oe4pY93kkcdGJWxpGwOm++/Wh0QpQhDrpIoVMrmrsI5AGQ==} + vite@8.1.3: + resolution: {integrity: sha512-Ds+gBRbj0lwRO2Y5hwnUBdxSwlAve9LeRyU4sNnAr0ewW0gWF0n5bgXgUzbgZ49MV9BVUAQUFYVcDUcilUExMA==} engines: {node: ^20.19.0 || >=22.12.0} hasBin: true peerDependencies: @@ -4043,20 +4043,20 @@ packages: yaml: optional: true - vitest@4.1.9: - resolution: {integrity: sha512-nE3/LEyc0z87uHYLZebqCUOaJr2hdtuPp7BQ4BosVFnfltxgAvMG08NyrSGlPpOUWvR27c5flSmYFTNr78L9GQ==} + vitest@4.1.10: + resolution: {integrity: sha512-R9jUTe5S4Qb0HCd4TNqpC7oGcrMssMRGXLW80ubjWsW9VH5GF8y1Y0SFLY9AbqSk6nt0PnOx4H4WNJYZ13GUPw==} engines: {node: ^20.0.0 || ^22.0.0 || >=24.0.0} hasBin: true peerDependencies: '@edge-runtime/vm': '*' '@opentelemetry/api': ^1.9.0 '@types/node': ^20.0.0 || ^22.0.0 || >=24.0.0 - '@vitest/browser-playwright': 4.1.9 - '@vitest/browser-preview': 4.1.9 - '@vitest/browser-webdriverio': 4.1.9 - '@vitest/coverage-istanbul': 4.1.9 - '@vitest/coverage-v8': 4.1.9 - '@vitest/ui': 4.1.9 + '@vitest/browser-playwright': 4.1.10 + '@vitest/browser-preview': 4.1.10 + '@vitest/browser-webdriverio': 4.1.10 + '@vitest/coverage-istanbul': 4.1.10 + '@vitest/coverage-v8': 4.1.10 + '@vitest/ui': 4.1.10 happy-dom: '>=20.8.8' jsdom: '*' vite: ^6.0.0 || ^7.0.0 || ^8.0.0 @@ -4823,7 +4823,7 @@ snapshots: '@ota-meshi/ast-token-store@0.3.0': {} - '@oxc-project/types@0.138.0': {} + '@oxc-project/types@0.139.0': {} '@pandacss/is-valid-prop@1.11.4': {} @@ -4833,63 +4833,63 @@ snapshots: dependencies: playwright: 1.61.1 - '@rolldown/binding-android-arm64@1.1.4': + '@rolldown/binding-android-arm64@1.1.5': optional: true - '@rolldown/binding-darwin-arm64@1.1.4': + '@rolldown/binding-darwin-arm64@1.1.5': optional: true - '@rolldown/binding-darwin-x64@1.1.4': + '@rolldown/binding-darwin-x64@1.1.5': optional: true - '@rolldown/binding-freebsd-x64@1.1.4': + '@rolldown/binding-freebsd-x64@1.1.5': optional: true - '@rolldown/binding-linux-arm-gnueabihf@1.1.4': + '@rolldown/binding-linux-arm-gnueabihf@1.1.5': optional: true - '@rolldown/binding-linux-arm64-gnu@1.1.4': + '@rolldown/binding-linux-arm64-gnu@1.1.5': optional: true - '@rolldown/binding-linux-arm64-musl@1.1.4': + '@rolldown/binding-linux-arm64-musl@1.1.5': optional: true - '@rolldown/binding-linux-ppc64-gnu@1.1.4': + '@rolldown/binding-linux-ppc64-gnu@1.1.5': optional: true - '@rolldown/binding-linux-s390x-gnu@1.1.4': + '@rolldown/binding-linux-s390x-gnu@1.1.5': optional: true - '@rolldown/binding-linux-x64-gnu@1.1.4': + '@rolldown/binding-linux-x64-gnu@1.1.5': optional: true - '@rolldown/binding-linux-x64-musl@1.1.4': + '@rolldown/binding-linux-x64-musl@1.1.5': optional: true - '@rolldown/binding-openharmony-arm64@1.1.4': + '@rolldown/binding-openharmony-arm64@1.1.5': optional: true - '@rolldown/binding-wasm32-wasi@1.1.4': + '@rolldown/binding-wasm32-wasi@1.1.5': dependencies: '@emnapi/core': 1.11.1 '@emnapi/runtime': 1.11.1 '@napi-rs/wasm-runtime': 1.1.6(@emnapi/core@1.11.1)(@emnapi/runtime@1.11.1) optional: true - '@rolldown/binding-win32-arm64-msvc@1.1.4': + '@rolldown/binding-win32-arm64-msvc@1.1.5': optional: true - '@rolldown/binding-win32-x64-msvc@1.1.4': + '@rolldown/binding-win32-x64-msvc@1.1.5': optional: true - '@rolldown/plugin-babel@0.2.3(@babel/core@7.29.0)(@babel/runtime@7.29.7)(rolldown@1.1.4)(vite@8.1.2(@types/node@24.13.2)(jiti@1.21.7)(yaml@2.9.0))': + '@rolldown/plugin-babel@0.2.3(@babel/core@7.29.0)(@babel/runtime@7.29.7)(rolldown@1.1.5)(vite@8.1.3(@types/node@24.13.2)(jiti@1.21.7)(yaml@2.9.0))': dependencies: '@babel/core': 7.29.0 picomatch: 4.0.5 - rolldown: 1.1.4 + rolldown: 1.1.5 optionalDependencies: '@babel/runtime': 7.29.7 - vite: 8.1.2(@types/node@24.13.2)(jiti@1.21.7)(yaml@2.9.0) + vite: 8.1.3(@types/node@24.13.2)(jiti@1.21.7)(yaml@2.9.0) '@rolldown/pluginutils@1.0.1': {} @@ -5339,81 +5339,81 @@ snapshots: d3-time-format: 4.1.0 internmap: 2.0.3 - '@vitejs/plugin-react-swc@4.3.1(@swc/helpers@0.5.23)(vite@8.1.2(@types/node@24.13.2)(jiti@1.21.7)(yaml@2.9.0))': + '@vitejs/plugin-react-swc@4.3.1(@swc/helpers@0.5.23)(vite@8.1.3(@types/node@24.13.2)(jiti@1.21.7)(yaml@2.9.0))': dependencies: '@rolldown/pluginutils': 1.0.1 '@swc/core': 1.15.43(@swc/helpers@0.5.23) - vite: 8.1.2(@types/node@24.13.2)(jiti@1.21.7)(yaml@2.9.0) + vite: 8.1.3(@types/node@24.13.2)(jiti@1.21.7)(yaml@2.9.0) transitivePeerDependencies: - '@swc/helpers' - '@vitejs/plugin-react@6.0.3(@rolldown/plugin-babel@0.2.3(@babel/core@7.29.0)(@babel/runtime@7.29.7)(rolldown@1.1.4)(vite@8.1.2(@types/node@24.13.2)(jiti@1.21.7)(yaml@2.9.0)))(babel-plugin-react-compiler@1.0.0)(vite@8.1.2(@types/node@24.13.2)(jiti@1.21.7)(yaml@2.9.0))': + '@vitejs/plugin-react@6.0.3(@rolldown/plugin-babel@0.2.3(@babel/core@7.29.0)(@babel/runtime@7.29.7)(rolldown@1.1.5)(vite@8.1.3(@types/node@24.13.2)(jiti@1.21.7)(yaml@2.9.0)))(babel-plugin-react-compiler@1.0.0)(vite@8.1.3(@types/node@24.13.2)(jiti@1.21.7)(yaml@2.9.0))': dependencies: '@rolldown/pluginutils': 1.0.1 - vite: 8.1.2(@types/node@24.13.2)(jiti@1.21.7)(yaml@2.9.0) + vite: 8.1.3(@types/node@24.13.2)(jiti@1.21.7)(yaml@2.9.0) optionalDependencies: - '@rolldown/plugin-babel': 0.2.3(@babel/core@7.29.0)(@babel/runtime@7.29.7)(rolldown@1.1.4)(vite@8.1.2(@types/node@24.13.2)(jiti@1.21.7)(yaml@2.9.0)) + '@rolldown/plugin-babel': 0.2.3(@babel/core@7.29.0)(@babel/runtime@7.29.7)(rolldown@1.1.5)(vite@8.1.3(@types/node@24.13.2)(jiti@1.21.7)(yaml@2.9.0)) babel-plugin-react-compiler: 1.0.0 - '@vitest/coverage-v8@4.1.9(vitest@4.1.9)': + '@vitest/coverage-v8@4.1.10(vitest@4.1.10)': dependencies: '@bcoe/v8-coverage': 1.0.2 - '@vitest/utils': 4.1.9 + '@vitest/utils': 4.1.10 ast-v8-to-istanbul: 1.0.4 istanbul-lib-coverage: 3.2.2 istanbul-lib-report: 3.0.1 istanbul-reports: 3.2.0 magicast: 0.5.3 obug: 2.1.3 - std-env: 4.1.0 + std-env: 4.2.0 tinyrainbow: 3.1.0 - vitest: 4.1.9(@types/node@24.13.2)(@vitest/coverage-v8@4.1.9)(happy-dom@20.10.6)(msw@2.14.6(@types/node@24.13.2)(typescript@6.0.3))(vite@8.1.2(@types/node@24.13.2)(jiti@1.21.7)(yaml@2.9.0)) + vitest: 4.1.10(@types/node@24.13.2)(@vitest/coverage-v8@4.1.10)(happy-dom@20.10.6)(msw@2.14.6(@types/node@24.13.2)(typescript@6.0.3))(vite@8.1.3(@types/node@24.13.2)(jiti@1.21.7)(yaml@2.9.0)) - '@vitest/expect@4.1.9': + '@vitest/expect@4.1.10': dependencies: '@standard-schema/spec': 1.1.0 '@types/chai': 5.2.3 - '@vitest/spy': 4.1.9 - '@vitest/utils': 4.1.9 + '@vitest/spy': 4.1.10 + '@vitest/utils': 4.1.10 chai: 6.2.2 tinyrainbow: 3.1.0 - '@vitest/mocker@4.1.9(msw@2.14.6(@types/node@24.13.2)(typescript@6.0.3))(vite@8.1.2(@types/node@24.13.2)(jiti@1.21.7)(yaml@2.9.0))': + '@vitest/mocker@4.1.10(msw@2.14.6(@types/node@24.13.2)(typescript@6.0.3))(vite@8.1.3(@types/node@24.13.2)(jiti@1.21.7)(yaml@2.9.0))': dependencies: - '@vitest/spy': 4.1.9 + '@vitest/spy': 4.1.10 estree-walker: 3.0.3 magic-string: 0.30.21 optionalDependencies: msw: 2.14.6(@types/node@24.13.2)(typescript@6.0.3) - vite: 8.1.2(@types/node@24.13.2)(jiti@1.21.7)(yaml@2.9.0) + vite: 8.1.3(@types/node@24.13.2)(jiti@1.21.7)(yaml@2.9.0) - '@vitest/pretty-format@4.1.9': + '@vitest/pretty-format@4.1.10': dependencies: tinyrainbow: 3.1.0 - '@vitest/runner@4.1.9': + '@vitest/runner@4.1.10': dependencies: - '@vitest/utils': 4.1.9 + '@vitest/utils': 4.1.10 pathe: 2.0.3 - '@vitest/snapshot@4.1.9': + '@vitest/snapshot@4.1.10': dependencies: - '@vitest/pretty-format': 4.1.9 - '@vitest/utils': 4.1.9 + '@vitest/pretty-format': 4.1.10 + '@vitest/utils': 4.1.10 magic-string: 0.30.21 pathe: 2.0.3 - '@vitest/spy@4.1.9': {} + '@vitest/spy@4.1.10': {} - '@vitest/utils@4.1.9': + '@vitest/utils@4.1.10': dependencies: - '@vitest/pretty-format': 4.1.9 + '@vitest/pretty-format': 4.1.10 convert-source-map: 2.0.0 tinyrainbow: 3.1.0 - '@xyflow/react@12.11.1(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)': + '@xyflow/react@12.11.2(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)': dependencies: - '@xyflow/system': 0.0.78 + '@xyflow/system': 0.0.79 classcat: 5.0.5 react: 19.2.7 react-dom: 19.2.7(react@19.2.7) @@ -5424,7 +5424,7 @@ snapshots: transitivePeerDependencies: - immer - '@xyflow/system@0.0.78': + '@xyflow/system@0.0.79': dependencies: '@types/d3-drag': 3.0.7 '@types/d3-interpolate': 3.0.4 @@ -7525,7 +7525,7 @@ snapshots: fault: 1.0.4 highlight.js: 10.7.3 - lru-cache@11.5.1: {} + lru-cache@11.5.2: {} lru-cache@5.1.1: dependencies: @@ -8127,7 +8127,7 @@ snapshots: path-scurry@2.0.2: dependencies: - lru-cache: 11.5.1 + lru-cache: 11.5.2 minipass: 7.1.3 path-to-regexp@6.3.0: {} @@ -8225,7 +8225,7 @@ snapshots: react: 19.2.7 scheduler: 0.27.0 - react-hook-form@7.80.0(react@19.2.7): + react-hook-form@7.81.0(react@19.2.7): dependencies: react: 19.2.7 @@ -8432,26 +8432,26 @@ snapshots: robust-predicates@3.0.2: {} - rolldown@1.1.4: + rolldown@1.1.5: dependencies: - '@oxc-project/types': 0.138.0 + '@oxc-project/types': 0.139.0 '@rolldown/pluginutils': 1.0.1 optionalDependencies: - '@rolldown/binding-android-arm64': 1.1.4 - '@rolldown/binding-darwin-arm64': 1.1.4 - '@rolldown/binding-darwin-x64': 1.1.4 - '@rolldown/binding-freebsd-x64': 1.1.4 - '@rolldown/binding-linux-arm-gnueabihf': 1.1.4 - '@rolldown/binding-linux-arm64-gnu': 1.1.4 - '@rolldown/binding-linux-arm64-musl': 1.1.4 - '@rolldown/binding-linux-ppc64-gnu': 1.1.4 - '@rolldown/binding-linux-s390x-gnu': 1.1.4 - '@rolldown/binding-linux-x64-gnu': 1.1.4 - '@rolldown/binding-linux-x64-musl': 1.1.4 - '@rolldown/binding-openharmony-arm64': 1.1.4 - '@rolldown/binding-wasm32-wasi': 1.1.4 - '@rolldown/binding-win32-arm64-msvc': 1.1.4 - '@rolldown/binding-win32-x64-msvc': 1.1.4 + '@rolldown/binding-android-arm64': 1.1.5 + '@rolldown/binding-darwin-arm64': 1.1.5 + '@rolldown/binding-darwin-x64': 1.1.5 + '@rolldown/binding-freebsd-x64': 1.1.5 + '@rolldown/binding-linux-arm-gnueabihf': 1.1.5 + '@rolldown/binding-linux-arm64-gnu': 1.1.5 + '@rolldown/binding-linux-arm64-musl': 1.1.5 + '@rolldown/binding-linux-ppc64-gnu': 1.1.5 + '@rolldown/binding-linux-s390x-gnu': 1.1.5 + '@rolldown/binding-linux-x64-gnu': 1.1.5 + '@rolldown/binding-linux-x64-musl': 1.1.5 + '@rolldown/binding-openharmony-arm64': 1.1.5 + '@rolldown/binding-wasm32-wasi': 1.1.5 + '@rolldown/binding-win32-arm64-msvc': 1.1.5 + '@rolldown/binding-win32-x64-msvc': 1.1.5 safe-array-concat@1.1.3: dependencies: @@ -8562,7 +8562,7 @@ snapshots: statuses@2.0.2: {} - std-env@4.1.0: {} + std-env@4.2.0: {} strict-event-emitter@0.5.1: {} @@ -8861,16 +8861,16 @@ snapshots: '@types/unist': 3.0.3 vfile-message: 4.0.2 - vite-plugin-css-injected-by-js@3.5.2(vite@8.1.2(@types/node@24.13.2)(jiti@1.21.7)(yaml@2.9.0)): + vite-plugin-css-injected-by-js@3.5.2(vite@8.1.3(@types/node@24.13.2)(jiti@1.21.7)(yaml@2.9.0)): dependencies: - vite: 8.1.2(@types/node@24.13.2)(jiti@1.21.7)(yaml@2.9.0) + vite: 8.1.3(@types/node@24.13.2)(jiti@1.21.7)(yaml@2.9.0) - vite@8.1.2(@types/node@24.13.2)(jiti@1.21.7)(yaml@2.9.0): + vite@8.1.3(@types/node@24.13.2)(jiti@1.21.7)(yaml@2.9.0): dependencies: lightningcss: 1.32.0 picomatch: 4.0.5 postcss: 8.5.16 - rolldown: 1.1.4 + rolldown: 1.1.5 tinyglobby: 0.2.17 optionalDependencies: '@types/node': 24.13.2 @@ -8878,31 +8878,31 @@ snapshots: jiti: 1.21.7 yaml: 2.9.0 - vitest@4.1.9(@types/node@24.13.2)(@vitest/coverage-v8@4.1.9)(happy-dom@20.10.6)(msw@2.14.6(@types/node@24.13.2)(typescript@6.0.3))(vite@8.1.2(@types/node@24.13.2)(jiti@1.21.7)(yaml@2.9.0)): + vitest@4.1.10(@types/node@24.13.2)(@vitest/coverage-v8@4.1.10)(happy-dom@20.10.6)(msw@2.14.6(@types/node@24.13.2)(typescript@6.0.3))(vite@8.1.3(@types/node@24.13.2)(jiti@1.21.7)(yaml@2.9.0)): dependencies: - '@vitest/expect': 4.1.9 - '@vitest/mocker': 4.1.9(msw@2.14.6(@types/node@24.13.2)(typescript@6.0.3))(vite@8.1.2(@types/node@24.13.2)(jiti@1.21.7)(yaml@2.9.0)) - '@vitest/pretty-format': 4.1.9 - '@vitest/runner': 4.1.9 - '@vitest/snapshot': 4.1.9 - '@vitest/spy': 4.1.9 - '@vitest/utils': 4.1.9 + '@vitest/expect': 4.1.10 + '@vitest/mocker': 4.1.10(msw@2.14.6(@types/node@24.13.2)(typescript@6.0.3))(vite@8.1.3(@types/node@24.13.2)(jiti@1.21.7)(yaml@2.9.0)) + '@vitest/pretty-format': 4.1.10 + '@vitest/runner': 4.1.10 + '@vitest/snapshot': 4.1.10 + '@vitest/spy': 4.1.10 + '@vitest/utils': 4.1.10 es-module-lexer: 2.3.0 expect-type: 1.4.0 magic-string: 0.30.21 obug: 2.1.3 pathe: 2.0.3 picomatch: 4.0.5 - std-env: 4.1.0 + std-env: 4.2.0 tinybench: 2.9.0 tinyexec: 1.2.4 tinyglobby: 0.2.17 tinyrainbow: 3.1.0 - vite: 8.1.2(@types/node@24.13.2)(jiti@1.21.7)(yaml@2.9.0) + vite: 8.1.3(@types/node@24.13.2)(jiti@1.21.7)(yaml@2.9.0) why-is-node-running: 2.3.0 optionalDependencies: '@types/node': 24.13.2 - '@vitest/coverage-v8': 4.1.9(vitest@4.1.9) + '@vitest/coverage-v8': 4.1.10(vitest@4.1.10) happy-dom: 20.10.6 transitivePeerDependencies: - msw From 0a76609e0c21617c472cc0cea13f0c7774b524d3 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Mon, 13 Jul 2026 01:58:02 +0200 Subject: [PATCH 070/297] [v3-3-test] Support delegating providers release process to non-PMC committers (#69417) (#69634) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Add framework for delegating providers release to non-PMC committers The provider release cycle currently assumes a PMC member drives every step, yet ASF policy only reserves the binding vote and dist/release publication to the PMC. Documenting a clear split lets a non-PMC committer run the bulk of the process while the PMC signs, publishes to PyPI, and casts the binding votes — both spreading the release-manager load and giving prospective PMC members a supervised on-ramp. Generated-by: Claude Code (Opus 4.8 1M context) * Merge the provider build step into the PMC-owned release block A PMC member who only signs artifacts built by someone else does not actually know what they are signing, per ASF release policy guidance on owned/controlled hardware. Move the build step into the PMC block so the same PMC member builds, signs, commits to dist/dev, and publishes the PyPI RC without handing control back to the Delegate. (cherry picked from commit 4d56e6c93dd8c0ff69c603789a837abcea77f7d8) Co-authored-by: Shahar Epstein <60007259+shahar1@users.noreply.github.com> --- dev/README_RELEASE_PROVIDERS.md | 112 +++++++++++++++++++++++++++++++- 1 file changed, 111 insertions(+), 1 deletion(-) diff --git a/dev/README_RELEASE_PROVIDERS.md b/dev/README_RELEASE_PROVIDERS.md index eab74f00b42ba..0237a4342a3bf 100644 --- a/dev/README_RELEASE_PROVIDERS.md +++ b/dev/README_RELEASE_PROVIDERS.md @@ -23,6 +23,7 @@ - [Intro](#intro) - [What the provider distributions are](#what-the-provider-distributions-are) - [Decide when to release](#decide-when-to-release) + - [Delegating release duties to a non-PMC committer](#delegating-release-duties-to-a-non-pmc-committer) - [Collect ambiguities during the release (for a follow-up doc PR)](#collect-ambiguities-during-the-release-for-a-follow-up-doc-pr) - [Special procedures (done very infrequently)](#special-procedures-done-very-infrequently) - [Bump min Airflow version for providers](#bump-min-airflow-version-for-providers) @@ -87,6 +88,47 @@ a given provider needs to be released due to new features or due to bug fixes. package separately, but due to voting and release overhead we try to group releases of Provider distributions together. +## Delegating release duties to a non-PMC committer + +Per the [ASF release policy](http://www.apache.org/legal/release-policy.html), the Release Manager +does not need to be a PMC member, and there is no requirement that only a PMC member may call a +release vote. The policy's own wording: *"If the Release Manager is not a member of the PMC, they +will need to ask a PMC member to do the actual release publication"* — i.e. the one hard boundary +is write access to the `dist/release` SVN area and the binding vote itself; everything else can be +run by any committer. + +This means a non-PMC committer (the **Delegate** below) can run most of the provider release +process end to end, with a PMC member only stepping in for the parts ASF policy reserves to the +PMC. Split of duties: + +| Step | Owner | Notes | +|---|---|---| +| [Convert commits to changelog entries and bump provider versions](#convert-commits-to-changelog-entries-and-bump-provider-versions) | Delegate | Normal PR review/merge process, no PMC involvement needed. | +| [Build](#build-provider-distributions-for-svn-apache-upload) + [sign](#build-and-sign-the-source-and-convenience-packages) + [commit to `dist/dev`](#commit-the-source-packages-to-apache-svn-repo) + [publish RC to PyPI](#publish-the-regular-distributions-to-pypi-release-candidates) | PMC | Kept as **one contiguous PMC block**: per [ASF release policy](https://www.apache.org/legal/release-policy.html#owned-controlled-hardware), a PMC member signing a release should build it themselves from source rather than sign artifacts someone else built, so they know what they're actually signing. The PMC member builds, signs with their own key (already in the project's `KEYS` file), commits packages + signatures to `dist/dev`, and uploads the RC to the `apache-airflow-providers-*` PyPI namespace under the PMC's trusted publishing identity. The PMC then hands `files/packages.txt` (the PyPI URLs) back to the Delegate for the vote email. | +| [Push the RC tags](#push-the-rc-tags) | Delegate | Plain git tag push, no elevated access needed. | +| [Prepare documentation in Staging](#prepare-documentation-in-staging) | Delegate | | +| [Prepare issue in GitHub to keep status of testing](#prepare-issue-in-github-to-keep-status-of-testing) | Delegate | Delegate also tracks the issue, the vote thread, and related PRs throughout the release. | +| [Prepare voting email for Providers release candidate](#prepare-voting-email-for-providers-release-candidate) | Delegate | Delegate may send the `[VOTE]` email, but must **not** claim a personal binding `+1` (see note in that section) — only PMC votes are binding. | +| Casting the deciding vote(s) | PMC | At least 3 binding `+1` votes from PMC members are required for the release to pass; this cannot be delegated. | +| [Summarize the voting for the Apache Airflow release](#summarize-the-voting-for-the-apache-airflow-release) (`[RESULT][VOTE]`) | Delegate | Only after at least 3 binding PMC `+1` votes are already visible in the thread — the Delegate is reporting a result the PMC already reached, not deciding it. | +| [Publish release to SVN](#publish-release-to-svn) (`dist/release`) | PMC | Per [ASF Infra policy](https://infra.apache.org/release-publishing), `dist/release` write access is PMC-only by default (a project can request Infra to open it to all committers, but Airflow has not done so). | +| [Publish the packages to PyPI](#publish-the-packages-to-pypi) (final) | PMC | | +| [Add the final release tag in git](#add-the-final-release-tag-in-git) | Either | Not privileged; whoever is running that phase of the process does it. | +| [Publish documentation](#publish-documentation) (live) | Delegate | | +| [Update providers metadata](#update-providers-metadata) | Delegate | | +| [Notify developers of release](#notify-developers-of-release), security announcements, social media, committee report | PMC | Official project communications made under the PMC's authority. | +| [Close the testing status issue](#close-the-testing-status-issue) | Delegate | | + +The PMC continues to oversee the overall process regardless of how many steps are delegated, and +remains the party accountable for the release under ASF policy. + +> [!NOTE] +> Delegation is also a runway toward PMC membership. The first time a committer takes on the +> Delegate role, the overseeing PMC member is encouraged to walk them through the reserved block +> live — sharing their screen (or pairing) through the build → sign → `dist/dev` → PyPI-RC steps so the +> Delegate sees exactly how it is done. The aim is simply that these steps are familiar rather than +> a surprise if and when the Delegate later becomes a PMC member and runs them for real. + # Collect ambiguities during the release (for a follow-up doc PR) These instructions are imperfect. Every release uncovers at least one command @@ -457,6 +499,14 @@ breeze release-management prepare-provider-documentation --include-removed-provi ## Build Provider distributions for SVN apache upload +> [!NOTE] +> Under the [delegated process](#delegating-release-duties-to-a-non-pmc-committer) this build step +> begins the **PMC block**, together with signing, the `dist/dev` commit, and the PyPI RC upload. +> Per [ASF release policy](https://www.apache.org/legal/release-policy.html#owned-controlled-hardware), +> a PMC member should build the release themselves before signing it, rather than sign artifacts +> built by someone else — otherwise they don't actually know what they're signing. So the PMC member +> who signs also runs the build below, instead of the Delegate handing over pre-built artifacts. + Those packages might get promoted to "final" packages by just renaming the files, so internally they should keep the final version number without the rc suffix, even if they are rc1/rc2/... candidates. @@ -513,6 +563,13 @@ key you want to use. ## Build and sign the source and convenience packages +> [!NOTE] +> Under the [delegated process](#delegating-release-duties-to-a-non-pmc-committer) this step +> continues the **PMC block** started at [Build Provider distributions](#build-provider-distributions-for-svn-apache-upload) +> — the same PMC member builds and signs with their own key (already in the project's `KEYS` +> file), then stays on through the `dist/dev` commit and the PyPI RC upload without handing control +> back. + * Cleanup dist folder: ```shell script @@ -568,6 +625,13 @@ check above steps to install them. ## Commit the source packages to Apache SVN repo +> [!NOTE] +> Under the [delegated process](#delegating-release-duties-to-a-non-pmc-committer) this is the middle +> of the **PMC block** — the same PMC member who signed commits the packages and signatures here and +> continues to the PyPI RC upload. (`dist/dev` is committer-writable per [ASF Infra +> policy](https://infra.apache.org/release-publishing), so a Delegate *could* do this step, but it is +> kept with the PMC to avoid bouncing control mid-way.) + * Push the artifacts to ASF dev dist repo ```shell script @@ -608,6 +672,14 @@ cd "$AIRFLOW_REPO_ROOT" ## Publish the Regular distributions to PyPI (release candidates) +> [!NOTE] +> Under the [delegated process](#delegating-release-duties-to-a-non-pmc-committer) this is the end of +> the **PMC block** (build → sign → `dist/dev` → PyPI RC) — all uploads to the `apache-airflow-providers-*` +> PyPI namespace, RC and final alike, go through the PMC's trusted publishing identity. When done, +> the PMC member hands the generated `files/packages.txt` (the PyPI URLs) back to the Delegate, who +> needs it for the vote email's completeness gate and body, and the Delegate resumes at [Push the RC +> tags](#push-the-rc-tags). + In order to publish release candidate to PyPI you just need to build and release packages. The packages should however contain the rcN suffix in the version file name but not internally in the package, so you need to use `--version-suffix` switch to prepare those packages. @@ -872,6 +944,13 @@ breeze release-management check-release-files providers --release-date "${RELEAS Send out a vote to the dev@airflow.apache.org mailing list. Here you can prepare text of the email. +> [!NOTE] +> If you are a non-PMC Delegate running this step under the [delegated +> process](#delegating-release-duties-to-a-non-pmc-committer), set `IS_RM_VOTE_BINDING=false` below +> — your vote is not binding under ASF policy. Ask a PMC member to reply to the vote thread with +> their own explicit `+1 (binding)` as soon as they've verified the release; the vote is not valid +> until at least 3 such binding replies are posted, regardless of who sent the `[VOTE]` email. + ```shell script export VOTE_DURATION_IN_HOURS=72 export IS_SHORTEN_VOTE=$([ $VOTE_DURATION_IN_HOURS -ge 72 ] && echo "false" || echo "true") @@ -883,6 +962,10 @@ else # Linux fi export RELEASE_MANAGER_NAME="TODO:RELEASE_MANAGER_NAME" export GITHUB_ISSUE_LINK="TODO:ISSUE_LINK" +# true if the PMC itself is running the vote, false for a non-PMC Delegate (see note above) +export IS_RM_VOTE_BINDING=true +export RM_VOTE_BINDING_TEXT=$([ "$IS_RM_VOTE_BINDING" = "true" ] && echo "binding" || echo "non-binding") +export NON_PMC_RM_TEXT=$([ "$IS_RM_VOTE_BINDING" = "true" ] && echo "" || echo "I am a non-PMC committer running this release under Airflow's delegated release process; a PMC member will cast the binding votes needed to pass it.") ``` subject: @@ -901,7 +984,8 @@ I have just cut the new wave Airflow Providers packages with release preparation which will last for $VOTE_DURATION_IN_HOURS hours - which means that it will end on $VOTE_END_TIME UTC and until 3 binding +1 votes have been received. $([ "$IS_SHORTEN_VOTE" = "true" ] && echo "${SHORTEN_VOTE_TEXT}" || echo "") -Consider this my (binding) +1. +Consider this my ($RM_VOTE_BINDING_TEXT) +1. +$([ -n "$NON_PMC_RM_TEXT" ] && echo "$NON_PMC_RM_TEXT" || echo "") @@ -1393,6 +1477,12 @@ echo "prepare release date is ${RELEASE_DATE}" Once the vote has been passed, you will need to send a result vote to dev@airflow.apache.org: +> [!NOTE] +> A Delegate may send this `[RESULT][VOTE]` email under the [delegated +> process](#delegating-release-duties-to-a-non-pmc-committer), but only after confirming at least 3 +> binding `+1` votes from PMC members are already present in the vote thread — the email reports a +> decision the PMC has already made, it does not make that decision. + In both subject and message update DATE OF RELEASE, FIRST/LAST NAMES and numbers). In case some providers were excluded, explain why they were excluded and what is the plan for them (otherwise remove the optional part of the message). There are two options for releasing @@ -1447,6 +1537,13 @@ EOF ## Publish release to SVN +> [!NOTE] +> Under the [delegated process](#delegating-release-duties-to-a-non-pmc-committer) this step is +> owned by the **PMC member** — moving artifacts into `dist/release` requires PMC write karma that a +> non-PMC Delegate does not have (see the ownership table). The Delegate should have already +> confirmed the vote passed and, if helpful, staged the exact `svn cp`/`clean-old-provider-artifacts` +> commands below for the PMC member to run. + The best way of doing this is to svn cp between the two repos (this avoids having to upload the binaries again, and gives a clearer history in the svn commit logs. @@ -1533,6 +1630,11 @@ This is simply by removing the relevant files locally. ## Publish the packages to PyPI +> [!NOTE] +> Under the [delegated process](#delegating-release-duties-to-a-non-pmc-committer) this final PyPI +> upload is owned by the **PMC member**, using the same trusted publishing identity as the RC upload +> — it is not delegated. + By that time the packages should be in your dist folder. ```shell script @@ -1717,6 +1819,14 @@ gh pr create --title "Update providers metadata ${current_date}" --web ## Notify developers of release +> [!NOTE] +> Under the [delegated process](#delegating-release-duties-to-a-non-pmc-committer) the official +> post-release communications — this announcement, the [security-issue +> announcements](#send-announcements-about-security-issues-fixed-in-the-release), [social +> media](#announce-about-the-release-in-social-media), and the [committee +> report](#add-release-data-to-apache-committee-report-helper) — are made by the **PMC member** +> under the PMC's authority. The Delegate can still draft the text and hand it over. + Notify users@airflow.apache.org (cc'ing dev@airflow.apache.org) that the artifacts have been published. From e6e90704be7665b510c5a293c75b2a23a55b0b19 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Mon, 13 Jul 2026 01:58:05 +0200 Subject: [PATCH 071/297] [v3-3-test] Clarify provider release verification steps (#69577) (#69618) While running through the steps today I noticed a few small things to fix: - Clarify wording provider release SVN check - Drop duplicate cd in verification steps (cherry picked from commit 478f25e10663bbe4bc748cc08fbced6da85db693) Co-authored-by: Niko Oliveira --- dev/README_RELEASE_PROVIDERS.md | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/dev/README_RELEASE_PROVIDERS.md b/dev/README_RELEASE_PROVIDERS.md index 0237a4342a3bf..5c1443cee2b91 100644 --- a/dev/README_RELEASE_PROVIDERS.md +++ b/dev/README_RELEASE_PROVIDERS.md @@ -1096,9 +1096,11 @@ cd asf-dist/dev/airflow export PATH_TO_AIRFLOW_SVN=$(pwd -P) ``` -Optionally you can use the `breeze release-management check-release-files` command -to verify that all expected files are present in SVN. This command will produce a `Dockerfile.pmc` which -may help with verifying installation of the packages. +Verify that all expected files are present in SVN. You can do this manually by inspecting the +directory listing against the file counts described above, but the recommended way is to run the +`breeze release-management check-release-files` command below, which checks completeness for you +(it is the same gate the release manager runs before sending the vote email). As a bonus it produces +a `Dockerfile.pmc` which helps with verifying installation of the packages. Once you have cloned/updated the SVN repository, copy the PyPi URLs shared in the email to a file called `packages.txt` in the `$AIRFLOW_REPO_ROOT/files` @@ -1132,7 +1134,7 @@ it means that the build has a verified provenance. How to verify it: -1) Change directory where your airflow sources are checked out +1) Change directory to where your airflow sources are checked out: ```shell cd "$AIRFLOW_REPO_ROOT" @@ -1141,7 +1143,6 @@ cd "$AIRFLOW_REPO_ROOT" 2) Check out the ``providers/YYYY-MM-DD`` tag: ```shell -cd "$AIRFLOW_REPO_ROOT" git fetch upstream --tags git checkout providers/${RELEASE_DATE} ``` From 91825dd552ead4b72d4ba301e09bc92909be8779 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Mon, 13 Jul 2026 02:28:01 +0200 Subject: [PATCH 072/297] [v3-3-test] Prevent scheduler crash when process/thread are missing from log format (#69402) (#69787) When a record reaches the percent formatter without callsite information, for example a stdlib warning routed through the logging bridge, the process and thread fields are absent. The formatter fell back to the "(unknown)" string for them, so a format string using the numeric "%(process)d" or "%(thread)d" specifiers raised "TypeError: %d format: a real number is required" and could take down the scheduler at startup. Give those two numeric callsite parameters a numeric fallback of 0, the same way lineno is already handled, so the format never receives a string where a number is expected. (cherry picked from commit fd7d535c3ff5d3618c2a4a83a634fdec09c911e9) Co-authored-by: Anas Khan --- .../logging/percent_formatter.py | 6 ++++++ .../tests/logging/test_percent_formatter.py | 18 ++++++++++++++++++ 2 files changed, 24 insertions(+) diff --git a/shared/logging/src/airflow_shared/logging/percent_formatter.py b/shared/logging/src/airflow_shared/logging/percent_formatter.py index ee9195e59b594..0efbb78d9922e 100644 --- a/shared/logging/src/airflow_shared/logging/percent_formatter.py +++ b/shared/logging/src/airflow_shared/logging/percent_formatter.py @@ -55,6 +55,12 @@ def __getitem__(self, key): # https://github.com/python/cpython/blob/d3c888b4ec15dbd7d6b6ef4f15b558af77c228af/Lib/logging/__init__.py#L1652C34-L1652C48 if key == "lineno": return self.event.get("lineno") or 0 + # process and thread are numeric callsite params formatted with %d, so fall back to a + # number (like lineno above) rather than the "(unknown)" string used for text params; + # otherwise "%(process)d"/"%(thread)d" raises TypeError when the callsite info is absent + # (e.g. warnings routed through the logging bridge). + if key == "process" or key == "thread": + return self.event.get(key) or 0 if key == "filename": return self.event.get("filename", "(unknown file)") if key == "funcName": diff --git a/shared/logging/tests/logging/test_percent_formatter.py b/shared/logging/tests/logging/test_percent_formatter.py index 3a3ae84f562eb..217c23708e5e2 100644 --- a/shared/logging/tests/logging/test_percent_formatter.py +++ b/shared/logging/tests/logging/test_percent_formatter.py @@ -19,6 +19,8 @@ from unittest import mock +import pytest + from airflow_shared.logging.percent_formatter import PercentFormatRender @@ -40,3 +42,19 @@ def test_lineno_is_none(self): ) assert formatted == "test.py:0 our msg" + + @pytest.mark.parametrize( + "event", + [ + pytest.param({"event": "our msg"}, id="missing"), + pytest.param({"event": "our msg", "process": None, "thread": None}, id="none"), + ], + ) + def test_numeric_callsite_without_process_or_thread(self, event): + # Regression for a scheduler crash: a %d specifier for process/thread with no callsite + # info (e.g. a warning routed through the logging bridge) must not raise TypeError. + fmter = PercentFormatRender("%(process)d:%(thread)d %(message)s") + + formatted = fmter(mock.Mock(name="Logger"), "info", event) + + assert formatted == "0:0 our msg" From 334838e752f8ff0d9ba6c1393f8e57fe7ea5235a Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Mon, 13 Jul 2026 02:28:11 +0200 Subject: [PATCH 073/297] [v3-3-test] Fix generate-providers-metadata hang by using spawn pools (#69763) (#69784) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The multiprocessing pools in the providers-metadata generation flow used the platform-default start method (fork on Linux). Before the pools are created the parent process has already used GitPython — which opens persistent `git cat-file --batch` subprocesses and is not fork-safe — and holds open network sockets from the version and constraints downloads. Forking that state into the workers left them with broken inherited file descriptors, so the pool deadlocked and the command hung forever, most reliably when --refresh-constraints-and-airflow-releases forces the parent through git and the network before forking. Switching these pools to the spawn start method gives each worker a clean interpreter with no inherited git subprocesses or sockets. (cherry picked from commit 583369271f5ae2098e9d800a0a427cf2eeb4edd4) Co-authored-by: Jarek Potiuk --- .../commands/release_management_commands.py | 9 ++++++--- .../src/airflow_breeze/utils/provider_dependencies.py | 9 +++++++-- 2 files changed, 13 insertions(+), 5 deletions(-) diff --git a/dev/breeze/src/airflow_breeze/commands/release_management_commands.py b/dev/breeze/src/airflow_breeze/commands/release_management_commands.py index 146b3ba180c36..a553bc97916e8 100644 --- a/dev/breeze/src/airflow_breeze/commands/release_management_commands.py +++ b/dev/breeze/src/airflow_breeze/commands/release_management_commands.py @@ -34,7 +34,7 @@ from datetime import datetime from enum import Enum from functools import partial -from multiprocessing import Pool +from multiprocessing import get_context from pathlib import Path from subprocess import DEVNULL from typing import IO, TYPE_CHECKING, Any, Literal, NamedTuple @@ -3598,7 +3598,10 @@ def generate_providers_metadata( ) console_print("\n[info]Checking provider.yaml versions[1:] against PyPI for stale entries...[/]\n") - with Pool() as pypi_pool: + # "spawn" (not the platform-default fork): the parent has already used GitPython and + # opened network sockets before reaching here, and forking that state into workers + # deadlocks. See get_all_constraint_files_and_airflow_releases for the same reasoning. + with get_context("spawn").Pool() as pypi_pool: pruned_per_provider = pypi_pool.map(prune_unreleased_versions_from_provider_yaml, package_ids) total_pruned = 0 for pid, pruned in zip(package_ids, pruned_per_provider): @@ -3623,7 +3626,7 @@ def generate_providers_metadata( airflow_release_dates=airflow_release_dates, current_metadata=current_metadata, ) - with Pool() as pool: + with get_context("spawn").Pool() as pool: results = pool.map( partial_generate_providers_metadata, package_ids, diff --git a/dev/breeze/src/airflow_breeze/utils/provider_dependencies.py b/dev/breeze/src/airflow_breeze/utils/provider_dependencies.py index a8d489b05cacc..963f96cff1716 100644 --- a/dev/breeze/src/airflow_breeze/utils/provider_dependencies.py +++ b/dev/breeze/src/airflow_breeze/utils/provider_dependencies.py @@ -26,7 +26,7 @@ import urllib.request from collections.abc import Generator from functools import cache, partial -from multiprocessing import Pool +from multiprocessing import get_context from pathlib import Path from threading import Lock from typing import NamedTuple @@ -274,7 +274,12 @@ def get_all_constraint_files_and_airflow_releases( airflow_release_dates_path.write_text(json.dumps(airflow_release_dates, indent=2)) console_print(f"[info]Airflow release dates saved in: {airflow_release_dates_path}[/]") with ci_group("Downloading constraints for all Airflow versions for all historical Python versions"): - with Pool() as pool: + # Use the "spawn" start method rather than the platform default: GitPython + # (used in the workers via get_tag_date) opens persistent `git cat-file --batch` + # subprocesses and is not fork-safe, and the parent already holds open network + # sockets from the version/constraints downloads. Forking that state into workers + # deadlocks; spawn gives each worker a clean interpreter. + with get_context("spawn").Pool() as pool: # We use partial to pass the common parameters to the function get_constraints_for_python_version_partial = partial( get_constraints_for_python_version, From 11c116ba1f8e726ae3d0ac1bf159700b1f0c81f1 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Mon, 13 Jul 2026 02:28:21 +0200 Subject: [PATCH 074/297] [v3-3-test] Add expand/collapse all for Dag Run conf JSON in Dag Runs list (#69567) (#69777) (cherry picked from commit dfd1ea30527af9dfd2c1662703f98a8f7de27796) Co-authored-by: Ashutosh Shaha <61512480+ashutosh264@users.noreply.github.com> --- .../airflow/ui/src/mocks/handlers/dag_runs.ts | 2 +- .../ui/src/pages/DagRuns/DagRuns.test.tsx | 44 ++++++++++++++++++- .../airflow/ui/src/pages/DagRuns/DagRuns.tsx | 21 +++++++-- 3 files changed, 60 insertions(+), 7 deletions(-) diff --git a/airflow-core/src/airflow/ui/src/mocks/handlers/dag_runs.ts b/airflow-core/src/airflow/ui/src/mocks/handlers/dag_runs.ts index ed55e14b01986..7fcb78ebfc919 100644 --- a/airflow-core/src/airflow/ui/src/mocks/handlers/dag_runs.ts +++ b/airflow-core/src/airflow/ui/src/mocks/handlers/dag_runs.ts @@ -38,7 +38,7 @@ const dagRunBeforeFilter = { }; const dagRunInRange = { - conf: null, + conf: { batch: 42, env: "prod" }, dag_display_name: "test_dag", dag_id: "test_dag", dag_run_id: "run_in_range", diff --git a/airflow-core/src/airflow/ui/src/pages/DagRuns/DagRuns.test.tsx b/airflow-core/src/airflow/ui/src/pages/DagRuns/DagRuns.test.tsx index 50d1d9d8e6a61..3b9b19e0eabef 100644 --- a/airflow-core/src/airflow/ui/src/pages/DagRuns/DagRuns.test.tsx +++ b/airflow-core/src/airflow/ui/src/pages/DagRuns/DagRuns.test.tsx @@ -17,11 +17,19 @@ * under the License. */ import "@testing-library/jest-dom"; -import { render, screen, waitFor } from "@testing-library/react"; -import { describe, expect, it } from "vitest"; +import { fireEvent, render, screen, waitFor } from "@testing-library/react"; +import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; import { AppWrapper } from "src/utils/AppWrapper"; +// Stand in for the Monaco-backed JSON viewer so the test can assert the collapse +// state without loading the editor. +vi.mock("src/components/RenderedJsonField", () => ({ + default: ({ collapsed }: { readonly collapsed?: boolean }) => ( +
+ ), +})); + // The dag_runs mock handler (see src/mocks/handlers/dag_runs.ts) returns: // - run_before_filter (logical_date: 2024-12-31) — excluded when filtering Jan 2025 // - run_in_range (logical_date: 2025-01-15) — included when filtering Jan 2025 @@ -46,3 +54,35 @@ describe("DagRuns logical date filter", () => { expect(screen.queryByText("run_before_filter")).not.toBeInTheDocument(); }); }); + +describe("DagRuns conf expand/collapse", () => { + beforeEach(() => { + // The conf column is hidden by default; reveal it so the JSON viewer renders. + globalThis.localStorage.setItem( + "dataTable:common:dagRun:columnVisibility", + JSON.stringify({ conf: true }), + ); + }); + + afterEach(() => { + globalThis.localStorage.clear(); + }); + + it("toggles conf JSON collapse state via the expand/collapse all buttons", async () => { + render(); + + await waitFor(() => expect(screen.getByTestId("rendered-json-field")).toBeInTheDocument()); + + expect(screen.getByTestId("rendered-json-field")).toHaveAttribute("data-collapsed", "true"); + + fireEvent.click(screen.getByTestId("expand-all-button")); + await waitFor(() => + expect(screen.getByTestId("rendered-json-field")).toHaveAttribute("data-collapsed", "false"), + ); + + fireEvent.click(screen.getByTestId("collapse-all-button")); + await waitFor(() => + expect(screen.getByTestId("rendered-json-field")).toHaveAttribute("data-collapsed", "true"), + ); + }); +}); diff --git a/airflow-core/src/airflow/ui/src/pages/DagRuns/DagRuns.tsx b/airflow-core/src/airflow/ui/src/pages/DagRuns/DagRuns.tsx index ac4d1b9a886d2..d9efe994719c7 100644 --- a/airflow-core/src/airflow/ui/src/pages/DagRuns/DagRuns.tsx +++ b/airflow-core/src/airflow/ui/src/pages/DagRuns/DagRuns.tsx @@ -16,7 +16,7 @@ * specific language governing permissions and limitations * under the License. */ -import { Flex, HStack, Text } from "@chakra-ui/react"; +import { Flex, HStack, Text, useDisclosure } from "@chakra-ui/react"; import type { ColumnDef } from "@tanstack/react-table"; import type { TFunction } from "i18next"; import { useTranslation } from "react-i18next"; @@ -36,6 +36,7 @@ import { } from "src/components/DataTable/useRowSelection"; import { useTableURLState } from "src/components/DataTable/useTableUrlState"; import { ErrorAlert } from "src/components/ErrorAlert"; +import { ExpandCollapseButtons } from "src/components/ExpandCollapseButtons"; import { LimitedItemsList } from "src/components/LimitedItemsList"; import { MarkRunAsButton } from "src/components/MarkAs"; import RenderedJsonField from "src/components/RenderedJsonField"; @@ -86,10 +87,11 @@ const { type ColumnProps = { readonly dagId?: string; + readonly open: boolean; readonly translate: TFunction; } & GetColumnsParams; -const runColumns = ({ dagId, translate }: ColumnProps): Array> => [ +const runColumns = ({ dagId, open, translate }: ColumnProps): Array> => [ { accessorKey: "select", cell: ({ row }) => , @@ -196,7 +198,7 @@ const runColumns = ({ dagId, translate }: ColumnProps): Array original.conf && Object.keys(original.conf).length > 0 ? ( - + ) : undefined, header: translate("dagRun.conf"), }, @@ -226,6 +228,7 @@ export const DagRuns = () => { useDocumentTitle(dagId === undefined ? translate("common:dagRun_other") : undefined); const [searchParams] = useSearchParams(); + const { onClose, onOpen, open } = useDisclosure(); const { setTableURLState, tableURLState } = useTableURLState({ columnVisibility: { @@ -337,6 +340,7 @@ export const DagRuns = () => { const columns = runColumns({ dagId, multiTeam: false, + open, translate, }); @@ -347,7 +351,16 @@ export const DagRuns = () => { onSelectAll={handleSelectAll} selectedRows={selectedRows} > - + + + + Date: Mon, 13 Jul 2026 02:28:32 +0200 Subject: [PATCH 075/297] [v3-3-test] Add the option to select bundle version parameter on dag run trigger endpoint (#61550) (#69719) * feat: adding the option to add bundle version parameter on dag run trigger endpoint * removing code comments and adding the new parameter to the schema * feat: adding the new parameter to schema and types files * fix: fixing format and missing field in class * fix: fixing the usage of bundle version in orm dagrun creation * feat: added migration file * feat: adding unit test for bundle version parameter dagrun endpoint * fix: fixing the unit test by removing unecessary asserts * fix: removing duplicate dag version check and raising exception accordingly * fix: adding a raise exception when passing incorrect bundle version * fix: verify exception message to raise accordingly * fix: wiring to bundle version and moved to the correct version target * fix: missed adding bundle_version field in 2 unit tests response assertion * fix: adding missing bundle version field in tests * fix: handling dag and dag's tasks correctly when passing bundle version * feat: added new airflow exception to handle no dag version has found * fix: assigning run dag to dag version when only using bundle version * fix: fixing assertion equal failing * fix: removed unecessary init files after prek hook added them * fix: adding support for all the dag-level callbacks * feat: adding versioning test for bundle version parameter * fix: rebasing branch * feat: adding the option to add bundle version parameter on dag run trigger endpoint * removing code comments and adding the new parameter to the schema * feat: adding the new parameter to schema and types files * fix: fixing format and missing field in class * fix: fixing the usage of bundle version in orm dagrun creation * feat: added migration file * feat: adding unit test for bundle version parameter dagrun endpoint * fix: fixing the unit test by removing unecessary asserts * fix: removing duplicate dag version check and raising exception accordingly * fix: adding a raise exception when passing incorrect bundle version * fix: verify exception message to raise accordingly * fix: wiring to bundle version and moved to the correct version target * fix: missed adding bundle_version field in 2 unit tests response assertion * fix: adding missing bundle version field in tests * fix: handling dag and dag's tasks correctly when passing bundle version * feat: added new airflow exception to handle no dag version has found * fix: assigning run dag to dag version when only using bundle version * fix: fixing assertion equal failing * fix: removed unecessary init files after prek hook added them * fix: adding support for all the dag-level callbacks * fix: prek didn't updated datamodels for airflowctl * fix: adding bundle version support also in asset route, fixing execution api versioning, and also getting latest version of dag with bundle version * fix: duplicate import and duplicate methods * fix: remove duplicate DagVersion lookup and unintended dag swap for non-explicit triggers * fix: reverting uv lock due to unnecessary changes * fix: fixing test to resolve ci errors * fix: restore uv.lock to upstream/main * fix: remove double code, moving to new CalVer file, raising correct exception and add more callbacks support in test * fix: instead of always using latest dag using the correct dag if bundle version is passed * fix: missing fields in schema.json * Regenerate supervisor schema snapshot after rebase onto main The schema.json was stale: it contained legacy top-level $defs (DagRun, DagRunState, DagRunType, JsonValue, and nested asset reference types) that the generator no longer emits after upstream main restructured them. bundle_version also needed to land at the correct offset in the generated output. * fix: consolidate bundle version resolution, fix double deserialization, drop bundle_version from Execution API * restored provider_dependencies.json.sha256sum * Fix bundle_version overwritten by sync_dag_to_db and stale supervisor schema snapshot * revert accidently changes to uv.lock * Move AddPartitionDateField to 2026-06-30 version to match partition_date introduction timeline * Remove dead AirflowBadRequest handler and unintended execution API version split * Derive run context from requested bundle version on trigger * Validate partition_key and allowed_run_types against the requested bundle version * Revert accidental uv.lock * Regenerate private UI OpenAPI spec * Removing unnecessary callback attributes * Remove unnecessary test due to remove bundle version from execution api --------- (cherry picked from commit 1ce3f17906c1d2356abeb21d319ad144f016c8bd) Co-authored-by: Itay Adler <35665981+itayweb@users.noreply.github.com> Co-authored-by: Itay Adler Co-authored-by: Itay Adler --- .../core_api/datamodels/dag_run.py | 1 + .../openapi/v2-rest-api-generated.yaml | 10 + .../core_api/routes/public/assets.py | 36 ++- .../core_api/routes/public/dag_run.py | 58 +++-- airflow-core/src/airflow/exceptions.py | 4 + .../src/airflow/models/dag_version.py | 9 +- .../airflow/serialization/definitions/dag.py | 57 ++++- .../ui/openapi-gen/requests/schemas.gen.ts | 22 ++ .../ui/openapi-gen/requests/types.gen.ts | 2 + .../core_api/routes/public/test_assets.py | 88 +++++++ .../core_api/routes/public/test_dag_run.py | 217 +++++++++++++++++- airflow-core/tests/unit/models/test_dag.py | 118 ++++++++++ .../airflowctl/api/datamodels/generated.py | 2 + .../src/tests_common/test_utils/dag.py | 10 +- 14 files changed, 593 insertions(+), 41 deletions(-) diff --git a/airflow-core/src/airflow/api_fastapi/core_api/datamodels/dag_run.py b/airflow-core/src/airflow/api_fastapi/core_api/datamodels/dag_run.py index 91a60cb776f63..cae9d3a8339d4 100644 --- a/airflow-core/src/airflow/api_fastapi/core_api/datamodels/dag_run.py +++ b/airflow-core/src/airflow/api_fastapi/core_api/datamodels/dag_run.py @@ -228,6 +228,7 @@ class TriggerDAGRunPostBody(StrictBaseModel): conf: dict | None = Field(default_factory=dict) note: str | None = None partition_key: str | None = None + bundle_version: str | None = None @model_validator(mode="after") def check_data_intervals(self): diff --git a/airflow-core/src/airflow/api_fastapi/core_api/openapi/v2-rest-api-generated.yaml b/airflow-core/src/airflow/api_fastapi/core_api/openapi/v2-rest-api-generated.yaml index abb77e1dfe98a..436df0b354342 100644 --- a/airflow-core/src/airflow/api_fastapi/core_api/openapi/v2-rest-api-generated.yaml +++ b/airflow-core/src/airflow/api_fastapi/core_api/openapi/v2-rest-api-generated.yaml @@ -15110,6 +15110,11 @@ components: - type: string - type: 'null' title: Partition Key + bundle_version: + anyOf: + - type: string + - type: 'null' + title: Bundle Version additionalProperties: false type: object title: MaterializeAssetBody @@ -16643,6 +16648,11 @@ components: - type: string - type: 'null' title: Partition Key + bundle_version: + anyOf: + - type: string + - type: 'null' + title: Bundle Version additionalProperties: false type: object required: diff --git a/airflow-core/src/airflow/api_fastapi/core_api/routes/public/assets.py b/airflow-core/src/airflow/api_fastapi/core_api/routes/public/assets.py index a19f4c7da7797..dd8638e8246a4 100644 --- a/airflow-core/src/airflow/api_fastapi/core_api/routes/public/assets.py +++ b/airflow-core/src/airflow/api_fastapi/core_api/routes/public/assets.py @@ -73,7 +73,7 @@ from airflow.api_fastapi.logging.decorators import action_logging from airflow.assets.manager import asset_manager from airflow.configuration import conf -from airflow.exceptions import ParamValidationError +from airflow.exceptions import DagVersionNotFound, ParamValidationError from airflow.models.asset import ( AssetAliasModel, AssetDagRunQueue, @@ -82,6 +82,7 @@ AssetWatcherModel, TaskOutletAssetReference, ) +from airflow.models.dag_version import DagVersion from airflow.typing_compat import Unpack from airflow.utils.state import DagRunState from airflow.utils.types import DagRunTriggeredByType, DagRunType @@ -445,14 +446,31 @@ def materialize_asset( dag = get_latest_version_of_dag(dag_bag, dag_id, session) - if dag.allowed_run_types is not None and DagRunType.ASSET_MATERIALIZATION not in dag.allowed_run_types: - raise HTTPException( - status.HTTP_400_BAD_REQUEST, - f"Dag with dag_id: '{dag_id}' does not allow asset materialization runs", - ) + resolved_body = body or MaterializeAssetBody() try: - params = (body or MaterializeAssetBody()).validate_context(dag) + preloaded_dag_version = None + context_dag = dag + if resolved_body.bundle_version is not None and not dag.disable_bundle_versioning: + preloaded_dag_version = DagVersion.get_latest_version( + dag_id, bundle_version=resolved_body.bundle_version, load_serialized_dag=True, session=session + ) + if not preloaded_dag_version: + raise DagVersionNotFound( + f"DAG with dag_id: '{dag_id}' does not have a version for bundle_version '{resolved_body.bundle_version}'" + ) + context_dag = preloaded_dag_version.serialized_dag.dag + + if ( + context_dag.allowed_run_types is not None + and DagRunType.ASSET_MATERIALIZATION not in context_dag.allowed_run_types + ): + raise HTTPException( + status.HTTP_400_BAD_REQUEST, + f"Dag with dag_id: '{dag_id}' does not allow asset materialization runs", + ) + + params = resolved_body.validate_context(context_dag) return dag.create_dagrun( run_id=params["run_id"], logical_date=params["logical_date"], @@ -467,9 +485,13 @@ def materialize_asset( partition_date=params["partition_date"], note=params["note"], session=session, + bundle_version=resolved_body.bundle_version, + dag_version=preloaded_dag_version, ) except (ParamValidationError, ValueError) as e: raise HTTPException(status.HTTP_400_BAD_REQUEST, str(e)) from e + except DagVersionNotFound as e: + raise HTTPException(status.HTTP_404_NOT_FOUND, str(e)) from e @assets_router.get( diff --git a/airflow-core/src/airflow/api_fastapi/core_api/routes/public/dag_run.py b/airflow-core/src/airflow/api_fastapi/core_api/routes/public/dag_run.py index 889251bf81e99..015e17648aba3 100644 --- a/airflow-core/src/airflow/api_fastapi/core_api/routes/public/dag_run.py +++ b/airflow-core/src/airflow/api_fastapi/core_api/routes/public/dag_run.py @@ -108,7 +108,7 @@ perform_clear_dag_run, ) from airflow.api_fastapi.logging.decorators import action_logging -from airflow.exceptions import ParamValidationError +from airflow.exceptions import DagVersionNotFound, ParamValidationError from airflow.models import DagModel, DagRun from airflow.models.asset import AssetEvent from airflow.models.dag_version import DagVersion @@ -690,21 +690,44 @@ def trigger_dag_run( f"Dag with dag_id: '{dag_id}' has import errors and cannot be triggered", ) - if dm.allowed_run_types is not None and DagRunType.MANUAL not in dm.allowed_run_types: - raise HTTPException( - status.HTTP_400_BAD_REQUEST, - f"Dag with dag_id: '{dag_id}' does not allow manual runs", - ) - referer = request.headers.get("referer") if referer: triggered_by = DagRunTriggeredByType.UI else: triggered_by = DagRunTriggeredByType.REST_API - dag = get_latest_version_of_dag(dag_bag, dag_id, session) try: - params = body.validate_context(dag) + dag = get_latest_version_of_dag(dag_bag, dag_id, session) + preloaded_dag_version = None + context_dag = dag + if body.bundle_version is not None and not dag.disable_bundle_versioning: + preloaded_dag_version = DagVersion.get_latest_version( + dag_id, bundle_version=body.bundle_version, load_serialized_dag=True, session=session + ) + if not preloaded_dag_version: + raise DagVersionNotFound( + f"DAG with dag_id: '{dag_id}' does not have a version for bundle_version '{body.bundle_version}'" + ) + context_dag = preloaded_dag_version.serialized_dag.dag + + if ( + context_dag.allowed_run_types is not None + and DagRunType.MANUAL not in context_dag.allowed_run_types + ): + raise HTTPException( + status.HTTP_400_BAD_REQUEST, + f"Dag with dag_id: '{dag_id}' does not allow manual runs", + ) + + params = body.validate_context(context_dag) + + if body.bundle_version is not None: + if dag.disable_bundle_versioning: + raise HTTPException( + status.HTTP_400_BAD_REQUEST, + f"DAG with dag_id: '{dag_id}' does not support bundle versioning", + ) + dag_run = dag.create_dagrun( run_id=params["run_id"], logical_date=params["logical_date"], @@ -716,17 +739,22 @@ def trigger_dag_run( triggering_user_name=user.get_name(), state=DagRunState.QUEUED, partition_key=params["partition_key"], + bundle_version=body.bundle_version, + dag_version=preloaded_dag_version, partition_date=params["partition_date"], session=session, ) + + dag_run_note = body.note + if dag_run_note: + current_user_id = user.get_id() + dag_run.note = (dag_run_note, current_user_id) + return dag_run + except (ParamValidationError, ValueError) as e: raise HTTPException(status.HTTP_400_BAD_REQUEST, str(e)) from e - - dag_run_note = body.note - if dag_run_note: - current_user_id = user.get_id() - dag_run.note = (dag_run_note, current_user_id) - return dag_run + except DagVersionNotFound as e: + raise HTTPException(status.HTTP_404_NOT_FOUND, str(e)) from e @dag_run_router.get( diff --git a/airflow-core/src/airflow/exceptions.py b/airflow-core/src/airflow/exceptions.py index addd4f8f2a547..cb45b050fce34 100644 --- a/airflow-core/src/airflow/exceptions.py +++ b/airflow-core/src/airflow/exceptions.py @@ -141,6 +141,10 @@ class DagRunNotFound(AirflowNotFoundException): """Raise when a DAG Run is not available in the system.""" +class DagVersionNotFound(AirflowNotFoundException): + """Raised when a DagVersion for the given dag_id / bundle_version is not found.""" + + class DagNotPartitionedError(ValueError): """Raise when a partition_key is supplied for a Dag that is not partitioned.""" diff --git a/airflow-core/src/airflow/models/dag_version.py b/airflow-core/src/airflow/models/dag_version.py index e6564a6da0668..023aa2ba3bfab 100644 --- a/airflow-core/src/airflow/models/dag_version.py +++ b/airflow-core/src/airflow/models/dag_version.py @@ -154,6 +154,7 @@ def _latest_version_select( bundle_version: str | None = None, load_dag_model: bool = False, load_bundle_model: bool = False, + load_serialized_dag: bool = False, ) -> Select: """ Get the select object to get the latest version of the DAG. @@ -162,7 +163,7 @@ def _latest_version_select( :return: The select object. """ query = select(cls).where(cls.dag_id == dag_id) - if bundle_version: + if bundle_version is not None: query = query.where(cls.bundle_version == bundle_version) if load_dag_model: @@ -171,6 +172,9 @@ def _latest_version_select( if load_bundle_model: query = query.options(joinedload(cls.bundle)) + if load_serialized_dag: + query = query.options(joinedload(cls.serialized_dag)) + # Order by version_number, not created_at: version_number is monotonic and unique per # dag_id, so it is deterministic even when two versions share a created_at timestamp. # write_dag relies on this select to compute the next version_number; ordering by @@ -188,6 +192,7 @@ def get_latest_version( bundle_version: str | None = None, load_dag_model: bool = False, load_bundle_model: bool = False, + load_serialized_dag: bool = False, session: Session = NEW_SESSION, ) -> DagVersion | None: """ @@ -197,6 +202,7 @@ def get_latest_version( :param session: The database session. :param load_dag_model: Whether to load the DAG model. :param load_bundle_model: Whether to load the DagBundle model. + :param load_serialized_dag: Whether to eagerly load the serialized DAG. :return: The latest version of the DAG or None if not found. """ return session.scalar( @@ -205,6 +211,7 @@ def get_latest_version( bundle_version=bundle_version, load_dag_model=load_dag_model, load_bundle_model=load_bundle_model, + load_serialized_dag=load_serialized_dag, ) ) diff --git a/airflow-core/src/airflow/serialization/definitions/dag.py b/airflow-core/src/airflow/serialization/definitions/dag.py index 9dc816eb9cc45..e0447f0fda3dc 100644 --- a/airflow-core/src/airflow/serialization/definitions/dag.py +++ b/airflow-core/src/airflow/serialization/definitions/dag.py @@ -36,6 +36,7 @@ from airflow.exceptions import ( AirflowException, DagNotPartitionedError, + DagVersionNotFound, InvalidPartitionKeyError, NodeNotFound, TaskNotFound, @@ -581,8 +582,10 @@ def create_dagrun( creating_job_id: int | None = None, backfill_id: NonNegativeInt | None = None, partition_key: str | None = None, + bundle_version: str | None = None, partition_date: datetime.datetime | None = None, note: str | None = None, + dag_version: DagVersion | None = None, session: Session = NEW_SESSION, ) -> DagRun: """ @@ -650,10 +653,28 @@ def create_dagrun( f"is reserved for {inferred_run_type.value} runs" ) - self.validate_partition_key(partition_key) - # todo: AIP-78 add verification that if run type is backfill then we have a backfill id - copied_params = self.params.deep_merge(conf) + + # When triggering against a specific bundle version, resolve that version first so + # partition_key and conf are validated against it (not the live/latest dag). + if bundle_version is not None: + if self.disable_bundle_versioning: + raise ValueError(f"DAG with dag_id: '{self.dag_id}' does not support bundle versioning") + if dag_version is None: + dag_version = DagVersion.get_latest_version( + self.dag_id, bundle_version=bundle_version, load_serialized_dag=True, session=session + ) + if not dag_version: + raise DagVersionNotFound( + f"DAG with dag_id: '{self.dag_id}' does not have a version for bundle_version '{bundle_version}'" + ) + params_dag = dag_version.serialized_dag.dag + else: + params_dag = self + + params_dag.validate_partition_key(partition_key) + + copied_params = params_dag.params.deep_merge(conf) copied_params.validate() orm_dagrun = _create_orm_dagrun( dag=self, @@ -670,12 +691,15 @@ def create_dagrun( triggered_by=triggered_by, triggering_user_name=triggering_user_name, partition_key=partition_key, + bundle_version=bundle_version, partition_date=partition_date, note=note, + dag_version=dag_version, + resolved_dag=params_dag if bundle_version is not None else None, session=session, ) - if self.deadline: + if params_dag.deadline: self._process_dagrun_deadline_alerts(orm_dagrun, session) return orm_dagrun @@ -1391,16 +1415,25 @@ def _create_orm_dagrun( triggered_by: DagRunTriggeredByType, triggering_user_name: str | None = None, partition_key: str | None = None, + bundle_version: str | None = None, partition_date: datetime.datetime | None = None, note: str | None = None, + dag_version: DagVersion | None = None, + resolved_dag: SerializedDAG | None = None, session: Session = NEW_SESSION, ) -> DagRun: - bundle_version = None - if not dag.disable_bundle_versioning: - bundle_version = session.scalar( - select(DagModel.bundle_version).where(DagModel.dag_id == dag.dag_id), - ) - dag_version = DagVersion.get_latest_version(dag.dag_id, session=session) + resolved_bundle_version: str | None = None + use_resolved_dag = False + if dag_version is not None: + resolved_bundle_version = bundle_version + use_resolved_dag = True + else: + if not dag.disable_bundle_versioning: + resolved_bundle_version = session.scalar( + select(DagModel.bundle_version).where(DagModel.dag_id == dag.dag_id) + ) + dag_version = DagVersion.get_latest_version(dag.dag_id, session=session) + if not dag_version: raise AirflowException(f"Cannot create DagRun for DAG {dag.dag_id} because the dag is not serialized") @@ -1418,7 +1451,7 @@ def _create_orm_dagrun( triggered_by=triggered_by, triggering_user_name=triggering_user_name, backfill_id=backfill_id, - bundle_version=bundle_version, + bundle_version=resolved_bundle_version, partition_key=partition_key, partition_date=partition_date, note=note, @@ -1431,6 +1464,8 @@ def _create_orm_dagrun( session.add(run) session.flush() run.dag = dag + if use_resolved_dag: + run.dag = resolved_dag if resolved_dag is not None else dag_version.serialized_dag.dag # create the associated task instances # state is None at the moment of creation run.verify_integrity(session=session, dag_version_id=dag_version.id) diff --git a/airflow-core/src/airflow/ui/openapi-gen/requests/schemas.gen.ts b/airflow-core/src/airflow/ui/openapi-gen/requests/schemas.gen.ts index 690c79c131422..16f14472c6aa0 100644 --- a/airflow-core/src/airflow/ui/openapi-gen/requests/schemas.gen.ts +++ b/airflow-core/src/airflow/ui/openapi-gen/requests/schemas.gen.ts @@ -5465,6 +5465,17 @@ export const $MaterializeAssetBody = { } ], title: 'Partition Key' + }, + bundle_version: { + anyOf: [ + { + type: 'string' + }, + { + type: 'null' + } + ], + title: 'Bundle Version' } }, additionalProperties: false, @@ -7772,6 +7783,17 @@ export const $TriggerDAGRunPostBody = { } ], title: 'Partition Key' + }, + bundle_version: { + anyOf: [ + { + type: 'string' + }, + { + type: 'null' + } + ], + title: 'Bundle Version' } }, additionalProperties: false, diff --git a/airflow-core/src/airflow/ui/openapi-gen/requests/types.gen.ts b/airflow-core/src/airflow/ui/openapi-gen/requests/types.gen.ts index 7bd02c5597926..d22f8d38234cb 100644 --- a/airflow-core/src/airflow/ui/openapi-gen/requests/types.gen.ts +++ b/airflow-core/src/airflow/ui/openapi-gen/requests/types.gen.ts @@ -1424,6 +1424,7 @@ export type MaterializeAssetBody = { } | null; note?: string | null; partition_key?: string | null; + bundle_version?: string | null; }; /** @@ -1931,6 +1932,7 @@ export type TriggerDAGRunPostBody = { } | null; note?: string | null; partition_key?: string | null; + bundle_version?: string | null; }; /** diff --git a/airflow-core/tests/unit/api_fastapi/core_api/routes/public/test_assets.py b/airflow-core/tests/unit/api_fastapi/core_api/routes/public/test_assets.py index f28915973af0c..d5462426049ae 100644 --- a/airflow-core/tests/unit/api_fastapi/core_api/routes/public/test_assets.py +++ b/airflow-core/tests/unit/api_fastapi/core_api/routes/public/test_assets.py @@ -1674,6 +1674,43 @@ def test_should_respond_400_if_materialization_runs_denied(self, test_client, se == f"Dag with dag_id: '{self.DAG_ASSET1_ID}' does not allow asset materialization runs" ) + def test_materialize_allowed_run_types_from_requested_version(self, test_client, session, dag_maker): + """Asset materialization allowed_run_types is enforced from the requested bundle version, not latest.""" + bundle_name = "allowed_run_types_bundle" + asset = session.get(AssetModel, 1).to_serialized() + + with dag_maker( + self.DAG_ASSET1_ID, + bundle_name=bundle_name, + bundle_version="v1", + schedule=None, + session=session, + ): + EmptyOperator(task_id="task_v1", outlets=asset) + + with dag_maker( + self.DAG_ASSET1_ID, + bundle_name=bundle_name, + bundle_version="v2", + schedule="@daily", + allowed_run_types=[DagRunType.SCHEDULED], + session=session, + ): + EmptyOperator(task_id="task_v2", outlets=asset) + + # v1 allows materialization; latest v2 does not. Requesting v1 must succeed. + response = test_client.post("/assets/1/materialize", json={"bundle_version": "v1"}) + assert response.status_code == 200 + assert response.json()["bundle_version"] == "v1" + + # Without bundle_version the latest (v2) governs and rejects the run. + response = test_client.post("/assets/1/materialize") + assert response.status_code == 400 + assert ( + response.json()["detail"] + == f"Dag with dag_id: '{self.DAG_ASSET1_ID}' does not allow asset materialization runs" + ) + @pytest.mark.usefixtures("configure_git_connection_for_dag_bundle") def test_should_respond_403_when_user_cannot_trigger_dag(self, test_client): with mock.patch( @@ -1695,6 +1732,57 @@ def test_should_respond_403_when_user_cannot_trigger_dag(self, test_client): user=mock.ANY, ) + def test_should_respond_with_bundle_version(self, test_client, session, dag_maker): + """Test that asset materialization respects bundle_version parameter.""" + bundle_name = "testing_bundle" + asset = session.get(AssetModel, 1).to_serialized() + + with dag_maker( + self.DAG_ASSET1_ID, + bundle_name=bundle_name, + bundle_version="v1", + schedule=None, + session=session, + ): + EmptyOperator(task_id="task_v1", outlets=asset) + + with dag_maker( + self.DAG_ASSET1_ID, + bundle_name=bundle_name, + bundle_version="v2", + schedule=None, + session=session, + ): + EmptyOperator(task_id="task_v2", outlets=asset) + + response = test_client.post("/assets/1/materialize", json={"bundle_version": "v1"}) + assert response.status_code == 200 + assert response.json()["bundle_version"] == "v1" + + response = test_client.post("/assets/1/materialize", json={"bundle_version": "invalid_version"}) + assert response.status_code == 404 + assert ( + f"DAG with dag_id: '{self.DAG_ASSET1_ID}' does not have a version for bundle_version 'invalid_version'" + in response.json()["detail"] + ) + + with dag_maker( + self.DAG_ASSET1_ID, + bundle_name=bundle_name, + bundle_version="v3", + schedule=None, + session=session, + ): + EmptyOperator(task_id="task_v3", outlets=asset) + dag_maker.dag.disable_bundle_versioning = True + + response = test_client.post("/assets/1/materialize", json={"bundle_version": "v1"}) + assert response.status_code == 400 + assert ( + f"DAG with dag_id: '{self.DAG_ASSET1_ID}' does not support bundle versioning" + in response.json()["detail"] + ) + @pytest.mark.usefixtures("configure_git_connection_for_dag_bundle") def test_should_respond_400_on_invalid_dag_run_id(self, test_client): """A dag_run_id containing '..' triggers ValueError in DagRun.validate_run_id. diff --git a/airflow-core/tests/unit/api_fastapi/core_api/routes/public/test_dag_run.py b/airflow-core/tests/unit/api_fastapi/core_api/routes/public/test_dag_run.py index 43a9e36c198f7..1f3bfc887bc6e 100644 --- a/airflow-core/tests/unit/api_fastapi/core_api/routes/public/test_dag_run.py +++ b/airflow-core/tests/unit/api_fastapi/core_api/routes/public/test_dag_run.py @@ -3247,12 +3247,22 @@ def test_should_respond_400_if_a_dag_has_import_errors(self, test_client, sessio == "Dag with dag_id: 'import_errors' has import errors and cannot be triggered" ) - def test_should_respond_400_if_manual_runs_denied(self, test_client, session, testing_dag_bundle): + def test_should_respond_400_if_manual_runs_denied(self, test_client, session, dag_maker): now = timezone.utcnow().isoformat() - self._dags_for_trigger_tests(session) - response = test_client.post("/dags/allowed_scheduled/dagRuns", json={"logical_date": now}) + dag_id = "allowed_scheduled" + with dag_maker( + dag_id=dag_id, + schedule="@daily", + allowed_run_types=[DagRunType.SCHEDULED], + session=session, + serialized=True, + ): + EmptyOperator(task_id="task") + session.commit() + + response = test_client.post(f"/dags/{dag_id}/dagRuns", json={"logical_date": now}) assert response.status_code == 400 - assert response.json()["detail"] == "Dag with dag_id: 'allowed_scheduled' does not allow manual runs" + assert response.json()["detail"] == f"Dag with dag_id: '{dag_id}' does not allow manual runs" @time_machine.travel(timezone.utcnow(), tick=False) @pytest.mark.usefixtures("configure_git_connection_for_dag_bundle") @@ -3459,6 +3469,205 @@ def test_custom_timetable_generate_run_id_for_manual_trigger(self, dag_maker, te run = session.scalars(select(DagRun).where(DagRun.run_id == run_id_without_logical_date)).one() assert run.dag_id == custom_dag_id + @pytest.mark.usefixtures("configure_git_connection_for_dag_bundle") + def test_trigger_dag_run_with_bundle_version(self, test_client, session, dag_maker): + """Test triggering a DAG run with a specific bundle version.""" + from tests_common.test_utils.dag import sync_dag_to_db + + dag_id = "test_bundle_version_dag" + bundle_name = "testing_bundle" + + with dag_maker( + dag_id=dag_id, + bundle_name=bundle_name, + bundle_version="v1", + session=session, + ) as dag1: + EmptyOperator(task_id="task_1") + sync_dag_to_db(dag1, bundle_name=bundle_name, bundle_version="v1") + + with dag_maker( + dag_id=dag_id, + bundle_name=bundle_name, + bundle_version="v2", + session=session, + ) as dag2: + EmptyOperator(task_id="task_1") + EmptyOperator(task_id="task_2") + sync_dag_to_db(dag2, bundle_name=bundle_name, bundle_version="v2") + + response = test_client.post( + f"/dags/{dag_id}/dagRuns", json={"logical_date": "2024-01-01T00:00:00Z", "bundle_version": "v1"} + ) + assert response.status_code == 200 + assert response.json()["dag_versions"][0]["bundle_version"] == "v1" + run_id_v1 = response.json()["dag_run_id"] + dr_v1 = session.scalars(select(DagRun).where(DagRun.run_id == run_id_v1)).one() + assert {ti.task_id for ti in dr_v1.task_instances} == {"task_1"} + + response = test_client.post( + f"/dags/{dag_id}/dagRuns", + json={ + "logical_date": "2024-01-02T00:00:00Z", + }, + ) + assert response.status_code == 200 + assert response.json()["dag_versions"][0]["bundle_version"] == "v2" + run_id_v2 = response.json()["dag_run_id"] + dr_v2 = session.scalars(select(DagRun).where(DagRun.run_id == run_id_v2)).one() + assert {ti.task_id for ti in dr_v2.task_instances} == {"task_1", "task_2"} + + response = test_client.post( + f"/dags/{dag_id}/dagRuns", + json={"logical_date": "2024-01-03T00:00:00Z", "bundle_version": "invalid_version"}, + ) + assert response.status_code == 404 + assert ( + f"DAG with dag_id: '{dag_id}' does not have a version for bundle_version 'invalid_version'" + in response.json()["detail"] + ) + + dag2.disable_bundle_versioning = True + sync_dag_to_db(dag2, bundle_name=bundle_name) + + response = test_client.post( + f"/dags/{dag_id}/dagRuns", json={"logical_date": "2024-01-04T00:00:00Z", "bundle_version": "v1"} + ) + assert response.status_code == 400 + assert f"DAG with dag_id: '{dag_id}' does not support bundle versioning" in response.json()["detail"] + + def test_trigger_dag_run_bundle_version_validates_against_old_param_schema( + self, test_client, session, dag_maker + ): + """Conf is validated against the requested bundle version's param schema, not the live dag's.""" + from tests_common.test_utils.dag import sync_dag_to_db + + dag_id = "test_bundle_param_schema_dag" + bundle_name = "param_schema_bundle" + + with dag_maker( + dag_id=dag_id, + bundle_name=bundle_name, + bundle_version="v1", + session=session, + params={"env": Param("staging", type="string", enum=["staging", "prod"])}, + ) as dag1: + EmptyOperator(task_id="task_1") + sync_dag_to_db(dag1, bundle_name=bundle_name, bundle_version="v1") + + with dag_maker( + dag_id=dag_id, + bundle_name=bundle_name, + bundle_version="v2", + session=session, + params={"env": Param("dev", type="string", enum=["dev", "staging", "prod"])}, + ) as dag2: + EmptyOperator(task_id="task_1") + sync_dag_to_db(dag2, bundle_name=bundle_name, bundle_version="v2") + + # "dev" is valid for v2 but not for v1's enum — triggering v1 should reject it. + response = test_client.post( + f"/dags/{dag_id}/dagRuns", + json={"logical_date": "2024-02-01T00:00:00Z", "bundle_version": "v1", "conf": {"env": "dev"}}, + ) + assert response.status_code == 400 + + # "staging" is valid for both v1 and v2 — triggering v1 should accept it. + response = test_client.post( + f"/dags/{dag_id}/dagRuns", + json={ + "logical_date": "2024-02-02T00:00:00Z", + "bundle_version": "v1", + "conf": {"env": "staging"}, + }, + ) + assert response.status_code == 200 + + @pytest.mark.usefixtures("configure_git_connection_for_dag_bundle") + def test_trigger_dag_run_bundle_version_uses_v1_timetable(self, test_client, session, dag_maker): + """Triggering with bundle_version='v1' must derive data_interval from v1's timetable, not v2's.""" + from tests_common.test_utils.dag import sync_dag_to_db + + dag_id = "test_bundle_timetable_dag" + bundle_name = "timetable_bundle" + + with dag_maker( + dag_id=dag_id, + bundle_name=bundle_name, + bundle_version="v1", + schedule=CronDataIntervalTimetable("0 0 * * *", timezone="UTC"), + session=session, + ) as dag1: + EmptyOperator(task_id="task_1") + sync_dag_to_db(dag1, bundle_name=bundle_name, bundle_version="v1") + + with dag_maker( + dag_id=dag_id, + bundle_name=bundle_name, + bundle_version="v2", + schedule=None, + session=session, + ) as dag2: + EmptyOperator(task_id="task_1") + sync_dag_to_db(dag2, bundle_name=bundle_name, bundle_version="v2") + + response = test_client.post( + f"/dags/{dag_id}/dagRuns", + json={"logical_date": "2024-01-01T00:00:00Z", "bundle_version": "v1"}, + ) + assert response.status_code == 200 + data = response.json() + assert data["dag_versions"][0]["bundle_version"] == "v1" + # data_interval must come from v1's daily cron timetable, not v2's null timetable. + # For a "0 0 * * *" cron, logical_date is the interval END, so interval is [prev_day, logical_date]. + assert data["data_interval_start"] == "2023-12-31T00:00:00Z" + assert data["data_interval_end"] == "2024-01-01T00:00:00Z" + + @pytest.mark.usefixtures("configure_git_connection_for_dag_bundle") + def test_trigger_dag_run_allowed_run_types_from_requested_version(self, test_client, session, dag_maker): + """allowed_run_types is enforced from the requested bundle version, not the latest.""" + from tests_common.test_utils.dag import sync_dag_to_db + + dag_id = "test_bundle_allowed_run_types_dag" + bundle_name = "allowed_run_types_bundle" + + with dag_maker( + dag_id=dag_id, + bundle_name=bundle_name, + bundle_version="v1", + schedule="@daily", + allowed_run_types=[DagRunType.MANUAL, DagRunType.SCHEDULED], + session=session, + ) as dag1: + EmptyOperator(task_id="task_1") + sync_dag_to_db(dag1, bundle_name=bundle_name, bundle_version="v1") + + with dag_maker( + dag_id=dag_id, + bundle_name=bundle_name, + bundle_version="v2", + schedule="@daily", + allowed_run_types=[DagRunType.SCHEDULED], + session=session, + ) as dag2: + EmptyOperator(task_id="task_1") + sync_dag_to_db(dag2, bundle_name=bundle_name, bundle_version="v2") + + # Latest (v2) disallows manual runs; v1 allows them. Triggering v1 must succeed. + response = test_client.post( + f"/dags/{dag_id}/dagRuns", + json={"logical_date": "2024-02-01T00:00:00Z", "bundle_version": "v1"}, + ) + assert response.status_code == 200 + + # Without bundle_version the latest (v2) governs and rejects the manual run. + response = test_client.post( + f"/dags/{dag_id}/dagRuns", + json={"logical_date": "2024-02-02T00:00:00Z"}, + ) + assert response.status_code == 400 + assert response.json()["detail"] == f"Dag with dag_id: '{dag_id}' does not allow manual runs" + def test_should_respond_400_when_partition_key_given_for_non_partitioned_dag(self, test_client): """Passing partition_key to a non-partitioned Dag via REST trigger must return 400, not 500. diff --git a/airflow-core/tests/unit/models/test_dag.py b/airflow-core/tests/unit/models/test_dag.py index 81198d8e41810..6a1e9e5782c49 100644 --- a/airflow-core/tests/unit/models/test_dag.py +++ b/airflow-core/tests/unit/models/test_dag.py @@ -1581,6 +1581,38 @@ def test_create_dagrun_int_partition_key_rejected_for_partitioned_dag(self, dag_ partition_key=123, ) + def test_create_dagrun_partition_key_validated_against_requested_version(self, dag_maker, session): + """create_dagrun validates partition_key against the requested bundle version, not the latest.""" + dag_id = "test_create_dagrun_partition_key_bundle_version" + + with dag_maker( + dag_id, + schedule=CronPartitionTimetable("@daily", timezone="UTC"), + bundle_version="v1", + session=session, + ): + EmptyOperator(task_id="task") + + with dag_maker(dag_id, schedule=None, bundle_version="v2", session=session): + EmptyOperator(task_id="task") + session.commit() + + scheduler_dag_v2 = dag_maker.serialized_dag + + # Latest (v2) is not partitioned, but the requested v1 is: the key must be + # accepted against v1 rather than rejected against the latest dag. + dr = scheduler_dag_v2.create_dagrun( + run_id="manual__partition_key_from_v1", + run_after=DEFAULT_DATE, + run_type=DagRunType.MANUAL, + state=State.NONE, + triggered_by=DagRunTriggeredByType.TEST, + partition_key="my-key", + bundle_version="v1", + session=session, + ) + assert dr.partition_key == "my-key" + @pytest.mark.need_serialized_dag @pytest.mark.parametrize( ("partition_key", "schedule", "should_raise"), @@ -4383,6 +4415,92 @@ def hello(): assert dr.bundle_version == expected +def test_create_dagrun_uses_resolved_bundle_version_for_integrity(dag_maker, session, clear_dags): + """ + When no explicit bundle_version is passed, the live dag drives TI creation and + created_dag_version points to the latest serialized version. DagRun.bundle_version + still records the DagModel.bundle_version for auditing purposes. + """ + with dag_maker( + dag_id="test_dag_bundle_version_integrity", + session=session, + serialized=True, + bundle_version="v1", + ) as _dag_v1: + EmptyOperator(task_id="t1") + + with dag_maker( + dag_id="test_dag_bundle_version_integrity", + session=session, + serialized=True, + bundle_version="v2", + ) as dag_v2: + EmptyOperator(task_id="t1") + EmptyOperator(task_id="t2") + + dag_model = session.scalar(select(DagModel).where(DagModel.dag_id == dag_v2.dag_id)) + dag_model.bundle_version = "v1" + session.commit() + + dr = dag_v2.create_dagrun( + run_id="bundle_version_integrity", + run_after=pendulum.now(), + run_type="manual", + triggered_by=DagRunTriggeredByType.TEST, + state=None, + ) + + # DagRun.bundle_version records the DagModel value at trigger time (audit field). + assert dr.bundle_version == "v1" + # created_dag_version reflects the latest serialized version (v2), not the DagModel audit value. + assert dr.created_dag_version.bundle_version == "v2" + # TIs come from the live dag (dag_v2 with t1+t2), not from the old serialized version. + assert {ti.task_id for ti in dr.get_task_instances(session=session)} == {"t1", "t2"} + + +def test_create_dagrun_without_bundle_version_uses_live_dag(dag_maker, session, clear_dags): + """ + When no explicit bundle_version is passed, TIs are created from the live dag even if + DagModel.bundle_version points to an older version. This confirms backfills and other + callers that don't pass bundle_version are unaffected by the bundle_version feature. + """ + with dag_maker( + dag_id="test_dag_backfill_bundle_version", + session=session, + serialized=True, + bundle_version="v1", + ) as _dag_v1: + EmptyOperator(task_id="t1") + + with dag_maker( + dag_id="test_dag_backfill_bundle_version", + session=session, + serialized=True, + bundle_version="v2", + ) as dag_v2: + EmptyOperator(task_id="t1") + EmptyOperator(task_id="t2") + + dag_model = session.scalar(select(DagModel).where(DagModel.dag_id == dag_v2.dag_id)) + dag_model.bundle_version = "v1" + session.commit() + + dr = dag_v2.create_dagrun( + run_id="no_bundle_version_uses_live_dag", + run_after=pendulum.now(), + run_type="manual", + triggered_by=DagRunTriggeredByType.TEST, + state=None, + ) + + # TIs come from the live dag (dag_v2), not from the v1 serialized version. + assert {ti.task_id for ti in dr.get_task_instances(session=session)} == {"t1", "t2"} + # created_dag_version reflects the latest serialization (v2). + assert dr.created_dag_version.bundle_version == "v2" + # DagRun.bundle_version still records the DagModel value at trigger time. + assert dr.bundle_version == "v1" + + def test_get_run_data_interval(): with DAG("dag", schedule=None, start_date=DEFAULT_DATE) as dag: EmptyOperator(task_id="empty_task") diff --git a/airflow-ctl/src/airflowctl/api/datamodels/generated.py b/airflow-ctl/src/airflowctl/api/datamodels/generated.py index 746ba1e02dabf..8804d2040ea4a 100644 --- a/airflow-ctl/src/airflowctl/api/datamodels/generated.py +++ b/airflow-ctl/src/airflowctl/api/datamodels/generated.py @@ -796,6 +796,7 @@ class MaterializeAssetBody(BaseModel): conf: Annotated[dict[str, Any] | None, Field(title="Conf")] = None note: Annotated[str | None, Field(title="Note")] = None partition_key: Annotated[str | None, Field(title="Partition Key")] = None + bundle_version: Annotated[str | None, Field(title="Bundle Version")] = None class NewTaskResponse(BaseModel): @@ -1126,6 +1127,7 @@ class TriggerDAGRunPostBody(BaseModel): conf: Annotated[dict[str, Any] | None, Field(title="Conf")] = None note: Annotated[str | None, Field(title="Note")] = None partition_key: Annotated[str | None, Field(title="Partition Key")] = None + bundle_version: Annotated[str | None, Field(title="Bundle Version")] = None class TriggerResponse(BaseModel): diff --git a/devel-common/src/tests_common/test_utils/dag.py b/devel-common/src/tests_common/test_utils/dag.py index 6e02ddf61ab67..891176eb499fc 100644 --- a/devel-common/src/tests_common/test_utils/dag.py +++ b/devel-common/src/tests_common/test_utils/dag.py @@ -41,15 +41,17 @@ def create_scheduler_dag(dag: DAG | SerializedDAG) -> SerializedDAG: def sync_dag_to_db( dag: DAG, bundle_name: str = "testing", + bundle_version: str | None = None, session: Session = NEW_SESSION, ) -> SerializedDAG: - return sync_dags_to_db([dag], bundle_name=bundle_name, session=session)[0] + return sync_dags_to_db([dag], bundle_name=bundle_name, bundle_version=bundle_version, session=session)[0] @provide_session def sync_dags_to_db( dags: Collection[DAG], bundle_name: str = "testing", + bundle_version: str | None = None, session: Session = NEW_SESSION, ) -> Sequence[SerializedDAG]: """ @@ -68,10 +70,12 @@ def sync_dags_to_db( def _write_dag(dag: DAG) -> SerializedDAG: data = DagSerialization.to_dict(dag) - SerializedDagModel.write_dag(LazyDeserializedDAG(data=data), bundle_name, session=session) + SerializedDagModel.write_dag( + LazyDeserializedDAG(data=data), bundle_name, bundle_version, session=session + ) return DagSerialization.from_dict(data) - SerializedDAG.bulk_write_to_db(bundle_name, None, dags, session=session) + SerializedDAG.bulk_write_to_db(bundle_name, bundle_version, dags, session=session) scheduler_dags = [_write_dag(dag) for dag in dags] session.flush() return scheduler_dags From f5d8b3fa84c1f0e1c02f28f72d7cc96fd48ffb95 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Mon, 13 Jul 2026 02:28:43 +0200 Subject: [PATCH 076/297] [v3-3-test] Surface durable execution badge in Airflow Registry (#69651) (#69672) * Stage 1: Durable execution detection * Stage 2: Adding supports_durable_execution to schema * Stage 3: Wiring up with eleventy to show it on the UI (cherry picked from commit 59a54336481636dde9eeb3059b1cc76c7f73fccf) Co-authored-by: Amogh Desai --- dev/registry/extract_parameters.py | 44 +++- dev/registry/registry_contract_models.py | 1 + dev/registry/tests/test_extract_parameters.py | 223 +++++++++++++++++- .../tests/test_registry_contract_models.py | 25 ++ registry/pnpm-workspace.yaml | 4 +- registry/src/css/main.css | 13 + registry/src/provider-version.njk | 7 +- 7 files changed, 312 insertions(+), 5 deletions(-) diff --git a/dev/registry/extract_parameters.py b/dev/registry/extract_parameters.py index 565057541df7e..0e9eff89a4df7 100644 --- a/dev/registry/extract_parameters.py +++ b/dev/registry/extract_parameters.py @@ -88,6 +88,7 @@ class Module: category: str provider_id: str provider_name: str + supports_durable_execution: bool def get_category(integration_name: str) -> str: @@ -373,16 +374,51 @@ def _get_source_line(cls: type) -> int | None: return None +def load_resumable_job_mixin() -> type | None: + """Import ResumableJobMixin for durable-execution capability checks, or None if unavailable.""" + try: + from airflow.sdk import ResumableJobMixin + + return ResumableJobMixin + except ImportError: + log.warning("Could not import ResumableJobMixin") + return None + + +def is_durable_capable(cls: type, resumable_mixin: type | None) -> bool: + """Return True if a class fully implements ResumableJobMixin's crash-recovery contract. + + Inheriting the mixin is not sufficient: a complete override is inert unless + execute() actually calls execute_resumable(). + """ + if resumable_mixin is None or resumable_mixin not in cls.__mro__: + return False + + if inspect.isabstract(cls): + return False + + execute = getattr(cls, "execute", None) + if execute is None: + return False + try: + source = inspect.getsource(execute) + except (OSError, TypeError): + return False + + return "execute_resumable" in source + + def discover_classes_from_provider( provider_yaml_path: Path, base_classes: dict[str, type], + resumable_mixin: type | None = None, inventory: dict[str, str] | None = None, version: str = "", ) -> list[dict]: """Discover classes from a single provider by importing its modules at runtime. Reads the provider.yaml to find which modules/classes to inspect, imports them, - and returns metadata for each discovered class with all 11 Module fields. + and returns metadata for each discovered class with all 12 Module fields. """ with open(provider_yaml_path) as f: provider_yaml = yaml.safe_load(f) @@ -431,7 +467,7 @@ def make_entry( category: str = "", transfer_desc: str | None = None, ) -> dict: - """Build a full module entry dict with all 11 fields.""" + """Build a full module entry dict with all 12 fields.""" module_name = module_path.split(".")[-1] docstring = _get_first_docstring_line(cls_or_obj) short_desc = docstring or transfer_desc or f"{integration} {module_type}".strip() @@ -448,6 +484,7 @@ def make_entry( "category": category or get_category(integration), "provider_id": provider_id, "provider_name": provider_name, + "supports_durable_execution": is_durable_capable(cls_or_obj, resumable_mixin), } discovered: list[dict] = [] @@ -889,6 +926,8 @@ def _main_discover( base_classes = load_base_classes() print(f"Loaded {len(base_classes)} base classes: {', '.join(sorted(base_classes))}") + resumable_mixin = load_resumable_job_mixin() + # Load all provider.yaml data and map provider_id -> yaml dict / path provider_yamls_by_id: dict[str, dict] = {} provider_paths_by_id: dict[str, Path] = {} @@ -923,6 +962,7 @@ def _main_discover( discovered = discover_classes_from_provider( yaml_path, base_classes, + resumable_mixin, inventory=inventories.get(pid), version=version, ) diff --git a/dev/registry/registry_contract_models.py b/dev/registry/registry_contract_models.py index 119a0227c1fb9..7295cfb403614 100644 --- a/dev/registry/registry_contract_models.py +++ b/dev/registry/registry_contract_models.py @@ -110,6 +110,7 @@ class ModuleContract(BaseModel): category: str provider_id: str | None = None provider_name: str | None = None + supports_durable_execution: bool = False class ModulesCatalogContract(BaseModel): diff --git a/dev/registry/tests/test_extract_parameters.py b/dev/registry/tests/test_extract_parameters.py index bfa3b05b56fd5..be0f608e17ed7 100644 --- a/dev/registry/tests/test_extract_parameters.py +++ b/dev/registry/tests/test_extract_parameters.py @@ -18,11 +18,14 @@ from __future__ import annotations +import abc +import builtins import json import types from unittest.mock import patch import pytest +import yaml from extract_parameters import ( Module, _get_source_line, @@ -31,6 +34,8 @@ compare_with_ast, discover_classes_from_provider, get_category, + is_durable_capable, + load_resumable_job_mixin, ) @@ -101,11 +106,147 @@ def test_returns_none_for_dynamic_class(self): assert _get_source_line(DynamicClass) is None +# --------------------------------------------------------------------------- +# load_resumable_job_mixin +# --------------------------------------------------------------------------- +class TestLoadResumableJobMixin: + def test_returns_none_when_airflow_sdk_unavailable(self): + real_import = builtins.__import__ + + def fake_import(name, *args, **kwargs): + if name == "airflow.sdk": + raise ImportError("no airflow.sdk here") + return real_import(name, *args, **kwargs) + + with patch("builtins.__import__", side_effect=fake_import): + assert load_resumable_job_mixin() is None + + +# --------------------------------------------------------------------------- +# is_durable_capable +# --------------------------------------------------------------------------- +class FakeResumableJobMixin(abc.ABC): + """Stand-in for airflow.sdk.ResumableJobMixin's abstract-method contract.""" + + @abc.abstractmethod + def submit_job(self, context): + raise NotImplementedError + + @abc.abstractmethod + def get_job_status(self, external_id, context): + raise NotImplementedError + + @abc.abstractmethod + def is_job_active(self, status): + raise NotImplementedError + + @abc.abstractmethod + def is_job_succeeded(self, status): + raise NotImplementedError + + @abc.abstractmethod + def poll_until_complete(self, external_id, context): + raise NotImplementedError + + @abc.abstractmethod + def get_job_result(self, external_id, context): + raise NotImplementedError + + def execute_resumable(self, context): + raise NotImplementedError + + +class FullyImplementedResumableOperator(FakeResumableJobMixin): + def execute(self, context): + return self.execute_resumable(context) + + def submit_job(self, context): + return "job-1" + + def get_job_status(self, external_id, context): + return "RUNNING" + + def is_job_active(self, status): + return status == "RUNNING" + + def is_job_succeeded(self, status): + return status == "SUCCEEDED" + + def poll_until_complete(self, external_id, context): + return None + + def get_job_result(self, external_id, context): + return None + + +class PartiallyImplementedResumableOperator(FakeResumableJobMixin): + """Retry-path methods left unoverridden -- would only blow up on an actual crash-recovery retry.""" + + def execute(self, context): + return self.execute_resumable(context) + + def submit_job(self, context): + return "job-1" + + def poll_until_complete(self, external_id, context): + return None + + def get_job_result(self, external_id, context): + return None + + +class UnwiredResumableOperator(FakeResumableJobMixin): + """execute() never calls execute_resumable -- dead capability, never exercised.""" + + def execute(self, context): + return self.submit_job(context) + + def submit_job(self, context): + return "job-1" + + def get_job_status(self, external_id, context): + return "RUNNING" + + def is_job_active(self, status): + return status == "RUNNING" + + def is_job_succeeded(self, status): + return status == "SUCCEEDED" + + def poll_until_complete(self, external_id, context): + return None + + def get_job_result(self, external_id, context): + return None + + +class PlainOperator: + def execute(self, context): + return None + + +class TestIsDurableCapable: + def test_fully_implemented_and_wired_qualifies(self): + assert is_durable_capable(FullyImplementedResumableOperator, FakeResumableJobMixin) is True + + def test_missing_retry_path_overrides_disqualifies(self): + assert is_durable_capable(PartiallyImplementedResumableOperator, FakeResumableJobMixin) is False + + def test_implemented_but_not_called_from_execute_disqualifies(self): + assert is_durable_capable(UnwiredResumableOperator, FakeResumableJobMixin) is False + + def test_no_mixin_in_mro_disqualifies(self): + assert is_durable_capable(PlainOperator, FakeResumableJobMixin) is False + + def test_mixin_unavailable_disqualifies(self): + assert is_durable_capable(FullyImplementedResumableOperator, None) is False + + # --------------------------------------------------------------------------- # Module dataclass # --------------------------------------------------------------------------- class TestModuleDataclass: - def test_has_all_11_fields(self): + def test_has_all_12_fields(self): m = Module( id="amazon-s3-S3Hook", name="S3Hook", @@ -118,6 +259,7 @@ def test_has_all_11_fields(self): category="amazon-s3", provider_id="amazon", provider_name="Amazon", + supports_durable_execution=False, ) assert m.id == "amazon-s3-S3Hook" assert m.provider_name == "Amazon" @@ -534,6 +676,85 @@ class MySensor(FakeBaseSensorOperator): assert result[0]["name"] == "MySensor" +# --------------------------------------------------------------------------- +# TestDiscoverClassesFromProvider: supports_durable_execution wiring +# --------------------------------------------------------------------------- +class TestDiscoverClassesFromProviderDurableExecution: + def test_marks_durable_capable_and_plain_operators(self, tmp_path): + class ResumableOperator(FullyImplementedResumableOperator): + __module__ = "airflow.providers.test.operators.spark" + + class PlainProviderOperator(PlainOperator): + __module__ = "airflow.providers.test.operators.spark" + + provider_yaml = { + "package-name": "apache-airflow-providers-test", + "name": "Test", + "operators": [ + { + "integration-name": "Test", + "python-modules": ["airflow.providers.test.operators.spark"], + }, + ], + } + provider_dir = tmp_path / "test" + provider_dir.mkdir() + yaml_path = provider_dir / "provider.yaml" + yaml_path.write_text(yaml.dump(provider_yaml)) + + mod = _make_module( + "airflow.providers.test.operators.spark", + { + "ResumableOperator": ResumableOperator, + "PlainProviderOperator": PlainProviderOperator, + }, + ) + + with ( + patch("extract_parameters.PROVIDERS_DIR", tmp_path), + patch("extract_parameters.importlib.import_module", return_value=mod), + ): + result = discover_classes_from_provider( + yaml_path, base_classes={}, resumable_mixin=FakeResumableJobMixin + ) + + by_name = {r["name"]: r for r in result} + assert by_name["ResumableOperator"]["supports_durable_execution"] is True + assert by_name["PlainProviderOperator"]["supports_durable_execution"] is False + + def test_defaults_to_false_when_mixin_unavailable(self, tmp_path): + class ResumableOperator(FullyImplementedResumableOperator): + __module__ = "airflow.providers.test.operators.spark" + + provider_yaml = { + "package-name": "apache-airflow-providers-test", + "name": "Test", + "operators": [ + { + "integration-name": "Test", + "python-modules": ["airflow.providers.test.operators.spark"], + }, + ], + } + provider_dir = tmp_path / "test" + provider_dir.mkdir() + yaml_path = provider_dir / "provider.yaml" + yaml_path.write_text(yaml.dump(provider_yaml)) + + mod = _make_module( + "airflow.providers.test.operators.spark", + {"ResumableOperator": ResumableOperator}, + ) + + with ( + patch("extract_parameters.PROVIDERS_DIR", tmp_path), + patch("extract_parameters.importlib.import_module", return_value=mod), + ): + result = discover_classes_from_provider(yaml_path, base_classes={}) + + assert result[0]["supports_durable_execution"] is False + + # --------------------------------------------------------------------------- # TestDiscoverClassLevelEntries # --------------------------------------------------------------------------- diff --git a/dev/registry/tests/test_registry_contract_models.py b/dev/registry/tests/test_registry_contract_models.py index 146a16ff3e691..120c682da78d7 100644 --- a/dev/registry/tests/test_registry_contract_models.py +++ b/dev/registry/tests/test_registry_contract_models.py @@ -22,6 +22,7 @@ from pydantic import ValidationError from registry_contract_models import ( build_openapi_document, + validate_modules_catalog, validate_provider_parameters, validate_provider_version_metadata, validate_provider_versions, @@ -75,6 +76,30 @@ def test_validate_provider_parameters_preserves_mro_alias(): assert "mro_chain" not in class_entry +def _module_payload(**overrides): + payload = { + "name": "Example", + "type": "operator", + "import_path": "airflow.providers.test.mod.Example", + "short_description": "Example module.", + "docs_url": "https://example.invalid/docs", + "source_url": "https://example.invalid/source", + "category": "test", + } + payload.update(overrides) + return payload + + +def test_module_contract_accepts_legacy_modules_without_supports_durable_execution(): + validated = validate_modules_catalog({"modules": [_module_payload()]}) + assert "supports_durable_execution" not in validated["modules"][0] + + +def test_module_contract_preserves_supports_durable_execution_true(): + validated = validate_modules_catalog({"modules": [_module_payload(supports_durable_execution=True)]}) + assert validated["modules"][0]["supports_durable_execution"] is True + + def test_validate_version_metadata_accepts_legacy_version_modules_without_ids(): payload = { "provider_id": "test", diff --git a/registry/pnpm-workspace.yaml b/registry/pnpm-workspace.yaml index de82ed55b6e4a..aae7d571c4baf 100644 --- a/registry/pnpm-workspace.yaml +++ b/registry/pnpm-workspace.yaml @@ -14,11 +14,13 @@ # KIND, either express or implied. See the License for the # specific language governing permissions and limitations # under the License. - --- packages: - '.' +allowBuilds: + '@scarf/scarf': false + overrides: brace-expansion@<1.1.13: '>=1.1.13' liquidjs@<10.25.0: '>=10.25.0' diff --git a/registry/src/css/main.css b/registry/src/css/main.css index a0adbd071c00b..2f8f4d4cc6a0d 100644 --- a/registry/src/css/main.css +++ b/registry/src/css/main.css @@ -3252,6 +3252,19 @@ main { margin-bottom: var(--space-1); } +.provider-detail-page .modules .module .content h3 .durable-badge { + display: inline-flex; + align-items: center; + vertical-align: middle; + margin-left: var(--space-2); + padding: 0.1rem var(--space-2); + font-size: var(--text-xs); + font-weight: var(--font-medium); + border-radius: var(--radius-lg); + background: color-mix(in srgb, var(--color-teal-500) 15%, transparent); + color: var(--color-teal-400); +} + .provider-detail-page .modules .module .content p { font-size: var(--text-sm); color: var(--text-secondary); diff --git a/registry/src/provider-version.njk b/registry/src/provider-version.njk index 51179078bab49..49356c0796fc6 100644 --- a/registry/src/provider-version.njk +++ b/registry/src/provider-version.njk @@ -355,7 +355,12 @@ eleventyComputed: {{ module.type[0] | upper }}
-

{{ module.name }}

+

+ {{ module.name }} + {% if module.supports_durable_execution %} + Durable + {% endif %} +

{{ module.short_description }}

{{ module.import_path }} From cf8dda9608567a8ccd6cbb59701fab43cdc53abd Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Mon, 13 Jul 2026 02:28:53 +0200 Subject: [PATCH 077/297] [v3-3-test] Added static check for conn-fields defined in provider.yaml (#69473) (#69653) * refactor: Added static check for conn-fields defined in provider.yaml to make sure those correspond with the ones defined in the get_connection_form_widgets method of the corresponding hook * Update scripts/in_container/run_provider_yaml_files_check.py * refactor: Updated comment in _get_widget_keys method --------- (cherry picked from commit 53de1c173aca56d9555165e18bbc83a666df4d4a) Co-authored-by: David Blain Co-authored-by: Jarek Potiuk --- scripts/ci/prek/check_provider_conn_fields.py | 112 ++++++++++++++++ .../run_provider_yaml_files_check.py | 65 +++++++++ ...st_check_conn_fields_match_form_widgets.py | 124 ++++++++++++++++++ 3 files changed, 301 insertions(+) create mode 100644 scripts/ci/prek/check_provider_conn_fields.py create mode 100644 scripts/tests/ci/prek/test_check_conn_fields_match_form_widgets.py diff --git a/scripts/ci/prek/check_provider_conn_fields.py b/scripts/ci/prek/check_provider_conn_fields.py new file mode 100644 index 0000000000000..ed4faf157c553 --- /dev/null +++ b/scripts/ci/prek/check_provider_conn_fields.py @@ -0,0 +1,112 @@ +#!/usr/bin/env python +# +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. +""" +Validation helpers for the conn-fields ↔ get_connection_form_widgets() check. + +These functions have no third-party dependencies so they can be unit-tested +outside of the Breeze container without any stubbing. + +Used by ``scripts/in_container/run_provider_yaml_files_check.py``. +""" + +from __future__ import annotations + +from collections.abc import Callable + + +def check_conn_fields_for_entry( + conn_type_entry: dict, + yaml_file_path: str, + get_widget_keys: Callable[[str], set[str] | None], +) -> list[str]: + """ + Validate a single connection-type entry. Returns a (possibly empty) list of error strings. + + *get_widget_keys(hook_class_name)* is a callable supplied by the caller that: + + - returns the set of field keys from ``get_connection_form_widgets()`` on success, + - returns ``None`` to signal that the hook could not be imported, its UI + dependencies are absent, or it does not implement ``get_connection_form_widgets()`` + at all — the entry is then skipped entirely (no ``conn-fields`` check), or + - raises any other ``Exception`` to signal an unexpected failure (converted + here into an error string so callers never need to catch it). + """ + hook_class_name: str = conn_type_entry["hook-class-name"] + connection_type: str = conn_type_entry.get("connection-type", "?") + + try: + widget_keys = get_widget_keys(hook_class_name) + except Exception as exc: + return [ + f"Failed to call `{hook_class_name}.get_connection_form_widgets()` " + f"while checking {yaml_file_path}: {exc}" + ] + + if widget_keys is None: + return [] + + conn_fields = conn_type_entry.get("conn-fields") + if conn_fields is None: + # No conn-fields declared: the new UI simply exposes no custom fields for this + # connection type, which is intentional. Nothing to validate. + return [] + + error = build_mismatch_error( + set(conn_fields.keys()), widget_keys, connection_type, yaml_file_path, hook_class_name + ) + return [error] if error else [] + + +def build_mismatch_error( + yaml_keys: set[str], + hook_keys: set[str], + connection_type: str, + yaml_file_path: str, + hook_class_name: str, +) -> str | None: + """ + Check that every key declared in ``conn-fields`` exists in + ``get_connection_form_widgets()``. + + ``conn-fields`` is the new React UI's view of a connection type and is + intentionally a *subset* of the Flask form widgets — fields can be omitted + from ``conn-fields`` on purpose. We therefore only flag keys that appear in + ``conn-fields`` but are absent from the hook's form (invalid / stale + declarations). The reverse direction (hook fields not listed in + ``conn-fields``) is not an error. + + Return an error string when stale keys are found, or ``None`` when the + declared keys are all valid. + """ + only_in_yaml = yaml_keys - hook_keys + + if not only_in_yaml: + return None + + lines = [ + f"Mismatch between `conn-fields` in {yaml_file_path} and " + f"`{hook_class_name}.get_connection_form_widgets()` " + f"for connection-type '{connection_type}':" + ] + lines.append( + " Fields in provider.yaml conn-fields but NOT in get_connection_form_widgets(): " + + ", ".join(sorted(only_in_yaml)) + ) + lines.append("[yellow]How to fix it[/]: Remove the stale key(s) from conn-fields in provider.yaml.") + return "\n".join(lines) diff --git a/scripts/in_container/run_provider_yaml_files_check.py b/scripts/in_container/run_provider_yaml_files_check.py index db664a66e011c..922e84b710bc4 100755 --- a/scripts/in_container/run_provider_yaml_files_check.py +++ b/scripts/in_container/run_provider_yaml_files_check.py @@ -50,6 +50,11 @@ from airflow.exceptions import AirflowOptionalProviderFeatureException, AirflowProviderDeprecationWarning from airflow.providers_manager import ProvidersManager +# check_provider_conn_fields lives in scripts/ci/prek/ which is not on sys.path when +# this script runs inside Breeze; resolve it relative to this file. +sys.path.insert(0, str(pathlib.Path(__file__).parent.parent / "ci" / "prek")) +from check_provider_conn_fields import check_conn_fields_for_entry + # Those are deprecated modules that contain removed Hooks/Sensors/Operators that we left in the code # so that users can get a very specific error message when they try to use them. @@ -473,6 +478,65 @@ def check_hook_class_name_entries_in_connection_types(yaml_files: dict[str, dict return num_connection_types, num_errors +@run_check("Checking that conn-fields in provider.yaml match get_connection_form_widgets() of the hook class") +def check_conn_fields_match_form_widgets(yaml_files: dict[str, dict]) -> tuple[int, int]: + """ + For every connection-type entry whose hook declares ``conn-fields``, + verify that every key in ``conn-fields`` also exists in the hook's + ``get_connection_form_widgets()``. + + ``conn-fields`` is optional and is intentionally allowed to be a *subset* + of the hook's form widgets (the new React UI may expose fewer fields than + the legacy Flask form), so extra hook widgets are not flagged — only + ``conn-fields`` keys absent from the hook are reported as stale. + """ + num_checks = 0 + num_errors = 0 + + for yaml_file_path, provider_data in yaml_files.items(): + for conn_type_entry in provider_data.get("connection-types", []): + num_checks += 1 + for error in check_conn_fields_for_entry(conn_type_entry, yaml_file_path, _get_widget_keys): + errors.append(error) + num_errors += 1 + + return num_checks, num_errors + + +def _get_widget_keys(hook_class_name: str) -> set[str] | None: + """ + Import *hook_class_name* and return the keys of ``get_connection_form_widgets()``. + + Returns ``None`` when the hook or its UI dependencies cannot be imported, + or when the hook does not override ``get_connection_form_widgets()`` (meaning it + has no custom connection fields and the conn-fields check should be skipped). + Raises for unexpected errors so ``check_conn_fields_for_entry`` can convert them + to an error string. + """ + try: + module_name, class_name = hook_class_name.rsplit(".", maxsplit=1) + with warnings.catch_warnings(record=True): + hook_class = getattr(importlib.import_module(module_name), class_name) + except (ImportError, AirflowOptionalProviderFeatureException, AttributeError): + return None + + # Only validate hooks that override get_connection_form_widgets() in their own __dict__, + # because that method is the source-of-truth for what conn-fields should be declared. + # Hooks that inherit it without overriding have no provider-specific widget definition + # to diff against, so the check is intentionally skipped for them. As of writing this + # includes HttpHook, the common/ai hooks, and AzureComputeHook — any provider whose hook + # falls into this category will NOT be validated here, even if it declares conn-fields. + if "get_connection_form_widgets" not in hook_class.__dict__: + return None + + with warnings.catch_warnings(record=True): + try: + form_widgets: dict[str, Any] = hook_class.get_connection_form_widgets() + return set(form_widgets.keys()) + except (ImportError, AirflowOptionalProviderFeatureException, AttributeError): + return None + + @run_check("Checking that hook classes defining conn_type are registered in connection-types") def check_hook_classes_with_conn_type_are_registered(yaml_files: dict[str, dict]) -> tuple[int, int]: """Find Hook subclasses that define conn_type but are not listed in connection-types.""" @@ -1046,6 +1110,7 @@ def check_providers_have_all_documentation_files(yaml_files: dict[str, dict]): check_completeness_of_list_of_transfers(all_parsed_yaml_files) check_hook_class_name_entries_in_connection_types(all_parsed_yaml_files) + check_conn_fields_match_form_widgets(all_parsed_yaml_files) check_hook_classes_with_conn_type_are_registered(all_parsed_yaml_files) check_executor_classes(all_parsed_yaml_files) check_queue_classes(all_parsed_yaml_files) diff --git a/scripts/tests/ci/prek/test_check_conn_fields_match_form_widgets.py b/scripts/tests/ci/prek/test_check_conn_fields_match_form_widgets.py new file mode 100644 index 0000000000000..caa2b2fc35264 --- /dev/null +++ b/scripts/tests/ci/prek/test_check_conn_fields_match_form_widgets.py @@ -0,0 +1,124 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. +from __future__ import annotations + +import pytest +from check_provider_conn_fields import ( + build_mismatch_error, + check_conn_fields_for_entry, +) + +YAML_PATH = "providers/my_provider/provider.yaml" +HOOK_CLASS = "my_provider.hooks.my_hook.MyHook" +CONN_TYPE = "my_conn_type" + + +def _entry(conn_fields: list[str] | None) -> dict: + entry: dict = {"hook-class-name": HOOK_CLASS, "connection-type": CONN_TYPE} + if conn_fields is not None: + entry["conn-fields"] = {k: {} for k in conn_fields} + return entry + + +def _get_keys(*keys: str): + """Return a get_widget_keys callable that always returns the given keys.""" + return lambda _hook_class_name: set(keys) + + +def _skip(_hook_class_name: str) -> None: + """get_widget_keys callable that signals 'skip silently'.""" + return None + + +def _raise(_hook_class_name: str) -> None: + raise RuntimeError("boom") + + +class TestBuildMismatchError: + @pytest.mark.parametrize( + "yaml_keys, hook_keys", + [ + pytest.param({"a", "b"}, {"a", "b"}, id="matching-keys"), + pytest.param(set(), set(), id="empty-sets"), + # Hook may have more keys than conn-fields — that is intentional (subset allowed). + pytest.param({"a"}, {"a", "extra_hook"}, id="hook-has-extra-keys-no-error"), + ], + ) + def test_no_mismatch_returns_none(self, yaml_keys, hook_keys): + assert build_mismatch_error(yaml_keys, hook_keys, CONN_TYPE, YAML_PATH, HOOK_CLASS) is None + + @pytest.mark.parametrize( + "yaml_keys, hook_keys, expected_in_error, not_expected_in_error", + [ + pytest.param( + {"a", "extra_yaml"}, + {"a"}, + ["extra_yaml", "NOT in get_connection_form_widgets"], + ["NOT in provider.yaml conn-fields"], + id="extra-in-yaml", + ), + # only_in_hook no longer triggers an error — only only_in_yaml does + pytest.param( + {"a", "only_yaml"}, + {"a", "only_hook"}, + [ + "only_yaml", + "NOT in get_connection_form_widgets", + ], + ["only_hook", "NOT in provider.yaml conn-fields"], + id="both-sides-only-yaml-reported", + ), + ], + ) + def test_mismatch_error_content(self, yaml_keys, hook_keys, expected_in_error, not_expected_in_error): + error = build_mismatch_error(yaml_keys, hook_keys, CONN_TYPE, YAML_PATH, HOOK_CLASS) + assert error is not None + for expected in expected_in_error: + assert expected in error + for not_expected in not_expected_in_error: + assert not_expected not in error + + +class TestCheckConnFieldsForEntry: + @pytest.mark.parametrize( + "conn_fields, get_keys", + [ + pytest.param(["a", "b"], _get_keys("a", "b"), id="matching-keys"), + pytest.param([], _get_keys(), id="empty-on-both-sides"), + pytest.param(["a"], _skip, id="skip-hook-without-get-connection-form-widgets"), + pytest.param(None, _skip, id="skip-missing-conn-fields-when-hook-has-no-widgets"), + # Hook with widgets but no conn-fields is allowed: new UI intentionally omits custom fields. + pytest.param(None, _get_keys("field_a"), id="no-conn-fields-with-hook-widgets-is-ok"), + # Hook has extra keys not in conn-fields — allowed (conn-fields is a valid subset). + pytest.param(["a"], _get_keys("a", "extra_hook"), id="hook-extra-keys-no-error"), + ], + ) + def test_no_errors(self, conn_fields, get_keys): + assert check_conn_fields_for_entry(_entry(conn_fields), YAML_PATH, get_keys) == [] + + @pytest.mark.parametrize( + "conn_fields, get_keys, expected_in_error", + [ + pytest.param(["a", "extra"], _get_keys("a"), "extra", id="extra-key-in-yaml"), + pytest.param(["a"], _raise, "boom", id="unexpected-exception-message"), + pytest.param(["a"], _raise, HOOK_CLASS, id="unexpected-exception-mentions-hook-class"), + ], + ) + def test_one_error_containing(self, conn_fields, get_keys, expected_in_error): + errors = check_conn_fields_for_entry(_entry(conn_fields), YAML_PATH, get_keys) + assert len(errors) == 1 + assert expected_in_error in errors[0] From 6358821cdb9965cddf1115891556ed3662b2a6ec Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Mon, 13 Jul 2026 03:24:52 +0200 Subject: [PATCH 078/297] [v3-3-test] API: Return 503 when SQLite locks during backfill creation (#67900) (#69659) SQLite backfill creation via the REST API could return HTTP 500 and leave an orphan Backfill row when the scheduler and API server contend for the database, blocking retries with 409. Map lock errors to HTTP 503 with a clear message and clean up partial DagRuns and TaskInstances so users can retry against a clean slate. (cherry picked from commit 2fb232178f10c49b6ba6d5125280c87309913345) Signed-off-by: Lohit Kolluri Co-authored-by: Lohit Kolluri --- .../openapi/v2-rest-api-generated.yaml | 12 + .../core_api/routes/public/backfills.py | 42 +++- airflow-core/src/airflow/models/backfill.py | 94 +++++--- .../ui/openapi-gen/requests/services.gen.ts | 6 +- .../ui/openapi-gen/requests/types.gen.ts | 8 + airflow-core/src/airflow/utils/sqlalchemy.py | 4 + .../core_api/routes/public/test_backfills.py | 221 +++++++++++++++++- 7 files changed, 344 insertions(+), 43 deletions(-) diff --git a/airflow-core/src/airflow/api_fastapi/core_api/openapi/v2-rest-api-generated.yaml b/airflow-core/src/airflow/api_fastapi/core_api/openapi/v2-rest-api-generated.yaml index 436df0b354342..58ac2e2840cf2 100644 --- a/airflow-core/src/airflow/api_fastapi/core_api/openapi/v2-rest-api-generated.yaml +++ b/airflow-core/src/airflow/api_fastapi/core_api/openapi/v2-rest-api-generated.yaml @@ -1161,6 +1161,12 @@ paths: schema: $ref: '#/components/schemas/HTTPExceptionResponse' description: Conflict + '503': + content: + application/json: + schema: + $ref: '#/components/schemas/HTTPExceptionResponse' + description: Service Unavailable '422': description: Validation Error content: @@ -1420,6 +1426,12 @@ paths: application/json: schema: $ref: '#/components/schemas/HTTPExceptionResponse' + '503': + description: Service Unavailable + content: + application/json: + schema: + $ref: '#/components/schemas/HTTPExceptionResponse' '422': description: Validation Error content: diff --git a/airflow-core/src/airflow/api_fastapi/core_api/routes/public/backfills.py b/airflow-core/src/airflow/api_fastapi/core_api/routes/public/backfills.py index dcfba91946533..c4971b883a11b 100644 --- a/airflow-core/src/airflow/api_fastapi/core_api/routes/public/backfills.py +++ b/airflow-core/src/airflow/api_fastapi/core_api/routes/public/backfills.py @@ -16,12 +16,13 @@ # under the License. from __future__ import annotations -from typing import Annotated +from typing import Annotated, NoReturn from fastapi import Depends, HTTPException, status from fastapi.exceptions import RequestValidationError from pydantic import NonNegativeInt from sqlalchemy import select, update +from sqlalchemy.exc import OperationalError from sqlalchemy.orm import joinedload from airflow._shared.timezones import timezone @@ -60,11 +61,25 @@ _create_backfill, _do_dry_run, ) +from airflow.utils.sqlalchemy import is_lock_not_available_error from airflow.utils.state import DagRunState backfills_router = AirflowRouter(tags=["Backfill"], prefix="/backfills") +def _raise_locked_response_or_reraise(e: OperationalError, action: str) -> NoReturn: + """Map a database lock OperationalError to HTTP 503, or re-raise if not a lock error.""" + if not is_lock_not_available_error(e): + raise + raise HTTPException( + status_code=status.HTTP_503_SERVICE_UNAVAILABLE, + detail=( + f"Database is locked. Backfill {action} is not supported on SQLite " + "under concurrent access. Please use PostgreSQL or MySQL." + ), + ) + + @backfills_router.get( path="", dependencies=[ @@ -219,7 +234,12 @@ def cancel_backfill(backfill_id: NonNegativeInt, session: SessionDep) -> Backfil @backfills_router.post( path="", responses=create_openapi_http_exception_doc( - [status.HTTP_400_BAD_REQUEST, status.HTTP_404_NOT_FOUND, status.HTTP_409_CONFLICT] + [ + status.HTTP_400_BAD_REQUEST, + status.HTTP_404_NOT_FOUND, + status.HTTP_409_CONFLICT, + status.HTTP_503_SERVICE_UNAVAILABLE, + ] ), dependencies=[ Depends(action_logging()), @@ -252,6 +272,8 @@ def create_backfill( run_on_latest_version=resolved_run_on_latest, ) return BackfillResponse.model_validate(backfill_obj) + except OperationalError as e: + _raise_locked_response_or_reraise(e, "creation") except AlreadyRunningBackfill: raise HTTPException( @@ -283,7 +305,13 @@ def create_backfill( @backfills_router.post( path="/dry_run", - responses=create_openapi_http_exception_doc([status.HTTP_404_NOT_FOUND, status.HTTP_409_CONFLICT]), + responses=create_openapi_http_exception_doc( + [ + status.HTTP_404_NOT_FOUND, + status.HTTP_409_CONFLICT, + status.HTTP_503_SERVICE_UNAVAILABLE, + ] + ), dependencies=[ Depends(requires_access_backfill(method="POST")), ], @@ -307,13 +335,15 @@ def create_backfill_dry_run( ) backfills = [ DryRunBackfillResponse( - logical_date=d.logical_date, partition_key=d.partition_key, partition_date=d.partition_date + logical_date=d.logical_date, + partition_key=d.partition_key, + partition_date=d.partition_date, ) for d in backfills_dry_run ] - return DryRunBackfillCollectionResponse(backfills=backfills, total_entries=len(backfills)) - + except OperationalError as e: + _raise_locked_response_or_reraise(e, "dry-run") except DagNotFound: raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, diff --git a/airflow-core/src/airflow/models/backfill.py b/airflow-core/src/airflow/models/backfill.py index 1f507de94bb92..13b9e6d004f6a 100644 --- a/airflow-core/src/airflow/models/backfill.py +++ b/airflow-core/src/airflow/models/backfill.py @@ -36,17 +36,18 @@ Integer, String, UniqueConstraint, + delete, func, select, ) -from sqlalchemy.exc import IntegrityError +from sqlalchemy.exc import IntegrityError, OperationalError from sqlalchemy.orm import Mapped, mapped_column, relationship, validates from airflow._shared.timezones import timezone from airflow.exceptions import AirflowException, DagNotFound, DagRunTypeNotAllowed from airflow.models.base import Base, StringID from airflow.utils.session import create_session -from airflow.utils.sqlalchemy import UtcDateTime, with_row_locks +from airflow.utils.sqlalchemy import UtcDateTime, is_lock_not_available_error, with_row_locks from airflow.utils.state import DagRunState from airflow.utils.types import DagRunTriggeredByType, DagRunType @@ -687,7 +688,7 @@ def _create_backfill( f"No runs to create for Dag {dag_id} in the range [{from_date}, {to_date}]" ) - br = Backfill( + backfill = Backfill( dag_id=dag_id, from_date=from_date, to_date=to_date, @@ -697,33 +698,58 @@ def _create_backfill( dag_model=dag, triggering_user_name=triggering_user_name, ) - session.add(br) + session.add(backfill) + # Commit immediately so the backfill is visible to concurrent requests + # checking num_active backfills, preventing duplicate active backfills + # for the same dag. session.commit() session.scalars(select(DagModel).where(DagModel.dag_id == dag_id)).one() first_info = dagrun_info_list[0] - if first_info.partition_key: - _create_runs_partitioned( - br=br, - dag=dag, - dagrun_info_list=dagrun_info_list, - session=session, - ) - else: - _create_runs_non_partitioned( - br=br, - dag=dag, - dagrun_info_list=dagrun_info_list, - run_on_latest_version=run_on_latest_version, - session=session, - ) - return br + try: + if first_info.partition_key: + _create_runs_partitioned( + backfill=backfill, + dag=dag, + dagrun_info_list=dagrun_info_list, + session=session, + ) + else: + _create_runs_non_partitioned( + backfill=backfill, + dag=dag, + dagrun_info_list=dagrun_info_list, + run_on_latest_version=run_on_latest_version, + session=session, + ) + except OperationalError as e: + if is_lock_not_available_error(e): + # Lock error: clean up the orphan so the user can retry. The + # helper is best-effort; if it fails the original error still + # surfaces and the route returns 503. + _cleanup_partial_backfill(backfill, session) + raise + return backfill + + +def _cleanup_partial_backfill(backfill: Backfill, session: Session) -> None: + """Best-effort removal of a partially-created backfill after a lock error.""" + from airflow.models.dagrun import DagRun + + try: + session.rollback() + session.execute(delete(BackfillDagRun).where(BackfillDagRun.backfill_id == backfill.id)) + session.execute(delete(DagRun).where(DagRun.backfill_id == backfill.id)) + session.delete(backfill) + session.commit() + except Exception: + session.rollback() def _create_runs_partitioned( *, - br: Backfill, + backfill: Backfill, dag: SerializedDAG, dagrun_info_list: list[DagRunInfo], session: Session, @@ -735,24 +761,24 @@ def _create_runs_partitioned( _create_backfill_dag_run_partitioned( dag=dag, info=info, - backfill_id=br.id, - dag_run_conf=br.dag_run_conf, - reprocess_behavior=ReprocessBehavior(br.reprocess_behavior), + backfill_id=backfill.id, + dag_run_conf=backfill.dag_run_conf, + reprocess_behavior=ReprocessBehavior(backfill.reprocess_behavior), backfill_sort_ordinal=backfill_sort_ordinal, - triggering_user_name=br.triggering_user_name, + triggering_user_name=backfill.triggering_user_name, session=session, ) log.info( "Created backfill Dag run.", dag_id=dag.dag_id, - backfill_id=br.id, - info=info, + backfill_id=backfill.id, + logical_date=info.logical_date, ) def _create_runs_non_partitioned( *, - br: Backfill, + backfill: Backfill, dag: SerializedDAG, dagrun_info_list: list[DagRunInfo], run_on_latest_version: bool, @@ -766,17 +792,17 @@ def _create_runs_non_partitioned( _create_backfill_dag_run_non_partitioned( dag=dag, info=info, - backfill_id=br.id, - dag_run_conf=br.dag_run_conf, - reprocess_behavior=ReprocessBehavior(br.reprocess_behavior), + backfill_id=backfill.id, + dag_run_conf=backfill.dag_run_conf, + reprocess_behavior=ReprocessBehavior(backfill.reprocess_behavior), backfill_sort_ordinal=backfill_sort_ordinal, - triggering_user_name=br.triggering_user_name, + triggering_user_name=backfill.triggering_user_name, run_on_latest_version=run_on_latest_version, session=session, ) log.info( "Created backfill Dag run.", dag_id=dag.dag_id, - backfill_id=br.id, - info=info, + backfill_id=backfill.id, + logical_date=info.logical_date, ) diff --git a/airflow-core/src/airflow/ui/openapi-gen/requests/services.gen.ts b/airflow-core/src/airflow/ui/openapi-gen/requests/services.gen.ts index e51b6cf770c9d..b934568d8ad9a 100644 --- a/airflow-core/src/airflow/ui/openapi-gen/requests/services.gen.ts +++ b/airflow-core/src/airflow/ui/openapi-gen/requests/services.gen.ts @@ -477,7 +477,8 @@ export class BackfillService { 403: 'Forbidden', 404: 'Not Found', 409: 'Conflict', - 422: 'Validation Error' + 422: 'Validation Error', + 503: 'Service Unavailable' } }); } @@ -595,7 +596,8 @@ export class BackfillService { 403: 'Forbidden', 404: 'Not Found', 409: 'Conflict', - 422: 'Validation Error' + 422: 'Validation Error', + 503: 'Service Unavailable' } }); } diff --git a/airflow-core/src/airflow/ui/openapi-gen/requests/types.gen.ts b/airflow-core/src/airflow/ui/openapi-gen/requests/types.gen.ts index d22f8d38234cb..c27b0d8e16325 100644 --- a/airflow-core/src/airflow/ui/openapi-gen/requests/types.gen.ts +++ b/airflow-core/src/airflow/ui/openapi-gen/requests/types.gen.ts @@ -5053,6 +5053,10 @@ export type $OpenApiTs = { * Validation Error */ 422: HTTPValidationError; + /** + * Service Unavailable + */ + 503: HTTPExceptionResponse; }; }; }; @@ -5204,6 +5208,10 @@ export type $OpenApiTs = { * Validation Error */ 422: HTTPValidationError; + /** + * Service Unavailable + */ + 503: HTTPExceptionResponse; }; }; }; diff --git a/airflow-core/src/airflow/utils/sqlalchemy.py b/airflow-core/src/airflow/utils/sqlalchemy.py index c47d8fd4796b0..3cda65e7ba759 100644 --- a/airflow-core/src/airflow/utils/sqlalchemy.py +++ b/airflow-core/src/airflow/utils/sqlalchemy.py @@ -592,6 +592,10 @@ def is_lock_not_available_error(error: OperationalError): # importing it. This doesn't if db_err_code in ("55P03", 1205, 3572): return True + # SQLite: `database is locked` (SQLITE_BUSY) — check the error text since + # sqlite3.OperationalError.args[0] is a human-readable string, not a numeric code + if error.orig and "database is locked" in str(error.orig).lower(): + return True return False diff --git a/airflow-core/tests/unit/api_fastapi/core_api/routes/public/test_backfills.py b/airflow-core/tests/unit/api_fastapi/core_api/routes/public/test_backfills.py index d1bf2b517298d..d0fdd77e764b6 100644 --- a/airflow-core/tests/unit/api_fastapi/core_api/routes/public/test_backfills.py +++ b/airflow-core/tests/unit/api_fastapi/core_api/routes/public/test_backfills.py @@ -17,16 +17,18 @@ from __future__ import annotations import os +import sqlite3 from datetime import datetime, timedelta from unittest import mock import pendulum import pytest from sqlalchemy import and_, func, select +from sqlalchemy.exc import OperationalError, ProgrammingError from airflow._shared.timezones import timezone from airflow.dag_processing.dagbag import DagBag -from airflow.models import DagModel, DagRun +from airflow.models import DagModel, DagRun, TaskInstance from airflow.models.backfill import ( Backfill, BackfillDagRun, @@ -41,6 +43,7 @@ from airflow.sdk import CronPartitionTimetable from airflow.utils.session import provide_session from airflow.utils.state import DagRunState +from airflow.utils.types import DagRunType from tests_common.test_utils.asserts import assert_queries_count from tests_common.test_utils.db import ( @@ -360,6 +363,196 @@ def test_create_backfill_with_depends_on_past( == "Dag has tasks for which depends_on_past=True. You must set reprocess behavior to reprocess completed or reprocess failed." ) + def test_create_backfill_database_locked(self, session, dag_maker, test_client): + """SQLite 'database is locked' during backfill creation returns HTTP 503.""" + with dag_maker(session=session, dag_id="TEST_DAG_1", schedule="0 * * * *") as dag: + EmptyOperator(task_id="mytask") + session.scalars(select(DagModel)).all() + session.commit() + + from_date = pendulum.parse("2024-01-01") + to_date = pendulum.parse("2024-02-01") + + data = { + "dag_id": dag.dag_id, + "from_date": to_iso(from_date), + "to_date": to_iso(to_date), + "max_active_runs": 5, + "run_backwards": False, + "dag_run_conf": {}, + } + + with mock.patch( + "airflow.api_fastapi.core_api.routes.public.backfills._create_backfill", + side_effect=OperationalError( + "statement", "params", sqlite3.OperationalError("database is locked") + ), + ): + response = test_client.post("/backfills", json=data) + + # OperationalError with "database is locked" should return 503 + assert response.status_code == 503 + assert "database is locked" in response.json()["detail"].lower() + + def test_create_backfill_cleans_up_orphan_on_lock_error(self, session, dag_maker): + """The partial Backfill row is removed when the cleanup runs after a lock error.""" + from airflow.models.backfill import Backfill, _cleanup_partial_backfill + + with dag_maker(session=session, dag_id="TEST_DAG_CLEANUP", schedule="0 * * * *") as dag: + EmptyOperator(task_id="mytask") + session.commit() + + bf = Backfill( + dag_id=dag.dag_id, + from_date=pendulum.parse("2024-01-01"), + to_date=pendulum.parse("2024-02-01"), + max_active_runs=5, + dag_run_conf={}, + reprocess_behavior="none", + dag_model=dag, + triggering_user_name="test", + ) + session.add(bf) + session.commit() + bf_id = bf.id + + assert session.scalar(select(func.count()).select_from(Backfill).where(Backfill.id == bf_id)) == 1 + + _cleanup_partial_backfill(bf, session) + + assert session.scalar(select(func.count()).select_from(Backfill).where(Backfill.id == bf_id)) == 0 + + def test_create_backfill_cleans_up_after_failed_transaction(self, session, dag_maker): + """The cleanup works when the session is in a deactivated state. + + Mirrors the real flow inside ``_create_backfill`` after an + ``OperationalError``: SQLAlchemy deactivates the session until + an explicit ``rollback()``. The cleanup must call it before any + further operation; without that, ``session.execute()`` raises + ``InvalidRequestError`` and the cleanup is a silent no-op. + """ + from sqlalchemy import text + + from airflow.models.backfill import Backfill, _cleanup_partial_backfill + + with dag_maker(session=session, dag_id="TEST_DAG_CLEANUP_DEACT", schedule="0 * * * *") as dag: + EmptyOperator(task_id="mytask") + session.commit() + + bf = Backfill( + dag_id=dag.dag_id, + from_date=pendulum.parse("2024-01-01"), + to_date=pendulum.parse("2024-02-01"), + max_active_runs=5, + dag_run_conf={}, + reprocess_behavior="none", + dag_model=dag, + triggering_user_name="test", + ) + session.add(bf) + session.commit() + bf_id = bf.id + + # Force the session into a deactivated state (same shape as after + # a failed flush). SQLite raises OperationalError; Postgres/MySQL raise ProgrammingError. + with pytest.raises((OperationalError, ProgrammingError)): + session.execute(text("INVALID SQL STATEMENT")) + + _cleanup_partial_backfill(bf, session) + + assert session.scalar(select(func.count()).select_from(Backfill).where(Backfill.id == bf_id)) == 0 + + def test_create_backfill_cleanup_removes_partial_dag_runs(self, session, dag_maker): + """Cleanup removes partial DagRuns, TIs, and BackfillDagRun rows alongside the Backfill.""" + from airflow.models.backfill import _cleanup_partial_backfill + + with dag_maker(session=session, dag_id="TEST_DAG_CLEANUP_DR", schedule="0 * * * *") as dag: + EmptyOperator(task_id="mytask") + session.commit() + + bf = Backfill( + dag_id=dag.dag_id, + from_date=pendulum.parse("2024-01-01"), + to_date=pendulum.parse("2024-02-01"), + max_active_runs=5, + dag_run_conf={}, + reprocess_behavior="none", + dag_model=dag, + triggering_user_name="test", + ) + session.add(bf) + session.commit() + + dr1 = dag_maker.create_dagrun( + logical_date=pendulum.parse("2024-01-01"), + run_type=DagRunType.BACKFILL_JOB, + backfill_id=bf.id, + state=DagRunState.QUEUED, + ) + session.add( + BackfillDagRun( + backfill_id=bf.id, + dag_run_id=dr1.id, + logical_date=pendulum.parse("2024-01-01"), + sort_ordinal=1, + ) + ) + + dr2 = dag_maker.create_dagrun( + logical_date=pendulum.parse("2024-01-02"), + run_type=DagRunType.BACKFILL_JOB, + backfill_id=bf.id, + state=DagRunState.QUEUED, + ) + session.add( + BackfillDagRun( + backfill_id=bf.id, + dag_run_id=dr2.id, + logical_date=pendulum.parse("2024-01-02"), + sort_ordinal=2, + ) + ) + session.commit() + + bf_id = bf.id + dr1_id = dr1.id + dr2_id = dr2.id + run_ids = [dr1.run_id, dr2.run_id] + + assert session.scalar(select(func.count()).select_from(Backfill).where(Backfill.id == bf_id)) == 1 + assert session.scalar(select(func.count()).select_from(DagRun).where(DagRun.id == dr1_id)) == 1 + assert session.scalar(select(func.count()).select_from(DagRun).where(DagRun.id == dr2_id)) == 1 + assert ( + session.scalar( + select(func.count()).select_from(BackfillDagRun).where(BackfillDagRun.backfill_id == bf_id) + ) + == 2 + ) + assert ( + session.scalar( + select(func.count()).select_from(TaskInstance).where(TaskInstance.run_id.in_(run_ids)) + ) + >= 2 + ) + + _cleanup_partial_backfill(bf, session) + + assert session.scalar(select(func.count()).select_from(Backfill).where(Backfill.id == bf_id)) == 0 + assert session.scalar(select(func.count()).select_from(DagRun).where(DagRun.id == dr1_id)) == 0 + assert session.scalar(select(func.count()).select_from(DagRun).where(DagRun.id == dr2_id)) == 0 + assert ( + session.scalar( + select(func.count()).select_from(BackfillDagRun).where(BackfillDagRun.backfill_id == bf_id) + ) + == 0 + ) + assert ( + session.scalar( + select(func.count()).select_from(TaskInstance).where(TaskInstance.run_id.in_(run_ids)) + ) + == 0 + ) + @pytest.mark.parametrize( "run_backwards", [ @@ -632,6 +825,32 @@ def test_should_respond_403(self, unauthorized_test_client, dag_maker, session): class TestCreateBackfillDryRun(TestBackfillEndpoint): + def test_create_backfill_dry_run_database_locked(self, session, dag_maker, test_client): + """SQLite 'database is locked' during backfill dry-run returns HTTP 503.""" + with dag_maker(session=session, dag_id="TEST_DAG_DRY_LOCK", schedule="0 * * * *") as dag: + EmptyOperator(task_id="mytask") + session.commit() + + data = { + "dag_id": dag.dag_id, + "from_date": to_iso(pendulum.parse("2024-01-01")), + "to_date": to_iso(pendulum.parse("2024-02-01")), + "max_active_runs": 5, + "run_backwards": False, + "dag_run_conf": {}, + } + + with mock.patch( + "airflow.api_fastapi.core_api.routes.public.backfills._do_dry_run", + side_effect=OperationalError( + "statement", "params", sqlite3.OperationalError("database is locked") + ), + ): + response = test_client.post("/backfills/dry_run", json=data) + + assert response.status_code == 503 + assert "database is locked" in response.json()["detail"].lower() + @pytest.mark.parametrize( ("reprocess_behavior", "expected_dates"), [ From 02ec156e29a5116e3dcff57ba151cc4877821820 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Mon, 13 Jul 2026 08:43:26 -0400 Subject: [PATCH 079/297] [v3-3-test] Fix scheduler firing on_failure_callback for heartbeat-timed-out retries (#66767) (#69824) When a worker stops heartbeating (OOMKill, node eviction), the scheduler's ``_purge_task_instances_without_heartbeats`` built a ``TaskCallbackRequest`` without ``task_callback_type``. The Dag processor's task-callback dispatch branches on that field: ``UP_FOR_RETRY`` runs ``on_retry_callback``, anything else (including ``None``) runs ``on_failure_callback``. So heartbeat-timeout cleanup always fired ``on_failure_callback`` even when the task still had retries remaining, producing spurious failure alerts for tasks that ultimately succeeded on retry. Set ``task_callback_type`` from ``ti.is_eligible_to_retry()``, the canonical retry-eligibility predicate, guarded by ``max_tries > 0``. The guard covers the one gap the predicate has here: this path doesn't load ``ti.task``, so the predicate falls back to ``try_number <= max_tries`` and drops the retries-configured check its task-loaded branch applies. Deferring to the predicate also keeps a ``RESTARTING`` task (cleared while running) retry- eligible past ``max_tries``, where a hand-rolled ``try_number <= max_tries`` check would have fired ``on_failure_callback``. closes: #65400 (cherry picked from commit f2403ccb58b32ae9a073b70d6648c11447d6c9b0) Signed-off-by: 1fanwang <1fannnw@gmail.com> Co-authored-by: Stefan Wang <1fannnw@gmail.com> Co-authored-by: kimhaggie --- .../src/airflow/jobs/scheduler_job_runner.py | 9 ++ .../tests/unit/jobs/test_scheduler_job.py | 115 ++++++++++++++++++ 2 files changed, 124 insertions(+) diff --git a/airflow-core/src/airflow/jobs/scheduler_job_runner.py b/airflow-core/src/airflow/jobs/scheduler_job_runner.py index 355f693db0a02..dce7c88bad71c 100644 --- a/airflow-core/src/airflow/jobs/scheduler_job_runner.py +++ b/airflow-core/src/airflow/jobs/scheduler_job_runner.py @@ -3548,12 +3548,21 @@ def _purge_task_instances_without_heartbeats( # Backfill dag_version_id for legacy tasks (Pydantic requires uuid.UUID). if not _ensure_ti_has_dag_version_id(ti, session, self.log): continue + # ti.task isn't loaded in this purge path, so is_eligible_to_retry() uses its + # no-task fallback (``try_number <= max_tries``), which skips the retries-configured + # check its task-loaded branch applies; guard with ``max_tries > 0`` so a task + # declared with retries=0 isn't treated as retry-eligible here. + if ti.max_tries > 0 and ti.is_eligible_to_retry(): + task_callback_type = TaskInstanceState.UP_FOR_RETRY + else: + task_callback_type = TaskInstanceState.FAILED request = TaskCallbackRequest( filepath=ti.dag_model.relative_fileloc or "", bundle_name=_hb_bundle_name, bundle_version=_hb_bundle_version, ti=ti, msg=str(task_instance_heartbeat_timeout_message_details), + task_callback_type=task_callback_type, context_from_server=TIRunContext( dag_run=DRDataModel.model_validate(ti.dag_run, from_attributes=True), max_tries=ti.max_tries, diff --git a/airflow-core/tests/unit/jobs/test_scheduler_job.py b/airflow-core/tests/unit/jobs/test_scheduler_job.py index d7b08e662b19b..f5aeceb34b39c 100644 --- a/airflow-core/tests/unit/jobs/test_scheduler_job.py +++ b/airflow-core/tests/unit/jobs/test_scheduler_job.py @@ -8608,6 +8608,121 @@ def test_scheduler_passes_context_from_server_on_heartbeat_timeout(self, dag_mak assert callback_request.context_from_server.dag_run.logical_date == dag_run.logical_date assert callback_request.context_from_server.max_tries == ti.max_tries + @pytest.mark.parametrize( + ("state", "retries", "try_number", "expected_callback_type", "expected_dispatched_callback"), + [ + pytest.param( + TaskInstanceState.RUNNING, + 0, + 1, + TaskInstanceState.FAILED, + "on_failure_callback", + id="no_retries", + ), + pytest.param( + TaskInstanceState.RUNNING, + 2, + 1, + TaskInstanceState.UP_FOR_RETRY, + "on_retry_callback", + id="retries_available_first_attempt", + ), + pytest.param( + TaskInstanceState.RUNNING, + 2, + 2, + TaskInstanceState.UP_FOR_RETRY, + "on_retry_callback", + id="retries_available_mid_chain", + ), + pytest.param( + TaskInstanceState.RUNNING, + 2, + 3, + TaskInstanceState.FAILED, + "on_failure_callback", + id="retries_exhausted", + ), + pytest.param( + TaskInstanceState.RESTARTING, + 1, + 5, + TaskInstanceState.UP_FOR_RETRY, + "on_retry_callback", + id="restarting_stays_eligible_past_max_tries", + ), + ], + ) + def test_heartbeat_timeout_sets_callback_type_by_retry_eligibility( + self, + dag_maker, + session, + state, + retries, + try_number, + expected_callback_type, + expected_dispatched_callback, + ): + """Heartbeat-timeout cleanup must populate ``task_callback_type`` so the Dag processor + fires ``on_retry_callback`` when the task still has retries left, not + ``on_failure_callback``. + + Reproduces the bug end-to-end through the actual scheduler purge path: + + 1. A TI is ``RUNNING`` (or ``RESTARTING``) with a stale ``last_heartbeat_at`` (worker + OOMKilled, node evicted, scheduler restarted, etc.). + 2. ``_find_and_purge_task_instances_without_heartbeats`` builds a + ``TaskCallbackRequest`` and hands it to the executor's ``send_callback``. + 3. The Dag processor branches on ``request.task_callback_type``: + ``UP_FOR_RETRY`` -> ``task.on_retry_callback``; anything else (including ``None``) + -> ``task.on_failure_callback``. See + ``airflow-core/src/airflow/dag_processing/processor.py``::``_execute_task_callbacks``. + + Before the fix, step 2 left ``task_callback_type`` as ``None``, so step 3 always fell + into the ``else`` branch and ``on_failure_callback`` fired even when the task still had + retries left -- producing spurious failure alerts for tasks that ultimately succeeded on + retry. + + The parametrized cases cover the full ``max_tries`` / ``try_number`` matrix for a + ``RUNNING`` TI -- no retries, retries available (first attempt and mid-chain), and + retries exhausted (``try_number > max_tries``) -- plus a ``RESTARTING`` TI (cleared + while running), which ``is_eligible_to_retry`` keeps retry-eligible even past + ``max_tries``. The ``expected_dispatched_callback`` column mirrors the Dag processor's + branch so the assertion captures the user-visible outcome, not just the field value. + """ + with dag_maker(dag_id=f"hb_timeout_r{retries}_t{try_number}", session=session): + EmptyOperator(task_id="test_task", retries=retries) + + dag_run = dag_maker.create_dagrun(run_id="test_run", state=DagRunState.RUNNING) + + mock_executor = MagicMock() + scheduler_job = Job() + self.job_runner = SchedulerJobRunner(scheduler_job, executors=[mock_executor]) + + ti = dag_run.get_task_instance(task_id="test_task") + ti.state = state + ti.try_number = try_number + ti.queued_by_job_id = scheduler_job.id + ti.last_heartbeat_at = timezone.utcnow() - timedelta(seconds=600) + session.merge(ti) + session.commit() + + self.job_runner._find_and_purge_task_instances_without_heartbeats() + + mock_executor.send_callback.assert_called_once() + request = mock_executor.send_callback.call_args[0][0] + assert isinstance(request, TaskCallbackRequest) + assert request.task_callback_type == expected_callback_type + # Mirror processor._execute_task_callbacks: UP_FOR_RETRY -> on_retry_callback, else + # on_failure_callback. Asserting the dispatched callback closes the loop on the + # user-visible behaviour, not just the field value. + dispatched_callback = ( + "on_retry_callback" + if request.task_callback_type is TaskInstanceState.UP_FOR_RETRY + else "on_failure_callback" + ) + assert dispatched_callback == expected_dispatched_callback + @pytest.mark.parametrize( ("retries", "callback_kind", "expected"), [ From 331d120c14593ab7f71bcb1fb4355c34a08b4524 Mon Sep 17 00:00:00 2001 From: Wei Lee Date: Tue, 14 Jul 2026 21:57:39 +0800 Subject: [PATCH 080/297] [v3-3-test] Fix pending partition run lookups for slash keys and duplicate rows (#69700) (#69844) --- .../core_api/openapi/_private_ui.yaml | 4 +- .../routes/ui/partitioned_dag_runs.py | 19 ++- .../ui/openapi-gen/requests/services.gen.ts | 6 +- .../ui/openapi-gen/requests/types.gen.ts | 2 +- .../routes/ui/test_partitioned_dag_runs.py | 161 ++++++++++++++++-- 5 files changed, 174 insertions(+), 18 deletions(-) diff --git a/airflow-core/src/airflow/api_fastapi/core_api/openapi/_private_ui.yaml b/airflow-core/src/airflow/api_fastapi/core_api/openapi/_private_ui.yaml index d786e259b4e77..a8adab13d4c63 100644 --- a/airflow-core/src/airflow/api_fastapi/core_api/openapi/_private_ui.yaml +++ b/airflow-core/src/airflow/api_fastapi/core_api/openapi/_private_ui.yaml @@ -181,7 +181,7 @@ paths: application/json: schema: $ref: '#/components/schemas/HTTPValidationError' - /ui/pending_partitioned_dag_run/{dag_id}/{partition_key}: + /ui/pending_partitioned_dag_run/{dag_id}: get: tags: - PartitionedDagRun @@ -199,7 +199,7 @@ paths: type: string title: Dag Id - name: partition_key - in: path + in: query required: true schema: type: string diff --git a/airflow-core/src/airflow/api_fastapi/core_api/routes/ui/partitioned_dag_runs.py b/airflow-core/src/airflow/api_fastapi/core_api/routes/ui/partitioned_dag_runs.py index e7f949e92cf73..96ab8c1c38502 100644 --- a/airflow-core/src/airflow/api_fastapi/core_api/routes/ui/partitioned_dag_runs.py +++ b/airflow-core/src/airflow/api_fastapi/core_api/routes/ui/partitioned_dag_runs.py @@ -342,7 +342,7 @@ def get_partitioned_dag_runs( @partitioned_dag_runs_router.get( - "/pending_partitioned_dag_run/{dag_id}/{partition_key}", + "/pending_partitioned_dag_run/{dag_id}", dependencies=[Depends(requires_access_asset(method="GET")), Depends(requires_access_dag(method="GET"))], ) def get_pending_partitioned_dag_run( @@ -351,6 +351,9 @@ def get_pending_partitioned_dag_run( session: SessionDep, ) -> PartitionedDagRunDetailResponse: """Return full details for pending PartitionedDagRun.""" + # partition_key is a query param, not a path segment: it is a free-form key + # (up to 250 chars) that may itself contain "/", which would otherwise be + # ambiguous (or mis-routed) as a path segment. partitioned_dag_run = session.execute( select( AssetPartitionDagRun.id, @@ -366,7 +369,11 @@ def get_pending_partitioned_dag_run( AssetPartitionDagRun.partition_key == partition_key, AssetPartitionDagRun.created_dag_run_id.is_(None), ) - ).one_or_none() + # Duplicate pending rows for the same (dag_id, partition_key) can exist + # after a crash; mirror _get_or_create_apdr and work on the latest one. + .order_by(AssetPartitionDagRun.id.desc()) + .limit(1) + ).first() if partitioned_dag_run is None: raise HTTPException( @@ -440,9 +447,15 @@ def get_pending_partitioned_dag_run( required_keys = [] received_count = 0 required_count = 1 - else: + elif is_rollup: received_count = len(received_keys) required_count = len(required_keys) + else: + # Match the list route's _compute_received_count: a non-rollup asset is + # satisfied by any single received event, so credit caps at 1 even if + # several distinct upstream keys mapped onto this one target key. + required_count = len(required_keys) + received_count = 1 if received_keys else 0 assets.append( PartitionedDagRunAssetResponse( asset_id=asset_row.id, diff --git a/airflow-core/src/airflow/ui/openapi-gen/requests/services.gen.ts b/airflow-core/src/airflow/ui/openapi-gen/requests/services.gen.ts index b934568d8ad9a..810e9a9a0f54f 100644 --- a/airflow-core/src/airflow/ui/openapi-gen/requests/services.gen.ts +++ b/airflow-core/src/airflow/ui/openapi-gen/requests/services.gen.ts @@ -4696,9 +4696,11 @@ export class PartitionedDagRunService { public static getPendingPartitionedDagRun(data: GetPendingPartitionedDagRunData): CancelablePromise { return __request(OpenAPI, { method: 'GET', - url: '/ui/pending_partitioned_dag_run/{dag_id}/{partition_key}', + url: '/ui/pending_partitioned_dag_run/{dag_id}', path: { - dag_id: data.dagId, + dag_id: data.dagId + }, + query: { partition_key: data.partitionKey }, errors: { diff --git a/airflow-core/src/airflow/ui/openapi-gen/requests/types.gen.ts b/airflow-core/src/airflow/ui/openapi-gen/requests/types.gen.ts index c27b0d8e16325..6433c3bfcd65a 100644 --- a/airflow-core/src/airflow/ui/openapi-gen/requests/types.gen.ts +++ b/airflow-core/src/airflow/ui/openapi-gen/requests/types.gen.ts @@ -8238,7 +8238,7 @@ export type $OpenApiTs = { }; }; }; - '/ui/pending_partitioned_dag_run/{dag_id}/{partition_key}': { + '/ui/pending_partitioned_dag_run/{dag_id}': { get: { req: GetPendingPartitionedDagRunData; res: { diff --git a/airflow-core/tests/unit/api_fastapi/core_api/routes/ui/test_partitioned_dag_runs.py b/airflow-core/tests/unit/api_fastapi/core_api/routes/ui/test_partitioned_dag_runs.py index 3bea3b87d01d3..2075aca6ac1c0 100644 --- a/airflow-core/tests/unit/api_fastapi/core_api/routes/ui/test_partitioned_dag_runs.py +++ b/airflow-core/tests/unit/api_fastapi/core_api/routes/ui/test_partitioned_dag_runs.py @@ -544,11 +544,13 @@ def test_list_route_total_required_includes_inactive_asset(self, test_client, da class TestGetPendingPartitionedDagRun: def test_should_response_401(self, unauthenticated_test_client): - response = unauthenticated_test_client.get("/pending_partitioned_dag_run/any_dag/any_key") + response = unauthenticated_test_client.get( + "/pending_partitioned_dag_run/any_dag?partition_key=any_key" + ) assert response.status_code == 401 def test_should_response_403(self, unauthorized_test_client): - response = unauthorized_test_client.get("/pending_partitioned_dag_run/any_dag/any_key") + response = unauthorized_test_client.get("/pending_partitioned_dag_run/any_dag?partition_key=any_key") assert response.status_code == 403 @pytest.mark.parametrize( @@ -583,7 +585,15 @@ def test_should_response_404(self, test_client, dag_maker, session, dag_id, part ) session.commit() - resp = test_client.get(f"/pending_partitioned_dag_run/{dag_id}/{partition_key}") + resp = test_client.get(f"/pending_partitioned_dag_run/{dag_id}?partition_key={partition_key}") + assert resp.status_code == 404 + + def test_missing_partition_key_query_param_returns_422(self, test_client): + resp = test_client.get("/pending_partitioned_dag_run/any_dag") + assert resp.status_code == 422 + + def test_empty_partition_key_query_param_returns_404(self, test_client): + resp = test_client.get("/pending_partitioned_dag_run/any_dag?partition_key=") assert resp.status_code == 404 @pytest.mark.parametrize( @@ -644,7 +654,7 @@ def test_should_response_200(self, test_client, dag_maker, session, num_assets, ) session.commit() - resp = test_client.get("/pending_partitioned_dag_run/detail_dag/2024-07-01") + resp = test_client.get("/pending_partitioned_dag_run/detail_dag?partition_key=2024-07-01") assert resp.status_code == 200 body = resp.json() assert body["dag_id"] == "detail_dag" @@ -677,7 +687,7 @@ def test_is_rollup_false_for_non_rollup_asset(self, test_client, dag_maker, sess session.add(AssetPartitionDagRun(target_dag_id="nr_detail_dag", partition_key="2024-07-01")) session.commit() - resp = test_client.get("/pending_partitioned_dag_run/nr_detail_dag/2024-07-01") + resp = test_client.get("/pending_partitioned_dag_run/nr_detail_dag?partition_key=2024-07-01") assert resp.status_code == 200 assets = resp.json()["assets"] assert len(assets) == 2 @@ -713,7 +723,7 @@ def test_is_rollup_true_for_default_rollup_mapper(self, test_client, dag_maker, session.add(AssetPartitionDagRun(target_dag_id="rollup_default_dag", partition_key="2024-06-03")) session.commit() - resp = test_client.get("/pending_partitioned_dag_run/rollup_default_dag/2024-06-03") + resp = test_client.get("/pending_partitioned_dag_run/rollup_default_dag?partition_key=2024-06-03") assert resp.status_code == 200 body = resp.json() assert body["total_required"] == 14 @@ -761,7 +771,7 @@ def test_is_rollup_true_for_rollup_asset(self, test_client, dag_maker, session): ) session.commit() - resp = test_client.get("/pending_partitioned_dag_run/rollup_detail_dag/2024-06-03") + resp = test_client.get("/pending_partitioned_dag_run/rollup_detail_dag?partition_key=2024-06-03") assert resp.status_code == 200 body = resp.json() assert body["total_required"] == 7 @@ -832,7 +842,9 @@ def test_rollup_mapper_failure_treats_asset_as_not_satisfied(self, test_client, ), mock.patch("airflow.api_fastapi.core_api.routes.ui.partitioned_dag_runs.log") as mock_log, ): - resp = test_client.get("/pending_partitioned_dag_run/rollup_warn_detail_dag/2024-06-03") + resp = test_client.get( + "/pending_partitioned_dag_run/rollup_warn_detail_dag?partition_key=2024-06-03" + ) assert resp.status_code == 200 a = resp.json()["assets"][0] @@ -873,7 +885,7 @@ def test_partitioned_dag_runs_asset_inactive_true_when_deactivated(self, test_cl session.delete(asset_active_row) session.commit() - resp = test_client.get("/pending_partitioned_dag_run/inactive_detail_dag/2024-07-01") + resp = test_client.get("/pending_partitioned_dag_run/inactive_detail_dag?partition_key=2024-07-01") assert resp.status_code == 200 assets = resp.json()["assets"] assert len(assets) == 1 @@ -896,8 +908,137 @@ def test_partitioned_dag_runs_asset_inactive_false_for_active_asset( session.add(AssetPartitionDagRun(target_dag_id="active_detail_dag", partition_key="2024-07-01")) session.commit() - resp = test_client.get("/pending_partitioned_dag_run/active_detail_dag/2024-07-01") + resp = test_client.get("/pending_partitioned_dag_run/active_detail_dag?partition_key=2024-07-01") assert resp.status_code == 200 assets = resp.json()["assets"] assert len(assets) == 1 assert assets[0]["asset_inactive"] is False + + def test_partition_key_containing_slash_round_trips(self, test_client, dag_maker, session): + """ + A partition key containing ``/`` must survive as a query parameter. + + As a path segment, a URL-encoded ``/`` (``%2F``) is decoded before Starlette + routing sees it, so any key containing a literal ``/`` would 404. Sending it + as a query parameter instead avoids that ambiguity entirely. + """ + asset = Asset(uri="s3://bucket/slash_key", name="slash_key") + with dag_maker( + dag_id="slash_key_dag", + schedule=PartitionedAssetTimetable(assets=asset), + serialized=True, + ): + EmptyOperator(task_id="t") + dag_maker.create_dagrun() + dag_maker.sync_dagbag_to_db() + + session.add(AssetPartitionDagRun(target_dag_id="slash_key_dag", partition_key="region/us")) + session.commit() + + resp = test_client.get( + "/pending_partitioned_dag_run/slash_key_dag", params={"partition_key": "region/us"} + ) + assert resp.status_code == 200 + body = resp.json() + assert body["dag_id"] == "slash_key_dag" + assert body["partition_key"] == "region/us" + + def test_duplicate_pending_apdr_rows_return_latest(self, test_client, dag_maker, session): + """ + Duplicate pending APDR rows for the same (dag_id, partition_key) must not 500. + + The model docstring for ``AssetPartitionDagRun`` says callers should always + work on the latest row when duplicates exist; the route must do the same + instead of raising ``MultipleResultsFound`` from ``.one_or_none()``. + """ + asset = Asset(uri="s3://bucket/dup_apdr", name="dup_apdr") + with dag_maker( + dag_id="dup_apdr_dag", + schedule=PartitionedAssetTimetable(assets=asset), + serialized=True, + ): + EmptyOperator(task_id="t") + dag_maker.create_dagrun() + dag_maker.sync_dagbag_to_db() + + asset = session.scalar(select(AssetModel).where(AssetModel.uri == "s3://bucket/dup_apdr")) + + # Older duplicate row: no received events. + stale_pdr = AssetPartitionDagRun(target_dag_id="dup_apdr_dag", partition_key="2024-08-01") + session.add(stale_pdr) + session.flush() + + # Newer duplicate row (higher id): has a received event. + latest_pdr = AssetPartitionDagRun(target_dag_id="dup_apdr_dag", partition_key="2024-08-01") + session.add(latest_pdr) + session.flush() + event = AssetEvent(asset_id=asset.id, timestamp=pendulum.now()) + session.add(event) + session.flush() + session.add( + PartitionedAssetKeyLog( + asset_id=asset.id, + asset_event_id=event.id, + asset_partition_dag_run_id=latest_pdr.id, + source_partition_key="2024-08-01", + target_dag_id="dup_apdr_dag", + target_partition_key="2024-08-01", + ) + ) + session.commit() + + resp = test_client.get("/pending_partitioned_dag_run/dup_apdr_dag?partition_key=2024-08-01") + assert resp.status_code == 200 + body = resp.json() + assert body["id"] == latest_pdr.id + assert body["total_received"] == 1 + + def test_non_rollup_many_to_one_received_capped_at_one(self, test_client, dag_maker, session): + """ + Detail route must cap non-rollup received credit at 1, matching the list route. + + Multiple distinct upstream ``source_partition_key`` values logged against the + same non-rollup asset (a many-to-one mapper) must not inflate ``received_count`` + past ``required_count`` — otherwise the detail view shows e.g. "2/1" while the + list view shows "1/1" for the same APDR. + """ + asset = Asset(uri="s3://bucket/many_to_one", name="many_to_one") + with dag_maker( + dag_id="many_to_one_dag", + schedule=PartitionedAssetTimetable(assets=asset), + serialized=True, + ): + EmptyOperator(task_id="t") + dag_maker.create_dagrun() + dag_maker.sync_dagbag_to_db() + + asset = session.scalar(select(AssetModel).where(AssetModel.uri == "s3://bucket/many_to_one")) + pdr = AssetPartitionDagRun(target_dag_id="many_to_one_dag", partition_key="2024-08-01") + session.add(pdr) + session.flush() + + for source_key in ("2024-08-01", "different-key"): + event = AssetEvent(asset_id=asset.id, timestamp=pendulum.now()) + session.add(event) + session.flush() + session.add( + PartitionedAssetKeyLog( + asset_id=asset.id, + asset_event_id=event.id, + asset_partition_dag_run_id=pdr.id, + source_partition_key=source_key, + target_dag_id="many_to_one_dag", + target_partition_key="2024-08-01", + ) + ) + session.commit() + + resp = test_client.get("/pending_partitioned_dag_run/many_to_one_dag?partition_key=2024-08-01") + assert resp.status_code == 200 + body = resp.json() + assert body["total_required"] == 1 + assert body["total_received"] == 1 + asset_resp = body["assets"][0] + assert asset_resp["required_count"] == 1 + assert asset_resp["received_count"] == 1 + assert asset_resp["received"] is True From f6109538055da28978c70af6befb71b7fd7849e1 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Tue, 14 Jul 2026 14:06:38 -0400 Subject: [PATCH 081/297] [v3-3-test] Pin databricks-sql-connector>=4.0.0 in generated constraints (#69863) (#69871) thrift 0.24.0 (apache/thrift#3584) is incompatible with the databricks-sql-connector thrift<=0.23.0 requirement, so during highest resolution the resolver preferred the newest thrift and downgraded the connector, which in turn dragged the databricks provider back to an old version. Pinning the connector to >=4.0.0 keeps PyPI constraints installable with a current databricks-sql-connector and provider. (cherry picked from commit 98b4b01394c6624d02b53e87d47f6c26fc74dc2f) closes: #69603 Co-authored-by: Jarek Potiuk --- scripts/in_container/run_generate_constraints.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/scripts/in_container/run_generate_constraints.py b/scripts/in_container/run_generate_constraints.py index 3b3a839d1e5e5..8ead3a6703dd7 100755 --- a/scripts/in_container/run_generate_constraints.py +++ b/scripts/in_container/run_generate_constraints.py @@ -398,10 +398,15 @@ def generate_constraints_pypi_providers(config_params: ConfigParams) -> None: # does not yet carry this cap, so we mirror it here so PyPI constraints stay installable # until the SQLAlchemy fix is released. Tracked upstream at # https://github.com/sqlalchemy/sqlalchemy/issues/13306 - # + # * databricks-sql-connector>=4.0.0 - added to keep databricks-sql-connector from downgrading + # because of https://github.com/apache/thrift/pull/3584 - which shipped thrift 0.24.0. Older + # versions of databricks-sql-connector do not have the thrift<=0.23.0 limitation, so the + # resolver preferred the latest thrift over the latest connector and downgraded the connector. + # This is tracked in https://github.com/databricks/databricks-sql-python/issues/859 additional_constraints_for_highest_resolution: list[str] = [ "pyarrow>=22.0.0; python_version >= '3.14'", "pymysql>=1.0.3,<1.2", + "databricks-sql-connector>=4.0.0", ] result = run_command( From 3f64a978ada68ab33e215eecc39d5a3ebab42d75 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Wed, 15 Jul 2026 16:55:07 +0800 Subject: [PATCH 082/297] [v3-3-test] Make cheat-sheet test independent of rich table padding (#69802) (#69892) (cherry picked from commit fc51ac92017b154d76ca6e8ffa23a2a382e33ec7) Signed-off-by: PoAn Yang Co-authored-by: PoAn Yang --- .../cli/commands/test_cheat_sheet_command.py | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/airflow-core/tests/unit/cli/commands/test_cheat_sheet_command.py b/airflow-core/tests/unit/cli/commands/test_cheat_sheet_command.py index b00bb56059304..5c4baa8d807e5 100644 --- a/airflow-core/tests/unit/cli/commands/test_cheat_sheet_command.py +++ b/airflow-core/tests/unit/cli/commands/test_cheat_sheet_command.py @@ -16,6 +16,7 @@ # under the License. from __future__ import annotations +import re from unittest import mock from airflow.cli import cli_parser @@ -72,20 +73,25 @@ def noop(): ] ALL_COMMANDS = """\ -airflow cmd_b | Help text D +airflow cmd_b | Help text D """ SECTION_A = """\ -airflow cmd_a cmd_b | Help text B -airflow cmd_a cmd_c | Help text C +airflow cmd_a cmd_b | Help text B +airflow cmd_a cmd_c | Help text C """ SECTION_E = """\ -airflow cmd_e cmd_f | Help text F -airflow cmd_e cmd_g | Help text G +airflow cmd_e cmd_f | Help text F +airflow cmd_e cmd_g | Help text G """ +def normalize_spaces(text: str) -> str: + """Collapse runs of spaces so assertions do not depend on the rich version's exact column padding.""" + return re.sub(r" +", " ", text) + + class TestCheatSheetCommand: @classmethod def setup_class(cls): @@ -96,7 +102,7 @@ def test_should_display_index(self, stdout_capture): with stdout_capture as temp_stdout: args = self.parser.parse_args(["cheat-sheet"]) args.func(args) - output = temp_stdout.getvalue() + output = normalize_spaces(temp_stdout.getvalue()) assert ALL_COMMANDS in output assert SECTION_A in output assert SECTION_E in output From 610d1348183e646c03d3f9cd1f3b0f1f7dfbffbf Mon Sep 17 00:00:00 2001 From: Pierre Jeambrun Date: Wed, 15 Jul 2026 15:14:18 +0200 Subject: [PATCH 083/297] Remove redundant ORM result uniquing in core queries (#69913) (#69918) The affected queries select unique entities or only eager-load scalar relationships, so identity filtering adds unnecessary result processing. (cherry picked from commit 4e0f297dc44424f903285f3eed3c08647b39cc17) Co-authored-by: Ephraim Anierobi --- .../airflow/api_fastapi/common/db/dag_runs.py | 2 +- .../core_api/routes/public/dag_run.py | 6 ++-- .../core_api/routes/public/task_instances.py | 32 +++++++------------ .../core_api/routes/ui/deadlines.py | 2 +- .../api_fastapi/core_api/routes/ui/grid.py | 2 +- .../services/public/task_instances.py | 4 +-- .../src/airflow/jobs/scheduler_job_runner.py | 18 ++++------- 7 files changed, 26 insertions(+), 40 deletions(-) diff --git a/airflow-core/src/airflow/api_fastapi/common/db/dag_runs.py b/airflow-core/src/airflow/api_fastapi/common/db/dag_runs.py index f9ae3d6c3b6c6..1508ea8528ebb 100644 --- a/airflow-core/src/airflow/api_fastapi/common/db/dag_runs.py +++ b/airflow-core/src/airflow/api_fastapi/common/db/dag_runs.py @@ -127,7 +127,7 @@ def attach_dag_versions_to_runs(dag_runs: Sequence[DagRun], *, session: Session) .where(DagVersion.id.in_(all_version_ids)) .options(joinedload(DagVersion.bundle)) ) - versions_by_id = {dv.id: dv for dv in session.scalars(dv_query).unique()} + versions_by_id = {dv.id: dv for dv in session.scalars(dv_query)} versions_per_run: dict[tuple[str, str], dict[UUID, DagVersion]] = defaultdict(dict) for row in rows: diff --git a/airflow-core/src/airflow/api_fastapi/core_api/routes/public/dag_run.py b/airflow-core/src/airflow/api_fastapi/core_api/routes/public/dag_run.py index 015e17648aba3..617b70ea8941c 100644 --- a/airflow-core/src/airflow/api_fastapi/core_api/routes/public/dag_run.py +++ b/airflow-core/src/airflow/api_fastapi/core_api/routes/public/dag_run.py @@ -618,7 +618,7 @@ def get_dag_runs( dag_run_select = order_by.to_orm(dag_run_select, reversed=True) dag_run_select = apply_cursor_filter(dag_run_select, token, order_by, is_backward=is_backward) - fetched = list(session.scalars(dag_run_select).unique()) + fetched = list(session.scalars(dag_run_select)) has_more = len(fetched) > page_limit dag_runs = fetched[:page_limit] @@ -648,7 +648,7 @@ def get_dag_runs( limit=limit, session=session, ) - dag_runs = list(session.scalars(dag_run_select).unique()) + dag_runs = list(session.scalars(dag_run_select)) attach_dag_versions_to_runs(dag_runs, session=session) return DAGRunCollectionResponse( @@ -930,7 +930,7 @@ def get_list_dag_runs_batch( session=session, ) - dag_runs = list(session.scalars(dag_runs_select).unique()) + dag_runs = list(session.scalars(dag_runs_select)) attach_dag_versions_to_runs(dag_runs, session=session) return DAGRunCollectionResponse( diff --git a/airflow-core/src/airflow/api_fastapi/core_api/routes/public/task_instances.py b/airflow-core/src/airflow/api_fastapi/core_api/routes/public/task_instances.py index 68120ace15380..30726b9524da3 100644 --- a/airflow-core/src/airflow/api_fastapi/core_api/routes/public/task_instances.py +++ b/airflow-core/src/airflow/api_fastapi/core_api/routes/public/task_instances.py @@ -970,16 +970,12 @@ def _collect_relatives(run_id: str, direction: Literal["upstream", "downstream"] # dag.clear() returns TIs without this relationship loaded; re-query with joinedload. # populate_existing=True ensures the joinedload updates TIs already in the identity map. if task_instances: - task_instances = ( - session.scalars( - select(TI) - .options(joinedload(TI.rendered_task_instance_fields)) - .where(TI.id.in_([ti.id for ti in task_instances])) - .execution_options(populate_existing=True) - ) - .unique() - .all() - ) + task_instances = session.scalars( + select(TI) + .options(joinedload(TI.rendered_task_instance_fields)) + .where(TI.id.in_([ti.id for ti in task_instances])) + .execution_options(populate_existing=True) + ).all() return TaskInstanceCollectionResponse( task_instances=[TaskInstanceResponse.model_validate(ti) for ti in task_instances], @@ -1136,16 +1132,12 @@ def patch_task_instance_dry_run( # set_task_instance_state() returns TIs without this relationship loaded; re-query with joinedload. # populate_existing=True ensures the joinedload updates TIs already in the identity map. if tis: - tis = ( - session.scalars( - select(TI) - .options(joinedload(TI.rendered_task_instance_fields)) - .where(TI.id.in_([ti.id for ti in tis])) - .execution_options(populate_existing=True) - ) - .unique() - .all() - ) + tis = session.scalars( + select(TI) + .options(joinedload(TI.rendered_task_instance_fields)) + .where(TI.id.in_([ti.id for ti in tis])) + .execution_options(populate_existing=True) + ).all() return TaskInstanceCollectionResponse( task_instances=[ diff --git a/airflow-core/src/airflow/api_fastapi/core_api/routes/ui/deadlines.py b/airflow-core/src/airflow/api_fastapi/core_api/routes/ui/deadlines.py index d9cfea10d6d94..06eda42ed8957 100644 --- a/airflow-core/src/airflow/api_fastapi/core_api/routes/ui/deadlines.py +++ b/airflow-core/src/airflow/api_fastapi/core_api/routes/ui/deadlines.py @@ -129,7 +129,7 @@ def get_deadlines( session=session, ) - deadlines = session.scalars(deadlines_select).unique() + deadlines = session.scalars(deadlines_select) if dag_run_id != "~" and total_entries == 0: dag_run = session.scalar(select(DagRun).where(DagRun.dag_id == dag_id, DagRun.run_id == dag_run_id)) diff --git a/airflow-core/src/airflow/api_fastapi/core_api/routes/ui/grid.py b/airflow-core/src/airflow/api_fastapi/core_api/routes/ui/grid.py index 72d92fbef2c2f..ee81e2887c7f3 100644 --- a/airflow-core/src/airflow/api_fastapi/core_api/routes/ui/grid.py +++ b/airflow-core/src/airflow/api_fastapi/core_api/routes/ui/grid.py @@ -328,7 +328,7 @@ def get_grid_runs( limit=limit, return_total_entries=False, ) - results = session.execute(dag_runs_select_filter).unique().all() + results = session.execute(dag_runs_select_filter).all() dag_runs = [run for run, _ in results] attach_dag_versions_to_runs(dag_runs, session=session) grid_runs = [] diff --git a/airflow-core/src/airflow/api_fastapi/core_api/services/public/task_instances.py b/airflow-core/src/airflow/api_fastapi/core_api/services/public/task_instances.py index a5874035e27f8..c1738f7382ad9 100644 --- a/airflow-core/src/airflow/api_fastapi/core_api/services/public/task_instances.py +++ b/airflow-core/src/airflow/api_fastapi/core_api/services/public/task_instances.py @@ -140,9 +140,7 @@ def _reload_tis_with_rendered_fields(tis: list[TI], session: Session) -> list[TI .options(joinedload(TI.rendered_task_instance_fields)) .where(TI.id.in_([ti.id for ti in tis])) .execution_options(populate_existing=True) - ) - .unique() - .all() + ).all() ) diff --git a/airflow-core/src/airflow/jobs/scheduler_job_runner.py b/airflow-core/src/airflow/jobs/scheduler_job_runner.py index dce7c88bad71c..c37887c8c4bd0 100644 --- a/airflow-core/src/airflow/jobs/scheduler_job_runner.py +++ b/airflow-core/src/airflow/jobs/scheduler_job_runner.py @@ -2391,19 +2391,15 @@ def _create_dag_runs(self, dag_models: Collection[DagModel], session: Session) - # as DagModel.dag_id and DagModel.next_dagrun # This list is used to verify if the DagRun already exist so that we don't attempt to create # duplicate DagRuns - existing_dagrun_objects = ( - session.scalars( - select(DagRun) - .where( - tuple_(DagRun.dag_id, DagRun.logical_date).in_( - (dm.dag_id, dm.next_dagrun) for dm in dag_models - ) + existing_dagrun_objects = session.scalars( + select(DagRun) + .where( + tuple_(DagRun.dag_id, DagRun.logical_date).in_( + (dm.dag_id, dm.next_dagrun) for dm in dag_models ) - .options(load_only(DagRun.dag_id, DagRun.logical_date)) ) - .unique() - .all() - ) + .options(load_only(DagRun.dag_id, DagRun.logical_date)) + ).all() existing_dagruns = {(x.dag_id, x.logical_date): x for x in existing_dagrun_objects} # backfill runs are not created by scheduler and their concurrency is separate From 73148c5f790d6f3fef1e67066fbaa56c774099be Mon Sep 17 00:00:00 2001 From: Pierre Jeambrun Date: Wed, 15 Jul 2026 18:02:53 +0200 Subject: [PATCH 084/297] Stop re-rendering the whole Grid on every hover (#69912) (#69928) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Every cell in the Dag Grid view subscribed to a shared hover context, so hovering any cell changed the context value and re-rendered the entire grid. On a ~20-run x 20-task grid that measured ~3 React commits plus full-grid render work per hover — the interaction lag reported in the issue, which only gets worse as the Dag grows. Move the run-column / task-row crosshair highlight to a single delegated pointerover handler that toggles CSS classes on the matching cells by data-run-id / data-task-id, so hovering does no React render work at all. The same delegated root covers the gantt so the shared row highlight stays in sync. Measured on the same grid, React commits per hover dropped from ~3.0 to ~0.07. related: #69531 (cherry picked from commit ae6188dbc79c1445998f0290d39e634a70ff460b) --- .../ui/src/context/hover/HoverProvider.tsx | 31 -- .../src/airflow/ui/src/context/hover/index.ts | 21 - .../airflow/ui/src/context/hover/useHover.ts | 31 -- .../ui/src/layouts/Details/DetailsLayout.tsx | 405 +++++++++--------- .../ui/src/layouts/Details/Gantt/Gantt.tsx | 16 +- .../Details/Gantt/GanttTimeline.test.tsx | 7 +- .../layouts/Details/Gantt/GanttTimeline.tsx | 9 +- .../ui/src/layouts/Details/Grid/Bar.tsx | 12 +- .../ui/src/layouts/Details/Grid/GridTI.tsx | 14 +- .../Details/Grid/TaskInstancesColumn.tsx | 15 +- .../ui/src/layouts/Details/Grid/TaskNames.tsx | 19 +- .../Details/Grid/gridHover.css} | 18 +- .../Details/Grid/useGridCrosshairHover.ts | 95 ++++ 13 files changed, 323 insertions(+), 370 deletions(-) delete mode 100644 airflow-core/src/airflow/ui/src/context/hover/HoverProvider.tsx delete mode 100644 airflow-core/src/airflow/ui/src/context/hover/index.ts delete mode 100644 airflow-core/src/airflow/ui/src/context/hover/useHover.ts rename airflow-core/src/airflow/ui/src/{context/hover/Context.ts => layouts/Details/Grid/gridHover.css} (67%) create mode 100644 airflow-core/src/airflow/ui/src/layouts/Details/Grid/useGridCrosshairHover.ts diff --git a/airflow-core/src/airflow/ui/src/context/hover/HoverProvider.tsx b/airflow-core/src/airflow/ui/src/context/hover/HoverProvider.tsx deleted file mode 100644 index 0afb9bea99c6a..0000000000000 --- a/airflow-core/src/airflow/ui/src/context/hover/HoverProvider.tsx +++ /dev/null @@ -1,31 +0,0 @@ -/*! - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance - * with the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ -import type { PropsWithChildren } from "react"; -import { useState } from "react"; - -import { HoverContext } from "./Context"; - -export const HoverProvider = ({ children }: PropsWithChildren) => { - const [hoveredRunId, setHoveredRunId] = useState(undefined); - const [hoveredTaskId, setHoveredTaskId] = useState(undefined); - - const value = { hoveredRunId, hoveredTaskId, setHoveredRunId, setHoveredTaskId }; - - return {children}; -}; diff --git a/airflow-core/src/airflow/ui/src/context/hover/index.ts b/airflow-core/src/airflow/ui/src/context/hover/index.ts deleted file mode 100644 index a1f52fc6f640e..0000000000000 --- a/airflow-core/src/airflow/ui/src/context/hover/index.ts +++ /dev/null @@ -1,21 +0,0 @@ -/*! - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance - * with the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ -export { HoverProvider } from "./HoverProvider"; -export { useHover } from "./useHover"; -export type { HoverContextType } from "./Context"; diff --git a/airflow-core/src/airflow/ui/src/context/hover/useHover.ts b/airflow-core/src/airflow/ui/src/context/hover/useHover.ts deleted file mode 100644 index 541d4306d9d41..0000000000000 --- a/airflow-core/src/airflow/ui/src/context/hover/useHover.ts +++ /dev/null @@ -1,31 +0,0 @@ -/*! - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance - * with the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ -import { useContext } from "react"; - -import { HoverContext } from "./Context"; - -export const useHover = () => { - const context = useContext(HoverContext); - - if (context === undefined) { - throw new Error("useHover must be used within a HoverProvider"); - } - - return context; -}; diff --git a/airflow-core/src/airflow/ui/src/layouts/Details/DetailsLayout.tsx b/airflow-core/src/airflow/ui/src/layouts/Details/DetailsLayout.tsx index 736f81828eb1d..c55f3bd0d3709 100644 --- a/airflow-core/src/airflow/ui/src/layouts/Details/DetailsLayout.tsx +++ b/airflow-core/src/airflow/ui/src/layouts/Details/DetailsLayout.tsx @@ -48,42 +48,37 @@ import { DEFAULT_DAG_VIEW_KEY } from "src/constants/localStorage"; import { SearchParamsKeys } from "src/constants/searchParams"; import { VersionIndicatorOptions } from "src/constants/showVersionIndicatorOptions"; import { GroupsProvider } from "src/context/groups"; -import { HoverProvider, useHover } from "src/context/hover"; import { useGridRuns } from "src/queries/useGridRuns.ts"; import { DagBreadcrumb } from "./DagBreadcrumb"; import { Gantt } from "./Gantt/Gantt"; import { Graph } from "./Graph"; import { Grid } from "./Grid"; +import { useGridCrosshairHover } from "./Grid/useGridCrosshairHover"; import { NavTabs, type NavTab } from "./NavTabs"; import { PanelButtons } from "./PanelButtons"; -// Separate component so useHover can be called inside HoverProvider. +// Shared scroll container for the grid + gantt in the combined view. const SharedScrollBox = ({ children, scrollRef, }: { readonly children: ReactNode; readonly scrollRef: RefObject; -}) => { - const { setHoveredTaskId } = useHover(); - - return ( - setHoveredTaskId(undefined)} - overflowX="hidden" - overflowY="auto" - ref={scrollRef} - style={{ scrollbarGutter: "stable" }} - w="100%" - > - {children} - - ); -}; +}) => ( + + {children} + +); type Props = { readonly error?: unknown; @@ -99,6 +94,13 @@ export const DetailsLayout = ({ children, error, isLoading, outletContext, tabs const { data: dag } = useDagServiceGetDag({ dagId }); const [dagView, setDagView] = useLocalStorage(DEFAULT_DAG_VIEW_KEY, "grid"); const panelGroupRef = useRef(null); + // Root for the delegated grid/gantt crosshair-hover handler (covers both the + // grid and the gantt so their shared row highlight stays in sync, with no + // React re-render on hover). + const gridHoverRootRef = useRef(null); + + useGridCrosshairHover(gridHoverRootRef); + const [searchParams, setSearchParams] = useSearchParams(); // Global setting: applies to all Dags (intentionally not scoped to dagId) @@ -216,107 +218,70 @@ export const DetailsLayout = ({ children, error, isLoading, outletContext, tabs const defaultSize = Math.max(dagView === "graph" ? 70 : 20, minSize); return ( - - - - - - - - - - {dag === undefined ? undefined : ( - - )} - - - - - - {isRightPanelCollapsed ? ( - setIsRightPanelCollapsed(false)} - position="absolute" - right={direction === "ltr" ? "0" : undefined} - size="2xs" - top="50%" - zIndex={10} - > - {direction === "ltr" ? : } - - ) : undefined} - + + + + + + + + {dag === undefined ? undefined : ( + + )} + + + + + + {isRightPanelCollapsed ? ( + setIsRightPanelCollapsed(false)} + position="absolute" + right={direction === "ltr" ? "0" : undefined} + size="2xs" + top="50%" + zIndex={10} > - - - - - {dagView === "graph" ? ( - - ) : dagView === "gantt" && Boolean(runId) ? ( - - - - - - - ) : ( - + {direction === "ltr" ? : } + + ) : undefined} + + + + + + {dagView === "graph" ? ( + + ) : dagView === "gantt" && Boolean(runId) ? ( + + - - )} - - - - {!isRightPanelCollapsed && ( - <> - { - if (!isDragging) { - const zoom = getZoom(); + + + + ) : ( + + + + )} + + + + {!isRightPanelCollapsed && ( + <> + { + if (!isDragging) { + const zoom = getZoom(); - void fitView({ maxZoom: zoom, minZoom: zoom }); - } - }} - > - - + void fitView({ maxZoom: zoom, minZoom: zoom }); + } + }} + > + + - {/* Collapse button positioned next to the resize handle */} + {/* Collapse button positioned next to the resize handle */} - - - setIsRightPanelCollapsed(true)} - position="absolute" - right={direction === "rtl" ? "0" : undefined} - size="2xs" - top="50%" - zIndex={2} - > - {direction === "ltr" ? : } - - {children} - {Boolean(error) || (warningData?.dag_warnings.length ?? 0) > 0 ? ( - <> - - - + + + setIsRightPanelCollapsed(true)} + position="absolute" + right={direction === "rtl" ? "0" : undefined} + size="2xs" + top="50%" + zIndex={2} + > + {direction === "ltr" ? : } + + {children} + {Boolean(error) || (warningData?.dag_warnings.length ?? 0) > 0 ? ( + <> + + + - - - ) : undefined} - - - - - + + + ) : undefined} + + + + - - - )} - - + + + + )} + - - + + ); }; diff --git a/airflow-core/src/airflow/ui/src/layouts/Details/Gantt/Gantt.tsx b/airflow-core/src/airflow/ui/src/layouts/Details/Gantt/Gantt.tsx index 2c20ff0e2a0be..6383d57623a35 100644 --- a/airflow-core/src/airflow/ui/src/layouts/Details/Gantt/Gantt.tsx +++ b/airflow-core/src/airflow/ui/src/layouts/Details/Gantt/Gantt.tsx @@ -24,7 +24,6 @@ import { useParams, useSearchParams } from "react-router-dom"; import { useGanttServiceGetGanttData } from "openapi/queries"; import type { DagRunState, DagRunType } from "openapi/requests/types.gen"; import { useGroups } from "src/context/groups"; -import { useHover } from "src/context/hover"; import { useTimezone } from "src/context/timezone"; import { NavigationModes, useNavigation } from "src/hooks/navigation"; import { @@ -73,7 +72,6 @@ export const Gantt = ({ const [searchParams] = useSearchParams(); const { openGroupIds, toggleGroupId } = useGroups(); const { selectedTimezone } = useTimezone(); - const { setHoveredTaskId } = useHover(); const filterRoot = searchParams.get("root") ?? undefined; const includeUpstream = searchParams.get("upstream") === "true"; @@ -153,10 +151,6 @@ export const Gantt = ({ return undefined; } - const handleStandaloneMouseLeave = () => { - setHoveredTaskId(undefined); - }; - const timeline = Boolean(selectedRun) && dagId ? ( - + {timeline} diff --git a/airflow-core/src/airflow/ui/src/layouts/Details/Gantt/GanttTimeline.test.tsx b/airflow-core/src/airflow/ui/src/layouts/Details/Gantt/GanttTimeline.test.tsx index fc990e2a0dcdb..59b32551acbb6 100644 --- a/airflow-core/src/airflow/ui/src/layouts/Details/Gantt/GanttTimeline.test.tsx +++ b/airflow-core/src/airflow/ui/src/layouts/Details/Gantt/GanttTimeline.test.tsx @@ -21,7 +21,6 @@ import type { PropsWithChildren, RefObject } from "react"; import { createRef } from "react"; import { afterEach, describe, expect, it, vi } from "vitest"; -import { HoverProvider } from "src/context/hover"; import { ROW_HEIGHT } from "src/layouts/Details/Grid/constants"; import type { GridTask } from "src/layouts/Details/Grid/utils"; import { Wrapper } from "src/utils/Wrapper"; @@ -45,11 +44,7 @@ vi.mock("@tanstack/react-virtual", () => ({ }), })); -const TestWrapper = ({ children }: PropsWithChildren) => ( - - {children} - -); +const TestWrapper = ({ children }: PropsWithChildren) => {children}; // Shared time range: 10:00 → 10:10 UTC on 2024-03-14 const MIN_MS = new Date("2024-03-14T10:00:00Z").getTime(); diff --git a/airflow-core/src/airflow/ui/src/layouts/Details/Gantt/GanttTimeline.tsx b/airflow-core/src/airflow/ui/src/layouts/Details/Gantt/GanttTimeline.tsx index 43c28dba56b52..b3ec2bac29d38 100644 --- a/airflow-core/src/airflow/ui/src/layouts/Details/Gantt/GanttTimeline.tsx +++ b/airflow-core/src/airflow/ui/src/layouts/Details/Gantt/GanttTimeline.tsx @@ -26,7 +26,6 @@ import { Link, useLocation, useParams } from "react-router-dom"; import type { LightGridTaskInstanceSummary } from "openapi/requests/types.gen"; import { StateIcon } from "src/components/StateIcon"; import TaskInstanceTooltip from "src/components/TaskInstanceTooltip"; -import { useHover } from "src/context/hover"; import { GANTT_AXIS_HEIGHT_PX, GANTT_TOP_PADDING_PX, @@ -115,7 +114,6 @@ export const GanttTimeline = ({ }: Props) => { const location = useLocation(); const { groupId: selectedGroupId, taskId: selectedTaskId } = useParams(); - const { hoveredTaskId, setHoveredTaskId } = useHover(); const [bodyWidthPx, setBodyWidthPx] = useState(0); const bodyRef = useRef(null); @@ -291,7 +289,6 @@ export const GanttTimeline = ({ : allSegments; const taskId = node.id; const isSelected = selectedTaskId === taskId || selectedGroupId === taskId; - const isHovered = hoveredTaskId === taskId; const gridSummary = summaryByTaskId.get(taskId); return ( @@ -309,11 +306,11 @@ export const GanttTimeline = ({ zIndex={1} > setHoveredTaskId(taskId)} - onMouseLeave={() => setHoveredTaskId(undefined)} overflow="hidden" position="relative" px="3px" diff --git a/airflow-core/src/airflow/ui/src/layouts/Details/Grid/Bar.tsx b/airflow-core/src/airflow/ui/src/layouts/Details/Grid/Bar.tsx index 0cfce328ac45e..c0757a4a81e4d 100644 --- a/airflow-core/src/airflow/ui/src/layouts/Details/Grid/Bar.tsx +++ b/airflow-core/src/airflow/ui/src/layouts/Details/Grid/Bar.tsx @@ -21,7 +21,6 @@ import { useParams, useSearchParams } from "react-router-dom"; import { RunTypeIcon } from "src/components/RunTypeIcon"; import { VersionIndicatorOptions } from "src/constants/showVersionIndicatorOptions"; -import { useHover } from "src/context/hover"; import { GridButton } from "./GridButton"; import { BundleVersionIndicator, DagVersionIndicator } from "./VersionIndicator"; @@ -42,20 +41,15 @@ type Props = { export const Bar = ({ max, onClick, run, showVersionIndicatorMode }: Props) => { const { dagId = "", runId } = useParams(); const [searchParams] = useSearchParams(); - const { hoveredRunId, setHoveredRunId } = useHover(); const isSelected = runId === run.run_id; - const isHovered = hoveredRunId === run.run_id; const search = searchParams.toString(); - const handleMouseEnter = () => setHoveredRunId(run.run_id); - const handleMouseLeave = () => setHoveredRunId(undefined); - return ( diff --git a/airflow-core/src/airflow/ui/src/layouts/Details/Grid/GridTI.tsx b/airflow-core/src/airflow/ui/src/layouts/Details/Grid/GridTI.tsx index 6cd21d252f915..0baa557b4c0f7 100644 --- a/airflow-core/src/airflow/ui/src/layouts/Details/Grid/GridTI.tsx +++ b/airflow-core/src/airflow/ui/src/layouts/Details/Grid/GridTI.tsx @@ -22,7 +22,6 @@ import { Link, useLocation, useParams, useSearchParams } from "react-router-dom" import type { LightGridTaskInstanceSummary } from "openapi/requests/types.gen"; import { StateIcon } from "src/components/StateIcon"; import TaskInstanceTooltip from "src/components/TaskInstanceTooltip"; -import { useHover } from "src/context/hover"; import { buildTaskInstanceUrl } from "src/utils/links"; type Props = { @@ -37,7 +36,6 @@ type Props = { }; export const GridTI = ({ dagId, instance, isGroup, isMapped, onClick, runId, taskId }: Props) => { - const { hoveredTaskId, setHoveredTaskId } = useHover(); const { groupId: selectedGroupId, taskId: selectedTaskId } = useParams(); const location = useLocation(); @@ -52,29 +50,25 @@ export const GridTI = ({ dagId, instance, isGroup, isMapped, onClick, runId, tas taskId, }); - const handleMouseEnter = () => setHoveredTaskId(taskId); - const handleMouseLeave = () => setHoveredTaskId(undefined); - // Remove try_number query param when navigating to reset to the // latest try of the task instance and avoid issues with invalid try numbers: // https://github.com/apache/airflow/issues/56977 searchParams.delete("try_number"); const redirectionSearch = searchParams.toString(); - // Determine background: selected takes priority over hovered const isSelected = selectedTaskId === taskId || selectedGroupId === taskId; - const isHovered = hoveredTaskId === taskId; return ( ({ index, size: ROW_HEIGHT, start: index * ROW_HEIGHT })); @@ -78,17 +75,13 @@ export const TaskInstancesColumn = ({ ); const hasMixedVersions = versionNumbers.size > 1; - const isHovered = hoveredRunId === run.run_id; - const hideRowBorders = isSelected || isHovered; - - const handleMouseEnter = () => setHoveredRunId(run.run_id); - const handleMouseLeave = () => setHoveredRunId(undefined); + const hideRowBorders = isSelected; return ( `${depth * 0.75 + 0.5}rem`; export const TaskNames = ({ nodes, onRowClick, virtualItems }: Props) => { const { t: translate } = useTranslation("dag"); - const { hoveredTaskId, setHoveredTaskId } = useHover(); const { toggleGroupId } = useGroups(); const { dagId = "", groupId, taskId } = useParams(); const [searchParams] = useSearchParams(); - const handleMouseEnter = (event: MouseEvent) => { - const { nodeId } = event.currentTarget.dataset; - - if (nodeId !== undefined) { - setHoveredTaskId(nodeId); - } - }; - - const handleMouseLeave = () => setHoveredTaskId(undefined); - const handleToggleGroup = (event: MouseEvent) => { event.preventDefault(); event.stopPropagation(); @@ -98,23 +86,22 @@ export const TaskNames = ({ nodes, onRowClick, virtualItems }: Props) => { } const isSelected = node.id === taskId || node.id === groupId; - const isHovered = hoveredTaskId === node.id; return ( void; - setHoveredTaskId: (taskId: string | undefined) => void; -}; - -export const HoverContext = createContext(undefined); +/* + * Grid/Gantt hover crosshair. The classes are toggled imperatively by + * useGridCrosshairHover (outside the React lifecycle), so hovering never + * re-renders the grid. Selected cells keep their own background. + */ +[data-run-id].grid-hover-col:not([data-selected="true"]), +[data-task-id].grid-hover-row:not([data-selected="true"]) { + background-color: var(--chakra-colors-brand-muted); +} diff --git a/airflow-core/src/airflow/ui/src/layouts/Details/Grid/useGridCrosshairHover.ts b/airflow-core/src/airflow/ui/src/layouts/Details/Grid/useGridCrosshairHover.ts new file mode 100644 index 0000000000000..0125a66bbb761 --- /dev/null +++ b/airflow-core/src/airflow/ui/src/layouts/Details/Grid/useGridCrosshairHover.ts @@ -0,0 +1,95 @@ +/*! + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +import { useEffect } from "react"; +import type { RefObject } from "react"; + +import "./gridHover.css"; + +const COL_CLASS = "grid-hover-col"; +const ROW_CLASS = "grid-hover-row"; + +/** + * Highlight the hovered run column and task row (the crosshair) via direct DOM + * class toggling instead of React context. A single delegated ``pointerover`` + * listener on ``rootRef`` reads the hovered cell's ``data-run-id`` / + * ``data-task-id`` and toggles the highlight classes on the matching elements, + * so moving the mouse across a large grid does zero React render work. The root + * must contain both the grid and (when present) the gantt so the shared row + * highlight stays in sync across the two. + */ +export const useGridCrosshairHover = (rootRef: RefObject) => { + useEffect(() => { + const root = rootRef.current; + + if (!root) { + return undefined; + } + + let currentRun: string | undefined; + let currentTask: string | undefined; + + const clear = (className: string) => { + root.querySelectorAll(`.${className}`).forEach((element) => element.classList.remove(className)); + }; + + const apply = (attribute: string, value: string, className: string) => { + root + .querySelectorAll(`[${attribute}="${CSS.escape(value)}"]`) + .forEach((element) => element.classList.add(className)); + }; + + const onPointerOver = (event: PointerEvent) => { + const target = event.target instanceof Element ? event.target : null; + const runId = target?.closest("[data-run-id]")?.dataset.runId; + const taskId = target?.closest("[data-task-id]")?.dataset.taskId; + + if (runId !== currentRun) { + clear(COL_CLASS); + if (runId !== undefined) { + apply("data-run-id", runId, COL_CLASS); + } + currentRun = runId; + } + + if (taskId !== currentTask) { + clear(ROW_CLASS); + if (taskId !== undefined) { + apply("data-task-id", taskId, ROW_CLASS); + } + currentTask = taskId; + } + }; + + const onPointerLeave = () => { + clear(COL_CLASS); + clear(ROW_CLASS); + currentRun = undefined; + currentTask = undefined; + }; + + root.addEventListener("pointerover", onPointerOver); + root.addEventListener("pointerleave", onPointerLeave); + + return () => { + root.removeEventListener("pointerover", onPointerOver); + root.removeEventListener("pointerleave", onPointerLeave); + onPointerLeave(); + }; + }, [rootRef]); +}; From 2b0b19c8c29fb20c92fb371bdae82697691dfe47 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Thu, 16 Jul 2026 15:00:47 +0200 Subject: [PATCH 085/297] [v3-3-test] Re-render only the changed column when Grid summaries stream in (#69917) (#69958) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The Dag Grid streams TI summaries as one NDJSON line per run and stores them in a Map; every line replaced the Map and re-rendered Grid. Because useVirtualizer (@tanstack/react-virtual) is on the React Compiler's known-incompatible list, the compiler skips optimizing all of Grid — so flatNodes and the click handlers got fresh references on every render and every column re-rendered on every line, not just the run whose summary arrived. On a ~20-run x 30-task grid that was 117 spurious column re-renders per load. Memoize the values handed down to the columns by hand — the escape hatch the compiler's own bailout message points to for incompatible-library APIs. Measured on the same grid: column re-renders per load dropped 171 -> 54 (all legitimate), grid load render work ~47% lower. Also drop a redundant immediate stream restart: an unconditional refresh tick fired the instant the interval effect mounted, aborting and reopening the just-opened mount stream (an AbortError on every grid mount with active runs). (cherry picked from commit 93d722e9b82faca703e34ea0e5461fbd4689bdaf) related: #69531 Co-authored-by: Pierre Jeambrun --- .../airflow/ui/src/layouts/Details/Grid/Grid.tsx | 15 ++++++++++----- .../airflow/ui/src/queries/useGridTISummaries.ts | 6 ++++-- 2 files changed, 14 insertions(+), 7 deletions(-) diff --git a/airflow-core/src/airflow/ui/src/layouts/Details/Grid/Grid.tsx b/airflow-core/src/airflow/ui/src/layouts/Details/Grid/Grid.tsx index eed38a10ad48f..a1cb9d36e991c 100644 --- a/airflow-core/src/airflow/ui/src/layouts/Details/Grid/Grid.tsx +++ b/airflow-core/src/airflow/ui/src/layouts/Details/Grid/Grid.tsx @@ -20,7 +20,7 @@ import { Box, Flex } from "@chakra-ui/react"; import { useVirtualizer } from "@tanstack/react-virtual"; import dayjs from "dayjs"; import dayjsDuration from "dayjs/plugin/duration"; -import { useRef } from "react"; +import { useCallback, useMemo, useRef } from "react"; import type { RefObject } from "react"; import { useParams, useSearchParams } from "react-router-dom"; @@ -141,7 +141,12 @@ export const Grid = ({ showVersionIndicatorMode, }); - const { flatNodes } = flattenNodes(dagStructure, openGroupIds); + // React Compiler skips optimizing this whole component: `useVirtualizer` (@tanstack/react-virtual) is + // on the compiler's known-incompatible list — its return value exposes functions that can't be + // memoized safely — so it declines to memoize anything in Grid. Without the manual memoization here + // and on the click handlers below, `flatNodes` and the handlers get fresh references every render, so + // each TI-summaries stream line re-renders every column instead of only the run whose summary changed. + const { flatNodes } = useMemo(() => flattenNodes(dagStructure, openGroupIds), [dagStructure, openGroupIds]); const taskNameColumnWidthPx = showGantt ? estimateTaskNameColumnWidthPx(flatNodes) : undefined; @@ -166,9 +171,9 @@ export const Grid = ({ tasks: flatNodes, }); - const handleRowClick = () => setMode(NavigationModes.TASK); - const handleCellClick = () => setMode(NavigationModes.TI); - const handleColumnClick = () => setMode(NavigationModes.RUN); + const handleRowClick = useCallback(() => setMode(NavigationModes.TASK), [setMode]); + const handleCellClick = useCallback(() => setMode(NavigationModes.TI), [setMode]); + const handleColumnClick = useCallback(() => setMode(NavigationModes.RUN), [setMode]); const rowVirtualizer = useVirtualizer({ count: flatNodes.length, diff --git a/airflow-core/src/airflow/ui/src/queries/useGridTISummaries.ts b/airflow-core/src/airflow/ui/src/queries/useGridTISummaries.ts index 1ec67127b3f02..d90b2f0078ece 100644 --- a/airflow-core/src/airflow/ui/src/queries/useGridTISummaries.ts +++ b/airflow-core/src/airflow/ui/src/queries/useGridTISummaries.ts @@ -144,8 +144,10 @@ export const useGridTiSummariesStream = ({ return undefined; } - // Kick off an immediate refresh so the stream doesn't have to wait for the first interval to elapse. - setRefreshTick((tick) => tick + 1); + // The stream already fetches on mount and whenever runIdsKey changes, so there is no first-interval + // wait to avoid. Bumping refreshTick here would abort that just-opened mount stream and immediately + // reopen it — a redundant connection plus an AbortError on every grid mount — so let the interval be + // the only re-stream trigger. const timer = setInterval(() => { setRefreshTick((tick) => tick + 1); }, baseRefetchInterval); From f49070829a8553b8c0851bd75aaadb046e3e38a8 Mon Sep 17 00:00:00 2001 From: Brent Bovenzi Date: Thu, 16 Jul 2026 13:29:47 -0400 Subject: [PATCH 086/297] [v3-3-test] Manually backport 68200 & 69883 (#69978) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Add keyboard shortcut help dialog to discover available shortcuts (? key) (#68200) * feat: keyboard shortcuts help dialog (press ?) * remove unnecessary useCallback and useMemo hooks * drop grid keyboard shortcut tooltip * centralize shortcut definitions in a single catalog * use nesting to reuse concept translation keys * register code viewer fullscreen shortcut in the catalog (cherry picked from commit 70bb308b19acbae0f04075063f8546eb5bc0e200) * [v3-3-test] Clean up graph/grid UI (#69883) Backport of #69883 to v3-3-test. Applied on top of the #68200 backport, so the only branch-specific adaptation is dropping the showPresetFilters change to GridFilters.tsx — the Preset Filters feature (#68484) is not on v3-3-test. (cherry picked from commit 707b1ccf137269d488a8b9f7e7fbc9a32cd7b296) --------- Co-authored-by: Yeonguk Choo --- .../ui/public/i18n/locales/ar/dag.json | 5 - .../ui/public/i18n/locales/ca/dag.json | 5 - .../ui/public/i18n/locales/de/dag.json | 5 - .../ui/public/i18n/locales/en/common.json | 41 ++++ .../ui/public/i18n/locales/en/dag.json | 5 - .../ui/public/i18n/locales/fr/dag.json | 5 - .../ui/public/i18n/locales/he/dag.json | 5 - .../ui/public/i18n/locales/hi/dag.json | 5 - .../ui/public/i18n/locales/hu/dag.json | 5 - .../ui/public/i18n/locales/ko/dag.json | 5 - .../ui/public/i18n/locales/pl/dag.json | 5 - .../ui/public/i18n/locales/tr/dag.json | 5 - .../ui/public/i18n/locales/zh-CN/dag.json | 5 - .../components/Clear/Run/ClearRunButton.tsx | 13 +- .../TaskInstance/ClearTaskInstanceButton.tsx | 13 +- .../FilterBar/filters/TextSearchFilter.tsx | 13 +- .../ui/src/components/Graph/TaskNode.tsx | 3 +- .../ui/src/components/Graph/elkGraphUtils.ts | 2 +- .../ui/src/components/GraphTaskFilters.tsx | 9 +- .../KeyboardShortcutsModal.tsx | 106 +++++++++++ .../formatShortcutCombo.test.ts | 41 ++++ .../KeyboardShortcuts/formatShortcutCombo.ts | 66 +++++++ .../src/components/KeyboardShortcuts/index.ts | 20 ++ .../components/MarkAs/Run/MarkRunAsButton.tsx | 23 +-- .../TaskGroup/MarkTaskGroupAsButton.tsx | 23 +-- .../TaskInstance/MarkTaskInstanceAsButton.tsx | 23 +-- .../airflow/ui/src/components/SearchBar.tsx | 13 +- .../SearchDags/SearchDagsButton.tsx | 15 +- .../airflow/ui/src/components/TaskName.tsx | 2 - .../src/context/keyboardShortcuts/Context.ts | 58 ++++++ .../ShortcutRegistryProvider.tsx | 45 +++++ .../ui/src/context/keyboardShortcuts/index.ts | 22 +++ .../context/keyboardShortcuts/shortcuts.ts | 179 ++++++++++++++++++ .../keyboardShortcuts/useShortcutRegistry.ts | 23 +++ .../hooks/navigation/useKeyboardNavigation.ts | 19 +- .../airflow/ui/src/hooks/useShortcut.test.tsx | 111 +++++++++++ .../src/airflow/ui/src/hooks/useShortcut.ts | 76 ++++++++ .../src/airflow/ui/src/layouts/BaseLayout.tsx | 2 + .../ui/src/layouts/Details/PanelButtons.tsx | 41 ++-- airflow-core/src/airflow/ui/src/main.tsx | 5 +- .../airflow/ui/src/pages/Dag/Code/Code.tsx | 14 +- .../ui/src/pages/Dag/Overview/FailedLogs.tsx | 8 +- .../ui/src/pages/GroupTaskInstance/Header.tsx | 4 +- .../TaskInstance/Logs/LogSearchInput.tsx | 13 +- .../ui/src/pages/TaskInstance/Logs/Logs.tsx | 33 +++- .../TaskInstance/Logs/TaskLogContent.tsx | 15 +- 46 files changed, 965 insertions(+), 189 deletions(-) create mode 100644 airflow-core/src/airflow/ui/src/components/KeyboardShortcuts/KeyboardShortcutsModal.tsx create mode 100644 airflow-core/src/airflow/ui/src/components/KeyboardShortcuts/formatShortcutCombo.test.ts create mode 100644 airflow-core/src/airflow/ui/src/components/KeyboardShortcuts/formatShortcutCombo.ts create mode 100644 airflow-core/src/airflow/ui/src/components/KeyboardShortcuts/index.ts create mode 100644 airflow-core/src/airflow/ui/src/context/keyboardShortcuts/Context.ts create mode 100644 airflow-core/src/airflow/ui/src/context/keyboardShortcuts/ShortcutRegistryProvider.tsx create mode 100644 airflow-core/src/airflow/ui/src/context/keyboardShortcuts/index.ts create mode 100644 airflow-core/src/airflow/ui/src/context/keyboardShortcuts/shortcuts.ts create mode 100644 airflow-core/src/airflow/ui/src/context/keyboardShortcuts/useShortcutRegistry.ts create mode 100644 airflow-core/src/airflow/ui/src/hooks/useShortcut.test.tsx create mode 100644 airflow-core/src/airflow/ui/src/hooks/useShortcut.ts diff --git a/airflow-core/src/airflow/ui/public/i18n/locales/ar/dag.json b/airflow-core/src/airflow/ui/public/i18n/locales/ar/dag.json index 44d0b882563d1..6253b5c4a3183 100644 --- a/airflow-core/src/airflow/ui/public/i18n/locales/ar/dag.json +++ b/airflow-core/src/airflow/ui/public/i18n/locales/ar/dag.json @@ -118,11 +118,6 @@ "viewInExternal": "عرض السجلات في {{name}} (المحاولة {{attempt}})", "warning": "تحذير" }, - "navigation": { - "navigation": "التنقل: {{arrow}}", - "openGraphFilters": "مرشحات المهام: Ctrl+Shift+F", - "toggleGroup": "تبديل المجموعة: المسافة" - }, "notFound": { "back": "رجوع", "backToDags": "العودة إلى Dags", diff --git a/airflow-core/src/airflow/ui/public/i18n/locales/ca/dag.json b/airflow-core/src/airflow/ui/public/i18n/locales/ca/dag.json index c90bc964f6df5..3cd77f612a7dc 100644 --- a/airflow-core/src/airflow/ui/public/i18n/locales/ca/dag.json +++ b/airflow-core/src/airflow/ui/public/i18n/locales/ca/dag.json @@ -106,11 +106,6 @@ "viewInExternal": "Veure els registres a {{name}} (intent {{attempt}})", "warning": "ADVERTIMENT" }, - "navigation": { - "navigation": "Navegació: Shift+{{arrow}}", - "openGraphFilters": "Filtres de tasques: Ctrl+Maj+F", - "toggleGroup": "Alternar grup: Espai" - }, "notFound": { "back": "Enrere", "backToDags": "Tornar als Dags", diff --git a/airflow-core/src/airflow/ui/public/i18n/locales/de/dag.json b/airflow-core/src/airflow/ui/public/i18n/locales/de/dag.json index 829a1aaac4d5c..d48f59856f61f 100644 --- a/airflow-core/src/airflow/ui/public/i18n/locales/de/dag.json +++ b/airflow-core/src/airflow/ui/public/i18n/locales/de/dag.json @@ -106,11 +106,6 @@ "viewInExternal": "Protokoll in {{name}} (Versuch {{attempt}}) ansehen", "warning": "WARNING" }, - "navigation": { - "navigation": "Navigation: Umschalttaste+{{arrow}}", - "openGraphFilters": "Task-Filter: Strg+Umschalt+F", - "toggleGroup": "Gruppen umschalten: Leertaste" - }, "notFound": { "back": "Zurück", "backToDags": "Zurück zu Dags", diff --git a/airflow-core/src/airflow/ui/public/i18n/locales/en/common.json b/airflow-core/src/airflow/ui/public/i18n/locales/en/common.json index 88a348e1de54d..70e600b332dae 100644 --- a/airflow-core/src/airflow/ui/public/i18n/locales/en/common.json +++ b/airflow-core/src/airflow/ui/public/i18n/locales/en/common.json @@ -226,6 +226,47 @@ }, "selectLanguage": "Select Language", "selected": "Selected", + "shortcuts": { + "categories": { + "code": "Code", + "dagView": "Dag View", + "filters": "Filters", + "global": "Global", + "logs": "Logs", + "navigation": "Navigation", + "runActions": "Run & Task Actions", + "search": "Search" + }, + "descriptions": { + "clearRun": "Clear $t(dagRun_one)", + "clearTaskInstance": "Clear $t(taskInstance_one)", + "downloadLogs": "Download logs", + "focusFilterSearch": "Focus filter search", + "focusLogSearch": "Search logs", + "focusSearch": "Focus search", + "markRunFailed": "Mark $t(dagRun_one) as failed", + "markRunSuccess": "Mark $t(dagRun_one) as success", + "markTaskFailed": "Mark $t(task_one) as failed", + "markTaskGroupFailed": "Mark $t(taskGroup_one) as failed", + "markTaskGroupSuccess": "Mark $t(taskGroup_one) as success", + "markTaskSuccess": "Mark $t(task_one) as success", + "navigateTasks": "Navigate $t(task_other)", + "openGraphFilters": "Open graph filters", + "scrollBottom": "Scroll to bottom", + "scrollTop": "Scroll to top", + "searchDags": "Search $t(dag_other)", + "showHelp": "Show $t(shortcuts.title)", + "toggleExpand": "Expand or collapse all groups", + "toggleFullscreen": "Toggle fullscreen", + "toggleGraphGrid": "Toggle graph / grid view", + "toggleSource": "Toggle source", + "toggleTaskGroup": "Expand or collapse $t(taskGroup_one)", + "toggleTimestamp": "Toggle timestamps", + "toggleWrap": "Toggle $t(wrap.wrap)" + }, + "empty": "No keyboard shortcuts are available on this page.", + "title": "Keyboard Shortcuts" + }, "showDetailsPanel": "Show Details Panel", "signedInAs": "Signed in as", "source": { diff --git a/airflow-core/src/airflow/ui/public/i18n/locales/en/dag.json b/airflow-core/src/airflow/ui/public/i18n/locales/en/dag.json index b51add8d5ec09..907fba9e5eac4 100644 --- a/airflow-core/src/airflow/ui/public/i18n/locales/en/dag.json +++ b/airflow-core/src/airflow/ui/public/i18n/locales/en/dag.json @@ -102,11 +102,6 @@ "viewInExternal": "View logs in {{name}} (attempt {{attempt}})", "warning": "WARNING" }, - "navigation": { - "navigation": "Navigation: Shift+{{arrow}}", - "openGraphFilters": "Task Filters: Ctrl+Shift+F", - "toggleGroup": "Toggle group: Space" - }, "notFound": { "back": "Go Back", "backToDags": "Back to Dags", diff --git a/airflow-core/src/airflow/ui/public/i18n/locales/fr/dag.json b/airflow-core/src/airflow/ui/public/i18n/locales/fr/dag.json index de7952e02204c..b3a970fdf9d33 100644 --- a/airflow-core/src/airflow/ui/public/i18n/locales/fr/dag.json +++ b/airflow-core/src/airflow/ui/public/i18n/locales/fr/dag.json @@ -109,11 +109,6 @@ "viewInExternal": "Voir les journaux dans {{name}} (tentative {{attempt}})", "warning": "AVERTISSEMENT" }, - "navigation": { - "navigation": "Navigation : {{arrow}}", - "openGraphFilters": "Filtres de tâches : Ctrl+Maj+F", - "toggleGroup": "Basculer le groupe : Espace" - }, "notFound": { "back": "Retour", "backToDags": "Retour aux Dags", diff --git a/airflow-core/src/airflow/ui/public/i18n/locales/he/dag.json b/airflow-core/src/airflow/ui/public/i18n/locales/he/dag.json index 0214ee911024e..29b902030b718 100644 --- a/airflow-core/src/airflow/ui/public/i18n/locales/he/dag.json +++ b/airflow-core/src/airflow/ui/public/i18n/locales/he/dag.json @@ -109,11 +109,6 @@ "viewInExternal": "צפה ברישומים ב-{{name}} (ניסיון {{attempt}})", "warning": "WARNING" }, - "navigation": { - "navigation": "ניווט: {{arrow}}", - "openGraphFilters": "פילטרים משימות: Ctrl+Shift+F", - "toggleGroup": "החלפת קבוצה: רווח" - }, "notFound": { "back": "חזור", "backToDags": "חזור ל-Dags", diff --git a/airflow-core/src/airflow/ui/public/i18n/locales/hi/dag.json b/airflow-core/src/airflow/ui/public/i18n/locales/hi/dag.json index cbd6a675e78dd..8e436ab7a4b0f 100644 --- a/airflow-core/src/airflow/ui/public/i18n/locales/hi/dag.json +++ b/airflow-core/src/airflow/ui/public/i18n/locales/hi/dag.json @@ -106,11 +106,6 @@ "viewInExternal": "{{name}} में लॉग्स देखें (प्रयास {{attempt}})", "warning": "WARNING" }, - "navigation": { - "navigation": "नेवीगेशन: Shift+{{arrow}}", - "openGraphFilters": "Task फ़िल्टर: Ctrl+Shift+F", - "toggleGroup": "ग्रुप टॉगल करें: Space" - }, "notFound": { "back": "वापस जाएं", "backToDags": "डैग्स पर वापस जाएं", diff --git a/airflow-core/src/airflow/ui/public/i18n/locales/hu/dag.json b/airflow-core/src/airflow/ui/public/i18n/locales/hu/dag.json index 62b7a4cb7907c..a4a8c1cb057bf 100644 --- a/airflow-core/src/airflow/ui/public/i18n/locales/hu/dag.json +++ b/airflow-core/src/airflow/ui/public/i18n/locales/hu/dag.json @@ -106,11 +106,6 @@ "viewInExternal": "Naplók megtekintése itt: {{name}} (próbálkozás: {{attempt}})", "warning": "FIGYELMEZTETÉS" }, - "navigation": { - "navigation": "Navigáció: {{arrow}}", - "openGraphFilters": "Feladat szűrők: Ctrl+Shift+F", - "toggleGroup": "Csoport váltása: Szóköz" - }, "notFound": { "back": "Vissza", "backToDags": "Vissza a Dag-ekhez", diff --git a/airflow-core/src/airflow/ui/public/i18n/locales/ko/dag.json b/airflow-core/src/airflow/ui/public/i18n/locales/ko/dag.json index 30c09fb580417..9b71c510afddd 100644 --- a/airflow-core/src/airflow/ui/public/i18n/locales/ko/dag.json +++ b/airflow-core/src/airflow/ui/public/i18n/locales/ko/dag.json @@ -102,11 +102,6 @@ "viewInExternal": "{{name}}에서 로그 보기 (시도 {{attempt}})", "warning": "경고" }, - "navigation": { - "navigation": "탐색: Shift+{{arrow}}", - "openGraphFilters": "태스크 필터: Ctrl+Shift+F", - "toggleGroup": "그룹 전환: Space" - }, "notFound": { "back": "뒤로", "backToDags": "Dags로 돌아가기", diff --git a/airflow-core/src/airflow/ui/public/i18n/locales/pl/dag.json b/airflow-core/src/airflow/ui/public/i18n/locales/pl/dag.json index a8c500309f3d3..0f29dda9351ed 100644 --- a/airflow-core/src/airflow/ui/public/i18n/locales/pl/dag.json +++ b/airflow-core/src/airflow/ui/public/i18n/locales/pl/dag.json @@ -112,11 +112,6 @@ "viewInExternal": "Zobacz logi w {{name}} (próba {{attempt}})", "warning": "WARNING" }, - "navigation": { - "navigation": "Przewiń: {{arrow}}", - "openGraphFilters": "Filtry zadań: Ctrl+Shift+F", - "toggleGroup": "Przełącz grupę: Space" - }, "notFound": { "back": "Wróć", "backToDags": "Powrót do Dagów", diff --git a/airflow-core/src/airflow/ui/public/i18n/locales/tr/dag.json b/airflow-core/src/airflow/ui/public/i18n/locales/tr/dag.json index 7770759b5e78e..60b91f86bfd61 100644 --- a/airflow-core/src/airflow/ui/public/i18n/locales/tr/dag.json +++ b/airflow-core/src/airflow/ui/public/i18n/locales/tr/dag.json @@ -106,11 +106,6 @@ "viewInExternal": "Günlükleri {{name}} içinde görüntüle (deneme {{attempt}})", "warning": "UYARI" }, - "navigation": { - "navigation": "Gezin: {{arrow}}", - "openGraphFilters": "Görev Filtreleri: Ctrl+Shift+F", - "toggleGroup": "Grubu aç/kapat: Space" - }, "notFound": { "back": "Geri Dön", "backToDags": "Dag'lere Geri Dön", diff --git a/airflow-core/src/airflow/ui/public/i18n/locales/zh-CN/dag.json b/airflow-core/src/airflow/ui/public/i18n/locales/zh-CN/dag.json index 25dcfaf83c228..6a0c4613be05c 100644 --- a/airflow-core/src/airflow/ui/public/i18n/locales/zh-CN/dag.json +++ b/airflow-core/src/airflow/ui/public/i18n/locales/zh-CN/dag.json @@ -106,11 +106,6 @@ "viewInExternal": "在 {{name}} 中查看日志(重试 {{attempt}})", "warning": "WARNING" }, - "navigation": { - "navigation": "导航: {{arrow}}", - "openGraphFilters": "任务筛选器: Ctrl+Shift+F", - "toggleGroup": "展开/收起分组: 空格键" - }, "notFound": { "back": "返回", "backToDags": "返回 Dags", diff --git a/airflow-core/src/airflow/ui/src/components/Clear/Run/ClearRunButton.tsx b/airflow-core/src/airflow/ui/src/components/Clear/Run/ClearRunButton.tsx index 6cbc3aa08b004..c4c040a1c1bc5 100644 --- a/airflow-core/src/airflow/ui/src/components/Clear/Run/ClearRunButton.tsx +++ b/airflow-core/src/airflow/ui/src/components/Clear/Run/ClearRunButton.tsx @@ -17,12 +17,13 @@ * under the License. */ import { useDisclosure } from "@chakra-ui/react"; -import { useHotkeys } from "react-hotkeys-hook"; import { useTranslation } from "react-i18next"; import { CgRedo } from "react-icons/cg"; import type { DAGRunResponse } from "openapi/requests/types.gen"; import { IconButton } from "src/components/ui"; +import { SHORTCUTS } from "src/context/keyboardShortcuts"; +import { useShortcut } from "src/hooks/useShortcut"; import ClearRunDialog from "./ClearRunDialog"; @@ -35,13 +36,13 @@ const ClearRunButton = ({ dagRun, isHotkeyEnabled = false }: Props) => { const { onClose, onOpen, open } = useDisclosure(); const { t: translate } = useTranslation(); - useHotkeys( - "shift+c", - () => { + useShortcut({ + ...SHORTCUTS.runActions.clearRun, + callback: () => { onOpen(); }, - { enabled: isHotkeyEnabled }, - ); + options: { enabled: isHotkeyEnabled }, + }); return ( <> diff --git a/airflow-core/src/airflow/ui/src/components/Clear/TaskInstance/ClearTaskInstanceButton.tsx b/airflow-core/src/airflow/ui/src/components/Clear/TaskInstance/ClearTaskInstanceButton.tsx index 84fc78c1e687b..07bfc56f80c45 100644 --- a/airflow-core/src/airflow/ui/src/components/Clear/TaskInstance/ClearTaskInstanceButton.tsx +++ b/airflow-core/src/airflow/ui/src/components/Clear/TaskInstance/ClearTaskInstanceButton.tsx @@ -17,13 +17,14 @@ * under the License. */ import { useDisclosure } from "@chakra-ui/react"; -import { useHotkeys } from "react-hotkeys-hook"; import { useTranslation } from "react-i18next"; import { CgRedo } from "react-icons/cg"; import type { LightGridTaskInstanceSummary, TaskInstanceResponse } from "openapi/requests/types.gen"; import { ClearGroupTaskInstanceDialog } from "src/components/Clear/TaskInstance/ClearGroupTaskInstanceDialog"; import { IconButton } from "src/components/ui"; +import { SHORTCUTS } from "src/context/keyboardShortcuts"; +import { useShortcut } from "src/hooks/useShortcut"; import ClearTaskInstanceDialog from "./ClearTaskInstanceDialog"; @@ -58,17 +59,17 @@ const ClearTaskInstanceButton = ({ const selectedInstance = taskInstance ?? groupTaskInstance; - useHotkeys( - "shift+c", - () => { + useShortcut({ + ...SHORTCUTS.runActions.clearTaskInstance, + callback: () => { if (onOpen && selectedInstance) { onOpen(selectedInstance); } else { onOpenInternal(); } }, - { enabled: isHotkeyEnabled }, - ); + options: { enabled: isHotkeyEnabled }, + }); const label = allMapped ? isHotkeyEnabled diff --git a/airflow-core/src/airflow/ui/src/components/FilterBar/filters/TextSearchFilter.tsx b/airflow-core/src/airflow/ui/src/components/FilterBar/filters/TextSearchFilter.tsx index 32bfa29231849..09f1c5d3ee4ba 100644 --- a/airflow-core/src/airflow/ui/src/components/FilterBar/filters/TextSearchFilter.tsx +++ b/airflow-core/src/airflow/ui/src/components/FilterBar/filters/TextSearchFilter.tsx @@ -18,11 +18,12 @@ */ import { HStack } from "@chakra-ui/react"; import { useRef } from "react"; -import { useHotkeys } from "react-hotkeys-hook"; import { LuRegex } from "react-icons/lu"; import { AdvancedSearchToggle } from "src/components/AdvancedSearchToggle"; +import { SHORTCUTS } from "src/context/keyboardShortcuts"; import { useAdvancedSearch } from "src/hooks/useAdvancedSearch"; +import { useShortcut } from "src/hooks/useShortcut"; import { InputWithAddon } from "../../ui"; import { FilterPill } from "../FilterPill"; @@ -42,15 +43,15 @@ export const TextSearchFilter = ({ filter, onChange, onRemove }: FilterPluginPro onChange(newValue || undefined); }; - useHotkeys( - "mod+k", - () => { + useShortcut({ + ...SHORTCUTS.search.focusFilterSearch, + callback: () => { if (!filter.config.hotkeyDisabled) { hotkeyInputRef.current?.focus(); } }, - { enabled: !filter.config.hotkeyDisabled, preventDefault: true }, - ); + options: { enabled: !filter.config.hotkeyDisabled, preventDefault: true }, + }); const isAdvanced = showAdvancedToggle && advanced.enabled; const stringValue = hasValue && typeof filter.value === "string" ? filter.value : ""; diff --git a/airflow-core/src/airflow/ui/src/components/Graph/TaskNode.tsx b/airflow-core/src/airflow/ui/src/components/Graph/TaskNode.tsx index 5d8affeca3059..0efe7dbbb81a3 100644 --- a/airflow-core/src/airflow/ui/src/components/Graph/TaskNode.tsx +++ b/airflow-core/src/airflow/ui/src/components/Graph/TaskNode.tsx @@ -19,6 +19,7 @@ import { Box, Button, Flex, HStack, LinkOverlay, Text } from "@chakra-ui/react"; import type { NodeProps, Node as NodeType } from "@xyflow/react"; import { useTranslation } from "react-i18next"; +import { AiOutlineGroup } from "react-icons/ai"; import { TaskIcon } from "src/assets/TaskIcon"; import { StateBadge } from "src/components/StateBadge"; @@ -112,7 +113,7 @@ export const TaskNode = ({ width={`${width + (isSelected ? 4 : 0)}px`} > - + {isGroup ? : } { const { t: translate } = useTranslation(["dag", "tasks"]); @@ -140,7 +141,11 @@ export const GraphTaskFilters = () => { const [isOpen, setIsOpen] = useState(false); - useHotkeys("mod+shift+f", () => setIsOpen(true), { preventDefault: true }); + useShortcut({ + ...SHORTCUTS.filters.openGraphFilters, + callback: () => setIsOpen(true), + options: { preventDefault: true }, + }); const panelTitle = translate("dag:panel.graphFilters.title"); diff --git a/airflow-core/src/airflow/ui/src/components/KeyboardShortcuts/KeyboardShortcutsModal.tsx b/airflow-core/src/airflow/ui/src/components/KeyboardShortcuts/KeyboardShortcutsModal.tsx new file mode 100644 index 0000000000000..6ff477c626c30 --- /dev/null +++ b/airflow-core/src/airflow/ui/src/components/KeyboardShortcuts/KeyboardShortcutsModal.tsx @@ -0,0 +1,106 @@ +/*! + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +import { Box, Flex, HStack, Heading, Kbd, Text, VStack } from "@chakra-ui/react"; +import { useState } from "react"; +import { useTranslation } from "react-i18next"; + +import { Dialog } from "src/components/ui"; +import { + SHORTCUT_CATEGORIES, + SHORTCUTS, + type ShortcutEntry, + useShortcutRegistry, +} from "src/context/keyboardShortcuts"; +import { useShortcut } from "src/hooks/useShortcut"; +import { getMetaKey } from "src/utils"; + +import { formatShortcutCombo } from "./formatShortcutCombo"; + +const buildGroups = (shortcuts: ReadonlyArray) => + SHORTCUT_CATEGORIES.map((category) => { + const seen = new Set(); + const items = shortcuts + .filter((entry) => entry.category === category) + .filter((entry) => { + const key = `${entry.keys.join("+")}|${entry.description}`; + + if (seen.has(key)) { + return false; + } + seen.add(key); + + return true; + }); + + return { category, items }; + }).filter((group) => group.items.length > 0); + +export const KeyboardShortcutsModal = () => { + const { t: translate } = useTranslation("common"); + const { shortcuts } = useShortcutRegistry(); + const [open, setOpen] = useState(false); + const metaKey = getMetaKey(); + + const toggle = () => setOpen((prev) => !prev); + + useShortcut({ + ...SHORTCUTS.global.showHelp, + callback: toggle, + }); + + const groups = buildGroups(shortcuts); + + return ( + setOpen(event.open)} open={open} size="md"> + + {translate("shortcuts.title")} + + + {groups.length === 0 ? ( + {translate("shortcuts.empty")} + ) : ( + + {groups.map(({ category, items }) => ( + + + {translate(`shortcuts.categories.${category}`)} + + + {items.map((entry) => ( + + {entry.description} + + {entry.keys.map((combo) => ( + + {formatShortcutCombo(combo, metaKey)} + + ))} + + + ))} + + + ))} + + )} + + + + ); +}; diff --git a/airflow-core/src/airflow/ui/src/components/KeyboardShortcuts/formatShortcutCombo.test.ts b/airflow-core/src/airflow/ui/src/components/KeyboardShortcuts/formatShortcutCombo.test.ts new file mode 100644 index 0000000000000..e128fc376f6a3 --- /dev/null +++ b/airflow-core/src/airflow/ui/src/components/KeyboardShortcuts/formatShortcutCombo.test.ts @@ -0,0 +1,41 @@ +/*! + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +import { describe, expect, it } from "vitest"; + +import { formatShortcutCombo } from "./formatShortcutCombo"; + +describe("formatShortcutCombo", () => { + it.each([ + ["w", "⌘", "W"], + ["mod+k", "⌘", "⌘ K"], + ["mod+k", "Ctrl", "Ctrl K"], + ["mod+shift+f", "⌘", "⌘ ⇧ F"], + ["mod+ArrowUp", "Ctrl", "Ctrl ↑"], + ["shift+ArrowDown", "⌘", "⇧ ↓"], + ["space", "⌘", "Space"], + ["/", "⌘", "/"], + ])("formats %s (meta=%s) as %s", (combo, metaKey, expected) => { + expect(formatShortcutCombo(combo, metaKey)).toBe(expected); + }); + + it("renders the ? alias for the help shortcut regardless of casing", () => { + expect(formatShortcutCombo("shift+Slash", "⌘")).toBe("?"); + expect(formatShortcutCombo("shift+slash", "Ctrl")).toBe("?"); + }); +}); diff --git a/airflow-core/src/airflow/ui/src/components/KeyboardShortcuts/formatShortcutCombo.ts b/airflow-core/src/airflow/ui/src/components/KeyboardShortcuts/formatShortcutCombo.ts new file mode 100644 index 0000000000000..4819a84f5a1ae --- /dev/null +++ b/airflow-core/src/airflow/ui/src/components/KeyboardShortcuts/formatShortcutCombo.ts @@ -0,0 +1,66 @@ +/*! + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +// Symbols for keys whose name is not the desired display label. +const KEY_SYMBOLS: Record = { + alt: "⌥", + arrowdown: "↓", + arrowleft: "←", + arrowright: "→", + arrowup: "↑", + ctrl: "Ctrl", + shift: "⇧", + slash: "/", + space: "Space", +}; + +// Combos bound to one key for matching reasons but better shown as a single glyph. +// `shift+Slash` is how react-hotkeys-hook reliably matches the "?" key. +const COMBO_ALIASES: Record = { + "shift+slash": "?", +}; + +/** + * Turn a `react-hotkeys-hook` combo such as `"mod+shift+f"` into a human-readable + * label like `"⌘ ⇧ F"`. `metaKey` is the platform's meta key symbol (`⌘`/`Ctrl`). + */ +export const formatShortcutCombo = (combo: string, metaKey: string): string => { + const alias = COMBO_ALIASES[combo.toLowerCase()]; + + if (alias !== undefined) { + return alias; + } + + return combo + .split("+") + .map((part) => { + const lower = part.toLowerCase(); + + if (lower === "mod") { + return metaKey; + } + + if (lower in KEY_SYMBOLS) { + return KEY_SYMBOLS[lower]; + } + + return part.length === 1 ? part.toUpperCase() : part; + }) + .join(" "); +}; diff --git a/airflow-core/src/airflow/ui/src/components/KeyboardShortcuts/index.ts b/airflow-core/src/airflow/ui/src/components/KeyboardShortcuts/index.ts new file mode 100644 index 0000000000000..f98aa5f41b7ca --- /dev/null +++ b/airflow-core/src/airflow/ui/src/components/KeyboardShortcuts/index.ts @@ -0,0 +1,20 @@ +/*! + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +export { KeyboardShortcutsModal } from "./KeyboardShortcutsModal"; +export { formatShortcutCombo } from "./formatShortcutCombo"; diff --git a/airflow-core/src/airflow/ui/src/components/MarkAs/Run/MarkRunAsButton.tsx b/airflow-core/src/airflow/ui/src/components/MarkAs/Run/MarkRunAsButton.tsx index e933ec8883871..b89946aa35255 100644 --- a/airflow-core/src/airflow/ui/src/components/MarkAs/Run/MarkRunAsButton.tsx +++ b/airflow-core/src/airflow/ui/src/components/MarkAs/Run/MarkRunAsButton.tsx @@ -18,7 +18,6 @@ */ import { Box, HStack, useDisclosure } from "@chakra-ui/react"; import { useState } from "react"; -import { useHotkeys } from "react-hotkeys-hook"; import { useTranslation } from "react-i18next"; import { FiX } from "react-icons/fi"; import { LuCheck } from "react-icons/lu"; @@ -26,6 +25,8 @@ import { LuCheck } from "react-icons/lu"; import type { DagRunMutableStates, DAGRunResponse } from "openapi/requests/types.gen"; import { StateBadge } from "src/components/StateBadge"; import { IconButton, Menu, Tooltip } from "src/components/ui"; +import { SHORTCUTS } from "src/context/keyboardShortcuts"; +import { useShortcut } from "src/hooks/useShortcut"; import { allowedStates } from "../utils"; import MarkRunAsDialog from "./MarkRunAsDialog"; @@ -40,23 +41,23 @@ const MarkRunAsButton = ({ dagRun, isHotkeyEnabled = false }: Props) => { const [state, setState] = useState("success"); const { t: translate } = useTranslation(); - useHotkeys( - "shift+f", - () => { + useShortcut({ + ...SHORTCUTS.runActions.markRunFailed, + callback: () => { setState("failed"); onOpen(); }, - { enabled: isHotkeyEnabled }, - ); + options: { enabled: isHotkeyEnabled }, + }); - useHotkeys( - "shift+s", - () => { + useShortcut({ + ...SHORTCUTS.runActions.markRunSuccess, + callback: () => { setState("success"); onOpen(); }, - { enabled: isHotkeyEnabled }, - ); + options: { enabled: isHotkeyEnabled }, + }); const label = translate("dags:runAndTaskActions.markAs.button", { type: translate("dagRun_one"), diff --git a/airflow-core/src/airflow/ui/src/components/MarkAs/TaskGroup/MarkTaskGroupAsButton.tsx b/airflow-core/src/airflow/ui/src/components/MarkAs/TaskGroup/MarkTaskGroupAsButton.tsx index fa5cd2322ae5d..9cc44162fb061 100644 --- a/airflow-core/src/airflow/ui/src/components/MarkAs/TaskGroup/MarkTaskGroupAsButton.tsx +++ b/airflow-core/src/airflow/ui/src/components/MarkAs/TaskGroup/MarkTaskGroupAsButton.tsx @@ -18,7 +18,6 @@ */ import { Box, HStack, useDisclosure } from "@chakra-ui/react"; import { useState } from "react"; -import { useHotkeys } from "react-hotkeys-hook"; import { useTranslation } from "react-i18next"; import { FiX } from "react-icons/fi"; import { LuCheck } from "react-icons/lu"; @@ -26,6 +25,8 @@ import { LuCheck } from "react-icons/lu"; import type { LightGridTaskInstanceSummary, TaskInstanceState } from "openapi/requests/types.gen"; import { StateBadge } from "src/components/StateBadge"; import { IconButton, Menu, Tooltip } from "src/components/ui"; +import { SHORTCUTS } from "src/context/keyboardShortcuts"; +import { useShortcut } from "src/hooks/useShortcut"; import { allowedStates } from "../utils"; import MarkTaskGroupAsDialog from "./MarkTaskGroupAsDialog"; @@ -40,23 +41,23 @@ const MarkTaskGroupAsButton = ({ groupTaskInstance, isHotkeyEnabled = false }: P const { t: translate } = useTranslation(); const [state, setState] = useState("success"); - useHotkeys( - "shift+f", - () => { + useShortcut({ + ...SHORTCUTS.runActions.markTaskGroupFailed, + callback: () => { setState("failed"); onOpen(); }, - { enabled: isHotkeyEnabled }, - ); + options: { enabled: isHotkeyEnabled }, + }); - useHotkeys( - "shift+s", - () => { + useShortcut({ + ...SHORTCUTS.runActions.markTaskGroupSuccess, + callback: () => { setState("success"); onOpen(); }, - { enabled: isHotkeyEnabled }, - ); + options: { enabled: isHotkeyEnabled }, + }); const label = translate("dags:runAndTaskActions.markAs.button", { type: translate("taskGroup_one"), diff --git a/airflow-core/src/airflow/ui/src/components/MarkAs/TaskInstance/MarkTaskInstanceAsButton.tsx b/airflow-core/src/airflow/ui/src/components/MarkAs/TaskInstance/MarkTaskInstanceAsButton.tsx index 20680890aa72d..5c235474ac832 100644 --- a/airflow-core/src/airflow/ui/src/components/MarkAs/TaskInstance/MarkTaskInstanceAsButton.tsx +++ b/airflow-core/src/airflow/ui/src/components/MarkAs/TaskInstance/MarkTaskInstanceAsButton.tsx @@ -18,7 +18,6 @@ */ import { Box, HStack, useDisclosure } from "@chakra-ui/react"; import { useState } from "react"; -import { useHotkeys } from "react-hotkeys-hook"; import { useTranslation } from "react-i18next"; import { FiX } from "react-icons/fi"; import { LuCheck } from "react-icons/lu"; @@ -26,6 +25,8 @@ import { LuCheck } from "react-icons/lu"; import type { TaskInstanceResponse, TaskInstanceState } from "openapi/requests/types.gen"; import { StateBadge } from "src/components/StateBadge"; import { IconButton, Menu, Tooltip } from "src/components/ui"; +import { SHORTCUTS } from "src/context/keyboardShortcuts"; +import { useShortcut } from "src/hooks/useShortcut"; import { allowedStates } from "../utils"; import MarkTaskInstanceAsDialog from "./MarkTaskInstanceAsDialog"; @@ -41,23 +42,23 @@ const MarkTaskInstanceAsButton = ({ isHotkeyEnabled = false, taskInstance }: Pro const [state, setState] = useState("success"); - useHotkeys( - "shift+f", - () => { + useShortcut({ + ...SHORTCUTS.runActions.markTaskFailed, + callback: () => { setState("failed"); onOpen(); }, - { enabled: isHotkeyEnabled }, - ); + options: { enabled: isHotkeyEnabled }, + }); - useHotkeys( - "shift+s", - () => { + useShortcut({ + ...SHORTCUTS.runActions.markTaskSuccess, + callback: () => { setState("success"); onOpen(); }, - { enabled: isHotkeyEnabled }, - ); + options: { enabled: isHotkeyEnabled }, + }); const label = translate("dags:runAndTaskActions.markAs.button", { type: translate("taskInstance_one"), diff --git a/airflow-core/src/airflow/ui/src/components/SearchBar.tsx b/airflow-core/src/airflow/ui/src/components/SearchBar.tsx index cf51c59981ad9..57981808c6250 100644 --- a/airflow-core/src/airflow/ui/src/components/SearchBar.tsx +++ b/airflow-core/src/airflow/ui/src/components/SearchBar.tsx @@ -18,12 +18,13 @@ */ import { CloseButton, HStack, Input, InputGroup, Kbd, type InputGroupProps } from "@chakra-ui/react"; import { useEffect, useRef, useState, type ChangeEvent } from "react"; -import { useHotkeys } from "react-hotkeys-hook"; import { useTranslation } from "react-i18next"; import { FiSearch } from "react-icons/fi"; import { useDebouncedCallback } from "use-debounce"; import { AdvancedSearchToggle, type AdvancedSearchToggleProps } from "src/components/AdvancedSearchToggle"; +import { SHORTCUTS } from "src/context/keyboardShortcuts"; +import { useShortcut } from "src/hooks/useShortcut"; import { getMetaKey } from "src/utils"; const debounceDelay = 200; @@ -74,13 +75,13 @@ export const SearchBar = ({ onChange(""); }; - useHotkeys( - "mod+k", - () => { + useShortcut({ + ...SHORTCUTS.search.focusSearch, + callback: () => { searchRef.current?.focus(); }, - { enabled: !hotkeyDisabled, preventDefault: true }, - ); + options: { enabled: !hotkeyDisabled, preventDefault: true }, + }); const inputGroup = ( { setIsOpen(false); }; - useHotkeys( - "mod+k", - () => { + useShortcut({ + ...SHORTCUTS.search.searchDags, + callback: () => { setIsOpen(true); }, - [isOpen], - { preventDefault: true }, - ); + dependencies: [isOpen], + options: { preventDefault: true }, + }); return ( diff --git a/airflow-core/src/airflow/ui/src/components/TaskName.tsx b/airflow-core/src/airflow/ui/src/components/TaskName.tsx index 9349cb27a5156..1f35b5bfc2e43 100644 --- a/airflow-core/src/airflow/ui/src/components/TaskName.tsx +++ b/airflow-core/src/airflow/ui/src/components/TaskName.tsx @@ -55,7 +55,6 @@ export const TaskName = ({ fontWeight="bold" overflow="hidden" textOverflow="ellipsis" - title={label} whiteSpace="nowrap" {...rest} > @@ -71,7 +70,6 @@ export const TaskName = ({ fontWeight="bold" overflow="hidden" textOverflow="ellipsis" - title={label} whiteSpace="nowrap" {...rest} > diff --git a/airflow-core/src/airflow/ui/src/context/keyboardShortcuts/Context.ts b/airflow-core/src/airflow/ui/src/context/keyboardShortcuts/Context.ts new file mode 100644 index 0000000000000..e4320ed9c0629 --- /dev/null +++ b/airflow-core/src/airflow/ui/src/context/keyboardShortcuts/Context.ts @@ -0,0 +1,58 @@ +/*! + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +import { createContext } from "react"; + +// Display order of shortcut categories in the help modal. +export const SHORTCUT_CATEGORIES = [ + "global", + "navigation", + "dagView", + "search", + "filters", + "logs", + "code", + "runActions", +] as const; + +export type ShortcutCategory = (typeof SHORTCUT_CATEGORIES)[number]; + +export type ShortcutEntry = { + readonly category: ShortcutCategory; + readonly description: string; + readonly id: string; + readonly keys: ReadonlyArray; +}; + +export type ShortcutRegistryContextValue = { + readonly register: (entry: ShortcutEntry) => void; + readonly shortcuts: ReadonlyArray; + readonly unregister: (id: string) => void; +}; + +// No-op default so components using shortcuts can render without the provider +// (e.g. in isolated unit tests). The hotkeys still work; they just aren't +// listed in the help dialog. The real app wraps the tree in +// ShortcutRegistryProvider, so the dialog is populated there. +const NOOP_REGISTRY: ShortcutRegistryContextValue = { + register: () => undefined, + shortcuts: [], + unregister: () => undefined, +}; + +export const ShortcutRegistryContext = createContext(NOOP_REGISTRY); diff --git a/airflow-core/src/airflow/ui/src/context/keyboardShortcuts/ShortcutRegistryProvider.tsx b/airflow-core/src/airflow/ui/src/context/keyboardShortcuts/ShortcutRegistryProvider.tsx new file mode 100644 index 0000000000000..645a5a5da1965 --- /dev/null +++ b/airflow-core/src/airflow/ui/src/context/keyboardShortcuts/ShortcutRegistryProvider.tsx @@ -0,0 +1,45 @@ +/*! + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +import { useState, type PropsWithChildren } from "react"; + +import { ShortcutRegistryContext, type ShortcutEntry } from "./Context"; + +/** + * Holds the set of keyboard shortcuts that are currently mounted and enabled. + * + * Each `useShortcut` call registers itself here on mount and removes itself on + * unmount, mirroring the lifecycle of the underlying `react-hotkeys-hook` + * binding. The help modal renders from this registry, so it always reflects the + * shortcuts actually available on the current page. + */ +export const ShortcutRegistryProvider = ({ children }: PropsWithChildren) => { + const [shortcuts, setShortcuts] = useState>([]); + + const register = (entry: ShortcutEntry) => { + setShortcuts((prev) => [...prev.filter((item) => item.id !== entry.id), entry]); + }; + + const unregister = (id: string) => { + setShortcuts((prev) => prev.filter((item) => item.id !== id)); + }; + + const value = { register, shortcuts, unregister }; + + return {children}; +}; diff --git a/airflow-core/src/airflow/ui/src/context/keyboardShortcuts/index.ts b/airflow-core/src/airflow/ui/src/context/keyboardShortcuts/index.ts new file mode 100644 index 0000000000000..b2634450013b7 --- /dev/null +++ b/airflow-core/src/airflow/ui/src/context/keyboardShortcuts/index.ts @@ -0,0 +1,22 @@ +/*! + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +export { SHORTCUT_CATEGORIES, type ShortcutCategory, type ShortcutEntry } from "./Context"; +export { SHORTCUTS, type ShortcutDefinition } from "./shortcuts"; +export { ShortcutRegistryProvider } from "./ShortcutRegistryProvider"; +export { useShortcutRegistry } from "./useShortcutRegistry"; diff --git a/airflow-core/src/airflow/ui/src/context/keyboardShortcuts/shortcuts.ts b/airflow-core/src/airflow/ui/src/context/keyboardShortcuts/shortcuts.ts new file mode 100644 index 0000000000000..1271037a99f38 --- /dev/null +++ b/airflow-core/src/airflow/ui/src/context/keyboardShortcuts/shortcuts.ts @@ -0,0 +1,179 @@ +/*! + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +import type { Keys } from "react-hotkeys-hook"; + +import type { ShortcutCategory } from "./Context"; + +/** + * Static definition of a keyboard shortcut: its key combination, the category it + * belongs to in the help modal, and the `common` i18n key for its description. + * + * The runtime callback and any dynamic options (e.g. `enabled`) stay at the call + * site, since they depend on component state. + */ +export type ShortcutDefinition = { + readonly category: ShortcutCategory; + readonly descriptionKey: string; + readonly keys: Keys; +}; + +// Every category must be present, and each entry's `category` field must match +// the group it sits in — both are enforced at compile time. +type CategorizedShortcuts = { + readonly [Category in ShortcutCategory]: Record< + string, + { readonly category: Category } & ShortcutDefinition + >; +}; + +/** + * Single source of truth for every keyboard shortcut in the UI, grouped by the + * category shown in the help modal. + * + * Each `useShortcut` call spreads one of these entries (e.g. + * `...SHORTCUTS.logs.toggleWrap`) instead of redeclaring its keys, category and + * description inline, so maintainers can see every shortcut that already exists + * in one place and avoid clashing key bindings. Description text lives in + * `common.json` under the referenced `descriptionKey`. + */ +export const SHORTCUTS = { + code: { + toggleFullscreen: { + category: "code", + descriptionKey: "shortcuts.descriptions.toggleFullscreen", + keys: "f", + }, + toggleWrap: { category: "code", descriptionKey: "shortcuts.descriptions.toggleWrap", keys: "w" }, + }, + dagView: { + toggleGraphGrid: { + category: "dagView", + descriptionKey: "shortcuts.descriptions.toggleGraphGrid", + keys: "g", + }, + }, + filters: { + openGraphFilters: { + category: "filters", + descriptionKey: "shortcuts.descriptions.openGraphFilters", + keys: "mod+shift+f", + }, + }, + global: { + showHelp: { + category: "global", + descriptionKey: "shortcuts.descriptions.showHelp", + keys: "shift+Slash", + }, + }, + logs: { + downloadLogs: { category: "logs", descriptionKey: "shortcuts.descriptions.downloadLogs", keys: "d" }, + focusLogSearch: { + category: "logs", + descriptionKey: "shortcuts.descriptions.focusLogSearch", + keys: "/", + }, + scrollBottom: { + category: "logs", + descriptionKey: "shortcuts.descriptions.scrollBottom", + keys: "mod+ArrowDown", + }, + scrollTop: { category: "logs", descriptionKey: "shortcuts.descriptions.scrollTop", keys: "mod+ArrowUp" }, + toggleExpand: { category: "logs", descriptionKey: "shortcuts.descriptions.toggleExpand", keys: "e" }, + toggleFullscreen: { + category: "logs", + descriptionKey: "shortcuts.descriptions.toggleFullscreen", + keys: "f", + }, + toggleSource: { category: "logs", descriptionKey: "shortcuts.descriptions.toggleSource", keys: "s" }, + toggleTimestamp: { + category: "logs", + descriptionKey: "shortcuts.descriptions.toggleTimestamp", + keys: "t", + }, + toggleWrap: { category: "logs", descriptionKey: "shortcuts.descriptions.toggleWrap", keys: "w" }, + }, + navigation: { + navigateTasks: { + category: "navigation", + descriptionKey: "shortcuts.descriptions.navigateTasks", + keys: ["shift+ArrowDown", "shift+ArrowUp", "shift+ArrowLeft", "shift+ArrowRight"], + }, + toggleTaskGroup: { + category: "navigation", + descriptionKey: "shortcuts.descriptions.toggleTaskGroup", + keys: "space", + }, + }, + runActions: { + clearRun: { + category: "runActions", + descriptionKey: "shortcuts.descriptions.clearRun", + keys: "shift+c", + }, + clearTaskInstance: { + category: "runActions", + descriptionKey: "shortcuts.descriptions.clearTaskInstance", + keys: "shift+c", + }, + markRunFailed: { + category: "runActions", + descriptionKey: "shortcuts.descriptions.markRunFailed", + keys: "shift+f", + }, + markRunSuccess: { + category: "runActions", + descriptionKey: "shortcuts.descriptions.markRunSuccess", + keys: "shift+s", + }, + markTaskFailed: { + category: "runActions", + descriptionKey: "shortcuts.descriptions.markTaskFailed", + keys: "shift+f", + }, + markTaskGroupFailed: { + category: "runActions", + descriptionKey: "shortcuts.descriptions.markTaskGroupFailed", + keys: "shift+f", + }, + markTaskGroupSuccess: { + category: "runActions", + descriptionKey: "shortcuts.descriptions.markTaskGroupSuccess", + keys: "shift+s", + }, + markTaskSuccess: { + category: "runActions", + descriptionKey: "shortcuts.descriptions.markTaskSuccess", + keys: "shift+s", + }, + }, + search: { + focusFilterSearch: { + category: "search", + descriptionKey: "shortcuts.descriptions.focusFilterSearch", + keys: "mod+k", + }, + focusSearch: { + category: "search", + descriptionKey: "shortcuts.descriptions.focusSearch", + keys: "mod+k", + }, + searchDags: { category: "search", descriptionKey: "shortcuts.descriptions.searchDags", keys: "mod+k" }, + }, +} satisfies CategorizedShortcuts; diff --git a/airflow-core/src/airflow/ui/src/context/keyboardShortcuts/useShortcutRegistry.ts b/airflow-core/src/airflow/ui/src/context/keyboardShortcuts/useShortcutRegistry.ts new file mode 100644 index 0000000000000..ff35397696ad8 --- /dev/null +++ b/airflow-core/src/airflow/ui/src/context/keyboardShortcuts/useShortcutRegistry.ts @@ -0,0 +1,23 @@ +/*! + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +import { useContext } from "react"; + +import { ShortcutRegistryContext, type ShortcutRegistryContextValue } from "./Context"; + +export const useShortcutRegistry = (): ShortcutRegistryContextValue => useContext(ShortcutRegistryContext); diff --git a/airflow-core/src/airflow/ui/src/hooks/navigation/useKeyboardNavigation.ts b/airflow-core/src/airflow/ui/src/hooks/navigation/useKeyboardNavigation.ts index 5a99d9cc8f87f..cfeb0976c3c36 100644 --- a/airflow-core/src/airflow/ui/src/hooks/navigation/useKeyboardNavigation.ts +++ b/airflow-core/src/airflow/ui/src/hooks/navigation/useKeyboardNavigation.ts @@ -16,12 +16,11 @@ * specific language governing permissions and limitations * under the License. */ -import { useHotkeys } from "react-hotkeys-hook"; +import { SHORTCUTS } from "src/context/keyboardShortcuts"; +import { useShortcut } from "src/hooks/useShortcut"; import type { ArrowKey, NavigationDirection } from "./types"; -const ARROW_KEYS = ["shift+ArrowDown", "shift+ArrowUp", "shift+ArrowLeft", "shift+ArrowRight"] as const; - type Props = { enabled?: boolean; onNavigate: (direction: NavigationDirection) => void; @@ -55,7 +54,17 @@ export const useKeyboardNavigation = ({ enabled = true, onNavigate, onToggleGrou const hotkeyOptions = { enabled, preventDefault: true }; - useHotkeys(ARROW_KEYS.join(","), handleNormalKeyPress, hotkeyOptions, [onNavigate]); + useShortcut({ + ...SHORTCUTS.navigation.navigateTasks, + callback: handleNormalKeyPress, + dependencies: [onNavigate], + options: hotkeyOptions, + }); - useHotkeys("space", () => onToggleGroup?.(), hotkeyOptions, [onToggleGroup]); + useShortcut({ + ...SHORTCUTS.navigation.toggleTaskGroup, + callback: () => onToggleGroup?.(), + dependencies: [onToggleGroup], + options: hotkeyOptions, + }); }; diff --git a/airflow-core/src/airflow/ui/src/hooks/useShortcut.test.tsx b/airflow-core/src/airflow/ui/src/hooks/useShortcut.test.tsx new file mode 100644 index 0000000000000..45e1f5bf6a80d --- /dev/null +++ b/airflow-core/src/airflow/ui/src/hooks/useShortcut.test.tsx @@ -0,0 +1,111 @@ +/*! + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +import { renderHook } from "@testing-library/react"; +import type { PropsWithChildren } from "react"; +import { describe, expect, it, vi } from "vitest"; + +import { + ShortcutRegistryProvider, + type ShortcutEntry, + useShortcutRegistry, +} from "src/context/keyboardShortcuts"; + +import { useShortcut } from "./useShortcut"; + +const mockTranslate = vi.fn((key: string) => { + const translations: Record = { + "shortcuts.descriptions.toggleWrap": "Toggle Wrap", + }; + + return translations[key] ?? key; +}); + +vi.mock("react-i18next", () => ({ + useTranslation: () => ({ + i18n: { language: "en" }, + // eslint-disable-next-line id-length + t: mockTranslate, + }), +})); + +const wrapper = ({ children }: PropsWithChildren) => ( + {children} +); + +const renderShortcut = (initialEnabled = true) => + renderHook( + ({ enabled }: { enabled: boolean }): ReadonlyArray => { + useShortcut({ + callback: vi.fn(), + category: "logs", + descriptionKey: "shortcuts.descriptions.toggleWrap", + keys: "w", + options: { enabled }, + }); + + return useShortcutRegistry().shortcuts; + }, + { initialProps: { enabled: initialEnabled }, wrapper }, + ); + +describe("useShortcut", () => { + it("registers the shortcut while mounted", () => { + const { result } = renderShortcut(); + + expect(result.current).toHaveLength(1); + expect(result.current[0]).toMatchObject({ + category: "logs", + description: "Toggle Wrap", + keys: ["w"], + }); + }); + + it("removes the shortcut when it becomes disabled", () => { + const { rerender, result } = renderShortcut(true); + + expect(result.current).toHaveLength(1); + rerender({ enabled: false }); + expect(result.current).toHaveLength(0); + }); + + it("does not register a disabled shortcut", () => { + const { result } = renderShortcut(false); + + expect(result.current).toHaveLength(0); + }); + + it("registers each combo of a multi-key shortcut for display", () => { + const { result } = renderHook( + (): ReadonlyArray => { + useShortcut({ + callback: vi.fn(), + category: "navigation", + descriptionKey: "shortcuts.descriptions.navigateTasks", + keys: ["shift+ArrowUp", "shift+ArrowDown"], + }); + + return useShortcutRegistry().shortcuts; + }, + { wrapper }, + ); + + expect(result.current).toHaveLength(1); + expect(result.current[0]?.keys).toEqual(["shift+ArrowUp", "shift+ArrowDown"]); + }); +}); diff --git a/airflow-core/src/airflow/ui/src/hooks/useShortcut.ts b/airflow-core/src/airflow/ui/src/hooks/useShortcut.ts new file mode 100644 index 0000000000000..548389657d5db --- /dev/null +++ b/airflow-core/src/airflow/ui/src/hooks/useShortcut.ts @@ -0,0 +1,76 @@ +/*! + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +import { type DependencyList, useEffect, useId } from "react"; +import { type HotkeyCallback, type Options, useHotkeys } from "react-hotkeys-hook"; +import { useTranslation } from "react-i18next"; + +import { type ShortcutDefinition, useShortcutRegistry } from "src/context/keyboardShortcuts"; + +type UseShortcutParams = { + readonly callback: HotkeyCallback; + readonly dependencies?: DependencyList; + readonly options?: Options; +} & ShortcutDefinition; + +/** + * Thin wrapper around `react-hotkeys-hook`'s `useHotkeys` that also publishes the + * shortcut to the keyboard-shortcut registry so it shows up in the help modal. + * + * The static `category`, `descriptionKey` and `keys` come from a `SHORTCUTS` + * definition (spread in at the call site); `callback`, `options` and + * `dependencies` stay at the call site since they depend on component state. The + * shortcut is registered only while it is enabled, matching when the hotkey is + * actually bound. + */ +export const useShortcut = ({ + callback, + category, + dependencies, + descriptionKey, + keys, + options, +}: UseShortcutParams) => { + const id = useId(); + const { t: translate } = useTranslation("common"); + const { register, unregister } = useShortcutRegistry(); + + const ref = useHotkeys(keys, callback, options, dependencies); + + const description = translate(descriptionKey); + + const keyList: Array = typeof keys === "string" ? [keys] : [...keys]; + // A `false` literal means the hotkey is not bound; a function trigger is evaluated + // per event but the binding still exists, so we treat it as enabled here. + const isEnabled = options?.enabled !== false; + const keyListId = keyList.join(","); + + useEffect(() => { + if (!isEnabled) { + return undefined; + } + + register({ category, description, id, keys: keyList }); + + return () => unregister(id); + // `keyListId` stands in for `keyList`'s identity to avoid re-running on each render. + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [category, description, id, isEnabled, keyListId, register, unregister]); + + return ref; +}; diff --git a/airflow-core/src/airflow/ui/src/layouts/BaseLayout.tsx b/airflow-core/src/airflow/ui/src/layouts/BaseLayout.tsx index e06c92bc0e50d..5664e702a33ef 100644 --- a/airflow-core/src/airflow/ui/src/layouts/BaseLayout.tsx +++ b/airflow-core/src/airflow/ui/src/layouts/BaseLayout.tsx @@ -23,6 +23,7 @@ import { Outlet } from "react-router-dom"; import { usePluginServiceGetPlugins } from "openapi/queries"; import type { ReactAppResponse } from "openapi/requests/types.gen"; +import { KeyboardShortcutsModal } from "src/components/KeyboardShortcuts"; import { ReactPlugin } from "src/pages/ReactPlugin"; import { useConfig } from "src/queries/useConfig"; import { DocumentTitleProvider } from "src/utils"; @@ -101,6 +102,7 @@ export const BaseLayout = ({ children }: PropsWithChildren) => { +