diff --git a/.github/actionlint.yml b/.github/actionlint.yml new file mode 100644 index 00000000..120b667d --- /dev/null +++ b/.github/actionlint.yml @@ -0,0 +1,7 @@ +# See: https://github.com/rhysd/actionlint/blob/main/docs/config.md +# Register custom org-provided runner labels so actionlint does not flag them +# as unknown. `ubuntu-latest-8c` is a larger GitHub-hosted runner used by the +# release workflow. +self-hosted-runner: + labels: + - ubuntu-latest-8c diff --git a/.github/workflows/builder.yml b/.github/workflows/builder.yml index 5320030f..9d5adafb 100644 --- a/.github/workflows/builder.yml +++ b/.github/workflows/builder.yml @@ -55,4 +55,3 @@ jobs: labels: ${{ steps.meta.outputs.labels }} cache-from: type=gha cache-to: type=gha,mode=max - diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 9557de4f..cd52fbfa 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -85,3 +85,9 @@ jobs: - name: Run Tests + Coverage Gate run: ./tools/lint/scripts/covgate.sh + + surface-lint: + # yamllint disable rule:line-length + uses: mirurobotics/.github/.github/workflows/surface-lint.yml@main + with: + runs-on: ubuntu-latest diff --git a/.shellcheckrc b/.shellcheckrc new file mode 100644 index 00000000..478ea351 --- /dev/null +++ b/.shellcheckrc @@ -0,0 +1,8 @@ +# Do not set a global `shell=` directive: the agent repo has mixed shebangs +# (#!/bin/sh, #!/usr/bin/env bash, #!/bin/bash). Let shellcheck honor each +# file's shebang so bash scripts (scripts/preflight.sh, api/regen.sh) are not +# falsely flagged with SC3040 (pipefail undefined in POSIX sh). + +# SC1091: Not following sourced files. Several scripts source files shellcheck +# cannot resolve (.venv/bin/activate, $HOME/.cargo/env, build/git-tags.sh). +disable=SC1091 diff --git a/.yamllint.yml b/.yamllint.yml new file mode 100644 index 00000000..27e2a52a --- /dev/null +++ b/.yamllint.yml @@ -0,0 +1,22 @@ +# See: https://yamllint.readthedocs.io/en/stable/ + +extends: default + +rules: + document-start: + present: false + line-length: + level: warning + allow-non-breakable-inline-mappings: true + # GitHub Actions uses `on:` as a workflow trigger key; YAML 1.1 treats `on` + # as a truthy boolean alias. Disable key checking to avoid false positives. + truthy: + check-keys: false + # SHA-pinned action comments use a single space before `#`. Reduce the + # minimum from the default 2 to 1 to match the existing convention. + comments: + min-spaces-from-content: 1 + +ignore: | + .agents/ + api/specs/ diff --git a/build/.goreleaser.yaml b/build/.goreleaser.yaml index 3d769850..45fda0e2 100644 --- a/build/.goreleaser.yaml +++ b/build/.goreleaser.yaml @@ -30,7 +30,7 @@ nfpms: description: | Miru Agent. Miru provides the infrastructure to version, manage, and deploy application configurations at scale. This debian package is the miru agent, which handles configuration deployment to your robots in production. - + formats: - deb @@ -126,7 +126,7 @@ release: ## Miru Agent {{.Tag}} **Full Changelog**: https://github.com/mirurobotics/agent/compare/{{ .PreviousTag }}...{{ .Tag }} - + For documentation, visit: https://docs.mirurobotics.com @@ -134,4 +134,3 @@ source: enabled: false report_sizes: true - diff --git a/scripts/install/install.sh b/scripts/install/install.sh index 3d459b00..a365672a 100755 --- a/scripts/install/install.sh +++ b/scripts/install/install.sh @@ -3,11 +3,12 @@ set -e # Script: install.sh # Jinja Template: install.j2 -# Build Timestamp: 2026-05-09T19:53:58.827613 +# Build Timestamp: 2026-06-03T19:14:13.912548 # Description: Install the Miru Agent # DISPLAY # # ======= # +# shellcheck shell=sh RED='\033[0;31m' GREEN='\033[0;32m' YELLOW='\033[1;33m' @@ -16,7 +17,9 @@ NO_COLOR='\033[0m' debug() { echo "${BLUE}==>${NO_COLOR} $1"; } log() { echo "${GREEN}==>${NO_COLOR} $1"; } +# shellcheck disable=SC2317,SC2329 # part of the shared logging API; not every script calls every helper warn() { echo "${YELLOW}Warning:${NO_COLOR} $1"; } +# shellcheck disable=SC2317,SC2329 # part of the shared logging API; not every script calls every helper error() { echo "${RED}Error:${NO_COLOR} $1"; } fatal() { echo "${RED}Error:${NO_COLOR} $1"; exit 1; } @@ -102,6 +105,7 @@ for cmd in curl grep cut jq; do done +# shellcheck shell=sh verify_checksum() { file=$1 expected_checksum=$2 @@ -152,6 +156,7 @@ esac # USE PROVIDED PACKAGE # # -------------------- # +# shellcheck shell=sh if [ -n "$FROM_PKG" ]; then log "Installing from package on local machine: '$FROM_PKG'" if [ ! -f "$FROM_PKG" ]; then @@ -166,13 +171,16 @@ if [ -n "$FROM_PKG" ]; then if [ "$(dpkg -f "$FROM_PKG" Architecture)" != "$DEB_ARCH" ]; then fatal "The provided package architecture ($(dpkg -f "$FROM_PKG" Architecture)) does not match this machine's architecture ($DEB_ARCH)." fi + # shellcheck disable=SC2034 # consumed by a later partial in the rendered script AGENT_DEB_PKG=$FROM_PKG + # shellcheck disable=SC2034 # consumed by a later partial in the rendered script VERSION=$(dpkg -f "$FROM_PKG" Version) fi # DETERMINE THE VERSION # # --------------------- # +# shellcheck shell=sh if [ -z "$VERSION" ]; then if [ "$PRERELEASE" = true ]; then log "Fetching latest pre-release version..." @@ -208,9 +216,11 @@ fi # DOWNLOAD THE AGENT # # ------------------ # +# shellcheck shell=sh INSTALLED_VERSION=$(dpkg-query -W -f='${Version}' "$AGENT_DEB_PKG_NAME" 2>/dev/null || echo "") -# replace '~' with '-' +# replace '~' with '-' if [ -n "$INSTALLED_VERSION" ]; then + # shellcheck disable=SC2001 # POSIX sh lacks ${var//search/replace}; sed is required INSTALLED_VERSION=$(echo "$INSTALLED_VERSION" | sed 's/~/-/g') fi @@ -254,6 +264,8 @@ fi # ACTIVATE THE AGENT # # ------------------ # +# shellcheck shell=sh +# shellcheck disable=SC2317,SC2329 # cleanup() is invoked indirectly via trap cleanup() { exit_code=$? @@ -261,7 +273,7 @@ cleanup() { log "Restarting the Miru Agent" sudo systemctl restart miru >/dev/null 2>&1 - exit $exit_code + exit "$exit_code" } trap cleanup EXIT INT TERM QUIT HUP @@ -288,5 +300,6 @@ fi sudo chown -R miru:miru /srv/miru # Execute the installer +# shellcheck disable=SC2086 # word-splitting of the argument list is intentional sudo -u miru -E env MIRU_ACTIVATION_TOKEN="$MIRU_ACTIVATION_TOKEN" /usr/sbin/miru-agent --install $args exit 0 \ No newline at end of file diff --git a/scripts/install/provision.sh b/scripts/install/provision.sh index d01c379d..cd1cbba9 100755 --- a/scripts/install/provision.sh +++ b/scripts/install/provision.sh @@ -3,11 +3,12 @@ set -e # Script: provision.sh # Jinja Template: provision.j2 -# Build Timestamp: 2026-05-09T19:53:58.827613 +# Build Timestamp: 2026-06-03T19:14:13.912548 # Description: Provision a device & install the Miru Agent # DISPLAY # # ======= # +# shellcheck shell=sh RED='\033[0;31m' GREEN='\033[0;32m' YELLOW='\033[1;33m' @@ -16,7 +17,9 @@ NO_COLOR='\033[0m' debug() { echo "${BLUE}==>${NO_COLOR} $1"; } log() { echo "${GREEN}==>${NO_COLOR} $1"; } +# shellcheck disable=SC2317,SC2329 # part of the shared logging API; not every script calls every helper warn() { echo "${YELLOW}Warning:${NO_COLOR} $1"; } +# shellcheck disable=SC2317,SC2329 # part of the shared logging API; not every script calls every helper error() { echo "${RED}Error:${NO_COLOR} $1"; } fatal() { echo "${RED}Error:${NO_COLOR} $1"; exit 1; } @@ -116,6 +119,7 @@ for cmd in curl grep cut jq; do done +# shellcheck shell=sh verify_checksum() { file=$1 expected_checksum=$2 @@ -166,6 +170,7 @@ esac # USE PROVIDED PACKAGE # # -------------------- # +# shellcheck shell=sh if [ -n "$FROM_PKG" ]; then log "Installing from package on local machine: '$FROM_PKG'" if [ ! -f "$FROM_PKG" ]; then @@ -180,18 +185,22 @@ if [ -n "$FROM_PKG" ]; then if [ "$(dpkg -f "$FROM_PKG" Architecture)" != "$DEB_ARCH" ]; then fatal "The provided package architecture ($(dpkg -f "$FROM_PKG" Architecture)) does not match this machine's architecture ($DEB_ARCH)." fi + # shellcheck disable=SC2034 # consumed by a later partial in the rendered script AGENT_DEB_PKG=$FROM_PKG + # shellcheck disable=SC2034 # consumed by a later partial in the rendered script VERSION=$(dpkg -f "$FROM_PKG" Version) fi # PROVISION THE DEVICE # # --------------------- # +# shellcheck shell=sh if [ -z "$MIRU_API_KEY" ]; then echo "MIRU_API_KEY is not set" exit 1 fi +# shellcheck disable=SC2153 # DEVICE_NAME is an external environment variable response_body=$(curl --request POST \ --url "$BACKEND_HOST"/v1/devices \ --header 'Content-Type: application/json' \ @@ -261,6 +270,7 @@ response_body=$(echo "$response_body" | head -n -1) # Check if the request succeeded if [ "$http_code" -eq 200 ] || [ "$http_code" -eq 201 ]; then log "Successfully created activation token" + # shellcheck disable=SC2034 # consumed by a later partial in the rendered script MIRU_ACTIVATION_TOKEN=$(echo "$response_body" | jq -r '.token') else error "Activation token request failed (HTTP status $http_code)" @@ -270,6 +280,7 @@ fi # DETERMINE THE VERSION # # --------------------- # +# shellcheck shell=sh if [ -z "$VERSION" ]; then if [ "$PRERELEASE" = true ]; then log "Fetching latest pre-release version..." @@ -305,9 +316,11 @@ fi # DOWNLOAD THE AGENT # # ------------------ # +# shellcheck shell=sh INSTALLED_VERSION=$(dpkg-query -W -f='${Version}' "$AGENT_DEB_PKG_NAME" 2>/dev/null || echo "") -# replace '~' with '-' +# replace '~' with '-' if [ -n "$INSTALLED_VERSION" ]; then + # shellcheck disable=SC2001 # POSIX sh lacks ${var//search/replace}; sed is required INSTALLED_VERSION=$(echo "$INSTALLED_VERSION" | sed 's/~/-/g') fi @@ -351,6 +364,8 @@ fi # ACTIVATE THE AGENT # # ------------------ # +# shellcheck shell=sh +# shellcheck disable=SC2317,SC2329 # cleanup() is invoked indirectly via trap cleanup() { exit_code=$? @@ -358,7 +373,7 @@ cleanup() { log "Restarting the Miru Agent" sudo systemctl restart miru >/dev/null 2>&1 - exit $exit_code + exit "$exit_code" } trap cleanup EXIT INT TERM QUIT HUP @@ -385,5 +400,6 @@ fi sudo chown -R miru:miru /srv/miru # Execute the installer +# shellcheck disable=SC2086 # word-splitting of the argument list is intentional sudo -u miru -E env MIRU_ACTIVATION_TOKEN="$MIRU_ACTIVATION_TOKEN" /usr/sbin/miru-agent --install $args exit 0 \ No newline at end of file diff --git a/scripts/install/staging-install.sh b/scripts/install/staging-install.sh index fe76697d..182110c5 100755 --- a/scripts/install/staging-install.sh +++ b/scripts/install/staging-install.sh @@ -3,11 +3,12 @@ set -e # Script: staging-install.sh # Jinja Template: install.j2 -# Build Timestamp: 2026-05-09T19:53:58.827613 +# Build Timestamp: 2026-06-03T19:14:13.912548 # Description: Install the Miru Agent in the staging environment # DISPLAY # # ======= # +# shellcheck shell=sh RED='\033[0;31m' GREEN='\033[0;32m' YELLOW='\033[1;33m' @@ -16,7 +17,9 @@ NO_COLOR='\033[0m' debug() { echo "${BLUE}==>${NO_COLOR} $1"; } log() { echo "${GREEN}==>${NO_COLOR} $1"; } +# shellcheck disable=SC2317,SC2329 # part of the shared logging API; not every script calls every helper warn() { echo "${YELLOW}Warning:${NO_COLOR} $1"; } +# shellcheck disable=SC2317,SC2329 # part of the shared logging API; not every script calls every helper error() { echo "${RED}Error:${NO_COLOR} $1"; } fatal() { echo "${RED}Error:${NO_COLOR} $1"; exit 1; } @@ -102,6 +105,7 @@ for cmd in curl grep cut jq; do done +# shellcheck shell=sh verify_checksum() { file=$1 expected_checksum=$2 @@ -152,6 +156,7 @@ esac # USE PROVIDED PACKAGE # # -------------------- # +# shellcheck shell=sh if [ -n "$FROM_PKG" ]; then log "Installing from package on local machine: '$FROM_PKG'" if [ ! -f "$FROM_PKG" ]; then @@ -166,13 +171,16 @@ if [ -n "$FROM_PKG" ]; then if [ "$(dpkg -f "$FROM_PKG" Architecture)" != "$DEB_ARCH" ]; then fatal "The provided package architecture ($(dpkg -f "$FROM_PKG" Architecture)) does not match this machine's architecture ($DEB_ARCH)." fi + # shellcheck disable=SC2034 # consumed by a later partial in the rendered script AGENT_DEB_PKG=$FROM_PKG + # shellcheck disable=SC2034 # consumed by a later partial in the rendered script VERSION=$(dpkg -f "$FROM_PKG" Version) fi # DETERMINE THE VERSION # # --------------------- # +# shellcheck shell=sh if [ -z "$VERSION" ]; then if [ "$PRERELEASE" = true ]; then log "Fetching latest pre-release version..." @@ -208,9 +216,11 @@ fi # DOWNLOAD THE AGENT # # ------------------ # +# shellcheck shell=sh INSTALLED_VERSION=$(dpkg-query -W -f='${Version}' "$AGENT_DEB_PKG_NAME" 2>/dev/null || echo "") -# replace '~' with '-' +# replace '~' with '-' if [ -n "$INSTALLED_VERSION" ]; then + # shellcheck disable=SC2001 # POSIX sh lacks ${var//search/replace}; sed is required INSTALLED_VERSION=$(echo "$INSTALLED_VERSION" | sed 's/~/-/g') fi @@ -254,6 +264,8 @@ fi # ACTIVATE THE AGENT # # ------------------ # +# shellcheck shell=sh +# shellcheck disable=SC2317,SC2329 # cleanup() is invoked indirectly via trap cleanup() { exit_code=$? @@ -261,7 +273,7 @@ cleanup() { log "Restarting the Miru Agent" sudo systemctl restart miru >/dev/null 2>&1 - exit $exit_code + exit "$exit_code" } trap cleanup EXIT INT TERM QUIT HUP @@ -288,5 +300,6 @@ fi sudo chown -R miru:miru /srv/miru # Execute the installer +# shellcheck disable=SC2086 # word-splitting of the argument list is intentional sudo -u miru -E env MIRU_ACTIVATION_TOKEN="$MIRU_ACTIVATION_TOKEN" /usr/sbin/miru-agent --install $args exit 0 \ No newline at end of file diff --git a/scripts/install/staging-provision.sh b/scripts/install/staging-provision.sh index 36bc2f7d..05c89c94 100755 --- a/scripts/install/staging-provision.sh +++ b/scripts/install/staging-provision.sh @@ -3,11 +3,12 @@ set -e # Script: staging-provision.sh # Jinja Template: provision.j2 -# Build Timestamp: 2026-05-09T19:53:58.827613 +# Build Timestamp: 2026-06-03T19:14:13.912548 # Description: Provision a device & install the Miru Agent in the staging environment # DISPLAY # # ======= # +# shellcheck shell=sh RED='\033[0;31m' GREEN='\033[0;32m' YELLOW='\033[1;33m' @@ -16,7 +17,9 @@ NO_COLOR='\033[0m' debug() { echo "${BLUE}==>${NO_COLOR} $1"; } log() { echo "${GREEN}==>${NO_COLOR} $1"; } +# shellcheck disable=SC2317,SC2329 # part of the shared logging API; not every script calls every helper warn() { echo "${YELLOW}Warning:${NO_COLOR} $1"; } +# shellcheck disable=SC2317,SC2329 # part of the shared logging API; not every script calls every helper error() { echo "${RED}Error:${NO_COLOR} $1"; } fatal() { echo "${RED}Error:${NO_COLOR} $1"; exit 1; } @@ -116,6 +119,7 @@ for cmd in curl grep cut jq; do done +# shellcheck shell=sh verify_checksum() { file=$1 expected_checksum=$2 @@ -166,6 +170,7 @@ esac # USE PROVIDED PACKAGE # # -------------------- # +# shellcheck shell=sh if [ -n "$FROM_PKG" ]; then log "Installing from package on local machine: '$FROM_PKG'" if [ ! -f "$FROM_PKG" ]; then @@ -180,18 +185,22 @@ if [ -n "$FROM_PKG" ]; then if [ "$(dpkg -f "$FROM_PKG" Architecture)" != "$DEB_ARCH" ]; then fatal "The provided package architecture ($(dpkg -f "$FROM_PKG" Architecture)) does not match this machine's architecture ($DEB_ARCH)." fi + # shellcheck disable=SC2034 # consumed by a later partial in the rendered script AGENT_DEB_PKG=$FROM_PKG + # shellcheck disable=SC2034 # consumed by a later partial in the rendered script VERSION=$(dpkg -f "$FROM_PKG" Version) fi # PROVISION THE DEVICE # # --------------------- # +# shellcheck shell=sh if [ -z "$MIRU_API_KEY" ]; then echo "MIRU_API_KEY is not set" exit 1 fi +# shellcheck disable=SC2153 # DEVICE_NAME is an external environment variable response_body=$(curl --request POST \ --url "$BACKEND_HOST"/v1/devices \ --header 'Content-Type: application/json' \ @@ -261,6 +270,7 @@ response_body=$(echo "$response_body" | head -n -1) # Check if the request succeeded if [ "$http_code" -eq 200 ] || [ "$http_code" -eq 201 ]; then log "Successfully created activation token" + # shellcheck disable=SC2034 # consumed by a later partial in the rendered script MIRU_ACTIVATION_TOKEN=$(echo "$response_body" | jq -r '.token') else error "Activation token request failed (HTTP status $http_code)" @@ -270,6 +280,7 @@ fi # DETERMINE THE VERSION # # --------------------- # +# shellcheck shell=sh if [ -z "$VERSION" ]; then if [ "$PRERELEASE" = true ]; then log "Fetching latest pre-release version..." @@ -305,9 +316,11 @@ fi # DOWNLOAD THE AGENT # # ------------------ # +# shellcheck shell=sh INSTALLED_VERSION=$(dpkg-query -W -f='${Version}' "$AGENT_DEB_PKG_NAME" 2>/dev/null || echo "") -# replace '~' with '-' +# replace '~' with '-' if [ -n "$INSTALLED_VERSION" ]; then + # shellcheck disable=SC2001 # POSIX sh lacks ${var//search/replace}; sed is required INSTALLED_VERSION=$(echo "$INSTALLED_VERSION" | sed 's/~/-/g') fi @@ -351,6 +364,8 @@ fi # ACTIVATE THE AGENT # # ------------------ # +# shellcheck shell=sh +# shellcheck disable=SC2317,SC2329 # cleanup() is invoked indirectly via trap cleanup() { exit_code=$? @@ -358,7 +373,7 @@ cleanup() { log "Restarting the Miru Agent" sudo systemctl restart miru >/dev/null 2>&1 - exit $exit_code + exit "$exit_code" } trap cleanup EXIT INT TERM QUIT HUP @@ -385,5 +400,6 @@ fi sudo chown -R miru:miru /srv/miru # Execute the installer +# shellcheck disable=SC2086 # word-splitting of the argument list is intentional sudo -u miru -E env MIRU_ACTIVATION_TOKEN="$MIRU_ACTIVATION_TOKEN" /usr/sbin/miru-agent --install $args exit 0 \ No newline at end of file diff --git a/scripts/install/uat-install.sh b/scripts/install/uat-install.sh index a00c1a02..478c179b 100755 --- a/scripts/install/uat-install.sh +++ b/scripts/install/uat-install.sh @@ -3,11 +3,12 @@ set -e # Script: uat-install.sh # Jinja Template: install.j2 -# Build Timestamp: 2026-05-09T19:53:58.827613 +# Build Timestamp: 2026-06-03T19:14:13.912548 # Description: Install the Miru Agent in the UAT environment # DISPLAY # # ======= # +# shellcheck shell=sh RED='\033[0;31m' GREEN='\033[0;32m' YELLOW='\033[1;33m' @@ -16,7 +17,9 @@ NO_COLOR='\033[0m' debug() { echo "${BLUE}==>${NO_COLOR} $1"; } log() { echo "${GREEN}==>${NO_COLOR} $1"; } +# shellcheck disable=SC2317,SC2329 # part of the shared logging API; not every script calls every helper warn() { echo "${YELLOW}Warning:${NO_COLOR} $1"; } +# shellcheck disable=SC2317,SC2329 # part of the shared logging API; not every script calls every helper error() { echo "${RED}Error:${NO_COLOR} $1"; } fatal() { echo "${RED}Error:${NO_COLOR} $1"; exit 1; } @@ -102,6 +105,7 @@ for cmd in curl grep cut jq; do done +# shellcheck shell=sh verify_checksum() { file=$1 expected_checksum=$2 @@ -152,6 +156,7 @@ esac # USE PROVIDED PACKAGE # # -------------------- # +# shellcheck shell=sh if [ -n "$FROM_PKG" ]; then log "Installing from package on local machine: '$FROM_PKG'" if [ ! -f "$FROM_PKG" ]; then @@ -166,13 +171,16 @@ if [ -n "$FROM_PKG" ]; then if [ "$(dpkg -f "$FROM_PKG" Architecture)" != "$DEB_ARCH" ]; then fatal "The provided package architecture ($(dpkg -f "$FROM_PKG" Architecture)) does not match this machine's architecture ($DEB_ARCH)." fi + # shellcheck disable=SC2034 # consumed by a later partial in the rendered script AGENT_DEB_PKG=$FROM_PKG + # shellcheck disable=SC2034 # consumed by a later partial in the rendered script VERSION=$(dpkg -f "$FROM_PKG" Version) fi # DETERMINE THE VERSION # # --------------------- # +# shellcheck shell=sh if [ -z "$VERSION" ]; then if [ "$PRERELEASE" = true ]; then log "Fetching latest pre-release version..." @@ -208,9 +216,11 @@ fi # DOWNLOAD THE AGENT # # ------------------ # +# shellcheck shell=sh INSTALLED_VERSION=$(dpkg-query -W -f='${Version}' "$AGENT_DEB_PKG_NAME" 2>/dev/null || echo "") -# replace '~' with '-' +# replace '~' with '-' if [ -n "$INSTALLED_VERSION" ]; then + # shellcheck disable=SC2001 # POSIX sh lacks ${var//search/replace}; sed is required INSTALLED_VERSION=$(echo "$INSTALLED_VERSION" | sed 's/~/-/g') fi @@ -254,6 +264,8 @@ fi # ACTIVATE THE AGENT # # ------------------ # +# shellcheck shell=sh +# shellcheck disable=SC2317,SC2329 # cleanup() is invoked indirectly via trap cleanup() { exit_code=$? @@ -261,7 +273,7 @@ cleanup() { log "Restarting the Miru Agent" sudo systemctl restart miru >/dev/null 2>&1 - exit $exit_code + exit "$exit_code" } trap cleanup EXIT INT TERM QUIT HUP @@ -288,5 +300,6 @@ fi sudo chown -R miru:miru /srv/miru # Execute the installer +# shellcheck disable=SC2086 # word-splitting of the argument list is intentional sudo -u miru -E env MIRU_ACTIVATION_TOKEN="$MIRU_ACTIVATION_TOKEN" /usr/sbin/miru-agent --install $args exit 0 \ No newline at end of file diff --git a/scripts/install/uat-provision.sh b/scripts/install/uat-provision.sh index 379b0ba5..d8a96e31 100755 --- a/scripts/install/uat-provision.sh +++ b/scripts/install/uat-provision.sh @@ -3,11 +3,12 @@ set -e # Script: uat-provision.sh # Jinja Template: provision.j2 -# Build Timestamp: 2026-05-09T19:53:58.827613 +# Build Timestamp: 2026-06-03T19:14:13.912548 # Description: Provision a device & install the Miru Agent in the UAT environment # DISPLAY # # ======= # +# shellcheck shell=sh RED='\033[0;31m' GREEN='\033[0;32m' YELLOW='\033[1;33m' @@ -16,7 +17,9 @@ NO_COLOR='\033[0m' debug() { echo "${BLUE}==>${NO_COLOR} $1"; } log() { echo "${GREEN}==>${NO_COLOR} $1"; } +# shellcheck disable=SC2317,SC2329 # part of the shared logging API; not every script calls every helper warn() { echo "${YELLOW}Warning:${NO_COLOR} $1"; } +# shellcheck disable=SC2317,SC2329 # part of the shared logging API; not every script calls every helper error() { echo "${RED}Error:${NO_COLOR} $1"; } fatal() { echo "${RED}Error:${NO_COLOR} $1"; exit 1; } @@ -116,6 +119,7 @@ for cmd in curl grep cut jq; do done +# shellcheck shell=sh verify_checksum() { file=$1 expected_checksum=$2 @@ -166,6 +170,7 @@ esac # USE PROVIDED PACKAGE # # -------------------- # +# shellcheck shell=sh if [ -n "$FROM_PKG" ]; then log "Installing from package on local machine: '$FROM_PKG'" if [ ! -f "$FROM_PKG" ]; then @@ -180,18 +185,22 @@ if [ -n "$FROM_PKG" ]; then if [ "$(dpkg -f "$FROM_PKG" Architecture)" != "$DEB_ARCH" ]; then fatal "The provided package architecture ($(dpkg -f "$FROM_PKG" Architecture)) does not match this machine's architecture ($DEB_ARCH)." fi + # shellcheck disable=SC2034 # consumed by a later partial in the rendered script AGENT_DEB_PKG=$FROM_PKG + # shellcheck disable=SC2034 # consumed by a later partial in the rendered script VERSION=$(dpkg -f "$FROM_PKG" Version) fi # PROVISION THE DEVICE # # --------------------- # +# shellcheck shell=sh if [ -z "$MIRU_API_KEY" ]; then echo "MIRU_API_KEY is not set" exit 1 fi +# shellcheck disable=SC2153 # DEVICE_NAME is an external environment variable response_body=$(curl --request POST \ --url "$BACKEND_HOST"/v1/devices \ --header 'Content-Type: application/json' \ @@ -261,6 +270,7 @@ response_body=$(echo "$response_body" | head -n -1) # Check if the request succeeded if [ "$http_code" -eq 200 ] || [ "$http_code" -eq 201 ]; then log "Successfully created activation token" + # shellcheck disable=SC2034 # consumed by a later partial in the rendered script MIRU_ACTIVATION_TOKEN=$(echo "$response_body" | jq -r '.token') else error "Activation token request failed (HTTP status $http_code)" @@ -270,6 +280,7 @@ fi # DETERMINE THE VERSION # # --------------------- # +# shellcheck shell=sh if [ -z "$VERSION" ]; then if [ "$PRERELEASE" = true ]; then log "Fetching latest pre-release version..." @@ -305,9 +316,11 @@ fi # DOWNLOAD THE AGENT # # ------------------ # +# shellcheck shell=sh INSTALLED_VERSION=$(dpkg-query -W -f='${Version}' "$AGENT_DEB_PKG_NAME" 2>/dev/null || echo "") -# replace '~' with '-' +# replace '~' with '-' if [ -n "$INSTALLED_VERSION" ]; then + # shellcheck disable=SC2001 # POSIX sh lacks ${var//search/replace}; sed is required INSTALLED_VERSION=$(echo "$INSTALLED_VERSION" | sed 's/~/-/g') fi @@ -351,6 +364,8 @@ fi # ACTIVATE THE AGENT # # ------------------ # +# shellcheck shell=sh +# shellcheck disable=SC2317,SC2329 # cleanup() is invoked indirectly via trap cleanup() { exit_code=$? @@ -358,7 +373,7 @@ cleanup() { log "Restarting the Miru Agent" sudo systemctl restart miru >/dev/null 2>&1 - exit $exit_code + exit "$exit_code" } trap cleanup EXIT INT TERM QUIT HUP @@ -385,5 +400,6 @@ fi sudo chown -R miru:miru /srv/miru # Execute the installer +# shellcheck disable=SC2086 # word-splitting of the argument list is intentional sudo -u miru -E env MIRU_ACTIVATION_TOKEN="$MIRU_ACTIVATION_TOKEN" /usr/sbin/miru-agent --install $args exit 0 \ No newline at end of file diff --git a/scripts/jinja/install.yaml b/scripts/jinja/install.yaml index 2793ed98..e9d25fc2 100644 --- a/scripts/jinja/install.yaml +++ b/scripts/jinja/install.yaml @@ -131,7 +131,7 @@ base_install_vars: &base_install_vars debian_pkg_mime_type: *debian_pkg_mime_type prod_install: &prod_install - args: + args: <<: *base_install_args backend_host: *prod_backend_host mqtt_broker_host: *prod_mqtt_broker_host @@ -140,7 +140,7 @@ prod_install: &prod_install utilities: *checksum_utility staging_install: &staging_install - args: + args: <<: *base_install_args backend_host: *staging_backend_host mqtt_broker_host: *staging_mqtt_broker_host @@ -149,7 +149,7 @@ staging_install: &staging_install utilities: *checksum_utility uat_install: &uat_install - args: + args: <<: *base_install_args backend_host: *uat_backend_host mqtt_broker_host: *uat_mqtt_broker_host @@ -175,7 +175,7 @@ base_provision_vars: &base_provision_vars debian_pkg_mime_type: *debian_pkg_mime_type prod_provision: &prod_provision - args: + args: <<: *base_provision_args backend_host: *prod_backend_host mqtt_broker_host: *prod_mqtt_broker_host @@ -184,7 +184,7 @@ prod_provision: &prod_provision utilities: *checksum_utility staging_provision: &staging_provision - args: + args: <<: *base_provision_args backend_host: *staging_backend_host mqtt_broker_host: *staging_mqtt_broker_host @@ -193,7 +193,7 @@ staging_provision: &staging_provision utilities: *checksum_utility uat_provision: &uat_provision - args: + args: <<: *base_provision_args backend_host: *uat_backend_host mqtt_broker_host: *uat_mqtt_broker_host @@ -206,31 +206,31 @@ uat_provision: &uat_provision scripts: # installs - name: "install.sh" - template: "install.j2" + template: "install.j2" description: "Install the Miru Agent" variables: *prod_install - name: "staging-install.sh" - template: "install.j2" + template: "install.j2" description: "Install the Miru Agent in the staging environment" variables: *staging_install - name: "uat-install.sh" - template: "install.j2" + template: "install.j2" description: "Install the Miru Agent in the UAT environment" variables: *uat_install # provisions - name: "provision.sh" - template: "provision.j2" + template: "provision.j2" description: "Provision a device & install the Miru Agent" variables: <<: *prod_provision - name: "staging-provision.sh" - template: "provision.j2" + template: "provision.j2" description: "Provision a device & install the Miru Agent in the staging environment" variables: <<: *staging_provision - name: "uat-provision.sh" - template: "provision.j2" + template: "provision.j2" description: "Provision a device & install the Miru Agent in the UAT environment" variables: - <<: *uat_provision \ No newline at end of file + <<: *uat_provision diff --git a/scripts/jinja/render.sh b/scripts/jinja/render.sh index 68f7dbec..916987e7 100755 --- a/scripts/jinja/render.sh +++ b/scripts/jinja/render.sh @@ -10,7 +10,6 @@ SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" RED='\033[0;31m' GREEN='\033[0;32m' YELLOW='\033[1;33m' -BLUE='\033[0;34m' NC='\033[0m' log() { echo "${GREEN}==>${NC} $1"; } diff --git a/scripts/jinja/templates/partials/display.sh b/scripts/jinja/templates/partials/display.sh index 12867d9f..be6f0c16 100644 --- a/scripts/jinja/templates/partials/display.sh +++ b/scripts/jinja/templates/partials/display.sh @@ -1,3 +1,4 @@ +# shellcheck shell=sh RED='\033[0;31m' GREEN='\033[0;32m' YELLOW='\033[1;33m' @@ -6,6 +7,8 @@ NO_COLOR='\033[0m' debug() { echo "${BLUE}==>${NO_COLOR} $1"; } log() { echo "${GREEN}==>${NO_COLOR} $1"; } +# shellcheck disable=SC2317,SC2329 # part of the shared logging API; not every script calls every helper warn() { echo "${YELLOW}Warning:${NO_COLOR} $1"; } +# shellcheck disable=SC2317,SC2329 # part of the shared logging API; not every script calls every helper error() { echo "${RED}Error:${NO_COLOR} $1"; } fatal() { echo "${RED}Error:${NO_COLOR} $1"; exit 1; } \ No newline at end of file diff --git a/scripts/jinja/templates/partials/utils/activate.sh b/scripts/jinja/templates/partials/utils/activate.sh index f131644e..5cfdebfb 100644 --- a/scripts/jinja/templates/partials/utils/activate.sh +++ b/scripts/jinja/templates/partials/utils/activate.sh @@ -1,3 +1,5 @@ +# shellcheck shell=sh +# shellcheck disable=SC2317,SC2329 # cleanup() is invoked indirectly via trap cleanup() { exit_code=$? @@ -5,7 +7,7 @@ cleanup() { log "Restarting the Miru Agent" sudo systemctl restart miru >/dev/null 2>&1 - exit $exit_code + exit "$exit_code" } trap cleanup EXIT INT TERM QUIT HUP @@ -32,4 +34,5 @@ fi sudo chown -R miru:miru /srv/miru # Execute the installer +# shellcheck disable=SC2086 # word-splitting of the argument list is intentional sudo -u miru -E env MIRU_ACTIVATION_TOKEN="$MIRU_ACTIVATION_TOKEN" /usr/sbin/miru-agent --install $args \ No newline at end of file diff --git a/scripts/jinja/templates/partials/utils/checksum.sh b/scripts/jinja/templates/partials/utils/checksum.sh index 04186fe3..aea28079 100644 --- a/scripts/jinja/templates/partials/utils/checksum.sh +++ b/scripts/jinja/templates/partials/utils/checksum.sh @@ -1,3 +1,4 @@ +# shellcheck shell=sh verify_checksum() { file=$1 expected_checksum=$2 diff --git a/scripts/jinja/templates/partials/utils/download.sh b/scripts/jinja/templates/partials/utils/download.sh index bc37697a..46768b72 100644 --- a/scripts/jinja/templates/partials/utils/download.sh +++ b/scripts/jinja/templates/partials/utils/download.sh @@ -1,6 +1,8 @@ +# shellcheck shell=sh INSTALLED_VERSION=$(dpkg-query -W -f='${Version}' "$AGENT_DEB_PKG_NAME" 2>/dev/null || echo "") -# replace '~' with '-' +# replace '~' with '-' if [ -n "$INSTALLED_VERSION" ]; then + # shellcheck disable=SC2001 # POSIX sh lacks ${var//search/replace}; sed is required INSTALLED_VERSION=$(echo "$INSTALLED_VERSION" | sed 's/~/-/g') fi diff --git a/scripts/jinja/templates/partials/utils/parse-from-pkg.sh b/scripts/jinja/templates/partials/utils/parse-from-pkg.sh index 70703a9c..baed9a44 100644 --- a/scripts/jinja/templates/partials/utils/parse-from-pkg.sh +++ b/scripts/jinja/templates/partials/utils/parse-from-pkg.sh @@ -1,3 +1,4 @@ +# shellcheck shell=sh if [ -n "$FROM_PKG" ]; then log "Installing from package on local machine: '$FROM_PKG'" if [ ! -f "$FROM_PKG" ]; then @@ -12,7 +13,9 @@ if [ -n "$FROM_PKG" ]; then if [ "$(dpkg -f "$FROM_PKG" Architecture)" != "$DEB_ARCH" ]; then fatal "The provided package architecture ($(dpkg -f "$FROM_PKG" Architecture)) does not match this machine's architecture ($DEB_ARCH)." fi + # shellcheck disable=SC2034 # consumed by a later partial in the rendered script AGENT_DEB_PKG=$FROM_PKG + # shellcheck disable=SC2034 # consumed by a later partial in the rendered script VERSION=$(dpkg -f "$FROM_PKG" Version) fi \ No newline at end of file diff --git a/scripts/jinja/templates/partials/utils/provision.sh b/scripts/jinja/templates/partials/utils/provision.sh index 9543811b..da0fd017 100644 --- a/scripts/jinja/templates/partials/utils/provision.sh +++ b/scripts/jinja/templates/partials/utils/provision.sh @@ -1,8 +1,10 @@ +# shellcheck shell=sh if [ -z "$MIRU_API_KEY" ]; then echo "MIRU_API_KEY is not set" exit 1 fi +# shellcheck disable=SC2153 # DEVICE_NAME is an external environment variable response_body=$(curl --request POST \ --url "$BACKEND_HOST"/v1/devices \ --header 'Content-Type: application/json' \ @@ -72,6 +74,7 @@ response_body=$(echo "$response_body" | head -n -1) # Check if the request succeeded if [ "$http_code" -eq 200 ] || [ "$http_code" -eq 201 ]; then log "Successfully created activation token" + # shellcheck disable=SC2034 # consumed by a later partial in the rendered script MIRU_ACTIVATION_TOKEN=$(echo "$response_body" | jq -r '.token') else error "Activation token request failed (HTTP status $http_code)" diff --git a/scripts/jinja/templates/partials/utils/version.sh b/scripts/jinja/templates/partials/utils/version.sh index 71b408f0..46d7eecc 100644 --- a/scripts/jinja/templates/partials/utils/version.sh +++ b/scripts/jinja/templates/partials/utils/version.sh @@ -1,3 +1,4 @@ +# shellcheck shell=sh if [ -z "$VERSION" ]; then if [ "$PRERELEASE" = true ]; then log "Fetching latest pre-release version..." diff --git a/scripts/lib/covgate.sh b/scripts/lib/covgate.sh index d5ecbeb7..ed88bb1b 100755 --- a/scripts/lib/covgate.sh +++ b/scripts/lib/covgate.sh @@ -71,7 +71,7 @@ find "$SRC_DIR" -name '.covgate' -type f | sort > "$covgate_list" while read -r covgate_file; do module_path=$(dirname "$covgate_file") - module_display="${module_path#$SRC_DIR/}" + module_display="${module_path#"$SRC_DIR"/}" # If .covgate is directly in SRC_DIR, display as the directory name if [ "$module_display" = "$module_path" ]; then module_display=$(basename "$SRC_DIR") diff --git a/scripts/lib/install-lint-deps.sh b/scripts/lib/install-lint-deps.sh index eb21e61d..b30488a3 100755 --- a/scripts/lib/install-lint-deps.sh +++ b/scripts/lib/install-lint-deps.sh @@ -75,3 +75,24 @@ if ! command_exists cargo-diet && ! has_cargo_subcommand cargo-diet; then echo "---------------------" install_cargo_tool cargo-diet fi + +# Surface lint tools: yamllint, shellcheck, actionlint. These power +# scripts/lint-surface.sh (YAML / shell / GitHub Actions linting). Each install +# is guarded by command_exists so re-running this script is idempotent. +if ! command_exists yamllint; then + echo "Installing yamllint" + echo "-------------------" + pip install --user yamllint || pipx install yamllint +fi +if ! command_exists shellcheck; then + echo "Installing shellcheck" + echo "---------------------" + # Debian/Ubuntu; adjust per platform if needed. + sudo apt-get install -y shellcheck +fi +if ! command_exists actionlint; then + echo "Installing actionlint" + echo "---------------------" + go install github.com/rhysd/actionlint/cmd/actionlint@latest \ + || curl -sSfL https://raw.githubusercontent.com/rhysd/actionlint/main/scripts/download-actionlint.bash | bash +fi diff --git a/scripts/lib/update-covgates.sh b/scripts/lib/update-covgates.sh index 42db3630..3ce32b2d 100755 --- a/scripts/lib/update-covgates.sh +++ b/scripts/lib/update-covgates.sh @@ -66,7 +66,7 @@ find "$SRC_DIR" -name '.covgate' -type f | sort > "$covgate_list" while read -r covgate_file; do module_path=$(dirname "$covgate_file") - module_display="${module_path#$SRC_DIR/}" + module_display="${module_path#"$SRC_DIR"/}" # If .covgate is directly in SRC_DIR, display as the directory name if [ "$module_display" = "$module_path" ]; then module_display=$(basename "$SRC_DIR") diff --git a/scripts/lint-surface.sh b/scripts/lint-surface.sh new file mode 100755 index 00000000..dc7521ae --- /dev/null +++ b/scripts/lint-surface.sh @@ -0,0 +1,34 @@ +#!/bin/sh +set -e + +command -v yamllint >/dev/null 2>&1 || { echo "error: yamllint not installed" >&2; exit 1; } +command -v shellcheck >/dev/null 2>&1 || { echo "error: shellcheck not installed" >&2; exit 1; } +command -v actionlint >/dev/null 2>&1 || { echo "error: actionlint not installed" >&2; exit 1; } + +REPO_ROOT=$(git rev-parse --show-toplevel) +cd "$REPO_ROOT" + +echo "yamllint" +echo "--------" +yamllint -c .yamllint.yml . +echo "" + +echo "shellcheck" +echo "----------" +# Exclude only the .agents subtree (content owned by another repo). Every other +# .sh file — including the shebang-less jinja partials under +# scripts/jinja/templates/ and the generated install scripts under +# scripts/install/ — is linted here, mirroring the shared surface-lint workflow. +# Findings in the generated scripts must still be fixed in the jinja templates, +# not the rendered output. +find . -name '*.sh' \ + ! -path './.agents/*' \ + -exec shellcheck {} + +echo "" + +echo "actionlint" +echo "----------" +actionlint +echo "" + +echo "Surface lint complete" diff --git a/scripts/lint.sh b/scripts/lint.sh index 9df6b6c4..2a0e5290 100755 --- a/scripts/lint.sh +++ b/scripts/lint.sh @@ -10,4 +10,13 @@ export ASSERT_LINT_PATHS="$REPO_ROOT/agent/tests" export IMPORT_LINT_CONFIG="$REPO_ROOT/.lint-imports.toml" export RUN_DIET="1" -exec "$REPO_ROOT/scripts/lib/lint.sh" +"$REPO_ROOT/scripts/lib/lint.sh" + +# Surface lint (YAML / shell / GitHub Actions) is enforced in CI by a dedicated +# job (.github/workflows/ci.yml: surface-lint, via the shared reusable +# workflow), which installs its own toolchain. Run it here only for local +# developer invocations so the Rust lint CI job need not install the surface +# tools. Mirrors the CI guard in scripts/lib/install-lint-deps.sh. +if [ "$CI" != "true" ]; then + "$REPO_ROOT/scripts/lint-surface.sh" +fi