Compare commits

..

53 Commits

Author SHA1 Message Date
Codex
2d777f9862 Adapt action runtime for Gitea node20 runners
Some checks failed
Lint and Test / lint-and-test (push) Has been cancelled
2026-04-17 23:54:53 +08:00
Jill Regan
2a9101f21d Merge pull request #156 from 1Password/jill/bump-actions
Some checks failed
Lint and Test / lint-and-test (push) Has been cancelled
Bump Actions
2026-03-27 08:26:34 -04:00
Jill Regan
5b18565a3b bump actions 2026-03-24 15:05:17 -04:00
Jill Regan
92467eb28f Merge pull request #153 from 1Password/release/v4.0.0
Prepare Release v4.0.0
2026-03-23 15:20:50 -04:00
Jill Regan
21f21cb312 Update readme 2026-03-23 15:09:56 -04:00
Jill Regan
a82b5b0e62 create new build 2026-03-23 09:55:35 -04:00
Jill Regan
355a4641a8 Merge pull request #151 from 1Password/fix/dependabot-alerts
fix Dependabot / npm audit vulnerabilities
2026-03-23 09:47:15 -04:00
Jill Regan
50dbf2f36c Run npm isntall 2026-03-19 13:40:01 -04:00
Jill Regan
2a602963c2 Undo version bump 2026-03-19 13:26:29 -04:00
Jill Regan
61e04e231d Overide minimatch 2026-03-19 13:25:24 -04:00
Jill Regan
93d1217e3f Merge pull request #150 from 1Password/jill/update-tests-to-use-node-24
Update test actions to use node 24
2026-03-19 11:51:32 -04:00
Jill Regan
17a79a92a6 Undo mistaken udpate 2026-03-19 10:14:53 -04:00
Jill Regan
7a0b59f9ab Update to v6 2026-03-19 10:10:35 -04:00
Jill Regan
a916812f67 Update tests to use node 24 2026-03-19 09:51:12 -04:00
Jill Regan
15f53e00f6 Merge pull request #149 from jjavieralv/fix/node-version-update
Update node version due to GH deprecation
2026-03-19 09:46:41 -04:00
jjavieralv
911ae9a9d1 Update node version due to GH deprecation 2026-03-17 10:09:54 +01:00
Jill Regan
18c9485047 Merge pull request #147 from 1Password/jill/add-e2e-tests
Add e2e test cases
2026-03-13 12:03:25 -04:00
Jill Regan
7eb7055d29 Remove matrix os array 2026-03-13 09:54:10 -04:00
Jill Regan
ebcbcb60ac Update to use health endpoint check 2026-03-13 09:48:35 -04:00
Jill Regan
13dac1510b remove unecessary checks 2026-03-10 16:01:04 -04:00
Jill Regan
960f48270b Use notes plain 2026-03-09 16:43:38 -04:00
Jill Regan
d87677b04d Add credentils test 2026-03-09 16:25:17 -04:00
Jill Regan
bd0f47e27e Wait for connect sync 2026-03-09 15:16:34 -04:00
Jill Regan
6352983a5d Remove file content test 2026-03-09 15:10:36 -04:00
Jill Regan
be02de5ede Update connect tests 2026-03-09 15:05:23 -04:00
Jill Regan
2b062ec18c Reduce runs in parallel 2026-03-09 15:03:24 -04:00
Jill Regan
f4a6c38f2a Add token to request 2026-03-09 14:56:21 -04:00
Jill Regan
bc8523c04b Add sync wait 2026-03-09 14:48:37 -04:00
Jill Regan
6ecbf76d39 Improve website check 2026-03-09 14:24:14 -04:00
Jill Regan
2763f7b0b3 Update assert script 2026-03-09 14:15:20 -04:00
Jill Regan
934acd2a2a Remove website check 2026-03-09 14:08:08 -04:00
Jill Regan
affe8f4720 Use correct method 2026-03-09 14:02:47 -04:00
Jill Regan
ad358d4370 Add e2e test cases 2026-03-09 13:57:59 -04:00
Jill Regan
dafbe7cb03 Merge pull request #146 from 1Password/release/v3.2.1
Prepare Release v3.2.1
2026-03-02 19:32:56 -05:00
Jill Regan
7f98afc2d7 Remove binary 2026-03-02 19:13:10 -05:00
Jill Regan
9cdab8f59d create new build 2026-03-02 19:03:41 -05:00
Jill Regan
6a14785fa5 Merge pull request #145 from 1Password/revert-pr-137
Rollback Migration to SDK with Service Account
2026-03-02 19:01:19 -05:00
Jill Regan
7d4e24a43f Revert "Merge pull request #137 from 1Password/feature/migrate-to-sdk"
This reverts commit 38b333095d, reversing
changes made to 652d567877.
2026-03-02 18:41:50 -05:00
Jill Regan
cb7c5acc8a Merge pull request #143 from 1Password/release/v3.2.0
Prepare Release/v3.2.0
2026-03-02 15:21:21 -05:00
Jill Regan
ea49b14f1e Rebuild 2026-03-02 14:05:27 -05:00
Jill Regan
93abacc9df Fix version 2026-03-02 14:03:42 -05:00
Jill Regan
40e52b6cef Update version and build 2026-03-02 08:41:29 -05:00
Jill Regan
38b333095d Merge pull request #137 from 1Password/feature/migrate-to-sdk
feat: migrate service account ot use 1Password sdk
2026-02-27 14:13:44 -05:00
Jill Regan
5b2af23419 remove version 2026-02-27 11:34:19 -05:00
Jill Regan
652d567877 Merge pull request #141 from 1Password/fix/upgrade-actions-toolkit
Upgrade actions toolkit
2026-02-27 10:51:00 -05:00
Jill Regan
ee92e1fd32 Update dist 2026-02-26 08:18:20 -05:00
Jill Regan
d688c27248 Update actions/exec 2026-02-26 08:14:16 -05:00
Jill Regan
a312828d43 switch to common js 2026-02-25 08:44:00 -05:00
Jill Regan
e6b45e828c remove require statement 2026-02-24 10:44:26 -05:00
Jill Regan
639ddd6614 Update build configure 2026-02-24 09:51:56 -05:00
Jill Regan
e5d7353d74 use module exports 2026-02-24 09:40:54 -05:00
Jill Regan
a665f2c1ab Merge pull request #135 from 1Password/jill/validate-secret-reference
Add secret ref validation
2026-02-23 11:42:24 -05:00
Jill Regan
485265b41c Upgrade actions toolkit 2026-02-22 12:50:32 -05:00
22 changed files with 37535 additions and 33813 deletions

View File

