Compare commits

...

5 Commits

Author SHA1 Message Date
Nicolas Schweitzer f37146e7c2
Merge ab5d862ce8 into 064fe7f331 2026-01-09 15:54:32 +01:00
Copilot 064fe7f331
Add orchestration_id to git user-agent when ACTIONS_ORCHESTRATION_ID is set (#2355)
* Initial plan

* Add orchestration ID support to git user-agent

Co-authored-by: TingluoHuang <1750815+TingluoHuang@users.noreply.github.com>

* Apply suggestion from @Copilot

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

* Improve tests to verify user-agent content and handle empty sanitized IDs

Co-authored-by: TingluoHuang <1750815+TingluoHuang@users.noreply.github.com>

* Simplify orchestration ID validation to accept any non-empty sanitized value

Co-authored-by: TingluoHuang <1750815+TingluoHuang@users.noreply.github.com>

* Remove test for orchestration ID with only invalid characters

Co-authored-by: TingluoHuang <1750815+TingluoHuang@users.noreply.github.com>

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: TingluoHuang <1750815+TingluoHuang@users.noreply.github.com>
Co-authored-by: Tingluo Huang <tingluohuang@github.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-01-08 15:07:38 -05:00
Nicolas Schweitzer ab5d862ce8
fix tests and update index.js 2025-01-22 11:55:00 +01:00
Nicolas Schweitzer 5fba9eb899
codereview: define a git-user slug instead of a true/false config 2025-01-21 18:50:25 +01:00
Nicolas Schweitzer f3b199b7ed
feat(git config): Set default user.name and user.email in git config 2024-12-19 16:24:48 +01:00
14 changed files with 228 additions and 22 deletions

View File

@ -75,6 +75,12 @@ Please refer to the [release page](https://github.com/actions/checkout/releases/
# Default: ${{ github.token }} # Default: ${{ github.token }}
token: '' token: ''
# Github slug used to configure local user.name and user.email for git. This is
# required to push a commit from a Github Action Workflow. Set to '' to disable
# this configuration.
# Default: github-action[bot]
git-user: ''
# SSH key used to fetch the repository. The SSH key is configured with the local # SSH key used to fetch the repository. The SSH key is configured with the local
# git config, which enables your scripts to run authenticated git commands. The # git config, which enables your scripts to run authenticated git commands. The
# post-job step removes the SSH key. # post-job step removes the SSH key.
@ -324,8 +330,6 @@ jobs:
- run: | - run: |
date > generated.txt date > generated.txt
# Note: the following account information will not work on GHES # Note: the following account information will not work on GHES
git config user.name "github-actions[bot]"
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
git add . git add .
git commit -m "generated" git commit -m "generated"
git push git push
@ -348,8 +352,6 @@ jobs:
- run: | - run: |
date > generated.txt date > generated.txt
# Note: the following account information will not work on GHES # Note: the following account information will not work on GHES
git config user.name "github-actions[bot]"
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
git add . git add .
git commit -m "generated" git commit -m "generated"
git push git push

View File

@ -1,12 +1,12 @@
import * as core from '@actions/core' import * as core from '@actions/core'
import * as fs from 'fs' import * as fs from 'fs'
import * as gitAuthHelper from '../lib/git-auth-helper' import * as gitAuthHelper from '../src/git-auth-helper'
import * as io from '@actions/io' import * as io from '@actions/io'
import * as os from 'os' import * as os from 'os'
import * as path from 'path' import * as path from 'path'
import * as stateHelper from '../lib/state-helper' import * as stateHelper from '../src/state-helper'
import {IGitCommandManager} from '../lib/git-command-manager' import {IGitCommandManager} from '../src/git-command-manager'
import {IGitSourceSettings} from '../lib/git-source-settings' import {IGitSourceSettings} from '../src/git-source-settings'
const isWindows = process.platform === 'win32' const isWindows = process.platform === 'win32'
const testWorkspace = path.join(__dirname, '_temp', 'git-auth-helper') const testWorkspace = path.join(__dirname, '_temp', 'git-auth-helper')
@ -1173,7 +1173,8 @@ async function setup(testName: string): Promise<void> {
sshUser: '', sshUser: '',
workflowOrganizationId: 123456, workflowOrganizationId: 123456,
setSafeDirectory: true, setSafeDirectory: true,
githubServerUrl: githubServerUrl githubServerUrl: githubServerUrl,
gitUser: 'github-action[bot]'
} }
} }

