Multi-Site Astro Deployment: Git Submodules + GitHub Actions

· combray's blog


Summary #

Combining multiple Astro sites using git submodules is straightforward with GitHub Actions. The approach involves: (1) adding your separate Astro repos as git submodules, (2) checking them out recursively in CI, (3) building each sub-site with a custom output path, and (4) deploying the combined result[1][2].

The key insight is that you cannot use withastro/action@v2 directly for multi-site builds—it's designed for single-site deployment. Instead, you need a custom workflow that builds each submodule independently and merges the outputs before uploading the pages artifact[3]. GitHub's actions/checkout@v4 has first-class support for recursive submodule checkout[1].

For your use case (outputting to public/reports/aie-coding-2025), you have two options: (A) configure each sub-site's outDir in astro.config.mjs, or (B) build to default dist/ and copy to the target location. Option B is simpler and more reliable[4].

Philosophy & Mental Model #

Think of this as a build-time composition pattern:

Main Site (thefocus-landing)
├── src/pages/           → builds to dist/
├── public/              → copied to dist/ as-is
└── submodules/
    ├── aie-report/      → builds to dist/reports/aie-coding-2025
    └── weekend-warrior/ → builds to dist/weekend-warrior/coding-agent

Each submodule is a complete, standalone Astro project that can be developed and tested independently. At build time, they're all compiled and their outputs are merged into the main site's dist/ directory.

The submodule approach differs from a monorepo: submodules maintain their own git history, can be versioned independently, and can be reused across multiple parent projects[5].

Setup #

Step 1: Add Submodules to Your Repository #

1# From your main repo root
2git submodule add https://github.com/your-org/aie-report.git submodules/aie-report
3git submodule add https://github.com/your-org/weekend-warrior.git submodules/weekend-warrior
4
5# Commit the .gitmodules file and submodule references
6git add .gitmodules submodules/
7git commit -m "Add submodule sites"

This creates a .gitmodules file:

1[submodule "submodules/aie-report"]
2    path = submodules/aie-report
3    url = https://github.com/your-org/aie-report.git
4
5[submodule "submodules/weekend-warrior"]
6    path = submodules/weekend-warrior
7    url = https://github.com/your-org/weekend-warrior.git

Step 2: Configure Sub-Site Base Paths #

Each submodule's Astro config needs the correct base path for assets to work:

submodules/aie-report/astro.config.mjs:

1import { defineConfig } from "astro/config";
2
3export default defineConfig({
4  // Critical: set base to match final deployment path
5  base: "/reports/aie-coding-2025",
6  // Optional: can also customize outDir, but we'll handle this in CI
7  // outDir: "../../public/reports/aie-coding-2025",
8});

submodules/weekend-warrior/astro.config.mjs:

1import { defineConfig } from "astro/config";
2
3export default defineConfig({
4  base: "/weekend-warrior/coding-agent",
5});

Step 3: Create the Combined Build Workflow #

Replace your .github/workflows/deploy.yml:

 1name: Deploy Multi-Site to GitHub Pages
 2
 3on:
 4  push:
 5    branches: [main]
 6  workflow_dispatch:
 7
 8permissions:
 9  contents: read
