@@ -42,6 +42,9 @@ def from_env(cls) -> 'GitlabConfig':
4242 mr_source_branch = os .getenv ('CI_MERGE_REQUEST_SOURCE_BRANCH_NAME' )
4343 default_branch = os .getenv ('CI_DEFAULT_BRANCH' , '' )
4444
45+ # Determine which authentication pattern to use
46+ headers = cls ._get_auth_headers (token )
47+
4548 return cls (
4649 commit_sha = os .getenv ('CI_COMMIT_SHA' , '' ),
4750 api_url = os .getenv ('CI_API_V4_URL' , '' ),
@@ -57,18 +60,119 @@ def from_env(cls) -> 'GitlabConfig':
5760 token = token ,
5861 repository = project_name ,
5962 is_default_branch = (mr_source_branch == default_branch if mr_source_branch else False ),
60- headers = {
61- 'Authorization' : f"Bearer { token } " ,
62- 'User-Agent' : 'SocketPythonScript/0.0.1' ,
63- "accept" : "application/json"
64- }
63+ headers = headers
6564 )
6665
66+ @staticmethod
67+ def _get_auth_headers (token : str ) -> dict :
68+ """
69+ Determine the appropriate authentication headers for GitLab API.
70+
71+ GitLab supports two authentication patterns:
72+ 1. Bearer token (OAuth 2.0 tokens, personal access tokens with api scope)
73+ 2. Private token (personal access tokens)
74+
75+ Logic for token type determination:
76+ - CI_JOB_TOKEN: Always use Bearer (GitLab CI job token)
77+ - Tokens starting with 'glpat-': Personal access tokens, try Bearer first
78+ - OAuth tokens: Use Bearer
79+ - Other tokens: Use PRIVATE-TOKEN as fallback
80+ """
81+ base_headers = {
82+ 'User-Agent' : 'SocketPythonScript/0.0.1' ,
83+ "accept" : "application/json"
84+ }
85+
86+ # Check if this is a GitLab CI job token
87+ if token == os .getenv ('CI_JOB_TOKEN' ):
88+ log .debug ("Using Bearer authentication for GitLab CI job token" )
89+ return {
90+ ** base_headers ,
91+ 'Authorization' : f"Bearer { token } "
92+ }
93+
94+ # Check for personal access token pattern
95+ if token .startswith ('glpat-' ):
96+ log .debug ("Using Bearer authentication for GitLab personal access token" )
97+ return {
98+ ** base_headers ,
99+ 'Authorization' : f"Bearer { token } "
100+ }
101+
102+ # Check for OAuth token pattern (typically longer and alphanumeric)
103+ if len (token ) > 40 and token .isalnum ():
104+ log .debug ("Using Bearer authentication for potential OAuth token" )
105+ return {
106+ ** base_headers ,
107+ 'Authorization' : f"Bearer { token } "
108+ }
109+
110+ # Default to PRIVATE-TOKEN for other token types
111+ log .debug ("Using PRIVATE-TOKEN authentication for GitLab token" )
112+ return {
113+ ** base_headers ,
114+ 'PRIVATE-TOKEN' : f"{ token } "
115+ }
116+
67117class Gitlab :
68118 def __init__ (self , client : CliClient , config : Optional [GitlabConfig ] = None ):
69119 self .config = config or GitlabConfig .from_env ()
70120 self .client = client
71121
122+ def _request_with_fallback (self , ** kwargs ):
123+ """
124+ Make a request with automatic fallback between Bearer and PRIVATE-TOKEN authentication.
125+ This provides robustness when the initial token type detection is incorrect.
126+ """
127+ try :
128+ # Try the initial request with the configured headers
129+ return self .client .request (** kwargs )
130+ except Exception as e :
131+ # Check if this is an authentication error (401)
132+ if hasattr (e , 'response' ) and e .response and e .response .status_code == 401 :
133+ log .debug (f"Authentication failed with initial headers, trying fallback method" )
134+
135+ # Determine the fallback headers
136+ original_headers = kwargs .get ('headers' , self .config .headers )
137+ fallback_headers = self ._get_fallback_headers (original_headers )
138+
139+ if fallback_headers and fallback_headers != original_headers :
140+ log .debug ("Retrying request with fallback authentication method" )
141+ kwargs ['headers' ] = fallback_headers
142+ return self .client .request (** kwargs )
143+
144+ # Re-raise the original exception if it's not an auth error or fallback failed
145+ raise
146+
147+ def _get_fallback_headers (self , original_headers : dict ) -> dict :
148+ """
149+ Generate fallback authentication headers.
150+ If using Bearer, fallback to PRIVATE-TOKEN and vice versa.
151+ """
152+ base_headers = {
153+ 'User-Agent' : 'SocketPythonScript/0.0.1' ,
154+ "accept" : "application/json"
155+ }
156+
157+ # If currently using Bearer, try PRIVATE-TOKEN
158+ if 'Authorization' in original_headers and 'Bearer' in original_headers ['Authorization' ]:
159+ log .debug ("Falling back from Bearer to PRIVATE-TOKEN authentication" )
160+ return {
161+ ** base_headers ,
162+ 'PRIVATE-TOKEN' : f"{ self .config .token } "
163+ }
164+
165+ # If currently using PRIVATE-TOKEN, try Bearer
166+ elif 'PRIVATE-TOKEN' in original_headers :
167+ log .debug ("Falling back from PRIVATE-TOKEN to Bearer authentication" )
168+ return {
169+ ** base_headers ,
170+ 'Authorization' : f"Bearer { self .config .token } "
171+ }
172+
173+ # No fallback available
174+ return None
175+
72176 def check_event_type (self ) -> str :
73177 pipeline_source = self .config .pipeline_source .lower ()
74178 if pipeline_source in ["web" , 'merge_request_event' , "push" , "api" ]:
@@ -84,7 +188,7 @@ def check_event_type(self) -> str:
84188 def post_comment (self , body : str ) -> None :
85189 path = f"projects/{ self .config .mr_project_id } /merge_requests/{ self .config .mr_iid } /notes"
86190 payload = {"body" : body }
87- self .client . request (
191+ self ._request_with_fallback (
88192 path = path ,
89193 payload = payload ,
90194 method = "POST" ,
@@ -95,7 +199,7 @@ def post_comment(self, body: str) -> None:
95199 def update_comment (self , body : str , comment_id : str ) -> None :
96200 path = f"projects/{ self .config .mr_project_id } /merge_requests/{ self .config .mr_iid } /notes/{ comment_id } "
97201 payload = {"body" : body }
98- self .client . request (
202+ self ._request_with_fallback (
99203 path = path ,
100204 payload = payload ,
101205 method = "PUT" ,
@@ -106,7 +210,7 @@ def update_comment(self, body: str, comment_id: str) -> None:
106210 def get_comments_for_pr (self ) -> dict :
107211 log .debug (f"Getting Gitlab comments for Repo { self .config .repository } for PR { self .config .mr_iid } " )
108212 path = f"projects/{ self .config .mr_project_id } /merge_requests/{ self .config .mr_iid } /notes"
109- response = self .client . request (
213+ response = self ._request_with_fallback (
110214 path = path ,
111215 headers = self .config .headers ,
112216 base_url = self .config .api_url
0 commit comments