├── .github
├── CODEOWNERS
├── pull_request_template.md
└── workflows
│ ├── lint.yml
│ ├── release-please.yml
│ ├── test.yml
│ └── trunk-upgrade.yml
├── .gitignore
├── .jest
└── setupTests.ts
├── .trunk
├── .gitignore
├── configs
│ ├── .markdownlint.yaml
│ └── .yamllint.yaml
└── trunk.yaml
├── .vscode
└── launch.json
├── CHANGELOG.md
├── README.md
├── __tests__
├── index.test.ts
├── mockResults.ts
├── sample_coverage_multiple_uncovered_lines.txt
├── sample_coverage_output.txt
└── sample_test_output.txt
├── action.yml
├── assets
├── banner-pr-comment-example.png
├── opa-logo.png
├── readme-example-1.png
├── readme-example-2.png
├── readme-example-3.png
├── readme-test-results.png
└── test-file-structure-example.png
├── dist
├── index.js
├── index.js.map
└── sourcemap-register.js
├── examples
├── README.md
├── cancel-in-progress-runs.rego
├── do-not-delete-stateful-resources.rego
├── drift-detection.rego
├── enforce-module-use-policy.rego
├── enforce-password-length.rego
├── ignore-changes-outside-root.rego
├── notification-stack-failure-origins.rego
├── readers-writers-admins-teams.rego
├── tests
│ ├── cancel-in-progress-runs_test.rego
│ ├── do-not-delete-stateful-resources_test.rego
│ ├── enforce-module-use-policy_test.rego
│ ├── enforce-password-length_test.rego
│ ├── ignore-changes-outside-root_test.rego
│ ├── notification-stack-failure-origins_test.rego
│ ├── readers-writers-admins-teams_test.rego
│ └── track-using-labels_test.rego
└── track-using-labels.rego
├── package-lock.json
├── package.json
├── src
├── formatResults.ts
├── index.ts
├── interfaces.ts
├── opaCommands.ts
└── testResultProcessing.ts
└── tsconfig.json
/.github/CODEOWNERS:
--------------------------------------------------------------------------------
1 | # Use this file to define individuals or teams that are responsible for code in a repository.
2 | # Read more:
3 | #
4 | # Order is important: the last matching pattern takes the most precedence
5 |
6 | # These owners will be the default owners for everything
7 | * @masterpointio/masterpoint-open-source
8 | * @oycyc
9 |
--------------------------------------------------------------------------------
/.github/pull_request_template.md:
--------------------------------------------------------------------------------
1 | ## Describe your changes
2 |
3 | ## Reminder:
4 |
5 | - When the PR is ready, be sure to run `npm run build` to compile into the distribution `/dist` folder, which is the source code that the Action uses.
6 |
--------------------------------------------------------------------------------
/.github/workflows/lint.yml:
--------------------------------------------------------------------------------
1 | name: Lint
2 |
3 | concurrency:
4 | group: lint-${{ github.head_ref || github.run_id }}
5 | cancel-in-progress: true
6 |
7 | on: pull_request
8 |
9 | permissions:
10 | actions: read
11 | checks: write
12 | contents: read
13 | pull-requests: read
14 |
15 | jobs:
16 | lint:
17 | runs-on: ubuntu-latest
18 | steps:
19 | - name: Check out Git repository
20 | uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
21 | - name: Trunk Check
22 | uses: trunk-io/trunk-action@4d5ecc89b2691705fd08c747c78652d2fc806a94 # v1.1.19
23 |
24 | conventional-title:
25 | runs-on: ubuntu-latest
26 | steps:
27 | - uses: amannn/action-semantic-pull-request@0723387faaf9b38adef4775cd42cfd5155ed6017 # v5.5.3
28 | env:
29 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
30 |
--------------------------------------------------------------------------------
/.github/workflows/release-please.yml:
--------------------------------------------------------------------------------
1 | name: Create Release via Google Release Please
2 |
3 | on:
4 | push:
5 | branches:
6 | - main
7 |
8 | permissions:
9 | contents: write
10 | pull-requests: write
11 |
12 | jobs:
13 | release-please:
14 | runs-on: ubuntu-latest
15 | steps:
16 | - uses: googleapis/release-please-action@7987652d64b4581673a76e33ad5e98e3dd56832f # 4.1.3
17 | with:
18 | release-type: simple
19 |
--------------------------------------------------------------------------------
/.github/workflows/test.yml:
--------------------------------------------------------------------------------
1 | name: Tests
2 |
3 | on: pull_request
4 |
5 | permissions:
6 | contents: read
7 | checks: write
8 | pull-requests: write
9 |
10 | jobs:
11 | test-typescript:
12 | runs-on: ubuntu-latest
13 |
14 | steps:
15 | - uses: actions/checkout@v4
16 | - name: Setup Node
17 | uses: actions/setup-node@v4
18 | with:
19 | node-version: 22
20 | cache: npm
21 | - run: npm ci
22 | - run: npm run build --if-present
23 | - run: npm test
24 | - name: Jest Coverage Report
25 | uses: ArtiomTr/jest-coverage-report-action@c026e98ae079f4b0b027252c8e957f5ebd420610 # v2.3.0
26 |
27 | test-action-on-itself:
28 | name: Test the GitHub Action on itself
29 | runs-on: ubuntu-latest
30 |
31 | steps:
32 | - name: Checkout
33 | id: checkout
34 | uses: actions/checkout@v4
35 |
36 | - name: Test Local Action (Individual File Mode)
37 | id: test-action-opa-files
38 | uses: ./
39 | with:
40 | path: ./examples
41 | test_mode: file
42 | report_untested_files: true
43 | pr_comment_title: Below is the Action testing on itself with this PR's source code against policies in `/examples` file by file. Confirm it is as expected.
44 |
45 | - name: Test Local Action (Directory Package Mode)
46 | if: (success() || failure())
47 | id: test-action-opa-package
48 | uses: ./
49 | with:
50 | path: ./examples
51 | test_mode: directory
52 | report_untested_files: true
53 | pr_comment_title: Below is the Action testing on itself with this PR's source code against `/examples` entire package directory. Confirm it is as expected.
54 |
--------------------------------------------------------------------------------
/.github/workflows/trunk-upgrade.yml:
--------------------------------------------------------------------------------
1 | name: Trunk Upgrade
2 |
3 | on:
4 | schedule:
5 | # On the first day of every month @ 8am
6 | - cron: 0 8 1 * *
7 | workflow_dispatch: {}
8 |
9 | permissions: read-all
10 |
11 | jobs:
12 | trunk-upgrade:
13 | runs-on: ubuntu-latest
14 | permissions:
15 | # For trunk to create PRs
16 | contents: write
17 | pull-requests: write
18 | steps:
19 | - name: Checkout
20 | uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 #v4.1.7
21 |
22 | - name: Upgrade
23 | uses: trunk-io/trunk-action/upgrade@86b68ffae610a05105e90b1f52ad8c549ef482c2 #v1.1.16
24 | with:
25 | reviewers: "@masterpointio/masterpoint-internal"
26 | prefix: "chore: "
27 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules
2 |
--------------------------------------------------------------------------------
/.jest/setupTests.ts:
--------------------------------------------------------------------------------
1 | // Set up the environment for the tests
2 | // All these values can be empty because in the tests, we are testing the functions directly.
3 | process.env.test_result = "mock";
4 |
--------------------------------------------------------------------------------
/.trunk/.gitignore:
--------------------------------------------------------------------------------
1 | *out
2 | *logs
3 | *actions
4 | *notifications
5 | *tools
6 | plugins
7 | user_trunk.yaml
8 | user.yaml
9 | tmp
10 |
--------------------------------------------------------------------------------
/.trunk/configs/.markdownlint.yaml:
--------------------------------------------------------------------------------
1 | # Prettier friendly markdownlint config (all formatting rules disabled)
2 | extends: markdownlint/style/prettier
3 |
--------------------------------------------------------------------------------
/.trunk/configs/.yamllint.yaml:
--------------------------------------------------------------------------------
1 | rules:
2 | quoted-strings:
3 | required: only-when-needed
4 | extra-allowed: ["{|}"]
5 | key-duplicates: {}
6 | octal-values:
7 | forbid-implicit-octal: true
8 |
--------------------------------------------------------------------------------
/.trunk/trunk.yaml:
--------------------------------------------------------------------------------
1 | # This file controls the behavior of Trunk: https://docs.trunk.io/cli
2 | # To learn more about the format of this file, see https://docs.trunk.io/reference/trunk-yaml
3 | version: 0.1
4 | cli:
5 | version: 1.22.15
6 | # Trunk provides extensibility via plugins. (https://docs.trunk.io/plugins)
7 | plugins:
8 | sources:
9 | - id: trunk
10 | ref: v1.6.8
11 | uri: https://github.com/trunk-io/plugins
12 | # Many linters and tools depend on runtimes - configure them here. (https://docs.trunk.io/runtimes)
13 | runtimes:
14 | enabled:
15 | - node@18.20.5
16 | - python@3.10.8
17 | # This is the section where you manage your linters. (https://docs.trunk.io/check/configuration)
18 | lint:
19 | ignore:
20 | - linters: [ALL]
21 | paths:
22 | # Ignore linting the distribution packaged folder
23 | - dist/**
24 | - .github/pull_request_template.md
25 | enabled:
26 | - actionlint@1.7.7
27 | - checkov@3.2.413
28 | - git-diff-check
29 | - markdownlint@0.44.0
30 | - osv-scanner@2.0.2
31 | - oxipng@9.1.5
32 | - prettier@3.5.3
33 | - trivy@0.61.1
34 | - trufflehog@3.88.26
35 | - yamllint@1.37.0
36 | actions:
37 | enabled:
38 | - trunk-announce
39 | - trunk-check-pre-push
40 | - trunk-fmt-pre-commit
41 | - trunk-upgrade-available
42 |
--------------------------------------------------------------------------------
/.vscode/launch.json:
--------------------------------------------------------------------------------
1 | {
2 | "version": "1.0.0",
3 | "configurations": [
4 | {
5 | "name": "TS-Node",
6 | "type": "node",
7 | "request": "launch",
8 | "runtimeExecutable": "npx",
9 | "runtimeArgs": [
10 | "ts-node",
11 | "./src/index.ts"
12 | // "--transpile-only",
13 | // "--esm"
14 | ],
15 | // "program": "${file}",
16 | // "program": "${workspaceRoot}/src/index.ts",
17 | // "cwd": "${workspaceRoot}",
18 | "internalConsoleOptions": "openOnSessionStart",
19 | "skipFiles": ["/**", "node_modules/**"],
20 | "env": {
21 | "path": "./examples",
22 | "test_file_postfix": "_test",
23 | "test_mode": "directory"
24 | }
25 | }
26 | ]
27 | }
28 |
--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | # Changelog
2 |
3 | ## [2.0.0](https://github.com/masterpointio/github-action-opa-rego-test/compare/v1.2.0...v2.0.0) (2025-05-30)
4 |
5 |
6 | ### ⚠ BREAKING CHANGES
7 |
8 | * **ts:** use @actions/exec instead of bash script for OPA commands, allow testing entire directory vs file by file ([#27](https://github.com/masterpointio/github-action-opa-rego-test/issues/27))
9 |
10 | ### Bug Fixes
11 |
12 | * **ts:** coverage only if it passed ([#30](https://github.com/masterpointio/github-action-opa-rego-test/issues/30)) ([7e4e69c](https://github.com/masterpointio/github-action-opa-rego-test/commit/7e4e69cb57b96bc5aa401772a4fb3b7911251c77))
13 |
14 |
15 | ### Code Refactoring
16 |
17 | * **ts:** use @actions/exec instead of bash script for OPA commands, allow testing entire directory vs file by file ([#27](https://github.com/masterpointio/github-action-opa-rego-test/issues/27)) ([9eecaf7](https://github.com/masterpointio/github-action-opa-rego-test/commit/9eecaf74d47ca3976fee01bbcb5a5b724ebc2d0d))
18 |
19 | ## [1.2.0](https://github.com/masterpointio/github-action-opa-rego-test/compare/v1.1.0...v1.2.0) (2025-04-30)
20 |
21 |
22 | ### Features
23 |
24 | * allow to request static opa binary ([#24](https://github.com/masterpointio/github-action-opa-rego-test/issues/24)) ([1e51290](https://github.com/masterpointio/github-action-opa-rego-test/commit/1e51290cee35301008ad302400f434d8aa37565e))
25 |
26 | ## [1.1.0](https://github.com/masterpointio/github-action-opa-rego-test/compare/v1.0.0...v1.1.0) (2025-01-15)
27 |
28 |
29 | ### Features
30 |
31 | * **github-action:** add actionlint to lint against gha yamls ([#18](https://github.com/masterpointio/github-action-opa-rego-test/issues/18)) ([7a7b55d](https://github.com/masterpointio/github-action-opa-rego-test/commit/7a7b55dea277136e7c1f2f96d2c5870d2c5282d9))
32 |
33 |
34 | ### Bug Fixes
35 |
36 | * auto trunk upgrade action ([#17](https://github.com/masterpointio/github-action-opa-rego-test/issues/17)) ([491cd45](https://github.com/masterpointio/github-action-opa-rego-test/commit/491cd45379004718df5e7a6ea918998d0707f086))
37 | * **packagejson:** clean up ([#11](https://github.com/masterpointio/github-action-opa-rego-test/issues/11)) ([d11ba7e](https://github.com/masterpointio/github-action-opa-rego-test/commit/d11ba7e55b93c49396b44b9f8aaf862776e92161))
38 |
39 | ## 1.0.0 (2024-08-22)
40 |
41 |
42 | ### Features
43 |
44 | * allow different postfix and other inputs and documentation ([#2](https://github.com/masterpointio/github-action-opa-rego-test/issues/2)) ([0ea31f1](https://github.com/masterpointio/github-action-opa-rego-test/commit/0ea31f17b4596799906098c50b0ab5294e8065a0))
45 | * **codestyle:** linting and trunk ([#6](https://github.com/masterpointio/github-action-opa-rego-test/issues/6)) ([8f4578a](https://github.com/masterpointio/github-action-opa-rego-test/commit/8f4578a3cfa231fa7053402d29e88fb1edd3025b))
46 | * GHA for testing OPA Rego policies and generating a summary report ([#1](https://github.com/masterpointio/github-action-opa-rego-test/issues/1)) ([8cb09b7](https://github.com/masterpointio/github-action-opa-rego-test/commit/8cb09b77db55a086aaabc33654a09fe6bd2737f2))
47 | * outputs and release please ([#4](https://github.com/masterpointio/github-action-opa-rego-test/issues/4)) ([1be1316](https://github.com/masterpointio/github-action-opa-rego-test/commit/1be1316628dd30e39648c44f92dd26f33c61b8a4))
48 | * pin to sha + additional inputs ([#3](https://github.com/masterpointio/github-action-opa-rego-test/issues/3)) ([6236e8b](https://github.com/masterpointio/github-action-opa-rego-test/commit/6236e8b25c8fe69a7c792b9f99d182b758736285))
49 | * **report-message:** add `indicate_source_message` flag for reference to this repo ([#9](https://github.com/masterpointio/github-action-opa-rego-test/issues/9)) ([e5f6875](https://github.com/masterpointio/github-action-opa-rego-test/commit/e5f6875239ac9aa38c493fca0b7b484e7788f024))
50 | * **tests:** typescript optimizations and ci tests ([#8](https://github.com/masterpointio/github-action-opa-rego-test/issues/8)) ([d1c2ec7](https://github.com/masterpointio/github-action-opa-rego-test/commit/d1c2ec7568a3ab949ee57aecffb2a011d54c8d6f))
51 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | [](https://masterpoint.io)
2 |
3 | # GitHub Action for OPA Rego Policy Tests [](https://github.com/masterpointio/github-action-opa-rego-test/releases/latest)
4 |
5 | GitHub Action to automate testing for your OPA (Open Policy Agent) Rego policies, generates a report with coverage information, and posts the test results as a comment on your pull requests, making it easy for your team to review and approve policies.
6 |
7 | Use this to test your OPA Rego files for [Spacelift policies](https://docs.spacelift.io/concepts/policy), Kubernetes Admission Controller policies, Docker authorization policies, or any other use case that uses [Open Policy Agent's policy language Rego](https://www.openpolicyagent.org/docs/latest/). This Action also updates PR comments with the test results in place to prevent duplication.
8 |
9 |
10 |
11 |
12 |
13 | See examples of the pull request comments below at the [Example Pull Request Comments section](#-example-pull-request-comments).
14 |
15 | 📚 Table of Contents
16 |
17 | - [🚀 Usage](#-usage)
18 | - [Inputs](#inputs)
19 | - [⚙️ How It Works](#️-how-it-works)
20 | - [🧪 Running Tests](#-running-tests)
21 | - [🏗️ Setup & Run Locally](#️-setup--run-locally)
22 | - [📦 Releases / Packaging for Distribution](#-releases--packaging-for-distribution)
23 | - [🤝 Contributing](#-contributing)
24 | - [💬 Example Pull Request Comments](#-example-pull-request-comments)
25 |
26 | ## 🚀 Usage
27 |
28 | It's super easy to get started and use this GitHub Action to test your OPA Rego policies. In your repository/directory with the `.rego` files and the `_test.rego` files, simply checkout the repository and add the step with `uses: masterpointio/github-action-opa-rego-test@main`. It's as simple as adding the step with no required inputs! It will then generate a PR comment (that updates in place) with the test results!
29 |
30 | ```yaml
31 | - name: Run OPA Rego Tests
32 | uses: masterpointio/github-action-opa-rego-test@main
33 | with:
34 | path: ./examples
35 | test_mode: directory # Whether to test the Rego by directory (e.g. opa test ./) or by individual files (e.g. opa test a_test.rego a.rego). Options of `directory` or `file`.
36 | report_untested_files: true # Flag to check & report Rego files that does NOT have corresponding test files. Optional, defaults to false.
37 | ```
38 |
39 |
40 | Expand to see full usage example!
41 |
42 | ```yaml
43 | name: Spacelift Policy OPA Rego Tests
44 |
45 | on:
46 | pull_request:
47 | # Optionally only trigger tests on affecting .rego files.
48 | # paths:
49 | # - '**.rego'
50 |
51 | permissions:
52 | id-token: write
53 | contents: read
54 | pull-requests: write # required to comment on PRs
55 |
56 | jobs:
57 | opa-tests:
58 | runs-on: ubuntu-latest
59 | steps:
60 | - name: Check out repository code
61 | uses: actions/checkout@v4
62 |
63 | - name: Run OPA Rego Tests
64 | uses: masterpointio/github-action-opa-rego-test@main
65 | with:
66 | path: "./config/spacelift-policies" # Path of the directory where the OPA Rego policies are stored.
67 | report_untested_files: true # Flag to check & report Rego files without corresponding test files. Optional, defaults to false.
68 | ```
69 |
70 |
71 |
72 | Be sure to always append the postfix to your test files. The default input for the `test_file_postfix` is `_test`, per [OPA's best practices](https://www.openpolicyagent.org/docs/latest/policy-testing/#test-format). If you have a different postfix for your test files, you can specify it in the inputs. This is how GitHub Action know what test to run on files.
73 |
74 | For example, if you have a file named `my-policy.rego`, you would need a file named `my-policy_test.rego`. It does not matter where the `_test.rego` file is located, just that it is in the root path, meaning that it can be in a subdirectory.
75 |
76 | In the example below, all `_test.rego` files' location are valid and will be executed.
77 |
78 |
79 |
80 | ### Inputs
81 |
82 | | Input | Description | Required | Default |
83 | | ------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------- | --------------------------------- |
84 | | `path` | Path to the directory containing OPA Rego files to test | Yes | REQUIRED |
85 | | `test_mode` | Whether to test the Rego by an entire directory (including entire package, e.g. `opa test ./`) or by individual files (e.g. `opa test a_test.rego a.rego`). Options of `directory` or `file`. | No | `directory` |
86 | | `test_file_postfix` | Postfix of the test files to run (e.g. notification.rego <> notification_test.rego) | No | `_test` |
87 | | `write_pr_comment` | Flag to write a user-friendly PR comment with test results | No | `true` |
88 | | `pr_comment_title` | Title of the PR comment for test results | No | `🧪 OPA Rego Policy Test Results` |
89 | | `pr_comment_mode` | Mode that will be used to update comment. Options of upsert (update in place) or recreate. | No | `upsert` |
90 | | `run_coverage_report` | Flag to run OPA coverage tests and include in PR comment | No | `true` |
91 | | `report_untested_files` | Check & report Rego files without corresponding test files | No | `false` |
92 | | `opa_version` | Version of the OPA CLI to use. | No | `1.4.2` |
93 | | `opa_static` | Whether to use the static binary for OPA installation. use. | No | `false` |
94 | | `indicate_source_message` | Flag to comment the origins watermark (this repository) of the GitHub Action in the PR comment. | No | `true` |
95 |
96 | ### Outputs
97 |
98 | | Output | Description |
99 | | ---------------- | ---------------------------------------------------------------------- |
100 | | `parsed_results` | The parsed results after processing the tests and/or coverage report. |
101 | | `tests_failed` | A `true` or `false` flag indicating if any of the tests failed or not. |
102 |
103 | ## ⚙️ How It Works
104 |
105 | This GitHub Action automates the process of testing OPA (Open Policy Agent) Rego policies and generating coverage reports. Here's a breakdown of its operation:
106 |
107 | 1. Setup: The action begins by setting up OPA using the open-policy-agent/setup-opa@v2 action, ensuring the necessary tools are available.
108 | 2. Run OPA Tests: It executes `opa test` on all .rego files in the specified directory (default is the root directory). The test results are captured and stored as an output.
109 | 3. Run OPA Coverage Tests: Enabled by default but optional, the action performs coverage tests on each .rego file that has a corresponding \_test.rego file. This step identifies which parts of your policies are covered by tests.
110 | 4. Find Untested Files: Optionally if enabled, it can identify Rego files that don't have corresponding test files, helping you maintain comprehensive test coverage.
111 | 5. Parse and Format Results: A custom TypeScript script (index.ts) processes the raw test and coverage outputs. It parses the results into a structured format and generates a user-friendly summary.
112 | 6. Generate PR Comment: The formatted results are used to create or update a comment on the pull request.
113 | 7. Fail the Action if Tests Fail: If any tests fail, the action is marked as failed, which can be used to block PR merges or trigger other workflows.
114 |
115 | 
116 |
117 | ## 🧪 Running Tests
118 |
119 | On each pull request, there is a GitHub Actions workflow that runs the tests automatically, along with it testing itself by running the Action on itself against the `/examples` directory and commenting the OPA results on the same PR. To test locally, see below:
120 |
121 | 1. `npm install`
122 | 2. `npm run test`
123 |
124 |
125 |
126 | ## 🏗️ Setup & Run Locally
127 |
128 | You can use [nektos/act](https://github.com/nektos/act) to simulate and run a GitHub Actions workflow locally.
129 |
130 | To directly test the custom TypeScript action locally, you can:
131 |
132 | 1. `npm run install`
133 | 2. `node ./dist/index.js`
134 | This is assuming you have `npm` and `node` installed already. Note: You will have to manually provide the required inputs since this is directly executing the TypeScript code.
135 | Additionally, if you are using VS Code, you can use the `.vscode/launch.json` (which executes `npx ts-node ./src/index.ts`) to run and attach the debugger.
136 |
137 | ## 📦 Releases / Packaging for Distribution
138 |
139 | This Action executes the source from the `/dist` directory. It is generated using [@vercel/ncc](https://github.com/vercel/ncc) to easily compile the TypeScript module into a single file together with all its dependencies, gcc-style, to package it up for use and distribute.
140 |
141 | To package for distribution, simply run the command which will do the above and generate into the `/dist` directory (see the source in `package.json`):
142 |
143 | ```bash
144 | npm run build
145 | ```
146 |
147 | To create a new release, merge the pull request created by [Release Please](https://github.com/googleapis/release-please). This will automatically create a new release with the version number and the changes made.
148 |
149 | ## 🤝 Contributing
150 |
151 | Contributions are welcome! Please feel free to submit a Pull Request or open any issues you may have.
152 |
153 | ## 💬 Example Pull Request Comments
154 |
155 | One of the testing steps is running the test workflow against this Action itself. You can see some live examples in the closed PR section, including this [example here](https://github.com/masterpointio/github-action-opa-rego-test/pull/9#issuecomment-2305253112).
156 |
157 | - 
158 | - Using `report_untested_files` to indicate policies without corresponding tests.
159 | - 
160 | - 
161 |
--------------------------------------------------------------------------------
/__tests__/index.test.ts:
--------------------------------------------------------------------------------
1 | import {
2 | ProcessedTestResult,
3 | ProcessedCoverageResult,
4 | } from "../src/interfaces";
5 | import {
6 | mockProcessedTestResults,
7 | mockProcessedCoverageResults,
8 | } from "./mockResults";
9 | import {
10 | processTestResults,
11 | processCoverageReport,
12 | } from "../src/testResultProcessing";
13 | import { formatResults } from "../src/formatResults";
14 |
15 | import * as path from "path";
16 | import * as fs from "fs";
17 |
18 | // Obtained by running `opa test ./spacelift_policies --format=json --v0-compatible`
19 | export const testOutput = fs.readFileSync(
20 | path.join(__dirname, "sample_test_output.txt"),
21 | "utf8",
22 | );
23 |
24 | export const coverageOutput = fs.readFileSync(
25 | path.join(__dirname, "sample_coverage_output.txt"),
26 | "utf8",
27 | );
28 |
29 | export const multipleUncoveredLinesOutput = fs.readFileSync(
30 | path.join(__dirname, "sample_coverage_multiple_uncovered_lines.txt"),
31 | "utf8",
32 | );
33 |
34 | describe("processTestResults", () => {
35 | it("should correctly parse test output", () => {
36 | const result = processTestResults(JSON.parse(testOutput));
37 | expect(result).toHaveLength(8);
38 |
39 | expect(result[0]).toEqual({
40 | file: "tests/cancel-in-progress-runs_test.rego",
41 | status: "PASS",
42 | passed: 2,
43 | total: 2,
44 | details: expect.arrayContaining([
45 | "✅ test_cancel_runs_allowed",
46 | "✅ test_cancel_runs_denied",
47 | ]),
48 | });
49 | });
50 |
51 | it("should handle empty input", () => {
52 | const result = processTestResults([]);
53 | expect(result).toEqual([]);
54 | });
55 |
56 | it("should correctly parse failed tests", () => {
57 | const result = processTestResults(JSON.parse(testOutput));
58 | expect(result[2]).toEqual({
59 | file: "tests/enforce-module-use-policy_test.rego",
60 | status: "FAIL",
61 | passed: 3,
62 | total: 4,
63 | details: [
64 | "✅ test_deny_creation_of_controlled_resource_type",
65 | "✅ test_deny_update_of_controlled_resource_type",
66 | "❌ test_allow_deletion_of_controlled_resource_type",
67 | "✅ test_allow_creation_of_uncontrolled_resource_type",
68 | ],
69 | });
70 | });
71 | });
72 |
73 | describe("processCoverageReport", () => {
74 | const parsedCoverageResults = processCoverageReport(
75 | JSON.parse(coverageOutput),
76 | );
77 |
78 | it("should correctly parse coverage output - singular not covered lines", () => {
79 | const targetFile = "cancel-in-progress-runs.rego";
80 | const result = parsedCoverageResults.find(
81 | (item) => item.file === targetFile,
82 | );
83 |
84 | expect(result).toBeDefined();
85 | expect(result!.coverage).toBeCloseTo(83.33);
86 | expect(result!.notCoveredLines).toBe("16");
87 | });
88 |
89 | it("should correctly parse coverage output - multiple, hyphenated not covered lines", () => {
90 | const targetFile = "enforce-module-use-policy.rego";
91 | const result = parsedCoverageResults.find(
92 | (item) => item.file === targetFile,
93 | );
94 |
95 | expect(result).toBeDefined();
96 | expect(result!.coverage).toBeCloseTo(47.826);
97 | expect(result!.notCoveredLines).toBe(
98 | "37, 42, 46, 52, 54, 57, 60-61, 64, 68, 78, 80",
99 | );
100 | });
101 |
102 | it("should correctly parse coverage output - multiple, comma-separated not covered lines", () => {
103 | const targetFile = "readers-writers-admins-teams.rego";
104 | const result = parsedCoverageResults.find(
105 | (item) => item.file === targetFile,
106 | );
107 |
108 | expect(result).toBeDefined();
109 | expect(result!.coverage).toBeCloseTo(83.33);
110 | expect(result!.notCoveredLines).toBe("16, 24, 28");
111 | });
112 |
113 | it("should correctly parse coverage output - undefined coverage", () => {
114 | const targetFile = "drift-detection.rego";
115 | const result = parsedCoverageResults.find(
116 | (item) => item.file === targetFile,
117 | );
118 |
119 | expect(result).toBeDefined();
120 | expect(result!.coverage).toBeUndefined();
121 | expect(result!.notCoveredLines).toBe("3, 5, 8, 11");
122 | });
123 |
124 | it("should correctly parse coverage output - 100% coverage with empty not covered lines", () => {
125 | const targetFile = "tests/cancel-in-progress-runs_test.rego";
126 | const result = parsedCoverageResults.find(
127 | (item) => item.file === targetFile,
128 | );
129 |
130 | expect(result).toBeDefined();
131 | expect(result!.coverage).toBe(100);
132 | expect(result!.notCoveredLines).toBe("");
133 | });
134 | });
135 |
136 | describe("formatResults", () => {
137 | const parsedTestResults = mockProcessedTestResults;
138 | const parsedCoverageResults = mockProcessedCoverageResults;
139 |
140 | it("should correctly format results with coverage", () => {
141 | const testResults: ProcessedTestResult[] = [
142 | {
143 | file: "./examples/tests/ignore-changes-outside-root_test.rego",
144 | status: "PASS",
145 | passed: 12,
146 | total: 12,
147 | details: ["✅ test1", "✅ test2", "✅ test3"],
148 | },
149 | ];
150 | const specificCoverageResult = parsedCoverageResults.filter(
151 | (res) => res.file === "tests/ignore-changes-outside-root.rego",
152 | );
153 |
154 | const result = formatResults(testResults, specificCoverageResult, true);
155 | expect(result).toContain("# 🧪 OPA Rego Policy Test Results");
156 | expect(result).toContain(
157 | "| File | Status | Passed | Total | Coverage | Details |",
158 | );
159 | expect(result).toContain(
160 | "| ./examples/tests/ignore-changes-outside-root_test.rego | ✅ PASS | 12 | 12 | 97.44% | Show Details
✅ test1
✅ test2
✅ test3 |",
161 | );
162 | expect(result).toContain(
163 | "Show Details
✅ test1
✅ test2
✅ test3 ",
164 | );
165 | });
166 |
167 | it("should correctly format results without coverage and failed", () => {
168 | const testResults: ProcessedTestResult[] = [
169 | {
170 | file: "./examples/tests/ignore-changes-outside-root_test.rego",
171 | status: "FAIL",
172 | passed: 11,
173 | total: 12,
174 | details: ["✅ test1", "✅ test2", "❌ test3"],
175 | },
176 | ];
177 | const result = formatResults(testResults, [], false);
178 | expect(result).toContain("# 🧪 OPA Rego Policy Test Results");
179 | expect(result).toContain("| File | Status | Passed | Total | Details |");
180 | expect(result).toContain(
181 | "| ./examples/tests/ignore-changes-outside-root_test.rego | ❌ FAIL | 11 | 12 | Show Details
✅ test1
✅ test2
❌ test3 |",
182 | );
183 | });
184 |
185 | it("should handle all test statuses and coverage scenarios from parsed results", () => {
186 | const result = formatResults(
187 | mockProcessedTestResults,
188 | mockProcessedCoverageResults,
189 | true,
190 | );
191 |
192 | expect(result).toContain(
193 | "| tests/ignore-changes-outside-root_test.rego | ✅ PASS | 12 | 12 | 97.44% | Show Details
✅ test1
✅ test2
✅ test3
✅ test4
✅ test5
✅ test6
✅ test7
✅ test8
✅ test9
✅ test10
✅ test11
✅ test12 |",
194 | );
195 |
196 | expect(result).toContain(
197 | "| tests/track-using-labels_test.rego | ✅ PASS | 8 | 8 | 76.47% | Show Details
✅ test_label_match
✅ test_label_no_match
✅ test_no_labels_on_resource
✅ test_no_labels_on_constraint
✅ test_empty_labels_on_resource
✅ test_empty_labels_on_constraint
✅ test_multiple_labels_match
✅ test_multiple_labels_no_match |",
198 | );
199 |
200 | expect(result).toContain(
201 | "| tests/readers-writers-admins-teams_test.rego | ✅ PASS | 6 | 6 | 83.33% Uncovered Lines
16, 24, 28 | Show Details
✅ test_reader_access
✅ test_writer_access
✅ test_admin_access
✅ test_no_access
✅ test_multiple_roles
✅ test_nested_teams |",
202 | );
203 |
204 | expect(result).toContain(
205 | "| tests/cancel-in-progress-runs_test.rego | ✅ PASS | 2 | 2 | 83.33% Uncovered Lines
16 | Show Details
✅ test_cancel_successful
✅ test_cancel_failure |",
206 | );
207 |
208 | const fileNames = [
209 | "ignore-changes-outside-root_test.rego",
210 | "track-using-labels_test.rego",
211 | "enforce-password-length_test.rego",
212 | "notification-stack-failure-origins_test.rego",
213 | "enforce-module-use-policy_test.rego",
214 | "readers-writers-admins-teams_test.rego",
215 | "cancel-in-progress-runs_test.rego",
216 | "do-not-delete-stateful-resources_test.rego",
217 | ];
218 |
219 | fileNames.forEach((fileName) => {
220 | expect(result).toContain(`tests/${fileName}`);
221 | });
222 |
223 | const resultRows = result
224 | .split("\n")
225 | .filter((line) => line.startsWith("|") && line.includes("PASS"));
226 | expect(resultRows.length).toBe(
227 | parsedTestResults.filter((r) => r.status === "PASS").length,
228 | );
229 | });
230 |
231 | it("should format results without coverage when showCoverage is false", () => {
232 | const result = formatResults(
233 | parsedTestResults,
234 | parsedCoverageResults,
235 | false,
236 | );
237 | expect(result).not.toContain("Coverage");
238 | expect(result).toContain("| File | Status | Passed | Total | Details |");
239 | expect(result).toContain(
240 | "| tests/ignore-changes-outside-root_test.rego | ✅ PASS | 12 | 12 |",
241 | );
242 | });
243 |
244 | it("should handle 'NO TESTS' status", () => {
245 | const testResults: ProcessedTestResult[] = [
246 | {
247 | file: "./examples/no_test_file.rego",
248 | status: "NO TESTS",
249 | passed: 0,
250 | total: 0,
251 | details: [],
252 | },
253 | ];
254 | const result = formatResults(testResults, [], false);
255 | expect(result).toContain(
256 | "| ./examples/no_test_file.rego | ⚠️ NO TESTS | 0 | 0 | Show Details
No test file found |",
257 | );
258 | });
259 |
260 | it("should correctly match coverage info with test file", () => {
261 | const testResults: ProcessedTestResult[] = [
262 | {
263 | file: "./examples/tests/ignore-changes-outside-root_test.rego",
264 | status: "PASS",
265 | passed: 12,
266 | total: 12,
267 | details: ["✅ test1", "✅ test2", "✅ test3"],
268 | },
269 | ];
270 | const specificCoverageResult = parsedCoverageResults.filter(
271 | (res) => res.file === "tests/ignore-changes-outside-root.rego",
272 | );
273 | const result = formatResults(testResults, specificCoverageResult, true);
274 | expect(result).toContain(
275 | "| ./examples/tests/ignore-changes-outside-root_test.rego | ✅ PASS | 12 | 12 | 97.44% | Show Details
✅ test1
✅ test2
✅ test3 |",
276 | );
277 | });
278 |
279 | it("should handle cases where coverage info is not found", () => {
280 | const testResults: ProcessedTestResult[] = [
281 | {
282 | file: "./examples/tests/non-existent-file_test.rego",
283 | status: "PASS",
284 | passed: 1,
285 | total: 1,
286 | details: ["✅ test1"],
287 | },
288 | ];
289 | const result = formatResults(testResults, parsedCoverageResults, true);
290 | expect(result).toContain(
291 | "| ./examples/tests/non-existent-file_test.rego | ✅ PASS | 1 | 1 | N/A | Show Details
✅ test1 |",
292 | );
293 | });
294 | });
295 |
--------------------------------------------------------------------------------
/__tests__/mockResults.ts:
--------------------------------------------------------------------------------
1 | import {
2 | ProcessedTestResult,
3 | ProcessedCoverageResult,
4 | } from "../src/interfaces";
5 |
6 | export const mockProcessedTestResults: ProcessedTestResult[] = [
7 | {
8 | file: "tests/ignore-changes-outside-root_test.rego",
9 | status: "PASS",
10 | passed: 12,
11 | total: 12,
12 | details: [
13 | "✅ test1",
14 | "✅ test2",
15 | "✅ test3",
16 | "✅ test4",
17 | "✅ test5",
18 | "✅ test6",
19 | "✅ test7",
20 | "✅ test8",
21 | "✅ test9",
22 | "✅ test10",
23 | "✅ test11",
24 | "✅ test12",
25 | ],
26 | },
27 | {
28 | file: "tests/track-using-labels_test.rego",
29 | status: "PASS",
30 | passed: 8,
31 | total: 8,
32 | details: [
33 | "✅ test_label_match",
34 | "✅ test_label_no_match",
35 | "✅ test_no_labels_on_resource",
36 | "✅ test_no_labels_on_constraint",
37 | "✅ test_empty_labels_on_resource",
38 | "✅ test_empty_labels_on_constraint",
39 | "✅ test_multiple_labels_match",
40 | "✅ test_multiple_labels_no_match",
41 | ],
42 | },
43 | {
44 | file: "tests/enforce-password-length_test.rego",
45 | status: "PASS",
46 | passed: 3,
47 | total: 3,
48 | details: [
49 | "✅ test_password_too_short",
50 | "✅ test_password_just_right",
51 | "✅ test_password_too_long",
52 | ],
53 | },
54 | {
55 | file: "tests/notification-stack-failure-origins_test.rego",
56 | status: "PASS",
57 | passed: 5,
58 | total: 5,
59 | details: [
60 | "✅ test_stack_failure_origin_ami",
61 | "✅ test_stack_failure_origin_instance",
62 | "✅ test_stack_failure_origin_security_group",
63 | "✅ test_stack_failure_origin_vpc",
64 | "✅ test_stack_failure_origin_unknown",
65 | ],
66 | },
67 | {
68 | file: "tests/enforce-module-use-policy_test.rego",
69 | status: "PASS",
70 | passed: 4,
71 | total: 4,
72 | details: [
73 | "✅ test_valid_module_use",
74 | "✅ test_invalid_module_use",
75 | "✅ test_no_module_imports",
76 | "✅ test_mixed_module_use",
77 | ],
78 | },
79 | {
80 | file: "tests/readers-writers-admins-teams_test.rego",
81 | status: "PASS",
82 | passed: 6,
83 | total: 6,
84 | details: [
85 | "✅ test_reader_access",
86 | "✅ test_writer_access",
87 | "✅ test_admin_access",
88 | "✅ test_no_access",
89 | "✅ test_multiple_roles",
90 | "✅ test_nested_teams",
91 | ],
92 | },
93 | {
94 | file: "tests/cancel-in-progress-runs_test.rego",
95 | status: "PASS",
96 | passed: 2,
97 | total: 2,
98 | details: ["✅ test_cancel_successful", "✅ test_cancel_failure"],
99 | },
100 | {
101 | file: "tests/do-not-delete-stateful-resources_test.rego",
102 | status: "PASS",
103 | passed: 1,
104 | total: 1,
105 | details: ["✅ test_delete_stateful_resource"],
106 | },
107 | {
108 | file: "./examples/no_test_file.rego",
109 | status: "NO TESTS",
110 | passed: 0,
111 | total: 0,
112 | details: ["No test file found"],
113 | },
114 | {
115 | file: "./examples/tests/non-existent-file_test.rego",
116 | status: "PASS",
117 | passed: 1,
118 | total: 1,
119 | details: ["✅ test1"],
120 | },
121 | ];
122 |
123 | export const mockProcessedCoverageResults: ProcessedCoverageResult[] = [
124 | {
125 | file: "tests/ignore-changes-outside-root.rego",
126 | coverage: 97.44,
127 | notCoveredLines: "",
128 | },
129 | {
130 | file: "tests/track-using-labels.rego",
131 | coverage: 76.47,
132 | notCoveredLines: "",
133 | },
134 | {
135 | file: "tests/enforce-password-length.rego",
136 | coverage: 100.0,
137 | notCoveredLines: "",
138 | },
139 | {
140 | file: "tests/notification-stack-failure-origins.rego",
141 | coverage: 90.0,
142 | notCoveredLines: "10, 15",
143 | },
144 | {
145 | file: "tests/enforce-module-use-policy.rego",
146 | coverage: 85.0,
147 | notCoveredLines: "5",
148 | },
149 | {
150 | file: "tests/readers-writers-admins-teams.rego",
151 | coverage: 83.33,
152 | notCoveredLines: "16, 24, 28",
153 | },
154 | {
155 | file: "tests/cancel-in-progress-runs.rego",
156 | coverage: 83.33,
157 | notCoveredLines: "16",
158 | },
159 | {
160 | file: "tests/do-not-delete-stateful-resources.rego",
161 | coverage: 100.0,
162 | notCoveredLines: "",
163 | },
164 | ];
165 |
--------------------------------------------------------------------------------
/__tests__/sample_coverage_multiple_uncovered_lines.txt:
--------------------------------------------------------------------------------
1 | {
2 | "files": {
3 | "./examples/test.rego": {
4 | "covered": [],
5 | "not_covered": [
6 | {
7 | "start": {
8 | "row": 10
9 | },
10 | "end": {
11 | "row": 12
12 | }
13 | },
14 | {
15 | "start": {
16 | "row": 15
17 | },
18 | "end": {
19 | "row": 15
20 | }
21 | }
22 | ],
23 | "coverage": 80
24 | }
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/__tests__/sample_coverage_output.txt:
--------------------------------------------------------------------------------
1 | {
2 | "files": {
3 | "cancel-in-progress-runs.rego": {
4 | "covered": [
5 | {
6 | "start": {
7 | "row": 7
8 | },
9 | "end": {
10 | "row": 11
11 | }
12 | }
13 | ],
14 | "not_covered": [
15 | {
16 | "start": {
17 | "row": 16
18 | },
19 | "end": {
20 | "row": 16
21 | }
22 | }
23 | ],
24 | "covered_lines": 5,
25 | "not_covered_lines": 1,
26 | "coverage": 83.33333333333333
27 | },
28 | "do-not-delete-stateful-resources.rego": {
29 | "covered": [
30 | {
31 | "start": {
32 | "row": 10
33 | },
34 | "end": {
35 | "row": 10
36 | }
37 | },
38 | {
39 | "start": {
40 | "row": 12
41 | },
42 | "end": {
43 | "row": 12
44 | }
45 | },
46 | {
47 | "start": {
48 | "row": 15
49 | },
50 | "end": {
51 | "row": 15
52 | }
53 | },
54 | {
55 | "start": {
56 | "row": 18
57 | },
58 | "end": {
59 | "row": 18
60 | }
61 | },
62 | {
63 | "start": {
64 | "row": 26
65 | },
66 | "end": {
67 | "row": 26
68 | }
69 | },
70 | {
71 | "start": {
72 | "row": 29
73 | },
74 | "end": {
75 | "row": 29
76 | }
77 | }
78 | ],
79 | "not_covered": [
80 | {
81 | "start": {
82 | "row": 34
83 | },
84 | "end": {
85 | "row": 34
86 | }
87 | }
88 | ],
89 | "covered_lines": 6,
90 | "not_covered_lines": 1,
91 | "coverage": 85.71428571428571
92 | },
93 | "drift-detection.rego": {
94 | "not_covered": [
95 | {
96 | "start": {
97 | "row": 3
98 | },
99 | "end": {
100 | "row": 3
101 | }
102 | },
103 | {
104 | "start": {
105 | "row": 5
106 | },
107 | "end": {
108 | "row": 5
109 | }
110 | },
111 | {
112 | "start": {
113 | "row": 8
114 | },
115 | "end": {
116 | "row": 8
117 | }
118 | },
119 | {
120 | "start": {
121 | "row": 11
122 | },
123 | "end": {
124 | "row": 11
125 | }
126 | }
127 | ],
128 | "not_covered_lines": 4
129 | },
130 | "enforce-module-use-policy.rego": {
131 | "covered": [
132 | {
133 | "start": {
134 | "row": 17
135 | },
136 | "end": {
137 | "row": 17
138 | }
139 | },
140 | {
141 | "start": {
142 | "row": 24
143 | },
144 | "end": {
145 | "row": 30
146 | }
147 | },
148 | {
149 | "start": {
150 | "row": 32
151 | },
152 | "end": {
153 | "row": 32
154 | }
155 | },
156 | {
157 | "start": {
158 | "row": 39
159 | },
160 | "end": {
161 | "row": 39
162 | }
163 | },
164 | {
165 | "start": {
166 | "row": 79
167 | },
168 | "end": {
169 | "row": 79
170 | }
171 | }
172 | ],
173 | "not_covered": [
174 | {
175 | "start": {
176 | "row": 37
177 | },
178 | "end": {
179 | "row": 37
180 | }
181 | },
182 | {
183 | "start": {
184 | "row": 42
185 | },
186 | "end": {
187 | "row": 42
188 | }
189 | },
190 | {
191 | "start": {
192 | "row": 46
193 | },
194 | "end": {
195 | "row": 46
196 | }
197 | },
198 | {
199 | "start": {
200 | "row": 52
201 | },
202 | "end": {
203 | "row": 52
204 | }
205 | },
206 | {
207 | "start": {
208 | "row": 54
209 | },
210 | "end": {
211 | "row": 54
212 | }
213 | },
214 | {
215 | "start": {
216 | "row": 57
217 | },
218 | "end": {
219 | "row": 57
220 | }
221 | },
222 | {
223 | "start": {
224 | "row": 60
225 | },
226 | "end": {
227 | "row": 61
228 | }
229 | },
230 | {
231 | "start": {
232 | "row": 64
233 | },
234 | "end": {
235 | "row": 64
236 | }
237 | },
238 | {
239 | "start": {
240 | "row": 68
241 | },
242 | "end": {
243 | "row": 68
244 | }
245 | },
246 | {
247 | "start": {
248 | "row": 78
249 | },
250 | "end": {
251 | "row": 78
252 | }
253 | },
254 | {
255 | "start": {
256 | "row": 80
257 | },
258 | "end": {
259 | "row": 80
260 | }
261 | }
262 | ],
263 | "covered_lines": 11,
264 | "not_covered_lines": 12,
265 | "coverage": 47.82608695652174
266 | },
267 | "enforce-password-length.rego": {
268 | "covered": [
269 | {
270 | "start": {
271 | "row": 11
272 | },
273 | "end": {
274 | "row": 13
275 | }
276 | },
277 | {
278 | "start": {
279 | "row": 16
280 | },
281 | "end": {
282 | "row": 18
283 | }
284 | },
285 | {
286 | "start": {
287 | "row": 21
288 | },
289 | "end": {
290 | "row": 24
291 | }
292 | }
293 | ],
294 | "not_covered": [
295 | {
296 | "start": {
297 | "row": 29
298 | },
299 | "end": {
300 | "row": 29
301 | }
302 | }
303 | ],
304 | "covered_lines": 10,
305 | "not_covered_lines": 1,
306 | "coverage": 90.9090909090909
307 | },
308 | "ignore-changes-outside-root.rego": {
309 | "covered": [
310 | {
311 | "start": {
312 | "row": 13
313 | },
314 | "end": {
315 | "row": 15
316 | }
317 | },
318 | {
319 | "start": {
320 | "row": 18
321 | },
322 | "end": {
323 | "row": 19
324 | }
325 | },
326 | {
327 | "start": {
328 | "row": 22
329 | },
330 | "end": {
331 | "row": 23
332 | }
333 | },
334 | {
335 | "start": {
336 | "row": 26
337 | },
338 | "end": {
339 | "row": 27
340 | }
341 | },
342 | {
343 | "start": {
344 | "row": 33
345 | },
346 | "end": {
347 | "row": 34
348 | }
349 | },
350 | {
351 | "start": {
352 | "row": 36
353 | },
354 | "end": {
355 | "row": 37
356 | }
357 | }
358 | ],
359 | "not_covered": [
360 | {
361 | "start": {
362 | "row": 42
363 | },
364 | "end": {
365 | "row": 42
366 | }
367 | }
368 | ],
369 | "covered_lines": 13,
370 | "not_covered_lines": 1,
371 | "coverage": 92.85714285714286
372 | },
373 | "notification-stack-failure-origins.rego": {
374 | "covered": [
375 | {
376 | "start": {
377 | "row": 8
378 | },
379 | "end": {
380 | "row": 8
381 | }
382 | },
383 | {
384 | "start": {
385 | "row": 10
386 | },
387 | "end": {
388 | "row": 10
389 | }
390 | },
391 | {
392 | "start": {
393 | "row": 15
394 | },
395 | "end": {
396 | "row": 15
397 | }
398 | },
399 | {
400 | "start": {
401 | "row": 20
402 | },
403 | "end": {
404 | "row": 20
405 | }
406 | },
407 | {
408 | "start": {
409 | "row": 23
410 | },
411 | "end": {
412 | "row": 25
413 | }
414 | },
415 | {
416 | "start": {
417 | "row": 28
418 | },
419 | "end": {
420 | "row": 30
421 | }
422 | },
423 | {
424 | "start": {
425 | "row": 35
426 | },
427 | "end": {
428 | "row": 35
429 | }
430 | },
431 | {
432 | "start": {
433 | "row": 37
434 | },
435 | "end": {
436 | "row": 38
437 | }
438 | },
439 | {
440 | "start": {
441 | "row": 45
442 | },
443 | "end": {
444 | "row": 45
445 | }
446 | },
447 | {
448 | "start": {
449 | "row": 47
450 | },
451 | "end": {
452 | "row": 50
453 | }
454 | },
455 | {
456 | "start": {
457 | "row": 54
458 | },
459 | "end": {
460 | "row": 55
461 | }
462 | },
463 | {
464 | "start": {
465 | "row": 60
466 | },
467 | "end": {
468 | "row": 63
469 | }
470 | },
471 | {
472 | "start": {
473 | "row": 69
474 | },
475 | "end": {
476 | "row": 71
477 | }
478 | },
479 | {
480 | "start": {
481 | "row": 76
482 | },
483 | "end": {
484 | "row": 77
485 | }
486 | }
487 | ],
488 | "not_covered": [
489 | {
490 | "start": {
491 | "row": 80
492 | },
493 | "end": {
494 | "row": 80
495 | }
496 | }
497 | ],
498 | "covered_lines": 29,
499 | "not_covered_lines": 1,
500 | "coverage": 96.66666666666667
501 | },
502 | "readers-writers-admins-teams.rego": {
503 | "covered": [
504 | {
505 | "start": {
506 | "row": 8
507 | },
508 | "end": {
509 | "row": 8
510 | }
511 | },
512 | {
513 | "start": {
514 | "row": 10
515 | },
516 | "end": {
517 | "row": 10
518 | }
519 | },
520 | {
521 | "start": {
522 | "row": 12
523 | },
524 | "end": {
525 | "row": 12
526 | }
527 | },
528 | {
529 | "start": {
530 | "row": 17
531 | },
532 | "end": {
533 | "row": 19
534 | }
535 | },
536 | {
537 | "start": {
538 | "row": 25
539 | },
540 | "end": {
541 | "row": 27
542 | }
543 | },
544 | {
545 | "start": {
546 | "row": 33
547 | },
548 | "end": {
549 | "row": 38
550 | }
551 | }
552 | ],
553 | "not_covered": [
554 | {
555 | "start": {
556 | "row": 16
557 | },
558 | "end": {
559 | "row": 16
560 | }
561 | },
562 | {
563 | "start": {
564 | "row": 24
565 | },
566 | "end": {
567 | "row": 24
568 | }
569 | },
570 | {
571 | "start": {
572 | "row": 28
573 | },
574 | "end": {
575 | "row": 28
576 | }
577 | }
578 | ],
579 | "covered_lines": 15,
580 | "not_covered_lines": 3,
581 | "coverage": 83.33333333333333
582 | },
583 | "tests/cancel-in-progress-runs_test.rego": {
584 | "covered": [
585 | {
586 | "start": {
587 | "row": 3
588 | },
589 | "end": {
590 | "row": 4
591 | }
592 | },
593 | {
594 | "start": {
595 | "row": 15
596 | },
597 | "end": {
598 | "row": 16
599 | }
600 | }
601 | ],
602 | "covered_lines": 4,
603 | "coverage": 100
604 | },
605 | "tests/do-not-delete-stateful-resources_test.rego": {
606 | "covered": [
607 | {
608 | "start": {
609 | "row": 4
610 | },
611 | "end": {
612 | "row": 4
613 | }
614 | },
615 | {
616 | "start": {
617 | "row": 6
618 | },
619 | "end": {
620 | "row": 6
621 | }
622 | },
623 | {
624 | "start": {
625 | "row": 14
626 | },
627 | "end": {
628 | "row": 14
629 | }
630 | },
631 | {
632 | "start": {
633 | "row": 16
634 | },
635 | "end": {
636 | "row": 16
637 | }
638 | },
639 | {
640 | "start": {
641 | "row": 24
642 | },
643 | "end": {
644 | "row": 24
645 | }
646 | },
647 | {
648 | "start": {
649 | "row": 26
650 | },
651 | "end": {
652 | "row": 26
653 | }
654 | },
655 | {
656 | "start": {
657 | "row": 34
658 | },
659 | "end": {
660 | "row": 34
661 | }
662 | },
663 | {
664 | "start": {
665 | "row": 36
666 | },
667 | "end": {
668 | "row": 36
669 | }
670 | },
671 | {
672 | "start": {
673 | "row": 44
674 | },
675 | "end": {
676 | "row": 44
677 | }
678 | },
679 | {
680 | "start": {
681 | "row": 46
682 | },
683 | "end": {
684 | "row": 46
685 | }
686 | }
687 | ],
688 | "covered_lines": 10,
689 | "coverage": 100
690 | },
691 | "tests/enforce-module-use-policy_test.rego": {
692 | "covered": [
693 | {
694 | "start": {
695 | "row": 4
696 | },
697 | "end": {
698 | "row": 5
699 | }
700 | },
701 | {
702 | "start": {
703 | "row": 13
704 | },
705 | "end": {
706 | "row": 14
707 | }
708 | },
709 | {
710 | "start": {
711 | "row": 22
712 | },
713 | "end": {
714 | "row": 23
715 | }
716 | }
717 | ],
718 | "covered_lines": 6,
719 | "coverage": 100
720 | },
721 | "tests/enforce-password-length_test.rego": {
722 | "covered": [
723 | {
724 | "start": {
725 | "row": 4
726 | },
727 | "end": {
728 | "row": 5
729 | }
730 | },
731 | {
732 | "start": {
733 | "row": 16
734 | },
735 | "end": {
736 | "row": 17
737 | }
738 | },
739 | {
740 | "start": {
741 | "row": 28
742 | },
743 | "end": {
744 | "row": 29
745 | }
746 | }
747 | ],
748 | "covered_lines": 6,
749 | "coverage": 100
750 | },
751 | "tests/ignore-changes-outside-root_test.rego": {
752 | "covered": [
753 | {
754 | "start": {
755 | "row": 3
756 | },
757 | "end": {
758 | "row": 4
759 | }
760 | },
761 | {
762 | "start": {
763 | "row": 10
764 | },
765 | "end": {
766 | "row": 11
767 | }
768 | },
769 | {
770 | "start": {
771 | "row": 17
772 | },
773 | "end": {
774 | "row": 18
775 | }
776 | },
777 | {
778 | "start": {
779 | "row": 24
780 | },
781 | "end": {
782 | "row": 25
783 | }
784 | },
785 | {
786 | "start": {
787 | "row": 31
788 | },
789 | "end": {
790 | "row": 32
791 | }
792 | },
793 | {
794 | "start": {
795 | "row": 35
796 | },
797 | "end": {
798 | "row": 36
799 | }
800 | },
801 | {
802 | "start": {
803 | "row": 39
804 | },
805 | "end": {
806 | "row": 40
807 | }
808 | },
809 | {
810 | "start": {
811 | "row": 44
812 | },
813 | "end": {
814 | "row": 45
815 | }
816 | },
817 | {
818 | "start": {
819 | "row": 48
820 | },
821 | "end": {
822 | "row": 49
823 | }
824 | },
825 | {
826 | "start": {
827 | "row": 52
828 | },
829 | "end": {
830 | "row": 52
831 | }
832 | },
833 | {
834 | "start": {
835 | "row": 57
836 | },
837 | "end": {
838 | "row": 58
839 | }
840 | },
841 | {
842 | "start": {
843 | "row": 61
844 | },
845 | "end": {
846 | "row": 62
847 | }
848 | },
849 | {
850 | "start": {
851 | "row": 65
852 | },
853 | "end": {
854 | "row": 66
855 | }
856 | }
857 | ],
858 | "covered_lines": 25,
859 | "coverage": 100
860 | },
861 | "tests/notification-stack-failure-origins_test.rego": {
862 | "covered": [
863 | {
864 | "start": {
865 | "row": 11
866 | },
867 | "end": {
868 | "row": 12
869 | }
870 | },
871 | {
872 | "start": {
873 | "row": 29
874 | },
875 | "end": {
876 | "row": 34
877 | }
878 | },
879 | {
880 | "start": {
881 | "row": 38
882 | },
883 | "end": {
884 | "row": 39
885 | }
886 | },
887 | {
888 | "start": {
889 | "row": 44
890 | },
891 | "end": {
892 | "row": 44
893 | }
894 | },
895 | {
896 | "start": {
897 | "row": 48
898 | },
899 | "end": {
900 | "row": 49
901 | }
902 | },
903 | {
904 | "start": {
905 | "row": 54
906 | },
907 | "end": {
908 | "row": 54
909 | }
910 | },
911 | {
912 | "start": {
913 | "row": 58
914 | },
915 | "end": {
916 | "row": 59
917 | }
918 | },
919 | {
920 | "start": {
921 | "row": 76
922 | },
923 | "end": {
924 | "row": 78
925 | }
926 | },
927 | {
928 | "start": {
929 | "row": 82
930 | },
931 | "end": {
932 | "row": 83
933 | }
934 | },
935 | {
936 | "start": {
937 | "row": 100
938 | },
939 | "end": {
940 | "row": 107
941 | }
942 | },
943 | {
944 | "start": {
945 | "row": 111
946 | },
947 | "end": {
948 | "row": 112
949 | }
950 | },
951 | {
952 | "start": {
953 | "row": 117
954 | },
955 | "end": {
956 | "row": 117
957 | }
958 | },
959 | {
960 | "start": {
961 | "row": 121
962 | },
963 | "end": {
964 | "row": 122
965 | }
966 | },
967 | {
968 | "start": {
969 | "row": 127
970 | },
971 | "end": {
972 | "row": 127
973 | }
974 | }
975 | ],
976 | "covered_lines": 35,
977 | "coverage": 100
978 | },
979 | "tests/readers-writers-admins-teams_test.rego": {
980 | "covered": [
981 | {
982 | "start": {
983 | "row": 9
984 | },
985 | "end": {
986 | "row": 10
987 | }
988 | },
989 | {
990 | "start": {
991 | "row": 13
992 | },
993 | "end": {
994 | "row": 14
995 | }
996 | },
997 | {
998 | "start": {
999 | "row": 17
1000 | },
1001 | "end": {
1002 | "row": 18
1003 | }
1004 | },
1005 | {
1006 | "start": {
1007 | "row": 22
1008 | },
1009 | "end": {
1010 | "row": 23
1011 | }
1012 | },
1013 | {
1014 | "start": {
1015 | "row": 27
1016 | },
1017 | "end": {
1018 | "row": 28
1019 | }
1020 | },
1021 | {
1022 | "start": {
1023 | "row": 32
1024 | },
1025 | "end": {
1026 | "row": 33
1027 | }
1028 | }
1029 | ],
1030 | "covered_lines": 12,
1031 | "coverage": 100
1032 | },
1033 | "tests/track-using-labels_test.rego": {
1034 | "covered": [
1035 | {
1036 | "start": {
1037 | "row": 4
1038 | },
1039 | "end": {
1040 | "row": 4
1041 | }
1042 | },
1043 | {
1044 | "start": {
1045 | "row": 6
1046 | },
1047 | "end": {
1048 | "row": 6
1049 | }
1050 | },
1051 | {
1052 | "start": {
1053 | "row": 8
1054 | },
1055 | "end": {
1056 | "row": 8
1057 | }
1058 | },
1059 | {
1060 | "start": {
1061 | "row": 13
1062 | },
1063 | "end": {
1064 | "row": 14
1065 | }
1066 | },
1067 | {
1068 | "start": {
1069 | "row": 21
1070 | },
1071 | "end": {
1072 | "row": 22
1073 | }
1074 | },
1075 | {
1076 | "start": {
1077 | "row": 29
1078 | },
1079 | "end": {
1080 | "row": 30
1081 | }
1082 | },
1083 | {
1084 | "start": {
1085 | "row": 37
1086 | },
1087 | "end": {
1088 | "row": 40
1089 | }
1090 | },
1091 | {
1092 | "start": {
1093 | "row": 45
1094 | },
1095 | "end": {
1096 | "row": 48
1097 | }
1098 | },
1099 | {
1100 | "start": {
1101 | "row": 53
1102 | },
1103 | "end": {
1104 | "row": 56
1105 | }
1106 | },
1107 | {
1108 | "start": {
1109 | "row": 61
1110 | },
1111 | "end": {
1112 | "row": 64
1113 | }
1114 | },
1115 | {
1116 | "start": {
1117 | "row": 69
1118 | },
1119 | "end": {
1120 | "row": 72
1121 | }
1122 | }
1123 | ],
1124 | "covered_lines": 29,
1125 | "coverage": 100
1126 | },
1127 | "track-using-labels.rego": {
1128 | "covered": [
1129 | {
1130 | "start": {
1131 | "row": 4
1132 | },
1133 | "end": {
1134 | "row": 5
1135 | }
1136 | },
1137 | {
1138 | "start": {
1139 | "row": 8
1140 | },
1141 | "end": {
1142 | "row": 9
1143 | }
1144 | },
1145 | {
1146 | "start": {
1147 | "row": 13
1148 | },
1149 | "end": {
1150 | "row": 13
1151 | }
1152 | },
1153 | {
1154 | "start": {
1155 | "row": 17
1156 | },
1157 | "end": {
1158 | "row": 20
1159 | }
1160 | },
1161 | {
1162 | "start": {
1163 | "row": 23
1164 | },
1165 | "end": {
1166 | "row": 26
1167 | }
1168 | },
1169 | {
1170 | "start": {
1171 | "row": 29
1172 | },
1173 | "end": {
1174 | "row": 29
1175 | }
1176 | },
1177 | {
1178 | "start": {
1179 | "row": 31
1180 | },
1181 | "end": {
1182 | "row": 32
1183 | }
1184 | },
1185 | {
1186 | "start": {
1187 | "row": 35
1188 | },
1189 | "end": {
1190 | "row": 35
1191 | }
1192 | },
1193 | {
1194 | "start": {
1195 | "row": 37
1196 | },
1197 | "end": {
1198 | "row": 38
1199 | }
1200 | }
1201 | ],
1202 | "not_covered": [
1203 | {
1204 | "start": {
1205 | "row": 3
1206 | },
1207 | "end": {
1208 | "row": 3
1209 | }
1210 | },
1211 | {
1212 | "start": {
1213 | "row": 12
1214 | },
1215 | "end": {
1216 | "row": 12
1217 | }
1218 | },
1219 | {
1220 | "start": {
1221 | "row": 41
1222 | },
1223 | "end": {
1224 | "row": 41
1225 | }
1226 | }
1227 | ],
1228 | "covered_lines": 19,
1229 | "not_covered_lines": 3,
1230 | "coverage": 86.36363636363636
1231 | }
1232 | },
1233 | "covered_lines": 235,
1234 | "not_covered_lines": 27,
1235 | "coverage": 89.69465648854961
1236 | }
1237 |
--------------------------------------------------------------------------------
/__tests__/sample_test_output.txt:
--------------------------------------------------------------------------------
1 | [
2 | {
3 | "location": {
4 | "file": "tests/cancel-in-progress-runs_test.rego",
5 | "row": 3,
6 | "col": 1
7 | },
8 | "package": "data.spacelift",
9 | "name": "test_cancel_runs_allowed",
10 | "duration": 255417
11 | },
12 | {
13 | "location": {
14 | "file": "tests/cancel-in-progress-runs_test.rego",
15 | "row": 15,
16 | "col": 1
17 | },
18 | "package": "data.spacelift",
19 | "name": "test_cancel_runs_denied",
20 | "duration": 162583
21 | },
22 | {
23 | "location": {
24 | "file": "tests/do-not-delete-stateful-resources_test.rego",
25 | "row": 4,
26 | "col": 1
27 | },
28 | "package": "data.spacelift",
29 | "name": "test_deny_s3_bucket_deletion",
30 | "duration": 619459
31 | },
32 | {
33 | "location": {
34 | "file": "tests/do-not-delete-stateful-resources_test.rego",
35 | "row": 14,
36 | "col": 1
37 | },
38 | "package": "data.spacelift",
39 | "name": "test_deny_db_instance_deletion",
40 | "duration": 163791
41 | },
42 | {
43 | "location": {
44 | "file": "tests/do-not-delete-stateful-resources_test.rego",
45 | "row": 24,
46 | "col": 1
47 | },
48 | "package": "data.spacelift",
49 | "name": "test_deny_efs_file_system_deletion",
50 | "duration": 177250
51 | },
52 | {
53 | "location": {
54 | "file": "tests/do-not-delete-stateful-resources_test.rego",
55 | "row": 34,
56 | "col": 1
57 | },
58 | "package": "data.spacelift",
59 | "name": "test_deny_dynamodb_table_deletion",
60 | "duration": 152166
61 | },
62 | {
63 | "location": {
64 | "file": "tests/do-not-delete-stateful-resources_test.rego",
65 | "row": 44,
66 | "col": 1
67 | },
68 | "package": "data.spacelift",
69 | "name": "test_allow_instance_deletion",
70 | "duration": 138917
71 | },
72 | {
73 | "location": {
74 | "file": "tests/enforce-module-use-policy_test.rego",
75 | "row": 4,
76 | "col": 1
77 | },
78 | "package": "data.spacelift",
79 | "name": "test_deny_creation_of_controlled_resource_type",
80 | "duration": 215291
81 | },
82 | {
83 | "location": {
84 | "file": "tests/enforce-module-use-policy_test.rego",
85 | "row": 13,
86 | "col": 1
87 | },
88 | "package": "data.spacelift",
89 | "name": "test_deny_update_of_controlled_resource_type",
90 | "duration": 208042
91 | },
92 | {
93 | "location": {
94 | "file": "tests/enforce-module-use-policy_test.rego",
95 | "row": 22,
96 | "col": 1
97 | },
98 | "package": "data.spacelift",
99 | "name": "test_allow_deletion_of_controlled_resource_type",
100 | "fail": true,
101 | "duration": 147958
102 | },
103 | {
104 | "location": {
105 | "file": "tests/enforce-module-use-policy_test.rego",
106 | "row": 31,
107 | "col": 1
108 | },
109 | "package": "data.spacelift",
110 | "name": "test_allow_creation_of_uncontrolled_resource_type",
111 | "duration": 131542
112 | },
113 | {
114 | "location": {
115 | "file": "tests/enforce-password-length_test.rego",
116 | "row": 4,
117 | "col": 1
118 | },
119 | "package": "data.spacelift",
120 | "name": "test_deny_creation_of_password_with_less_than_16_characters",
121 | "duration": 172208
122 | },
123 | {
124 | "location": {
125 | "file": "tests/enforce-password-length_test.rego",
126 | "row": 16,
127 | "col": 1
128 | },
129 | "package": "data.spacelift",
130 | "name": "test_warn_creation_of_password_between_16_and_20_characters",
131 | "duration": 89042
132 | },
133 | {
134 | "location": {
135 | "file": "tests/enforce-password-length_test.rego",
136 | "row": 28,
137 | "col": 1
138 | },
139 | "package": "data.spacelift",
140 | "name": "test_allow_creation_of_password_longer_than_20_characters",
141 | "duration": 86959
142 | },
143 | {
144 | "location": {
145 | "file": "tests/ignore-changes-outside-root_test.rego",
146 | "row": 3,
147 | "col": 1
148 | },
149 | "package": "data.spacelift",
150 | "name": "test_affected_no_files",
151 | "duration": 51958
152 | },
153 | {
154 | "location": {
155 | "file": "tests/ignore-changes-outside-root_test.rego",
156 | "row": 10,
157 | "col": 1
158 | },
159 | "package": "data.spacelift",
160 | "name": "test_affected_tf_files",
161 | "duration": 103042
162 | },
163 | {
164 | "location": {
165 | "file": "tests/ignore-changes-outside-root_test.rego",
166 | "row": 17,
167 | "col": 1
168 | },
169 | "package": "data.spacelift",
170 | "name": "test_affected_no_tf_files",
171 | "duration": 392583
172 | },
173 | {
174 | "location": {
175 | "file": "tests/ignore-changes-outside-root_test.rego",
176 | "row": 24,
177 | "col": 1
178 | },
179 | "package": "data.spacelift",
180 | "name": "test_affected_outside_project_root",
181 | "duration": 272209
182 | },
183 | {
184 | "location": {
185 | "file": "tests/ignore-changes-outside-root_test.rego",
186 | "row": 31,
187 | "col": 1
188 | },
189 | "package": "data.spacelift",
190 | "name": "test_ignore_affected",
191 | "duration": 48208
192 | },
193 | {
194 | "location": {
195 | "file": "tests/ignore-changes-outside-root_test.rego",
196 | "row": 35,
197 | "col": 1
198 | },
199 | "package": "data.spacelift",
200 | "name": "test_ignore_not_affected",
201 | "duration": 49334
202 | },
203 | {
204 | "location": {
205 | "file": "tests/ignore-changes-outside-root_test.rego",
206 | "row": 39,
207 | "col": 1
208 | },
209 | "package": "data.spacelift",
210 | "name": "test_ignore_tag",
211 | "duration": 61291
212 | },
213 | {
214 | "location": {
215 | "file": "tests/ignore-changes-outside-root_test.rego",
216 | "row": 44,
217 | "col": 1
218 | },
219 | "package": "data.spacelift",
220 | "name": "test_propose_affected",
221 | "duration": 51625
222 | },
223 | {
224 | "location": {
225 | "file": "tests/ignore-changes-outside-root_test.rego",
226 | "row": 48,
227 | "col": 1
228 | },
229 | "package": "data.spacelift",
230 | "name": "test_propose_not_affected",
231 | "duration": 41833
232 | },
233 | {
234 | "location": {
235 | "file": "tests/ignore-changes-outside-root_test.rego",
236 | "row": 57,
237 | "col": 1
238 | },
239 | "package": "data.spacelift",
240 | "name": "test_track_affected",
241 | "duration": 60291
242 | },
243 | {
244 | "location": {
245 | "file": "tests/ignore-changes-outside-root_test.rego",
246 | "row": 61,
247 | "col": 1
248 | },
249 | "package": "data.spacelift",
250 | "name": "test_track_not_affected",
251 | "duration": 59042
252 | },
253 | {
254 | "location": {
255 | "file": "tests/ignore-changes-outside-root_test.rego",
256 | "row": 65,
257 | "col": 1
258 | },
259 | "package": "data.spacelift",
260 | "name": "test_track_not_stack_branch",
261 | "duration": 52666
262 | },
263 | {
264 | "location": {
265 | "file": "tests/notification-stack-failure-origins_test.rego",
266 | "row": 11,
267 | "col": 1
268 | },
269 | "package": "data.spacelift",
270 | "name": "test_slack_notification_for_tracked_failed_run",
271 | "duration": 283000
272 | },
273 | {
274 | "location": {
275 | "file": "tests/notification-stack-failure-origins_test.rego",
276 | "row": 38,
277 | "col": 1
278 | },
279 | "package": "data.spacelift",
280 | "name": "test_no_slack_notification_for_non_tracked_run",
281 | "duration": 47916
282 | },
283 | {
284 | "location": {
285 | "file": "tests/notification-stack-failure-origins_test.rego",
286 | "row": 48,
287 | "col": 1
288 | },
289 | "package": "data.spacelift",
290 | "name": "test_no_slack_notification_for_successful_run",
291 | "duration": 48083
292 | },
293 | {
294 | "location": {
295 | "file": "tests/notification-stack-failure-origins_test.rego",
296 | "row": 58,
297 | "col": 1
298 | },
299 | "package": "data.spacelift",
300 | "name": "test_slack_notification_with_unknown_github_user",
301 | "duration": 185666
302 | },
303 | {
304 | "location": {
305 | "file": "tests/notification-stack-failure-origins_test.rego",
306 | "row": 82,
307 | "col": 1
308 | },
309 | "package": "data.spacelift",
310 | "name": "test_pr_comment_for_tracked_failed_run",
311 | "duration": 189083
312 | },
313 | {
314 | "location": {
315 | "file": "tests/notification-stack-failure-origins_test.rego",
316 | "row": 111,
317 | "col": 1
318 | },
319 | "package": "data.spacelift",
320 | "name": "test_no_pr_comment_for_non_tracked_run",
321 | "duration": 45875
322 | },
323 | {
324 | "location": {
325 | "file": "tests/notification-stack-failure-origins_test.rego",
326 | "row": 121,
327 | "col": 1
328 | },
329 | "package": "data.spacelift",
330 | "name": "test_no_pr_comment_for_successful_run",
331 | "duration": 47250
332 | },
333 | {
334 | "location": {
335 | "file": "tests/readers-writers-admins-teams_test.rego",
336 | "row": 9,
337 | "col": 1
338 | },
339 | "package": "data.spacelift_test",
340 | "name": "test_allow_writers",
341 | "duration": 45292
342 | },
343 | {
344 | "location": {
345 | "file": "tests/readers-writers-admins-teams_test.rego",
346 | "row": 13,
347 | "col": 1
348 | },
349 | "package": "data.spacelift_test",
350 | "name": "test_allow_admins",
351 | "duration": 44667
352 | },
353 | {
354 | "location": {
355 | "file": "tests/readers-writers-admins-teams_test.rego",
356 | "row": 17,
357 | "col": 1
358 | },
359 | "package": "data.spacelift_test",
360 | "name": "test_allow_readers",
361 | "duration": 45291
362 | },
363 | {
364 | "location": {
365 | "file": "tests/readers-writers-admins-teams_test.rego",
366 | "row": 22,
367 | "col": 1
368 | },
369 | "package": "data.spacelift_test",
370 | "name": "test_space_admin_access",
371 | "duration": 58625
372 | },
373 | {
374 | "location": {
375 | "file": "tests/readers-writers-admins-teams_test.rego",
376 | "row": 27,
377 | "col": 1
378 | },
379 | "package": "data.spacelift_test",
380 | "name": "test_space_write_access",
381 | "duration": 58667
382 | },
383 | {
384 | "location": {
385 | "file": "tests/readers-writers-admins-teams_test.rego",
386 | "row": 32,
387 | "col": 1
388 | },
389 | "package": "data.spacelift_test",
390 | "name": "test_space_read_access",
391 | "duration": 85959
392 | },
393 | {
394 | "location": {
395 | "file": "tests/track-using-labels_test.rego",
396 | "row": 13,
397 | "col": 1
398 | },
399 | "package": "data.spacelift",
400 | "name": "test_track_different_branches",
401 | "duration": 51834
402 | },
403 | {
404 | "location": {
405 | "file": "tests/track-using-labels_test.rego",
406 | "row": 21,
407 | "col": 1
408 | },
409 | "package": "data.spacelift",
410 | "name": "test_propose_non_empty_branch",
411 | "duration": 53416
412 | },
413 | {
414 | "location": {
415 | "file": "tests/track-using-labels_test.rego",
416 | "row": 29,
417 | "col": 1
418 | },
419 | "package": "data.spacelift",
420 | "name": "test_propose_empty_branch",
421 | "duration": 50166
422 | },
423 | {
424 | "location": {
425 | "file": "tests/track-using-labels_test.rego",
426 | "row": 37,
427 | "col": 1
428 | },
429 | "package": "data.spacelift",
430 | "name": "test_affected_directory",
431 | "duration": 474417
432 | },
433 | {
434 | "location": {
435 | "file": "tests/track-using-labels_test.rego",
436 | "row": 45,
437 | "col": 1
438 | },
439 | "package": "data.spacelift",
440 | "name": "test_affected_extension",
441 | "duration": 176334
442 | },
443 | {
444 | "location": {
445 | "file": "tests/track-using-labels_test.rego",
446 | "row": 53,
447 | "col": 1
448 | },
449 | "package": "data.spacelift",
450 | "name": "test_not_affected_directory",
451 | "duration": 143458
452 | },
453 | {
454 | "location": {
455 | "file": "tests/track-using-labels_test.rego",
456 | "row": 61,
457 | "col": 1
458 | },
459 | "package": "data.spacelift",
460 | "name": "test_not_affected_extension",
461 | "duration": 140709
462 | },
463 | {
464 | "location": {
465 | "file": "tests/track-using-labels_test.rego",
466 | "row": 69,
467 | "col": 1
468 | },
469 | "package": "data.spacelift",
470 | "name": "test_ignore_not_affected#01",
471 | "duration": 145166
472 | }
473 | ]
474 |
--------------------------------------------------------------------------------
/action.yml:
--------------------------------------------------------------------------------
1 | name: "OPA Rego Test and Coverage Report"
2 | description: "Run OPA tests and generate coverage report for PRs. Test your OPA Rego policies!"
3 | author: Masterpoint
4 |
5 | branding:
6 | icon: "zap"
7 | color: "blue"
8 |
9 | inputs:
10 | path:
11 | description: "Path to the directory containing OPA Rego files to test."
12 | required: true
13 | test_mode:
14 | description: Whether to test the Rego by an entire directory (including entire package, e.g. `opa test ./`) or by individual files (e.g. `opa test a_test.rego a.rego`). Options of `directory` or `file`. Default is `directory`.
15 | required: false
16 | default: directory
17 | test_file_postfix:
18 | description: 'The postfix to use for test files, only applicable for testing file by file. E.g. notification.rego <> notification_test.rego. Default is "_test".'
19 | required: false
20 | default: "_test"
21 | write_pr_comment:
22 | description: "Flag to write an user friendly PR comment of the test results. Default of true."
23 | required: false
24 | default: "true"
25 | pr_comment_title:
26 | description: "Title of the PR comment of the test results."
27 | required: false
28 | default: "🧪 OPA Rego Policy Test Results"
29 | pr_comment_mode:
30 | description: Mode that will be used to update comment. Options of upsert (update in place) or recreate.
31 | default: "upsert"
32 | run_coverage_report:
33 | description: "Flag to run OPA coverage tests and write to the PR. The `write_pr_comment` must be enabled for the coverage report to be written. Default of true."
34 | required: false
35 | default: "true"
36 | report_untested_files:
37 | description: "Check & report in the PR comments of the Rego files that do not have any corresponding test files. For best conventions, append the postfix `_test` (or what you set as the `test_file_postfix` input) for your test file. E.g. `notification.rego` <> `notification_test.rego`"
38 | required: false
39 | default: "false"
40 | opa_version:
41 | description: "Version of OPA CLI to use. Default is 1.4.2, latest as of 2025-05-15."
42 | required: false
43 | default: "1.4.2"
44 | opa_static:
45 | description: "Whether to use the static binary. Default is false."
46 | required: false
47 | default: "false"
48 | indicate_source_message:
49 | description: Flag to comment the origins (this repository) of the GitHub Action in the PR comment. Default of true.
50 | required: false
51 | default: "true"
52 | outputs:
53 | parsed_results:
54 | description: The parsed results after processing the tests and/or coverage report.
55 | value: ${{ steps.parse-results.outputs.parsed_results }}
56 | tests_failed:
57 | description: A `true` or `false` flag indicating if any of the tests failed or not.
58 | value: ${{ steps.parse-results.outputs.tests_failed }}
59 |
60 | runs:
61 | using: "composite"
62 | steps:
63 | - name: Setup OPA
64 | uses: open-policy-agent/setup-opa@v2
65 | with:
66 | version: ${{ inputs.opa_version }}
67 | static: ${{ inputs.opa_static }}
68 |
69 | - name: Find Rego files without tests
70 | if: inputs.report_untested_files == 'true'
71 | id: find-no-test
72 | shell: bash
73 | run: |
74 | main_dir="${{ inputs.path }}"
75 | echo "Searching for untested Rego files in: $main_dir"
76 |
77 | no_test_files=$(find "$main_dir" -type f -name "*.rego" ! -name "*${{ inputs.test_file_postfix }}.rego" | while read file; do
78 | base_name=$(basename "$file" .rego)
79 |
80 | # Search for a corresponding test file anywhere in the project
81 | test_file=$(find "$main_dir" -type f -name "${base_name}${{ inputs.test_file_postfix }}.rego")
82 |
83 | if [ -z "$test_file" ]; then
84 | echo "$file"
85 | fi
86 | done)
87 |
88 | echo "no_test_files<> $GITHUB_OUTPUT
89 | echo "$no_test_files" >> $GITHUB_OUTPUT
90 | echo "EOF" >> $GITHUB_OUTPUT
91 |
92 | echo "Search complete, found the following Rego files without tests: $no_test_files"
93 |
94 | # Parse and format the test results which will be consumed by the following step to comment on the PR.
95 | - name: "Execute and Parse Results from Tests"
96 | id: parse-results
97 | run: node ${{ github.action_path }}/dist/index.js
98 | shell: bash
99 | env:
100 | test_mode: ${{ inputs.test_mode }}
101 | report_untested_files: ${{ inputs.report_untested_files }}
102 | no_test_files: ${{ steps.find-no-test.outputs.no_test_files }}
103 | pr_comment_title: ${{ inputs.pr_comment_title }}
104 | run_coverage_report: ${{ inputs.run_coverage_report }}
105 | indicate_source_message: ${{ inputs.indicate_source_message }}
106 | path: ${{ inputs.path }}
107 | test_file_postfix: ${{ inputs.test_file_postfix }}
108 |
109 | # Create (or update in-place) a PR comment of the test result output
110 | - name: Comment on PR
111 | uses: thollander/actions-comment-pull-request@24bffb9b452ba05a4f3f77933840a6a841d1b32b # v3.0.1
112 | # If `write_pr_comment` enabled, regardless of if test is success or fail, write the results of the failure.
113 | # Even if input is bool, it has to be treated as string bc of GH's behavior (https://github.com/actions/runner/issues/1483)
114 | if: inputs.write_pr_comment == 'true' && (success() || failure())
115 | with:
116 | message: |
117 | ${{ steps.parse-results.outputs.parsed_results }}
118 | comment-tag: opa-test-results-${{ github.action }} # using the action name as the tag as it is unique, and in case multiple executions of the action are in the same PR
119 | mode: ${{ inputs.pr_comment_mode }}
120 |
--------------------------------------------------------------------------------
/assets/banner-pr-comment-example.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/masterpointio/github-action-opa-rego-test/7a055ee4efd9f5f536230156adda071c1944d421/assets/banner-pr-comment-example.png
--------------------------------------------------------------------------------
/assets/opa-logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/masterpointio/github-action-opa-rego-test/7a055ee4efd9f5f536230156adda071c1944d421/assets/opa-logo.png
--------------------------------------------------------------------------------
/assets/readme-example-1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/masterpointio/github-action-opa-rego-test/7a055ee4efd9f5f536230156adda071c1944d421/assets/readme-example-1.png
--------------------------------------------------------------------------------
/assets/readme-example-2.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/masterpointio/github-action-opa-rego-test/7a055ee4efd9f5f536230156adda071c1944d421/assets/readme-example-2.png
--------------------------------------------------------------------------------
/assets/readme-example-3.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/masterpointio/github-action-opa-rego-test/7a055ee4efd9f5f536230156adda071c1944d421/assets/readme-example-3.png
--------------------------------------------------------------------------------
/assets/readme-test-results.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/masterpointio/github-action-opa-rego-test/7a055ee4efd9f5f536230156adda071c1944d421/assets/readme-test-results.png
--------------------------------------------------------------------------------
/assets/test-file-structure-example.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/masterpointio/github-action-opa-rego-test/7a055ee4efd9f5f536230156adda071c1944d421/assets/test-file-structure-example.png
--------------------------------------------------------------------------------
/dist/sourcemap-register.js:
--------------------------------------------------------------------------------
1 | (()=>{var e={650:e=>{var r=Object.prototype.toString;var n=typeof Buffer.alloc==="function"&&typeof Buffer.allocUnsafe==="function"&&typeof Buffer.from==="function";function isArrayBuffer(e){return r.call(e).slice(8,-1)==="ArrayBuffer"}function fromArrayBuffer(e,r,t){r>>>=0;var o=e.byteLength-r;if(o<0){throw new RangeError("'offset' is out of bounds")}if(t===undefined){t=o}else{t>>>=0;if(t>o){throw new RangeError("'length' is out of bounds")}}return n?Buffer.from(e.slice(r,r+t)):new Buffer(new Uint8Array(e.slice(r,r+t)))}function fromString(e,r){if(typeof r!=="string"||r===""){r="utf8"}if(!Buffer.isEncoding(r)){throw new TypeError('"encoding" must be a valid string encoding')}return n?Buffer.from(e,r):new Buffer(e,r)}function bufferFrom(e,r,t){if(typeof e==="number"){throw new TypeError('"value" argument must not be a number')}if(isArrayBuffer(e)){return fromArrayBuffer(e,r,t)}if(typeof e==="string"){return fromString(e,r)}return n?Buffer.from(e):new Buffer(e)}e.exports=bufferFrom},274:(e,r,n)=>{var t=n(339);var o=Object.prototype.hasOwnProperty;var i=typeof Map!=="undefined";function ArraySet(){this._array=[];this._set=i?new Map:Object.create(null)}ArraySet.fromArray=function ArraySet_fromArray(e,r){var n=new ArraySet;for(var t=0,o=e.length;t=0){return r}}else{var n=t.toSetString(e);if(o.call(this._set,n)){return this._set[n]}}throw new Error('"'+e+'" is not in the set.')};ArraySet.prototype.at=function ArraySet_at(e){if(e>=0&&e{var t=n(190);var o=5;var i=1<>1;return r?-n:n}r.encode=function base64VLQ_encode(e){var r="";var n;var i=toVLQSigned(e);do{n=i&a;i>>>=o;if(i>0){n|=u}r+=t.encode(n)}while(i>0);return r};r.decode=function base64VLQ_decode(e,r,n){var i=e.length;var s=0;var l=0;var c,p;do{if(r>=i){throw new Error("Expected more digits in base 64 VLQ value.")}p=t.decode(e.charCodeAt(r++));if(p===-1){throw new Error("Invalid base64 digit: "+e.charAt(r-1))}c=!!(p&u);p&=a;s=s+(p<{var n="ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/".split("");r.encode=function(e){if(0<=e&&e{r.GREATEST_LOWER_BOUND=1;r.LEAST_UPPER_BOUND=2;function recursiveSearch(e,n,t,o,i,a){var u=Math.floor((n-e)/2)+e;var s=i(t,o[u],true);if(s===0){return u}else if(s>0){if(n-u>1){return recursiveSearch(u,n,t,o,i,a)}if(a==r.LEAST_UPPER_BOUND){return n1){return recursiveSearch(e,u,t,o,i,a)}if(a==r.LEAST_UPPER_BOUND){return u}else{return e<0?-1:e}}}r.search=function search(e,n,t,o){if(n.length===0){return-1}var i=recursiveSearch(-1,n.length,e,n,t,o||r.GREATEST_LOWER_BOUND);if(i<0){return-1}while(i-1>=0){if(t(n[i],n[i-1],true)!==0){break}--i}return i}},680:(e,r,n)=>{var t=n(339);function generatedPositionAfter(e,r){var n=e.generatedLine;var o=r.generatedLine;var i=e.generatedColumn;var a=r.generatedColumn;return o>n||o==n&&a>=i||t.compareByGeneratedPositionsInflated(e,r)<=0}function MappingList(){this._array=[];this._sorted=true;this._last={generatedLine:-1,generatedColumn:0}}MappingList.prototype.unsortedForEach=function MappingList_forEach(e,r){this._array.forEach(e,r)};MappingList.prototype.add=function MappingList_add(e){if(generatedPositionAfter(this._last,e)){this._last=e;this._array.push(e)}else{this._sorted=false;this._array.push(e)}};MappingList.prototype.toArray=function MappingList_toArray(){if(!this._sorted){this._array.sort(t.compareByGeneratedPositionsInflated);this._sorted=true}return this._array};r.H=MappingList},758:(e,r)=>{function swap(e,r,n){var t=e[r];e[r]=e[n];e[n]=t}function randomIntInRange(e,r){return Math.round(e+Math.random()*(r-e))}function doQuickSort(e,r,n,t){if(n{var t;var o=n(339);var i=n(345);var a=n(274).I;var u=n(449);var s=n(758).U;function SourceMapConsumer(e,r){var n=e;if(typeof e==="string"){n=o.parseSourceMapInput(e)}return n.sections!=null?new IndexedSourceMapConsumer(n,r):new BasicSourceMapConsumer(n,r)}SourceMapConsumer.fromSourceMap=function(e,r){return BasicSourceMapConsumer.fromSourceMap(e,r)};SourceMapConsumer.prototype._version=3;SourceMapConsumer.prototype.__generatedMappings=null;Object.defineProperty(SourceMapConsumer.prototype,"_generatedMappings",{configurable:true,enumerable:true,get:function(){if(!this.__generatedMappings){this._parseMappings(this._mappings,this.sourceRoot)}return this.__generatedMappings}});SourceMapConsumer.prototype.__originalMappings=null;Object.defineProperty(SourceMapConsumer.prototype,"_originalMappings",{configurable:true,enumerable:true,get:function(){if(!this.__originalMappings){this._parseMappings(this._mappings,this.sourceRoot)}return this.__originalMappings}});SourceMapConsumer.prototype._charIsMappingSeparator=function SourceMapConsumer_charIsMappingSeparator(e,r){var n=e.charAt(r);return n===";"||n===","};SourceMapConsumer.prototype._parseMappings=function SourceMapConsumer_parseMappings(e,r){throw new Error("Subclasses must implement _parseMappings")};SourceMapConsumer.GENERATED_ORDER=1;SourceMapConsumer.ORIGINAL_ORDER=2;SourceMapConsumer.GREATEST_LOWER_BOUND=1;SourceMapConsumer.LEAST_UPPER_BOUND=2;SourceMapConsumer.prototype.eachMapping=function SourceMapConsumer_eachMapping(e,r,n){var t=r||null;var i=n||SourceMapConsumer.GENERATED_ORDER;var a;switch(i){case SourceMapConsumer.GENERATED_ORDER:a=this._generatedMappings;break;case SourceMapConsumer.ORIGINAL_ORDER:a=this._originalMappings;break;default:throw new Error("Unknown order of iteration.")}var u=this.sourceRoot;a.map((function(e){var r=e.source===null?null:this._sources.at(e.source);r=o.computeSourceURL(u,r,this._sourceMapURL);return{source:r,generatedLine:e.generatedLine,generatedColumn:e.generatedColumn,originalLine:e.originalLine,originalColumn:e.originalColumn,name:e.name===null?null:this._names.at(e.name)}}),this).forEach(e,t)};SourceMapConsumer.prototype.allGeneratedPositionsFor=function SourceMapConsumer_allGeneratedPositionsFor(e){var r=o.getArg(e,"line");var n={source:o.getArg(e,"source"),originalLine:r,originalColumn:o.getArg(e,"column",0)};n.source=this._findSourceIndex(n.source);if(n.source<0){return[]}var t=[];var a=this._findMapping(n,this._originalMappings,"originalLine","originalColumn",o.compareByOriginalPositions,i.LEAST_UPPER_BOUND);if(a>=0){var u=this._originalMappings[a];if(e.column===undefined){var s=u.originalLine;while(u&&u.originalLine===s){t.push({line:o.getArg(u,"generatedLine",null),column:o.getArg(u,"generatedColumn",null),lastColumn:o.getArg(u,"lastGeneratedColumn",null)});u=this._originalMappings[++a]}}else{var l=u.originalColumn;while(u&&u.originalLine===r&&u.originalColumn==l){t.push({line:o.getArg(u,"generatedLine",null),column:o.getArg(u,"generatedColumn",null),lastColumn:o.getArg(u,"lastGeneratedColumn",null)});u=this._originalMappings[++a]}}}return t};r.SourceMapConsumer=SourceMapConsumer;function BasicSourceMapConsumer(e,r){var n=e;if(typeof e==="string"){n=o.parseSourceMapInput(e)}var t=o.getArg(n,"version");var i=o.getArg(n,"sources");var u=o.getArg(n,"names",[]);var s=o.getArg(n,"sourceRoot",null);var l=o.getArg(n,"sourcesContent",null);var c=o.getArg(n,"mappings");var p=o.getArg(n,"file",null);if(t!=this._version){throw new Error("Unsupported version: "+t)}if(s){s=o.normalize(s)}i=i.map(String).map(o.normalize).map((function(e){return s&&o.isAbsolute(s)&&o.isAbsolute(e)?o.relative(s,e):e}));this._names=a.fromArray(u.map(String),true);this._sources=a.fromArray(i,true);this._absoluteSources=this._sources.toArray().map((function(e){return o.computeSourceURL(s,e,r)}));this.sourceRoot=s;this.sourcesContent=l;this._mappings=c;this._sourceMapURL=r;this.file=p}BasicSourceMapConsumer.prototype=Object.create(SourceMapConsumer.prototype);BasicSourceMapConsumer.prototype.consumer=SourceMapConsumer;BasicSourceMapConsumer.prototype._findSourceIndex=function(e){var r=e;if(this.sourceRoot!=null){r=o.relative(this.sourceRoot,r)}if(this._sources.has(r)){return this._sources.indexOf(r)}var n;for(n=0;n1){v.source=l+_[1];l+=_[1];v.originalLine=i+_[2];i=v.originalLine;v.originalLine+=1;v.originalColumn=a+_[3];a=v.originalColumn;if(_.length>4){v.name=c+_[4];c+=_[4]}}m.push(v);if(typeof v.originalLine==="number"){d.push(v)}}}s(m,o.compareByGeneratedPositionsDeflated);this.__generatedMappings=m;s(d,o.compareByOriginalPositions);this.__originalMappings=d};BasicSourceMapConsumer.prototype._findMapping=function SourceMapConsumer_findMapping(e,r,n,t,o,a){if(e[n]<=0){throw new TypeError("Line must be greater than or equal to 1, got "+e[n])}if(e[t]<0){throw new TypeError("Column must be greater than or equal to 0, got "+e[t])}return i.search(e,r,o,a)};BasicSourceMapConsumer.prototype.computeColumnSpans=function SourceMapConsumer_computeColumnSpans(){for(var e=0;e=0){var t=this._generatedMappings[n];if(t.generatedLine===r.generatedLine){var i=o.getArg(t,"source",null);if(i!==null){i=this._sources.at(i);i=o.computeSourceURL(this.sourceRoot,i,this._sourceMapURL)}var a=o.getArg(t,"name",null);if(a!==null){a=this._names.at(a)}return{source:i,line:o.getArg(t,"originalLine",null),column:o.getArg(t,"originalColumn",null),name:a}}}return{source:null,line:null,column:null,name:null}};BasicSourceMapConsumer.prototype.hasContentsOfAllSources=function BasicSourceMapConsumer_hasContentsOfAllSources(){if(!this.sourcesContent){return false}return this.sourcesContent.length>=this._sources.size()&&!this.sourcesContent.some((function(e){return e==null}))};BasicSourceMapConsumer.prototype.sourceContentFor=function SourceMapConsumer_sourceContentFor(e,r){if(!this.sourcesContent){return null}var n=this._findSourceIndex(e);if(n>=0){return this.sourcesContent[n]}var t=e;if(this.sourceRoot!=null){t=o.relative(this.sourceRoot,t)}var i;if(this.sourceRoot!=null&&(i=o.urlParse(this.sourceRoot))){var a=t.replace(/^file:\/\//,"");if(i.scheme=="file"&&this._sources.has(a)){return this.sourcesContent[this._sources.indexOf(a)]}if((!i.path||i.path=="/")&&this._sources.has("/"+t)){return this.sourcesContent[this._sources.indexOf("/"+t)]}}if(r){return null}else{throw new Error('"'+t+'" is not in the SourceMap.')}};BasicSourceMapConsumer.prototype.generatedPositionFor=function SourceMapConsumer_generatedPositionFor(e){var r=o.getArg(e,"source");r=this._findSourceIndex(r);if(r<0){return{line:null,column:null,lastColumn:null}}var n={source:r,originalLine:o.getArg(e,"line"),originalColumn:o.getArg(e,"column")};var t=this._findMapping(n,this._originalMappings,"originalLine","originalColumn",o.compareByOriginalPositions,o.getArg(e,"bias",SourceMapConsumer.GREATEST_LOWER_BOUND));if(t>=0){var i=this._originalMappings[t];if(i.source===n.source){return{line:o.getArg(i,"generatedLine",null),column:o.getArg(i,"generatedColumn",null),lastColumn:o.getArg(i,"lastGeneratedColumn",null)}}}return{line:null,column:null,lastColumn:null}};t=BasicSourceMapConsumer;function IndexedSourceMapConsumer(e,r){var n=e;if(typeof e==="string"){n=o.parseSourceMapInput(e)}var t=o.getArg(n,"version");var i=o.getArg(n,"sections");if(t!=this._version){throw new Error("Unsupported version: "+t)}this._sources=new a;this._names=new a;var u={line:-1,column:0};this._sections=i.map((function(e){if(e.url){throw new Error("Support for url field in sections not implemented.")}var n=o.getArg(e,"offset");var t=o.getArg(n,"line");var i=o.getArg(n,"column");if(t{var t=n(449);var o=n(339);var i=n(274).I;var a=n(680).H;function SourceMapGenerator(e){if(!e){e={}}this._file=o.getArg(e,"file",null);this._sourceRoot=o.getArg(e,"sourceRoot",null);this._skipValidation=o.getArg(e,"skipValidation",false);this._sources=new i;this._names=new i;this._mappings=new a;this._sourcesContents=null}SourceMapGenerator.prototype._version=3;SourceMapGenerator.fromSourceMap=function SourceMapGenerator_fromSourceMap(e){var r=e.sourceRoot;var n=new SourceMapGenerator({file:e.file,sourceRoot:r});e.eachMapping((function(e){var t={generated:{line:e.generatedLine,column:e.generatedColumn}};if(e.source!=null){t.source=e.source;if(r!=null){t.source=o.relative(r,t.source)}t.original={line:e.originalLine,column:e.originalColumn};if(e.name!=null){t.name=e.name}}n.addMapping(t)}));e.sources.forEach((function(t){var i=t;if(r!==null){i=o.relative(r,t)}if(!n._sources.has(i)){n._sources.add(i)}var a=e.sourceContentFor(t);if(a!=null){n.setSourceContent(t,a)}}));return n};SourceMapGenerator.prototype.addMapping=function SourceMapGenerator_addMapping(e){var r=o.getArg(e,"generated");var n=o.getArg(e,"original",null);var t=o.getArg(e,"source",null);var i=o.getArg(e,"name",null);if(!this._skipValidation){this._validateMapping(r,n,t,i)}if(t!=null){t=String(t);if(!this._sources.has(t)){this._sources.add(t)}}if(i!=null){i=String(i);if(!this._names.has(i)){this._names.add(i)}}this._mappings.add({generatedLine:r.line,generatedColumn:r.column,originalLine:n!=null&&n.line,originalColumn:n!=null&&n.column,source:t,name:i})};SourceMapGenerator.prototype.setSourceContent=function SourceMapGenerator_setSourceContent(e,r){var n=e;if(this._sourceRoot!=null){n=o.relative(this._sourceRoot,n)}if(r!=null){if(!this._sourcesContents){this._sourcesContents=Object.create(null)}this._sourcesContents[o.toSetString(n)]=r}else if(this._sourcesContents){delete this._sourcesContents[o.toSetString(n)];if(Object.keys(this._sourcesContents).length===0){this._sourcesContents=null}}};SourceMapGenerator.prototype.applySourceMap=function SourceMapGenerator_applySourceMap(e,r,n){var t=r;if(r==null){if(e.file==null){throw new Error("SourceMapGenerator.prototype.applySourceMap requires either an explicit source file, "+'or the source map\'s "file" property. Both were omitted.')}t=e.file}var a=this._sourceRoot;if(a!=null){t=o.relative(a,t)}var u=new i;var s=new i;this._mappings.unsortedForEach((function(r){if(r.source===t&&r.originalLine!=null){var i=e.originalPositionFor({line:r.originalLine,column:r.originalColumn});if(i.source!=null){r.source=i.source;if(n!=null){r.source=o.join(n,r.source)}if(a!=null){r.source=o.relative(a,r.source)}r.originalLine=i.line;r.originalColumn=i.column;if(i.name!=null){r.name=i.name}}}var l=r.source;if(l!=null&&!u.has(l)){u.add(l)}var c=r.name;if(c!=null&&!s.has(c)){s.add(c)}}),this);this._sources=u;this._names=s;e.sources.forEach((function(r){var t=e.sourceContentFor(r);if(t!=null){if(n!=null){r=o.join(n,r)}if(a!=null){r=o.relative(a,r)}this.setSourceContent(r,t)}}),this)};SourceMapGenerator.prototype._validateMapping=function SourceMapGenerator_validateMapping(e,r,n,t){if(r&&typeof r.line!=="number"&&typeof r.column!=="number"){throw new Error("original.line and original.column are not numbers -- you probably meant to omit "+"the original mapping entirely and only map the generated position. If so, pass "+"null for the original mapping instead of an object with empty or null values.")}if(e&&"line"in e&&"column"in e&&e.line>0&&e.column>=0&&!r&&!n&&!t){return}else if(e&&"line"in e&&"column"in e&&r&&"line"in r&&"column"in r&&e.line>0&&e.column>=0&&r.line>0&&r.column>=0&&n){return}else{throw new Error("Invalid mapping: "+JSON.stringify({generated:e,source:n,original:r,name:t}))}};SourceMapGenerator.prototype._serializeMappings=function SourceMapGenerator_serializeMappings(){var e=0;var r=1;var n=0;var i=0;var a=0;var u=0;var s="";var l;var c;var p;var f;var g=this._mappings.toArray();for(var h=0,d=g.length;h0){if(!o.compareByGeneratedPositionsInflated(c,g[h-1])){continue}l+=","}}l+=t.encode(c.generatedColumn-e);e=c.generatedColumn;if(c.source!=null){f=this._sources.indexOf(c.source);l+=t.encode(f-u);u=f;l+=t.encode(c.originalLine-1-i);i=c.originalLine-1;l+=t.encode(c.originalColumn-n);n=c.originalColumn;if(c.name!=null){p=this._names.indexOf(c.name);l+=t.encode(p-a);a=p}}s+=l}return s};SourceMapGenerator.prototype._generateSourcesContent=function SourceMapGenerator_generateSourcesContent(e,r){return e.map((function(e){if(!this._sourcesContents){return null}if(r!=null){e=o.relative(r,e)}var n=o.toSetString(e);return Object.prototype.hasOwnProperty.call(this._sourcesContents,n)?this._sourcesContents[n]:null}),this)};SourceMapGenerator.prototype.toJSON=function SourceMapGenerator_toJSON(){var e={version:this._version,sources:this._sources.toArray(),names:this._names.toArray(),mappings:this._serializeMappings()};if(this._file!=null){e.file=this._file}if(this._sourceRoot!=null){e.sourceRoot=this._sourceRoot}if(this._sourcesContents){e.sourcesContent=this._generateSourcesContent(e.sources,e.sourceRoot)}return e};SourceMapGenerator.prototype.toString=function SourceMapGenerator_toString(){return JSON.stringify(this.toJSON())};r.h=SourceMapGenerator},351:(e,r,n)=>{var t;var o=n(591).h;var i=n(339);var a=/(\r?\n)/;var u=10;var s="$$$isSourceNode$$$";function SourceNode(e,r,n,t,o){this.children=[];this.sourceContents={};this.line=e==null?null:e;this.column=r==null?null:r;this.source=n==null?null:n;this.name=o==null?null:o;this[s]=true;if(t!=null)this.add(t)}SourceNode.fromStringWithSourceMap=function SourceNode_fromStringWithSourceMap(e,r,n){var t=new SourceNode;var o=e.split(a);var u=0;var shiftNextLine=function(){var e=getNextLine();var r=getNextLine()||"";return e+r;function getNextLine(){return u=0;r--){this.prepend(e[r])}}else if(e[s]||typeof e==="string"){this.children.unshift(e)}else{throw new TypeError("Expected a SourceNode, string, or an array of SourceNodes and strings. Got "+e)}return this};SourceNode.prototype.walk=function SourceNode_walk(e){var r;for(var n=0,t=this.children.length;n0){r=[];for(n=0;n{function getArg(e,r,n){if(r in e){return e[r]}else if(arguments.length===3){return n}else{throw new Error('"'+r+'" is a required argument.')}}r.getArg=getArg;var n=/^(?:([\w+\-.]+):)?\/\/(?:(\w+:\w+)@)?([\w.-]*)(?::(\d+))?(.*)$/;var t=/^data:.+\,.+$/;function urlParse(e){var r=e.match(n);if(!r){return null}return{scheme:r[1],auth:r[2],host:r[3],port:r[4],path:r[5]}}r.urlParse=urlParse;function urlGenerate(e){var r="";if(e.scheme){r+=e.scheme+":"}r+="//";if(e.auth){r+=e.auth+"@"}if(e.host){r+=e.host}if(e.port){r+=":"+e.port}if(e.path){r+=e.path}return r}r.urlGenerate=urlGenerate;function normalize(e){var n=e;var t=urlParse(e);if(t){if(!t.path){return e}n=t.path}var o=r.isAbsolute(n);var i=n.split(/\/+/);for(var a,u=0,s=i.length-1;s>=0;s--){a=i[s];if(a==="."){i.splice(s,1)}else if(a===".."){u++}else if(u>0){if(a===""){i.splice(s+1,u);u=0}else{i.splice(s,2);u--}}}n=i.join("/");if(n===""){n=o?"/":"."}if(t){t.path=n;return urlGenerate(t)}return n}r.normalize=normalize;function join(e,r){if(e===""){e="."}if(r===""){r="."}var n=urlParse(r);var o=urlParse(e);if(o){e=o.path||"/"}if(n&&!n.scheme){if(o){n.scheme=o.scheme}return urlGenerate(n)}if(n||r.match(t)){return r}if(o&&!o.host&&!o.path){o.host=r;return urlGenerate(o)}var i=r.charAt(0)==="/"?r:normalize(e.replace(/\/+$/,"")+"/"+r);if(o){o.path=i;return urlGenerate(o)}return i}r.join=join;r.isAbsolute=function(e){return e.charAt(0)==="/"||n.test(e)};function relative(e,r){if(e===""){e="."}e=e.replace(/\/$/,"");var n=0;while(r.indexOf(e+"/")!==0){var t=e.lastIndexOf("/");if(t<0){return r}e=e.slice(0,t);if(e.match(/^([^\/]+:\/)?\/*$/)){return r}++n}return Array(n+1).join("../")+r.substr(e.length+1)}r.relative=relative;var o=function(){var e=Object.create(null);return!("__proto__"in e)}();function identity(e){return e}function toSetString(e){if(isProtoString(e)){return"$"+e}return e}r.toSetString=o?identity:toSetString;function fromSetString(e){if(isProtoString(e)){return e.slice(1)}return e}r.fromSetString=o?identity:fromSetString;function isProtoString(e){if(!e){return false}var r=e.length;if(r<9){return false}if(e.charCodeAt(r-1)!==95||e.charCodeAt(r-2)!==95||e.charCodeAt(r-3)!==111||e.charCodeAt(r-4)!==116||e.charCodeAt(r-5)!==111||e.charCodeAt(r-6)!==114||e.charCodeAt(r-7)!==112||e.charCodeAt(r-8)!==95||e.charCodeAt(r-9)!==95){return false}for(var n=r-10;n>=0;n--){if(e.charCodeAt(n)!==36){return false}}return true}function compareByOriginalPositions(e,r,n){var t=strcmp(e.source,r.source);if(t!==0){return t}t=e.originalLine-r.originalLine;if(t!==0){return t}t=e.originalColumn-r.originalColumn;if(t!==0||n){return t}t=e.generatedColumn-r.generatedColumn;if(t!==0){return t}t=e.generatedLine-r.generatedLine;if(t!==0){return t}return strcmp(e.name,r.name)}r.compareByOriginalPositions=compareByOriginalPositions;function compareByGeneratedPositionsDeflated(e,r,n){var t=e.generatedLine-r.generatedLine;if(t!==0){return t}t=e.generatedColumn-r.generatedColumn;if(t!==0||n){return t}t=strcmp(e.source,r.source);if(t!==0){return t}t=e.originalLine-r.originalLine;if(t!==0){return t}t=e.originalColumn-r.originalColumn;if(t!==0){return t}return strcmp(e.name,r.name)}r.compareByGeneratedPositionsDeflated=compareByGeneratedPositionsDeflated;function strcmp(e,r){if(e===r){return 0}if(e===null){return 1}if(r===null){return-1}if(e>r){return 1}return-1}function compareByGeneratedPositionsInflated(e,r){var n=e.generatedLine-r.generatedLine;if(n!==0){return n}n=e.generatedColumn-r.generatedColumn;if(n!==0){return n}n=strcmp(e.source,r.source);if(n!==0){return n}n=e.originalLine-r.originalLine;if(n!==0){return n}n=e.originalColumn-r.originalColumn;if(n!==0){return n}return strcmp(e.name,r.name)}r.compareByGeneratedPositionsInflated=compareByGeneratedPositionsInflated;function parseSourceMapInput(e){return JSON.parse(e.replace(/^\)]}'[^\n]*\n/,""))}r.parseSourceMapInput=parseSourceMapInput;function computeSourceURL(e,r,n){r=r||"";if(e){if(e[e.length-1]!=="/"&&r[0]!=="/"){e+="/"}r=e+r}if(n){var t=urlParse(n);if(!t){throw new Error("sourceMapURL could not be parsed")}if(t.path){var o=t.path.lastIndexOf("/");if(o>=0){t.path=t.path.substring(0,o+1)}}r=join(urlGenerate(t),r)}return normalize(r)}r.computeSourceURL=computeSourceURL},997:(e,r,n)=>{n(591).h;r.SourceMapConsumer=n(952).SourceMapConsumer;n(351)},284:(e,r,n)=>{e=n.nmd(e);var t=n(997).SourceMapConsumer;var o=n(17);var i;try{i=n(147);if(!i.existsSync||!i.readFileSync){i=null}}catch(e){}var a=n(650);function dynamicRequire(e,r){return e.require(r)}var u=false;var s=false;var l=false;var c="auto";var p={};var f={};var g=/^data:application\/json[^,]+base64,/;var h=[];var d=[];function isInBrowser(){if(c==="browser")return true;if(c==="node")return false;return typeof window!=="undefined"&&typeof XMLHttpRequest==="function"&&!(window.require&&window.module&&window.process&&window.process.type==="renderer")}function hasGlobalProcessEventEmitter(){return typeof process==="object"&&process!==null&&typeof process.on==="function"}function globalProcessVersion(){if(typeof process==="object"&&process!==null){return process.version}else{return""}}function globalProcessStderr(){if(typeof process==="object"&&process!==null){return process.stderr}}function globalProcessExit(e){if(typeof process==="object"&&process!==null&&typeof process.exit==="function"){return process.exit(e)}}function handlerExec(e){return function(r){for(var n=0;n"}var n=this.getLineNumber();if(n!=null){r+=":"+n;var t=this.getColumnNumber();if(t){r+=":"+t}}}var o="";var i=this.getFunctionName();var a=true;var u=this.isConstructor();var s=!(this.isToplevel()||u);if(s){var l=this.getTypeName();if(l==="[object Object]"){l="null"}var c=this.getMethodName();if(i){if(l&&i.indexOf(l)!=0){o+=l+"."}o+=i;if(c&&i.indexOf("."+c)!=i.length-c.length-1){o+=" [as "+c+"]"}}else{o+=l+"."+(c||"")}}else if(u){o+="new "+(i||"")}else if(i){o+=i}else{o+=r;a=false}if(a){o+=" ("+r+")"}return o}function cloneCallSite(e){var r={};Object.getOwnPropertyNames(Object.getPrototypeOf(e)).forEach((function(n){r[n]=/^(?:is|get)/.test(n)?function(){return e[n].call(e)}:e[n]}));r.toString=CallSiteToString;return r}function wrapCallSite(e,r){if(r===undefined){r={nextPosition:null,curPosition:null}}if(e.isNative()){r.curPosition=null;return e}var n=e.getFileName()||e.getScriptNameOrSourceURL();if(n){var t=e.getLineNumber();var o=e.getColumnNumber()-1;var i=/^v(10\.1[6-9]|10\.[2-9][0-9]|10\.[0-9]{3,}|1[2-9]\d*|[2-9]\d|\d{3,}|11\.11)/;var a=i.test(globalProcessVersion())?0:62;if(t===1&&o>a&&!isInBrowser()&&!e.isEval()){o-=a}var u=mapSourcePosition({source:n,line:t,column:o});r.curPosition=u;e=cloneCallSite(e);var s=e.getFunctionName;e.getFunctionName=function(){if(r.nextPosition==null){return s()}return r.nextPosition.name||s()};e.getFileName=function(){return u.source};e.getLineNumber=function(){return u.line};e.getColumnNumber=function(){return u.column+1};e.getScriptNameOrSourceURL=function(){return u.source};return e}var l=e.isEval()&&e.getEvalOrigin();if(l){l=mapEvalOrigin(l);e=cloneCallSite(e);e.getEvalOrigin=function(){return l};return e}return e}function prepareStackTrace(e,r){if(l){p={};f={}}var n=e.name||"Error";var t=e.message||"";var o=n+": "+t;var i={nextPosition:null,curPosition:null};var a=[];for(var u=r.length-1;u>=0;u--){a.push("\n at "+wrapCallSite(r[u],i));i.nextPosition=i.curPosition}i.curPosition=i.nextPosition=null;return o+a.reverse().join("")}function getErrorSource(e){var r=/\n at [^(]+ \((.*):(\d+):(\d+)\)/.exec(e.stack);if(r){var n=r[1];var t=+r[2];var o=+r[3];var a=p[n];if(!a&&i&&i.existsSync(n)){try{a=i.readFileSync(n,"utf8")}catch(e){a=""}}if(a){var u=a.split(/(?:\r\n|\r|\n)/)[t-1];if(u){return n+":"+t+"\n"+u+"\n"+new Array(o).join(" ")+"^"}}}return null}function printErrorAndExit(e){var r=getErrorSource(e);var n=globalProcessStderr();if(n&&n._handle&&n._handle.setBlocking){n._handle.setBlocking(true)}if(r){console.error();console.error(r)}console.error(e.stack);globalProcessExit(1)}function shimEmitUncaughtException(){var e=process.emit;process.emit=function(r){if(r==="uncaughtException"){var n=arguments[1]&&arguments[1].stack;var t=this.listeners(r).length>0;if(n&&!t){return printErrorAndExit(arguments[1])}}return e.apply(this,arguments)}}var S=h.slice(0);var _=d.slice(0);r.wrapCallSite=wrapCallSite;r.getErrorSource=getErrorSource;r.mapSourcePosition=mapSourcePosition;r.retrieveSourceMap=v;r.install=function(r){r=r||{};if(r.environment){c=r.environment;if(["node","browser","auto"].indexOf(c)===-1){throw new Error("environment "+c+" was unknown. Available options are {auto, browser, node}")}}if(r.retrieveFile){if(r.overrideRetrieveFile){h.length=0}h.unshift(r.retrieveFile)}if(r.retrieveSourceMap){if(r.overrideRetrieveSourceMap){d.length=0}d.unshift(r.retrieveSourceMap)}if(r.hookRequire&&!isInBrowser()){var n=dynamicRequire(e,"module");var t=n.prototype._compile;if(!t.__sourceMapSupport){n.prototype._compile=function(e,r){p[r]=e;f[r]=undefined;return t.call(this,e,r)};n.prototype._compile.__sourceMapSupport=true}}if(!l){l="emptyCacheBetweenOperations"in r?r.emptyCacheBetweenOperations:false}if(!u){u=true;Error.prepareStackTrace=prepareStackTrace}if(!s){var o="handleUncaughtExceptions"in r?r.handleUncaughtExceptions:true;try{var i=dynamicRequire(e,"worker_threads");if(i.isMainThread===false){o=false}}catch(e){}if(o&&hasGlobalProcessEventEmitter()){s=true;shimEmitUncaughtException()}}};r.resetRetrieveHandlers=function(){h.length=0;d.length=0;h=S.slice(0);d=_.slice(0);v=handlerExec(d);m=handlerExec(h)}},147:e=>{"use strict";e.exports=require("fs")},17:e=>{"use strict";e.exports=require("path")}};var r={};function __webpack_require__(n){var t=r[n];if(t!==undefined){return t.exports}var o=r[n]={id:n,loaded:false,exports:{}};var i=true;try{e[n](o,o.exports,__webpack_require__);i=false}finally{if(i)delete r[n]}o.loaded=true;return o.exports}(()=>{__webpack_require__.nmd=e=>{e.paths=[];if(!e.children)e.children=[];return e}})();if(typeof __webpack_require__!=="undefined")__webpack_require__.ab=__dirname+"/";var n={};(()=>{__webpack_require__(284).install()})();module.exports=n})();
--------------------------------------------------------------------------------
/examples/README.md:
--------------------------------------------------------------------------------
1 | # These example OPA policies are taken from [Spacelift's example policy library](https://github.com/spacelift-io/spacelift-policies-example-library)
2 |
--------------------------------------------------------------------------------
/examples/cancel-in-progress-runs.rego:
--------------------------------------------------------------------------------
1 | package spacelift
2 |
3 | # The push policy can be used to have the new run pre-empt any runs that are
4 | # currently in progress. The input document includes the in_progress key, which
5 | # contains an array of runs that are currently either still queued or are awaiting
6 | # human confirmation. You can use it in conjunction with the cancel rule like this:
7 | cancel[run.id] {
8 | run := input.in_progress[_]
9 | run.type == "PROPOSED"
10 | run.state == "QUEUED"
11 | run.branch == input.pull_request.head.branch
12 | }
13 |
14 | # Learn more about sampling policy evaluations here:
15 | # https://docs.spacelift.io/concepts/policy#sampling-policy-inputs
16 | sample := true
17 |
--------------------------------------------------------------------------------
/examples/do-not-delete-stateful-resources.rego:
--------------------------------------------------------------------------------
1 | package spacelift
2 |
3 | import future.keywords.in
4 |
5 | # This policy is a plan policy, it will validate the resources during the plan phase.
6 | # More details at: https://docs.spacelift.io/concepts/policy/terraform-plan-policy
7 | # The "deny" rule fires when a specified resource is being deleted.
8 | # The result is a formatted message with the address of the offending resource.
9 |
10 | deny[sprintf(message, [resource.address])] {
11 | # Define the error message format
12 | message := "do not delete %s"
13 |
14 | # Loop over each resource change in the plan
15 | resource := input.terraform.resource_changes[_]
16 |
17 | # Define a set of resource types for which deletions should be prevented.
18 | prevent_delete := {
19 | "aws_db_instance",
20 | "aws_efs_file_system",
21 | "aws_dynamodb_table",
22 | "aws_s3_bucket",
23 | }
24 |
25 | # Check if the resource type is one of those defined in prevent_delete
26 | prevent_delete[resource.type]
27 |
28 | # Check if any of the actions on the resource is "delete"
29 | "delete" in resource.change.actions
30 | }
31 |
32 | # Learn more about sampling policy evaluations here:
33 | # https://docs.spacelift.io/concepts/policy#sampling-policy-inputs
34 | sample := true
35 |
--------------------------------------------------------------------------------
/examples/drift-detection.rego:
--------------------------------------------------------------------------------
1 | package spacelift
2 |
3 | slack[{"channel_id": "C000000"}] {
4 | # Checking if drift detection is present in the run update
5 | input.run_updated.run.drift_detection
6 |
7 | # Condition to verify that there is at least one change
8 | count(input.run_updated.run.changes) > 0
9 | }
10 |
11 | sample := true
12 |
--------------------------------------------------------------------------------
/examples/enforce-module-use-policy.rego:
--------------------------------------------------------------------------------
1 | package spacelift
2 |
3 | import future.keywords.in
4 |
5 | # Note: This policy requires the configuration of your terraform state to be provided. In this policy,
6 | # we reference this via `input.third_party_metadata.custom.configuration` (line 56 below)
7 | # which is provided to the policy if you follow the instructions documented here:
8 | # https://docs.spacelift.io/concepts/policy/terraform-plan-policy#example-exposing-terraform-configuration-to-the-plan-policy
9 |
10 | # TODO: Upgrade to take into account approved versions of the approved modules, and implement
11 | # ability to warn on deprecated versions, and deny on no longer supported versions!
12 |
13 | # This is a map of resource types and the list of modules which are
14 | # approved to be used to create them. Note, you do not need to allow explicitly
15 | # any "wrapper" modules... this is checking the immediate module parent of the resource itself
16 | # Some example resources and approved module(s), you of course can specify your spacelift.io hosted modules
17 | controlled_resource_types := {
18 | "aws_s3_bucket": ["terraform-aws-modules/s3-bucket/aws"],
19 | "aws_s3_bucket_acl": ["terraform-aws-modules/s3-bucket/aws"],
20 | "aws_s3_bucket_website_configuration": ["terraform-aws-modules/s3-bucket/aws"],
21 | }
22 |
23 | # Deny ability to create the resource directly (aka not in a module we identify)
24 | deny[reason] {
25 | resource := input.terraform.resource_changes[_]
26 | actions := {"create", "update"}
27 | actions[resource.change.actions[_]]
28 | module_source = controlled_resource_types[resource.type]
29 | not resource.module_address
30 | reason := sprintf(
31 | "Resource '%s' cannot be created directly. Module(s) '%s' must be used instead",
32 | [resource.address, concat("', '", controlled_resource_types[resource.type])],
33 | )
34 | }
35 |
36 | # Deny ability to create the resource in an unapproved module
37 | deny[failed_reasons] {
38 | # Did any of the resources fail to pass?
39 | count(invalid_resources[_]) > 0
40 |
41 | # Build a list of reasons for each failure
42 | failed_reasons := [sprintf(
43 | "Resource '%s' in top level module named '%s' is being created with the incorrect terraform module '%s'. Module(s) '%s' must be used instead.",
44 | [reason.resource_type, reason.top_level_module_name, reason.resource_module_name, concat("', '", controlled_resource_types[reason.resource_type])],
45 | ) |
46 | reason := invalid_resources[_]
47 | ][_]
48 | }
49 |
50 | # Walk the "configuration" tree and find all the resources which appear in our
51 | # "controlled_resource_types"
52 | controlled_resources[resource] {
53 | # Recursively walk the module hierarchy
54 | [path, module_ref] := walk(input.third_party_metadata.custom.configuration)
55 |
56 | # Filter out only objects that are modules, i.e. have a source property
57 | source := module_ref.source
58 |
59 | # Get all resources created in the module and their types
60 | resources := module_ref.module.resources
61 | resource_type := resources[_].type
62 |
63 | # Filter out resources that are considered controlled
64 | resource_type in object.keys(controlled_resource_types)
65 |
66 | # Create an object with the module and the resource type,
67 | # this is a set so duplicates will be removed based on this object
68 | resource := {
69 | "resource_module_name": source,
70 | "top_level_module_name": path[2],
71 | "resource_type": resource_type,
72 | "resource_module_version_constraint": module_ref.version_constraint,
73 | }
74 | }
75 |
76 | # When the controlled resources are collected, iterate through them and
77 | # see if they comply
78 | invalid_resources[resource_instance] {
79 | resource_instance := controlled_resources[_]
80 | not resource_instance.resource_module_name in controlled_resource_types[resource_instance.resource_type]
81 | }
82 |
--------------------------------------------------------------------------------
/examples/enforce-password-length.rego:
--------------------------------------------------------------------------------
1 | package spacelift
2 |
3 | import future.keywords.in
4 |
5 | # This example plan policy prevents you from creating weak passwords, and warns
6 | # you when passwords are meh.
7 | #
8 | # You can read more about plan policies here:
9 | # https://docs.spacelift.io/concepts/policy/terraform-plan-policy
10 |
11 | deny[sprintf("We require that passwords have at least 16 characters (%s)", [resource.address])] {
12 | resource := new_password[_]
13 | resource.change.after.length < 16
14 | }
15 |
16 | warn[sprintf("We advise that passwords have at least 20 characters (%s)", [resource.address])] {
17 | resource := new_password[_]
18 | resource.change.after.length < 20
19 | }
20 |
21 | new_password[resource] {
22 | resource := input.terraform.resource_changes[_]
23 | "create" in resource.change.actions
24 | resource.type == "random_password"
25 | }
26 |
27 | # Learn more about sampling policy evaluations here:
28 | # https://docs.spacelift.io/concepts/policy#sampling-policy-inputs
29 | sample := true
30 |
--------------------------------------------------------------------------------
/examples/ignore-changes-outside-root.rego:
--------------------------------------------------------------------------------
1 | package spacelift
2 |
3 | import rego.v1
4 |
5 | # This example Git push policy ignores all changes that are outside a project's
6 | # root. Other than that, it follows the defaults - pushes to the tracked branch
7 | # trigger tracked runs, pushes to all other branches trigger proposed runs, tag
8 | # pushes are ignored.
9 | #
10 | # You can read more about push policies here:
11 | # https://docs.spacelift.io/concepts/policy/git-push-policy
12 |
13 | track if {
14 | affected
15 | input.push.branch == input.stack.branch
16 | }
17 |
18 | propose if {
19 | affected
20 | }
21 |
22 | ignore if {
23 | not affected
24 | }
25 |
26 | ignore if {
27 | input.push.tag != ""
28 | }
29 |
30 | # Here's a definition of an affected file - its path must both:
31 | # a) start with the Stack's project root, and;
32 | # b) end with ".tf", indicating that it's a Terraform source file;
33 | affected if {
34 | filepath := input.push.affected_files[_]
35 |
36 | startswith(filepath, input.stack.project_root)
37 | endswith(filepath, ".tf")
38 | }
39 |
40 | # Learn more about sampling policy evaluations here:
41 | # https://docs.spacelift.io/concepts/policy#sampling-policy-inputs
42 | sample := true
43 |
--------------------------------------------------------------------------------
/examples/notification-stack-failure-origins.rego:
--------------------------------------------------------------------------------
1 | # Notify author of PR/commit which potentially introduced failure of a Spacelift Stack
2 |
3 | package spacelift
4 |
5 | import future.keywords.contains
6 | import future.keywords.if
7 |
8 | run_url := sprintf(
9 | "https://%s.app.spacelift.io/stack/%s/run/%s",
10 | [input.account.name, input.run_updated.stack.id, input.run_updated.run.id],
11 | )
12 |
13 | # Mapping of GitHub usernames to Slack user IDs since display names can't be tagged
14 | # https://api.slack.com/reference/surfaces/formatting#mentioning-users
15 | github_to_slack := {
16 | "GITHUB_USERNAME_EXAMPLE1": "SLACK_ID_EXAMPLE1",
17 | "GITHUB_USERNAME_EXAMPLE2": "SLACK_ID_EXAMPLE2",
18 | }
19 |
20 | github_author := input.run_updated.run.commit.author
21 |
22 | # Function to get Slack ID corresponding to GH username from the map if exists, else, mention @here for Slack notification
23 | retrieve_slack_user_id(github_username) := slack_id if {
24 | slack_id := github_to_slack[github_username]
25 | } else := "here"
26 |
27 | # Ensure this is a tracked run and it failed
28 | tracked_failed if {
29 | input.run_updated.run.type == "TRACKED"
30 | input.run_updated.run.state == "FAILED"
31 | }
32 |
33 | # Notify via Slack of the failure, mentioning author of the tracked commit that Spacelift ran on
34 | # Hyperlinks defined as - https://api.slack.com/reference/surfaces/formatting#linking-urls
35 | slack contains {
36 | "channel_id": "YOUR_SLACK_CHANNEL",
37 | "message": sprintf(
38 | concat("", [
39 | "⚠️Spacelift Stack Failure⚠️ - `%s`\n",
40 | "Hey <@%s>, your commit/PR introduced changes that potentially caused the failure of the `%s` stack.\n",
41 | "Check out the <%s|run details here.>\n",
42 | "Tracked Commit: <%s|%s>",
43 | ]),
44 | [
45 | input.run_updated.stack.name,
46 | slack_user_id,
47 | input.run_updated.stack.name,
48 | run_url,
49 | input.run_updated.run.commit.url,
50 | input.run_updated.run.commit.hash,
51 | ],
52 | ),
53 | } if {
54 | tracked_failed
55 | slack_user_id := retrieve_slack_user_id(github_author)
56 | }
57 |
58 | # Write a comment on the PR that introduced the change where this stack failed
59 | # Use `deduplication_key` to update existing comment related to stack in place, so multiple failures of the same stack won't comment again
60 | pull_request contains {
61 | "commit": input.run_updated.run.commit.hash,
62 | "body": sprintf(
63 | concat("", [
64 | "## ⚠️Spacelift Stack Failure⚠️\n",
65 | "@%s - This pull request was deployed and the following Spacelift stack failed:\n",
66 | "* ⛔️ `%s` - [view the run here](%s) ⛔️",
67 | ]),
68 | [
69 | github_author,
70 | input.run_updated.stack.id,
71 | run_url,
72 | ],
73 | ),
74 | "deduplication_key": deduplication_key,
75 | } if {
76 | tracked_failed
77 | deduplication_key := input.run_updated.stack.id
78 | }
79 |
80 | sample := true
81 |
--------------------------------------------------------------------------------
/examples/readers-writers-admins-teams.rego:
--------------------------------------------------------------------------------
1 | package spacelift
2 |
3 | import future.keywords.contains
4 | import future.keywords.if
5 | import future.keywords.in
6 |
7 | # Define team roles
8 | admins := {"team1", "team2", "team3"}
9 |
10 | writers := {"team4", "team5", "team6"}
11 |
12 | readers := {"team7", "team8", "team9"}
13 |
14 | # Space access rules
15 | # Admin access rule - highest priority
16 | space_admin contains space.id if {
17 | some space in input.spaces
18 | some login in input.session.teams
19 | admins[login] # User is an admin
20 | }
21 |
22 | # Writer access rule - second priority
23 | # Only consider this rule if the user is not an admin
24 | space_write contains space.id if {
25 | some space in input.spaces
26 | some login in input.session.teams
27 | writers[login] # User is a writer
28 | not admins[login] # Ensure user is not an admin
29 | }
30 |
31 | # Reader access rule - third priority
32 | # Only consider this rule if the user is neither an admin nor a writer
33 | space_read contains space.id if {
34 | some space in input.spaces
35 | some login in input.session.teams
36 | readers[login] # User is a reader
37 | not admins[login] # Ensure user is not an admin
38 | not writers[login] # Ensure user is not a writer
39 | }
40 |
--------------------------------------------------------------------------------
/examples/tests/cancel-in-progress-runs_test.rego:
--------------------------------------------------------------------------------
1 | package spacelift
2 |
3 | test_cancel_runs_allowed {
4 | cancel.test with input as {
5 | "pull_request": {"head": {"branch": "main"}},
6 | "in_progress": [{
7 | "id": "test",
8 | "type": "PROPOSED",
9 | "state": "QUEUED",
10 | "branch": "main",
11 | }],
12 | }
13 | }
14 |
15 | test_cancel_runs_denied {
16 | not cancel.test with input as {
17 | "pull_request": {"head": {"branch": "feature/example"}},
18 | "in_progress": [{
19 | "id": "test",
20 | "type": "PROPOSED",
21 | "state": "QUEUED",
22 | "branch": "main",
23 | }],
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/examples/tests/do-not-delete-stateful-resources_test.rego:
--------------------------------------------------------------------------------
1 | package spacelift
2 |
3 | # Test case for an aws_s3_bucket being deleted
4 | test_deny_s3_bucket_deletion {
5 | # Assert deny rule fires with the expected message
6 | deny["do not delete aws_s3_bucket.my_bucket"] with input as {"terraform": {"resource_changes": [{
7 | "address": "aws_s3_bucket.my_bucket",
8 | "type": "aws_s3_bucket",
9 | "change": {"actions": ["delete"]},
10 | }]}}
11 | }
12 |
13 | # Test case for an aws_db_instance being deleted
14 | test_deny_db_instance_deletion {
15 | # Assert deny rule fires with the expected message
16 | deny["do not delete aws_db_instance.my_rds"] with input as {"terraform": {"resource_changes": [{
17 | "address": "aws_db_instance.my_rds",
18 | "type": "aws_db_instance",
19 | "change": {"actions": ["delete"]},
20 | }]}}
21 | }
22 |
23 | # Test case for an aws_efs_file_system being deleted
24 | test_deny_efs_file_system_deletion {
25 | # Assert deny rule fires with the expected message
26 | deny["do not delete aws_efs_file_system.my_efs"] with input as {"terraform": {"resource_changes": [{
27 | "address": "aws_efs_file_system.my_efs",
28 | "type": "aws_efs_file_system",
29 | "change": {"actions": ["delete"]},
30 | }]}}
31 | }
32 |
33 | # Test case for an aws_dynamodb_table being deleted
34 | test_deny_dynamodb_table_deletion {
35 | # Assert deny rule fires with the expected message
36 | deny["do not delete aws_dynamodb_table.my_table"] with input as {"terraform": {"resource_changes": [{
37 | "address": "aws_dynamodb_table.my_table",
38 | "type": "aws_dynamodb_table",
39 | "change": {"actions": ["delete"]},
40 | }]}}
41 | }
42 |
43 | # Test case for an aws_instance being deleted (which should not be denied)
44 | test_allow_instance_deletion {
45 | # Assert deny rule does not fire
46 | count(deny) == 0 with input as {"terraform": {"resource_changes": [{
47 | "address": "aws_instance.my_instance",
48 | "type": "aws_instance",
49 | "change": {"actions": ["delete"]},
50 | }]}}
51 | }
52 |
--------------------------------------------------------------------------------
/examples/tests/enforce-module-use-policy_test.rego:
--------------------------------------------------------------------------------
1 | package spacelift
2 |
3 | # Test case for denying creation of controlled resource type.
4 | test_deny_creation_of_controlled_resource_type {
5 | deny["Resource 'aws_s3_bucket.bucket_1' cannot be created directly. Module(s) 'terraform-aws-modules/s3-bucket/aws' must be used instead"] with input as {"terraform": {"resource_changes": [{
6 | "address": "aws_s3_bucket.bucket_1",
7 | "type": "aws_s3_bucket",
8 | "change": {"actions": ["create"]},
9 | }]}}
10 | }
11 |
12 | # Test case for update creation of controlled resource type.
13 | test_deny_update_of_controlled_resource_type {
14 | deny["Resource 'aws_s3_bucket.bucket_1' cannot be created directly. Module(s) 'terraform-aws-modules/s3-bucket/aws' must be used instead"] with input as {"terraform": {"resource_changes": [{
15 | "address": "aws_s3_bucket.bucket_1",
16 | "type": "aws_s3_bucket",
17 | "change": {"actions": ["update"]},
18 | }]}}
19 | }
20 |
21 | # Test case for allowing deletion of controlled resource type.
22 | # TO ADD BACK
23 |
24 | # Test case for allowing creation of uncontrolled resource type.
25 | test_allow_creation_of_uncontrolled_resource_type {
26 | count(deny) == 0 with input as {"terraform": {"resource_changes": [{
27 | "address": "aws_ecs_cluster.one",
28 | "type": "aws_ecs_cluster",
29 | "change": {"actions": ["create"]},
30 | }]}}
31 | }
32 |
--------------------------------------------------------------------------------
/examples/tests/enforce-password-length_test.rego:
--------------------------------------------------------------------------------
1 | package spacelift
2 |
3 | # Test case for denying creation of a password with less than 16 characters.
4 | test_deny_creation_of_password_with_less_than_16_characters {
5 | deny["We require that passwords have at least 16 characters (random_password.password_1)"] with input as {"terraform": {"resource_changes": [{
6 | "address": "random_password.password_1",
7 | "type": "random_password",
8 | "change": {
9 | "actions": ["create"],
10 | "after": {"length": 1},
11 | },
12 | }]}}
13 | }
14 |
15 | # Test case for warning creation of a password between 16 and 20 characters.
16 | test_warn_creation_of_password_between_16_and_20_characters {
17 | warn["We advise that passwords have at least 20 characters (random_password.password_1)"] with input as {"terraform": {"resource_changes": [{
18 | "address": "random_password.password_1",
19 | "type": "random_password",
20 | "change": {
21 | "actions": ["create"],
22 | "after": {"length": 18},
23 | },
24 | }]}}
25 | }
26 |
27 | # Test case for allowing creation of a password longer than 20 characters.
28 | test_allow_creation_of_password_longer_than_20_characters {
29 | count(warn) == 0 with input as {"terraform": {"resource_changes": [{
30 | "address": "random_password.password_1",
31 | "type": "random_password",
32 | "change": {
33 | "actions": ["create"],
34 | "after": {"length": 21},
35 | },
36 | }]}}
37 | }
38 |
--------------------------------------------------------------------------------
/examples/tests/ignore-changes-outside-root_test.rego:
--------------------------------------------------------------------------------
1 | package spacelift
2 |
3 | test_affected_no_files {
4 | not affected with input as {
5 | "stack": {"project_root": ""},
6 | "push": {"affected_files": []},
7 | }
8 | }
9 |
10 | test_affected_tf_files {
11 | affected with input as {
12 | "stack": {"project_root": ""},
13 | "push": {"affected_files": ["main.tf", "stacks.tf"]},
14 | }
15 | }
16 |
17 | test_affected_no_tf_files {
18 | not affected with input as {
19 | "stack": {"project_root": ""},
20 | "push": {"affected_files": ["README", "myicon.png"]},
21 | }
22 | }
23 |
24 | test_affected_outside_project_root {
25 | not affected with input as {
26 | "stack": {"project_root": "stacks/my-stack"},
27 | "push": {"affected_files": ["stacks/another-stack/main.tf"]},
28 | }
29 | }
30 |
31 | test_ignore_affected {
32 | ignore with affected as false
33 | }
34 |
35 | test_ignore_not_affected {
36 | not ignore with affected as true
37 | }
38 |
39 | test_ignore_tag {
40 | ignore with input as {"push": {"tag": "v1.0.0"}}
41 | with affected as true
42 | }
43 |
44 | test_propose_affected {
45 | propose with affected as true
46 | }
47 |
48 | test_propose_not_affected {
49 | not propose with affected as false
50 | }
51 |
52 | matching_branch_input := {
53 | "push": {"branch": "main"},
54 | "stack": {"branch": "main"},
55 | }
56 |
57 | test_track_affected {
58 | track with input as matching_branch_input with affected as true
59 | }
60 |
61 | test_track_not_affected {
62 | not track with input as matching_branch_input with affected as false
63 | }
64 |
65 | test_track_not_stack_branch {
66 | not track with input as {
67 | "push": {"branch": "my-feature"},
68 | "stack": {"branch": "main"},
69 | }
70 | with affected as true
71 | }
72 |
--------------------------------------------------------------------------------
/examples/tests/notification-stack-failure-origins_test.rego:
--------------------------------------------------------------------------------
1 |
2 | package spacelift
3 |
4 | import future.keywords.contains
5 | import future.keywords.if
6 | import future.keywords.in
7 |
8 | import data.spacelift
9 |
10 | # Test successful Slack notification for a tracked, failed run
11 | test_slack_notification_for_tracked_failed_run if {
12 | result := spacelift.slack with input as {
13 | "account": {"name": "test-account"},
14 | "run_updated": {
15 | "stack": {"id": "test-stack", "name": "Test Stack"},
16 | "run": {
17 | "id": "test-run",
18 | "type": "TRACKED",
19 | "state": "FAILED",
20 | "commit": {
21 | "author": "GITHUB_USERNAME_EXAMPLE1",
22 | "hash": "abcdef123456",
23 | "url": "https://github.com/org/repo/commit/abcdef123456",
24 | },
25 | },
26 | },
27 | }
28 |
29 | count(result) == 1
30 | slack_message := result[_]
31 | slack_message.channel_id == "YOUR_SLACK_CHANNEL"
32 | contains(slack_message.message, "⚠️Spacelift Stack Failure⚠️ - `Test Stack`")
33 | contains(slack_message.message, "Hey <@SLACK_ID_EXAMPLE1>")
34 | contains(slack_message.message, "https://test-account.app.spacelift.io/stack/test-stack/run/test-run")
35 | }
36 |
37 | # Test no Slack notification for a non-tracked run
38 | test_no_slack_notification_for_non_tracked_run if {
39 | result := spacelift.slack with input as {"run_updated": {"run": {
40 | "type": "PROPOSED",
41 | "state": "FAILED",
42 | }}}
43 |
44 | count(result) == 0
45 | }
46 |
47 | # Test no Slack notification for a successful FINISHED run
48 | test_no_slack_notification_for_successful_run if {
49 | result := spacelift.slack with input as {"run_updated": {"run": {
50 | "type": "TRACKED",
51 | "state": "FINISHED",
52 | }}}
53 |
54 | count(result) == 0
55 | }
56 |
57 | # Test Slack notification with unknown GitHub user which should route to @here
58 | test_slack_notification_with_unknown_github_user if {
59 | result := spacelift.slack with input as {
60 | "account": {"name": "test-account"},
61 | "run_updated": {
62 | "stack": {"id": "test-stack", "name": "Test Stack"},
63 | "run": {
64 | "id": "test-run",
65 | "type": "TRACKED",
66 | "state": "FAILED",
67 | "commit": {
68 | "author": "unknown-user",
69 | "hash": "abcdef123456",
70 | "url": "https://github.com/org/repo/commit/abcdef123456",
71 | },
72 | },
73 | },
74 | }
75 |
76 | count(result) == 1
77 | slack_message := result[_]
78 | contains(slack_message.message, "Hey <@here>")
79 | }
80 |
81 | # Test PR comment for a tracked, failed run
82 | test_pr_comment_for_tracked_failed_run if {
83 | result := spacelift.pull_request with input as {
84 | "account": {"name": "test-account"},
85 | "run_updated": {
86 | "stack": {"id": "test-stack", "name": "Test Stack"},
87 | "run": {
88 | "id": "test-run",
89 | "type": "TRACKED",
90 | "state": "FAILED",
91 | "commit": {
92 | "author": "GITHUB_USERNAME_EXAMPLE1",
93 | "hash": "abcdef123456",
94 | "url": "https://github.com/org/repo/commit/abcdef123456",
95 | },
96 | },
97 | },
98 | }
99 |
100 | count(result) == 1
101 | pr_comment := result[_]
102 | pr_comment.commit == "abcdef123456"
103 | contains(pr_comment.body, "## ⚠️Spacelift Stack Failure⚠️")
104 | contains(pr_comment.body, "@GITHUB_USERNAME_EXAMPLE1")
105 | contains(pr_comment.body, "⛔️ `test-stack`")
106 | contains(pr_comment.body, "https://test-account.app.spacelift.io/stack/test-stack/run/test-run")
107 | pr_comment.deduplication_key == "test-stack"
108 | }
109 |
110 | # Test no PR comment for a non-tracked run
111 | test_no_pr_comment_for_non_tracked_run if {
112 | result := spacelift.pull_request with input as {"run_updated": {"run": {
113 | "type": "PROPOSED",
114 | "state": "FAILED",
115 | }}}
116 |
117 | count(result) == 0
118 | }
119 |
120 | # Test no PR comment for a successful run
121 | test_no_pr_comment_for_successful_run if {
122 | result := spacelift.pull_request with input as {"run_updated": {"run": {
123 | "type": "TRACKED",
124 | "state": "FINISHED",
125 | }}}
126 |
127 | count(result) == 0
128 | }
129 |
--------------------------------------------------------------------------------
/examples/tests/readers-writers-admins-teams_test.rego:
--------------------------------------------------------------------------------
1 | package spacelift_test
2 |
3 | # Import the spacelift package to access its rules.
4 | import data.spacelift
5 | import future.keywords.contains
6 | import future.keywords.if
7 | import future.keywords.in
8 |
9 | test_allow_writers if {
10 | spacelift.space_write with input as {"session": {"teams": ["team4"]}}
11 | }
12 |
13 | test_allow_admins if {
14 | spacelift.space_admin with input as {"session": {"teams": ["team1"]}}
15 | }
16 |
17 | test_allow_readers if {
18 | spacelift.space_read with input as {"session": {"teams": ["team7"]}}
19 | }
20 |
21 | # Test space access for admins
22 | test_space_admin_access if {
23 | spacelift.space_admin with input as {"session": {"teams": ["team4"]}, "spaces": [{"id": "space1"}]}
24 | }
25 |
26 | # Test space access for writers
27 | test_space_write_access if {
28 | spacelift.space_write with input as {"session": {"teams": ["team1"]}, "spaces": [{"id": "space1"}]}
29 | }
30 |
31 | # Test space access for readers
32 | test_space_read_access if {
33 | spacelift.space_read with input as {"session": {"teams": ["team7"]}, "spaces": [{"id": "space1"}]}
34 | }
35 |
--------------------------------------------------------------------------------
/examples/tests/track-using-labels_test.rego:
--------------------------------------------------------------------------------
1 | package spacelift
2 |
3 | # Mocked Data for Tests
4 | mock_push_affected := {"affected_files": ["tracked/file.txt", "untracked/file.txt"]}
5 |
6 | mock_push_not_affected := {"affected_files": ["not_affected/file.txt"]}
7 |
8 | mock_stack_labels_with_trackings := {"labels": ["trackeddirectories:tracked", "trackedextensions:.txt"]}
9 |
10 | # Tests
11 |
12 | # Test: track rule with different branches
13 | test_track_different_branches {
14 | not track with input as {
15 | "push": {"branch": "main"},
16 | "stack": {"branch": "develop"},
17 | }
18 | }
19 |
20 | # Test: propose rule with non-empty branch
21 | test_propose_non_empty_branch {
22 | propose with input as {
23 | "push": {"branch": "main"},
24 | "stack": {},
25 | }
26 | }
27 |
28 | # Test: propose rule with empty branch
29 | test_propose_empty_branch {
30 | not propose with input as {
31 | "push": {"branch": ""},
32 | "stack": {},
33 | }
34 | }
35 |
36 | # Test: affected by directory path
37 | test_affected_directory {
38 | affected with input as {
39 | "push": mock_push_affected,
40 | "stack": mock_stack_labels_with_trackings,
41 | }
42 | }
43 |
44 | # Test: affected by file extension
45 | test_affected_extension {
46 | affected with input as {
47 | "push": mock_push_affected,
48 | "stack": mock_stack_labels_with_trackings,
49 | }
50 | }
51 |
52 | # Test: not track by directory path
53 | test_not_affected_directory {
54 | not track with input as {
55 | "push": mock_push_not_affected,
56 | "stack": mock_stack_labels_with_trackings,
57 | }
58 | }
59 |
60 | # Test: not track by file extension
61 | test_not_affected_extension {
62 | not track with input as {
63 | "push": mock_push_not_affected,
64 | "stack": mock_stack_labels_with_trackings,
65 | }
66 | }
67 |
68 | # Test: track rule with not affected files
69 | test_ignore_not_affected {
70 | not track with input as {
71 | "push": mock_push_not_affected,
72 | "stack": mock_stack_labels_with_trackings,
73 | }
74 | }
75 |
--------------------------------------------------------------------------------
/examples/track-using-labels.rego:
--------------------------------------------------------------------------------
1 | package spacelift
2 |
3 | track {
4 | input.push.branch == input.stack.branch
5 | affected
6 | }
7 |
8 | propose {
9 | input.push.branch != ""
10 | }
11 |
12 | ignore {
13 | not affected
14 | }
15 |
16 | # Extract and use tracked directories and extensions from labels
17 | tracked_directories[tracked_directory] {
18 | label := input.stack.labels[_]
19 | startswith(label, "trackeddirectories:")
20 | tracked_directory := split(label, ":")[1]
21 | }
22 |
23 | tracked_extensions[tracked_extension] {
24 | label := input.stack.labels[_]
25 | startswith(label, "trackedextensions:")
26 | tracked_extension := split(label, ":")[1]
27 | }
28 |
29 | affected {
30 | some i, j
31 | path := input.push.affected_files[i]
32 | startswith(path, tracked_directories[j])
33 | }
34 |
35 | affected {
36 | some i, j
37 | path := input.push.affected_files[i]
38 | endswith(path, tracked_extensions[j])
39 | }
40 |
41 | sample := true
42 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "opa-rego-test-action",
3 | "scripts": {
4 | "build": "npx ncc build src/index.ts -o dist --source-map",
5 | "test": "npx jest",
6 | "local": "npx ts-node src/index.ts",
7 | "local:resultProcessing": "npx ts-node ./src/testResultProcessing.ts"
8 | },
9 | "author": "Masterpoint",
10 | "license": "MIT",
11 | "devDependencies": {
12 | "@types/jest": "^29.5.12",
13 | "@types/node": "^22.1.0",
14 | "jest": "^29.7.0",
15 | "ts-jest": "^29.2.4",
16 | "typescript": "^5.5.4"
17 | },
18 | "dependencies": {
19 | "@actions/core": "^1.10.1",
20 | "@actions/exec": "^1.1.1",
21 | "@vercel/ncc": "^0.38.1"
22 | },
23 | "jest": {
24 | "preset": "ts-jest",
25 | "testEnvironment": "node",
26 | "moduleFileExtensions": [
27 | "ts",
28 | "tsx",
29 | "js",
30 | "jsx",
31 | "json",
32 | "node"
33 | ],
34 | "setupFiles": [
35 | "./.jest/setupTests.ts"
36 | ],
37 | "testPathIgnorePatterns": [
38 | "/node_modules/",
39 | "/__tests__/mockResults.ts"
40 | ]
41 | }
42 | }
43 |
--------------------------------------------------------------------------------
/src/formatResults.ts:
--------------------------------------------------------------------------------
1 | import { ProcessedTestResult, ProcessedCoverageResult } from "./interfaces";
2 |
3 | /**
4 | * Formats the test results and coverage results into a Markdown table for GitHub comments.
5 | * @param results - The processed test results.
6 | * @param coverageResults - The processed coverage results.
7 | * @param showCoverage - Whether to include coverage information in the output.
8 | * @returns A string containing the formatted Markdown table.
9 | */
10 | export function formatResults(
11 | results: ProcessedTestResult[],
12 | coverageResults: ProcessedCoverageResult[],
13 | showCoverage: boolean,
14 | ): string {
15 | let output = `# ${process.env.pr_comment_title || "🧪 OPA Rego Policy Test Results"}\n\n`;
16 |
17 | // Build header row
18 | let header = "| File | Status | Passed | Total |";
19 | let separator = "|------|--------|--------|-------|";
20 |
21 | if (showCoverage) {
22 | header += " Coverage |";
23 | separator += "----------|";
24 | }
25 | header += " Details |\n";
26 | separator += "----------|\n";
27 | output += header;
28 | output += separator;
29 |
30 | for (const result of results) {
31 | let statusEmoji, statusText;
32 | switch (result.status) {
33 | case "PASS":
34 | statusEmoji = "✅";
35 | statusText = `${statusEmoji} PASS`;
36 | break;
37 | case "FAIL":
38 | statusEmoji = "❌";
39 | statusText = `${statusEmoji} FAIL`;
40 | break;
41 | case "NO TESTS":
42 | statusEmoji = "⚠️";
43 | statusText = `${statusEmoji} NO TESTS`;
44 | break;
45 | }
46 |
47 | const testFileName = result.file;
48 | const details =
49 | result.status === "NO TESTS"
50 | ? "No test file found"
51 | : result.details.join("
");
52 | const detailsColumn = `Show Details
${details} `;
53 |
54 | let row = `| ${testFileName} | ${statusText} | ${result.passed} | ${result.total} |`;
55 |
56 | if (showCoverage) {
57 | const coverageInfo = coverageResults.find((cr) => {
58 | const lastSlashIndex = cr.file.lastIndexOf("/");
59 | const dotRegoIndex = cr.file.lastIndexOf(".rego");
60 |
61 | if (lastSlashIndex === -1 || dotRegoIndex === -1) return false;
62 |
63 | const fileNameWithoutExtension = cr.file.slice(
64 | lastSlashIndex + 1,
65 | dotRegoIndex,
66 | );
67 | return (
68 | testFileName.includes(fileNameWithoutExtension) &&
69 | !cr.file.includes(testFileName)
70 | );
71 | });
72 |
73 | let coverageText = "N/A";
74 | let uncoveredLinesDetails = "";
75 | if (coverageInfo && result.status === "PASS") {
76 | try {
77 | coverageText = `${coverageInfo.coverage.toFixed(2)}%`;
78 | if (
79 | coverageInfo.notCoveredLines &&
80 | coverageInfo.notCoveredLines !== "N/A"
81 | ) {
82 | uncoveredLinesDetails = ` Uncovered Lines
${coverageInfo.notCoveredLines} `;
83 | }
84 | coverageText += uncoveredLinesDetails; // Combine text and details
85 | } catch (error) {
86 | console.error("Error processing coverage information:", error);
87 | console.log("Coverage Info:", coverageInfo);
88 | }
89 | }
90 | row += ` ${coverageText} |`;
91 | }
92 |
93 | row += ` ${detailsColumn} |\n`;
94 | output += row;
95 | }
96 |
97 | if (process.env.indicate_source_message === "true") {
98 | output +=
99 | "\n\nReport generated by [🧪 GitHub Actions for OPA Rego Test](https://github.com/masterpointio/github-action-opa-rego-test)";
100 | }
101 |
102 | return output;
103 | }
104 |
--------------------------------------------------------------------------------
/src/index.ts:
--------------------------------------------------------------------------------
1 | import {
2 | processTestResults,
3 | processCoverageReport,
4 | } from "./testResultProcessing";
5 | import {
6 | executeIndividualOpaTests,
7 | executeOpaTestByDirectory,
8 | } from "./opaCommands";
9 | import { formatResults } from "./formatResults";
10 |
11 | import { ProcessedTestResult, ProcessedCoverageResult } from "./interfaces";
12 |
13 | import * as core from "@actions/core";
14 |
15 | const errorString =
16 | "⛔️⛔️ An unknown error has occurred in generating the results, either from tests failing or an error running OPA or an issue with GItHub actions. View the logs for more information. ⛔️⛔️";
17 |
18 | export async function main() {
19 | try {
20 | const test_mode = process.env.test_mode;
21 | const reportNoTestFiles = process.env.report_untested_files === "true";
22 | const noTestFiles = process.env.no_test_files;
23 | const runCoverageReport = process.env.run_coverage_report === "true";
24 | const path = process.env.path;
25 | const test_file_postfix = process.env.test_file_postfix;
26 |
27 | if (!path || !test_file_postfix) {
28 | throw new Error(
29 | "Both 'path' and 'test_file_postfix' environment variables must be set.",
30 | );
31 | }
32 |
33 | let opaOutput: string = "";
34 | let opaError: string = "";
35 | let exitCode: number = 0;
36 | let coverageOutput: string | undefined;
37 |
38 | if (test_mode === "directory") {
39 | ({
40 | output: opaOutput,
41 | error: opaError,
42 | exitCode: exitCode,
43 | coverageOutput: coverageOutput,
44 | } = await executeOpaTestByDirectory(path, true));
45 | } else {
46 | ({
47 | output: opaOutput,
48 | error: opaError,
49 | exitCode: exitCode,
50 | coverageOutput: coverageOutput,
51 | } = await executeIndividualOpaTests(path, test_file_postfix, true));
52 | }
53 |
54 | let parsedResults = processTestResults(JSON.parse(opaOutput));
55 |
56 | let coverageResults: ProcessedCoverageResult[] = [];
57 | if (runCoverageReport) {
58 | if (coverageOutput) {
59 | coverageResults = processCoverageReport(JSON.parse(coverageOutput));
60 | }
61 | }
62 |
63 | // At the end of the table, if the reportNoTestFile flag is on, add all the files that didn't have an associated test with it.
64 | if (noTestFiles && reportNoTestFiles) {
65 | const noTestFileResults: ProcessedTestResult[] = noTestFiles
66 | .split("\n")
67 | .map((file) => ({
68 | file: file.trim(),
69 | status: "NO TESTS",
70 | passed: 0,
71 | total: 0,
72 | details: [],
73 | }));
74 | parsedResults = [...parsedResults, ...noTestFileResults];
75 | }
76 |
77 | let formattedOutput = formatResults(
78 | parsedResults,
79 | coverageResults,
80 | runCoverageReport,
81 | );
82 |
83 | if (formattedOutput === "") {
84 | formattedOutput = errorString;
85 | }
86 |
87 | // This is the output that will be used in the GitHub Pull Request comment.
88 | core.setOutput("parsed_results", formattedOutput);
89 |
90 | const testsFailed = parsedResults.some(
91 | (result) => result.status === "FAIL",
92 | );
93 | core.setOutput("tests_failed", testsFailed.toString());
94 |
95 | if (testsFailed) {
96 | core.setFailed(`One or more OPA tests failed: ${opaError}`);
97 | }
98 | } catch (error) {
99 | if (error instanceof Error) {
100 | core.setFailed(`Action failed with error: ${error.message}`);
101 | } else {
102 | core.setFailed("Action failed with an unknown error");
103 | }
104 | }
105 | }
106 |
107 | main();
108 |
--------------------------------------------------------------------------------
/src/interfaces.ts:
--------------------------------------------------------------------------------
1 | export interface ProcessedTestResult {
2 | file: string;
3 | status: "PASS" | "FAIL" | "NO TESTS";
4 | passed: number;
5 | total: number;
6 | details: string[]; // Array of either "✅ test_name" or "❌ test_name" for the test within the file
7 | }
8 |
9 | export interface ProcessedCoverageResult {
10 | file: string;
11 | coverage: number;
12 | notCoveredLines: string;
13 | }
14 |
15 | // This is what is returned from the OPA test command with `--format=json` and `--coverage`
16 | export interface OpaRawJsonCoverageReport {
17 | files: {
18 | [filePath: string]: {
19 | // Array of covered code sections
20 | covered?: Array<{
21 | start: {
22 | row: number;
23 | };
24 | end: {
25 | row: number;
26 | };
27 | }>;
28 |
29 | // Array of code sections that are not covered by tests
30 | not_covered?: Array<{
31 | start: {
32 | row: number;
33 | };
34 | end: {
35 | row: number;
36 | };
37 | }>;
38 |
39 | covered_lines?: number;
40 |
41 | // Total number of lines not covered by tests (only present if there are uncovered lines)
42 | not_covered_lines?: number;
43 |
44 | // Coverage percentage (0-100)
45 | coverage: number;
46 | };
47 | };
48 | }
49 |
50 | // Interface for individual OPA test result - this is what is returned from the OPA test command with --format=json
51 | export interface OpaRawJsonTestResult {
52 | location: {
53 | file: string;
54 | row: number;
55 | col: number;
56 | };
57 | package: string;
58 | name: string;
59 | fail?: boolean;
60 | duration: number;
61 | }
62 |
--------------------------------------------------------------------------------
/src/opaCommands.ts:
--------------------------------------------------------------------------------
1 | import { OpaRawJsonTestResult } from "./interfaces";
2 | import * as exec from "@actions/exec";
3 | import path from "path";
4 |
5 | const opaV0CompatibleFlag = "--v0-compatible"; // https://www.openpolicyagent.org/docs/latest/v0-compatibility/
6 |
7 | export async function executeOpaTestByDirectory(
8 | path: string,
9 | runCoverageReport: boolean = false,
10 | ): Promise<{
11 | output: string;
12 | error: string;
13 | exitCode: number;
14 | coverageOutput?: string;
15 | coverageExitCode?: number;
16 | }> {
17 | let opaOutput = "";
18 | let opaError = "";
19 | let opaCoverageOutput = "";
20 | let exitCode = 0;
21 | let coverageExitCode;
22 |
23 | const options: exec.ExecOptions = {
24 | listeners: {
25 | stdout: (data: Buffer) => {
26 | opaOutput += data.toString();
27 | },
28 | stderr: (data: Buffer) => {
29 | opaError += data.toString();
30 | },
31 | },
32 | ignoreReturnCode: true,
33 | };
34 |
35 | exitCode = await exec.exec(
36 | "opa",
37 | ["test", path, "--format=json", opaV0CompatibleFlag],
38 | options,
39 | );
40 |
41 | if (runCoverageReport) {
42 | const coverageOptions: exec.ExecOptions = {
43 | listeners: {
44 | stdout: (data: Buffer) => {
45 | opaCoverageOutput += data.toString();
46 | },
47 | stderr: (data: Buffer) => {
48 | opaError += `\nCoverage: ${data.toString()}`;
49 | },
50 | },
51 | ignoreReturnCode: true,
52 | };
53 |
54 | coverageExitCode = await exec.exec(
55 | "opa",
56 | ["test", path, "--format=json", "--coverage", opaV0CompatibleFlag],
57 | coverageOptions,
58 | );
59 | } else {
60 | console.log(
61 | "Coverage reporting skipped due to runCoverageReport flag set to false",
62 | );
63 | }
64 |
65 | console.log("OPA test commands completed");
66 |
67 | return {
68 | output: opaOutput,
69 | error: opaError,
70 | exitCode: exitCode,
71 | ...(runCoverageReport && {
72 | coverageOutput: opaCoverageOutput,
73 | coverageExitCode: coverageExitCode,
74 | }),
75 | };
76 | }
77 |
78 | /**
79 | * Run OPA tests on all files matching the given test file postfix in the specified base path.
80 | * @param basePath - The base path to search for test files.
81 | * @param testFilePostfix - The postfix of the test files to look for (e.g., "_test").
82 | * @param runCoverageReport - Whether to run coverage report (default: false).
83 | * @returns An object containing the test results, error messages, and exit codes.
84 | */
85 | export async function executeIndividualOpaTests(
86 | basePath: string,
87 | testFilePostfix: string,
88 | runCoverageReport = false,
89 | ): Promise<{
90 | output: string;
91 | error: string;
92 | exitCode: number;
93 | coverageOutput?: string;
94 | coverageExitCode?: number;
95 | }> {
96 | const allTestResults: OpaRawJsonTestResult[] = [];
97 | let opaError = "";
98 | let exitCode = 0;
99 |
100 | const coverageFiles: Record = {};
101 | let coverageExitCode = 0;
102 |
103 | // ---------- locate test files ----------
104 | let findStdout = "";
105 | let findStderr = "";
106 | await exec.exec(
107 | "find",
108 | [basePath, "-type", "f", "-name", `*${testFilePostfix}.rego`],
109 | {
110 | listeners: {
111 | stdout: (b: Buffer) => (findStdout += b.toString()),
112 | stderr: (b: Buffer) => (findStderr += b.toString()),
113 | },
114 | },
115 | );
116 |
117 | if (findStderr) {
118 | opaError += findStderr + "\n";
119 | exitCode = 1;
120 | }
121 |
122 | const testFiles = findStdout.trim().split("\n").filter(Boolean);
123 |
124 | for (const testFile of testFiles) {
125 | const base = path.basename(testFile, `${testFilePostfix}.rego`);
126 | const dir = path.dirname(testFile);
127 |
128 | // locate impl file
129 | let implOut = "";
130 | await exec.exec(
131 | "find",
132 | [
133 | dir,
134 | `${dir}/..`,
135 | "-maxdepth",
136 | "1",
137 | "-type",
138 | "f",
139 | "-name",
140 | `${base}.rego`,
141 | ],
142 | {
143 | listeners: { stdout: (b: Buffer) => (implOut += b.toString()) },
144 | },
145 | );
146 | const implFile = implOut.trim().split("\n").find(Boolean);
147 | if (!implFile) {
148 | const msg = `Error: Implementation file not found for test: ${testFile}`;
149 | opaError += msg + "\n";
150 | exitCode = 1;
151 | coverageExitCode = 1;
152 | continue;
153 | }
154 |
155 | // -------- Running OPA test --------
156 | let testOutput = "";
157 | let testErrMsg = "";
158 | const testExitCode = await exec.exec(
159 | "opa",
160 | ["test", testFile, implFile, "--format=json", opaV0CompatibleFlag],
161 | {
162 | listeners: {
163 | stdout: (b: Buffer) => (testOutput += b.toString()),
164 | stderr: (b: Buffer) => (testErrMsg += b.toString()),
165 | },
166 | ignoreReturnCode: true,
167 | },
168 | );
169 |
170 | if (testExitCode) exitCode = testExitCode;
171 | if (testErrMsg) opaError += testErrMsg;
172 |
173 | try {
174 | const parsed = JSON.parse(testOutput);
175 | if (Array.isArray(parsed)) allTestResults.push(...parsed);
176 | } catch (e) {
177 | opaError += `Error parsing test results for ${testFile}: ${e}\n`;
178 | exitCode = 1;
179 | }
180 |
181 | // -------- coverage (optional) --------
182 | if (runCoverageReport) {
183 | let covOut = "";
184 | let covErr = "";
185 | const covExit = await exec.exec(
186 | "opa",
187 | [
188 | "test",
189 | testFile,
190 | implFile,
191 | "--coverage",
192 | "--format=json",
193 | opaV0CompatibleFlag,
194 | ],
195 | {
196 | listeners: {
197 | stdout: (b: Buffer) => (covOut += b.toString()),
198 | stderr: (b: Buffer) => (covErr += b.toString()),
199 | },
200 | ignoreReturnCode: true,
201 | },
202 | );
203 | coverageExitCode = Math.max(coverageExitCode, covExit);
204 | if (covErr) opaError += `Coverage error for ${testFile}: ${covErr}`;
205 |
206 | try {
207 | const covJson = JSON.parse(covOut);
208 | if (covJson?.files) {
209 | Object.assign(coverageFiles, covJson.files);
210 | }
211 | } catch (e) {
212 | opaError += `Error parsing coverage for ${testFile}: ${e}\n`;
213 | coverageExitCode = 1;
214 | }
215 | }
216 | }
217 |
218 | return {
219 | output: JSON.stringify(allTestResults),
220 | error: opaError,
221 | exitCode,
222 | ...(runCoverageReport && {
223 | coverageOutput: JSON.stringify({ files: coverageFiles }),
224 | coverageExitCode,
225 | }),
226 | };
227 | }
228 |
--------------------------------------------------------------------------------
/src/testResultProcessing.ts:
--------------------------------------------------------------------------------
1 | import {
2 | ProcessedTestResult,
3 | OpaRawJsonTestResult,
4 | OpaRawJsonCoverageReport,
5 | ProcessedCoverageResult,
6 | } from "./interfaces";
7 |
8 | /**
9 | * Processes the raw JSON test results from OPA and formats them into a structure ready to be formatted into a GitHub Pull Request comment.
10 | * @param opaRawJsonTestResult - The raw JSON test results from OPA, obtained from `opa test --format=json`. This is done in the `opaCommands.ts` file.
11 | * @returns An array of processed test results. See the interface for structure.
12 | */
13 | export function processTestResults(
14 | opaRawJsonTestResult: OpaRawJsonTestResult[],
15 | ): ProcessedTestResult[] {
16 | // Group by file
17 | const fileMap = new Map();
18 |
19 | // Group tests by file
20 | opaRawJsonTestResult.forEach((result) => {
21 | const file = result.location.file;
22 | if (!fileMap.has(file)) {
23 | fileMap.set(file, []);
24 | }
25 | fileMap.get(file)!.push(result);
26 | });
27 |
28 | // Process each file's results
29 | const testResults: ProcessedTestResult[] = [];
30 |
31 | fileMap.forEach((tests, file) => {
32 | const result: ProcessedTestResult = {
33 | file,
34 | status: "PASS",
35 | passed: 0,
36 | total: tests.length,
37 | details: [],
38 | };
39 |
40 | // Count passed tests and collect details
41 | tests.forEach((test) => {
42 | const passed = !test.fail;
43 |
44 | if (passed) {
45 | result.passed++;
46 | result.details.push(`✅ ${test.name}`);
47 | } else {
48 | // If any test fails, the file status is FAIL
49 | result.status = "FAIL";
50 | result.details.push(`❌ ${test.name}`);
51 | }
52 | });
53 |
54 | testResults.push(result);
55 | });
56 |
57 | return testResults;
58 | }
59 |
60 | /**
61 | * Processes the raw JSON coverage report from OPA and formats it into a structure ready to be formatted into a GitHub Pull Request comment.
62 | * @param opaRawJsonCoverageReport - The raw JSON coverage report from OPA, obtained from `opa test --format=json --coverage`. This is done in the `opaCommands.ts` file.
63 | * @returns An array of processed coverage results. See the interface for structure.
64 | */
65 | export function processCoverageReport(
66 | opaRawJsonCoverageReport: OpaRawJsonCoverageReport,
67 | ): ProcessedCoverageResult[] {
68 | const coverageResults: ProcessedCoverageResult[] = [];
69 |
70 | // Iterate through each file in the report
71 | for (const [filePath, fileData] of Object.entries(
72 | opaRawJsonCoverageReport.files,
73 | )) {
74 | // Skip if there are no uncovered lines (100% coverage)
75 | if (!fileData.not_covered || fileData.not_covered.length === 0) {
76 | coverageResults.push({
77 | file: filePath,
78 | coverage: fileData.coverage,
79 | notCoveredLines: "", // No uncovered lines
80 | });
81 | continue;
82 | }
83 |
84 | // Process not_covered sections to create the formatted string
85 | const notCoveredRanges: string[] = [];
86 |
87 | for (const section of fileData.not_covered) {
88 | const startRow = section.start.row;
89 | const endRow = section.end.row;
90 |
91 | if (startRow === endRow) {
92 | // Single line
93 | notCoveredRanges.push(startRow.toString());
94 | } else {
95 | // Range of lines, e.g. "10-12"
96 | notCoveredRanges.push(`${startRow}-${endRow}`);
97 | }
98 | }
99 |
100 | // Sort numerically
101 | notCoveredRanges.sort((a, b) => {
102 | // Extract the first number from each range for comparison
103 | const aStart = parseInt(a.split("-")[0]);
104 | const bStart = parseInt(b.split("-")[0]);
105 | return aStart - bStart;
106 | });
107 |
108 | coverageResults.push({
109 | file: filePath,
110 | coverage: fileData.coverage,
111 | notCoveredLines: notCoveredRanges.join(", "),
112 | });
113 | }
114 |
115 | return coverageResults;
116 | }
117 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "es2016" /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */,
4 | "lib": [
5 | "es6"
6 | ] /* Specify a set of bundled library declaration files that describe the target runtime environment. */,
7 | "module": "commonjs" /* Specify what module code is generated. */,
8 | "rootDir": "." /* Specify the root folder within your source files. */,
9 | "resolveJsonModule": true /* Enable importing .json files. */,
10 | "allowJs": true /* Allow JavaScript files to be a part of your program. Use the 'checkJS' option to get errors from these files. */,
11 | "outDir": "build" /* Specify an output folder for all emitted files. */,
12 | "esModuleInterop": true /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables 'allowSyntheticDefaultImports' for type compatibility. */,
13 | "forceConsistentCasingInFileNames": true /* Ensure that casing is correct in imports. */,
14 | "strict": true /* Enable all strict type-checking options. */,
15 | "noImplicitAny": true /* Enable error reporting for expressions and declarations with an implied 'any' type. */,
16 | "skipLibCheck": true /* Skip type checking all .d.ts files. */
17 | }
18 | }
19 |
--------------------------------------------------------------------------------