Update readme, fix docker-compose typo, add pipelines
This commit is contained in:
159
.woodpecker/build.yml
Normal file
159
.woodpecker/build.yml
Normal file
@@ -0,0 +1,159 @@
|
|||||||
|
# Deploy: build image, push to registry, trigger Portainer stack redeploy.
|
||||||
|
# Runs on push/tag/manual to main only, after ci workflow succeeds.
|
||||||
|
when:
|
||||||
|
- branch: main
|
||||||
|
event: [push, tag, manual]
|
||||||
|
- event: deployment
|
||||||
|
evaluate: 'CI_PIPELINE_DEPLOY_TARGET == "production"'
|
||||||
|
|
||||||
|
depends_on:
|
||||||
|
- ci
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Site build
|
||||||
|
image: node:22-alpine
|
||||||
|
commands:
|
||||||
|
- corepack enable
|
||||||
|
- corepack prepare pnpm@10.29.2 --activate
|
||||||
|
- pnpm install --frozen-lockfile
|
||||||
|
- pnpm build
|
||||||
|
|
||||||
|
- name: Docker image build
|
||||||
|
image: docker:latest
|
||||||
|
depends_on:
|
||||||
|
- Site build
|
||||||
|
environment:
|
||||||
|
REGISTRY_REPO: git.mifi.dev/mifi-holdings/landing
|
||||||
|
DOCKER_API_VERSION: '1.43'
|
||||||
|
DOCKER_BUILDKIT: '1'
|
||||||
|
volumes:
|
||||||
|
- /var/run/docker.sock:/var/run/docker.sock
|
||||||
|
commands:
|
||||||
|
- set -e
|
||||||
|
- echo "=== Building Docker image (BuildKit) ==="
|
||||||
|
- 'echo "Commit SHA: ${CI_COMMIT_SHA:0:8}"'
|
||||||
|
- 'echo "Registry repo: $REGISTRY_REPO"'
|
||||||
|
- |
|
||||||
|
build() {
|
||||||
|
docker build \
|
||||||
|
--progress=plain \
|
||||||
|
--platform=linux/amd64 \
|
||||||
|
--tag $REGISTRY_REPO:${CI_COMMIT_SHA} \
|
||||||
|
--tag $REGISTRY_REPO:latest \
|
||||||
|
--label "git.commit=${CI_COMMIT_SHA}" \
|
||||||
|
--label "git.branch=${CI_COMMIT_BRANCH}" \
|
||||||
|
.
|
||||||
|
}
|
||||||
|
for attempt in 1 2 3; do
|
||||||
|
echo "Build attempt $attempt/3"
|
||||||
|
if build; then
|
||||||
|
echo "✓ Docker image built successfully"
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
echo "Build attempt $attempt failed, retrying in 30s..."
|
||||||
|
sleep 30
|
||||||
|
done
|
||||||
|
echo "All build attempts failed"
|
||||||
|
exit 1
|
||||||
|
|
||||||
|
- name: Send Docker Image Build Status Notification (success)
|
||||||
|
image: curlimages/curl
|
||||||
|
environment:
|
||||||
|
MATTERMOST_BOT_ACCESS_TOKEN:
|
||||||
|
from_secret: mattermost_bot_access_token
|
||||||
|
MATTERMOST_CHANNEL_ID:
|
||||||
|
from_secret: mattermost_pushes_channel_id
|
||||||
|
MATTERMOST_POST_API_URL:
|
||||||
|
from_secret: mattermost_post_api_url
|
||||||
|
commands:
|
||||||
|
- |
|
||||||
|
BODY=$(printf '{"channel_id":"%s","message":"[%s - Build #%s] Docker image build success 🎉"}' "$MATTERMOST_CHANNEL_ID" "$CI_REPO" "$CI_PIPELINE_NUMBER")
|
||||||
|
curl -sS -X POST -H "Content-Type: application/json" -d "$BODY" -H "Authorization: Bearer $MATTERMOST_BOT_ACCESS_TOKEN" "$MATTERMOST_POST_API_URL"
|
||||||
|
depends_on:
|
||||||
|
- Site build
|
||||||
|
- Docker image build
|
||||||
|
when:
|
||||||
|
- status: [success]
|
||||||
|
|
||||||
|
- name: Send Docker Image Build Status Notification (failure)
|
||||||
|
image: curlimages/curl
|
||||||
|
environment:
|
||||||
|
MATTERMOST_BOT_ACCESS_TOKEN:
|
||||||
|
from_secret: mattermost_bot_access_token
|
||||||
|
MATTERMOST_CHANNEL_ID:
|
||||||
|
from_secret: mattermost_pushes_channel_id
|
||||||
|
MATTERMOST_POST_API_URL:
|
||||||
|
from_secret: mattermost_post_api_url
|
||||||
|
commands:
|
||||||
|
- |
|
||||||
|
BODY=$(printf '{"channel_id":"%s","message":"[%s - Build #%s] Docker image build failure 💩"}' "$MATTERMOST_CHANNEL_ID" "$CI_REPO" "$CI_PIPELINE_NUMBER")
|
||||||
|
curl -sS -X POST -H "Content-Type: application/json" -d "$BODY" -H "Authorization: Bearer $MATTERMOST_BOT_ACCESS_TOKEN" "$MATTERMOST_POST_API_URL"
|
||||||
|
depends_on:
|
||||||
|
- Site build
|
||||||
|
- Docker image build
|
||||||
|
when:
|
||||||
|
- status: [failure]
|
||||||
|
|
||||||
|
- name: Push to registry
|
||||||
|
image: docker:latest
|
||||||
|
environment:
|
||||||
|
DOCKER_API_VERSION: '1.43'
|
||||||
|
REGISTRY_URL: git.mifi.dev
|
||||||
|
REGISTRY_REPO: git.mifi.dev/mifi-holdings/landing
|
||||||
|
REGISTRY_USERNAME:
|
||||||
|
from_secret: gitea_registry_username
|
||||||
|
REGISTRY_PASSWORD:
|
||||||
|
from_secret: gitea_package_token
|
||||||
|
volumes:
|
||||||
|
- /var/run/docker.sock:/var/run/docker.sock
|
||||||
|
commands:
|
||||||
|
- set -e
|
||||||
|
- echo "=== Pushing to registry ==="
|
||||||
|
- 'echo "Registry: $REGISTRY_URL"'
|
||||||
|
- 'echo "Repository: $REGISTRY_REPO"'
|
||||||
|
- |
|
||||||
|
echo "$REGISTRY_PASSWORD" | docker login "$REGISTRY_URL" \
|
||||||
|
-u "$REGISTRY_USERNAME" \
|
||||||
|
--password-stdin
|
||||||
|
- docker push $REGISTRY_REPO:${CI_COMMIT_SHA}
|
||||||
|
- docker push $REGISTRY_REPO:latest
|
||||||
|
- echo "✓ Images pushed successfully"
|
||||||
|
depends_on:
|
||||||
|
- Site build
|
||||||
|
- Docker image build
|
||||||
|
|
||||||
|
- name: Send Push to Registry Status Notification (success)
|
||||||
|
image: curlimages/curl
|
||||||
|
environment:
|
||||||
|
MATTERMOST_BOT_ACCESS_TOKEN:
|
||||||
|
from_secret: mattermost_bot_access_token
|
||||||
|
MATTERMOST_CHANNEL_ID:
|
||||||
|
from_secret: mattermost_pushes_channel_id
|
||||||
|
MATTERMOST_POST_API_URL:
|
||||||
|
from_secret: mattermost_post_api_url
|
||||||
|
commands:
|
||||||
|
- |
|
||||||
|
BODY=$(printf '{"channel_id":"%s","message":"[%s - Build #%s] Push to registry success 🎉"}' "$MATTERMOST_CHANNEL_ID" "$CI_REPO" "$CI_PIPELINE_NUMBER")
|
||||||
|
curl -sS -X POST -H "Content-Type: application/json" -d "$BODY" -H "Authorization: Bearer $MATTERMOST_BOT_ACCESS_TOKEN" "$MATTERMOST_POST_API_URL"
|
||||||
|
depends_on:
|
||||||
|
- Push to registry
|
||||||
|
when:
|
||||||
|
- status: [success]
|
||||||
|
|
||||||
|
- name: Send Push to Registry Status Notification (failure)
|
||||||
|
image: curlimages/curl
|
||||||
|
environment:
|
||||||
|
MATTERMOST_BOT_ACCESS_TOKEN:
|
||||||
|
from_secret: mattermost_bot_access_token
|
||||||
|
MATTERMOST_CHANNEL_ID:
|
||||||
|
from_secret: mattermost_pushes_channel_id
|
||||||
|
MATTERMOST_POST_API_URL:
|
||||||
|
from_secret: mattermost_post_api_url
|
||||||
|
commands:
|
||||||
|
- |
|
||||||
|
BODY=$(printf '{"channel_id":"%s","message":"[%s - Build #%s] Push to registry failure 💩"}' "$MATTERMOST_CHANNEL_ID" "$CI_REPO" "$CI_PIPELINE_NUMBER")
|
||||||
|
curl -sS -X POST -H "Content-Type: application/json" -d "$BODY" -H "Authorization: Bearer $MATTERMOST_BOT_ACCESS_TOKEN" "$MATTERMOST_POST_API_URL"
|
||||||
|
depends_on:
|
||||||
|
- Push to registry
|
||||||
|
when:
|
||||||
|
- status: [failure]
|
||||||
119
.woodpecker/ci.yml
Normal file
119
.woodpecker/ci.yml
Normal file
@@ -0,0 +1,119 @@
|
|||||||
|
# CI: lint and format check. Runs on every PR and every push to main.
|
||||||
|
when:
|
||||||
|
- event: pull_request
|
||||||
|
- branch: main
|
||||||
|
event: push
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: install
|
||||||
|
image: node:22-alpine
|
||||||
|
commands:
|
||||||
|
- corepack enable
|
||||||
|
- corepack prepare pnpm@10.29.2 --activate
|
||||||
|
- pnpm install --frozen-lockfile
|
||||||
|
|
||||||
|
- name: Prettier Format Check
|
||||||
|
image: node:22-alpine
|
||||||
|
commands:
|
||||||
|
- corepack enable
|
||||||
|
- corepack prepare pnpm@10.29.2 --activate
|
||||||
|
- pnpm install --frozen-lockfile
|
||||||
|
- pnpm format:check
|
||||||
|
depends_on:
|
||||||
|
- install
|
||||||
|
|
||||||
|
- name: Send Prettier Format Check Status Notification (failure)
|
||||||
|
image: curlimages/curl
|
||||||
|
environment:
|
||||||
|
MATTERMOST_BOT_ACCESS_TOKEN:
|
||||||
|
from_secret: mattermost_bot_access_token
|
||||||
|
MATTERMOST_CHANNEL_ID:
|
||||||
|
from_secret: mattermost_tests_channel_id
|
||||||
|
MATTERMOST_POST_API_URL:
|
||||||
|
from_secret: mattermost_post_api_url
|
||||||
|
commands:
|
||||||
|
- |
|
||||||
|
BODY=$(printf '{"channel_id":"%s","message":"[%s - Build #%s] Prettier Format Check failure 💩"}' "$MATTERMOST_CHANNEL_ID" "$CI_REPO" "$CI_PIPELINE_NUMBER")
|
||||||
|
curl -sS -X POST -H "Content-Type: application/json" -d "$BODY" -H "Authorization: Bearer $MATTERMOST_BOT_ACCESS_TOKEN" $MATTERMOST_POST_API_URL
|
||||||
|
depends_on:
|
||||||
|
- Prettier Format Check
|
||||||
|
when:
|
||||||
|
- status: [failure]
|
||||||
|
|
||||||
|
- name: Lint Check
|
||||||
|
image: node:22-alpine
|
||||||
|
commands:
|
||||||
|
- corepack enable
|
||||||
|
- corepack prepare pnpm@10.29.2 --activate
|
||||||
|
- pnpm install --frozen-lockfile
|
||||||
|
- pnpm lint
|
||||||
|
depends_on:
|
||||||
|
- install
|
||||||
|
|
||||||
|
- name: Send Lint Status Notification (failure)
|
||||||
|
image: curlimages/curl
|
||||||
|
environment:
|
||||||
|
MATTERMOST_BOT_ACCESS_TOKEN:
|
||||||
|
from_secret: mattermost_bot_access_token
|
||||||
|
MATTERMOST_CHANNEL_ID:
|
||||||
|
from_secret: mattermost_tests_channel_id
|
||||||
|
MATTERMOST_POST_API_URL:
|
||||||
|
from_secret: mattermost_post_api_url
|
||||||
|
commands:
|
||||||
|
- |
|
||||||
|
BODY=$(printf '{"channel_id":"%s","message":"[%s - Build #%s] Lint failure 💩"}' "$MATTERMOST_CHANNEL_ID" "$CI_REPO" "$CI_PIPELINE_NUMBER")
|
||||||
|
curl -sS -X POST -H "Content-Type: application/json" -d "$BODY" -H "Authorization: Bearer $MATTERMOST_BOT_ACCESS_TOKEN" $MATTERMOST_POST_API_URL
|
||||||
|
depends_on:
|
||||||
|
- Lint Check
|
||||||
|
when:
|
||||||
|
- status: [failure]
|
||||||
|
|
||||||
|
|
||||||
|
- name: Build
|
||||||
|
image: node:22-alpine
|
||||||
|
commands:
|
||||||
|
- corepack enable
|
||||||
|
- corepack prepare pnpm@10.29.2 --activate
|
||||||
|
- pnpm install --frozen-lockfile
|
||||||
|
- pnpm build
|
||||||
|
depends_on:
|
||||||
|
- install
|
||||||
|
|
||||||
|
- name: Send Build Status Notification (failure)
|
||||||
|
image: curlimages/curl
|
||||||
|
environment:
|
||||||
|
MATTERMOST_BOT_ACCESS_TOKEN:
|
||||||
|
from_secret: mattermost_bot_access_token
|
||||||
|
MATTERMOST_CHANNEL_ID:
|
||||||
|
from_secret: mattermost_tests_channel_id
|
||||||
|
MATTERMOST_POST_API_URL:
|
||||||
|
from_secret: mattermost_post_api_url
|
||||||
|
commands:
|
||||||
|
- |
|
||||||
|
BODY=$(printf '{"channel_id":"%s","message":"[%s - Build #%s] Build failure 💩"}' "$MATTERMOST_CHANNEL_ID" "$CI_REPO" "$CI_PIPELINE_NUMBER")
|
||||||
|
curl -sS -X POST -H "Content-Type: application/json" -d "$BODY" -H "Authorization: Bearer $MATTERMOST_BOT_ACCESS_TOKEN" $MATTERMOST_POST_API_URL
|
||||||
|
depends_on:
|
||||||
|
- Build
|
||||||
|
when:
|
||||||
|
- status: [failure]
|
||||||
|
|
||||||
|
- name: Send CI Pipeline Status Notification (success)
|
||||||
|
image: curlimages/curl
|
||||||
|
environment:
|
||||||
|
MATTERMOST_BOT_ACCESS_TOKEN:
|
||||||
|
from_secret: mattermost_bot_access_token
|
||||||
|
MATTERMOST_CHANNEL_ID:
|
||||||
|
from_secret: mattermost_tests_channel_id
|
||||||
|
MATTERMOST_POST_API_URL:
|
||||||
|
from_secret: mattermost_post_api_url
|
||||||
|
commands:
|
||||||
|
- |
|
||||||
|
BODY=$(printf '{"channel_id":"%s","message":"[%s - Build #%s] CI pipeline success 🎉"}' "$MATTERMOST_CHANNEL_ID" "$CI_REPO" "$CI_PIPELINE_NUMBER")
|
||||||
|
curl -sS -X POST -H "Content-Type: application/json" -d "$BODY" -H "Authorization: Bearer $MATTERMOST_BOT_ACCESS_TOKEN" $MATTERMOST_POST_API_URL
|
||||||
|
depends_on:
|
||||||
|
- install
|
||||||
|
- Prettier Format Check
|
||||||
|
- Lint Check
|
||||||
|
- Build
|
||||||
|
when:
|
||||||
|
- status: [success]
|
||||||
70
.woodpecker/deploy.yml
Normal file
70
.woodpecker/deploy.yml
Normal file
@@ -0,0 +1,70 @@
|
|||||||
|
# Deploy: build image, push to registry, trigger Portainer stack redeploy.
|
||||||
|
# Runs on push/tag/manual to main only, after ci workflow succeeds.
|
||||||
|
skip_clone: true
|
||||||
|
# Use writable workspace when clone is skipped (no root clone step to create /woodpecker/src)
|
||||||
|
workspace:
|
||||||
|
base: /tmp
|
||||||
|
path: deploy
|
||||||
|
when:
|
||||||
|
- branch: main
|
||||||
|
event: [push, tag, manual]
|
||||||
|
- event: deployment
|
||||||
|
evaluate: 'CI_PIPELINE_DEPLOY_TARGET == "production"'
|
||||||
|
|
||||||
|
depends_on:
|
||||||
|
- ci
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Trigger Portainer stack redeploy
|
||||||
|
image: curlimages/curl:latest
|
||||||
|
environment:
|
||||||
|
PORTAINER_WEBHOOK_URL:
|
||||||
|
from_secret: portainer_webhook_url
|
||||||
|
commands:
|
||||||
|
- set -e
|
||||||
|
- echo "=== Triggering Portainer stack redeploy ==="
|
||||||
|
- |
|
||||||
|
resp=$(curl -s -w "\n%{http_code}" -X POST "$PORTAINER_WEBHOOK_URL")
|
||||||
|
body=$(echo "$resp" | head -n -1)
|
||||||
|
code=$(echo "$resp" | tail -n 1)
|
||||||
|
if [ "$code" != "200" ] && [ "$code" != "204" ]; then
|
||||||
|
echo "Webhook failed (HTTP $code): $body"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
echo "✓ Portainer redeploy triggered (HTTP $code)"
|
||||||
|
|
||||||
|
- name: Send Deploy Status Notification (success)
|
||||||
|
image: curlimages/curl
|
||||||
|
environment:
|
||||||
|
MATTERMOST_BOT_ACCESS_TOKEN:
|
||||||
|
from_secret: mattermost_bot_access_token
|
||||||
|
MATTERMOST_CHANNEL_ID:
|
||||||
|
from_secret: mattermost_pushes_channel_id
|
||||||
|
MATTERMOST_POST_API_URL:
|
||||||
|
from_secret: mattermost_post_api_url
|
||||||
|
commands:
|
||||||
|
- |
|
||||||
|
BODY=$(printf '{"channel_id":"%s","message":"[%s - Build #%s] Production Deploy success 🎉"}' "$MATTERMOST_CHANNEL_ID" "$CI_REPO" "$CI_PIPELINE_NUMBER")
|
||||||
|
curl -sS -X POST -H "Content-Type: application/json" -d "$BODY" -H "Authorization: Bearer $MATTERMOST_BOT_ACCESS_TOKEN" "$MATTERMOST_POST_API_URL"
|
||||||
|
depends_on:
|
||||||
|
- Trigger Portainer stack redeploy
|
||||||
|
when:
|
||||||
|
- status: [success]
|
||||||
|
|
||||||
|
- name: Send Deploy Status Notification (failure)
|
||||||
|
image: curlimages/curl
|
||||||
|
environment:
|
||||||
|
MATTERMOST_BOT_ACCESS_TOKEN:
|
||||||
|
from_secret: mattermost_bot_access_token
|
||||||
|
MATTERMOST_CHANNEL_ID:
|
||||||
|
from_secret: mattermost_pushes_channel_id
|
||||||
|
MATTERMOST_POST_API_URL:
|
||||||
|
from_secret: mattermost_post_api_url
|
||||||
|
commands:
|
||||||
|
- |
|
||||||
|
BODY=$(printf '{"channel_id":"%s","message":"[%s - Build #%s] Production Deploy failure 💩"}' "$MATTERMOST_CHANNEL_ID" "$CI_REPO" "$CI_PIPELINE_NUMBER")
|
||||||
|
curl -sS -X POST -H "Content-Type: application/json" -d "$BODY" -H "Authorization: Bearer $MATTERMOST_BOT_ACCESS_TOKEN" "$MATTERMOST_POST_API_URL"
|
||||||
|
depends_on:
|
||||||
|
- Trigger Portainer stack redeploy
|
||||||
|
when:
|
||||||
|
- status: [failure]
|
||||||
69
README.md
69
README.md
@@ -1,6 +1,6 @@
|
|||||||
# mifi.holdings — Landing Page
|
# mifi.holdings — Landing Page
|
||||||
|
|
||||||
Static landing site for **mifi.holdings** (and www). Plain HTML/CSS, no framework; served by **nginx** in Docker behind **Traefik**, with CI/CD via **Woodpecker** and deployment via **Portainer**.
|
Static landing site for **mifi.holdings** (and www). Plain HTML/CSS/JS source; a **build step** produces minified assets and inlines critical CSS. Served by **nginx** in Docker behind **Traefik**, with CI/CD via **Woodpecker** and deployment via **Portainer**.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -14,6 +14,7 @@ Static landing site for **mifi.holdings** (and www). Plain HTML/CSS, no framewor
|
|||||||
| **CI/CD** | Woodpecker (ci → build → deploy) |
|
| **CI/CD** | Woodpecker (ci → build → deploy) |
|
||||||
| **Registry** | `git.mifi.dev/mifi-holdings/landing` |
|
| **Registry** | `git.mifi.dev/mifi-holdings/landing` |
|
||||||
| **Package manager** | pnpm |
|
| **Package manager** | pnpm |
|
||||||
|
| **Build output** | `dist/` (gitignored) |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -28,11 +29,11 @@ Static landing site for **mifi.holdings** (and www). Plain HTML/CSS, no framewor
|
|||||||
│ Docker container │ mifi-holdings-landing
|
│ Docker container │ mifi-holdings-landing
|
||||||
│ nginx:alpine │ port 80
|
│ nginx:alpine │ port 80
|
||||||
│ /usr/share/ │
|
│ /usr/share/ │
|
||||||
│ nginx/html ← │ static files from image
|
│ nginx/html ← │ built output from dist/
|
||||||
└─────────────────┘
|
└─────────────────┘
|
||||||
```
|
```
|
||||||
|
|
||||||
- **Build**: Docker image = `nginx:alpine` + project `nginx/conf.d/` + `src/` copied into `/usr/share/nginx/html`.
|
- **Build**: `pnpm build` copies `src/` → `dist/`, minifies JS/CSS, inlines critical CSS (Critters). Docker image = `nginx:alpine` + `nginx/conf.d/` + **`dist/`** into `/usr/share/nginx/html`.
|
||||||
- **Run**: Single service on external network `marina-net`; Traefik routes `mifi.holdings` and `www.mifi.holdings` to this container (HTTPS, `security-prison@file` middleware).
|
- **Run**: Single service on external network `marina-net`; Traefik routes `mifi.holdings` and `www.mifi.holdings` to this container (HTTPS, `security-prison@file` middleware).
|
||||||
|
|
||||||
---
|
---
|
||||||
@@ -41,52 +42,70 @@ Static landing site for **mifi.holdings** (and www). Plain HTML/CSS, no framewor
|
|||||||
|
|
||||||
```
|
```
|
||||||
.
|
.
|
||||||
├── src/ # Static site (served as-is)
|
├── src/ # Source (HTML, CSS, JS)
|
||||||
│ ├── index.html
|
│ ├── index.html
|
||||||
│ └── css/
|
│ └── assets/
|
||||||
│ └── style.css
|
│ ├── css/style.css
|
||||||
|
│ ├── js/ga-init.js
|
||||||
|
│ └── images/ # favicon, etc.
|
||||||
|
├── scripts/
|
||||||
|
│ └── build.js # Build: copy, minify, inline critical CSS
|
||||||
|
├── dist/ # Build output (gitignored)
|
||||||
├── nginx/
|
├── nginx/
|
||||||
│ └── conf.d/
|
│ └── conf.d/
|
||||||
│ └── default.conf # nginx server config (cache rules, SPA fallback)
|
│ └── default.conf # nginx server config (cache rules, SPA fallback)
|
||||||
├── .woodpecker/
|
├── .woodpecker/
|
||||||
│ ├── ci.yml # Lint + format check (PR + push to main)
|
│ ├── ci.yml # Lint, format, build check (PR + push to main)
|
||||||
│ ├── build.yml # Build image, push to registry (main)
|
│ ├── build.yml # Site build → Docker image → push (main)
|
||||||
│ └── deploy.yml # Trigger Portainer redeploy (main)
|
│ └── deploy.yml # Trigger Portainer redeploy (main)
|
||||||
├── docker-compose.yml # Service + Traefik labels for production
|
├── docker-compose.yml # Service + Traefik labels for production
|
||||||
├── Dockerfile # nginx + config + src
|
├── Dockerfile # nginx + config + dist (not src)
|
||||||
├── package.json # pnpm scripts: format, lint, docker build/push
|
├── package.json # pnpm scripts: build, format, lint, docker
|
||||||
└── README.md
|
└── README.md
|
||||||
```
|
```
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
## Build (pnpm build)
|
||||||
|
|
||||||
|
- **Output**: `dist/` (gitignored). Do not edit `dist/`; it is generated.
|
||||||
|
- **Steps** (see `scripts/build.js`):
|
||||||
|
1. Clean and copy `src/` → `dist/`.
|
||||||
|
2. Minify all `.js` (terser) and `.css` (clean-css).
|
||||||
|
3. Inline critical CSS with **Critters** (lazy-loads the rest; no browser required, so it works in CI).
|
||||||
|
- **When**: Run before `docker build`. CI and the build pipeline both run `pnpm build` before packaging.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## Tech stack
|
## Tech stack
|
||||||
|
|
||||||
- **Frontend**: Static HTML + CSS only (no JS build step).
|
- **Frontend**: Static HTML + CSS + JS in `src/`; production build minifies and inlines critical CSS.
|
||||||
- **Server**: nginx (Alpine) in Docker.
|
- **Server**: nginx (Alpine) in Docker.
|
||||||
- **Tooling**: **pnpm** (format with Prettier, lint with yamllint for `.woodpecker/*.yml` and `docker-compose.yml`).
|
- **Tooling**: **pnpm**; Prettier (format); ESLint (JS), Stylelint (CSS), yamllint (YAML); **terser**, **clean-css**, **critters** (build).
|
||||||
- **Deployment**: Docker image → Gitea registry → Portainer stack redeploy.
|
- **Deployment**: Docker image (from `dist/`) → Gitea registry → Portainer stack redeploy.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Local development
|
## Local development
|
||||||
|
|
||||||
- **Dependencies**: `pnpm install` (only devDependencies: Prettier, yaml-lint).
|
- **Dependencies**: `pnpm install`.
|
||||||
- **Format**: `pnpm format` / `pnpm format:check`.
|
- **Format**: `pnpm format` / `pnpm format:check`.
|
||||||
- **Lint**: `pnpm lint` (yamllint on Woodpecker and docker-compose YAML).
|
- **Lint**: `pnpm lint` (ESLint for `src/**/*.js`, Stylelint for `src/**/*.css`, yamllint for Woodpecker + docker-compose). Use `pnpm lint:fix` to auto-fix where supported.
|
||||||
|
- **Build**: `pnpm build` → produces `dist/`. Required before building the Docker image.
|
||||||
- **Run locally (Docker)**:
|
- **Run locally (Docker)**:
|
||||||
- Build: `pnpm docker:build` (or `docker build --platform linux/amd64 -t git.mifi.dev/mifi-holdings/landing:latest .`).
|
- Build site: `pnpm build`.
|
||||||
|
- Image: `pnpm docker:build` (or `docker build --platform linux/amd64 -t git.mifi.dev/mifi-holdings/landing:latest .`).
|
||||||
- Run: use `docker-compose up` **only** on a host that has `marina-net` and Traefik; otherwise run the image with a port map and open `http://localhost:<port>`.
|
- Run: use `docker-compose up` **only** on a host that has `marina-net` and Traefik; otherwise run the image with a port map and open `http://localhost:<port>`.
|
||||||
|
|
||||||
There is no dev server in this repo; edit `src/` and refresh the browser or rebuild the image to test.
|
No dev server; edit `src/` and run `pnpm build` (and optionally `docker run` or a local static server on `dist/`) to test.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Docker
|
## Docker
|
||||||
|
|
||||||
- **Dockerfile**: Copies `nginx/conf.d/` and `src/` into an `nginx:alpine` image. No multi-stage build; the image is the final runtime.
|
- **Dockerfile**: Copies `nginx/conf.d/` and **`dist/`** (not `src/`) into an `nginx:alpine` image. Run `pnpm build` first so `dist/` exists.
|
||||||
- **Image**: Tagged as `git.mifi.dev/mifi-holdings/landing:latest` (and `:<commit-sha>` in CI).
|
- **Image**: Tagged as `git.mifi.dev/mifi-holdings/landing:latest` (and `:<commit-sha>` in CI).
|
||||||
- **Local build/push**: `pnpm docker:build`, `pnpm docker:push` (requires login to `git.mifi.dev`).
|
- **Local build/push**: `pnpm build` → `pnpm docker:build` → `pnpm docker:push` (requires login to `git.mifi.dev`).
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -96,11 +115,11 @@ Three pipelines:
|
|||||||
|
|
||||||
1. **ci** (`.woodpecker/ci.yml`)
|
1. **ci** (`.woodpecker/ci.yml`)
|
||||||
- **When**: Every PR and every push to `main`.
|
- **When**: Every PR and every push to `main`.
|
||||||
- **Steps**: Install deps → Prettier format check → yamllint (Woodpecker + docker-compose). Mattermost notifications on success/failure.
|
- **Steps**: Install deps → Prettier format check → lint (ESLint, Stylelint, yamllint) → **Build** (`pnpm build`). Mattermost notifications on success/failure.
|
||||||
|
|
||||||
2. **build** (`.woodpecker/build.yml`)
|
2. **build** (`.woodpecker/build.yml`)
|
||||||
- **When**: Push/tag/manual on `main` (and deployment to production); **depends_on: ci**.
|
- **When**: Push/tag/manual on `main` (and deployment to production); **depends_on: ci**.
|
||||||
- **Steps**: Build Docker image (linux/amd64, up to 3 retries) → push `:latest` and `:<CI_COMMIT_SHA>` to `git.mifi.dev/mifi-holdings/landing`. Mattermost notifications.
|
- **Steps**: **Site build** (`pnpm install`, `pnpm build`) → Docker image build (linux/amd64, up to 3 retries) → push `:latest` and `:<CI_COMMIT_SHA>` to `git.mifi.dev/mifi-holdings/landing`. Mattermost notifications.
|
||||||
|
|
||||||
3. **deploy** (`.woodpecker/deploy.yml`)
|
3. **deploy** (`.woodpecker/deploy.yml`)
|
||||||
- **When**: Same as build (main / production deploy); **depends_on: ci**.
|
- **When**: Same as build (main / production deploy); **depends_on: ci**.
|
||||||
@@ -128,7 +147,7 @@ To deploy manually: pull the latest image and redeploy the stack in Portainer, o
|
|||||||
|
|
||||||
## nginx behavior
|
## nginx behavior
|
||||||
|
|
||||||
- **Root**: `/usr/share/nginx/html` (contents of `src/`).
|
- **Root**: `/usr/share/nginx/html` (contents of **`dist/`** after build).
|
||||||
- **HTML**: `Cache-Control: public, no-cache` so updates are visible quickly.
|
- **HTML**: `Cache-Control: public, no-cache` so updates are visible quickly.
|
||||||
- **JS/CSS**: Long cache, `immutable` for hashed/versioned assets.
|
- **JS/CSS**: Long cache, `immutable` for hashed/versioned assets.
|
||||||
- **Images/fonts**: Cached (30d / 1y).
|
- **Images/fonts**: Cached (30d / 1y).
|
||||||
@@ -138,6 +157,6 @@ To deploy manually: pull the latest image and redeploy the stack in Portainer, o
|
|||||||
|
|
||||||
## Summary
|
## Summary
|
||||||
|
|
||||||
- **What it is**: Static landing for mifi.holdings.
|
- **What it is**: Static landing for mifi.holdings; source in `src/`, built output in `dist/`.
|
||||||
- **How it runs**: nginx in Docker, fronted by Traefik on `marina-net`.
|
- **How it runs**: nginx in Docker serving `dist/`, fronted by Traefik on `marina-net`.
|
||||||
- **How it’s updated**: Push to `main` → Woodpecker runs ci, build (image + push), deploy (Portainer webhook); Mattermost reports status.
|
- **How it’s updated**: Push to `main` → Woodpecker runs ci (lint, format, **build**), then build pipeline (**site build** → Docker image → push), then deploy (Portainer webhook); Mattermost reports status.
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ services:
|
|||||||
- 'traefik.docker.network=marina-net'
|
- 'traefik.docker.network=marina-net'
|
||||||
- 'traefik.http.routers.holdings-landing.rule=Host(`mifi.holdings`) || Host(`www.mifi.holdings`)'
|
- 'traefik.http.routers.holdings-landing.rule=Host(`mifi.holdings`) || Host(`www.mifi.holdings`)'
|
||||||
- 'traefik.http.routers.holdings-landing.entrypoints=websecure'
|
- 'traefik.http.routers.holdings-landing.entrypoints=websecure'
|
||||||
- 'traefik.http.routers.holdings-landing.middlewares=ecurity-supermax-with-analytics@@file,redirect-www-to-non-www@file'
|
- 'traefik.http.routers.holdings-landing.middlewares=security-supermax-with-analytics@@file,redirect-www-to-non-www@file'
|
||||||
- 'traefik.http.routers.holdings-landing.tls=true'
|
- 'traefik.http.routers.holdings-landing.tls=true'
|
||||||
- 'traefik.http.routers.holdings-landing.tls.certresolver=letsencrypt'
|
- 'traefik.http.routers.holdings-landing.tls.certresolver=letsencrypt'
|
||||||
- 'traefik.http.services.holdings-landing.loadbalancer.server.port=80'
|
- 'traefik.http.services.holdings-landing.loadbalancer.server.port=80'
|
||||||
|
|||||||
Reference in New Issue
Block a user