To migrate Bitbucket Pipelines to GitHub Actions, treat each Bitbucket step as a GitHub job, not a GitHub step: a Bitbucket step starts its own Docker container, so the files it shares travel as artifacts, exactly as between GitHub jobs. Then map caches to actions/cache, services to services: with mapped ports, deployment to environment: plus concurrency:, each pipe to an action or a CLI call, and size to a runs-on label.

Each Bitbucket Pipelines concept and the GitHub Actions construct that replaces it, with the behavior that differs. The second row decides the shape of the file.
A bitbucket-pipelines.yml reads like a GitHub workflow with different key names, and that is the trap. The unit of isolation is different: in Bitbucket, each step is a fresh container with a fresh clone, while a GitHub job keeps one workspace across all of its steps.
Get that one mapping right and most of the file follows. This page gives the full concept map, a checklist of what to collect before you start, and a working before and after with a service database, a cache, a parallel group, an artifact and an OIDC deploy.
The concept map
The rows below follow Atlassian's step options reference and GitHub's workflow syntax, read 2026-11-12. Most rows are a rename; slow down where a Bitbucket step and a GitHub step behave differently, starting with the second row.
| Bitbucket Pipelines | GitHub Actions | What changes |
|---|---|---|
default, branches, pull-requests, tags, custom
|
on: push, pull_request, push: tags, workflow_dispatch
|
default runs on every push without a branch pipeline; GitHub has no fallback section |
step, its own Docker container |
A job, on its own runner | Files cross only as artifacts, in both |
script lines |
run: steps in one job |
Steps in one job share the workspace |
image |
container:, or a setup action |
Job containers need a Linux runner |
parallel |
Jobs with no needs, or a matrix |
Bitbucket fail-fast defaults to false, a GitHub matrix to true
|
caches, with key: files
|
actions/cache with a hashFiles() key |
Bitbucket saves under 1 GB compressed, clears after 1 week; GitHub 10 GB, 7 days unused |
artifacts |
upload-artifact, download-artifact
|
Bitbucket keeps 14 days and 1 GB; GitHub up to 90 days |
services |
services: |
Map ports: to reach it on localhost; Bitbucket allows 5 per step |
deployment, trigger: manual
|
environment: and concurrency:
|
Required reviewers on private repositories need GitHub Enterprise |
pipe |
uses: an action, or a CLI in run:
|
Pipe variables become with: inputs |
size: 1x to 32x
|
A runs-on label |
2x is 4 CPU and 8 GB; latchkey-small is 2 vCPU and 8 GB |
Bitbucket figures are from Atlassian Support: step options, cache dependencies, use artifacts in steps, databases and service containers, and deployment concurrency control. GitHub figures are from its workflow syntax and dependency caching references and the
retention-daysinput ofactions/upload-artifactv7.0.2. All read 2026-11-12.
The migration checklist
Collect these from the Bitbucket repository settings and the YAML before you write the workflow.
-
Labels. Every
size:value and every self-hostedruns-on:label. Atlassian's table gives 1x as 2 CPU and 4 GB, 2x as 4 CPU and 8 GB, and 4x and above on paid plans only, at a multiple of the 1x build minutes. - Secrets. Repository, workspace and deployment variables, marked Secured or not. Secured ones become GitHub secrets, deployment variables become environment secrets, and the rest become configuration variables.
-
Cache keys. Predefined caches such as
nodehave no key at all; Atlassian notes that predefined caches do not support file-based keys. Give each one ahashFiles()key inactions/cache, or use thecache:input of a setup action. -
Matrix. Bitbucket has no matrix: a
parallelblock of near-identical steps is one, written out by hand. Collapse it intostrategy.matrix, and setfail-fast: falseif a leg must not cancel the others. -
Artifacts. Every
artifacts:path, and which later step reads it. Steps that only exist to passdist/along can often merge into one job and drop the artifact. -
Concurrency. Bitbucket pauses a later deployment to an environment that already has one in progress, until someone resumes or reruns it. GitHub does nothing of the kind unless the deploy job sets
concurrency:, and onlyqueue: maxkeeps every waiting deploy. -
Max time.
max-timedefaults to 120 minutes in Bitbucket;timeout-minutesdefaults to 360 in GitHub. Carry the shorter limit across.
Before: bitbucket-pipelines.yml
Tests run on pull requests and on main against a Postgres service, a build runs beside them on main, and a manual deploy step pushes dist/ to S3 through the atlassian/aws-s3-deploy pipe with OIDC. Note that the Build step and the Deploy step are separate containers, so dist/** has to be declared as an artifact to reach the deploy.
image: node:22
definitions:
caches:
npm-lock:
key:
files:
- package-lock.json
path: ~/.npm
services:
postgres:
image: postgres:16
variables:
POSTGRES_PASSWORD: test
steps:
- step: &test
name: Test
caches:
- npm-lock
services:
- postgres
script:
- npm ci
- npm test
pipelines:
pull-requests:
'**':
- step: *test
branches:
main:
- parallel:
steps:
- step: *test
- step:
name: Build
caches:
- npm-lock
script:
- npm ci
- npm run build
artifacts:
- dist/**
- step:
name: Deploy
deployment: production
trigger: manual
oidc: true
script:
- pipe: atlassian/aws-s3-deploy:2.1.0
variables:
AWS_DEFAULT_REGION: 'us-east-1'
AWS_OIDC_ROLE_ARN: $AWS_DEPLOY_ROLE_ARN
S3_BUCKET: $S3_BUCKET
LOCAL_PATH: 'dist'
After: .github/workflows/ci.yml
Three jobs. test runs on both events, build and deploy only on a push to main. The Postgres service gets ports: 5432:5432, because the job runs on the runner rather than in a container, and that mapping is what makes localhost:5432 work as it did in Bitbucket. The health check holds the job until the database accepts connections.
The manual trigger becomes the production environment, which pauses the job for a reviewer when the environment has required reviewers; on a private repository that needs GitHub Enterprise, so check your plan. The pipe becomes aws-actions/configure-aws-credentials with id-token: write, followed by the AWS CLI call the pipe was making.
The concurrency: block stands in for Bitbucket's deployment lock. With queue: max, a second push to main waits for the first deploy and then starts on its own. Without it, GitHub keeps one pending run per group, and a third push cancels the second one while it waits.
name: ci
on:
push:
branches: [main]
pull_request:
permissions:
contents: read
jobs:
test:
runs-on: ubuntu-latest
services:
postgres:
image: postgres:16
env:
POSTGRES_PASSWORD: test
ports:
- 5432:5432
options: >-
--health-cmd pg_isready
--health-interval 10s
--health-timeout 5s
--health-retries 5
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- uses: actions/setup-node@949feb2413d6458794dcd2491c4babbbce0c15c1 # v7.1.0
with:
node-version: '22'
cache: npm
- run: npm ci
- run: npm test
build:
if: github.event_name == 'push'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- uses: actions/setup-node@949feb2413d6458794dcd2491c4babbbce0c15c1 # v7.1.0
with:
node-version: '22'
cache: npm
- run: npm ci && npm run build
- uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
with:
name: dist
path: dist
deploy:
needs: [test, build]
runs-on: ubuntu-latest
environment: production
concurrency:
group: production
queue: max
permissions:
contents: read
id-token: write
steps:
- uses: actions/download-artifact@9000827ccba6bdab643e8b6fd33ac0654aef8333 # v8.0.2
with:
name: dist
path: dist
- uses: aws-actions/configure-aws-credentials@e1253824e5c10ff9df46874f81ed3ec929e19cfd # v6.3.0
with:
role-to-assume: ${{ vars.AWS_DEPLOY_ROLE_ARN }}
aws-region: us-east-1
- run: aws s3 sync dist "s3://${{ vars.S3_BUCKET }}/"
Bitbucket's paused deployments are from Atlassian Support, deployment concurrency control. The default
queue: singleandqueue: max(up to 100 pending) are from GitHub Docs: Control the concurrency of workflows and jobs. Both read 2026-11-12.
Pipes are the slow part
Atlassian describes a pipe as a script that lives in a Docker container, configured through variables:. There is no automatic translation, but most popular pipes wrap a vendor CLI, and the vendor usually publishes a GitHub action for the same job: credentials from one action, then the CLI in run:. Read the pipe's README for the command it runs and the defaults it sets; aws-s3-deploy, for example, documents a DELETE_FLAG that defaults to false, which is why the sync above has no --delete.
Bitbucket's OIDC token arrives as BITBUCKET_STEP_OIDC_TOKEN when a step sets oidc: true. On GitHub the token is requested by the action itself, and only when the job grants id-token: write. Your cloud's trust policy needs a new provider and a new subject either way, because the issuer changes.
Caches: from named caches to keys
Bitbucket saves a cache only when it is empty and the build succeeds, and clears it after a week. actions/cache saves when no entry matches the exact key, which is why the key must change when the lockfile does. For npm, the cache: npm input on actions/setup-node does this for you; for anything else, write the key yourself.
```.github/workflows/ci.yml (cache step)
- uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 with: path: ~/.npm key: npm-${{ runner.os }}-${{ hashFiles('package-lock.json') }} restore-keys: npm-${{ runner.os }}- ```
What GitHub Actions Importer covers
GitHub Actions Importer has a bitbucket subcommand, covered with its limits on GitHub Actions Importer. It is worth running for the triggers, the steps and the caches. Check what it writes against the concept map: the step-to-job boundary, the service ports and the deployment concurrency are where a mechanical conversion is most likely to be correct YAML and wrong behavior.
The pipe's
aws s3 synccall andDELETE_FLAGdefault: theatlassian/aws-s3-deploy2.1.0 README andpipe.sh, read 2026-11-12.
FAQ
What is the GitHub Actions equivalent of a Bitbucket pipe?
An action, used with uses: and configured with with:. Most pipes wrap a vendor CLI, so the usual translation is the vendor's credentials action followed by the same CLI command in a run: step, which also makes the command visible in the workflow file.
How do I run steps in parallel in GitHub Actions like Bitbucket parallel?
Make them separate jobs with no needs: between them; GitHub starts independent jobs at the same time. When the parallel steps differ only in one value, such as a test shard, use strategy.matrix, and set fail-fast: false to keep Bitbucket's default of letting the other legs finish.
How do I pass files between jobs in GitHub Actions?
Upload them with actions/upload-artifact in the job that makes them, and download them by name with actions/download-artifact in a job that lists the first one under needs:. Steps in one job need none of this, because they share the workspace, which is why merging two Bitbucket steps into one job can drop an artifact entirely.
How do I use a service container like Postgres in GitHub Actions?
Declare it under the job's services: with an image: and env:. When the job runs directly on the runner, map the port with ports: - 5432:5432 and connect to localhost; when the job runs in a container:, reach the service by its label as the host name instead.
References
- Atlassian Support: Bitbucket Pipelines step options, size and max-time (verified 2026-11-12)
- Atlassian Support: cache dependencies in Bitbucket Pipelines (verified 2026-11-12)
- Atlassian Support: use artifacts in steps, 14 days and 1 GB (verified 2026-11-12)
- GitHub Docs: workflow syntax, services, containers and timeouts (verified 2026-11-12)
- GitHub Docs: dependency caching reference, limits and eviction (verified 2026-11-12)
Top comments (0)