├── .gitignore ├── package.json ├── LICENSE.md ├── README.md └── bin └── changelog-scanner /.gitignore: -------------------------------------------------------------------------------- 1 | npm-debug.log 2 | *.DS_Store 3 | *.npmignore 4 | node_modules 5 | config.js 6 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@apostrophecms/changelog-scanner", 3 | "version": "2.0.0", 4 | "description": "Scan every changelog in a github organization for changes since a certain date.", 5 | "main": "bin/changelog-scanner", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "author": "Apostrophe Technologies", 10 | "license": "MIT", 11 | "dependencies": { 12 | "boring": "^1.0.0", 13 | "qs": "^6.9.4" 14 | }, 15 | "repository": { 16 | "type": "git", 17 | "url": "https://github.com/apostrophecms/changelog-scanner.git" 18 | }, 19 | "bin": { 20 | "changelog-scanner": "bin/changelog-scanner" 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2025 Apostrophe Technologies, Inc. 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # changelog-scanner 2 | 3 | A simple command-line tool to fetch all commit messages since a given date across every repository in one or more GitHub organizations. 4 | 5 | Perfect for maintainers who need to: 6 | - Generate release notes across multiple repositories 7 | - Audit activity across an organization 8 | - Catch omissions in changelogs before publishing 9 | - Review recent work across your projects 10 | 11 | ## Installation 12 | 13 | ```bash 14 | npm install -g @apostrophecms/changelog-scanner 15 | ``` 16 | 17 | ## Usage 18 | 19 | First, set your GitHub personal access token: 20 | 21 | ```bash 22 | export GITHUB_ACCESS_TOKEN=your_token_here 23 | ``` 24 | 25 | Then scan one or more organizations: 26 | 27 | ```bash 28 | changelog-scanner orgname1 orgname2 orgname3 --since=2025-09-01 29 | ``` 30 | 31 | ### Example Output 32 | 33 | ``` 34 | ⏳ Collecting repos for myorg 35 | ⏳ 25 repos found. Checking for more... 36 | ⏳ Found 25 repos for myorg 37 | ⏳ Checking commits for myorg/my-project 38 | 💁🏼‍♀️ myorg/my-project *** 39 | Fixed bug in authentication handler 40 | Jane Developer 41 | jane@example.com 42 | 2025-09-15T14:23:45Z 43 | 44 | Added support for new API endpoint 45 | ... 46 | ``` 47 | 48 | ## Options 49 | 50 | ### `--since=YYYY-MM-DD` (required) 51 | 52 | Fetch commits since this date. 53 | 54 | ```bash 55 | changelog-scanner myorg --since=2025-09-01 56 | ``` 57 | 58 | ### `--sort=` (optional) 59 | 60 | Sort repositories by: 61 | - `created` - When the repo was created (default, newest first) 62 | - `updated` - Last updated (newest first) 63 | - `pushed` - Last pushed (newest first) 64 | - `full_name` - Alphabetically (A-Z) 65 | 66 | ```bash 67 | changelog-scanner myorg --since=2025-09-01 --sort=full_name 68 | ``` 69 | 70 | ## Requirements 71 | 72 | - Node.js 20 or higher 73 | - A GitHub personal access token with `repo` scope access to the organizations you want to scan 74 | - You can [create a token here](https://github.com/settings/tokens) 75 | 76 | ## About 77 | 78 | Built with ❤️ by the team at [ApostropheCMS](https://github.com/apostrophecms) to streamline our release process. If you find this useful, consider giving [Apostrophe](https://github.com/apostrophecms/apostrophe) a star — it's an open-source CMS that helps teams build powerful Node.js applications. 79 | 80 | ## License 81 | 82 | MIT -------------------------------------------------------------------------------- /bin/changelog-scanner: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | const qs = require('qs'); 4 | const argv = require('boring')(); 5 | const fs = require('fs'); 6 | const results = {}; 7 | let team = []; 8 | let allRepos = []; 9 | 10 | const since = argv.since; 11 | const sort = argv.sort || 'created'; 12 | 13 | const orgs = argv._; 14 | if (orgs.length < 1) { 15 | usage(); 16 | } 17 | if (!since || typeof since !== 'string') { 18 | usage(); 19 | } 20 | const token = process.env.GITHUB_ACCESS_TOKEN; 21 | if (!token) { 22 | usage(); 23 | } 24 | 25 | go(); 26 | 27 | async function go() { 28 | try { 29 | for (const org of orgs) { 30 | console.log('⏳ Collecting repos for', org); 31 | let page = 1; 32 | while (true) { 33 | const params = { 34 | page, 35 | sort 36 | }; 37 | const url = `https://api.github.com/orgs/${org}/repos?${qs.stringify(params)}`; 38 | 39 | const repos = await get(url); 40 | 41 | if (!Array.isArray(repos)) { 42 | console.error('Error retrieving repos:', repos); 43 | return; 44 | } 45 | 46 | allRepos = allRepos.concat(repos.map(repo => `${org}/${repo.name}`)); 47 | if (!repos.length) { 48 | break; 49 | } 50 | console.log(`⏳ ${repos.length} repos found. Checking for more...`) 51 | page++; 52 | } 53 | 54 | console.log(`⏳ Found ${allRepos.length} repos for ${org}`) 55 | } 56 | 57 | for (const repo of allRepos) { 58 | console.log(`⏳ Checking commits for ${repo}`) 59 | let page = 1; 60 | let allCommits = []; 61 | while (true) { 62 | const params = { 63 | page, 64 | since 65 | }; 66 | const url = `https://api.github.com/repos/${repo}/commits?${qs.stringify(params)}`; 67 | const commits = await get(url); 68 | allCommits = allCommits.concat(commits); 69 | if (!commits.length) { 70 | break; 71 | } 72 | page++; 73 | } 74 | if (allCommits.length) { 75 | console.log(`💁🏼‍♀️ ${repo} ***`); 76 | for (const commit of allCommits) { 77 | console.log(commit.commit.message); 78 | console.log(commit.commit.committer.name); 79 | console.log(commit.commit.committer.email); 80 | console.log(commit.commit.committer.date); 81 | console.log(); 82 | } 83 | } 84 | } 85 | } catch (e) { 86 | console.error(e); 87 | process.exit(1); 88 | } 89 | } 90 | 91 | async function get(url) { 92 | const response = await fetch(url, { 93 | headers: { 94 | 'User-Agent': 'changelog-scanner', 95 | 'Authorization': `token ${token}` 96 | } 97 | }); 98 | if (response.status >= 400) { 99 | throw response; 100 | } 101 | const result = await response.json(); 102 | return result; 103 | } 104 | 105 | function usage() { 106 | console.error('Usage: changelog-scanner org1 org2 org3... --since=YYYY-MM-DD'); 107 | console.error('You must also make sure GITHUB_ACCESS_TOKEN is set in your environment.'); 108 | process.exit(1); 109 | } --------------------------------------------------------------------------------