Create deploy status (#5)

* Create a simple status update with curl

* Add some lint and a new entrypoint.sh

* Use key-value rather than json resultfile

* Switch to pure shell

* Remove unused line

* Verbose it

* Correct way of getting deployment-id

* Use json in statusupdate

* string interpolation

* Clean up and lint it

* Rename to github-deploy

* Add licenses and README
This commit is contained in:
Ole Christian Langfjæran
2019-01-07 10:22:55 +01:00
committed by GitHub
parent c905238b76
commit 529a438c0b
18 changed files with 720 additions and 1 deletions

View File

@@ -0,0 +1,173 @@
# https://github.com/projectatomic/dockerfile_lint
---
profile:
name: "Default"
description: "Default Profile. Checks basic syntax."
line_rules:
LABEL:
paramSyntaxRegex: /.+/
defined_namevals:
Name:
valueRegex: /[\w]+/
message: "Label 'name' is missing or has invalid format"
level: "error"
required: true
Version:
valueRegex: /[\w.${}()"'\\\/~<>\-?\%:]+/
message: "Label 'version' is missing or has invalid format"
level: "error"
required: true
Maintainer:
valueRegex: /[\w]+/
message: "Label 'maintainer' is missing or has invalid format"
level: "error"
required: true
FROM:
paramSyntaxRegex: /^[\w./\-:]+(:[\w.]+)?(-[\w]+)?( as \w+)?$/i
rules:
-
label: "is_latest_tag"
regex: /latest/
level: "error"
message: "base image uses 'latest' tag"
description: "using the 'latest' tag may cause unpredictable builds. It is recommended that a specific tag is used in the FROM line or *-released which is the latest supported release."
reference_url:
- "https://docs.docker.com/engine/reference/builder/"
- "#from"
-
label: "no_tag"
regex: /^[:]/
level: "error"
message: "No tag is used"
description: "lorem ipsum tar"
reference_url:
- "https://docs.docker.com/engine/reference/builder/"
- "#from"
RUN:
paramSyntaxRegex: /.+/
rules:
-
label: "no_yum_clean_all"
regex: /yum(?!.+clean all|.+\.repo|-config|\.conf)/g
level: "warn"
message: "yum clean all is not used"
description: "the yum cache will remain in this layer making the layer unnecessarily large"
reference_url:
- "http://docs.projectatomic.io/container-best-practices/#"
- "_clear_packaging_caches_and_temporary_package_downloads"
-
label: "yum_update_all"
regex: /yum(.+update all|.+upgrade|.+update|\.config)/
level: "info"
message: "updating the entire base image may add unnecessary size to the container"
description: "update the entire base image may add unnecessary size to the container"
reference_url:
- "http://docs.projectatomic.io/container-best-practices/#"
- "_clear_packaging_caches_and_temporary_package_downloads"
-
label: "no_dnf_clean_all"
regex: /dnf(?!.+clean all|.+\.repo)/g
level: "warn"
message: "dnf clean all is not used"
description: "the dnf cache will remain in this layer making the layer unnecessarily large"
reference_url:
- "http://docs.projectatomic.io/container-best-practices/#"
- "_clear_packaging_caches_and_temporary_package_downloads"
-
label: "no_rvm_cleanup_all"
regex: /rvm install(?!.+cleanup all)/g
level: "warn"
message: "rvm cleanup is not used"
description: "the rvm cache will remain in this layer making the layer unnecessarily large"
reference_url:
- "http://docs.projectatomic.io/container-best-practices/#"
- "_clear_packaging_caches_and_temporary_package_downloads"
-
label: "no_gem_clean_all"
regex: /gem install(?!.+cleanup|.+\rvm cleanup all)/g
level: "warn"
message: "gem cleanup all is not used"
description: "the gem cache will remain in this layer making the layer unnecessarily large"
reference_url:
- "http://docs.projectatomic.io/container-best-practices/#"
- "_clear_packaging_caches_and_temporary_package_downloads"
-
label: "no_apt-get_clean"
regex: /apt-get install(?!.+clean)/g
level: "warn"
message: "apt-get clean is not used"
description: "the apt-get cache will remain in this layer making the layer unnecessarily large"
reference_url:
- "http://docs.projectatomic.io/container-best-practices/#"
- "_clear_packaging_caches_and_temporary_package_downloads"
-
label: "privileged_run_container"
regex: /privileged/
level: "warn"
message: "a privileged run container is allowed access to host devices"
description: "Does this run need to be privileged?"
reference_url:
- "http://docs.docker.com/engine/reference/run/#"
- "runtime-privilege-and-linux-capabilities"
-
label: "installing_ssh"
regex: /openssh-server/
level: "warn"
message: "installing SSH in a container is not recommended"
description: "Do you really need SSH in this image?"
reference_url: "https://github.com/jpetazzo/nsenter"
-
label: "no_ampersand_usage"
regex: / ; /
level: "warn"
message: "using ; instead of &&"
description: "RUN do_1 && do_2: The ampersands change the resulting evaluation into do_1 and then do_2 only if do_1 was successful."
reference_url:
- "http://docs.projectatomic.io/container-best-practices/#"
- "#_using_semi_colons_vs_double_ampersands"
EXPOSE:
paramSyntaxRegex: /^[\d-\s\w/\\]+$/
rules: []
ENV:
paramSyntaxRegex: /.+/
rules: []
ADD:
paramSyntaxRegex: /^~?([\w-.~:/?#\[\]\\\/*@!$&'()*+,;=.{}"]+[\s]*)+$/
COPY:
paramSyntaxRegex: /.+/
rules: []
ENTRYPOINT:
paramSyntaxRegex: /.+/
rules: []
VOLUME:
paramSyntaxRegex: /.+/
rules: []
USER:
paramSyntaxRegex: /^[a-z0-9_][a-z0-9_]{0,40}$/
rules: []
WORKDIR:
paramSyntaxRegex: /^~?[\w\d-\/.{}$\/:]+[\s]*$/
rules: []
ONBUILD:
paramSyntaxRegex: /.+/
rules: []
required_instructions:
-
instruction: "ENTRYPOINT"
count: 1
level: "info"
message: "There is no 'ENTRYPOINT' instruction"
description: "None"
reference_url:
- "https://docs.docker.com/engine/reference/builder/"
- "#entrypoint"
-
instruction: "CMD"
count: 1
level: "info"
message: "There is no 'CMD' instruction"
description: "None"
reference_url:
- "https://docs.docker.com/engine/reference/builder/"
- "#cmd"

View File

@@ -0,0 +1,138 @@
# https://github.com/projectatomic/dockerfile_lint
profile:
name: "GitHub Actions"
description: "Checks for GitHub Actions."
includes:
- default_rules.yaml
general:
# It appears these get duplicated rather than overriding. The hope was to use this as a counter to the
# `required_instructions` section, but perhaps it defines the `line_rules` map. It would be great to either be able
# to set `required_instructions` to a 0 value or have an `invalid_instructions` section?
valid_instructions:
- FROM
- RUN
- CMD
- LABEL
- ENV
- ADD
- COPY
- ENTRYPOINT
- WORKDIR
- ONBUILD
- ARG
- STOPSIGNAL
- SHELL
line_rules:
# Invalid Lines
ADD:
paramSyntaxRegex: /.+/
rules:
-
label: "add_antipattern"
regex: /.+/
level: "info"
message: "Avoid using ADD"
description: "It is generally an anti-pattern to us ADD, use COPY instead."
EXPOSE:
paramSyntaxRegex: /.+/
rules:
-
label: "expose_invalid"
regex: /.+/
level: "error"
message: "There should not be an 'EXPOSE' instruction"
description: "Actions should not expose ports."
HEALTHCHECK:
paramSyntaxRegex: /.+/
rules:
-
label: "healthcheck_invalid"
regex: /.+/
level: "error"
message: "There should not be a 'HEALTHCHECK' instruction"
description: "Actions should not require HEALTHCHECKs."
MAINTAINER:
paramSyntaxRegex: /.+/
rules:
-
label: "maintainer_deprecated"
regex: /.+/
level: "info"
message: "the MAINTAINER command is deprecated"
description: "MAINTAINER is deprecated in favor of using LABEL since Docker v1.13.0"
reference_url:
- "https://github.com/docker/cli/blob/master/docs/deprecated.md"
- "#maintainer-in-dockerfile"
SHELL:
paramSyntaxRegex: /.+/
rules:
-
label: "shell_invalid"
regex: /.+/
level: "info"
message: "There should not be a 'SHELL' instruction"
description: "Actions generally rely on sh and setting an alternative shell may have unexpected consequences."
USER:
paramSyntaxRegex: /.+/
rules:
-
label: "user_discouraged"
regex: /.+/
level: "warn"
message: "'USER' instruction exists"
description: "Actions don't expect a USER to be set."
VOLUME:
paramSyntaxRegex: /.+/
rules:
-
label: "volume_invalid"
regex: /.+/
level: "error"
message: "There should not be a 'VOLUME' instruction"
description: "Actions do not support volumes."
# Required Labels
LABEL:
paramSyntaxRegex: /.+/
defined_namevals:
com.github.actions.name:
valueRegex: /[\w]+/
message: "Label 'com.github.actions.name' is missing or has invalid format"
level: "error"
required: true
com.github.actions.description:
valueRegex: /[\w]+/
message: "Label 'com.github.actions.description' is missing or has invalid format"
level: "error"
required: true
com.github.actions.icon:
valueRegex: /[\w]+/
message: "Label 'com.github.actions.icon' is missing or has invalid format"
level: "error"
required: true
com.github.actions.color:
valueRegex: /[\w]+/
message: "Label 'com.github.actions.color' is missing or has invalid format"
level: "error"
required: true
required_instructions:
-
instruction: "ENTRYPOINT"
count: 1
level: "error"
message: "There is no 'ENTRYPOINT' instruction"
description: "Actions require that a default ENTRYPOINT be set"
reference_url:
- "https://docs.docker.com/engine/reference/builder/"
- "#entrypoint"
-
instruction: "CMD"
count: 1
level: "info"
message: "There is no 'CMD' instruction"
description: "In most cases it is helpful to include reasonable defaults for CMD"
reference_url:
- "https://docs.docker.com/engine/reference/builder/"
- "#cmd"

1
.gitignore vendored Normal file
View File

@@ -0,0 +1 @@
.idea/

25
Makefile Normal file
View File

@@ -0,0 +1,25 @@
export ROOT_DIR=$(CURDIR)
export DOCKER_REPO=unacast
MODULES=$(dir $(wildcard */Makefile))
.PHONY: clean
clean: ## Call the 'clean' target on all sub-modules
$(foreach mod,$(MODULES),($(MAKE) -C $(mod) $@) || exit $$?;)
.PHONY: lint
lint: ## Call the 'lint' target on all sub-modules
$(foreach mod,$(MODULES),($(MAKE) -C $(mod) $@) || exit $$?;)
.PHONY: build
build: ## Call the 'build' target on all sub-modules
$(foreach mod,$(MODULES),($(MAKE) -C $(mod) $@) || exit $$?;)
.PHONY: test
test: ## Call the 'test' target on all sub-modules
$(foreach mod,$(MODULES),($(MAKE) -C $(mod) $@) || exit $$?;)
.PHONY: dev-all
dev-all: lint build test
include help.mk

View File

@@ -1,2 +1,2 @@
# actions
Github Actions
Various Github Actions

17
docker.mk Normal file
View File

@@ -0,0 +1,17 @@
IMAGE_NAME=$(shell basename $(CURDIR))
.PHONY: docker-lint
docker-lint: ## Run Dockerfile Lint on all dockerfiles.
$(COMMAND) dockerfile_lint -r $(ROOT_DIR)/.dockerfile_lint/github_actions.yaml $(wildcard Dockerfile* */Dockerfile*)
.PHONY: docker-build
docker-build: ## Build the top level Dockerfile using the directory or $IMAGE_NAME as the name.
docker build -t $(IMAGE_NAME) .
.PHONY: docker-tag
docker-tag: ## Tag the docker image using the tag script.
tag $(IMAGE_NAME) $(DOCKER_REPO)/$(IMAGE_NAME) --no-latest
.PHONY: docker-publish
docker-publish: docker-tag ## Publish the image and tags to a repository.
docker push $(DOCKER_REPO)/$(IMAGE_NAME)

19
github-deploy/Dockerfile Normal file
View File

@@ -0,0 +1,19 @@
FROM alpine
LABEL "name"="deploystatus"
LABEL "maintainer"="Unacast <developers+github@unacast.com>"
LABEL "version"="1.0.0"
LABEL "com.github.actions.name"="Deploy status"
LABEL "com.github.actions.description"="Create a deploy status"
LABEL "com.github.actions.icon"="upload"
LABEL "com.github.actions.color"="green"
RUN mkdir /deploy-scripts
COPY ./bin /deploy-scripts/bin
COPY "entrypoint.sh" "/entrypoint.sh"
ENTRYPOINT ["/entrypoint.sh"]
CMD [""]

View File

@@ -0,0 +1,15 @@
Apache License, Version 2.0
Copyright (c) 2011 Dominic Tarr
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.

22
github-deploy/LICENSE.MIT Normal file
View File

@@ -0,0 +1,22 @@
The MIT License (MIT)
Copyright (c) 2018 GitHub, Inc. and contributors
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.

18
github-deploy/Makefile Normal file
View File

@@ -0,0 +1,18 @@
include ../docker.mk
include ../help.mk
include ../shell.mk
.PHONY: clean
clean: ## Clean up after the build process.
.PHONY: lint
lint: shell-lint docker-lint ## Lint all of the files for this Action.
.PHONY: build
build: docker-build ## Build this Action.
.PHONY: test
test: ## Test the components of this Action.
.PHONY: publish
publish: docker-publish ## Publish this Action.

45
github-deploy/README.md Normal file
View File

@@ -0,0 +1,45 @@
# github-deploy
This actions adds functions for interacting with Github Deployment API and a Workflow instantiated from a deployment event
## Usage
The action installs functions in `${HOME}/bin`, for which you can use in later `action`s which does the actual deploy. An example workflow might look this:
```
workflow "On deploy" {
on = "deployment"
resolves = ["Deploy"]
}
action "Add deployscripts" {
uses = "unacast/actions/github-deploy@master"
}
action "Deploy" {
uses = "docker://alpine"
secrets = ["GITHUB_TOKEN"]
args = "./scripts/deploy.sh"
needs = ["Add deployscripts"]
}
```
Furthermore, in the "`deploy.sh`" of your choice, you can utilize the following script commands from ${HOME}/bin:
### deployment-get-environment
This gets the submitted environment from within ${GITHUB_EVENT_PATH}. For example `production`
### deployment-get-id
This gets the created deployment-id from within ${GITHUB_EVENT_PATH}. Usually you won't have to deal with this, it is for most purposes used by `deployment-set-status`
### deployment-create-status
This function updates the Deployment API with the result of your deploy. For example `${HOME}/bin/deployment-create-status success` creates a success-status on the deployment event.
`success` can be replaced with the other allowed statuses (`error`, `failure`, `inactive`, `in_progress`, `queued` or `pending`) from the [Create a deployment status
endpoint](https://developer.github.com/v3/repos/deployments/#create-a-deployment-status)
## License
The Dockerfile and associated scripts and documentation in this project are released under the [MIT License](LICENSE.MIT) and [Apache 2](LICENSE.APACHE2).
It uses the excellent [JSON.sh](https://github.com/dominictarr/JSON.sh) for json parsing.

210
github-deploy/bin/JSON.sh Executable file
View File

@@ -0,0 +1,210 @@
#!/bin/sh
# Source is from the great https://github.com/dominictarr/JSON.sh
throw() {
echo "$*" >&2
exit 1
}
BRIEF=0
LEAFONLY=0
PRUNE=0
NO_HEAD=0
NORMALIZE_SOLIDUS=0
usage() {
echo
echo "Usage: JSON.sh [-b] [-l] [-p] [-s] [-h]"
echo
echo "-p - Prune empty. Exclude fields with empty values."
echo "-l - Leaf only. Only show leaf nodes, which stops data duplication."
echo "-b - Brief. Combines 'Leaf only' and 'Prune empty' options."
echo "-n - No-head. Do not show nodes that have no path (lines that start with [])."
echo "-s - Remove escaping of the solidus symbol (straight slash)."
echo "-h - This help text."
echo
}
parse_options() {
set -- "$@"
local ARGN=$#
while [ "$ARGN" -ne 0 ]
do
case $1 in
-h) usage
exit 0
;;
-b) BRIEF=1
LEAFONLY=1
PRUNE=1
;;
-l) LEAFONLY=1
;;
-p) PRUNE=1
;;
-n) NO_HEAD=1
;;
-s) NORMALIZE_SOLIDUS=1
;;
?*) echo "ERROR: Unknown option."
usage
exit 0
;;
esac
shift 1
ARGN=$((ARGN-1))
done
}
awk_egrep () {
local pattern_string=$1
gawk '{
while ($0) {
start=match($0, pattern);
token=substr($0, start, RLENGTH);
print token;
$0=substr($0, start+RLENGTH);
}
}' pattern="$pattern_string"
}
tokenize () {
local GREP
local ESCAPE
local CHAR
if echo "test string" | egrep -ao --color=never "test" >/dev/null 2>&1
then
GREP='egrep -ao --color=never'
else
GREP='egrep -ao'
fi
if echo "test string" | egrep -o "test" >/dev/null 2>&1
then
ESCAPE='(\\[^u[:cntrl:]]|\\u[0-9a-fA-F]{4})'
CHAR='[^[:cntrl:]"\\]'
else
GREP=awk_egrep
ESCAPE='(\\\\[^u[:cntrl:]]|\\u[0-9a-fA-F]{4})'
CHAR='[^[:cntrl:]"\\\\]'
fi
local STRING="\"$CHAR*($ESCAPE$CHAR*)*\""
local NUMBER='-?(0|[1-9][0-9]*)([.][0-9]*)?([eE][+-]?[0-9]*)?'
local KEYWORD='null|false|true'
local SPACE='[[:space:]]+'
# Force zsh to expand $A into multiple words
local is_wordsplit_disabled=$(unsetopt 2>/dev/null | grep -c '^shwordsplit$')
if [ $is_wordsplit_disabled != 0 ]; then setopt shwordsplit; fi
$GREP "$STRING|$NUMBER|$KEYWORD|$SPACE|." | egrep -v "^$SPACE$"
if [ $is_wordsplit_disabled != 0 ]; then unsetopt shwordsplit; fi
}
parse_array () {
local index=0
local ary=''
read -r token
case "$token" in
']') ;;
*)
while :
do
parse_value "$1" "$index"
index=$((index+1))
ary="$ary""$value"
read -r token
case "$token" in
']') break ;;
',') ary="$ary," ;;
*) throw "EXPECTED , or ] GOT ${token:-EOF}" ;;
esac
read -r token
done
;;
esac
[ "$BRIEF" -eq 0 ] && value=$(printf '[%s]' "$ary") || value=
:
}
parse_object () {
local key
local obj=''
read -r token
case "$token" in
'}') ;;
*)
while :
do
case "$token" in
'"'*'"') key=$token ;;
*) throw "EXPECTED string GOT ${token:-EOF}" ;;
esac
read -r token
case "$token" in
':') ;;
*) throw "EXPECTED : GOT ${token:-EOF}" ;;
esac
read -r token
parse_value "$1" "$key"
obj="$obj$key:$value"
read -r token
case "$token" in
'}') break ;;
',') obj="$obj," ;;
*) throw "EXPECTED , or } GOT ${token:-EOF}" ;;
esac
read -r token
done
;;
esac
[ "$BRIEF" -eq 0 ] && value=$(printf '{%s}' "$obj") || value=
:
}
parse_value () {
local jpath="${1:+$1,}$2" isleaf=0 isempty=0 print=0
case "$token" in
'{') parse_object "$jpath" ;;
'[') parse_array "$jpath" ;;
# At this point, the only valid single-character tokens are digits.
''|[!0-9]) throw "EXPECTED value GOT ${token:-EOF}" ;;
*) value=$token
# if asked, replace solidus ("\/") in json strings with normalized value: "/"
[ "$NORMALIZE_SOLIDUS" -eq 1 ] && value=$(echo "$value" | sed 's#\\/#/#g')
isleaf=1
[ "$value" = '""' ] && isempty=1
;;
esac
[ "$value" = '' ] && return
[ "$NO_HEAD" -eq 1 ] && [ -z "$jpath" ] && return
[ "$LEAFONLY" -eq 0 ] && [ "$PRUNE" -eq 0 ] && print=1
[ "$LEAFONLY" -eq 1 ] && [ "$isleaf" -eq 1 ] && [ $PRUNE -eq 0 ] && print=1
[ "$LEAFONLY" -eq 0 ] && [ "$PRUNE" -eq 1 ] && [ "$isempty" -eq 0 ] && print=1
[ "$LEAFONLY" -eq 1 ] && [ "$isleaf" -eq 1 ] && \
[ $PRUNE -eq 1 ] && [ $isempty -eq 0 ] && print=1
[ "$print" -eq 1 ] && printf "[%s]\t%s\n" "$jpath" "$value"
:
}
parse () {
read -r token
parse_value
read -r token
case "$token" in
'') ;;
*) throw "EXPECTED EOF GOT $token" ;;
esac
}
if ([ "$0" = "$BASH_SOURCE" ] || ! [ -n "$BASH_SOURCE" ]);
then
parse_options "$@"
tokenize | parse
fi
# vi: expandtab sw=2 ts=2

