mirror of
https://github.com/actions/checkout.git
synced 2025-06-03 14:02:37 +08:00
Compare commits
8 Commits
68a03db899
...
b9de40a228
Author | SHA1 | Date | |
---|---|---|---|
|
b9de40a228 | ||
|
85e6279cec | ||
|
009b9ae9e4 | ||
|
cbb722410c | ||
|
3b9b8c884f | ||
|
11bd71901b | ||
|
e3d2460bbb | ||
|
163217dfcd |
@ -1,5 +1,9 @@
|
||||
# Changelog
|
||||
|
||||
## v4.2.2
|
||||
* `url-helper.ts` now leverages well-known environment variables by @jww3 in https://github.com/actions/checkout/pull/1941
|
||||
* Expand unit test coverage for `isGhes` by @jww3 in https://github.com/actions/checkout/pull/1946
|
||||
|
||||
## v4.2.1
|
||||
* Check out other refs/* by commit if provided, fall back to ref by @orhantoy in https://github.com/actions/checkout/pull/1924
|
||||
|
||||
|
53
README.md
53
README.md
@ -126,6 +126,11 @@ Please refer to the [release page](https://github.com/actions/checkout/releases/
|
||||
# running from unless specified. Example URLs are https://github.com or
|
||||
# https://my-ghes-server.example.com
|
||||
github-server-url: ''
|
||||
|
||||
# Use the given object format when creating local repository. Specifically, use
|
||||
# 'sha256' to checkout a SHA-256 repository.
|
||||
# Default: null
|
||||
object-format: ''
|
||||
```
|
||||
<!-- end usage -->
|
||||
|
||||
@ -143,6 +148,8 @@ Please refer to the [release page](https://github.com/actions/checkout/releases/
|
||||
- [Checkout pull request HEAD commit instead of merge commit](#Checkout-pull-request-HEAD-commit-instead-of-merge-commit)
|
||||
- [Checkout pull request on closed event](#Checkout-pull-request-on-closed-event)
|
||||
- [Push a commit using the built-in token](#Push-a-commit-using-the-built-in-token)
|
||||
- [Push a commit to a PR using the built-in token](#Push-a-commit-to-a-PR-using-the-built-in-token)
|
||||
- [Checkout SHA-256 repository](#checkout-sha-256-repository)
|
||||
|
||||
## Fetch only the root files
|
||||
|
||||
@ -211,7 +218,7 @@ Please refer to the [release page](https://github.com/actions/checkout/releases/
|
||||
repository: my-org/my-tools
|
||||
path: my-tools
|
||||
```
|
||||
> - If your secondary repository is private you will need to add the option noted in [Checkout multiple repos (private)](#Checkout-multiple-repos-private)
|
||||
> - If your secondary repository is private or internal you will need to add the option noted in [Checkout multiple repos (private)](#Checkout-multiple-repos-private)
|
||||
|
||||
## Checkout multiple repos (nested)
|
||||
|
||||
@ -225,7 +232,7 @@ Please refer to the [release page](https://github.com/actions/checkout/releases/
|
||||
repository: my-org/my-tools
|
||||
path: my-tools
|
||||
```
|
||||
> - If your secondary repository is private you will need to add the option noted in [Checkout multiple repos (private)](#Checkout-multiple-repos-private)
|
||||
> - If your secondary repository is private or internal you will need to add the option noted in [Checkout multiple repos (private)](#Checkout-multiple-repos-private)
|
||||
|
||||
## Checkout multiple repos (private)
|
||||
|
||||
@ -288,6 +295,48 @@ jobs:
|
||||
```
|
||||
*NOTE:* The user email is `{user.id}+{user.login}@users.noreply.github.com`. See users API: https://api.github.com/users/github-actions%5Bbot%5D
|
||||
|
||||
## Push a commit to a PR using the built-in token
|
||||
|
||||
In a pull request trigger, `ref` is required as GitHub Actions checks out in detached HEAD mode, meaning it doesn’t check out your branch by default.
|
||||
|
||||
```yaml
|
||||
on: pull_request
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
ref: ${{ github.head_ref }}
|
||||
- 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
|
||||
```
|
||||
|
||||
*NOTE:* The user email is `{user.id}+{user.login}@users.noreply.github.com`. See users API: https://api.github.com/users/github-actions%5Bbot%5D
|
||||
|
||||
# Recommended permissions
|
||||
|
||||
When using the `checkout` action in your GitHub Actions workflow, it is recommended to set the following `GITHUB_TOKEN` permissions to ensure proper functionality, unless alternative auth is provided via the `token` or `ssh-key` inputs:
|
||||
|
||||
```yaml
|
||||
permissions:
|
||||
contents: read
|
||||
```
|
||||
|
||||
## Checkout SHA-256 repository
|
||||
|
||||
```yaml
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
object-format: sha256
|
||||
```
|
||||
|
||||
# License
|
||||
|
||||
The scripts and documentation in this project are released under the [MIT License](LICENSE)
|
||||
|
@ -824,7 +824,8 @@ async function setup(testName: string): Promise<void> {
|
||||
sshUser: '',
|
||||
workflowOrganizationId: 123456,
|
||||
setSafeDirectory: true,
|
||||
githubServerUrl: githubServerUrl
|
||||
githubServerUrl: githubServerUrl,
|
||||
objectFormat: undefined
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -375,4 +375,25 @@ describe('Test fetchDepth and fetchTags options', () => {
|
||||
expect.any(Object)
|
||||
)
|
||||
})
|
||||
|
||||
it('should call execGit with the correct arguments when sha256 is used', async () => {
|
||||
jest.spyOn(exec, 'exec').mockImplementation(mockExec)
|
||||
|
||||
const workingDirectory = 'test'
|
||||
const lfs = false
|
||||
const doSparseCheckout = false
|
||||
git = await commandManager.createCommandManager(
|
||||
workingDirectory,
|
||||
lfs,
|
||||
doSparseCheckout
|
||||
)
|
||||
|
||||
await git.init({objectFormat: 'sha256'})
|
||||
|
||||
expect(mockExec).toHaveBeenCalledWith(
|
||||
expect.any(String),
|
||||
['init', '--object-format=sha256', 'test'],
|
||||
expect.any(Object)
|
||||
)
|
||||
})
|
||||
})
|
||||
|
92
__test__/url-helper.test.ts
Normal file
92
__test__/url-helper.test.ts
Normal file
@ -0,0 +1,92 @@
|
||||
import * as urlHelper from '../src/url-helper'
|
||||
|
||||
describe('getServerUrl tests', () => {
|
||||
it('basics', async () => {
|
||||
// Note that URL::toString will append a trailing / when passed just a domain name ...
|
||||
expect(urlHelper.getServerUrl().toString()).toBe('https://github.com/')
|
||||
expect(urlHelper.getServerUrl(' ').toString()).toBe('https://github.com/')
|
||||
expect(urlHelper.getServerUrl(' ').toString()).toBe('https://github.com/')
|
||||
expect(urlHelper.getServerUrl('http://contoso.com').toString()).toBe(
|
||||
'http://contoso.com/'
|
||||
)
|
||||
expect(urlHelper.getServerUrl('https://contoso.com').toString()).toBe(
|
||||
'https://contoso.com/'
|
||||
)
|
||||
expect(urlHelper.getServerUrl('https://contoso.com/').toString()).toBe(
|
||||
'https://contoso.com/'
|
||||
)
|
||||
|
||||
// ... but can't make that same assumption when passed an URL that includes some deeper path.
|
||||
expect(urlHelper.getServerUrl('https://contoso.com/a/b').toString()).toBe(
|
||||
'https://contoso.com/a/b'
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
describe('isGhes tests', () => {
|
||||
const pristineEnv = process.env
|
||||
|
||||
beforeEach(() => {
|
||||
jest.resetModules()
|
||||
process.env = {...pristineEnv}
|
||||
})
|
||||
|
||||
afterAll(() => {
|
||||
process.env = pristineEnv
|
||||
})
|
||||
|
||||
it('basics', async () => {
|
||||
delete process.env['GITHUB_SERVER_URL']
|
||||
expect(urlHelper.isGhes()).toBeFalsy()
|
||||
expect(urlHelper.isGhes('https://github.com')).toBeFalsy()
|
||||
expect(urlHelper.isGhes('https://contoso.ghe.com')).toBeFalsy()
|
||||
expect(urlHelper.isGhes('https://test.github.localhost')).toBeFalsy()
|
||||
expect(urlHelper.isGhes('https://src.onpremise.fabrikam.com')).toBeTruthy()
|
||||
})
|
||||
|
||||
it('returns false when the GITHUB_SERVER_URL environment variable is not defined', async () => {
|
||||
delete process.env['GITHUB_SERVER_URL']
|
||||
expect(urlHelper.isGhes()).toBeFalsy()
|
||||
})
|
||||
|
||||
it('returns false when the GITHUB_SERVER_URL environment variable is set to github.com', async () => {
|
||||
process.env['GITHUB_SERVER_URL'] = 'https://github.com'
|
||||
expect(urlHelper.isGhes()).toBeFalsy()
|
||||
})
|
||||
|
||||
it('returns false when the GITHUB_SERVER_URL environment variable is set to a GitHub Enterprise Cloud-style URL', async () => {
|
||||
process.env['GITHUB_SERVER_URL'] = 'https://contoso.ghe.com'
|
||||
expect(urlHelper.isGhes()).toBeFalsy()
|
||||
})
|
||||
|
||||
it('returns false when the GITHUB_SERVER_URL environment variable has a .localhost suffix', async () => {
|
||||
process.env['GITHUB_SERVER_URL'] = 'https://mock-github.localhost'
|
||||
expect(urlHelper.isGhes()).toBeFalsy()
|
||||
})
|
||||
|
||||
it('returns true when the GITHUB_SERVER_URL environment variable is set to some other URL', async () => {
|
||||
process.env['GITHUB_SERVER_URL'] = 'https://src.onpremise.fabrikam.com'
|
||||
expect(urlHelper.isGhes()).toBeTruthy()
|
||||
})
|
||||
})
|
||||
|
||||
describe('getServerApiUrl tests', () => {
|
||||
it('basics', async () => {
|
||||
expect(urlHelper.getServerApiUrl()).toBe('https://api.github.com')
|
||||
expect(urlHelper.getServerApiUrl('https://github.com')).toBe(
|
||||
'https://api.github.com'
|
||||
)
|
||||
expect(urlHelper.getServerApiUrl('https://GitHub.com')).toBe(
|
||||
'https://api.github.com'
|
||||
)
|
||||
expect(urlHelper.getServerApiUrl('https://contoso.ghe.com')).toBe(
|
||||
'https://api.contoso.ghe.com'
|
||||
)
|
||||
expect(urlHelper.getServerApiUrl('https://fabrikam.GHE.COM')).toBe(
|
||||
'https://api.fabrikam.ghe.com'
|
||||
)
|
||||
expect(
|
||||
urlHelper.getServerApiUrl('https://src.onpremise.fabrikam.com')
|
||||
).toBe('https://src.onpremise.fabrikam.com/api/v3')
|
||||
})
|
||||
})
|
@ -98,6 +98,11 @@ inputs:
|
||||
github-server-url:
|
||||
description: The base URL for the GitHub instance that you are trying to clone from, will use environment defaults to fetch from the same instance that the workflow is running from unless specified. Example URLs are https://github.com or https://my-ghes-server.example.com
|
||||
required: false
|
||||
object-format:
|
||||
description: >
|
||||
Use the given object format when creating local repository. Specifically, use
|
||||
'sha256' to checkout a SHA-256 repository.
|
||||
default: null
|
||||
outputs:
|
||||
ref:
|
||||
description: 'The branch, tag or SHA that was checked out'
|
||||
|
68
dist/index.js
vendored
68
dist/index.js
vendored
@ -709,9 +709,13 @@ class GitCommandManager {
|
||||
getWorkingDirectory() {
|
||||
return this.workingDirectory;
|
||||
}
|
||||
init() {
|
||||
init(options) {
|
||||
return __awaiter(this, void 0, void 0, function* () {
|
||||
yield this.execGit(['init', this.workingDirectory]);
|
||||
yield this.execGit([
|
||||
'init',
|
||||
...((options === null || options === void 0 ? void 0 : options.objectFormat) ? [`--object-format=${options.objectFormat}`] : []),
|
||||
this.workingDirectory
|
||||
]);
|
||||
});
|
||||
}
|
||||
isDetached() {
|
||||
@ -1236,7 +1240,7 @@ function getSource(settings) {
|
||||
// Initialize the repository
|
||||
if (!fsHelper.directoryExistsSync(path.join(settings.repositoryPath, '.git'))) {
|
||||
core.startGroup('Initializing the repository');
|
||||
yield git.init();
|
||||
yield git.init({ objectFormat: settings.objectFormat });
|
||||
yield git.remoteAdd('origin', repositoryUrl);
|
||||
core.endGroup();
|
||||
}
|
||||
@ -1831,6 +1835,14 @@ 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}`);
|
||||
// Object format
|
||||
const objectFormat = core.getInput('object-format');
|
||||
if (objectFormat) {
|
||||
if (objectFormat != 'sha1' && objectFormat != 'sha256') {
|
||||
throw Error(`Invalid object format '${objectFormat}'`);
|
||||
}
|
||||
result.objectFormat = objectFormat;
|
||||
}
|
||||
return result;
|
||||
});
|
||||
}
|
||||
@ -2454,22 +2466,50 @@ function getFetchUrl(settings) {
|
||||
return `${serviceUrl.origin}/${encodedOwner}/${encodedName}`;
|
||||
}
|
||||
function getServerUrl(url) {
|
||||
let urlValue = url && url.trim().length > 0
|
||||
? url
|
||||
: process.env['GITHUB_SERVER_URL'] || 'https://github.com';
|
||||
return new url_1.URL(urlValue);
|
||||
let resolvedUrl = process.env['GITHUB_SERVER_URL'] || 'https://github.com';
|
||||
if (hasContent(url, WhitespaceMode.Trim)) {
|
||||
resolvedUrl = url;
|
||||
}
|
||||
return new url_1.URL(resolvedUrl);
|
||||
}
|
||||
function getServerApiUrl(url) {
|
||||
let apiUrl = 'https://api.github.com';
|
||||
if (isGhes(url)) {
|
||||
const serverUrl = getServerUrl(url);
|
||||
apiUrl = new url_1.URL(`${serverUrl.origin}/api/v3`).toString();
|
||||
if (hasContent(url, WhitespaceMode.Trim)) {
|
||||
let serverUrl = getServerUrl(url);
|
||||
if (isGhes(url)) {
|
||||
serverUrl.pathname = 'api/v3';
|
||||
}
|
||||
else {
|
||||
serverUrl.hostname = 'api.' + serverUrl.hostname;
|
||||
}
|
||||
return pruneSuffix(serverUrl.toString(), '/');
|
||||
}
|
||||
return apiUrl;
|
||||
return process.env['GITHUB_API_URL'] || 'https://api.github.com';
|
||||
}
|
||||
function isGhes(url) {
|
||||
const ghUrl = getServerUrl(url);
|
||||
return ghUrl.hostname.toUpperCase() !== 'GITHUB.COM';
|
||||
const ghUrl = new url_1.URL(url || process.env['GITHUB_SERVER_URL'] || 'https://github.com');
|
||||
const hostname = ghUrl.hostname.trimEnd().toUpperCase();
|
||||
const isGitHubHost = hostname === 'GITHUB.COM';
|
||||
const isGitHubEnterpriseCloudHost = hostname.endsWith('.GHE.COM');
|
||||
const isLocalHost = hostname.endsWith('.LOCALHOST');
|
||||
return !isGitHubHost && !isGitHubEnterpriseCloudHost && !isLocalHost;
|
||||
}
|
||||
function pruneSuffix(text, suffix) {
|
||||
if (hasContent(suffix, WhitespaceMode.Preserve) && (text === null || text === void 0 ? void 0 : text.endsWith(suffix))) {
|
||||
return text.substring(0, text.length - suffix.length);
|
||||
}
|
||||
return text;
|
||||
}
|
||||
var WhitespaceMode;
|
||||
(function (WhitespaceMode) {
|
||||
WhitespaceMode[WhitespaceMode["Trim"] = 0] = "Trim";
|
||||
WhitespaceMode[WhitespaceMode["Preserve"] = 1] = "Preserve";
|
||||
})(WhitespaceMode || (WhitespaceMode = {}));
|
||||
function hasContent(text, whitespaceMode) {
|
||||
let refinedText = text !== null && text !== void 0 ? text : '';
|
||||
if (whitespaceMode == WhitespaceMode.Trim) {
|
||||
refinedText = refinedText.trim();
|
||||
}
|
||||
return refinedText.length > 0;
|
||||
}
|
||||
|
||||
|
||||
|
4
package-lock.json
generated
4
package-lock.json
generated
@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "checkout",
|
||||
"version": "4.2.1",
|
||||
"version": "4.2.2",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "checkout",
|
||||
"version": "4.2.1",
|
||||
"version": "4.2.2",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@actions/core": "^1.10.1",
|
||||
|
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "checkout",
|
||||
"version": "4.2.1",
|
||||
"version": "4.2.2",
|
||||
"description": "checkout action",
|
||||
"main": "lib/main.js",
|
||||
"scripts": {
|
||||
|
@ -42,7 +42,7 @@ export interface IGitCommandManager {
|
||||
): Promise<void>
|
||||
getDefaultBranch(repositoryUrl: string): Promise<string>
|
||||
getWorkingDirectory(): string
|
||||
init(): Promise<void>
|
||||
init(options?: {objectFormat?: string}): Promise<void>
|
||||
isDetached(): Promise<boolean>
|
||||
lfsFetch(ref: string): Promise<void>
|
||||
lfsInstall(): Promise<void>
|
||||
@ -327,8 +327,14 @@ class GitCommandManager {
|
||||
return this.workingDirectory
|
||||
}
|
||||
|
||||
async init(): Promise<void> {
|
||||
await this.execGit(['init', this.workingDirectory])
|
||||
async init(options?: {objectFormat?: string}): Promise<void> {
|
||||
await this.execGit([
|
||||
'init',
|
||||
...(options?.objectFormat
|
||||
? [`--object-format=${options.objectFormat}`]
|
||||
: []),
|
||||
this.workingDirectory
|
||||
])
|
||||
}
|
||||
|
||||
async isDetached(): Promise<boolean> {
|
||||
|
@ -110,7 +110,7 @@ export async function getSource(settings: IGitSourceSettings): Promise<void> {
|
||||
!fsHelper.directoryExistsSync(path.join(settings.repositoryPath, '.git'))
|
||||
) {
|
||||
core.startGroup('Initializing the repository')
|
||||
await git.init()
|
||||
await git.init({objectFormat: settings.objectFormat})
|
||||
await git.remoteAdd('origin', repositoryUrl)
|
||||
core.endGroup()
|
||||
}
|
||||
|
@ -118,4 +118,9 @@ export interface IGitSourceSettings {
|
||||
* User override on the GitHub Server/Host URL that hosts the repository to be cloned
|
||||
*/
|
||||
githubServerUrl: string | undefined
|
||||
|
||||
/**
|
||||
* Object format used for the repo, if it is not default
|
||||
*/
|
||||
objectFormat: 'sha1' | 'sha256' | undefined
|
||||
}
|
||||
|
@ -161,5 +161,14 @@ export async function getInputs(): Promise<IGitSourceSettings> {
|
||||
result.githubServerUrl = core.getInput('github-server-url')
|
||||
core.debug(`GitHub Host URL = ${result.githubServerUrl}`)
|
||||
|
||||
// Object format
|
||||
const objectFormat = core.getInput('object-format')
|
||||
if (objectFormat) {
|
||||
if (objectFormat != 'sha1' && objectFormat != 'sha256') {
|
||||
throw Error(`Invalid object format '${objectFormat}'`)
|
||||
}
|
||||
result.objectFormat = objectFormat
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
@ -21,26 +21,61 @@ export function getFetchUrl(settings: IGitSourceSettings): string {
|
||||
}
|
||||
|
||||
export function getServerUrl(url?: string): URL {
|
||||
let urlValue =
|
||||
url && url.trim().length > 0
|
||||
? url
|
||||
: process.env['GITHUB_SERVER_URL'] || 'https://github.com'
|
||||
return new URL(urlValue)
|
||||
let resolvedUrl = process.env['GITHUB_SERVER_URL'] || 'https://github.com'
|
||||
if (hasContent(url, WhitespaceMode.Trim)) {
|
||||
resolvedUrl = url!
|
||||
}
|
||||
|
||||
return new URL(resolvedUrl)
|
||||
}
|
||||
|
||||
export function getServerApiUrl(url?: string): string {
|
||||
let apiUrl = 'https://api.github.com'
|
||||
if (hasContent(url, WhitespaceMode.Trim)) {
|
||||
let serverUrl = getServerUrl(url)
|
||||
if (isGhes(url)) {
|
||||
serverUrl.pathname = 'api/v3'
|
||||
} else {
|
||||
serverUrl.hostname = 'api.' + serverUrl.hostname
|
||||
}
|
||||
|
||||
if (isGhes(url)) {
|
||||
const serverUrl = getServerUrl(url)
|
||||
apiUrl = new URL(`${serverUrl.origin}/api/v3`).toString()
|
||||
return pruneSuffix(serverUrl.toString(), '/')
|
||||
}
|
||||
|
||||
return apiUrl
|
||||
return process.env['GITHUB_API_URL'] || 'https://api.github.com'
|
||||
}
|
||||
|
||||
export function isGhes(url?: string): boolean {
|
||||
const ghUrl = getServerUrl(url)
|
||||
const ghUrl = new URL(
|
||||
url || process.env['GITHUB_SERVER_URL'] || 'https://github.com'
|
||||
)
|
||||
|
||||
return ghUrl.hostname.toUpperCase() !== 'GITHUB.COM'
|
||||
const hostname = ghUrl.hostname.trimEnd().toUpperCase()
|
||||
const isGitHubHost = hostname === 'GITHUB.COM'
|
||||
const isGitHubEnterpriseCloudHost = hostname.endsWith('.GHE.COM')
|
||||
const isLocalHost = hostname.endsWith('.LOCALHOST')
|
||||
|
||||
return !isGitHubHost && !isGitHubEnterpriseCloudHost && !isLocalHost
|
||||
}
|
||||
|
||||
function pruneSuffix(text: string, suffix: string) {
|
||||
if (hasContent(suffix, WhitespaceMode.Preserve) && text?.endsWith(suffix)) {
|
||||
return text.substring(0, text.length - suffix.length)
|
||||
}
|
||||
return text
|
||||
}
|
||||
|
||||
enum WhitespaceMode {
|
||||
Trim,
|
||||
Preserve
|
||||
}
|
||||
|
||||
function hasContent(
|
||||
text: string | undefined,
|
||||
whitespaceMode: WhitespaceMode
|
||||
): boolean {
|
||||
let refinedText = text ?? ''
|
||||
if (whitespaceMode == WhitespaceMode.Trim) {
|
||||
refinedText = refinedText.trim()
|
||||
}
|
||||
return refinedText.length > 0
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user