Compare commits
188 Commits
6864903cff
...
v1.15.19
| Author | SHA1 | Date | |
|---|---|---|---|
| 0a9cb81841 | |||
| c505050cb4 | |||
| d5d0d77180 | |||
| 9399be3ea9 | |||
| 7ec82027a8 | |||
| 631d051c2e | |||
| 1a8758a88c | |||
| 7d6d34dd7b | |||
| 1f0de18755 | |||
| eeb0920afd | |||
| f8eec731c0 | |||
| 8b80af3d1a | |||
| de42890476 | |||
| fbd33da925 | |||
| f3bab24bd3 | |||
| 3b668fc876 | |||
| 2827239796 | |||
| 7cc1e49ba6 | |||
| 258eb9464e | |||
| 015386ba4a | |||
| 49abaaf2fd | |||
| 7e9005e338 | |||
| bbcc7d159c | |||
| 39f5bd3986 | |||
| 944d369d43 | |||
| 6aaf8ac44f | |||
| 2097b571f3 | |||
| aeb1814a34 | |||
| 57e093ea54 | |||
| 76e25d03b9 | |||
| 19ef042fb6 | |||
| 6032394ac9 | |||
| 033a9ad65e | |||
| 3f6fa36f9b | |||
| c52a132d62 | |||
| 11f735bbdf | |||
| c81dae0b7b | |||
| 716ece1c6c | |||
| e0ccf1cdfb | |||
| 6a6fbb6f19 | |||
| 6b6b2b8ece | |||
| 9f412d81a8 | |||
| 9c401f13de | |||
| 5857404ac1 | |||
| 34a96f8aef | |||
| 4e6f3f29cf | |||
| 1bd516fbe4 | |||
| 4d0e3433a6 | |||
| ee9cde1ed0 | |||
| 33cf701034 | |||
| 1fae5edee3 | |||
| 0e143bf9c1 | |||
| d86e26bc33 | |||
| a1c0736274 | |||
| 7b642426fb | |||
| 6a228248e0 | |||
| bd1a822d32 | |||
| 81af49f880 | |||
| 1defb5758f | |||
| b4dd073711 | |||
| 59ea4bfd02 | |||
| 4a20e1f51f | |||
| 9aa3ee42e4 | |||
| 0ac022df57 | |||
| e71965267d | |||
| 8d12f92da8 | |||
| 4303124ec5 | |||
| badf81644e | |||
| cdd38b3654 | |||
| 1a195a388a | |||
| b4fbf3bf2a | |||
| 8569105529 | |||
| 316afe004f | |||
| b20a999da8 | |||
| 237d68bc5a | |||
| 0fdc20cabb | |||
| 2aa617ce3b | |||
| 54cd94831d | |||
| c8df20bbee | |||
| 07755c9674 | |||
| ff7ba14a4a | |||
| ebe42adb6f | |||
| a45d0110d3 | |||
| 9abd4f4fe7 | |||
| 3a4fd1d06d | |||
| c0b9c55ecf | |||
| 7e320c08d9 | |||
| c5746978aa | |||
| cd88c2f20f | |||
| 1c87d5341e | |||
| 6a14c9924f | |||
| ee50808596 | |||
| e9fbe45feb | |||
| b27566a336 | |||
| 71ef49e73d | |||
| a98572e183 | |||
| eacb14ff7d | |||
| 41a090db58 | |||
| 2bdb6bbb98 | |||
| 99ee47507b | |||
| 2d96000385 | |||
| 39ea0a35dd | |||
| 1c24822787 | |||
| d21c12c2b4 | |||
| cdf2bb5fdc | |||
| c4aaea30c1 | |||
| cbb3cf0be3 | |||
| bc3a75a915 | |||
| 1455845d44 | |||
| db31f06bc0 | |||
| 546b8ee72b | |||
| 6174b44570 | |||
| 89d258e63d | |||
| 13a484ce59 | |||
| d82c836fcb | |||
| b2f6627ec5 | |||
| 2ab5a8a41f | |||
| e43c980a5d | |||
| 88b4626d6e | |||
| 90856da773 | |||
| 964cd79ca8 | |||
| 9c5e2c6099 | |||
| 984a641b90 | |||
| c8ff76f299 | |||
| 1fffdf00ee | |||
| 70de139cb0 | |||
| b015c62650 | |||
| b7dac5d463 | |||
| 10bdfdfe97 | |||
| 9ad63a0a82 | |||
| eb117cc0b8 | |||
| 23ee915194 | |||
| 3dff891023 | |||
| f55c27c43d | |||
| 3e04427646 | |||
| 6b51d63c8b | |||
| 60ca4ad656 | |||
| aae5275990 | |||
| b639fffe7f | |||
| ab15f7f35b | |||
| 025906889c | |||
| 760a6d6db3 | |||
| 7f8cea4728 | |||
| fb09b1de9a | |||
| cb4afe2e91 | |||
| 1f68234a49 | |||
| e2d68c2828 | |||
| cb6f133e0c | |||
| 7990189505 | |||
| 2167044543 | |||
| 0665e3e224 | |||
| 2bdcbfb907 | |||
| ac1e0081f7 | |||
| 4f452cf2a9 | |||
| 1404aa0406 | |||
| 9e10ce06ed | |||
| a400e6f94d | |||
| 2f95c8d968 | |||
| 9aa6f5f4d0 | |||
| 071302fe6b | |||
| cf3a96cead | |||
| af5f91e6f8 | |||
| 5e453418d6 | |||
| 10980ba8b3 | |||
| 6444aea5f6 | |||
| ad50929bf3 | |||
| 07928a182f | |||
| b493ce0ba0 | |||
| db445d0b76 | |||
| 22a6a06a4e | |||
| 4f66dd914c | |||
| bb54750085 | |||
| 5cbbd81384 | |||
| c167e36626 | |||
| 0fb872161d | |||
| a360ea6a98 | |||
| a537294832 | |||
| 459bdc6eda | |||
| 905ce98bc4 | |||
| ce63a1ac69 | |||
| 6444cf1e81 | |||
| 4b5609a75e | |||
| 8907963d57 | |||
| 844a5f5412 | |||
| 96de68a063 | |||
| 4a5017124a | |||
| 283de11b11 | |||
| 31685a458b |
@@ -1,9 +1,13 @@
|
||||
node_modules
|
||||
.next
|
||||
out
|
||||
dist
|
||||
*.log
|
||||
.git
|
||||
# Exclude all binary/dependency folders recursively
|
||||
**/node_modules
|
||||
**/.pnpm-store
|
||||
**/.git
|
||||
**/.next
|
||||
**/dist
|
||||
**/out
|
||||
**/*.log
|
||||
|
||||
# Specific exclusions for this project
|
||||
.DS_Store
|
||||
cloned-websites
|
||||
storage
|
||||
@@ -11,3 +15,11 @@ storage
|
||||
verify_ci
|
||||
pnpm_install_log.txt
|
||||
full_tree.json
|
||||
backups
|
||||
data
|
||||
|
||||
# Ensure we don't copy the sibling's build artifacts either
|
||||
_at-mintel/**/node_modules
|
||||
_at-mintel/**/dist
|
||||
_at-mintel/**/.next
|
||||
_at-mintel/.git
|
||||
|
||||
@@ -1,33 +0,0 @@
|
||||
name: CI - Quality Assurance
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
|
||||
jobs:
|
||||
qa:
|
||||
name: 🧪 QA
|
||||
runs-on: docker
|
||||
container:
|
||||
image: catthehacker/ubuntu:act-latest
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 20
|
||||
- name: Setup pnpm
|
||||
uses: pnpm/action-setup@v3
|
||||
with:
|
||||
version: 10
|
||||
- name: 🔐 Registry Auth
|
||||
run: |
|
||||
echo "@mintel:registry=https://${{ vars.REGISTRY_HOST || 'npm.infra.mintel.me' }}" > .npmrc
|
||||
echo "//${{ vars.REGISTRY_HOST || 'npm.infra.mintel.me' }}/:_authToken=${{ secrets.REGISTRY_PASS }}" >> .npmrc
|
||||
- name: Install dependencies
|
||||
run: pnpm install --frozen-lockfile
|
||||
- name: 🧪 Parallel Checks
|
||||
run: |
|
||||
pnpm lint &
|
||||
pnpm build &
|
||||
wait
|
||||
@@ -1,9 +1,10 @@
|
||||
# Heartbeat to trigger fresh CI run after stall
|
||||
name: Build & Deploy
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
- "**"
|
||||
tags:
|
||||
- "v*"
|
||||
workflow_dispatch:
|
||||
@@ -13,6 +14,9 @@ on:
|
||||
required: false
|
||||
default: "false"
|
||||
|
||||
env:
|
||||
PUPPETEER_SKIP_DOWNLOAD: "true"
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ (github.ref_type == 'tag' && !contains(github.ref_name, '-')) && 'prod' || (github.ref_name == 'main' && 'testing' || github.ref_name) }}
|
||||
cancel-in-progress: true
|
||||
@@ -76,7 +80,11 @@ jobs:
|
||||
TRAEFIK_HOST="staging.${DOMAIN}"
|
||||
fi
|
||||
else
|
||||
TARGET="skip"
|
||||
TARGET="branch"
|
||||
SLUG=$(echo "$REF" | tr '[:upper:]' '[:lower:]' | sed 's/[^a-z0-9]/-/g' | sed 's/--*/-/g' | sed 's/^-//;s/-$//')
|
||||
IMAGE_TAG="branch-${SLUG}-${SHORT_SHA}"
|
||||
ENV_FILE=".env.branch-${SLUG}"
|
||||
TRAEFIK_HOST="${SLUG}.branch.${DOMAIN}"
|
||||
fi
|
||||
|
||||
if [[ "$TARGET" != "skip" ]]; then
|
||||
@@ -97,74 +105,27 @@ jobs:
|
||||
echo "traefik_rule=$TRAEFIK_RULE"
|
||||
echo "next_public_url=https://$PRIMARY_HOST"
|
||||
echo "directus_url=https://cms.$PRIMARY_HOST"
|
||||
echo "project_name=$PRJ-$TARGET"
|
||||
if [[ "$TARGET" == "branch" ]]; then
|
||||
echo "project_name=$PRJ-branch-$SLUG"
|
||||
else
|
||||
echo "project_name=$PRJ-$TARGET"
|
||||
fi
|
||||
echo "short_sha=$SHORT_SHA"
|
||||
} >> "$GITHUB_OUTPUT"
|
||||
|
||||
# ⏳ Wait for Upstream Packages/Images if Tagged
|
||||
if [[ "${{ github.ref_type }}" == "tag" ]]; then
|
||||
echo "🔎 Checking for @mintel dependencies in package.json..."
|
||||
# Extract any @mintel/ version (they should be synced in monorepo)
|
||||
UPSTREAM_VERSION=$(grep -o '"@mintel/.*": "[^"]*"' package.json | head -1 | cut -d'"' -f4 | sed 's/\^//; s/\~//')
|
||||
TAG_TO_WAIT="v$UPSTREAM_VERSION"
|
||||
|
||||
if [[ -n "$UPSTREAM_VERSION" && "$UPSTREAM_VERSION" != "workspace:"* ]]; then
|
||||
echo "⏳ This release depends on @mintel v$UPSTREAM_VERSION. Waiting for upstream build..."
|
||||
# Fetch script from monorepo (main)
|
||||
curl -s -H "Authorization: token ${{ secrets.GITHUB_TOKEN }}" \
|
||||
"https://git.infra.mintel.me/mmintel/at-mintel/raw/branch/main/packages/infra/scripts/wait-for-upstream.sh" > wait-for-upstream.sh
|
||||
chmod +x wait-for-upstream.sh
|
||||
|
||||
GITEA_TOKEN=${{ secrets.GITHUB_TOKEN }} ./wait-for-upstream.sh "mmintel/at-mintel" "$TAG_TO_WAIT"
|
||||
fi
|
||||
fi
|
||||
|
||||
else
|
||||
echo "target=skip" >> "$GITHUB_OUTPUT"
|
||||
fi
|
||||
|
||||
# ──────────────────────────────────────────────────────────────────────────────
|
||||
# JOB 2: QA (Lint, Build Test)
|
||||
# ──────────────────────────────────────────────────────────────────────────────
|
||||
qa:
|
||||
name: 🧪 QA
|
||||
needs: prepare
|
||||
if: needs.prepare.outputs.target != 'skip'
|
||||
runs-on: docker
|
||||
container:
|
||||
image: catthehacker/ubuntu:act-latest
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 20
|
||||
- name: Setup pnpm
|
||||
uses: pnpm/action-setup@v3
|
||||
with:
|
||||
version: 10
|
||||
- name: 🔐 Registry Auth
|
||||
run: |
|
||||
echo "@mintel:registry=https://${{ vars.REGISTRY_HOST || 'npm.infra.mintel.me' }}" > .npmrc
|
||||
echo "//${{ vars.REGISTRY_HOST || 'npm.infra.mintel.me' }}/:_authToken=${{ secrets.REGISTRY_PASS }}" >> .npmrc
|
||||
- name: Install dependencies
|
||||
run: pnpm install --frozen-lockfile
|
||||
- name: 🧪 QA Checks
|
||||
if: github.event.inputs.skip_checks != 'true'
|
||||
run: |
|
||||
pnpm lint
|
||||
pnpm --filter "@mintel/web" exec tsc --noEmit
|
||||
pnpm --filter "@mintel/web" test
|
||||
- name: 🏗️ Build Test
|
||||
if: github.event.inputs.skip_checks != 'true'
|
||||
run: pnpm build
|
||||
# (JOB 2: QA was removed to reduce pipeline noise)
|
||||
|
||||
# ──────────────────────────────────────────────────────────────────────────────
|
||||
# JOB 3: Build & Push
|
||||
# ──────────────────────────────────────────────────────────────────────────────
|
||||
build:
|
||||
name: 🏗️ Build
|
||||
needs: prepare
|
||||
needs: [prepare]
|
||||
if: needs.prepare.outputs.target != 'skip'
|
||||
runs-on: docker
|
||||
container:
|
||||
@@ -172,33 +133,104 @@ jobs:
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
- name: Provide sibling monorepo (context)
|
||||
run: |
|
||||
git clone https://git.infra.mintel.me/mmintel/at-mintel.git _at-mintel
|
||||
# Force ALL @mintel packages to use the local clone instead of the registry
|
||||
perl -pi -e 's/"\@mintel\/([^"]+)"\s*:\s*"[^"]+"/"\@mintel\/$1": "link:.\/_at-mintel\/packages\/$1"/g' package.json
|
||||
perl -pi -e 's/link:\.\/_at-mintel\/packages\/pdf"/link:.\/_at-mintel\/packages\/pdf-library"/g' package.json
|
||||
perl -pi -e 's/"\@mintel\/([^"]+)"\s*:\s*"[^"]+"/"\@mintel\/$1": "link:..\/\.\.\/_at-mintel\/packages\/$1"/g' apps/web/package.json
|
||||
perl -pi -e 's/link:\.\.\/\.\.\/_at-mintel\/packages\/pdf"/link:..\/\.\.\/_at-mintel\/packages\/pdf-library"/g' apps/web/package.json
|
||||
|
||||
- name: 🧹 Free Disk Space
|
||||
run: |
|
||||
docker builder prune -af || true
|
||||
docker image prune -af || true
|
||||
|
||||
- name: 🐳 Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
|
||||
- name: 🔐 Registry Login
|
||||
run: echo "${{ secrets.REGISTRY_PASS }}" | docker login registry.infra.mintel.me -u "${{ secrets.REGISTRY_USER }}" --password-stdin
|
||||
run: |
|
||||
echo "${{ secrets.REGISTRY_PASS }}" | docker login registry.infra.mintel.me -u "${{ secrets.REGISTRY_USER }}" --password-stdin
|
||||
|
||||
- name: 🧹 Nuclear Runner Cleanup
|
||||
run: |
|
||||
echo "Disk space before nuclear prune:"
|
||||
df -h
|
||||
|
||||
# Massive cleanup of pre-installed runner software to free up ~5GB+
|
||||
# These are standard on VM-based runners and often unnecessary
|
||||
sudo rm -rf /usr/share/dotnet || true
|
||||
sudo rm -rf /usr/local/lib/android || true
|
||||
sudo rm -rf /opt/ghc || true
|
||||
sudo rm -rf /opt/hostedtoolcache/CodeQL || true
|
||||
|
||||
# Comprehensive Docker purge
|
||||
docker system prune -af --volumes
|
||||
docker builder prune -af
|
||||
docker buildx prune -af
|
||||
|
||||
# Clean temp build artifacts
|
||||
rm -rf /tmp/docker-actions-toolkit-* || true
|
||||
|
||||
echo "Disk space after nuclear prune:"
|
||||
df -h
|
||||
continue-on-error: true
|
||||
|
||||
- name: 🏗️ Build and Push
|
||||
uses: docker/build-push-action@v5
|
||||
with:
|
||||
context: .
|
||||
push: true
|
||||
platforms: linux/arm64
|
||||
provenance: false
|
||||
platforms: linux/amd64
|
||||
build-args: |
|
||||
NEXT_PUBLIC_BASE_URL=${{ needs.prepare.outputs.next_public_url }}
|
||||
NEXT_PUBLIC_TARGET=${{ needs.prepare.outputs.target }}
|
||||
DIRECTUS_URL=${{ needs.prepare.outputs.directus_url }}
|
||||
NPM_TOKEN=${{ secrets.REGISTRY_PASS }}
|
||||
NPM_TOKEN=${{ secrets.NPM_TOKEN }}
|
||||
S3_ENDPOINT=${{ secrets.S3_ENDPOINT || vars.S3_ENDPOINT || 'https://fsn1.your-objectstorage.com' }}
|
||||
S3_ACCESS_KEY=${{ secrets.S3_ACCESS_KEY || vars.S3_ACCESS_KEY }}
|
||||
S3_SECRET_KEY=${{ secrets.S3_SECRET_KEY || vars.S3_SECRET_KEY }}
|
||||
S3_BUCKET=${{ secrets.S3_BUCKET || vars.S3_BUCKET || 'mintel' }}
|
||||
S3_REGION=${{ secrets.S3_REGION || vars.S3_REGION || 'fsn1' }}
|
||||
S3_PREFIX=${{ secrets.S3_PREFIX || vars.S3_PREFIX || 'mintel.me' }}
|
||||
DATABASE_URI=${{ secrets.DATABASE_URI || (needs.prepare.outputs.target == 'testing' && secrets.TESTING_DIRECTUS_DB_PASSWORD && format('postgres://directus:{0}@postgres-db:5432/directus', secrets.TESTING_DIRECTUS_DB_PASSWORD)) || 'postgres://payload:payload@127.0.0.1:5432/payload' }}
|
||||
PAYLOAD_SECRET=${{ secrets.PAYLOAD_SECRET || 'secret' }}
|
||||
BUILD_ID=${{ github.sha }}
|
||||
tags: registry.infra.mintel.me/mintel/mintel.me:${{ needs.prepare.outputs.image_tag }}
|
||||
cache-from: type=registry,ref=registry.infra.mintel.me/mintel/mintel.me:buildcache
|
||||
cache-to: type=registry,ref=registry.infra.mintel.me/mintel/mintel.me:buildcache,mode=max
|
||||
# Temporarily disable registry cache export to save runner disk/bandwidth
|
||||
cache-from: type=registry,ref=registry.infra.mintel.me/mintel/mintel.me:buildcache-${{ needs.prepare.outputs.target }}
|
||||
# cache-to: type=registry,ref=registry.infra.mintel.me/mintel/mintel.me:buildcache-${{ needs.prepare.outputs.target }},mode=max
|
||||
secrets: |
|
||||
NPM_TOKEN=${{ secrets.REGISTRY_PASS }}
|
||||
NPM_TOKEN=${{ secrets.NPM_TOKEN }}
|
||||
|
||||
# ──────────────────────────────────────────────────────────────────────────────
|
||||
- name: 🚨 Extract Build Error Logs
|
||||
if: failure()
|
||||
run: |
|
||||
set +e
|
||||
mkdir -p ~/.ssh
|
||||
echo "${{ secrets.ALPHA_SSH_KEY }}" > ~/.ssh/id_ed25519
|
||||
chmod 600 ~/.ssh/id_ed25519
|
||||
ssh-keyscan -H alpha.mintel.me >> ~/.ssh/known_hosts 2>/dev/null
|
||||
echo "Re-running docker build with plain progress to capture exact logs..."
|
||||
echo "${{ secrets.NPM_TOKEN }}" > /tmp/npm_token.txt
|
||||
docker build \
|
||||
--build-arg NEXT_PUBLIC_BASE_URL=${{ needs.prepare.outputs.next_public_url }} \
|
||||
--build-arg NEXT_PUBLIC_TARGET=${{ needs.prepare.outputs.target }} \
|
||||
--build-arg DIRECTUS_URL=${{ needs.prepare.outputs.directus_url }} \
|
||||
--build-arg NPM_TOKEN=${{ secrets.NPM_TOKEN }} \
|
||||
--secret id=NPM_TOKEN,src=/tmp/npm_token.txt \
|
||||
--progress plain \
|
||||
-t temp-image . > docker_build_failed.log 2>&1
|
||||
cat login.log >> docker_build_failed.log || true
|
||||
scp docker_build_failed.log root@alpha.mintel.me:/root/docker_build_failed.log
|
||||
# JOB 4: Deploy
|
||||
# ──────────────────────────────────────────────────────────────────────────────
|
||||
deploy:
|
||||
name: 🚀 Deploy
|
||||
needs: [prepare, build, qa]
|
||||
needs: [prepare, build]
|
||||
runs-on: docker
|
||||
container:
|
||||
image: catthehacker/ubuntu:act-latest
|
||||
@@ -214,10 +246,10 @@ jobs:
|
||||
postgres_DB_NAME: ${{ secrets.DIRECTUS_DB_NAME || vars.DIRECTUS_DB_NAME || 'directus' }}
|
||||
postgres_DB_USER: ${{ secrets.DIRECTUS_DB_USER || vars.DIRECTUS_DB_USER || 'directus' }}
|
||||
postgres_DB_PASSWORD: ${{ (needs.prepare.outputs.target == 'testing' && secrets.TESTING_DIRECTUS_DB_PASSWORD) || (needs.prepare.outputs.target == 'staging' && secrets.STAGING_DIRECTUS_DB_PASSWORD) || secrets.DIRECTUS_DB_PASSWORD || vars.DIRECTUS_DB_PASSWORD || 'directus' }}
|
||||
DATABASE_URI: postgres://${{ env.postgres_DB_USER }}:${{ env.postgres_DB_PASSWORD }}@postgres-db:5432/${{ env.postgres_DB_NAME }}
|
||||
DATABASE_URI: postgres://${{ secrets.DIRECTUS_DB_USER || vars.DIRECTUS_DB_USER || 'directus' }}:${{ (needs.prepare.outputs.target == 'testing' && secrets.TESTING_DIRECTUS_DB_PASSWORD) || (needs.prepare.outputs.target == 'staging' && secrets.STAGING_DIRECTUS_DB_PASSWORD) || secrets.DIRECTUS_DB_PASSWORD || vars.DIRECTUS_DB_PASSWORD || 'directus' }}@postgres-db:5432/${{ secrets.DIRECTUS_DB_NAME || vars.DIRECTUS_DB_NAME || 'directus' }}
|
||||
PAYLOAD_SECRET: ${{ secrets.PAYLOAD_SECRET || vars.PAYLOAD_SECRET || 'secret' }}
|
||||
|
||||
# Secrets mapping (Mail)
|
||||
# Mail
|
||||
MAIL_HOST: ${{ secrets.SMTP_HOST || vars.SMTP_HOST }}
|
||||
MAIL_PORT: ${{ secrets.SMTP_PORT || vars.SMTP_PORT || '587' }}
|
||||
MAIL_USERNAME: ${{ secrets.SMTP_USER || vars.SMTP_USER }}
|
||||
@@ -235,6 +267,14 @@ jobs:
|
||||
UMAMI_WEBSITE_ID: ${{ secrets.UMAMI_WEBSITE_ID || secrets.NEXT_PUBLIC_UMAMI_WEBSITE_ID || vars.UMAMI_WEBSITE_ID || vars.NEXT_PUBLIC_UMAMI_WEBSITE_ID }}
|
||||
UMAMI_API_ENDPOINT: ${{ secrets.UMAMI_API_ENDPOINT || secrets.NEXT_PUBLIC_UMAMI_SCRIPT_URL || vars.UMAMI_API_ENDPOINT || 'https://analytics.infra.mintel.me' }}
|
||||
PROJECT_COLOR: ${{ secrets.PROJECT_COLOR || vars.PROJECT_COLOR || '#ff00ff' }}
|
||||
|
||||
# S3 Object Storage
|
||||
S3_ENDPOINT: ${{ secrets.S3_ENDPOINT || vars.S3_ENDPOINT || 'https://fsn1.your-objectstorage.com' }}
|
||||
S3_ACCESS_KEY: ${{ secrets.S3_ACCESS_KEY || vars.S3_ACCESS_KEY }}
|
||||
S3_SECRET_KEY: ${{ secrets.S3_SECRET_KEY || vars.S3_SECRET_KEY }}
|
||||
S3_BUCKET: ${{ secrets.S3_BUCKET || vars.S3_BUCKET || 'mintel' }}
|
||||
S3_REGION: ${{ secrets.S3_REGION || vars.S3_REGION || 'fsn1' }}
|
||||
S3_PREFIX: ${{ secrets.S3_PREFIX || vars.S3_PREFIX || github.event.repository.name }}
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
@@ -246,7 +286,6 @@ jobs:
|
||||
GATEKEEPER_HOST: gatekeeper.${{ needs.prepare.outputs.traefik_host }}
|
||||
ENV_FILE: ${{ needs.prepare.outputs.env_file }}
|
||||
run: |
|
||||
# Middleware & Auth Logic
|
||||
LOG_LEVEL=$( [[ "$TARGET" == "testing" || "$TARGET" == "development" ]] && echo "debug" || echo "info" )
|
||||
STD_MW="${PROJECT_NAME}-forward,compress"
|
||||
|
||||
@@ -254,15 +293,16 @@ jobs:
|
||||
AUTH_MIDDLEWARE="$STD_MW"
|
||||
COMPOSE_PROFILES=""
|
||||
else
|
||||
# Order: Forward (Proto) -> Auth -> Compression
|
||||
AUTH_MIDDLEWARE="${PROJECT_NAME}-forward,${PROJECT_NAME}-auth,compress"
|
||||
COMPOSE_PROFILES="gatekeeper"
|
||||
fi
|
||||
|
||||
# Gatekeeper Origin
|
||||
GATEKEEPER_ORIGIN="$NEXT_PUBLIC_BASE_URL/gatekeeper"
|
||||
|
||||
# Generate Environment File
|
||||
if [[ "$UMAMI_API_ENDPOINT" != http* ]]; then
|
||||
UMAMI_API_ENDPOINT="https://$UMAMI_API_ENDPOINT"
|
||||
fi
|
||||
|
||||
cat > .env.deploy << EOF
|
||||
# Generated by CI - $TARGET
|
||||
IMAGE_TAG=$IMAGE_TAG
|
||||
@@ -271,32 +311,29 @@ jobs:
|
||||
SENTRY_DSN=$SENTRY_DSN
|
||||
PROJECT_COLOR=$PROJECT_COLOR
|
||||
LOG_LEVEL=$LOG_LEVEL
|
||||
|
||||
# Payload DB
|
||||
postgres_DB_NAME=$postgres_DB_NAME
|
||||
postgres_DB_USER=$postgres_DB_USER
|
||||
postgres_DB_PASSWORD=$postgres_DB_PASSWORD
|
||||
DATABASE_URI=$DATABASE_URI
|
||||
PAYLOAD_SECRET=$PAYLOAD_SECRET
|
||||
|
||||
# Mail
|
||||
MAIL_HOST=$MAIL_HOST
|
||||
MAIL_PORT=$MAIL_PORT
|
||||
MAIL_USERNAME=$MAIL_USERNAME
|
||||
MAIL_PASSWORD=$MAIL_PASSWORD
|
||||
MAIL_FROM=$MAIL_FROM
|
||||
MAIL_RECIPIENTS=$MAIL_RECIPIENTS
|
||||
|
||||
# Authentication
|
||||
GATEKEEPER_PASSWORD=$GATEKEEPER_PASSWORD
|
||||
AUTH_COOKIE_NAME=$AUTH_COOKIE_NAME
|
||||
COOKIE_DOMAIN=$COOKIE_DOMAIN
|
||||
|
||||
# Analytics
|
||||
UMAMI_WEBSITE_ID=$UMAMI_WEBSITE_ID
|
||||
NEXT_PUBLIC_UMAMI_WEBSITE_ID=$UMAMI_WEBSITE_ID
|
||||
UMAMI_API_ENDPOINT=$UMAMI_API_ENDPOINT
|
||||
|
||||
S3_ENDPOINT=$S3_ENDPOINT
|
||||
S3_ACCESS_KEY=$S3_ACCESS_KEY
|
||||
S3_SECRET_KEY=$S3_SECRET_KEY
|
||||
S3_BUCKET=$S3_BUCKET
|
||||
S3_REGION=$S3_REGION
|
||||
S3_PREFIX=$S3_PREFIX
|
||||
TARGET=$TARGET
|
||||
SENTRY_ENVIRONMENT=$TARGET
|
||||
PROJECT_NAME=$PROJECT_NAME
|
||||
@@ -305,6 +342,9 @@ jobs:
|
||||
TRAEFIK_HOST='$TRAEFIK_HOST'
|
||||
COMPOSE_PROFILES=$COMPOSE_PROFILES
|
||||
TRAEFIK_MIDDLEWARES=$AUTH_MIDDLEWARE
|
||||
TRAEFIK_ENTRYPOINT=websecure
|
||||
TRAEFIK_TLS=true
|
||||
TRAEFIK_CERT_RESOLVER=le
|
||||
EOF
|
||||
|
||||
- name: 🚀 SSH Deploy
|
||||
@@ -317,57 +357,174 @@ jobs:
|
||||
chmod 600 ~/.ssh/id_ed25519
|
||||
ssh-keyscan -H alpha.mintel.me >> ~/.ssh/known_hosts 2>/dev/null
|
||||
|
||||
# Transfer and Restart
|
||||
SITE_DIR="/home/deploy/sites/mintel.me"
|
||||
ssh root@alpha.mintel.me "mkdir -p $SITE_DIR/directus/schema $SITE_DIR/directus/uploads $SITE_DIR/directus/extensions"
|
||||
# SSH keepalive to prevent timeout during long docker pull
|
||||
cat > ~/.ssh/config <<SSHCFG
|
||||
Host alpha.mintel.me
|
||||
ServerAliveInterval 15
|
||||
ServerAliveCountMax 20
|
||||
ConnectTimeout 30
|
||||
SSHCFG
|
||||
chmod 600 ~/.ssh/config
|
||||
|
||||
if [[ "$TARGET" == "production" ]]; then
|
||||
SITE_DIR="/home/deploy/sites/mintel.me"
|
||||
elif [[ "$TARGET" == "testing" ]]; then
|
||||
SITE_DIR="/home/deploy/sites/testing.mintel.me"
|
||||
elif [[ "$TARGET" == "staging" ]]; then
|
||||
SITE_DIR="/home/deploy/sites/staging.mintel.me"
|
||||
else
|
||||
SITE_DIR="/home/deploy/sites/branch.mintel.me/${SLUG:-unknown}"
|
||||
fi
|
||||
|
||||
# Upload files
|
||||
ssh root@alpha.mintel.me "mkdir -p $SITE_DIR/directus/schema $SITE_DIR/directus/uploads $SITE_DIR/directus/extensions"
|
||||
scp .env.deploy root@alpha.mintel.me:$SITE_DIR/$ENV_FILE
|
||||
scp docker-compose.yml root@alpha.mintel.me:$SITE_DIR/docker-compose.yml
|
||||
|
||||
ssh root@alpha.mintel.me "cd $SITE_DIR && echo '${{ secrets.REGISTRY_PASS }}' | docker login registry.infra.mintel.me -u '${{ secrets.REGISTRY_USER }}' --password-stdin"
|
||||
ssh root@alpha.mintel.me "cd $SITE_DIR && docker compose -p '${{ needs.prepare.outputs.project_name }}' --env-file '$ENV_FILE' pull"
|
||||
ssh root@alpha.mintel.me "cd $SITE_DIR && docker compose -p '${{ needs.prepare.outputs.project_name }}' --env-file '$ENV_FILE' up -d --remove-orphans"
|
||||
# Deploy
|
||||
echo "Testing available secrets against git.infra.mintel.me Docker registry..."
|
||||
TOKENS="${{ secrets.GITHUB_TOKEN }} ${{ secrets.GITEA_PAT }} ${{ secrets.MINTEL_PRIVATE_TOKEN }} ${{ secrets.NPM_TOKEN }}"
|
||||
USERS="${{ github.repository_owner }} ${{ github.actor }} marcmintel mintel mmintel"
|
||||
|
||||
VALID_TOKEN=""
|
||||
VALID_USER=""
|
||||
for T_RAW in $TOKENS; do
|
||||
if [ -n "$T_RAW" ]; then
|
||||
T=$(echo "$T_RAW" | tr -d ' ' | tr -d '\n' | tr -d '\r')
|
||||
for U in $USERS; do
|
||||
if [ -n "$U" ]; then
|
||||
echo "Attempting docker login for a token with user $U..."
|
||||
if echo "$T" | docker login git.infra.mintel.me -u "$U" --password-stdin > /dev/null 2>&1; then
|
||||
echo "✅ Successfully authenticated with a token."
|
||||
VALID_TOKEN="$T"
|
||||
VALID_USER="$U"
|
||||
break 2
|
||||
fi
|
||||
fi
|
||||
done
|
||||
fi
|
||||
done
|
||||
if [ -z "$VALID_TOKEN" ]; then echo "❌ All tokens failed to authenticate!"; exit 1; fi
|
||||
TOKEN="$VALID_TOKEN"
|
||||
|
||||
# Deploy — alpha is pre-logged into registry.infra.mintel.me, no credential passing needed
|
||||
ssh root@alpha.mintel.me "
|
||||
docker network create '${{ needs.prepare.outputs.project_name }}-internal' || true
|
||||
docker volume create 'mintel-me_payload-db-data' || true
|
||||
cd $SITE_DIR
|
||||
docker compose -p '${{ needs.prepare.outputs.project_name }}' --env-file $ENV_FILE pull
|
||||
docker compose -p '${{ needs.prepare.outputs.project_name }}' --env-file $ENV_FILE up -d --wait --remove-orphans
|
||||
"
|
||||
|
||||
ssh root@alpha.mintel.me "docker system prune -f --filter 'until=24h'"
|
||||
- name: 🧹 Purge S3 Cache
|
||||
shell: bash
|
||||
run: |
|
||||
echo "Installing rclone..."
|
||||
curl -s -O https://downloads.rclone.org/rclone-current-linux-amd64.deb
|
||||
sudo dpkg -i rclone-current-linux-amd64.deb > /dev/null 2>&1
|
||||
|
||||
echo "Configuring rclone..."
|
||||
cat > rclone.conf <<EOF
|
||||
[mintel-s3]
|
||||
type = s3
|
||||
provider = Other
|
||||
access_key_id = ${{ secrets.S3_ACCESS_KEY || vars.S3_ACCESS_KEY }}
|
||||
secret_access_key = ${{ secrets.S3_SECRET_KEY || vars.S3_SECRET_KEY }}
|
||||
endpoint = ${{ secrets.S3_ENDPOINT || vars.S3_ENDPOINT || 'https://fsn1.your-objectstorage.com' }}
|
||||
region = ${{ secrets.S3_REGION || vars.S3_REGION || 'fsn1' }}
|
||||
EOF
|
||||
|
||||
echo "Purging S3 cache for ${{ env.S3_PREFIX }} ..."
|
||||
rclone --config rclone.conf delete mintel-s3:${{ env.S3_BUCKET }}/${{ env.S3_PREFIX }}/cache/ --include "*" || true
|
||||
|
||||
rm rclone.conf rclone-current-linux-amd64.deb
|
||||
|
||||
- name: 🧹 Post-Deploy Cleanup (Runner)
|
||||
if: always()
|
||||
run: docker builder prune -f --filter "until=1h"
|
||||
|
||||
# ──────────────────────────────────────────────────────────────────────────────
|
||||
# JOB 5: Health Check
|
||||
# JOB 5: Post-Deploy Verification
|
||||
# ──────────────────────────────────────────────────────────────────────────────
|
||||
healthcheck:
|
||||
name: 🩺 Health Check
|
||||
post_deploy_checks:
|
||||
name: 🩺 Smoke Test
|
||||
needs: [prepare, deploy]
|
||||
if: needs.deploy.result == 'success'
|
||||
runs-on: docker
|
||||
if: needs.deploy.result == 'success'
|
||||
container:
|
||||
image: catthehacker/ubuntu:act-latest
|
||||
image: alpine:latest
|
||||
steps:
|
||||
- name: 🔍 Smoke Test
|
||||
- name: 🌐 Check Production URL
|
||||
shell: sh
|
||||
run: |
|
||||
apk add --no-cache curl
|
||||
# Wait longer (up to 2 minutes) for Next.js to fully prime
|
||||
COUNT=0
|
||||
MAX=24
|
||||
URL="${{ needs.prepare.outputs.next_public_url }}"
|
||||
echo "Checking health of $URL..."
|
||||
for i in {1..12}; do
|
||||
if curl -s -f "$URL" > /dev/null; then
|
||||
echo "✅ Health check passed!"
|
||||
exit 0
|
||||
|
||||
echo "Verifying $URL is responsive..."
|
||||
while [ $COUNT -lt $MAX ]; do
|
||||
STATUS=$(curl -s -o /dev/null -w "%{http_code}" "$URL/" || echo "000")
|
||||
if [ "$STATUS" = "200" ]; then
|
||||
echo "✅ Site is UP (200 OK)"
|
||||
break
|
||||
fi
|
||||
echo "Waiting for service to be ready... ($i/12)"
|
||||
sleep 10
|
||||
echo "⏳ Wait for 200 OK (Status: $STATUS)..."
|
||||
sleep 5
|
||||
COUNT=$((COUNT + 1))
|
||||
done
|
||||
echo "❌ Health check failed after 2 minutes."
|
||||
exit 1
|
||||
|
||||
if [ "$STATUS" != "200" ]; then
|
||||
echo "❌ Site failed smoke test after 2 minutes! (Status: $STATUS)"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
- name: 🌐 Check Case Study Assets and Rewrites
|
||||
shell: sh
|
||||
run: |
|
||||
BASE_URL="${{ needs.prepare.outputs.next_public_url }}"
|
||||
|
||||
echo "Verifying diagnostic rewrite..."
|
||||
STATUS=$(curl -s -L -o /dev/null -w "%{http_code}" "$BASE_URL/robots-test")
|
||||
if [ "$STATUS" != "200" ]; then
|
||||
echo "❌ Diagnostic rewrite /robots-test failed! (Status: $STATUS)"
|
||||
exit 1
|
||||
fi
|
||||
echo "✅ Diagnostic rewrite OK"
|
||||
|
||||
echo "Verifying case study page..."
|
||||
STATUS=$(curl -s -o /dev/null -w "%{http_code}" "$BASE_URL/case-studies/klz-cables")
|
||||
if [ "$STATUS" != "200" ]; then
|
||||
echo "❌ Case study page failed! (Status: $STATUS)"
|
||||
exit 1
|
||||
fi
|
||||
echo "✅ Case study page UP"
|
||||
|
||||
echo "Verifying breeze CSS (root path)..."
|
||||
CSS_PATH="/assets/klz-cables.com/wp-content/cache/breeze-minification/css/breeze_klz-cables-com-1-10895.css"
|
||||
STATUS=$(curl -s -o /dev/null -w "%{http_code}" "$BASE_URL$CSS_PATH")
|
||||
if [ "$STATUS" != "200" ]; then
|
||||
echo "❌ Root asset path failed! (Status: $STATUS)"
|
||||
exit 1
|
||||
fi
|
||||
echo "✅ Root asset path OK"
|
||||
|
||||
echo "Verifying breeze CSS (relative path from case-study)..."
|
||||
REL_CSS_PATH="/case-studies/assets/klz-cables.com/wp-content/cache/breeze-minification/css/breeze_klz-cables-com-1-10895.css"
|
||||
STATUS=$(curl -s -o /dev/null -w "%{http_code}" "$BASE_URL$REL_CSS_PATH")
|
||||
if [ "$STATUS" != "200" ]; then
|
||||
echo "❌ Relative asset path failed! (Status: $STATUS)"
|
||||
exit 1
|
||||
fi
|
||||
echo "✅ Relative asset path OK"
|
||||
|
||||
# ──────────────────────────────────────────────────────────────────────────────
|
||||
# JOB 6: Notifications
|
||||
# ──────────────────────────────────────────────────────────────────────────────
|
||||
notifications:
|
||||
name: 🔔 Notify
|
||||
needs: [prepare, deploy, healthcheck]
|
||||
needs: [prepare, deploy, post_deploy_checks]
|
||||
if: always()
|
||||
runs-on: docker
|
||||
container:
|
||||
@@ -375,11 +532,20 @@ jobs:
|
||||
steps:
|
||||
- name: 🔔 Gotify
|
||||
run: |
|
||||
STATUS="${{ needs.deploy.result }}"
|
||||
TITLE="mintel.me: $STATUS"
|
||||
[[ "$STATUS" == "success" ]] && PRIORITY=5 || PRIORITY=8
|
||||
DEPLOY="${{ needs.deploy.result }}"
|
||||
SMOKE="${{ needs.post_deploy_checks.result }}"
|
||||
TARGET="${{ needs.prepare.outputs.target }}"
|
||||
VERSION="${{ needs.prepare.outputs.image_tag }}"
|
||||
|
||||
if [[ "$DEPLOY" == "success" ]]; then
|
||||
PRIORITY=5
|
||||
EMOJI="✅"
|
||||
else
|
||||
PRIORITY=8
|
||||
EMOJI="🚨"
|
||||
fi
|
||||
|
||||
curl -s -k -X POST "${{ secrets.GOTIFY_URL }}/message?token=${{ secrets.GOTIFY_TOKEN }}" \
|
||||
-F "title=$TITLE" \
|
||||
-F "message=Deploy to ${{ needs.prepare.outputs.target }} finished with status $STATUS.\nVersion: ${{ needs.prepare.outputs.image_tag }}" \
|
||||
-F "title=$EMOJI mintel.me $VERSION -> $TARGET" \
|
||||
-F "message=Deploy: $DEPLOY" \
|
||||
-F "priority=$PRIORITY" || true
|
||||
|
||||
232
.gitea/workflows/qa.yml
Normal file
@@ -0,0 +1,232 @@
|
||||
name: Nightly QA
|
||||
|
||||
on:
|
||||
workflow_run:
|
||||
workflows: ["Build & Deploy"]
|
||||
branches: [main]
|
||||
types:
|
||||
- completed
|
||||
schedule:
|
||||
- cron: "0 3 * * *"
|
||||
workflow_dispatch:
|
||||
|
||||
env:
|
||||
TARGET_URL: "https://testing.mintel.me"
|
||||
PROJECT_NAME: "mintel.me"
|
||||
|
||||
jobs:
|
||||
# ────────────────────────────────────────────────────
|
||||
# 1. Static Checks (HTML, Assets, HTTP)
|
||||
# ────────────────────────────────────────────────────
|
||||
static:
|
||||
name: 🔍 Static Analysis
|
||||
runs-on: docker
|
||||
container:
|
||||
image: catthehacker/ubuntu:act-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: pnpm/action-setup@v3
|
||||
with:
|
||||
version: 10
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 20
|
||||
- name: 🔐 Registry Auth
|
||||
run: |
|
||||
echo "@mintel:registry=https://git.infra.mintel.me/api/packages/mmintel/npm" > .npmrc
|
||||
echo "//git.infra.mintel.me/api/packages/mmintel/npm/:_authToken=${{ secrets.NPM_TOKEN }}" >> .npmrc
|
||||
- name: 📦 Cache node_modules
|
||||
uses: actions/cache@v4
|
||||
id: cache-deps
|
||||
with:
|
||||
path: node_modules
|
||||
key: pnpm-${{ hashFiles('pnpm-lock.yaml') }}
|
||||
- name: Install
|
||||
if: steps.cache-deps.outputs.cache-hit != 'true'
|
||||
run: |
|
||||
pnpm store prune
|
||||
pnpm install --no-frozen-lockfile
|
||||
- name: 🌐 Install Chrome & Dependencies
|
||||
run: |
|
||||
apt-get update && apt-get install -y --fix-missing \
|
||||
libnss3 libnspr4 libatk1.0-0 libatk-bridge2.0-0 libcups2 libdrm2 \
|
||||
libxkbcommon0 libxcomposite1 libxdamage1 libxext6 libxfixes3 \
|
||||
libxrandr2 libgbm1 libpango-1.0-0 libcairo2 || true
|
||||
apt-get install -y libasound2t64 || apt-get install -y libasound2 || true
|
||||
npx puppeteer browsers install chrome || true
|
||||
- name: 🖼️ OG Images
|
||||
continue-on-error: true
|
||||
env:
|
||||
TEST_URL: ${{ env.TARGET_URL }}
|
||||
GATEKEEPER_PASSWORD: ${{ secrets.GATEKEEPER_PASSWORD }}
|
||||
run: pnpm --filter @mintel/web run check:og
|
||||
|
||||
# ────────────────────────────────────────────────────
|
||||
# 2. E2E (Forms)
|
||||
# ────────────────────────────────────────────────────
|
||||
e2e:
|
||||
name: 📝 E2E
|
||||
runs-on: docker
|
||||
container:
|
||||
image: catthehacker/ubuntu:act-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: pnpm/action-setup@v3
|
||||
with:
|
||||
version: 10
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 20
|
||||
- name: 🔐 Registry Auth
|
||||
run: |
|
||||
echo "@mintel:registry=https://git.infra.mintel.me/api/packages/mmintel/npm" > .npmrc
|
||||
echo "//git.infra.mintel.me/api/packages/mmintel/npm/:_authToken=${{ secrets.NPM_TOKEN }}" >> .npmrc
|
||||
- name: 📦 Cache node_modules
|
||||
uses: actions/cache@v4
|
||||
id: cache-deps
|
||||
with:
|
||||
path: node_modules
|
||||
key: pnpm-${{ hashFiles('pnpm-lock.yaml') }}
|
||||
- name: Install
|
||||
if: steps.cache-deps.outputs.cache-hit != 'true'
|
||||
run: |
|
||||
pnpm store prune
|
||||
pnpm install --no-frozen-lockfile
|
||||
- name: 🌐 Install Chrome & Dependencies
|
||||
run: |
|
||||
apt-get update && apt-get install -y libnss3 libnspr4 libatk1.0-0 libatk-bridge2.0-0 libcups2 libdrm2 libxkbcommon0 libxcomposite1 libxdamage1 libxext6 libxfixes3 libxrandr2 libgbm1 libasound2t64 libpango-1.0-0 libcairo2
|
||||
npx puppeteer browsers install chrome || true
|
||||
- name: 📝 E2E Form Submission Test
|
||||
continue-on-error: true
|
||||
env:
|
||||
TEST_URL: ${{ env.TARGET_URL }}
|
||||
NEXT_PUBLIC_BASE_URL: ${{ env.TARGET_URL }}
|
||||
GATEKEEPER_PASSWORD: ${{ secrets.GATEKEEPER_PASSWORD }}
|
||||
run: pnpm --filter @mintel/web run check:forms
|
||||
|
||||
# ────────────────────────────────────────────────────
|
||||
# 3. Performance (Lighthouse)
|
||||
# ────────────────────────────────────────────────────
|
||||
lighthouse:
|
||||
name: 🎭 Lighthouse
|
||||
runs-on: docker
|
||||
container:
|
||||
image: catthehacker/ubuntu:act-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: pnpm/action-setup@v3
|
||||
with:
|
||||
version: 10
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 20
|
||||
- name: 🔐 Registry Auth
|
||||
run: |
|
||||
echo "@mintel:registry=https://git.infra.mintel.me/api/packages/mmintel/npm" > .npmrc
|
||||
echo "//git.infra.mintel.me/api/packages/mmintel/npm/:_authToken=${{ secrets.NPM_TOKEN }}" >> .npmrc
|
||||
- name: 📦 Cache node_modules
|
||||
uses: actions/cache@v4
|
||||
id: cache-deps
|
||||
with:
|
||||
path: node_modules
|
||||
key: pnpm-${{ hashFiles('pnpm-lock.yaml') }}
|
||||
- name: Install
|
||||
if: steps.cache-deps.outputs.cache-hit != 'true'
|
||||
run: |
|
||||
pnpm store prune
|
||||
pnpm install --no-frozen-lockfile
|
||||
- name: 🌐 Install Chrome & Dependencies
|
||||
run: |
|
||||
apt-get update && apt-get install -y libnss3 libnspr4 libatk1.0-0 libatk-bridge2.0-0 libcups2 libdrm2 libxkbcommon0 libxcomposite1 libxdamage1 libxext6 libxfixes3 libxrandr2 libgbm1 libasound2t64 libpango-1.0-0 libcairo2
|
||||
npx puppeteer browsers install chrome || true
|
||||
- name: 🎭 Desktop
|
||||
env:
|
||||
NEXT_PUBLIC_BASE_URL: ${{ env.TARGET_URL }}
|
||||
GATEKEEPER_PASSWORD: ${{ secrets.GATEKEEPER_PASSWORD }}
|
||||
PAGESPEED_LIMIT: 5
|
||||
run: pnpm --filter @mintel/web run pagespeed:test
|
||||
- name: 📱 Mobile
|
||||
env:
|
||||
NEXT_PUBLIC_BASE_URL: ${{ env.TARGET_URL }}
|
||||
GATEKEEPER_PASSWORD: ${{ secrets.GATEKEEPER_PASSWORD }}
|
||||
PAGESPEED_LIMIT: 5
|
||||
run: pnpm --filter @mintel/web run pagespeed:test
|
||||
|
||||
# ────────────────────────────────────────────────────
|
||||
# 4. Link Check & Dependency Audit
|
||||
# ────────────────────────────────────────────────────
|
||||
links:
|
||||
name: 🔗 Links & Deps
|
||||
runs-on: docker
|
||||
container:
|
||||
image: catthehacker/ubuntu:act-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: pnpm/action-setup@v3
|
||||
with:
|
||||
version: 10
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 20
|
||||
- name: 🔐 Registry Auth
|
||||
run: |
|
||||
echo "@mintel:registry=https://git.infra.mintel.me/api/packages/mmintel/npm" > .npmrc
|
||||
echo "//git.infra.mintel.me/api/packages/mmintel/npm/:_authToken=${{ secrets.NPM_TOKEN }}" >> .npmrc
|
||||
- name: 📦 Cache node_modules
|
||||
uses: actions/cache@v4
|
||||
id: cache-deps
|
||||
with:
|
||||
path: node_modules
|
||||
key: pnpm-${{ hashFiles('pnpm-lock.yaml') }}
|
||||
- name: Install
|
||||
if: steps.cache-deps.outputs.cache-hit != 'true'
|
||||
run: |
|
||||
pnpm store prune
|
||||
pnpm install --no-frozen-lockfile
|
||||
- name: 📦 Depcheck
|
||||
continue-on-error: true
|
||||
run: pnpm dlx depcheck --ignores="*eslint*,*typescript*,*tailwindcss*,*postcss*,*prettier*,*@types/*,*husky*,*lint-staged*,*@next/*,*@lhci/*,*commitlint*,*cspell*,*rimraf*,*@payloadcms/*,*start-server-and-test*,*html-validate*,*critters*,*dotenv*,*turbo*" || true
|
||||
- name: 🔗 Lychee Link Check
|
||||
uses: lycheeverse/lychee-action@v2
|
||||
continue-on-error: true
|
||||
with:
|
||||
args: --accept 200,204,429 --timeout 10 --insecure --exclude "file://*" --exclude "https://logs.infra.mintel.me/*" --exclude "https://git.infra.mintel.me/*" --exclude "https://mintel.me/*" '*.md' 'docs/*.md'
|
||||
fail: false
|
||||
|
||||
# ────────────────────────────────────────────────────
|
||||
# 5. Notification
|
||||
# ────────────────────────────────────────────────────
|
||||
notify:
|
||||
name: 🔔 Notify
|
||||
needs: [static, e2e, lighthouse, links]
|
||||
if: failure()
|
||||
runs-on: docker
|
||||
container:
|
||||
image: catthehacker/ubuntu:act-latest
|
||||
steps:
|
||||
- name: 🔔 Gotify
|
||||
shell: bash
|
||||
run: |
|
||||
STATIC="${{ needs.static.result }}"
|
||||
E2E="${{ needs.e2e.result }}"
|
||||
LIGHTHOUSE="${{ needs.lighthouse.result }}"
|
||||
LINKS="${{ needs.links.result }}"
|
||||
|
||||
if [[ "$STATIC" != "success" || "$LIGHTHOUSE" != "success" ]]; then
|
||||
PRIORITY=8
|
||||
EMOJI="🚨"
|
||||
STATUS="Failed"
|
||||
else
|
||||
PRIORITY=2
|
||||
EMOJI="✅"
|
||||
STATUS="Passed"
|
||||
fi
|
||||
|
||||
TITLE="$EMOJI ${{ env.PROJECT_NAME }} QA $STATUS"
|
||||
MESSAGE="Static: $STATIC | E2E: $E2E | Lighthouse: $LIGHTHOUSE | Links: $LINKS
|
||||
${{ env.TARGET_URL }}"
|
||||
|
||||
curl -s -k -X POST "${{ secrets.GOTIFY_URL }}/message?token=${{ secrets.GOTIFY_TOKEN }}" \
|
||||
-F "title=$TITLE" \
|
||||
-F "message=$MESSAGE" \
|
||||
-F "priority=$PRIORITY" || true
|
||||
14
.gitignore
vendored
@@ -47,3 +47,17 @@ pnpm-debug.log*
|
||||
.cache/
|
||||
cloned-websites/
|
||||
storage/
|
||||
data/postgres/
|
||||
|
||||
# Estimation Engine Data
|
||||
data/crawls/
|
||||
apps/web/out/estimations/
|
||||
|
||||
# Backups
|
||||
backups/
|
||||
|
||||
.turbo
|
||||
# Manual build artifacts
|
||||
_at-mintel/
|
||||
local_build_*.log
|
||||
*.tar
|
||||
|
||||
5
.npmrc
@@ -1,3 +1,2 @@
|
||||
@mintel:registry=https://npm.infra.mintel.me/
|
||||
//npm.infra.mintel.me/:_authToken=${NPM_TOKEN}
|
||||
always-auth=true
|
||||
@mintel:registry=https://git.infra.mintel.me/api/packages/mmintel/npm/
|
||||
//git.infra.mintel.me/api/packages/mmintel/npm/:_authToken=263e7f75d8ada27f3a2e71fd6bd9d95298d48a4d
|
||||
|
||||
1
.turbo/cache/41a721a9104bd76c-meta.json
vendored
Normal file
@@ -0,0 +1 @@
|
||||
{ "hash": "41a721a9104bd76c", "duration": 2524 }
|
||||
BIN
.turbo/cache/41a721a9104bd76c.tar.zst
vendored
Normal file
1
.turbo/cache/441277b34176cf11-meta.json
vendored
Normal file
@@ -0,0 +1 @@
|
||||
{ "hash": "441277b34176cf11", "duration": 2934 }
|
||||
BIN
.turbo/cache/441277b34176cf11.tar.zst
vendored
Normal file
1
.turbo/cache/708dc951079154e6-meta.json
vendored
Normal file
@@ -0,0 +1 @@
|
||||
{ "hash": "708dc951079154e6", "duration": 194 }
|
||||
BIN
.turbo/cache/708dc951079154e6.tar.zst
vendored
Normal file
1
.turbo/cache/84b66091bfb55705-meta.json
vendored
Normal file
@@ -0,0 +1 @@
|
||||
{ "hash": "84b66091bfb55705", "duration": 2417 }
|
||||
BIN
.turbo/cache/84b66091bfb55705.tar.zst
vendored
Normal file
1
.turbo/cache/ba4a4a0aae882f7f-meta.json
vendored
Normal file
@@ -0,0 +1 @@
|
||||
{ "hash": "ba4a4a0aae882f7f", "duration": 5009 }
|
||||
BIN
.turbo/cache/ba4a4a0aae882f7f.tar.zst
vendored
Normal file
63
Dockerfile
@@ -1,5 +1,5 @@
|
||||
# Stage 1: Builder
|
||||
FROM registry.infra.mintel.me/mintel/nextjs:latest AS builder
|
||||
FROM git.infra.mintel.me/mmintel/nextjs:latest AS builder
|
||||
WORKDIR /app
|
||||
|
||||
# Arguments for build-time configuration
|
||||
@@ -7,44 +7,85 @@ ARG NEXT_PUBLIC_BASE_URL
|
||||
ARG NEXT_PUBLIC_TARGET
|
||||
ARG UMAMI_API_ENDPOINT
|
||||
ARG NPM_TOKEN
|
||||
ARG S3_ENDPOINT
|
||||
ARG S3_ACCESS_KEY
|
||||
ARG S3_SECRET_KEY
|
||||
ARG S3_BUCKET
|
||||
ARG S3_REGION
|
||||
ARG S3_PREFIX
|
||||
ARG DATABASE_URI
|
||||
ARG PAYLOAD_SECRET
|
||||
ARG BUILD_ID
|
||||
|
||||
# Environment variables for Next.js build
|
||||
ENV NEXT_PUBLIC_BASE_URL=$NEXT_PUBLIC_BASE_URL
|
||||
ENV NEXT_PUBLIC_TARGET=$NEXT_PUBLIC_TARGET
|
||||
ENV UMAMI_API_ENDPOINT=$UMAMI_API_ENDPOINT
|
||||
ENV S3_ENDPOINT=$S3_ENDPOINT
|
||||
ENV S3_ACCESS_KEY=$S3_ACCESS_KEY
|
||||
ENV S3_SECRET_KEY=$S3_SECRET_KEY
|
||||
ENV S3_BUCKET=$S3_BUCKET
|
||||
ENV S3_REGION=$S3_REGION
|
||||
ENV S3_PREFIX=$S3_PREFIX
|
||||
ENV DATABASE_URI=$DATABASE_URI
|
||||
ENV PAYLOAD_SECRET=$PAYLOAD_SECRET
|
||||
ENV SKIP_RUNTIME_ENV_VALIDATION=true
|
||||
ENV NEXT_BUILD_WORKERS=1
|
||||
ENV NEXT_DISABLE_SOURCEMAPS=true
|
||||
ENV SENTRY_SKIP_LOCAL_SOURCES=true
|
||||
ENV NEXT_PRIVATE_LOCAL_WEBPACK=true
|
||||
ENV CI=true
|
||||
|
||||
# Copy manifest files specifically for better layer caching
|
||||
COPY pnpm-lock.yaml pnpm-workspace.yaml package.json .npmrc* ./
|
||||
COPY apps/web/package.json ./apps/web/package.json
|
||||
# Install build dependencies for native modules (like canvas)
|
||||
RUN apk add --no-cache python3 make g++ pkgconfig pixman-dev cairo-dev pango-dev libjpeg-turbo-dev giflib-dev librsvg-dev
|
||||
|
||||
# Copy sibling monorepo for linked dependencies (cloned during CI)
|
||||
# Placing it inside /app so relative links like ../../_at-mintel resolve correctly!
|
||||
COPY _at-mintel* /app/_at-mintel/
|
||||
|
||||
|
||||
# Install dependencies with cache mount and dynamic .npmrc (High Fidelity pattern)
|
||||
RUN --mount=type=cache,id=pnpm,target=/pnpm/store \
|
||||
--mount=type=secret,id=NPM_TOKEN \
|
||||
export NPM_TOKEN=$(cat /run/secrets/NPM_TOKEN 2>/dev/null || echo $NPM_TOKEN) && \
|
||||
echo "@mintel:registry=https://npm.infra.mintel.me" > .npmrc && \
|
||||
echo "//npm.infra.mintel.me/:_authToken=\${NPM_TOKEN}" >> .npmrc && \
|
||||
pnpm install --frozen-lockfile && \
|
||||
echo "//npm.infra.mintel.me/:_authToken=\"\${NPM_TOKEN}\"" >> .npmrc && \
|
||||
echo "always-auth=true" >> .npmrc && \
|
||||
cd /app/_at-mintel && pnpm install --no-frozen-lockfile && pnpm build && \
|
||||
cd /app && pnpm install --no-frozen-lockfile && \
|
||||
rm .npmrc
|
||||
|
||||
# Copy source code
|
||||
# We use BUILD_ID here to ensure that if the commit changes, we always COPY the latest files
|
||||
# even if Docker's metadata-based fingerprinting for the public directory fails.
|
||||
ARG BUILD_ID
|
||||
RUN echo "Building with ID: ${BUILD_ID}"
|
||||
COPY . .
|
||||
|
||||
# Build application (monorepo filter)
|
||||
RUN pnpm --filter @mintel/web build
|
||||
ENV NODE_OPTIONS="--max_old_space_size=4096"
|
||||
RUN NEXT_BUILD_WORKERS=1 pnpm --filter @mintel/web build
|
||||
|
||||
# Stage 2: Runner
|
||||
FROM registry.infra.mintel.me/mintel/runtime:latest AS runner
|
||||
FROM git.infra.mintel.me/mmintel/runtime:latest AS runner
|
||||
WORKDIR /app
|
||||
|
||||
# Copy standalone output and static files (Monorepo paths)
|
||||
# Note: Base image already handles the non-root user and basic env
|
||||
COPY --from=builder /app/apps/web/public ./apps/web/public
|
||||
COPY --from=builder /app/apps/web/.next/standalone ./
|
||||
COPY --from=builder /app/apps/web/.next/static ./apps/web/.next/static
|
||||
COPY --from=builder /app/apps/web/.next/cache ./apps/web/.next/cache
|
||||
COPY --from=builder --chown=1001:65533 /app/apps/web/public ./apps/web/public
|
||||
COPY --from=builder --chown=1001:65533 /app/apps/web/.next/standalone ./
|
||||
COPY --from=builder --chown=1001:65533 /app/apps/web/.next/static ./apps/web/.next/static
|
||||
|
||||
# Explicitly copy Payload dynamically generated importMap.js excluded by Standalone tracing
|
||||
COPY --from=builder --chown=1001:65533 /app/apps/web/app/(payload)/admin/importMap.js ./apps/web/app/(payload)/admin/importMap.js
|
||||
|
||||
# Fix permissions for the non-root user (Standard uid/gid from base image)
|
||||
USER nextjs
|
||||
|
||||
# Start from the app directory to ensure references solve correctly
|
||||
WORKDIR /app/apps/web
|
||||
CMD ["node", "server.js"]
|
||||
# In Standalone mode, Next.js expects node_modules and public relative to the server.js
|
||||
WORKDIR /app
|
||||
CMD ["node", "apps/web/server.js"]
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
FROM node:20-alpine
|
||||
|
||||
# Install essential build tools if needed (e.g., for node-gyp)
|
||||
RUN apk add --no-cache libc6-compat python3 make g++
|
||||
RUN apk add --no-cache libc6-compat git python3 make g++
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
|
||||
|
Before Width: | Height: | Size: 953 KiB |
|
Before Width: | Height: | Size: 198 KiB |
|
Before Width: | Height: | Size: 1.4 MiB |
|
Before Width: | Height: | Size: 5.0 MiB |
|
Before Width: | Height: | Size: 915 KiB |
|
Before Width: | Height: | Size: 187 KiB |
|
Before Width: | Height: | Size: 1.1 MiB |
|
Before Width: | Height: | Size: 5.2 MiB |
|
Before Width: | Height: | Size: 654 KiB |
|
Before Width: | Height: | Size: 133 KiB |
|
Before Width: | Height: | Size: 987 KiB |
|
Before Width: | Height: | Size: 4.8 MiB |
|
Before Width: | Height: | Size: 659 KiB |
|
Before Width: | Height: | Size: 137 KiB |
|
Before Width: | Height: | Size: 1.1 MiB |
|
Before Width: | Height: | Size: 4.5 MiB |
|
Before Width: | Height: | Size: 670 KiB |
|
Before Width: | Height: | Size: 133 KiB |
|
Before Width: | Height: | Size: 987 KiB |
|
Before Width: | Height: | Size: 4.5 MiB |
|
Before Width: | Height: | Size: 690 KiB |
|
Before Width: | Height: | Size: 162 KiB |
|
Before Width: | Height: | Size: 1.1 MiB |
|
Before Width: | Height: | Size: 1.1 MiB |
|
Before Width: | Height: | Size: 743 KiB |
|
Before Width: | Height: | Size: 165 KiB |
|
Before Width: | Height: | Size: 1.2 MiB |
|
Before Width: | Height: | Size: 4.9 MiB |
|
Before Width: | Height: | Size: 792 KiB |
|
Before Width: | Height: | Size: 170 KiB |
|
Before Width: | Height: | Size: 1.3 MiB |
|
Before Width: | Height: | Size: 4.8 MiB |
|
Before Width: | Height: | Size: 891 KiB |
|
Before Width: | Height: | Size: 198 KiB |
|
Before Width: | Height: | Size: 1.4 MiB |
|
Before Width: | Height: | Size: 5.6 MiB |
|
Before Width: | Height: | Size: 606 KiB |
|
Before Width: | Height: | Size: 133 KiB |
|
Before Width: | Height: | Size: 1.1 MiB |
|
Before Width: | Height: | Size: 4.3 MiB |
|
Before Width: | Height: | Size: 606 KiB |
|
Before Width: | Height: | Size: 123 KiB |
|
Before Width: | Height: | Size: 1.0 MiB |
|
Before Width: | Height: | Size: 4.5 MiB |
|
Before Width: | Height: | Size: 783 KiB |
|
Before Width: | Height: | Size: 166 KiB |
|
Before Width: | Height: | Size: 1.2 MiB |
|
Before Width: | Height: | Size: 5.2 MiB |
|
Before Width: | Height: | Size: 635 KiB |
|
Before Width: | Height: | Size: 131 KiB |
|
Before Width: | Height: | Size: 1.1 MiB |
|
Before Width: | Height: | Size: 4.8 MiB |
|
Before Width: | Height: | Size: 501 KiB |
|
Before Width: | Height: | Size: 102 KiB |
|
Before Width: | Height: | Size: 868 KiB |
|
Before Width: | Height: | Size: 3.8 MiB |
|
Before Width: | Height: | Size: 633 KiB |
|
Before Width: | Height: | Size: 141 KiB |
|
Before Width: | Height: | Size: 1.1 MiB |
|
Before Width: | Height: | Size: 4.3 MiB |
|
Before Width: | Height: | Size: 653 KiB |
|
Before Width: | Height: | Size: 134 KiB |
|
Before Width: | Height: | Size: 1.0 MiB |
|
Before Width: | Height: | Size: 4.3 MiB |
|
Before Width: | Height: | Size: 752 KiB |
|
Before Width: | Height: | Size: 170 KiB |
|
Before Width: | Height: | Size: 1.3 MiB |
|
Before Width: | Height: | Size: 5.0 MiB |
|
Before Width: | Height: | Size: 620 KiB |
|
Before Width: | Height: | Size: 123 KiB |
|
Before Width: | Height: | Size: 1.0 MiB |
|
Before Width: | Height: | Size: 4.2 MiB |
|
Before Width: | Height: | Size: 575 KiB |
|
Before Width: | Height: | Size: 123 KiB |
|
Before Width: | Height: | Size: 966 KiB |
|
Before Width: | Height: | Size: 4.3 MiB |
|
Before Width: | Height: | Size: 862 KiB |
|
Before Width: | Height: | Size: 178 KiB |
|
Before Width: | Height: | Size: 1.4 MiB |
|
Before Width: | Height: | Size: 5.0 MiB |
|
Before Width: | Height: | Size: 620 KiB |
|
Before Width: | Height: | Size: 128 KiB |