View File

@@ -0,0 +1,9 @@
#!/bin/sh
BASEDIR=$(dirname "$0")
DEPLOYMENT_ID=$(${BASEDIR}/deployment-get-id)
echo "Setting status to ${1} for deployment-id '${DEPLOYMENT_ID}'"
curl --silent --show-error --fail -X POST -H "Authorization: token ${GITHUB_TOKEN}" \
--data '{"state":"'"${1}"'"}' \
"https://api.github.com/repos/${GITHUB_REPOSITORY}/deployments/${DEPLOYMENT_ID}/statuses"

View File

@@ -0,0 +1,5 @@
#!/bin/sh
BASEDIR=$(dirname "$0")
cat ${GITHUB_EVENT_PATH} | ${BASEDIR}/JSON.sh | grep '\["deployment","environment"]' | cut -f2 | sed -e 's/^"//' -e 's/"$//'

View File

@@ -0,0 +1,5 @@
#!/bin/sh
BASEDIR=$(dirname "$0")
cat ${GITHUB_EVENT_PATH} | ${BASEDIR}/JSON.sh | grep '\["deployment","id"]' | cut -f2

4
github-deploy/entrypoint.sh Executable file
View File

@@ -0,0 +1,4 @@
#!/bin/sh
# Copy all files to USER Home, so that they are available for later actions
cp -R /deploy-scripts/bin "${HOME}"/.

3
help.mk Normal file
View File

@@ -0,0 +1,3 @@
.PHONY: help
help:
@grep -E '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | sort | sed 's/^[^:]*://g' | awk 'BEGIN {FS = ":.*?## "}; {printf "\033[36m%-30s\033[0m %s\n", $$1, $$2}'

10
shell.mk Normal file
View File

@@ -0,0 +1,10 @@
SHELL_FILES=$(filter-out bin/JSON.sh, $(wildcard *.sh */*.sh))
BATS_TESTS=$(wildcard *.bats */*.bats)
.PHONY: shell-lint
shell-lint:
shellcheck $(SHELL_FILES)
.PHONY: shell-test
shell-test:
bats $(BATS_TESTS)