diff --git a/bitbucket-pipelines.yml b/bitbucket-pipelines.yml index e4557f3..ec7ec94 100644 --- a/bitbucket-pipelines.yml +++ b/bitbucket-pipelines.yml @@ -1,181 +1,350 @@ +# ============================================================================= +# 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: - - script: &install - pip install uv; - 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}"; + + # 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: - - 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 + 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: - - apt-get update && apt-get install -y git - - pip install boto3 requests - - 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 PR_URL="https://bitbucket.org/${BITBUCKET_WORKSPACE}/${BITBUCKET_REPO_SLUG}/pull-requests/${BITBUCKET_PR_ID}" - - cd code-review-agent && python main.py "$PR_URL" + # 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: - - 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. + # + # 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/*": - - step: *install-deps - - parallel: - - step: *lint - - step: *typecheck - - step: *unit-tests + # 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 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 - 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: + - | + # 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 + - 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: + - | + # 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 + - 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: + - | + # 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 + - 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: + - | + # 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 + - 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 +352,178 @@ 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. + # 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 + 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. + # 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 + - 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: - - export RELEASE_TAG=$(cat release_tag.txt) + # 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 (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: "" # user MUST provide a real tag (e.g. v1.2.0) + + # 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 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." + + # 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"