@@ -21,10 +21,7 @@ on:
OP_SERVICE_ACCOUNT_TOKEN:
required: true
VAULT:
description: "1Password vault name"
required: true
VAULT_ID:
description: "1Password vault UUID"
description: "1Password vault name or UUID"
required: true
jobs:
@@ -33,21 +30,22 @@ jobs:
runs-on: ${{ matrix.os }}
strategy:
fail-fast: true
max-parallel: 4
matrix:
os: [ubuntu-latest, macos-latest, windows-latest]
version: [latest, 2.30.0]
export-env: [true, false]
steps:
- name: Checkout
uses: actions/checkout@v5
uses: actions/checkout@v6
with:
fetch-depth: 0
ref: ${{ inputs.ref }}
- name: Setup Node.js
uses: actions/setup-node@v4
uses: actions/setup-node@v6
with:
node-version: 20
node-version: 24
cache: npm
- name: Install dependencies
@@ -59,27 +57,12 @@ jobs:
- name: Generate .env.tpl
shell: bash
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
echo "SECRET_WITH_FILE=op://${{ secrets.VAULT }}/file-secret/test.txt" >> tests/.env.tpl
echo "SECRET_WITH_FILE_IN_SECTION=op://${{ secrets.VAULT }}/file-secret/file section/test.txt" >> tests/.env.tpl
echo "DOUBLE_SECTION_SECRET=op://${{ secrets.VAULT }}/double-section-secret/test-section/password" >> tests/.env.tpl
echo "SSH_PRIVATE_KEY=op://${{ secrets.VAULT }}/test-ssh-key/private key" >> tests/.env.tpl
echo "SSH_PRIVATE_KEY_OPENSSH=op://${{ secrets.VAULT }}/test-ssh-key/private key?ssh-format=openssh" >> tests/.env.tpl
- name: Generate .vaultId_env.tpl
shell: bash
run: |
echo "FILE_SECRET=op://${{ secrets.VAULT_ID }}/test-secret/password" > tests/.vaultId_env.tpl
echo "FILE_SECRET_IN_SECTION=op://${{ secrets.VAULT_ID }}/test-secret/test-section/password" >> tests/.vaultId_env.tpl
echo "FILE_MULTILINE_SECRET=op://${{ secrets.VAULT_ID }}/multiline-secret/notesPlain" >> tests/.vaultId_env.tpl
echo "SECRET_WITH_FILE=op://${{ secrets.VAULT_ID }}/file-secret/test.txt" >> tests/.vaultId_env.tpl
echo "SECRET_WITH_FILE_IN_SECTION=op://${{ secrets.VAULT_ID }}/file-secret/file section/test.txt" >> tests/.vaultId_env.tpl
echo "DOUBLE_SECTION_SECRET=op://${{ secrets.VAULT_ID }}/double-section-secret/test-section/password" >> tests/.vaultId_env.tpl
echo "SSH_PRIVATE_KEY=op://${{ secrets.VAULT_ID }}/test-ssh-key/private key" >> tests/.vaultId_env.tpl
echo "SSH_PRIVATE_KEY_OPENSSH=op://${{ secrets.VAULT_ID }}/test-ssh-key/private key?ssh-format=openssh" >> tests/.vaultId_env.tpl
echo "FILE_WEBSITE=op://${{ secrets.VAULT }}/test-secret/website" >> tests/.env.tpl
echo "FILE_TEST_SSH_KEY=op://${{ secrets.VAULT }}/test-ssh-key/private key" >> tests/.env.tpl
echo "FILE_TEST_SSH_KEY_OPENSSH=op://${{ secrets.VAULT }}/test-ssh-key/private key?ssh-format=openssh" >> tests/.env.tpl
- name: Configure Service account
uses: ./configure
@@ -96,47 +79,51 @@ jobs:
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
SECRET_WITH_FILE: op://${{ secrets.VAULT }}/file-secret/test.txt
SECRET_WITH_FILE_IN_SECTION: op://${{ secrets.VAULT }}/file-secret/file section/test.txt
DOUBLE_SECTION_SECRET: op://${{ secrets.VAULT }}/double-section-secret/test-section/password
SSH_PRIVATE_KEY: op://${{ secrets.VAULT }}/test-ssh-key/private key
SSH_PRIVATE_KEY_OPENSSH: op://${{ secrets.VAULT }}/test-ssh-key/private key?ssh-format=openssh
WEBSITE: op://${{ secrets.VAULT }}/test-secret/website
TEST_SSH_KEY: op://${{ secrets.VAULT }}/test-ssh-key/private key
TEST_SSH_KEY_OPENSSH: "op://${{ secrets.VAULT }}/test-ssh-key/private key?ssh-format=openssh"
OP_ENV_FILE: ./tests/.env.tpl
- name: Assert test secret values [step output]
if: ${{ !matrix.export-env }}
shell: bash
env:
ASSERT_WEBSITE: "true"
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 }}
SECRET_WITH_FILE: ${{ steps.load_secrets.outputs.SECRET_WITH_FILE }}
SECRET_WITH_FILE_IN_SECTION: ${{ steps.load_secrets.outputs.SECRET_WITH_FILE_IN_SECTION }}
DOUBLE_SECTION_SECRET: ${{ steps.load_secrets.outputs.DOUBLE_SECTION_SECRET }}
SSH_PRIVATE_KEY: ${{ steps.load_secrets.outputs.SSH_PRIVATE_KEY }}
SSH_PRIVATE_KEY_OPENSSH: ${{ steps.load_secrets.outputs.SSH_PRIVATE_KEY_OPENSSH }}
WEBSITE: ${{ steps.load_secrets.outputs.WEBSITE }}
FILE_WEBSITE: ${{ steps.load_secrets.outputs.FILE_WEBSITE }}
TEST_SSH_KEY: ${{ steps.load_secrets.outputs.TEST_SSH_KEY }}
FILE_TEST_SSH_KEY: ${{ steps.load_secrets.outputs.FILE_TEST_SSH_KEY }}
TEST_SSH_KEY_OPENSSH: ${{ steps.load_secrets.outputs.TEST_SSH_KEY_OPENSSH }}
FILE_TEST_SSH_KEY_OPENSSH: ${{ steps.load_secrets.outputs.FILE_TEST_SSH_KEY_OPENSSH }}
run: ./tests/assert-env-set.sh
- name: Assert SSH key env vars [step output]
if: ${{ !matrix.export-env }}
shell: bash
env:
TEST_SSH_KEY: ${{ steps.load_secrets.outputs.TEST_SSH_KEY }}
FILE_TEST_SSH_KEY: ${{ steps.load_secrets.outputs.FILE_TEST_SSH_KEY }}
TEST_SSH_KEY_OPENSSH: ${{ steps.load_secrets.outputs.TEST_SSH_KEY_OPENSSH }}
FILE_TEST_SSH_KEY_OPENSSH: ${{ steps.load_secrets.outputs.FILE_TEST_SSH_KEY_OPENSSH }}
run: ./tests/assert-ssh-keys-set.sh
- name: Assert test secret values [exported env]
if: ${{ matrix.export-env }}
shell: bash
env:
ASSERT_WEBSITE: "true"
run: ./tests/assert-env-set.sh
- name: Assert SSH keys [step output]
if: ${{ !matrix.export-env }}
shell: bash
env:
SSH_PRIVATE_KEY: ${{ steps.load_secrets.outputs.SSH_PRIVATE_KEY }}
SSH_PRIVATE_KEY_OPENSSH: ${{ steps.load_secrets.outputs.SSH_PRIVATE_KEY_OPENSSH }}
run: ./tests/assert-ssh-keys.sh
- name: Assert SSH keys [exported env]
- name: Assert SSH key env vars [exported env]
if: ${{ matrix.export-env }}
shell: bash
run: ./tests/assert-ssh-keys.sh
run: ./tests/assert-ssh-keys-set.sh
- name: Remove secrets [exported env]
if: ${{ matrix.export-env }}
@@ -149,75 +136,25 @@ jobs:
shell: bash
run: ./tests/assert-env-unset.sh
- name: Load secrets (invalid ref - expect failure)
id: load_invalid
continue-on-error: true
uses: ./
env:
BAD_REF: "op://x"
with:
export-env: true
- name: Assert invalid ref failed
shell: bash
run: ./tests/assert-invalid-ref-failed.sh
env:
STEP_OUTCOME: ${{ steps.load_invalid.outcome }}
- name: Load secrets by vault ID
id: load_secrets_by_vault_id
uses: ./
with:
version: ${{ matrix.version }}
export-env: ${{ matrix.export-env }}
env:
SECRET: op://${{ secrets.VAULT_ID }}/test-secret/password
SECRET_IN_SECTION: op://${{ secrets.VAULT_ID }}/test-secret/test-section/password
MULTILINE_SECRET: op://${{ secrets.VAULT_ID }}/multiline-secret/notesPlain
SECRET_WITH_FILE: op://${{ secrets.VAULT_ID }}/file-secret/test.txt
SECRET_WITH_FILE_IN_SECTION: op://${{ secrets.VAULT_ID }}/file-secret/file section/test.txt
DOUBLE_SECTION_SECRET: op://${{ secrets.VAULT_ID }}/double-section-secret/test-section/password
SSH_PRIVATE_KEY: op://${{ secrets.VAULT_ID }}/test-ssh-key/private key
SSH_PRIVATE_KEY_OPENSSH: op://${{ secrets.VAULT_ID }}/test-ssh-key/private key?ssh-format=openssh
OP_ENV_FILE: ./tests/.vaultId_env.tpl
- name: Assert test secret values [vault by ID]
if: ${{ !matrix.export-env }}
shell: bash
env:
SECRET: ${{ steps.load_secrets_by_vault_id.outputs.SECRET }}
SECRET_IN_SECTION: ${{ steps.load_secrets_by_vault_id.outputs.SECRET_IN_SECTION }}
MULTILINE_SECRET: ${{ steps.load_secrets_by_vault_id.outputs.MULTILINE_SECRET }}
FILE_SECRET: ${{ steps.load_secrets_by_vault_id.outputs.FILE_SECRET }}
FILE_SECRET_IN_SECTION: ${{ steps.load_secrets_by_vault_id.outputs.FILE_SECRET_IN_SECTION }}
FILE_MULTILINE_SECRET: ${{ steps.load_secrets_by_vault_id.outputs.FILE_MULTILINE_SECRET }}
SECRET_WITH_FILE: ${{ steps.load_secrets_by_vault_id.outputs.SECRET_WITH_FILE }}
SECRET_WITH_FILE_IN_SECTION: ${{ steps.load_secrets_by_vault_id.outputs.SECRET_WITH_FILE_IN_SECTION }}
DOUBLE_SECTION_SECRET: ${{ steps.load_secrets_by_vault_id.outputs.DOUBLE_SECTION_SECRET }}
SSH_PRIVATE_KEY: ${{ steps.load_secrets_by_vault_id.outputs.SSH_PRIVATE_KEY }}
SSH_PRIVATE_KEY_OPENSSH: ${{ steps.load_secrets_by_vault_id.outputs.SSH_PRIVATE_KEY_OPENSSH }}
run: ./tests/assert-env-set.sh
test-connect:
name: Connect (ubuntu-latest, ${{ matrix.version }}, export-env=${{ matrix.export-env }})
runs-on: ubuntu-latest
strategy:
fail-fast: true
matrix:
os: [ubuntu-latest, macos-latest, windows-latest]
version: [latest, 2.30.0]
export-env: [true, false]
steps:
- name: Checkout
uses: actions/checkout@v5
uses: actions/checkout@v6
with:
fetch-depth: 0
ref: ${{ inputs.ref }}
- name: Setup Node.js
uses: actions/setup-node@v4
uses: actions/setup-node@v6
with:
node-version: 20
node-version: 24
cache: npm
- name: Install dependencies
@@ -232,29 +169,16 @@ jobs:
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
echo "SECRET_WITH_FILE=op://${{ secrets.VAULT }}/file-secret/test.txt" >> tests/.env.tpl
echo "SECRET_WITH_FILE_IN_SECTION=op://${{ secrets.VAULT }}/file-secret/file section/test.txt" >> tests/.env.tpl
echo "DOUBLE_SECTION_SECRET=op://${{ secrets.VAULT }}/double-section-secret/test-section/password" >> tests/.env.tpl
echo "SSH_PRIVATE_KEY=op://${{ secrets.VAULT }}/test-ssh-key/private key" >> tests/.env.tpl
echo "SSH_PRIVATE_KEY_OPENSSH=op://${{ secrets.VAULT }}/test-ssh-key/private key?ssh-format=openssh" >> tests/.env.tpl
- name: Generate .vaultId_env.tpl
run: |
echo "FILE_SECRET=op://${{ secrets.VAULT_ID }}/test-secret/password" > tests/.vaultId_env.tpl
echo "FILE_SECRET_IN_SECTION=op://${{ secrets.VAULT_ID }}/test-secret/test-section/password" >> tests/.vaultId_env.tpl
echo "FILE_MULTILINE_SECRET=op://${{ secrets.VAULT_ID }}/multiline-secret/notesPlain" >> tests/.vaultId_env.tpl
echo "SECRET_WITH_FILE=op://${{ secrets.VAULT_ID }}/file-secret/test.txt" >> tests/.vaultId_env.tpl
echo "SECRET_WITH_FILE_IN_SECTION=op://${{ secrets.VAULT_ID }}/file-secret/file section/test.txt" >> tests/.vaultId_env.tpl
echo "DOUBLE_SECTION_SECRET=op://${{ secrets.VAULT_ID }}/double-section-secret/test-section/password" >> tests/.vaultId_env.tpl
echo "SSH_PRIVATE_KEY=op://${{ secrets.VAULT_ID }}/test-ssh-key/private key" >> tests/.vaultId_env.tpl
echo "SSH_PRIVATE_KEY_OPENSSH=op://${{ secrets.VAULT_ID }}/test-ssh-key/private key?ssh-format=openssh" >> tests/.vaultId_env.tpl
echo "FILE_TEST_SSH_KEY=op://${{ secrets.VAULT }}/test-ssh-key/private key" >> tests/.env.tpl
echo "FILE_TEST_SSH_KEY_OPENSSH=op://${{ secrets.VAULT }}/test-ssh-key/private key?ssh-format=openssh" >> 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 30
docker compose -f tests/fixtures/docker-compose.yml up -d
timeout 60 bash -c 'until curl -sf http://localhost:8080/health >/dev/null 2>&1; do sleep 2; done'
- name: Configure 1Password Connect
uses: ./configure
@@ -272,45 +196,44 @@ jobs:
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
SECRET_WITH_FILE: op://${{ secrets.VAULT }}/file-secret/test.txt
SECRET_WITH_FILE_IN_SECTION: op://${{ secrets.VAULT }}/file-secret/file section/test.txt
DOUBLE_SECTION_SECRET: op://${{ secrets.VAULT }}/double-section-secret/test-section/password
SSH_PRIVATE_KEY: op://${{ secrets.VAULT }}/test-ssh-key/private key
SSH_PRIVATE_KEY_OPENSSH: op://${{ secrets.VAULT }}/test-ssh-key/private key?ssh-format=openssh
TEST_SSH_KEY: op://${{ secrets.VAULT }}/test-ssh-key/private key
TEST_SSH_KEY_OPENSSH: "op://${{ secrets.VAULT }}/test-ssh-key/private key?ssh-format=openssh"
OP_ENV_FILE: ./tests/.env.tpl
- name: Assert test secret values [step output]
if: ${{ !matrix.export-env }}
env:
ASSERT_WEBSITE: "false"
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 }}
SECRET_WITH_FILE: ${{ steps.load_secrets.outputs.SECRET_WITH_FILE }}
SECRET_WITH_FILE_IN_SECTION: ${{ steps.load_secrets.outputs.SECRET_WITH_FILE_IN_SECTION }}
DOUBLE_SECTION_SECRET: ${{ steps.load_secrets.outputs.DOUBLE_SECTION_SECRET }}
SSH_PRIVATE_KEY: ${{ steps.load_secrets.outputs.SSH_PRIVATE_KEY }}
SSH_PRIVATE_KEY_OPENSSH: ${{ steps.load_secrets.outputs.SSH_PRIVATE_KEY_OPENSSH }}
TEST_SSH_KEY: ${{ steps.load_secrets.outputs.TEST_SSH_KEY }}
FILE_TEST_SSH_KEY: ${{ steps.load_secrets.outputs.FILE_TEST_SSH_KEY }}
TEST_SSH_KEY_OPENSSH: ${{ steps.load_secrets.outputs.TEST_SSH_KEY_OPENSSH }}
FILE_TEST_SSH_KEY_OPENSSH: ${{ steps.load_secrets.outputs.FILE_TEST_SSH_KEY_OPENSSH }}
run: ./tests/assert-env-set.sh
- name: Assert SSH key env vars [step output]
if: ${{ !matrix.export-env }}
env:
TEST_SSH_KEY: ${{ steps.load_secrets.outputs.TEST_SSH_KEY }}
FILE_TEST_SSH_KEY: ${{ steps.load_secrets.outputs.FILE_TEST_SSH_KEY }}
TEST_SSH_KEY_OPENSSH: ${{ steps.load_secrets.outputs.TEST_SSH_KEY_OPENSSH }}
FILE_TEST_SSH_KEY_OPENSSH: ${{ steps.load_secrets.outputs.FILE_TEST_SSH_KEY_OPENSSH }}
run: ./tests/assert-ssh-keys-set.sh
- name: Assert test secret values [exported env]
if: ${{ matrix.export-env }}
env:
ASSERT_WEBSITE: "false"
run: ./tests/assert-env-set.sh
- name: Assert SSH keys [step output]
if: ${{ !matrix.export-env }}
shell: bash
env:
SSH_PRIVATE_KEY: ${{ steps.load_secrets.outputs.SSH_PRIVATE_KEY }}
SSH_PRIVATE_KEY_OPENSSH: ${{ steps.load_secrets.outputs.SSH_PRIVATE_KEY_OPENSSH }}
run: ./tests/assert-ssh-keys.sh
- name: Assert SSH keys [exported env]
- name: Assert SSH key env vars [exported env]
if: ${{ matrix.export-env }}
shell: bash
run: ./tests/assert-ssh-keys.sh
run: ./tests/assert-ssh-keys-set.sh
- name: Remove secrets [exported env]
if: ${{ matrix.export-env }}
@@ -321,48 +244,3 @@ jobs:
- name: Assert removed secrets [exported env]
if: ${{ matrix.export-env }}
run: ./tests/assert-env-unset.sh
- name: Load secrets (invalid ref - expect failure)
id: load_invalid
continue-on-error: true
uses: ./
env:
BAD_REF: "op://x"
with:
export-env: true
- name: Assert invalid ref failed
shell: bash
run: ./tests/assert-invalid-ref-failed.sh
env:
STEP_OUTCOME: ${{ steps.load_invalid.outcome }}
- name: Load secrets by vault ID
id: load_secrets_by_vault_id
uses: ./
with:
version: ${{ matrix.version }}
export-env: ${{ matrix.export-env }}
env:
SECRET: op://${{ secrets.VAULT_ID }}/test-secret/password
SECRET_IN_SECTION: op://${{ secrets.VAULT_ID }}/test-secret/test-section/password
MULTILINE_SECRET: op://${{ secrets.VAULT_ID }}/multiline-secret/notesPlain
SECRET_WITH_FILE: op://${{ secrets.VAULT_ID }}/file-secret/test.txt
SECRET_WITH_FILE_IN_SECTION: op://${{ secrets.VAULT_ID }}/file-secret/file section/test.txt
DOUBLE_SECTION_SECRET: op://${{ secrets.VAULT_ID }}/double-section-secret/test-section/password
OP_ENV_FILE: ./tests/.vaultId_env.tpl
- name: Assert test secret values [vault by ID]
if: ${{ !matrix.export-env }}
shell: bash
env:
SECRET: ${{ steps.load_secrets_by_vault_id.outputs.SECRET }}
SECRET_IN_SECTION: ${{ steps.load_secrets_by_vault_id.outputs.SECRET_IN_SECTION }}
MULTILINE_SECRET: ${{ steps.load_secrets_by_vault_id.outputs.MULTILINE_SECRET }}
FILE_SECRET: ${{ steps.load_secrets_by_vault_id.outputs.FILE_SECRET }}
FILE_SECRET_IN_SECTION: ${{ steps.load_secrets_by_vault_id.outputs.FILE_SECRET_IN_SECTION }}
FILE_MULTILINE_SECRET: ${{ steps.load_secrets_by_vault_id.outputs.FILE_MULTILINE_SECRET }}
SECRET_WITH_FILE: ${{ steps.load_secrets_by_vault_id.outputs.SECRET_WITH_FILE }}
SECRET_WITH_FILE_IN_SECTION: ${{ steps.load_secrets_by_vault_id.outputs.SECRET_WITH_FILE_IN_SECTION }}
DOUBLE_SECTION_SECRET: ${{ steps.load_secrets_by_vault_id.outputs.DOUBLE_SECTION_SECRET }}
run: ./tests/assert-env-set.sh

