diff --git a/.github/cla/cla.sh b/.github/cla/cla.sh new file mode 100755 index 00000000..5473beea --- /dev/null +++ b/.github/cla/cla.sh @@ -0,0 +1,220 @@ +#!/usr/bin/env bash +# CLA workflow logic. Sourced by .github/workflows/cla.yml (which calls cla_main) +# and tested by .github/cla/cla.test.sh (which sources and calls the pure +# functions). Side effects (git push, gh api) live only in cla_main. + +set -euo pipefail + +# 0 if $login is in space-separated $allowlist; bracket characters in bot +# names like "dependabot[bot]" are matched literally. +cla_allowlisted() { + local login="$1" allowlist="$2" + [[ " $allowlist " == *" $login "* ]] +} + +# 0 if $user_id is already in $signatures_file's signedContributors. +cla_signed() { + local user_id="$1" signatures_file="$2" + local count + count=$(jq --argjson id "$user_id" \ + '[.signedContributors[] | select(.id == $id)] | length' \ + "$signatures_file") + [ "$count" != "0" ] +} + +# Append a signature row in place. Caller is responsible for the idempotency +# check (cla_signed) before invoking — this function always appends. +cla_add_signature() { + local name="$1" user_id="$2" ts="$3" pr="$4" signatures_file="$5" + jq --arg name "$name" --argjson id "$user_id" --arg ts "$ts" --argjson pr "$pr" \ + '.signedContributors += [{name: $name, id: $id, signed_at: $ts, pull_request_no: $pr}]' \ + "$signatures_file" > "$signatures_file.tmp" + mv "$signatures_file.tmp" "$signatures_file" +} + +# 0 if $login is a public member of $org. Returns 1 for non-members, private +# members (default GITHUB_TOKEN can't see them), unknown orgs, and API errors — +# fail-closed: anyone not provably an org member must sign once. +# Tests override this function with a stub. +cla_org_member() { + local login="$1" org="$2" + [ -z "$org" ] && return 1 + gh api "orgs/$org/members/$login" --silent 2>/dev/null +} + +# 0 if $login should be skipped from the CLA check entirely (no JSON row, +# no comment listing, no warning) — either because they're on the literal +# allowlist or because they're a public member of $org. +cla_should_skip() { + local login="$1" allowlist="$2" org="${3:-}" + cla_allowlisted "$login" "$allowlist" && return 0 + [ -n "$org" ] && cla_org_member "$login" "$org" && return 0 + return 1 +} + +# Render the unsigned-contributors comment. Wording matches the templates from +# contributor-assistant/github-action so the experience is familiar to anyone +# who has signed a CLA at another OSS project. +# +# Usage: cla_render_unsigned_comment +# status_json: {"signed":["alice"], "unsigned":["bob"], "unknown":[{"name":"X","email":"x@y"}]} +# Note: allowlisted and org members are excluded from signed/unsigned by the caller. +# Unknown = commits whose email isn't linked to any GitHub account; surfaced as a +# warning per the original action, but doesn't gate the check. +cla_render_unsigned_comment() { + local cla_url="$1" sign_phrase="$2" marker="$3" status_json="$4" + local signed_count unsigned_count unknown_count total you matrix="" unknown_section="" + signed_count=$(echo "$status_json" | jq '.signed | length') + unsigned_count=$(echo "$status_json" | jq '.unsigned | length') + unknown_count=$(echo "$status_json" | jq '(.unknown // []) | length') + total=$((signed_count + unsigned_count)) + if [ "$total" -gt 1 ]; then + you="you all" + matrix=$(printf '\n\n**%d** out of **%d** committers have signed the CLA.' "$signed_count" "$total") + while IFS= read -r login; do + [ -z "$login" ] && continue + matrix+=$(printf '
:white_check_mark: [%s](https://github.com/%s)' "$login" "$login") + done < <(echo "$status_json" | jq -r '.signed[]') + while IFS= read -r login; do + [ -z "$login" ] && continue + matrix+=$(printf '
:x: @%s' "$login") + done < <(echo "$status_json" | jq -r '.unsigned[]') + else + you="you" + fi + if [ "$unknown_count" -gt 0 ]; then + local seem names + [ "$unknown_count" -gt 1 ] && seem="seem" || seem="seems" + names=$(echo "$status_json" | jq -r '(.unknown // []) | map(.name) | join(", ")') + unknown_section=$(printf '\n\n**%s** %s not to be a GitHub user. You need a GitHub account to be able to sign the CLA. If you have already a GitHub account, please [add the email address used for this commit to your account](https://help.github.com/articles/why-are-my-commits-linked-to-the-wrong-user/#commits-are-not-linked-to-any-user).' "$names" "$seem") + fi + cat <You can retrigger this bot by commenting **recheck** in this Pull Request. +Posted by the CLA bot. + +${marker} +EOF +} + +cla_render_signed_comment() { + local marker="$1" + cat <Posted by the CLA bot. + +${marker} +EOF +} + +cla_init_signatures() { + local signatures_file="$1" + mkdir -p "$(dirname "$signatures_file")" + [ -f "$signatures_file" ] || echo '{"signedContributors":[]}' > "$signatures_file" +} + +# Orchestrates the full workflow. The only function with side effects. +# Required env: REPO, PR_NUMBER, EVENT_NAME, ALLOWLIST, CLA_URL, SIGN_PHRASE. +# Required env when EVENT_NAME=issue_comment: COMMENT_USER_LOGIN, COMMENT_USER_ID. +cla_main() { + local signatures="signatures/version1/cla.json" + local marker='' + + cla_init_signatures "$signatures" + + # Record signature first if this run was triggered by a sign comment. + # Skipped for: allowlisted bots/maintainers, org members, and signers + # already on file. Idempotent across all three cases. + if [ "${EVENT_NAME:-}" = "issue_comment" ]; then + if cla_should_skip "$COMMENT_USER_LOGIN" "$ALLOWLIST" "${CLA_ORG:-}"; then + : # allowlisted or org member — no JSON row needed + elif ! cla_signed "$COMMENT_USER_ID" "$signatures"; then + cla_add_signature "$COMMENT_USER_LOGIN" "$COMMENT_USER_ID" \ + "$(date -u +%Y-%m-%dT%H:%M:%SZ)" "$PR_NUMBER" "$signatures" + git config user.name "github-actions[bot]" + git config user.email "41898282+github-actions[bot]@users.noreply.github.com" + git add "$signatures" + git commit -m "Record CLA signature for @${COMMENT_USER_LOGIN} (PR #${PR_NUMBER})" + git push origin main + fi + fi + + # Pull commits + head SHA in one API call. Partition authors three ways: + # signed, unsigned, and unknown (commits whose email isn't linked to any + # GitHub account — these would otherwise silently bypass the CLA check). + local pr_json commits_json authors_json unknown_json signed_logins unsigned_logins status_json head_sha + pr_json=$(gh pr view "$PR_NUMBER" --repo "$REPO" --json commits,headRefOid) + commits_json=$(echo "$pr_json" | jq -c '{commits}') + head_sha=$(echo "$pr_json" | jq -r '.headRefOid') + authors_json=$(echo "$commits_json" | jq -c \ + '[.commits[].authors[] | select(.user != null) | {login: .user.login, id: .user.id}] | unique_by(.id)') + # Unknown = commit authors with no linked GitHub user. Dedup by name. + unknown_json=$(echo "$commits_json" | jq -c \ + '[.commits[].authors[] | select(.user == null) | {name: .name, email: .email}] | unique_by("\(.name)<\(.email)>")') + + signed_logins=() + unsigned_logins=() + while IFS=$'\t' read -r login user_id; do + if cla_should_skip "$login" "$ALLOWLIST" "${CLA_ORG:-}"; then continue; fi + if cla_signed "$user_id" "$signatures"; then + signed_logins+=("$login") + else + unsigned_logins+=("$login") + fi + done < <(echo "$authors_json" | jq -r '.[] | "\(.login)\t\(.id)"') + + local unknown_count + unknown_count=$(echo "$unknown_json" | jq 'length') + + status_json=$(jq -n \ + --argjson signed "$(printf '%s\n' "${signed_logins[@]:-}" | jq -R . | jq -s 'map(select(length>0))')" \ + --argjson unsigned "$(printf '%s\n' "${unsigned_logins[@]:-}" | jq -R . | jq -s 'map(select(length>0))')" \ + --argjson unknown "$unknown_json" \ + '{signed: $signed, unsigned: $unsigned, unknown: $unknown}') + + # Gate matches contributor-assistant/github-action: pass/fail is based only + # on signed-vs-unsigned. Unknown committers (commits with no linked GitHub + # user) surface as a warning in the unsigned comment but don't block. + local body status_state status_desc + if [ "${#unsigned_logins[@]}" -eq 0 ]; then + status_state="success" + status_desc="All contributors signed the CLA" + body=$(cla_render_signed_comment "$marker") + else + status_state="failure" + status_desc="Awaiting CLA signature" + body=$(cla_render_unsigned_comment "$CLA_URL" "$SIGN_PHRASE" "$marker" "$status_json") + fi + + # Upsert the sticky CLA comment (one per PR, identified by the marker). + local existing + existing=$(gh api "repos/${REPO}/issues/${PR_NUMBER}/comments" --paginate \ + | jq -r --arg m "$marker" '[.[] | select(.body | contains($m)) | .id] | first // empty') + if [ -n "$existing" ]; then + gh api -X PATCH "repos/${REPO}/issues/comments/${existing}" -f body="$body" > /dev/null + else + gh api -X POST "repos/${REPO}/issues/${PR_NUMBER}/comments" -f body="$body" > /dev/null + fi + + # Set the commit status check on the PR head. + gh api -X POST "repos/${REPO}/statuses/${head_sha}" \ + -f state="$status_state" \ + -f context="CLA" \ + -f description="$status_desc" \ + -f target_url="$CLA_URL" > /dev/null +} + +# Run cla_main when this script is executed directly (from the workflow). +# Stay quiet when sourced (from the test file or an interactive shell). +if [ "${BASH_SOURCE[0]:-}" = "${0}" ]; then + cla_main +fi diff --git a/.github/cla/cla.test.sh b/.github/cla/cla.test.sh new file mode 100755 index 00000000..636a0a1c --- /dev/null +++ b/.github/cla/cla.test.sh @@ -0,0 +1,206 @@ +#!/usr/bin/env bash +# Hermetic tests for .github/cla/cla.sh — no network, no git, no gh calls. +# Run locally: bash .github/cla/cla.test.sh +# Run in CI: same command, wired into .github/workflows/ci.yml. + +set -uo pipefail +cd "$(dirname "$0")/../.." + +# shellcheck source=cla.sh +source .github/cla/cla.sh + +PASS=0 +FAIL=0 +TMPDIR=$(mktemp -d) +trap 'rm -rf "$TMPDIR"' EXIT + +assert_eq() { + local name="$1" expected="$2" actual="$3" + if [ "$expected" = "$actual" ]; then + printf ' \xe2\x9c\x93 %s\n' "$name" + PASS=$((PASS + 1)) + else + printf ' \xe2\x9c\x97 %s\n expected: %q\n got: %q\n' "$name" "$expected" "$actual" + FAIL=$((FAIL + 1)) + fi +} + +assert_true() { + local name="$1"; shift + if "$@"; then + printf ' \xe2\x9c\x93 %s\n' "$name" + PASS=$((PASS + 1)) + else + printf ' \xe2\x9c\x97 %s (command returned non-zero)\n' "$name" + FAIL=$((FAIL + 1)) + fi +} + +assert_false() { + local name="$1"; shift + if ! "$@"; then + printf ' \xe2\x9c\x93 %s\n' "$name" + PASS=$((PASS + 1)) + else + printf ' \xe2\x9c\x97 %s (command returned zero)\n' "$name" + FAIL=$((FAIL + 1)) + fi +} + +ALLOW='dependabot[bot] github-actions[bot] renovate[bot] jedrazb' + +echo "cla_allowlisted:" +assert_true "bracketed bot name matches literally" cla_allowlisted "dependabot[bot]" "$ALLOW" +assert_true "plain user matches" cla_allowlisted "jedrazb" "$ALLOW" +assert_false "stranger does not match" cla_allowlisted "stranger" "$ALLOW" +assert_false "username prefix is not a partial match" cla_allowlisted "depend" "$ALLOW" +assert_false "username substring is not a partial match" cla_allowlisted "abot" "$ALLOW" + +echo +echo "cla_signed:" +SIGS="$TMPDIR/sigs.json" +echo '{"signedContributors":[{"name":"alice","id":111,"signed_at":"2026-05-09T10:00:00Z","pull_request_no":1}]}' > "$SIGS" +assert_true "alice (id 111) is recognized" cla_signed 111 "$SIGS" +assert_false "id 222 not recognized" cla_signed 222 "$SIGS" + +echo +echo "cla_add_signature:" +echo '{"signedContributors":[]}' > "$SIGS" +cla_add_signature "bob" 222 "2026-05-09T10:00:00Z" 5 "$SIGS" +assert_eq "first add records bob" \ + '[{"name":"bob","id":222,"signed_at":"2026-05-09T10:00:00Z","pull_request_no":5}]' \ + "$(jq -c .signedContributors "$SIGS")" + +cla_add_signature "carol" 333 "2026-05-09T10:01:00Z" 6 "$SIGS" +assert_eq "second add appends carol" \ + '2' \ + "$(jq '.signedContributors | length' "$SIGS")" + +echo +echo "sign-once guarantee (the load-bearing test):" +# Scenario: alice signed on PR #1. She opens PR #2, then later comments the +# sign phrase again on PR #2 by mistake. The `cla_signed` gate must short-circuit +# so we never re-append, never re-commit, never re-push. +echo '{"signedContributors":[{"name":"alice","id":111,"signed_at":"2026-05-09T10:00:00Z","pull_request_no":1}]}' > "$SIGS" +assert_true "returning signer recognized → caller skips append+push" \ + cla_signed 111 "$SIGS" + +# What if alice posts the sign phrase a SECOND time (duplicate sign)? +# We test the gate: cla_signed returns true, so the orchestrator skips the +# add+commit+push branch entirely. +before_count=$(jq '.signedContributors | length' "$SIGS") +if cla_signed 111 "$SIGS"; then + : # gate fired, no append happens +else + cla_add_signature "alice" 111 "would-not-happen" 99 "$SIGS" +fi +after_count=$(jq '.signedContributors | length' "$SIGS") +assert_eq "duplicate sign comment is a no-op (count unchanged)" \ + "$before_count" "$after_count" + +echo +echo "cla_org_member (mocked):" +# Override the real function with a stub for hermetic testing. The stub +# treats only "alice" and "carol" as eigenpal members. Mirrors the real +# function's contract: empty $org means "no org check, return 1." +cla_org_member() { + [ -z "$2" ] && return 1 + case "$1" in + alice|carol) return 0 ;; + *) return 1 ;; + esac +} +assert_true "alice is a member of eigenpal" cla_org_member "alice" "eigenpal" +assert_false "stranger is not a member" cla_org_member "stranger" "eigenpal" +assert_false "no org configured → not a match" cla_org_member "alice" "" + +echo +echo "cla_should_skip (allowlist + org-member combined):" +assert_true "literal allowlist match → skip" cla_should_skip "dependabot[bot]" "$ALLOW" "" +assert_true "named maintainer match → skip" cla_should_skip "jedrazb" "$ALLOW" "" +assert_false "non-allowlisted, no org configured → don't skip" \ + cla_should_skip "stranger" "$ALLOW" "" +assert_true "org member match (mocked alice) → skip" cla_should_skip "alice" "$ALLOW" "eigenpal" +assert_false "non-allowlisted, non-org-member → don't skip" \ + cla_should_skip "stranger" "$ALLOW" "eigenpal" +assert_true "literal allowlist still wins when org configured" \ + cla_should_skip "jedrazb" "$ALLOW" "eigenpal" +assert_false "empty allowlist + empty org → no one skipped" \ + cla_should_skip "anyone" "" "" + +echo +echo "cla_render_unsigned_comment (single contributor — uses 'you'):" +body=$(cla_render_unsigned_comment "https://e.x/CLA.md" "I sign" "" \ + '{"signed":[],"unsigned":["alice"]}') +case "$body" in *"that you sign"*) ok=1 ;; *) ok=0 ;; esac +assert_eq "uses singular 'you' for single contributor" "1" "$ok" +case "$body" in *"that you all sign"*) ok=1 ;; *) ok=0 ;; esac +assert_eq "does NOT use 'you all' for single contributor" "0" "$ok" +case "$body" in *"committers have signed the CLA"*) ok=1 ;; *) ok=0 ;; esac +assert_eq "does NOT show committer matrix for single contributor" "0" "$ok" +case "$body" in *"https://e.x/CLA.md"*) ok=1 ;; *) ok=0 ;; esac +assert_eq "includes CLA URL" "1" "$ok" +case "$body" in *"recheck"*) ok=1 ;; *) ok=0 ;; esac +assert_eq "mentions the recheck keyword" "1" "$ok" +case "$body" in *""*) ok=1 ;; *) ok=0 ;; esac +assert_eq "includes sticky-comment marker" "1" "$ok" + +echo +echo "cla_render_unsigned_comment (multiple contributors — uses 'you all' + matrix):" +body=$(cla_render_unsigned_comment "https://e.x/CLA.md" "I sign" "" \ + '{"signed":["alice"],"unsigned":["bob","carol"]}') +case "$body" in *"that you all sign"*) ok=1 ;; *) ok=0 ;; esac +assert_eq "uses plural 'you all' for >1 contributor" "1" "$ok" +case "$body" in *"**1** out of **3** committers have signed the CLA"*) ok=1 ;; *) ok=0 ;; esac +assert_eq "shows '1 out of 3' summary" "1" "$ok" +case "$body" in *":white_check_mark: [alice](https://github.com/alice)"*) ok=1 ;; *) ok=0 ;; esac +assert_eq "alice (signed) shown with check + profile link" "1" "$ok" +case "$body" in *":x: @bob"*":x: @carol"*) ok=1 ;; *) ok=0 ;; esac +assert_eq "bob and carol (unsigned) shown with x" "1" "$ok" + +echo +echo "cla_render_unsigned_comment (with unknown committer warning):" +body=$(cla_render_unsigned_comment "https://e.x/CLA.md" "I sign" "" \ + '{"signed":[],"unsigned":["bob"],"unknown":[{"name":"Anonymous Coward","email":"anon@example.com"}]}') +case "$body" in *"Anonymous Coward"*"seems not to be a GitHub user"*) ok=1 ;; *) ok=0 ;; esac +assert_eq "names the unknown committer with the standard warning" "1" "$ok" +case "$body" in *"add the email address used for this commit to your account"*) ok=1 ;; *) ok=0 ;; esac +assert_eq "links to GitHub help on email linking" "1" "$ok" + +body=$(cla_render_unsigned_comment "https://e.x/CLA.md" "I sign" "" \ + '{"signed":[],"unsigned":["bob"],"unknown":[{"name":"X","email":"x@y"},{"name":"Y","email":"y@z"}]}') +case "$body" in *"**X, Y** seem not to be a GitHub user"*) ok=1 ;; *) ok=0 ;; esac +assert_eq "uses plural 'seem' for >1 unknown" "1" "$ok" + +body=$(cla_render_unsigned_comment "https://e.x/CLA.md" "I sign" "" \ + '{"signed":[],"unsigned":["bob"]}') +case "$body" in *"not to be a GitHub user"*) ok=1 ;; *) ok=0 ;; esac +assert_eq "no unknown section when status_json has no .unknown key" "0" "$ok" + +echo +echo "cla_render_signed_comment:" +body=$(cla_render_signed_comment "") +case "$body" in *"All contributors have signed the CLA"*) ok=1 ;; *) ok=0 ;; esac +assert_eq "matches action's all-signed wording" "1" "$ok" +case "$body" in *"✍️"*"✅"*) ok=1 ;; *) ok=0 ;; esac +assert_eq "includes the celebratory emoji" "1" "$ok" +case "$body" in *""*) ok=1 ;; *) ok=0 ;; esac +assert_eq "includes sticky-comment marker" "1" "$ok" + +echo +echo "cla_init_signatures:" +NEW="$TMPDIR/sub/cla.json" +cla_init_signatures "$NEW" +[ -f "$NEW" ] && exists=1 || exists=0 +assert_eq "creates parent directory + file" "1" "$exists" +assert_eq "initial structure is empty array" \ + '{"signedContributors":[]}' "$(jq -c . "$NEW")" + +echo '{"signedContributors":[{"name":"x","id":1,"signed_at":"y","pull_request_no":1}]}' > "$NEW" +cla_init_signatures "$NEW" +assert_eq "existing signatures file is preserved" \ + "x" "$(jq -r '.signedContributors[0].name' "$NEW")" + +echo +echo "Results: $PASS passed, $FAIL failed" +[ "$FAIL" -eq 0 ] diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 266882a1..ffc738df 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -27,6 +27,9 @@ jobs: - name: Run tests run: bun test + - name: CLA logic tests + run: bash .github/cla/cla.test.sh + - name: Build run: bun run build diff --git a/.github/workflows/cla.yml b/.github/workflows/cla.yml new file mode 100644 index 00000000..cf89e6d0 --- /dev/null +++ b/.github/workflows/cla.yml @@ -0,0 +1,58 @@ +name: CLA + +# All logic lives in .github/cla/cla.sh and is unit-tested by +# .github/cla/cla.test.sh (run by ci.yml). This workflow is a thin orchestrator +# that sets the env, checks out main so the script can read/write the +# signatures file, and invokes the script. + +on: + pull_request_target: + types: [opened, synchronize, reopened] + issue_comment: + types: [created] + +concurrency: + group: cla-${{ github.event.pull_request.number || github.event.issue.number }} + cancel-in-progress: false + +permissions: + contents: write + pull-requests: write + statuses: write + +jobs: + cla: + runs-on: ubuntu-latest + if: | + github.event_name == 'pull_request_target' || + (github.event_name == 'issue_comment' && + github.event.issue.pull_request != null && + (contains(github.event.comment.body, 'I have read the CLA Document and I hereby sign the CLA') || + contains(github.event.comment.body, 'recheck'))) + steps: + - name: Checkout main + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + ref: main + token: ${{ secrets.GITHUB_TOKEN }} + + - name: Process CLA + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + REPO: ${{ github.repository }} + PR_NUMBER: ${{ github.event.pull_request.number || github.event.issue.number }} + EVENT_NAME: ${{ github.event_name }} + COMMENT_USER_LOGIN: ${{ github.event.comment.user.login }} + COMMENT_USER_ID: ${{ github.event.comment.user.id }} + # Always-allowlisted: bots, plus named maintainer handles as a + # safety net (in case the org-membership lookup fails for any + # reason — private membership, transient API error, etc.). + ALLOWLIST: 'dependabot[bot] github-actions[bot] renovate[bot] jedrazb' + # Auto-allowlist members of this org. Public members are recognized + # via the workflow's GITHUB_TOKEN; private members will be prompted + # to sign once (then they're recorded like anyone else). To turn the + # org check off entirely, leave CLA_ORG empty. + CLA_ORG: 'eigenpal' + CLA_URL: ${{ github.server_url }}/${{ github.repository }}/blob/main/CLA.md + SIGN_PHRASE: 'I have read the CLA Document and I hereby sign the CLA' + run: bash .github/cla/cla.sh diff --git a/CLA.md b/CLA.md new file mode 100644 index 00000000..82281384 --- /dev/null +++ b/CLA.md @@ -0,0 +1,114 @@ +# Eigenpal Individual Contributor License Agreement + +Thank you for your interest in contributing to software projects +maintained by Eigenpal ("Us"). This Agreement clarifies the +intellectual property license granted with Contributions from any +person or entity. This license is for Your protection as a +Contributor as well as the protection of Us and recipients of +software distributed by Us; it does not change Your rights to use +Your own Contributions for any other purpose. + +You accept and agree to the following terms and conditions for Your +present and future Contributions submitted to Us. Except for the +license granted herein to Us and recipients of software distributed +by Us, You reserve all right, title, and interest in and to Your +Contributions. + +## 1. Definitions + +"You" (or "Your") shall mean the copyright owner or legal entity +authorized by the copyright owner that is making this Agreement +with Us. + +"Contribution" shall mean any original work of authorship, including +any modifications or additions to an existing work, that is +intentionally submitted by You to Us for inclusion in, or +documentation of, any of the products owned or managed by Us +(the "Work"). For the purposes of this definition, "submitted" +means any form of electronic, verbal, or written communication +sent to Us or our representatives, including but not limited to +communication on electronic mailing lists, source code control +systems, and issue tracking systems that are managed by, or on +behalf of, Us for the purpose of discussing and improving the +Work, but excluding communication that is conspicuously marked or +otherwise designated in writing by You as "Not a Contribution." + +## 2. Grant of Copyright License + +Subject to the terms and conditions of this Agreement, You hereby +grant to Us and to recipients of software distributed by Us a +perpetual, worldwide, non-exclusive, no-charge, royalty-free, +irrevocable copyright license to reproduce, prepare derivative +works of, publicly display, publicly perform, sublicense, and +distribute Your Contributions and such derivative works. + +## 3. Grant of Patent License + +Subject to the terms and conditions of this Agreement, You hereby +grant to Us and to recipients of software distributed by Us a +perpetual, worldwide, non-exclusive, no-charge, royalty-free, +irrevocable (except as stated in this section) patent license to +make, have made, use, offer to sell, sell, import, and otherwise +transfer the Work, where such license applies only to those +patent claims licensable by You that are necessarily infringed by +Your Contribution(s) alone or by combination of Your +Contribution(s) with the Work to which such Contribution(s) was +submitted. If any entity institutes patent litigation against You +or any other entity (including a cross-claim or counterclaim in a +lawsuit) alleging that Your Contribution, or the Work to which +You have contributed, constitutes direct or contributory patent +infringement, then any patent licenses granted to that entity +under this Agreement for that Contribution or Work shall terminate +as of the date such litigation is filed. + +## 4. Representations + +You represent that You are legally entitled to grant the above +license. If Your employer(s) has rights to intellectual property +that You create that includes Your Contributions, You represent +that You have received permission to make Contributions on behalf +of that employer, that Your employer has waived such rights for +Your Contributions to Us, or that Your employer has executed a +separate Corporate Contributor License Agreement with Us. + +You represent that each of Your Contributions is Your original +creation (see Section 7 for submissions on behalf of others). You +represent that Your Contribution submissions include complete +details of any third-party license or other restriction (including, +but not limited to, related patents and trademarks) of which You +are personally aware and which are associated with any part of +Your Contributions. + +## 5. Support + +You are not expected to provide support for Your Contributions, +except to the extent You desire to provide support. You may +provide support for free, for a fee, or not at all. Unless +required by applicable law or agreed to in writing, You provide +Your Contributions on an "AS IS" BASIS, WITHOUT WARRANTIES OR +CONDITIONS OF ANY KIND, either express or implied, including, +without limitation, any warranties or conditions of TITLE, +NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR +PURPOSE. + +## 6. Notification + +You agree to notify Us of any facts or circumstances of which You +become aware that would make these representations inaccurate in +any respect. + +## 7. Submissions on behalf of others + +Should You wish to submit work that is not Your original creation, +You may submit it to Us separately from any Contribution, +identifying the complete details of its source and of any license +or other restriction (including, but not limited to, related +patents, trademarks, and license agreements) of which You are +personally aware, and conspicuously marking the work as +"Submitted on behalf of a third-party: [named here]". + +--- + +By signing this Agreement on a pull request as instructed by the +project's CLA assistant, You accept and agree to these terms for +Your present and future Contributions to Eigenpal projects. diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 0e8d11f0..3179074c 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -49,6 +49,10 @@ bun run lint:fix bun run format ``` +## Contributor License Agreement + +Contributors are required to sign our [Contributor License Agreement](CLA.md). The CLA assistant will leave a comment on your first pull request with signing instructions — one short comment, about 30 seconds. That signature covers all of your future contributions. + ## Making Changes 1. **Fork** the repository and create a branch from `main` @@ -59,7 +63,7 @@ bun run format ```bash bun run typecheck && bun test && bun run build ``` -6. **Submit a PR** against `main` +6. **Submit a PR** against `main` — the CLA bot will prompt you on your first one ## Architecture Overview diff --git a/README.md b/README.md index f42f4d81..f139dc33 100644 --- a/README.md +++ b/README.md @@ -73,6 +73,10 @@ Examples: [Vite](examples/vite) | [Next.js](examples/nextjs) | [Remix](examples/ **[Documentation](https://www.docx-editor.dev/docs)** | **[Props & Ref Methods](https://www.docx-editor.dev/docs/props)** | **[Plugins](https://www.docx-editor.dev/docs/plugins)** | **[Architecture](https://www.docx-editor.dev/docs/architecture)** +## Contributing + +Contributions welcome. See [CONTRIBUTING.md](CONTRIBUTING.md) for setup, tests, and the one-time CLA signature. + ## Translations | Locale | Language | Coverage |