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+ 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+
85+ assertPending : false ,
86+ assertChangesAllowForTargetLabel : false ,
87+ assertPassingCi : false ,
88+ assertCompletedReviews : false ,
89+ assertEnforcedStatuses : false ,
90+ assertMinimumReviews : false ,
91+ } ) ,
92+ ) ;
93+ core . info ( 'Validated PR information:' ) ;
94+ core . info ( JSON . stringify ( pullRequest , null , 2 ) ) ;
95+ /** Whether any fatal validation failures were discovered. */
96+ let hasFatalFailures = false ;
97+ /** The status information to be pushed as a status to the pull request. */
98+ let statusInfo : CommmitStatus = await ( async ( ) => {
99+ // Log validation failures and check for any fatal failures.
100+ if ( pullRequest . validationFailures . length !== 0 ) {
101+ core . info ( `Found ${ pullRequest . validationFailures . length } failing validation(s)` ) ;
102+ await core . group ( 'Validation failures' , async ( ) => {
103+ for ( const failure of pullRequest . validationFailures ) {
104+ hasFatalFailures = ! failure . canBeForceIgnored || hasFatalFailures ;
105+ core . info ( failure . message ) ;
106+ }
107+ } ) ;
108+ }
109+
110+ // With any fatal failure the check is not necessary to do.
111+ if ( hasFatalFailures ) {
112+ core . info ( 'One of the validations was fatal, setting the status as pending for the pr' ) ;
113+ return {
114+ description : 'Waiting to check until the pull request is ready' ,
115+ state : 'pending' ,
116+ } ;
117+ }
118+
119+ try {
120+ git . run ( [ 'checkout' , config . github . mainBranchName ] ) ;
121+ /**
122+ * A merge strategy used to perform the merge check.
123+ * Any concrete class implementing MergeStrategy is sufficient as all of our usage is
124+ * defined in the abstract base class.
125+ * */
126+ const strategy = new AutosquashMergeStrategy ( git ) ;
127+ await strategy . prepare ( pullRequest ) ;
128+ await strategy . check ( pullRequest ) ;
129+ core . info ( 'Merge check passes, setting a passing status on the pr' ) ;
130+ return {
131+ description : `Merges cleanly to ${ pullRequest . targetBranches . join ( ', ' ) } ` ,
132+ state : 'success' ,
133+ } ;
134+ } catch ( e ) {
135+ // As the merge strategy class will express the failures during checks, any thrown error is a
136+ // failure for our merge check.
137+ let description : string ;
138+ if ( e instanceof MergeConflictsFatalError ) {
139+ core . info ( 'Merge conflict found' ) ;
140+ const passingBranches = pullRequest . targetBranches . filter (
141+ ( branch ) => ! e . failedBranches . includes ( branch ) ,
142+ ) ;
143+ description = `Unable to merge into: ${ e . failedBranches . join ( ', ' ) } | Can merge into: ${ passingBranches . join ( ',' ) } ` ;
144+ } else {
145+ core . info ( 'Unknown error found when checking merge:' ) ;
146+ core . error ( e as Error ) ;
147+ description =
148+ 'Cannot cleanly merge to all target branches, please update changes or PR target' ;
149+ }
150+ return {
151+ description,
152+ state : 'failure' ,
153+ } ;
154+ }
155+ } ) ( ) ;
156+
157+ await setMergeabilityStatusOnPullRequest ( statusInfo ) ;
158+ } catch ( e : Error | unknown ) {
159+ let description : string ;
160+ const { runId, repo, serverUrl} = actionContext ;
161+ const targetUrl = `${ serverUrl } /${ repo . owner } /${ repo . repo } /actions/runs/${ runId } ` ;
162+ if ( e instanceof Error ) {
163+ description = e . message ;
164+ } else {
165+ description = 'Internal Error, see link for action log' ;
166+ }
167+ await setMergeabilityStatusOnPullRequest ( {
168+ state : 'error' ,
169+ description,
170+ targetUrl,
171+ } ) ;
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