adding CONTRIBUTING.md

This commit is contained in:
tapframe 2026-04-19 18:59:45 +05:30
parent 023c497fa8
commit f2e9b27df5
9 changed files with 853 additions and 0 deletions

218
.github/ISSUE_TEMPLATE/bug_report.yml vendored Normal file
View 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
View 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.

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

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