Compare commits

..

37 Commits

Author SHA1 Message Date
Jill Regan
dd76a122aa Update dists 2026-02-26 11:22:17 -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
398c918d60 Fix format 2026-02-23 08:13:30 -05:00
Jill Regan
dc90451a94 Apply code suggestions 2026-02-22 12:31:37 -05:00
Jill Regan
9d7acefac9 Move comment 2026-02-20 08:34:52 -05:00
Jill Regan
04984a6c91 Add eslint disable 2026-02-20 08:31:14 -05:00
Jill Regan
db7314de7b Merge branch 'feature/migrate-to-sdk' into jill/validate-secret-reference 2026-02-20 08:24:44 -05:00
Jill Regan
3f9ba481c9 Merge pull request #134 from 1Password/jill/use-sdk-for-service-account
Migrate to use  1Password SDK with Service Account
2026-02-20 08:22:34 -05:00
Jill Regan
1e8273d4be Fix formatting 2026-02-19 14:24:51 -05:00
Jill Regan
015b03300e Code cleanup 2026-02-19 14:17:40 -05:00
Jill Regan
ab44f9f69c Remove unit test 2026-02-18 18:01:22 -05:00
Jill Regan
af49dd18de Remove connect handling 2026-02-18 18:00:42 -05:00
Jill Regan
d456b72513 Add connect e2e test 2026-02-18 17:50:34 -05:00
Jill Regan
2a828228a8 Try woth test failure 2026-02-18 17:48:21 -05:00
Jill Regan
604a86ce4e Make assert-invalid-ref-failed.sh executable 2026-02-18 17:44:26 -05:00
Jill Regan
7998453500 Update script 2026-02-18 17:41:39 -05:00
Jill Regan
e7fe4397d9 Add e2e test 2026-02-18 17:38:00 -05:00
Jill Regan
6911316fe3 Add secret ref validation 2026-02-18 17:24:55 -05:00
Jill Regan
24235f3b6b Fix formatting 2026-02-18 16:37:41 -05:00
Jill Regan
a2ce22dd39 Add error handling 2026-02-18 16:35:10 -05:00
Jill Regan
d2fdd9df66 Update unit test 2026-02-18 14:12:14 -05:00
Jill Regan
95478552e8 Fix linting issues 2026-02-18 13:57:08 -05:00
Jill Regan
4a997a0402 Use SDK with service account 2026-02-18 13:48:19 -05:00
Volodymyr Zotov
81bc2a50b4 Merge pull request #133 from 1Password/vzt/fix-commit-ref-in-e2e-test-workflow
Pass latest commit ref to checkout
2026-01-28 08:35:26 -06:00
Volodymyr Zotov
1dfe1fc19e Build action before testing 2026-01-27 14:19:04 -06:00
Volodymyr Zotov
856971e6d6 Pass latest commit ref to checkout 2026-01-27 12:12:48 -06:00
Volodymyr Zotov
5fd6fbcfdf Merge pull request #132 from toga4/empty-strings
fix: set outputs/env vars for empty string field values
2026-01-27 10:06:27 -06:00
toga4
13f927c806 fix: set outputs/env vars for empty string field values
Empty string field values from 1Password were causing the action to skip setting outputs and environment variables entirely.
This was inconsistent with `op run` behavior, which sets the variable with an empty value.

- Change falsy check to explicit null/undefined check in extractSecret
- Skip setSecret for empty strings to avoid runner warning
- Add tests for empty string value handling
2026-01-23 10:38:43 +09:00
Volodymyr Zotov
fdb192f5dc Merge pull request #130 from BolajiOlajide/bo/ssh-secret-doc
docs: add SSH key format parameter documentation
2025-12-22 11:19:47 -06:00
Bolaji Olajide
13c259d353 update 2025-12-22 18:08:38 +01:00
Bolaji Olajide
b91fef0861 move section to quoickstart 2025-12-22 16:33:55 +01:00
Bolaji Olajide
2d74546fd1 docs: add SSH key format parameter documentation 2025-12-21 05:41:19 +01:00
Volodymyr Zotov
8d0d610af1 Merge pull request #129 from 1Password/release/v3.1.0
Prepare release v3.1.0
2025-12-16 13:06:48 -06:00
Volodymyr Zotov
76bec67e89 Make latest build 2025-12-16 12:59:55 -06:00
Volodymyr Zotov
74311b1273 Bump version in package-lock.json 2025-12-16 12:37:51 -06:00
Volodymyr Zotov
5999940e48 Bump version to 3.1.0 2025-12-16 12:36:37 -06:00
Volodymyr Zotov
b43a2248cc Make latest build 2025-12-16 12:05:07 -06:00
17 changed files with 3546 additions and 833 deletions

View File

