ref-helper.ts
1 import {URL} from 'url' 2 import {IGitCommandManager} from './git-command-manager' 3 import * as core from '@actions/core' 4 import * as github from '@actions/github' 5 6 export const tagsRefSpec = '+refs/tags/*:refs/tags/*' 7 8 export interface ICheckoutInfo { 9 ref: string 10 startPoint: string 11 } 12 13 export async function getCheckoutInfo( 14 git: IGitCommandManager, 15 ref: string, 16 commit: string 17 ): Promise<ICheckoutInfo> { 18 if (!git) { 19 throw new Error('Arg git cannot be empty') 20 } 21 22 if (!ref && !commit) { 23 throw new Error('Args ref and commit cannot both be empty') 24 } 25 26 const result = ({} as unknown) as ICheckoutInfo 27 const upperRef = (ref || '').toUpperCase() 28 29 // SHA only 30 if (!ref) { 31 result.ref = commit 32 } 33 // refs/heads/ 34 else if (upperRef.startsWith('REFS/HEADS/')) { 35 const branch = ref.substring('refs/heads/'.length) 36 result.ref = branch 37 result.startPoint = `refs/remotes/origin/${branch}` 38 } 39 // refs/pull/ 40 else if (upperRef.startsWith('REFS/PULL/')) { 41 const branch = ref.substring('refs/pull/'.length) 42 result.ref = `refs/remotes/pull/${branch}` 43 } 44 // refs/tags/ 45 else if (upperRef.startsWith('REFS/')) { 46 result.ref = ref 47 } 48 // Unqualified ref, check for a matching branch or tag 49 else { 50 if (await git.branchExists(true, `origin/${ref}`)) { 51 result.ref = ref 52 result.startPoint = `refs/remotes/origin/${ref}` 53 } else if (await git.tagExists(`${ref}`)) { 54 result.ref = `refs/tags/${ref}` 55 } else { 56 throw new Error( 57 `A branch or tag with the name '${ref}' could not be found` 58 ) 59 } 60 } 61 62 return result 63 } 64 65 export function getRefSpecForAllHistory(ref: string, commit: string): string[] { 66 const result = ['+refs/heads/*:refs/remotes/origin/*', tagsRefSpec] 67 if (ref && ref.toUpperCase().startsWith('REFS/PULL/')) { 68 const branch = ref.substring('refs/pull/'.length) 69 result.push(`+${commit || ref}:refs/remotes/pull/${branch}`) 70 } 71 72 return result 73 } 74 75 export function getRefSpec(ref: string, commit: string): string[] { 76 if (!ref && !commit) { 77 throw new Error('Args ref and commit cannot both be empty') 78 } 79 80 const upperRef = (ref || '').toUpperCase() 81 82 // SHA 83 if (commit) { 84 // refs/heads 85 if (upperRef.startsWith('REFS/HEADS/')) { 86 const branch = ref.substring('refs/heads/'.length) 87 return [`+${commit}:refs/remotes/origin/${branch}`] 88 } 89 // refs/pull/ 90 else if (upperRef.startsWith('REFS/PULL/')) { 91 const branch = ref.substring('refs/pull/'.length) 92 return [`+${commit}:refs/remotes/pull/${branch}`] 93 } 94 // refs/tags/ 95 else if (upperRef.startsWith('REFS/TAGS/')) { 96 return [`+${commit}:${ref}`] 97 } 98 // Otherwise no destination ref 99 else { 100 return [commit] 101 } 102 } 103 // Unqualified ref, check for a matching branch or tag 104 else if (!upperRef.startsWith('REFS/')) { 105 return [ 106 `+refs/heads/${ref}*:refs/remotes/origin/${ref}*`, 107 `+refs/tags/${ref}*:refs/tags/${ref}*` 108 ] 109 } 110 // refs/heads/ 111 else if (upperRef.startsWith('REFS/HEADS/')) { 112 const branch = ref.substring('refs/heads/'.length) 113 return [`+${ref}:refs/remotes/origin/${branch}`] 114 } 115 // refs/pull/ 116 else if (upperRef.startsWith('REFS/PULL/')) { 117 const branch = ref.substring('refs/pull/'.length) 118 return [`+${ref}:refs/remotes/pull/${branch}`] 119 } 120 // refs/tags/ 121 else { 122 return [`+${ref}:${ref}`] 123 } 124 } 125 126 /** 127 * Tests whether the initial fetch created the ref at the expected commit 128 */ 129 export async function testRef( 130 git: IGitCommandManager, 131 ref: string, 132 commit: string 133 ): Promise<boolean> { 134 if (!git) { 135 throw new Error('Arg git cannot be empty') 136 } 137 138 if (!ref && !commit) { 139 throw new Error('Args ref and commit cannot both be empty') 140 } 141 142 // No SHA? Nothing to test 143 if (!commit) { 144 return true 145 } 146 // SHA only? 147 else if (!ref) { 148 return await git.shaExists(commit) 149 } 150 151 const upperRef = ref.toUpperCase() 152 153 // refs/heads/ 154 if (upperRef.startsWith('REFS/HEADS/')) { 155 const branch = ref.substring('refs/heads/'.length) 156 return ( 157 (await git.branchExists(true, `origin/${branch}`)) && 158 commit === (await git.revParse(`refs/remotes/origin/${branch}`)) 159 ) 160 } 161 // refs/pull/ 162 else if (upperRef.startsWith('REFS/PULL/')) { 163 // Assume matches because fetched using the commit 164 return true 165 } 166 // refs/tags/ 167 else if (upperRef.startsWith('REFS/TAGS/')) { 168 const tagName = ref.substring('refs/tags/'.length) 169 return ( 170 (await git.tagExists(tagName)) && commit === (await git.revParse(ref)) 171 ) 172 } 173 // Unexpected 174 else { 175 core.debug(`Unexpected ref format '${ref}' when testing ref info`) 176 return true 177 } 178 } 179 180 export async function checkCommitInfo( 181 token: string, 182 commitInfo: string, 183 repositoryOwner: string, 184 repositoryName: string, 185 ref: string, 186 commit: string 187 ): Promise<void> { 188 try { 189 // GHES? 190 if (isGhes()) { 191 return 192 } 193 194 // Auth token? 195 if (!token) { 196 return 197 } 198 199 // Public PR synchronize, for workflow repo? 200 if ( 201 fromPayload('repository.private') !== false || 202 github.context.eventName !== 'pull_request' || 203 fromPayload('action') !== 'synchronize' || 204 repositoryOwner !== github.context.repo.owner || 205 repositoryName !== github.context.repo.repo || 206 ref !== github.context.ref || 207 !ref.startsWith('refs/pull/') || 208 commit !== github.context.sha 209 ) { 210 return 211 } 212 213 // Head SHA 214 const expectedHeadSha = fromPayload('after') 215 if (!expectedHeadSha) { 216 core.debug('Unable to determine head sha') 217 return 218 } 219 220 // Base SHA 221 const expectedBaseSha = fromPayload('pull_request.base.sha') 222 if (!expectedBaseSha) { 223 core.debug('Unable to determine base sha') 224 return 225 } 226 227 // Expected message? 228 const expectedMessage = `Merge ${expectedHeadSha} into ${expectedBaseSha}` 229 if (commitInfo.indexOf(expectedMessage) >= 0) { 230 return 231 } 232 233 // Extract details from message 234 const match = commitInfo.match(/Merge ([0-9a-f]{40}) into ([0-9a-f]{40})/) 235 if (!match) { 236 core.debug('Unexpected message format') 237 return 238 } 239 240 // Post telemetry 241 const actualHeadSha = match[1] 242 if (actualHeadSha !== expectedHeadSha) { 243 core.debug( 244 `Expected head sha ${expectedHeadSha}; actual head sha ${actualHeadSha}` 245 ) 246 const octokit = new github.GitHub(token, { 247 userAgent: `actions-checkout-tracepoint/1.0 (code=STALE_MERGE;owner=${repositoryOwner};repo=${repositoryName};pr=${fromPayload( 248 'number' 249 )};run_id=${ 250 process.env['GITHUB_RUN_ID'] 251 };expected_head_sha=${expectedHeadSha};actual_head_sha=${actualHeadSha})` 252 }) 253 await octokit.repos.get({owner: repositoryOwner, repo: repositoryName}) 254 } 255 } catch (err) { 256 core.debug(`Error when validating commit info: ${err.stack}`) 257 } 258 } 259 260 function fromPayload(path: string): any { 261 return select(github.context.payload, path) 262 } 263 264 function select(obj: any, path: string): any { 265 if (!obj) { 266 return undefined 267 } 268 269 const i = path.indexOf('.') 270 if (i < 0) { 271 return obj[path] 272 } 273 274 const key = path.substr(0, i) 275 return select(obj[key], path.substr(i + 1)) 276 } 277 278 function isGhes(): boolean { 279 const ghUrl = new URL( 280 process.env['GITHUB_SERVER_URL'] || 'https://github.com' 281 ) 282 return ghUrl.hostname.toUpperCase() !== 'GITHUB.COM' 283 }