View File

@ -1,6 +1,6 @@
import * as exec from '@actions/exec' import * as exec from '@actions/exec'
import * as fshelper from '../lib/fs-helper' import * as fshelper from '../src/fs-helper'
import * as commandManager from '../lib/git-command-manager' import * as commandManager from '../src/git-command-manager'
let git: commandManager.IGitCommandManager let git: commandManager.IGitCommandManager
let mockExec = jest.fn() let mockExec = jest.fn()
@ -376,3 +376,120 @@ describe('Test fetchDepth and fetchTags options', () => {
) )
}) })
}) })
describe('git user-agent with orchestration ID', () => {
beforeEach(async () => {
jest.spyOn(fshelper, 'fileExistsSync').mockImplementation(jest.fn())
jest.spyOn(fshelper, 'directoryExistsSync').mockImplementation(jest.fn())
})
afterEach(() => {
jest.restoreAllMocks()
// Clean up environment variable to prevent test pollution
delete process.env['ACTIONS_ORCHESTRATION_ID']
})
it('should include orchestration ID in user-agent when ACTIONS_ORCHESTRATION_ID is set', async () => {
const orchId = 'test-orch-id-12345'
process.env['ACTIONS_ORCHESTRATION_ID'] = orchId
let capturedEnv: any = null
mockExec.mockImplementation((path, args, options) => {
if (args.includes('version')) {
options.listeners.stdout(Buffer.from('2.18'))
}
// Capture env on any command
capturedEnv = options.env
return 0
})
jest.spyOn(exec, 'exec').mockImplementation(mockExec)
const workingDirectory = 'test'
const lfs = false
const doSparseCheckout = false
git = await commandManager.createCommandManager(
workingDirectory,
lfs,
doSparseCheckout
)
// Call a git command to trigger env capture after user-agent is set
await git.init()
// Verify the user agent includes the orchestration ID
expect(git).toBeDefined()
expect(capturedEnv).toBeDefined()
expect(capturedEnv['GIT_HTTP_USER_AGENT']).toBe(
`git/2.18 (github-actions-checkout) actions_orchestration_id/${orchId}`
)
})
it('should sanitize invalid characters in orchestration ID', async () => {
const orchId = 'test (with) special/chars'
process.env['ACTIONS_ORCHESTRATION_ID'] = orchId
let capturedEnv: any = null
mockExec.mockImplementation((path, args, options) => {
if (args.includes('version')) {
options.listeners.stdout(Buffer.from('2.18'))
}
// Capture env on any command
capturedEnv = options.env
return 0
})
jest.spyOn(exec, 'exec').mockImplementation(mockExec)
const workingDirectory = 'test'
const lfs = false
const doSparseCheckout = false
git = await commandManager.createCommandManager(
workingDirectory,
lfs,
doSparseCheckout
)
// Call a git command to trigger env capture after user-agent is set
await git.init()
// Verify the user agent has sanitized orchestration ID (spaces, parentheses, slash replaced)
expect(git).toBeDefined()
expect(capturedEnv).toBeDefined()
expect(capturedEnv['GIT_HTTP_USER_AGENT']).toBe(
'git/2.18 (github-actions-checkout) actions_orchestration_id/test__with__special_chars'
)
})
it('should not modify user-agent when ACTIONS_ORCHESTRATION_ID is not set', async () => {
delete process.env['ACTIONS_ORCHESTRATION_ID']
let capturedEnv: any = null
mockExec.mockImplementation((path, args, options) => {
if (args.includes('version')) {
options.listeners.stdout(Buffer.from('2.18'))
}
// Capture env on any command
capturedEnv = options.env
return 0
})
jest.spyOn(exec, 'exec').mockImplementation(mockExec)
const workingDirectory = 'test'
const lfs = false
const doSparseCheckout = false
git = await commandManager.createCommandManager(
workingDirectory,
lfs,
doSparseCheckout
)
// Call a git command to trigger env capture after user-agent is set
await git.init()
// Verify the user agent does NOT contain orchestration ID
expect(git).toBeDefined()
expect(capturedEnv).toBeDefined()
expect(capturedEnv['GIT_HTTP_USER_AGENT']).toBe(
'git/2.18 (github-actions-checkout)'
)
})
})

