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 .changeset/small-cases-buy.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@openfn/cli': patch
---

When running commands with v2 sync, if no checkout metadata is detected, prompt the user
3 changes: 2 additions & 1 deletion integration-tests/cli/.gitignore
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
repo
repo
.state.json
53 changes: 0 additions & 53 deletions integration-tests/cli/.state.json

This file was deleted.

7 changes: 2 additions & 5 deletions packages/cli/src/projects/checkout.ts
Original file line number Diff line number Diff line change
Expand Up @@ -164,11 +164,8 @@ export const handler = async (options: CheckoutOptions, logger?: Logger) => {
});
if (isAdHoc) {
// Don't touch the workspace or checkout metadata
for (const { path: configPath } of switchProject.generateConfig(
options.branch
)) {
delete files[configPath];
}
delete files[switchProject.generateConfig().path];
delete files[switchProject.generateCheckout(options.branch).path];
}
for (const f in files) {
if (files[f]) {
Expand Down
27 changes: 17 additions & 10 deletions packages/cli/src/projects/clean.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { rimraf } from 'rimraf';

import { build, ensure } from '../util/command-builders';
import { handler as checkout } from './checkout';
import ensureCheckout from './ensure-checkout';
import type { Logger } from '../util/logger';
import * as o from '../options';
import * as po from './options';
Expand All @@ -29,9 +30,22 @@ export default command;

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

// Find out what's checked out before deleting anything
const workspace = await ensureCheckout(
new Workspace(workspacePath, logger, true, {
branch: options.branch,
}),
logger
);

// This has to be checked before deleting anything
const activeProject = workspace.activeProject;
if (!activeProject) {
throw new Error(
'No active project found in workspace. Run `project pull` first.'
);
}

const skip = options.force || options.confirm === false;
const doIt = await logger.confirm(
Expand All @@ -44,13 +58,6 @@ export const handler = async (options: CleanOptions, logger: Logger) => {

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);
};
30 changes: 16 additions & 14 deletions packages/cli/src/projects/deploy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,12 +18,13 @@ import {
fetchProject,
serialize,
getSerializePath,
updateForkedFrom,
writeCheckoutFile,
findLocallyChangedWorkflows,
AuthOptions,
} from './util';
import { build, ensure } from '../util/command-builders';
import { printRichDiff } from './diff';
import ensureCheckout from './ensure-checkout';
import { getCredentialsVisitor, remapCredentials } from './credentials-helpers';

import type { Provisioner } from '@openfn/lexicon/lightning';
Expand Down Expand Up @@ -404,6 +405,13 @@ export async function handler(options: DeployOptions, logger: Logger) {
branch: options.branch,
});

// We only need to know what's checked out if we're going to write the
// checkout back after deploying, and if the user hasn't told us which
// project to deploy to
if (options.checkout !== false && !targetIdentifier) {
ws = await ensureCheckout(ws, logger);
}

const active = ws.getTrackedProject();

if (!alias && active?.alias) {
Expand Down Expand Up @@ -617,25 +625,19 @@ export async function handler(options: DeployOptions, logger: Logger) {
);

if (options.checkout !== false) {
updateForkedFrom(finalProject);
const workspacePath = options.workspace ?? process.cwd();

// Write the updated openfn.yaml and checkout file
// TODO: allow us to suppress writing this stuff
// (useful if posting from spec)
for (const configData of finalProject.generateConfig(options.branch)) {
const configPath = path.resolve(
options.workspace ?? process.cwd(),
configData.path
);
await mkdir(path.dirname(configPath), { recursive: true });
await writeFile(configPath, configData.content);
}
const configData = finalProject.generateConfig();
const configPath = path.resolve(workspacePath, configData.path);
await mkdir(path.dirname(configPath), { recursive: true });
await writeFile(configPath, configData.content);
await writeCheckoutFile(workspacePath, finalProject, options.branch);

// Sync collections into resources.yaml (anything else in there is left alone)
const resourcesPath = path.resolve(
options.workspace ?? process.cwd(),
RESOURCES_FILE
);
const resourcesPath = path.resolve(workspacePath, RESOURCES_FILE);
const existingResources = await readFile(resourcesPath, 'utf8').catch(
(e) => {
if (e.code !== 'ENOENT') throw e;
Expand Down
125 changes: 125 additions & 0 deletions packages/cli/src/projects/ensure-checkout.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
import { select } from '@inquirer/prompts';
import { Workspace, hasCheckoutMeta } from '@openfn/project';
import type Project from '@openfn/project';

import abort from '../util/abort';
import type { Logger } from '../util/logger';
import { writeCheckoutFile } from './util';

// Resolves to the chosen project, or null if the user skipped the question
type Prompt = (
projects: Project[],
defaultProject: Project
) => Promise<Project | null>;

type EnsureCheckoutOptions = {
// Can we ask the user questions? Defaults to whether stdin is a terminal
interactive?: boolean;
prompt?: Prompt;
// If false, carry on without a checkout when we can't ask the user, rather
// than aborting (for commands that can work without one)
required?: boolean;
};

const name = (project: Project) => project.alias ?? project.id;

// eg `staging (my-project | 1234-abcd)`. Either id may be missing
export const describeProject = (project: Project) => {
const ids = [project.id, project.openfn?.uuid].filter(Boolean).join(' | ');
return `${project.alias || '(no alias)'}${ids ? ` (${ids})` : ''}`;
};

const promptForProject: Prompt = (projects, defaultProject) =>
select<Project | null>({
message:
'No checked out project found. Select the project which is currently tracked.',
choices: [
...projects.map((project) => ({
name: describeProject(project),
value: project as Project | null,
description: project.openfn?.endpoint,
})),
{
name: 'Skip checkout',
value: null,
description: 'Continue without setting the checked out project',
},
],
default: defaultProject,
});

// Guess which project is checked out: one with an alias matching the git
// branch (or main, if there's no branch), else just the first.
// Pass undefined, not null or false, if there is no branch
export const getDefaultProject = (projects: Project[], branch = 'main') =>
projects.find((p) => p.alias === branch) ?? projects[0];

/**
* Make sure the workspace knows which project is checked out.
*
* If there's no checkout metadata (say, after a fresh git clone, since it's
* not tracked in git), work out which project the files belong to and set
* up the checkout file. The files themselves are not touched.
*
* Returns a workspace that reflects the checkout.
*/
export default async (
workspace: Workspace,
logger: Logger,
{
interactive = Boolean(process.stdin.isTTY),
prompt = promptForProject,
required = true,
}: EnsureCheckoutOptions = {}
) => {
if (!workspace.valid || hasCheckoutMeta(workspace.root, workspace.branch)) {
return workspace;
}

const projects = workspace.list();
if (!projects.length) {
// Nothing to choose from: let the command report that in its own way
return workspace;
}

if (projects.length > 1 && !interactive && !required) {
logger.debug('No checked out project found: carrying on without one');
return workspace;
}

let project: Project | null = null;
if (projects.length === 1) {
project = projects[0];
logger.info(`No checked out project found: using ${name(project)}`);
} else if (interactive) {
logger.info(
'Failed to find a checkout.yaml file, which describes which project is currently checked out. This usually happens when pulling a project from git.'
);
logger.info(
"No problem - just pick which project is checked out. This won't affect your working tree, it'll just update local metadata and tell the CLI which remote project instance to track\n."
);
project = await prompt(
projects,
getDefaultProject(projects, workspace.branch || undefined)
);
} else {
abort(logger, 'No checked out project found', {
details: `This workspace tracks several projects (${projects
.map(name)
.join(', ')}) and it's not known which one is checked out`,
fix: 'Run this command in an interactive terminal to choose one',
});
}

if (!project) {
logger.info('Skipped: no checked out project has been set');
return workspace;
}

await writeCheckoutFile(workspace.root, project, workspace.branch);
logger.success(`Set the checked out project to ${name(project)}`);

return new Workspace(workspace.root, logger, true, {
branch: workspace.branch,
});
};
7 changes: 6 additions & 1 deletion packages/cli/src/projects/list.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import * as po from './options';

import type { Opts } from './options';
import abort from '../util/abort';
import ensureCheckout from './ensure-checkout';

export type ProjectListOptions = Pick<Opts, 'log' | 'workspace' | 'branch'>;

Expand All @@ -28,7 +29,7 @@ export const handler = async (options: ProjectListOptions, logger: Logger) => {
logger.info(' ', options.workspace);
logger.break();

const workspace = new Workspace(options.workspace!, undefined, true, {
let workspace = new Workspace(options.workspace!, undefined, true, {
branch: options.branch,
});

Expand All @@ -42,6 +43,10 @@ export const handler = async (options: ProjectListOptions, logger: Logger) => {
});
}

// Listing works fine without a checkout: it just can't mark the active
// project, so there's no need to abort if we can't ask which it is
workspace = await ensureCheckout(workspace, logger, { required: false });

logger.always(`Available openfn projects\n\n${workspace
.list()
.map((p) => describeProject(p, p === workspace.getTrackedProject()))
Expand Down
6 changes: 5 additions & 1 deletion packages/cli/src/projects/merge.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import * as po from './options';

import type { Opts } from './options';
import { handler as checkout } from './checkout';
import ensureCheckout from './ensure-checkout';

export type MergeOptions = Required<
Pick<
Expand Down Expand Up @@ -63,13 +64,16 @@ export default command;

export const handler = async (options: MergeOptions, logger: Logger) => {
const workspacePath = options.workspace;
const workspace = new Workspace(workspacePath, undefined, true, {
let workspace = new Workspace(workspacePath, undefined, true, {
branch: options.branch,
});
if (!workspace.valid) {
logger.error('Command was run in an invalid openfn workspace');
return;
}
if (!options.base) {
workspace = await ensureCheckout(workspace, logger);
}

let targetProject: Project;
if (options.base) {
Expand Down
Loading
Loading