From e0e6f58165410803feeef8a8c7a9e439e2df93fe Mon Sep 17 00:00:00 2001 From: mgcam Date: Fri, 2 Aug 2024 13:21:38 +0100 Subject: [PATCH 1/8] Added "create_token" action. Also fixed problems in the change log. --- CHANGELOG.md | 10 +++++++-- src/npg_porch_cli/api.py | 34 ++++++++++++++++++++++++++++++- src/npg_porch_cli/api_cli_user.py | 13 ++++++++++-- tests/test_api.py | 31 ++++++++++++++++++++++++++-- 4 files changed, 81 insertions(+), 7 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 88746e0..3d59c27 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,12 +1,18 @@ -# Change Log for npg_porch Project + +# Change Log for npg_porch_cli Project The format is based on [Keep a Changelog](http://keepachangelog.com/). This project adheres to [Semantic Versioning](http://semver.org/). ## [Unreleased] +### Added + +* Implemented the `create_token` action. Provided the caller has an admin token, + this action generates and returns a new pipeline-specific token. + ## [0.1.0] - 2024-07-23 ### Added -# Initial project scaffold, code and tests +* Initial project scaffold, code and tests diff --git a/src/npg_porch_cli/api.py b/src/npg_porch_cli/api.py index 11f6dba..e639cc1 100644 --- a/src/npg_porch_cli/api.py +++ b/src/npg_porch_cli/api.py @@ -162,7 +162,9 @@ def list_client_actions() -> list[str]: return sorted(_PORCH_CLIENT_ACTIONS.keys()) -def send(action: PorchAction, pipeline: Pipeline = None) -> dict | list: +def send( + action: PorchAction, pipeline: Pipeline = None, description: str | None = None +) -> dict | list: """Sends a request to the porch API server. Sends a request to the porch API server to perform an action defined @@ -174,6 +176,8 @@ def send(action: PorchAction, pipeline: Pipeline = None) -> dict | list: npg_porch_cli.api.PorchAction object pipeline: npg_porch_cli.api.Pipeline object + description: + A description for the new token, optional Returns: The server's response is returned as a Python data structure. @@ -183,6 +187,8 @@ def send(action: PorchAction, pipeline: Pipeline = None) -> dict | list: function = _PORCH_CLIENT_ACTIONS[action.action] if action.action == "list_pipelines": return function(action=action) + elif action.action == "create_token": + return function(action=action, pipeline=pipeline, description=description) return function(action=action, pipeline=pipeline) @@ -333,6 +339,31 @@ def update_task(action: PorchAction, pipeline: Pipeline): ) +def create_token(action: PorchAction, pipeline: Pipeline, description: str): + """Creates a new token for the pipeline. + + Args: + action: + npg_porch_cli.api.PorchAction object + pipeline: + npg_porch_cli.api.Pipeline object + description: + A short token description + + Returns: + A dictionary containing a new token. + """ + + if not description: + raise TypeError("Token description should be given") + + return send_request( + validate_ca_cert=action.validate_ca_cert, + url=urljoin(action.porch_url, f"pipelines/{pipeline.name}/token/{description}"), + method="POST", + ) + + _PORCH_CLIENT_ACTIONS = { "list_tasks": list_tasks, "list_pipelines": list_pipelines, @@ -340,6 +371,7 @@ def update_task(action: PorchAction, pipeline: Pipeline): "add_task": add_task, "claim_task": claim_task, "update_task": update_task, + "create_token": create_token, } diff --git a/src/npg_porch_cli/api_cli_user.py b/src/npg_porch_cli/api_cli_user.py index cd94ddc..3fcdd7a 100755 --- a/src/npg_porch_cli/api_cli_user.py +++ b/src/npg_porch_cli/api_cli_user.py @@ -43,6 +43,7 @@ def run(): list_tasks list_pipelines add_pipeline + create_token add_task claim_task update_task @@ -54,13 +55,15 @@ def run(): `--pipeline_name` is defined, `list_tasks` returns a list of tasks for this pipeline, otherwise all registered tasks are returned. - All non-list actions require all `--pipeline`, `pipeline_url` and + All non-list actions require `--pipeline`, `pipeline_url` and `--pipeline_version` defined. The `add_task` and `update_task` actions require the `--task_json` to be defined. In addition to this, for the `update_task` action `--status` should be defined. + The `create_token` action requires that the `--description` is defined. + NPG_PORCH_TOKEN environment variable should be set to the value of either an admin or project-specific token. @@ -94,6 +97,7 @@ def run(): parser.add_argument("--pipeline", type=str, help="Pipeline name, optional") parser.add_argument("--task_json", type=str, help="Task as JSON, optional") parser.add_argument("--status", type=str, help="New status to set, optional") + parser.add_argument("--description", type=str, help="Token description, optional") args = parser.parse_args() @@ -110,4 +114,9 @@ def run(): name=args.pipeline, uri=args.pipeline_url, version=args.pipeline_version ) - print(json.dumps(send(action=action, pipeline=pipeline), indent=2)) + print( + json.dumps( + send(action=action, pipeline=pipeline, description=args.description), + indent=2, + ) + ) diff --git a/tests/test_api.py b/tests/test_api.py index 685b47b..9de7f7f 100644 --- a/tests/test_api.py +++ b/tests/test_api.py @@ -51,6 +51,7 @@ def test_listing_actions(): "add_pipeline", "add_task", "claim_task", + "create_token", "list_pipelines", "list_tasks", "update_task", @@ -76,8 +77,8 @@ def test_porch_action_class(monkeypatch): PorchAction(porch_url=url, action="list_tools") assert ( e.value.args[0] == "Action 'list_tools' is not valid. " - "Valid actions: add_pipeline, add_task, claim_task, list_pipelines, " - "list_tasks, update_task" + "Valid actions: add_pipeline, add_task, claim_task, create_token, " + "list_pipelines, list_tasks, update_task" ) pa = PorchAction(porch_url=url, action="list_tasks") @@ -247,3 +248,29 @@ def mock_get_200(*args, **kwargs): porch_url=url, action="update_task", task_input=task, task_status="DONE" ) assert send(action=pa, pipeline=p) == response_data + + with monkeypatch.context() as mkp: + response_data = { + "name": "p1", + "description": "for my pipeline", + "token": "ccceddd450aaa", + } + + def mock_get_200(*args, **kwargs): + return MockPorchResponse(response_data, 200) + + mkp.setattr(requests, "request", mock_get_200) + + pa = PorchAction(porch_url=url, action="create_token") + + error_message = "Token description should be given" + with pytest.raises(TypeError) as e: + send(action=pa, pipeline=p) + assert e.value.args[0] == error_message + with pytest.raises(TypeError) as e: + send(action=pa, pipeline=p, description="") + assert e.value.args[0] == error_message + + assert ( + send(action=pa, pipeline=p, description="for my pipeline") == response_data + ) From ecd30aa680b188340ffa3e22550246288a279f11 Mon Sep 17 00:00:00 2001 From: Avnish Pratap Singh Date: Tue, 1 Oct 2024 09:31:40 +0100 Subject: [PATCH 2/8] Added dependabot.yml to auto-update GitHub actions --- .github/dependabot.yml | 10 ++++++++++ CHANGELOG.md | 1 + 2 files changed, 11 insertions(+) create mode 100644 .github/dependabot.yml diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000..619e9bd --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,10 @@ +# Referenced from: +# https://docs.github.com/en/code-security/dependabot/working-with-dependabot/keeping-your-actions-up-to-date-with-dependabot + +version: 2 +updates: + + - package-ecosystem: "github-actions" + directory: "/" + schedule: + interval: "weekly" diff --git a/CHANGELOG.md b/CHANGELOG.md index 3d59c27..6e797b0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,7 @@ This project adheres to [Semantic Versioning](http://semver.org/). ### Added +* Added .github/dependabot.yml file to auto-update GitHub actions * Implemented the `create_token` action. Provided the caller has an admin token, this action generates and returns a new pipeline-specific token. From 957a50daa2d7ad44def909bc6d65ecaae943aa87 Mon Sep 17 00:00:00 2001 From: Kieron Taylor Date: Wed, 11 Dec 2024 14:01:09 +0000 Subject: [PATCH 3/8] Correct CA validation docs --- src/npg_porch_cli/api.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/npg_porch_cli/api.py b/src/npg_porch_cli/api.py index e639cc1..9ccccce 100644 --- a/src/npg_porch_cli/api.py +++ b/src/npg_porch_cli/api.py @@ -171,6 +171,8 @@ def send( by the `action` attribute of the `action` argument. The context of the query is defined by the pipeline argument. + See also send_request for SSL validation guidance + Args: action: npg_porch_cli.api.PorchAction object @@ -390,8 +392,8 @@ def send_request( Args: validate_ca_cert: A boolean flag defining whether the server CA certificate - will be validated. If set to True, SSL_CERT_FILE environment - variable should be set. + will be validated. If set to True, REQUESTS_CA_BUNDLE environment + variable should be set, for example to /etc/ssl/certs/ca-certificates.crt url: A URL to send the request to. method: From 7377b3fdec9564fcfa2be5045a21d5abe93b0711 Mon Sep 17 00:00:00 2001 From: Kieron Taylor Date: Tue, 17 Dec 2024 16:25:55 +0000 Subject: [PATCH 4/8] Lift get_config_data from npg_notifications. Improve error handling by checking for a necessary config file. Use a dataclass to validate all properties are set in conf. --- src/npg_porch_cli/config.py | 98 +++++++++++++++++++++++++++++++ tests/data/conf.ini | 17 ++++++ tests/test_porch_client_config.py | 37 ++++++++++++ 3 files changed, 152 insertions(+) create mode 100644 src/npg_porch_cli/config.py create mode 100644 tests/data/conf.ini create mode 100644 tests/test_porch_client_config.py diff --git a/src/npg_porch_cli/config.py b/src/npg_porch_cli/config.py new file mode 100644 index 0000000..69713cf --- /dev/null +++ b/src/npg_porch_cli/config.py @@ -0,0 +1,98 @@ +import configparser +import json +import pathlib +from dataclasses import dataclass + +"""Common utility functions for the package.""" + +DEFAULT_CONF_FILE_TYPE = "ini" + + +def get_config_data(conf_file_path: str, conf_file_section: str = None): + """ + Parses a configuration file and returns its content. + + Allows for two types of configuration files, 'ini' and 'json'. The type of + the file is determined from the extension of the file name. In case of no + extension an 'ini' type is assumed. + + The content of the file is not cached, so subsequent calls to get data from + the same configuration file result in re-reading and re-parsing of the file. + + Args: + + conf_file_path: + A configuration file with database connection details. + conf_file_section: + The section of the configuration file. Optional. Should be defined + for 'ini' files. + + Returns: + For an 'ini' file returns the content of the given section of the file as + a dictionary. + For a 'json' file, if the conf_file_section argument is not defined, the + content of a file as a Python object is returned. If the conf_file_section + argument is defined, the object returned by the parser is assumed to be + a dictionary that has the value of the 'conf_file_section' argument as a key. + The value corresponding to this key is returned. + """ + + path = pathlib.Path(conf_file_path) + + conf_file_extension = path.suffix + if conf_file_extension: + conf_file_extension = conf_file_extension[1:] + else: + conf_file_extension = DEFAULT_CONF_FILE_TYPE + + if conf_file_extension == DEFAULT_CONF_FILE_TYPE: + if not conf_file_section: + raise Exception( + "'conf_file_section' argument is not given, " + "it should be defined for '{DEFAULT_CONF_FILE_TYPE}' " + "configuration file." + ) + + config = configparser.ConfigParser() + with open(conf_file_path) as cf: + if not cf.readable(): + raise Exception(f"{path} has a permissions issue") + + config.read_file(cf) + + return {i[0]: i[1] for i in config[conf_file_section].items()} + + elif conf_file_extension == "json": + conf: dict = json.load(conf_file_path) + if conf_file_section: + if isinstance(conf, dict) is False: + raise Exception(f"{conf_file_path} does not have sections.") + if conf_file_section in conf.keys: + conf = conf[conf_file_section] + else: + raise Exception( + f"{conf_file_path} does not contain {conf_file_section} key" + ) + + return conf + + else: + raise Exception(f"Parsing for '{conf_file_extension}' files is not implemented") + + +@dataclass(frozen=True, kw_only=True) +class PorchClientConfig: + """ + Suggested config file content for interacting with a Porch server instance + """ + + api_url: str + pipeline_name: str + pipeline_uri: str + pipeline_version: str + npg_porch_token: str + + @classmethod + def from_config_file(cls, conf_file_path: str, conf_file_section: str = "PORCH"): + conf = get_config_data(conf_file_path, conf_file_section=conf_file_section) + return cls(**conf) diff --git a/tests/data/conf.ini b/tests/data/conf.ini new file mode 100644 index 0000000..1f7381a --- /dev/null +++ b/tests/data/conf.ini @@ -0,0 +1,17 @@ +[STUFF] + +logging = INFO + +[PORCH] + +api_url = https://porch.dnapipelines.sanger.ac.uk +pipeline_name = test_pipeline +pipeline_uri = https://test.pipeline.com +pipeline_version = 9.9.9 +npg_porch_token = 0123456789abcdef0123456789abcdef + +[PARTIALPORCH] + +api_url = https://porch.dnapipelines.sanger.ac.uk +pipeline_name = test_pipeline +pipeline_uri = https://test.pipeline.com diff --git a/tests/test_porch_client_config.py b/tests/test_porch_client_config.py new file mode 100644 index 0000000..6e901ae --- /dev/null +++ b/tests/test_porch_client_config.py @@ -0,0 +1,37 @@ +from pytest import raises + +from npg_porch_cli.config import PorchClientConfig, get_config_data + + +def test_config_file_reading(): + conf = get_config_data( + conf_file_path="tests/data/conf.ini", conf_file_section="STUFF" + ) + + assert conf == {"logging": "INFO"} + + +def test_conf_obj(): + config_obj = PorchClientConfig.from_config_file("tests/data/conf.ini") + assert config_obj.pipeline_name == "test_pipeline" + assert config_obj.pipeline_version == "9.9.9" + + with raises(Exception): + PorchClientConfig.from_config_file( + "tests/data/conf.ini", conf_file_section="ABSENT" + ) + + with raises(Exception): + PorchClientConfig.from_config_file("notafile", conf_file_section="ABSENT") + + with raises(TypeError) as e: + PorchClientConfig.from_config_file( + "tests/data/conf.ini", conf_file_section="STUFF" + ) + assert e.match("unexpected keyword argument 'logging'") + + with raises(Exception) as e: + PorchClientConfig.from_config_file( + "tests/data/conf.ini", conf_file_section="PARTIALPORCH" + ) + assert e.match("missing 2 required keyword-only arguments") From ba7fecac0f515ac0ce8a25b39a8103750ad0efa2 Mon Sep 17 00:00:00 2001 From: Kieron Taylor Date: Tue, 17 Dec 2024 16:46:39 +0000 Subject: [PATCH 5/8] Narrow down exception tests in the name of Flake virtues --- tests/test_porch_client_config.py | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/tests/test_porch_client_config.py b/tests/test_porch_client_config.py index 6e901ae..a761bbd 100644 --- a/tests/test_porch_client_config.py +++ b/tests/test_porch_client_config.py @@ -16,22 +16,20 @@ def test_conf_obj(): assert config_obj.pipeline_name == "test_pipeline" assert config_obj.pipeline_version == "9.9.9" - with raises(Exception): + with raises(KeyError, match="ABSENT"): PorchClientConfig.from_config_file( "tests/data/conf.ini", conf_file_section="ABSENT" ) - with raises(Exception): + with raises(FileNotFoundError, match="No such file or directory: 'notafile'"): PorchClientConfig.from_config_file("notafile", conf_file_section="ABSENT") - with raises(TypeError) as e: + with raises(TypeError, match="unexpected keyword argument 'logging'"): PorchClientConfig.from_config_file( "tests/data/conf.ini", conf_file_section="STUFF" ) - assert e.match("unexpected keyword argument 'logging'") - with raises(Exception) as e: + with raises(TypeError, match="missing 2 required keyword-only arguments"): PorchClientConfig.from_config_file( "tests/data/conf.ini", conf_file_section="PARTIALPORCH" ) - assert e.match("missing 2 required keyword-only arguments") From 102c98bdc71709af67b8c316fc0b331bbae8c71a Mon Sep 17 00:00:00 2001 From: Kieron Taylor Date: Wed, 18 Dec 2024 10:43:03 +0000 Subject: [PATCH 6/8] Prep for release 0.2.0 --- CHANGELOG.md | 4 +++- pyproject.toml | 4 ++-- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6e797b0..9abf3a7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,13 +4,15 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/). This project adheres to [Semantic Versioning](http://semver.org/). -## [Unreleased] +## [0.2.0] - 2024-12-18 ### Added * Added .github/dependabot.yml file to auto-update GitHub actions * Implemented the `create_token` action. Provided the caller has an admin token, this action generates and returns a new pipeline-specific token. +* Brought some semi-standard Porch client config over from npg_notifications to + improve reusability ## [0.1.0] - 2024-07-23 diff --git a/pyproject.toml b/pyproject.toml index 049982b..37cb1f7 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "npg_porch_cli" -version = "0.1.0" +version = "0.2.0" authors = [ "Marina Gourtovaia", "Kieron Taylor", @@ -11,7 +11,7 @@ readme = "README.md" license = "GPL-3.0-or-later" [tool.poetry.scripts] -npg_porch_client = "npg_porch_cli.api_cli_user:run" +npg_porch_client = "npg_porch_cli.api_cli_user:run" [tool.poetry.dependencies] python = "^3.10" From 82259cca24d632ab50ab7f3cd339f7241009cccf Mon Sep 17 00:00:00 2001 From: Kieron Taylor Date: Wed, 18 Dec 2024 16:28:19 +0000 Subject: [PATCH 7/8] Replace config-reading code with npg-python-lib npg.conf library --- pyproject.toml | 1 + src/npg_porch_cli/config.py | 94 ++++++++----------------------- tests/test_porch_client_config.py | 28 ++------- 3 files changed, 30 insertions(+), 93 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 37cb1f7..94f5a45 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -16,6 +16,7 @@ npg_porch_client = "npg_porch_cli.api_cli_user:run" [tool.poetry.dependencies] python = "^3.10" requests = "^2.31.0" +npg-python-lib = { git="https://github.com/wtsi-npg/npg-python-lib", tag="0.3.4"} [tool.poetry.dev-dependencies] black = "^22.3.0" diff --git a/src/npg_porch_cli/config.py b/src/npg_porch_cli/config.py index 69713cf..4ef0a08 100644 --- a/src/npg_porch_cli/config.py +++ b/src/npg_porch_cli/config.py @@ -1,23 +1,28 @@ -import configparser -import json -import pathlib from dataclasses import dataclass +from os import R_OK, access +from os.path import isfile -"""Common utility functions for the package.""" +from npg.conf import IniData -DEFAULT_CONF_FILE_TYPE = "ini" - -def get_config_data(conf_file_path: str, conf_file_section: str = None): +@dataclass(frozen=True, kw_only=True) +class PorchClientConfig: """ - Parses a configuration file and returns its content. + Suggested config file content for interacting with a Porch server instance + """ + + api_url: str + pipeline_name: str + pipeline_uri: str + pipeline_version: str + npg_porch_token: str - Allows for two types of configuration files, 'ini' and 'json'. The type of - the file is determined from the extension of the file name. In case of no - extension an 'ini' type is assumed. - The content of the file is not cached, so subsequent calls to get data from - the same configuration file result in re-reading and re-parsing of the file. +def get_config_data( + conf_file_path: str, conf_file_section: str = "PORCH" +) -> PorchClientConfig: + """ + Parses a configuration file and returns its content. Args: @@ -37,62 +42,11 @@ def get_config_data(conf_file_path: str, conf_file_section: str = None): The value corresponding to this key is returned. """ - path = pathlib.Path(conf_file_path) - - conf_file_extension = path.suffix - if conf_file_extension: - conf_file_extension = conf_file_extension[1:] - else: - conf_file_extension = DEFAULT_CONF_FILE_TYPE - - if conf_file_extension == DEFAULT_CONF_FILE_TYPE: - if not conf_file_section: - raise Exception( - "'conf_file_section' argument is not given, " - "it should be defined for '{DEFAULT_CONF_FILE_TYPE}' " - "configuration file." - ) - - config = configparser.ConfigParser() - with open(conf_file_path) as cf: - if not cf.readable(): - raise Exception(f"{path} has a permissions issue") - - config.read_file(cf) - - return {i[0]: i[1] for i in config[conf_file_section].items()} - - elif conf_file_extension == "json": - conf: dict = json.load(conf_file_path) - if conf_file_section: - if isinstance(conf, dict) is False: - raise Exception(f"{conf_file_path} does not have sections.") - if conf_file_section in conf.keys: - conf = conf[conf_file_section] - else: - raise Exception( - f"{conf_file_path} does not contain {conf_file_section} key" - ) - - return conf - + if isfile(conf_file_path) and access(conf_file_path, R_OK): + porch_conf = IniData(PorchClientConfig).from_file( + conf_file_path, conf_file_section + ) else: - raise Exception(f"Parsing for '{conf_file_extension}' files is not implemented") - - -@dataclass(frozen=True, kw_only=True) -class PorchClientConfig: - """ - Suggested config file content for interacting with a Porch server instance - """ - - api_url: str - pipeline_name: str - pipeline_uri: str - pipeline_version: str - npg_porch_token: str + raise FileNotFoundError(f"{conf_file_path} is not present or cannot be read") - @classmethod - def from_config_file(cls, conf_file_path: str, conf_file_section: str = "PORCH"): - conf = get_config_data(conf_file_path, conf_file_section=conf_file_section) - return cls(**conf) + return porch_conf diff --git a/tests/test_porch_client_config.py b/tests/test_porch_client_config.py index a761bbd..8c37e11 100644 --- a/tests/test_porch_client_config.py +++ b/tests/test_porch_client_config.py @@ -3,33 +3,15 @@ from npg_porch_cli.config import PorchClientConfig, get_config_data -def test_config_file_reading(): - conf = get_config_data( - conf_file_path="tests/data/conf.ini", conf_file_section="STUFF" - ) - - assert conf == {"logging": "INFO"} - - def test_conf_obj(): - config_obj = PorchClientConfig.from_config_file("tests/data/conf.ini") + config_obj = get_config_data("tests/data/conf.ini") assert config_obj.pipeline_name == "test_pipeline" assert config_obj.pipeline_version == "9.9.9" - with raises(KeyError, match="ABSENT"): - PorchClientConfig.from_config_file( - "tests/data/conf.ini", conf_file_section="ABSENT" - ) - - with raises(FileNotFoundError, match="No such file or directory: 'notafile'"): - PorchClientConfig.from_config_file("notafile", conf_file_section="ABSENT") + assert type(config_obj) is PorchClientConfig - with raises(TypeError, match="unexpected keyword argument 'logging'"): - PorchClientConfig.from_config_file( - "tests/data/conf.ini", conf_file_section="STUFF" - ) + with raises(FileNotFoundError, match="notafile is not present or cannot be read"): + get_config_data("notafile", conf_file_section="ABSENT") with raises(TypeError, match="missing 2 required keyword-only arguments"): - PorchClientConfig.from_config_file( - "tests/data/conf.ini", conf_file_section="PARTIALPORCH" - ) + get_config_data("tests/data/conf.ini", conf_file_section="PARTIALPORCH") From 6f9c27eff41ff9be0c5fc392d1389e613424eb93 Mon Sep 17 00:00:00 2001 From: Kieron Taylor Date: Wed, 18 Dec 2024 16:31:47 +0000 Subject: [PATCH 8/8] Fix changelog --- CHANGELOG.md | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9abf3a7..3365670 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,8 +11,7 @@ This project adheres to [Semantic Versioning](http://semver.org/). * Added .github/dependabot.yml file to auto-update GitHub actions * Implemented the `create_token` action. Provided the caller has an admin token, this action generates and returns a new pipeline-specific token. -* Brought some semi-standard Porch client config over from npg_notifications to - improve reusability +* Used npg-python-lib to read Porch config ## [0.1.0] - 2024-07-23