diff --git a/.mk/.mk-common-version b/.mk/.mk-common-version new file mode 100644 index 0000000..1a56440 --- /dev/null +++ b/.mk/.mk-common-version @@ -0,0 +1 @@ +leinardi/make-common@v1 a5d37077c784b7e0ded1faf17ce70ae97ec68f8c diff --git a/.mk/help.mk b/.mk/help.mk new file mode 100644 index 0000000..82e4619 --- /dev/null +++ b/.mk/help.mk @@ -0,0 +1,16 @@ +ifndef MK_COMMON_HELP_INCLUDED +MK_COMMON_HELP_INCLUDED := 1 + +# Only set default if not already set +ifeq ($(.DEFAULT_GOAL),) + .DEFAULT_GOAL := help +endif + +.PHONY: help +help: ## Show this help and usage + @awk 'BEGIN {FS=":.*##"} /^[a-zA-Z0-9_\/-]+:.*##/ { printf "%s:%s\n", $$1, $$2 }' $(MAKEFILE_LIST) \ + | sort \ + | awk 'BEGIN {FS=":"; print "\nTargets:"} { printf " \033[36m%-24s\033[0m %s\n", $$1, $$2 }' + @echo "" + +endif # MK_COMMON_HELP_INCLUDED diff --git a/.mk/password.mk b/.mk/password.mk new file mode 100644 index 0000000..73f34a8 --- /dev/null +++ b/.mk/password.mk @@ -0,0 +1,9 @@ +ifndef MK_COMMON_PASSWORD_INCLUDED +MK_COMMON_PASSWORD_INCLUDED := 1 + +.PHONY: password +password: ## Generate a 99-character password (PostgreSQL compatible) + @echo "Generating a 99-character password (PostgreSQL compatible)..." + @env LC_ALL=C tr -dc 'A-Za-z0-9_.-+=,' < /dev/urandom | head -c 99; echo + +endif # MK_COMMON_PASSWORD_INCLUDED diff --git a/.mk/pre-commit.mk b/.mk/pre-commit.mk new file mode 100644 index 0000000..501ee62 --- /dev/null +++ b/.mk/pre-commit.mk @@ -0,0 +1,32 @@ +ifndef MK_COMMON_PRECOMMIT_INCLUDED +MK_COMMON_PRECOMMIT_INCLUDED := 1 + +.PHONY: check +check: check-installed-pre-commit ## Run pre-commit on all files + @pre-commit run --all-files + +.PHONY: check-stage +check-stage: check-installed-pre-commit ## Run pre-commit on the current staging area + @echo "Running pre-commit on current staging area..." + @pre-commit run + +.PHONY: pre-commit-install +pre-commit-install: check-installed-pre-commit ## Install pre-commit git hook in this repo + @pre-commit install + +.PHONY: pre-commit-autoupdate +pre-commit-autoupdate: check-installed-pre-commit ## Update pre-commit hook versions (immediate) + @pre-commit autoupdate + +.PHONY: check-installed-pre-commit +check-installed-pre-commit: # Verify that pre-commit is installed, print install help otherwise + @if ! command -v pre-commit >/dev/null 2>&1; then \ + echo "Error: pre-commit is not installed."; \ + echo "Install it with:"; \ + echo " macOS: brew install pre-commit"; \ + echo " Ubuntu: sudo apt install pre-commit"; \ + echo " (or) pipx install pre-commit"; \ + exit 1; \ + fi + +endif # MK_COMMON_PRECOMMIT_INCLUDED diff --git a/Makefile b/Makefile index c56a02f..c3097be 100644 --- a/Makefile +++ b/Makefile @@ -1,102 +1,41 @@ -# https://www.gnu.org/software/make/manual/html_node/Phony-Targets.html -.PHONY: check -check: check-installed-pre-commit - @pre-commit run --all-files +# Resolve repository root (Makefile can live anywhere) +REPO_ROOT := $(shell git rev-parse --show-toplevel 2>/dev/null || pwd) -.PHONY: check-stage -check-stage: check-installed-pre-commit - @echo "Running pre-commit on current staging area..." - pre-commit run +MK_COMMON_REPO ?= leinardi/make-common +MK_COMMON_VERSION ?= v1 -.PHONY: check-master-changes -check-master-changes: check-installed-pre-commit - @REF=; \ - if git rev-parse --verify --quiet origin/master >/dev/null && \ - git merge-base --is-ancestor origin/master HEAD; then \ - REF=origin/master; \ - elif git rev-parse --verify --quiet master >/dev/null && \ - git merge-base --is-ancestor master HEAD; then \ - REF=master; \ - else \ - echo "Error: neither origin/master nor master is an ancestor of HEAD." >&2; \ - exit 1; \ - fi; \ - echo "Running pre-commit on new commits since $$REF..."; \ - echo "pre-commit run --from-ref $$REF --to-ref HEAD"; \ - pre-commit run --from-ref $$REF --to-ref HEAD - -# Task to run shellcheck on all shell scripts -.PHONY: shellcheck -shellcheck: check-installed-pre-commit check-installed-shellcheck - @echo "Running shellcheck..." - pre-commit run shellcheck --all-files - -# Task to run prettier on all .yaml and .yaml files -.PHONY: prettier -prettier: check-installed-pre-commit - pre-commit run prettier-yaml --all-files - -# Task to run ansible-lint on all playbooks after prettier -.PHONY: ansible-lint -ansible-lint: check-installed-pre-commit - @echo "Running ansible-lint..." - pre-commit run ansible-lint --all-files - -.PHONY: actionlint -actionlint: check-installed-pre-commit - @echo "Running actionlint..." - pre-commit run actionlint --all-files +MK_COMMON_DIR := $(REPO_ROOT)/.mk +MK_COMMON_FILES := help.mk pre-commit.mk password.mk -# Task to run yamllint on all playbooks after prettier -.PHONY: yamllint -yamllint: check-installed-pre-commit - @echo "Running yamllint..." - pre-commit run yamllint --all-files +MK_COMMON_BOOTSTRAP_SCRIPT := $(REPO_ROOT)/scripts/bootstrap-mk-common.sh -.PHONY: check-installed-shellcheck -check-installed-shellcheck: - @if ! command -v shellcheck >/dev/null 2>&1; then \ - echo "Error: shellcheck is not installed."; \ - echo "Install it with:"; \ - echo " macOS: brew install shellcheck"; \ - echo " Ubuntu: sudo apt-get install -y shellcheck"; \ - exit 1; \ - fi +# Bootstrap: the script will self-update and fetch the selected .mk snippets +MK_COMMON_BOOTSTRAP := $(shell "$(MK_COMMON_BOOTSTRAP_SCRIPT)" \ + "$(MK_COMMON_REPO)" \ + "$(MK_COMMON_VERSION)" \ + "$(MK_COMMON_DIR)" \ + "$(MK_COMMON_FILES)") -.PHONY: check-installed-prettier -check-installed-prettier: - @if ! command -v prettier >/dev/null 2>&1; then \ - echo "Error: prettier is not installed."; \ - echo "Install it with:"; \ - echo " macOS: brew install prettier"; \ - echo " Ubuntu: npm install --global prettier"; \ - exit 1; \ - fi +# Include shared make logic +include $(addprefix $(MK_COMMON_DIR)/,$(MK_COMMON_FILES)) -.PHONY: check-installed-pre-commit -check-installed-pre-commit: - @if ! command -v pre-commit >/dev/null 2>&1; then \ - echo "Error: pre-commit is not installed."; \ - echo "Install it with:"; \ - echo " macOS: brew install pre-commit"; \ - echo " Ubuntu: pip install pre-commit"; \ - exit 1; \ - fi +.PHONY: mk-common-update +mk-common-update: ## Check for remote updates of shared .mk files + @echo "[mk] Checking for updates from $(MK_COMMON_REPO)@$(MK_COMMON_VERSION)" + MK_COMMON_UPDATE=1 "$(MK_COMMON_BOOTSTRAP_SCRIPT)" \ + "$(MK_COMMON_REPO)" \ + "$(MK_COMMON_VERSION)" \ + "$(MK_COMMON_DIR)" \ + "$(MK_COMMON_FILES)" -.PHONY: install-pre-commit -install-pre-commit: check-installed-pre-commit - @echo "Setting up pre-commit..." - pre-commit install - -# Task to generate inventory/group_vars/all.yaml .PHONY: generate-group-vars -generate-group-vars: check-installed-pre-commit +generate-group-vars: check-installed-pre-commit ## Generate inventory/group_vars/all.yaml @echo "Generating inventory/group_vars/all.yaml..." pre-commit run generate-group-vars --all-files -# Task to run ansible-playbook with optional parameters like TAGS, LIMIT, EXTRA_VARS, OTHER_PARAMS + .PHONY: install -install: check-ansible +install: check-ansible ## Run ansible-playbook with optional parameters @echo "Running ansible-playbook with optional parameters..." ansible-playbook playbooks/macos-setup.yaml --ask-become-pass $(strip \ $(if $(TAGS),--tags=$(TAGS)) \ @@ -104,9 +43,8 @@ install: check-ansible $(if $(EXTRA_VARS),--extra-vars="$(EXTRA_VARS)") \ $(if $(OTHER_PARAMS),$(OTHER_PARAMS))) -# Check if ansible is installed, and run the ansible installation script if not .PHONY: check-ansible -check-ansible: +check-ansible: # Check if ansible is installed, and run the ansible installation script if not @if ! command -v ansible >/dev/null 2>&1; then \ echo "Ansible is not installed, running setup script..."; \ ./install_ansible.sh; \ diff --git a/scripts/bootstrap-mk-common.sh b/scripts/bootstrap-mk-common.sh new file mode 100755 index 0000000..d75688b --- /dev/null +++ b/scripts/bootstrap-mk-common.sh @@ -0,0 +1,123 @@ +#!/usr/bin/env bash +set -euo pipefail + +MK_REPO="${1:?MK repo (e.g. leinardi/make-common) required}" +VERSION="${2:?version (e.g. v1.2.0 or latest) required}" +MK_DIR="${3:?target .mk directory required}" +FILES="${4:?list of .mk files required}" + +# Mode: normal bootstrap vs update +MK_UPDATE_MODE="${MK_COMMON_UPDATE:-0}" + +# Resolve repo root (so Makefile can live anywhere in the repo) +REPO_ROOT="$(git rev-parse --show-toplevel 2>/dev/null || pwd)" + +SCRIPT_PATH="${REPO_ROOT}/scripts/bootstrap-mk-common.sh" + +# Normalise MK_DIR and derive version file +MK_DIR="${MK_DIR%/}" +MK_VERSION_FILE="${MK_DIR}/.mk-common-version" +EXPECTED="${MK_REPO}@${VERSION}" + +NEED_REFRESH=0 +STORED_EXPECTED="" +STORED_SHA="" +REMOTE_SHA="" + +# Read existing version file, if any: format "@ [sha]" +if [[ -f "${MK_VERSION_FILE}" ]]; then + IFS=' ' read -r STORED_EXPECTED STORED_SHA < "${MK_VERSION_FILE}" || true +fi + +if [[ "${MK_UPDATE_MODE}" = "1" ]]; then + # --------------------------------------------------------------------------- + # UPDATE MODE: go online, compare remote SHA, refresh only if changed + # --------------------------------------------------------------------------- + REMOTE_URL="https://github.com/${MK_REPO}.git" + REMOTE_SHA="$(git ls-remote "${REMOTE_URL}" "${VERSION}" 2>/dev/null | awk 'NR==1 {print $1}')" + + if [[ -z "${REMOTE_SHA}" ]]; then + echo "[mk] ERROR: could not resolve '${VERSION}' in ${REMOTE_URL}" >&2 + echo "[mk] Check MK_COMMON_VERSION or your network connection." >&2 + exit 1 + fi + + # Decide if we need to refresh: + # - tag name changed + # - or no stored SHA (first time / old format) + # - or stored SHA != remote SHA (mutable tag/branch moved) + if [[ "${STORED_EXPECTED}" != "${EXPECTED}" ]] \ + || [[ -z "${STORED_SHA}" ]] \ + || [[ "${STORED_SHA}" != "${REMOTE_SHA}" ]]; then + NEED_REFRESH=1 + fi + + if [[ "${NEED_REFRESH}" -eq 0 ]]; then + echo "[mk] Shared makefiles already up to date for ${EXPECTED} (${REMOTE_SHA})" >&2 + exit 0 + fi +else + # --------------------------------------------------------------------------- + # NORMAL MODE: only compare tag string + ensure files exist + # --------------------------------------------------------------------------- + if [[ -z "${STORED_EXPECTED}" ]] || [[ "${STORED_EXPECTED}" != "${EXPECTED}" ]]; then + NEED_REFRESH=1 + fi + + # Also refresh if any requested .mk file is missing + if [[ "${NEED_REFRESH}" -eq 0 ]]; then + for f in ${FILES}; do + if [[ ! -f "${MK_DIR}/${f}" ]]; then + NEED_REFRESH=1 + break + fi + done + fi +fi + +# --------------------------------------------------------------------------- +# Refresh: update script + .mk files, write version file, re-exec +# --------------------------------------------------------------------------- + +if [[ "${NEED_REFRESH}" -eq 1 ]]; then + echo "[mk] Updating bootstrap-mk-common.sh and .mk files from ${MK_REPO}@${VERSION}" >&2 + mkdir -p "${REPO_ROOT}/scripts" "${MK_DIR}" + + # If we don't already know REMOTE_SHA (normal mode), resolve it now. + # This only happens when we are *already* going online to download files. + if [[ -z "${REMOTE_SHA}" ]]; then + REMOTE_URL="https://github.com/${MK_REPO}.git" + REMOTE_SHA="$(git ls-remote "${REMOTE_URL}" "${VERSION}" 2>/dev/null | awk 'NR==1 {print $1}')" + if [[ -z "${REMOTE_SHA}" ]]; then + echo "[mk] WARNING: could not resolve SHA for ${EXPECTED};" \ + "version file will not contain a SHA." >&2 + fi + fi + + # Fetch the script itself from the tagged ref + curl -fsSL \ + "https://raw.githubusercontent.com/${MK_REPO}/${VERSION}/scripts/bootstrap-mk-common.sh" \ + -o "${SCRIPT_PATH}" + chmod +x "${SCRIPT_PATH}" + + # Fetch all requested .mk files + for f in ${FILES}; do + echo "[mk] Fetching ${f} from ${MK_REPO}@${VERSION}" >&2 + curl -fsSL \ + "https://raw.githubusercontent.com/${MK_REPO}/${VERSION}/.mk/${f}" \ + -o "${MK_DIR}/${f}" + done + + # Store "@ " (sha may be empty only if resolution failed) + if [[ -n "${REMOTE_SHA}" ]]; then + printf '%s %s\n' "${EXPECTED}" "${REMOTE_SHA}" > "${MK_VERSION_FILE}" + else + printf '%s\n' "${EXPECTED}" > "${MK_VERSION_FILE}" + fi + + # Re-exec the freshly downloaded script so any new logic applies immediately + exec "${SCRIPT_PATH}" "$@" +fi + +# If we reach here in normal mode, script + .mk files are already up to date. +# Nothing else to do; Make just needed us for our side effects.