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
9 changes: 9 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,12 @@ jobs:
steps:
- uses: actions/checkout@v5

- name: Setup Node.js
uses: actions/setup-node@v5
with:
node-version-file: package.json
package-manager-cache: false

- name: Reject CRLF in shell and deploy assets
run: |
bad=$(git ls-files -z \
Expand All @@ -53,6 +59,9 @@ jobs:
done < <(git ls-files -z '*.sh' '*.bash')
exit "$rc"

- name: Verify release and version-generation contracts
run: node --test scripts/tauri-release-manifest.test.mjs scripts/linux-binaries-manifest.test.mjs scripts/version-generation.test.mjs

- name: Verify minisign download fallback
run: |
set -euo pipefail
Expand Down
27 changes: 24 additions & 3 deletions .github/workflows/desktop-package.yml
Original file line number Diff line number Diff line change
Expand Up @@ -233,6 +233,9 @@ jobs:
- name: Install dependencies
run: pnpm install --frozen-lockfile

- name: Verify release version metadata
run: node scripts/verify-release-version-sync.mjs --version "${{ needs.prepare.outputs.version }}"

- name: Build desktop app
run: ${{ matrix.platform.build_command }}

Expand Down Expand Up @@ -505,6 +508,20 @@ jobs:
echo "Relay image descriptor:"
find relay-image-assets -type f | sort

- name: Prepare versioned Windows installer
run: |
node scripts/prepare-windows-installer-asset.mjs \
--assets-dir release-assets \
--version "${{ needs.prepare.outputs.version }}" \
--out-dir release-manual-assets

- name: Sign versioned Windows installer
shell: bash
env:
BITFUN_SIGNING_KEY: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY }}
BITFUN_SIGNING_PASSWORD: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY_PASSWORD }}
run: bash scripts/sign-release-assets.sh release-manual-assets/*.exe

- name: Collect updater assets
run: |
node scripts/collect-tauri-updater-assets.mjs \
Expand All @@ -517,6 +534,7 @@ jobs:
run: |
node scripts/generate-tauri-latest-json.mjs \
--assets-dir release-updater-assets \
--manual-assets-dir release-manual-assets \
--version "${{ needs.prepare.outputs.version }}" \
--tag "${{ needs.prepare.outputs.release_tag }}" \
--repo "GCWing/BitFun" \
Expand All @@ -528,7 +546,8 @@ jobs:
node scripts/verify-tauri-latest-json.mjs \
--manifest release-updater-assets/latest.json \
--version "${{ needs.prepare.outputs.version }}" \
--required-platforms "${REQUIRED_UPDATER_PLATFORMS}"
--required-platforms "${REQUIRED_UPDATER_PLATFORMS}" \
--required-manual-platforms "windows-x86_64"

- name: Generate Linux binaries manifest
run: |
Expand Down Expand Up @@ -556,7 +575,7 @@ jobs:
mapfile -t assets < <(
find release-assets -type f \
\( -name '*.AppImage' -o -name '*.deb' -o -name '*.rpm' \
-o -name '*.dmg' -o -name '*bitfun-installer.exe' \) | sort
-o -name '*.dmg' \) | sort
)
if [[ "${#assets[@]}" -eq 0 ]]; then
echo "No installer packages found to sign."
Expand All @@ -575,11 +594,12 @@ jobs:
generate_release_notes: true
files: |
release-updater-assets/*
release-manual-assets/*.exe
release-manual-assets/*.exe.sig
release-assets/**/*.AppImage
release-assets/**/*.deb
release-assets/**/*.dmg
release-assets/**/*.rpm
release-assets/**/*bitfun-installer.exe
release-assets/**/*.sig
release-assets/minisign.pub
linux-release-assets/bitfun-cli-*.tar.gz
Expand All @@ -602,6 +622,7 @@ jobs:
--manifest latest.published.json \
--version "${{ needs.prepare.outputs.version }}" \
--required-platforms "${REQUIRED_UPDATER_PLATFORMS}" \
--required-manual-platforms "windows-x86_64" \
--check-urls true

