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