Compare commits
3 Commits
v4
...
feature/ad
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
cb23d65d8d | ||
|
|
adde26c41b | ||
|
|
6bfaeeb67d |
246
.github/workflows/e2e-tests.yml
vendored
246
.github/workflows/e2e-tests.yml
vendored
@@ -1,246 +0,0 @@
|
|||||||
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
36
.github/workflows/lint-and-test.yml
vendored
@@ -1,36 +0,0 @@
|
|||||||
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
|
|
||||||
10
.github/workflows/lint.yml
vendored
Normal file
10
.github/workflows/lint.yml
vendored
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
on: pull_request
|
||||||
|
name: Lint
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
lint:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v2
|
||||||
|
- name: ShellCheck
|
||||||
|
uses: ludeeus/action-shellcheck@1.1.0
|
||||||
25
.github/workflows/ok-to-test.yml
vendored
25
.github/workflows/ok-to-test.yml
vendored
@@ -1,25 +0,0 @@
|
|||||||
# 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
13
.github/workflows/pr-check-signed-commits.yml
vendored
@@ -1,13 +0,0 @@
|
|||||||
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
120
.github/workflows/test-e2e.yml
vendored
@@ -1,120 +0,0 @@
|
|||||||
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 }}
|
|
||||||
95
.github/workflows/test.yml
vendored
Normal file
95
.github/workflows/test.yml
vendored
Normal file
@@ -0,0 +1,95 @@
|
|||||||
|
on: push
|
||||||
|
name: Run acceptance tests
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
test:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v2
|
||||||
|
- name: Launch 1Password Connect instance
|
||||||
|
env:
|
||||||
|
OP_CONNECT_CREDENTIALS: ${{ secrets.OP_CONNECT_CREDENTIALS }}
|
||||||
|
run: |
|
||||||
|
echo "$OP_CONNECT_CREDENTIALS" > 1password-credentials.json
|
||||||
|
docker-compose -f tests/fixtures/docker-compose.yml up -d && sleep 10
|
||||||
|
- name: Configure 1Password Connect
|
||||||
|
uses: ./configure # 1password/load-secrets-action/configure@<version>
|
||||||
|
with:
|
||||||
|
use-connect: yes
|
||||||
|
connect-host: http://localhost:8080
|
||||||
|
connect-token: ${{ secrets.OP_CONNECT_TOKEN }}
|
||||||
|
- name: Load secrets
|
||||||
|
uses: ./ # 1password/load-secrets-action@<version>
|
||||||
|
env:
|
||||||
|
SECRET: op://v5pz6venw4roosmkzdq2nhpv6u/hrgkzhrlvscomepxlgafb2m3ca/password
|
||||||
|
SECRET_IN_SECTION: op://v5pz6venw4roosmkzdq2nhpv6u/hrgkzhrlvscomepxlgafb2m3ca/test-section/password
|
||||||
|
UNMASKED_VALUE: op://v5pz6venw4roosmkzdq2nhpv6u/hrgkzhrlvscomepxlgafb2m3ca/test-section/username
|
||||||
|
- name: Load multiline secret
|
||||||
|
uses: ./ # 1password/load-secrets-action@<version>
|
||||||
|
env:
|
||||||
|
MULTILINE_SECRET: op://v5pz6venw4roosmkzdq2nhpv6u/ghtz3jvcc6dqmzc53d3r3eskge/notesPlain
|
||||||
|
- name: Print environment variables with masked secrets
|
||||||
|
run: printenv
|
||||||
|
- 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: Print environment variables with secrets removed
|
||||||
|
run: printenv
|
||||||
|
- name: Assert removed secrets
|
||||||
|
run: ./tests/assert-env-unset.sh
|
||||||
|
- name: Load secrets by vault and item titles
|
||||||
|
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: Print environment variables with masked secrets
|
||||||
|
run: printenv
|
||||||
|
- name: Assert test secret values again
|
||||||
|
run: ./tests/assert-env-set.sh
|
||||||
|
test-user:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v2
|
||||||
|
- name: Configure 1Password with a user
|
||||||
|
uses: ./configure # 1password/load-secrets-action/configure@<version>
|
||||||
|
with:
|
||||||
|
user-domain: ${{ secrets.USER_DOMAIN }}
|
||||||
|
user-email: ${{ secrets.USER_EMAIL }}
|
||||||
|
user-key: ${{ secrets.USER_KEY }}
|
||||||
|
user-pwd: ${{ secrets.USER_PWD }}
|
||||||
|
- name: Load secrets
|
||||||
|
uses: ./ # 1password/load-secrets-action@<version>
|
||||||
|
env:
|
||||||
|
SECRET: op://p4a7xwirm3lcwtu6okulbhym5i/mybhbbwhewncjb67cy4v7hjpgi/password
|
||||||
|
SECRET_IN_SECTION: op://p4a7xwirm3lcwtu6okulbhym5i/mybhbbwhewncjb67cy4v7hjpgi/test-section/password
|
||||||
|
UNMASKED_VALUE: op://p4a7xwirm3lcwtu6okulbhym5i/mybhbbwhewncjb67cy4v7hjpgi/test-section/username
|
||||||
|
- name: Load multiline secret
|
||||||
|
uses: ./ # 1password/load-secrets-action@<version>
|
||||||
|
env:
|
||||||
|
MULTILINE_SECRET: op://p4a7xwirm3lcwtu6okulbhym5i/bcshnrykfwf5i5zm7nkqy63dca/notesPlain
|
||||||
|
- name: Print environment variables with masked secrets
|
||||||
|
run: printenv
|
||||||
|
- 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: Print environment variables with secrets removed
|
||||||
|
run: printenv
|
||||||
|
- name: Assert removed secrets
|
||||||
|
run: ./tests/assert-env-unset.sh
|
||||||
|
- name: Load secrets by vault and item titles
|
||||||
|
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: Print environment variables with masked secrets
|
||||||
|
run: printenv
|
||||||
|
- name: Assert test secret values again
|
||||||
|
run: ./tests/assert-env-set.sh
|
||||||
4
.gitignore
vendored
4
.gitignore
vendored
@@ -1,4 +0,0 @@
|
|||||||
coverage/
|
|
||||||
node_modules/
|
|
||||||
.idea/
|
|
||||||
1password-credentials.json
|
|
||||||
@@ -1,54 +0,0 @@
|
|||||||
# 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)
|
|
||||||
175
README.md
175
README.md
@@ -1,29 +1,10 @@
|
|||||||
<!-- Image sourced from https://blog.1password.com/1password-service-accounts/ -->
|
# Load Secrets from 1Password - GitHub Action
|
||||||
<img alt="" role="img" src="https://blog.1password.com/posts/2023/1password-service-accounts/header.png"/>
|
|
||||||
|
|
||||||
<div align="center">
|
The action to load secrets from [1Password Connect](https://1password.com/secrets/) into GitHub Actions.
|
||||||
<h1>Load Secrets from 1Password - GitHub Action</h1>
|
|
||||||
<p>Provide the secrets your GitHub runner needs from 1Password.</p>
|
|
||||||
<a href="https://developer.1password.com/docs/ci-cd/github-actions">
|
|
||||||
<img alt="Get started" src="https://user-images.githubusercontent.com/45081667/226940040-16d3684b-60f4-4d95-adb2-5757a8f1bc15.png" height="37"/>
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
---
|
Specify right from your workflow YAML which secrets from 1Password should be loaded into your job, and the action will make them available as environment variables for the next steps.
|
||||||
|
|
||||||
`load-secrets-action` loads secrets from 1Password into GitHub Actions using [Service Accounts](https://developer.1password.com/docs/service-accounts) or [1Password Connect](https://developer.1password.com/docs/connect).
|
## Usage
|
||||||
|
|
||||||
Specify in your workflow YAML file which secrets from 1Password should be loaded into your job, and the action will make them available as environment variables for the next steps.
|
|
||||||
|
|
||||||
Read more on the [1Password Developer Portal](https://developer.1password.com/docs/ci-cd/github-actions).
|
|
||||||
|
|
||||||
## 🪄 See it in action!
|
|
||||||
|
|
||||||
[](https://www.youtube.com/watch?v=kVBl5iQYgSA "Using 1Password Service Accounts with GitHub Actions")
|
|
||||||
|
|
||||||
## ✨ Quickstart
|
|
||||||
|
|
||||||
### Export secrets as a step's output (recommended)
|
|
||||||
|
|
||||||
```yml
|
```yml
|
||||||
on: push
|
on: push
|
||||||
@@ -31,69 +12,141 @@ jobs:
|
|||||||
hello-world:
|
hello-world:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v2
|
||||||
|
|
||||||
- name: Load secret
|
- name: Load secret
|
||||||
id: load_secrets
|
uses: 1password/load-secrets-action@v1
|
||||||
uses: 1password/load-secrets-action@v4
|
|
||||||
env:
|
env:
|
||||||
OP_SERVICE_ACCOUNT_TOKEN: ${{ secrets.OP_SERVICE_ACCOUNT_TOKEN }}
|
OP_CONNECT_HOST: <Your Connect instance URL>
|
||||||
|
OP_CONNECT_TOKEN: ${{ secrets.OP_CONNECT_TOKEN }}
|
||||||
SECRET: op://app-cicd/hello-world/secret
|
SECRET: op://app-cicd/hello-world/secret
|
||||||
OP_ENV_FILE: "./path/to/.env.tpl" # see tests/.env.tpl for example
|
|
||||||
|
|
||||||
- name: Print masked secret
|
- name: Print masked secret
|
||||||
run: 'echo "Secret: ${{ steps.load_secrets.outputs.SECRET }}"'
|
run: echo "Secret: $SECRET"
|
||||||
# Prints: Secret: ***
|
# Prints: Secret: ***
|
||||||
```
|
```
|
||||||
|
<details>
|
||||||
### Export secrets as env variables
|
<summary><b>Longer usage example</b></summary>
|
||||||
|
|
||||||
```yml
|
```yml
|
||||||
on: push
|
on: push
|
||||||
|
name: Deploy app
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
hello-world:
|
test:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v2
|
||||||
|
|
||||||
- name: Load secret
|
- name: Configure 1Password Connect
|
||||||
uses: 1password/load-secrets-action@v4
|
uses: 1password/load-secrets-action/configure@v1
|
||||||
with:
|
with:
|
||||||
# Export loaded secrets as environment variables
|
# Persist the 1Password Connect URL for next steps. You can also persist
|
||||||
export-env: true
|
# the Connect token using input `connect-token`, but keep in mind that
|
||||||
|
# every single step in the job would then be able to access the token.
|
||||||
|
connect-host: https://1password.acme.com
|
||||||
|
|
||||||
|
- name: Load Docker credentials
|
||||||
|
uses: 1password/load-secrets-action@v1
|
||||||
env:
|
env:
|
||||||
OP_SERVICE_ACCOUNT_TOKEN: ${{ secrets.OP_SERVICE_ACCOUNT_TOKEN }}
|
OP_CONNECT_TOKEN: ${{ secrets.OP_CONNECT_TOKEN }}
|
||||||
SECRET: op://app-cicd/hello-world/secret
|
DOCKERHUB_USERNAME: op://app-cicd/docker/username
|
||||||
OP_ENV_FILE: "./path/to/.env.tpl" # see tests/.env.tpl for example
|
DOCKERHUB_TOKEN: op://app-cicd/docker/token
|
||||||
|
|
||||||
- name: Print masked secret
|
- name: Login to Docker Hub
|
||||||
run: 'echo "Secret: $SECRET"'
|
uses: docker/login-action@v1
|
||||||
# Prints: Secret: ***
|
with:
|
||||||
|
username: ${{ env.DOCKERHUB_USERNAME }}
|
||||||
|
password: ${{ env.DOCKERHUB_TOKEN }}
|
||||||
|
|
||||||
|
- name: Print environment variables with masked secrets
|
||||||
|
run: printenv
|
||||||
|
|
||||||
|
- name: Build and push Docker image
|
||||||
|
uses: docker/build-push-action@v2
|
||||||
|
with:
|
||||||
|
push: true
|
||||||
|
tags: acme/app:latest
|
||||||
|
|
||||||
|
- name: Load AWS credentials
|
||||||
|
uses: 1password/load-secrets-action@v1
|
||||||
|
with:
|
||||||
|
# Remove local copies of the Docker credentials, which are not needed anymore
|
||||||
|
unset-previous: true
|
||||||
|
env:
|
||||||
|
OP_CONNECT_TOKEN: ${{ secrets.OP_CONNECT_TOKEN }}
|
||||||
|
AWS_ACCESS_KEY_ID: op://app-cicd/aws/access-key-id
|
||||||
|
AWS_SECRET_ACCESS_KEY: op://app-cicd/aws/secret-access-key
|
||||||
|
|
||||||
|
- name: Deploy app
|
||||||
|
# This script expects AWS_ACCESS_KEY_ID and AWS_SECRET_ACCESS_KEY to be set, which was
|
||||||
|
# done automatically by the step above
|
||||||
|
run: ./deploy.sh
|
||||||
```
|
```
|
||||||
|
</details>
|
||||||
|
|
||||||
### 🔑 SSH Key Format
|
## Action Inputs
|
||||||
|
|
||||||
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.
|
| Name | Default | Description |
|
||||||
|
|---|---|---|
|
||||||
|
| `unset-previous` | `false` | Whether to unset environment variables populated by 1Password in earlier job steps |
|
||||||
|
|
||||||
|
## Secrets Reference Syntax
|
||||||
|
|
||||||
|
To specify which secret should be loaded into which environment variable, the action will look for `op://` reference URIs in environment variables, and replace those with the actual secret values.
|
||||||
|
|
||||||
|
These reference URIs have the following syntax:
|
||||||
|
|
||||||
|
> `op://<vault>/<item>[/<section>]/<field>`
|
||||||
|
|
||||||
|
So for example, the reference URI `op://app-cicd/aws/secret-access-key` would be interpreted as:
|
||||||
|
* **Vault:** `app-cicd`
|
||||||
|
* **Item:** `aws`
|
||||||
|
* **Section:** default section
|
||||||
|
* **Field:** `secret-access-key`
|
||||||
|
|
||||||
|
## Masking
|
||||||
|
|
||||||
|
Similar to regular GitHub repository secrets, secret fields from 1Password will automatically be masked from the GitHub Actions logs too.
|
||||||
|
A 1Password field is considered 'secret' when it's marked as concealed (which shows as `•••••••` in the 1Password GUI) or when it's a secure note.
|
||||||
|
So if one of these values accidentally gets printed, it'll get replaced with `***`.
|
||||||
|
|
||||||
|
This means that a username or port field for example will not get masked.
|
||||||
|
|
||||||
|
## 1Password Connect Configuration
|
||||||
|
|
||||||
|
To use the action, you need to have a [1Password Connect](https://support.1password.com/secrets-automation/#step-1-set-up-a-secrets-automation-workflow) instance deployed somewhere.
|
||||||
|
To configure the action with your Connect URL and a Connect token, you can set the `OP_CONNECT_HOST` and `OP_CONNECT_TOKEN` variables.
|
||||||
|
|
||||||
|
If you're using the `load-secrets` action more than once in a single job, you can use the `configure` action to avoid duplicate configuration:
|
||||||
|
|
||||||
```yml
|
```yml
|
||||||
- name: Load SSH key
|
on: push
|
||||||
uses: 1password/load-secrets-action@v4
|
jobs:
|
||||||
env:
|
hello-world:
|
||||||
OP_SERVICE_ACCOUNT_TOKEN: ${{ secrets.OP_SERVICE_ACCOUNT_TOKEN }}
|
runs-on: ubuntu-latest
|
||||||
# Load SSH private key in OpenSSH format
|
steps:
|
||||||
SSH_PRIVATE_KEY: op://vault/item/private key?ssh-format=openssh
|
- uses: actions/checkout@v2
|
||||||
|
|
||||||
|
- name: Configure 1Password Connect
|
||||||
|
uses: 1password/load-secrets-action/configure@v1
|
||||||
|
with:
|
||||||
|
connect-host: <Your Connect instance URL>
|
||||||
|
connect-token: ${{ secrets.OP_CONNECT_TOKEN }}
|
||||||
|
|
||||||
|
- name: Load secret
|
||||||
|
uses: 1password/load-secrets-action@v1
|
||||||
|
env:
|
||||||
|
SECRET: op://app-cicd/hello-world/secret
|
||||||
```
|
```
|
||||||
|
|
||||||
For more details on secret reference syntax, see the [1Password CLI documentation](https://developer.1password.com/docs/cli/secret-reference-syntax/#ssh-format-parameter).
|
### `configure` Action Inputs
|
||||||
|
|
||||||
## 💙 Community & Support
|
| Name | Default | Environment variable | Description |
|
||||||
|
|---|---|---|---|
|
||||||
|
| `connect-host` | | `OP_CONNECT_HOST` | Your 1Password Connect instance URL |
|
||||||
|
| `connect-token` | | `OP_CONNECT_TOKEN` | Token to authenticate to your 1Password Connect instance |
|
||||||
|
|
||||||
- File an [issue](https://github.com/1Password/load-secrets-action/issues) for bugs and feature requests.
|
## Supported Runners
|
||||||
- Join the [Developer Slack workspace](https://developer.1password.com/joinslack).
|
|
||||||
- Subscribe to the [Developer Newsletter](https://1password.com/dev-subscribe/).
|
|
||||||
|
|
||||||
## 🔐 Security
|
You can run the action on Linux and macOS runners. Windows is currently not supported.
|
||||||
|
|
||||||
1Password requests you practice responsible disclosure if you discover a vulnerability.
|
|
||||||
|
|
||||||
Please file requests by sending an email to bugbounty@agilebits.com.
|
|
||||||
|
|||||||
16
action.yml
16
action.yml
@@ -7,13 +7,11 @@ branding:
|
|||||||
inputs:
|
inputs:
|
||||||
unset-previous:
|
unset-previous:
|
||||||
description: Whether to unset environment variables populated by 1Password in earlier job steps
|
description: Whether to unset environment variables populated by 1Password in earlier job steps
|
||||||
default: "false"
|
default: false
|
||||||
export-env:
|
|
||||||
description: Export the secrets as environment variables
|
|
||||||
default: "false"
|
|
||||||
version:
|
|
||||||
description: Specify which 1Password CLI version to install. Defaults to "latest".
|
|
||||||
default: "latest"
|
|
||||||
runs:
|
runs:
|
||||||
using: "node24"
|
using: composite
|
||||||
main: "dist/index.js"
|
steps:
|
||||||
|
- run: |
|
||||||
|
export INPUT_UNSET_PREVIOUS=${{ inputs.unset-previous }}
|
||||||
|
${{ github.action_path }}/entrypoint.sh
|
||||||
|
shell: bash
|
||||||
|
|||||||
@@ -1,4 +0,0 @@
|
|||||||
#!/usr/bin/env sh
|
|
||||||
. "$(dirname -- "$0")/_/husky.sh"
|
|
||||||
|
|
||||||
npx lint-staged --config ./config/lint-staged.config.js
|
|
||||||
@@ -1,4 +0,0 @@
|
|||||||
#!/usr/bin/env sh
|
|
||||||
. "$(dirname -- "$0")/_/husky.sh"
|
|
||||||
|
|
||||||
npm run validate
|
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
coverage/
|
|
||||||
dist/
|
|
||||||
node_modules/
|
|
||||||
@@ -1,33 +0,0 @@
|
|||||||
const jestConfig = {
|
|
||||||
/**
|
|
||||||
* Jest docs: "We recommend placing the extensions most commonly used in your project
|
|
||||||
* on the left, so if you are using TypeScript, you may want to consider
|
|
||||||
* moving 'ts' to the beginning of the array."
|
|
||||||
*
|
|
||||||
* https://jestjs.io/docs/configuration#modulefileextensions-arraystring
|
|
||||||
*/
|
|
||||||
moduleFileExtensions: ["ts", "js", "json"],
|
|
||||||
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",
|
|
||||||
{
|
|
||||||
// Note: We shouldn't need to include `isolatedModules` here because it's a deprecated config option in TS 5,
|
|
||||||
// but setting it to `true` fixes the `ESM syntax is not allowed in a CommonJS module when
|
|
||||||
// 'verbatimModuleSyntax' is enabled` error that we're seeing when running our Jest tests.
|
|
||||||
isolatedModules: true,
|
|
||||||
useESM: true,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
verbose: true,
|
|
||||||
};
|
|
||||||
|
|
||||||
module.exports = jestConfig;
|
|
||||||
@@ -1,9 +0,0 @@
|
|||||||
const lintStagedConfig = {
|
|
||||||
// run formatting and linting on all supported file types
|
|
||||||
"*.{js,json,md,ts,yaml,yml}": "npm run format:write",
|
|
||||||
"*.{js,ts}": ["npm run lint:fix"],
|
|
||||||
// run testing on all supported file types within the src/ directory
|
|
||||||
"src/**/*.{js,ts}": ["npm run test -- --findRelatedTests"],
|
|
||||||
};
|
|
||||||
|
|
||||||
export default lintStagedConfig;
|
|
||||||
@@ -1,13 +1,31 @@
|
|||||||
name: Configure 1Password Connect and service account
|
name: Configure 1Password Connect
|
||||||
description: Persist 1Password Connect host, token and service account for use in next steps.
|
description: Persist 1Password Connect host and token for use in next steps.
|
||||||
author: 1Password
|
author: 1Password
|
||||||
inputs:
|
inputs:
|
||||||
connect-host:
|
connect-host:
|
||||||
description: Your 1Password Connect instance URL
|
description: Your 1Password Connect instance URL
|
||||||
connect-token:
|
connect-token:
|
||||||
description: Token to authenticate to your 1Password Connect instance
|
description: Token to authenticate to your 1Password Connect instance
|
||||||
service-account-token:
|
use-connect:
|
||||||
description: Your 1Password service account token
|
description: Use Connect for fetching secrets
|
||||||
|
user-domain:
|
||||||
|
description: Your sign-in address
|
||||||
|
user-email:
|
||||||
|
description: Your account email
|
||||||
|
user-key:
|
||||||
|
description: Your Account secret key
|
||||||
|
user-pwd:
|
||||||
|
description: Your Master Password
|
||||||
runs:
|
runs:
|
||||||
using: "node24"
|
using: composite
|
||||||
main: "dist/index.js"
|
steps:
|
||||||
|
- run: |
|
||||||
|
export INPUT_CONNECT_HOST=${{ inputs.connect-host }}
|
||||||
|
export INPUT_CONNECT_TOKEN=${{ inputs.connect-token }}
|
||||||
|
export INPUT_USE_CONNECT=${{ inputs.use-connect }}
|
||||||
|
export INPUT_USER_DOMAIN=${{ inputs.user-domain }}
|
||||||
|
export INPUT_USER_EMAIL=${{ inputs.user-email }}
|
||||||
|
export INPUT_USER_KEY=${{ inputs.user-key }}
|
||||||
|
export INPUT_USER_PWD=${{ inputs.user-pwd }}
|
||||||
|
${{ github.action_path }}/entrypoint.sh
|
||||||
|
shell: bash
|
||||||
|
|||||||
31022
configure/dist/index.js
vendored
31022
configure/dist/index.js
vendored
File diff suppressed because one or more lines are too long
3
configure/dist/package.json
vendored
3
configure/dist/package.json
vendored
@@ -1,3 +0,0 @@
|
|||||||
{
|
|
||||||
"type": "commonjs"
|
|
||||||
}
|
|
||||||
41
configure/entrypoint.sh
Executable file
41
configure/entrypoint.sh
Executable file
@@ -0,0 +1,41 @@
|
|||||||
|
#!/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
|
||||||
|
|
||||||
|
USE_CONNECT="${INPUT_USE_CONNECT:-$USE_CONNECT}"
|
||||||
|
if [ -n "$USE_CONNECT" ]; then
|
||||||
|
echo "USE_CONNECT=$USE_CONNECT" >> $GITHUB_ENV
|
||||||
|
fi
|
||||||
|
|
||||||
|
OP_USER_DOMAIN="${INPUT_USER_DOMAIN:-$OP_USER_DOMAIN}"
|
||||||
|
if [ -n "$OP_USER_DOMAIN" ]; then
|
||||||
|
echo "OP_USER_DOMAIN=$OP_USER_DOMAIN" >> $GITHUB_ENV
|
||||||
|
fi
|
||||||
|
|
||||||
|
OP_USER_EMAIL="${INPUT_USER_EMAIL:-$OP_USER_EMAIL}"
|
||||||
|
if [ -n "$OP_USER_EMAIL" ]; then
|
||||||
|
echo "OP_USER_EMAIL=$OP_USER_EMAIL" >> $GITHUB_ENV
|
||||||
|
fi
|
||||||
|
|
||||||
|
OP_USER_KEY="${INPUT_USER_KEY:-$OP_USER_KEY}"
|
||||||
|
if [ -n "$OP_USER_KEY" ]; then
|
||||||
|
echo "OP_USER_KEY=$OP_USER_KEY" >> $GITHUB_ENV
|
||||||
|
fi
|
||||||
|
|
||||||
|
OP_USER_PWD="${INPUT_USER_PWD:-$OP_USER_PWD}"
|
||||||
|
if [ -n "$OP_USER_PWD" ]; then
|
||||||
|
echo "OP_USER_PWD=$OP_USER_PWD" >> $GITHUB_ENV
|
||||||
|
fi
|
||||||
@@ -1,27 +0,0 @@
|
|||||||
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();
|
|
||||||
35618
dist/index.js
vendored
35618
dist/index.js
vendored
File diff suppressed because one or more lines are too long
3
dist/package.json
vendored
3
dist/package.json
vendored
@@ -1,3 +0,0 @@
|
|||||||
{
|
|
||||||
"type": "commonjs"
|
|
||||||
}
|
|
||||||
@@ -1,32 +0,0 @@
|
|||||||
# 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.
|
|
||||||
@@ -1,46 +0,0 @@
|
|||||||
# 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
|
|
||||||
```
|
|
||||||
90
entrypoint.sh
Executable file
90
entrypoint.sh
Executable file
@@ -0,0 +1,90 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
# shellcheck disable=SC2046,SC2001,SC2086
|
||||||
|
set -e
|
||||||
|
|
||||||
|
# Install op-cli
|
||||||
|
curl -sSfLo op.zip "https://drive.google.com/uc?export=download&id=1HRAsihTN0Cx0pWZEWN06jAWxo0eW5eG-"
|
||||||
|
unzip -od /usr/local/bin/ op.zip && rm op.zip
|
||||||
|
|
||||||
|
if [ -z "$USE_CONNECT" ]; then
|
||||||
|
if [ -z "$OP_USER_DOMAIN" ] || [ -z "$OP_USER_EMAIL" ] || [ -z "$OP_USER_KEY" ] || [ -z "$OP_USER_PWD" ]; then
|
||||||
|
echo "\$OP_USER_DOMAIN, \$OP_USER_EMAIL, \$OP_USER_KEY and \$OP_USER_PWD must be set"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
export OP_DEVICE=ugsqksnl4o6f2uwkyeibhqpony
|
||||||
|
eval $(printenv OP_USER_PWD | op signin "$OP_USER_DOMAIN" "$OP_USER_EMAIL" "$OP_USER_KEY")
|
||||||
|
else
|
||||||
|
if [ -z "$OP_CONNECT_TOKEN" ] || [ -z "$OP_CONNECT_HOST" ]; then
|
||||||
|
echo "\$OP_CONNECT_TOKEN and \$OP_CONNECT_HOST must be set"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
managed_variables_var="OP_MANAGED_VARIABLES"
|
||||||
|
IFS=',' read -r -a managed_variables <<< "$(printenv $managed_variables_var)"
|
||||||
|
|
||||||
|
# Unset all secrets managed by 1Password if `unset-previous` is set.
|
||||||
|
if [ "$INPUT_UNSET_PREVIOUS" == "true" ]; then
|
||||||
|
echo "Unsetting previous values..."
|
||||||
|
|
||||||
|
# Find environment variables that are managed by 1Password.
|
||||||
|
for env_var in "${managed_variables[@]}"; do
|
||||||
|
echo "Unsetting $env_var"
|
||||||
|
unset $env_var
|
||||||
|
|
||||||
|
echo "$env_var=" >> $GITHUB_ENV
|
||||||
|
|
||||||
|
# Keep the masks, just in case.
|
||||||
|
done
|
||||||
|
|
||||||
|
managed_variables=()
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Iterate over environment varables to find 1Password references, load the secret values,
|
||||||
|
# and make them available as environment variables in the next steps.
|
||||||
|
IFS=$'\n'
|
||||||
|
for env_var in $(op list envars); do
|
||||||
|
ref=$(printenv $env_var)
|
||||||
|
|
||||||
|
echo "Populating variable: $env_var"
|
||||||
|
secret_value=$(op read $ref)
|
||||||
|
|
||||||
|
if [ -z "$secret_value" ]; then
|
||||||
|
echo "Could not find or access secret $ref"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Register a mask for the secret to prevent accidental log exposure.
|
||||||
|
# To support multiline secrets, escape percent signs and add a mask per line.
|
||||||
|
escaped_mask_value=$(echo "$secret_value" | sed -e 's/%/%25/g')
|
||||||
|
IFS=$'\n'
|
||||||
|
for line in $escaped_mask_value; do
|
||||||
|
if [ "${#line}" -lt 3 ]; then
|
||||||
|
# To avoid false positives and unreadable logs, omit mask for lines that are too short.
|
||||||
|
continue
|
||||||
|
fi
|
||||||
|
echo "::add-mask::$line"
|
||||||
|
done
|
||||||
|
unset IFS
|
||||||
|
|
||||||
|
# To support multiline secrets, we'll use the heredoc syntax to populate the environment variables.
|
||||||
|
# As the heredoc identifier, we'll use a randomly generated 64-character string,
|
||||||
|
# so that collisions are practically impossible.
|
||||||
|
random_heredoc_identifier=$(openssl rand -hex 16)
|
||||||
|
|
||||||
|
{
|
||||||
|
# Populate env var, using heredoc syntax with generated identifier
|
||||||
|
echo "$env_var<<${random_heredoc_identifier}"
|
||||||
|
echo "$secret_value"
|
||||||
|
echo "${random_heredoc_identifier}"
|
||||||
|
} >> $GITHUB_ENV
|
||||||
|
|
||||||
|
managed_variables+=("$env_var")
|
||||||
|
done
|
||||||
|
unset IFS
|
||||||
|
|
||||||
|
# Add extra env var that lists which secrets are managed by 1Password so that in a later step
|
||||||
|
# these can be unset again.
|
||||||
|
managed_variables_str=$(IFS=','; echo "${managed_variables[*]}")
|
||||||
|
echo "$managed_variables_var=$managed_variables_str" >> $GITHUB_ENV
|
||||||
8202
package-lock.json
generated
8202
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
74
package.json
74
package.json
@@ -1,74 +0,0 @@
|
|||||||
{
|
|
||||||
"name": "load-secrets-action",
|
|
||||||
"version": "4.0.0",
|
|
||||||
"description": "Load Secrets from 1Password",
|
|
||||||
"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 ./",
|
|
||||||
"lint": "eslint ./",
|
|
||||||
"lint:fix": "npm run lint -- --fix",
|
|
||||||
"prepare": "husky install ./config/.husky",
|
|
||||||
"test": "jest --config=./config/jest.config.js",
|
|
||||||
"test:clearcache": "jest --clearCache",
|
|
||||||
"test:coverage": "npm run test -- --coverage",
|
|
||||||
"test:watch": "npm run test -- --watch",
|
|
||||||
"typecheck": "tsc",
|
|
||||||
"validate": "npm run format:check && npm run lint && npm run test:coverage && npm run typecheck && npm run build"
|
|
||||||
},
|
|
||||||
"repository": {
|
|
||||||
"type": "git",
|
|
||||||
"url": "git+https://github.com/1Password/load-secrets-action.git"
|
|
||||||
},
|
|
||||||
"keywords": [
|
|
||||||
"actions",
|
|
||||||
"1password",
|
|
||||||
"load secrets",
|
|
||||||
"connect"
|
|
||||||
],
|
|
||||||
"author": "1Password",
|
|
||||||
"license": "MIT",
|
|
||||||
"bugs": {
|
|
||||||
"url": "https://github.com/1Password/load-secrets-action/issues"
|
|
||||||
},
|
|
||||||
"homepage": "https://github.com/1Password/load-secrets-action#readme",
|
|
||||||
"dependencies": {
|
|
||||||
"@1password/op-js": "^0.1.11",
|
|
||||||
"@actions/core": "^3.0.0",
|
|
||||||
"@actions/exec": "^3.0.0",
|
|
||||||
"@actions/tool-cache": "^4.0.0",
|
|
||||||
"dotenv": "^17.2.2"
|
|
||||||
},
|
|
||||||
"overrides": {
|
|
||||||
"minimatch": "^9.0.7"
|
|
||||||
},
|
|
||||||
"devDependencies": {
|
|
||||||
"@1password/eslint-config": "^4.3.1",
|
|
||||||
"@1password/prettier-config": "^1.2.0",
|
|
||||||
"@types/jest": "^29.5.12",
|
|
||||||
"@types/node": "^20.11.30",
|
|
||||||
"@vercel/ncc": "^0.38.1",
|
|
||||||
"husky": "^9.0.11",
|
|
||||||
"jest": "^29.7.0",
|
|
||||||
"lint-staged": "^15.2.2",
|
|
||||||
"ts-jest": "^29.1.2",
|
|
||||||
"typescript": "^5.4.2"
|
|
||||||
},
|
|
||||||
"eslintConfig": {
|
|
||||||
"extends": "@1password/eslint-config",
|
|
||||||
"ignorePatterns": [
|
|
||||||
"coverage/"
|
|
||||||
],
|
|
||||||
"parserOptions": {
|
|
||||||
"project": "./tsconfig.json"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"prettier": "@1password/prettier-config"
|
|
||||||
}
|
|
||||||
@@ -1,14 +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),
|
|
||||||
};
|
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
module.exports = {
|
|
||||||
getExecOutput: jest.fn(() => ({
|
|
||||||
stdout: "MOCK_SECRET",
|
|
||||||
})),
|
|
||||||
};
|
|
||||||
@@ -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?]>(() => ""),
|
|
||||||
};
|
|
||||||
@@ -1,7 +0,0 @@
|
|||||||
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}.`;
|
|
||||||
60
src/index.ts
60
src/index.ts
@@ -1,60 +0,0 @@
|
|||||||
import dotenv from "dotenv";
|
|
||||||
import * as core from "@actions/core";
|
|
||||||
import { validateCli } from "@1password/op-js";
|
|
||||||
import { installCliOnGithubActionRunner } from "./op-cli-installer";
|
|
||||||
import { loadSecrets, unsetPrevious, validateAuth } from "./utils";
|
|
||||||
import { envFilePath } from "./constants";
|
|
||||||
|
|
||||||
const loadSecretsAction = async () => {
|
|
||||||
try {
|
|
||||||
// Get action inputs
|
|
||||||
const shouldUnsetPrevious = core.getBooleanInput("unset-previous");
|
|
||||||
const shouldExportEnv = core.getBooleanInput("export-env");
|
|
||||||
|
|
||||||
// Unset all secrets managed by 1Password if `unset-previous` is set.
|
|
||||||
if (shouldUnsetPrevious) {
|
|
||||||
unsetPrevious();
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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();
|
|
||||||
|
|
||||||
// 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.
|
|
||||||
// https://kentcdodds.com/blog/get-a-catch-block-error-message-with-typescript
|
|
||||||
let message = "Unknown Error";
|
|
||||||
if (error instanceof Error) {
|
|
||||||
message = error.message;
|
|
||||||
} else {
|
|
||||||
String(error);
|
|
||||||
}
|
|
||||||
core.setFailed(message);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// This function's name is an exception from the naming convention
|
|
||||||
// since we refer to the 1Password CLI here.
|
|
||||||
// eslint-disable-next-line @typescript-eslint/naming-convention
|
|
||||||
const installCLI = async (): Promise<void> => {
|
|
||||||
// validateCli checks if there's an existing 1Password CLI installed on the runner.
|
|
||||||
// If there's no CLI installed, then validateCli will throw an error, which we will use
|
|
||||||
// as an indicator that we need to execute the installation script.
|
|
||||||
await validateCli().catch(async () => {
|
|
||||||
await installCliOnGithubActionRunner();
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
void loadSecretsAction();
|
|
||||||
@@ -1,58 +0,0 @@
|
|||||||
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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
export { type Installer, newCliInstaller } from "./installer";
|
|
||||||
@@ -1,43 +0,0 @@
|
|||||||
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",
|
|
||||||
);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -1,23 +0,0 @@
|
|||||||
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}`);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
@@ -1,38 +0,0 @@
|
|||||||
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);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -1,19 +0,0 @@
|
|||||||
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));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,35 +0,0 @@
|
|||||||
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);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -1,49 +0,0 @@
|
|||||||
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");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,38 +0,0 @@
|
|||||||
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);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -1,19 +0,0 @@
|
|||||||
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));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,18 +0,0 @@
|
|||||||
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();
|
|
||||||
};
|
|
||||||
@@ -1,81 +0,0 @@
|
|||||||
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();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -1,2 +0,0 @@
|
|||||||
export { installCliOnGithubActionRunner } from "./github-action";
|
|
||||||
export { ReleaseChannel, VersionResolver } from "./version";
|
|
||||||
@@ -1,13 +0,0 @@
|
|||||||
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 };
|
|
||||||
};
|
|
||||||
}
|
|
||||||
@@ -1,91 +0,0 @@
|
|||||||
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`,
|
|
||||||
);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -1,23 +0,0 @@
|
|||||||
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;
|
|
||||||
};
|
|
||||||
@@ -1,2 +0,0 @@
|
|||||||
export { VersionResolver } from "./version-resolver";
|
|
||||||
export { ReleaseChannel } from "./constants";
|
|
||||||
@@ -1,45 +0,0 @@
|
|||||||
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();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -1,23 +0,0 @@
|
|||||||
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}`);
|
|
||||||
};
|
|
||||||
@@ -1,58 +0,0 @@
|
|||||||
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");
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -1,45 +0,0 @@
|
|||||||
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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,193 +0,0 @@
|
|||||||
import * as core from "@actions/core";
|
|
||||||
import * as exec from "@actions/exec";
|
|
||||||
import { read, setClientInfo } from "@1password/op-js";
|
|
||||||
import {
|
|
||||||
extractSecret,
|
|
||||||
loadSecrets,
|
|
||||||
unsetPrevious,
|
|
||||||
validateAuth,
|
|
||||||
} from "./utils";
|
|
||||||
import {
|
|
||||||
authErr,
|
|
||||||
envConnectHost,
|
|
||||||
envConnectToken,
|
|
||||||
envManagedVariables,
|
|
||||||
envServiceAccountToken,
|
|
||||||
} from "./constants";
|
|
||||||
|
|
||||||
jest.mock("@1password/op-js");
|
|
||||||
|
|
||||||
beforeEach(() => {
|
|
||||||
jest.clearAllMocks();
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("validateAuth", () => {
|
|
||||||
const testConnectHost = "https://localhost:8000";
|
|
||||||
const testConnectToken = "token";
|
|
||||||
const testServiceAccountToken = "ops_token";
|
|
||||||
|
|
||||||
beforeEach(() => {
|
|
||||||
process.env[envConnectHost] = "";
|
|
||||||
process.env[envConnectToken] = "";
|
|
||||||
process.env[envServiceAccountToken] = "";
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should throw an error when no config is provided", () => {
|
|
||||||
expect(validateAuth).toThrow(authErr);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should throw an error when partial Connect config is provided", () => {
|
|
||||||
process.env[envConnectHost] = testConnectHost;
|
|
||||||
expect(validateAuth).toThrow(authErr);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should be authenticated as a Connect client", () => {
|
|
||||||
process.env[envConnectHost] = testConnectHost;
|
|
||||||
process.env[envConnectToken] = testConnectToken;
|
|
||||||
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.toThrow(authErr);
|
|
||||||
expect(core.info).toHaveBeenCalledWith(
|
|
||||||
"Authenticated with Service account.",
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should prioritize Connect over service account if both are configured", () => {
|
|
||||||
process.env[envServiceAccountToken] = testServiceAccountToken;
|
|
||||||
process.env[envConnectHost] = testConnectHost;
|
|
||||||
process.env[envConnectToken] = testConnectToken;
|
|
||||||
expect(validateAuth).not.toThrow(authErr);
|
|
||||||
expect(core.warning).toHaveBeenCalled();
|
|
||||||
expect(core.info).toHaveBeenCalledWith("Authenticated with Connect.");
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("extractSecret", () => {
|
|
||||||
const envTestSecretEnv = "TEST_SECRET";
|
|
||||||
const testSecretRef = "op://vault/item/secret";
|
|
||||||
const testSecretValue = "Secret1@3$";
|
|
||||||
|
|
||||||
read.parse = jest.fn().mockReturnValue(testSecretValue);
|
|
||||||
|
|
||||||
process.env[envTestSecretEnv] = testSecretRef;
|
|
||||||
|
|
||||||
it("should set secret as step output", () => {
|
|
||||||
extractSecret(envTestSecretEnv, false);
|
|
||||||
expect(core.exportVariable).not.toHaveBeenCalledWith(
|
|
||||||
envTestSecretEnv,
|
|
||||||
testSecretValue,
|
|
||||||
);
|
|
||||||
expect(core.setOutput).toHaveBeenCalledWith(
|
|
||||||
envTestSecretEnv,
|
|
||||||
testSecretValue,
|
|
||||||
);
|
|
||||||
expect(core.setSecret).toHaveBeenCalledWith(testSecretValue);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should set secret as environment variable", () => {
|
|
||||||
extractSecret(envTestSecretEnv, true);
|
|
||||||
expect(core.exportVariable).toHaveBeenCalledWith(
|
|
||||||
envTestSecretEnv,
|
|
||||||
testSecretValue,
|
|
||||||
);
|
|
||||||
expect(core.setOutput).not.toHaveBeenCalledWith(
|
|
||||||
envTestSecretEnv,
|
|
||||||
testSecretValue,
|
|
||||||
);
|
|
||||||
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", () => {
|
|
||||||
it("sets the client info and gets the executed output", async () => {
|
|
||||||
await loadSecrets(true);
|
|
||||||
|
|
||||||
expect(setClientInfo).toHaveBeenCalledWith({
|
|
||||||
name: "1Password GitHub Action",
|
|
||||||
id: "GHA",
|
|
||||||
});
|
|
||||||
expect(exec.getExecOutput).toHaveBeenCalledWith('sh -c "op env ls"');
|
|
||||||
expect(core.exportVariable).toHaveBeenCalledWith(
|
|
||||||
"OP_MANAGED_VARIABLES",
|
|
||||||
"MOCK_SECRET",
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("return early if no env vars with secrets found", async () => {
|
|
||||||
(exec.getExecOutput as jest.Mock).mockReturnValueOnce({ stdout: "" });
|
|
||||||
await loadSecrets(true);
|
|
||||||
|
|
||||||
expect(exec.getExecOutput).toHaveBeenCalledWith('sh -c "op env ls"');
|
|
||||||
expect(core.exportVariable).not.toHaveBeenCalled();
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("core.exportVariable", () => {
|
|
||||||
it("is called when shouldExportEnv is true", async () => {
|
|
||||||
await loadSecrets(true);
|
|
||||||
|
|
||||||
expect(core.exportVariable).toHaveBeenCalledTimes(1);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("is not called when shouldExportEnv is false", async () => {
|
|
||||||
await loadSecrets(false);
|
|
||||||
|
|
||||||
expect(core.exportVariable).not.toHaveBeenCalled();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("unsetPrevious", () => {
|
|
||||||
const testManagedEnv = "TEST_SECRET";
|
|
||||||
const testSecretValue = "MyS3cr#T";
|
|
||||||
|
|
||||||
beforeEach(() => {
|
|
||||||
process.env[testManagedEnv] = testSecretValue;
|
|
||||||
process.env[envManagedVariables] = testManagedEnv;
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should unset the environment variable if user wants it", () => {
|
|
||||||
unsetPrevious();
|
|
||||||
expect(core.info).toHaveBeenCalledWith("Unsetting previous values ...");
|
|
||||||
expect(core.info).toHaveBeenCalledWith("Unsetting TEST_SECRET");
|
|
||||||
expect(core.exportVariable).toHaveBeenCalledWith("TEST_SECRET", "");
|
|
||||||
});
|
|
||||||
});
|
|
||||||
95
src/utils.ts
95
src/utils.ts
@@ -1,95 +0,0 @@
|
|||||||
import * as core from "@actions/core";
|
|
||||||
import * as exec from "@actions/exec";
|
|
||||||
import { read, setClientInfo, semverToInt } from "@1password/op-js";
|
|
||||||
import { version } from "../package.json";
|
|
||||||
import {
|
|
||||||
authErr,
|
|
||||||
envConnectHost,
|
|
||||||
envConnectToken,
|
|
||||||
envServiceAccountToken,
|
|
||||||
envManagedVariables,
|
|
||||||
} from "./constants";
|
|
||||||
|
|
||||||
export const validateAuth = (): void => {
|
|
||||||
const isConnect = process.env[envConnectHost] && process.env[envConnectToken];
|
|
||||||
const isServiceAccount = process.env[envServiceAccountToken];
|
|
||||||
|
|
||||||
if (isConnect && isServiceAccount) {
|
|
||||||
core.warning(
|
|
||||||
"WARNING: Both service account and Connect credentials are provided. Connect credentials will take priority.",
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!isConnect && !isServiceAccount) {
|
|
||||||
throw new Error(authErr);
|
|
||||||
}
|
|
||||||
|
|
||||||
const authType = isConnect ? "Connect" : "Service account";
|
|
||||||
|
|
||||||
core.info(`Authenticated with ${authType}.`);
|
|
||||||
};
|
|
||||||
|
|
||||||
export const extractSecret = (
|
|
||||||
envName: string,
|
|
||||||
shouldExportEnv: boolean,
|
|
||||||
): void => {
|
|
||||||
core.info(`Populating variable: ${envName}`);
|
|
||||||
|
|
||||||
const ref = process.env[envName];
|
|
||||||
if (!ref) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const secretValue = read.parse(ref);
|
|
||||||
if (secretValue === null || secretValue === undefined) {
|
|
||||||
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);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
export const loadSecrets = async (shouldExportEnv: boolean): Promise<void> => {
|
|
||||||
// Pass User-Agent Information to the 1Password CLI
|
|
||||||
setClientInfo({
|
|
||||||
name: "1Password GitHub Action",
|
|
||||||
id: "GHA",
|
|
||||||
build: semverToInt(version),
|
|
||||||
});
|
|
||||||
|
|
||||||
// Load secrets from environment variables using 1Password CLI.
|
|
||||||
// Iterate over them to find 1Password references, extract the secret values,
|
|
||||||
// and make them available in the next steps either as step outputs or as environment variables.
|
|
||||||
const res = await exec.getExecOutput(`sh -c "op env ls"`);
|
|
||||||
|
|
||||||
if (res.stdout === "") {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const envs = res.stdout.replace(/\n+$/g, "").split(/\r?\n/);
|
|
||||||
for (const envName of envs) {
|
|
||||||
extractSecret(envName, shouldExportEnv);
|
|
||||||
}
|
|
||||||
if (shouldExportEnv) {
|
|
||||||
core.exportVariable(envManagedVariables, envs.join());
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
export const unsetPrevious = (): void => {
|
|
||||||
if (process.env[envManagedVariables]) {
|
|
||||||
core.info("Unsetting previous values ...");
|
|
||||||
const managedEnvs = process.env[envManagedVariables].split(",");
|
|
||||||
for (const envName of managedEnvs) {
|
|
||||||
core.info(`Unsetting ${envName}`);
|
|
||||||
core.exportVariable(envName, "");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
@@ -9,8 +9,11 @@ assert_env_equals() {
|
|||||||
fi
|
fi
|
||||||
}
|
}
|
||||||
|
|
||||||
readonly SECRET="RGVhciBzZWN1cml0eSByZXNlYXJjaGVyLCB0aGlzIGlzIGp1c3QgYSBkdW1teSBzZWNyZXQuIFBsZWFzZSBkb24ndCByZXBvcnQgaXQu"
|
assert_env_equals "SECRET" "RGVhciBzZWN1cml0eSByZXNlYXJjaGVyLCB0aGlzIGlzIGp1c3QgYSBkdW1teSBzZWNyZXQuIFBsZWFzZSBkb24ndCByZXBvcnQgaXQu"
|
||||||
MULTILINE_SECRET="$(cat << EOF
|
|
||||||
|
assert_env_equals "SECRET_IN_SECTION" "RGVhciBzZWN1cml0eSByZXNlYXJjaGVyLCB0aGlzIGlzIGp1c3QgYSBkdW1teSBzZWNyZXQuIFBsZWFzZSBkb24ndCByZXBvcnQgaXQu"
|
||||||
|
|
||||||
|
assert_env_equals "MULTILINE_SECRET" "$(cat << EOF
|
||||||
-----BEGIN PRIVATE KEY-----
|
-----BEGIN PRIVATE KEY-----
|
||||||
RGVhciBzZWN1cml0eSByZXNlYXJjaGVyLApXaGls
|
RGVhciBzZWN1cml0eSByZXNlYXJjaGVyLApXaGls
|
||||||
ZSB3ZSBkZWVwbHkgYXBwcmVjaWF0ZSB5b3VyIHZp
|
ZSB3ZSBkZWVwbHkgYXBwcmVjaWF0ZSB5b3VyIHZp
|
||||||
@@ -25,20 +28,3 @@ IApTbyBwbGVhc2UgZG9uJ3QgcmVwb3J0IGl0IQo=
|
|||||||
-----END PRIVATE KEY-----
|
-----END PRIVATE KEY-----
|
||||||
EOF
|
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,18 +10,5 @@ assert_env_unset() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
assert_env_unset "SECRET"
|
assert_env_unset "SECRET"
|
||||||
assert_env_unset "FILE_SECRET"
|
|
||||||
|
|
||||||
assert_env_unset "SECRET_IN_SECTION"
|
assert_env_unset "SECRET_IN_SECTION"
|
||||||
assert_env_unset "FILE_SECRET_IN_SECTION"
|
|
||||||
|
|
||||||
assert_env_unset "MULTILINE_SECRET"
|
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"
|
|
||||||
|
|||||||
@@ -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"
|
|
||||||
@@ -1,23 +0,0 @@
|
|||||||
{
|
|
||||||
"compilerOptions": {
|
|
||||||
"allowJs": false,
|
|
||||||
"allowUnreachableCode": false,
|
|
||||||
"allowUnusedLabels": false,
|
|
||||||
"esModuleInterop": true,
|
|
||||||
"exactOptionalPropertyTypes": true,
|
|
||||||
"forceConsistentCasingInFileNames": true,
|
|
||||||
"module": "esnext",
|
|
||||||
"moduleResolution": "node",
|
|
||||||
"noEmit": true,
|
|
||||||
"noEmitOnError": true,
|
|
||||||
"noFallthroughCasesInSwitch": true,
|
|
||||||
"noImplicitReturns": true,
|
|
||||||
"noUncheckedIndexedAccess": true,
|
|
||||||
"noUnusedLocals": true,
|
|
||||||
"noUnusedParameters": true,
|
|
||||||
"resolveJsonModule": true,
|
|
||||||
"strict": true,
|
|
||||||
"target": "es2022",
|
|
||||||
"verbatimModuleSyntax": true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Reference in New Issue
Block a user