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]:
- Go to GitHub → Settings → Developer Settings → Personal Access Tokens → Fine-grained tokens
- Create token with read-only access to repository Contents
- Add as repository secret named
GH_PAT - 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 #
-
Submodule Commit Pinning: Submodules point to a specific commit, not a branch. You must explicitly update them with
git submodule update --remoteand commit the new reference. Dependabot can automate this[6]. -
CI Build Time: Each submodule requires its own
pnpm install, which adds build time. Consider caching node_modules per submodule or using a build matrix for parallel builds. -
Shared Dependencies: If submodules share dependencies with the main site, they're installed separately, increasing disk usage. For tightly coupled sites, a pnpm workspace monorepo may be better.
-
Private Submodules: Require a Personal Access Token (PAT) with read access. The default
GITHUB_TOKENonly has access to the current repository[1]. -
Nested Submodules: If a submodule contains its own submodules, you need
submodules: recursive(not justsubmodules: true). -
Pages Artifact Size: GitHub Pages has a 1GB official limit (10GB absolute maximum). Combine carefully if sub-sites are large[8].
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