diff --git a/Makefile b/Makefile index 3fb41dd0..26172d46 100644 --- a/Makefile +++ b/Makefile @@ -1,6 +1,10 @@ -SHELL := /bin/bash -.SHELLFLAGS := -eu -o pipefail .PHONY: check - check: @./bin/setup.sh + +.PHONY: test-cloudapi-graphql +test-cloudapi-graphql: + $(MAKE) -C cloudapi-graphql test + +.PHONY: test +test: test-cloudapi-graphql diff --git a/bin/history-check b/bin/history-check new file mode 100755 index 00000000..3891b23f --- /dev/null +++ b/bin/history-check @@ -0,0 +1,203 @@ +#! /usr/bin/env bash + +# +# Prelude - make bash behave sanely +# http://redsymbol.net/articles/unofficial-bash-strict-mode/ +# +set -euo pipefail +IFS=$'\n\t' + +# +# Globals +# +remember_git_start_and_end() { + HEAD="$(git rev-parse HEAD)" + ROOT="$(git log --pretty=format:%H | tail -n 1)" +} + +# +# Utilities +# +die() { + local msg="$@" + [[ -z "${msg}" ]] || { + tput setaf 1 # red + tput bold + echo "${msg}" + tput sgr0 # reset + } + exit 1 +} + +error() { + local msg="$@" + echo -n '| ' + tput setaf 1 # red + echo -n ' ✖' + tput sgr0 # reset + echo " ${msg}" +} + +success() { + local msg="$@" + echo -n '| ' + tput setaf 2 # green + echo -n ' ✓' + tput sgr0 # reset + echo " ${msg}" +} + +log_commit() { + echo "○ $@" +} + +# Check a command is present +ensure_command() { + local cmd="$1" + + command -v "${cmd}" > /dev/null 2>&1 || { + die "Couldn't find required command: ${cmd}" + } +} + +# +# Signal handling +# +cleanup() { + git reset --hard "${HEAD}" > /dev/null 2>&1 + rm -f $$_commit_message +} + +trap cleanup SIGHUP SIGINT SIGTERM + +# +# Git helpers +# + +# Go back one commit in history (first parent for merges) +step_back_one_commit() { + git reset --hard HEAD^ > /dev/null + log_commit "$(git rev-parse HEAD)" +} + +current_commit_message() { + GIT_PAGER= git log --format=%B -n 1 +} + +current_commit_sha() { + git rev-parse HEAD +} + +exit_if_not_git_repo() { + local gitroot="$(git rev-parse --show-toplevel 2> /dev/null)" + + [[ "${gitroot}" == "" ]] && die 'Current directory is not in a repository' + return 0 +} + +# +# Checks +# +check_commit_message() { + local lineno=0 + local length=0 + local succeded=1 + + while read -r line ; do + let succeded=1 + let lineno+=1 + length=${#line} + + [[ "${lineno}" -eq "1" ]] && { + [[ "${length}" -gt 50 ]] && { + error "Commit message: Subject line longer than 50 characters"; + succeded=0 + }; + + [[ ! "${line}" =~ ^[A-Z].*$ ]] && { + error "Commit message: Subject line not capitalised"; + succeded=0 + }; + + [[ "${line}" == *. ]] && { + error "Commit message: Subject line ended with a full stop"; + succeded=0 + }; + } + + [[ "${lineno}" -eq "2" ]] && [[ -n "${line}" ]] && { + error "Commit message: Subject line not separated by a blank line"; + succeded=0; + }; + + [[ "${lineno}" -gt "1" ]] && [[ "${length}" -gt "72" ]] && { + error "Commit message: Body not wrapped at 72 characters"; + succeded=0 + }; + done < $$_commit_message + + [[ "${succeded}" -eq "1" ]] && success "Commit message" + return 0 +} + +run_checks() { + current_commit_message > $$_commit_message + check_commit_message + rm -f $$_commit_message + set +e + npm run lint > /dev/null 2>&1 + if [[ "$?" -eq 0 ]]; then + success 'Lint' + else + error 'Lint: script did not exit successfully' + fi + npm test > /dev/null 2>&1 + if [[ "$?" -eq 0 ]]; then + success 'Test' + else + error 'Test: script did not exit successfully' + fi + set -e +} + +check_project() { + exit_if_not_git_repo + + [[ -f './package.json' ]] || { + die 'This does not appear to be a node project' + } + + [[ -z "$(json -f package.json 'scripts.lint')" ]] && { + die 'There is no lint script in the package.json' + } + + [[ -z "$(json -f package.json 'scripts.test')" ]] && { + die 'There is no test script in the package.json' + } + + return 0 +} + +traverse_history() { + while [[ "${ROOT}" != "$(current_commit_sha)" ]] ; do + run_checks + step_back_one_commit + done +} + +# +# Main +# +ensure_command git +ensure_command tail +ensure_command npm +ensure_command json +check_project +remember_git_start_and_end +log_commit "HEAD: $(current_commit_sha)" +traverse_history +run_checks +log_commit "ROOT: $(current_commit_sha)" +cleanup + +# vim: syntax=sh et ts=2 sts=2 sw=2 diff --git a/circle.yml b/circle.yml new file mode 100644 index 00000000..23bc9980 --- /dev/null +++ b/circle.yml @@ -0,0 +1,20 @@ +## Customize the test machine +machine: + + timezone: + Europe/London + +test: + override: + - make test + +## Customize deployment commands +# deployment: +# staging: +# branch: master + +## Custom notifications +# notify: +# webhooks: +# # A list of hashes representing hooks. Only the url field is supported. +# - url: https://someurl.com/hooks/circle diff --git a/cloudapi-graphql/Makefile b/cloudapi-graphql/Makefile new file mode 100644 index 00000000..ac24a173 --- /dev/null +++ b/cloudapi-graphql/Makefile @@ -0,0 +1,3 @@ +.PHONY: test +test: + ./node_modules/.bin/ava diff --git a/cloudapi-graphql/package.json b/cloudapi-graphql/package.json index bfd8c33e..a2407902 100644 --- a/cloudapi-graphql/package.json +++ b/cloudapi-graphql/package.json @@ -19,6 +19,7 @@ "user-home": "^2.0.0" }, "devDependencies": { + "ava": "^0.16.0", "eslint": "3.7.0", "eslint-config-semistandard": "^7.0.0", "eslint-config-standard": "^6.2.0", diff --git a/cloudapi-graphql/test/noop.test.js b/cloudapi-graphql/test/noop.test.js new file mode 100644 index 00000000..d614ca9d --- /dev/null +++ b/cloudapi-graphql/test/noop.test.js @@ -0,0 +1,13 @@ +// TODO: REMOVE, JUST FOR AN EXAMPLE + +const test = require('ava'); + +test('foo', t => { + t.pass(); +}); + +test('bar', t => { + const bar = Promise.resolve('bar'); + + bar.then(() => t.is(bar, 'bar')); +});