# ============================================================================= # 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). # # 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. # ============================================================================= # ============================================================================= # DEFINITIONS — Reusable building blocks # ============================================================================= definitions: # --------------------------------------------------------------------------- # Custom cache: uv stores packages in ~/.cache/uv, NOT ~/.cache/pip. # 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 # --------------------------------------------------------------------------- # 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. # --locked (NOT --frozen) asserts uv.lock matches pyproject.toml and # fails loudly on mismatch. --frozen would silently use a stale lockfile. # 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; # --------------------------------------------------------------------------- # Reusable step definitions # Each step is a self-contained CI job with its own Docker container. # --------------------------------------------------------------------------- steps: # -- 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 # install uv + sync dependencies - uv run black --check src/ # check formatting (no changes made) caches: - 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 # install uv + sync dependencies - uv run mypy src/ # run type checker on source code caches: - uv # -- Unit tests: runs the test suite with pytest -------------------------- - step: &unit-tests name: Unit Tests (pytest) image: python:3.12.7 script: - *install # install uv + sync dependencies - uv run pytest src/tests/ # run all tests under src/tests/ caches: - 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.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 # --no-install-recommends skips git-man/less/openssh-client etc. - 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 --- # 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" # 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 # 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. # 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)" # ============================================================================= # 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: - 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. # # 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. # 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 # image is lighter and avoids pulling a full Python toolchain. - step: name: "Gate: feature → dev" image: atlassian/default-image:4 script: - | # Resolve source branch (fallback to BITBUCKET_BRANCH if PR variable unavailable) SOURCE_BRANCH="${BITBUCKET_PR_SOURCE_BRANCH:-$BITBUCKET_BRANCH}" TARGET_BRANCH="${BITBUCKET_PR_DESTINATION_BRANCH}" # Gate: feature branches can only target dev ALLOWED="dev" echo "Source: $SOURCE_BRANCH" echo "Target: $TARGET_BRANCH (allowed: $ALLOWED)" IFS=',' read -ra targets <<< "$ALLOWED" for t in "${targets[@]}"; do [ "${TARGET_BRANCH}" = "$t" ] && exit 0 done echo "BLOCKED: target must be one of: $ALLOWED"; exit 1 - 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" image: atlassian/default-image:4 script: - | # Resolve source branch (fallback to BITBUCKET_BRANCH if PR variable unavailable) SOURCE_BRANCH="${BITBUCKET_PR_SOURCE_BRANCH:-$BITBUCKET_BRANCH}" TARGET_BRANCH="${BITBUCKET_PR_DESTINATION_BRANCH}" # Gate: bugfix branches can only target dev ALLOWED="dev" echo "Source: ${SOURCE_BRANCH}" echo "Target: ${TARGET_BRANCH} (allowed: ${ALLOWED})" IFS=',' read -ra targets <<< "$ALLOWED" for t in "${targets[@]}"; do [ "${TARGET_BRANCH}" = "$t" ] && exit 0 done echo "BLOCKED: target must be one of: ${ALLOWED}"; exit 1 - 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). # 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" image: atlassian/default-image:4 script: - | # Resolve source branch (fallback to BITBUCKET_BRANCH if PR variable unavailable) SOURCE_BRANCH="${BITBUCKET_PR_SOURCE_BRANCH:-$BITBUCKET_BRANCH}" TARGET_BRANCH="${BITBUCKET_PR_DESTINATION_BRANCH}" # Gate: hotfix branches can target main (fast-track) or dev ALLOWED="main,dev" echo "Source: ${SOURCE_BRANCH}" echo "Target: ${TARGET_BRANCH} (allowed: ${ALLOWED})" IFS=',' read -ra targets <<< "$ALLOWED" for t in "${targets[@]}"; do [ "${TARGET_BRANCH}" = "$t" ] && exit 0 done echo "BLOCKED: target must be one of: ${ALLOWED}"; exit 1 - 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. # 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" image: atlassian/default-image:4 script: - | # Resolve source branch (fallback to BITBUCKET_BRANCH if PR variable unavailable) SOURCE_BRANCH="${BITBUCKET_PR_SOURCE_BRANCH:-$BITBUCKET_BRANCH}" TARGET_BRANCH="${BITBUCKET_PR_DESTINATION_BRANCH}" # Gate: dev can only be promoted to stg ALLOWED="stg" echo "Source: $SOURCE_BRANCH" echo "Target: ${TARGET_BRANCH} (allowed: ${ALLOWED})" IFS=',' read -ra targets <<< "$ALLOWED" for t in "${targets[@]}"; do [ "${TARGET_BRANCH}" = "$t" ] && exit 0 done echo "BLOCKED: target must be one of: ${ALLOWED}"; exit 1 - 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. # 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" image: atlassian/default-image:4 script: - | # Resolve source branch (fallback to BITBUCKET_BRANCH if PR variable unavailable) SOURCE_BRANCH="${BITBUCKET_PR_SOURCE_BRANCH:-$BITBUCKET_BRANCH}" TARGET_BRANCH="${BITBUCKET_PR_DESTINATION_BRANCH}" # Gate: stg can only be promoted to main ALLOWED="main" echo "Source: ${SOURCE_BRANCH}" echo "Target: ${TARGET_BRANCH} (allowed: ${ALLOWED})" IFS=',' read -ra targets <<< "$ALLOWED" for t in "${targets[@]}"; do [ "${TARGET_BRANCH}" = "$t" ] && exit 0 done echo "BLOCKED: target must be one of: ${ALLOWED}"; exit 1 - step: *ai-code-review # ------------------------------------------------------------------------- # Catch-all: Enforce branch naming standards # ------------------------------------------------------------------------- # Any branch that doesn't match the patterns above (feature/*, bugfix/*, # hotfix/*, dev, stg) will match this catch-all. It fails immediately # to enforce branch naming conventions. # # Allowed branch prefixes: # • feature/* (new features) # • bugfix/* (bug fixes) # • hotfix/* (urgent production fixes) # • dev (development integration) # • stg (staging promotion) # ------------------------------------------------------------------------- "**": - step: name: "Validate branch naming" image: atlassian/default-image:4 script: - | # Resolve source branch (fallback to BITBUCKET_BRANCH if PR variable unavailable) SOURCE_BRANCH="${BITBUCKET_PR_SOURCE_BRANCH:-$BITBUCKET_BRANCH}" echo "============================================================" echo "BRANCH NAMING STANDARD VIOLATION" echo "============================================================" echo "Branch: ${SOURCE_BRANCH}" echo "" echo "This branch does not match allowed naming conventions." echo "" echo "Allowed prefixes:" echo " • feature/* — for new features" echo " • bugfix/* — for bug fixes" echo " • hotfix/* — for urgent production fixes" echo "" echo "Examples of valid branch names:" echo " • feature/user-authentication" echo " • bugfix/login-page-crash" echo " • hotfix/security-patch" echo "" echo "Please rename your branch to match the standard and try again." echo "============================================================" exit 1 # =========================================================================== # 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 default: minor 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: # 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." 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 # 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. # 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//') # 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 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 - | if [ "$RELEASE_TYPE" = "major" ]; then major=$((major + 1)); minor=0; patch=0 elif [ "$RELEASE_TYPE" = "minor" ]; then minor=$((minor + 1)); patch=0 else patch=$((patch + 1)) fi # 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}" 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 # passed to the deploy step below # Step 2: Deploy to production - step: name: "Deploy to production" deployment: production # Bitbucket deployment environment image: atlassian/default-image:4 script: # 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)" # 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 # 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." 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: "" # user MUST provide a real tag (e.g. v1.2.0) # Step 1: Validate the tag exists - step: name: "Validate rollback tag" image: atlassian/default-image:4 script: # 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." exit 1 fi # Make sure a tag was actually provided - | if [ -z "${ROLLBACK_TAG}" ]; then echo "BLOCKED: ROLLBACK_TAG is required." exit 1 fi # 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 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." # 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." echo "Use \$RELEASE_TAG inside the command to reference the version." exit 1 fi - sh -c "$DEPLOY_COMMAND"