CI/CD & Deployment

Table of Contents


Overview

Dual pipeline architecture: Monorepo pipeline (left) and MFE pipeline (right) side by side Monorepo Pipeline (platform-core) Turborepo CI Changesets Version Publish to GPR Design System Shared SDK / Utils Worker Deploys MFE Pipeline (polyrepos) Build + Test Upload to R2 Register Version Preview per PR Auto-activate dev Promote via Admin

The platform operates two distinct CI/CD pipeline architectures that reflect its hybrid monorepo/polyrepo structure:

  1. Monorepo pipeline (platform-core) -- Manages shared packages (design system, SDK, utilities, Workers) with coordinated versioning via Changesets and publishing to GitHub Packages.
  2. MFE polyrepo pipelines -- Each micro frontend repository has its own independent pipeline that builds, tests, deploys, and registers the MFE with the Version Config Service.

Key Principles

PrincipleImplementation
Fast feedbackTurborepo remote caching, parallelized CI steps, incremental builds
Independent deployabilityEach MFE deploys on its own cadence without coordinating with other teams
Automated version managementChangesets for monorepo packages; semantic git tags for MFEs
Preview environmentsEvery PR gets a unique preview URL for the changed MFE
SecurityOIDC authentication to Cloudflare (planned), short-lived API tokens in CI

Technology Stack

  • CI/CD Platform: GitHub Actions
  • Monorepo orchestration: Turborepo (with remote caching)
  • Package management: pnpm (with workspace protocol in monorepo)
  • Version management: Changesets (monorepo), semantic git tags (polyrepos)
  • Package registry: GitHub Packages (npm)
  • Deployment target: Cloudflare Workers, R2, KV
  • Dependency automation: Renovate

Monorepo CI/CD (platform-core)

The platform-core monorepo contains all shared infrastructure: the design system, the MFE SDK, shared TypeScript configs, Workers (Shell App, Version Config Service, Auth Gateway), and shared utilities.

CI Pipeline (on every PR)

The CI pipeline runs on every pull request and push to main. It leverages Turborepo to only run tasks for packages that have changed, dramatically reducing CI times for targeted changes.

.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

How Turborepo caching works in CI:

  • Each task (lint, typecheck, test, build) declares its inputs in turbo.json.
  • On first run, outputs are hashed and uploaded to the Turborepo remote cache.
  • Subsequent runs on the same inputs produce cache hits, skipping execution entirely.
  • A PR that only modifies the design system will skip lint/test/build for Workers and the SDK.

Release Pipeline (Changesets)