@@ -1,118 +0,0 @@
name: Acceptance test
on:
workflow_call:
inputs:
secret:
required: true
type: string
secret-in-section:
required: true
type: string
multiline-secret:
required: true
type: string
export-env:
required: true
type: boolean
version:
required: false
type: string
default: "latest"
os:
required: true
type: string
default: "ubuntu-latest"
auth:
required: true
type: string
jobs:
acceptance-test:
runs-on: ${{ inputs.os }}
steps:
- name: Base checkout
uses: actions/checkout@v4
if: |
github.event_name != 'repository_dispatch' &&
(
github.ref == 'refs/heads/main' ||
(
github.event_name == 'pull_request' &&
github.event.pull_request.head.repo.full_name == github.repository
)
)
- name: Fork based /ok-to-test checkout
uses: actions/checkout@v4
with:
ref: ${{ github.event.client_payload.pull_request.head.sha }}
if: |
github.event_name == 'repository_dispatch' &&
github.event.client_payload.slash_command.args.named.sha != '' &&
contains(
github.event.client_payload.pull_request.head.sha,
github.event.client_payload.slash_command.args.named.sha
)
- name: Launch 1Password Connect instance
if: ${{ inputs.auth == 'connect' }}
env:
OP_CONNECT_CREDENTIALS: ${{ secrets.OP_CONNECT_CREDENTIALS }}
run: |
echo "$OP_CONNECT_CREDENTIALS" > 1password-credentials.json
docker compose -f tests/fixtures/docker-compose.yml up -d && sleep 10
- name: Configure Service account
if: ${{ inputs.auth == 'service-account' }}
uses: ./configure
with:
service-account-token: ${{ secrets.OP_SERVICE_ACCOUNT_TOKEN }}
- name: Verify Service Account env var is set
if: ${{ inputs.auth == 'service-account' }}
shell: bash
run: |
if [ -z "${OP_SERVICE_ACCOUNT_TOKEN}" ]; then
echo "OP_SERVICE_ACCOUNT_TOKEN environment variable is not set" >&2
exit 1
fi
- name: Configure 1Password Connect
if: ${{ inputs.auth == 'connect' }}
uses: ./configure # 1password/load-secrets-action/configure@<version>
with:
connect-host: http://localhost:8080
connect-token: ${{ secrets.OP_CONNECT_TOKEN }}
- name: Verify Connect env vars are set
if: ${{ inputs.auth == 'connect' }}
run: |
if [ -z "$OP_CONNECT_HOST" ] || [ -z "$OP_CONNECT_TOKEN" ]; then
echo "OP_CONNECT_HOST or OP_CONNECT_TOKEN environment variables are not set" >&2
exit 1
fi
- name: Load secrets
id: load_secrets
uses: ./ # 1password/load-secrets-action@<version>
with:
version: ${{ inputs.version }}
export-env: ${{ inputs.export-env }}
env:
SECRET: ${{ inputs.secret }}
SECRET_IN_SECTION: ${{ inputs.secret-in-section }}
MULTILINE_SECRET: ${{ inputs.multiline-secret }}
OP_ENV_FILE: ./tests/.env.tpl
- name: Assert test secret values [step output]
if: ${{ !inputs.export-env }}
env:
SECRET: ${{ steps.load_secrets.outputs.SECRET }}
SECRET_IN_SECTION: ${{ steps.load_secrets.outputs.SECRET_IN_SECTION }}
MULTILINE_SECRET: ${{ steps.load_secrets.outputs.MULTILINE_SECRET }}
OP_ENV_FILE: ./tests/.env.tpl
run: ./tests/assert-env-set.sh
- name: Assert test secret values [exported env]
if: ${{ inputs.export-env }}
run: ./tests/assert-env-set.sh
- name: Remove secrets [exported env]
if: ${{ inputs.export-env }}
uses: ./ # 1password/load-secrets-action@<version>
with:
unset-previous: true
- name: Assert removed secrets [exported env]
if: ${{ inputs.export-env }}
run: ./tests/assert-env-unset.sh

View File

@@ -8,6 +8,11 @@ on:
# For test.yml to call this workflow
workflow_call:
inputs:
ref:
description: "Git ref to checkout"
required: true
type: string
secrets:
OP_CONNECT_CREDENTIALS:
required: true
@@ -34,6 +39,19 @@ jobs:
uses: actions/checkout@v5
with:
fetch-depth: 0
ref: ${{ inputs.ref }}
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: 20
cache: npm
- name: Install dependencies
run: npm ci
- name: Build actions
run: npm run build:all
- name: Generate .env.tpl
shell: bash
@@ -87,6 +105,22 @@ 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"
OP_SERVICE_ACCOUNT_TOKEN: ${{ secrets.OP_SERVICE_ACCOUNT_TOKEN }}
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 }}
test-connect:
name: Connect (ubuntu-latest, ${{ matrix.version }}, export-env=${{ matrix.export-env }})
runs-on: ubuntu-latest
@@ -101,6 +135,19 @@ jobs:
uses: actions/checkout@v5
with:
fetch-depth: 0
ref: ${{ inputs.ref }}
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: 20
cache: npm
- name: Install dependencies
run: npm ci
- name: Build actions
run: npm run build:all
- name: Generate .env.tpl
run: |

