Compare commits
165 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
92467eb28f | ||
|
|
21f21cb312 | ||
|
|
a82b5b0e62 | ||
|
|
355a4641a8 | ||
|
|
50dbf2f36c | ||
|
|
2a602963c2 | ||
|
|
61e04e231d | ||
|
|
93d1217e3f | ||
|
|
17a79a92a6 | ||
|
|
7a0b59f9ab | ||
|
|
a916812f67 | ||
|
|
15f53e00f6 | ||
|
|
911ae9a9d1 | ||
|
|
18c9485047 | ||
|
|
7eb7055d29 | ||
|
|
ebcbcb60ac | ||
|
|
13dac1510b | ||
|
|
960f48270b | ||
|
|
d87677b04d | ||
|
|
bd0f47e27e | ||
|
|
6352983a5d | ||
|
|
be02de5ede | ||
|
|
2b062ec18c | ||
|
|
f4a6c38f2a | ||
|
|
bc8523c04b | ||
|
|
6ecbf76d39 | ||
|
|
2763f7b0b3 | ||
|
|
934acd2a2a | ||
|
|
affe8f4720 | ||
|
|
ad358d4370 | ||
|
|
dafbe7cb03 | ||
|
|
7f98afc2d7 | ||
|
|
9cdab8f59d | ||
|
|
6a14785fa5 | ||
|
|
7d4e24a43f | ||
|
|
cb7c5acc8a | ||
|
|
ea49b14f1e | ||
|
|
93abacc9df | ||
|
|
40e52b6cef | ||
|
|
38b333095d | ||
|
|
5b2af23419 | ||
|
|
652d567877 | ||
|
|
ee92e1fd32 | ||
|
|
d688c27248 | ||
|
|
a312828d43 | ||
|
|
e6b45e828c | ||
|
|
639ddd6614 | ||
|
|
e5d7353d74 | ||
|
|
a665f2c1ab | ||
|
|
398c918d60 | ||
|
|
485265b41c | ||
|
|
dc90451a94 | ||
|
|
9d7acefac9 | ||
|
|
04984a6c91 | ||
|
|
db7314de7b | ||
|
|
3f9ba481c9 | ||
|
|
1e8273d4be | ||
|
|
015b03300e | ||
|
|
ab44f9f69c | ||
|
|
af49dd18de | ||
|
|
d456b72513 | ||
|
|
2a828228a8 | ||
|
|
604a86ce4e | ||
|
|
7998453500 | ||
|
|
e7fe4397d9 | ||
|
|
6911316fe3 | ||
|
|
24235f3b6b | ||
|
|
a2ce22dd39 | ||
|
|
d2fdd9df66 | ||
|
|
95478552e8 | ||
|
|
4a997a0402 | ||
|
|
81bc2a50b4 | ||
|
|
1dfe1fc19e | ||
|
|
856971e6d6 | ||
|
|
5fd6fbcfdf | ||
|
|
13f927c806 | ||
|
|
fdb192f5dc | ||
|
|
13c259d353 | ||
|
|
b91fef0861 | ||
|
|
2d74546fd1 | ||
|
|
8d0d610af1 | ||
|
|
76bec67e89 | ||
|
|
74311b1273 | ||
|
|
5999940e48 | ||
|
|
b43a2248cc | ||
|
|
c2b96b53cd | ||
|
|
6f52eddca2 | ||
|
|
dc5cd4d17f | ||
|
|
b4962e1861 | ||
|
|
2f243ca4fa | ||
|
|
c96389a7ae | ||
|
|
ba38da7905 | ||
|
|
6961848b51 | ||
|
|
fac78884c8 | ||
|
|
2c0496a719 | ||
|
|
483f83267a | ||
|
|
0ee5bc7530 | ||
|
|
fee9db6b39 | ||
|
|
1824b2f006 | ||
|
|
0f3110274c | ||
|
|
80f581e4b5 | ||
|
|
74df766d96 | ||
|
|
df80909445 | ||
|
|
e3aa72700f | ||
|
|
6a721fb6aa | ||
|
|
7ee331322f | ||
|
|
9fb38d43e1 | ||
|
|
bdf1f8ceff | ||
|
|
7c3deef5f9 | ||
|
|
564bf5b01f | ||
|
|
0ff92dd768 | ||
|
|
1850a6b487 | ||
|
|
08a0af8ec3 | ||
|
|
d11f2d1dac | ||
|
|
2c12b97549 | ||
|
|
211132e91f | ||
|
|
13f58eec61 | ||
|
|
f9847b316a | ||
|
|
438a01224c | ||
|
|
ba891d4bf2 | ||
|
|
909c7e01f1 | ||
|
|
1c443d83da | ||
|
|
867fee7815 | ||
|
|
ee4b4919bf | ||
|
|
13f110716c | ||
|
|
7914f19c4d | ||
|
|
25aa72f51b | ||
|
|
2fa8a509ca | ||
|
|
f30ae37660 | ||
|
|
93b787fdef | ||
|
|
29afdd3b50 | ||
|
|
c03c0b6bbe | ||
|
|
cb2930c65f | ||
|
|
65a7f5e592 | ||
|
|
a8d5f2a285 | ||
|
|
f5fc2382af | ||
|
|
596d8007a1 | ||
|
|
c9ae724dfd | ||
|
|
40b6ef7b57 | ||
|
|
a4866d442c | ||
|
|
10084cd57d | ||
|
|
d6b7427345 | ||
|
|
96de656797 | ||
|
|
c0724d8845 | ||
|
|
3a62b7cf63 | ||
|
|
43fd9cdb84 | ||
|
|
73195c1d43 | ||
|
|
85e0e789db | ||
|
|
39cf694bee | ||
|
|
39b7248332 | ||
|
|
a5e5c78980 | ||
|
|
7d16183347 | ||
|
|
0cbceff209 | ||
|
|
fec5c39dcc | ||
|
|
a525a84c53 | ||
|
|
6483669c68 | ||
|
|
06962f2427 | ||
|
|
3e2909a6b2 | ||
|
|
734cd437f8 | ||
|
|
555e0c6a63 | ||
|
|
0a309926fa | ||
|
|
a51c02d593 | ||
|
|
d36634f96f | ||
|
|
a8494ee438 | ||
|
|
904025a654 |
246
.github/workflows/e2e-tests.yml
vendored
Normal file
246
.github/workflows/e2e-tests.yml
vendored
Normal file
@@ -0,0 +1,246 @@
|
||||
name: E2E Tests
|
||||
|
||||
on:
|
||||
# For local testing with: act push -W .github/workflows/e2e-tests.yml
|
||||
push:
|
||||
branches-ignore:
|
||||
- "**" # Never runs on GitHub, only locally with act
|
||||
|
||||
# For test.yml to call this workflow
|
||||
workflow_call:
|
||||
inputs:
|
||||
ref:
|
||||
description: "Git ref to checkout"
|
||||
required: true
|
||||
type: string
|
||||
secrets:
|
||||
OP_CONNECT_CREDENTIALS:
|
||||
required: true
|
||||
OP_CONNECT_TOKEN:
|
||||
required: true
|
||||
OP_SERVICE_ACCOUNT_TOKEN:
|
||||
required: true
|
||||
VAULT:
|
||||
description: "1Password vault name or UUID"
|
||||
required: true
|
||||
|
||||
jobs:
|
||||
test-service-account:
|
||||
name: Service Account (${{ matrix.os }}, ${{ matrix.version }}, export-env=${{ matrix.export-env }})
|
||||
runs-on: ${{ matrix.os }}
|
||||
strategy:
|
||||
fail-fast: true
|
||||
max-parallel: 4
|
||||
matrix:
|
||||
os: [ubuntu-latest, macos-latest, windows-latest]
|
||||
version: [latest, 2.30.0]
|
||||
export-env: [true, false]
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v5
|
||||
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: Generate .env.tpl
|
||||
shell: bash
|
||||
run: |
|
||||
echo "FILE_SECRET=op://${{ secrets.VAULT }}/test-secret/password" > tests/.env.tpl
|
||||
echo "FILE_SECRET_IN_SECTION=op://${{ secrets.VAULT }}/test-secret/test-section/password" >> tests/.env.tpl
|
||||
echo "FILE_MULTILINE_SECRET=op://${{ secrets.VAULT }}/multiline-secret/notesPlain" >> tests/.env.tpl
|
||||
echo "FILE_WEBSITE=op://${{ secrets.VAULT }}/test-secret/website" >> tests/.env.tpl
|
||||
echo "FILE_TEST_SSH_KEY=op://${{ secrets.VAULT }}/test-ssh-key/private key" >> tests/.env.tpl
|
||||
echo "FILE_TEST_SSH_KEY_OPENSSH=op://${{ secrets.VAULT }}/test-ssh-key/private key?ssh-format=openssh" >> tests/.env.tpl
|
||||
|
||||
- name: Configure Service account
|
||||
uses: ./configure
|
||||
with:
|
||||
service-account-token: ${{ secrets.OP_SERVICE_ACCOUNT_TOKEN }}
|
||||
|
||||
- name: Load secrets
|
||||
id: load_secrets
|
||||
uses: ./
|
||||
with:
|
||||
version: ${{ matrix.version }}
|
||||
export-env: ${{ matrix.export-env }}
|
||||
env:
|
||||
SECRET: op://${{ secrets.VAULT }}/test-secret/password
|
||||
SECRET_IN_SECTION: op://${{ secrets.VAULT }}/test-secret/test-section/password
|
||||
MULTILINE_SECRET: op://${{ secrets.VAULT }}/multiline-secret/notesPlain
|
||||
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: ./
|
||||
with:
|
||||
unset-previous: true
|
||||
|
||||
- name: Assert removed secrets [exported env]
|
||||
if: ${{ matrix.export-env }}
|
||||
shell: bash
|
||||
run: ./tests/assert-env-unset.sh
|
||||
|
||||
test-connect:
|
||||
name: Connect (ubuntu-latest, ${{ matrix.version }}, export-env=${{ matrix.export-env }})
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
fail-fast: true
|
||||
matrix:
|
||||
version: [latest, 2.30.0]
|
||||
export-env: [true, false]
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v5
|
||||
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: Generate .env.tpl
|
||||
run: |
|
||||
mkdir -p tests
|
||||
echo "FILE_SECRET=op://${{ secrets.VAULT }}/test-secret/password" > tests/.env.tpl
|
||||
echo "FILE_SECRET_IN_SECTION=op://${{ secrets.VAULT }}/test-secret/test-section/password" >> tests/.env.tpl
|
||||
echo "FILE_MULTILINE_SECRET=op://${{ secrets.VAULT }}/multiline-secret/notesPlain" >> tests/.env.tpl
|
||||
echo "FILE_TEST_SSH_KEY=op://${{ secrets.VAULT }}/test-ssh-key/private key" >> tests/.env.tpl
|
||||
echo "FILE_TEST_SSH_KEY_OPENSSH=op://${{ secrets.VAULT }}/test-ssh-key/private key?ssh-format=openssh" >> tests/.env.tpl
|
||||
|
||||
- name: Launch 1Password Connect instance
|
||||
env:
|
||||
OP_CONNECT_CREDENTIALS: ${{ secrets.OP_CONNECT_CREDENTIALS }}
|
||||
run: |
|
||||
echo "$OP_CONNECT_CREDENTIALS" > 1password-credentials.json
|
||||
docker compose -f tests/fixtures/docker-compose.yml up -d
|
||||
timeout 60 bash -c 'until curl -sf http://localhost:8080/health >/dev/null 2>&1; do sleep 2; done'
|
||||
|
||||
- name: Configure 1Password Connect
|
||||
uses: ./configure
|
||||
with:
|
||||
connect-host: http://localhost:8080
|
||||
connect-token: ${{ secrets.OP_CONNECT_TOKEN }}
|
||||
|
||||
- name: Load secrets
|
||||
id: load_secrets
|
||||
uses: ./
|
||||
with:
|
||||
version: ${{ matrix.version }}
|
||||
export-env: ${{ matrix.export-env }}
|
||||
env:
|
||||
SECRET: op://${{ secrets.VAULT }}/test-secret/password
|
||||
SECRET_IN_SECTION: op://${{ secrets.VAULT }}/test-secret/test-section/password
|
||||
MULTILINE_SECRET: op://${{ secrets.VAULT }}/multiline-secret/notesPlain
|
||||
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: ./
|
||||
with:
|
||||
unset-previous: true
|
||||
|
||||
- name: Assert removed secrets [exported env]
|
||||
if: ${{ matrix.export-env }}
|
||||
run: ./tests/assert-env-unset.sh
|
||||
36
.github/workflows/lint-and-test.yml
vendored
Normal file
36
.github/workflows/lint-and-test.yml
vendored
Normal file
@@ -0,0 +1,36 @@
|
||||
name: Lint and Test
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [main]
|
||||
pull_request:
|
||||
|
||||
jobs:
|
||||
lint-and-test:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
|
||||
- name: Run ShellCheck
|
||||
uses: ludeeus/action-shellcheck@2.0.0
|
||||
with:
|
||||
ignore_paths: >-
|
||||
.husky
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v6
|
||||
with:
|
||||
node-version: 24
|
||||
cache: npm
|
||||
|
||||
- name: Install dependencies
|
||||
run: npm ci
|
||||
|
||||
- name: Check formatting
|
||||
run: npm run format:check
|
||||
|
||||
- name: Check lint
|
||||
run: npm run lint
|
||||
|
||||
- name: Run unit tests
|
||||
run: npm test
|
||||
13
.github/workflows/lint.yml
vendored
13
.github/workflows/lint.yml
vendored
@@ -1,13 +0,0 @@
|
||||
on: 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
|
||||
25
.github/workflows/ok-to-test.yml
vendored
Normal file
25
.github/workflows/ok-to-test.yml
vendored
Normal file
@@ -0,0 +1,25 @@
|
||||
# Write comments "/ok-to-test sha=<hash>" on a pull request. This will emit a repository_dispatch event.
|
||||
name: Ok To Test
|
||||
|
||||
on:
|
||||
issue_comment:
|
||||
types: [created]
|
||||
|
||||
jobs:
|
||||
ok-to-test:
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
pull-requests: write # For adding reactions to the pull request comments
|
||||
contents: write # For executing the repository_dispatch event
|
||||
# Only run for PRs, not issue comments
|
||||
if: ${{ github.event.issue.pull_request }}
|
||||
steps:
|
||||
- name: Slash Command Dispatch
|
||||
uses: peter-evans/slash-command-dispatch@v5
|
||||
with:
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
reaction-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
issue-type: pull-request
|
||||
commands: ok-to-test
|
||||
# The repository permission level required by the user to dispatch commands. Only allows 1Password collaborators to run this.
|
||||
permission: write
|
||||
13
.github/workflows/pr-check-signed-commits.yml
vendored
Normal file
13
.github/workflows/pr-check-signed-commits.yml
vendored
Normal file
@@ -0,0 +1,13 @@
|
||||
name: Check signed commits in PR
|
||||
on: pull_request_target
|
||||
|
||||
jobs:
|
||||
build:
|
||||
name: Check signed commits in PR
|
||||
permissions:
|
||||
contents: read
|
||||
pull-requests: write
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Check signed commits in PR
|
||||
uses: 1Password/check-signed-commits-action@v1
|
||||
120
.github/workflows/test-e2e.yml
vendored
Normal file
120
.github/workflows/test-e2e.yml
vendored
Normal file
@@ -0,0 +1,120 @@
|
||||
name: E2E Tests
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [main]
|
||||
paths-ignore: &ignore_paths
|
||||
- "docs/**"
|
||||
- "config/**"
|
||||
- "*.md"
|
||||
- ".gitignore"
|
||||
- "LICENSE"
|
||||
pull_request:
|
||||
paths-ignore: *ignore_paths
|
||||
repository_dispatch:
|
||||
types: [ok-to-test-command]
|
||||
|
||||
concurrency:
|
||||
group: >-
|
||||
${{ github.event_name == 'pull_request' &&
|
||||
format('e2e-{0}', github.event.pull_request.head.ref) ||
|
||||
format('e2e-{0}', github.ref) }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
check-external-pr:
|
||||
runs-on: ubuntu-latest
|
||||
outputs:
|
||||
condition: ${{ steps.check.outputs.condition }}
|
||||
ref: ${{ steps.check.outputs.ref }}
|
||||
steps:
|
||||
- name: Check if PR is from external contributor
|
||||
id: check
|
||||
run: |
|
||||
echo "Event name: ${{ github.event_name }}"
|
||||
echo "Repository: ${{ github.repository }}"
|
||||
|
||||
if [ "${{ github.event_name }}" == "pull_request" ]; then
|
||||
# For pull_request events, check if PR is from external fork
|
||||
echo "PR head repo: ${{ github.event.pull_request.head.repo.full_name }}"
|
||||
if [ "${{ github.actor }}" == "dependabot[bot]" ]; then
|
||||
echo "condition=skip" >> $GITHUB_OUTPUT
|
||||
echo "Setting condition=skip (Dependabot PR)"
|
||||
elif [ "${{ github.event.pull_request.head.repo.full_name }}" != "${{ github.repository }}" ]; then
|
||||
echo "condition=skip" >> $GITHUB_OUTPUT
|
||||
echo "Setting condition=skip (external fork PR creation)"
|
||||
else
|
||||
echo "condition=pr-creation-maintainer" >> $GITHUB_OUTPUT
|
||||
echo "Setting condition=pr-creation-maintainer (internal PR creation)"
|
||||
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
|
||||
SHA_PARAM="${{ github.event.client_payload.slash_command.args.named.sha }}"
|
||||
PR_HEAD_SHA="${{ github.event.client_payload.pull_request.head.sha }}"
|
||||
|
||||
echo "Checking dispatch event conditions..."
|
||||
echo "SHA from command: $SHA_PARAM"
|
||||
echo "PR head SHA: $PR_HEAD_SHA"
|
||||
|
||||
if [ -n "$SHA_PARAM" ] && [[ "$PR_HEAD_SHA" == *"$SHA_PARAM"* ]]; then
|
||||
echo "condition=dispatch-event" >> $GITHUB_OUTPUT
|
||||
echo "Setting condition=dispatch-event (sha matches)"
|
||||
echo "ref=$PR_HEAD_SHA" >> $GITHUB_OUTPUT
|
||||
else
|
||||
echo "condition=skip" >> $GITHUB_OUTPUT
|
||||
echo "Setting condition=skip (sha does not match or empty)"
|
||||
fi
|
||||
elif [ "${{ github.event_name }}" == "push" ] && [ "${{ github.ref_name }}" == "main" ]; then
|
||||
echo "condition=push-to-main" >> $GITHUB_OUTPUT
|
||||
echo "Setting condition=push-to-main (push to main)"
|
||||
echo "ref=${{ github.sha }}" >> $GITHUB_OUTPUT
|
||||
else
|
||||
# Unknown event type
|
||||
echo "condition=skip" >> $GITHUB_OUTPUT
|
||||
echo "Setting condition=skip (unknown event type: ${{ github.event_name }})"
|
||||
fi
|
||||
|
||||
e2e:
|
||||
needs: check-external-pr
|
||||
if: |
|
||||
(needs.check-external-pr.outputs.condition == 'pr-creation-maintainer')
|
||||
||
|
||||
(needs.check-external-pr.outputs.condition == 'dispatch-event')
|
||||
||
|
||||
needs.check-external-pr.outputs.condition == 'push-to-main'
|
||||
uses: ./.github/workflows/e2e-tests.yml
|
||||
with:
|
||||
ref: ${{ needs.check-external-pr.outputs.ref }}
|
||||
secrets:
|
||||
OP_CONNECT_CREDENTIALS: ${{ secrets.OP_CONNECT_CREDENTIALS }}
|
||||
OP_CONNECT_TOKEN: ${{ secrets.OP_CONNECT_TOKEN }}
|
||||
OP_SERVICE_ACCOUNT_TOKEN: ${{ secrets.OP_SERVICE_ACCOUNT_TOKEN }}
|
||||
VAULT: ${{ secrets.VAULT }}
|
||||
|
||||
# Post comment on fork PRs after /ok-to-test
|
||||
comment-pr:
|
||||
needs: [check-external-pr, e2e]
|
||||
runs-on: ubuntu-latest
|
||||
if: always() && needs.check-external-pr.outputs.condition == 'dispatch-event'
|
||||
permissions:
|
||||
pull-requests: write
|
||||
steps:
|
||||
- name: Create URL to the run output
|
||||
id: vars
|
||||
run: echo "run-url=https://github.com/$GITHUB_REPOSITORY/actions/runs/$GITHUB_RUN_ID" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Create comment on PR
|
||||
uses: peter-evans/create-or-update-comment@v5
|
||||
with:
|
||||
issue-number: ${{ github.event.client_payload.pull_request.number }}
|
||||
body: |
|
||||
${{
|
||||
needs.e2e.result == 'success' && '✅ E2E tests passed.' ||
|
||||
needs.e2e.result == 'failure' && '❌ E2E tests failed.' ||
|
||||
'⚠️ E2E tests completed.'
|
||||
}}
|
||||
|
||||
[View test run output][1]
|
||||
|
||||
[1]: ${{ steps.vars.outputs.run-url }}
|
||||
146
.github/workflows/test.yml
vendored
146
.github/workflows/test.yml
vendored
@@ -1,146 +0,0 @@
|
||||
on: push
|
||||
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:
|
||||
strategy:
|
||||
matrix:
|
||||
os: [ubuntu-latest, macos-latest]
|
||||
auth: [connect, service-account]
|
||||
exclude:
|
||||
- os: macos-latest
|
||||
auth: connect
|
||||
runs-on: ${{ matrix.os }}
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- name: Launch 1Password Connect instance
|
||||
if: ${{ matrix.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: ${{ matrix.auth == 'service-account' }}
|
||||
uses: ./configure
|
||||
with:
|
||||
service-account-token: ${{ secrets.OP_SERVICE_ACCOUNT_TOKEN }}
|
||||
- name: Configure 1Password Connect
|
||||
if: ${{ matrix.auth == 'connect' }}
|
||||
uses: ./configure # 1password/load-secrets-action/configure@<version>
|
||||
with:
|
||||
connect-host: http://localhost:8080
|
||||
connect-token: ${{ secrets.OP_CONNECT_TOKEN }}
|
||||
- name: Load secrets
|
||||
id: load_secrets
|
||||
uses: ./ # 1password/load-secrets-action@<version>
|
||||
with:
|
||||
export-env: false
|
||||
env:
|
||||
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
|
||||
- name: Assert test secret values
|
||||
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 }}
|
||||
run: ./tests/assert-env-set.sh
|
||||
test-with-export-env:
|
||||
strategy:
|
||||
matrix:
|
||||
os: [ubuntu-latest, macos-latest]
|
||||
auth: [connect, service-account]
|
||||
exclude:
|
||||
- os: macos-latest
|
||||
auth: connect
|
||||
runs-on: ${{ matrix.os }}
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- name: Launch 1Password Connect instance
|
||||
if: ${{ matrix.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: ${{ matrix.auth == 'service-account' }}
|
||||
uses: ./configure
|
||||
with:
|
||||
service-account-token: ${{ secrets.OP_SERVICE_ACCOUNT_TOKEN }}
|
||||
- name: Configure 1Password Connect
|
||||
if: ${{ matrix.auth == 'connect' }}
|
||||
uses: ./configure # 1password/load-secrets-action/configure@<version>
|
||||
with:
|
||||
connect-host: http://localhost:8080
|
||||
connect-token: ${{ secrets.OP_CONNECT_TOKEN }}
|
||||
- name: Load secrets
|
||||
id: load_secrets
|
||||
uses: ./ # 1password/load-secrets-action@<version>
|
||||
env:
|
||||
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
|
||||
- name: Assert test secret values
|
||||
run: ./tests/assert-env-set.sh
|
||||
- name: Remove secrets
|
||||
uses: ./ # 1password/load-secrets-action@<version>
|
||||
with:
|
||||
unset-previous: true
|
||||
- name: Assert removed secrets
|
||||
run: ./tests/assert-env-unset.sh
|
||||
test-references-with-ids:
|
||||
strategy:
|
||||
matrix:
|
||||
os: [ubuntu-latest, macos-latest]
|
||||
auth: [connect, service-account]
|
||||
exclude:
|
||||
- os: macos-latest
|
||||
auth: connect
|
||||
runs-on: ${{ matrix.os }}
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- name: Launch 1Password Connect instance
|
||||
if: ${{ matrix.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: ${{ matrix.auth == 'service-account' }}
|
||||
uses: ./configure
|
||||
with:
|
||||
service-account-token: ${{ secrets.OP_SERVICE_ACCOUNT_TOKEN }}
|
||||
- name: Configure 1Password Connect
|
||||
if: ${{ matrix.auth == 'connect' }}
|
||||
uses: ./configure # 1password/load-secrets-action/configure@<version>
|
||||
with:
|
||||
connect-host: http://localhost:8080
|
||||
connect-token: ${{ secrets.OP_CONNECT_TOKEN }}
|
||||
- name: Load secrets
|
||||
id: load_secrets
|
||||
uses: ./ # 1password/load-secrets-action@<version>
|
||||
with:
|
||||
export-env: false
|
||||
env:
|
||||
SECRET: op://v5pz6venw4roosmkzdq2nhpv6u/hrgkzhrlvscomepxlgafb2m3ca/password
|
||||
SECRET_IN_SECTION: op://v5pz6venw4roosmkzdq2nhpv6u/hrgkzhrlvscomepxlgafb2m3ca/Section_tco6nsqycj6jcbyx63h5isxcny/doxu3mhkozcznnk5vjrkpdqayy
|
||||
MULTILINE_SECRET: op://v5pz6venw4roosmkzdq2nhpv6u/ghtz3jvcc6dqmzc53d3r3eskge/notesPlain
|
||||
- name: Assert test secret values
|
||||
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 }}
|
||||
run: ./tests/assert-env-set.sh
|
||||
2
.gitignore
vendored
2
.gitignore
vendored
@@ -1,2 +1,4 @@
|
||||
coverage/
|
||||
node_modules/
|
||||
.idea/
|
||||
1password-credentials.json
|
||||
|
||||
54
CONTRIBUTING.md
Normal file
54
CONTRIBUTING.md
Normal file
@@ -0,0 +1,54 @@
|
||||
# Contributing
|
||||
|
||||
Thank you for your interest in contributing to the 1Password load-secrets-action project 👋! Before you start, please take a moment to read through this guide to understand our contribution process.
|
||||
|
||||
## Testing
|
||||
|
||||
Unit tests can be run with `npm run test`.
|
||||
|
||||
After following the steps below for signing commits, you can test against your PR with these steps:
|
||||
|
||||
1. Create or use an existing repo to run the `load-secrets` GitHub Action.
|
||||
2. In a workflow yaml file that uses the GitHub Action, modify the `uses: 1Password/load-secrets-action` line to be
|
||||
|
||||
```yaml
|
||||
uses: 1Password/load-secrets-action@<branch-name>
|
||||
```
|
||||
|
||||
OR
|
||||
|
||||
```yaml
|
||||
uses: 1Password/load-secrets-action@<commit-hash>
|
||||
```
|
||||
|
||||
3. Trigger the action, which now includes your changes.
|
||||
|
||||
## Documentation Updates
|
||||
|
||||
If applicable, update the [README.md](./README.md) to reflect any changes introduced by the new code.
|
||||
|
||||
## Sign your commits
|
||||
|
||||
To get your PR merged, we require you to sign your commits.
|
||||
|
||||
### Sign commits with 1Password
|
||||
|
||||
You can also sign commits using 1Password, which lets you sign commits with biometrics without the signing key leaving the local 1Password process.
|
||||
|
||||
Learn how to use [1Password to sign your commits](https://developer.1password.com/docs/ssh/git-commit-signing/).
|
||||
|
||||
### Sign commits with ssh-agent
|
||||
|
||||
Follow the steps below to set up commit signing with `ssh-agent`:
|
||||
|
||||
1. [Generate an SSH key and add it to ssh-agent](https://docs.github.com/en/authentication/connecting-to-github-with-ssh/generating-a-new-ssh-key-and-adding-it-to-the-ssh-agent)
|
||||
2. [Add the SSH key to your GitHub account](https://docs.github.com/en/authentication/connecting-to-github-with-ssh/adding-a-new-ssh-key-to-your-github-account)
|
||||
3. [Configure git to use your SSH key for commits signing](https://docs.github.com/en/authentication/managing-commit-signature-verification/telling-git-about-your-signing-key#telling-git-about-your-ssh-key)
|
||||
|
||||
### Sign commits with gpg
|
||||
|
||||
Follow the steps below to set up commit signing with `gpg`:
|
||||
|
||||
1. [Generate a GPG key](https://docs.github.com/en/authentication/managing-commit-signature-verification/generating-a-new-gpg-key)
|
||||
2. [Add the GPG key to your GitHub account](https://docs.github.com/en/authentication/managing-commit-signature-verification/adding-a-gpg-key-to-your-github-account)
|
||||
3. [Configure git to use your GPG key for commits signing](https://docs.github.com/en/authentication/managing-commit-signature-verification/telling-git-about-your-signing-key#telling-git-about-your-gpg-key)
|
||||
51
README.md
51
README.md
@@ -23,38 +23,77 @@ Read more on the [1Password Developer Portal](https://developer.1password.com/do
|
||||
|
||||
## ✨ Quickstart
|
||||
|
||||
### Export secrets as a step's output (recommended)
|
||||
|
||||
```yml
|
||||
on: push
|
||||
jobs:
|
||||
hello-world:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Load secret
|
||||
uses: 1password/load-secrets-action@v2
|
||||
id: load_secrets
|
||||
uses: 1password/load-secrets-action@v4
|
||||
env:
|
||||
OP_SERVICE_ACCOUNT_TOKEN: ${{ secrets.OP_SERVICE_ACCOUNT_TOKEN }}
|
||||
SECRET: op://app-cicd/hello-world/secret
|
||||
OP_ENV_FILE: "./path/to/.env.tpl" # see tests/.env.tpl for example
|
||||
|
||||
- name: Print masked secret
|
||||
run: 'echo "Secret: ${{ steps.load_secrets.outputs.SECRET }}"'
|
||||
# Prints: Secret: ***
|
||||
```
|
||||
|
||||
### Export secrets as env variables
|
||||
|
||||
```yml
|
||||
on: push
|
||||
jobs:
|
||||
hello-world:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Load secret
|
||||
uses: 1password/load-secrets-action@v4
|
||||
with:
|
||||
# Export loaded secrets as environment variables
|
||||
export-env: true
|
||||
env:
|
||||
OP_SERVICE_ACCOUNT_TOKEN: ${{ secrets.OP_SERVICE_ACCOUNT_TOKEN }}
|
||||
SECRET: op://app-cicd/hello-world/secret
|
||||
OP_ENV_FILE: "./path/to/.env.tpl" # see tests/.env.tpl for example
|
||||
|
||||
- name: Print masked secret
|
||||
run: 'echo "Secret: $SECRET"'
|
||||
# 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@v4
|
||||
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.
|
||||
- Join the [Developer Slack workspace](https://join.slack.com/t/1password-devs/shared_invite/zt-1halo11ps-6o9pEv96xZ3LtX_VE0fJQA).
|
||||
- Join the [Developer Slack workspace](https://developer.1password.com/joinslack).
|
||||
- Subscribe to the [Developer Newsletter](https://1password.com/dev-subscribe/).
|
||||
|
||||
## 🔐 Security
|
||||
|
||||
1Password requests you practice responsible disclosure if you discover a vulnerability.
|
||||
|
||||
Please file requests via [**BugCrowd**](https://bugcrowd.com/agilebits).
|
||||
|
||||
For information about security practices, please visit the [1Password Bug Bounty Program](https://bugcrowd.com/agilebits).
|
||||
Please file requests by sending an email to bugbounty@agilebits.com.
|
||||
|
||||
@@ -10,7 +10,10 @@ inputs:
|
||||
default: "false"
|
||||
export-env:
|
||||
description: Export the secrets as environment variables
|
||||
default: "true"
|
||||
default: "false"
|
||||
version:
|
||||
description: Specify which 1Password CLI version to install. Defaults to "latest".
|
||||
default: "latest"
|
||||
runs:
|
||||
using: "node20"
|
||||
using: "node24"
|
||||
main: "dist/index.js"
|
||||
|
||||
@@ -10,6 +10,11 @@ const jestConfig = {
|
||||
rootDir: "../src/",
|
||||
testEnvironment: "node",
|
||||
testRegex: "(/__tests__/.*|(\\.|/)test)\\.ts",
|
||||
moduleNameMapper: {
|
||||
"^@actions/core$": "<rootDir>/__mocks__/actions-core.ts",
|
||||
"^@actions/tool-cache$": "<rootDir>/__mocks__/actions-tool-cache.ts",
|
||||
"^@actions/exec$": "<rootDir>/__mocks__/actions-exec.ts",
|
||||
},
|
||||
transform: {
|
||||
".ts": [
|
||||
"ts-jest",
|
||||
@@ -25,4 +30,4 @@ const jestConfig = {
|
||||
verbose: true,
|
||||
};
|
||||
|
||||
export default jestConfig;
|
||||
module.exports = jestConfig;
|
||||
|
||||
@@ -9,12 +9,5 @@ inputs:
|
||||
service-account-token:
|
||||
description: Your 1Password service account token
|
||||
runs:
|
||||
using: composite
|
||||
steps:
|
||||
- shell: bash
|
||||
env:
|
||||
INPUT_CONNECT_HOST: ${{ inputs.connect-host }}
|
||||
INPUT_CONNECT_TOKEN: ${{ inputs.connect-token }}
|
||||
INPUT_SERVICE_ACCOUNT_TOKEN: ${{ inputs.service-account-token }}
|
||||
run: |
|
||||
${{ github.action_path }}/entrypoint.sh
|
||||
using: "node24"
|
||||
main: "dist/index.js"
|
||||
|
||||
31022
configure/dist/index.js
vendored
Normal file
31022
configure/dist/index.js
vendored
Normal file
File diff suppressed because one or more lines are too long
3
configure/dist/package.json
vendored
Normal file
3
configure/dist/package.json
vendored
Normal file
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"type": "commonjs"
|
||||
}
|
||||
@@ -1,21 +0,0 @@
|
||||
#!/bin/bash
|
||||
# shellcheck disable=SC2086
|
||||
set -e
|
||||
|
||||
# Capture Connect configuration in $GITHUB_ENV, giving (optional) inputs
|
||||
# precendence over OP_CONNECT_* environment variables.
|
||||
|
||||
OP_CONNECT_HOST="${INPUT_CONNECT_HOST:-$OP_CONNECT_HOST}"
|
||||
if [ -n "$OP_CONNECT_HOST" ]; then
|
||||
echo "OP_CONNECT_HOST=$OP_CONNECT_HOST" >> $GITHUB_ENV
|
||||
fi
|
||||
|
||||
OP_CONNECT_TOKEN="${INPUT_CONNECT_TOKEN:-$OP_CONNECT_TOKEN}"
|
||||
if [ -n "$OP_CONNECT_TOKEN" ]; then
|
||||
echo "OP_CONNECT_TOKEN=$OP_CONNECT_TOKEN" >> $GITHUB_ENV
|
||||
fi
|
||||
|
||||
OP_SERVICE_ACCOUNT_TOKEN="${INPUT_SERVICE_ACCOUNT_TOKEN:-$OP_SERVICE_ACCOUNT_TOKEN}"
|
||||
if [ -n "$OP_SERVICE_ACCOUNT_TOKEN" ]; then
|
||||
echo "OP_SERVICE_ACCOUNT_TOKEN=$OP_SERVICE_ACCOUNT_TOKEN" >> $GITHUB_ENV
|
||||
fi
|
||||
27
configure/index.js
Normal file
27
configure/index.js
Normal file
@@ -0,0 +1,27 @@
|
||||
import * as core from "@actions/core";
|
||||
|
||||
const configure = () => {
|
||||
const OP_CONNECT_HOST =
|
||||
core.getInput("connect-host", { required: false }) ||
|
||||
process.env.OP_CONNECT_HOST;
|
||||
const OP_CONNECT_TOKEN =
|
||||
core.getInput("connect-token", { required: false }) ||
|
||||
process.env.OP_CONNECT_TOKEN;
|
||||
const OP_SERVICE_ACCOUNT_TOKEN =
|
||||
core.getInput("service-account-token", { required: false }) ||
|
||||
process.env.OP_SERVICE_ACCOUNT_TOKEN;
|
||||
|
||||
if (OP_CONNECT_HOST) {
|
||||
core.exportVariable("OP_CONNECT_HOST", OP_CONNECT_HOST);
|
||||
}
|
||||
|
||||
if (OP_CONNECT_TOKEN) {
|
||||
core.exportVariable("OP_CONNECT_TOKEN", OP_CONNECT_TOKEN);
|
||||
}
|
||||
|
||||
if (OP_SERVICE_ACCOUNT_TOKEN) {
|
||||
core.exportVariable("OP_SERVICE_ACCOUNT_TOKEN", OP_SERVICE_ACCOUNT_TOKEN);
|
||||
}
|
||||
};
|
||||
|
||||
configure();
|
||||
36403
dist/index.js
vendored
36403
dist/index.js
vendored
File diff suppressed because one or more lines are too long
2
dist/package.json
vendored
2
dist/package.json
vendored
@@ -1,3 +1,3 @@
|
||||
{
|
||||
"type": "module"
|
||||
"type": "commonjs"
|
||||
}
|
||||
|
||||
32
docs/fork-pr-testing.md
Normal file
32
docs/fork-pr-testing.md
Normal file
@@ -0,0 +1,32 @@
|
||||
# Fork PR Testing Guide
|
||||
|
||||
This document explains how testing works for external pull requests from forks.
|
||||
|
||||
## Overview
|
||||
|
||||
The testing system consists of two main workflows:
|
||||
|
||||
1. **E2E Tests** (`test-e2e.yml`) - Runs automatically for internal PRs, need manual trigger on external PRs.
|
||||
2. **Ok To Test** (`ok-to-test.yml`) - Dispatches `repository_dispatch` event when maintainer puts the `/ok-to-test sha=<commit hash>` comment in the forked PR thread.
|
||||
|
||||
## How It Works
|
||||
|
||||
### 1. PR is created by maintainer:
|
||||
|
||||
For the PR created by maintainer `E2E Test` workflow starts automatically. The PR check will reflect the status of the job.
|
||||
|
||||
### 2. PR is created by external contributor:
|
||||
|
||||
For the PR created by external contributor `E2E Test` workflow **won't** start automatically.
|
||||
Maintainer should make a sanity check of the changes and run it manually by:
|
||||
|
||||
1. Putting a comment `/ok-to-test sha=<latest commit hash>` in the PR thread.
|
||||
2. `E2E Test` workflow starts.
|
||||
3. After `E2E Test` workflow finishes, a comment with a link to the workflow, along with its status will be posted in the PR.
|
||||
4. Maintainer can merge PR or request the changes based on the `E2E Test` results.
|
||||
|
||||
## Notes
|
||||
|
||||
- Only users with **write** permissions can trigger the `/ok-to-test` command.
|
||||
- External PRs are automatically detected and prevented from running e2e tests automatically.
|
||||
- Running e2e test on the external PR is optional. Maintainer can merge PR without running it. Maintainer decides whether it's needed to run an E2E test.
|
||||
46
docs/local-testing.md
Normal file
46
docs/local-testing.md
Normal file
@@ -0,0 +1,46 @@
|
||||
# Local Testing Guide
|
||||
|
||||
This document explains how to run e2e tests locally using `act`.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
1. **Docker** installed and running
|
||||
2. **act** installed ([install guide](https://github.com/nektos/act#installation))
|
||||
```bash
|
||||
brew install act # macOS
|
||||
```
|
||||
3. **1Password credentials** (see [Required Secrets](#required-secrets))
|
||||
4. Build action
|
||||
|
||||
## Required env variables
|
||||
|
||||
| Secret | Description |
|
||||
| -------------------------- | --------------------- |
|
||||
| `OP_SERVICE_ACCOUNT_TOKEN` | Service Account token |
|
||||
| `VAULT` | Vault name or UUID |
|
||||
|
||||
## Building Before Testing
|
||||
|
||||
If you've modified TypeScript code, rebuild before running E2E tests:
|
||||
|
||||
```bash
|
||||
npm run build
|
||||
```
|
||||
|
||||
## Testing
|
||||
|
||||
### Run E2E tests using Service Account
|
||||
|
||||
```bash
|
||||
act push -W .github/workflows/e2e-tests.yml \
|
||||
-s OP_SERVICE_ACCOUNT_TOKEN="$OP_SERVICE_ACCOUNT_TOKEN" \
|
||||
-s VAULT="$VAULT" \
|
||||
-j test-service-account \
|
||||
--matrix os:ubuntu-latest
|
||||
```
|
||||
|
||||
## Run unit tests
|
||||
|
||||
```bash
|
||||
npm test
|
||||
```
|
||||
@@ -1,46 +0,0 @@
|
||||
#!/bin/bash
|
||||
set -e
|
||||
|
||||
# Install op-cli
|
||||
install_op_cli() {
|
||||
# Create a temporary directory where the CLI is installed
|
||||
OP_INSTALL_DIR="$(mktemp -d)"
|
||||
if [[ ! -d "$OP_INSTALL_DIR" ]]; then
|
||||
echo "Install dir $OP_INSTALL_DIR not found"
|
||||
exit 1
|
||||
fi
|
||||
echo "::debug::OP_INSTALL_DIR: ${OP_INSTALL_DIR}"
|
||||
|
||||
# Get the latest stable version of the CLI
|
||||
CLI_VERSION="v$(curl https://app-updates.agilebits.com/check/1/0/CLI2/en/2.0.0/N -s | grep -Eo '[0-9]+\.[0-9]+\.[0-9]+')"
|
||||
|
||||
if [[ "$OSTYPE" == "linux-gnu"* ]]; then
|
||||
# Get runner's architecture
|
||||
ARCH=$(uname -m)
|
||||
if [[ "$(getconf LONG_BIT)" = 32 ]]; then
|
||||
ARCH="386"
|
||||
elif [[ "$ARCH" == "x86_64" ]]; then
|
||||
ARCH="amd64"
|
||||
elif [[ "$ARCH" == "aarch64" ]]; then
|
||||
ARCH="arm64"
|
||||
fi
|
||||
|
||||
if [[ "$ARCH" != "386" ]] && [[ "$ARCH" != "amd64" ]] && [[ "$ARCH" != "arm" ]] && [[ "$ARCH" != "arm64" ]]; then
|
||||
echo "Unsupported architecture for the 1Password CLI: $ARCH."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
curl -sSfLo op.zip "https://cache.agilebits.com/dist/1P/op2/pkg/${CLI_VERSION}/op_linux_${ARCH}_${CLI_VERSION}.zip"
|
||||
unzip -od "$OP_INSTALL_DIR" op.zip && rm op.zip
|
||||
elif [[ "$OSTYPE" == "darwin"* ]]; then
|
||||
curl -sSfLo op.pkg "https://cache.agilebits.com/dist/1P/op2/pkg/${CLI_VERSION}/op_apple_universal_${CLI_VERSION}.pkg"
|
||||
pkgutil --expand op.pkg temp-pkg
|
||||
tar -xvf temp-pkg/op.pkg/Payload -C "$OP_INSTALL_DIR"
|
||||
rm -rf temp-pkg && rm op.pkg
|
||||
else
|
||||
echo "Operating system not supported yet for this GitHub Action: $OSTYPE."
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
install_op_cli
|
||||
5907
package-lock.json
generated
5907
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
21
package.json
21
package.json
@@ -1,14 +1,15 @@
|
||||
{
|
||||
"name": "load-secrets-action",
|
||||
"version": "2.0.0",
|
||||
"version": "4.0.0",
|
||||
"description": "Load Secrets from 1Password",
|
||||
"type": "module",
|
||||
"main": "dist/index.js",
|
||||
"directories": {
|
||||
"test": "tests"
|
||||
},
|
||||
"scripts": {
|
||||
"build": "ncc build ./src/index.ts",
|
||||
"build:configure": "ncc build ./configure/index.js -o ./configure/dist",
|
||||
"build:all": "npm run build && npm run build:configure",
|
||||
"format": "prettier --ignore-path ./config/.prettierignore",
|
||||
"format:check": "npm run format -- --check ./",
|
||||
"format:write": "npm run format -- --write ./",
|
||||
@@ -40,11 +41,17 @@
|
||||
"homepage": "https://github.com/1Password/load-secrets-action#readme",
|
||||
"dependencies": {
|
||||
"@1password/op-js": "^0.1.11",
|
||||
"@actions/core": "^1.10.1",
|
||||
"@actions/exec": "^1.1.1"
|
||||
"@actions/core": "^3.0.0",
|
||||
"@actions/exec": "^3.0.0",
|
||||
"@actions/tool-cache": "^4.0.0",
|
||||
"dotenv": "^17.2.2"
|
||||
},
|
||||
"overrides": {
|
||||
"minimatch": "^9.0.7"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@1password/front-end-style": "^6.0.1",
|
||||
"@1password/eslint-config": "^4.3.1",
|
||||
"@1password/prettier-config": "^1.2.0",
|
||||
"@types/jest": "^29.5.12",
|
||||
"@types/node": "^20.11.30",
|
||||
"@vercel/ncc": "^0.38.1",
|
||||
@@ -55,7 +62,7 @@
|
||||
"typescript": "^5.4.2"
|
||||
},
|
||||
"eslintConfig": {
|
||||
"extends": "./node_modules/@1password/front-end-style/eslintrc.yml",
|
||||
"extends": "@1password/eslint-config",
|
||||
"ignorePatterns": [
|
||||
"coverage/"
|
||||
],
|
||||
@@ -63,5 +70,5 @@
|
||||
"project": "./tsconfig.json"
|
||||
}
|
||||
},
|
||||
"prettier": "./node_modules/@1password/front-end-style/prettierrc.json"
|
||||
"prettier": "@1password/prettier-config"
|
||||
}
|
||||
|
||||
14
src/__mocks__/actions-core.ts
Normal file
14
src/__mocks__/actions-core.ts
Normal file
@@ -0,0 +1,14 @@
|
||||
module.exports = {
|
||||
getInput: jest.fn(() => ""),
|
||||
getBooleanInput: jest.fn(() => false),
|
||||
setOutput: jest.fn(),
|
||||
setSecret: jest.fn(),
|
||||
exportVariable: jest.fn(),
|
||||
setFailed: jest.fn(),
|
||||
info: jest.fn(),
|
||||
warning: jest.fn(),
|
||||
error: jest.fn(),
|
||||
debug: jest.fn(),
|
||||
addPath: jest.fn(),
|
||||
isDebug: jest.fn(() => false),
|
||||
};
|
||||
5
src/__mocks__/actions-exec.ts
Normal file
5
src/__mocks__/actions-exec.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
module.exports = {
|
||||
getExecOutput: jest.fn(() => ({
|
||||
stdout: "MOCK_SECRET",
|
||||
})),
|
||||
};
|
||||
10
src/__mocks__/actions-tool-cache.ts
Normal file
10
src/__mocks__/actions-tool-cache.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
module.exports = {
|
||||
downloadTool: jest.fn(),
|
||||
extractTar: jest.fn(),
|
||||
extractZip: jest.fn(),
|
||||
cacheDir: jest.fn<Promise<string>, [string]>(async (dir) => {
|
||||
await Promise.resolve();
|
||||
return dir;
|
||||
}),
|
||||
find: jest.fn<string, [string, string?, string?]>(() => ""),
|
||||
};
|
||||
@@ -2,5 +2,6 @@ export const envConnectHost = "OP_CONNECT_HOST";
|
||||
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 authErr = `Authentication error with environment variables: you must set either 1) ${envServiceAccountToken}, or 2) both ${envConnectHost} and ${envConnectToken}.`;
|
||||
|
||||
29
src/index.ts
29
src/index.ts
@@ -1,9 +1,9 @@
|
||||
import path from "path";
|
||||
import url from "url";
|
||||
import dotenv from "dotenv";
|
||||
import * as core from "@actions/core";
|
||||
import * as exec from "@actions/exec";
|
||||
import { validateCli } from "@1password/op-js";
|
||||
import { installCliOnGithubActionRunner } from "./op-cli-installer";
|
||||
import { loadSecrets, unsetPrevious, validateAuth } from "./utils";
|
||||
import { envFilePath } from "./constants";
|
||||
|
||||
const loadSecretsAction = async () => {
|
||||
try {
|
||||
@@ -19,6 +19,13 @@ const loadSecretsAction = async () => {
|
||||
// Validate that a proper authentication configuration is set for the CLI
|
||||
validateAuth();
|
||||
|
||||
// Set environment variables from OP_ENV_FILE
|
||||
const file = process.env[envFilePath];
|
||||
if (file) {
|
||||
core.info(`Loading environment variables from file: ${file}`);
|
||||
dotenv.config({ path: file });
|
||||
}
|
||||
|
||||
// Download and install the CLI
|
||||
await installCLI();
|
||||
|
||||
@@ -46,21 +53,7 @@ const installCLI = async (): Promise<void> => {
|
||||
// If there's no CLI installed, then validateCli will throw an error, which we will use
|
||||
// as an indicator that we need to execute the installation script.
|
||||
await validateCli().catch(async () => {
|
||||
const currentFile = url.fileURLToPath(import.meta.url);
|
||||
const currentDir = path.dirname(currentFile);
|
||||
const parentDir = path.resolve(currentDir, "..");
|
||||
|
||||
// Execute bash script
|
||||
const cmdOut = await exec.getExecOutput(
|
||||
`sh -c "` + parentDir + `/install_cli.sh"`,
|
||||
);
|
||||
|
||||
// Add path to 1Password CLI to $PATH
|
||||
const outArr = cmdOut.stdout.split("\n");
|
||||
if (outArr[0] && process.env.PATH) {
|
||||
const cliPath = outArr[0]?.replace(/^(::debug::OP_INSTALL_DIR: )/, "");
|
||||
core.addPath(cliPath);
|
||||
}
|
||||
await installCliOnGithubActionRunner();
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
@@ -0,0 +1,58 @@
|
||||
import os from "os";
|
||||
|
||||
import * as core from "@actions/core";
|
||||
import * as tc from "@actions/tool-cache";
|
||||
|
||||
export type SupportedPlatform = Extract<
|
||||
NodeJS.Platform,
|
||||
"linux" | "darwin" | "win32"
|
||||
>;
|
||||
|
||||
// maps OS architecture names to 1Password CLI installer architecture names
|
||||
export const archMap: Record<string, string> = {
|
||||
ia32: "386",
|
||||
x64: "amd64",
|
||||
arm: "arm",
|
||||
arm64: "arm64",
|
||||
};
|
||||
|
||||
// Builds the download URL for the 1Password CLI based on the platform and version.
|
||||
export const cliUrlBuilder: Record<
|
||||
SupportedPlatform,
|
||||
(version: string, arch?: string) => string
|
||||
> = {
|
||||
linux: (version, arch) =>
|
||||
`https://cache.agilebits.com/dist/1P/op2/pkg/${version}/op_linux_${arch}_${version}.zip`,
|
||||
darwin: (version) =>
|
||||
`https://cache.agilebits.com/dist/1P/op2/pkg/${version}/op_apple_universal_${version}.pkg`,
|
||||
win32: (version, arch) =>
|
||||
`https://cache.agilebits.com/dist/1P/op2/pkg/${version}/op_windows_${arch}_${version}.zip`,
|
||||
};
|
||||
|
||||
export class CliInstaller {
|
||||
public readonly version: string;
|
||||
public readonly arch: string;
|
||||
|
||||
public constructor(version: string) {
|
||||
this.version = version;
|
||||
this.arch = this.getArch();
|
||||
}
|
||||
|
||||
public async install(url: string): Promise<void> {
|
||||
console.info(`Downloading 1Password CLI from: ${url}`);
|
||||
const downloadPath = await tc.downloadTool(url);
|
||||
console.info("Installing 1Password CLI");
|
||||
const extractedPath = await tc.extractZip(downloadPath);
|
||||
core.addPath(extractedPath);
|
||||
core.info("1Password CLI installed");
|
||||
}
|
||||
|
||||
private getArch(): string {
|
||||
const arch = archMap[os.arch()];
|
||||
if (!arch) {
|
||||
throw new Error("Unsupported architecture");
|
||||
}
|
||||
|
||||
return arch;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export { type Installer, newCliInstaller } from "./installer";
|
||||
@@ -0,0 +1,43 @@
|
||||
import os from "os";
|
||||
|
||||
import { newCliInstaller } from "./installer";
|
||||
import { LinuxInstaller } from "./linux";
|
||||
import { MacOsInstaller } from "./macos";
|
||||
import { WindowsInstaller } from "./windows";
|
||||
|
||||
afterEach(() => {
|
||||
jest.restoreAllMocks();
|
||||
});
|
||||
|
||||
describe("newCliInstaller", () => {
|
||||
const version = "1.0.0";
|
||||
|
||||
afterEach(() => {
|
||||
jest.resetAllMocks();
|
||||
});
|
||||
|
||||
it("should return LinuxInstaller for linux platform", () => {
|
||||
jest.spyOn(os, "platform").mockReturnValue("linux");
|
||||
const installer = newCliInstaller(version);
|
||||
expect(installer).toBeInstanceOf(LinuxInstaller);
|
||||
});
|
||||
|
||||
it("should return MacOsInstaller for darwin platform", () => {
|
||||
jest.spyOn(os, "platform").mockReturnValue("darwin");
|
||||
const installer = newCliInstaller(version);
|
||||
expect(installer).toBeInstanceOf(MacOsInstaller);
|
||||
});
|
||||
|
||||
it("should return WindowsInstaller for win32 platform", () => {
|
||||
jest.spyOn(os, "platform").mockReturnValue("win32");
|
||||
const installer = newCliInstaller(version);
|
||||
expect(installer).toBeInstanceOf(WindowsInstaller);
|
||||
});
|
||||
|
||||
it("should throw error for unsupported platform", () => {
|
||||
jest.spyOn(os, "platform").mockReturnValue("sunos");
|
||||
expect(() => newCliInstaller(version)).toThrow(
|
||||
"Unsupported platform: sunos",
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,23 @@
|
||||
import os from "os";
|
||||
|
||||
import { LinuxInstaller } from "./linux";
|
||||
import { MacOsInstaller } from "./macos";
|
||||
import { WindowsInstaller } from "./windows";
|
||||
|
||||
export interface Installer {
|
||||
installCli(): Promise<void>;
|
||||
}
|
||||
|
||||
export const newCliInstaller = (version: string): Installer => {
|
||||
const platform = os.platform();
|
||||
switch (platform) {
|
||||
case "linux":
|
||||
return new LinuxInstaller(version);
|
||||
case "darwin":
|
||||
return new MacOsInstaller(version);
|
||||
case "win32":
|
||||
return new WindowsInstaller(version);
|
||||
default:
|
||||
throw new Error(`Unsupported platform: ${platform}`);
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,38 @@
|
||||
import os from "os";
|
||||
|
||||
import {
|
||||
archMap,
|
||||
CliInstaller,
|
||||
cliUrlBuilder,
|
||||
type SupportedPlatform,
|
||||
} from "./cli-installer";
|
||||
import { LinuxInstaller } from "./linux";
|
||||
|
||||
afterEach(() => {
|
||||
jest.restoreAllMocks();
|
||||
});
|
||||
|
||||
describe("LinuxInstaller", () => {
|
||||
const version = "1.2.3";
|
||||
const arch: NodeJS.Architecture = "arm64";
|
||||
|
||||
it("should construct with given version and architecture", () => {
|
||||
jest.spyOn(os, "arch").mockReturnValue(arch);
|
||||
const installer = new LinuxInstaller(version);
|
||||
expect(installer.version).toEqual(version);
|
||||
expect(installer.arch).toEqual(archMap[arch]);
|
||||
});
|
||||
|
||||
it("should call install with correct URL", async () => {
|
||||
const installer = new LinuxInstaller(version);
|
||||
const installMock = jest
|
||||
.spyOn(CliInstaller.prototype, "install")
|
||||
.mockResolvedValue();
|
||||
|
||||
await installer.installCli();
|
||||
|
||||
const builder = cliUrlBuilder["linux" as SupportedPlatform];
|
||||
const url = builder(version, installer.arch);
|
||||
expect(installMock).toHaveBeenCalledWith(url);
|
||||
});
|
||||
});
|
||||
19
src/op-cli-installer/github-action/cli-installer/linux.ts
Normal file
19
src/op-cli-installer/github-action/cli-installer/linux.ts
Normal file
@@ -0,0 +1,19 @@
|
||||
import {
|
||||
CliInstaller,
|
||||
cliUrlBuilder,
|
||||
type SupportedPlatform,
|
||||
} from "./cli-installer";
|
||||
import type { Installer } from "./installer";
|
||||
|
||||
export class LinuxInstaller extends CliInstaller implements Installer {
|
||||
private readonly platform: SupportedPlatform = "linux"; // Node.js platform identifier for Linux
|
||||
|
||||
public constructor(version: string) {
|
||||
super(version);
|
||||
}
|
||||
|
||||
public async installCli(): Promise<void> {
|
||||
const urlBuilder = cliUrlBuilder[this.platform];
|
||||
await super.install(urlBuilder(this.version, this.arch));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
import os from "os";
|
||||
|
||||
import {
|
||||
archMap,
|
||||
cliUrlBuilder,
|
||||
type SupportedPlatform,
|
||||
} from "./cli-installer";
|
||||
import { MacOsInstaller } from "./macos";
|
||||
|
||||
afterEach(() => {
|
||||
jest.restoreAllMocks();
|
||||
});
|
||||
|
||||
describe("MacOsInstaller", () => {
|
||||
const version = "1.2.3";
|
||||
const arch: NodeJS.Architecture = "x64";
|
||||
|
||||
it("should construct with given version and architecture", () => {
|
||||
jest.spyOn(os, "arch").mockReturnValue(arch);
|
||||
const installer = new MacOsInstaller(version);
|
||||
expect(installer.version).toEqual(version);
|
||||
expect(installer.arch).toEqual(archMap[arch]);
|
||||
});
|
||||
|
||||
it("should call install with correct URL", async () => {
|
||||
const installer = new MacOsInstaller(version);
|
||||
const installMock = jest.spyOn(installer, "install").mockResolvedValue();
|
||||
|
||||
await installer.installCli();
|
||||
|
||||
const builder = cliUrlBuilder["darwin" as SupportedPlatform];
|
||||
const url = builder(version, installer.arch);
|
||||
expect(installMock).toHaveBeenCalledWith(url);
|
||||
});
|
||||
});
|
||||
49
src/op-cli-installer/github-action/cli-installer/macos.ts
Normal file
49
src/op-cli-installer/github-action/cli-installer/macos.ts
Normal file
@@ -0,0 +1,49 @@
|
||||
import { execFile } from "child_process";
|
||||
import * as fs from "fs";
|
||||
import * as path from "path";
|
||||
import { promisify } from "util";
|
||||
|
||||
import * as core from "@actions/core";
|
||||
import * as tc from "@actions/tool-cache";
|
||||
|
||||
import {
|
||||
CliInstaller,
|
||||
cliUrlBuilder,
|
||||
type SupportedPlatform,
|
||||
} from "./cli-installer";
|
||||
import { type Installer } from "./installer";
|
||||
|
||||
const execFileAsync = promisify(execFile);
|
||||
|
||||
export class MacOsInstaller extends CliInstaller implements Installer {
|
||||
private readonly platform: SupportedPlatform = "darwin"; // Node.js platform identifier for macOS
|
||||
|
||||
public constructor(version: string) {
|
||||
super(version);
|
||||
}
|
||||
|
||||
public async installCli(): Promise<void> {
|
||||
const urlBuilder = cliUrlBuilder[this.platform];
|
||||
await this.install(urlBuilder(this.version));
|
||||
}
|
||||
|
||||
// @actions/tool-cache package does not support .pkg files, so we need to handle the installation manually
|
||||
public override async install(downloadUrl: string): Promise<void> {
|
||||
console.info(`Downloading 1Password CLI from: ${downloadUrl}`);
|
||||
const pkgPath = await tc.downloadTool(downloadUrl);
|
||||
const pkgWithExtension = `${pkgPath}.pkg`;
|
||||
fs.renameSync(pkgPath, pkgWithExtension);
|
||||
|
||||
const expandDir = "temp-pkg";
|
||||
await execFileAsync("pkgutil", ["--expand", pkgWithExtension, expandDir]);
|
||||
const payloadPath = path.join(expandDir, "op.pkg", "Payload");
|
||||
console.info("Installing 1Password CLI");
|
||||
const cliPath = await tc.extractTar(payloadPath);
|
||||
core.addPath(cliPath);
|
||||
|
||||
fs.rmSync(expandDir, { recursive: true, force: true });
|
||||
fs.rmSync(pkgPath, { force: true });
|
||||
|
||||
core.info("1Password CLI installed");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
import os from "os";
|
||||
|
||||
import {
|
||||
archMap,
|
||||
CliInstaller,
|
||||
cliUrlBuilder,
|
||||
type SupportedPlatform,
|
||||
} from "./cli-installer";
|
||||
import { WindowsInstaller } from "./windows";
|
||||
|
||||
afterEach(() => {
|
||||
jest.restoreAllMocks();
|
||||
});
|
||||
|
||||
describe("WindowsInstaller", () => {
|
||||
const version = "1.2.3";
|
||||
const arch: NodeJS.Architecture = "x64";
|
||||
|
||||
it("should construct with given version and architecture", () => {
|
||||
jest.spyOn(os, "arch").mockReturnValue(arch);
|
||||
const installer = new WindowsInstaller(version);
|
||||
expect(installer.version).toEqual(version);
|
||||
expect(installer.arch).toEqual(archMap[arch]);
|
||||
});
|
||||
|
||||
it("should call install with correct URL", async () => {
|
||||
const installer = new WindowsInstaller(version);
|
||||
const installMock = jest
|
||||
.spyOn(CliInstaller.prototype, "install")
|
||||
.mockResolvedValue();
|
||||
|
||||
await installer.installCli();
|
||||
|
||||
const builder = cliUrlBuilder["win32" as SupportedPlatform];
|
||||
const url = builder(version, installer.arch);
|
||||
expect(installMock).toHaveBeenCalledWith(url);
|
||||
});
|
||||
});
|
||||
19
src/op-cli-installer/github-action/cli-installer/windows.ts
Normal file
19
src/op-cli-installer/github-action/cli-installer/windows.ts
Normal file
@@ -0,0 +1,19 @@
|
||||
import {
|
||||
CliInstaller,
|
||||
cliUrlBuilder,
|
||||
type SupportedPlatform,
|
||||
} from "./cli-installer";
|
||||
import type { Installer } from "./installer";
|
||||
|
||||
export class WindowsInstaller extends CliInstaller implements Installer {
|
||||
private readonly platform: SupportedPlatform = "win32"; // Node.js platform identifier for Windows
|
||||
|
||||
public constructor(version: string) {
|
||||
super(version);
|
||||
}
|
||||
|
||||
public async installCli(): Promise<void> {
|
||||
const urlBuilder = cliUrlBuilder[this.platform];
|
||||
await super.install(urlBuilder(this.version, this.arch));
|
||||
}
|
||||
}
|
||||
18
src/op-cli-installer/github-action/index.ts
Normal file
18
src/op-cli-installer/github-action/index.ts
Normal file
@@ -0,0 +1,18 @@
|
||||
import * as core from "@actions/core";
|
||||
|
||||
import { ReleaseChannel, VersionResolver } from "../version";
|
||||
|
||||
import { newCliInstaller } from "./cli-installer";
|
||||
|
||||
// Installs the 1Password CLI on a GitHub Action runner.
|
||||
export const installCliOnGithubActionRunner = async (
|
||||
version?: string,
|
||||
): Promise<void> => {
|
||||
// Get the version from parameter, if not passed - from the job input. Defaults to latest if no version is provided
|
||||
const providedVersion =
|
||||
version || core.getInput("version") || ReleaseChannel.latest;
|
||||
const versionResolver = new VersionResolver(providedVersion);
|
||||
await versionResolver.resolve();
|
||||
const installer = newCliInstaller(versionResolver.get());
|
||||
await installer.installCli();
|
||||
};
|
||||
81
src/op-cli-installer/index.test.ts
Normal file
81
src/op-cli-installer/index.test.ts
Normal file
@@ -0,0 +1,81 @@
|
||||
import * as core from "@actions/core";
|
||||
|
||||
import { newCliInstaller } from "./github-action/cli-installer";
|
||||
import {
|
||||
installCliOnGithubActionRunner,
|
||||
ReleaseChannel,
|
||||
VersionResolver,
|
||||
} from "./index";
|
||||
|
||||
jest.mock("./github-action/cli-installer", () => ({
|
||||
newCliInstaller: jest.fn().mockImplementation((_resolved: string) => ({
|
||||
installCli: jest.fn(),
|
||||
})),
|
||||
}));
|
||||
|
||||
beforeEach(() => {
|
||||
jest.restoreAllMocks();
|
||||
});
|
||||
|
||||
describe("installCliOnGithubActionRunner", () => {
|
||||
it("should defaults to `latest` when nothing is passed", async () => {
|
||||
jest.spyOn(core, "getInput").mockReturnValue("");
|
||||
jest.spyOn(VersionResolver.prototype, "resolve").mockResolvedValue();
|
||||
jest
|
||||
.spyOn(VersionResolver.prototype, "get")
|
||||
.mockReturnValue(ReleaseChannel.latest);
|
||||
|
||||
await installCliOnGithubActionRunner();
|
||||
|
||||
expect(newCliInstaller).toHaveBeenCalledWith(ReleaseChannel.latest);
|
||||
});
|
||||
|
||||
it("should defaults to `latest` when undefined is passed", async () => {
|
||||
jest.spyOn(core, "getInput").mockReturnValue("");
|
||||
jest.spyOn(VersionResolver.prototype, "resolve").mockResolvedValue();
|
||||
jest
|
||||
.spyOn(VersionResolver.prototype, "get")
|
||||
.mockReturnValue(ReleaseChannel.latest);
|
||||
|
||||
await installCliOnGithubActionRunner(undefined);
|
||||
|
||||
expect(newCliInstaller).toHaveBeenCalledWith(ReleaseChannel.latest);
|
||||
});
|
||||
|
||||
it("should set provided explicit version", async () => {
|
||||
const providedVersion = "1.2.3";
|
||||
jest.spyOn(core, "getInput").mockReturnValue("");
|
||||
jest.spyOn(VersionResolver.prototype, "resolve").mockResolvedValue();
|
||||
jest
|
||||
.spyOn(VersionResolver.prototype, "get")
|
||||
.mockReturnValue(providedVersion);
|
||||
|
||||
await installCliOnGithubActionRunner(providedVersion);
|
||||
|
||||
expect(newCliInstaller).toHaveBeenCalledWith(providedVersion);
|
||||
});
|
||||
|
||||
it("should set version provided as job input", async () => {
|
||||
const providedVersion = "3.0.0";
|
||||
jest.spyOn(core, "getInput").mockReturnValue(providedVersion);
|
||||
jest.spyOn(VersionResolver.prototype, "resolve").mockResolvedValue();
|
||||
jest
|
||||
.spyOn(VersionResolver.prototype, "get")
|
||||
.mockReturnValue(providedVersion);
|
||||
|
||||
await installCliOnGithubActionRunner();
|
||||
|
||||
expect(newCliInstaller).toHaveBeenCalledWith(providedVersion);
|
||||
});
|
||||
|
||||
it("should throw error for invalid version", async () => {
|
||||
const providedVersion = "invalid";
|
||||
jest.spyOn(core, "getInput").mockReturnValue(providedVersion);
|
||||
jest.spyOn(VersionResolver.prototype, "resolve").mockResolvedValue();
|
||||
jest
|
||||
.spyOn(VersionResolver.prototype, "get")
|
||||
.mockReturnValue(providedVersion);
|
||||
|
||||
await expect(installCliOnGithubActionRunner()).rejects.toThrow();
|
||||
});
|
||||
});
|
||||
2
src/op-cli-installer/index.ts
Normal file
2
src/op-cli-installer/index.ts
Normal file
@@ -0,0 +1,2 @@
|
||||
export { installCliOnGithubActionRunner } from "./github-action";
|
||||
export { ReleaseChannel, VersionResolver } from "./version";
|
||||
13
src/op-cli-installer/version/constants.ts
Normal file
13
src/op-cli-installer/version/constants.ts
Normal file
@@ -0,0 +1,13 @@
|
||||
export enum ReleaseChannel {
|
||||
latest = "latest",
|
||||
latestBeta = "latest-beta",
|
||||
}
|
||||
|
||||
export interface VersionResponse {
|
||||
// eslint disabled next line as CLI2 is expected in getting CLI versions response
|
||||
/* eslint-disable-next-line @typescript-eslint/naming-convention */
|
||||
CLI2: {
|
||||
release: { version: string };
|
||||
beta: { version: string };
|
||||
};
|
||||
}
|
||||
91
src/op-cli-installer/version/helper.test.ts
Normal file
91
src/op-cli-installer/version/helper.test.ts
Normal file
@@ -0,0 +1,91 @@
|
||||
import { ReleaseChannel } from "./constants";
|
||||
import { getLatestVersion } from "./helper";
|
||||
|
||||
describe("getLatestVersion", () => {
|
||||
beforeEach(() => {
|
||||
jest.restoreAllMocks();
|
||||
});
|
||||
|
||||
it("should return latest stable version", async () => {
|
||||
const mockResponse = {
|
||||
// eslint-disable-next-line @typescript-eslint/naming-convention
|
||||
CLI2: {
|
||||
release: { version: "2.31.0" },
|
||||
beta: { version: "2.32.0-beta.01" },
|
||||
},
|
||||
};
|
||||
|
||||
jest.spyOn(global, "fetch").mockResolvedValueOnce({
|
||||
// eslint-disable-next-line @typescript-eslint/require-await
|
||||
json: async () => mockResponse,
|
||||
} as Response);
|
||||
|
||||
const version = await getLatestVersion(ReleaseChannel.latest);
|
||||
expect(version).toBe("2.31.0");
|
||||
});
|
||||
|
||||
it("should return latest beta version", async () => {
|
||||
const mockResponse = {
|
||||
// eslint-disable-next-line @typescript-eslint/naming-convention
|
||||
CLI2: {
|
||||
release: { version: "2.31.0" },
|
||||
beta: { version: "2.32.0-beta.01" },
|
||||
},
|
||||
};
|
||||
|
||||
jest.spyOn(global, "fetch").mockResolvedValueOnce({
|
||||
// eslint-disable-next-line @typescript-eslint/require-await
|
||||
json: async () => mockResponse,
|
||||
} as Response);
|
||||
|
||||
const version = await getLatestVersion(ReleaseChannel.latestBeta);
|
||||
expect(version).toBe("2.32.0-beta.01");
|
||||
});
|
||||
|
||||
it("should throw if no CLI2 field", async () => {
|
||||
jest.spyOn(global, "fetch").mockResolvedValueOnce({
|
||||
// eslint-disable-next-line @typescript-eslint/require-await
|
||||
json: async () => ({}),
|
||||
} as Response);
|
||||
|
||||
await expect(getLatestVersion(ReleaseChannel.latest)).rejects.toThrow(
|
||||
`No ${ReleaseChannel.latest} versions found`,
|
||||
);
|
||||
});
|
||||
|
||||
it("should throw if no stable version found", async () => {
|
||||
const mockResponse = {
|
||||
// eslint-disable-next-line @typescript-eslint/naming-convention
|
||||
CLI2: {
|
||||
beta: { version: "2.32.0-beta.01" },
|
||||
},
|
||||
};
|
||||
|
||||
jest.spyOn(global, "fetch").mockResolvedValueOnce({
|
||||
// eslint-disable-next-line @typescript-eslint/require-await
|
||||
json: async () => mockResponse,
|
||||
} as Response);
|
||||
|
||||
await expect(getLatestVersion(ReleaseChannel.latest)).rejects.toThrow(
|
||||
`No ${ReleaseChannel.latest} versions found`,
|
||||
);
|
||||
});
|
||||
|
||||
it("should throw if no beta version found", async () => {
|
||||
const mockResponse = {
|
||||
// eslint-disable-next-line @typescript-eslint/naming-convention
|
||||
CLI2: {
|
||||
release: { version: "2.32.0" },
|
||||
},
|
||||
};
|
||||
|
||||
jest.spyOn(global, "fetch").mockResolvedValueOnce({
|
||||
// eslint-disable-next-line @typescript-eslint/require-await
|
||||
json: async () => mockResponse,
|
||||
} as Response);
|
||||
|
||||
await expect(getLatestVersion(ReleaseChannel.latestBeta)).rejects.toThrow(
|
||||
`No ${ReleaseChannel.latestBeta} versions found`,
|
||||
);
|
||||
});
|
||||
});
|
||||
23
src/op-cli-installer/version/helper.ts
Normal file
23
src/op-cli-installer/version/helper.ts
Normal file
@@ -0,0 +1,23 @@
|
||||
import * as core from "@actions/core";
|
||||
|
||||
import { ReleaseChannel, type VersionResponse } from "./constants";
|
||||
|
||||
// Returns the latest version of the 1Password CLI based on the specified channel.
|
||||
export const getLatestVersion = async (
|
||||
channel: ReleaseChannel,
|
||||
): Promise<string> => {
|
||||
core.info(`Getting ${channel} version number`);
|
||||
const res = await fetch("https://app-updates.agilebits.com/latest");
|
||||
const json = (await res.json()) as VersionResponse;
|
||||
const latestStable = json?.CLI2?.release?.version;
|
||||
const latestBeta = json?.CLI2?.beta?.version;
|
||||
const version =
|
||||
channel === ReleaseChannel.latestBeta ? latestBeta : latestStable;
|
||||
|
||||
if (!version) {
|
||||
core.error(`No ${channel} versions found`);
|
||||
throw new Error(`No ${channel} versions found`);
|
||||
}
|
||||
|
||||
return version;
|
||||
};
|
||||
2
src/op-cli-installer/version/index.ts
Normal file
2
src/op-cli-installer/version/index.ts
Normal file
@@ -0,0 +1,2 @@
|
||||
export { VersionResolver } from "./version-resolver";
|
||||
export { ReleaseChannel } from "./constants";
|
||||
45
src/op-cli-installer/version/validate.test.ts
Normal file
45
src/op-cli-installer/version/validate.test.ts
Normal file
@@ -0,0 +1,45 @@
|
||||
import { describe, expect, it } from "@jest/globals";
|
||||
|
||||
import { validateVersion } from "./validate";
|
||||
|
||||
describe("validateVersion", () => {
|
||||
it('should not throw for "latest"', () => {
|
||||
expect(() => validateVersion("latest")).not.toThrow();
|
||||
});
|
||||
|
||||
it('should not throw for "latest-beta"', () => {
|
||||
expect(() => validateVersion("latest-beta")).not.toThrow();
|
||||
});
|
||||
|
||||
it('should not throw for valid semver version "2.18.0"', () => {
|
||||
expect(() => validateVersion("2.18.0")).not.toThrow();
|
||||
});
|
||||
|
||||
it('should throw for partial version "2"', () => {
|
||||
expect(() => validateVersion("2")).toThrow();
|
||||
});
|
||||
|
||||
it('should throw for partial version "2.1"', () => {
|
||||
expect(() => validateVersion("2.1")).toThrow();
|
||||
});
|
||||
|
||||
it('should not throw for valid beta "2.19.0-beta.01"', () => {
|
||||
expect(() => validateVersion("2.19.0-beta.01")).not.toThrow();
|
||||
});
|
||||
|
||||
it('should not throw for valid beta "2.19.3-beta.12"', () => {
|
||||
expect(() => validateVersion("2.19.3-beta.12")).not.toThrow();
|
||||
});
|
||||
|
||||
it('should not throw for coerced version "v2.19.0"', () => {
|
||||
expect(() => validateVersion("v2.19.0")).not.toThrow();
|
||||
});
|
||||
|
||||
it('should throw for invalid version "latest-abc"', () => {
|
||||
expect(() => validateVersion("latest-abc")).toThrow();
|
||||
});
|
||||
|
||||
it("should throw for empty string", () => {
|
||||
expect(() => validateVersion("")).toThrow();
|
||||
});
|
||||
});
|
||||
23
src/op-cli-installer/version/validate.ts
Normal file
23
src/op-cli-installer/version/validate.ts
Normal file
@@ -0,0 +1,23 @@
|
||||
import semver from "semver";
|
||||
|
||||
import { ReleaseChannel } from "./constants";
|
||||
|
||||
// Validates if the provided version type is a valid enum value or a valid semver version.
|
||||
export const validateVersion = (input: string): void => {
|
||||
if (Object.values(ReleaseChannel).includes(input as ReleaseChannel)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 1Password beta releases (aka 2.19.0-beta.01) are not semver compliant.
|
||||
// According to semver, it should be "2.19.0-beta.1".
|
||||
// That's why we need to normalize them before validating.
|
||||
// Accepts valid semver versions like "2.18.0" or beta-releases like "2.19.0-beta.01"
|
||||
// or versions with 'v' prefix like "v2.19.0"
|
||||
const normalized = input.replace(/-beta\.0*(\d+)/, "-beta.$1");
|
||||
const normInput = new semver.SemVer(normalized);
|
||||
if (semver.valid(normInput)) {
|
||||
return;
|
||||
}
|
||||
|
||||
throw new Error(`Invalid version input: ${input}`);
|
||||
};
|
||||
58
src/op-cli-installer/version/version-resolver.test.ts
Normal file
58
src/op-cli-installer/version/version-resolver.test.ts
Normal file
@@ -0,0 +1,58 @@
|
||||
import { expect } from "@jest/globals";
|
||||
|
||||
import { ReleaseChannel } from "./constants";
|
||||
import { VersionResolver } from "./version-resolver";
|
||||
|
||||
describe("VersionResolver", () => {
|
||||
test("should throw error when invalid version provided", () => {
|
||||
expect(() => new VersionResolver("vv")).toThrow();
|
||||
});
|
||||
|
||||
test("should throw error when version is empty", () => {
|
||||
expect(() => new VersionResolver("")).toThrow();
|
||||
});
|
||||
|
||||
test("should throw error for major version only", () => {
|
||||
expect(() => new VersionResolver("1")).toThrow();
|
||||
});
|
||||
|
||||
test("should throw error for major and minor version only", () => {
|
||||
expect(() => new VersionResolver("1.0")).toThrow();
|
||||
});
|
||||
|
||||
test("should resolve latest stable version", async () => {
|
||||
const versionResolver = new VersionResolver(ReleaseChannel.latest);
|
||||
await versionResolver.resolve();
|
||||
expect(versionResolver.get()).toBeDefined();
|
||||
});
|
||||
|
||||
test("should resolve latest beta version", async () => {
|
||||
const versionResolver = new VersionResolver(ReleaseChannel.latestBeta);
|
||||
await versionResolver.resolve();
|
||||
expect(versionResolver.get()).toBeDefined();
|
||||
});
|
||||
|
||||
test("should resolve version without 'v' prefix", async () => {
|
||||
const versionResolver = new VersionResolver("1.0.0");
|
||||
await versionResolver.resolve();
|
||||
expect(versionResolver.get()).toBe("v1.0.0");
|
||||
});
|
||||
|
||||
test("should resolve version with 'v' prefix", async () => {
|
||||
const versionResolver = new VersionResolver("v1.0.0");
|
||||
await versionResolver.resolve();
|
||||
expect(versionResolver.get()).toBe("v1.0.0");
|
||||
});
|
||||
|
||||
test("should resolve beta version without 'v' prefix", async () => {
|
||||
const versionResolver = new VersionResolver("2.19.0-beta.01");
|
||||
await versionResolver.resolve();
|
||||
expect(versionResolver.get()).toBe("v2.19.0-beta.01");
|
||||
});
|
||||
|
||||
test("should resolve beta version with 'v' prefix", async () => {
|
||||
const versionResolver = new VersionResolver("v2.19.0-beta.01");
|
||||
await versionResolver.resolve();
|
||||
expect(versionResolver.get()).toBe("v2.19.0-beta.01");
|
||||
});
|
||||
});
|
||||
45
src/op-cli-installer/version/version-resolver.ts
Normal file
45
src/op-cli-installer/version/version-resolver.ts
Normal file
@@ -0,0 +1,45 @@
|
||||
import * as core from "@actions/core";
|
||||
|
||||
import { ReleaseChannel } from "./constants";
|
||||
import { getLatestVersion } from "./helper";
|
||||
import { validateVersion } from "./validate";
|
||||
|
||||
export class VersionResolver {
|
||||
private version: string;
|
||||
|
||||
public constructor(version: string) {
|
||||
this.validate(version);
|
||||
this.version = version;
|
||||
}
|
||||
|
||||
public get(): string {
|
||||
return this.version;
|
||||
}
|
||||
|
||||
public async resolve(): Promise<void> {
|
||||
core.info(`Resolving version: ${this.version}`);
|
||||
if (!this.version) {
|
||||
core.error("Version is not provided");
|
||||
throw new Error("Version is not provided");
|
||||
}
|
||||
|
||||
if (this.isReleaseChannel(this.version)) {
|
||||
this.version = await getLatestVersion(this.version);
|
||||
}
|
||||
|
||||
// add `v` prefix if not already present
|
||||
this.version = this.version.startsWith("v")
|
||||
? this.version
|
||||
: `v${this.version}`;
|
||||
}
|
||||
|
||||
private validate(version: string) {
|
||||
core.info(`Validating version number: '${version}'`);
|
||||
validateVersion(version);
|
||||
core.info(`Version number '${version}' is valid`);
|
||||
}
|
||||
|
||||
private isReleaseChannel(value: string): value is ReleaseChannel {
|
||||
return Object.values(ReleaseChannel).includes(value as ReleaseChannel);
|
||||
}
|
||||
}
|
||||
@@ -15,12 +15,6 @@ import {
|
||||
envServiceAccountToken,
|
||||
} from "./constants";
|
||||
|
||||
jest.mock("@actions/core");
|
||||
jest.mock("@actions/exec", () => ({
|
||||
getExecOutput: jest.fn(() => ({
|
||||
stdout: "MOCK_SECRET",
|
||||
})),
|
||||
}));
|
||||
jest.mock("@1password/op-js");
|
||||
|
||||
beforeEach(() => {
|
||||
@@ -39,24 +33,24 @@ describe("validateAuth", () => {
|
||||
});
|
||||
|
||||
it("should throw an error when no config is provided", () => {
|
||||
expect(validateAuth).toThrowError(authErr);
|
||||
expect(validateAuth).toThrow(authErr);
|
||||
});
|
||||
|
||||
it("should throw an error when partial Connect config is provided", () => {
|
||||
process.env[envConnectHost] = testConnectHost;
|
||||
expect(validateAuth).toThrowError(authErr);
|
||||
expect(validateAuth).toThrow(authErr);
|
||||
});
|
||||
|
||||
it("should be authenticated as a Connect client", () => {
|
||||
process.env[envConnectHost] = testConnectHost;
|
||||
process.env[envConnectToken] = testConnectToken;
|
||||
expect(validateAuth).not.toThrowError(authErr);
|
||||
expect(validateAuth).not.toThrow(authErr);
|
||||
expect(core.info).toHaveBeenCalledWith("Authenticated with Connect.");
|
||||
});
|
||||
|
||||
it("should be authenticated as a service account", () => {
|
||||
process.env[envServiceAccountToken] = testServiceAccountToken;
|
||||
expect(validateAuth).not.toThrowError(authErr);
|
||||
expect(validateAuth).not.toThrow(authErr);
|
||||
expect(core.info).toHaveBeenCalledWith(
|
||||
"Authenticated with Service account.",
|
||||
);
|
||||
@@ -66,7 +60,7 @@ describe("validateAuth", () => {
|
||||
process.env[envServiceAccountToken] = testServiceAccountToken;
|
||||
process.env[envConnectHost] = testConnectHost;
|
||||
process.env[envConnectToken] = testConnectToken;
|
||||
expect(validateAuth).not.toThrowError(authErr);
|
||||
expect(validateAuth).not.toThrow(authErr);
|
||||
expect(core.warning).toHaveBeenCalled();
|
||||
expect(core.info).toHaveBeenCalledWith("Authenticated with Connect.");
|
||||
});
|
||||
@@ -106,6 +100,41 @@ 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", () => {
|
||||
|
||||
@@ -41,7 +41,7 @@ export const extractSecret = (
|
||||
}
|
||||
|
||||
const secretValue = read.parse(ref);
|
||||
if (!secretValue) {
|
||||
if (secretValue === null || secretValue === undefined) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -50,7 +50,11 @@ export const extractSecret = (
|
||||
} else {
|
||||
core.setOutput(envName, secretValue);
|
||||
}
|
||||
// Skip setSecret for empty strings to avoid the warning:
|
||||
// "Can't add secret mask for empty string in ##[add-mask] command."
|
||||
if (secretValue) {
|
||||
core.setSecret(secretValue);
|
||||
}
|
||||
};
|
||||
|
||||
export const loadSecrets = async (shouldExportEnv: boolean): Promise<void> => {
|
||||
|
||||
@@ -9,11 +9,8 @@ assert_env_equals() {
|
||||
fi
|
||||
}
|
||||
|
||||
assert_env_equals "SECRET" "RGVhciBzZWN1cml0eSByZXNlYXJjaGVyLCB0aGlzIGlzIGp1c3QgYSBkdW1teSBzZWNyZXQuIFBsZWFzZSBkb24ndCByZXBvcnQgaXQu"
|
||||
|
||||
assert_env_equals "SECRET_IN_SECTION" "RGVhciBzZWN1cml0eSByZXNlYXJjaGVyLCB0aGlzIGlzIGp1c3QgYSBkdW1teSBzZWNyZXQuIFBsZWFzZSBkb24ndCByZXBvcnQgaXQu"
|
||||
|
||||
assert_env_equals "MULTILINE_SECRET" "$(cat << EOF
|
||||
readonly SECRET="RGVhciBzZWN1cml0eSByZXNlYXJjaGVyLCB0aGlzIGlzIGp1c3QgYSBkdW1teSBzZWNyZXQuIFBsZWFzZSBkb24ndCByZXBvcnQgaXQu"
|
||||
MULTILINE_SECRET="$(cat << EOF
|
||||
-----BEGIN PRIVATE KEY-----
|
||||
RGVhciBzZWN1cml0eSByZXNlYXJjaGVyLApXaGls
|
||||
ZSB3ZSBkZWVwbHkgYXBwcmVjaWF0ZSB5b3VyIHZp
|
||||
@@ -28,3 +25,20 @@ IApTbyBwbGVhc2UgZG9uJ3QgcmVwb3J0IGl0IQo=
|
||||
-----END PRIVATE KEY-----
|
||||
EOF
|
||||
)"
|
||||
readonly MULTILINE_SECRET
|
||||
readonly WEBSITE="www.test.com"
|
||||
|
||||
assert_env_equals "SECRET" "${SECRET}"
|
||||
assert_env_equals "FILE_SECRET" "${SECRET}"
|
||||
|
||||
assert_env_equals "SECRET_IN_SECTION" "${SECRET}"
|
||||
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
|
||||
|
||||
@@ -10,5 +10,18 @@ assert_env_unset() {
|
||||
}
|
||||
|
||||
assert_env_unset "SECRET"
|
||||
assert_env_unset "FILE_SECRET"
|
||||
|
||||
assert_env_unset "SECRET_IN_SECTION"
|
||||
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"
|
||||
|
||||
26
tests/assert-ssh-keys-set.sh
Executable file
26
tests/assert-ssh-keys-set.sh
Executable file
@@ -0,0 +1,26 @@
|
||||
#!/bin/bash
|
||||
set -e
|
||||
|
||||
assert_ssh_key_set() {
|
||||
local var="$1"
|
||||
local val
|
||||
val="$(printenv "$var" || true)"
|
||||
if [ -z "$val" ]; then
|
||||
echo "Expected $var to be set"
|
||||
exit 1
|
||||
fi
|
||||
[ "$val" = "***" ] && return 0
|
||||
local line
|
||||
line="$(echo "$val" | head -1)"
|
||||
if echo "$var" | grep -q "OPENSSH"; then
|
||||
echo "$line" | grep -q "OPENSSH" || { echo "Expected $var to start with -----BEGIN OPENSSH PRIVATE KEY-----"; exit 1; }
|
||||
else
|
||||
echo "$line" | grep -q "BEGIN.*PRIVATE KEY" || { echo "Expected $var to be a private key"; exit 1; }
|
||||
fi
|
||||
echo "$var OK"
|
||||
}
|
||||
|
||||
assert_ssh_key_set "TEST_SSH_KEY"
|
||||
assert_ssh_key_set "TEST_SSH_KEY_OPENSSH"
|
||||
assert_ssh_key_set "FILE_TEST_SSH_KEY"
|
||||
assert_ssh_key_set "FILE_TEST_SSH_KEY_OPENSSH"
|
||||
Reference in New Issue
Block a user