Skip to content
Open
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
19 changes: 14 additions & 5 deletions src/force.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,22 +22,31 @@ export const force = async (api: APIInterface): Promise<string> => {
dep => dep.suffix == suffix
);

if (repo) {
if (repo.length > 0) {
// An existing deployment was found — delete it before re-deploying.
res = await del(
repo[0].prefix,
repo[0].suffix,
repo[0].version,
api
);
args['plan'] = repoSubscriptionDetails[0].plan;

// Restore the plan from the subscription that owned this deployment
// so the re-deploy is charged to the same subscription slot.
if (repoSubscriptionDetails.length > 0) {
args['plan'] = repoSubscriptionDetails[0].plan;
}
} else {
// No prior deployment found skip deletion and proceed normally.
info(
'No existing deployment found for this project. Continuing as a fresh deploy.'
);
}
} catch (e) {
error(
'Deployment Aborted because this directory is not being used by any applications.'
'Deployment Aborted due to an unexpected error while checking existing deployments.'
);
}

return res;
};

// One improvement can be done is, if with force flag, a person tries to deploy an app, and the app is not present actually there then it should behave as normal deployment procedure
91 changes: 91 additions & 0 deletions src/test/force.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
/**
* Unit tests for src/force.ts
*
* Fixes: https://github.com/metacall/deploy/issues #208
* "force() exits with error when no existing deployment matches the target suffix"
*
* All tests use a mock API, no network or real credentials required.
*/

import { Deployment, DeployStatus } from '@metacall/protocol/deployment';
import { Plans } from '@metacall/protocol/plan';
import {
API as APIInterface,
SubscriptionDeploy
} from '@metacall/protocol/protocol';
import { strictEqual } from 'assert';
import { basename } from 'path';
import args from '../cli/args';
import { force } from '../force';

// force() derives the suffix from args['projectName'].toLowerCase() when
// --addrepo is not set. Mirror that here so mock data aligns with the filter.
const TEST_SUFFIX = basename(process.cwd()).toLowerCase();

const makeDeployment = (): Deployment => ({
status: 'ready' as DeployStatus,
prefix: 'test-prefix',
suffix: TEST_SUFFIX,
version: 'v1',
packages: {} as Deployment['packages'],
ports: []
});

const makeSubscriptionDeploy = (): SubscriptionDeploy => ({
id: 'sub-id-abc123',
plan: Plans.Essential,
date: Date.now(),
deploy: TEST_SUFFIX
});

// Only the three methods force() actually invokes are given real stubs.
// Everything else resolves to a safe empty value.
const makeMockApi = (
deployments: Deployment[],
subscriptionDeploys: SubscriptionDeploy[]
): APIInterface => ({
refresh: () => Promise.resolve(''),
validate: () => Promise.resolve(true),
deployEnabled: () => Promise.resolve(true),
listSubscriptions: () => Promise.resolve({}),
listSubscriptionsDeploys: () => Promise.resolve(subscriptionDeploys),
inspect: () => Promise.resolve(deployments),
upload: () => Promise.resolve(''),
add: () => Promise.resolve({ id: '' }),
deploy: () => Promise.resolve({ suffix: '', prefix: '', version: '' }),
deployDelete: () => Promise.resolve('deleted-ok'),
logs: () => Promise.resolve(''),
branchList: () => Promise.resolve({ branches: ['main'] }),
fileList: () => Promise.resolve([])
});

describe('Unit force() emptyrepo guard', () => {
const originalPlan = args['plan'];

afterEach(() => {
args['plan'] = originalPlan;
});

it('returns empty string and does not throw when no deployment exists', async () => {
const api = makeMockApi([], []);
const result = await force(api);
strictEqual(result, '');
});

it('deletes the existing deployment and restores args.plan from subscription', async () => {
const api = makeMockApi([makeDeployment()], [makeSubscriptionDeploy()]);
const result = await force(api);

strictEqual(result, 'deleted-ok');
strictEqual(args['plan'], Plans.Essential);
});

it('deletes deployment without crashing when subscription list has no match', async () => {
const api = makeMockApi([makeDeployment()], []);
const result = await force(api);

// Deletion succeeds; args.plan stays unchanged since no subscription matched.
strictEqual(result, 'deleted-ok');
strictEqual(args['plan'], originalPlan);
});
});