From 6058ab0b7f62f223f64b7493e3eee6123be445a1 Mon Sep 17 00:00:00 2001 From: Aditi Agarwal Date: Tue, 14 Apr 2026 20:51:06 +0530 Subject: [PATCH 1/9] Update bitbucket pipelines --- bitbucket-pipelines.yml | 505 +++++++++++++++++++++++++++------------- 1 file changed, 343 insertions(+), 162 deletions(-) diff --git a/bitbucket-pipelines.yml b/bitbucket-pipelines.yml index e4557f3..21013bc 100644 --- a/bitbucket-pipelines.yml +++ b/bitbucket-pipelines.yml @@ -1,181 +1,307 @@ +# ============================================================================= +# Bitbucket Pipelines — CI/CD Configuration +# ============================================================================= +# +# Promotion flow (the path code takes from dev to production): +# +# feature/* ──┐ +# bugfix/* ──┼──→ dev ──→ stg ──→ main ──→ [release tag] ──→ production +# hotfix/* ──┼──→ main (fast-track for urgent fixes) +# └──→ dev (to keep dev in sync with the hotfix) +# +# Three pipeline triggers: +# default → Runs on every push/commit when NO PR is open +# pull-requests → Runs when a PR is opened or updated +# custom → Runs manually from the Bitbucket UI (release / rollback) +# +# Branch name policy: +# Only feature/*, bugfix/*, hotfix/*, dev, stg have PR pipelines defined. +# Any other branch name simply gets NO PR pipeline → checks won't pass +# → PR cannot be merged. No catch-all '**' pattern (that caused double runs). +# ============================================================================= + + +# ============================================================================= +# DEFINITIONS — Reusable building blocks +# ============================================================================= definitions: + + # --------------------------------------------------------------------------- + # Custom cache: uv stores packages in ~/.cache/uv, NOT ~/.cache/pip. + # Without this, every pipeline run re-downloads all dependencies. + # --------------------------------------------------------------------------- + caches: + uv: ~/.cache/uv + + # --------------------------------------------------------------------------- + # Reusable script fragments (YAML anchors — defined with & , used with * ) + # These are single strings that get injected into a step's script list. + # --------------------------------------------------------------------------- scripts: + + # Installs the uv package manager (pinned to avoid surprise breaks), + # then syncs both dev and test dependency groups from pyproject.toml - script: &install - pip install uv; + pip install uv==0.11.5; uv sync --group dev --group test; - - script: &resolve-branch - source resolve_source_branch.sh; - echo "PR source branch:$SOURCE_BRANCH"; - echo "PR destination branch:${BITBUCKET_PR_DESTINATION_BRANCH}"; + + # --------------------------------------------------------------------------- + # Reusable step definitions + # Each step is a self-contained CI job with its own Docker container. + # --------------------------------------------------------------------------- steps: - - step: &install-deps - name: Install Dependencies - image: python:3.12.7 - script: - - *install - caches: - - pip + + # -- Lint: checks that all Python files are formatted with Black ---------- + # Fails if any file would be reformatted (--check = read-only mode) - step: &lint name: Lint (black) image: python:3.12.7 script: - - *install - - uv run black --check src/ + - *install # install uv + sync dependencies + - uv run black --check src/ # check formatting (no changes made) caches: - - pip + - uv # cache uv packages between runs + + # -- Type check: static analysis with mypy -------------------------------- + # Catches type errors at CI time instead of at runtime - step: &typecheck name: Type Check (mypy) image: python:3.12.7 script: - - *install - - uv run mypy src/ + - *install # install uv + sync dependencies + - uv run mypy src/ # run type checker on source code caches: - - pip + - uv + + # -- Unit tests: runs the test suite with pytest -------------------------- - step: &unit-tests name: Unit Tests (pytest) image: python:3.12.7 script: - - *install - - uv run pytest src/tests/ + - *install # install uv + sync dependencies + - uv run pytest src/tests/ # run all tests under src/tests/ caches: - - pip + - uv + + # -- AI Code Review: automated review via DoczyAI ------------------------- + # Uses AWS OIDC auth (no stored secrets) to call the review agent. + # The agent clones itself from a separate repo, analyzes the PR diff, + # and posts review comments directly on the pull request. - step: &ai-code-review name: AI Code Review image: python:3.12-slim - oidc: true + oidc: true # enables OIDC token injection script: + # Slim image doesn't include git — install it + AWS/HTTP libs - apt-get update && apt-get install -y git - pip install boto3 requests + + # --- AWS OIDC authentication setup --- + # How it works: + # 1. Bitbucket generates a short-lived OIDC token for this step + # 2. We write that token to a file + # 3. AWS SDK reads the file + role ARN to assume the IAM role + # 4. No long-lived AWS keys needed — token expires after the step - export AWS_REGION=us-east-1 - - export AWS_ROLE_ARN=arn:aws:iam::975049960860:role/DoczyAI-Bitbucket-OIDC - - export AWS_WEB_IDENTITY_TOKEN_FILE=$(pwd)/web-identity-token - - echo $BITBUCKET_STEP_OIDC_TOKEN > $(pwd)/web-identity-token - - git clone https://x-token-auth:${BITBUCKET_CLONE_TOKEN}@bitbucket.org/${BITBUCKET_WORKSPACE}/code-review-agent.git + - export AWS_ROLE_ARN="arn:aws:iam::975049960860:role/DoczyAI-Bitbucket-OIDC" + - export AWS_WEB_IDENTITY_TOKEN_FILE="$(pwd)/web-identity-token" + - echo "$BITBUCKET_STEP_OIDC_TOKEN" > "$(pwd)/web-identity-token" + # ↑ Quoted to prevent word-splitting if token has special chars + + # Clone the review agent repo (private — needs BITBUCKET_CLONE_TOKEN) + # -q flag suppresses URL output to avoid leaking the token in logs + - git clone -q "https://x-token-auth:${BITBUCKET_CLONE_TOKEN}@bitbucket.org/${BITBUCKET_WORKSPACE}/code-review-agent.git" + + # Build the PR URL and run the agent - export PR_URL="https://bitbucket.org/${BITBUCKET_WORKSPACE}/${BITBUCKET_REPO_SLUG}/pull-requests/${BITBUCKET_PR_ID}" - cd code-review-agent && python main.py "$PR_URL" + +# ============================================================================= +# PIPELINES +# ============================================================================= pipelines: + + # =========================================================================== + # DEFAULT — runs on every push when there is NO open PR for the branch + # =========================================================================== + # Basic safety net. When a developer pushes commits to their branch + # before opening a PR, these checks run to catch issues early. + # No gate checks — this is just "did you break anything?" default: - - step: *install-deps - - parallel: + - parallel: # all three run at the same time - step: *lint - step: *typecheck - step: *unit-tests + # =========================================================================== + # PULL REQUESTS — runs when a PR is opened or updated + # =========================================================================== + # Each entry matches a SOURCE branch pattern (the branch the PR comes + # FROM, not the branch it targets). The gate step inside validates + # the DESTINATION branch using ALLOWED_TARGETS. + # + # Flow for every PR: + # Step 1: Gate → is this PR targeting the correct branch? + # Step 2: Quality → lint + typecheck + tests (in parallel) + # Step 3: AI review → automated code review comments on the PR + # + # If the gate fails (step 1), steps 2 and 3 never run. + # =========================================================================== pull-requests: + + # ------------------------------------------------------------------------- + # feature/* → dev only + # ------------------------------------------------------------------------- + # Feature branches represent new functionality. They must go through + # dev first for integration testing before promotion to stg/main. "feature/*": - - step: *install-deps - - parallel: - - step: *lint - - step: *typecheck - - step: *unit-tests - step: - name: "Gate: feature PR must target dev only" - script: - - *resolve-branch - - | - if [ "${BITBUCKET_PR_DESTINATION_BRANCH}" != "dev" ]; then - echo "Blocked: feature PR must target dev only" - exit 1 - fi - - step: *ai-code-review - dev: - - step: *install-deps - - parallel: - - step: *lint - - step: *typecheck - - step: *unit-tests - - step: - name: "Gate: only allow PR from dev -> stg" - script: - - *resolve-branch - - | - if [ "${BITBUCKET_PR_DESTINATION_BRANCH}" != "stg" ]; then - echo "Blocked: dev PR must target stg only for this pipeline." - exit 1 - fi - - step: *ai-code-review - stg: - - step: *install-deps - - parallel: - - step: *lint - - step: *typecheck - - step: *unit-tests - - step: - name: "Gate: only allow PR from stg -> main" - script: - - *resolve-branch - - | - if [ "${BITBUCKET_PR_DESTINATION_BRANCH}" != "main" ]; then - echo "Blocked: stg PR must target main only." - exit 1 - fi - - step: *ai-code-review - -# Hotfix promotion gate (hotfix/* -> main OR dev only) - "hotfix/*": - - step: *install-deps - - parallel: - - step: *lint - - step: *typecheck - - step: *unit-tests - - step: - name: "Gate: allow hotfix PR only to main or dev" - script: - - *resolve-branch - - | - if [ "${BITBUCKET_PR_DESTINATION_BRANCH}" != "main" ] && \ - [ "${BITBUCKET_PR_DESTINATION_BRANCH}" != "dev" ]; then - echo "Blocked: hotfix PR must target main or dev only." - exit 1 - fi - - step: *ai-code-review -# bugfix promotion gate (bugfix/* -> dev only) - "bugfix/*": - - step: *install-deps - - parallel: - - step: *lint - - step: *typecheck - - step: *unit-tests - - step: - name: "Gate: bugfix PR must target dev only" - script: - - *resolve-branch - - | - if [ "${BITBUCKET_PR_DESTINATION_BRANCH}" != "dev" ]; then - echo "Blocked: bugfix PR must target dev only" - exit 1 - fi - - step: *ai-code-review - -#PR pipeline check for branch name validation - '**': - - step: *install-deps - - parallel: - - step: *lint - - step: *typecheck - - step: *unit-tests - - step: - name: Validate branch name + name: "Gate: feature → dev" image: atlassian/default-image:4 script: - | - echo "Validating PR source branch name..." - - *resolve-branch - - | - if [[ ! "$SOURCE_BRANCH" =~ ^(feature|bugfix|hotfix)/.+$ ]]; then - echo " ERROR - Invalid branch name" - echo "OK - Allowed formats:" - echo " - feature/" - echo " - bugfix/" - echo " - hotfix/" - exit 1 - fi - - echo "OK - Branch name is valid" + # Gate: feature branches can only target dev + ALLOWED="dev" + echo "Source: ${BITBUCKET_PR_SOURCE_BRANCH}" + echo "Target: ${BITBUCKET_PR_DESTINATION_BRANCH} (allowed: ${ALLOWED})" + IFS=',' read -ra targets <<< "$ALLOWED" + for t in "${targets[@]}"; do + [ "${BITBUCKET_PR_DESTINATION_BRANCH}" = "$t" ] && exit 0 + done + echo "BLOCKED: target must be one of: ${ALLOWED}"; exit 1 + - parallel: + - step: *lint + - step: *typecheck + - step: *unit-tests - step: *ai-code-review + # ------------------------------------------------------------------------- + # bugfix/* → dev only + # ------------------------------------------------------------------------- + # Bug fixes follow the same path as features — must land in dev first. + "bugfix/*": + - step: + name: "Gate: bugfix → dev" + image: atlassian/default-image:4 + script: + - | + # Gate: bugfix branches can only target dev + ALLOWED="dev" + echo "Source: ${BITBUCKET_PR_SOURCE_BRANCH}" + echo "Target: ${BITBUCKET_PR_DESTINATION_BRANCH} (allowed: ${ALLOWED})" + IFS=',' read -ra targets <<< "$ALLOWED" + for t in "${targets[@]}"; do + [ "${BITBUCKET_PR_DESTINATION_BRANCH}" = "$t" ] && exit 0 + done + echo "BLOCKED: target must be one of: ${ALLOWED}"; exit 1 + - parallel: + - step: *lint + - step: *typecheck + - step: *unit-tests + - step: *ai-code-review + + # ------------------------------------------------------------------------- + # hotfix/* → main OR dev + # ------------------------------------------------------------------------- + # Hotfixes are urgent production fixes. They can go directly to main + # (fast-track) or to dev (to keep the dev branch in sync). + "hotfix/*": + - step: + name: "Gate: hotfix → main or dev" + image: atlassian/default-image:4 + script: + - | + # Gate: hotfix branches can target main (fast-track) or dev + ALLOWED="main,dev" + echo "Source: ${BITBUCKET_PR_SOURCE_BRANCH}" + echo "Target: ${BITBUCKET_PR_DESTINATION_BRANCH} (allowed: ${ALLOWED})" + IFS=',' read -ra targets <<< "$ALLOWED" + for t in "${targets[@]}"; do + [ "${BITBUCKET_PR_DESTINATION_BRANCH}" = "$t" ] && exit 0 + done + echo "BLOCKED: target must be one of: ${ALLOWED}"; exit 1 + - parallel: + - step: *lint + - step: *typecheck + - step: *unit-tests + - step: *ai-code-review + + # ------------------------------------------------------------------------- + # dev → stg only + # ------------------------------------------------------------------------- + # Once features/fixes are integrated in dev, a PR from dev to stg + # promotes the code to staging for final validation. + dev: + - step: + name: "Gate: dev → stg" + image: atlassian/default-image:4 + script: + - | + # Gate: dev can only be promoted to stg + ALLOWED="stg" + echo "Source: ${BITBUCKET_PR_SOURCE_BRANCH}" + echo "Target: ${BITBUCKET_PR_DESTINATION_BRANCH} (allowed: ${ALLOWED})" + IFS=',' read -ra targets <<< "$ALLOWED" + for t in "${targets[@]}"; do + [ "${BITBUCKET_PR_DESTINATION_BRANCH}" = "$t" ] && exit 0 + done + echo "BLOCKED: target must be one of: ${ALLOWED}"; exit 1 + - parallel: + - step: *lint + - step: *typecheck + - step: *unit-tests + - step: *ai-code-review + + # ------------------------------------------------------------------------- + # stg → main only + # ------------------------------------------------------------------------- + # Final promotion: staging to main. After this merges, the code is + # ready for a production release via the custom release-prod pipeline. + stg: + - step: + name: "Gate: stg → main" + image: atlassian/default-image:4 + script: + - | + # Gate: stg can only be promoted to main + ALLOWED="main" + echo "Source: ${BITBUCKET_PR_SOURCE_BRANCH}" + echo "Target: ${BITBUCKET_PR_DESTINATION_BRANCH} (allowed: ${ALLOWED})" + IFS=',' read -ra targets <<< "$ALLOWED" + for t in "${targets[@]}"; do + [ "${BITBUCKET_PR_DESTINATION_BRANCH}" = "$t" ] && exit 0 + done + echo "BLOCKED: target must be one of: ${ALLOWED}"; exit 1 + - parallel: + - step: *lint + - step: *typecheck + - step: *unit-tests + - step: *ai-code-review + + + # =========================================================================== + # CUSTOM — manually triggered from the Bitbucket UI + # =========================================================================== + # These don't run automatically. Go to: + # Pipelines → Run pipeline → select the pipeline → fill in variables custom: + + # ------------------------------------------------------------------------- + # release-prod: Create a version tag and deploy to production + # ------------------------------------------------------------------------- + # Steps: + # 1. Verify we're on main (releases only come from main) + # 2. Find the latest existing tag (e.g., v1.2.0) + # 3. Bump the version based on RELEASE_TYPE (major / minor / patch) + # 4. Create + push the new git tag + # 5. Deploy using the DEPLOY_COMMAND repository variable + # + # Version bump examples from v1.2.3: + # major → v2.0.0 (breaking changes) + # minor → v1.3.0 (new features, backwards compatible) + # patch → v1.2.4 (bug fixes only) + # ------------------------------------------------------------------------- release-prod: - variables: - name: RELEASE_TYPE @@ -183,85 +309,140 @@ pipelines: allowed-values: - major - minor + - patch # supports hotfix releases (v1.2.1) + + # Step 1: Create the release tag - step: name: "Create release tag" + image: atlassian/default-image:4 script: - - source resolve_source_branch.sh - - echo "PR source branch:$SOURCE_BRANCH" + # Verify we're running from main — only main can be released + - | + SOURCE_BRANCH="${BITBUCKET_PR_SOURCE_BRANCH:-$BITBUCKET_BRANCH}" if [ "$SOURCE_BRANCH" != "main" ]; then - echo "Blocked- releases can only be created from main." + echo "BLOCKED: releases can only be created from main." exit 1 fi + + # Configure git identity for the tag commit - git config user.email "bitbucket-pipelines@local" - git config user.name "Bitbucket Pipelines" + + # Fetch all existing tags to find the latest version - git fetch --tags --force - - latest_tag=$(git tag -l "v*" | sort -V | tail -n1) - - if [ -z "$latest_tag" ]; then latest_tag="v0.0.0"; fi - - version=$(echo "$latest_tag" | sed 's/^v//') - - major=$(echo "$version" | cut -d. -f1) - - minor=$(echo "$version" | cut -d. -f2) + + # Find the latest tag matching v* (e.g., v1.2.0) + # sort -V = version-aware sorting so v1.9 < v1.10 + # If no tags exist yet, start from v0.0.0 + - | + latest_tag=$(git tag -l "v*" | sort -V | tail -n1) + if [ -z "$latest_tag" ]; then latest_tag="v0.0.0"; fi + echo "Latest tag: ${latest_tag}" + + # Parse the tag into its three version components + - | + version=$(echo "$latest_tag" | sed 's/^v//') + major=$(echo "$version" | cut -d. -f1) + minor=$(echo "$version" | cut -d. -f2) + patch=$(echo "$version" | cut -d. -f3) + + # Bump the correct component, reset everything below it - | if [ "$RELEASE_TYPE" = "major" ]; then - major=$((major + 1)) - minor=0 - patch=0 + major=$((major + 1)); minor=0; patch=0 + elif [ "$RELEASE_TYPE" = "minor" ]; then + minor=$((minor + 1)); patch=0 else - minor=$((minor + 1)) - patch=0 + patch=$((patch + 1)) fi - - new_tag="v${major}.${minor}.${patch}" - - echo "Creating tag:${new_tag}" - - git tag -a "$new_tag" -m "Release ${new_tag}" - - git push origin "$new_tag" + + # Create the annotated tag and push it to the remote + - | + new_tag="v${major}.${minor}.${patch}" + echo "Creating tag: ${new_tag}" + git tag -a "$new_tag" -m "Release ${new_tag}" + git push origin "$new_tag" + + # Save the tag name as an artifact for the deploy step - echo "$new_tag" > release_tag.txt artifacts: - - release_tag.txt + - release_tag.txt # passed to the deploy step below + + # Step 2: Deploy to production - step: - name: "Deploy release to production" - deployment: production + name: "Deploy to production" + deployment: production # Bitbucket deployment environment + image: atlassian/default-image:4 script: + # Read the tag that was created in step 1 - export RELEASE_TAG=$(cat release_tag.txt) - echo "Deploying ${RELEASE_TAG}" + + # DEPLOY_COMMAND is a repository variable configured in + # Bitbucket → Repository settings → Repository variables. + # It can reference $RELEASE_TAG. Example: + # kubectl set image deployment/app container=myrepo/app:$RELEASE_TAG - | if [ -z "${DEPLOY_COMMAND}" ]; then - echo "Blocked: set repository variable DEPLOY_COMMAND (use \\$RELEASE_TAG inside command)." + echo "BLOCKED: set repository variable DEPLOY_COMMAND." + echo "Use \$RELEASE_TAG inside the command to reference the version." exit 1 fi - sh -c "$DEPLOY_COMMAND" + # ------------------------------------------------------------------------- + # rollback-prod: Roll back production to a previous release tag + # ------------------------------------------------------------------------- + # Use when a release goes wrong and you need to revert quickly. + # Provide the tag to roll back to (e.g., v1.1.0) and it redeploys + # that version using the same DEPLOY_COMMAND. + # ------------------------------------------------------------------------- rollback-prod: - variables: - name: ROLLBACK_TAG - default: v0.0.0 + default: v0.0.0 # user must change this to a real tag + + # Step 1: Validate the tag exists - step: - name: "Rollback: validate release tag" + name: "Validate rollback tag" + image: atlassian/default-image:4 script: - - source resolve_source_branch.sh - - echo "PR source branch:$SOURCE_BRANCH" + # Verify we're running from main + - | + SOURCE_BRANCH="${BITBUCKET_PR_SOURCE_BRANCH:-$BITBUCKET_BRANCH}" if [ "$SOURCE_BRANCH" != "main" ]; then - echo "Blocked- rollback can only run from main." + echo "BLOCKED: rollback can only run from main." exit 1 fi + + # Make sure a tag was actually provided - | if [ -z "${ROLLBACK_TAG}" ]; then - echo "Blocked: ROLLBACK_TAG is required." + echo "BLOCKED: ROLLBACK_TAG is required." exit 1 fi + + # Verify the tag exists in the remote - git fetch --tags --force - | if ! git rev-parse "${ROLLBACK_TAG}" >/dev/null 2>&1; then - echo "Blocked: tag ${ROLLBACK_TAG} does not exist." + echo "BLOCKED: tag ${ROLLBACK_TAG} does not exist." exit 1 fi + - echo "Tag ${ROLLBACK_TAG} found — proceeding with rollback." + + # Step 2: Redeploy using the rollback tag - step: name: "Rollback deploy to production" deployment: production + image: atlassian/default-image:4 script: - export RELEASE_TAG="${ROLLBACK_TAG}" - echo "Rolling back to ${RELEASE_TAG}" - | if [ -z "${DEPLOY_COMMAND}" ]; then - echo "Blocked: set repository variable DEPLOY_COMMAND (use \\$RELEASE_TAG inside command)." + echo "BLOCKED: set repository variable DEPLOY_COMMAND." + echo "Use \$RELEASE_TAG inside the command to reference the version." exit 1 fi - sh -c "$DEPLOY_COMMAND" From f3a36eea1e927207a1773311bbd7e33f8e1b5851 Mon Sep 17 00:00:00 2001 From: Siddhant Medar Date: Tue, 14 Apr 2026 16:24:40 -0500 Subject: [PATCH 2/9] fix: harden bitbucket-pipelines.yml with security and correctness improvements --- bitbucket-pipelines.yml | 61 +++++++++++++++++++++++++++++++---------- 1 file changed, 46 insertions(+), 15 deletions(-) diff --git a/bitbucket-pipelines.yml b/bitbucket-pipelines.yml index 21013bc..53d3c41 100644 --- a/bitbucket-pipelines.yml +++ b/bitbucket-pipelines.yml @@ -18,6 +18,12 @@ # Only feature/*, bugfix/*, hotfix/*, dev, stg have PR pipelines defined. # Any other branch name simply gets NO PR pipeline → checks won't pass # → PR cannot be merged. No catch-all '**' pattern (that caused double runs). +# +# SETUP REQUIRED: enforce these gates by enabling "Require a successful +# build to merge" for dev, stg, and main in: +# Bitbucket → Repository settings → Branch permissions +# Without this, the gates are advisory only — PRs can be merged regardless +# of whether the gate, lint, tests, or AI review pass or fail. # ============================================================================= @@ -43,7 +49,7 @@ definitions: # then syncs both dev and test dependency groups from pyproject.toml - script: &install pip install uv==0.11.5; - uv sync --group dev --group test; + uv sync --frozen --group dev --group test; # --------------------------------------------------------------------------- # Reusable step definitions @@ -89,12 +95,17 @@ definitions: # and posts review comments directly on the pull request. - step: &ai-code-review name: AI Code Review - image: python:3.12-slim + image: python:3.12.7-slim # slim is fine here — no uv sync + # SETUP REQUIRED: configure OIDC provider in + # Bitbucket → Workspace settings → Security → OpenID Connect + # and add the matching trust policy to the IAM role below. + # Without it, the step fails at startup before any script runs. oidc: true # enables OIDC token injection script: # Slim image doesn't include git — install it + AWS/HTTP libs - - apt-get update && apt-get install -y git - - pip install boto3 requests + # --no-install-recommends skips git-man/less/openssh-client etc. + - apt-get update && apt-get install -y --no-install-recommends git + - pip install "boto3==1.35.*" "requests==2.32.*" # --- AWS OIDC authentication setup --- # How it works: @@ -105,16 +116,30 @@ definitions: - export AWS_REGION=us-east-1 - export AWS_ROLE_ARN="arn:aws:iam::975049960860:role/DoczyAI-Bitbucket-OIDC" - export AWS_WEB_IDENTITY_TOKEN_FILE="$(pwd)/web-identity-token" - - echo "$BITBUCKET_STEP_OIDC_TOKEN" > "$(pwd)/web-identity-token" - # ↑ Quoted to prevent word-splitting if token has special chars + - printf '%s' "$BITBUCKET_STEP_OIDC_TOKEN" > "$(pwd)/web-identity-token" + # printf (not echo) avoids appending a trailing newline to the JWT # Clone the review agent repo (private — needs BITBUCKET_CLONE_TOKEN) + # SETUP REQUIRED: set BITBUCKET_CLONE_TOKEN (marked "Secured") in + # Bitbucket → Repository settings → Repository variables + # Guard against missing token so failure is self-diagnosing + - | + if [ -z "${BITBUCKET_CLONE_TOKEN}" ]; then + echo "BLOCKED: BITBUCKET_CLONE_TOKEN not set." + echo "Set it in Repository settings → Repository variables (Secured)." + exit 1 + fi # -q flag suppresses URL output to avoid leaking the token in logs - git clone -q "https://x-token-auth:${BITBUCKET_CLONE_TOKEN}@bitbucket.org/${BITBUCKET_WORKSPACE}/code-review-agent.git" - # Build the PR URL and run the agent + # Build the PR URL and run the agent. + # The || echo below only covers the python main.py call: + # agent crashes, AWS STS errors, and runtime exceptions become + # warnings instead of hard failures. A failure in git clone or + # apt-get above is still a hard failure (rare in practice). + # AI review is advisory, not a gate. - export PR_URL="https://bitbucket.org/${BITBUCKET_WORKSPACE}/${BITBUCKET_REPO_SLUG}/pull-requests/${BITBUCKET_PR_ID}" - - cd code-review-agent && python main.py "$PR_URL" + - cd code-review-agent && python main.py "$PR_URL" || echo "WARNING: AI review step failed (non-blocking)" # ============================================================================= @@ -139,7 +164,7 @@ pipelines: # =========================================================================== # Each entry matches a SOURCE branch pattern (the branch the PR comes # FROM, not the branch it targets). The gate step inside validates - # the DESTINATION branch using ALLOWED_TARGETS. + # the DESTINATION branch. # # Flow for every PR: # Step 1: Gate → is this PR targeting the correct branch? @@ -356,7 +381,10 @@ pipelines: patch=$((patch + 1)) fi - # Create the annotated tag and push it to the remote + # Create the annotated tag and push it to the remote. + # SETUP REQUIRED: enable "Repository write access" in + # Bitbucket → Repository settings → Pipelines + # Without it, git push fails with 403 and releases don't work. - | new_tag="v${major}.${minor}.${patch}" echo "Creating tag: ${new_tag}" @@ -375,7 +403,7 @@ pipelines: image: atlassian/default-image:4 script: # Read the tag that was created in step 1 - - export RELEASE_TAG=$(cat release_tag.txt) + - export RELEASE_TAG="$(cat release_tag.txt)" - echo "Deploying ${RELEASE_TAG}" # DEPLOY_COMMAND is a repository variable configured in @@ -400,7 +428,7 @@ pipelines: rollback-prod: - variables: - name: ROLLBACK_TAG - default: v0.0.0 # user must change this to a real tag + default: "" # user MUST provide a real tag (e.g. v1.2.0) # Step 1: Validate the tag exists - step: @@ -422,11 +450,14 @@ pipelines: exit 1 fi - # Verify the tag exists in the remote + # Verify the ref exists AND is specifically a tag (not a branch, + # HEAD, or commit hash). git rev-parse would accept any ref; + # `git tag -l` + exact-match grep ensures we only accept tags. - git fetch --tags --force - | - if ! git rev-parse "${ROLLBACK_TAG}" >/dev/null 2>&1; then - echo "BLOCKED: tag ${ROLLBACK_TAG} does not exist." + if ! git tag -l "${ROLLBACK_TAG}" | grep -qxF "${ROLLBACK_TAG}"; then + echo "BLOCKED: ${ROLLBACK_TAG} is not a tag." + echo "Rollback requires a release tag (e.g. v1.2.0), not a branch/commit." exit 1 fi - echo "Tag ${ROLLBACK_TAG} found — proceeding with rollback." From cbe941a7d23f25dc29e7d24247666009f953f231 Mon Sep 17 00:00:00 2001 From: Siddhant Medar Date: Tue, 14 Apr 2026 16:31:18 -0500 Subject: [PATCH 3/9] fix: move indented comment to correct indentation in ai-code-review Bitbucket's YAML parser was interpreting the deeper-indented comment after the printf line as a phantom empty list item, causing a 'Missing or empty command string' error at script item 9. --- bitbucket-pipelines.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bitbucket-pipelines.yml b/bitbucket-pipelines.yml index 53d3c41..fb95627 100644 --- a/bitbucket-pipelines.yml +++ b/bitbucket-pipelines.yml @@ -116,8 +116,8 @@ definitions: - export AWS_REGION=us-east-1 - export AWS_ROLE_ARN="arn:aws:iam::975049960860:role/DoczyAI-Bitbucket-OIDC" - export AWS_WEB_IDENTITY_TOKEN_FILE="$(pwd)/web-identity-token" + # printf (not echo) avoids appending a trailing newline to the JWT - printf '%s' "$BITBUCKET_STEP_OIDC_TOKEN" > "$(pwd)/web-identity-token" - # printf (not echo) avoids appending a trailing newline to the JWT # Clone the review agent repo (private — needs BITBUCKET_CLONE_TOKEN) # SETUP REQUIRED: set BITBUCKET_CLONE_TOKEN (marked "Secured") in From 2a0617cd7903e169b6931cdd04990b151fc2ab4a Mon Sep 17 00:00:00 2001 From: Siddhant Medar Date: Tue, 14 Apr 2026 16:43:59 -0500 Subject: [PATCH 4/9] fix: escape colon in WARNING message to prevent YAML mapping parse The unquoted ': ' (colon + space) in 'WARNING: AI review step failed' was being interpreted as a YAML mapping key-value separator, causing the entire script item to be parsed as a dict instead of a command string. Bitbucket then rejected it with 'Missing or empty command string' error at pull-requests > feature/* > 2 > step > script > 9. Replaced the colon with a hyphen. Validated with yaml.safe_load that all 10 script items in the ai-code-review step now parse as strings. --- bitbucket-pipelines.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bitbucket-pipelines.yml b/bitbucket-pipelines.yml index fb95627..b8ac02d 100644 --- a/bitbucket-pipelines.yml +++ b/bitbucket-pipelines.yml @@ -139,7 +139,7 @@ definitions: # apt-get above is still a hard failure (rare in practice). # AI review is advisory, not a gate. - export PR_URL="https://bitbucket.org/${BITBUCKET_WORKSPACE}/${BITBUCKET_REPO_SLUG}/pull-requests/${BITBUCKET_PR_ID}" - - cd code-review-agent && python main.py "$PR_URL" || echo "WARNING: AI review step failed (non-blocking)" + - cd code-review-agent && python main.py "$PR_URL" || echo "WARNING - AI review step failed (non-blocking)" # ============================================================================= From 4836e36f65c387ad3bf59f7946605942b1e7f2d0 Mon Sep 17 00:00:00 2001 From: Siddhant Medar Date: Wed, 15 Apr 2026 09:01:35 -0500 Subject: [PATCH 5/9] harden pipeline per PR review: locked lockfile, token permissions, image comment MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 1. Replace 'uv sync --frozen' with 'uv sync --locked'. --frozen silently uses a stale lockfile if pyproject.toml is updated without regenerating uv.lock, masking missed dependencies. --locked fails loudly with a clear error when the lockfile is out of sync. Verified empirically with uv 0.9.14: --frozen exits 0 on mismatch, --locked exits 1. 2. Add 'chmod 600' on the OIDC token file after writing it. The container is already single-user root so this is defensive/cosmetic, but it silences security scanners and signals intent. 3. Add a comment explaining why gate steps use atlassian/default-image:4 instead of python:3.12.7 (gates run bash only, no Python toolchain needed — lighter image, faster pull). Explicitly rejected from the PR review: - Token-masking sed mitigation (delimiter collision with / in real Bitbucket clone tokens; cmd | sed || exit 1 swallows git failures without set -o pipefail). Bitbucket's built-in Secured variable masking is the correct mitigation and is already documented as a setup requirement. - SSH key alternative for git clone (architecturally worse — more secrets to manage; HTTPS+Secured is the Bitbucket-recommended pattern). - cd/python refactor ('fragile logic bug'). Verified empirically that 'cd X && python Y || echo Z' with set -e correctly catches both cd and python failures via the ||. The step exits 0 as intended by the 'advisory, not a gate' design. The suggested refactor would introduce a hard-fail regression. --- bitbucket-pipelines.yml | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/bitbucket-pipelines.yml b/bitbucket-pipelines.yml index b8ac02d..a8392dc 100644 --- a/bitbucket-pipelines.yml +++ b/bitbucket-pipelines.yml @@ -46,10 +46,12 @@ definitions: scripts: # Installs the uv package manager (pinned to avoid surprise breaks), - # then syncs both dev and test dependency groups from pyproject.toml + # then syncs both dev and test dependency groups from pyproject.toml. + # --locked (NOT --frozen) asserts uv.lock matches pyproject.toml and + # fails loudly on mismatch. --frozen would silently use a stale lockfile. - script: &install pip install uv==0.11.5; - uv sync --frozen --group dev --group test; + uv sync --locked --group dev --group test; # --------------------------------------------------------------------------- # Reusable step definitions @@ -116,8 +118,11 @@ definitions: - export AWS_REGION=us-east-1 - export AWS_ROLE_ARN="arn:aws:iam::975049960860:role/DoczyAI-Bitbucket-OIDC" - export AWS_WEB_IDENTITY_TOKEN_FILE="$(pwd)/web-identity-token" - # printf (not echo) avoids appending a trailing newline to the JWT + # printf (not echo) avoids appending a trailing newline to the JWT. + # chmod 600 restricts the token file to owner-only (defensive — + # the container is already single-user root, but silences scanners). - printf '%s' "$BITBUCKET_STEP_OIDC_TOKEN" > "$(pwd)/web-identity-token" + - chmod 600 "$(pwd)/web-identity-token" # Clone the review agent repo (private — needs BITBUCKET_CLONE_TOKEN) # SETUP REQUIRED: set BITBUCKET_CLONE_TOKEN (marked "Secured") in @@ -181,6 +186,9 @@ pipelines: # Feature branches represent new functionality. They must go through # dev first for integration testing before promotion to stg/main. "feature/*": + # Gate steps below use atlassian/default-image:4 (not python:3.12.7) + # because gates only run bash/shell — no Python needed. The default + # image is lighter and avoids pulling a full Python toolchain. - step: name: "Gate: feature → dev" image: atlassian/default-image:4 From e94c5e01001c0adaaff7e298f0f622260011292c Mon Sep 17 00:00:00 2001 From: Siddhant Medar Date: Wed, 15 Apr 2026 13:48:17 -0500 Subject: [PATCH 6/9] apply two real items from second PR review round MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 1. Tag format validation in release-prod (MEDIUM, defensive). Before parsing major/minor/patch from the latest tag, assert it matches ^v[0-9]+\.[0-9]+\.[0-9]+$. Empirically tested: catches v1, v1.2, v1.2.3.4, v1.0.0-rc1, v1.2.3-beta+build, and still accepts v0.0.0 (the first-release fallback). Without this, bash arithmetic silently mangles non-semver tags into wrong results via its 'treat empty/non-numeric as 0' rule. Worst case: tag 'v1' → cut -d. -f2/-f3 both return '1' → minor bump produces v1.1.2 instead of v1.0.1. No visible error. 2. Rewrite the AI review comment to match actual behavior. The old comment said 'advisory, not a gate' but also admitted git clone / apt-get are hard failures, contradicting itself. The new comment makes the runtime vs. infrastructure distinction explicit: runtime failures (agent crash, STS errors, Python exceptions) become warnings via || echo; infrastructure failures (missing token, clone failure, apt-get failure) stay hard. This is a doc fix, not a code change. Deliberately preserving the hard-fail behavior on config errors because silent degradation of AI review is the worst outcome — feature disappears from CI with no signal. Explicitly rejected from the second review (empirically verified): - Gate logic 'fragile' claim: tested in bash, correct for all our hardcoded ALLOWED values. - ROLLBACK_TAG -z check 'passes empty': tested, -z correctly catches empty strings. Reviewer has the semantics wrong. - chmod 600 + set +x: our comment already documents chmod 600 as cosmetic/scanner-silencing; set +x wouldn't help because Bitbucket's line-echo is runner-level, not bash set -x. - cd/python refactor (previous round): already rebutted in commit 4836e36f via empirical bash AND-OR list tests. - Option B for git clone wrapping: silent degradation of config errors is worse than the current loud-fail behavior. --- bitbucket-pipelines.yml | 22 ++++++++++++++++------ 1 file changed, 16 insertions(+), 6 deletions(-) diff --git a/bitbucket-pipelines.yml b/bitbucket-pipelines.yml index a8392dc..730cd2a 100644 --- a/bitbucket-pipelines.yml +++ b/bitbucket-pipelines.yml @@ -138,11 +138,13 @@ definitions: - git clone -q "https://x-token-auth:${BITBUCKET_CLONE_TOKEN}@bitbucket.org/${BITBUCKET_WORKSPACE}/code-review-agent.git" # Build the PR URL and run the agent. - # The || echo below only covers the python main.py call: - # agent crashes, AWS STS errors, and runtime exceptions become - # warnings instead of hard failures. A failure in git clone or - # apt-get above is still a hard failure (rare in practice). - # AI review is advisory, not a gate. + # AI review is advisory for *runtime* failures: agent crashes, + # AWS STS errors, and Python exceptions become warnings via the + # || echo below (which only wraps the python main.py call). + # *Infrastructure/configuration* failures (missing BITBUCKET_CLONE_TOKEN, + # git clone failures, apt-get failures) remain HARD failures by + # design — they indicate the pipeline setup itself is broken and + # need to be loud, not silently degraded into a stale warning. - export PR_URL="https://bitbucket.org/${BITBUCKET_WORKSPACE}/${BITBUCKET_REPO_SLUG}/pull-requests/${BITBUCKET_PR_ID}" - cd code-review-agent && python main.py "$PR_URL" || echo "WARNING - AI review step failed (non-blocking)" @@ -372,9 +374,17 @@ pipelines: if [ -z "$latest_tag" ]; then latest_tag="v0.0.0"; fi echo "Latest tag: ${latest_tag}" - # Parse the tag into its three version components + # Parse the tag into its three version components. + # Validate strict semver first — bash arithmetic silently mangles + # non-semver tags (v1, v1.2, v1.2.3-rc1 etc.) into wrong results. + # Fail loudly with a clear error so nobody ships a bad release. - | version=$(echo "$latest_tag" | sed 's/^v//') + if ! [[ "$version" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]; then + echo "BLOCKED: latest tag '$latest_tag' is not strict semver (expected v#.#.#)." + echo "Fix by creating a properly-formatted tag: git tag -a vX.Y.Z -m 'Release vX.Y.Z'" + exit 1 + fi major=$(echo "$version" | cut -d. -f1) minor=$(echo "$version" | cut -d. -f2) patch=$(echo "$version" | cut -d. -f3) From d9acfc7cc73ed0635a1166e43ac7c27c5b938533 Mon Sep 17 00:00:00 2001 From: sujit deokar Date: Thu, 16 Apr 2026 08:55:49 +0000 Subject: [PATCH 7/9] Based on code review feedback, made the changes. --- bitbucket-pipelines.yml | 43 +++++++++++++++++++++++++++++------------ 1 file changed, 31 insertions(+), 12 deletions(-) diff --git a/bitbucket-pipelines.yml b/bitbucket-pipelines.yml index 730cd2a..81f4378 100644 --- a/bitbucket-pipelines.yml +++ b/bitbucket-pipelines.yml @@ -34,7 +34,9 @@ definitions: # --------------------------------------------------------------------------- # Custom cache: uv stores packages in ~/.cache/uv, NOT ~/.cache/pip. - # Without this, every pipeline run re-downloads all dependencies. + # pip cache is intentionally absent — uv fully replaces pip for dependency + # management, so caching pip's download dir would waste space and never hit. + # Without this uv cache, every pipeline run re-downloads all dependencies. # --------------------------------------------------------------------------- caches: uv: ~/.cache/uv @@ -49,8 +51,9 @@ definitions: # then syncs both dev and test dependency groups from pyproject.toml. # --locked (NOT --frozen) asserts uv.lock matches pyproject.toml and # fails loudly on mismatch. --frozen would silently use a stale lockfile. - - script: &install - pip install uv==0.11.5; + # allows patch security updates, blocks breaking minor/major bumps — review uv releases quarterly + - script: &install | + pip install "uv>=0.11.5,<0.12"; uv sync --locked --group dev --group test; # --------------------------------------------------------------------------- @@ -118,11 +121,12 @@ definitions: - export AWS_REGION=us-east-1 - export AWS_ROLE_ARN="arn:aws:iam::975049960860:role/DoczyAI-Bitbucket-OIDC" - export AWS_WEB_IDENTITY_TOKEN_FILE="$(pwd)/web-identity-token" - # printf (not echo) avoids appending a trailing newline to the JWT. - # chmod 600 restricts the token file to owner-only (defensive — - # the container is already single-user root, but silences scanners). + # install -m 600 atomically creates the file with restricted permissions + # BEFORE any data is written, closing the race window that exists when + # writing first and chmodding second (default umask would expose the + # token briefly as 644). printf (not echo) avoids a trailing newline. + - install -m 600 /dev/null "$(pwd)/web-identity-token" - printf '%s' "$BITBUCKET_STEP_OIDC_TOKEN" > "$(pwd)/web-identity-token" - - chmod 600 "$(pwd)/web-identity-token" # Clone the review agent repo (private — needs BITBUCKET_CLONE_TOKEN) # SETUP REQUIRED: set BITBUCKET_CLONE_TOKEN (marked "Secured") in @@ -134,8 +138,13 @@ definitions: echo "Set it in Repository settings → Repository variables (Secured)." exit 1 fi - # -q flag suppresses URL output to avoid leaking the token in logs - - git clone -q "https://x-token-auth:${BITBUCKET_CLONE_TOKEN}@bitbucket.org/${BITBUCKET_WORKSPACE}/code-review-agent.git" + # Use git credential helper to keep the token out of the command line + # and out of git error messages. Writing the credential to a file that + # is never echoed prevents token exposure in process lists and logs. + - git config --global credential.helper store + - printf 'https://x-token-auth:%s@bitbucket.org\n' "${BITBUCKET_CLONE_TOKEN}" > ~/.git-credentials + - chmod 600 ~/.git-credentials + - git clone -q "https://bitbucket.org/${BITBUCKET_WORKSPACE}/code-review-agent.git" # Build the PR URL and run the agent. # AI review is advisory for *runtime* failures: agent crashes, @@ -380,8 +389,10 @@ pipelines: # Fail loudly with a clear error so nobody ships a bad release. - | version=$(echo "$latest_tag" | sed 's/^v//') - if ! [[ "$version" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]; then - echo "BLOCKED: latest tag '$latest_tag' is not strict semver (expected v#.#.#)." + # Reject leading zeros (e.g. v01.02.03) — bash arithmetic treats + # them as octal, which silently produces wrong version numbers. + if ! [[ "$version" =~ ^(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)$ ]]; then + echo "BLOCKED: latest tag '$latest_tag' is not strict semver (expected v#.#.#, no leading zeros)." echo "Fix by creating a properly-formatted tag: git tag -a vX.Y.Z -m 'Release vX.Y.Z'" exit 1 fi @@ -420,7 +431,15 @@ pipelines: deployment: production # Bitbucket deployment environment image: atlassian/default-image:4 script: - # Read the tag that was created in step 1 + # Read the tag that was created in step 1. + # Validate the artifact exists first — a missing file means the + # tag-creation step failed partway and should not silently deploy. + - | + if [ ! -f release_tag.txt ]; then + echo "BLOCKED: release_tag.txt not found." + echo "The tag creation step may have failed before writing the artifact." + exit 1 + fi - export RELEASE_TAG="$(cat release_tag.txt)" - echo "Deploying ${RELEASE_TAG}" From fda9c2b24363e699918ce0f30e68f4cfb865ca6f Mon Sep 17 00:00:00 2001 From: sujit deokar Date: Thu, 16 Apr 2026 10:44:00 +0000 Subject: [PATCH 8/9] enhance pipeline: add validation for release tag format and improve AI code review steps remove duplicate lint, type and unit test validation from PR pipeline. --- bitbucket-pipelines.yml | 46 +++++++++++++++++++++++------------------ 1 file changed, 26 insertions(+), 20 deletions(-) diff --git a/bitbucket-pipelines.yml b/bitbucket-pipelines.yml index 81f4378..8bc4097 100644 --- a/bitbucket-pipelines.yml +++ b/bitbucket-pipelines.yml @@ -125,8 +125,10 @@ definitions: # BEFORE any data is written, closing the race window that exists when # writing first and chmodding second (default umask would expose the # token briefly as 644). printf (not echo) avoids a trailing newline. + - set +x - install -m 600 /dev/null "$(pwd)/web-identity-token" - printf '%s' "$BITBUCKET_STEP_OIDC_TOKEN" > "$(pwd)/web-identity-token" + - set -x # Clone the review agent repo (private — needs BITBUCKET_CLONE_TOKEN) # SETUP REQUIRED: set BITBUCKET_CLONE_TOKEN (marked "Secured") in @@ -196,6 +198,9 @@ pipelines: # ------------------------------------------------------------------------- # Feature branches represent new functionality. They must go through # dev first for integration testing before promotion to stg/main. + # Note: Code quality checks (lint, typecheck, unit-tests) run in the + # default pipeline on every push. This PR pipeline validates branch + # routing and performs AI code review only. "feature/*": # Gate steps below use atlassian/default-image:4 (not python:3.12.7) # because gates only run bash/shell — no Python needed. The default @@ -214,16 +219,15 @@ pipelines: [ "${BITBUCKET_PR_DESTINATION_BRANCH}" = "$t" ] && exit 0 done echo "BLOCKED: target must be one of: ${ALLOWED}"; exit 1 - - parallel: - - step: *lint - - step: *typecheck - - step: *unit-tests - step: *ai-code-review # ------------------------------------------------------------------------- # bugfix/* → dev only # ------------------------------------------------------------------------- # Bug fixes follow the same path as features — must land in dev first. + # Note: Code quality checks (lint, typecheck, unit-tests) run in the + # default pipeline on every push. This PR pipeline validates branch + # routing and performs AI code review only. "bugfix/*": - step: name: "Gate: bugfix → dev" @@ -239,10 +243,6 @@ pipelines: [ "${BITBUCKET_PR_DESTINATION_BRANCH}" = "$t" ] && exit 0 done echo "BLOCKED: target must be one of: ${ALLOWED}"; exit 1 - - parallel: - - step: *lint - - step: *typecheck - - step: *unit-tests - step: *ai-code-review # ------------------------------------------------------------------------- @@ -250,6 +250,9 @@ pipelines: # ------------------------------------------------------------------------- # Hotfixes are urgent production fixes. They can go directly to main # (fast-track) or to dev (to keep the dev branch in sync). + # Note: Code quality checks (lint, typecheck, unit-tests) run in the + # default pipeline on every push. This PR pipeline validates branch + # routing and performs AI code review only. "hotfix/*": - step: name: "Gate: hotfix → main or dev" @@ -265,10 +268,6 @@ pipelines: [ "${BITBUCKET_PR_DESTINATION_BRANCH}" = "$t" ] && exit 0 done echo "BLOCKED: target must be one of: ${ALLOWED}"; exit 1 - - parallel: - - step: *lint - - step: *typecheck - - step: *unit-tests - step: *ai-code-review # ------------------------------------------------------------------------- @@ -276,6 +275,9 @@ pipelines: # ------------------------------------------------------------------------- # Once features/fixes are integrated in dev, a PR from dev to stg # promotes the code to staging for final validation. + # Note: Code quality checks (lint, typecheck, unit-tests) run in the + # default pipeline on every push. This PR pipeline validates branch + # routing and performs AI code review only. dev: - step: name: "Gate: dev → stg" @@ -291,10 +293,6 @@ pipelines: [ "${BITBUCKET_PR_DESTINATION_BRANCH}" = "$t" ] && exit 0 done echo "BLOCKED: target must be one of: ${ALLOWED}"; exit 1 - - parallel: - - step: *lint - - step: *typecheck - - step: *unit-tests - step: *ai-code-review # ------------------------------------------------------------------------- @@ -302,6 +300,9 @@ pipelines: # ------------------------------------------------------------------------- # Final promotion: staging to main. After this merges, the code is # ready for a production release via the custom release-prod pipeline. + # Note: Code quality checks (lint, typecheck, unit-tests) run in the + # default pipeline on every push. This PR pipeline validates branch + # routing and performs AI code review only. stg: - step: name: "Gate: stg → main" @@ -317,10 +318,6 @@ pipelines: [ "${BITBUCKET_PR_DESTINATION_BRANCH}" = "$t" ] && exit 0 done echo "BLOCKED: target must be one of: ${ALLOWED}"; exit 1 - - parallel: - - step: *lint - - step: *typecheck - - step: *unit-tests - step: *ai-code-review @@ -441,6 +438,15 @@ pipelines: exit 1 fi - export RELEASE_TAG="$(cat release_tag.txt)" + # Validate tag format before deployment — reject corrupted/empty tags. + # Regex: v followed by three version numbers (no leading zeros). + # Escaped dots: \. matches literal dots, not any character. + - | + if ! [[ "$RELEASE_TAG" =~ ^v(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)$ ]]; then + echo "BLOCKED: invalid release tag format: $RELEASE_TAG" + echo "Expected format: v#.#.# (e.g., v1.2.3, no leading zeros)" + exit 1 + fi - echo "Deploying ${RELEASE_TAG}" # DEPLOY_COMMAND is a repository variable configured in From 72ccdcd341f73a06f500a6e1e7b32691c3a11138 Mon Sep 17 00:00:00 2001 From: sujit deokar Date: Thu, 16 Apr 2026 11:49:33 +0000 Subject: [PATCH 9/9] harden pipeline: improve security for OIDC token handling and add version component validation --- bitbucket-pipelines.yml | 33 ++++++++++++++++++++------------- 1 file changed, 20 insertions(+), 13 deletions(-) diff --git a/bitbucket-pipelines.yml b/bitbucket-pipelines.yml index 8bc4097..ec7ec94 100644 --- a/bitbucket-pipelines.yml +++ b/bitbucket-pipelines.yml @@ -109,7 +109,7 @@ definitions: script: # Slim image doesn't include git — install it + AWS/HTTP libs # --no-install-recommends skips git-man/less/openssh-client etc. - - apt-get update && apt-get install -y --no-install-recommends git + - apt-get update && apt-get install -y --no-install-recommends git && rm -rf /var/lib/apt/lists/* - pip install "boto3==1.35.*" "requests==2.32.*" # --- AWS OIDC authentication setup --- @@ -121,14 +121,6 @@ definitions: - export AWS_REGION=us-east-1 - export AWS_ROLE_ARN="arn:aws:iam::975049960860:role/DoczyAI-Bitbucket-OIDC" - export AWS_WEB_IDENTITY_TOKEN_FILE="$(pwd)/web-identity-token" - # install -m 600 atomically creates the file with restricted permissions - # BEFORE any data is written, closing the race window that exists when - # writing first and chmodding second (default umask would expose the - # token briefly as 644). printf (not echo) avoids a trailing newline. - - set +x - - install -m 600 /dev/null "$(pwd)/web-identity-token" - - printf '%s' "$BITBUCKET_STEP_OIDC_TOKEN" > "$(pwd)/web-identity-token" - - set -x # Clone the review agent repo (private — needs BITBUCKET_CLONE_TOKEN) # SETUP REQUIRED: set BITBUCKET_CLONE_TOKEN (marked "Secured") in @@ -140,12 +132,21 @@ definitions: echo "Set it in Repository settings → Repository variables (Secured)." exit 1 fi - # Use git credential helper to keep the token out of the command line - # and out of git error messages. Writing the credential to a file that - # is never echoed prevents token exposure in process lists and logs. + + # Disable debug output (set +x) during token/credential handling to prevent + # token exposure in process lists or build logs. Wrap all credential operations + # in this block. Re-enable debug (set -x) after. + - set +x + # Create OIDC token file with secure permissions + - install -m 600 /dev/null "$(pwd)/web-identity-token" + - printf '%s' "$BITBUCKET_STEP_OIDC_TOKEN" > "$(pwd)/web-identity-token" + # Configure git credential helper and store credentials - git config --global credential.helper store - printf 'https://x-token-auth:%s@bitbucket.org\n' "${BITBUCKET_CLONE_TOKEN}" > ~/.git-credentials - chmod 600 ~/.git-credentials + - set -x + + # Clone the review agent repo - git clone -q "https://bitbucket.org/${BITBUCKET_WORKSPACE}/code-review-agent.git" # Build the PR URL and run the agent. @@ -157,7 +158,8 @@ definitions: # design — they indicate the pipeline setup itself is broken and # need to be loud, not silently degraded into a stale warning. - export PR_URL="https://bitbucket.org/${BITBUCKET_WORKSPACE}/${BITBUCKET_REPO_SLUG}/pull-requests/${BITBUCKET_PR_ID}" - - cd code-review-agent && python main.py "$PR_URL" || echo "WARNING - AI review step failed (non-blocking)" + - cd code-review-agent + - python main.py "$PR_URL" || echo "WARNING - AI review step failed (non-blocking)" # ============================================================================= @@ -396,6 +398,11 @@ pipelines: major=$(echo "$version" | cut -d. -f1) minor=$(echo "$version" | cut -d. -f2) patch=$(echo "$version" | cut -d. -f3) + # Validate all version components were extracted + if [ -z "$major" ] || [ -z "$minor" ] || [ -z "$patch" ]; then + echo "BLOCKED: failed to parse version components from $latest_tag" + exit 1 + fi # Bump the correct component, reset everything below it - |