Compare commits

..

6 Commits

Author SHA1 Message Date
Gauvain
7c1900a27d fix(issue-form): bound the gh release fetch with a 30s timeout 2026-06-10 20:24:35 +02:00
Gauvain
888b8bb342 fix(issue-form): pin checkout to develop, serialize runs, simplify release fetch
- `release` events check out the tagged commit, so the script regenerated
  the form from the tag's stale copy and the bot PR could revert form
  edits made on develop since that release - checkout develop explicitly.
- Fixed concurrency group: a release event and the weekly cron no longer
  race on the same ci/update-issue-form branch (queue, no cancel).
- Replace `gh api --paginate` (fetches every release page) with
  `gh release list --exclude-drafts --exclude-pre-releases --limit N`.
- Document the GITHUB_TOKEN limitation on the auto-merge step: bot PRs
  don't trigger required checks; close/reopen kicks them, then auto-merge
  completes after reviews.
2026-06-10 20:20:56 +02:00
Gauvain
1c8a0ac35e Merge remote-tracking branch 'origin/develop' into ci/auto-update-issue-form
# Conflicts:
#	.github/ISSUE_TEMPLATE/issue_report.yml
2026-06-10 20:11:27 +02:00
Gauvino
e4def1f2a1 docs(issue-form): guide TestFlight/dev users to report the exact build
The version dropdown lists published releases only, so a TestFlight or dev
build will not appear in it. Point those reporters to the
"TestFlight/Development build" option and the exact version shown in the
app's Settings.
2026-06-05 13:38:52 +02:00
Gauvino
55376cd824 fix(issue-form): list published releases only, drop TestFlight draft entries
Per maintainer feedback (fredrikburmester, lancechant): the version dropdown
no longer surfaces draft releases as "X (TestFlight)" — drafts and prereleases
are unreliable version identifiers. update-issue-form.mjs now fetches only
published, non-prerelease releases and drops the per-version TestFlight label;
the workflow triggers on `released` events only; and the generic
"TestFlight/Development build" sentinel is restored at the end of the list.
2026-06-05 13:30:47 +02:00
Gauvino
3c8369ea4d ci(issue-form): auto-populate version dropdown from GitHub releases
Replaces the broken update workflow (it targeted a non-existent bug_report.yml
via the npm-based populate action; Streamyfin isn't on npm) with
scripts/update-issue-form.mjs (Bun, dep-free): reads release tags via gh and
rewrites the 'version' dropdown in issue_report.yml, preserving sentinels
(older, TestFlight/Development build), newest-first, capped at
ISSUE_FORM_LIMIT (5) — so each new release rotates the oldest entry out.

Drafts/prereleases are included on purpose: release.yml drafts
v<app.json version> for every TestFlight (iOS) / beta (Android) build, and the
app shows that nativeApplicationVersion to users (UserInfo.tsx), so those
in-flight versions must be selectable. Runs on release events + a weekly cron
safety net + workflow_dispatch, opens a PR and enables auto-merge. Seeds the
current versions.
2026-06-01 16:53:53 +02:00
8 changed files with 219 additions and 85 deletions

View File

@@ -75,10 +75,13 @@ body:
id: version id: version
attributes: attributes:
label: Streamyfin Version label: Streamyfin Version
description: What version of Streamyfin are you using? description: What version of Streamyfin are you running? On a TestFlight or development build, choose "TestFlight/Development build" and include the exact version string shown in the app's Settings.
options: options:
- 0.54.1 - 0.54.1
- 0.51.0 - 0.51.0
- 0.47.1
- 0.30.2
- 0.28.0
- Older - Older
- TestFlight/Development build - TestFlight/Development build
validations: validations:

View File

@@ -12,13 +12,10 @@ on:
branches: [develop, master] branches: [develop, master]
# Exposed to `expo prebuild` (app.config.js → extra.build) so Settings can show the # Exposed to `expo prebuild` (app.config.js → extra.build) so Settings can show the
# branch + commit + Actions run a CI build was made from. EAS cloud builds use # branch + commit a CI build was made from. EAS cloud builds use EAS_BUILD_* instead.
# EAS_BUILD_* instead. The run number maps a sideloaded build back to its Actions
# run (artifacts + logs) without needing Expo access.
env: env:
EXPO_PUBLIC_GIT_BRANCH: ${{ github.head_ref || github.ref_name }} EXPO_PUBLIC_GIT_BRANCH: ${{ github.head_ref || github.ref_name }}
EXPO_PUBLIC_GIT_COMMIT: ${{ github.sha }} EXPO_PUBLIC_GIT_COMMIT: ${{ github.sha }}
EXPO_PUBLIC_GIT_RUN_NUMBER: ${{ github.run_number }}
jobs: jobs:
build-android-phone: build-android-phone:
@@ -240,9 +237,7 @@ jobs:
- name: 🚀 Build iOS app - name: 🚀 Build iOS app
env: env:
EXPO_TV: 0 EXPO_TV: 0
# `ci` profile (extends production, autoIncrement off): keeps CI builds out of run: eas build -p ios --local --non-interactive
# the production version tier and stops them inflating the store build counter.
run: eas build -p ios --local --non-interactive --profile ci
- name: 📅 Set date tag - name: 📅 Set date tag
run: echo "DATE_TAG=$(date +%d-%m-%Y_%H-%M-%S)" >> $GITHUB_ENV run: echo "DATE_TAG=$(date +%d-%m-%Y_%H-%M-%S)" >> $GITHUB_ENV
@@ -367,7 +362,7 @@ jobs:
- name: 🚀 Build iOS app - name: 🚀 Build iOS app
env: env:
EXPO_TV: 1 EXPO_TV: 1
run: eas build -p ios --local --non-interactive --profile ci_tv run: eas build -p ios --local --non-interactive
- name: 📅 Set date tag - name: 📅 Set date tag
run: echo "DATE_TAG=$(date +%d-%m-%Y_%H-%M-%S)" >> $GITHUB_ENV run: echo "DATE_TAG=$(date +%d-%m-%Y_%H-%M-%S)" >> $GITHUB_ENV

View File

@@ -1,67 +1,102 @@
name: 🐛 Update Bug Report Template name: 🐛 Update Issue Form Versions
on: on:
release: release:
types: [published] # Run on every published release on any branch # Only full releases populate the dropdown (no drafts/prereleases).
types: [released]
schedule:
- cron: "0 3 * * 1" # Weekly safety net (Mondays 03:00 UTC) in case a release event was missed
workflow_dispatch:
# Fixed group so a release event and the weekly cron can't race on the same
# ci/update-issue-form branch — runs queue instead of force-pushing over each other.
concurrency: concurrency:
group: update-issue-form-${{ github.event.release.tag_name || github.run_id }} group: update-issue-form
cancel-in-progress: true cancel-in-progress: false
permissions:
contents: read
jobs: jobs:
update-bug-report: update-issue-form:
name: 🔢 Populate version dropdown
runs-on: ubuntu-24.04
permissions: permissions:
contents: write contents: write
pull-requests: write pull-requests: write
issues: write
runs-on: ubuntu-24.04
steps: steps:
- name: 📥 Checkout repository - name: 📥 Checkout repository
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
- name: "🟢 Setup Node.js"
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
with: with:
node-version: '24.x' # On `release` events GITHUB_SHA is the tagged commit — without this the
cache: 'npm' # script would regenerate the form from the tag's (stale) copy and the bot
# PR would revert any form edits made on develop since that release.
ref: develop
- name: 🔍 Extract minor version from app.json - name: 🍞 Setup Bun
id: minor uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2.2.0
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # main
with: with:
result-encoding: string bun-version: latest
script: |
const fs = require('fs-extra');
const semver = require('semver');
const content = fs.readJsonSync('./app.json');
const version = content.expo.version;
const minorVersion = semver.minor(version);
return minorVersion.toString();
- name: 📝 Update bug report version - name: 🔢 Populate version dropdown from GitHub releases
uses: ShaMan123/gha-populate-form-version@be012141ca560dbb92156e3fe098c46035f6260d #v2.0.5 id: populate
with: run: bun scripts/update-issue-form.mjs
semver: '^0.${{ steps.minor.outputs.result }}.0' env:
dry_run: no-push GH_TOKEN: ${{ github.token }}
GITHUB_REPOSITORY: ${{ github.repository }}
- name: ⚙️ Update bug report node version dropdown - name: 📬 Create pull request
uses: ShaMan123/gha-populate-form-version@be012141ca560dbb92156e3fe098c46035f6260d #v2.0.5 id: cpr
with:
dropdown: _node_version
package: node
semver: '>=24.0.0'
dry_run: no-push
- name: 📬 Commit and create pull request
uses: peter-evans/create-pull-request@5f6978faf089d4d20b00c7766989d076bb2fc7f1 # v8.1.1 uses: peter-evans/create-pull-request@5f6978faf089d4d20b00c7766989d076bb2fc7f1 # v8.1.1
with: with:
add-paths: .github/ISSUE_TEMPLATE/bug_report.yml add-paths: .github/ISSUE_TEMPLATE/issue_report.yml
branch: ci-update-bug-report branch: ci/update-issue-form
base: develop base: develop
delete-branch: true delete-branch: true
labels: ⚙️ ci, 🤖 github-actions labels: ⚙️ ci, 🤖 github-actions
title: 'chore(): Update bug report template to match release version' commit-message: "chore: update issue form version dropdown"
title: "chore: update issue form version dropdown"
# Follows .github/pull_request_template.md so the bot PR isn't flagged by PR validation.
body: | body: |
Automated update to `.github/ISSUE_TEMPLATE/bug_report.yml` # 📦 Pull Request
Triggered by workflow run [${{ github.run_id }}](https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }})
## 📝 Description
Automated update of the **Streamyfin Version** dropdown in `.github/ISSUE_TEMPLATE/issue_report.yml`, populated from the latest published GitHub releases by `scripts/update-issue-form.mjs`.
**Version dropdown now lists:** ${{ steps.populate.outputs.versions }}
Triggered by `${{ github.event_name }}`${{ github.event.release.tag_name && format(' — release {0}', github.event.release.tag_name) || '' }} · [run ${{ github.run_id }}](https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}).
## 🏷️ Ticket / Issue
N/A — automated maintenance.
### 🖼️ Screenshots / GIFs (if UI)
N/A — issue-template metadata only, no app UI.
## ✅ Checklist
- [x] Ive read the [contribution guidelines](CONTRIBUTING.md)
- [x] Verified that changes behave as expected for all platforms
- [x] Code passes lint/formatting and type checks (`tsc`/`biome`)
- [x] No secrets, hardcoded credentials, or private config files are included
- [x] I've declared if AI was used to assist with this PR (by uncommenting the line at the bottom, or not)
## 🔍 Testing Instructions
N/A — generated by CI from published releases; review the dropdown diff in `issue_report.yml`.
- name: 🔀 Enable auto-merge
if: steps.cpr.outputs.pull-request-operation == 'created'
env:
GH_TOKEN: ${{ github.token }}
# Known limitation: PRs created with GITHUB_TOKEN don't trigger CI workflows
# (GitHub anti-recursion), so the required checks stay "Expected" until a
# maintainer kicks them (close/reopen the PR, or push an empty commit).
# Auto-merge is still worth enabling: once checks run and reviews land,
# the PR merges itself.
run: |
gh pr merge --squash --auto "${{ steps.cpr.outputs.pull-request-number }}" \
|| echo "::warning::Could not enable auto-merge — enable 'Allow auto-merge' in repo settings (and branch protection); merge the PR manually for now."

