Skip to content
Open
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
10 changes: 5 additions & 5 deletions .github/workflows/check-submission.yml
Original file line number Diff line number Diff line change
Expand Up @@ -26,11 +26,11 @@ jobs:
permissions:
contents: read
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4
with:
persist-credentials: false

- uses: actions/checkout@v4
- uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4
with:
repository: EchoTools/echo-mod-template-repo
path: template
Expand All @@ -50,7 +50,7 @@ jobs:
ISSUE_NUMBER: ${{ github.event.issue.number }}
run: python3 scripts/inspect_submission.py --template template --smoke-host "$PWD/smoke_host.exe" --out report.json

- uses: actions/upload-artifact@v4
- uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
with:
name: report
path: report.json
Expand All @@ -63,11 +63,11 @@ jobs:
contents: read
issues: write
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4
with:
persist-credentials: false

- uses: actions/download-artifact@v4
- uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4
with:
name: report

Expand Down
120 changes: 120 additions & 0 deletions .github/workflows/gates.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
# The pull-request gates (scripts/gates.py): a conventional PR title and commits, well-formed credit
# trailers, a current approval from someone other than the author on changes to anything but docs, and
# actions pinned by commit. Each prints one JSON record; the records are kept as an artifact.
#
# Trust boundary: `pull_request` and `pull_request_review`, never `pull_request_target`. The token is
# read-only, no secret is named, nothing from the PR is executed except this repo's own scripts, and the
# title reaches the script through env only.
name: gates

on:
pull_request:
types: [opened, edited, synchronize, reopened]
pull_request_review:
types: [submitted, dismissed]

permissions: {}

# `shell: bash` runs with -eo pipefail; the default does not, and `| tee` would turn a failed gate green.
defaults:
run:
shell: bash

concurrency:
group: gates-${{ github.event.pull_request.number }}
cancel-in-progress: true

jobs:
title:
if: github.event_name == 'pull_request'
runs-on: ubuntu-24.04
permissions:
contents: read
steps:
- uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4
with:
persist-credentials: false
- name: The PR title is a conventional commit header
env:
PR_TITLE: ${{ github.event.pull_request.title }}
run: python3 -I scripts/gates.py title | tee gate-title.jsonl
- uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
if: always()
with:
name: gate-title
path: gate-title.jsonl
retention-days: 90

commits:
if: github.event_name == 'pull_request'
runs-on: ubuntu-24.04
permissions:
contents: read
steps:
- uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4
with:
fetch-depth: 0
persist-credentials: false
- name: Every commit is conventional and its credit trailers are well formed
env:
BASE: ${{ github.event.pull_request.base.sha }}
HEAD: ${{ github.event.pull_request.head.sha }}
run: python3 -I scripts/gates.py commits --base "$BASE" --head "$HEAD" | tee gate-commits.jsonl
- uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
if: always()
with:
name: gate-commits
path: gate-commits.jsonl
retention-days: 90

review:
runs-on: ubuntu-24.04
permissions:
contents: read
pull-requests: read
steps:
- uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4
with:
persist-credentials: false
- name: A current approval from someone else, when more than docs changed
env:
GH_TOKEN: ${{ github.token }}
REPO: ${{ github.repository }}
PR: ${{ github.event.pull_request.number }}
run: python3 -I scripts/gates.py review --repo "$REPO" --pr "$PR" | tee gate-review.jsonl
- uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
if: always()
with:
name: gate-review
path: gate-review.jsonl
retention-days: 90

pins:
if: github.event_name == 'pull_request'
runs-on: ubuntu-24.04
permissions:
contents: read
steps:
- uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4
with:
persist-credentials: false
- name: Every action is pinned by commit
run: python3 -I scripts/gates.py pins | tee gate-pins.jsonl
- uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
if: always()
with:
name: gate-pins
path: gate-pins.jsonl
retention-days: 90

tests:
if: github.event_name == 'pull_request'
runs-on: ubuntu-24.04
permissions:
contents: read
steps:
- uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4
with:
persist-credentials: false
- name: The gates' own tests
run: python3 -I -m unittest discover -s tests -k test_gates -v
111 changes: 111 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
# AGENTS.md

Rules for any agent or contributor working in this repo. The home-level `AGENTS.md` files (logging, read
before acting, verify then say done) apply as well; this file adds to them and wins where it is more specific.

## What this repo is

The catalog and submission place for nEVR plugins. An author submits once (an issue from the form); a
check comments what it found; a maintainer approves; each listed plugin points at its EchoTools fork and
release and carries what a plugin browser needs: description, media, versions, author, credits.

| Path | What |
|---|---|
| `.github/ISSUE_TEMPLATE/` | The submit and pull-a-version forms |
| `.github/workflows/check-submission.yml`, `scripts/inspect_submission.py`, `scripts/report_submission.py` | The automatic check of a submission, in a read-only job, and its comment |
| `mods/<id>.json`, `schema/entry.schema.json`, `docs/entry-format.md`, `scripts/validate_entries.py` | The listed entries and their format |
| `.github/workflows/gates.yml`, `scripts/gates.py`, `tests/test_gates.py` | The pull-request gates below |