- name: Verify published Linux binaries manifest
Expand Down
27 changes: 23 additions & 4 deletions .github/workflows/nightly.yml
Original file line number Diff line number Diff line change
Expand Up @@ -180,13 +180,21 @@ jobs:
const pkg = JSON.parse(fs.readFileSync('package.json', 'utf-8'));
pkg.version = process.env.NIGHTLY_VERSION.split('+')[0];
fs.writeFileSync('package.json', JSON.stringify(pkg, null, 2) + '\n');
for (const file of ['BitFun-Installer/package.json', 'BitFun-Installer/package-lock.json']) {
const data = JSON.parse(fs.readFileSync(file, 'utf-8'));
data.version = pkg.version;
if (data.packages?.['']) data.packages[''].version = pkg.version;
fs.writeFileSync(file, JSON.stringify(data, null, 2) + '\n');
}
"

# Patch Cargo workspace version (semver: nightly suffix uses hyphen)
# Cargo.toml only accepts: MAJOR.MINOR.PATCH or MAJOR.MINOR.PATCH-PRE
CARGO_VERSION="$(echo "$NIGHTLY_VERSION" | sed 's/+.*//')"
sed -i.bak "s/^version = \".*\" # x-release-please-version/version = \"${CARGO_VERSION}\" # x-release-please-version/" Cargo.toml
rm -f Cargo.toml.bak
sed -i.bak "0,/^version = \".*\"/s//version = \"${CARGO_VERSION}\"/" BitFun-Installer/src-tauri/Cargo.toml
rm -f BitFun-Installer/src-tauri/Cargo.toml.bak

echo "package.json version: $(jq -r '.version' package.json)"
echo "Cargo.toml version: $(grep 'x-release-please-version' Cargo.toml)"
Expand Down Expand Up @@ -418,6 +426,15 @@ jobs:
--repo "GCWing/BitFun" \
--out linux-release-assets/linux-binaries.json

- name: Prepare versioned Windows installer
env:
NIGHTLY_VERSION: ${{ needs.check-changes.outputs.nightly_version }}
run: |
node scripts/prepare-windows-installer-asset.mjs \
--assets-dir release-assets \
--version "${NIGHTLY_VERSION%%+*}" \
--out-dir release-manual-assets

# The Tauri bundler signs the five updater artifacts during `tauri build`,
# but the installers people download by hand from the release page — dmg,
# deb, rpm, the Windows installer and the direct AppImages — shipped with
Expand All @@ -432,11 +449,12 @@ jobs:
BITFUN_SIGNING_PUBKEY: ${{ secrets.TAURI_UPDATER_PUBKEY }}
run: |
set -euo pipefail
mapfile -t assets < <(
mapfile -t assets < <({
find release-assets -type f \
\( -name '*.AppImage' -o -name '*.deb' -o -name '*.rpm' \
-o -name '*.dmg' -o -name '*bitfun-installer.exe' \) | sort
)
-o -name '*.dmg' \)
find release-manual-assets -type f -name '*.exe'
} | sort)
if [[ "${#assets[@]}" -eq 0 ]]; then
echo "No installer packages found to sign."
exit 0
Expand Down Expand Up @@ -466,8 +484,9 @@ jobs:
release-assets/**/*.deb
release-assets/**/*.dmg
release-assets/**/*.rpm
release-assets/**/*bitfun-installer.exe
release-assets/**/*.sig
release-manual-assets/*.exe
release-manual-assets/*.exe.sig
release-assets/minisign.pub
release-assets/**/bitfun-cli-*-apple-darwin.tar.gz
release-assets/**/bitfun-cli-*-apple-darwin.tar.gz.sha256
Expand Down
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,8 @@
"copy-monaco": "copyfiles -u 5 \"src/web-ui/node_modules/monaco-editor/min/vs/**/*\" src/web-ui/public/monaco-editor && node scripts/prune-monaco-nls.cjs",
"copy-icons": "copyfiles -f \"src/apps/desktop/icons/Logo-ICON.png\" \"src/web-ui/public/\"",
"copy-assets": "pnpm run copy-monaco && pnpm run copy-icons",
"generate-version": "node scripts/generate-version.cjs",
"generate-version": "node scripts/generate-version.cjs --build-env production",
"generate-version:dev": "node scripts/generate-version.cjs --build-env development",
"generate-startup-appearance-bootstrap": "node scripts/generate-startup-appearance-bootstrap.mjs",
"generate-all": "pnpm run generate-version && pnpm run generate-startup-appearance-bootstrap",
"postinstall": "pnpm run copy-assets",
Expand Down
2 changes: 1 addition & 1 deletion scripts/desktop-tauri-build.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -216,7 +216,7 @@ export function prepareTauriConfig(
endpoints: [primaryEndpoint, fallbackEndpoint],
pubkey,
windows: {
installMode: 'passive',
installMode: 'quiet',
},
},
};
Expand Down
33 changes: 33 additions & 0 deletions scripts/desktop-tauri-build.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,39 @@ test('Desktop Tauri projection consumes only the resolved member identity', () =
}
});

