Learn about our RFC process, Open RFC meetings & more.Join in the discussion! »

@sjmdev/sjm-deployment-tools

0.9.3 • Public • Published

SJM Deployment Tools

Table of Contents

About

SJM Deployment Tools contains procedures and scripts which aid in the deployment of various SJM projects.

Installation

Install sjm-deployment-tools via npm package registry as follows:

npm install --save-dev @sjmdev/sjm-deployment-tools

Setup

Import the sjm-deployment-tools module into a given file as follows:

const sjmDeploymentTools = require( '@sjmdev/sjm-deployment-tools' );

Alternatively, the submodules of the sjmDeploymentTools package may be imported individually.

// Using property access:
const sjmChangelog = sjmDeploymentTools.sjmChangelog;

// Using object destructuring:
const { sjmChangelog } = sjmDeploymentTools;

Usage

Each of the submodules exposed by sjmDeploymentTools can be used in the following ways:

  • Imported (eg. require( 'sjmDeploymentTools' ).sjmChangelog)
  • Invoked via CLI command (eg. sjm-changelog).

For a detailed overview of each submodule, see the documentation section below.

Import

This usage method makes the submodule and its public API available to the importing script. The API and internal functions for a given submodule live in /lib/{{ SUBMODULE }}/index.js.

Invoke

This usage method allows for the core API to be accessed via the command line. The CLI wrapper for a given submodule lives in /lib/{{ SUBMODULE }}/bin.js.

The help menu for a given submodule can be accessed by including the --help flag. For example: sjm-changelog --help.

Documentation

sjmChangelog

The sjmChangelog submodule exposes the following methods:

getChangelogEntry( entryIdentifier?. options? )

Scans the root directory of the current repository for a CHANGELOG.md file, returns a Promise which will contain one of the following:

  • If no file is found:
    • Promise will contain an error.
  • If a file is found:
    • If invoked without arguments: Promise will contain the latest entry.
    • If invoked with the argument 'Unreleased': Promise will contain the 'Unreleased' entry if it exists, error otherwise.
    • If invoked with a valid version (eg. 1.0.0): Promise will contain the corresponding entry if it exists, error otherwise.

If invoked with the skipUnreleased option/flag, getChangelogEntry() will return the most recent named version.

// From the command line.
sjm-changelog --skip-unreleased
// From within a dependent script.
sjmChangelog.getChangelogEntry( null, { skipUnreleased: true } );

broadcastChangelogEntry( entryObj, options? )

Given an entry data object (eg. the value returned by getChangelogEntry()), method posts the data to Slack and returns a Promise. In order for broadcastChangelogEntry() to function correctly, the Slack API endpoint must be provided using either of the following methods:

  • Via the sjm field of the package.json file. See below:
...
"sjm": {
  "slack": {
    "webhooks": {
      "default": "https://..."
    }
  }
}
...
  • As a key within the options object: { endpoint: '...' }

The Promise returned by broadcastChangelogEntry() will contain one of the following:

  • Promise will contain an error if any of the following are true:
    • Method is invoked without an entry data object.
    • Slack API endpoint does not exist.
    • Slack API rejects request.
  • Otherwise, Promise will contain a status code (eg. '200').

Under the hood, broadcastChangelogEntry() converts the CHANGELOG entry data into a format which can be posted to the Slack API. As part of this process, broadcastChangelogEntry() attempts to convert any plaintext JIRA ticket references (eg. PROJ-123) to embedded links. In order for this functionality to work correctly, the dependent project must contain:

  • a package.json file.
  • a bugs fields which meets the following criteria:
    • Is a valid URL.
    • Points to the JIRA homepage for the dependent project.
    • Includes the dependent project's JIRA identifier as the final path segment.
// package.json
...
"bugs": "https://scl.atlassian.net/browse/PROJ"
...
// CHANGELOG.md
- Fixed the thing. PROJ-1.

Given the examples above, the PROJ-1 ticket reference would link to the following: https://scl.atlassian.net/browse/PROJ-1.

Any messages posted to Slack via broadcastChangelogEntry() will be attributed to the webhook's default user (specified at webhook creation). This behaviour can be overriden by providing a username key within the sjm.slack object.

...
"sjm": {
  "slack": {
    "username": "My Custom Username",
    "webhooks": {
      "default": "https://..."
    }
  }
}
...

Given the example above, any messages posted to Slack would be attributed to "My Custom Username".

sjmDeploy

The sjmDeploy submodule exposes the following methods:

doDeploy()

