Pipeline Automation for Forked Repository Environment Management


Bicycle

Introduction

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.


Forked Repo Recap

  • Model: Each environment (dev, staging, prod) is a forked repository.
  • Promotion: Code travels via PRs (pull/merge requests) across repositories.
  • Each repo has isolated configuration, history, secrets, and permissions.


Pipeline Principles

  • Each repo maintains its own CI/CD pipeline, deploying only its environment on main.
  • Promotions may be a manual process or automated cross-repo PR creation (with bots or scripts).
  • Sync/Promotion workflow is the hardest challenge.

Automating Promotion

  • Developers create PRs from myapp-devmyapp-stagingmyapp-prod.
  • Optionally, use bots/scripts to automate opening PRs in the upstream forks after approvals.

Example: GitHub Actions in Forked Repos

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.

Example: GitLab CI/CD

1stages:
2    - deploy
3
4deploy:
5    stage: deploy
6    script:
7    - ./deploy $ENV_NAME

Example: GitLab Multi-Project Pipeline Automation

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}"

Gotchas & Best Practices

  • Fragmented history: Track promotion PRs carefully.
  • Duplicated config: Keep CI/CD files and deployment logic in sync across repos.
  • Automate as much as possible: Otherwise, promotion will be slow and error-prone.
  • Audit and rollbacks: Cross-repo PRs should always be code-reviewed and logged for compliance.

Conclusion

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.

Go Back explore our courses

We are here for you

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