En esta guía (13 elementos)
GitHub Actions CI/CD
GitHub Actions — Reusable Workflow CI/CD Guideline
House standard for CI/CD on GitHub Actions, organized as a central library of
reusable workflows (workflow_call) composed by thin per-repo callers. One
platform repo owns the workflows; product repos invoke them. This keeps pipeline
logic DRY, versioned, and auditable across every stack.
Examples use placeholders —
${ORG}(GitHub org),<stack>(java/krakend/ react/…), generic secret names. Replace with your real values; never commit account IDs, ARNs, or tokens.
1. Principles
- Reusable-first. Pipeline logic lives in
workflow_callworkflows in a central${ORG}/ci-templatesrepo. Product repos hold a ~20-line caller only. - Compose, don’t copy. A
*-main-pipeline.ymlorchestrates atomic reusable workflows viauses:+needs:. No step is duplicated across stacks. - Stack taxonomy. Files named
<stack>-<concern>.yml; cross-stack onesshared-<concern>.yml. Predictable, greppable. - Everything gated, everything observable. Quality/security checks gate artifact+deploy; failures open issues and notify chat.
- Least privilege. Explicit
permissions:per workflow; pin actions; prefer OIDC over long-lived cloud keys. - Branch protection as code. Rulesets (JSON) version-control the merge rules.
2. Reusable workflow contract (workflow_call)
Every reusable workflow declares a typed interface: inputs (with
type/required/default), secrets, and outputs wired from job/step
outputs. Callers stay declarative.
# .github/workflows/java-build.yml (reusable)name: Java - Buildon: workflow_call: inputs: runner: { type: string, required: false, default: 'ubuntu-latest' } java_version: { type: string, required: false, default: '21' } java_distribution: { type: string, required: false, default: 'temurin' } run_spotless: { type: boolean, required: false, default: true } upload_artifact: { type: boolean, required: false, default: true } secrets: GH_PACKAGES_USERNAME: { required: false } GH_PACKAGES_TOKEN: { required: false } outputs: artifact_name: description: 'Name of the uploaded JAR artifact' value: ${{ jobs.build.outputs.artifact_name }}
jobs: build: runs-on: ${{ inputs.runner }} timeout-minutes: 15 permissions: contents: read outputs: artifact_name: ${{ steps.jar-info.outputs.name }} steps: - uses: actions/checkout@v5 - uses: actions/setup-java@v5 with: distribution: ${{ inputs.java_distribution }} java-version: ${{ inputs.java_version }} - name: Build env: GH_PACKAGES_USERNAME: ${{ secrets.GH_PACKAGES_USERNAME }} GH_PACKAGES_TOKEN: ${{ secrets.GH_PACKAGES_TOKEN }} run: ./gradlew clean build -x test --no-daemon --build-cache --parallelRules
- Inputs optional with sane defaults; boolean flags toggle stages.
- Declare each secret consumed; never read undeclared secrets.
- Expose outputs from step IDs (
steps.<id>.outputs.<k>→ job output → workflow output) so orchestrators can chain them. - Set
timeout-minutesand a minimalpermissions:on every job.
3. Pipeline orchestration
A top-level pipeline composes reusable workflows. Stages run in parallel unless
ordered by needs:; feature flags (run_* inputs) turn stages on/off; gating
if: guards artifact/deploy on upstream success.
# java-main-pipeline.yml (orchestrator, itself workflow_call)jobs: commit-lint: if: inputs.run_commit_lint uses: ./.github/workflows/shared-commit-lint.yml
build: if: inputs.run_build uses: ./.github/workflows/java-build.yml with: { upload_artifact: ${{ inputs.run_artifact }} } secrets: inherit
test: if: inputs.run_test needs: build uses: ./.github/workflows/java-test.yml secrets: inherit
artifact-ecr: if: | inputs.run_artifact && always() && needs.build.result == 'success' && (needs.test.result == 'success' || needs.test.result == 'skipped') needs: [build, test] uses: ./.github/workflows/shared-artifact-docker-ecr.yml with: artifact_name: ${{ needs.build.outputs.artifact_name }} image_tag: ${{ inputs.image_tag }} secrets: inherit
deploy: if: inputs.run_deploy needs: [artifact-ecr] uses: ./.github/workflows/shared-deploy-eks.yml with: { image_tag: ${{ needs.artifact-ecr.outputs.image_tag }} } secrets: inheritJob graph
commit-lint security dependency-review (parallel, no deps) └─► build ─► test · owasp · code-analysis (parallel on build) └─► artifact-* (gated on all checks green/skipped) └─► deploy ─► release ─► tag └─► notify / create-issue-on-failureCaller in a product repo — thin, just triggers + flags:
on: push: branches: [main, develop] paths-ignore: ['README.md', '.gitignore'] workflow_dispatch:concurrency: group: ${{ github.repository }}-${{ github.ref }} cancel-in-progress: truejobs: pipeline: uses: ${ORG}/ci-templates/.github/workflows/java-main-pipeline.yml@v1 with: run_test: true run_artifact: true run_deploy: true secrets: inheritRules
- Pass data between jobs only via
needs.<job>.outputs.<key>. - Use
always() && needs.X.result == 'success'gates so skipped optional stages don’t block, but failed required stages do. - Set
concurrencyto cancel superseded runs per ref. - A separate
*-pr-pipeline.ymlruns lint/build/test/security onpull_request(no artifact/deploy).
4. Naming & taxonomy
| Prefix | Scope |
|---|---|
java-*, react-*, krakend-*, nginx-*, contracts-* |
stack-specific |
shared-* |
cross-stack (commit-lint, semver, release, artifact, deploy, notify) |
security-* |
dependency review, secret scanning |
<stack>-main-pipeline.yml / <stack>-pr-pipeline.yml |
orchestrators |
Concerns: build, test, commit-lint, semver, artifact-docker-*,
artifact-dependency-*, deploy-*, delete-branch, create-issue-on-failure,
slack-notify, validate-source-branch, tag-release, release.
Promote a workflow to shared-* the moment a second stack needs it.
5. SemVer & release (from conventional commits)
Version is derived from commit history; tags + releases are automated. Pairs with
git-workflow.md (conventional commits) — feat → minor,
fix → patch, BREAKING CHANGE/MAJOR → major.
# shared-semver.yml (reusable)on: workflow_call: inputs: tag_prefix: { type: string, default: 'v' } major_pattern: { type: string, default: '(MAJOR|BREAKING CHANGE)' } minor_pattern: { type: string, default: '(feat)' } outputs: version: { value: ${{ jobs.semver.outputs.version }} }jobs: semver: runs-on: ubuntu-latest outputs: { version: ${{ steps.ver.outputs.version }} } steps: - uses: actions/checkout@v5 with: { fetch-depth: 0 } # full history for version calc - id: ver uses: PaulHatch/semantic-version@v5.4.0 with: { tag_prefix: ${{ inputs.tag_prefix }} }Release stage tags vX.Y.Z, generates a changelog from commits, and creates the
GitHub Release — runs only after a successful deploy. Tags are immutable (§8).
6. Security gates
Run as parallel jobs that gate artifact/deploy; keep them
permissions: contents: read unless they must write.
| Check | Action | Trigger | Gate |
|---|---|---|---|
| Dependency review | actions/dependency-review-action@v5 |
PR only | fail-on-severity: high + license allow/deny |
| Secret scan | trufflesecurity/trufflehog@<sha> (SHA-pinned) |
push + PR | --only-verified, fail on findings |
| Vulnerable deps | OWASP Dependency-Check (Gradle/Maven) | build | fail on CRITICAL, optionally HIGH (CVSS threshold) |
| Static analysis | Qodana / SonarQube quality gate | build | gate status must pass |
# security-trufflehog.yml — pin third-party actions to a full SHA- uses: trufflesecurity/trufflehog@37b77001d0174ebec2fcca2bd83ff83a6d45a3ab # v3.95.3 with: extra_args: ${{ inputs.only_verified && '--only-verified' || '' }}OWASP gate pattern (parse JSON report, fail by severity):
CRITICAL=$(jq '[.dependencies[]?.vulnerabilities[]? | select(.severity=="CRITICAL")] | length' "$REPORT")[ "$CRITICAL" -gt 0 ] && { echo "::error::$CRITICAL CRITICAL vulns"; exit 1; }7. Deploy
Reusable shared-deploy-* per target (EKS, EC2, S3). Take image_tag +
environment; bind to a GitHub Environment for approvals/secrets.
deploy: environment: ${{ inputs.environment }} # enables required reviewers + env secrets permissions: id-token: write # OIDC — preferred over static keys contents: read steps: - uses: aws-actions/configure-aws-credentials@v4 with: role-to-assume: ${{ secrets.AWS_DEPLOY_ROLE_ARN }} # OIDC role, not access keys aws-region: ${{ secrets.AWS_REGION }}Rules
- Prefer OIDC (
id-token: write+role-to-assume) overAWS_ACCESS_KEY_ID/AWS_SECRET_ACCESS_KEYlong-lived secrets. - Gate prod deploys behind a GitHub Environment with required reviewers.
- Deploy the immutable artifact built earlier (
needs.artifact.outputs.image_tag); never rebuild in the deploy job. - One reusable workflow per target; pick the target via a caller input.
8. Branch protection as code (rulesets)
Store GitHub rulesets as JSON in .github/ruleset/ and apply via API/Terraform.
Production branches (main, develop) and release tags are protected.
{ "name": "Protect main (PR-only, 2 approvals, linear, squash)", "target": "branch", "enforcement": "active", "conditions": { "ref_name": { "include": ["refs/heads/main"] }, "repository_name": { "include": ["${ORG}-ms-*", "${ORG}-sdk-*"] } }, "rules": [ { "type": "deletion" }, { "type": "non_fast_forward" }, { "type": "required_linear_history" }, { "type": "pull_request", "parameters": { "dismiss_stale_reviews_on_push": true, "require_last_push_approval": true, "required_approving_review_count": 2, "required_review_thread_resolution": true, "allowed_merge_methods": ["squash"] } }, { "type": "required_status_checks", "parameters": { "strict_required_status_checks_policy": true, "required_status_checks": [ { "context": "validate / PR Quality Gates" } ] } } ], "bypass_actors": []}Tag ruleset (release immutability) — target: "tag", refs/tags/v[0-9]*.[0-9]*.[0-9]*,
rules creation/deletion/non_fast_forward/update, bypass only the Actions
bot + admin role. Use enforcement: "evaluate" to dry-run a ruleset before
flipping to "active".
9. Cross-cutting reusable workflows
- commit-lint (
shared-commit-lint.yml):npx commitlintover the PR commit range (--from base.sha --to head.sha) orHEAD~1..HEADon push. Enforces conventional commits. - create-issue-on-failure: opens a labelled GitHub issue with repo/branch/ actor/run-link when the pipeline fails.
- slack-notify: posts status to a channel on failure/deploy/release.
- delete-branch: deletes the merged source branch on push to a protected branch.
- validate-source-branch: enforces branch-naming (
feature/*,fix/*, …) before allowing a PR to proceed.
10. CODEOWNERS, action pinning, permissions
- CODEOWNERS in
.github/: at least a default owner (* @${TEAM}); scope per path for sensitive areas (/.github/workflows/ @${PLATFORM_TEAM}). - Pin actions: first-party (
actions/*,aws-actions/*) to a major tag (@v5); third-party to a full commit SHA with a version comment. Never use@main/@master. - Permissions: set the minimum at workflow or job level. Default the repo to
permissions: {}and grant per job (contents: read, addid-token: writeonly where OIDC is needed,packages: writeonly to publish). - Secrets: pass with
secrets: inheritto reusable workflows; declare each in thesecrets:block; never echo a secret; mask derived values.
11. Consumer adoption (thin callers + GitFlow stage matrix)
The reusable workflows live once in the central ci-templates repo. Each
product repo ships thin ~20-line callers that uses: them — no build logic,
just with: inputs + secrets: inherit. Adopt a stack by copying its stage
templates into .github/workflows/ and replacing <org>:
cp templates/java-validate.yml .github/workflows/ # PR + branch gatecp templates/java-develop-deploy.yml .github/workflows/ # dev on mergecp templates/java-release-deploy.yml .github/workflows/ # staging on release/*cp templates/java-main-deploy.yml .github/workflows/ # prod on merge to main# then: replace <org> with your GitHub org in each `uses:` lineGitFlow stage → caller matrix (one caller per lifecycle stage):
| Caller | Trigger | Calls | Flow |
|---|---|---|---|
validate.yml |
push feature/*·bugfix/*·hotfix/*, PR→develop |
*-pr-pipeline.yml |
build→test→coverage→security. No deploy. Job id = required status check |
develop-deploy.yml |
push→develop |
*-main-pipeline.yml |
build→test→artifact(ECR)→deploy(dev)→delete-branch→release PR |
release-deploy.yml |
push release/* |
*-main-pipeline.yml |
build→test→artifact→deploy(staging) |
main-deploy.yml |
push→main |
*-main-pipeline.yml |
build→artifact→deploy(prod)→tag vX.Y.Z+Release |
deploy_target input — pick per caller; the orchestrator routes to the
matching shared-deploy-* workflow (§7):
| Value | Target |
|---|---|
ec2 |
SSH deploy to a directly reachable EC2 |
ec2-vpn |
SSH deploy to a private EC2; runner brings up a WireGuard wg0 tunnel, deploys, tears it down |
eks |
EKS cluster (plain manifests or Helm) |
s3 |
static site upload (react/nginx) |
Rules
- Callers stay declarative — only
uses:+with:+secrets: inherit. Any real step belongs in the reusable library, not the caller. - Pin
uses:to a release tag (@v1) or SHA on prod-facing callers — not a moving branch (@main). §10. - The
validatejob id must match the ruleset’s required-status-check context (e.g.validate / PR Quality Gates) — §8, or the gate silently passes. - Ratchet quality gates per repo: set
coverage_*_threshold/owasp_fail_on_cvssto the current baseline, raise over time — never a global hardcode. - Document the required secrets per
deploy_target(AWS/ECR always; EC2 SSH set forec2/ec2-vpn; extraWG_*forec2-vpn;SLACK_WEBHOOK_URLfor notifications) in the caller header.
12. Anti-patterns
| ❌ | ✅ |
|---|---|
| Copy-pasting steps across repos | central workflow_call library + thin callers |
uses: some/action@main |
pin to tag (first-party) or SHA (third-party) |
permissions: write-all / default token everywhere |
least-privilege permissions: per job |
Long-lived AWS_* keys in secrets |
OIDC id-token: write + role-to-assume |
| Rebuilding the image in the deploy job | deploy the artifact built upstream |
Secrets in with: inputs or run: echoes |
secrets: block + ${{ secrets.X }} env |
| Unbounded jobs | timeout-minutes on every job |
| No concurrency control | concurrency.cancel-in-progress per ref |
Direct push to main |
PR-only ruleset, squash, linear history |
| Mutable release tags | tag ruleset: no update/delete/force-push |
| Quality/security checks that don’t gate | needs: + if: gate artifact/deploy |
| Build logic pasted into a product-repo caller | thin caller: uses: + with: + secrets: inherit |
validate job id ≠ ruleset required-check context |
match exactly, or the gate no-ops |
13. Pre-merge checklist
- New pipeline logic added as a reusable
workflow_call, not inline per repo. - Inputs typed with defaults; secrets declared; outputs wired from step IDs.
- Orchestrator gates artifact/deploy on upstream
success/skipped. -
permissions:minimal per job; OIDC for cloud, no static keys. - Actions pinned (tag for first-party, SHA for third-party); no
@main. -
timeout-minutes+concurrencyset. - Security gates present (dependency review, secret scan, vuln scan) and gating.
- commit-lint runs; conventional commits enforced (see git-workflow.md).
- Prod deploy behind a GitHub Environment with required reviewers.
- Branch + tag rulesets updated if protection rules changed.
- No org-specific tokens (account IDs, ARNs, secret values) committed.
CI/CD on GitHub Actions via a central reusable-workflow library. Pairs with
git-workflow.md (conventional commits + branching that drive
semver), gradle.md (build tasks invoked by java-build), and
security.md (what the security gates enforce).