New 2-step release process: script/{prep,push}-release.sh

Steps:
1. On a clean tree on `master`, add a new entry to `CHANGELOG.md` in the
   same format as the other entries, then run `script/prep-release.sh`
   to do everything that can be done locally: bump version, build,
   package as tarball + zipfile, commit, tag.
2. After double-checking the build/package/commit/tag, run
   `script/push-release.sh` (with a GitHub access token env variable)
   to automatically publish to NPM, push to GitHub, create a new GitHub
   Release, and cleanup the tarballs etc.

Notes:
- `CHANGELOG.md`: I changed the format to resemble [the GitHub Releases
  page] more. In particular, each entry has a one-line summary on the
  GitHub Releases page that was the commit message for the version bump/
  changelog addition, but didn't show up in the changelog at all.
- `Makefile`: since these scripts create and cleanup the tarballs and
  zipfiles, there's no need for `make dist` anymore, and `make clean` is
  now, well, cleaner.
- Creating GitHub Releases: this was loosely inspired by the
  [gh-release Bash script], but was mostly based on the [GitHub Releases
  API docs].

[the GitHub Releases page] https://github.com/mathquill/mathquill/releases
[gh-release Bash script]: https://github.com/progrium/gh-release/blob/master/bash/gh-release.bash
[itHub Releases API docs]: https://developer.github.com/v3/repos/releases/#create-a-release

--

I designed this release process to have 2 steps:

1. Do everything that can be done locally: build, changelogs, bump
   version, commit, tag, create tarballs and zipfiles
2. Only after getting a chance to double-check the stuff done locally do
   we publish on public servers like NPM and GitHub

Not coincidentally, `npm` has two relevant commands, [`npm version`]
and [`npm publish`]. They seem like they'd correspond nicely with my
2 steps, so I tried implementing the release process as hooks into those
commands, but it was too annoying:
- [`npm version`] ensures that the working tree is clean even before
  calling the `preversion` hook, and then automatically commits, so
  the releaser would have to edit `CHANGELOG.md` during one of those
  hook calls (like it opens a shell or editor or something).
- [`npm publish`] is not as well-documented as `npm version`, but I
  [stumbled on] a sub-step: [`npm pack`], which figures out the files
  to be included and packs them into a tarball to be uploaded to the
  NPM registry. However, we actually want that to happen during the
  Step 1, so we can double-check it before uploading, not this step.

So using NPM hook scripts, the workflow would have to be:
1. On a clean tree, run `npm version patch`, which opens an editor on
   `CHANGELOG.md`, then builds, commits, tags, and in particular
   packages a tarball like `mathquill-0.10.2.tgz`.
2. After double-checking the build/package/commit/tag, run
   `npm publish mathquill-0.10.2.tgz`. Note that plain `npm publish`
   would re-run `npm pack` instead of using the tarball from Step 1.
   (I guess that would be fine, just inefficient?)

Rather than work within these inconveniences, I figured it'd be simpler
to just use "lower-level" tools, or at least, the same tools but in a
lower-level capacity. Specifically, `prep-release.sh` calls
`npm version --no-git-tag-version`, which just bumps the version in
package.json and doesn't do anything with Git, and then calls
`npm pack` to package the tarball; and `push-release.sh` calls
`npm publish <tarball>`, skipping the internal call to `npm pack` simply
using `npm publish` to upload to the NPM registry.

