Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 14 additions & 1 deletion .github/workflows/android-apk.yml
Original file line number Diff line number Diff line change
Expand Up @@ -492,6 +492,19 @@ jobs:
"$out_dir/OpenLess_${version}_x86.apk" \
"$out_dir/OpenLess_${version}_x86_64.apk"

- name: Stage direct download aliases
if: needs.plan.outputs.is_tag_release == 'true'
working-directory: openless-all/app
env:
OPENLESS_DOWNLOAD_SOURCE_DIR: ${{ steps.stage.outputs.out_dir }}
OPENLESS_DOWNLOAD_OUTPUT_DIR: ${{ steps.stage.outputs.out_dir }}
run: |
set -euo pipefail
for arch in aarch64 armv7 i686 x86_64; do
OPENLESS_DOWNLOAD_TARGET=android OPENLESS_DOWNLOAD_ARCH="$arch" \
node scripts/stage-release-download-aliases.mjs
done

- name: Write Android updater manifests
working-directory: openless-all/app
env:
Expand All @@ -515,7 +528,7 @@ jobs:
out_dir="${{ steps.stage.outputs.out_dir }}"
{
echo "${{ steps.stage.outputs.release_files }}"
for f in "$out_dir"/*.apk.sig "$out_dir"/latest-android-*.json; do
for f in "$out_dir"/*.apk.sig "$out_dir"/latest-android-*.json "$out_dir"/OpenLess_android_*.apk; do
[ -e "$f" ] && echo "$f"
done
} > "$RUNNER_TEMP/android-release-files.txt"
Expand Down
24 changes: 24 additions & 0 deletions .github/workflows/check-linux-egui.yml
Original file line number Diff line number Diff line change
Expand Up @@ -158,6 +158,22 @@ jobs:
( cd "$OUTPUT" && sha256sum ./*.deb ./*.rpm > SHA256SUMS )
bash scripts/verify-linux-egui-packages.sh

- name: Stage direct download aliases
if: inputs.scope != 'none' && (github.event_name != 'push' || env.RELEASE_TAG != '')
shell: bash
run: |
set -euo pipefail
output=openless-all/app/target/linux-egui-packages
alias_dir="$RUNNER_TEMP/openless-linux-egui-direct-download"
rm -rf "$alias_dir"
mkdir -p "$alias_dir"
debs=("$output"/OpenLess-Linux-egui-*.deb)
rpms=("$output"/OpenLess-Linux-egui-*.rpm)
test "${#debs[@]}" -eq 1
test "${#rpms[@]}" -eq 1
cp "${debs[0]}" "$alias_dir/OpenLess_linux_x86_64.deb"
cp "${rpms[0]}" "$alias_dir/OpenLess_linux_x86_64.rpm"

- name: Upload verified Linux packages as workflow artifacts
# Push builds keep the release path warm but do not publish artifacts;
# the tag release and pull request runs are the consumers.
Expand All @@ -167,3 +183,11 @@ jobs:
name: openless-linux-egui-x86_64
path: openless-all/app/target/linux-egui-packages/*
if-no-files-found: error

- name: Upload direct download aliases
if: inputs.scope != 'none' && (github.event_name != 'push' || env.RELEASE_TAG != '')
uses: actions/upload-artifact@v7
with:
name: openless-linux-egui-direct-download
path: ${{ runner.temp }}/openless-linux-egui-direct-download/*
if-no-files-found: error
11 changes: 11 additions & 0 deletions .github/workflows/release-tauri.yml
Original file line number Diff line number Diff line change
Expand Up @@ -535,6 +535,16 @@ jobs:
fi
done

- name: Stage direct download alias
if: startsWith(github.ref, 'refs/tags/v') && endsWith(github.ref, '-tauri')
working-directory: openless-all/app
env:
OPENLESS_DOWNLOAD_TARGET: ${{ matrix.updater-target }}
OPENLESS_DOWNLOAD_ARCH: ${{ matrix.updater-arch }}
OPENLESS_DOWNLOAD_SOURCE_DIR: ${{ matrix.updater-target == 'darwin' && 'src-tauri/target/release/bundle/dmg' || 'src-tauri/target/release/bundle/nsis' }}
OPENLESS_DOWNLOAD_OUTPUT_DIR: src-tauri/target/release/downloads
run: node scripts/stage-release-download-aliases.mjs

- name: Upload macOS artifacts
if: startsWith(matrix.platform, 'macos')
uses: actions/upload-artifact@v4
Expand Down Expand Up @@ -648,3 +658,4 @@ jobs:
openless-all/app/src-tauri/target/release/bundle/msi/*.msi
openless-all/app/src-tauri/target/release/bundle/msi/*.msi.sig
openless-all/app/src-tauri/target/release/bundle/latest-*.json
openless-all/app/src-tauri/target/release/downloads/*
6 changes: 3 additions & 3 deletions .github/workflows/sync-homebrew-cask.yml
Original file line number Diff line number Diff line change
Expand Up @@ -72,12 +72,12 @@ jobs:
mkdir -p "$RUNNER_TEMP/dmg"
gh release download "$TAG" --repo "$GITHUB_REPOSITORY" \
--dir "$RUNNER_TEMP/dmg" \
--pattern '*aarch64.dmg' --pattern '*_x64.dmg'
--pattern 'OpenLess_[0-9]*_aarch64.dmg' --pattern 'OpenLess_[0-9]*_x64.dmg'

# Tags omit +build metadata; filenames and the cask must preserve it.
version="$(node openless-all/app/scripts/resolve-release-cask-version.mjs "$TAG" "$RUNNER_TEMP/dmg")"
arm_sha="$(sha256sum "$RUNNER_TEMP"/dmg/*aarch64.dmg | cut -d' ' -f1)"
intel_sha="$(sha256sum "$RUNNER_TEMP"/dmg/*_x64.dmg | cut -d' ' -f1)"
arm_sha="$(sha256sum "$RUNNER_TEMP/dmg/OpenLess_${version}_aarch64.dmg" | cut -d' ' -f1)"
intel_sha="$(sha256sum "$RUNNER_TEMP/dmg/OpenLess_${version}_x64.dmg" | cut -d' ' -f1)"

{
echo "version=$version"
Expand Down
16 changes: 16 additions & 0 deletions RELEASING.md
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,22 @@ from the last `-N` suffix (for example `2.0.0-Beta.2-79` → `2.0.0-Beta.2-80`).
Both local packaging and the shared CI/tag build use this explicit revision; never drop
it or derive it from the Tauri tag, whose SemVer remains `2.0.0-Beta.2`.

## Direct download aliases

The release workflows also publish stable, version-independent asset names for the official website:

- macOS Apple Silicon: `OpenLess_macos_aarch64.dmg`
- macOS Intel: `OpenLess_macos_x86_64.dmg`
- Windows x64: `OpenLess_windows_x86_64-setup.exe`
- Android: `OpenLess_android_<abi>.apk` (one each for `arm64-v8a`, `armeabi-v7a`, `x86`, and `x86_64`)
- Linux: `OpenLess_linux_x86_64.deb` and `OpenLess_linux_x86_64.rpm`

Stable website links use `https://github.com/Open-Less/openless/releases/latest/download/<asset-name>`.
Beta links must use the release tag because `latest` excludes pre-releases:
`https://github.com/Open-Less/openless/releases/download/<tag>/<asset-name>`.

The Tauri and Android workflows attach their aliases automatically. Linux remains administrator-attached after product acceptance, so the administrator must attach the two files from the `openless-linux-egui-direct-download` Actions artifact to the accepted Release as well as the versioned packages. Do not replace the versioned assets; the aliases are an additional website-facing contract.

Under the [2026-09-06 2.0 requirements](docs/2.0-requirements.md), Windows and macOS must fully retain their respective Tauri 1.x features. The egui team owns Linux Host/UI work and Linux product acceptance. Linux application gaps do not block the Windows/macOS delivery, but shared Core defects do. Linux packages must not be attached before their own product acceptance. Existing Android builds do not expand this scope into a new full-support commitment.

## Version-sync gate
Expand Down
1 change: 1 addition & 0 deletions openless-all/app/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@
"check:windows-ui-config": "node scripts/windows-ui-config.test.mjs",
"check:pin-persistence-security": "node scripts/pin-persistence-security-contract.test.mjs",
"check:android-updater-pubkey": "node scripts/check-android-updater-pubkey.mjs",
"check:release-download-aliases": "node scripts/stage-release-download-aliases.test.mjs",
"check:android-shizuku-scaffolding": "node scripts/merge-android-shizuku-manifest.test.mjs && node scripts/patch-android-shizuku-deps.test.mjs",
"format": "prettier --write \"src/**/*.{ts,tsx,css}\" \"android/frontend/**/*.{ts,tsx}\" \"scripts/**/*.{js,mjs,sh}\" \"*.{ts,json,html,sh}\" \"assets/remote-input/*.{js,css,html}\"",
"format:check": "prettier --check \"src/**/*.{ts,tsx,css}\" \"android/frontend/**/*.{ts,tsx}\" \"scripts/**/*.{js,mjs,sh}\" \"*.{ts,json,html,sh}\" \"assets/remote-input/*.{js,css,html}\"",
Expand Down
46 changes: 46 additions & 0 deletions openless-all/app/scripts/android-apk-workflow-contract.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -148,6 +148,52 @@ assert.match(workflow, /CARGO_PROFILE_RELEASE_LTO/);
assert.match(workflow, /first ABI may compile twice|android-studio-script/);
assert.match(workflow, /publish-android-release/);
assert.match(workflow, /download-artifact/);
assert.match(workflow, /stage-release-download-aliases\.mjs/);
assert.match(workflow, /OpenLess_android_\*\.apk/);
assert.match(workflow, /Rust cache/);

// Exercise the upload list so overlapping globs cannot upload the same asset twice.
const releaseTmp = mkdtempSync(join(tmpdir(), 'openless-apk-assets-'));
try {
const directory = releaseTmp.replace(/\\/g, '/');
const version = '2.0.0-Beta.3+build.20260925';
const originals = [];
const names = ANDROID_ABI_MATRIX.flatMap(({ abi, gradleAbi }) => {
const apk = `OpenLess_${version}_${gradleAbi}.apk`;
const alias = `OpenLess_android_${gradleAbi}.apk`;
originals.push(`${directory}/${apk}`);
return [
apk,
`${apk}.sig`,
alias,
`${alias}.sig`,
`latest-android-${abi}.json`,
`latest-android-${abi}-mirror.json`,
];
});
for (const name of names) writeFileSync(join(releaseTmp, name), 'fixture');
const step = parsed.jobs['publish-android-release'].steps.find(
(step) => step.id === 'release_assets',
);
const result = spawnSync('bash', ['-s'], {
input: step.run
.replaceAll('${{ steps.stage.outputs.out_dir }}', '$RUNNER_TEMP')
.replaceAll('${{ steps.stage.outputs.release_files }}', '$RELEASE_FILES'),
env: {
...process.env,
RUNNER_TEMP: directory,
GITHUB_OUTPUT: `${directory}/output`,
RELEASE_FILES: originals.join('\n'),
},
encoding: 'utf8',
});
assert.equal(result.status, 0, result.stderr);
const assets = readFileSync(join(releaseTmp, 'android-release-files.txt'), 'utf8')
.trim()
.split(/\r?\n/);
assert.deepEqual(assets.sort(), names.map((name) => `${directory}/${name}`).sort());
} finally {
rmSync(releaseTmp, { recursive: true, force: true });
}

console.log('android-apk-workflow-contract checks passed');
3 changes: 3 additions & 0 deletions openless-all/app/scripts/linux-egui-release-contract.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,9 @@ assert.match(check, /cargo build --locked --release -p openless-linux-egui/);
assert.match(check, /bash scripts\/package-linux-egui\.sh/);
assert.match(check, /sha256sum \.\/\*\.deb \.\/\*\.rpm > SHA256SUMS/);
assert.match(check, /bash scripts\/verify-linux-egui-packages\.sh/);
assert.match(check, /OpenLess_linux_x86_64\.deb/);
assert.match(check, /OpenLess_linux_x86_64\.rpm/);
assert.match(check, /openless-linux-egui-direct-download/);
assert.match(gate, /cargo tree --locked/);
assert.match(gate, /tauri\|wry\|webkit2gtk/);
assert.doesNotMatch(ci + check, /libgtk-3|webkit2gtk|libwebkit/);
Expand Down
26 changes: 26 additions & 0 deletions openless-all/app/scripts/resolve-release-cask-version.test.mjs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import assert from 'node:assert/strict';
import { readFileSync } from 'node:fs';
import { spawnSync } from 'node:child_process';
import { resolveReleaseCaskVersion } from './resolve-release-cask-version.mjs';

const files = (version) => [`OpenLess_${version}_aarch64.dmg`, `OpenLess_${version}_x64.dmg`];
Expand Down Expand Up @@ -54,4 +55,29 @@ assert.match(
workflow,
/version="\$\(node openless-all\/app\/scripts\/resolve-release-cask-version\.mjs "\$TAG" "\$RUNNER_TEMP\/dmg"\)"/,
);
const downloadPatterns = [...workflow.matchAll(/--pattern '([^']+)'/g)].map((match) => match[1]);
assert.equal(downloadPatterns.length, 2);
for (const version of ['1.3.18', '2.0.0+build.20261015']) {
const selected = spawnSync(
'bash',
[
'-c',
'arm=$1; intel=$2; shift 2; for name; do if [[ "$name" == $arm || "$name" == $intel ]]; then printf "%s\\n" "$name"; fi; done',
'download-patterns',
...downloadPatterns,
...files(version),
'OpenLess_macos_aarch64.dmg',
'OpenLess_macos_x86_64.dmg',
],
{ encoding: 'utf8' },
);
assert.equal(selected.status, 0, selected.stderr);
const downloaded = selected.stdout.trim().split(/\r?\n/);
assert.deepEqual(downloaded, files(version), 'Homebrew must only download versioned DMGs');
assert.equal(resolveReleaseCaskVersion(`v${version.split('+')[0]}-tauri`, downloaded), version);
const checksumNames = [...workflow.matchAll(/sha256sum "\$RUNNER_TEMP\/dmg\/([^"]+)"/g)].map(
(match) => match[1].replaceAll('${version}', version),
);
assert.deepEqual(checksumNames, files(version), 'Homebrew must checksum the resolved version');
}
console.log('resolve-release-cask-version.test.mjs passed');
118 changes: 118 additions & 0 deletions openless-all/app/scripts/stage-release-download-aliases.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
#!/usr/bin/env node
import { copyFileSync, existsSync, mkdirSync, readdirSync } from 'node:fs';
import { basename, join, resolve } from 'node:path';
import process from 'node:process';

