diff --git a/.gitea/workflows/build-and-deploy.yaml b/.gitea/workflows/build-and-deploy.yaml new file mode 100644 index 0000000..f582d0b --- /dev/null +++ b/.gitea/workflows/build-and-deploy.yaml @@ -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 diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..de3d7e4 --- /dev/null +++ b/Dockerfile @@ -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 diff --git a/build.js b/build.js new file mode 100644 index 0000000..43d23f8 --- /dev/null +++ b/build.js @@ -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 = ` + + + + + Signal Ledger + + + +
+

Signal Ledger

+

Independent news. Clear signal.

+
+
+

Welcome to Signal Ledger — a subsidiary of Jopdorp.

+
+ + + +`; + +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/'); diff --git a/dist/index.html b/dist/index.html new file mode 100644 index 0000000..df271e3 --- /dev/null +++ b/dist/index.html @@ -0,0 +1,21 @@ + + + + + + Signal Ledger + + + +
+

Signal Ledger

+

Independent news. Clear signal.

+
+
+

Welcome to Signal Ledger — a subsidiary of Jopdorp.

+
+ + + diff --git a/dist/style.css b/dist/style.css new file mode 100644 index 0000000..07fd72d --- /dev/null +++ b/dist/style.css @@ -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} \ No newline at end of file diff --git a/k8s/deployment.yaml b/k8s/deployment.yaml new file mode 100644 index 0000000..d3aa6b6 --- /dev/null +++ b/k8s/deployment.yaml @@ -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 diff --git a/k8s/ingress.yaml b/k8s/ingress.yaml new file mode 100644 index 0000000..bad1381 --- /dev/null +++ b/k8s/ingress.yaml @@ -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 diff --git a/k8s/namespace.yaml b/k8s/namespace.yaml new file mode 100644 index 0000000..dcb847f --- /dev/null +++ b/k8s/namespace.yaml @@ -0,0 +1,7 @@ +apiVersion: v1 +kind: Namespace +metadata: + name: openclaw-private + labels: + app.kubernetes.io/name: signalledger + app.kubernetes.io/part-of: signalledger diff --git a/k8s/service.yaml b/k8s/service.yaml new file mode 100644 index 0000000..354dcef --- /dev/null +++ b/k8s/service.yaml @@ -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 diff --git a/package.json b/package.json new file mode 100644 index 0000000..9a08128 --- /dev/null +++ b/package.json @@ -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": {} +} diff --git a/test.js b/test.js new file mode 100644 index 0000000..6e810af --- /dev/null +++ b/test.js @@ -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('Signal Ledger')], + ['header', html.includes('

Signal Ledger

')], + ['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.');