> ## Documentation Index
> Fetch the complete documentation index at: https://docs.katilyst.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Integrating to GitHub

> Award credit when a champion merges security work, using a GitHub Actions workflow.

This guide connects GitHub to Katilyst with a workflow file. When a champion merges a pull request carrying security work, the workflow posts the event to Katilyst and the champion earns credit.

Unlike the no-code integrations, this one lives in your repository as a file your team reviews and versions like any other code. The whole integration is about twenty lines.

The pattern applies to any GitHub event you can act on. How you recognize security work, and how you resolve a GitHub user to a Katilyst User, depend on how your organization works — treat the specifics below as a worked example.

## Prerequisites

* **A Katilyst API key.** An Account Admin generates one from the Admin Portal. Keys are shown once when generated, and a lost key cannot be recovered.
* **Permission to add workflow files and repository secrets** in the repositories you are instrumenting.
* **A way to resolve GitHub users to email addresses.** This is the part that needs thought — see below.

## Resolve identity first

Katilyst identifies a User by email address. GitHub webhook payloads carry a **username**, and the email field is frequently empty because users can keep it private. Settle this before writing any workflow, because every other part is straightforward and this one is not.

Three approaches, in the order most organizations land on them.

> **Derive the email from the username**
>
> If your GitHub accounts are provisioned through SSO, usernames usually match the local part of a work email, so `jsmith` becomes `jsmith@example.com`.
>
> This is the simplest approach and it works well in GitHub Enterprise Cloud with SAML. It fails for anyone whose GitHub account predates the SSO rollout, and for contractors on personal accounts.

> **Keep a mapping file**
>
> Store a small JSON or CSV file in the repository mapping GitHub usernames to email addresses, and look up the author in the workflow.
>
> This handles the exceptions the derivation approach misses, and it has the advantage of being reviewable. It needs maintaining as people join and leave, which is work somebody has to own.

> **Query your identity provider**
>
> If you run SCIM provisioning, your identity provider holds the authoritative mapping and can be queried at run time.
>
> The most robust option and the most work. Worth it when you are instrumenting many repositories and the mapping would otherwise drift.

> [!WARNING]
>
> Do not use the commit author email without checking it first. GitHub substitutes a `users.noreply.github.com` address when a user enables email privacy, and that address will not match anyone in your Katilyst Project.

The worked example below derives the email from the username, which is the right starting point for most teams.

## Decide your three mappings

As with any integration, three choices determine how this is built.

| What you are deciding | Common approaches | Where it lands |
| - | - | - |
| **What signals the work is complete** | A merged pull request; a closed code scanning alert; a closed issue; a published release | The workflow trigger and its condition |
| **What marks the work as security work** | A label such as `security`; a path filter on sensitive directories; a linked scanning alert; a branch naming convention | The `if` condition on the job |
| **Who earns the credit** | The pull request author; the merger; a reviewer from the security team | The `userEmail` value |

The third deserves the most thought. The author wrote the fix, the merger approved it, and a reviewer may have been the one who caught the issue. Your program decides which of those behaviours it wants more of.

> [!WARNING]
>
> Frequency limits configured on an Available Action do not apply to actions logged through the API, so the `if` condition on your job is the only thing limiting how often this workflow awards credit. A batch of fifty merges logs fifty actions. If that is realistic in your repository, constrain it in the workflow rather than relying on the limit set in Katilyst.

## Configure the action in Katilyst

**1. Create an Available Action**

In Katilyst, create an Instance action named something recognizable, such as **GitHub — Security Fix Merged**. Set the Attribute it changes, the amount, and any frequency limit.

