Skip to content

Commit 594ea29

Browse files
authored
Merge pull request #33 from Expensify/Rory-ValidateGitHubActions
Check for unsafe action references in GitHub Actions
2 parents eb3f028 + fa3093e commit 594ea29

File tree

12 files changed

+374
-232
lines changed

12 files changed

+374
-232
lines changed

.github/actionlint.yml

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
# This file contains the config for actionlint.
2+
# See https://github.com/rhysd/actionlint/blob/main/docs/config.md
3+
#
4+
# Primarily, we use this config to define some large runners that are not registered in actionlint
5+
self-hosted-runner:
6+
labels:
7+
- ubuntu-latest-xl
8+
- macos-15-large
9+
- macos-15-xlarge
10+
- macos-12
11+
- ubuntu-24.04-v4
12+
13+
paths:
14+
'**/*':
15+
ignore:
16+
# This is meant to be a temporary workaround for a bug in actionlint. Upstream:
17+
# - issue: https://github.com/rhysd/actionlint/issues/511
18+
# - PR: https://github.com/rhysd/actionlint/pull/513
19+
- '"env" is not allowed in "runs" section because .* is a Composite action.*'

.github/scripts/actionlint.sh

Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
#!/bin/bash
2+
#################################################################
3+
# Lint workflows with https://github.com/rhysd/actionlint #
4+
#################################################################
5+
6+
# Verify that shellcheck is installed (preinstalled on GitHub Actions runners)
7+
if ! command -v shellcheck &>/dev/null; then
8+
error "This script requires shellcheck. Please install it and try again"
9+
exit 1
10+
fi
11+
12+
SCRIPT_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" &> /dev/null && pwd)"
13+
readonly SCRIPT_DIR
14+
15+
# To update the actionlint version, replace this file with the updated checksums file from the GitHub release
16+
readonly CHECKSUMS_FILE="$SCRIPT_DIR/actionlint_checksums.txt"
17+
readonly ACTIONLINT_PATH="$SCRIPT_DIR/actionlint"
18+
19+
source "$SCRIPT_DIR/shellUtils.sh"
20+
21+
title "Lint Github Actions via actionlint (https://github.com/rhysd/actionlint)"
22+
23+
# Get the actionlint tarball name from the checksums file, used both for downloading and verifying checksums
24+
OS="$(uname)"
25+
ARCH="$(uname -m)"
26+
readonly OS ARCH
27+
OS_ARCH=""
28+
if [[ "$OS" == "Darwin" && "$ARCH" == "arm64" ]]; then
29+
OS_ARCH="darwin_arm64"
30+
elif [[ "$OS" == "Linux" && "$ARCH" == "x86_64" ]]; then
31+
OS_ARCH="linux_amd64"
32+
elif [[ "$OS" == "Linux" && "$ARCH" == "aarch64" ]]; then
33+
OS_ARCH="linux_arm64"
34+
else
35+
error "Unknown architecture, unable to get actionlint tarball name" >&2
36+
exit 1
37+
fi
38+
readonly OS_ARCH
39+
TARBALL_NAME="$(grep "$OS_ARCH" "$CHECKSUMS_FILE" | awk '{print $2}')"
40+
readonly TARBALL_NAME
41+
42+
# Get the expected version and checksum of actionlint binary
43+
EXPECTED_VERSION="$(echo "$TARBALL_NAME" | grep -oE "actionlint_[0-9\.]+_" | awk -F_ '{print $2}')"
44+
EXPECTED_CHECKSUM="$(grep "$TARBALL_NAME" "$CHECKSUMS_FILE" | awk '{print $1}')"
45+
readonly EXPECTED_VERSION EXPECTED_CHECKSUM
46+
47+
# Get actionlint binary
48+
if [[ -x "$ACTIONLINT_PATH" && "$EXPECTED_VERSION" == "$("$ACTIONLINT_PATH" -version | head -n 1)" ]]; then
49+
info "Found actionlint version $EXPECTED_VERSION already installed" >&2
50+
else
51+
info "Downloading and verifying actionlint verion $EXPECTED_VERSION..." >&2
52+
53+
readonly TARBALL="$SCRIPT_DIR/actionlint.tar.gz"
54+
if ! curl -sL "https://github.com/rhysd/actionlint/releases/download/v${EXPECTED_VERSION}/${TARBALL_NAME}" -o "$TARBALL"; then
55+
error "Unable to download actionlint binary"
56+
exit 1
57+
fi
58+
59+
# Ensure tarball is cleaned up
60+
trap 'rm -f "$TARBALL"' EXIT
61+
ACTUAL_CHECKSUM="$(sha256sum "$TARBALL" | awk '{print $1}')"
62+
readonly ACTUAL_CHECKSUM
63+
64+
if [[ "$ACTUAL_CHECKSUM" != "$EXPECTED_CHECKSUM" ]] ; then
65+
error "Checksums did not match, expected $EXPECTED_CHECKSUM, got $ACTUAL_CHECKSUM" >&2
66+
exit 1
67+
fi
68+
69+
readonly TMPDIR="${SCRIPT_DIR}/actionlint-${EXPECTED_VERSION}"
70+
mkdir -p "$TMPDIR"
71+
72+
# It's only possible to have one exit trap, so we have to update to include both items we wish to remove
73+
trap 'rm -rf "$TMPDIR" "$TARBALL"' EXIT
74+
tar -C "$TMPDIR" -xzf "$TARBALL"
75+
mv "$TMPDIR/actionlint" "$SCRIPT_DIR/actionlint"
76+
77+
INSTALLED_VERSION="$("$SCRIPT_DIR/actionlint" -version | head -n 1)"
78+
readonly INSTALLED_VERSION
79+
info "Successfully installed actionlint version $INSTALLED_VERSION" >&2
80+
fi
81+
82+
info "Linting workflows..."
83+
echo
84+
if ! "$SCRIPT_DIR/actionlint" -color; then
85+
error "Workflows did not pass actionlint :("
86+
exit 1
87+
fi
88+
89+
success "Workflows passed actionlint!"
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
28e5de5a05fc558474f638323d736d822fff183d2d492f0aecb2b73cc44584f5 actionlint_1.7.7_darwin_amd64.tar.gz
2+
2693315b9093aeacb4ebd91a993fea54fc215057bf0da2659056b4bc033873db actionlint_1.7.7_darwin_arm64.tar.gz
3+
a6ee742126aa632b32009bdd6f820e0274c5b24536dc302a9574118378ee92f4 actionlint_1.7.7_freebsd_386.tar.gz
4+
f6eec0e5efd17183a954f0d88280885b7a58f39808050cdfcd7f068fc1734bc8 actionlint_1.7.7_freebsd_amd64.tar.gz
5+
01d4c173f411aeecf670d5219c008e07bf11539cf1181fdeee0cdb0eb8244aac actionlint_1.7.7_linux_386.tar.gz
6+
023070a287cd8cccd71515fedc843f1985bf96c436b7effaecce67290e7e0757 actionlint_1.7.7_linux_amd64.tar.gz
7+
401942f9c24ed71e4fe71b76c7d638f66d8633575c4016efd2977ce7c28317d0 actionlint_1.7.7_linux_arm64.tar.gz
8+
82e98d7252341b83fa557764824f225fa50431801b8e3d8e99f70dc1efc317b2 actionlint_1.7.7_linux_armv6.tar.gz
9+
66de2b65bcee17de1866287a513cadf47d812229e976e99024e9757645258adb actionlint_1.7.7_windows_386.zip
10+
7f12f1801bca3d480d67aaf7774f4c2a6359a3ca8eebe382c95c10c9704aa731 actionlint_1.7.7_windows_amd64.zip
11+
76e9514cfac18e5677aa04f3a89873c981f16a2f2353bb97372a86cd09b1f5a8 actionlint_1.7.7_windows_arm64.zip

