Compare commits

..

40 Commits

Author SHA1 Message Date
Eduard Filip
581a835fb5 Prepare release v2.0.0 (#67)
Some checks failed
Run acceptance tests / unit-tests (push) Has been cancelled
Run acceptance tests / test-with-output-secrets (connect, ubuntu-latest) (push) Has been cancelled
Run acceptance tests / test-with-output-secrets (service-account, macos-latest) (push) Has been cancelled
Run acceptance tests / test-with-output-secrets (service-account, ubuntu-latest) (push) Has been cancelled
Run acceptance tests / test-with-export-env (connect, ubuntu-latest) (push) Has been cancelled
Run acceptance tests / test-with-export-env (service-account, macos-latest) (push) Has been cancelled
Run acceptance tests / test-with-export-env (service-account, ubuntu-latest) (push) Has been cancelled
Run acceptance tests / test-references-with-ids (connect, ubuntu-latest) (push) Has been cancelled
Run acceptance tests / test-references-with-ids (service-account, macos-latest) (push) Has been cancelled
Run acceptance tests / test-references-with-ids (service-account, ubuntu-latest) (push) Has been cancelled
2024-03-20 15:06:34 +01:00
Eduard Filip
cf1a288161 Remove protocol prepending (#66)
This code is no longer needed. Instead, we will enforce users to add the protocol themselves.
2024-02-21 21:19:05 +01:00
Eduard Filip
2792fede48 Migrate action to Typescript (#36)
* Make function for executing script

* Migrate auth validation

* Migrate load secret functionality

  We make use of the following in the migration:
  - `op-js` package (make direct calls to the CLI and nicely get the output of the commands)
  - `core.exportVariable` to nicely export a secret as an environment variable
  - `core.setOutput` to nicely export a secret a the step’s output.
  - `core.setSecret` to mask the value of the secret if logged on the action’s output.

  Note: `core.exportVariable` and `core.setOutput` work with multiline secrets without any additional work on our side.

  Also, we export the temporary path where the CLI is installed to make sure the `op-js` package can find it.

* Fix CLI installation process

* Fix conditional of appending protocol

  Fix conditional of appending `http://` to the Connect host.

* Update CLI version and improve script

* Use core.addPath

  This is a safer and nicer way to ensure the path to the CLI is included later in the pipeline (including this GitHub action).

* Use version from package.json

  This eliminates the duplication of version in the code

* Upgrade to Typescript 5

* Prettify test.yml

* Move constants to constants.ts

  This shows better what constants we use and they will be later used in both code and tests.

* Move 'validateAuth' to 'utils.ts'

* Add validate auth tests

* Extract functionality for extracting a secret

  This will enable us to easily test the functionality of the action regarding the extraction of secret and how it provides it to the rest of the pipeline based on user's input

* Add tests for extracting secret

* Move 'unsetPrevious' to 'utils.ts'

* Add unit test pipeline

* Add tests for 'unsetPrevious'

* Improve disabling eslint rules

  Disable the ES Lint rules only for the next line and add a comment explaining why it’s disabled.

* Improve code based on PR review feedback

  This contains code improvements that were easy to address based on PR review feedback.

* Improve CLI installation functionality

  Two key elements are improved:
  - The action will now automatically fetch the latest stable version of the CLI. There’s no longer the need to hardcode the version and manually update it.
  - The action will now perform a check if the CLI exists in the pipeline and install it if it’s not available.

* Simplify extractSecret functionality

  Eliminate the nested conditionals to have a cleaner and more readable code.

* Fix CLI version

  The curl would return the version number, but we forgot to append the `v` in the version (i.e. from 2.18.0 to v2.18.0). Now it should be fixed.

* Move loadSecrets function to utils.ts

  This is done to keep things modular and narrow down the scope and complexity of index.ts.

  `installCLI` will be kept in `index.ts` for the following reasons:
  - Moving it to utils brings complications (`import.meta.url` doesn’t work)
  - This code will be removed once the action will make use of the separate install CLI action

* Simplify code related to mocking

* Use semverToInt from op-js

  Version `0.1.9` of the `op-js` exports function `semverToInt`, therefore we no longer need to duplicate it in our code.

* Improve CLI installation script

  - Add architectures for Linux runners. Fail if the architecture is not supported.
  - Fail if the runner’s operating system is not supported.

* Change from debug messages to info

  In pre-TS GitHub Action, we’d print some messages to the output as info (e.g. authenticated as, populating variable, unsetting previous values). Therefore, we apply the same principle here since there’s useful info.

* use toHaveBeenCalled consistently in tests

  `toBeCalled` is an alias for `toHaveBeenCalled` and `toBeCalledWith` is an alias for `toHaveBeenCalledWith`. For consistency, we will use `toHaveBeenCalled` and `toHaveBeenCalledWith` consistently across our tests.

* Add warning if both configs are provided

  1Password CLI will prioritize Connect config (with `OP_CONNECT_HOST` and `OP_CONNECT_TOKEN`) over service account one (with `OP_SERVICE_ACCOUNT_TOKEN`). This shouldn’t happen, therefore we print a warning to the user if both are provided.

* Add comment about cli validation process

  The code itself seems a bit confusing, therefore we add a comment explaining how it works.

* test: assertions for loadSecrets function

* Improve loadSecrets function

  Return early if no env vars with valid secret references are found

* Update dependencies

* Upgrade action to use Node20

---------

Co-authored-by: Dustin Ruetz <dustin.ruetz@agilebits.com>
2024-02-21 17:38:38 +01:00
Eduard Filip
b575844081 Fix example snippet in README (#65) 2024-02-20 17:38:19 +01:00
Eduard Filip
c7a1c1e3bb Update dependencies (#57) 2023-10-24 18:02:10 +02:00
Eduard Filip
a2a357a196 Update dependencies (#55) 2023-10-10 17:52:45 +02:00
Eduard Filip
d1a4e73495 Update packages (#52)
Some checks failed
Run acceptance tests / test-with-output-secrets (connect, ubuntu-latest) (push) Has been cancelled
Run acceptance tests / test-with-output-secrets (service-account, macos-latest) (push) Has been cancelled
Run acceptance tests / test-with-output-secrets (service-account, ubuntu-latest) (push) Has been cancelled
Run acceptance tests / test-with-export-env (connect, ubuntu-latest) (push) Has been cancelled
Run acceptance tests / test-with-export-env (service-account, macos-latest) (push) Has been cancelled
Run acceptance tests / test-with-export-env (service-account, ubuntu-latest) (push) Has been cancelled
Run acceptance tests / test-references-with-ids (connect, ubuntu-latest) (push) Has been cancelled
Run acceptance tests / test-references-with-ids (service-account, macos-latest) (push) Has been cancelled
Run acceptance tests / test-references-with-ids (service-account, ubuntu-latest) (push) Has been cancelled
Fix dependabot vulnerability
2023-07-06 17:02:51 +01:00
Eduard Filip
83858b7236 Extract CLI version without jq (#51)
It seems that we can't assume that images have `jq` built-in, therefore we will use `grep` which comes built-in with all UNIX systems to extract the latest CLI version number.
2023-07-06 12:54:55 +01:00
Eduard Filip
10ed0757b7 Improve the shell script (#49)
Some checks failed
Run acceptance tests / test-with-output-secrets (connect, ubuntu-latest) (push) Has been cancelled
Run acceptance tests / test-with-output-secrets (service-account, macos-latest) (push) Has been cancelled
Run acceptance tests / test-with-output-secrets (service-account, ubuntu-latest) (push) Has been cancelled
Run acceptance tests / test-with-export-env (connect, ubuntu-latest) (push) Has been cancelled
Run acceptance tests / test-with-export-env (service-account, macos-latest) (push) Has been cancelled
Run acceptance tests / test-with-export-env (service-account, ubuntu-latest) (push) Has been cancelled
Run acceptance tests / test-references-with-ids (connect, ubuntu-latest) (push) Has been cancelled
Run acceptance tests / test-references-with-ids (service-account, macos-latest) (push) Has been cancelled
Run acceptance tests / test-references-with-ids (service-account, ubuntu-latest) (push) Has been cancelled
* Improve CLI installation script
  - Add additional architectures for Linux.
  - Stop the action if the runner is executed in an unsupported OS.
  - Fetch automatically the latest stable CLI version.

* Switch to new syntax for setting step output.
  GitHub has deprecated the syntax we were using for setting a step’s output (https://github.blog/changelog/2022-10-11-github-actions-deprecating-save-state-and-set-output-commands/). Therefore, we’re switching to the new one.

* Stop action if arch is unsupported for Linux runners.
2023-07-05 16:50:31 +01:00
Eduard Filip
539eaa66ee Improve the repo’s README (#48)
Use a new template for the README file to better present the content.
2023-07-05 11:36:30 +01:00
Eduard Filip
08315da4b3 Merge pull request #44 from settlemint/main
feat: install the right op for arm on linux
2023-07-03 16:54:07 +01:00
roderik.eth
0e91b4a315 fix: arm uname 2023-05-18 21:48:54 +02:00
roderik.eth
9c2d98ed07 feat: install the right op for arm on linux 2023-05-18 20:51:49 +02:00
volodymyrZotov
d8ac5d7286 Merge pull request #42 from 1Password/ruetz-service-accounts-exiting-beta
Remove 'BETA' references from Service Accounts
2023-05-16 20:09:59 +03:00
Dustin Ruetz
15d95ae871 style: auto-formatting fixes via Prettier 2023-05-09 14:23:45 -04:00
Dustin Ruetz
a48d1fcd00 docs: remove 'BETA' references from Service Accounts 2023-05-09 14:21:13 -04:00
Eduard Filip
8fd274c5eb Merge pull request #39 from 1Password/eddy/service-account-docs
Add documentation related to Service Accounts (currently in beta)
2023-04-24 11:19:45 +02:00
Eddy Filip
f4303b27ca Improve wording 2023-04-24 11:17:52 +02:00
Eddy Filip
f4a99d4598 Make small edits 2023-04-20 20:50:59 +02:00
Eddy Filip
9c1afd6054 Adjust action versions used in examples
In this way we keep them relevant with the latest versions
2023-04-20 18:53:49 +02:00
Eddy Filip
a02ee663cc Add documentation for service accounts 2023-04-20 18:50:07 +02:00
Eduard Filip
663ac229cb Merge pull request #37 from 1Password/eddy/improve-pipelines
Improve pipelines
2023-04-11 10:42:15 +02:00
Eddy Filip
9bb44334eb Compress tests
Since we use matrices now for os and authentication type, we’ve optimized the yaml file to have only 3 jobs, each one making 3 separate piepeline tests (2 for service accounts, 1 for Connect)
2023-04-10 16:52:51 +02:00
Eddy Filip
1ec261f63f exclude macos runners for Connect tests 2023-04-10 16:46:08 +02:00
Eddy Filip
2e386ac304 Try an authentication matrix 2023-04-10 16:37:39 +02:00
Eddy Filip
0b706bbe43 Add os matrix for tests 2023-04-10 16:12:29 +02:00
Eddy Filip
e23df52c69 Update checkout version 2023-04-10 16:10:52 +02:00
Dustin Ruetz
ade3078eb5 Tooling improvements (#34)
* feat: update tsconfig, uninstall vercel/ncc package and use tsc to build project

* feat: install 1password/front-end-style and configure Prettier

* feat: configure ESLint and fix lint errors

* build(deps): update types/node package to latest version

* feat: configure Jest

* feat: add 'validate' script to run formatting/linting/testing/building all together

* build: rebuild the dist/index.js file

* feat: make NPM scripts more granular

* refactor: make it clearer that Prettier config is being loaded from an existing dependency

* feat: add Husky and lint-staged to run pre-commit and pre-push checks

* fix: lint-staged testing step and Jest config file

* build(deps): update types/node package to latest version

* refactor: remove findrelated test script

* fix: move tsconfig.json to root directory, reinstall vercel/ncc for building

* fix: call to run function in index.ts

* build: rebuild the dist/index.js file

* fix: replace CommonJS __dirname with an ESModule equivalent

* fix: ignore config/.husky during ShellCheck

* fix: ignore .husky directory during ShellCheck

* fix: update lint.yml config to match ShellCheck README.md

* fix: remove coveragePathIgnorePatterns option from Jest config since node_modules is already the default value

* refactor: use './' prefix to refer to folders in the current directory in tsconfig.json

* fix: handle edge case where Error constructor is modified and add comments for context

* feat: bump package.json version to 1.2.0 to match current release

* fix: update lint.yml to use ShellCheck 2.0.0 exact version

* build(deps): update types/node package to latest version

* fix: update package-lock.json version 1.2.0

* feat: remove pre-commit and pre-push NPM scripts to simplify package.json

* fix: remove empty 'Default' column from 'configure Action Inputs' table

* fix: change the default values in action.yml to strings as per YAML validation
2023-03-29 11:34:37 -04:00
Eduard Filip
4ee6567f7b Merge pull request #35 from 1Password/fix/docs-link
Fix documentation link
2023-03-24 16:18:13 +01:00
Eddy Filip
27b96b5fd8 Fix documentation link 2023-03-24 13:32:26 +01:00
Eduard Filip
14687e434a Enable installing the CLI on MacOS runners without sudo privileges (#32) 2023-03-08 12:17:03 +01:00
Eduard Filip
1a17146422 Merge pull request #28 from mamercad/permission-denied-usr-local-bin
Use temp directory for `op` (versus static `/usr/local/bin`)
2023-03-07 13:01:50 +01:00
Eduard Filip
94dcd16f05 Merge pull request #31 from 1Password/revert-29-environment-file-output
Revert "Use environment file to manage output instead of `set-output`"
2023-03-03 15:08:49 +01:00
Eduard Filip
4cd70e1a09 Revert "Use environment file to manage output instead of set-output" 2023-03-03 12:11:10 +01:00
Eduard Filip
b30803866e Merge pull request #29 from Manokii/environment-file-output
Use environment file to manage output instead of `set-output`
2023-03-01 18:06:39 +01:00
Eduard Filip
feb6ed7c04 Merge pull request #30 from 1Password/update-packages
Update packages
2023-03-01 14:26:05 +01:00
Eddy Filip
d669a8ba06 Update packages 2023-03-01 12:29:55 +01:00
Jasper Concepcion
936f62b7d9 fix: use environment file to manage output
resolves 1Password/load-secrets-action#27
2023-02-14 00:13:38 +08:00
Mark Mercado
95681075d8 Use temp directory for op (versus static /usr/local/bin) 2023-01-29 09:46:53 -05:00
Eduard Filip
0a7975f916 Ensure that the action is backwards-compatible (#25)
Some checks failed
Run acceptance tests / use-connect-without-export-env (push) Has been cancelled
Run acceptance tests / use-connect-with-export-env (push) Has been cancelled
Run acceptance tests / use-connect-with-references-with-id (push) Has been cancelled
Run acceptance tests / use-service-account-without-export-env (push) Has been cancelled
Run acceptance tests / use-service-account-with-export-env (push) Has been cancelled
Run acceptance tests / use-service-account-with-references-with-id (push) Has been cancelled
Run acceptance tests / run-on-macos-12 (push) Has been cancelled
Bring 2 changes that ensure that the GitHub Action is backwards compatible:

- Append `http://` if the prefix is not provided in the `OP_CONNECT_HOST` (this is caused by the fact that `curl` guesses the protocol if not provided (https://linux.die.net/man/1/curl), which we missed when switching to using the 1Password CLI as the backend of the action)
- Set the default of export-env to true, since that was the default behavior of the action until we added the possibility to export secrets as step's output.

Also, the documentation is adjusted to reflect these changes.
2022-12-22 12:46:28 +02:00
21 changed files with 37792 additions and 902 deletions

View File

@@ -5,6 +5,9 @@ jobs:
lint:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: ShellCheck
uses: ludeeus/action-shellcheck@1.1.0
- uses: actions/checkout@v3
- name: Run ShellCheck
uses: ludeeus/action-shellcheck@2.0.0
with:
ignore_paths: >-
.husky

View File

@@ -2,17 +2,41 @@ on: push
name: Run acceptance tests
jobs:
use-connect-without-export-env:
unit-tests:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: actions/checkout@v3
- uses: actions/setup-node@v3
with:
node-version: 20
- run: npm ci
- run: npm test
test-with-output-secrets:
strategy:
matrix:
os: [ubuntu-latest, macos-latest]
auth: [connect, service-account]
exclude:
- os: macos-latest
auth: connect
runs-on: ${{ matrix.os }}
steps:
- uses: actions/checkout@v3
- name: Launch 1Password Connect instance
if: ${{ matrix.auth == 'connect' }}
env:
OP_CONNECT_CREDENTIALS: ${{ secrets.OP_CONNECT_CREDENTIALS }}
run: |
echo "$OP_CONNECT_CREDENTIALS" > 1password-credentials.json
docker-compose -f tests/fixtures/docker-compose.yml up -d && sleep 10
- name: Configure Service account
if: ${{ matrix.auth == 'service-account' }}
uses: ./configure
with:
service-account-token: ${{ secrets.OP_SERVICE_ACCOUNT_TOKEN }}
- name: Configure 1Password Connect
if: ${{ matrix.auth == 'connect' }}
uses: ./configure # 1password/load-secrets-action/configure@<version>
with:
connect-host: http://localhost:8080
@@ -20,6 +44,8 @@ jobs:
- name: Load secrets
id: load_secrets
uses: ./ # 1password/load-secrets-action@<version>
with:
export-env: false
env:
SECRET: op://acceptance-tests/test-secret/password
SECRET_IN_SECTION: op://acceptance-tests/test-secret/test-section/password
@@ -30,17 +56,31 @@ jobs:
SECRET_IN_SECTION: ${{ steps.load_secrets.outputs.SECRET_IN_SECTION }}
MULTILINE_SECRET: ${{ steps.load_secrets.outputs.MULTILINE_SECRET }}
run: ./tests/assert-env-set.sh
use-connect-with-export-env:
runs-on: ubuntu-latest
test-with-export-env:
strategy:
matrix:
os: [ubuntu-latest, macos-latest]
auth: [connect, service-account]
exclude:
- os: macos-latest
auth: connect
runs-on: ${{ matrix.os }}
steps:
- uses: actions/checkout@v2
- uses: actions/checkout@v3
- name: Launch 1Password Connect instance
if: ${{ matrix.auth == 'connect' }}
env:
OP_CONNECT_CREDENTIALS: ${{ secrets.OP_CONNECT_CREDENTIALS }}
run: |
echo "$OP_CONNECT_CREDENTIALS" > 1password-credentials.json
docker-compose -f tests/fixtures/docker-compose.yml up -d && sleep 10
- name: Configure Service account
if: ${{ matrix.auth == 'service-account' }}
uses: ./configure
with:
service-account-token: ${{ secrets.OP_SERVICE_ACCOUNT_TOKEN }}
- name: Configure 1Password Connect
if: ${{ matrix.auth == 'connect' }}
uses: ./configure # 1password/load-secrets-action/configure@<version>
with:
connect-host: http://localhost:8080
@@ -48,8 +88,6 @@ jobs:
- name: Load secrets
id: load_secrets
uses: ./ # 1password/load-secrets-action@<version>
with:
export-env: true
env:
SECRET: op://acceptance-tests/test-secret/password
SECRET_IN_SECTION: op://acceptance-tests/test-secret/test-section/password
@@ -62,77 +100,41 @@ jobs:
unset-previous: true
- name: Assert removed secrets
run: ./tests/assert-env-unset.sh
use-connect-with-references-with-id:
runs-on: ubuntu-latest
test-references-with-ids:
strategy:
matrix:
os: [ubuntu-latest, macos-latest]
auth: [connect, service-account]
exclude:
- os: macos-latest
auth: connect
runs-on: ${{ matrix.os }}
steps:
- uses: actions/checkout@v2
- uses: actions/checkout@v3
- name: Launch 1Password Connect instance
if: ${{ matrix.auth == 'connect' }}
env:
OP_CONNECT_CREDENTIALS: ${{ secrets.OP_CONNECT_CREDENTIALS }}
run: |
echo "$OP_CONNECT_CREDENTIALS" > 1password-credentials.json
docker-compose -f tests/fixtures/docker-compose.yml up -d && sleep 10
- name: Configure Service account
if: ${{ matrix.auth == 'service-account' }}
uses: ./configure
with:
service-account-token: ${{ secrets.OP_SERVICE_ACCOUNT_TOKEN }}
- name: Configure 1Password Connect
if: ${{ matrix.auth == 'connect' }}
uses: ./configure # 1password/load-secrets-action/configure@<version>
with:
connect-host: http://localhost:8080
connect-token: ${{ secrets.OP_CONNECT_TOKEN }}
- name: Load secrets
id: load_secrets
uses: ./ # 1password/load-secrets-action@<version>
env:
SECRET: op://v5pz6venw4roosmkzdq2nhpv6u/hrgkzhrlvscomepxlgafb2m3ca/password
SECRET_IN_SECTION: op://v5pz6venw4roosmkzdq2nhpv6u/hrgkzhrlvscomepxlgafb2m3ca/Section_tco6nsqycj6jcbyx63h5isxcny/doxu3mhkozcznnk5vjrkpdqayy
MULTILINE_SECRET: op://v5pz6venw4roosmkzdq2nhpv6u/ghtz3jvcc6dqmzc53d3r3eskge/notesPlain
- name: Assert test secret values
env:
SECRET: ${{ steps.load_secrets.outputs.SECRET }}
SECRET_IN_SECTION: ${{ steps.load_secrets.outputs.SECRET_IN_SECTION }}
MULTILINE_SECRET: ${{ steps.load_secrets.outputs.MULTILINE_SECRET }}
run: ./tests/assert-env-set.sh
use-service-account-without-export-env:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Load secrets
id: load_secrets
uses: ./ # 1password/load-secrets-action@<version>
env:
OP_SERVICE_ACCOUNT_TOKEN: ${{ secrets.OP_SERVICE_ACCOUNT_TOKEN }}
SECRET: op://acceptance-tests/test-secret/password
SECRET_IN_SECTION: op://acceptance-tests/test-secret/test-section/password
MULTILINE_SECRET: op://acceptance-tests/multiline-secret/notesPlain
- name: Assert test secret values
env:
SECRET: ${{ steps.load_secrets.outputs.SECRET }}
SECRET_IN_SECTION: ${{ steps.load_secrets.outputs.SECRET_IN_SECTION }}
MULTILINE_SECRET: ${{ steps.load_secrets.outputs.MULTILINE_SECRET }}
run: ./tests/assert-env-set.sh
use-service-account-with-export-env:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Load secrets
id: load_secrets
uses: ./ # 1password/load-secrets-action@<version>
with:
export-env: true
export-env: false
env:
OP_SERVICE_ACCOUNT_TOKEN: ${{ secrets.OP_SERVICE_ACCOUNT_TOKEN }}
SECRET: op://acceptance-tests/test-secret/password
SECRET_IN_SECTION: op://acceptance-tests/test-secret/test-section/password
MULTILINE_SECRET: op://acceptance-tests/multiline-secret/notesPlain
- name: Assert test secret values
run: ./tests/assert-env-set.sh
use-service-account-with-references-with-id:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Load secrets
id: load_secrets
uses: ./ # 1password/load-secrets-action@<version>
env:
OP_SERVICE_ACCOUNT_TOKEN: ${{ secrets.OP_SERVICE_ACCOUNT_TOKEN }}
SECRET: op://v5pz6venw4roosmkzdq2nhpv6u/hrgkzhrlvscomepxlgafb2m3ca/password
SECRET_IN_SECTION: op://v5pz6venw4roosmkzdq2nhpv6u/hrgkzhrlvscomepxlgafb2m3ca/Section_tco6nsqycj6jcbyx63h5isxcny/doxu3mhkozcznnk5vjrkpdqayy
MULTILINE_SECRET: op://v5pz6venw4roosmkzdq2nhpv6u/ghtz3jvcc6dqmzc53d3r3eskge/notesPlain
@@ -142,21 +144,3 @@ jobs:
SECRET_IN_SECTION: ${{ steps.load_secrets.outputs.SECRET_IN_SECTION }}
MULTILINE_SECRET: ${{ steps.load_secrets.outputs.MULTILINE_SECRET }}
run: ./tests/assert-env-set.sh
run-on-macos-12:
runs-on: macos-12
steps:
- uses: actions/checkout@v2
- name: Load secrets
id: load_secrets
uses: ./ # 1password/load-secrets-action@<version>
env:
OP_SERVICE_ACCOUNT_TOKEN: ${{ secrets.OP_SERVICE_ACCOUNT_TOKEN }}
SECRET: op://acceptance-tests/test-secret/password
SECRET_IN_SECTION: op://acceptance-tests/test-secret/test-section/password
MULTILINE_SECRET: op://acceptance-tests/multiline-secret/notesPlain
- name: Assert test secret values
env:
SECRET: ${{ steps.load_secrets.outputs.SECRET }}
SECRET_IN_SECTION: ${{ steps.load_secrets.outputs.SECRET_IN_SECTION }}
MULTILINE_SECRET: ${{ steps.load_secrets.outputs.MULTILINE_SECRET }}
run: ./tests/assert-env-set.sh

1
.gitignore vendored
View File

@@ -1 +1,2 @@
coverage/
node_modules/

267
README.md
View File

@@ -1,36 +1,27 @@
# Load Secrets from 1Password - GitHub Action
<!-- Image sourced from https://blog.1password.com/1password-service-accounts/ -->
<img alt="" role="img" src="https://blog.1password.com/posts/2023/1password-service-accounts/header.png"/>
This action loads secrets from 1Password into GitHub Actions using [1Password Connect](https://1password.com/secrets/).
<div align="center">
<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>
---
`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).
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/ci-cd/github-actions).
Read more on the [1Password Developer Portal](https://developer.1password.com/docs/ci-cd/github-actions).
## 🪄 See it in action!
## Requirements
[![Using 1Password Service Accounts with GitHub Actions - showcase](https://img.youtube.com/vi/kVBl5iQYgSA/maxresdefault.jpg)](https://www.youtube.com/watch?v=kVBl5iQYgSA "Using 1Password Service Accounts with GitHub Actions")
Before you get started, you'll need to:
- [Deploy 1Password Connect](/docs/connect/get-started#step-2-deploy-1password-connect-server) in your infrastructure.
- Set the `OP_CONNECT_HOST` and `OP_CONNECT_TOKEN` environment variables to your Connect instance's credentials, so it'll be used to load secrets.
_Supported runners_: You can run the action on Mac and Linux runners. Windows is currently not supported.
## Usage
You can configure the action to use your 1Password Connect instance.
If you provide `OP_CONNECT_HOST` and `OP_CONNECT_TOKEN` variables, the Connect instance will be used to load secrets. Make sure [1Password Connect](https://support.1password.com/secrets-automation/#step-2-deploy-a-1password-connect-server) is deployed in your infrastructure.
There are two ways that secrets can be loaded:
- [use the secrets from the action's ouput](#use-secrets-from-the-actions-output)
- [export secrets as environment variables](#export-secrets-as-environment-variables)
### Use secrets from the action's output
This method allows for you to use the loaded secrets as an output from the step: `steps.step-id.outputs.secret-name`. You will need to set an id for the step that uses this action to be able to access its outputs. For more details, , see [`outputs.<output_id>`](https://docs.github.com/en/actions/creating-actions/metadata-syntax-for-github-actions#outputsoutput_id).
## ✨ Quickstart
```yml
on: push
@@ -38,230 +29,32 @@ jobs:
hello-world:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: actions/checkout@v3
- name: Load secret
id: op-load-secret
uses: 1password/load-secrets-action@v1
uses: 1password/load-secrets-action@v2
with:
# Export loaded secrets as environment variables
export-env: true
env:
OP_CONNECT_HOST: <Your Connect instance URL>
OP_CONNECT_TOKEN: ${{ secrets.OP_CONNECT_TOKEN }}
OP_SERVICE_ACCOUNT_TOKEN: ${{ secrets.OP_SERVICE_ACCOUNT_TOKEN }}
SECRET: op://app-cicd/hello-world/secret
- name: Print masked secret
run: echo "Secret: ${{ steps.op-load-secret.outputs.SECRET }}"
run: 'echo "Secret: $SECRET"'
# Prints: Secret: ***
```
<details>
<summary><b>Longer usage example</b></summary>
## 💙 Community & Support
```yml
on: push
name: Deploy app
- File an [issue](https://github.com/1Password/load-secrets-action/issues) for bugs and feature requests.
- Join the [Developer Slack workspace](https://join.slack.com/t/1password-devs/shared_invite/zt-1halo11ps-6o9pEv96xZ3LtX_VE0fJQA).
- Subscribe to the [Developer Newsletter](https://1password.com/dev-subscribe/).
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Configure 1Password Connect
uses: 1password/load-secrets-action/configure@v1
with:
# Persist the 1Password Connect URL for next steps. You can also persist
# 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
id: load-docker-credentials
uses: 1password/load-secrets-action@v1
env:
OP_CONNECT_TOKEN: ${{ secrets.OP_CONNECT_TOKEN }}
DOCKERHUB_USERNAME: op://app-cicd/docker/username
DOCKERHUB_TOKEN: op://app-cicd/docker/token
- name: Login to Docker Hub
uses: docker/login-action@v1
with:
username: ${{ steps.load-docker-credentials.outputs.DOCKERHUB_USERNAME }}
password: ${{ steps.load-docker-credentials.outputs.DOCKERHUB_TOKEN }}
- name: Build and push Docker image
uses: docker/build-push-action@v2
with:
push: true
tags: acme/app:latest
```
</details>
### Export secrets as environment variables
This method, allows the action to access the loaded secrets as environment variables. These environment variables are accessible at a job level.
```yml
on: push
jobs:
hello-world:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Load secret
uses: 1password/load-secrets-action@v1
with:
# Export loaded secrets as environment variables
export-env: true
env:
OP_CONNECT_HOST: <Your Connect instance URL>
OP_CONNECT_TOKEN: ${{ secrets.OP_CONNECT_TOKEN }}
SECRET: op://app-cicd/hello-world/secret
- name: Print masked secret
run: echo "Secret: $SECRET"
# Prints: Secret: ***
```
<details>
<summary><b>Longer usage example</b></summary>
```yml
on: push
name: Deploy app
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Configure 1Password Connect
uses: 1password/load-secrets-action/configure@v1
with:
# Persist the 1Password Connect URL for next steps. You can also persist
# 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
with:
# Export loaded secrets as environment variables
export-env: true
env:
OP_CONNECT_TOKEN: ${{ secrets.OP_CONNECT_TOKEN }}
DOCKERHUB_USERNAME: op://app-cicd/docker/username
DOCKERHUB_TOKEN: op://app-cicd/docker/token
- name: Login to Docker Hub
uses: docker/login-action@v1
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:
# Export loaded secrets as environment variables
export-env: true
# 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>
## Action Inputs
| Name | Default | Description |
| ---------------- | ------- | ---------------------------------------------------------------------------------- |
| `export-env` | `false` | Export the loaded secrets as environment variables |
| `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, fields from 1Password will automatically be masked from the GitHub Actions logs too.
So if one of these values accidentally gets printed, it'll get replaced with `***`.
## 1Password Configuration
To use the action with Connect, 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 host and token, set the `OP_CONNECT_HOST` and `OP_CONNECT_TOKEN` environment 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
on: push
jobs:
hello-world:
runs-on: ubuntu-latest
steps:
- 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
```
### `configure` Action Inputs
| 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 |
## Supported Runners
You can run the action on Linux and macOS runners. Windows is currently not supported.
## Security
## 🔐 Security
1Password requests you practice responsible disclosure if you discover a vulnerability.
Please file requests via [**BugCrowd**](https://bugcrowd.com/agilebits).
For information about security practices, please visit our [Security homepage](https://bugcrowd.com/agilebits).
## Getting help
If you find yourself stuck, visit our [**Support Page**](https://support.1password.com/) for help.
For information about security practices, please visit the [1Password Bug Bounty Program](https://bugcrowd.com/agilebits).

View File

@@ -7,10 +7,10 @@ branding:
inputs:
unset-previous:
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
default: "true"
runs:
using: 'node16'
main: 'dist/index.js'
using: "node20"
main: "dist/index.js"

4
config/.husky/pre-commit Executable file
View File

@@ -0,0 +1,4 @@
#!/usr/bin/env sh
. "$(dirname -- "$0")/_/husky.sh"
npx lint-staged --config ./config/lint-staged.config.js

4
config/.husky/pre-push Executable file
View File

@@ -0,0 +1,4 @@
#!/usr/bin/env sh
. "$(dirname -- "$0")/_/husky.sh"
npm run validate

3
config/.prettierignore Normal file
View File

@@ -0,0 +1,3 @@
coverage/
dist/
node_modules/

28
config/jest.config.js Normal file
View File

@@ -0,0 +1,28 @@
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",
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,
};
export default jestConfig;

View File

@@ -0,0 +1,9 @@
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;

28723
dist/index.js vendored

File diff suppressed because one or more lines are too long

3
dist/package.json vendored Normal file
View File

@@ -0,0 +1,3 @@
{
"type": "module"
}

View File

@@ -1,125 +0,0 @@
#!/bin/bash
# shellcheck disable=SC2046,SC2001,SC2086
set -e
# Pass User-Agent Inforomation to the 1Password CLI
export OP_INTEGRATION_NAME="1Password GitHub Action"
export OP_INTEGRATION_ID="GHA"
export OP_INTEGRATION_BUILDNUMBER="1010001"
readonly CONNECT="CONNECT"
readonly SERVICE_ACCOUNT="SERVICE_ACCOUNT"
auth_type=$CONNECT
managed_variables_var="OP_MANAGED_VARIABLES"
IFS=','
# Unset all secrets managed by 1Password if `unset-previous` is set.
unset_prev_secrets() {
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
}
# Install op-cli
install_op_cli() {
if [[ "$OSTYPE" == "linux-gnu"* ]]; then
curl -sSfLo op.zip "https://cache.agilebits.com/dist/1P/op2/pkg/v2.10.0-beta.02/op_linux_amd64_v2.10.0-beta.02.zip"
unzip -od /usr/local/bin/ op.zip && rm op.zip
elif [[ "$OSTYPE" == "darwin"* ]]; then
curl -sSfLo op.pkg "https://cache.agilebits.com/dist/1P/op2/pkg/v2.10.0-beta.02/op_apple_universal_v2.10.0-beta.02.pkg"
sudo installer -pkg op.pkg -target /usr/local/bin/ && rm op.pkg
fi
}
populating_secret() {
ref=$(printenv $1)
echo "Populating variable: $1"
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
if [ "$INPUT_EXPORT_ENV" == "true" ]; then
# 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 32)
{
# Populate env var, using heredoc syntax with generated identifier
echo "$env_var<<${random_heredoc_identifier}"
echo "$secret_value"
echo "${random_heredoc_identifier}"
} >> $GITHUB_ENV
echo "GITHUB_ENV: $(cat $GITHUB_ENV)"
else
# Prepare the secret_value to be outputed properly (especially multiline secrets)
secret_value=$(echo "$secret_value" | awk -v ORS='%0A' '1')
echo "::set-output name=$env_var::$secret_value"
fi
managed_variables+=("$env_var")
}
# Load environment variables using op cli. Iterate over them to find 1Password references, load the secret values,
# and make them available as environment variables in the next steps.
extract_secrets() {
IFS=$'\n'
for env_var in $(op env ls); do
populating_secret $env_var
done
}
read -r -a managed_variables <<< "$(printenv $managed_variables_var)"
if [ -z "$OP_CONNECT_TOKEN" ] || [ -z "$OP_CONNECT_HOST" ]; then
if [ -z "$OP_SERVICE_ACCOUNT_TOKEN" ]; then
echo "(\$OP_CONNECT_TOKEN and \$OP_CONNECT_HOST) or \$OP_SERVICE_ACCOUNT_TOKEN must be set"
exit 1
fi
auth_type=$SERVICE_ACCOUNT
fi
printf "Authenticated with %s \n" $auth_type
unset_prev_secrets
install_op_cli
extract_secrets
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

46
install_cli.sh Executable file
View File

@@ -0,0 +1,46 @@
#!/bin/bash
set -e
# Install op-cli
install_op_cli() {
# Create a temporary directory where the CLI is installed
OP_INSTALL_DIR="$(mktemp -d)"
if [[ ! -d "$OP_INSTALL_DIR" ]]; then
echo "Install dir $OP_INSTALL_DIR not found"
exit 1
fi
echo "::debug::OP_INSTALL_DIR: ${OP_INSTALL_DIR}"
# Get the latest stable version of the CLI
CLI_VERSION="v$(curl https://app-updates.agilebits.com/check/1/0/CLI2/en/2.0.0/N -s | grep -Eo '[0-9]+\.[0-9]+\.[0-9]+')"
if [[ "$OSTYPE" == "linux-gnu"* ]]; then
# Get runner's architecture
ARCH=$(uname -m)
if [[ "$(getconf LONG_BIT)" = 32 ]]; then
ARCH="386"
elif [[ "$ARCH" == "x86_64" ]]; then
ARCH="amd64"
elif [[ "$ARCH" == "aarch64" ]]; then
ARCH="arm64"
fi
if [[ "$ARCH" != "386" ]] && [[ "$ARCH" != "amd64" ]] && [[ "$ARCH" != "arm" ]] && [[ "$ARCH" != "arm64" ]]; then
echo "Unsupported architecture for the 1Password CLI: $ARCH."
exit 1
fi
curl -sSfLo op.zip "https://cache.agilebits.com/dist/1P/op2/pkg/${CLI_VERSION}/op_linux_${ARCH}_${CLI_VERSION}.zip"
unzip -od "$OP_INSTALL_DIR" op.zip && rm op.zip
elif [[ "$OSTYPE" == "darwin"* ]]; then
curl -sSfLo op.pkg "https://cache.agilebits.com/dist/1P/op2/pkg/${CLI_VERSION}/op_apple_universal_${CLI_VERSION}.pkg"
pkgutil --expand op.pkg temp-pkg
tar -xvf temp-pkg/op.pkg/Payload -C "$OP_INSTALL_DIR"
rm -rf temp-pkg && rm op.pkg
else
echo "Operating system not supported yet for this GitHub Action: $OSTYPE."
exit 1
fi
}
install_op_cli

8849
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,37 +1,67 @@
{
"name": "load-secrets-action",
"version": "1.1.0",
"description": "Load Secrets from 1Password",
"main": "dist/index.js",
"directories": {
"test": "tests"
},
"scripts": {
"build": "ncc build src/index.ts"
},
"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": {
"@actions/core": "^1.9.1",
"@actions/exec": "^1.1.1"
},
"devDependencies": {
"@types/node": "^18.7.14",
"@vercel/ncc": "^0.34.0",
"typescript": "^4.8.2"
}
"name": "load-secrets-action",
"version": "2.0.0",
"description": "Load Secrets from 1Password",
"type": "module",
"main": "dist/index.js",
"directories": {
"test": "tests"
},
"scripts": {
"build": "ncc build ./src/index.ts",
"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": "^1.10.1",
"@actions/exec": "^1.1.1"
},
"devDependencies": {
"@1password/front-end-style": "^6.0.1",
"@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": "./node_modules/@1password/front-end-style/eslintrc.yml",
"ignorePatterns": [
"coverage/"
],
"parserOptions": {
"project": "./tsconfig.json"
}
},
"prettier": "./node_modules/@1password/front-end-style/prettierrc.json"
}

6
src/constants.ts Normal file
View File

@@ -0,0 +1,6 @@
export const envConnectHost = "OP_CONNECT_HOST";
export const envConnectToken = "OP_CONNECT_TOKEN";
export const envServiceAccountToken = "OP_SERVICE_ACCOUNT_TOKEN";
export const envManagedVariables = "OP_MANAGED_VARIABLES";
export const authErr = `Authentication error with environment variables: you must set either 1) ${envServiceAccountToken}, or 2) both ${envConnectHost} and ${envConnectToken}.`;

View File

@@ -1,21 +1,67 @@
import * as core from '@actions/core';
import * as exec from '@actions/exec';
import path from 'path';
import path from "path";
import url from "url";
import * as core from "@actions/core";
import * as exec from "@actions/exec";
import { validateCli } from "@1password/op-js";
import { loadSecrets, unsetPrevious, validateAuth } from "./utils";
async function run(): Promise<void> {
try {
const parentDir = path.resolve(__dirname, '..');
// Get action inputs
process.env.INPUT_UNSET_PREVIOUS = core.getInput('unset-previous');
process.env.INPUT_EXPORT_ENV = core.getInput('export-env');
const loadSecretsAction = async () => {
try {
// Get action inputs
const shouldUnsetPrevious = core.getBooleanInput("unset-previous");
const shouldExportEnv = core.getBooleanInput("export-env");
// Execute bash script
await exec.exec(`sh -c "` + parentDir + `/entrypoint.sh"`);
// Unset all secrets managed by 1Password if `unset-previous` is set.
if (shouldUnsetPrevious) {
unsetPrevious();
}
} catch (error: any) {
core.setFailed(error.message);
}
}
// Validate that a proper authentication configuration is set for the CLI
validateAuth();
run();
// 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 () => {
const currentFile = url.fileURLToPath(import.meta.url);
const currentDir = path.dirname(currentFile);
const parentDir = path.resolve(currentDir, "..");
// Execute bash script
const cmdOut = await exec.getExecOutput(
`sh -c "` + parentDir + `/install_cli.sh"`,
);
// Add path to 1Password CLI to $PATH
const outArr = cmdOut.stdout.split("\n");
if (outArr[0] && process.env.PATH) {
const cliPath = outArr[0]?.replace(/^(::debug::OP_INSTALL_DIR: )/, "");
core.addPath(cliPath);
}
});
};
void loadSecretsAction();

164
src/utils.test.ts Normal file
View File

@@ -0,0 +1,164 @@
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("@actions/core");
jest.mock("@actions/exec", () => ({
getExecOutput: jest.fn(() => ({
stdout: "MOCK_SECRET",
})),
}));
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).toThrowError(authErr);
});
it("should throw an error when partial Connect config is provided", () => {
process.env[envConnectHost] = testConnectHost;
expect(validateAuth).toThrowError(authErr);
});
it("should be authenticated as a Connect client", () => {
process.env[envConnectHost] = testConnectHost;
process.env[envConnectToken] = testConnectToken;
expect(validateAuth).not.toThrowError(authErr);
expect(core.info).toHaveBeenCalledWith("Authenticated with Connect.");
});
it("should be authenticated as a service account", () => {
process.env[envServiceAccountToken] = testServiceAccountToken;
expect(validateAuth).not.toThrowError(authErr);
expect(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.toThrowError(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("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", "");
});
});

91
src/utils.ts Normal file
View File

@@ -0,0 +1,91 @@
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) {
return;
}
if (shouldExportEnv) {
core.exportVariable(envName, secretValue);
} else {
core.setOutput(envName, 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, "");
}
}
};

View File

@@ -1,13 +1,23 @@
{
"compilerOptions": {
"target": "es6",
"module": "commonjs",
"moduleResolution": "node",
"outDir": "./dist",
"rootDir": "./src",
"strict": true,
"noImplicitAny": true,
"esModuleInterop": true
},
"exclude": ["node_modules"]
"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
}
}