From 4b07c233083389a62b512a8967f5b94adea4f8a4 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 23 Mar 2026 11:01:59 +0000 Subject: [PATCH 1/6] Initial plan From 36ac5af89fe11b6e537a0479d36a2ba9e41963e9 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 23 Mar 2026 11:05:20 +0000 Subject: [PATCH 2/6] Add run_workflows_locally.sh to run all GitHub Actions locally with act Co-authored-by: gensyn <36128035+gensyn@users.noreply.github.com> Agent-Logs-Url: https://github.com/gensyn/ssh_command/sessions/58c43d00-2112-458f-9c4b-351fbc5b5fc7 --- run_workflows_locally.sh | 264 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 264 insertions(+) create mode 100755 run_workflows_locally.sh diff --git a/run_workflows_locally.sh b/run_workflows_locally.sh new file mode 100755 index 0000000..4b32bcb --- /dev/null +++ b/run_workflows_locally.sh @@ -0,0 +1,264 @@ +#!/usr/bin/env bash +# run_workflows_locally.sh +# +# Runs all GitHub Actions workflows in this repository locally using Docker and +# act (https://github.com/nektos/act). +# +# Both tools are installed automatically if they are not already present. +# +# Usage: +# ./run_workflows_locally.sh [--include-release] +# +# --include-release Also run release.yaml (requires a GITHUB_TOKEN env var +# with write permissions to the repository and will upload +# artefacts to the real GitHub release – use with care). + +set -euo pipefail + +# ── Colour helpers ──────────────────────────────────────────────────────────── +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +BLUE='\033[0;34m' +BOLD='\033[1m' +NC='\033[0m' + +info() { echo -e "${BLUE}[INFO]${NC} $*"; } +success() { echo -e "${GREEN}[PASS]${NC} $*"; } +warn() { echo -e "${YELLOW}[WARN]${NC} $*"; } +error() { echo -e "${RED}[FAIL]${NC} $*"; } +header() { echo -e "\n${BOLD}$*${NC}"; } + +command_exists() { command -v "$1" &>/dev/null; } + +os_type() { + case "$OSTYPE" in + linux*) echo "linux" ;; + darwin*) echo "macos" ;; + *) echo "unknown" ;; + esac +} + +# ── Docker installation ─────────────────────────────────────────────────────── +install_docker() { + if command_exists docker; then + info "Docker is already installed: $(docker --version)" + return 0 + fi + + header "Installing Docker…" + case "$(os_type)" in + linux) + curl -fsSL https://get.docker.com | sudo sh + sudo usermod -aG docker "$USER" || true + warn "Docker installed. You may need to run 'newgrp docker' or re-login for group membership to take effect." + ;; + macos) + if command_exists brew; then + brew install --cask docker + info "Please launch Docker Desktop to start the daemon, then re-run this script." + exit 0 + else + error "Homebrew not found. Install Docker Desktop from https://www.docker.com/products/docker-desktop/ and re-run." + exit 1 + fi + ;; + *) + error "Unsupported OS '$OSTYPE'. Install Docker manually from https://docs.docker.com/engine/install/ and re-run." + exit 1 + ;; + esac +} + +# ── act installation ────────────────────────────────────────────────────────── +install_act() { + if command_exists act; then + info "act is already installed: $(act --version)" + return 0 + fi + + header "Installing act…" + case "$(os_type)" in + linux) + curl -fsSL https://raw.githubusercontent.com/nektos/act/master/install.sh \ + | sudo bash -s -- -b /usr/local/bin + ;; + macos) + if command_exists brew; then + brew install act + else + curl -fsSL https://raw.githubusercontent.com/nektos/act/master/install.sh \ + | sudo bash -s -- -b /usr/local/bin + fi + ;; + *) + error "Unsupported OS '$OSTYPE'. Install act manually from https://github.com/nektos/act and re-run." + exit 1 + ;; + esac +} + +# ── Docker daemon check ─────────────────────────────────────────────────────── +ensure_docker_running() { + if docker info &>/dev/null; then + return 0 + fi + + warn "Docker daemon is not running – attempting to start it…" + case "$(os_type)" in + linux) + if command_exists systemctl; then + sudo systemctl start docker + else + sudo service docker start + fi + sleep 3 + ;; + macos) + open -a Docker || true + info "Waiting up to 60 seconds for Docker Desktop to start…" + local i + for i in $(seq 1 12); do + sleep 5 + docker info &>/dev/null && return 0 + done + ;; + esac + + if ! docker info &>/dev/null; then + error "Docker daemon is still not running. Please start Docker manually and re-run this script." + exit 1 + fi +} + +# ── Workflow runner ─────────────────────────────────────────────────────────── + +# Ubuntu runner image used by act. The "act-latest" tag is a medium-sized +# image that supports most common Actions without requiring the 20 GB+ full +# image. +ACT_IMAGE="catthehacker/ubuntu:act-latest" + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +WORKFLOWS_DIR="$SCRIPT_DIR/.github/workflows" + +# run_workflow +# Returns 0 on success, 1 on failure. +run_workflow() { + local workflow_file="$1" + local event="$2" + local name + name="$(basename "$workflow_file")" + + info "Running [$name] with event '$event'…" + + if act "$event" \ + -W "$workflow_file" \ + -P "ubuntu-latest=$ACT_IMAGE" \ + --rm \ + 2>&1; then + success "$name passed" + return 0 + else + error "$name failed" + return 1 + fi +} + +run_all_workflows() { + local include_release="${1:-false}" + + # List of workflows and the event used to trigger each one locally. + # Parallel arrays: workflow_files[i] uses workflow_events[i]. + local workflow_files=( + "test.yml" + "pylint.yml" + "integration-tests.yml" + "hassfest.yaml" + "validate.yaml" + ) + local workflow_events=( + "push" + "push" + "push" + "push" + "workflow_dispatch" + ) + + local passed=() + local failed=() + local skipped=() + + # Optionally include the release workflow + if [[ "$include_release" == "true" ]]; then + workflow_files+=("release.yaml") + workflow_events+=("release") + warn "Including release.yaml – this uploads artefacts to a real GitHub release." + else + skipped+=("release.yaml (skipped by default – pass --include-release to run it)") + fi + + local i + for i in "${!workflow_files[@]}"; do + local workflow="${workflow_files[$i]}" + local event="${workflow_events[$i]}" + local workflow_path="$WORKFLOWS_DIR/$workflow" + + if [[ ! -f "$workflow_path" ]]; then + warn "Workflow file not found, skipping: $workflow" + skipped+=("$workflow (file not found)") + continue + fi + + if run_workflow "$workflow_path" "$event"; then + passed+=("$workflow") + else + failed+=("$workflow") + fi + done + + # ── Summary ─────────────────────────────────────────────────────────────── + header "══════════════════════════════════════════════" + header " Results" + header "══════════════════════════════════════════════" + + if [[ ${#passed[@]} -gt 0 ]]; then + success "Passed (${#passed[@]}): ${passed[*]}" + fi + if [[ ${#skipped[@]} -gt 0 ]]; then + warn "Skipped (${#skipped[@]}): ${skipped[*]}" + fi + if [[ ${#failed[@]} -gt 0 ]]; then + error "Failed (${#failed[@]}): ${failed[*]}" + return 1 + fi + + echo "" + success "All runnable workflows completed successfully." +} + +# ── Main ────────────────────────────────────────────────────────────────────── +main() { + local include_release="false" + + for arg in "$@"; do + case "$arg" in + --include-release) include_release="true" ;; + *) + error "Unknown argument: $arg" + echo "Usage: $0 [--include-release]" + exit 1 + ;; + esac + done + + header "════════════════════════════════════════════════════" + header " Running GitHub Actions workflows locally with act" + header "════════════════════════════════════════════════════" + + install_docker + install_act + ensure_docker_running + run_all_workflows "$include_release" +} + +main "$@" From c24c1e1e92f1cff45714b25f5fc2bbc657360e79 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 25 Mar 2026 08:17:58 +0000 Subject: [PATCH 3/6] Remove macOS support from run_workflows_locally.sh Co-authored-by: gensyn <36128035+gensyn@users.noreply.github.com> Agent-Logs-Url: https://github.com/gensyn/ssh_command/sessions/92b93971-363d-43b9-9ca3-fc82698b22ab --- run_workflows_locally.sh | 77 ++++++---------------------------------- 1 file changed, 11 insertions(+), 66 deletions(-) diff --git a/run_workflows_locally.sh b/run_workflows_locally.sh index 4b32bcb..42ccad1 100755 --- a/run_workflows_locally.sh +++ b/run_workflows_locally.sh @@ -31,14 +31,6 @@ header() { echo -e "\n${BOLD}$*${NC}"; } command_exists() { command -v "$1" &>/dev/null; } -os_type() { - case "$OSTYPE" in - linux*) echo "linux" ;; - darwin*) echo "macos" ;; - *) echo "unknown" ;; - esac -} - # ── Docker installation ─────────────────────────────────────────────────────── install_docker() { if command_exists docker; then @@ -47,27 +39,9 @@ install_docker() { fi header "Installing Docker…" - case "$(os_type)" in - linux) - curl -fsSL https://get.docker.com | sudo sh - sudo usermod -aG docker "$USER" || true - warn "Docker installed. You may need to run 'newgrp docker' or re-login for group membership to take effect." - ;; - macos) - if command_exists brew; then - brew install --cask docker - info "Please launch Docker Desktop to start the daemon, then re-run this script." - exit 0 - else - error "Homebrew not found. Install Docker Desktop from https://www.docker.com/products/docker-desktop/ and re-run." - exit 1 - fi - ;; - *) - error "Unsupported OS '$OSTYPE'. Install Docker manually from https://docs.docker.com/engine/install/ and re-run." - exit 1 - ;; - esac + curl -fsSL https://get.docker.com | sudo sh + sudo usermod -aG docker "$USER" || true + warn "Docker installed. You may need to run 'newgrp docker' or re-login for group membership to take effect." } # ── act installation ────────────────────────────────────────────────────────── @@ -78,24 +52,8 @@ install_act() { fi header "Installing act…" - case "$(os_type)" in - linux) - curl -fsSL https://raw.githubusercontent.com/nektos/act/master/install.sh \ - | sudo bash -s -- -b /usr/local/bin - ;; - macos) - if command_exists brew; then - brew install act - else - curl -fsSL https://raw.githubusercontent.com/nektos/act/master/install.sh \ - | sudo bash -s -- -b /usr/local/bin - fi - ;; - *) - error "Unsupported OS '$OSTYPE'. Install act manually from https://github.com/nektos/act and re-run." - exit 1 - ;; - esac + curl -fsSL https://raw.githubusercontent.com/nektos/act/master/install.sh \ + | sudo bash -s -- -b /usr/local/bin } # ── Docker daemon check ─────────────────────────────────────────────────────── @@ -105,25 +63,12 @@ ensure_docker_running() { fi warn "Docker daemon is not running – attempting to start it…" - case "$(os_type)" in - linux) - if command_exists systemctl; then - sudo systemctl start docker - else - sudo service docker start - fi - sleep 3 - ;; - macos) - open -a Docker || true - info "Waiting up to 60 seconds for Docker Desktop to start…" - local i - for i in $(seq 1 12); do - sleep 5 - docker info &>/dev/null && return 0 - done - ;; - esac + if command_exists systemctl; then + sudo systemctl start docker + else + sudo service docker start + fi + sleep 3 if ! docker info &>/dev/null; then error "Docker daemon is still not running. Please start Docker manually and re-run this script." From 7f49ae76254b0399adcb8cd76253722d90257ae1 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 25 Mar 2026 08:21:11 +0000 Subject: [PATCH 4/6] Use sudo for all docker/act daemon commands Co-authored-by: gensyn <36128035+gensyn@users.noreply.github.com> Agent-Logs-Url: https://github.com/gensyn/ssh_command/sessions/97cb93c5-3338-489c-b83a-d9ef15beb647 --- run_workflows_locally.sh | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/run_workflows_locally.sh b/run_workflows_locally.sh index 42ccad1..3f7151f 100755 --- a/run_workflows_locally.sh +++ b/run_workflows_locally.sh @@ -34,7 +34,7 @@ command_exists() { command -v "$1" &>/dev/null; } # ── Docker installation ─────────────────────────────────────────────────────── install_docker() { if command_exists docker; then - info "Docker is already installed: $(docker --version)" + info "Docker is already installed: $(sudo docker --version)" return 0 fi @@ -58,7 +58,7 @@ install_act() { # ── Docker daemon check ─────────────────────────────────────────────────────── ensure_docker_running() { - if docker info &>/dev/null; then + if sudo docker info &>/dev/null; then return 0 fi @@ -70,7 +70,7 @@ ensure_docker_running() { fi sleep 3 - if ! docker info &>/dev/null; then + if ! sudo docker info &>/dev/null; then error "Docker daemon is still not running. Please start Docker manually and re-run this script." exit 1 fi @@ -96,7 +96,7 @@ run_workflow() { info "Running [$name] with event '$event'…" - if act "$event" \ + if sudo act "$event" \ -W "$workflow_file" \ -P "ubuntu-latest=$ACT_IMAGE" \ --rm \ From 0211332f7ebcf275a54fdc4d807f9197a67857d5 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 25 Mar 2026 09:13:38 +0000 Subject: [PATCH 5/6] Skip hassfest and validate workflows by default (known act incompatibilities) Co-authored-by: gensyn <36128035+gensyn@users.noreply.github.com> Agent-Logs-Url: https://github.com/gensyn/ssh_command/sessions/5c417f2d-de7b-4c1d-88e2-1176a47ec298 --- run_workflows_locally.sh | 45 ++++++++++++++++++++++++++++++++-------- 1 file changed, 36 insertions(+), 9 deletions(-) diff --git a/run_workflows_locally.sh b/run_workflows_locally.sh index 3f7151f..df27c77 100755 --- a/run_workflows_locally.sh +++ b/run_workflows_locally.sh @@ -7,8 +7,14 @@ # Both tools are installed automatically if they are not already present. # # Usage: -# ./run_workflows_locally.sh [--include-release] +# ./run_workflows_locally.sh [--include-hassfest] [--include-validate] [--include-release] # +# --include-hassfest Also run hassfest.yaml (may fail locally because act sets +# GITHUB_WORKSPACE to the host path, causing hassfest's +# directory-name check to fail). +# --include-validate Also run validate.yaml (HACS validation fetches data from +# the live GitHub repository and may not behave identically +# when run locally via act). # --include-release Also run release.yaml (requires a GITHUB_TOKEN env var # with write permissions to the repository and will upload # artefacts to the real GitHub release – use with care). @@ -110,7 +116,9 @@ run_workflow() { } run_all_workflows() { - local include_release="${1:-false}" + local include_hassfest="${1:-false}" + local include_validate="${2:-false}" + local include_release="${3:-false}" # List of workflows and the event used to trigger each one locally. # Parallel arrays: workflow_files[i] uses workflow_events[i]. @@ -118,21 +126,36 @@ run_all_workflows() { "test.yml" "pylint.yml" "integration-tests.yml" - "hassfest.yaml" - "validate.yaml" ) local workflow_events=( "push" "push" "push" - "push" - "workflow_dispatch" ) local passed=() local failed=() local skipped=() + # hassfest: skipped by default because act sets GITHUB_WORKSPACE to the + # host filesystem path, which causes hassfest's directory-name check to + # fail even though the integration is valid (works fine on GitHub). + if [[ "$include_hassfest" == "true" ]]; then + workflow_files+=("hassfest.yaml") + workflow_events+=("push") + else + skipped+=("hassfest.yaml (skipped by default – pass --include-hassfest to run it)") + fi + + # validate (HACS): skipped by default because it fetches live data from + # the GitHub repository and may not behave identically when run locally. + if [[ "$include_validate" == "true" ]]; then + workflow_files+=("validate.yaml") + workflow_events+=("workflow_dispatch") + else + skipped+=("validate.yaml (skipped by default – pass --include-validate to run it)") + fi + # Optionally include the release workflow if [[ "$include_release" == "true" ]]; then workflow_files+=("release.yaml") @@ -183,14 +206,18 @@ run_all_workflows() { # ── Main ────────────────────────────────────────────────────────────────────── main() { + local include_hassfest="false" + local include_validate="false" local include_release="false" for arg in "$@"; do case "$arg" in - --include-release) include_release="true" ;; + --include-hassfest) include_hassfest="true" ;; + --include-validate) include_validate="true" ;; + --include-release) include_release="true" ;; *) error "Unknown argument: $arg" - echo "Usage: $0 [--include-release]" + echo "Usage: $0 [--include-hassfest] [--include-validate] [--include-release]" exit 1 ;; esac @@ -203,7 +230,7 @@ main() { install_docker install_act ensure_docker_running - run_all_workflows "$include_release" + run_all_workflows "$include_hassfest" "$include_validate" "$include_release" } main "$@" From 9fc522c077d68b508c785bcf731a009a470f61c1 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 25 Mar 2026 09:17:21 +0000 Subject: [PATCH 6/6] Silently skip GitHub-dependent workflows; no opt-in flags Co-authored-by: gensyn <36128035+gensyn@users.noreply.github.com> Agent-Logs-Url: https://github.com/gensyn/ssh_command/sessions/05029d96-bf01-438d-9f2f-29f4d31157ef --- run_workflows_locally.sh | 86 ++++++++-------------------------------- 1 file changed, 16 insertions(+), 70 deletions(-) diff --git a/run_workflows_locally.sh b/run_workflows_locally.sh index df27c77..d4b4304 100755 --- a/run_workflows_locally.sh +++ b/run_workflows_locally.sh @@ -1,23 +1,16 @@ #!/usr/bin/env bash # run_workflows_locally.sh # -# Runs all GitHub Actions workflows in this repository locally using Docker and -# act (https://github.com/nektos/act). +# Runs the CI workflows (tests and linting) in this repository locally using +# Docker and act (https://github.com/nektos/act). # # Both tools are installed automatically if they are not already present. # -# Usage: -# ./run_workflows_locally.sh [--include-hassfest] [--include-validate] [--include-release] +# Workflows that depend on GitHub infrastructure (hassfest, HACS validation, +# release) are silently skipped as they cannot run meaningfully offline. # -# --include-hassfest Also run hassfest.yaml (may fail locally because act sets -# GITHUB_WORKSPACE to the host path, causing hassfest's -# directory-name check to fail). -# --include-validate Also run validate.yaml (HACS validation fetches data from -# the live GitHub repository and may not behave identically -# when run locally via act). -# --include-release Also run release.yaml (requires a GITHUB_TOKEN env var -# with write permissions to the repository and will upload -# artefacts to the real GitHub release – use with care). +# Usage: +# ./run_workflows_locally.sh set -euo pipefail @@ -116,12 +109,9 @@ run_workflow() { } run_all_workflows() { - local include_hassfest="${1:-false}" - local include_validate="${2:-false}" - local include_release="${3:-false}" - - # List of workflows and the event used to trigger each one locally. - # Parallel arrays: workflow_files[i] uses workflow_events[i]. + # Only workflows that run entirely locally (tests and linting). + # Workflows that depend on GitHub infrastructure (hassfest, HACS validation, + # release) are silently omitted. local workflow_files=( "test.yml" "pylint.yml" @@ -135,35 +125,6 @@ run_all_workflows() { local passed=() local failed=() - local skipped=() - - # hassfest: skipped by default because act sets GITHUB_WORKSPACE to the - # host filesystem path, which causes hassfest's directory-name check to - # fail even though the integration is valid (works fine on GitHub). - if [[ "$include_hassfest" == "true" ]]; then - workflow_files+=("hassfest.yaml") - workflow_events+=("push") - else - skipped+=("hassfest.yaml (skipped by default – pass --include-hassfest to run it)") - fi - - # validate (HACS): skipped by default because it fetches live data from - # the GitHub repository and may not behave identically when run locally. - if [[ "$include_validate" == "true" ]]; then - workflow_files+=("validate.yaml") - workflow_events+=("workflow_dispatch") - else - skipped+=("validate.yaml (skipped by default – pass --include-validate to run it)") - fi - - # Optionally include the release workflow - if [[ "$include_release" == "true" ]]; then - workflow_files+=("release.yaml") - workflow_events+=("release") - warn "Including release.yaml – this uploads artefacts to a real GitHub release." - else - skipped+=("release.yaml (skipped by default – pass --include-release to run it)") - fi local i for i in "${!workflow_files[@]}"; do @@ -173,7 +134,6 @@ run_all_workflows() { if [[ ! -f "$workflow_path" ]]; then warn "Workflow file not found, skipping: $workflow" - skipped+=("$workflow (file not found)") continue fi @@ -192,36 +152,22 @@ run_all_workflows() { if [[ ${#passed[@]} -gt 0 ]]; then success "Passed (${#passed[@]}): ${passed[*]}" fi - if [[ ${#skipped[@]} -gt 0 ]]; then - warn "Skipped (${#skipped[@]}): ${skipped[*]}" - fi if [[ ${#failed[@]} -gt 0 ]]; then error "Failed (${#failed[@]}): ${failed[*]}" return 1 fi echo "" - success "All runnable workflows completed successfully." + success "All workflows completed successfully." } # ── Main ────────────────────────────────────────────────────────────────────── main() { - local include_hassfest="false" - local include_validate="false" - local include_release="false" - - for arg in "$@"; do - case "$arg" in - --include-hassfest) include_hassfest="true" ;; - --include-validate) include_validate="true" ;; - --include-release) include_release="true" ;; - *) - error "Unknown argument: $arg" - echo "Usage: $0 [--include-hassfest] [--include-validate] [--include-release]" - exit 1 - ;; - esac - done + if [[ $# -gt 0 ]]; then + error "This script takes no arguments." + echo "Usage: $0" + exit 1 + fi header "════════════════════════════════════════════════════" header " Running GitHub Actions workflows locally with act" @@ -230,7 +176,7 @@ main() { install_docker install_act ensure_docker_running - run_all_workflows "$include_hassfest" "$include_validate" "$include_release" + run_all_workflows } main "$@"