Skip to content

Merge pull request #48 from dinooo13/claude/review-seo-geo-dJYXO #59

Merge pull request #48 from dinooo13/claude/review-seo-geo-dJYXO

Merge pull request #48 from dinooo13/claude/review-seo-geo-dJYXO #59

Workflow file for this run

name: ci-cd
# Unified pipeline:
# - lint, typecheck, build run in parallel on every push and PR.
# - build is gated by paths-filter; only runs when site files changed.
# - deploy-production runs on push to main when site changed; reuses build artifact.
# - deploy-preview runs on same-repo PRs when site changed; reuses build artifact.
# - cleanup-preview runs on PR close.
#
# preview.fmeyer.dev must be configured in the hosting panel to serve from
# ./previews/. Each PR is deployed to ./previews/pr-<n>/ and removed on close.
on:
push:
branches: [main]
pull_request:
types: [opened, synchronize, reopened, closed]
workflow_dispatch:
permissions:
contents: read
# Cancel superseded PR runs; never cancel branch pushes (avoid killing FTP mid-upload).
concurrency:
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.run_id }}
cancel-in-progress: ${{ github.event_name == 'pull_request' }}
jobs:
changes:
if: github.event.action != 'closed'
runs-on: ubuntu-24.04-arm
outputs:
site: ${{ steps.filter.outputs.site }}
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
fetch-depth: 0
- id: filter
shell: bash
env:
BASE_SHA: ${{ github.event.pull_request.base.sha || github.event.before }}
HEAD_SHA: ${{ github.event.pull_request.head.sha || github.sha }}
run: |
if [ "$GITHUB_EVENT_NAME" = "workflow_dispatch" ] \
|| [ -z "$BASE_SHA" ] \
|| [ "$BASE_SHA" = "0000000000000000000000000000000000000000" ]; then
echo "site=true" >> "$GITHUB_OUTPUT"
exit 0
fi
if git diff --name-only "$BASE_SHA" "$HEAD_SHA" | grep -qE '^(app/|content/|public/|nuxt\.config\.ts$|content\.config\.ts$|package\.json$|pnpm-lock\.yaml$|tsconfig\.json$|eslint\.config\.mjs$)'; then
echo "site=true" >> "$GITHUB_OUTPUT"
else
echo "site=false" >> "$GITHUB_OUTPUT"
fi
lint:
if: github.event.action != 'closed' && (needs.changes.outputs.site == 'true' || github.event_name == 'workflow_dispatch')
needs: changes
runs-on: ubuntu-24.04-arm
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- uses: ./.github/actions/setup
- name: Cache ESLint
uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
with:
path: .eslintcache
key: eslint-${{ runner.os }}-${{ runner.arch }}-${{ github.run_id }}
restore-keys: |
eslint-${{ runner.os }}-${{ runner.arch }}-
- run: pnpm run lint
typecheck:
if: github.event.action != 'closed' && (needs.changes.outputs.site == 'true' || github.event_name == 'workflow_dispatch')
needs: changes
runs-on: ubuntu-24.04-arm
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- uses: ./.github/actions/setup
- run: pnpm run typecheck
build:
if: github.event.action != 'closed' && (needs.changes.outputs.site == 'true' || github.event_name == 'workflow_dispatch')
needs: changes
runs-on: ubuntu-24.04-arm
env:
NUXT_TELEMETRY_DISABLED: '1'
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- uses: ./.github/actions/setup
- name: Cache Nuxt build
uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
with:
path: |
.nuxt
.nitro
node_modules/.cache
key: nuxt-build-${{ runner.os }}-${{ runner.arch }}-${{ github.event_name == 'pull_request' && 'preview' || 'production' }}-${{ hashFiles('pnpm-lock.yaml', 'app/**', 'content/**', 'public/**', 'nuxt.config.ts', 'content.config.ts', 'tsconfig.json') }}
restore-keys: |
nuxt-build-${{ runner.os }}-${{ runner.arch }}-${{ github.event_name == 'pull_request' && 'preview' || 'production' }}-
- name: Generate static site (production)
if: github.event_name != 'pull_request'
run: pnpm generate
- name: Generate static site (preview)
if: github.event_name == 'pull_request'
env:
NUXT_APP_BASE_URL: /pr-${{ github.event.pull_request.number }}/
NUXT_PUBLIC_SITE_URL: https://preview.fmeyer.dev
NUXT_PUBLIC_NOINDEX: 'true'
run: pnpm generate
- name: Upload site artifact
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: site-output
path: .output/public
# Required so .htaccess (and any other dotfile in the output) is
# packed into the artifact β€” upload-artifact strips hidden files
# by default.
include-hidden-files: true
retention-days: 1
compression-level: 0
if-no-files-found: error
deploy-production:
if: |
needs.changes.outputs.site == 'true' &&
((github.event_name == 'push' && github.ref == 'refs/heads/main') || github.event_name == 'workflow_dispatch')
needs: [changes, lint, typecheck, build]
runs-on: ubuntu-24.04-arm
concurrency:
group: deploy-production
environment:
name: production
steps:
- name: Download site artifact
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
with:
name: site-output
path: .output/public
- name: Install lftp
run: |
sudo apt-get install -y --no-install-recommends lftp \
|| (sudo apt-get update && sudo apt-get install -y --no-install-recommends lftp)
- name: Deploy via FTPS
env:
FTP_HOST: ${{ secrets.HOST }}
FTP_USER: ${{ secrets.USERNAME }}
FTP_PASS: ${{ secrets.PASSWORD }}
run: |
lftp -u "$FTP_USER,$FTP_PASS" "$FTP_HOST" <<'EOF'
set ftp:ssl-force true
set ftp:ssl-protect-data true
set ssl:verify-certificate true
set net:max-retries 5
set net:reconnect-interval-base 5
set net:reconnect-interval-multiplier 1.5
set net:timeout 30
set mirror:use-pget-n 4
set mirror:parallel-directories yes
mirror --reverse --delete --continue --parallel=4 --verbose ./.output/public/ ./httpdocs/
bye
EOF
deploy-preview:
if: |
github.event_name == 'pull_request' &&
github.event.action != 'closed' &&
github.event.pull_request.head.repo.full_name == github.repository &&
needs.changes.outputs.site == 'true'
needs: [changes, lint, typecheck, build]
runs-on: ubuntu-24.04-arm
concurrency:
group: preview-${{ github.event.pull_request.number }}
cancel-in-progress: true
environment:
name: preview
permissions:
contents: read
deployments: write
steps:
- name: Download site artifact
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
with:
name: site-output
path: .output/public
- name: Install lftp
run: |
sudo apt-get install -y --no-install-recommends lftp \
|| (sudo apt-get update && sudo apt-get install -y --no-install-recommends lftp)
- name: Deploy via FTPS
env:
FTP_HOST: ${{ secrets.HOST }}
FTP_USER: ${{ secrets.SFTP_PREVIEW_USER }}
FTP_PASS: ${{ secrets.SFTP_PREVIEW_PASSWORD }}
PR_NUMBER: ${{ github.event.pull_request.number }}
run: |
lftp -u "$FTP_USER,$FTP_PASS" "$FTP_HOST" <<EOF
set ftp:ssl-force true
set ftp:ssl-protect-data true
set ssl:verify-certificate true
set net:max-retries 5
set net:reconnect-interval-base 5
set net:reconnect-interval-multiplier 1.5
set net:timeout 30
set mirror:use-pget-n 4
set mirror:parallel-directories yes
mirror --reverse --delete --continue --parallel=4 --verbose ./.output/public/ ./previews/pr-${PR_NUMBER}/
bye
EOF
set-environment-url:
needs: [deploy-production, deploy-preview]
if: |
!cancelled() &&
(needs.deploy-production.result == 'success' || needs.deploy-preview.result == 'success')
runs-on: ubuntu-24.04-arm
permissions:
deployments: write
steps:
- name: Set deployment URL
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
with:
script: |
const isPreview = context.eventName === 'pull_request';
const env = isPreview ? 'preview' : 'production';
const sha = isPreview ? context.payload.pull_request.head.sha : context.sha;
const url = isPreview
? `https://preview.fmeyer.dev/pr-${context.payload.pull_request.number}/`
: 'https://fmeyer.dev';
const { owner, repo } = context.repo;
const deployments = await github.paginate(github.rest.repos.listDeployments, {
owner,
repo,
environment: env,
sha,
per_page: 100
});
deployments.sort((a, b) => b.id - a.id);
const deployment = deployments[0];
if (!deployment) {
core.setFailed(`No deployment found for SHA ${sha} in ${env} environment`);
return;
}
await github.rest.repos.createDeploymentStatus({
owner,
repo,
deployment_id: deployment.id,
state: 'success',
environment_url: url,
log_url: `${context.serverUrl}/${owner}/${repo}/actions/runs/${context.runId}`
});
cleanup-preview:
if: |
github.event_name == 'pull_request' &&
github.event.action == 'closed' &&
github.event.pull_request.head.repo.full_name == github.repository
runs-on: ubuntu-24.04-arm
concurrency:
group: preview-${{ github.event.pull_request.number }}
permissions:
deployments: write
steps:
- name: Install lftp
run: |
sudo apt-get install -y --no-install-recommends lftp \
|| (sudo apt-get update && sudo apt-get install -y --no-install-recommends lftp)
- name: Delete preview directory
env:
FTP_HOST: ${{ secrets.HOST }}
FTP_USER: ${{ secrets.SFTP_PREVIEW_USER }}
FTP_PASS: ${{ secrets.SFTP_PREVIEW_PASSWORD }}
PR_NUMBER: ${{ github.event.pull_request.number }}
run: |
lftp -u "$FTP_USER,$FTP_PASS" "$FTP_HOST" <<EOF
set ftp:ssl-force true
set ftp:ssl-protect-data true
set cmd:fail-exit no
rm -r ./previews/pr-${PR_NUMBER}
bye
EOF
- name: Deactivate preview deployments for this PR
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
with:
script: |
const prNumber = context.payload.pull_request.number;
const previewUrl = `https://preview.fmeyer.dev/pr-${prNumber}/`;
const { owner, repo } = context.repo;
const deployments = await github.paginate(github.rest.repos.listDeployments, {
owner,
repo,
environment: 'preview',
per_page: 100
});
for (const d of deployments) {
const { data: statuses } = await github.rest.repos.listDeploymentStatuses({
owner,
repo,
deployment_id: d.id,
per_page: 1
});
const latest = statuses[0];
if (latest?.environment_url !== previewUrl) continue;
await github.rest.repos.createDeploymentStatus({
owner,
repo,
deployment_id: d.id,
state: 'inactive'
});
await github.rest.repos.deleteDeployment({
owner,
repo,
deployment_id: d.id
});
}