@ffflorian/auto-merge 1.0.2 → 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -27,7 +27,6 @@ Options:
27
27
  -a, --approve approve before merging
28
28
  -c, --config <path> specify a configuration file (default: .automergerc.json)
29
29
  -d, --dry-run don't send any data
30
- -f, --merge-drafts merge draft PRs (default: false)
31
30
  -s, --squash squash when merging (default: false)
32
31
  -V, --version output the version number
33
32
  -h, --help display help for command
@@ -41,20 +40,18 @@ The structure of the configuration file is the following:
41
40
 
42
41
  ```ts
43
42
  {
44
- /** The GitHub auth token */
43
+ /** The GitHub auth token (needs read and write access to code and pull requests) */
45
44
  authToken: string;
46
45
  /** Approve before merging */
47
46
  autoApprove?: boolean;
48
47
  /** Don't send any data */
49
48
  dryRun?: boolean;
50
- /** Merge draft PRs */
51
- mergeDrafts?: boolean;
52
49
  /** All projects to include */
53
50
  projects: {
54
51
  /** All projects hosted on GitHub in the format `user/repo` */
55
52
  gitHub: string[];
56
53
  };
57
- /* Squash when merging */
54
+ /** Squash when merging */
58
55
  squash?: boolean;
59
56
  }
60
57
  ```
