/ src / ref-helper.ts
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  }