Compare commits

...

3 Commits

Author SHA1 Message Date
Nacho Orlandoni db5603096d
Merge 65f77605c0 into 900f2210b1 2026-05-15 13:55:55 +02:00
Yashwanth Anantharaju 900f2210b1
fix: expand merge commit SHA regex and add SHA-256 test cases (#2414)
* fix: expand merge commit SHA regex and add SHA-256 test cases

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* test: add checkCommitInfo SHA coverage

Add checkCommitInfo tests for SHA-1 and SHA-256 merge messages and reject invalid 50-character hex merge heads.\n\nCo-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* style: fix Prettier formatting in test and source files

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

---------

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-05-04 13:30:55 -04:00
Nacho Orlandoni 65f77605c0
Reference support
* Add support for reference repository parameter

---------

Co-authored-by: Diego Dompe <ddompe@gmail.com>
2023-10-28 11:30:25 -04:00
12 changed files with 235 additions and 5 deletions

View File

@ -137,6 +137,11 @@ Please refer to the [release page](https://github.com/actions/checkout/releases/
# Default: true
show-progress: ''
# Path to a local bare git [reference repository to minimize network
# usage](https://git-scm.com/docs/git-clone#Documentation/git-clone.txt---reference-if-ableltrepositorygt).
# If the directory doesn't exists this option will ignore it and log a message.
reference: ''
# Whether to download Git-LFS files
# Default: false
lfs: ''

View File

@ -1048,6 +1048,7 @@ async function setup(testName: string): Promise<void> {
lfsInstall: jest.fn(),
log1: jest.fn(),
remoteAdd: jest.fn(),
referenceAdd: jest.fn(),
removeEnvironmentVariable: jest.fn((name: string) => delete git.env[name]),
revParse: jest.fn(),
setEnvironmentVariable: jest.fn((name: string, value: string) => {
@ -1173,7 +1174,8 @@ async function setup(testName: string): Promise<void> {
sshUser: '',
workflowOrganizationId: 123456,
setSafeDirectory: true,
githubServerUrl: githubServerUrl
githubServerUrl: githubServerUrl,
reference: '/some/path'
}
}

View File

@ -479,6 +479,7 @@ async function setup(testName: string): Promise<void> {
lfsInstall: jest.fn(),
log1: jest.fn(),
remoteAdd: jest.fn(),
referenceAdd: jest.fn(),
removeEnvironmentVariable: jest.fn(),
revParse: jest.fn(),
setEnvironmentVariable: jest.fn(),

View File

@ -91,6 +91,7 @@ describe('input-helper tests', () => {
expect(settings.repositoryOwner).toBe('some-owner')
expect(settings.repositoryPath).toBe(gitHubWorkspace)
expect(settings.setSafeDirectory).toBe(true)
expect(settings.reference).toBe(undefined)
})
it('qualifies ref', async () => {
@ -133,6 +134,16 @@ describe('input-helper tests', () => {
expect(settings.commit).toBe('1111111111222222222233333333334444444444')
})
it('sets ref to empty when explicit sha-256', async () => {
inputs.ref =
'1111111111222222222233333333334444444444555555555566666666667777'
const settings: IGitSourceSettings = await inputHelper.getInputs()
expect(settings.ref).toBeFalsy()
expect(settings.commit).toBe(
'1111111111222222222233333333334444444444555555555566666666667777'
)
})
it('sets sha to empty when explicit ref', async () => {
inputs.ref = 'refs/heads/some-other-ref'
const settings: IGitSourceSettings = await inputHelper.getInputs()

View File

@ -1,8 +1,12 @@
import * as assert from 'assert'
import * as core from '@actions/core'
import * as github from '@actions/github'
import * as refHelper from '../lib/ref-helper'
import {IGitCommandManager} from '../lib/git-command-manager'
const commit = '1234567890123456789012345678901234567890'
const sha256Commit =
'1234567890123456789012345678901234567890123456789012345678901234'
let git: IGitCommandManager
describe('ref-helper tests', () => {
@ -37,6 +41,12 @@ describe('ref-helper tests', () => {
expect(checkoutInfo.startPoint).toBeFalsy()
})
it('getCheckoutInfo sha-256 only', async () => {
const checkoutInfo = await refHelper.getCheckoutInfo(git, '', sha256Commit)
expect(checkoutInfo.ref).toBe(sha256Commit)
expect(checkoutInfo.startPoint).toBeFalsy()
})
it('getCheckoutInfo refs/heads/', async () => {
const checkoutInfo = await refHelper.getCheckoutInfo(
git,
@ -227,4 +237,142 @@ describe('ref-helper tests', () => {
'+refs/heads/my/branch:refs/remotes/origin/my/branch'
)
})
describe('checkCommitInfo', () => {
const repositoryOwner = 'some-owner'
const repositoryName = 'some-repo'
const ref = 'refs/pull/123/merge'
const sha1Head = '1111111111222222222233333333334444444444'
const sha1Base = 'aaaaaaaaaabbbbbbbbbbccccccccccdddddddddd'
const sha256Head =
'1111111111222222222233333333334444444444555555555566666666667777'
const sha256Base =
'aaaaaaaaaabbbbbbbbbbccccccccccddddddddddeeeeeeeeeeffffffffff0000'
let debugSpy: jest.SpyInstance
let getOctokitSpy: jest.SpyInstance
let repoGetSpy: jest.Mock
let originalEventName: string
let originalPayload: unknown
let originalRef: string
let originalSha: string
function setPullRequestContext(
expectedHeadSha: string,
expectedBaseSha: string,
mergeCommit: string
): void {
;(github.context as any).eventName = 'pull_request'
github.context.ref = ref
github.context.sha = mergeCommit
;(github.context as any).payload = {
action: 'synchronize',
after: expectedHeadSha,
number: 123,
pull_request: {
base: {
sha: expectedBaseSha
}
},
repository: {
private: false
}
}
}
beforeEach(() => {
originalEventName = github.context.eventName
originalPayload = github.context.payload
originalRef = github.context.ref
originalSha = github.context.sha
jest.spyOn(github.context, 'repo', 'get').mockReturnValue({
owner: repositoryOwner,
repo: repositoryName
})
debugSpy = jest.spyOn(core, 'debug').mockImplementation(jest.fn())
repoGetSpy = jest.fn(async () => ({}))
getOctokitSpy = jest.spyOn(github, 'getOctokit').mockReturnValue({
rest: {
repos: {
get: repoGetSpy
}
}
} as any)
})
afterEach(() => {
;(github.context as any).eventName = originalEventName
;(github.context as any).payload = originalPayload
github.context.ref = originalRef
github.context.sha = originalSha
jest.restoreAllMocks()
})
it('returns early for SHA-1 merge commit', async () => {
setPullRequestContext(sha1Head, sha1Base, commit)
await refHelper.checkCommitInfo(
'token',
`Merge ${sha1Head} into ${sha1Base}`,
repositoryOwner,
repositoryName,
ref,
commit
)
expect(getOctokitSpy).not.toHaveBeenCalled()
expect(repoGetSpy).not.toHaveBeenCalled()
})
it('matches SHA-256 merge commit info', async () => {
const actualHeadSha =
'9999999999888888888877777777776666666666555555555544444444443333'
setPullRequestContext(sha256Head, sha256Base, sha256Commit)
await refHelper.checkCommitInfo(
'token',
`Merge ${actualHeadSha} into ${sha256Base}`,
repositoryOwner,
repositoryName,
ref,
sha256Commit
)
expect(getOctokitSpy).toHaveBeenCalledWith(
'token',
expect.objectContaining({
userAgent: expect.stringContaining(
`expected_head_sha=${sha256Head};actual_head_sha=${actualHeadSha}`
)
})
)
expect(repoGetSpy).toHaveBeenCalledWith({
owner: repositoryOwner,
repo: repositoryName
})
expect(debugSpy).toHaveBeenCalledWith(
`Expected head sha ${sha256Head}; actual head sha ${actualHeadSha}`
)
expect(debugSpy).not.toHaveBeenCalledWith('Unexpected message format')
})
it('does not match 50-char hex as a valid merge', async () => {
const invalidHeadSha =
'99999999998888888888777777777766666666665555555555'
setPullRequestContext(sha1Head, sha1Base, commit)
await refHelper.checkCommitInfo(
'token',
`Merge ${invalidHeadSha} into ${sha1Base}`,
repositoryOwner,
repositoryName,
ref,
commit
)
expect(getOctokitSpy).not.toHaveBeenCalled()
expect(repoGetSpy).not.toHaveBeenCalled()
expect(debugSpy).toHaveBeenCalledWith('Unexpected message format')
})
})
})

View File

@ -80,6 +80,12 @@ inputs:
show-progress:
description: 'Whether to show progress status output when fetching.'
default: true
reference:
required: false
description: >
Path to a local bare git [reference repository to minimize network usage](https://git-scm.com/docs/git-clone#Documentation/git-clone.txt---reference-if-ableltrepositorygt).
If the directory doesn't exists this option will ignore it and log a message.
lfs:
description: 'Whether to download Git-LFS files'
default: false

24
dist/index.js vendored
View File

@ -935,6 +935,13 @@ class GitCommandManager {
yield this.execGit(['remote', 'add', remoteName, remoteUrl]);
});
}
referenceAdd(alternateObjects) {
return __awaiter(this, void 0, void 0, function* () {
const alternatePath = path.join(this.workingDirectory, '.git/objects/info/alternates');
core.info(`Adding a git alternate to reference repo at ${alternateObjects}`);
yield fs.promises.writeFile(alternatePath, `${alternateObjects}\n`);
});
}
removeEnvironmentVariable(name) {
delete this.gitEnv[name];
}
@ -1490,6 +1497,17 @@ function getSource(settings) {
yield git.init();
yield git.remoteAdd('origin', repositoryUrl);
core.endGroup();
if (settings.reference !== undefined) {
const alternateObjects = path.join(settings.reference, '/objects');
if (fsHelper.directoryExistsSync(alternateObjects, false)) {
core.startGroup('Adding a reference repository');
yield git.referenceAdd(alternateObjects);
core.endGroup();
}
else {
core.warning(`Reference repository was specified, but directory ${alternateObjects} does not exists`);
}
}
}
// Disable automatic garbage collection
core.startGroup('Disabling automatic garbage collection');
@ -2021,7 +2039,7 @@ function getInputs() {
}
}
// SHA?
else if (result.ref.match(/^[0-9a-fA-F]{40}$/)) {
else if (result.ref.match(/^(?:[0-9a-fA-F]{40}|[0-9a-fA-F]{64})$/)) {
result.commit = result.ref;
result.ref = '';
}
@ -2095,6 +2113,8 @@ function getInputs() {
// Determine the GitHub URL that the repository is being hosted from
result.githubServerUrl = core.getInput('github-server-url');
core.debug(`GitHub Host URL = ${result.githubServerUrl}`);
result.reference = core.getInput('reference');
core.debug(`Reference repository = ${result.reference}`);
return result;
});
}
@ -2444,7 +2464,7 @@ function checkCommitInfo(token, commitInfo, repositoryOwner, repositoryName, ref
return;
}
// Extract details from message
const match = commitInfo.match(/Merge ([0-9a-f]{40}) into ([0-9a-f]{40})/);
const match = commitInfo.match(/Merge ([0-9a-f]{40}|[0-9a-f]{64}) into ([0-9a-f]{40}|[0-9a-f]{64})/);
if (!match) {
core.debug('Unexpected message format');
return;

View File

@ -49,6 +49,7 @@ export interface IGitCommandManager {
lfsInstall(): Promise<void>
log1(format?: string): Promise<string>
remoteAdd(remoteName: string, remoteUrl: string): Promise<void>
referenceAdd(reference: string): Promise<void>
removeEnvironmentVariable(name: string): void
revParse(ref: string): Promise<string>
setEnvironmentVariable(name: string, value: string): void
@ -401,6 +402,15 @@ class GitCommandManager {
await this.execGit(['remote', 'add', remoteName, remoteUrl])
}
async referenceAdd(alternateObjects: string): Promise<void> {
const alternatePath = path.join(
this.workingDirectory,
'.git/objects/info/alternates'
)
core.info(`Adding a git alternate to reference repo at ${alternateObjects}`)
await fs.promises.writeFile(alternatePath, `${alternateObjects}\n`)
}
removeEnvironmentVariable(name: string): void {
delete this.gitEnv[name]
}

View File

@ -113,6 +113,18 @@ export async function getSource(settings: IGitSourceSettings): Promise<void> {
await git.init()
await git.remoteAdd('origin', repositoryUrl)
core.endGroup()
if (settings.reference !== undefined) {
const alternateObjects = path.join(settings.reference, '/objects')
if (fsHelper.directoryExistsSync(alternateObjects, false)) {
core.startGroup('Adding a reference repository')
await git.referenceAdd(alternateObjects)
core.endGroup()
} else {
core.warning(`Reference repository was specified, but directory ${alternateObjects} does not exists`);
}
}
}
// Disable automatic garbage collection

View File

@ -49,6 +49,11 @@ export interface IGitSourceSettings {
*/
fetchDepth: number
/**
* The local reference repository
*/
reference: string | undefined
/**
* Fetch tags, even if fetchDepth > 0 (default: false)
*/
@ -59,6 +64,11 @@ export interface IGitSourceSettings {
*/
showProgress: boolean
/**
* The local reference repository
*/
reference: string | undefined
/**
* Indicates whether to fetch LFS objects
*/

View File

@ -71,7 +71,7 @@ export async function getInputs(): Promise<IGitSourceSettings> {
}
}
// SHA?
else if (result.ref.match(/^[0-9a-fA-F]{40}$/)) {
else if (result.ref.match(/^(?:[0-9a-fA-F]{40}|[0-9a-fA-F]{64})$/)) {
result.commit = result.ref
result.ref = ''
}
@ -161,5 +161,8 @@ export async function getInputs(): Promise<IGitSourceSettings> {
result.githubServerUrl = core.getInput('github-server-url')
core.debug(`GitHub Host URL = ${result.githubServerUrl}`)
result.reference = core.getInput('reference')
core.debug(`Reference repository = ${result.reference}`)
return result
}

View File

@ -258,7 +258,9 @@ export async function checkCommitInfo(
}
// Extract details from message
const match = commitInfo.match(/Merge ([0-9a-f]{40}) into ([0-9a-f]{40})/)
const match = commitInfo.match(
/Merge ([0-9a-f]{40}|[0-9a-f]{64}) into ([0-9a-f]{40}|[0-9a-f]{64})/
)
if (!match) {
core.debug('Unexpected message format')
return