test('Windows updater installs NSIS packages without showing its progress window', () => {
const fixture = join(tmpdir(), `bitfun-tauri-updater-${process.pid}-${Date.now()}`);
const baseConfig = join(fixture, 'tauri.conf.json');
const updaterEnv = {
BITFUN_ENABLE_UPDATER_ARTIFACTS: process.env.BITFUN_ENABLE_UPDATER_ARTIFACTS,
TAURI_SIGNING_PRIVATE_KEY: process.env.TAURI_SIGNING_PRIVATE_KEY,
TAURI_UPDATER_PUBKEY: process.env.TAURI_UPDATER_PUBKEY,
};
mkdirSync(fixture, { recursive: true });
writeFileSync(baseConfig, JSON.stringify({ bundle: { resources: {} } }));
process.env.BITFUN_ENABLE_UPDATER_ARTIFACTS = 'true';
process.env.TAURI_SIGNING_PRIVATE_KEY = 'test-private-key';
process.env.TAURI_UPDATER_PUBKEY = 'test-public-key';

try {
const generated = prepareTauriConfig(baseConfig, {
desktopDir: fixture,
flashgrepBinary: join(fixture, 'flashgrep'),
});
const config = JSON.parse(readFileSync(generated, 'utf8'));
assert.equal(config.plugins.updater.windows.installMode, 'quiet');
} finally {
for (const [name, value] of Object.entries(updaterEnv)) {
if (value === undefined) {
delete process.env[name];
} else {
process.env[name] = value;
}
}
rmSync(fixture, { force: true, recursive: true });
}
});

