Skip to content

Commit e665a3c

Browse files
Fix release notes and back-merge workflow
1 parent ceda432 commit e665a3c

6 files changed

Lines changed: 191 additions & 33 deletions

File tree

.github/workflows/backmerge-main-to-dev.yml

Lines changed: 38 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ on:
88

99
permissions:
1010
contents: write
11+
pull-requests: write
1112

1213
concurrency:
1314
group: folderview-plus-backmerge-main-to-dev
@@ -16,6 +17,8 @@ concurrency:
1617
jobs:
1718
backmerge:
1819
runs-on: ubuntu-latest
20+
env:
21+
BACKMERGE_BRANCH: backmerge/main-to-dev
1922

2023
steps:
2124
- name: Checkout repository
@@ -63,16 +66,48 @@ jobs:
6366
chmod +x scripts/run_ci_suite.sh
6467
bash scripts/run_ci_suite.sh
6568
66-
- name: Push dev when updated
69+
- name: Push back-merge branch when updated
70+
id: push_backmerge
6771
run: |
68-
git fetch origin dev
72+
git fetch origin dev "${BACKMERGE_BRANCH}" || true
6973
LOCAL_DEV="$(git rev-parse dev)"
7074
REMOTE_DEV="$(git rev-parse origin/dev)"
7175
if [ "${LOCAL_DEV}" = "${REMOTE_DEV}" ]; then
7276
echo "No dev changes produced by back-merge."
77+
echo "updated=0" >> "${GITHUB_OUTPUT}"
7378
exit 0
7479
fi
75-
git push origin dev
80+
git push --force-with-lease origin dev:"${BACKMERGE_BRANCH}"
81+
echo "updated=1" >> "${GITHUB_OUTPUT}"
82+
83+
- name: Create or update back-merge PR
84+
if: steps.push_backmerge.outputs.updated == '1'
85+
env:
86+
GH_TOKEN: ${{ github.token }}
87+
run: |
88+
set -euo pipefail
89+
TITLE="Sync main into dev"
90+
BODY_FILE="$(mktemp)"
91+
cat > "${BODY_FILE}" <<'EOF'
92+
## Summary
93+
94+
Automated back-merge of main-only fixes into `dev`.
95+
96+
This PR contains the commits present on `main` but not on `dev`, excluding stable release artifacts:
97+
- `folderview.plus.plg`
98+
- `folderview.plus.xml`
99+
- `archive/*.txz`
100+
- `archive/*.sha256`
101+
EOF
102+
103+
EXISTING_PR="$(gh pr list --state open --base dev --head "${BACKMERGE_BRANCH}" --json number --jq '.[0].number')"
104+
if [ -n "${EXISTING_PR}" ]; then
105+
gh pr edit "${EXISTING_PR}" --title "${TITLE}" --body-file "${BODY_FILE}"
106+
echo "Updated back-merge PR #${EXISTING_PR}."
107+
else
108+
gh pr create --base dev --head "${BACKMERGE_BRANCH}" --title "${TITLE}" --body-file "${BODY_FILE}"
109+
echo "Created back-merge PR from ${BACKMERGE_BRANCH} into dev."
110+
fi
76111
77112
- name: Collect back-merge debug artifacts on failure
78113
if: failure()

