From 54af4ab33688b66a0c42b61dd9dc45c4cb2175e5 Mon Sep 17 00:00:00 2001 From: skulpok-akamai Date: Mon, 15 Dec 2025 15:40:34 +0100 Subject: [PATCH 1/2] Added functionality to clean-up old Object Storage keys created by linode-cli (#824) Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> Co-authored-by: Lena Garber --- .pylintrc | 4 - linodecli/configuration/config.py | 69 +++++-- linodecli/plugins/obj/__init__.py | 244 ++++++++++++++++++++++-- pyproject.toml | 3 +- tests/unit/test_plugin_obj.py | 304 +++++++++++++++++++++++++++++- 5 files changed, 593 insertions(+), 31 deletions(-) diff --git a/.pylintrc b/.pylintrc index 03ddde567..1329f16e7 100644 --- a/.pylintrc +++ b/.pylintrc @@ -89,10 +89,6 @@ py-version=3.9 # Discover python modules and packages in the file system subtree. recursive=no -# When enabled, pylint would attempt to guess common misconfiguration and emit -# user-friendly hints instead of false-positive error messages. -suggestion-mode=yes - # Allow loading of arbitrary C extensions. Extensions are imported into the # active Python interpreter and may run arbitrary code. unsafe-load-any-extension=no diff --git a/linodecli/configuration/config.py b/linodecli/configuration/config.py index 700e82910..9dc85d9dc 100644 --- a/linodecli/configuration/config.py +++ b/linodecli/configuration/config.py @@ -5,7 +5,7 @@ import argparse import os import sys -from typing import Any, Dict, List, Optional +from typing import Any, Dict, List, Optional, Type, TypeVar, cast from linodecli.exit_codes import ExitCodes @@ -27,6 +27,8 @@ ENV_TOKEN_NAME = "LINODE_CLI_TOKEN" +T = TypeVar("T") + class CLIConfig: """ @@ -216,15 +218,12 @@ def plugin_set_value(self, key: str, value: Any): :param value: The value to set for this key :type value: any """ - if self.running_plugin is None: - raise RuntimeError( - "No running plugin to retrieve configuration for!" - ) - username = self.username or self.default_username() - self.config.set(username, f"plugin-{self.running_plugin}-{key}", value) + self.config.set(username, self._get_plugin_key(key), value) - def plugin_get_value(self, key: str) -> Optional[Any]: + def plugin_get_value( + self, key: str, default: Optional[T] = None, value_type: Type[T] = str + ) -> Optional[T]: """ Retrieves and returns a config value previously set for a plugin. Your plugin should have set this value in the past. If this value does not @@ -235,18 +234,54 @@ def plugin_get_value(self, key: str) -> Optional[Any]: :param key: The key of the value to return :type key: str + :param default: The default value to return if the key is not set + :type default: T + + :param value_type: The type to which the value should be cast + :type value_type: Type[T] + :returns: The value for this plugin for this key, or None if not set :rtype: any """ + username = self.username or self.default_username() or "DEFAULT" + value = self.config.get( + username, self._get_plugin_key(key), fallback=None + ) + if value is None: + return default + + if value_type == str: + return value + + if value_type == bool: + bool_value = self.parse_boolean(value) + return bool_value if bool_value is not None else default + + try: + return cast(T, value_type(value)) + except (ValueError, TypeError): + print( + f"Could not cast config value {value} to {value_type}.", + file=sys.stderr, + ) + return default + + def plugin_remove_option(self, key: str): + """ + Removes a plugin configuration option. + + :param key: The key of the option to remove + """ + username = self.username or self.default_username() + self.config.remove_option(username, self._get_plugin_key(key)) + + def _get_plugin_key(self, key: str) -> str: if self.running_plugin is None: raise RuntimeError( "No running plugin to retrieve configuration for!" ) - username = self.username or self.default_username() or "DEFAULT" - full_key = f"plugin-{self.running_plugin}-{key}" - - return self.config.get(username, full_key, fallback=None) + return f"plugin-{self.running_plugin}-{key}" # TODO: this is more of an argparsing function than it is a config function # might be better to move this to argparsing during refactor and just have @@ -654,3 +689,13 @@ def get_custom_aliases(self) -> Dict[str, str]: if (self.config.has_section("custom_aliases")) else {} ) + + def parse_boolean(self, value: str) -> Optional[bool]: + """ + Parses a string config value into a boolean. Returns None if the value + cannot be parsed as a boolean. + + :param value: The string value to parse. + :return: The parsed boolean value. + """ + return self.config.BOOLEAN_STATES.get(value.lower(), None) diff --git a/linodecli/plugins/obj/__init__.py b/linodecli/plugins/obj/__init__.py index 45c552db1..2d5761340 100644 --- a/linodecli/plugins/obj/__init__.py +++ b/linodecli/plugins/obj/__init__.py @@ -4,6 +4,7 @@ """ import getpass import os +import re import socket import sys import time @@ -11,13 +12,13 @@ from contextlib import suppress from datetime import datetime from math import ceil -from typing import List +from typing import List, Optional +from pytimeparse import parse as parse_time from rich import print as rprint from rich.table import Table from linodecli.cli import CLI -from linodecli.configuration import _do_get_request from linodecli.configuration.helpers import _default_text_input from linodecli.exit_codes import ExitCodes from linodecli.help_formatter import SortingHelpFormatter @@ -65,6 +66,17 @@ HAS_BOTO = False +CLUSTER_KEY = "cluster" +KEY_CLEANUP_ENABLED_KEY = "key-cleanup-enabled" +KEY_LIFESPAN_KEY = "key-lifespan" +KEY_ROTATION_PERIOD_KEY = "key-rotation-period" +KEY_CLEANUP_BATCH_SIZE_KEY = "key-cleanup-batch-size" +LAST_KEY_CLEANUP_TIMESTAMP_KEY = "last-key-cleanup-timestamp" +ACCESS_KEY_KEY = "access-key" +SECRET_KEY_KEY = "secret-key" +TOKEN_KEY = "token" + + def generate_url(get_client, args, **kwargs): # pylint: disable=unused-argument """ Generates a URL to an object @@ -314,13 +326,44 @@ def get_obj_args_parser(): metavar="COMMAND", nargs="?", type=str, - help="The command to execute in object storage", + help="The command to execute in object storage.", ) parser.add_argument( "--cluster", metavar="CLUSTER", type=str, - help="The cluster to use for the operation", + help="The cluster to use for the operation.", + ) + parser.add_argument( + "--force-key-cleanup", + action="store_true", + help="Performs cleanup of old linode-cli generated Object Storage keys" + " before executing the Object Storage command. It overrides" + " the --perform-key-cleanup option.", + ) + parser.add_argument( + "--key-cleanup-enabled", + choices=["yes", "no"], + help="If set to 'yes', performs cleanup of old linode-cli generated Object Storage" + " keys before executing the Object Storage command. Cleanup occurs" + " at most once every 24 hours.", + ) + parser.add_argument( + "--key-lifespan", + type=str, + help="Specifies the lifespan of linode-cli generated Object Storage keys" + " (e.g. 30d for 30 days). Used only during key cleanup.", + ) + parser.add_argument( + "--key-rotation-period", + type=str, + help="Specifies the period after which the linode-cli generated Object Storage" + " key must be rotated (e.g. 10d for 10 days). Used only during key cleanup.", + ) + parser.add_argument( + "--key-cleanup-batch-size", + type=int, + help="Number of old linode-cli generated Object Storage keys to clean up at once.", ) return parser @@ -400,8 +443,9 @@ def call( access_key = None secret_key = None - # make a client, but only if we weren't printing help + # make a client and clean-up keys, but only if we weren't printing help if not is_help: + _cleanup_keys(context.client, parsed) access_key, secret_key = get_credentials(context.client) cluster = parsed.cluster @@ -497,8 +541,8 @@ def _get_s3_creds(client: CLI, force: bool = False): :returns: The access key and secret key for this user :rtype: tuple(str, str) """ - access_key = client.config.plugin_get_value("access-key") - secret_key = client.config.plugin_get_value("secret-key") + access_key = client.config.plugin_get_value(ACCESS_KEY_KEY) + secret_key = client.config.plugin_get_value(SECRET_KEY_KEY) if force or access_key is None: # this means there are no stored s3 creds for this user - set them up @@ -507,7 +551,7 @@ def _get_s3_creds(client: CLI, force: bool = False): # being provided by the environment, but if the CLI is running without a # config, we shouldn't generate new keys (or we'd end up doing so with each # request) - instead ask for them to be set up. - if client.config.get_value("token") is None: + if client.config.get_value(TOKEN_KEY) is None: print( "You are running the Linode CLI without a configuration file, but " "object storage keys were not configured. " @@ -571,8 +615,8 @@ def _get_s3_creds(client: CLI, force: bool = False): access_key = resp["access_key"] secret_key = resp["secret_key"] - client.config.plugin_set_value("access-key", access_key) - client.config.plugin_set_value("secret-key", secret_key) + client.config.plugin_set_value(ACCESS_KEY_KEY, access_key) + client.config.plugin_set_value(SECRET_KEY_KEY, secret_key) client.config.write_config() return access_key, secret_key @@ -580,14 +624,188 @@ def _get_s3_creds(client: CLI, force: bool = False): def _configure_plugin(client: CLI): """ - Configures a default cluster value. + Configures Object Storage plugin. """ - cluster = _default_text_input( # pylint: disable=protected-access "Default cluster for operations (e.g. `us-mia-1`)", optional=False, ) if cluster: - client.config.plugin_set_value("cluster", cluster) + client.config.plugin_set_value(CLUSTER_KEY, cluster) + client.config.write_config() + + +def _cleanup_keys(client: CLI, options) -> None: + """ + Cleans up stale linode-cli generated object storage keys. + """ + + try: + current_timestamp = int(time.time()) + if not _should_perform_key_cleanup(client, options, current_timestamp): + return + + cleanup_message = ( + "Cleaning up old linode-cli generated Object Storage keys." + ) + if not options.force_key_cleanup and not options.key_cleanup_enabled: + cleanup_message += ( + " To disable this, use the '--key-cleanup-enabled no' option." + ) + print(cleanup_message, file=sys.stderr) + + status, keys = client.call_operation("object-storage", "keys-list") + if status != 200: + print( + "Failed to list object storage keys for cleanup", + file=sys.stderr, + ) + return + + key_lifespan = _get_key_lifespan(client, options) + key_rotation_period = _get_key_rotation_period(client, options) + cleanup_batch_size = _get_cleanup_batch_size(client, options) + + linode_cli_keys = _get_linode_cli_keys( + keys["data"], key_lifespan, key_rotation_period, current_timestamp + ) + + _rotate_current_key_if_needed(client, linode_cli_keys) + _delete_stale_keys(client, linode_cli_keys, cleanup_batch_size) + + client.config.plugin_set_value( + LAST_KEY_CLEANUP_TIMESTAMP_KEY, str(current_timestamp) + ) + client.config.write_config() + + except Exception as e: + print( + f"Unable to clean up stale linode-cli Object Storage keys: {e}", + file=sys.stderr, + ) + + +def _should_perform_key_cleanup( + client: CLI, options, current_timestamp +) -> bool: + if options.force_key_cleanup: + return True + if not _is_key_cleanup_enabled(client, options): + return False + + last_cleanup = client.config.plugin_get_value( + LAST_KEY_CLEANUP_TIMESTAMP_KEY + ) + + # if we did a cleanup in the last 24 hours, skip it this time + return ( + last_cleanup is None + or int(last_cleanup) <= current_timestamp - 24 * 60 * 60 + ) + + +def _is_key_cleanup_enabled(client, options) -> bool: + if options.key_cleanup_enabled in ["yes", "no"]: + return options.key_cleanup_enabled == "yes" + return client.config.plugin_get_value(KEY_CLEANUP_ENABLED_KEY, True, bool) + + +def _get_key_lifespan(client, options) -> str: + return options.key_lifespan or client.config.plugin_get_value( + KEY_LIFESPAN_KEY, "30d" + ) + + +def _get_key_rotation_period(client, options) -> str: + return options.key_rotation_period or client.config.plugin_get_value( + KEY_ROTATION_PERIOD_KEY, "10d" + ) + + +def _get_cleanup_batch_size(client, options) -> int: + return options.key_cleanup_batch_size or client.config.plugin_get_value( + KEY_CLEANUP_BATCH_SIZE_KEY, 10, int + ) + + +def _get_linode_cli_keys( + keys_data: list, + key_lifespan: str, + key_rotation_period: str, + current_timestamp: int, +) -> list: + stale_threshold = current_timestamp - parse_time(key_lifespan) + rotation_threshold = current_timestamp - parse_time(key_rotation_period) + + def extract_key_info(key: dict) -> Optional[dict]: + match = re.match(r"^linode-cli-.+@.+-(\d{10,})$", key["label"]) + if not match: + return None + + created_timestamp = int(match.group(1)) + is_stale = created_timestamp < stale_threshold + needs_rotation = is_stale or created_timestamp <= rotation_threshold + + return { + "id": key["id"], + "label": key["label"], + "access_key": key["access_key"], + "created_timestamp": created_timestamp, + "is_stale": is_stale, + "needs_rotation": needs_rotation, + } + + return sorted( + [info for key in keys_data if (info := extract_key_info(key))], + key=lambda k: k["created_timestamp"], + ) + + +def _rotate_current_key_if_needed(client: CLI, linode_cli_keys: list) -> None: + current_access_key = client.config.plugin_get_value(ACCESS_KEY_KEY) + + key_to_rotate = next( + ( + key_info + for key_info in linode_cli_keys + if key_info["access_key"] == current_access_key + and key_info["needs_rotation"] + ), + None, + ) + if key_to_rotate: + _delete_key(client, key_to_rotate["id"], key_to_rotate["label"]) + linode_cli_keys.remove(key_to_rotate) + client.config.plugin_remove_option(ACCESS_KEY_KEY) + client.config.plugin_remove_option(SECRET_KEY_KEY) + client.config.write_config() + + +def _delete_stale_keys( + client: CLI, linode_cli_keys: list, batch_size: int +) -> None: + stale_keys = [k for k in linode_cli_keys if k["is_stale"]] + for key_info in stale_keys[:batch_size]: + _delete_key(client, key_info["id"], key_info["label"]) + + +def _delete_key(client: CLI, key_id: str, label: str) -> None: + try: + print( + f"Deleting linode-cli Object Storage key: {label}", file=sys.stderr + ) + status, _ = client.call_operation( + "object-storage", "keys-delete", [str(key_id)] + ) + if status != 200: + print( + f"Failed to delete key: {label}; status {status}", + file=sys.stderr, + ) + except Exception as e: + print( + f"Exception occurred while deleting key: {label}; {e}", + file=sys.stderr, + ) diff --git a/pyproject.toml b/pyproject.toml index 27a8aea5a..e4fdba13d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -17,7 +17,8 @@ dependencies = [ "packaging", "rich", "urllib3<3", - "linode-metadata>=0.3.0" + "linode-metadata>=0.3.0", + "pytimeparse" ] dynamic = ["version"] diff --git a/tests/unit/test_plugin_obj.py b/tests/unit/test_plugin_obj.py index 17ae5c425..608508099 100644 --- a/tests/unit/test_plugin_obj.py +++ b/tests/unit/test_plugin_obj.py @@ -1,6 +1,10 @@ +import time +from unittest.mock import patch + from pytest import CaptureFixture -from linodecli import CLI +from linodecli import CLI, plugins +from linodecli.plugins import obj from linodecli.plugins.obj import get_obj_args_parser, helpers, print_help @@ -34,3 +38,301 @@ def test_helpers_denominate(): assert helpers._denominate(123456789) == "117.74 MB" assert helpers._denominate(1e23) == "90949470177.29 TB" + + +def test_obj_action_triggers_key_cleanup_and_deletes_stale_key(): + now = int(time.time()) + stale_timestamp = ( + now - 31 * 24 * 60 * 60 + ) # 31 days ago (assuming 30d lifespan) + fresh_timestamp = now + + stale_key = { + "id": "stale-id", + "label": f"linode-cli-testuser@localhost-{stale_timestamp}", + "access_key": "STALEKEY", + } + fresh_key = { + "id": "fresh-id", + "label": f"linode-cli-testuser@localhost-{fresh_timestamp}", + "access_key": "FRESHKEY", + } + + # Mocks for Linode CLI commands + def call_operation_side_effect(resource, action, *args, **kwargs): + if resource == "object-storage" and action == "keys-list": + return 200, {"data": [stale_key, fresh_key]} + + if resource == "object-storage" and action == "keys-delete": + return 200, {} + + if resource == "object-storage" and action == "keys-create": + return 200, {"access_key": "NEWKEY", "secret_key": "NEWSECRET"} + + if resource == "account" and action == "view": + return 200, {} + + return 200, {} + + # OBJ plugin & CLI config mocks + with ( + patch("linodecli.plugins.obj.CLI") as MockCLI, + patch.dict( + obj.COMMAND_MAP, + { + # We don't want to actually execute any S3 operations + "ls": lambda *args, **kwargs: None, + }, + ), + ): + mock_client = MockCLI.return_value + mock_client.call_operation.side_effect = call_operation_side_effect + + mock_client.config.plugin_get_value.side_effect = ( + lambda k, d=None, t=None: { + "key-cleanup-enabled": True, + "key-lifespan": "30d", + "key-rotation-period": "10d", + "key-cleanup-batch-size": 10, + }.get(k, None) + ) + + mock_client.config.plugin_set_value.return_value = None + mock_client.config.write_config.return_value = None + + # Execute the ls command + obj.call( + ["ls", "bucket"], + plugins.PluginContext("12345", mock_client), + ) + + # Check that keys-delete was called for the stale key only + delete_calls = [ + c + for c in mock_client.call_operation.mock_calls + if c[1][1] == "keys-delete" + ] + assert any( + c[1][2][0] == "stale-id" for c in delete_calls + ), "Stale key was not deleted" + assert not any( + c[1][2][0] == "fresh-id" for c in delete_calls + ), "Fresh key should not be deleted" + + +def test_obj_action_triggers_key_rotation(): + now = int(time.time()) + # Key created 31 days ago, rotation period is 30 days + old_timestamp = now - 60 * 60 * 24 * 31 + + key_due_for_rotation = { + "id": "rotate-id", + "label": f"linode-cli-testuser@localhost-{old_timestamp}", + "access_key": "ROTATEKEY", + } + + # Mocks for Linode CLI commands + def call_operation_side_effect(resource, action, *args, **kwargs): + if resource == "object-storage" and action == "keys-list": + return 200, {"data": [key_due_for_rotation]} + + if resource == "object-storage" and action == "keys-delete": + return 200, {} + + if resource == "object-storage" and action == "keys-create": + return 200, {"access_key": "NEWKEY", "secret_key": "NEWSECRET"} + + if resource == "account" and action == "view": + return 200, {} + + return 200, {} + + # OBJ plugin & CLI config mocks + with ( + patch("linodecli.plugins.obj.CLI") as MockCLI, + patch.dict( + obj.COMMAND_MAP, + { + # We don't want to actually execute any S3 operations + "ls": lambda *args, **kwargs: None, + }, + ), + ): + mock_client = MockCLI.return_value + mock_client.call_operation.side_effect = call_operation_side_effect + mock_client.config.plugin_set_value.return_value = None + mock_client.config.write_config.return_value = None + + mock_options = { + "access-key": "ROTATEKEY", + "secret-key": "12345", + "key-cleanup-enabled": True, + "key-lifespan": "90d", + "key-rotation-period": "30d", + "key-cleanup-batch-size": 10, + } + + mock_client.config.plugin_get_value.side_effect = ( + lambda k, d=None, t=None: mock_options.get(k, None) + ) + + # This test relies on the plugin updating the config + mock_client.config.plugin_remove_option.side_effect = ( + lambda k, d=None, t=None: mock_options.pop(k, None) + ) + + obj.call( + ["ls", "bucket"], + plugins.PluginContext("12345", mock_client), + ) + + # Check that keys-create (rotation) was called + create_calls = [ + c + for c in mock_client.call_operation.mock_calls + if c[1][1] == "keys-create" + ] + assert create_calls, "Key rotation (keys-create) was not triggered" + + # Check that keys-delete was called for the old key + delete_calls = [ + c + for c in mock_client.call_operation.mock_calls + if c[1][1] == "keys-delete" + ] + assert any( + c[1][2][0] == "rotate-id" for c in delete_calls + ), "Old key was not deleted after rotation" + + +def test_obj_action_does_not_trigger_cleanup_if_recent(): + now = int(time.time()) + # Set last cleanup to 1 hour ago (less than 24h) + last_cleanup = now - 60 * 60 + + stale_timestamp = now - 31 * 24 * 60 * 60 + stale_key = { + "id": "stale-id", + "label": f"linode-cli-testuser@localhost-{stale_timestamp}", + "access_key": "STALEKEY", + } + + def call_operation_side_effect(resource, action, *args, **kwargs): + if resource == "object-storage" and action == "keys-list": + return 200, {"data": [stale_key]} + + if resource == "object-storage" and action == "keys-delete": + return 200, {} + + if resource == "object-storage" and action == "keys-create": + return 200, {"access_key": "NEWKEY", "secret_key": "NEWSECRET"} + + if resource == "account" and action == "view": + return 200, {} + + return 200, {} + + with ( + patch("linodecli.plugins.obj.CLI") as MockCLI, + patch.dict( + obj.COMMAND_MAP, + { + # We don't want to actually execute any S3 operations + "ls": lambda *args, **kwargs: None, + }, + ), + ): + mock_client = MockCLI.return_value + + mock_client.call_operation.side_effect = call_operation_side_effect + mock_client.config.plugin_set_value.return_value = None + mock_client.config.write_config.return_value = None + + mock_client.config.plugin_get_value.side_effect = ( + lambda k, d=None, t=None: { + "cluster": "us-mia-1", + "key-cleanup-enabled": True, + "key-lifespan": "30d", + "key-rotation-period": "10d", + "key-cleanup-batch-size": 10, + "last-key-cleanup-timestamp": str(last_cleanup), + }.get(k, None) + ) + + obj.call( + ["ls", "bucket"], + plugins.PluginContext("12345", mock_client), + ) + + # Check that keys-delete was NOT called + delete_calls = [ + c + for c in mock_client.call_operation.mock_calls + if c[1][1] == "keys-delete" + ] + assert ( + not delete_calls + ), "Cleanup should not be performed if it was done in the last 24 hours" + + +def test_obj_action_does_not_trigger_cleanup_if_disabled(): + now = int(time.time()) + stale_timestamp = now - 31 * 24 * 60 * 60 + stale_key = { + "id": "stale-id", + "label": f"linode-cli-testuser@localhost-{stale_timestamp}", + "access_key": "STALEKEY", + } + + def call_operation_side_effect(resource, action, *args, **kwargs): + if resource == "object-storage" and action == "keys-list": + return 200, {"data": [stale_key]} + + if resource == "object-storage" and action == "keys-delete": + return 200, {} + + if resource == "object-storage" and action == "keys-create": + return 200, {"access_key": "NEWKEY", "secret_key": "NEWSECRET"} + + if resource == "account" and action == "view": + return 200, {} + + return 200, {} + + with ( + patch("linodecli.plugins.obj.CLI") as MockCLI, + patch.dict( + obj.COMMAND_MAP, + { + # We don't want to actually execute any S3 operations + "ls": lambda *args, **kwargs: None, + }, + ), + ): + mock_client = MockCLI.return_value + mock_client.config.plugin_get_value.side_effect = ( + lambda k, d=None, t=None: { + "key-cleanup-enabled": False, # Cleanup disabled + "key-lifespan": "30d", + "key-rotation-period": "10d", + "key-cleanup-batch-size": 10, + }.get(k, None) + ) + mock_client.config.plugin_set_value.return_value = None + mock_client.config.write_config.return_value = None + mock_client.call_operation.side_effect = call_operation_side_effect + + obj.call( + ["ls", "bucket"], + plugins.PluginContext("12345", mock_client), + ) + + # Check that keys-delete was NOT called + delete_calls = [ + c + for c in mock_client.call_operation.mock_calls + if c[1][1] == "keys-delete" + ] + assert ( + not delete_calls + ), "Cleanup should not be performed when key-cleanup-enabled is False" From 73879c62daf0f6d45b4b4add28c6d242f1ab0078 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 15 Dec 2025 14:43:44 -0500 Subject: [PATCH 2/2] build(deps): bump actions/checkout from 5 to 6 (#838) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/ci.yml | 8 ++++---- .github/workflows/codeql.yml | 2 +- .github/workflows/dependency-review.yml | 2 +- .github/workflows/e2e-suite-windows.yml | 4 ++-- .github/workflows/e2e-suite.yml | 8 ++++---- .github/workflows/labeler.yml | 2 +- .github/workflows/nightly-smoke-tests.yml | 2 +- .github/workflows/publish-wiki.yml | 2 +- .github/workflows/release.yml | 4 ++-- .github/workflows/remote-release-trigger.yml | 2 +- 10 files changed, 18 insertions(+), 18 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 8b53e76ec..e6030adda 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -7,7 +7,7 @@ jobs: docker-build: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v5 + - uses: actions/checkout@v6 - name: Build the Docker image run: docker build . --file Dockerfile --tag linode/cli:$(date +%s) --build-arg="github_token=$GITHUB_TOKEN" env: @@ -17,7 +17,7 @@ jobs: runs-on: ubuntu-latest steps: - name: checkout repo - uses: actions/checkout@v5 + uses: actions/checkout@v6 - name: setup python 3 uses: actions/setup-python@v6 @@ -37,7 +37,7 @@ jobs: python-version: [ "3.9","3.10","3.11", "3.12", "3.13" ] steps: - name: Clone Repository - uses: actions/checkout@v5 + uses: actions/checkout@v6 - name: Setup Python uses: actions/setup-python@v6 @@ -59,7 +59,7 @@ jobs: runs-on: windows-latest steps: - name: Clone Repository - uses: actions/checkout@v5 + uses: actions/checkout@v6 - name: Setup Python uses: actions/setup-python@v6 diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index 256aafe61..3e7460f3d 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -23,7 +23,7 @@ jobs: build-mode: none steps: - name: Checkout repository - uses: actions/checkout@v5 + uses: actions/checkout@v6 - name: Initialize CodeQL uses: github/codeql-action/init@v4 diff --git a/.github/workflows/dependency-review.yml b/.github/workflows/dependency-review.yml index ec737e92e..a427c8661 100644 --- a/.github/workflows/dependency-review.yml +++ b/.github/workflows/dependency-review.yml @@ -12,7 +12,7 @@ jobs: runs-on: ubuntu-latest steps: - name: 'Checkout repository' - uses: actions/checkout@v5 + uses: actions/checkout@v6 - name: 'Dependency Review' uses: actions/dependency-review-action@v4 with: diff --git a/.github/workflows/e2e-suite-windows.yml b/.github/workflows/e2e-suite-windows.yml index 843ce2fc1..43843f8df 100644 --- a/.github/workflows/e2e-suite-windows.yml +++ b/.github/workflows/e2e-suite-windows.yml @@ -31,7 +31,7 @@ jobs: steps: # Check out merge commit - name: Checkout PR - uses: actions/checkout@v5 + uses: actions/checkout@v6 with: ref: ${{ inputs.sha }} @@ -109,7 +109,7 @@ jobs: steps: - name: Checkout code - uses: actions/checkout@v5 + uses: actions/checkout@v6 with: fetch-depth: 0 submodules: 'recursive' diff --git a/.github/workflows/e2e-suite.yml b/.github/workflows/e2e-suite.yml index 2998c906c..fa5581798 100644 --- a/.github/workflows/e2e-suite.yml +++ b/.github/workflows/e2e-suite.yml @@ -59,7 +59,7 @@ jobs: steps: - name: Checkout Repository with SHA if: ${{ inputs.sha != '' }} - uses: actions/checkout@v5 + uses: actions/checkout@v6 with: fetch-depth: 0 submodules: 'recursive' @@ -67,7 +67,7 @@ jobs: - name: Checkout Repository without SHA if: ${{ inputs.sha == '' }} - uses: actions/checkout@v5 + uses: actions/checkout@v6 with: fetch-depth: 0 submodules: 'recursive' @@ -170,7 +170,7 @@ jobs: steps: - name: Checkout code - uses: actions/checkout@v5 + uses: actions/checkout@v6 with: fetch-depth: 0 submodules: 'recursive' @@ -237,7 +237,7 @@ jobs: steps: - name: Checkout code - uses: actions/checkout@v5 + uses: actions/checkout@v6 with: fetch-depth: 0 submodules: 'recursive' diff --git a/.github/workflows/labeler.yml b/.github/workflows/labeler.yml index 7a3ee5f37..843a41c4d 100644 --- a/.github/workflows/labeler.yml +++ b/.github/workflows/labeler.yml @@ -18,7 +18,7 @@ jobs: steps: - name: Checkout - uses: actions/checkout@v5 + uses: actions/checkout@v6 - name: Run Labeler uses: crazy-max/ghaction-github-labeler@24d110aa46a59976b8a7f35518cb7f14f434c916 diff --git a/.github/workflows/nightly-smoke-tests.yml b/.github/workflows/nightly-smoke-tests.yml index d2e3730db..22db9347e 100644 --- a/.github/workflows/nightly-smoke-tests.yml +++ b/.github/workflows/nightly-smoke-tests.yml @@ -18,7 +18,7 @@ jobs: steps: - name: Checkout code - uses: actions/checkout@v5 + uses: actions/checkout@v6 with: fetch-depth: 0 submodules: 'recursive' diff --git a/.github/workflows/publish-wiki.yml b/.github/workflows/publish-wiki.yml index e24607285..b846f089d 100644 --- a/.github/workflows/publish-wiki.yml +++ b/.github/workflows/publish-wiki.yml @@ -14,5 +14,5 @@ jobs: publish-wiki: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v5 + - uses: actions/checkout@v6 - uses: Andrew-Chen-Wang/github-wiki-action@6448478bd55f1f3f752c93af8ac03207eccc3213 # pin@v5.0.3 diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 13b6d9871..77e26ca66 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -28,7 +28,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Clone Repository - uses: actions/checkout@v5 + uses: actions/checkout@v6 - name: setup python 3 uses: actions/setup-python@v6 @@ -86,7 +86,7 @@ jobs: environment: pypi-release steps: - name: Checkout - uses: actions/checkout@v5 + uses: actions/checkout@v6 - name: Setup Python uses: actions/setup-python@v6 diff --git a/.github/workflows/remote-release-trigger.yml b/.github/workflows/remote-release-trigger.yml index 6d472f860..2dfb75c2c 100644 --- a/.github/workflows/remote-release-trigger.yml +++ b/.github/workflows/remote-release-trigger.yml @@ -15,7 +15,7 @@ jobs: private_key: ${{ secrets.CLI_RELEASE_PRIVATE_KEY }} - name: Checkout - uses: actions/checkout@v5 + uses: actions/checkout@v6 with: # We want to checkout the main branch ref: 'main'