Add `GitState`, `Environment` to the `VersionInfo` (#692)

Summary:
The version number displayed in the application now displays much more
specific information. It now lists the Git commit from which the build
was constructed, and will identify whether we have accidentally deployed
a development instance (which would be slow) or an instance with
uncommitted changes (which would be bad).

The version information is computed during the initialization of the
Webpack config. For development, this means that it is computed when you
run `yarn start`, and not updated thenafter. If the stale information
presents actual confusion, we would need to backport Webpack 4’s support
for runtime values in `DefinePlugin` to Webpack 3 (or upgrade Webpack
by a major version).

Test Plan:
The logic for `GitState` and `Environment` has existing tests. With both
a clean tree and a dirty tree, run `yarn start` and build the static
site, and check that the resulting versions are correct.

wchargin-branch: use-rich-version-types
This commit is contained in:
William Chargin 2018-08-16 13:38:13 -07:00 committed by GitHub
parent 01071866be
commit 3216f5596e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 127 additions and 9 deletions

View File

@ -1,8 +1,13 @@
// @flow
const {spawnSync, execFileSync} = require("child_process");
const fs = require("fs");
const stringify = require("json-stable-stringify");
const path = require("path");
const paths = require("./paths");
/*:: import type {GitState} from "../src/app/version"; */
// Make sure that including paths.js after env.js will read .env variables.
delete require.cache[require.resolve("./paths")];
@ -55,11 +60,69 @@ process.env.NODE_PATH = (process.env.NODE_PATH || "")
.map((folder) => path.resolve(appDirectory, folder))
.join(path.delimiter);
// Get the state of the SourceCred Git repository. This requires that
// Git be installed. If this fails for you, please install Git.
//
// If the dependency on Git becomes a problem, we can consider making
// this optional. However, note that this computation is performed at
// build time, so end users of SourceCred as a library or application
// should not need this dependency.
function getGitState() /*: GitState */ {
const env = {
LANG: "C",
LC_ALL: "C",
TZ: "UTC",
GIT_CONFIG_NOSYSTEM: "1",
GIT_ATTR_NOSYSTEM: "1",
};
const diffIndex = spawnSync(
"git",
["-C", __dirname, "diff-index", "--quiet", "HEAD", "--"],
{env}
);
const dirty = diffIndex.status !== 0;
if (diffIndex.status !== 0 && diffIndex.status !== 1) {
throw new Error(diffIndex.status + ": " + diffIndex.stderr.toString());
}
const commitHash = execFileSync(
"git",
["-C", __dirname, "rev-parse", "--short=12", "--verify", "HEAD"],
{env}
)
.toString()
.trim();
const commitTimestamp = execFileSync(
"git",
[
"-C",
__dirname,
"show",
"--no-patch",
"--format=%cd",
"--date=format:%Y%m%d-%H%M",
commitHash,
],
{env}
)
.toString()
.trim();
return {commitHash, commitTimestamp, dirty};
}
const SOURCECRED_GIT_STATE = stringify(getGitState());
process.env.SOURCECRED_GIT_STATE = SOURCECRED_GIT_STATE;
function getClientEnvironment() {
const raw = {};
// Useful for determining whether were running in production mode.
// Most importantly, it switches React into the correct mode.
raw.NODE_ENV = process.env.NODE_ENV || "development";
// Used by `src/app/version.js`.
raw.SOURCECRED_GIT_STATE = SOURCECRED_GIT_STATE;
// Stringify all values so we can feed into Webpack's DefinePlugin.
const stringified = {"process.env": {}};

View File

@ -1,3 +1,6 @@
// @flow
// Set up the environment before we include any other modules.
require("../env");
global.fetch = require("jest-fetch-mock");

View File

@ -9,7 +9,7 @@ import GithubLogo from "./GithubLogo";
import TwitterLogo from "./TwitterLogo";
import {routeData} from "./routeData";
import * as NullUtil from "../util/null";
import {VERSION} from "./version";
import {VERSION_SHORT, VERSION_FULL} from "./version";
export default class Page extends React.Component<{|
+assets: Assets,
@ -71,7 +71,9 @@ export default class Page extends React.Component<{|
</div>
<footer className={css(style.footer)}>
<div className={css(style.footerWrapper)}>
<span className={css(style.footerText)}>{VERSION}</span>
<span className={css(style.footerText)}>
({VERSION_FULL}) <strong>{VERSION_SHORT}</strong>
</span>
</div>
</footer>
</React.Fragment>

View File

@ -4,6 +4,8 @@ export type VersionInfo = {|
+major: number,
+minor: number,
+patch: number,
+gitState: GitState,
+environment: Environment,
|};
export type GitState = {|
+commitHash: string,
@ -38,6 +40,7 @@ export function parseGitState(raw: ?string): GitState {
}
return gitState;
}
const gitState = parseGitState(process.env.SOURCECRED_GIT_STATE);
/**
* Parse the given string as an `Environment`, throwing an error if it
@ -51,15 +54,30 @@ export function parseEnvironment(raw: ?string): Environment {
}
return raw;
}
const environment = parseEnvironment(process.env.NODE_ENV);
export const VERSION_INFO = Object.freeze({
export const VERSION_INFO: VersionInfo = Object.freeze({
major: 0,
minor: 0,
patch: 0,
gitState,
environment,
});
export function format(info: VersionInfo): string {
export function formatShort(info: VersionInfo): string {
return `v${info.major}.${info.minor}.${info.patch}`;
}
export const VERSION = format(VERSION_INFO);
export function formatFull(info: VersionInfo): string {
const parts = [
`v${info.major}.${info.minor}.${info.patch}`,
info.gitState.commitHash,
info.gitState.commitTimestamp,
info.gitState.dirty ? "dirty" : "clean",
info.environment,
];
return parts.join("-");
}
export const VERSION_SHORT: string = formatShort(VERSION_INFO);
export const VERSION_FULL: string = formatFull(VERSION_INFO);

View File

@ -1,11 +1,16 @@
// @flow
import {type Environment, parseEnvironment, parseGitState} from "./version";
import {
type Environment,
type VersionInfo,
formatFull,
formatShort,
parseEnvironment,
parseGitState,
} from "./version";
describe("app/version", () => {
// Like `VersionInfo`, but with some extra properties that will
// shortly be added to that type.
const version = () => ({
const version = (): VersionInfo => ({
major: 3,
minor: 13,
patch: 37,
@ -106,4 +111,31 @@ describe("app/version", () => {
expect(() => parseEnvironment("wat")).toThrow('environment: "wat"');
});
});
describe("formatShort", () => {
it("includes the major, minor, and patch versions", () => {
expect(formatShort(version())).toContain("3.13.37");
});
it("does not include the Git hash", () => {
expect(formatShort(version())).not.toContain("d0e1");
});
it("does not include the Node environment", () => {
expect(formatShort(version())).not.toContain("-test");
});
});
describe("formatFull", () => {
it("includes the major, minor, and patch versions", () => {
expect(formatFull(version())).toContain("3.13.37");
});
it("includes the Git hash and timestamp", () => {
expect(formatFull(version())).toContain("d0e1a2d3b4e5-20010203-0405");
});
it("includes the dirty state", () => {
expect(formatFull(version())).toContain("-dirty");
});
it("includes the Node environment", () => {
expect(formatFull(version())).toContain("-test");
});
});
});