44-----------
55- sync: Reconcile all project authorizations from a curated file into the
66 database (create/update/delete).
7-
87- create-dt-projects: Create the DependencyTrack projects referenced by a curated
98 file (provisioning only; no database access).
109
11- - add-workload: Register a CI/CD workload that is allowed to upload SBOMs for an
12- Eclipse Foundation project.
13-
14- - add-dt-project: Register a DependencyTrack project as the upload target for a
15- given Eclipse Foundation project.
16-
17- Usage Examples
18- --------------
19- Reconcile All Authorizations From a Curated File:
20-
10+ Usage Example
11+ -------------
2112 PIA_DATABASE_URL=postgresql://user:secret@localhost:5432/pia \
2213 PIA_DEPENDENCY_TRACK_API_KEY=<API key with VIEW_PORTFOLIO permission> \
2314 PIA_GITHUB_TOKEN=<optional token with public read permission for rate limit> \
2415 uv run pia sync projects.yaml --dt-url https://sbom.eclipse.org --dry-run
2516
26- Register GitHub Actions:
27-
28- PIA_DATABASE_URL=postgresql://user:secret@localhost:5432/pia \
29- uv run pia add-workload eclipse-foo \
30- https://github.com/eclipse-foo/repo
31-
32- Register Jenkins Instance:
33-
34- PIA_DATABASE_URL=postgresql://user:secret@localhost:5432/pia \
35- uv run pia add-workload eclipse-bar \
36- https://ci.eclipse.org/eclipse-bar/oidc
37-
38- Register DependencyTrack Project:
39-
40- PIA_DATABASE_URL=postgresql://user:secret@localhost:5432/pia \
41- PIA_DEPENDENCY_TRACK_API_KEY=<API key with VIEW_PORTFOLIO permission> \
42- uv run pia add-dt-project eclipse-baz \
43- https://sbom.eclipse.org "Eclipse Baz" baz-server
44-
4517"""
4618
4719import logging
4820import os
49- from typing import Any
50- from urllib .parse import urlparse
5121
5222import click
53- import requests
5423from sqlalchemy import create_engine
5524from sqlalchemy .orm import Session , sessionmaker
5625
57- from .models import (
58- GITHUB_BASE_URL ,
59- JENKINS_ISSUER_BASE_URL ,
60- DependencyTrackProject ,
61- EclipseFoundationProject ,
62- GitHubWorkload ,
63- JenkinsWorkload ,
64- Workload ,
65- )
6626from .sync import (
6727 apply_plan ,
6828 build_desired ,
7333 validate_projects_file ,
7434)
7535
76- logger = logging .getLogger (__name__ )
77-
7836
7937@click .group ()
8038@click .option ("-v" , "--verbose" , is_flag = True , help = "Enable debug logging." )
@@ -91,173 +49,6 @@ def _make_session() -> Session:
9149 return sessionmaker (bind = engine )()
9250
9351
94- def _get_dt_api_key () -> str :
95- key = os .environ .get ("PIA_DEPENDENCY_TRACK_API_KEY" )
96- if not key :
97- raise click .ClickException ("PIA_DEPENDENCY_TRACK_API_KEY is not set" )
98- return key
99-
100-
101- def _create_ef_project_if_needed (session : Session , ef_project_id : str ) -> None :
102- if session .get (EclipseFoundationProject , ef_project_id ) is None :
103- logger .info (f"Creating EclipseFoundationProject { ef_project_id !r} " )
104- session .add (EclipseFoundationProject (id = ef_project_id ))
105- else :
106- logger .info (f"Using existing EclipseFoundationProject { ef_project_id !r} " )
107-
108-
109- def _fetch_github_owner_id (owner : str ) -> str :
110- url = f"https://api.github.com/users/{ owner } "
111- logger .info (f"Fetching GitHub owner id from { url } " )
112- response = requests .get (url , headers = {"Accept" : "application/vnd.github+json" })
113- response .raise_for_status ()
114- owner_id = str (response .json ()["id" ])
115- logger .info (f"GitHub owner { owner !r} has id { owner_id } " )
116- return owner_id
117-
118-
119- def _dt_find_root_project_by_name (
120- dt_url : str , name : str , api_key : str
121- ) -> dict [str , Any ]:
122- """Look up a root DependencyTrack project by name, asserting exactly one match."""
123- url = f"{ dt_url .rstrip ('/' )} /api/v1/project"
124- logger .info (f"Querying DependencyTrack root projects at { url } for name={ name !r} " )
125- response = requests .get (
126- url ,
127- params = {"name" : name , "onlyRoot" : "true" },
128- headers = {"X-Api-Key" : api_key , "Accept" : "application/json" },
129- )
130- response .raise_for_status ()
131- projects = response .json ()
132- if len (projects ) != 1 :
133- raise click .ClickException (
134- f"Expected exactly one root DependencyTrack project named { name !r} , "
135- f"found { len (projects )} "
136- )
137- return projects [0 ]
138-
139-
140- @cli .command ("add-workload" )
141- @click .argument ("ef_project_id" )
142- @click .argument ("url" )
143- @click .option (
144- "--dry-run" ,
145- is_flag = True ,
146- help = "Look up data and prepare the row, but do not commit." ,
147- )
148- def add_workload (ef_project_id : str , url : str , dry_run : bool ) -> None :
149- """Register a GitHub or Jenkins workload for an Eclipse Foundation project.
150-
151- URL type is determined by its value: github.com URLs create a GitHubWorkload,
152- ci.eclipse.org URLs create a JenkinsWorkload with the URL as issuer. Both are
153- matched on the URL's scheme and host. Any other URL is rejected.
154- """
155- # Fail early, before the GitHub lookup, rather than after it.
156- if not os .environ .get ("PIA_DATABASE_URL" ):
157- raise click .ClickException ("PIA_DATABASE_URL is not set" )
158-
159- parsed = urlparse (url )
160- base_url = f"{ parsed .scheme } ://{ parsed .hostname } "
161- if base_url == GITHUB_BASE_URL :
162- path_parts = parsed .path .strip ("/" ).split ("/" )
163- if len (path_parts ) != 2 or not all (path_parts ):
164- raise click .ClickException (f"GitHub URL must include owner/repo: { url } " )
165- owner , repo = path_parts [0 ], path_parts [1 ]
166- owner_id = _fetch_github_owner_id (owner )
167- workload : Workload = GitHubWorkload (
168- ef_project_id = ef_project_id ,
169- repo_owner = owner ,
170- repo_name = repo ,
171- repo_owner_id = owner_id ,
172- )
173- logger .info (
174- f"Prepared GitHubWorkload(ef_project_id={ ef_project_id !r} , "
175- f"repo_owner={ owner !r} , repo_name={ repo !r} , repo_owner_id={ owner_id } )"
176- )
177- elif base_url == JENKINS_ISSUER_BASE_URL :
178- workload = JenkinsWorkload (ef_project_id = ef_project_id , issuer = url )
179- logger .info (
180- f"Prepared JenkinsWorkload(ef_project_id={ ef_project_id !r} , issuer={ url !r} )"
181- )
182- else :
183- raise click .ClickException (
184- f"URL must be a { GITHUB_BASE_URL } repo URL or a "
185- f"{ JENKINS_ISSUER_BASE_URL } issuer URL: { url } "
186- )
187-
188- with _make_session () as session :
189- _create_ef_project_if_needed (session , ef_project_id )
190- session .add (workload )
191- if dry_run :
192- logger .info ("Dry-run: rolling back transaction" )
193- session .rollback ()
194- else :
195- session .commit ()
196- logger .info ("Committed workload" )
197-
198-
199- @cli .command ("add-dt-project" )
200- @click .argument ("ef_project_id" )
201- @click .argument ("dt_url" )
202- @click .argument ("parent_name" )
203- @click .argument ("project_name" )
204- @click .option (
205- "--dry-run" ,
206- is_flag = True ,
207- help = "Look up data and prepare the row, but do not commit." ,
208- )
209- def add_dt_project (
210- ef_project_id : str ,
211- dt_url : str ,
212- parent_name : str ,
213- project_name : str ,
214- dry_run : bool ,
215- ) -> None :
216- """Register a DependencyTrack project for an Eclipse Foundation project.
217-
218- Fetches the root project matching PARENT_NAME (asserting exactly one), then
219- finds PROJECT_NAME among its children (asserting exactly one), and stores
220- that child's UUID.
221- """
222- # Fail early, before the DependencyTrack lookups, rather than after them.
223- if not os .environ .get ("PIA_DATABASE_URL" ):
224- raise click .ClickException ("PIA_DATABASE_URL is not set" )
225-
226- api_key = _get_dt_api_key ()
227-
228- parent = _dt_find_root_project_by_name (dt_url , parent_name , api_key )
229- logger .info (f"Resolved parent { parent_name !r} -> uuid={ parent ['uuid' ]} " )
230-
231- children = [c for c in parent .get ("children" , []) if c .get ("name" ) == project_name ]
232- if len (children ) != 1 :
233- raise click .ClickException (
234- f"Expected exactly one child named { project_name !r} under { parent_name !r} , "
235- f"found { len (children )} "
236- )
237- child_uuid = children [0 ]["uuid" ]
238- logger .info (f"Resolved child { project_name !r} -> uuid={ child_uuid } " )
239-
240- dt_project = DependencyTrackProject (
241- ef_project_id = ef_project_id ,
242- name = project_name ,
243- parent_uuid = child_uuid ,
244- )
245- logger .info (
246- f"Prepared DependencyTrackProject(ef_project_id={ ef_project_id !r} , "
247- f"name={ project_name !r} , parent_uuid={ child_uuid } )"
248- )
249-
250- with _make_session () as session :
251- _create_ef_project_if_needed (session , ef_project_id )
252- session .add (dt_project )
253- if dry_run :
254- logger .info ("Dry-run: rolling back transaction" )
255- session .rollback ()
256- else :
257- session .commit ()
258- logger .info ("Committed DependencyTrack project" )
259-
260-
26152@cli .command ("sync" )
26253@click .argument ("file" , type = click .Path (exists = True , dir_okay = False ))
26354@click .option (
0 commit comments