Part 7 of 830 min read · 8 diagramsAI-assisted

Bitbucket & Bitbucket Pipelines

Table of Contents#

  1. Where Bitbucket Fits — Atlassian's Ecosystem Play
  2. Bitbucket Plans and Pipelines Pricing at a Glance
  3. Anatomy of bitbucket-pipelines.yml
  4. The default, branches, pull-requests, and custom Sections
  5. A Minimal Pipeline, Built Up Step by Step
  6. Parallel Steps and Stages
  7. The Matrix Gap — What Bitbucket Doesn't Have
  8. Artifacts and Caching
  9. Pipes — Bitbucket's Reusability Model
  10. YAML Anchors — DRY Without a Reusable-Workflow Primitive
  11. Service Containers — Databases for Integration Tests
  12. Scheduling Custom Pipelines
  13. Deployments and Environments
  14. Branch Restrictions and Merge Checks
  15. OIDC — Eliminating Long-Lived Cloud Credentials
  16. A Full Worked OIDC Example: Deploying to AWS
  17. Secrets — Repository, Deployment, and Workspace Variables
  18. Bitbucket Runners — Self-Hosted Execution
  19. Debugging a Failed Step — SSH Debug Sessions
  20. Jira Integration — Smart Commits and Deployment Tracking
  21. Security Scanning — a More Limited Built-In Story
  22. A Full Realistic Multi-Stage Pipeline
  23. GitHub vs. GitLab vs. Bitbucket — a Three-Way Comparison
  24. Common Mistakes
  25. Worked Practice Problems
  26. Summary and What's Next

Where Bitbucket Fits — Atlassian's Ecosystem Play#

Parts 4-6 covered two platforms whose core identity is either "git host with a first-party CI product" (GitHub) or "single integrated DevOps platform" (GitLab). Bitbucket's actual pitch is a third thing entirely, worth naming precisely: Bitbucket's core value proposition is being the git host that's natively wired into the rest of the Atlassian suite — Jira for issue tracking, Confluence for docs, Trello-style boards — not being the most feature-rich CI/CD engine or the deepest security-scanning platform on its own merits.

Diagram

This matters for how to actually evaluate Bitbucket Pipelines fairly: it is a genuinely capable, real CI/CD system — but it is not trying to out-feature GitHub Actions' Marketplace ecosystem or GitLab's built-in DAST suite. Its distinguishing strength, covered later in this chapter, is how tightly a pipeline's activity (a build, a deployment) surfaces directly inside a linked Jira issue, closing the loop between "here's the ticket for this bug" and "here's proof it's actually deployed to production" without any separate integration tooling. A team already running Jira for project management is Bitbucket Pipelines' single strongest use case; a team with no Atlassian footprint at all has comparatively little unique reason to pick it over GitHub or GitLab on CI/CD merits alone.

This chapter follows the same structure as Parts 4-6 — building up pipeline mechanics from a minimal example, then covering security and reusability — but, in the spirit of fair comparison already established across this series, is equally direct about where Bitbucket's story is genuinely thinner than its two predecessors, not just where it's different. Both are worth knowing precisely before recommending a platform to a real team.


Bitbucket Plans and Pipelines Pricing at a Glance#

PlanIncluded build minutes/monthTypical fit
Free50Small personal projects, evaluation
Standard2,500Small paid teams
Premium3,500Teams needing deployment permissions, merge checks, IP allowlisting
Enterprise5 minutes-multipliers apply; sold with organization-wide governanceLarge orgs needing SSO, audit logs, enterprise-scale governance

The minute-multiplier concept from GitHub (Part 4) applies here too, with Bitbucket's own numbers: a Linux build minute is billed at 1×, while a build using a larger, more powerful runner size consumes minutes at a proportionally higher rate — the same "bigger machine, faster wall-clock, more minutes consumed per minute of actual runtime" tradeoff already covered for GitHub-hosted larger runners in Part 4.


Anatomy of bitbucket-pipelines.yml#

A single YAML file, bitbucket-pipelines.yml, at the repository root — closer in spirit to GitLab's single-file model than GitHub's many-independent-workflow-files approach, though Bitbucket's file has its own distinct internal structure:

Diagram
  • Pipeline — the whole file, plus one top-level image: every step inherits unless overridden.
  • Trigger section (default, branches, pull-requests, tags, custom) — which specific Git event a given pipeline definition responds to; this is Bitbucket's version of GitHub's on: and GitLab's rules:/workflow:rules:, but structured as separate named YAML sections rather than a single conditional expression language.
  • Step — the actual unit of work, running in its own container, with its own script: (a flat list of shell commands, matching GitLab's model rather than GitHub's discrete-step model).

The default, branches, pull-requests, and custom Sections#

image: node:20

