Просмотр исходного кода

Add support for publishing Hex releases and docs

Loïc Hoguin 4 лет назад
Родитель
Сommit
4fdc52c60c
5 измененных файлов с 799 добавлено и 1 удалено
  1. 4 0
      CHANGELOG.asciidoc
  2. 1 0
      build.config
  3. 336 0
      plugins/hex.mk
  4. 1 1
      test/Makefile
  5. 457 0
      test/plugin_hex.mk

+ 4 - 0
CHANGELOG.asciidoc

@@ -23,3 +23,7 @@
 2020/06/18: Concuerror integration has been added. It is
             currently minimal but usable. Experimentation
             and feedback is welcome.
+
+2020/11/30: Support for publishing Hex releases and docs
+            has been added. It is currently experimental.
+            Feedback is more than welcome.

+ 1 - 0
build.config

@@ -29,6 +29,7 @@ plugins/edoc
 plugins/erlydtl
 plugins/escript
 plugins/eunit
+plugins/hex
 plugins/proper
 plugins/protobuffs
 plugins/relx

+ 336 - 0
plugins/hex.mk

@@ -0,0 +1,336 @@
+# Copyright (c) 2020, Loïc Hoguin <essen@ninenines.eu>
+# This file is part of erlang.mk and subject to the terms of the ISC License.
+
+HEX_CORE_GIT ?= https://github.com/hexpm/hex_core
+HEX_CORE_COMMIT ?= v0.7.0
+
+PACKAGES += hex_core
+pkg_hex_core_name = hex_core
+pkg_hex_core_description = Reference implementation of Hex specifications
+pkg_hex_core_homepage = $(HEX_CORE_GIT)
+pkg_hex_core_fetch = git
+pkg_hex_core_repo = $(HEX_CORE_GIT)
+pkg_hex_core_commit = $(HEX_CORE_COMMIT)
+
+# We automatically depend on hex_core when the project isn't already.
+$(if $(filter hex_core,$(DEPS) $(BUILD_DEPS) $(DOC_DEPS) $(REL_DEPS) $(TEST_DEPS)),,\
+	$(eval $(call dep_target,hex_core)))
+
+hex-core: $(DEPS_DIR)/hex_core
+	$(verbose) if [ ! -e $(DEPS_DIR)/hex_core/ebin/dep_built ]; then \
+		$(MAKE) -C $(DEPS_DIR)/hex_core IS_DEP=1; \
+		touch $(DEPS_DIR)/hex_core/ebin/dep_built; \
+	fi
+
+# @todo This must also apply to fetching.
+HEX_CONFIG ?=
+
+define hex_config.erl
+	begin
+		Config0 = hex_core:default_config(),
+		Config0$(HEX_CONFIG)
+	end
+endef
+
+define hex_user_create.erl
+	{ok, _} = application:ensure_all_started(ssl),
+	{ok, _} = application:ensure_all_started(inets),
+	Config = $(hex_config.erl),
+	case hex_api_user:create(Config, <<"$(strip $1)">>, <<"$(strip $2)">>, <<"$(strip $3)">>) of
+		{ok, {201, _, #{<<"email">> := Email, <<"url">> := URL, <<"username">> := Username}}} ->
+			io:format("User ~s (~s) created at ~s~n"
+				"Please check your inbox for a confirmation email.~n"
+				"You must confirm before you are allowed to publish packages.~n",
+				[Username, Email, URL]),
+			halt(0);
+		{ok, {Status, _, Errors}} ->
+			io:format("Error ~b: ~0p~n", [Status, Errors]),
+			halt(80)
+	end
+endef
+
+# The $(info ) call inserts a new line after the password prompt.
+hex-user-create: hex-core
+	$(if $(HEX_USERNAME),,$(eval HEX_USERNAME := $(shell read -p "Username: " username; echo $$username)))
+	$(if $(HEX_PASSWORD),,$(eval HEX_PASSWORD := $(shell stty -echo; read -p "Password: " password; stty echo; echo $$password) $(info )))
+	$(if $(HEX_EMAIL),,$(eval HEX_EMAIL := $(shell read -p "Email: " email; echo $$email)))
+	$(gen_verbose) $(call erlang,$(call hex_user_create.erl,$(HEX_USERNAME),$(HEX_PASSWORD),$(HEX_EMAIL)))
+
+define hex_key_add.erl
+	{ok, _} = application:ensure_all_started(ssl),
+	{ok, _} = application:ensure_all_started(inets),
+	Config = $(hex_config.erl),
+	ConfigF = Config#{api_key => iolist_to_binary([<<"Basic ">>, base64:encode(<<"$(strip $1):$(strip $2)">>)])},
+	Permissions = [
+		case string:split(P, <<":">>) of
+			[D] -> #{domain => D};
+			[D, R] -> #{domain => D, resource => R}
+		end
+	|| P <- string:split(<<"$(strip $4)">>, <<",">>, all)],
+	case hex_api_key:add(ConfigF, <<"$(strip $3)">>, Permissions) of
+		{ok, {201, _, #{<<"secret">> := Secret}}} ->
+			io:format("Key ~s created for user ~s~nSecret: ~s~n"
+				"Please store the secret in a secure location, such as a password store.~n"
+				"The secret will be requested for most Hex-related operations.~n",
+				[<<"$(strip $3)">>, <<"$(strip $1)">>, Secret]),
+			halt(0);
+		{ok, {Status, _, Errors}} ->
+			io:format("Error ~b: ~0p~n", [Status, Errors]),
+			halt(81)
+	end
+endef
+
+hex-key-add: hex-core
+	$(if $(HEX_USERNAME),,$(eval HEX_USERNAME := $(shell read -p "Username: " username; echo $$username)))
+	$(if $(HEX_PASSWORD),,$(eval HEX_PASSWORD := $(shell stty -echo; read -p "Password: " password; stty echo; echo $$password) $(info )))
+	$(gen_verbose) $(call erlang,$(call hex_key_add.erl,$(HEX_USERNAME),$(HEX_PASSWORD),\
+		$(if $(name),$(name),$(shell hostname)-erlang-mk),\
+		$(if $(perm),$(perm),api)))
+
+HEX_TARBALL_EXTRA_METADATA ?=
+
+# @todo Check that we can += files
+HEX_TARBALL_FILES ?= \
+	$(wildcard early-plugins.mk) \
+	$(wildcard ebin/$(PROJECT).app) \
+	$(wildcard ebin/$(PROJECT).appup) \
+	$(wildcard $(notdir $(ERLANG_MK_FILENAME))) \
+	$(sort $(call core_find,include/,*.hrl)) \
+	$(wildcard LICENSE*) \
+	$(wildcard Makefile) \
+	$(wildcard plugins.mk) \
+	$(sort $(call core_find,priv/,*)) \
+	$(wildcard README*) \
+	$(wildcard rebar.config) \
+	$(sort $(call core_find,src/,*))
+
+HEX_TARBALL_OUTPUT_FILE ?= $(ERLANG_MK_TMP)/$(PROJECT).tar
+
+# @todo Need to check for rebar.config and/or the absence of DEPS to know
+# whether a project will work with Rebar.
+#
+# @todo contributors licenses links in HEX_TARBALL_EXTRA_METADATA
+
+# In order to build the requirements metadata we look into DEPS.
+# We do not require that the project use Hex dependencies, however
+# Hex.pm does require that the package name and version numbers
+# correspond to a real Hex package.
+define hex_tarball_create.erl
+	Files0 = [$(call comma_list,$(patsubst %,"%",$(HEX_TARBALL_FILES)))],
+	Requirements0 = #{
+		$(foreach d,$(DEPS),
+			<<"$(if $(subst hex,,$(call query_fetch_method,$d)),$d,$(if $(word 3,$(dep_$d)),$(word 3,$(dep_$d)),$d))">> => #{
+				<<"app">> => <<"$d">>,
+				<<"optional">> => false,
+				<<"requirement">> => <<"$(call query_version,$d)">>
+			},)
+		$(if $(DEPS),dummy => dummy)
+	},
+	Requirements = maps:remove(dummy, Requirements0),
+	Metadata0 = #{
+		app => <<"$(strip $(PROJECT))">>,
+		build_tools => [<<"make">>, <<"rebar3">>],
+		description => <<"$(strip $(PROJECT_DESCRIPTION))">>,
+		files => [unicode:characters_to_binary(F) || F <- Files0],
+		name => <<"$(strip $(PROJECT))">>,
+		requirements => Requirements,
+		version => <<"$(strip $(PROJECT_VERSION))">>
+	},
+	Metadata = Metadata0$(HEX_TARBALL_EXTRA_METADATA),
+	Files = [case file:read_file(F) of
+		{ok, Bin} ->
+			{F, Bin};
+		{error, Reason} ->
+			io:format("Error trying to open file ~0p: ~0p~n", [F, Reason]),
+			halt(82)
+	end || F <- Files0],
+	case hex_tarball:create(Metadata, Files) of
+		{ok, #{tarball := Tarball}} ->
+			ok = file:write_file("$(strip $(HEX_TARBALL_OUTPUT_FILE))", Tarball),
+			halt(0);
+		{error, Reason} ->
+			io:format("Error ~0p~n", [Reason]),
+			halt(83)
+	end
+endef
+
+hex_tar_verbose_0 = @echo " TAR    $(notdir $(ERLANG_MK_TMP))/$(@F)";
+hex_tar_verbose_2 = set -x;
+hex_tar_verbose = $(hex_tar_verbose_$(V))
+
+$(HEX_TARBALL_OUTPUT_FILE): hex-core app
+	$(hex_tar_verbose) $(call erlang,$(call hex_tarball_create.erl))
+
+hex-tarball-create: $(HEX_TARBALL_OUTPUT_FILE)
+
+define hex_release_publish_summary.erl
+	{ok, Tarball} = erl_tar:open("$(strip $(HEX_TARBALL_OUTPUT_FILE))", [read]),
+	ok = erl_tar:extract(Tarball, [{cwd, "$(ERLANG_MK_TMP)"}, {files, ["metadata.config"]}]),
+	{ok, Metadata} = file:consult("$(ERLANG_MK_TMP)/metadata.config"),
+	#{
+		<<"name">> := Name,
+		<<"version">> := Version,
+		<<"files">> := Files,
+		<<"requirements">> := Deps
+	} = maps:from_list(Metadata),
+	io:format("Publishing ~s ~s~n  Dependencies:~n", [Name, Version]),
+	case Deps of
+		[] ->
+			io:format("    (none)~n");
+		_ ->
+			[begin
+				#{<<"app">> := DA, <<"requirement">> := DR} = maps:from_list(D),
+				io:format("    ~s ~s~n", [DA, DR])
+			end || {_, D} <- Deps]
+	end,
+	io:format("  Included files:~n"),
+	[io:format("    ~s~n", [F]) || F <- Files],
+	io:format("You may also review the contents of the tarball file.~n"
+		"Please enter your secret key to proceed.~n"),
+	halt(0)
+endef
+
+define hex_release_publish.erl
+	{ok, _} = application:ensure_all_started(ssl),
+	{ok, _} = application:ensure_all_started(inets),
+	Config = $(hex_config.erl),
+	ConfigF = Config#{api_key => <<"$(strip $1)">>},
+	{ok, Tarball} = file:read_file("$(strip $(HEX_TARBALL_OUTPUT_FILE))"),
+	case hex_api_release:publish(ConfigF, Tarball, [{replace, $2}]) of
+		{ok, {200, _, #{}}} ->
+			io:format("Release replaced~n"),
+			halt(0);
+		{ok, {201, _, #{}}} ->
+			io:format("Release published~n"),
+			halt(0);
+		{ok, {Status, _, Errors}} ->
+			io:format("Error ~b: ~0p~n", [Status, Errors]),
+			halt(84)
+	end
+endef
+
+hex-release-tarball: hex-core $(HEX_TARBALL_OUTPUT_FILE)
+	$(verbose) $(call erlang,$(call hex_release_publish_summary.erl))
+
+hex-release-publish: hex-core hex-release-tarball
+	$(if $(HEX_SECRET),,$(eval HEX_SECRET := $(shell stty -echo; read -p "Secret: " secret; stty echo; echo $$secret) $(info )))
+	$(gen_verbose) $(call erlang,$(call hex_release_publish.erl,$(HEX_SECRET),false))
+
+hex-release-replace: hex-core hex-release-tarball
+	$(if $(HEX_SECRET),,$(eval HEX_SECRET := $(shell stty -echo; read -p "Secret: " secret; stty echo; echo $$secret) $(info )))
+	$(gen_verbose) $(call erlang,$(call hex_release_publish.erl,$(HEX_SECRET),true))
+
+define hex_release_delete.erl
+	{ok, _} = application:ensure_all_started(ssl),
+	{ok, _} = application:ensure_all_started(inets),
+	Config = $(hex_config.erl),
+	ConfigF = Config#{api_key => <<"$(strip $1)">>},
+	case hex_api_release:delete(ConfigF, <<"$(strip $(PROJECT))">>, <<"$(strip $(PROJECT_VERSION))">>) of
+		{ok, {204, _, _}} ->
+			io:format("Release $(strip $(PROJECT_VERSION)) deleted~n"),
+			halt(0);
+		{ok, {Status, _, Errors}} ->
+			io:format("Error ~b: ~0p~n", [Status, Errors]),
+			halt(85)
+	end
+endef
+
+hex-release-delete: hex-core
+	$(if $(HEX_SECRET),,$(eval HEX_SECRET := $(shell stty -echo; read -p "Secret: " secret; stty echo; echo $$secret) $(info )))
+	$(gen_verbose) $(call erlang,$(call hex_release_delete.erl,$(HEX_SECRET)))
+
+define hex_release_retire.erl
+	{ok, _} = application:ensure_all_started(ssl),
+	{ok, _} = application:ensure_all_started(inets),
+	Config = $(hex_config.erl),
+	ConfigF = Config#{api_key => <<"$(strip $1)">>},
+	Params = #{<<"reason">> => <<"$(strip $3)">>, <<"message">> => <<"$(strip $4)">>},
+	case hex_api_release:retire(ConfigF, <<"$(strip $(PROJECT))">>, <<"$(strip $2)">>, Params) of
+		{ok, {204, _, _}} ->
+			io:format("Release $(strip $2) has been retired~n"),
+			halt(0);
+		{ok, {Status, _, Errors}} ->
+			io:format("Error ~b: ~0p~n", [Status, Errors]),
+			halt(86)
+	end
+endef
+
+hex-release-retire: hex-core
+	$(if $(HEX_SECRET),,$(eval HEX_SECRET := $(shell stty -echo; read -p "Secret: " secret; stty echo; echo $$secret) $(info )))
+	$(gen_verbose) $(call erlang,$(call hex_release_retire.erl,$(HEX_SECRET),\
+		$(if $(HEX_VERSION),$(HEX_VERSION),$(PROJECT_VERSION)),\
+		$(if $(HEX_REASON),$(HEX_REASON),invalid),\
+		$(HEX_MESSAGE)))
+
+define hex_release_unretire.erl
+	{ok, _} = application:ensure_all_started(ssl),
+	{ok, _} = application:ensure_all_started(inets),
+	Config = $(hex_config.erl),
+	ConfigF = Config#{api_key => <<"$(strip $1)">>},
+	case hex_api_release:unretire(ConfigF, <<"$(strip $(PROJECT))">>, <<"$(strip $2)">>) of
+		{ok, {204, _, _}} ->
+			io:format("Release $(strip $2) is not retired anymore~n"),
+			halt(0);
+		{ok, {Status, _, Errors}} ->
+			io:format("Error ~b: ~0p~n", [Status, Errors]),
+			halt(87)
+	end
+endef
+
+hex-release-unretire: hex-core
+	$(if $(HEX_SECRET),,$(eval HEX_SECRET := $(shell stty -echo; read -p "Secret: " secret; stty echo; echo $$secret) $(info )))
+	$(gen_verbose) $(call erlang,$(call hex_release_unretire.erl,$(HEX_SECRET),\
+		$(if $(HEX_VERSION),$(HEX_VERSION),$(PROJECT_VERSION))))
+
+HEX_DOCS_DOC_DIR ?= doc/
+HEX_DOCS_TARBALL_FILES ?= $(sort $(call core_find,$(HEX_DOCS_DOC_DIR),*))
+HEX_DOCS_TARBALL_OUTPUT_FILE ?= $(ERLANG_MK_TMP)/$(PROJECT)-docs.tar.gz
+
+$(HEX_DOCS_TARBALL_OUTPUT_FILE): hex-core app docs
+	$(hex_tar_verbose) tar czf $(HEX_DOCS_TARBALL_OUTPUT_FILE) -C $(HEX_DOCS_DOC_DIR) \
+		$(HEX_DOCS_TARBALL_FILES:$(HEX_DOCS_DOC_DIR)%=%)
+
+hex-docs-tarball-create: $(HEX_DOCS_TARBALL_OUTPUT_FILE)
+
+define hex_docs_publish.erl
+	{ok, _} = application:ensure_all_started(ssl),
+	{ok, _} = application:ensure_all_started(inets),
+	Config = $(hex_config.erl),
+	ConfigF = Config#{api_key => <<"$(strip $1)">>},
+	{ok, Tarball} = file:read_file("$(strip $(HEX_DOCS_TARBALL_OUTPUT_FILE))"),
+	case hex_api:post(ConfigF,
+			["packages", "$(strip $(PROJECT))", "releases", "$(strip $(PROJECT_VERSION))", "docs"],
+			{"application/octet-stream", Tarball}) of
+		{ok, {Status, _, _}} when Status >= 200, Status < 300 ->
+			io:format("Docs published~n"),
+			halt(0);
+		{ok, {Status, _, Errors}} ->
+			io:format("Error ~b: ~0p~n", [Status, Errors]),
+			halt(88)
+	end
+endef
+
+hex-docs-publish: hex-core hex-docs-tarball-create
+	$(if $(HEX_SECRET),,$(eval HEX_SECRET := $(shell stty -echo; read -p "Secret: " secret; stty echo; echo $$secret) $(info )))
+	$(gen_verbose) $(call erlang,$(call hex_docs_publish.erl,$(HEX_SECRET)))
+
+define hex_docs_delete.erl
+	{ok, _} = application:ensure_all_started(ssl),
+	{ok, _} = application:ensure_all_started(inets),
+	Config = $(hex_config.erl),
+	ConfigF = Config#{api_key => <<"$(strip $1)">>},
+	case hex_api:delete(ConfigF,
+			["packages", "$(strip $(PROJECT))", "releases", "$(strip $2)", "docs"]) of
+		{ok, {Status, _, _}} when Status >= 200, Status < 300 ->
+			io:format("Docs removed~n"),
+			halt(0);
+		{ok, {Status, _, Errors}} ->
+			io:format("Error ~b: ~0p~n", [Status, Errors]),
+			halt(89)
+	end
+endef
+
+hex-docs-delete: hex-core
+	$(if $(HEX_SECRET),,$(eval HEX_SECRET := $(shell stty -echo; read -p "Secret: " secret; stty echo; echo $$secret) $(info )))
+	$(gen_verbose) $(call erlang,$(call hex_docs_delete.erl,$(HEX_SECRET),\
+		$(if $(HEX_VERSION),$(HEX_VERSION),$(PROJECT_VERSION))))

+ 1 - 1
test/Makefile

@@ -124,7 +124,7 @@ endef
 all:: core
 
 clean::
-	$t rm -rf erl_crash.dump packages/ test_*/
+	$t rm -rf erl_crash.dump packages/ $(filter-out test_rebar_git/,$(wildcard test_*/))
 
 init: clean
 	$i "Prefetch Rebar if necessary"

+ 457 - 0
test/plugin_hex.mk

@@ -0,0 +1,457 @@
+# Hex plugin.
+#
+# In order to run these tests, a local Hex.pm instance must
+# be started and available on port 4000, and the HEX=1 variable
+# must be set.
+
+HEX_TARGETS = $(call list_targets,hex)
+
+.PHONY: hex $(HEX_TARGETS)
+
+hex: $(HEX_TARGETS)
+
+ifeq ($(shell netcat -z localhost 4000 && echo ok),ok)
+hex-user-create: init
+
+	$i "Bootstrap a new OTP application named $(APP)"
+	$t mkdir $(APP)/
+	$t cp ../erlang.mk $(APP)/
+	$t $(MAKE) -C $(APP) -f erlang.mk bootstrap $v
+
+	$i "Configure a local Hex provider"
+	$t perl -ni.bak -e 'print;if ($$.==1) {print "define HEX_CONFIG\n#{api_url => <<\"http://localhost:4000/api\">>}\nendef\n"}' $(APP)/Makefile
+
+	$i "Create a Hex user"
+	$t $(MAKE) -C $(APP) hex-user-create HEX_USERNAME=$(APP) HEX_PASSWORD="1234567" HEX_EMAIL=$(APP)@noone.nope $v
+
+	$i "Check that the user exists"
+	$t curl -sf http://localhost:4000/api/users/$(APP) >/dev/null
+
+# @todo Fix this.
+#hex-user-create-password-with-dollar-sign: init
+#
+#	$i "Bootstrap a new OTP application named $(APP)"
+#	$t mkdir $(APP)/
+#	$t cp ../erlang.mk $(APP)/
+#	$t $(MAKE) -C $(APP) -f erlang.mk bootstrap $v
+#
+#	$i "Configure a local Hex provider"
+#	$t perl -ni.bak -e 'print;if ($$.==1) {print "define HEX_CONFIG\n#{api_url => <<\"http://localhost:4000/api\">>}\nendef\n"}' $(APP)/Makefile
+#
+#	$i "Create a Hex user"
+#	$t $(MAKE) -C $(APP) hex-user-create HEX_USERNAME=$(APP) HEX_PASSWORD="123$$567" HEX_EMAIL=$(APP)@noone.nope $v
+#
+#	$i "Check that the user exists"
+#	$t curl --user "$(APP):123$$567" -sf http://localhost:4000/api/users/$(APP) >/dev/null
+
+# @todo Fix this.
+#hex-user-create-password-with-backslash: init
+#
+#	$i "Bootstrap a new OTP application named $(APP)"
+#	$t mkdir $(APP)/
+#	$t cp ../erlang.mk $(APP)/
+#	$t $(MAKE) -C $(APP) -f erlang.mk bootstrap $v
+#
+#	$i "Configure a local Hex provider"
+#	$t perl -ni.bak -e 'print;if ($$.==1) {print "define HEX_CONFIG\n#{api_url => <<\"http://localhost:4000/api\">>}\nendef\n"}' $(APP)/Makefile
+#
+#	$i "Create a Hex user"
+#	$t $(MAKE) -C $(APP) hex-user-create HEX_USERNAME=$(APP) HEX_PASSWORD="123\\567" HEX_EMAIL=$(APP)@noone.nope $v
+#
+#	$i "Check that the user exists"
+#	$t curl --user "$(APP):123\\567" -sf http://localhost:4000/api/users/$(APP) >/dev/null
+
+hex-user-create-password-with-space: init
+
+	$i "Bootstrap a new OTP application named $(APP)"
+	$t mkdir $(APP)/
+	$t cp ../erlang.mk $(APP)/
+	$t $(MAKE) -C $(APP) -f erlang.mk bootstrap $v
+
+	$i "Configure a local Hex provider"
+	$t perl -ni.bak -e 'print;if ($$.==1) {print "define HEX_CONFIG\n#{api_url => <<\"http://localhost:4000/api\">>}\nendef\n"}' $(APP)/Makefile
+
+	$i "Create a Hex user"
+	$t $(MAKE) -C $(APP) hex-user-create HEX_USERNAME=$(APP) HEX_PASSWORD="123 567" HEX_EMAIL=$(APP)@noone.nope $v
+
+	$i "Check that the user exists"
+	$t curl --user "$(APP):123 567" -sf http://localhost:4000/api/users/$(APP) >/dev/null
+
+hex-key-add: init
+
+	$i "Bootstrap a new OTP application named $(APP)"
+	$t mkdir $(APP)/
+	$t cp ../erlang.mk $(APP)/
+	$t $(MAKE) -C $(APP) -f erlang.mk bootstrap $v
+
+	$i "Configure a local Hex provider"
+	$t perl -ni.bak -e 'print;if ($$.==1) {print "define HEX_CONFIG\n#{api_url => <<\"http://localhost:4000/api\">>}\nendef\n"}' $(APP)/Makefile
+
+	$i "Create a Hex user"
+	$t $(MAKE) -C $(APP) hex-user-create HEX_USERNAME=$(APP) HEX_PASSWORD="1234567" HEX_EMAIL=$(APP)@noone.nope $v
+
+	$i "Create a key for that user"
+	$t $(MAKE) -C $(APP) hex-key-add HEX_USERNAME=$(APP) HEX_PASSWORD="1234567" $v
+
+	$i "Check that the key exists"
+	$t curl --user $(APP):1234567 -sf http://localhost:4000/api/keys/$(shell hostname)-erlang-mk >/dev/null
+
+hex-tarball-create: init
+
+	$i "Bootstrap a new OTP application named $(APP)"
+	$t mkdir $(APP)/
+	$t cp ../erlang.mk $(APP)/
+	$t $(MAKE) -C $(APP) -f erlang.mk bootstrap $v
+
+	$i "Create a release tarball"
+	$t $(MAKE) -C $(APP) hex-tarball-create $v
+
+	$i "Confirm the tarball contents can be extracted"
+	$t cd $(APP)/.erlang.mk/ && tar xf $(APP).tar
+
+	$i "Confirm the tarball contains a CHECKSUM file"
+	$t test -f $(APP)/.erlang.mk/CHECKSUM
+
+	$i "Confirm the tarball contains a VERSION file containing '3'"
+	$t cat $(APP)/.erlang.mk/VERSION | grep -q ^3$$
+
+	$i "Confirm the tarball contains a valid metadata.config file"
+	$t $(ERL) -eval " \
+		{ok, _} = file:consult(\"$(APP)/.erlang.mk/metadata.config\"), \
+		halt(0)"
+
+	$i "Confirm the tarball contains a contents.tar.gz file that can be extracted"
+	$t cd $(APP)/.erlang.mk/ && tar xf contents.tar.gz
+
+	$i "Confirm contents.tar.gz contains the expected files"
+	$t printf "%s\n" \
+		erlang.mk \
+		Makefile \
+		ebin/$(APP).app \
+		src/$(APP)_app.erl \
+		src/$(APP)_sup.erl | sort > $(APP)/.erlang.mk/EXPECT
+	$t cd $(APP)/.erlang.mk/ && tar tf contents.tar.gz | sort | diff EXPECT -
+
+hex-tarball-create-with-deps: init
+
+	$i "Bootstrap a new OTP application named $(APP)"
+	$t mkdir $(APP)/
+	$t cp ../erlang.mk $(APP)/
+	$t $(MAKE) -C $(APP) -f erlang.mk bootstrap $v
+
+	$i "Add Cowlib to the list of dependencies"
+	$t perl -ni.bak -e 'print;if ($$.==1) {print "DEPS = cowlib\ndep_cowlib_commit = 2.10.1\n"}' $(APP)/Makefile
+
+ifdef LEGACY
+	$i "Add Cowlib to the applications key in the .app.src file"
+	$t perl -ni.bak -e 'print;if ($$.==7) {print "\t\tcowlib,\n"}' $(APP)/src/$(APP).app.src
+endif
+
+	$i "Create a release tarball"
+	$t $(MAKE) -C $(APP) hex-tarball-create $v
+
+	$i "Confirm the tarball contents can be extracted"
+	$t cd $(APP)/.erlang.mk/ && tar xf $(APP).tar
+
+	$i "Confirm the tarball contains a metadata.config file that lists Cowlib as requirement"
+	$t $(ERL) -eval " \
+		{ok, Metadata} = file:consult(\"$(APP)/.erlang.mk/metadata.config\"), \
+		{_, [{<<\"cowlib\">>, Cowlib}]} = lists:keyfind(<<\"requirements\">>, 1, Metadata), \
+		{_, <<\"cowlib\">>} = lists:keyfind(<<\"app\">>, 1, Cowlib), \
+		{_, false} = lists:keyfind(<<\"optional\">>, 1, Cowlib), \
+		{_, <<\"2.10.1\">>} = lists:keyfind(<<\"requirement\">>, 1, Cowlib), \
+		halt(0)"
+
+hex-release-publish: init
+
+	$i "Bootstrap a new OTP application named $(APP)"
+	$t mkdir $(APP)/
+	$t cp ../erlang.mk $(APP)/
+	$t $(MAKE) -C $(APP) -f erlang.mk bootstrap $v
+
+	$i "Configure a local Hex provider"
+	$t perl -ni.bak -e 'print;if ($$.==1) {print "define HEX_CONFIG\n#{api_url => <<\"http://localhost:4000/api\">>}\nendef\n"}' $(APP)/Makefile
+
+	$i "Add extra Hex metadata"
+	$t perl -ni.bak -e 'print;if ($$.==1) {print "define HEX_TARBALL_EXTRA_METADATA\n#{licenses => [<<\"ISC\">>]}\nendef\n"}' $(APP)/Makefile
+
+	$i "Create a Hex user"
+	$t $(MAKE) -C $(APP) hex-user-create HEX_USERNAME=$(APP) HEX_PASSWORD="1234567" HEX_EMAIL=$(APP)@noone.nope $v
+
+	$i "Create a key for that user"
+	$t $(MAKE) -C $(APP) hex-key-add HEX_USERNAME=$(APP) HEX_PASSWORD="1234567" | grep ^Secret: | cut -f2 -d" " > $(APP)/hex.key
+
+	$i "Publish the release"
+	$t $(MAKE) -C $(APP) hex-release-publish HEX_SECRET=`cat $(APP)/hex.key` $v
+
+	$i "Check that the release exists"
+	$t curl -sf http://localhost:4000/api/packages/$(APP)/releases/0.1.0 >/dev/null
+
+hex-release-publish-with-deps: init
+
+	$i "Bootstrap a new OTP application named $(APP)"
+	$t mkdir $(APP)/
+	$t cp ../erlang.mk $(APP)/
+	$t $(MAKE) -C $(APP) -f erlang.mk bootstrap $v
+
+	$i "Add Cowlib to the list of dependencies"
+	$t perl -ni.bak -e 'print;if ($$.==1) {print "DEPS = cowlib\ndep_cowlib_commit = 2.10.1\n"}' $(APP)/Makefile
+
+ifdef LEGACY
+	$i "Add Cowlib to the applications key in the .app.src file"
+	$t perl -ni.bak -e 'print;if ($$.==7) {print "\t\tcowlib,\n"}' $(APP)/src/$(APP).app.src
+endif
+
+	$i "Build the application"
+	$t $(MAKE) -C $(APP) $v
+
+	$i "Configure a local Hex provider for Cowlib"
+	$t perl -ni.bak -e 'print;if ($$.==1) {print "define HEX_CONFIG\n#{api_url => <<\"http://localhost:4000/api\">>}\nendef\n"}' $(APP)/deps/cowlib/Makefile
+
+	$i "Configure a local Hex provider"
+	$t perl -ni.bak -e 'print;if ($$.==1) {print "define HEX_CONFIG\n#{api_url => <<\"http://localhost:4000/api\">>}\nendef\n"}' $(APP)/Makefile
+
+	$i "Add extra Hex metadata"
+	$t perl -ni.bak -e 'print;if ($$.==1) {print "define HEX_TARBALL_EXTRA_METADATA\n#{licenses => [<<\"ISC\">>]}\nendef\n"}' $(APP)/Makefile
+
+	$i "Create a Hex user"
+	$t $(MAKE) -C $(APP) hex-user-create HEX_USERNAME=$(APP) HEX_PASSWORD="1234567" HEX_EMAIL=$(APP)@noone.nope $v
+
+	$i "Create a key for that user"
+	$t $(MAKE) -C $(APP) hex-key-add HEX_USERNAME=$(APP) HEX_PASSWORD="1234567" | grep ^Secret: | cut -f2 -d" " > $(APP)/hex.key
+
+	$i "Publish the Cowlib release"
+	$t $(MAKE) -C $(APP)/deps/cowlib hex-release-publish HEX_SECRET=`cat $(APP)/hex.key` $v
+
+	$i "Publish the release"
+	$t $(MAKE) -C $(APP) hex-release-publish HEX_SECRET=`cat $(APP)/hex.key` $v
+
+	$i "Check that the release exists and includes Cowlib as requirement"
+	$t curl -sf http://localhost:4000/api/packages/$(APP)/releases/0.1.0 | grep -q cowlib
+
+hex-release-replace: init
+
+	$i "Bootstrap a new OTP application named $(APP)"
+	$t mkdir $(APP)/
+	$t cp ../erlang.mk $(APP)/
+	$t $(MAKE) -C $(APP) -f erlang.mk bootstrap $v
+
+	$i "Configure a local Hex provider"
+	$t perl -ni.bak -e 'print;if ($$.==1) {print "define HEX_CONFIG\n#{api_url => <<\"http://localhost:4000/api\">>}\nendef\n"}' $(APP)/Makefile
+
+	$i "Add extra Hex metadata"
+	$t perl -ni.bak -e 'print;if ($$.==1) {print "define HEX_TARBALL_EXTRA_METADATA\n#{licenses => [<<\"ISC\">>]}\nendef\n"}' $(APP)/Makefile
+
+	$i "Create a Hex user"
+	$t $(MAKE) -C $(APP) hex-user-create HEX_USERNAME=$(APP) HEX_PASSWORD="1234567" HEX_EMAIL=$(APP)@noone.nope $v
+
+	$i "Create a key for that user"
+	$t $(MAKE) -C $(APP) hex-key-add HEX_USERNAME=$(APP) HEX_PASSWORD="1234567" | grep ^Secret: | cut -f2 -d" " > $(APP)/hex.key
+
+	$i "Publish the release"
+	$t $(MAKE) -C $(APP) hex-release-publish HEX_SECRET=`cat $(APP)/hex.key` $v
+
+	$i "Update the project description"
+	$t sed -i.bak s/"PROJECT_DESCRIPTION = New project"/"PROJECT_DESCRIPTION = Best project"/ $(APP)/Makefile
+
+	$i "Publish the release again, replacing the existing one"
+	$t $(MAKE) -C $(APP) hex-release-replace HEX_SECRET=`cat $(APP)/hex.key` $v
+
+	$i "Check that the release was replaced"
+	$t curl -sf http://localhost:4000/api/packages/$(APP) | grep -q "Best project"
+
+hex-release-delete: init
+
+	$i "Bootstrap a new OTP application named $(APP)"
+	$t mkdir $(APP)/
+	$t cp ../erlang.mk $(APP)/
+	$t $(MAKE) -C $(APP) -f erlang.mk bootstrap $v
+
+	$i "Configure a local Hex provider"
+	$t perl -ni.bak -e 'print;if ($$.==1) {print "define HEX_CONFIG\n#{api_url => <<\"http://localhost:4000/api\">>}\nendef\n"}' $(APP)/Makefile
+
+	$i "Add extra Hex metadata"
+	$t perl -ni.bak -e 'print;if ($$.==1) {print "define HEX_TARBALL_EXTRA_METADATA\n#{licenses => [<<\"ISC\">>]}\nendef\n"}' $(APP)/Makefile
+
+	$i "Create a Hex user"
+	$t $(MAKE) -C $(APP) hex-user-create HEX_USERNAME=$(APP) HEX_PASSWORD="1234567" HEX_EMAIL=$(APP)@noone.nope $v
+
+	$i "Create a key for that user"
+	$t $(MAKE) -C $(APP) hex-key-add HEX_USERNAME=$(APP) HEX_PASSWORD="1234567" | grep ^Secret: | cut -f2 -d" " > $(APP)/hex.key
+
+	$i "Publish the release"
+	$t $(MAKE) -C $(APP) hex-release-publish HEX_SECRET=`cat $(APP)/hex.key` $v
+
+	$i "Delete the release"
+	$t $(MAKE) -C $(APP) hex-release-delete HEX_SECRET=`cat $(APP)/hex.key` $v
+
+	$i "Check that the release was deleted"
+	$t ! curl -sf http://localhost:4000/api/packages/$(APP)/releases/0.1.0 >/dev/null
+
+hex-release-retire: init
+
+	$i "Bootstrap a new OTP application named $(APP)"
+	$t mkdir $(APP)/
+	$t cp ../erlang.mk $(APP)/
+	$t $(MAKE) -C $(APP) -f erlang.mk bootstrap $v
+
+	$i "Configure a local Hex provider"
+	$t perl -ni.bak -e 'print;if ($$.==1) {print "define HEX_CONFIG\n#{api_url => <<\"http://localhost:4000/api\">>}\nendef\n"}' $(APP)/Makefile
+
+	$i "Add extra Hex metadata"
+	$t perl -ni.bak -e 'print;if ($$.==1) {print "define HEX_TARBALL_EXTRA_METADATA\n#{licenses => [<<\"ISC\">>]}\nendef\n"}' $(APP)/Makefile
+
+	$i "Create a Hex user"
+	$t $(MAKE) -C $(APP) hex-user-create HEX_USERNAME=$(APP) HEX_PASSWORD="1234567" HEX_EMAIL=$(APP)@noone.nope $v
+
+	$i "Create a key for that user"
+	$t $(MAKE) -C $(APP) hex-key-add HEX_USERNAME=$(APP) HEX_PASSWORD="1234567" | grep ^Secret: | cut -f2 -d" " > $(APP)/hex.key
+
+	$i "Publish the release"
+	$t $(MAKE) -C $(APP) hex-release-publish HEX_SECRET=`cat $(APP)/hex.key` $v
+
+	$i "Retire the release"
+	$t $(MAKE) -C $(APP) hex-release-retire HEX_SECRET=`cat $(APP)/hex.key` $v
+
+	$i "Check that the release was retired"
+	$t ! curl -sf http://localhost:4000/api/packages/$(APP)/releases/0.1.0 | grep -q \"retirement\":null
+
+hex-release-unretire: init
+
+	$i "Bootstrap a new OTP application named $(APP)"
+	$t mkdir $(APP)/
+	$t cp ../erlang.mk $(APP)/
+	$t $(MAKE) -C $(APP) -f erlang.mk bootstrap $v
+
+	$i "Configure a local Hex provider"
+	$t perl -ni.bak -e 'print;if ($$.==1) {print "define HEX_CONFIG\n#{api_url => <<\"http://localhost:4000/api\">>}\nendef\n"}' $(APP)/Makefile
+
+	$i "Add extra Hex metadata"
+	$t perl -ni.bak -e 'print;if ($$.==1) {print "define HEX_TARBALL_EXTRA_METADATA\n#{licenses => [<<\"ISC\">>]}\nendef\n"}' $(APP)/Makefile
+
+	$i "Create a Hex user"
+	$t $(MAKE) -C $(APP) hex-user-create HEX_USERNAME=$(APP) HEX_PASSWORD="1234567" HEX_EMAIL=$(APP)@noone.nope $v
+
+	$i "Create a key for that user"
+	$t $(MAKE) -C $(APP) hex-key-add HEX_USERNAME=$(APP) HEX_PASSWORD="1234567" | grep ^Secret: | cut -f2 -d" " > $(APP)/hex.key
+
+	$i "Publish the release"
+	$t $(MAKE) -C $(APP) hex-release-publish HEX_SECRET=`cat $(APP)/hex.key` $v
+
+	$i "Retire the release"
+	$t $(MAKE) -C $(APP) hex-release-retire HEX_SECRET=`cat $(APP)/hex.key` $v
+
+	$i "Unretire the release"
+	$t $(MAKE) -C $(APP) hex-release-unretire HEX_SECRET=`cat $(APP)/hex.key` $v
+
+	$i "Check that the release is no longer retired"
+	$t curl -sf http://localhost:4000/api/packages/$(APP)/releases/0.1.0 | grep -q \"retirement\":null
+
+hex-docs-tarball-create: init
+
+	$i "Bootstrap a new OTP application named $(APP)"
+	$t mkdir $(APP)/
+	$t cp ../erlang.mk $(APP)/
+	$t $(MAKE) -C $(APP) -f erlang.mk bootstrap $v
+
+	$i "Generate a doc/overview.edoc file"
+	$t mkdir $(APP)/doc
+	$t printf "%s\n" \
+		"@author R. J. Hacker <rjh@acme.com>" \
+		"@copyright 2007 R. J. Hacker" \
+		"@version 1.0.0" \
+		"@title Welcome to the 'frob' application!" \
+		"@doc 'frob' is a highly advanced frobnicator with low latency," > $(APP)/doc/overview.edoc
+
+	$i "Create a docs tarball"
+	$t $(MAKE) -C $(APP) hex-docs-tarball-create $v
+
+	$i "Confirm the tarball contents can be extracted"
+	$t cd $(APP)/.erlang.mk/ && tar xf $(APP)-docs.tar.gz
+
+	$i "Confirm the tarball contains the expected files"
+	$t printf "%s\n" \
+		edoc-info \
+		erlang.png \
+		index.html \
+		modules-frame.html \
+		overview-summary.html \
+		overview.edoc \
+		stylesheet.css \
+		$(APP)_app.html \
+		$(APP)_sup.html | sort > $(APP)/.erlang.mk/EXPECT
+	$t cd $(APP)/.erlang.mk/ && tar tf $(APP)-docs.tar.gz | sort | diff EXPECT -
+
+hex-docs-publish: init
+
+	$i "Bootstrap a new OTP application named $(APP)"
+	$t mkdir $(APP)/
+	$t cp ../erlang.mk $(APP)/
+	$t $(MAKE) -C $(APP) -f erlang.mk bootstrap $v
+
+	$i "Generate a doc/overview.edoc file"
+	$t mkdir $(APP)/doc
+	$t printf "%s\n" \
+		"@author R. J. Hacker <rjh@acme.com>" \
+		"@copyright 2007 R. J. Hacker" \
+		"@version 1.0.0" \
+		"@title Welcome to the 'frob' application!" \
+		"@doc 'frob' is a highly advanced frobnicator with low latency," > $(APP)/doc/overview.edoc
+
+	$i "Configure a local Hex provider"
+	$t perl -ni.bak -e 'print;if ($$.==1) {print "define HEX_CONFIG\n#{api_url => <<\"http://localhost:4000/api\">>}\nendef\n"}' $(APP)/Makefile
+
+	$i "Add extra Hex metadata"
+	$t perl -ni.bak -e 'print;if ($$.==1) {print "define HEX_TARBALL_EXTRA_METADATA\n#{licenses => [<<\"ISC\">>]}\nendef\n"}' $(APP)/Makefile
+
+	$i "Create a Hex user"
+	$t $(MAKE) -C $(APP) hex-user-create HEX_USERNAME=$(APP) HEX_PASSWORD="1234567" HEX_EMAIL=$(APP)@noone.nope $v
+
+	$i "Create a key for that user"
+	$t $(MAKE) -C $(APP) hex-key-add HEX_USERNAME=$(APP) HEX_PASSWORD="1234567" | grep ^Secret: | cut -f2 -d" " > $(APP)/hex.key
+
+	$i "Publish the release"
+	$t $(MAKE) -C $(APP) hex-release-publish HEX_SECRET=`cat $(APP)/hex.key` $v
+
+	$i "Publish the documentation for the release"
+	$t $(MAKE) -C $(APP) hex-docs-publish HEX_SECRET=`cat $(APP)/hex.key` $v
+
+# @todo hex-docs-publish when there are no docs
+
+hex-docs-delete: init
+
+	$i "Bootstrap a new OTP application named $(APP)"
+	$t mkdir $(APP)/
+	$t cp ../erlang.mk $(APP)/
+	$t $(MAKE) -C $(APP) -f erlang.mk bootstrap $v
+
+	$i "Generate a doc/overview.edoc file"
+	$t mkdir $(APP)/doc
+	$t printf "%s\n" \
+		"@author R. J. Hacker <rjh@acme.com>" \
+		"@copyright 2007 R. J. Hacker" \
+		"@version 1.0.0" \
+		"@title Welcome to the 'frob' application!" \
+		"@doc 'frob' is a highly advanced frobnicator with low latency," > $(APP)/doc/overview.edoc
+
+	$i "Configure a local Hex provider"
+	$t perl -ni.bak -e 'print;if ($$.==1) {print "define HEX_CONFIG\n#{api_url => <<\"http://localhost:4000/api\">>}\nendef\n"}' $(APP)/Makefile
+
+	$i "Add extra Hex metadata"
+	$t perl -ni.bak -e 'print;if ($$.==1) {print "define HEX_TARBALL_EXTRA_METADATA\n#{licenses => [<<\"ISC\">>]}\nendef\n"}' $(APP)/Makefile
+
+	$i "Create a Hex user"
+	$t $(MAKE) -C $(APP) hex-user-create HEX_USERNAME=$(APP) HEX_PASSWORD="1234567" HEX_EMAIL=$(APP)@noone.nope $v
+
+	$i "Create a key for that user"
+	$t $(MAKE) -C $(APP) hex-key-add HEX_USERNAME=$(APP) HEX_PASSWORD="1234567" | grep ^Secret: | cut -f2 -d" " > $(APP)/hex.key
+
+	$i "Publish the release"
+	$t $(MAKE) -C $(APP) hex-release-publish HEX_SECRET=`cat $(APP)/hex.key` $v
+
+	$i "Publish the documentation for the release"
+	$t $(MAKE) -C $(APP) hex-docs-publish HEX_SECRET=`cat $(APP)/hex.key` $v
+
+	$i "Delete the documentation for the release"
+	$t $(MAKE) -C $(APP) hex-docs-delete HEX_SECRET=`cat $(APP)/hex.key` $v
+endif