Compare commits

...

4 Commits

Author SHA1 Message Date
brian m. carlson 806a103494
Merge 02ade5d400 into df4cb1c069 2026-06-11 17:30:40 +02:00
Yashwanth Anantharaju df4cb1c069
Update changelog for v6.0.3 (#2446) 2026-06-02 10:31:30 -04:00
Yashwanth Anantharaju 1cce3390c2
Fix checkout init for SHA-256 repositories (#2439)
* Fix checkout init for SHA-256 repositories

* Remove unused object format result field
2026-06-01 11:35:58 -04:00
brian m. carlson 02ade5d400
Don't overwrite annotated tags with commit object
When checking out a repository with full history, a full clone is done
and then the ref is finally updated to point to the commit that caused
the workflow to be run.  Normally, this is a good protection against
someone pushing to the repository twice in short succession, but it
causes problems with annotated tags.

Specifically, because the entry in refs/tags is set to the commit hash,
if an annotated tag was used, the tag is turned merely into a
lightweight one, which breaks `git describe`.  Every other tag in the
repository will continue to remain a valid annotated tag except the one
for which the workflow was invoked, which is not what the user expected.

Let's work around this by not performing a fetch if what we're fetching
is a tag.  Technically, annotated tags can be anywhere in the hierarchy
at any ref, but this should work as a suitable heuristic for now.

Note that the proper solution would be to expose the revision of the
actual object and check against that instead of the commit, but it
doesn't presently appear that that information is exposed.  Also, we
explicitly do not case-fold since Git refs are case sensitive.
2022-02-14 23:18:53 +00:00
7 changed files with 287 additions and 8 deletions

View File

@ -1,5 +1,9 @@
# Changelog
## v6.0.3
* Fix checkout init for SHA-256 repositories by @yaananth in https://github.com/actions/checkout/pull/2439
* fix: expand merge commit SHA regex and add SHA-256 test cases by @yaananth in https://github.com/actions/checkout/pull/2414
## v6.0.2
* Fix tag handling: preserve annotations and explicit fetch-tags by @ericsciple in https://github.com/actions/checkout/pull/2356

View File

@ -378,6 +378,59 @@ describe('Test fetchDepth and fetchTags options', () => {
})
})
describe('repository initialization object format', () => {
beforeEach(async () => {
jest.spyOn(fshelper, 'fileExistsSync').mockImplementation(jest.fn())
jest.spyOn(fshelper, 'directoryExistsSync').mockImplementation(jest.fn())
})
afterEach(() => {
jest.restoreAllMocks()
})
it('initializes SHA-256 repositories with the matching object format', async () => {
mockExec.mockImplementation((path, args, options) => {
if (args.includes('version')) {
options.listeners.stdout(Buffer.from('git version 2.50.1'))
}
return 0
})
jest.spyOn(exec, 'exec').mockImplementation(mockExec)
git = await commandManager.createCommandManager('test', false, false)
await git.init('sha256')
expect(mockExec).toHaveBeenCalledWith(
expect.any(String),
['init', '--object-format=sha256', 'test'],
expect.any(Object)
)
})
it('initializes SHA-1 repositories with existing default arguments', async () => {
mockExec.mockImplementation((path, args, options) => {
if (args.includes('version')) {
options.listeners.stdout(Buffer.from('git version 2.50.1'))
}
return 0
})
jest.spyOn(exec, 'exec').mockImplementation(mockExec)
git = await commandManager.createCommandManager('test', false, false)
await git.init('sha1')
expect(mockExec).toHaveBeenCalledWith(
expect.any(String),
['init', 'test'],
expect.any(Object)
)
})
})
describe('git user-agent with orchestration ID', () => {
beforeEach(async () => {
jest.spyOn(fshelper, 'fileExistsSync').mockImplementation(jest.fn())

View File

@ -0,0 +1,98 @@
import * as core from '@actions/core'
import * as github from '@actions/github'
import * as githubApiHelper from '../lib/github-api-helper'
describe('github-api-helper object format', () => {
let getOctokitSpy: jest.SpyInstance
let debugSpy: jest.SpyInstance
let request: jest.Mock
function mockHashAlgorithmApi(hashAlgorithm: string): void {
request = jest.fn(async () => ({
data: {
hash_algorithm: hashAlgorithm
}
}))
getOctokitSpy = jest.spyOn(github, 'getOctokit').mockReturnValue({
request
} as any)
}
beforeEach(() => {
debugSpy = jest.spyOn(core, 'debug').mockImplementation(jest.fn())
})
afterEach(() => {
jest.restoreAllMocks()
})
it('detects SHA-256 from the repository hash algorithm endpoint', async () => {
mockHashAlgorithmApi('sha256')
await expect(
githubApiHelper.tryGetRepositoryObjectFormat('token', 'owner', 'repo')
).resolves.toEqual({format: 'sha256', succeeded: true})
expect(getOctokitSpy).toHaveBeenCalledWith(
'token',
expect.objectContaining({baseUrl: 'https://api.github.com'})
)
expect(request).toHaveBeenCalledWith(
'GET /repos/{owner}/{repo}/hash-algorithm',
{owner: 'owner', repo: 'repo'}
)
})
it('detects SHA-1 from the repository hash algorithm endpoint', async () => {
mockHashAlgorithmApi('sha1')
await expect(
githubApiHelper.tryGetRepositoryObjectFormat('token', 'owner', 'repo')
).resolves.toEqual({format: 'sha1', succeeded: true})
})
it('detects object format from an existing commit without API calls', async () => {
const commitSha =
'9422233ca7ee1b17f1e905d0e141faf0c401556c41cdc6acd71c6bd685da2e92'
getOctokitSpy = jest.spyOn(github, 'getOctokit')
await expect(
githubApiHelper.tryGetRepositoryObjectFormat(
'token',
'owner',
'repo',
undefined,
commitSha
)
).resolves.toEqual({format: 'sha256', succeeded: true})
expect(getOctokitSpy).not.toHaveBeenCalled()
})
it('returns unsuccessful when the hash algorithm endpoint value is not recognized', async () => {
mockHashAlgorithmApi('unknown')
await expect(
githubApiHelper.tryGetRepositoryObjectFormat('token', 'owner', 'repo')
).resolves.toEqual({format: '', succeeded: false})
expect(debugSpy).toHaveBeenCalledWith(
'Unable to determine repository object format from hash-algorithm endpoint'
)
})
it('returns unsuccessful when the hash algorithm API lookup fails', async () => {
request = jest.fn(async () => {
throw new Error('not found')
})
jest.spyOn(github, 'getOctokit').mockReturnValue({
request
} as any)
await expect(
githubApiHelper.tryGetRepositoryObjectFormat('token', 'owner', 'repo')
).resolves.toEqual({format: '', succeeded: false})
expect(debugSpy).toHaveBeenCalledWith(
'Unable to determine repository object format from hash-algorithm endpoint: not found'
)
})
})

55
dist/index.js vendored
View File

@ -896,9 +896,14 @@ class GitCommandManager {
getWorkingDirectory() {
return this.workingDirectory;
}
init() {
init(objectFormat) {
return __awaiter(this, void 0, void 0, function* () {
yield this.execGit(['init', this.workingDirectory]);
const args = ['init'];
if (objectFormat === 'sha256') {
args.push('--object-format=sha256');
}
args.push(this.workingDirectory);
yield this.execGit(args);
});
}
isDetached() {
@ -1486,8 +1491,17 @@ function getSource(settings) {
stateHelper.setRepositoryPath(settings.repositoryPath);
// Initialize the repository
if (!fsHelper.directoryExistsSync(path.join(settings.repositoryPath, '.git'))) {
core.startGroup('Determining repository object format');
const objectFormatResult = yield githubApiHelper.tryGetRepositoryObjectFormat(settings.authToken, settings.repositoryOwner, settings.repositoryName, settings.githubServerUrl, settings.commit);
const objectFormat = objectFormatResult.succeeded
? objectFormatResult.format
: '';
if (objectFormat === 'sha256') {
core.info('Detected SHA-256 repository object format');
}
core.endGroup();
core.startGroup('Initializing the repository');
yield git.init();
yield git.init(objectFormat);
yield git.remoteAdd('origin', repositoryUrl);
core.endGroup();
}
@ -1810,6 +1824,7 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge
Object.defineProperty(exports, "__esModule", ({ value: true }));
exports.downloadRepository = downloadRepository;
exports.getDefaultBranch = getDefaultBranch;
exports.tryGetRepositoryObjectFormat = tryGetRepositoryObjectFormat;
const assert = __importStar(__nccwpck_require__(9491));
const core = __importStar(__nccwpck_require__(2186));
const fs = __importStar(__nccwpck_require__(7147));
@ -1911,6 +1926,40 @@ function getDefaultBranch(authToken, owner, repo, baseUrl) {
}));
});
}
function tryGetRepositoryObjectFormat(authToken, owner, repo, baseUrl, commit) {
return __awaiter(this, void 0, void 0, function* () {
var _a;
const commitFormat = getObjectFormat(commit);
if (commitFormat) {
return { format: commitFormat, succeeded: true };
}
try {
const octokit = github.getOctokit(authToken, {
baseUrl: (0, url_helper_1.getServerApiUrl)(baseUrl)
});
const response = yield octokit.request('GET /repos/{owner}/{repo}/hash-algorithm', { owner, repo });
const hashAlgorithm = response.data.hash_algorithm;
if (hashAlgorithm === 'sha256' || hashAlgorithm === 'sha1') {
return { format: hashAlgorithm, succeeded: true };
}
core.debug('Unable to determine repository object format from hash-algorithm endpoint');
return { format: '', succeeded: false };
}
catch (err) {
core.debug(`Unable to determine repository object format from hash-algorithm endpoint: ${(_a = err === null || err === void 0 ? void 0 : err.message) !== null && _a !== void 0 ? _a : err}`);
return { format: '', succeeded: false };
}
});
}
function getObjectFormat(sha) {
if (/^[0-9a-fA-F]{64}$/.test(sha || '')) {
return 'sha256';
}
if (/^[0-9a-fA-F]{40}$/.test(sha || '')) {
return 'sha1';
}
return '';
}
function downloadArchive(authToken, owner, repo, ref, commit, baseUrl) {
return __awaiter(this, void 0, void 0, function* () {
const octokit = github.getOctokit(authToken, {

View File

@ -43,7 +43,7 @@ export interface IGitCommandManager {
getDefaultBranch(repositoryUrl: string): Promise<string>
getSubmoduleConfigPaths(recursive: boolean): Promise<string[]>
getWorkingDirectory(): string
init(): Promise<void>
init(objectFormat?: string): Promise<void>
isDetached(): Promise<boolean>
lfsFetch(ref: string): Promise<void>
lfsInstall(): Promise<void>
@ -364,8 +364,14 @@ class GitCommandManager {
return this.workingDirectory
}
async init(): Promise<void> {
await this.execGit(['init', this.workingDirectory])
async init(objectFormat?: string): Promise<void> {
const args = ['init']
if (objectFormat === 'sha256') {
args.push('--object-format=sha256')
}
args.push(this.workingDirectory)
await this.execGit(args)
}
async isDetached(): Promise<boolean> {

View File

@ -109,8 +109,25 @@ export async function getSource(settings: IGitSourceSettings): Promise<void> {
if (
!fsHelper.directoryExistsSync(path.join(settings.repositoryPath, '.git'))
) {
core.startGroup('Determining repository object format')
const objectFormatResult =
await githubApiHelper.tryGetRepositoryObjectFormat(
settings.authToken,
settings.repositoryOwner,
settings.repositoryName,
settings.githubServerUrl,
settings.commit
)
const objectFormat = objectFormatResult.succeeded
? objectFormatResult.format
: ''
if (objectFormat === 'sha256') {
core.info('Detected SHA-256 repository object format')
}
core.endGroup()
core.startGroup('Initializing the repository')
await git.init()
await git.init(objectFormat)
await git.remoteAdd('origin', repositoryUrl)
core.endGroup()
}
@ -178,7 +195,7 @@ export async function getSource(settings: IGitSourceSettings): Promise<void> {
// When all history is fetched, the ref we're interested in may have moved to a different
// commit (push or force push). If so, fetch again with a targeted refspec.
if (!(await refHelper.testRef(git, settings.ref, settings.commit))) {
if (!settings.refs.startsWith("refs/tags") && !(await refHelper.testRef(git, settings.ref, settings.commit))) {
refSpec = refHelper.getRefSpec(settings.ref, settings.commit)
await git.fetch(refSpec, fetchOptions)

View File

@ -11,6 +11,11 @@ import {getServerApiUrl} from './url-helper'
const IS_WINDOWS = process.platform === 'win32'
export interface RepositoryObjectFormatResult {
format: string
succeeded: boolean
}
export async function downloadRepository(
authToken: string,
owner: string,
@ -122,6 +127,53 @@ export async function getDefaultBranch(
})
}
export async function tryGetRepositoryObjectFormat(
authToken: string,
owner: string,
repo: string,
baseUrl?: string,
commit?: string
): Promise<RepositoryObjectFormatResult> {
const commitFormat = getObjectFormat(commit)
if (commitFormat) {
return {format: commitFormat, succeeded: true}
}
try {
const octokit = github.getOctokit(authToken, {
baseUrl: getServerApiUrl(baseUrl)
})
const response = await octokit.request(
'GET /repos/{owner}/{repo}/hash-algorithm',
{owner, repo}
)
const hashAlgorithm = response.data.hash_algorithm
if (hashAlgorithm === 'sha256' || hashAlgorithm === 'sha1') {
return {format: hashAlgorithm, succeeded: true}
}
core.debug(
'Unable to determine repository object format from hash-algorithm endpoint'
)
return {format: '', succeeded: false}
} catch (err) {
core.debug(
`Unable to determine repository object format from hash-algorithm endpoint: ${(err as any)?.message ?? err}`
)
return {format: '', succeeded: false}
}
}
function getObjectFormat(sha?: string): string {
if (/^[0-9a-fA-F]{64}$/.test(sha || '')) {
return 'sha256'
}
if (/^[0-9a-fA-F]{40}$/.test(sha || '')) {
return 'sha1'
}
return ''
}
async function downloadArchive(
authToken: string,
owner: string,