View File

@ -1,9 +1,9 @@
import * as core from '@actions/core' import * as core from '@actions/core'
import * as fs from 'fs' import * as fs from 'fs'
import * as gitDirectoryHelper from '../lib/git-directory-helper' import * as gitDirectoryHelper from '../src/git-directory-helper'
import * as io from '@actions/io' import * as io from '@actions/io'
import * as path from 'path' import * as path from 'path'
import {IGitCommandManager} from '../lib/git-command-manager' import {IGitCommandManager} from '../src/git-command-manager'
const testWorkspace = path.join(__dirname, '_temp', 'git-directory-helper') const testWorkspace = path.join(__dirname, '_temp', 'git-directory-helper')
let repositoryPath: string let repositoryPath: string

View File

@ -1,10 +1,10 @@
import * as core from '@actions/core' import * as core from '@actions/core'
import * as fsHelper from '../lib/fs-helper' import * as fsHelper from '../src/fs-helper'
import * as github from '@actions/github' import * as github from '@actions/github'
import * as inputHelper from '../lib/input-helper' import * as inputHelper from '../src/input-helper'
import * as path from 'path' import * as path from 'path'
import * as workflowContextHelper from '../lib/workflow-context-helper' import * as workflowContextHelper from '../src/workflow-context-helper'
import {IGitSourceSettings} from '../lib/git-source-settings' import {IGitSourceSettings} from '../src/git-source-settings'
const originalGitHubWorkspace = process.env['GITHUB_WORKSPACE'] const originalGitHubWorkspace = process.env['GITHUB_WORKSPACE']
const gitHubWorkspace = path.resolve('/checkout-tests/workspace') const gitHubWorkspace = path.resolve('/checkout-tests/workspace')

View File

@ -1,6 +1,6 @@
import * as assert from 'assert' import * as assert from 'assert'
import * as refHelper from '../lib/ref-helper' import * as refHelper from '../src/ref-helper'
import {IGitCommandManager} from '../lib/git-command-manager' import {IGitCommandManager} from '../src/git-command-manager'
const commit = '1234567890123456789012345678901234567890' const commit = '1234567890123456789012345678901234567890'
let git: IGitCommandManager let git: IGitCommandManager

View File

@ -1,5 +1,5 @@
import * as core from '@actions/core' import * as core from '@actions/core'
import {RetryHelper} from '../lib/retry-helper' import {RetryHelper} from '../src/retry-helper'
let info: string[] let info: string[]
let retryHelper: any let retryHelper: any

View File

