diff --git a/.travis.yml b/.travis.yml
index 280da056995..0d8641e45ed 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -188,7 +188,7 @@ matrix:
       script:
         MESSAGE_FILE=$(mktemp -t msg.XXXXXX);
         . src/ci/docker/x86_64-gnu-tools/repo.sh;
-        commit_toolstate_change "$MESSAGE_FILE" "$TRAVIS_BUILD_DIR/src/tools/publish_toolstate.py" "$(git rev-parse HEAD)" "$(git log --format=%s -n1 HEAD)" "$MESSAGE_FILE"
+        commit_toolstate_change "$MESSAGE_FILE" "$TRAVIS_BUILD_DIR/src/tools/publish_toolstate.py" "$(git rev-parse HEAD)" "$(git log --format=%s -n1 HEAD)" "$MESSAGE_FILE" "$TOOLSTATE_REPO_ACCESS_TOKEN";
 
 env:
   global:
diff --git a/src/bootstrap/builder.rs b/src/bootstrap/builder.rs
index b5946b44e05..1df85323c41 100644
--- a/src/bootstrap/builder.rs
+++ b/src/bootstrap/builder.rs
@@ -231,7 +231,7 @@ pub struct ShouldRun<'a> {
     paths: BTreeSet<PathSet>,
 
     // If this is a default rule, this is an additional constraint placed on
-    // it's run. Generally something like compiler docs being enabled.
+    // its run. Generally something like compiler docs being enabled.
     is_really_default: bool,
 }
 