@@ -1,47 +1,4 @@
1
- /** @see https://docs.github.com/en/rest/reference/pulls#get-a-pull-request */
2
- interface GitHubPullRequest {
3
- draft: boolean;
4
- head: {
5
- /** The branch name */
6
- ref: string;
7
- /** The commit SHA-1 hash */
8
- sha: string;
9
- };
10
- /** The pull request number */
11
- number: number;
12
- /** The pull request title */
13
- title: string;
14
- }
15
- export interface ActionResult {
16
- error?: string;
17
- pullNumber: number;
18
- status: 'bad' | 'good';
19
- }
20
- export interface AutoMergeConfig {
21
- /** The GitHub auth token */
22
- authToken: string;
23
- /** Approve before merging */
24
- autoApprove?: boolean;
25
- /** Don't send any data */
26
- dryRun?: boolean;
27
- /** Merge draft PRs */
28
- mergeDrafts?: boolean;
29
- /** All projects to include */
30
- projects: {
31
- /** All projects hosted on GitHub in the format `user/repo` */
32
- gitHub: string[];
33
- };
34
- /** Squash when merging */
35
- squash?: boolean;
36
- }
37
- export interface Repository {
38
- pullRequests: GitHubPullRequest[];
39
- repositorySlug: string;
40
- }
41
- export interface RepositoryResult {
42
- actionResults: ActionResult[];
43
- repositorySlug: string;
44
- }
1
+ import type { AutoMergeConfig, ActionResult, Repository, RepositoryResult } from './types/index.js';
45
2
  export declare class AutoMerge {
46
3
  private readonly apiClient;
47
4
  private readonly config;
@@ -51,6 +8,7 @@ export declare class AutoMerge {
51
8
  private checkRepositorySlug;
52
9
  approveByMatch(regex: RegExp, repositories?: Repository[]): Promise<RepositoryResult[]>;
53
10
  private getMatchingRepositories;
11
+ private isPullRequestMergeable;
54
12
  mergeByMatch(regex: RegExp, repositories?: Repository[]): Promise<RepositoryResult[]>;
55
13
  approveByPullNumber(repositorySlug: string, pullNumber: number): Promise<ActionResult>;
56
14
  mergePullRequest(repositorySlug: string, pullNumber: number, squash?: boolean): Promise<ActionResult>;
@@ -62,4 +20,3 @@ export declare class AutoMerge {
62
20
  private putMerge;
63
21
  private getPullRequestsBySlug;
64
22
  }
65
- export {};
package/dist/AutoMerge.js CHANGED
@@ -17,6 +17,7 @@ export class AutoMerge {
17
17
  this.apiClient = axios.create({
18
18
  baseURL: 'https://api.github.com',
19
19
  headers: {
20
+ Accept: 'application/vnd.github+json',
20
21
  Authorization: `token ${this.config.authToken}`,
21
22
  'User-Agent': `${toolName} v${toolVersion}`,
22
23
  },
@@ -45,33 +46,48 @@ export class AutoMerge {
45
46
  async approveByMatch(regex, repositories) {
46
47
  const allRepositories = repositories || (await this.getRepositoriesWithOpenPullRequests());
47
48
  const matchingRepositories = this.getMatchingRepositories(allRepositories, regex);
48
- const resultPromises = matchingRepositories.map(async ({ pullRequests, repositorySlug }) => {
49
- const actionPromises = pullRequests.map(pullRequest => this.approveByPullNumber(repositorySlug, pullRequest.number));
50
- const actionResults = await Promise.all(actionPromises);
51
- return { actionResults, repositorySlug };
52
- });
53
- return Promise.all(resultPromises);
49
+ const processedRepositories = [];
50
+ for (const { pullRequests, repositorySlug } of matchingRepositories) {
51
+ const actionResults = [];
52
+ for (const pullRequest of pullRequests) {
53
+ actionResults.push(await this.approveByPullNumber(repositorySlug, pullRequest.number));
54
+ }
55
+ processedRepositories.push({ actionResults, repositorySlug });
56
+ }
57
+ return processedRepositories;
54
58
  }
55
59
  getMatchingRepositories(repositories, regex) {
56
- return repositories
57
- .map(repository => {
60
+ const matchingRepositories = [];
61
+ for (const repository of repositories) {
58
62
  const matchingPullRequests = repository.pullRequests.filter(pullRequest => new RegExp(regex).test(pullRequest.head.ref));
59
63
  if (matchingPullRequests.length) {
60
- return { pullRequests: matchingPullRequests, repositorySlug: repository.repositorySlug };
64
+ matchingRepositories.push({ pullRequests: matchingPullRequests, repositorySlug: repository.repositorySlug });
61
65
  }
62
- return undefined;
63
- })
64
- .filter(Boolean);
66
+ }
67
+ return matchingRepositories;
68
+ }
69
+ async isPullRequestMergeable(repositorySlug, pullNumber) {
70
+ const resourceUrl = `/repos/${repositorySlug}/pulls/${pullNumber}`;
71
+ const response = await this.apiClient.get(resourceUrl);
72
+ return response.data.mergeable_state === 'clean';
65
73
  }
66
74
  async mergeByMatch(regex, repositories) {
67
75
  const allRepositories = repositories || (await this.getRepositoriesWithOpenPullRequests());
68
76
  const matchingRepositories = this.getMatchingRepositories(allRepositories, regex);
69
- const resultPromises = matchingRepositories.map(async ({ pullRequests, repositorySlug }) => {
70
- const actionPromises = pullRequests.map(pullRequest => this.mergePullRequest(repositorySlug, pullRequest.number, this.config.squash));
71
- const actionResults = await Promise.all(actionPromises);
72
- return { actionResults, repositorySlug };
73
- });
74
- return Promise.all(resultPromises);
77
+ const processedRepositories = [];
78
+ for (const { pullRequests, repositorySlug } of matchingRepositories) {
79
+ const actionResults = [];
80
+ for (const pullRequest of pullRequests) {
81
+ const isMergeable = this.isPullRequestMergeable(repositorySlug, pullRequest.number);
82
+ if (!isMergeable) {
83
+ this.logger.warn(`Pull request #${pullRequest.number} in "${repositorySlug}" is not mergeable. Skipping.`);
84
+ continue;
85
+ }
86
+ actionResults.push(await this.mergePullRequest(repositorySlug, pullRequest.number, this.config.squash));
87
+ }
88
+ processedRepositories.push({ actionResults, repositorySlug });
89
+ }
90
+ return processedRepositories;
75
91
  }
76
92
  async approveByPullNumber(repositorySlug, pullNumber) {
77
93
  const actionResult = { pullNumber, status: 'good' };
@@ -103,17 +119,17 @@ export class AutoMerge {
103
119
  }
104
120
  async getAllRepositories() {
105
121
  const repositorySlugs = this.config.projects.gitHub.filter(repositorySlug => this.checkRepositorySlug(repositorySlug));
106
- const repositoriesPromises = repositorySlugs.map(async (repositorySlug) => {
122
+ const repositories = [];
123
+ for (const repositorySlug of repositorySlugs) {
107
124
  try {
108
125
  const pullRequests = await this.getPullRequestsBySlug(repositorySlug);
109
- return { pullRequests, repositorySlug };
126
+ repositories.push({ pullRequests, repositorySlug });
110
127
  }
111
128
  catch (error) {
112
129
  this.logger.error(`Could not get pull requests for "${repositorySlug}": ${error.message}`);
113
- return undefined;
114
130
  }
115
- });
116
- return (await Promise.all(repositoriesPromises)).filter(Boolean);
131
+ }
132
+ return repositories;
117
133
  }
118
134
  async getRepositoriesWithOpenPullRequests() {
119
135
  const allRepositories = await this.getAllRepositories();
@@ -131,11 +147,8 @@ export class AutoMerge {
131
147
  }
132
148
  async getPullRequestsBySlug(repositorySlug) {
133
149
  const resourceUrl = `/repos/${repositorySlug}/pulls`;
134
- const params = { state: 'open' };
150
+ const params = { per_page: 100, state: 'open' };
135
151
  const response = await this.apiClient.get(resourceUrl, { params });
136
- if (this.config.mergeDrafts) {
137
- response.data = response.data.filter(pr => !pr.draft);
138
- }
139
152
  return response.data;
140
153
  }
141
154
  }
@@ -44,11 +44,15 @@ describe('AutoMerge', () => {
44
44
  },
45
45
  });
46
46
  nock(autoMerge['apiClient'].defaults.baseURL)
47
- .post(/^\/repos\/[^/]+\/[^/]+\/pulls\/\d+\/reviews\/?$/)
47
+ .post(/^\/repos\/.+?\/.+?\/pulls\/\d+\/reviews\/?$/)
48
48
  .reply(HTTP_STATUS.OK, { data: 'not-used' })
49
49
  .persist();
50
50
  nock(autoMerge['apiClient'].defaults.baseURL)
51
- .put(/^\/repos\/[^/]+\/[^/]+\/pulls\/\d+\/merge\/?$/)
51
+ .get(/^\/repos\/.+?\/.+?\/pulls\/\d+\/?$/)
52
+ .reply(HTTP_STATUS.OK, { data: 'not-used' })
53
+ .persist();
54
+ nock(autoMerge['apiClient'].defaults.baseURL)
55
+ .put(/^\/repos\/.+?\/.+?\/pulls\/\d+\/merge\/?$/)
52
56
  .reply(HTTP_STATUS.OK, { data: 'not-used' })
53
57
  .persist();
54
58
  });
package/dist/cli.js CHANGED
@@ -37,7 +37,6 @@ const configFileData = {
37
37
  ...configResult.config,
38
38
  ...(commanderOptions.approve && { autoApprove: commanderOptions.approve }),
39
39
  ...(commanderOptions.dryRun && { dryRun: commanderOptions.dryRun }),
40
- ...(commanderOptions.mergeDrafts && { mergeDrafts: commanderOptions.mergeDrafts }),
41
40
  };
42
41
  async function runAction(autoMerge, repositories, pullRequestSlug) {
43
42
  const regex = new RegExp(pullRequestSlug, 'gi');
@@ -46,12 +45,18 @@ async function runAction(autoMerge, repositories, pullRequestSlug) {
46
45
  approveResults = await autoMerge.approveByMatch(regex, repositories);
47
46
  }
48
47
  const mergeResults = await autoMerge.mergeByMatch(regex, repositories);
49
- const actedRepositories = [...approveResults, ...mergeResults].reduce((count, repository) => {
50
- return count + repository.actionResults.length;
51
- }, 0);
52
- const prPluralized = pluralize('PR', actedRepositories);
48
+ const successCount = [...approveResults, ...mergeResults].filter(repository => {
49
+ return repository.actionResults.some(result => typeof result.error === 'undefined');
50
+ }).length;
51
+ const prPluralized = pluralize('PR', successCount);
53
52
  const doAction = configFileData.autoApprove ? 'Approved and merged' : 'Merged';
54
- logger.info(`${doAction} ${actedRepositories} ${prPluralized} matching "${regex}".`);
53
+ const infoMessage = `${doAction} ${successCount} ${prPluralized} matching "${regex}".`;
54
+ if (successCount === 0) {
55
+ logger.warn(infoMessage);
56
+ }
57
+ else {
58
+ logger.info(infoMessage);
59
+ }
55
60
  }
56
61
  function askQuestion(question) {
57
62
  return new Promise(resolve => {
package/package.json CHANGED
@@ -2,8 +2,8 @@
2
2
  "author": "Florian Imdahl <git@ffflorian.de>",
3
3
  "bin": "dist/cli.js",
4
4
  "dependencies": {
5
- "axios": "1.12.2",
6
- "commander": "14.0.1",
5
+ "axios": "1.13.2",
6
+ "commander": "14.0.2",
7
7
  "cosmiconfig": "9.0.0",
8
8
  "logdown": "3.3.1"
9
9
  },
@@ -11,9 +11,10 @@
11
11
  "devDependencies": {
12
12
  "http-status-codes": "2.3.0",
13
13
  "nock": "14.0.10",
14
- "rimraf": "6.0.1",
15
- "typescript": "5.9.2",
16
- "vitest": "3.2.4"
14
+ "rimraf": "6.1.0",
15
+ "tsx": "4.20.6",
16
+ "typescript": "5.9.3",
17
+ "vitest": "4.0.6"
17
18
  },
18
19
  "engines": {
19
20
  "node": ">= 18.0"
@@ -34,9 +35,10 @@
34
35
  "build": "tsc -p tsconfig.json",
35
36
  "clean": "rimraf dist",
36
37
  "dist": "yarn clean && yarn build",
37
- "start": "node --loader ts-node/esm src/cli.ts",
38
+ "start": "tsx src/cli.ts",
38
39
  "test": "vitest run"
39
40
  },
40
41
  "type": "module",
41
- "version": "1.0.2"
42
+ "version": "1.1.0",
43
+ "gitHead": "710af5a32b89acf0765daa81ced41e5031f67116"
42
44
  }