@ -22,6 +22,12 @@ inputs:
[Learn more about creating and using encrypted secrets](https://help.github.com/en/actions/automating-your-workflow-with-github-actions/creating-and-using-encrypted-secrets) [Learn more about creating and using encrypted secrets](https://help.github.com/en/actions/automating-your-workflow-with-github-actions/creating-and-using-encrypted-secrets)
default: ${{ github.token }} default: ${{ github.token }}
git-user:
description: >
Github slug used to configure local user.name and user.email for git.
This is required to push a commit from a Github Action Workflow.
Set to '' to disable this configuration.
default: "github-action[bot]"
ssh-key: ssh-key:
description: > description: >
SSH key used to fetch the repository. The SSH key is configured with the local SSH key used to fetch the repository. The SSH key is configured with the local

33
dist/index.js vendored
View File

@ -1206,7 +1206,17 @@ class GitCommandManager {
} }
} }
// Set the user agent // Set the user agent
const gitHttpUserAgent = `git/${this.gitVersion} (github-actions-checkout)`; let gitHttpUserAgent = `git/${this.gitVersion} (github-actions-checkout)`;
// Append orchestration ID if set
const orchId = process.env['ACTIONS_ORCHESTRATION_ID'];
if (orchId) {
// Sanitize the orchestration ID to ensure it contains only valid characters
// Valid characters: 0-9, a-z, _, -, .
const sanitizedId = orchId.replace(/[^a-z0-9_.-]/gi, '_');
if (sanitizedId) {
gitHttpUserAgent = `${gitHttpUserAgent} actions_orchestration_id/${sanitizedId}`;
}
}
core.debug(`Set git useragent to: ${gitHttpUserAgent}`); core.debug(`Set git useragent to: ${gitHttpUserAgent}`);
this.gitEnv['GIT_HTTP_USER_AGENT'] = gitHttpUserAgent; this.gitEnv['GIT_HTTP_USER_AGENT'] = gitHttpUserAgent;
}); });
@ -1599,6 +1609,15 @@ function getSource(settings) {
core.setOutput('commit', commitSHA.trim()); core.setOutput('commit', commitSHA.trim());
// Check for incorrect pull request merge commit // Check for incorrect pull request merge commit
yield refHelper.checkCommitInfo(settings.authToken, commitInfo, settings.repositoryOwner, settings.repositoryName, settings.ref, settings.commit, settings.githubServerUrl); yield refHelper.checkCommitInfo(settings.authToken, commitInfo, settings.repositoryOwner, settings.repositoryName, settings.ref, settings.commit, settings.githubServerUrl);
if (settings.gitUser) {
if (!(yield git.configExists('user.name', true))) {
yield git.config('user.name', settings.gitUser, true);
}
if (!(yield git.configExists('user.email', true))) {
const userId = yield githubApiHelper.getUserId(settings.gitUser, settings.authToken, settings.githubServerUrl);
yield git.config('user.email', `${userId}+${settings.gitUser}@users.noreply.github.com`, true);
}
}
} }
finally { finally {
// Remove auth // Remove auth
@ -1788,6 +1807,7 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge
Object.defineProperty(exports, "__esModule", ({ value: true })); Object.defineProperty(exports, "__esModule", ({ value: true }));
exports.downloadRepository = downloadRepository; exports.downloadRepository = downloadRepository;
exports.getDefaultBranch = getDefaultBranch; exports.getDefaultBranch = getDefaultBranch;
exports.getUserId = getUserId;
const assert = __importStar(__nccwpck_require__(9491)); const assert = __importStar(__nccwpck_require__(9491));
const core = __importStar(__nccwpck_require__(2186)); const core = __importStar(__nccwpck_require__(2186));
const fs = __importStar(__nccwpck_require__(7147)); const fs = __importStar(__nccwpck_require__(7147));
@ -1905,6 +1925,15 @@ function downloadArchive(authToken, owner, repo, ref, commit, baseUrl) {
return Buffer.from(response.data); // response.data is ArrayBuffer return Buffer.from(response.data); // response.data is ArrayBuffer
}); });
} }
function getUserId(username, authToken, baseUrl) {
return __awaiter(this, void 0, void 0, function* () {
const octokit = github.getOctokit(authToken, {
baseUrl: (0, url_helper_1.getServerApiUrl)(baseUrl)
});
const user = yield octokit.rest.users.getByUsername({ username, });
return user.data.id;
});
}
/***/ }), /***/ }),
@ -2055,6 +2084,8 @@ function getInputs() {
core.debug(`recursive submodules = ${result.nestedSubmodules}`); core.debug(`recursive submodules = ${result.nestedSubmodules}`);
// Auth token // Auth token
result.authToken = core.getInput('token', { required: true }); result.authToken = core.getInput('token', { required: true });
// Git user
result.gitUser = core.getInput('git-user') || 'github-action[bot]';
// SSH // SSH
result.sshKey = core.getInput('ssh-key'); result.sshKey = core.getInput('ssh-key');
result.sshKnownHosts = core.getInput('ssh-known-hosts'); result.sshKnownHosts = core.getInput('ssh-known-hosts');

View File

@ -730,7 +730,19 @@ class GitCommandManager {
} }
} }
// Set the user agent // Set the user agent
const gitHttpUserAgent = `git/${this.gitVersion} (github-actions-checkout)` let gitHttpUserAgent = `git/${this.gitVersion} (github-actions-checkout)`
// Append orchestration ID if set
const orchId = process.env['ACTIONS_ORCHESTRATION_ID']
if (orchId) {
// Sanitize the orchestration ID to ensure it contains only valid characters
// Valid characters: 0-9, a-z, _, -, .
const sanitizedId = orchId.replace(/[^a-z0-9_.-]/gi, '_')
if (sanitizedId) {
gitHttpUserAgent = `${gitHttpUserAgent} actions_orchestration_id/${sanitizedId}`
}
}
core.debug(`Set git useragent to: ${gitHttpUserAgent}`) core.debug(`Set git useragent to: ${gitHttpUserAgent}`)
this.gitEnv['GIT_HTTP_USER_AGENT'] = gitHttpUserAgent this.gitEnv['GIT_HTTP_USER_AGENT'] = gitHttpUserAgent
} }

View File

@ -274,6 +274,23 @@ export async function getSource(settings: IGitSourceSettings): Promise<void> {
settings.commit, settings.commit,
settings.githubServerUrl settings.githubServerUrl
) )
if (settings.gitUser) {
if (!(await git.configExists('user.name', true))) {
await git.config('user.name', settings.gitUser, true)
}
if (!(await git.configExists('user.email', true))) {
const userId = await githubApiHelper.getUserId(
settings.gitUser,
settings.authToken,
settings.githubServerUrl
)
await git.config(
'user.email',
`${userId}+${settings.gitUser}@users.noreply.github.com`,
true
)
}
}
} finally { } finally {
// Remove auth // Remove auth
if (authHelper) { if (authHelper) {

View File

@ -79,6 +79,11 @@ export interface IGitSourceSettings {
*/ */
authToken: string authToken: string
/**
* A github user slug to set a default user name and email in the local git config
*/
gitUser: string
/** /**
* The SSH key to configure * The SSH key to configure
*/ */

View File

@ -143,3 +143,15 @@ async function downloadArchive(
}) })
return Buffer.from(response.data as ArrayBuffer) // response.data is ArrayBuffer return Buffer.from(response.data as ArrayBuffer) // response.data is ArrayBuffer
} }
export async function getUserId(
username: string,
authToken: string,
baseUrl?: string
): Promise<number> {
const octokit = github.getOctokit(authToken, {
baseUrl: getServerApiUrl(baseUrl)
})
const user = await octokit.rest.users.getByUsername({username})
return user.data.id
}

View File

@ -138,6 +138,9 @@ export async function getInputs(): Promise<IGitSourceSettings> {
// Auth token // Auth token
result.authToken = core.getInput('token', {required: true}) result.authToken = core.getInput('token', {required: true})
// Git user
result.gitUser = core.getInput('git-user') || 'github-action[bot]'
// SSH // SSH
result.sshKey = core.getInput('ssh-key') result.sshKey = core.getInput('ssh-key')
result.sshKnownHosts = core.getInput('ssh-known-hosts') result.sshKnownHosts = core.getInput('ssh-known-hosts')