Comprehensive guide for migrating from inline GitHub Actions commands to git-flow reusable workflows.
- Why Migrate?
- Migration Strategy
- Docker Workflows
- Security Workflows
- Kubernetes/Helm Workflows
- Terraform Workflows
- GitOps Workflows
- Common Migration Patterns
- Troubleshooting
Before (Inline Commands):
- ❌ Duplicate code across multiple repositories
- ❌ Manual action version updates (security risk)
- ❌ Inconsistent security scanning practices
- ❌ No centralized workflow improvements
- ❌ Difficult to enforce organizational standards
After (Reusable Workflows):
- ✅ Single source of truth for CI/CD patterns
- ✅ Automatic action updates via Renovate
- ✅ Consistent security practices across all repos
- ✅ Centralized improvements benefit all consumers
- ✅ Enforced organizational standards
- ✅ Reduced maintenance burden (update once, benefit everywhere)
| Workflow Type | Effort | Time Estimate |
|---|---|---|
| Docker | Low | 15-30 minutes |
| Security | Low | 10-20 minutes |
| Kubernetes/Helm | Medium | 30-60 minutes |
| Terraform | Medium | 45-90 minutes |
| GitOps | High | 1-3 hours |
- Identify: Audit existing workflows and identify migration candidates
- Prioritize: Start with high-value, low-risk workflows (Docker, Security)
- Test: Create PR with reusable workflow, test thoroughly
- Deploy: Merge PR after validation
- Monitor: Watch first few runs for issues
- Iterate: Move to next workflow
- Run in parallel: Keep old workflow temporarily, run both side-by-side
- Feature flags: Use workflow_dispatch to test before enabling on push/PR
- Rollback plan: Keep old workflow committed but disabled for quick rollback
- Gradual rollout: Migrate one repository at a time, not all at once
Before (Inline Commands):
name: Build Docker Image
on:
push:
branches: [main]
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Log in to GitHub Container Registry
uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Extract metadata
id: meta
uses: docker/metadata-action@v5
with:
images: ghcr.io/${{ github.repository }}
- name: Build and push
uses: docker/build-push-action@v6
with:
context: .
push: true
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}After (Reusable Workflow):
name: Build Docker Image
on:
push:
branches: [main]
jobs:
build:
uses: samuelho-dev/git-flow/.github/docker-build-push.yml@v1
with:
image: my-app
push: true
secrets: inheritSavings: 30 lines → 10 lines (67% reduction)
Before (Inline Commands):
name: Build and Scan
on:
push:
branches: [main]
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Log in to GHCR
uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Build and push
uses: docker/build-push-action@v6
with:
context: .
push: true
tags: ghcr.io/${{ github.repository }}:${{ github.sha }}
- name: Run Trivy scanner
uses: aquasecurity/trivy-action@master
with:
image-ref: ghcr.io/${{ github.repository }}:${{ github.sha }}
format: 'sarif'
output: 'trivy-results.sarif'
- name: Upload Trivy results
uses: github/codeql-action/upload-sarif@v3
with:
sarif_file: 'trivy-results.sarif'
- name: Install Cosign
uses: sigstore/cosign-installer@v3
- name: Sign image
run: |
cosign sign --yes ghcr.io/${{ github.repository }}:${{ github.sha }}After (Reusable Workflow):
name: Build and Scan
on:
push:
branches: [main]
jobs:
build:
uses: samuelho-dev/git-flow/.github/docker-build-push.yml@v1
with:
image: my-app
push: true
scan: true # Trivy scanning
sign: true # Cosign signing
sbom: true # SBOM generation
secrets: inheritSavings: 45 lines → 13 lines (71% reduction)
Before (Inline Commands):
name: Multi-Platform Build
on:
push:
tags:
- 'v*'
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Set up QEMU
uses: docker/setup-qemu-action@v3
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Log in to GHCR
uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Build and push
uses: docker/build-push-action@v6
with:
context: .
platforms: linux/amd64,linux/arm64,linux/arm/v7
push: true
tags: |
ghcr.io/${{ github.repository }}:${{ github.ref_name }}
ghcr.io/${{ github.repository }}:latest
cache-from: type=registry,ref=ghcr.io/${{ github.repository }}:buildcache
cache-to: type=registry,ref=ghcr.io/${{ github.repository }}:buildcache,mode=maxAfter (Reusable Workflow):
name: Multi-Platform Build
on:
push:
tags:
- 'v*'
jobs:
build:
uses: samuelho-dev/git-flow/.github/docker-build-push.yml@v1
with:
image: my-app
platforms: linux/amd64,linux/arm64,linux/arm/v7
push: true
cache-registry: true
secrets: inheritSavings: 40 lines → 12 lines (70% reduction)
Before (Inline Commands):
name: Security Scan
on:
push:
branches: [main]
schedule:
- cron: '0 2 * * 1'
jobs:
scan:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Run Trivy vulnerability scanner
uses: aquasecurity/trivy-action@master
with:
scan-type: 'fs'
scan-ref: '.'
format: 'sarif'
output: 'trivy-results.sarif'
severity: 'HIGH,CRITICAL'
- name: Upload Trivy results to GitHub Security
uses: github/codeql-action/upload-sarif@v3
with:
sarif_file: 'trivy-results.sarif'After (Reusable Workflow):
name: Security Scan
on:
push:
branches: [main]
schedule:
- cron: '0 2 * * 1'
jobs:
scan:
uses: samuelho-dev/git-flow/.github/trivy-scan.yml@v1
with:
scan-type: fs
scan-ref: .
severity: HIGH,CRITICALSavings: 25 lines → 10 lines (60% reduction)
Before (Inline Commands):
name: Secret Scan
on:
push:
pull_request:
jobs:
scan:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Run Gitleaks
uses: gitleaks/gitleaks-action@v2
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
GITLEAKS_LICENSE: ${{ secrets.GITLEAKS_LICENSE }}After (Reusable Workflow):
name: Secret Scan
on:
push:
pull_request:
jobs:
scan:
uses: samuelho-dev/git-flow/.github/gitleaks-scan.yml@v1
with:
fail-on-findings: trueSavings: 18 lines → 8 lines (56% reduction)
Before (Inline Commands):
name: Generate SBOM
on:
push:
branches: [main]
jobs:
sbom:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Generate SBOM
uses: anchore/sbom-action@v0
with:
path: .
format: spdx-json
output-file: sbom.spdx.json
- name: Scan SBOM for vulnerabilities
uses: anchore/scan-action@v3
with:
sbom: sbom.spdx.json
fail-build: false
- name: Upload SBOM
uses: actions/upload-artifact@v4
with:
name: sbom
path: sbom.spdx.jsonAfter (Reusable Workflow):
name: Generate SBOM
on:
push:
branches: [main]
jobs:
sbom:
uses: samuelho-dev/git-flow/.github/sbom-generate.yml@v1
with:
target-type: directory
target: .
format: spdx-json
scan-sbom: trueSavings: 28 lines → 11 lines (61% reduction)
Before (Inline Commands):
name: Helm Lint
on:
push:
paths:
- 'charts/**'
jobs:
lint:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Set up Helm
uses: azure/setup-helm@v4
with:
version: 'v3.14.0'
- name: Lint Helm chart
run: |
helm lint charts/my-app
- name: Install kubeconform
run: |
wget https://github.com/yannh/kubeconform/releases/latest/download/kubeconform-linux-amd64.tar.gz
tar xf kubeconform-linux-amd64.tar.gz
sudo mv kubeconform /usr/local/bin
- name: Template and validate
run: |
helm template charts/my-app | kubeconform -strict -kubernetes-version 1.30.0After (Reusable Workflow):
name: Helm Lint
on:
push:
paths:
- 'charts/**'
jobs:
lint:
uses: samuelho-dev/git-flow/.github/helm-lint.yml@v1
with:
chart-path: charts/my-app
kubeconform: true
kubernetes-version: '1.30.0'
strict: trueSavings: 30 lines → 12 lines (60% reduction)
Before (Inline Commands):
name: Helm Test
on:
pull_request:
paths:
- 'charts/**'
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Set up Helm
uses: azure/setup-helm@v4
- name: Install unittest plugin
run: |
helm plugin install https://github.com/helm-unittest/helm-unittest
- name: Run tests
run: |
helm unittest charts/my-appAfter (Reusable Workflow):
name: Helm Test
on:
pull_request:
paths:
- 'charts/**'
jobs:
test:
uses: samuelho-dev/git-flow/.github/helm-test.yml@v1
with:
chart-path: charts/my-app
output-format: junitSavings: 20 lines → 10 lines (50% reduction)
Before (Inline Commands):
name: Publish Helm Chart
on:
push:
branches: [main]
paths:
- 'charts/**'
jobs:
publish:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Set up Helm
uses: azure/setup-helm@v4
- name: Log in to GHCR
run: |
echo "${{ secrets.GITHUB_TOKEN }}" | helm registry login ghcr.io -u ${{ github.actor }} --password-stdin
- name: Package chart
run: |
helm package charts/my-app
- name: Push chart
run: |
helm push my-app-*.tgz oci://ghcr.io/${{ github.repository_owner }}/chartsAfter (Reusable Workflow):
name: Publish Helm Chart
on:
push:
branches: [main]
paths:
- 'charts/**'
jobs:
publish:
uses: samuelho-dev/git-flow/.github/helm-publish.yml@v1
with:
chart-path: charts/my-app
registry: ghcr.io
secrets: inheritSavings: 30 lines → 12 lines (60% reduction)
Before (Inline Commands):
name: Terraform Validate
on:
pull_request:
paths:
- 'terraform/**'
jobs:
validate:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Setup Terraform
uses: hashicorp/setup-terraform@v3
with:
terraform_version: 1.9.8
- name: Terraform Init
run: terraform init
working-directory: terraform/
- name: Terraform Format Check
run: terraform fmt -check -recursive
working-directory: terraform/
- name: Terraform Validate
run: terraform validate
working-directory: terraform/
- name: Run tfsec
uses: aquasecurity/tfsec-action@v1.0.3
with:
working_directory: terraform/After (Reusable Workflow):
name: Terraform Validate
on:
pull_request:
paths:
- 'terraform/**'
jobs:
validate:
uses: samuelho-dev/git-flow/.github/terraform-validate.yml@v1
with:
terraform-path: terraform/
terraform-version: 1.9.8
fmt-check: true
tfsec-scan: true
secrets:
terraform-token: ${{ secrets.TF_API_TOKEN }}Savings: 32 lines → 14 lines (56% reduction)
Before (Inline Commands):
name: Terraform Plan
on:
pull_request:
paths:
- 'terraform/**'
jobs:
plan:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Setup Terraform
uses: hashicorp/setup-terraform@v3
with:
terraform_version: 1.9.8
cli_config_credentials_token: ${{ secrets.TF_API_TOKEN }}
- name: Configure AWS credentials
uses: aws-actions/configure-aws-credentials@v4
with:
aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
aws-region: us-east-1
- name: Terraform Init
run: terraform init
working-directory: terraform/
- name: Terraform Plan
run: terraform plan -out=tfplan
working-directory: terraform/
- name: Save plan
uses: actions/upload-artifact@v4
with:
name: tfplan
path: terraform/tfplanAfter (Reusable Workflow):
name: Terraform Plan
on:
pull_request:
paths:
- 'terraform/**'
jobs:
plan:
uses: samuelho-dev/git-flow/.github/terraform-plan.yml@v1
with:
terraform-path: terraform/
terraform-version: 1.9.8
upload-plan: true
enable-infracost: true
post-pr-comment: true
secrets:
terraform-token: ${{ secrets.TF_API_TOKEN }}
aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
infracost-api-key: ${{ secrets.INFRACOST_API_KEY }}Savings: 40 lines → 18 lines (55% reduction)
Added Benefits:
- 💰 Infracost cost estimation
- 💬 Automated PR comments with plan summary
- 📊 Resource change statistics
Before (Inline Commands):
name: Terraform Apply
on:
push:
branches: [main]
paths:
- 'terraform/**'
jobs:
apply:
runs-on: ubuntu-latest
environment: production
steps:
- uses: actions/checkout@v4
- name: Setup Terraform
uses: hashicorp/setup-terraform@v3
with:
terraform_version: 1.9.8
cli_config_credentials_token: ${{ secrets.TF_API_TOKEN }}
- name: Configure AWS credentials
uses: aws-actions/configure-aws-credentials@v4
with:
aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
aws-region: us-east-1
- name: Terraform Init
run: terraform init
working-directory: terraform/
- name: Terraform Apply
run: terraform apply -auto-approve
working-directory: terraform/After (Reusable Workflow):
name: Terraform Apply
on:
push:
branches: [main]
paths:
- 'terraform/**'
jobs:
apply:
uses: samuelho-dev/git-flow/.github/terraform-apply.yml@v1
with:
terraform-path: terraform/
terraform-version: 1.9.8
environment: production
backup-state: true
secrets:
terraform-token: ${{ secrets.TF_API_TOKEN }}
aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}Savings: 35 lines → 16 lines (54% reduction)
Added Benefits:
- 💾 Automatic state backup before apply
- ✅ Post-apply validation
- 📊 Resource change tracking
Before (Manual Process):
- Build Docker image
- Manually edit Kubernetes YAML files
- Change image tags in multiple files
- Commit changes
- Push to repository
- Wait for ArgoCD auto-sync (or manually sync)
After (Automated Workflow):
name: GitOps Deployment
on:
push:
branches: [main]
jobs:
# Build image
build:
uses: samuelho-dev/git-flow/.github/docker-build-push.yml@v1
with:
image: my-app
push: true
secrets: inherit
# Update manifests automatically
update-manifests:
needs: build
uses: samuelho-dev/git-flow/.github/gitops-update-manifests.yml@v1
with:
manifest-path: deploy/k8s/production
update-type: image
image-name: ghcr.io/${{ github.repository_owner }}/my-app
image-tag: sha-${{ github.sha }}
# Sync ArgoCD automatically
argocd-sync:
needs: update-manifests
uses: samuelho-dev/git-flow/.github/argocd-sync.yml@v1
with:
argocd-server: argocd.example.com
argocd-app-name: my-app-production
wait-for-sync: true
health-check: true
secrets:
argocd-token: ${{ secrets.ARGOCD_TOKEN }}Benefits:
- ✅ Fully automated deployment pipeline
- ✅ No manual manifest editing
- ✅ Consistent image tag updates
- ✅ Automated ArgoCD sync with health checks
- ✅ Complete audit trail in Git
Before (Manual Process):
# Manual steps
yq eval '.image.tag = "v1.2.3"' -i values-production.yaml
git add values-production.yaml
git commit -m "Update image tag to v1.2.3"
git pushAfter (Automated Workflow):
name: Update Helm Values
on:
workflow_dispatch:
inputs:
environment:
type: choice
options: [dev, staging, production]
helm-key:
type: string
helm-value:
type: string
jobs:
update:
uses: samuelho-dev/git-flow/.github/gitops-update-manifests.yml@v1
with:
manifest-path: deploy/helm/environments/${{ inputs.environment }}
update-type: helm-values
helm-key: ${{ inputs.helm-key }}
helm-value: ${{ inputs.helm-value }}
create-pr: trueBenefits:
- ✅ No local yq/kubectl required
- ✅ Creates PR for review
- ✅ Consistent commit messages
- ✅ Automated validation
Run old and new workflows side-by-side during migration:
name: Migration Testing
on:
push:
branches: [main]
jobs:
# Old workflow (to be deprecated)
build-old:
name: Build (Old - Deprecated)
runs-on: ubuntu-latest
steps:
# ... existing inline steps ...
# New reusable workflow
build-new:
name: Build (New - Testing)
uses: samuelho-dev/git-flow/.github/docker-build-push.yml@v1
with:
image: my-app
push: true
secrets: inherit
# Comparison step
compare:
name: Compare Results
needs: [build-old, build-new]
runs-on: ubuntu-latest
steps:
- name: Compare outputs
run: |
echo "Both workflows completed successfully"
echo "Old: ${{ needs.build-old.result }}"
echo "New: ${{ needs.build-new.result }}"Use workflow_dispatch to test new workflow before enabling on push:
name: Feature Flag Migration
on:
push:
branches: [main]
workflow_dispatch:
inputs:
use-reusable-workflow:
description: 'Use new reusable workflow'
type: boolean
default: false
jobs:
build-inline:
name: Build (Inline)
if: github.event_name == 'push' || !inputs.use-reusable-workflow
runs-on: ubuntu-latest
steps:
# ... inline steps ...
build-reusable:
name: Build (Reusable)
if: github.event_name == 'workflow_dispatch' && inputs.use-reusable-workflow
uses: samuelho-dev/git-flow/.github/docker-build-push.yml@v1
with:
image: my-app
secrets: inheritMigrate one job at a time:
name: Gradual Migration
on:
push:
branches: [main]
jobs:
# ✅ Migrated: Using reusable workflow
build:
uses: samuelho-dev/git-flow/.github/docker-build-push.yml@v1
with:
image: my-app
secrets: inherit
# ⏳ Not migrated yet: Still using inline steps
deploy:
needs: build
runs-on: ubuntu-latest
steps:
# ... inline deployment steps ...
# ⏳ Not migrated yet: Still using inline steps
notify:
needs: deploy
runs-on: ubuntu-latest
steps:
# ... inline notification steps ...Symptom:
Error: samuelho-dev/git-flow/.github/docker-build-push.yml@v1 not found
Solution:
- Verify repository name is correct
- Ensure workflow file exists at specified path
- Check you're using correct version tag (
@v1vs@main) - Verify repository is public or you have access
Symptom:
Error: Input required and not supplied: registry-password
Solution:
Use secrets: inherit to pass all secrets:
jobs:
build:
uses: samuelho-dev/git-flow/.github/docker-build-push.yml@v1
with:
image: my-app
secrets: inherit # ← Add thisOr pass secrets explicitly:
jobs:
build:
uses: samuelho-dev/git-flow/.github/docker-build-push.yml@v1
with:
image: my-app
secrets:
registry-password: ${{ secrets.GITHUB_TOKEN }}Symptom:
Error: Resource not accessible by integration
Solution:
Add required permissions to calling workflow:
name: Build
on:
push:
permissions:
contents: read
packages: write # ← For GHCR push
security-events: write # ← For SARIF uploads
id-token: write # ← For OIDC/Cosign
jobs:
build:
uses: samuelho-dev/git-flow/.github/docker-build-push.yml@v1
# ...Symptom:
Error: ${{ github.repository }} not available in this context
Solution:
Pass GitHub context as inputs:
jobs:
build:
uses: samuelho-dev/git-flow/.github/docker-build-push.yml@v1
with:
image: ${{ github.event.repository.name }} # ← Evaluate in caller
push: ${{ github.ref == 'refs/heads/main' }} # ← Conditional logicSymptom:
Error: Unable to find artifact for name: terraform-plan-abc123
Solution:
Ensure artifact names match exactly:
jobs:
plan:
uses: samuelho-dev/git-flow/.github/terraform-plan.yml@v1
with:
upload-plan: true # Uploads as terraform-plan-${{ github.sha }}
apply:
needs: plan
uses: samuelho-dev/git-flow/.github/terraform-apply.yml@v1
with:
plan-artifact-name: terraform-plan-${{ github.sha }} # ← Must matchSymptom: Workflow completes successfully but expected outputs are missing.
Solution:
Check input parameters are being passed correctly:
# ❌ Wrong: Missing required inputs
jobs:
build:
uses: samuelho-dev/git-flow/.github/docker-build-push.yml@v1
secrets: inherit
# ✅ Correct: All required inputs provided
jobs:
build:
uses: samuelho-dev/git-flow/.github/docker-build-push.yml@v1
with:
image: my-app # ← Required input
push: true
secrets: inheritUse this checklist to track your migration progress:
- Audit existing workflows
- Identify migration candidates
- Review USAGE.md documentation
- Review EXAMPLES.md for patterns
- Set up test repository for validation
- Create migration branch
- Update workflow file
- Test with workflow_dispatch
- Compare old vs new outputs
- Update documentation/README
- Create PR with migration
- Monitor first 3-5 runs
- Verify expected outputs
- Check artifact retention
- Update team documentation
- Remove old workflow (after 2 weeks)
- Keep old workflow committed but disabled
- Document rollback procedure
- Test rollback process
- Set calendar reminder to clean up old workflow
- Documentation: USAGE.md | EXAMPLES.md
- Issues: GitHub Issues
- Discussions: GitHub Discussions
Migration Support
If you encounter issues during migration:
- Check Troubleshooting section
- Search existing issues
- Create new issue with
migrationlabel
Last Updated: 2025-01-20