Compare commits

..

1 Commits

Author SHA1 Message Date
Jill Regan dd76a122aa Update dists 2026-02-26 11:22:17 -05:00
32 changed files with 32554 additions and 38510 deletions
+30 -149
View File
@@ -20,12 +20,6 @@ on:
required: true
OP_SERVICE_ACCOUNT_TOKEN:
required: true
OP_WORKLOAD_ID:
required: true
OP_ENVIRONMENT_ID:
required: true
OP_INTEGRATION_KEY:
required: true
VAULT:
description: "1Password vault name or UUID"
required: true
@@ -36,22 +30,21 @@ 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@v6
uses: actions/checkout@v5
with:
fetch-depth: 0
ref: ${{ inputs.ref }}
- name: Setup Node.js
uses: actions/setup-node@v6
uses: actions/setup-node@v4
with:
node-version: 24
node-version: 20
cache: npm
- name: Install dependencies
@@ -63,15 +56,10 @@ jobs:
- name: Generate .env.tpl
shell: bash
run: |
echo "FILE_SECRET=op://${VAULT}/test-secret/password" > tests/.env.tpl
echo "FILE_SECRET_IN_SECTION=op://${VAULT}/test-secret/test-section/password" >> tests/.env.tpl
echo "FILE_MULTILINE_SECRET=op://${VAULT}/multiline-secret/notesPlain" >> tests/.env.tpl
echo "FILE_WEBSITE=op://${VAULT}/test-secret/website" >> tests/.env.tpl
echo "FILE_TEST_SSH_KEY=op://${VAULT}/test-ssh-key/private key" >> tests/.env.tpl
echo "FILE_TEST_SSH_KEY_OPENSSH=op://${VAULT}/test-ssh-key/private key?ssh-format=openssh" >> tests/.env.tpl
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
env:
VAULT: ${{ secrets.VAULT }}
- name: Configure Service account
uses: ./configure
with:
@@ -87,52 +75,25 @@ 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
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 }}
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 key env vars [exported env]
if: ${{ matrix.export-env }}
shell: bash
run: ./tests/assert-ssh-keys-set.sh
- name: Remove secrets [exported env]
if: ${{ matrix.export-env }}
uses: ./
@@ -144,25 +105,42 @@ 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
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@v6
uses: actions/checkout@v5
with:
fetch-depth: 0
ref: ${{ inputs.ref }}
- name: Setup Node.js
uses: actions/setup-node@v6
uses: actions/setup-node@v4
with:
node-version: 24
node-version: 20
cache: npm
- name: Install dependencies
@@ -174,21 +152,16 @@ jobs:
- name: Generate .env.tpl
run: |
mkdir -p tests
echo "FILE_SECRET=op://${VAULT}/test-secret/password" > tests/.env.tpl
echo "FILE_SECRET_IN_SECTION=op://${VAULT}/test-secret/test-section/password" >> tests/.env.tpl
echo "FILE_MULTILINE_SECRET=op://${VAULT}/multiline-secret/notesPlain" >> tests/.env.tpl
echo "FILE_TEST_SSH_KEY=op://${VAULT}/test-ssh-key/private key" >> tests/.env.tpl
echo "FILE_TEST_SSH_KEY_OPENSSH=op://${VAULT}/test-ssh-key/private key?ssh-format=openssh" >> tests/.env.tpl
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
env:
VAULT: ${{ secrets.VAULT }}
- 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
timeout 60 bash -c 'until curl -sf http://localhost:8080/health >/dev/null 2>&1; do sleep 2; done'
docker compose -f tests/fixtures/docker-compose.yml up -d && sleep 10
- name: Configure 1Password Connect
uses: ./configure
@@ -206,45 +179,23 @@ 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
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 }}
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 key env vars [exported env]
if: ${{ matrix.export-env }}
run: ./tests/assert-ssh-keys-set.sh
- name: Remove secrets [exported env]
if: ${{ matrix.export-env }}
uses: ./
@@ -254,73 +205,3 @@ jobs:
- name: Assert removed secrets [exported env]
if: ${{ matrix.export-env }}
run: ./tests/assert-env-unset.sh
test-workload-identity:
name: Workload Identity (ubuntu-latest, export-env=${{ matrix.export-env }})
runs-on: ubuntu-latest
permissions:
id-token: write
contents: read
strategy:
fail-fast: true
matrix:
export-env: [true, false]
steps:
- name: Checkout
uses: actions/checkout@v6
with:
fetch-depth: 0
ref: ${{ inputs.ref }}
- name: Setup Node.js
uses: actions/setup-node@v6
with:
node-version: 24
cache: npm
- name: Install dependencies
run: npm ci
- name: Build actions
run: npm run build:all
- name: Load secrets
id: load_secrets
uses: ./
with:
export-env: ${{ matrix.export-env }}
env:
OP_WORKLOAD_ID: ${{ secrets.OP_WORKLOAD_ID }}
OP_ENVIRONMENT_ID: ${{ secrets.OP_ENVIRONMENT_ID }}
OP_INTEGRATION_KEY: ${{ secrets.OP_INTEGRATION_KEY }}
- name: Assert test secret values [step output]
if: ${{ !matrix.export-env }}
shell: bash
env:
ANOTHER_TEST: ${{ steps.load_secrets.outputs.ANOTHER_TEST }}
SUPER_SECRET: ${{ steps.load_secrets.outputs.SUPER_SECRET }}
TEST_SECRET: ${{ steps.load_secrets.outputs.TEST_SECRET }}
run: ./tests/assert-workload-identity.sh
- name: Assert test secret values [exported env]
if: ${{ matrix.export-env }}
shell: bash
run: ./tests/assert-workload-identity.sh
- name: Remove secrets [exported env]
if: ${{ matrix.export-env }}
uses: ./
with:
unset-previous: true
- name: Assert removed secrets [exported env]
if: ${{ matrix.export-env }}
shell: bash
run: |
for var in ANOTHER_TEST SUPER_SECRET TEST_SECRET; do
if [ -n "$(printenv "$var")" ]; then
echo "Expected secret $var to be unset"
exit 1
fi
done
+4 -4
View File
@@ -9,18 +9,18 @@ jobs:
lint-and-test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- uses: actions/checkout@v5
- name: Run ShellCheck
uses: ludeeus/action-shellcheck@00cae500b08a931fb5698e11e79bfbd38e612a38 # 2.0.0
uses: ludeeus/action-shellcheck@2.0.0
with:
ignore_paths: >-
.husky
- name: Setup Node.js
uses: actions/setup-node@v6
uses: actions/setup-node@v4
with:
node-version: 24
node-version: 20
cache: npm
- name: Install dependencies
+1 -1
View File
@@ -15,7 +15,7 @@ jobs:
if: ${{ github.event.issue.pull_request }}
steps:
- name: Slash Command Dispatch
uses: peter-evans/slash-command-dispatch@9bdcd7914ec1b75590b790b844aa3b8eee7c683a # v5
uses: peter-evans/slash-command-dispatch@v5
with:
token: ${{ secrets.GITHUB_TOKEN }}
reaction-token: ${{ secrets.GITHUB_TOKEN }}
@@ -10,4 +10,4 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Check signed commits in PR
uses: 1Password/check-signed-commits-action@ed2885f3ed2577a4f5d3c3fe895432a557d23d52 # v1
uses: 1Password/check-signed-commits-action@v1
+2 -7
View File
@@ -65,7 +65,7 @@ jobs:
echo "condition=skip" >> $GITHUB_OUTPUT
echo "Setting condition=skip (sha does not match or empty)"
fi
elif [ "${{ github.event_name }}" == "push" ] && [ "${REF_NAME}" == "main" ]; then
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
@@ -75,8 +75,6 @@ jobs:
echo "Setting condition=skip (unknown event type: ${{ github.event_name }})"
fi
env:
REF_NAME: ${{ github.ref_name }}
e2e:
needs: check-external-pr
if: |
@@ -92,9 +90,6 @@ jobs:
OP_CONNECT_CREDENTIALS: ${{ secrets.OP_CONNECT_CREDENTIALS }}
OP_CONNECT_TOKEN: ${{ secrets.OP_CONNECT_TOKEN }}
OP_SERVICE_ACCOUNT_TOKEN: ${{ secrets.OP_SERVICE_ACCOUNT_TOKEN }}
OP_WORKLOAD_ID: ${{ secrets.OP_WORKLOAD_ID }}
OP_ENVIRONMENT_ID: ${{ secrets.OP_ENVIRONMENT_ID }}
OP_INTEGRATION_KEY: ${{ secrets.OP_INTEGRATION_KEY }}
VAULT: ${{ secrets.VAULT }}
# Post comment on fork PRs after /ok-to-test
@@ -110,7 +105,7 @@ jobs:
run: echo "run-url=https://github.com/$GITHUB_REPOSITORY/actions/runs/$GITHUB_RUN_ID" >> $GITHUB_OUTPUT
- name: Create comment on PR
uses: peter-evans/create-or-update-comment@e8674b075228eee787fea43ef493e45ece1004c9 # v5
uses: peter-evans/create-or-update-comment@v5
with:
issue-number: ${{ github.event.client_payload.pull_request.number }}
body: |
+3 -34
View File
@@ -17,8 +17,6 @@ Specify in your workflow YAML file which secrets from 1Password should be loaded
Read more on the [1Password Developer Portal](https://developer.1password.com/docs/ci-cd/github-actions).
_This project is licensed under [MIT](./LICENSE). Use of the 1Password APIs and services accessed through these tools is governed by the [1Password API Terms of Service](https://1password.com/legal/api-sdk-terms-of-service)._
## 🪄 See it in action!
[![Using 1Password Service Accounts with GitHub Actions - showcase](https://img.youtube.com/vi/kVBl5iQYgSA/maxresdefault.jpg)](https://www.youtube.com/watch?v=kVBl5iQYgSA "Using 1Password Service Accounts with GitHub Actions")
@@ -37,7 +35,7 @@ jobs:
- name: Load secret
id: load_secrets
uses: 1password/load-secrets-action@v4
uses: 1password/load-secrets-action@v3
env:
OP_SERVICE_ACCOUNT_TOKEN: ${{ secrets.OP_SERVICE_ACCOUNT_TOKEN }}
SECRET: op://app-cicd/hello-world/secret
@@ -59,7 +57,7 @@ jobs:
- uses: actions/checkout@v4
- name: Load secret
uses: 1password/load-secrets-action@v4
uses: 1password/load-secrets-action@v3
with:
# Export loaded secrets as environment variables
export-env: true
@@ -79,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@v4
uses: 1password/load-secrets-action@v3
env:
OP_SERVICE_ACCOUNT_TOKEN: ${{ secrets.OP_SERVICE_ACCOUNT_TOKEN }}
# Load SSH private key in OpenSSH format
@@ -88,35 +86,6 @@ When loading SSH keys, you can specify the format using the `ssh-format` query p
For more details on secret reference syntax, see the [1Password CLI documentation](https://developer.1password.com/docs/cli/secret-reference-syntax/#ssh-format-parameter).
## 🧪 Workload Identity (private beta)
> [!NOTE]
> Workload Identity is in **private beta**. It's available to invited participants only. [Contact 1Password](https://developer.1password.com/joinslack) if you're interested in joining the beta.
Instead of a Service Account token or Connect credentials, you can authenticate using Workload Identity, which exchanges your GitHub Actions OIDC token for short-lived 1Password access. To use it, set all three of the following environment variables (and do not set the Service Account token or the Connect variables):
```yml
on: push
jobs:
hello-world:
runs-on: ubuntu-latest
permissions:
id-token: write # required for the action to request a GitHub OIDC token
contents: read
steps:
- name: Load secret
id: load_secrets
uses: 1password/load-secrets-action@v5beta
env:
OP_WORKLOAD_ID: ${{ vars.OP_WORKLOAD_ID }}
OP_ENVIRONMENT_ID: ${{ vars.OP_ENVIRONMENT_ID }}
OP_INTEGRATION_KEY: ${{ secrets.OP_INTEGRATION_KEY }}
```
Unlike the Service Account and Connect flows, you don't select secrets with individual `op://` references. Instead, **all variables defined in the configured 1Password environment are loaded** and each one is exported as an environment variable (or set as a step output). Scope your environment to only the variables you want available to the job.
If only some of the three variables are set, or if they're combined with another authentication method, the action fails with a configuration error.
## 💙 Community & Support
- File an [issue](https://github.com/1Password/load-secrets-action/issues) for bugs and feature requests.
+1 -1
View File
@@ -15,5 +15,5 @@ inputs:
description: Specify which 1Password CLI version to install. Defaults to "latest".
default: "latest"
runs:
using: "node24"
using: "node20"
main: "dist/index.js"
+1 -7
View File
@@ -10,12 +10,6 @@ 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",
"^@1password/sdk$": "<rootDir>/__mocks__/1password-sdk.ts",
},
transform: {
".ts": [
"ts-jest",
@@ -31,4 +25,4 @@ const jestConfig = {
verbose: true,
};
module.exports = jestConfig;
export default jestConfig;
+1 -1
View File
@@ -9,5 +9,5 @@ inputs:
service-account-token:
description: Your 1Password service account token
runs:
using: "node24"
using: "node20"
main: "dist/index.js"
+14524 -17958
View File
File diff suppressed because one or more lines are too long
+1 -1
View File
@@ -1,4 +1,4 @@
import * as core from "@actions/core";
const core = require("@actions/core");
const configure = () => {
const OP_CONNECT_HOST =
BIN
View File
Binary file not shown.
+17241 -19505
View File
File diff suppressed because one or more lines are too long
+141 -63
View File
@@ -1,19 +1,19 @@
{
"name": "load-secrets-action",
"version": "5.0.0-beta.1",
"version": "3.1.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "load-secrets-action",
"version": "5.0.0-beta.1",
"version": "3.1.0",
"license": "MIT",
"dependencies": {
"@1password/op-js": "^0.1.11",
"@1password/sdk": "0.5.0-beta.1",
"@actions/core": "^3.0.0",
"@actions/exec": "^3.0.0",
"@actions/tool-cache": "^4.0.0",
"@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"
},
"devDependencies": {
@@ -74,64 +74,73 @@
}
},
"node_modules/@1password/sdk": {
"version": "0.5.0-beta.1",
"resolved": "https://registry.npmjs.org/@1password/sdk/-/sdk-0.5.0-beta.1.tgz",
"integrity": "sha512-GY1kcn86qkb39jt20AyOftEu5Tw/Kyq4f84GOHXKRjur4TvqvzdhapynBBosRcBL+kBrc+E8cx7Tp7GEfqAomw==",
"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.5.0-beta.1"
"@1password/sdk-core": "0.4.0"
}
},
"node_modules/@1password/sdk-core": {
"version": "0.5.0-beta.1",
"resolved": "https://registry.npmjs.org/@1password/sdk-core/-/sdk-core-0.5.0-beta.1.tgz",
"integrity": "sha512-61Q2n0kKYXBVAbW5ZVFqtbK1KX3lUfFi8wdsv+UjIVtbFd+X1GpFbLFs+nPtPgX+Z7oc2tTN/czK0S9Cz4oF/A==",
"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": "3.0.0",
"resolved": "https://registry.npmjs.org/@actions/core/-/core-3.0.0.tgz",
"integrity": "sha512-zYt6cz+ivnTmiT/ksRVriMBOiuoUpDCJJlZ5KPl2/FRdvwU3f7MPh9qftvbkXJThragzUZieit2nyHUyw53Seg==",
"version": "1.11.1",
"resolved": "https://registry.npmjs.org/@actions/core/-/core-1.11.1.tgz",
"integrity": "sha512-hXJCSrkwfA46Vd9Z3q4cpEpHB1rL5NG04+/rbqW9d3+CSvtB1tYe8UTpAlixa1vj0m/ULglfEK2UKxMGxCxv5A==",
"license": "MIT",
"dependencies": {
"@actions/exec": "^3.0.0",
"@actions/http-client": "^4.0.0"
"@actions/exec": "^1.1.1",
"@actions/http-client": "^2.0.1"
}
},
"node_modules/@actions/exec": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/@actions/exec/-/exec-3.0.0.tgz",
"integrity": "sha512-6xH/puSoNBXb72VPlZVm7vQ+svQpFyA96qdDBvhB8eNZOE8LtPf9L4oAsfzK/crCL8YZ+19fKYVnM63Sl+Xzlw==",
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/@actions/exec/-/exec-1.1.1.tgz",
"integrity": "sha512-+sCcHHbVdk93a0XT19ECtO/gIXoxvdsgQLzb2fE2/5sIZmWQuluYyjPQtrtTHdU1YzTZ7bAPN4sITq2xi1679w==",
"license": "MIT",
"dependencies": {
"@actions/io": "^3.0.2"
"@actions/io": "^1.0.1"
}
},
"node_modules/@actions/http-client": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/@actions/http-client/-/http-client-4.0.0.tgz",
"integrity": "sha512-QuwPsgVMsD6qaPD57GLZi9sqzAZCtiJT8kVBCDpLtxhL5MydQ4gS+DrejtZZPdIYyB1e95uCK9Luyds7ybHI3g==",
"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==",
"license": "MIT",
"dependencies": {
"tunnel": "^0.0.6",
"undici": "^6.23.0"
"undici": "^5.25.4"
}
},
"node_modules/@actions/io": {
"version": "3.0.2",
"resolved": "https://registry.npmjs.org/@actions/io/-/io-3.0.2.tgz",
"integrity": "sha512-nRBchcMM+QK1pdjO7/idu86rbJI5YHUKCvKs0KxnSYbVe3F51UfGxuZX4Qy/fWlp6l7gWFwIkrOzN+oUK03kfw=="
"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"
},
"node_modules/@actions/tool-cache": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/@actions/tool-cache/-/tool-cache-4.0.0.tgz",
"integrity": "sha512-L8P9HbXvpvqjZDveb/fdsa55IVC0trfPgQ4ZwGo6r5af6YDVdM9vMGPZ7rgY2fAT9gGj4PSYd6bYlg3p3jD78A==",
"license": "MIT",
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/@actions/tool-cache/-/tool-cache-2.0.2.tgz",
"integrity": "sha512-fBhNNOWxuoLxztQebpOaWu6WeVmuwa77Z+DxIZ1B+OYvGkGQon6kTVg6Z32Cb13WCuw0szqonK+hh03mJV7Z6w==",
"dependencies": {
"@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"
"@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"
}
},
"node_modules/@ampproject/remapping": {
@@ -766,6 +775,15 @@
"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",
@@ -1572,6 +1590,32 @@
}
}
},
"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",
@@ -1660,10 +1704,11 @@
}
},
"node_modules/ajv": {
"version": "6.14.0",
"resolved": "https://registry.npmjs.org/ajv/-/ajv-6.14.0.tgz",
"integrity": "sha512-IWrosm/yrn43eiKqkfkHis7QioDleaXQHdDVPKg0FSwwd/DuvyX79TZnFOnYpB7dcsFAMmtFztZuXPDvSePkFw==",
"version": "6.12.6",
"resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz",
"integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"fast-deep-equal": "^3.1.1",
@@ -2105,13 +2150,14 @@
"license": "MIT"
},
"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==",
"version": "1.1.12",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz",
"integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==",
"dev": true,
"license": "MIT",
"dependencies": {
"balanced-match": "^1.0.0"
"balanced-match": "^1.0.0",
"concat-map": "0.0.1"
}
},
"node_modules/braces": {
@@ -2491,6 +2537,13 @@
"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",
@@ -3714,6 +3767,29 @@
"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",
@@ -3758,10 +3834,11 @@
}
},
"node_modules/flatted": {
"version": "3.4.2",
"resolved": "https://registry.npmjs.org/flatted/-/flatted-3.4.2.tgz",
"integrity": "sha512-PjDse7RzhcPkIJwy5t7KPWQSZ9cAbzQXcafsetQoD7sOJRQlGikNbx7yZp2OotDnJyrDcbyRq3Ttb18iYOqkxA==",
"version": "3.3.2",
"resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.2.tgz",
"integrity": "sha512-AiwGJM8YcNOaobumgtng+6NHuOqC3A7MixFeDafM3X9cIUM+xUXoS5Vfgf+OihAYe20fxqNM9yPBXJzRtZ/4eA==",
"dev": true,
"license": "ISC",
"peer": true
},
"node_modules/for-each": {
@@ -6135,19 +6212,16 @@
}
},
"node_modules/minimatch": {
"version": "9.0.9",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.9.tgz",
"integrity": "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==",
"version": "3.1.2",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
"integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==",
"dev": true,
"license": "ISC",
"dependencies": {
"brace-expansion": "^2.0.2"
"brace-expansion": "^1.1.7"
},
"engines": {
"node": ">=16 || 14 >=14.17"
},
"funding": {
"url": "https://github.com/sponsors/isaacs"
"node": "*"
}
},
"node_modules/minimist": {
@@ -6965,9 +7039,9 @@
}
},
"node_modules/semver": {
"version": "7.7.4",
"resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz",
"integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==",
"version": "7.6.3",
"resolved": "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz",
"integrity": "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==",
"license": "ISC",
"bin": {
"semver": "bin/semver.js"
@@ -7834,11 +7908,15 @@
}
},
"node_modules/undici": {
"version": "6.24.1",
"resolved": "https://registry.npmjs.org/undici/-/undici-6.24.1.tgz",
"integrity": "sha512-sC+b0tB1whOCzbtlx20fx3WgCXwkW627p4EA9uM+/tNNPkSS+eSEld6pAs9nDv7WbY1UUljBMYPtu9BCOrCWKA==",
"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"
},
"engines": {
"node": ">=18.17"
"node": ">=14.0"
}
},
"node_modules/undici-types": {
+5 -8
View File
@@ -1,6 +1,6 @@
{
"name": "load-secrets-action",
"version": "5.0.0-beta.1",
"version": "3.1.0",
"description": "Load Secrets from 1Password",
"main": "dist/index.js",
"directories": {
@@ -40,16 +40,13 @@
},
"homepage": "https://github.com/1Password/load-secrets-action#readme",
"dependencies": {
"@1password/sdk": "0.5.0-beta.1",
"@1password/op-js": "^0.1.11",
"@actions/core": "^3.0.0",
"@actions/exec": "^3.0.0",
"@actions/tool-cache": "^4.0.0",
"@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"
},
"overrides": {
"minimatch": "^9.0.7"
},
"devDependencies": {
"@1password/eslint-config": "^4.3.1",
"@1password/prettier-config": "^1.2.0",
-1
View File
@@ -1 +0,0 @@
export const createClient = jest.fn();
-16
View File
@@ -1,16 +0,0 @@
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),
// eslint-disable-next-line @typescript-eslint/naming-convention
getIDToken: jest.fn().mockResolvedValue("mock-oidc-token"),
};
-5
View File
@@ -1,5 +0,0 @@
module.exports = {
getExecOutput: jest.fn(() => ({
stdout: "MOCK_SECRET",
})),
};
-10
View File
@@ -1,10 +0,0 @@
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?]>(() => ""),
};
-3
View File
@@ -3,8 +3,5 @@ export const envConnectToken = "OP_CONNECT_TOKEN";
export const envServiceAccountToken = "OP_SERVICE_ACCOUNT_TOKEN";
export const envManagedVariables = "OP_MANAGED_VARIABLES";
export const envFilePath = "OP_ENV_FILE";
export const envWorkloadId = "OP_WORKLOAD_ID";
export const envEnvironmentId = "OP_ENVIRONMENT_ID";
export const envIntegrationKey = "OP_INTEGRATION_KEY";
export const authErr = `Authentication error with environment variables: you must set either 1) ${envServiceAccountToken}, or 2) both ${envConnectHost} and ${envConnectToken}.`;
+7 -31
View File
@@ -2,15 +2,8 @@ import dotenv from "dotenv";
import * as core from "@actions/core";
import { validateCli } from "@1password/op-js";
import { installCliOnGithubActionRunner } from "./op-cli-installer";
import {
getWorkloadIdentityConfig,
hasCliAuth,
loadSecrets,
unsetPrevious,
validateAuth,
} from "./utils";
import { loadSecretsFromSDK } from "./sdk-client";
import { envFilePath } from "./constants";
import { loadSecrets, unsetPrevious, validateAuth } from "./utils";
import { envFilePath, envConnectHost, envConnectToken } from "./constants";
const loadSecretsAction = async () => {
try {
@@ -23,26 +16,6 @@ const loadSecretsAction = async () => {
unsetPrevious();
}
const workloadConfig = getWorkloadIdentityConfig();
// `unset-previous` can run with no credentials present: Workload Identity creds
// are inline per-step and intentionally not persisted (persisting them would make
// every later step re-load all variables). Nothing to auth or load, we're done.
if (shouldUnsetPrevious && !workloadConfig && !hasCliAuth()) {
core.info(
"No authentication configured; unset previously managed variables. No secrets were loaded.",
);
return;
}
if (workloadConfig) {
await loadSecretsFromSDK(
workloadConfig.workloadId,
workloadConfig.environmentId,
workloadConfig.integrationKey,
shouldExportEnv,
);
} else {
// Validate that a proper authentication configuration is set for the CLI
validateAuth();
@@ -53,12 +26,15 @@ const loadSecretsAction = async () => {
dotenv.config({ path: file });
}
// Download and install the CLI
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);
}
} catch (error) {
// It's possible for the Error constructor to be modified to be anything
// in JavaScript, so the following code accounts for this possibility.
@@ -1,18 +1,13 @@
import fs from "fs";
import os from "os";
import * as core from "@actions/core";
import * as tc from "@actions/tool-cache";
import {
archMap,
CliInstaller,
cliUrlBuilder,
type SupportedPlatform,
} from "./cli-installer";
import { WindowsInstaller } from "./windows";
jest.mock("fs");
afterEach(() => {
jest.restoreAllMocks();
});
@@ -30,7 +25,9 @@ describe("WindowsInstaller", () => {
it("should call install with correct URL", async () => {
const installer = new WindowsInstaller(version);
const installMock = jest.spyOn(installer, "install").mockResolvedValue();
const installMock = jest
.spyOn(CliInstaller.prototype, "install")
.mockResolvedValue();
await installer.installCli();
@@ -38,23 +35,4 @@ describe("WindowsInstaller", () => {
const url = builder(version, installer.arch);
expect(installMock).toHaveBeenCalledWith(url);
});
it("should rename downloaded file with .zip extension before extracting", async () => {
const downloadPath = "/tmp/abc-123";
const extractedPath = "/tmp/extracted";
(tc.downloadTool as jest.Mock).mockResolvedValue(downloadPath);
(tc.extractZip as jest.Mock).mockResolvedValue(extractedPath);
const installer = new WindowsInstaller(version);
await installer.installCli();
expect(tc.downloadTool).toHaveBeenCalled();
expect(fs.renameSync).toHaveBeenCalledWith(
downloadPath,
`${downloadPath}.zip`,
);
expect(tc.extractZip).toHaveBeenCalledWith(`${downloadPath}.zip`);
expect(core.addPath).toHaveBeenCalledWith(extractedPath);
});
});
@@ -1,8 +1,3 @@
import * as fs from "fs";
import * as core from "@actions/core";
import * as tc from "@actions/tool-cache";
import {
CliInstaller,
cliUrlBuilder,
@@ -19,19 +14,6 @@ export class WindowsInstaller extends CliInstaller implements Installer {
public async installCli(): Promise<void> {
const urlBuilder = cliUrlBuilder[this.platform];
await this.install(urlBuilder(this.version, this.arch));
}
// Windows PowerShell's Expand-Archive requires files to have a .zip extension.
// tc.downloadTool saves to a UUID filename with no extension, so we rename it.
public override async install(url: string): Promise<void> {
console.info(`Downloading 1Password CLI from: ${url}`);
const downloadPath = await tc.downloadTool(url);
const zipPath = `${downloadPath}.zip`;
fs.renameSync(downloadPath, zipPath);
console.info("Installing 1Password CLI");
const extractedPath = await tc.extractZip(zipPath);
core.addPath(extractedPath);
core.info("1Password CLI installed");
await super.install(urlBuilder(this.version, this.arch));
}
}
-114
View File
@@ -1,114 +0,0 @@
import * as core from "@actions/core";
import { createClient } from "@1password/sdk";
import { envManagedVariables } from "./constants";
import { getOIDCToken, loadSecretsFromSDK } from "./sdk-client";
jest.mock("@1password/sdk");
const mockGetVariables = jest.fn();
beforeEach(() => {
jest.clearAllMocks();
(createClient as jest.Mock).mockResolvedValue({
environments: {
getVariables: mockGetVariables,
},
});
});
describe("getOIDCToken", () => {
it("delegates to core.getIDToken", async () => {
(core.getIDToken as jest.Mock).mockResolvedValue("oidc-token");
await expect(getOIDCToken("test-audience")).resolves.toBe("oidc-token");
expect(core.getIDToken).toHaveBeenCalledWith("test-audience");
});
});
describe("loadSecretsFromSDK", () => {
const workloadId = "workload-uuid";
const environmentId = "environment-uuid";
const integrationKey = "integration-key";
const variables = [
{ name: "DOCKERHUB_USERNAME", value: "myuser" },
{ name: "DOCKERHUB_TOKEN", value: "mypassword" },
];
beforeEach(() => {
mockGetVariables.mockResolvedValue({ variables });
});
it("sets secrets as step outputs by default", async () => {
await loadSecretsFromSDK(workloadId, environmentId, integrationKey, false);
expect(core.setOutput).toHaveBeenCalledWith("DOCKERHUB_USERNAME", "myuser");
expect(core.setOutput).toHaveBeenCalledWith(
"DOCKERHUB_TOKEN",
"mypassword",
);
expect(core.exportVariable).not.toHaveBeenCalledWith(
"DOCKERHUB_USERNAME",
"myuser",
);
expect(core.setSecret).toHaveBeenCalledWith("myuser");
expect(core.setSecret).toHaveBeenCalledWith("mypassword");
expect(core.exportVariable).not.toHaveBeenCalledWith(
envManagedVariables,
expect.any(String),
);
});
it("exports secrets as environment variables when shouldExportEnv is true", async () => {
await loadSecretsFromSDK(workloadId, environmentId, integrationKey, true);
expect(core.exportVariable).toHaveBeenCalledWith(
"DOCKERHUB_USERNAME",
"myuser",
);
expect(core.exportVariable).toHaveBeenCalledWith(
"DOCKERHUB_TOKEN",
"mypassword",
);
expect(core.setOutput).not.toHaveBeenCalled();
expect(core.exportVariable).toHaveBeenCalledWith(
envManagedVariables,
"DOCKERHUB_USERNAME,DOCKERHUB_TOKEN",
);
});
describe("when secret value is empty string", () => {
beforeEach(() => {
mockGetVariables.mockResolvedValue({
variables: [{ name: "EMPTY_SECRET", value: "" }],
});
});
it("sets empty string as step output", async () => {
await loadSecretsFromSDK(
workloadId,
environmentId,
integrationKey,
false,
);
expect(core.setOutput).toHaveBeenCalledWith("EMPTY_SECRET", "");
expect(core.setSecret).not.toHaveBeenCalledWith("");
});
it("sets empty string as environment variable", async () => {
await loadSecretsFromSDK(workloadId, environmentId, integrationKey, true);
expect(core.exportVariable).toHaveBeenCalledWith("EMPTY_SECRET", "");
expect(core.setSecret).not.toHaveBeenCalledWith("");
});
});
it("does not export OP_MANAGED_VARIABLES when no variables are returned", async () => {
mockGetVariables.mockResolvedValue({ variables: [] });
await loadSecretsFromSDK(workloadId, environmentId, integrationKey, true);
expect(core.exportVariable).not.toHaveBeenCalled();
});
});
-52
View File
@@ -1,52 +0,0 @@
import * as core from "@actions/core";
import { createClient } from "@1password/sdk";
import { version } from "../package.json";
import { envManagedVariables } from "./constants";
// eslint-disable-next-line @typescript-eslint/naming-convention
export const getOIDCToken = async (audience: string): Promise<string> =>
core.getIDToken(audience);
// eslint-disable-next-line @typescript-eslint/naming-convention
export const loadSecretsFromSDK = async (
workloadId: string,
environmentId: string,
integrationKey: string,
shouldExportEnv: boolean,
): Promise<void> => {
// Temporary fix: strip base64 padding from integrationKey — this will eventually be handled by the SDK core itself
const customerManagedSecret = integrationKey.replace(/=+$/, "");
core.setSecret(customerManagedSecret);
const client = await createClient({
integrationName: "1Password GitHub Action",
integrationVersion: version,
oidcFetcher: getOIDCToken,
workloadDetails: {
customerManagedSecret,
workloadUuid: workloadId,
},
});
core.info("Authenticated with Workload Identity.");
const { variables } = await client.environments.getVariables(environmentId);
const envNames: string[] = [];
for (const { name, value } of variables) {
core.info(`Populating variable: ${name}`);
if (shouldExportEnv) {
core.exportVariable(name, value);
} else {
core.setOutput(name, value);
}
if (value) {
core.setSecret(value);
}
envNames.push(name);
}
if (shouldExportEnv && envNames.length > 0) {
core.exportVariable(envManagedVariables, envNames.join());
}
};
+214 -116
View File
@@ -1,10 +1,9 @@
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,
getWorkloadIdentityConfig,
hasCliAuth,
loadSecrets,
unsetPrevious,
validateAuth,
@@ -13,14 +12,24 @@ import {
authErr,
envConnectHost,
envConnectToken,
envEnvironmentId,
envIntegrationKey,
envManagedVariables,
envServiceAccountToken,
envWorkloadId,
} 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(),
},
}));
beforeEach(() => {
jest.clearAllMocks();
@@ -71,96 +80,6 @@ describe("validateAuth", () => {
});
});
describe("getWorkloadIdentityConfig", () => {
const testWorkloadId = "workload-id";
const testEnvironmentId = "environment-id";
const testIntegrationKey = "integration-key";
beforeEach(() => {
process.env[envWorkloadId] = "";
process.env[envEnvironmentId] = "";
process.env[envIntegrationKey] = "";
process.env[envConnectHost] = "";
process.env[envConnectToken] = "";
process.env[envServiceAccountToken] = "";
});
it("should return null when no variables are set", () => {
expect(getWorkloadIdentityConfig()).toBeNull();
});
it("should return the config when all variables are set", () => {
process.env[envWorkloadId] = testWorkloadId;
process.env[envEnvironmentId] = testEnvironmentId;
process.env[envIntegrationKey] = testIntegrationKey;
expect(getWorkloadIdentityConfig()).toEqual({
workloadId: testWorkloadId,
environmentId: testEnvironmentId,
integrationKey: testIntegrationKey,
});
});
it("should throw an error when only some variables are set", () => {
process.env[envWorkloadId] = testWorkloadId;
expect(getWorkloadIdentityConfig).toThrow(
/Incomplete Workload Identity configuration/,
);
});
it("should throw an error when combined with Connect credentials", () => {
process.env[envWorkloadId] = testWorkloadId;
process.env[envEnvironmentId] = testEnvironmentId;
process.env[envIntegrationKey] = testIntegrationKey;
process.env[envConnectHost] = "https://localhost:8000";
process.env[envConnectToken] = "token";
expect(getWorkloadIdentityConfig).toThrow(
/Conflicting authentication configuration/,
);
});
it("should throw an error when combined with a service account token", () => {
process.env[envWorkloadId] = testWorkloadId;
process.env[envEnvironmentId] = testEnvironmentId;
process.env[envIntegrationKey] = testIntegrationKey;
process.env[envServiceAccountToken] = "ops_token";
expect(getWorkloadIdentityConfig).toThrow(
/Conflicting authentication configuration/,
);
});
});
describe("hasCliAuth", () => {
beforeEach(() => {
process.env[envConnectHost] = "";
process.env[envConnectToken] = "";
process.env[envServiceAccountToken] = "";
});
it("returns false when no CLI auth is configured", () => {
expect(hasCliAuth()).toBe(false);
});
it("returns false when only the Connect host is set", () => {
process.env[envConnectHost] = "https://localhost:8000";
expect(hasCliAuth()).toBe(false);
});
it("returns true with both Connect host and token", () => {
process.env[envConnectHost] = "https://localhost:8000";
process.env[envConnectToken] = "token";
expect(hasCliAuth()).toBe(true);
});
it("returns true with a service account token", () => {
process.env[envServiceAccountToken] = "ops_token";
expect(hasCliAuth()).toBe(true);
});
});
describe("extractSecret", () => {
const envTestSecretEnv = "TEST_SECRET";
const testSecretRef = "op://vault/item/secret";
@@ -232,7 +151,13 @@ describe("extractSecret", () => {
});
});
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);
@@ -270,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";
@@ -285,24 +403,4 @@ describe("unsetPrevious", () => {
expect(core.info).toHaveBeenCalledWith("Unsetting TEST_SECRET");
expect(core.exportVariable).toHaveBeenCalledWith("TEST_SECRET", "");
});
it("should unset every variable listed in OP_MANAGED_VARIABLES", () => {
process.env[envManagedVariables] = "TEST_SECRET,ANOTHER_TEST,SUPER_SECRET";
unsetPrevious();
expect(core.exportVariable).toHaveBeenCalledWith("TEST_SECRET", "");
expect(core.exportVariable).toHaveBeenCalledWith("ANOTHER_TEST", "");
expect(core.exportVariable).toHaveBeenCalledWith("SUPER_SECRET", "");
expect(core.exportVariable).toHaveBeenCalledTimes(3);
});
it("should do nothing when no variables are managed", () => {
process.env[envManagedVariables] = "";
unsetPrevious();
expect(core.exportVariable).not.toHaveBeenCalled();
expect(core.info).not.toHaveBeenCalledWith("Unsetting previous values ...");
});
});
+113 -68
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,
@@ -8,61 +9,8 @@ import {
envConnectToken,
envServiceAccountToken,
envManagedVariables,
envWorkloadId,
envEnvironmentId,
envIntegrationKey,
} from "./constants";
export interface WorkloadIdentityConfig {
workloadId: string;
environmentId: string;
integrationKey: string;
}
// Returns the Workload Identity configuration when all variables are set,
// or null when none are set (so the CLI auth path can be used instead).
// Throws if the configuration is only partially set, or if it is combined
// with the CLI auth methods (Connect / service account).
export const getWorkloadIdentityConfig = (): WorkloadIdentityConfig | null => {
const workloadId = process.env[envWorkloadId];
const environmentId = process.env[envEnvironmentId];
const integrationKey = process.env[envIntegrationKey];
// None set: fall back to the CLI auth path.
if (!workloadId && !environmentId && !integrationKey) {
return null;
}
// Some but not all set: configuration is incomplete.
if (!workloadId || !environmentId || !integrationKey) {
throw new Error(
`Incomplete Workload Identity configuration. To use Workload Identity, set all of ${envWorkloadId}, ${envEnvironmentId}, and ${envIntegrationKey}.`,
);
}
// Workload Identity is fully configured, so it must not be combined with the
// CLI auth methods (Connect / service account), which are mutually exclusive.
if (
process.env[envConnectHost] ||
process.env[envConnectToken] ||
process.env[envServiceAccountToken]
) {
throw new Error(
`Conflicting authentication configuration: Workload Identity cannot be combined with Connect (${envConnectHost}/${envConnectToken}) or a service account (${envServiceAccountToken}). Set only one authentication method.`,
);
}
return { workloadId, environmentId, integrationKey };
};
// Whether CLI authentication (1Password Connect or a service account) is
// configured via environment variables.
export const hasCliAuth = (): boolean =>
Boolean(
(process.env[envConnectHost] && process.env[envConnectToken]) ||
process.env[envServiceAccountToken],
);
export const validateAuth = (): void => {
const isConnect = process.env[envConnectHost] && process.env[envConnectToken];
const isServiceAccount = process.env[envServiceAccountToken];
@@ -82,12 +30,60 @@ 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}`);
if (shouldExportEnv) {
core.exportVariable(envName, secretValue);
} else {
core.setOutput(envName, secretValue);
}
if (secretValue) {
core.setSecret(secretValue);
}
};
export const extractSecret = (
envName: string,
shouldExportEnv: boolean,
): void => {
const ref = process.env[envName];
if (!ref) {
return;
@@ -98,25 +94,17 @@ export const extractSecret = (
return;
}
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);
}
setResolvedSecret(envName, secretValue, shouldExportEnv);
};
export const loadSecrets = async (shouldExportEnv: boolean): Promise<void> => {
// Strip any prerelease suffix; semverToInt only accepts MAJOR.MINOR.PATCH.
const [releaseVersion] = version.split("-");
// Connect loads secrets via the 1Password CLI
const loadSecretsViaConnect = async (
shouldExportEnv: boolean,
): Promise<void> => {
setClientInfo({
name: "1Password GitHub Action",
id: "GHA",
build: semverToInt(releaseVersion ?? version),
build: semverToInt(version),
});
// Load secrets from environment variables using 1Password CLI.
@@ -137,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 ...");
-7
View File
@@ -26,7 +26,6 @@ IApTbyBwbGVhc2UgZG9uJ3QgcmVwb3J0IGl0IQo=
EOF
)"
readonly MULTILINE_SECRET
readonly WEBSITE="www.test.com"
assert_env_equals "SECRET" "${SECRET}"
assert_env_equals "FILE_SECRET" "${SECRET}"
@@ -36,9 +35,3 @@ assert_env_equals "FILE_SECRET_IN_SECTION" "${SECRET}"
assert_env_equals "MULTILINE_SECRET" "${MULTILINE_SECRET}"
assert_env_equals "FILE_MULTILINE_SECRET" "${MULTILINE_SECRET}"
# 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
-8
View File
@@ -17,11 +17,3 @@ assert_env_unset "FILE_SECRET_IN_SECTION"
assert_env_unset "MULTILINE_SECRET"
assert_env_unset "FILE_MULTILINE_SECRET"
assert_env_unset "WEBSITE"
assert_env_unset "FILE_WEBSITE"
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"
+7
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"
-26
View File
@@ -1,26 +0,0 @@
#!/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"
-16
View File
@@ -1,16 +0,0 @@
#!/bin/bash
# shellcheck disable=SC2086
set -e
# Asserts the secrets loaded via Workload Identity.
assert_env_equals() {
if [ "$(printenv $1)" != "$2" ]; then
echo -e "Expected $1 to be set to:\n$2\nBut got:\n$(printenv $1)"
exit 1
fi
}
assert_env_equals "ANOTHER_TEST" "anothertest123"
assert_env_equals "SUPER_SECRET" "supersecret"
assert_env_equals "TEST_SECRET" "thisisatest"