pipelines:
  default:                        # runs on every push to any branch NOT matched below
    - step:
        name: Build and test
        script:
          - npm ci
          - npm test

  branches:
    main:                         # runs ONLY on pushes to main — overrides `default` for this branch
      - step:
          name: Build, test, and deploy
          script:
            - npm ci
            - npm test
            - ./deploy.sh

  pull-requests:
    '**':                         # runs on every PR, regardless of source/target branch pattern
      - step:
          name: PR checks
          script:
            - npm ci
            - npm run lint
            - npm test

  custom:
    nightly-security-scan:        # NEVER runs automatically — only via manual trigger or a schedule
      - step:
          script:
            - npm audit

The branch-pattern matching under branches: uses glob-style patterns (main, release/*, feature/**), and a branch matching a specific pattern under branches: runs that pipeline instead of default — not in addition to it. This "most specific match wins, and only one pipeline definition runs per push" model is a genuinely different mental model from GitHub's independent-workflow-files (where multiple separate workflow files can all trigger off the same push) and worth internalizing early, since a common mistake is assuming default and a matched branches: entry both run.


A Minimal Pipeline, Built Up Step by Step#

Step 1 — bare minimum:

pipelines:
  default:
    - step:
        script:
          - echo "hello"

Step 2 — a real Node project:

image: node:20
pipelines:
  default:
    - step:
        name: Test
        script:
          - npm ci
          - npm test

Step 3 — splitting build and test into separate, sequential steps (steps within one pipeline definition run sequentially by default, each in its own fresh container, unlike GitLab's stage-parallel-within-stage model):

image: node:20
pipelines:
  default:
    - step:
        name: Build
        script:
          - npm ci
          - npm run build
        artifacts:
          - dist/**
    - step:
        name: Test
        script:
          - npm test

Step 4 — adding a distinct branch-specific deploy pipeline:

image: node:20
pipelines:
  default:
    - step:
        name: Build
        script: [npm ci, npm run build]
        artifacts: [dist/**]
    - step:
        name: Test
        script: [npm test]
  branches:
    main:
      - step:
          name: Build
          script: [npm ci, npm run build]
          artifacts: [dist/**]
      - step:
          name: Test
          script: [npm test]
      - step:
          name: Deploy
          deployment: production
          script: [./deploy.sh]

Notice the duplication between default and branches: main — Bitbucket's per-trigger-section model means shared logic between pipeline definitions is genuinely copy-pasted unless explicitly factored out via YAML anchors (covered shortly), a real structural difference from GitLab's single unified pipeline with conditional rules: gating individual jobs.


Parallel Steps and Stages#

By default, steps in a pipeline definition run sequentially, each in a completely fresh container — a real contrast with both GitHub (jobs parallel-by-default) and GitLab (jobs within a stage parallel-by-default). Two mechanisms opt back into parallelism:

parallel: — a block of steps that run concurrently:

pipelines:
  default:
    - step:
        name: Build
        script: [npm ci, npm run build]
        artifacts: [dist/**]
    - parallel:
        - step:
            name: Lint
            script: [npm run lint]
        - step:
            name: Unit tests
            script: [npm test]
        - step:
            name: Security audit
            script: [npm audit]
    - step:
        name: Deploy
        deployment: production
        script: [./deploy.sh]
Diagram

stage: — a named, higher-level grouping of steps (introduced later than parallel:), useful for organizing a longer pipeline into logical phases in the Bitbucket UI, conceptually closer to GitLab's stages: naming than to anything GitHub Actions has:

pipelines:
  default:
    - stage:
        name: Build and Test
        steps:
          - step: { script: [npm ci, npm run build] }
          - step: { script: [npm test] }
    - stage:
        name: Deploy
        deployment: production
        steps:
          - step: { script: [./deploy.sh] }

The Matrix Gap — What Bitbucket Doesn't Have#

Worth stating plainly rather than glossing over, since it's a genuine, notable gap against both prior platforms in this series: Bitbucket Pipelines has no native matrix-build primitive — no strategy.matrix (GitHub, Part 4), no parallel:matrix (GitLab, Part 6). Testing across multiple Node versions or operating systems requires hand-writing a separate step per combination:

pipelines:
  default:
    - parallel:
        - step:
            name: Test on Node 18
            image: node:18
            script: [npm ci, npm test]
        - step:
            name: Test on Node 20
            image: node:20
            script: [npm ci, npm test]
        - step:
            name: Test on Node 22
            image: node:22
            script: [npm ci, npm test]

The practical consequence: what's a single strategy.matrix block generating 9 jobs automatically on GitHub becomes 9 hand-written, largely duplicated step: blocks on Bitbucket (mitigated somewhat by YAML anchors, covered next, but never eliminated the way a genuine matrix primitive would). For a team whose testing strategy genuinely depends on wide compatibility-matrix coverage (many OS × runtime-version combinations), this is a real, concrete limitation worth weighing when choosing a platform — not a minor syntax inconvenience.


Artifacts and Caching#

The same conceptual split covered for both prior platforms — cache for speed, artifacts for passing data between steps — Bitbucket's syntax:

pipelines:
  default:
    - step:
        name: Build
        caches:
          - node                    # a built-in, predefined cache definition for node_modules
        script:
          - npm ci
          - npm run build
        artifacts:
          - dist/**
    - step:
        name: Deploy
        script:
          - ./deploy.sh dist/       # dist/ is automatically available - artifacts flow to the NEXT step

Two Bitbucket-specific details worth knowing: first, caches: ships several predefined cache definitions (node, pip, docker, gradle, and others) that just work out of the box by name, without manually specifying the path and cache key the way GitHub's actions/cache or GitLab's cache: require — a genuine convenience for common ecosystems. Second, artifacts automatically flow to the immediately next sequential step without an explicit download step (similar to GitLab's automatic artifact propagation via needs:/stage order) — but only within the same pipeline run; there's no cross-pipeline artifact sharing equivalent to what a registry-based approach would provide.


Pipes — Bitbucket's Reusability Model#

A Pipe is Bitbucket's packaged, reusable unit of automation — closest in spirit to a GitHub Action, though narrower in scope: a Pipe is typically a single, focused, pre-built integration (deploy to AWS, post to Slack, run a specific scanner) rather than an arbitrary reusable job or step sequence.

pipelines:
  default:
    - step:
        name: Deploy to S3
        script:
          - pipe: atlassian/aws-s3-deploy:1.6.1
            variables:
              AWS_ACCESS_KEY_ID: $AWS_ACCESS_KEY_ID
              AWS_SECRET_ACCESS_KEY: $AWS_SECRET_ACCESS_KEY
              AWS_DEFAULT_REGION: us-east-1
              S3_BUCKET: my-production-bucket
              LOCAL_PATH: dist

Bitbucket maintains an official catalog of first-party Pipes (atlassian/) covering common integrations (AWS, Slack, Jira, Docker registries), and third-party/community Pipes exist for less common targets — but the catalog is meaningfully smaller than GitHub's Marketplace, reflecting Bitbucket's narrower ecosystem generally.

GitHub ActionGitLab CI/CD ComponentBitbucket Pipe
ScopeSteps or full jobsFull reusable job configurationsTypically one focused integration
Ecosystem sizeVery large (Marketplace)Growing (CI/CD Catalog)Smaller, Atlassian-curated
VersioningTag or SHASemantic versionSemantic version (@1.6.1)
Can define its own multi-step logic?Yes (composite action)Yes (component)No — a Pipe is invoked as one script: line within a step you still define

The practical implication: a Pipe genuinely simplifies calling into a well-known external system, but Bitbucket has no direct equivalent of a GitHub reusable workflow or a GitLab CI/CD Component for centralizing an organization's own custom, multi-step golden-path pipeline — the closest available tool for that specific need is the YAML-anchor approach covered next, which is a weaker, more manual mechanism than either alternative platform offers.

A team can also author and publish its own private Pipes for genuinely reusable single-purpose automation (an internal deploy-to-internal-registry Pipe, say) — the authoring model is a small Docker image plus a pipe.yml manifest describing its inputs, conceptually similar to a GitHub composite action's action.yml but packaged as a container rather than as inline YAML steps.


YAML Anchors — DRY Without a Reusable-Workflow Primitive#

Given the gaps just described (no native matrix, no first-party reusable-job primitive), Bitbucket pipelines commonly lean on plain YAML anchors and aliases — a generic YAML language feature, not a Bitbucket-specific one — to avoid outright copy-paste:

definitions:
  steps:
    - step: &build-step
        name: Build
        image: node:20
        caches: [node]
        script:
          - npm ci
          - npm run build
        artifacts:
          - dist/**

pipelines:
  default:
    - step: *build-step
    - step:
        name: Test
        script: [npm test]
  branches:
    main:
      - step: *build-step          # the exact same build step definition, reused via the YAML anchor
      - step:
          name: Test
          script: [npm test]
      - step:
          name: Deploy
          deployment: production
          script: [./deploy.sh]

This is a meaningfully weaker reuse mechanism than a GitHub reusable workflow or a GitLab CI/CD Component, and it's worth being precise about why: a YAML anchor is pure textual substitution, resolved entirely client-side by the YAML parser before Bitbucket ever sees pipeline semantics — it has no concept of typed inputs, no independent versioning, and (critically) cannot be shared across repositories the way a GitHub reusable workflow or GitLab Component can; an anchor only works within the single file it's defined in. An organization wanting a genuine cross-repository golden-path pipeline on Bitbucket has to reach for definitions: shared within one repo at most, or fall back to a template-repository-and-manual-sync pattern — there's no first-party mechanism for what Parts 4 and 6 covered as reusable workflows / CI/CD Components.


Service Containers — Databases for Integration Tests#

Bitbucket supports the same "sidecar database container for the duration of a step" pattern already covered for GitHub's service containers (Part 4) and available via GitLab's own services: keyword (Part 6), defined once under definitions: and referenced by name from any step:

definitions:
  services:
    postgres:
      image: postgres:16
      variables:
        POSTGRES_DB: testdb
        POSTGRES_PASSWORD: testpass

pipelines:
  default:
    - step:
        name: Integration tests
        services:
          - postgres
        script:
          - npm ci
          - npm run test:integration
        # the postgres service is reachable at hostname "localhost" on its default port

Unlike GitHub Actions' options: health-check block covered in Part 4, Bitbucket's service containers don't expose an equivalent first-class health-check configuration — a step's script needs to handle "wait for the database to actually be ready" itself if the test suite doesn't already retry its own initial connection gracefully (many database client libraries and ORMs do this by default, but it's worth verifying rather than assuming, since the platform provides no automatic wait-for-healthy gate the way GitHub's does).


Scheduling Custom Pipelines#

The custom: trigger section (introduced earlier) covers pipelines that never run automatically off a Git event — but a custom pipeline isn't limited to purely manual, on-demand triggering. Bitbucket supports attaching a schedule (configured via Repository Settings → Pipelines → Schedules, not inline YAML) to any custom: pipeline definition, running it on a recurring cadence independent of any push:

pipelines:
  custom:
    nightly-security-scan:
      - step:
          name: Full dependency audit
          script:
            - npm audit --audit-level=high
    weekly-dependency-report:
      - step:
          script:
            - ./generate-dependency-report.sh

Each named custom pipeline (nightly-security-scan, weekly-dependency-report) can independently be attached to its own schedule (daily, weekly, or a specific cron-like cadence) in the repository settings UI, or triggered on-demand from the same UI or via the REST API. This split — the pipeline's actual logic lives in YAML (versioned, reviewable, part of the repo's history), but the schedule attaching it to a cadence lives in repository settings (not YAML at all) — is a genuine structural difference from GitHub's schedule: cron: (fully inline, versioned YAML) and GitLab's Pipeline Schedules (also UI/API-configured, similar to Bitbucket's split model). A team auditing "what runs automatically and when" on Bitbucket specifically needs to check repository settings, not just the YAML file, to get the complete picture — the YAML alone under-reports what actually executes on a recurring basis.


Deployments and Environments#

Bitbucket's Deployments feature is its version of GitHub Environments (Part 4) and GitLab protected environments (Part 6) — a named target (test, staging, production) with its own scoped variables and, on paid plans, deployment permissions:

pipelines:
  branches:
    main:
      - step:
          name: Build and test
          script: [npm ci, npm run build, npm test]
      - step:
          name: Deploy to staging
          deployment: staging
          script:
            - ./deploy.sh --env staging
      - step:
          name: Deploy to production
          deployment: production
          trigger: manual              # requires a human to click "run" — the manual approval gate
          script:
            - ./deploy.sh --env production
Diagram

On Premium/Enterprise plans, a production-type deployment environment can additionally require designated Deployment Permissions — specific users/groups who alone are allowed to trigger a deploy to that environment, layered on top of trigger: manual's pause — the closer analogue to GitHub's Environment required reviewers and GitLab's protected-environment approval rules; on the Free/Standard tiers, trigger: manual alone gates when a deploy runs, but not who specifically is allowed to click it, beyond ordinary repo write access.

Deployment environments are also typed (test, staging, production) rather than freely-named, which drives some default UI/dashboard behavior (the Deployments dashboard groups by these types specifically) — a minor but real difference from GitHub's and GitLab's freely-named environment strings.


Branch Restrictions and Merge Checks#

Bitbucket's branch protection equivalent, split across two settings areas worth distinguishing:

Branch restrictions (Repository Settings → Branch restrictions) control who can push/merge/force-push to a matched branch pattern — the access-control layer, analogous to GitHub branch protection rules and GitLab protected branches.

Merge checks (configured per branch restriction) are the CI-enforcement layer specifically:

  • Minimum number of approvals before merge is allowed.
  • Minimum number of successful builds — requires the Pipelines run on the PR's latest commit to have actually passed.
  • No unresolved merge conflicts.
  • All tasks resolved — Bitbucket's inline PR comment "tasks" (a checklist-style comment) must all be checked off.
# Not YAML config — these are UI/API-configured settings, shown here as the conceptual shape:
# branch_restriction:
#   pattern: main
#   kind: require_passes_build
#   value: 1  (at least 1 successful Pipelines run required)

The one structural difference worth flagging against GitHub/GitLab's equivalent controls: Bitbucket has no direct equivalent of CODEOWNERS-style path-scoped mandatory reviewers built into the core product — "require review from specific people for specific paths" is a materially weaker story on Bitbucket than the CODEOWNERS + branch-protection combination covered for GitHub in Part 5, or GitLab's approval rules with eligible-approver groups from Part 6. A team needing that specific level of path-scoped review governance should weigh this gap directly against the other two platforms.


OIDC — Eliminating Long-Lived Cloud Credentials#

Bitbucket's OIDC implementation follows the identical pattern already established twice in this series (GitHub Part 5, GitLab Part 6): a short-lived, signed JWT minted per pipeline step, exchanged with a cloud provider for temporary credentials.

Diagram

One genuine ergonomic difference from GitHub's model, worth calling out: Bitbucket does not require an explicit permissions: opt-in flag analogous to GitHub's id-token: write — the BITBUCKET_STEP_OIDC_TOKEN environment variable is available in any step by default, requiring only that oidc: true be set on the specific step. This is a smaller surface to misconfigure than GitHub's separate permissions declaration, at the tradeoff of slightly less explicit, self-documenting opt-in at the step level.

- step:
    name: Deploy with OIDC
    oidc: true              # the ONE thing required to make BITBUCKET_STEP_OIDC_TOKEN available
    script:
      - export AWS_ROLE_ARN=arn:aws:iam::123456789012:role/bitbucket-deploy
      - # exchange BITBUCKET_STEP_OIDC_TOKEN for real AWS credentials (full example next section)

A Full Worked OIDC Example: Deploying to AWS#

Step 1 — one-time AWS setup, trusting Bitbucket's OIDC issuer:

{
  "Effect": "Allow",
  "Principal": { "Federated": "arn:aws:iam::123456789012:oidc-provider/api.bitbucket.org/2.0/workspaces/my-workspace/pipelines-config/identity/oidc" },
  "Action": "sts:AssumeRoleWithWebIdentity",
  "Condition": {
    "StringEquals": {
      "api.bitbucket.org/2.0/workspaces/my-workspace/pipelines-config/identity/oidc:aud": "ari:cloud:bitbucket::workspace/my-workspace-id"
    },
    "StringLike": {
      "api.bitbucket.org/2.0/workspaces/my-workspace/pipelines-config/identity/oidc:sub": "{a5c8f5e1-...repo-uuid...}:production:*"
    }
  }
}

Step 2 — the pipeline step:

pipelines:
  branches:
    main:
      - step:
          name: Deploy to production via OIDC
          oidc: true
          deployment: production
          script:
            - export AWS_ROLE_ARN=arn:aws:iam::123456789012:role/bitbucket-deploy
            - export AWS_WEB_IDENTITY_TOKEN_FILE=$(pwd)/web-identity-token
            - echo $BITBUCKET_STEP_OIDC_TOKEN > $(pwd)/web-identity-token
            - aws sts assume-role-with-web-identity --role-arn $AWS_ROLE_ARN --role-session-name bitbucket --web-identity-token file://$(pwd)/web-identity-token
            - aws s3 sync ./dist s3://my-production-bucket

The trust policy's sub condition scopes to a specific repository UUID and a specific deployment environment name — the same defense-in-depth restriction pattern as GitHub's environment:production claim (Part 5) and GitLab's environment_protected claim (Part 6): a pipeline step running against a different environment, even in the same repository, is rejected by AWS before ever obtaining credentials, regardless of what the Bitbucket-side pipeline definition itself allows.


Secrets — Repository, Deployment, and Workspace Variables#

ScopeVisible toTypical use
Workspace variablesEvery repository in the workspaceShared credentials many repos need
Repository variablesEvery pipeline in this one repoRepo-specific, not tied to one deploy target
Deployment variablesOnly steps whose deployment: matchesPer-environment credentials, the critical isolation boundary

Marking a variable Secured (a checkbox at creation) masks it in build logs — the same naive, exact-string-match masking caveat already established for GitHub (Part 5) and GitLab (Part 6) applies identically here: a transformed or encoded value bypasses masking.

Deployment variables are the direct equivalent of GitHub's Environment-scoped secrets and GitLab's Protected CI/CD variables — a PROD_DB_PASSWORD deployment variable scoped to the production deployment environment is invisible to a step whose deployment: targets staging, even within the same pipeline run and the same repository, closing the same "a compromised staging deploy shouldn't leak production credentials" gap covered for both prior platforms.


Bitbucket Runners — Self-Hosted Execution#

Bitbucket's self-hosted execution option, functionally parallel to GitHub self-hosted runners (Part 5) and GitLab self-managed Runner (Part 6): a machine registered at the workspace, repository, or (via a Docker/Kubernetes-based runner) more granular scope, that Bitbucket dispatches pipeline steps to instead of its own hosted infrastructure.

- step:
    name: Build on internal hardware
    runs-on:
      - self.hosted
      - linux.shell
    script:
      - ./build-with-internal-access.sh

The same underlying reasons apply as covered for both prior platforms — specialized hardware, network access to private infrastructure, and cost control at high volume (self-hosted runner minutes aren't charged against the plan's included build minutes) — and the same security caveat applies with equal force: a self-hosted runner attached to a repository accepting external contributions carries the same "untrusted PR code can execute on your infrastructure" risk already covered in depth for GitHub in Part 5, requiring the same category of mitigation (avoid running untrusted-source pipelines on self-hosted infrastructure, or heavily sandbox/ephemeral-ize the runner).


Debugging a Failed Step — SSH Debug Sessions#

A genuinely useful, less-publicized Bitbucket feature worth knowing about: when a pipeline step fails, Bitbucket offers a "Run again with SSH debugging" option directly from the failed run's UI — this re-runs the step but drops into an interactive SSH session into the exact same container, at the exact point of failure, rather than only ever showing static logs after the fact.

Diagram

This is a genuinely different debugging experience from GitHub Actions or GitLab CI/CD, both of which require either exhaustively verbose logging added in advance or reproducing the exact runner environment locally (via act for GitHub, or the GitLab Runner binary run locally) to get equivalent interactive access — Bitbucket's SSH debug session gives that access directly against the actual failed run's container, with no local environment-reproduction step needed at all. The security caveat is the same one that applies to any interactive access to a build environment: SSH debug sessions should be restricted to trusted team members (Bitbucket scopes this to users with repository write access by default), since an interactive shell inside a build container can potentially reach whatever secrets/network access that container's pipeline step had.


Jira Integration — Smart Commits and Deployment Tracking#

This is Bitbucket's most genuinely distinctive feature relative to GitHub and GitLab, and the concrete payoff of the Atlassian-ecosystem positioning from this chapter's opening section.

Smart Commits let a commit message directly drive Jira issue state, using a simple inline syntax:

git commit -m "PROJ-123 #comment Fixed the null pointer exception #time 2h #resolve"

This single commit message, once pushed, automatically: posts a comment on Jira issue PROJ-123, logs 2 hours of work against it, and transitions its status to Resolved — with zero separate Jira UI interaction and zero webhook or integration configuration required beyond the (already-standard) Bitbucket-Jira link every workspace in the same Atlassian organization gets by default.

Deployment tracking goes further, surfacing pipeline activity, not just commit activity, directly on the linked Jira issue:

Diagram

A product manager or QA engineer looking at Jira issue PROJ-123 sees, without asking an engineer or checking a separate CI dashboard, exactly which environments the fix has actually reached — a genuinely different, lower-friction experience than the equivalent GitHub or GitLab setup, which would require either a third-party Jira integration (GitHub) or manually cross-referencing GitLab's own issue tracker against pipeline history (GitLab, if not also using Jira separately). This is the single most concrete, hard-to-replicate reason a Jira-centric organization gains real value from choosing Bitbucket over an equivalent GitHub/GitLab setup with a bolted-on Jira integration — not because the underlying CI/CD mechanics are superior, but because the two products were built by the same company specifically to interlock this tightly.


Security Scanning — a More Limited Built-In Story#

Worth stating honestly, continuing this chapter's pattern of not overselling Bitbucket's comparative position: Bitbucket's native, built-in security scanning is meaningfully thinner than GitLab's SAST/DAST/dependency-scanning suite (Part 6) and even GitHub's GHAS ecosystem (Part 5).

CapabilityBitbucket's story
Secret detectionBuilt-in, scans pushes for known credential patterns
SASTNo first-party equivalent — relies on third-party Pipes (e.g. a Snyk or SonarQube Pipe)
Dependency scanningAvailable via Atlassian's separate "Bitbucket Security" add-on or third-party Pipes, not a core-product include:
DASTNo first-party equivalent at all
Container scanningThird-party Pipe only

The practical pattern this leads to: a Bitbucket pipeline that wants comparable security-scanning depth to what GitLab ships nearly for free typically assembles it from several third-party Pipes (a SonarQube Pipe for SAST, a Snyk Pipe for dependency scanning), each with its own separate account/API-key setup — structurally closer to GitHub's ecosystem-driven, opt-in model from Part 4-5 than to GitLab's built-in-by-default one, but with a smaller catalog of available Pipes to assemble from than GitHub's Marketplace offers of equivalent Actions. A security-scanning-heavy organization evaluating all three platforms should treat this as a genuine, material factor, not an afterthought.


A Full Realistic Multi-Stage Pipeline#

The same build → test (parallel) → deploy-staging → smoke-test → manual-gated deploy-production shape from Parts 4 and 6, in Bitbucket's step/parallel/deployment model:

image: node:20

definitions:
  caches:
    node-modules: node_modules

pipelines:
  branches:
    main:
      - step:
          name: Build
          caches: [node]
          script:
            - npm ci
            - npm run build
          artifacts:
            - dist/**
      - parallel:
          - step:
              name: Unit tests
              script: [npm test]
          - step:
              name: Secret scan
              script:
                - pipe: atlassian/git-secrets-scan:0.5.1
      - step:
          name: Deploy to staging
          deployment: staging
          script:
            - ./deploy.sh --env staging
      - step:
          name: Smoke test
          script:
            - curl -f https://staging.example.com/healthz
      - step:
          name: Deploy to production
          deployment: production
          trigger: manual            # the approval gate
          script:
            - ./deploy.sh --env production
Diagram

GitHub vs. GitLab vs. Bitbucket — a Three-Way Comparison#

GitHub ActionsGitLab CI/CDBitbucket Pipelines
Pipeline file(s)Many independent filesTypically one .gitlab-ci.ymlOne bitbucket-pipelines.yml with named trigger sections
Default step/job schedulingParallel unless needs:Sequential-by-stage unless needs: (DAG)Fully sequential unless parallel:
Native matrix buildsYes (strategy.matrix)Yes (parallel:matrix)No — hand-written parallel steps only
Cross-repo reusable pipeline logicReusable WorkflowsCI/CD Components / CatalogNo first-party equivalent — YAML anchors (single-repo only) or Pipes (narrow, integration-focused)
Manual approval gateEnvironment required reviewersProtected environment + when: manualDeployment + trigger: manual (+ Deployment Permissions on paid plans)
Built-in SAST/DAST/dependency scanningOpt-in via GHAS + ActionsNear one-line include, deeply integratedThin — mostly third-party Pipes
OIDC to cloudpermissions: id-token: writeid_tokens:oidc: true on the step
CODEOWNERS-equivalent path-scoped reviewYes (CODEOWNERS)Yes (approval rules + eligible groups)No direct equivalent
Distinctive strengthEnormous third-party ecosystemDeepest built-in security/complianceDeepest native Jira/Atlassian-suite integration

The honest summary, stated plainly rather than diplomatically hedged: of the three platforms covered so far, Bitbucket Pipelines is functionally capable for straightforward build/test/deploy pipelines, but has the thinnest reusability story (no matrix, no cross-repo reusable-workflow equivalent) and the thinnest built-in security-scanning story of the three. Its case for adoption rests almost entirely on the Jira/Atlassian-ecosystem integration covered above — a team without that specific context has comparatively little unique reason to choose it over GitHub or GitLab on CI/CD capability alone, and a team with deep Jira usage should weigh that integration's real, hard-to-replicate value against these genuine capability gaps.

This is exactly the kind of tradeoff worth stating explicitly to a stakeholder making a real platform decision, rather than deferring to whichever platform a given engineer happens to have used most recently — the right choice depends on organizational context (an existing Atlassian investment, a compliance-driven need for built-in scanning, an open-source-heavy Marketplace dependency) far more than on any single feature-by-feature scorecard.


Common Mistakes#

MistakeWhy it's a problemFix
Assuming default and a matched branches: entry both runOnly the most specific matching pipeline definition runs per push, never bothDuplicate (or YAML-anchor-share) any logic that genuinely needs to run in both places
Hand-writing a large matrix without YAML anchorsMassive duplication across near-identical parallel stepsExtract the shared step shape into a definitions: anchor, reused via *alias
Expecting a Pipe to define multi-step custom logicA Pipe is one focused integration call, not a reusable job the way a GitHub reusable workflow isUse YAML anchors for shared custom logic; reserve Pipes for well-known external integrations
Storing a long-lived AWS key as a repository variable instead of using OIDCThe same indefinite-exposure-until-rotated risk covered for both prior platformsUse oidc: true and a scoped IAM trust policy instead
Relying on trigger: manual alone as "real" deployment governanceOn Free/Standard, it's a pause button, not an authorization control — anyone with pipeline access can click itOn Premium/Enterprise, also configure Deployment Permissions restricting who may trigger the deploy step
Assuming Bitbucket has GitLab-equivalent built-in DAST/SASTIt doesn't — the built-in story is materially thinnerBudget for third-party Pipes (Snyk, SonarQube) if deep scanning is a real requirement
Assuming a schedule attached to a custom pipeline lives in the YAMLSchedules are configured separately in repo settings, invisible from the file aloneCheck Repository Settings → Pipelines → Schedules when auditing what actually runs automatically
No health-check wait for a service container before running integration testsBitbucket provides no automatic wait-for-healthy gate, unlike GitHub's options: health checkHave the test suite (or an explicit retry loop in the script) handle initial connection retries itself
Expecting path-scoped CODEOWNERS-style mandatory reviewBitbucket has no direct core-product equivalentCompensate with branch restrictions' minimum-approvals count plus team process, or weigh this gap against GitHub/GitLab if it's a hard requirement

Worked Practice Problems#

Problem 1: A team's bitbucket-pipelines.yml has both a default: section running tests and a branches: { main: [...] } section also running the same tests plus a deploy step. They notice pushes to main only ever show the branches: main pipeline run, never a separate default run too, and ask if this is a bug. Is it, and why?

Answer: Not a bug — this is Bitbucket's designed behavior. Trigger-section matching picks the single most specific pipeline definition for a given push; a push to main matches the explicit branches: { main: ... } entry, which entirely replaces (not supplements) what default: would have run. If the team wants the exact same test logic in both places, it needs to be explicitly present in both sections (ideally via a shared YAML anchor to avoid drift) — default: is only ever a fallback for branches with no more specific match, never an "always also runs" baseline layered underneath a branch-specific pipeline.

Problem 2: A team needs to test their application against Node 18, 20, and 22, on both a Debian-based and an Alpine-based image — 6 total combinations — and is frustrated at how much boilerplate this takes compared to a GitHub Actions matrix they used on a previous project. Is there a way to reduce the duplication meaningfully, short of switching platforms?

Answer: Partially, not fully. YAML anchors can factor out the shared script logic (the actual test commands, caching config) so each of the 6 parallel: step entries differs only in its image: line and name:, meaningfully reducing the duplicated logic even though 6 separate step entries are still required in the YAML — Bitbucket has no mechanism that generates those 6 entries automatically from a compact declaration the way GitHub's strategy.matrix or GitLab's parallel:matrix does. This is a genuine, acknowledged platform limitation (covered earlier in this chapter) rather than a configuration mistake — for a team whose testing strategy leans heavily on wide matrix coverage specifically, this is a legitimate factor worth weighing in a platform choice, not something to work around indefinitely with cleverer YAML.

Problem 3: A product manager wants to know, without asking an engineer, whether a specific bug fix (Jira issue PROJ-456) has reached production yet. On a GitHub+Jira-integration setup this requires checking a third-party integration's own dashboard; describe how the same question is answered natively on Bitbucket, and why that's structurally easier.

Answer: On Bitbucket, the PM opens Jira issue PROJ-456 directly — Bitbucket's native Deployment tracking (assuming the fixing commit referenced the issue key, per the Smart Commits convention) automatically surfaces which environments that specific commit's pipeline has deployed to, right on the issue itself, with a link back to the actual pipeline run. This is structurally easier specifically because Bitbucket and Jira are the same vendor's products built to interlock by design — the deployment-tracking data flows natively, with no separate integration to configure, authenticate, or keep in sync, unlike a third-party GitHub-Jira integration which is inherently an external system polling or receiving webhooks from two products never built with each other in mind.

Problem 4: A team configures a nightly-security-scan custom pipeline and is confused a week later when it hasn't run even once, despite the YAML looking correct and a manual trigger from the UI working fine. What's the most likely cause, and how would you confirm it?

Answer: The most likely cause is that the custom pipeline was never actually attached to a schedule — on Bitbucket, defining a custom: pipeline in YAML only makes it triggerable; recurring execution requires a separate schedule configured in Repository Settings → Pipelines → Schedules, which is not part of the YAML file at all. Since a manual trigger from the UI works, the pipeline definition itself is confirmed correct — the gap is specifically the missing schedule attachment. Confirm by checking that settings page directly; this is exactly the "YAML alone under-reports what actually executes on a recurring basis" gap flagged earlier in this chapter, and a common trap for anyone used to GitHub's fully-inline schedule: cron: or assuming Bitbucket works the same way.


Summary and What's Next#

Bitbucket Pipelines structures CI/CD around a single bitbucket-pipelines.yml with named trigger sections (default, branches, pull-requests, custom) where the most specific match replaces rather than supplements less specific ones, and steps run sequentially by default unless explicitly grouped under parallel: or stage:. Pipes provide focused, well-known-integration reusability (the narrower analogue of a GitHub Action), while genuine cross-repository reusable-pipeline logic has no first-party equivalent — YAML anchors are the practical (single-repo-only) DRY mechanism. Deployments and Deployment variables implement the same environment-scoping and manual-approval-gate concepts covered for GitHub and GitLab, and OIDC eliminates long-lived cloud credentials via the same pattern established twice already in this series. Two genuine, honestly-stated gaps distinguish Bitbucket from the two prior platforms: no native matrix-build primitive, and a materially thinner built-in security-scanning story. Its standout, hard-to-replicate strength is deep, native Jira integration — Smart Commits and automatic deployment tracking surfaced directly on linked issues — which is the primary rational reason a team already invested in the Atlassian ecosystem would choose it.

Part 8, the final platform chapter in this series, covers Azure DevOps — Microsoft's entry, notable for both a YAML-pipeline model and an older classic/visual-designer pipeline model still in wide enterprise use, plus a closing four-way comparison table lining up GitHub, GitLab, Bitbucket, and Azure DevOps side by side against every dimension this series has now covered three times over.

A closing thought worth carrying into that final chapter, since it's the actual pattern running underneath all three platforms covered so far: every platform in this series solves the same underlying problems — trigger a pipeline off a Git event, run isolated units of work with some dependency ordering between them, pass data between those units, gate a deploy behind approval, authenticate to a cloud provider without a long-lived secret, and scope access so a compromised job can't reach more than it needs. The YAML keywords differ (on: vs. rules: vs. named trigger sections; needs: vs. needs: vs. sequential-by-default), and the depth of built-in tooling differs sharply (GitLab's integrated scanning suite vs. Bitbucket's thinner, Pipe-assembled equivalent), but the underlying CI/CD model — the one this series built tool-agnostically back in Part 1 — is the same model everywhere. Learning a fourth platform, or a fifth one this series never gets to, is mostly a matter of mapping its specific vocabulary onto that same underlying model, not learning CI/CD from scratch again.