View File

@@ -1,29 +0,0 @@
on:
push:
branches: [main]
pull_request:
name: Lint
jobs:
lint:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Run ShellCheck
uses: ludeeus/action-shellcheck@2.0.0
with:
ignore_paths: >-
.husky
- name: Setup Node.js
id: setup-node
uses: actions/setup-node@v4
with:
node-version: 20
cache: npm
- name: Install Dependencies
id: install
run: npm ci
- name: Check formatting
run: npm run format:check
- name: Check lint
run: npm run lint

View File

@@ -26,6 +26,7 @@ jobs:
runs-on: ubuntu-latest
outputs:
condition: ${{ steps.check.outputs.condition }}
ref: ${{ steps.check.outputs.ref }}
steps:
- name: Check if PR is from external contributor
id: check
@@ -45,6 +46,7 @@ jobs:
else
echo "condition=pr-creation-maintainer" >> $GITHUB_OUTPUT
echo "Setting condition=pr-creation-maintainer (internal PR creation)"
echo "ref=${{ github.event.pull_request.head.sha }}" >> $GITHUB_OUTPUT
fi
elif [ "${{ github.event_name }}" == "repository_dispatch" ]; then
# For repository_dispatch events (ok-to-test), check if sha matches
@@ -58,6 +60,7 @@ jobs:
if [ -n "$SHA_PARAM" ] && [[ "$PR_HEAD_SHA" == *"$SHA_PARAM"* ]]; then
echo "condition=dispatch-event" >> $GITHUB_OUTPUT
echo "Setting condition=dispatch-event (sha matches)"
echo "ref=$PR_HEAD_SHA" >> $GITHUB_OUTPUT
else
echo "condition=skip" >> $GITHUB_OUTPUT
echo "Setting condition=skip (sha does not match or empty)"
@@ -65,6 +68,7 @@ jobs:
elif [ "${{ github.event_name }}" == "push" ] && [ "${{ github.ref_name }}" == "main" ]; then
echo "condition=push-to-main" >> $GITHUB_OUTPUT
echo "Setting condition=push-to-main (push to main)"
echo "ref=${{ github.sha }}" >> $GITHUB_OUTPUT
else
# Unknown event type
echo "condition=skip" >> $GITHUB_OUTPUT
@@ -80,6 +84,8 @@ jobs:
||
needs.check-external-pr.outputs.condition == 'push-to-main'
uses: ./.github/workflows/e2e-tests.yml
with:
ref: ${{ needs.check-external-pr.outputs.ref }}
secrets:
OP_CONNECT_CREDENTIALS: ${{ secrets.OP_CONNECT_CREDENTIALS }}
OP_CONNECT_TOKEN: ${{ secrets.OP_CONNECT_TOKEN }}

View File