.github/scripts/shellCheck.sh

Lines changed: 22 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -1,48 +1,34 @@
11
#!/bin/bash
22

3-
CURRENT_DIR=$(pwd)
4-
ROOT_DIR="$(dirname "$(dirname "$(dirname "$(realpath "${BASH_SOURCE[0]}")")")")"
3+
ROOT_DIR=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")/../.." &>/dev/null && pwd)
4+
readonly ROOT_DIR
55

6-
cd "$ROOT_DIR" || exit 1
6+
source "$ROOT_DIR"/.github/scripts/shellUtils.sh
77

8-
source ./.github/scripts/shellUtils.sh
9-
10-
declare -r DIRECTORIES_TO_IGNORE=(
11-
'./node_modules'
12-
'./vendor'
13-
'./ios/Pods'
14-
'./.husky'
15-
)
8+
readonly DIRECTORIES_TO_IGNORE="
9+
-path $ROOT_DIR/node_modules
10+
-o -path $ROOT_DIR/vendor
11+
-o -path $ROOT_DIR/ios/Pods
12+
-o -path $ROOT_DIR/.husky"
1613

1714
# This lists all shell scripts in this repo except those in directories we want to ignore
18-
read -ra IGNORE_DIRS < <(join_by_string ' -o -path ' "${DIRECTORIES_TO_IGNORE[@]}")
19-
SHELL_SCRIPTS=$(find . -type d \( -path "${IGNORE_DIRS[@]}" \) -prune -o -name '*.sh' -print)
20-
info "👀 Linting the following shell scripts using ShellCheck: $SHELL_SCRIPTS"
21-
info
22-
23-
ASYNC_PROCESSES=()
24-
for SHELL_SCRIPT in $SHELL_SCRIPTS; do
25-
if [[ "$CI" == 'true' ]]; then
26-
# ShellCheck is installed by default on GitHub Actions ubuntu runners
27-
shellcheck -e SC1091 "$SHELL_SCRIPT" &
28-
else
29-
# Otherwise shellcheck is used via npx
30-
npx shellcheck -e SC1091 "$SHELL_SCRIPT" &
31-
fi
32-
ASYNC_PROCESSES+=($!)
33-
done
15+
# Note: `-print` is required to prevent pruned directories from being printed
16+
# shellcheck disable=SC2086
17+
SHELL_SCRIPTS="$(find "$ROOT_DIR" -type d \( $DIRECTORIES_TO_IGNORE \) -prune -o -name '*.sh' -print)"
18+
info "👀 Linting the following shell scripts using ShellCheck:"
19+
echo "$SHELL_SCRIPTS"
20+
echo
3421

