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

Pull Request Preview Environments #456

Open
wants to merge 2 commits into
base: master
Choose a base branch
from
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
58 changes: 58 additions & 0 deletions .aws/start-page-task-definition.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
{
"containerDefinitions": [
{
"name": "thunderbird-start-page",
"image": "latest",
"cpu": 0,
"portMappings": [
{
"name": "thunderbird-start-page-80-tcp",
"containerPort": 80,
"hostPort": 80,
"protocol": "tcp",
"appProtocol": "http"
}
],
"essential": true,
"environment": [],
"environmentFiles": [],
"mountPoints": [],
"volumesFrom": []
}
],
"family": "thunderbird-start-page",
"executionRoleArn": "arn:aws:iam::768512802988:role/ecsTaskExecutionRole",
"networkMode": "awsvpc",
"revision": 2,
"volumes": [],
"status": "ACTIVE",
"requiresAttributes": [
{
"name": "com.amazonaws.ecs.capability.ecr-auth"
},
{
"name": "ecs.capability.execution-role-ecr-pull"
},
{
"name": "com.amazonaws.ecs.capability.docker-remote-api.1.18"
},
{
"name": "ecs.capability.task-eni"
}
],
"placementConstraints": [],
"compatibilities": [
"EC2",
"FARGATE"
],
"requiresCompatibilities": [
"FARGATE"
],
"cpu": "256",
"memory": "2048",
"runtimePlatform": {
"cpuArchitecture": "X86_64",
"operatingSystemFamily": "LINUX"
},
"tags": []
}
58 changes: 58 additions & 0 deletions .aws/website-task-definition.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
{
"containerDefinitions": [
{
"name": "thunderbird-website",
"image": "latest",
"cpu": 0,
"portMappings": [
{
"name": "thunderbird-website-80-tcp",
"containerPort": 80,
"hostPort": 80,
"protocol": "tcp",
"appProtocol": "http"
}
],
"essential": true,
"environment": [],
"environmentFiles": [],
"mountPoints": [],
"volumesFrom": []
}
],
"family": "thunderbird-website",
"executionRoleArn": "arn:aws:iam::768512802988:role/ecsTaskExecutionRole",
"networkMode": "awsvpc",
"revision": 2,
"volumes": [],
"status": "ACTIVE",
"requiresAttributes": [
{
"name": "com.amazonaws.ecs.capability.ecr-auth"
},
{
"name": "ecs.capability.execution-role-ecr-pull"
},
{
"name": "com.amazonaws.ecs.capability.docker-remote-api.1.18"
},
{
"name": "ecs.capability.task-eni"
}
],
"placementConstraints": [],
"compatibilities": [
"EC2",
"FARGATE"
],
"requiresCompatibilities": [
"FARGATE"
],
"cpu": "256",
"memory": "2048",
"runtimePlatform": {
"cpuArchitecture": "X86_64",
"operatingSystemFamily": "LINUX"
},
"tags": []
}
26 changes: 26 additions & 0 deletions .github/scripts/delete-comments.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
module.exports = async ({github, context}) => {

const response_data = await github.paginate(github.rest.issues.listComments, {
issue_number: context.payload.number, // Pr number
owner: context.repo.owner,
repo: context.repo.repo,
}
);

if (response_data) {
for (const comment of response_data) {
// Find the bots previous comments
if (comment.user.login === 'github-actions[bot]' && comment.body.indexOf('preview environments') !== -1) {
// If we have an existing comment, delete it
// Reference: https://octokit.github.io/rest.js/v19#issues-delete-comment
github.rest.issues.deleteComment({
comment_id: comment.id,
issue_number: context.payload.number, // Pr number
owner: context.repo.owner,
repo: context.repo.repo,
});
}
}
}

};
111 changes: 111 additions & 0 deletions .github/scripts/delete-tasks.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
module.exports = async ({
github,
context,
require,
exec,
partial_value,
pr_id_key,
pr_id_value,
ecs_cluster,
aws_listener_arn,
aws_load_balancer_arn
}) => {

// Needed for aws output
let output = '';
const options = {};
options.listeners = {
stdout: (data) => {
output += data.toString();
},
};

// Remove the load balancer stuff first
// Rules
await exec.exec(`aws elbv2 describe-rules --listener-arn ${aws_listener_arn} --query "Rules" --output json`, [], options);
const rules = JSON.parse(output);
output = '';

const rule_arns_to_destroy = [];

for (const listener_rule of rules) {

// describe doesn't include tags...
await exec.exec(`aws elbv2 describe-tags --resource-arns ${listener_rule['RuleArn']} --query "TagDescriptions[0].Tags" --output json`, [], options);
const tags = JSON.parse(output);
output = '';

for (const tag of tags) {
// Look for our PR_ID_KEY, and if we're doing a partial string match, see if PR_ID_VALUE is in value, otherwise do an exact match
if (tag['Key'] === pr_id_key && ((partial_value && tag['Value'].indexOf(pr_id_value) === 0) || (!partial_value && tag['Value'] === pr_id_value))) {
rule_arns_to_destroy.push(listener_rule['RuleArn']);
}
}

for (const rule of rule_arns_to_destroy) {
await exec.exec(`aws elbv2 delete-rule --rule-arn "${rule}"`, [], options);
output = '';
}
}

// Target groups
const target_groups_to_destroy = [];

await exec.exec(`aws elbv2 describe-target-groups --load-balancer-arn ${aws_load_balancer_arn} --query "TargetGroups" --output json`, [], options);
const target_groups = JSON.parse(output);
output = '';

for (const target_group of target_groups) {

// describe doesn't include tags...
await exec.exec(`aws elbv2 describe-tags --resource-arns ${target_group['TargetGroupArn']} --query "TagDescriptions[0].Tags" --output json`, [], options);
const tags = JSON.parse(output);
output = '';


for (const tag of tags) {
// Look for our PR_ID_KEY, and if we're doing a partial string match, see if PR_ID_VALUE is in value, otherwise do an exact match
if (tag['Key'] === pr_id_key && ((partial_value && tag['Value'].indexOf(pr_id_value) === 0) || (!partial_value && tag['Value'] === pr_id_value))) {
target_groups_to_destroy.push(target_group['TargetGroupArn']);
}
}

for (const target_group_arn of target_groups_to_destroy) {
await exec.exec(`aws elbv2 delete-target-group --target-group-arn "${target_group_arn}"`, [], options);
output = '';
}
}
// --

// Grab a list of tasks
await exec.exec(`aws ecs list-tasks --cluster ${ecs_cluster} --query "taskArns" --output json`, [], options);
const tasks = JSON.parse(output);
output = '';

let task_arns_to_destroy = [];

// Look for tasks created by this PR which are identified by our pr_id_key and pr_id_value
// There shouldn't ever be more than one task per PR, but y'know just in case...
for (const task_arn of tasks) {
await exec.exec(`aws ecs describe-tasks --cluster ${ecs_cluster} --task "${task_arn}" --query "tasks[0].tags" --include TAGS --output json`, [], options);
const tags = JSON.parse(output);
output = '';

for (const tag of tags) {
// Look for our PR_ID_KEY, and if we're doing a partial string match, see if PR_ID_VALUE is in value, otherwise do an exact match
if (tag['key'] === pr_id_key && ((partial_value && tag['value'].indexOf(pr_id_value) === 0) || (!partial_value && tag['value'] === pr_id_value))) {
task_arns_to_destroy.push(task_arn);
break;
}
}
}

// Delete them all!
for (const task_arn of task_arns_to_destroy) {
await exec.exec(`aws ecs stop-task --cluster ${ecs_cluster} --task "${task_arn}" --output json`, [], options);
output = '';
}

const delete_comments = await require('./.github/scripts/delete-comments.js');
await delete_comments({github: github, context: context});
};
41 changes: 41 additions & 0 deletions .github/scripts/post-comment-with-link.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
module.exports = async ({core, github, context, require, website_url_file, start_page_url_file}) => {
let website_url = null;
let start_page_url = null;
try {
website_url = await require(website_url_file);
} catch {
// Ignore
}
try {
start_page_url = await require(start_page_url_file);
} catch {
// Ignore
}

if (!website_url && !start_page_url) {
core.setFailed("No urls to post!");
return;
}

// Plural depending on if we have both urls available.
const messages = [
website_url && start_page_url ? 'Your preview environments have been started and will be available shortly at:' :
'Your preview environment has started and will be available shortly at:'
];

if (website_url) {
messages.push(`thunderbird.net: ${website_url}`);
}
if (start_page_url) {
messages.push(`start.thunderbird.net: ${start_page_url}`);
}

// Add a check that succeeds with output url :)
// Reference: https://octokit.github.io/rest.js/v19#issues-create-comment
github.rest.issues.createComment({
issue_number: context.payload.number, // Pr number
owner: context.repo.owner,
repo: context.repo.repo,
body: messages.join('\n')
});
};
Loading