From 0bcc780230a9ab80437cf10b0062d38b9636ac36 Mon Sep 17 00:00:00 2001 From: Marc Mintel Date: Tue, 12 May 2026 11:13:28 +0200 Subject: [PATCH] chore: unify pipeline with klz-2026 standards and harden registry auth --- .gitea/workflows/ci.yml | 6 +- .gitea/workflows/deploy.yml | 127 ++++++--------------------------- .gitea/workflows/qa.yml | 24 +++---- .npmrc | 2 +- scripts/registry-auth.sh | 137 ++++++++++++++++++------------------ scripts/smoke.ts | 74 +++++++++++++++++++ 6 files changed, 181 insertions(+), 189 deletions(-) create mode 100644 scripts/smoke.ts diff --git a/.gitea/workflows/ci.yml b/.gitea/workflows/ci.yml index e60002019..f44cb7b47 100644 --- a/.gitea/workflows/ci.yml +++ b/.gitea/workflows/ci.yml @@ -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 diff --git a/.gitea/workflows/deploy.yml b/.gitea/workflows/deploy.yml index f1630cb93..ad3a1ce0a 100644 --- a/.gitea/workflows/deploy.yml +++ b/.gitea/workflows/deploy.yml @@ -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" \ diff --git a/.gitea/workflows/qa.yml b/.gitea/workflows/qa.yml index 37044420c..11e4fa1d4 100644 --- a/.gitea/workflows/qa.yml +++ b/.gitea/workflows/qa.yml @@ -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: | diff --git a/.npmrc b/.npmrc index f7d706b20..d829d802d 100644 --- a/.npmrc +++ b/.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} diff --git a/scripts/registry-auth.sh b/scripts/registry-auth.sh index 3ef18c216..272740a85 100755 --- a/scripts/registry-auth.sh +++ b/scripts/registry-auth.sh @@ -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})." diff --git a/scripts/smoke.ts b/scripts/smoke.ts new file mode 100644 index 000000000..3dabca19e --- /dev/null +++ b/scripts/smoke.ts @@ -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();