Skip to content

Commit b8792be

Browse files
Add support for AzureDevops (#68)
* Add support for AzureDevops * Update pylynk/utils/ci_info.py Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> Signed-off-by: Ritesh Noronha <riteshnoronha@users.noreply.github.com> * Update pylynk/utils/ci_info.py Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> Signed-off-by: Ritesh Noronha <riteshnoronha@users.noreply.github.com> * Update pylynk/utils/ci_info.py Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> Signed-off-by: Ritesh Noronha <riteshnoronha@users.noreply.github.com> --------- Signed-off-by: Ritesh Noronha <riteshnoronha@users.noreply.github.com> Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
1 parent 30df6b1 commit b8792be

File tree

2 files changed

+127
-3
lines changed

2 files changed

+127
-3
lines changed

README.md

Lines changed: 45 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -309,7 +309,7 @@ docker run -e INTERLYNK_SECURITY_TOKEN=$INTERLYNK_SECURITY_TOKEN -v $(pwd):/app/
309309

310310
## CI/CD Integration
311311

312-
PyLynk automatically detects and captures CI/CD environment information when running in GitHub Actions, Bitbucket Pipelines, or other CI environments. This metadata is sent with API requests during **upload operations only** to provide context about the build and deployment pipeline.
312+
PyLynk automatically detects and captures CI/CD environment information when running in GitHub Actions, Bitbucket Pipelines, Azure DevOps, or other CI environments. This metadata is sent with API requests during **upload operations only** to provide context about the build and deployment pipeline.
313313

314314
### Automatic PR and Build Information Extraction
315315

@@ -390,6 +390,49 @@ pipelines:
390390
# - Repository information
391391
```
392392

393+
### Azure DevOps Integration
394+
395+
In Azure DevOps Pipelines, PyLynk automatically detects and extracts pipeline information:
396+
397+
```yaml
398+
trigger:
399+
branches:
400+
include:
401+
- main
402+
- develop
403+
tags:
404+
include:
405+
- v*
406+
407+
pr:
408+
branches:
409+
include:
410+
- main
411+
412+
steps:
413+
- script: |
414+
pip install -r requirements.txt
415+
displayName: 'Install dependencies'
416+
417+
- script: |
418+
# Generate SBOM here
419+
displayName: 'Generate SBOM'
420+
421+
- script: |
422+
python3 pylynk.py upload --prod 'my-product' --sbom sbom.json
423+
displayName: 'Upload SBOM to Interlynk'
424+
env:
425+
INTERLYNK_SECURITY_TOKEN: $(INTERLYNK_TOKEN)
426+
# PyLynk automatically captures:
427+
# - Event type (pull_request, push, or release)
428+
# - Release tag (for tag-triggered builds)
429+
# - PR ID and URL (for PR events)
430+
# - PR source/target branches
431+
# - PR author (BUILD_REQUESTEDFOR)
432+
# - Build ID, number, and URL
433+
# - Repository information
434+
```
435+
393436
### Generic CI Support
394437

395438
PyLynk also supports generic CI environments by checking common environment variables. When `CI=true` is set, PyLynk will attempt to extract build and PR information from standard environment variables:
@@ -505,7 +548,7 @@ The extracted CI information is sent as HTTP headers with upload API requests:
505548
506549
| Header | Description | Example |
507550
|--------|-------------|---------|
508-
| `X-CI-Provider` | CI platform name | `github_actions`, `bitbucket_pipelines`, `generic_ci` |
551+
| `X-CI-Provider` | CI platform name | `github_actions`, `bitbucket_pipelines`, `azure_devops`, `generic_ci` |
509552
| `X-Event-Type` | CI event type | `pull_request`, `push`, `release` |
510553
| `X-Release-Tag` | Release tag name (when event is release) | `v1.2.3` |
511554
| `X-PR-Number` | Pull request number | `123` |

pylynk/utils/ci_info.py

Lines changed: 82 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
logger = logging.getLogger(__name__)
88

99
class CIInfo:
10-
"""Extract CI/CD environment information for GitHub Actions and Bitbucket Pipelines."""
10+
"""Extract CI/CD environment information for GitHub Actions, Bitbucket Pipelines, and Azure DevOps."""
1111

1212
def __init__(self):
1313
self.ci_provider = self._detect_ci_provider()
@@ -23,6 +23,8 @@ def _detect_ci_provider(self):
2323
return 'github_actions'
2424
if os.getenv('BITBUCKET_BUILD_NUMBER'):
2525
return 'bitbucket_pipelines'
26+
if os.getenv('TF_BUILD', '').lower() in ['true', '1'] or os.getenv('SYSTEM_TEAMFOUNDATIONCOLLECTIONURI'):
27+
return 'azure_devops'
2628
if os.getenv('CI'):
2729
return 'generic_ci'
2830
return None
@@ -103,6 +105,55 @@ def _extract_event_info(self):
103105
'author': release_data.get('author', {}).get('login')
104106
})
105107

108+
elif self.ci_provider == 'azure_devops':
109+
# Event type detection for Azure DevOps
110+
reason = os.getenv('BUILD_REASON', '')
111+
logger.debug(f"Azure DevOps build reason: {reason}")
112+
113+
if reason == 'PullRequest':
114+
event_info['event_type'] = 'pull_request'
115+
# Extract PR information from environment variables
116+
pr_id = os.getenv('SYSTEM_PULLREQUEST_PULLREQUESTID')
117+
source_branch = os.getenv('SYSTEM_PULLREQUEST_SOURCEBRANCH', '').replace('refs/heads/', '')
118+
target_branch = os.getenv('SYSTEM_PULLREQUEST_TARGETBRANCH', '').replace('refs/heads/', '')
119+
120+
# Build PR URL
121+
org_url = os.getenv('SYSTEM_TEAMFOUNDATIONCOLLECTIONURI', '')
122+
project = os.getenv('SYSTEM_TEAMPROJECT', '')
123+
repo = os.getenv('BUILD_REPOSITORY_NAME', '')
124+
125+
pr_url = None
126+
if org_url and project and repo and pr_id:
127+
# Remove trailing slash from org_url if present
128+
org_url = org_url.rstrip('/')
129+
pr_url = f"{org_url}/{project}/_git/{urllib.parse.quote(repo)}/pullrequest/{pr_id}"
130+
131+
event_info.update({
132+
'number': pr_id,
133+
'url': pr_url,
134+
'source_branch': source_branch,
135+
'target_branch': target_branch,
136+
'author': os.getenv('BUILD_REQUESTEDFOR')
137+
})
138+
elif reason in ['IndividualCI', 'BatchedCI', 'Manual', 'Schedule']:
139+
# Check if this is a tag/release
140+
source_branch = os.getenv('BUILD_SOURCEBRANCH', '')
141+
if source_branch.startswith('refs/tags/'):
142+
event_info['event_type'] = 'release'
143+
event_info.update({
144+
'release_tag': source_branch.replace('refs/tags/', ''),
145+
'author': os.getenv('BUILD_REQUESTEDFOR')
146+
})
147+
else:
148+
event_info['event_type'] = 'push'
149+
event_info.update({
150+
'source_branch': source_branch.replace('refs/heads/', ''),
151+
'author': os.getenv('BUILD_REQUESTEDFOR')
152+
})
153+
else:
154+
event_info['event_type'] = 'unknown'
155+
event_info['author'] = os.getenv('BUILD_REQUESTEDFOR')
156+
106157
elif self.ci_provider == 'bitbucket_pipelines':
107158
# Event type detection
108159
# Check for tag first (highest priority)
@@ -154,6 +205,23 @@ def _extract_build_info(self):
154205
'commit_sha': os.getenv('GITHUB_SHA'),
155206
'build_url': f"{os.getenv('GITHUB_SERVER_URL', 'https://github.com')}/{os.getenv('GITHUB_REPOSITORY')}/actions/runs/{os.getenv('GITHUB_RUN_ID')}"
156207
})
208+
elif self.ci_provider == 'azure_devops':
209+
org_url = os.getenv('SYSTEM_TEAMFOUNDATIONCOLLECTIONURI', '')
210+
project = os.getenv('SYSTEM_TEAMPROJECT', '')
211+
build_id = os.getenv('BUILD_BUILDID')
212+
213+
build_url = None
214+
if org_url and project and build_id:
215+
# Remove trailing slash from org_url if present
216+
org_url = org_url.rstrip('/')
217+
build_url = f"{org_url}/{urllib.parse.quote(project)}/_build/results?buildId={build_id}"
218+
219+
build_info.update({
220+
'build_id': build_id,
221+
'build_number': os.getenv('BUILD_BUILDNUMBER'),
222+
'commit_sha': os.getenv('BUILD_SOURCEVERSION'),
223+
'build_url': build_url
224+
})
157225
elif self.ci_provider == 'bitbucket_pipelines':
158226
build_info.update({
159227
'build_number': os.getenv('BITBUCKET_BUILD_NUMBER'),
@@ -173,6 +241,19 @@ def _extract_repository_info(self):
173241
if '/' in repository:
174242
repo_info['owner'], repo_info['name'] = repository.split('/', 1)
175243
repo_info['url'] = f"{os.getenv('GITHUB_SERVER_URL', 'https://github.com')}/{repository}" if repository else None
244+
elif self.ci_provider == 'azure_devops':
245+
# Azure DevOps repository information
246+
repo_uri = os.getenv('BUILD_REPOSITORY_URI')
247+
repo_name = os.getenv('BUILD_REPOSITORY_NAME')
248+
249+
# Try to extract owner from the repository URI or project
250+
owner = os.getenv('SYSTEM_TEAMPROJECT')
251+
252+
repo_info.update({
253+
'name': repo_name,
254+
'owner': owner,
255+
'url': repo_uri
256+
})
176257
elif self.ci_provider == 'bitbucket_pipelines':
177258
repo_info.update({
178259
'name': os.getenv('BITBUCKET_REPO_SLUG'),

0 commit comments

Comments
 (0)