feat: initial CI/CD pipeline, Dockerfile, K8s manifests, build+test scripts
Build and Deploy / build-and-test (push) Failing after 3m37s
Build and Deploy / build-image (push) Has been skipped
Build and Deploy / deploy (push) Has been skipped

This commit is contained in:
2026-05-31 13:31:56 +00:00
parent b05e0f0a53
commit abf29e61de
11 changed files with 370 additions and 0 deletions
+102
View File
@@ -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
View File
@@ -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
+46
View File
@@ -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/');
+21
View File
@@ -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>
+1
View File
@@ -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}
+53
View File
@@ -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
+40
View File
@@ -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
+7
View File
@@ -0,0 +1,7 @@
apiVersion: v1
kind: Namespace
metadata:
name: openclaw-private
labels:
app.kubernetes.io/name: signalledger
app.kubernetes.io/part-of: signalledger
+17
View File
@@ -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
+10
View File
@@ -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": {}
}
+35
View File
@@ -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.');