chore(vscode): update to 1.53.2
These conflicts will be resolved in the following commits. We do it this way so that PR review is possible.
This commit is contained in:
217
lib/vscode/.github/actions/build-chat/src/main.ts
vendored
Normal file
217
lib/vscode/.github/actions/build-chat/src/main.ts
vendored
Normal file
@ -0,0 +1,217 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import * as core from '@actions/core';
|
||||
import { Octokit, RestEndpointMethodTypes } from '@octokit/rest';
|
||||
import { WebClient } from '@slack/web-api';
|
||||
import * as storage from 'azure-storage';
|
||||
import { WritableStreamBuffer } from 'stream-buffers';
|
||||
|
||||
(async () => {
|
||||
const actionUrl = core.getInput('workflow_run_url');
|
||||
const url = actionUrl || 'https://api.github.com/repos/microsoft/vscode/actions/runs/503514090';
|
||||
console.log(url);
|
||||
const parts = url.split('/');
|
||||
const owner = parts[parts.length - 5];
|
||||
const repo = parts[parts.length - 4];
|
||||
const runId = parseInt(parts[parts.length - 1], 10);
|
||||
if (actionUrl) {
|
||||
await handleNotification(owner, repo, runId);
|
||||
} else {
|
||||
const results = await buildComplete(owner, repo, runId);
|
||||
for (const message of [...results.logMessages, ...results.messages]) {
|
||||
console.log(message);
|
||||
}
|
||||
}
|
||||
})()
|
||||
.then(null, console.error);
|
||||
|
||||
const testChannels = ['bot-log', 'bot-test-log'];
|
||||
|
||||
async function handleNotification(owner: string, repo: string, runId: number) {
|
||||
|
||||
const results = await buildComplete(owner, repo, runId);
|
||||
if (results.logMessages.length || results.messages.length) {
|
||||
|
||||
const web = new WebClient(process.env.SLACK_TOKEN);
|
||||
const memberships = await listAllMemberships(web);
|
||||
const memberTestChannels = memberships.filter(m => testChannels.indexOf(m.name) !== -1);
|
||||
|
||||
for (const message of results.logMessages) {
|
||||
for (const testChannel of memberTestChannels) {
|
||||
await web.chat.postMessage({
|
||||
text: message,
|
||||
link_names: true,
|
||||
channel: testChannel.id,
|
||||
});
|
||||
}
|
||||
}
|
||||
for (const message of results.messages) {
|
||||
for (const channel of memberships) {
|
||||
await web.chat.postMessage({
|
||||
text: message,
|
||||
link_names: true,
|
||||
channel: channel.id,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function buildComplete(owner: string, repo: string, runId: number) {
|
||||
console.log(`buildComplete: https://github.com/${owner}/${repo}/actions/runs/${runId}`);
|
||||
const auth = `token ${process.env.GITHUB_TOKEN}`;
|
||||
const octokit = new Octokit({ auth });
|
||||
const buildResult = (await octokit.actions.getWorkflowRun({
|
||||
owner,
|
||||
repo,
|
||||
run_id: runId,
|
||||
})).data;
|
||||
if (buildResult.head_branch !== 'master' && !buildResult.head_branch?.startsWith('release/')) {
|
||||
console.error('Private branch. Terminating.')
|
||||
return { logMessages: [], messages: [] };
|
||||
}
|
||||
|
||||
// const buildQuery = `${buildsApiUrl}?$top=10&maxTime=${buildResult.finishTime}&definitions=${buildResult.definition.id}&branchName=${buildResult.sourceBranch}&resultFilter=${results.join(',')}&api-version=5.0-preview.4`;
|
||||
|
||||
const buildResults = (await octokit.actions.listWorkflowRuns({
|
||||
owner,
|
||||
repo,
|
||||
workflow_id: buildResult.workflow_id,
|
||||
branch: buildResult.head_branch || undefined,
|
||||
per_page: 5, // More returns 502s.
|
||||
})).data.workflow_runs
|
||||
.filter(run => run.status === 'completed');
|
||||
|
||||
const currentBuildIndex = buildResults.findIndex(build => build.id === buildResult.id);
|
||||
if (currentBuildIndex === -1) {
|
||||
console.error('Build not on first page. Terminating.')
|
||||
console.error(buildResults.map(({ id, status, conclusion }) => ({ id, status, conclusion })));
|
||||
return { logMessages: [], messages: [] };
|
||||
}
|
||||
const slicedResults = buildResults.slice(currentBuildIndex, currentBuildIndex + 2);
|
||||
const builds = slicedResults
|
||||
.map<Build>((build, i, array) => ({
|
||||
data: build,
|
||||
previousSourceVersion: i < array.length - 1 ? array[i + 1].head_sha : undefined,
|
||||
authors: [],
|
||||
buildHtmlUrl: build.html_url,
|
||||
changesHtmlUrl: '',
|
||||
}));
|
||||
const logMessages = builds.slice(0, 1)
|
||||
.map(build => `Id: ${build.data.id} | Branch: ${build.data.head_branch} | Conclusion: ${build.data.conclusion} | Created: ${build.data.created_at} | Updated: ${build.data.updated_at}`);
|
||||
const transitionedBuilds = builds.filter((build, i, array) => i < array.length - 1 && transitioned(build, array[i + 1]));
|
||||
await Promise.all(transitionedBuilds
|
||||
.map(async build => {
|
||||
if (build.previousSourceVersion) {
|
||||
const cmp = await compareCommits(octokit, owner, repo, build.previousSourceVersion, build.data.head_sha);
|
||||
const commits = cmp.data.commits;
|
||||
const authors = new Set<string>([
|
||||
...commits.map((c: any) => c.author.login),
|
||||
...commits.map((c: any) => c.committer.login),
|
||||
]);
|
||||
authors.delete('web-flow'); // GitHub Web UI committer
|
||||
build.authors = [...authors];
|
||||
build.changesHtmlUrl = `https://github.com/${owner}/${repo}/compare/${build.previousSourceVersion.substr(0, 7)}...${build.data.head_sha.substr(0, 7)}`; // Shorter than: cmp.data.html_url
|
||||
}
|
||||
}));
|
||||
const vscode = repo === 'vscode';
|
||||
const name = vscode ? `VS Code ${buildResult.name} Build` : buildResult.name;
|
||||
// TBD: `Requester: ${vstsToSlackUser(build.requester, build.degraded)}${pingBenForSmokeTests && releaseBuild && build.result === 'partiallySucceeded' ? ' | Ping: @bpasero' : ''}`
|
||||
const accounts = await readAccounts();
|
||||
const githubAccountMap = githubToAccounts(accounts);
|
||||
const messages = transitionedBuilds.map(build => `${name}
|
||||
Result: ${build.data.conclusion} | Branch: ${build.data.head_branch} | Authors: ${githubToSlackUsers(githubAccountMap, build.authors, build.degraded).sort().join(', ') || `None (rebuild)`}
|
||||
Build: ${build.buildHtmlUrl}
|
||||
Changes: ${build.changesHtmlUrl}`);
|
||||
return { logMessages, messages };
|
||||
}
|
||||
|
||||
const conclusions = ['success', 'failure']
|
||||
|
||||
function transitioned(newer: Build, older: Build) {
|
||||
const newerResult = newer.data.conclusion || 'success';
|
||||
const olderResult = older.data.conclusion || 'success';
|
||||
if (newerResult === olderResult) {
|
||||
return false;
|
||||
}
|
||||
if (conclusions.indexOf(newerResult) > conclusions.indexOf(olderResult)) {
|
||||
newer.degraded = true;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
async function compareCommits(octokit: Octokit, owner: string, repo: string, base: string, head: string) {
|
||||
return octokit.repos.compareCommits({ owner, repo, base, head });
|
||||
}
|
||||
|
||||
function githubToSlackUsers(githubToAccounts: Record<string, Accounts>, githubUsers: string[], at?: boolean) {
|
||||
return githubUsers.map(g => githubToAccounts[g] ? `${at ? '@' : ''}${githubToAccounts[g].slack}` : g);
|
||||
}
|
||||
|
||||
interface Accounts {
|
||||
github: string;
|
||||
slack: string;
|
||||
vsts: string;
|
||||
}
|
||||
|
||||
function githubToAccounts(accounts: Accounts[]) {
|
||||
return accounts.reduce((m, e) => {
|
||||
m[e.github] = e;
|
||||
return m;
|
||||
}, <Record<string, Accounts>>{});
|
||||
}
|
||||
|
||||
async function readAccounts() {
|
||||
const connectionString = process.env.BUILD_CHAT_STORAGE_CONNECTION_STRING;
|
||||
if (!connectionString) {
|
||||
console.error('Connection string missing.');
|
||||
return [];
|
||||
}
|
||||
const buf = await readFile(connectionString, 'config', '/', 'accounts.json');
|
||||
return JSON.parse(buf.toString()) as Accounts[];
|
||||
}
|
||||
|
||||
async function readFile(connectionString: string, share: string, directory: string, filename: string) {
|
||||
return new Promise<Buffer>((resolve, reject) => {
|
||||
const stream = new WritableStreamBuffer()
|
||||
const fileService = storage.createFileService(connectionString);
|
||||
fileService.getFileToStream(share, directory, filename, stream, err => {
|
||||
if (err) {
|
||||
reject(err);
|
||||
} else {
|
||||
const contents = stream.getContents();
|
||||
if (contents) {
|
||||
resolve(contents);
|
||||
} else {
|
||||
reject(new Error('No content'));
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
interface AllChannels {
|
||||
channels: {
|
||||
id: string;
|
||||
name: string;
|
||||
is_member: boolean;
|
||||
}[];
|
||||
}
|
||||
|
||||
async function listAllMemberships(web: WebClient) {
|
||||
const groups = await web.conversations.list({ types: 'public_channel,private_channel' }) as unknown as AllChannels;
|
||||
return groups.channels
|
||||
.filter(c => c.is_member);
|
||||
}
|
||||
|
||||
interface Build {
|
||||
data: RestEndpointMethodTypes['actions']['getWorkflowRun']['response']['data'];
|
||||
previousSourceVersion: string | undefined;
|
||||
authors: string[];
|
||||
buildHtmlUrl: string;
|
||||
changesHtmlUrl: string;
|
||||
degraded?: boolean;
|
||||
}
|
Reference in New Issue
Block a user