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
4 changes: 2 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@contentstack/cli-launch",
"version": "1.8.0",
"version": "1.9.1",
"description": "Launch related operations",
"author": "Contentstack CLI",
"bin": {
Expand Down
164 changes: 164 additions & 0 deletions src/adapters/github.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,164 @@
import GitHub from './github';
import { getRemoteUrls } from '../util/create-git-meta';
import { repositoriesQuery } from '../graphql';
import BaseClass from './base-class';

jest.mock('../util/create-git-meta');

const repositories = [
{
__typename: 'GitRepository',
id: '495370701',
url: 'https://github.com/test-user/nextjs-ssr-isr-demo',
name: 'nextjs-ssr-isr-demo',
fullName: 'test-user/nextjs-ssr-isr-demo',
defaultBranch: 'main',
},
{
__typename: 'GitRepository',
id: '555341263',
url: 'https://github.com/test-user/static-site-demo',
name: 'static-site-demo',
fullName: 'test-user/static-site-demo',
defaultBranch: 'main',
},
{
__typename: 'GitRepository',
id: '647250661',
url: 'https://github.com/test-user/eleventy-sample',
name: 'eleventy-sample',
fullName: 'test-user/eleventy-sample',
defaultBranch: 'main',
},
];

describe('GitHub Adapter', () => {
describe('checkGitRemoteAvailableAndValid', () => {
const repositoriesResponse = { data: { repositories } };
let logMock: jest.Mock;
let exitMock: jest.Mock;

beforeEach(() => {
logMock = jest.fn();
exitMock = jest.fn().mockImplementationOnce(() => {
throw new Error('1');
});
});

afterEach(() => {
jest.resetAllMocks();
});

it(`should successfully check if the git remote is available and valid
when the github remote URL is HTTPS based`, async () => {
(getRemoteUrls as jest.Mock).mockResolvedValueOnce({
origin: 'https://github.com/test-user/eleventy-sample.git',
});
const apolloClient = {
query: jest.fn().mockResolvedValueOnce(repositoriesResponse),
} as any;
const githubAdapterInstance = new GitHub({
config: { projectBasePath: '/home/project1' },
apolloClient: apolloClient,
} as any);

const result = await githubAdapterInstance.checkGitRemoteAvailableAndValid();

expect(getRemoteUrls as jest.Mock).toHaveBeenCalledWith('/home/project1/.git/config');
expect(apolloClient.query).toHaveBeenCalledWith({ query: repositoriesQuery });
expect(githubAdapterInstance.config.repository).toEqual({
__typename: 'GitRepository',
id: '647250661',
url: 'https://github.com/test-user/eleventy-sample',
name: 'eleventy-sample',
fullName: 'test-user/eleventy-sample',
defaultBranch: 'main',
});
expect(result).toBe(true);
});

it(`should successfully check if the git remote is available and valid
when the github remote URL is SSH based`, async () => {
(getRemoteUrls as jest.Mock).mockResolvedValueOnce({
origin: 'git@github.com:test-user/eleventy-sample.git',
});
const apolloClient = {
query: jest.fn().mockResolvedValueOnce(repositoriesResponse),
} as any;
const githubAdapterInstance = new GitHub({
config: { projectBasePath: '/home/project1' },
apolloClient: apolloClient,
} as any);

const result = await githubAdapterInstance.checkGitRemoteAvailableAndValid();

expect(getRemoteUrls as jest.Mock).toHaveBeenCalledWith('/home/project1/.git/config');
expect(apolloClient.query).toHaveBeenCalledWith({ query: repositoriesQuery });
expect(githubAdapterInstance.config.repository).toEqual({
__typename: 'GitRepository',
id: '647250661',
url: 'https://github.com/test-user/eleventy-sample',
name: 'eleventy-sample',
fullName: 'test-user/eleventy-sample',
defaultBranch: 'main',
});
expect(result).toBe(true);
});

it(`should log an error and proceed to connection via UI
if git repo remote url is unavailable and exit`, async () => {
(getRemoteUrls as jest.Mock).mockResolvedValueOnce(undefined);
const connectToAdapterOnUiMock
= jest.spyOn(BaseClass.prototype, 'connectToAdapterOnUi').mockResolvedValueOnce(undefined);
const githubAdapterInstance = new GitHub({
config: { projectBasePath: '/home/project1' },
log: logMock,
exit: exitMock
} as any);
let err;

try {
await githubAdapterInstance.checkGitRemoteAvailableAndValid();
} catch (error: any) {
err = error;
}


expect(getRemoteUrls as jest.Mock).toHaveBeenCalledWith('/home/project1/.git/config');
expect(logMock).toHaveBeenCalledWith('GitHub project not identified!', 'error');
expect(connectToAdapterOnUiMock).toHaveBeenCalled();
expect(exitMock).toHaveBeenCalledWith(1);
expect(err).toEqual(new Error('1'));
expect(githubAdapterInstance.config.repository).toBeUndefined();
});

it('should log an error and exit if repository is not found in the list of available repositories', async () => {
(getRemoteUrls as jest.Mock).mockResolvedValueOnce({
origin: 'https://github.com/test-user/test-repo-2.git',
});
const apolloClient = {
query: jest.fn().mockResolvedValueOnce(repositoriesResponse),
} as any;
const githubAdapterInstance = new GitHub({
config: { projectBasePath: '/home/project1' },
log: logMock,
exit: exitMock,
apolloClient: apolloClient,
} as any);
let err;

try {
await githubAdapterInstance.checkGitRemoteAvailableAndValid();
} catch (error: any) {
err = error;
}

expect(getRemoteUrls as jest.Mock).toHaveBeenCalledWith('/home/project1/.git/config');
expect(apolloClient.query).toHaveBeenCalledWith({ query: repositoriesQuery });
expect(logMock).toHaveBeenCalledWith('Repository not found in the list!', 'error');
expect(exitMock).toHaveBeenCalledWith(1);
expect(err).toEqual(new Error('1'));
expect(githubAdapterInstance.config.repository).toBeUndefined();
});
});
});
28 changes: 25 additions & 3 deletions src/adapters/github.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ import omit from 'lodash/omit';
import find from 'lodash/find';
import split from 'lodash/split';
import { exec } from 'child_process';
import replace from 'lodash/replace';
import includes from 'lodash/includes';
import { configHandler, cliux as ux } from '@contentstack/cli-utilities';

Expand Down Expand Up @@ -267,27 +266,49 @@ export default class GitHub extends BaseClass {
return this.config.userConnection;
}

private extractRepoFullNameFromGithubRemoteURL(url: string) {
let match;

// HTTPS format: https://github.com/owner/repo.git
match = url.match(/^https:\/\/github\.com\/([^/]+)\/([^/]+)(\.git)?$/);
if (match) {
return `${match[1]}/${match[2].replace(/\.git$/, '')}`;
}

// SSH format: git@github.com:owner/repo.git
match = url.match(/^git@github\.com:([^/]+)\/([^/]+)(\.git)?$/);
if (match) {
return `${match[1]}/${match[2].replace(/\.git$/, '')}`;
}
}


/**
* @method checkGitRemoteAvailableAndValid - GitHub repository verification
*
* @return {*} {(Promise<boolean | void>)}
* @memberof GitHub
*/
async checkGitRemoteAvailableAndValid(): Promise<boolean | void> {
const localRemoteUrl = (await getRemoteUrls(resolve(this.config.projectBasePath, '.git/config')))?.origin || '';
const gitConfigFilePath = resolve(this.config.projectBasePath, '.git/config');
const remoteUrls = await getRemoteUrls(gitConfigFilePath);
const localRemoteUrl = remoteUrls?.origin || '';

if (!localRemoteUrl) {
this.log('GitHub project not identified!', 'error');
await this.connectToAdapterOnUi();
this.exit(1);
}

const repositories = await this.apolloClient
.query({ query: repositoriesQuery })
.then(({ data: { repositories } }) => repositories)
.catch((error) => this.log(error, 'error'));

const repoFullName = this.extractRepoFullNameFromGithubRemoteURL(localRemoteUrl);

this.config.repository = find(repositories, {
url: replace(localRemoteUrl, '.git', ''),
fullName: repoFullName,
});

if (!this.config.repository) {
Expand All @@ -306,6 +327,7 @@ export default class GitHub extends BaseClass {
*/
async checkUserGitHubAccess(): Promise<boolean> {
return new Promise<boolean>((resolve, reject) => {
// eslint-disable-next-line @typescript-eslint/no-this-alias
const self = this;
const defaultBranch = this.config.repository?.defaultBranch;
if (!defaultBranch) return reject('Branch not found');
Expand Down