Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[DOP-3911]: Check project path for monorepo #894

Merged
merged 32 commits into from
Sep 7, 2023
Merged
Show file tree
Hide file tree
Changes from 30 commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
3a1f0d1
[DOP-3911]: Add way to check file changes and determine what projects…
branberry Aug 24, 2023
f406287
Merge branch 'master' into DOP-3911
branberry Aug 25, 2023
a790a75
[DOP-3911]: Add some comments
branberry Aug 25, 2023
28451bb
Merge branch 'master' into DOP-3911
branberry Aug 28, 2023
e47d9f5
[DOP-3911]: Add checks for snooty.toml in monorepo
branberry Aug 28, 2023
665cce6
[DOP-3911]: Add commit info and simplify files
branberry Aug 29, 2023
7c02379
[DOP-3911]: Add unit tests
branberry Aug 29, 2023
1bcfe51
[DOP-3911]: Update tests
branberry Aug 29, 2023
9081419
[DOP-3911]: Update comments
branberry Aug 29, 2023
d825499
[DOP-3911]: Remove extra comment
branberry Aug 29, 2023
5461d09
[DOP-3911]: Remove unused code
branberry Aug 29, 2023
b441922
[DOP-3911]: Remove unused property in job payload
branberry Aug 29, 2023
9e26f66
[DOP-3911]: Remove unused import
branberry Aug 29, 2023
0a7640d
[DOP-3911]: Remove log and irrelevant comments
branberry Aug 29, 2023
e3f20c2
[DOP-3911]: Refactor to start using tree approach and create new dire…
branberry Aug 29, 2023
e296ff5
[DOP-3911]: Refactor set to be called from parent function
branberry Aug 30, 2023
3874381
[DOP-3911]: More refactoring, update tests to use set
branberry Aug 30, 2023
a6728f4
[DOP-3911]: Clean up
branberry Aug 30, 2023
f546e8c
[DOP-3911]: Resolve merge conflicts
branberry Aug 30, 2023
66bee93
[DOP-3911]: Resolve merge conflicts
branberry Aug 30, 2023
d829955
[DOP-3911]: Add types and add comments
branberry Aug 31, 2023
3859aca
[DOP-3911]: Add feature
branberry Aug 31, 2023
daa10eb
[DOP-3911]: Add feature flag for testing monorepo path
branberry Sep 1, 2023
172d6e2
Merge branch 'master' into DOP-3911
branberry Sep 1, 2023
35bd5c0
[DOP-3911]: Fix typo
branberry Sep 1, 2023
a089ede
[DOP-3911]: Remove get snooty.toml
branberry Sep 1, 2023
80cab7e
Merge branch 'master' into DOP-3911
branberry Sep 6, 2023
ee4691b
[DOP-3911]: Remove use of config library, and clean up code
branberry Sep 6, 2023
8fe0d9b
[DOP-3911]: Add parameter store flag for ease of testing
branberry Sep 6, 2023
46918e7
[DOP-3911]: Comment update
branberry Sep 6, 2023
15a3642
[DOP-3911]: Properly name test
branberry Sep 7, 2023
7cad533
Merge branch 'master' into DOP-3911
branberry Sep 7, 2023
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
2 changes: 1 addition & 1 deletion .github/workflows/update-feature-branch.yml
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ jobs:
cd cdk-infra/
npm ci
npm run deploy:feature:stack -- -c env=stg -c customFeatureName=enhancedApp-stg-${{github.head_ref}} \
auto-builder-stack-enhancedApp-stg-${{github.head_ref}}-webhook
auto-builder-stack-enhancedApp-stg-${{github.head_ref}}-webhooks
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixes bug with feature branch update.


- name: Update Worker Stack
if: steps.filter.outputs.worker == 'true'
Expand Down
22 changes: 20 additions & 2 deletions api/controllers/v2/github.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ import { ConsoleLogger } from '../../../src/services/logger';
import { RepoBranchesRepository } from '../../../src/repositories/repoBranchesRepository';
import { EnhancedJob, JobStatus } from '../../../src/entities/job';
import { markBuildArtifactsForDeletion, validateJsonWebhook } from '../../handlers/github';
import { getMonorepoPaths } from '../../../src/monorepo';
import { getUpdatedFilePaths } from '../../../src/monorepo/utils/path-utils';

