mirror of
https://github.com/ksyasuda/SubMiner.git
synced 2026-04-09 04:19:27 -07:00
chore: prep 0.12.0-beta.1 prerelease workflow
This commit is contained in:
389
.github/workflows/prerelease.yml
vendored
Normal file
389
.github/workflows/prerelease.yml
vendored
Normal file
@@ -0,0 +1,389 @@
|
||||
name: Prerelease
|
||||
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- 'v*-beta.*'
|
||||
- 'v*-rc.*'
|
||||
|
||||
concurrency:
|
||||
group: prerelease-${{ github.ref }}
|
||||
cancel-in-progress: false
|
||||
|
||||
jobs:
|
||||
quality-gate:
|
||||
runs-on: ubuntu-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/subminer-yomitan/node_modules
|
||||
key: ${{ runner.os }}-bun-${{ hashFiles('bun.lock', 'stats/bun.lock', '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: Lint stats (formatting)
|
||||
run: bun run lint:stats
|
||||
|
||||
- name: Build (TypeScript check)
|
||||
run: bun run typecheck
|
||||
|
||||
- name: Test suite (source)
|
||||
run: bun run test:fast
|
||||
|
||||
- name: Coverage suite (maintained source lane)
|
||||
run: bun run test:coverage:src
|
||||
|
||||
- name: Upload coverage artifact
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: coverage-test-src
|
||||
path: coverage/test-src/lcov.info
|
||||
if-no-files-found: error
|
||||
|
||||
- name: Launcher smoke suite (source)
|
||||
run: bun run test:launcher:smoke:src
|
||||
|
||||
- name: Upload launcher smoke artifacts (on failure)
|
||||
if: failure()
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: launcher-smoke
|
||||
path: .tmp/launcher-smoke/**
|
||||
if-no-files-found: ignore
|
||||
|
||||
- name: Build (bundle)
|
||||
run: bun run build
|
||||
|
||||
- name: Immersion SQLite verification
|
||||
run: bun run test:immersion:sqlite:dist
|
||||
|
||||
- name: Dist smoke suite
|
||||
run: bun run test:smoke:dist
|
||||
|
||||
build-linux:
|
||||
needs: [quality-gate]
|
||||
runs-on: ubuntu-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
|
||||
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
|
||||
|
||||
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
|
||||
|
||||
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: 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
|
||||
if-no-files-found: error
|
||||
|
||||
release:
|
||||
needs: [build-linux, build-macos, build-windows]
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: write
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Download AppImage
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: appimage
|
||||
path: release
|
||||
|
||||
- name: Download macOS artifacts
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: macos
|
||||
path: release
|
||||
|
||||
- name: Download Windows artifacts
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: windows
|
||||
path: release
|
||||
|
||||
- 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
|
||||
key: ${{ runner.os }}-bun-${{ hashFiles('bun.lock') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-bun-
|
||||
|
||||
- name: Install dependencies
|
||||
run: bun install --frozen-lockfile
|
||||
|
||||
- name: Build Bun subminer wrapper
|
||||
run: make build-launcher
|
||||
|
||||
- name: Verify Bun subminer wrapper
|
||||
run: dist/launcher/subminer --help >/dev/null
|
||||
|
||||
- name: Enforce generated launcher workflow
|
||||
run: bash scripts/verify-generated-launcher.sh
|
||||
|
||||
- name: Verify generated config examples
|
||||
run: bun run verify:config-example
|
||||
|
||||
- name: Package optional assets bundle
|
||||
run: |
|
||||
tar -czf "release/subminer-assets.tar.gz" \
|
||||
config.example.jsonc \
|
||||
plugin/subminer \
|
||||
plugin/subminer.conf \
|
||||
assets/themes/subminer.rasi
|
||||
|
||||
- name: Generate checksums
|
||||
run: |
|
||||
shopt -s nullglob
|
||||
files=(release/*.AppImage release/*.dmg release/*.exe release/*.zip release/*.tar.gz dist/launcher/subminer)
|
||||
if [ "${#files[@]}" -eq 0 ]; then
|
||||
echo "No release artifacts found for checksum generation."
|
||||
exit 1
|
||||
fi
|
||||
sha256sum "${files[@]}" > release/SHA256SUMS.txt
|
||||
|
||||
- name: Get version from tag
|
||||
id: version
|
||||
run: echo "VERSION=${GITHUB_REF#refs/tags/}" >> "$GITHUB_OUTPUT"
|
||||
|
||||
- name: Generate prerelease notes from pending fragments
|
||||
run: bun run changelog:prerelease-notes --version "${{ steps.version.outputs.VERSION }}"
|
||||
|
||||
- name: Publish Prerelease
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
|
||||
if gh release view "${{ steps.version.outputs.VERSION }}" >/dev/null 2>&1; then
|
||||
gh release edit "${{ steps.version.outputs.VERSION }}" \
|
||||
--draft=false \
|
||||
--prerelease \
|
||||
--title "${{ steps.version.outputs.VERSION }}" \
|
||||
--notes-file release/prerelease-notes.md
|
||||
else
|
||||
gh release create "${{ steps.version.outputs.VERSION }}" \
|
||||
--latest=false \
|
||||
--prerelease \
|
||||
--title "${{ steps.version.outputs.VERSION }}" \
|
||||
--notes-file release/prerelease-notes.md
|
||||
fi
|
||||
|
||||
shopt -s nullglob
|
||||
artifacts=(
|
||||
release/*.AppImage
|
||||
release/*.dmg
|
||||
release/*.exe
|
||||
release/*.zip
|
||||
release/*.tar.gz
|
||||
release/SHA256SUMS.txt
|
||||
dist/launcher/subminer
|
||||
)
|
||||
|
||||
if [ "${#artifacts[@]}" -eq 0 ]; then
|
||||
echo "No release artifacts found for upload."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
for asset in "${artifacts[@]}"; do
|
||||
gh release upload "${{ steps.version.outputs.VERSION }}" "$asset" --clobber
|
||||
done
|
||||
3
.github/workflows/release.yml
vendored
3
.github/workflows/release.yml
vendored
@@ -4,6 +4,9 @@ on:
|
||||
push:
|
||||
tags:
|
||||
- 'v*'
|
||||
tags-ignore:
|
||||
- 'v*-beta.*'
|
||||
- 'v*-rc.*'
|
||||
|
||||
concurrency:
|
||||
group: release-${{ github.ref }}
|
||||
|
||||
5
changes/2026-04-09-prerelease-workflow.md
Normal file
5
changes/2026-04-09-prerelease-workflow.md
Normal file
@@ -0,0 +1,5 @@
|
||||
type: internal
|
||||
area: release
|
||||
|
||||
- Added a dedicated beta/rc prerelease GitHub Actions workflow that publishes GitHub prereleases without consuming pending changelog fragments or updating AUR.
|
||||
- Added prerelease note generation so beta and release-candidate tags can reuse the current pending `changes/*.md` fragments while leaving stable changelog publication for the final release cut.
|
||||
@@ -30,3 +30,9 @@ Rules:
|
||||
- each non-empty body line becomes a bullet
|
||||
- `README.md` is ignored by the generator
|
||||
- if a PR should not produce release notes, apply the `skip-changelog` label instead of adding a fragment
|
||||
|
||||
Prerelease notes:
|
||||
|
||||
- prerelease tags like `v0.11.3-beta.1` and `v0.11.3-rc.1` reuse the current pending fragments to generate `release/prerelease-notes.md`
|
||||
- prerelease note generation does not consume fragments and does not update `CHANGELOG.md` or `docs-site/changelog.md`
|
||||
- the final stable release is the point where `bun run changelog:build` consumes fragments into the stable changelog and release notes
|
||||
|
||||
@@ -2,6 +2,8 @@
|
||||
|
||||
# Releasing
|
||||
|
||||
## Stable Release
|
||||
|
||||
1. Confirm `main` is green: `gh run list --workflow CI --limit 5`.
|
||||
2. Confirm release-facing docs are current: `README.md`, `changes/*.md`, and any touched `docs-site/` pages/config examples.
|
||||
3. Run `bun run changelog:lint`.
|
||||
@@ -24,15 +26,37 @@
|
||||
10. Tag the commit: `git tag v<version>`.
|
||||
11. Push commit + tag.
|
||||
|
||||
## Prerelease
|
||||
|
||||
1. Confirm release-facing docs and pending `changes/*.md` fragments are current.
|
||||
2. Run `bun run changelog:lint`.
|
||||
3. Bump `package.json` to the prerelease version, for example `0.11.3-beta.1` or `0.11.3-rc.1`.
|
||||
4. Run the prerelease gate locally:
|
||||
`bun run changelog:prerelease-notes --version <version>`
|
||||
`bun run verify:config-example`
|
||||
`bun run typecheck`
|
||||
`bun run test:fast`
|
||||
`bun run test:env`
|
||||
`bun run build`
|
||||
5. Commit the prerelease prep. Do not run `bun run changelog:build`.
|
||||
6. Tag the commit: `git tag v<version>`.
|
||||
7. Push commit + tag.
|
||||
|
||||
Prerelease tags publish a GitHub prerelease only. They do not update `CHANGELOG.md`, `docs-site/changelog.md`, or the AUR package, and they do not consume `changes/*.md` fragments. The final stable release is still the point where `bun run changelog:build` consumes fragments into `CHANGELOG.md` and regenerates stable release notes.
|
||||
|
||||
Notes:
|
||||
|
||||
- Versioning policy: SubMiner stays 0-ver. Large or breaking release lines still bump the minor number (`0.x.0`), not `1.0.0`. Example: the next major line after `0.6.5` is `0.7.0`.
|
||||
- Supported prerelease channels are `beta` and `rc`, with versions like `0.11.3-beta.1` and `0.11.3-rc.1`.
|
||||
- Pass `--date` explicitly when you want the release stamped with the local cut date; otherwise the generator uses the current ISO date, which can roll over to the next UTC day late at night.
|
||||
- `changelog:check` now rejects tag/package version mismatches.
|
||||
- `changelog:prerelease-notes` also rejects tag/package version mismatches and writes `release/prerelease-notes.md` without mutating tracked changelog files.
|
||||
- `changelog:build` generates `CHANGELOG.md` + `release/release-notes.md` and removes the released `changes/*.md` fragments.
|
||||
- In the same way, the release workflow now auto-runs `changelog:build` when it detects unreleased `changes/*.md` on a tag-based run, then verifies and publishes.
|
||||
- Do not tag while `changes/*.md` fragments still exist.
|
||||
- Prerelease tags intentionally keep `changes/*.md` fragments in place so multiple prereleases can reuse the same cumulative pending notes until the final stable cut.
|
||||
- If you need to repair a published release body (for example, a prior version’s section was omitted), regenerate notes from `CHANGELOG.md` and re-edit the release with `gh release edit --notes-file`.
|
||||
- Prerelease tags are handled by `.github/workflows/prerelease.yml`, which always publishes a GitHub prerelease with all current release platforms and never runs the AUR sync job.
|
||||
- Tagged release workflow now also attempts to update `subminer-bin` on the AUR after GitHub Release publication.
|
||||
- AUR publish is best-effort: the workflow retries transient SSH clone/push failures, then warns and leaves the GitHub Release green if AUR still fails. Follow up with a manual `git push aur master` from the AUR checkout when needed.
|
||||
- Required GitHub Actions secret: `AUR_SSH_PRIVATE_KEY`. Add the matching public key to your AUR account before relying on the automation.
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
"name": "subminer",
|
||||
"productName": "SubMiner",
|
||||
"desktopName": "SubMiner.desktop",
|
||||
"version": "0.11.2",
|
||||
"version": "0.12.0-beta.1",
|
||||
"description": "All-in-one sentence mining overlay with AnkiConnect and dictionary integration",
|
||||
"packageManager": "bun@1.3.5",
|
||||
"main": "dist/main-entry.js",
|
||||
@@ -26,6 +26,7 @@
|
||||
"changelog:lint": "bun run scripts/build-changelog.ts lint",
|
||||
"changelog:pr-check": "bun run scripts/build-changelog.ts pr-check",
|
||||
"changelog:release-notes": "bun run scripts/build-changelog.ts release-notes",
|
||||
"changelog:prerelease-notes": "bun run scripts/build-changelog.ts prerelease-notes",
|
||||
"format": "prettier --write .",
|
||||
"format:check": "prettier --check .",
|
||||
"format:src": "bash scripts/prettier-scope.sh --write",
|
||||
@@ -69,7 +70,7 @@
|
||||
"test:launcher": "bun run test:launcher:src",
|
||||
"test:core": "bun run test:core:src",
|
||||
"test:subtitle": "bun run test:subtitle:src",
|
||||
"test:fast": "bun run test:config:src && bun run test:core:src && bun run test:docs:kb && bun test src/main-entry-runtime.test.ts src/anki-integration.test.ts src/anki-integration/anki-connect-proxy.test.ts src/anki-integration/field-grouping-workflow.test.ts src/anki-integration/field-grouping.test.ts src/anki-integration/field-grouping-merge.test.ts src/release-workflow.test.ts src/ci-workflow.test.ts scripts/build-changelog.test.ts scripts/electron-builder-after-pack.test.ts scripts/mkv-to-readme-video.test.ts scripts/run-coverage-lane.test.ts scripts/update-aur-package.test.ts && bun test src/core/services/immersion-tracker/__tests__/query.test.ts src/core/services/immersion-tracker/__tests__/query-split-modules.test.ts && bun run tsc && bun test dist/main/runtime/registry.test.js",
|
||||
"test:fast": "bun run test:config:src && bun run test:core:src && bun run test:docs:kb && bun test src/main-entry-runtime.test.ts src/anki-integration.test.ts src/anki-integration/anki-connect-proxy.test.ts src/anki-integration/field-grouping-workflow.test.ts src/anki-integration/field-grouping.test.ts src/anki-integration/field-grouping-merge.test.ts src/release-workflow.test.ts src/prerelease-workflow.test.ts src/ci-workflow.test.ts scripts/build-changelog.test.ts scripts/electron-builder-after-pack.test.ts scripts/mkv-to-readme-video.test.ts scripts/run-coverage-lane.test.ts scripts/update-aur-package.test.ts && bun test src/core/services/immersion-tracker/__tests__/query.test.ts src/core/services/immersion-tracker/__tests__/query-split-modules.test.ts && bun run tsc && bun test dist/main/runtime/registry.test.js",
|
||||
"generate:config-example": "bun run src/generate-config-example.ts",
|
||||
"verify:config-example": "bun run src/verify-config-example.ts",
|
||||
"start": "bun run build && electron . --start",
|
||||
|
||||
@@ -310,3 +310,186 @@ test('verifyPullRequestChangelog requires fragments for user-facing changes and
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
test('writePrereleaseNotesForVersion writes cumulative beta notes without mutating stable changelog artifacts', async () => {
|
||||
const { writePrereleaseNotesForVersion } = await loadModule();
|
||||
const workspace = createWorkspace('prerelease-beta-notes');
|
||||
const projectRoot = path.join(workspace, 'SubMiner');
|
||||
const changelogPath = path.join(projectRoot, 'CHANGELOG.md');
|
||||
const docsChangelogPath = path.join(projectRoot, 'docs-site', 'changelog.md');
|
||||
const existingChangelog = '# Changelog\n\n## v0.11.2 (2026-04-07)\n- Stable release.\n';
|
||||
const existingDocsChangelog = '# Changelog\n\n## v0.11.2 (2026-04-07)\n- Stable docs release.\n';
|
||||
|
||||
fs.mkdirSync(path.join(projectRoot, 'changes'), { recursive: true });
|
||||
fs.mkdirSync(path.join(projectRoot, 'docs-site'), { recursive: true });
|
||||
fs.writeFileSync(
|
||||
path.join(projectRoot, 'package.json'),
|
||||
JSON.stringify({ name: 'subminer', version: '0.11.3-beta.1' }, null, 2),
|
||||
'utf8',
|
||||
);
|
||||
fs.writeFileSync(changelogPath, existingChangelog, 'utf8');
|
||||
fs.writeFileSync(docsChangelogPath, existingDocsChangelog, 'utf8');
|
||||
fs.writeFileSync(
|
||||
path.join(projectRoot, 'changes', '001.md'),
|
||||
['type: added', 'area: overlay', '', '- Added prerelease coverage.'].join('\n'),
|
||||
'utf8',
|
||||
);
|
||||
fs.writeFileSync(
|
||||
path.join(projectRoot, 'changes', '002.md'),
|
||||
['type: fixed', 'area: launcher', '', '- Fixed prerelease packaging checks.'].join('\n'),
|
||||
'utf8',
|
||||
);
|
||||
|
||||
try {
|
||||
const outputPath = writePrereleaseNotesForVersion({
|
||||
cwd: projectRoot,
|
||||
version: '0.11.3-beta.1',
|
||||
});
|
||||
|
||||
assert.equal(outputPath, path.join(projectRoot, 'release', 'prerelease-notes.md'));
|
||||
assert.equal(
|
||||
fs.readFileSync(changelogPath, 'utf8'),
|
||||
existingChangelog,
|
||||
'stable CHANGELOG.md should remain unchanged',
|
||||
);
|
||||
assert.equal(
|
||||
fs.readFileSync(docsChangelogPath, 'utf8'),
|
||||
existingDocsChangelog,
|
||||
'docs-site changelog should remain unchanged',
|
||||
);
|
||||
assert.equal(fs.existsSync(path.join(projectRoot, 'changes', '001.md')), true);
|
||||
assert.equal(fs.existsSync(path.join(projectRoot, 'changes', '002.md')), true);
|
||||
|
||||
const prereleaseNotes = fs.readFileSync(outputPath, 'utf8');
|
||||
assert.match(prereleaseNotes, /^> This is a prerelease build for testing\./m);
|
||||
assert.match(prereleaseNotes, /## Highlights\n### Added\n- Overlay: Added prerelease coverage\./);
|
||||
assert.match(
|
||||
prereleaseNotes,
|
||||
/### Fixed\n- Launcher: Fixed prerelease packaging checks\./,
|
||||
);
|
||||
assert.match(prereleaseNotes, /## Installation\n\nSee the README and docs\/installation guide/);
|
||||
} finally {
|
||||
fs.rmSync(workspace, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
test('writePrereleaseNotesForVersion supports rc prereleases', async () => {
|
||||
const { writePrereleaseNotesForVersion } = await loadModule();
|
||||
const workspace = createWorkspace('prerelease-rc-notes');
|
||||
const projectRoot = path.join(workspace, 'SubMiner');
|
||||
|
||||
fs.mkdirSync(path.join(projectRoot, 'changes'), { recursive: true });
|
||||
fs.writeFileSync(
|
||||
path.join(projectRoot, 'package.json'),
|
||||
JSON.stringify({ name: 'subminer', version: '0.11.3-rc.1' }, null, 2),
|
||||
'utf8',
|
||||
);
|
||||
fs.writeFileSync(
|
||||
path.join(projectRoot, 'changes', '001.md'),
|
||||
['type: changed', 'area: release', '', '- Prepared release candidate notes.'].join('\n'),
|
||||
'utf8',
|
||||
);
|
||||
|
||||
try {
|
||||
const outputPath = writePrereleaseNotesForVersion({
|
||||
cwd: projectRoot,
|
||||
version: '0.11.3-rc.1',
|
||||
});
|
||||
|
||||
const prereleaseNotes = fs.readFileSync(outputPath, 'utf8');
|
||||
assert.match(
|
||||
prereleaseNotes,
|
||||
/## Highlights\n### Changed\n- Release: Prepared release candidate notes\./,
|
||||
);
|
||||
} finally {
|
||||
fs.rmSync(workspace, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
test('writePrereleaseNotesForVersion rejects unsupported prerelease identifiers', async () => {
|
||||
const { writePrereleaseNotesForVersion } = await loadModule();
|
||||
const workspace = createWorkspace('prerelease-alpha-reject');
|
||||
const projectRoot = path.join(workspace, 'SubMiner');
|
||||
|
||||
fs.mkdirSync(path.join(projectRoot, 'changes'), { recursive: true });
|
||||
fs.writeFileSync(
|
||||
path.join(projectRoot, 'package.json'),
|
||||
JSON.stringify({ name: 'subminer', version: '0.11.3-alpha.1' }, null, 2),
|
||||
'utf8',
|
||||
);
|
||||
fs.writeFileSync(
|
||||
path.join(projectRoot, 'changes', '001.md'),
|
||||
['type: added', 'area: overlay', '', '- Unsupported alpha prerelease.'].join('\n'),
|
||||
'utf8',
|
||||
);
|
||||
|
||||
try {
|
||||
assert.throws(
|
||||
() =>
|
||||
writePrereleaseNotesForVersion({
|
||||
cwd: projectRoot,
|
||||
version: '0.11.3-alpha.1',
|
||||
}),
|
||||
/Unsupported prerelease version \(0\.11\.3-alpha\.1\)/,
|
||||
);
|
||||
} finally {
|
||||
fs.rmSync(workspace, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
test('writePrereleaseNotesForVersion rejects mismatched package versions', async () => {
|
||||
const { writePrereleaseNotesForVersion } = await loadModule();
|
||||
const workspace = createWorkspace('prerelease-version-mismatch');
|
||||
const projectRoot = path.join(workspace, 'SubMiner');
|
||||
|
||||
fs.mkdirSync(path.join(projectRoot, 'changes'), { recursive: true });
|
||||
fs.writeFileSync(
|
||||
path.join(projectRoot, 'package.json'),
|
||||
JSON.stringify({ name: 'subminer', version: '0.11.3-beta.1' }, null, 2),
|
||||
'utf8',
|
||||
);
|
||||
fs.writeFileSync(
|
||||
path.join(projectRoot, 'changes', '001.md'),
|
||||
['type: added', 'area: overlay', '', '- Mismatched prerelease.'].join('\n'),
|
||||
'utf8',
|
||||
);
|
||||
|
||||
try {
|
||||
assert.throws(
|
||||
() =>
|
||||
writePrereleaseNotesForVersion({
|
||||
cwd: projectRoot,
|
||||
version: '0.11.3-beta.2',
|
||||
}),
|
||||
/package\.json version \(0\.11\.3-beta\.1\) does not match requested release version \(0\.11\.3-beta\.2\)/,
|
||||
);
|
||||
} finally {
|
||||
fs.rmSync(workspace, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
test('writePrereleaseNotesForVersion rejects empty prerelease note generation when no fragments exist', async () => {
|
||||
const { writePrereleaseNotesForVersion } = await loadModule();
|
||||
const workspace = createWorkspace('prerelease-no-fragments');
|
||||
const projectRoot = path.join(workspace, 'SubMiner');
|
||||
|
||||
fs.mkdirSync(path.join(projectRoot, 'changes'), { recursive: true });
|
||||
fs.writeFileSync(
|
||||
path.join(projectRoot, 'package.json'),
|
||||
JSON.stringify({ name: 'subminer', version: '0.11.3-beta.1' }, null, 2),
|
||||
'utf8',
|
||||
);
|
||||
|
||||
try {
|
||||
assert.throws(
|
||||
() =>
|
||||
writePrereleaseNotesForVersion({
|
||||
cwd: projectRoot,
|
||||
version: '0.11.3-beta.1',
|
||||
}),
|
||||
/No changelog fragments found in changes\//,
|
||||
);
|
||||
} finally {
|
||||
fs.rmSync(workspace, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
@@ -38,6 +38,7 @@ type PullRequestChangelogOptions = {
|
||||
};
|
||||
|
||||
const RELEASE_NOTES_PATH = path.join('release', 'release-notes.md');
|
||||
const PRERELEASE_NOTES_PATH = path.join('release', 'prerelease-notes.md');
|
||||
const CHANGELOG_HEADER = '# Changelog';
|
||||
const CHANGE_TYPES: FragmentType[] = ['added', 'changed', 'fixed', 'docs', 'internal'];
|
||||
const CHANGE_TYPE_HEADINGS: Record<FragmentType, string> = {
|
||||
@@ -75,6 +76,10 @@ function resolveVersion(options: Pick<ChangelogOptions, 'cwd' | 'version' | 'dep
|
||||
return normalizeVersion(options.version ?? resolvePackageVersion(cwd, readFileSync));
|
||||
}
|
||||
|
||||
function isSupportedPrereleaseVersion(version: string): boolean {
|
||||
return /^\d+\.\d+\.\d+-(beta|rc)\.\d+$/u.test(normalizeVersion(version));
|
||||
}
|
||||
|
||||
function verifyRequestedVersionMatchesPackageVersion(
|
||||
options: Pick<ChangelogOptions, 'cwd' | 'version' | 'deps'>,
|
||||
): void {
|
||||
@@ -314,8 +319,15 @@ export function resolveChangelogOutputPaths(options?: { cwd?: string }): string[
|
||||
return [path.join(cwd, 'CHANGELOG.md')];
|
||||
}
|
||||
|
||||
function renderReleaseNotes(changes: string): string {
|
||||
function renderReleaseNotes(
|
||||
changes: string,
|
||||
options?: {
|
||||
disclaimer?: string;
|
||||
},
|
||||
): string {
|
||||
const prefix = options?.disclaimer ? [options.disclaimer, ''] : [];
|
||||
return [
|
||||
...prefix,
|
||||
'## Highlights',
|
||||
changes,
|
||||
'',
|
||||
@@ -334,13 +346,21 @@ function renderReleaseNotes(changes: string): string {
|
||||
].join('\n');
|
||||
}
|
||||
|
||||
function writeReleaseNotesFile(cwd: string, changes: string, deps?: ChangelogFsDeps): string {
|
||||
function writeReleaseNotesFile(
|
||||
cwd: string,
|
||||
changes: string,
|
||||
deps?: ChangelogFsDeps,
|
||||
options?: {
|
||||
disclaimer?: string;
|
||||
outputPath?: string;
|
||||
},
|
||||
): string {
|
||||
const mkdirSync = deps?.mkdirSync ?? fs.mkdirSync;
|
||||
const writeFileSync = deps?.writeFileSync ?? fs.writeFileSync;
|
||||
const releaseNotesPath = path.join(cwd, RELEASE_NOTES_PATH);
|
||||
const releaseNotesPath = path.join(cwd, options?.outputPath ?? RELEASE_NOTES_PATH);
|
||||
|
||||
mkdirSync(path.dirname(releaseNotesPath), { recursive: true });
|
||||
writeFileSync(releaseNotesPath, renderReleaseNotes(changes), 'utf8');
|
||||
writeFileSync(releaseNotesPath, renderReleaseNotes(changes, options), 'utf8');
|
||||
return releaseNotesPath;
|
||||
}
|
||||
|
||||
@@ -613,6 +633,30 @@ export function writeReleaseNotesForVersion(options?: ChangelogOptions): string
|
||||
return writeReleaseNotesFile(cwd, changes, options?.deps);
|
||||
}
|
||||
|
||||
export function writePrereleaseNotesForVersion(options?: ChangelogOptions): string {
|
||||
verifyRequestedVersionMatchesPackageVersion(options ?? {});
|
||||
|
||||
const cwd = options?.cwd ?? process.cwd();
|
||||
const version = resolveVersion(options ?? {});
|
||||
if (!isSupportedPrereleaseVersion(version)) {
|
||||
throw new Error(
|
||||
`Unsupported prerelease version (${version}). Expected x.y.z-beta.N or x.y.z-rc.N.`,
|
||||
);
|
||||
}
|
||||
|
||||
const fragments = readChangeFragments(cwd, options?.deps);
|
||||
if (fragments.length === 0) {
|
||||
throw new Error('No changelog fragments found in changes/.');
|
||||
}
|
||||
|
||||
const changes = renderGroupedChanges(fragments);
|
||||
return writeReleaseNotesFile(cwd, changes, options?.deps, {
|
||||
disclaimer:
|
||||
'> This is a prerelease build for testing. Stable changelog and docs-site updates remain pending until the final stable release.',
|
||||
outputPath: PRERELEASE_NOTES_PATH,
|
||||
});
|
||||
}
|
||||
|
||||
function parseCliArgs(argv: string[]): {
|
||||
baseRef?: string;
|
||||
cwd?: string;
|
||||
@@ -710,6 +754,11 @@ function main(): void {
|
||||
return;
|
||||
}
|
||||
|
||||
if (command === 'prerelease-notes') {
|
||||
writePrereleaseNotesForVersion(options);
|
||||
return;
|
||||
}
|
||||
|
||||
if (command === 'docs') {
|
||||
generateDocsChangelog(options);
|
||||
return;
|
||||
|
||||
61
src/prerelease-workflow.test.ts
Normal file
61
src/prerelease-workflow.test.ts
Normal file
@@ -0,0 +1,61 @@
|
||||
import test from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import { readFileSync } from 'node:fs';
|
||||
import { resolve } from 'node:path';
|
||||
|
||||
const prereleaseWorkflowPath = resolve(__dirname, '../.github/workflows/prerelease.yml');
|
||||
const prereleaseWorkflow = readFileSync(prereleaseWorkflowPath, 'utf8');
|
||||
const packageJsonPath = resolve(__dirname, '../package.json');
|
||||
const packageJson = JSON.parse(readFileSync(packageJsonPath, 'utf8')) as {
|
||||
scripts: Record<string, string>;
|
||||
};
|
||||
|
||||
test('prerelease workflow triggers on beta and rc tags only', () => {
|
||||
assert.match(prereleaseWorkflow, /name: Prerelease/);
|
||||
assert.match(prereleaseWorkflow, /tags:\s*\n\s*-\s*'v\*-beta\.\*'/);
|
||||
assert.match(prereleaseWorkflow, /tags:\s*\n(?:.*\n)*\s*-\s*'v\*-rc\.\*'/);
|
||||
});
|
||||
|
||||
test('package scripts expose prerelease notes generation separately from stable changelog build', () => {
|
||||
assert.equal(
|
||||
packageJson.scripts['changelog:prerelease-notes'],
|
||||
'bun run scripts/build-changelog.ts prerelease-notes',
|
||||
);
|
||||
});
|
||||
|
||||
test('prerelease workflow generates prerelease notes from pending fragments', () => {
|
||||
assert.match(prereleaseWorkflow, /bun run changelog:prerelease-notes --version/);
|
||||
assert.doesNotMatch(prereleaseWorkflow, /bun run changelog:build --version/);
|
||||
});
|
||||
|
||||
test('prerelease workflow publishes GitHub prereleases and keeps them off latest', () => {
|
||||
assert.match(prereleaseWorkflow, /gh release edit[\s\S]*--prerelease/);
|
||||
assert.match(prereleaseWorkflow, /gh release create[\s\S]*--prerelease/);
|
||||
assert.match(prereleaseWorkflow, /gh release create[\s\S]*--latest=false/);
|
||||
});
|
||||
|
||||
test('prerelease workflow builds and uploads all release platforms', () => {
|
||||
assert.match(prereleaseWorkflow, /build-linux:/);
|
||||
assert.match(prereleaseWorkflow, /build-macos:/);
|
||||
assert.match(prereleaseWorkflow, /build-windows:/);
|
||||
assert.match(prereleaseWorkflow, /name: appimage/);
|
||||
assert.match(prereleaseWorkflow, /name: macos/);
|
||||
assert.match(prereleaseWorkflow, /name: windows/);
|
||||
});
|
||||
|
||||
test('prerelease workflow publishes the same release assets as the stable workflow', () => {
|
||||
assert.match(
|
||||
prereleaseWorkflow,
|
||||
/files=\(release\/\*\.AppImage release\/\*\.dmg release\/\*\.exe release\/\*\.zip release\/\*\.tar\.gz dist\/launcher\/subminer\)/,
|
||||
);
|
||||
assert.match(
|
||||
prereleaseWorkflow,
|
||||
/artifacts=\([\s\S]*release\/\*\.exe[\s\S]*release\/SHA256SUMS\.txt[\s\S]*\)/,
|
||||
);
|
||||
});
|
||||
|
||||
test('prerelease workflow does not publish to AUR', () => {
|
||||
assert.doesNotMatch(prereleaseWorkflow, /aur-publish:/);
|
||||
assert.doesNotMatch(prereleaseWorkflow, /AUR_SSH_PRIVATE_KEY/);
|
||||
assert.doesNotMatch(prereleaseWorkflow, /scripts\/update-aur-package\.sh/);
|
||||
});
|
||||
@@ -22,6 +22,11 @@ test('publish release leaves prerelease unset so gh creates a normal release', (
|
||||
assert.ok(!releaseWorkflow.includes('--prerelease'));
|
||||
});
|
||||
|
||||
test('stable release workflow excludes prerelease beta and rc tags', () => {
|
||||
assert.match(releaseWorkflow, /tags-ignore:\s*\n\s*-\s*'v\*-beta\.\*'/);
|
||||
assert.match(releaseWorkflow, /tags-ignore:\s*\n(?:.*\n)*\s*-\s*'v\*-rc\.\*'/);
|
||||
});
|
||||
|
||||
test('publish release forces an existing draft tag release to become public', () => {
|
||||
assert.ok(releaseWorkflow.includes('--draft=false'));
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user