3522
EXIT_CODE=0
36-
for PID in "${ASYNC_PROCESSES[@]}"; do
37-
if ! wait "$PID"; then
38-
EXIT_CODE=1
39-
fi
23+
for SHELL_SCRIPT in $SHELL_SCRIPTS; do
24+
if ! shellcheck -e SC1091 "$SHELL_SCRIPT"; then
25+
EXIT_CODE=1
26+
fi
4027
done
4128

42-
cd "$CURRENT_DIR" || exit 1
43-
44-
if [ $EXIT_CODE == 0 ]; then
45-
success "ShellCheck passed for all files!"
29+
if [[ $EXIT_CODE -ne 0 ]]; then
30+
error "ShellCheck failed for one or more files"
31+
exit $EXIT_CODE
4632
fi
4733

48-
exit $EXIT_CODE
34+
success "ShellCheck passed for all files!"

.github/scripts/shellUtils.sh

Lines changed: 9 additions & 91 deletions
Original file line numberDiff line numberDiff line change
@@ -2,123 +2,41 @@
22

33
# Check if GREEN has already been defined
44
if [ -z "${GREEN+x}" ]; then
5-
declare -r GREEN=$'\e[1;32m'
5+
declare -r GREEN=$'\e[1;32m'
66
fi
77

88
# Check if RED has already been defined
99
if [ -z "${RED+x}" ]; then
10-
declare -r RED=$'\e[1;31m'
10+
declare -r RED=$'\e[1;31m'
1111
fi
1212

