From 785e2005a3e29aa3b24119702f4428e546ed59e2 Mon Sep 17 00:00:00 2001 From: PathGao Date: Wed, 16 Sep 2026 22:16:40 +0800 Subject: [PATCH] chore: align repository governance --- .github/CONTRIBUTING.md | 43 ++++-------- .github/GOVERNANCE.md | 38 +++++++++++ .github/ISSUE_TEMPLATE/config.yml | 4 ++ .github/PULL_REQUEST_TEMPLATE.md | 50 ++------------ .github/SECURITY.md | 15 ++-- .github/labels.json | 92 +++++++++++++++++++++++++ .github/labels.yml | 73 -------------------- .github/rulesets/main.json | 67 ++++++++++++++++++ .github/rulesets/version-tags.json | 27 ++++++++ .github/settings.json | 7 ++ Tools/README.md | 2 +- Tools/sync-labels.sh | 106 ++++++++++++++++------------- 12 files changed, 322 insertions(+), 202 deletions(-) create mode 100644 .github/GOVERNANCE.md create mode 100644 .github/labels.json delete mode 100644 .github/labels.yml create mode 100644 .github/rulesets/main.json create mode 100644 .github/rulesets/version-tags.json create mode 100644 .github/settings.json diff --git a/.github/CONTRIBUTING.md b/.github/CONTRIBUTING.md index dcfc179..84f4cb6 100644 --- a/.github/CONTRIBUTING.md +++ b/.github/CONTRIBUTING.md @@ -106,35 +106,20 @@ adding the file on disk alone compiles nothing and reports no error. ## Pull requests -Write the description as prose about the change, not a log of your afternoon. -The three things a reviewer needs: - -1. **What changed**, in a sentence or two. -2. **Why this way** — the mechanism behind the old behaviour, and what makes - this the right place to fix it. `WKWebView`, AppKit window levels and - Spaces all have defaults that surprise people; naming the one you hit is - usually the most useful sentence in the whole description. -3. **What you traded away** — what you left alone on purpose, what you're - unsure about, which macOS versions or display setups you couldn't test. - -Stating the gaps is worth more than a description with no gaps in it. Nobody -has every display configuration. - -The pull request template has these as headings. Delete the ones that don't -apply. - -Small and focused beats large and complete. If you find a second problem while -fixing the first, mention it in the description or open an issue — a pull -request that does one thing gets reviewed; one that does four gets postponed. - -**Write commit messages, pull requests and issues in English.** Not because -English is better, but because it is the one language everyone reading this -repository has in common, and a commit message is read by whoever runs -`git blame` on the line they are about to change — years later, by someone the -author will never meet. The message is where the reasoning lives; a reader who -cannot read it gets the diff and nothing else. - -Say why, not what. The diff already says what changed. +Use the [PR template](PULL_REQUEST_TEMPLATE.md): a short summary, verification +and issue links. Explain the cause and tradeoff when they help review; do not +repeat the diff. Keep each PR focused on one reviewable change. + +Record the commands and results, relevant macOS/hardware and any untested paths. +For site submissions, describe the site and settings; app-specific checks are +only relevant when app code changes. + +Use `Closes #123` or `Fixes #123` when the PR completes an issue. For partial +work, use `Related to #123` and state what remains. Completed issues close on +merge; there is no release-confirmation or inactivity-close bot. + +Write commit messages, pull requests and issues in English. Repository settings +and label conventions are documented in [Governance](GOVERNANCE.md). ## Relationship to Plash diff --git a/.github/GOVERNANCE.md b/.github/GOVERNANCE.md new file mode 100644 index 0000000..380c4f9 --- /dev/null +++ b/.github/GOVERNANCE.md @@ -0,0 +1,38 @@ +# Repository governance + +Adopted baseline: **2026-09-16** from [PathGao/governance](https://github.com/PathGao/PathGao/tree/main/governance). +Adopted [source snapshot](https://github.com/PathGao/PathGao/tree/efab3e421c6606c1ce0a1c5836e98ba0ff64addb/governance). +Updates are reviewed and copied manually; there is no automatic inheritance. + +## Workflow + +Use the short PR template. `Closes #123` or `Fixes #123` closes a completed issue +on merge. For partial work, use `Related to #123` and state what remains. There +is no release-confirmation or inactivity-close automation. + +`question` is a usage question. `needs info` waits for reporter details, +`awaiting decision` waits for a maintainer decision, and `planned` means accepted. +Type and resolution labels are defined in [labels.json](labels.json). + +## Maintenance + +[settings.json](settings.json) and [rulesets/](rulesets/) are desired GitHub +configuration, applied through the API as described in the baseline guide. +Committing these files alone does not change repository settings. Main requires +PRs and blocks deletion/force pushes; administrators retain a recovery bypass. +Release tags matching `v*` cannot be updated/deleted, except `v*-test*` tags. + +Preview label changes from the repository root: + +```sh +./Tools/sync-labels.sh PathGao/Nifro +``` + +Add `--apply` to create/update the declared labels. Labels outside the manifest +are preserved, including historical labels attached to closed issues. + +## Project differences + +Nifro retains site submission and other project labels, existing issue forms, +and its five required CI jobs. `Final_Check_Request` remains only as a legacy +GitHub label for historical issues; it is outside the managed manifest. diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml index 0086358..cd947a9 100644 --- a/.github/ISSUE_TEMPLATE/config.yml +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -1 +1,5 @@ blank_issues_enabled: true +contact_links: + - name: Security vulnerability + url: https://github.com/PathGao/Nifro/security/advisories/new + about: Report vulnerabilities privately. diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index 3f6a08f..f76f92e 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -1,51 +1,13 @@ - -Adding a site to sites/? Only "What this is" matters — say what the site is and -what settings you used, and ignore the rest. ---> +## Summary -## What this is - -What changes, and `Closes #123` if there's an issue. If someone reported it, -say who and where. - -## Mechanism - -Why the old behaviour happened — the specific line, default or assumption -responsible — rather than what you did about it. `WKWebView`, AppKit window -levels and Spaces all have defaults that surprise people, so naming the one -you hit is the most valuable part of the description. If you measured -something, paste the numbers. - -## Scope - -What you deliberately left alone, and why. Anything you noticed while working -and chose not to fix here belongs in this section too. A small fix plus a note -about the larger problem is easier to review, and easier to revert, than both -at once. + ## Verification -`swift test` covers the pure logic — the crop and zoom geometry, the menu bar strip, schedule -windows, which website is current on which display, video embedding and URL commands. Anything that -needs a window server or a web view has no automated coverage at all, so say what you ran by hand. - -- Which macOS version and hardware you ran it on. -- The websites you tried it against, by URL. -- Whether you tested with more than one display, and with browsing mode both - on and off, if your change goes anywhere near either. -- That `swiftlint` passed — it runs as a build phase, so a clean build is - enough. - -And what you *didn't* verify: macOS versions or hardware you couldn't try, -paths you reasoned about rather than ran. That's more useful to a reviewer -than a list with no gaps in it. + -## Screenshots +## Issues -For anything visible, before and after. Drag images straight into the -description. + diff --git a/.github/SECURITY.md b/.github/SECURITY.md index ab1452b..d32ab82 100644 --- a/.github/SECURITY.md +++ b/.github/SECURITY.md @@ -8,9 +8,9 @@ rather than as a public issue. That form is the only channel — there is one maintainer and no security mailing list, and a public issue for a live vulnerability is the one thing that cannot be undone. -Expect a first reply within a week. If a report holds up, the fix ships in the -next release and the advisory is published with it, crediting you unless you -ask otherwise. +Security fixes target the latest release. Give the maintainer time to investigate +and ship a fix before public disclosure; reporters can request credit. This +community project does not promise a response time. ## What is in scope @@ -35,9 +35,12 @@ their windows. The interesting boundary is between that page and the machine: app runs pages the user chose, with the JavaScript the user pasted; that is the feature. - Anything that requires the attacker to already have the user's account. -- Gatekeeper warnings on directly downloaded builds. That is expected and - documented in [docs/RELEASE.md](../docs/RELEASE.md) — builds are self-signed and - not notarized. Homebrew is the supported route. + +## Release integrity + +Official releases from v0.9.1 use Apple Developer ID signing and notarization. +Report unexpected signature, notarization or artifact-integrity failures with +the download source and exact system message. See [the release guide](../docs/RELEASE.md). ## Supported versions diff --git a/.github/labels.json b/.github/labels.json new file mode 100644 index 0000000..05b38db --- /dev/null +++ b/.github/labels.json @@ -0,0 +1,92 @@ +[ + { + "name": "bug", + "color": "d73a4a", + "description": "Something isn't working" + }, + { + "name": "enhancement", + "color": "a2eeef", + "description": "New feature or improvement" + }, + { + "name": "documentation", + "color": "bfd4f2", + "description": "Docs, README or contributor-facing text" + }, + { + "name": "question", + "color": "fbca04", + "description": "A usage question, not a request for missing information" + }, + { + "name": "needs info", + "color": "f9d0c4", + "description": "Waiting on the reporter to supply missing information" + }, + { + "name": "awaiting decision", + "color": "c129c4", + "description": "Needs a maintainer decision before work can start" + }, + { + "name": "planned", + "color": "0453ab", + "description": "Accepted and queued for work" + }, + { + "name": "duplicate", + "color": "cfd3d7", + "description": "Same issue or pull request; link to the original" + }, + { + "name": "wontfix", + "color": "6e7781", + "description": "Decided against, with a reason" + }, + { + "name": "dependencies", + "color": "b9d130", + "description": "Dependency update" + }, + { + "name": "github_actions", + "color": "000000", + "description": "GitHub Actions update" + }, + { + "name": "accessibility", + "color": "f143ab", + "description": "Barrier affecting people with disabilities" + }, + { + "name": "good first issue", + "color": "7057ff", + "description": "Good for newcomers" + }, + { + "name": "help wanted", + "color": "008672", + "description": "Extra attention is needed" + }, + { + "name": "invalid", + "color": "e4e669", + "description": "This doesn't seem right" + }, + { + "name": "site submission", + "color": "1D76DB", + "description": "A page proposed for the curated site list" + }, + { + "name": "swift", + "color": "F05138", + "description": "Pull requests that update Swift code" + }, + { + "name": "upstream", + "color": "E4E669", + "description": "The cause is in WebKit, macOS or the site itself, not in Nifro" + } +] diff --git a/.github/labels.yml b/.github/labels.yml deleted file mode 100644 index ce76118..0000000 --- a/.github/labels.yml +++ /dev/null @@ -1,73 +0,0 @@ -# The label set, kept in the repository so it is reviewable and reproducible. -# -# Apply it with: -# -# ./Tools/sync-labels.sh -# -# Carried over from Markpad, whose triage this follows. Three of these do work no -# default label does, and they are the ones worth explaining: -# -# needs info the issue is stalled on the reporter, not on us. Without it, -# "open" means both "waiting for me" and "waiting for you". -# awaiting decision the work is understood and the product call is not made. Separated -# from `planned` so that "we know how" and "we have agreed to" are -# never the same state. -# Final_Check_Request a fix has landed and nobody has confirmed the symptom is gone. -# A commit is not a confirmation, and closing on one is how a bug -# gets reported twice. - -- name: bug - color: d73a4a - description: Something isn't working - -- name: enhancement - color: a2eeef - description: New feature or request - -- name: question - color: FBCA04 - description: A usage question, not a defect - -- name: documentation - color: BFD4F2 - description: Docs, README or contributor-facing text - -- name: site submission - color: 1D76DB - description: A page proposed for the curated site list - -- name: needs info - color: F9D0C4 - description: Waiting on the reporter to confirm or supply detail - -- name: awaiting decision - color: c129c4 - description: Needs a product-direction call from the maintainer before work can start - -- name: planned - color: "0453ab" - description: Accepted and queued for the next round of work - -- name: Final_Check_Request - color: 5ade98 - description: "Looks fixed, but do not close yet: needs confirmation, or part of it is still open" - -- name: wontfix - color: 6E7781 - description: Decided against, with a reason - -- name: upstream - color: E4E669 - description: The cause is in WebKit, macOS or the site itself, not in Nifro - -- name: dependencies - color: b9d130 - description: Pull requests that update a dependency file - -- name: github_actions - color: "000000" - description: Pull requests that update GitHub Actions code - -- name: swift - color: F05138 - description: Pull requests that update Swift code diff --git a/.github/rulesets/main.json b/.github/rulesets/main.json new file mode 100644 index 0000000..8ac6755 --- /dev/null +++ b/.github/rulesets/main.json @@ -0,0 +1,67 @@ +{ + "name": "main", + "target": "branch", + "enforcement": "active", + "bypass_actors": [ + { + "actor_id": 5, + "actor_type": "RepositoryRole", + "bypass_mode": "always" + } + ], + "conditions": { + "ref_name": { + "exclude": [], + "include": [ + "~DEFAULT_BRANCH" + ] + } + }, + "rules": [ + { + "type": "deletion" + }, + { + "type": "non_fast_forward" + }, + { + "type": "pull_request", + "parameters": { + "required_approving_review_count": 0, + "dismiss_stale_reviews_on_push": false, + "required_reviewers": [], + "require_code_owner_review": false, + "require_last_push_approval": false, + "required_review_thread_resolution": false, + "require_extra_approval_for_unattributed_changes": true, + "allowed_merge_methods": [ + "squash" + ] + } + }, + { + "type": "required_status_checks", + "parameters": { + "strict_required_status_checks_policy": false, + "do_not_enforce_on_create": false, + "required_status_checks": [ + { + "context": "Build" + }, + { + "context": "Geometry tests" + }, + { + "context": "SwiftLint" + }, + { + "context": "Unused code" + }, + { + "context": "Site registry" + } + ] + } + } + ] +} diff --git a/.github/rulesets/version-tags.json b/.github/rulesets/version-tags.json new file mode 100644 index 0000000..306f421 --- /dev/null +++ b/.github/rulesets/version-tags.json @@ -0,0 +1,27 @@ +{ + "name": "version tags", + "target": "tag", + "enforcement": "active", + "bypass_actors": [], + "conditions": { + "ref_name": { + "exclude": [ + "refs/tags/v*-test*" + ], + "include": [ + "refs/tags/v*" + ] + } + }, + "rules": [ + { + "type": "deletion" + }, + { + "type": "non_fast_forward" + }, + { + "type": "update" + } + ] +} diff --git a/.github/settings.json b/.github/settings.json new file mode 100644 index 0000000..3985190 --- /dev/null +++ b/.github/settings.json @@ -0,0 +1,7 @@ +{ + "delete_branch_on_merge": true, + "allow_squash_merge": true, + "allow_merge_commit": false, + "allow_rebase_merge": false, + "allow_auto_merge": true +} diff --git a/Tools/README.md b/Tools/README.md index 6649a94..f2a9823 100644 --- a/Tools/README.md +++ b/Tools/README.md @@ -6,7 +6,7 @@ Development and maintenance helpers. The main build entry point is [`../build.sh | --- | --- | | `setup-signing.sh` | Creates a stable self-signed identity for local development only | | `build-local.sh` | Calls `../build.sh dev`; optionally copies to a new destination without overwriting | -| `sync-labels.sh` | Makes the repository's labels match `.github/labels.yml` | +| `sync-labels.sh` | Previews label changes from `.github/labels.json`; pass `OWNER/REPO --apply` to apply | | `validate-sites.py` | Checks every `sites/*.yml` against `sites/schema.json`. CI runs this | | `generate-site-catalog.py` | Writes `sites/index.json` and the bundled Swift copy from the YAML. CI fails if they disagree | | `check-strings.py` | Reconciles `Localizable.xcstrings` with the strings the compiler says the app displays, in both directions. CI runs this after the build | diff --git a/Tools/sync-labels.sh b/Tools/sync-labels.sh index 166746b..7fafad8 100755 --- a/Tools/sync-labels.sh +++ b/Tools/sync-labels.sh @@ -1,51 +1,59 @@ -#!/bin/zsh -# Make the repository's labels match .github/labels.yml. -# -# Creates what is missing and updates the colour and description of what is not. It does not delete -# labels it does not know about: GitHub creates a few by default, and a label already in use on an -# issue is somebody's filed state, not ours to remove. -set -euo pipefail - +#!/bin/sh +# Preview by default; --apply creates/updates declared labels and never deletes any. +set -eu cd "$(dirname "$0")/.." - -repository="${1:-$(gh repo view --json nameWithOwner --jq .nameWithOwner)}" - -python3 - "$repository" <<'PY' -import json, subprocess, sys, re, pathlib - -repository = sys.argv[1] -text = pathlib.Path(".github/labels.yml").read_text() - -# A tiny reader for the shape this file actually has, rather than a dependency on PyYAML for it. -labels = [] -for block in re.split(r"\n(?=- name: )", text): - if not block.lstrip().startswith("- name:"): - continue - entry = {} - for key in ("name", "color", "description"): - match = re.search(rf"^\s*-?\s*{key}: (.*)$", block, re.MULTILINE) - if match: - entry[key] = match.group(1).strip().strip('"') - labels.append(entry) - -existing = { - label["name"] - for label in json.loads( - subprocess.run( - ["gh", "label", "list", "--repo", repository, "--limit", "200", "--json", "name"], - capture_output=True, text=True, check=True, - ).stdout - ) -} - +python3 - "$@" <<'PYTHON' +import argparse +import json +from pathlib import Path +import re +import subprocess + +parser = argparse.ArgumentParser(description="Preview or apply .github/labels.json") +parser.add_argument("repository", help="Explicit OWNER/REPO target") +parser.add_argument("--apply", action="store_true") +args = parser.parse_args() +if not re.fullmatch(r"[A-Za-z0-9_.-]+/[A-Za-z0-9_.-]+", args.repository): + parser.error("repository must be OWNER/REPO") +labels = json.loads(Path(".github/labels.json").read_text()) +if not isinstance(labels, list): + parser.error("labels.json must contain an array") +seen = set() for label in labels: - action = ["--force"] if label["name"] in existing else [] - subprocess.run( - ["gh", "label", "create", label["name"], "--repo", repository, - "--color", label["color"], "--description", label.get("description", "")] + action, - check=True, capture_output=True, - ) - print(f" {'updated' if label['name'] in existing else 'created'} {label['name']}") - -print(f"{len(labels)} labels applied to {repository}") -PY + if (not isinstance(label, dict) or set(label) != {"name", "color", "description"} + or not isinstance(label["name"], str) or not label["name"].strip() + or len(label["name"]) > 50 + or not isinstance(label["description"], str) or len(label["description"]) > 100 + or not isinstance(label["color"], str) + or not re.fullmatch(r"[0-9a-fA-F]{6}", label["color"])): + parser.error("each label needs a name (1-50 chars), six-digit color and description (0-100 chars)") + key = label["name"].casefold() + if key in seen: + parser.error("duplicate label: " + label["name"]) + seen.add(key) +result = subprocess.run( + ["gh", "api", "--paginate", "--slurp", f"repos/{args.repository}/labels?per_page=100"], + check=True, capture_output=True, text=True, +) +existing = {label["name"].casefold(): label for page in json.loads(result.stdout) for label in page} +changed = 0 +for label in labels: + old = existing.get(label["name"].casefold()) + if old and old["name"] == label["name"] and old["color"].lower() == label["color"].lower() and (old.get("description") or "") == label["description"]: + continue + changed += 1 + verb = "update" if old else "create" + print(f"{verb}: {label['name']}", flush=True) + if args.apply: + endpoint = f"repos/{args.repository}/labels" + payload = dict(label) + method = "POST" + if old: + from urllib.parse import quote + endpoint += "/" + quote(old["name"], safe="") + payload["new_name"] = payload.pop("name") + method = "PATCH" + subprocess.run(["gh", "api", "--method", method, endpoint, "--input", "-", "--silent"], + input=json.dumps(payload), text=True, check=True) +print(f"{changed} label changes {'applied' if args.apply else 'proposed'} for {args.repository}; undeclared labels preserved") +PYTHON