mirror of
https://github.com/Novattz/creamlinux-installer.git
synced 2026-01-30 23:32:49 -05:00
feat: Started on updates and workflows
This commit is contained in:
135
.github/workflows/build-release.yml
vendored
Normal file
135
.github/workflows/build-release.yml
vendored
Normal file
@@ -0,0 +1,135 @@
|
|||||||
|
name: 'Build and Release CreamLinux'
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches: ['main']
|
||||||
|
pull_request:
|
||||||
|
branches: ['main']
|
||||||
|
|
||||||
|
env:
|
||||||
|
CARGO_TERM_COLOR: always
|
||||||
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build-and-release:
|
||||||
|
strategy:
|
||||||
|
fail-fast: false
|
||||||
|
matrix:
|
||||||
|
platform: [ubuntu-latest, macos-latest, windows-latest]
|
||||||
|
|
||||||
|
runs-on: ${{ matrix.platform }}
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
fetch-depth: 0 # Required for semantic-release
|
||||||
|
|
||||||
|
- name: Set up semantic-release environment
|
||||||
|
run: |
|
||||||
|
echo "GITHUB_TOKEN=${{ secrets.GITHUB_TOKEN }}" >> $GITHUB_ENV
|
||||||
|
# More environment setup for release if needed
|
||||||
|
|
||||||
|
- name: Setup Node.js
|
||||||
|
uses: actions/setup-node@v4
|
||||||
|
with:
|
||||||
|
node-version: 20
|
||||||
|
cache: 'npm'
|
||||||
|
|
||||||
|
- name: Install frontend dependencies
|
||||||
|
run: npm ci
|
||||||
|
|
||||||
|
- name: Run ESLint
|
||||||
|
run: npm run lint
|
||||||
|
|
||||||
|
- name: Install Rust toolchain
|
||||||
|
uses: dtolnay/rust-toolchain@stable
|
||||||
|
|
||||||
|
- name: Cache Rust dependencies
|
||||||
|
uses: Swatinem/rust-cache@v2
|
||||||
|
with:
|
||||||
|
workspaces: 'src-tauri -> target'
|
||||||
|
cache-on-failure: true
|
||||||
|
|
||||||
|
# Setup platform-specific dependencies
|
||||||
|
- name: Install Linux dependencies
|
||||||
|
if: matrix.platform == 'ubuntu-latest'
|
||||||
|
run: |
|
||||||
|
sudo apt-get update
|
||||||
|
sudo apt-get install -y libgtk-3-dev libwebkit2gtk-4.1-dev libappindicator3-dev librsvg2-dev patchelf
|
||||||
|
|
||||||
|
- name: Install macOS dependencies
|
||||||
|
if: matrix.platform == 'macos-latest'
|
||||||
|
run: |
|
||||||
|
rustup target add aarch64-apple-darwin
|
||||||
|
|
||||||
|
- name: Install Windows dependencies
|
||||||
|
if: matrix.platform == 'windows-latest'
|
||||||
|
run: |
|
||||||
|
# Windows typically doesn't need additional dependencies
|
||||||
|
|
||||||
|
# Sync package version
|
||||||
|
- name: Sync Version
|
||||||
|
run: npm run sync-version
|
||||||
|
|
||||||
|
# Run semantic-release only on the ubuntu runner to avoid conflicts
|
||||||
|
- name: Semantic Release
|
||||||
|
if: matrix.platform == 'ubuntu-latest' && github.event_name == 'push' && github.ref == 'refs/heads/main'
|
||||||
|
run: npx semantic-release
|
||||||
|
env:
|
||||||
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
|
||||||
|
# Build the app with updater artifacts
|
||||||
|
- name: Build the app
|
||||||
|
run: npm run tauri build
|
||||||
|
|
||||||
|
# Upload the artifacts for each platform
|
||||||
|
- name: Upload Linux artifacts
|
||||||
|
if: matrix.platform == 'ubuntu-latest'
|
||||||
|
uses: actions/upload-artifact@v4
|
||||||
|
with:
|
||||||
|
name: creamlinux-linux
|
||||||
|
path: |
|
||||||
|
src-tauri/target/release/bundle/deb/*.deb
|
||||||
|
src-tauri/target/release/bundle/appimage/*.AppImage
|
||||||
|
src-tauri/target/release/bundle/appimage/*.AppImage.sig
|
||||||
|
|
||||||
|
- name: Upload macOS artifacts
|
||||||
|
if: matrix.platform == 'macos-latest'
|
||||||
|
uses: actions/upload-artifact@v4
|
||||||
|
with:
|
||||||
|
name: creamlinux-macos
|
||||||
|
path: |
|
||||||
|
src-tauri/target/release/bundle/macos/*.app
|
||||||
|
src-tauri/target/release/bundle/macos/*.app.tar.gz
|
||||||
|
src-tauri/target/release/bundle/macos/*.app.tar.gz.sig
|
||||||
|
src-tauri/target/release/bundle/dmg/*.dmg
|
||||||
|
|
||||||
|
- name: Upload Windows artifacts
|
||||||
|
if: matrix.platform == 'windows-latest'
|
||||||
|
uses: actions/upload-artifact@v4
|
||||||
|
with:
|
||||||
|
name: creamlinux-windows
|
||||||
|
path: |
|
||||||
|
src-tauri/target/release/bundle/msi/*.msi
|
||||||
|
src-tauri/target/release/bundle/msi/*.msi.sig
|
||||||
|
src-tauri/target/release/bundle/nsis/*.exe
|
||||||
|
src-tauri/target/release/bundle/nsis/*.exe.sig
|
||||||
|
|
||||||
|
# Generate updater JSON file (run only on ubuntu)
|
||||||
|
- name: Generate updater JSON
|
||||||
|
if: matrix.platform == 'ubuntu-latest' && github.event_name == 'push' && github.ref == 'refs/heads/main'
|
||||||
|
run: |
|
||||||
|
node scripts/generate-updater-json.js
|
||||||
|
|
||||||
|
# Create GitHub release with all artifacts
|
||||||
|
- name: Create Release
|
||||||
|
uses: softprops/action-gh-release@v1
|
||||||
|
if: matrix.platform == 'ubuntu-latest' && github.event_name == 'push' && github.ref == 'refs/heads/main'
|
||||||
|
with:
|
||||||
|
files: |
|
||||||
|
latest.json
|
||||||
|
src-tauri/target/release/bundle/**/*.{AppImage,AppImage.sig,deb,dmg,app.tar.gz,app.tar.gz.sig,msi,msi.sig,exe,exe.sig}
|
||||||
|
draft: false
|
||||||
|
prerelease: false
|
||||||
|
token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
generate_release_notes: true
|
||||||
59
.github/workflows/build.yml
vendored
59
.github/workflows/build.yml
vendored
@@ -1,59 +0,0 @@
|
|||||||
name: 'Build CreamLinux'
|
|
||||||
|
|
||||||
on:
|
|
||||||
push:
|
|
||||||
branches: ['main']
|
|
||||||
pull_request:
|
|
||||||
branches: ['main']
|
|
||||||
|
|
||||||
env:
|
|
||||||
CARGO_TERM_COLOR: always
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
build-tauri:
|
|
||||||
strategy:
|
|
||||||
fail-fast: false
|
|
||||||
matrix:
|
|
||||||
platform: [ubuntu-latest]
|
|
||||||
|
|
||||||
runs-on: ${{ matrix.platform }}
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v4
|
|
||||||
|
|
||||||
- name: Setup Node.js
|
|
||||||
uses: actions/setup-node@v4
|
|
||||||
with:
|
|
||||||
node-version: 20
|
|
||||||
cache: 'npm'
|
|
||||||
|
|
||||||
- name: Install Rust toolchain
|
|
||||||
uses: dtolnay/rust-toolchain@stable
|
|
||||||
|
|
||||||
- name: Cache Rust dependencies
|
|
||||||
uses: Swatinem/rust-cache@v2
|
|
||||||
with:
|
|
||||||
workspaces: 'src-tauri -> target'
|
|
||||||
cache-on-failure: true
|
|
||||||
|
|
||||||
- name: Install system dependencies
|
|
||||||
run: |
|
|
||||||
sudo apt-get update
|
|
||||||
sudo apt-get install -y libgtk-3-dev libwebkit2gtk-4.1-dev libappindicator3-dev librsvg2-dev patchelf
|
|
||||||
|
|
||||||
- name: Install frontend dependencies
|
|
||||||
run: npm install
|
|
||||||
|
|
||||||
- name: Run ESLint
|
|
||||||
run: npm run lint
|
|
||||||
|
|
||||||
- name: Build the app
|
|
||||||
run: npm run tauri build
|
|
||||||
|
|
||||||
- name: Upload binary artifacts
|
|
||||||
uses: actions/upload-artifact@v4
|
|
||||||
with:
|
|
||||||
name: creamlinux-${{ runner.os }}
|
|
||||||
path: |
|
|
||||||
src-tauri/target/release/creamlinux
|
|
||||||
src-tauri/target/release/bundle/deb/*.deb
|
|
||||||
src-tauri/target/release/bundle/appimage/*.AppImage
|
|
||||||
31
.releaserc.js
Normal file
31
.releaserc.js
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
module.exports = {
|
||||||
|
branches: ['main'],
|
||||||
|
plugins: [
|
||||||
|
'@semantic-release/commit-analyzer',
|
||||||
|
'@semantic-release/release-notes-generator',
|
||||||
|
'@semantic-release/changelog',
|
||||||
|
[
|
||||||
|
'@semantic-release/npm',
|
||||||
|
{
|
||||||
|
// Don't actually publish to npm
|
||||||
|
npmPublish: false,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'@semantic-release/git',
|
||||||
|
{
|
||||||
|
assets: ['package.json', 'CHANGELOG.md'],
|
||||||
|
message: 'chore(release): ${nextRelease.version} [skip ci]\n\n${nextRelease.notes}',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'@semantic-release/github',
|
||||||
|
{
|
||||||
|
assets: [
|
||||||
|
{ path: 'latest.json', label: 'Updater manifest file' },
|
||||||
|
// We don't need to specify release assets here as they're handled by the GitHub release action
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
],
|
||||||
|
}
|
||||||
6268
package-lock.json
generated
6268
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
14
package.json
14
package.json
@@ -9,10 +9,18 @@
|
|||||||
"lint": "eslint .",
|
"lint": "eslint .",
|
||||||
"preview": "vite preview",
|
"preview": "vite preview",
|
||||||
"tauri": "tauri",
|
"tauri": "tauri",
|
||||||
"optimize-svg": "node scripts/optimize-svg.js"
|
"optimize-svg": "node scripts/optimize-svg.js",
|
||||||
|
"sync-version": "node scripts/sync-version.js",
|
||||||
|
"generate-updater": "node scripts/generate-updater-json.js",
|
||||||
|
"update-tauri-config": "node scripts/update-tauri-config.js",
|
||||||
|
"update-api-changelog": "node scripts/api-changelog.js",
|
||||||
|
"prepare-release": "npm run sync-version && npm run update-api-changelog && npm run update-tauri-config",
|
||||||
|
"release": "semantic-release"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@tauri-apps/api": "^2.5.0",
|
"@tauri-apps/api": "^2.5.0",
|
||||||
|
"@tauri-apps/plugin-process": "^2.2.1",
|
||||||
|
"@tauri-apps/plugin-updater": "^2.7.1",
|
||||||
"react": "^19.0.0",
|
"react": "^19.0.0",
|
||||||
"react-dom": "^19.0.0",
|
"react-dom": "^19.0.0",
|
||||||
"sass": "^1.89.0",
|
"sass": "^1.89.0",
|
||||||
@@ -20,6 +28,9 @@
|
|||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@eslint/js": "^9.22.0",
|
"@eslint/js": "^9.22.0",
|
||||||
|
"@semantic-release/changelog": "^6.0.3",
|
||||||
|
"@semantic-release/git": "^10.0.1",
|
||||||
|
"@semantic-release/github": "^11.0.2",
|
||||||
"@svgr/core": "^8.1.0",
|
"@svgr/core": "^8.1.0",
|
||||||
"@svgr/webpack": "^8.1.0",
|
"@svgr/webpack": "^8.1.0",
|
||||||
"@tauri-apps/cli": "^2.5.0",
|
"@tauri-apps/cli": "^2.5.0",
|
||||||
@@ -32,6 +43,7 @@
|
|||||||
"eslint-plugin-react-refresh": "^0.4.19",
|
"eslint-plugin-react-refresh": "^0.4.19",
|
||||||
"globals": "^16.0.0",
|
"globals": "^16.0.0",
|
||||||
"sass-embedded": "^1.86.3",
|
"sass-embedded": "^1.86.3",
|
||||||
|
"semantic-release": "^24.2.4",
|
||||||
"typescript": "~5.7.2",
|
"typescript": "~5.7.2",
|
||||||
"typescript-eslint": "^8.26.1",
|
"typescript-eslint": "^8.26.1",
|
||||||
"vite": "^6.3.1",
|
"vite": "^6.3.1",
|
||||||
|
|||||||
215
scripts/api-changelog.js
Normal file
215
scripts/api-changelog.js
Normal file
@@ -0,0 +1,215 @@
|
|||||||
|
const fs = require('fs')
|
||||||
|
const path = require('path')
|
||||||
|
const { execSync } = require('child_process')
|
||||||
|
|
||||||
|
// Define paths
|
||||||
|
const rustFilesPath = path.join(__dirname, '..', 'src-tauri')
|
||||||
|
|
||||||
|
function getApiDefinitions() {
|
||||||
|
// Get a list of all Rust files
|
||||||
|
const rustFiles = findRustFiles(rustFilesPath)
|
||||||
|
|
||||||
|
// Extract API functions and structs from Rust files
|
||||||
|
const apiDefinitions = {
|
||||||
|
commands: [],
|
||||||
|
structs: [],
|
||||||
|
}
|
||||||
|
|
||||||
|
rustFiles.forEach((filePath) => {
|
||||||
|
const content = fs.readFileSync(filePath, 'utf8')
|
||||||
|
|
||||||
|
// Find Tauri commands (API endpoints)
|
||||||
|
const commandRegex = /#\[tauri::command\]\s+(?:pub\s+)?(?:async\s+)?fn\s+([a-zA-Z0-9_]+)/g
|
||||||
|
let match
|
||||||
|
while ((match = commandRegex.exec(content)) !== null) {
|
||||||
|
apiDefinitions.commands.push({
|
||||||
|
name: match[1],
|
||||||
|
file: path.relative(rustFilesPath, filePath),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Find structs that are likely part of the API
|
||||||
|
const structRegex = /#\[derive\(.*Serialize.*\)\]\s+(?:pub\s+)?struct\s+([a-zA-Z0-9_]+)/g
|
||||||
|
while ((match = structRegex.exec(content)) !== null) {
|
||||||
|
apiDefinitions.structs.push({
|
||||||
|
name: match[1],
|
||||||
|
file: path.relative(rustFilesPath, filePath),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
return apiDefinitions
|
||||||
|
}
|
||||||
|
|
||||||
|
function findRustFiles(dir) {
|
||||||
|
let results = []
|
||||||
|
const list = fs.readdirSync(dir)
|
||||||
|
|
||||||
|
list.forEach((file) => {
|
||||||
|
const filePath = path.join(dir, file)
|
||||||
|
const stat = fs.statSync(filePath)
|
||||||
|
|
||||||
|
if (stat && stat.isDirectory() && file !== 'target') {
|
||||||
|
// Recursively search subdirectories, but skip the 'target' directory
|
||||||
|
results = results.concat(findRustFiles(filePath))
|
||||||
|
} else if (path.extname(file) === '.rs') {
|
||||||
|
// Add Rust files to the results
|
||||||
|
results.push(filePath)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
return results
|
||||||
|
}
|
||||||
|
|
||||||
|
function compareApiDefinitions(oldApi, newApi) {
|
||||||
|
const changes = {
|
||||||
|
commands: {
|
||||||
|
added: [],
|
||||||
|
removed: [],
|
||||||
|
},
|
||||||
|
structs: {
|
||||||
|
added: [],
|
||||||
|
removed: [],
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
// Find added and removed commands
|
||||||
|
const oldCommandNames = oldApi.commands.map((cmd) => cmd.name)
|
||||||
|
const newCommandNames = newApi.commands.map((cmd) => cmd.name)
|
||||||
|
|
||||||
|
changes.commands.added = newApi.commands.filter((cmd) => !oldCommandNames.includes(cmd.name))
|
||||||
|
changes.commands.removed = oldApi.commands.filter((cmd) => !newCommandNames.includes(cmd.name))
|
||||||
|
|
||||||
|
// Find added and removed structs
|
||||||
|
const oldStructNames = oldApi.structs.map((struct) => struct.name)
|
||||||
|
const newStructNames = newApi.structs.map((struct) => struct.name)
|
||||||
|
|
||||||
|
changes.structs.added = newApi.structs.filter((struct) => !oldStructNames.includes(struct.name))
|
||||||
|
changes.structs.removed = oldApi.structs.filter((struct) => !newStructNames.includes(struct.name))
|
||||||
|
|
||||||
|
return changes
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateChangelog(changes) {
|
||||||
|
const changelogPath = path.join(__dirname, '..', 'CHANGELOG.md')
|
||||||
|
let changelog = ''
|
||||||
|
|
||||||
|
// Create changelog if it doesn't exist
|
||||||
|
if (fs.existsSync(changelogPath)) {
|
||||||
|
changelog = fs.readFileSync(changelogPath, 'utf8')
|
||||||
|
} else {
|
||||||
|
changelog =
|
||||||
|
'# Changelog\n\nAll notable changes to this project will be documented in this file.\n\n'
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get the current version and date
|
||||||
|
const packageJson = require(path.join(__dirname, '..', 'package.json'))
|
||||||
|
const version = packageJson.version
|
||||||
|
const date = new Date().toISOString().split('T')[0]
|
||||||
|
|
||||||
|
// Create the new changelog entry
|
||||||
|
let newEntry = `## [${version}] - ${date}\n\n`
|
||||||
|
|
||||||
|
if (
|
||||||
|
changes.commands.added.length > 0 ||
|
||||||
|
changes.commands.removed.length > 0 ||
|
||||||
|
changes.structs.added.length > 0 ||
|
||||||
|
changes.structs.removed.length > 0
|
||||||
|
) {
|
||||||
|
newEntry += '### API Changes\n\n'
|
||||||
|
|
||||||
|
if (changes.commands.added.length > 0) {
|
||||||
|
newEntry += '#### New Commands\n\n'
|
||||||
|
changes.commands.added.forEach((cmd) => {
|
||||||
|
newEntry += `- \`${cmd.name}\` in \`${cmd.file}\`\n`
|
||||||
|
})
|
||||||
|
newEntry += '\n'
|
||||||
|
}
|
||||||
|
|
||||||
|
if (changes.commands.removed.length > 0) {
|
||||||
|
newEntry += '#### Removed Commands\n\n'
|
||||||
|
changes.commands.removed.forEach((cmd) => {
|
||||||
|
newEntry += `- \`${cmd.name}\` (was in \`${cmd.file}\`)\n`
|
||||||
|
})
|
||||||
|
newEntry += '\n'
|
||||||
|
}
|
||||||
|
|
||||||
|
if (changes.structs.added.length > 0) {
|
||||||
|
newEntry += '#### New Structures\n\n'
|
||||||
|
changes.structs.added.forEach((struct) => {
|
||||||
|
newEntry += `- \`${struct.name}\` in \`${struct.file}\`\n`
|
||||||
|
})
|
||||||
|
newEntry += '\n'
|
||||||
|
}
|
||||||
|
|
||||||
|
if (changes.structs.removed.length > 0) {
|
||||||
|
newEntry += '#### Removed Structures\n\n'
|
||||||
|
changes.structs.removed.forEach((struct) => {
|
||||||
|
newEntry += `- \`${struct.name}\` (was in \`${struct.file}\`)\n`
|
||||||
|
})
|
||||||
|
newEntry += '\n'
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
newEntry += 'No API changes in this release.\n\n'
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add git commit information since last release
|
||||||
|
try {
|
||||||
|
// Get the latest tag
|
||||||
|
const latestTag = execSync('git describe --tags --abbrev=0', { encoding: 'utf8' }).trim()
|
||||||
|
|
||||||
|
// Get commits since the latest tag
|
||||||
|
const commits = execSync(`git log ${latestTag}..HEAD --pretty=format:"%h %s (%an)"`, {
|
||||||
|
encoding: 'utf8',
|
||||||
|
}).trim()
|
||||||
|
|
||||||
|
if (commits) {
|
||||||
|
newEntry += '### Other Changes\n\n'
|
||||||
|
|
||||||
|
// Split commits by line and add them to the changelog
|
||||||
|
commits.split('\n').forEach((commit) => {
|
||||||
|
newEntry += `- ${commit}\n`
|
||||||
|
})
|
||||||
|
|
||||||
|
newEntry += '\n'
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
// If there are no tags or other git issues, just continue without commit info
|
||||||
|
console.warn('Could not get git commit information:', error.message)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add the new entry to the changelog
|
||||||
|
if (changelog.includes('## [')) {
|
||||||
|
// Insert new entry after the first heading
|
||||||
|
changelog = changelog.replace('# Changelog\n\n', `# Changelog\n\n${newEntry}`)
|
||||||
|
} else {
|
||||||
|
// Append to the end if no existing versions
|
||||||
|
changelog += newEntry
|
||||||
|
}
|
||||||
|
|
||||||
|
// Write the updated changelog
|
||||||
|
fs.writeFileSync(changelogPath, changelog)
|
||||||
|
console.log(`Updated CHANGELOG.md with API changes for version ${version}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Main execution
|
||||||
|
try {
|
||||||
|
// Check if we have a saved API definition
|
||||||
|
const apiCachePath = path.join(__dirname, '.api-cache.json')
|
||||||
|
let oldApi = { commands: [], structs: [] }
|
||||||
|
let newApi = getApiDefinitions()
|
||||||
|
|
||||||
|
if (fs.existsSync(apiCachePath)) {
|
||||||
|
oldApi = JSON.parse(fs.readFileSync(apiCachePath, 'utf8'))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Compare and update the changelog
|
||||||
|
const changes = compareApiDefinitions(oldApi, newApi)
|
||||||
|
updateChangelog(changes)
|
||||||
|
|
||||||
|
// Save the new API definition for next time
|
||||||
|
fs.writeFileSync(apiCachePath, JSON.stringify(newApi, null, 2))
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error updating API changelog:', error)
|
||||||
|
process.exit(1)
|
||||||
|
}
|
||||||
82
scripts/generate-updater-json.js
Normal file
82
scripts/generate-updater-json.js
Normal file
@@ -0,0 +1,82 @@
|
|||||||
|
const fs = require('fs')
|
||||||
|
const path = require('path')
|
||||||
|
const { execSync } = require('child_process')
|
||||||
|
|
||||||
|
// Read the current version from package.json
|
||||||
|
const packageJson = require('../package.json')
|
||||||
|
const version = packageJson.version
|
||||||
|
console.log(`Current version: ${version}`)
|
||||||
|
|
||||||
|
// Get the current date in RFC 3339 format for pub_date
|
||||||
|
const pubDate = new Date().toISOString()
|
||||||
|
|
||||||
|
// Base URL where the assets will be available
|
||||||
|
const baseUrl = 'https://github.com/tickbase/creamlinux/releases/download'
|
||||||
|
const releaseTag = `v${version}`
|
||||||
|
const releaseUrl = `${baseUrl}/${releaseTag}`
|
||||||
|
|
||||||
|
// Create the updater JSON structure
|
||||||
|
const updaterJson = {
|
||||||
|
version,
|
||||||
|
notes: `Release version ${version}`,
|
||||||
|
pub_date: pubDate,
|
||||||
|
platforms: {
|
||||||
|
// Windows x64
|
||||||
|
'windows-x86_64': {
|
||||||
|
url: `${releaseUrl}/creamlinux-setup.exe`,
|
||||||
|
signature: readSignature('windows', 'creamlinux-setup.exe'),
|
||||||
|
},
|
||||||
|
// Linux x64
|
||||||
|
'linux-x86_64': {
|
||||||
|
url: `${releaseUrl}/creamlinux.AppImage`,
|
||||||
|
signature: readSignature('linux', 'creamlinux.AppImage'),
|
||||||
|
},
|
||||||
|
// macOS x64 and arm64 (universal)
|
||||||
|
'darwin-universal': {
|
||||||
|
url: `${releaseUrl}/creamlinux.app.tar.gz`,
|
||||||
|
signature: readSignature('macos', 'creamlinux.app.tar.gz'),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
// Write the updater JSON file
|
||||||
|
fs.writeFileSync('latest.json', JSON.stringify(updaterJson, null, 2))
|
||||||
|
console.log('Created latest.json updater file')
|
||||||
|
|
||||||
|
// Helper function to read signature files
|
||||||
|
function readSignature(platform, filename) {
|
||||||
|
try {
|
||||||
|
// Determine path based on platform
|
||||||
|
let sigPath
|
||||||
|
|
||||||
|
switch (platform) {
|
||||||
|
case 'windows':
|
||||||
|
// Check both NSIS and MSI
|
||||||
|
try {
|
||||||
|
sigPath = path.join('src-tauri', 'target', 'release', 'bundle', 'nsis', `${filename}.sig`)
|
||||||
|
return fs.readFileSync(sigPath, 'utf8').trim()
|
||||||
|
} catch (e) {
|
||||||
|
sigPath = path.join('src-tauri', 'target', 'release', 'bundle', 'msi', `${filename}.sig`)
|
||||||
|
return fs.readFileSync(sigPath, 'utf8').trim()
|
||||||
|
}
|
||||||
|
case 'linux':
|
||||||
|
sigPath = path.join(
|
||||||
|
'src-tauri',
|
||||||
|
'target',
|
||||||
|
'release',
|
||||||
|
'bundle',
|
||||||
|
'appimage',
|
||||||
|
`${filename}.sig`
|
||||||
|
)
|
||||||
|
return fs.readFileSync(sigPath, 'utf8').trim()
|
||||||
|
case 'macos':
|
||||||
|
sigPath = path.join('src-tauri', 'target', 'release', 'bundle', 'macos', `${filename}.sig`)
|
||||||
|
return fs.readFileSync(sigPath, 'utf8').trim()
|
||||||
|
default:
|
||||||
|
throw new Error(`Unknown platform: ${platform}`)
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`Error reading signature for ${platform}/${filename}:`, error.message)
|
||||||
|
return ''
|
||||||
|
}
|
||||||
|
}
|
||||||
29
scripts/sync-version.js
Normal file
29
scripts/sync-version.js
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
const fs = require('fs')
|
||||||
|
const path = require('path')
|
||||||
|
|
||||||
|
// Read current version from package.json
|
||||||
|
const packageJsonPath = path.join(__dirname, '..', 'package.json')
|
||||||
|
const packageJson = require(packageJsonPath)
|
||||||
|
const version = packageJson.version
|
||||||
|
|
||||||
|
console.log(`Current version: ${version}`)
|
||||||
|
|
||||||
|
// Update Cargo.toml
|
||||||
|
const cargoTomlPath = path.join(__dirname, '..', 'src-tauri', 'Cargo.toml')
|
||||||
|
let cargoToml = fs.readFileSync(cargoTomlPath, 'utf8')
|
||||||
|
|
||||||
|
// Replace the version in Cargo.toml
|
||||||
|
cargoToml = cargoToml.replace(/version\s*=\s*"[^"]+"/m, `version = "${version}"`)
|
||||||
|
fs.writeFileSync(cargoTomlPath, cargoToml)
|
||||||
|
console.log(`Updated Cargo.toml version to ${version}`)
|
||||||
|
|
||||||
|
// Update tauri.conf.json
|
||||||
|
const tauriConfigPath = path.join(__dirname, '..', 'src-tauri', 'tauri.conf.json')
|
||||||
|
const tauriConfig = JSON.parse(fs.readFileSync(tauriConfigPath, 'utf8'))
|
||||||
|
|
||||||
|
// Set the version in tauri.conf.json
|
||||||
|
tauriConfig.version = version
|
||||||
|
fs.writeFileSync(tauriConfigPath, JSON.stringify(tauriConfig, null, 2))
|
||||||
|
console.log(`Updated tauri.conf.json version to ${version}`)
|
||||||
|
|
||||||
|
console.log('Version synchronization completed!')
|
||||||
42
scripts/update-tauri-config.js
Normal file
42
scripts/update-tauri-config.js
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
const fs = require('fs')
|
||||||
|
const path = require('path')
|
||||||
|
|
||||||
|
// Path to your tauri.conf.json file
|
||||||
|
const tauriConfigPath = path.join(__dirname, '..', 'src-tauri', 'tauri.conf.json')
|
||||||
|
|
||||||
|
// Read the current config
|
||||||
|
const rawConfig = fs.readFileSync(tauriConfigPath, 'utf8')
|
||||||
|
const config = JSON.parse(rawConfig)
|
||||||
|
|
||||||
|
// Add or update the updater configuration
|
||||||
|
if (!config.plugins) {
|
||||||
|
config.plugins = {}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get the public key from environment variable
|
||||||
|
const pubkey = process.env.TAURI_PUBLIC_KEY || ''
|
||||||
|
|
||||||
|
if (!pubkey) {
|
||||||
|
console.warn(
|
||||||
|
'Warning: TAURI_PUBLIC_KEY environment variable is not set. Updater will not work correctly!'
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Configure the updater plugin
|
||||||
|
config.plugins.updater = {
|
||||||
|
pubkey,
|
||||||
|
endpoints: ['https://github.com/tickbase/creamlinux/releases/latest/download/latest.json'],
|
||||||
|
}
|
||||||
|
|
||||||
|
// Configure bundle settings for updater artifacts
|
||||||
|
if (!config.bundle) {
|
||||||
|
config.bundle = {}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set createUpdaterArtifacts to true
|
||||||
|
config.bundle.createUpdaterArtifacts = true
|
||||||
|
|
||||||
|
// Write the updated config back to the file
|
||||||
|
fs.writeFileSync(tauriConfigPath, JSON.stringify(config, null, 2))
|
||||||
|
|
||||||
|
console.log('Tauri config updated with updater configuration')
|
||||||
@@ -30,6 +30,10 @@ tauri-plugin-shell = "2.0.0-rc"
|
|||||||
tauri-plugin-dialog = "2.0.0-rc"
|
tauri-plugin-dialog = "2.0.0-rc"
|
||||||
tauri-plugin-fs = "2.0.0-rc"
|
tauri-plugin-fs = "2.0.0-rc"
|
||||||
num_cpus = "1.16.0"
|
num_cpus = "1.16.0"
|
||||||
|
tauri-plugin-process = "2"
|
||||||
|
|
||||||
[features]
|
[features]
|
||||||
custom-protocol = ["tauri/custom-protocol"]
|
custom-protocol = ["tauri/custom-protocol"]
|
||||||
|
|
||||||
|
[target.'cfg(not(any(target_os = "android", target_os = "ios")))'.dependencies]
|
||||||
|
tauri-plugin-updater = "2"
|
||||||
|
|||||||
14
src-tauri/capabilities/desktop.json
Normal file
14
src-tauri/capabilities/desktop.json
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
{
|
||||||
|
"identifier": "desktop-capability",
|
||||||
|
"platforms": [
|
||||||
|
"macOS",
|
||||||
|
"windows",
|
||||||
|
"linux"
|
||||||
|
],
|
||||||
|
"windows": [
|
||||||
|
"main"
|
||||||
|
],
|
||||||
|
"permissions": [
|
||||||
|
"updater:default"
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -512,6 +512,8 @@ fn main() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
tauri::Builder::default()
|
tauri::Builder::default()
|
||||||
|
.plugin(tauri_plugin_process::init())
|
||||||
|
.plugin(tauri_plugin_updater::Builder::new().build())
|
||||||
.plugin(tauri_plugin_shell::init())
|
.plugin(tauri_plugin_shell::init())
|
||||||
.plugin(tauri_plugin_dialog::init())
|
.plugin(tauri_plugin_dialog::init())
|
||||||
.plugin(tauri_plugin_fs::init())
|
.plugin(tauri_plugin_fs::init())
|
||||||
|
|||||||
@@ -4,19 +4,25 @@
|
|||||||
"frontendDist": "../dist",
|
"frontendDist": "../dist",
|
||||||
"devUrl": "http://localhost:1420",
|
"devUrl": "http://localhost:1420",
|
||||||
"beforeDevCommand": "npm run dev",
|
"beforeDevCommand": "npm run dev",
|
||||||
"beforeBuildCommand": "npm run build"
|
"beforeBuildCommand": "npm run sync-version && npm run build"
|
||||||
},
|
},
|
||||||
"bundle": {
|
"bundle": {
|
||||||
"active": true,
|
"active": true,
|
||||||
"targets": "all",
|
"targets": "all",
|
||||||
"category": "Utility",
|
"category": "Utility",
|
||||||
|
"createUpdaterArtifacts": true,
|
||||||
"icon": ["icons/128x128.png", "icons/128x128@2x.png", "icons/icon.png"]
|
"icon": ["icons/128x128.png", "icons/128x128@2x.png", "icons/icon.png"]
|
||||||
},
|
},
|
||||||
"productName": "Creamlinux",
|
"productName": "Creamlinux",
|
||||||
"mainBinaryName": "creamlinux",
|
"mainBinaryName": "creamlinux",
|
||||||
"version": "0.1.0",
|
"version": "0.1.0",
|
||||||
"identifier": "com.creamlinux.dev",
|
"identifier": "com.creamlinux.dev",
|
||||||
"plugins": {},
|
"plugins": {
|
||||||
|
"updater": {
|
||||||
|
"pubkey": "dW50cnVzdGVkIGNvbW1lbnQ6IG1pbmlzaWduIHB1YmxpYyBrZXk6IDJDNEI1NzBBRDUxODQ3RjEKUldUeFJ4alZDbGRMTE5Vc241NG5yL080UklnaW1iUGdUWElPRXloRGtKZ3M2SWkzK0RGSDh3Q2kK",
|
||||||
|
"endpoints": ["https://github.com/Novattz/rust-gui-dev/releases/latest/download/latest.json"]
|
||||||
|
}
|
||||||
|
},
|
||||||
"app": {
|
"app": {
|
||||||
"withGlobalTauri": false,
|
"withGlobalTauri": false,
|
||||||
"windows": [
|
"windows": [
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { useAppContext } from '@/contexts/useAppContext'
|
import { useAppContext } from '@/contexts/useAppContext'
|
||||||
|
import { UpdateChecker } from '@/components/updater'
|
||||||
import { useAppLogic } from '@/hooks'
|
import { useAppLogic } from '@/hooks'
|
||||||
import './styles/main.scss'
|
import './styles/main.scss'
|
||||||
|
|
||||||
@@ -104,6 +105,7 @@ function App() {
|
|||||||
onClose={handleDlcDialogClose}
|
onClose={handleDlcDialogClose}
|
||||||
onConfirm={handleDlcConfirm}
|
onConfirm={handleDlcConfirm}
|
||||||
/>
|
/>
|
||||||
|
<UpdateChecker />
|
||||||
</div>
|
</div>
|
||||||
</ErrorBoundary>
|
</ErrorBoundary>
|
||||||
)
|
)
|
||||||
|
|||||||
147
src/components/updater/UpdateChecker.tsx
Normal file
147
src/components/updater/UpdateChecker.tsx
Normal file
@@ -0,0 +1,147 @@
|
|||||||
|
import { useState, useEffect } from 'react'
|
||||||
|
import { check, type Update, type DownloadEvent } from '@tauri-apps/plugin-updater'
|
||||||
|
import { relaunch } from '@tauri-apps/plugin-process'
|
||||||
|
import { Button } from '@/components/buttons'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* React component that checks for updates and provides
|
||||||
|
* UI for downloading and installing them
|
||||||
|
*/
|
||||||
|
const UpdateChecker = () => {
|
||||||
|
const [updateAvailable, setUpdateAvailable] = useState(false)
|
||||||
|
const [updateInfo, setUpdateInfo] = useState<Update | null>(null)
|
||||||
|
const [isChecking, setIsChecking] = useState(false)
|
||||||
|
const [isDownloading, setIsDownloading] = useState(false)
|
||||||
|
const [downloadProgress, setDownloadProgress] = useState(0)
|
||||||
|
const [error, setError] = useState<string | null>(null)
|
||||||
|
|
||||||
|
// Check for updates on component mount
|
||||||
|
useEffect(() => {
|
||||||
|
checkForUpdates()
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
const checkForUpdates = async () => {
|
||||||
|
try {
|
||||||
|
setIsChecking(true)
|
||||||
|
setError(null)
|
||||||
|
|
||||||
|
// Check for updates
|
||||||
|
const update = await check()
|
||||||
|
|
||||||
|
if (update) {
|
||||||
|
console.log(`Update available: ${update.version}`)
|
||||||
|
setUpdateAvailable(true)
|
||||||
|
setUpdateInfo(update)
|
||||||
|
} else {
|
||||||
|
console.log('No updates available')
|
||||||
|
setUpdateAvailable(false)
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Failed to check for updates:', err)
|
||||||
|
setError(`Failed to check for updates: ${err instanceof Error ? err.message : String(err)}`)
|
||||||
|
} finally {
|
||||||
|
setIsChecking(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const downloadAndInstallUpdate = async () => {
|
||||||
|
if (!updateInfo) return
|
||||||
|
|
||||||
|
try {
|
||||||
|
setIsDownloading(true)
|
||||||
|
setError(null)
|
||||||
|
|
||||||
|
let downloaded = 0
|
||||||
|
let contentLength = 0
|
||||||
|
|
||||||
|
// Download and install update
|
||||||
|
await updateInfo.downloadAndInstall((event: DownloadEvent) => {
|
||||||
|
switch (event.event) {
|
||||||
|
case 'Started':
|
||||||
|
// Started event includes contentLength
|
||||||
|
if ('contentLength' in event.data && typeof event.data.contentLength === 'number') {
|
||||||
|
contentLength = event.data.contentLength
|
||||||
|
console.log(`Started downloading ${contentLength} bytes`)
|
||||||
|
}
|
||||||
|
break
|
||||||
|
case 'Progress':
|
||||||
|
// Progress event includes chunkLength
|
||||||
|
if ('chunkLength' in event.data && typeof event.data.chunkLength === 'number' && contentLength > 0) {
|
||||||
|
downloaded += event.data.chunkLength
|
||||||
|
const progress = (downloaded / contentLength) * 100
|
||||||
|
setDownloadProgress(progress)
|
||||||
|
console.log(`Downloaded ${downloaded} from ${contentLength}`)
|
||||||
|
}
|
||||||
|
break
|
||||||
|
case 'Finished':
|
||||||
|
console.log('Download finished')
|
||||||
|
break
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
console.log('Update installed, relaunching application')
|
||||||
|
await relaunch()
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Failed to download and install update:', err)
|
||||||
|
setError(`Failed to download and install update: ${err instanceof Error ? err.message : String(err)}`)
|
||||||
|
setIsDownloading(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isChecking) {
|
||||||
|
return <div className="update-checker">Checking for updates...</div>
|
||||||
|
}
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
return (
|
||||||
|
<div className="update-checker error">
|
||||||
|
<p>{error}</p>
|
||||||
|
<Button variant="primary" onClick={checkForUpdates}>Try Again</Button>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!updateAvailable || !updateInfo) {
|
||||||
|
return null // Don't show anything if there's no update
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="update-checker">
|
||||||
|
<div className="update-info">
|
||||||
|
<h3>Update Available</h3>
|
||||||
|
<p>Version {updateInfo.version} is available to download.</p>
|
||||||
|
{updateInfo.body && <p className="update-notes">{updateInfo.body}</p>}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{isDownloading ? (
|
||||||
|
<div className="update-progress">
|
||||||
|
<div className="progress-bar-container">
|
||||||
|
<div
|
||||||
|
className="progress-bar"
|
||||||
|
style={{ width: `${downloadProgress}%` }}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<p>Downloading: {Math.round(downloadProgress)}%</p>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="update-actions">
|
||||||
|
<Button
|
||||||
|
variant="primary"
|
||||||
|
onClick={downloadAndInstallUpdate}
|
||||||
|
disabled={isDownloading}
|
||||||
|
>
|
||||||
|
Download & Install
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
variant="secondary"
|
||||||
|
onClick={() => setUpdateAvailable(false)}
|
||||||
|
>
|
||||||
|
Later
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default UpdateChecker
|
||||||
1
src/components/updater/index.ts
Normal file
1
src/components/updater/index.ts
Normal file
@@ -0,0 +1 @@
|
|||||||
|
export { default as UpdateChecker } from './UpdateChecker'
|
||||||
@@ -1 +1,2 @@
|
|||||||
@forward './loading';
|
@forward './loading';
|
||||||
|
@forward './updater';
|
||||||
|
|||||||
85
src/styles/components/common/_updater.scss
Normal file
85
src/styles/components/common/_updater.scss
Normal file
@@ -0,0 +1,85 @@
|
|||||||
|
@use '../../themes/index' as *;
|
||||||
|
@use '../../abstracts/index' as *;
|
||||||
|
|
||||||
|
/*
|
||||||
|
Update checker component styles
|
||||||
|
*/
|
||||||
|
.update-checker {
|
||||||
|
border-radius: var(--radius-md);
|
||||||
|
background-color: var(--elevated-bg);
|
||||||
|
padding: 1.25rem;
|
||||||
|
margin: 1rem 0;
|
||||||
|
border: 1px solid var(--border-soft);
|
||||||
|
box-shadow: var(--shadow-standard);
|
||||||
|
max-width: 500px;
|
||||||
|
position: fixed;
|
||||||
|
bottom: 20px;
|
||||||
|
right: 20px;
|
||||||
|
z-index: var(--z-modal) - 1;
|
||||||
|
|
||||||
|
&.error {
|
||||||
|
border-color: var(--danger);
|
||||||
|
background-color: var(--danger-soft);
|
||||||
|
}
|
||||||
|
|
||||||
|
.update-info {
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
|
||||||
|
h3 {
|
||||||
|
font-size: 1.2rem;
|
||||||
|
color: var(--primary-color);
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
|
font-weight: var(--bold);
|
||||||
|
}
|
||||||
|
|
||||||
|
p {
|
||||||
|
color: var(--text-secondary);
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.update-notes {
|
||||||
|
font-size: 0.85rem;
|
||||||
|
color: var(--text-soft);
|
||||||
|
max-height: 120px;
|
||||||
|
overflow-y: auto;
|
||||||
|
padding: 0.5rem;
|
||||||
|
background-color: rgba(0, 0, 0, 0.1);
|
||||||
|
border-radius: var(--radius-sm);
|
||||||
|
white-space: pre-line;
|
||||||
|
margin-top: 0.5rem;
|
||||||
|
@include custom-scrollbar;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.update-progress {
|
||||||
|
margin-top: 1rem;
|
||||||
|
|
||||||
|
.progress-bar-container {
|
||||||
|
height: 6px;
|
||||||
|
background-color: var(--border-soft);
|
||||||
|
border-radius: 3px;
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.progress-bar {
|
||||||
|
height: 100%;
|
||||||
|
background-color: var(--primary-color);
|
||||||
|
border-radius: 3px;
|
||||||
|
transition: width 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
p {
|
||||||
|
font-size: 0.8rem;
|
||||||
|
color: var(--text-secondary);
|
||||||
|
text-align: right;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.update-actions {
|
||||||
|
display: flex;
|
||||||
|
gap: 0.75rem;
|
||||||
|
margin-top: 1rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user