7 min read
One deploy.yml to Rule Them All: Branch-Aware CI/CD on Self-Hosted Runners
CI/CDGitHub ActionsGCPJava
Duplicated workflow files are a time bomb. Someone fixes Maven auth in deploy-dev.yml, forgets deploy-stage.yml, and three weeks later stage breaks "for no reason."
The consolidation pattern
One deploy.yml per repo. The branch decides the environment:
name: Deployon: push: branches: [dev, stage]jobs: deploy-dev: if: github.ref == 'refs/heads/dev' runs-on: [self-hosted, dev-runner] steps: - uses: actions/checkout@v4 - name: Build run: mvn -B clean package -DskipTests - name: Deploy service run: | sudo cp target/*.jar /opt/ala-microservices/app.jar sudo systemctl restart ala-service deploy-stage: if: github.ref == 'refs/heads/stage' runs-on: [self-hosted, stage-runner] steps: - uses: actions/checkout@v4 - name: Build run: mvn -B clean package -DskipTests - name: Deploy service run: | sudo cp target/*.jar /opt/ala-microservices/app.jar sudo systemctl restart ala-serviceSame file, two jobs, branch conditions doing the routing. Merging dev into stage promotes both the code and any pipeline improvements - drift becomes structurally impossible.
Why self-hosted runners
For deploys onto private GCP VMs, self-hosted runners on (or beside) the target servers mean:
- No SSH keys stored in GitHub secrets.
- No inbound firewall holes - runners make outbound connections to GitHub.
- Artifacts land directly where they run; no copy step across the internet.
The sharp edges
Three problems ate most of the migration time, so you can skip them:
- JAR naming. Maven's versioned artifact names break
systemdunits expecting a fixed path. Normalize at deploy time (cp target/*.jar app.jar), not in the unit file. - Runner permissions. The runner user needs *scoped* sudo - exactly
cpto one directory andsystemctl restarton named units. A blanketNOPASSWD: ALLon a CI user is an incident waiting for a headline. - Private Maven repos. Put credentials in a runner-local
settings.xml, not in the workflow, so tokens never transit GitHub's log pipeline.
The payoff: five repos, one mental model, and deploys boring enough that nobody watches them anymore. Boring is the goal.