Read `README.md` and `docs/entry-format.md` before changing an entry or the check. The signing design this
repo is part of is release-signer's `docs/plugin-release.md`.

## Logging

Everything here that runs logs what it did, as JSON, durably. Nothing in it is allowed to pass quietly.

- The gates and the entry validator print one JSON record per check (`event`, `at`, `ok`, `errors`, and
what was checked). CI keeps those records as artifacts for 90 days. A gate that cannot run prints a
`cannot_run` record and exits 2; it never passes by default.
- The submission check writes `report.json` (an artifact) and an issue comment for every run.
- A new script or workflow step is not done until you have shown a real record from a real run and a test
that asserts it. Never print or log a token or a secrets file.

## Commits and PR titles

- Commits and PR titles follow [Conventional Commits 1.0.0](https://www.conventionalcommits.org/en/v1.0.0/):
`type(scope)!: description`, with `type` one of `feat fix docs style refactor perf test build ci chore
revert`. One logical change per commit. The history before this file is free text; new commits are not.
- The `gates` workflow checks the title and every commit of a PR (`scripts/gates.py title`, `commits`).
- Version tags are SemVer, `v<version>`. The changelog is the git log.

## Credit

Credit the people whose work we integrate, every time.

- A commit that draws on a person's code, patch, research or findings carries `Co-Authored-By: Name
<id+login@users.noreply.github.com>` for that person (`gh api users/<login>` gives the id). The gate
rejects a trailer that is not `Name <email>` with a GitHub noreply address or the house addresses
(`a@sprock.io`, `agents@sprock.io`). Someone with no GitHub account is credited by name in the commit
body.
- A listed mod credits everyone whose work it builds on in the entry's `credits` (name, role, link).
The mod's own repo keeps their license files and says who they are in `NOTICE` and its README.
- Never list a mod whose credits you know to be missing: send it back to the author.

## Review

- Every PR that changes more than `README.md` and `docs/` needs a current approval from an owner, member
or collaborator who did not write it. "Current" means given on the PR's latest commit. An open request
for changes blocks. `gates.yml`'s `review` job (`scripts/gates.py review`) checks it.
- The reviewer reads the diff and the code around it, runs what the PR claims, and reports findings with
`file:line`. An entry's reviewer also reads the mod: what it hooks, what it sends where, and whether its
capabilities say what it does.
- A change to a gate (`scripts/gates.py`, `.github/workflows/`) is reviewed with extra care: the PR's own
copy of the gate is the one that runs on it.
- Owner setting, not in this repo: make `gates / review`, `title`, `commits`, `pins` and `tests` required
checks on `main`, and restrict who can merge. Until then the gates report but do not block.

## Issues, branches and PRs

- Every unit of work has an issue before it starts. The PR body names it (`Closes #N`, or `Refs #N` while
part stays open) and carries the commands you ran and their output.
- One worktree per PR: `git worktree add --no-track -b <branch> <path> origin/main`. Push with
`git push origin <local>:refs/heads/<branch>`. Never push `main`, never force-push, never rewrite a
pushed branch: fix with a new commit.
- A PR is one small change. When it is merged, delete its branch and remove its worktree
(`git worktree remove <path>`); the repo owner merges, not the author.

## Workflows

- Pin every action by its 40-hex commit, with the tag in a comment. The `pins` gate checks it.
- Trust boundary: use `pull_request` (and `issues` for the submission check), never `pull_request_target`.
A job that opens, downloads or loads anything a submitter controls holds a read-only token and no secret;
the job that writes (the comment) runs nothing from the submission. Untrusted text reaches a script
through `env`, never inside a script body.
- `permissions: {}` at the top of a workflow, and the least each job needs.

## Entries

- `mods/<id>.json` is created by a maintainer when a submission is approved, and changed when a release
is listed or pulled. It is checked against `schema/entry.schema.json`; `docs/entry-format.md` says what
each field means.
- The `signer` block records what the release-signer did. Copy its digests from the signer's public
record; never invent or edit them by hand to make the check pass.
- Pulling a version sets `pulled` on it; it never edits the release's digests.

## Commands

```
python3 -I -m unittest discover -s tests -v # the tests (validate_entries needs: pip install jsonschema==4.26.0)
python3 -I scripts/validate_entries.py # every mods/*.json
python3 -I scripts/gates.py pins # actions pinned
python3 -I scripts/gates.py title --title "fix: x"
python3 -I scripts/gates.py commits --base origin/main --head HEAD
python3 -I scripts/gates.py review --repo EchoTools/echo-mods --pr <N>
```

## Never

- Sign, hold keys or secrets, or add a secret to a workflow here: signing happens only in release-signer.
- Delete, rename or reorganize files, or delete logs, without the owner's word.
- List a mod that failed the check without a maintainer saying why in the issue.
- Turn a check into a warning, or add `|| true`, to make a PR green. Fix the thing it found.
Loading
Loading