diff --git a/src/plugins/generic.ts b/src/plugins/generic.ts index 1ff7bfc2ea..4a68450a25 100644 --- a/src/plugins/generic.ts +++ b/src/plugins/generic.ts @@ -8,6 +8,7 @@ import { exec } from "child-process-promise" import * as Joi from "joi" +import { join } from "path" import { joiArray, joiEnvVars, @@ -42,8 +43,10 @@ import { baseTestSpecSchema, } from "../types/test" import { spawn } from "../util/util" +import { TreeVersion, writeVersionFile, readVersionFile } from "../vcs/base" export const name = "generic" +export const buildVersionFilename = ".garden-build-version" export interface GenericTestSpec extends BaseTestSpec { command: string[], @@ -92,8 +95,6 @@ export async function parseGenericModule( } export async function buildGenericModule({ module }: BuildModuleParams): Promise { - // By default we run the specified build command in the module root, if any. - // TODO: Keep track of which version has been built (needs local data store/cache). const config: ModuleConfig = module.config if (config.build.command) { @@ -103,6 +104,14 @@ export async function buildGenericModule({ module }: BuildModuleParams { - // Each module handler should keep track of this for now. - // Defaults to return false if a build command is specified. - return { ready: !module.config.build.command } + if (!module.config.build.command) { + return { ready: true } + } + + const buildVersionFilePath = join(await module.getBuildPath(), buildVersionFilename) + const builtVersion = await readVersionFile(buildVersionFilePath) + const moduleVersion = await module.getVersion() + + if (builtVersion && builtVersion.latestCommit === moduleVersion.versionString) { + return { ready: true } + } + + return { ready: false } }, }, }, diff --git a/src/types/module.ts b/src/types/module.ts index 12a1b14c41..8bb279d75c 100644 --- a/src/types/module.ts +++ b/src/types/module.ts @@ -319,5 +319,3 @@ export class Module< } } } - -export type ModuleConfigType = M["_ConfigType"] diff --git a/src/vcs/base.ts b/src/vcs/base.ts index 91e167eafa..d58c4f03af 100644 --- a/src/vcs/base.ts +++ b/src/vcs/base.ts @@ -14,7 +14,7 @@ import * as Joi from "joi" import { validate } from "../types/common" import { join } from "path" import { GARDEN_VERSIONFILE_NAME } from "../constants" -import { pathExists, readFile } from "fs-extra" +import { pathExists, readFile, writeFile } from "fs-extra" import { ConfigurationError } from "../exceptions" export const NEW_MODULE_VERSION = "0000000000" @@ -74,25 +74,8 @@ export abstract class VcsHandler { async resolveTreeVersion(module: Module): Promise { // the version file is used internally to specify versions outside of source control const versionFilePath = join(module.path, GARDEN_VERSIONFILE_NAME) - const versionFileContents = await pathExists(versionFilePath) - && (await readFile(versionFilePath)).toString().trim() - - if (!!versionFileContents) { - try { - return validate(JSON.parse(versionFileContents), treeVersionSchema) - } catch (err) { - throw new ConfigurationError( - `Unable to parse ${GARDEN_VERSIONFILE_NAME} as valid version file in module directory ${module.path}`, - { - modulePath: module.path, - versionFilePath, - versionFileContents, - }, - ) - } - } else { - return this.getTreeVersion([module.path]) - } + const fileVersion = await readVersionFile(versionFilePath) + return fileVersion || this.getTreeVersion([module.path]) } async resolveVersion(module: Module, dependencies: Module[]): Promise { @@ -170,3 +153,33 @@ function hashVersions(versions: NamedTreeVersion[]) { // this format is kinda arbitrary, but prefixing the "v" is useful to visually spot hashed versions return "v" + versionHash.digest("hex").slice(0, 10) } + +export async function readVersionFile(path: string): Promise { + if (!(await pathExists(path))) { + return null + } + + // this is used internally to specify version outside of source control + const versionFileContents = (await readFile(path)).toString().trim() + + if (!versionFileContents) { + return null + } + + try { + return validate(JSON.parse(versionFileContents), treeVersionSchema) + } catch (error) { + throw new ConfigurationError( + `Unable to parse ${path} as valid version file`, + { + path, + versionFileContents, + error, + }, + ) + } +} + +export async function writeVersionFile(path: string, version: TreeVersion) { + await writeFile(path, JSON.stringify(version)) +} diff --git a/test/src/commands/push.ts b/test/src/commands/push.ts index 564487bc56..57c9acea72 100644 --- a/test/src/commands/push.ts +++ b/test/src/commands/push.ts @@ -76,11 +76,13 @@ testProviderNoPush.pluginName = "test-plugin" async function getTestContext() { const garden = await Garden.factory(projectRootB, { plugins: [testProvider] }) + await garden.clearBuilds() return garden.pluginContext } describe("PushCommand", () => { // TODO: Verify that services don't get redeployed when same version is already deployed. + it("should build and push modules in a project", async () => { const ctx = await getTestContext() const command = new PushCommand() @@ -171,6 +173,7 @@ describe("PushCommand", () => { it("should fail gracefully if module does not have a provider for push", async () => { const ctx = await makeTestContextA() + await ctx.clearBuilds() const command = new PushCommand() diff --git a/test/src/plugins/generic.ts b/test/src/plugins/generic.ts new file mode 100644 index 0000000000..f069148a74 --- /dev/null +++ b/test/src/plugins/generic.ts @@ -0,0 +1,74 @@ +import { expect } from "chai" +import { + join, + resolve, +} from "path" +import { Garden } from "../../../src/garden" +import { PluginContext } from "../../../src/plugin-context" +import { + gardenPlugin, +} from "../../../src/plugins/container" +import { + buildVersionFilename, +} from "../../../src/plugins/generic" +import { Environment } from "../../../src/types/common" +import { + readVersionFile, + writeVersionFile, +} from "../../../src/vcs/base" +import { + dataDir, + makeTestGarden, +} from "../../helpers" + +describe("generic plugin", () => { + const projectRoot = resolve(dataDir, "test-project-a") + const moduleName = "module-a" + + let garden: Garden + let ctx: PluginContext + let env: Environment + + beforeEach(async () => { + garden = await makeTestGarden(projectRoot, [gardenPlugin]) + ctx = garden.pluginContext + env = garden.getEnvironment() + await garden.clearBuilds() + }) + + describe("getModuleBuildStatus", () => { + it("should read a build version file if it exists", async () => { + const module = await ctx.getModule(moduleName) + const version = await module.getVersion() + const buildPath = await module.getBuildPath() + const versionFilePath = join(buildPath, buildVersionFilename) + + await writeVersionFile(versionFilePath, { + latestCommit: version.versionString, + dirtyTimestamp: version.dirtyTimestamp, + }) + + const result = await ctx.getModuleBuildStatus({ moduleName }) + + expect(result.ready).to.be.true + }) + }) + + describe("buildModule", () => { + it("should write a build version file after building", async () => { + const module = await ctx.getModule(moduleName) + const version = await module.getVersion() + const buildPath = await module.getBuildPath() + const versionFilePath = join(buildPath, buildVersionFilename) + + await ctx.buildModule({ moduleName }) + + const versionFileContents = await readVersionFile(versionFilePath) + + expect(versionFileContents).to.eql({ + latestCommit: version.versionString, + dirtyTimestamp: version.dirtyTimestamp, + }) + }) + }) +})