11import * as core from '@actions/core' ;
2- import {
3- assertValidPullRequestConfig ,
4- PullRequestConfig ,
5- } from '../../../../ng-dev/pr/config/index.js' ;
2+ import { context as actionContext } from '@actions/github' ;
63import { loadAndValidatePullRequest } from '../../../../ng-dev/pr/merge/pull-request.js' ;
74import { AutosquashMergeStrategy } from '../../../../ng-dev/pr/merge/strategies/autosquash-merge.js' ;
8- import {
9- assertValidCaretakerConfig ,
10- assertValidGithubConfig ,
11- CaretakerConfig ,
12- getConfig ,
13- GithubConfig ,
14- setConfig ,
15- } from '../../../../ng-dev/utils/config.js' ;
16- import { AuthenticatedGitClient } from '../../../../ng-dev/utils/git/authenticated-git-client.js' ;
5+ import { setupConfigAndGitClient } from './git.js' ;
6+ import { cloneRepoIntoTmpLocation } from './tmp.js' ;
177import {
188 ANGULAR_ROBOT ,
199 getAuthTokenFor ,
2010 revokeActiveInstallationToken ,
2111} from '../../../../github-actions/utils.js' ;
2212import { MergeConflictsFatalError } from '../../../../ng-dev/pr/merge/failures.js' ;
23- import { chdir } from 'process' ;
24- import { spawnSync } from 'child_process' ;
2513import { createPullRequestValidationConfig } from '../../../../ng-dev/pr/common/validation/validation-config.js' ;
2614
2715interface CommmitStatus {
2816 state : 'pending' | 'error' | 'failure' | 'success' ;
2917 description : string ;
18+ targetUrl ?: string ;
3019}
3120
32- /** The directory name for the temporary repo used for validation. */
33- const tempRepo = 'branch-mananger-repo' ;
3421/** The context name used for the commmit status applied. */
3522const statusContextName = 'mergeability' ;
36- /** The branch used as the primary branch for the temporary repo. */
37- const mainBranchName = 'main' ;
38-
39- async function main ( repo : { owner : string ; repo : string } , token : string , pr : number ) {
40- // Because we want to perform this check in the targetted repository, we first need to check out the repo
41- // and then move to the directory it is cloned into.
42- chdir ( '/tmp' ) ;
43- console . log (
44- spawnSync ( 'git' , [
45- 'clone' ,
46- '--depth=1' ,
47- `https://github.com/${ repo . owner } /${ repo . repo } .git` ,
48- `./${ tempRepo } ` ,
49- ] ) . output . toString ( ) ,
50- ) ;
51- chdir ( `/tmp/${ tempRepo } ` ) ;
52-
53- // Manually define the configuration for the pull request and github to prevent having to
54- // checkout the repository before defining the config.
55- // TODO(josephperrott): Load this from the actual repository.
56- setConfig ( < { pullRequest : PullRequestConfig ; github : GithubConfig ; caretaker : CaretakerConfig } > {
57- github : {
58- mainBranchName,
59- owner : repo . owner ,
60- name : repo . repo ,
61- } ,
62- pullRequest : {
63- githubApiMerge : false ,
64- } ,
65- caretaker : { } ,
66- } ) ;
67- /** The configuration used for the ng-dev tooling. */
68- const config = await getConfig ( [
69- assertValidGithubConfig ,
70- assertValidPullRequestConfig ,
71- assertValidCaretakerConfig ,
72- ] ) ;
73-
74- AuthenticatedGitClient . configure ( token ) ;
75- /** The git client used to perform actions. */
76- const git = await AuthenticatedGitClient . get ( ) ;
77-
78- // Needed for testing the merge-ability via `git cherry-pick` in the merge strategy.
79- git . run ( [ 'config' , 'user.email' , '[email protected] ' ] ) ; 80- git . run ( [ 'config' , 'user.name' , 'Angular Robot' ] ) ;
81-
82- /** The pull request after being retrieved and validated. */
83- const pullRequest = await loadAndValidatePullRequest (
84- { git, config} ,
85- pr ,
86- createPullRequestValidationConfig ( {
87- assertSignedCla : true ,
88- assertMergeReady : true ,
89-
90- assertPending : false ,
91- assertChangesAllowForTargetLabel : false ,
92- assertPassingCi : false ,
93- assertCompletedReviews : false ,
94- assertEnforcedStatuses : false ,
95- assertMinimumReviews : false ,
96- } ) ,
97- ) ;
98- core . info ( 'Validated PR information:' ) ;
99- core . info ( JSON . stringify ( pullRequest ) ) ;
100- /** Whether any fatal validation failures were discovered. */
101- let hasFatalFailures = false ;
102- /** The status information to be pushed as a status to the pull request. */
103- let statusInfo : CommmitStatus = await ( async ( ) => {
104- // Log validation failures and check for any fatal failures.
105- if ( pullRequest . validationFailures . length !== 0 ) {
106- core . info ( `Found ${ pullRequest . validationFailures . length } failing validation(s)` ) ;
107- await core . group ( 'Validation failures' , async ( ) => {
108- for ( const failure of pullRequest . validationFailures ) {
109- hasFatalFailures = ! failure . canBeForceIgnored || hasFatalFailures ;
110- core . info ( failure . message ) ;
111- }
112- } ) ;
113- }
114-
115- // With any fatal failure the check is not necessary to do.
116- if ( hasFatalFailures ) {
117- core . info ( 'One of the validations was fatal, setting the status as pending for the pr' ) ;
118- return {
119- description : 'Waiting to check until the pull request is ready' ,
120- state : 'pending' ,
121- } ;
122- }
123-
124- try {
125- git . run ( [ 'checkout' , mainBranchName ] ) ;
126- /**
127- * A merge strategy used to perform the merge check.
128- * Any concrete class implementing MergeStrategy is sufficient as all of our usage is
129- * defined in the abstract base class.
130- * */
131- const strategy = new AutosquashMergeStrategy ( git ) ;
132- await strategy . prepare ( pullRequest ) ;
133- await strategy . check ( pullRequest ) ;
134- core . info ( 'Merge check passes, setting a passing status on the pr' ) ;
135- return {
136- description : `Merges cleanly to ${ pullRequest . targetBranches . join ( ', ' ) } ` ,
137- state : 'success' ,
138- } ;
139- } catch ( e ) {
140- // As the merge strategy class will express the failures during checks, any thrown error is a
141- // failure for our merge check.
142- let description : string ;
143- if ( e instanceof MergeConflictsFatalError ) {
144- core . info ( 'Merge conflict found' ) ;
145- const passingBranches = pullRequest . targetBranches . filter (
146- ( branch ) => ! e . failedBranches . includes ( branch ) ,
147- ) ;
148- description = `Unable to merge into: ${ e . failedBranches . join ( ', ' ) } | Can merge into: ${ passingBranches . join ( ',' ) } ` ;
149- } else {
150- core . info ( 'Unknown error found when checking merge:' ) ;
151- core . error ( e as Error ) ;
152- description =
153- 'Cannot cleanly merge to all target branches, please update changes or PR target' ;
154- }
155- return {
156- description,
157- state : 'failure' ,
158- } ;
159- }
160- } ) ( ) ;
161-
162- await git . github . repos . createCommitStatus ( {
163- ...repo ,
164- state : statusInfo . state ,
165- // Status descriptions are limited to 140 characters.
166- description : statusInfo . description . substring ( 0 , 139 ) ,
167- sha : pullRequest . headSha ,
168- context : statusContextName ,
169- } ) ;
170- }
171-
17223/** The repository name for the pull request. */
17324const repo = core . getInput ( 'repo' , { required : true , trimWhitespace : true } ) ;
17425/** The owner of the repository for the pull request. */
@@ -182,9 +33,148 @@ if (isNaN(pr)) {
18233}
18334/** The token for the angular robot to perform actions in the requested repo. */
18435const token = await getAuthTokenFor ( ANGULAR_ROBOT , { repo, owner} ) ;
36+ const {
37+ /** The ng-dev configuration used for the environment */
38+ config,
39+ /** The Authenticated Git Client instance. */
40+ git,
41+ } = await setupConfigAndGitClient ( token , { owner, repo} ) ;
42+ /** The sha of the latest commit on the pull request, which when provided is what triggered the check. */
43+ const sha = await ( async ( ) => {
44+ let sha = core . getInput ( 'sha' , { required : false , trimWhitespace : true } ) || undefined ;
45+ if ( sha === undefined ) {
46+ sha = ( await git . github . pulls . get ( { owner, repo, pull_number : pr } ) ) . data . head . sha as string ;
47+ }
48+ return sha ;
49+ } ) ( ) ;
50+
51+ /** Set the mergability status on the pull request provided in the environment. */
52+ async function setMergeabilityStatusOnPullRequest ( { state, description, targetUrl} : CommmitStatus ) {
53+ await git . github . repos . createCommitStatus ( {
54+ owner,
55+ repo,
56+ sha,
57+ context : statusContextName ,
58+ state,
59+ // Status descriptions are limited to 140 characters.
60+ description : description . substring ( 0 , 139 ) ,
61+ target_url : targetUrl ,
62+ } ) ;
63+ }
64+
65+ async function main ( ) {
66+ try {
67+ // This is intentionally not awaited because we are just setting the status to pending, and wanting
68+ // to continue working.
69+ let _unawaitedPromise = setMergeabilityStatusOnPullRequest ( {
70+ state : 'pending' ,
71+ description : 'Mergability check in progress' ,
72+ } ) ;
73+
74+ // Create a tmp directory to perform checks in and change working to directory to it.
75+ await cloneRepoIntoTmpLocation ( { owner, repo} ) ;
76+
77+ /** The pull request after being retrieved and validated. */
78+ const pullRequest = await loadAndValidatePullRequest (
79+ { git, config} ,
80+ pr ,
81+ createPullRequestValidationConfig ( {
82+ assertSignedCla : true ,
83+ assertMergeReady : true ,
84+ assertPending : false ,
85+ assertChangesAllowForTargetLabel : false ,
86+ assertPassingCi : false ,
87+ assertCompletedReviews : false ,
88+ assertEnforcedStatuses : false ,
89+ assertMinimumReviews : false ,
90+ } ) ,
91+ ) ;
92+ core . info ( 'Validated PR information:' ) ;
93+ core . info ( JSON . stringify ( pullRequest , null , 2 ) ) ;
94+ /** Whether any fatal validation failures were discovered. */
95+ let hasFatalFailures = false ;
96+ /** The status information to be pushed as a status to the pull request. */
97+ let statusInfo : CommmitStatus = await ( async ( ) => {
98+ // Log validation failures and check for any fatal failures.
99+ if ( pullRequest . validationFailures . length !== 0 ) {
100+ core . info ( `Found ${ pullRequest . validationFailures . length } failing validation(s)` ) ;
101+ await core . group ( 'Validation failures' , async ( ) => {
102+ for ( const failure of pullRequest . validationFailures ) {
103+ hasFatalFailures = ! failure . canBeForceIgnored || hasFatalFailures ;
104+ core . info ( failure . message ) ;
105+ }
106+ } ) ;
107+ }
108+
109+ // With any fatal failure the check is not necessary to do.
110+ if ( hasFatalFailures ) {
111+ core . info ( 'One of the validations was fatal, setting the status as pending for the pr' ) ;
112+ return {
113+ description : 'Waiting to check until the pull request is ready' ,
114+ state : 'pending' ,
115+ } ;
116+ }
117+
118+ try {
119+ git . run ( [ 'checkout' , config . github . mainBranchName ] ) ;
120+ /**
121+ * A merge strategy used to perform the merge check.
122+ * Any concrete class implementing MergeStrategy is sufficient as all of our usage is
123+ * defined in the abstract base class.
124+ * */
125+ const strategy = new AutosquashMergeStrategy ( git ) ;
126+ await strategy . prepare ( pullRequest ) ;
127+ await strategy . check ( pullRequest ) ;
128+ core . info ( 'Merge check passes, setting a passing status on the pr' ) ;
129+ return {
130+ description : `Merges cleanly to ${ pullRequest . targetBranches . join ( ', ' ) } ` ,
131+ state : 'success' ,
132+ } ;
133+ } catch ( e ) {
134+ // As the merge strategy class will express the failures during checks, any thrown error is a
135+ // failure for our merge check.
136+ let description : string ;
137+ if ( e instanceof MergeConflictsFatalError ) {
138+ core . info ( 'Merge conflict found' ) ;
139+ const passingBranches = pullRequest . targetBranches . filter (
140+ ( branch ) => ! e . failedBranches . includes ( branch ) ,
141+ ) ;
142+ description = `Unable to merge into: ${ e . failedBranches . join ( ', ' ) } | Can merge into: ${ passingBranches . join ( ',' ) } ` ;
143+ } else {
144+ core . info ( 'Unknown error found when checking merge:' ) ;
145+ core . error ( e as Error ) ;
146+ description =
147+ 'Cannot cleanly merge to all target branches, please update changes or PR target' ;
148+ }
149+ return {
150+ description,
151+ state : 'failure' ,
152+ } ;
153+ }
154+ } ) ( ) ;
155+
156+ await setMergeabilityStatusOnPullRequest ( statusInfo ) ;
157+ } catch ( e : Error | unknown ) {
158+ let description : string ;
159+ const { runId, repo, serverUrl} = actionContext ;
160+ const targetUrl = `${ serverUrl } /${ repo . owner } /${ repo . repo } /actions/runs/${ runId } ` ;
161+ if ( e instanceof Error ) {
162+ description = e . message ;
163+ } else {
164+ description = 'Internal Error, see link for action log' ;
165+ }
166+ await setMergeabilityStatusOnPullRequest ( {
167+ state : 'error' ,
168+ description,
169+ targetUrl,
170+ } ) ;
171+ // Re-throw the error so that the action run is set as failing.
172+ throw e ;
173+ }
174+ }
185175
186176try {
187- await main ( { repo , owner } , token , pr ) . catch ( ( e : Error ) => {
177+ await main ( ) . catch ( ( e : Error ) => {
188178 core . error ( e ) ;
189179 core . setFailed ( e . message ) ;
190180 } ) ;
0 commit comments