From 7f9c5b18e09f703ba2fc4bbd41345a8eb1cad105 Mon Sep 17 00:00:00 2001 From: Ritesh Noronha Date: Sat, 18 Oct 2025 11:06:19 -0700 Subject: [PATCH] Add support for circleci --- README.md | 100 ++++++++++++++++++++++++++++++++++++++++ pylynk/utils/ci_info.py | 56 +++++++++++++++++++++- 2 files changed, 155 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 61902d7..6422a34 100644 --- a/README.md +++ b/README.md @@ -433,6 +433,106 @@ steps: # - Repository information ``` +### CircleCI Integration + +In CircleCI, PyLynk automatically detects and extracts workflow information: + +#### Using Docker Image (Recommended) +```yaml +version: 2.1 + +jobs: + upload-sbom: + docker: + - image: cimg/base:stable + steps: + - checkout + - setup_remote_docker + + - run: + name: Generate SBOM + command: | + # Your SBOM generation command here + # Example: syft . -o json=sbom.json + + - run: + name: Upload SBOM to Interlynk + command: | + docker run --rm \ + -e INTERLYNK_SECURITY_TOKEN=${INTERLYNK_TOKEN} \ + -e CIRCLECI=${CIRCLECI} \ + -e CIRCLE_TAG=${CIRCLE_TAG} \ + -e CIRCLE_PULL_REQUEST=${CIRCLE_PULL_REQUEST} \ + -e CIRCLE_BRANCH=${CIRCLE_BRANCH} \ + -e CIRCLE_USERNAME=${CIRCLE_USERNAME} \ + -e CIRCLE_BUILD_NUM=${CIRCLE_BUILD_NUM} \ + -e CIRCLE_SHA1=${CIRCLE_SHA1} \ + -e CIRCLE_BUILD_URL=${CIRCLE_BUILD_URL} \ + -e CIRCLE_WORKFLOW_ID=${CIRCLE_WORKFLOW_ID} \ + -e CIRCLE_JOB=${CIRCLE_JOB} \ + -e CIRCLE_PROJECT_REPONAME=${CIRCLE_PROJECT_REPONAME} \ + -e CIRCLE_PROJECT_USERNAME=${CIRCLE_PROJECT_USERNAME} \ + -e CIRCLE_REPOSITORY_URL=${CIRCLE_REPOSITORY_URL} \ + -v $(pwd):/app/data \ + ghcr.io/interlynk-io/pylynk \ + upload --prod 'my-product' --sbom /app/data/sbom.json + # PyLynk automatically captures: + # - Event type (pull_request, push, or release) + # - Release tag (for tag-triggered builds via CIRCLE_TAG) + # - PR number and URL (extracted from CIRCLE_PULL_REQUEST) + # - Source branch (CIRCLE_BRANCH) + # - Author (CIRCLE_USERNAME) + # - Build number and URL + # - Workflow ID and job name + # - Repository information + +workflows: + version: 2 + build_and_upload: + jobs: + - upload-sbom: + filters: + tags: + only: /^v.*/ +``` + +#### Using Python Directly +```yaml +version: 2.1 + +jobs: + upload-sbom: + docker: + - image: cimg/python:3.9 + steps: + - checkout + + - run: + name: Install dependencies + command: pip install -r requirements.txt + + - run: + name: Generate SBOM + command: | + # Your SBOM generation command here + + - run: + name: Upload SBOM to Interlynk + command: | + python3 pylynk.py upload --prod 'my-product' --sbom sbom.json + environment: + INTERLYNK_SECURITY_TOKEN: ${INTERLYNK_TOKEN} + +workflows: + version: 2 + build_and_upload: + jobs: + - upload-sbom: + filters: + tags: + only: /^v.*/ +``` + ### Generic CI Support 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: diff --git a/pylynk/utils/ci_info.py b/pylynk/utils/ci_info.py index 5455ca0..497b9ab 100644 --- a/pylynk/utils/ci_info.py +++ b/pylynk/utils/ci_info.py @@ -8,7 +8,7 @@ logger = logging.getLogger(__name__) class CIInfo: - """Extract CI/CD environment information for GitHub Actions, Bitbucket Pipelines, and Azure DevOps.""" + """Extract CI/CD environment information for GitHub Actions, Bitbucket Pipelines, Azure DevOps, and CircleCI.""" def __init__(self): self.ci_provider = self._detect_ci_provider() @@ -28,6 +28,8 @@ def _detect_ci_provider(self): return 'bitbucket_pipelines' if os.getenv('TF_BUILD', '').lower() in ['true', '1'] or os.getenv('SYSTEM_TEAMFOUNDATIONCOLLECTIONURI'): return 'azure_devops' + if os.getenv('CIRCLECI') == 'true': + return 'circleci' return None def _extract_event_info(self): @@ -184,6 +186,43 @@ def _extract_event_info(self): else: event_info['event_type'] = "unknown" + elif self.ci_provider == 'circleci': + # Event type detection for CircleCI + # Check for tag first (highest priority) + if os.getenv('CIRCLE_TAG'): + event_info['event_type'] = 'release' + event_info.update({ + 'release_tag': os.getenv('CIRCLE_TAG'), + 'author': os.getenv('CIRCLE_USERNAME') + }) + # Check if this is a PR event + elif os.getenv('CIRCLE_PULL_REQUEST'): + event_info['event_type'] = 'pull_request' + pr_url = os.getenv('CIRCLE_PULL_REQUEST') + # Extract PR number from URL (format: https://github.com/owner/repo/pull/123) + pr_number = None + if pr_url: + parts = pr_url.rstrip('/').split('/') + if len(parts) > 0 and parts[-2] == 'pull': + pr_number = parts[-1] + + event_info.update({ + 'number': pr_number, + 'url': pr_url, + 'source_branch': os.getenv('CIRCLE_BRANCH'), + # CircleCI doesn't provide target branch in env vars + 'author': os.getenv('CIRCLE_USERNAME') + }) + # Otherwise it's a push + elif os.getenv('CIRCLE_BRANCH'): + event_info['event_type'] = 'push' + event_info.update({ + 'source_branch': os.getenv('CIRCLE_BRANCH'), + 'author': os.getenv('CIRCLE_USERNAME') + }) + else: + event_info['event_type'] = 'unknown' + else: # Generic CI fallback - support common CI environment variables # Detect event type @@ -261,6 +300,15 @@ def _extract_build_info(self): 'commit_sha': os.getenv('BITBUCKET_COMMIT'), 'build_url': f"https://bitbucket.org/{os.getenv('BITBUCKET_WORKSPACE')}/{os.getenv('BITBUCKET_REPO_SLUG')}/pipelines/results/{os.getenv('BITBUCKET_BUILD_NUMBER')}" }) + elif self.ci_provider == 'circleci': + build_info.update({ + 'build_id': os.getenv('CIRCLE_BUILD_NUM'), + 'build_number': os.getenv('CIRCLE_BUILD_NUM'), + 'commit_sha': os.getenv('CIRCLE_SHA1'), + 'build_url': os.getenv('CIRCLE_BUILD_URL'), + 'workflow_id': os.getenv('CIRCLE_WORKFLOW_ID'), + 'job_name': os.getenv('CIRCLE_JOB') + }) # Fallbacks - check multiple common environment variable names build_info.setdefault('commit_sha', os.getenv('GIT_COMMIT') or @@ -298,6 +346,12 @@ def _extract_repository_info(self): 'owner': os.getenv('BITBUCKET_WORKSPACE'), 'url': f"https://bitbucket.org/{os.getenv('BITBUCKET_WORKSPACE')}/{os.getenv('BITBUCKET_REPO_SLUG')}" }) + elif self.ci_provider == 'circleci': + repo_info.update({ + 'name': os.getenv('CIRCLE_PROJECT_REPONAME'), + 'owner': os.getenv('CIRCLE_PROJECT_USERNAME'), + 'url': os.getenv('CIRCLE_REPOSITORY_URL') + }) # Fallbacks - check common repository environment variables repo_info.setdefault('url', os.getenv('REPO_URL') or os.getenv('REPOSITORY_URL')) repo_info.setdefault('name', os.getenv('REPO_NAME') or os.getenv('REPOSITORY_NAME'))