> [!TIP]
>
> Instance actions award credit every time they are logged, which suits merged pull requests. See [Core concepts](https://docs.katilyst.com/core-concepts.md) for how this differs from State actions.

**2. Find your project ID**

Call [List projects](https://docs.katilyst.com/api-reference/v1/list-projects.md) and note the `id` of the program you are integrating with.

```bash theme={null}
curl https://api.katilyst.com/v1/projects \
  -H "Authorization: API-Key your_key_here"
```

**3. Find your action ID**

Call [List a project's available actions](https://docs.katilyst.com/api-reference/v1/list-a-projects-available-actions.md) and note the `id` of the action you created.

```bash theme={null}
curl https://api.katilyst.com/v1/projects/{projectId}/available-actions \
  -H "Authorization: API-Key your_key_here"
```

## Store the API key as a secret

In the repository, go to **Settings → Secrets and variables → Actions → New repository secret**. Name it `KATILYST_API_KEY` and paste the key as the value.

Store the key at the organization level instead if you are instrumenting several repositories, so there is one place to rotate it.

> [!NOTE]
>
> GitHub encrypts secrets and masks them in run logs, so the key never appears in output. This is a real advantage over integrations that store credentials in a rule configuration where anyone with edit access can read them.

## Add the workflow

Create `.github/workflows/katilyst.yml` in the repository.

```yaml .github/workflows/katilyst.yml theme={null}
name: Log security contribution to Katilyst

on:
  pull_request:
    types: [closed]

permissions: {}

jobs:
  log-contribution:
    if: >
      github.event.pull_request.merged == true &&
      contains(github.event.pull_request.labels.*.name, 'security')
    runs-on: ubuntu-latest
    steps:
      - name: Post logged action
        env:
          PR_TITLE: ${{ github.event.pull_request.title }}
          PR_NUMBER: ${{ github.event.pull_request.number }}
          PR_AUTHOR: ${{ github.event.pull_request.user.login }}
          KATILYST_KEY: ${{ secrets.KATILYST_API_KEY }}
          ACTION_ID: 1111-2222-3333-action-id
          EMAIL_DOMAIN: example.com
        run: |
          jq -n \
            --arg email "${PR_AUTHOR}@${EMAIL_DOMAIN}" \
            --arg actionId "${ACTION_ID}" \
            --arg details "${PR_TITLE} (#${PR_NUMBER})" \
            '{
              userEmail:         $email,
              availableActionId: $actionId,
              creator:           "GitHub Actions",
              details:           $details
            }' \
          | curl --fail --silent --show-error \
              --request POST \
              --url https://api.katilyst.com/v1/logged-actions \
              --header "Authorization: API-Key ${KATILYST_KEY}" \
              --header "Content-Type: application/json" \
              --data @-
```

### Why it is written this way

Two details are worth keeping if you adapt this.

**Untrusted values go through `env`, never directly into the shell.** A pull request title is attacker-controlled text, and interpolating `${{ github.event.pull_request.title }}` straight into a `run:` block lets anyone who can open a pull request execute commands on your runner. Passing it as an environment variable and referencing `${PR_TITLE}` closes that. This is GitHub's own guidance on script injection and it applies to any workflow that touches pull request content.

**`jq` builds the JSON rather than string interpolation.** A title containing a quote or a backslash would produce malformed JSON and a confusing `400`. `jq` escapes correctly and is preinstalled on GitHub-hosted runners.

`permissions: {}` grants the job no access to the repository, which is all it needs since it never checks out code.

### Adapting it

| To change | Edit |
| - | - |
| The label that marks security work | The `contains(...)` condition |
| Crediting the merger instead of the author | `github.event.pull_request.merged_by.login` |
| The email domain | `EMAIL_DOMAIN` |
| Triggering on code scanning alerts | `on: code_scanning_alert` with `types: [closed_by_user, fixed]` |

For several repositories, move the body of this workflow into a reusable workflow or a composite action and call it from each one, so there is a single place to update when the action ID or the identity rule changes.

> [!WARNING]
>
> If your contributions arrive as pull requests from forks, secrets are not available to workflows triggered by `pull_request`. Internal repositories where people branch within the same repository are unaffected. If forks are part of your flow, bring this to whoever owns your CI configuration rather than reaching for `pull_request_target`, which carries its own risks.

## Verify

Open a pull request, apply your security label, and merge it.

1. Open the **Actions** tab in the repository and find the run. A successful step prints nothing, since `curl --silent` stays quiet on success and `--fail` makes a non-2xx response fail the step.
2. Call [List a user's logged actions](https://docs.katilyst.com/api-reference/v1/list-a-users-logged-actions.md) and confirm the award landed.

```bash theme={null}
curl "https://api.katilyst.com/v1/projects/{projectId}/users/{userId}/logged-actions" \
  -H "Authorization: API-Key your_key_here"
```

Read the `status` field on the result. `APPROVED` means the reward applied.

## Troubleshooting

* **The workflow never runs.** The `if` condition did not match. Comment it out temporarily, merge a test pull request, and confirm the job fires at all before narrowing again. An unmatched condition produces no run and no error, which makes it easy to misread as a broken workflow.
* **The workflow succeeds and no reward appears.** Read the action back and check its `status`. `PENDING` means the Available Action requires admin approval and is waiting for it, which is program configuration rather than a workflow fault. A `LIMIT_EXCEEDED_TIMEFRAME` record came from someone submitting inside the platform rather than from this workflow — frequency limits do not apply to API calls.
* **`curl` exits 22.** `--fail` returning this means Katilyst rejected the request outright. Add `--include` temporarily to see the response body and status code in the run log. A `409` here means the User is not eligible for that action, which is a program rule rather than an integration fault.
* **A `400` response.** Usually the derived email does not match a User in that Katilyst Project. Call [List a project's users](https://docs.katilyst.com/api-reference/v1/list-a-projects-users.md) and compare against what the workflow built.
* **A `401` response.** The key is invalid, the secret is missing or misnamed, or the `API-Key` prefix was dropped from the header value. An invalid key is the most common cause — confirm it has not been rotated or revoked since the secret was stored.
* **The run log shows an empty email.** `PR_AUTHOR` resolved to nothing, which usually means the event payload differs from what the workflow expects. Print the value to the log before the curl call to see what arrived.

## Related

> **[Core concepts](https://docs.katilyst.com/core-concepts.md)**
>
> Instance versus State actions, guardrails, and how rewards are decided.

> **[Log an action](https://docs.katilyst.com/api-reference/v1/log-an-action.md)**
>
> Full reference for the endpoint this workflow calls.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.