View File

@@ -9,7 +9,7 @@ jobs:
lint-and-test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v5
- uses: actions/checkout@v6
- name: Run ShellCheck
uses: ludeeus/action-shellcheck@2.0.0
@@ -18,9 +18,9 @@ jobs:
.husky
- name: Setup Node.js
uses: actions/setup-node@v4
uses: actions/setup-node@v6
with:
node-version: 20
node-version: 24
cache: npm
- name: Install dependencies

View File

@@ -91,7 +91,6 @@ jobs:
OP_CONNECT_TOKEN: ${{ secrets.OP_CONNECT_TOKEN }}
OP_SERVICE_ACCOUNT_TOKEN: ${{ secrets.OP_SERVICE_ACCOUNT_TOKEN }}
VAULT: ${{ secrets.VAULT }}
VAULT_ID: ${{ secrets.VAULT_ID }}
# Post comment on fork PRs after /ok-to-test
comment-pr:

View File

@@ -35,7 +35,7 @@ jobs:
- name: Load secret
id: load_secrets
uses: 1password/load-secrets-action@v3
uses: 1password/load-secrets-action@v4
env:
OP_SERVICE_ACCOUNT_TOKEN: ${{ secrets.OP_SERVICE_ACCOUNT_TOKEN }}
SECRET: op://app-cicd/hello-world/secret
@@ -57,7 +57,7 @@ jobs:
- uses: actions/checkout@v4
- name: Load secret
uses: 1password/load-secrets-action@v3
uses: 1password/load-secrets-action@v4
with:
# Export loaded secrets as environment variables
export-env: true
@@ -77,7 +77,7 @@ When loading SSH keys, you can specify the format using the `ssh-format` query p
```yml
- name: Load SSH key
uses: 1password/load-secrets-action@v3
uses: 1password/load-secrets-action@v4
env:
OP_SERVICE_ACCOUNT_TOKEN: ${{ secrets.OP_SERVICE_ACCOUNT_TOKEN }}
# Load SSH private key in OpenSSH format

View File

@@ -10,6 +10,11 @@ const jestConfig = {
rootDir: "../src/",
testEnvironment: "node",
testRegex: "(/__tests__/.*|(\\.|/)test)\\.ts",
moduleNameMapper: {
"^@actions/core$": "<rootDir>/__mocks__/actions-core.ts",
"^@actions/tool-cache$": "<rootDir>/__mocks__/actions-tool-cache.ts",
"^@actions/exec$": "<rootDir>/__mocks__/actions-exec.ts",
},
transform: {
".ts": [
"ts-jest",
@@ -25,4 +30,4 @@ const jestConfig = {
verbose: true,
};
export default jestConfig;
module.exports = jestConfig;

32768
configure/dist/index.js vendored

File diff suppressed because one or more lines are too long

View File

@@ -1,4 +1,4 @@
const core = require("@actions/core");
import * as core from "@actions/core";
const configure = () => {
const OP_CONNECT_HOST =

36363
dist/index.js vendored

File diff suppressed because one or more lines are too long

View File

@@ -17,8 +17,7 @@ This document explains how to run e2e tests locally using `act`.
| Secret | Description |
| -------------------------- | --------------------- |
| `OP_SERVICE_ACCOUNT_TOKEN` | Service Account token |
| `VAULT` | Vault name |
| `VAULT_ID` | Vault UUID |
| `VAULT` | Vault name or UUID |
## Building Before Testing

493
package-lock.json generated
View File

@@ -1,29 +1,25 @@
{
"name": "load-secrets-action",
"version": "3.1.0",
"version": "4.0.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "load-secrets-action",
"version": "3.1.0",
"version": "4.0.0",
"license": "MIT",
"dependencies": {
"@1password/connect": "^1.4.2",
"@1password/op-js": "^0.1.11",
"@1password/sdk": "^0.4.0",
"@actions/core": "^1.10.1",
"@actions/exec": "^1.1.1",
"@actions/tool-cache": "^2.0.2",
"dotenv": "^17.2.2",
"sshpk": "^1.18.0"
"@actions/core": "^3.0.0",
"@actions/exec": "^3.0.0",
"@actions/tool-cache": "^4.0.0",
"dotenv": "^17.2.2"
},
"devDependencies": {
"@1password/eslint-config": "^4.3.1",
"@1password/prettier-config": "^1.2.0",
"@types/jest": "^29.5.12",
"@types/node": "^20.11.30",
"@types/sshpk": "^1.17.4",
"@vercel/ncc": "^0.38.1",
"husky": "^9.0.11",
"jest": "^29.7.0",
@@ -32,19 +28,6 @@
"typescript": "^5.4.2"
}
},
"node_modules/@1password/connect": {
"version": "1.4.2",
"resolved": "https://registry.npmjs.org/@1password/connect/-/connect-1.4.2.tgz",
"integrity": "sha512-CxcDQIr76nloWwGWRrmz/U7DuU65WKrN/yarq45LrC3L6b/pC7bZyskvougadG32fRwBieLJX143lTI8T1bAtQ==",
"license": "MIT",
"dependencies": {
"axios": "^1.10.0",
"debug": "^4.4.1",
"lodash.clonedeep": "^4.5.0",
"slugify": "^1.6.6",
"uuid": "^9.0.1"
}
},
"node_modules/@1password/eslint-config": {
"version": "4.3.1",
"resolved": "https://registry.npmjs.org/@1password/eslint-config/-/eslint-config-4.3.1.tgz",
@@ -89,74 +72,50 @@
"prettier": "^2.0.0 || ^3.0.0"
}
},
"node_modules/@1password/sdk": {
"version": "0.4.0",
"resolved": "https://registry.npmjs.org/@1password/sdk/-/sdk-0.4.0.tgz",
"integrity": "sha512-RIypujc9R/UeUaobjyClTYokqRFpcaIkHq+EO/X9XoHId98Vg+SbjwGV+yygRC4MyHwYNo1KP1iEbZcqJ4ZTdw==",
"license": "MIT",
"dependencies": {
"@1password/sdk-core": "0.4.0"
}
},
"node_modules/@1password/sdk-core": {
"version": "0.4.0",
"resolved": "https://registry.npmjs.org/@1password/sdk-core/-/sdk-core-0.4.0.tgz",
"integrity": "sha512-vjeI1o4wiONY+t1naA4dtUp6HktdLH1D2S+tN1Lh4l41S9XIUHxrljov9B5u6G+VHr7f2MUoxmzXA9zT3aokQQ==",
"license": "MIT"
},
"node_modules/@actions/core": {
"version": "1.11.1",
"resolved": "https://registry.npmjs.org/@actions/core/-/core-1.11.1.tgz",
"integrity": "sha512-hXJCSrkwfA46Vd9Z3q4cpEpHB1rL5NG04+/rbqW9d3+CSvtB1tYe8UTpAlixa1vj0m/ULglfEK2UKxMGxCxv5A==",
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/@actions/core/-/core-3.0.0.tgz",
"integrity": "sha512-zYt6cz+ivnTmiT/ksRVriMBOiuoUpDCJJlZ5KPl2/FRdvwU3f7MPh9qftvbkXJThragzUZieit2nyHUyw53Seg==",
"license": "MIT",
"dependencies": {
"@actions/exec": "^1.1.1",
"@actions/http-client": "^2.0.1"
"@actions/exec": "^3.0.0",
"@actions/http-client": "^4.0.0"
}
},
"node_modules/@actions/exec": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/@actions/exec/-/exec-1.1.1.tgz",
"integrity": "sha512-+sCcHHbVdk93a0XT19ECtO/gIXoxvdsgQLzb2fE2/5sIZmWQuluYyjPQtrtTHdU1YzTZ7bAPN4sITq2xi1679w==",
"license": "MIT",
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/@actions/exec/-/exec-3.0.0.tgz",
"integrity": "sha512-6xH/puSoNBXb72VPlZVm7vQ+svQpFyA96qdDBvhB8eNZOE8LtPf9L4oAsfzK/crCL8YZ+19fKYVnM63Sl+Xzlw==",
"dependencies": {
"@actions/io": "^1.0.1"
"@actions/io": "^3.0.2"
}
},
"node_modules/@actions/http-client": {
"version": "2.2.3",
"resolved": "https://registry.npmjs.org/@actions/http-client/-/http-client-2.2.3.tgz",
"integrity": "sha512-mx8hyJi/hjFvbPokCg4uRd4ZX78t+YyRPtnKWwIl+RzNaVuFpQHfmlGVfsKEJN8LwTCvL+DfVgAM04XaHkm6bA==",
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/@actions/http-client/-/http-client-4.0.0.tgz",
"integrity": "sha512-QuwPsgVMsD6qaPD57GLZi9sqzAZCtiJT8kVBCDpLtxhL5MydQ4gS+DrejtZZPdIYyB1e95uCK9Luyds7ybHI3g==",
"license": "MIT",
"dependencies": {
"tunnel": "^0.0.6",
"undici": "^5.25.4"
"undici": "^6.23.0"
}
},
"node_modules/@actions/io": {
"version": "1.1.3",
"resolved": "https://registry.npmjs.org/@actions/io/-/io-1.1.3.tgz",
"integrity": "sha512-wi9JjgKLYS7U/z8PPbco+PvTb/nRWjeoFlJ1Qer83k/3C5PHQi28hiVdeE2kHXmIL99mQFawx8qt/JPjZilJ8Q==",
"license": "MIT"
"version": "3.0.2",
"resolved": "https://registry.npmjs.org/@actions/io/-/io-3.0.2.tgz",
"integrity": "sha512-nRBchcMM+QK1pdjO7/idu86rbJI5YHUKCvKs0KxnSYbVe3F51UfGxuZX4Qy/fWlp6l7gWFwIkrOzN+oUK03kfw=="
},
"node_modules/@actions/tool-cache": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/@actions/tool-cache/-/tool-cache-2.0.2.tgz",
"integrity": "sha512-fBhNNOWxuoLxztQebpOaWu6WeVmuwa77Z+DxIZ1B+OYvGkGQon6kTVg6Z32Cb13WCuw0szqonK+hh03mJV7Z6w==",
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/@actions/tool-cache/-/tool-cache-4.0.0.tgz",
"integrity": "sha512-L8P9HbXvpvqjZDveb/fdsa55IVC0trfPgQ4ZwGo6r5af6YDVdM9vMGPZ7rgY2fAT9gGj4PSYd6bYlg3p3jD78A==",
"license": "MIT",
"dependencies": {
"@actions/core": "^1.11.1",
"@actions/exec": "^1.0.0",
"@actions/http-client": "^2.0.1",
"@actions/io": "^1.1.1",
"semver": "^6.1.0"
}
},
"node_modules/@actions/tool-cache/node_modules/semver": {
"version": "6.3.1",
"resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz",
"integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==",
"bin": {
"semver": "bin/semver.js"
"@actions/core": "^3.0.0",
"@actions/exec": "^3.0.0",
"@actions/http-client": "^4.0.0",
"@actions/io": "^3.0.0",
"semver": "^7.7.3"
}
},
"node_modules/@ampproject/remapping": {
@@ -791,15 +750,6 @@
"node": "^12.22.0 || ^14.17.0 || >=16.0.0"
}
},
"node_modules/@fastify/busboy": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/@fastify/busboy/-/busboy-2.1.1.tgz",
"integrity": "sha512-vBZP4NlzfOlerQTnba4aqZoMhE/a9HY7HRqoOPaETQcSQuWEIyZMHGfVu6w9wGtGK5fED5qRs2DteVCjOH60sA==",
"license": "MIT",
"engines": {
"node": ">=14"
}
},
"node_modules/@humanwhocodes/config-array": {
"version": "0.13.0",
"resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.13.0.tgz",
@@ -1304,15 +1254,6 @@
"@sinonjs/commons": "^3.0.0"
}
},
"node_modules/@types/asn1": {
"version": "0.2.4",
"resolved": "https://registry.npmjs.org/@types/asn1/-/asn1-0.2.4.tgz",
"integrity": "sha512-V91DSJ2l0h0gRhVP4oBfBzRBN9lAbPUkGDMCnwedqPKX2d84aAMc9CulOvxdw1f7DfEYx99afab+Rsm3e52jhA==",
"dev": true,
"dependencies": {
"@types/node": "*"
}
},
"node_modules/@types/babel__core": {
"version": "7.20.5",
"resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz",
@@ -1437,16 +1378,6 @@
"dev": true,
"license": "MIT"
},
"node_modules/@types/sshpk": {
"version": "1.17.4",
"resolved": "https://registry.npmjs.org/@types/sshpk/-/sshpk-1.17.4.tgz",
"integrity": "sha512-5gI/7eJn6wmkuIuFY8JZJ1g5b30H9K5U5vKrvOuYu+hoZLb2xcVEgxhYZ2Vhbs0w/ACyzyfkJq0hQtBfSCugjw==",
"dev": true,
"dependencies": {
"@types/asn1": "*",
"@types/node": "*"
}
},
"node_modules/@types/stack-utils": {
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/@types/stack-utils/-/stack-utils-2.0.3.tgz",
@@ -1625,32 +1556,6 @@
}
}
},
"node_modules/@typescript-eslint/typescript-estree/node_modules/brace-expansion": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz",
"integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"balanced-match": "^1.0.0"
}
},
"node_modules/@typescript-eslint/typescript-estree/node_modules/minimatch": {
"version": "9.0.3",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.3.tgz",
"integrity": "sha512-RHiac9mvaRw0x3AYRgDC1CxAP7HTcNrrECeA8YYJeWnpo+2Q5CegtZjaotWTWxDG3UeGA1coE05iH1mPjT/2mg==",
"dev": true,
"license": "ISC",
"dependencies": {
"brace-expansion": "^2.0.1"
},
"engines": {
"node": ">=16 || 14 >=14.17"
},
"funding": {
"url": "https://github.com/sponsors/isaacs"
}
},
"node_modules/@typescript-eslint/utils": {
"version": "6.21.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-6.21.0.tgz",
@@ -1739,11 +1644,10 @@
}
},
"node_modules/ajv": {
"version": "6.12.6",
"resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz",
"integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==",
"version": "6.14.0",
"resolved": "https://registry.npmjs.org/ajv/-/ajv-6.14.0.tgz",
"integrity": "sha512-IWrosm/yrn43eiKqkfkHis7QioDleaXQHdDVPKg0FSwwd/DuvyX79TZnFOnYpB7dcsFAMmtFztZuXPDvSePkFw==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"fast-deep-equal": "^3.1.1",
@@ -2001,22 +1905,6 @@
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/asn1": {
"version": "0.2.6",
"resolved": "https://registry.npmjs.org/asn1/-/asn1-0.2.6.tgz",
"integrity": "sha512-ix/FxPn0MDjeyJ7i/yoHGFt/EX6LyNbxSEhPPXODPL+KB0VPk86UYfL0lMdy+KCnv+fmvIzySwaK5COwqVbWTQ==",
"dependencies": {
"safer-buffer": "~2.1.0"
}
},
"node_modules/assert-plus": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz",
"integrity": "sha512-NfJ4UzBCcQGLDlQq7nHxH+tv3kyZ0hHQqF5BO6J7tNJeP5do1llPr8dZ8zHonfhAu0PHAdMkSo+8o0wxg9lZWw==",
"engines": {
"node": ">=0.8"
}
},
"node_modules/ast-types-flow": {
"version": "0.0.8",
"resolved": "https://registry.npmjs.org/ast-types-flow/-/ast-types-flow-0.0.8.tgz",
@@ -2031,12 +1919,6 @@
"dev": true,
"license": "MIT"
},
"node_modules/asynckit": {
"version": "0.4.0",
"resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
"integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==",
"license": "MIT"
},
"node_modules/available-typed-arrays": {
"version": "1.0.7",
"resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz",
@@ -2063,17 +1945,6 @@
"node": ">=4"
}
},
"node_modules/axios": {
"version": "1.13.5",
"resolved": "https://registry.npmjs.org/axios/-/axios-1.13.5.tgz",
"integrity": "sha512-cz4ur7Vb0xS4/KUN0tPWe44eqxrIu31me+fbang3ijiNscE129POzipJJA6zniq2C/Z6sJCjMimjS8Lc/GAs8Q==",
"license": "MIT",
"dependencies": {
"follow-redirects": "^1.15.11",
"form-data": "^4.0.5",
"proxy-from-env": "^1.1.0"
}
},
"node_modules/axobject-query": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-4.1.0.tgz",
@@ -2217,23 +2088,14 @@
"dev": true,
"license": "MIT"
},
"node_modules/bcrypt-pbkdf": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz",
"integrity": "sha512-qeFIXtP4MSoi6NLqO12WfqARWWuCKi2Rn/9hJLEmtB5yTNr9DqFWkJRCf2qShWzPeAMRnOgCrq0sg/KLv5ES9w==",
"dependencies": {
"tweetnacl": "^0.14.3"
}
},
"node_modules/brace-expansion": {
"version": "1.1.12",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz",
"integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==",
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz",
"integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"balanced-match": "^1.0.0",
"concat-map": "0.0.1"
"balanced-match": "^1.0.0"
}
},
"node_modules/braces": {
@@ -2348,6 +2210,7 @@
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.1.tgz",
"integrity": "sha512-BhYE+WDaywFg2TBWYNXAE+8B1ATnThNBqXHP5nQu0jWJdVvY2hvkpyB3qOmtmDePiS5/BDQ8wASEWGMWRG148g==",
"dev": true,
"license": "MIT",
"dependencies": {
"es-errors": "^1.3.0",
@@ -2592,18 +2455,6 @@
"dev": true,
"license": "MIT"
},
"node_modules/combined-stream": {
"version": "1.0.8",
"resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz",
"integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==",
"license": "MIT",
"dependencies": {
"delayed-stream": "~1.0.0"
},
"engines": {
"node": ">= 0.8"
}
},
"node_modules/commander": {
"version": "12.1.0",
"resolved": "https://registry.npmjs.org/commander/-/commander-12.1.0.tgz",
@@ -2624,13 +2475,6 @@
"node": ">= 12.0.0"
}
},
"node_modules/concat-map": {
"version": "0.0.1",
"resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
"integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==",
"dev": true,
"license": "MIT"
},
"node_modules/convert-source-map": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz",
@@ -2682,17 +2526,6 @@
"dev": true,
"license": "BSD-2-Clause"
},
"node_modules/dashdash": {
"version": "1.14.1",
"resolved": "https://registry.npmjs.org/dashdash/-/dashdash-1.14.1.tgz",
"integrity": "sha512-jRFi8UDGo6j+odZiEpjazZaWqEal3w/basFjQHQEwVtZJGDpxbH1MeYluwCS8Xq5wmLJooDlMgvVarmWfGM44g==",
"dependencies": {
"assert-plus": "^1.0.0"
},
"engines": {
"node": ">=0.10"
}
},
"node_modules/data-view-buffer": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/data-view-buffer/-/data-view-buffer-1.0.1.tgz",
@@ -2748,9 +2581,10 @@
}
},
"node_modules/debug": {
"version": "4.4.3",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
"integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==",
"version": "4.4.0",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz",
"integrity": "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==",
"dev": true,
"license": "MIT",
"dependencies": {
"ms": "^2.1.3"
@@ -2833,15 +2667,6 @@
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/delayed-stream": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
"integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==",
"license": "MIT",
"engines": {
"node": ">=0.4.0"
}
},
"node_modules/detect-newline": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/detect-newline/-/detect-newline-3.1.0.tgz",
@@ -2905,6 +2730,7 @@
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",
"integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==",
"dev": true,
"license": "MIT",
"dependencies": {
"call-bind-apply-helpers": "^1.0.1",
@@ -2915,15 +2741,6 @@
"node": ">= 0.4"
}
},
"node_modules/ecc-jsbn": {
"version": "0.1.2",
"resolved": "https://registry.npmjs.org/ecc-jsbn/-/ecc-jsbn-0.1.2.tgz",
"integrity": "sha512-eh9O+hwRHNbG4BLTjEl3nw044CkGm5X6LoaCf7LPp7UU8Qrt47JYNi6nPX8xjW97TKGKm1ouctg0QSpZe9qrnw==",
"dependencies": {
"jsbn": "~0.1.0",
"safer-buffer": "^2.1.0"
}
},
"node_modules/ejs": {
"version": "3.1.10",
"resolved": "https://registry.npmjs.org/ejs/-/ejs-3.1.10.tgz",
@@ -3057,6 +2874,7 @@
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz",
"integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">= 0.4"
@@ -3066,6 +2884,7 @@
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz",
"integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">= 0.4"
@@ -3102,6 +2921,7 @@
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.0.0.tgz",
"integrity": "sha512-MZ4iQ6JwHOBQjahnjwaC1ZtIBH+2ohjamzAO3oaHcXYup7qxjF2fixyH+Q71voWHeOkI2q/TnJao/KfXYIZWbw==",
"dev": true,
"license": "MIT",
"dependencies": {
"es-errors": "^1.3.0"
@@ -3111,15 +2931,15 @@
}
},
"node_modules/es-set-tostringtag": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz",
"integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==",
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.0.3.tgz",
"integrity": "sha512-3T8uNMC3OQTHkFUsFq8r/BwAXLHvU/9O9mE0fBc/MY5iq/8H7ncvO947LmYA6ldWw9Uh8Yhf25zu6n7nML5QWQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"es-errors": "^1.3.0",
"get-intrinsic": "^1.2.6",
"get-intrinsic": "^1.2.4",
"has-tostringtag": "^1.0.2",
"hasown": "^2.0.2"
"hasown": "^2.0.1"
},
"engines": {
"node": ">= 0.4"
@@ -3878,29 +3698,6 @@
"minimatch": "^5.0.1"
}
},
"node_modules/filelist/node_modules/brace-expansion": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz",
"integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"balanced-match": "^1.0.0"
}
},
"node_modules/filelist/node_modules/minimatch": {
"version": "5.1.6",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.6.tgz",
"integrity": "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==",
"dev": true,
"license": "ISC",
"dependencies": {
"brace-expansion": "^2.0.1"
},
"engines": {
"node": ">=10"
}
},
"node_modules/fill-range": {
"version": "7.1.1",
"resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz",
@@ -3945,33 +3742,12 @@
}
},
"node_modules/flatted": {
"version": "3.3.2",
"resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.2.tgz",
"integrity": "sha512-AiwGJM8YcNOaobumgtng+6NHuOqC3A7MixFeDafM3X9cIUM+xUXoS5Vfgf+OihAYe20fxqNM9yPBXJzRtZ/4eA==",
"version": "3.4.2",
"resolved": "https://registry.npmjs.org/flatted/-/flatted-3.4.2.tgz",
"integrity": "sha512-PjDse7RzhcPkIJwy5t7KPWQSZ9cAbzQXcafsetQoD7sOJRQlGikNbx7yZp2OotDnJyrDcbyRq3Ttb18iYOqkxA==",
"dev": true,
"license": "ISC",
"peer": true
},
"node_modules/follow-redirects": {
"version": "1.15.11",
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.11.tgz",
"integrity": "sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ==",
"funding": [
{
"type": "individual",
"url": "https://github.com/sponsors/RubenVerborgh"
}
],
"license": "MIT",
"engines": {
"node": ">=4.0"
},
"peerDependenciesMeta": {
"debug": {
"optional": true
}
}
},
"node_modules/for-each": {
"version": "0.3.3",
"resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.3.tgz",
@@ -3982,22 +3758,6 @@
"is-callable": "^1.1.3"
}
},
"node_modules/form-data": {
"version": "4.0.5",
"resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.5.tgz",
"integrity": "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==",
"license": "MIT",
"dependencies": {
"asynckit": "^0.4.0",
"combined-stream": "^1.0.8",
"es-set-tostringtag": "^2.1.0",
"hasown": "^2.0.2",
"mime-types": "^2.1.12"
},
"engines": {
"node": ">= 6"
}
},
"node_modules/fs.realpath": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz",
@@ -4024,6 +3784,7 @@
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
"integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==",
"dev": true,
"license": "MIT",
"funding": {
"url": "https://github.com/sponsors/ljharb"
@@ -4096,6 +3857,7 @@
"version": "1.2.6",
"resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.6.tgz",
"integrity": "sha512-qxsEs+9A+u85HhllWJJFicJfPDhRmjzoYdl64aMWW9yRIJmSyxdn8IEkuIM530/7T+lv0TIHd8L6Q/ra0tEoeA==",
"dev": true,
"license": "MIT",
"dependencies": {
"call-bind-apply-helpers": "^1.0.1",
@@ -4157,14 +3919,6 @@
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/getpass": {
"version": "0.1.7",
"resolved": "https://registry.npmjs.org/getpass/-/getpass-0.1.7.tgz",
"integrity": "sha512-0fzj9JxOLfJ+XGLhR8ze3unN0KZCgZwiSSDz168VERjK8Wl8kVSdcu2kspd4s4wtAa1y/qrVRiAA0WclVsu0ng==",
"dependencies": {
"assert-plus": "^1.0.0"
}
},
"node_modules/glob": {
"version": "7.2.3",
"resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz",
@@ -4260,6 +4014,7 @@
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz",
"integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">= 0.4"
@@ -4345,6 +4100,7 @@
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz",
"integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">= 0.4"
@@ -4357,6 +4113,7 @@
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz",
"integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==",
"dev": true,
"license": "MIT",
"dependencies": {
"has-symbols": "^1.0.3"
@@ -4372,6 +4129,7 @@
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz",
"integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"function-bind": "^1.1.2"
@@ -5717,11 +5475,6 @@
"js-yaml": "bin/js-yaml.js"
}
},
"node_modules/jsbn": {
"version": "0.1.1",
"resolved": "https://registry.npmjs.org/jsbn/-/jsbn-0.1.1.tgz",
"integrity": "sha512-UVU9dibq2JcFWxQPA6KCqj5O42VOmAY3zQUfEKxU0KpTGXwNoCjkX1e13eHNvw/xPynt6pU0rZ1htjWTNTSXsg=="
},
"node_modules/jsdoc-type-pratt-parser": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/jsdoc-type-pratt-parser/-/jsdoc-type-pratt-parser-4.0.0.tgz",
@@ -6107,12 +5860,6 @@
"node": ">=8"
}
},
"node_modules/lodash.clonedeep": {
"version": "4.5.0",
"resolved": "https://registry.npmjs.org/lodash.clonedeep/-/lodash.clonedeep-4.5.0.tgz",
"integrity": "sha512-H5ZhCF25riFd9uB5UCkVKo61m3S/xZk1x4wA6yp/L3RFP6Z/eHH1ymQcGLo7J3GMPfm0V/7m1tryHuGVxpqEBQ==",
"license": "MIT"
},
"node_modules/lodash.memoize": {
"version": "4.1.2",
"resolved": "https://registry.npmjs.org/lodash.memoize/-/lodash.memoize-4.1.2.tgz",
@@ -6311,6 +6058,7 @@
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.0.0.tgz",
"integrity": "sha512-4MqMiKP90ybymYvsut0CH2g4XWbfLtmlCkXmtmdcDCxNB+mQcu1w/1+L/VD7vi/PSv7X2JYV7SCcR+jiPXnQtA==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">= 0.4"
@@ -6347,27 +6095,6 @@
"node": ">=8.6"
}
},
"node_modules/mime-db": {
"version": "1.52.0",
"resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
"integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==",
"license": "MIT",
"engines": {
"node": ">= 0.6"
}
},
"node_modules/mime-types": {
"version": "2.1.35",
"resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz",
"integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==",
"license": "MIT",
"dependencies": {
"mime-db": "1.52.0"
},
"engines": {
"node": ">= 0.6"
}
},
"node_modules/mimic-fn": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz",
@@ -6392,16 +6119,19 @@
}
},
"node_modules/minimatch": {
"version": "3.1.2",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
"integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==",
"version": "9.0.9",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.9.tgz",
"integrity": "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==",
"dev": true,
"license": "ISC",
"dependencies": {
"brace-expansion": "^1.1.7"
"brace-expansion": "^2.0.2"
},
"engines": {
"node": "*"
"node": ">=16 || 14 >=14.17"
},
"funding": {
"url": "https://github.com/sponsors/isaacs"
}
},
"node_modules/minimist": {
@@ -6418,6 +6148,7 @@
"version": "2.1.3",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
"dev": true,
"license": "MIT"
},
"node_modules/natural-compare": {
@@ -6903,12 +6634,6 @@
"dev": true,
"license": "MIT"
},
"node_modules/proxy-from-env": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz",
"integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==",
"license": "MIT"
},
"node_modules/punycode": {
"version": "2.3.1",
"resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz",
@@ -7223,15 +6948,10 @@
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/safer-buffer": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
"integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="
},
"node_modules/semver": {
"version": "7.6.3",
"resolved": "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz",
"integrity": "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==",
"version": "7.7.4",
"resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz",
"integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==",
"license": "ISC",
"bin": {
"semver": "bin/semver.js"
@@ -7427,15 +7147,6 @@
"url": "https://github.com/chalk/ansi-styles?sponsor=1"
}
},
"node_modules/slugify": {
"version": "1.6.6",
"resolved": "https://registry.npmjs.org/slugify/-/slugify-1.6.6.tgz",
"integrity": "sha512-h+z7HKHYXj6wJU+AnS/+IH8Uh9fdcX1Lrhg1/VMdf9PwoBQXFcXiAdsy2tSK0P6gKwJLXp02r90ahUCqHk9rrw==",
"license": "MIT",
"engines": {
"node": ">=8.0.0"
}
},
"node_modules/source-map": {
"version": "0.6.1",
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
@@ -7489,30 +7200,6 @@
"dev": true,
"license": "BSD-3-Clause"
},
"node_modules/sshpk": {
"version": "1.18.0",
"resolved": "https://registry.npmjs.org/sshpk/-/sshpk-1.18.0.tgz",
"integrity": "sha512-2p2KJZTSqQ/I3+HX42EpYOa2l3f8Erv8MWKsy2I9uf4wA7yFIkXRffYdsx86y6z4vHtV8u7g+pPlr8/4ouAxsQ==",
"dependencies": {
"asn1": "~0.2.3",
"assert-plus": "^1.0.0",
"bcrypt-pbkdf": "^1.0.0",
"dashdash": "^1.12.0",
"ecc-jsbn": "~0.1.1",
"getpass": "^0.1.1",
"jsbn": "~0.1.0",
"safer-buffer": "^2.0.2",
"tweetnacl": "~0.14.0"
},
"bin": {
"sshpk-conv": "bin/sshpk-conv",
"sshpk-sign": "bin/sshpk-sign",
"sshpk-verify": "bin/sshpk-verify"
},
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/stack-utils": {
"version": "2.0.6",
"resolved": "https://registry.npmjs.org/stack-utils/-/stack-utils-2.0.6.tgz",
@@ -7981,11 +7668,6 @@
"node": ">=0.6.11 <=0.7.0 || >=0.7.3"
}
},
"node_modules/tweetnacl": {
"version": "0.14.5",
"resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-0.14.5.tgz",
"integrity": "sha512-KXXFFdAbFXY4geFIwoyNK+f5Z1b7swfXABfL7HXCmoIWMKU3dmS26672A4EeQtDzLKy7SXmfBu51JolvEKwtGA=="
},
"node_modules/type-check": {
"version": "0.4.0",
"resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz",
@@ -8136,15 +7818,11 @@
}
},
"node_modules/undici": {
"version": "5.29.0",
"resolved": "https://registry.npmjs.org/undici/-/undici-5.29.0.tgz",
"integrity": "sha512-raqeBD6NQK4SkWhQzeYKd1KmIG6dllBOTt55Rmkt4HtI9mwdWtJljnrXjAFUBLTSN67HWrOIZ3EPF4kjUw80Bg==",
"license": "MIT",
"dependencies": {
"@fastify/busboy": "^2.0.0"
},
"version": "6.24.1",
"resolved": "https://registry.npmjs.org/undici/-/undici-6.24.1.tgz",
"integrity": "sha512-sC+b0tB1whOCzbtlx20fx3WgCXwkW627p4EA9uM+/tNNPkSS+eSEld6pAs9nDv7WbY1UUljBMYPtu9BCOrCWKA==",
"engines": {
"node": ">=14.0"
"node": ">=18.17"
}
},
"node_modules/undici-types": {
@@ -8196,19 +7874,6 @@
"punycode": "^2.1.0"
}
},
"node_modules/uuid": {
"version": "9.0.1",
"resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.1.tgz",
"integrity": "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==",
"funding": [
"https://github.com/sponsors/broofa",
"https://github.com/sponsors/ctavan"
],
"license": "MIT",
"bin": {
"uuid": "dist/bin/uuid"
}
},
"node_modules/v8-to-istanbul": {
"version": "9.3.0",
"resolved": "https://registry.npmjs.org/v8-to-istanbul/-/v8-to-istanbul-9.3.0.tgz",

View File

@@ -1,6 +1,6 @@
{
"name": "load-secrets-action",
"version": "3.1.0",
"version": "4.0.0",
"description": "Load Secrets from 1Password",
"main": "dist/index.js",
"directories": {
@@ -41,20 +41,19 @@
"homepage": "https://github.com/1Password/load-secrets-action#readme",
"dependencies": {
"@1password/op-js": "^0.1.11",
"@1password/sdk": "^0.4.0",
"@1password/connect": "^1.4.2",
"@actions/core": "^1.10.1",
"@actions/exec": "^1.1.1",
"@actions/tool-cache": "^2.0.2",
"dotenv": "^17.2.2",
"sshpk": "^1.18.0"
"@actions/core": "^3.0.0",
"@actions/exec": "^3.0.0",
"@actions/tool-cache": "^4.0.0",
"dotenv": "^17.2.2"
},
"overrides": {
"minimatch": "^9.0.7"
},
"devDependencies": {
"@1password/eslint-config": "^4.3.1",
"@1password/prettier-config": "^1.2.0",
"@types/jest": "^29.5.12",
"@types/node": "^20.11.30",
"@types/sshpk": "^1.17.4",
"@vercel/ncc": "^0.38.1",
"husky": "^9.0.11",
"jest": "^29.7.0",

View File

@@ -0,0 +1,14 @@
module.exports = {
getInput: jest.fn(() => ""),
getBooleanInput: jest.fn(() => false),
setOutput: jest.fn(),
setSecret: jest.fn(),
exportVariable: jest.fn(),
setFailed: jest.fn(),
info: jest.fn(),
warning: jest.fn(),
error: jest.fn(),
debug: jest.fn(),
addPath: jest.fn(),
isDebug: jest.fn(() => false),
};

View File

@@ -0,0 +1,5 @@
module.exports = {
getExecOutput: jest.fn(() => ({
stdout: "MOCK_SECRET",
})),
};

View File

@@ -0,0 +1,10 @@
module.exports = {
downloadTool: jest.fn(),
extractTar: jest.fn(),
extractZip: jest.fn(),
cacheDir: jest.fn<Promise<string>, [string]>(async (dir) => {
await Promise.resolve();
return dir;
}),
find: jest.fn<string, [string, string?, string?]>(() => ""),
};

View File

@@ -1,5 +1,7 @@
import dotenv from "dotenv";
import * as core from "@actions/core";
import { validateCli } from "@1password/op-js";
import { installCliOnGithubActionRunner } from "./op-cli-installer";
import { loadSecrets, unsetPrevious, validateAuth } from "./utils";
import { envFilePath } from "./constants";
@@ -24,6 +26,9 @@ const loadSecretsAction = async () => {
dotenv.config({ path: file });
}
// Download and install the CLI
await installCLI();
// Load secrets
await loadSecrets(shouldExportEnv);
} catch (error) {
@@ -34,10 +39,22 @@ const loadSecretsAction = async () => {
if (error instanceof Error) {
message = error.message;
} else {
message = String(error);
String(error);
}
core.setFailed(message);
}
};
// This function's name is an exception from the naming convention
// since we refer to the 1Password CLI here.
// eslint-disable-next-line @typescript-eslint/naming-convention
const installCLI = async (): Promise<void> => {
// validateCli checks if there's an existing 1Password CLI installed on the runner.
// If there's no CLI installed, then validateCli will throw an error, which we will use
// as an indicator that we need to execute the installation script.
await validateCli().catch(async () => {
await installCliOnGithubActionRunner();
});
};
void loadSecretsAction();

View File

@@ -1,17 +1,11 @@
import * as core from "@actions/core";
import * as exec from "@actions/exec";
import { read } from "@1password/op-js";
import { createClient, Secrets } from "@1password/sdk";
import { OnePasswordConnect, FullItem } from "@1password/connect";
import { read, setClientInfo } from "@1password/op-js";
import {
extractSecret,
loadSecrets,
unsetPrevious,
validateAuth,
findMatchingFieldAndFile,
findSectionIdsByQuery,
parseOpRef,
getEnvVarNamesWithSecretRefs,
} from "./utils";
import {
authErr,
@@ -21,21 +15,7 @@ import {
envServiceAccountToken,
} from "./constants";
jest.mock("@actions/core");
jest.mock("@actions/exec", () => ({
getExecOutput: jest.fn(() => ({
stdout: "MOCK_SECRET",
})),
}));
jest.mock("@1password/op-js");
jest.mock("@1password/sdk", () => ({
createClient: jest.fn(),
// eslint-disable-next-line @typescript-eslint/naming-convention
Secrets: {
validateSecretReference: jest.fn(),
},
}));
jest.mock("@1password/connect");
beforeEach(() => {
jest.clearAllMocks();
@@ -157,227 +137,34 @@ describe("extractSecret", () => {
});
});
describe("loadSecrets when using Connect", () => {
beforeEach(() => {
process.env[envConnectHost] = "https://connect.example";
process.env[envConnectToken] = "test-token";
process.env[envServiceAccountToken] = "";
Object.keys(process.env).forEach((key) => {
if (
typeof process.env[key] === "string" &&
process.env[key]?.startsWith("op://")
) {
delete process.env[key];
}
});
process.env.MY_SECRET = "op://vault/item/field";
(OnePasswordConnect as jest.Mock).mockReturnValue({
getVault: jest.fn().mockResolvedValue({ id: "vault-id-123" }),
getItem: jest.fn().mockResolvedValue({
fields: [
{ label: "field", value: "resolved-via-connect", section: undefined },
],
sections: [],
}),
});
});
it("resolves ref via Connect SDK and exports secret", async () => {
describe("loadSecrets", () => {
it("sets the client info and gets the executed output", async () => {
await loadSecrets(true);
expect(setClientInfo).toHaveBeenCalledWith({
name: "1Password GitHub Action",
id: "GHA",
});
expect(exec.getExecOutput).toHaveBeenCalledWith('sh -c "op env ls"');
expect(core.exportVariable).toHaveBeenCalledWith(
"MY_SECRET",
"resolved-via-connect",
);
expect(core.exportVariable).toHaveBeenCalledWith(
envManagedVariables,
"MY_SECRET",
"OP_MANAGED_VARIABLES",
"MOCK_SECRET",
);
});
it("return early if no env vars with secrets found", async () => {
delete process.env.MY_SECRET;
(exec.getExecOutput as jest.Mock).mockReturnValueOnce({ stdout: "" });
await loadSecrets(true);
expect(exec.getExecOutput).toHaveBeenCalledWith('sh -c "op env ls"');
expect(core.exportVariable).not.toHaveBeenCalled();
});
it("sets step output when shouldExportEnv is false", async () => {
await loadSecrets(false);
expect(core.setOutput).toHaveBeenCalledWith(
"MY_SECRET",
"resolved-via-connect",
);
expect(core.exportVariable).not.toHaveBeenCalled();
});
it("masks resolved secret with setSecret", async () => {
await loadSecrets(true);
expect(core.setSecret).toHaveBeenCalledWith("resolved-via-connect");
});
it("calls getVault with vault segment from ref", async () => {
process.env.MY_SECRET = "op://my-vault-name/my-item/field";
const mockGetVault = jest.fn().mockResolvedValue({ id: "vault-uuid" });
const mockGetItem = jest.fn().mockResolvedValue({
fields: [{ label: "field", value: "secret-value", section: undefined }],
sections: [],
});
(OnePasswordConnect as jest.Mock).mockReturnValue({
getVault: mockGetVault,
getItem: mockGetItem,
});
await loadSecrets(false);
expect(mockGetVault).toHaveBeenCalledWith("my-vault-name");
});
it("throws when getVault returns vault without id", async () => {
const mockGetVault = jest.fn().mockResolvedValue({});
(OnePasswordConnect as jest.Mock).mockReturnValue({
getVault: mockGetVault,
getItem: jest.fn(),
});
await expect(loadSecrets(true)).rejects.toThrow(
/Could not find valid vault "vault" for ref "op:\/\/vault\/item\/field"/,
);
expect(mockGetVault).toHaveBeenCalledWith("vault");
});
it("resolves vault by name and uses returned id for getItem", async () => {
process.env.MY_SECRET = "op://My Vault/My Item/field";
const mockGetVault = jest
.fn()
.mockResolvedValue({ id: "uuid-for-my-vault" });
const mockGetItem = jest.fn().mockResolvedValue({
fields: [
{
label: "field",
value: "secret-from-named-vault",
section: undefined,
},
],
sections: [],
});
(OnePasswordConnect as jest.Mock).mockReturnValue({
getVault: mockGetVault,
getItem: mockGetItem,
});
await loadSecrets(true);
expect(mockGetVault).toHaveBeenCalledWith("My Vault");
expect(mockGetItem).toHaveBeenCalledWith("uuid-for-my-vault", "My Item");
expect(core.exportVariable).toHaveBeenCalledWith(
"MY_SECRET",
"secret-from-named-vault",
);
});
it("calls getItem with vault id from getVault, not ref vault segment", async () => {
const mockGetVault = jest
.fn()
.mockResolvedValue({ id: "resolved-vault-id" });
const mockGetItem = jest.fn().mockResolvedValue({
fields: [
{ label: "field", value: "resolved-via-connect", section: undefined },
],
sections: [],
});
(OnePasswordConnect as jest.Mock).mockReturnValue({
getVault: mockGetVault,
getItem: mockGetItem,
});
await loadSecrets(true);
expect(mockGetVault).toHaveBeenCalledWith("vault");
expect(mockGetItem).toHaveBeenCalledWith("resolved-vault-id", "item");
});
it("rejects when getItem fails", async () => {
const mockGetVault = jest.fn().mockResolvedValue({ id: "vault-id-123" });
const mockGetItem = jest
.fn()
.mockRejectedValue(new Error("Item not found"));
(OnePasswordConnect as jest.Mock).mockReturnValue({
getVault: mockGetVault,
getItem: mockGetItem,
});
await expect(loadSecrets(true)).rejects.toThrow("Item not found");
});
it("resolves refs in different vaults using each vault id", async () => {
delete process.env.MY_SECRET;
process.env.SECRET_A = "op://vault-a/item1/field1";
process.env.SECRET_B = "op://vault-b/item2/field2";
const mockGetVault = jest
.fn()
.mockImplementation(async (vaultName: string) =>
Promise.resolve({
id: vaultName === "vault-a" ? "id-a" : "id-b",
}),
);
const mockGetItem = jest
.fn()
.mockResolvedValueOnce({
fields: [{ label: "field1", value: "value-a", section: undefined }],
sections: [],
})
.mockResolvedValueOnce({
fields: [{ label: "field2", value: "value-b", section: undefined }],
sections: [],
});
(OnePasswordConnect as jest.Mock).mockReturnValue({
getVault: mockGetVault,
getItem: mockGetItem,
});
await loadSecrets(true);
expect(mockGetVault).toHaveBeenCalledWith("vault-a");
expect(mockGetVault).toHaveBeenCalledWith("vault-b");
expect(mockGetItem).toHaveBeenNthCalledWith(1, "id-a", "item1");
expect(mockGetItem).toHaveBeenNthCalledWith(2, "id-b", "item2");
expect(core.exportVariable).toHaveBeenCalledWith("SECRET_A", "value-a");
expect(core.exportVariable).toHaveBeenCalledWith("SECRET_B", "value-b");
});
it("throws on invalid ref before calling Connect", async () => {
delete process.env.MY_SECRET;
process.env.BAD_REF = "op://x";
const mockGetVault = jest.fn();
const mockGetItem = jest.fn();
(OnePasswordConnect as jest.Mock).mockReturnValue({
getVault: mockGetVault,
getItem: mockGetItem,
});
await expect(loadSecrets(true)).rejects.toThrow(/invalid|reference/i);
expect(mockGetVault).not.toHaveBeenCalled();
expect(mockGetItem).not.toHaveBeenCalled();
});
describe("core.exportVariable", () => {
it("is called when shouldExportEnv is true", async () => {
await loadSecrets(true);
expect(core.exportVariable).toHaveBeenCalledTimes(2);
expect(core.exportVariable).toHaveBeenCalledWith(
"MY_SECRET",
"resolved-via-connect",
);
expect(core.exportVariable).toHaveBeenCalledWith(
envManagedVariables,
"MY_SECRET",
);
expect(core.exportVariable).toHaveBeenCalledTimes(1);
});
it("is not called when shouldExportEnv is false", async () => {
@@ -388,199 +175,6 @@ describe("loadSecrets when using Connect", () => {
});
});
describe("loadSecrets when using Service Account", () => {
const mockResolve = jest.fn();
beforeEach(() => {
process.env[envConnectHost] = "";
process.env[envConnectToken] = "";
process.env[envServiceAccountToken] = "ops_token";
Object.keys(process.env).forEach((key) => {
if (
typeof process.env[key] === "string" &&
process.env[key]?.startsWith("op://")
) {
delete process.env[key];
}
});
process.env.MY_SECRET = "op://vault/item/field";
(createClient as jest.Mock).mockResolvedValue({
secrets: { resolve: mockResolve },
});
mockResolve.mockResolvedValue("resolved-secret-value");
});
it("does not call op env ls when using Service Account", async () => {
await loadSecrets(false);
expect(exec.getExecOutput).not.toHaveBeenCalled();
});
it("sets step output with resolved value when export-env is false", async () => {
await loadSecrets(false);
expect(core.setOutput).toHaveBeenCalledTimes(1);
expect(core.setOutput).toHaveBeenCalledWith(
"MY_SECRET",
"resolved-secret-value",
);
});
it("masks secret with setSecret when export-env is false", async () => {
await loadSecrets(false);
expect(core.setSecret).toHaveBeenCalledTimes(1);
expect(core.setSecret).toHaveBeenCalledWith("resolved-secret-value");
});
it("does not call exportVariable when export-env is false", async () => {
await loadSecrets(false);
expect(core.exportVariable).not.toHaveBeenCalled();
});
it("exports env and sets OP_MANAGED_VARIABLES when export-env is true", async () => {
await loadSecrets(true);
expect(core.exportVariable).toHaveBeenCalledWith(
"MY_SECRET",
"resolved-secret-value",
);
expect(core.exportVariable).toHaveBeenCalledWith(
envManagedVariables,
"MY_SECRET",
);
});
it("does not set step output when export-env is true", async () => {
await loadSecrets(true);
expect(core.setOutput).not.toHaveBeenCalledWith(
"MY_SECRET",
expect.anything(),
);
});
it("masks secret with setSecret when export-env is true", async () => {
await loadSecrets(true);
expect(core.setSecret).toHaveBeenCalledTimes(1);
expect(core.setSecret).toHaveBeenCalledWith("resolved-secret-value");
});
it("returns early when no env vars have op:// refs", async () => {
Object.keys(process.env).forEach((key) => {
if (
typeof process.env[key] === "string" &&
process.env[key]?.startsWith("op://")
) {
delete process.env[key];
}
});
await loadSecrets(true);
expect(exec.getExecOutput).not.toHaveBeenCalled();
expect(core.exportVariable).not.toHaveBeenCalled();
});
it("wraps createClient errors with a descriptive message", async () => {
(createClient as jest.Mock).mockRejectedValue(
new Error("invalid token format"),
);
await expect(loadSecrets(false)).rejects.toThrow(
"Service account authentication failed: invalid token format",
);
});
describe("multiple refs", () => {
const ref1 = "op://vault/item/field";
const ref2 = "op://vault/other/item";
const ref3 = "op://vault/file/secret";
beforeEach(() => {
process.env.MY_SECRET = ref1;
process.env.ANOTHER_SECRET = ref2;
process.env.FILE_SECRET = ref3;
mockResolve
.mockResolvedValueOnce("value1")
.mockResolvedValueOnce("value2")
.mockResolvedValueOnce("value3");
});
it("resolves each ref and sets step output for each when export-env is false", async () => {
await loadSecrets(false);
expect(mockResolve).toHaveBeenCalledTimes(3);
expect(mockResolve).toHaveBeenCalledWith(ref1);
expect(mockResolve).toHaveBeenCalledWith(ref2);
expect(mockResolve).toHaveBeenCalledWith(ref3);
expect(core.setOutput).toHaveBeenCalledTimes(3);
expect(core.setOutput).toHaveBeenCalledWith("MY_SECRET", "value1");
expect(core.setOutput).toHaveBeenCalledWith("ANOTHER_SECRET", "value2");
expect(core.setOutput).toHaveBeenCalledWith("FILE_SECRET", "value3");
expect(core.setSecret).toHaveBeenCalledTimes(3);
});
it("resolves each ref and exports each and sets OP_MANAGED_VARIABLES when export-env is true", async () => {
await loadSecrets(true);
expect(mockResolve).toHaveBeenCalledTimes(3);
expect(core.exportVariable).toHaveBeenCalledWith("MY_SECRET", "value1");
expect(core.exportVariable).toHaveBeenCalledWith(
"ANOTHER_SECRET",
"value2",
);
expect(core.exportVariable).toHaveBeenCalledWith("FILE_SECRET", "value3");
const exportVariableCalls = (core.exportVariable as jest.Mock).mock
.calls as [string, string][];
const managedVarsCall = exportVariableCalls.find(
([name]) => name === envManagedVariables,
);
expect(managedVarsCall).toBeDefined();
const managedList = (managedVarsCall as [string, string])[1].split(",");
expect(managedList).toContain("MY_SECRET");
expect(managedList).toContain("ANOTHER_SECRET");
expect(managedList).toContain("FILE_SECRET");
expect(managedList).toHaveLength(3);
expect(core.setSecret).toHaveBeenCalledTimes(3);
});
});
describe("secret reference validation", () => {
it("fails with clear message when a secret reference is invalid", async () => {
process.env.MY_SECRET = "op://x";
(Secrets.validateSecretReference as jest.Mock).mockImplementationOnce(
() => {
throw new Error("invalid reference format");
},
);
await expect(loadSecrets(true)).rejects.toThrow(
"Invalid secret reference(s): MY_SECRET",
);
expect(mockResolve).not.toHaveBeenCalled();
});
it("validates all refs before resolving any secrets", async () => {
process.env.MY_SECRET = "op://vault/item/field";
process.env.OTHER = "op://vault/other/item";
(Secrets.validateSecretReference as jest.Mock).mockImplementation(
(ref: string) => {
if (ref === "op://vault/other/item") {
throw new Error("invalid");
}
},
);
await expect(loadSecrets(false)).rejects.toThrow(
"Invalid secret reference(s): OTHER",
);
expect(mockResolve).not.toHaveBeenCalled();
});
});
});
describe("unsetPrevious", () => {
const testManagedEnv = "TEST_SECRET";
const testSecretValue = "MyS3cr#T";
@@ -597,360 +191,3 @@ describe("unsetPrevious", () => {
expect(core.exportVariable).toHaveBeenCalledWith("TEST_SECRET", "");
});
});
describe("findMatchingFieldAndFile", () => {
interface TestField {
id?: string;
label?: string;
value?: string | null;
section?: { id: string } | null | undefined;
}
interface TestFile {
id?: string;
name?: string;
section?: { id: string } | null | undefined;
}
const item = (opts: { fields?: TestField[]; files?: TestFile[] }): FullItem =>
({
fields: opts.fields ?? [],
files: opts.files ?? [],
sections: [],
}) as unknown as FullItem;
const find = (
opts: { fields?: TestField[]; files?: TestFile[] },
sectionIds: string[] = [],
) => findMatchingFieldAndFile(item(opts), "password", sectionIds);
describe("when section filter is used (sectionIds.length > 0)", () => {
it.each<{
name: string;
itemOpts: { fields?: TestField[]; files?: TestFile[] };
expected: { fieldValue?: string; fileId?: string };
}>([
{
name: "returns field value when one field matches query and is in ref sections",
itemOpts: {
fields: [
{
id: "f1",
label: "password",
value: "secret123",
section: { id: "section-1" },
},
],
},
expected: { fieldValue: "secret123" },
},
{
name: "returns file id when one file matches query and is in ref sections",
itemOpts: {
files: [
{
id: "file-uuid",
name: "password",
section: { id: "section-1" },
},
],
},
expected: { fileId: "file-uuid" },
},
{
name: "returns empty object when no field or file matches",
itemOpts: {
fields: [
{ label: "other", value: "x", section: { id: "section-1" } },
],
files: [],
},
expected: {},
},
{
name: "returns field value when field matches by id",
itemOpts: {
fields: [
{
id: "password",
label: "Password Label",
value: "secret-by-id",
section: { id: "section-1" },
},
],
},
expected: { fieldValue: "secret-by-id" },
},
])("$name", ({ itemOpts, expected }) => {
expect(find(itemOpts, ["section-1"])).toEqual(expected);
});
it.each<{
name: string;
itemOpts: { fields?: TestField[]; files?: TestFile[] };
error: RegExp;
}>([
{
name: "throws when multiple fields match",
itemOpts: {
fields: [
{ label: "password", value: "a", section: { id: "section-1" } },
{ label: "password", value: "b", section: { id: "section-1" } },
],
},
error: /Multiple matches/,
},
{
name: "throws when multiple files match",
itemOpts: {
files: [
{ id: "id1", name: "password", section: { id: "section-1" } },
{ id: "id2", name: "password", section: { id: "section-1" } },
],
},
error: /Multiple matches/,
},
{
name: "throws when both a field and a file match",
itemOpts: {
fields: [
{ label: "password", value: "v", section: { id: "section-1" } },
],
files: [
{ id: "fid", name: "password", section: { id: "section-1" } },
],
},
error: /Both a field and a file match/,
},
{
name: "throws when field has no value",
itemOpts: {
fields: [
{ label: "password", value: null, section: { id: "section-1" } },
],
},
error: /has no value/,
},
])("$name", ({ itemOpts, error }) => {
expect(() => find(itemOpts, ["section-1"])).toThrow(error);
});
});
describe("when no section filter (sectionIds.length === 0)", () => {
const sectionIds: string[] = [];
it.each<{
name: string;
itemOpts: { fields?: TestField[]; files?: TestFile[] };
expected: { fieldValue?: string; fileId?: string };
}>([
{
name: "returns field value when one field has no section and matches query",
itemOpts: {
fields: [{ label: "password", value: "secret", section: undefined }],
},
expected: { fieldValue: "secret" },
},
{
name: "returns file id when one file has no section and matches query",
itemOpts: {
files: [{ id: "file-id", name: "password", section: undefined }],
},
expected: { fileId: "file-id" },
},
{
name: "returns field value from fallback (any section) when no field with no section matches",
itemOpts: {
fields: [
{ label: "other", value: "x", section: undefined },
{
label: "password",
value: "from-any-section",
section: { id: "sec" },
},
],
},
expected: { fieldValue: "from-any-section" },
},
{
name: "returns file id from fallback (any section) when no file with no section matches",
itemOpts: {
files: [
{ id: "other", name: "x", section: undefined },
{ id: "file-any", name: "password", section: { id: "sec" } },
],
},
expected: { fileId: "file-any" },
},
{
name: "returns empty object when no match",
itemOpts: {
fields: [{ label: "other", value: "x", section: undefined }],
files: [],
},
expected: {},
},
])("$name", ({ itemOpts, expected }) => {
expect(find(itemOpts, sectionIds)).toEqual(expected);
});
it.each<{
name: string;
itemOpts: { fields?: TestField[]; files?: TestFile[] };
error: RegExp;
}>([
{
name: "throws when multiple fields with no section match",
itemOpts: {
fields: [
{ label: "password", value: "a", section: undefined },
{ label: "password", value: "b", section: undefined },
],
},
error: /Multiple matches/,
},
{
name: "throws when multiple files with no section match",
itemOpts: {
files: [
{ id: "1", name: "password", section: undefined },
{ id: "2", name: "password", section: undefined },
],
},
error: /Multiple matches/,
},
{
name: "throws when both field and file match",
itemOpts: {
fields: [{ label: "password", value: "value", section: undefined }],
files: [{ id: "fid", name: "password", section: undefined }],
},
error: /Both a field and a file match/,
},
])("$name", ({ itemOpts, error }) => {
expect(() => find(itemOpts, sectionIds)).toThrow(error);
});
});
});
describe("findSectionIdsByQuery", () => {
it("throws when sections is empty", () => {
expect(() => findSectionIdsByQuery([], "section-1")).toThrow(
/Item has no sections; cannot resolve section "section-1"/,
);
});
it("throws when sections is null/undefined", () => {
expect(() =>
findSectionIdsByQuery(undefined as unknown as FullItem["sections"], "x"),
).toThrow(/Item has no sections; cannot resolve section "x"/);
});
it("throws when section query matches no section", () => {
const sections = [{ id: "sec-1", label: "Other" }];
expect(() =>
findSectionIdsByQuery(sections as FullItem["sections"], "nonexistent"),
).toThrow(/No section matching "nonexistent" found in specified item/);
});
it("returns section id when section matches by label", () => {
const sections = [{ id: "sec-1", label: "My Section" }];
expect(
findSectionIdsByQuery(sections as FullItem["sections"], "My Section"),
).toEqual(["sec-1"]);
});
it("throws when section query matches no section", () => {
const sections = [{ id: "sec-1", label: "Other" }];
expect(() =>
findSectionIdsByQuery(sections as FullItem["sections"], "nonexistent"),
).toThrow(/No section matching "nonexistent" found in specified item/);
});
it("returns multiple ids when multiple sections match", () => {
const sections = [
{ id: "sec-1", label: "A" },
{ id: "sec-2", label: "A" },
];
expect(
findSectionIdsByQuery(sections as FullItem["sections"], "A"),
).toEqual(["sec-1", "sec-2"]);
});
});
describe("parseOpRef", () => {
it("parses 3-segment ref (vault/item/field)", () => {
expect(parseOpRef("op://vault/item/field")).toEqual({
vault: "vault",
item: "item",
field: "field",
section: undefined,
});
});
it("parses 4-segment ref (vault/item/section/field)", () => {
expect(parseOpRef("op://vault/item/MySection/password")).toEqual({
vault: "vault",
item: "item",
section: "MySection",
field: "password",
});
});
it("decodes URI-encoded segments", () => {
expect(parseOpRef("op://my%20vault/my%20item/field")).toEqual({
vault: "my vault",
item: "my item",
field: "field",
section: undefined,
});
});
it("throws when ref does not start with op://", () => {
expect(() => parseOpRef("invalid-ref")).toThrow(
/Invalid op reference: invalid-ref/,
);
});
it("throws when segment count is invalid", () => {
expect(() => parseOpRef("op://vault/item")).toThrow(
/use op:\/\/<vault>\/<item>\/<field>/,
);
expect(() => parseOpRef("op://a/b/c/d/e")).toThrow(
/use op:\/\/<vault>\/<item>\/<field>/,
);
});
it("throws when vault or item or field is empty", () => {
expect(() => parseOpRef("op:///item/field")).toThrow(/vault is required/);
expect(() => parseOpRef("op://vault//field")).toThrow(/item is required/);
expect(() => parseOpRef("op://vault/item/")).toThrow(/field is required/);
});
it("throws when 4-segment ref has empty section", () => {
expect(() => parseOpRef("op://vault/item//field")).toThrow(
/section is required when using 4 path segments/,
);
});
it("throws when last segment is empty (trailing slash)", () => {
expect(() => parseOpRef("op://vault/item/field/")).toThrow(
/field is required/,
);
});
});
describe("getEnvVarNamesWithSecretRefs", () => {
it("returns only env var names whose value is a string starting with op://", () => {
process.env.OP_REF = "op://vault/item/field";
process.env.NOT_OP_REF = "https://example.com";
process.env.EMPTY_REF = "";
process.env.OP_REF_OTHER = "op://other/vault/item/secret";
const result = getEnvVarNamesWithSecretRefs();
expect(result).toContain("OP_REF");
expect(result).toContain("OP_REF_OTHER");
expect(result).not.toContain("NOT_OP_REF");
expect(result).not.toContain("EMPTY_REF");
});
});

View File

@@ -1,8 +1,6 @@
import * as core from "@actions/core";
import sshpk from "sshpk";
import { read } from "@1password/op-js";
import { createClient, Secrets } from "@1password/sdk";
import { OnePasswordConnect, FullItem, OPConnect } from "@1password/connect";
import * as exec from "@actions/exec";
import { read, setClientInfo, semverToInt } from "@1password/op-js";
import { version } from "../package.json";
import {
authErr,
@@ -12,336 +10,6 @@ import {
envManagedVariables,
} from "./constants";
// #region Op ref parsing
interface ParsedOpRef {
vault: string;
item: string;
section: string | undefined;
field: string;
queryParams: string | undefined;
}
export const parseOpRef = (ref: string): ParsedOpRef => {
// Safety check: refs are validated by validateSecretRefs before this runs
// this guards against parseOpRef being called directly with invalid input
if (!ref.startsWith("op://")) {
throw new Error(`Invalid op reference: ${ref}`);
}
const segments = ref
.slice("op://".length)
.split("/")
.map((s) => decodeURIComponent(s));
if (segments.length < 3 || segments.length > 4) {
throw new Error(
`Invalid op reference: use op://<vault>/<item>/<field> or op://<vault>/<item>/<section>/<field>. Got: ${ref}`,
);
}
const vault = segments[0] ?? "";
if (!vault) {
throw new Error(`Invalid op reference: vault is required`);
}
const item = segments[1] ?? "";
if (!item) {
throw new Error(`Invalid op reference: item is required`);
}
// Last segment is the field; it may include a query string (e.g. "private key?ssh-format=openssh")
const lastSegment = segments[segments.length - 1] ?? "";
const [fieldPart, queryPart] = lastSegment.split("?");
const field = fieldPart ?? "";
if (!field) {
throw new Error(`Invalid op reference: field is required`);
}
const queryParams = queryPart ?? undefined;
// Second to last segment is the section if it exists
let section: string | undefined;
if (segments.length === 4) {
section = segments[2];
if (!section) {
throw new Error(
`Invalid op reference: section is required when using 4 path segments`,
);
}
}
return {
vault,
item,
field,
section,
queryParams,
};
};
// #endregion
// #region Connect item resolution
const getSecretFromConnectItem = async (
client: OPConnect,
item: FullItem,
parsed: ParsedOpRef,
): Promise<string> => {
const sectionIds = parsed.section
? findSectionIdsByQuery(item.sections, parsed.section)
: [];
const { fieldValue, fileId } = findMatchingFieldAndFile(
item,
parsed.field,
sectionIds,
);
if (fieldValue !== undefined) {
return fieldValue;
}
if (fileId) {
return getFileContentWithRetry(client, parsed.vault, parsed.item, fileId);
}
if (parsed.section) {
throw new Error(
`could not find field or file ${parsed.field} in section ${parsed.section} on item ${parsed.item} in vault ${parsed.vault}`,
);
}
throw new Error(
`could not find field or file ${parsed.field} on item ${parsed.item} in vault ${parsed.vault}`,
);
};
const getFileContentWithRetry = async (
client: OPConnect,
vaultId: string,
itemId: string,
fileId: string,
): Promise<string> => {
const maxAttempts = 3;
const retryDelayMs = 2000;
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
try {
return await client.getFileContent(vaultId, itemId, fileId);
} catch (err) {
// Retry on 503 errors as this can happen on multiple secret fetches
const is503 =
err !== null &&
typeof err === "object" &&
(err as Record<string, unknown>).statusCode === 503;
if (is503 && attempt < maxAttempts) {
await new Promise((r) => setTimeout(r, retryDelayMs));
continue;
}
throw err;
}
}
return "";
};
export const findSectionIdsByQuery = (
sections: FullItem["sections"],
sectionQuery: string | undefined,
): string[] => {
// If no sections were returned with the item throw an error
if (!sections || sections.length === 0) {
throw new Error(
`Item has no sections; cannot resolve section "${sectionQuery}"`,
);
}
const ids = sections
.filter((s) => s.id === sectionQuery || s.label === sectionQuery)
.flatMap((s) => (s.id ? [s.id] : []));
// If no sections were found with the given query throw an error
if (ids.length === 0) {
throw new Error(
`No section matching "${sectionQuery}" found in specified item`,
);
}
return ids;
};
export const findMatchingFieldAndFile = (
item: FullItem,
fieldOrFileQuery: string,
sectionIds: string[],
): { fieldValue?: string; fileId?: string } => {
// Get the fields/files from the item and check if the ref has a section filter
const fields = item.fields ?? [];
const files = item.files ?? [];
const sectionFilter = sectionIds.length > 0;
const fieldMatchesQuery = (f: (typeof fields)[0]) =>
f.id === fieldOrFileQuery || f.label === fieldOrFileQuery;
const fileMatchesQuery = (f: (typeof files)[0]) =>
f.id === fieldOrFileQuery || f.name === fieldOrFileQuery;
let matchedField: (typeof fields)[0] | undefined;
let matchedFile: (typeof files)[0] | undefined;
if (sectionFilter) {
// If the ref has a section filter only accept matches inside the referenced sections
const matchingFields = fields.filter((f) => {
const sectionId = f.section?.id;
const inRefSections =
sectionId !== null &&
sectionId !== undefined &&
sectionIds.includes(sectionId);
return fieldMatchesQuery(f) && inRefSections;
});
matchedField = findSingleMatch(matchingFields);
const matchingFiles = files.filter((f) => {
const sectionId = f.section?.id;
const inRefSections =
sectionId !== null &&
sectionId !== undefined &&
sectionIds.includes(sectionId);
return fileMatchesQuery(f) && inRefSections;
});
matchedFile = findSingleMatch(matchingFiles);
} else {
// If the ref has no section filter search for matches with no section
const matchingFields = fields.filter((f) => {
const hasNoSection =
f.section?.id === null || f.section?.id === undefined;
return fieldMatchesQuery(f) && hasNoSection;
});
matchedField = findSingleMatch(matchingFields);
// If no matches were found with no section, search for matches in any section
if (!matchedField) {
const matchingFieldsInAnySection = fields.filter(fieldMatchesQuery);
matchedField = findSingleMatch(matchingFieldsInAnySection);
}
const matchingFiles = files.filter((f) => {
const hasNoSection =
f.section?.id === null || f.section?.id === undefined;
return fileMatchesQuery(f) && hasNoSection;
});
matchedFile = findSingleMatch(matchingFiles);
if (!matchedFile) {
const matchingFilesInAnySection = files.filter(fileMatchesQuery);
matchedFile = findSingleMatch(matchingFilesInAnySection);
}
}
if (matchedField && matchedFile) {
throw new Error(
`Both a field and a file match "${fieldOrFileQuery}". Rename one or use the ID in your op:// reference.`,
);
}
if (matchedField) {
if (matchedField.value === undefined || matchedField.value === null) {
throw new Error(
`field ${fieldOrFileQuery} has no value in specified item`,
);
}
return { fieldValue: matchedField.value };
}
if (matchedFile?.id) {
return { fileId: matchedFile.id };
}
return {};
};
const findSingleMatch = <T>(matches: T[]): T | undefined => {
if (matches.length > 1) {
throw new Error(
"Multiple matches found. Rename one or use an ID in your op:// reference.",
);
}
return matches[0];
};
const createConnectClient = (host: string, token: string): OPConnect => {
try {
return OnePasswordConnect({
// eslint-disable-next-line @typescript-eslint/naming-convention
serverURL: host,
token,
});
} catch (err) {
const message = err instanceof Error ? err.message : String(err);
throw new Error(`Connect authentication failed: ${message}`);
}
};
// eslint-disable-next-line @typescript-eslint/naming-convention
const toOpenSSH = (value: string, _queryParams: string | undefined): string => {
try {
const key = sshpk.parsePrivateKey(value, "auto");
return key.toString("openssh");
} catch {
core.warning(
`Failed to parse private key to OpenSSH, returning original value`,
);
return value;
}
};
// #endregion
// #region Shared helpers and auth
export const getEnvVarNamesWithSecretRefs = (): string[] =>
Object.keys(process.env).filter(
(key) =>
typeof process.env[key] === "string" &&
process.env[key]?.startsWith("op://"),
);
const validateSecretRefs = (envNames: string[]): void => {
const invalid: { name: string; message: string }[] = [];
for (const envName of envNames) {
const ref = process.env[envName];
if (!ref) {
continue;
}
try {
Secrets.validateSecretReference(ref);
} catch (err) {
const message = err instanceof Error ? err.message : String(err);
invalid.push({ name: envName, message });
}
}
// Throw an error if any secret references are invalid
if (invalid.length > 0) {
const details = invalid
.map(({ name, message }) => `${name}: ${message}`)
.join("; ");
throw new Error(`Invalid secret reference(s): ${details}`);
}
};
const setResolvedSecret = (
envName: string,
secretValue: string,
shouldExportEnv: boolean,
): void => {
core.info(`Populating variable: ${envName}`);
if (shouldExportEnv) {
core.exportVariable(envName, secretValue);
} else {
core.setOutput(envName, secretValue);
}
if (secretValue) {
core.setSecret(secretValue);
}
};
export const validateAuth = (): void => {
const isConnect = process.env[envConnectHost] && process.env[envConnectToken];
const isServiceAccount = process.env[envServiceAccountToken];
@@ -365,6 +33,8 @@ export const extractSecret = (
envName: string,
shouldExportEnv: boolean,
): void => {
core.info(`Populating variable: ${envName}`);
const ref = process.env[envName];
if (!ref) {
return;
@@ -375,7 +45,42 @@ export const extractSecret = (
return;
}
setResolvedSecret(envName, secretValue, shouldExportEnv);
if (shouldExportEnv) {
core.exportVariable(envName, secretValue);
} else {
core.setOutput(envName, secretValue);
}
// Skip setSecret for empty strings to avoid the warning:
// "Can't add secret mask for empty string in ##[add-mask] command."
if (secretValue) {
core.setSecret(secretValue);
}
};
export const loadSecrets = async (shouldExportEnv: boolean): Promise<void> => {
// Pass User-Agent Information to the 1Password CLI
setClientInfo({
name: "1Password GitHub Action",
id: "GHA",
build: semverToInt(version),
});
// Load secrets from environment variables using 1Password CLI.
// Iterate over them to find 1Password references, extract the secret values,
// and make them available in the next steps either as step outputs or as environment variables.
const res = await exec.getExecOutput(`sh -c "op env ls"`);
if (res.stdout === "") {
return;
}
const envs = res.stdout.replace(/\n+$/g, "").split(/\r?\n/);
for (const envName of envs) {
extractSecret(envName, shouldExportEnv);
}
if (shouldExportEnv) {
core.exportVariable(envManagedVariables, envs.join());
}
};
export const unsetPrevious = (): void => {
@@ -388,142 +93,3 @@ export const unsetPrevious = (): void => {
}
}
};
const fetchVaultId = async (
client: OPConnect,
vaultQuery: string,
ref: string,
vaultIdCache: Map<string, string>,
): Promise<string> => {
// Check if the vault ID is already cached to avoid unnecessary API calls
const cached = vaultIdCache.get(vaultQuery);
if (cached !== undefined) {
return cached;
}
const vault = await client.getVault(vaultQuery);
if (!vault.id) {
throw new Error(
`Could not find valid vault "${vaultQuery}" for ref "${ref}"`,
);
}
vaultIdCache.set(vaultQuery, vault.id);
return vault.id;
};
// #endregion
// #region Load secrets
// Connect loads secrets via the Connect JS SDK
const loadSecretsViaConnect = async (
shouldExportEnv: boolean,
): Promise<void> => {
const envs = getEnvVarNamesWithSecretRefs();
if (envs.length === 0) {
return;
}
validateSecretRefs(envs);
const host = process.env[envConnectHost];
const token = process.env[envConnectToken];
if (!host || !token) {
throw new Error(authErr);
}
const client = createConnectClient(host, token);
const vaultIdCache = new Map<string, string>();
for (const envName of envs) {
const ref = process.env[envName];
if (!ref) {
continue;
}
try {
// Parse the op ref and get the item from the Connect SDK
const parsed = parseOpRef(ref);
const vaultId = await fetchVaultId(
client,
parsed.vault,
ref,
vaultIdCache,
);
const item = await client.getItem(vaultId, parsed.item);
// Get the secret value from the item as Connect returns a full item object
const secretValue = await getSecretFromConnectItem(client, item, parsed);
let valueToSet = secretValue;
if (parsed.queryParams?.includes("ssh-format=openssh")) {
valueToSet = toOpenSSH(secretValue, parsed.queryParams);
}
setResolvedSecret(envName, valueToSet, shouldExportEnv);
} catch (err) {
const msg = err instanceof Error ? err.message : String(err);
throw new Error(`Failed to load ref "${ref}": ${msg}`);
}
}
if (shouldExportEnv) {
core.exportVariable(envManagedVariables, envs.join());
}
};
// Service Account loads secrets via the 1Password SDK
const loadSecretsViaServiceAccount = async (
shouldExportEnv: boolean,
): Promise<void> => {
const envs = getEnvVarNamesWithSecretRefs();
if (envs.length === 0) {
return;
}
validateSecretRefs(envs);
const token = process.env[envServiceAccountToken];
if (!token) {
throw new Error(authErr);
}
// Authenticate with the 1Password SDK
let client;
try {
client = await createClient({
auth: token,
integrationName: "1Password GitHub Action",
integrationVersion: version,
});
} catch (err) {
const message = err instanceof Error ? err.message : String(err);
throw new Error(`Service account authentication failed: ${message}`);
}
for (const envName of envs) {
const ref = process.env[envName];
if (!ref) {
continue;
}
// Resolve the secret value using the 1Password SDK
// and make it available either as step outputs or as environment variables
const secretValue = await client.secrets.resolve(ref);
setResolvedSecret(envName, secretValue, shouldExportEnv);
}
if (shouldExportEnv) {
core.exportVariable(envManagedVariables, envs.join());
}
};
export const loadSecrets = async (shouldExportEnv: boolean): Promise<void> => {
const isConnect = process.env[envConnectHost] && process.env[envConnectToken];
if (isConnect) {
await loadSecretsViaConnect(shouldExportEnv);
return;
}
await loadSecretsViaServiceAccount(shouldExportEnv);
};
// #endregion

View File

@@ -10,9 +10,6 @@ assert_env_equals() {
}
readonly SECRET="RGVhciBzZWN1cml0eSByZXNlYXJjaGVyLCB0aGlzIGlzIGp1c3QgYSBkdW1teSBzZWNyZXQuIFBsZWFzZSBkb24ndCByZXBvcnQgaXQu"
readonly FILE_SECRET_CONTENT="This is a test"
readonly DOUBLE_SECTION_SECRET_CONTENT="test-password"
MULTILINE_SECRET="$(cat << EOF
-----BEGIN PRIVATE KEY-----
RGVhciBzZWN1cml0eSByZXNlYXJjaGVyLApXaGls
@@ -29,6 +26,7 @@ IApTbyBwbGVhc2UgZG9uJ3QgcmVwb3J0IGl0IQo=
EOF
)"
readonly MULTILINE_SECRET
readonly WEBSITE="www.test.com"
assert_env_equals "SECRET" "${SECRET}"
assert_env_equals "FILE_SECRET" "${SECRET}"
@@ -39,7 +37,8 @@ assert_env_equals "FILE_SECRET_IN_SECTION" "${SECRET}"
assert_env_equals "MULTILINE_SECRET" "${MULTILINE_SECRET}"
assert_env_equals "FILE_MULTILINE_SECRET" "${MULTILINE_SECRET}"
assert_env_equals "SECRET_WITH_FILE" "${FILE_SECRET_CONTENT}"
assert_env_equals "SECRET_WITH_FILE_IN_SECTION" "${FILE_SECRET_CONTENT}"
assert_env_equals "DOUBLE_SECTION_SECRET" "${DOUBLE_SECTION_SECRET_CONTENT}"
# WEBSITE/FILE_WEBSITE: required when ASSERT_WEBSITE=true (Service Account), skipped when false (Connect)
if [ "${ASSERT_WEBSITE:-false}" = "true" ]; then
assert_env_equals "WEBSITE" "${WEBSITE}"
assert_env_equals "FILE_WEBSITE" "${WEBSITE}"
fi

