mirror of
https://github.com/ksyasuda/SubMiner.git
synced 2026-09-11 05:16:27 -07:00
Compare commits
4
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e6dc9dfec5
|
||
|
|
6d69a56574 | ||
|
|
0c37c665a2 | ||
|
|
8ae5bde6a4
|
@@ -0,0 +1,266 @@
|
|||||||
|
name: Package release
|
||||||
|
|
||||||
|
on:
|
||||||
|
workflow_call:
|
||||||
|
secrets:
|
||||||
|
CSC_LINK:
|
||||||
|
required: true
|
||||||
|
CSC_KEY_PASSWORD:
|
||||||
|
required: true
|
||||||
|
APPLE_ID:
|
||||||
|
required: true
|
||||||
|
APPLE_APP_SPECIFIC_PASSWORD:
|
||||||
|
required: true
|
||||||
|
APPLE_TEAM_ID:
|
||||||
|
required: true
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build-linux:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
submodules: true
|
||||||
|
persist-credentials: false
|
||||||
|
|
||||||
|
- name: Setup Bun
|
||||||
|
uses: oven-sh/setup-bun@v2
|
||||||
|
with:
|
||||||
|
bun-version: 1.3.5
|
||||||
|
|
||||||
|
- name: Cache dependencies
|
||||||
|
uses: actions/cache@v4
|
||||||
|
with:
|
||||||
|
path: |
|
||||||
|
~/.bun/install/cache
|
||||||
|
node_modules
|
||||||
|
stats/node_modules
|
||||||
|
vendor/texthooker-ui/node_modules
|
||||||
|
vendor/subminer-yomitan/node_modules
|
||||||
|
key: ${{ runner.os }}-${{ runner.arch }}-bun-${{ hashFiles('bun.lock', 'stats/bun.lock', 'vendor/texthooker-ui/bun.lock', 'vendor/subminer-yomitan/package-lock.json') }}
|
||||||
|
restore-keys: |
|
||||||
|
${{ runner.os }}-${{ runner.arch }}-bun-
|
||||||
|
|
||||||
|
- name: Install dependencies
|
||||||
|
run: |
|
||||||
|
bun install --frozen-lockfile
|
||||||
|
cd stats && bun install --frozen-lockfile
|
||||||
|
|
||||||
|
- name: Build texthooker-ui
|
||||||
|
run: |
|
||||||
|
cd vendor/texthooker-ui
|
||||||
|
bun install --frozen-lockfile
|
||||||
|
bun run build
|
||||||
|
|
||||||
|
- name: Download previous package size reports
|
||||||
|
shell: bash
|
||||||
|
env:
|
||||||
|
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
run: |
|
||||||
|
mkdir -p .tmp/package-baseline
|
||||||
|
previous=$(gh api "repos/$GITHUB_REPOSITORY/releases" --jq '[.[] | select(.draft == false and .tag_name != env.GITHUB_REF_NAME)] | sort_by(.published_at) | last | .tag_name // empty')
|
||||||
|
if [ -n "$previous" ]; then
|
||||||
|
gh release download "$previous" --pattern 'package-size-*.json' --dir .tmp/package-baseline || echo 'Previous release has no package size reports; size comparison will be skipped.'
|
||||||
|
fi
|
||||||
|
|
||||||
|
- name: Build AppImage
|
||||||
|
run: bun run build:appimage
|
||||||
|
|
||||||
|
- name: Build unversioned AppImage
|
||||||
|
run: |
|
||||||
|
shopt -s nullglob
|
||||||
|
appimages=(release/SubMiner-*.AppImage)
|
||||||
|
if [ "${#appimages[@]}" -eq 0 ]; then
|
||||||
|
echo "No versioned AppImage found to create unversioned artifact."
|
||||||
|
ls -la release
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
cp "${appimages[0]}" release/SubMiner.AppImage
|
||||||
|
|
||||||
|
- name: Smoke packaged runtime assets
|
||||||
|
shell: bash
|
||||||
|
run: xvfb-run -a bun run test:package "release/linux-unpacked/resources"
|
||||||
|
|
||||||
|
- name: Upload AppImage artifact
|
||||||
|
uses: actions/upload-artifact@v4
|
||||||
|
with:
|
||||||
|
name: appimage
|
||||||
|
path: |
|
||||||
|
release/*.AppImage
|
||||||
|
release/latest*.yml
|
||||||
|
release/*.blockmap
|
||||||
|
release/package-size-*.json
|
||||||
|
if-no-files-found: error
|
||||||
|
|
||||||
|
build-macos:
|
||||||
|
runs-on: macos-latest
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
submodules: true
|
||||||
|
persist-credentials: false
|
||||||
|
|
||||||
|
- name: Setup Bun
|
||||||
|
uses: oven-sh/setup-bun@v2
|
||||||
|
with:
|
||||||
|
bun-version: 1.3.5
|
||||||
|
|
||||||
|
- name: Cache dependencies
|
||||||
|
uses: actions/cache@v4
|
||||||
|
with:
|
||||||
|
path: |
|
||||||
|
~/.bun/install/cache
|
||||||
|
node_modules
|
||||||
|
stats/node_modules
|
||||||
|
vendor/texthooker-ui/node_modules
|
||||||
|
vendor/subminer-yomitan/node_modules
|
||||||
|
key: ${{ runner.os }}-${{ runner.arch }}-bun-${{ hashFiles('bun.lock', 'stats/bun.lock', 'vendor/texthooker-ui/bun.lock', 'vendor/subminer-yomitan/package-lock.json') }}
|
||||||
|
restore-keys: |
|
||||||
|
${{ runner.os }}-${{ runner.arch }}-bun-
|
||||||
|
|
||||||
|
- name: Validate macOS signing/notarization secrets
|
||||||
|
run: |
|
||||||
|
missing=0
|
||||||
|
for name in CSC_LINK CSC_KEY_PASSWORD APPLE_ID APPLE_APP_SPECIFIC_PASSWORD APPLE_TEAM_ID; do
|
||||||
|
if [ -z "${!name}" ]; then
|
||||||
|
echo "Missing required secret: $name"
|
||||||
|
missing=1
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
if [ "$missing" -ne 0 ]; then
|
||||||
|
echo "Set all required macOS signing/notarization secrets and rerun."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
env:
|
||||||
|
CSC_LINK: ${{ secrets.CSC_LINK }}
|
||||||
|
CSC_KEY_PASSWORD: ${{ secrets.CSC_KEY_PASSWORD }}
|
||||||
|
APPLE_ID: ${{ secrets.APPLE_ID }}
|
||||||
|
APPLE_APP_SPECIFIC_PASSWORD: ${{ secrets.APPLE_APP_SPECIFIC_PASSWORD }}
|
||||||
|
APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }}
|
||||||
|
|
||||||
|
- name: Install dependencies
|
||||||
|
run: |
|
||||||
|
bun install --frozen-lockfile
|
||||||
|
cd stats && bun install --frozen-lockfile
|
||||||
|
|
||||||
|
- name: Build texthooker-ui
|
||||||
|
run: |
|
||||||
|
cd vendor/texthooker-ui
|
||||||
|
bun install --frozen-lockfile
|
||||||
|
bun run build
|
||||||
|
|
||||||
|
- name: Download previous package size reports
|
||||||
|
shell: bash
|
||||||
|
env:
|
||||||
|
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
run: |
|
||||||
|
mkdir -p .tmp/package-baseline
|
||||||
|
previous=$(gh api "repos/$GITHUB_REPOSITORY/releases" --jq '[.[] | select(.draft == false and .tag_name != env.GITHUB_REF_NAME)] | sort_by(.published_at) | last | .tag_name // empty')
|
||||||
|
if [ -n "$previous" ]; then
|
||||||
|
gh release download "$previous" --pattern 'package-size-*.json' --dir .tmp/package-baseline || echo 'Previous release has no package size reports; size comparison will be skipped.'
|
||||||
|
fi
|
||||||
|
|
||||||
|
- name: Build signed + notarized macOS artifacts
|
||||||
|
run: bun run build:mac
|
||||||
|
env:
|
||||||
|
CSC_LINK: ${{ secrets.CSC_LINK }}
|
||||||
|
CSC_KEY_PASSWORD: ${{ secrets.CSC_KEY_PASSWORD }}
|
||||||
|
APPLE_ID: ${{ secrets.APPLE_ID }}
|
||||||
|
APPLE_APP_SPECIFIC_PASSWORD: ${{ secrets.APPLE_APP_SPECIFIC_PASSWORD }}
|
||||||
|
APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }}
|
||||||
|
|
||||||
|
- name: Smoke packaged runtime assets
|
||||||
|
shell: bash
|
||||||
|
run: bun run test:package "release/mac-arm64/SubMiner.app/Contents/Resources"
|
||||||
|
|
||||||
|
- name: Upload macOS artifacts
|
||||||
|
uses: actions/upload-artifact@v4
|
||||||
|
with:
|
||||||
|
name: macos
|
||||||
|
path: |
|
||||||
|
release/*.dmg
|
||||||
|
release/*.zip
|
||||||
|
release/latest*.yml
|
||||||
|
release/*.blockmap
|
||||||
|
release/package-size-*.json
|
||||||
|
if-no-files-found: error
|
||||||
|
|
||||||
|
build-windows:
|
||||||
|
runs-on: windows-latest
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
submodules: true
|
||||||
|
persist-credentials: false
|
||||||
|
|
||||||
|
- name: Setup Bun
|
||||||
|
uses: oven-sh/setup-bun@v2
|
||||||
|
with:
|
||||||
|
bun-version: 1.3.5
|
||||||
|
|
||||||
|
- name: Cache dependencies
|
||||||
|
uses: actions/cache@v4
|
||||||
|
with:
|
||||||
|
path: |
|
||||||
|
~/.bun/install/cache
|
||||||
|
node_modules
|
||||||
|
stats/node_modules
|
||||||
|
vendor/texthooker-ui/node_modules
|
||||||
|
vendor/subminer-yomitan/node_modules
|
||||||
|
key: ${{ runner.os }}-${{ runner.arch }}-bun-${{ hashFiles('bun.lock', 'stats/bun.lock', 'vendor/texthooker-ui/bun.lock', 'vendor/subminer-yomitan/package-lock.json') }}
|
||||||
|
restore-keys: |
|
||||||
|
${{ runner.os }}-${{ runner.arch }}-bun-
|
||||||
|
|
||||||
|
- name: Install dependencies
|
||||||
|
run: |
|
||||||
|
bun install --frozen-lockfile
|
||||||
|
cd stats && bun install --frozen-lockfile
|
||||||
|
|
||||||
|
- name: Build texthooker-ui
|
||||||
|
shell: powershell
|
||||||
|
run: |
|
||||||
|
Set-Location vendor/texthooker-ui
|
||||||
|
bun install --frozen-lockfile
|
||||||
|
bun run build
|
||||||
|
|
||||||
|
- name: Download previous package size reports
|
||||||
|
shell: bash
|
||||||
|
env:
|
||||||
|
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
run: |
|
||||||
|
mkdir -p .tmp/package-baseline
|
||||||
|
previous=$(gh api "repos/$GITHUB_REPOSITORY/releases" --jq '[.[] | select(.draft == false and .tag_name != env.GITHUB_REF_NAME)] | sort_by(.published_at) | last | .tag_name // empty')
|
||||||
|
if [ -n "$previous" ]; then
|
||||||
|
gh release download "$previous" --pattern 'package-size-*.json' --dir .tmp/package-baseline || echo 'Previous release has no package size reports; size comparison will be skipped.'
|
||||||
|
fi
|
||||||
|
|
||||||
|
- name: Verify managed Windows launcher
|
||||||
|
run: bun test src/main/runtime/managed-launcher.test.ts
|
||||||
|
|
||||||
|
- name: Verify Windows launcher bootstrap
|
||||||
|
run: bun test src/main/runtime/windows-launcher-bootstrap.test.ts
|
||||||
|
|
||||||
|
- name: Build unsigned Windows artifacts
|
||||||
|
run: bun run build:win:unsigned
|
||||||
|
|
||||||
|
- name: Smoke packaged runtime assets
|
||||||
|
shell: bash
|
||||||
|
run: bun run test:package "release/win-unpacked/resources"
|
||||||
|
|
||||||
|
- name: Upload Windows artifacts
|
||||||
|
uses: actions/upload-artifact@v4
|
||||||
|
with:
|
||||||
|
name: windows
|
||||||
|
path: |
|
||||||
|
release/*.exe
|
||||||
|
release/*.zip
|
||||||
|
release/latest*.yml
|
||||||
|
release/*.blockmap
|
||||||
|
release/package-size-*.json
|
||||||
|
if-no-files-found: error
|
||||||
@@ -16,207 +16,20 @@ jobs:
|
|||||||
contents: read
|
contents: read
|
||||||
uses: ./.github/workflows/quality-gate.yml
|
uses: ./.github/workflows/quality-gate.yml
|
||||||
|
|
||||||
build-linux:
|
package:
|
||||||
needs: [quality-gate]
|
needs: [quality-gate]
|
||||||
runs-on: ubuntu-latest
|
permissions:
|
||||||
steps:
|
contents: read
|
||||||
- name: Checkout
|
uses: ./.github/workflows/package-release.yml
|
||||||
uses: actions/checkout@v4
|
secrets:
|
||||||
with:
|
CSC_LINK: ${{ secrets.CSC_LINK }}
|
||||||
submodules: true
|
CSC_KEY_PASSWORD: ${{ secrets.CSC_KEY_PASSWORD }}
|
||||||
|
APPLE_ID: ${{ secrets.APPLE_ID }}
|
||||||
- name: Setup Bun
|
APPLE_APP_SPECIFIC_PASSWORD: ${{ secrets.APPLE_APP_SPECIFIC_PASSWORD }}
|
||||||
uses: oven-sh/setup-bun@v2
|
APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }}
|
||||||
with:
|
|
||||||
bun-version: 1.3.5
|
|
||||||
|
|
||||||
- name: Cache dependencies
|
|
||||||
uses: actions/cache@v4
|
|
||||||
with:
|
|
||||||
path: |
|
|
||||||
~/.bun/install/cache
|
|
||||||
node_modules
|
|
||||||
stats/node_modules
|
|
||||||
vendor/texthooker-ui/node_modules
|
|
||||||
vendor/subminer-yomitan/node_modules
|
|
||||||
key: ${{ runner.os }}-${{ runner.arch }}-bun-${{ hashFiles('bun.lock', 'stats/bun.lock', 'vendor/texthooker-ui/package.json', 'vendor/subminer-yomitan/package-lock.json') }}
|
|
||||||
restore-keys: |
|
|
||||||
${{ runner.os }}-${{ runner.arch }}-bun-
|
|
||||||
|
|
||||||
- name: Install dependencies
|
|
||||||
run: |
|
|
||||||
bun install --frozen-lockfile
|
|
||||||
cd stats && bun install --frozen-lockfile
|
|
||||||
|
|
||||||
- name: Build texthooker-ui
|
|
||||||
run: |
|
|
||||||
cd vendor/texthooker-ui
|
|
||||||
bun install
|
|
||||||
bun run build
|
|
||||||
|
|
||||||
- name: Build AppImage
|
|
||||||
run: bun run build:appimage
|
|
||||||
|
|
||||||
- name: Build unversioned AppImage
|
|
||||||
run: |
|
|
||||||
shopt -s nullglob
|
|
||||||
appimages=(release/SubMiner-*.AppImage)
|
|
||||||
if [ "${#appimages[@]}" -eq 0 ]; then
|
|
||||||
echo "No versioned AppImage found to create unversioned artifact."
|
|
||||||
ls -la release
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
cp "${appimages[0]}" release/SubMiner.AppImage
|
|
||||||
|
|
||||||
- name: Upload AppImage artifact
|
|
||||||
uses: actions/upload-artifact@v4
|
|
||||||
with:
|
|
||||||
name: appimage
|
|
||||||
path: |
|
|
||||||
release/*.AppImage
|
|
||||||
release/latest*.yml
|
|
||||||
release/*.blockmap
|
|
||||||
if-no-files-found: error
|
|
||||||
|
|
||||||
build-macos:
|
|
||||||
needs: [quality-gate]
|
|
||||||
runs-on: macos-latest
|
|
||||||
steps:
|
|
||||||
- name: Checkout
|
|
||||||
uses: actions/checkout@v4
|
|
||||||
with:
|
|
||||||
submodules: true
|
|
||||||
|
|
||||||
- name: Setup Bun
|
|
||||||
uses: oven-sh/setup-bun@v2
|
|
||||||
with:
|
|
||||||
bun-version: 1.3.5
|
|
||||||
|
|
||||||
- name: Cache dependencies
|
|
||||||
uses: actions/cache@v4
|
|
||||||
with:
|
|
||||||
path: |
|
|
||||||
~/.bun/install/cache
|
|
||||||
node_modules
|
|
||||||
stats/node_modules
|
|
||||||
vendor/texthooker-ui/node_modules
|
|
||||||
vendor/subminer-yomitan/node_modules
|
|
||||||
key: ${{ runner.os }}-${{ runner.arch }}-bun-${{ hashFiles('bun.lock', 'stats/bun.lock', 'vendor/texthooker-ui/package.json', 'vendor/subminer-yomitan/package-lock.json') }}
|
|
||||||
restore-keys: |
|
|
||||||
${{ runner.os }}-${{ runner.arch }}-bun-
|
|
||||||
|
|
||||||
- name: Validate macOS signing/notarization secrets
|
|
||||||
run: |
|
|
||||||
missing=0
|
|
||||||
for name in CSC_LINK CSC_KEY_PASSWORD APPLE_ID APPLE_APP_SPECIFIC_PASSWORD APPLE_TEAM_ID; do
|
|
||||||
if [ -z "${!name}" ]; then
|
|
||||||
echo "Missing required secret: $name"
|
|
||||||
missing=1
|
|
||||||
fi
|
|
||||||
done
|
|
||||||
if [ "$missing" -ne 0 ]; then
|
|
||||||
echo "Set all required macOS signing/notarization secrets and rerun."
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
env:
|
|
||||||
CSC_LINK: ${{ secrets.CSC_LINK }}
|
|
||||||
CSC_KEY_PASSWORD: ${{ secrets.CSC_KEY_PASSWORD }}
|
|
||||||
APPLE_ID: ${{ secrets.APPLE_ID }}
|
|
||||||
APPLE_APP_SPECIFIC_PASSWORD: ${{ secrets.APPLE_APP_SPECIFIC_PASSWORD }}
|
|
||||||
APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }}
|
|
||||||
|
|
||||||
- name: Install dependencies
|
|
||||||
run: |
|
|
||||||
bun install --frozen-lockfile
|
|
||||||
cd stats && bun install --frozen-lockfile
|
|
||||||
|
|
||||||
- name: Build texthooker-ui
|
|
||||||
run: |
|
|
||||||
cd vendor/texthooker-ui
|
|
||||||
bun install
|
|
||||||
bun run build
|
|
||||||
|
|
||||||
- name: Build signed + notarized macOS artifacts
|
|
||||||
run: bun run build:mac
|
|
||||||
env:
|
|
||||||
CSC_LINK: ${{ secrets.CSC_LINK }}
|
|
||||||
CSC_KEY_PASSWORD: ${{ secrets.CSC_KEY_PASSWORD }}
|
|
||||||
APPLE_ID: ${{ secrets.APPLE_ID }}
|
|
||||||
APPLE_APP_SPECIFIC_PASSWORD: ${{ secrets.APPLE_APP_SPECIFIC_PASSWORD }}
|
|
||||||
APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }}
|
|
||||||
|
|
||||||
- name: Upload macOS artifacts
|
|
||||||
uses: actions/upload-artifact@v4
|
|
||||||
with:
|
|
||||||
name: macos
|
|
||||||
path: |
|
|
||||||
release/*.dmg
|
|
||||||
release/*.zip
|
|
||||||
release/latest*.yml
|
|
||||||
release/*.blockmap
|
|
||||||
if-no-files-found: error
|
|
||||||
|
|
||||||
build-windows:
|
|
||||||
needs: [quality-gate]
|
|
||||||
runs-on: windows-latest
|
|
||||||
steps:
|
|
||||||
- name: Checkout
|
|
||||||
uses: actions/checkout@v4
|
|
||||||
with:
|
|
||||||
submodules: true
|
|
||||||
|
|
||||||
- name: Setup Bun
|
|
||||||
uses: oven-sh/setup-bun@v2
|
|
||||||
with:
|
|
||||||
bun-version: 1.3.5
|
|
||||||
|
|
||||||
- name: Cache dependencies
|
|
||||||
uses: actions/cache@v4
|
|
||||||
with:
|
|
||||||
path: |
|
|
||||||
~/.bun/install/cache
|
|
||||||
node_modules
|
|
||||||
stats/node_modules
|
|
||||||
vendor/texthooker-ui/node_modules
|
|
||||||
vendor/subminer-yomitan/node_modules
|
|
||||||
key: ${{ runner.os }}-${{ runner.arch }}-bun-${{ hashFiles('bun.lock', 'stats/bun.lock', 'vendor/texthooker-ui/package.json', 'vendor/subminer-yomitan/package-lock.json') }}
|
|
||||||
restore-keys: |
|
|
||||||
${{ runner.os }}-${{ runner.arch }}-bun-
|
|
||||||
|
|
||||||
- name: Install dependencies
|
|
||||||
run: |
|
|
||||||
bun install --frozen-lockfile
|
|
||||||
cd stats && bun install --frozen-lockfile
|
|
||||||
|
|
||||||
- name: Build texthooker-ui
|
|
||||||
shell: powershell
|
|
||||||
run: |
|
|
||||||
Set-Location vendor/texthooker-ui
|
|
||||||
bun install
|
|
||||||
bun run build
|
|
||||||
|
|
||||||
- name: Verify managed Windows launcher
|
|
||||||
run: bun test src/main/runtime/managed-launcher.test.ts
|
|
||||||
|
|
||||||
- name: Verify Windows launcher bootstrap
|
|
||||||
run: bun test src/main/runtime/windows-launcher-bootstrap.test.ts
|
|
||||||
|
|
||||||
- name: Build unsigned Windows artifacts
|
|
||||||
run: bun run build:win:unsigned
|
|
||||||
|
|
||||||
- name: Upload Windows artifacts
|
|
||||||
uses: actions/upload-artifact@v4
|
|
||||||
with:
|
|
||||||
name: windows
|
|
||||||
path: |
|
|
||||||
release/*.exe
|
|
||||||
release/*.zip
|
|
||||||
release/latest*.yml
|
|
||||||
release/*.blockmap
|
|
||||||
if-no-files-found: error
|
|
||||||
|
|
||||||
release:
|
release:
|
||||||
needs: [build-linux, build-macos, build-windows]
|
needs: [package]
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
permissions:
|
permissions:
|
||||||
contents: write
|
contents: write
|
||||||
@@ -291,6 +104,7 @@ jobs:
|
|||||||
run: |
|
run: |
|
||||||
shopt -s nullglob
|
shopt -s nullglob
|
||||||
files=(release/*.AppImage release/*.dmg release/*.exe release/*.zip release/*.tar.gz release/latest*.yml release/*.blockmap dist/launcher/subminer dist/launcher/subminer.cmd)
|
files=(release/*.AppImage release/*.dmg release/*.exe release/*.zip release/*.tar.gz release/latest*.yml release/*.blockmap dist/launcher/subminer dist/launcher/subminer.cmd)
|
||||||
|
files+=(release/package-size-*.json)
|
||||||
if [ "${#files[@]}" -eq 0 ]; then
|
if [ "${#files[@]}" -eq 0 ]; then
|
||||||
echo "No release artifacts found for checksum generation."
|
echo "No release artifacts found for checksum generation."
|
||||||
exit 1
|
exit 1
|
||||||
@@ -337,6 +151,7 @@ jobs:
|
|||||||
release/latest*.yml
|
release/latest*.yml
|
||||||
release/*.blockmap
|
release/*.blockmap
|
||||||
release/SHA256SUMS.txt
|
release/SHA256SUMS.txt
|
||||||
|
release/package-size-*.json
|
||||||
dist/launcher/subminer
|
dist/launcher/subminer
|
||||||
dist/launcher/subminer.cmd
|
dist/launcher/subminer.cmd
|
||||||
)
|
)
|
||||||
|
|||||||
+13
-196
@@ -17,205 +17,20 @@ jobs:
|
|||||||
contents: read
|
contents: read
|
||||||
uses: ./.github/workflows/quality-gate.yml
|
uses: ./.github/workflows/quality-gate.yml
|
||||||
|
|
||||||
build-linux:
|
package:
|
||||||
needs: [quality-gate]
|
needs: [quality-gate]
|
||||||
runs-on: ubuntu-latest
|
permissions:
|
||||||
steps:
|
contents: read
|
||||||
- name: Checkout
|
uses: ./.github/workflows/package-release.yml
|
||||||
uses: actions/checkout@v4
|
secrets:
|
||||||
with:
|
CSC_LINK: ${{ secrets.CSC_LINK }}
|
||||||
submodules: true
|
CSC_KEY_PASSWORD: ${{ secrets.CSC_KEY_PASSWORD }}
|
||||||
|
APPLE_ID: ${{ secrets.APPLE_ID }}
|
||||||
- name: Setup Bun
|
APPLE_APP_SPECIFIC_PASSWORD: ${{ secrets.APPLE_APP_SPECIFIC_PASSWORD }}
|
||||||
uses: oven-sh/setup-bun@v2
|
APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }}
|
||||||
with:
|
|
||||||
bun-version: 1.3.5
|
|
||||||
|
|
||||||
- name: Cache dependencies
|
|
||||||
uses: actions/cache@v4
|
|
||||||
with:
|
|
||||||
path: |
|
|
||||||
~/.bun/install/cache
|
|
||||||
node_modules
|
|
||||||
stats/node_modules
|
|
||||||
vendor/texthooker-ui/node_modules
|
|
||||||
vendor/subminer-yomitan/node_modules
|
|
||||||
key: ${{ runner.os }}-bun-${{ hashFiles('bun.lock', 'stats/bun.lock', 'vendor/texthooker-ui/package.json', 'vendor/subminer-yomitan/package-lock.json') }}
|
|
||||||
restore-keys: |
|
|
||||||
${{ runner.os }}-bun-
|
|
||||||
|
|
||||||
- name: Install dependencies
|
|
||||||
run: |
|
|
||||||
bun install --frozen-lockfile
|
|
||||||
cd stats && bun install --frozen-lockfile
|
|
||||||
|
|
||||||
- name: Build texthooker-ui
|
|
||||||
run: |
|
|
||||||
cd vendor/texthooker-ui
|
|
||||||
bun install
|
|
||||||
bun run build
|
|
||||||
|
|
||||||
- name: Build AppImage
|
|
||||||
run: bun run build:appimage
|
|
||||||
|
|
||||||
- name: Build unversioned AppImage
|
|
||||||
run: |
|
|
||||||
shopt -s nullglob
|
|
||||||
appimages=(release/SubMiner-*.AppImage)
|
|
||||||
if [ "${#appimages[@]}" -eq 0 ]; then
|
|
||||||
echo "No versioned AppImage found to create unversioned artifact."
|
|
||||||
ls -la release
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
cp "${appimages[0]}" release/SubMiner.AppImage
|
|
||||||
|
|
||||||
- name: Upload AppImage artifact
|
|
||||||
uses: actions/upload-artifact@v4
|
|
||||||
with:
|
|
||||||
name: appimage
|
|
||||||
path: |
|
|
||||||
release/*.AppImage
|
|
||||||
release/latest*.yml
|
|
||||||
release/*.blockmap
|
|
||||||
|
|
||||||
build-macos:
|
|
||||||
needs: [quality-gate]
|
|
||||||
runs-on: macos-latest
|
|
||||||
steps:
|
|
||||||
- name: Checkout
|
|
||||||
uses: actions/checkout@v4
|
|
||||||
with:
|
|
||||||
submodules: true
|
|
||||||
|
|
||||||
- name: Setup Bun
|
|
||||||
uses: oven-sh/setup-bun@v2
|
|
||||||
with:
|
|
||||||
bun-version: 1.3.5
|
|
||||||
|
|
||||||
- name: Cache dependencies
|
|
||||||
uses: actions/cache@v4
|
|
||||||
with:
|
|
||||||
path: |
|
|
||||||
~/.bun/install/cache
|
|
||||||
node_modules
|
|
||||||
stats/node_modules
|
|
||||||
vendor/texthooker-ui/node_modules
|
|
||||||
vendor/subminer-yomitan/node_modules
|
|
||||||
key: ${{ runner.os }}-bun-${{ hashFiles('bun.lock', 'stats/bun.lock', 'vendor/texthooker-ui/package.json', 'vendor/subminer-yomitan/package-lock.json') }}
|
|
||||||
restore-keys: |
|
|
||||||
${{ runner.os }}-bun-
|
|
||||||
|
|
||||||
- name: Validate macOS signing/notarization secrets
|
|
||||||
run: |
|
|
||||||
missing=0
|
|
||||||
for name in CSC_LINK CSC_KEY_PASSWORD APPLE_ID APPLE_APP_SPECIFIC_PASSWORD APPLE_TEAM_ID; do
|
|
||||||
if [ -z "${!name}" ]; then
|
|
||||||
echo "Missing required secret: $name"
|
|
||||||
missing=1
|
|
||||||
fi
|
|
||||||
done
|
|
||||||
if [ "$missing" -ne 0 ]; then
|
|
||||||
echo "Set all required macOS signing/notarization secrets and rerun."
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
env:
|
|
||||||
CSC_LINK: ${{ secrets.CSC_LINK }}
|
|
||||||
CSC_KEY_PASSWORD: ${{ secrets.CSC_KEY_PASSWORD }}
|
|
||||||
APPLE_ID: ${{ secrets.APPLE_ID }}
|
|
||||||
APPLE_APP_SPECIFIC_PASSWORD: ${{ secrets.APPLE_APP_SPECIFIC_PASSWORD }}
|
|
||||||
APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }}
|
|
||||||
|
|
||||||
- name: Install dependencies
|
|
||||||
run: |
|
|
||||||
bun install --frozen-lockfile
|
|
||||||
cd stats && bun install --frozen-lockfile
|
|
||||||
|
|
||||||
- name: Build texthooker-ui
|
|
||||||
run: |
|
|
||||||
cd vendor/texthooker-ui
|
|
||||||
bun install
|
|
||||||
bun run build
|
|
||||||
|
|
||||||
- name: Build signed + notarized macOS artifacts
|
|
||||||
run: bun run build:mac
|
|
||||||
env:
|
|
||||||
CSC_LINK: ${{ secrets.CSC_LINK }}
|
|
||||||
CSC_KEY_PASSWORD: ${{ secrets.CSC_KEY_PASSWORD }}
|
|
||||||
APPLE_ID: ${{ secrets.APPLE_ID }}
|
|
||||||
APPLE_APP_SPECIFIC_PASSWORD: ${{ secrets.APPLE_APP_SPECIFIC_PASSWORD }}
|
|
||||||
APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }}
|
|
||||||
|
|
||||||
- name: Upload macOS artifacts
|
|
||||||
uses: actions/upload-artifact@v4
|
|
||||||
with:
|
|
||||||
name: macos
|
|
||||||
path: |
|
|
||||||
release/*.dmg
|
|
||||||
release/*.zip
|
|
||||||
release/latest*.yml
|
|
||||||
release/*.blockmap
|
|
||||||
|
|
||||||
build-windows:
|
|
||||||
needs: [quality-gate]
|
|
||||||
runs-on: windows-latest
|
|
||||||
steps:
|
|
||||||
- name: Checkout
|
|
||||||
uses: actions/checkout@v4
|
|
||||||
with:
|
|
||||||
submodules: true
|
|
||||||
|
|
||||||
- name: Setup Bun
|
|
||||||
uses: oven-sh/setup-bun@v2
|
|
||||||
with:
|
|
||||||
bun-version: 1.3.5
|
|
||||||
|
|
||||||
- name: Cache dependencies
|
|
||||||
uses: actions/cache@v4
|
|
||||||
with:
|
|
||||||
path: |
|
|
||||||
~/.bun/install/cache
|
|
||||||
node_modules
|
|
||||||
stats/node_modules
|
|
||||||
vendor/texthooker-ui/node_modules
|
|
||||||
vendor/subminer-yomitan/node_modules
|
|
||||||
key: ${{ runner.os }}-bun-${{ hashFiles('bun.lock', 'stats/bun.lock', 'vendor/texthooker-ui/package.json', 'vendor/subminer-yomitan/package-lock.json') }}
|
|
||||||
restore-keys: |
|
|
||||||
${{ runner.os }}-bun-
|
|
||||||
|
|
||||||
- name: Install dependencies
|
|
||||||
run: |
|
|
||||||
bun install --frozen-lockfile
|
|
||||||
cd stats && bun install --frozen-lockfile
|
|
||||||
|
|
||||||
- name: Build texthooker-ui
|
|
||||||
shell: powershell
|
|
||||||
run: |
|
|
||||||
Set-Location vendor/texthooker-ui
|
|
||||||
bun install
|
|
||||||
bun run build
|
|
||||||
|
|
||||||
- name: Verify managed Windows launcher
|
|
||||||
run: bun test src/main/runtime/managed-launcher.test.ts
|
|
||||||
|
|
||||||
- name: Verify Windows launcher bootstrap
|
|
||||||
run: bun test src/main/runtime/windows-launcher-bootstrap.test.ts
|
|
||||||
|
|
||||||
- name: Build unsigned Windows artifacts
|
|
||||||
run: bun run build:win:unsigned
|
|
||||||
|
|
||||||
- name: Upload Windows artifacts
|
|
||||||
uses: actions/upload-artifact@v4
|
|
||||||
with:
|
|
||||||
name: windows
|
|
||||||
path: |
|
|
||||||
release/*.exe
|
|
||||||
release/*.zip
|
|
||||||
release/latest*.yml
|
|
||||||
release/*.blockmap
|
|
||||||
if-no-files-found: error
|
|
||||||
|
|
||||||
release:
|
release:
|
||||||
needs: [build-linux, build-macos, build-windows]
|
needs: [package]
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
permissions:
|
permissions:
|
||||||
contents: write
|
contents: write
|
||||||
@@ -290,6 +105,7 @@ jobs:
|
|||||||
run: |
|
run: |
|
||||||
shopt -s nullglob
|
shopt -s nullglob
|
||||||
files=(release/*.AppImage release/*.dmg release/*.exe release/*.zip release/*.tar.gz release/latest*.yml release/*.blockmap dist/launcher/subminer dist/launcher/subminer.cmd)
|
files=(release/*.AppImage release/*.dmg release/*.exe release/*.zip release/*.tar.gz release/latest*.yml release/*.blockmap dist/launcher/subminer dist/launcher/subminer.cmd)
|
||||||
|
files+=(release/package-size-*.json)
|
||||||
if [ "${#files[@]}" -eq 0 ]; then
|
if [ "${#files[@]}" -eq 0 ]; then
|
||||||
echo "No release artifacts found for checksum generation."
|
echo "No release artifacts found for checksum generation."
|
||||||
exit 1
|
exit 1
|
||||||
@@ -354,6 +170,7 @@ jobs:
|
|||||||
release/latest*.yml
|
release/latest*.yml
|
||||||
release/*.blockmap
|
release/*.blockmap
|
||||||
release/SHA256SUMS.txt
|
release/SHA256SUMS.txt
|
||||||
|
release/package-size-*.json
|
||||||
dist/launcher/subminer
|
dist/launcher/subminer
|
||||||
dist/launcher/subminer.cmd
|
dist/launcher/subminer.cmd
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -18,6 +18,7 @@
|
|||||||
"ws": "^8.21.0",
|
"ws": "^8.21.0",
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
"@electron/asar": "3.4.1",
|
||||||
"@types/node": "^24.10.0",
|
"@types/node": "^24.10.0",
|
||||||
"@types/ws": "^8.18.1",
|
"@types/ws": "^8.18.1",
|
||||||
"electron": "42.6.0",
|
"electron": "42.6.0",
|
||||||
|
|||||||
@@ -0,0 +1,4 @@
|
|||||||
|
type: fixed
|
||||||
|
area: overlay
|
||||||
|
|
||||||
|
- Keep Hyprland recovery dialogs above SubMiner windows so overlay placement updates do not cover their Wait and Close buttons.
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
type: changed
|
||||||
|
area: release
|
||||||
|
|
||||||
|
- Reduced installer and unpacked app size by excluding documentation demo media, dependency source maps, TypeScript files, test and fixture directories, other development files, and unused Koffi platform binaries, and sharing the existing Japanese UI font across windows.
|
||||||
|
- Added package content checks, published size reports with release comparisons, and packaged asset/native-module smoke checks to the shared stable and prerelease build workflow. Size growth is reported without blocking releases.
|
||||||
@@ -378,6 +378,10 @@ windowrule = no_blur on, match:class SubMiner
|
|||||||
|
|
||||||
If you still see a solid background or visual artifacts instead of the mpv video underneath, the culprit is almost always a global opacity/blur rule applying to the overlay - the `opaque`/`opacity` and `no_blur` fields above override it.
|
If you still see a solid background or visual artifacts instead of the mpv video underneath, the culprit is almost always a global opacity/blur rule applying to the overlay - the `opaque`/`opacity` and `no_blur` fields above override it.
|
||||||
|
|
||||||
|
**Application Not Responding dialog covered by the overlay**
|
||||||
|
|
||||||
|
SubMiner keeps visible Hyprland system dialogs above its windows on the same workspace when updating overlay placement. This lets you click the recovery dialog even while the overlay accepts mouse input. If the whole SubMiner process is frozen, use Hyprland's window-focus bindings to reach the dialog; SubMiner cannot update window order until it resumes.
|
||||||
|
|
||||||
**Global shortcuts not working**
|
**Global shortcuts not working**
|
||||||
|
|
||||||
On Hyprland, Electron cannot register global shortcuts on its own. You must explicitly pass keybindings to SubMiner using `pass` rules:
|
On Hyprland, Electron cannot register global shortcuts on its own. You must explicitly pass keybindings to SubMiner using `pass` rules:
|
||||||
|
|||||||
@@ -11,6 +11,48 @@
|
|||||||
`ANTHROPIC_API_KEY` works. Install from <https://claude.com/claude-code> if
|
`ANTHROPIC_API_KEY` works. Install from <https://claude.com/claude-code> if
|
||||||
you don't already have it.
|
you don't already have it.
|
||||||
|
|
||||||
|
## Package contents and size checks
|
||||||
|
|
||||||
|
Stable and prerelease workflows share `.github/workflows/package-release.yml`.
|
||||||
|
Both callers explicitly pass the five required macOS signing/notarization
|
||||||
|
secrets; `GITHUB_TOKEN` remains automatically available to the reusable workflow.
|
||||||
|
Each platform verifies its ASAR and external resources before signing, then
|
||||||
|
measures the signed app and installers before upload. Missing runtime assets,
|
||||||
|
foreign SQLite/Koffi binaries, duplicate UI fonts, demo media, source maps,
|
||||||
|
TypeScript files, and nested test or fixture directories
|
||||||
|
fail the build. Size measurements are informational and do not block releases.
|
||||||
|
Current targets are Linux x64, macOS arm64, and Windows x64.
|
||||||
|
|
||||||
|
The runtime allowlist includes `dist/`, `stats/dist/`, and
|
||||||
|
`vendor/texthooker-ui/docs/` plus metadata, config example, and license. The
|
||||||
|
texthooker `docs/` directory is its built UI. Keep the positive `package.json`
|
||||||
|
pattern in platform `files` lists: electron-builder otherwise treats an
|
||||||
|
exclusion-only platform list as a separate include-all matcher. Windows keeps
|
||||||
|
only its target Koffi binary; other platforms omit Koffi. Desktop UIs share the
|
||||||
|
original M PLUS 1 TTF in `dist/fonts/`.
|
||||||
|
|
||||||
|
`release/package-size-<platform>-<arch>.json` reports unpacked bytes, largest
|
||||||
|
files inside and outside ASAR, native binaries, and compressed artifact sizes.
|
||||||
|
Framework symlinks are not counted twice. Reports are checksummed and published.
|
||||||
|
CI downloads the preceding release's reports for comparison; older releases
|
||||||
|
without reports skip comparison. Review the inventory and reason for growth
|
||||||
|
when comparing releases. An AppImage normally
|
||||||
|
runs compressed; its extracted size is a separate measurement.
|
||||||
|
|
||||||
|
The shared workflow runs `bun run test:package <resources-directory>` with the
|
||||||
|
pinned Electron runtime and temporary user data. On headless Linux, prefix it
|
||||||
|
with `xvfb-run -a`. This checks packaged SQLite, Windows FFI loading/polling,
|
||||||
|
texthooker serving, Yomitan loading, UI assets, and Japanese font loading.
|
||||||
|
Standalone pages lack app IPC handlers and can log related errors; this check
|
||||||
|
does not replace an installed app session.
|
||||||
|
|
||||||
|
Before shipping packaging changes, check each platform's installed app:
|
||||||
|
startup and mpv tracking, dictionary lookup and stroke orders, settings/sync UI,
|
||||||
|
stats persistence, sentence mining with AnkiConnect, and updating from the prior
|
||||||
|
release. Preserve Electron locales, graphics fallbacks, codecs, dictionaries,
|
||||||
|
license notices, updater metadata, blockmaps, and the macOS updater ZIP. Trim
|
||||||
|
files before signing and generating updater hashes, never from a signed app.
|
||||||
|
|
||||||
## Stable Release
|
## Stable Release
|
||||||
|
|
||||||
1. Confirm `main` is green: `gh run list --workflow CI --limit 5`.
|
1. Confirm `main` is green: `gh run list --workflow CI --limit 5`.
|
||||||
|
|||||||
@@ -52,6 +52,11 @@ bun run docs:build
|
|||||||
- Runtime-compat / compiled behavior: `bun run test:runtime:compat`
|
- Runtime-compat / compiled behavior: `bun run test:runtime:compat`
|
||||||
- Stats dashboard UI: `bun run test:stats`
|
- Stats dashboard UI: `bun run test:stats`
|
||||||
- Build/release scripts (`scripts/**`): `bun run test:scripts`
|
- Build/release scripts (`scripts/**`): `bun run test:scripts`
|
||||||
|
- Packaging: build the platform package, then run `bun run test:package <resources-directory>`.
|
||||||
|
On headless Linux: `xvfb-run -a bun run test:package release/linux-unpacked/resources`.
|
||||||
|
Content checks and informational size reporting run inside electron-builder hooks. See the
|
||||||
|
[release guide](../RELEASING.md#package-contents-and-size-checks) for size reports
|
||||||
|
and the installed-app verification checklist.
|
||||||
- Coverage for the maintained source lane: `bun run test:coverage:src`
|
- Coverage for the maintained source lane: `bun run test:coverage:src`
|
||||||
- Deep/local full gate: default handoff gate above
|
- Deep/local full gate: default handoff gate above
|
||||||
|
|
||||||
|
|||||||
+40
-43
@@ -80,7 +80,8 @@
|
|||||||
"build:mac:unsigned": "bun run build && env -u APPLE_ID -u APPLE_APP_SPECIFIC_PASSWORD -u APPLE_TEAM_ID -u CSC_LINK -u CSC_KEY_PASSWORD CSC_IDENTITY_AUTO_DISCOVERY=false electron-builder --mac dmg zip --publish never",
|
"build:mac:unsigned": "bun run build && env -u APPLE_ID -u APPLE_APP_SPECIFIC_PASSWORD -u APPLE_TEAM_ID -u CSC_LINK -u CSC_KEY_PASSWORD CSC_IDENTITY_AUTO_DISCOVERY=false electron-builder --mac dmg zip --publish never",
|
||||||
"build:mac:zip": "bun run build && electron-builder --mac zip --publish never",
|
"build:mac:zip": "bun run build && electron-builder --mac zip --publish never",
|
||||||
"build:win": "bun run build && electron-builder --win nsis zip --publish never",
|
"build:win": "bun run build && electron-builder --win nsis zip --publish never",
|
||||||
"build:win:unsigned": "bun run build && node scripts/build-win-unsigned.mjs"
|
"build:win:unsigned": "bun run build && node scripts/build-win-unsigned.mjs",
|
||||||
|
"test:package": "bun scripts/run-package-smoke.mjs"
|
||||||
},
|
},
|
||||||
"overrides": {
|
"overrides": {
|
||||||
"@xmldom/xmldom": "0.8.15",
|
"@xmldom/xmldom": "0.8.15",
|
||||||
@@ -124,15 +125,16 @@
|
|||||||
"ws": "^8.21.0"
|
"ws": "^8.21.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
"@electron/asar": "3.4.1",
|
||||||
"@types/node": "^24.10.0",
|
"@types/node": "^24.10.0",
|
||||||
"@types/ws": "^8.18.1",
|
"@types/ws": "^8.18.1",
|
||||||
"electron": "42.6.0",
|
"electron": "42.6.0",
|
||||||
"electron-builder": "26.15.3",
|
"electron-builder": "26.15.3",
|
||||||
"undici": "7.29.0",
|
|
||||||
"esbuild": "^0.25.12",
|
"esbuild": "^0.25.12",
|
||||||
"eslint": "^10.8.0",
|
"eslint": "^10.8.0",
|
||||||
"prettier": "^3.8.1",
|
"prettier": "^3.8.1",
|
||||||
"typescript": "^5.9.3"
|
"typescript": "^5.9.3",
|
||||||
|
"undici": "7.29.0"
|
||||||
},
|
},
|
||||||
"build": {
|
"build": {
|
||||||
"appId": "com.sudacode.SubMiner",
|
"appId": "com.sudacode.SubMiner",
|
||||||
@@ -159,6 +161,10 @@
|
|||||||
"category": "AudioVideo",
|
"category": "AudioVideo",
|
||||||
"executableArgs": [
|
"executableArgs": [
|
||||||
"--background"
|
"--background"
|
||||||
|
],
|
||||||
|
"files": [
|
||||||
|
"package.json",
|
||||||
|
"!node_modules/koffi{,/**/*}"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"mac": {
|
"mac": {
|
||||||
@@ -177,6 +183,10 @@
|
|||||||
"from": "dist/scripts/get-mpv-window-macos",
|
"from": "dist/scripts/get-mpv-window-macos",
|
||||||
"to": "scripts/get-mpv-window-macos"
|
"to": "scripts/get-mpv-window-macos"
|
||||||
}
|
}
|
||||||
|
],
|
||||||
|
"files": [
|
||||||
|
"package.json",
|
||||||
|
"!node_modules/koffi{,/**/*}"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"dmg": {
|
"dmg": {
|
||||||
@@ -188,7 +198,11 @@
|
|||||||
"nsis",
|
"nsis",
|
||||||
"zip"
|
"zip"
|
||||||
],
|
],
|
||||||
"icon": "assets/SubMiner.ico"
|
"icon": "assets/SubMiner.ico",
|
||||||
|
"files": [
|
||||||
|
"package.json",
|
||||||
|
"!node_modules/koffi/build/koffi/!(win32_${arch}){,/**/*}"
|
||||||
|
]
|
||||||
},
|
},
|
||||||
"nsis": {
|
"nsis": {
|
||||||
"artifactName": "SubMiner-${version}.${ext}",
|
"artifactName": "SubMiner-${version}.${ext}",
|
||||||
@@ -198,43 +212,19 @@
|
|||||||
"include": "build/installer.nsh"
|
"include": "build/installer.nsh"
|
||||||
},
|
},
|
||||||
"files": [
|
"files": [
|
||||||
"**/*",
|
"dist/**/*",
|
||||||
"!assets{,/**/*}",
|
"stats/dist/**/*",
|
||||||
"!src{,/**/*}",
|
"vendor/texthooker-ui/docs/**/*",
|
||||||
"!launcher{,/**/*}",
|
"config.example.jsonc",
|
||||||
"!docs{,/**/*}",
|
"LICENSE",
|
||||||
"!tests{,/**/*}",
|
"!**/*.map",
|
||||||
"!packaging{,/**/*}",
|
"!**/*.{ts,tsx,mts,cts}",
|
||||||
"!README.md",
|
"!**/*.{test,spec}.*",
|
||||||
"!CHANGELOG.md",
|
"!**/{test,tests,__tests__,fixture,fixtures,__fixtures__}{,/**/*}",
|
||||||
"!AGENTS.md",
|
"!dist/launcher{,/**/*}",
|
||||||
"!CLAUDE.md",
|
"!dist/scripts{,/**/*}",
|
||||||
"!stats/src{,/**/*}",
|
"!dist/{renderer,settings,syncui}/fonts{,/**/*}",
|
||||||
"!stats/index.html",
|
"!node_modules/koffi/{src,vendor,doc}{,/**/*}",
|
||||||
"!stats/public{,/**/*}",
|
|
||||||
"!stats/package.json",
|
|
||||||
"!stats/tsconfig.json",
|
|
||||||
"!stats/vite.config.ts",
|
|
||||||
"!docs-site{,/**/*}",
|
|
||||||
"!changes{,/**/*}",
|
|
||||||
"!.tmp{,/**/*}",
|
|
||||||
"!release-*{,/**/*}",
|
|
||||||
"!dist/**/*.map",
|
|
||||||
"!dist/**/*.test.*",
|
|
||||||
"!dist/**/__tests__{,/**/*}",
|
|
||||||
"!scripts/**/*.test.*",
|
|
||||||
"!plugin{,/**/*}",
|
|
||||||
"!vendor/subminer-yomitan{,/**/*}",
|
|
||||||
"!vendor/yomitan-jlpt-vocab{,/**/*}",
|
|
||||||
"!vendor/texthooker-ui/src{,/**/*}",
|
|
||||||
"!vendor/texthooker-ui/node_modules{,/**/*}",
|
|
||||||
"!vendor/texthooker-ui/.svelte-kit{,/**/*}",
|
|
||||||
"!vendor/texthooker-ui/.vscode{,/**/*}",
|
|
||||||
"!vendor/texthooker-ui/public{,/**/*}",
|
|
||||||
"!vendor/texthooker-ui/README.md",
|
|
||||||
"!vendor/texthooker-ui/package.json",
|
|
||||||
"!vendor/texthooker-ui/package-lock.json",
|
|
||||||
"!vendor/texthooker-ui/tsconfig*.json",
|
|
||||||
"!node_modules/@libsql/linux-x64-musl{,/**/*}"
|
"!node_modules/@libsql/linux-x64-musl{,/**/*}"
|
||||||
],
|
],
|
||||||
"extraResources": [
|
"extraResources": [
|
||||||
@@ -248,7 +238,13 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"from": "assets",
|
"from": "assets",
|
||||||
"to": "assets"
|
"to": "assets",
|
||||||
|
"filter": [
|
||||||
|
"SubMiner*.png",
|
||||||
|
"SubMiner.ico",
|
||||||
|
"themes/**/*",
|
||||||
|
"thumbnailers/**/*"
|
||||||
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"from": "plugin/subminer",
|
"from": "plugin/subminer",
|
||||||
@@ -273,7 +269,8 @@
|
|||||||
"from": "CHANGELOG.md",
|
"from": "CHANGELOG.md",
|
||||||
"to": "CHANGELOG.md"
|
"to": "CHANGELOG.md"
|
||||||
}
|
}
|
||||||
]
|
],
|
||||||
|
"afterAllArtifactBuild": "scripts/package-audit.cjs"
|
||||||
},
|
},
|
||||||
"patchedDependencies": {
|
"patchedDependencies": {
|
||||||
"@discordjs/rest@2.6.1": "patches/@discordjs%2Frest@2.6.1.patch"
|
"@discordjs/rest@2.6.1": "patches/@discordjs%2Frest@2.6.1.patch"
|
||||||
|
|||||||
@@ -105,6 +105,7 @@ async function afterPack(context, deps = {}) {
|
|||||||
await stageLinuxAppImageSharedLibrary(context);
|
await stageLinuxAppImageSharedLibrary(context);
|
||||||
await verifyMacOSWindowHelper(context);
|
await verifyMacOSWindowHelper(context);
|
||||||
await stageBundledBunRuntime(context, deps);
|
await stageBundledBunRuntime(context, deps);
|
||||||
|
await (deps.auditPackage ?? require('./package-audit.cjs').auditPackage)(context);
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ const {
|
|||||||
packager?: { appInfo?: { productFilename?: string } };
|
packager?: { appInfo?: { productFilename?: string } };
|
||||||
},
|
},
|
||||||
deps?: {
|
deps?: {
|
||||||
|
auditPackage?: (context: { appOutDir: string }) => Promise<void>;
|
||||||
stageBunRuntime?: (options: {
|
stageBunRuntime?: (options: {
|
||||||
appOutDir: string;
|
appOutDir: string;
|
||||||
platform: string;
|
platform: string;
|
||||||
@@ -172,11 +173,12 @@ test('afterPack propagates Linux staging failures', async () => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
test('afterPack preserves Linux staging and forwards the electron-builder target to Bun staging', async () => {
|
test('afterPack stages Linux and Bun runtime assets before auditing the package', async () => {
|
||||||
const workspace = createWorkspace('subminer-after-pack-target');
|
const workspace = createWorkspace('subminer-after-pack-target');
|
||||||
const appOutDir = path.join(workspace, 'SubMiner-linux-arm64');
|
const appOutDir = path.join(workspace, 'SubMiner-linux-arm64');
|
||||||
const sourceLibraryPath = path.join(appOutDir, LINUX_FFMPEG_LIBRARY);
|
const sourceLibraryPath = path.join(appOutDir, LINUX_FFMPEG_LIBRARY);
|
||||||
const targetLibraryPath = path.join(appOutDir, 'usr', 'lib', LINUX_FFMPEG_LIBRARY);
|
const targetLibraryPath = path.join(appOutDir, 'usr', 'lib', LINUX_FFMPEG_LIBRARY);
|
||||||
|
const operations: string[] = [];
|
||||||
let stagedOptions:
|
let stagedOptions:
|
||||||
| {
|
| {
|
||||||
appOutDir: string;
|
appOutDir: string;
|
||||||
@@ -200,10 +202,17 @@ test('afterPack preserves Linux staging and forwards the electron-builder target
|
|||||||
{
|
{
|
||||||
stageBunRuntime: async (options) => {
|
stageBunRuntime: async (options) => {
|
||||||
stagedOptions = options;
|
stagedOptions = options;
|
||||||
|
operations.push('stage-bun');
|
||||||
|
},
|
||||||
|
auditPackage: async (context) => {
|
||||||
|
assert.equal(context.appOutDir, appOutDir);
|
||||||
|
assert.equal(fs.readFileSync(targetLibraryPath, 'utf8'), 'bundled ffmpeg');
|
||||||
|
operations.push('audit');
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
|
assert.deepEqual(operations, ['stage-bun', 'audit']);
|
||||||
assert.deepEqual(stagedOptions, {
|
assert.deepEqual(stagedOptions, {
|
||||||
appOutDir,
|
appOutDir,
|
||||||
platform: 'linux',
|
platform: 'linux',
|
||||||
|
|||||||
@@ -0,0 +1,243 @@
|
|||||||
|
const fs = require('node:fs');
|
||||||
|
const path = require('node:path');
|
||||||
|
const assert = require('node:assert/strict');
|
||||||
|
const asar = require('@electron/asar');
|
||||||
|
const { Arch } = require('builder-util');
|
||||||
|
|
||||||
|
const MIB = 1024 * 1024;
|
||||||
|
const currentReports = new Set();
|
||||||
|
const REQUIRED_APP_FILES = [
|
||||||
|
'package.json',
|
||||||
|
'LICENSE',
|
||||||
|
'config.example.jsonc',
|
||||||
|
'dist/main-entry.js',
|
||||||
|
'dist/main.js',
|
||||||
|
'dist/preload.js',
|
||||||
|
'dist/preload-settings.js',
|
||||||
|
'dist/preload-syncui.js',
|
||||||
|
'dist/preload-stats.js',
|
||||||
|
'dist/preload-jellyfin-setup.js',
|
||||||
|
'dist/fonts/MPLUS1[wght].ttf',
|
||||||
|
'stats/dist/index.html',
|
||||||
|
'vendor/texthooker-ui/docs/index.html',
|
||||||
|
...['renderer', 'settings', 'syncui'].flatMap((ui) => [
|
||||||
|
`dist/${ui}/index.html`,
|
||||||
|
`dist/${ui}/style.css`,
|
||||||
|
`dist/${ui}/${ui}.js`,
|
||||||
|
]),
|
||||||
|
];
|
||||||
|
const REQUIRED_RESOURCES = [
|
||||||
|
'yomitan/manifest.json',
|
||||||
|
'yomitan/data/fonts/kanji-stroke-orders.ttf',
|
||||||
|
'yomitan/fonts/NotoSansJP-Regular.ttf',
|
||||||
|
'yomitan/lib/resvg.wasm',
|
||||||
|
'launcher/subminer',
|
||||||
|
'plugin/subminer/main.lua',
|
||||||
|
'plugin/subminer.conf',
|
||||||
|
'assets/SubMiner.png',
|
||||||
|
'assets/SubMiner-square.png',
|
||||||
|
'assets/themes/subminer.rasi',
|
||||||
|
'assets/thumbnailers/subminer-ffmpegthumbnailer.thumbnailer',
|
||||||
|
'CHANGELOG.md',
|
||||||
|
];
|
||||||
|
|
||||||
|
// Do not follow framework symlinks or count ASAR unpacked entries twice.
|
||||||
|
function listFiles(root, prefix = '') {
|
||||||
|
return fs.readdirSync(path.join(root, prefix), { withFileTypes: true }).flatMap((entry) => {
|
||||||
|
const name = prefix ? `${prefix}/${entry.name}` : entry.name;
|
||||||
|
if (entry.isSymbolicLink()) return [];
|
||||||
|
if (entry.isDirectory()) return listFiles(root, name);
|
||||||
|
return [{ path: name, bytes: fs.statSync(path.join(root, name)).size }];
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function listAppFiles(archive) {
|
||||||
|
return asar.listPackage(archive).flatMap((entry) => {
|
||||||
|
const name = entry.replaceAll('\\', '/').replace(/^\//, '');
|
||||||
|
const stat = asar.statFile(archive, name);
|
||||||
|
return 'size' in stat ? [{ path: name, bytes: stat.size }] : [];
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function verifyAppPath(name, platform, arch) {
|
||||||
|
const allowedRoots = new Set([
|
||||||
|
'dist',
|
||||||
|
'node_modules',
|
||||||
|
'stats',
|
||||||
|
'vendor',
|
||||||
|
'package.json',
|
||||||
|
'LICENSE',
|
||||||
|
'config.example.jsonc',
|
||||||
|
]);
|
||||||
|
assert(allowedRoots.has(name.split('/')[0]), `Unexpected app file: ${name}`);
|
||||||
|
assert(!name.endsWith('.map'), `Packaged source map: ${name}`);
|
||||||
|
assert(!/\.(?:[cm]?ts|tsx)$/.test(name), `Packaged TypeScript: ${name}`);
|
||||||
|
assert(!/\.(?:test|spec)\./.test(name), `Packaged test: ${name}`);
|
||||||
|
assert(
|
||||||
|
!/(?:^|\/)(?:tests?|__tests__|fixtures?|__fixtures__)\//.test(name),
|
||||||
|
`Packaged test or fixture directory: ${name}`,
|
||||||
|
);
|
||||||
|
assert(!/^dist\/.*\.test\./.test(name), `Packaged test: ${name}`);
|
||||||
|
assert(!/^dist\/(launcher|scripts)\//.test(name), `Duplicate helper: ${name}`);
|
||||||
|
assert(!/^dist\/(renderer|settings|syncui)\/fonts\//.test(name), `Duplicate font: ${name}`);
|
||||||
|
assert(!name.startsWith('stats/') || name.startsWith('stats/dist/'), `Stats source: ${name}`);
|
||||||
|
assert(
|
||||||
|
!name.startsWith('vendor/') || name.startsWith('vendor/texthooker-ui/docs/'),
|
||||||
|
`Vendor source: ${name}`,
|
||||||
|
);
|
||||||
|
if (name.startsWith('node_modules/koffi/')) {
|
||||||
|
assert.equal(platform, 'win32', `Koffi shipped on ${platform}`);
|
||||||
|
assert(!/^node_modules\/koffi\/(src|vendor|doc)\//.test(name), `Koffi build files: ${name}`);
|
||||||
|
if (name.endsWith('.node')) {
|
||||||
|
assert.equal(name, `node_modules/koffi/build/koffi/win32_${arch}/koffi.node`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function verifyContents(archive, resources, platform, arch) {
|
||||||
|
const entries = listAppFiles(archive);
|
||||||
|
const names = new Set(entries.map((entry) => entry.path));
|
||||||
|
for (const name of REQUIRED_APP_FILES) assert(names.has(name), `Missing app file: ${name}`);
|
||||||
|
for (const name of REQUIRED_RESOURCES) {
|
||||||
|
assert(fs.statSync(path.join(resources, name)).size > 0, `Empty resource: ${name}`);
|
||||||
|
}
|
||||||
|
assert(listFiles(path.join(resources, 'yomitan-jlpt-vocab')).length > 0, 'Missing JLPT data');
|
||||||
|
for (const { path: name } of entries) verifyAppPath(name, platform, arch);
|
||||||
|
const libsqlPlatform = {
|
||||||
|
linux: `linux-${arch}-gnu`,
|
||||||
|
darwin: `darwin-${arch}`,
|
||||||
|
win32: `win32-${arch}-msvc`,
|
||||||
|
}[platform];
|
||||||
|
const libsqlBinary = `node_modules/@libsql/${libsqlPlatform}/index.node`;
|
||||||
|
assert(names.has(libsqlBinary), `Missing SQLite native binary: ${libsqlBinary}`);
|
||||||
|
for (const name of names) {
|
||||||
|
if (name.startsWith('node_modules/@libsql/') && name.endsWith('.node')) {
|
||||||
|
assert.equal(name, libsqlBinary, `Foreign SQLite binary: ${name}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (platform === 'win32') {
|
||||||
|
for (const name of [
|
||||||
|
'index.js',
|
||||||
|
'package.json',
|
||||||
|
'LICENSE.txt',
|
||||||
|
`build/koffi/win32_${arch}/koffi.node`,
|
||||||
|
]) {
|
||||||
|
assert(names.has(`node_modules/koffi/${name}`), `Missing Windows FFI file: ${name}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (const name of listFiles(path.join(resources, 'assets'))) {
|
||||||
|
assert(!name.path.startsWith('minecard'), `Demo media shipped: ${name.path}`);
|
||||||
|
}
|
||||||
|
for (const ui of ['renderer', 'settings', 'syncui']) {
|
||||||
|
const css = asar.extractFile(archive, `dist/${ui}/style.css`).toString();
|
||||||
|
assert(css.includes('../fonts/MPLUS1[wght].ttf'), `Shared font missing from ${ui} CSS`);
|
||||||
|
}
|
||||||
|
return entries;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function auditPackage(context) {
|
||||||
|
const platform = context.electronPlatformName;
|
||||||
|
const arch = Arch[context.arch];
|
||||||
|
const key = `${platform}-${arch}`;
|
||||||
|
const appRoot =
|
||||||
|
platform === 'darwin'
|
||||||
|
? path.join(context.appOutDir, `${context.packager.appInfo.productFilename}.app`)
|
||||||
|
: context.appOutDir;
|
||||||
|
const resources = path.join(appRoot, platform === 'darwin' ? 'Contents/Resources' : 'resources');
|
||||||
|
const appFiles = verifyContents(path.join(resources, 'app.asar'), resources, platform, arch);
|
||||||
|
const files = listFiles(appRoot);
|
||||||
|
const unpackedBytes = files.reduce((sum, entry) => sum + entry.bytes, 0);
|
||||||
|
const report = {
|
||||||
|
version: context.packager.appInfo.version,
|
||||||
|
platform,
|
||||||
|
arch,
|
||||||
|
unpackedBytes,
|
||||||
|
appDirectory: path.relative(context.outDir, appRoot),
|
||||||
|
largestFiles: [...files].sort((a, b) => b.bytes - a.bytes).slice(0, 25),
|
||||||
|
largestAppFiles: [...appFiles].sort((a, b) => b.bytes - a.bytes).slice(0, 25),
|
||||||
|
nativeBinaries: files.filter((entry) => /\.(node|dll|dylib)$|\.so(?:\.|$)/.test(entry.path)),
|
||||||
|
artifacts: [],
|
||||||
|
};
|
||||||
|
const output = path.join(context.outDir, `package-size-${key}.json`);
|
||||||
|
fs.mkdirSync(path.dirname(output), { recursive: true });
|
||||||
|
fs.writeFileSync(output, `${JSON.stringify(report, null, 2)}\n`);
|
||||||
|
currentReports.add(output);
|
||||||
|
console.log(
|
||||||
|
`Package contents verified: ${key}, ${(unpackedBytes / MIB).toFixed(2)} MiB unpacked`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function artifactKind(name) {
|
||||||
|
if (name.endsWith('-mac.zip')) return 'mac.zip';
|
||||||
|
if (name.endsWith('-win.zip')) return 'win.zip';
|
||||||
|
const extension = path.extname(name).slice(1);
|
||||||
|
return ['AppImage', 'dmg', 'exe'].includes(extension) ? extension : undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
function compareSizes(report, previous) {
|
||||||
|
assert.equal(previous.platform, report.platform);
|
||||||
|
assert.equal(previous.arch, report.arch);
|
||||||
|
assert(Number.isFinite(previous.unpackedBytes), 'Invalid previous size report');
|
||||||
|
const previousArtifacts = Array.isArray(previous.artifacts) ? previous.artifacts : [];
|
||||||
|
return {
|
||||||
|
version: previous.version,
|
||||||
|
unpackedDeltaBytes: report.unpackedBytes - previous.unpackedBytes,
|
||||||
|
artifacts: report.artifacts.flatMap((artifact) => {
|
||||||
|
const old = previousArtifacts.find(
|
||||||
|
(entry) => entry && entry.kind === artifact.kind && Number.isFinite(entry.bytes),
|
||||||
|
);
|
||||||
|
return old ? [{ kind: artifact.kind, deltaBytes: artifact.bytes - old.bytes }] : [];
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Runs after signing and installer creation, before release upload.
|
||||||
|
async function afterAllArtifactBuild(result) {
|
||||||
|
const reports = [];
|
||||||
|
for (const reportPath of currentReports) {
|
||||||
|
const filename = path.basename(reportPath);
|
||||||
|
const report = JSON.parse(fs.readFileSync(reportPath, 'utf8'));
|
||||||
|
const key = `${report.platform}-${report.arch}`;
|
||||||
|
const files = listFiles(path.join(result.outDir, report.appDirectory));
|
||||||
|
report.unpackedBytes = files.reduce((sum, entry) => sum + entry.bytes, 0);
|
||||||
|
report.largestFiles = [...files].sort((a, b) => b.bytes - a.bytes).slice(0, 25);
|
||||||
|
report.artifacts = result.artifactPaths.flatMap((file) => {
|
||||||
|
const kind = artifactKind(file);
|
||||||
|
if (!kind) return [];
|
||||||
|
const bytes = fs.statSync(file).size;
|
||||||
|
return [{ name: path.basename(file), kind, bytes }];
|
||||||
|
});
|
||||||
|
const previousPath = path.join(result.outDir, '..', '.tmp', 'package-baseline', filename);
|
||||||
|
if (fs.existsSync(previousPath)) {
|
||||||
|
const previous = JSON.parse(fs.readFileSync(previousPath, 'utf8'));
|
||||||
|
report.comparison = compareSizes(report, previous);
|
||||||
|
}
|
||||||
|
fs.writeFileSync(reportPath, `${JSON.stringify(report, null, 2)}\n`);
|
||||||
|
const summary = [
|
||||||
|
`### Package size: ${key}`,
|
||||||
|
'',
|
||||||
|
`Unpacked: ${(report.unpackedBytes / MIB).toFixed(2)} MiB`,
|
||||||
|
...report.artifacts.map((entry) => `${entry.name}: ${(entry.bytes / MIB).toFixed(2)} MiB`),
|
||||||
|
report.comparison
|
||||||
|
? `Change from ${report.comparison.version}: ${(report.comparison.unpackedDeltaBytes / MIB).toFixed(2)} MiB unpacked`
|
||||||
|
: 'No previous size report available.',
|
||||||
|
'',
|
||||||
|
].join('\n');
|
||||||
|
console.log(summary);
|
||||||
|
if (process.env.GITHUB_STEP_SUMMARY)
|
||||||
|
fs.appendFileSync(process.env.GITHUB_STEP_SUMMARY, summary);
|
||||||
|
reports.push(reportPath);
|
||||||
|
}
|
||||||
|
assert(reports.length > 0, 'No package size reports generated by afterPack');
|
||||||
|
return reports;
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
auditPackage,
|
||||||
|
verifyContents,
|
||||||
|
verifyAppPath,
|
||||||
|
listFiles,
|
||||||
|
listAppFiles,
|
||||||
|
compareSizes,
|
||||||
|
default: afterAllArtifactBuild,
|
||||||
|
};
|
||||||
@@ -0,0 +1,178 @@
|
|||||||
|
import assert from 'node:assert/strict';
|
||||||
|
import { mkdtempSync, mkdirSync, writeFileSync, statSync, rmSync, createReadStream } from 'node:fs';
|
||||||
|
import { tmpdir } from 'node:os';
|
||||||
|
import path from 'node:path';
|
||||||
|
import test from 'node:test';
|
||||||
|
import { createPackageFromStreams } from '@electron/asar';
|
||||||
|
import { FileMatcher, getFileMatchers } from 'app-builder-lib/out/fileMatcher';
|
||||||
|
import config from '../package.json';
|
||||||
|
import { listAppFiles, listFiles, compareSizes, verifyAppPath } from './package-audit.cjs';
|
||||||
|
|
||||||
|
test('platform packaging preserves the runtime allowlist after builder normalizes global filters', () => {
|
||||||
|
const root = process.cwd();
|
||||||
|
const fileStat = statSync('package.json');
|
||||||
|
for (const platform of ['linux', 'mac', 'win'] as const) {
|
||||||
|
const matchers = getFileMatchers(
|
||||||
|
{ files: [{ filter: config.build.files }] },
|
||||||
|
'files',
|
||||||
|
'/tmp/subminer-filter-output',
|
||||||
|
{
|
||||||
|
defaultSrc: root,
|
||||||
|
globalOutDir: path.join(root, 'release'),
|
||||||
|
customBuildOptions: { files: config.build[platform].files },
|
||||||
|
macroExpander: (value) => value.replaceAll('${arch}', 'x64'),
|
||||||
|
},
|
||||||
|
);
|
||||||
|
assert(matchers);
|
||||||
|
// This is builder's default for an exclusion-only platform matcher.
|
||||||
|
for (const matcher of matchers) {
|
||||||
|
if (matcher.containsOnlyIgnore()) matcher.prependPattern('**/*');
|
||||||
|
}
|
||||||
|
const included = (name: string) =>
|
||||||
|
matchers.some((matcher) => matcher.createFilter()(path.join(root, name), fileStat));
|
||||||
|
for (const name of [
|
||||||
|
'dist/main-entry.js',
|
||||||
|
'dist/fonts/MPLUS1[wght].ttf',
|
||||||
|
'stats/dist/index.html',
|
||||||
|
'vendor/texthooker-ui/docs/index.html',
|
||||||
|
'package.json',
|
||||||
|
]) {
|
||||||
|
assert(included(name), `${platform} must ship ${name}`);
|
||||||
|
}
|
||||||
|
for (const name of [
|
||||||
|
'.agents/skills/test.md',
|
||||||
|
'src/main.ts',
|
||||||
|
'scripts/build-yomitan.mjs',
|
||||||
|
'docs-site/index.md',
|
||||||
|
'dist/main.js.map',
|
||||||
|
'dist/main.test.js',
|
||||||
|
'dist/nested/source.ts',
|
||||||
|
'dist/nested/__tests__/helper.js',
|
||||||
|
'stats/dist/nested/fixtures/data.json',
|
||||||
|
'vendor/texthooker-ui/docs/nested/component.tsx',
|
||||||
|
'dist/launcher/subminer',
|
||||||
|
'dist/settings/fonts/MPLUS1[wght].ttf',
|
||||||
|
'vendor/subminer-yomitan/ext/manifest.json',
|
||||||
|
]) {
|
||||||
|
assert(!included(name), `${platform} must exclude ${name}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
test('dependency filters keep only the target Windows Koffi binary', () => {
|
||||||
|
const root = process.cwd();
|
||||||
|
for (const arch of ['x64', 'arm64']) {
|
||||||
|
for (const platform of ['linux', 'mac', 'win'] as const) {
|
||||||
|
const patterns = [
|
||||||
|
'**/*',
|
||||||
|
...config.build.files.filter((name) => name.startsWith('!')),
|
||||||
|
...config.build[platform].files.filter((name) => name.startsWith('!')),
|
||||||
|
];
|
||||||
|
const filter = new FileMatcher(
|
||||||
|
root,
|
||||||
|
'/tmp/subminer-filter-output',
|
||||||
|
(value) => value.replaceAll('${arch}', arch),
|
||||||
|
patterns,
|
||||||
|
).createFilter();
|
||||||
|
const included = (name: string) =>
|
||||||
|
filter(path.join(root, 'node_modules', name), statSync('package.json'));
|
||||||
|
assert(included('@libsql/win32-x64-msvc/index.node'));
|
||||||
|
assert(!included('axios/dist/axios.js.map'));
|
||||||
|
assert(!included('koffi/src/koffi/src/ffi.c'));
|
||||||
|
assert(!included('agent-base/src/index.ts'));
|
||||||
|
assert(!included('@discordjs/rest/dist/index.d.mts'));
|
||||||
|
assert(!included('example/lib/tests/helper.js'));
|
||||||
|
for (const target of [
|
||||||
|
'win32_x64',
|
||||||
|
'win32_arm64',
|
||||||
|
'linux_x64',
|
||||||
|
'darwin_arm64',
|
||||||
|
'openbsd_x64',
|
||||||
|
]) {
|
||||||
|
assert.equal(
|
||||||
|
included(`koffi/build/koffi/${target}/koffi.node`),
|
||||||
|
platform === 'win' && target === `win32_${arch}`,
|
||||||
|
`${platform}/${arch}: ${target}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
assert.equal(included('koffi/index.js'), platform === 'win');
|
||||||
|
assert.equal(included('koffi/LICENSE.txt'), platform === 'win');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
test('content audit rejects development files beneath approved roots', () => {
|
||||||
|
for (const root of ['dist', 'stats/dist', 'vendor/texthooker-ui/docs', 'node_modules/example']) {
|
||||||
|
for (const suffix of [
|
||||||
|
'nested/source.ts',
|
||||||
|
'nested/component.tsx',
|
||||||
|
'nested/types.d.mts',
|
||||||
|
'nested/source.cts',
|
||||||
|
'nested/__tests__/helper.js',
|
||||||
|
'nested/tests/helper.js',
|
||||||
|
'nested/test/helper.js',
|
||||||
|
'nested/__fixtures__/data.json',
|
||||||
|
'nested/fixtures/data.json',
|
||||||
|
'nested/fixture/data.json',
|
||||||
|
'nested/component.spec.js',
|
||||||
|
'nested/component.test.cjs',
|
||||||
|
]) {
|
||||||
|
assert.throws(() => verifyAppPath(`${root}/${suffix}`, 'linux', 'x64'), /Packaged/);
|
||||||
|
}
|
||||||
|
for (const suffix of ['nested/runtime.js', 'nested/style.css', 'nested/data.json']) {
|
||||||
|
assert.doesNotThrow(() => verifyAppPath(`${root}/${suffix}`, 'linux', 'x64'));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
test('archive inventory handles native files without counting them twice on disk', async () => {
|
||||||
|
const root = mkdtempSync(path.join(tmpdir(), 'subminer-audit-'));
|
||||||
|
try {
|
||||||
|
const input = path.join(root, 'input');
|
||||||
|
const output = path.join(root, 'output');
|
||||||
|
mkdirSync(input);
|
||||||
|
mkdirSync(output);
|
||||||
|
writeFileSync(path.join(input, 'main.js'), 'hello');
|
||||||
|
writeFileSync(path.join(input, 'native.node'), 'native');
|
||||||
|
const archive = path.join(output, 'app.asar');
|
||||||
|
await createPackageFromStreams(
|
||||||
|
archive,
|
||||||
|
['main.js', 'native.node'].map((name) => ({
|
||||||
|
path: name,
|
||||||
|
type: 'file',
|
||||||
|
unpacked: name.endsWith('.node'),
|
||||||
|
stat: statSync(path.join(input, name)),
|
||||||
|
streamGenerator: () => createReadStream(path.join(input, name)),
|
||||||
|
})),
|
||||||
|
);
|
||||||
|
assert.deepEqual(listAppFiles(archive), [
|
||||||
|
{ path: 'main.js', bytes: 5 },
|
||||||
|
{ path: 'native.node', bytes: 6 },
|
||||||
|
]);
|
||||||
|
assert.equal(
|
||||||
|
listFiles(output).reduce((sum: number, entry: { bytes: number }) => sum + entry.bytes, 0),
|
||||||
|
statSync(archive).size + 6,
|
||||||
|
);
|
||||||
|
} finally {
|
||||||
|
rmSync(root, { recursive: true, force: true });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
test('size comparison tolerates older reports without artifact measurements', () => {
|
||||||
|
const previous = { version: '0.19.6', platform: 'linux', arch: 'x64', unpackedBytes: 100 };
|
||||||
|
const current = { ...previous, unpackedBytes: 80, artifacts: [{ kind: 'AppImage', bytes: 40 }] };
|
||||||
|
assert.deepEqual(compareSizes(current, previous), {
|
||||||
|
version: '0.19.6',
|
||||||
|
unpackedDeltaBytes: -20,
|
||||||
|
artifacts: [],
|
||||||
|
});
|
||||||
|
assert.deepEqual(
|
||||||
|
compareSizes(current, { ...previous, artifacts: [null, { kind: 'AppImage', bytes: 50 }] })
|
||||||
|
.artifacts,
|
||||||
|
[{ kind: 'AppImage', deltaBytes: -10 }],
|
||||||
|
);
|
||||||
|
assert.throws(
|
||||||
|
() => compareSizes(current, { ...previous, unpackedBytes: 'unknown' }),
|
||||||
|
/Invalid previous size report/,
|
||||||
|
);
|
||||||
|
});
|
||||||
@@ -29,10 +29,6 @@ function copyFile(sourcePath, outputPath) {
|
|||||||
function copyAssets(sourceDir, outputDir, label) {
|
function copyAssets(sourceDir, outputDir, label) {
|
||||||
copyFile(path.join(sourceDir, 'index.html'), path.join(outputDir, 'index.html'));
|
copyFile(path.join(sourceDir, 'index.html'), path.join(outputDir, 'index.html'));
|
||||||
copyFile(path.join(sourceDir, 'style.css'), path.join(outputDir, 'style.css'));
|
copyFile(path.join(sourceDir, 'style.css'), path.join(outputDir, 'style.css'));
|
||||||
fs.cpSync(path.join(rendererSourceDir, 'fonts'), path.join(outputDir, 'fonts'), {
|
|
||||||
recursive: true,
|
|
||||||
force: true,
|
|
||||||
});
|
|
||||||
process.stdout.write(`Staged ${label} assets in ${outputDir}\n`);
|
process.stdout.write(`Staged ${label} assets in ${outputDir}\n`);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -102,6 +98,10 @@ function buildMacosHelper() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function main() {
|
function main() {
|
||||||
|
fs.cpSync(path.join(rendererSourceDir, 'fonts'), path.join(repoRoot, 'dist', 'fonts'), {
|
||||||
|
recursive: true,
|
||||||
|
force: true,
|
||||||
|
});
|
||||||
copyRendererAssets();
|
copyRendererAssets();
|
||||||
copySettingsAssets();
|
copySettingsAssets();
|
||||||
copySyncUiAssets();
|
copySyncUiAssets();
|
||||||
|
|||||||
@@ -0,0 +1,24 @@
|
|||||||
|
import fs from 'node:fs';
|
||||||
|
import os from 'node:os';
|
||||||
|
import path from 'node:path';
|
||||||
|
import { spawnSync } from 'node:child_process';
|
||||||
|
import { createRequire } from 'node:module';
|
||||||
|
import { fileURLToPath } from 'node:url';
|
||||||
|
|
||||||
|
const resources = process.argv[2];
|
||||||
|
if (!resources) throw new Error('Usage: bun run test:package <resources-directory>');
|
||||||
|
const require = createRequire(import.meta.url);
|
||||||
|
const profile = fs.mkdtempSync(path.join(os.tmpdir(), 'subminer-package-smoke-'));
|
||||||
|
const env = { ...process.env, SUBMINER_PACKAGE_SMOKE_DATA: profile };
|
||||||
|
delete env.ELECTRON_RUN_AS_NODE;
|
||||||
|
try {
|
||||||
|
const result = spawnSync(
|
||||||
|
require('electron'),
|
||||||
|
[fileURLToPath(new URL('./smoke-package.cjs', import.meta.url)), path.resolve(resources)],
|
||||||
|
{ env, stdio: 'inherit', timeout: 75_000 },
|
||||||
|
);
|
||||||
|
if (result.error) throw result.error;
|
||||||
|
process.exitCode = result.status ?? 1;
|
||||||
|
} finally {
|
||||||
|
fs.rmSync(profile, { recursive: true, force: true, maxRetries: 3 });
|
||||||
|
}
|
||||||
@@ -0,0 +1,94 @@
|
|||||||
|
// Run with the pinned Electron runtime against a finished app's resources folder.
|
||||||
|
const { app, BrowserWindow, session } = require('electron');
|
||||||
|
const fs = require('node:fs');
|
||||||
|
const path = require('node:path');
|
||||||
|
const { createRequire } = require('node:module');
|
||||||
|
const assert = require('node:assert/strict');
|
||||||
|
const { once } = require('node:events');
|
||||||
|
|
||||||
|
const resources = path.resolve(process.argv[2]);
|
||||||
|
const archive = path.join(resources, 'app.asar');
|
||||||
|
const isolatedData = process.env.SUBMINER_PACKAGE_SMOKE_DATA;
|
||||||
|
assert(
|
||||||
|
isolatedData && fs.existsSync(isolatedData),
|
||||||
|
'Use bun run test:package to create an isolated profile',
|
||||||
|
);
|
||||||
|
app.setPath('userData', isolatedData);
|
||||||
|
app.disableHardwareAcceleration();
|
||||||
|
app.on('window-all-closed', () => {});
|
||||||
|
const timeout = setTimeout(() => {
|
||||||
|
console.error('Package smoke timed out');
|
||||||
|
app.exit(1);
|
||||||
|
}, 60_000);
|
||||||
|
|
||||||
|
async function smoke() {
|
||||||
|
await app.whenReady();
|
||||||
|
const packagedRequire = createRequire(path.join(archive, 'package.json'));
|
||||||
|
const Database = packagedRequire('libsql');
|
||||||
|
const database = new Database(':memory:');
|
||||||
|
assert.equal(database.prepare('select 42 as answer').get().answer, 42);
|
||||||
|
database.close();
|
||||||
|
if (process.platform === 'win32') {
|
||||||
|
const win32 = packagedRequire('./dist/window-trackers/win32.js');
|
||||||
|
assert(Array.isArray(win32.findMpvWindows().matches));
|
||||||
|
}
|
||||||
|
const { Texthooker } = packagedRequire('./dist/core/services/texthooker.js');
|
||||||
|
const texthooker = new Texthooker();
|
||||||
|
const server = texthooker.start(0);
|
||||||
|
assert(server, 'Packaged texthooker assets could not be found');
|
||||||
|
try {
|
||||||
|
await once(server, 'listening');
|
||||||
|
const response = await fetch(`http://127.0.0.1:${server.address().port}/`);
|
||||||
|
assert.equal(response.status, 200);
|
||||||
|
assert((await response.text()).includes('<html'));
|
||||||
|
} finally {
|
||||||
|
texthooker.stop();
|
||||||
|
}
|
||||||
|
const extension = await session.defaultSession.extensions.loadExtension(
|
||||||
|
path.join(resources, 'yomitan'),
|
||||||
|
{ allowFileAccess: true },
|
||||||
|
);
|
||||||
|
assert(extension.id, 'Yomitan extension failed to load');
|
||||||
|
const failedRequests = [];
|
||||||
|
session.defaultSession.webRequest.onErrorOccurred({ urls: ['file://*/*'] }, (details) => {
|
||||||
|
if (details.error !== 'net::ERR_ABORTED')
|
||||||
|
failedRequests.push(`${details.url}: ${details.error}`);
|
||||||
|
});
|
||||||
|
for (const ui of ['renderer', 'settings', 'syncui', 'stats']) {
|
||||||
|
const win = new BrowserWindow({
|
||||||
|
show: false,
|
||||||
|
webPreferences: {
|
||||||
|
sandbox: false,
|
||||||
|
preload: path.join(archive, 'dist', ui === 'renderer' ? 'preload.js' : `preload-${ui}.js`),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
try {
|
||||||
|
await win.loadFile(
|
||||||
|
path.join(archive, ui === 'stats' ? 'stats/dist/index.html' : `dist/${ui}/index.html`),
|
||||||
|
);
|
||||||
|
if (ui !== 'stats') {
|
||||||
|
const loaded = await win.webContents.executeJavaScript(
|
||||||
|
`document.fonts.load('400 16px "M PLUS 1"', '日本語').then(fonts => fonts.length > 0 && fonts.every(font => font.status === 'loaded'))`,
|
||||||
|
);
|
||||||
|
assert(loaded, `${ui}: shared Japanese font failed to load`);
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
win.destroy();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
assert.deepEqual(failedRequests, [], 'Packaged UI resources failed to load');
|
||||||
|
console.log(
|
||||||
|
'Package smoke passed: SQLite, platform FFI, texthooker, Yomitan loading, UI pages, shared Japanese font.',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
smoke()
|
||||||
|
.then(() => {
|
||||||
|
clearTimeout(timeout);
|
||||||
|
app.exit(0);
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
console.error(error);
|
||||||
|
clearTimeout(timeout);
|
||||||
|
app.exit(1);
|
||||||
|
});
|
||||||
@@ -156,6 +156,107 @@ test('buildHyprlandPlacementDispatches does not pin already floating overlay win
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('Hyprland placement keeps a recovery dialog above the input-catching overlay', () => {
|
||||||
|
for (const configProvider of ['hyprlang', 'lua']) {
|
||||||
|
for (const retryBounds of [false, true]) {
|
||||||
|
// Bottom to top, as when Hyprland opens its recovery dialog over playback.
|
||||||
|
const stack = ['0xmpv', '0xoverlay', '0xdialog'];
|
||||||
|
const clients = [
|
||||||
|
{
|
||||||
|
address: '0xoverlay',
|
||||||
|
pid: 456,
|
||||||
|
title: 'SubMiner Overlay',
|
||||||
|
floating: true,
|
||||||
|
workspace: { id: 1 },
|
||||||
|
at: [10, 20],
|
||||||
|
size: [100, 100],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
address: '0xdialog',
|
||||||
|
class: 'hyprland-dialog',
|
||||||
|
mapped: true,
|
||||||
|
hidden: false,
|
||||||
|
workspace: { id: 1 },
|
||||||
|
},
|
||||||
|
];
|
||||||
|
let clientReads = 0;
|
||||||
|
const status = ensureHyprlandWindowFloatingByTitleWithStatus({
|
||||||
|
title: 'SubMiner Overlay',
|
||||||
|
platform: 'linux',
|
||||||
|
env: { HYPRLAND_INSTANCE_SIGNATURE: 'abc' },
|
||||||
|
pid: 456,
|
||||||
|
bounds: retryBounds ? { x: 0, y: 0, width: 1280, height: 720 } : undefined,
|
||||||
|
execFileSync: (_command, args) => {
|
||||||
|
if (args.join(' ') === '-j clients') {
|
||||||
|
clientReads += 1;
|
||||||
|
return JSON.stringify(clients);
|
||||||
|
}
|
||||||
|
if (args.join(' ') === '-j status') return JSON.stringify({ configProvider });
|
||||||
|
if (args.join(' ').match(/alterzorder|alter_zorder/)) {
|
||||||
|
const address = args.join(' ').match(/address:(0x\w+)/)?.[1];
|
||||||
|
assert.ok(address);
|
||||||
|
stack.splice(stack.indexOf(address), 1);
|
||||||
|
stack.push(address);
|
||||||
|
}
|
||||||
|
return '';
|
||||||
|
},
|
||||||
|
});
|
||||||
|
assert.equal(status.dispatched, true);
|
||||||
|
assert.equal(clientReads, retryBounds ? 2 : 1);
|
||||||
|
assert.deepEqual(stack, ['0xmpv', '0xoverlay', '0xdialog'], configProvider);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Hyprland placement only promotes mapped dialogs on the placed window workspace', () => {
|
||||||
|
const calls: string[] = [];
|
||||||
|
const dialog = {
|
||||||
|
class: 'hyprland-dialog',
|
||||||
|
mapped: true,
|
||||||
|
hidden: false,
|
||||||
|
workspace: { id: 1 },
|
||||||
|
};
|
||||||
|
const clients = [
|
||||||
|
{
|
||||||
|
address: '0xoverlay',
|
||||||
|
pid: 456,
|
||||||
|
title: 'SubMiner Overlay',
|
||||||
|
floating: true,
|
||||||
|
workspace: { id: 1 },
|
||||||
|
},
|
||||||
|
{ ...dialog, address: '0xhidden', hidden: true },
|
||||||
|
{ ...dialog, address: '0xunmapped', mapped: false },
|
||||||
|
{ ...dialog, address: '0xother', workspace: { id: 2 } },
|
||||||
|
{ ...dialog, address: '0xordinary', class: 'terminal' },
|
||||||
|
{ ...dialog, address: '0xinitial', class: '', initialClass: 'hyprland-dialog' },
|
||||||
|
];
|
||||||
|
for (const promote of [true, false]) {
|
||||||
|
calls.length = 0;
|
||||||
|
ensureHyprlandWindowFloatingByTitleWithStatus({
|
||||||
|
title: 'SubMiner Overlay',
|
||||||
|
platform: 'linux',
|
||||||
|
env: { HYPRLAND_INSTANCE_SIGNATURE: 'abc' },
|
||||||
|
pid: 456,
|
||||||
|
promote,
|
||||||
|
execFileSync: (_command, args) => {
|
||||||
|
if (args.join(' ') === '-j clients') return JSON.stringify(clients);
|
||||||
|
if (args.join(' ') === '-j status') return JSON.stringify({ configProvider: 'hyprlang' });
|
||||||
|
calls.push(args.join(' '));
|
||||||
|
return '';
|
||||||
|
},
|
||||||
|
});
|
||||||
|
assert.deepEqual(
|
||||||
|
calls,
|
||||||
|
promote
|
||||||
|
? [
|
||||||
|
'dispatch alterzorder top,address:0xoverlay',
|
||||||
|
'dispatch alterzorder top,address:0xinitial',
|
||||||
|
]
|
||||||
|
: [],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
test('buildHyprlandPlacementDispatches can update placement without raising z-order', () => {
|
test('buildHyprlandPlacementDispatches can update placement without raising z-order', () => {
|
||||||
const buildDispatches = buildHyprlandPlacementDispatches as (
|
const buildDispatches = buildHyprlandPlacementDispatches as (
|
||||||
client: Parameters<typeof buildHyprlandPlacementDispatches>[0],
|
client: Parameters<typeof buildHyprlandPlacementDispatches>[0],
|
||||||
|
|||||||
@@ -3,14 +3,17 @@ import { execFileSync } from 'node:child_process';
|
|||||||
export interface HyprlandPlacementClient {
|
export interface HyprlandPlacementClient {
|
||||||
address?: string;
|
address?: string;
|
||||||
at?: [number, number];
|
at?: [number, number];
|
||||||
|
class?: string;
|
||||||
floating?: boolean;
|
floating?: boolean;
|
||||||
hidden?: boolean;
|
hidden?: boolean;
|
||||||
|
initialClass?: string;
|
||||||
initialTitle?: string;
|
initialTitle?: string;
|
||||||
mapped?: boolean;
|
mapped?: boolean;
|
||||||
pid?: number;
|
pid?: number;
|
||||||
pinned?: boolean;
|
pinned?: boolean;
|
||||||
size?: [number, number];
|
size?: [number, number];
|
||||||
title?: string;
|
title?: string;
|
||||||
|
workspace?: { id: number };
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface HyprlandPlacementBounds {
|
export interface HyprlandPlacementBounds {
|
||||||
@@ -25,7 +28,11 @@ export interface HyprlandPlacementDispatchOptions {
|
|||||||
promote?: boolean;
|
promote?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
type ExecFileSync = typeof execFileSync;
|
type ExecFileSync = (
|
||||||
|
file: string,
|
||||||
|
args: string[],
|
||||||
|
options: NonNullable<Parameters<typeof execFileSync>[2]>,
|
||||||
|
) => ReturnType<typeof execFileSync>;
|
||||||
export type HyprlandConfigProvider = 'hyprlang' | 'lua';
|
export type HyprlandConfigProvider = 'hyprlang' | 'lua';
|
||||||
|
|
||||||
export function shouldAttemptHyprlandWindowPlacement(
|
export function shouldAttemptHyprlandWindowPlacement(
|
||||||
@@ -154,6 +161,33 @@ function luaWindowDispatch(name: string, windowAddress: string, fields: string[]
|
|||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Compositor recovery dialogs must remain clickable even when an overlay still owns input.
|
||||||
|
function buildHyprlandDialogPromotionDispatches(
|
||||||
|
clients: HyprlandPlacementClient[],
|
||||||
|
placedClient: HyprlandPlacementClient,
|
||||||
|
configProvider: HyprlandConfigProvider,
|
||||||
|
): string[][] {
|
||||||
|
if (typeof placedClient.workspace?.id !== 'number') return [];
|
||||||
|
return clients.flatMap((client) => {
|
||||||
|
if (
|
||||||
|
!client.address ||
|
||||||
|
client.address === placedClient.address ||
|
||||||
|
client.mapped === false ||
|
||||||
|
client.hidden === true ||
|
||||||
|
client.workspace?.id !== placedClient.workspace?.id ||
|
||||||
|
(client.class !== 'hyprland-dialog' && client.initialClass !== 'hyprland-dialog')
|
||||||
|
) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
const windowAddress = `address:${client.address}`;
|
||||||
|
return [
|
||||||
|
configProvider === 'lua'
|
||||||
|
? luaWindowDispatch('alter_zorder', windowAddress, ['mode = "top"'])
|
||||||
|
: ['dispatch', 'alterzorder', `top,${windowAddress}`],
|
||||||
|
];
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
function luaWindowSetProp(windowAddress: string, prop: string, value: string): string[] {
|
function luaWindowSetProp(windowAddress: string, prop: string, value: string): string[] {
|
||||||
return luaWindowDispatch('set_prop', windowAddress, [
|
return luaWindowDispatch('set_prop', windowAddress, [
|
||||||
`prop = ${luaString(prop)}`,
|
`prop = ${luaString(prop)}`,
|
||||||
@@ -331,12 +365,16 @@ export function ensureHyprlandWindowFloatingByTitleWithStatus(options: {
|
|||||||
configProvider,
|
configProvider,
|
||||||
promote: options.promote,
|
promote: options.promote,
|
||||||
});
|
});
|
||||||
|
if (options.promote !== false) {
|
||||||
|
dispatches.push(...buildHyprlandDialogPromotionDispatches(clients, client, configProvider));
|
||||||
|
}
|
||||||
for (const args of dispatches) {
|
for (const args of dispatches) {
|
||||||
run('hyprctl', args, { stdio: 'ignore' });
|
run('hyprctl', args, { stdio: 'ignore' });
|
||||||
}
|
}
|
||||||
if (shouldVerifyBounds) {
|
if (shouldVerifyBounds) {
|
||||||
try {
|
try {
|
||||||
const refreshedClient = findHyprlandWindowForPlacement(readHyprlandPlacementClients(run), {
|
const refreshedClients = readHyprlandPlacementClients(run);
|
||||||
|
const refreshedClient = findHyprlandWindowForPlacement(refreshedClients, {
|
||||||
pid: options.pid ?? process.pid,
|
pid: options.pid ?? process.pid,
|
||||||
title: options.title,
|
title: options.title,
|
||||||
});
|
});
|
||||||
@@ -345,10 +383,20 @@ export function ensureHyprlandWindowFloatingByTitleWithStatus(options: {
|
|||||||
targetBounds &&
|
targetBounds &&
|
||||||
clientMatchesPlacementBounds(refreshedClient, targetBounds) === false
|
clientMatchesPlacementBounds(refreshedClient, targetBounds) === false
|
||||||
) {
|
) {
|
||||||
for (const args of buildHyprlandPlacementDispatches(refreshedClient, targetBounds, {
|
const retryDispatches = buildHyprlandPlacementDispatches(refreshedClient, targetBounds, {
|
||||||
configProvider,
|
configProvider,
|
||||||
promote: options.promote,
|
promote: options.promote,
|
||||||
})) {
|
});
|
||||||
|
if (options.promote !== false) {
|
||||||
|
retryDispatches.push(
|
||||||
|
...buildHyprlandDialogPromotionDispatches(
|
||||||
|
refreshedClients,
|
||||||
|
refreshedClient,
|
||||||
|
configProvider,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
for (const args of retryDispatches) {
|
||||||
run('hyprctl', args, { stdio: 'ignore' });
|
run('hyprctl', args, { stdio: 'ignore' });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1037,7 +1037,13 @@ test('registerIpcHandlers accepts per-controller profile config updates', async
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
await saveHandler({}, update);
|
await saveHandler({}, update);
|
||||||
assert.deepEqual(controllerSaves, [update]);
|
assert.deepEqual(controllerSaves, [
|
||||||
|
{
|
||||||
|
...update,
|
||||||
|
// Validation uses a null prototype to safely store arbitrary profile IDs.
|
||||||
|
profiles: { __proto__: null, ...update.profiles },
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
await assert.rejects(async () => {
|
await assert.rejects(async () => {
|
||||||
await saveHandler(
|
await saveHandler(
|
||||||
|
|||||||
@@ -66,7 +66,7 @@ function mergedOccurrences(dbPath: string): Array<{ word: string; seenMs: number
|
|||||||
for (const legacyOccurrences of [false, true]) {
|
for (const legacyOccurrences of [false, true]) {
|
||||||
const label = legacyOccurrences ? 'a peer predating the seen_ms column' : 'a current peer';
|
const label = legacyOccurrences ? 'a peer predating the seen_ms column' : 'a current peer';
|
||||||
|
|
||||||
test(`sync merge carries occurrence timestamps in from ${label}`, () => {
|
test(`sync merge carries occurrence timestamps in from ${label}`, { timeout: 15_000 }, () => {
|
||||||
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'subminer-merge-occurrences-test-'));
|
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'subminer-merge-occurrences-test-'));
|
||||||
try {
|
try {
|
||||||
const local = buildDb(dir, 'local.sqlite', {
|
const local = buildDb(dir, 'local.sqlite', {
|
||||||
|
|||||||
@@ -396,7 +396,8 @@ function createInjectedScriptVm(store: ReplayMessageStore): (script: string) =>
|
|||||||
Set,
|
Set,
|
||||||
String,
|
String,
|
||||||
});
|
});
|
||||||
return async (script: string) => await vm.runInContext(script, context);
|
// Clone results into the host realm, matching Electron's process boundary.
|
||||||
|
return async (script: string) => structuredClone(await vm.runInContext(script, context));
|
||||||
}
|
}
|
||||||
|
|
||||||
export function createReplayTokenizerDeps(fixture: GoldenFixture): TokenizerServiceDeps {
|
export function createReplayTokenizerDeps(fixture: GoldenFixture): TokenizerServiceDeps {
|
||||||
|
|||||||
@@ -64,7 +64,8 @@ export async function runInjectedYomitanScript(
|
|||||||
script: string,
|
script: string,
|
||||||
handler: (action: string, params: unknown) => unknown,
|
handler: (action: string, params: unknown) => unknown,
|
||||||
): Promise<unknown> {
|
): Promise<unknown> {
|
||||||
return await vm.runInNewContext(script, createYomitanScriptSandbox(handler));
|
// Clone results into the host realm, matching Electron's process boundary.
|
||||||
|
return structuredClone(await vm.runInNewContext(script, createYomitanScriptSandbox(handler)));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Persistent page context shared across executeJavaScript calls, matching the
|
// Persistent page context shared across executeJavaScript calls, matching the
|
||||||
@@ -75,7 +76,8 @@ function createPersistentYomitanScriptRunner(
|
|||||||
handler: (action: string, params: unknown) => unknown,
|
handler: (action: string, params: unknown) => unknown,
|
||||||
): (script: string) => Promise<unknown> {
|
): (script: string) => Promise<unknown> {
|
||||||
const context = vm.createContext(createYomitanScriptSandbox(handler));
|
const context = vm.createContext(createYomitanScriptSandbox(handler));
|
||||||
return async (script: string) => await vm.runInContext(script, context);
|
// Clone results into the host realm, matching Electron's process boundary.
|
||||||
|
return async (script: string) => structuredClone(await vm.runInContext(script, context));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Deps whose parser window executes every injected script (profile metadata,
|
// Deps whose parser window executes every injected script (profile metadata,
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import assert from 'node:assert/strict';
|
|||||||
import { readFileSync } from 'node:fs';
|
import { readFileSync } from 'node:fs';
|
||||||
import { resolve } from 'node:path';
|
import { resolve } from 'node:path';
|
||||||
import {
|
import {
|
||||||
|
executableRunLines,
|
||||||
jobSteps,
|
jobSteps,
|
||||||
readWorkflow,
|
readWorkflow,
|
||||||
stepRunsCommand,
|
stepRunsCommand,
|
||||||
@@ -12,7 +13,14 @@ import {
|
|||||||
|
|
||||||
const prereleaseWorkflowPath = resolve(__dirname, '../.github/workflows/prerelease.yml');
|
const prereleaseWorkflowPath = resolve(__dirname, '../.github/workflows/prerelease.yml');
|
||||||
const prereleaseWorkflow = readFileSync(prereleaseWorkflowPath, 'utf8').replace(/\r\n/g, '\n');
|
const prereleaseWorkflow = readFileSync(prereleaseWorkflowPath, 'utf8').replace(/\r\n/g, '\n');
|
||||||
|
const packageWorkflow = readFileSync(
|
||||||
|
resolve(__dirname, '../.github/workflows/package-release.yml'),
|
||||||
|
'utf8',
|
||||||
|
);
|
||||||
const parsedPrereleaseWorkflow = readWorkflow(prereleaseWorkflowPath);
|
const parsedPrereleaseWorkflow = readWorkflow(prereleaseWorkflowPath);
|
||||||
|
const parsedPackageWorkflow = readWorkflow(
|
||||||
|
resolve(__dirname, '../.github/workflows/package-release.yml'),
|
||||||
|
);
|
||||||
const packageJsonPath = resolve(__dirname, '../package.json');
|
const packageJsonPath = resolve(__dirname, '../package.json');
|
||||||
const packageJson = JSON.parse(readFileSync(packageJsonPath, 'utf8')) as {
|
const packageJson = JSON.parse(readFileSync(packageJsonPath, 'utf8')) as {
|
||||||
scripts: Record<string, string>;
|
scripts: Record<string, string>;
|
||||||
@@ -42,15 +50,10 @@ test('prerelease workflow uses committed prerelease notes and never calls claude
|
|||||||
});
|
});
|
||||||
|
|
||||||
test('prerelease delegates its quality gate instead of duplicating quality steps', () => {
|
test('prerelease delegates its quality gate instead of duplicating quality steps', () => {
|
||||||
assert.match(
|
assert.deepEqual(parsedPrereleaseWorkflow.jobs?.['quality-gate'], {
|
||||||
prereleaseWorkflow,
|
permissions: { contents: 'read' },
|
||||||
/quality-gate:\s*\n\s*permissions:\s*\n\s*contents: read\s*\n\s*uses: \.\/\.github\/workflows\/quality-gate\.yml/,
|
uses: './.github/workflows/quality-gate.yml',
|
||||||
);
|
});
|
||||||
const qualityGateJob = prereleaseWorkflow.match(/quality-gate:[\s\S]*?(?=\n build-linux:)/)?.[0];
|
|
||||||
assert.ok(qualityGateJob);
|
|
||||||
assert.doesNotMatch(qualityGateJob, /oven-sh\/setup-bun/);
|
|
||||||
assert.doesNotMatch(qualityGateJob, /bun run test:coverage:src/);
|
|
||||||
assert.doesNotMatch(qualityGateJob, /bun run test:env/);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
test('prerelease workflow publishes GitHub prereleases and keeps them off latest', () => {
|
test('prerelease workflow publishes GitHub prereleases and keeps them off latest', () => {
|
||||||
@@ -60,10 +63,10 @@ test('prerelease workflow publishes GitHub prereleases and keeps them off latest
|
|||||||
});
|
});
|
||||||
|
|
||||||
test('prerelease packaging workflows scope dependency caches by runner architecture', () => {
|
test('prerelease packaging workflows scope dependency caches by runner architecture', () => {
|
||||||
const archScopedCacheKeyMatches = prereleaseWorkflow.match(
|
const archScopedCacheKeyMatches = (prereleaseWorkflow + packageWorkflow).match(
|
||||||
/key:\s*\${{\s*runner\.os\s*}}-\${{\s*runner\.arch\s*}}-bun-/g,
|
/key:\s*\${{\s*runner\.os\s*}}-\${{\s*runner\.arch\s*}}-bun-/g,
|
||||||
);
|
);
|
||||||
const archScopedRestoreKeyMatches = prereleaseWorkflow.match(
|
const archScopedRestoreKeyMatches = (prereleaseWorkflow + packageWorkflow).match(
|
||||||
/\${{\s*runner\.os\s*}}-\${{\s*runner\.arch\s*}}-bun-/g,
|
/\${{\s*runner\.os\s*}}-\${{\s*runner\.arch\s*}}-bun-/g,
|
||||||
);
|
);
|
||||||
assert.equal(archScopedCacheKeyMatches?.length, 4);
|
assert.equal(archScopedCacheKeyMatches?.length, 4);
|
||||||
@@ -71,12 +74,79 @@ test('prerelease packaging workflows scope dependency caches by runner architect
|
|||||||
});
|
});
|
||||||
|
|
||||||
test('prerelease workflow builds and uploads all release platforms', () => {
|
test('prerelease workflow builds and uploads all release platforms', () => {
|
||||||
assert.match(prereleaseWorkflow, /build-linux:/);
|
assert.deepEqual(Object.keys(parsedPrereleaseWorkflow.jobs ?? {}).sort(), [
|
||||||
assert.match(prereleaseWorkflow, /build-macos:/);
|
'package',
|
||||||
assert.match(prereleaseWorkflow, /build-windows:/);
|
'quality-gate',
|
||||||
assert.match(prereleaseWorkflow, /name: appimage/);
|
'release',
|
||||||
assert.match(prereleaseWorkflow, /name: macos/);
|
]);
|
||||||
assert.match(prereleaseWorkflow, /name: windows/);
|
assert.equal(
|
||||||
|
parsedPrereleaseWorkflow.jobs?.package?.uses,
|
||||||
|
'./.github/workflows/package-release.yml',
|
||||||
|
);
|
||||||
|
assert.deepEqual(parsedPrereleaseWorkflow.jobs?.package?.needs, ['quality-gate']);
|
||||||
|
assert.deepEqual(parsedPrereleaseWorkflow.jobs?.release?.needs, ['package']);
|
||||||
|
assert.deepEqual(Object.keys(parsedPackageWorkflow.jobs ?? {}).sort(), [
|
||||||
|
'build-linux',
|
||||||
|
'build-macos',
|
||||||
|
'build-windows',
|
||||||
|
]);
|
||||||
|
for (const [job, name, paths] of [
|
||||||
|
['build-linux', 'appimage', ['release/*.AppImage']],
|
||||||
|
['build-macos', 'macos', ['release/*.dmg', 'release/*.zip']],
|
||||||
|
['build-windows', 'windows', ['release/*.exe', 'release/*.zip']],
|
||||||
|
] as const) {
|
||||||
|
const uploads = jobSteps(parsedPackageWorkflow, job).filter(
|
||||||
|
(step) => step.uses === 'actions/upload-artifact@v4',
|
||||||
|
);
|
||||||
|
assert.equal(uploads.length, 1);
|
||||||
|
const upload = uploads[0];
|
||||||
|
assert.ok(upload);
|
||||||
|
assert.equal(upload.with?.name, name);
|
||||||
|
assert.equal(upload.with?.['if-no-files-found'], 'error');
|
||||||
|
const uploadPath = upload.with?.path;
|
||||||
|
assert.ok(typeof uploadPath === 'string');
|
||||||
|
assert.deepEqual(uploadPath.trim().split('\n'), [
|
||||||
|
...paths,
|
||||||
|
'release/latest*.yml',
|
||||||
|
'release/*.blockmap',
|
||||||
|
'release/package-size-*.json',
|
||||||
|
]);
|
||||||
|
const download = jobSteps(parsedPrereleaseWorkflow, 'release').find(
|
||||||
|
(step) => step.uses === 'actions/download-artifact@v4' && step.with?.name === name,
|
||||||
|
);
|
||||||
|
assert.equal(download?.with?.path, 'release');
|
||||||
|
}
|
||||||
|
const steps = jobSteps(parsedPrereleaseWorkflow, 'release');
|
||||||
|
const checksum = steps.find((step) => step.name === 'Generate checksums');
|
||||||
|
const publish = steps.find((step) => step.name === 'Publish Prerelease');
|
||||||
|
assert.ok(checksum);
|
||||||
|
assert.ok(publish);
|
||||||
|
assert.ok(executableRunLines(checksum).includes('files+=(release/package-size-*.json)'));
|
||||||
|
assert.ok(executableRunLines(publish).includes('release/package-size-*.json'));
|
||||||
|
});
|
||||||
|
|
||||||
|
test('release callers pass only the declared macOS signing secrets to packaging', () => {
|
||||||
|
const secrets = [
|
||||||
|
'CSC_LINK',
|
||||||
|
'CSC_KEY_PASSWORD',
|
||||||
|
'APPLE_ID',
|
||||||
|
'APPLE_APP_SPECIFIC_PASSWORD',
|
||||||
|
'APPLE_TEAM_ID',
|
||||||
|
];
|
||||||
|
assert.deepEqual(
|
||||||
|
parsedPackageWorkflow.on?.workflow_call?.secrets,
|
||||||
|
Object.fromEntries(secrets.map((name) => [name, { required: true }])),
|
||||||
|
);
|
||||||
|
for (const workflow of [
|
||||||
|
parsedPrereleaseWorkflow,
|
||||||
|
readWorkflow(resolve(__dirname, '../.github/workflows/release.yml')),
|
||||||
|
]) {
|
||||||
|
assert.equal(workflow.jobs?.package?.uses, './.github/workflows/package-release.yml');
|
||||||
|
assert.deepEqual(
|
||||||
|
workflow.jobs?.package?.secrets,
|
||||||
|
Object.fromEntries(secrets.map((name) => [name, '${{ secrets.' + name + ' }}'])),
|
||||||
|
);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
test('prerelease workflow publishes both launcher wrappers with the platform packages', () => {
|
test('prerelease workflow publishes both launcher wrappers with the platform packages', () => {
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import assert from 'node:assert/strict';
|
|||||||
import { readFileSync } from 'node:fs';
|
import { readFileSync } from 'node:fs';
|
||||||
import { resolve } from 'node:path';
|
import { resolve } from 'node:path';
|
||||||
import {
|
import {
|
||||||
|
jobSteps,
|
||||||
readWorkflow,
|
readWorkflow,
|
||||||
stepsMissingEnvDeclaration,
|
stepsMissingEnvDeclaration,
|
||||||
templateExpressionsInRunBodies,
|
templateExpressionsInRunBodies,
|
||||||
@@ -10,6 +11,10 @@ import {
|
|||||||
|
|
||||||
const releaseWorkflowPath = resolve(__dirname, '../.github/workflows/release.yml');
|
const releaseWorkflowPath = resolve(__dirname, '../.github/workflows/release.yml');
|
||||||
const releaseWorkflow = readFileSync(releaseWorkflowPath, 'utf8');
|
const releaseWorkflow = readFileSync(releaseWorkflowPath, 'utf8');
|
||||||
|
const packageWorkflow = readFileSync(
|
||||||
|
resolve(__dirname, '../.github/workflows/package-release.yml'),
|
||||||
|
'utf8',
|
||||||
|
);
|
||||||
const docsPagesWorkflowPath = resolve(__dirname, '../.github/workflows/docs-pages.yml');
|
const docsPagesWorkflowPath = resolve(__dirname, '../.github/workflows/docs-pages.yml');
|
||||||
const docsPagesWorkflow = readFileSync(docsPagesWorkflowPath, 'utf8');
|
const docsPagesWorkflow = readFileSync(docsPagesWorkflowPath, 'utf8');
|
||||||
const parsedReleaseWorkflow = readWorkflow(releaseWorkflowPath);
|
const parsedReleaseWorkflow = readWorkflow(releaseWorkflowPath);
|
||||||
@@ -96,20 +101,25 @@ test('release delegates its quality gate instead of duplicating quality steps',
|
|||||||
releaseWorkflow,
|
releaseWorkflow,
|
||||||
/quality-gate:\s*\n\s*permissions:\s*\n\s*contents: read\s*\n\s*uses: \.\/\.github\/workflows\/quality-gate\.yml/,
|
/quality-gate:\s*\n\s*permissions:\s*\n\s*contents: read\s*\n\s*uses: \.\/\.github\/workflows\/quality-gate\.yml/,
|
||||||
);
|
);
|
||||||
const qualityGateJob = releaseWorkflow.match(/quality-gate:[\s\S]*?(?=\n build-linux:)/)?.[0];
|
const qualityGateJob = releaseWorkflow.match(/quality-gate:[\s\S]*?(?=\n package:)/)?.[0];
|
||||||
assert.ok(qualityGateJob);
|
assert.ok(qualityGateJob);
|
||||||
assert.doesNotMatch(qualityGateJob, /oven-sh\/setup-bun/);
|
assert.doesNotMatch(qualityGateJob, /oven-sh\/setup-bun/);
|
||||||
assert.doesNotMatch(qualityGateJob, /bun run test:coverage:src/);
|
assert.doesNotMatch(qualityGateJob, /bun run test:coverage:src/);
|
||||||
assert.doesNotMatch(qualityGateJob, /bun run test:env/);
|
assert.doesNotMatch(qualityGateJob, /bun run test:env/);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('release build jobs install and cache stats dependencies before packaging', () => {
|
test('each release build job installs stats dependencies before packaging', () => {
|
||||||
assert.match(releaseWorkflow, /build-linux:[\s\S]*stats\/node_modules/);
|
const workflow = readWorkflow(resolve(__dirname, '../.github/workflows/package-release.yml'));
|
||||||
assert.match(releaseWorkflow, /build-macos:[\s\S]*stats\/node_modules/);
|
for (const job of ['build-linux', 'build-macos', 'build-windows']) {
|
||||||
assert.match(releaseWorkflow, /build-windows:[\s\S]*stats\/node_modules/);
|
const steps = jobSteps(workflow, job);
|
||||||
assert.match(releaseWorkflow, /build-linux:[\s\S]*cd stats && bun install --frozen-lockfile/);
|
const install = steps.findIndex((step) =>
|
||||||
assert.match(releaseWorkflow, /build-macos:[\s\S]*cd stats && bun install --frozen-lockfile/);
|
step.run?.includes('cd stats && bun install --frozen-lockfile'),
|
||||||
assert.match(releaseWorkflow, /build-windows:[\s\S]*cd stats && bun install --frozen-lockfile/);
|
);
|
||||||
|
const build = steps.findIndex((step) =>
|
||||||
|
/bun run build:(appimage|mac|win)/.test(step.run ?? ''),
|
||||||
|
);
|
||||||
|
assert(install >= 0 && build > install, `${job} must install stats before packaging`);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
test('release workflow generates release notes from committed changelog output', () => {
|
test('release workflow generates release notes from committed changelog output', () => {
|
||||||
@@ -163,42 +173,6 @@ test('top-level package metadata keeps Linux Electron runtime app identity canon
|
|||||||
assert.equal(packageJson.desktopName, 'SubMiner.desktop');
|
assert.equal(packageJson.desktopName, 'SubMiner.desktop');
|
||||||
});
|
});
|
||||||
|
|
||||||
test('release packaging keeps default file inclusion and excludes large source-only trees explicitly', () => {
|
|
||||||
const files = packageJson.build?.files ?? [];
|
|
||||||
assert.ok(files.includes('**/*'));
|
|
||||||
assert.ok(files.includes('!src{,/**/*}'));
|
|
||||||
assert.ok(files.includes('!launcher{,/**/*}'));
|
|
||||||
assert.ok(files.includes('!stats/src{,/**/*}'));
|
|
||||||
assert.ok(files.includes('!.tmp{,/**/*}'));
|
|
||||||
assert.ok(files.includes('!release-*{,/**/*}'));
|
|
||||||
assert.ok(files.includes('!vendor/subminer-yomitan{,/**/*}'));
|
|
||||||
assert.ok(files.includes('!vendor/texthooker-ui/src{,/**/*}'));
|
|
||||||
assert.ok(files.includes('!assets{,/**/*}'));
|
|
||||||
assert.ok(files.includes('!plugin{,/**/*}'));
|
|
||||||
assert.ok(files.includes('!vendor/yomitan-jlpt-vocab{,/**/*}'));
|
|
||||||
assert.ok(files.includes('!docs{,/**/*}'));
|
|
||||||
assert.ok(files.includes('!tests{,/**/*}'));
|
|
||||||
assert.ok(files.includes('!packaging{,/**/*}'));
|
|
||||||
assert.ok(files.includes('!README.md'));
|
|
||||||
assert.ok(files.includes('!CHANGELOG.md'));
|
|
||||||
assert.ok(files.includes('!AGENTS.md'));
|
|
||||||
assert.ok(files.includes('!CLAUDE.md'));
|
|
||||||
assert.ok(files.includes('!stats/public{,/**/*}'));
|
|
||||||
assert.ok(files.includes('!stats/package.json'));
|
|
||||||
assert.ok(files.includes('!stats/tsconfig.json'));
|
|
||||||
assert.ok(files.includes('!stats/vite.config.ts'));
|
|
||||||
assert.ok(files.includes('!dist/**/*.map'));
|
|
||||||
assert.ok(files.includes('!dist/**/*.test.*'));
|
|
||||||
assert.ok(files.includes('!dist/**/__tests__{,/**/*}'));
|
|
||||||
assert.ok(files.includes('!scripts/**/*.test.*'));
|
|
||||||
assert.ok(files.includes('!vendor/texthooker-ui/public{,/**/*}'));
|
|
||||||
assert.ok(files.includes('!vendor/texthooker-ui/.vscode{,/**/*}'));
|
|
||||||
assert.ok(files.includes('!vendor/texthooker-ui/README.md'));
|
|
||||||
assert.ok(files.includes('!vendor/texthooker-ui/package.json'));
|
|
||||||
assert.ok(files.includes('!vendor/texthooker-ui/tsconfig*.json'));
|
|
||||||
assert.ok(files.includes('!node_modules/@libsql/linux-x64-musl{,/**/*}'));
|
|
||||||
});
|
|
||||||
|
|
||||||
test('release packaging stages only the generated launcher runtime artifacts', () => {
|
test('release packaging stages only the generated launcher runtime artifacts', () => {
|
||||||
const launcherResource = packageJson.build?.extraResources?.find(
|
const launcherResource = packageJson.build?.extraResources?.find(
|
||||||
(resource) => resource.from === 'dist/launcher' && resource.to === 'launcher',
|
(resource) => resource.from === 'dist/launcher' && resource.to === 'launcher',
|
||||||
@@ -239,12 +213,12 @@ test('config example generation runs directly from source without unrelated bund
|
|||||||
});
|
});
|
||||||
|
|
||||||
test('windows release workflow publishes unsigned artifacts directly without SignPath', () => {
|
test('windows release workflow publishes unsigned artifacts directly without SignPath', () => {
|
||||||
assert.match(releaseWorkflow, /Build unsigned Windows artifacts/);
|
assert.match(packageWorkflow, /Build unsigned Windows artifacts/);
|
||||||
assert.match(releaseWorkflow, /run: bun run build:win:unsigned/);
|
assert.match(packageWorkflow, /run: bun run build:win:unsigned/);
|
||||||
assert.match(releaseWorkflow, /name: windows/);
|
assert.match(packageWorkflow, /name: windows/);
|
||||||
assert.match(releaseWorkflow, /path: \|\n\s+release\/\*\.exe\n\s+release\/\*\.zip/);
|
assert.match(packageWorkflow, /path: \|\n\s+release\/\*\.exe\n\s+release\/\*\.zip/);
|
||||||
assert.ok(!releaseWorkflow.includes('signpath/github-action-submit-signing-request'));
|
assert.ok(!packageWorkflow.includes('signpath/github-action-submit-signing-request'));
|
||||||
assert.ok(!releaseWorkflow.includes('SIGNPATH_'));
|
assert.ok(!packageWorkflow.includes('SIGNPATH_'));
|
||||||
});
|
});
|
||||||
|
|
||||||
test('release artifact names are distinct before upload', () => {
|
test('release artifact names are distinct before upload', () => {
|
||||||
@@ -306,3 +280,21 @@ test('release and docs workflows keep tag-derived values out of shell bodies', (
|
|||||||
// that would be substituted into the condition before the shell reads it.
|
// that would be substituted into the condition before the shell reads it.
|
||||||
assert.match(docsPagesWorkflow, /if \[\[ ! "\$TAG_NAME" =~/);
|
assert.match(docsPagesWorkflow, /if \[\[ ! "\$TAG_NAME" =~/);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('stable and prerelease builds use the same packaging gate', () => {
|
||||||
|
const prerelease = readFileSync(
|
||||||
|
resolve(__dirname, '../.github/workflows/prerelease.yml'),
|
||||||
|
'utf8',
|
||||||
|
);
|
||||||
|
for (const workflow of [releaseWorkflow, prerelease]) {
|
||||||
|
assert.match(workflow, /uses: \.\/\.github\/workflows\/package-release\.yml/);
|
||||||
|
assert.match(workflow, /needs: \[package\]/);
|
||||||
|
assert.match(workflow, /release\/package-size-\*\.json/);
|
||||||
|
}
|
||||||
|
assert.deepEqual(
|
||||||
|
templateExpressionsInRunBodies(
|
||||||
|
readWorkflow(resolve(__dirname, '../.github/workflows/package-release.yml')),
|
||||||
|
),
|
||||||
|
[],
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|||||||
@@ -18,7 +18,7 @@
|
|||||||
|
|
||||||
@font-face {
|
@font-face {
|
||||||
font-family: 'M PLUS 1';
|
font-family: 'M PLUS 1';
|
||||||
src: url('./fonts/MPLUS1[wght].ttf') format('truetype');
|
src: url('../fonts/MPLUS1[wght].ttf') format('truetype');
|
||||||
font-weight: 100 900;
|
font-weight: 100 900;
|
||||||
font-display: swap;
|
font-display: swap;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
@font-face {
|
@font-face {
|
||||||
font-family: 'M PLUS 1';
|
font-family: 'M PLUS 1';
|
||||||
src: url('./fonts/MPLUS1[wght].ttf') format('truetype');
|
src: url('../fonts/MPLUS1[wght].ttf') format('truetype');
|
||||||
font-weight: 100 900;
|
font-weight: 100 900;
|
||||||
font-display: swap;
|
font-display: swap;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
@font-face {
|
@font-face {
|
||||||
font-family: 'M PLUS 1';
|
font-family: 'M PLUS 1';
|
||||||
src: url('./fonts/MPLUS1[wght].ttf') format('truetype');
|
src: url('../fonts/MPLUS1[wght].ttf') format('truetype');
|
||||||
font-weight: 100 900;
|
font-weight: 100 900;
|
||||||
font-display: swap;
|
font-display: swap;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,10 +4,23 @@ export type WorkflowStep = {
|
|||||||
name?: string;
|
name?: string;
|
||||||
run?: string;
|
run?: string;
|
||||||
env?: Record<string, unknown>;
|
env?: Record<string, unknown>;
|
||||||
|
uses?: string;
|
||||||
|
with?: Record<string, unknown>;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type ParsedWorkflow = {
|
export type ParsedWorkflow = {
|
||||||
jobs?: Record<string, { steps?: WorkflowStep[] } | undefined>;
|
on?: { workflow_call?: { secrets?: Record<string, { required?: boolean }> } };
|
||||||
|
jobs?: Record<
|
||||||
|
string,
|
||||||
|
| {
|
||||||
|
steps?: WorkflowStep[];
|
||||||
|
uses?: string;
|
||||||
|
needs?: string | string[];
|
||||||
|
permissions?: Record<string, string>;
|
||||||
|
secrets?: string | Record<string, string>;
|
||||||
|
}
|
||||||
|
| undefined
|
||||||
|
>;
|
||||||
};
|
};
|
||||||
|
|
||||||
// Workflow tests only ever run under `bun test`, which parses YAML natively.
|
// Workflow tests only ever run under `bun test`, which parses YAML natively.
|
||||||
|
|||||||
Reference in New Issue
Block a user