diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml
index ab4b4b87..a211b518 100644
--- a/.github/workflows/tests.yml
+++ b/.github/workflows/tests.yml
@@ -130,9 +130,38 @@ jobs:
${{ steps.templates-error.outputs.summary }}
GITHUB_TOKEN: ${{ secrets.SANDBOX_TEMPLATE_TOKEN }}
+ check-cla:
+ runs-on: ubuntu-latest
+ if: github.event_name == 'pull_request'
+ steps:
+ - name: Checkout Source
+ uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
+
+ - name: Check CLA
+ uses: ./check-cla
+ with:
+ label: cla-test
+ repository: conda-sandbox/cla
+ contributor-id: ${{ github.event.pull_request.number }}
+ # NOTE: using a real username here will will trigger GH notifications to that user, we wish
+ # to avoid that, GH restricts certain keywords, 'login' is one of those restricted keywords
+ # and hence is not a real username, https://github.com/login is the login page, not a user
+ contributor-login: login
+ commit-status-context: CLA test
+ token: ${{ secrets.SANDBOX_CLA_TOKEN }}
+ pr-token: ${{ secrets.SANDBOX_CLA_PR_TOKEN }}
+ fork-token: ${{ secrets.SANDBOX_FORK_TOKEN }}
+ # GitHub flavored markdown reinvents how paragraphs work, adjoined lines of text are not
+ # concatenated so instead we rely on YAML multi-line + extra newlines
+ comment-blurb: >-
+ > [!WARNING]
+
+ > This is a test of the CLA system. Review for correctness but otherwise ignore this comment.
+
+
# required check
analyze:
- needs: [pytest, read-file, template-files]
+ needs: [pytest, read-file, template-files, check-cla]
if: '!cancelled()'
runs-on: ubuntu-latest
steps:
@@ -140,6 +169,8 @@ jobs:
uses: re-actors/alls-green@05ac9388f0aebcb5727afa17fcccfecd6f8ec5fe # v1.2.2
id: alls-green
with:
+ # permit jobs to be skipped when triggered by a non-pull request event
+ allowed-skips: ${{ toJSON(needs) }}
jobs: ${{ toJSON(needs) }}
- name: Checkout our source
diff --git a/check-cla/README.md b/check-cla/README.md
index f4ad9bbf..6ece8f75 100644
--- a/check-cla/README.md
+++ b/check-cla/README.md
@@ -3,53 +3,37 @@
A custom GitHub action to be used in the conda GitHub organization for checking the
conda contributor license agreement.
-## GitHub Action Usage
-
-In your GitHub repository include the action in your workflows:
+## Action Inputs
+
+| Name | Description | Default |
+| ---- | ----------- | ------- |
+| `label` | Label to apply to contributor's PR once the CLA is signed. | `cla-signed` |
+| `repository` | Repository in which to create PR adding CLA signature. | `conda/cla` |
+| `path` | Path to the CLA signees file within the provided `repository`. | `.cla-signers` |
+| `magic-command` | Magic word to trigger the action via a comment. | `@conda-bot check` |
+| `author` | Git-format author to use for the CLA commits. | @conda-bot |
+| `token` | GitHub token to comment on PRs, change PR labels, and modify the commit status in the current repository.
Fine-grained PAT: `pull_request: write; statuses: write` | `${{ github.token }}` |
+| `pr-token` | GitHub token to create pull request in the `repository`.
Fine-grained PAT: `pull_request: write` | `${{ inputs.token }}` |
+| `fork-token` | GitHub token to create and push to a `repository` fork.
Fine-grained PAT: `administration: write; contents: write` | `${{ inputs.pr-token }}` |
+| `contributor-id` | Contributor ID to check for CLA signature. | `${{ github.event.pull_request.user.id || github.event.issue.user.id }}` |
+| `contributor-login` | Contributor login to check for CLA signature. | `${{ github.event.pull_request.user.login || github.event.issue.user.login }}` |
+| `commit-status-context` | Commit status label/identifier. | `CLA check` |
+| `comment-blurb` | Additional comment to add to PRs for contributors who have not signed the CLA. | |
+
+## Sample Workflows
```yaml
name: Check CLA
on:
- issue_comment:
- types: [created]
pull_request_target:
jobs:
check:
- if: >-
- (
- github.event.comment.body == '@conda-bot check'
- && github.event.issue.pull_request
- || github.event_name == 'pull_request_target'
- )
steps:
- uses: conda/actions/check-cla
with:
- # [required]
- # A token with ability to comment, label, and modify the commit status
- # (`pull_request: write` and `statuses: write` for fine-grained PAT; `repo` for classic PAT)
- # (default: secrets.GITHUB_TOKEN)
- token:
- # [required]
- # Label to apply to contributor's PR once CLA is signed
- label:
-
- # Upstream repository in which to create PR
- # (default: conda/infrastructure)
- cla_repo:
- # Path to the CLA signees file within the provided `cla_repo`
- # (default: .clabot)
- cla_path:
-
- # Fork of cla_repo in which to create branch
- # (default: conda-bot/infrastructure)
- cla_fork:
- # [required]
- # Token for opening signee PR in the provided `cla_repo`
- # (`pull_request: write` for fine-grained PAT; `repo` and `workflow` for classic PAT)
- cla_token:
- # Git-format author/committer to use for pull request commits
- # (default: Conda Bot <18747875+conda-bot@users.noreply.github.com>)
- cla_author:
+ token: ...
+ pr-token: ...
+ fork-token: ...
```
diff --git a/check-cla/action.yml b/check-cla/action.yml
index 65795646..08634c81 100644
--- a/check-cla/action.yml
+++ b/check-cla/action.yml
@@ -1,198 +1,211 @@
-name: CLA check
+name: Check CLA
description: Reacts to new PRs and check if the contributor has previously signed the conda contributor license agreement (CLA).
inputs:
- token:
- description: >-
- A token with ability to comment, label, and modify the commit status
- (`pull_request: write` and `statuses: write` for fine-grained PAT; `repo` for classic PAT)
- default: ${{ github.token }}
- required: true
label:
- description: Label to apply to contributor's PR once CLA is signed
- required: true
- cla_repo:
- description: Upstream repository in which to create PR
- default: conda/infrastructure
- cla_path:
- description: Path to the CLA signees file within the provided `cla_repo`
- default: .clabot
- cla_fork:
- description: Fork of `cla_repo` in which to create branch
- default: conda-bot/infrastructure
- cla_token:
- description: >-
- Token for opening signee PR in `cla_fork`
- (`pull_request: write` for fine-grained PAT; `repo` and `workflow` for classic PAT)
- required: true
- cla_author:
- description: Git-format author/committer to use for pull request commits
+ description: Label to apply to contributor's PR once the CLA is signed.
+ default: cla-signed
+ repository:
+ description: Repository in which to create PR adding CLA signature.
+ default: conda/cla
+ path:
+ description: Path to the CLA signees file within the provided `repository`.
+ default: .cla-signers
+ magic-command:
+ description: Magic word to trigger the action via a comment.
+ default: '@conda-bot check'
+ author:
+ description: Git-format author to use for the CLA commits.
default: Conda Bot <18747875+conda-bot@users.noreply.github.com>
+ token:
+ description: |
+ GitHub token to comment on PRs, change PR labels, and modify the commit status in the current repository.
+ Fine-grained PAT: `pull_request: write; statuses: write`
+ default: ${{ github.token }}
+ pr-token:
+ description: |
+ GitHub token to create pull request in the `repository`.
+ Fine-grained PAT: `pull_request: write`
+ # default: ${{ inputs.token }}
+ fork-token:
+ description: |
+ GitHub token to create and push to a `repository` fork.
+ Fine-grained PAT: `administration: write; contents: write`
+ # default: ${{ inputs.pr-token }}
+ contributor-id:
+ description: Contributor ID to check for CLA signature.
+ default: ${{ github.event.pull_request.user.id || github.event.issue.user.id }}
+ contributor-login:
+ description: Contributor login to check for CLA signature.
+ default: ${{ github.event.pull_request.user.login || github.event.issue.user.login }}
+ commit-status-context:
+ description: Commit status label/identifier.
+ default: CLA check
+ comment-blurb:
+ description: Comment to add to PRs for contributors who have not signed the CLA.
runs:
using: composite
steps:
- # if triggered by a comment, leave a reaction
- - name: React to comment
- uses: peter-evans/create-or-update-comment@71345be0265236311c031f5c7866368bd1eff043
+ - name: Set Comment Reaction [eyes]
+ uses: peter-evans/create-or-update-comment@71345be0265236311c031f5c7866368bd1eff043 # v4.0.0
if: github.event_name == 'issue_comment'
with:
token: ${{ inputs.token }}
comment-id: ${{ github.event.comment.id }}
reactions: eyes
+ reactions-edit-mode: replace
- # commit status → pending
- - name: Set commit status with pending
- uses: conda/actions/set-commit-status@8ff3faa82ad80f5c05d91c22bcd37d897f80ca46 # v25.1.1
+ - name: Set Commit Status [pending]
+ uses: conda/actions/set-commit-status@7873f9d7c90877290866eb893b8f6eff2e88429a # v25.1.2
with:
token: ${{ inputs.token }}
- context: CLA check
+ context: ${{ inputs.commit-status-context }}
description: Checking conda CLA...
state: pending
- # has_label, number, contributor, url, has_signed
- - name: Collect PR metadata
- uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea
- id: metadata
+ - name: Read CLA Signees
+ id: read-cla
+ uses: conda/actions/read-file@7873f9d7c90877290866eb893b8f6eff2e88429a # v25.1.2
with:
- github-token: ${{ inputs.token }}
- script: |
- const { owner, repo, number } = context.issue;
- core.debug(`owner: ${owner}`);
- core.debug(`repo: ${repo}`);
- core.setOutput('number', number);
- core.debug(`number: ${number}`);
-
- const raw = await github.rest.pulls.get({
- owner: owner,
- repo: repo,
- pull_number: number
- });
- const labels = raw.data.labels.map(label => label.name);
- core.debug(`labels: ${labels}`);
-
- const has_label = labels.includes('${{ inputs.label }}');
- core.setOutput('has_label', has_label);
- core.debug(`has_label: ${has_label}`);
-
- const cla_repo = '${{ inputs.cla_repo }}'.split('/', 2);
- const { content, encoding } = (await github.rest.repos.getContent({
- owner: cla_repo[0],
- repo: cla_repo[1],
- path: '${{ inputs.cla_path }}'
- })).data;
- const contributors = JSON.parse(
- Buffer.from(content, encoding).toString('utf-8')
- ).contributors;
- core.debug(`contributors: ${contributors}`);
-
- const payload = context.payload.issue || context.payload.pull_request || context.payload;
- const contributor = payload.user.login;
- core.setOutput('contributor', contributor);
- core.debug(`contributor: ${contributor}`);
-
- const url = payload.html_url;
- core.setOutput('url', url);
- core.debug(`url: ${url}`);
-
- const has_signed = contributors.includes(contributor);
- core.setOutput('has_signed', has_signed);
- core.debug(`has_signed: ${has_signed}`);
-
- # if contributor has already signed, add [cla-signed] label
- - name: Add label to PR
- uses: actions-ecosystem/action-add-labels@18f1af5e3544586314bbe15c0273249c770b2daf
- if: steps.metadata.outputs.has_signed == 'true' && steps.metadata.outputs.has_label == 'false'
+ path: https://raw.githubusercontent.com/${{ inputs.repository }}/main/${{ inputs.path }}
+ parser: json
+ default: '{}'
+
+ - name: Detect Signature
+ id: detect-signature
+ shell: bash
+ run: echo signed=${{ contains(fromJSON(steps.read-cla.outputs.content), inputs.contributor-id) }} >> $GITHUB_OUTPUT
+
+ - name: Add PR Label
+ uses: actions-ecosystem/action-add-labels@18f1af5e3544586314bbe15c0273249c770b2daf # v1.1.3
+ if: inputs.label && steps.detect-signature.outputs.signed == 'true' && !contains(github.event.pull_request.labels || github.event.issue.labels, inputs.label)
with:
github_token: ${{ inputs.token }}
labels: ${{ inputs.label }}
- # if contributor has not signed yet, remove [cla-signed] label
- - name: Remove label to PR
- uses: actions-ecosystem/action-remove-labels@2ce5d41b4b6aa8503e285553f75ed56e0a40bae0
- if: steps.metadata.outputs.has_signed == 'false' && steps.metadata.outputs.has_label == 'true'
+ - name: Remove PR Label
+ uses: actions-ecosystem/action-remove-labels@2ce5d41b4b6aa8503e285553f75ed56e0a40bae0 # v1.3.0
+ if: inputs.label && steps.detect-signature.outputs.signed == 'false' && contains(github.event.pull_request.labels || github.event.issue.labels, inputs.label)
with:
github_token: ${{ inputs.token }}
labels: ${{ inputs.label }}
- # if unsigned, checkout cla_repo
- - name: Clone CLA signee repo
+ # cloning upstream (not fork) since main branch is not in sync
+ - name: Clone CLA Repository
+ if: steps.detect-signature.outputs.signed == 'false'
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- if: steps.metadata.outputs.has_signed == 'false'
with:
- repository: ${{ inputs.cla_repo }}
-
- # if unsigned, update cla_path
- - name: Add contributor as a CLA signee
- shell: python
- if: steps.metadata.outputs.has_signed == 'false'
- run: |
- import json
- from pathlib import Path
-
- path = Path("${{ inputs.cla_path }}")
- signees = json.loads(path.read_text())
- signees["contributors"].append("${{ steps.metadata.outputs.contributor }}")
- signees["contributors"].sort(key=str.lower)
- path.write_text(json.dumps(signees, indent=2) + "\n")
-
- # if unsigned, create PR
- - name: Create PR with new CLA signee
- uses: peter-evans/create-pull-request@67ccf781d68cd99b580ae25a5c18a1cc84ffff1f # v7.0.6
+ repository: ${{ inputs.repository }}
+ path: .github_cache/actions/cla
+
+ - name: Create Fork
+ id: create-fork
+ if: steps.detect-signature.outputs.signed == 'false'
+ uses: conda/actions/create-fork@7873f9d7c90877290866eb893b8f6eff2e88429a # v25.1.2
+ with:
+ repository: ${{ inputs.repository }}
+ token: ${{ inputs.fork-token || inputs.pr-token || inputs.token }}
+
+ - name: Setup Python
+ if: steps.detect-signature.outputs.signed == 'false'
+ uses: actions/setup-python@82c7e631bb3cdc910f68e0081d67478d79c6982d # v5.1.0
+ with:
+ python-version: '>=3.9'
+
+ - name: Add Contributor Signature
+ if: steps.detect-signature.outputs.signed == 'false'
+ shell: bash
+ run: >
+ python ${{ github.action_path }}/check_cla.py
+ .github_cache/actions/cla/${{ inputs.path }}
+ --id=${{ inputs.contributor-id }}
+ --login=${{ inputs.contributor-login }}
+
+ - name: Diff
+ shell: bash
+ run: git diff
+
+ - name: Create PR
id: pull
- if: steps.metadata.outputs.has_signed == 'false'
+ if: steps.detect-signature.outputs.signed == 'false'
+ uses: peter-evans/create-pull-request@67ccf781d68cd99b580ae25a5c18a1cc84ffff1f # v7.0.6
with:
- push-to-fork: ${{ inputs.cla_fork }}
- token: ${{ inputs.cla_token }}
- branch: cla-${{ steps.metadata.outputs.contributor }}
+ path: .github_cache/actions/cla
+ # push to the fork
+ branch-token: ${{ inputs.fork-token || inputs.pr-token || inputs.token }}
+ push-to-fork: ${{ steps.create-fork.outputs.fork }}
+ branch: cla-${{ inputs.contributor-id }}
+ commit-message: 🤖 Add ${{ inputs.contributor-login }} (${{ inputs.contributor-id }}) as CLA signee
+ author: ${{ inputs.author }}
+ committer: ${{ inputs.author }}
+ # create PR
+ token: ${{ inputs.pr-token || inputs.token }}
delete-branch: true
- commit-message: Adding CLA signee ${{ steps.metadata.outputs.contributor }}
- author: ${{ inputs.cla_author }}
- committer: ${{ inputs.cla_author }}
- title: Adding CLA signee ${{ steps.metadata.outputs.contributor }}
- body: |
- Adding CLA signee @${{ steps.metadata.outputs.contributor }}
-
- Xref ${{ steps.metadata.outputs.url }}
+ title: 🤖 Add ${{ inputs.contributor-login }} (${{ inputs.contributor-id }}) as CLA signee
+ body: Xref ${{ github.event.pull_request.html_url || github.event.issue.html_url }}
- # if unsigned, create sticky comment
- - name: Create comment regarding missing CLA signature
+ - name: Create Comment
uses: marocchino/sticky-pull-request-comment@52423e01640425a022ef5fd42c6fb5f633a02728 # v2.9.1
- if: steps.metadata.outputs.has_signed == 'false'
+ if: steps.detect-signature.outputs.signed == 'false'
with:
- number: ${{ steps.metadata.outputs.number }}
+ number: ${{ github.event.number || github.event.issue.number }}
# GitHub flavored markdown reinvents how paragraphs work, adjoined lines of text are not
# concatenated so instead we rely on YAML multi-line + extra newlines
message: >-
[cla]: https://conda.io/en/latest/contributing.html#conda-contributor-license-agreement
+ ${{ inputs.comment-blurb }}
+
+
We require contributors to sign our [Contributor License Agreement][cla] and we don't
- have one on file for @${{ steps.metadata.outputs.contributor }}.
+ have one on file for @${{ inputs.contributor-login }} (${{ inputs.contributor-id }}).
In order for us to review and merge your code, please e-sign the
[Contributor License Agreement PDF][cla]. We then need to manually verify your
signature, merge the PR (${{ steps.pull.outputs.pull-request-url }}), and ping the bot
to refresh the PR.
+
+
+
+
+ Commands
+
+
+ Trigger actions by commenting on this PR:
+
+
+ - `${{ inputs.magic-command }}` will check whether a CLA has been signed for this PR author
+
+
+
GITHUB_TOKEN: ${{ inputs.token }}
- # commit status → error
- - name: Set commit status to error
- if: steps.metadata.outputs.has_signed == 'false'
- uses: conda/actions/set-commit-status@8ff3faa82ad80f5c05d91c22bcd37d897f80ca46 # v25.1.1
+ - name: Set Commit Status [success]
+ if: steps.detect-signature.outputs.signed == 'true'
+ uses: conda/actions/set-commit-status@7873f9d7c90877290866eb893b8f6eff2e88429a # v25.1.2
+ with:
+ token: ${{ inputs.token }}
+ context: ${{ inputs.commit-status-context }}
+ description: CLA signed, thank you!
+ state: success
+
+ - name: Set Commit Status [error]
+ if: steps.detect-signature.outputs.signed == 'false'
+ uses: conda/actions/set-commit-status@7873f9d7c90877290866eb893b8f6eff2e88429a # v25.1.2
with:
token: ${{ inputs.token }}
- context: CLA check
+ context: ${{ inputs.commit-status-context }}
description: Please follow the details link to sign the conda CLA. →
- state: error
target_url: https://conda.io/en/latest/contributing.html#conda-contributor-license-agreement
+ state: error
- # commit status → success
- - name: Set commit status to success
- if: steps.metadata.outputs.has_signed == 'true'
- uses: conda/actions/set-commit-status@8ff3faa82ad80f5c05d91c22bcd37d897f80ca46 # v25.1.1
+ - name: Set Comment Reaction [hooray/confused]
+ uses: peter-evans/create-or-update-comment@71345be0265236311c031f5c7866368bd1eff043 # v4.0.0
+ if: github.event_name == 'issue_comment'
with:
token: ${{ inputs.token }}
- context: CLA check
- description: CLA signed, thank you!
- state: success
+ comment-id: ${{ github.event.comment.id }}
+ reactions: ${{ steps.detect-signature.outputs.signed == 'true' && 'hooray' || 'confused' }}
+ reactions-edit-mode: replace
diff --git a/check-cla/check_cla.py b/check-cla/check_cla.py
new file mode 100644
index 00000000..6abcf471
--- /dev/null
+++ b/check-cla/check_cla.py
@@ -0,0 +1,51 @@
+"""Add a contributor to the list of signees in the CLA."""
+
+from __future__ import annotations
+
+import json
+from argparse import ArgumentParser
+from pathlib import Path
+from typing import TYPE_CHECKING
+
+if TYPE_CHECKING:
+ from argparse import Namespace
+ from collections.abc import Sequence
+
+
+def parse_args(argv: Sequence[str] | None = None) -> Namespace:
+ # parse CLI for inputs
+ parser = ArgumentParser()
+ parser.add_argument("path", type=Path, help="Local path to the CLA file.")
+ parser.add_argument(
+ "--id",
+ type=int,
+ required=True,
+ help="Contributor to add to the CLA.",
+ )
+ parser.add_argument(
+ "--login",
+ type=str,
+ required=True,
+ help="Contributor's GitHub login.",
+ )
+ return parser.parse_args(argv)
+
+
+def read_cla(path: Path) -> dict[int, str]:
+ try:
+ return json.loads(path.read_text())
+ except FileNotFoundError:
+ return {}
+
+
+def write_cla(path: Path, signees: dict[int, str]) -> None:
+ path.write_text(json.dumps(signees, indent=2, sort_keys=True) + "\n")
+
+
+def main() -> None:
+ args = parse_args()
+ write_cla(args.path, {**read_cla(args.path), args.id: args.login})
+
+
+if __name__ == "__main__":
+ main()
diff --git a/check-cla/data/empty.json b/check-cla/data/empty.json
new file mode 100644
index 00000000..0967ef42
--- /dev/null
+++ b/check-cla/data/empty.json
@@ -0,0 +1 @@
+{}
diff --git a/check-cla/data/multiple.json b/check-cla/data/multiple.json
new file mode 100644
index 00000000..a03fa597
--- /dev/null
+++ b/check-cla/data/multiple.json
@@ -0,0 +1,5 @@
+{
+ "111": "foo",
+ "123": "login",
+ "222": "bar"
+}
diff --git a/check-cla/data/single.json b/check-cla/data/single.json
new file mode 100644
index 00000000..374f20e1
--- /dev/null
+++ b/check-cla/data/single.json
@@ -0,0 +1,3 @@
+{
+ "123": "login"
+}
diff --git a/check-cla/test_check_cla.py b/check-cla/test_check_cla.py
new file mode 100644
index 00000000..0d9add8a
--- /dev/null
+++ b/check-cla/test_check_cla.py
@@ -0,0 +1,67 @@
+from __future__ import annotations
+
+import filecmp
+from argparse import Namespace
+from pathlib import Path
+from typing import TYPE_CHECKING
+
+import pytest
+
+from check_cla import parse_args, read_cla, write_cla
+
+if TYPE_CHECKING:
+ from typing import Final
+
+DATA: Final = Path(__file__).parent / "data"
+
+EMPTY: Final[dict[int, str]] = {}
+SINGLE: Final = {"123": "login"}
+MULTIPLE: Final = {"111": "foo", "123": "login", "222": "bar"}
+
+
+def test_parse_args() -> None:
+ with pytest.raises(SystemExit):
+ parse_args([])
+ with pytest.raises(SystemExit):
+ parse_args(["file"])
+ with pytest.raises(SystemExit):
+ parse_args(["--id=123"])
+ with pytest.raises(SystemExit):
+ parse_args(["--login=login"])
+ with pytest.raises(SystemExit):
+ parse_args(["file", "--id=123"])
+ with pytest.raises(SystemExit):
+ parse_args(["file", "--login=login"])
+ with pytest.raises(SystemExit):
+ parse_args(["--id=123", "--login=login"])
+ assert parse_args(["file", "--id=123", "--login=login"]) == Namespace(
+ path=Path("file"),
+ id=123,
+ login="login",
+ )
+
+
+@pytest.mark.parametrize(
+ "path,signees",
+ [
+ ("missing", EMPTY),
+ ("empty.json", EMPTY),
+ ("single.json", SINGLE),
+ ("multiple.json", MULTIPLE),
+ ],
+)
+def test_read_cla(path: str, signees: dict[int, str]) -> None:
+ assert read_cla(DATA / path) == signees
+
+
+@pytest.mark.parametrize(
+ "path,signees",
+ [
+ ("empty.json", EMPTY),
+ ("single.json", SINGLE),
+ ("multiple.json", MULTIPLE),
+ ],
+)
+def test_write_cla(tmp_path: Path, path: str, signees: dict[int, str]) -> None:
+ write_cla(tmp := tmp_path / path, signees)
+ assert filecmp.cmp(tmp, DATA / path)
diff --git a/pyproject.toml b/pyproject.toml
index 1328b87c..ea961ca4 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -17,6 +17,7 @@ omit = [
[tool.pytest.ini_options]
addopts = [
"--color=yes",
+ "--cov=check-cla",
"--cov=combine-durations",
"--cov=read-file",
"--cov=template-files",
@@ -49,6 +50,7 @@ select = [
[tool.ruff.lint.isort]
known-first-party = [
+ "check_cla",
"combine_durations",
"read_file",
"template_files",