docs/releases/2026.04.05.02.md

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
### Features
2+
- Added `Copy Folder Settings` and `Paste Folder Settings` to Docker and VM folder menus.
3+
- Added `Apply to folders...` in the folder editor so the current folder configuration, including unsaved changes, can be applied safely to other existing folders.
4+
- Added a guarded server-side merge path for folder settings transfer so target folder names, parents, members, regex, and ids stay intact.
5+
- Added automatic backup snapshot creation before folder settings batch-apply operations.
6+
7+
### Support System Overhaul
8+
- Rebuilt the support export into a structured v2 support bundle.
9+
- Added richer troubleshooting coverage for environment, plugin state, runtime state, UI telemetry, recent actions, browser errors, asset versions, build identity, and plugin-scoped server diagnostics.
10+
- Added sanitized redaction manifests and export preview so support bundles stay useful without collecting unnecessary personal information.
11+
12+
### Editor, UI, and Theme Work
13+
- Retired the legacy folder editor and moved fully to the modern editor.
14+
- Improved the modern folder editor across parent selection, icon workflows, live preview, rules, diagnostics, and white-theme presentation.
15+
- Polished folder editor, settings, diagnostics, and dashboard surfaces across dark and light themes, including chevrons, dropdown chrome, parent picker, icon picker, and diagnostics layout.
16+
- Expanded theme-token coverage across runtime menus, dialogs, diagnostics, and status surfaces to remove remaining hardcoded dark-mode behavior.
17+
18+
### Runtime Fixes
19+
- Fixed Docker update detection so container rows, folder rows, root folders, and folder-editor update summaries stay aligned with native Unraid update state.
20+
- Fixed multi-row Docker previews so clicking a previewed container opens the normal Docker menu again while WebUI, Console, and Logs remain direct actions.
21+
- Hardened Docker order sync so it only runs when membership or order inputs actually change, coalesces concurrent requests, and avoids rewriting unchanged order files.
22+
- Fixed diagnostics theme reporting, theme self-heal false positives, and diagnostics support export context.
23+
24+
### Refactor and Quality
25+
- Split major settings, folder editor, Docker runtime, and server monoliths into focused helper modules so the main runtimes act as coordinators instead of implementation dumps.
26+
- Removed dead runtime code and dead selectors, tightened unused-code guards, and expanded regression coverage.
27+
- Hardened release automation, recovery paths, subsystem-aware release notes, and package validation so stable release publishing is more reliable.
28+
29+
### Release Follow-Up Fixes
30+
- Fixed release validation so locale keys referenced through shared i18n wrappers no longer trigger false language-usage failures.
31+
- Fixed the stable release package/archive synchronization for the April 5, 2026 release cut.

scripts/build_release_notes.sh

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ fvplus::require_commands awk sed bash
1111
VERSION=""
1212
OUTPUT=""
1313
INSTALL_BRANCH="${FVPLUS_RELEASE_INSTALL_BRANCH:-main}"
14+
OVERRIDE_FILE=""
1415

