Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions .github/actions/build/action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -3,17 +3,17 @@ 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: ""

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
Expand Down
10 changes: 6 additions & 4 deletions .github/workflows/_detect-code-changes.yml
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,8 @@ jobs:
code: ${{ steps.filter.outputs.code }}
steps:
- name: Checkout
uses: actions/[email protected]
if: github.event_name == 'pull_request'
uses: actions/[email protected]
with:
fetch-depth: 0

Expand All @@ -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"
Expand Down
21 changes: 18 additions & 3 deletions .github/workflows/build-and-test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand All @@ -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
Expand All @@ -38,3 +44,12 @@ jobs:

- name: Verify build
run: npm run build

- name: Upload dist artifact
if: ${{ inputs.upload-dist }}
uses: actions/[email protected]
with:
name: dist
path: dist
if-no-files-found: error
retention-days: 7
52 changes: 36 additions & 16 deletions .github/workflows/deploy.yml
Original file line number Diff line number Diff line change
@@ -1,26 +1,23 @@
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:
runs-on: ubuntu-latest
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

Expand All @@ -29,44 +26,67 @@ jobs:
permissions:
contents: read
uses: ./.github/workflows/build-and-test.yml
with:
upload-dist: ${{ github.ref_name == 'main' }}

call-lint:
needs: [guard]
permissions:
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/[email protected]
with:
node-version-file: ".nvmrc"

- name: Download dist artifact
uses: actions/[email protected]
with:
name: dist
path: dist

- name: Cache Netlify CLI
uses: actions/[email protected]
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 }}

# 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
Expand All @@ -77,7 +97,7 @@ jobs:
uses: actions/[email protected]

- name: Upload artifact
uses: actions/upload-pages-artifact@v4.0.0
uses: actions/upload-pages-artifact@v5.0.0
with:
path: ./dist

Expand Down
6 changes: 3 additions & 3 deletions .github/workflows/lint.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
10 changes: 6 additions & 4 deletions .github/workflows/security-audit.yml
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,10 @@ on:
branches:
- main
- dev
workflow_call:
workflow_dispatch:
schedule:
- cron: '0 0 * * 0'
- cron: '1 9 1 * *'

jobs:
changes:
Expand All @@ -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
Expand Down
8 changes: 4 additions & 4 deletions .github/workflows/sonarqube.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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 }}
16 changes: 9 additions & 7 deletions .github/workflows/sync-main-to-dev.yml
Original file line number Diff line number Diff line change
Expand Up @@ -6,22 +6,26 @@ on:
- main
workflow_dispatch:

concurrency:
group: sync-main-to-dev
cancel-in-progress: false

permissions:
contents: write
contents: read

jobs:
sync:
runs-on: ubuntu-latest
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 }}
Expand All @@ -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
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ dist-ssr

# Editor directories and files
.vscode/*
!.vscode/settings.json
!.vscode/extensions.json
.idea
.DS_Store
Expand Down
10 changes: 10 additions & 0 deletions .vscode/settings.json
Original file line number Diff line number Diff line change
@@ -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"
}
}
10 changes: 6 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 |
Expand All @@ -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

Expand Down Expand Up @@ -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

Expand Down
15 changes: 0 additions & 15 deletions index.html
Original file line number Diff line number Diff line change
Expand Up @@ -86,21 +86,6 @@
</script>
</head>
<body>
<!-- Netlify forms - hidden form for Netlify to detect (required for SPAs) -->
<form
name="contacto"
data-netlify="true"
netlify-honeypot="bot-field"
hidden
>
<input type="hidden" name="form-name" value="contacto" />
<input name="bot-field" />
<input type="text" name="nome" />
<input type="email" name="email" />
<input type="tel" name="telefone" />
<input type="text" name="assunto" />
<textarea name="mensagem"></textarea>
</form>
<div id="root"></div>
<script type="module" src="/src/main.jsx"></script>
</body>
Expand Down
Loading