From: Simon Glass <simon.glass@canonical.com> Add support for storing a GitLab API token in ~/.config/pickman.conf instead of an environment variable. This allows using a dedicated bot account for pickman without affecting the user's personal GitLab credentials. Config file format: [gitlab] token = glpat-xxxxxxxxxxxxxxxxxxxx This falls back to GITLAB_TOKEN environment variable if config file is not present. Co-developed-by: Claude Opus 4.5 <noreply@anthropic.com> Signed-off-by: Simon Glass <simon.glass@canonical.com> --- tools/pickman/README.rst | 17 ++++++++++--- tools/pickman/ftest.py | 49 +++++++++++++++++++++++++++++++++++++ tools/pickman/gitlab_api.py | 39 ++++++++++++++++++++++++++++- 3 files changed, 101 insertions(+), 4 deletions(-) diff --git a/tools/pickman/README.rst b/tools/pickman/README.rst index 4b99de552b2..ce520f09a45 100644 --- a/tools/pickman/README.rst +++ b/tools/pickman/README.rst @@ -192,9 +192,20 @@ To use the ``-p`` (push) option for GitLab integration, install python-gitlab:: pip install python-gitlab -You will also need a GitLab API token set in the ``GITLAB_TOKEN`` environment -variable. See `GitLab Personal Access Tokens`_ for instructions on creating one. -The token needs ``api`` scope. +You will also need a GitLab API token. The token can be configured in a config +file or environment variable. Pickman checks in this order: + +1. Config file ``~/.config/pickman.conf``:: + + [gitlab] + token = glpat-xxxxxxxxxxxxxxxxxxxx + +2. ``GITLAB_TOKEN`` environment variable +3. ``GITLAB_API_TOKEN`` environment variable + +See `GitLab Personal Access Tokens`_ for instructions on creating a token. +The token needs ``api`` scope. Using a dedicated bot account for pickman is +recommended. .. _GitLab Personal Access Tokens: https://docs.gitlab.com/ee/user/profile/personal_access_tokens.html diff --git a/tools/pickman/ftest.py b/tools/pickman/ftest.py index 3e898eff188..66e087e6625 100644 --- a/tools/pickman/ftest.py +++ b/tools/pickman/ftest.py @@ -1431,6 +1431,55 @@ class TestCheckAvailable(unittest.TestCase): self.assertTrue(result) +class TestConfigFile(unittest.TestCase): + """Tests for config file support.""" + + def setUp(self): + """Set up test fixtures.""" + self.config_dir = tempfile.mkdtemp() + self.config_file = os.path.join(self.config_dir, 'pickman.conf') + + def tearDown(self): + """Clean up test fixtures.""" + shutil.rmtree(self.config_dir) + + def test_get_token_from_config(self): + """Test getting token from config file.""" + with open(self.config_file, 'w', encoding='utf-8') as fhandle: + fhandle.write('[gitlab]\ntoken = test-config-token\n') + + with mock.patch.object(gitlab_api, 'CONFIG_FILE', self.config_file): + token = gitlab_api.get_token() + self.assertEqual(token, 'test-config-token') + + def test_get_token_fallback_to_env(self): + """Test falling back to environment variable.""" + # Config file doesn't exist + with mock.patch.object(gitlab_api, 'CONFIG_FILE', '/nonexistent/path'): + with mock.patch.dict(os.environ, {'GITLAB_TOKEN': 'env-token'}): + token = gitlab_api.get_token() + self.assertEqual(token, 'env-token') + + def test_get_token_config_missing_section(self): + """Test config file without gitlab section.""" + with open(self.config_file, 'w', encoding='utf-8') as fhandle: + fhandle.write('[other]\nkey = value\n') + + with mock.patch.object(gitlab_api, 'CONFIG_FILE', self.config_file): + with mock.patch.dict(os.environ, {'GITLAB_TOKEN': 'env-token'}): + token = gitlab_api.get_token() + self.assertEqual(token, 'env-token') + + def test_get_config_value(self): + """Test get_config_value function.""" + with open(self.config_file, 'w', encoding='utf-8') as fhandle: + fhandle.write('[section1]\nkey1 = value1\n') + + with mock.patch.object(gitlab_api, 'CONFIG_FILE', self.config_file): + value = gitlab_api.get_config_value('section1', 'key1') + self.assertEqual(value, 'value1') + + class TestUpdateMrDescription(unittest.TestCase): """Tests for update_mr_description function.""" diff --git a/tools/pickman/gitlab_api.py b/tools/pickman/gitlab_api.py index 508168aa75c..d2297f40c93 100644 --- a/tools/pickman/gitlab_api.py +++ b/tools/pickman/gitlab_api.py @@ -6,6 +6,7 @@ """GitLab integration for pickman - push branches and create merge requests.""" from collections import namedtuple +import configparser import os import re import sys @@ -50,12 +51,48 @@ def check_available(): return True +CONFIG_FILE = os.path.expanduser('~/.config/pickman.conf') + + +def get_config_value(section, key): + """Get a value from the pickman config file + + Args: + section (str): Config section name + key (str): Config key name + + Returns: + str: Value or None if not found + """ + if not os.path.exists(CONFIG_FILE): + return None + + config = configparser.ConfigParser() + config.read(CONFIG_FILE) + + try: + return config.get(section, key) + except (configparser.NoSectionError, configparser.NoOptionError): + return None + + def get_token(): - """Get GitLab API token from environment + """Get GitLab API token from config file or environment + + Checks in order: + 1. Config file (~/.config/pickman.conf) [gitlab] token + 2. GITLAB_TOKEN environment variable + 3. GITLAB_API_TOKEN environment variable Returns: str: Token or None if not set """ + # Try config file first + token = get_config_value('gitlab', 'token') + if token: + return token + + # Fall back to environment variables return os.environ.get('GITLAB_TOKEN') or os.environ.get('GITLAB_API_TOKEN') -- 2.43.0