diff --git a/.github/workflows/generate-weekly-report.yml b/.github/workflows/generate-weekly-report.yml
index f08e9f44bbc5..5660fa6cf261 100644
--- a/.github/workflows/generate-weekly-report.yml
+++ b/.github/workflows/generate-weekly-report.yml
@@ -4,233 +4,21 @@
name: 'Generate Weekly Report'
on:
workflow_dispatch:
- push:
- branches: [ chore/weekly-report-gen ]
+ schedule:
+ # run every tuesday at 1am UTC
+ - cron: "0 1 * * 2"
jobs:
get_issues:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
+ - run: npm install js-yaml
+ working-directory: ./.github/workflows/scripts
- uses: actions/github-script@v6
id: get-issues
with:
retries: 3
script: |
- async function getIssues(github, queryParams, filterPrs = true) {
- let allIssues = [];
- try {
- while (true) {
- const response = await github.issues.listForRepo(queryParams);
- // filter out pull requests
- const issues = filterPrs ? response.data.filter(issue => !issue.pull_request) : response.data;
- allIssues = allIssues.concat(issues);
-
- // Check the 'link' header to see if there are more pages
- const linkHeader = response.headers.link;
- if (!linkHeader || !linkHeader.includes('rel="next"')) {
- break;
- }
-
- queryParams.page++;
- }
- return allIssues;
- } catch (error) {
- console.error('Error fetching issues:', error);
- return [];
- }
- }
-
- function genLookbackDates() {
- const now = new Date();
- const midnightYesterday = new Date(now.getFullYear(), now.getMonth(), now.getDate() - 1);
- const sevenDaysAgo = new Date(midnightYesterday);
- sevenDaysAgo.setDate(midnightYesterday.getDate() - 7);
- return { sevenDaysAgo, midnightYesterday};
- }
-
- function filterOnDateRange({ issue, sevenDaysAgo, midnightYesterday }) {
- const createdAt = new Date(issue.created_at);
- return createdAt >= sevenDaysAgo && createdAt <= midnightYesterday;
- }
-
- async function getNewIssues(github) {
- const { sevenDaysAgo, midnightYesterday } = genLookbackDates();
- const queryParams = {
- owner: 'open-telemetry',
- repo: 'opentelemetry-collector-contrib',
- state: 'all', // To get both open and closed issues
- per_page: 100, // Number of items per page (maximum allowed)
- page: 1, // Start with page 1
- since: sevenDaysAgo.toISOString(),
- };
-
- try {
- const allIssues = await getIssues(github, queryParams)
- const filteredIssues = allIssues.filter(issue => filterOnDateRange({ issue, sevenDaysAgo, midnightYesterday }));
- return filteredIssues;
- } catch (error) {
- console.error('Error fetching issues:', error);
- return [];
- }
- }
-
- async function getTargetLabelIssues(octokit, labels, filterPrs) {
- const queryParams = {
- owner: 'open-telemetry',
- repo: 'opentelemetry-collector-contrib',
- state: 'open',
- per_page: 100, // Number of items per page (maximum allowed)
- page: 1, // Start with page 1
- labels
- };
- try {
- const allIssues = await getIssues(octokit, queryParams, filterPrs)
- return allIssues;
- } catch (error) {
- console.error('Error fetching issues:', error);
- return [];
- }
- }
-
- async function getIssuesData(github) {
- const targetLabels = {
- "needs triage": {
- filterPrs: true,
- alias: "issuesTriage",
- },
- "ready to merge": {
- filterPrs: false,
- alias: "issuesReadyToMerge",
- },
- "Sponsor Needed": {
- filterPrs: true,
- alias: "issuesSponsorNeeded",
- },
- };
-
- const issuesNew = await getNewIssues(github.rest);
- const issuesWithLabels = {};
- for (const lbl of Object.keys(targetLabels)) {
- const filterPrs = targetLabels[lbl].filterPrs;
- const resp = await getTargetLabelIssues(github.rest, lbl, filterPrs);
- issuesWithLabels[lbl] = resp;
- }
-
- // tally results
- const stats = {
- issuesNew: {
- title: "New issues",
- count: 0,
- data: []
- },
- issuesTriage: {
- title: "Issues needing triage",
- count: 0,
- data: []
- },
- issuesReadyToMerge: {
- title: "Issues ready to merge",
- count: 0,
- data: []
- },
- issuesSponsorNeeded: {
- title: "Issues needing sponsorship",
- count: 0,
- data: []
- },
- issuesNewSponsorNeeded: {
- title: "New issues needing sponsorship",
- count: 0,
- data: []
- },
- }
-
- // add new issues
- issuesNew.forEach(issue => {
- stats.issuesNew.count++;
- const { url, title, number } = issue;
- stats.issuesNew.data.push({ url, title, number });
- });
-
- // add issues with labels
- for (const lbl of Object.keys(targetLabels)) {
- const alias = targetLabels[lbl].alias;
- stats[alias].count = issuesWithLabels[lbl].length;
- stats[alias].data = issuesWithLabels[lbl].map(issue => {
- const { url, title, number } = issue;
- return { url, title, number };
- })
- }
-
- // add new issues with sponsor needed label
- const { sevenDaysAgo, midnightYesterday } = genLookbackDates();
- const sponsorNeededIssues = issuesWithLabels["Sponsor Needed"].filter(issue => filterOnDateRange({ issue, sevenDaysAgo, midnightYesterday }));
- sponsorNeededIssues.forEach(issue => {
- stats.issuesNewSponsorNeeded.count++;
- const { url, title, number } = issue;
- stats.issuesNewSponsorNeeded.data.push({ url, title, number });
- });
- return stats
- }
-
- function generateReport({issuesData}) {
- const out = ['
'];
-
- for (const lbl of Object.keys(issuesData)) {
- const section = [];
- const {count, data, title} = issuesData[lbl];
- section.push(`- ${title}: ${count}`);
- section.push(`
- Issues
-
- ${data.map((issue) => {
- const {url, title, number} = issue;
- return `- #${number} ${title}
`;
- }).join('\n')}
-
- `);
- section.push(' ');
- out.push(section.join('\n'));
- }
-
- out.push('
');
-
- // add json data
- out.push('\n ## JSON Data');
- out.push('');
- out.push( `
- Expand
-
- {
- "issuesData": ${JSON.stringify(issuesData, null, 2)}
- }
-
- `);
- const report = out.join('\n');
- return report;
- }
-
- async function createIssue({github, lookbackData, report}) {
- const title = `Weekly Report: ${lookbackData.sevenDaysAgo.toISOString().slice(0, 10)} - ${lookbackData.midnightYesterday.toISOString().slice(0, 10)}`;
- return github.rest.issues.create({
- // TODO: change
- owner: "kevinslin",
- repo: "opentelemetry-collector-contrib",
- title,
- body: report,
- labels: ["report"]
- })
- }
-
- async function main({ github, context }) {
- const lookbackData = genLookbackDates();
- const issuesData = await getIssuesData(github)
- const report = generateReport({issuesData})
-
- // const fs = require('fs');
- // const report = fs.readFileSync('./fixtures/report.md', 'utf8')
- await createIssue({github, lookbackData, report});
- }
-
- await main({github, context})
\ No newline at end of file
+ const script = require('.github/workflows/scripts/generate-weekly-report.js')
+ await script({github, context})
diff --git a/.github/workflows/scripts/generate-weekly-report.js b/.github/workflows/scripts/generate-weekly-report.js
new file mode 100644
index 000000000000..4c7011a3a4a3
--- /dev/null
+++ b/.github/workflows/scripts/generate-weekly-report.js
@@ -0,0 +1,428 @@
+// Copyright The OpenTelemetry Authors
+// SPDX-License-Identifier: Apache-2.0
+
+const fs = require('fs');
+const path = require('path');
+const yaml = require('js-yaml');
+
+
+const REPO_NAME = "opentelemetry-collector-contrib"
+const REPO_OWNER = "open-telemetry"
+
+function debug(msg) {
+ console.log(JSON.stringify(msg, null, 2))
+}
+
+async function getIssues(github, queryParams, filterPrs = true) {
+ let allIssues = [];
+ try {
+ while (true) {
+ const response = await github.issues.listForRepo(queryParams);
+ // filter out pull requests
+ const issues = filterPrs ? response.data.filter(issue => !issue.pull_request) : response.data;
+ allIssues = allIssues.concat(issues);
+
+ // Check the 'link' header to see if there are more pages
+ const linkHeader = response.headers.link;
+ if (!linkHeader || !linkHeader.includes('rel="next"')) {
+ break;
+ }
+
+ queryParams.page++;
+ }
+ return allIssues;
+ } catch (error) {
+ console.error('Error fetching issues:', error);
+ return [];
+ }
+}
+
+function genLookbackDates() {
+ const now = new Date()
+ const midnightYesterday = new Date(
+ Date.UTC(
+ now.getUTCFullYear(),
+ now.getUTCMonth(),
+ now.getUTCDate(),
+ 0, 0, 0, 0
+ )
+ );
+ const sevenDaysAgo = new Date(midnightYesterday);
+ sevenDaysAgo.setDate(midnightYesterday.getDate() - 7);
+ return { sevenDaysAgo, midnightYesterday };
+}
+
+function filterOnDateRange({ issue, sevenDaysAgo, midnightYesterday }) {
+ const createdAt = new Date(issue.created_at);
+ return createdAt >= sevenDaysAgo && createdAt <= midnightYesterday;
+}
+
+async function getNewIssues({github, context}) {
+ const { sevenDaysAgo, midnightYesterday } = genLookbackDates();
+ const queryParams = {
+ owner: REPO_OWNER,
+ repo: REPO_NAME,
+ state: 'all', // To get both open and closed issues
+ per_page: 100, // Number of items per page (maximum allowed)
+ page: 1, // Start with page 1
+ since: sevenDaysAgo.toISOString(),
+ };
+
+ try {
+ const allIssues = await getIssues(github, queryParams)
+ const filteredIssues = allIssues.filter(issue => filterOnDateRange({ issue, sevenDaysAgo, midnightYesterday }));
+ return filteredIssues;
+ } catch (error) {
+ console.error('Error fetching issues:', error);
+ return [];
+ }
+}
+
+async function getTargetLabelIssues({octokit, labels, filterPrs, context}) {
+ const queryParams = {
+ owner: REPO_OWNER,
+ repo: REPO_NAME,
+ state: 'open',
+ per_page: 100, // Number of items per page (maximum allowed)
+ page: 1, // Start with page 1
+ labels
+ };
+ debug({msg: "fetching issues", queryParams})
+ try {
+ const allIssues = await getIssues(octokit, queryParams, filterPrs)
+ return allIssues;
+ } catch (error) {
+ console.error('Error fetching issues:', error);
+ return [];
+ }
+}
+
+/**
+ * Get data required for issues report
+ */
+async function getIssuesData({github, context}) {
+ const targetLabels = {
+ "needs triage": {
+ filterPrs: true,
+ alias: "issuesTriage",
+ },
+ "ready to merge": {
+ filterPrs: false,
+ alias: "issuesReadyToMerge",
+ },
+ "Sponsor Needed": {
+ filterPrs: true,
+ alias: "issuesSponsorNeeded",
+ },
+ };
+
+ const issuesNew = await getNewIssues({github: github.rest, context});
+ const issuesWithLabels = {};
+ for (const lbl of Object.keys(targetLabels)) {
+ const filterPrs = targetLabels[lbl].filterPrs;
+ const resp = await getTargetLabelIssues({octokit: github.rest, labels: lbl, filterPrs, context});
+ issuesWithLabels[lbl] = resp;
+ }
+
+ // tally results
+ const stats = {
+ issuesNew: {
+ title: "New issues",
+ count: 0,
+ data: []
+ },
+ issuesTriage: {
+ title: "Issues needing triage",
+ count: 0,
+ data: []
+ },
+ issuesReadyToMerge: {
+ title: "Issues ready to merge",
+ count: 0,
+ data: []
+ },
+ issuesSponsorNeeded: {
+ title: "Issues needing sponsorship",
+ count: 0,
+ data: []
+ },
+ issuesNewSponsorNeeded: {
+ title: "New issues needing sponsorship",
+ count: 0,
+ data: []
+ },
+ }
+
+ // add new issues
+ issuesNew.forEach(issue => {
+ stats.issuesNew.count++;
+ const { html_url: url, title, number } = issue;
+ stats.issuesNew.data.push({ url, title, number });
+ });
+
+ // add issues with labels
+ for (const lbl of Object.keys(targetLabels)) {
+ const alias = targetLabels[lbl].alias;
+ stats[alias].count = issuesWithLabels[lbl].length;
+ stats[alias].data = issuesWithLabels[lbl].map(issue => {
+ const { html_url: url, title, number } = issue;
+ return { url, title, number };
+ })
+ }
+
+ // add new issues with sponsor needed label
+ const { sevenDaysAgo, midnightYesterday } = genLookbackDates();
+ const sponsorNeededIssues = issuesWithLabels["Sponsor Needed"].filter(issue => filterOnDateRange({ issue, sevenDaysAgo, midnightYesterday }));
+ sponsorNeededIssues.forEach(issue => {
+ stats.issuesNewSponsorNeeded.count++;
+ const { html_url: url, title, number } = issue;
+ stats.issuesNewSponsorNeeded.data.push({ url, title, number });
+ });
+ return stats
+}
+
+function generateReport({ issuesData, previousReport, componentData }) {
+ const out = [
+ `## Format`,
+ "- `{CATEGORY}: {COUNT} ({CHANGE_FROM_PREVIOUS_WEEK})`",
+ "## Issues Report",
+ ''];
+
+ // generate report for issues
+ for (const lbl of Object.keys(issuesData)) {
+ const section = [``];
+ const { count, data, title } = issuesData[lbl];
+
+ if (previousReport === null) {
+ section.push(`- ${title}: ${count}`);
+ } else {
+ const previousCount = previousReport.issuesData[lbl].count;
+ section.push(`
- ${title}: ${count} (${count - previousCount})`);
+ }
+
+ // generate summary if issues exist
+ // NOTE: the newline after is required for markdown to render correctly
+ if (data.length !== 0) {
+ section.push(`
+ Issues
\n`);
+ section.push(`${data.map((issue) => {
+ const { url, title, number } = issue;
+ return `- [ ] ${title} ([#${number}](${url}))`;
+ }).join('\n')}
+ `);
+ }
+
+ section.push(' ');
+ out.push(section.join('\n'));
+ }
+ out.push('
');
+
+ // generate report for components
+ out.push('\n## Components Report', '');
+ for (const lbl of Object.keys(componentData)) {
+ const section = [``];
+ const data = componentData[lbl];
+ const count = Object.keys(data).length;
+
+ section.push(`- ${lbl}: ${count}`);
+ if (data.length !== 0) {
+ // NOTE: the newline after is required for markdown to render correctly
+ section.push(`
+ Components
\n`);
+ section.push(`${Object.keys(data).map((compName) => {
+ const {stability} = data[compName]
+ return `- [ ] ${compName}: ${JSON.stringify(stability)}`
+ }).join('\n')}
+ `);
+ }
+ section.push(' ');
+ out.push(section.join('\n'));
+ }
+ out.push('
');
+
+ // add json data
+ out.push('\n ## JSON Data');
+ out.push('');
+ out.push(`
+Expand
+
+{
+ "issuesData": ${JSON.stringify(issuesData, null, 2)},
+ "componentData": ${JSON.stringify(componentData, null, 2)}
+}
+
+ `);
+ const report = out.join('\n');
+ return report;
+}
+
+async function createIssue({ github, lookbackData, report, context }) {
+ const title = `Weekly Report: ${lookbackData.sevenDaysAgo.toISOString().slice(0, 10)} - ${lookbackData.midnightYesterday.toISOString().slice(0, 10)}`;
+ return github.rest.issues.create({
+ owner: context.payload.repository.owner.login,
+ repo: "opentelemetry-collector-contrib",
+ title,
+ body: report,
+ labels: ["report"]
+ })
+}
+
+async function getLastWeeksReport({ github, since, context }) {
+ const issues = await github.rest.issues.listForRepo({
+ owner: context.payload.repository.owner.login,
+ repo: 'opentelemetry-collector-contrib',
+ state: 'all', // To get both open and closed issues
+ labels: ["report"],
+ since: since.toISOString(),
+ per_page: 1,
+ direction: "asc"
+ });
+ if (issues.data.length === 0) {
+ return null;
+ }
+ // grab earliest issue if multiple
+ return issues.data[0];
+}
+
+function parseJsonFromText(text) {
+ // Use regex to find the JSON data within the tags
+ const regex = /\s*([\s\S]*?)\s*<\/pre>/;
+ const match = text.match(regex);
+
+ if (match && match[1]) {
+ // Parse the found string to JSON
+ return JSON.parse(match[1]);
+ } else {
+ throw new Error("JSON data not found");
+ }
+}
+
+async function processIssues({ github, context, lookbackData }) {
+ const issuesData = await getIssuesData({github, context});
+
+ const prevReportLookback = new Date(lookbackData.sevenDaysAgo)
+ prevReportLookback.setDate(prevReportLookback.getDate() - 7)
+ const previousReportIssue = await getLastWeeksReport({github, since: prevReportLookback, context});
+ // initialize to zeros
+ let previousReport = null;
+
+ if (previousReportIssue !== null) {
+ const {created_at, id, url, title} = previousReportIssue;
+ debug({ msg: "previous issue", created_at, id, url, title })
+ previousReport = parseJsonFromText(previousReportIssue.body)
+ }
+
+ return {issuesData, previousReport}
+
+
+}
+
+const findFilesByName = (startPath, filter) => {
+ let results = [];
+
+ // Check if directory exists
+ let files;
+ try {
+ files = fs.readdirSync(startPath);
+ } catch (error) {
+ console.error("Error reading directory: ", startPath, error);
+ return [];
+ }
+
+ for (let i = 0; i < files.length; i++) {
+ const filename = path.join(startPath, files[i]);
+ let stat;
+ try {
+ stat = fs.lstatSync(filename);
+ } catch (error) {
+ console.error("Error stating file: ", filename, error);
+ continue;
+ }
+
+ if (stat.isDirectory()) {
+ const innerResults = findFilesByName(filename, filter); // Recursive call
+ results = results.concat(innerResults);
+ } else if (filename.indexOf(filter) >= 0) {
+ results.push(filename);
+ }
+ }
+ return results;
+};
+
+function processFiles(files) {
+ const results = {};
+
+ for (let filePath of files) {
+ const component = path.basename(path.dirname(path.dirname(filePath))); // Top level directory
+ const name = path.basename(path.dirname(filePath)); // Directory of the file
+ const fileData = fs.readFileSync(filePath, 'utf8'); // Read the file as a string
+
+ let data;
+ try {
+ data = yaml.load(fileData); // Parse YAML
+ } catch (err) {
+ console.error(`Error parsing YAML for file ${filePath}:`, err);
+ continue; // Skip this file if there's an error in parsing
+ }
+
+ if (!results[component]) {
+ results[component] = {};
+ }
+
+ results[component][name] = {
+ path: filePath,
+ data
+ };
+ }
+
+ return results;
+}
+
+const processStatusResults = (results) => {
+ const filteredResults = {};
+
+ for (const component in results) {
+ for (const name in results[component]) {
+ const { path, data } = results[component][name];
+
+ if (data && data.status && data.status.stability) {
+ const { stability } = data.status;
+ const statuses = ['unmaintained', 'deprecated'];
+
+ for (const status of statuses) {
+ if (stability[status] && stability[status].length > 0) {
+ if (!filteredResults[status]) {
+ filteredResults[status] = {};
+ }
+ filteredResults[status][name] = { path, stability: data.status.stability, component };
+ }
+ }
+ }
+ }
+ }
+
+ return filteredResults;
+};
+
+async function processComponents() {
+ const results = findFilesByName(`.`, 'metadata.yaml');
+ const resultsClean = processFiles(results)
+ const resultsWithStability = processStatusResults(resultsClean)
+ return resultsWithStability
+
+}
+
+async function main({ github, context }) {
+ debug({msg: "running..."})
+ const lookbackData = genLookbackDates();
+ const {issuesData, previousReport} = await processIssues({ github, context, lookbackData })
+ const componentData = await processComponents()
+
+ const report = generateReport({ issuesData, previousReport, componentData })
+
+ await createIssue({github, lookbackData, report, context});
+}
+
+module.exports = async ({ github, context }) => {
+ await main({ github, context })
+}
\ No newline at end of file