Skip to content

Friendlier per-owner subtasks #34

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

Open
wants to merge 13 commits into
base: main
Choose a base branch
from
Open
25 changes: 11 additions & 14 deletions Jenkinsfiles/Jenkinsfile.params
Original file line number Diff line number Diff line change
Expand Up @@ -111,12 +111,8 @@ pipeline {
stage('Confirm settings ') {
steps {
sh '''#!/bin/bash
echo "Windows Browser Branch: $params.WINDOWS_BROWSER_BRANCH"
echo "DDG Extension Branch: $params.EXTENSION_BRANCH"
echo "Apple Browsers Branch: $params.APPLE_BROWSERS_BRANCH"
echo "Pixel Schema Branch: $params.PIXEL_SCHEMA_BRANCH"
echo "Asana Utils Branch: $params.ASANA_UTILS_BRANCH"
echo "Asana Project: $params.ASANA_PROJECT"
echo "User map: $USER_MAP"
echo "Asana Project: $ASANA_PROJECT"
'''
}
}
Expand All @@ -128,7 +124,7 @@ pipeline {
MAIN_DIR="../duckduckgo-privacy-extension/pixel-definitions/"
cd pixel-schema
echo "Starting validation for duckduckgo-privacy-extension..."
./scripts/validateRepo.sh $MAIN_DIR $USER_MAP $params.ASANA_PROJECT
./scripts/validateRepo.sh $MAIN_DIR $USER_MAP $ASANA_PROJECT
exit_code=$?
echo "validateRepo.sh exit code: $exit_code"
cd ..
Expand All @@ -150,7 +146,7 @@ pipeline {
MAIN_DIR="../windows-browser/PixelDefinitions/"
cd pixel-schema
echo "Starting validation for windows-browser..."
./scripts/validateRepo.sh $MAIN_DIR $USER_MAP $params.ASANA_PROJECT
./scripts/validateRepo.sh $MAIN_DIR $USER_MAP $ASANA_PROJECT
exit_code=$?
echo "validateRepo.sh exit code: $exit_code"
cd ..
Expand All @@ -175,7 +171,7 @@ pipeline {
MAIN_DIR="../apple-browsers/macOS/PixelDefinitions/"
cd pixel-schema
echo "Starting validation for apple-browsers (macOS)..."
./scripts/validateRepo.sh $MAIN_DIR $USER_MAP $params.ASANA_PROJECT
./scripts/validateRepo.sh $MAIN_DIR $USER_MAP $ASANA_PROJECT
exit_code=$?
echo "validateRepo.sh exit code: $exit_code"
cd ..
Expand All @@ -200,7 +196,7 @@ pipeline {
MAIN_DIR="../apple-browsers/iOS/PixelDefinitions/"
cd pixel-schema
echo "Starting validation for apple-browsers (iOS)..."
./scripts/validateRepo.sh $MAIN_DIR $USER_MAP $params.ASANA_PROJECT
./scripts/validateRepo.sh $MAIN_DIR $USER_MAP $ASANA_PROJECT
exit_code=$?
echo "validateRepo.sh exit code: $exit_code"
cd ..
Expand All @@ -222,13 +218,14 @@ pipeline {

sh '''#!/bin/bash
echo "Attempting to create Asana task for failure..."
DATE=`date --rfc-3339=seconds | sed -e 's/ /-/g'`
echo $DATE

ddg-perl /usr/local/ddg/sysadmin/scripts/asana_create_task.pl \
--assignee jmatthews@duckduckgo.com \
--description "Jenkins pixel validation failed" \
--project_id $params.ASANA_PROJECT \
--no_duplicates \
"pixel validation failed"
--description "Pixel validation failed see console output at https://jenkins.duckduckgo.com/job/PixelValidation/" \
--project_id $ASANA_PROJECT \
"Pixel validation failed $DATE"
'''
}
}
Expand Down
115 changes: 99 additions & 16 deletions live_validation_scripts/asana_reports.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -70,10 +70,12 @@ function readAsanaNotifyFile() {
}

async function createOwnerSubtask(owner, parentTaskGid) {
console.log(`Creating subtask for ${owner}...`);

const thisOwnersPixelsWithErrors = [];
for (const pixel of Object.values(pixelsWithErrors)) {
for (const [pixelName, pixel] of Object.entries(pixelsWithErrors)) {
if (pixel.owners && pixel.owners.includes(owner)) {
thisOwnersPixelsWithErrors.push(pixel);
thisOwnersPixelsWithErrors.push({ name: pixelName, ...pixel });
}
}

Expand All @@ -82,17 +84,69 @@ async function createOwnerSubtask(owner, parentTaskGid) {
const tempFilePath = path.join(dirPath, `pixel_with_errors_${owner}.json`);
fs.writeFileSync(tempFilePath, JSON.stringify(thisOwnersPixelsWithErrors, null, 2));

const subtaskNotes = `<body>
${thisOwnersPixelsWithErrors.length} pixels with errors - check the attachment for details.
New to these reports? See <a href="https://app.asana.com/1/137249556945/project/1210856607616307/task/1210948723611775?focus=true">View task</a>
</body>`;
const pixelWord = thisOwnersPixelsWithErrors.length === 1 ? 'pixel' : 'pixels';
const header = `${thisOwnersPixelsWithErrors.length} ${pixelWord} with errors. Check the attachment for more details.
New to these reports? See <a href="https://app.asana.com/1/137249556945/project/1210856607616307/task/1210948723611775?focus=true">View task</a>`;

const pixelNameWidth = 200;
const errorTypeWidth = 400;

const table = `
<table>
<tr>
<td data-cell-widths="${pixelNameWidth}"><strong>Pixel Name</strong></td>
<td data-cell-widths="${errorTypeWidth}"><strong>Error Type</strong></td>
</tr>
${thisOwnersPixelsWithErrors
.map((pixel) => {
// Get error types (excluding 'owners' property) and limit to first 3
const allErrorTypes = Object.keys(pixel).filter((key) => key !== 'owners' && key !== 'name');
const errorTypes = allErrorTypes.slice(0, 3);

// Create rows for each error type
const rows = [];
errorTypes.forEach((errorType, index) => {
const examples = Array.from(pixel[errorType]);
if (examples.length > 0) {
/*
// Truncate long error messages for readability
let errorMsg = examples[0];
// Truncate to first 150 characters and add ellipsis if longer
if (errorMsg.length > 150) {
errorMsg = errorMsg.substring(0, 150) + '...';
}
*/

// Only show pixel name in the first row
const pixelNameCell =
index === 0
? `<td rowspan="${errorTypes.length}" data-cell-widths="${pixelNameWidth}">${pixel.name}</td>`
: '';

// HTML escape the error type to prevent breaking the table
const escapedErrorType = errorType
.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;');

rows.push(`<tr>${pixelNameCell}<td data-cell-widths="${errorTypeWidth}">${escapedErrorType}</td></tr>`);
}
});

return rows.join('');
})
.join('')}
</table>
`;
const taskNotes = `<body> ${header} ${table}</body>`;

// Make a subtask for each owner
const subtaskName = `${owner}`;
const subtaskName = `Failing pixels report for owner ${owner}`;
const subtaskData = {
workspace: DDG_ASANA_WORKSPACEID,
name: subtaskName,
html_notes: subtaskNotes,
html_notes: taskNotes,
text: 'Per-owner subtask',
parent: parentTaskGid,
};
Expand All @@ -105,12 +159,21 @@ async function createOwnerSubtask(owner, parentTaskGid) {
data: subtaskData,
};

const opts = {};
const subtaskResult = await tasksApi.createTask(subtaskBody, opts);
console.log(`Subtask created for ${owner}: ${subtaskResult.data.gid}`);
let subtaskResult = null;
try {
const opts = {};
subtaskResult = await tasksApi.createTask(subtaskBody, opts);
console.log(`Subtask created for ${owner}: ${subtaskResult.data.gid}`);
} catch (subtaskError) {
console.error(`Error creating subtask for ${owner}:`, subtaskError.message);
console.error('Full error:', subtaskError);
console.error(`Task notes: ${taskNotes}`);
return false;
}

try {
console.log(`Attempting to attach ${thisOwnersPixelsWithErrors.length} pixels with errors`);
const pixelWordForLog = thisOwnersPixelsWithErrors.length === 1 ? 'pixel' : 'pixels';
console.log(`Attempting to attach ${thisOwnersPixelsWithErrors.length} ${pixelWordForLog} with errors`);

const attachmentResult = await superagent.default
.post('https://app.asana.com/api/1.0/attachments')
Expand All @@ -124,14 +187,18 @@ async function createOwnerSubtask(owner, parentTaskGid) {
} catch (attachmentError) {
console.error(`Error adding attachment for ${dirPath}:`, attachmentError.message);
console.error('Full error:', attachmentError);
return false;
}
}
return true;
}

async function main() {
console.log('AsanaWorkspace ID: ' + DDG_ASANA_WORKSPACEID);
console.log('Asana Pixel Validation Project: ', DDG_ASANA_PIXEL_VALIDATION_PROJECT);

let hasErrors = false;

// Load user map
if (!fs.existsSync(argv.userMapFile)) {
console.error(`User map file ${argv.userMapFile} does not exist!`);
Expand Down Expand Up @@ -170,7 +237,7 @@ async function main() {

// Build ownersWithErrors from pixelsWithErrors
const ownersSet = new Set();
for (const pixel of Object.values(pixelsWithErrors)) {
for (const [, pixel] of Object.entries(pixelsWithErrors)) {
if (pixel.owners) {
pixel.owners.forEach((owner) => ownersSet.add(owner));
}
Expand All @@ -195,8 +262,9 @@ async function main() {

// For valid formatting options: https://developers.asana.com/docs/rich-text#reading-rich-text
if (numPixelsWithErrors > 0) {
topLevelStatement = `${numPixelsWithErrors} pixels with errors - check the attachment for details.
New to these reports? See <a href="https://app.asana.com/1/137249556945/project/1210856607616307/task/1210948723611775?focus=true">View task</a>
const topLevelPixelWord = numPixelsWithErrors === 1 ? 'pixel' : 'pixels';
topLevelStatement = `${numPixelsWithErrors} ${topLevelPixelWord} with errors - check the attachment for details.
New to these reports? See <a href="https://app.asana.com/1/137249556945/project/1210856607616307/task/1210948723611775?focus=true">View task</a>
`;
} else {
topLevelStatement = `No errors found.`;
Expand Down Expand Up @@ -258,18 +326,33 @@ async function main() {
} catch (attachmentError) {
console.error(`Error adding attachment for ${dirPath}:`, attachmentError.message);
console.error('Full error:', attachmentError);
hasErrors = true;
}

// Even if there are no errors, continue to create per-owner subtasks where possible
if (MAKE_PER_OWNER_SUBTASKS) {
// Create subtasks for each owner
for (const owner of ownersWithErrors) {
await createOwnerSubtask(owner, taskGid);
const success = await createOwnerSubtask(owner, taskGid);
if (!success) {
console.error(`Error creating subtask for ${owner}`);
hasErrors = true;
}
}
}
}
} catch (error) {
console.error(`Error creating task for ${dirPath}:`, error);
process.exit(1);
}

if (hasErrors) {
console.error('There were errors during Asana task creation');
process.exit(1);
}

console.log('Asana tasks created successfully');
process.exit(0);
}

main().catch(console.error);