From c170eefc2657d93cc91397be50a299bff978a052 Mon Sep 17 00:00:00 2001 From: eric sciple Date: Thu, 12 Dec 2019 13:49:26 -0500 Subject: [PATCH] add input persist-credentials (#107) --- .github/workflows/test.yml | 10 ++-- README.md | 18 ++++-- __test__/input-helper.test.ts | 2 +- action.yml | 14 +++-- dist/index.js | 97 +++++++++++++++--------------- src/git-command-manager.ts | 13 +++-- src/git-source-provider.ts | 107 +++++++++++++++++----------------- src/github-api-helper.ts | 8 +-- src/input-helper.ts | 8 ++- 9 files changed, 149 insertions(+), 128 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 62dce24..93a3792 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -18,6 +18,7 @@ jobs: - run: npm run lint - run: npm run pack - run: npm run gendocs + - run: npm test - name: Verify no unstaged changes run: __test__/verify-no-unstaged-changes.sh @@ -84,15 +85,12 @@ jobs: test-job-container: runs-on: ubuntu-latest - container: pstauffer/curl:latest + container: alpine:latest steps: # Clone this repo - # todo: after v2-beta contains the latest changes, switch this to "uses: actions/checkout@v2-beta". Also switch to "alpine:latest" + # todo: after v2-beta contains the latest changes, switch this to "uses: actions/checkout@v2-beta" - name: Checkout - run: | - curl --location --user token:${{ github.token }} --output checkout.tar.gz https://api.github.com/repos/actions/checkout/tarball/${{ github.sha }} - tar -xzf checkout.tar.gz - mv */* ./ + uses: actions/checkout@a572f640b07e96fc5837b3adfa0e5a2ddd8dae21 # Basic checkout - name: Basic checkout diff --git a/README.md b/README.md index 44aea2c..f27df23 100644 --- a/README.md +++ b/README.md @@ -15,16 +15,16 @@ Refer [here](https://help.github.com/en/articles/events-that-trigger-workflows) - Improved fetch performance - The default behavior now fetches only the commit being checked-out - Script authenticated git commands - - Persists `with.token` in the local git config + - Persists the input `token` in the local git config - Enables your scripts to run authenticated git commands - Post-job cleanup removes the token - - Coming soon: Opt out by setting `with.persist-credentials` to `false` + - Opt out by setting the input `persist-credentials: false` - Creates a local branch - No longer detached HEAD when checking out a branch - A local branch is created with the corresponding upstream branch set - Improved layout - - `with.path` is always relative to `github.workspace` - - Aligns better with container actions, where `github.workspace` gets mapped in + - The input `path` is always relative to $GITHUB_WORKSPACE + - Aligns better with container actions, where $GITHUB_WORKSPACE gets mapped in - Fallback to REST API download - When Git 2.18 or higher is not in the PATH, the REST API will be used to download the files - Removed input `submodules` @@ -41,15 +41,21 @@ Refer [here](https://github.com/actions/checkout/blob/v1/README.md) for previous # Default: ${{ github.repository }} repository: '' - # The branch, tag or SHA to checkout. When checking out the repository that + # The branch, tag or SHA to checkout. When checking out the repository that # triggered a workflow, this defaults to the reference or SHA for that event. # Otherwise, defaults to `master`. ref: '' - # Access token for clone repository + # Auth token used to fetch the repository. The token is stored in the local git + # config, which enables your scripts to run authenticated git commands. The + # post-job step removes the token from the git config. # Default: ${{ github.token }} token: '' + # Whether to persist the token in the git config + # Default: true + persist-credentials: '' + # Relative path under $GITHUB_WORKSPACE to place the repository path: '' diff --git a/__test__/input-helper.test.ts b/__test__/input-helper.test.ts index 6010e11..ecd3cd1 100644 --- a/__test__/input-helper.test.ts +++ b/__test__/input-helper.test.ts @@ -63,7 +63,7 @@ describe('input-helper tests', () => { it('sets defaults', () => { const settings: ISourceSettings = inputHelper.getInputs() expect(settings).toBeTruthy() - expect(settings.accessToken).toBeFalsy() + expect(settings.authToken).toBeFalsy() expect(settings.clean).toBe(true) expect(settings.commit).toBeTruthy() expect(settings.commit).toBe('1234567890123456789012345678901234567890') diff --git a/action.yml b/action.yml index d21e5f1..54cf4b3 100644 --- a/action.yml +++ b/action.yml @@ -6,12 +6,18 @@ inputs: default: ${{ github.repository }} ref: description: > - The branch, tag or SHA to checkout. When checking out the repository - that triggered a workflow, this defaults to the reference or SHA for - that event. Otherwise, defaults to `master`. + The branch, tag or SHA to checkout. When checking out the repository that + triggered a workflow, this defaults to the reference or SHA for that + event. Otherwise, defaults to `master`. token: - description: 'Access token for clone repository' + description: > + Auth token used to fetch the repository. The token is stored in the local + git config, which enables your scripts to run authenticated git commands. + The post-job step removes the token from the git config. default: ${{ github.token }} + persist-credentials: + description: 'Whether to persist the token in the git config' + default: true path: description: 'Relative path under $GITHUB_WORKSPACE to place the repository' clean: diff --git a/dist/index.js b/dist/index.js index c6444b5..380d8c3 100644 --- a/dist/index.js +++ b/dist/index.js @@ -4838,7 +4838,7 @@ class GitCommandManager { } config(configKey, configValue) { return __awaiter(this, void 0, void 0, function* () { - yield this.execGit(['config', configKey, configValue]); + yield this.execGit(['config', '--local', configKey, configValue]); }); } configExists(configKey) { @@ -4846,7 +4846,7 @@ class GitCommandManager { const pattern = configKey.replace(/[^a-zA-Z0-9_]/g, x => { return `\\${x}`; }); - const output = yield this.execGit(['config', '--name-only', '--get-regexp', pattern], true); + const output = yield this.execGit(['config', '--local', '--name-only', '--get-regexp', pattern], true); return output.exitCode === 0; }); } @@ -4932,19 +4932,19 @@ class GitCommandManager { } tryConfigUnset(configKey) { return __awaiter(this, void 0, void 0, function* () { - const output = yield this.execGit(['config', '--unset-all', configKey], true); + const output = yield this.execGit(['config', '--local', '--unset-all', configKey], true); return output.exitCode === 0; }); } tryDisableAutomaticGarbageCollection() { return __awaiter(this, void 0, void 0, function* () { - const output = yield this.execGit(['config', 'gc.auto', '0'], true); + const output = yield this.execGit(['config', '--local', 'gc.auto', '0'], true); return output.exitCode === 0; }); } tryGetFetchUrl() { return __awaiter(this, void 0, void 0, function* () { - const output = yield this.execGit(['config', '--get', 'remote.origin.url'], true); + const output = yield this.execGit(['config', '--local', '--get', 'remote.origin.url'], true); if (output.exitCode !== 0) { return ''; } @@ -5121,7 +5121,7 @@ function getSource(settings) { // Downloading using REST API core.info(`The repository will be downloaded using the GitHub REST API`); core.info(`To create a local Git repository instead, add Git ${gitCommandManager.MinimumGitVersion} or higher to the PATH`); - yield githubApiHelper.downloadRepository(settings.accessToken, settings.repositoryOwner, settings.repositoryName, settings.ref, settings.commit, settings.repositoryPath); + yield githubApiHelper.downloadRepository(settings.authToken, settings.repositoryOwner, settings.repositoryName, settings.ref, settings.commit, settings.repositoryPath); } else { // Save state for POST action @@ -5137,30 +5137,34 @@ function getSource(settings) { } // Remove possible previous extraheader yield removeGitConfig(git, authConfigKey); - // Add extraheader (auth) - const base64Credentials = Buffer.from(`x-access-token:${settings.accessToken}`, 'utf8').toString('base64'); - core.setSecret(base64Credentials); - const authConfigValue = `AUTHORIZATION: basic ${base64Credentials}`; - yield git.config(authConfigKey, authConfigValue); - // LFS install - if (settings.lfs) { - yield git.lfsInstall(); + try { + // Config auth token + yield configureAuthToken(git, settings.authToken); + // LFS install + if (settings.lfs) { + yield git.lfsInstall(); + } + // Fetch + const refSpec = refHelper.getRefSpec(settings.ref, settings.commit); + yield git.fetch(settings.fetchDepth, refSpec); + // Checkout info + const checkoutInfo = yield refHelper.getCheckoutInfo(git, settings.ref, settings.commit); + // LFS fetch + // Explicit lfs-fetch to avoid slow checkout (fetches one lfs object at a time). + // Explicit lfs fetch will fetch lfs objects in parallel. + if (settings.lfs) { + yield git.lfsFetch(checkoutInfo.startPoint || checkoutInfo.ref); + } + // Checkout + yield git.checkout(checkoutInfo.ref, checkoutInfo.startPoint); + // Dump some info about the checked out commit + yield git.log1(); } - // Fetch - const refSpec = refHelper.getRefSpec(settings.ref, settings.commit); - yield git.fetch(settings.fetchDepth, refSpec); - // Checkout info - const checkoutInfo = yield refHelper.getCheckoutInfo(git, settings.ref, settings.commit); - // LFS fetch - // Explicit lfs-fetch to avoid slow checkout (fetches one lfs object at a time). - // Explicit lfs fetch will fetch lfs objects in parallel. - if (settings.lfs) { - yield git.lfsFetch(checkoutInfo.startPoint || checkoutInfo.ref); + finally { + if (!settings.persistCredentials) { + yield removeGitConfig(git, authConfigKey); + } } - // Checkout - yield git.checkout(checkoutInfo.ref, checkoutInfo.startPoint); - // Dump some info about the checked out commit - yield git.log1(); } }); } @@ -5265,23 +5269,21 @@ function prepareExistingDirectory(git, repositoryPath, repositoryUrl, clean) { } }); } +function configureAuthToken(git, authToken) { + return __awaiter(this, void 0, void 0, function* () { + // Add extraheader (auth) + const base64Credentials = Buffer.from(`x-access-token:${authToken}`, 'utf8').toString('base64'); + core.setSecret(base64Credentials); + const authConfigValue = `AUTHORIZATION: basic ${base64Credentials}`; + yield git.config(authConfigKey, authConfigValue); + }); +} function removeGitConfig(git, configKey) { return __awaiter(this, void 0, void 0, function* () { if ((yield git.configExists(configKey)) && !(yield git.tryConfigUnset(configKey))) { // Load the config contents - core.warning(`Failed to remove '${configKey}' from the git config. Attempting to remove the config value by editing the file directly.`); - const configPath = path.join(git.getWorkingDirectory(), '.git', 'config'); - fsHelper.fileExistsSync(configPath); - let contents = fs.readFileSync(configPath).toString() || ''; - // Filter - only includes lines that do not contain the config key - const upperConfigKey = configKey.toUpperCase(); - const split = contents - .split('\n') - .filter(x => !x.toUpperCase().includes(upperConfigKey)); - contents = split.join('\n'); - // Rewrite the config file - fs.writeFileSync(configPath, contents); + core.warning(`Failed to remove '${configKey}' from the git config`); } }); } @@ -8403,12 +8405,12 @@ const retryHelper = __importStar(__webpack_require__(587)); const toolCache = __importStar(__webpack_require__(533)); const v4_1 = __importDefault(__webpack_require__(826)); const IS_WINDOWS = process.platform === 'win32'; -function downloadRepository(accessToken, owner, repo, ref, commit, repositoryPath) { +function downloadRepository(authToken, owner, repo, ref, commit, repositoryPath) { return __awaiter(this, void 0, void 0, function* () { // Download the archive let archiveData = yield retryHelper.execute(() => __awaiter(this, void 0, void 0, function* () { core.info('Downloading the archive'); - return yield downloadArchive(accessToken, owner, repo, ref, commit); + return yield downloadArchive(authToken, owner, repo, ref, commit); })); // Write archive to disk core.info('Writing archive to disk'); @@ -8449,9 +8451,9 @@ function downloadRepository(accessToken, owner, repo, ref, commit, repositoryPat }); } exports.downloadRepository = downloadRepository; -function downloadArchive(accessToken, owner, repo, ref, commit) { +function downloadArchive(authToken, owner, repo, ref, commit) { return __awaiter(this, void 0, void 0, function* () { - const octokit = new github.GitHub(accessToken); + const octokit = new github.GitHub(authToken); const params = { owner: owner, repo: repo, @@ -12764,8 +12766,11 @@ function getInputs() { // LFS result.lfs = (core.getInput('lfs') || 'false').toUpperCase() === 'TRUE'; core.debug(`lfs = ${result.lfs}`); - // Access token - result.accessToken = core.getInput('token'); + // Auth token + result.authToken = core.getInput('token'); + // Persist credentials + result.persistCredentials = + (core.getInput('persist-credentials') || 'false').toUpperCase() === 'TRUE'; return result; } exports.getInputs = getInputs; diff --git a/src/git-command-manager.ts b/src/git-command-manager.ts index ddd5ec6..f86b8c0 100644 --- a/src/git-command-manager.ts +++ b/src/git-command-manager.ts @@ -116,7 +116,7 @@ class GitCommandManager { } async config(configKey: string, configValue: string): Promise { - await this.execGit(['config', configKey, configValue]) + await this.execGit(['config', '--local', configKey, configValue]) } async configExists(configKey: string): Promise { @@ -124,7 +124,7 @@ class GitCommandManager { return `\\${x}` }) const output = await this.execGit( - ['config', '--name-only', '--get-regexp', pattern], + ['config', '--local', '--name-only', '--get-regexp', pattern], true ) return output.exitCode === 0 @@ -211,20 +211,23 @@ class GitCommandManager { async tryConfigUnset(configKey: string): Promise { const output = await this.execGit( - ['config', '--unset-all', configKey], + ['config', '--local', '--unset-all', configKey], true ) return output.exitCode === 0 } async tryDisableAutomaticGarbageCollection(): Promise { - const output = await this.execGit(['config', 'gc.auto', '0'], true) + const output = await this.execGit( + ['config', '--local', 'gc.auto', '0'], + true + ) return output.exitCode === 0 } async tryGetFetchUrl(): Promise { const output = await this.execGit( - ['config', '--get', 'remote.origin.url'], + ['config', '--local', '--get', 'remote.origin.url'], true ) diff --git a/src/git-source-provider.ts b/src/git-source-provider.ts index bf086ff..6b7a9f7 100644 --- a/src/git-source-provider.ts +++ b/src/git-source-provider.ts @@ -1,5 +1,4 @@ import * as core from '@actions/core' -import * as coreCommand from '@actions/core/lib/command' import * as fs from 'fs' import * as fsHelper from './fs-helper' import * as gitCommandManager from './git-command-manager' @@ -21,7 +20,8 @@ export interface ISourceSettings { clean: boolean fetchDepth: number lfs: boolean - accessToken: string + authToken: string + persistCredentials: boolean } export async function getSource(settings: ISourceSettings): Promise { @@ -65,7 +65,7 @@ export async function getSource(settings: ISourceSettings): Promise { `To create a local Git repository instead, add Git ${gitCommandManager.MinimumGitVersion} or higher to the PATH` ) await githubApiHelper.downloadRepository( - settings.accessToken, + settings.authToken, settings.repositoryOwner, settings.repositoryName, settings.ref, @@ -94,43 +94,43 @@ export async function getSource(settings: ISourceSettings): Promise { // Remove possible previous extraheader await removeGitConfig(git, authConfigKey) - // Add extraheader (auth) - const base64Credentials = Buffer.from( - `x-access-token:${settings.accessToken}`, - 'utf8' - ).toString('base64') - core.setSecret(base64Credentials) - const authConfigValue = `AUTHORIZATION: basic ${base64Credentials}` - await git.config(authConfigKey, authConfigValue) + try { + // Config auth token + await configureAuthToken(git, settings.authToken) - // LFS install - if (settings.lfs) { - await git.lfsInstall() + // LFS install + if (settings.lfs) { + await git.lfsInstall() + } + + // Fetch + const refSpec = refHelper.getRefSpec(settings.ref, settings.commit) + await git.fetch(settings.fetchDepth, refSpec) + + // Checkout info + const checkoutInfo = await refHelper.getCheckoutInfo( + git, + settings.ref, + settings.commit + ) + + // LFS fetch + // Explicit lfs-fetch to avoid slow checkout (fetches one lfs object at a time). + // Explicit lfs fetch will fetch lfs objects in parallel. + if (settings.lfs) { + await git.lfsFetch(checkoutInfo.startPoint || checkoutInfo.ref) + } + + // Checkout + await git.checkout(checkoutInfo.ref, checkoutInfo.startPoint) + + // Dump some info about the checked out commit + await git.log1() + } finally { + if (!settings.persistCredentials) { + await removeGitConfig(git, authConfigKey) + } } - - // Fetch - const refSpec = refHelper.getRefSpec(settings.ref, settings.commit) - await git.fetch(settings.fetchDepth, refSpec) - - // Checkout info - const checkoutInfo = await refHelper.getCheckoutInfo( - git, - settings.ref, - settings.commit - ) - - // LFS fetch - // Explicit lfs-fetch to avoid slow checkout (fetches one lfs object at a time). - // Explicit lfs fetch will fetch lfs objects in parallel. - if (settings.lfs) { - await git.lfsFetch(checkoutInfo.startPoint || checkoutInfo.ref) - } - - // Checkout - await git.checkout(checkoutInfo.ref, checkoutInfo.startPoint) - - // Dump some info about the checked out commit - await git.log1() } } @@ -255,6 +255,20 @@ async function prepareExistingDirectory( } } +async function configureAuthToken( + git: IGitCommandManager, + authToken: string +): Promise { + // Add extraheader (auth) + const base64Credentials = Buffer.from( + `x-access-token:${authToken}`, + 'utf8' + ).toString('base64') + core.setSecret(base64Credentials) + const authConfigValue = `AUTHORIZATION: basic ${base64Credentials}` + await git.config(authConfigKey, authConfigValue) +} + async function removeGitConfig( git: IGitCommandManager, configKey: string @@ -264,21 +278,6 @@ async function removeGitConfig( !(await git.tryConfigUnset(configKey)) ) { // Load the config contents - core.warning( - `Failed to remove '${configKey}' from the git config. Attempting to remove the config value by editing the file directly.` - ) - const configPath = path.join(git.getWorkingDirectory(), '.git', 'config') - fsHelper.fileExistsSync(configPath) - let contents = fs.readFileSync(configPath).toString() || '' - - // Filter - only includes lines that do not contain the config key - const upperConfigKey = configKey.toUpperCase() - const split = contents - .split('\n') - .filter(x => !x.toUpperCase().includes(upperConfigKey)) - contents = split.join('\n') - - // Rewrite the config file - fs.writeFileSync(configPath, contents) + core.warning(`Failed to remove '${configKey}' from the git config`) } } diff --git a/src/github-api-helper.ts b/src/github-api-helper.ts index b370d06..83a2b86 100644 --- a/src/github-api-helper.ts +++ b/src/github-api-helper.ts @@ -12,7 +12,7 @@ import {ReposGetArchiveLinkParams} from '@octokit/rest' const IS_WINDOWS = process.platform === 'win32' export async function downloadRepository( - accessToken: string, + authToken: string, owner: string, repo: string, ref: string, @@ -22,7 +22,7 @@ export async function downloadRepository( // Download the archive let archiveData = await retryHelper.execute(async () => { core.info('Downloading the archive') - return await downloadArchive(accessToken, owner, repo, ref, commit) + return await downloadArchive(authToken, owner, repo, ref, commit) }) // Write archive to disk @@ -68,13 +68,13 @@ export async function downloadRepository( } async function downloadArchive( - accessToken: string, + authToken: string, owner: string, repo: string, ref: string, commit: string ): Promise { - const octokit = new github.GitHub(accessToken) + const octokit = new github.GitHub(authToken) const params: ReposGetArchiveLinkParams = { owner: owner, repo: repo, diff --git a/src/input-helper.ts b/src/input-helper.ts index d7d8779..56b58f2 100644 --- a/src/input-helper.ts +++ b/src/input-helper.ts @@ -97,8 +97,12 @@ export function getInputs(): ISourceSettings { result.lfs = (core.getInput('lfs') || 'false').toUpperCase() === 'TRUE' core.debug(`lfs = ${result.lfs}`) - // Access token - result.accessToken = core.getInput('token') + // Auth token + result.authToken = core.getInput('token') + + // Persist credentials + result.persistCredentials = + (core.getInput('persist-credentials') || 'false').toUpperCase() === 'TRUE' return result }