View File

@@ -18,9 +18,10 @@ assert_env_unset "FILE_SECRET_IN_SECTION"
assert_env_unset "MULTILINE_SECRET"
assert_env_unset "FILE_MULTILINE_SECRET"
assert_env_unset "SECRET_WITH_FILE"
assert_env_unset "SECRET_WITH_FILE_IN_SECTION"
assert_env_unset "DOUBLE_SECTION_SECRET"
assert_env_unset "WEBSITE"
assert_env_unset "FILE_WEBSITE"
assert_env_unset "SSH_PRIVATE_KEY"
assert_env_unset "SSH_PRIVATE_KEY_OPENSSH"
assert_env_unset "TEST_SSH_KEY"
assert_env_unset "FILE_TEST_SSH_KEY"
assert_env_unset "TEST_SSH_KEY_OPENSSH"
assert_env_unset "FILE_TEST_SSH_KEY_OPENSSH"

View File

@@ -1,7 +0,0 @@
#!/bin/bash
set -e
if [ "$STEP_OUTCOME" != "failure" ]; then
echo "Expected action to fail on invalid ref, got: $STEP_OUTCOME"
exit 1
fi
echo "Action correctly failed on invalid ref"

26
tests/assert-ssh-keys-set.sh Executable file
View File

@@ -0,0 +1,26 @@
#!/bin/bash
set -e
assert_ssh_key_set() {
local var="$1"
local val
val="$(printenv "$var" || true)"
if [ -z "$val" ]; then
echo "Expected $var to be set"
exit 1
fi
[ "$val" = "***" ] && return 0
local line
line="$(echo "$val" | head -1)"
if echo "$var" | grep -q "OPENSSH"; then
echo "$line" | grep -q "OPENSSH" || { echo "Expected $var to start with -----BEGIN OPENSSH PRIVATE KEY-----"; exit 1; }
else
echo "$line" | grep -q "BEGIN.*PRIVATE KEY" || { echo "Expected $var to be a private key"; exit 1; }
fi
echo "$var OK"
}
assert_ssh_key_set "TEST_SSH_KEY"
assert_ssh_key_set "TEST_SSH_KEY_OPENSSH"
assert_ssh_key_set "FILE_TEST_SSH_KEY"
assert_ssh_key_set "FILE_TEST_SSH_KEY_OPENSSH"