@@ -326,7 +326,9 @@ impl<'a> Builder<'a> {
                 test::RunPassPretty, test::RunFailPretty, test::RunPassValgrindPretty,
                 test::RunPassFullDepsPretty, test::RunFailFullDepsPretty, test::RunMake,
                 test::Crate, test::CrateLibrustc, test::Rustdoc, test::Linkcheck, test::Cargotest,
-                test::Cargo, test::Rls, test::Docs, test::ErrorIndex, test::Distcheck,
+                test::Cargo, test::Rls, test::ErrorIndex, test::Distcheck,
+                test::Nomicon, test::Reference, test::RustdocBook, test::RustByExample,
+                test::TheBook, test::UnstableBook,
                 test::Rustfmt, test::Miri, test::Clippy, test::RustdocJS, test::RustdocTheme),
             Kind::Bench => describe!(test::Crate, test::CrateLibrustc),
             Kind::Doc => describe!(doc::UnstableBook, doc::UnstableBookGen, doc::TheBook,
diff --git a/src/bootstrap/compile.rs b/src/bootstrap/compile.rs
index 2c9f0ddb6c3..408d63be6c6 100644
--- a/src/bootstrap/compile.rs
+++ b/src/bootstrap/compile.rs
@@ -629,6 +629,8 @@ impl Step for CodegenBackend {
             .arg(build.src.join("src/librustc_trans/Cargo.toml"));
         rustc_cargo_env(build, &mut cargo);
 
+        let _folder = build.fold_output(|| format!("stage{}-rustc_trans", compiler.stage));
+
         match &*self.backend {
             "llvm" | "emscripten" => {
                 // Build LLVM for our target. This will implicitly build the
@@ -642,7 +644,6 @@ impl Step for CodegenBackend {
                     features.push_str(" emscripten");
                 }
 
-                let _folder = build.fold_output(|| format!("stage{}-rustc_trans", compiler.stage));
                 println!("Building stage{} codegen artifacts ({} -> {}, {})",
                          compiler.stage, &compiler.host, target, self.backend);
 
diff --git a/src/bootstrap/test.rs b/src/bootstrap/test.rs
index bd8c36a296c..b27ddfdbc5e 100644
--- a/src/bootstrap/test.rs
+++ b/src/bootstrap/test.rs
@@ -78,15 +78,17 @@ fn try_run(build: &Build, cmd: &mut Command) -> bool {
     true
 }
 
-fn try_run_quiet(build: &Build, cmd: &mut Command) {
+fn try_run_quiet(build: &Build, cmd: &mut Command) -> bool {
     if !build.fail_fast {
         if !build.try_run_quiet(cmd) {
             let mut failures = build.delayed_failures.borrow_mut();
             failures.push(format!("{:?}", cmd));
+            return false;
         }
     } else {
         build.run_quiet(cmd);
     }
+    true
 }
 
 #[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)]
@@ -994,23 +996,19 @@ impl Step for Compiletest {
 }
 
 #[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)]
-pub struct Docs {
+struct DocTest {
     compiler: Compiler,
+    path: &'static str,
+    name: &'static str,
+    is_ext_doc: bool,
 }
 
-impl Step for Docs {
+impl Step for DocTest {
     type Output = ();
-    const DEFAULT: bool = true;
     const ONLY_HOSTS: bool = true;
 
     fn should_run(run: ShouldRun) -> ShouldRun {
-        run.path("src/doc")
-    }
-
-    fn make_run(run: RunConfig) {
-        run.builder.ensure(Docs {
-            compiler: run.builder.compiler(run.builder.top_stage, run.host),
-        });
+        run.never()
     }
 
     /// Run `rustdoc --test` for all documentation in `src/doc`.
@@ -1026,9 +1024,9 @@ impl Step for Docs {
 
         // Do a breadth-first traversal of the `src/doc` directory and just run
         // tests for all files that end in `*.md`
-        let mut stack = vec![build.src.join("src/doc")];
+        let mut stack = vec![build.src.join(self.path)];
         let _time = util::timeit();
-        let _folder = build.fold_output(|| "test_docs");
+        let _folder = build.fold_output(|| format!("test_{}", self.name));
 
         while let Some(p) = stack.pop() {
             if p.is_dir() {
@@ -1046,11 +1044,64 @@ impl Step for Docs {
                 continue;
             }
 
-            markdown_test(builder, compiler, &p);
+            let test_result = markdown_test(builder, compiler, &p);
+            if self.is_ext_doc {
+                let toolstate = if test_result {
+                    ToolState::TestPass
+                } else {
+                    ToolState::TestFail
+                };
+                build.save_toolstate(self.name, toolstate);
+            }
         }
     }
 }
 
+macro_rules! test_book {
+    ($($name:ident, $path:expr, $book_name:expr, default=$default:expr;)+) => {
+        $(
+            #[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)]
+            pub struct $name {
+                compiler: Compiler,
+            }
+
+            impl Step for $name {
+                type Output = ();
+                const DEFAULT: bool = $default;
+                const ONLY_HOSTS: bool = true;
+
+                fn should_run(run: ShouldRun) -> ShouldRun {
+                    run.path($path)
+                }
+
+                fn make_run(run: RunConfig) {
+                    run.builder.ensure($name {
+                        compiler: run.builder.compiler(run.builder.top_stage, run.host),
+                    });
+                }
+
+                fn run(self, builder: &Builder) {
+                    builder.ensure(DocTest {
+                        compiler: self.compiler,
+                        path: $path,
+                        name: $book_name,
+                        is_ext_doc: !$default,
+                    });
+                }
+            }
+        )+
+    }
+}
+
+test_book!(
+    Nomicon, "src/doc/nomicon", "nomicon", default=false;
+    Reference, "src/doc/reference", "reference", default=false;
+    RustdocBook, "src/doc/rustdoc", "rustdoc", default=true;
+    RustByExample, "src/doc/rust-by-example", "rust-by-example", default=false;
+    TheBook, "src/doc/book", "book", default=false;
+    UnstableBook, "src/doc/unstable-book", "unstable-book", default=true;
+);
+
 #[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)]
 pub struct ErrorIndex {
     compiler: Compiler,
@@ -1101,13 +1152,13 @@ impl Step for ErrorIndex {
     }
 }
 
-fn markdown_test(builder: &Builder, compiler: Compiler, markdown: &Path) {
+fn markdown_test(builder: &Builder, compiler: Compiler, markdown: &Path) -> bool {
     let build = builder.build;
     let mut file = t!(File::open(markdown));
     let mut contents = String::new();
     t!(file.read_to_string(&mut contents));
     if !contents.contains("```") {
-        return;
+        return true;
     }
 
     println!("doc tests for: {}", markdown.display());
@@ -1121,9 +1172,9 @@ fn markdown_test(builder: &Builder, compiler: Compiler, markdown: &Path) {
     cmd.arg("--test-args").arg(test_args);
 
     if build.config.quiet_tests {
-        try_run_quiet(build, &mut cmd);
+        try_run_quiet(build, &mut cmd)
     } else {
-        try_run(build, &mut cmd);
+        try_run(build, &mut cmd)
     }
 }
 
diff --git a/src/ci/docker/x86_64-gnu-tools/Dockerfile b/src/ci/docker/x86_64-gnu-tools/Dockerfile
index 8975d419d20..bab9145cbcb 100644
--- a/src/ci/docker/x86_64-gnu-tools/Dockerfile
+++ b/src/ci/docker/x86_64-gnu-tools/Dockerfile
@@ -17,6 +17,7 @@ RUN apt-get update && apt-get install -y --no-install-recommends \
 COPY scripts/sccache.sh /scripts/
 RUN sh /scripts/sccache.sh
 
+COPY x86_64-gnu-tools/checkregression.py /tmp/
 COPY x86_64-gnu-tools/checktools.sh /tmp/
 COPY x86_64-gnu-tools/repo.sh /tmp/
 
diff --git a/src/ci/docker/x86_64-gnu-tools/checkregression.py b/src/ci/docker/x86_64-gnu-tools/checkregression.py
new file mode 100755
index 00000000000..df791d12645
--- /dev/null
+++ b/src/ci/docker/x86_64-gnu-tools/checkregression.py
@@ -0,0 +1,40 @@
+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+
+# Copyright 2018 The Rust Project Developers. See the COPYRIGHT
+# file at the top-level directory of this distribution and at
+# http://rust-lang.org/COPYRIGHT.
+#
+# Licensed under the Apache License, Version 2.0 <LICENSE-APACHE or
+# http://www.apache.org/licenses/LICENSE-2.0> or the MIT license
+# <LICENSE-MIT or http://opensource.org/licenses/MIT>, at your
+# option. This file may not be copied, modified, or distributed
+# except according to those terms.
+
+import sys
+import json
+
+if __name__ == '__main__':
+    os_name = sys.argv[1]
+    toolstate_file = sys.argv[2]
+    current_state = sys.argv[3]
+
+    with open(toolstate_file, 'r') as f:
+        toolstate = json.load(f)
+    with open(current_state, 'r') as f:
+        current = json.load(f)
+
+    regressed = False
+    for cur in current:
+        tool = cur['tool']
+        state = cur[os_name]
+        new_state = toolstate.get(tool, '')
+        if new_state < state:
+            print(
+                'Error: The state of "{}" has regressed from "{}" to "{}"'
+                .format(tool, state, new_state)
+            )
+            regressed = True
+
+    if regressed:
+        sys.exit(1)
diff --git a/src/ci/docker/x86_64-gnu-tools/checktools.sh b/src/ci/docker/x86_64-gnu-tools/checktools.sh
index 61bb5a84d21..da89aa9423b 100755
--- a/src/ci/docker/x86_64-gnu-tools/checktools.sh
+++ b/src/ci/docker/x86_64-gnu-tools/checktools.sh
@@ -17,11 +17,18 @@ TOOLSTATE_FILE="$(realpath $2)"
 OS="$3"
 COMMIT="$(git rev-parse HEAD)"
 CHANGED_FILES="$(git diff --name-status HEAD HEAD^)"
+SIX_WEEK_CYCLE="$(( ($(date +%s) / 604800 - 3) % 6 ))"
+# ^ 1970 Jan 1st is a Thursday, and our release dates are also on Thursdays,
+#   thus we could divide by 604800 (7 days in seconds) directly.
 
 touch "$TOOLSTATE_FILE"
 
 set +e
 python2.7 "$X_PY" test --no-fail-fast \
+    src/doc/book \
+    src/doc/nomicon \
+    src/doc/reference \
+    src/doc/rust-by-example \
     src/tools/rls \
     src/tools/rustfmt \
     src/tools/miri \
@@ -29,27 +36,38 @@ python2.7 "$X_PY" test --no-fail-fast \
 set -e
 
 cat "$TOOLSTATE_FILE"
+echo
 
-# If this PR is intended to update one of these tools, do not let the build pass
-# when they do not test-pass.
-for TOOL in rls rustfmt clippy; do
-    echo "Verifying status of $TOOL..."
-    if echo "$CHANGED_FILES" | grep -q "^M[[:blank:]]src/tools/$TOOL$"; then
-        echo "This PR updated 'src/tools/$TOOL', verifying if status is 'test-pass'..."
-        if grep -vq '"'"$TOOL"'[^"]*":"test-pass"' "$TOOLSTATE_FILE"; then
+verify_status() {
+    echo "Verifying status of $1..."
+    if echo "$CHANGED_FILES" | grep -q "^M[[:blank:]]$2$"; then
+        echo "This PR updated '$2', verifying if status is 'test-pass'..."
+        if grep -vq '"'"$1"'":"test-pass"' "$TOOLSTATE_FILE"; then
             echo
-            echo "⚠️ We detected that this PR updated '$TOOL', but its tests failed."
+            echo "⚠️ We detected that this PR updated '$1', but its tests failed."
             echo
-            echo "If you do intend to update '$TOOL', please check the error messages above and"
+            echo "If you do intend to update '$1', please check the error messages above and"
             echo "commit another update."
             echo
-            echo "If you do NOT intend to update '$TOOL', please ensure you did not accidentally"
-            echo "change the submodule at 'src/tools/$TOOL'. You may ask your reviewer for the"
+            echo "If you do NOT intend to update '$1', please ensure you did not accidentally"
+            echo "change the submodule at '$2'. You may ask your reviewer for the"
             echo "proper steps."
             exit 3
         fi
     fi
-done
+}
+
+# If this PR is intended to update one of these tools, do not let the build pass
+# when they do not test-pass.
+
+verify_status book src/doc/book
+verify_status nomicon src/doc/nomicon
+verify_status reference src/doc/reference
+verify_status rust-by-example src/doc/rust-by-example
+verify_status rls src/tool/rls
+verify_status rustfmt src/tool/rustfmt
+verify_status clippy-driver src/tool/clippy
+#verify_status miri src/tool/miri
 
 if [ "$RUST_RELEASE_CHANNEL" = nightly -a -n "${TOOLSTATE_REPO_ACCESS_TOKEN+is_set}" ]; then
     . "$(dirname $0)/repo.sh"
@@ -59,6 +77,11 @@ if [ "$RUST_RELEASE_CHANNEL" = nightly -a -n "${TOOLSTATE_REPO_ACCESS_TOKEN+is_s
         sed -i "1 a\\
 $COMMIT\t$(cat "$TOOLSTATE_FILE")
 " "history/$OS.tsv"
+    # if we are at the last week in the 6-week release cycle, reject any kind of regression.
+    if [ $SIX_WEEK_CYCLE -eq 5 ]; then
+        python2.7 "$(dirname $0)/checkregression.py" \
+            "$OS" "$TOOLSTATE_FILE" "rust-toolstate/_data/latest.json"
+    fi
     rm -f "$MESSAGE_FILE"
     exit 0
 fi
diff --git a/src/tools/publish_toolstate.py b/src/tools/publish_toolstate.py
index b90947e5a43..8e23519f57e 100755
--- a/src/tools/publish_toolstate.py
+++ b/src/tools/publish_toolstate.py
@@ -17,6 +17,11 @@ import json
 import copy
 import datetime
 import collections
+import textwrap
+try:
+    import urllib2
+except ImportError:
+    import urllib.request as urllib2
 
 # List of people to ping when the status of a tool changed.
 MAINTAINERS = {
@@ -24,6 +29,10 @@ MAINTAINERS = {
     'clippy-driver': '@Manishearth @llogiq @mcarton @oli-obk',
     'rls': '@nrc',
     'rustfmt': '@nrc',
+    'book': '@carols10cents @steveklabnik',
+    'nomicon': '@frewsxcv @Gankro',
+    'reference': '@steveklabnik @Havvy @matthewjasper @alercah',
+    'rust-by-example': '@steveklabnik @marioidival @projektir',
 }
 
 
@@ -38,7 +47,12 @@ def read_current_status(current_commit, path):
     return {}
 
 
-def update_latest(current_commit, relevant_pr_number, current_datetime):
+def update_latest(
+    current_commit,
+    relevant_pr_number,
+    relevant_pr_url,
+    current_datetime
+):
     '''Updates `_data/latest.json` to match build result of the given commit.
     '''
     with open('_data/latest.json', 'rb+') as f:
@@ -50,8 +64,13 @@ def update_latest(current_commit, relevant_pr_number, current_datetime):
         }
 
         slug = 'rust-lang/rust'
-        message = '📣 Toolstate changed by {}!\n\nTested on commit {}@{}.\n\n' \
-            .format(relevant_pr_number, slug, current_commit)
+        message = textwrap.dedent('''\
+            📣 Toolstate changed by {}!
+
+            Tested on commit {}@{}.
+            Direct link to PR: <{}>
+
+        ''').format(relevant_pr_number, slug, current_commit, relevant_pr_url)
         anything_changed = False
         for status in latest:
             tool = status['tool']
@@ -68,7 +87,7 @@ def update_latest(current_commit, relevant_pr_number, current_datetime):
                 elif new < old:
                     changed = True
                     message += '💔 {} on {}: {} → {} (cc {}).\n' \
-                        .format(tool, os, old, new, MAINTAINERS[tool])
+                        .format(tool, os, old, new, MAINTAINERS.get(tool))
 
             if changed:
                 status['commit'] = current_commit
@@ -89,17 +108,41 @@ if __name__ == '__main__':
     cur_datetime = datetime.datetime.utcnow().strftime('%Y-%m-%dT%H:%M:%SZ')
     cur_commit_msg = sys.argv[2]
     save_message_to_path = sys.argv[3]
+    github_token = sys.argv[4]
 
-    relevant_pr_match = re.search('#[0-9]+', cur_commit_msg)
+    relevant_pr_match = re.search('#([0-9]+)', cur_commit_msg)
     if relevant_pr_match:
-        relevant_pr_number = 'rust-lang/rust' + relevant_pr_match.group(0)
+        number = relevant_pr_match.group(1)
+        relevant_pr_number = 'rust-lang/rust#' + number
+        relevant_pr_url = 'https://github.com/rust-lang/rust/pull/' + number
     else:
+        number = '-1'
         relevant_pr_number = '<unknown PR>'
+        relevant_pr_url = '<unknown>'
 
-    message = update_latest(cur_commit, relevant_pr_number, cur_datetime)
-    if message:
-        print(message)
-        with open(save_message_to_path, 'w') as f:
-            f.write(message)
-    else:
+    message = update_latest(
+        cur_commit,
+        relevant_pr_number,
+        relevant_pr_url,
+        cur_datetime
+    )
+    if not message:
         print('<Nothing changed>')
+        sys.exit(0)
+
+    print(message)
+    with open(save_message_to_path, 'w') as f:
+        f.write(message)
+
+    # Write the toolstate comment on the PR as well.
+    gh_url = 'https://api.github.com/repos/rust-lang/rust/issues/{}/comments' \
+        .format(number)
+    response = urllib2.urlopen(urllib2.Request(
+        gh_url,
+        json.dumps({'body': message}),
+        {
+            'Authorization': 'token ' + github_token,
+            'Content-Type': 'application/json',
+        }
+    ))
+    response.read()