From 4c489fe5f2a0a362057f4a4202776380d553c772 Mon Sep 17 00:00:00 2001 From: "Max P." Date: Wed, 9 Jul 2025 21:28:30 +0200 Subject: [PATCH] feat(action): add Markdown to HTML publishing workflow - Introduce a GitHub Action to convert Markdown files to HTML using Pandoc - Configure steps for repository checkout, git setup, and branch management - Include scripts for Markdown conversion, Pandoc installation, and git setup - Automate the publishing of HTML output to a specified branch --- action.yml | 85 +++++++++++++++++++++ scripts/convert-md-tree.sh | 99 +++++++++++++++++++++++++ scripts/install-pandoc.sh | 147 +++++++++++++++++++++++++++++++++++++ scripts/setup-git.sh | 29 ++++++++ 4 files changed, 360 insertions(+) create mode 100644 action.yml create mode 100644 scripts/convert-md-tree.sh create mode 100755 scripts/install-pandoc.sh create mode 100755 scripts/setup-git.sh diff --git a/action.yml b/action.yml new file mode 100644 index 0000000..e55d1b8 --- /dev/null +++ b/action.yml @@ -0,0 +1,85 @@ +name: "Markdown → HTML Publisher" +description: > + Converts all *.md files in the given path (recursively) to HTML via Pandoc + and commits the result into the specified branch as an orphan branch, + replacing the entire branch content and removing history. + +inputs: + path: + description: "Directory to scan for Markdown files (default: $GITHUB_WORKSPACE)" + required: false + branch: + description: "Target branch that will receive the generated HTML" + required: false + default: pages + +runs: + using: composite + steps: + # 0) Checkout repository with full history so we can delete/reset branches + - name: Checkout repository + uses: actions/checkout@v4 + with: + fetch-depth: 0 + + # 1) Configure git author (uses default values if none supplied) + - name: Setup git author + shell: bash + run: | + bash "${{ github.action_path }}/scripts/setup-git.sh" + + # 2) Install or ensure pandoc is present + - name: Install pandoc + shell: bash + run: | + bash "${{ github.action_path }}/scripts/install-pandoc.sh" + + # 3) Convert Markdown files to HTML into a temporary directory + - name: Convert Markdown tree + shell: bash + run: | + SRC_DIR="${{ inputs.path }}" + [[ -z "$SRC_DIR" ]] && SRC_DIR="$GITHUB_WORKSPACE" + TMP_DIR="$(mktemp -d)" + bash "${{ github.action_path }}/scripts/convert-md-tree.sh" "$SRC_DIR" "$TMP_DIR" + echo "TMP_DIR=$TMP_DIR" >> "$GITHUB_ENV" + + # 4) Delete target branch (if it exists) and create new orphan branch + - name: Recreate orphan target branch + shell: bash + run: | + TARGET_BRANCH="${{ inputs.branch }}" + [[ -z "$TARGET_BRANCH" ]] && TARGET_BRANCH="pages" + + if git show-ref --quiet "refs/heads/$TARGET_BRANCH"; then + git branch -D "$TARGET_BRANCH" + fi + + git switch --orphan "$TARGET_BRANCH" + git rm -rf . || true + + echo "TARGET_BRANCH=$TARGET_BRANCH" >> "$GITHUB_ENV" + + # 5) Replace contents of the branch with generated HTML + - name: Replace branch contents + shell: bash + run: | + shopt -s dotglob + rm -rf ./* || true + cp -r "$TMP_DIR"/. . + rm -rf "$TMP_DIR" + + # 6) Commit and push changes if there are any + - name: Commit and push changes + shell: bash + run: | + TARGET_BRANCH="${TARGET_BRANCH:-${{ inputs.branch }}}" + [[ -z "$TARGET_BRANCH" ]] && TARGET_BRANCH="pages" + if git status --porcelain | grep -q . + then + git add . + git commit -m "Automatic HTML publish: $(date -u +'%Y-%m-%d %H:%M:%S UTC')" + git push --force origin "$TARGET_BRANCH" + else + echo "ℹ️ No changes to commit." + fi diff --git a/scripts/convert-md-tree.sh b/scripts/convert-md-tree.sh new file mode 100644 index 0000000..420d891 --- /dev/null +++ b/scripts/convert-md-tree.sh @@ -0,0 +1,99 @@ +#!/usr/bin/env bash +#=== HELP START === +# convert-md-tree.sh – Recursive Markdown → HTML conversion (Pandoc) +# +# Usage: +# ./convert-md-tree.sh [options] +# +# Options: +# -n, --dry-run Show what would be done without converting +# -p, --pandoc-args Extra arguments passed to pandoc (e.g. "-t html5") +# -h, --help Show this help message +# +# Arguments: +# Root directory containing .md files +# Destination directory for the HTML output +# +# Behaviour: +# • Preserves the directory structure in the destination. +# • Creates required sub-directories automatically. +# • Emits clear errors for missing programs or invalid parameters. +#=== HELP END === +set -euo pipefail + +# ─────────────────────────────────────────────── +# Defaults +DRY_RUN=false +PANDOC_ARGS="" +# ─────────────────────────────────────────────── + +# 0 Help function ------------------------------------------------------------- +show_help() { + sed -n '/^#=== HELP START ===/,/^#=== HELP END ===/ { + /^#=== HELP START ===/d + /^#=== HELP END ===/d + s/^#// + p + }' "$0" +} + +# 1 Parse options ------------------------------------------------------------- +POSITIONAL=() +while [[ $# -gt 0 ]]; do + case "$1" in + -h|--help) show_help; exit 0 ;; + -n|--dry-run) DRY_RUN=true; shift ;; + -p|--pandoc-args) + [[ $# -lt 2 ]] && { echo "❌ Option $1 requires an argument"; exit 1; } + PANDOC_ARGS="$2"; shift 2 ;; + --) shift; break ;; + -*) + echo "❌ Unknown option: $1"; exit 1 ;; + *) POSITIONAL+=("$1"); shift ;; + esac +done +set -- "${POSITIONAL[@]}" + +[[ $# -lt 2 ]] && { echo "❌ Missing and/or "; echo; show_help; exit 1; } + +SRC="$1" +DEST="$2" + +# 2 Validate arguments -------------------------------------------------------- +[[ ! -d "$SRC" ]] && { echo "❌ Source directory does not exist: $SRC"; exit 1; } + +mkdir -p "$DEST" || { echo "❌ Cannot create/access dest dir: $DEST"; exit 1; } + +# Remove trailing slash for path manipulation +SRC="${SRC%/}" + +# 3 Check required tools ------------------------------------------------------ +need() { command -v "$1" >/dev/null || { echo "❌ $1 is missing"; exit 1; }; } +need pandoc; need find; need mkdir + +# 4 Process files ------------------------------------------------------------- +echo "🔍 Scanning for .md files in $SRC …" +COUNT_TOTAL=$(find "$SRC" -type f -name '*.md' | wc -l | tr -d ' ') +[[ "$COUNT_TOTAL" -eq 0 ]] && { echo "ℹ️ No Markdown files found – nothing to do."; exit 0; } +echo "📄 Found $COUNT_TOTAL Markdown file(s)." + +CONVERTED=0 +while IFS= read -r -d '' FILE; do + REL_PATH="${FILE#$SRC/}" + OUT_FILE="$DEST/${REL_PATH%.md}.html" + mkdir -p "$(dirname "$OUT_FILE")" + + if $DRY_RUN; then + echo "🧪 Would convert: $FILE -> $OUT_FILE" + else + pandoc $PANDOC_ARGS "$FILE" -s -o "$OUT_FILE" + echo "✅ Converted: $FILE -> $OUT_FILE" + fi + ((CONVERTED++)) +done < <(find "$SRC" -type f -name '*.md' -print0) + +if $DRY_RUN; then + echo "🧪 Dry-run complete – $CONVERTED file(s) would be processed." +else + echo "🏁 Finished – $CONVERTED file(s) converted." +fi diff --git a/scripts/install-pandoc.sh b/scripts/install-pandoc.sh new file mode 100755 index 0000000..fd1e05b --- /dev/null +++ b/scripts/install-pandoc.sh @@ -0,0 +1,147 @@ +#!/usr/bin/env bash +#=== HELP START === +# install-pandoc.sh – Installs the Pandoc binary +# +# Usage: +# sudo ./install-pandoc.sh [options] [] +# +# Options: +# -a, --arch Target architecture (default: auto-detected) +# -d, --dir Installation directory (default: /usr/local/bin) +# -n, --dry-run Download and verify, but DO NOT install +# -h, --help Show this help message +# +# Arguments: +# Pandoc version to install (default: latest) +# +# Behaviour: +# • If pandoc is already in PATH and no version was requested, the script exits. +# • If a specific version was requested, the installed version is only upgraded if it differs. +#=== HELP END === +set -euo pipefail + +REPO="jgm/pandoc" + +# ─────────────────────────────────────────────── +# Defaults +INSTALL_DIR_DEFAULT="/usr/local/bin" +INSTALL_DIR="$INSTALL_DIR_DEFAULT" +ARCH_INPUT="" +VERSION="latest" +DRY_RUN=false +USER_VERSION_SPECIFIED=false +# ─────────────────────────────────────────────── + +# 0 Help function +show_help() { + sed -n '/^#=== HELP START ===/,/^#=== HELP END ===/ { + /^#=== HELP START ===/d + /^#=== HELP END ===/d + s/^#// + p + }' "$0" +} + +# 1 Parse options +POSITIONAL=() +while [[ $# -gt 0 ]]; do + case "$1" in + -h|--help) show_help; exit 0 ;; + -n|--dry-run) DRY_RUN=true; shift ;; + -a|--arch) + [[ $# -lt 2 ]] && { echo "❌ Option $1 requires an argument"; exit 1; } + ARCH_INPUT="$2"; shift 2 ;; + -d|--dir) + [[ $# -lt 2 ]] && { echo "❌ Option $1 requires an argument"; exit 1; } + INSTALL_DIR="$2"; shift 2 ;; + *) POSITIONAL+=("$1"); shift ;; + esac +done +if [[ ${#POSITIONAL[@]} -gt 0 ]]; then + VERSION="${POSITIONAL[0]}" + USER_VERSION_SPECIFIED=true +fi + +# 2 Detect / normalize architecture +detect_arch() { + case "$(uname -m)" in + x86_64) echo "linux-amd64" ;; + aarch64|arm64) echo "linux-arm64" ;; + *) echo "unsupported" ;; + esac +} + +ARCH_TAG="" +if [[ -n "$ARCH_INPUT" ]]; then + case "$ARCH_INPUT" in + amd64|linux-amd64) ARCH_TAG="linux-amd64" ;; + arm64|aarch64|linux-arm64) ARCH_TAG="linux-arm64" ;; + *) echo "❌ Unknown architecture: $ARCH_INPUT"; exit 1 ;; + esac +else + ARCH_TAG=$(detect_arch) + if [[ "$ARCH_TAG" == "unsupported" ]]; then + echo "❌ Automatic architecture detection failed. Please use -a." + exit 1 + fi +fi + +# 3 Pre-check existing installation +if command -v pandoc >/dev/null; then + INSTALLED_PATH=$(command -v pandoc) + INSTALLED_VER=$(pandoc -v | head -n1 | awk '{print $2}') + if ! $USER_VERSION_SPECIFIED; then + echo "✅ pandoc already present at ${INSTALLED_PATH} (version ${INSTALLED_VER}) – nothing to do." + exit 0 + else + if [[ "$INSTALLED_VER" == "$VERSION" ]]; then + echo "✅ pandoc ${INSTALLED_VER} already installed – requested version matches." + exit 0 + else + echo "ℹ️ pandoc ${INSTALLED_VER} installed, upgrading to ${VERSION}..." + fi + fi +fi + +# 4 Check required tools +need() { command -v "$1" >/dev/null || { echo "$1 is missing"; exit 1; }; } +need curl; need tar; need grep; need sed; need awk; need jq + +# 5 Fetch release info +if [[ "$VERSION" == "latest" ]]; then + API_URL="https://api.github.com/repos/${REPO}/releases/latest" +else + API_URL="https://api.github.com/repos/${REPO}/releases/tags/${VERSION}" +fi + +echo "🔍 Fetching release info (${API_URL})…" +JSON=$(curl -fsSL "$API_URL") || { echo "❌ Failed to fetch release info"; exit 1; } + +VERSION=$(jq -r '.tag_name' <<< "$JSON") || { echo "❌ Could not extract version"; exit 1; } + +ASSET_URL=$(jq -r '.assets[]?.browser_download_url' <<< "$JSON" | + grep -E "pandoc-.*-${ARCH_TAG}\.tar\.gz$" | head -n1) + +[[ -z "$ASSET_URL" ]] && { echo "❌ Matching asset not found for architecture ${ARCH_TAG}"; exit 1; } + +ASSET_FILE=$(basename "$ASSET_URL") +echo "📦 Downloading pandoc ${VERSION} (${ASSET_FILE}) …" +TMP=$(mktemp -d) +curl -#L -o "${TMP}/${ASSET_FILE}" "$ASSET_URL" + +# 6 Extract +tar -C "$TMP" -xzf "${TMP}/${ASSET_FILE}" + +# 7 Locate binary +BIN_PATH=$(find "$TMP" -type f -name pandoc -perm -u+x | head -n1) +[[ -z "$BIN_PATH" ]] && { echo "❌ Binary not found"; exit 1; } + +# 8 Install / Dry-run +if $DRY_RUN; then + echo "🧪 Dry-run: verified pandoc binary at ${BIN_PATH}" + echo "ℹ️ Would install to: ${INSTALL_DIR}/pandoc (sudo install -m755 …)" + echo "✅ pandoc $("$BIN_PATH" -v | head -n1) would be installed successfully" +else + sudo install -m755 "$BIN_PATH" "${INSTALL_DIR}/pandoc" + echo "✅ pandoc $(pandoc -v | head -n1) installed in ${INSTALL_DIR}" +fi diff --git a/scripts/setup-git.sh b/scripts/setup-git.sh new file mode 100755 index 0000000..35f57ea --- /dev/null +++ b/scripts/setup-git.sh @@ -0,0 +1,29 @@ +#!/usr/bin/env bash +set -euo pipefail + +# Optional inputs (positionals) or fallback to environment variables +AUTHOR_NAME="${1:-${CI_COMMIT_AUTHOR_NAME:-CI Bot}}" +AUTHOR_EMAIL="${2:-${CI_COMMIT_AUTHOR_EMAIL:-ci@bot.none}}" + +echo "🔧 Setting up git author:" +echo " Name : $AUTHOR_NAME" +echo " Email: $AUTHOR_EMAIL" + +git config --global user.name "$AUTHOR_NAME" +git config --global user.email "$AUTHOR_EMAIL" + +# Check if the values were set correctly +CONFIGURED_NAME=$(git config --global user.name) +CONFIGURED_EMAIL=$(git config --global user.email) + +if [[ "$CONFIGURED_NAME" != "$AUTHOR_NAME" ]]; then + echo "❌ Error: Git username was not set correctly!" >&2 + exit 1 +fi + +if [[ "$CONFIGURED_EMAIL" != "$AUTHOR_EMAIL" ]]; then + echo "❌ Error: Git email was not set correctly!" >&2 + exit 1 +fi + +echo "✅ Git configuration completed successfully."