View File

@@ -33,12 +33,6 @@ const buildMeta = {
process.env.EAS_BUILD_PROFILE || process.env.EAS_BUILD_PROFILE ||
process.env.EXPO_PUBLIC_BUILD_PROFILE || process.env.EXPO_PUBLIC_BUILD_PROFILE ||
null, null,
// GitHub Actions run number (#2098) — lets anyone map a sideloaded CI build back
// to its Actions run (artifacts + logs) without Expo access. Null outside CI.
runNumber:
process.env.GITHUB_RUN_NUMBER ||
process.env.EXPO_PUBLIC_GIT_RUN_NUMBER ||
null,
builtAt: new Date().toISOString(), builtAt: new Date().toISOString(),
}; };

View File

@@ -14,7 +14,7 @@ export const UserInfo: React.FC<Props> = ({ ...props }) => {
const { t } = useTranslation(); const { t } = useTranslation();
// Graduated build identifier — see utils/version.ts: // Graduated build identifier — see utils/version.ts:
// dev → "0.54.1 · branch · commit", develop/CI → "0.54.1 · commit · #run", production → "0.54.1". // dev → "0.54.1 · branch · commit", develop/CI → "0.54.1 · commit", production → "0.54.1 (42)".
const { display: version } = getVersionInfo(); const { display: version } = getVersionInfo();
return ( return (

View File

@@ -97,14 +97,6 @@
"credentialsSource": "local", "credentialsSource": "local",
"config": "ios-production.yml" "config": "ios-production.yml"
} }
},
"ci": {
"extends": "production",
"autoIncrement": false
},
"ci_tv": {
"extends": "production_tv",
"autoIncrement": false
} }
}, },
"submit": { "submit": {

View File

@@ -0,0 +1,122 @@
#!/usr/bin/env bun
/**
* Populates the "Streamyfin Version" dropdown in the issue report form with the
* latest GitHub releases. Run by the "Update Issue Form Versions" workflow on
* release events + a weekly cron (and manually via workflow_dispatch).
*
* Source: published, non-draft, non-prerelease GitHub releases, newest first.
* Non-version sentinels (e.g. "older", "TestFlight/Development build") are
* preserved at the end of the list.
*
* Usage:
* bun scripts/update-issue-form.mjs # rewrite the form in place
* ISSUE_FORM_LIMIT=8 bun scripts/update-issue-form.mjs
* bun scripts/update-issue-form.mjs --dry-run # print the new options, don't write
*
* Env: GITHUB_REPOSITORY (owner/repo), GH_TOKEN/GITHUB_TOKEN (for gh, provided in CI).
*/
import { execFileSync } from "node:child_process";
import {
appendFileSync,
readFileSync as read,
writeFileSync as write,
} from "node:fs";
const FORM = ".github/ISSUE_TEMPLATE/issue_report.yml";
const DROPDOWN_ID = "version"; // the `id:` of the dropdown to populate
const parsedLimit = Number.parseInt(process.env.ISSUE_FORM_LIMIT ?? "", 10);
const LIMIT =
Number.isInteger(parsedLimit) && parsedLimit > 0 ? parsedLimit : 5;
const REPO = process.env.GITHUB_REPOSITORY || "streamyfin/streamyfin";
const DRY = process.argv.includes("--dry-run");
// Matches "0.54.1" and prerelease/beta tags like "0.54.0-beta.1".
const isVersion = (s) => /^\d+\.\d+/.test(s.trim());
// 1. Fetch the latest published releases (newest first) — drafts and prereleases
// aren't a full release users run, so they don't belong in the dropdown.
const raw = execFileSync(
"gh",
[
"release",
"list",
"--repo",
REPO,
"--exclude-drafts",
"--exclude-pre-releases",
"--limit",
String(LIMIT),
"--json",
"tagName",
"--jq",
".[].tagName",
],
// Bounded timeout so a stuck gh process fails the job fast instead of
// holding the workflow open until the job-level timeout.
{ encoding: "utf8", timeout: 30_000 },
);
const seen = new Set();
const versions = [];
for (const tag of raw.split("\n")) {
if (!tag) continue;
const ver = tag.trim().replace(/^v/, "");
if (!isVersion(ver) || seen.has(ver)) continue;
seen.add(ver);
versions.push(ver);
}
if (!versions.length) {
console.error("No release versions found — leaving the form untouched.");
process.exit(1);
}
// 2. rewrite the dropdown options, preserving non-version sentinels
// (e.g. "older", "TestFlight/Development build") at the end of the list.
const lines = read(FORM, "utf8").split("\n");
const idIdx = lines.findIndex((l) =>
l.match(new RegExp(`^\\s*id:\\s*${DROPDOWN_ID}\\s*$`)),
);
if (idIdx === -1)
throw new Error(`dropdown id: ${DROPDOWN_ID} not found in ${FORM}`);
const optIdx = lines.findIndex(
(l, i) => i > idIdx && /^\s*options:\s*$/.test(l),
);
if (optIdx === -1)
throw new Error(`options: not found after id: ${DROPDOWN_ID}`);
const itemIndent = lines[optIdx].match(/^\s*/)[0] + " "; // options items are nested one level deeper
let end = optIdx + 1;
const sentinels = [];
while (end < lines.length && /^\s*-\s+/.test(lines[end])) {
const val = lines[end].replace(/^\s*-\s+/, "");
if (!isVersion(val)) sentinels.push(val);
end++;
}
const newOptions = [...versions, ...sentinels].map(
(v) => `${itemIndent}- ${v}`,
);
const updated = [
...lines.slice(0, optIdx + 1),
...newOptions,
...lines.slice(end),
].join("\n");
console.log(
`Versions: ${versions.join(", ")}${sentinels.length ? ` | kept: ${sentinels.join(", ")}` : ""}`,
);
if (DRY) {
console.log("--dry-run: not writing.");
} else {
write(FORM, updated);
console.log(`Updated ${FORM}.`);
}
// Expose the resulting list for the workflow (PR description).
if (process.env.GITHUB_OUTPUT) {
appendFileSync(
process.env.GITHUB_OUTPUT,
`versions=${versions.join(", ")}\n`,
);
}

View File

@@ -10,7 +10,6 @@ export interface BuildMeta {
commit?: string | null; commit?: string | null;
branch?: string | null; branch?: string | null;
profile?: string | null; profile?: string | null;
runNumber?: string | null;
builtAt?: string | null; builtAt?: string | null;
} }
@@ -23,10 +22,8 @@ export interface VersionInfo {
commit: string | null; commit: string | null;
/** Git branch the build was made from, e.g. "develop". */ /** Git branch the build was made from, e.g. "develop". */
branch: string | null; branch: string | null;
/** EAS build profile, e.g. "production", "ci", "preview", or null for local. */ /** EAS build profile, e.g. "production", "preview", or null for local. */
profile: string | null; profile: string | null;
/** GitHub Actions run number the build came from, e.g. "2098". Null outside CI. */
runNumber: string | null;
isDev: boolean; isDev: boolean;
isProduction: boolean; isProduction: boolean;
/** Graduated label for the Settings "App version" row (see tiering below). */ /** Graduated label for the Settings "App version" row (see tiering below). */
@@ -37,13 +34,13 @@ export interface VersionInfo {
* Resolve a graduated version string for Settings. * Resolve a graduated version string for Settings.
* *
* Tiering (most → least detailed): * Tiering (most → least detailed):
* - dev / local build → `version · branch · commit` (full context for debugging) * - dev / local build → `version · branch · commit` (full context for debugging)
* - develop / CI / preview → `version · commit · #run` (pin the exact source; the * - develop / CI / preview → `version · commit` (pin the exact source)
* Actions run number maps the build to its run — artifacts + logs — without * - production (store / TestFlight) → `version (build)` (store-correlatable; the
* Expo access) * build number lets TestFlight reports pin a build whose version isn't a
* - production (store / TestFlight) → `version` (build number intentionally * published release. Note: TestFlight and the public App Store ship the same
* not shown: TestFlight already displays it to testers, and the commit pins the * binary — telling them apart needs a runtime iOS receipt check, intentionally
* binary better) * not done here.)
*/ */
export function getVersionInfo(): VersionInfo { export function getVersionInfo(): VersionInfo {
// Read native/config values defensively — a version string must never crash Settings // Read native/config values defensively — a version string must never crash Settings
@@ -63,7 +60,6 @@ export function getVersionInfo(): VersionInfo {
const commit = meta.commit ?? null; const commit = meta.commit ?? null;
const branch = meta.branch ?? null; const branch = meta.branch ?? null;
const profile = meta.profile ?? null; const profile = meta.profile ?? null;
const runNumber = meta.runNumber ?? null;
const isDev = __DEV__ === true; const isDev = __DEV__ === true;
const isProduction = const isProduction =
typeof profile === "string" && profile.startsWith("production"); typeof profile === "string" && profile.startsWith("production");
@@ -72,12 +68,10 @@ export function getVersionInfo(): VersionInfo {
if (isDev) { if (isDev) {
display = [version ?? "dev", branch, commit].filter(Boolean).join(" · "); display = [version ?? "dev", branch, commit].filter(Boolean).join(" · ");
} else if (isProduction) { } else if (isProduction) {
display = version ?? build ?? "N/A";
} else {
display = display =
[version, commit, runNumber && `#${runNumber}`] version && build ? `${version} (${build})` : (version ?? build ?? "N/A");
.filter(Boolean) } else {
.join(" · ") || "N/A"; display = [version, commit].filter(Boolean).join(" · ") || version || "N/A";
} }
return { return {
@@ -86,7 +80,6 @@ export function getVersionInfo(): VersionInfo {
commit, commit,
branch, branch,
profile, profile,
runNumber,
isDev, isDev,
isProduction, isProduction,
display, display,