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:
parent
01071866be
commit
3216f5596e
|
@ -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 we’re 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": {}};
|
||||
|
|
|
@ -1,3 +1,6 @@
|
|||
// @flow
|
||||
|
||||
// Set up the environment before we include any other modules.
|
||||
require("../env");
|
||||
|
||||
global.fetch = require("jest-fetch-mock");
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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");
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
Loading…
Reference in New Issue