const TARGETS = new Set(['darwin', 'windows', 'android']);
const ANDROID_ABIS = {
aarch64: 'arm64-v8a',
armv7: 'armeabi-v7a',
i686: 'x86',
x86_64: 'x86_64',
};
const DARWIN_BUNDLE_ARCHES = {
aarch64: 'aarch64',
x86_64: 'x64',
};

function readOption(name) {
const index = process.argv.indexOf(`--${name}`);
if (index !== -1) {
const value = process.argv[index + 1];
if (!value || value.startsWith('--')) {
throw new Error(`Missing value for --${name}`);
}
return value;
}
return process.env[`OPENLESS_DOWNLOAD_${name.replaceAll('-', '_').toUpperCase()}`];
}

const target = readOption('target');
const arch = readOption('arch');
const sourceDirValue = readOption('source-dir');
const outputDirValue = readOption('output-dir');

if (!TARGETS.has(target)) {
throw new Error(`Invalid --target: ${target || '(missing)'}`);
}
if (!arch) {
throw new Error('--arch is required');
}
if (!sourceDirValue || !outputDirValue) {
throw new Error('--source-dir and --output-dir are required');
}

const sourceDir = resolve(sourceDirValue);
const outputDir = resolve(outputDirValue);

function chooseSingle(predicate, description) {
if (!existsSync(sourceDir)) {
throw new Error(`Source directory does not exist: ${sourceDir}`);
}
const matches = readdirSync(sourceDir)
.filter((name) => predicate(name))
.sort();
if (matches.length !== 1) {
throw new Error(
`Expected exactly one ${description} in ${sourceDir}, found ${matches.length}: ${matches.join(', ') || '(none)'}`,
);
}
return matches[0];
}

