Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
59 changes: 59 additions & 0 deletions .github/workflows/add-cla-signature.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
# Manually record a CLA signature for a contributor who signed out-of-band
# (emailed us a signed PDF, etc.) and has no PR sign-comment to record.
#
# Run from the Actions tab: "Record offline CLA signature" -> Run workflow.

name: "Record offline CLA signature"

on:
workflow_dispatch:
inputs:
username:
description: "GitHub username (with or without @)"
required: true
type: string
reason:
description: "Justification, e.g. 'Signed ICLA emailed 2026-05-17, Acme Corp'"
required: true
type: string
cla_version:
description: "CLA version directory (default v1.1)"
required: false
type: string
default: "v1.1"

permissions:
contents: read

jobs:
record:
runs-on: ubuntu-latest
steps:
- name: Generate GitHub App token
id: app-token
uses: actions/create-github-app-token@v1
with:
app-id: ${{ secrets.CLA_APP_ID }}
private-key: ${{ secrets.CLA_APP_PRIVATE_KEY }}
owner: netfoundry
repositories: cla

- name: Checkout
uses: actions/checkout@v4
with:
token: ${{ steps.app-token.outputs.token }}
fetch-depth: 0

- name: Configure bot identity
run: |
git config user.email "nf-cla-bot[bot]@users.noreply.github.com"
git config user.name "nf-cla-bot[bot]"

- name: Append signature and push
env:
GH_TOKEN: ${{ steps.app-token.outputs.token }}
run: |
bash scripts/add-cla-signature.sh \
--username "${{ inputs.username }}" \
--reason "${{ inputs.reason }}" \
--cla-version "${{ inputs.cla_version }}"
4 changes: 3 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,9 @@ If you're contributing on behalf of your employer, your company needs to sign a
3. Email the signed agreement to **cla@netfoundry.io**
4. Include GitHub usernames of employees authorized to contribute

We'll add your team to our allowlist within a few business days.
We'll add your team to our allowlist within a few business days. Maintainers record offline
signatures using the [Record offline CLA signature](./SETUP.md#recording-offline-signatures)
workflow, which commits an entry to the signature ledger as the CLA bot.

## Repositories Covered

Expand Down
34 changes: 34 additions & 0 deletions SETUP.md
Original file line number Diff line number Diff line change
Expand Up @@ -138,6 +138,40 @@ To add additional users (e.g., contractors not in the org), either:
- Add them to the allowlist in `cla-workflow.yml`
- Or add them directly to the current version's `cla.json`

## Recording Offline Signatures

When a contributor (typically a corporate contributor) signs the CLA out-of-band -- emails us a
signed PDF, signs on paper, etc. -- they have no PR comment for the bot to record. Use the
**Record offline CLA signature** workflow in this repo to add them to the ledger:

1. Go to the Actions tab in `netfoundry/cla`.
2. Select "Record offline CLA signature" -> "Run workflow".
3. Fill in:
- **username** -- the contributor's GitHub handle (with or without `@`).
- **reason** -- short justification, e.g. `Signed ICLA emailed 2026-05-17, Acme Corp`. This is
stored in the JSON entry as a `note` field and in the commit message, so future maintainers
can answer "why is this person in the ledger without a sign comment?".
- **cla_version** -- defaults to `v1.1`; change when a new version is current.
4. Run. The workflow resolves the user's numeric GitHub id, appends an entry to
`<version>/cla.json`, and commits to `main` as `nf-cla-bot[bot]`.

The workflow refuses to add a duplicate (it matches by numeric id, so a later username change does
not bypass the check) and fails loudly if the GitHub user does not exist.

### Running locally

All logic lives in `scripts/add-cla-signature.sh`, so you can preview a change before pushing:

```bash
GH_TOKEN=$(gh auth token) bash scripts/add-cla-signature.sh \
--username someone \
--reason "Signed ICLA emailed 2026-05-17, Acme Corp" \
--no-push
```

`--no-push` leaves the commit local. Drop the flag (and use a token with write access to
`netfoundry/cla`) to push directly.

## CLA Documents

The workflows link to the official NetFoundry CLA PDFs:
Expand Down
115 changes: 115 additions & 0 deletions scripts/add-cla-signature.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
#!/usr/bin/env bash
# Manually record a CLA signature for a contributor who signed out-of-band
# (e.g. emailed us a signed PDF) and therefore has no signing comment on a PR.
#
# Required env:
# GH_TOKEN -- a token with contents:write on netfoundry/cla.
# In CI, this is the nf-cla-bot App token.
# Locally, a PAT with `repo` scope works.
#
# Required args:
# --username <handle> GitHub handle (with or without leading @)
# --reason <text> Free-text justification, recorded in JSON + commit
#
# Optional args:
# --cla-version <vX.Y> Defaults to v1.1
# --repo-root <path> Defaults to current directory
# --no-push Skip the git push (for local testing)
#
# Exits non-zero on any failure (user not found, already signed, etc.).

set -euo pipefail

USERNAME=""
REASON=""
CLA_VERSION="v1.1"
REPO_ROOT="$(pwd)"
DO_PUSH=1

while [[ $# -gt 0 ]]; do
case "$1" in
--username) USERNAME="$2"; shift 2 ;;
--reason) REASON="$2"; shift 2 ;;
--cla-version) CLA_VERSION="$2"; shift 2 ;;
--repo-root) REPO_ROOT="$2"; shift 2 ;;
--no-push) DO_PUSH=0; shift ;;
*) echo "Unknown argument: $1" >&2; exit 2 ;;
esac
done

