feat: initial CI/CD pipeline, Dockerfile, K8s manifests, build+test scripts
This commit is contained in:
@@ -0,0 +1,102 @@
|
|||||||
|
name: Build and Deploy
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches: [main]
|
||||||
|
pull_request:
|
||||||
|
branches: [main]
|
||||||
|
|
||||||
|
env:
|
||||||
|
REGISTRY: registry.claw.jopdorp.nl
|
||||||
|
IMAGE_NAME: signalledger
|
||||||
|
NAMESPACE: openclaw-private
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build-and-test:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Set up Node
|
||||||
|
uses: actions/setup-node@v4
|
||||||
|
with:
|
||||||
|
node-version: '22'
|
||||||
|
cache: 'npm'
|
||||||
|
|
||||||
|
- name: Install dependencies
|
||||||
|
run: npm ci
|
||||||
|
|
||||||
|
- name: Run tests
|
||||||
|
run: npm test
|
||||||
|
|
||||||
|
- name: Build site
|
||||||
|
run: npm run build
|
||||||
|
|
||||||
|
- name: Upload build artifact
|
||||||
|
uses: actions/upload-artifact@v4
|
||||||
|
with:
|
||||||
|
name: dist
|
||||||
|
path: dist/
|
||||||
|
|
||||||
|
build-image:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
needs: build-and-test
|
||||||
|
if: github.event_name == 'push' && github.ref == 'refs/heads/main'
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Set up Docker Buildx
|
||||||
|
uses: docker/setup-buildx-action@v3
|
||||||
|
|
||||||
|
- name: Log in to registry
|
||||||
|
uses: docker/login-action@v3
|
||||||
|
with:
|
||||||
|
registry: ${{ env.REGISTRY }}
|
||||||
|
username: ${{ gitea.actor }}
|
||||||
|
password: ${{ secrets.REGISTRY_TOKEN }}
|
||||||
|
|
||||||
|
- name: Docker meta
|
||||||
|
id: meta
|
||||||
|
uses: docker/metadata-action@v5
|
||||||
|
with:
|
||||||
|
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
|
||||||
|
tags: |
|
||||||
|
type=sha,prefix=,suffix=,format=short
|
||||||
|
type=raw,value=latest
|
||||||
|
|
||||||
|
- name: Build and push
|
||||||
|
uses: docker/build-push-action@v5
|
||||||
|
with:
|
||||||
|
context: .
|
||||||
|
push: true
|
||||||
|
tags: ${{ steps.meta.outputs.tags }}
|
||||||
|
labels: ${{ steps.meta.outputs.labels }}
|
||||||
|
cache-from: type=gha
|
||||||
|
cache-to: type=gha,mode=max
|
||||||
|
|
||||||
|
deploy:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
needs: build-image
|
||||||
|
if: github.event_name == 'push' && github.ref == 'refs/heads/main'
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Set up kubectl
|
||||||
|
uses: tale/kubectl-action@v1
|
||||||
|
with:
|
||||||
|
base64-kube-config: ${{ secrets.KUBECONFIG_BASE64 }}
|
||||||
|
|
||||||
|
- name: Update image tag in manifest
|
||||||
|
run: |
|
||||||
|
sed -i "s|image: .*signalledger.*|image: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:sha-$(git rev-parse --short HEAD)|" k8s/deployment.yaml
|
||||||
|
|
||||||
|
- name: Apply Kubernetes manifests
|
||||||
|
run: |
|
||||||
|
kubectl apply -f k8s/namespace.yaml
|
||||||
|
kubectl apply -f k8s/deployment.yaml
|
||||||
|
kubectl apply -f k8s/service.yaml
|
||||||
|
kubectl apply -f k8s/ingress.yaml
|
||||||
|
kubectl rollout status deployment/signalledger -n ${{ env.NAMESPACE }} --timeout=120s
|
||||||
+38
@@ -0,0 +1,38 @@
|
|||||||
|
# Build stage
|
||||||
|
FROM node:22-alpine AS builder
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
# Copy package files and install dependencies
|
||||||
|
COPY package*.json ./
|
||||||
|
RUN npm ci
|
||||||
|
|
||||||
|
# Copy source and build
|
||||||
|
COPY . .
|
||||||
|
RUN npm run build
|
||||||
|
|
||||||
|
# Production stage — nginx serving static files
|
||||||
|
FROM nginx:alpine
|
||||||
|
|
||||||
|
# Copy built site
|
||||||
|
COPY --from=builder /app/dist /usr/share/nginx/html
|
||||||
|
|
||||||
|
# Copy ads.txt if present at root
|
||||||
|
COPY --from=builder /app/ads.txt /usr/share/nginx/html/ads.txt
|
||||||
|
|
||||||
|
# Custom nginx config for SPA/history routing
|
||||||
|
RUN echo 'server { \
|
||||||
|
listen 80; \
|
||||||
|
server_name localhost; \
|
||||||
|
root /usr/share/nginx/html; \
|
||||||
|
index index.html; \
|
||||||
|
location / { \
|
||||||
|
try_files $uri $uri/ /index.html; \
|
||||||
|
} \
|
||||||
|
location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg|woff|woff2|ttf|eot)$ { \
|
||||||
|
expires 1y; \
|
||||||
|
add_header Cache-Control "public, immutable"; \
|
||||||
|
} \
|
||||||
|
}' > /etc/nginx/conf.d/default.conf
|
||||||
|
|
||||||
|
EXPOSE 80
|
||||||
@@ -0,0 +1,46 @@
|
|||||||
|
const fs = require('fs');
|
||||||
|
const path = require('path');
|
||||||
|
|
||||||
|
const srcDir = path.join(__dirname, 'src');
|
||||||
|
const distDir = path.join(__dirname, 'dist');
|
||||||
|
|
||||||
|
if (!fs.existsSync(distDir)) {
|
||||||
|
fs.mkdirSync(distDir, { recursive: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Simple build: copy static assets and generate index.html
|
||||||
|
const indexHtml = `<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>Signal Ledger</title>
|
||||||
|
<link rel="stylesheet" href="/style.css">
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<header>
|
||||||
|
<h1>Signal Ledger</h1>
|
||||||
|
<p class="tagline">Independent news. Clear signal.</p>
|
||||||
|
</header>
|
||||||
|
<main>
|
||||||
|
<p>Welcome to Signal Ledger — a subsidiary of Jopdorp.</p>
|
||||||
|
</main>
|
||||||
|
<footer>
|
||||||
|
<p>Signal Ledger is a subsidiary of Jopdorp.</p>
|
||||||
|
</footer>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
`;
|
||||||
|
|
||||||
|
fs.writeFileSync(path.join(distDir, 'index.html'), indexHtml);
|
||||||
|
|
||||||
|
// Copy style.css if it exists
|
||||||
|
const styleSrc = path.join(srcDir, 'style.css');
|
||||||
|
const styleDest = path.join(distDir, 'style.css');
|
||||||
|
if (fs.existsSync(styleSrc)) {
|
||||||
|
fs.copyFileSync(styleSrc, styleDest);
|
||||||
|
} else {
|
||||||
|
fs.writeFileSync(styleDest, `body{font-family:system-ui,sans-serif;margin:0;padding:2rem;background:#0b0c10;color:#c5c6c7}header{border-bottom:1px solid #45a29e;padding-bottom:1rem;margin-bottom:2rem}h1{color:#66fcf1;margin:0}.tagline{color:#45a29e}footer{margin-top:4rem;padding-top:1rem;border-top:1px solid #45a29e;font-size:.875rem;color:#45a29e}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('Build complete: dist/');
|
||||||
Vendored
+21
@@ -0,0 +1,21 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>Signal Ledger</title>
|
||||||
|
<link rel="stylesheet" href="/style.css">
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<header>
|
||||||
|
<h1>Signal Ledger</h1>
|
||||||
|
<p class="tagline">Independent news. Clear signal.</p>
|
||||||
|
</header>
|
||||||
|
<main>
|
||||||
|
<p>Welcome to Signal Ledger — a subsidiary of Jopdorp.</p>
|
||||||
|
</main>
|
||||||
|
<footer>
|
||||||
|
<p>Signal Ledger is a subsidiary of Jopdorp.</p>
|
||||||
|
</footer>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
Vendored
+1
@@ -0,0 +1 @@
|
|||||||
|
body{font-family:system-ui,sans-serif;margin:0;padding:2rem;background:#0b0c10;color:#c5c6c7}header{border-bottom:1px solid #45a29e;padding-bottom:1rem;margin-bottom:2rem}h1{color:#66fcf1;margin:0}.tagline{color:#45a29e}footer{margin-top:4rem;padding-top:1rem;border-top:1px solid #45a29e;font-size:.875rem;color:#45a29e}
|
||||||
@@ -0,0 +1,53 @@
|
|||||||
|
apiVersion: apps/v1
|
||||||
|
kind: Deployment
|
||||||
|
metadata:
|
||||||
|
name: signalledger
|
||||||
|
namespace: openclaw-private
|
||||||
|
labels:
|
||||||
|
app.kubernetes.io/name: signalledger
|
||||||
|
app.kubernetes.io/component: frontend
|
||||||
|
app.kubernetes.io/part-of: signalledger
|
||||||
|
app.kubernetes.io/managed-by: gitea-actions
|
||||||
|
spec:
|
||||||
|
replicas: 2
|
||||||
|
strategy:
|
||||||
|
type: RollingUpdate
|
||||||
|
rollingUpdate:
|
||||||
|
maxSurge: 1
|
||||||
|
maxUnavailable: 0
|
||||||
|
selector:
|
||||||
|
matchLabels:
|
||||||
|
app.kubernetes.io/name: signalledger
|
||||||
|
template:
|
||||||
|
metadata:
|
||||||
|
labels:
|
||||||
|
app.kubernetes.io/name: signalledger
|
||||||
|
app.kubernetes.io/component: frontend
|
||||||
|
app.kubernetes.io/part-of: signalledger
|
||||||
|
spec:
|
||||||
|
containers:
|
||||||
|
- name: signalledger
|
||||||
|
image: registry.claw.jopdorp.nl/signalledger:latest
|
||||||
|
imagePullPolicy: Always
|
||||||
|
ports:
|
||||||
|
- containerPort: 80
|
||||||
|
name: http
|
||||||
|
resources:
|
||||||
|
requests:
|
||||||
|
memory: "32Mi"
|
||||||
|
cpu: "50m"
|
||||||
|
limits:
|
||||||
|
memory: "128Mi"
|
||||||
|
cpu: "200m"
|
||||||
|
livenessProbe:
|
||||||
|
httpGet:
|
||||||
|
path: /
|
||||||
|
port: 80
|
||||||
|
initialDelaySeconds: 10
|
||||||
|
periodSeconds: 10
|
||||||
|
readinessProbe:
|
||||||
|
httpGet:
|
||||||
|
path: /
|
||||||
|
port: 80
|
||||||
|
initialDelaySeconds: 5
|
||||||
|
periodSeconds: 5
|
||||||
@@ -0,0 +1,40 @@
|
|||||||
|
apiVersion: networking.k8s.io/v1
|
||||||
|
kind: Ingress
|
||||||
|
metadata:
|
||||||
|
name: signalledger
|
||||||
|
namespace: openclaw-private
|
||||||
|
labels:
|
||||||
|
app.kubernetes.io/name: signalledger
|
||||||
|
app.kubernetes.io/component: frontend
|
||||||
|
app.kubernetes.io/part-of: signalledger
|
||||||
|
annotations:
|
||||||
|
cert-manager.io/cluster-issuer: "letsencrypt-prod"
|
||||||
|
nginx.ingress.kubernetes.io/ssl-redirect: "true"
|
||||||
|
spec:
|
||||||
|
ingressClassName: nginx
|
||||||
|
tls:
|
||||||
|
- hosts:
|
||||||
|
- signalledger.nl
|
||||||
|
- www.signalledger.nl
|
||||||
|
secretName: signalledger-tls
|
||||||
|
rules:
|
||||||
|
- host: signalledger.nl
|
||||||
|
http:
|
||||||
|
paths:
|
||||||
|
- path: /
|
||||||
|
pathType: Prefix
|
||||||
|
backend:
|
||||||
|
service:
|
||||||
|
name: signalledger
|
||||||
|
port:
|
||||||
|
number: 80
|
||||||
|
- host: www.signalledger.nl
|
||||||
|
http:
|
||||||
|
paths:
|
||||||
|
- path: /
|
||||||
|
pathType: Prefix
|
||||||
|
backend:
|
||||||
|
service:
|
||||||
|
name: signalledger
|
||||||
|
port:
|
||||||
|
number: 80
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
apiVersion: v1
|
||||||
|
kind: Namespace
|
||||||
|
metadata:
|
||||||
|
name: openclaw-private
|
||||||
|
labels:
|
||||||
|
app.kubernetes.io/name: signalledger
|
||||||
|
app.kubernetes.io/part-of: signalledger
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
apiVersion: v1
|
||||||
|
kind: Service
|
||||||
|
metadata:
|
||||||
|
name: signalledger
|
||||||
|
namespace: openclaw-private
|
||||||
|
labels:
|
||||||
|
app.kubernetes.io/name: signalledger
|
||||||
|
app.kubernetes.io/component: frontend
|
||||||
|
app.kubernetes.io/part-of: signalledger
|
||||||
|
spec:
|
||||||
|
selector:
|
||||||
|
app.kubernetes.io/name: signalledger
|
||||||
|
ports:
|
||||||
|
- port: 80
|
||||||
|
targetPort: 80
|
||||||
|
name: http
|
||||||
|
type: ClusterIP
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
{
|
||||||
|
"name": "signalledger.nl",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"description": "Signal Ledger news site",
|
||||||
|
"scripts": {
|
||||||
|
"build": "node build.js",
|
||||||
|
"test": "node test.js"
|
||||||
|
},
|
||||||
|
"devDependencies": {}
|
||||||
|
}
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
const fs = require('fs');
|
||||||
|
const path = require('path');
|
||||||
|
|
||||||
|
const distDir = path.join(__dirname, 'dist');
|
||||||
|
|
||||||
|
// Run build first if dist doesn't exist
|
||||||
|
if (!fs.existsSync(distDir)) {
|
||||||
|
require('./build.js');
|
||||||
|
}
|
||||||
|
|
||||||
|
const indexPath = path.join(distDir, 'index.html');
|
||||||
|
if (!fs.existsSync(indexPath)) {
|
||||||
|
console.error('FAIL: dist/index.html not found');
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
const html = fs.readFileSync(indexPath, 'utf8');
|
||||||
|
const checks = [
|
||||||
|
['title', html.includes('<title>Signal Ledger</title>')],
|
||||||
|
['header', html.includes('<h1>Signal Ledger</h1>')],
|
||||||
|
['footer', html.includes('Signal Ledger is a subsidiary of Jopdorp.')],
|
||||||
|
];
|
||||||
|
|
||||||
|
let failed = false;
|
||||||
|
for (const [name, pass] of checks) {
|
||||||
|
if (pass) {
|
||||||
|
console.log(`PASS: ${name}`);
|
||||||
|
} else {
|
||||||
|
console.error(`FAIL: ${name}`);
|
||||||
|
failed = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (failed) process.exit(1);
|
||||||
|
console.log('All tests passed.');
|
||||||
Reference in New Issue
Block a user