@@ -1,92 +0,0 @@
on:
repository_dispatch:
types: [ok-to-test-command]
name: Run acceptance tests [fork]
jobs:
test-with-output-secrets:
if: |
github.event_name == 'repository_dispatch' &&
github.event.client_payload.slash_command.args.named.sha != '' &&
contains(
github.event.client_payload.pull_request.head.sha,
github.event.client_payload.slash_command.args.named.sha
)
uses: 1password/load-secrets-action/.github/workflows/acceptance-test.yml@main
secrets: inherit
with:
secret: op://acceptance-tests/test-secret/password
secret-in-section: op://acceptance-tests/test-secret/test-section/password
multiline-secret: op://acceptance-tests/multiline-secret/notesPlain
export-env: false
test-with-export-env:
if: |
github.event_name == 'repository_dispatch' &&
github.event.client_payload.slash_command.args.named.sha != '' &&
contains(
github.event.client_payload.pull_request.head.sha,
github.event.client_payload.slash_command.args.named.sha
)
uses: 1password/load-secrets-action/.github/workflows/acceptance-test.yml@main
secrets: inherit
with:
secret: op://acceptance-tests/test-secret/password
secret-in-section: op://acceptance-tests/test-secret/test-section/password
multiline-secret: op://acceptance-tests/multiline-secret/notesPlain
export-env: true
test-references-with-ids:
if: |
github.event_name == 'repository_dispatch' &&
github.event.client_payload.slash_command.args.named.sha != '' &&
contains(
github.event.client_payload.pull_request.head.sha,
github.event.client_payload.slash_command.args.named.sha
)
uses: 1password/load-secrets-action/.github/workflows/acceptance-test.yml@main
secrets: inherit
with:
secret: op://v5pz6venw4roosmkzdq2nhpv6u/hrgkzhrlvscomepxlgafb2m3ca/password
secret-in-section: op://v5pz6venw4roosmkzdq2nhpv6u/hrgkzhrlvscomepxlgafb2m3ca/Section_tco6nsqycj6jcbyx63h5isxcny/doxu3mhkozcznnk5vjrkpdqayy
multiline-secret: op://v5pz6venw4roosmkzdq2nhpv6u/ghtz3jvcc6dqmzc53d3r3eskge/notesPlain
export-env: false
update-checks:
# required permissions for updating the status of the pull request checks
permissions:
pull-requests: write
checks: write
runs-on: ubuntu-latest
if: ${{ always() }}
strategy:
matrix:
job-name:
[
test-with-output-secrets,
test-with-export-env,
test-references-with-ids,
]
needs:
[test-with-output-secrets, test-with-export-env, test-references-with-ids]
steps:
- uses: actions/github-script@v6
env:
job: ${{ matrix.job-name }}
ref: ${{ github.event.client_payload.pull_request.head.sha }}
conclusion: ${{ needs[format('{0}', matrix.job-name )].result }}
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
script: |
const { data: checks } = await github.rest.checks.listForRef({
...context.repo,
ref: process.env.ref
});
const check = checks.check_runs.filter(c => c.name === process.env.job);
const { data: result } = await github.rest.checks.update({
...context.repo,
check_run_id: check[0].id,
status: 'completed',
conclusion: process.env.conclusion
});
return result;

View File

@@ -1,100 +0,0 @@
on:
push:
branches: [main]
pull_request:
name: Run acceptance tests
jobs:
unit-tests:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/setup-node@v3
with:
node-version: 20
- run: npm ci
- run: npm test
test-with-output-secrets:
if: |
github.ref == 'refs/heads/main' ||
(
github.event_name == 'pull_request' &&
github.event.pull_request.head.repo.full_name == github.repository
)
uses: 1password/load-secrets-action/.github/workflows/acceptance-test.yml@main
secrets: inherit
strategy:
matrix:
os: [ubuntu-latest, macos-latest, windows-latest]
version: [latest, latest-beta, 2.30.0, 2.30.0-beta.03]
auth: [connect, service-account]
exclude:
- os: macos-latest
auth: connect
- os: windows-latest
auth: connect
with:
os: ${{ matrix.os }}
version: ${{ matrix.version }}
auth: ${{ matrix.auth }}
secret: op://acceptance-tests/test-secret/password
secret-in-section: op://acceptance-tests/test-secret/test-section/password
multiline-secret: op://acceptance-tests/multiline-secret/notesPlain
export-env: false
test-with-export-env:
if: |
github.ref == 'refs/heads/main' ||
(
github.event_name == 'pull_request' &&
github.event.pull_request.head.repo.full_name == github.repository
)
uses: 1password/load-secrets-action/.github/workflows/acceptance-test.yml@main
secrets: inherit
strategy:
matrix:
os: [ubuntu-latest, macos-latest, windows-latest]
version: [latest, latest-beta, 2.30.0, 2.30.0-beta.03]
auth: [connect, service-account]
exclude:
- os: macos-latest
auth: connect
- os: windows-latest
auth: connect
with:
os: ${{ matrix.os }}
version: ${{ matrix.version }}
auth: ${{ matrix.auth }}
secret: op://acceptance-tests/test-secret/password
secret-in-section: op://acceptance-tests/test-secret/test-section/password
multiline-secret: op://acceptance-tests/multiline-secret/notesPlain
export-env: true
test-references-with-ids:
if: |
github.ref == 'refs/heads/main' ||
(
github.event_name == 'pull_request' &&
github.event.pull_request.head.repo.full_name == github.repository
)
uses: 1password/load-secrets-action/.github/workflows/acceptance-test.yml@main
secrets: inherit
strategy:
matrix:
os: [ubuntu-latest, macos-latest, windows-latest]
version: [latest, latest-beta, 2.30.0, 2.30.0-beta.03]
auth: [connect, service-account]
exclude:
- os: macos-latest
auth: connect
- os: windows-latest
auth: connect
with:
os: ${{ matrix.os }}
version: ${{ matrix.version }}
auth: ${{ matrix.auth }}
secret: op://v5pz6venw4roosmkzdq2nhpv6u/hrgkzhrlvscomepxlgafb2m3ca/password
secret-in-section: op://v5pz6venw4roosmkzdq2nhpv6u/hrgkzhrlvscomepxlgafb2m3ca/Section_tco6nsqycj6jcbyx63h5isxcny/doxu3mhkozcznnk5vjrkpdqayy
multiline-secret: op://v5pz6venw4roosmkzdq2nhpv6u/ghtz3jvcc6dqmzc53d3r3eskge/notesPlain
export-env: false

