diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..f6df77c --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,110 @@ +name: Release + +on: + push: + tags: + - "v*.*.*" + workflow_dispatch: + inputs: + tag: + description: "Existing release tag to publish, for example v0.1.0" + required: true + type: string + draft: + description: "Create the GitHub release as a draft" + required: true + default: true + type: boolean + prerelease: + description: "Mark the GitHub release as a prerelease" + required: true + default: false + type: boolean + +permissions: + contents: write + +concurrency: + group: release-${{ github.event_name == 'workflow_dispatch' && inputs.tag || github.ref_name }} + cancel-in-progress: false + +env: + CARGO_TERM_COLOR: always + RELEASE_TAG: ${{ github.event_name == 'workflow_dispatch' && inputs.tag || github.ref_name }} + RELEASE_DRAFT: ${{ github.event_name == 'workflow_dispatch' && format('{0}', inputs.draft) || 'false' }} + RELEASE_PRERELEASE: ${{ github.event_name == 'workflow_dispatch' && format('{0}', inputs.prerelease) || format('{0}', contains(github.ref_name, '-')) }} + +jobs: + publish-tauri: + name: Build ${{ matrix.name }} + runs-on: ${{ matrix.os }} + strategy: + fail-fast: false + matrix: + include: + - name: Windows + os: windows-latest + rust_targets: "" + args: "" + - name: Linux + os: ubuntu-22.04 + rust_targets: "" + args: "" + - name: macOS arm64 + os: macos-latest + rust_targets: aarch64-apple-darwin,x86_64-apple-darwin + args: --target aarch64-apple-darwin + - name: macOS x64 + os: macos-latest + rust_targets: aarch64-apple-darwin,x86_64-apple-darwin + args: --target x86_64-apple-darwin + + steps: + - name: Checkout release tag + uses: actions/checkout@v4 + with: + ref: ${{ env.RELEASE_TAG }} + fetch-depth: 0 + + - name: Install Rust + uses: dtolnay/rust-toolchain@stable + with: + targets: ${{ matrix.rust_targets }} + + - name: Install Node.js + uses: actions/setup-node@v4 + with: + node-version: 20 + cache: npm + cache-dependency-path: inspectra-gui/package-lock.json + + - name: Install Linux dependencies + if: runner.os == 'Linux' + run: | + sudo apt-get update + sudo apt-get install -y \ + libgtk-3-dev \ + libwebkit2gtk-4.0-dev \ + libayatana-appindicator3-dev \ + librsvg2-dev \ + patchelf + + - name: Install frontend dependencies + working-directory: inspectra-gui + run: npm ci + + - name: Build and publish desktop bundles + uses: tauri-apps/tauri-action@v0.6.2 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + with: + projectPath: inspectra-gui + args: ${{ matrix.args }} + tagName: ${{ env.RELEASE_TAG }} + releaseName: Inspectra ${{ env.RELEASE_TAG }} + releaseBody: | + Automated Inspectra desktop release. + + Built from `${{ github.sha }}`. See `CHANGELOG.md` for release notes. + releaseDraft: ${{ env.RELEASE_DRAFT }} + prerelease: ${{ env.RELEASE_PRERELEASE }} diff --git a/README.md b/README.md index 7cc709d..a66ad42 100644 --- a/README.md +++ b/README.md @@ -98,6 +98,7 @@ if chrome: - **[API Documentation](docs/API.md)**: Complete API reference - **[Architecture](docs/ARCHITECTURE.md)**: System design and architecture - **[Development Guide](docs/DEVELOPMENT.md)**: Contributing and development workflow +- **[Release Process](docs/RELEASE.md)**: Versioning, installers, and GitHub Releases - **[Roadmap](docs/ROADMAP.md)**: Future plans and features ## ๐Ÿ—๏ธ Project Structure @@ -144,6 +145,14 @@ pip install maturin maturin develop ``` +### Desktop Releases + +Inspectra desktop releases are built automatically by GitHub Actions when a +`vX.Y.Z` tag is pushed. The release workflow builds the Tauri app for Windows, +Linux and macOS and uploads the generated bundles to GitHub Releases. + +See [docs/RELEASE.md](docs/RELEASE.md) for the full release procedure. + ## ๐Ÿงช Examples ### List Processes @@ -251,4 +260,3 @@ If you find Inspectra useful, please consider: For private security reports and sensitive issues: kevin.gregoire@nodasys.com For general issues, use GitHub Issues once the repository is public. - diff --git a/docs/RELEASE.md b/docs/RELEASE.md new file mode 100644 index 0000000..54baf17 --- /dev/null +++ b/docs/RELEASE.md @@ -0,0 +1,89 @@ +# Release Process + +Inspectra publishes desktop builds through GitHub Actions and GitHub Releases. + +The release workflow builds the Tauri desktop app on Windows, Linux and macOS, +then uploads the generated bundles to the matching GitHub Release. + +## Release artifacts + +The workflow is triggered by tags named `vX.Y.Z`, for example `v0.2.0`. + +Expected artifacts: + +- Windows installer/bundle generated by Tauri. +- Linux bundles generated by Tauri, usually AppImage and Debian packages. +- macOS bundles generated by Tauri for Apple Silicon and Intel, usually + DMG/app archives. + +Windows and macOS artifacts are currently unsigned. This is acceptable for early +internal releases, but public production releases should add Authenticode +signing on Windows and Apple Developer ID signing/notarization on macOS. + +## Prepare a release + +Run the version preparation script from the repository root: + +```powershell +node scripts/prepare-release.mjs 0.2.0 +``` + +The script synchronizes the version in: + +- `Cargo.toml` +- `inspectra-gui/src-tauri/Cargo.toml` +- `inspectra-gui/src-tauri/tauri.conf.json` +- `inspectra-gui/package.json` +- `inspectra-gui/package-lock.json` +- `bindings/python/pyproject.toml` + +Review and test locally: + +```powershell +cargo test -p inspectra-core +cd inspectra-gui +npm ci +npm run build +cd .. +``` + +Commit, tag and push: + +```powershell +git add Cargo.toml inspectra-gui bindings/python/pyproject.toml +git commit -m "Release v0.2.0" +git tag v0.2.0 +git push origin main v0.2.0 +``` + +Pushing the tag starts `.github/workflows/release.yml`. For normal tags, the +release is published automatically. Tags containing a prerelease suffix such as +`v0.2.0-beta.1` are marked as prereleases. + +## Manual release dispatch + +If a tag already exists and the release needs to be rebuilt, run the `Release` +workflow manually from GitHub Actions and provide the existing tag. Manual runs +default to draft releases so artifacts can be inspected before publishing. + +## GitHub settings + +The repository must allow GitHub Actions to write releases: + +1. Open repository settings. +2. Go to `Actions` > `General`. +3. Set workflow permissions to `Read and write permissions`. + +## Future auto-updates + +The next professional step is to enable the Tauri updater after the release +pipeline is stable: + +- Generate and protect the Tauri updater signing key. +- Store the private key and password in GitHub Actions secrets. +- Add the updater config to `inspectra-gui/src-tauri/tauri.conf.json`. +- Serve update metadata from either GitHub Releases or a dedicated endpoint such + as `https://updates.nodasys.com/inspectra/{{target}}/{{current_version}}`. + +GitHub Releases are enough for the first phase. A dedicated update server can be +added later without changing the release tag discipline. diff --git a/scripts/prepare-release.mjs b/scripts/prepare-release.mjs new file mode 100644 index 0000000..cf66584 --- /dev/null +++ b/scripts/prepare-release.mjs @@ -0,0 +1,129 @@ +#!/usr/bin/env node +import { readFileSync, writeFileSync } from "node:fs"; +import { dirname, resolve } from "node:path"; +import { fileURLToPath } from "node:url"; + +const args = process.argv.slice(2); +const dryRun = args.includes("--dry-run"); +const versionArg = args.find((arg) => !arg.startsWith("-")); + +if (!versionArg) { + console.error("Usage: node scripts/prepare-release.mjs [--dry-run]"); + console.error("Example: node scripts/prepare-release.mjs 0.2.0"); + process.exit(1); +} + +const version = versionArg.replace(/^v/, ""); +const tag = `v${version}`; +const semver = + /^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-[0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*)?(?:\+[0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*)?$/; + +if (!semver.test(version)) { + console.error(`Invalid semantic version: ${versionArg}`); + console.error("Expected a version like 0.2.0 or v0.2.0-beta.1."); + process.exit(1); +} + +const root = resolve(dirname(fileURLToPath(import.meta.url)), ".."); +const changes = []; + +function readProjectFile(path) { + return readFileSync(resolve(root, path), "utf8"); +} + +function writeProjectFile(path, contents) { + if (!dryRun) { + writeFileSync(resolve(root, path), contents); + } +} + +function record(path, changed) { + changes.push({ path, changed }); +} + +function updateJson(path, mutate) { + const before = readProjectFile(path); + const data = JSON.parse(before); + const beforeData = JSON.stringify(data); + mutate(data); + const hasDataChanged = beforeData !== JSON.stringify(data); + const after = `${JSON.stringify(data, null, 2)}\n`; + record(path, hasDataChanged); + if (hasDataChanged) { + writeProjectFile(path, after); + } +} + +function replaceOnce(path, pattern, replacement, label) { + const before = readProjectFile(path); + const matches = before.match(pattern); + if (!matches) { + throw new Error(`Could not find ${label} in ${path}`); + } + const after = before.replace(pattern, replacement); + record(path, before !== after); + if (before !== after) { + writeProjectFile(path, after); + } +} + +replaceOnce( + "Cargo.toml", + /(\[workspace\.package\][\s\S]*?version\s*=\s*")[^"]+(")/, + `$1${version}$2`, + "workspace package version", +); + +replaceOnce( + "inspectra-gui/src-tauri/Cargo.toml", + /(^version\s*=\s*")[^"]+(")/m, + `$1${version}$2`, + "Tauri package version", +); + +replaceOnce( + "bindings/python/pyproject.toml", + /(\[project\][\s\S]*?version\s*=\s*")[^"]+(")/, + `$1${version}$2`, + "Python package version", +); + +updateJson("inspectra-gui/src-tauri/tauri.conf.json", (data) => { + data.package.version = version; +}); + +updateJson("inspectra-gui/package.json", (data) => { + data.version = version; +}); + +updateJson("inspectra-gui/package-lock.json", (data) => { + data.version = version; + if (data.packages?.[""]) { + data.packages[""].version = version; + } +}); + +const changed = changes.filter((entry) => entry.changed); +const unchanged = changes.filter((entry) => !entry.changed); + +console.log(`${dryRun ? "Validated" : "Prepared"} Inspectra ${tag}.`); + +if (changed.length > 0) { + console.log("\nUpdated files:"); + for (const entry of changed) { + console.log(`- ${entry.path}`); + } +} + +if (unchanged.length > 0) { + console.log("\nAlready up to date:"); + for (const entry of unchanged) { + console.log(`- ${entry.path}`); + } +} + +console.log("\nNext release commands:"); +console.log("git add Cargo.toml inspectra-gui bindings/python/pyproject.toml"); +console.log(`git commit -m "Release ${tag}"`); +console.log(`git tag ${tag}`); +console.log(`git push origin main ${tag}`);