1516
usage() {
1617
cat <<'EOF'
@@ -47,6 +48,19 @@ if [[ -z "${OUTPUT}" ]]; then
4748
fvplus::fail "--output is required"
4849
fi
4950

51+
OVERRIDE_FILE="docs/releases/${VERSION}.md"
52+
53+
if [[ -f "${OVERRIDE_FILE}" ]]; then
54+
cat > "${OUTPUT}" <<EOF
55+
## FolderView Plus ${VERSION}
56+
57+
Install URL: \`https://raw.githubusercontent.com/alexphillips-dev/FolderView-Plus/${INSTALL_BRANCH}/folderview.plus.plg\`
58+
59+
$(cat "${OVERRIDE_FILE}")
60+
EOF
61+
exit 0
62+
fi
63+
5064
NOTES_BLOCK="$(awk -v version="${VERSION}" '
5165
BEGIN { capture = 0 }
5266
/^###/ {

scripts/sync_main_to_dev.sh

Lines changed: 72 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ set -euo pipefail
44
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
55
cd "${ROOT_DIR}"
66

7-
DEV_BRANCH="dev"
7+
DEV_BRANCH="${FVPLUS_BACKMERGE_LOCAL_BRANCH:-dev}"
88
MAIN_REF="origin/main"
99
DEV_REF="origin/dev"
1010

@@ -22,6 +22,26 @@ release_only_path() {
2222
esac
2323
}
2424

25+
latest_dev_merge_into_main() {
26+
git log "${MAIN_REF}" --first-parent --grep='^Merge dev into main for ' --format='%H' -n 1
27+
}
28+
29+
resolve_release_only_conflicts() {
30+
local path=""
31+
mapfile -t CONFLICT_PATHS < <(git diff --name-only --diff-filter=U || true)
32+
if [ "${#CONFLICT_PATHS[@]}" -eq 0 ]; then
33+
return 1
34+
fi
35+
for path in "${CONFLICT_PATHS[@]}"; do
36+
if ! release_only_path "${path}"; then
37+
return 1
38+
fi
39+
done
40+
git checkout --ours -- "${CONFLICT_PATHS[@]}"
41+
git add -- "${CONFLICT_PATHS[@]}"
42+
return 0
43+
}
44+
2545
main_differs_from_dev_only_by_release_artifacts() {
2646
local path=""
2747
mapfile -t DIFF_PATHS < <(git diff --name-only "${DEV_REF}..${MAIN_REF}" || true)
@@ -38,6 +58,7 @@ main_differs_from_dev_only_by_release_artifacts() {
3858

3959
if git show-ref --verify --quiet "refs/heads/${DEV_BRANCH}"; then
4060
git checkout "${DEV_BRANCH}"
61+
git reset --hard "${DEV_REF}"
4162
else
4263
git checkout -b "${DEV_BRANCH}" "${DEV_REF}"
4364
fi
@@ -52,43 +73,65 @@ if main_differs_from_dev_only_by_release_artifacts; then
5273
exit 0
5374
fi
5475

55-
MERGED_CLEANLY=1
56-
if ! git merge --no-ff --no-commit "${MAIN_REF}"; then
57-
MERGED_CLEANLY=0
76+
SYNC_BASE="$(latest_dev_merge_into_main || true)"
77+
if [ -z "${SYNC_BASE}" ]; then
78+
SYNC_BASE="$(git merge-base "${DEV_REF}" "${MAIN_REF}")"
79+
fi
80+
81+
mapfile -t MAIN_ONLY_COMMITS < <(git rev-list --reverse --first-parent --no-merges "${SYNC_BASE}..${MAIN_REF}" || true)
82+
83+
if [ "${#MAIN_ONLY_COMMITS[@]}" -eq 0 ]; then
84+
echo "No linear main-only commits to sync."
85+
exit 0
5886
fi
5987

60-
if [ "${MERGED_CLEANLY}" -eq 0 ]; then
61-
mapfile -t CONFLICTS < <(git diff --name-only --diff-filter=U)
88+
applied_commits=0
6289

63-
if [ "${#CONFLICTS[@]}" -eq 0 ]; then
64-
echo "Merge reported conflicts but none were detected." >&2
65-
exit 1
90+
for COMMIT in "${MAIN_ONLY_COMMITS[@]}"; do
91+
mapfile -t COMMIT_PATHS < <(git show --pretty=format: --name-only "${COMMIT}" | sed '/^[[:space:]]*$/d')
92+
if [ "${#COMMIT_PATHS[@]}" -eq 0 ]; then
93+
continue
6694
fi
6795

68-
for FILE in "${CONFLICTS[@]}"; do
69-
if ! release_only_path "${FILE}"; then
70-
echo "Unexpected merge conflict in ${FILE}; aborting auto back-merge." >&2
71-
git merge --abort
72-
exit 1
96+
commit_has_non_release_paths=0
97+
commit_touched_release_paths=0
98+
for FILE in "${COMMIT_PATHS[@]}"; do
99+
if release_only_path "${FILE}"; then
100+
commit_touched_release_paths=1
101+
else
102+
commit_has_non_release_paths=1
73103
fi
74104
done
75105

76-
git checkout HEAD -- archive folderview.plus.plg
77-
git add archive folderview.plus.plg
78-
fi
106+
if [ "${commit_has_non_release_paths}" -eq 0 ]; then
107+
echo "Skipping release-only commit ${COMMIT}."
108+
continue
109+
fi
79110

80-
# Always force dev channel URLs in case main touched these lines without conflicts.
81-
sed -E -i 's|^<!ENTITY pluginURL ".*">|<!ENTITY pluginURL "https://raw.githubusercontent.com/\&github;/dev/folderview.plus.plg">|' folderview.plus.plg
82-
sed -E -i 's|<URL>https://raw.githubusercontent.com/.*?/archive/.*</URL>|<URL>https://raw.githubusercontent.com/\&github;/dev/archive/\&name;-\&version;.txz</URL>|' folderview.plus.plg
83-
git add folderview.plus.plg
111+
if ! git cherry-pick -x "${COMMIT}"; then
112+
if resolve_release_only_conflicts && git cherry-pick --continue; then
113+
:
114+
else
115+
echo "Cherry-pick failed for ${COMMIT}; aborting auto back-merge." >&2
116+
git cherry-pick --abort
117+
exit 1
118+
fi
119+
fi
84120

85-
if git rev-parse -q --verify MERGE_HEAD >/dev/null; then
86-
if git diff --cached --quiet; then
87-
git commit --allow-empty -m "Sync main into dev (auto back-merge)"
88-
else
89-
git commit -m "Sync main into dev (auto back-merge)"
121+
if [ "${commit_touched_release_paths}" -eq 1 ]; then
122+
git restore --source=HEAD^ --staged --worktree folderview.plus.plg folderview.plus.xml archive
123+
if ! git diff --cached --quiet || ! git diff --quiet; then
124+
git add folderview.plus.plg folderview.plus.xml archive
125+
git commit --amend --no-edit
126+
fi
90127
fi
91-
echo "Back-merge commit created."
92-
else
93-
echo "No merge head present; nothing to commit."
128+
129+
applied_commits=1
130+
done
131+
132+
if [ "${applied_commits}" -eq 0 ]; then
133+
echo "No non-release commits required syncing."
134+
exit 0
94135
fi
136+
137+
echo "Back-merge branch updated linearly."

scripts/workflow_self_check.sh

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,17 @@ if (!/upload-artifact@v4/.test(backmergeWorkflow)) {
9393
if (!/FVPLUS_EXPECT_PLUGIN_BRANCH:\s*'dev'/.test(backmergeWorkflow)) {
9494
fail('Back-merge workflow must validate merged dev state with FVPLUS_EXPECT_PLUGIN_BRANCH set to dev.');
9595
}
96+
if (!/pull-requests:\s*write/.test(backmergeWorkflow)) {
97+
fail('Back-merge workflow must have pull-requests: write permission.');
98+
}
99+
if (!/Create or update back-merge PR/.test(backmergeWorkflow) ||
100+
!/gh pr create/.test(backmergeWorkflow) ||
101+
!/gh pr edit/.test(backmergeWorkflow)) {
102+
fail('Back-merge workflow must open or update a PR into dev instead of pushing directly.');
103+
}
104+
if (/git push origin dev/.test(backmergeWorkflow)) {
105+
fail('Back-merge workflow must not push directly to protected dev.');
106+
}
96107
97108
for (const workflowPath of [
98109
'.github/workflows/ci.yml',

tests/versioning-guard.test.mjs

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ const unraidMatrixSmokePath = path.join(repoRoot, 'scripts/unraid_matrix_smoke.s
4040
const ensureChangesPath = path.join(repoRoot, 'scripts/ensure_plg_changes_entry.sh');
4141
const doctorPath = path.join(repoRoot, 'scripts/doctor.sh');
4242
const sharedLibPath = path.join(repoRoot, 'scripts/lib.sh');
43+
const syncMainToDevPath = path.join(repoRoot, 'scripts/sync_main_to_dev.sh');
4344
const prePushHookPath = path.join(repoRoot, '.githooks/pre-push');
4445
const perfBaselinePath = path.join(repoRoot, 'scripts/perf_baseline.json');
4546
const jsUnusedSymbolsGuardPath = path.join(repoRoot, 'scripts/js_unused_symbols_guard.mjs');
@@ -68,6 +69,7 @@ const remotePublishGuard = fs.readFileSync(remotePublishGuardPath, 'utf8');
6869
const releaseNotesConsistencyGuard = fs.readFileSync(releaseNotesConsistencyGuardPath, 'utf8');
6970
const runCiSuite = fs.readFileSync(runCiSuitePath, 'utf8');
7071
const workflowSelfCheck = fs.readFileSync(path.join(repoRoot, 'scripts/workflow_self_check.sh'), 'utf8');
72+
const syncMainToDev = fs.readFileSync(syncMainToDevPath, 'utf8');
7173
const themeMatrixSmokeShell = fs.readFileSync(themeMatrixSmokeShellPath, 'utf8');
7274
const themeMatrixSmokeNode = fs.readFileSync(themeMatrixSmokeNodePath, 'utf8');
7375
const installSmoke = fs.readFileSync(installSmokePath, 'utf8');
@@ -425,6 +427,7 @@ test('validation workflows delegate to the shared ci suite with dev coverage, fa
425427
assert.match(backmergeWorkflow, /uses:\s*\.\/\.github\/actions\/setup-ci-env/);
426428
assert.match(backmergeWorkflow, /FVPLUS_BROWSER_SMOKE_REQUIRED:\s*'0'/);
427429
assert.match(backmergeWorkflow, /FVPLUS_THEME_MATRIX_REQUIRED:\s*'0'/);
430+
assert.match(backmergeWorkflow, /pull-requests:\s*write/);
428431
assert.match(backmergeWorkflow, /Upload back-merge debug artifacts on failure/);
429432

430433
assert.match(releasePrepare, /bash scripts\/doctor\.sh/);
@@ -447,6 +450,12 @@ test('release-on-main validates remote raw publish artifacts before publishing r
447450
assert.doesNotMatch(releaseMainWorkflow, /bash scripts\/remote_publish_guard\.sh/);
448451
});
449452

453+
test('release notes builder supports curated per-version override files', () => {
454+
assert.match(buildReleaseNotes, /OVERRIDE_FILE="docs\/releases\/\$\{VERSION\}\.md"/);
455+
assert.match(buildReleaseNotes, /\[\[ -f "\$\{OVERRIDE_FILE\}" \]\]/);
456+
assert.match(buildReleaseNotes, /cat "\$\{OVERRIDE_FILE\}"/);
457+
});
458+
450459
test('release workflows serialize concurrent runs with shared release concurrency group', () => {
451460
for (const workflow of [releaseMainWorkflow, releaseOnMainWorkflow]) {
452461
assert.match(workflow, /concurrency:/);
@@ -494,11 +503,26 @@ test('back-merge workflow validates merged dev state before pushing', () => {
494503
assert.match(backmergeWorkflow, /Setup CI environment/);
495504
assert.match(backmergeWorkflow, /Sync main into dev/);
496505
assert.match(backmergeWorkflow, /Validate merged dev state before push/);
497-
assert.match(backmergeWorkflow, /Push dev when updated/);
506+
assert.match(backmergeWorkflow, /Push back-merge branch when updated/);
507+
assert.match(backmergeWorkflow, /Create or update back-merge PR/);
508+
assert.match(backmergeWorkflow, /git push --force-with-lease origin dev:"\$\{BACKMERGE_BRANCH\}"/);
509+
assert.match(backmergeWorkflow, /gh pr create --base dev --head "\$\{BACKMERGE_BRANCH\}"/);
510+
assert.match(backmergeWorkflow, /gh pr edit "\$\{EXISTING_PR\}"/);
498511
assert.match(backmergeWorkflow, /Collect back-merge debug artifacts on failure/);
499512
assert.match(backmergeWorkflow, /Upload back-merge debug artifacts on failure/);
500513
});
501514

515+
test('back-merge sync script keeps dev linear and drops stable release artifacts', () => {
516+
assert.match(syncMainToDev, /git log "\$\{MAIN_REF\}" --first-parent --grep='\^Merge dev into main for ' --format='%H' -n 1/);
517+
assert.match(syncMainToDev, /git rev-list --reverse --first-parent --no-merges "\$\{SYNC_BASE\}\.\.\$\{MAIN_REF\}"/);
518+
assert.match(syncMainToDev, /git cherry-pick -x "\$\{COMMIT\}"/);
519+
assert.match(syncMainToDev, /git diff --name-only --diff-filter=U/);
520+
assert.match(syncMainToDev, /git checkout --ours -- "\$\{CONFLICT_PATHS\[@\]\}"/);
521+
assert.match(syncMainToDev, /git cherry-pick --continue/);
522+
assert.match(syncMainToDev, /git restore --source=HEAD\^ --staged --worktree folderview\.plus\.plg folderview\.plus\.xml archive/);
523+
assert.doesNotMatch(syncMainToDev, /git merge --no-ff --no-commit/);
524+
});
525+
502526
test('install smoke supports configurable archive directory override', () => {
503527
assert.match(installSmoke, /source "\$\{ROOT_DIR\}\/scripts\/lib\.sh"/);
504528
assert.match(installSmoke, /VERSION="\$\(fvplus::read_plg_version "\$\{PLG_FILE\}"\)"/);

0 commit comments

Comments
 (0)