Deploys the latest version of a given site (note: script must be executed within a git repository). Deployment process includes the following steps:

  • Switches to dedicated 'deploy' user (spec'd by sjm.deploy.user field within package.json).
  • Sets 'deploy' user to owner of ALL files within repository (custom permissions can be configured via sjm.deploy.permissionOverrides field within package.json).
  • Fetches updates.
  • Checks out stable branch.
  • Pulls updates from origin/stable.

doDeploy() returns a Promise which will containing one of the following:

  • Promise will contain an error if any of the following are true:
    • Command is not run as sudo.
    • Permission set/reset fails.
    • 'deploy user' is unavailable or invalid.
    • Fetch/checkout/pull fails.
  • Otherwise, Promise will contain an object of 'deployment data' with the following shape:
{
    commits: {
        pre: '12345678', // The latest commit SHA, prior to deployment.
        post: 'abcdef12', // The latest commit SHA, following deployment.
    },
    version: 'v0.0.0', // The deployed version.
    branch: 'stable', // The current branch, following deployment.
}

When executed from the command line, sjm-deploy will exit/abort if the pre and post commit SHAs are equal (eg. deployment process did not introduce any new commits).

When executed from the command line, the --restart flag may be provided in order to invoke doRestart() upon completion of the deployment process.

In order to apply custom permissions at deploy time, doDeploy() checks the package.json file for a permissionOverrides array (located at sjm.deploy.permissionOverrides). If the array exists and contains one or more valid 'permission override' objects (see examples below), doDeploy() will attempt to apply the custom permissions.

...
"sjm": {
  "deploy": {
    "user": "deploy",
    "permissionOverrides": [
      {
        path: "path/to/folder", // Path from the root of the repository to the target file/folder.
        "user": "johnsmith" // The desired owner for the file/folder located at "path".
      }
    ]
  }
}
...

doRestart( data? )

Used to restart/reload services after the deployment is complete. By default, doRestart will perform the following actions:

  • apache2 reload
  • varnish restart

The default values can be overridden via the sjm.deploy.services field within the package.json file. For example:

...
"sjm": {
  "deploy": {
    "services": [
      {
        "name": "serviceA",
        "action": "restart"
      },
      {
        "name": "serviceB",
        "action": "reload"
      }
    ]
  }
}
...

On completion, doRestart() returns a Promise which will contain one of the following:

  • Promise will contain an error if any of the services cannot be restarted/reloaded.
  • Otherwise, Promise will contain an object.

If invoked with an object of 'deployment data', the resolved Promise will contain a decorated version of the object.

sjmRelease

The sjmRelease submodule exposes the following methods:

doRelease( entryIdentifier )

Given a valid release identifier (ie. 'major', 'minor', or 'patch'), doRelease() rolls a new release and returns the result as a Promise.

doRelease() can be invoked directly from the command line as follows:

npm run sjm-release {{ RELEASE IDENTIFIER }}

// NOTE:
// - Where {{ RELEASE IDENTIFIER }} is one of: major; minor; patch.
// - Command must be run from the 'master' branch.
// - Command will abort if run as `sudo`.

During execution, doRelease() completes the following steps:

  • Identifies the new, semver-compliant release identifier.
  • Updates the version field within the project's package.json file.
  • Generates a new commit whose message matches the new release identifier.
  • Tags the new commit.

Since doRelease() invokes npm version {{ RELEASE IDENTIFIER }} under the hood, a given project can make use of the preversion, version, and postversion hooks exposed by npm. For example, the scripts field for a project may look something like this:

...
"scripts": {
  "sjm-release": "sjm-release",
  "version": "sjm-version"
  "postversion": "sjm-postversion"
}
...

Given the example above, running npm run sjm-release {{ RELEASE IDENTIFIER }} would complete the default release process, as well as any logic defined in the sjm-version and sjm-postversion scripts.

sjmRollback

The sjmRollback submodule exposes the following methods:

doRollback( options? )

Method resets the git history to the commit which matches the requested release, returns the result as a Promise.

If invoked with 0x arguments, method attempts to reset to the most recent release (excluding HEAD). For example:

// From the command line:
sjm-rollback

// From within a parent script:
sjmRollback.doRollback();

If invoked with an options object which contains a valid release identifier, method resets to the specified release. For example:

// From the command line:
sjm-rollback --release=1.0.0

// From within a parent script:
sjmRollback.doRollback( { release: '1.0.0' } );

If the reset is successful, Promise will resolve with a resultObj that has the following shape:

{
    msg: '...',
    settings: {
        ...
    }
}

If executed in 'dry run' mode, doRollback() method will perform all steps (argument validation, release scrubbing, etc.) OTHER THAN resetting the git history. This mode yields a response of the same shape (eg. either a resultObj or an error).

// From the command line:
sjm-rollback --dry-run

// From within a parent script:
sjmRollback.doRollback( { dryRun: true } );

If the reset is unsuccessful, Promise will reject with a descriptive error. The reset may be unsuccessful for any of the following reasons:

  • git command does not exist.
  • Current working directory is not a git repository.
  • Current git repository does not contain any commits.
  • Method was invoked with an invalid release value.
  • Fetching of git commits resulted in an error.
  • Git history does not contain any releases.
  • Git history does not contain target/requested release.
  • Current commit is the only release (eg. cannot rollback).
  • Resetting of history resulted in an error.

sjmPreVersion

The sjmPreVersion submodule is intended to be run as part of a running npm version process at the preversion hook. It performs various tests on the current repo, and aborts npm version if any of these tests are true:

  • the current package is sjm-genus. sjm-genus has its own version script which does NOT update the package version.
  • the repo is not on the master branch.
  • the project does not have a CHANGELOG.md file.
  • the CHANGELOG.md does not have an [Unreleased] line.
  • [Unreleased] section is empty.

If any of these conditions are true, npm version will abort and you will have to fix the repo manually. Please note, sjmPreVersion always expects CHANGELOG.md to be in the root of the current project.

Aside from requiring this package within your project, there are some other changes that need to be made to your project in order to consume the sjmPreVersion script.

First, you need to refer to it within the scripts section of your package.json:

...

  "scripts": {
    "preversion": "sjm-preversion"
  },
...

sjmVersion

The sjmVersion submodule is intended to be run as part of a running npm version process at the version hook. It performs some extra file operations beyond what npm version does:

  • replaces the [Unreleased] section in the CHANGELOG.md file with the new version number/date stamp (eg. [1.0.0] - 1970-01-01).
  • inserts an empty [Unreleased] section into the CHANGELOG.md file.
  • optionally updates one or more 'manifest' files with the new version number.
  • stages updated files (to be committed alongside updated package.json file).

Please note, sjmPreVersion always expects CHANGELOG.md to be in the root of the current project.

Aside from requiring this package within your project, there are some other changes that need to be made in order to consume the sjmVersion script.

First, you need to refer to it within the scripts section of your package.json:

...

  "scripts": {
    "version": "sjm-version"
  },
...

To optionally update one or more 'manifest' files, the package.json file must contain a manifests array (located at sjm.manifests). If the array exists and contains one or more valid 'manifest' objects (see examples below), sjmVersion will attempt to update and stage the specified files.

...
"sjm": {
  "manifests": [
    {
      "path": "path/to/file.ext", // Path from the root of the repository to the target file.
      "regex": (Version:\s?)([0-9]+.[0-9]+.[0-9]+) // Regular expression which will be applied to the target file. Please note: regex must be composed of capture groups; regex must include a semver capture group.
    }
  ]
}
...

If the manifests array is provided, all 'manifest objects' must include a valid path key. The regex key may be ommitted, in which case the default value will be used (see example above). If any of the 'manifest objects' are invalid (eg. missing/invalid path key, regex does not yield matches, etc.), sjmVersion will fail.

Additionally, the empty [Unreleased] section can be customized by including an UNRELEASED.md file in the root of the project. If the file exists, its contents will be inserted into the changelog. If the project does not contain an UNRELEASED.md file, a default section will be used instead.

sjmPostVersion

The sjmPostVersion submodule is intended to be run as part of a running npm version process at the postversion hook. It pushes the release commit and its tag up to the master branch:

git push origin master --tags

Aside from requiring this package within your project, there are some other changes that need to be made to your project in order to consume the sjmPostVersion script. You need to refer to it within the scripts section of your package.json:

...

  "scripts": {
    "version": "sjm-postversion"
  },
...

Development

Overview

Include below are a selection of notes, instructions, and general points to keep in mind when working on/updating this project.

Code Style

ESlint is used to enforce a consistent code style. Providing that all development dependencies have been installed, the project can be linted by running the following command:

gulp scripts:lint

Running this command will output a list of warnings/errors. Some of these errors may be fixable by ESLint itself (eg. indentation, spacing around keywords, etc.). This can be achived by running the following command:

gulp scripts:lint:fix

Please note that any errors which remain must be fixed manually.

Testing

Providing that all development dependencies have been installed, the test suite can be run as follows:

npm test

The repository also includes a bitbucket-pipelines.yml file, which will case the test suite to be run when a given update is pushed to the remote repo.

Keywords

none

Install

npm i @sjmdev/sjm-deployment-tools

DownloadsWeekly Downloads

28

Version

0.9.3

License

ISC

Unpacked Size

85.3 kB

Total Files

33

Last publish

Collaborators

  • avatar
  • avatar
  • avatar
  • avatar
  • avatar