diff --git a/.github/dependabot.yml b/.github/dependabot.yml index d61ff86..13cb229 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -6,9 +6,9 @@ updates: schedule: interval: daily labels: - - "dependencies" - - "e2e" - - "github_actions" + - 'dependencies' + - 'e2e' + - 'github_actions' - package-ecosystem: npm directory: / @@ -19,8 +19,8 @@ updates: groups: typescript-eslint: patterns: - - "@typescript-eslint/*" + - '@typescript-eslint/*' labels: - - "dependencies" - - "e2e" - - "javascript" + - 'dependencies' + - 'e2e' + - 'javascript' diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md index f35e1c6..2d41391 100644 --- a/.github/pull_request_template.md +++ b/.github/pull_request_template.md @@ -2,4 +2,4 @@ \ No newline at end of file +--> diff --git a/.github/workflows/e2e.yml b/.github/workflows/e2e.yml index ca64ca8..2be09a6 100644 --- a/.github/workflows/e2e.yml +++ b/.github/workflows/e2e.yml @@ -36,9 +36,9 @@ jobs: - name: Install Stackrox env: - MAIN_IMAGE_TAG: "latest" - SENSOR_HELM_DEPLOY: "true" - ROX_SCANNER_V4: "false" + MAIN_IMAGE_TAG: 'latest' + SENSOR_HELM_DEPLOY: 'true' + ROX_SCANNER_V4: 'false' run: | stackrox/deploy/k8s/deploy-local.sh ROX_PASSWORD=$(cat stackrox/deploy/k8s/central-deploy/password) diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index 8ff86aa..1a6b961 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -20,6 +20,10 @@ jobs: run: | npm install + - name: Format + run: | + npm run format-check + - name: Lint run: | - npm run lint \ No newline at end of file + npm run lint diff --git a/.github/workflows/package.yml b/.github/workflows/package.yml index 2a023b2..fb5c51d 100644 --- a/.github/workflows/package.yml +++ b/.github/workflows/package.yml @@ -17,7 +17,7 @@ jobs: contents: write env: # Output logs to file in case we need to inspect errors. - GITSIGN_LOG: "/tmp/gitsign.log" + GITSIGN_LOG: '/tmp/gitsign.log' steps: - name: Checkout uses: actions/checkout@v7 diff --git a/README.md b/README.md index b0ae70c..101ffbd 100644 --- a/README.md +++ b/README.md @@ -12,20 +12,20 @@ other actions to use. This is as simple as adding the following step to your workflow: ```yaml - - name: Central Login - uses: stackrox/central-login@v1 - with: - audience: central.stackrox.io - endpoint: https://:443 +- name: Central Login + uses: stackrox/central-login@v1 + with: + audience: central.stackrox.io + endpoint: https://:443 ``` ## Parameters -| Parameter name | Required? | Description | -|-------------------|----------------|----------------------------------------------------------------------| +| Parameter name | Required? | Description | +| ----------------- | -------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `audience` | (optional) | Custom audience for the GitHub OIDC identity token request. Must match the expected audience configured in the Central machine-to-machine authentication integration. When left empty, the default GitHub audience is used. | -| `endpoint` | **(required)** | API endpoint of the ACS Central instance. | -| `skip-tls-verify` | (optional) | Skip TLS certificat verification for ACS Central's API endpoint. | +| `endpoint` | **(required)** | API endpoint of the ACS Central instance. | +| `skip-tls-verify` | (optional) | Skip TLS certificat verification for ACS Central's API endpoint. | ## Overview @@ -52,6 +52,7 @@ originating from GitHub Action workflow runs. For more information on how to configure this, follow the [RHACS documentation](https://docs.openshift.com/acs/4.4/operating/manage-user-access/configure-short-lived-access.html). Below is a sample configuration via API you can use: + ```bash curl \ https:///v1/auth/m2m \ @@ -87,11 +88,11 @@ After the ACS Central instance has been configured to allow exchanging tokens fr add the following step to your workflow: ```yaml - - name: Central Login - uses: stackrox/central-login@v1 - with: - audience: central.stackrox.io - endpoint: https://:443 +- name: Central Login + uses: stackrox/central-login@v1 + with: + audience: central.stackrox.io + endpoint: https://:443 ``` After the central login step has succeeded, the following environment variables are configured for other steps to use: @@ -102,21 +103,21 @@ After the central login step has succeeded, the following environment variables For verifying everything works correctly, the example below can be used: ```yaml - - name: Login to Central - uses: stackrox/central-login@v1 - with: - audience: central.stackrox.io - endpoint: https://:443 - - - name: Install roxctl from Central - uses: stackrox/roxctl-installer-action@v1 - with: - central-endpoint: https://${{ env.ROX_ENDPOINT }} - central-token: ${{ env.ROX_API_TOKEN }} - - - name: roxctl central whoami - run: | - roxctl central whoami +- name: Login to Central + uses: stackrox/central-login@v1 + with: + audience: central.stackrox.io + endpoint: https://:443 + +- name: Install roxctl from Central + uses: stackrox/roxctl-installer-action@v1 + with: + central-endpoint: https://${{ env.ROX_ENDPOINT }} + central-token: ${{ env.ROX_API_TOKEN }} + +- name: roxctl central whoami + run: | + roxctl central whoami ``` This will output the specifics about the access token (i.e. it's associated permissions and roles) as well as the diff --git a/action.yml b/action.yml index f2ac4c1..d79a90e 100644 --- a/action.yml +++ b/action.yml @@ -8,7 +8,7 @@ inputs: required: true description: 'Central API endpoint in the format of https://:' skip-tls-verify: - default: "false" + default: 'false' required: false description: "Skip TLS certificate verification for Central's API Endpoint" runs: diff --git a/eslint.config.mjs b/eslint.config.mjs index e8b8916..4984ee2 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -1,88 +1,90 @@ -import jest from "eslint-plugin-jest"; -import stylistic from "@stylistic/eslint-plugin"; -import typescriptEslint from "@typescript-eslint/eslint-plugin"; -import globals from "globals"; -import tsParser from "@typescript-eslint/parser"; +import jest from 'eslint-plugin-jest' +import stylistic from '@stylistic/eslint-plugin' +import typescriptEslint from '@typescript-eslint/eslint-plugin' +import globals from 'globals' +import tsParser from '@typescript-eslint/parser' export default [ - { - ignores: [ - "dist/", - "lib/", - "node_modules/", - ], + { + ignores: ['dist/', 'lib/', 'node_modules/'] + }, + { + plugins: { + jest, + '@stylistic': stylistic, + '@typescript-eslint': typescriptEslint }, - { - plugins: { - jest, - "@stylistic": stylistic, - "@typescript-eslint": typescriptEslint, - }, - languageOptions: { - globals: { - ...globals.node, - ...jest.environments.globals.globals, - }, + languageOptions: { + globals: { + ...globals.node, + ...jest.environments.globals.globals + }, - parser: tsParser, - ecmaVersion: 9, - sourceType: "module", + parser: tsParser, + ecmaVersion: 9, + sourceType: 'module', - parserOptions: { - project: "./tsconfig.json", - }, - }, + parserOptions: { + project: './tsconfig.json' + } + }, - files: [ "**/*.ts" ], + files: ['**/*.ts'], - rules: { - "i18n-text/no-en": "off", - "eslint-comments/no-use": "off", - "import/no-namespace": "off", - "no-unused-vars": "off", - "@typescript-eslint/no-unused-vars": "error", + rules: { + 'i18n-text/no-en': 'off', + 'eslint-comments/no-use': 'off', + 'import/no-namespace': 'off', + 'no-unused-vars': 'off', + '@typescript-eslint/no-unused-vars': 'error', - "@typescript-eslint/explicit-member-accessibility": ["error", { - accessibility: "no-public", - }], + '@typescript-eslint/explicit-member-accessibility': [ + 'error', + { + accessibility: 'no-public' + } + ], - "@typescript-eslint/no-require-imports": "error", - "@typescript-eslint/array-type": "error", - "@typescript-eslint/await-thenable": "error", - "@typescript-eslint/ban-ts-comment": "error", - camelcase: "off", - "@typescript-eslint/consistent-type-assertions": "error", + '@typescript-eslint/no-require-imports': 'error', + '@typescript-eslint/array-type': 'error', + '@typescript-eslint/await-thenable': 'error', + '@typescript-eslint/ban-ts-comment': 'error', + camelcase: 'off', + '@typescript-eslint/consistent-type-assertions': 'error', - "@typescript-eslint/explicit-function-return-type": ["error", { - allowExpressions: true, - }], + '@typescript-eslint/explicit-function-return-type': [ + 'error', + { + allowExpressions: true + } + ], - "@stylistic/function-call-spacing": ["error", "never"], - "@typescript-eslint/no-array-constructor": "error", - "@typescript-eslint/no-empty-interface": "error", - "@typescript-eslint/no-explicit-any": "error", - "@typescript-eslint/no-extraneous-class": "error", - "@typescript-eslint/no-for-in-array": "error", - "@typescript-eslint/no-inferrable-types": "error", - "@typescript-eslint/no-misused-new": "error", - "@typescript-eslint/no-namespace": "error", - "@typescript-eslint/no-non-null-assertion": "warn", - "@typescript-eslint/no-unnecessary-qualifier": "error", - "@typescript-eslint/no-unnecessary-type-assertion": "error", - "@typescript-eslint/no-useless-constructor": "error", - "@typescript-eslint/no-var-requires": "error", - "@typescript-eslint/prefer-for-of": "warn", - "@typescript-eslint/prefer-function-type": "warn", - "@typescript-eslint/prefer-includes": "error", - "@typescript-eslint/prefer-string-starts-ends-with": "error", - "@typescript-eslint/promise-function-async": "error", - "@typescript-eslint/require-array-sort-compare": "error", - "@typescript-eslint/restrict-plus-operands": "error", - semi: "off", - "@stylistic/semi": ["error", "never"], - "@stylistic/type-annotation-spacing": "error", - "@typescript-eslint/unbound-method": "error", - }, + '@stylistic/function-call-spacing': ['error', 'never'], + '@typescript-eslint/no-array-constructor': 'error', + '@typescript-eslint/no-empty-interface': 'error', + '@typescript-eslint/no-explicit-any': 'error', + '@typescript-eslint/no-extraneous-class': 'error', + '@typescript-eslint/no-for-in-array': 'error', + '@typescript-eslint/no-inferrable-types': 'error', + '@typescript-eslint/no-misused-new': 'error', + '@typescript-eslint/no-namespace': 'error', + '@typescript-eslint/no-non-null-assertion': 'warn', + '@typescript-eslint/no-unnecessary-qualifier': 'error', + '@typescript-eslint/no-unnecessary-type-assertion': 'error', + '@typescript-eslint/no-useless-constructor': 'error', + '@typescript-eslint/no-var-requires': 'error', + '@typescript-eslint/prefer-for-of': 'warn', + '@typescript-eslint/prefer-function-type': 'warn', + '@typescript-eslint/prefer-includes': 'error', + '@typescript-eslint/prefer-string-starts-ends-with': 'error', + '@typescript-eslint/promise-function-async': 'error', + '@typescript-eslint/require-array-sort-compare': 'error', + '@typescript-eslint/restrict-plus-operands': 'error', + semi: 'off', + '@stylistic/semi': ['error', 'never'], + '@stylistic/type-annotation-spacing': 'error', + '@typescript-eslint/unbound-method': 'error' } -]; + } +] diff --git a/package.json b/package.json index 95a8880..100f88d 100644 --- a/package.json +++ b/package.json @@ -5,8 +5,8 @@ "main": "src/main.ts", "scripts": { "build": "tsc", - "format": "prettier --write '**/*.ts'", - "format-check": "prettier --check '**/*.ts'", + "format": "prettier --write '**/*.ts' '**/*.yml'", + "format-check": "prettier --check '**/*.ts' '**/*.yml'", "lint": "eslint src/**/*.ts --fix", "package": "esbuild src/main.ts --bundle --platform=node --target=node20 --outfile=dist/index.js --sourcemap", "all": "npm run build && npm run format && npm run lint && npm run package" diff --git a/src/main.ts b/src/main.ts index 640a0a1..9123b8e 100644 --- a/src/main.ts +++ b/src/main.ts @@ -1,6 +1,6 @@ -import * as core from '@actions/core'; -import axios, {isAxiosError, AxiosError} from 'axios'; -import isRetryAllowed from 'is-retry-allowed'; +import * as core from '@actions/core' +import axios, {isAxiosError, AxiosError} from 'axios' +import isRetryAllowed from 'is-retry-allowed' import * as https from 'https' @@ -14,7 +14,11 @@ async function run(): Promise { try { core.info(`Attempting to obtain an access token for Central ${endpoint}...`) - const accessToken = await obtainAccessToken(audience, parsedURL, skipTLSVerify) + const accessToken = await obtainAccessToken( + audience, + parsedURL, + skipTLSVerify + ) core.info(`Successfully obtained an access token for central ${endpoint}!`) @@ -36,8 +40,9 @@ async function run(): Promise { core.info(`${error}`) if (isAxiosError(error)) { core.setFailed( - `Failed to exchange token: HTTP Status: ${error?.response - ?.status} Response: ${JSON.stringify(error?.response?.data)}` + `Failed to exchange token: HTTP Status: ${ + error?.response?.status + } Response: ${JSON.stringify(error?.response?.data)}` ) } else if (error instanceof Error) core.setFailed(error.message) } @@ -46,7 +51,7 @@ async function run(): Promise { async function obtainAccessToken( audience: string, endpoint: URL, - skipTLSVerify: boolean, + skipTLSVerify: boolean ): Promise { const agent = new https.Agent({rejectUnauthorized: !skipTLSVerify}) @@ -63,8 +68,8 @@ async function obtainAccessToken( endpoint, JSON.stringify(exchangeTokenRequest), 3, - 2000, - ); + 2000 + ) return response } @@ -76,56 +81,53 @@ async function postWithRetries( maxRetries: number = 3, baseDelay: number = 2000 ): Promise { - let lastError; + let lastError for (let attempt = 0; attempt <= maxRetries; attempt++) { try { if (attempt > 0) { core.info(`HTTP request retry attempt: ${attempt}`) - const delay = baseDelay * attempt; - await new Promise( (resolve) => setTimeout(resolve, delay) ); + const delay = baseDelay * attempt + await new Promise(resolve => setTimeout(resolve, delay)) } - const result = await axios.post( - endpoint.toString(), - payload, - {httpsAgent: agent, headers: {'User-Agent': 'central-login-GHA'}}, - ); + const result = await axios.post(endpoint.toString(), payload, { + httpsAgent: agent, + headers: {'User-Agent': 'central-login-GHA'} + }) core.info( - `Received status ${ - result.status - } from endpoint ${endpoint.toString()}` + `Received status ${result.status} from endpoint ${endpoint.toString()}` ) - return result.data['accessToken']; + return result.data['accessToken'] } catch (error) { - lastError = error; + lastError = error if (isAxiosError(error)) { if (isRetryableError(error) && attempt < maxRetries) { - continue; + continue } } - return Promise.reject(error); + return Promise.reject(error) } } - return Promise.reject(lastError); + return Promise.reject(lastError) } function isRetryableError(error: AxiosError): boolean { - core.warning(error); + core.warning(error) if (error.code === 'ECONNABORTED') { - return false; + return false } if (!error.response) { - return true; + return true } - const errorStatus = error.response.status; + const errorStatus = error.response.status if (errorStatus === 429) { - return true; + return true } if (errorStatus >= 500 && errorStatus <= 599) { - return true; + return true } - return isRetryAllowed(error); + return isRetryAllowed(error) } function getHostWithPort(url: URL): string { diff --git a/tsconfig.json b/tsconfig.json index 618c8a4..d47bf33 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,13 +1,13 @@ { "compilerOptions": { - "target": "es6", /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019' or 'ESNEXT'. */ - "module": "commonjs", /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', or 'ESNext'. */ - "outDir": "./lib", /* Redirect output structure to the directory. */ - "rootDir": "./src", /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */ - "strict": true, /* Enable all strict type-checking options. */ - "noImplicitAny": true, /* Raise error on expressions and declarations with an implied 'any' type. */ - "esModuleInterop": true, /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */ - "types": ["node"] /* Include Node.js type definitions. */ + "target": "es6" /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019' or 'ESNEXT'. */, + "module": "commonjs" /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', or 'ESNext'. */, + "outDir": "./lib" /* Redirect output structure to the directory. */, + "rootDir": "./src" /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */, + "strict": true /* Enable all strict type-checking options. */, + "noImplicitAny": true /* Raise error on expressions and declarations with an implied 'any' type. */, + "esModuleInterop": true /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */, + "types": ["node"] /* Include Node.js type definitions. */ }, "exclude": ["node_modules", "**/*.test.ts"] }