Skip to content

About

Disposable hosted canary; retain until independent review

Resources

Contributing

Security policy

Stars

0 stars

Watchers

0 watching

Forks

Repository files navigation

GitHub-native event starter

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.

Repository roles

There are four repository roles in a release-qualified event:

  1. pythonhk/github-native-event-starter is this reusable source template.
  2. A disposable public canary is generated from one immutable starter release, exercised with unrelated accounts, and archived as release evidence.
  3. 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.
  4. 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.

Security boundaries

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-state branch. 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.

Organizer quick start

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 eventctl release pinned by tools/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}}'
  1. 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.

  2. Clone the generated repository. Do not initialize it by copying this template's Git history. Choose the derived repository's reviewer in exact @USER or @ORG/TEAM form and retain it for every bootstrap command. The active starter's .github/CODEOWNERS protects this reusable template and is not automatically the right reviewer for a derived event.

  3. After the lock has been updated from its fail-closed UNRELEASED sentinel, install and verify the locked eventctl, then run eventctl doctor:

    ./tools/verify-eventctl-lock.sh
    ./tools/install-eventctl.sh
    ./tools/eventctl version --json
    ./tools/eventctl doctor
  4. 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 verify event/config-delegation.json and signed event/event.yaml. Keep the event disabled and in draft. The shipped examples are deliberately unverifiable and must never be bootstrapped by copying them unchanged.

  5. 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
  6. 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

    apply resolves the reviewer and renders .github/CODEOWNERS locally from settings/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 before verify.

  7. 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

    verify must 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.

  8. 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 / gate run and one completed, successful event-template / bats (macOS 15, Bash 3.2) run. Bootstrap requires both names exactly once from the same positive github-actions App ID and binds that integration_id to both active required-check contexts. Provision the already reviewed state-writer App credentials, and repeat plan, apply, and verify with EVENT_ACTIVATE_RULESETS=true and the numeric STATE_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, verified event-state genesis, App, and active rulesets should bootstrap activate run with that same --codeowner. Activation alone neither opens nor enables the current target.

  9. 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.

  10. 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.

Participant flow

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.

Event lifecycle

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.

State and recovery

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.

Documentation map

Contributing

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.

About

Disposable hosted canary; retain until independent review

Resources

Contributing

Security policy

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages