Skip to content

Commit

Permalink
Enable Dependency Graph Integrator to raise PRs to add workflow (#1164)
Browse files Browse the repository at this point in the history
* feat: add dep graph integrator topic and flag

* feat: send dep graph integrator msgs to sns

* feat: query actions usage table

* feat: add tests to sns functions

* feat: add actions usage table to DEV db

* refactor: remove dep graph PR flag

* fix: correct logic after local testing

* fix: remove flag

* refactor: only send messages if on PROD

* refactor: rename sns function to make clearer

* refactor: move db call up with others
  • Loading branch information
tjsilver authored Jul 8, 2024
1 parent aa095c8 commit 9b05209
Show file tree
Hide file tree
Showing 7 changed files with 297 additions and 0 deletions.
1 change: 1 addition & 0 deletions packages/dev-environment/docker-compose.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ services:

# These tables will be copied from the CODE database, and loaded into the DEV database.
TABLES: |
- guardian_github_actions_usage
- snyk_issues
- snyk_projects
- galaxies_teams_table
Expand Down
8 changes: 8 additions & 0 deletions packages/repocop/src/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,11 @@ export interface Config extends PrismaConfig {
* The ARN of the Snyk Integrator input topic.
*/
snykIntegratorTopic: string;

/**
* The ARN of the Dependency Graph Integrator input topic.
*/
dependencyGraphIntegratorTopic: string;
}

export async function getConfig(): Promise<Config> {
Expand Down Expand Up @@ -92,5 +97,8 @@ export async function getConfig(): Promise<Config> {
snykIntegrationPREnabled:
process.env.SNYK_INTEGRATION_PR_ENABLED === 'true',
snykIntegratorTopic: getEnvOrThrow('SNYK_INTEGRATOR_INPUT_TOPIC_ARN'),
dependencyGraphIntegratorTopic: getEnvOrThrow(
'DEPENDENCY_GRAPH_INPUT_TOPIC_ARN',
),
};
}
15 changes: 15 additions & 0 deletions packages/repocop/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { CloudWatchClient } from '@aws-sdk/client-cloudwatch';
import type {
guardian_github_actions_usage,
PrismaClient,
repocop_github_repository_rules,
view_repo_ownership,
Expand All @@ -17,6 +18,7 @@ import {
import { sendToCloudwatch } from './metrics';
import {
getDependabotVulnerabilities,
getProductionWorkflowUsages,
getRepoOwnership,
getRepositories,
getRepositoryBranches,
Expand All @@ -27,6 +29,7 @@ import {
getTeams,
} from './query';
import { protectBranches } from './remediation/branch-protector/branch-protection';
import { sendOneRepoToDepGraphIntegrator } from './remediation/dependency_graph-integrator/send-to-sns';
import { sendUnprotectedRepo } from './remediation/snyk-integrator/send-to-sns';
import { sendPotentialInteractives } from './remediation/topics/topic-monitor-interactive';
import { applyProductionTopicAndMessageTeams } from './remediation/topics/topic-monitor-production';
Expand Down Expand Up @@ -90,6 +93,11 @@ export async function main() {

console.log(productionDependabotVulnerabilities);

// Dependency Graph Integrator
const productionWorkflowUsages: guardian_github_actions_usage[] =
await getProductionWorkflowUsages(prisma, productionRepos);


const evaluationResults: EvaluationResult[] = await evaluateRepositories(
unarchivedRepos,
branches,
Expand Down Expand Up @@ -189,5 +197,12 @@ export async function main() {
);
}

await sendOneRepoToDepGraphIntegrator(
config,
repoLanguages,
productionRepos,
productionWorkflowUsages,
);

console.log('Done');
}
13 changes: 13 additions & 0 deletions packages/repocop/src/query.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import type {
github_languages,
github_repository_branches,
guardian_github_actions_usage,
PrismaClient,
view_repo_ownership,
} from '@prisma/client';
Expand Down Expand Up @@ -182,3 +183,15 @@ export async function getDependabotVulnerabilities(

return dependabotVulnerabilities;
}

export async function getProductionWorkflowUsages(
client: PrismaClient,
productionRepos: Repository[],
): Promise<NonEmptyArray<guardian_github_actions_usage>> {
const actions_usage = await client.guardian_github_actions_usage.findMany({
where: {
full_name: { in: productionRepos.map((repo) => repo.full_name) },
},
});
return toNonEmptyArray(actions_usage);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,154 @@
import type {
github_languages,
guardian_github_actions_usage,
} from '@prisma/client';
import type { Repository } from 'common/src/types';
import { removeRepoOwner } from '../shared-utilities';
import {
checkRepoForLanguage,
createSnsEventsForDependencyGraphIntegration,
doesRepoHaveWorkflow,
} from './send-to-sns';

const fullName = 'guardian/repo-name';
const fullName2 = 'guardian/repo2';
const scalaLang = 'Scala';

function createActionsUsage(
fullName: string,
workflowUses: string[],
): guardian_github_actions_usage {
return {
evaluated_on: new Date('2024-01-01'),
full_name: fullName,
workflow_path: '.github/workflows/some-workflow.yaml',
workflow_uses: workflowUses,
};
}

function repoWithLanguages(
fullName: string,
languages: string[],
): github_languages {
return {
cq_sync_time: null,
cq_parent_id: null,
cq_id: '',
cq_source_name: null,
full_name: fullName,
name: fullName,
languages,
};
}

function repository(fullName: string): Repository {
return {
archived: false,
name: removeRepoOwner(fullName),
full_name: fullName,
id: BigInt(1),
default_branch: null,
created_at: null,
pushed_at: null,
updated_at: null,
topics: [],
};
}

function repoWithTargetLanguage(fullName: string): github_languages {
return repoWithLanguages(fullName, ['Scala', 'TypeScript']);
}

function repoWithoutTargetLanguage(fullName: string): github_languages {
return repoWithLanguages(fullName, ['Rust', 'Typescript']);
}

function repoWithDepSubmissionWorkflow(
fullName: string,
): guardian_github_actions_usage {
return createActionsUsage(fullName, [
'actions/checkout@v2',
'scalacenter/sbt-dependency-submission@v2',
'aws-actions/configure-aws-credentials@v1',
]);
}

function repoWithoutWorkflow(fullName: string): guardian_github_actions_usage {
return createActionsUsage(fullName, [
'actions/checkout@v2',
'aws-actions/configure-aws-credentials@v1',
]);
}

describe('When trying to find repos using Scala', () => {
test('return true if Scala is found in the repo', () => {
const result = checkRepoForLanguage(
repository(fullName),
[repoWithTargetLanguage(fullName)],
scalaLang,
);

expect(result).toBe(true);
});
test('return false if Scala is not found in the repo', () => {
const result = checkRepoForLanguage(
repository(fullName),
[repoWithoutTargetLanguage(fullName)],
scalaLang,
);
expect(result).toBe(false);
});
});

describe('When checking a repo for an existing dependency submission workflow', () => {
test('return true if repo workflow is present', () => {
const result = doesRepoHaveWorkflow(repository(fullName), [
repoWithDepSubmissionWorkflow(fullName),
]);
expect(result).toBe(true);
});
test('return false if workflow is not present', () => {
const result = doesRepoHaveWorkflow(repository(fullName), [
repoWithoutWorkflow(fullName),
]);
expect(result).toBe(false);
});
});

describe('When getting suitable events to send to SNS', () => {
test('return an event when a Scala repo is found without an existing workflow', () => {
const result = createSnsEventsForDependencyGraphIntegration(
[repoWithTargetLanguage(fullName)],
[repository(fullName)],
[repoWithoutWorkflow(fullName)],
);
expect(result).toEqual([{ name: removeRepoOwner(fullName) }]);
});
test('return empty event array when a Scala repo is found with an existing workflow', () => {
const result = createSnsEventsForDependencyGraphIntegration(
[repoWithTargetLanguage(fullName)],
[repository(fullName)],
[repoWithDepSubmissionWorkflow(fullName)],
);
expect(result).toEqual([]);
});
test('return empty array when non-Scala repo is found with without an existing workflow', () => {
const result = createSnsEventsForDependencyGraphIntegration(
[repoWithoutTargetLanguage(fullName)],
[repository(fullName)],
[repoWithoutWorkflow(fullName)],
);
expect(result).toEqual([]);
});
test('return 2 events when 2 Scala repos are found without an existing workflow', () => {
const result = createSnsEventsForDependencyGraphIntegration(
[repoWithTargetLanguage(fullName), repoWithTargetLanguage(fullName2)],
[repository(fullName), repository(fullName2)],
[repoWithoutWorkflow(fullName), repoWithoutWorkflow(fullName2)],
);
expect(result).toEqual([
{ name: removeRepoOwner(fullName) },
{ name: removeRepoOwner(fullName2) },
]);
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
import { PublishCommand, SNSClient } from '@aws-sdk/client-sns';
import type {
github_languages,
guardian_github_actions_usage,
} from '@prisma/client';
import { awsClientConfig } from 'common/src/aws';
import { shuffle } from 'common/src/functions';
import type {
DependencyGraphIntegratorEvent,
Repository,
} from 'common/src/types';
import type { Config } from '../../config';
import { removeRepoOwner } from '../shared-utilities';

export function checkRepoForLanguage(
repo: Repository,
languages: github_languages[],
targetLanguage: string,
): boolean {
const languagesInRepo: string[] =
languages.find((language) => language.full_name === repo.full_name)
?.languages ?? [];
return languagesInRepo.includes(targetLanguage);
}

export function doesRepoHaveWorkflow(
repo: Repository,
workflow_usages: guardian_github_actions_usage[],
): boolean {
const actionsForRepo = workflow_usages
.filter((usages) => repo.full_name === usages.full_name)
.flatMap((workflow) => workflow.workflow_uses);

const dependencySubmissionWorkflow = actionsForRepo.find((action) =>
action.includes('scalacenter/sbt-dependency-submission'),
);
if (dependencySubmissionWorkflow) {
return true;
}
return false;
}

export function createSnsEventsForDependencyGraphIntegration(
languages: github_languages[],
productionRepos: Repository[],
workflow_usages: guardian_github_actions_usage[],
): DependencyGraphIntegratorEvent[] {
const scalaRepos = productionRepos.filter((repo) =>
checkRepoForLanguage(repo, languages, 'Scala'),
);

console.log(`Found ${scalaRepos.length} Scala repos in production`);

const scalaReposWithoutWorkflows = scalaRepos.filter(
(repo) => !doesRepoHaveWorkflow(repo, workflow_usages),
);

console.log(
`Found ${scalaRepos.length} production Scala repos without dependency submission workflows`,
);

const events = scalaReposWithoutWorkflows.map((repo) => ({
name: removeRepoOwner(repo.full_name),
}));
console.log(`Found ${events.length} events to send to SNS`);
return events;
}

export async function sendOneRepoToDepGraphIntegrator(
config: Config,
repoLanguages: github_languages[],
productionRepos: Repository[],
workflowUsages: guardian_github_actions_usage[],
) {
const eventToSend = shuffle(
createSnsEventsForDependencyGraphIntegration(
repoLanguages,
productionRepos,
workflowUsages,
),
)[0];

if (eventToSend) {
if (config.stage === 'PROD') {
const publishRequestEntry = new PublishCommand({
Message: JSON.stringify(eventToSend),
TopicArn: config.dependencyGraphIntegratorTopic,
});
console.log(`Sending ${eventToSend.name} to Dependency Graph Integrator`);
await new SNSClient(awsClientConfig(config.stage)).send(
publishRequestEntry,
);
} else {
console.log(
`Would have sent ${eventToSend.name} to Dependency Graph Integrator`,
);
}
} else {
console.log(
'No Scala repos found without SBT dependency submission workflow',
);
}
}
3 changes: 3 additions & 0 deletions scripts/setup.sh
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,8 @@ setup_environment() {

SNYK_INTEGRATOR_INPUT_TOPIC_ARN=$(aws sns list-topics --profile "$PROFILE" --region "$REGION" --output text --query 'Topics[*]' | grep snykintegratorinputtopicCODE)

DEPENDENCY_GRAPH_INPUT_TOPIC_ARN=$(aws sns list-topics --profile "$PROFILE" --region "$REGION" --output text --query 'Topics[*]' | grep dependencygraphintegratorinputtopicCODE)

CLOUDQUERY_API_KEY=$(
aws secretsmanager get-secret-value \
--secret-id /CODE/deploy/service-catalogue/cloudquery-api-key \
Expand All @@ -113,6 +115,7 @@ INTERACTIVE_MONITOR_TOPIC_ARN=${INTERACTIVE_MONITOR_TOPIC_ARN}
GITHUB_PRIVATE_KEY_PATH=${GITHUB_PRIVATE_KEY_PATH}
CLOUDQUERY_API_KEY=${CLOUDQUERY_API_KEY}
SNYK_INTEGRATOR_INPUT_TOPIC_ARN=${SNYK_INTEGRATOR_INPUT_TOPIC_ARN}
DEPENDENCY_GRAPH_INPUT_TOPIC_ARN=${DEPENDENCY_GRAPH_INPUT_TOPIC_ARN}
"

# Check if .env.local file exists in ~/.gu/service_catalogue/
Expand Down

0 comments on commit 9b05209

Please sign in to comment.