diff --git a/.actrc b/.actrc new file mode 100644 index 0000000..06dda89 --- /dev/null +++ b/.actrc @@ -0,0 +1,2 @@ +--container-architecture=linux/amd64 +-P ubuntu-latest=catthehacker/ubuntu:act-latest diff --git a/.github/act/pull_request.json b/.github/act/pull_request.json new file mode 100644 index 0000000..07b36cd --- /dev/null +++ b/.github/act/pull_request.json @@ -0,0 +1,15 @@ +{ + "pull_request": { + "number": 1, + "head": { + "ref": "local-testing" + }, + "base": { + "ref": "main" + } + }, + "repository": { + "full_name": "local/cwms-cli", + "default_branch": "main" + } +} diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 25c2457..9b0683b 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -31,3 +31,38 @@ Formatting of code is done via black. You must ensure you have walked through th ## Code Review Create an [issue](https://github.com/hydrologicengineeringcenter/cwms-cli/issues) using one of the templates. Then from that issue on the sidebar select "create branch from issue". This ensures branches are linked to issues and issues are properly closed when resolved. Leaving no orphaned issues. + +## Run GitHub Actions locally + +You can rehearse the main GitHub Actions workflows locally with +[`act`](https://github.com/nektos/act) before pushing a branch. + + +This is **NOT** required and is more of a convenience to test locally before waiting for the actions to run it the first time/subsequent times. + +### Prerequisites: + +- `act` +- Docker + +### Repo-local setup is already included: + +- `.actrc` configures the default `ubuntu-latest` runner image +- `.github/act/pull_request.json` provides a local pull request event payload +- `scripts/run-local-actions.sh` wraps the common workflows + +### Examples: + +```sh +scripts/run-local-actions.sh cli-tests +scripts/run-local-actions.sh code-check +scripts/run-local-actions.sh docs +scripts/run-local-actions.sh all +``` + +### Notes: + +- When you run `run-local-actions.sh` it will prompt if you would like to install `act`. It assumes you have docker installed already. +- Deployment-oriented workflows such as PyPI publish are not included in the wrapper because they rely on GitHub-hosted credentials and release context. +- You can pass through extra `act` arguments. Example: `scripts/run-local-actions.sh docs -j html` + diff --git a/scripts/run-local-actions.sh b/scripts/run-local-actions.sh new file mode 100755 index 0000000..b51cc00 --- /dev/null +++ b/scripts/run-local-actions.sh @@ -0,0 +1,220 @@ +#!/usr/bin/env bash +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +REPO_ROOT="$(cd "${SCRIPT_DIR}/.." && pwd)" +EVENT_FILE="${REPO_ROOT}/.github/act/pull_request.json" +ACT_INSTALL_DIR="/usr/local/bin" +REPO_BIN_DIR="${REPO_ROOT}/bin" +BASH_PROFILE="${HOME}/.bash_profile" + +usage() { + cat <<'EOF' +Usage: + scripts/run-local-actions.sh [act args...] + +Targets: + cli-tests Run .github/workflows/cli-tests.yml + code-check Run .github/workflows/code-check.yml + docs Run .github/workflows/docs.yml + all Run the three local-safe workflows above + clean Remove local act runner image and cache directories + list Show act workflows + +Examples: + scripts/run-local-actions.sh cli-tests + scripts/run-local-actions.sh code-check --verbose + scripts/run-local-actions.sh docs -j html + scripts/run-local-actions.sh clean + +EOF +} + +run_act() { + local workflow="$1" + shift + act pull_request -W "${workflow}" -e "${EVENT_FILE}" "$@" +} + +cleanup_act_artifacts() { + echo "Cleaning act Docker image and local caches..." + + cleanup_act_containers + docker image rm -f catthehacker/ubuntu:act-latest >/dev/null 2>&1 || true + rm -rf "${HOME}/.cache/act" "${HOME}/.cache/actcache" + + echo "Cleanup complete." +} + +cleanup_act_containers() { + local container_ids + container_ids="$(docker ps -aq --filter "name=act-")" + if [[ -n "${container_ids}" ]]; then + echo "Removing leftover act containers..." + # shellcheck disable=SC2086 + docker rm -f ${container_ids} >/dev/null 2>&1 || true + fi +} + +maybe_cleanup_after_run() { + local exit_code=$? + if [[ "${target:-}" != "clean" ]] && [[ "${target:-}" != "list" ]] && [[ -n "${target:-}" ]]; then + cleanup_act_containers + fi + return "${exit_code}" +} + +detect_os_name() { + if [[ -r /etc/os-release ]]; then + # shellcheck disable=SC1091 + . /etc/os-release + if [[ -n "${PRETTY_NAME:-}" ]]; then + printf '%s\n' "${PRETTY_NAME}" + return + fi + if [[ -n "${NAME:-}" ]]; then + printf '%s\n' "${NAME}" + return + fi + fi + uname -s +} + +ensure_path_in_profile() { + local dir="$1" + local path_line="export PATH=\"${dir}:\$PATH\"" + touch "${BASH_PROFILE}" + if ! grep -Fqx "${path_line}" "${BASH_PROFILE}"; then + { + echo "" + echo "# Added by cwms-cli local GitHub Actions helper" + echo "${path_line}" + } >> "${BASH_PROFILE}" + echo "Added ${dir} to ${BASH_PROFILE}" + fi +} + +prompt_install_act() { + local os_name="$1" + local prompt + prompt="act is not installed. Would you like to install act for ${os_name} into ${ACT_INSTALL_DIR}? [y/N] " + read -r -p "${prompt}" reply + case "${reply}" in + y|Y|yes|YES) + return 0 + ;; + *) + return 1 + ;; + esac +} + +install_act() { + local os_name="$1" + + if ! prompt_install_act "${os_name}"; then + echo "act is required to run local GitHub Actions workflows." >&2 + exit 1 + fi + + echo "Installing act for ${os_name} into ${ACT_INSTALL_DIR}..." + curl --proto '=https' --tlsv1.2 -sSf \ + https://raw.githubusercontent.com/nektos/act/master/install.sh | sudo bash -s -- -b "${ACT_INSTALL_DIR}" + + hash -r + + if command -v act >/dev/null 2>&1; then + echo "act installed successfully." + return 0 + fi + + if [[ -x "${ACT_INSTALL_DIR}/act" ]]; then + export PATH="${ACT_INSTALL_DIR}:$PATH" + hash -r + if command -v act >/dev/null 2>&1; then + ensure_path_in_profile "${ACT_INSTALL_DIR}" + echo "act installed successfully." + return 0 + fi + fi + + if [[ -x "${REPO_BIN_DIR}/act" ]]; then + export PATH="${REPO_BIN_DIR}:$PATH" + hash -r + if command -v act >/dev/null 2>&1; then + ensure_path_in_profile "${REPO_BIN_DIR}" + echo "Found act in ${REPO_BIN_DIR} and added it to your PATH." + echo "act installed successfully." + return 0 + fi + fi + + if [[ ! -x "${ACT_INSTALL_DIR}/act" ]] && [[ ! -x "${REPO_BIN_DIR}/act" ]] && ! command -v act >/dev/null 2>&1; then + echo "act installation completed, but the binary was not found in ${ACT_INSTALL_DIR}, ${REPO_BIN_DIR}, or PATH." >&2 + echo "Check the install output above and verify where the binary was placed." >&2 + exit 1 + fi + + echo "act installed successfully." +} + +target="${1:-}" + +if [[ -z "${target}" ]]; then + usage + exit 1 +fi + +shift || true + +cd "${REPO_ROOT}" + +case "${target}" in + -h|--help|help) + usage + exit 0 + ;; + cli-tests|code-check|docs|all|list|clean) + ;; + *) + echo "Unknown target: ${target}" >&2 + usage + exit 1 + ;; +esac + +if ! command -v act >/dev/null 2>&1; then + install_act "$(detect_os_name)" +fi + +if ! command -v docker >/dev/null 2>&1; then + echo "Missing dependency: docker" >&2 + echo "act requires Docker to run these workflows locally." >&2 + exit 1 +fi + +trap maybe_cleanup_after_run EXIT + +case "${target}" in + cli-tests) + run_act ".github/workflows/cli-tests.yml" "$@" + ;; + code-check) + run_act ".github/workflows/code-check.yml" "$@" + ;; + docs) + run_act ".github/workflows/docs.yml" "$@" + ;; + all) + run_act ".github/workflows/cli-tests.yml" "$@" + run_act ".github/workflows/code-check.yml" "$@" + run_act ".github/workflows/docs.yml" "$@" + ;; + clean) + cleanup_act_artifacts + exit 0 + ;; + list) + act --list + ;; +esac