View File

@@ -71,6 +71,21 @@ jobs:
# Prints: Secret: ***
```
### 🔑 SSH Key Format
When loading SSH keys, you can specify the format using the `ssh-format` query parameter. This is useful when you need the private key in a specific format like OpenSSH.
```yml
- name: Load SSH key
uses: 1password/load-secrets-action@v3
env:
OP_SERVICE_ACCOUNT_TOKEN: ${{ secrets.OP_SERVICE_ACCOUNT_TOKEN }}
# Load SSH private key in OpenSSH format
SSH_PRIVATE_KEY: op://vault/item/private key?ssh-format=openssh
```
For more details on secret reference syntax, see the [1Password CLI documentation](https://developer.1password.com/docs/cli/secret-reference-syntax/#ssh-format-parameter).
## 💙 Community & Support
- File an [issue](https://github.com/1Password/load-secrets-action/issues) for bugs and feature requests.

File diff suppressed because one or more lines are too long

BIN
dist/core_bg.wasm vendored Normal file

Binary file not shown.

2603
dist/index.js vendored

File diff suppressed because one or more lines are too long

97
package-lock.json generated
View File

@@ -1,15 +1,16 @@
{
"name": "load-secrets-action",
"version": "3.0.0",
"version": "3.1.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "load-secrets-action",
"version": "3.0.0",
"version": "3.1.0",
"license": "MIT",
"dependencies": {
"@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",
@@ -72,6 +73,21 @@
"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",
@@ -172,7 +188,6 @@
"integrity": "sha512-i1SLeK+DzNnQ3LL/CswPCa/E5u4lh1k6IAEphON8F+cXt0t9euTshDru0q7/IqMa1PMPz5RnHuHscF8/ZJsStg==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"@ampproject/remapping": "^2.2.0",
"@babel/code-frame": "^7.26.0",
@@ -708,6 +723,7 @@
"integrity": "sha512-269Z39MS6wVJtsoUl10L60WdkhJVdPG24Q4eZTH3nnF6lpvSShEK3wQjDX9JRWAUPvPh7COouPpU9IrqaZFvtQ==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"ajv": "^6.12.4",
"debug": "^4.3.2",
@@ -731,7 +747,8 @@
"resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz",
"integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==",
"dev": true,
"license": "Python-2.0"
"license": "Python-2.0",
"peer": true
},
"node_modules/@eslint/eslintrc/node_modules/js-yaml": {
"version": "4.1.1",
@@ -739,6 +756,7 @@
"integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"argparse": "^2.0.1"
},
@@ -752,6 +770,7 @@
"integrity": "sha512-d9zaMRSTIKDLhctzH12MtXvJKSSUhaHcjV+2Z+GK+EEY7XKpP5yR4x+N3TAcHTcu963nIr+TMcCb4DBCYX1z6Q==",
"dev": true,
"license": "MIT",
"peer": true,
"engines": {
"node": "^12.22.0 || ^14.17.0 || >=16.0.0"
}
@@ -772,6 +791,7 @@
"deprecated": "Use @eslint/config-array instead",
"dev": true,
"license": "Apache-2.0",
"peer": true,
"dependencies": {
"@humanwhocodes/object-schema": "^2.0.3",
"debug": "^4.3.1",
@@ -787,6 +807,7 @@
"integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==",
"dev": true,
"license": "Apache-2.0",
"peer": true,
"engines": {
"node": ">=12.22"
},
@@ -801,7 +822,8 @@
"integrity": "sha512-93zYdMES/c1D69yZiKDBj0V24vqNzB/koF26KPaagAfd3P/4gUlh3Dys5ogAK+Exi9QyzlD8x/08Zt7wIKcDcA==",
"deprecated": "Use @eslint/object-schema instead",
"dev": true,
"license": "BSD-3-Clause"
"license": "BSD-3-Clause",
"peer": true
},
"node_modules/@istanbuljs/load-nyc-config": {
"version": "1.1.0",
@@ -1456,7 +1478,6 @@
"integrity": "sha512-tbsV1jPne5CkFQCgPBcDOt30ItF7aJoZL997JSF7MhGQqOeT3svWRYxiqlfA5RUdlHN6Fi+EI9bxqbdyAUZjYQ==",
"dev": true,
"license": "BSD-2-Clause",
"peer": true,
"dependencies": {
"@typescript-eslint/scope-manager": "6.21.0",
"@typescript-eslint/types": "6.21.0",
@@ -1644,7 +1665,8 @@
"resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.2.1.tgz",
"integrity": "sha512-fEzPV3hSkSMltkw152tJKNARhOupqbH96MZWyRjNaYZOMIzbrTeQDG+MTc6Mr2pgzFQzFxAfmhGDNP5QK++2ZA==",
"dev": true,
"license": "ISC"
"license": "ISC",
"peer": true
},
"node_modules/@vercel/ncc": {
"version": "0.38.3",
@@ -1676,6 +1698,7 @@
"integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==",
"dev": true,
"license": "MIT",
"peer": true,
"peerDependencies": {
"acorn": "^6.0.0 || ^7.0.0 || ^8.0.0"
}
@@ -1686,6 +1709,7 @@
"integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"fast-deep-equal": "^3.1.1",
"fast-json-stable-stringify": "^2.0.0",
@@ -2169,7 +2193,6 @@
}
],
"license": "MIT",
"peer": true,
"dependencies": {
"caniuse-lite": "^1.0.30001688",
"electron-to-chromium": "^1.5.73",
@@ -2664,7 +2687,8 @@
"resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz",
"integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==",
"dev": true,
"license": "MIT"
"license": "MIT",
"peer": true
},
"node_modules/deepmerge": {
"version": "4.3.1",
@@ -2751,6 +2775,7 @@
"integrity": "sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==",
"dev": true,
"license": "Apache-2.0",
"peer": true,
"dependencies": {
"esutils": "^2.0.2"
},
@@ -3409,6 +3434,7 @@
"integrity": "sha512-dOt21O7lTMhDM+X9mB4GX+DZrZtCUJPL/wlcTqxyrx5IvO0IYtILdtrQGQp+8n5S0gwSVmOf9NQrjMOgfQZlIg==",
"dev": true,
"license": "BSD-2-Clause",
"peer": true,
"dependencies": {
"esrecurse": "^4.3.0",
"estraverse": "^5.2.0"
@@ -3438,7 +3464,8 @@
"resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz",
"integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==",
"dev": true,
"license": "Python-2.0"
"license": "Python-2.0",
"peer": true
},
"node_modules/eslint/node_modules/find-up": {
"version": "5.0.0",
@@ -3446,6 +3473,7 @@
"integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"locate-path": "^6.0.0",
"path-exists": "^4.0.0"
@@ -3463,6 +3491,7 @@
"integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"argparse": "^2.0.1"
},
@@ -3476,6 +3505,7 @@
"integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"p-locate": "^5.0.0"
},
@@ -3492,6 +3522,7 @@
"integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"p-limit": "^3.0.2"
},
@@ -3508,6 +3539,7 @@
"integrity": "sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ==",
"dev": true,
"license": "BSD-2-Clause",
"peer": true,
"dependencies": {
"acorn": "^8.9.0",
"acorn-jsx": "^5.3.2",
@@ -3553,6 +3585,7 @@
"integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==",
"dev": true,
"license": "BSD-2-Clause",
"peer": true,
"dependencies": {
"estraverse": "^5.2.0"
},
@@ -3642,7 +3675,8 @@
"resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",
"integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==",
"dev": true,
"license": "MIT"
"license": "MIT",
"peer": true
},
"node_modules/fast-glob": {
"version": "3.3.2",
@@ -3686,7 +3720,8 @@
"resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz",
"integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==",
"dev": true,
"license": "MIT"
"license": "MIT",
"peer": true
},
"node_modules/fastq": {
"version": "1.17.1",
@@ -3714,6 +3749,7 @@
"integrity": "sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"flat-cache": "^3.0.4"
},
@@ -3787,6 +3823,7 @@
"integrity": "sha512-CYcENa+FtcUKLmhhqyctpclsq7QF38pKjZHsGNiSQF5r4FtoKDWabFDl3hzaEQMvT1LHEysw5twgLvpYYb4vbw==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"flatted": "^3.2.9",
"keyv": "^4.5.3",
@@ -3801,7 +3838,8 @@
"resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.2.tgz",
"integrity": "sha512-AiwGJM8YcNOaobumgtng+6NHuOqC3A7MixFeDafM3X9cIUM+xUXoS5Vfgf+OihAYe20fxqNM9yPBXJzRtZ/4eA==",
"dev": true,
"license": "ISC"
"license": "ISC",
"peer": true
},
"node_modules/for-each": {
"version": "0.3.3",
@@ -4002,6 +4040,7 @@
"integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==",
"dev": true,
"license": "ISC",
"peer": true,
"dependencies": {
"is-glob": "^4.0.3"
},
@@ -4015,6 +4054,7 @@
"integrity": "sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"type-fest": "^0.20.2"
},
@@ -4240,6 +4280,7 @@
"integrity": "sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"parent-module": "^1.0.0",
"resolve-from": "^4.0.0"
@@ -4606,6 +4647,7 @@
"integrity": "sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==",
"dev": true,
"license": "MIT",
"peer": true,
"engines": {
"node": ">=8"
}
@@ -4896,7 +4938,6 @@
"integrity": "sha512-NIy3oAFp9shda19hy4HK0HRTWKtPJmGdnvywu01nOqNC2vZg+Z+fvJDxpMQA88eb2I9EcafcdjYgsDthnYTvGw==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"@jest/core": "^29.7.0",
"@jest/types": "^29.6.3",
@@ -5555,7 +5596,8 @@
"resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz",
"integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==",
"dev": true,
"license": "MIT"
"license": "MIT",
"peer": true
},
"node_modules/json-parse-even-better-errors": {
"version": "2.3.1",
@@ -5569,14 +5611,16 @@
"resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz",
"integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==",
"dev": true,
"license": "MIT"
"license": "MIT",
"peer": true
},
"node_modules/json-stable-stringify-without-jsonify": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz",
"integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==",
"dev": true,
"license": "MIT"
"license": "MIT",
"peer": true
},
"node_modules/json5": {
"version": "2.2.3",
@@ -5613,6 +5657,7 @@
"integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"json-buffer": "3.0.1"
}
@@ -5663,6 +5708,7 @@
"integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"prelude-ls": "^1.2.1",
"type-check": "~0.4.0"
@@ -5919,7 +5965,8 @@
"resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz",
"integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==",
"dev": true,
"license": "MIT"
"license": "MIT",
"peer": true
},
"node_modules/log-update": {
"version": "6.1.0",
@@ -6374,6 +6421,7 @@
"integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"deep-is": "^0.1.3",
"fast-levenshtein": "^2.0.6",
@@ -6447,6 +6495,7 @@
"integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"callsites": "^3.0.0"
},
@@ -6592,6 +6641,7 @@
"integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==",
"dev": true,
"license": "MIT",
"peer": true,
"engines": {
"node": ">= 0.8.0"
}
@@ -6680,6 +6730,7 @@
"integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==",
"dev": true,
"license": "MIT",
"peer": true,
"engines": {
"node": ">=6"
}
@@ -6828,6 +6879,7 @@
"integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==",
"dev": true,
"license": "MIT",
"peer": true,
"engines": {
"node": ">=4"
}
@@ -6913,6 +6965,7 @@
"deprecated": "Rimraf versions prior to v4 are no longer supported",
"dev": true,
"license": "ISC",
"peer": true,
"dependencies": {
"glob": "^7.1.3"
},
@@ -7535,7 +7588,8 @@
"resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz",
"integrity": "sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==",
"dev": true,
"license": "MIT"
"license": "MIT",
"peer": true
},
"node_modules/tmpl": {
"version": "1.0.5",
@@ -7710,6 +7764,7 @@
"integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"prelude-ls": "^1.2.1"
},
@@ -7733,6 +7788,7 @@
"integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==",
"dev": true,
"license": "(MIT OR CC0-1.0)",
"peer": true,
"engines": {
"node": ">=10"
},
@@ -7824,7 +7880,6 @@
"integrity": "sha512-i5t66RHxDvVN40HfDd1PsEThGNnlMCMT3jMUuoh9/0TaqWevNontacunWyN02LA9/fIbEWlcHZcgTKb9QoaLfg==",
"dev": true,
"license": "Apache-2.0",
"peer": true,
"bin": {
"tsc": "bin/tsc",
"tsserver": "bin/tsserver"
@@ -7908,6 +7963,7 @@
"integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==",
"dev": true,
"license": "BSD-2-Clause",
"peer": true,
"dependencies": {
"punycode": "^2.1.0"
}
@@ -8046,6 +8102,7 @@
"integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==",
"dev": true,
"license": "MIT",
"peer": true,
"engines": {
"node": ">=0.10.0"
}

View File

@@ -1,6 +1,6 @@
{
"name": "load-secrets-action",
"version": "3.0.0",
"version": "3.1.0",
"description": "Load Secrets from 1Password",
"main": "dist/index.js",
"directories": {
@@ -41,6 +41,7 @@
"homepage": "https://github.com/1Password/load-secrets-action#readme",
"dependencies": {
"@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",

View File

@@ -3,7 +3,7 @@ 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";
import { envFilePath, envConnectHost, envConnectToken } from "./constants";
const loadSecretsAction = async () => {
try {
@@ -26,8 +26,12 @@ const loadSecretsAction = async () => {
dotenv.config({ path: file });
}
// Download and install the CLI
await installCLI();
const isConnect =
process.env[envConnectHost] && process.env[envConnectToken];
// If Connect is used, download and install the CLI
if (isConnect) {
await installCLI();
}
// Load secrets
await loadSecrets(shouldExportEnv);

View File

@@ -1,6 +1,7 @@
import * as core from "@actions/core";
import * as exec from "@actions/exec";
import { read, setClientInfo } from "@1password/op-js";
import { createClient, Secrets } from "@1password/sdk";
import {
extractSecret,
loadSecrets,
@@ -22,6 +23,13 @@ jest.mock("@actions/exec", () => ({
})),
}));
jest.mock("@1password/op-js");
jest.mock("@1password/sdk", () => ({
createClient: jest.fn(),
// eslint-disable-next-line @typescript-eslint/naming-convention
Secrets: {
validateSecretReference: jest.fn(),
},
}));
beforeEach(() => {
jest.clearAllMocks();
@@ -106,9 +114,50 @@ describe("extractSecret", () => {
);
expect(core.setSecret).toHaveBeenCalledWith(testSecretValue);
});
describe("when secret value is empty string", () => {
const emptySecretValue = "";
beforeEach(() => {
(read.parse as jest.Mock).mockReturnValue(emptySecretValue);
});
afterEach(() => {
(read.parse as jest.Mock).mockReturnValue(testSecretValue);
});
it("should set empty string as step output", () => {
extractSecret(envTestSecretEnv, false);
expect(core.setOutput).toHaveBeenCalledWith(
envTestSecretEnv,
emptySecretValue,
);
expect(core.exportVariable).not.toHaveBeenCalled();
});
it("should set empty string as environment variable", () => {
extractSecret(envTestSecretEnv, true);
expect(core.exportVariable).toHaveBeenCalledWith(
envTestSecretEnv,
emptySecretValue,
);
expect(core.setOutput).not.toHaveBeenCalled();
});
it("should not call setSecret for empty string", () => {
extractSecret(envTestSecretEnv, false);
expect(core.setSecret).not.toHaveBeenCalled();
});
});
});
describe("loadSecrets", () => {
describe("loadSecrets when using Connect", () => {
beforeEach(() => {
process.env[envConnectHost] = "https://localhost:8000";
process.env[envConnectToken] = "token";
process.env[envServiceAccountToken] = "";
});
it("sets the client info and gets the executed output", async () => {
await loadSecrets(true);
@@ -146,6 +195,199 @@ describe("loadSecrets", () => {
});
});
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";

View File

@@ -1,6 +1,7 @@
import * as core from "@actions/core";
import * as exec from "@actions/exec";
import { read, setClientInfo, semverToInt } from "@1password/op-js";
import { createClient, Secrets } from "@1password/sdk";
import { version } from "../package.json";
import {
authErr,
@@ -29,32 +30,77 @@ export const validateAuth = (): void => {
core.info(`Authenticated with ${authType}.`);
};
export const extractSecret = (
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}`);
const ref = process.env[envName];
if (!ref) {
return;
}
const secretValue = read.parse(ref);
if (!secretValue) {
return;
}
if (shouldExportEnv) {
core.exportVariable(envName, secretValue);
} else {
core.setOutput(envName, secretValue);
}
core.setSecret(secretValue);
if (secretValue) {
core.setSecret(secretValue);
}
};
export const loadSecrets = async (shouldExportEnv: boolean): Promise<void> => {
// Pass User-Agent Information to the 1Password CLI
export const extractSecret = (
envName: string,
shouldExportEnv: boolean,
): void => {
const ref = process.env[envName];
if (!ref) {
return;
}
const secretValue = read.parse(ref);
if (secretValue === null || secretValue === undefined) {
return;
}
setResolvedSecret(envName, secretValue, shouldExportEnv);
};
// Connect loads secrets via the 1Password CLI
const loadSecretsViaConnect = async (
shouldExportEnv: boolean,
): Promise<void> => {
setClientInfo({
name: "1Password GitHub Action",
id: "GHA",
@@ -79,6 +125,63 @@ export const loadSecrets = async (shouldExportEnv: boolean): Promise<void> => {
}
};
// 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);
};
export const unsetPrevious = (): void => {
if (process.env[envManagedVariables]) {
core.info("Unsetting previous values ...");

View File

@@ -1,3 +0,0 @@
FILE_SECRET=op://acceptance-tests/test-secret/password
FILE_SECRET_IN_SECTION=op://acceptance-tests/test-secret/test-section/password
FILE_MULTILINE_SECRET=op://acceptance-tests/multiline-secret/notesPlain

View File

@@ -0,0 +1,7 @@
#!/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"