Skip to content

Commit

Permalink
Use Slack threads to produce periodic report (#91)
Browse files Browse the repository at this point in the history
Currently, the GitHub workflow that runs `module-lint` once a week posts
each package's lint report in a separate Slack message. This works fine
for now because there are only a few packages we are running
`module-lint` against, but once we scale this up, it will create a lot
of noise.

To combat this, this commit updates the workflow to produce a summary
message and then post all of the per-package messages in a thread.
  • Loading branch information
mcmire authored Jul 24, 2024
1 parent 3f32dac commit 4dd889f
Show file tree
Hide file tree
Showing 10 changed files with 684 additions and 59 deletions.
16 changes: 13 additions & 3 deletions .eslintrc.js
Original file line number Diff line number Diff line change
Expand Up @@ -29,20 +29,30 @@ module.exports = {
},

{
files: ['src/cli.ts'],
files: ['src/cli.ts', '.github/scripts/**/*.ts'],
parserOptions: {
sourceType: 'script',
},
rules: {
// It's okay if this file has a shebang; it's meant to be executed
// directly.
// These are scripts and are meant to have shebangs.
'n/shebang': 'off',
},
},

{
files: ['.github/scripts/**/*.ts'],
parserOptions: {
sourceType: 'script',
},
rules: {
'n/no-process-env': 'off',
},
},
],

ignorePatterns: [
'!.eslintrc.js',
'!.github/',
'!.prettierrc.js',
'.yarn/',
'dist/',
Expand Down
265 changes: 265 additions & 0 deletions .github/scripts/determine-initial-slack-payload.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,265 @@
#!/usr/bin/env tsx

import { setOutput } from '@actions/core';
import fs from 'fs';
import path from 'path';

type Inputs = {
githubRepository: string;
githubRunId: string;
moduleLintRunsDirectory: string;
channelId: string;
isRunningOnCi: boolean;
};

/**
* Obtains the inputs for this script from environment variables.
*
* @returns The inputs for this script.
*/
function getInputs(): Inputs {
return {
githubRepository: requireEnvironmentVariable('GITHUB_REPOSITORY'),
githubRunId: requireEnvironmentVariable('GITHUB_RUN_ID'),
moduleLintRunsDirectory: requireEnvironmentVariable(
'MODULE_LINT_RUNS_DIRECTORY',
),
channelId: requireEnvironmentVariable('SLACK_CHANNEL_ID'),
isRunningOnCi: process.env.CI !== undefined,
};
}

/**
* Obtains the given environment variable, throwing if it has not been set.
*
* @param variableName - The name of the desired environment variable.
* @returns The value of the given environment variable.
* @throws if the given environment variable has not been set.
*/
function requireEnvironmentVariable(variableName: string): string {
const value = process.env[variableName];

if (value === undefined || value === '') {
throw new Error(`Missing environment variable ${variableName}.`);
}

return value;
}

/**
* Reads the exit code files produced from previous `module-lint` runs.
*
* @param moduleLintRunsDirectory - The directory that holds the exit code
* files.
* @returns An array of exit codes.
*/
async function readModuleLintExitCodeFiles(moduleLintRunsDirectory: string) {
const entryNames = (await fs.promises.readdir(moduleLintRunsDirectory)).map(
(entryName) => path.join(moduleLintRunsDirectory, entryName),
);
const exitCodeFileNames = entryNames.filter((entry) =>
entry.endsWith('--exitcode.txt'),
);
return Promise.all(
exitCodeFileNames.map(async (exitCodeFileName) => {
const content = (
await fs.promises.readFile(exitCodeFileName, 'utf8')
).trim();
const exitCode = Number(content);
if (Number.isNaN(exitCode)) {
throw new Error(`Could not parse '${content}' as exit code`);
}
return exitCode;
}),
);
}

/**
* Generates the Slack message that will appear when all `module-lint` runs are
* successful.
*
* @returns The Slack payload blocks.
*/
function constructSlackPayloadBlocksForSuccessfulRun() {
return [
{
type: 'rich_text',
elements: [
{
type: 'rich_text_section',
elements: [
{
type: 'emoji',
name: 'package',
},
{
type: 'text',
text: ' ',
},
{
type: 'text',
text: 'A new package standardization report is available.',
style: {
bold: true,
},
},
{
type: 'text',
text: '\n\nGreat work! Your team has ',
},
{
type: 'text',
text: '5 repositories',
style: {
bold: true,
},
},
{
type: 'text',
text: ' that fully align with the module template.\n\n',
},
{
type: 'text',
text: 'Open this thread to view more details:',
},
{
type: 'emoji',
name: 'point_right',
},
],
},
],
},
];
}

/**
* Generates the Slack message that will appear when some `module-lint` runs are
* unsuccessful.
*
* @param inputs - The inputs to this script.
* @returns The Slack payload blocks.
*/
function constructSlackPayloadBlocksForUnSuccessfulRun(inputs: Inputs) {
return [
{
type: 'rich_text',
elements: [
{
type: 'rich_text_section',
elements: [
{
type: 'emoji',
name: 'package',
},
{
type: 'text',
text: ' ',
},
{
type: 'text',
text: 'A new package standardization report is available.',
style: {
bold: true,
},
},
{
type: 'text',
text: '\n\nYour team has ',
},
{
type: 'text',
text: '4 repositories',
style: {
bold: true,
},
},
{
type: 'text',
text: ' that require maintenance in order to align with the module template. This is important for maintaining conventions across MetaMask and adhering to our security principles.\n\n',
},
{
type: 'link',
text: 'View this run',
url: `https://github.com/${inputs.githubRepository}/actions/runs/${inputs.githubRunId}`,
},
{
type: 'text',
text: ', or open this thread to view more details:',
},
{
type: 'emoji',
name: 'point_right',
},
],
},
],
},
];
}

/**
* Constructs the payload that will be used to post a message in Slack
* containing information about previous `module-lint` runs.
*
* @param inputs - The inputs to this script.
* @param allModuleLintRunsSuccessful - Whether all of the previously linted projects passed
* lint.
* @returns The Slack payload.
*/
function constructSlackPayload(
inputs: Inputs,
allModuleLintRunsSuccessful: boolean,
) {
const text =
'A new package standardization report is available. Open this thread to view more details.';

const blocks = allModuleLintRunsSuccessful
? constructSlackPayloadBlocksForSuccessfulRun()
: constructSlackPayloadBlocksForUnSuccessfulRun(inputs);

if (inputs.isRunningOnCi) {
return {
text,
blocks,
// The Slack API dictates use of this property.
// eslint-disable-next-line @typescript-eslint/naming-convention
icon_url:
'https://raw.githubusercontent.com/MetaMask/action-npm-publish/main/robo.png',
username: 'MetaMask Bot',
channel: inputs.channelId,
};
}

return { blocks };
}

/**
* The entrypoint for this script.
*/
async function main() {
const inputs = getInputs();

const exitCodes = await readModuleLintExitCodeFiles(
inputs.moduleLintRunsDirectory,
);
const allModuleLintRunsSuccessful = exitCodes.every(
(exitCode) => exitCode === 0,
);

const slackPayload = constructSlackPayload(
inputs,
allModuleLintRunsSuccessful,
);

// Offer two different ways of outputting the Slack payload so that this
// script can be run locally and the output can be fed into Slack's Block Kit
// Builder
if (inputs.isRunningOnCi) {
setOutput('SLACK_PAYLOAD', JSON.stringify(slackPayload));
} else {
console.log(JSON.stringify(slackPayload, null, ' '));
}
}

main().catch(console.error);
Loading

0 comments on commit 4dd889f

Please sign in to comment.