diff --git a/.distignore b/.distignore
new file mode 100755
index 0000000000000000000000000000000000000000..1649ea423609d218434907825a314314c96c3fa8
--- /dev/null
+++ b/.distignore
@@ -0,0 +1,30 @@
+# A set of files you probably don't want in your WordPress.org distribution
+.distignore
+.editorconfig
+.git
+.gitignore
+.gitlab-ci.yml
+.travis.yml
+.DS_Store
+Thumbs.db
+behat.yml
+bin
+circle.yml
+composer.json
+composer.lock
+Gruntfile.js
+package.json
+phpunit.xml
+phpunit.xml.dist
+multisite.xml
+multisite.xml.dist
+phpcs.xml
+phpcs.xml.dist
+README.md
+wp-cli.local.yml
+tests
+vendor
+node_modules
+*.sql
+*.tar.gz
+*.zip
diff --git a/.editorconfig b/.editorconfig
new file mode 100755
index 0000000000000000000000000000000000000000..79207a40cb9326b8c6b8c958fa864b5345f94e68
--- /dev/null
+++ b/.editorconfig
@@ -0,0 +1,22 @@
+# This file is for unifying the coding style for different editors and IDEs
+# editorconfig.org
+
+# WordPress Coding Standards
+# https://make.wordpress.org/core/handbook/coding-standards/
+
+root = true
+
+[*]
+charset = utf-8
+end_of_line = lf
+insert_final_newline = true
+trim_trailing_whitespace = true
+indent_style = tab
+indent_size = 4
+
+[{.jshintrc,*.json,*.yml}]
+indent_style = space
+indent_size = 2
+
+[{*.txt,wp-config-sample.php}]
+end_of_line = crlf
diff --git a/.gitignore b/.gitignore
new file mode 100755
index 0000000000000000000000000000000000000000..7b4c57a3988f856fdb2e5688204256b53672c91f
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,7 @@
+.DS_Store
+Thumbs.db
+wp-cli.local.yml
+node_modules/
+*.sql
+*.tar.gz
+*.zip
diff --git a/.travis.yml b/.travis.yml
new file mode 100755
index 0000000000000000000000000000000000000000..a6ccc2eeb7d1ff64145d83a2531fa0a4d8774a3b
--- /dev/null
+++ b/.travis.yml
@@ -0,0 +1,61 @@
+sudo: false
+
+language: php
+
+notifications:
+  email:
+    on_success: never
+    on_failure: change
+
+cache:
+  directories:
+    - vendor
+    - $HOME/.composer/cache
+
+matrix:
+  include:
+    # PHPUnit
+    - php: 7.2
+      env: WP_VERSION=latest
+    - php: 7.2
+      env: WP_VERSION=trunk
+    - php: 7.1
+      env: WP_VERSION=latest
+    - php: 7.1
+      env: WP_VERSION=trunk
+    - php: 7.0
+      env: WP_VERSION=latest
+    - php: 7.0
+      env: WP_VERSION=trunk
+    # PHPCS
+    - php: 7.1
+      env: WP_TRAVISCI=phpcs
+
+before_script:
+  - phpenv config-rm xdebug.ini
+  - export PATH="$HOME/.composer/vendor/bin:$PATH"
+  - |
+    if [[ ! -z "$WP_VERSION" ]] ; then
+      bash bin/install-wp-tests.sh wordpress_test root '' localhost $WP_VERSION
+      if [[ ${TRAVIS_PHP_VERSION:0:2} == "5." ]]; then
+        composer global require "phpunit/phpunit=4.8.*"
+      else
+        composer global require "phpunit/phpunit=5.7.*"
+      fi
+    fi
+  - |
+    if [[ "$WP_TRAVISCI" == "phpcs" ]] ; then
+      composer global require wp-coding-standards/wpcs
+      phpcs --config-set installed_paths $HOME/.composer/vendor/wp-coding-standards/wpcs
+    fi
+
+script:
+  - |
+    if [[ ! -z "$WP_VERSION" ]] ; then
+      phpunit
+      WP_MULTISITE=1 phpunit
+    fi
+  - |
+    if [[ "$WP_TRAVISCI" == "phpcs" ]] ; then
+      phpcs
+    fi
diff --git a/Gruntfile.js b/Gruntfile.js
new file mode 100755
index 0000000000000000000000000000000000000000..bc0fcf617a006bdf32648830e622c2c201283693
--- /dev/null
+++ b/Gruntfile.js
@@ -0,0 +1,54 @@
+module.exports = function( grunt ) {
+
+	'use strict';
+	var banner = '/**\n * <%= pkg.homepage %>\n * Copyright (c) <%= grunt.template.today("yyyy") %>\n * This file is generated automatically. Do not edit.\n */\n';
+	// Project configuration
+	grunt.initConfig( {
+
+		pkg: grunt.file.readJSON( 'package.json' ),
+
+		addtextdomain: {
+			options: {
+				textdomain: 'bulk-edit-cron-offload',
+			},
+			update_all_domains: {
+				options: {
+					updateDomains: true
+				},
+				src: [ '*.php', '**/*.php', '!node_modules/**', '!php-tests/**', '!bin/**' ]
+			}
+		},
+
+		wp_readme_to_markdown: {
+			your_target: {
+				files: {
+					'README.md': 'readme.txt'
+				}
+			},
+		},
+
+		makepot: {
+			target: {
+				options: {
+					domainPath: '/languages',
+					mainFile: 'bulk-edit-cron-offload.php',
+					potFilename: 'bulk-edit-cron-offload.pot',
+					potHeaders: {
+						poedit: true,
+						'x-poedit-keywordslist': true
+					},
+					type: 'wp-plugin',
+					updateTimestamp: true
+				}
+			}
+		},
+	} );
+
+	grunt.loadNpmTasks( 'grunt-wp-i18n' );
+	grunt.loadNpmTasks( 'grunt-wp-readme-to-markdown' );
+	grunt.registerTask( 'i18n', ['addtextdomain', 'makepot'] );
+	grunt.registerTask( 'readme', ['wp_readme_to_markdown'] );
+
+	grunt.util.linefeed = '\n';
+
+};
diff --git a/bin/install-wp-tests.sh b/bin/install-wp-tests.sh
new file mode 100755
index 0000000000000000000000000000000000000000..73bb4c787eb2ccf4b4225729ddaa3578cb606741
--- /dev/null
+++ b/bin/install-wp-tests.sh
@@ -0,0 +1,127 @@
+#!/usr/bin/env bash
+
+if [ $# -lt 3 ]; then
+	echo "usage: $0 <db-name> <db-user> <db-pass> [db-host] [wp-version] [skip-database-creation]"
+	exit 1
+fi
+
+DB_NAME=$1
+DB_USER=$2
+DB_PASS=$3
+DB_HOST=${4-localhost}
+WP_VERSION=${5-latest}
+SKIP_DB_CREATE=${6-false}
+
+WP_TESTS_DIR=${WP_TESTS_DIR-/tmp/wordpress-tests-lib}
+WP_CORE_DIR=${WP_CORE_DIR-/tmp/wordpress/}
+
+download() {
+    if [ `which curl` ]; then
+        curl -s "$1" > "$2";
+    elif [ `which wget` ]; then
+        wget -nv -O "$2" "$1"
+    fi
+}
+
+if [[ $WP_VERSION =~ [0-9]+\.[0-9]+(\.[0-9]+)? ]]; then
+	WP_TESTS_TAG="tags/$WP_VERSION"
+elif [[ $WP_VERSION == 'nightly' || $WP_VERSION == 'trunk' ]]; then
+	WP_TESTS_TAG="trunk"
+else
+	# http serves a single offer, whereas https serves multiple. we only want one
+	download http://api.wordpress.org/core/version-check/1.7/ /tmp/wp-latest.json
+	grep '[0-9]+\.[0-9]+(\.[0-9]+)?' /tmp/wp-latest.json
+	LATEST_VERSION=$(grep -o '"version":"[^"]*' /tmp/wp-latest.json | sed 's/"version":"//')
+	if [[ -z "$LATEST_VERSION" ]]; then
+		echo "Latest WordPress version could not be found"
+		exit 1
+	fi
+	WP_TESTS_TAG="tags/$LATEST_VERSION"
+fi
+
+set -ex
+
+install_wp() {
+
+	if [ -d $WP_CORE_DIR ]; then
+		return;
+	fi
+
+	mkdir -p $WP_CORE_DIR
+
+	if [[ $WP_VERSION == 'nightly' || $WP_VERSION == 'trunk' ]]; then
+		mkdir -p /tmp/wordpress-nightly
+		download https://wordpress.org/nightly-builds/wordpress-latest.zip  /tmp/wordpress-nightly/wordpress-nightly.zip
+		unzip -q /tmp/wordpress-nightly/wordpress-nightly.zip -d /tmp/wordpress-nightly/
+		mv /tmp/wordpress-nightly/wordpress/* $WP_CORE_DIR
+	else
+		if [ $WP_VERSION == 'latest' ]; then
+			local ARCHIVE_NAME='latest'
+		else
+			local ARCHIVE_NAME="wordpress-$WP_VERSION"
+		fi
+		download https://wordpress.org/${ARCHIVE_NAME}.tar.gz  /tmp/wordpress.tar.gz
+		tar --strip-components=1 -zxmf /tmp/wordpress.tar.gz -C $WP_CORE_DIR
+	fi
+
+	download https://raw.github.com/markoheijnen/wp-mysqli/master/db.php $WP_CORE_DIR/wp-content/db.php
+}
+
+install_test_suite() {
+	# portable in-place argument for both GNU sed and Mac OSX sed
+	if [[ $(uname -s) == 'Darwin' ]]; then
+		local ioption='-i .bak'
+	else
+		local ioption='-i'
+	fi
+
+	# set up testing suite if it doesn't yet exist
+	if [ ! -d $WP_TESTS_DIR ]; then
+		# set up testing suite
+		mkdir -p $WP_TESTS_DIR
+		svn co --quiet https://develop.svn.wordpress.org/${WP_TESTS_TAG}/tests/phpunit/includes/ $WP_TESTS_DIR/includes
+		svn co --quiet https://develop.svn.wordpress.org/${WP_TESTS_TAG}/tests/phpunit/data/ $WP_TESTS_DIR/data
+	fi
+
+	if [ ! -f wp-tests-config.php ]; then
+		download https://develop.svn.wordpress.org/${WP_TESTS_TAG}/wp-tests-config-sample.php "$WP_TESTS_DIR"/wp-tests-config.php
+		# remove all forward slashes in the end
+		WP_CORE_DIR=$(echo $WP_CORE_DIR | sed "s:/\+$::")
+		sed $ioption "s:dirname( __FILE__ ) . '/src/':'$WP_CORE_DIR/':" "$WP_TESTS_DIR"/wp-tests-config.php
+		sed $ioption "s/youremptytestdbnamehere/$DB_NAME/" "$WP_TESTS_DIR"/wp-tests-config.php
+		sed $ioption "s/yourusernamehere/$DB_USER/" "$WP_TESTS_DIR"/wp-tests-config.php
+		sed $ioption "s/yourpasswordhere/$DB_PASS/" "$WP_TESTS_DIR"/wp-tests-config.php
+		sed $ioption "s|localhost|${DB_HOST}|" "$WP_TESTS_DIR"/wp-tests-config.php
+	fi
+
+}
+
+install_db() {
+
+	if [ ${SKIP_DB_CREATE} = "true" ]; then
+		return 0
+	fi
+
+	# parse DB_HOST for port or socket references
+	local PARTS=(${DB_HOST//\:/ })
+	local DB_HOSTNAME=${PARTS[0]};
+	local DB_SOCK_OR_PORT=${PARTS[1]};
+	local EXTRA=""
+
+	if ! [ -z $DB_HOSTNAME ] ; then
+		if [ $(echo $DB_SOCK_OR_PORT | grep -e '^[0-9]\{1,\}$') ]; then
+			EXTRA=" --host=$DB_HOSTNAME --port=$DB_SOCK_OR_PORT --protocol=tcp"
+		elif ! [ -z $DB_SOCK_OR_PORT ] ; then
+			EXTRA=" --socket=$DB_SOCK_OR_PORT"
+		elif ! [ -z $DB_HOSTNAME ] ; then
+			EXTRA=" --host=$DB_HOSTNAME --protocol=tcp"
+		fi
+	fi
+
+	# create database
+	mysqladmin create $DB_NAME --user="$DB_USER" --password="$DB_PASS"$EXTRA
+}
+
+install_wp
+install_test_suite
+install_db
diff --git a/bulk-edit-cron-offload.php b/bulk-edit-cron-offload.php
index 5297a8dd51d0b7f2c0913fd08f89d0230d2c1686..05afa82e896de99cf2271c6eadd21f94decff7fe 100644
--- a/bulk-edit-cron-offload.php
+++ b/bulk-edit-cron-offload.php
@@ -1,11 +1,15 @@
 <?php
-/*
- Plugin Name: Offload Bulk Edit to Cron
- Plugin URI: https://vip.wordpress.com/
- Description: Process Bulk Edit requests using Cron
- Author: Erick Hitter, Automattic
- Version: 1.0
- Text Domain: automattic-bulk-edit-cron-offload
+/**
+ * Plugin Name:     Bulk Edit Cron Offload
+ * Plugin URI:      https://vip.wordpress.com/
+ * Description:     Process Bulk Edit requests using Cron
+ * Author:          Erick Hitter, Automattic
+ * Author URI:      https://automattic.com/
+ * Text Domain:     automattic-bulk-edit-cron-offload
+ * Domain Path:     /languages
+ * Version:         1.0
+ *
+ * @package         Bulk_Edit_Cron_Offload
  */
 
 namespace Automattic\WP\Bulk_Edit_Cron_Offload;
diff --git a/package.json b/package.json
new file mode 100755
index 0000000000000000000000000000000000000000..5a00fccd3e902633481250cdf20302e0b2a2f91d
--- /dev/null
+++ b/package.json
@@ -0,0 +1,12 @@
+
+{
+  "name": "bulk-edit-cron-offload",
+  "version": "0.1.0",
+  "main": "Gruntfile.js",
+  "author": "YOUR NAME HERE",
+  "devDependencies": {
+    "grunt": "~0.4.5",
+    "grunt-wp-i18n": "~0.5.0",
+    "grunt-wp-readme-to-markdown": "~1.0.0"
+  }
+}
diff --git a/phpcs.xml.dist b/phpcs.xml.dist
new file mode 100644
index 0000000000000000000000000000000000000000..2c9e091164834d4505603e2f39ec9a96e193ec8e
--- /dev/null
+++ b/phpcs.xml.dist
@@ -0,0 +1,17 @@
+<?xml version="1.0"?>
+<ruleset name="WordPress Coding Standards for Plugins">
+	<description>Generally-applicable sniffs for WordPress plugins</description>
+
+	<rule ref="WordPress-Core" />
+	<rule ref="WordPress-Docs" />
+
+	<!-- Check all PHP files in directory tree by default. -->
+	<arg name="extensions" value="php"/>
+	<file>.</file>
+
+	<!-- Show sniff codes in all reports -->
+	<arg value="s"/>
+
+	<exclude-pattern>*/node_modules/*</exclude-pattern>
+	<exclude-pattern>*/vendor/*</exclude-pattern>
+</ruleset>
diff --git a/phpunit.xml.dist b/phpunit.xml.dist
new file mode 100644
index 0000000000000000000000000000000000000000..2bff769f357d6564eed7ebf3b12b6cdff5647bc9
--- /dev/null
+++ b/phpunit.xml.dist
@@ -0,0 +1,14 @@
+<phpunit
+	bootstrap="tests/bootstrap.php"
+	backupGlobals="false"
+	colors="true"
+	convertErrorsToExceptions="true"
+	convertNoticesToExceptions="true"
+	convertWarningsToExceptions="true"
+	>
+	<testsuites>
+		<testsuite>
+			<directory suffix=".php">./tests/tests/</directory>
+		</testsuite>
+	</testsuites>
+</phpunit>
diff --git a/readme.txt b/readme.txt
new file mode 100755
index 0000000000000000000000000000000000000000..57472658e428a21d8c58cf0079dabbaf0e047823
--- /dev/null
+++ b/readme.txt
@@ -0,0 +1,30 @@
+=== Bulk Edit Cron Offload ===
+Contributors: ethitter, automattic
+Tags: cron, bulk edit
+Requires at least: 4.8.1
+Tested up to: 4.9
+Stable tag: 1.0
+License: GPLv2 or later
+License URI: http://www.gnu.org/licenses/gpl-2.0.html
+
+Process Core's Bulk Edit requests using Cron
+
+== Description ==
+
+Process Core's Bulk Edit requests using Cron, rather than via a `$_GET` request.
+
+== Installation ==
+
+1. Upload the `bulk-edit-cron-offload` directory to the `/wp-content/plugins/` directory
+1. Activate the plugin through the 'Plugins' menu in WordPress
+
+== Frequently Asked Questions ==
+
+= A question that someone might have =
+
+An answer to that question.
+
+== Changelog ==
+
+= 1.0 =
+* Initial release
diff --git a/tests/bootstrap.php b/tests/bootstrap.php
new file mode 100755
index 0000000000000000000000000000000000000000..dde0131d4f82fcad28438b657a4acec782c8545b
--- /dev/null
+++ b/tests/bootstrap.php
@@ -0,0 +1,25 @@
+<?php
+/**
+ * PHPUnit bootstrap file
+ *
+ * @package Bulk_Edit_Cron_Offload
+ */
+
+$_tests_dir = getenv( 'WP_TESTS_DIR' );
+if ( ! $_tests_dir ) {
+	$_tests_dir = '/tmp/wordpress-tests-lib';
+}
+
+// Give access to tests_add_filter() function.
+require_once $_tests_dir . '/includes/functions.php';
+
+/**
+ * Manually load the plugin being tested.
+ */
+function _manually_load_plugin() {
+	require dirname( dirname( __FILE__ ) ) . '/bulk-edit-cron-offload.php';
+}
+tests_add_filter( 'muplugins_loaded', '_manually_load_plugin' );
+
+// Start up the WP testing environment.
+require $_tests_dir . '/includes/bootstrap.php';
diff --git a/tests/tests/class-sampletest.php b/tests/tests/class-sampletest.php
new file mode 100755
index 0000000000000000000000000000000000000000..814ff7bfb0a76a782a230f37a4c60111ad4bc22f
--- /dev/null
+++ b/tests/tests/class-sampletest.php
@@ -0,0 +1,20 @@
+<?php
+/**
+ * Class SampleTest
+ *
+ * @package Bulk_Edit_Cron_Offload
+ */
+
+/**
+ * Sample test case.
+ */
+class SampleTest extends WP_UnitTestCase {
+
+	/**
+	 * A single example test.
+	 */
+	function test_sample() {
+		// Replace this with some actual testing code.
+		$this->assertTrue( true );
+	}
+}