Merged in feature/upDatepipelinesFile (pull request #956)
Update bitbucket pipelines Approved-by: Sujit Deokar
This commit is contained in:
+434
-172
@@ -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/<description>"
|
||||
echo " - bugfix/<description>"
|
||||
echo " - hotfix/<description>"
|
||||
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"
|
||||
|
||||
Reference in New Issue
Block a user