This repository is a security-first template for public events operated through GitHub. It provides self-service participant registration, unanimous team formation, encrypted and actor-bound submissions, an append-only Git-backed state journal, isolated scoring hooks, lifecycle controls, and an auditable leaderboard.
The template intentionally contains no maintained Node.js or Python application
layer. Repository automation is Bash 3.2-compatible and delegates canonical
serialization, signatures, encryption, and protocol validation to the released
Go eventctl CLI.
Important
A repository generated from this template starts disabled. GitHub copies the repository files, but it does not copy or guarantee the repository's rulesets, Actions settings, GitHub App installation, secrets, environments, variables, collaborators, or other control-plane settings. An organizer must run and verify bootstrap before opening the event.
There are four repository roles in a release-qualified event:
pythonhk/github-native-event-starteris this reusable source template.- A disposable public canary is generated from one immutable starter release, exercised with unrelated accounts, and archived as release evidence.
- Only after the canary passes, an organizer generates a different production event repository from the same reviewed release. Do not fork the starter or reuse the canary to create production.
- Participants fork the production event repository and open submission pull requests back to it.
The canary and production repository are each authoritative only for their own event identity. Their configurations, keys, Apps, state, and evidence are never interchangeable.
The design separates work into four trust lanes:
- Untrusted validation runs for public-fork pull requests with read-only contents access, no secrets, and no self-hosted runner. It never executes participant-controlled code.
- Scoring accepts a digest-bound, schema-checked artifact only through an external isolated judge. Protocol v1 ships no in-repository scoring mode and never executes participant content in GitHub Actions.
- State writing uses a narrowly installed GitHub App and applies a pure,
idempotent transition to the protected
event-statebranch. It never checks out or executes a participant branch. - Reporting publishes bounded results and an optional PR comment without receiving the state-writer credential.
Participant identity is the numeric GitHub account ID supplied by trusted GitHub event/API metadata. Usernames are display labels only. Packaging signs an inner plaintext manifest and a public encrypted-bundle envelope. After the bundle is committed and a pull request exists, a separate signed submission request binds their digests to the event, epoch, actor, team, attempt ID, pull request number and immutable database ID, numeric fork repository, branch, exact head commit, and configuration digest. This post-push third layer avoids the impossible requirement for a commit to contain a signature over its own SHA. The pull request and branch locate the attempt; the commit SHA seals the content against later mutation.
See SECURITY.md for the full trust model and reporting process.
Prerequisites:
- organization permission to create a public repository and install the event-specific controls;
- stable GitHub CLI (
gh) 2.93.0 or newer, Git,jq,rg, OpenSSL, and Bash 3.2 or newer; - a dedicated state-writer GitHub App installation or the permission to create one;
- one explicit derived-repository reviewer: either a GitHub user with effective write/admin access or a visible same-organization team with push access; and
- the exact
eventctlrelease pinned bytools/eventctl.lock.json.
Then:
Set EVENT_REPOSITORY, EVENT_EXPECTED_ID, and EVENT_CODEOWNER once from the
generated repository and approved event plan. Do not copy an example identity
into a live event.
EVENT_REPOSITORY='{{EVENT_REPOSITORY}}'
EVENT_EXPECTED_ID='{{EVENT_ID}}'
EVENT_CODEOWNER='{{EVENT_CODEOWNER}}'-
Use the executable starter-publication ceremony to verify the source's active rulesets and immutable release, open and read back a bounded template window, and create the disposable public canary. The source release tag and default branch must equal the same reviewed SHA before and after generation, and the complete generated tree must match.
-
Clone the generated repository. Do not initialize it by copying this template's Git history. Choose the derived repository's reviewer in exact
@USERor@ORG/TEAMform and retain it for every bootstrap command. The active starter's.github/CODEOWNERSprotects this reusable template and is not automatically the right reviewer for a derived event. -
After the lock has been updated from its fail-closed
UNRELEASEDsentinel, install and verify the lockedeventctl, then runeventctl doctor:./tools/verify-eventctl-lock.sh ./tools/install-eventctl.sh ./tools/eventctl version --json ./tools/eventctl doctor
-
Follow the event customization, state-writer App, and organizer quick-start runbooks. Render the participant guide and exact
event/terms.md; create and read back the private organization-owned, selected-repository App; create the offline root, delegated configuration, submission-recipient, scorer-result, and receipt keys; bind the terms digest and live App slug; then produce and verifyevent/config-delegation.jsonand signedevent/event.yaml. Keep the event disabled and indraft. The shipped examples are deliberately unverifiable and must never be bootstrapped by copying them unchanged. -
Preview bootstrap without mutation:
./scripts/bootstrap/bootstrap.sh plan \ --repo "$EVENT_REPOSITORY" \ --config event/event.yaml \ --event-id "$EVENT_EXPECTED_ID" \ --codeowner "$EVENT_CODEOWNER" \ --root-key /secure/path/organizer-root-public.json
-
Review the plan, then apply and verify the inactive baseline. Bootstrap deliberately installs rulesets disabled and leaves the sole control-plane gate,
EVENT_TEMPLATE_ACTIVATED, false:./scripts/bootstrap/bootstrap.sh apply \ --repo "$EVENT_REPOSITORY" \ --config event/event.yaml \ --event-id "$EVENT_EXPECTED_ID" \ --codeowner "$EVENT_CODEOWNER" \ --root-key /secure/path/organizer-root-public.json
applyresolves the reviewer and renders.github/CODEOWNERSlocally fromsettings/CODEOWNERS.template; it does not commit or push it. Put the rendered CODEOWNERS, signed configuration/delegation, event guide, exact terms, and all rendered documentation in one independently reviewed pull request. Merge that change to the exact default branch beforeverify. -
Independently read back and compare the inactive persisted controls:
./scripts/bootstrap/bootstrap.sh verify \ --repo "$EVENT_REPOSITORY" \ --config event/event.yaml \ --event-id "$EVENT_EXPECTED_ID" \ --codeowner "$EVENT_CODEOWNER" \ --root-key /secure/path/organizer-root-public.json
verifymust read back byte-identical CODEOWNERS from the default branch, confirm GitHub reports no CODEOWNERS parse errors there, and recheck current user/team access before any later active-ruleset apply or activation. -
Re-run the template-residue and terms/config checks on the merged default branch. On its exact current HEAD, seed one completed, successful
event-template / gaterun and one completed, successfulevent-template / bats (macOS 15, Bash 3.2)run. Bootstrap requires both names exactly once from the same positivegithub-actionsApp ID and binds thatintegration_idto both active required-check contexts. Provision the already reviewed state-writer App credentials, and repeatplan,apply, andverifywithEVENT_ACTIVATE_RULESETS=trueand the numericSTATE_WRITER_INTEGRATION_ID, always passing the same reviewed--codeowner "$EVENT_CODEOWNER". The active release-tag creation ruleset contains only creation and gives its sole bypass to that live-resolved numeric User/Team. A separate no-bypass integrity ruleset blocks every tag update, deletion, and non-fast-forward move. Only after exact default-branch CODEOWNERS content/errors/access readback, both live checks, verifiedevent-stategenesis, App, and active rulesets should bootstrapactivaterun with that same--codeowner. Activation alone neither opens nor enables the current target. -
Run the exact hosted-canary harness and complete every canary item in TEMPLATE_CHECKLIST.md. Retain the archived canary and its signed evidence for independent review.
-
Repeat the full source-publication and organizer ceremony for a new production repository. Require a different repository name and numeric ID, event ID, configuration/delegation, keys, App/installation, environment secrets, genesis, and evidence root. Only then use the quick start's production-opening section; never unarchive or reuse the canary.
The bootstrap commands are deliberately split: plan is read-only, apply
mutates the generated repository, and verify performs fresh API readback.
Successful script exit is not a substitute for verify.
The complete setup procedure is in docs/organizer/quick-start.md.
Participants normally perform this sequence:
review event details and exact terms/digest
|
install and verify eventctl
|
generate and back up a signing key
|
register the key to the authenticated GitHub account
|
propose or consent to one fixed team manifest
|
fork the event repository and prepare an encrypted submission
|
open a pull request and verify the signed reservation and applicable terminal receipt
Start with the event-specific participant details and participant quick start. Private keys and plaintext confidential submissions must never be committed.
The normal state progression is:
draft -> registration_open -> formation_open -> submissions_open
-> frozen -> closed -> archived
enabled is a separate emergency gate. Pausing immediately blocks participant
intake/reservation, judge dispatch, and fresh scorer finalization even if a
workflow was already queued; queued writers must refetch state before
committing. It still permits idempotent receipt publication/reconciliation for
effects already committed, authenticated organizer control/audit operations,
and receipt-bearing organizer cancellation of an existing reservation. A
cancellation retains the quota charge and creates no scorer evidence or score.
Closing and archiving are explicit authenticated organizer state transitions,
not edits to Git history.
See docs/organizer/lifecycle.md.
The protected orphan event-state branch contains an append-only, hash-linked
journal and deterministic materialized views. It contains public protocol state
only: no secrets, private keys, confidential plaintext, email addresses, or raw
unbounded participant text.
Never force-push, rewrite, squash, or manually repair this branch. Verify and replay the journal with the documented tooling. Protocol v1 has no compensating recovery transition, configuration adoption, event-epoch increment, or trusted state-branch switch. A mistake or compromise that prevents safe continuation requires disabling and preserving the old event, then creating a brand-new event repository with fresh genesis and participant re-registration.
See docs/recovery/journal-and-replay.md and docs/recovery/incident-response.md.
- Normative protocol v1
- Threat model and security invariants
- Executable security test matrix
- Architecture decisions
- Starter publication and exact generation
- Organizer quick start
- Disposable hosted-canary harness
- Generated-event customization
- State-writer GitHub App
- Configuration and invariants
- Lifecycle and shutdown
- Exact archive runbook
- Generated-event upgrades
- Release and verification evidence
- Participant CLI installation
- Event-specific participant details
- Keys and registration
- Unanimous team formation
- Encrypted submissions
- Receipt retrieval and verification
- Scoring adapter contract
- External judge isolation
- Journal replay and backup
- Incident response
Security invariants and protocol compatibility take precedence over convenience. Read CONTRIBUTING.md before changing workflows, schemas, state reducers, bootstrap controls, or cryptographic envelopes.
This project is licensed under the MIT License.