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 }}
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
# git config, which enables your scripts to run authenticated git commands. The
# post-job step removes the SSH key.
@ -324,8 +330,6 @@ jobs:
- run: |
date > generated.txt
# 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 commit -m "generated"
git push
@ -348,8 +352,6 @@ jobs:
- run: |
date > generated.txt
# 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 commit -m "generated"
git push

View File

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

View File

@ -1,6 +1,6 @@
import * as exec from '@actions/exec'
import * as fshelper from '../lib/fs-helper'
import * as commandManager from '../lib/git-command-manager'
import * as fshelper from '../src/fs-helper'
import * as commandManager from '../src/git-command-manager'
let git: commandManager.IGitCommandManager
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 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 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')
let repositoryPath: string

View File

@ -1,10 +1,10 @@
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 inputHelper from '../lib/input-helper'
import * as inputHelper from '../src/input-helper'
import * as path from 'path'
import * as workflowContextHelper from '../lib/workflow-context-helper'
import {IGitSourceSettings} from '../lib/git-source-settings'
import * as workflowContextHelper from '../src/workflow-context-helper'
import {IGitSourceSettings} from '../src/git-source-settings'
const originalGitHubWorkspace = process.env['GITHUB_WORKSPACE']
const gitHubWorkspace = path.resolve('/checkout-tests/workspace')

View File

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

View File

@ -1,5 +1,5 @@
import * as core from '@actions/core'
import {RetryHelper} from '../lib/retry-helper'
import {RetryHelper} from '../src/retry-helper'
let info: string[]
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)
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:
description: >
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
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}`);
this.gitEnv['GIT_HTTP_USER_AGENT'] = gitHttpUserAgent;
});
@ -1599,6 +1609,15 @@ function getSource(settings) {
core.setOutput('commit', commitSHA.trim());
// Check for incorrect pull request merge commit
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 {
// Remove auth
@ -1788,6 +1807,7 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge
Object.defineProperty(exports, "__esModule", ({ value: true }));
exports.downloadRepository = downloadRepository;
exports.getDefaultBranch = getDefaultBranch;
exports.getUserId = getUserId;
const assert = __importStar(__nccwpck_require__(9491));
const core = __importStar(__nccwpck_require__(2186));
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
});
}
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}`);
// Auth token
result.authToken = core.getInput('token', { required: true });
// Git user
result.gitUser = core.getInput('git-user') || 'github-action[bot]';
// SSH
result.sshKey = core.getInput('ssh-key');
result.sshKnownHosts = core.getInput('ssh-known-hosts');

View File

@ -730,7 +730,19 @@ class GitCommandManager {
}
}
// 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}`)
this.gitEnv['GIT_HTTP_USER_AGENT'] = gitHttpUserAgent
}

View File

@ -274,6 +274,23 @@ export async function getSource(settings: IGitSourceSettings): Promise<void> {
settings.commit,
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 {
// Remove auth
if (authHelper) {

View File

@ -79,6 +79,11 @@ export interface IGitSourceSettings {
*/
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
*/

View File

@ -143,3 +143,15 @@ async function downloadArchive(
})
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
result.authToken = core.getInput('token', {required: true})
// Git user
result.gitUser = core.getInput('git-user') || 'github-action[bot]'
// SSH
result.sshKey = core.getInput('ssh-key')
result.sshKnownHosts = core.getInput('ssh-known-hosts')