[`npm version`]: https://docs.npmjs.com/cli/version
[`npm publish`]: https://docs.npmjs.com/cli/publish
[`npm pack`]: https://docs.npmjs.com/cli/pack
[stumbled on]: https://github.com/npm/npm/pull/13080
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 4a787ed..e48a512 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,4 +1,6 @@
-## v0.10.1: 2016-03-21
+## v0.10.1: Fix `font-size: 0` typing problems and more
+
+_2016-03-21_
 
 Important fix: remove `font-size: 0` on textarea (#585), fixing typing
 in Chrome Canary (#540) as well as the Enter key not triggering the
@@ -35,7 +37,9 @@
 **build system fixes:**
 - (#532) add console output to show URL of local test pages
 
-## v0.10.0: 2016-02-20
+## v0.10.0: Total API overhaul, new features galore
+
+_2016-02-20_
 
 Many major changes including a total overhaul of the API (no more
 auto-MathQuill-ifying of `.mathquill-editable` etc, and no more jQuery
@@ -137,7 +141,9 @@
 - (#117, #142, #186, #287) massive refactor of cursor methods to not
   assume the edit tree is double-layered
 
-## v0.9.4: 2014-1-22
+## v0.9.4: URGENT HOTFIX for cursor showing up as an ugly box in Chrome 40
+
+_2014-1-22_
 
 URGENT HOTFIX for cursor showing up as an ugly box in Chrome 40 (#371)
 
@@ -155,7 +161,9 @@
 **docs:**
 - (#283) change license from LGPL to Mozilla Public License
 
-## v0.9.3: 2013-11-11
+## v0.9.3: Fix `NZQRC` appearing double-struck/blackboard bold
+
+_2013-11-11_
 
 **new features:**
 - (#185) add `\vec`
@@ -174,7 +182,9 @@
 - (#189) replace Connect with tiny handwritten static server
 - upgrade to uglifyjs2
 
-## v0.9.2: 2013-04-02
+## v0.9.2: Fix bug in hotfix for typing over selections in Safari 5.1
+
+_2013-04-02_
 
 NOTE: The hotfix for typing over selections in Safari 5.1 (#135) from
 v0.9.1 had a huge bug, fixed as #166.
@@ -199,7 +209,9 @@
 - New site-building system
 - no more submodules, `npm` only
 
-## v0.9.1: 2012-12-19
+## v0.9.1: Hotfix for typing over selections in Safari 5.1
+
+_2012-12-19_
 
   * Started the changelog
   * Added a `make publish` script
diff --git a/Makefile b/Makefile
index a238724..102f909 100644
--- a/Makefile
+++ b/Makefile
@@ -80,12 +80,6 @@
 BUILD_TEST = $(BUILD_DIR)/mathquill.test.js
 UGLY_JS = $(BUILD_DIR)/mathquill.min.js
 UGLY_BASIC_JS = $(BUILD_DIR)/mathquill-basic.min.js
-CLEAN += $(BUILD_DIR)/*
-
-DISTDIR = ./mathquill-$(VERSION)
-DISTTAR = $(DISTDIR).tgz
-DISTZIP = $(DISTDIR).zip
-CLEAN += $(DISTTAR) $(DISTZIP)
 
 # programs and flags
 UGLIFY ?= ./node_modules/.bin/uglifyjs
@@ -111,7 +105,7 @@
 # -*- Build tasks -*-
 #
 
-.PHONY: all basic dev js uglify css font dist clean
+.PHONY: all basic dev js uglify css font clean
 all: font css uglify
 basic: $(UGLY_BASIC_JS) $(BASIC_CSS)
 # dev is like all, but without minification
@@ -121,7 +115,7 @@
 css: $(BUILD_CSS)
 font: $(FONT_TARGET)
 clean:
-	rm -rf $(CLEAN)
+	rm -rf $(BUILD_DIR)
 
 $(PJS_SRC): $(NODE_MODULES_INSTALLED)
 
@@ -159,13 +153,6 @@
 	rm -rf $@
 	cp -r $< $@
 
-dist: $(UGLY_JS) $(BUILD_JS) $(BUILD_CSS) $(FONT_TARGET)
-	rm -rf $(DISTDIR)
-	cp -r $(BUILD_DIR) $(DISTDIR)
-	zip -r -X $(DISTZIP) $(DISTDIR)
-	tar -czf $(DISTTAR) $(DISTDIR)
-	rm -r $(DISTDIR)
-
 #
 # -*- Test tasks -*-
 #
diff --git a/script/prep-release.sh b/script/prep-release.sh
new file mode 100755
index 0000000..1f0d2bf
--- /dev/null
+++ b/script/prep-release.sh
@@ -0,0 +1,90 @@
+#!/bin/bash
+set -e -o pipefail
+die () { printf '\n\tERROR: %s\n\n' "$*"; exit 1; }
+
+#
+# -1. Old versions of npm omit random files due to race condition https://git.io/vooV3
+#
+equalOrNewer () { # inspired by http://stackoverflow.com/a/25731924/362030
+  printf '%s\n%s\n' "$@" | sort -cnrt . -k 1,1 -k 2,2 -k 3,3 2>/dev/null
+}
+npm_v="$(npm -v)"
+if echo "$npm_v" | grep -q '^2\.'; then
+  equalOrNewer "$npm_v" 2.15.8 \
+    || die 'Your npm@2 version must be >=2.15.8, see https://git.io/vooV3'
+else
+  equalOrNewer "$npm_v" 3.10.1 \
+    || die 'Your npm@3 version must be >=3.10.1, see https://git.io/vooV3'
+fi
+
+#
+# 0. Clean tree & repo state except for CHANGELOG
+#
+files="$(git diff --name-only HEAD)"
+test "$files" \
+  || { echo 'First, you must add an entry to CHANGELOG.md'; exit 1; }
+test "$files" = CHANGELOG.md \
+  || die 'You have uncommitted changes other than to CHANGELOG.md'
+test "$(git rev-parse --abbrev-ref HEAD)" = master \
+  || die 'You must be on master'
+test "$(git rev-list --count @{upstream}..)" = 0 \
+  || test "$1" = --allow-unpushed-commits \
+  || die "You have unpushed commits (do $0 --allow-unpushed-commits to continue anyway)"
+
+#
+# 1. Bump package.json version
+#
+change_summary="$(git diff HEAD | grep '^+' | sed -n '2 s/^+## // p')"
+version="$(echo "$change_summary" | sed 's/:.*//')"
+git cat-file -e "$version" 2>/dev/null \
+  && die "$version already exists"
+npm version "$version" --no-git-tag-version >/dev/null
+echo "1. Bumped package.json version to \""$(node -p 'require("./package.json").version')"\""
+
+#
+# 2. Build
+#
+echo '2. make:'
+make 2>&1 | sed 's/^/     /'
+
+#
+# 3. Package as tarball + zipfile
+#
+tarball=$(npm pack) # create tarball
+tar -xzf $tarball # extract tarball as package/
+zipfile=${tarball%.tgz}.zip
+zip -qrX $zipfile package # create zipfile from package/
+echo "3. Collected release files into package/, packed as $tarball and $zipfile"
+
+#
+# 4. Commit
+#
+git add CHANGELOG.md package.json
+git commit -m "$change_summary" | sed '1 s/^/4. Committed: /; 2,$ s/^/              /'
+
+#
+# 5. Record shrinkwrap
+#
+npm shrinkwrap --dev | sed 's/^/5. /'
+shrinkwrap="$(<npm-shrinkwrap.json)"
+rm npm-shrinkwrap.json
+
+#
+# 6. Tag
+#
+{
+  echo "$change_summary"
+  echo
+  echo Created automatically by: $0
+  echo
+  echo npm-shrinkwrap.json:
+  echo "$shrinkwrap"
+} | git tag -F - $version
+echo "6. Tagged $version"
+
+#
+# Done!
+#
+echo
+git status -sb
+echo After double-checking the build/package/commit/tag, run script/push-release.sh to publish the release
diff --git a/script/push-release.sh b/script/push-release.sh
new file mode 100755
index 0000000..f588fc3
--- /dev/null
+++ b/script/push-release.sh
@@ -0,0 +1,71 @@
+#!/bin/bash
+set -e -o pipefail
+die () { printf '\n\tERROR: %s\n\n' "$*"; exit 1; }
+
+#
+# 0. Precheck that a release has been prepped (and we have an access token)
+#
+test "$(git rev-list --count @{upstream}..)" != 0 \
+  || die 'No unpushed commits, first run script/prep-release.sh'
+
+tagname="$(git describe --candidates=0 --match 'v*.*.*')"
+test "$tagname" \
+  || die 'No version tag for HEAD, first run script/prep-release.sh'
+
+tarball="mathquill-${tagname#v}.tgz"
+zipfile="mathquill-${tagname#v}.zip"
+ls "$tarball" "$zipfile" >/dev/null \
+  || die 'No tarball or zipfile, first run script/prep-release.sh'
+
+test "$GITHUB_ACCESS_TOKEN" || {
+  echo
+  echo '	ERROR: No $GITHUB_ACCESS_TOKEN defined.'
+  echo
+  echo 'This script needs an access token to create GitHub Releases.'
+  echo 'Follow these instructions to create a token authorized for the "repo" scope:'
+  echo '  https://help.github.com/articles/creating-an-access-token-for-command-line-use/'
+  echo 'Then do:'
+  echo "  GITHUB_ACCESS_TOKEN=<token> $0"
+  exit 1
+}
+
+#
+# 1. npm publish
+#
+npm publish $tarball
+
+#
+# 2. git push, with tag
+#
+git push origin master tag $tagname
+
+#
+# 3. Create GitHub Release
+#
+changelog_entry="$(git show CHANGELOG.md | grep '^+' | sed -n '2,$ s/^+// p')"
+json="$(
+  tagname=$tagname \
+  summary="$(echo "$changelog_entry" | sed -n '1 s/^## // p')" \
+  body="$(echo "$changelog_entry" | tail +5)" \
+  node -p 'JSON.stringify({
+    tag_name: process.env.tagname,
+    name: process.env.summary,
+    body: process.env.body
+  })'
+)"
+
+endpoint='https://api.github.com/repos/mathquill/mathquill/releases'
+release_response="$(curl -s "$endpoint" -d "$json" \
+                      -H "Authorization: token $GITHUB_ACCESS_TOKEN")"
+upload_url="$(response="$release_response" \
+  node -p 'JSON.parse(process.env.response).upload_url' | sed 's/{.*}$//')"
+
+cat $tarball | curl "$upload_url?name=$tarball" --data-binary @- \
+  -H 'Content-Type: application/x-gzip' -H "Authorization: token $GITHUB_ACCESS_TOKEN"
+cat $zipfile | curl "$upload_url?name=$zipfile" --data-binary @- \
+  -H 'Content-Type: application/zip' -H "Authorization: token $GITHUB_ACCESS_TOKEN"
+
+#
+# 4. Cleanup
+#
+rm -rf package $tarball $zipfile