diff --git a/.github/workflows/docs-freshness-check.yml b/.github/workflows/docs-freshness-check.yml new file mode 100644 index 0000000000..09d5cd9beb --- /dev/null +++ b/.github/workflows/docs-freshness-check.yml @@ -0,0 +1,36 @@ +# Checks for outdated documentation content and sends notification about it +name: Docs Freshness Check +on: + schedule: + - cron: '0 0 1 * *' # once a month + +jobs: + freshness-check: + runs-on: ubuntu-latest + env: + GH_TOKEN: ${{ secrets.REFERENCE_PAT }} + LINEAR_API_KEY: ${{ secrets.LINEAR_API_KEY }} + steps: + - name: Cancel Previous Runs + uses: styfle/cancel-workflow-action@0.9.1 + with: + access_token: ${{ github.token }} + + - name: Checkout + uses: actions/checkout@v2.3.5 + with: + fetch-depth: 0 + + - name: Setup Node.js environment + uses: actions/setup-node@v2.4.1 + with: + node-version: "14" + cache: "yarn" + + - name: Install dependencies + uses: ./.github/actions/cache-deps + with: + extension: docs-freshness-check + + - name: Perform Freshness Check + run: yarn check:freshness \ No newline at end of file diff --git a/package.json b/package.json index 35899d1079..5f7c327575 100644 --- a/package.json +++ b/package.json @@ -70,11 +70,13 @@ "generate:js-client": "typedoc --options typedoc.js-client.js", "generate:entities": "typedoc --options typedoc.entities.js", "release:snapshot": "changeset publish --no-git-tags --snapshot --tag snapshot", - "generate:announcement": "node ./scripts/doc-change-release.js" + "generate:announcement": "node ./scripts/doc-change-release.js", + "check:freshness": "node ./scripts/freshness-check.js" }, "dependencies": { "@changesets/changelog-github": "^0.4.5", "@changesets/cli": "^2.23.0", + "@linear/sdk": "^1.22.0", "@octokit/core": "^4.0.5", "global": "^4.4.0", "import-from": "^3.0.0", diff --git a/scripts/freshness-check.js b/scripts/freshness-check.js new file mode 100644 index 0000000000..92ddb60be9 --- /dev/null +++ b/scripts/freshness-check.js @@ -0,0 +1,154 @@ +#!/usr/bin/env node + +const { LinearClient } = require("@linear/sdk"); +const { Octokit } = require("@octokit/core"); +const fs = require('fs'); +const path = require('path'); + +const octokit = new Octokit({ + auth: process.env.GH_TOKEN +}); + +const linearClient = new LinearClient({ + apiKey: process.env.LINEAR_API_KEY +}); + +const repoPath = path.join('docs', 'content'); +let freshnessCheckLabelId = ""; +let documentationTeamId = ""; + +async function scanDirectory (startPath) { + const files = fs.readdirSync(path.join(startPath), { + withFileTypes: true + }); + + for (const file of files) { + const filePath = path.join(startPath, file.name); + if (file.isDirectory()) { + //if it's references directory, skip + if (file.name !== 'references' && file.name !== 'upgrade-guides') { + await scanDirectory(filePath); + } + continue; + } + + //check that the file is a markdown file + if (file.name.indexOf('.md') === -1 && file.name.indexOf('.mdx') === -1 ) { + continue; + } + + //if it is a file, check its commits in GitHub + const commitResponse = await octokit.request('GET /repos/{owner}/{repo}/commits', { + owner: 'medusajs', + repo: 'medusa', + path: filePath, + per_page: 1 + }) + + if (!commitResponse.data.length) { + continue; + } + + const today = new Date(); + const lastEditedDate = new Date(commitResponse.data[0].commit.committer.date); + const monthsSinceEdited = getMonthDifference(lastEditedDate, today); + + if (monthsSinceEdited > 6) { + //file was edited more than 6 months ago. + //check if there's an issue created for this file since the commit date + const existingIssue = await linearClient.issues({ + filter: { + createdAt: { + gte: subtractMonths(monthsSinceEdited - 6, today) + }, + title: { + containsIgnoreCase: `Freshness check for ${filePath}` + }, + labels: { + some: { + id: { + eq: freshnessCheckLabelId + } + } + } + }, + first: 1 + }); + + if (existingIssue.nodes.length) { + //an issue has been created for the past 6 months. Don't create an issue for it. + continue; + } + + console.log(`Creating an issue for ${filePath}...`); + + //there are no issues in the past 6 months. Create an issue + await linearClient.issueCreate({ + teamId: documentationTeamId, + title: `Freshness check for ${filePath}`, + labelIds: [ + freshnessCheckLabelId + ], + description: `File \`${filePath}\` was last edited on ${lastEditedDate.toDateString()}.` + }) + } + } +} + +async function main () { + //fetch documentation team ID from linear + const documentationTeam = await linearClient.teams({ + filter: { + name: { + eqIgnoreCase: 'Documentation' + } + }, + first: 1 + }); + + if (!documentationTeam.nodes.length) { + console.log("Please add Documentation team in Linear first then try again"); + process.exit(1); + } + + documentationTeamId = documentationTeam.nodes[0].id; + + //fetch freshness check label ID from linear + const freshnessCheckLabel = await linearClient.issueLabels({ + filter: { + name: { + eqIgnoreCase: 'type: freshness-check' + }, + team: { + id: { + eq: documentationTeamId + } + } + } + }); + + if (!freshnessCheckLabel.nodes.length) { + console.log("Please add freshness check label in Linear under the documentation team first then try again"); + process.exit(1); + } + + freshnessCheckLabelId = freshnessCheckLabel.nodes[0].id; + + await scanDirectory(repoPath); +} + +function getMonthDifference(startDate, endDate) { + return ( + endDate.getMonth() - + startDate.getMonth() + + 12 * (endDate.getFullYear() - startDate.getFullYear()) + ); +} + +function subtractMonths(numOfMonths, date = new Date()) { + date.setMonth(date.getMonth() - numOfMonths); + + return date; +} + +main() \ No newline at end of file diff --git a/yarn.lock b/yarn.lock index 6a42491f1e..a78475075c 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2891,6 +2891,15 @@ __metadata: languageName: node linkType: hard +"@graphql-typed-document-node/core@npm:^3.1.0": + version: 3.1.1 + resolution: "@graphql-typed-document-node/core@npm:3.1.1" + peerDependencies: + graphql: ^0.8.0 || ^0.9.0 || ^0.10.0 || ^0.11.0 || ^0.12.0 || ^0.13.0 || ^14.0.0 || ^15.0.0 || ^16.0.0 + checksum: c186e5adceb0dfdaa770856d2f17c831a474f5927d79f984326ecb3d8680ba3c1ee2314f7def1d863692cd9cbe4dffc8bb52fc74ee0aa9b31e9491f24ef59f90 + languageName: node + linkType: hard + "@hapi/address@npm:^2.1.2": version: 2.1.4 resolution: "@hapi/address@npm:2.1.4" @@ -4165,6 +4174,17 @@ __metadata: languageName: node linkType: hard +"@linear/sdk@npm:^1.22.0": + version: 1.22.0 + resolution: "@linear/sdk@npm:1.22.0" + dependencies: + "@graphql-typed-document-node/core": ^3.1.0 + graphql: ^15.4.0 + isomorphic-unfetch: ^3.1.0 + checksum: 7e8f24f617631d027fd606334a498b04014d4c33603bcb3e08073d14f86260d116597983567f8bc147a935e4557180158175b52b9a9a8a270a234b946894a82b + languageName: node + linkType: hard + "@lmdb/lmdb-darwin-arm64@npm:2.5.2": version: 2.5.2 resolution: "@lmdb/lmdb-darwin-arm64@npm:2.5.2" @@ -18218,7 +18238,7 @@ __metadata: languageName: node linkType: hard -"graphql@npm:^15.5.1, graphql@npm:^15.7.2": +"graphql@npm:^15.4.0, graphql@npm:^15.5.1, graphql@npm:^15.7.2": version: 15.8.0 resolution: "graphql@npm:15.8.0" checksum: 30cc09b77170a9d1ed68e4c017ec8c5265f69501c96e4f34f8f6613f39a886c96dd9853eac925f212566ed651736334c8fe24ceae6c44e8d7625c95c3009a801 @@ -30050,6 +30070,7 @@ __metadata: "@babel/runtime": ^7.11.2 "@changesets/changelog-github": ^0.4.5 "@changesets/cli": ^2.23.0 + "@linear/sdk": ^1.22.0 "@octokit/core": ^4.0.5 "@redocly/cli": latest "@typescript-eslint/eslint-plugin": ^5.36.2