CI/CD y Despliegue

Tabla de Contenidos


Visión general

Arquitectura de doble pipeline: Pipeline del Monorepo (izquierda) y Pipeline de MFE (derecha) en paralelo Pipeline del Monorepo (platform-core) Turborepo CI Changesets Version Publicar en GPR Design System SDK / Utils compartidos Despliegue de Workers Pipeline de MFE (polyrepos) Build + Test Upload a R2 Registrar Versión Preview por PR Auto-activar en dev Promover vía Admin

La plataforma opera dos arquitecturas de pipeline CI/CD distintas que reflejan su estructura híbrida monorepo/polyrepo:

  1. Pipeline del monorepo (platform-core) -- Gestiona paquetes compartidos (design system, SDK, utilidades, Workers) con versionado coordinado mediante Changesets y publicación en GitHub Packages.
  2. Pipelines de MFE en polyrepos -- Cada repositorio de micro frontend tiene su propio pipeline independiente que compila, testea, despliega y registra el MFE en el Version Config Service.

Principios Clave

PrincipioImplementación
Feedback rápidoTurborepo remote caching, pasos de CI paralelizados, builds incrementales
Desplegabilidad independienteCada MFE se despliega a su propio ritmo sin coordinarse con otros equipos
Gestión automatizada de versionesChangesets para paquetes del monorepo; git tags semánticos para MFEs
Entornos de previewCada PR obtiene una URL de preview única para el MFE modificado
SeguridadAutenticación OIDC con Cloudflare (planificado), tokens de API de corta duración en CI

Stack Tecnológico

  • Plataforma CI/CD: GitHub Actions
  • Orquestación del monorepo: Turborepo (con remote caching)
  • Gestión de paquetes: pnpm (con workspace protocol en el monorepo)
  • Gestión de versiones: Changesets (monorepo), git tags semánticos (polyrepos)
  • Registro de paquetes: GitHub Packages (npm)
  • Destino de despliegue: Cloudflare Workers, R2, KV
  • Automatización de dependencias: Renovate

CI/CD del Monorepo (platform-core)

El monorepo platform-core contiene toda la infraestructura compartida: el design system, el MFE SDK, configuraciones TypeScript compartidas, Workers (Shell App, Version Config Service, Auth Gateway) y utilidades compartidas.

Pipeline de CI (en cada PR)

El pipeline de CI se ejecuta en cada pull request y push a main. Aprovecha Turborepo para ejecutar tareas solo en los paquetes que han cambiado, reduciendo drásticamente los tiempos de CI en cambios específicos.

.github/workflows/ci.yml

name: CI

on:
  pull_request:
    branches: [main]
  push:
    branches: [main]

concurrency:
  group: ci-${{ github.ref }}
  cancel-in-progress: true

env:
  TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }}
  TURBO_TEAM: ${{ vars.TURBO_TEAM }}

