feat: add pages-deploy action for static site containerization and TrueNAS Traefik deployment
Update README Actions Table / update-readme (push) Successful in 10s
Update README Actions Table / update-readme (push) Successful in 10s
This commit is contained in:
@@ -10,6 +10,7 @@ This repository contains reusable composite actions for CI/CD workflows across o
|
|||||||
| :--- | :--- | :--- |
|
| :--- | :--- | :--- |
|
||||||
| [Deploy to Coolify](./coolify-deploy) | Triggers a Coolify deployment using a webhook and authorization token. | `rodydavis/shared-actions/coolify-deploy@main` |
|
| [Deploy to Coolify](./coolify-deploy) | Triggers a Coolify deployment using a webhook and authorization token. | `rodydavis/shared-actions/coolify-deploy@main` |
|
||||||
| [Enforce Flutter Version Constraint](./enforce-flutter-version) | Scans all pubspec.yaml files to ensure they use the required Flutter SDK constraint. | `rodydavis/shared-actions/enforce-flutter-version@main` |
|
| [Enforce Flutter Version Constraint](./enforce-flutter-version) | Scans all pubspec.yaml files to ensure they use the required Flutter SDK constraint. | `rodydavis/shared-actions/enforce-flutter-version@main` |
|
||||||
|
| [Deploy Static Pages](./pages-deploy) | Packages static web assets into a hardened Nginx container, pushes to Gitea registry, and deploys to TrueNAS with Traefik ingress. | `rodydavis/shared-actions/pages-deploy@main` |
|
||||||
| [Run Buildifier](./run-buildifier) | Downloads and runs buildifier on Starlark files. | `rodydavis/shared-actions/run-buildifier@main` |
|
| [Run Buildifier](./run-buildifier) | Downloads and runs buildifier on Starlark files. | `rodydavis/shared-actions/run-buildifier@main` |
|
||||||
| [Setup Bazel (Bazelisk)](./setup-bazel) | Installs Node, Bazelisk, and configures Git for private SSH dependencies. | `rodydavis/shared-actions/setup-bazel@main` |
|
| [Setup Bazel (Bazelisk)](./setup-bazel) | Installs Node, Bazelisk, and configures Git for private SSH dependencies. | `rodydavis/shared-actions/setup-bazel@main` |
|
||||||
| [Setup Flutter](./setup-flutter) | Cross-platform setup for Flutter with local mirror and multi-runner concurrency support. | `rodydavis/shared-actions/setup-flutter@main` |
|
| [Setup Flutter](./setup-flutter) | Cross-platform setup for Flutter with local mirror and multi-runner concurrency support. | `rodydavis/shared-actions/setup-flutter@main` |
|
||||||
|
|||||||
@@ -0,0 +1,92 @@
|
|||||||
|
# Deploy Static Pages Action
|
||||||
|
|
||||||
|
Custom Gitea Action for building and deploying static web applications (Vite, Lit, React, Flutter Web, Astro, vanilla HTML) as Docker containers with Traefik ingress on TrueNAS SCALE.
|
||||||
|
|
||||||
|
## Features
|
||||||
|
|
||||||
|
- **Zero-Boilerplate Containerization**: Dynamically creates a multi-stage Nginx container with SPA routing, gzip, PWA cache policies, and security headers.
|
||||||
|
- **Auto-Configured Ingress**: Generates Traefik routing labels using target domain/subdomain on the `apps-net` Docker network.
|
||||||
|
- **Direct Registry Publishing**: Authenticates and pushes images directly to Gitea Container Registry (`git.rodydavis.dev`).
|
||||||
|
- **Seamless TrueNAS Deployment**: Securely transfers `docker-compose.yml` to TrueNAS SCALE and pulls/restarts the service.
|
||||||
|
|
||||||
|
## Inputs
|
||||||
|
|
||||||
|
| Name | Description | Default | Required |
|
||||||
|
| :--- | :--- | :--- | :---: |
|
||||||
|
| `path` | Path to static build output directory (`./dist`, `./build/web`, `./public`) | `./dist` | No |
|
||||||
|
| `domain` | Target URL/domain for Traefik router (e.g. `myapp.rodydavis.dev`, `pocketpiano.app`) | `<repo-name>.rodydavis.dev` | No |
|
||||||
|
| `app_name` | Container and directory name | `<repo-name>` | No |
|
||||||
|
| `git_token` | Gitea token with package write permissions | `""` | No |
|
||||||
|
| `ssh_key` | SSH private key for TrueNAS SCALE | `""` | No |
|
||||||
|
| `registry` | Container registry host | `git.rodydavis.dev` | No |
|
||||||
|
| `host` | TrueNAS host IP/domain | `10.0.0.218` | No |
|
||||||
|
| `user` | TrueNAS SSH user | `root` | No |
|
||||||
|
| `network` | Docker external network for Traefik | `apps-net` | No |
|
||||||
|
| `spa` | Enable SPA fallback (`try_files $uri $uri/ /index.html`) | `true` | No |
|
||||||
|
| `push_image` | Push built image to container registry | `true` | No |
|
||||||
|
| `deploy` | Deploy to TrueNAS | `true` | No |
|
||||||
|
|
||||||
|
## Usage Examples
|
||||||
|
|
||||||
|
### 1. Node / Vite / Lit / TypeScript App
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
name: Build and Deploy
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches: ["main", "master"]
|
||||||
|
workflow_dispatch:
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
deploy:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- uses: actions/setup-node@v4
|
||||||
|
with:
|
||||||
|
node-version: 22
|
||||||
|
cache: 'npm'
|
||||||
|
|
||||||
|
- run: npm ci && npm run build
|
||||||
|
|
||||||
|
- name: Deploy Static Pages
|
||||||
|
uses: https://git.rodydavis.dev/rodydavis/shared-actions/pages-deploy@main
|
||||||
|
with:
|
||||||
|
path: './dist'
|
||||||
|
domain: 'bullmastiff.rodydavis.dev'
|
||||||
|
git_token: ${{ secrets.GIT_TOKEN }}
|
||||||
|
ssh_key: ${{ secrets.TRUENAS_SSH_KEY }}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. Flutter Web App
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
name: Build and Deploy
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches: ["main", "master"]
|
||||||
|
workflow_dispatch:
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
deploy:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- uses: https://git.rodydavis.dev/rodydavis/shared-actions/setup-flutter@main
|
||||||
|
|
||||||
|
- run: |
|
||||||
|
flutter pub get
|
||||||
|
flutter build web --release
|
||||||
|
|
||||||
|
- name: Deploy Static Pages
|
||||||
|
uses: https://git.rodydavis.dev/rodydavis/shared-actions/pages-deploy@main
|
||||||
|
with:
|
||||||
|
path: './build/web'
|
||||||
|
domain: 'flow-image-editor.rodydavis.dev'
|
||||||
|
git_token: ${{ secrets.GIT_TOKEN }}
|
||||||
|
ssh_key: ${{ secrets.TRUENAS_SSH_KEY }}
|
||||||
|
```
|
||||||
@@ -0,0 +1,275 @@
|
|||||||
|
name: 'Deploy Static Pages'
|
||||||
|
description: 'Packages static web assets into a hardened Nginx container, pushes to Gitea registry, and deploys to TrueNAS with Traefik ingress.'
|
||||||
|
inputs:
|
||||||
|
path:
|
||||||
|
description: 'Path to static files directory (e.g. ./dist, ./build/web, ./public)'
|
||||||
|
required: false
|
||||||
|
default: './dist'
|
||||||
|
domain:
|
||||||
|
description: 'Target URL or domain for Traefik router (e.g. "my-app.rodydavis.dev" or "https://my-app.rodydavis.dev")'
|
||||||
|
required: false
|
||||||
|
default: ''
|
||||||
|
app_name:
|
||||||
|
description: 'Application / container name (defaults to repository name)'
|
||||||
|
required: false
|
||||||
|
default: ''
|
||||||
|
registry:
|
||||||
|
description: 'Container registry host'
|
||||||
|
required: false
|
||||||
|
default: 'git.rodydavis.dev'
|
||||||
|
image_name:
|
||||||
|
description: 'Container image tag (defaults to registry/repository:latest)'
|
||||||
|
required: false
|
||||||
|
default: ''
|
||||||
|
git_token:
|
||||||
|
description: 'Token with package write permissions for container registry'
|
||||||
|
required: false
|
||||||
|
default: ''
|
||||||
|
ssh_key:
|
||||||
|
description: 'Private SSH key for authenticating to TrueNAS SCALE'
|
||||||
|
required: false
|
||||||
|
default: ''
|
||||||
|
host:
|
||||||
|
description: 'TrueNAS SSH host IP or domain'
|
||||||
|
required: false
|
||||||
|
default: '10.0.0.218'
|
||||||
|
user:
|
||||||
|
description: 'TrueNAS SSH user'
|
||||||
|
required: false
|
||||||
|
default: 'root'
|
||||||
|
network:
|
||||||
|
description: 'Docker external network name for Traefik'
|
||||||
|
required: false
|
||||||
|
default: 'apps-net'
|
||||||
|
spa:
|
||||||
|
description: 'Enable SPA router fallback in Nginx (try_files $uri $uri/ /index.html)'
|
||||||
|
required: false
|
||||||
|
default: 'true'
|
||||||
|
traefik_entrypoints:
|
||||||
|
description: 'Traefik entrypoint (e.g. web, websecure)'
|
||||||
|
required: false
|
||||||
|
default: 'web'
|
||||||
|
push_image:
|
||||||
|
description: 'Whether to push the built Docker image to registry'
|
||||||
|
required: false
|
||||||
|
default: 'true'
|
||||||
|
deploy:
|
||||||
|
description: 'Whether to deploy to TrueNAS'
|
||||||
|
required: false
|
||||||
|
default: 'true'
|
||||||
|
|
||||||
|
runs:
|
||||||
|
using: 'composite'
|
||||||
|
steps:
|
||||||
|
- name: Package and Deploy Static Pages
|
||||||
|
shell: bash
|
||||||
|
env:
|
||||||
|
INPUT_PATH: ${{ inputs.path }}
|
||||||
|
INPUT_DOMAIN: ${{ inputs.domain }}
|
||||||
|
INPUT_APP_NAME: ${{ inputs.app_name }}
|
||||||
|
INPUT_REGISTRY: ${{ inputs.registry }}
|
||||||
|
INPUT_IMAGE_NAME: ${{ inputs.image_name }}
|
||||||
|
INPUT_GIT_TOKEN: ${{ inputs.git_token }}
|
||||||
|
INPUT_SSH_KEY: ${{ inputs.ssh_key }}
|
||||||
|
INPUT_HOST: ${{ inputs.host }}
|
||||||
|
INPUT_USER: ${{ inputs.user }}
|
||||||
|
INPUT_NETWORK: ${{ inputs.network }}
|
||||||
|
INPUT_SPA: ${{ inputs.spa }}
|
||||||
|
INPUT_ENTRYPOINTS: ${{ inputs.traefik_entrypoints }}
|
||||||
|
INPUT_PUSH_IMAGE: ${{ inputs.push_image }}
|
||||||
|
INPUT_DEPLOY: ${{ inputs.deploy }}
|
||||||
|
REPO_NAME: ${{ github.event.repository.name }}
|
||||||
|
GITHUB_REPOSITORY: ${{ github.repository }}
|
||||||
|
GITHUB_ACTOR: ${{ github.actor }}
|
||||||
|
run: |
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
echo "🚀 Starting Static Pages Deployment..."
|
||||||
|
|
||||||
|
# 1. Resolve repository name fallback
|
||||||
|
if [ -z "${REPO_NAME:-}" ]; then
|
||||||
|
REPO_NAME=$(basename "${GITHUB_REPOSITORY:-app}")
|
||||||
|
fi
|
||||||
|
|
||||||
|
# 2. Locate static build assets
|
||||||
|
ASSET_DIR="$INPUT_PATH"
|
||||||
|
if [ ! -d "$ASSET_DIR" ]; then
|
||||||
|
echo "Specified path '$ASSET_DIR' not found. Searching common output directories..."
|
||||||
|
if [ -d "./dist" ]; then
|
||||||
|
ASSET_DIR="./dist"
|
||||||
|
elif [ -d "./build/web" ]; then
|
||||||
|
ASSET_DIR="./build/web"
|
||||||
|
elif [ -d "./web/dist" ]; then
|
||||||
|
ASSET_DIR="./web/dist"
|
||||||
|
elif [ -d "./public" ]; then
|
||||||
|
ASSET_DIR="./public"
|
||||||
|
elif [ -d "./build" ]; then
|
||||||
|
ASSET_DIR="./build"
|
||||||
|
elif [ -d "./out" ]; then
|
||||||
|
ASSET_DIR="./out"
|
||||||
|
else
|
||||||
|
echo "::error::Could not find static asset directory! Checked: '$INPUT_PATH', ./dist, ./build/web, ./public, ./build, ./out"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
echo "📁 Using static assets from: $ASSET_DIR"
|
||||||
|
|
||||||
|
# 3. Resolve target domain & router name
|
||||||
|
DOMAIN="$INPUT_DOMAIN"
|
||||||
|
if [ -n "$DOMAIN" ]; then
|
||||||
|
DOMAIN=$(echo "$DOMAIN" | sed -E 's#^https?://##' | sed 's#/.*$##')
|
||||||
|
else
|
||||||
|
CLEAN_REPO=$(echo "$REPO_NAME" | tr '_' '-' | tr '[:upper:]' '[:lower:]')
|
||||||
|
DOMAIN="${CLEAN_REPO}.rodydavis.dev"
|
||||||
|
fi
|
||||||
|
|
||||||
|
APP_NAME="${INPUT_APP_NAME:-$REPO_NAME}"
|
||||||
|
APP_NAME=$(echo "$APP_NAME" | tr '_' '-' | tr '[:upper:]' '[:lower:]')
|
||||||
|
ROUTER_NAME=$(echo "$APP_NAME" | tr -cd '[:alnum:]')
|
||||||
|
|
||||||
|
# 4. Resolve container image tag
|
||||||
|
REGISTRY="${INPUT_REGISTRY:-git.rodydavis.dev}"
|
||||||
|
if [ -n "$INPUT_IMAGE_NAME" ]; then
|
||||||
|
IMAGE_NAME="$INPUT_IMAGE_NAME"
|
||||||
|
else
|
||||||
|
REPO_LOWER=$(echo "$GITHUB_REPOSITORY" | tr '[:upper:]' '[:lower:]')
|
||||||
|
IMAGE_NAME="${REGISTRY}/${REPO_LOWER}:latest"
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "🌐 Routing Target: https://${DOMAIN}"
|
||||||
|
echo "📦 Container Name: ${APP_NAME}-web"
|
||||||
|
echo "🏷️ Image Tag: ${IMAGE_NAME}"
|
||||||
|
|
||||||
|
# 5. Prepare build bundle in temporary staging directory
|
||||||
|
BUILD_STAGING=$(mktemp -d)
|
||||||
|
trap 'rm -rf "$BUILD_STAGING"' EXIT
|
||||||
|
|
||||||
|
mkdir -p "${BUILD_STAGING}/html"
|
||||||
|
cp -R "${ASSET_DIR}/." "${BUILD_STAGING}/html/"
|
||||||
|
|
||||||
|
# Configure SPA fallback rule
|
||||||
|
if [ "$INPUT_SPA" = "true" ]; then
|
||||||
|
SPA_DIRECTIVE='try_files $uri $uri/ /index.html;'
|
||||||
|
else
|
||||||
|
SPA_DIRECTIVE='try_files $uri $uri/ =404;'
|
||||||
|
fi
|
||||||
|
|
||||||
|
cat <<EOF > "${BUILD_STAGING}/nginx.conf"
|
||||||
|
server {
|
||||||
|
listen 80;
|
||||||
|
listen [::]:80;
|
||||||
|
server_name localhost;
|
||||||
|
|
||||||
|
root /usr/share/nginx/html;
|
||||||
|
index index.html index.htm;
|
||||||
|
|
||||||
|
gzip on;
|
||||||
|
gzip_vary on;
|
||||||
|
gzip_min_length 1024;
|
||||||
|
gzip_proxied expired no-cache no-store private auth;
|
||||||
|
gzip_types text/plain text/css text/xml text/javascript application/x-javascript application/xml application/javascript application/json application/manifest+json image/svg+xml;
|
||||||
|
|
||||||
|
location = /sw.js {
|
||||||
|
add_header Cache-Control "no-cache, no-store, must-revalidate";
|
||||||
|
expires 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
location = /manifest.json {
|
||||||
|
add_header Cache-Control "no-cache, must-revalidate";
|
||||||
|
expires 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
location / {
|
||||||
|
${SPA_DIRECTIVE}
|
||||||
|
|
||||||
|
add_header X-Frame-Options "SAMEORIGIN" always;
|
||||||
|
add_header X-Content-Type-Options "nosniff" always;
|
||||||
|
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
|
||||||
|
}
|
||||||
|
|
||||||
|
location ~* \.(?:css|js|jpg|jpeg|gif|png|ico|cur|gz|svg|svgz|mp4|ogg|ogv|webm|htc|woff|woff2)$ {
|
||||||
|
expires 1y;
|
||||||
|
add_header Cache-Control "public, immutable";
|
||||||
|
}
|
||||||
|
|
||||||
|
error_page 500 502 503 504 /50x.html;
|
||||||
|
location = /50x.html {
|
||||||
|
root /usr/share/nginx/html;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
EOF
|
||||||
|
|
||||||
|
cat <<EOF > "${BUILD_STAGING}/Dockerfile"
|
||||||
|
FROM nginx:alpine
|
||||||
|
WORKDIR /usr/share/nginx/html
|
||||||
|
RUN rm -rf ./*
|
||||||
|
COPY nginx.conf /etc/nginx/conf.d/default.conf
|
||||||
|
COPY html/ ./
|
||||||
|
EXPOSE 80
|
||||||
|
CMD ["nginx", "-g", "daemon off;"]
|
||||||
|
EOF
|
||||||
|
|
||||||
|
cat <<EOF > "${BUILD_STAGING}/docker-compose.yml"
|
||||||
|
version: '3.8'
|
||||||
|
|
||||||
|
services:
|
||||||
|
${APP_NAME}-web:
|
||||||
|
image: ${IMAGE_NAME}
|
||||||
|
container_name: ${APP_NAME}-web
|
||||||
|
restart: unless-stopped
|
||||||
|
networks:
|
||||||
|
- ${INPUT_NETWORK}
|
||||||
|
labels:
|
||||||
|
- "traefik.enable=true"
|
||||||
|
- "traefik.http.routers.${ROUTER_NAME}.rule=Host(\`${DOMAIN}\`)"
|
||||||
|
- "traefik.http.routers.${ROUTER_NAME}.entrypoints=${INPUT_ENTRYPOINTS}"
|
||||||
|
- "traefik.http.services.${ROUTER_NAME}.loadbalancer.server.port=80"
|
||||||
|
|
||||||
|
networks:
|
||||||
|
${INPUT_NETWORK}:
|
||||||
|
external: true
|
||||||
|
EOF
|
||||||
|
|
||||||
|
# 6. Docker Build
|
||||||
|
echo "🔨 Building Docker image..."
|
||||||
|
docker build -t "$IMAGE_NAME" -f "${BUILD_STAGING}/Dockerfile" "$BUILD_STAGING"
|
||||||
|
|
||||||
|
# 7. Push image to registry
|
||||||
|
if [ "$INPUT_PUSH_IMAGE" = "true" ]; then
|
||||||
|
if [ -n "$INPUT_GIT_TOKEN" ]; then
|
||||||
|
echo "🔐 Logging into ${REGISTRY}..."
|
||||||
|
echo "$INPUT_GIT_TOKEN" | docker login "$REGISTRY" -u "${GITHUB_ACTOR}" --password-stdin
|
||||||
|
fi
|
||||||
|
echo "📤 Pushing image to ${IMAGE_NAME}..."
|
||||||
|
docker push "$IMAGE_NAME"
|
||||||
|
else
|
||||||
|
echo "⏭️ Skipping image push (push_image=false)"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# 8. Deploy to TrueNAS
|
||||||
|
if [ "$INPUT_DEPLOY" = "true" ]; then
|
||||||
|
if [ -z "$INPUT_SSH_KEY" ]; then
|
||||||
|
echo "⚠️ No ssh_key provided for TrueNAS. Skipping remote deployment step."
|
||||||
|
else
|
||||||
|
echo "🚢 Deploying to TrueNAS SCALE (${INPUT_HOST})..."
|
||||||
|
mkdir -p ~/.ssh
|
||||||
|
echo "$INPUT_SSH_KEY" > ~/.ssh/truenas_deploy_key
|
||||||
|
chmod 600 ~/.ssh/truenas_deploy_key
|
||||||
|
ssh-keyscan -H "$INPUT_HOST" >> ~/.ssh/known_hosts 2>/dev/null
|
||||||
|
|
||||||
|
REMOTE_DIR="/mnt/jupiter/apps/${APP_NAME}"
|
||||||
|
echo "Ensuring remote directory ${REMOTE_DIR} on ${INPUT_HOST}..."
|
||||||
|
ssh -i ~/.ssh/truenas_deploy_key -o StrictHostKeyChecking=no "${INPUT_USER}@${INPUT_HOST}" "mkdir -p ${REMOTE_DIR}"
|
||||||
|
|
||||||
|
echo "Syncing docker-compose.yml to ${REMOTE_DIR}..."
|
||||||
|
scp -i ~/.ssh/truenas_deploy_key -o StrictHostKeyChecking=no "${BUILD_STAGING}/docker-compose.yml" "${INPUT_USER}@${INPUT_HOST}:${REMOTE_DIR}/docker-compose.yml"
|
||||||
|
|
||||||
|
echo "Pulling latest image and restarting stack..."
|
||||||
|
ssh -i ~/.ssh/truenas_deploy_key -o StrictHostKeyChecking=no "${INPUT_USER}@${INPUT_HOST}" "cd ${REMOTE_DIR} && docker compose pull && docker compose up -d --remove-orphans"
|
||||||
|
|
||||||
|
rm -f ~/.ssh/truenas_deploy_key
|
||||||
|
echo "✅ Deployment complete! Accessible at: https://${DOMAIN}"
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
echo "⏭️ Skipping deployment step (deploy=false)"
|
||||||
|
fi
|
||||||
Reference in New Issue
Block a user