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