├── .gitattributes ├── .env.template ├── assets └── preview.gif ├── package.json ├── index.js ├── utils ├── writeFile.js ├── params.js ├── openAiStream.js ├── isMerged.js ├── getGpt4Summary.js └── getMergedIssues.js ├── README.md ├── .gitignore └── yarn.lock /.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files and perform LF normalization 2 | * text=auto 3 | -------------------------------------------------------------------------------- /.env.template: -------------------------------------------------------------------------------- 1 | LINEAR_API_KEY= 2 | OPENAI_API_KEY= 3 | GITHUB_API_TOKEN= 4 | OWNER= 5 | REPO= -------------------------------------------------------------------------------- /assets/preview.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/typefully/auto-release-notes/HEAD/assets/preview.gif -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "auto-release-notes", 3 | "version": "1.0.0", 4 | "main": "index.js", 5 | "license": "MIT", 6 | "scripts": { 7 | "start": "node index.js" 8 | }, 9 | "dependencies": { 10 | "@linear/sdk": "^2.6.0", 11 | "chalk": "4", 12 | "cross-fetch": "^3.1.5", 13 | "dayjs": "^1.11.7", 14 | "dotenv": "^16.0.3", 15 | "eventsource-parser": "^1.0.0" 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | const chalk = require("chalk"); 2 | 3 | const getMergedIssues = require("./utils/getMergedIssues"); 4 | const getGpt4Summary = require("./utils/getGpt4Summary"); 5 | const writeFile = require("./utils/writeFile"); 6 | const { parseCommandLineArgs } = require("./utils/params"); 7 | 8 | const main = async () => { 9 | console.clear(); 10 | try { 11 | const timeRangeType = parseCommandLineArgs(); 12 | 13 | console.log(chalk.blue(`✓ Using time range: ${timeRangeType}`)); 14 | 15 | try { 16 | const issueText = await getMergedIssues(timeRangeType); 17 | 18 | if (!issueText) return; 19 | 20 | const summary = await getGpt4Summary(issueText); 21 | 22 | writeFile(timeRangeType, summary); 23 | } catch (error) { 24 | console.error(chalk.red("Error while fetching issues:"), error.message); 25 | } 26 | } catch (error) { 27 | console.error( 28 | chalk.red("Error while parsing command line arguments:"), 29 | error 30 | ); 31 | } 32 | }; 33 | 34 | main(); 35 | -------------------------------------------------------------------------------- /utils/writeFile.js: -------------------------------------------------------------------------------- 1 | const chalk = require("chalk"); 2 | const fs = require("fs"); 3 | const dayjs = require("dayjs"); 4 | 5 | function writeFile(timeRangeType, summaryText) { 6 | const isStartOfWeek = dayjs().day() === 1; 7 | 8 | const fileName = (() => { 9 | switch (timeRangeType) { 10 | case "last-7-days": 11 | return `release-notes-${dayjs() 12 | .subtract(7, "days") 13 | .format("YYYY-MM-DD")}>${dayjs().format("YYYY-MM-DD")}.md`; 14 | 15 | case "curr-week": 16 | const startOfWeek = dayjs().startOf("week"); 17 | return isStartOfWeek 18 | ? `release-notes-${dayjs().format("YYYY-MM-DD")}.md` 19 | : `release-notes-${startOfWeek.format("YYYY-MM-DD")}>${dayjs().format( 20 | "YYYY-MM-DD" 21 | )}.md`; 22 | 23 | case "prev-week": 24 | const startOfPrevWeek = dayjs().startOf("week").subtract(1, "weeks"); 25 | return `release-notes-${startOfPrevWeek.format( 26 | "YYYY-MM-DD" 27 | )}>${startOfPrevWeek.add(6, "day").format("YYYY-MM-DD")}.md`; 28 | 29 | case "curr-month": 30 | const currMonth = dayjs().format("YYYY-MM"); 31 | return `release-notes-${currMonth}.md`; 32 | 33 | case "prev-month": 34 | const prevMonth = dayjs().subtract(1, "months").format("YYYY-MM"); 35 | return `release-notes-${prevMonth}.md`; 36 | 37 | default: 38 | throw new Error("Invalid time range type"); 39 | } 40 | })(); 41 | 42 | fs.writeFileSync(fileName, summaryText); 43 | console.log(chalk.bold(chalk.green(`\n✅ ${fileName} file created/updated`))); 44 | } 45 | 46 | module.exports = writeFile; 47 | -------------------------------------------------------------------------------- /utils/params.js: -------------------------------------------------------------------------------- 1 | const chalk = require("chalk"); 2 | 3 | /* ----------------------------- Display help ----------------------------- */ 4 | 5 | function displayHelpMessage(params) { 6 | // Display the help message with all supported parameters 7 | console.log(chalk.bold(chalk.yellow("Usage:"))); 8 | console.log(" yarn start \n"); 9 | 10 | console.log(chalk.bold(chalk.yellow("Supported Parameters:"))); 11 | for (const [key, value] of Object.entries(params)) { 12 | console.log(` ${key.padEnd(15)}${value}`); 13 | } 14 | } 15 | 16 | /* ------------------------- Supported parameters ------------------------- */ 17 | 18 | const supportedParams = { 19 | "last-7-days": "Generate release notes for the last 7 days (default).", 20 | "curr-week": "Generate release notes for the current week.", 21 | "prev-week": "Generate release notes for the previous week.", 22 | "curr-month": "Generate release notes for the current month.", 23 | "prev-month": "Generate release notes for the previous month.", 24 | "--help": "Display this help message.", 25 | }; 26 | 27 | /* ----------------------------- Parse arguments ---------------------------- */ 28 | 29 | function parseCommandLineArgs() { 30 | const args = process.argv.slice(2); 31 | 32 | const type = args[0] || "last-7-days"; // Use the default value if no arguments are provided. 33 | 34 | switch (type) { 35 | case "last-7-days": 36 | case "curr-week": 37 | case "prev-week": 38 | case "curr-month": 39 | case "prev-month": 40 | return type; 41 | case "--help": 42 | displayHelpMessage(supportedParams); 43 | process.exit(0); 44 | default: 45 | throw new Error("Invalid parameter. Pass '--help' for more information."); 46 | } 47 | } 48 | 49 | module.exports = { 50 | parseCommandLineArgs, 51 | }; 52 | -------------------------------------------------------------------------------- /utils/openAiStream.js: -------------------------------------------------------------------------------- 1 | require("dotenv").config(); 2 | 3 | const fetch = require("cross-fetch"); 4 | const { createParser } = require("eventsource-parser"); 5 | const { Readable } = require("stream"); 6 | 7 | const OPENAI_API_KEY = process.env.OPENAI_API_KEY || ""; 8 | 9 | // Set up OpenAI streaming 10 | 11 | async function openAIStream(payload) { 12 | const encoder = new TextEncoder(); 13 | let counter = 0; 14 | 15 | const res = await fetch("https://api.openai.com/v1/chat/completions", { 16 | headers: { 17 | "Content-Type": "application/json", 18 | Authorization: `Bearer ${OPENAI_API_KEY}`, 19 | }, 20 | method: "POST", 21 | body: JSON.stringify(payload), 22 | }); 23 | 24 | // Handle the data received from OpenAI 25 | const onParse = (event) => { 26 | if (event.type === "event") { 27 | const data = event.data; 28 | if (data === "[DONE]") { 29 | customReadableStream.push(null); 30 | return; 31 | } 32 | try { 33 | const json = JSON.parse(data); 34 | const text = json.choices[0].delta?.content || ""; 35 | 36 | if (counter < 2 && (text.match(/\n/) || []).length) { 37 | return; 38 | } 39 | 40 | const queue = encoder.encode(text); 41 | 42 | if (!queue) { 43 | customReadableStream.push(null); 44 | } else { 45 | customReadableStream.push(queue); 46 | } 47 | counter++; 48 | } catch (e) { 49 | console.error(e); 50 | customReadableStream.push(null); 51 | } 52 | } 53 | }; 54 | 55 | const parser = createParser(onParse); 56 | 57 | const customReadableStream = new Readable({ 58 | read() {}, 59 | }); 60 | 61 | res.body.setEncoding("utf8"); 62 | 63 | res.body.on("data", (chunk) => { 64 | parser.feed(chunk); 65 | }); 66 | 67 | res.body.on("end", () => { 68 | parser.feed("", { done: true }); 69 | }); 70 | 71 | return customReadableStream; 72 | } 73 | 74 | module.exports = openAIStream; 75 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Release Notes Generator 2 | 3 | At [Typefully](https://typefully.com), we're often busy developing and don't spend much time communicating the work we do. That's why I created this Node.js script to **automatically generate release notes** for our completed Linear issues from the past week. 4 | 5 | It uses the [Linear](https://linear.app/), [OpenAI](https://beta.openai.com/), and [GitHub](https://github.com/) APIs to create a nicely formatted Markdown file with sections for **New Features**, **Improvements**, and **Fixes**. 6 | 7 | ![Release Notes Generator](./assets/preview.gif) 8 | 9 | ## Setup 10 | 11 | Install the required dependencies using yarn: 12 | 13 | ```bash 14 | yarn install 15 | ``` 16 | 17 | Copy the `.env.template` file to a new file named `.env` and fill in the required environment variables: 18 | 19 | ``` 20 | cp .env.template .env 21 | ``` 22 | 23 | Open the .env file in your favorite text editor and replace the placeholder values with your actual Linear, OpenAI, and GitHub API keys. 24 | 25 | - The **Linear** API is used to pull the completed issues for the chosen time range 26 | - The **GitHub** API is used to understand if an issue has been merged 27 | - The **OpenAI** API is used to generate the release notes 28 | 29 | You need [GPT-4 access](https://openai.com/waitlist/gpt-4-api) to make best use of this script, since it doesn't seems to work well with any other model. 30 | 31 | ## Run the Script 32 | 33 | Once you have set up the environment variables, simply run the script using yarn: 34 | 35 | ``` 36 | yarn start [timerange] 37 | ``` 38 | 39 | `timerange` is optional. It can be one of the following values: 40 | 41 | * `last-7` (default) 42 | * `curr-week` 43 | * `prev-week` 44 | * `curr-month` 45 | * `prev-month` 46 | 47 | The script will generate the release notes and save them in a file named `release-notes-[timerange].md` in the root of the project. 48 | 49 | ## Customize the Script 50 | 51 | I recommend customizing the `getMergedIssues` function to suit your needs, for example filtering issues by project or label. -------------------------------------------------------------------------------- /utils/isMerged.js: -------------------------------------------------------------------------------- 1 | const chalk = require("chalk"); 2 | const fetch = require("node-fetch"); 3 | 4 | const GITHUB_API_TOKEN = process.env.GITHUB_API_TOKEN || ""; 5 | 6 | // GitHub repo URL and production branch to filter merged issues 7 | const OWNER = process.env.OWNER || ""; 8 | const REPO = process.env.REPO || ""; 9 | 10 | // Check if a commit or pull request has been merged into the main branch 11 | 12 | async function isMerged(gitHubUrl) { 13 | const prRegExp = /\/pull\/(\d+)/; 14 | const commitRegExp = /\/commit\/(\w+)/; 15 | let sha; 16 | 17 | const prMatch = gitHubUrl.match(prRegExp); 18 | 19 | if (prMatch) { 20 | const prNumber = prMatch[1]; 21 | const prUrl = `https://api.github.com/repos/${OWNER}/${REPO}/pulls/${prNumber}`; 22 | try { 23 | const headers = { Authorization: `token ${GITHUB_API_TOKEN}` }; 24 | const response = await fetch(prUrl, { headers: headers }); 25 | const prJson = await response.json(); 26 | 27 | sha = prJson.head ? prJson.head.sha : null; 28 | } catch (error) { 29 | console.error(`Error: ${error}`); 30 | return false; 31 | } 32 | } else { 33 | const commitMatch = gitHubUrl.match(commitRegExp); 34 | if (commitMatch) { 35 | sha = commitMatch[1]; 36 | } else { 37 | console.error("Invalid gitHubUrl"); 38 | return false; 39 | } 40 | } 41 | 42 | if (!sha) return false; 43 | 44 | const compareUrl = `https://api.github.com/repos/${OWNER}/${REPO}/compare/${sha}...main`; 45 | 46 | const headers = { 47 | Authorization: `token ${GITHUB_API_TOKEN}`, 48 | }; 49 | 50 | try { 51 | const response = await fetch(compareUrl, { headers: headers }); 52 | const compareJson = await response.json(); 53 | 54 | if (compareJson.message?.includes("No commit found")) { 55 | return false; 56 | } 57 | 58 | // Check if the commit (or PR's latest commit) exists in the main branch 59 | const status = compareJson.status; 60 | 61 | return ( 62 | status === "identical" || // The commit is at the tip of the main branch 63 | status === "ahead" || // The commit is included in the main branch and there are new commits in main after it 64 | status === "diverged" // The commit is included in the main branch, but both the main branch and the compared branch have new commits since the compared commit 65 | ); 66 | } catch (error) { 67 | console.error(`Error: ${error}`); 68 | return false; 69 | } 70 | } 71 | 72 | module.exports = isMerged; 73 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | lerna-debug.log* 8 | .pnpm-debug.log* 9 | 10 | # Diagnostic reports (https://nodejs.org/api/report.html) 11 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 12 | 13 | # Runtime data 14 | pids 15 | *.pid 16 | *.seed 17 | *.pid.lock 18 | 19 | # Directory for instrumented libs generated by jscoverage/JSCover 20 | lib-cov 21 | 22 | # Coverage directory used by tools like istanbul 23 | coverage 24 | *.lcov 25 | 26 | # nyc test coverage 27 | .nyc_output 28 | 29 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 30 | .grunt 31 | 32 | # Bower dependency directory (https://bower.io/) 33 | bower_components 34 | 35 | # node-waf configuration 36 | .lock-wscript 37 | 38 | # Compiled binary addons (https://nodejs.org/api/addons.html) 39 | build/Release 40 | 41 | # Dependency directories 42 | node_modules/ 43 | jspm_packages/ 44 | 45 | # Snowpack dependency directory (https://snowpack.dev/) 46 | web_modules/ 47 | 48 | # TypeScript cache 49 | *.tsbuildinfo 50 | 51 | # Optional npm cache directory 52 | .npm 53 | 54 | # Optional eslint cache 55 | .eslintcache 56 | 57 | # Optional stylelint cache 58 | .stylelintcache 59 | 60 | # Microbundle cache 61 | .rpt2_cache/ 62 | .rts2_cache_cjs/ 63 | .rts2_cache_es/ 64 | .rts2_cache_umd/ 65 | 66 | # Optional REPL history 67 | .node_repl_history 68 | 69 | # Output of 'npm pack' 70 | *.tgz 71 | 72 | # Yarn Integrity file 73 | .yarn-integrity 74 | 75 | # dotenv environment variable files 76 | .env 77 | .env.development.local 78 | .env.test.local 79 | .env.production.local 80 | .env.local 81 | 82 | # parcel-bundler cache (https://parceljs.org/) 83 | .cache 84 | .parcel-cache 85 | 86 | # Next.js build output 87 | .next 88 | out 89 | 90 | # Nuxt.js build / generate output 91 | .nuxt 92 | dist 93 | 94 | # Gatsby files 95 | .cache/ 96 | # Comment in the public line in if your project uses Gatsby and not Next.js 97 | # https://nextjs.org/blog/next-9-1#public-directory-support 98 | # public 99 | 100 | # vuepress build output 101 | .vuepress/dist 102 | 103 | # vuepress v2.x temp and cache directory 104 | .temp 105 | .cache 106 | 107 | # Serverless directories 108 | .serverless/ 109 | 110 | # FuseBox cache 111 | .fusebox/ 112 | 113 | # DynamoDB Local files 114 | .dynamodb/ 115 | 116 | # TernJS port file 117 | .tern-port 118 | 119 | # Stores VSCode versions used for testing VSCode extensions 120 | .vscode-test 121 | 122 | # yarn v2 123 | .yarn/cache 124 | .yarn/unplugged 125 | .yarn/build-state.yml 126 | .yarn/install-state.gz 127 | .pnp.* 128 | 129 | release-notes-*.md -------------------------------------------------------------------------------- /utils/getGpt4Summary.js: -------------------------------------------------------------------------------- 1 | require("dotenv").config(); 2 | 3 | const dayjs = require("dayjs"); 4 | const chalk = require("chalk"); 5 | const fs = require("fs"); 6 | 7 | const openAiStream = require("./openAiStream"); 8 | 9 | /* ------------- Generate the release notes markdown with GPT-4 ------------- */ 10 | 11 | async function getGpt4Summary(issueText) { 12 | try { 13 | const messages = [ 14 | { 15 | role: "system", 16 | content: "You are an expert team assistant and writer.", 17 | }, 18 | { 19 | role: "user", 20 | content: `These are all the tasks we've completed in the last 7 days: 21 | 22 | ${issueText} 23 | 24 | Can you please write a great release-notes.md file for me from the above tasks? 25 | 26 | You can use these ## headlines, if you find related completed tasks: 27 | 28 | * New Features 29 | * Improvements 30 | * Fixes 31 | 32 | For "New Features" you can add a separate ### title for each new feature you've found. 33 | 34 | For "Improvements" and "Fixes", you can directly write a * bullet list of completed tasks. 35 | 36 | Make sure each task is nicely written, clear, and very concise (don't just repeat the task title). Never include links or tags. Remember that an issue title could be referencing to the problem, not the solution, but the release notes should be written from the solution point of view. 37 | 38 | Feel free to use **bold text** at the start of the issue if you think it's important, but not for Fixes. 39 | 40 | If you don't include something in the changelog (because you think it's an internal fix or it contains sensitive information), append it at the end of the file after a --- separator and an "Excluded" title. 41 | 42 | Try your best to not write more than 500 words (for example fixes can be more concise than features and improvements). 43 | 44 | If you spot major improvements or a common theme in the tasks, write a brief introduction at the start. No "welcome" or unnecessary text, go straight to the point. 45 | 46 | Please now reply directly with the generated release-notes.md content`, 47 | }, 48 | ]; 49 | 50 | const payload = { 51 | model: "gpt-4", 52 | messages: messages, 53 | temperature: 0.7, 54 | stream: true, 55 | }; 56 | 57 | const summaryStream = await openAiStream(payload); 58 | 59 | console.log(); 60 | console.log(chalk.blue("↩ Generating summary...")); 61 | 62 | let summary = ""; 63 | 64 | for await (const chunk of summaryStream) { 65 | const textChunk = chunk.toString(); 66 | summary += textChunk; 67 | process.stdout.write(textChunk); 68 | } 69 | 70 | return summary; 71 | } catch (error) { 72 | console.error(chalk.red("Error while generating summary:"), error); 73 | } 74 | } 75 | 76 | module.exports = getGpt4Summary; 77 | -------------------------------------------------------------------------------- /utils/getMergedIssues.js: -------------------------------------------------------------------------------- 1 | require("dotenv").config(); 2 | 3 | const { LinearClient, LinearDocument, Issue, Project } = require("@linear/sdk"); 4 | const dayjs = require("dayjs"); 5 | const chalk = require("chalk"); 6 | 7 | const isMerged = require("./isMerged"); 8 | 9 | const LINEAR_API_KEY = process.env.LINEAR_API_KEY || ""; 10 | 11 | const linearClient = new LinearClient({ 12 | apiKey: LINEAR_API_KEY, 13 | }); 14 | 15 | // Fetch and format completed issues from the past week 16 | 17 | async function getMergedIssues(type) { 18 | const now = dayjs(); 19 | let updatedAtAfter; 20 | 21 | switch (type) { 22 | case "curr-week": 23 | updatedAtAfter = now.startOf("week").toISOString(); 24 | break; 25 | case "prev-week": 26 | updatedAtAfter = now.subtract(1, "week").startOf("week").toISOString(); 27 | break; 28 | case "curr-month": 29 | updatedAtAfter = now.startOf("month").toISOString(); 30 | break; 31 | case "prev-month": 32 | updatedAtAfter = now.subtract(1, "month").startOf("month").toISOString(); 33 | break; 34 | case "last-7-days": 35 | default: 36 | updatedAtAfter = now.subtract(7, "day").toISOString(); 37 | break; 38 | } 39 | 40 | const issues = await linearClient.issues({ 41 | orderBy: LinearDocument.PaginationOrderBy.UpdatedAt, 42 | filter: { 43 | completedAt: { gte: updatedAtAfter }, 44 | }, 45 | }); 46 | 47 | // Filter issues that have been merged in production by 48 | // checking if they have a GitHub attachment with the 49 | // production branch name in the subtitle 50 | 51 | console.log(chalk.blue(`Finding merged issues...`)); 52 | 53 | const mergedIssues = ( 54 | await Promise.all( 55 | issues.nodes.map(async (issue) => { 56 | const attachments = await issue.attachments(); 57 | 58 | // Check if there's a GitHub attachment 59 | const githubAttachment = attachments.nodes.find((attachment) => 60 | attachment.url.includes("https://github.com/") 61 | ); 62 | 63 | if (!githubAttachment) { 64 | return null; 65 | } 66 | 67 | // Check if the commit has been merged 68 | const merged = await isMerged(githubAttachment.url); 69 | 70 | if (merged) { 71 | console.log(chalk.bold(chalk.green(`✓ ${issue.title}`))); 72 | } 73 | 74 | return merged ? issue : null; 75 | }) 76 | ) 77 | ).filter((issue) => issue); 78 | 79 | console.log(); 80 | if (mergedIssues.length > 0) { 81 | console.log(chalk.bold(`Found ${mergedIssues.length} completed issues`)); 82 | } else { 83 | console.log(chalk.bold(`No completed issues found`)); 84 | return null; 85 | } 86 | 87 | // Format issue details and generate task summaries 88 | let issueText = await Promise.all( 89 | mergedIssues.map(async (issue) => { 90 | // Format each issue labels 91 | const labels = await issue.labels(); 92 | const labelsString = 93 | labels.nodes.length > 0 94 | ? "Tags: " + labels.nodes.map((label) => label.name).join(", ") + "\n" 95 | : ""; 96 | 97 | // Format issue description 98 | const description = issue.description || ""; 99 | const cleanedDescription = description 100 | .split(". ") 101 | .slice(0, 2) 102 | .join(". ") 103 | .split("![")[0] 104 | .split(`\`\`\``)[0] 105 | .slice(0, 120) 106 | .trim(); 107 | 108 | const filteredDescription = 109 | cleanedDescription.startsWith("!") || cleanedDescription.startsWith("[") 110 | ? "" 111 | : cleanedDescription; 112 | 113 | return `# ${issue.title} 114 | ${labelsString}${filteredDescription}`.trim(); 115 | }) 116 | ); 117 | 118 | issueText = issueText.join("\n\n---\n\n"); 119 | 120 | return issueText; 121 | } 122 | 123 | module.exports = getMergedIssues; 124 | -------------------------------------------------------------------------------- /yarn.lock: -------------------------------------------------------------------------------- 1 | # THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. 2 | # yarn lockfile v1 3 | 4 | 5 | "@graphql-typed-document-node/core@^3.1.0": 6 | version "3.2.0" 7 | resolved "https://registry.yarnpkg.com/@graphql-typed-document-node/core/-/core-3.2.0.tgz#5f3d96ec6b2354ad6d8a28bf216a1d97b5426861" 8 | integrity sha512-mB9oAsNCm9aM3/SOv4YtBMqZbYj10R7dkq8byBqxGY/ncFwhf2oQzMV+LCRlWoDSEBJ3COiR1yeDvMtsoOsuFQ== 9 | 10 | "@linear/sdk@^2.6.0": 11 | version "2.6.0" 12 | resolved "https://registry.yarnpkg.com/@linear/sdk/-/sdk-2.6.0.tgz#b3d6fadd7c78b9c34f2f029b2fc426728aef4b69" 13 | integrity sha512-L7Bsd5Ooa+RusZ6Y1yuzsq3NnrSOqHbhZHvdY+anNTOEtFPKonFCNeUdWXpBlyM21rzx7MWq+TfjtMo9dbxxPQ== 14 | dependencies: 15 | "@graphql-typed-document-node/core" "^3.1.0" 16 | graphql "^15.4.0" 17 | isomorphic-unfetch "^3.1.0" 18 | 19 | ansi-styles@^4.1.0: 20 | version "4.3.0" 21 | resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-4.3.0.tgz#edd803628ae71c04c85ae7a0906edad34b648937" 22 | integrity sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg== 23 | dependencies: 24 | color-convert "^2.0.1" 25 | 26 | chalk@4: 27 | version "4.1.2" 28 | resolved "https://registry.yarnpkg.com/chalk/-/chalk-4.1.2.tgz#aac4e2b7734a740867aeb16bf02aad556a1e7a01" 29 | integrity sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA== 30 | dependencies: 31 | ansi-styles "^4.1.0" 32 | supports-color "^7.1.0" 33 | 34 | color-convert@^2.0.1: 35 | version "2.0.1" 36 | resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-2.0.1.tgz#72d3a68d598c9bdb3af2ad1e84f21d896abd4de3" 37 | integrity sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ== 38 | dependencies: 39 | color-name "~1.1.4" 40 | 41 | color-name@~1.1.4: 42 | version "1.1.4" 43 | resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.4.tgz#c2a09a87acbde69543de6f63fa3995c826c536a2" 44 | integrity sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA== 45 | 46 | cross-fetch@^3.1.5: 47 | version "3.1.5" 48 | resolved "https://registry.yarnpkg.com/cross-fetch/-/cross-fetch-3.1.5.tgz#e1389f44d9e7ba767907f7af8454787952ab534f" 49 | integrity sha512-lvb1SBsI0Z7GDwmuid+mU3kWVBwTVUbe7S0H52yaaAdQOXq2YktTCZdlAcNKFzE6QtRz0snpw9bNiPeOIkkQvw== 50 | dependencies: 51 | node-fetch "2.6.7" 52 | 53 | dayjs@^1.11.7: 54 | version "1.11.7" 55 | resolved "https://registry.yarnpkg.com/dayjs/-/dayjs-1.11.7.tgz#4b296922642f70999544d1144a2c25730fce63e2" 56 | integrity sha512-+Yw9U6YO5TQohxLcIkrXBeY73WP3ejHWVvx8XCk3gxvQDCTEmS48ZrSZCKciI7Bhl/uCMyxYtE9UqRILmFphkQ== 57 | 58 | dotenv@^16.0.3: 59 | version "16.0.3" 60 | resolved "https://registry.yarnpkg.com/dotenv/-/dotenv-16.0.3.tgz#115aec42bac5053db3c456db30cc243a5a836a07" 61 | integrity sha512-7GO6HghkA5fYG9TYnNxi14/7K9f5occMlp3zXAuSxn7CKCxt9xbNWG7yF8hTCSUchlfWSe3uLmlPfigevRItzQ== 62 | 63 | eventsource-parser@^1.0.0: 64 | version "1.0.0" 65 | resolved "https://registry.yarnpkg.com/eventsource-parser/-/eventsource-parser-1.0.0.tgz#6332e37fd5512e3c8d9df05773b2bf9e152ccc04" 66 | integrity sha512-9jgfSCa3dmEme2ES3mPByGXfgZ87VbP97tng1G2nWwWx6bV2nYxm2AWCrbQjXToSe+yYlqaZNtxffR9IeQr95g== 67 | 68 | graphql@^15.4.0: 69 | version "15.8.0" 70 | resolved "https://registry.yarnpkg.com/graphql/-/graphql-15.8.0.tgz#33410e96b012fa3bdb1091cc99a94769db212b38" 71 | integrity sha512-5gghUc24tP9HRznNpV2+FIoq3xKkj5dTQqf4v0CpdPbFVwFkWoxOM+o+2OC9ZSvjEMTjfmG9QT+gcvggTwW1zw== 72 | 73 | has-flag@^4.0.0: 74 | version "4.0.0" 75 | resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-4.0.0.tgz#944771fd9c81c81265c4d6941860da06bb59479b" 76 | integrity sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ== 77 | 78 | isomorphic-unfetch@^3.1.0: 79 | version "3.1.0" 80 | resolved "https://registry.yarnpkg.com/isomorphic-unfetch/-/isomorphic-unfetch-3.1.0.tgz#87341d5f4f7b63843d468438128cb087b7c3e98f" 81 | integrity sha512-geDJjpoZ8N0kWexiwkX8F9NkTsXhetLPVbZFQ+JTW239QNOwvB0gniuR1Wc6f0AMTn7/mFGyXvHTifrCp/GH8Q== 82 | dependencies: 83 | node-fetch "^2.6.1" 84 | unfetch "^4.2.0" 85 | 86 | node-fetch@2.6.7: 87 | version "2.6.7" 88 | resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.6.7.tgz#24de9fba827e3b4ae44dc8b20256a379160052ad" 89 | integrity sha512-ZjMPFEfVx5j+y2yF35Kzx5sF7kDzxuDj6ziH4FFbOp87zKDZNx8yExJIb05OGF4Nlt9IHFIMBkRl41VdvcNdbQ== 90 | dependencies: 91 | whatwg-url "^5.0.0" 92 | 93 | node-fetch@^2.6.1: 94 | version "2.6.9" 95 | resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.6.9.tgz#7c7f744b5cc6eb5fd404e0c7a9fec630a55657e6" 96 | integrity sha512-DJm/CJkZkRjKKj4Zi4BsKVZh3ValV5IR5s7LVZnW+6YMh0W1BfNA8XSs6DLMGYlId5F3KnA70uu2qepcR08Qqg== 97 | dependencies: 98 | whatwg-url "^5.0.0" 99 | 100 | supports-color@^7.1.0: 101 | version "7.2.0" 102 | resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-7.2.0.tgz#1b7dcdcb32b8138801b3e478ba6a51caa89648da" 103 | integrity sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw== 104 | dependencies: 105 | has-flag "^4.0.0" 106 | 107 | tr46@~0.0.3: 108 | version "0.0.3" 109 | resolved "https://registry.yarnpkg.com/tr46/-/tr46-0.0.3.tgz#8184fd347dac9cdc185992f3a6622e14b9d9ab6a" 110 | integrity sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw== 111 | 112 | unfetch@^4.2.0: 113 | version "4.2.0" 114 | resolved "https://registry.yarnpkg.com/unfetch/-/unfetch-4.2.0.tgz#7e21b0ef7d363d8d9af0fb929a5555f6ef97a3be" 115 | integrity sha512-F9p7yYCn6cIW9El1zi0HI6vqpeIvBsr3dSuRO6Xuppb1u5rXpCPmMvLSyECLhybr9isec8Ohl0hPekMVrEinDA== 116 | 117 | webidl-conversions@^3.0.0: 118 | version "3.0.1" 119 | resolved "https://registry.yarnpkg.com/webidl-conversions/-/webidl-conversions-3.0.1.tgz#24534275e2a7bc6be7bc86611cc16ae0a5654871" 120 | integrity sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ== 121 | 122 | whatwg-url@^5.0.0: 123 | version "5.0.0" 124 | resolved "https://registry.yarnpkg.com/whatwg-url/-/whatwg-url-5.0.0.tgz#966454e8765462e37644d3626f6742ce8b70965d" 125 | integrity sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw== 126 | dependencies: 127 | tr46 "~0.0.3" 128 | webidl-conversions "^3.0.0" 129 | --------------------------------------------------------------------------------