async function prepGithubPushPayload(
githubEvent: PushEvent,
Expand Down Expand Up @@ -75,9 +77,9 @@ export const TriggerBuild = async (event: APIGatewayEvent): Promise<APIGatewayPr
body: errMsg,
};
}
let body;
let body: PushEvent;
try {
body = JSON.parse(event.body);
body = JSON.parse(event.body) as PushEvent;
} catch (e) {
console.log('[TriggerBuild]: ERROR! Could not parse event.body', e);
return {
Expand All @@ -101,6 +103,22 @@ export const TriggerBuild = async (event: APIGatewayEvent): Promise<APIGatewayPr

const job = await prepGithubPushPayload(body, repoBranchesRepository, jobPrefix);

if (process.env.MONOREPO_PATH_FEATURE === 'true') {
try {
if (body.head_commit && body.repository.owner.name) {
const monorepoPaths = await getMonorepoPaths({
commitSha: body.head_commit.id,
repoName: body.repository.name,
ownerName: body.repository.owner.name,
updatedFilePaths: getUpdatedFilePaths(body.head_commit),
});
console.log('monorepoPaths: ', monorepoPaths);
}
} catch (error) {
console.warn('Warning, attempting to get repo paths caused an error', error);
}
}

try {
consoleLogger.info(job.title, 'Creating Job');
const jobId = await jobRepository.insertJob(job, c.get('jobsQueueUrl'));
Expand Down
7 changes: 7 additions & 0 deletions cdk-infra/lib/constructs/api/webhook-env-construct.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,12 @@ export class WebhookEnvConstruct extends Construct {
const ssmPrefix = getSsmPathPrefix();
const env = getEnv();

// Create configurable feature flag that lives in parameter store.
const monorepoPathFeature = new StringParameter(this, 'monorepoPathFeature', {
parameterName: `${ssmPrefix}/monorepo/path_feature`,
stringValue: env === 'dotcomstg' || env === 'stg' ? 'true' : 'false',
});

const dbName = StringParameter.valueFromLookup(this, `${ssmPrefix}/atlas/dbname`);
const snootyDbName = StringParameter.valueFromLookup(this, `${ssmPrefix}/atlas/collections/snooty`);
const repoBranchesCollection = StringParameter.valueFromLookup(this, `${ssmPrefix}/atlas/collections/repo`);
Expand Down Expand Up @@ -47,6 +53,7 @@ export class WebhookEnvConstruct extends Construct {
USER_ENTITLEMENT_COL_NAME: entitlementCollection,
DASHBOARD_URL: getDashboardUrl(env, jobCollection),
STAGE: env,
MONOREPO_PATH_FEATURE: monorepoPathFeature.stringValue,
};
}
}
19 changes: 19 additions & 0 deletions src/clients/githubClient.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import { Octokit } from '@octokit/rest';

let client: Octokit;

export function getOctokitClient(): Octokit {
if (client) return client;

try {
const { GITHUB_BOT_PASSWORD } = process.env;

if (!GITHUB_BOT_PASSWORD) throw new Error('GITHUB_BOT_PASSWORD is not defined');

client = new Octokit({ auth: GITHUB_BOT_PASSWORD });
return client;
} catch (error) {
console.error('ERROR! Failed to create Octokit client. Is GITHUB_BOT_PASSWORD defined?', error);
throw error;
}
}
42 changes: 42 additions & 0 deletions src/monorepo/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import { getSnootyDirSet } from './utils/path-utils';
import { GitCommitInfo } from './types/github-types';
import { getProjectDirFromPath } from './services/get-paths';

interface FileUpdatePayload {
ownerName: string;
repoName: string;
commitSha: string;
updatedFilePaths: string[];
}

/**
* Retrieves the path of project directories. This is determined
* by finding the nearest parent directory that has a snooty.toml file
* for a given updated file path from a commit.
* @param repoName Name of the repository to check.
* @param ownerName Name of the owner of the repository.
* @param commitSha The Git commit SHA that contains the changed files.
* @param updatedFilePaths An array of all of the changed files (added, removed, modified)
* from the commit. The method `getUpdatedFilePaths` in the `src/monorepo/utils/path-utils.ts
* can be used to parse these paths from a GitHub `Commit` object.
* @returns An array of all the project paths that need to be built.
*/
export async function getMonorepoPaths({
repoName,
ownerName,
commitSha,
updatedFilePaths,
}: FileUpdatePayload): Promise<string[]> {
const commitInfo: GitCommitInfo = {
ownerName,
repoName,
commitSha,
};

const snootyDirSet = await getSnootyDirSet(commitInfo);

const projects = updatedFilePaths.map((path) => getProjectDirFromPath(path, snootyDirSet));

// remove empty strings and remove duplicated values
return Array.from(new Set(projects.filter((dir) => !!dir)));
}
36 changes: 36 additions & 0 deletions src/monorepo/services/get-paths.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import { SNOOTY_TOML_FILENAME } from '../utils/monorepo-constants';

/**
* This function returns the project path for a given file change from a docs repository
* within the monorepo. This function supports nested projects.
* @param path An added/modified/removed file path from a commit e.g. server-docs/source/index.rst
* @param commitInfo Contains information
* @returns The closest file path that contains a snooty.toml, relative to the path parameter.
*/
export function getProjectDirFromPath(path: string, snootyDirSet: Set<string>): string {
const pathArray = path.split('/');
if (pathArray.length === 0) {
console.warn('WARNING! Empty path found: ', path);
return '';
}

/**
* If the changed file is the snooty.toml file, we know that we
* are in the project's root directory. We can join the original
* pathArray to get the project path since the snooty.toml has been removed.
*/
const changedFile = pathArray.pop();

if (changedFile === SNOOTY_TOML_FILENAME) return pathArray.join('/');

while (pathArray.length > 0) {
const currDir = pathArray.join('/');

if (snootyDirSet.has(currDir)) return currDir;

pathArray.pop();
}

console.warn(`WARNING! No snooty.toml found for the given path: ${path}`);
return '';
}
25 changes: 25 additions & 0 deletions src/monorepo/types/atlas-types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
interface DirectoryConfig {
snooty_toml?: string;
source?: string;
}

interface RepoConfig {
repoName: string;
deployable: boolean;
branches: BranchConfig[];
}

interface BranchConfig {
gitBranchName: string;
}

// TODO: Populate these more. For DOP-3911, they are
// being added for testing purposes.
export interface DocSetEntry {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Currently incomplete, but this contains some of the type information that will be needed for the docset collection once it is created.

project: string;
prefix: string;
bucket: string;
url: string;
directories?: DirectoryConfig;
repos?: RepoConfig[];
}
5 changes: 5 additions & 0 deletions src/monorepo/types/github-types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
export interface GitCommitInfo {
commitSha: string;
ownerName: string;
repoName: string;
}
2 changes: 2 additions & 0 deletions src/monorepo/utils/monorepo-constants.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export const SNOOTY_TOML_FILENAME = 'snooty.toml';
export const MONOREPO_NAME = 'docs-monorepo';
50 changes: 50 additions & 0 deletions src/monorepo/utils/path-utils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import { Commit } from '@octokit/webhooks-types';
import { getOctokitClient } from '../../clients/githubClient';
import { GitCommitInfo } from '../types/github-types';
import { SNOOTY_TOML_FILENAME } from './monorepo-constants';

/**
* Creates a `Set` of all `snooty.toml` paths within the monorepo.
* The function retrieves the monorepo's tree structure from GitHub.
*/
export async function getSnootyDirSet({ commitSha, ownerName, repoName }: GitCommitInfo): Promise<Set<string>> {
try {
const client = getOctokitClient();

// getting the repository tree for a given commit SHA. This returns an object
// with the property `tree` that is a flat array of all files in the repository.
// The tree array contains objects that hold the file path.
// Unlike the contents API for repositories, the actual file content is not returned.
const { data } = await client.request('GET /repos/{owner}/{repo}/git/trees/{tree_sha}', {
owner: ownerName,
repo: repoName,
tree_sha: commitSha,
recursive: 'true',
});

const snootyTomlDirs = data.tree
.filter((treeNode) => !!treeNode.path?.includes(SNOOTY_TOML_FILENAME))
.map((treeNode) => {
// casting the `treeNode.path` from `(string | undefined)` to `string` since the filter will ensure that the result
// only includes treeNode.path values that are defined and include snooty.toml
// in the path i.e. we will not have `undefined` as a value in the resulting array.
const path = treeNode.path as string;

// the - 1 is to remove the trailing slash
return path.slice(0, path.length - SNOOTY_TOML_FILENAME.length - 1);
});

const snootyDirSet = new Set(snootyTomlDirs);

return snootyDirSet;
} catch (error) {
console.error(
`ERROR! Unable to retrieve tree for SHA: ${commitSha} owner name: ${ownerName} repo name: ${repoName}`,
error
);
throw error;
}
}

export const getUpdatedFilePaths = (commit: Commit): string[] =>
commit.modified.concat(commit.added).concat(commit.removed);
45 changes: 45 additions & 0 deletions src/repositories/docSetRepository.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import { IConfig } from 'config';
import { Db } from 'mongodb';
import { ILogger } from '../services/logger';
import { BaseRepository } from './baseRepository';

const docSetCollectionName = process.env.DOCS_SET_COLLECTION_NAME || 'docset';

export class DocSetRepository extends BaseRepository {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Currently not used since the docset collection in Atlas is not yet configured, but we will need this functionality in the future.

constructor(db: Db, config: IConfig, logger: ILogger) {
super(config, logger, 'DocSetRepository', db.collection(docSetCollectionName));
}

/**
* Compares the project path from a monorepo push event, and compares it with
* what is configured in the docset entry in Atlas.
* @param path The project path where the snooty.toml file exists from the monorepo.
* This path will reflect the current project path from a given commit.
* @param projectName The project name for the docset entry.
* @returns A boolean representing whether or not the configured docset entry snooty_toml path
* matches the path found in GitHub.
*/
async checkSnootyTomlPath(path: string, projectName: string) {
const query = { project: projectName };
try {
const docSetObject = await this.findOne(
query,
`Mongo Timeout Error: Timedout while retrieving repos entry for ${path}`
);

if (!docSetObject) {
console.warn(`WARNING: The docset does not exist for the following project: ${projectName} \n path: ${path}`);

return false;
}

return docSetObject.directories.snooty_toml === path;
} catch (error) {
console.warn(
`WARNING: Error occurred when retrieving project path for ${projectName}. The following path was provided: ${path}`,
error
);
return false;
}
}
}
Loading
Loading