mirror of
https://github.com/actions/checkout.git
synced 2025-04-25 23:50:13 +00:00
Re: https://github.com/actions/checkout/issues/1186 @dscho discovered that the checkout action could stall for a considerable amount of time on Windows runners waiting for PowerShell invocations made from 'windows-release' npm package to complete. Then I studied the dependency chain to figure out where 'windows-release' was imported: '@actions/checkout'@main <- '@actions/github'@2.2.0 <- '@octokit/endpoint'@6.0.1 <- '@octokit/graphql'@4.3.1 <- '@octokit/request'@5.4.2 <- '@octokit/rest'@16.43.1 <- 'universal-user-agent'@4.0.1 <- 'os-name'@3.1.0 <- 'windows-release'@3.1.0 'universal-user-agent' package dropped its dependency on 'os-name' in https://github.com/gr2m/universal-user-agent/releases/tag/v6.0.0 . '@actions/github' v3 removed dependency on '@octokit/rest'@16.43.1 and allows users to move away from the old 'universal-user-agent' v4. (https://github.com/actions/toolkit/pull/453) This pull request attempts to update the version of '@actions/github' used in the checkout action to avoid importing 'windows-release'. Based on testing in my own repositories, I can see an improvement in reduced wait time between entering the checkout action and git actually starts to do useful work.
284 lines
7.2 KiB
TypeScript
284 lines
7.2 KiB
TypeScript
import {IGitCommandManager} from './git-command-manager'
|
|
import * as core from '@actions/core'
|
|
import * as github from '@actions/github'
|
|
import {isGhes} from './url-helper'
|
|
|
|
export const tagsRefSpec = '+refs/tags/*:refs/tags/*'
|
|
|
|
export interface ICheckoutInfo {
|
|
ref: string
|
|
startPoint: string
|
|
}
|
|
|
|
export async function getCheckoutInfo(
|
|
git: IGitCommandManager,
|
|
ref: string,
|
|
commit: string
|
|
): Promise<ICheckoutInfo> {
|
|
if (!git) {
|
|
throw new Error('Arg git cannot be empty')
|
|
}
|
|
|
|
if (!ref && !commit) {
|
|
throw new Error('Args ref and commit cannot both be empty')
|
|
}
|
|
|
|
const result = ({} as unknown) as ICheckoutInfo
|
|
const upperRef = (ref || '').toUpperCase()
|
|
|
|
// SHA only
|
|
if (!ref) {
|
|
result.ref = commit
|
|
}
|
|
// refs/heads/
|
|
else if (upperRef.startsWith('REFS/HEADS/')) {
|
|
const branch = ref.substring('refs/heads/'.length)
|
|
result.ref = branch
|
|
result.startPoint = `refs/remotes/origin/${branch}`
|
|
}
|
|
// refs/pull/
|
|
else if (upperRef.startsWith('REFS/PULL/')) {
|
|
const branch = ref.substring('refs/pull/'.length)
|
|
result.ref = `refs/remotes/pull/${branch}`
|
|
}
|
|
// refs/tags/
|
|
else if (upperRef.startsWith('REFS/')) {
|
|
result.ref = ref
|
|
}
|
|
// Unqualified ref, check for a matching branch or tag
|
|
else {
|
|
if (await git.branchExists(true, `origin/${ref}`)) {
|
|
result.ref = ref
|
|
result.startPoint = `refs/remotes/origin/${ref}`
|
|
} else if (await git.tagExists(`${ref}`)) {
|
|
result.ref = `refs/tags/${ref}`
|
|
} else {
|
|
throw new Error(
|
|
`A branch or tag with the name '${ref}' could not be found`
|
|
)
|
|
}
|
|
}
|
|
|
|
return result
|
|
}
|
|
|
|
export function getRefSpecForAllHistory(ref: string, commit: string): string[] {
|
|
const result = ['+refs/heads/*:refs/remotes/origin/*', tagsRefSpec]
|
|
if (ref && ref.toUpperCase().startsWith('REFS/PULL/')) {
|
|
const branch = ref.substring('refs/pull/'.length)
|
|
result.push(`+${commit || ref}:refs/remotes/pull/${branch}`)
|
|
}
|
|
|
|
return result
|
|
}
|
|
|
|
export function getRefSpec(ref: string, commit: string): string[] {
|
|
if (!ref && !commit) {
|
|
throw new Error('Args ref and commit cannot both be empty')
|
|
}
|
|
|
|
const upperRef = (ref || '').toUpperCase()
|
|
|
|
// SHA
|
|
if (commit) {
|
|
// refs/heads
|
|
if (upperRef.startsWith('REFS/HEADS/')) {
|
|
const branch = ref.substring('refs/heads/'.length)
|
|
return [`+${commit}:refs/remotes/origin/${branch}`]
|
|
}
|
|
// refs/pull/
|
|
else if (upperRef.startsWith('REFS/PULL/')) {
|
|
const branch = ref.substring('refs/pull/'.length)
|
|
return [`+${commit}:refs/remotes/pull/${branch}`]
|
|
}
|
|
// refs/tags/
|
|
else if (upperRef.startsWith('REFS/TAGS/')) {
|
|
return [`+${commit}:${ref}`]
|
|
}
|
|
// Otherwise no destination ref
|
|
else {
|
|
return [commit]
|
|
}
|
|
}
|
|
// Unqualified ref, check for a matching branch or tag
|
|
else if (!upperRef.startsWith('REFS/')) {
|
|
return [
|
|
`+refs/heads/${ref}*:refs/remotes/origin/${ref}*`,
|
|
`+refs/tags/${ref}*:refs/tags/${ref}*`
|
|
]
|
|
}
|
|
// refs/heads/
|
|
else if (upperRef.startsWith('REFS/HEADS/')) {
|
|
const branch = ref.substring('refs/heads/'.length)
|
|
return [`+${ref}:refs/remotes/origin/${branch}`]
|
|
}
|
|
// refs/pull/
|
|
else if (upperRef.startsWith('REFS/PULL/')) {
|
|
const branch = ref.substring('refs/pull/'.length)
|
|
return [`+${ref}:refs/remotes/pull/${branch}`]
|
|
}
|
|
// refs/tags/
|
|
else {
|
|
return [`+${ref}:${ref}`]
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Tests whether the initial fetch created the ref at the expected commit
|
|
*/
|
|
export async function testRef(
|
|
git: IGitCommandManager,
|
|
ref: string,
|
|
commit: string
|
|
): Promise<boolean> {
|
|
if (!git) {
|
|
throw new Error('Arg git cannot be empty')
|
|
}
|
|
|
|
if (!ref && !commit) {
|
|
throw new Error('Args ref and commit cannot both be empty')
|
|
}
|
|
|
|
// No SHA? Nothing to test
|
|
if (!commit) {
|
|
return true
|
|
}
|
|
// SHA only?
|
|
else if (!ref) {
|
|
return await git.shaExists(commit)
|
|
}
|
|
|
|
const upperRef = ref.toUpperCase()
|
|
|
|
// refs/heads/
|
|
if (upperRef.startsWith('REFS/HEADS/')) {
|
|
const branch = ref.substring('refs/heads/'.length)
|
|
return (
|
|
(await git.branchExists(true, `origin/${branch}`)) &&
|
|
commit === (await git.revParse(`refs/remotes/origin/${branch}`))
|
|
)
|
|
}
|
|
// refs/pull/
|
|
else if (upperRef.startsWith('REFS/PULL/')) {
|
|
// Assume matches because fetched using the commit
|
|
return true
|
|
}
|
|
// refs/tags/
|
|
else if (upperRef.startsWith('REFS/TAGS/')) {
|
|
const tagName = ref.substring('refs/tags/'.length)
|
|
return (
|
|
(await git.tagExists(tagName)) && commit === (await git.revParse(ref))
|
|
)
|
|
}
|
|
// Unexpected
|
|
else {
|
|
core.debug(`Unexpected ref format '${ref}' when testing ref info`)
|
|
return true
|
|
}
|
|
}
|
|
|
|
export async function checkCommitInfo(
|
|
token: string,
|
|
commitInfo: string,
|
|
repositoryOwner: string,
|
|
repositoryName: string,
|
|
ref: string,
|
|
commit: string,
|
|
baseUrl?: string
|
|
): Promise<void> {
|
|
try {
|
|
// GHES?
|
|
if (isGhes(baseUrl)) {
|
|
return
|
|
}
|
|
|
|
// Auth token?
|
|
if (!token) {
|
|
return
|
|
}
|
|
|
|
// Public PR synchronize, for workflow repo?
|
|
if (
|
|
fromPayload('repository.private') !== false ||
|
|
github.context.eventName !== 'pull_request' ||
|
|
fromPayload('action') !== 'synchronize' ||
|
|
repositoryOwner !== github.context.repo.owner ||
|
|
repositoryName !== github.context.repo.repo ||
|
|
ref !== github.context.ref ||
|
|
!ref.startsWith('refs/pull/') ||
|
|
commit !== github.context.sha
|
|
) {
|
|
return
|
|
}
|
|
|
|
// Head SHA
|
|
const expectedHeadSha = fromPayload('after')
|
|
if (!expectedHeadSha) {
|
|
core.debug('Unable to determine head sha')
|
|
return
|
|
}
|
|
|
|
// Base SHA
|
|
const expectedBaseSha = fromPayload('pull_request.base.sha')
|
|
if (!expectedBaseSha) {
|
|
core.debug('Unable to determine base sha')
|
|
return
|
|
}
|
|
|
|
// Expected message?
|
|
const expectedMessage = `Merge ${expectedHeadSha} into ${expectedBaseSha}`
|
|
if (commitInfo.indexOf(expectedMessage) >= 0) {
|
|
return
|
|
}
|
|
|
|
// Extract details from message
|
|
const match = commitInfo.match(/Merge ([0-9a-f]{40}) into ([0-9a-f]{40})/)
|
|
if (!match) {
|
|
core.debug('Unexpected message format')
|
|
return
|
|
}
|
|
|
|
// Post telemetry
|
|
const actualHeadSha = match[1]
|
|
if (actualHeadSha !== expectedHeadSha) {
|
|
core.debug(
|
|
`Expected head sha ${expectedHeadSha}; actual head sha ${actualHeadSha}`
|
|
)
|
|
const octokit = github.getOctokit(token, {
|
|
baseUrl: baseUrl,
|
|
userAgent: `actions-checkout-tracepoint/1.0 (code=STALE_MERGE;owner=${repositoryOwner};repo=${repositoryName};pr=${fromPayload(
|
|
'number'
|
|
)};run_id=${
|
|
process.env['GITHUB_RUN_ID']
|
|
};expected_head_sha=${expectedHeadSha};actual_head_sha=${actualHeadSha})`
|
|
})
|
|
await octokit.rest.repos.get({
|
|
owner: repositoryOwner,
|
|
repo: repositoryName
|
|
})
|
|
}
|
|
} catch (err) {
|
|
core.debug(
|
|
`Error when validating commit info: ${(err as any)?.stack ?? err}`
|
|
)
|
|
}
|
|
}
|
|
|
|
function fromPayload(path: string): any {
|
|
return select(github.context.payload, path)
|
|
}
|
|
|
|
function select(obj: any, path: string): any {
|
|
if (!obj) {
|
|
return undefined
|
|
}
|
|
|
|
const i = path.indexOf('.')
|
|
if (i < 0) {
|
|
return obj[path]
|
|
}
|
|
|
|
const key = path.substr(0, i)
|
|
return select(obj[key], path.substr(i + 1))
|
|
}
|