10  pages: write
11  id-token: write
12
13jobs:
14  build:
15    runs-on: ubuntu-latest
16    steps:
17      - name: Send Telegram Build start
18        run: |
19          curl -s -X POST "https://api.telegram.org/bot${{ secrets.TELEGRAM_BOT_TOKEN }}/sendMessage" \
20          -d chat_id="${{ secrets.TELEGRAM_CHAT_ID }}" \
21          -d text="🔨 Multi-site build starting for ${{ github.repository }}!"
22
23      - name: Checkout with submodules
24        uses: actions/checkout@v4
25        with:
26          submodules: recursive
27          # For private submodules, uncomment and add GH_PAT secret:
28          # token: ${{ secrets.GH_PAT }}
29
30      - name: Setup pnpm
31        uses: pnpm/action-setup@v4
32        with:
33          version: 9
34
35      - name: Setup Node.js
36        uses: actions/setup-node@v4
37        with:
38          node-version: 20
39          cache: pnpm
40
41      # Build main site
42      - name: Install main site dependencies
43        run: pnpm install
44
45      - name: Build main site
46        run: pnpm build
47
48      # Build submodule: aie-report
49      - name: Build aie-report submodule
50        run: |
51          cd submodules/aie-report
52          pnpm install
53          pnpm build
54          # Copy built site to main dist folder
55          mkdir -p ../../dist/reports/aie-coding-2025
56          cp -r dist/* ../../dist/reports/aie-coding-2025/
57
58      # Build submodule: weekend-warrior
59      - name: Build weekend-warrior submodule
60        run: |
61          cd submodules/weekend-warrior
62          pnpm install
63          pnpm build
64          mkdir -p ../../dist/weekend-warrior/coding-agent
65          cp -r dist/* ../../dist/weekend-warrior/coding-agent/
66
67      - name: Upload Pages artifact
68        uses: actions/upload-pages-artifact@v3
69        with:
70          path: dist/
71
72      - name: Send Telegram Success
73        if: success()
74        run: |
75          curl -s -X POST "https://api.telegram.org/bot${{ secrets.TELEGRAM_BOT_TOKEN }}/sendMessage" \
76          -d chat_id="${{ secrets.TELEGRAM_CHAT_ID }}" \
77          -d text="✅ Multi-site build succeeded for ${{ github.repository }}!"
78
79      - name: Send Telegram Failure
80        if: failure()
81        run: |
82          curl -s -X POST "https://api.telegram.org/bot${{ secrets.TELEGRAM_BOT_TOKEN }}/sendMessage" \
83          -d chat_id="${{ secrets.TELEGRAM_CHAT_ID }}" \
84          -d text="❌ Multi-site build failed for ${{ github.repository }}!"
85
86  deploy:
87    needs: build
88    runs-on: ubuntu-latest
89    environment:
90      name: github-pages
91      url: ${{ steps.deployment.outputs.page_url }}
92    steps:
93      - name: Deploy to GitHub Pages
94        id: deployment
95        uses: actions/deploy-pages@v4

Step 4: Auto-Update Submodules with Dependabot (Optional) #

Create .github/dependabot.yml to automatically create PRs when submodules update[6]:

1version: 2
2updates:
3  - package-ecosystem: gitsubmodule
4    directory: /
5    schedule:
6      interval: daily
7    open-pull-requests-limit: 5

Core Usage Patterns #

Pattern 1: Update Submodules Locally #

 1# Pull latest changes for all submodules
 2git submodule update --remote --merge
 3
 4# Or update a specific submodule
 5git submodule update --remote submodules/aie-report
 6
 7# Commit the updated submodule reference
 8git add submodules/aie-report
 9git commit -m "Update aie-report submodule to latest"
10git push

Pattern 2: Clone with Submodules #

When cloning the repo fresh:

1# Option A: Clone with submodules in one command
2git clone --recurse-submodules https://github.com/your-org/thefocus-landing.git
3
4# Option B: If already cloned, initialize submodules
5git submodule update --init --recursive

Pattern 3: Build Script for Local Development #

Create scripts/build-all.sh:

 1#!/bin/bash
 2set -e
 3
 4echo "Building main site..."
 5pnpm build
 6
 7echo "Building aie-report..."
 8cd submodules/aie-report
 9pnpm install
10pnpm build
11mkdir -p ../../dist/reports/aie-coding-2025
12cp -r dist/* ../../dist/reports/aie-coding-2025/
13cd ../..
14
15echo "Building weekend-warrior..."
16cd submodules/weekend-warrior
17pnpm install
18pnpm build
19mkdir -p ../../dist/weekend-warrior/coding-agent
20cp -r dist/* ../../dist/weekend-warrior/coding-agent/
21cd ../..
22
23echo "All sites built! Preview with: pnpm preview"

Pattern 4: Private Submodules Authentication #

For private repos, create a fine-grained PAT with read access to Contents[1]:

  1. Go to GitHub → Settings → Developer Settings → Personal Access Tokens → Fine-grained tokens
  2. Create token with read-only access to repository Contents
  3. Add as repository secret named GH_PAT
  4. Update checkout step:
1- uses: actions/checkout@v4
2  with:
3    submodules: recursive
4    token: ${{ secrets.GH_PAT }}

Pattern 5: Trigger Rebuild from Submodule #

Add to submodule's workflow to trigger parent rebuild:

 1# In submodule repo: .github/workflows/trigger-parent.yml
 2name: Trigger Parent Rebuild
 3
 4on:
 5  push:
 6    branches: [main]
 7
 8jobs:
 9  trigger:
10    runs-on: ubuntu-latest
11    steps:
12      - name: Trigger parent workflow
13        run: |
14          curl -X POST \
15            -H "Accept: application/vnd.github+json" \
16            -H "Authorization: Bearer ${{ secrets.PARENT_REPO_PAT }}" \
17            https://api.github.com/repos/your-org/thefocus-landing/dispatches \
18            -d '{"event_type":"submodule-update"}'

Then add to parent's workflow triggers:

1on:
2  push:
3    branches: [main]
4  repository_dispatch:
5    types: [submodule-update]
6  workflow_dispatch:

Anti-Patterns & Pitfalls #

❌ Don't: Use withastro/action for Multi-Site Builds #

1# BAD: This only builds the main site
2- uses: withastro/action@v2
3  with:
4    path: .

Why it's wrong: withastro/action is designed for single-site deployment and automatically uploads artifacts. You lose control over the build process needed for multi-site[3].

✅ Instead: Build Manually and Upload Once #

1# GOOD: Manual build with full control
2- run: pnpm build
3- run: |
4    cd submodules/aie-report && pnpm install && pnpm build
5    cp -r dist/* ../../dist/reports/aie-coding-2025/
6- uses: actions/upload-pages-artifact@v3
7  with:
8    path: dist/

❌ Don't: Forget the base Path in Sub-Sites #

1// BAD: Missing base path
2export default defineConfig({
3  // Assets will load from / instead of /reports/aie-coding-2025/
4});

Why it's wrong: Without base, asset paths like /styles.css will 404 when the site is mounted at a subpath[4].

✅ Instead: Always Set Base to Match Deployment Path #

1// GOOD: Base matches deployment location
2export default defineConfig({
3  base: "/reports/aie-coding-2025",
4});

❌ Don't: Mix v3 and v4 Artifact Actions #

1# BAD: Mixing versions
2- uses: actions/upload-artifact@v3
3# later...
4- uses: actions/download-artifact@v4  # Won't find v3 artifacts!

Why it's wrong: v3 and v4 artifact actions are incompatible. v3 is deprecated as of November 2024[7].

✅ Instead: Use v4 Consistently #

1# GOOD: Consistent v4 usage
2- uses: actions/upload-pages-artifact@v3
3- uses: actions/deploy-pages@v4

❌ Don't: Checkout Without submodules: recursive #

1# BAD: Submodules won't be checked out
2- uses: actions/checkout@v4

Why it's wrong: By default, actions/checkout does not initialize submodules[1].

✅ Instead: Always Use Recursive #

1# GOOD: All submodules are initialized
2- uses: actions/checkout@v4
3  with:
4    submodules: recursive

❌ Don't: Hardcode Paths in Multiple Places #

1# BAD: Path repeated and can drift
2- run: mkdir -p ../../dist/reports/aie-coding-2025
3# ... and in astro.config.mjs: base: "/reports/aie-coding-2025"
4# ... and in another script...

Why it's wrong: Path drift causes 404s that are hard to debug.

✅ Instead: Use Environment Variables or a Config File #

1# GOOD: Single source of truth
2env:
3  AIE_REPORT_PATH: reports/aie-coding-2025
4
5steps:
6  - run: |
7      mkdir -p dist/${{ env.AIE_REPORT_PATH }}
8      cp -r submodules/aie-report/dist/* dist/${{ env.AIE_REPORT_PATH }}/

Caveats #

Alternative: Checkout Multiple Repos Without Submodules #

If you don't want to use submodules, you can checkout multiple repos directly:

 1- uses: actions/checkout@v4
 2  with:
 3    repository: your-org/thefocus-landing
 4    path: main
 5
 6- uses: actions/checkout@v4
 7  with:
 8    repository: your-org/aie-report
 9    path: aie-report
10    token: ${{ secrets.GH_PAT }}  # Only needed for private repos
11
12- run: |
13    cd main && pnpm install && pnpm build
14    cd ../aie-report && pnpm install && pnpm build
15    cp -r dist/* ../main/dist/reports/aie-coding-2025/

This approach is simpler for one-off builds but doesn't track submodule versions in your git history.

References #

[1] Stack Overflow: How to checkout submodule in GitHub action? - Details on actions/checkout submodule options and authentication

[2] Micah Henning: Checkout Submodules with Least Privilege - Security best practices for PAT tokens with submodules

[3] withastro/action GitHub Repository - Official Astro GitHub Action documentation

[4] Astro Docs: Configuration Reference - base and outDir configuration options

[5] GitHub Blog: GitHub Actions Unified Build Pipeline for Multi-Repo - Patterns for multi-repository builds

[6] GitHub Docs: Dependabot for Git Submodules - Auto-updating submodules with Dependabot

[7] GitHub Blog: Get started with v4 of GitHub Actions Artifacts - Artifact v4 changes and migration guide

[8] GitHub: actions/upload-pages-artifact - GitHub Pages artifact requirements and size limits

last updated: