mirror of
https://github.com/tapframe/NuvioStreaming.git
synced 2026-05-17 15:32:01 +00:00
adding CONTRIBUTING.md
This commit is contained in:
parent
023c497fa8
commit
f2e9b27df5
9 changed files with 853 additions and 0 deletions
218
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
Normal file
218
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
Normal file
|
|
@ -0,0 +1,218 @@
|
|||
name: Bug report
|
||||
description: Report a reproducible bug (one per issue).
|
||||
title: "[Bug]: short summary here"
|
||||
labels:
|
||||
- bug
|
||||
body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: |
|
||||
Thanks for reporting a bug.
|
||||
|
||||
If we can reproduce it, we can usually fix it. This form is just to get the basics in one place.
|
||||
Please replace the default title with a short summary of the actual problem.
|
||||
|
||||
If the app crashes, logs are required. Crash reports without logs may be labeled `needs-info`.
|
||||
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: |
|
||||
## Quick checks
|
||||
|
||||
- type: checkboxes
|
||||
id: checks
|
||||
attributes:
|
||||
label: Pre-flight checks
|
||||
options:
|
||||
- label: I searched existing issues and this is not a duplicate.
|
||||
required: true
|
||||
- label: I can reproduce this on the latest release or latest main build.
|
||||
required: false
|
||||
- label: This issue is limited to a single bug (not multiple unrelated problems).
|
||||
required: true
|
||||
- label: This is not a source/stream-specific error (the issue happens regardless of which source is used).
|
||||
required: true
|
||||
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: |
|
||||
## Version & device
|
||||
|
||||
- type: input
|
||||
id: app_version
|
||||
attributes:
|
||||
label: App version / commit
|
||||
description: Release version (e.g. 1.2.3) or commit hash. If unsure, say where you got the build from.
|
||||
placeholder: "e.g. 1.2.3, or main@abc1234"
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: dropdown
|
||||
id: install_method
|
||||
attributes:
|
||||
label: Install method
|
||||
options:
|
||||
- GitHub Release / App Store / Play Store
|
||||
- TestFlight
|
||||
- CI build / Nightly
|
||||
- Built from source
|
||||
- Other (please describe below)
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: dropdown
|
||||
id: platform
|
||||
attributes:
|
||||
label: Platform
|
||||
options:
|
||||
- Android (phone/tablet)
|
||||
- iOS (iPhone)
|
||||
- iOS (iPad)
|
||||
- macOS (desktop)
|
||||
- Windows (desktop)
|
||||
- Linux (desktop)
|
||||
- Android emulator
|
||||
- iOS simulator
|
||||
- Other (please describe below)
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: input
|
||||
id: device_model
|
||||
attributes:
|
||||
label: Device model
|
||||
description: "Example: iPhone 15 Pro, Pixel 8, MacBook Air M2, etc."
|
||||
placeholder: "e.g. iPhone 15 Pro"
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: input
|
||||
id: os_version
|
||||
attributes:
|
||||
label: OS version
|
||||
placeholder: "e.g. iOS 18.1, Android 14, macOS 15.2"
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: |
|
||||
## What happened?
|
||||
|
||||
- type: dropdown
|
||||
id: area
|
||||
attributes:
|
||||
label: Area (tag)
|
||||
description: Pick the closest match. It helps triage.
|
||||
options:
|
||||
- Playback (start/stop/buffering)
|
||||
- Streams / Sources (selection, loading, errors)
|
||||
- Next Episode / Auto-play
|
||||
- Watch Progress (resume, watched state, history)
|
||||
- Subtitles (download, styling, sync)
|
||||
- Audio tracks
|
||||
- Navigation / Gestures
|
||||
- UI / Layout
|
||||
- Settings
|
||||
- Sync (Trakt / remote)
|
||||
- Downloads / Offline
|
||||
- Platform-specific (iOS only / Android only / Desktop only)
|
||||
- Other
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
id: steps
|
||||
attributes:
|
||||
label: Steps to reproduce
|
||||
description: Exact steps. If it depends on specific content, describe it (movie/series, season/episode, source/addon name) without sharing private links.
|
||||
placeholder: |
|
||||
1. Open ...
|
||||
2. Navigate to ...
|
||||
3. Tap / Click ...
|
||||
4. Observe ...
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
id: expected
|
||||
attributes:
|
||||
label: Expected behavior
|
||||
placeholder: "What you expected to happen."
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
id: actual
|
||||
attributes:
|
||||
label: Actual behavior
|
||||
placeholder: "What actually happened (include any on-screen error text/codes)."
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: dropdown
|
||||
id: frequency
|
||||
attributes:
|
||||
label: Frequency
|
||||
options:
|
||||
- Always
|
||||
- Often (more than 50%)
|
||||
- Sometimes
|
||||
- Rarely
|
||||
- Once
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: dropdown
|
||||
id: regression
|
||||
attributes:
|
||||
label: Did this work before?
|
||||
options:
|
||||
- Not sure
|
||||
- Yes, it used to work
|
||||
- No, it never worked
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: |
|
||||
## Extra context (optional)
|
||||
|
||||
- type: textarea
|
||||
id: media_details
|
||||
attributes:
|
||||
label: Media details (optional)
|
||||
description: Only include what you can safely share.
|
||||
placeholder: |
|
||||
- Content type: series/movie
|
||||
- Season/Episode: S1E2
|
||||
- Stream/source: (addon name / source label)
|
||||
- Video format: (if known)
|
||||
validations:
|
||||
required: false
|
||||
|
||||
- type: textarea
|
||||
id: logs
|
||||
attributes:
|
||||
label: Logs (required for crash reports)
|
||||
description: |
|
||||
Required if the app crashes or force closes.
|
||||
For other bug reports, logs are optional but still helpful.
|
||||
|
||||
**Android:** `adb logcat -d | tail -n 300`
|
||||
**iOS:** Crash log from Xcode Organizer or Console.app
|
||||
**Desktop:** Terminal/console output from around the time the issue occurred
|
||||
render: shell
|
||||
placeholder: |
|
||||
Paste logs here
|
||||
validations:
|
||||
required: false
|
||||
|
||||
- type: textarea
|
||||
id: extra
|
||||
attributes:
|
||||
label: Anything else? (optional)
|
||||
description: Screenshots/recordings, related issues, workarounds, etc.
|
||||
validations:
|
||||
required: false
|
||||
8
.github/ISSUE_TEMPLATE/config.yml
vendored
Normal file
8
.github/ISSUE_TEMPLATE/config.yml
vendored
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
blank_issues_enabled: false
|
||||
contact_links:
|
||||
- name: Downloads / Releases
|
||||
url: https://github.com/NuvioMedia/NuvioMobile/releases
|
||||
about: Grab the latest release here.
|
||||
- name: Documentation
|
||||
url: https://github.com/NuvioMedia/NuvioMobile/blob/main/README.md
|
||||
about: Read the README for setup and usage details.
|
||||
83
.github/ISSUE_TEMPLATE/feature_request.yml
vendored
Normal file
83
.github/ISSUE_TEMPLATE/feature_request.yml
vendored
Normal file
|
|
@ -0,0 +1,83 @@
|
|||
name: Feature request
|
||||
description: Suggest an improvement or new feature.
|
||||
title: "[Feature]: "
|
||||
labels:
|
||||
- enhancement
|
||||
body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: |
|
||||
One feature request per issue, please. The more real-world your use case is, the easier it is to evaluate.
|
||||
|
||||
Feature requests are reviewed as product proposals first.
|
||||
Please do not open a pull request for a new feature, major UX change, or broad cosmetic update unless a maintainer has explicitly approved it first.
|
||||
Unapproved feature PRs will usually be closed.
|
||||
|
||||
**Any large PR or change that is not a simple bug fix MUST be discussed and approved via a feature request issue first.**
|
||||
PRs that introduce large changes without a linked, approved feature request **will not be reviewed at all** and will be closed immediately. No exceptions.
|
||||
|
||||
- type: dropdown
|
||||
id: area
|
||||
attributes:
|
||||
label: Area (tag)
|
||||
options:
|
||||
- Playback
|
||||
- Streams / Sources
|
||||
- Next Episode / Auto-play
|
||||
- Watch Progress
|
||||
- Subtitles
|
||||
- Audio
|
||||
- Navigation / Gestures
|
||||
- UI / Layout
|
||||
- Settings
|
||||
- Sync (Trakt / remote)
|
||||
- Downloads / Offline
|
||||
- Platform-specific (iOS only / Android only / Desktop only)
|
||||
- Other
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
id: problem
|
||||
attributes:
|
||||
label: Problem statement
|
||||
description: What problem are you trying to solve?
|
||||
placeholder: "I want to be able to..."
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
id: proposed
|
||||
attributes:
|
||||
label: Proposed solution
|
||||
description: What would you like the app to do?
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: dropdown
|
||||
id: contribution_plan
|
||||
attributes:
|
||||
label: Are you planning to implement this yourself?
|
||||
description: Major features are usually implemented in-house unless approved first.
|
||||
options:
|
||||
- No, this is only a proposal
|
||||
- Maybe, but only if approved first
|
||||
- Yes, but I understand implementation still needs maintainer approval
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
id: alternatives
|
||||
attributes:
|
||||
label: Alternatives considered (optional)
|
||||
description: Any workarounds or other approaches you considered.
|
||||
validations:
|
||||
required: false
|
||||
|
||||
- type: textarea
|
||||
id: extra
|
||||
attributes:
|
||||
label: Additional context (optional)
|
||||
description: Mockups, examples from other apps, etc.
|
||||
validations:
|
||||
required: false
|
||||
47
.github/PULL_REQUEST_TEMPLATE.md
vendored
Normal file
47
.github/PULL_REQUEST_TEMPLATE.md
vendored
Normal file
|
|
@ -0,0 +1,47 @@
|
|||
## Summary
|
||||
|
||||
<!-- What changed in this PR? -->
|
||||
|
||||
## PR type
|
||||
|
||||
<!-- Pick one and delete the others -->
|
||||
- Bug fix
|
||||
- Small maintenance improvement
|
||||
- Docs fix
|
||||
- Translation update
|
||||
- Approved larger change (link approval below)
|
||||
|
||||
## Why
|
||||
|
||||
<!-- Why this change is needed. Link bug/issue/context. -->
|
||||
|
||||
## Policy check
|
||||
|
||||
<!-- ALL boxes must be checked or the PR will be closed without review. -->
|
||||
- [ ] This PR is not cosmetic-only, unless it is a translation PR.
|
||||
- [ ] This PR does not add a new major feature without prior approval.
|
||||
- [ ] This PR is small in scope and focused on one problem.
|
||||
- [ ] If this is a larger or directional change, I linked the **approved** feature request issue below.
|
||||
|
||||
> **Large PRs without a linked, approved feature request issue will be closed immediately without review. No exceptions.**
|
||||
|
||||
## Approved feature request (required for large/non-trivial PRs)
|
||||
|
||||
<!-- Link the approved feature request issue. Delete this section ONLY for small bug fixes. -->
|
||||
<!-- Example: Approved in #123 -->
|
||||
|
||||
## Testing
|
||||
|
||||
<!-- What you tested and how (manual + automated). -->
|
||||
|
||||
## Screenshots / Video (UI changes only)
|
||||
|
||||
<!-- If UI changed, add before/after screenshots or a short clip. -->
|
||||
|
||||
## Breaking changes
|
||||
|
||||
<!-- Any breaking behavior/config/schema changes? If none, write: None -->
|
||||
|
||||
## Linked issues
|
||||
|
||||
<!-- Example: Fixes #123 -->
|
||||
83
.github/workflows/close-unlabeled-issues.yml
vendored
Normal file
83
.github/workflows/close-unlabeled-issues.yml
vendored
Normal file
|
|
@ -0,0 +1,83 @@
|
|||
name: Close unlabeled issues
|
||||
|
||||
on:
|
||||
schedule:
|
||||
- cron: "23 6 * * *" # daily
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
dry_run:
|
||||
description: Log matching issues without commenting or closing them
|
||||
required: false
|
||||
type: boolean
|
||||
default: false
|
||||
|
||||
permissions:
|
||||
issues: write
|
||||
|
||||
jobs:
|
||||
close_unlabeled:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Close open issues with no labels
|
||||
uses: actions/github-script@v7
|
||||
with:
|
||||
script: |
|
||||
const owner = context.repo.owner;
|
||||
const repo = context.repo.repo;
|
||||
const inputs = context.payload.inputs || {};
|
||||
|
||||
const dryRun = String(inputs.dry_run || "false").toLowerCase() === "true";
|
||||
const closeMarker = "<!-- nuvio-bot:close-unlabeled -->";
|
||||
|
||||
const items = await github.paginate(github.rest.search.issuesAndPullRequests, {
|
||||
q: `repo:${owner}/${repo} is:issue is:open no:label`,
|
||||
per_page: 100,
|
||||
});
|
||||
|
||||
core.info(`Found ${items.length} open unlabeled issues.`);
|
||||
|
||||
for (const item of items) {
|
||||
const issueNumber = item.number;
|
||||
|
||||
if (dryRun) {
|
||||
core.info(`#${issueNumber}: would comment and close.`);
|
||||
continue;
|
||||
}
|
||||
|
||||
const comments = await github.paginate(github.rest.issues.listComments, {
|
||||
owner,
|
||||
repo,
|
||||
issue_number: issueNumber,
|
||||
per_page: 100,
|
||||
});
|
||||
|
||||
const alreadyCommented = comments.some(comment =>
|
||||
(comment.body || "").includes(closeMarker)
|
||||
);
|
||||
|
||||
if (!alreadyCommented) {
|
||||
const body =
|
||||
`${closeMarker}\n` +
|
||||
`Sorry about the churn here, and thanks for taking the time to report this.\n\n` +
|
||||
`Closing this issue because it does not have any labels.\n\n` +
|
||||
`Issue labels and triage rules were introduced later, so older unlabeled issues are no longer tracked reliably.\n\n` +
|
||||
`If you are still facing this problem, please open a new issue using the appropriate issue template and label so it can be triaged correctly.`;
|
||||
|
||||
await github.rest.issues.createComment({
|
||||
owner,
|
||||
repo,
|
||||
issue_number: issueNumber,
|
||||
body,
|
||||
});
|
||||
}
|
||||
|
||||
await github.rest.issues.update({
|
||||
owner,
|
||||
repo,
|
||||
issue_number: issueNumber,
|
||||
state: "closed",
|
||||
state_reason: "not_planned",
|
||||
});
|
||||
|
||||
core.info(`#${issueNumber}: closed.`);
|
||||
}
|
||||
74
.github/workflows/pr-template-check.yml
vendored
Normal file
74
.github/workflows/pr-template-check.yml
vendored
Normal file
|
|
@ -0,0 +1,74 @@
|
|||
name: PR Template Check
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
types: [opened, edited, synchronize, reopened]
|
||||
|
||||
permissions:
|
||||
pull-requests: read
|
||||
|
||||
jobs:
|
||||
validate_pr_body:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Validate required PR sections
|
||||
uses: actions/github-script@v7
|
||||
with:
|
||||
script: |
|
||||
const pr = context.payload.pull_request;
|
||||
const body = (pr.body || "").trim();
|
||||
|
||||
function sectionContent(title) {
|
||||
const escaped = title.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
||||
const re = new RegExp(`^##\\s+${escaped}\\s*$`, "m");
|
||||
const match = body.match(re);
|
||||
if (!match) return null;
|
||||
const start = match.index + match[0].length;
|
||||
const rest = body.slice(start);
|
||||
const next = rest.search(/^##\s+/m);
|
||||
return (next === -1 ? rest : rest.slice(0, next)).trim();
|
||||
}
|
||||
|
||||
const required = ["Summary", "Why", "Testing", "Breaking changes", "Linked issues"];
|
||||
const missing = [];
|
||||
const empty = [];
|
||||
|
||||
for (const name of required) {
|
||||
const content = sectionContent(name);
|
||||
if (content === null) {
|
||||
missing.push(name);
|
||||
continue;
|
||||
}
|
||||
|
||||
const cleaned = content
|
||||
.replace(/<!--[\s\S]*?-->/g, "")
|
||||
.replace(/`/g, "")
|
||||
.replace(/\s+/g, " ")
|
||||
.trim()
|
||||
.toLowerCase();
|
||||
|
||||
if (
|
||||
cleaned.length < 4 ||
|
||||
cleaned === "none" ||
|
||||
cleaned.includes("what changed in this pr") ||
|
||||
cleaned.includes("why this change is needed") ||
|
||||
cleaned.includes("what you tested") ||
|
||||
cleaned.includes("example: fixes #123")
|
||||
) {
|
||||
empty.push(name);
|
||||
}
|
||||
}
|
||||
|
||||
if (missing.length || empty.length) {
|
||||
const lines = [
|
||||
"PR description is missing required detail.",
|
||||
"",
|
||||
];
|
||||
if (missing.length) lines.push(`Missing sections: ${missing.join(", ")}`);
|
||||
if (empty.length) lines.push(`Incomplete sections: ${empty.join(", ")}`);
|
||||
lines.push("");
|
||||
lines.push("Please fill the PR template before merging.");
|
||||
core.setFailed(lines.join("\n"));
|
||||
} else {
|
||||
core.info("PR template check passed.");
|
||||
}
|
||||
70
.github/workflows/stale-needs-info.yml
vendored
Normal file
70
.github/workflows/stale-needs-info.yml
vendored
Normal file
|
|
@ -0,0 +1,70 @@
|
|||
name: Close stale needs-info issues
|
||||
|
||||
on:
|
||||
schedule:
|
||||
- cron: "17 6 * * *" # daily
|
||||
workflow_dispatch: {}
|
||||
|
||||
permissions:
|
||||
issues: write
|
||||
|
||||
jobs:
|
||||
close_stale:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Warn then close inactive needs-info
|
||||
uses: actions/github-script@v7
|
||||
with:
|
||||
script: |
|
||||
const owner = context.repo.owner;
|
||||
const repo = context.repo.repo;
|
||||
|
||||
const NEEDS_INFO = "needs-info";
|
||||
const WARN_AFTER_DAYS = 14;
|
||||
const CLOSE_AFTER_DAYS = 21;
|
||||
|
||||
const warnMarker = "<!-- nuvio-bot:stale-warning -->";
|
||||
const closeMarker = "<!-- nuvio-bot:stale-close -->";
|
||||
|
||||
const now = Date.now();
|
||||
const warnCutoff = now - WARN_AFTER_DAYS * 24 * 60 * 60 * 1000;
|
||||
const closeCutoff = now - CLOSE_AFTER_DAYS * 24 * 60 * 60 * 1000;
|
||||
|
||||
async function listOpenNeedsInfoIssues() {
|
||||
const q = `repo:${owner}/${repo} is:issue is:open label:"${NEEDS_INFO}"`;
|
||||
const res = await github.rest.search.issuesAndPullRequests({ q, per_page: 50 });
|
||||
return res.data.items || [];
|
||||
}
|
||||
|
||||
const items = await listOpenNeedsInfoIssues();
|
||||
for (const item of items) {
|
||||
const issue_number = item.number;
|
||||
const updatedAtMs = new Date(item.updated_at).getTime();
|
||||
|
||||
const comments = await github.paginate(github.rest.issues.listComments, {
|
||||
owner,
|
||||
repo,
|
||||
issue_number,
|
||||
per_page: 100,
|
||||
});
|
||||
const hasWarned = comments.some(c => (c.body || "").includes(warnMarker));
|
||||
const hasClosedComment = comments.some(c => (c.body || "").includes(closeMarker));
|
||||
|
||||
if (updatedAtMs <= closeCutoff && hasWarned && !hasClosedComment) {
|
||||
const body =
|
||||
`${closeMarker}\n` +
|
||||
`Closing this for now since we didn't get the requested details.\n\n` +
|
||||
`If you can share the missing info, reply here and we can reopen.`;
|
||||
await github.rest.issues.createComment({ owner, repo, issue_number, body });
|
||||
await github.rest.issues.update({ owner, repo, issue_number, state: "closed" });
|
||||
continue;
|
||||
}
|
||||
|
||||
if (updatedAtMs <= warnCutoff && !hasWarned) {
|
||||
const body =
|
||||
`${warnMarker}\n` +
|
||||
`Just a quick ping: this issue is labeled \`${NEEDS_INFO}\` and hasn't had any updates in a bit.\n\n` +
|
||||
`If you can add the missing details, we can keep going. Otherwise it may be closed after a grace period.`;
|
||||
await github.rest.issues.createComment({ owner, repo, issue_number, body });
|
||||
}
|
||||
}
|
||||
154
.github/workflows/triage-needs-info.yml
vendored
Normal file
154
.github/workflows/triage-needs-info.yml
vendored
Normal file
|
|
@ -0,0 +1,154 @@
|
|||
name: Triage (needs-info)
|
||||
|
||||
on:
|
||||
issues:
|
||||
types: [opened, edited, reopened]
|
||||
|
||||
permissions:
|
||||
issues: write
|
||||
|
||||
jobs:
|
||||
needs_info:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Label low-context issues
|
||||
uses: actions/github-script@v7
|
||||
with:
|
||||
script: |
|
||||
const owner = context.repo.owner;
|
||||
const repo = context.repo.repo;
|
||||
const issue_number = context.payload.issue.number;
|
||||
|
||||
const issue = context.payload.issue;
|
||||
const title = (issue.title || "").trim();
|
||||
const body = issue.body || "";
|
||||
const labels = (issue.labels || []).map(l => (typeof l === "string" ? l : l.name).toLowerCase());
|
||||
|
||||
const NEEDS_INFO = "needs-info";
|
||||
const NEEDS_INFO_COLOR = "d4c5f9";
|
||||
const NEEDS_INFO_DESC = "More details needed to reproduce / triage.";
|
||||
|
||||
function hasLabel(name) {
|
||||
return labels.includes(name.toLowerCase());
|
||||
}
|
||||
|
||||
function extractSection(title) {
|
||||
const re = new RegExp(`^###\\s+${title.replace(/[.*+?^${}()|[\\]\\\\]/g, "\\\\$&")}\\s*$`, "m");
|
||||
const match = body.match(re);
|
||||
if (!match) return "";
|
||||
const start = match.index + match[0].length;
|
||||
const rest = body.slice(start);
|
||||
const next = rest.search(/^###\s+/m);
|
||||
const section = (next === -1 ? rest : rest.slice(0, next));
|
||||
return section.trim();
|
||||
}
|
||||
|
||||
function extractFirstSection(titles) {
|
||||
for (const sectionTitle of titles) {
|
||||
const value = extractSection(sectionTitle);
|
||||
if (value) return value;
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
function normalizeText(value) {
|
||||
return (value || "").replace(/\s+/g, " ").trim();
|
||||
}
|
||||
|
||||
function stripIssuePrefix(value) {
|
||||
return normalizeText(value).replace(/^\[[^\]]+\]:\s*/i, "").trim();
|
||||
}
|
||||
|
||||
const steps = extractSection("Steps to reproduce");
|
||||
const expected = extractSection("Expected behavior");
|
||||
const actual = extractSection("Actual behavior");
|
||||
const logs = extractFirstSection([
|
||||
"Logs (required for crash reports)",
|
||||
"Logs (optional but helpful)",
|
||||
]);
|
||||
const extra = extractSection("Anything else? (optional)");
|
||||
const summaryTitle = stripIssuePrefix(title);
|
||||
|
||||
const looksLikeBugForm = !!(steps || expected || actual);
|
||||
const isBugIssue = hasLabel("bug") || looksLikeBugForm;
|
||||
const isFeatureIssue =
|
||||
hasLabel("enhancement") ||
|
||||
hasLabel("feature") ||
|
||||
hasLabel("feature request");
|
||||
|
||||
if (!isBugIssue || isFeatureIssue) {
|
||||
return;
|
||||
}
|
||||
|
||||
const problems = [];
|
||||
const genericTitle = /^(bug|issue|problem|help|question|crash|broken|error|bug report|short summary here|title here)$/i;
|
||||
const numericOnlyTitle = /^#?\d+$/;
|
||||
const crashPattern = /\b(crash|crashes|crashed|crashing|force close|force closes|force closed|fatal exception|app closes|app closed unexpectedly)\b/i;
|
||||
const crashContext = [summaryTitle, steps, actual, extra].map(normalizeText).join("\n");
|
||||
const isCrashIssue = crashPattern.test(crashContext);
|
||||
const normalizedLogs = normalizeText(logs);
|
||||
const hasLogs = normalizedLogs.length >= 20 && !/^(n\/a|na|none|no|not available)$/i.test(normalizedLogs);
|
||||
|
||||
if (!summaryTitle || summaryTitle.length < 8 || genericTitle.test(summaryTitle) || numericOnlyTitle.test(summaryTitle)) {
|
||||
problems.push("Issue title (replace the default `[Bug]:` prefix with a short summary of the actual problem)");
|
||||
}
|
||||
if (!steps || steps.length < 30) problems.push("Steps to reproduce (please list exact steps)");
|
||||
if (!expected || expected.length < 10) problems.push("Expected behavior");
|
||||
if (!actual || actual.length < 10) problems.push("Actual behavior (include any on-screen error text)");
|
||||
if (isCrashIssue && !hasLogs) {
|
||||
problems.push("Logs (required for crash reports; include a log snippet or stack trace)");
|
||||
}
|
||||
|
||||
async function ensureLabel(name, color, description) {
|
||||
try {
|
||||
await github.rest.issues.getLabel({ owner, repo, name });
|
||||
} catch (e) {
|
||||
try {
|
||||
await github.rest.issues.createLabel({ owner, repo, name, color, description });
|
||||
} catch (_) {}
|
||||
}
|
||||
}
|
||||
|
||||
const hasNeedsInfo = hasLabel(NEEDS_INFO);
|
||||
|
||||
if (problems.length > 0) {
|
||||
await ensureLabel(NEEDS_INFO, NEEDS_INFO_COLOR, NEEDS_INFO_DESC);
|
||||
if (!hasNeedsInfo) {
|
||||
await github.rest.issues.addLabels({
|
||||
owner,
|
||||
repo,
|
||||
issue_number,
|
||||
labels: [NEEDS_INFO],
|
||||
});
|
||||
}
|
||||
|
||||
const marker = "<!-- nuvio-bot:needs-info -->";
|
||||
const commentBody =
|
||||
`${marker}\n` +
|
||||
`Thanks for the report. Could you add a bit more detail so we can reproduce it?\n\n` +
|
||||
`Missing / too short:\n` +
|
||||
problems.map(p => `- ${p}`).join("\n") +
|
||||
`\n\n` +
|
||||
`Use a specific title, for example: \`[Bug]: Playback freezes when switching audio tracks on iOS\`.\n` +
|
||||
`${isCrashIssue ? `Crash reports must include logs.\n` : `Logs are optional for most issues, but they help a lot.\n`}`;
|
||||
|
||||
const comments = await github.paginate(github.rest.issues.listComments, {
|
||||
owner,
|
||||
repo,
|
||||
issue_number,
|
||||
per_page: 100,
|
||||
});
|
||||
const alreadyCommented = comments.some(c => (c.body || "").includes(marker));
|
||||
if (!alreadyCommented) {
|
||||
await github.rest.issues.createComment({
|
||||
owner,
|
||||
repo,
|
||||
issue_number,
|
||||
body: commentBody,
|
||||
});
|
||||
}
|
||||
} else if (hasNeedsInfo) {
|
||||
try {
|
||||
await github.rest.issues.removeLabel({ owner, repo, issue_number, name: NEEDS_INFO });
|
||||
} catch (_) {}
|
||||
}
|
||||
116
CONTRIBUTING.md
Normal file
116
CONTRIBUTING.md
Normal file
|
|
@ -0,0 +1,116 @@
|
|||
# Contributing
|
||||
|
||||
Thanks for helping improve Nuvio.
|
||||
|
||||
## Strict rules — read before opening anything
|
||||
|
||||
These rules are enforced strictly. Issues and PRs that do not follow them will be closed without review.
|
||||
|
||||
---
|
||||
|
||||
## PR policy
|
||||
|
||||
Pull requests are currently intended for:
|
||||
|
||||
- Reproducible bug fixes
|
||||
- Small stability improvements
|
||||
- Minor maintenance work
|
||||
- Small documentation fixes that improve accuracy
|
||||
- Translation updates
|
||||
|
||||
Pull requests are generally **not** accepted for:
|
||||
|
||||
- New major features
|
||||
- Product direction changes
|
||||
- Large UX / UI redesigns
|
||||
- Cosmetic-only changes
|
||||
- Refactors without a clear user-facing or maintenance benefit
|
||||
|
||||
Translation PRs are allowed, as long as they stay focused on translation/localization work and do not bundle unrelated feature or UI changes.
|
||||
|
||||
### Large PRs and large changes
|
||||
|
||||
**Any large PR or change that is not a simple bug fix must be discussed and approved via a feature request issue first.**
|
||||
|
||||
1. Open a **Feature Request** issue describing the change.
|
||||
2. Wait for explicit maintainer approval on that issue.
|
||||
3. Link the approved issue in your PR description.
|
||||
|
||||
PRs that introduce large changes without a linked, approved feature request **will not be reviewed at all** and will be closed immediately. No exceptions.
|
||||
|
||||
This applies to — but is not limited to — UI changes, new features, architecture changes, dependency additions, and large refactors.
|
||||
|
||||
---
|
||||
|
||||
## Where to ask questions
|
||||
|
||||
- Use **Issues** for bugs, feature requests, setup help, and general support.
|
||||
|
||||
---
|
||||
|
||||
## Bug reports (rules)
|
||||
|
||||
To keep issues fixable, bug reports should include:
|
||||
|
||||
- A short, specific issue title that describes the bug
|
||||
- App version (release version or commit hash)
|
||||
- Platform (Android / iOS / Desktop) + device model + OS version
|
||||
- Install method (release build / TestFlight / CI / built from source)
|
||||
- Steps to reproduce (exact steps)
|
||||
- Expected vs actual behavior
|
||||
- Frequency (always/sometimes/once)
|
||||
|
||||
Do not leave the title as just `[Bug]:` or another generic placeholder.
|
||||
|
||||
Logs are optional for most issues, but they are **required** for crash / force-close reports.
|
||||
|
||||
### How to capture logs (optional)
|
||||
|
||||
**Android:**
|
||||
|
||||
```sh
|
||||
adb logcat -d | tail -n 300
|
||||
```
|
||||
|
||||
**iOS:**
|
||||
|
||||
Attach a crash log from Xcode Organizer or Console.app, or reproduce while connected to Xcode and copy the relevant log output.
|
||||
|
||||
**Desktop:**
|
||||
|
||||
Copy the relevant terminal/console output from around the time the issue occurred.
|
||||
|
||||
---
|
||||
|
||||
## Feature requests (rules)
|
||||
|
||||
Please include:
|
||||
|
||||
- The problem you are solving (use case)
|
||||
- Your proposed solution
|
||||
- Alternatives considered (if any)
|
||||
|
||||
Opening a feature request does **not** mean a pull request will be accepted for it. If the feature affects product scope, UX direction, or adds a significant new surface area, do not start implementation unless a maintainer explicitly approves it first.
|
||||
|
||||
**Large changes require an approved feature request before any PR is submitted.** See the [Large PRs and large changes](#large-prs-and-large-changes) section above.
|
||||
|
||||
---
|
||||
|
||||
## Before opening a PR
|
||||
|
||||
Please make sure your PR is all of the following:
|
||||
|
||||
- Small in scope
|
||||
- Focused on one problem
|
||||
- Clearly aligned with the current direction of the project
|
||||
- Not cosmetic-only, unless it is a translation PR
|
||||
- Not a new major feature unless it was discussed and approved first
|
||||
- **If large or non-trivial: linked to an approved feature request issue**
|
||||
|
||||
PRs that do not fit this policy will be closed without merge so review time can stay focused on bugs, regressions, and small improvements.
|
||||
|
||||
---
|
||||
|
||||
## One issue per problem
|
||||
|
||||
Please open separate issues for separate bugs/features. It makes tracking, fixing, and closing issues much faster.
|
||||
Loading…
Reference in a new issue