wip: contenido inicial + test de reusable workflow / composite action

This commit is contained in:
devops committed 2026-08-17 00:12:10 -05:00
1 parent e9fe7d0567
commit 262c0f271a
22 files changed
+1480

No files matched your search

+17
View File
@@ -0,0 +1,17 @@
on:
workflow_call:
inputs:
greeting:
type: string
required: true
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: devops/platform-templates/devsecops-snippets/gitleaks-step@main
with:
source: devsecops-snippets
- shell: bash
run: echo "Reusable workflow input recibido -> '${{ inputs.greeting }}'"
@@ -0,0 +1,193 @@
# Arquetipo de pipeline: Next.js standalone.
# Extraído del pipeline real de ecommerce-frontend (apps-registry),
# generalizando lo específico de esa app (rutas hardcodeadas, checks
# de negocio del catálogo) en inputs.
#
# Cómo usarlo desde tu repo: ver pipelines/archetype-nextjs-frontend/README.md.
on:
workflow_call:
inputs:
app_dir:
description: "Directorio de la app dentro de tu repo (ej. app/)."
type: string
required: true
image_name:
description: "Nombre completo de la imagen sin tag (ej. gitea.cruzcloud.net/devops/mi-app)."
type: string
required: true
manifest_file:
description: "Manifiesto K8s cuyo tag de imagen se actualiza tras el push (CD)."
type: string
required: true
semgrep_configs:
description: "Rulesets de Semgrep, separados por coma."
type: string
required: false
default: "p/typescript,p/react,p/nextjs,p/security-audit"
trivy_ignorefile:
description: "Ruta a un .trivyignore, si tu app tiene excepciones documentadas."
type: string
required: false
default: ""
secrets:
REGISTRY_USER:
required: true
REGISTRY_PASSWORD:
required: true
COSIGN_PRIVATE_KEY:
required: true
COSIGN_PASSWORD:
required: true
jobs:
gitleaks:
name: Escaneo de Secretos (Gitleaks)
runs-on: ubuntu-latest
timeout-minutes: 10
steps:
- uses: actions/checkout@v3
with:
fetch-depth: 1
- uses: devops/platform-templates/devsecops-snippets/gitleaks-step@main
with:
source: ${{ inputs.app_dir }}
build:
name: Construir y Subir Imagen
needs: gitleaks
if: github.event_name == 'push'
runs-on: ubuntu-latest
timeout-minutes: 20
env:
APP_DIR: ${{ inputs.app_dir }}
MANIFEST_FILE: ${{ inputs.manifest_file }}
IMAGE_NAME: ${{ inputs.image_name }}
steps:
- name: Checkout del código
uses: actions/checkout@v3
with:
fetch-depth: 1
persist-credentials: true
- name: Definir versión semántica
id: vars
shell: bash
run: |
set -euo pipefail
echo "VERSION=v1.0.${{ github.run_number }}" >> "$GITHUB_OUTPUT"
- name: Validar secretos del Registry
shell: bash
env:
REGISTRY_USER: ${{ secrets.REGISTRY_USER }}
REGISTRY_PASSWORD: ${{ secrets.REGISTRY_PASSWORD }}
run: |
set -euo pipefail
test -n "${REGISTRY_USER}" || { echo "ERROR: REGISTRY_USER no está configurado."; exit 1; }
test -n "${REGISTRY_PASSWORD}" || { echo "ERROR: REGISTRY_PASSWORD no está configurado."; exit 1; }
- uses: devops/platform-templates/devsecops-snippets/trivy-iac-step@main
with:
target: ${{ env.MANIFEST_FILE }}
- uses: devops/platform-templates/devsecops-snippets/semgrep-step@main
with:
target: ${{ env.APP_DIR }}
configs: ${{ inputs.semgrep_configs }}
- name: Login en Gitea Registry
uses: docker/login-action@v2
with:
registry: gitea.cruzcloud.net
username: ${{ secrets.REGISTRY_USER }}
password: ${{ secrets.REGISTRY_PASSWORD }}
logout: true
# push: false — la imagen se queda cargada localmente para poder
# escanearla con Trivy antes de subirla.
- name: Construir Imagen
uses: docker/build-push-action@v4
with:
context: ${{ env.APP_DIR }}
push: false
load: true
tags: |
${{ env.IMAGE_NAME }}:${{ steps.vars.outputs.VERSION }}
${{ env.IMAGE_NAME }}:latest
- uses: devops/platform-templates/devsecops-snippets/trivy-image-step@main
with:
image_ref: ${{ env.IMAGE_NAME }}:${{ steps.vars.outputs.VERSION }}
ignorefile: ${{ inputs.trivy_ignorefile }}
- uses: devops/platform-templates/devsecops-snippets/syft-sbom-step@main
with:
image_ref: ${{ env.IMAGE_NAME }}:${{ steps.vars.outputs.VERSION }}
artifact_suffix: ${{ steps.vars.outputs.VERSION }}
- name: Subir Imagen al Registry
id: push
shell: bash
run: |
set -euo pipefail
PUSH_OUTPUT="$(docker push "${IMAGE_NAME}:${{ steps.vars.outputs.VERSION }}")"
echo "${PUSH_OUTPUT}"
docker push "${IMAGE_NAME}:latest"
DIGEST="$(echo "${PUSH_OUTPUT}" | grep -oE 'sha256:[a-f0-9]{64}' | tail -1)"
test -n "${DIGEST}" || { echo "ERROR: no se pudo extraer el digest del push."; exit 1; }
echo "digest=${DIGEST}" >> "$GITHUB_OUTPUT"
- uses: devops/platform-templates/devsecops-snippets/cosign-sign-verify-step@main
with:
image_name: ${{ env.IMAGE_NAME }}
digest: ${{ steps.push.outputs.digest }}
public_key_path: ${{ env.APP_DIR }}/cosign.pub
cosign_private_key: ${{ secrets.COSIGN_PRIVATE_KEY }}
cosign_password: ${{ secrets.COSIGN_PASSWORD }}
- name: Verificar promoción segura
id: promotion
shell: bash
run: |
set -euo pipefail
git fetch origin main
CURRENT_SHA="${{ github.sha }}"
REMOTE_SHA="$(git rev-parse origin/main)"
if [ "${CURRENT_SHA}" = "${REMOTE_SHA}" ]; then
echo "promote=true" >> "$GITHUB_OUTPUT"
else
echo "promote=false" >> "$GITHUB_OUTPUT"
echo "Hay un commit más reciente; no se actualizará el manifiesto."
fi
- name: Actualizar Manifiesto GitOps (CD)
if: steps.promotion.outputs.promote == 'true'
shell: bash
run: |
set -euo pipefail
VERSION="${{ steps.vars.outputs.VERSION }}"
git config user.name "gitea-actions"
git config user.email "[email protected]"
git fetch origin main
git checkout -B main origin/main
sed -i -E "s|(image: ${IMAGE_NAME}:).*|\1${VERSION}|g" "${MANIFEST_FILE}"
git add "${MANIFEST_FILE}"
if git diff --cached --quiet; then
echo "No hay cambios en el manifiesto."
exit 0
fi
git commit -m "chore(gitops): deploy ${VERSION} [skip ci]"
git push origin HEAD:main
- name: Resumen del pipeline
if: always()
shell: bash
run: |
echo "========================================"
echo "Versión: ${{ steps.vars.outputs.VERSION }}"
echo "Commit: ${{ github.sha }}"
echo "Promoción GitOps: ${{ steps.promotion.outputs.promote }}"
echo "========================================"
@@ -0,0 +1,193 @@
# Arquetipo de pipeline: API Node/Express (o similar, sin SSR).
# Extraído del pipeline real de commerce-backend/Medusa (apps-registry).
#
# Cómo usarlo desde tu repo: ver pipelines/archetype-node-api/README.md.
on:
workflow_call:
inputs:
app_dir:
description: "Directorio de la app dentro de tu repo."
type: string
required: true
image_name:
description: "Nombre completo de la imagen sin tag."
type: string
required: true
manifest_file:
description: "Manifiesto K8s cuyo tag de imagen se actualiza tras el push (CD)."
type: string
required: true
semgrep_configs:
description: "Rulesets de Semgrep, separados por coma."
type: string
required: false
default: "p/typescript,p/security-audit,p/owasp-top-ten"
trivy_ignorefile:
description: "Ruta a un .trivyignore, si tu app tiene excepciones documentadas."
type: string
required: false
default: ""
secrets:
REGISTRY_USER:
required: true
REGISTRY_PASSWORD:
required: true
COSIGN_PRIVATE_KEY:
required: true
COSIGN_PASSWORD:
required: true
jobs:
gitleaks:
name: Escaneo de Secretos (Gitleaks)
runs-on: ubuntu-latest
timeout-minutes: 10
steps:
- uses: actions/checkout@v3
with:
fetch-depth: 1
- uses: devops/platform-templates/devsecops-snippets/gitleaks-step@main
with:
source: ${{ inputs.app_dir }}
build:
name: Construir y Subir Imagen
needs: gitleaks
if: github.event_name == 'push'
runs-on: ubuntu-latest
timeout-minutes: 45
env:
APP_DIR: ${{ inputs.app_dir }}
MANIFEST_FILE: ${{ inputs.manifest_file }}
IMAGE_NAME: ${{ inputs.image_name }}
steps:
- name: Checkout del código
uses: actions/checkout@v3
with:
fetch-depth: 1
persist-credentials: true
- name: Definir versión
id: vars
shell: bash
run: |
set -euo pipefail
echo "VERSION=v1.0.${{ github.run_number }}" >> "$GITHUB_OUTPUT"
- name: Validar secretos del Registry
shell: bash
env:
REGISTRY_USER: ${{ secrets.REGISTRY_USER }}
REGISTRY_PASSWORD: ${{ secrets.REGISTRY_PASSWORD }}
run: |
set -euo pipefail
test -n "${REGISTRY_USER}" || { echo "ERROR: REGISTRY_USER no está configurado."; exit 1; }
test -n "${REGISTRY_PASSWORD}" || { echo "ERROR: REGISTRY_PASSWORD no está configurado."; exit 1; }
- uses: devops/platform-templates/devsecops-snippets/trivy-iac-step@main
with:
target: ${{ env.MANIFEST_FILE }}
- uses: devops/platform-templates/devsecops-snippets/semgrep-step@main
with:
target: ${{ env.APP_DIR }}
configs: ${{ inputs.semgrep_configs }}
- name: Login en Gitea Registry
uses: docker/login-action@v2
with:
registry: gitea.cruzcloud.net
username: ${{ secrets.REGISTRY_USER }}
password: ${{ secrets.REGISTRY_PASSWORD }}
logout: true
- name: Construir Imagen
uses: docker/build-push-action@v4
with:
context: ${{ env.APP_DIR }}
push: false
load: true
tags: |
${{ env.IMAGE_NAME }}:${{ steps.vars.outputs.VERSION }}
${{ env.IMAGE_NAME }}:latest
# --timeout 15m0s ya viene por default en trivy-image-step —
# commerce-backend (Medusa v2) es exactamente el caso real que
# justificó ese default (node_modules completo del framework +
# admin-sdk + cli, mucho más que escanear que una API chica).
- uses: devops/platform-templates/devsecops-snippets/trivy-image-step@main
with:
image_ref: ${{ env.IMAGE_NAME }}:${{ steps.vars.outputs.VERSION }}
ignorefile: ${{ inputs.trivy_ignorefile }}
- uses: devops/platform-templates/devsecops-snippets/syft-sbom-step@main
with:
image_ref: ${{ env.IMAGE_NAME }}:${{ steps.vars.outputs.VERSION }}
artifact_suffix: ${{ steps.vars.outputs.VERSION }}
- name: Subir Imagen al Registry
id: push
shell: bash
run: |
set -euo pipefail
PUSH_OUTPUT="$(docker push "${IMAGE_NAME}:${{ steps.vars.outputs.VERSION }}")"
echo "${PUSH_OUTPUT}"
docker push "${IMAGE_NAME}:latest"
DIGEST="$(echo "${PUSH_OUTPUT}" | grep -oE 'sha256:[a-f0-9]{64}' | tail -1)"
test -n "${DIGEST}" || { echo "ERROR: no se pudo extraer el digest del push."; exit 1; }
echo "digest=${DIGEST}" >> "$GITHUB_OUTPUT"
- uses: devops/platform-templates/devsecops-snippets/cosign-sign-verify-step@main
with:
image_name: ${{ env.IMAGE_NAME }}
digest: ${{ steps.push.outputs.digest }}
public_key_path: ${{ env.APP_DIR }}/cosign.pub
cosign_private_key: ${{ secrets.COSIGN_PRIVATE_KEY }}
cosign_password: ${{ secrets.COSIGN_PASSWORD }}
- name: Verificar promoción segura
id: promotion
shell: bash
run: |
set -euo pipefail
git fetch origin main
CURRENT_SHA="${{ github.sha }}"
REMOTE_SHA="$(git rev-parse origin/main)"
if [ "${CURRENT_SHA}" = "${REMOTE_SHA}" ]; then
echo "promote=true" >> "$GITHUB_OUTPUT"
else
echo "promote=false" >> "$GITHUB_OUTPUT"
echo "Hay un commit más reciente; no se actualizará el manifiesto."
fi
- name: Actualizar manifiesto
if: steps.promotion.outputs.promote == 'true'
shell: bash
run: |
set -euo pipefail
VERSION="${{ steps.vars.outputs.VERSION }}"
git config user.name "gitea-actions"
git config user.email "[email protected]"
git fetch origin main
git checkout -B main origin/main
sed -i -E "s|(${IMAGE_NAME}:)v[0-9]+\.[0-9]+\.[0-9]+|\1${VERSION}|g" "${MANIFEST_FILE}"
git add "${MANIFEST_FILE}"
if git diff --cached --quiet; then
echo "El manifiesto ya apunta a ${VERSION}."
exit 0
fi
git commit -m "chore(gitops): deploy ${VERSION} [skip ci]"
git push origin HEAD:main
- name: Resumen del pipeline
if: always()
shell: bash
run: |
echo "========================================"
echo "Versión: ${{ steps.vars.outputs.VERSION }}"
echo "Commit: ${{ github.sha }}"
echo "Promoción GitOps: ${{ steps.promotion.outputs.promote }}"
echo "========================================"
@@ -0,0 +1,195 @@
# Arquetipo de pipeline: sitio estático servido por nginx (MkDocs,
# Hugo, Astro, HTML plano). Extraído del pipeline real de docs-portal
# (apps-registry).
#
# Diferencia clave con los otros 2 arquetipos: gitleaks y Trivy IaC
# escanean todo APP_DIR (no un único manifest_file) — un sitio estático
# suele tener varios manifiestos K8s separados (deployment/service/
# ingress) en vez de uno solo.
#
# Cómo usarlo desde tu repo: ver pipelines/archetype-static-site/README.md.
on:
workflow_call:
inputs:
app_dir:
description: "Directorio de la app dentro de tu repo."
type: string
required: true
image_name:
description: "Nombre completo de la imagen sin tag."
type: string
required: true
manifest_file:
description: "Manifiesto K8s (Deployment) cuyo tag de imagen se actualiza tras el push (CD)."
type: string
required: true
semgrep_configs:
description: "Rulesets de Semgrep, separados por coma."
type: string
required: false
default: "p/python,p/security-audit"
trivy_ignorefile:
description: "Ruta a un .trivyignore, si tu app tiene excepciones documentadas."
type: string
required: false
default: ""
secrets:
REGISTRY_USER:
required: true
REGISTRY_PASSWORD:
required: true
COSIGN_PRIVATE_KEY:
required: true
COSIGN_PASSWORD:
required: true
jobs:
gitleaks:
name: Escaneo de Secretos (Gitleaks)
runs-on: ubuntu-latest
timeout-minutes: 10
steps:
- uses: actions/checkout@v3
with:
fetch-depth: 1
- uses: devops/platform-templates/devsecops-snippets/gitleaks-step@main
with:
source: ${{ inputs.app_dir }}
build:
name: Construir y publicar
needs: gitleaks
if: github.event_name == 'push'
runs-on: ubuntu-latest
timeout-minutes: 20
env:
APP_DIR: ${{ inputs.app_dir }}
MANIFEST_FILE: ${{ inputs.manifest_file }}
IMAGE_NAME: ${{ inputs.image_name }}
steps:
- name: Checkout del código
uses: actions/checkout@v3
with:
fetch-depth: 1
persist-credentials: true
- name: Definir versión
id: vars
shell: bash
run: |
set -euo pipefail
echo "VERSION=v1.0.${{ github.run_number }}" >> "$GITHUB_OUTPUT"
- name: Validar secretos del Registry
shell: bash
env:
REGISTRY_USER: ${{ secrets.REGISTRY_USER }}
REGISTRY_PASSWORD: ${{ secrets.REGISTRY_PASSWORD }}
run: |
set -euo pipefail
test -n "${REGISTRY_USER}" || { echo "ERROR: REGISTRY_USER no está configurado."; exit 1; }
test -n "${REGISTRY_PASSWORD}" || { echo "ERROR: REGISTRY_PASSWORD no está configurado."; exit 1; }
- uses: devops/platform-templates/devsecops-snippets/trivy-iac-step@main
with:
target: ${{ env.APP_DIR }}
- uses: devops/platform-templates/devsecops-snippets/semgrep-step@main
with:
target: ${{ env.APP_DIR }}
configs: ${{ inputs.semgrep_configs }}
- name: Login en Gitea Registry
uses: docker/login-action@v2
with:
registry: gitea.cruzcloud.net
username: ${{ secrets.REGISTRY_USER }}
password: ${{ secrets.REGISTRY_PASSWORD }}
logout: true
- name: Construir Imagen
uses: docker/build-push-action@v4
with:
context: ${{ env.APP_DIR }}
push: false
load: true
tags: |
${{ env.IMAGE_NAME }}:${{ steps.vars.outputs.VERSION }}
${{ env.IMAGE_NAME }}:latest
- uses: devops/platform-templates/devsecops-snippets/trivy-image-step@main
with:
image_ref: ${{ env.IMAGE_NAME }}:${{ steps.vars.outputs.VERSION }}
ignorefile: ${{ inputs.trivy_ignorefile }}
- uses: devops/platform-templates/devsecops-snippets/syft-sbom-step@main
with:
image_ref: ${{ env.IMAGE_NAME }}:${{ steps.vars.outputs.VERSION }}
artifact_suffix: ${{ steps.vars.outputs.VERSION }}
- name: Subir Imagen al Registry
id: push
shell: bash
run: |
set -euo pipefail
PUSH_OUTPUT="$(docker push "${IMAGE_NAME}:${{ steps.vars.outputs.VERSION }}")"
echo "${PUSH_OUTPUT}"
docker push "${IMAGE_NAME}:latest"
DIGEST="$(echo "${PUSH_OUTPUT}" | grep -oE 'sha256:[a-f0-9]{64}' | tail -1)"
test -n "${DIGEST}" || { echo "ERROR: no se pudo extraer el digest del push."; exit 1; }
echo "digest=${DIGEST}" >> "$GITHUB_OUTPUT"
- uses: devops/platform-templates/devsecops-snippets/cosign-sign-verify-step@main
with:
image_name: ${{ env.IMAGE_NAME }}
digest: ${{ steps.push.outputs.digest }}
public_key_path: ${{ env.APP_DIR }}/cosign.pub
cosign_private_key: ${{ secrets.COSIGN_PRIVATE_KEY }}
cosign_password: ${{ secrets.COSIGN_PASSWORD }}
- name: Verificar promoción segura
id: promotion
shell: bash
run: |
set -euo pipefail
git fetch origin main
CURRENT_SHA="${{ github.sha }}"
REMOTE_SHA="$(git rev-parse origin/main)"
if [ "${CURRENT_SHA}" = "${REMOTE_SHA}" ]; then
echo "promote=true" >> "$GITHUB_OUTPUT"
else
echo "promote=false" >> "$GITHUB_OUTPUT"
echo "Hay un commit más reciente; no se actualizará el manifiesto."
fi
- name: Actualizar manifiesto GitOps
if: steps.promotion.outputs.promote == 'true'
shell: bash
run: |
set -euo pipefail
VERSION="${{ steps.vars.outputs.VERSION }}"
git config user.name "gitea-actions"
git config user.email "[email protected]"
git fetch origin main
git checkout -B main origin/main
sed -i -E "s|(image: ${IMAGE_NAME}:).*|\1${VERSION}|g" "${MANIFEST_FILE}"
git add "${MANIFEST_FILE}"
if git diff --cached --quiet; then
echo "El manifiesto ya apunta a ${VERSION}."
exit 0
fi
git commit -m "chore(gitops): deploy ${VERSION} [skip ci]"
git push origin HEAD:main
- name: Resumen del pipeline
if: always()
shell: bash
run: |
echo "========================================"
echo "Versión: ${{ steps.vars.outputs.VERSION }}"
echo "Commit: ${{ github.sha }}"
echo "Promoción GitOps: ${{ steps.promotion.outputs.promote }}"
echo "========================================"