function sourceAndAlias() {
if (target === 'darwin') {
if (!['aarch64', 'x86_64'].includes(arch)) {
throw new Error(`Invalid macOS arch: ${arch}`);
}
const bundleArch = DARWIN_BUNDLE_ARCHES[arch];
const sourceName = chooseSingle(
(name) => name.endsWith(`_${bundleArch}.dmg`) && !name.startsWith('OpenLess_macos_'),
`macOS ${arch} DMG`,
);
return {
sourceName,
aliasName: `OpenLess_macos_${arch}.dmg`,
};
}

if (target === 'windows') {
if (arch !== 'x86_64') {
throw new Error(`Invalid Windows arch: ${arch}`);
}
const sourceName = chooseSingle(
(name) => name.endsWith('_x64-setup.exe') && !name.startsWith('OpenLess_windows_'),
'Windows x64 NSIS installer',
);
return {
sourceName,
aliasName: 'OpenLess_windows_x86_64-setup.exe',
};
}

const abi = ANDROID_ABIS[arch];
if (!abi) {
throw new Error(`Invalid Android updater arch: ${arch}`);
}
const sourceName = chooseSingle(
(name) => name.endsWith(`_${abi}.apk`) && !name.startsWith('OpenLess_android_'),
`Android ${abi} APK`,
);
return {
sourceName,
aliasName: `OpenLess_android_${abi}.apk`,
};
}

const { sourceName, aliasName } = sourceAndAlias();
mkdirSync(outputDir, { recursive: true });
const sourcePath = join(sourceDir, sourceName);
const aliasPath = join(outputDir, aliasName);
copyFileSync(sourcePath, aliasPath);

const signaturePath = `${sourcePath}.sig`;
if (existsSync(signaturePath)) {
copyFileSync(signaturePath, `${aliasPath}.sig`);
}

console.log(`Staged ${basename(sourcePath)} as ${basename(aliasPath)}`);
Loading
Loading