View File

@@ -1,34 +0,0 @@
#!/bin/bash
set -e
# SSH_PRIVATE_KEY: any private key format
v="$(printenv SSH_PRIVATE_KEY)"
if [ -z "$v" ]; then
echo "SSH_PRIVATE_KEY is not set"
exit 1
fi
if ! echo "$v" | head -1 | grep -qE -- '^-----BEGIN (RSA |EC |OPENSSH )?PRIVATE KEY-----'; then
echo "SSH_PRIVATE_KEY does not start with a private key header"
exit 1
fi
if ! echo "$v" | tail -1 | grep -qE -- '-----END (RSA |EC |OPENSSH )?PRIVATE KEY-----$'; then
echo "SSH_PRIVATE_KEY does not end with a private key footer"
exit 1
fi
echo "SSH_PRIVATE_KEY has valid key format"
# SSH_PRIVATE_KEY_OPENSSH: OpenSSH format only
v="$(printenv SSH_PRIVATE_KEY_OPENSSH)"
if [ -z "$v" ]; then
echo "SSH_PRIVATE_KEY_OPENSSH is not set"
exit 1
fi
if ! echo "$v" | head -1 | grep -q -- '-----BEGIN OPENSSH PRIVATE KEY-----'; then
echo "SSH_PRIVATE_KEY_OPENSSH is not in OpenSSH format"
exit 1
fi
if ! echo "$v" | tail -1 | grep -q -- '-----END OPENSSH PRIVATE KEY-----$'; then
echo "SSH_PRIVATE_KEY_OPENSSH does not end with OpenSSH private key footer"
exit 1
fi
echo "SSH_PRIVATE_KEY_OPENSSH has valid OpenSSH key format"