USERNAME="${USERNAME#@}"

if [[ -z "$USERNAME" || -z "$REASON" ]]; then
echo "ERROR: --username and --reason are required" >&2
exit 2
fi

if [[ -z "${GH_TOKEN:-}" ]]; then
echo "ERROR: GH_TOKEN env var is required" >&2
exit 2
fi

LEDGER="$REPO_ROOT/$CLA_VERSION/cla.json"
if [[ ! -f "$LEDGER" ]]; then
echo "ERROR: ledger not found at $LEDGER" >&2
exit 2
fi

echo "Resolving GitHub user: $USERNAME"
USER_JSON=$(gh api "users/$USERNAME")
USER_ID=$(echo "$USER_JSON" | jq -r '.id')
RESOLVED_LOGIN=$(echo "$USER_JSON" | jq -r '.login')

if [[ -z "$USER_ID" || "$USER_ID" == "null" ]]; then
echo "ERROR: could not resolve user id for $USERNAME" >&2
exit 1
fi

echo "Resolved $USERNAME -> login=$RESOLVED_LOGIN id=$USER_ID"

EXISTING=$(jq --argjson id "$USER_ID" '.signedContributors[] | select(.id == $id) | .name' "$LEDGER")
if [[ -n "$EXISTING" ]]; then
echo "ERROR: user id $USER_ID ($EXISTING) already in $LEDGER -- nothing to do" >&2
exit 1
fi

NOW=$(date -u +"%Y-%m-%dT%H:%M:%SZ")

TMP=$(mktemp)
jq \
--arg name "$RESOLVED_LOGIN" \
--argjson id "$USER_ID" \
--arg created_at "$NOW" \
--arg note "$REASON" \
'.signedContributors += [{
name: $name,
id: $id,
comment_id: 0,
created_at: $created_at,
repoId: 0,
pullRequestNo: 0,
note: $note
}]' "$LEDGER" > "$TMP"
mv "$TMP" "$LEDGER"

echo "Appended entry for $RESOLVED_LOGIN ($USER_ID) to $LEDGER"

cd "$REPO_ROOT"

# Configure committer identity only if not already set (CI sets these).
if [[ -z "$(git config user.email || true)" ]]; then
git config user.email "nf-cla-bot@users.noreply.github.com"
git config user.name "nf-cla-bot"
fi

git add "$CLA_VERSION/cla.json"
git commit -m "Manually record CLA signature for @$RESOLVED_LOGIN

$REASON"

if [[ "$DO_PUSH" -eq 1 ]]; then
git push origin HEAD:main
echo "Pushed signature commit to main."
else
echo "Skipped push (--no-push). Commit is local."
fi