test('Desktop release config bundles models.dev notices and provenance', () => {
const config = JSON.parse(
readFileSync(join(ROOT, 'src', 'apps', 'desktop', 'tauri.conf.json'), 'utf8')
Expand Down
6 changes: 5 additions & 1 deletion scripts/dev.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -666,7 +666,11 @@ async function main() {
},
{
name: 'Generate version info',
promise: runCommandPrefixed('version', 'node', ['scripts/generate-version.cjs']),
promise: runCommandPrefixed('version', 'node', [
'scripts/generate-version.cjs',
'--build-env',
'development',
]),
},
];

Expand Down
17 changes: 17 additions & 0 deletions scripts/generate-tauri-latest-json.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ const tag = requireArg(args, 'tag');
const repo = requireArg(args, 'repo');
const out = requireArg(args, 'out');
const requiredPlatforms = parseListArg(args['required-platforms'] || '');
const manualAssetsDir = args['manual-assets-dir'];

if (!existsSync(assetsDir)) {
fail(`Assets directory does not exist: ${assetsDir}`);
Expand Down Expand Up @@ -55,6 +56,22 @@ const manifest = {
platforms,
};

if (manualAssetsDir) {
const installerName = `BitFun_${version}_windows-x86_64-installer.exe`;
const installerPath = join(manualAssetsDir, installerName);
const signaturePath = `${installerPath}.sig`;
if (!existsSync(installerPath) || !existsSync(signaturePath)) {
fail(`Missing signed manual installer pair: ${installerPath} and ${signaturePath}`);
}
const assetUrl = `https://github.com/${repo}/releases/download/${encodeURIComponent(tag)}/${encodeURIComponent(installerName)}`;
manifest.manual_installers = {
'windows-x86_64': {
url: assetUrl,
signature_url: `${assetUrl}.sig`,
},
};
}

mkdirSync(dirname(out), { recursive: true });
writeFileSync(out, `${JSON.stringify(manifest, null, 2)}\n`, 'utf8');
console.log(`[latest-json] Wrote ${out}`);
Expand Down
41 changes: 28 additions & 13 deletions scripts/generate-version.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,20 @@ const {
const packageJsonPath = path.resolve(__dirname, '../package.json');
const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf-8'));

function parseBuildEnv(args) {
const index = args.indexOf('--build-env');
const buildEnv = index >= 0 ? args[index + 1] : undefined;
if (!['development', 'production', 'preview'].includes(buildEnv)) {
throw new Error('Expected --build-env development|production|preview');
}
return buildEnv;
}

function readArg(args, name) {
const index = args.indexOf(name);
return index >= 0 ? args[index + 1] : undefined;
}

function getGitInfo() {
try {
const gitCommitFull = execSync('git rev-parse HEAD', { encoding: 'utf-8' }).trim();
Expand All @@ -40,11 +54,10 @@ function getGitInfo() {
}
}

function generateVersionInfo() {
function generateVersionInfo(buildEnv) {
const gitInfo = getGitInfo();
const buildDate = new Date().toISOString();
const buildTimestamp = Date.now();
const buildEnv = process.env.NODE_ENV || 'development';
const isDev = buildEnv === 'development';

const versionInfo = {
Expand All @@ -60,8 +73,8 @@ function generateVersionInfo() {
return versionInfo;
}

function saveVersionInfoToJson(versionInfo) {
const outputPath = path.resolve(__dirname, '../src/web-ui/public/version.json');
function saveVersionInfoToJson(versionInfo, outputRoot) {
const outputPath = path.resolve(outputRoot, 'src/web-ui/public/version.json');

const dir = path.dirname(outputPath);
if (!fs.existsSync(dir)) {
Expand All @@ -75,8 +88,8 @@ function saveVersionInfoToJson(versionInfo) {
);
}

function saveVersionInfoToTS(versionInfo) {
const outputPath = path.resolve(__dirname, '../src/web-ui/src/generated/version.ts');
function saveVersionInfoToTS(versionInfo, outputRoot) {
const outputPath = path.resolve(outputRoot, 'src/web-ui/src/generated/version.ts');

const dir = path.dirname(outputPath);
if (!fs.existsSync(dir)) {
Expand Down Expand Up @@ -104,13 +117,16 @@ function generateHtmlInjectionScript(versionInfo) {
}

function main() {
const versionInfo = generateVersionInfo();
const args = process.argv.slice(2);
const buildEnv = parseBuildEnv(args);
const outputRoot = path.resolve(readArg(args, '--output-root') || path.resolve(__dirname, '..'));
const versionInfo = generateVersionInfo(buildEnv);

saveVersionInfoToJson(versionInfo);
saveVersionInfoToTS(versionInfo);
saveVersionInfoToJson(versionInfo, outputRoot);
saveVersionInfoToTS(versionInfo, outputRoot);

const htmlScript = generateHtmlInjectionScript(versionInfo);
const htmlScriptPath = path.resolve(__dirname, '../src/web-ui/src/generated/version-injection.html');
const htmlScriptPath = path.resolve(outputRoot, 'src/web-ui/src/generated/version-injection.html');

const htmlDir = path.dirname(htmlScriptPath);
if (!fs.existsSync(htmlDir)) {
Expand All @@ -123,12 +139,11 @@ function main() {
printSuccess(`${versionInfo.name} v${versionInfo.version}${gitStr}`);
}

// On failure: warn and exit 0 so build is not interrupted
try {
main();
} catch (err) {
printWarning('Version info generation failed, skipped: ' + (err.message || err));
process.exit(0);
printWarning('Version info generation failed: ' + (err.message || err));
process.exit(1);
}


Loading