Beyond Copy-Paste: Building Backstage with AI-Assisted Development
How Claude Sonnet 4.5 and GitHub Copilot helped us navigate the maze of custom Backstage integrations The Backstage Promise (and the Reality) Spotify's

If your team chose the forked repo model for maximum isolation—or due to regulatory/access concerns—your CI/CD strategy has special requirements. This article shows how to set up and automate deployment pipelines when each environment is a special-purpose repo fork.
dev, staging, prod) is a forked repository.
main.myapp-dev → myapp-staging → myapp-prod.In each repo (myapp-dev, myapp-staging, myapp-prod):
1# .github/workflows/deploy.yml
2name: Deploy
3
4on:
5 push:
6 branches: [main]
7
8jobs:
9 deploy:
10 runs-on: ubuntu-latest
11 steps:
12 - uses: actions/checkout@v3
13 - run: ./deploy.sh $ENV_NAME
$ENV_NAME could be set in repo secrets or CI environment variables.1stages:
2 - deploy
3
4deploy:
5 stage: deploy
6 script:
7 - ./deploy $ENV_NAME
You can have a “downstream pipeline” in myapp-dev creating a pull/merge-request in myapp-staging:
1on:
2 workflow_run:
3 workflows: ["Deploy"]
4 types:
5 - completed
6
7jobs:
8 promote:
9 env:
10 DOWNSTREAM_REPOSITORY: myapp-staging
11 TARGET_BRANCH: main
12 steps:
13 - run: |
14
15 git config --global "user.name" "Machine"
16 git config --global "user.email" "machine@infralovers.com"
17
18 export GIT_COMMIT_DATE=$(git log -1 --format=%cd --date=format:%Y%m%dh%H%M%S)
19 export GIT_COMMIT_MSG=$(git log -1 --format=%s)
20 export BRANCH_NAME="${GITHUB_REPOSITORY}-update-${GIT_COMMIT_DATE}"
21
22 git fetch -v origin
23 git remote add downstream https://x-token-auth:${GH_TOKEN}@${GITHUB_SERVER_URL}/${GITHUB_REPOSITORY_OWNER}/${DOWNSTREAM_REPOSITORY}
24 git fetch -v downstream
25 git checkout -b "${BRANCH_NAME}" "downstream/${TARGET_BRANCH}"
26 git config merge.ours.driver true
27 git merge origin/main --no-commit
28 git reset HEAD CHANGELOG.md .github
29 git checkout -- CHANGELOG.md .github
30 git status --short
31 export CI_COMMIT_PREFIX=$(echo $GIT_COMMIT_MSG | sed -E "s/(.*):.*/\1/")
32 git commit -m "${CI_COMMIT_PREFIX}:automatic update ${GITHUB_REPOSITORY}
33 $GIT_COMMIT_MSG"
34 git push -u downstream "${BRANCH_NAME}"
35
36 gh pr create --repo "${GITHUB_REPOSITORY_OWNER}/${DOWNSTREAM_REPOSITORY}"
37 --title "${CI_COMMIT_PREFIX}:automatic update from ${GITHUB_REPOSITORY}"
38 --head "${BRANCH_NAME}"
39 --base "${TARGET_BRANCH}"
1# In .gitlab-ci.yml of myapp-dev
2stages:
3 - deploy
4 - promote
5
6promote_staging:
7 stage: promote
8 image: python:3-alpine
9 variables:
10 DOWNSTREAM_REPOSITORY: myapp-staging
11 TARGET_BRANCH: main
12 rules:
13 - if: '$CI_COMMIT_BRANCH == "main"'
14 before_script:
15 - apk add --no-cache git
16 - git config --global "user.name" "Machine"
17 - git config --global "user.email" "machine@infralovers.com"
18 script:
19 - export BRANCH_NAME="${CI_PROJECT_NAME}-update-${CI_COMMIT_TIMESTAMP}"
20 - git fetch -v origin
21 - git remote add downstream https://gitlab-ci-token:${GL_TOKEN}@${CI_SERVER_HOST}/$DOWNSTREAM_REPOSITORY
22 - git fetch -v downstream
23 - git checkout -b "${BRANCH_NAME}" "downstream/${TARGET_BRANCH}"
24 - git config merge.ours.driver true
25 - git merge origin/main --no-commit
26 - git reset HEAD CHANGELOG.md .gitlab-ci.yml
27 - git checkout -- CHANGELOG.md .gitlab-ci.yml
28 - git status --short
29 - export CI_COMMIT_PREFIX=$(echo $CI_COMMIT_TITLE | sed -E "s/(.*):.*/\1/")
30 - git commit -m "${CI_COMMIT_PREFIX}:automatic update ${CI_PROJECT_NAME}
31 $CI_COMMIT_MESSAGE"
32 - git push -u downstream
33 -o merge_request.create
34 -o merge_request.target="${TARGET_BRANCH}"
35 -o merge_request.title="${CI_COMMIT_PREFIX}:automatic update from ${CI_PROJECT_NAME}"
36 -o merge_request.description="automatic update from ${CI_PROJECT_NAME}"
37 -o merge_request.remove_source_branch
38 "${BRANCH_NAME}"
Forked repos with separated environments are powerful for security but require thoughtful, often custom pipeline automation. If your needs shift toward speed and simplification, consider moving toward a trunk-based model within the next article in this series.
You are interested in our courses or you simply have a question that needs answering? You can contact us at anytime! We will do our best to answer all your questions.
Contact us