diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..cbf6c0b --- /dev/null +++ b/.dockerignore @@ -0,0 +1,3 @@ +.git +.forgejo +README.md diff --git a/.forgejo/workflows/deploy-hugo-site.yml b/.forgejo/workflows/deploy-hugo-site.yml new file mode 100644 index 0000000..ef1c680 --- /dev/null +++ b/.forgejo/workflows/deploy-hugo-site.yml @@ -0,0 +1,105 @@ +name: Deploy Hugo Site + +on: + workflow_call: + inputs: + workflow_name: + description: Display name for the caller workflow + required: false + default: Deploy + type: string + checkout_submodules: + description: Whether checkout should fetch submodules recursively + required: false + default: false + type: boolean + deploy_script: + description: Path to the repo-local deploy script + required: false + default: ./scripts/deploy.sh + type: string + deploy_ssh_host: + description: Deployment SSH host + required: false + default: "" + type: string + deploy_ssh_user: + description: Deployment SSH user + required: false + default: "" + type: string + secrets: + deploy_ssh_key: + required: true + +jobs: + deploy: + name: ${{ inputs.workflow_name }} + timeout-minutes: 20 + runs-on: docker-desktop + container: + image: registry.develton.com/cliper/hugo-deployment:v1 + defaults: + run: + shell: bash + env: + DEPLOY_SSH_HOST: ${{ inputs.deploy_ssh_host != '' && inputs.deploy_ssh_host || vars.DEPLOY_SSH_HOST }} + DEPLOY_SSH_PORT: ${{ vars.DEPLOY_SSH_PORT }} + DEPLOY_SSH_USER: ${{ inputs.deploy_ssh_user != '' && inputs.deploy_ssh_user || vars.DEPLOY_SSH_USER }} + DEPLOY_KNOWN_HOSTS: ${{ vars.DEPLOY_KNOWN_HOSTS }} + DEPLOY_PATH: ${{ vars.DEPLOY_PATH }} + HUGO_PARAMS_ANALYTICS_GOOGLE_SITEVERIFICATIONTAG: ${{ vars.GOOGLE_SITE_VERIFICATION_TAG }} + HUGO_PARAMS_TURNSTILESITEKEY: ${{ vars.TURNSTILE_SITE_KEY }} + HUGO_WORKER_URL: ${{ vars.WORKER_URL }} + steps: + - name: Checkout repository + uses: https://data.forgejo.org/actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 + with: + submodules: ${{ inputs.checkout_submodules && 'recursive' || 'false' }} + + - name: Validate deploy configuration + env: + DEPLOY_SSH_KEY: ${{ secrets.deploy_ssh_key }} + run: | + set -euo pipefail + missing=() + [[ -n "${DEPLOY_SSH_HOST:-}" ]] || missing+=("DEPLOY_SSH_HOST") + [[ -n "${DEPLOY_SSH_USER:-}" ]] || missing+=("DEPLOY_SSH_USER") + [[ -n "${DEPLOY_PATH:-}" ]] || missing+=("DEPLOY_PATH") + [[ -n "${DEPLOY_SSH_KEY:-}" ]] || missing+=("DEPLOY_SSH_KEY") + if (( ${#missing[@]} > 0 )); then + printf 'Missing required deploy settings: %s\n' "${missing[*]}" >&2 + exit 1 + fi + + - name: Write deploy key + env: + DEPLOY_SSH_KEY: ${{ secrets.deploy_ssh_key }} + run: | + set -euo pipefail + install -d -m 0700 "$HOME/.ssh" + printf '%s\n' "$DEPLOY_SSH_KEY" > "$HOME/.ssh/deploy_key" + chmod 0600 "$HOME/.ssh/deploy_key" + + - name: Add deploy host to known_hosts + run: | + set -euo pipefail + install -d -m 0700 "$HOME/.ssh" + if [[ -n "${DEPLOY_KNOWN_HOSTS:-}" ]]; then + printf '%s\n' "$DEPLOY_KNOWN_HOSTS" > "$HOME/.ssh/known_hosts" + else + ssh-keyscan -H -p "${DEPLOY_SSH_PORT:-22}" "${DEPLOY_SSH_HOST}" > "$HOME/.ssh/known_hosts" + fi + chmod 0600 "$HOME/.ssh/known_hosts" + + - name: Run deploy script + run: | + set -euo pipefail + test -x "${{ inputs.deploy_script }}" || chmod +x "${{ inputs.deploy_script }}" + export SSH_IDENTITY_FILE="$HOME/.ssh/deploy_key" + export SSH_KNOWN_HOSTS_FILE="$HOME/.ssh/known_hosts" + "${{ inputs.deploy_script }}" + + - name: Remove deploy key + if: ${{ always() }} + run: rm -f "$HOME/.ssh/deploy_key" diff --git a/.forgejo/workflows/publish-image.yml b/.forgejo/workflows/publish-image.yml new file mode 100644 index 0000000..0d0a218 --- /dev/null +++ b/.forgejo/workflows/publish-image.yml @@ -0,0 +1,75 @@ +name: Publish Hugo Deployment Image + +on: + push: + branches: + - main + workflow_dispatch: + inputs: + image_tag: + description: Image tag to publish + required: false + type: string + publish_latest: + description: Also update latest + required: false + type: boolean + default: false + +concurrency: + group: publish-image-${{ forgejo.repository }} + cancel-in-progress: false + +jobs: + publish: + runs-on: docker-desktop + env: + EVENT_NAME: ${{ forgejo.event_name }} + REGISTRY_HOST: ${{ vars.REGISTRY_HOST }} + REGISTRY_USERNAME: ${{ vars.REGISTRY_USERNAME }} + IMAGE_REPO: cliper/hugo-deployment + IMAGE_TAG: ${{ inputs.image_tag != '' && inputs.image_tag || forgejo.sha }} + PUBLISH_LATEST: ${{ inputs.publish_latest }} + steps: + - name: Checkout repository + uses: https://data.forgejo.org/actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 + + - name: Validate registry configuration + env: + REGISTRY_PASSWORD: ${{ secrets.REGISTRY_PASSWORD }} + run: | + set -euo pipefail + test -n "${REGISTRY_HOST:-}" || { echo "Missing REGISTRY_HOST" >&2; exit 1; } + test -n "${REGISTRY_USERNAME:-}" || { echo "Missing REGISTRY_USERNAME" >&2; exit 1; } + test -n "${REGISTRY_PASSWORD:-}" || { echo "Missing REGISTRY_PASSWORD" >&2; exit 1; } + + - name: Validate image tag + run: | + set -euo pipefail + printf '%s' "$IMAGE_TAG" | grep -Eq '^[A-Za-z0-9_][A-Za-z0-9_.-]{0,127}$' || { + echo "Invalid IMAGE_TAG: $IMAGE_TAG" >&2 + exit 1 + } + + - name: Log in to registry + env: + REGISTRY_PASSWORD: ${{ secrets.REGISTRY_PASSWORD }} + run: | + set -euo pipefail + printf '%s' "$REGISTRY_PASSWORD" | docker login "$REGISTRY_HOST" -u "$REGISTRY_USERNAME" --password-stdin + + - name: Build image + run: | + set -euo pipefail + docker build \ + -t "${REGISTRY_HOST}/${IMAGE_REPO}:${IMAGE_TAG}" \ + -t "${REGISTRY_HOST}/${IMAGE_REPO}:latest" \ + . + + - name: Push image tags + run: | + set -euo pipefail + docker push "${REGISTRY_HOST}/${IMAGE_REPO}:${IMAGE_TAG}" + if [ "${EVENT_NAME:-}" = "push" ] || [ "${PUBLISH_LATEST:-false}" = "true" ]; then + docker push "${REGISTRY_HOST}/${IMAGE_REPO}:latest" + fi diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..d91cf0e --- /dev/null +++ b/Dockerfile @@ -0,0 +1,28 @@ +FROM debian:bookworm-slim + +ARG HUGO_VERSION=0.160.0 + +RUN apt-get update \ + && apt-get install -y --no-install-recommends \ + ca-certificates \ + curl \ + git \ + rsync \ + openssh-client \ + tar \ + && rm -rf /var/lib/apt/lists/* + +RUN case "$(dpkg --print-architecture)" in \ + amd64) asset="hugo_extended_${HUGO_VERSION}_linux-amd64.tar.gz" ;; \ + arm64) asset="hugo_extended_${HUGO_VERSION}_linux-arm64.tar.gz" ;; \ + *) echo "Unsupported architecture: $(dpkg --print-architecture)" >&2; exit 1 ;; \ + esac \ + && curl -fsSLo /tmp/hugo-checksums.txt "https://github.com/gohugoio/hugo/releases/download/v${HUGO_VERSION}/hugo_${HUGO_VERSION}_checksums.txt" \ + && curl -fsSLo /tmp/hugo.tar.gz "https://github.com/gohugoio/hugo/releases/download/v${HUGO_VERSION}/${asset}" \ + && grep " ${asset}$" /tmp/hugo-checksums.txt > /tmp/hugo.tar.gz.sha256 \ + && sha256sum -c /tmp/hugo.tar.gz.sha256 \ + && tar -xzf /tmp/hugo.tar.gz -C /tmp hugo \ + && install -m 0755 /tmp/hugo /usr/local/bin/hugo \ + && rm -rf /tmp/hugo /tmp/hugo.tar.gz /tmp/hugo-checksums.txt /tmp/hugo.tar.gz.sha256 + +WORKDIR /workspace diff --git a/README.md b/README.md index e69de29..4e741c7 100644 --- a/README.md +++ b/README.md @@ -0,0 +1,73 @@ +# Hugo Deployment + +Shared Forgejo deployment image scaffold for Develton-managed Hugo sites. + +## Contents + +- Prebuilt deployment image with Hugo, rsync, and OpenSSH client tools +- Forgejo workflow that publishes versioned and `latest` image tags +- Image publish path intended for `registry.develton.com` + +## Prerequisites + +- Repository must be public if other repos call the reusable workflow directly +- Forgejo variables: `REGISTRY_HOST`, `REGISTRY_USERNAME` +- Forgejo secrets: `REGISTRY_PASSWORD` +- The image was smoke-tested locally, and `hugo version`, `rsync --version`, and `ssh -V` were confirmed successfully. + +## Sites + +- `neilb.com` +- `boholtoday.com` +- `boholcentral.com` +- `cloud.develton.com` +- `komsysnet.com` +- `thefreelancemastery.com` + +## Image Publishing + +The publish workflow runs on `docker-desktop` and publishes the versioned tag on every run. + +- Pushes to `main` publish `registry.develton.com/cliper/hugo-deployment:` and also move `latest` +- Manual runs can override the tag with `image_tag` +- Manual runs do not move `latest` unless `publish_latest` is set to `true` + +It pushes: + +- `registry.develton.com/cliper/hugo-deployment:` +- `registry.develton.com/cliper/hugo-deployment:latest` + +Required variables and secrets: + +- `REGISTRY_HOST` +- `REGISTRY_USERNAME` +- `REGISTRY_PASSWORD` + +Workflow inputs: + +- `image_tag` +- `publish_latest` + +## Reusable Workflow + +Site repos call the shared workflow with: + +```yaml +jobs: + deploy: + uses: cliper/hugo_deployment/.forgejo/workflows/deploy-hugo-site.yml@v1 + with: + deploy_script: ./scripts/deploy.sh + secrets: + deploy_ssh_key: ${{ secrets.DEPLOY_SSH_KEY }} +``` + +Notes: + +- For the current caller pattern, `DEPLOY_SSH_HOST`, `DEPLOY_SSH_USER`, and `DEPLOY_PATH` are required repo variables unless the caller explicitly passes host and user as workflow inputs +- Caller repos can set `DEPLOY_KNOWN_HOSTS` in repo variables with a pinned `known_hosts` entry for stronger host verification +- If `DEPLOY_KNOWN_HOSTS` is not set, the workflow falls back to `ssh-keyscan` so current repos keep working +- The reusable workflow itself always runs on `docker-desktop` +- The reusable workflow resolves its container image as `registry.develton.com/cliper/hugo-deployment:v1`, so caller repos do not need `REGISTRY_HOST` just to start the job +- Because the reusable workflow job container is resolved before checkout, callers must not switch over until `registry.develton.com/cliper/hugo-deployment:v1` has been published and verified pullable by the runner +- The reusable workflow is currently pinned to `registry.develton.com/cliper/hugo-deployment:v1`