From 5c8a879208f2882fb13ccaa7dd47176d5df87b08 Mon Sep 17 00:00:00 2001 From: "chrisert-sync-bot[bot]" Date: Thu, 4 Jun 2026 16:14:27 +0000 Subject: [PATCH 1/4] chore: sync from main From 6bf17fc7c46c15b52a1fee49b4aaa0487be51bb6 Mon Sep 17 00:00:00 2001 From: "chrisert-sync-bot[bot]" Date: Thu, 4 Jun 2026 16:33:14 +0000 Subject: [PATCH 2/4] chore: sync from main From a6779f074abb56bce99ba9cbe0e6b7d17e270792 Mon Sep 17 00:00:00 2001 From: "chrisert-sync-bot[bot]" Date: Thu, 20 Aug 2026 16:28:02 +0000 Subject: [PATCH 3/4] chore: sync from main From 8fe5fc29108c9dd2015ea966e0e73ce2f67d4aa3 Mon Sep 17 00:00:00 2001 From: Fernando Tona <105774270+fernandotonacoder@users.noreply.github.com> Date: Thu, 20 Aug 2026 23:39:38 +0100 Subject: [PATCH 4/4] quality: sonarqube cloud issues and overall CI/CD improvement (#100) * quality: address issues detected by sonarqube cloud * improve CI/CD and netlify features * update doc and .gitignore --- .github/actions/build/action.yml | 6 +-- .github/workflows/_detect-code-changes.yml | 10 +++-- .github/workflows/build-and-test.yml | 21 +++++++-- .github/workflows/deploy.yml | 52 +++++++++++++++------- .github/workflows/lint.yml | 6 +-- .github/workflows/security-audit.yml | 10 +++-- .github/workflows/sonarqube.yml | 8 ++-- .github/workflows/sync-main-to-dev.yml | 16 ++++--- .gitignore | 1 + .vscode/settings.json | 10 +++++ README.md | 10 +++-- index.html | 15 ------- public/__forms.html | 30 +++++++++++++ src/pages/ContactPage.jsx | 1 - src/pages/PortfolioPage.jsx | 3 ++ src/pages/PortfolioPage.test.jsx | 14 +++--- 16 files changed, 142 insertions(+), 71 deletions(-) create mode 100644 .vscode/settings.json create mode 100644 public/__forms.html diff --git a/.github/actions/build/action.yml b/.github/actions/build/action.yml index 727cad7..574333c 100644 --- a/.github/actions/build/action.yml +++ b/.github/actions/build/action.yml @@ -3,7 +3,7 @@ description: Install dependencies and build the production bundle. inputs: deploy-target: - description: "Value for DEPLOY_TARGET (e.g. 'github-pages'); empty builds for the root domain (Netlify)." + description: "Set to 'github-pages' to build with the /chrisert/ base path; leave empty for the root domain (Netlify)." required: false default: "" @@ -11,9 +11,9 @@ runs: using: composite steps: - name: Setup Node.js - uses: actions/setup-node@v6.3.0 + uses: actions/setup-node@v7.0.0 with: - node-version: "24" + node-version-file: ".nvmrc" cache: "npm" - name: Install dependencies diff --git a/.github/workflows/_detect-code-changes.yml b/.github/workflows/_detect-code-changes.yml index 4c4780b..d182beb 100644 --- a/.github/workflows/_detect-code-changes.yml +++ b/.github/workflows/_detect-code-changes.yml @@ -19,7 +19,8 @@ jobs: code: ${{ steps.filter.outputs.code }} steps: - name: Checkout - uses: actions/checkout@v6.0.2 + if: github.event_name == 'pull_request' + uses: actions/checkout@v7.0.1 with: fetch-depth: 0 @@ -30,9 +31,10 @@ jobs: echo "code=true" >> "$GITHUB_OUTPUT" exit 0 fi - if git diff --name-only \ - "${{ github.event.pull_request.base.sha }}...${{ github.event.pull_request.head.sha }}" \ - | grep -qvE '^(README\.md$|docs/)'; then + changed="$(git diff --name-only \ + "${{ github.event.pull_request.base.sha }}...${{ github.event.pull_request.head.sha }}")" \ + || { echo "::error::git diff failed - cannot determine changed files."; exit 1; } + if printf '%s\n' "$changed" | grep -qvE '^(README\.md$|docs/)'; then echo "code=true" >> "$GITHUB_OUTPUT" else echo "code=false" >> "$GITHUB_OUTPUT" diff --git a/.github/workflows/build-and-test.yml b/.github/workflows/build-and-test.yml index b346f41..ecd4dd6 100644 --- a/.github/workflows/build-and-test.yml +++ b/.github/workflows/build-and-test.yml @@ -6,6 +6,12 @@ on: - main - dev workflow_call: + inputs: + upload-dist: + description: "Upload dist/ as the 'dist' artifact so the caller can deploy it. The build here runs without DEPLOY_TARGET, i.e. Vite base '/' (Netlify)." + required: false + type: boolean + default: false jobs: changes: @@ -22,12 +28,12 @@ jobs: contents: read steps: - name: Checkout - uses: actions/checkout@v6.0.2 + uses: actions/checkout@v7.0.1 - name: Setup Node.js - uses: actions/setup-node@v6.3.0 + uses: actions/setup-node@v7.0.0 with: - node-version: "24" + node-version-file: ".nvmrc" cache: "npm" - name: Install dependencies @@ -38,3 +44,12 @@ jobs: - name: Verify build run: npm run build + + - name: Upload dist artifact + if: ${{ inputs.upload-dist }} + uses: actions/upload-artifact@v7.0.1 + with: + name: dist + path: dist + if-no-files-found: error + retention-days: 7 diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index 3452909..1741795 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -1,16 +1,11 @@ name: Deploy -# Manual deploy. The branch picked in the "Run workflow" dropdown decides -# the target: dev → GitHub Pages (staging), main → Netlify (production). -# The deployed code is always the selected branch, so it can never mismatch -# the environment. Lock this down further with environment deployment branch -# policies (Settings → Environments): netlify → main only, github-pages → dev only. on: workflow_dispatch: concurrency: group: deploy-${{ github.ref_name }} - cancel-in-progress: true + cancel-in-progress: false jobs: guard: @@ -18,9 +13,11 @@ jobs: permissions: {} steps: - name: Validate target branch + env: + REF_NAME: ${{ github.ref_name }} run: | - if [ "${{ github.ref_name }}" != "dev" ] && [ "${{ github.ref_name }}" != "main" ]; then - echo "::error::Deploy runs only from 'dev' (-> GitHub Pages) or 'main' (-> Netlify); got '${{ github.ref_name }}'." + if [ "$REF_NAME" != "dev" ] && [ "$REF_NAME" != "main" ]; then + echo "::error::Deploy runs only from 'dev' (-> GitHub Pages) or 'main' (-> Netlify); got '$REF_NAME'." exit 1 fi @@ -29,6 +26,8 @@ jobs: permissions: contents: read uses: ./.github/workflows/build-and-test.yml + with: + upload-dist: ${{ github.ref_name == 'main' }} call-lint: needs: [guard] @@ -36,23 +35,44 @@ jobs: contents: read uses: ./.github/workflows/lint.yml + call-audit: + needs: [guard] + permissions: + contents: read + uses: ./.github/workflows/security-audit.yml + # main → Netlify (production) netlify: if: github.ref_name == 'main' - needs: [call-test, call-lint] + needs: [call-test, call-lint, call-audit] runs-on: ubuntu-latest + timeout-minutes: 15 environment: netlify permissions: contents: read steps: - name: Checkout - uses: actions/checkout@v6.0.2 + uses: actions/checkout@v7.0.1 - - name: Build - uses: ./.github/actions/build + - name: Setup Node.js + uses: actions/setup-node@v7.0.0 + with: + node-version-file: ".nvmrc" + + - name: Download dist artifact + uses: actions/download-artifact@v8.0.1 + with: + name: dist + path: dist + + - name: Cache Netlify CLI + uses: actions/cache@v6.1.0 + with: + path: ~/.npm/_npx + key: npx-netlify-cli-27.1.2-${{ runner.os }} - name: Deploy to Netlify - run: npx netlify-cli deploy --dir=dist --prod + run: npx --ignore-scripts netlify-cli@27.1.2 deploy --dir=dist --prod env: NETLIFY_AUTH_TOKEN: ${{ secrets.NETLIFY_AUTH_TOKEN }} NETLIFY_SITE_ID: ${{ secrets.NETLIFY_SITE_ID }} @@ -60,13 +80,13 @@ jobs: # dev → GitHub Pages (staging) pages-build: if: github.ref_name == 'dev' - needs: [call-test, call-lint] + needs: [call-test, call-lint, call-audit] runs-on: ubuntu-latest permissions: contents: read steps: - name: Checkout - uses: actions/checkout@v6.0.2 + uses: actions/checkout@v7.0.1 - name: Build uses: ./.github/actions/build @@ -77,7 +97,7 @@ jobs: uses: actions/configure-pages@v6.0.0 - name: Upload artifact - uses: actions/upload-pages-artifact@v4.0.0 + uses: actions/upload-pages-artifact@v5.0.0 with: path: ./dist diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index 489407c..51f428f 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -21,12 +21,12 @@ jobs: contents: read steps: - name: Checkout - uses: actions/checkout@v6.0.2 + uses: actions/checkout@v7.0.1 - name: Setup Node.js - uses: actions/setup-node@v6.3.0 + uses: actions/setup-node@v7.0.0 with: - node-version: "24" + node-version-file: ".nvmrc" cache: "npm" - name: Install dependencies diff --git a/.github/workflows/security-audit.yml b/.github/workflows/security-audit.yml index 776809d..8699535 100644 --- a/.github/workflows/security-audit.yml +++ b/.github/workflows/security-audit.yml @@ -5,8 +5,10 @@ on: branches: - main - dev + workflow_call: + workflow_dispatch: schedule: - - cron: '0 0 * * 0' + - cron: '1 9 1 * *' jobs: changes: @@ -22,12 +24,12 @@ jobs: contents: read steps: - name: Checkout - uses: actions/checkout@v6.0.2 + uses: actions/checkout@v7.0.1 - name: Setup Node.js - uses: actions/setup-node@v6.3.0 + uses: actions/setup-node@v7.0.0 with: - node-version: "24" + node-version-file: ".nvmrc" cache: "npm" - name: Install dependencies diff --git a/.github/workflows/sonarqube.yml b/.github/workflows/sonarqube.yml index 791ff9c..5b72a56 100644 --- a/.github/workflows/sonarqube.yml +++ b/.github/workflows/sonarqube.yml @@ -19,14 +19,14 @@ jobs: permissions: contents: read steps: - - uses: actions/checkout@v6.0.2 + - uses: actions/checkout@v7.0.1 with: fetch-depth: 0 - name: Setup Node.js - uses: actions/setup-node@v6.3.0 + uses: actions/setup-node@v7.0.0 with: - node-version: "24" + node-version-file: ".nvmrc" cache: "npm" - name: Install dependencies @@ -36,7 +36,7 @@ jobs: run: npm run test:coverage - name: Official SonarQube Scan - uses: SonarSource/sonarqube-scan-action@v7.0.0 + uses: SonarSource/sonarqube-scan-action@v8.2.1 env: SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} SONAR_HOST_URL: ${{ secrets.SONAR_HOST_URL }} diff --git a/.github/workflows/sync-main-to-dev.yml b/.github/workflows/sync-main-to-dev.yml index 8bb36f6..74c2e6d 100644 --- a/.github/workflows/sync-main-to-dev.yml +++ b/.github/workflows/sync-main-to-dev.yml @@ -6,8 +6,12 @@ on: - main workflow_dispatch: +concurrency: + group: sync-main-to-dev + cancel-in-progress: false + permissions: - contents: write + contents: read jobs: sync: @@ -15,13 +19,13 @@ jobs: steps: - name: Generate GitHub App Token id: app-token - uses: actions/create-github-app-token@v1 + uses: actions/create-github-app-token@v3.2.0 with: app-id: ${{ secrets.SYNC_BOT_APP_ID }} private-key: ${{ secrets.SYNC_BOT_PRIVATE_KEY }} - name: Checkout - uses: actions/checkout@v6.0.2 + uses: actions/checkout@v7.0.1 with: fetch-depth: 0 token: ${{ steps.app-token.outputs.token }} @@ -40,14 +44,12 @@ jobs: if git rebase origin/main; then echo "Rebase successful" else - echo "Rebase failed due to conflicts - resetting dev to main" + echo "::error::Rebase of dev onto main hit conflicts - resolve manually (dev left untouched)." git rebase --abort - git reset --hard origin/main + exit 1 fi # Create empty commit to mark sync completion git commit --allow-empty -m "chore: sync from main" - # Push with force-with-lease (safe force push that fails if remote has new commits) - # This workflow uses a GitHub App token to push to the protected branch git push origin dev --force-with-lease diff --git a/.gitignore b/.gitignore index 6bbd5b9..d1fa275 100644 --- a/.gitignore +++ b/.gitignore @@ -14,6 +14,7 @@ dist-ssr # Editor directories and files .vscode/* +!.vscode/settings.json !.vscode/extensions.json .idea .DS_Store diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..ccd5a63 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,10 @@ +{ + // Composite actions are NOT workflows: without this, VS Code can fall back to + // a cached "GitHub Actions Workflow" language mode for action.yml and validate + // it against the workflow schema, demanding `on:`/`jobs:` and rejecting the + // valid `inputs:`/`runs:` keys. + "files.associations": { + "**/.github/actions/**/action.yml": "yaml", + "**/.github/actions/**/action.yaml": "yaml" + } +} diff --git a/README.md b/README.md index c85c6f8..b3b520b 100644 --- a/README.md +++ b/README.md @@ -188,6 +188,8 @@ A professional ETICS insulation firm needed a complete digital presence. No logo Netlify Forms handles contact submissions, eliminating backend complexity given there's no heavy business logic or database requirements involved. +> **Note:** Netlify detects forms by parsing the deployed HTML at build time, which a client-rendered SPA never exposes. `public/__forms.html` is that detection stub — a hidden static copy of the contact form's field names. **Do not delete it, and keep its field names in sync with `src/pages/ContactPage.jsx`:** submissions fail silently if it drifts or disappears, with no build error and no failing test. + ## 🚀 Tech Stack | Category | Technology | @@ -208,7 +210,7 @@ Netlify Forms handles contact submissions, eliminating backend complexity given ### Prerequisites -- Node.js (v24 or higher) +- Node.js (v24 or higher) — the version is pinned in `.nvmrc`, so `nvm use` or `fnm use` picks it up automatically, and CI reads the same file ### Available Scripts @@ -246,10 +248,10 @@ npm run preview - **Branch Protection:** Both `main` and `dev` are protected with linear history required; all changes must go through PRs - **Enforce Dev-to-Main:** A required check on `main` blocks any PR not originating from `dev`, ensuring all code goes through staging first - **Automated Testing:** Vitest + build verification runs on every PR to `dev` and `main` -- **Security:** Four complementary layers — `npm audit` (dependency vulnerabilities, runs weekly and on every PR), Dependabot alerts (continuous dependency monitoring at the repo level), CodeQL (static analysis for code-level vulnerabilities), and SonarQube (security ratings, hotspots, and vulnerability scanning on both `main` and `dev`) -- **Deployments:** Triggered manually only via Actions → Run workflow (`workflow_dispatch`) and gated by environment approval — no automatic deploy on push +- **Security:** Four complementary layers — `npm audit` (dependency vulnerabilities, runs monthly and on every PR), Dependabot alerts (continuous dependency monitoring at the repo level), CodeQL (static analysis for code-level vulnerabilities), and SonarQube (security ratings, hotspots, and vulnerability scanning on both `main` and `dev`) +- **Deployments:** Triggered manually only via Actions → Run workflow (`workflow_dispatch`) and gated by environment approval — no automatic deploy on push. Every deploy re-runs Build and Test, Lint and Security Audit before publishing; production ships the exact bundle those checks verified, downloaded as an artifact rather than rebuilt - **SonarQube:** SonarQube Server (self-hosted) scans on every push to `dev` and can be triggered manually — it runs tests with coverage before sending results; SonarQube Cloud automatically analyzes `main` and decorates PRs with quality feedback (SQ Server Community edition limitation: server does not support PR analysis) -- **Auto-Sync:** After each push to `main`, changes are automatically rebased onto `dev` to keep branches in sync +- **Auto-Sync:** After each push to `main`, changes are automatically rebased onto `dev` to keep branches in sync; if the rebase hits conflicts the workflow fails and leaves `dev` untouched, so nothing awaiting promotion is ever discarded ### Code Quality Strategy diff --git a/index.html b/index.html index 4579d63..a9168f2 100644 --- a/index.html +++ b/index.html @@ -86,21 +86,6 @@ - -
diff --git a/public/__forms.html b/public/__forms.html new file mode 100644 index 0000000..852848a --- /dev/null +++ b/public/__forms.html @@ -0,0 +1,30 @@ + + + + + + Netlify Forms + + + + + diff --git a/src/pages/ContactPage.jsx b/src/pages/ContactPage.jsx index 9431b8a..8d7abda 100644 --- a/src/pages/ContactPage.jsx +++ b/src/pages/ContactPage.jsx @@ -99,7 +99,6 @@ const ContactPage = () => { name="contacto" method="POST" data-netlify="true" - netlify-honeypot="bot-field" onSubmit={form.handleSubmit(onSubmit)} className="space-y-6" > diff --git a/src/pages/PortfolioPage.jsx b/src/pages/PortfolioPage.jsx index ee01b44..057d770 100644 --- a/src/pages/PortfolioPage.jsx +++ b/src/pages/PortfolioPage.jsx @@ -24,6 +24,8 @@ const PortfolioPage = () => { if (gallery.isFullscreen || !api) return; const handleKeyDown = (e) => { + if (e.target.closest?.("button, a, input, textarea, select, [tabindex]")) + return; if (e.key === "ArrowLeft") api.scrollPrev(); else if (e.key === "ArrowRight") api.scrollNext(); else if (e.key === "Enter") gallery.open(current); @@ -87,6 +89,7 @@ const PortfolioPage = () => { {portfolioImages.map((project, index) => (