diff --git a/action.test.ts b/action.test.ts index 80f3745..32dc950 100644 --- a/action.test.ts +++ b/action.test.ts @@ -2,7 +2,7 @@ import {describe, expect, test, beforeEach, afterAll} from '@jest/globals'; import {join as pathJoin} from 'node:path'; import {runAction} from './action'; import {randomBytes} from 'crypto'; -import fetch from 'node-fetch'; +import fetch, {RequestInit} from 'node-fetch'; import {mkdir} from 'node:fs/promises'; // Emulate https://github.com/actions/toolkit/blob/819157bf8/packages/core/src/core.ts#L128 @@ -22,6 +22,21 @@ const ensureEnv = (name: string): string => { }; const token = ensureEnv('HUMANITEC_TOKEN'); + + +const humanitecReq = (path: string, options: RequestInit) => { + options = { + headers: { + 'Authorization': `Bearer ${token}`, + 'Content-Type': 'application/json', + 'User-Agent': 'gh-action-build-push-to-humanitec/latest', + }, + ...options, + }; + + return fetch(`https://api.humanitec.io/${path}`, options); +}; + const orgId = ensureEnv('HUMANITEC_ORG'); const tenMinInMs = 10 * 60 * 1000; @@ -32,15 +47,7 @@ describe('action', () => { afterAll(async () => { - const res = await fetch( - `https://api.humanitec.io/orgs/${orgId}/artefacts?type=container`, { - method: 'GET', - headers: { - 'Authorization': `Bearer ${token}`, - 'Content-Type': 'application/json', - 'User-Agent': 'gh-action-build-push-to-humanitec/latest', - }, - }); + const res = await humanitecReq(`orgs/${orgId}/artefacts?type=container`, {method: 'GET'}); expect(res.status).toBe(200); @@ -55,21 +62,15 @@ describe('action', () => { continue; } - const res = await fetch( - `https://api.humanitec.io/orgs/${orgId}/artefacts/${artefact.id}`, { - method: 'DELETE', - headers: { - 'Authorization': `Bearer ${token}`, - 'Content-Type': 'application/json', - 'User-Agent': 'gh-action-build-push-to-humanitec/latest', - }, - }); - expect(res.status).toBe(204); + const res = await humanitecReq(`orgs/${orgId}/artefacts/${artefact.id}`, {method: 'DELETE'}); + + // Multiple tests might delete artifacts + expect([204, 404]).toContain(res.status); } }); beforeEach(async () => { - await mkdir(pathJoin(fixtures, '.git')); + await mkdir(pathJoin(fixtures, '.git'), {recursive: true}); setInput('humanitec-token', token); setInput('organization', orgId); @@ -87,23 +88,47 @@ describe('action', () => { await runAction(); expect(process.exitCode).toBeFalsy; - const res = await fetch( - `https://api.humanitec.io/orgs/${orgId}/artefact-versions`, { - method: 'GET', - headers: { - 'Authorization': `Bearer ${token}`, - 'Content-Type': 'application/json', - 'User-Agent': 'gh-action-build-push-to-humanitec/latest', - }, - }); + const res = await humanitecReq(`orgs/${orgId}/artefact-versions`, {method: 'GET'}); + expect(res.status).toBe(200); + + const body = await res.json(); + + expect(body).toEqual( + expect.arrayContaining( + [ + expect.objectContaining({ + commit: commit, + name: `registry.humanitec.io/${orgId}/${repo}`, + }), + ], + ), + ); + }); + + test('with slashed docker build args', async () => { + setInput('additional-docker-arguments', ` + --build-arg version=123 \\ + --build-arg build_time=123 \\ + --build-arg gitsha=132 + + `); + + await runAction(); + expect(process.exitCode).toBeFalsy; + const res = await humanitecReq(`orgs/${orgId}/artefact-versions`, {method: 'GET'}); expect(res.status).toBe(200); const body = await res.json(); expect(body).toEqual( expect.arrayContaining( - [expect.objectContaining({commit: commit})], + [ + expect.objectContaining({ + commit: commit, + name: `registry.humanitec.io/${orgId}/${repo}`, + }), + ], ), ); }); diff --git a/dist/index.js b/dist/index.js index 3c2b62a..89a4edd 100644 --- a/dist/index.js +++ b/dist/index.js @@ -1,7 +1,7 @@ /******/ (() => { // webpackBootstrap /******/ var __webpack_modules__ = ({ -/***/ 5241: +/***/ 7351: /***/ (function(__unused_webpack_module, exports, __nccwpck_require__) { "use strict"; @@ -135,7 +135,7 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge }; Object.defineProperty(exports, "__esModule", ({ value: true })); exports.getIDToken = exports.getState = exports.saveState = exports.group = exports.endGroup = exports.startGroup = exports.info = exports.notice = exports.warning = exports.error = exports.debug = exports.isDebug = exports.setFailed = exports.setCommandEcho = exports.setOutput = exports.getBooleanInput = exports.getMultilineInput = exports.getInput = exports.addPath = exports.setSecret = exports.exportVariable = exports.ExitCode = void 0; -const command_1 = __nccwpck_require__(5241); +const command_1 = __nccwpck_require__(7351); const file_command_1 = __nccwpck_require__(717); const utils_1 = __nccwpck_require__(5278); const os = __importStar(__nccwpck_require__(2037)); @@ -1042,7 +1042,7 @@ const os = __nccwpck_require__(2037); const events = __nccwpck_require__(2361); const child = __nccwpck_require__(2081); const path = __nccwpck_require__(1017); -const io = __nccwpck_require__(7351); +const io = __nccwpck_require__(7436); const ioUtil = __nccwpck_require__(1962); /* eslint-disable @typescript-eslint/unbound-method */ const IS_WINDOWS = process.platform === 'win32'; @@ -2587,7 +2587,7 @@ function isUnixExecutable(stats) { /***/ }), -/***/ 7351: +/***/ 7436: /***/ (function(__unused_webpack_module, exports, __nccwpck_require__) { "use strict"; @@ -4587,6 +4587,58 @@ exports.Response = Response; exports.FetchError = FetchError; +/***/ }), + +/***/ 9453: +/***/ ((__unused_webpack_module, exports) => { + +"use strict"; + +exports.__esModule = true; +function parseArgsStringToArgv(value, env, file) { + // ([^\s'"]([^\s'"]*(['"])([^\3]*?)\3)+[^\s'"]*) Matches nested quotes until the first space outside of quotes + // [^\s'"]+ or Match if not a space ' or " + // (['"])([^\5]*?)\5 or Match "quoted text" without quotes + // `\3` and `\5` are a backreference to the quote style (' or ") captured + var myRegexp = /([^\s'"]([^\s'"]*(['"])([^\3]*?)\3)+[^\s'"]*)|[^\s'"]+|(['"])([^\5]*?)\5/gi; + var myString = value; + var myArray = []; + if (env) { + myArray.push(env); + } + if (file) { + myArray.push(file); + } + var match; + do { + // Each call to exec returns the next regex match as an array + match = myRegexp.exec(myString); + if (match !== null) { + // Index 1 in the array is the captured group if it exists + // Index 0 is the matched text, which we use if no captured group exists + myArray.push(firstString(match[1], match[6], match[0])); + } + } while (match !== null); + return myArray; +} +exports["default"] = parseArgsStringToArgv; +exports.parseArgsStringToArgv = parseArgsStringToArgv; +// Accepts any number of arguments, and returns the first one that is a string +// (even an empty string) +function firstString() { + var args = []; + for (var _i = 0; _i < arguments.length; _i++) { + args[_i] = arguments[_i]; + } + for (var i = 0; i < args.length; i++) { + var arg = args[i]; + if (typeof arg === "string") { + return arg; + } + } +} + + /***/ }), /***/ 4256: @@ -7677,89 +7729,7 @@ module.exports.implForWrapper = function (wrapper) { /***/ }), -/***/ 8355: -/***/ ((__unused_webpack_module, exports) => { - -"use strict"; - -Object.defineProperty(exports, "__esModule", ({ value: true })); -exports.args = void 0; -const args = (str) => { - const args = []; - str = str.trim(); - let currentArg = ''; - for (let i = 0; i < str.length; i++) { - switch (str[i]) { - case '\'': - const endQuoteIndex = str.indexOf('\'', i + 1); - if (endQuoteIndex < 0) { - throw 'single quote not closed'; - } - currentArg = currentArg + str.substring(i + 1, endQuoteIndex); - i = endQuoteIndex; - break; - case '"': - // Double quotes can contain escaped characters - for (i++; i < str.length && str[i] !== '"'; i++) { - if (str[i] === '\\' && (i + 1) < str.length) { - i++; - switch (str[i]) { - case 'n': - currentArg += '\n'; - break; - case 'r': - currentArg += '\r'; - break; - case 't': - currentArg += '\t'; - break; - default: - currentArg += str[i]; - } - } - else { - currentArg += str[i]; - } - } - if (i >= str.length) { - throw 'double quote not closed'; - } - break; - case ' ': - case '\t': - args.push(currentArg); - currentArg = ''; - while (i < str.length && (str[i] === ' ' || str[i] === '\t')) { - i++; - } - // We will have advanced to the next non-whitespace - i--; - break; - case '\\': - i++; - if (i < str.length) { - currentArg = currentArg + str[i]; - } - else { - throw 'uncompleted escape character'; - } - break; - default: - currentArg = currentArg + str[i]; - break; - } - } - if (currentArg != '') { - args.push(currentArg); - } - return args; -}; -exports.args = args; - - -/***/ }), - -/***/ 1723: +/***/ 633: /***/ (function(__unused_webpack_module, exports, __nccwpck_require__) { "use strict"; @@ -7787,11 +7757,117 @@ var __importStar = (this && this.__importStar) || function (mod) { __setModuleDefault(result, mod); return result; }; +Object.defineProperty(exports, "__esModule", ({ value: true })); +exports.runAction = void 0; +const docker = __importStar(__nccwpck_require__(1723)); +const humanitec_1 = __nccwpck_require__(9362); +const node_fs_1 = __nccwpck_require__(7561); +const core = __importStar(__nccwpck_require__(2186)); +/** + * Performs the GitHub action. + */ +async function runAction() { + // Get GitHub Action inputs + const token = core.getInput('humanitec-token', { required: true }); + const orgId = core.getInput('organization', { required: true }); + const imageName = core.getInput('image-name') || (process.env.GITHUB_REPOSITORY || '').replace(/.*\//, ''); + const context = core.getInput('context') || core.getInput('dockerfile') || '.'; + const file = core.getInput('file') || ''; + const registryHost = core.getInput('humanitec-registry') || 'registry.humanitec.io'; + const apiHost = core.getInput('humanitec-api') || 'api.humanitec.io'; + const tag = core.getInput('tag') || ''; + const commit = process.env.GITHUB_SHA || ''; + const autoTag = /^\s*(true|1)\s*$/i.test(core.getInput('auto-tag')); + const additionalDockerArguments = core.getInput('additional-docker-arguments') || ''; + const ref = process.env.GITHUB_REF || ''; + if (!(0, node_fs_1.existsSync)(`${process.env.GITHUB_WORKSPACE}/.git`)) { + core.error('It does not look like anything was checked out.'); + core.error('Did you run a checkout step before this step? ' + + 'http:/docs.humanitec.com/connecting-your-ci#github-actions'); + core.setFailed('No .git directory found in workspace.'); + return; + } + if (file != '' && !(0, node_fs_1.existsSync)(file)) { + core.error(`Cannot find file ${file}`); + core.setFailed('Cannot find file.'); + return; + } + if (!(0, node_fs_1.existsSync)(context)) { + core.error(`Context path does not exist: ${context}`); + core.setFailed('Context path does not exist.'); + return; + } + const humanitec = (0, humanitec_1.humanitecFactory)(token, orgId, apiHost); + let registryCreds; + try { + registryCreds = await humanitec.getRegistryCredentials(); + } + catch (error) { + core.error('Unable to fetch repository credentials.'); + core.error('Did you add the token to your Github Secrets? ' + + 'http:/docs.humanitec.com/connecting-your-ci#github-actions'); + core.setFailed('Unable to access Humanitec.'); + return; + } + if (!docker.login(registryCreds.username, registryCreds.password, registryHost)) { + core.setFailed('Unable to connect to the humanitec registry.'); + return; + } + process.chdir((process.env.GITHUB_WORKSPACE || '')); + let version = ''; + if (autoTag && ref.includes('/tags/')) { + version = ref.replace(/.*\/tags\//, ''); + } + else if (tag) { + version = tag; + } + else { + version = commit; + } + const localTag = `${orgId}/${imageName}:${version}`; + const imageId = await docker.build(localTag, file, additionalDockerArguments, context); + if (!imageId) { + core.setFailed('Unable build image from Dockerfile.'); + return; + } + const remoteTag = `${registryHost}/${localTag}`; + if (!docker.push(imageId, remoteTag)) { + core.setFailed('Unable to push image to registry'); + return; + } + const payload = { + name: `${registryHost}/${orgId}/${imageName}`, + type: 'container', + version, + ref, + commit, + }; + try { + await humanitec.addNewVersion(payload); + } + catch (error) { + core.error('Unable to notify Humanitec about build.'); + core.error('Did you add the token to your Github Secrets? ' + + 'http:/docs.humanitec.com/connecting-your-ci#github-actions'); + core.setFailed('Unable to access Humanitec.'); + return; + } +} +exports.runAction = runAction; + + +/***/ }), + +/***/ 1723: +/***/ ((__unused_webpack_module, exports, __nccwpck_require__) => { + +"use strict"; + Object.defineProperty(exports, "__esModule", ({ value: true })); exports.push = exports.build = exports.login = void 0; const node_child_process_1 = __nccwpck_require__(7718); const exec_1 = __nccwpck_require__(1514); -const chunk = __importStar(__nccwpck_require__(8355)); +const string_argv_1 = __nccwpck_require__(9453); /** * Authenticates with a remote docker registry. * @param {string} username - The username to log in with. @@ -7826,10 +7902,8 @@ const build = async function (tag, file, additionalDockerArguments, contextPath) args.push('-f', file); } if (additionalDockerArguments != '') { - const argArray = chunk.args(additionalDockerArguments); - for (let i = 0; i < argArray.length; i++) { - args.push(argArray[i]); - } + const argArray = (0, string_argv_1.parseArgsStringToArgv)(additionalDockerArguments).filter((a) => a !== '\\'); + args.push(...argArray); } args.push(contextPath); await (0, exec_1.exec)('docker', args); @@ -7959,101 +8033,9 @@ var __importStar = (this && this.__importStar) || function (mod) { return result; }; Object.defineProperty(exports, "__esModule", ({ value: true })); -const docker = __importStar(__nccwpck_require__(1723)); -const humanitec_1 = __nccwpck_require__(9362); -const node_fs_1 = __nccwpck_require__(7561); const core = __importStar(__nccwpck_require__(2186)); -/** - * Performs the GitHub action. - */ -async function runAction() { - // Get GitHub Action inputs - const token = core.getInput('humanitec-token', { required: true }); - const orgId = core.getInput('organization', { required: true }); - const imageName = core.getInput('image-name') || (process.env.GITHUB_REPOSITORY || '').replace(/.*\//, ''); - const context = core.getInput('context') || core.getInput('dockerfile') || '.'; - const file = core.getInput('file') || ''; - const registryHost = core.getInput('humanitec-registry') || 'registry.humanitec.io'; - const apiHost = core.getInput('humanitec-api') || 'api.humanitec.io'; - const tag = core.getInput('tag') || ''; - const commit = process.env.GITHUB_SHA || ''; - const autoTag = /^\s*(true|1)\s*$/i.test(core.getInput('auto-tag')); - const additionalDockerArguments = core.getInput('additional-docker-arguments') || ''; - const ref = process.env.GITHUB_REF || ''; - if (!(0, node_fs_1.existsSync)(`${process.env.GITHUB_WORKSPACE}/.git`)) { - core.error('It does not look like anything was checked out.'); - core.error('Did you run a checkout step before this step? ' + - 'http:/docs.humanitec.com/connecting-your-ci#github-actions'); - core.setFailed('No .git directory found in workspace.'); - return; - } - if (file != '' && !(0, node_fs_1.existsSync)(file)) { - core.error(`Cannot find file ${file}`); - core.setFailed('Cannot find file.'); - return; - } - if (!(0, node_fs_1.existsSync)(context)) { - core.error(`Context path does not exist: ${context}`); - core.setFailed('Context path does not exist.'); - return; - } - const humanitec = (0, humanitec_1.humanitecFactory)(token, orgId, apiHost); - let registryCreds; - try { - registryCreds = await humanitec.getRegistryCredentials(); - } - catch (error) { - core.error('Unable to fetch repository credentials.'); - core.error('Did you add the token to your Github Secrets? ' + - 'http:/docs.humanitec.com/connecting-your-ci#github-actions'); - core.setFailed('Unable to access Humanitec.'); - return; - } - if (!docker.login(registryCreds.username, registryCreds.password, registryHost)) { - core.setFailed('Unable to connect to the humanitec registry.'); - return; - } - process.chdir((process.env.GITHUB_WORKSPACE || '')); - let version = ''; - if (autoTag && ref.includes('/tags/')) { - version = ref.replace(/.*\/tags\//, ''); - } - else if (tag) { - version = tag; - } - else { - version = commit; - } - const localTag = `${orgId}/${imageName}:${version}`; - const imageId = await docker.build(localTag, file, additionalDockerArguments, context); - if (!imageId) { - core.setFailed('Unable build image from Dockerfile.'); - return; - } - const remoteTag = `${registryHost}/${localTag}`; - if (!docker.push(imageId, remoteTag)) { - core.setFailed('Unable to push image to registry'); - return; - } - const payload = { - name: `${registryHost}/${orgId}/${imageName}`, - type: 'container', - version, - ref, - commit, - }; - try { - await humanitec.addNewVersion(payload); - } - catch (error) { - core.error('Unable to notify Humanitec about build.'); - core.error('Did you add the token to your Github Secrets? ' + - 'http:/docs.humanitec.com/connecting-your-ci#github-actions'); - core.setFailed('Unable to access Humanitec.'); - return; - } -} -runAction().catch((e) => { +const action_1 = __nccwpck_require__(633); +(0, action_1.runAction)().catch((e) => { core.error('Action failed'); core.error(`${e.name} ${e.message}`); core.setFailed(`${e.name} ${e.message}`); diff --git a/docker.ts b/docker.ts index 25d9cd9..08e890b 100644 --- a/docker.ts +++ b/docker.ts @@ -1,6 +1,6 @@ import {execSync} from 'node:child_process'; import {exec as actionsExec} from '@actions/exec'; -import * as chunk from './chunk'; +import {parseArgsStringToArgv} from 'string-argv'; /** * Authenticates with a remote docker registry. @@ -38,10 +38,8 @@ export const build = async function( args.push('-f', file); } if (additionalDockerArguments != '') { - const argArray = chunk.args(additionalDockerArguments); - for (let i=0; i < argArray.length; i++) { - args.push(argArray[i]); - } + const argArray = parseArgsStringToArgv(additionalDockerArguments).filter((a) => a !== '\\'); + args.push(...argArray); } args.push(contextPath); await actionsExec('docker', args); diff --git a/package-lock.json b/package-lock.json index dd59fbf..3c526bd 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11,7 +11,8 @@ "dependencies": { "@actions/core": "^1.9.1", "@actions/exec": "^1.0.3", - "node-fetch": "^2.6.1" + "node-fetch": "^2.6.1", + "string-argv": "^0.3.1" }, "devDependencies": { "@jest/globals": "^29.3.1", @@ -4596,6 +4597,14 @@ "node": ">=8" } }, + "node_modules/string-argv": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/string-argv/-/string-argv-0.3.1.tgz", + "integrity": "sha512-a1uQGz7IyVy9YwhqjZIZu1c8JO8dNIe20xBmSS6qu9kv++k3JGzCVmprbNN5Kn+BgzD5E7YYwg1CcjuJMRNsvg==", + "engines": { + "node": ">=0.6.19" + } + }, "node_modules/string-length": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/string-length/-/string-length-4.0.2.tgz", @@ -8466,6 +8475,11 @@ } } }, + "string-argv": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/string-argv/-/string-argv-0.3.1.tgz", + "integrity": "sha512-a1uQGz7IyVy9YwhqjZIZu1c8JO8dNIe20xBmSS6qu9kv++k3JGzCVmprbNN5Kn+BgzD5E7YYwg1CcjuJMRNsvg==" + }, "string-length": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/string-length/-/string-length-4.0.2.tgz", diff --git a/package.json b/package.json index 85dc22b..3a4cd58 100644 --- a/package.json +++ b/package.json @@ -28,6 +28,7 @@ "dependencies": { "@actions/core": "^1.9.1", "@actions/exec": "^1.0.3", - "node-fetch": "^2.6.1" + "node-fetch": "^2.6.1", + "string-argv": "^0.3.1" } }