icebear0828 commited on
Commit
1562152
Β·
1 Parent(s): 325c8ea

feat: add version bump workflow + release pipeline tests

Browse files

- bump-electron.yml: auto bump both root + electron version on master
push, replacing the old sync-electron.yml version management
- release-pipeline.test.ts: validates full build chain (core β†’ desktop β†’
esbuild β†’ prepare-pack) and CI workflow file references

.github/workflows/bump-electron.yml ADDED
@@ -0,0 +1,83 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ name: Bump Electron version
2
+
3
+ on:
4
+ push:
5
+ branches: [master]
6
+ paths-ignore:
7
+ - "packages/electron/package.json"
8
+ - "*.md"
9
+ workflow_dispatch:
10
+
11
+ concurrency:
12
+ group: bump-electron
13
+ cancel-in-progress: false
14
+
15
+ permissions:
16
+ contents: write
17
+
18
+ jobs:
19
+ bump:
20
+ runs-on: ubuntu-latest
21
+ steps:
22
+ - uses: actions/checkout@v4
23
+ with:
24
+ fetch-depth: 0
25
+
26
+ - name: Check for new commits since last tag
27
+ id: check
28
+ run: |
29
+ LAST_TAG=$(git tag --sort=-v:refname | grep '^v' | head -1 || echo "")
30
+ if [ -z "$LAST_TAG" ]; then
31
+ echo "No previous tag, skipping"
32
+ echo "skip=true" >> "$GITHUB_OUTPUT"
33
+ exit 0
34
+ fi
35
+
36
+ # Count non-merge, non-chore commits since last tag
37
+ NEW_COMMITS=$(git log "${LAST_TAG}..HEAD" --no-merges \
38
+ --pretty=format:"%s" | grep -cvE "^(chore|docs|ci)" || true)
39
+ if [ "$NEW_COMMITS" -eq 0 ]; then
40
+ echo "No meaningful commits since $LAST_TAG, skipping"
41
+ echo "skip=true" >> "$GITHUB_OUTPUT"
42
+ exit 0
43
+ fi
44
+
45
+ echo "Found $NEW_COMMITS new commit(s) since $LAST_TAG"
46
+ echo "skip=false" >> "$GITHUB_OUTPUT"
47
+ echo "last_tag=$LAST_TAG" >> "$GITHUB_OUTPUT"
48
+
49
+ - name: Bump version + tag
50
+ if: steps.check.outputs.skip != 'true'
51
+ run: |
52
+ git config user.name "github-actions[bot]"
53
+ git config user.email "github-actions[bot]@users.noreply.github.com"
54
+
55
+ LAST_TAG="${{ steps.check.outputs.last_tag }}"
56
+ CURRENT="${LAST_TAG#v}"
57
+ IFS='.' read -r MAJOR MINOR PATCH <<< "$CURRENT"
58
+ NEW_PATCH=$((PATCH + 1))
59
+ NEW_VERSION="${MAJOR}.${MINOR}.${NEW_PATCH}"
60
+ NEW_TAG="v${NEW_VERSION}"
61
+
62
+ # Check tag doesn't already exist
63
+ if git rev-parse "$NEW_TAG" >/dev/null 2>&1; then
64
+ echo "Tag $NEW_TAG already exists, skipping"
65
+ exit 0
66
+ fi
67
+
68
+ # Bump both root and electron package versions
69
+ node -e "
70
+ const fs = require('fs');
71
+ for (const p of ['package.json', 'packages/electron/package.json']) {
72
+ const pkg = JSON.parse(fs.readFileSync(p, 'utf-8'));
73
+ pkg.version = '${NEW_VERSION}';
74
+ fs.writeFileSync(p, JSON.stringify(pkg, null, 2) + '\n');
75
+ }
76
+ "
77
+
78
+ git add package.json packages/electron/package.json
79
+ git commit -m "chore: bump version to ${NEW_VERSION} [skip ci]"
80
+ git tag -a "$NEW_TAG" -m "Release ${NEW_TAG}"
81
+ git push origin master --follow-tags
82
+
83
+ echo "Bumped to $NEW_VERSION, tagged $NEW_TAG"
packages/electron/__tests__/release-pipeline.test.ts ADDED
@@ -0,0 +1,113 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /**
2
+ * Release pipeline validation.
3
+ *
4
+ * Verifies the full build chain works end-to-end without actually
5
+ * running electron-builder (which downloads 100MB+ of Electron).
6
+ * Tests the sequence: core build β†’ desktop build β†’ esbuild β†’ prepare-pack.
7
+ */
8
+
9
+ import { describe, it, expect, afterAll } from "vitest";
10
+ import { existsSync, rmSync, readFileSync, statSync } from "fs";
11
+ import { resolve } from "path";
12
+ import { execFileSync } from "child_process";
13
+
14
+ const PKG_DIR = resolve(import.meta.dirname, "..");
15
+ const ROOT_DIR = resolve(PKG_DIR, "..", "..");
16
+ const DIST_ELECTRON = resolve(PKG_DIR, "dist-electron");
17
+
18
+ describe("release pipeline", () => {
19
+ afterAll(() => {
20
+ // Clean up build artifacts
21
+ if (existsSync(DIST_ELECTRON)) rmSync(DIST_ELECTRON, { recursive: true });
22
+ // Clean up prepare-pack copies
23
+ try {
24
+ execFileSync("node", ["electron/prepare-pack.mjs", "--clean"], {
25
+ cwd: PKG_DIR,
26
+ });
27
+ } catch { /* ignore */ }
28
+ });
29
+
30
+ it("core build produces web assets", () => {
31
+ // Core should already be built (npm run build runs in CI before tests)
32
+ const publicDir = resolve(ROOT_DIR, "public");
33
+ const indexHtml = resolve(publicDir, "index.html");
34
+ expect(existsSync(publicDir)).toBe(true);
35
+ expect(existsSync(indexHtml)).toBe(true);
36
+ });
37
+
38
+ it("esbuild produces valid server bundle", () => {
39
+ execFileSync("node", ["electron/build.mjs"], {
40
+ cwd: PKG_DIR,
41
+ timeout: 30_000,
42
+ });
43
+
44
+ const serverMjs = resolve(DIST_ELECTRON, "server.mjs");
45
+ expect(existsSync(serverMjs)).toBe(true);
46
+ // Server bundle should be substantial (includes all deps)
47
+ expect(statSync(serverMjs).size).toBeGreaterThan(100_000);
48
+ });
49
+
50
+ it("esbuild produces valid main process bundle", () => {
51
+ const mainCjs = resolve(DIST_ELECTRON, "main.cjs");
52
+ expect(existsSync(mainCjs)).toBe(true);
53
+ // Main bundle is smaller (only Electron main process code)
54
+ expect(statSync(mainCjs).size).toBeGreaterThan(1000);
55
+ });
56
+
57
+ it("prepare-pack copies all required resources", () => {
58
+ execFileSync("node", ["electron/prepare-pack.mjs"], {
59
+ cwd: PKG_DIR,
60
+ timeout: 10_000,
61
+ });
62
+
63
+ // Verify all resources are in place for electron-builder
64
+ expect(existsSync(resolve(PKG_DIR, "config", "default.yaml"))).toBe(true);
65
+ expect(existsSync(resolve(PKG_DIR, "public", "index.html"))).toBe(true);
66
+ expect(existsSync(resolve(PKG_DIR, "bin"))).toBe(true);
67
+ expect(existsSync(resolve(PKG_DIR, "dist-electron", "main.cjs"))).toBe(true);
68
+ expect(existsSync(resolve(PKG_DIR, "dist-electron", "server.mjs"))).toBe(true);
69
+ expect(existsSync(resolve(PKG_DIR, "electron", "assets", "icon.png"))).toBe(true);
70
+ expect(existsSync(resolve(PKG_DIR, "package.json"))).toBe(true);
71
+ });
72
+
73
+ it("version is consistent between root and electron package", () => {
74
+ const rootPkg = JSON.parse(
75
+ readFileSync(resolve(ROOT_DIR, "package.json"), "utf-8"),
76
+ ) as { version: string };
77
+ const electronPkg = JSON.parse(
78
+ readFileSync(resolve(PKG_DIR, "package.json"), "utf-8"),
79
+ ) as { version: string };
80
+
81
+ // Versions may diverge (electron can be ahead), but both must be valid semver
82
+ expect(rootPkg.version).toMatch(/^\d+\.\d+\.\d+$/);
83
+ expect(electronPkg.version).toMatch(/^\d+\.\d+\.\d+$/);
84
+ });
85
+
86
+ it("release.yml references correct workflow steps", () => {
87
+ const releaseYml = readFileSync(
88
+ resolve(ROOT_DIR, ".github", "workflows", "release.yml"),
89
+ "utf-8",
90
+ );
91
+
92
+ // Must include workspace-aware build steps
93
+ expect(releaseYml).toContain("packages/electron");
94
+ expect(releaseYml).toContain("electron/build.mjs");
95
+ expect(releaseYml).toContain("prepare-pack.mjs");
96
+ expect(releaseYml).toContain("electron-builder");
97
+ });
98
+
99
+ it("bump-electron.yml workflow exists", () => {
100
+ const bumpYml = resolve(
101
+ ROOT_DIR,
102
+ ".github",
103
+ "workflows",
104
+ "bump-electron.yml",
105
+ );
106
+ expect(existsSync(bumpYml)).toBe(true);
107
+
108
+ const content = readFileSync(bumpYml, "utf-8");
109
+ // Must bump both root and electron package versions
110
+ expect(content).toContain("package.json");
111
+ expect(content).toContain("packages/electron/package.json");
112
+ });
113
+ });