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
6 changes: 6 additions & 0 deletions packages/cli/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
# @openfn/cli

## 1.30.0

### Minor Changes

- Add a `project clean` command

## 1.29.2

### Patch Changes
Expand Down
2 changes: 1 addition & 1 deletion packages/cli/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@openfn/cli",
"version": "1.29.2",
"version": "1.30.0",
"description": "CLI devtools for the OpenFn toolchain",
"engines": {
"node": ">=18",
Expand Down
2 changes: 2 additions & 0 deletions packages/cli/src/commands.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ export type CommandList =
| 'execute'
| 'metadata'
| 'project-checkout'
| 'project-clean'
| 'project-deploy'
| 'project-fetch'
| 'project-list'
Expand Down Expand Up @@ -74,6 +75,7 @@ const handlers = {
['project-version']: projects.version,
['project-merge']: projects.merge,
['project-checkout']: projects.checkout,
['project-clean']: projects.clean,
['project-fetch']: projects.fetch,
version: async (opts: Opts, logger: Logger) =>
printVersions(logger, opts, true),
Expand Down
54 changes: 54 additions & 0 deletions packages/cli/src/projects/clean.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
import yargs from 'yargs';
import { Workspace } from '@openfn/project';
import { rimraf } from 'rimraf';

import { build, ensure } from '../util/command-builders';
import { handler as checkout } from './checkout';
import type { Logger } from '../util/logger';
import * as o from '../options';
import * as po from './options';

import type { Opts } from './options';

export type CleanOptions = Pick<
Opts,
'command' | 'workspace' | 'log' | 'confirm' | 'force'
>;

const options = [o.log, o.confirm, o.force, po.workspace];

const command: yargs.CommandModule = {
command: 'clean',
describe:
'Delete the workflows folder and re-checkout the currently active project',
handler: ensure('project-clean', options),
builder: (yargs) => build(options, yargs),
};

export default command;

export const handler = async (options: CleanOptions, logger: Logger) => {
const workspacePath = options.workspace ?? process.cwd();
const workspace = new Workspace(workspacePath, logger);

const skip = options.force || options.confirm === false;
const doIt = await logger.confirm(
`This will delete all files in ${workspace.workflowsPath}. Do you want to proceed?`,
skip
);
if (!doIt) {
return;
}

await rimraf(workspace.workflowsPath);

const activeProject = workspace.activeProject;
if (!activeProject) {
throw new Error(
'No active project found in workspace. Run `project pull` first.'
);
}

const projectId = String(activeProject.uuid ?? (activeProject as any).id);
await checkout({ ...options, project: projectId, force: true }, logger);
};
2 changes: 2 additions & 0 deletions packages/cli/src/projects/command.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import list from './list';
import version from './version';
import merge from './merge';
import checkout from './checkout';
import clean from './clean';
import fetch from './fetch';
import { command as pull } from './pull';
import { command as deploy } from './deploy';
Expand All @@ -21,6 +22,7 @@ export const projectsCommand = {
.command(version)
.command(merge)
.command(checkout)
.command(clean)
.command(fetch as any)
.example('project', 'list all projects in the workspace')
.example('project list', 'list all projects in the workspace')
Expand Down
1 change: 1 addition & 0 deletions packages/cli/src/projects/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ export { handler as list } from './list';
export { handler as version } from './version';
export { handler as merge } from './merge';
export { handler as checkout } from './checkout';
export { handler as clean } from './clean';
export { handler as fetch } from './fetch';
export { handler as pull } from './pull';
export { handler as deploy } from './deploy';
117 changes: 117 additions & 0 deletions packages/cli/test/projects/clean.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
import test from 'ava';
import { createMockLogger } from '@openfn/logger';
import { handler as cleanHandler } from '../../src/projects/clean';
import mock from 'mock-fs';
import fs from 'fs';
import { jsonToYaml } from '@openfn/project';

const logger = createMockLogger('', { level: 'debug' });

const projectStateFile = jsonToYaml({
id: 'my-project',
name: 'My Project',
workflows: [
{
name: 'simple-workflow-main',
id: 'wf-id-main',
version_history: ['a'],
jobs: [
{
name: 'Transform data',
body: 'fn(state => state)',
adaptor: '@openfn/language-http@latest',
id: 'job-a',
},
],
triggers: [{ type: 'webhook', enabled: true, id: 'trigger-id' }],
edges: [
{
id: 'edge-id',
target_job_id: 'job-a',
enabled: true,
source_trigger_id: 'trigger-id',
condition_type: 'always',
},
],
},
],
});

test.beforeEach(() => {
mock({
'/ws/workflows/old-workflow': {
'old-job.js': 'fn(s => s)',
},
'/ws/openfn.yaml': jsonToYaml({
project: {
id: 'my-project',
},
workspace: {
workflowRoot: 'workflows',
formats: {
openfn: 'yaml',
project: 'yaml',
workflow: 'yaml',
},
},
}),
'/ws/.projects/project@app.openfn.org.yaml': projectStateFile,
});
});

test.afterEach(() => {
mock.restore();
logger._reset();
});

test.serial(
'clean: removes existing workflows and checks out active project',
async (t) => {
t.true(fs.existsSync('/ws/workflows/old-workflow/old-job.js'));

await cleanHandler(
{ command: 'project-clean', workspace: '/ws', force: true },
logger
);

t.false(fs.existsSync('/ws/workflows/old-workflow'));
t.deepEqual(fs.readdirSync('/ws/workflows'), ['simple-workflow-main']);
}
);

test.serial('clean: shows a confirmation prompt before deleting', async (t) => {
await cleanHandler({ command: 'project-clean', workspace: '/ws' }, logger);

const confirm = logger._find('confirm', /workflows/);
t.truthy(confirm);
});

test.serial('clean: aborts if user declines confirmation', async (t) => {
const declineLogger = {
...logger,
confirm: async () => false,
};

await cleanHandler(
{ command: 'project-clean', workspace: '/ws' },
declineLogger as any
);

t.true(fs.existsSync('/ws/workflows/old-workflow/old-job.js'));
});

test.serial(
'clean: throws if no active project found in workspace',
async (t) => {
mock({ '/ws/workflows': {} });

await t.throwsAsync(
() =>
cleanHandler(
{ command: 'project-clean', workspace: '/ws', force: true },
logger
),
{ message: /No active project found/ }
);
}
);