Audit Log
An audit log is an append only record of who changed what and when. What an entry carries, how a GitHub Actions team queries one, and how to export it.
An audit log is an append only record of who changed what and when, kept so that a change can be reviewed after the fact. Each entry names an actor, an action, a target, and a timestamp, and entries are added rather than edited, so the sequence read a year later matches the sequence written at the time.
For a team running GitHub Actions, the audit log is where configuration questions land: which account added a runner, who moved a repository into a runner group, and when a permission was widened. Job output lives elsewhere.
Definition
An audit log is defined by four properties, and a record missing any one of them answers a reviewer's question only partly.
| Property | What it means | Why a reviewer needs it |
|---|---|---|
| Append only | Entries are added; existing entries are never rewritten or deleted in place | A record that can be edited proves nothing about the past |
| Attributed | Every entry names the identity that acted: a user account, an application, or a machine identity | "Someone changed the setting" closes no review |
| Timestamped | Every entry carries a fixed time, normally in UTC | Ordering and windows such as "during Q2" depend on it |
| Retained and readable | Entries survive for a defined period and can be read by people other than the actor | Self-reported history is not independent |
Application logs, metrics, and traces exist to explain what a running system is doing right now, and they are routinely sampled, rotated, and dropped. An audit log serves the opposite time horizon: it is read months later by someone reconstructing a decision, often during an access review or an incident postmortem.
What an entry carries
GitHub exposes organization audit events through the REST endpoint GET /orgs/{org}/audit-log, documented in the GitHub REST API reference for organization audit logs, checked on 2026-08-13. Each entry in the response is a flat JSON object with a stable set of fields.
| Field | Content |
|---|---|
action | The event name in category.verb form, such as self_hosted_runner.created |
actor | The account that performed the action |
actor_ip | The source address, present when the organization has enabled IP display |
created_at | Event time as a Unix timestamp in milliseconds |
org | The organization the event belongs to |
repo | The repository the event targeted, when the action has one |
user | The account the action was performed on, for membership and permission events |
_document_id | A unique id per entry, useful for deduplicating repeated exports |
Two parameters shape the result set. include selects the event families to return and accepts web, git, or all. phrase carries a search string built from qualifiers, the same syntax the web interface uses.
Qualifiers that answer a review question
| Qualifier | Example | Matches |
|---|---|---|
action: | action:self_hosted_runner | Every event in that category, or a single event with the full category.verb form |
actor: | actor:dana-eng | Events performed by one account |
repo: | repo:octo-org/api | Events targeting one repository |
created: | created:2026-04-01..2026-06-30 | Events inside a date window |
country: | country:DE | Events by source country |
The qualifier list and the date range syntax are documented in GitHub's guide to reviewing the audit log for an organization, checked on 2026-08-13. The event names themselves, grouped by category, are in audit log events for your organization. Runner changes sit in the self_hosted_runner and self_hosted_runner_group categories.
Retention is the property teams discover late. GitHub keeps audit events for a documented window rather than forever, so a review that reaches back further than that window needs the events copied somewhere the organization controls before they age out.
Example
A reviewer opens a quarterly access review with a narrow question: which account changed a runner setting in the last quarter, and when. The question has two halves, an actor and a time, which is exactly the shape an audit log is built to answer.
In the web interface the path is organization Settings, then Logs, then Audit log, with this search phrase:
action:self_hosted_runner created:2026-04-01..2026-06-30The same query through the API returns JSON that can be attached to the review:
gh api --paginate "/orgs/octo-org/audit-log" \
-f include=web \
-f phrase="action:self_hosted_runner created:2026-04-01..2026-06-30"A matching entry looks like this, and it closes both halves of the question:
{
"action": "self_hosted_runner.created",
"actor": "dana-eng",
"org": "octo-org",
"created_at": 1780531200000,
"_document_id": "aBc123-example"
}Running that query by hand once a quarter works until the window in question is older than the retention period. A scheduled workflow keeps the evidence instead:
name: export-audit-log
on:
schedule:
- cron: '0 6 1 * *'
workflow_dispatch:
permissions:
contents: read
jobs:
export:
runs-on: ubuntu-latest
steps:
- name: Pull the last month of runner configuration events
env:
GH_TOKEN: ${{ secrets.AUDIT_LOG_TOKEN }}
run: |
since=$(date -u -d '1 month ago' +%Y-%m-%d)
gh api --paginate "/orgs/${{ github.repository_owner }}/audit-log" \
-f include=web \
-f phrase="action:self_hosted_runner created:>=${since}" \
> runner-events.json
- name: Keep the export with the review record
uses: actions/upload-artifact@v4
with:
name: runner-audit-events
path: runner-events.json
retention-days: 90Three details in that workflow matter. The token comes from a secret because the GITHUB_TOKEN minted for a run is scoped to one repository and cannot read organization audit events; the endpoint requires an organization owner and the read:audit_log scope. --paginate is present because a quarter of events for a large organization exceeds one page. And _document_id in each entry lets a downstream job drop duplicates when two monthly runs overlap at a boundary.
An artifact is a first step rather than an archive. Copying the export into storage the workflow cannot rewrite, and that a repository administrator cannot clear, keeps the append only property intact once the events leave GitHub.
Related Terms
- How to find which account changed a runner setting and when: the query, the event categories to search, and what to capture for a review.
- What a GitHub Actions security review asks for: the evidence a security questionnaire expects around runners, isolation, and access.
- Single sign-on, the identity layer behind the actor field: how a central identity provider makes the actor on each entry map to a real person.
- WarpBuild runner security documentation: how runner isolation, storage, and secrets handling are documented.
- WarpBuild single sign-on setup: the SAML 2.0 and OIDC connection steps for an organization.
- WarpBuild pricing: per minute rates by runner type.
FAQ
What separates an audit log from an application log?
Purpose and write behavior. An application log records what a program did so an engineer can debug it, and it can be rotated, sampled, or rewritten. An audit log records who changed a configuration or a permission so a reviewer can reconstruct that change later, so entries are written once, carry an identity and a timestamp, and are readable by people who did not make the change.
Which GitHub Actions changes appear in an organization audit log?
Configuration and permission changes rather than job output. Adding or removing a self-hosted runner, moving a runner between runner groups, changing Actions settings for a repository, and changing who can administer those settings all produce entries. What a job printed lives in the workflow run logs instead, under a separate retention setting.
Can a workflow read the audit log through the REST API?
Yes, with a token that carries the read:audit_log scope and belongs to an organization owner. The GITHUB_TOKEN minted for a workflow run is scoped to one repository and does not carry that scope, so a scheduled export job reads the token from a secret instead.
Start with $10 in free credits
Change the runner label in your workflow and keep the rest of your GitHub Actions setup. Runner time is billed per minute.