jobs:
  ci:
    name: Lint, Typecheck, Test, Build
    runs-on: ubuntu-latest
    timeout-minutes: 15

    steps:
      - name: Checkout repository
        uses: actions/checkout@v6
        with:
          # Full history is needed for Changesets to correctly determine
          # which packages changed. fetch-depth: 2 is insufficient.
          fetch-depth: 0

      - name: Setup pnpm
        uses: pnpm/action-setup@v4

      - name: Setup Node.js
        uses: actions/setup-node@v6
        with:
          # Node 20 reaches EOL in April 2026. Node 24 is now Active LTS.
          node-version: 22
          cache: pnpm

      - name: Install dependencies
        run: pnpm install --frozen-lockfile

      - name: Run lint
        run: pnpm turbo run lint

      - name: Run typecheck
        run: pnpm turbo run typecheck

      - name: Run tests
        run: pnpm turbo run test -- --coverage

      - name: Run build
        run: pnpm turbo run build

      - name: Upload coverage reports
        if: github.event_name == 'pull_request'
        uses: actions/upload-artifact@v6
        with:
          name: coverage-reports
          path: packages/*/coverage/
          retention-days: 7

Cómo funciona el caching de Turborepo en CI:

  • Cada tarea (lint, typecheck, test, build) declara sus inputs en turbo.json.
  • En la primera ejecución, los outputs se hashean y se suben al remote cache de Turborepo.
  • Las ejecuciones posteriores con los mismos inputs producen cache hits, saltando la ejecución por completo.
  • Un PR que solo modifica el design system saltará lint/test/build para los Workers y el SDK.

Pipeline de Release (Changesets)

Flujo de release del monorepo: Changesets a PR de versión, merge, publicación en GitHub Packages Changesets .changeset/*.md PR de Versión bump + changelog Merge a main Publicar paquetes npm GitHub Packages registro @org/* Tras publicar: repository-dispatch notifica a todos los polyrepos de MFE para actualizaciones de Renovate

El pipeline de release automatiza el bump de versiones, la generación de changelogs y la publicación usando Changesets. Los desarrolladores añaden archivos de changeset durante el desarrollo (pnpm changeset), y el pipeline de release los consume.

.github/workflows/release.yml

name: Release

on:
  push:
    branches: [main]

concurrency:
  group: release-${{ github.ref }}
  cancel-in-progress: false

env:
  TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }}
  TURBO_TEAM: ${{ vars.TURBO_TEAM }}

permissions:
  contents: write
  packages: write
  pull-requests: write

jobs:
  release:
    name: Release Packages
    runs-on: ubuntu-latest
    timeout-minutes: 20

    steps:
      - name: Checkout repository
        uses: actions/checkout@v6
        with:
          fetch-depth: 0

      - name: Setup pnpm
        uses: pnpm/action-setup@v4

      - name: Setup Node.js
        uses: actions/setup-node@v6
        with:
          # Node 20 reaches EOL in April 2026. Node 24 is now Active LTS.
          node-version: 22
          cache: pnpm
          registry-url: https://npm.pkg.github.com
          scope: '@org'

      - name: Install dependencies
        run: pnpm install --frozen-lockfile

      - name: Build all packages
        run: pnpm turbo run build

      - name: Create Release PR or Publish
        id: changesets
        uses: changesets/action@v1
        with:
          version: pnpm changeset version
          publish: pnpm changeset publish
          title: 'chore: version packages'
          commit: 'chore: version packages'
        env:
          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
          NODE_AUTH_TOKEN: ${{ secrets.GITHUB_TOKEN }}

      - name: Notify polyrepos of new releases
        if: steps.changesets.outputs.published == 'true'
        uses: actions/github-script@v8
        with:
          github-token: ${{ secrets.DISPATCH_TOKEN }}
          script: |
            const publishedPackages = JSON.parse('${{ steps.changesets.outputs.publishedPackages }}');

            const repos = [
              'myorg/mfe-dashboard',
              'myorg/mfe-settings',
              'myorg/mfe-analytics',
              'myorg/mfe-billing',
            ];

            const results = [];
            for (const repo of repos) {
              const [owner, repoName] = repo.split('/');
              try {
                await github.rest.repos.createDispatchEvent({
                  owner,
                  repo: repoName,
                  event_type: 'platform-core-release',
                  client_payload: {
                    packages: publishedPackages.map(p => ({
                      name: p.name,
                      version: p.version,
                    })),
                  },
                });
                results.push({ repo, status: 'success' });
              } catch (error) {
                // Log the error but do not fail the workflow -- dispatch
                // failures should not block the release pipeline.
                console.error(`Failed to dispatch to ${repo}: ${error.message}`);
                results.push({ repo, status: 'failed', error: error.message });
              }
            }

            const failed = results.filter(r => r.status === 'failed');
            console.log(`Dispatched release notification to ${repos.length} repos (${failed.length} failed)`);
            if (failed.length > 0) {
              core.warning(`Repository dispatch failed for: ${failed.map(r => r.repo).join(', ')}. These repos will pick up updates on the next Renovate schedule.`);
            }

Flujo de release:

  1. El desarrollador crea un PR con cambios de código y un archivo de changeset (p. ej., .changeset/cool-feature.md).
  2. Se revisa el PR y se mergea a main.
  3. La Changesets Action detecta changesets pendientes y abre (o actualiza) un PR de "Version Packages".
  4. El PR de "Version Packages" contiene las versiones de package.json incrementadas y los archivos CHANGELOG.md actualizados.
  5. Cuando el equipo mergea el PR de "Version Packages", la action publica todos los paquetes con versión actualizada en GitHub Packages.
  6. Tras la publicación, un evento repository-dispatch notifica a todos los polyrepos de MFE para que Renovate pueda crear PRs de actualización de forma inmediata.

Despliegue del Design System como MF Remote

El paquete del design system (@org/design-system) cumple un doble rol: se publica en GitHub Packages como paquete npm y se despliega como un remote de Module Federation para que los MFEs puedan consumir componentes compartidos en runtime.

Tras una publicación npm exitosa, el workflow de release también despliega el bundle del design system en R2 y lo registra en el Version Config Service.

Paso adicional de deploy añadido a .github/workflows/release.yml:

      # --- Design System MF Remote Deployment ---

      - name: Check if design system was published
        id: check-ds
        if: steps.changesets.outputs.published == 'true'
        run: |
          PUBLISHED='${{ steps.changesets.outputs.publishedPackages }}'
          DS_VERSION=$(echo "$PUBLISHED" | jq -r '.[] | select(.name == "@org/design-system") | .version')
          if [ -n "$DS_VERSION" ]; then
            echo "version=$DS_VERSION" >> "$GITHUB_OUTPUT"
            echo "published=true" >> "$GITHUB_OUTPUT"
          else
            echo "published=false" >> "$GITHUB_OUTPUT"
          fi

      - name: Build design system MF remote
        if: steps.check-ds.outputs.published == 'true'
        working-directory: packages/design-system
        run: pnpm build:federation
        env:
          MF_PUBLIC_PATH: https://static.myplatform.com/design-system/${{ steps.check-ds.outputs.version }}/

      - name: Upload design system to R2
        if: steps.check-ds.outputs.published == 'true'
        # Wrangler v3 reached EOL Q1 2026; use v4.
        uses: cloudflare/wrangler-action@v4
        with:
          apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }}
          command: >
            r2 object put
            static-assets/design-system/${{ steps.check-ds.outputs.version }}/
            --local packages/design-system/dist/
            --recursive

      - name: Register design system version
        if: steps.check-ds.outputs.published == 'true'
        run: |
          curl -sf -X POST \
            "${{ vars.VERSION_CONFIG_SERVICE_URL }}/api/remotes/design-system/versions" \
            -H "Authorization: Bearer ${{ secrets.VCS_DEPLOY_TOKEN }}" \
            -H "Content-Type: application/json" \
            -d '{
              "version": "${{ steps.check-ds.outputs.version }}",
              "manifestUrl": "https://static.myplatform.com/design-system/${{ steps.check-ds.outputs.version }}/mf-manifest.json",
              "activateIn": ["dev"]
            }'

Esto garantiza que cualquier nueva versión del design system esté disponible de inmediato como remote de Module Federation en el entorno dev. La promoción a staging y production se gestiona a través del Admin UI o la API del Version Config Service.


CI/CD de MFEs (polyrepos)

Cada micro frontend vive en su propio repositorio con su propio pipeline de CI/CD. Los pipelines siguen una estructura consistente pero pueden personalizarse por equipo.

Pipeline de CI (en cada PR)

.github/workflows/ci.yml

name: CI

on:
  pull_request:
    branches: [main]

concurrency:
  group: ci-${{ github.ref }}
  cancel-in-progress: true

permissions:
  contents: read
  packages: read
  pull-requests: write
  deployments: write

jobs:
  ci:
    name: Lint, Typecheck, Test
    runs-on: ubuntu-latest
    timeout-minutes: 15

    steps:
      - name: Checkout repository
        uses: actions/checkout@v6

      - name: Setup pnpm
        uses: pnpm/action-setup@v4

      - name: Setup Node.js
        uses: actions/setup-node@v6
        with:
          # Node 20 reaches EOL in April 2026. Node 24 is now Active LTS.
          node-version: 22
          cache: pnpm
          registry-url: https://npm.pkg.github.com
          scope: '@org'

      - name: Install dependencies
        run: pnpm install --frozen-lockfile
        env:
          NODE_AUTH_TOKEN: ${{ secrets.GITHUB_TOKEN }}

      - name: Run lint
        run: pnpm lint

      - name: Run typecheck
        run: pnpm typecheck

      - name: Run unit tests
        run: pnpm test -- --coverage

      - name: Build MFE
        run: pnpm build
        env:
          MF_PUBLIC_PATH: https://static.myplatform.com/${{ github.event.repository.name }}/pr-${{ github.event.pull_request.number }}/

      - name: Upload build artifact
        uses: actions/upload-artifact@v6
        with:
          name: mfe-build
          path: dist/
          retention-days: 3

  e2e:
    name: E2E Tests
    needs: ci
    runs-on: ubuntu-latest
    timeout-minutes: 20

    steps:
      - name: Checkout repository
        uses: actions/checkout@v6

      - name: Setup pnpm
        uses: pnpm/action-setup@v4

      - name: Setup Node.js
        uses: actions/setup-node@v6
        with:
          # Node 20 reaches EOL in April 2026. Node 24 is now Active LTS.
          node-version: 22
          cache: pnpm
          registry-url: https://npm.pkg.github.com
          scope: '@org'

      - name: Install dependencies
        run: pnpm install --frozen-lockfile
        env:
          NODE_AUTH_TOKEN: ${{ secrets.GITHUB_TOKEN }}

      - name: Install Playwright browsers
        run: pnpm exec playwright install --with-deps chromium

      - name: Download build artifact
        uses: actions/download-artifact@v8
        with:
          name: mfe-build
          path: dist/

      # NOTE: The E2E tests reference BASE_URL=http://localhost:3000 but
      # there is no step that starts a local dev server to serve the
      # downloaded build artifact. You must add a server step before
      # running E2E tests. For example, use `pnpm exec serve -s dist -l 3000`
      # or `npx http-server dist -p 3000` as a background process:
      - name: Start local preview server
        run: npx --yes serve -s dist -l 3000 &

      - name: Wait for server to be ready
        run: npx --yes wait-on http://localhost:3000 --timeout 30000

      - name: Run E2E tests
        run: pnpm test:e2e
        env:
          BASE_URL: http://localhost:3000

      - name: Upload Playwright report
        if: failure()
        uses: actions/upload-artifact@v6
        with:
          name: playwright-report
          path: playwright-report/
          retention-days: 7

  preview:
    name: Deploy Preview
    needs: ci
    runs-on: ubuntu-latest
    timeout-minutes: 10
    environment:
      name: preview
      url: ${{ steps.preview-url.outputs.url }}

    steps:
      - name: Checkout repository
        uses: actions/checkout@v6

      - name: Download build artifact
        uses: actions/download-artifact@v8
        with:
          name: mfe-build
          path: dist/

      - name: Upload preview to R2
        # Wrangler v3 reached EOL Q1 2026; use v4.
        uses: cloudflare/wrangler-action@v4
        with:
          apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }}
          command: >
            r2 object put
            static-assets/${{ github.event.repository.name }}/pr-${{ github.event.pull_request.number }}/
            --local dist/
            --recursive

      - name: Set preview URL
        id: preview-url
        run: |
          SHELL_URL="${{ vars.SHELL_APP_DEV_URL }}"
          MFE_NAME="${{ github.event.repository.name }}"
          PR_NUM="${{ github.event.pull_request.number }}"
          echo "url=${SHELL_URL}?mfe_${MFE_NAME#mfe-}=pr-${PR_NUM}" >> "$GITHUB_OUTPUT"

      - name: Comment preview URL on PR
        uses: actions/github-script@v8
        with:
          script: |
            const mfeName = context.repo.repo;
            const prNumber = context.payload.pull_request.number;
            const previewUrl = '${{ steps.preview-url.outputs.url }}';
            const directUrl = `https://static.myplatform.com/${mfeName}/pr-${prNumber}/`;

            const body = [
              `### Preview Deployment`,
              ``,
              `| Resource | URL |`,
              `|---|---|`,
              `| **Shell with preview MFE** | [${previewUrl}](${previewUrl}) |`,
              `| **Direct bundle** | [${directUrl}](${directUrl}) |`,
              ``,
              `> Load this preview MFE in any environment by appending \`?mfe_${mfeName.replace('mfe-', '')}=pr-${prNumber}\` to the shell URL.`,
            ].join('\n');

            // Find and update existing comment or create new one
            const comments = await github.rest.issues.listComments({
              owner: context.repo.owner,
              repo: context.repo.repo,
              issue_number: prNumber,
            });

            const existing = comments.data.find(c =>
              c.user.login === 'github-actions[bot]' &&
              c.body.includes('### Preview Deployment')
            );

            if (existing) {
              await github.rest.issues.updateComment({
                owner: context.repo.owner,
                repo: context.repo.repo,
                comment_id: existing.id,
                body,
              });
            } else {
              await github.rest.issues.createComment({
                owner: context.repo.owner,
                repo: context.repo.repo,
                issue_number: prNumber,
                body,
              });
            }

Aspectos clave del pipeline de CI de MFE:

  • El .npmrc de cada polyrepo está configurado para obtener paquetes del scope @org desde GitHub Packages.
  • El paso de build establece MF_PUBLIC_PATH a la URL de preview para que las referencias a chunks se resuelvan correctamente.
  • Los tests E2E se ejecutan contra el MFE compilado cargado en un entorno de shell aislado.
  • El despliegue de preview sube el build a R2 en una ruta específica del PR.

Pipeline de Deploy (al mergear a main)

Pipeline de deploy de MFE: build, upload a R2, registro de versión, URL de preview, promover Build MFE Rsbuild prod Upload a R2 /{mfe}/{version}/ Registrar Versión Config Service API Activar en dev auto al mergear Promover staging/prod El despliegue (subir artefactos) está desacoplado del release (activar una versión)

.github/workflows/deploy.yml

name: Deploy

on:
  push:
    branches: [main]

concurrency:
  group: deploy-${{ github.ref }}
  cancel-in-progress: false

permissions:
  contents: write
  packages: read
  deployments: write

jobs:
  deploy:
    name: Build and Deploy
    runs-on: ubuntu-latest
    timeout-minutes: 15
    environment:
      name: dev
      url: ${{ steps.deploy-url.outputs.url }}

    steps:
      - name: Checkout repository
        uses: actions/checkout@v6
        with:
          fetch-depth: 0

      - name: Setup pnpm
        uses: pnpm/action-setup@v4

      - name: Setup Node.js
        uses: actions/setup-node@v6
        with:
          # Node 20 reaches EOL in April 2026. Node 24 is now Active LTS.
          node-version: 22
          cache: pnpm
          registry-url: https://npm.pkg.github.com
          scope: '@org'

      - name: Install dependencies
        run: pnpm install --frozen-lockfile
        env:
          NODE_AUTH_TOKEN: ${{ secrets.GITHUB_TOKEN }}

      - name: Determine version
        id: version
        run: |
          VERSION=$(node -p "require('./package.json').version")
          SHORT_SHA=$(git rev-parse --short HEAD)
          FULL_VERSION="${VERSION}+${SHORT_SHA}"
          echo "version=${VERSION}" >> "$GITHUB_OUTPUT"
          echo "full_version=${FULL_VERSION}" >> "$GITHUB_OUTPUT"
          echo "Deploying version: ${FULL_VERSION}"

      - name: Build MFE
        run: pnpm build
        env:
          MF_PUBLIC_PATH: https://static.myplatform.com/${{ github.event.repository.name }}/${{ steps.version.outputs.version }}/
          NODE_ENV: production

      - name: Upload bundles to R2
        # Wrangler v3 reached EOL Q1 2026; use v4.
        uses: cloudflare/wrangler-action@v4
        with:
          apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }}
          command: >
            r2 object put
            static-assets/${{ github.event.repository.name }}/${{ steps.version.outputs.version }}/
            --local dist/
            --recursive

      - name: Register version with Version Config Service
        run: |
          MFE_NAME="${{ github.event.repository.name }}"
          VERSION="${{ steps.version.outputs.version }}"
          FULL_VERSION="${{ steps.version.outputs.full_version }}"
          COMMIT_SHA="${{ github.sha }}"

          curl -sf -X POST \
            "${{ vars.VERSION_CONFIG_SERVICE_URL }}/api/remotes/${MFE_NAME}/versions" \
            -H "Authorization: Bearer ${{ secrets.VCS_DEPLOY_TOKEN }}" \
            -H "Content-Type: application/json" \
            -d "{
              \"version\": \"${VERSION}\",
              \"buildId\": \"${FULL_VERSION}\",
              \"commitSha\": \"${COMMIT_SHA}\",
              \"manifestUrl\": \"https://static.myplatform.com/${MFE_NAME}/${VERSION}/mf-manifest.json\",
              \"activateIn\": [\"dev\"]
            }"

      - name: Tag release
        run: |
          git tag "v${{ steps.version.outputs.version }}"
          git push origin "v${{ steps.version.outputs.version }}"

      - name: Set deploy URL
        id: deploy-url
        run: |
          echo "url=${{ vars.SHELL_APP_DEV_URL }}" >> "$GITHUB_OUTPUT"

      - name: Notify Slack
        if: always()
        uses: slackapi/slack-github-action@v2
        with:
          webhook: ${{ secrets.SLACK_DEPLOY_WEBHOOK }}
          webhook-type: incoming-webhook
          payload: |
            {
              "text": "${{ job.status == 'success' && ':white_check_mark:' || ':x:' }} *${{ github.event.repository.name }}* v${{ steps.version.outputs.version }} deployment to *dev* ${{ job.status }}",
              "blocks": [
                {
                  "type": "section",
                  "text": {
                    "type": "mrkdwn",
                    "text": "${{ job.status == 'success' && ':white_check_mark:' || ':x:' }} *${{ github.event.repository.name }}* `v${{ steps.version.outputs.version }}` deployment to *dev* *${{ job.status }}*\n\n*Commit:* <${{ github.event.head_commit.url }}|${{ github.event.head_commit.message }}>\n*Author:* ${{ github.event.head_commit.author.name }}"
                  }
                }
              ]
            }

Flujo de despliegue:

  1. Se mergea código a main.
  2. El pipeline compila el MFE con un MF_PUBLIC_PATH de producción apuntando a la ruta versionada en R2.
  3. Los bundles compilados (incluyendo mf-manifest.json) se suben a R2 en /{mfe-name}/{version}/.
  4. La versión se registra en el Version Config Service y se auto-activa en el entorno dev.
  5. Se crea un git tag para trazabilidad.
  6. Se envía una notificación a Slack con el estado del despliegue.

La promoción a staging y production se realiza a través del Admin UI o la API, no a través del pipeline de CI. Esto desacopla el despliegue (subir artefactos) del release (activar una versión en un entorno).

Estrategia de versionado para MFEs

Las versiones de MFE siguen versionado semántico con directrices claras sobre cuándo incrementar cada segmento:

BumpCuándo usarloEjemplo
MajorCambio incompatible en el contrato de Module Federation (exports renombrados, módulos expuestos eliminados, cambios en los requisitos de dependencias compartidas)1.0.0 a 2.0.0
MinorNuevo módulo expuesto, nueva funcionalidad, adiciones a la API no incompatibles1.0.0 a 1.1.0
PatchCorrección de bugs, mejoras de rendimiento, cambios de estilos1.0.0 a 1.0.1

La versión se deriva de package.json y se etiqueta en git en cada despliegue a main.

scripts/version.sh

#!/usr/bin/env bash
set -euo pipefail

# Usage: ./scripts/version.sh <major|minor|patch>
# Bumps the version in package.json, commits, and prepares for deployment.

BUMP_TYPE="${1:?Usage: version.sh <major|minor|patch>}"

if [[ "$BUMP_TYPE" != "major" && "$BUMP_TYPE" != "minor" && "$BUMP_TYPE" != "patch" ]]; then
  echo "Error: bump type must be one of: major, minor, patch"
  exit 1
fi

# Ensure working directory is clean
if [[ -n "$(git status --porcelain)" ]]; then
  echo "Error: working directory is not clean. Commit or stash changes first."
  exit 1
fi

# Ensure we are on main
CURRENT_BRANCH=$(git branch --show-current)
if [[ "$CURRENT_BRANCH" != "main" ]]; then
  echo "Warning: you are on branch '$CURRENT_BRANCH', not 'main'."
  read -r -p "Continue anyway? [y/N] " confirm
  [[ "$confirm" =~ ^[Yy]$ ]] || exit 0
fi

# Read current version
CURRENT_VERSION=$(node -p "require('./package.json').version")
echo "Current version: $CURRENT_VERSION"

# Bump version using npm (updates package.json)
NEW_VERSION=$(npm version "$BUMP_TYPE" --no-git-tag-version)
NEW_VERSION="${NEW_VERSION#v}" # strip leading 'v'
echo "New version: $NEW_VERSION"

# Commit the version bump
git add package.json
git commit -m "chore: bump version to ${NEW_VERSION}"

echo ""
echo "Version bumped to ${NEW_VERSION}."
echo "Push to main to trigger deployment: git push origin main"
echo "The deploy pipeline will create the git tag automatically."

Los equipos también pueden usar npm version patch/minor/major directamente. El pipeline de deploy lee de package.json y crea los git tags automáticamente.


Entornos de Preview

Preview Deployments por PR

Entornos de preview: PR abierto, URL de preview única, shell más MFE en preview, revisión, merge PR Abierto CI se dispara Build + Upload R2: /pr-{n}/ URL de Preview shell + MFE preview Revisión QA + aprobación Merge limpiar preview El parámetro ?mfe_{name}=pr-{number} sobreescribe la URL del manifest en el shell

Cada pull request en un polyrepo de MFE obtiene un despliegue de preview único. El MFE de preview se sube a R2 en una ruta específica del PR y puede ser cargado por la shell app mediante un parámetro de URL que sobreescribe la configuración.

Cómo funciona:

  1. El pipeline de CI compila el MFE con MF_PUBLIC_PATH apuntando a https://static.myplatform.com/{mfe-name}/pr-{number}/.
  2. Los bundles compilados se suben a R2 en /{mfe-name}/pr-{number}/.
  3. Se publica un comentario en el PR con un enlace a la shell app cargando el MFE de preview.
  4. La shell app detecta el parámetro ?mfe_{name}=pr-{number} y sobreescribe la URL del manifest para ese MFE.
  5. Cuando el PR se cierra o se mergea, el preview se limpia.

Manejo de parámetros de URL en la shell app (simplificado):

// In the shell app's MFE resolution logic
function resolveManifestUrl(mfeName: string, defaultUrl: string): string {
  if (typeof window === 'undefined') return defaultUrl;

  const params = new URLSearchParams(window.location.search);
  const override = params.get(`mfe_${mfeName}`);

  if (override) {
    // override can be "pr-123" or a full URL
    if (override.startsWith('http')) {
      return override;
    }
    return `https://static.myplatform.com/mfe-${mfeName}/${override}/mf-manifest.json`;
  }

  return defaultUrl;
}

Workflow de limpieza de preview (.github/workflows/preview-cleanup.yml):

name: Preview Cleanup

on:
  pull_request:
    types: [closed]

permissions:
  contents: read
  deployments: write

jobs:
  cleanup:
    name: Clean Up Preview
    runs-on: ubuntu-latest
    timeout-minutes: 5

    steps:
      - name: Checkout repository
        uses: actions/checkout@v6

      - name: Delete preview assets from R2
        # Wrangler v3 reached EOL Q1 2026; use v4.
        uses: cloudflare/wrangler-action@v4
        with:
          apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }}
          command: >
            r2 object delete
            static-assets/${{ github.event.repository.name }}/pr-${{ github.event.pull_request.number }}/
            --recursive

      - name: Deactivate GitHub deployment
        uses: actions/github-script@v8
        with:
          script: |
            const deployments = await github.rest.repos.listDeployments({
              owner: context.repo.owner,
              repo: context.repo.repo,
              environment: 'preview',
              ref: context.payload.pull_request.head.ref,
            });

            for (const deployment of deployments.data) {
              await github.rest.repos.createDeploymentStatus({
                owner: context.repo.owner,
                repo: context.repo.repo,
                deployment_id: deployment.id,
                state: 'inactive',
              });
            }

      - name: Comment on PR
        uses: actions/github-script@v8
        with:
          script: |
            await github.rest.issues.createComment({
              owner: context.repo.owner,
              repo: context.repo.repo,
              issue_number: context.payload.pull_request.number,
              body: '> Preview environment has been cleaned up.',
            });

Entornos de Preview para Workers

Para cambios en Workers dentro del monorepo, los entornos de preview usan el soporte de entornos integrado de Wrangler.

Opción 1: Entorno de preview con nombre

# Deploy to a preview environment with separate KV/D1 bindings
wrangler deploy --env preview

# The wrangler.toml defines the preview environment:
# [env.preview]
# name = "shell-app-preview"
# kv_namespaces = [{ binding = "CONFIG", id = "preview-kv-id" }]
# d1_databases = [{ binding = "DB", database_name = "platform-preview", database_id = "preview-db-id" }]

Opción 2: Sesión de dev remota

# Start a remote preview session (uses actual Cloudflare infrastructure)
wrangler dev --remote

# This creates a temporary Worker that is accessible at a unique URL
# Useful for quick manual testing without a full deployment

Opción 3: Worker preview específico por PR (en CI)

      - name: Deploy Worker preview
        if: github.event_name == 'pull_request'
        # Wrangler v3 reached EOL Q1 2026; use v4.
        uses: cloudflare/wrangler-action@v4
        with:
          apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }}
          workingDirectory: workers/shell-app
          command: deploy --env preview --name shell-app-pr-${{ github.event.pull_request.number }}

Los previews de Workers usan KV namespaces y bases de datos D1 separadas para evitar contaminar los datos de producción. El wrangler.toml de cada Worker define bindings para cada entorno ([env.preview], [env.staging], [env.production]).


Gestión de Dependencias Cross-Repo

Configuración de Renovate

Renovate automatiza las actualizaciones de dependencias en todos los repositorios. Un preset compartido en el monorepo garantiza un comportamiento consistente.

Preset compartido (platform-core/.github/renovate-preset.json):

{
  "$schema": "https://docs.renovatebot.com/renovate-schema.json",
  "description": "Shared Renovate preset for the micro frontends platform",
  "extends": [
    "config:recommended",
    "helpers:pinGitHubActionDigests",
    ":semanticCommits"
  ],
  "timezone": "Europe/London",
  "schedule": ["before 9am on Monday"],
  "prHourlyLimit": 4,
  "prConcurrentLimit": 10,
  "labels": ["dependencies"],
  "hostRules": [
    {
      "matchHost": "npm.pkg.github.com",
      "hostType": "npm",
      "encrypted": {
        "token": "ENCRYPTED_GITHUB_TOKEN_HERE"
      }
    }
  ],
  "packageRules": [
    {
      "description": "Auto-merge patch updates from @org scope",
      "matchPackagePatterns": ["^@org/"],
      "matchUpdateTypes": ["patch"],
      "automerge": true,
      "automergeType": "pr",
      "automergeStrategy": "squash",
      "minimumReleaseAge": "0 days"
    },
    {
      "description": "Group all @org packages in a single PR",
      "matchPackagePatterns": ["^@org/"],
      "matchUpdateTypes": ["minor", "major"],
      "groupName": "platform-core packages",
      "groupSlug": "platform-core"
    },
    {
      "description": "Security updates get immediate PRs",
      "matchCategories": ["security"],
      "schedule": ["at any time"],
      "minimumReleaseAge": "0 days",
      "prPriority": 10,
      "labels": ["dependencies", "security"]
    },
    {
      "description": "Pin devDependencies",
      "matchDepTypes": ["devDependencies"],
      "rangeStrategy": "pin"
    },
    {
      "description": "Group Rsbuild-related packages",
      "matchPackagePatterns": ["^@rsbuild/", "^rsbuild"],
      "groupName": "rsbuild",
      "groupSlug": "rsbuild"
    },
    {
      "description": "Group Module Federation packages",
      "matchPackagePatterns": ["^@module-federation/"],
      "groupName": "module-federation",
      "groupSlug": "module-federation"
    },
    {
      "description": "Group React-related packages",
      "matchPackageNames": ["react", "react-dom", "@types/react", "@types/react-dom"],
      "groupName": "react",
      "groupSlug": "react"
    },
    {
      "description": "Group Playwright packages",
      "matchPackagePatterns": ["^@playwright/", "^playwright"],
      "groupName": "playwright",
      "groupSlug": "playwright"
    }
  ]
}

Configuración: Generar el token encriptado de Renovate. El placeholder ENCRYPTED_GITHUB_TOKEN_HERE debe reemplazarse con un token encriptado real antes de que Renovate pueda acceder a GitHub Packages. Para generarlo:

  1. Crear un GitHub Personal Access Token (classic) con el scope read:packages.
  2. Ir a https://app.renovatebot.com/encrypt y pegar el token. Seleccionar la organización de la GitHub App de Renovate como scope.
  3. Copiar el valor encriptado y reemplazar ENCRYPTED_GITHUB_TOKEN_HERE en el preset anterior.

Consultar la documentación de secretos encriptados de Renovate para más detalles.

renovate.json del polyrepo de MFE (extiende el preset compartido):

{
  "$schema": "https://docs.renovatebot.com/renovate-schema.json",
  "extends": [
    "github>myorg/platform-core:.github/renovate-preset.json"
  ]
}

Comportamientos clave:

  • Las actualizaciones patch del scope @org se auto-mergean tras pasar CI (no requieren revisión manual para cambios internos no incompatibles).
  • Las actualizaciones minor y major del scope @org se agrupan en un único PR para reducir ruido.
  • Las vulnerabilidades de seguridad omiten la planificación semanal y crean PRs de inmediato.
  • El resto de dependencias siguen la planificación semanal del lunes por la mañana.

Notificaciones vía Repository Dispatch

Cuando el monorepo publica nuevos paquetes, Renovate puede no detectar la actualización de inmediato (su planificación es semanal). Para garantizar una propagación rápida, el pipeline de release envía un evento repository-dispatch que lanza a Renovate a buscar actualizaciones de forma inmediata.

Handler de dispatch en el polyrepo de MFE (.github/workflows/renovate-trigger.yml):

name: Trigger Renovate

on:
  repository_dispatch:
    types: [platform-core-release]

permissions:
  contents: write
  pull-requests: write

jobs:
  trigger-renovate:
    name: Trigger Renovate Update Check
    runs-on: ubuntu-latest
    timeout-minutes: 10

    steps:
      - name: Log received packages
        run: |
          echo "Received platform-core release notification:"
          echo '${{ toJson(github.event.client_payload.packages) }}' | jq .

      - name: Trigger Renovate via GitHub API
        uses: actions/github-script@v8
        with:
          script: |
            // Create a repository dispatch to trigger Renovate's on-demand check
            await github.rest.repos.createDispatchEvent({
              owner: context.repo.owner,
              repo: context.repo.repo,
              event_type: 'renovate',
            });
            console.log('Triggered Renovate update check');

Esto crea un ciclo de feedback ajustado: el monorepo publica una nueva versión, los repos de MFE reciben un PR de Renovate en minutos, el auto-merge se activa para actualizaciones patch, y el MFE se recompila con los últimos paquetes compartidos.


Despliegue de Workers

wrangler deploy

Cada Worker en el monorepo se despliega usando wrangler deploy con configuración específica por entorno.

Comandos de despliegue por tipo de Worker:

# Shell App Worker
wrangler deploy --config workers/shell-app/wrangler.toml --env production

# Version Config Service Worker
wrangler deploy --config workers/version-config-service/wrangler.toml --env production

# Auth Gateway Worker
wrangler deploy --config workers/auth-gateway/wrangler.toml --env production

# Static Asset Server Worker (if not using R2 public bucket)
wrangler deploy --config workers/static-server/wrangler.toml --env production

Gestión de secrets:

# Set secrets for each Worker (done once, not in CI)
# Shell App
wrangler secret put WORKOS_API_KEY --config workers/shell-app/wrangler.toml --env production
wrangler secret put WORKOS_CLIENT_ID --config workers/shell-app/wrangler.toml --env production
wrangler secret put SESSION_SECRET --config workers/shell-app/wrangler.toml --env production

# Version Config Service
wrangler secret put DEPLOY_TOKEN --config workers/version-config-service/wrangler.toml --env production
wrangler secret put ADMIN_API_KEY --config workers/version-config-service/wrangler.toml --env production

# Auth Gateway
wrangler secret put WORKOS_API_KEY --config workers/auth-gateway/wrangler.toml --env production
wrangler secret put JWT_SIGNING_KEY --config workers/auth-gateway/wrangler.toml --env production

Workflow de despliegue de Workers (.github/workflows/deploy-workers.yml):

name: Deploy Workers

on:
  push:
    branches: [main]
    paths:
      - 'workers/**'
      - 'packages/shared-utils/**'

concurrency:
  group: deploy-workers-${{ github.ref }}
  cancel-in-progress: false

permissions:
  contents: read

jobs:
  detect-changes:
    name: Detect Changed Workers
    runs-on: ubuntu-latest
    outputs:
      shell-app: ${{ steps.changes.outputs.shell-app }}
      version-config-service: ${{ steps.changes.outputs.version-config-service }}
      auth-gateway: ${{ steps.changes.outputs.auth-gateway }}
    steps:
      - name: Checkout repository
        uses: actions/checkout@v6

      - name: Detect changes
        id: changes
        uses: dorny/paths-filter@v3
        with:
          filters: |
            shell-app:
              - 'workers/shell-app/**'
              - 'packages/shared-utils/**'
            version-config-service:
              - 'workers/version-config-service/**'
              - 'packages/shared-utils/**'
            auth-gateway:
              - 'workers/auth-gateway/**'
              - 'packages/shared-utils/**'

  deploy-shell-app:
    name: Deploy Shell App Worker
    needs: detect-changes
    if: needs.detect-changes.outputs.shell-app == 'true'
    runs-on: ubuntu-latest
    timeout-minutes: 10
    environment:
      name: production
      url: https://app.myplatform.com

    steps:
      - name: Checkout repository
        uses: actions/checkout@v6

      - name: Setup pnpm
        uses: pnpm/action-setup@v4

      - name: Setup Node.js
        uses: actions/setup-node@v6
        with:
          # Node 20 reaches EOL in April 2026. Node 24 is now Active LTS.
          node-version: 22
          cache: pnpm

      - name: Install dependencies
        run: pnpm install --frozen-lockfile

      - name: Build shared dependencies
        run: pnpm turbo run build --filter=shell-app...

      - name: Deploy to production
        # Wrangler v3 reached EOL Q1 2026; use v4.
        uses: cloudflare/wrangler-action@v4
        with:
          apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }}
          workingDirectory: workers/shell-app
          command: deploy --env production

  deploy-version-config-service:
    name: Deploy Version Config Service
    needs: detect-changes
    if: needs.detect-changes.outputs.version-config-service == 'true'
    runs-on: ubuntu-latest
    timeout-minutes: 10
    environment:
      name: production
      url: https://vcs.myplatform.com

    steps:
      - name: Checkout repository
        uses: actions/checkout@v6

      - name: Setup pnpm
        uses: pnpm/action-setup@v4

      - name: Setup Node.js
        uses: actions/setup-node@v6
        with:
          # Node 20 reaches EOL in April 2026. Node 24 is now Active LTS.
          node-version: 22
          cache: pnpm

      - name: Install dependencies
        run: pnpm install --frozen-lockfile

      - name: Build shared dependencies
        run: pnpm turbo run build --filter=version-config-service...

      - name: Run D1 migrations
        id: d1-migrate
        # Wrangler v3 reached EOL Q1 2026; use v4.
        uses: cloudflare/wrangler-action@v4
        with:
          apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }}
          workingDirectory: workers/version-config-service
          command: d1 migrations apply platform-db --env production

      - name: Verify D1 migrations succeeded
        if: steps.d1-migrate.outcome != 'success'
        run: |
          echo "::error::D1 migrations failed. Aborting deployment to prevent deploying code against an incompatible database schema."
          exit 1

      - name: Deploy to production
        # Only deploy if D1 migrations succeeded (the verify step above
        # will have failed the workflow otherwise, but this condition
        # provides an additional safety gate).
        if: steps.d1-migrate.outcome == 'success'
        # Wrangler v3 reached EOL Q1 2026; use v4.
        uses: cloudflare/wrangler-action@v4
        with:
          apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }}
          workingDirectory: workers/version-config-service
          command: deploy --env production

  deploy-auth-gateway:
    name: Deploy Auth Gateway
    needs: detect-changes
    if: needs.detect-changes.outputs.auth-gateway == 'true'
    runs-on: ubuntu-latest
    timeout-minutes: 10
    environment:
      name: production
      url: https://auth.myplatform.com

    steps:
      - name: Checkout repository
        uses: actions/checkout@v6

      - name: Setup pnpm
        uses: pnpm/action-setup@v4

      - name: Setup Node.js
        uses: actions/setup-node@v6
        with:
          # Node 20 reaches EOL in April 2026. Node 24 is now Active LTS.
          node-version: 22
          cache: pnpm

      - name: Install dependencies
        run: pnpm install --frozen-lockfile

      - name: Build shared dependencies
        run: pnpm turbo run build --filter=auth-gateway...

      - name: Deploy to production
        # Wrangler v3 reached EOL Q1 2026; use v4.
        uses: cloudflare/wrangler-action@v4
        with:
          apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }}
          workingDirectory: workers/auth-gateway
          command: deploy --env production

Estrategia de despliegue

Cloudflare Workers se despliegan atómicamente en toda la red global. No existe el concepto de "blue/green" o "canary" a nivel de aplicación -- cada despliegue reemplaza la versión anterior a nivel mundial en segundos.

Despliegue gradual:

Cloudflare ofrece una funcionalidad integrada de despliegue gradual (llamada "Gradual Deployments" o "Versioned Deployments") que permite dividir el tráfico entre dos versiones de un Worker.

# Deploy a new version without activating it
wrangler versions upload --config workers/shell-app/wrangler.toml --env production

# Gradually roll out: 10% new version, 90% current version
wrangler versions deploy \
  --version-id <new-version-id>=10% \
  --version-id <current-version-id>=90% \
  --config workers/shell-app/wrangler.toml \
  --env production

# Increase to 50/50
wrangler versions deploy \
  --version-id <new-version-id>=50% \
  --version-id <current-version-id>=50% \
  --config workers/shell-app/wrangler.toml \
  --env production

# Full rollout
wrangler versions deploy \
  --version-id <new-version-id>=100% \
  --config workers/shell-app/wrangler.toml \
  --env production

Rollback:

# Instant rollback to the previous version
wrangler rollback --config workers/shell-app/wrangler.toml --env production

# Or redeploy a specific previous version
wrangler versions deploy \
  --version-id <previous-version-id>=100% \
  --config workers/shell-app/wrangler.toml \
  --env production

Para rollbacks de MFE, se usa el Version Config Service en su lugar:

# Activate a previous MFE version in production
curl -X POST \
  "https://vcs.myplatform.com/api/remotes/mfe-dashboard/activate" \
  -H "Authorization: Bearer $DEPLOY_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "version": "1.2.3",
    "environment": "production"
  }'

Esto actualiza inmediatamente la configuración de versiones, y la shell app comienza a cargar la versión anterior del MFE en la siguiente carga de página.


Seguridad del Pipeline

GitHub Actions OIDC para Cloudflare

En lugar de almacenar tokens de API de Cloudflare de larga duración en los secrets de GitHub, los pipelines deberían usar GitHub Actions OIDC (OpenID Connect) para obtener tokens de corta duración.

# In the workflow job
jobs:
  deploy:
    permissions:
      id-token: write  # Required for OIDC
      contents: read

    steps:
      - name: Deploy to Cloudflare
        # Wrangler v3 reached EOL Q1 2026; use v4.
        uses: cloudflare/wrangler-action@v4
        with:
          apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }}
          # When Cloudflare supports OIDC natively, this will change to:
          # oidc:
          #   audience: "https://myplatform.cloudflareaccess.com"

TODO (objetivo: Q3 2026): Reemplazar los secrets CLOUDFLARE_API_TOKEN con federación OIDC de GitHub Actions una vez que Cloudflare añada soporte nativo de OIDC para despliegues de Wrangler/Workers. Esto eliminará todos los tokens de API de Cloudflare de larga duración de los secrets de CI y se alineará con la postura de seguridad zero-trust. Seguir el progreso en el Cloudflare OIDC feature request y actualizar todos los pasos de cloudflare/wrangler-action para usar el bloque de configuración oidc en lugar de apiToken.

Estado actual: Los tokens de API de Cloudflare se almacenan como secrets a nivel de organización en GitHub. Estos tokens deben tener permisos mínimos (p. ej., Workers Scripts:Edit, R2:Edit, D1:Edit por cuenta) y rotarse trimestralmente hasta que OIDC esté disponible.

Gestión de Secrets

# Secrets are organized by scope and stored in GitHub repository/organization secrets

# Organization-level secrets (shared across all repos):
# - CLOUDFLARE_API_TOKEN       : Cloudflare API token for deployments
# - CLOUDFLARE_ACCOUNT_ID      : Cloudflare account identifier
# - SLACK_DEPLOY_WEBHOOK       : Slack webhook for deployment notifications
# - TURBO_TOKEN                : Turborepo remote cache token

# Repository-level secrets (per-repo):
# - VCS_DEPLOY_TOKEN           : Token for Version Config Service API
# - DISPATCH_TOKEN             : GitHub PAT for cross-repo dispatch events

# Environment-level secrets (per deployment environment):
# - WORKOS_API_KEY             : WorkOS API key (different per environment)
# - SESSION_SECRET             : Session encryption secret
# - JWT_SIGNING_KEY            : JWT signing key

Reglas de Protección de Ramas

Todos los repositorios aplican las siguientes reglas de protección de ramas en main:

ReglaConfiguración
Requerir revisión de pull requests1 aprobación mínima
Descartar revisiones obsoletas en nuevos pushesHabilitado
Requerir que los status checks pasenEl workflow de CI debe pasar
Requerir que las ramas estén actualizadasHabilitado
Requerir commits firmadosOpcional (recomendado)
Restringir quién puede hacer push a ramas que coincidanSolo automatización de releases
Permitir force pushesDeshabilitado
Permitir eliminacionesDeshabilitado

Escaneo de Dependencias

# GitHub Dependabot is enabled for security alerts (in .github/dependabot.yml)
version: 2
updates:
  - package-ecosystem: npm
    directory: /
    schedule:
      interval: weekly
    # Note: Renovate handles dependency PRs; Dependabot is only for security alerts
    open-pull-requests-limit: 0

  - package-ecosystem: github-actions
    directory: /
    schedule:
      interval: weekly
    open-pull-requests-limit: 5

Renovate gestiona las actualizaciones regulares de dependencias, mientras que Dependabot proporciona alertas de vulnerabilidades de seguridad y puede crear PRs de seguridad de emergencia.


Monitorización y Notificaciones

Integración con Slack

Todos los pipelines de despliegue envían notificaciones a Slack con estado, versión e información del commit.

Configuración de notificación a Slack (acción reutilizable .github/actions/notify-slack/action.yml):

name: Notify Slack
description: Send deployment notification to Slack

inputs:
  webhook-url:
    description: Slack webhook URL
    required: true
  status:
    description: Deployment status (success, failure, cancelled)
    required: true
  environment:
    description: Target environment name
    required: true
  version:
    description: Deployed version
    required: false
    default: 'N/A'
  service-name:
    description: Name of the deployed service
    required: true

runs:
  using: composite
  steps:
    - name: Send Slack notification
      uses: slackapi/slack-github-action@v2
      with:
        webhook: ${{ inputs.webhook-url }}
        webhook-type: incoming-webhook
        payload: |
          {
            "blocks": [
              {
                "type": "header",
                "text": {
                  "type": "plain_text",
                  "text": "${{ inputs.status == 'success' && 'Deployment Succeeded' || inputs.status == 'failure' && 'Deployment Failed' || 'Deployment Cancelled' }}"
                }
              },
              {
                "type": "section",
                "fields": [
                  { "type": "mrkdwn", "text": "*Service:*\n${{ inputs.service-name }}" },
                  { "type": "mrkdwn", "text": "*Version:*\n`${{ inputs.version }}`" },
                  { "type": "mrkdwn", "text": "*Environment:*\n${{ inputs.environment }}" },
                  { "type": "mrkdwn", "text": "*Status:*\n${{ inputs.status }}" }
                ]
              },
              {
                "type": "context",
                "elements": [
                  {
                    "type": "mrkdwn",
                    "text": "<${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}|View workflow run> | Triggered by ${{ github.actor }}"
                  }
                ]
              }
            ]
          }

Registro de Eventos de Despliegue

Cada despliegue se registra en la base de datos D1 del Version Config Service para auditoría y el dashboard del Admin UI.

// workers/version-config-service/src/handlers/deployments.ts

interface DeploymentEvent {
  id: string;
  remoteName: string;
  version: string;
  environment: string;
  action: 'registered' | 'activated' | 'deactivated' | 'rolled_back';
  triggeredBy: string;
  commitSha: string | null;
  buildId: string | null;
  timestamp: string;
  metadata: Record<string, string>;
}

export async function logDeploymentEvent(
  db: D1Database,
  event: Omit<DeploymentEvent, 'id' | 'timestamp'>
): Promise<DeploymentEvent> {
  const id = crypto.randomUUID();
  const timestamp = new Date().toISOString();

  await db
    .prepare(
      `INSERT INTO deployment_events
        (id, remote_name, version, environment, action, triggered_by, commit_sha, build_id, timestamp, metadata)
       VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`
    )
    .bind(
      id,
      event.remoteName,
      event.version,
      event.environment,
      event.action,
      event.triggeredBy,
      event.commitSha,
      event.buildId,
      timestamp,
      JSON.stringify(event.metadata)
    )
    .run();

  return { ...event, id, timestamp };
}

export async function getDeploymentHistory(
  db: D1Database,
  remoteName: string,
  options: { environment?: string; limit?: number } = {}
): Promise<DeploymentEvent[]> {
  const { environment, limit = 50 } = options;

  let query = `SELECT * FROM deployment_events WHERE remote_name = ?`;
  const params: (string | number)[] = [remoteName];

  if (environment) {
    query += ` AND environment = ?`;
    params.push(environment);
  }

  query += ` ORDER BY timestamp DESC LIMIT ?`;
  params.push(limit);

  const result = await db
    .prepare(query)
    .bind(...params)
    .all<DeploymentEvent>();

  return result.results;
}

Alertas de Despliegues Fallidos

Los despliegues fallidos disparan alertas adicionales más allá de Slack:

# Added to deployment workflows as a final step
      - name: Alert on failure
        if: failure()
        uses: actions/github-script@v8
        with:
          script: |
            // Create a GitHub issue for tracking
            const title = `Deployment failure: ${{ github.event.repository.name }} to ${{ job.environment }}`;
            const body = [
              `## Deployment Failure`,
              ``,
              `| Field | Value |`,
              `|---|---|`,
              `| **Service** | ${{ github.event.repository.name }} |`,
              `| **Environment** | ${{ job.environment || 'dev' }} |`,
              `| **Workflow run** | [View logs](${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}) |`,
              `| **Commit** | [${{ github.sha }}](${{ github.server_url }}/${{ github.repository }}/commit/${{ github.sha }}) |`,
              `| **Triggered by** | @${{ github.actor }} |`,
              ``,
              `Please investigate and resolve.`,
            ].join('\n');

            await github.rest.issues.create({
              owner: context.repo.owner,
              repo: context.repo.repo,
              title,
              body,
              labels: ['deployment-failure', 'priority:high'],
              assignees: [context.actor],
            });

Dashboard de Despliegues del Admin UI

El Admin UI (un MFE en sí mismo) proporciona un dashboard de despliegues que lee de la API del Version Config Service:

// In the Admin MFE: hooks/useDeploymentHistory.ts

import { useQuery } from '@tanstack/react-query';

interface DeploymentEvent {
  id: string;
  remoteName: string;
  version: string;
  environment: string;
  action: 'registered' | 'activated' | 'deactivated' | 'rolled_back';
  triggeredBy: string;
  commitSha: string | null;
  buildId: string | null;
  timestamp: string;
}

export function useDeploymentHistory(
  remoteName: string,
  environment?: string
) {
  return useQuery({
    queryKey: ['deployment-history', remoteName, environment],
    queryFn: async (): Promise<DeploymentEvent[]> => {
      const params = new URLSearchParams();
      if (environment) params.set('environment', environment);

      const response = await fetch(
        `/api/remotes/${remoteName}/deployments?${params}`,
        {
          headers: {
            Authorization: `Bearer ${getAdminToken()}`,
          },
        }
      );

      if (!response.ok) {
        throw new Error(`Failed to fetch deployment history: ${response.statusText}`);
      }

      return response.json();
    },
    refetchInterval: 30_000, // Poll every 30 seconds
  });
}

El dashboard muestra:

  • Una línea temporal de eventos de despliegue por MFE y entorno.
  • Las versiones activas actualmente en cada entorno.
  • Acciones rápidas: activar una versión, hacer rollback, comparar versiones.
  • Metadatos del build incluyendo commit SHA, build ID y origen del trigger.

Referencias