Skip to content

Commit 2ba832a

Browse files
authored
ci(align-deps): update existing PR even if no RCs are found (#3734)
1 parent 33f8e07 commit 2ba832a

File tree

7 files changed

+155
-95
lines changed

7 files changed

+155
-95
lines changed

.changeset/long-birds-sing.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
---
2+
---

.github/workflows/align-deps.yml

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -29,21 +29,29 @@ jobs:
2929
yarn build --dependencies
3030
working-directory: packages/align-deps
3131
- name: Look for new React Native release candidates
32+
id: find-release-candidate
3233
run: |
33-
yarn update-profile --release-candidate > update-profile.output.txt
34+
yarn update-profile --release-candidate 1> update-profile.output.txt
35+
if [[ -s update-profile.output.txt ]]; then
36+
echo 'found=1' >> $GITHUB_OUTPUT
37+
fi
3438
working-directory: packages/align-deps
39+
continue-on-error: true
3540
- name: Include additional capabilities from PR feedback
41+
if: ${{ steps.find-release-candidate.outputs.found != '' }}
3642
run: |
3743
yarn update-preset
3844
yarn update-readme
3945
yarn format
4046
working-directory: packages/align-deps
4147
- name: Update local packages with the new profile
48+
if: ${{ steps.find-release-candidate.outputs.found != '' }}
4249
run: |
4350
yarn rnx-align-deps --write
4451
yarn install --mode update-lockfile
4552
- name: Generate changeset
4653
id: changeset
54+
if: ${{ steps.find-release-candidate.outputs.found != '' }}
4755
run: |
4856
echo '---' > .changeset/rnx-align-deps.md
4957
echo '"@rnx-kit/align-deps": patch' >> .changeset/rnx-align-deps.md
@@ -52,14 +60,16 @@ jobs:
5260
head -n 1 packages/align-deps/update-profile.output.txt >> .changeset/rnx-align-deps.md
5361
echo "message=$(head -n 1 packages/align-deps/update-profile.output.txt | tr '[:upper:]' '[:lower:]')" >> $GITHUB_OUTPUT
5462
- name: Generate token for submitting pull requests
55-
uses: actions/create-github-app-token@v2
5663
id: app-token
64+
if: ${{ steps.find-release-candidate.outputs.found != '' }}
65+
uses: actions/create-github-app-token@v2
5766
with:
5867
app-id: ${{ vars.APP_ID }}
5968
private-key: ${{ secrets.PRIVATE_KEY }}
6069
permission-contents: write
6170
permission-pull-requests: write
6271
- name: Create or update pull request
72+
if: ${{ steps.find-release-candidate.outputs.found != '' }}
6373
uses: peter-evans/create-pull-request@v7
6474
with:
6575
token: ${{ steps.app-token.outputs.token }}

package.json

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -194,6 +194,14 @@
194194
"@rnx-kit/babel-plugin-import-path-remapper"
195195
]
196196
},
197+
"packages/align-deps": {
198+
"entry": [
199+
"scripts/*",
200+
"src/cli.ts",
201+
"src/index.ts",
202+
"test/**/*.{js,ts}"
203+
]
204+
},
197205
"packages/cli": {
198206
"entry": [
199207
"scripts/**/*.ts",
Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
import type { OctokitOptions } from "@octokit/core";
2+
import { Octokit } from "@octokit/core";
3+
import { restEndpointMethods } from "@octokit/plugin-rest-endpoint-methods";
4+
import { info } from "@rnx-kit/console";
5+
import type { Preset } from "../src/types";
6+
7+
type GitHubClient = Octokit & ReturnType<typeof restEndpointMethods>;
8+
9+
const BASE_GITHUB_PARAMS = {
10+
owner: "microsoft",
11+
repo: "rnx-kit",
12+
};
13+
14+
const DEFAULT_BRANCH = "rnx-align-deps/main";
15+
16+
const MAGIC_KEYWORD = "@microsoft-react-native-sdk";
17+
18+
function isArray<T>(a: T[] | null | undefined): a is T[] {
19+
return Array.isArray(a) && a.length > 0;
20+
}
21+
22+
export function createGitHubClient(options: OctokitOptions = {}): GitHubClient {
23+
return new (Octokit.plugin(restEndpointMethods))(options);
24+
}
25+
26+
export function fetchPullRequests(
27+
octokit: GitHubClient,
28+
head = `${BASE_GITHUB_PARAMS.owner}:${DEFAULT_BRANCH}`
29+
) {
30+
return octokit.rest.pulls.list({
31+
...BASE_GITHUB_PARAMS,
32+
state: "open",
33+
head,
34+
base: "main",
35+
per_page: 1,
36+
page: 1,
37+
});
38+
}
39+
40+
export async function fetchPullRequestFeedback(
41+
octokit: GitHubClient,
42+
branch = DEFAULT_BRANCH
43+
) {
44+
const head = `${BASE_GITHUB_PARAMS.owner}:${branch}`;
45+
46+
const pullRequests = await fetchPullRequests(octokit, head);
47+
if (!isArray(pullRequests.data)) {
48+
info(`No pull requests found for '${head}'`);
49+
return;
50+
}
51+
52+
const pr = pullRequests.data[0];
53+
const reviewers = pr.requested_reviewers?.map((user) => user.id);
54+
if (!isArray(reviewers)) {
55+
info(`No reviewers found for pull request #${pr.number}`);
56+
return;
57+
}
58+
59+
const comments = await octokit.rest.issues.listComments({
60+
...BASE_GITHUB_PARAMS,
61+
issue_number: pr.number,
62+
});
63+
64+
for (const comment of comments.data) {
65+
if (
66+
!reviewers.includes(comment.user?.id || -1) ||
67+
!comment.body?.startsWith(MAGIC_KEYWORD)
68+
) {
69+
continue;
70+
}
71+
72+
const m = comment.body.match(/```json([^]*?)```/);
73+
if (!m) {
74+
continue;
75+
}
76+
77+
try {
78+
return JSON.parse(m[1]) as Preset;
79+
} catch (e) {
80+
info(`Failed to parse JSON from comment: ${e.message}`);
81+
continue;
82+
}
83+
}
84+
85+
info(`No feedback found for pull request #${pr.number}`);
86+
}

packages/align-deps/scripts/update-preset.ts

Lines changed: 3 additions & 68 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,13 @@
11
#!/usr/bin/env -S node --experimental-strip-types --no-warnings
22

3-
import { Octokit } from "@octokit/core";
4-
import { restEndpointMethods } from "@octokit/plugin-rest-endpoint-methods";
53
import { info } from "@rnx-kit/console";
64
import * as fs from "node:fs";
75
import * as recast from "recast";
86
import typescript from "recast/parsers/typescript.js";
9-
import type { MetaPackage, Package, Preset } from "../src/types";
7+
import type { MetaPackage, Package } from "../src/types";
8+
import { createGitHubClient, fetchPullRequestFeedback } from "./github.ts";
109
import { IGNORED_CAPABILITIES } from "./update-profile.ts";
1110

12-
const MAGIC_KEYWORD = "@microsoft-react-native-sdk";
13-
14-
function isArray<T>(a: T[] | null | undefined): a is T[] {
15-
return Array.isArray(a) && a.length > 0;
16-
}
17-
1811
function isExportNamedDeclaration(
1912
node: recast.types.namedTypes.Node
2013
): node is recast.types.namedTypes.ExportNamedDeclaration {
@@ -89,66 +82,8 @@ function valueLiteral(b: recast.types.builders, value: unknown) {
8982
}
9083
}
9184

92-
async function fetchPullRequestFeedback(branch = "rnx-align-deps/main") {
93-
const octokit = new (Octokit.plugin(restEndpointMethods))({});
94-
95-
const baseParams = {
96-
owner: "microsoft",
97-
repo: "rnx-kit",
98-
head: `microsoft:${branch}`,
99-
} as const;
100-
101-
const pullRequests = await octokit.rest.pulls.list({
102-
...baseParams,
103-
state: "open",
104-
base: "main",
105-
per_page: 1,
106-
page: 1,
107-
});
108-
109-
if (!isArray(pullRequests.data)) {
110-
info(`No pull requests found for '${baseParams.head}'`);
111-
return;
112-
}
113-
114-
const pr = pullRequests.data[0];
115-
const reviewers = pr.requested_reviewers?.map((user) => user.id);
116-
if (!isArray(reviewers)) {
117-
info(`No reviewers found for pull request #${pr.number}`);
118-
return;
119-
}
120-
121-
const comments = await octokit.rest.issues.listComments({
122-
...baseParams,
123-
issue_number: pr.number,
124-
});
125-
126-
for (const comment of comments.data) {
127-
if (
128-
!reviewers.includes(comment.user?.id || -1) ||
129-
!comment.body?.startsWith(MAGIC_KEYWORD)
130-
) {
131-
continue;
132-
}
133-
134-
const m = comment.body.match(/```json([^]*?)```/);
135-
if (!m) {
136-
continue;
137-
}
138-
139-
try {
140-
return JSON.parse(m[1]) as Preset;
141-
} catch (e) {
142-
info(`Failed to parse JSON from comment: ${e.message}`);
143-
continue;
144-
}
145-
}
146-
147-
info(`No feedback found for pull request #${pr.number}`);
148-
}
149-
15085
async function main() {
151-
const input = await fetchPullRequestFeedback();
86+
const input = await fetchPullRequestFeedback(createGitHubClient());
15287
if (!input) {
15388
return;
15489
}

packages/align-deps/scripts/update-profile.ts

Lines changed: 40 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
#!/usr/bin/env -S node --experimental-strip-types --no-warnings
22

3+
import { error, info } from "@rnx-kit/console";
34
import { untar } from "@rnx-kit/tools-shell";
45
import { markdownTable } from "markdown-table";
56
import * as fs from "node:fs";
@@ -11,6 +12,7 @@ import packageJson from "package-json";
1112
import semverCoerce from "semver/functions/coerce.js";
1213
import semverCompare from "semver/functions/compare.js";
1314
import type { MetaPackage, Package, Preset } from "../src/types.js";
15+
import { createGitHubClient, fetchPullRequests } from "./github.ts";
1416

1517
type Options = {
1618
targetVersion?: string;
@@ -203,6 +205,35 @@ async function fetchPackageInfo(
203205
};
204206
}
205207

208+
async function fetchReleaseCandidateVersion() {
209+
const [latest, next] = await Promise.allSettled([
210+
packageJson("react-native", { version: "latest" }),
211+
packageJson("react-native", { version: "next" }),
212+
]);
213+
214+
assertFulfilled(latest, "latest");
215+
assertFulfilled(next, "next");
216+
217+
const latestVersion = latest.value.version;
218+
const nextVersion = next.value.version;
219+
if (semverCompare(latestVersion, nextVersion) < 0) {
220+
return nextVersion;
221+
}
222+
223+
// If the release candidate was recently promoted to stable, we might still
224+
// have an open pull request
225+
const pullRequest = await fetchPullRequests(createGitHubClient());
226+
if (pullRequest.data.length > 0) {
227+
const pr = pullRequest.data[0];
228+
const m = pr.title.match(/add profile for (\d+\.\d+)$/);
229+
if (m) {
230+
return m[1];
231+
}
232+
}
233+
234+
throw new Error(`No new release candidate available since ${latestVersion}`);
235+
}
236+
206237
function getPackageVersion(
207238
packageName: string,
208239
dependencies?: Record<string, string>
@@ -557,7 +588,7 @@ async function main({
557588
const [dst, presetFile] = getProfilePath(presetName, targetVersion);
558589
fs.writeFile(dst, newProfile, () => {
559590
if (!pullRequest) {
560-
console.log(`Wrote to '${dst}'`);
591+
info(`Wrote to '${dst}'`);
561592
}
562593

563594
const profiles = fs
@@ -589,7 +620,7 @@ async function main({
589620
].join("\n");
590621
fs.writeFileSync(presetFile, preset);
591622
if (!pullRequest) {
592-
console.log(`Updated '${presetFile}'`);
623+
info(`Updated '${presetFile}'`);
593624
}
594625
});
595626
}
@@ -634,6 +665,8 @@ async function main({
634665
const collator = new Intl.Collator();
635666
table.sort((lhs, rhs) => collator.compare(lhs[0], rhs[0]));
636667
if (table.length > 0) {
668+
// The following lines are used for the pull request description; do not use
669+
// colors or prefixes
637670
console.log();
638671
console.log(markdownTable([headers, ...table]));
639672
}
@@ -691,30 +724,17 @@ async function parseArgs(): Promise<Options> {
691724
}
692725

693726
if (options.releaseCandidate) {
694-
const [latest, next] = await Promise.allSettled([
695-
packageJson("react-native", { version: "latest" }),
696-
packageJson("react-native", { version: "next" }),
697-
]);
698-
699-
assertFulfilled(latest, "latest");
700-
assertFulfilled(next, "next");
701-
702-
const latestVersion = latest.value.version;
703-
const nextVersion = next.value.version;
704-
if (semverCompare(latestVersion, nextVersion) >= 0) {
705-
throw new Error(
706-
`No new release candidate available since ${latestVersion}`
707-
);
708-
}
709-
727+
const nextVersion = await fetchReleaseCandidateVersion();
710728
const { major, minor } = coerceVersion(nextVersion);
711729
const targetVersion = `${major}.${minor}`;
712730
options.targetVersion = targetVersion;
713731

714732
if (options.pullRequest) {
733+
// The following line is used for the pull request description; do not use
734+
// colors or prefixes
715735
console.log("Add profile for", targetVersion);
716736
} else {
717-
console.log("Found release candidate:", nextVersion);
737+
info("Found release candidate:", nextVersion);
718738
}
719739
}
720740

@@ -725,7 +745,7 @@ if (fileURLToPath(import.meta.url) === path.resolve(process.argv[1])) {
725745
parseArgs()
726746
.then(main)
727747
.catch((e: Error) => {
728-
console.error(e.message);
748+
error(e.message);
729749
process.exitCode = 1;
730750
});
731751
}

packages/align-deps/src/manifest.ts

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -45,15 +45,14 @@ export function updateDependencies(
4545
return dependencies;
4646
}
4747

48-
const makeVersionRange = (() => {
48+
const makeVersionRange: (versions: Package[]) => string = (() => {
4949
switch (dependencyType) {
5050
case "direct":
51-
return (versions: Package[]) => versions[versions.length - 1].version;
51+
return (versions) => versions[versions.length - 1].version;
5252
case "development":
53-
return (versions: Package[]) => versions[0].version;
53+
return (versions) => versions[0].version;
5454
case "peer":
55-
return (versions: Package[]) =>
56-
versions.map((pkg) => pkg.version).join(" || ");
55+
return (versions) => versions.map((pkg) => pkg.version).join(" || ");
5756
}
5857
})();
5958
const shouldBeAdded = (pkg: Package) =>

0 commit comments

Comments
 (0)