Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .github/workflows/onPushToMain.yml
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,11 @@ jobs:
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: latest
- run: |
npm install -g corepack --force
corepack enable
- name: Check if version already exists
id: version-check
run: |
Expand Down
3 changes: 3 additions & 0 deletions .github/workflows/onRelease.yml
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,9 @@ jobs:
- uses: actions/setup-node@v4
with:
node-version: latest
- run: |
npm install -g corepack --force
corepack enable
- run: yarn
- uses: JS-DevTools/npm-publish@19c28f1ef146469e409470805ea4279d47c3d35c
with:
Expand Down
4 changes: 3 additions & 1 deletion .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,9 @@ jobs:
- uses: actions/setup-node@v4
with:
node-version: ${{ matrix.node_version }}
cache: yarn
- run: |
npm install -g corepack --force
corepack enable
- run: yarn
- run: yarn build
- run: yarn test
159 changes: 84 additions & 75 deletions src/commands/commit/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,81 +43,90 @@ export default class Commit extends Command {

this.log(`[tf-patch] Found module "${module}"`)

dir({keep: false, unsafeCleanup: true}, async (err, dirA, cleanup) => {
if (err) {
throw err
}

await fse.copy(moduleDir, dirA)
await fse.remove(path.join(dirA, '.git'))

this.log(`[tf-patch] Diffing source module and patch dir: \nA: ${dirA}\nB: ${dirB}`)
// Using yarn as reference - https://github.com/yarnpkg/berry/blob/7fab4f101d1b8a98691efe14696d18ce00ecd234/packages/plugin-patch/sources/patchUtils.ts#L281-L298
const gitDiff = await spawn(
`git`,
[
`-c`,
`core.safecrlf=false`,
`diff`,
`--src-prefix=a/`,
`--dst-prefix=b/`,
`--ignore-cr-at-eol`,
`--full-index`,
`--no-index`,
`--no-renames`,
`--text`,
dirA,
dirB,
],
{
env: {
...process.env,
// #region Predictable output
// These variables aim to ignore the global git config so we get predictable output
// https://git-scm.com/docs/git#Documentation/git.txt-codeGITCONFIGNOSYSTEMcode
GIT_CONFIG_NOSYSTEM: `1`,
HOME: ``,
USERPROFILE: ``,
XDG_CONFIG_HOME: ``,
// #endregion
},
},
)
const stderr = (await gitDiff.stderr.toArray()).join('\n')
// we cannot rely on exit code, because --no-index implies --exit-code
// i.e. git diff will exit with 1 if there were differences
if (stderr.length > 0) {
throw new Error(
`Unable to diff directories. Make sure you have a recent version of 'git' available in PATH.\nThe following error was reported by 'git':\n${stderr}`,
)
}

const controller = new AbortController()
const timer = setTimeout(() => {
this.log('[tf-patch] Timed out creating patch')
controller.abort()
}, 10_000)
const stdoutArr = await gitDiff.stdout.toArray({signal: controller.signal})
clearTimeout(timer)

const stdout = stdoutArr
.join('\n')
.replace(new RegExp(`(a|b)(${_.escapeRegExp(`/${normalizePath(dirA)}/`)})`, `g`), `$1/`)
.replace(new RegExp(`(a|b)${_.escapeRegExp(`/${normalizePath(dirB)}/`)}`, `g`), `$1/`)
.replace(new RegExp(_.escapeRegExp(`${dirA}/`), `g`), ``)
.replace(new RegExp(_.escapeRegExp(`${dirB}/`), `g`), ``)

if (!stdout.trim()) {
throw new Error(`No changes detected in module "${module}"`)
}

await fse.ensureDir(PATCHES_DIR)
const patchFile = path.join(PATCHES_DIR, `${module}.patch`)
await writeFile(patchFile, stdout)
this.log(`[tf-patch] Created patch file: ${patchFile}`)
this.log(`[tf-patch] Now you can apply the patches after "terraform init" by running "tf-patch init"`)

cleanup()
await new Promise<void>((resolve, reject) => {
dir({keep: false, unsafeCleanup: true}, async (err, dirA, cleanup) => {
if (err) {
reject(err)
return
}

try {
await fse.copy(moduleDir, dirA)
await fse.remove(path.join(dirA, '.git'))

this.log(`[tf-patch] Diffing source module and patch dir: \nA: ${dirA}\nB: ${dirB}`)
// Using yarn as reference - https://github.com/yarnpkg/berry/blob/7fab4f101d1b8a98691efe14696d18ce00ecd234/packages/plugin-patch/sources/patchUtils.ts#L281-L298
const gitDiff = await spawn(
`git`,
[
`-c`,
`core.safecrlf=false`,
`diff`,
`--src-prefix=a/`,
`--dst-prefix=b/`,
`--ignore-cr-at-eol`,
`--full-index`,
`--no-index`,
`--no-renames`,
`--text`,
dirA,
dirB,
],
{
env: {
...process.env,
// #region Predictable output
// These variables aim to ignore the global git config so we get predictable output
// https://git-scm.com/docs/git#Documentation/git.txt-codeGITCONFIGNOSYSTEMcode
GIT_CONFIG_NOSYSTEM: `1`,
HOME: ``,
USERPROFILE: ``,
XDG_CONFIG_HOME: ``,
// #endregion
},
},
)
const stderr = (await gitDiff.stderr.toArray()).join('\n')
// we cannot rely on exit code, because --no-index implies --exit-code
// i.e. git diff will exit with 1 if there were differences
if (stderr.length > 0) {
throw new Error(
`Unable to diff directories. Make sure you have a recent version of 'git' available in PATH.\nThe following error was reported by 'git':\n${stderr}`,
)
}

const controller = new AbortController()
const timer = setTimeout(() => {
this.log('[tf-patch] Timed out creating patch')
controller.abort()
}, 10_000)
const stdoutArr = await gitDiff.stdout.toArray({signal: controller.signal})
clearTimeout(timer)

const stdout = stdoutArr
.join('\n')
.replace(new RegExp(`(a|b)(${_.escapeRegExp(`/${normalizePath(dirA)}/`)})`, `g`), `$1/`)
.replace(new RegExp(`(a|b)${_.escapeRegExp(`/${normalizePath(dirB)}/`)}`, `g`), `$1/`)
.replace(new RegExp(_.escapeRegExp(`${dirA}/`), `g`), ``)
.replace(new RegExp(_.escapeRegExp(`${dirB}/`), `g`), ``)

if (!stdout.trim()) {
throw new Error(`No changes detected in module "${module}"`)
}

await fse.ensureDir(PATCHES_DIR)
const patchFile = path.join(PATCHES_DIR, `${module}.patch`)
await writeFile(patchFile, stdout)
this.log(`[tf-patch] Created patch file: ${patchFile}`)
this.log(`[tf-patch] Now you can apply the patches after "terraform init" by running "tf-patch init"`)

cleanup()
resolve()
} catch (error) {
cleanup()
reject(error)
}
})
})
}
}
50 changes: 29 additions & 21 deletions src/commands/patch/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,27 +37,35 @@ intended to be editable at will.`
throw new Error(`Module "${module}" not found in terraform modules directory`)
}

dir(async (err, tmpDir) => {
if (err) {
throw err
}

await writeFile(
path.join(tmpDir, PATCH_DATA_FILE),
JSON.stringify({
module,
}),
)

const tmpModule = path.join(tmpDir, 'module')
await fse.ensureDir(tmpModule)
await fse.copy(moduleDir, tmpModule)
await fse.remove(path.join(tmpModule, '.git'))

this.log(`[tf-patch] You can now edit the following folder: ${tmpModule}`)
this.log(
`[tf-patch] Once you are done run "tf-patch commit ${tmpModule}" and we will store a patchfile based on your changes.`,
)
await new Promise<void>((resolve, reject) => {
dir(async (err, tmpDir) => {
if (err) {
reject(err)
return
}

try {
await writeFile(
path.join(tmpDir, PATCH_DATA_FILE),
JSON.stringify({
module,
}),
)

const tmpModule = path.join(tmpDir, 'module')
await fse.ensureDir(tmpModule)
await fse.copy(moduleDir, tmpModule)
await fse.remove(path.join(tmpModule, '.git'))

this.log(`[tf-patch] You can now edit the following folder: ${tmpModule}`)
this.log(
`[tf-patch] Once you are done run "tf-patch commit ${tmpModule}" and we will store a patchfile based on your changes.`,
)
resolve()
} catch (error) {
reject(error)
}
})
})
}
}
61 changes: 61 additions & 0 deletions test/commands/commit/index.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
import {runCommand} from '@oclif/test'
import {expect} from 'chai'
import * as fse from 'fs-extra'
import {mkdtempSync} from 'node:fs'
import {readFile, writeFile} from 'node:fs/promises'
import {tmpdir} from 'node:os'
import path from 'node:path'

import {PATCHES_DIR} from '../../../src/commands/commit/index.js'
import {PATCH_DATA_FILE, TERRAFORM_MODULES_DIR} from '../../../src/commands/patch/index.js'

describe('commit', () => {
let tmpDir: string
let originalCwd: string

before(() => {
originalCwd = process.cwd()
tmpDir = mkdtempSync(path.join(tmpdir(), 'tf-patch-commit-test-'))
})

after(async () => {
process.chdir(originalCwd)
await fse.remove(tmpDir)
})

it('throws an error when folder is not found', async () => {
const {error} = await runCommand('commit /nonexistent-folder-xyz-abc')
expect(error?.message).to.contain('not found')
})

it('creates a patch file from a modified module', async () => {
process.chdir(tmpDir)

const module = 'test-module'
const moduleDir = path.join(tmpDir, TERRAFORM_MODULES_DIR, module)
await fse.ensureDir(moduleDir)
await writeFile(path.join(moduleDir, 'main.tf'), 'resource "null_resource" "original" {}\n')

// Create the patched folder that commit expects
const patchedDir = mkdtempSync(path.join(tmpdir(), 'tf-patch-patched-'))
const moduleFolder = path.join(patchedDir, 'module')
await fse.ensureDir(moduleFolder)
await writeFile(path.join(moduleFolder, 'main.tf'), 'resource "null_resource" "modified" {}\n')
await writeFile(
path.join(patchedDir, PATCH_DATA_FILE),
JSON.stringify({module}),
)

try {
const {stdout} = await runCommand(`commit ${moduleFolder}`)
expect(stdout).to.contain('Created patch file')
const patchFile = path.join(tmpDir, PATCHES_DIR, `${module}.patch`)
expect(await fse.pathExists(patchFile)).to.equal(true)
const patchContent = await readFile(patchFile, 'utf8')
expect(patchContent).to.contain('original')
expect(patchContent).to.contain('modified')
} finally {
await fse.remove(patchedDir)
}
})
})
9 changes: 0 additions & 9 deletions test/commands/hello/index.test.ts

This file was deleted.

9 changes: 0 additions & 9 deletions test/commands/hello/world.test.ts

This file was deleted.

Loading
Loading