[Concept,20/24] pickman: Add config-file support for GitLab token

Message ID 20251217022823.392557-21-sjg@u-boot.org
State New
Headers
Series pickman: Refine the feature set |

Commit Message

Simon Glass Dec. 17, 2025, 2:28 a.m. UTC
  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(-)
  

Patch

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')