diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..5693a52 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,19 @@ +# EditorConfig is awesome: https://EditorConfig.org + +# top-most EditorConfig file +root = true + +# Unix-style newlines with a newline ending every file +[*] +end_of_line = lf +indent_size = 2 +indent_style = space +insert_final_newline = true +trim_trailing_whitespace = true +insert_final_newline = true + +[*.json] +insert_final_newline = true + +[*.bru] +trim_trailing_whitespace = false diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..6313b56 --- /dev/null +++ b/.gitattributes @@ -0,0 +1 @@ +* text=auto eol=lf diff --git a/.github/bruno-collection/bruno.json b/.github/bruno-collection/bruno.json new file mode 100644 index 0000000..3e118a2 --- /dev/null +++ b/.github/bruno-collection/bruno.json @@ -0,0 +1,9 @@ +{ + "version": "1", + "name": "bruno-run-action-tests", + "type": "collection", + "ignore": [ + "node_modules", + ".git" + ] +} diff --git a/.github/bruno-collection/collection.bru b/.github/bruno-collection/collection.bru new file mode 100644 index 0000000..e69de29 diff --git a/.github/bruno-collection/dummy.csv b/.github/bruno-collection/dummy.csv new file mode 100644 index 0000000..47929ee --- /dev/null +++ b/.github/bruno-collection/dummy.csv @@ -0,0 +1 @@ +foo,bar diff --git a/.github/bruno-collection/echo/echo json.bru b/.github/bruno-collection/echo/echo json.bru new file mode 100644 index 0000000..298c53f --- /dev/null +++ b/.github/bruno-collection/echo/echo json.bru @@ -0,0 +1,31 @@ +meta { + name: echo json + type: http + seq: 1 +} + +post { + url: {{host}}/api/echo/json + body: json + auth: none +} + +body:json { + { + "hello": "bruno" + } +} + +assert { + res.status: eq 200 +} + +tests { + test("should return json", function() { + const data = res.getBody(); + expect(data).to.eql({ + "hello": "bruno" + }); + }); + +} diff --git a/.github/bruno-collection/echo/echo plaintext.bru b/.github/bruno-collection/echo/echo plaintext.bru new file mode 100644 index 0000000..f2a7b98 --- /dev/null +++ b/.github/bruno-collection/echo/echo plaintext.bru @@ -0,0 +1,27 @@ +meta { + name: echo plaintext + type: http + seq: 3 +} + +post { + url: {{host}}/api/echo/text + body: text + auth: none +} + +body:text { + hello +} + +assert { + res.status: eq 200 +} + +tests { + test("should return plain text", function() { + const data = res.getBody(); + expect(data).to.eql("hello"); + }); + +} diff --git a/.github/bruno-collection/echo/echo xml parsed.bru b/.github/bruno-collection/echo/echo xml parsed.bru new file mode 100644 index 0000000..4adea51 --- /dev/null +++ b/.github/bruno-collection/echo/echo xml parsed.bru @@ -0,0 +1,35 @@ +meta { + name: echo xml parsed + type: http + seq: 2 +} + +post { + url: {{host}}/api/echo/xml-parsed + body: xml + auth: none +} + +body:xml { + + bruno + +} + +assert { + res.status: eq 200 +} + +tests { + test("should return parsed xml", function() { + const data = res.getBody(); + expect(data).to.eql({ + "hello": { + "world": [ + "bruno" + ] + } + }); + }); + +} diff --git a/.github/bruno-collection/echo/secret/echo secret.bru b/.github/bruno-collection/echo/secret/echo secret.bru new file mode 100644 index 0000000..f87b75a --- /dev/null +++ b/.github/bruno-collection/echo/secret/echo secret.bru @@ -0,0 +1,39 @@ +meta { + name: echo secret + type: http + seq: 1 +} + +post { + url: {{host}}/api/echo/json + body: json + auth: none +} + +auth:basic { + username: asd + password: j +} + +auth:bearer { + token: +} + +body:json { + { + "{{secretKey}}": "{{secretMessage}}" + } +} + +assert { + res.status: eq 200 +} + +tests { + test("should return secret message", () => { + expect(res.getBody()).to.eql({ + "hello": "secret world!" + }); + }); + +} diff --git a/.github/bruno-collection/environments/cicd.bru b/.github/bruno-collection/environments/cicd.bru new file mode 100644 index 0000000..6d14934 --- /dev/null +++ b/.github/bruno-collection/environments/cicd.bru @@ -0,0 +1,7 @@ +vars { + host: https://testbench-sanity.usebruno.com +} +vars:secret [ + secretKey, + secretMessage +] diff --git a/.github/bruno-collection/ping.bru b/.github/bruno-collection/ping.bru new file mode 100644 index 0000000..c832e72 --- /dev/null +++ b/.github/bruno-collection/ping.bru @@ -0,0 +1,23 @@ +meta { + name: ping + type: http + seq: 1 +} + +get { + url: {{host}}/ping + body: none + auth: none +} + +assert { + res.status: eq 200 + ~res.body: eq {{pong}} +} + +tests { + test("should ping pong", function() { + const data = res.getBody(); + expect(data).to.equal("pong"); + }); +} diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000..9d98c2a --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,21 @@ +version: 2 +updates: + - package-ecosystem: docker + directory: / + schedule: + interval: weekly + groups: + docker-minor: + update-types: + - minor + - patch + + - package-ecosystem: github-actions + directory: / + schedule: + interval: weekly + groups: + actions-minor: + update-types: + - minor + - patch diff --git a/.github/linters/.checkov.yaml b/.github/linters/.checkov.yaml new file mode 100644 index 0000000..c306051 --- /dev/null +++ b/.github/linters/.checkov.yaml @@ -0,0 +1,6 @@ +quiet: true +skip-check: + # Ensure that HEALTHCHECK instructions have been added to container images + - CKV_DOCKER_2 + # Ensure that a user for the container has been created + - CKV_DOCKER_3 diff --git a/.github/linters/.hadolint.yaml b/.github/linters/.hadolint.yaml new file mode 100644 index 0000000..eee9f9b --- /dev/null +++ b/.github/linters/.hadolint.yaml @@ -0,0 +1,3 @@ +ignored: + - DL3018 + - DL3059 diff --git a/.github/linters/.yaml-lint.yml b/.github/linters/.yaml-lint.yml new file mode 100644 index 0000000..561b6de --- /dev/null +++ b/.github/linters/.yaml-lint.yml @@ -0,0 +1,10 @@ +rules: + document-end: disable + document-start: + level: warning + present: false + line-length: + level: warning + max: 100 + allow-non-breakable-words: true + allow-non-breakable-inline-mappings: true diff --git a/.github/workflows/ci.env b/.github/workflows/ci.env new file mode 100644 index 0000000..1a5e341 --- /dev/null +++ b/.github/workflows/ci.env @@ -0,0 +1,15 @@ +BRUNO_ACTION_DRY_RUN=true +IN_BAIL=true +IN_CA_CERT=customcert.pem +IN_CSV_FILEPATH=.github/bruno-collection/dummy.csv +IN_ENV=cicd +IN_ENV_VARS=apikey=myPassword\nid=myId +IN_FILENAME=users/get-user.bru +IN_IGNORE_TRUSTSTORE=true +IN_INSECURE=true +IN_OUTPUT=output.html +IN_OUTPUT_FORMAT=html +IN_PATH=. +IN_RECURSIVE=true +IN_SANDBOX=safe +IN_TESTS_ONLY=true diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..31b1761 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,129 @@ +name: Continuous Integration + +on: + pull_request: + branches: + - main + push: + branches: + - main + +permissions: + contents: read + +jobs: + test-docker: + name: Docker Tests + runs-on: ubuntu-latest + + # Run a local registry to push to + services: + registry: + image: registry:2 + ports: + - 5001:5000 + + env: + TEST_TAG: localhost:5001/actions/bruno-run-action:latest + RUN_OUTPUT: run-output.log + + steps: + - name: Checkout + id: checkout + uses: actions/checkout@v4 + + - name: Setup Docker BuildX + id: setup-buildx + uses: docker/setup-buildx-action@v3 + with: + install: true + driver-opts: network=host + + - name: Build the Container + id: build + uses: docker/build-push-action@v6 + with: + context: . + push: true + tags: ${{ env.TEST_TAG }} + + # prettier-ignore + - name: Run the Container + id: run + run: docker run --env-file .github/workflows/ci.env --rm ${{ env.TEST_TAG }} > ${{ env.RUN_OUTPUT }} + + # prettier-ignore + - name: Assert output + id: assert + run: | + cat ${{ env.RUN_OUTPUT }} + grep -q 'run users/get-user.bru' ${{ env.RUN_OUTPUT }} || (echo '::error::incorrect collection path' && exit 1) + grep -q '\-r' ${{ env.RUN_OUTPUT }} || (echo '::error::-r was not supplied' && exit 1) + grep -q '\-\-ignore\-truststore' ${{ env.RUN_OUTPUT }} || (echo '::error::--ignore-truststore was not applied' && exit 1) + grep -q '\-\-env cicd' ${{ env.RUN_OUTPUT }} || (echo '::error::incorrect --env supplied' && exit 1) + grep -q '\-\-env\-var apikey=myPassword' ${{ env.RUN_OUTPUT }} || (echo '::error::incorrect --env-var for apikey' && exit 1) + grep -q '\-\-env\-var id=myId' ${{ env.RUN_OUTPUT }} || (echo '::error::incorrect --env-var for id' && exit 1) + grep -q '\-\-output /usr/src/output.html' ${{ env.RUN_OUTPUT }} || (echo '::error::incorrect --output path' && exit 1) + grep -q '\-\-format html' ${{ env.RUN_OUTPUT }} || (echo '::error::incorrect --format' && exit 1) + grep -q '\-\-cacert customcert.pem' ${{ env.RUN_OUTPUT }} || (echo '::error::incorrect --ca-cert path' && exit 1) + grep -q '\-\-insecure' ${{ env.RUN_OUTPUT }} || (echo '::error::--insecure was not applied' && exit 1) + grep -q '\-\-sandbox safe' ${{ env.RUN_OUTPUT }} || (echo '::error::--sandbox was not supplied' && exit 1) + grep -q '\-\-tests\-only' ${{ env.RUN_OUTPUT }} || (echo '::error::--tests-only was not supplied' && exit 1) + grep -q '\-\-csv\-file\-path' ${{ env.RUN_OUTPUT }} || (echo '::error::--csv-file-path was not supplied' && exit 1) + grep -q '\-\-bail' ${{ env.RUN_OUTPUT }} || (echo '::error::--bail was not supplied' && exit 1) + + bru-run-successfully: + name: Action Successful Run + runs-on: ubuntu-latest + steps: + - name: Checkout + id: checkout + uses: actions/checkout@v4 + + - name: Test Local Action + id: bru-run + uses: ./ + with: + path: .github/bruno-collection + env: cicd + env-vars: |- + secretKey='${{ secrets.CI_TEST_SECRET_KEY }}' + secretMessage='${{ secrets.CI_TEST_SECRET_MESSAGE }}' + output: bru-run-output.json + output-format: json + insecure: true + tests-only: false + bail: false + + - name: Succeed if run was successful + if: ${{ steps.bru-run.outputs.success == 'true' }} + run: exit 0 + + - name: Fail if run was not successful + if: ${{ steps.bru-run.outputs.success == 'false' }} + run: exit 1 + + bru-run-failure: + name: Action Failure Run + runs-on: ubuntu-latest + steps: + - name: Checkout + id: checkout + uses: actions/checkout@v4 + + - name: Test Local Action + id: bru-run + continue-on-error: true + uses: ./ + with: + path: .github/bruno-collection + tests-only: false + bail: true + + - name: Succeed if run was not successful + if: ${{ steps.bru-run.outputs.success == 'false' }} + run: exit 0 + + - name: Fail if run was successful + if: ${{ steps.bru-run.outputs.success == 'true' }} + run: exit 1 diff --git a/.github/workflows/linter.yml b/.github/workflows/linter.yml new file mode 100644 index 0000000..4c88696 --- /dev/null +++ b/.github/workflows/linter.yml @@ -0,0 +1,34 @@ +name: Lint Codebase + +on: + pull_request: + branches: + - main + push: + branches: + - main + +permissions: + contents: read + packages: read + statuses: write + +jobs: + lint: + name: Lint Codebase + runs-on: ubuntu-latest + + steps: + - name: Checkout + id: checkout + uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Lint Codebase + id: super-linter + uses: super-linter/super-linter/slim@v7 + env: + DEFAULT_BRANCH: main + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + VALIDATE_ALL_CODEBASE: true diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..dcff431 --- /dev/null +++ b/.gitignore @@ -0,0 +1,25 @@ +# Logs +logs +*.log + +# Diagnostic reports (https://nodejs.org/api/report.html) +report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json + +# Runtime data +pids +*.pid +*.seed +*.pid.lock + +# dotenv environment variables file +.env +.env.test + +# OS metadata +.DS_Store +Thumbs.db + +# IDE files +.idea +.vscode +*.code-workspace diff --git a/.prettierignore b/.prettierignore new file mode 100644 index 0000000..3bfd435 --- /dev/null +++ b/.prettierignore @@ -0,0 +1 @@ +.github/bruno-collection diff --git a/.prettierrc.json b/.prettierrc.json new file mode 100644 index 0000000..b2154c2 --- /dev/null +++ b/.prettierrc.json @@ -0,0 +1,16 @@ +{ + "printWidth": 100, + "tabWidth": 2, + "useTabs": false, + "semi": false, + "singleQuote": true, + "quoteProps": "as-needed", + "jsxSingleQuote": false, + "trailingComma": "none", + "bracketSpacing": true, + "bracketSameLine": true, + "arrowParens": "avoid", + "proseWrap": "always", + "htmlWhitespaceSensitivity": "css", + "endOfLine": "lf" +} diff --git a/CODEOWNERS b/CODEOWNERS new file mode 100644 index 0000000..cb9106a --- /dev/null +++ b/CODEOWNERS @@ -0,0 +1,3 @@ +# Repository CODEOWNERS + +* @krummbar diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..181c30d --- /dev/null +++ b/Dockerfile @@ -0,0 +1,17 @@ +# Set the base image to use for subsequent instructions +FROM node:lts-alpine3.20 + +# bash is required for entrypoint.sh +RUN apk add --no-cache bash + +# Install bruno cli +RUN npm install -g @usebruno/cli@1.x + +# Set the working directory inside the container +WORKDIR /usr/src + +# Copy any source file(s) required for the action +COPY entrypoint.sh . + +# Configure the container to be run as an executable +ENTRYPOINT ["/usr/src/entrypoint.sh"] diff --git a/README.adoc b/README.adoc new file mode 100644 index 0000000..c8f5d6c --- /dev/null +++ b/README.adoc @@ -0,0 +1,260 @@ += Bruno Run Action +// ############################################################ +// ATTENTION! +// ---------- +// Do not edit the README.adoc file. It is generated from the sources +// located in the /docs folder. The root file for the documentation is +// /docs/index.adoc +// ############################################################ +:source-highlighter: highlight.js +:toc: macro +:icons: font +ifdef::env-github[] +:tip-caption: :bulb: +:note-caption: :information_source: +:important-caption: :heavy_exclamation_mark: +:caution-caption: :fire: +:warning-caption: :warning: +endif::[] +:action-badge-ci: https://github.com/krummbar/bruno-run-action/actions/workflows/ci.yml/badge.svg +:action-badge-lint: https://github.com/krummbar/bruno-run-action/actions/workflows/linter.yml/badge.svg +:bru-cli-version: 1.x +:url-action-ci: https://github.com/krummbar/bruno-run-action/actions/workflows/ci.yml +:url-action-lint: https://github.com/krummbar/bruno-run-action/actions/workflows/linter.yml +:url-bruno-cli: https://docs.usebruno.com/bru-cli/overview +:url-bruno-npm: https://www.npmjs.com/package/@usebruno/cli + +image:{action-badge-ci}[Continuous Integration,link={url-action-ci}] +image:{action-badge-lint}[Lint Codebase,link={url-action-lint}] + +Containerized GitHub Action executing {url-bruno-cli}[bruno cli] runner. +Can be used to execute sanity checks and integration tests via bruno collections. +Internally executes {url-bruno-npm}[`@usebruno/cli@1.x`], passing the action's input parameters to the command line interface. + +toc::[] + +:leveloffset: 1 + += Usage + +The following example runs a collection located in the folder `bruno/api-it`. +It uses the bruno environment `integration`, +and provides the secret environment variables `foo=bar` and `say=hello`. +The runner results are dumped to the JSON file `./output.json`. + +.Run full collection with secrets +[source,yaml] +---- +name: Bruno Test Collection + +on: + workflow_dispatch: + +jobs: + bruno-tests: + name: Bruno test collection + runs-on: ubuntu-latest + steps: + - name: Bruno CLI runner + id: bru-cli + uses: krummbar/bruno-run-action@main # <1> + with: + path: bruno/api-it + env: integration + env-vars: |- # <2> + foo=bar + say='${{ secrets.MESSAGE }}' + output: output.json + output-format: json +---- +<1> Change `@main` to a specific commit SHA or version tag, e.g.: + +`krummbar/bruno-run-action@e76147da8e5c81eaf017dede5645551d4b94427b` + +`krummbar/bruno-run-action@v0.1.0` +<2> envVars need to be separated with line breaks + +.Run single request +[source,yaml] +---- +name: Bruno Run Individual Folder + +on: + workflow_dispatch: + +jobs: + bruno-tests: + name: Bruno test collection + runs-on: ubuntu-latest + steps: + - name: Bruno CLI runner + id: bru-cli + uses: krummbar/bruno-run-action@main + with: + path: bruno/api-it # <1> + filename: echo # <2> + recursive: true # <3> +---- +<1> Path to bruno collection +<2> Relative path of the request folder inside the collection +<3> Includes all requests in subfolders + +== Inputs + +[source,yaml] +---- +csv-file-path: + description: CSV file pat for input parameters + required: false +path: + # prettier-ignore + description: >- + Path of the target bruno collection. + Should point to the root directory of the collection. + If not provided it uses the current working directory. + required: false + default: . +filename: + # prettier-ignore + description: >- + File or folder name of the requests to run. + It is relative to the `path` input. + required: false +recursive: + description: Indicate a recursive run. + required: false + default: 'false' +ca-cert: + description: CA certificate to verify peer against. + required: false +env: + description: Select a collection environment. + required: false + default: '' +env-vars: + description: List of environment variables to set with the run. + required: false + default: '' +ignore-truststore: + # prettier-ignore + description: >- + The specified custom CA certificate (--cacert) will be used exclusively and the default truststore is ignored, + if this option is specified. + Evaluated in combination with "--cacert" only. + required: false + default: 'false' +output: + # prettier-ignore + description: >- + Output file containing test results. + The path is relative to the current working-directory and not to the targeted bruno collection. + required: false +output-format: + # prettier-ignore + description: >- + The output format of the test results. + Possible values `html|json|junit`. + required: false + default: json +insecure: + description: Allow insecure server connections. + required: false + default: 'false' +sandbox: + description: Javscript sandbox to use. Available sandboxes are 'developer|safe' + required: false + default: '' +tests-only: + description: Only run requests that have a test. + required: false +bail: + description: Stop execution after a failure of a request, test, or assertion. + required: false +---- + +== Outputs + +[source,yaml] +---- +success: + description: Indicates test run success status. +bru-version: + description: Bruno CLI version used during workflow execution. +---- + +:leveloffset!: + +== Development + +:leveloffset: 2 + += Test Locally + +After you've cloned the repository to your local machine or codespace, you'll +need to perform some initial setup steps before you can test your action. + +[NOTE] +==== +You'll need to have a reasonably modern version of +https://www.docker.com/get-started/[Docker] handy (e.g. docker engine +version 20 or later). +==== + +. :hammer_and_wrench: Build the container ++ +[source,console] +---- +docker build -t bruno-run-action-local . +---- + +. :white_check_mark: Test the container ++ +You can pass individual environment variables using the `--env` or `-e` flag. ++ +[source,console] +---- +$ docker run --env INPUT_PATH=".github/bruno-collection" --env INPUT_ENV="cicd" --env BRUNO_ACTION_DRY_RUN="true" -v ${PWD}:/usr/src bruno-run-action-local +::notice::collection directory: '/usr/src/.github/bruno-collection' +::notice::bru run --env cicd +::notice::Executed in dry mode, skipped executing bruno collection +---- ++ +Or you can pass a file with environment variables using `--env-file`. ++ +[source,console] +---- +$ docker run --env-file .github/workflows/ci.env -v ${PWD}/.github:/usr/src/.github bruno-run-action-local +::notice::collection directory: '/usr/src/.github/bruno-collection' +::notice::bru run users/get-user.bru -r --env cicd --output /usr/src/output.html --format html --insecure --tests-only --bail --env-var apikey=myPassword --env-var id=myId +::notice::Executed in dry mode, skipped executing bruno collection +---- ++ +[TIP] +==== +If `BRUNO_ACTION_DRY_RUN=true` is provided, +execution of the actual collection is skipped. +Only the fully composed bru run command with all arguments is dumped. +==== + +:leveloffset!: + +:leveloffset: 2 + += Documentation +:url-asciidoctor: https://docs.asciidoctor.org/asciidoc/latest/ +:url-asciidoctor-reducer: https://github.com/asciidoctor/asciidoctor-reducer + +The `README.adoc` file is generated from the sources in the link:docs[docs] folder. +Any documentation changes must be applied to the files located in there. + +Prerequisites:: +* {url-asciidoctor}[AsciiDoc] +* {url-asciidoctor-reducer}[AsciiDoctor Reducer] + +In order to update the contents of the `README.adoc` run the following command. + +.Update README.adoc +[source,console] +---- +asciidoctor-reducer --preserve-conditionals -o README.adoc docs/index.adoc +---- + +:leveloffset!: diff --git a/action.yml b/action.yml new file mode 100644 index 0000000..fc5536e --- /dev/null +++ b/action.yml @@ -0,0 +1,103 @@ +name: bru-run-action +description: Executes Bru CLI against a bruno collection or single request. +author: Max Bauer +branding: + color: orange + icon: chevron-right + +inputs: + # tag::inputs[] + csv-file-path: + description: CSV file pat for input parameters + required: false + path: + # prettier-ignore + description: >- + Path of the target bruno collection. + Should point to the root directory of the collection. + If not provided it uses the current working directory. + required: false + default: . + filename: + # prettier-ignore + description: >- + File or folder name of the requests to run. + It is relative to the `path` input. + required: false + recursive: + description: Indicate a recursive run. + required: false + default: 'false' + ca-cert: + description: CA certificate to verify peer against. + required: false + env: + description: Select a collection environment. + required: false + default: '' + env-vars: + description: List of environment variables to set with the run. + required: false + default: '' + ignore-truststore: + # prettier-ignore + description: >- + The specified custom CA certificate (--cacert) will be used exclusively and the default truststore is ignored, + if this option is specified. + Evaluated in combination with "--cacert" only. + required: false + default: 'false' + output: + # prettier-ignore + description: >- + Output file containing test results. + The path is relative to the current working-directory and not to the targeted bruno collection. + required: false + output-format: + # prettier-ignore + description: >- + The output format of the test results. + Possible values `html|json|junit`. + required: false + default: json + insecure: + description: Allow insecure server connections. + required: false + default: 'false' + sandbox: + description: Javscript sandbox to use. Available sandboxes are 'developer|safe' + required: false + tests-only: + description: Only run requests that have a test. + required: false + bail: + description: Stop execution after a failure of a request, test, or assertion. + required: false + # end::inputs[] + +outputs: + # tag::outputs[] + success: + description: Indicates test run success status. + bru-version: + description: Bruno CLI version used during workflow execution. + # end::outputs[] + +runs: + using: docker + image: Dockerfile + env: + IN_CSV_FILEPATH: ${{ inputs.csv-file-path }} + IN_PATH: ${{ inputs.path }} + IN_FILENAME: ${{ inputs.filename }} + IN_IGNORE_TRUSTSTORE: ${{ inputs.ignore-truststore }} + IN_RECURSIVE: ${{ inputs.recursive }} + IN_CA_CERT: ${{ inputs.ca-cert }} + IN_ENV: ${{ inputs.env }} + IN_ENV_VARS: ${{ inputs.env-vars }} + IN_OUTPUT: ${{ inputs.output }} + IN_OUTPUT_FORMAT: ${{ inputs.output-format }} + IN_SANDBOX: ${{ inputs.sandbox }} + IN_INSECURE: ${{ inputs.insecure }} + IN_TESTS_ONLY: ${{ inputs.tests-only }} + IN_BAIL: ${{ inputs.bail }} diff --git a/docs/documentation.adoc b/docs/documentation.adoc new file mode 100644 index 0000000..6ec6cdb --- /dev/null +++ b/docs/documentation.adoc @@ -0,0 +1,18 @@ += Documentation +:url-asciidoctor: https://docs.asciidoctor.org/asciidoc/latest/ +:url-asciidoctor-reducer: https://github.com/asciidoctor/asciidoctor-reducer + +The `README.adoc` file is generated from the sources in the link:docs[docs] folder. +Any documentation changes must be applied to the files located in there. + +Prerequisites:: +* {url-asciidoctor}[AsciiDoc] +* {url-asciidoctor-reducer}[AsciiDoctor Reducer] + +In order to update the contents of the `README.adoc` run the following command. + +.Update README.adoc +[source,console] +---- +asciidoctor-reducer --preserve-conditionals -o README.adoc docs/index.adoc +---- diff --git a/docs/edit-advise.adoc b/docs/edit-advise.adoc new file mode 100644 index 0000000..22a18ea --- /dev/null +++ b/docs/edit-advise.adoc @@ -0,0 +1,7 @@ +// ############################################################ +// ATTENTION! +// ---------- +// Do not edit the README.adoc file. It is generated from the sources +// located in the /docs folder. The root file for the documentation is +// /docs/index.adoc +// ############################################################ diff --git a/docs/examples/usage-single-folder.yaml b/docs/examples/usage-single-folder.yaml new file mode 100644 index 0000000..1f03019 --- /dev/null +++ b/docs/examples/usage-single-folder.yaml @@ -0,0 +1,17 @@ +name: Bruno Run Individual Folder + +on: + workflow_dispatch: + +jobs: + bruno-tests: + name: Bruno test collection + runs-on: ubuntu-latest + steps: + - name: Bruno CLI runner + id: bru-cli + uses: krummbar/bruno-run-action@main + with: + path: bruno/api-it # <1> + filename: echo # <2> + recursive: true # <3> diff --git a/docs/examples/usage.yaml b/docs/examples/usage.yaml new file mode 100644 index 0000000..967c06a --- /dev/null +++ b/docs/examples/usage.yaml @@ -0,0 +1,21 @@ +name: Bruno Test Collection + +on: + workflow_dispatch: + +jobs: + bruno-tests: + name: Bruno test collection + runs-on: ubuntu-latest + steps: + - name: Bruno CLI runner + id: bru-cli + uses: krummbar/bruno-run-action@main # <1> + with: + path: bruno/api-it + env: integration + env-vars: |- # <2> + foo=bar + say='${{ secrets.MESSAGE }}' + output: output.json + output-format: json diff --git a/docs/index.adoc b/docs/index.adoc new file mode 100644 index 0000000..5f0dffa --- /dev/null +++ b/docs/index.adoc @@ -0,0 +1,36 @@ += Bruno Run Action +include::edit-advise.adoc[] +:source-highlighter: highlight.js +:toc: macro +:icons: font +ifdef::env-github[] +:tip-caption: :bulb: +:note-caption: :information_source: +:important-caption: :heavy_exclamation_mark: +:caution-caption: :fire: +:warning-caption: :warning: +endif::[] +:action-badge-ci: https://github.com/krummbar/bruno-run-action/actions/workflows/ci.yml/badge.svg +:action-badge-lint: https://github.com/krummbar/bruno-run-action/actions/workflows/linter.yml/badge.svg +:bru-cli-version: 1.x +:url-action-ci: https://github.com/krummbar/bruno-run-action/actions/workflows/ci.yml +:url-action-lint: https://github.com/krummbar/bruno-run-action/actions/workflows/linter.yml +:url-bruno-cli: https://docs.usebruno.com/bru-cli/overview +:url-bruno-npm: https://www.npmjs.com/package/@usebruno/cli + +image:{action-badge-ci}[Continuous Integration,link={url-action-ci}] +image:{action-badge-lint}[Lint Codebase,link={url-action-lint}] + +Containerized GitHub Action executing {url-bruno-cli}[bruno cli] runner. +Can be used to execute sanity checks and integration tests via bruno collections. +Internally executes {url-bruno-npm}[`@usebruno/cli@1.x`], passing the action's input parameters to the command line interface. + +toc::[] + +include::usage.adoc[leveloffset=1] + +== Development + +include::test-local.adoc[leveloffset=2] + +include::documentation.adoc[leveloffset=2] diff --git a/docs/test-local.adoc b/docs/test-local.adoc new file mode 100644 index 0000000..9263fe6 --- /dev/null +++ b/docs/test-local.adoc @@ -0,0 +1,47 @@ += Test Locally + +After you've cloned the repository to your local machine or codespace, you'll +need to perform some initial setup steps before you can test your action. + +[NOTE] +==== +You'll need to have a reasonably modern version of +https://www.docker.com/get-started/[Docker] handy (e.g. docker engine +version 20 or later). +==== + +. :hammer_and_wrench: Build the container ++ +[source,console] +---- +docker build -t bruno-run-action-local . +---- + +. :white_check_mark: Test the container ++ +You can pass individual environment variables using the `--env` or `-e` flag. ++ +[source,console] +---- +$ docker run --env INPUT_PATH=".github/bruno-collection" --env INPUT_ENV="cicd" --env BRUNO_ACTION_DRY_RUN="true" -v ${PWD}:/usr/src bruno-run-action-local +::notice::collection directory: '/usr/src/.github/bruno-collection' +::notice::bru run --env cicd +::notice::Executed in dry mode, skipped executing bruno collection +---- ++ +Or you can pass a file with environment variables using `--env-file`. ++ +[source,console] +---- +$ docker run --env-file .github/workflows/ci.env -v ${PWD}/.github:/usr/src/.github bruno-run-action-local +::notice::collection directory: '/usr/src/.github/bruno-collection' +::notice::bru run users/get-user.bru -r --env cicd --output /usr/src/output.html --format html --insecure --tests-only --bail --env-var apikey=myPassword --env-var id=myId +::notice::Executed in dry mode, skipped executing bruno collection +---- ++ +[TIP] +==== +If `BRUNO_ACTION_DRY_RUN=true` is provided, +execution of the actual collection is skipped. +Only the fully composed bru run command with all arguments is dumped. +==== diff --git a/docs/usage.adoc b/docs/usage.adoc new file mode 100644 index 0000000..7dc660d --- /dev/null +++ b/docs/usage.adoc @@ -0,0 +1,39 @@ += Usage + +The following example runs a collection located in the folder `bruno/api-it`. +It uses the bruno environment `integration`, +and provides the secret environment variables `foo=bar` and `say=hello`. +The runner results are dumped to the JSON file `./output.json`. + +.Run full collection with secrets +[source,yaml] +---- +include::examples/usage.yaml[] +---- +<1> Change `@main` to a specific commit SHA or version tag, e.g.: + +`krummbar/bruno-run-action@e76147da8e5c81eaf017dede5645551d4b94427b` + +`krummbar/bruno-run-action@v0.1.0` +<2> envVars need to be separated with line breaks + +.Run single request +[source,yaml] +---- +include::examples/usage-single-folder.yaml[] +---- +<1> Path to bruno collection +<2> Relative path of the request folder inside the collection +<3> Includes all requests in subfolders + +== Inputs + +[source,yaml] +---- +include::../action.yml[tag=inputs,indent=0] +---- + +== Outputs + +[source,yaml] +---- +include::../action.yml[tag=outputs,indent=0] +---- diff --git a/entrypoint.sh b/entrypoint.sh new file mode 100755 index 0000000..95fd85b --- /dev/null +++ b/entrypoint.sh @@ -0,0 +1,154 @@ +#!/bin/bash +dry_run="${BRUNO_ACTION_DRY_RUN}" + +function print_input { + echo "::debug::IN_CSV_FILEPATH='${IN_CSV_FILEPATH}'" + echo "::debug::IN_PATH='${IN_PATH}'" + echo "::debug::IN_FILENAME='${IN_FILENAME}'" + echo "::debug::IN_IGNORE_TRUSTSTORE='${IN_IGNORE_TRUSTSTORE}'" + echo "::debug::IN_RECURSIVE='${IN_RECURSIVE}'" + echo "::debug::IN_ENV='${IN_ENV}'" + echo "::debug::IN_ENV_VARS='${IN_ENV_VARS}'" + echo "::debug::IN_OUTPUT='${IN_OUTPUT}'" + echo "::debug::IN_OUTPUT_FORMAT='${IN_OUTPUT_FORMAT}'" + echo "::debug::IN_CA_CERT='${IN_CA_CERT}'" + echo "::debug::IN_INSECURE='${IN_INSECURE}'" + echo "::debug::IN_SANDBOX='${IN_SANDBOX}'" + echo "::debug::IN_TESTS_ONLY='${IN_TESTS_ONLY}'" + echo "::debug::IN_BAIL='${IN_BAIL}'" + echo "::debug::DRY_RUN='${dry_run}'" +} + +# Exit script with status code and message +# +# $1 - Exit code +# $2 - Message to be dumped before exiting +function exit_with { + prefix="::notice" + if [[ "${1}" != "0" ]]; then + prefix="::error" + fi + echo "${prefix}::$2" + exit "$1" +} + +# Takes absolute or relative paths and returns the absolute path value +# based on the current working directory. +# +# $1 - The path of the file that should be resolved as absolute path +# +# Examples (assuming pwd=/home/user/bruno-run-action) +# absolute_path "out/bruno.log" => /home/user/bruno-run-action/out/bruno.log +# absolute_path "/out/bruno.log" => /out/bruno.log +function absolute_path { + input_path="${1}" + origin_dir=$(pwd) + cd "$(dirname "${input_path}")" &>/dev/null || exit_with 1 "The directory of the provided path '${input_path}' does not exist." + out_abs_path=$(pwd) + out_filename=$(basename "${input_path}") + cd "${origin_dir}" || exit_with 1 "Something unexpected happened evaluating the absolute path of '${input_path}'." + echo "${out_abs_path}/${out_filename}" +} + +# Reads all action input parameters and converts them to bru CLI command arguments. +# +# Returns all CLI arguments as string +function parse_bru_args { + output_args="" + if [ -n "${IN_FILENAME}" ]; then + output_args="${IN_FILENAME}" + fi + + if [ -n "${IN_RECURSIVE}" ]; then + output_args="${output_args} -r" + fi + + if [ -n "${IN_ENV}" ]; then + output_args="${output_args} --env ${IN_ENV}" + fi + + if [ "${IN_IGNORE_TRUSTSTORE}" == "true" ]; then + output_args="${output_args} --ignore-truststore" + fi + + if [ -n "${IN_OUTPUT}" ]; then + output_args="${output_args} --output $(absolute_path "${IN_OUTPUT}")" + fi + + if [ -n "${IN_OUTPUT_FORMAT}" ]; then + output_args="${output_args} --format ${IN_OUTPUT_FORMAT}" + fi + + if [ -n "${IN_CA_CERT}" ]; then + output_args="${output_args} --cacert ${IN_CA_CERT}" + fi + + if [ "${IN_INSECURE}" == "true" ]; then + output_args="${output_args} --insecure" + fi + + if [ -n "${IN_SANDBOX}" ]; then + output_args="${output_args} --sandbox ${IN_SANDBOX}" + fi + + if [ "${IN_TESTS_ONLY}" == "true" ]; then + output_args="${output_args} --tests-only" + fi + + if [ "${IN_BAIL}" == "true" ]; then + output_args="${output_args} --bail" + fi + + if [ -n "${IN_CSV_FILEPATH}" ]; then + output_args="${output_args} --csv-file-path $(absolute_path "${IN_CSV_FILEPATH}")" + fi + + # Assign --env-var key=value if provided + # Key value pairs must be separated by line breaks + if [ -n "${IN_ENV_VARS}" ]; then + while read -r env_var; do + output_args="${output_args} --env-var ${env_var}" + done < <(echo -e "${IN_ENV_VARS}") + fi + echo "${output_args}" +} + +# Main function executing the bru CLI +# +# Exits with 0 if `bru run ...` was successful. +# Otherwise returns the exit code of the `bru run ...` command. +function main { + print_input + bru_version="$(bru --version)" + echo "::notice::bru version: ${bru_version}" + echo "bru-version='${bru_version}'" >>"${GITHUB_OUTPUT}" + bru_args="$(parse_bru_args)" + + # Change to provided working directory + if [ -n "${IN_PATH}" ]; then + cd "${IN_PATH}" || exit_with 1 "The provided bruno collection path '${IN_PATH}' does not exist." + fi + + # Dump current working directory + echo "::notice::collection directory: '$(pwd)'" + + # Only dump command if DRY_RUN is enabled and exit + if [ "${dry_run}" = true ]; then + echo "::notice::bru run ${bru_args}" + exit_with 0 "Executed in dry mode, skipped executing bruno collection." + fi + + # Execute 'bru run ...' and evaluate execution status + if eval "bru run ${bru_args}"; then + # Write outputs to the $GITHUB_OUTPUT file + echo "success=true" >>"${GITHUB_OUTPUT}" + exit_with 0 "bru run succeeded." + else + bru_exit_code=$? + # Write outputs to warning $GITHUB_OUTPUT file + echo "success=false" >>"${GITHUB_OUTPUT}" + exit_with ${bru_exit_code} "bru run failed failed with status: ${bru_exit_code}." + fi +} + +main