Monorepo release flow: Changesets to version PR to merge to publish to GitHub Packages Changesets .changeset/*.md Version PR bump + changelog Merge to main Publish npm packages GitHub Packages @org/* registry After publish: repository-dispatch notifies all MFE polyrepos for Renovate updates

The release pipeline automates version bumping, changelog generation, and publishing using Changesets. Developers add changeset files during development (pnpm changeset), and the release pipeline consumes them.

.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.`);
            }

Release flow:

  1. Developer creates a PR with code changes and a changeset file (e.g., .changeset/cool-feature.md).
  2. PR is reviewed and merged to main.
  3. The Changesets Action detects pending changesets and opens (or updates) a "Version Packages" PR.
  4. The "Version Packages" PR contains bumped package.json versions and updated CHANGELOG.md files.
  5. When the team merges the "Version Packages" PR, the action publishes all bumped packages to GitHub Packages.
  6. After publishing, a repository-dispatch event notifies all MFE polyrepos so Renovate can immediately create update PRs.

Design System MF Remote Deployment

The design system package (@org/design-system) serves a dual role: it is published to GitHub Packages as an npm package and deployed as a Module Federation remote so MFEs can consume shared components at runtime.

After a successful npm publish, the release workflow also deploys the design system bundle to R2 and registers it with the Version Config Service.

Additional deploy step appended to .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"]
            }'

This ensures that any new version of the design system is immediately available as a Module Federation remote in the dev environment. Promotion to staging and production is handled through the Admin UI or the Version Config Service API.


MFE CI/CD (polyrepos)

Each micro frontend lives in its own repository with its own CI/CD pipeline. The pipelines follow a consistent structure but can be customized per team.

CI Pipeline (on every 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,
              });
            }

Key aspects of the MFE CI pipeline:

  • The .npmrc in each polyrepo is configured to pull @org scoped packages from GitHub Packages.
  • The build step sets MF_PUBLIC_PATH to the preview URL so that chunk references resolve correctly.
  • E2E tests run against the built MFE loaded in an isolated shell environment.
  • The preview deployment uploads the build to R2 at a PR-specific path.

Deploy Pipeline (on merge to main)

MFE deploy pipeline: build, R2 upload, version register, preview URL, promote Build MFE Rsbuild prod R2 Upload /{mfe}/{version}/ Register Version Config Service API Activate dev auto on merge Promote staging/prod Deployment (uploading artifacts) is decoupled from release (activating a version)

.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 }}"
                  }
                }
              ]
            }

Deployment flow:

  1. Code is merged to main.
  2. The pipeline builds the MFE with a production MF_PUBLIC_PATH set to the versioned R2 path.
  3. The built bundles (including mf-manifest.json) are uploaded to R2 at /{mfe-name}/{version}/.
  4. The version is registered with the Version Config Service and auto-activated in the dev environment.
  5. A git tag is created for traceability.
  6. A Slack notification is sent with the deployment status.

Promotion to staging and production is performed through the Admin UI or API, not through the CI pipeline. This decouples deployment (uploading artifacts) from release (activating a version in an environment).

Version Strategy for MFEs

MFE versions follow semantic versioning with clear guidelines for when to bump each segment:

BumpWhen to UseExample
MajorBreaking change to the Module Federation contract (renamed exports, removed exposed modules, changed shared dependency requirements)1.0.0 to 2.0.0
MinorNew exposed module, new feature, non-breaking API additions1.0.0 to 1.1.0
PatchBug fixes, performance improvements, styling changes1.0.0 to 1.0.1

The version is derived from package.json and tagged in git on every deployment to 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."

Teams can also use npm version patch/minor/major directly. The deploy pipeline reads from package.json and creates git tags automatically.


Preview Environments

Per-PR Preview Deployments

Preview environments: PR opened, unique preview URL, shell plus MFE preview, review, merge PR Opened CI triggers Build + Upload R2: /pr-{n}/ Preview URL shell + MFE preview Review QA + approve Merge cleanup preview ?mfe_{name}=pr-{number} query param overrides the manifest URL in the shell

Every pull request in an MFE polyrepo gets a unique preview deployment. The preview MFE is uploaded to R2 at a PR-specific path and can be loaded by the shell app via a URL parameter override.

How it works:

  1. The CI pipeline builds the MFE with MF_PUBLIC_PATH set to https://static.myplatform.com/{mfe-name}/pr-{number}/.
  2. The built bundles are uploaded to R2 at /{mfe-name}/pr-{number}/.
  3. A comment is posted on the PR with a link to the shell app loading the preview MFE.
  4. The shell app detects the ?mfe_{name}=pr-{number} query parameter and overrides the manifest URL for that MFE.
  5. When the PR is closed or merged, the preview is cleaned up.

Shell app query parameter handling (simplified):

// 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;
}

Preview cleanup workflow (.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.',
            });

Worker Preview Environments

For Worker changes in the monorepo, preview environments use Wrangler's built-in environment support.

Option 1: Named preview environment

# 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" }]

Option 2: Remote dev session

# 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

Option 3: PR-specific Worker preview (in 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 }}

Worker previews use separate KV namespaces and D1 databases to avoid polluting production data. The wrangler.toml for each Worker defines bindings for each environment ([env.preview], [env.staging], [env.production]).


Cross-Repo Dependency Management

Renovate Configuration

Renovate automates dependency updates across all repositories. A shared preset in the monorepo ensures consistent behavior.

Shared preset (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"
    }
  ]
}

Setup: Generating the encrypted Renovate token. The ENCRYPTED_GITHUB_TOKEN_HERE placeholder must be replaced with a real encrypted token before Renovate can access GitHub Packages. To generate it:

  1. Create a GitHub Personal Access Token (classic) with the read:packages scope.
  2. Go to https://app.renovatebot.com/encrypt and paste the token. Select your Renovate GitHub App organization as the scope.
  3. Copy the encrypted value and replace ENCRYPTED_GITHUB_TOKEN_HERE in the preset above.

See the Renovate encrypted secrets documentation for details.

MFE polyrepo renovate.json (extends the shared preset):

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

Key behaviors:

  • Patch updates from @org scope are auto-merged after CI passes (no manual review needed for non-breaking internal changes).
  • Minor and major updates from @org scope are grouped into a single PR to reduce noise.
  • Security vulnerabilities bypass the weekly schedule and create PRs immediately.
  • All other dependencies follow the weekly Monday morning schedule.

Repository Dispatch Notifications

When the monorepo publishes new packages, Renovate may not detect the update immediately (its schedule is weekly). To ensure fast propagation, the release pipeline sends a repository-dispatch event that triggers Renovate to check for updates immediately.

Dispatch handler in MFE polyrepo (.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');

This creates a tight feedback loop: monorepo publishes a new version, MFE repos get a Renovate PR within minutes, auto-merge kicks in for patch updates, and the MFE is rebuilt with the latest shared packages.


Worker Deployments

wrangler deploy

Each Worker in the monorepo is deployed using wrangler deploy with environment-specific configuration.

Deployment commands by Worker type:

# 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

Managing 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

Worker deployment workflow (.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

Deployment Strategy

Cloudflare Workers are deployed atomically across the global network. There is no concept of "blue/green" or "canary" at the application level -- every deployment replaces the previous version worldwide within seconds.

Gradual rollout:

Cloudflare provides a built-in gradual deployment feature (called "Gradual Deployments" or "Versioned Deployments") that allows you to split traffic between two versions of a 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

For MFE rollbacks, the Version Config Service is used instead:

# 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"
  }'

This immediately updates the version config, and the shell app starts loading the previous MFE version on the next page load.


Pipeline Security

GitHub Actions OIDC for Cloudflare

Instead of storing long-lived Cloudflare API tokens in GitHub secrets, the pipelines should use GitHub Actions OIDC (OpenID Connect) to obtain short-lived tokens.

# 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 (target: Q3 2026): Replace CLOUDFLARE_API_TOKEN secrets with GitHub Actions OIDC federation once Cloudflare adds native OIDC support for Wrangler/Workers deployments. This will eliminate all long-lived Cloudflare API tokens from CI secrets and align with the zero-trust security posture. Track progress at the Cloudflare OIDC feature request and update all cloudflare/wrangler-action steps to use the oidc configuration block instead of apiToken.

Current state: Cloudflare API tokens are stored as GitHub organization-level secrets. These tokens should be scoped with minimal permissions (e.g., Workers Scripts:Edit, R2:Edit, D1:Edit per account) and rotated quarterly until OIDC is available.

Secrets Management

# 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

Branch Protection Rules

All repositories enforce the following branch protection rules on main:

RuleSetting
Require pull request reviews1 approval minimum
Dismiss stale reviews on new pushesEnabled
Require status checks to passCI workflow must pass
Require branches to be up to dateEnabled
Require signed commitsOptional (recommended)
Restrict who can push to matching branchesOnly release automation
Allow force pushesDisabled
Allow deletionsDisabled

Dependency Scanning

# 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 handles regular dependency updates, while Dependabot provides security vulnerability alerts and can create emergency security PRs.


Monitoring and Notifications

Slack Integration

All deployment pipelines send notifications to Slack with status, version, and commit information.

Slack notification configuration (reusable action .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 }}"
                  }
                ]
              }
            ]
          }

Deployment Event Logging

Every deployment is recorded in the Version Config Service's D1 database for auditing and the Admin UI dashboard.

// 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;
}

Failed Deployment Alerts

Failed deployments trigger additional alerting beyond 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],
            });

Admin UI Deployment Dashboard

The Admin UI (an MFE itself) provides a deployment dashboard that reads from the Version Config Service API:

// 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
  });
}

The dashboard shows:

  • A timeline of deployment events per MFE and environment.
  • Currently active versions in each environment.
  • Quick actions: activate a version, roll back, compare versions.
  • Build metadata including commit SHA, build ID, and trigger source.

References