diff --git a/.github/workflows/snap.yml b/.github/workflows/snap.yml new file mode 100644 index 00000000..9681e705 --- /dev/null +++ b/.github/workflows/snap.yml @@ -0,0 +1,86 @@ +name: Snap + +on: + release: + types: + - published + workflow_dispatch: + inputs: + tag: + description: Existing GitHub release tag like v0.0.102. Leave empty for artifact-only builds. + required: false + type: string + publish: + description: Publish the built snap to the Snap Store. + required: false + type: boolean + default: false + release_channel: + description: Snap Store channel to release to. + required: false + type: choice + default: stable + options: + - stable + - candidate + - beta + - edge + +permissions: + contents: write + +jobs: + snap: + runs-on: ubuntu-latest + env: + SNAPCRAFT_STORE_CREDENTIALS: ${{ secrets.SNAPCRAFT_STORE_CREDENTIALS }} + steps: + - uses: actions/checkout@0c366fd6a839edf440554fa01a7085ccba70ac98 + + - name: Resolve release metadata + id: meta + shell: bash + run: | + if [[ "${GITHUB_EVENT_NAME}" == "release" ]]; then + TAG="${{ github.event.release.tag_name }}" + else + TAG="${{ inputs.tag }}" + fi + + VERSION="$(python -c "import json; print(json.load(open('package.json'))['version'])")" + if [[ -n "$TAG" ]]; then + VERSION="${TAG#v}" + fi + + CHANNEL="stable" + if [[ "${GITHUB_EVENT_NAME}" == "workflow_dispatch" ]]; then + CHANNEL="${{ inputs.release_channel }}" + fi + + echo "tag=$TAG" >> "$GITHUB_OUTPUT" + echo "version=$VERSION" >> "$GITHUB_OUTPUT" + echo "channel=$CHANNEL" >> "$GITHUB_OUTPUT" + echo "DORA_RELEASE_VERSION=$VERSION" >> "$GITHUB_ENV" + + - name: Build snap + uses: snapcore/action-build@v1 + id: build + + - name: Upload snap artifact + uses: actions/upload-artifact@v4 + with: + name: dora-snap-${{ steps.meta.outputs.version }} + path: ${{ steps.build.outputs.snap }} + + - name: Upload snap to GitHub release + if: ${{ steps.meta.outputs.tag != '' }} + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: gh release upload ${{ steps.meta.outputs.tag }} ${{ steps.build.outputs.snap }} --clobber + + - name: Publish snap to Snap Store + if: ${{ env.SNAPCRAFT_STORE_CREDENTIALS != '' && (github.event_name == 'release' || inputs.publish) }} + uses: snapcore/action-publish@v1 + with: + snap: ${{ steps.build.outputs.snap }} + release: ${{ steps.meta.outputs.channel }} diff --git a/.github/workflows/winget.yml b/.github/workflows/winget.yml new file mode 100644 index 00000000..2741ebc3 --- /dev/null +++ b/.github/workflows/winget.yml @@ -0,0 +1,113 @@ +name: Winget + +on: + release: + types: + - published + workflow_dispatch: + inputs: + tag: + description: Existing GitHub release tag like v0.0.102. + required: true + type: string + submit_update: + description: Submit a wingetcreate update PR. + required: false + type: boolean + default: false + +permissions: + contents: write + +jobs: + generate-manifests: + runs-on: ubuntu-latest + outputs: + tag: ${{ steps.meta.outputs.tag }} + version: ${{ steps.meta.outputs.version }} + installer_url: ${{ steps.meta.outputs.installer_url }} + steps: + - uses: actions/checkout@0c366fd6a839edf440554fa01a7085ccba70ac98 + + - name: Setup bun + uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 + + - name: Resolve release metadata + id: meta + shell: bash + run: | + if [[ "${GITHUB_EVENT_NAME}" == "release" ]]; then + TAG="${{ github.event.release.tag_name }}" + else + TAG="${{ inputs.tag }}" + fi + + VERSION="${TAG#v}" + INSTALLER_URL="https://github.com/${GITHUB_REPOSITORY}/releases/download/${TAG}/Dora_${VERSION}_x64_en-US.msi" + CHECKSUMS_URL="https://github.com/${GITHUB_REPOSITORY}/releases/download/${TAG}/checksums-windows.txt" + + echo "tag=$TAG" >> "$GITHUB_OUTPUT" + echo "version=$VERSION" >> "$GITHUB_OUTPUT" + echo "installer_url=$INSTALLER_URL" >> "$GITHUB_OUTPUT" + echo "checksums_url=$CHECKSUMS_URL" >> "$GITHUB_OUTPUT" + + - name: Download Windows checksums + run: curl --fail --location --output checksums-windows.txt "${{ steps.meta.outputs.checksums_url }}" + + - name: Generate Winget manifests + run: | + bun run release:winget -- \ + --version=${{ steps.meta.outputs.version }} \ + --installer-url=${{ steps.meta.outputs.installer_url }} \ + --checksums-file=checksums-windows.txt \ + --installer-file=Dora_${{ steps.meta.outputs.version }}_x64_en-US.msi + + - name: Archive Winget manifests + run: | + tar -czf winget-manifests-${{ steps.meta.outputs.version }}.tar.gz \ + -C packaging/winget/manifests/${{ steps.meta.outputs.version }} . + + - name: Upload Winget manifest artifact + uses: actions/upload-artifact@v4 + with: + name: winget-manifests-${{ steps.meta.outputs.version }} + path: winget-manifests-${{ steps.meta.outputs.version }}.tar.gz + + - name: Upload Winget manifests to GitHub release + if: ${{ github.event_name == 'release' }} + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: gh release upload ${{ steps.meta.outputs.tag }} winget-manifests-${{ steps.meta.outputs.version }}.tar.gz --clobber + + submit-update: + needs: generate-manifests + if: ${{ (github.event_name == 'release' && vars.WINGET_PACKAGE_READY == 'true') || (github.event_name == 'workflow_dispatch' && inputs.submit_update) }} + runs-on: windows-latest + env: + WINGET_CREATE_GITHUB_TOKEN: ${{ secrets.WINGET_CREATE_GITHUB_TOKEN }} + steps: + - name: Skip when Winget token is not configured + if: ${{ env.WINGET_CREATE_GITHUB_TOKEN == '' }} + shell: pwsh + run: | + Write-Host "WINGET_CREATE_GITHUB_TOKEN is not configured. Skipping automated Winget submission." + exit 0 + + - name: Install WingetCreate dependencies + if: ${{ env.WINGET_CREATE_GITHUB_TOKEN != '' }} + shell: pwsh + run: | + Invoke-WebRequest https://aka.ms/Microsoft.VCLibs.x64.14.00.Desktop.appx -OutFile Microsoft.VCLibs.x64.14.00.Desktop.appx + Add-AppxPackage .\Microsoft.VCLibs.x64.14.00.Desktop.appx + Invoke-WebRequest https://aka.ms/wingetcreate/latest/msixbundle -OutFile wingetcreate.msixbundle + Add-AppxPackage .\wingetcreate.msixbundle + + - name: Submit Winget update + if: ${{ env.WINGET_CREATE_GITHUB_TOKEN != '' }} + shell: pwsh + run: | + wingetcreate update RemcoStoeten.Dora ` + -u "${{ needs.generate-manifests.outputs.installer_url }}" ` + -v "${{ needs.generate-manifests.outputs.version }}" ` + -t "${env:WINGET_CREATE_GITHUB_TOKEN}" ` + --submit diff --git a/docs/distribution/one-machine-playbook.md b/docs/distribution/one-machine-playbook.md new file mode 100644 index 00000000..1ef88193 --- /dev/null +++ b/docs/distribution/one-machine-playbook.md @@ -0,0 +1,171 @@ +# One-Machine Packaging Playbook + +This is the shortest path if you only have one main machine and need to use Docker or VMs for the ecosystem-specific steps. + +## VM bootstrap on Ubuntu + +If you want the repo to manage the packaging VMs for you, start here: + +```bash +bash tools/scripts/vm-lab.sh +``` + +Or directly: + +```bash +bash tools/scripts/vm-lab.sh setup-host +bash tools/scripts/vm-lab.sh create ubuntu +bash tools/scripts/vm-lab.sh create arch +``` + +Details live in `docs/distribution/vm-lab.md`. + +## First milestone: create a real tagged release + +Package-manager publishing should start only after a real tagged release exists +and its assets are visible on the GitHub release page. + +From the main repo: + +```bash +git tag v0.1.0 +git push origin v0.1.0 +``` + +Then wait for the `Release` GitHub Actions workflow to finish. That workflow now uploads: + +- release installers +- `checksums-linux.txt` +- `checksums-windows.txt` + +Verify the release page first. If the release assets are missing, stop there and fix the release before touching Winget, AUR, or Snap. + +## Winget from a Windows VM + +Use a Windows 11 VM because `wingetcreate` is Windows-native. You need this VM +for the first public package submission only. + +### In the VM + +1. Install GitHub CLI and sign in if needed. +2. Install `wingetcreate`: + +```powershell +winget install wingetcreate +``` + +3. Create the first package submission: + +```powershell +wingetcreate new "https://github.com/remcostoeten/dora/releases/download/v0.1.0/Dora_0.1.0_x64_en-US.msi" +``` + +4. Review the detected metadata carefully. +5. Let `wingetcreate` create the PR to `microsoft/winget-pkgs`. + +### On later releases + +Use: + +```powershell +wingetcreate update RemcoStoeten.Dora --urls "https://github.com/remcostoeten/dora/releases/download/v0.1.1/Dora_0.1.1_x64_en-US.msi" +``` + +After the package exists in `microsoft/winget-pkgs`, you can stop doing that by +hand. Set `WINGET_CREATE_GITHUB_TOKEN` in GitHub Actions, add the repository +variable `WINGET_PACKAGE_READY=true`, and let `.github/workflows/winget.yml` +submit update PRs automatically from published releases. + +## AUR from Docker on your main machine + +You do not need a full Arch VM just to validate the package. + +### Generate the package files + +```bash +bun run release:aur -- \ + --version=0.1.0 \ + --checksums-file=apps/desktop/src-tauri/target/release/bundle/checksums-linux.txt \ + --appimage-file=Dora_0.1.0_amd64.AppImage +``` + +### Validate in Docker + +```bash +bash tools/scripts/test-aur-docker.sh +``` + +That spins up `archlinux:latest`, installs `base-devel`, and runs `makepkg` against `packaging/aur`. + +### Publish to the real AUR repo + +1. Create an AUR account at `aur.archlinux.org`. +2. Add your SSH key there. +3. Clone the AUR repo: + +```bash +git clone ssh://aur@aur.archlinux.org/dora-bin.git ~/code/dora-bin-aur +``` + +4. Sync the generated files: + +```bash +bash tools/scripts/sync-aur-repo.sh ~/code/dora-bin-aur +``` + +5. Commit and push: + +```bash +cd ~/code/dora-bin-aur +git add PKGBUILD .SRCINFO +git commit -m "Update dora-bin to 0.1.0" +git push +``` + +## Snap from your main machine or GitHub Actions + +Snap does not need a separate repo. + +### Local build path + +If your machine is Ubuntu or another Linux system with Snap available: + +```bash +sudo snap install snapcraft --classic +sudo /snap/bin/snapcraft pack --destructive-mode +``` + +### CI build path + +The repo now has: + +- `.github/workflows/snap.yml` +- `snap/snapcraft.yaml` + +When a GitHub release is published, the workflow builds the `.snap`, uploads it +to the release, and publishes it to the Snap Store if the store credential +secret exists. Manual dispatch still works for artifact-only or test runs. + +### Store publish path + +1. Create or log into your Snapcraft account. +2. Register the `dora` snap name. +3. Export store credentials: + +```bash +snapcraft export-login --snaps=dora \ + --acls package_access,package_push,package_update,package_release \ + exported.txt +``` + +4. Add the contents of `exported.txt` as the GitHub Actions secret + `SNAPCRAFT_STORE_CREDENTIALS`. +5. Publish a GitHub release and let `.github/workflows/snap.yml` handle the + build and store upload. + +## Recommended order + +1. Make the tagged GitHub release actually publish assets. +2. Submit Winget from a Windows VM. +3. Validate and publish AUR from Docker plus your normal host shell. +4. Register the Snap name and add `SNAPCRAFT_STORE_CREDENTIALS`. diff --git a/docs/distribution/release-guide.md b/docs/distribution/release-guide.md new file mode 100644 index 00000000..23bbd3d3 --- /dev/null +++ b/docs/distribution/release-guide.md @@ -0,0 +1,19 @@ +# Interactive Release Guide + +Run this from your normal shell, including `fish`: + +```bash +bash tools/scripts/release-guide.sh +``` + +The script gives you an interactive menu that: + +- checks branch, tag, versions, dirty worktree, and GitHub auth +- generates a release notes draft from `CHANGELOG.md` +- creates the local tag when the tree is clean +- pushes the tag +- creates the GitHub release with the generated notes +- prints the next AUR command and the one-time bootstrap steps for Winget and + Snap if those channels are not configured yet + +It intentionally refuses to create a tag while the worktree is dirty. diff --git a/docs/distribution/snap.md b/docs/distribution/snap.md new file mode 100644 index 00000000..dbf3a011 --- /dev/null +++ b/docs/distribution/snap.md @@ -0,0 +1,57 @@ +# Snap Distribution Guide + +Snap support is fully wired in-repo. The GitHub Actions workflow now builds +the snap, uploads it as a workflow artifact, uploads it to the GitHub release, +and publishes it to the Snap Store when store credentials are configured. + +## What is in the repo + +- `snap/snapcraft.yaml` +- `snap/gui/dora.desktop` +- `snap/local/launch` +- `.github/workflows/snap.yml` + +## Build locally + +On Ubuntu with Snapcraft installed: + +```bash +sudo snap install snapcraft --classic +sudo /snap/bin/snapcraft pack --destructive-mode +``` + +That should produce a `.snap` artifact in the repository root. + +The snap version is set during the build from the release tag when CI runs on a +published release. Local builds fall back to the version in `package.json`. + +## CI build and publish + +The GitHub Actions workflow at `.github/workflows/snap.yml` runs in two modes: + +- On `release.published`, it builds the snap, uploads the `.snap` file to the + GitHub release, and publishes it to the Snap Store if + `SNAPCRAFT_STORE_CREDENTIALS` exists. +- On manual dispatch, it builds the snap as an artifact. You can optionally + provide an existing release tag and turn on store publishing. + +## Required one-time setup + +You must complete the Snapcraft account setup once before GitHub Actions can +publish automatically. + +1. Create or log into your Snapcraft account. +2. Register the `dora` snap name. +3. Export store credentials with the required ACLs: + +```bash +snapcraft export-login --snaps=dora \ + --acls package_access,package_push,package_update,package_release \ + exported.txt +``` + +4. Save the contents of `exported.txt` as the GitHub Actions secret + `SNAPCRAFT_STORE_CREDENTIALS`. + +After that, every published GitHub release can publish the snap to the chosen +channel without additional manual steps. diff --git a/docs/distribution/winget.md b/docs/distribution/winget.md new file mode 100644 index 00000000..030d2533 --- /dev/null +++ b/docs/distribution/winget.md @@ -0,0 +1,114 @@ +# Winget Distribution Guide + +Winget is now partly automated in-repo. Dora can generate versioned manifests on +every published GitHub release, attach those manifests to the release, and +submit update PRs with `wingetcreate` after the package has been bootstrapped in +`microsoft/winget-pkgs`. + +## Before releasing + +1. **Pick the canonical installer** – prefer the `.msi` bundle because Winget handles it cleanly. Keep the `Dora_${version}_x64_en-US.msi` naming pattern stable so a manifest can point to a predictable URL. +2. **Build and upload assets** – the GitHub release must include: + - `Dora_${version}_x64_en-US.msi` (or the defined installer) + - `Dora_${version}_x64-setup.exe` (if we keep the NSIS installer for fallback) + - `checksums.txt` that lists SHA256 hashes for every Windows artifact +3. **Publish release** – confirm that `docs/distribution/winget.md` remains aligned with the release tag (`vX.Y.Z`), because Winget manifests need the exact version string. + +## First submission + +The first public submission is still a one-time manual step. Microsoft documents +`wingetcreate new` as the starting point for creating the package entry in +`microsoft/winget-pkgs`. + +```powershell +winget install wingetcreate +wingetcreate new "https://github.com/remcostoeten/dora/releases/download/v0.1.0/Dora_0.1.0_x64_en-US.msi" +``` + +During the prompts: + +1. Confirm the detected package metadata. +2. Keep the package identifier stable. +3. Let `wingetcreate` submit the PR to `microsoft/winget-pkgs`. + +After that PR is merged, Dora's `winget.yml` workflow can handle later updates. + +## Repo-native manifest generation + +The repo contains a manifest generator and a dedicated workflow: + +- `tools/scripts/generate-winget-manifest.ts` +- `.github/workflows/winget.yml` + +On every published GitHub release, the workflow: + +1. Downloads `checksums-windows.txt` from the release. +2. Generates the three Winget manifest files. +3. Uploads a `winget-manifests-.tar.gz` artifact. +4. Uploads that manifest archive to the GitHub release. +5. Optionally runs `wingetcreate update --submit` when automation is enabled. + +You can still generate the manifests locally: + +1. Build and publish the tagged Windows release. +2. Generate the Windows checksums file from the release bundle: + +```bash +bun run release:checksums -- \ + --input-dir=apps/desktop/src-tauri/target/release/bundle \ + --output=apps/desktop/src-tauri/target/release/bundle/checksums-windows.txt \ + --extensions=.msi,.exe +``` + +3. Generate the three Winget manifest files: + +```bash +bun run release:winget -- \ + --version=0.1.0 \ + --installer-url=https://github.com/remcostoeten/dora/releases/download/v0.1.0/Dora_0.1.0_x64_en-US.msi \ + --checksums-file=apps/desktop/src-tauri/target/release/bundle/checksums-windows.txt \ + --installer-file=Dora_0.1.0_x64_en-US.msi +``` + +4. The repo writes: + - `packaging/winget/manifests//RemcoStoeten.Dora.yaml` + - `packaging/winget/manifests//RemcoStoeten.Dora.installer.yaml` + - `packaging/winget/manifests//RemcoStoeten.Dora.locale.en-US.yaml` +5. Validate locally with `winget validate --manifest ` or `winget install --manifest `. +6. Either copy the generated files into a `winget-pkgs` fork manually, or use + them as the source of truth when scripting `wingetcreate update`. + +## Automated update setup + +After the first package PR has merged, add the following repository settings: + +1. Add the GitHub Actions secret `WINGET_CREATE_GITHUB_TOKEN`. +2. Use a classic GitHub personal access token with the `public_repo` scope. +3. Add the repository variable `WINGET_PACKAGE_READY=true`. + +With that in place, each published GitHub release can submit a Winget update PR +automatically. Manual dispatch also supports a forced update submission for an +existing release tag. + +## Testing locally + +After the manifest is merged: + +1. `winget install Dora` – confirm the install succeeds and creates Start menu shortcuts. +2. `winget upgrade Dora` – test against a previous version to make sure upgrades are smooth. +3. `winget uninstall Dora` – ensure Dora is fully removed, including desktop icon and registry entries. + +## Rolling out the manifest + +1. Submit the PR to `microsoft/winget-pkgs`; `wingetcreate` can do this for you, or you can open the PR manually with the generated manifests. +2. Once the PR is merged, Winget users can run `winget install Dora` immediately. Track the manifest commit so we can update it each release. +3. Document the commands in our own README so users know how to install and keep the app updated. + +## Automating in CI + +- `release.yml` publishes `checksums-windows.txt` on every tagged Windows release. +- `winget.yml` consumes that checksum file and creates deterministic manifests. +- `winget.yml` can also submit update PRs through `wingetcreate` once the + package already exists in `microsoft/winget-pkgs`. + +The only remaining manual boundary is the very first public package submission. diff --git a/packaging/winget/README.md b/packaging/winget/README.md new file mode 100644 index 00000000..21dfe896 --- /dev/null +++ b/packaging/winget/README.md @@ -0,0 +1,65 @@ +# Winget Packaging + +This directory stores generated WinGet manifest files for Dora. The same +manifest structure is used locally and in `.github/workflows/winget.yml`. + +## First package submission + +On Windows, the simplest public submission flow is: + +```powershell +winget install wingetcreate +wingetcreate new "https://github.com/remcostoeten/dora/releases/download/v0.1.0/Dora_0.1.0_x64_en-US.msi" +``` + +That creates and can submit the PR to `microsoft/winget-pkgs`. + +After that package exists, the repo workflow can submit later updates +automatically with `wingetcreate update`. + +## Generate manifests + +Use the script after a tagged Windows release exists and you have the MSI URL plus SHA256 hash: + +```bash +bun run release:winget -- \ + --version=0.1.0 \ + --installer-url=https://github.com/remcostoeten/dora/releases/download/v0.1.0/Dora_0.1.0_x64_en-US.msi \ + --checksums-file=apps/desktop/src-tauri/target/release/bundle/checksums-windows.txt \ + --installer-file=Dora_0.1.0_x64_en-US.msi +``` + +The command writes three manifest files into `packaging/winget/manifests//`: + +- version manifest +- default locale manifest +- installer manifest + +The GitHub Actions workflow also archives those files into +`winget-manifests-.tar.gz` and attaches that archive to the published +GitHub release. + +## Validate locally + +On Windows, validate the generated manifest folder with: + +```powershell +winget validate --manifest .\packaging\winget\manifests\0.1.0 +``` + +Then test local installation: + +```powershell +winget settings --enable LocalManifestFiles +winget install --manifest .\packaging\winget\manifests\0.1.0 +``` + +## Enable CI submissions + +After the first package PR is merged into `microsoft/winget-pkgs`, add: + +- `WINGET_CREATE_GITHUB_TOKEN` as a repository secret +- `WINGET_PACKAGE_READY=true` as a repository variable + +That lets `.github/workflows/winget.yml` submit update PRs automatically for +published releases. diff --git a/packaging/winget/manifests/0.0.102/RemcoStoeten.Dora.installer.yaml b/packaging/winget/manifests/0.0.102/RemcoStoeten.Dora.installer.yaml new file mode 100644 index 00000000..d0693703 --- /dev/null +++ b/packaging/winget/manifests/0.0.102/RemcoStoeten.Dora.installer.yaml @@ -0,0 +1,13 @@ +# yaml-language-server: $schema=https://aka.ms/winget-manifest.installer.1.9.0.schema.json + +PackageIdentifier: RemcoStoeten.Dora +PackageVersion: 0.0.102 +InstallerType: msi +Scope: user +UpgradeBehavior: install +Installers: +- Architecture: x64 + InstallerUrl: https://github.com/remcostoeten/dora/releases/download/v0.0.102/Dora_0.0.102_x64_en-US.msi + InstallerSha256: 10DA6B55C239EAECECF02F9C0AB0CA6577BE1376C04FD1DA46B13BD0FB31A1BA +ManifestType: installer +ManifestVersion: 1.9.0 diff --git a/packaging/winget/manifests/0.0.102/RemcoStoeten.Dora.locale.en-US.yaml b/packaging/winget/manifests/0.0.102/RemcoStoeten.Dora.locale.en-US.yaml new file mode 100644 index 00000000..ceb7482f --- /dev/null +++ b/packaging/winget/manifests/0.0.102/RemcoStoeten.Dora.locale.en-US.yaml @@ -0,0 +1,21 @@ +# yaml-language-server: $schema=https://aka.ms/winget-manifest.defaultLocale.1.9.0.schema.json + +PackageIdentifier: RemcoStoeten.Dora +PackageVersion: 0.0.102 +PackageLocale: en-US +Publisher: Remco Stoeten +PublisherUrl: https://github.com/remcostoeten/dora +PublisherSupportUrl: https://github.com/remcostoeten/dora/issues +PackageName: Dora +PackageUrl: https://github.com/remcostoeten/dora +License: GPL-3.0-only +LicenseUrl: https://github.com/remcostoeten/dora/blob/master/LICENSE +ShortDescription: Dora is a desktop database studio for PostgreSQL, SQLite, and LibSQL. +Tags: +- database +- postgres +- sqlite +- tauri +ReleaseNotesUrl: https://github.com/remcostoeten/dora/releases/tag/v0.0.102 +ManifestType: defaultLocale +ManifestVersion: 1.9.0 diff --git a/packaging/winget/manifests/0.0.102/RemcoStoeten.Dora.yaml b/packaging/winget/manifests/0.0.102/RemcoStoeten.Dora.yaml new file mode 100644 index 00000000..9aaf7baf --- /dev/null +++ b/packaging/winget/manifests/0.0.102/RemcoStoeten.Dora.yaml @@ -0,0 +1,7 @@ +# yaml-language-server: $schema=https://aka.ms/winget-manifest.version.1.9.0.schema.json + +PackageIdentifier: RemcoStoeten.Dora +PackageVersion: 0.0.102 +DefaultLocale: en-US +ManifestType: version +ManifestVersion: 1.9.0 diff --git a/snap/snapcraft.yaml b/snap/snapcraft.yaml new file mode 100644 index 00000000..38cddbce --- /dev/null +++ b/snap/snapcraft.yaml @@ -0,0 +1,87 @@ +name: dora +base: core22 +version: '0.0.102' +summary: Clean, fast desktop database studio +description: | + Dora is a cross-platform desktop database studio built with Tauri, Rust, and React. + It supports PostgreSQL, SQLite, and LibSQL/Turso in a native desktop workflow. +grade: stable +confinement: strict + +apps: + dora: + command: bin/launch + desktop: meta/gui/dora.desktop + plugs: + - desktop + - desktop-legacy + - home + - network + - opengl + - password-manager-service + - removable-media + - ssh-keys + - wayland + - x11 + environment: + WEBKIT_DISABLE_DMABUF_RENDERER: '1' + +parts: + dora: + plugin: nil + source: . + build-snaps: + - node/22/stable + build-packages: + - build-essential + - curl + - file + - libayatana-appindicator3-dev + - libgtk-3-dev + - libjavascriptcoregtk-4.1-dev + - librsvg2-dev + - libsoup-3.0-dev + - libssl-dev + - libwebkit2gtk-4.1-dev + - patchelf + - pkg-config + stage-packages: + - libasound2 + - libayatana-appindicator3-1 + - libgtk-3-0 + - libjavascriptcoregtk-4.1-0 + - libnotify4 + - libnss3 + - librsvg2-2 + - libsecret-1-0 + - libsoup-3.0-0 + - libssl3 + - libwebkit2gtk-4.1-0 + - libxss1 + - zlib1g + override-build: | + set -eux + + export HOME="$CRAFT_PART_BUILD/home" + export BUN_INSTALL="$HOME/.bun" + export CARGO_HOME="$HOME/.cargo" + export RUSTUP_HOME="$HOME/.rustup" + export PATH="$BUN_INSTALL/bin:$CARGO_HOME/bin:$PATH" + export PATH="/snap/node/current/bin:$PATH" + + mkdir -p "$BUN_INSTALL" "$CARGO_HOME" "$RUSTUP_HOME" + + curl -fsSL https://bun.sh/install | bash + curl https://sh.rustup.rs -sSf | sh -s -- -y --profile minimal + + export DORA_RELEASE_VERSION="${DORA_RELEASE_VERSION:-$(node -p "require('./package.json').version")}" + craftctl set version="$DORA_RELEASE_VERSION" + + bun install + (cd apps/desktop && node node_modules/vite/bin/vite.js build) + cargo build --manifest-path apps/desktop/src-tauri/Cargo.toml --release + + install -Dm755 apps/desktop/src-tauri/target/release/app "$CRAFT_PART_INSTALL/bin/dora" + install -Dm755 snap/local/launch "$CRAFT_PART_INSTALL/bin/launch" + install -Dm644 snap/gui/dora.desktop "$CRAFT_PART_INSTALL/meta/gui/dora.desktop" + install -Dm644 apps/desktop/src-tauri/icons/icon.png "$CRAFT_PART_INSTALL/meta/gui/icon.png" diff --git a/tools/scripts/generate-winget-manifest.ts b/tools/scripts/generate-winget-manifest.ts new file mode 100644 index 00000000..df08d2ca --- /dev/null +++ b/tools/scripts/generate-winget-manifest.ts @@ -0,0 +1,183 @@ +import fs from 'fs' +import path from 'path' +import { logHeader, logKeyValue, logLevel } from './_shared' + +type Config = { + version: string + tag: string + installerUrl: string + installerSha256: string + installerFile: string + outDir: string + packageIdentifier: string + publisher: string + publisherUrl: string + packageName: string + shortDescription: string + license: string + licenseUrl: string + manifestVersion: string + installerType: string +} + +function getFlagValue(flag: string): string | undefined { + const prefix = `--${flag}=` + const arg = process.argv.find((value) => value.startsWith(prefix)) + return arg ? arg.slice(prefix.length) : undefined +} + +function requireFlag(flag: string): string { + const value = getFlagValue(flag) + if (!value) { + throw new Error(`Missing required flag --${flag}=...`) + } + return value +} + +function writeFile(outputPath: string, contents: string) { + fs.mkdirSync(path.dirname(outputPath), { recursive: true }) + fs.writeFileSync(outputPath, contents) + logLevel('success', `Wrote ${outputPath}`) +} + +function readChecksumFromFile(checksumsPath: string, installerFile: string): string { + const contents = fs.readFileSync(checksumsPath, 'utf8') + const lines = contents + .split(/\r?\n/) + .map((line) => line.trim()) + .filter(Boolean) + + for (const line of lines) { + const [hash, relativePath] = line.split(/\s{2,}/) + if (!hash || !relativePath) { + continue + } + + if (relativePath === installerFile || relativePath.endsWith(`/${installerFile}`)) { + return hash.toUpperCase() + } + } + + throw new Error(`Could not find ${installerFile} in ${checksumsPath}`) +} + +function createVersionManifest(config: Config): string { + return `# yaml-language-server: $schema=https://aka.ms/winget-manifest.version.${config.manifestVersion}.schema.json + +PackageIdentifier: ${config.packageIdentifier} +PackageVersion: ${config.version} +DefaultLocale: en-US +ManifestType: version +ManifestVersion: ${config.manifestVersion} +` +} + +function createLocaleManifest(config: Config): string { + return `# yaml-language-server: $schema=https://aka.ms/winget-manifest.defaultLocale.${config.manifestVersion}.schema.json + +PackageIdentifier: ${config.packageIdentifier} +PackageVersion: ${config.version} +PackageLocale: en-US +Publisher: ${config.publisher} +PublisherUrl: ${config.publisherUrl} +PublisherSupportUrl: ${config.publisherUrl}/issues +PackageName: ${config.packageName} +PackageUrl: ${config.publisherUrl} +License: ${config.license} +LicenseUrl: ${config.licenseUrl} +ShortDescription: ${config.shortDescription} +Tags: +- database +- postgres +- sqlite +- tauri +ReleaseNotesUrl: ${config.publisherUrl}/releases/tag/${config.tag} +ManifestType: defaultLocale +ManifestVersion: ${config.manifestVersion} +` +} + +function createInstallerManifest(config: Config): string { + return `# yaml-language-server: $schema=https://aka.ms/winget-manifest.installer.${config.manifestVersion}.schema.json + +PackageIdentifier: ${config.packageIdentifier} +PackageVersion: ${config.version} +InstallerType: ${config.installerType} +Scope: user +UpgradeBehavior: install +Installers: +- Architecture: x64 + InstallerUrl: ${config.installerUrl} + InstallerSha256: ${config.installerSha256} +ManifestType: installer +ManifestVersion: ${config.manifestVersion} +` +} + +function main() { + const version = requireFlag('version') + const installerFile = getFlagValue('installer-file') + const checksumsFile = getFlagValue('checksums-file') + const explicitSha = getFlagValue('installer-sha256') + + if (!explicitSha && (!checksumsFile || !installerFile)) { + throw new Error( + 'Provide --installer-sha256=... or both --checksums-file=... and --installer-file=...' + ) + } + + const resolvedInstallerFile = + installerFile || + decodeURIComponent(new URL(requireFlag('installer-url')).pathname.split('/').pop() || '') + + const config: Config = { + version, + tag: getFlagValue('tag') || `v${version}`, + installerUrl: requireFlag('installer-url'), + installerSha256: explicitSha + ? explicitSha.toUpperCase() + : readChecksumFromFile(path.resolve(checksumsFile as string), resolvedInstallerFile), + installerFile: resolvedInstallerFile, + outDir: path.resolve( + getFlagValue('out-dir') || path.join('packaging', 'winget', 'manifests', version) + ), + packageIdentifier: getFlagValue('package-identifier') || 'RemcoStoeten.Dora', + publisher: getFlagValue('publisher') || 'Remco Stoeten', + publisherUrl: getFlagValue('publisher-url') || 'https://github.com/remcostoeten/dora', + packageName: getFlagValue('package-name') || 'Dora', + shortDescription: + getFlagValue('short-description') || + 'Dora is a desktop database studio for PostgreSQL, SQLite, and LibSQL.', + license: getFlagValue('license') || 'GPL-3.0-only', + licenseUrl: + getFlagValue('license-url') || + 'https://github.com/remcostoeten/dora/blob/master/LICENSE', + manifestVersion: getFlagValue('manifest-version') || '1.9.0', + installerType: getFlagValue('installer-type') || 'msi' + } + + logHeader('Generating Winget Manifest') + logKeyValue('Version', config.version) + logKeyValue('Tag', config.tag) + logKeyValue('Installer File', config.installerFile) + logKeyValue('Installer URL', config.installerUrl) + logKeyValue('Output', config.outDir) + + const baseName = config.packageIdentifier + writeFile(path.join(config.outDir, `${baseName}.yaml`), createVersionManifest(config)) + writeFile( + path.join(config.outDir, `${baseName}.locale.en-US.yaml`), + createLocaleManifest(config) + ) + writeFile( + path.join(config.outDir, `${baseName}.installer.yaml`), + createInstallerManifest(config) + ) +} + +try { + main() +} catch (error) { + logLevel('error', error instanceof Error ? error.message : String(error)) + process.exit(1) +}