diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 00000000..bca43d8e --- /dev/null +++ b/.editorconfig @@ -0,0 +1,38 @@ +# https://editorconfig.org +root = true + +[*] +charset = utf-8 +end_of_line = lf +indent_style = space +insert_final_newline = true +trim_trailing_whitespace = true + +[*.sh] +binary_next_line = true +# We use tabs in shell scripts since heredoc indent stripping (<<-) requires them: +# https://www.gnu.org/software/bash/manual/html_node/Redirections.html#Here-Documents +indent_style = tab +shell_variant = bash +switch_case_indent = true + +# Catches scripts without a .sh file extension, such as the Buildpack API scripts. +[**/bin/**] +binary_next_line = true +indent_style = tab +shell_variant = bash +switch_case_indent = true + +# Catches sbin/ scripts without a .sh extension. +[**/sbin/**] +binary_next_line = true +indent_style = tab +shell_variant = bash +switch_case_indent = true + +# Vendored third-party script that must not be modified. +[test/shunit2.sh] +ignore = true + +[Makefile] +indent_style = tab diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 6148c9bd..3abe775e 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -10,7 +10,22 @@ on: permissions: contents: read +env: + FORCE_COLOR: 1 + jobs: + lint: + runs-on: ubuntu-24.04 + steps: + - name: Checkout + uses: actions/checkout@v6 + - name: Run ShellCheck + run: make lint-scripts + - name: Run shfmt + uses: docker://mvdan/shfmt:latest + with: + args: "--diff ." + integration-test: runs-on: ubuntu-latest strategy: diff --git a/.shellcheckrc b/.shellcheckrc new file mode 100644 index 00000000..da9f6a16 --- /dev/null +++ b/.shellcheckrc @@ -0,0 +1,2 @@ +# Enable all checks, including the optional ones that are off by default. +enable=all diff --git a/Makefile b/Makefile index de837f1c..41cb7472 100644 --- a/Makefile +++ b/Makefile @@ -1,4 +1,17 @@ -.PHONY: test test-parallel run run-ci publish +.PHONY: lint lint-scripts check-format format test test-parallel run run-ci publish + +lint: lint-scripts check-format + +lint-scripts: + @git ls-files -z --cached --others --exclude-standard 'bin/*' 'sbin/*' '*.sh' \ + | grep -zv '^test/shunit2\.sh$$' \ + | xargs -0 shellcheck --check-sourced --color=always + +check-format: + @shfmt --diff . + +format: + @shfmt --write --list . STACK ?= heroku-24 FIXTURE ?= test/fixtures/mod-basic-go126 diff --git a/bin/compile b/bin/compile index 9a57a0ea..2f9f0561 100755 --- a/bin/compile +++ b/bin/compile @@ -1,23 +1,23 @@ -#!/bin/bash +#!/usr/bin/env bash # usage: bin/compile -set -eo pipefail +set -euo pipefail -[ "$BUILDPACK_XTRACE" ] && set -o xtrace +[[ -n "${BUILDPACK_XTRACE:-}" ]] && set -o xtrace -mkdir -p "$1" "$2" -build=$(cd "$1/" && pwd) -cache_root=$(cd "$2/" && pwd) -cache="${cache_root}/.heroku/go" -mkdir -p "${cache}" -env_dir="${3}" -buildpack=$(cd "$(dirname $0)/.." && pwd) +BUILD_DIR="${1}" +CACHE_DIR="${2}" +ENV_DIR="${3}" -source "${buildpack}/lib/common.sh" +GO_CACHE_DIR="${CACHE_DIR}/.heroku/go" +mkdir -p "${GO_CACHE_DIR}" -# Set CACHE_DIR (used by build_data) -CACHE_DIR="${cache_root}" -source "${buildpack}/lib/build_data.sh" +# The absolute path to the root of the buildpack. +BUILDPACK_DIR=$(cd "$(dirname "$(dirname "${BASH_SOURCE[0]}")")" && pwd) + +source "${BUILDPACK_DIR}/lib/output.sh" +source "${BUILDPACK_DIR}/lib/common.sh" +source "${BUILDPACK_DIR}/lib/build_data.sh" compile_start_time=$(build_data::current_unix_realtime) @@ -30,38 +30,39 @@ snapshotBinBefore # Clean up old cache files if migrating from old cache structure. # We detect this legacy structure by looking for 'go-path' in the root. -if [ -d "${cache_root}/go-path" ] && [ ! -d "${cache}/go-path" ]; then - output::step "Clearing old build cache artifacts" - - # Remove artifacts that were previously stored in the cache root - rm -rf "${cache_root}"/go[0-9]* \ - "${cache_root}"/devel-* \ - "${cache_root}/go-path" \ - "${cache_root}/go-build-cache" \ - "${cache_root}/glide" \ - "${cache_root}/gb" \ - "${cache_root}/dep" \ - "${cache_root}/godep" \ - "${cache_root}/govendor" \ - "${cache_root}/.tq" \ - "${cache_root}/.jq" \ - "${cache_root}/github.com/golang-migrate" \ - "${cache_root}/github.com/mattes" 2>/dev/null || true +if [[ -d "${CACHE_DIR}/go-path" ]] && [[ ! -d "${GO_CACHE_DIR}/go-path" ]]; then + output::step "Clearing old build cache artifacts" + + # Remove artifacts that were previously stored in the cache root + rm -rf "${CACHE_DIR}"/go[0-9]* \ + "${CACHE_DIR}"/devel-* \ + "${CACHE_DIR}/go-path" \ + "${CACHE_DIR}/go-build-cache" \ + "${CACHE_DIR}/glide" \ + "${CACHE_DIR}/gb" \ + "${CACHE_DIR}/dep" \ + "${CACHE_DIR}/godep" \ + "${CACHE_DIR}/govendor" \ + "${CACHE_DIR}/.tq" \ + "${CACHE_DIR}/.jq" \ + "${CACHE_DIR}/github.com/golang-migrate" \ + "${CACHE_DIR}/github.com/mattes" 2>/dev/null || true fi -DefaultGoVersion="$(<${DataJSON} jq -r '.Go.DefaultVersion')" +# shellcheck disable=SC2154 +DefaultGoVersion="$(<"${DataJSON}" jq -r '.Go.DefaultVersion')" handleDefaultPkgSpec() { - if [ "${pkgs}" = "default" ]; then - output::warning <<-EOF + if [[ "${pkgs}" = "default" ]]; then + output::warning <<-EOF Installing package '.' (default) To install a different package spec add a comment in the following form to your go.mod file: // +heroku install ./cmd/... EOF - pkgs="." - fi + pkgs="." + fi } # Expand a version identifier to supported versions of Go. If the version @@ -70,130 +71,142 @@ handleDefaultPkgSpec() { # data.json for supported expansions. All others are returned as is without go # prepended to it. expandVer() { - local v="${1}" - if [[ "${v}" =~ ^[[:digit:]]+ ]]; then - v="go${v}" - fi - echo $(<${DataJSON} jq -r 'if .Go.VersionExpansion."'${v}'" then .Go.VersionExpansion."'${v}'" else "'${v}'" end') + local v="${1}" + if [[ "${v}" =~ ^[[:digit:]]+ ]]; then + v="go${v}" + fi + # shellcheck disable=SC2310 + <"${DataJSON}" jq -r 'if .Go.VersionExpansion."'"${v}"'" then .Go.VersionExpansion."'"${v}"'" else "'"${v}"'" end' } # Report development versions to user # Use after expandVer reportVer() { - local ver="${1}" - case $ver in - devel*) - output::warning <<-EOF + local ver="${1}" + case ${ver} in + devel*) + output::warning <<-EOF You are using a development build of Go. This is provided for users requiring an unreleased Go version but is otherwise unsupported. Build tests are NOT RUN!! EOF - ;; - esac + ;; + *) ;; + esac } ensureGo() { - local goVersion="${1}" - local goPath="${cache}/${goVersion}/go" - local goFile="" - local txt="Installing ${goVersion}" - if [ -d "${goPath}" ]; then - output::step "Using ${goVersion}" - else - #For a go version change, we delete everything in our namespace - output::step "New Go Version, clearing old cache" - if [ -d "${cache}/go-path" ]; then - find "${cache}/go-path" ! -perm -u=w -print0 | xargs -r -0 chmod u+w 2>&1 - fi - rm -rf ${cache}/* - case "${goVersion}" in - devel*) - local bGoVersion="$(expandVer ${DefaultGoVersion})" - goFile="${bGoVersion}.linux-amd64.tar.gz" - goPath="${cache}/${bGoVersion}/go" - txt="Installing bootstrap ${bGoVersion}" - ;; - go1) - goFile="go.go1.linux-amd64.tar.gz" - ;; - *) - goFile="${goVersion}.linux-amd64.tar.gz" - ;; - esac - - output::step "${txt}" - ensureFile "${goFile}" "${goPath}" "tar -C ${goPath} --strip-components=1 -zxf" - rm -f "${goPath}/${goFile}" - - case "${goVersion}" in - devel*) - pushd "${cache}" &> /dev/null - mkdir -p "${goVersion}" - pushd "${goVersion}" &> /dev/null - local sha=$(echo ${goVersion} | cut -d - -f 2) #assumes devel- or devel- - local url="https://github.com/golang/go/archive/$sha.tar.gz" - output::step "Downloading development Go version ${goVersion}" - ${CURL} ${url} | tar zxf - - mv go-${sha}* go - output::step "Compiling development Go version ${goVersion}" - pushd go/src &> /dev/null - echo "devel +${sha} $(date "+%a %b %H:%M:%S %G %z")"> ../VERSION - GOROOT_BOOTSTRAP=$(pushd ${cache}/${bGoVersion}/go > /dev/null; pwd; popd > /dev/null) ./make.bash 2>&1 - popd &> /dev/null - go/bin/go version - rm -rf "${goPath}" - popd &> /dev/null - popd &> /dev/null - goPath="${cache}/${goVersion}/go" - ;; - *) - ;; - esac - fi - - export GOROOT="${goPath}" - PATH="${goPath}/bin:${PATH}" - - # Export GOCACHE if Go >= 1.10 - if go env | grep -q '^GOCACHE='; then - export GOCACHE="${cache}/go-build-cache" - fi + local goVersion="${1}" + local goPath="${GO_CACHE_DIR}/${goVersion}/go" + local goFile="" + local txt="Installing ${goVersion}" + if [[ -d "${goPath}" ]]; then + output::step "Using ${goVersion}" + else + #For a go version change, we delete everything in our namespace + output::step "New Go Version, clearing old cache" + if [[ -d "${GO_CACHE_DIR}/go-path" ]]; then + find "${GO_CACHE_DIR}/go-path" ! -perm -u=w -print0 | xargs -r -0 chmod u+w 2>&1 + fi + rm -rf "${GO_CACHE_DIR:?}"/* + case "${goVersion}" in + devel*) + local bGoVersion + # shellcheck disable=SC2310 + bGoVersion="$(expandVer "${DefaultGoVersion}")" + goFile="${bGoVersion}.linux-amd64.tar.gz" + goPath="${GO_CACHE_DIR}/${bGoVersion}/go" + txt="Installing bootstrap ${bGoVersion}" + ;; + go1) + goFile="go.go1.linux-amd64.tar.gz" + ;; + *) + goFile="${goVersion}.linux-amd64.tar.gz" + ;; + esac + + output::step "${txt}" + ensureFile "${goFile}" "${goPath}" "tar -C ${goPath} --strip-components=1 -zxf" + rm -f "${goPath}/${goFile}" + + case "${goVersion}" in + devel*) + pushd "${GO_CACHE_DIR}" &>/dev/null + mkdir -p "${goVersion}" + pushd "${goVersion}" &>/dev/null + local sha + sha=$(echo "${goVersion}" | cut -d - -f 2) #assumes devel- or devel- + local url="https://github.com/golang/go/archive/${sha}.tar.gz" + output::step "Downloading development Go version ${goVersion}" + # shellcheck disable=SC2154 + ${CURL} "${url}" | tar zxf - + mv go-"${sha}"* go + output::step "Compiling development Go version ${goVersion}" + pushd go/src &>/dev/null + # shellcheck disable=SC2312 + echo "devel +${sha} $(date "+%a %b %H:%M:%S %G %z")" >../VERSION + # shellcheck disable=SC2312 + GOROOT_BOOTSTRAP=$( + pushd "${GO_CACHE_DIR}/${bGoVersion}/go" >/dev/null || exit + pwd + popd >/dev/null || exit + ) ./make.bash 2>&1 + popd &>/dev/null + go/bin/go version + rm -rf "${goPath}" + popd &>/dev/null + popd &>/dev/null + goPath="${GO_CACHE_DIR}/${goVersion}/go" + ;; + *) ;; + esac + fi + + export GOROOT="${goPath}" + PATH="${goPath}/bin:${PATH}" + + # Export GOCACHE if Go >= 1.10 + if go env | grep -q '^GOCACHE='; then + export GOCACHE="${GO_CACHE_DIR}/go-build-cache" + fi } warnGoVersionOverride() { - if [ ! -z "${GOVERSION}" ]; then - output::warning <<-EOF + if [[ -n "${GOVERSION:-}" ]]; then + output::warning <<-EOF Using \$GOVERSION override. \$GOVERSION = ${GOVERSION} If this isn't what you want please run: heroku config:unset GOVERSION -a EOF - fi + fi } warnPackageSpecOverride() { - if [ ! -z "${GO_INSTALL_PACKAGE_SPEC}" ]; then - output::warning <<-EOF + if [[ -n "${GO_INSTALL_PACKAGE_SPEC:-}" ]]; then + output::warning <<-EOF Using \$GO_INSTALL_PACKAGE_SPEC override. \$GO_INSTALL_PACKAGE_SPEC = ${GO_INSTALL_PACKAGE_SPEC} If this isn't what you want please run: heroku config:unset GO_INSTALL_PACKAGE_SPEC -a EOF - fi + fi } installPkgs() { - output::step "Running: go install -v ${FLAGS[*]} ${pkgs}" - go install -v "${FLAGS[@]}" ${pkgs} 2>&1 | output::indent + output::step "Running: go install -v ${FLAGS[*]} ${pkgs}" + # shellcheck disable=SC2086 + go install -v "${FLAGS[@]}" ${pkgs} 2>&1 | output::indent } -loadEnvDir "${env_dir}" +loadEnvDir "${ENV_DIR}" -setGitCredHelper "${env_dir}" +setGitCredHelper "${ENV_DIR}" trap clearGitCredHelper INT TERM EXIT FLAGS=(-tags heroku) @@ -201,29 +214,31 @@ FLAGS=(-tags heroku) determineTool # Track the requested version before expansion (for pinned check) +# shellcheck disable=SC2154 requested_go_version="${ver}" # Expand the version and track the resolved version -ver=$(expandVer $ver) +# shellcheck disable=SC2310 +ver=$(expandVer "${ver}") build_data::set_string "go_version" "${ver}" # Track whether the version was pinned (fully specified) -if [ "${requested_go_version}" = "${ver}" ]; then - build_data::set_raw "go_version_pinned" "true" +if [[ "${requested_go_version}" = "${ver}" ]]; then + build_data::set_raw "go_version_pinned" "true" else - build_data::set_raw "go_version_pinned" "false" + build_data::set_raw "go_version_pinned" "false" fi # If $GO_LINKER_SYMBOL and GO_LINKER_VALUE are set, tell the linker to DTRT -if [ -n "${GO_LINKER_SYMBOL}" -a -n "${GO_LINKER_VALUE}" ]; then - FLAGS+=(-ldflags "-X ${GO_LINKER_SYMBOL}=${GO_LINKER_VALUE}") +if [[ -n "${GO_LINKER_SYMBOL:-}" ]] && [[ -n "${GO_LINKER_VALUE:-}" ]]; then + FLAGS+=(-ldflags "-X ${GO_LINKER_SYMBOL}=${GO_LINKER_VALUE}") fi -if [ -e "${build}/bin" -a ! -d "${build}/bin" ]; then - output::error <<-EOF +if [[ -e "${BUILD_DIR}/bin" ]] && [[ ! -d "${BUILD_DIR}/bin" ]]; then + output::error <<-EOF Error: File bin exists and is not a directory. EOF - exit 1 + exit 1 fi reportVer "${ver}" @@ -232,45 +247,50 @@ go_install_start_time=$(build_data::current_unix_realtime) ensureGo "${ver}" build_data::set_duration "go_install_duration" "${go_install_start_time}" -mkdir -p "${build}/bin" +mkdir -p "${BUILD_DIR}/bin" export GOPATH mainPackagesInModule() { - # For an explanation of what this is doing, see https://dave.cheney.net/2014/09/14/go-list-your-swiss-army-knife - # Stderr (module download progress) is indented and redirected back to stderr so it displays - # neatly under the current step without being captured into the command substitution's stdout. - go list -find "${FLAGS[@]}" -f '{{ if eq .Name "main" }} {{.ImportPath}} {{ end }}' ./... 2> >(output::indent >&2) + # For an explanation of what this is doing, see https://dave.cheney.net/2014/09/14/go-list-your-swiss-army-knife + # Stderr (module download progress) is indented and redirected back to stderr so it displays + # neatly under the current step without being captured into the command substitution's stdout. + # shellcheck disable=SC2312 + go list -find "${FLAGS[@]}" -f '{{ if eq .Name "main" }} {{.ImportPath}} {{ end }}' ./... 2> >(output::indent >&2) } setupProcfile() { - if [ -f "${build}/Procfile" ]; then - return - fi - local pkgs=$(mainPackagesInModule) - if [ -z "${pkgs}" ]; then - return - fi - local pf=() - for pkg in ${pkgs}; do - local bn=$(basename ${pkg}) - pf+=("${bn}: bin/${bn}") - done - if [ ${#pf} -eq 0 ]; then - return - fi - output::step "Created a Procfile with the following entries:" - if [ ${#pf[@]} -eq 1 ]; then - local line="web: bin/$(echo ${pf[0]} | cut -d / -f 2)" - echo "${line}" > ${build}/Procfile - echo "${line}" | output::indent - elif [ ${#pf[@]} -gt 1 ]; then - for pfl in "${pf[@]}"; do - echo "${pfl}" >> ${build}/Procfile - echo "${pfl}" | output::indent - done - fi - output::notice <<-EOF + if [[ -f "${BUILD_DIR}/Procfile" ]]; then + return + fi + local pkgs + # shellcheck disable=SC2310 + pkgs=$(mainPackagesInModule) + if [[ -z "${pkgs}" ]]; then + return + fi + local pf=() + for pkg in ${pkgs}; do + local bn + bn=$(basename "${pkg}") + pf+=("${bn}: bin/${bn}") + done + if [[ ${#pf} -eq 0 ]]; then + return + fi + output::step "Created a Procfile with the following entries:" + if [[ ${#pf[@]} -eq 1 ]]; then + local line + line="web: bin/$(echo "${pf[0]}" | cut -d / -f 2)" + echo "${line}" >"${BUILD_DIR}/Procfile" + echo "${line}" | output::indent + elif [[ ${#pf[@]} -gt 1 ]]; then + for pfl in "${pf[@]}"; do + echo "${pfl}" >>"${BUILD_DIR}/Procfile" + echo "${pfl}" | output::indent + done + fi + output::notice <<-EOF If these entries look incomplete or incorrect please create a Procfile with the required entries. See https://devcenter.heroku.com/articles/procfile for more details about Procfiles @@ -279,41 +299,44 @@ setupProcfile() { dependencies_install_start_time=$(build_data::current_unix_realtime) -cd ${build} +cd "${BUILD_DIR}" || exit -export GOBIN="${build}/bin" -if [ "${GO_SETUP_GOPATH_FOR_MODULE_CACHE}" = "true" ]; then - export GOPATH="${build}/.heroku/go-path" - mkdir -p $GOPATH # ensure that it's created +export GOBIN="${BUILD_DIR}/bin" +if [[ "${GO_SETUP_GOPATH_FOR_MODULE_CACHE:-}" = "true" ]]; then + export GOPATH="${BUILD_DIR}/.heroku/go-path" + mkdir -p "${GOPATH}" # ensure that it's created else - export GOPATH="${cache}/go-path" + export GOPATH="${GO_CACHE_DIR}/go-path" fi # TODO: Check the desired language version (eg `go mod edit -json | jq -r '.Go'`) # and only do this if it's <1.14. The 1.14 release and beyond handles this automatically # when the desired language version is 1.14+ and vendoring should be used. # https://golang.org/doc/go1.14#vendor -if [ -d "${build}/vendor" -a -s "${build}/go.sum" ]; then - FLAGS+=(-mod=vendor) +if [[ -d "${BUILD_DIR}/vendor" ]] && [[ -s "${BUILD_DIR}/go.sum" ]]; then + FLAGS+=(-mod=vendor) fi output::step "Determining packages to install" -pkgs=${GO_INSTALL_PACKAGE_SPEC:-$(awk '{ if ($1 == "//" && $2 == "+heroku" && $3 == "install" ) { print substr($0, index($0,$4)); exit }}' ${goMOD})} -if [ -z "${pkgs}" ]; then - pkgs=$(mainPackagesInModule) - if [ -z "${pkgs}" ]; then - output::warning <<-EOF +# shellcheck disable=SC2154 +pkgs=${GO_INSTALL_PACKAGE_SPEC:-$(awk '{ if ($1 == "//" && $2 == "+heroku" && $3 == "install" ) { print substr($0, index($0,$4)); exit }}' "${goMOD}")} +if [[ -z "${pkgs}" ]]; then + # shellcheck disable=SC2310 + pkgs=$(mainPackagesInModule) + if [[ -z "${pkgs}" ]]; then + # shellcheck disable=SC2154 + output::warning <<-EOF No main packages found in module ${name} Unsure what package(s) to install, defaulting to '.' EOF - pkgs="default" - else - output::step "Detected the following main packages to install:" - for pkg in ${pkgs}; do - echo "${pkg}" | output::indent - done - fi + pkgs="default" + else + output::step "Detected the following main packages to install:" + for pkg in ${pkgs}; do + echo "${pkg}" | output::indent + done + fi fi warnPackageSpecOverride @@ -321,17 +344,17 @@ handleDefaultPkgSpec unset GIT_DIR # unset git dir or it will mess with goinstall if [[ -f bin/go-pre-compile ]]; then - output::step "Running bin/go-pre-compile hook" - chmod a+x bin/go-pre-compile - bin/go-pre-compile 2>&1 | output::indent + output::step "Running bin/go-pre-compile hook" + chmod a+x bin/go-pre-compile + bin/go-pre-compile 2>&1 | output::indent fi installPkgs if [[ -f bin/go-post-compile ]]; then - output::step "Running bin/go-post-compile hook" - chmod a+x bin/go-post-compile - bin/go-post-compile 2>&1 | output::indent + output::step "Running bin/go-post-compile hook" + chmod a+x bin/go-post-compile + bin/go-post-compile 2>&1 | output::indent fi build_data::set_duration "dependencies_install_duration" "${dependencies_install_start_time}" @@ -339,27 +362,32 @@ build_data::set_duration "dependencies_install_duration" "${dependencies_install _newOrUpdatedBinFiles=$(binDiff) output::step "Installed the following binaries:" for fyle in ${_newOrUpdatedBinFiles}; do - echo "${fyle}" | output::indent + echo "${fyle}" | output::indent done setupProcfile -cd $build -mkdir -p $build/.profile.d -echo 'PATH=$PATH:$HOME/bin' > $build/.profile.d/go.sh - -if [ "${GO_INSTALL_TOOLS_IN_IMAGE}" = "true" ]; then - output::step "Copying go tool chain to \$GOROOT=\$HOME/.heroku/go" - mkdir -p "${build}/.heroku/go" - cp -a "${GOROOT}/"* "${build}/.heroku/go" - echo 'export GOROOT=$HOME/.heroku/go' > "${build}/.profile.d/goroot.sh" - echo 'PATH=$PATH:$GOROOT/bin' >> "${build}/.profile.d/goroot.sh" +cd "${BUILD_DIR}" || exit +mkdir -p "${BUILD_DIR}/.profile.d" +# shellcheck disable=SC2016 +echo 'PATH=$PATH:$HOME/bin' >"${BUILD_DIR}/.profile.d/go.sh" + +if [[ "${GO_INSTALL_TOOLS_IN_IMAGE:-}" = "true" ]]; then + output::step "Copying go tool chain to \$GOROOT=\$HOME/.heroku/go" + mkdir -p "${BUILD_DIR}/.heroku/go" + cp -a "${GOROOT}/"* "${BUILD_DIR}/.heroku/go" + # shellcheck disable=SC2016 + echo 'export GOROOT=$HOME/.heroku/go' >"${BUILD_DIR}/.profile.d/goroot.sh" + # shellcheck disable=SC2016 + echo 'PATH=$PATH:$GOROOT/bin' >>"${BUILD_DIR}/.profile.d/goroot.sh" fi -t="${build}/.heroku/go" +t="${BUILD_DIR}/.heroku/go" mkdir -p "${t}" t="${t}/.meta" -echo "TOOL=${TOOL}" > "${t}" -echo "NAME=${name}" >> "${t}" +# shellcheck disable=SC2154 +echo "TOOL=${TOOL}" >"${t}" +# shellcheck disable=SC2154 +echo "NAME=${name}" >>"${t}" build_data::set_duration "total_duration" "${compile_start_time}" diff --git a/bin/detect b/bin/detect index f8cee7d6..474a4f7f 100755 --- a/bin/detect +++ b/bin/detect @@ -1,48 +1,50 @@ #!/usr/bin/env bash # bin/detect -set -e +set -euo pipefail -build=$(cd "$1/" && pwd) -buildpack=$(cd "$(dirname $0)/.." && pwd) +BUILD_DIR="${1}" -source "${buildpack}/lib/output.sh" +# The absolute path to the root of the buildpack. +BUILDPACK_DIR=$(cd "$(dirname "$(dirname "${BASH_SOURCE[0]}")")" && pwd) + +source "${BUILDPACK_DIR}/lib/output.sh" # Detect go.mod (supported) and legacy dependency manager files (unsupported, # but we still want to pass detection so that bin/compile can provide a helpful # migration error message rather than the generic "can't detect language" output). -if test -f "${build}/go.mod" || #go modules - test -f "${build}/Gopkg.lock" || #dep - test -f "${build}/Godeps/Godeps.json" || # godeps - test -f "${build}/vendor/vendor.json" || # govendor - test -f "${build}/glide.yaml" || # glide - (test -d "${build}/src" && test -n "$(find "${build}/src" -mindepth 2 -type f -name '*.go' | sed 1q)") # gb -then - echo Go +# shellcheck disable=SC2235,SC2312 +if test -f "${BUILD_DIR}/go.mod" \ + || test -f "${BUILD_DIR}/Gopkg.lock" \ + || test -f "${BUILD_DIR}/Godeps/Godeps.json" \ + || test -f "${BUILD_DIR}/vendor/vendor.json" \ + || test -f "${BUILD_DIR}/glide.yaml" \ + || (test -d "${BUILD_DIR}/src" && test -n "$(find "${BUILD_DIR}/src" -mindepth 2 -type f -name '*.go' | sed 1q)"); then + echo Go else - output::error <<-EOF - Error: Your app is configured to use the Go buildpack, - but we couldn't find a go.mod file. + output::error <<-EOF + Error: Your app is configured to use the Go buildpack, + but we couldn't find a go.mod file. - A Go app on Heroku must have a 'go.mod' file in the root - directory of its source code. + A Go app on Heroku must have a 'go.mod' file in the root + directory of its source code. - Currently the root directory of your app contains: + Currently the root directory of your app contains: - $(ls -1A --indicator-style=slash "${build}" || true) + $(ls -1A --indicator-style=slash "${BUILD_DIR}" || true) - If your app already has a go.mod file, check that it: + If your app already has a go.mod file, check that it: - 1. Is in the top level directory (not a subdirectory). - 2. Has the correct spelling (the filenames are case-sensitive). - 3. Isn't listed in '.gitignore' or '.slugignore'. - 4. Has been added to Git using 'git add go.mod' and committed. + 1. Is in the top level directory (not a subdirectory). + 2. Has the correct spelling (the filenames are case-sensitive). + 3. Isn't listed in '.gitignore' or '.slugignore'. + 4. Has been added to Git using 'git add go.mod' and committed. - To create a go.mod file, run 'go mod init ' in your - project directory. + To create a go.mod file, run 'go mod init ' in your + project directory. - For help with using Go on Heroku, see: - https://devcenter.heroku.com/articles/getting-started-with-go - https://devcenter.heroku.com/articles/go-support + For help with using Go on Heroku, see: + https://devcenter.heroku.com/articles/getting-started-with-go + https://devcenter.heroku.com/articles/go-support EOF - exit 1 + exit 1 fi diff --git a/bin/release b/bin/release index 4b7f74cc..750d026f 100755 --- a/bin/release +++ b/bin/release @@ -1,4 +1,6 @@ #!/usr/bin/env bash # bin/release +set -euo pipefail + echo "--- {}" diff --git a/bin/test b/bin/test index 8ffe14a0..1735cc24 100755 --- a/bin/test +++ b/bin/test @@ -1,69 +1,73 @@ -#!/bin/bash +#!/usr/bin/env bash # usage: bin/test -set -eo pipefail +set -euo pipefail -mkdir -p "${1}" "${2}" -build="$(cd ${1}/ && pwd)" -env_dir="$(cd ${2}/ && pwd)" -testpack=$(cd "$(dirname $0)/.." && pwd) -buildpack="${testpack}" -source "${testpack}/lib/common.sh" +BUILD_DIR="${1}" +ENV_DIR="${2}" -# loadEnvDir "${env_dir}" -# For now, load all of env_dir -if [ ! -z "${env_dir}" ]; then - for f in ${env_dir}/*; do - key=$(basename $f) - if [ -f "${f}" ]; then - export "${key}=$(cat "${f}" | sed -e "s:\${build_dir}:${build}:")" - fi - done +# The absolute path to the root of the buildpack. +BUILDPACK_DIR=$(cd "$(dirname "$(dirname "${BASH_SOURCE[0]}")")" && pwd) + +source "${BUILDPACK_DIR}/lib/output.sh" +source "${BUILDPACK_DIR}/lib/common.sh" + +# loadEnvDir "${ENV_DIR}" +# For now, load all of ENV_DIR +if [[ -n "${ENV_DIR}" ]]; then + for f in "${ENV_DIR}"/*; do + key=$(basename "${f}") + if [[ -f "${f}" ]]; then + export "${key}=$(sed -e "s:\${build_dir}:${BUILD_DIR}:" "${f}")" + fi + done fi -if [ -f "${build}/.heroku/go/.meta" ]; then - source "${build}/.heroku/go/.meta" +if [[ -f "${BUILD_DIR}/.heroku/go/.meta" ]]; then + # shellcheck disable=SC1091 + source "${BUILD_DIR}/.heroku/go/.meta" fi -export GOROOT="${build}/.heroku/go" +export GOROOT="${BUILD_DIR}/.heroku/go" # Use the GOPATH that was setup during compile, if it exists, so that # any modules downloaded during compile are re-used. -if [ -d "${build}/.heroku/go-path" ]; then - export GOPATH="${build}/.heroku/go-path" +if [[ -d "${BUILD_DIR}/.heroku/go-path" ]]; then + export GOPATH="${BUILD_DIR}/.heroku/go-path" else - # Otherwise use a GOPATH that is outside of ${build} - export GOPATH="$(mktemp -d)" + # Otherwise use a GOPATH that is outside of ${BUILD_DIR} + GOPATH="$(mktemp -d)" + export GOPATH fi -PATH="${build}/bin:${GOROOT}/bin:${PATH}" +PATH="${BUILD_DIR}/bin:${GOROOT}/bin:${PATH}" # Install our vendored copy of github.com/apg/patter -(cd "${testpack}/lib/src/github.com/apg/patter" && GOPATH="${testpack}/lib" GOBIN="${build}/bin" go install .) +(cd "${BUILDPACK_DIR}/lib/src/github.com/apg/patter" && GOPATH="${BUILDPACK_DIR}/lib" GOBIN="${BUILD_DIR}/bin" go install .) output=$(mktemp) -cd "${build}" -if [ -f "${build}/.golangci.yml" -o -f "${build}/.golangci.toml" -o -f "${build}/.golangci.json" ]; then - output::step "Running: golangci-lint -v --build-tags heroku run" - ${build}/.heroku/golangci/bin/golangci-lint -v --build-tags heroku run 2>&1 | output::indent +cd "${BUILD_DIR}" +if [[ -f "${BUILD_DIR}/.golangci.yml" ]] || [[ -f "${BUILD_DIR}/.golangci.toml" ]] || [[ -f "${BUILD_DIR}/.golangci.json" ]]; then + output::step "Running: golangci-lint -v --build-tags heroku run" + "${BUILD_DIR}"/.heroku/golangci/bin/golangci-lint -v --build-tags heroku run 2>&1 | output::indent fi _tflags=("-race" "-v") -if [ -s "./go.sum" -a -d "./vendor" ]; then - _tflags+=("-mod=vendor") +if [[ -s "./go.sum" ]] && [[ -d "./vendor" ]]; then + _tflags+=("-mod=vendor") else - _tflags+=("-mod=readonly") + _tflags+=("-mod=readonly") fi output::step "Running Tests With: go test ${_tflags[*]} ./... | patter" -go test ${_tflags[@]} ./... 2>&1 | tee -a ${output} | patter +go test "${_tflags[@]}" ./... 2>&1 | tee -a "${output}" | patter echo -if [[ -z "${GO_TEST_SKIP_BENCHMARK}" ]]; then - _tflags=("${_tflags[@]:1}") # push -race off - _tflags+=("-run=_") - _tflags+=("-bench=.") - _tflags+=("-benchmem") - output::step "Running Benchmarks With: go test ${_tflags[*]} ./..." - go test ${_tflags[@]} ./... 2>&1 | output::indent +if [[ -z "${GO_TEST_SKIP_BENCHMARK:-}" ]]; then + _tflags=("${_tflags[@]:1}") # push -race off + _tflags+=("-run=_") + _tflags+=("-bench=.") + _tflags+=("-benchmem") + output::step "Running Benchmarks With: go test ${_tflags[*]} ./..." + go test "${_tflags[@]}" ./... 2>&1 | output::indent fi output::step "Standard (Non TAP) test output" -output::indent < "${output}" +output::indent <"${output}" output::step "Finished" diff --git a/bin/test-compile b/bin/test-compile index 93ca00c7..7d3e8423 100755 --- a/bin/test-compile +++ b/bin/test-compile @@ -1,21 +1,23 @@ -#!/bin/bash +#!/usr/bin/env bash # usage: bin/test-compile -mkdir -p "$1" "$2" -build=$(cd "$1/" && pwd) -cache=$(cd "$2/" && pwd) -env_dir="${3}" -testpack=$(cd "$(dirname $0)/.." && pwd) +set -euo pipefail -echo "true" > "${env_dir}/GO_INSTALL_TOOLS_IN_IMAGE" -echo "true" > "${env_dir}/GO_SETUP_GOPATH_FOR_MODULE_CACHE" +BUILD_DIR="${1}" +CACHE_DIR="${2}" +ENV_DIR="${3}" -buildpack="${testpack}" -source ${testpack}/bin/compile "${build}" "${cache}" "${env_dir}" +# The absolute path to the root of the buildpack. +BUILDPACK_DIR=$(cd "$(dirname "$(dirname "${BASH_SOURCE[0]}")")" && pwd) -if [ -f "${build}/.golangci.yml" -o -f "${build}/.golangci.toml" -o -f "${build}/.golangci.json" ]; then - output::step "/.golangci.{yml,toml,json} detected" - tmp="$(mktemp -d)" - mkdir -p "${build}/.heroku/golangci/bin" - ensureFile "golangci-lint-1.20.0-linux-amd64.tar.gz" "${tmp}" "tar -C ${build}/.heroku/golangci/bin --strip-components=1 -zxf" -fi \ No newline at end of file +echo "true" >"${ENV_DIR}/GO_INSTALL_TOOLS_IN_IMAGE" +echo "true" >"${ENV_DIR}/GO_SETUP_GOPATH_FOR_MODULE_CACHE" + +source "${BUILDPACK_DIR}/bin/compile" "${BUILD_DIR}" "${CACHE_DIR}" "${ENV_DIR}" + +if [[ -f "${BUILD_DIR}/.golangci.yml" ]] || [[ -f "${BUILD_DIR}/.golangci.toml" ]] || [[ -f "${BUILD_DIR}/.golangci.json" ]]; then + output::step "/.golangci.{yml,toml,json} detected" + tmp="$(mktemp -d)" + mkdir -p "${BUILD_DIR}/.heroku/golangci/bin" + ensureFile "golangci-lint-1.20.0-linux-amd64.tar.gz" "${tmp}" "tar -C ${BUILD_DIR}/.heroku/golangci/bin --strip-components=1 -zxf" +fi diff --git a/buildpack.toml b/buildpack.toml index 1b3a949b..6d2c265f 100644 --- a/buildpack.toml +++ b/buildpack.toml @@ -7,6 +7,8 @@ name = "Go" "test/", "sbin/", "file-cache/", + ".editorconfig", ".gitignore", + ".shellcheckrc", "Makefile" ] \ No newline at end of file diff --git a/lib/build_data.sh b/lib/build_data.sh index f026bbaa..8199af2e 100644 --- a/lib/build_data.sh +++ b/lib/build_data.sh @@ -1,7 +1,8 @@ #!/usr/bin/env bash -# Note: -u is omitted for now so we don't break non-strict callers. -set -eo pipefail +# This is technically redundant, since all consumers of this lib will have enabled these, +# however, it helps Shellcheck realise the options under which these functions will run. +set -euo pipefail BUILD_DATA_FILE="${CACHE_DIR:?}/build-data/go.json" PREVIOUS_BUILD_DATA_FILE="${CACHE_DIR:?}/build-data/go-prev.json" diff --git a/lib/common.sh b/lib/common.sh index 2e254f16..f4656597 100644 --- a/lib/common.sh +++ b/lib/common.sh @@ -1,13 +1,15 @@ -#!/bin/bash +#!/usr/bin/env bash + +# This is technically redundant, since all consumers of this lib will have enabled these, +# however, it helps Shellcheck realise the options under which these functions will run. +set -euo pipefail +# shellcheck disable=SC2034,SC2154 +DataJSON="${BUILDPACK_DIR}/data.json" +# shellcheck disable=SC2034,SC2154 +FilesJSON="${BUILDPACK_DIR}/files.json" +# shellcheck disable=SC2154 +goMOD="${BUILD_DIR}/go.mod" -# ----------------------------------------- -# load environment variables -# allow apps to specify cgo flags. The literal text '${build_dir}' is substituted for the build directory -DataJSON="${buildpack}/data.json" -FilesJSON="${buildpack}/files.json" -goMOD="${build}/go.mod" - -source "$(dirname "${BASH_SOURCE[0]}")/output.sh" # We use --max-time/--retry-max-time for improved UX and metrics for hanging downloads compared to # seconds relying on the build system timeout. Go tarballs are up to ~70 MB and typically download in a few # seconds on Heroku, so we set relatively low timeouts to reduce delays before retries. @@ -16,62 +18,66 @@ CURL="curl --no-progress-meter --location --fail --max-time 30 --retry-max-time TOOL="" # Default to $SOURCE_VERSION environment variable: https://devcenter.heroku.com/articles/buildpack-api#bin-compile -GO_LINKER_VALUE=${SOURCE_VERSION} +# shellcheck disable=SC2034 +GO_LINKER_VALUE="${SOURCE_VERSION:-}" snapshotBinBefore() { - if [ ! -d "${build}/bin" ]; then - return 0 - fi - _oifs=$IFS - IFS=$'\n' - _binBefore=() - for f in ${build}/bin/*; do - if [ -f $f ]; then - _binBefore+=($(shasum $f)) - fi - done - IFS=$_oifs + if [[ ! -d "${BUILD_DIR}/bin" ]]; then + return 0 + fi + _oifs=${IFS} + IFS=$'\n' + _binBefore=() + for f in "${BUILD_DIR}"/bin/*; do + if [[ -f "${f}" ]]; then + # shellcheck disable=SC2207 + _binBefore+=("$(shasum "${f}")") + fi + done + IFS=${_oifs} } binDiff() { - _oifs=$IFS - IFS=$'\n' - local binAfter=() - for f in ${build}/bin/*; do - if [ -f $f ]; then - binAfter+=($(shasum $f)) - fi - done - - local new=() - for a in "${binAfter[@]}"; do - local let found=0 - - for b in "${_binBefore[@]}"; do - if [ "${a}" = "${b}" ]; then - let found+=1 - fi - done - - if [ $found -eq 0 ]; then - new+=( "./bin/$(basename $(echo $a | awk '{print $2}' ) )" ) - fi - done - IFS=$_oifs - echo ${new[@]} + _oifs=${IFS} + IFS=$'\n' + local binAfter=() + for f in "${BUILD_DIR}"/bin/*; do + if [[ -f "${f}" ]]; then + # shellcheck disable=SC2207 + binAfter+=("$(shasum "${f}")") + fi + done + + local new=() + for a in "${binAfter[@]}"; do + local found=0 + + for b in "${_binBefore[@]}"; do + if [[ "${a}" = "${b}" ]]; then + ((found += 1)) + fi + done + + if [[ "${found}" -eq 0 ]]; then + # shellcheck disable=SC2312 + new+=("./bin/$(basename "$(echo "${a}" | awk '{print $2}')")") + fi + done + IFS=${_oifs} + echo "${new[@]}" } - knownFile() { - local fileName="${1}" - <${FilesJSON} jq -e 'to_entries | map(select(.key == "'${fileName}'")) | any' &> /dev/null + local fileName="${1}" + <"${FilesJSON}" jq -e 'to_entries | map(select(.key == "'"${fileName}"'")) | any' &>/dev/null } downloadFile() { - local fileName="${1}" + local fileName="${1}" - if ! knownFile ${fileName}; then - output::error <<-EOF + # shellcheck disable=SC2310 + if ! knownFile "${fileName}"; then + output::error <<-EOF Error: The requested file (${fileName}) is unknown to the buildpack! The buildpack tracks and validates the SHA256 sums of the files @@ -81,107 +87,116 @@ downloadFile() { To find out more info about this error please visit: https://devcenter.heroku.com/articles/unknown-go-buildack-files EOF - exit 1 - fi - - local targetDir="${2}" - local xCmd="${3}" - local targetFile="${targetDir}/${fileName}" - - mkdir -p "${targetDir}" - pushd "${targetDir}" &> /dev/null - output::step "Fetching ${fileName}" - local url="$(<"${FilesJSON}" jq -r '."'${fileName}'".URL')" - ${CURL} -o "${fileName}" "${url}" 2>&1 | output::indent - if ! SHAValid "${fileName}" "${targetFile}"; then - output::error <<-EOF - Error: Downloaded file (${fileName}) SHA does not match recorded SHA. - - Unable to continue. - EOF - exit 1 - fi - if [ -n "${xCmd}" ]; then - ${xCmd} ${targetFile} 2>&1 | output::indent - fi - popd &> /dev/null + exit 1 + fi + + local targetDir="${2}" + local xCmd="${3}" + local targetFile="${targetDir}/${fileName}" + + mkdir -p "${targetDir}" + pushd "${targetDir}" &>/dev/null || return 1 + output::step "Fetching ${fileName}" + local url + url="$(<"${FilesJSON}" jq -r '."'"${fileName}"'".URL')" + # shellcheck disable=SC2312 + ${CURL} -o "${fileName}" "${url}" 2>&1 | output::indent + # shellcheck disable=SC2310 + if ! SHAValid "${fileName}" "${targetFile}"; then + output::error <<-EOF + Error: Downloaded file (${fileName}) SHA does not match recorded SHA. + + Unable to continue. + EOF + exit 1 + fi + if [[ -n "${xCmd}" ]]; then + # shellcheck disable=SC2312 + ${xCmd} "${targetFile}" 2>&1 | output::indent + fi + popd &>/dev/null || return 1 } SHAValid() { - local fileName="${1}" - local targetFile="${2}" - local expected="$(<"${FilesJSON}" jq -r '."'${fileName}'".SHA')" - local actual="$(shasum -a256 "${targetFile}" | cut -d \ -f 1)" - [ "${actual}" = "${expected}" ] + local fileName="${1}" + local targetFile="${2}" + local expected + expected="$(<"${FilesJSON}" jq -r '."'"${fileName}"'".SHA')" + local actual + # shellcheck disable=SC2312 + actual="$(shasum -a256 "${targetFile}" | cut -d \ -f 1)" + [[ "${actual}" = "${expected}" ]] } ensureFile() { - local fileName="${1}" - local targetDir="${2}" - local xCmd="${3}" - local targetFile="${targetDir}/${fileName}" - local download="false" - if [ ! -f "${targetFile}" ]; then - download="true" - elif ! SHAValid "${fileName}" "${targetFile}"; then - download="true" - fi - if [ "${download}" = "true" ]; then - downloadFile "${fileName}" "${targetDir}" "${xCmd}" - fi + local fileName="${1}" + local targetDir="${2}" + local xCmd="${3}" + local targetFile="${targetDir}/${fileName}" + local download="false" + # shellcheck disable=SC2310 + if [[ ! -f "${targetFile}" ]]; then + download="true" + elif ! SHAValid "${fileName}" "${targetFile}"; then + download="true" + fi + if [[ "${download}" = "true" ]]; then + downloadFile "${fileName}" "${targetDir}" "${xCmd}" + fi } addToPATH() { - local targetDir="${1}" - if echo "${PATH}" | grep -v "${targetDir}" &> /dev/null; then - PATH="${targetDir}:${PATH}" - fi + local targetDir="${1}" + if echo "${PATH}" | grep -v "${targetDir}" &>/dev/null; then + PATH="${targetDir}:${PATH}" + fi } ensureInPath() { - local fileName="${1}" - local targetDir="${2}" - local xCmd="${3:-chmod a+x}" - addToPATH "${targetDir}" - ensureFile "${fileName}" "${targetDir}" "${xCmd}" + local fileName="${1}" + local targetDir="${2}" + local xCmd="${3:-chmod a+x}" + addToPATH "${targetDir}" + ensureFile "${fileName}" "${targetDir}" "${xCmd}" } loadEnvDir() { - local envFlags=() - envFlags+=("CGO_CFLAGS") - envFlags+=("CGO_CPPFLAGS") - envFlags+=("CGO_CXXFLAGS") - envFlags+=("CGO_LDFLAGS") - envFlags+=("GO_LINKER_SYMBOL") - envFlags+=("GO_LINKER_VALUE") - envFlags+=("GOFLAGS") - envFlags+=("GOPROXY") - envFlags+=("GOPRIVATE") - envFlags+=("GONOPROXY") - envFlags+=("GOVERSION") - envFlags+=("GO_INSTALL_PACKAGE_SPEC") - envFlags+=("GO_INSTALL_TOOLS_IN_IMAGE") - envFlags+=("GO_SETUP_GOPATH_FOR_MODULE_CACHE") - envFlags+=("GO_TEST_SKIP_BENCHMARK") - local env_dir="${1}" - if [ ! -z "${env_dir}" ]; then - mkdir -p "${env_dir}" - env_dir=$(cd "${env_dir}/" && pwd) - for key in ${envFlags[@]}; do - if [ -f "${env_dir}/${key}" ]; then - export "${key}=$(cat "${env_dir}/${key}" | sed -e "s:\${build_dir}:${build}:")" - fi - done - fi + local envFlags=() + envFlags+=("CGO_CFLAGS") + envFlags+=("CGO_CPPFLAGS") + envFlags+=("CGO_CXXFLAGS") + envFlags+=("CGO_LDFLAGS") + envFlags+=("GO_LINKER_SYMBOL") + envFlags+=("GO_LINKER_VALUE") + envFlags+=("GOFLAGS") + envFlags+=("GOPROXY") + envFlags+=("GOPRIVATE") + envFlags+=("GONOPROXY") + envFlags+=("GOVERSION") + envFlags+=("GO_INSTALL_PACKAGE_SPEC") + envFlags+=("GO_INSTALL_TOOLS_IN_IMAGE") + envFlags+=("GO_SETUP_GOPATH_FOR_MODULE_CACHE") + envFlags+=("GO_TEST_SKIP_BENCHMARK") + local env_dir="${1}" + if [[ -n "${env_dir}" ]]; then + mkdir -p "${env_dir}" + env_dir=$(cd "${env_dir}/" && pwd) + for key in "${envFlags[@]}"; do + if [[ -f "${env_dir}/${key}" ]]; then + export "${key}=$(sed -e "s:\${build_dir}:${BUILD_DIR}:" <"${env_dir}/${key}")" + fi + done + fi } clearGitCredHelper() { - git config --global --unset credential.helper + git config --global --unset credential.helper } +# shellcheck disable=SC2016,SC2086,SC2250,SC2292,SC2002,SC2248,SC2312 setGitCredHelper() { - git config --global credential.helper '!#GoGitCredHelper - env_dir="'$(cd ${1}/ && pwd)'" + git config --global credential.helper '!#GoGitCredHelper + env_dir="'"$(cd "${1}"/ && pwd)"'" gitCredHelper() { #echo "${1}\n" >&2 #debug case "${1}" in @@ -250,50 +265,51 @@ setGitCredHelper() { } supportsGoModules() { - local version="${1}" - # Ex: "go1.10.4" | ["go1","10", "4"] | ["1","10","4"] | [1,10,4] | [1] [10] == exit 1 (fail) - echo "\"${version}\"" | jq -e 'split(".") | map(gsub("go";"")) | map(tonumber) | .[0] >= 1 and .[1] < 11' &> /dev/null + local version="${1}" + # Ex: "go1.10.4" | ["go1","10", "4"] | ["1","10","4"] | [1,10,4] | [1] [10] == exit 1 (fail) + echo "\"${version}\"" | jq -e 'split(".") | map(gsub("go";"")) | map(tonumber) | .[0] >= 1 and .[1] < 11' &>/dev/null } determineTool() { - # Check GOVERSION first - it overrides all tool-specific configurations - if [ -n "${GOVERSION}" ]; then - ver="${GOVERSION}" - go_version_origin="GOVERSION" - build_data::set_string "go_version_origin" "${go_version_origin}" - build_data::set_string "go_version_requested" "${ver}" - fi - - if [ -f "${goMOD}" ]; then - TOOL="gomodules" - build_data::set_string "go_tool" "${TOOL}" - - output::step "Detected go modules via go.mod" - - # Determine Go version from go.mod if not already set by GOVERSION - if [ -z "${ver}" ]; then - ver=$(awk '{ if ($1 == "//" && $2 == "+heroku" && $3 == "goVersion" ) { print $4; exit } }' ${goMOD}) - if [ -n "${ver}" ]; then - go_version_origin="go.mod (heroku comment)" - else - ver=$(awk '{ if ($1 == "go" ) { print "go" $2; exit } }' ${goMOD}) - if [ -n "${ver}" ]; then - go_version_origin="go.mod" - else - ver=${DefaultGoVersion} - go_version_origin="default" - fi - fi - build_data::set_string "go_version_origin" "${go_version_origin}" - build_data::set_string "go_version_requested" "${ver}" - fi - - name=$(awk '{ if ($1 == "module" ) { gsub(/"/, "", $2); print $2; exit } }' < ${goMOD}) - output::step "Detected Module Name: ${name}" - warnGoVersionOverride - - if [ "${go_version_origin}" = "default" ]; then - output::warning <<-EOF + # Check GOVERSION first - it overrides all tool-specific configurations + if [[ -n "${GOVERSION:-}" ]]; then + ver="${GOVERSION}" + go_version_origin="GOVERSION" + build_data::set_string "go_version_origin" "${go_version_origin}" + build_data::set_string "go_version_requested" "${ver}" + fi + + if [[ -f "${goMOD}" ]]; then + TOOL="gomodules" + build_data::set_string "go_tool" "${TOOL}" + + output::step "Detected go modules via go.mod" + + # Determine Go version from go.mod if not already set by GOVERSION + if [[ -z "${ver:-}" ]]; then + ver=$(awk '{ if ($1 == "//" && $2 == "+heroku" && $3 == "goVersion" ) { print $4; exit } }' "${goMOD}") + if [[ -n "${ver}" ]]; then + go_version_origin="go.mod (heroku comment)" + else + ver=$(awk '{ if ($1 == "go" ) { print "go" $2; exit } }' "${goMOD}") + if [[ -n "${ver}" ]]; then + go_version_origin="go.mod" + else + # shellcheck disable=SC2154 + ver=${DefaultGoVersion} + go_version_origin="default" + fi + fi + build_data::set_string "go_version_origin" "${go_version_origin}" + build_data::set_string "go_version_requested" "${ver}" + fi + + name=$(awk '{ if ($1 == "module" ) { gsub(/"/, "", $2); print $2; exit } }' <"${goMOD}") + output::step "Detected Module Name: ${name}" + warnGoVersionOverride + + if [[ "${go_version_origin}" = "default" ]]; then + output::warning <<-EOF The go.mod file for this project does not specify a Go version. Defaulting to ${ver} @@ -301,10 +317,11 @@ determineTool() { For more details see: https://devcenter.heroku.com/articles/go-apps-with-modules#build-configuration EOF - fi + fi - if supportsGoModules "${ver}"; then - output::error <<-EOF + # shellcheck disable=SC2310 + if supportsGoModules "${ver}"; then + output::error <<-EOF Error: You are using ${ver}, which does not support Go modules. Go modules are supported by go1.11 and above. @@ -315,25 +332,26 @@ determineTool() { Then commit and push again. EOF - exit 1 - fi - else - local legacy_tool="" - if [ -f "${build}/Gopkg.lock" ]; then - legacy_tool="dep" - elif [ -f "${build}/Godeps/Godeps.json" ]; then - legacy_tool="godep" - elif [ -f "${build}/vendor/vendor.json" ]; then - legacy_tool="govendor" - elif [ -f "${build}/glide.yaml" ]; then - legacy_tool="glide" - elif [ -d "${build}/src" ] && [ -n "$(find "${build}/src" -mindepth 2 -type f -name '*.go' | sed 1q)" ]; then - legacy_tool="gb" - fi - - if [ -n "${legacy_tool}" ]; then - build_data::set_string "go_tool" "${legacy_tool}" - output::error <<-EOF + exit 1 + fi + else + local legacy_tool="" + # shellcheck disable=SC2312 + if [[ -f "${BUILD_DIR}/Gopkg.lock" ]]; then + legacy_tool="dep" + elif [[ -f "${BUILD_DIR}/Godeps/Godeps.json" ]]; then + legacy_tool="godep" + elif [[ -f "${BUILD_DIR}/vendor/vendor.json" ]]; then + legacy_tool="govendor" + elif [[ -f "${BUILD_DIR}/glide.yaml" ]]; then + legacy_tool="glide" + elif [[ -d "${BUILD_DIR}/src" ]] && [[ -n "$(find "${BUILD_DIR}/src" -mindepth 2 -type f -name '*.go' | sed 1q)" ]]; then + legacy_tool="gb" + fi + + if [[ -n "${legacy_tool}" ]]; then + build_data::set_string "go_tool" "${legacy_tool}" + output::error <<-EOF Error: Your app appears to use '${legacy_tool}' for dependency management, but support for ${legacy_tool} has been removed. @@ -346,14 +364,14 @@ determineTool() { For more details see: https://devcenter.heroku.com/articles/go-modules EOF - else - output::error <<-EOF + else + output::error <<-EOF Error: A go.mod file is required. For help with using Go on Heroku, see: https://devcenter.heroku.com/articles/go-support EOF - fi - exit 1 - fi + fi + exit 1 + fi } diff --git a/lib/output.sh b/lib/output.sh index d0a797b2..1f6dda76 100644 --- a/lib/output.sh +++ b/lib/output.sh @@ -2,7 +2,7 @@ # This is technically redundant, since all consumers of this lib will have enabled these, # however, it helps Shellcheck realise the options under which these functions will run. -set -eo pipefail +set -euo pipefail ANSI_BLUE=$'\e[1;34m' ANSI_RED=$'\e[1;31m' diff --git a/sbin/add-version b/sbin/add-version index 1a6a4569..e2435ecd 100755 --- a/sbin/add-version +++ b/sbin/add-version @@ -1,25 +1,25 @@ -#!/bin/sh +#!/usr/bin/env bash -set -e +set -euo pipefail BASEURL="https://dl.google.com/go" V="$1" -if [ -z "$V" ]; then - echo "usage: $0 " >&2 - exit 1 +if [[ -z "${V}" ]]; then + echo "usage: $0 " >&2 + exit 1 fi -tgz_fn="go$V.linux-amd64.tar.gz" -tgz_url="$BASEURL/$tgz_fn" -sha256_url="$tgz_url.sha256" +tgz_fn="go${V}.linux-amd64.tar.gz" +tgz_url="${BASEURL}/${tgz_fn}" +sha256_url="${tgz_url}.sha256" -if ! sha256_content="$(curl -s -f "$sha256_url")"; then - echo "error: adding $V: couldn't fetch $sha256_url" >&2 - exit 1 +if ! sha256_content="$(curl -s -f "${sha256_url}")"; then + echo "error: adding ${V}: couldn't fetch ${sha256_url}" >&2 + exit 1 fi -TGZ_FN="$tgz_fn" TGZ_URL="$tgz_url" SHA256="$sha256_content" jq -S '.[env.TGZ_FN] = {URL: env.TGZ_URL, SHA: env.SHA256 }' files.json > files.json.tmp +TGZ_FN="${tgz_fn}" TGZ_URL="${tgz_url}" SHA256="${sha256_content}" jq -S '.[env.TGZ_FN] = {URL: env.TGZ_URL, SHA: env.SHA256 }' files.json >files.json.tmp mv files.json.tmp files.json -echo "added $V to files.json" +echo "added ${V} to files.json" diff --git a/sbin/publish.sh b/sbin/publish.sh index 2bace6a9..e81c2d1d 100755 --- a/sbin/publish.sh +++ b/sbin/publish.sh @@ -4,23 +4,23 @@ set -euo pipefail BP_NAME=${1:-"heroku/go"} -curVersion=$(heroku buildpacks:versions "$BP_NAME" | awk 'FNR == 3 { print $1 }') +curVersion=$(heroku buildpacks:versions "${BP_NAME}" | awk 'FNR == 3 { print $1 }') newVersion="v$((curVersion + 1))" -read -p "Deploy as version: $newVersion [y/n]? " choice -case "$choice" in - y|Y ) echo "";; - n|N ) exit 0;; - * ) exit 1;; +read -rp "Deploy as version: ${newVersion} [y/n]? " choice +case "${choice}" in + y | Y) echo "" ;; + n | N) exit 0 ;; + *) exit 1 ;; esac git fetch origin originMain=$(git rev-parse origin/main) -echo "Tagging commit $originMain with $newVersion... " -git tag "$newVersion" "${originMain:?}" -git push origin refs/tags/$newVersion +echo "Tagging commit ${originMain} with ${newVersion}... " +git tag "${newVersion}" "${originMain:?}" +git push origin "refs/tags/${newVersion}" echo -e "\nPublishing to the buildpack registry..." -heroku buildpacks:publish "$BP_NAME" "$newVersion" +heroku buildpacks:publish "${BP_NAME}" "${newVersion}" echo heroku buildpacks:versions "${BP_NAME}" | head -n 3 diff --git a/sbin/update-go-versions b/sbin/update-go-versions index 01df36ef..746b13ef 100755 --- a/sbin/update-go-versions +++ b/sbin/update-go-versions @@ -1,60 +1,60 @@ -#!/bin/bash +#!/usr/bin/env bash set -euo pipefail cd "$(dirname "${BASH_SOURCE[0]}")/.." -if [ -z "${1:-}" ]; then - echo "Usage: $0 [comma-separated-versions]" - echo "" - echo "Examples:" - echo " $0 1.24.12,1.25.6 # Add specific versions" - echo " $0 # Auto-detect and add latest stable versions" - echo "" - echo "No versions specified, fetching latest stable versions from go.dev..." +if [[ -z "${1:-}" ]]; then + echo "Usage: $0 [comma-separated-versions]" + echo "" + echo "Examples:" + echo " $0 1.24.12,1.25.6 # Add specific versions" + echo " $0 # Auto-detect and add latest stable versions" + echo "" + echo "No versions specified, fetching latest stable versions from go.dev..." - # Fetch latest stable versions from go.dev - latest_versions=$(curl \ - --no-progress-meter \ - --fail \ - --connect-timeout 3 \ - --max-time 60 \ - --retry 5 \ - --retry-max-time 60 \ - --retry-connrefused \ - 'https://go.dev/dl/?mode=json&include=stable' | \ - jq -r '.[].version | sub("^go"; "")') + # Fetch latest stable versions from go.dev + latest_versions=$(curl \ + --no-progress-meter \ + --fail \ + --connect-timeout 3 \ + --max-time 60 \ + --retry 5 \ + --retry-max-time 60 \ + --retry-connrefused \ + 'https://go.dev/dl/?mode=json&include=stable' \ + | jq -r '.[].version | sub("^go"; "")') - if [ -z "$latest_versions" ]; then - echo "Error: Failed to fetch latest versions from go.dev" - exit 1 - fi + if [[ -z "${latest_versions}" ]]; then + echo "Error: Failed to fetch latest versions from go.dev" + exit 1 + fi - # Convert newline-separated versions to comma-separated - versions_arg=$(echo "$latest_versions" | tr '\n' ',' | sed 's/,$//') - echo "Found latest stable versions: $versions_arg" - IFS=',' read -ra VERSION_ARRAY <<< "$versions_arg" + # Convert newline-separated versions to comma-separated + versions_arg=$(echo "${latest_versions}" | tr '\n' ',' | sed 's/,$//') + echo "Found latest stable versions: ${versions_arg}" + IFS=',' read -ra VERSION_ARRAY <<<"${versions_arg}" else - IFS=',' read -ra VERSION_ARRAY <<< "$1" + IFS=',' read -ra VERSION_ARRAY <<<"$1" fi TEMP_CHANGELOG=$(mktemp) -awk '/## \[Unreleased\]/{print; print ""; exit} {print}' CHANGELOG.md > "$TEMP_CHANGELOG" +awk '/## \[Unreleased\]/{print; print ""; exit} {print}' CHANGELOG.md >"${TEMP_CHANGELOG}" for version in "${VERSION_ARRAY[@]}"; do - tarball="go${version}.linux-amd64.tar.gz" + tarball="go${version}.linux-amd64.tar.gz" - if jq -e --arg key "$tarball" 'has($key)' files.json > /dev/null 2>&1; then - echo "Version $version already exists, skipping" - continue - fi + if jq -e --arg key "${tarball}" 'has($key)' files.json >/dev/null 2>&1; then + echo "Version ${version} already exists, skipping" + continue + fi - sbin/add-version "$version" - echo "* Add go${version}" >> "$TEMP_CHANGELOG" + sbin/add-version "${version}" + echo "* Add go${version}" >>"${TEMP_CHANGELOG}" - major_minor=$(echo "$version" | cut -d. -f1,2) + major_minor=$(echo "${version}" | cut -d. -f1,2) - jq --arg major_minor "go${major_minor}" --arg version "go${version}" ' + jq --arg major_minor "go${major_minor}" --arg version "go${version}" ' .Go.VersionExpansion[$major_minor] = $version | # Sort by descending Go version .Go.VersionExpansion = ( @@ -68,11 +68,11 @@ for version in "${VERSION_ARRAY[@]}"; do reverse | from_entries ) - ' data.json > data.json.tmp - mv data.json.tmp data.json - echo "* go${major_minor} defaults to ${version}" >> "$TEMP_CHANGELOG" + ' data.json >data.json.tmp + mv data.json.tmp data.json + echo "* go${major_minor} defaults to ${version}" >>"${TEMP_CHANGELOG}" done -awk '/## \[Unreleased\]/{flag=1; next} flag{if(flag==1 && NF==0){flag=2; next}} flag==2' CHANGELOG.md >> "$TEMP_CHANGELOG" +awk '/## \[Unreleased\]/{flag=1; next} flag{if(flag==1 && NF==0){flag=2; next}} flag==2' CHANGELOG.md >>"${TEMP_CHANGELOG}" -mv "$TEMP_CHANGELOG" CHANGELOG.md +mv "${TEMP_CHANGELOG}" CHANGELOG.md diff --git a/test/fixtures/mod-basic-with-hooks/bin/go-post-compile b/test/fixtures/mod-basic-with-hooks/bin/go-post-compile index 7a2ddc45..00a64e00 100755 --- a/test/fixtures/mod-basic-with-hooks/bin/go-post-compile +++ b/test/fixtures/mod-basic-with-hooks/bin/go-post-compile @@ -1,3 +1,3 @@ #!/usr/bin/env bash -echo "POST COMPILE" \ No newline at end of file +echo "POST COMPILE" diff --git a/test/fixtures/mod-basic-with-hooks/bin/go-pre-compile b/test/fixtures/mod-basic-with-hooks/bin/go-pre-compile index 15b91a11..e850e057 100644 --- a/test/fixtures/mod-basic-with-hooks/bin/go-pre-compile +++ b/test/fixtures/mod-basic-with-hooks/bin/go-pre-compile @@ -1,3 +1,3 @@ #!/usr/bin/env bash -echo "PRE COMPILE" \ No newline at end of file +echo "PRE COMPILE" diff --git a/test/run.sh b/test/run.sh index 03c89c28..3a7f11c6 100755 --- a/test/run.sh +++ b/test/run.sh @@ -2,352 +2,352 @@ # See README.md for info on running these tests. testTestPackModulesVendoredGolangLintCI() { - fixture "mod-deps-vendored-with-tests" - - dotest - assertCapturedExitSuccess - assertCaptured "RUN Test_BasicTest" - assertCaptured "PASS: Test_BasicTest" - assertCaptured "/.golangci.{yml,toml,json} detected" - assertCaptured "Running: golangci-lint -v --build-tags heroku run" + fixture "mod-deps-vendored-with-tests" + + dotest + assertCapturedExitSuccess + assertCaptured "RUN Test_BasicTest" + assertCaptured "PASS: Test_BasicTest" + assertCaptured "/.golangci.{yml,toml,json} detected" + assertCaptured "Running: golangci-lint -v --build-tags heroku run" } testTestPackModulesGolangLintCI() { - fixture "mod-deps-with-tests" + fixture "mod-deps-with-tests" - dotest - assertCapturedExitSuccess + dotest + assertCapturedExitSuccess - # The other deps are downloaded/installed - assertCaptured " + # The other deps are downloaded/installed + assertCaptured " go: finding github.com/gorilla/mux v1.6.2 go: finding github.com/gorilla/context v1.1.1 go: downloading github.com/gorilla/mux v1.6.2 go: extracting github.com/gorilla/mux v1.6.2 github.com/gorilla/mux " - assertCaptured "RUN Test_BasicTest" - assertCaptured "PASS: Test_BasicTest" - assertCaptured "/.golangci.{yml,toml,json} detected" - assertCaptured "Running: golangci-lint -v --build-tags heroku run" + assertCaptured "RUN Test_BasicTest" + assertCaptured "PASS: Test_BasicTest" + assertCaptured "/.golangci.{yml,toml,json} detected" + assertCaptured "Running: golangci-lint -v --build-tags heroku run" } testTestPackModulesGolangLintCI116() { - fixture "mod-deps-with-tests-116" + fixture "mod-deps-with-tests-116" - dotest - assertCapturedExitSuccess + dotest + assertCapturedExitSuccess - # The other deps are downloaded/installed - assertCaptured " + # The other deps are downloaded/installed + assertCaptured " go: finding github.com/gorilla/mux v1.6.2 go: finding github.com/gorilla/context v1.1.1 go: downloading github.com/gorilla/mux v1.6.2 go: extracting github.com/gorilla/mux v1.6.2 github.com/gorilla/mux " - assertCaptured "RUN Test_BasicTest" - assertCaptured "PASS: Test_BasicTest" - assertCaptured "/.golangci.{yml,toml,json} detected" - assertCaptured "Running: golangci-lint -v --build-tags heroku run" + assertCaptured "RUN Test_BasicTest" + assertCaptured "PASS: Test_BasicTest" + assertCaptured "/.golangci.{yml,toml,json} detected" + assertCaptured "Running: golangci-lint -v --build-tags heroku run" } testModProcfileCreation() { - fixture "mod-cmd-web" + fixture "mod-cmd-web" - assertDetected + assertDetected - compile - assertModulesBoilerplateCaptured - assertGoInstallCaptured "go1.12.17" - assertCaptured "Running: go install -v -tags heroku github.com/heroku/fixture/cmd/web + compile + assertModulesBoilerplateCaptured + assertGoInstallCaptured "go1.12.17" + assertCaptured "Running: go install -v -tags heroku github.com/heroku/fixture/cmd/web github.com/heroku/fixture/cmd/other" - assertCapturedExitSuccess - assertFile "other: bin/other + assertCapturedExitSuccess + assertFile "other: bin/other web: bin/web" "Procfile" } testModDepsRecompile() { - fixture "mod-deps" + fixture "mod-deps" - assertDetected + assertDetected - compile - assertModulesBoilerplateCaptured - assertGoInstallCaptured - assertGoInstallOnlyFixturePackageCaptured + compile + assertModulesBoilerplateCaptured + assertGoInstallCaptured + assertGoInstallOnlyFixturePackageCaptured - # The other deps are downloaded/installed - assertCaptured " + # The other deps are downloaded/installed + assertCaptured " go: finding github.com/gorilla/mux v1.6.2 go: finding github.com/gorilla/context v1.1.1 go: downloading github.com/gorilla/mux v1.6.2 go: extracting github.com/gorilla/mux v1.6.2 github.com/gorilla/mux " - assertCapturedExitSuccess - assertInstalledFixtureBinary - - # Second compile - compile - assertModulesBoilerplateCaptured - assertGoInstallOnlyFixturePackageCaptured - - # On the second compile go should already be fetched and installed & the packages should be downloaded already. - assertNotCaptured "Fetching ${DEFAULT_GO_VERSION}" - assertNotCaptured "Installing ${DEFAULT_GO_VERSION}" - assertNotCaptured "go: finding github.com/gorilla/mux v1.6.2" - assertNotCaptured "go: finding github.com/gorilla/context v1.1.1" - assertNotCaptured "go: downloading github.com/gorilla/mux v1.6.2" - assertNotCaptured "go: extracting github.com/gorilla/mux v1.6.2" - - assertCapturedExitSuccess - assertInstalledFixtureBinary + assertCapturedExitSuccess + assertInstalledFixtureBinary + + # Second compile + compile + assertModulesBoilerplateCaptured + assertGoInstallOnlyFixturePackageCaptured + + # On the second compile go should already be fetched and installed & the packages should be downloaded already. + assertNotCaptured "Fetching ${DEFAULT_GO_VERSION}" + assertNotCaptured "Installing ${DEFAULT_GO_VERSION}" + assertNotCaptured "go: finding github.com/gorilla/mux v1.6.2" + assertNotCaptured "go: finding github.com/gorilla/context v1.1.1" + assertNotCaptured "go: downloading github.com/gorilla/mux v1.6.2" + assertNotCaptured "go: extracting github.com/gorilla/mux v1.6.2" + + assertCapturedExitSuccess + assertInstalledFixtureBinary } testModWithQuotesModule() { - fixture "mod-with-quoted-module" + fixture "mod-with-quoted-module" - assertDetected + assertDetected - compile - assertModulesBoilerplateCaptured - assertGoInstallCaptured "go1.12.17" - assertGoInstallOnlyFixturePackageCaptured + compile + assertModulesBoilerplateCaptured + assertGoInstallCaptured "go1.12.17" + assertGoInstallOnlyFixturePackageCaptured - assertCapturedExitSuccess - assertInstalledFixtureBinary - assertFile "web: bin/fixture" "Procfile" + assertCapturedExitSuccess + assertInstalledFixtureBinary + assertFile "web: bin/fixture" "Procfile" } testModWithNonFilesInBin() { - fixture "mod-with-non-files-in-bin" + fixture "mod-with-non-files-in-bin" - assertDetected + assertDetected - compile - assertModulesBoilerplateCaptured - assertGoInstallCaptured - assertGoInstallOnlyFixturePackageCaptured + compile + assertModulesBoilerplateCaptured + assertGoInstallCaptured + assertGoInstallOnlyFixturePackageCaptured - assertNotCaptured "go: finding github.com/gorilla/mux v1.6.2" - assertNotCaptured "go: finding github.com/gorilla/context v1.1.1" - assertNotCaptured "go: downloading github.com/gorilla/mux v1.6.2" - assertNotCaptured "go: extracting github.com/gorilla/mux v1.6.2" + assertNotCaptured "go: finding github.com/gorilla/mux v1.6.2" + assertNotCaptured "go: finding github.com/gorilla/context v1.1.1" + assertNotCaptured "go: downloading github.com/gorilla/mux v1.6.2" + assertNotCaptured "go: extracting github.com/gorilla/mux v1.6.2" - assertCapturedExitSuccess - assertInstalledFixtureBinary + assertCapturedExitSuccess + assertInstalledFixtureBinary } testModcmdDetection() { - fixture "mod-cmd" + fixture "mod-cmd" - assertDetected + assertDetected - compile - assertModulesBoilerplateCaptured - assertGoInstallCaptured "go1.12.17" - assertCaptured "Detected the following main packages to install: + compile + assertModulesBoilerplateCaptured + assertGoInstallCaptured "go1.12.17" + assertCaptured "Detected the following main packages to install: github.com/heroku/fixture/cmd/fixture github.com/heroku/fixture/cmd/other" - assertCaptured "Running: go install -v -tags heroku github.com/heroku/fixture/cmd/fixture github.com/heroku/fixture/cmd/other + assertCaptured "Running: go install -v -tags heroku github.com/heroku/fixture/cmd/fixture github.com/heroku/fixture/cmd/other github.com/heroku/fixture/cmd/fixture github.com/heroku/fixture/cmd/other" - assertCaptured "Installed the following binaries: + assertCaptured "Installed the following binaries: ./bin/fixture ./bin/other" - assertFile "fixture: bin/fixture + assertFile "fixture: bin/fixture other: bin/other" "Procfile" - assertCapturedExitSuccess - assertInstalledFixtureBinary - assertCompiledBinaryExists other + assertCapturedExitSuccess + assertInstalledFixtureBinary + assertCompiledBinaryExists other } testModWithHooks() { - fixture "mod-basic-with-hooks" + fixture "mod-basic-with-hooks" - assertDetected + assertDetected - compile - assertModulesBoilerplateCaptured - assertGoInstallCaptured + compile + assertModulesBoilerplateCaptured + assertGoInstallCaptured - assertCaptured "Running bin/go-pre-compile hook + assertCaptured "Running bin/go-pre-compile hook PRE COMPILE" - assertGoInstallOnlyFixturePackageCaptured - assertCaptured "Running bin/go-post-compile hook + assertGoInstallOnlyFixturePackageCaptured + assertCaptured "Running bin/go-post-compile hook POST COMPILE" - assertCapturedExitSuccess - assertInstalledFixtureBinary + assertCapturedExitSuccess + assertInstalledFixtureBinary } testModNoVersion() { - fixture "mod-no-version" + fixture "mod-no-version" - assertDetected + assertDetected - compile - assertModulesBoilerplateCaptured - assertGoInstallCaptured - assertGoInstallOnlyFixturePackageCaptured + compile + assertModulesBoilerplateCaptured + assertGoInstallCaptured + assertGoInstallOnlyFixturePackageCaptured - assertCapturedExitSuccess - assertCapturedStderr "does not specify a Go version" - assertInstalledFixtureBinary + assertCapturedExitSuccess + assertCapturedStderr "does not specify a Go version" + assertInstalledFixtureBinary } testModOldVersion() { - fixture "mod-old-version" + fixture "mod-old-version" - assertDetected + assertDetected - compile - assertCaptured "Detected go modules via go.mod" - assertCaptured "Detected Module Name: github.com/heroku/fixture" - assertCapturedError 1 "a Go version >= go1.11 like so:" + compile + assertCaptured "Detected go modules via go.mod" + assertCaptured "Detected Module Name: github.com/heroku/fixture" + assertCapturedError 1 "a Go version >= go1.11 like so:" } testModInstall() { - fixture "mod-install" + fixture "mod-install" - assertDetected + assertDetected - compile - assertModulesBoilerplateCaptured - assertGoInstallCaptured + compile + assertModulesBoilerplateCaptured + assertGoInstallCaptured - assertCaptured "Running: go install -v -tags heroku ./cmd/... ./other + assertCaptured "Running: go install -v -tags heroku ./cmd/... ./other github.com/heroku/fixture/cmd/fixture1 github.com/heroku/fixture/cmd/fixture2 github.com/heroku/fixture/other" - assertCaptured "Installed the following binaries: + assertCaptured "Installed the following binaries: ./bin/fixture1 ./bin/fixture2 ./bin/other" - assertCapturedExitSuccess - assertCompiledBinaryExists "fixture1" - assertCompiledBinaryExists "fixture2" - assertCompiledBinaryExists "other" + assertCapturedExitSuccess + assertCompiledBinaryExists "fixture1" + assertCompiledBinaryExists "fixture2" + assertCompiledBinaryExists "other" } testModBasic() { - fixture "mod-basic" + fixture "mod-basic" - assertDetected + assertDetected - compile - assertModulesBoilerplateCaptured - assertGoInstallCaptured - assertGoInstallOnlyFixturePackageCaptured + compile + assertModulesBoilerplateCaptured + assertGoInstallCaptured + assertGoInstallOnlyFixturePackageCaptured - assertCapturedExitSuccess - assertInstalledFixtureBinary + assertCapturedExitSuccess + assertInstalledFixtureBinary } testModBasicGo111() { - fixture "mod-basic-go111" + fixture "mod-basic-go111" - assertDetected + assertDetected - compile - assertModulesBoilerplateCaptured - assertCaptured "Installing go1.11.13" - assertGoInstallOnlyFixturePackageCaptured + compile + assertModulesBoilerplateCaptured + assertCaptured "Installing go1.11.13" + assertGoInstallOnlyFixturePackageCaptured - assertCapturedExitSuccess - assertInstalledFixtureBinary + assertCapturedExitSuccess + assertInstalledFixtureBinary } testModBasicGo125() { - fixture "mod-basic-go125" + fixture "mod-basic-go125" - assertDetected + assertDetected - compile - assertModulesBoilerplateCaptured - assertCaptured "Installing go1.25" - assertGoInstallOnlyFixturePackageCaptured + compile + assertModulesBoilerplateCaptured + assertCaptured "Installing go1.25" + assertGoInstallOnlyFixturePackageCaptured - assertCapturedExitSuccess - assertInstalledFixtureBinary + assertCapturedExitSuccess + assertInstalledFixtureBinary } testModBasicGo126() { - fixture "mod-basic-go126" + fixture "mod-basic-go126" - assertDetected + assertDetected - compile - assertModulesBoilerplateCaptured - assertCaptured "Installing go1.26" - assertGoInstallOnlyFixturePackageCaptured + compile + assertModulesBoilerplateCaptured + assertCaptured "Installing go1.26" + assertGoInstallOnlyFixturePackageCaptured - assertCapturedExitSuccess - assertInstalledFixtureBinary + assertCapturedExitSuccess + assertInstalledFixtureBinary } testModBasicWithoutProcfile() { - fixture "mod-basic-wo-procfile" + fixture "mod-basic-wo-procfile" - assertDetected + assertDetected - compile - assertModulesBoilerplateCaptured - assertGoInstallCaptured - assertGoInstallOnlyFixturePackageCaptured + compile + assertModulesBoilerplateCaptured + assertGoInstallCaptured + assertGoInstallOnlyFixturePackageCaptured - assertCapturedExitSuccess - assertInstalledFixtureBinary - assertFile "web: bin/fixture" "Procfile" + assertCapturedExitSuccess + assertInstalledFixtureBinary + assertFile "web: bin/fixture" "Procfile" } testModPrivateProxy() { - local repo="${BUILDPACK_HOME}/test/fixtures/mod-private-proxy/repo" - fixture "mod-private-proxy/app" + local repo="${BUILDPACK_HOME}/test/fixtures/mod-private-proxy/repo" + fixture "mod-private-proxy/app" - env "GOPROXY" "file://$repo" - env "GOPRIVATE" "git.fury.io/*" - env "GONOPROXY" "none" + env "GOPROXY" "file://${repo}" + env "GOPRIVATE" "git.fury.io/*" + env "GONOPROXY" "none" - assertDetected + assertDetected - compile - assertModulesBoilerplateCaptured - assertGoInstallCaptured "go1.15.15" - assertGoInstallOnlyFixturePackageCaptured + compile + assertModulesBoilerplateCaptured + assertGoInstallCaptured "go1.15.15" + assertGoInstallOnlyFixturePackageCaptured - assertCapturedExitSuccess - assertInstalledFixtureBinary + assertCapturedExitSuccess + assertInstalledFixtureBinary } testModDeps() { - fixture "mod-deps" + fixture "mod-deps" - assertDetected + assertDetected - compile - assertModulesBoilerplateCaptured - assertGoInstallCaptured - assertGoInstallOnlyFixturePackageCaptured + compile + assertModulesBoilerplateCaptured + assertGoInstallCaptured + assertGoInstallOnlyFixturePackageCaptured - # The other deps are downloaded/installed - assertCaptured " + # The other deps are downloaded/installed + assertCaptured " go: finding github.com/gorilla/mux v1.6.2 go: finding github.com/gorilla/context v1.1.1 go: downloading github.com/gorilla/mux v1.6.2 go: extracting github.com/gorilla/mux v1.6.2 github.com/gorilla/mux " - assertCapturedExitSuccess - assertInstalledFixtureBinary + assertCapturedExitSuccess + assertInstalledFixtureBinary } # Ensure that a project works when: @@ -360,133 +360,132 @@ github.com/gorilla/mux # activates new consistency checks between go.mod and the vendor # directory, described at https://golang.org/doc/go1.14#vendor. testModDeps114() { - fixture "mod-deps-114" + fixture "mod-deps-114" - assertDetected + assertDetected - compile - assertModulesBoilerplateCaptured - assertCaptured "Installing go1.14.2" - assertGoInstallOnlyFixturePackageCaptured + compile + assertModulesBoilerplateCaptured + assertCaptured "Installing go1.14.2" + assertGoInstallOnlyFixturePackageCaptured - # The other deps are downloaded/installed - assertCaptured " + # The other deps are downloaded/installed + assertCaptured " go: finding github.com/gorilla/mux v1.6.2 go: finding github.com/gorilla/context v1.1.1 go: downloading github.com/gorilla/mux v1.6.2 go: extracting github.com/gorilla/mux v1.6.2 github.com/gorilla/mux " - assertCapturedExitSuccess - assertInstalledFixtureBinary + assertCapturedExitSuccess + assertInstalledFixtureBinary } testModDepsVendored() { - fixture "mod-deps-vendored" + fixture "mod-deps-vendored" - assertDetected + assertDetected - compile - assertModulesBoilerplateCaptured - assertGoInstallCaptured - assertGoInstallOnlyFixturePackageCaptured + compile + assertModulesBoilerplateCaptured + assertGoInstallCaptured + assertGoInstallOnlyFixturePackageCaptured - assertNotCaptured "go: finding github.com/gorilla/mux v1.6.2" - assertNotCaptured "go: finding github.com/gorilla/context v1.1.1" - assertNotCaptured "go: downloading github.com/gorilla/mux v1.6.2" - assertNotCaptured "go: extracting github.com/gorilla/mux v1.6.2" + assertNotCaptured "go: finding github.com/gorilla/mux v1.6.2" + assertNotCaptured "go: finding github.com/gorilla/context v1.1.1" + assertNotCaptured "go: downloading github.com/gorilla/mux v1.6.2" + assertNotCaptured "go: extracting github.com/gorilla/mux v1.6.2" - assertCapturedExitSuccess - assertInstalledFixtureBinary + assertCapturedExitSuccess + assertInstalledFixtureBinary } testModPackageSpecOverride() { - fixture "mod-cmd" + fixture "mod-cmd" - env "GO_INSTALL_PACKAGE_SPEC" "./cmd/fixture" + env "GO_INSTALL_PACKAGE_SPEC" "./cmd/fixture" - assertDetected + assertDetected - compile - assertModulesBoilerplateCaptured - assertGoInstallCaptured "go1.12.17" - assertCapturedStderr "Using \$GO_INSTALL_PACKAGE_SPEC override." - assertCaptured "Running: go install -v -tags heroku ./cmd/fixture" - assertCapturedExitSuccess - assertCompiledBinaryExists "fixture" - assertBuildDirFileDoesNotExist "bin/other" + compile + assertModulesBoilerplateCaptured + assertGoInstallCaptured "go1.12.17" + assertCapturedStderr "Using \$GO_INSTALL_PACKAGE_SPEC override." + assertCaptured "Running: go install -v -tags heroku ./cmd/fixture" + assertCapturedExitSuccess + assertCompiledBinaryExists "fixture" + assertBuildDirFileDoesNotExist "bin/other" } testModGOVERSIONOverride() { - fixture "mod-basic" + fixture "mod-basic" - env "GOVERSION" "go1.24" + env "GOVERSION" "go1.24" - assertDetected + assertDetected - compile - assertCaptured "Installing go1.24" - assertCapturedStderr "Using \$GOVERSION override." - assertGoInstallOnlyFixturePackageCaptured - assertCapturedExitSuccess - assertCompiledBinaryExists + compile + assertCaptured "Installing go1.24" + assertCapturedStderr "Using \$GOVERSION override." + assertGoInstallOnlyFixturePackageCaptured + assertCapturedExitSuccess + assertCompiledBinaryExists } testModBinFile() { - fixture "mod-bin-file" + fixture "mod-bin-file" - assertDetected + assertDetected - compile - assertCapturedError 1 "File bin exists and is not a directory." + compile + assertCapturedError 1 "File bin exists and is not a directory." } testModLDSymbolValue() { - fixture "mod-ld-symbol-value" + fixture "mod-ld-symbol-value" - env "GO_LINKER_SYMBOL" "main.fixture" - env "GO_LINKER_VALUE" "fixture" + env "GO_LINKER_SYMBOL" "main.fixture" + env "GO_LINKER_VALUE" "fixture" - assertDetected + assertDetected - compile - assertModulesBoilerplateCaptured - assertGoInstallCaptured - assertCaptured "Running: go install -v -tags heroku -ldflags -X main.fixture=fixture" - assertCaptured "github.com/heroku/fixture" - assertCapturedExitSuccess - assertCompiledBinaryExists - assertCompiledBinaryOutputs "fixture" "fixture" + compile + assertModulesBoilerplateCaptured + assertGoInstallCaptured + assertCaptured "Running: go install -v -tags heroku -ldflags -X main.fixture=fixture" + assertCaptured "github.com/heroku/fixture" + assertCapturedExitSuccess + assertCompiledBinaryExists + assertCompiledBinaryOutputs "fixture" "fixture" } testModBasicWithTools() { - fixture "mod-basic" + fixture "mod-basic" - env "GO_INSTALL_TOOLS_IN_IMAGE" "true" + env "GO_INSTALL_TOOLS_IN_IMAGE" "true" - assertDetected + assertDetected - compile - assertModulesBoilerplateCaptured - assertGoInstallCaptured - assertGoInstallOnlyFixturePackageCaptured - assertCaptured "Copying go tool chain to" - assertCapturedExitSuccess - assertCompiledBinaryExists - assertBuildDirFileExists ".heroku/go/bin/go" + compile + assertModulesBoilerplateCaptured + assertGoInstallCaptured + assertGoInstallOnlyFixturePackageCaptured + assertCaptured "Copying go tool chain to" + assertCapturedExitSuccess + assertCompiledBinaryExists + assertBuildDirFileExists ".heroku/go/bin/go" } testDeprecatedToolDetected() { - fixture "dep-deprecated" + fixture "dep-deprecated" - assertDetected + assertDetected - compile - assertCapturedError 1 "support for dep has been removed" + compile + assertCapturedError 1 "support for dep has been removed" } -pushd $(dirname 0) >/dev/null -popd >/dev/null - -source $(pwd)/test/utils.sh -source $(pwd)/test/shunit2.sh +BUILDPACK_DIR=$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd) +source "${BUILDPACK_DIR}/test/utils.sh" +# shellcheck disable=SC1091 +source "${BUILDPACK_DIR}/test/shunit2.sh" diff --git a/test/utils.sh b/test/utils.sh index f218b422..69cf3fe2 100644 --- a/test/utils.sh +++ b/test/utils.sh @@ -1,37 +1,40 @@ +#!/usr/bin/env bash + ############## ## shunit2 setup/teardown functions ## +# shellcheck disable=SC2154 # SHUNIT_TMPDIR is set by the shunit2 test framework oneTimeSetUp() { - TEST_SUITE_CACHE="$(mktemp -d ${SHUNIT_TMPDIR}/test_suite_cache.XXXX)" - BUILDPACK_HOME=$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd) - DEFAULT_GO_VERSION="$(<"${BUILDPACK_HOME}/data.json" jq -r '.Go.DefaultVersion')" + TEST_SUITE_CACHE="$(mktemp -d "${SHUNIT_TMPDIR}/test_suite_cache.XXXX")" + BUILDPACK_HOME=$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd) + DEFAULT_GO_VERSION="$(<"${BUILDPACK_HOME}/data.json" jq -r '.Go.DefaultVersion')" } oneTimeTearDown() { - rm -rf ${TEST_SUITE_CACHE} + rm -rf "${TEST_SUITE_CACHE}" } setUp() { - OUTPUT_DIR="$(mktemp -d ${SHUNIT_TMPDIR}/output.XXXX)" - STD_OUT="${OUTPUT_DIR}/stdout" - STD_ERR="${OUTPUT_DIR}/stderr" - BUILD_DIR="${OUTPUT_DIR}/build" - CACHE_DIR="${OUTPUT_DIR}/cache" - ENV_DIR="${OUTPUT_DIR}/env" - mkdir -p ${OUTPUT_DIR} - mkdir -p ${BUILD_DIR} - mkdir -p ${CACHE_DIR} - mkdir -p ${ENV_DIR} + OUTPUT_DIR="$(mktemp -d "${SHUNIT_TMPDIR}/output.XXXX")" + STD_OUT="${OUTPUT_DIR}/stdout" + STD_ERR="${OUTPUT_DIR}/stderr" + BUILD_DIR="${OUTPUT_DIR}/build" + CACHE_DIR="${OUTPUT_DIR}/cache" + ENV_DIR="${OUTPUT_DIR}/env" + mkdir -p "${OUTPUT_DIR}" + mkdir -p "${BUILD_DIR}" + mkdir -p "${CACHE_DIR}" + mkdir -p "${ENV_DIR}" } tearDown() { - if [ -d "${OUTPUT_DIR}" ]; then - # Go modules are read-only by default; give ourselves write permission to delete it first - chmod -R +w "${OUTPUT_DIR}" - rm -rf "${OUTPUT_DIR}" - fi + if [[ -d "${OUTPUT_DIR}" ]]; then + # Go modules are read-only by default; give ourselves write permission to delete it first + chmod -R +w "${OUTPUT_DIR}" + rm -rf "${OUTPUT_DIR}" + fi } ############## @@ -39,277 +42,295 @@ tearDown() { ## capture() { - resetCapture + resetCapture - LAST_COMMAND="$@" + LAST_COMMAND="$*" - $@ >${STD_OUT} 2>${STD_ERR} - RETURN=$? - rtrn=${RETURN} # deprecated + "$@" >"${STD_OUT}" 2>"${STD_ERR}" + RETURN=$? + # shellcheck disable=SC2034 # deprecated, used externally + rtrn=${RETURN} } continue_capture() { - LAST_COMMAND="$@" + # shellcheck disable=SC2034 # LAST_COMMAND is used externally + LAST_COMMAND="$*" - $@ >>${STD_OUT} 2>>${STD_ERR} - local cr=$? - if [ "$RETURN" = "0" ]; then - RETURN=$cr - fi - rtrn=${RETURN} # deprecated + "$@" >>"${STD_OUT}" 2>>"${STD_ERR}" + local cr=$? + if [[ "${RETURN}" = "0" ]]; then + RETURN=${cr} + fi + # shellcheck disable=SC2034 # deprecated, used externally + rtrn=${RETURN} } resetCapture() { - if [ -f ${STD_OUT} ]; then - rm ${STD_OUT} - fi + if [[ -f "${STD_OUT}" ]]; then + rm "${STD_OUT}" + fi - if [ -f "${STD_OUT}_no_color" ]; then - rm "${STD_OUT}_no_color" - fi + if [[ -f "${STD_OUT}_no_color" ]]; then + rm "${STD_OUT}_no_color" + fi - if [ -f ${STD_ERR} ]; then - rm ${STD_ERR} - fi + if [[ -f "${STD_ERR}" ]]; then + rm "${STD_ERR}" + fi - unset LAST_COMMAND - unset RETURN - unset rtrn # deprecated + unset LAST_COMMAND + unset RETURN + unset rtrn # deprecated } fixture() { - local fixture="${1}" - echo "* fixture: ${fixture}" - local fp="${BUILDPACK_HOME}/test/fixtures/${fixture}" - tar -cf - -C $fp . | tar -x -C ${BUILD_DIR} + local fixture="${1}" + echo "* fixture: ${fixture}" + local fp="${BUILDPACK_HOME}/test/fixtures/${fixture}" + # shellcheck disable=SC2312 + tar -cf - -C "${fp}" . | tar -x -C "${BUILD_DIR}" } env() { - local var="${1}" - local val="${2}" - if [ -z "${var}" ]; then - fail "set env var w/o specifying name" - exit 1 - fi - echo -n "${val}" > "${ENV_DIR}/${var}" + local var="${1}" + local val="${2}" + if [[ -z "${var}" ]]; then + fail "set env var w/o specifying name" + exit 1 + fi + echo -n "${val}" >"${ENV_DIR}/${var}" } detect() { - echo "* detect" - capture ${BUILDPACK_HOME}/bin/detect ${BUILD_DIR} + echo "* detect" + capture "${BUILDPACK_HOME}/bin/detect" "${BUILD_DIR}" } assertDetected() { - detect - assertCaptured "Go" - assertCapturedSuccess + detect + assertCaptured "Go" + assertCapturedSuccess } compile() { - echo "* compile" - capture ${BUILDPACK_HOME}/bin/compile ${BUILD_DIR} ${CACHE_DIR} ${ENV_DIR} + echo "* compile" + capture "${BUILDPACK_HOME}/bin/compile" "${BUILD_DIR}" "${CACHE_DIR}" "${ENV_DIR}" } dotest() { - # On Heroku CI, test-compile and test are run in such a way that the - # provided BUILD_DIR is the same as HOME. Simulate that here to get - # better fidelity. For example, this ensures that the default GOPATH of - # $HOME/go doesn't cause issues. - echo "* test-compile" - HOME="${BUILD_DIR}" capture "${BUILDPACK_HOME}/bin/test-compile" "${BUILD_DIR}" "${CACHE_DIR}" "${ENV_DIR}" - echo "* test" - HOME="${BUILD_DIR}" continue_capture "${BUILDPACK_HOME}/bin/test" "${BUILD_DIR}" "${ENV_DIR}" + # On Heroku CI, test-compile and test are run in such a way that the + # provided BUILD_DIR is the same as HOME. Simulate that here to get + # better fidelity. For example, this ensures that the default GOPATH of + # $HOME/go doesn't cause issues. + echo "* test-compile" + HOME="${BUILD_DIR}" capture "${BUILDPACK_HOME}/bin/test-compile" "${BUILD_DIR}" "${CACHE_DIR}" "${ENV_DIR}" + echo "* test" + HOME="${BUILD_DIR}" continue_capture "${BUILDPACK_HOME}/bin/test" "${BUILD_DIR}" "${ENV_DIR}" } release() { - capture ${BUILDPACK_HOME}/bin/release ${BUILD_DIR} + capture "${BUILDPACK_HOME}/bin/release" "${BUILD_DIR}" } assertFile() { - local content="${1}" - local name="${2}" - local tgt="${BUILD_DIR}/${name}" - assertEquals "${content}" "$(cat ${tgt})" + local content="${1}" + local name="${2}" + local tgt="${BUILD_DIR}/${name}" + # shellcheck disable=SC2312 + assertEquals "${content}" "$(cat "${tgt}")" } assertBuildDirFileDoesNotExist() { - local name="${1}" - local tgt="${BUILD_DIR}/${name}" - assertTrue "File ${name} exists" "[ ! -f ${tgt} ]" + local name="${1}" + local tgt="${BUILD_DIR}/${name}" + assertTrue "File ${name} exists" "[[ ! -f ${tgt} ]]" } assertBuildDirFileExists() { - local name="${1}" - local tgt="${BUILD_DIR}/${name}" - assertTrue "File ${name} does not exist" "[ -f ${tgt} ]" + local name="${1}" + local tgt="${BUILD_DIR}/${name}" + assertTrue "File ${name} does not exist" "[[ -f ${tgt} ]]" } assertFileExists() { - local name="${1}" - assertTrue "File ${name} does not exist" "[ -f ${name} ]" + local name="${1}" + assertTrue "File ${name} does not exist" "[[ -f ${name} ]]" } assertDirExists() { - local path="${1}" - assertTrue "Dir ${path} does not exist" "[ -d ${path} ]" + local path="${1}" + assertTrue "Dir ${path} does not exist" "[[ -d ${path} ]]" } assertDirDoesNotExist() { - local path="${1}" - assertTrue "Dir ${path} exists" "[ ! -d ${path} ]" + local path="${1}" + assertTrue "Dir ${path} exists" "[[ ! -d ${path} ]]" } assertCompiledBinaryExists() { - local name="${1:-fixture}" - local tgt="${BUILD_DIR}/bin/${name}" - assertTrue "Compiled binary (${tgt}) exists" "[ -x ${tgt} ]" + local name="${1:-fixture}" + local tgt="${BUILD_DIR}/bin/${name}" + assertTrue "Compiled binary (${tgt}) exists" "[[ -x ${tgt} ]]" } assertCompiledBinaryOutputs() { - local name="${1}" - local output="${2}" - capture "${BUILD_DIR}/bin/${name}" + local name="${1}" + # shellcheck disable=SC2034 # output is reserved for future use + local output="${2}" + capture "${BUILD_DIR}/bin/${name}" } assertCapturedEquals() { - assertEquals "$@" "$(cat ${STD_OUT})" + # shellcheck disable=SC2312 + assertEquals "$@" "$(cat "${STD_OUT}")" } assertCapturedNotEquals() { - assertNotEquals "$@" "$(cat ${STD_OUT})" + # shellcheck disable=SC2312 + assertNotEquals "$@" "$(cat "${STD_OUT}")" } assertCaptured() { - assertFileContains "$@" "${STD_OUT}" + assertFileContains "$@" "${STD_OUT}" } assertCapturedStderr() { - assertFileContains "$@" "${STD_ERR}" + assertFileContains "$@" "${STD_ERR}" } assertNotCaptured() { - assertFileNotContains "$@" "${STD_OUT}" + assertFileNotContains "$@" "${STD_OUT}" } assertCapturedSuccess() { - assertEquals "Expected captured exit code to be 0; was <${RETURN}>" "0" "${RETURN}" - assertEquals "Expected STD_ERR to be empty; was <$(cat ${STD_ERR})>" "" "$(cat ${STD_ERR})" + assertEquals "Expected captured exit code to be 0; was <${RETURN}>" "0" "${RETURN}" + # shellcheck disable=SC2312 + assertEquals "Expected STD_ERR to be empty; was <$(cat "${STD_ERR}")>" "" "$(cat "${STD_ERR}")" } assertCapturedExitSuccess() { - assertEquals "Expected captured exit code to be 0; was <${RETURN}>" "0" "${RETURN}" + assertEquals "Expected captured exit code to be 0; was <${RETURN}>" "0" "${RETURN}" } # assertCapturedError [[expectedErrorCode] expectedErrorMsg] assertCapturedError() { - if [ $# -gt 1 ]; then - local expectedErrorCode="${1}" - shift - fi + local expectedErrorCode="" + if [[ $# -gt 1 ]]; then + expectedErrorCode="${1}" + shift + fi - local expectedErrorMsg="${1:-""}" + local expectedErrorMsg="${1:-""}" - if [ -z ${expectedErrorCode} ]; then - assertTrue "Expected captured exit code to be greater than 0; was <${RETURN}>" "[ ${RETURN} -gt 0 ]" - else - assertTrue "Expected captured exit code to be <${expectedErrorCode}>; was <${RETURN}>" "[ ${RETURN} -eq ${expectedErrorCode} ]" - fi + if [[ -z "${expectedErrorCode}" ]]; then + assertTrue "Expected captured exit code to be greater than 0; was <${RETURN}>" "[[ ${RETURN} -gt 0 ]]" + else + assertTrue "Expected captured exit code to be <${expectedErrorCode}>; was <${RETURN}>" "[[ ${RETURN} -eq ${expectedErrorCode} ]]" + fi - if [ "${expectedErrorMsg}" != "" ]; then - assertFileContains "Expected STD_ERR to contain error <${expectedErrorMsg}>" "${expectedErrorMsg}" "${STD_ERR}" - fi + if [[ "${expectedErrorMsg}" != "" ]]; then + assertFileContains "Expected STD_ERR to contain error <${expectedErrorMsg}>" "${expectedErrorMsg}" "${STD_ERR}" + fi } _assertContains() { - if [ 5 -eq $# ]; then - local msg="${1}" - shift - elif [ ! 4 -eq $# ]; then - fail "Expected 4 or 5 parameters; Receieved $# parameters" - fi - - local needle=$1 - local haystack=$2 - local expectation=$3 - local haystack_type=$4 - - case "${haystack_type}" in - "file") - local haystack_no_color="${haystack}_no_color" - if [ ! -e ${haystack_no_color} ]; then - sed "s,\x1B\[[0-9;]*[a-zA-Z],,g" < ${haystack} > ${haystack_no_color} - fi - ## echo grep -q -F -e "${needle}" ${haystack_no_color} - grep -q -F -e "${needle}" ${haystack_no_color} ;; - "text") echo "${haystack}" | grep -q -F -e "${needle}" ;; - esac - - if [ "${expectation}" != "$?" ]; then - case "${expectation}" in - 0) default_msg="Expected <${haystack}> to contain <${needle}>" ;; - 1) default_msg="Did not expect <${haystack}> to contain <${needle}>" ;; - esac - - fail "${msg:-${default_msg}}" - fi + if [[ 5 -eq $# ]]; then + local msg="${1}" + shift + elif [[ 4 -ne $# ]]; then + fail "Expected 4 or 5 parameters; Receieved $# parameters" + fi + + local needle="${1}" + local haystack="${2}" + local expectation="${3}" + local haystack_type="${4}" + + case "${haystack_type}" in + "file") + local haystack_no_color="${haystack}_no_color" + if [[ ! -e "${haystack_no_color}" ]]; then + sed "s,\x1B\[[0-9;]*[a-zA-Z],,g" <"${haystack}" >"${haystack_no_color}" + fi + grep -q -F -e "${needle}" "${haystack_no_color}" + ;; + "text") echo "${haystack}" | grep -q -F -e "${needle}" ;; + *) fail "Unknown haystack_type: ${haystack_type}" ;; + esac + + local result=$? + if [[ "${expectation}" != "${result}" ]]; then + local default_msg + case "${expectation}" in + 0) default_msg="Expected <${haystack}> to contain <${needle}>" ;; + 1) default_msg="Did not expect <${haystack}> to contain <${needle}>" ;; + *) default_msg="Unexpected expectation value: ${expectation}" ;; + esac + + fail "${msg:-${default_msg}}" + fi } assertContains() { - _assertContains "$@" 0 "text" + _assertContains "$@" 0 "text" } assertNotContains() { - _assertContains "$@" 1 "text" + _assertContains "$@" 1 "text" } assertFileContains() { - _assertContains "$@" 0 "file" + _assertContains "$@" 0 "file" } assertFileNotContains() { - _assertContains "$@" 1 "file" + _assertContains "$@" 1 "file" } -command_exists () { - type "$1" > /dev/null 2>&1 ; +command_exists() { + type "$1" >/dev/null 2>&1 } assertFileMD5() { - expectedHash=$1 - filename=$2 - - if command_exists "md5sum"; then - md5_cmd="md5sum ${filename}" - expected_md5_cmd_output="${expectedHash} ${filename}" - elif command_exists "md5"; then - md5_cmd="md5 ${filename}" - expected_md5_cmd_output="MD5 (${filename}) = ${expectedHash}" - else - fail "no suitable MD5 hashing command found on this system" - fi - - assertEquals "${expected_md5_cmd_output}" "`${md5_cmd}`" + local expectedHash="${1}" + local filename="${2}" + local md5_cmd + local expected_md5_cmd_output + + # shellcheck disable=SC2310 # command_exists used in if condition + if command_exists "md5sum"; then + md5_cmd="md5sum ${filename}" + expected_md5_cmd_output="${expectedHash} ${filename}" + elif command_exists "md5"; then + md5_cmd="md5 ${filename}" + expected_md5_cmd_output="MD5 (${filename}) = ${expectedHash}" + else + fail "no suitable MD5 hashing command found on this system" + fi + + # shellcheck disable=SC2312 + assertEquals "${expected_md5_cmd_output}" "$(${md5_cmd})" } assertModulesBoilerplateCaptured() { - assertCaptured "Detected go modules via go.mod" - assertCaptured "Detected Module Name: github.com/heroku/fixture" - assertCaptured "Determining packages to install" + assertCaptured "Detected go modules via go.mod" + assertCaptured "Detected Module Name: github.com/heroku/fixture" + assertCaptured "Determining packages to install" } -assertGoInstallOnlyFixturePackageCaptured(){ - assertCaptured "Running: go install -v -tags heroku github.com/heroku/fixture +assertGoInstallOnlyFixturePackageCaptured() { + assertCaptured "Running: go install -v -tags heroku github.com/heroku/fixture github.com/heroku/fixture" } assertInstalledFixtureBinary() { - assertCaptured "Installed the following binaries: + assertCaptured "Installed the following binaries: ./bin/fixture" - assertCompiledBinaryExists fixture + assertCompiledBinaryExists fixture } assertGoInstallCaptured() { - local go_ver=${1:-${DEFAULT_GO_VERSION}} - assertCaptured "Installing ${go_ver}" - assertCaptured "Fetching ${go_ver}" + local go_ver="${1:-${DEFAULT_GO_VERSION}}" + assertCaptured "Installing ${go_ver}" + assertCaptured "Fetching ${go_ver}" }