mirror of
https://github.com/cline/cline.git
synced 2026-09-19 02:05:44 +08:00
feat(ci): add nightly desktop publish workflow (#14244)
* feat(ci): add nightly desktop publish workflow Support manually dispatched nightly desktop builds from main, with auto-generated version tags and conflict-safe concurrency. * fix(desktop): keep nightly builds as Actions artifacts
This commit is contained in:
@@ -1,11 +1,14 @@
|
||||
name: desktop-publish
|
||||
|
||||
# Like ext-vscode-publish-nightly, nightlies are manually dispatched because
|
||||
# PublishDesktop requires approval. Dispatch from main with channel=nightly.
|
||||
# Nightlies upload Actions artifacts only; they do not publish releases or feeds.
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
git_tag:
|
||||
description: "Existing release tag to publish, for example desktop-v0.1.0"
|
||||
required: true
|
||||
description: "Existing stable/beta tag; leave empty for nightly"
|
||||
required: false
|
||||
type: string
|
||||
confirm_publish:
|
||||
description: 'Type "publish" to confirm the desktop release.'
|
||||
@@ -18,8 +21,13 @@ on:
|
||||
options:
|
||||
- stable
|
||||
- beta
|
||||
- nightly
|
||||
default: stable
|
||||
|
||||
concurrency:
|
||||
group: desktop-publish-${{ inputs.channel }}
|
||||
cancel-in-progress: false
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
@@ -32,10 +40,12 @@ jobs:
|
||||
name: Validate release tag
|
||||
if: |
|
||||
github.repository == 'cline/cline' &&
|
||||
github.ref == 'refs/heads/main' &&
|
||||
github.event.inputs.confirm_publish == 'publish' &&
|
||||
!endsWith(github.actor, '[bot]')
|
||||
runs-on: ubuntu-latest
|
||||
outputs:
|
||||
ref: ${{ steps.version.outputs.ref }}
|
||||
version: ${{ steps.version.outputs.version }}
|
||||
tag: ${{ steps.version.outputs.tag }}
|
||||
channel: ${{ steps.version.outputs.channel }}
|
||||
@@ -84,7 +94,7 @@ jobs:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
ref: ${{ github.event.inputs.git_tag }}
|
||||
ref: ${{ inputs.channel == 'nightly' && github.sha || inputs.git_tag }}
|
||||
fetch-depth: 0
|
||||
fetch-tags: true
|
||||
|
||||
@@ -103,6 +113,20 @@ jobs:
|
||||
# manifest landing on desktop-latest would auto-update every stable
|
||||
# install onto the beta. The stable regex rejects prerelease
|
||||
# suffixes for the same reason.
|
||||
if [ "$CHANNEL" = "nightly" ]; then
|
||||
if [ -n "$TAG" ]; then
|
||||
echo "nightly builds do not use a release tag; leave git_tag empty"
|
||||
exit 1
|
||||
fi
|
||||
BASE=$(node -p "require('./apps/examples/desktop-app/package.json').version.split('-')[0]")
|
||||
VERSION="${BASE}-nightly.$(date -u +%Y%m%d%H%M%S).${GITHUB_RUN_ID}.${GITHUB_RUN_ATTEMPT}"
|
||||
echo "ref=$(git rev-parse HEAD)" >> "$GITHUB_OUTPUT"
|
||||
echo "version=${VERSION}" >> "$GITHUB_OUTPUT"
|
||||
echo "channel=nightly" >> "$GITHUB_OUTPUT"
|
||||
echo "product=Cline Nightly" >> "$GITHUB_OUTPUT"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
case "$CHANNEL" in
|
||||
stable)
|
||||
if ! printf "%s\n" "$TAG" | grep -Eq '^desktop-v[0-9]+\.[0-9]+\.[0-9]+$'; then
|
||||
@@ -155,6 +179,7 @@ jobs:
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "ref=${HEAD_COMMIT}" >> "$GITHUB_OUTPUT"
|
||||
echo "version=${VERSION}" >> "$GITHUB_OUTPUT"
|
||||
echo "tag=${TAG}" >> "$GITHUB_OUTPUT"
|
||||
echo "channel=${CHANNEL}" >> "$GITHUB_OUTPUT"
|
||||
@@ -237,7 +262,22 @@ jobs:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
ref: ${{ needs.validate.outputs.tag }}
|
||||
ref: ${{ needs.validate.outputs.ref }}
|
||||
|
||||
- name: Stamp nightly version
|
||||
if: needs.validate.outputs.channel == 'nightly'
|
||||
shell: bash
|
||||
env:
|
||||
VERSION: ${{ needs.validate.outputs.version }}
|
||||
run: |
|
||||
node <<'JS'
|
||||
const fs = require('node:fs');
|
||||
for (const file of ['apps/examples/desktop-app/package.json', 'apps/examples/desktop-app/src-tauri/tauri.conf.json']) {
|
||||
const config = JSON.parse(fs.readFileSync(file, 'utf8'));
|
||||
config.version = process.env.VERSION;
|
||||
fs.writeFileSync(file, JSON.stringify(config, null, 2) + '\n');
|
||||
}
|
||||
JS
|
||||
|
||||
- name: Setup Bun
|
||||
uses: oven-sh/setup-bun@v2
|
||||
@@ -292,7 +332,7 @@ jobs:
|
||||
# deliberately unquoted: it must word-split into separate flags.
|
||||
run: bunx tauri build --target universal-apple-darwin $CONFIG_ARGS
|
||||
env:
|
||||
CONFIG_ARGS: ${{ needs.validate.outputs.channel == 'beta' && '--config src-tauri/tauri.release.conf.json --config src-tauri/tauri.beta.conf.json' || '--config src-tauri/tauri.release.conf.json' }}
|
||||
CONFIG_ARGS: ${{ needs.validate.outputs.channel == 'nightly' && '--config src-tauri/tauri.release.conf.json --config src-tauri/tauri.nightly.conf.json' || needs.validate.outputs.channel == 'beta' && '--config src-tauri/tauri.release.conf.json --config src-tauri/tauri.beta.conf.json' || '--config src-tauri/tauri.release.conf.json' }}
|
||||
# Telemetry config for the sidecar binary. Tauri's beforeBuildCommand
|
||||
# (`bun run build` -> build:sidecar:bin) compiles the sidecar during
|
||||
# this step and inlines these values into the binary via `--define`
|
||||
@@ -369,11 +409,15 @@ jobs:
|
||||
case "$CHANNEL" in
|
||||
stable)
|
||||
WANT="releases/download/desktop-latest/latest.json"
|
||||
FORBID="releases/download/desktop-beta/latest.json"
|
||||
FORBID="releases/download/desktop-(beta|nightly)/latest.json"
|
||||
;;
|
||||
beta)
|
||||
WANT="releases/download/desktop-beta/latest.json"
|
||||
FORBID="releases/download/desktop-latest/latest.json"
|
||||
FORBID="releases/download/desktop-(latest|nightly)/latest.json"
|
||||
;;
|
||||
nightly)
|
||||
WANT=""
|
||||
FORBID="releases/download/desktop-(latest|beta|nightly)/latest.json"
|
||||
;;
|
||||
*)
|
||||
echo "unknown channel: ${CHANNEL}"
|
||||
@@ -386,22 +430,22 @@ jobs:
|
||||
# pipefail.
|
||||
found=0
|
||||
for bin in "$APP/Contents/MacOS/"*; do
|
||||
if strings -a "$bin" | grep "$FORBID" >/dev/null; then
|
||||
if strings -a "$bin" | grep -E "$FORBID" >/dev/null; then
|
||||
echo "$bin embeds the other channel's feed URL (${FORBID})"
|
||||
exit 1
|
||||
fi
|
||||
if strings -a "$bin" | grep "$WANT" >/dev/null; then
|
||||
if [ -n "$WANT" ] && strings -a "$bin" | grep "$WANT" >/dev/null; then
|
||||
found=1
|
||||
fi
|
||||
done
|
||||
|
||||
if [ "$found" -ne 1 ]; then
|
||||
if [ -n "$WANT" ] && [ "$found" -ne 1 ]; then
|
||||
echo "No binary in ${APP}/Contents/MacOS embeds ${WANT}."
|
||||
echo "The updater endpoint overlay did not apply; check the"
|
||||
echo "--config flags on the build step and tauri.beta.conf.json."
|
||||
exit 1
|
||||
fi
|
||||
echo "Updater endpoint verified: ${WANT}"
|
||||
echo "Updater endpoint verified: ${WANT:-disabled}"
|
||||
|
||||
# Guardrail: assert the telemetry config actually made it into the
|
||||
# compiled sidecar. Missing env on the build step (or a regression in
|
||||
@@ -465,6 +509,7 @@ jobs:
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: desktop-universal
|
||||
retention-days: 30
|
||||
path: apps/examples/desktop-app/dist/publish/*
|
||||
if-no-files-found: error
|
||||
|
||||
@@ -528,7 +573,22 @@ jobs:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4
|
||||
with:
|
||||
ref: ${{ needs.validate.outputs.tag }}
|
||||
ref: ${{ needs.validate.outputs.ref }}
|
||||
|
||||
- name: Stamp nightly version
|
||||
if: needs.validate.outputs.channel == 'nightly'
|
||||
shell: bash
|
||||
env:
|
||||
VERSION: ${{ needs.validate.outputs.version }}
|
||||
run: |
|
||||
node <<'JS'
|
||||
const fs = require('node:fs');
|
||||
for (const file of ['apps/examples/desktop-app/package.json', 'apps/examples/desktop-app/src-tauri/tauri.conf.json']) {
|
||||
const config = JSON.parse(fs.readFileSync(file, 'utf8'));
|
||||
config.version = process.env.VERSION;
|
||||
fs.writeFileSync(file, JSON.stringify(config, null, 2) + '\n');
|
||||
}
|
||||
JS
|
||||
|
||||
- name: Setup Bun
|
||||
uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2
|
||||
@@ -631,7 +691,7 @@ jobs:
|
||||
# deliberately unquoted: it must word-split into separate flags.
|
||||
run: bunx tauri build --bundles nsis $CONFIG_ARGS --config "$SIGN_CONF"
|
||||
env:
|
||||
CONFIG_ARGS: ${{ needs.validate.outputs.channel == 'beta' && '--config src-tauri/tauri.release.conf.json --config src-tauri/tauri.beta.conf.json' || '--config src-tauri/tauri.release.conf.json' }}
|
||||
CONFIG_ARGS: ${{ needs.validate.outputs.channel == 'nightly' && '--config src-tauri/tauri.release.conf.json --config src-tauri/tauri.nightly.conf.json' || needs.validate.outputs.channel == 'beta' && '--config src-tauri/tauri.release.conf.json --config src-tauri/tauri.beta.conf.json' || '--config src-tauri/tauri.release.conf.json' }}
|
||||
# Telemetry inlined into the sidecar at compile time, same as macOS.
|
||||
TELEMETRY_SERVICE_API_KEY: ${{ secrets.TELEMETRY_SERVICE_API_KEY }}
|
||||
ERROR_SERVICE_API_KEY: ${{ secrets.ERROR_SERVICE_API_KEY }}
|
||||
@@ -663,11 +723,15 @@ jobs:
|
||||
case "$CHANNEL" in
|
||||
stable)
|
||||
WANT="releases/download/desktop-latest/latest.json"
|
||||
FORBID="releases/download/desktop-beta/latest.json"
|
||||
FORBID="releases/download/desktop-(beta|nightly)/latest.json"
|
||||
;;
|
||||
beta)
|
||||
WANT="releases/download/desktop-beta/latest.json"
|
||||
FORBID="releases/download/desktop-latest/latest.json"
|
||||
FORBID="releases/download/desktop-(latest|nightly)/latest.json"
|
||||
;;
|
||||
nightly)
|
||||
WANT=""
|
||||
FORBID="releases/download/desktop-(latest|beta|nightly)/latest.json"
|
||||
;;
|
||||
*)
|
||||
echo "unknown channel: ${CHANNEL}"
|
||||
@@ -677,22 +741,22 @@ jobs:
|
||||
|
||||
found=0
|
||||
for bin in src-tauri/target/release/*.exe; do
|
||||
if grep -a "$FORBID" "$bin" >/dev/null; then
|
||||
if grep -aE "$FORBID" "$bin" >/dev/null; then
|
||||
echo "$bin embeds the other channel's feed URL (${FORBID})"
|
||||
exit 1
|
||||
fi
|
||||
if grep -a "$WANT" "$bin" >/dev/null; then
|
||||
if [ -n "$WANT" ] && grep -a "$WANT" "$bin" >/dev/null; then
|
||||
found=1
|
||||
fi
|
||||
done
|
||||
|
||||
if [ "$found" -ne 1 ]; then
|
||||
if [ -n "$WANT" ] && [ "$found" -ne 1 ]; then
|
||||
echo "No exe in src-tauri/target/release embeds ${WANT}."
|
||||
echo "The updater endpoint overlay did not apply; check the"
|
||||
echo "--config flags on the build step and tauri.beta.conf.json."
|
||||
exit 1
|
||||
fi
|
||||
echo "Updater endpoint verified: ${WANT}"
|
||||
echo "Updater endpoint verified: ${WANT:-disabled}"
|
||||
|
||||
# Same guardrail as the macOS job, run natively on the Windows sidecar.
|
||||
- name: Verify sidecar telemetry config was inlined
|
||||
@@ -769,12 +833,14 @@ jobs:
|
||||
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
|
||||
with:
|
||||
name: desktop-windows-x64
|
||||
retention-days: 30
|
||||
path: apps/examples/desktop-app/dist/publish/*
|
||||
if-no-files-found: error
|
||||
|
||||
release:
|
||||
name: Create GitHub release
|
||||
needs: [validate, build, build-windows]
|
||||
if: needs.validate.outputs.channel != 'nightly'
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: write
|
||||
@@ -782,7 +848,7 @@ jobs:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
ref: ${{ needs.validate.outputs.tag }}
|
||||
ref: ${{ needs.validate.outputs.ref }}
|
||||
fetch-depth: 0
|
||||
fetch-tags: true
|
||||
|
||||
@@ -860,15 +926,15 @@ jobs:
|
||||
- name: Get Previous Desktop Tag
|
||||
id: prev_tag
|
||||
env:
|
||||
CURRENT_TAG: ${{ needs.validate.outputs.tag }}
|
||||
SOURCE_REF: ${{ needs.validate.outputs.ref }}
|
||||
CHANNEL: ${{ needs.validate.outputs.channel }}
|
||||
run: |
|
||||
# Stable compare links skip beta tags so they read stable -> stable;
|
||||
# beta compares against whatever shipped last on either channel.
|
||||
if [ "$CHANNEL" = "stable" ]; then
|
||||
PREV_TAG=$(git describe --tags --abbrev=0 --match 'desktop-v*' --exclude 'desktop-v*-beta*' "$CURRENT_TAG^" 2>/dev/null || echo "")
|
||||
PREV_TAG=$(git describe --tags --abbrev=0 --match 'desktop-v*' --exclude 'desktop-v*-*' "$SOURCE_REF^" 2>/dev/null || echo "")
|
||||
else
|
||||
PREV_TAG=$(git describe --tags --abbrev=0 --match 'desktop-v*' "$CURRENT_TAG^" 2>/dev/null || echo "")
|
||||
PREV_TAG=$(git describe --tags --abbrev=0 --match 'desktop-v*' "$SOURCE_REF^" 2>/dev/null || echo "")
|
||||
fi
|
||||
echo "prev_tag=$PREV_TAG" >> $GITHUB_OUTPUT
|
||||
|
||||
@@ -876,6 +942,7 @@ jobs:
|
||||
uses: softprops/action-gh-release@v1
|
||||
with:
|
||||
tag_name: ${{ needs.validate.outputs.tag }}
|
||||
target_commitish: ${{ needs.validate.outputs.ref }}
|
||||
name: "Desktop v${{ needs.validate.outputs.version }}"
|
||||
# The repo-wide "latest" release stays owned by CLI releases; the
|
||||
# desktop auto-update feed is the rolling desktop-latest release.
|
||||
@@ -915,10 +982,10 @@ jobs:
|
||||
fi
|
||||
|
||||
if ! gh release view "$FEED" >/dev/null 2>&1; then
|
||||
if [ "$CHANNEL" = "beta" ]; then
|
||||
if [ "$CHANNEL" != "stable" ]; then
|
||||
gh release create "$FEED" \
|
||||
--title "Cline desktop beta (auto-update feed)" \
|
||||
--notes "Rolling release backing the beta desktop app auto-updater. The latest.json asset points at the newest desktop-vX.Y.Z-beta.N release. Only beta installs poll this feed; stable installs use desktop-latest. Do not delete." \
|
||||
--title "Cline desktop ${CHANNEL} (auto-update feed)" \
|
||||
--notes "Rolling release backing the ${CHANNEL} desktop app auto-updater. Only ${CHANNEL} installs poll this feed. Do not delete." \
|
||||
--latest=false \
|
||||
--prerelease \
|
||||
--target "$(git rev-parse HEAD)"
|
||||
|
||||
@@ -205,7 +205,22 @@ fn set_update_status(
|
||||
refresh_tray_status(app, update_state);
|
||||
}
|
||||
|
||||
// An empty endpoint list disables both background and on-demand updates.
|
||||
// Nightly builds use this configuration because they are Actions artifacts only.
|
||||
fn updates_enabled(app: &tauri::AppHandle) -> bool {
|
||||
app.config()
|
||||
.plugins
|
||||
.0
|
||||
.get("updater")
|
||||
.and_then(|config| config.get("endpoints"))
|
||||
.and_then(serde_json::Value::as_array)
|
||||
.is_some_and(|endpoints| !endpoints.is_empty())
|
||||
}
|
||||
|
||||
async fn check_and_install_update(app: &tauri::AppHandle, state: &UpdateState) {
|
||||
if !updates_enabled(app) {
|
||||
return;
|
||||
}
|
||||
let _cycle = state.cycle.lock().await;
|
||||
// An update that already finished downloading only needs a restart; keep
|
||||
// reporting "ready" instead of flipping back to transient states unless a
|
||||
@@ -1257,7 +1272,7 @@ fn main() {
|
||||
});
|
||||
// Dev builds are not installed app bundles, so there is nothing the
|
||||
// updater could meaningfully check or replace.
|
||||
if !cfg!(debug_assertions) {
|
||||
if !cfg!(debug_assertions) && updates_enabled(app.handle()) {
|
||||
let update_state = app.state::<Arc<UpdateState>>().inner().clone();
|
||||
let app_handle = app.handle().clone();
|
||||
tauri::async_runtime::spawn(async move {
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"$schema": "https://schema.tauri.app/config/2",
|
||||
"productName": "Cline Nightly",
|
||||
"identifier": "bot.cline.app.nightly",
|
||||
"plugins": {
|
||||
"updater": {
|
||||
"endpoints": []
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user