1313
# Check if BLUE has already been defined
1414
if [ -z "${BLUE+x}" ]; then
15-
declare -r BLUE=$'\e[1;34m'
15+
declare -r BLUE=$'\e[1;34m'
1616
fi
1717

1818
# Check if TITLE has already been defined
1919
if [ -z "${TITLE+x}" ]; then
20-
declare -r TITLE=$'\e[1;4;34m'
20+
declare -r TITLE=$'\e[1;4;34m'
2121
fi
2222

2323
# Check if RESET has already been defined
2424
if [ -z "${RESET+x}" ]; then
25-
declare -r RESET=$'\e[0m'
25+
declare -r RESET=$'\e[0m'
2626
fi
2727

2828
function success {
29-
echo "🎉 $GREEN$1$RESET"
29+
echo "🎉 $GREEN$1$RESET"
3030
}
3131

3232
function error {
33-
echo "💥 $RED$1$RESET"
33+
echo "💥 $RED$1$RESET"
3434
}
3535

3636
function info {
37-
echo "$BLUE$1$RESET"
37+
echo "$BLUE$1$RESET"
3838
}
3939

4040
function title {
41-
printf "\n%s%s%s\n" "$TITLE" "$1" "$RESET"
42-
}
43-
44-
# Function to clear the last printed line
45-
clear_last_line() {
46-
echo -ne "\033[1A\033[K"
47-
}
48-
49-
function assert_equal {
50-
if [[ "$1" != "$2" ]]; then
51-
error "Assertion failed: $1 is not equal to $2"
52-
exit 1
53-
else
54-
success "Assertion passed: $1 is equal to $1"
55-
fi
56-
}
57-
58-
# Usage: join_by_string <delimiter> ...strings
59-
# example: join_by_string ' + ' 'string 1' 'string 2'
60-
# example: join_by_string ',' "${ARRAY_OF_STRINGS[@]}"
61-
function join_by_string {
62-
local separator="$1"
63-
shift
64-
local first="$1"
65-
shift
66-
printf "%s" "$first" "${@/#/$separator}"
67-
}
68-
69-
# Usage: get_abs_path <path>
70-
# Will make a path absolute, resolving any relative paths
71-
# example: get_abs_path "./foo/bar"
72-
get_abs_path() {
73-
local the_path=$1
74-
local -a path_elements
75-
IFS='/' read -ra path_elements <<< "$the_path"
76-
77-
# If the path is already absolute, start with an empty string.
78-
# We'll prepend the / later when reconstructing the path.
79-
if [[ "$the_path" = /* ]]; then
80-
abs_path=""
81-
else
82-
abs_path="$(pwd)"
83-
fi
84-
85-
# Handle each path element
86-
for element in "${path_elements[@]}"; do
87-
if [ "$element" = "." ] || [ -z "$element" ]; then
88-
continue
89-
elif [ "$element" = ".." ]; then
90-
# Remove the last element from abs_path
91-
abs_path=$(dirname "$abs_path")
92-
else
93-
# Append element to the absolute path
94-
abs_path="${abs_path}/${element}"
95-
fi
96-
done
97-
98-
# Remove any trailing '/'
99-
while [[ $abs_path == */ ]]; do
100-
abs_path=${abs_path%/}
101-
done
102-
103-
# Special case for root
104-
[ -z "$abs_path" ] && abs_path="/"
105-
106-
# Special case to remove any starting '//' when the input path was absolute
107-
abs_path=${abs_path/#\/\//\/}
108-
109-
echo "$abs_path"
110-
}
111-
112-
# Function to read lines from standard input into an array using a temporary file.
113-
# This is a bash 3 polyfill for readarray.
114-
# Arguments:
115-
# $1: Name of the array variable to store the lines
116-
# Usage:
117-
# read_lines_into_array array_name
118-
read_lines_into_array() {
119-
local array_name="$1"
120-
local line
121-
while IFS= read -r line || [ -n "$line" ]; do
122-
eval "$array_name+=(\"$line\")"
123-
done
41+
printf "\n%s%s%s\n" "$TITLE" "$1" "$RESET"
12442
}

0 commit comments

Comments
 (0)