From e3aa72700fa9a10b84a69541184b72aa9f376225 Mon Sep 17 00:00:00 2001 From: Volodymyr Zotov Date: Fri, 12 Dec 2025 14:21:22 -0600 Subject: [PATCH 01/13] Make latest build --- dist/index.js | 487 ++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 487 insertions(+) diff --git a/dist/index.js b/dist/index.js index a4233d2..eb2fc10 100644 --- a/dist/index.js +++ b/dist/index.js @@ -5762,6 +5762,444 @@ function coerce (version, options) { } +/***/ }), + +/***/ 8889: +/***/ ((module, __unused_webpack_exports, __nccwpck_require__) => { + +const fs = __nccwpck_require__(9896) +const path = __nccwpck_require__(6928) +const os = __nccwpck_require__(857) +const crypto = __nccwpck_require__(6982) +const packageJson = __nccwpck_require__(56) + +const version = packageJson.version + +// Array of tips to display randomly +const TIPS = [ + '🔐 encrypt with Dotenvx: https://dotenvx.com', + '🔐 prevent committing .env to code: https://dotenvx.com/precommit', + '🔐 prevent building .env in docker: https://dotenvx.com/prebuild', + '📡 observe env with Radar: https://dotenvx.com/radar', + '📡 auto-backup env with Radar: https://dotenvx.com/radar', + '📡 version env with Radar: https://dotenvx.com/radar', + '🛠️ run anywhere with `dotenvx run -- yourcommand`', + '⚙️ specify custom .env file path with { path: \'/custom/path/.env\' }', + '⚙️ enable debug logging with { debug: true }', + '⚙️ override existing env vars with { override: true }', + '⚙️ suppress all logs with { quiet: true }', + '⚙️ write to custom object with { processEnv: myObject }', + '⚙️ load multiple .env files with { path: [\'.env.local\', \'.env\'] }' +] + +// Get a random tip from the tips array +function _getRandomTip () { + return TIPS[Math.floor(Math.random() * TIPS.length)] +} + +function parseBoolean (value) { + if (typeof value === 'string') { + return !['false', '0', 'no', 'off', ''].includes(value.toLowerCase()) + } + return Boolean(value) +} + +function supportsAnsi () { + return process.stdout.isTTY // && process.env.TERM !== 'dumb' +} + +function dim (text) { + return supportsAnsi() ? `\x1b[2m${text}\x1b[0m` : text +} + +const LINE = /(?:^|^)\s*(?:export\s+)?([\w.-]+)(?:\s*=\s*?|:\s+?)(\s*'(?:\\'|[^'])*'|\s*"(?:\\"|[^"])*"|\s*`(?:\\`|[^`])*`|[^#\r\n]+)?\s*(?:#.*)?(?:$|$)/mg + +// Parse src into an Object +function parse (src) { + const obj = {} + + // Convert buffer to string + let lines = src.toString() + + // Convert line breaks to same format + lines = lines.replace(/\r\n?/mg, '\n') + + let match + while ((match = LINE.exec(lines)) != null) { + const key = match[1] + + // Default undefined or null to empty string + let value = (match[2] || '') + + // Remove whitespace + value = value.trim() + + // Check if double quoted + const maybeQuote = value[0] + + // Remove surrounding quotes + value = value.replace(/^(['"`])([\s\S]*)\1$/mg, '$2') + + // Expand newlines if double quoted + if (maybeQuote === '"') { + value = value.replace(/\\n/g, '\n') + value = value.replace(/\\r/g, '\r') + } + + // Add to object + obj[key] = value + } + + return obj +} + +function _parseVault (options) { + options = options || {} + + const vaultPath = _vaultPath(options) + options.path = vaultPath // parse .env.vault + const result = DotenvModule.configDotenv(options) + if (!result.parsed) { + const err = new Error(`MISSING_DATA: Cannot parse ${vaultPath} for an unknown reason`) + err.code = 'MISSING_DATA' + throw err + } + + // handle scenario for comma separated keys - for use with key rotation + // example: DOTENV_KEY="dotenv://:key_1234@dotenvx.com/vault/.env.vault?environment=prod,dotenv://:key_7890@dotenvx.com/vault/.env.vault?environment=prod" + const keys = _dotenvKey(options).split(',') + const length = keys.length + + let decrypted + for (let i = 0; i < length; i++) { + try { + // Get full key + const key = keys[i].trim() + + // Get instructions for decrypt + const attrs = _instructions(result, key) + + // Decrypt + decrypted = DotenvModule.decrypt(attrs.ciphertext, attrs.key) + + break + } catch (error) { + // last key + if (i + 1 >= length) { + throw error + } + // try next key + } + } + + // Parse decrypted .env string + return DotenvModule.parse(decrypted) +} + +function _warn (message) { + console.error(`[dotenv@${version}][WARN] ${message}`) +} + +function _debug (message) { + console.log(`[dotenv@${version}][DEBUG] ${message}`) +} + +function _log (message) { + console.log(`[dotenv@${version}] ${message}`) +} + +function _dotenvKey (options) { + // prioritize developer directly setting options.DOTENV_KEY + if (options && options.DOTENV_KEY && options.DOTENV_KEY.length > 0) { + return options.DOTENV_KEY + } + + // secondary infra already contains a DOTENV_KEY environment variable + if (process.env.DOTENV_KEY && process.env.DOTENV_KEY.length > 0) { + return process.env.DOTENV_KEY + } + + // fallback to empty string + return '' +} + +function _instructions (result, dotenvKey) { + // Parse DOTENV_KEY. Format is a URI + let uri + try { + uri = new URL(dotenvKey) + } catch (error) { + if (error.code === 'ERR_INVALID_URL') { + const err = new Error('INVALID_DOTENV_KEY: Wrong format. Must be in valid uri format like dotenv://:key_1234@dotenvx.com/vault/.env.vault?environment=development') + err.code = 'INVALID_DOTENV_KEY' + throw err + } + + throw error + } + + // Get decrypt key + const key = uri.password + if (!key) { + const err = new Error('INVALID_DOTENV_KEY: Missing key part') + err.code = 'INVALID_DOTENV_KEY' + throw err + } + + // Get environment + const environment = uri.searchParams.get('environment') + if (!environment) { + const err = new Error('INVALID_DOTENV_KEY: Missing environment part') + err.code = 'INVALID_DOTENV_KEY' + throw err + } + + // Get ciphertext payload + const environmentKey = `DOTENV_VAULT_${environment.toUpperCase()}` + const ciphertext = result.parsed[environmentKey] // DOTENV_VAULT_PRODUCTION + if (!ciphertext) { + const err = new Error(`NOT_FOUND_DOTENV_ENVIRONMENT: Cannot locate environment ${environmentKey} in your .env.vault file.`) + err.code = 'NOT_FOUND_DOTENV_ENVIRONMENT' + throw err + } + + return { ciphertext, key } +} + +function _vaultPath (options) { + let possibleVaultPath = null + + if (options && options.path && options.path.length > 0) { + if (Array.isArray(options.path)) { + for (const filepath of options.path) { + if (fs.existsSync(filepath)) { + possibleVaultPath = filepath.endsWith('.vault') ? filepath : `${filepath}.vault` + } + } + } else { + possibleVaultPath = options.path.endsWith('.vault') ? options.path : `${options.path}.vault` + } + } else { + possibleVaultPath = path.resolve(process.cwd(), '.env.vault') + } + + if (fs.existsSync(possibleVaultPath)) { + return possibleVaultPath + } + + return null +} + +function _resolveHome (envPath) { + return envPath[0] === '~' ? path.join(os.homedir(), envPath.slice(1)) : envPath +} + +function _configVault (options) { + const debug = parseBoolean(process.env.DOTENV_CONFIG_DEBUG || (options && options.debug)) + const quiet = parseBoolean(process.env.DOTENV_CONFIG_QUIET || (options && options.quiet)) + + if (debug || !quiet) { + _log('Loading env from encrypted .env.vault') + } + + const parsed = DotenvModule._parseVault(options) + + let processEnv = process.env + if (options && options.processEnv != null) { + processEnv = options.processEnv + } + + DotenvModule.populate(processEnv, parsed, options) + + return { parsed } +} + +function configDotenv (options) { + const dotenvPath = path.resolve(process.cwd(), '.env') + let encoding = 'utf8' + let processEnv = process.env + if (options && options.processEnv != null) { + processEnv = options.processEnv + } + let debug = parseBoolean(processEnv.DOTENV_CONFIG_DEBUG || (options && options.debug)) + let quiet = parseBoolean(processEnv.DOTENV_CONFIG_QUIET || (options && options.quiet)) + + if (options && options.encoding) { + encoding = options.encoding + } else { + if (debug) { + _debug('No encoding is specified. UTF-8 is used by default') + } + } + + let optionPaths = [dotenvPath] // default, look for .env + if (options && options.path) { + if (!Array.isArray(options.path)) { + optionPaths = [_resolveHome(options.path)] + } else { + optionPaths = [] // reset default + for (const filepath of options.path) { + optionPaths.push(_resolveHome(filepath)) + } + } + } + + // Build the parsed data in a temporary object (because we need to return it). Once we have the final + // parsed data, we will combine it with process.env (or options.processEnv if provided). + let lastError + const parsedAll = {} + for (const path of optionPaths) { + try { + // Specifying an encoding returns a string instead of a buffer + const parsed = DotenvModule.parse(fs.readFileSync(path, { encoding })) + + DotenvModule.populate(parsedAll, parsed, options) + } catch (e) { + if (debug) { + _debug(`Failed to load ${path} ${e.message}`) + } + lastError = e + } + } + + const populated = DotenvModule.populate(processEnv, parsedAll, options) + + // handle user settings DOTENV_CONFIG_ options inside .env file(s) + debug = parseBoolean(processEnv.DOTENV_CONFIG_DEBUG || debug) + quiet = parseBoolean(processEnv.DOTENV_CONFIG_QUIET || quiet) + + if (debug || !quiet) { + const keysCount = Object.keys(populated).length + const shortPaths = [] + for (const filePath of optionPaths) { + try { + const relative = path.relative(process.cwd(), filePath) + shortPaths.push(relative) + } catch (e) { + if (debug) { + _debug(`Failed to load ${filePath} ${e.message}`) + } + lastError = e + } + } + + _log(`injecting env (${keysCount}) from ${shortPaths.join(',')} ${dim(`-- tip: ${_getRandomTip()}`)}`) + } + + if (lastError) { + return { parsed: parsedAll, error: lastError } + } else { + return { parsed: parsedAll } + } +} + +// Populates process.env from .env file +function config (options) { + // fallback to original dotenv if DOTENV_KEY is not set + if (_dotenvKey(options).length === 0) { + return DotenvModule.configDotenv(options) + } + + const vaultPath = _vaultPath(options) + + // dotenvKey exists but .env.vault file does not exist + if (!vaultPath) { + _warn(`You set DOTENV_KEY but you are missing a .env.vault file at ${vaultPath}. Did you forget to build it?`) + + return DotenvModule.configDotenv(options) + } + + return DotenvModule._configVault(options) +} + +function decrypt (encrypted, keyStr) { + const key = Buffer.from(keyStr.slice(-64), 'hex') + let ciphertext = Buffer.from(encrypted, 'base64') + + const nonce = ciphertext.subarray(0, 12) + const authTag = ciphertext.subarray(-16) + ciphertext = ciphertext.subarray(12, -16) + + try { + const aesgcm = crypto.createDecipheriv('aes-256-gcm', key, nonce) + aesgcm.setAuthTag(authTag) + return `${aesgcm.update(ciphertext)}${aesgcm.final()}` + } catch (error) { + const isRange = error instanceof RangeError + const invalidKeyLength = error.message === 'Invalid key length' + const decryptionFailed = error.message === 'Unsupported state or unable to authenticate data' + + if (isRange || invalidKeyLength) { + const err = new Error('INVALID_DOTENV_KEY: It must be 64 characters long (or more)') + err.code = 'INVALID_DOTENV_KEY' + throw err + } else if (decryptionFailed) { + const err = new Error('DECRYPTION_FAILED: Please check your DOTENV_KEY') + err.code = 'DECRYPTION_FAILED' + throw err + } else { + throw error + } + } +} + +// Populate process.env with parsed values +function populate (processEnv, parsed, options = {}) { + const debug = Boolean(options && options.debug) + const override = Boolean(options && options.override) + const populated = {} + + if (typeof parsed !== 'object') { + const err = new Error('OBJECT_REQUIRED: Please check the processEnv argument being passed to populate') + err.code = 'OBJECT_REQUIRED' + throw err + } + + // Set process.env + for (const key of Object.keys(parsed)) { + if (Object.prototype.hasOwnProperty.call(processEnv, key)) { + if (override === true) { + processEnv[key] = parsed[key] + populated[key] = parsed[key] + } + + if (debug) { + if (override === true) { + _debug(`"${key}" is already defined and WAS overwritten`) + } else { + _debug(`"${key}" is already defined and was NOT overwritten`) + } + } + } else { + processEnv[key] = parsed[key] + populated[key] = parsed[key] + } + } + + return populated +} + +const DotenvModule = { + configDotenv, + _configVault, + _parseVault, + config, + decrypt, + parse, + populate +} + +module.exports.configDotenv = DotenvModule.configDotenv +module.exports._configVault = DotenvModule._configVault +module.exports._parseVault = DotenvModule._parseVault +module.exports.config = DotenvModule.config +module.exports.decrypt = DotenvModule.decrypt +module.exports.parse = DotenvModule.parse +module.exports.populate = DotenvModule.populate + +module.exports = DotenvModule + + /***/ }), /***/ 5340: @@ -35112,6 +35550,14 @@ exports.VersionResolver = VersionResolver; "use strict"; module.exports = /*#__PURE__*/JSON.parse('{"name":"@1password/op-js","version":"0.1.13","description":"A typed JS wrapper for the 1Password CLI","main":"./dist/index.js","types":"./dist/src/index.d.ts","files":["dist/"],"repository":{"type":"git","url":"https://github.com/1Password/op-js"},"license":"MIT","scripts":{"build":"license-checker-rseidelsohn --direct --files licenses && yarn compile --minify && tsc -p tsconfig.release.json --emitDeclarationOnly","compile":"esbuild src/index.ts src/cli.ts --platform=node --format=cjs --outdir=dist","eslint":"eslint -c .eslintrc.json \'src/*.ts\'","prepare":"husky install","prettier":"prettier --check \'src/*.ts\'","test:unit":"jest --testMatch \'/src/*.test.ts\'","test:integration":"jest --testMatch \'/tests/*.test.ts\' --setupFilesAfterEnv \'/jest.setup.ts\' --runInBand","typecheck":"tsc -p tsconfig.release.json --noEmit","watch":"yarn compile --watch"},"prettier":"@1password/prettier-config","lint-staged":{"src/*.ts":["prettier --write","eslint -c .eslintrc.json --fix"]},"devDependencies":{"@1password/eslint-config":"^4.3.0","@1password/prettier-config":"^1.1.3","@types/jest":"^29.5.12","@types/node":"^20.12.12","@types/semver":"^7.5.8","@typescript-eslint/eslint-plugin":"^7.9.0","esbuild":"^0.21.2","eslint":"^8.57.0","husky":"^9.0.11","jest":"^29.7.0","jest-environment-jsdom":"^29.6.2","joi":"^17.13.1","license-checker-rseidelsohn":"^4.3.0","lint-staged":"^15.2.2","prettier":"^3.2.5","prettier-plugin-organize-imports":"^3.2.4","ts-jest":"^29.1.2","typescript":"5.4.5"},"dependencies":{"lookpath":"^1.2.2","semver":"^7.6.2"}}'); +/***/ }), + +/***/ 56: +/***/ ((module) => { + +"use strict"; +module.exports = /*#__PURE__*/JSON.parse('{"name":"dotenv","version":"17.2.2","description":"Loads environment variables from .env file","main":"lib/main.js","types":"lib/main.d.ts","exports":{".":{"types":"./lib/main.d.ts","require":"./lib/main.js","default":"./lib/main.js"},"./config":"./config.js","./config.js":"./config.js","./lib/env-options":"./lib/env-options.js","./lib/env-options.js":"./lib/env-options.js","./lib/cli-options":"./lib/cli-options.js","./lib/cli-options.js":"./lib/cli-options.js","./package.json":"./package.json"},"scripts":{"dts-check":"tsc --project tests/types/tsconfig.json","lint":"standard","pretest":"npm run lint && npm run dts-check","test":"tap run --allow-empty-coverage --disable-coverage --timeout=60000","test:coverage":"tap run --show-full-coverage --timeout=60000 --coverage-report=text --coverage-report=lcov","prerelease":"npm test","release":"standard-version"},"repository":{"type":"git","url":"git://github.com/motdotla/dotenv.git"},"homepage":"https://github.com/motdotla/dotenv#readme","funding":"https://dotenvx.com","keywords":["dotenv","env",".env","environment","variables","config","settings"],"readmeFilename":"README.md","license":"BSD-2-Clause","devDependencies":{"@types/node":"^18.11.3","decache":"^4.6.2","sinon":"^14.0.1","standard":"^17.0.0","standard-version":"^9.5.0","tap":"^19.2.0","typescript":"^4.8.4"},"engines":{"node":">=12"},"browser":{"fs":false}}'); + /***/ }) /******/ }); @@ -35147,6 +35593,35 @@ module.exports = /*#__PURE__*/JSON.parse('{"name":"@1password/op-js","version":" /******/ } /******/ /************************************************************************/ +/******/ /* webpack/runtime/compat get default export */ +/******/ (() => { +/******/ // getDefaultExport function for compatibility with non-harmony modules +/******/ __nccwpck_require__.n = (module) => { +/******/ var getter = module && module.__esModule ? +/******/ () => (module['default']) : +/******/ () => (module); +/******/ __nccwpck_require__.d(getter, { a: getter }); +/******/ return getter; +/******/ }; +/******/ })(); +/******/ +/******/ /* webpack/runtime/define property getters */ +/******/ (() => { +/******/ // define getter functions for harmony exports +/******/ __nccwpck_require__.d = (exports, definition) => { +/******/ for(var key in definition) { +/******/ if(__nccwpck_require__.o(definition, key) && !__nccwpck_require__.o(exports, key)) { +/******/ Object.defineProperty(exports, key, { enumerable: true, get: definition[key] }); +/******/ } +/******/ } +/******/ }; +/******/ })(); +/******/ +/******/ /* webpack/runtime/hasOwnProperty shorthand */ +/******/ (() => { +/******/ __nccwpck_require__.o = (obj, prop) => (Object.prototype.hasOwnProperty.call(obj, prop)) +/******/ })(); +/******/ /******/ /* webpack/runtime/compat */ /******/ /******/ if (typeof __nccwpck_require__ !== 'undefined') __nccwpck_require__.ab = __dirname + "/"; @@ -35163,6 +35638,9 @@ var core = __nccwpck_require__(7484); var dist = __nccwpck_require__(7521); // EXTERNAL MODULE: ./node_modules/op-cli-installer/dist/index.js var op_cli_installer_dist = __nccwpck_require__(1621); +// EXTERNAL MODULE: ./node_modules/dotenv/lib/main.js +var main = __nccwpck_require__(8889); +var main_default = /*#__PURE__*/__nccwpck_require__.n(main); // EXTERNAL MODULE: ./node_modules/@actions/exec/lib/exec.js var exec = __nccwpck_require__(5236); ;// CONCATENATED MODULE: ./package.json @@ -35172,6 +35650,7 @@ const envConnectHost = "OP_CONNECT_HOST"; const envConnectToken = "OP_CONNECT_TOKEN"; const envServiceAccountToken = "OP_SERVICE_ACCOUNT_TOKEN"; const envManagedVariables = "OP_MANAGED_VARIABLES"; +const envFilePath = "OP_ENV_FILE"; const authErr = `Authentication error with environment variables: you must set either 1) ${envServiceAccountToken}, or 2) both ${envConnectHost} and ${envConnectToken}.`; ;// CONCATENATED MODULE: ./src/utils.ts @@ -35248,6 +35727,8 @@ const unsetPrevious = () => { + + const loadSecretsAction = async () => { try { // Get action inputs @@ -35259,6 +35740,12 @@ const loadSecretsAction = async () => { } // Validate that a proper authentication configuration is set for the CLI validateAuth(); + // Set environment variables from OP_ENV_FILE + const file = process.env[envFilePath]; + if (file) { + core.info(`Loading environment variables from file: ${file}`); + main_default().config({ path: file }); + } // Download and install the CLI await installCLI(); // Load secrets From df80909445588e2e663f524c5831e07e811d1b74 Mon Sep 17 00:00:00 2001 From: Volodymyr Zotov Date: Fri, 12 Dec 2025 14:22:36 -0600 Subject: [PATCH 02/13] Refactor test workflows to keep the same patterns as in other repos (terraform, operator) --- .github/workflows/acceptance-test.yml | 131 --------------------- .github/workflows/e2e-tests.yml | 159 ++++++++++++++++++++++++++ .github/workflows/ok-to-test.yml | 4 +- .github/workflows/test-e2e.yml | 107 +++++++++++++++++ .github/workflows/test-fork.yml | 92 --------------- .github/workflows/test.yml | 102 ----------------- 6 files changed, 268 insertions(+), 327 deletions(-) delete mode 100644 .github/workflows/acceptance-test.yml create mode 100644 .github/workflows/e2e-tests.yml create mode 100644 .github/workflows/test-e2e.yml delete mode 100644 .github/workflows/test-fork.yml delete mode 100644 .github/workflows/test.yml diff --git a/.github/workflows/acceptance-test.yml b/.github/workflows/acceptance-test.yml deleted file mode 100644 index aaa4924..0000000 --- a/.github/workflows/acceptance-test.yml +++ /dev/null @@ -1,131 +0,0 @@ -name: Acceptance test - -on: - workflow_call: - inputs: - secret: - required: true - type: string - secret-in-section: - required: true - type: string - multiline-secret: - required: true - type: string - export-env: - required: true - type: boolean - version: - required: false - type: string - default: "latest" - os: - required: true - type: string - default: "ubuntu-latest" - auth: - required: true - type: string - -jobs: - acceptance-test: - runs-on: ${{ inputs.os }} - steps: - - name: Base checkout - uses: actions/checkout@v4 - if: | - github.event_name != 'repository_dispatch' && - ( - github.ref == 'refs/heads/main' || - ( - github.event_name == 'pull_request' && - github.event.pull_request.head.repo.full_name == github.repository - ) - ) - - - name: Fork based /ok-to-test checkout - uses: actions/checkout@v4 - with: - ref: ${{ github.event.client_payload.pull_request.head.sha }} - if: | - github.event_name == 'repository_dispatch' && - github.event.client_payload.slash_command.args.named.sha != '' && - contains( - github.event.client_payload.pull_request.head.sha, - github.event.client_payload.slash_command.args.named.sha - ) - - - name: Launch 1Password Connect instance - if: ${{ inputs.auth == 'connect' }} - env: - OP_CONNECT_CREDENTIALS: ${{ secrets.OP_CONNECT_CREDENTIALS }} - run: | - echo "$OP_CONNECT_CREDENTIALS" > 1password-credentials.json - docker compose -f tests/fixtures/docker-compose.yml up -d && sleep 10 - - - name: Configure Service account - if: ${{ inputs.auth == 'service-account' }} - uses: ./configure - with: - service-account-token: ${{ secrets.OP_SERVICE_ACCOUNT_TOKEN }} - - - name: Verify Service Account env var is set - if: ${{ inputs.auth == 'service-account' }} - shell: bash - run: | - if [ -z "${OP_SERVICE_ACCOUNT_TOKEN}" ]; then - echo "OP_SERVICE_ACCOUNT_TOKEN environment variable is not set" >&2 - exit 1 - fi - - - name: Configure 1Password Connect - if: ${{ inputs.auth == 'connect' }} - uses: ./configure # 1password/load-secrets-action/configure@ - with: - connect-host: http://localhost:8080 - connect-token: ${{ secrets.OP_CONNECT_TOKEN }} - - - name: Verify Connect env vars are set - if: ${{ inputs.auth == 'connect' }} - run: | - if [ -z "$OP_CONNECT_HOST" ] || [ -z "$OP_CONNECT_TOKEN" ]; then - echo "OP_CONNECT_HOST or OP_CONNECT_TOKEN environment variables are not set" >&2 - exit 1 - fi - - - name: Load secrets - id: load_secrets - uses: ./ # 1password/load-secrets-action@ - with: - version: ${{ inputs.version }} - export-env: ${{ inputs.export-env }} - env: - SECRET: ${{ inputs.secret }} - SECRET_IN_SECTION: ${{ inputs.secret-in-section }} - MULTILINE_SECRET: ${{ inputs.multiline-secret }} - OP_ENV_FILE: ./tests/.env.tpl - - - name: Assert test secret values [step output] - if: ${{ !inputs.export-env }} - env: - SECRET: ${{ steps.load_secrets.outputs.SECRET }} - SECRET_IN_SECTION: ${{ steps.load_secrets.outputs.SECRET_IN_SECTION }} - MULTILINE_SECRET: ${{ steps.load_secrets.outputs.MULTILINE_SECRET }} - FILE_SECRET: ${{ steps.load_secrets.outputs.FILE_SECRET }} - FILE_SECRET_IN_SECTION: ${{ steps.load_secrets.outputs.FILE_SECRET_IN_SECTION }} - FILE_MULTILINE_SECRET: ${{ steps.load_secrets.outputs.FILE_MULTILINE_SECRET }} - run: ./tests/assert-env-set.sh - - - name: Assert test secret values [exported env] - if: ${{ inputs.export-env }} - run: ./tests/assert-env-set.sh - - - name: Remove secrets [exported env] - if: ${{ inputs.export-env }} - uses: ./ # 1password/load-secrets-action@ - with: - unset-previous: true - - - name: Assert removed secrets [exported env] - if: ${{ inputs.export-env }} - run: ./tests/assert-env-unset.sh diff --git a/.github/workflows/e2e-tests.yml b/.github/workflows/e2e-tests.yml new file mode 100644 index 0000000..ee191e4 --- /dev/null +++ b/.github/workflows/e2e-tests.yml @@ -0,0 +1,159 @@ +name: E2E Tests + +on: + # For local testing with: act push -W .github/workflows/e2e-tests.yml + push: + branches-ignore: + - "**" # Never runs on GitHub, only locally with act + + # For test.yml to call this workflow + workflow_call: + secrets: + OP_CONNECT_CREDENTIALS: + required: true + OP_CONNECT_TOKEN: + required: true + OP_SERVICE_ACCOUNT_TOKEN: + required: true + VAULT: + description: "1Password vault name or UUID" + required: true + +jobs: + test-service-account: + name: Service Account (${{ matrix.os }}, ${{ matrix.version }}, export-env=${{ matrix.export-env }}) + runs-on: ${{ matrix.os }} + strategy: + fail-fast: false + matrix: + os: [ubuntu-latest, macos-latest, windows-latest] + version: [latest, 2.30.0] + export-env: [true, false] + steps: + - name: Checkout + uses: actions/checkout@v5 + with: + fetch-depth: 0 + + - name: Generate .env.tpl + shell: bash + run: | + echo "FILE_SECRET=op://${{ secrets.VAULT }}/test-secret/password" > tests/.env.tpl + echo "FILE_SECRET_IN_SECTION=op://${{ secrets.VAULT }}/test-secret/test-section/password" >> tests/.env.tpl + echo "FILE_MULTILINE_SECRET=op://${{ secrets.VAULT }}/multiline-secret/notesPlain" >> tests/.env.tpl + + - name: Configure Service account + uses: ./configure + with: + service-account-token: ${{ secrets.OP_SERVICE_ACCOUNT_TOKEN }} + + - name: Load secrets + id: load_secrets + uses: ./ + with: + version: ${{ matrix.version }} + export-env: ${{ matrix.export-env }} + env: + SECRET: op://${{ secrets.VAULT }}/test-secret/password + SECRET_IN_SECTION: op://${{ secrets.VAULT }}/test-secret/test-section/password + MULTILINE_SECRET: op://${{ secrets.VAULT }}/multiline-secret/notesPlain + OP_ENV_FILE: ./tests/.env.tpl + + - name: Assert test secret values [step output] + if: ${{ !matrix.export-env }} + shell: bash + env: + SECRET: ${{ steps.load_secrets.outputs.SECRET }} + SECRET_IN_SECTION: ${{ steps.load_secrets.outputs.SECRET_IN_SECTION }} + MULTILINE_SECRET: ${{ steps.load_secrets.outputs.MULTILINE_SECRET }} + FILE_SECRET: ${{ steps.load_secrets.outputs.FILE_SECRET }} + FILE_SECRET_IN_SECTION: ${{ steps.load_secrets.outputs.FILE_SECRET_IN_SECTION }} + FILE_MULTILINE_SECRET: ${{ steps.load_secrets.outputs.FILE_MULTILINE_SECRET }} + run: ./tests/assert-env-set.sh + + - name: Assert test secret values [exported env] + if: ${{ matrix.export-env }} + shell: bash + run: ./tests/assert-env-set.sh + + - name: Remove secrets [exported env] + if: ${{ matrix.export-env }} + uses: ./ + with: + unset-previous: true + + - name: Assert removed secrets [exported env] + if: ${{ matrix.export-env }} + shell: bash + run: ./tests/assert-env-unset.sh + + test-connect: + name: Connect (ubuntu-latest, ${{ matrix.version }}, export-env=${{ matrix.export-env }}) + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + version: [latest, 2.30.0] + export-env: [true, false] + steps: + - name: Checkout + uses: actions/checkout@v5 + with: + fetch-depth: 0 + + - name: Generate .env.tpl + run: | + mkdir -p tests + echo "FILE_SECRET=op://${{ secrets.VAULT }}/test-secret/password" > tests/.env.tpl + echo "FILE_SECRET_IN_SECTION=op://${{ secrets.VAULT }}/test-secret/test-section/password" >> tests/.env.tpl + echo "FILE_MULTILINE_SECRET=op://${{ secrets.VAULT }}/multiline-secret/notesPlain" >> tests/.env.tpl + + - name: Launch 1Password Connect instance + env: + OP_CONNECT_CREDENTIALS: ${{ secrets.OP_CONNECT_CREDENTIALS }} + run: | + echo "$OP_CONNECT_CREDENTIALS" > 1password-credentials.json + docker compose -f tests/fixtures/docker-compose.yml up -d && sleep 10 + + - name: Configure 1Password Connect + uses: ./configure + with: + connect-host: http://localhost:8080 + connect-token: ${{ secrets.OP_CONNECT_TOKEN }} + + - name: Load secrets + id: load_secrets + uses: ./ + with: + version: ${{ matrix.version }} + export-env: ${{ matrix.export-env }} + env: + SECRET: op://${{ secrets.VAULT }}/test-secret/password + SECRET_IN_SECTION: op://${{ secrets.VAULT }}/test-secret/test-section/password + MULTILINE_SECRET: op://${{ secrets.VAULT }}/multiline-secret/notesPlain + OP_ENV_FILE: ./tests/.env.tpl + + - name: Assert test secret values [step output] + if: ${{ !matrix.export-env }} + env: + SECRET: ${{ steps.load_secrets.outputs.SECRET }} + SECRET_IN_SECTION: ${{ steps.load_secrets.outputs.SECRET_IN_SECTION }} + MULTILINE_SECRET: ${{ steps.load_secrets.outputs.MULTILINE_SECRET }} + FILE_SECRET: ${{ steps.load_secrets.outputs.FILE_SECRET }} + FILE_SECRET_IN_SECTION: ${{ steps.load_secrets.outputs.FILE_SECRET_IN_SECTION }} + FILE_MULTILINE_SECRET: ${{ steps.load_secrets.outputs.FILE_MULTILINE_SECRET }} + run: ./tests/assert-env-set.sh + + - name: Assert test secret values [exported env] + if: ${{ matrix.export-env }} + run: ./tests/assert-env-set.sh + + - name: Remove secrets [exported env] + if: ${{ matrix.export-env }} + uses: ./ + with: + unset-previous: true + + - name: Assert removed secrets [exported env] + if: ${{ matrix.export-env }} + run: ./tests/assert-env-unset.sh diff --git a/.github/workflows/ok-to-test.yml b/.github/workflows/ok-to-test.yml index e1d4060..e2f8584 100644 --- a/.github/workflows/ok-to-test.yml +++ b/.github/workflows/ok-to-test.yml @@ -1,4 +1,4 @@ -# If someone with write access comments "/ok-to-test" on a pull request, emit a repository_dispatch event +# Write comments "/ok-to-test sha=" on a pull request. This will emit a repository_dispatch event. name: Ok To Test on: @@ -15,7 +15,7 @@ jobs: if: ${{ github.event.issue.pull_request }} steps: - name: Slash Command Dispatch - uses: peter-evans/slash-command-dispatch@v3 + uses: volodymyrZotov/slash-command-dispatch@7c1b623a2b0eba93f684c34f689a441f0be84cf1 # TODO: use peter-evans/slash-command-dispatch when fix for team permissions is released https://github.com/peter-evans/slash-command-dispatch/pull/424 with: token: ${{ secrets.GITHUB_TOKEN }} reaction-token: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/test-e2e.yml b/.github/workflows/test-e2e.yml new file mode 100644 index 0000000..ce1a011 --- /dev/null +++ b/.github/workflows/test-e2e.yml @@ -0,0 +1,107 @@ +name: E2E Tests + +on: + push: + branches: [main] + pull_request: + repository_dispatch: + types: [ok-to-test-command] + +concurrency: + group: >- + ${{ github.event_name == 'pull_request' && + format('e2e-{0}', github.event.pull_request.head.ref) || + format('e2e-{0}', github.ref) }} + cancel-in-progress: true + +jobs: + check-external-pr: + runs-on: ubuntu-latest + outputs: + condition: ${{ steps.check.outputs.condition }} + steps: + - name: Check if PR is from external contributor + id: check + run: | + echo "Event name: ${{ github.event_name }}" + echo "Repository: ${{ github.repository }}" + + if [ "${{ github.event_name }}" == "pull_request" ]; then + # For pull_request events, check if PR is from external fork + echo "PR head repo: ${{ github.event.pull_request.head.repo.full_name }}" + if [ "${{ github.actor }}" == "dependabot[bot]" ]; then + echo "condition=skip" >> $GITHUB_OUTPUT + echo "Setting condition=skip (Dependabot PR)" + elif [ "${{ github.event.pull_request.head.repo.full_name }}" != "${{ github.repository }}" ]; then + echo "condition=skip" >> $GITHUB_OUTPUT + echo "Setting condition=skip (external fork PR creation)" + else + echo "condition=pr-creation-maintainer" >> $GITHUB_OUTPUT + echo "Setting condition=pr-creation-maintainer (internal PR creation)" + fi + elif [ "${{ github.event_name }}" == "repository_dispatch" ]; then + # For repository_dispatch events (ok-to-test), check if sha matches + SHA_PARAM="${{ github.event.client_payload.slash_command.args.named.sha }}" + PR_HEAD_SHA="${{ github.event.client_payload.pull_request.head.sha }}" + + echo "Checking dispatch event conditions..." + echo "SHA from command: $SHA_PARAM" + echo "PR head SHA: $PR_HEAD_SHA" + + if [ -n "$SHA_PARAM" ] && [[ "$PR_HEAD_SHA" == *"$SHA_PARAM"* ]]; then + echo "condition=dispatch-event" >> $GITHUB_OUTPUT + echo "Setting condition=dispatch-event (sha matches)" + else + echo "condition=skip" >> $GITHUB_OUTPUT + echo "Setting condition=skip (sha does not match or empty)" + fi + elif [ "${{ github.event_name }}" == "push" ] && [ "${{ github.ref_name }}" == "main" ]; then + echo "condition=push-to-main" >> $GITHUB_OUTPUT + echo "Setting condition=push-to-main (push to main)" + else + # Unknown event type + echo "condition=skip" >> $GITHUB_OUTPUT + echo "Setting condition=skip (unknown event type: ${{ github.event_name }})" + fi + + e2e: + needs: check-external-pr + if: | + (needs.check-external-pr.outputs.condition == 'pr-creation-maintainer') + || + (needs.check-external-pr.outputs.condition == 'dispatch-event') + || + needs.check-external-pr.outputs.condition == 'push-to-main' + uses: ./.github/workflows/e2e-tests.yml + secrets: + OP_CONNECT_CREDENTIALS: ${{ secrets.OP_CONNECT_CREDENTIALS }} + OP_CONNECT_TOKEN: ${{ secrets.OP_CONNECT_TOKEN }} + OP_SERVICE_ACCOUNT_TOKEN: ${{ secrets.OP_SERVICE_ACCOUNT_TOKEN }} + VAULT: ${{ secrets.VAULT }} + + # Post comment on fork PRs after /ok-to-test + comment-pr: + needs: [check-external-pr, e2e] + runs-on: ubuntu-latest + if: always() && needs.check-external-pr.outputs.condition == 'dispatch-event' + permissions: + pull-requests: write + steps: + - name: Create URL to the run output + id: vars + run: echo "run-url=https://github.com/$GITHUB_REPOSITORY/actions/runs/$GITHUB_RUN_ID" >> $GITHUB_OUTPUT + + - name: Create comment on PR + uses: peter-evans/create-or-update-comment@v4 + with: + issue-number: ${{ github.event.client_payload.pull_request.number }} + body: | + ${{ + needs.e2e.result == 'success' && '✅ E2E tests passed.' || + needs.e2e.result == 'failure' && '❌ E2E tests failed.' || + '⚠️ E2E tests completed.' + }} + + [View test run output][1] + + [1]: ${{ steps.vars.outputs.run-url }} diff --git a/.github/workflows/test-fork.yml b/.github/workflows/test-fork.yml deleted file mode 100644 index cdfa906..0000000 --- a/.github/workflows/test-fork.yml +++ /dev/null @@ -1,92 +0,0 @@ -on: - repository_dispatch: - types: [ok-to-test-command] -name: Run acceptance tests [fork] - -jobs: - test-with-output-secrets: - if: | - github.event_name == 'repository_dispatch' && - github.event.client_payload.slash_command.args.named.sha != '' && - contains( - github.event.client_payload.pull_request.head.sha, - github.event.client_payload.slash_command.args.named.sha - ) - uses: 1password/load-secrets-action/.github/workflows/acceptance-test.yml@main - secrets: inherit - with: - secret: op://acceptance-tests/test-secret/password - secret-in-section: op://acceptance-tests/test-secret/test-section/password - multiline-secret: op://acceptance-tests/multiline-secret/notesPlain - export-env: false - test-with-export-env: - if: | - github.event_name == 'repository_dispatch' && - github.event.client_payload.slash_command.args.named.sha != '' && - contains( - github.event.client_payload.pull_request.head.sha, - github.event.client_payload.slash_command.args.named.sha - ) - uses: 1password/load-secrets-action/.github/workflows/acceptance-test.yml@main - secrets: inherit - with: - secret: op://acceptance-tests/test-secret/password - secret-in-section: op://acceptance-tests/test-secret/test-section/password - multiline-secret: op://acceptance-tests/multiline-secret/notesPlain - export-env: true - test-references-with-ids: - if: | - github.event_name == 'repository_dispatch' && - github.event.client_payload.slash_command.args.named.sha != '' && - contains( - github.event.client_payload.pull_request.head.sha, - github.event.client_payload.slash_command.args.named.sha - ) - uses: 1password/load-secrets-action/.github/workflows/acceptance-test.yml@main - secrets: inherit - with: - secret: op://v5pz6venw4roosmkzdq2nhpv6u/hrgkzhrlvscomepxlgafb2m3ca/password - secret-in-section: op://v5pz6venw4roosmkzdq2nhpv6u/hrgkzhrlvscomepxlgafb2m3ca/Section_tco6nsqycj6jcbyx63h5isxcny/doxu3mhkozcznnk5vjrkpdqayy - multiline-secret: op://v5pz6venw4roosmkzdq2nhpv6u/ghtz3jvcc6dqmzc53d3r3eskge/notesPlain - export-env: false - update-checks: - # required permissions for updating the status of the pull request checks - permissions: - pull-requests: write - checks: write - runs-on: ubuntu-latest - if: ${{ always() }} - strategy: - matrix: - job-name: - [ - test-with-output-secrets, - test-with-export-env, - test-references-with-ids, - ] - needs: - [test-with-output-secrets, test-with-export-env, test-references-with-ids] - steps: - - uses: actions/github-script@v6 - env: - job: ${{ matrix.job-name }} - ref: ${{ github.event.client_payload.pull_request.head.sha }} - conclusion: ${{ needs[format('{0}', matrix.job-name )].result }} - with: - github-token: ${{ secrets.GITHUB_TOKEN }} - script: | - const { data: checks } = await github.rest.checks.listForRef({ - ...context.repo, - ref: process.env.ref - }); - - const check = checks.check_runs.filter(c => c.name === process.env.job); - - const { data: result } = await github.rest.checks.update({ - ...context.repo, - check_run_id: check[0].id, - status: 'completed', - conclusion: process.env.conclusion - }); - - return result; diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml deleted file mode 100644 index 772ac00..0000000 --- a/.github/workflows/test.yml +++ /dev/null @@ -1,102 +0,0 @@ -on: - push: - branches: [main] - pull_request: - types: [opened, synchronize, reopened] - branches: ["**"] # run for PRs targeting any branch (main and others) -name: Run acceptance tests - -jobs: - unit-tests: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v3 - - uses: actions/setup-node@v3 - with: - node-version: 20 - - run: npm ci - - run: npm test - - test-with-output-secrets: - if: | - github.ref == 'refs/heads/main' || - ( - github.event_name == 'pull_request' && - github.event.pull_request.head.repo.full_name == github.repository - ) - uses: 1password/load-secrets-action/.github/workflows/acceptance-test.yml@main - secrets: inherit - strategy: - matrix: - os: [ubuntu-latest, macos-latest, windows-latest] - version: [latest, latest-beta, 2.30.0, 2.30.0-beta.03] - auth: [connect, service-account] - exclude: - - os: macos-latest - auth: connect - - os: windows-latest - auth: connect - with: - os: ${{ matrix.os }} - version: ${{ matrix.version }} - auth: ${{ matrix.auth }} - secret: op://acceptance-tests/test-secret/password - secret-in-section: op://acceptance-tests/test-secret/test-section/password - multiline-secret: op://acceptance-tests/multiline-secret/notesPlain - export-env: false - - test-with-export-env: - if: | - github.ref == 'refs/heads/main' || - ( - github.event_name == 'pull_request' && - github.event.pull_request.head.repo.full_name == github.repository - ) - uses: 1password/load-secrets-action/.github/workflows/acceptance-test.yml@main - secrets: inherit - strategy: - matrix: - os: [ubuntu-latest, macos-latest, windows-latest] - version: [latest, latest-beta, 2.30.0, 2.30.0-beta.03] - auth: [connect, service-account] - exclude: - - os: macos-latest - auth: connect - - os: windows-latest - auth: connect - with: - os: ${{ matrix.os }} - version: ${{ matrix.version }} - auth: ${{ matrix.auth }} - secret: op://acceptance-tests/test-secret/password - secret-in-section: op://acceptance-tests/test-secret/test-section/password - multiline-secret: op://acceptance-tests/multiline-secret/notesPlain - export-env: true - - test-references-with-ids: - if: | - github.ref == 'refs/heads/main' || - ( - github.event_name == 'pull_request' && - github.event.pull_request.head.repo.full_name == github.repository - ) - uses: 1password/load-secrets-action/.github/workflows/acceptance-test.yml@main - secrets: inherit - strategy: - matrix: - os: [ubuntu-latest, macos-latest, windows-latest] - version: [latest, latest-beta, 2.30.0, 2.30.0-beta.03] - auth: [connect, service-account] - exclude: - - os: macos-latest - auth: connect - - os: windows-latest - auth: connect - with: - os: ${{ matrix.os }} - version: ${{ matrix.version }} - auth: ${{ matrix.auth }} - secret: op://v5pz6venw4roosmkzdq2nhpv6u/hrgkzhrlvscomepxlgafb2m3ca/password - secret-in-section: op://v5pz6venw4roosmkzdq2nhpv6u/hrgkzhrlvscomepxlgafb2m3ca/Section_tco6nsqycj6jcbyx63h5isxcny/doxu3mhkozcznnk5vjrkpdqayy - multiline-secret: op://v5pz6venw4roosmkzdq2nhpv6u/ghtz3jvcc6dqmzc53d3r3eskge/notesPlain - export-env: false From 74df766d9655f232017750bf847a2a2985bf3b88 Mon Sep 17 00:00:00 2001 From: Volodymyr Zotov Date: Fri, 12 Dec 2025 14:23:00 -0600 Subject: [PATCH 03/13] Rename to lint and test and it now includes both lint and tests steps --- .../workflows/{lint.yml => lint-and-test.yml} | 19 +++++++++++++------ 1 file changed, 13 insertions(+), 6 deletions(-) rename .github/workflows/{lint.yml => lint-and-test.yml} (74%) diff --git a/.github/workflows/lint.yml b/.github/workflows/lint-and-test.yml similarity index 74% rename from .github/workflows/lint.yml rename to .github/workflows/lint-and-test.yml index 179c50e..e0ff6e8 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint-and-test.yml @@ -1,29 +1,36 @@ +name: Lint and Test + on: push: branches: [main] pull_request: -name: Lint jobs: - lint: + lint-and-test: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v5 + - name: Run ShellCheck uses: ludeeus/action-shellcheck@2.0.0 with: ignore_paths: >- .husky + - name: Setup Node.js - id: setup-node uses: actions/setup-node@v4 with: node-version: 20 cache: npm - - name: Install Dependencies - id: install + + - name: Install dependencies run: npm ci + - name: Check formatting run: npm run format:check + - name: Check lint run: npm run lint + + - name: Run unit tests + run: npm test From 80f581e4b5dd0f21a475b7f96c3a2ceda4df33f3 Mon Sep 17 00:00:00 2001 From: Volodymyr Zotov Date: Fri, 12 Dec 2025 14:23:30 -0600 Subject: [PATCH 04/13] Remove env.tpl file, as it will be created in the test workflow automatically --- tests/.env.tpl | 3 --- 1 file changed, 3 deletions(-) delete mode 100644 tests/.env.tpl diff --git a/tests/.env.tpl b/tests/.env.tpl deleted file mode 100644 index 15d7272..0000000 --- a/tests/.env.tpl +++ /dev/null @@ -1,3 +0,0 @@ -FILE_SECRET=op://acceptance-tests/test-secret/password -FILE_SECRET_IN_SECTION=op://acceptance-tests/test-secret/test-section/password -FILE_MULTILINE_SECRET=op://acceptance-tests/multiline-secret/notesPlain \ No newline at end of file From 0f3110274c4a719a770d8f641c6fc9398965277d Mon Sep 17 00:00:00 2001 From: Volodymyr Zotov Date: Fri, 12 Dec 2025 14:28:54 -0600 Subject: [PATCH 05/13] Fix test-e2e.yml --- .github/workflows/test-e2e.yml | 28 ++++++++++++++++++---------- 1 file changed, 18 insertions(+), 10 deletions(-) diff --git a/.github/workflows/test-e2e.yml b/.github/workflows/test-e2e.yml index ce1a011..66d46a3 100644 --- a/.github/workflows/test-e2e.yml +++ b/.github/workflows/test-e2e.yml @@ -3,7 +3,15 @@ name: E2E Tests on: push: branches: [main] + paths-ignore: &ignore_paths + - "docs/**" + - "config/**" + - "*.md" + - ".gitignore" + - "LICENSE" + - "tests/**" pull_request: + paths-ignore: *ignore_paths repository_dispatch: types: [ok-to-test-command] @@ -93,15 +101,15 @@ jobs: - name: Create comment on PR uses: peter-evans/create-or-update-comment@v4 - with: - issue-number: ${{ github.event.client_payload.pull_request.number }} - body: | - ${{ - needs.e2e.result == 'success' && '✅ E2E tests passed.' || - needs.e2e.result == 'failure' && '❌ E2E tests failed.' || - '⚠️ E2E tests completed.' - }} + with: + issue-number: ${{ github.event.client_payload.pull_request.number }} + body: | + ${{ + needs.e2e.result == 'success' && '✅ E2E tests passed.' || + needs.e2e.result == 'failure' && '❌ E2E tests failed.' || + '⚠️ E2E tests completed.' + }} - [View test run output][1] + [View test run output][1] - [1]: ${{ steps.vars.outputs.run-url }} + [1]: ${{ steps.vars.outputs.run-url }} From 1824b2f006d56ceb812d43a5a89d71212fad3e20 Mon Sep 17 00:00:00 2001 From: Volodymyr Zotov Date: Fri, 12 Dec 2025 14:32:18 -0600 Subject: [PATCH 06/13] Use the same test matrix for connect and fail early --- .github/workflows/e2e-tests.yml | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/.github/workflows/e2e-tests.yml b/.github/workflows/e2e-tests.yml index ee191e4..095592a 100644 --- a/.github/workflows/e2e-tests.yml +++ b/.github/workflows/e2e-tests.yml @@ -24,7 +24,7 @@ jobs: name: Service Account (${{ matrix.os }}, ${{ matrix.version }}, export-env=${{ matrix.export-env }}) runs-on: ${{ matrix.os }} strategy: - fail-fast: false + fail-fast: true matrix: os: [ubuntu-latest, macos-latest, windows-latest] version: [latest, 2.30.0] @@ -91,8 +91,9 @@ jobs: name: Connect (ubuntu-latest, ${{ matrix.version }}, export-env=${{ matrix.export-env }}) runs-on: ubuntu-latest strategy: - fail-fast: false + fail-fast: true matrix: + os: [ubuntu-latest, macos-latest, windows-latest] version: [latest, 2.30.0] export-env: [true, false] steps: From fee9db6b39272295b6133eac2bdd95d8947e171b Mon Sep 17 00:00:00 2001 From: Volodymyr Zotov Date: Fri, 12 Dec 2025 14:35:23 -0600 Subject: [PATCH 07/13] Use latest stable slash dispatch command action --- .github/workflows/ok-to-test.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ok-to-test.yml b/.github/workflows/ok-to-test.yml index e2f8584..cb1c547 100644 --- a/.github/workflows/ok-to-test.yml +++ b/.github/workflows/ok-to-test.yml @@ -15,7 +15,7 @@ jobs: if: ${{ github.event.issue.pull_request }} steps: - name: Slash Command Dispatch - uses: volodymyrZotov/slash-command-dispatch@7c1b623a2b0eba93f684c34f689a441f0be84cf1 # TODO: use peter-evans/slash-command-dispatch when fix for team permissions is released https://github.com/peter-evans/slash-command-dispatch/pull/424 + uses: peter-evans/slash-command-dispatch@v5 with: token: ${{ secrets.GITHUB_TOKEN }} reaction-token: ${{ secrets.GITHUB_TOKEN }} From 0ee5bc7530f5a953e0d095d528436e385ee8e68f Mon Sep 17 00:00:00 2001 From: Volodymyr Zotov Date: Fri, 12 Dec 2025 14:51:54 -0600 Subject: [PATCH 08/13] Update gitignore --- .gitignore | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index f538381..06f012b 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ coverage/ node_modules/ -.idea/ \ No newline at end of file +.idea/ +.1password-credentials.json From 483f83267aa82abe8d01eee8b9ac602e7a7e5671 Mon Sep 17 00:00:00 2001 From: Volodymyr Zotov Date: Fri, 12 Dec 2025 14:52:23 -0600 Subject: [PATCH 09/13] Update git ignore --- .gitignore | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index 06f012b..5b89572 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,4 @@ coverage/ node_modules/ .idea/ -.1password-credentials.json +1password-credentials.json From 2c0496a719d78eaa7642cef49e9d85d04258d497 Mon Sep 17 00:00:00 2001 From: Volodymyr Zotov Date: Mon, 15 Dec 2025 08:42:58 -0600 Subject: [PATCH 10/13] Add testing docs --- docs/fork-pr-testing.md | 32 ++++++++++++++++++++++++++++ docs/local-testing.md | 46 +++++++++++++++++++++++++++++++++++++++++ 2 files changed, 78 insertions(+) create mode 100644 docs/fork-pr-testing.md create mode 100644 docs/local-testing.md diff --git a/docs/fork-pr-testing.md b/docs/fork-pr-testing.md new file mode 100644 index 0000000..90448ba --- /dev/null +++ b/docs/fork-pr-testing.md @@ -0,0 +1,32 @@ +# Fork PR Testing Guide + +This document explains how testing works for external pull requests from forks. + +## Overview + +The testing system consists of two main workflows: + +1. **E2E Tests** (`test-e2e.yml`) - Runs automatically for internal PRs, need manual trigger on external PRs. +2. **Ok To Test** (`ok-to-test.yml`) - Dispatches `repository_dispatch` event when maintainer puts the `/ok-to-test sha=` comment in the forked PR thread. + +## How It Works + +### 1. PR is created by maintainer: + +For the PR created by maintainer `E2E Test` workflow starts automatically. The PR check will reflect the status of the job. + +### 2. PR is created by external contributor: + +For the PR created by external contributor `E2E Test` workflow **won't** start automatically. +Maintainer should make a sanity check of the changes and run it manually by: +1. Putting a comment `/ok-to-test sha=` in the PR thread. +2. `E2E Test` workflow starts. +3. After `E2E Test` workflow finishes, a comment with a link to the workflow, along with its status will be posted in the PR. +4. Maintainer can merge PR or request the changes based on the `E2E Test` results. + + +## Notes + +- Only users with **write** permissions can trigger the `/ok-to-test` command. +- External PRs are automatically detected and prevented from running e2e tests automatically. +- Running e2e test on the external PR is optional. Maintainer can merge PR without running it. Maintainer decides whether it's needed to run an E2E test. diff --git a/docs/local-testing.md b/docs/local-testing.md new file mode 100644 index 0000000..21f76df --- /dev/null +++ b/docs/local-testing.md @@ -0,0 +1,46 @@ +# Local Testing Guide + +This document explains how to run e2e tests locally using `act`. + +## Prerequisites + +1. **Docker** installed and running +2. **act** installed ([install guide](https://github.com/nektos/act#installation)) + ```bash + brew install act # macOS + ``` +3. **1Password credentials** (see [Required Secrets](#required-secrets)) +4. Build action + +## Required env variables + +| Secret | Description | +| -------------------------- | --------------------- | +| `OP_SERVICE_ACCOUNT_TOKEN` | Service Account token | +| `VAULT` | Vault name or UUID | + +## Building Before Testing + +If you've modified TypeScript code, rebuild before running E2E tests: + +```bash +npm run build +``` + +## Testing + +### Run E2E tests using Service Account + +```bash +act push -W .github/workflows/e2e-tests.yml \ + -s OP_SERVICE_ACCOUNT_TOKEN="$OP_SERVICE_ACCOUNT_TOKEN" \ + -s VAULT="$VAULT" \ + -j test-service-account \ + --matrix os:ubuntu-latest +``` + +## Run unit tests + +```bash +npm test +``` From fac78884c8835ee4570b491f042b7584344345f2 Mon Sep 17 00:00:00 2001 From: Volodymyr Zotov Date: Mon, 15 Dec 2025 08:43:16 -0600 Subject: [PATCH 11/13] Bump create-or-update-comment action to latest --- .github/workflows/test-e2e.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/test-e2e.yml b/.github/workflows/test-e2e.yml index 66d46a3..2fd6d98 100644 --- a/.github/workflows/test-e2e.yml +++ b/.github/workflows/test-e2e.yml @@ -100,7 +100,7 @@ jobs: run: echo "run-url=https://github.com/$GITHUB_REPOSITORY/actions/runs/$GITHUB_RUN_ID" >> $GITHUB_OUTPUT - name: Create comment on PR - uses: peter-evans/create-or-update-comment@v4 + uses: peter-evans/create-or-update-comment@v5 with: issue-number: ${{ github.event.client_payload.pull_request.number }} body: | From 6961848b5149cea0e242c1bc0b23de85844b1350 Mon Sep 17 00:00:00 2001 From: Volodymyr Zotov Date: Mon, 15 Dec 2025 08:46:32 -0600 Subject: [PATCH 12/13] Fix formatting --- docs/fork-pr-testing.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/fork-pr-testing.md b/docs/fork-pr-testing.md index 90448ba..c65162c 100644 --- a/docs/fork-pr-testing.md +++ b/docs/fork-pr-testing.md @@ -19,12 +19,12 @@ For the PR created by maintainer `E2E Test` workflow starts automatically. The P For the PR created by external contributor `E2E Test` workflow **won't** start automatically. Maintainer should make a sanity check of the changes and run it manually by: + 1. Putting a comment `/ok-to-test sha=` in the PR thread. 2. `E2E Test` workflow starts. 3. After `E2E Test` workflow finishes, a comment with a link to the workflow, along with its status will be posted in the PR. 4. Maintainer can merge PR or request the changes based on the `E2E Test` results. - ## Notes - Only users with **write** permissions can trigger the `/ok-to-test` command. From ba38da790551ba0fc5da4a088c9b8cafce053350 Mon Sep 17 00:00:00 2001 From: Volodymyr Zotov Date: Mon, 15 Dec 2025 09:56:42 -0600 Subject: [PATCH 13/13] Do not ignore tests dir --- .github/workflows/test-e2e.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/.github/workflows/test-e2e.yml b/.github/workflows/test-e2e.yml index 2fd6d98..9b2b360 100644 --- a/.github/workflows/test-e2e.yml +++ b/.github/workflows/test-e2e.yml @@ -9,7 +9,6 @@ on: - "*.md" - ".gitignore" - "LICENSE" - - "tests/**" pull_request: paths-ignore: *ignore_paths repository_dispatch: