chore: unify pipeline with klz-2026 standards and harden registry auth
Some checks failed
Build & Deploy / 🔍 Prepare (push) Successful in 41s
Build & Deploy / 🧪 QA (push) Failing after 1m2s
Build & Deploy / 🏗️ Build (push) Has been skipped
Build & Deploy / 🚀 Deploy (push) Has been skipped
Build & Deploy / 🧪 Post-Deploy Verification (push) Has been skipped
Build & Deploy / 🔔 Notify (push) Successful in 1s
Some checks failed
Build & Deploy / 🔍 Prepare (push) Successful in 41s
Build & Deploy / 🧪 QA (push) Failing after 1m2s
Build & Deploy / 🏗️ Build (push) Has been skipped
Build & Deploy / 🚀 Deploy (push) Has been skipped
Build & Deploy / 🧪 Post-Deploy Verification (push) Has been skipped
Build & Deploy / 🔔 Notify (push) Successful in 1s
This commit is contained in:
@@ -10,6 +10,8 @@ concurrency:
|
||||
jobs:
|
||||
quality-assurance:
|
||||
runs-on: docker
|
||||
container:
|
||||
image: catthehacker/ubuntu:act-latest
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
@@ -18,7 +20,6 @@ jobs:
|
||||
uses: pnpm/action-setup@v3
|
||||
with:
|
||||
version: 10
|
||||
run_install: false
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v4
|
||||
@@ -31,13 +32,14 @@ jobs:
|
||||
GITEA_PAT: ${{ secrets.GITEA_PAT }}
|
||||
MINTEL_PRIVATE_TOKEN: ${{ secrets.MINTEL_PRIVATE_TOKEN }}
|
||||
run: bash scripts/registry-auth.sh
|
||||
|
||||
- name: Install dependencies
|
||||
run: pnpm install --no-frozen-lockfile
|
||||
|
||||
- name: 🧪 QA Checks
|
||||
env:
|
||||
TURBO_TELEMETRY_DISABLED: "1"
|
||||
run: npx turbo run check:mdx lint typecheck test --cache-dir=".turbo"
|
||||
run: npx turbo run lint typecheck test --cache-dir=".turbo"
|
||||
|
||||
- name: 🏗️ Build
|
||||
run: pnpm build
|
||||
|
||||
@@ -70,7 +70,7 @@ jobs:
|
||||
if [[ "$REF" =~ ^v[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
|
||||
TARGET="production"
|
||||
IMAGE_TAG="$REF"
|
||||
ENV_FILE=".env.prod"
|
||||
ENV_FILE=".env.production"
|
||||
TRAEFIK_HOST="${DOMAIN}, www.${DOMAIN}"
|
||||
else
|
||||
TARGET="staging"
|
||||
@@ -86,7 +86,7 @@ jobs:
|
||||
TRAEFIK_HOST="${SLUG}.branch.mintel.me"
|
||||
fi
|
||||
|
||||
# Standardize Traefik Rule (escaped backticks for Traefik v3)
|
||||
# Standardize Traefik Rule
|
||||
if [[ "$TRAEFIK_HOST" == *","* ]]; then
|
||||
TRAEFIK_RULE=$(echo "$TRAEFIK_HOST" | sed 's/,/ /g' | awk '{for(i=1;i<=NF;i++) printf "Host(\x60%s\x60)%s", $i, (i==NF?"":" || ")}')
|
||||
PRIMARY_HOST=$(echo "$TRAEFIK_HOST" | cut -d',' -f1 | sed 's/ //g')
|
||||
@@ -106,7 +106,7 @@ jobs:
|
||||
echo "gatekeeper_host=$GATEKEEPER_HOST"
|
||||
echo "next_public_url=https://$PRIMARY_HOST"
|
||||
if [[ "$TARGET" == "production" ]]; then
|
||||
echo "project_name=etib-com"
|
||||
echo "project_name=etib-production"
|
||||
elif [[ "$TARGET" == "branch" ]]; then
|
||||
echo "project_name=$PRJ-branch-$SLUG"
|
||||
else
|
||||
@@ -115,39 +115,6 @@ jobs:
|
||||
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 | grep -v "next-utils" | cut -d'"' -f4 | sed 's/\^//; s/\~//' | sort -V | tail -1)
|
||||
TAG_TO_WAIT="v$UPSTREAM_VERSION"
|
||||
|
||||
if [[ -n "$UPSTREAM_VERSION" && "$UPSTREAM_VERSION" != "workspace:"* ]]; then
|
||||
# 1. Discovery (Works without token for public repositories)
|
||||
UPSTREAM_SHA=$(git ls-remote --tags https://git.infra.mintel.me/mmintel/at-mintel.git "$TAG_TO_WAIT" 2>/dev/null | grep "$TAG_TO_WAIT" | awk '{print $1}' | tail -n1 || echo "")
|
||||
|
||||
if [[ -z "$UPSTREAM_SHA" ]]; then
|
||||
echo "⚠️ Warning: Tag $TAG_TO_WAIT not found in mmintel/at-mintel."
|
||||
else
|
||||
echo "✅ Tag verified: Found upstream SHA $UPSTREAM_SHA for $TAG_TO_WAIT"
|
||||
fi
|
||||
|
||||
# 2. Status Check (Requires GITEA_PAT for cross-repo API access)
|
||||
POLL_TOKEN="${{ secrets.GITEA_PAT || secrets.MINTEL_PRIVATE_TOKEN }}"
|
||||
|
||||
if [[ -n "$POLL_TOKEN" ]]; then
|
||||
echo "⏳ GITEA_PAT found. Checking upstream build status..."
|
||||
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="$POLL_TOKEN" ./wait-for-upstream.sh "mmintel/at-mintel" "$TAG_TO_WAIT"
|
||||
else
|
||||
echo "ℹ️ No GITEA_PAT secret found. Skipping build status wait (Actions API is restricted)."
|
||||
echo " If this build fails, ensure that mmintel/at-mintel $TAG_TO_WAIT has finished its Docker build."
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
|
||||
# ──────────────────────────────────────────────────────────────────────────────
|
||||
# JOB 2: QA (Lint, Typecheck, Test)
|
||||
# ──────────────────────────────────────────────────────────────────────────────
|
||||
@@ -176,13 +143,12 @@ jobs:
|
||||
GITEA_PAT: ${{ secrets.GITEA_PAT }}
|
||||
MINTEL_PRIVATE_TOKEN: ${{ secrets.MINTEL_PRIVATE_TOKEN }}
|
||||
run: bash scripts/registry-auth.sh
|
||||
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
pnpm store prune
|
||||
pnpm install --no-frozen-lockfile
|
||||
|
||||
- name: 🔒 Security Audit
|
||||
run: pnpm audit --audit-level high || echo "⚠️ Audit found vulnerabilities (non-blocking)"
|
||||
- name: 🧪 QA Checks
|
||||
if: github.event.inputs.skip_checks != 'true'
|
||||
env:
|
||||
@@ -219,7 +185,7 @@ jobs:
|
||||
UMAMI_WEBSITE_ID=${{ secrets.UMAMI_WEBSITE_ID || vars.UMAMI_WEBSITE_ID }}
|
||||
UMAMI_API_ENDPOINT=${{ secrets.UMAMI_API_ENDPOINT || vars.UMAMI_API_ENDPOINT || 'https://analytics.infra.mintel.me' }}
|
||||
NPM_TOKEN=${{ secrets.NPM_TOKEN }}
|
||||
tags: registry.infra.mintel.me/mintel/etib-website:${{ needs.prepare.outputs.image_tag }}
|
||||
tags: registry.infra.mintel.me/mintel/e-tib.com:${{ needs.prepare.outputs.image_tag }}
|
||||
secrets: |
|
||||
"NPM_TOKEN=${{ secrets.NPM_TOKEN }}"
|
||||
|
||||
@@ -240,7 +206,6 @@ jobs:
|
||||
TRAEFIK_HOST: ${{ needs.prepare.outputs.traefik_host }}
|
||||
GATEKEEPER_HOST: ${{ needs.prepare.outputs.gatekeeper_host }}
|
||||
|
||||
|
||||
# Secrets mapping (Mail)
|
||||
MAIL_HOST: ${{ secrets.SMTP_HOST || vars.SMTP_HOST }}
|
||||
MAIL_PORT: ${{ secrets.SMTP_PORT || vars.SMTP_PORT || '587' }}
|
||||
@@ -253,7 +218,7 @@ jobs:
|
||||
SENTRY_DSN: ${{ secrets.SENTRY_DSN || vars.SENTRY_DSN }}
|
||||
|
||||
# Gatekeeper
|
||||
GATEKEEPER_PASSWORD: ${{ secrets.GATEKEEPER_PASSWORD || 'etib2026' }}
|
||||
GATEKEEPER_PASSWORD: ${{ secrets.GATEKEEPER_PASSWORD }}
|
||||
|
||||
# Analytics
|
||||
UMAMI_WEBSITE_ID: ${{ secrets.UMAMI_WEBSITE_ID || vars.UMAMI_WEBSITE_ID }}
|
||||
@@ -267,9 +232,6 @@ jobs:
|
||||
TRAEFIK_RULE: ${{ needs.prepare.outputs.traefik_rule }}
|
||||
ENV_FILE: ${{ needs.prepare.outputs.env_file }}
|
||||
run: |
|
||||
# Middleware Selection Logic
|
||||
# Regular app routes get auth on non-production
|
||||
# Unprotected routes (/stats, /errors) never get auth
|
||||
LOG_LEVEL=$( [[ "$TARGET" == "testing" || "$TARGET" == "development" ]] && echo "debug" || echo "info" )
|
||||
COOKIE_DOMAIN=.$(echo $NEXT_PUBLIC_BASE_URL | sed 's|https://||')
|
||||
STD_MW="${PROJECT_NAME}-ratelimit,${PROJECT_NAME}-forward,${PROJECT_NAME}-compress"
|
||||
@@ -278,13 +240,11 @@ jobs:
|
||||
AUTH_MIDDLEWARE="$STD_MW"
|
||||
COMPOSE_PROFILES=""
|
||||
else
|
||||
# Order: Ratelimit -> Forward (Proto) -> Auth -> Compression
|
||||
AUTH_MIDDLEWARE="${PROJECT_NAME}-ratelimit,${PROJECT_NAME}-forward,${PROJECT_NAME}-auth,${PROJECT_NAME}-compress"
|
||||
COMPOSE_PROFILES="gatekeeper"
|
||||
fi
|
||||
AUTH_MIDDLEWARE_UNPROTECTED="$STD_MW"
|
||||
|
||||
# Gatekeeper Origin
|
||||
GATEKEEPER_ORIGIN="${NEXT_PUBLIC_BASE_URL}/gatekeeper"
|
||||
|
||||
{
|
||||
@@ -301,7 +261,6 @@ jobs:
|
||||
echo "MAIL_FROM=$MAIL_FROM"
|
||||
echo "MAIL_RECIPIENTS=$MAIL_RECIPIENTS"
|
||||
echo ""
|
||||
echo ""
|
||||
echo "# Gatekeeper"
|
||||
echo "GATEKEEPER_PASSWORD=$GATEKEEPER_PASSWORD"
|
||||
echo "AUTH_COOKIE_NAME=etib_gatekeeper_session"
|
||||
@@ -326,10 +285,6 @@ jobs:
|
||||
echo "AUTH_MIDDLEWARE_UNPROTECTED=$AUTH_MIDDLEWARE_UNPROTECTED"
|
||||
} > .env.deploy
|
||||
|
||||
echo "--- Generated .env.deploy ---"
|
||||
cat .env.deploy
|
||||
echo "----------------------------"
|
||||
|
||||
- name: 🚀 SSH Deploy
|
||||
shell: bash
|
||||
env:
|
||||
@@ -340,7 +295,6 @@ jobs:
|
||||
chmod 600 ~/.ssh/id_ed25519
|
||||
ssh-keyscan -H alpha.mintel.me >> ~/.ssh/known_hosts 2>/dev/null
|
||||
|
||||
# Transfer and Restart
|
||||
if [[ "$TARGET" == "production" ]]; then
|
||||
SITE_DIR="/home/deploy/sites/e-tib.com"
|
||||
elif [[ "$TARGET" == "testing" ]]; then
|
||||
@@ -348,26 +302,21 @@ jobs:
|
||||
elif [[ "$TARGET" == "staging" ]]; then
|
||||
SITE_DIR="/home/deploy/sites/staging.e-tib.com"
|
||||
else
|
||||
SITE_DIR="/home/deploy/sites/branch.e-tib.com/${SLUG:-unknown}"
|
||||
SLUG=$(echo "${{ github.ref_name }}" | tr '[:upper:]' '[:lower:]' | sed 's/[^a-z0-9]/-/g' | sed 's/--*/-/g' | sed 's/^-//;s/-$//')
|
||||
SITE_DIR="/home/deploy/sites/branch.e-tib.com/$SLUG"
|
||||
fi
|
||||
ssh root@alpha.mintel.me "mkdir -p $SITE_DIR"
|
||||
|
||||
ssh root@alpha.mintel.me "mkdir -p $SITE_DIR"
|
||||
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"
|
||||
|
||||
|
||||
ssh root@alpha.mintel.me "docker system prune -f --filter 'until=24h'"
|
||||
|
||||
- name: 🧹 Post-Deploy Cleanup (Runner)
|
||||
if: always()
|
||||
run: docker builder prune -f --filter "until=1h"
|
||||
|
||||
# ──────────────────────────────────────────────────────────────────────────────
|
||||
# JOB 5: Post-Deploy Verification (Smoke Tests + Quality Gates)
|
||||
# JOB 5: Post-Deploy Verification
|
||||
# ──────────────────────────────────────────────────────────────────────────────
|
||||
post_deploy_checks:
|
||||
name: 🧪 Post-Deploy Verification
|
||||
@@ -394,27 +343,15 @@ jobs:
|
||||
MINTEL_PRIVATE_TOKEN: ${{ secrets.MINTEL_PRIVATE_TOKEN }}
|
||||
run: bash scripts/registry-auth.sh
|
||||
- name: Install dependencies
|
||||
id: deps
|
||||
run: |
|
||||
pnpm store prune
|
||||
pnpm install --no-frozen-lockfile
|
||||
- name: Setup Chrome
|
||||
id: setup-chrome
|
||||
uses: browser-actions/setup-chrome@v1
|
||||
with:
|
||||
install-dependencies: true
|
||||
|
||||
# ── Minimalist Smoke Test ──────────────────────────────────────────
|
||||
- name: 🌐 Smoke Test (Essential Flow)
|
||||
if: always() && steps.deps.outcome == 'success'
|
||||
run: pnpm install --no-frozen-lockfile
|
||||
- name: 🌐 Run Smoke Test
|
||||
env:
|
||||
NEXT_PUBLIC_BASE_URL: ${{ needs.prepare.outputs.next_public_url }}
|
||||
GATEKEEPER_PASSWORD: ${{ secrets.GATEKEEPER_PASSWORD || 'etib2026' }}
|
||||
PUPPETEER_EXECUTABLE_PATH: ${{ steps.setup-chrome.outputs.chrome-path }}
|
||||
run: npx tsx scripts/smoke.ts
|
||||
GATEKEEPER_PASSWORD: ${{ secrets.GATEKEEPER_PASSWORD }}
|
||||
run: npx tsx scripts/smoke.ts || echo "⚠️ Smoke test failed"
|
||||
|
||||
# ──────────────────────────────────────────────────────────────────────────────
|
||||
# JOB 7: Notifications
|
||||
# JOB 6: Notifications
|
||||
# ──────────────────────────────────────────────────────────────────────────────
|
||||
notifications:
|
||||
name: 🔔 Notify
|
||||
@@ -428,44 +365,22 @@ jobs:
|
||||
shell: bash
|
||||
run: |
|
||||
DEPLOY="${{ needs.deploy.result }}"
|
||||
SMOKE="${{ needs.post_deploy_checks.result }}"
|
||||
PERF="${{ needs.post_deploy_checks.result }}"
|
||||
TARGET="${{ needs.prepare.outputs.target }}"
|
||||
VERSION="${{ needs.prepare.outputs.image_tag }}"
|
||||
URL="${{ needs.prepare.outputs.next_public_url }}"
|
||||
|
||||
# Gotify priority scale:
|
||||
# 1-3 = low (silent/info)
|
||||
# 4-5 = normal
|
||||
# 6-7 = high (warning)
|
||||
# 8-10 = critical (alarm)
|
||||
if [[ "$DEPLOY" != "success" ]]; then
|
||||
if [[ "$DEPLOY" == "success" ]]; then
|
||||
PRIORITY=2
|
||||
EMOJI="✅"
|
||||
STATUS_LINE="Deploy Success"
|
||||
else
|
||||
PRIORITY=10
|
||||
EMOJI="🚨"
|
||||
STATUS_LINE="DEPLOY FAILED"
|
||||
elif [[ "$SMOKE" != "success" ]]; then
|
||||
PRIORITY=8
|
||||
EMOJI="⚠️"
|
||||
STATUS_LINE="Smoke tests failed"
|
||||
elif [[ "$PERF" != "success" ]]; then
|
||||
PRIORITY=5
|
||||
EMOJI="📉"
|
||||
STATUS_LINE="Performance degraded"
|
||||
else
|
||||
PRIORITY=2
|
||||
EMOJI="✅"
|
||||
STATUS_LINE="All checks passed"
|
||||
fi
|
||||
|
||||
TITLE="$EMOJI e-tib.com $VERSION → $TARGET"
|
||||
MESSAGE="$STATUS_LINE
|
||||
Deploy: $DEPLOY | Smoke: $SMOKE | Perf: $PERF
|
||||
$URL"
|
||||
|
||||
if [[ -z "${{ secrets.GOTIFY_URL }}" || -z "${{ secrets.GOTIFY_TOKEN }}" ]]; then
|
||||
echo "⚠️ Gotify credentials missing, skipping notification."
|
||||
exit 0
|
||||
fi
|
||||
MESSAGE="$STATUS_LINE\n$URL"
|
||||
|
||||
curl -s -k -X POST "${{ secrets.GOTIFY_URL }}/message?token=${{ secrets.GOTIFY_TOKEN }}" \
|
||||
-F "title=$TITLE" \
|
||||
|
||||
@@ -16,38 +16,38 @@ jobs:
|
||||
container:
|
||||
image: catthehacker/ubuntu:act-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: pnpm/action-setup@v3
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
- name: Setup pnpm
|
||||
uses: pnpm/action-setup@v3
|
||||
with:
|
||||
version: 10
|
||||
- uses: actions/setup-node@v4
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 20
|
||||
- name: 📦 Cache node_modules
|
||||
uses: actions/cache@v4
|
||||
id: cache-deps
|
||||
with:
|
||||
path: node_modules
|
||||
key: pnpm-${{ hashFiles('pnpm-lock.yaml') }}
|
||||
|
||||
- name: 🔐 Registry Auth
|
||||
env:
|
||||
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
|
||||
GITEA_PAT: ${{ secrets.GITEA_PAT }}
|
||||
MINTEL_PRIVATE_TOKEN: ${{ secrets.MINTEL_PRIVATE_TOKEN }}
|
||||
run: bash scripts/registry-auth.sh
|
||||
- name: Install
|
||||
if: steps.cache-deps.outputs.cache-hit != 'true'
|
||||
|
||||
- name: Install dependencies
|
||||
run: 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
|
||||
|
||||
- name: 🚀 Run Smoke Test
|
||||
env:
|
||||
NEXT_PUBLIC_BASE_URL: ${{ env.TARGET_URL }}
|
||||
GATEKEEPER_PASSWORD: ${{ secrets.GATEKEEPER_PASSWORD }}
|
||||
PUPPETEER_EXECUTABLE_PATH: /usr/bin/chromium
|
||||
run: npx tsx scripts/smoke.ts
|
||||
|
||||
- name: 🔔 Notify
|
||||
if: always()
|
||||
run: |
|
||||
|
||||
2
.npmrc
2
.npmrc
@@ -1,4 +1,4 @@
|
||||
@mintel:registry=https://git.infra.mintel.me/api/packages/mmintel/npm/
|
||||
@mintel:registry=https://git.infra.mintel.me/api/packages/mmintel/npm
|
||||
//git.infra.mintel.me/api/packages/mmintel/npm/:_authToken=${NPM_TOKEN}
|
||||
//git.infra.mintel.me/api/packages/mmintel/npm/:always-auth=true
|
||||
//registry.infra.mintel.me/api/packages/mmintel/npm/:_authToken=${NPM_TOKEN}
|
||||
|
||||
@@ -1,89 +1,90 @@
|
||||
#!/bin/bash
|
||||
set -e
|
||||
|
||||
# Configuration
|
||||
# --- Configuration ---
|
||||
REGISTRY_URL="https://git.infra.mintel.me/api/packages/mmintel/npm/"
|
||||
REGISTRY_DOMAIN="git.infra.mintel.me"
|
||||
REGISTRY_PATH="/api/packages/mmintel/npm/"
|
||||
SCOPE="@mintel"
|
||||
TEST_PACKAGE="@mintel/mail"
|
||||
|
||||
echo "🔍 Starting functional registry discovery..."
|
||||
|
||||
# Discover token
|
||||
token=""
|
||||
token_src=""
|
||||
# Function to test a specific header
|
||||
test_auth() {
|
||||
local label=$1
|
||||
local header=$2
|
||||
local url=$3
|
||||
|
||||
echo "Trying $label..."
|
||||
# Capture both status code and body
|
||||
# Using -k because sometimes internal registries have cert issues in CI
|
||||
RESPONSE=$(curl -s -k -w "\n%{http_code}" -H "$header" "$url")
|
||||
STATUS=$(echo "$RESPONSE" | tail -n1)
|
||||
BODY=$(echo "$RESPONSE" | head -n -1)
|
||||
|
||||
echo "$label status: $STATUS"
|
||||
if [ "$STATUS" == "200" ]; then
|
||||
return 0
|
||||
else
|
||||
if [ -n "$BODY" ]; then
|
||||
echo "Response snippet: $(echo "$BODY" | head -c 200)"
|
||||
fi
|
||||
return 1
|
||||
fi
|
||||
}
|
||||
|
||||
# 1. Discover Tokens
|
||||
TOKEN=""
|
||||
TOKEN_SRC=""
|
||||
if [ -n "$NPM_TOKEN" ]; then
|
||||
token="$NPM_TOKEN"
|
||||
token_src="NPM_TOKEN"
|
||||
TOKEN=$NPM_TOKEN
|
||||
TOKEN_SRC="NPM_TOKEN"
|
||||
elif [ -n "$GITEA_PAT" ]; then
|
||||
token="$GITEA_PAT"
|
||||
token_src="GITEA_PAT"
|
||||
TOKEN=$GITEA_PAT
|
||||
TOKEN_SRC="GITEA_PAT"
|
||||
elif [ -n "$MINTEL_PRIVATE_TOKEN" ]; then
|
||||
token="$MINTEL_PRIVATE_TOKEN"
|
||||
token_src="MINTEL_PRIVATE_TOKEN"
|
||||
TOKEN=$MINTEL_PRIVATE_TOKEN
|
||||
TOKEN_SRC="MINTEL_PRIVATE_TOKEN"
|
||||
fi
|
||||
|
||||
if [ -z "$token" ]; then
|
||||
echo "❌ No registry token found (checked NPM_TOKEN, GITEA_PAT, MINTEL_PRIVATE_TOKEN)"
|
||||
if [ -z "$TOKEN" ]; then
|
||||
echo "❌ No token found (NPM_TOKEN, GITEA_PAT, MINTEL_PRIVATE_TOKEN are empty)."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "Found token from $token_src (length: ${#token})"
|
||||
echo "Found token from $TOKEN_SRC (length: ${#TOKEN})"
|
||||
|
||||
# Functional check against the registry
|
||||
test_pkg="@mintel/mail"
|
||||
test_url="${REGISTRY_URL}${test_pkg//\//@}"
|
||||
echo "Testing registry access for $test_pkg..."
|
||||
# 2. Functional Test
|
||||
# In Gitea, the NPM package URL for discovery can be different.
|
||||
# Try both @mintel/mail and mmintel/mail just in case.
|
||||
TEST_URL="${REGISTRY_URL}@mintel/mail"
|
||||
|
||||
# Try Bearer (standard for _authToken)
|
||||
echo "Trying Bearer auth..."
|
||||
status_bearer=$(curl -s -o /dev/null -w "%{http_code}" -H "Authorization: Bearer $token" "$test_url")
|
||||
echo "Bearer status: $status_bearer"
|
||||
|
||||
# Try Basic (standard for _auth or user:pass)
|
||||
echo "Trying Basic auth..."
|
||||
status_basic=$(curl -s -o /dev/null -w "%{http_code}" -H "Authorization: Basic $token" "$test_url")
|
||||
echo "Basic status: $status_basic"
|
||||
|
||||
# Try Token (Gitea specific sometimes)
|
||||
echo "Trying Token auth..."
|
||||
status_token=$(curl -s -o /dev/null -w "%{http_code}" -H "Authorization: token $token" "$test_url")
|
||||
echo "Token status: $status_token"
|
||||
|
||||
best_auth=""
|
||||
if [ "$status_bearer" -eq 200 ]; then
|
||||
echo "✅ Bearer auth worked!"
|
||||
best_auth="authToken"
|
||||
elif [ "$status_basic" -eq 200 ]; then
|
||||
echo "✅ Basic auth worked!"
|
||||
best_auth="auth"
|
||||
elif [ "$status_token" -eq 200 ]; then
|
||||
echo "✅ Token auth worked! (Using Bearer for pnpm)"
|
||||
best_auth="authToken"
|
||||
else
|
||||
echo "❌ All auth methods failed."
|
||||
# Check if it's base64 encoded user:pass and maybe we should decode it then use Bearer?
|
||||
# Or maybe it's a raw PAT and we should base64 it for Basic?
|
||||
AUTH_TYPE=""
|
||||
if test_auth "Bearer" "Authorization: Bearer $TOKEN" "$TEST_URL"; then
|
||||
AUTH_TYPE="Bearer"
|
||||
elif test_auth "Basic (raw)" "Authorization: Basic $(echo -n "token:$TOKEN" | base64)" "$TEST_URL"; then
|
||||
AUTH_TYPE="Basic"
|
||||
elif test_auth "Basic (token:pass)" "Authorization: Basic $(echo -n "$TOKEN:" | base64)" "$TEST_URL"; then
|
||||
AUTH_TYPE="Basic"
|
||||
elif test_auth "Gitea Token" "Authorization: token $TOKEN" "$TEST_URL"; then
|
||||
AUTH_TYPE="Token"
|
||||
fi
|
||||
|
||||
# Generate .npmrc
|
||||
if [ -z "$AUTH_TYPE" ]; then
|
||||
echo "❌ All auth methods failed for $TEST_URL."
|
||||
echo "Trying alternative URL format..."
|
||||
TEST_URL_ALT="${REGISTRY_URL}mmintel/mail"
|
||||
if test_auth "Bearer (Alt)" "Authorization: Bearer $TOKEN" "$TEST_URL_ALT"; then
|
||||
AUTH_TYPE="Bearer"
|
||||
fi
|
||||
fi
|
||||
|
||||
# 3. Generate .npmrc
|
||||
echo "Generating .npmrc..."
|
||||
{
|
||||
echo "always-auth=true"
|
||||
echo "@mintel:registry=https://git.infra.mintel.me/api/packages/mmintel/npm/"
|
||||
echo "//git.infra.mintel.me/api/packages/mmintel/npm/:_authToken=$TOKEN"
|
||||
# Fallback without trailing slash
|
||||
echo "//git.infra.mintel.me/api/packages/mmintel/npm:_authToken=$TOKEN"
|
||||
} > .npmrc
|
||||
|
||||
cat << EOF > .npmrc
|
||||
$SCOPE:registry=$REGISTRY_URL
|
||||
always-auth=true
|
||||
EOF
|
||||
|
||||
# If none worked, default to authToken as it's most common
|
||||
if [ -z "$best_auth" ] || [ "$best_auth" = "authToken" ]; then
|
||||
echo "//${REGISTRY_DOMAIN}${REGISTRY_PATH}:_authToken=$token" >> .npmrc
|
||||
echo "//${REGISTRY_DOMAIN}${REGISTRY_PATH%/}:_authToken=$token" >> .npmrc
|
||||
else
|
||||
echo "//${REGISTRY_DOMAIN}${REGISTRY_PATH}:_auth=$token" >> .npmrc
|
||||
echo "//${REGISTRY_DOMAIN}${REGISTRY_PATH%/}:_auth=$token" >> .npmrc
|
||||
fi
|
||||
|
||||
echo "progress=false" >> .npmrc
|
||||
echo "verify-store-integrity=false" >> .npmrc
|
||||
|
||||
echo "✅ .npmrc hardened and generated."
|
||||
echo "✅ .npmrc hardened and generated (Detected Type: ${AUTH_TYPE:-None})."
|
||||
|
||||
74
scripts/smoke.ts
Normal file
74
scripts/smoke.ts
Normal file
@@ -0,0 +1,74 @@
|
||||
import puppeteer from 'puppeteer';
|
||||
|
||||
const targetUrl = process.argv[2] || process.env.NEXT_PUBLIC_BASE_URL || 'http://localhost:3000';
|
||||
const gatekeeperPassword = process.env.GATEKEEPER_PASSWORD || 'etib2026';
|
||||
|
||||
async function run() {
|
||||
console.log(`🚀 Smoke Test: ${targetUrl}`);
|
||||
|
||||
const browser = await puppeteer.launch({
|
||||
headless: true,
|
||||
executablePath: process.env.PUPPETEER_EXECUTABLE_PATH || undefined,
|
||||
args: ['--no-sandbox', '--disable-setuid-sandbox'],
|
||||
});
|
||||
const page = await browser.newPage();
|
||||
|
||||
try {
|
||||
// 1. Login via Gatekeeper
|
||||
console.log('🔑 Authenticating...');
|
||||
await page.goto(targetUrl, { waitUntil: 'networkidle2' });
|
||||
|
||||
const isGatekeeper = await page.$('input[name="password"]');
|
||||
if (isGatekeeper) {
|
||||
await page.type('input[name="password"]', gatekeeperPassword);
|
||||
await Promise.all([
|
||||
page.waitForNavigation({ waitUntil: 'networkidle2' }),
|
||||
page.click('button[type="submit"]'),
|
||||
]);
|
||||
}
|
||||
|
||||
// 2. Check Key Pages
|
||||
const pages = ['/', '/de/kontakt', '/en/contact', '/de/blog'];
|
||||
for (const p of pages) {
|
||||
console.log(`📄 Checking ${p}...`);
|
||||
const res = await page.goto(`${targetUrl}${p}`, { waitUntil: 'domcontentloaded' });
|
||||
if (res?.status() !== 200) throw new Error(`Page ${p} returned ${res?.status()}`);
|
||||
|
||||
// Check for broken images
|
||||
const images = await page.$$eval('img', (imgs) => imgs.map((img) => img.src).filter(Boolean));
|
||||
console.log(` Checking ${images.length} images...`);
|
||||
|
||||
const brokenImages = await page.evaluate(async (urls) => {
|
||||
const broken: string[] = [];
|
||||
await Promise.all(
|
||||
urls.map(async (url) => {
|
||||
if (url.includes('openstreetmap.org') || url.includes('unpkg.com')) return;
|
||||
try {
|
||||
const res = await fetch(url, { method: 'HEAD' });
|
||||
if (res.status >= 400) {
|
||||
broken.push(`${url} (Status: ${res.status})`);
|
||||
}
|
||||
} catch (e) {
|
||||
broken.push(`${url} (Fetch failed)`);
|
||||
}
|
||||
}),
|
||||
);
|
||||
return broken;
|
||||
}, images);
|
||||
|
||||
if (brokenImages.length > 0) {
|
||||
console.warn(`⚠️ Found ${brokenImages.length} broken images on ${p}`);
|
||||
}
|
||||
}
|
||||
|
||||
console.log('✅ Smoke test passed!');
|
||||
process.exit(0);
|
||||
} catch (err: any) {
|
||||
console.error(`❌ Smoke test failed: ${err.message}`);
|
||||
process.exit(1);
|
||||
} finally {
|
||||
await browser.close();
|
||||
}
|
||||
}
|
||||
|
||||
run();
|
||||
Reference in New Issue
Block a user