Publish Claude Code Binaries / Stage and publish binaries (push) Canceled after 0s
Publish Tutti App Runtime / Build darwin-arm64 (push) Canceled after 0s
Publish Tutti App Runtime / Build darwin-amd64 (push) Canceled after 0s
Publish Tutti App Runtime / Build linux-amd64 (push) Canceled after 0s
Publish Tutti App Runtime / Build windows-amd64 (push) Canceled after 0s
Sync Tutti Agent Skills / sync (push) Canceled after 0s
Windows Agent Adapters / Test Windows agent adapters (push) Canceled after 0s
Windows Daemon Adapters / Test Windows daemon adapters (push) Canceled after 0s
Publish Tutti App Runtime / Publish runtime catalog (push) Canceled after 0s
1071 lines
43 KiB
YAML
1071 lines
43 KiB
YAML
name: Desktop Release
|
|
|
|
run-name: >-
|
|
${{ github.event_name == 'workflow_dispatch'
|
|
&& format('Desktop Release · {0} · {1}', inputs.release_mode, github.ref_name)
|
|
|| format('Desktop Release · {0} · {1}', github.event_name, github.ref_name) }}
|
|
|
|
on:
|
|
push:
|
|
tags:
|
|
- "v*"
|
|
schedule:
|
|
- cron: "16 20 * * *"
|
|
workflow_dispatch:
|
|
inputs:
|
|
release_mode:
|
|
description: What to build and publish
|
|
required: true
|
|
type: choice
|
|
default: patch_rc_release
|
|
options:
|
|
- unsigned_dry_run
|
|
- patch_beta_release
|
|
- patch_rc_release
|
|
- patch_release
|
|
- minor_release
|
|
- major_release
|
|
- explicit_version_release
|
|
version:
|
|
description: Release semver for explicit_version_release, such as 0.1.0, 0.1.0-rc.0, or 0.1.0-beta.0
|
|
required: false
|
|
type: string
|
|
target_commitish:
|
|
description: Optional commit-ish override
|
|
required: false
|
|
type: string
|
|
notify_feishu:
|
|
description: Send a Feishu card after publishing
|
|
required: false
|
|
type: boolean
|
|
default: true
|
|
|
|
permissions:
|
|
contents: write
|
|
id-token: write
|
|
|
|
concurrency:
|
|
group: desktop-release-${{ github.event_name }}-${{ inputs.release_mode || github.ref_name }}-${{ inputs.version || github.ref_name }}
|
|
cancel-in-progress: false
|
|
|
|
env:
|
|
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true
|
|
|
|
jobs:
|
|
resolve:
|
|
if: ${{ vars.TUTTI_DESKTOP_RELEASE_WORKFLOW_ENABLED == 'true' }}
|
|
runs-on: ubuntu-latest
|
|
outputs:
|
|
release_dry_run: ${{ steps.mode.outputs.dry_run }}
|
|
release_make_latest: ${{ steps.release.outputs.release_make_latest }}
|
|
release_prerelease: ${{ steps.release.outputs.release_prerelease }}
|
|
release_tag: ${{ steps.release.outputs.release_tag }}
|
|
release_target: ${{ steps.release.outputs.release_target }}
|
|
release_version: ${{ steps.release.outputs.release_version }}
|
|
release_channel: ${{ steps.release.outputs.release_channel }}
|
|
release_candidate: ${{ steps.release.outputs.release_candidate }}
|
|
release_candidate_id: ${{ steps.release.outputs.release_candidate_id }}
|
|
release_candidate_tag: ${{ steps.release.outputs.release_candidate_tag }}
|
|
source_ref: ${{ steps.target.outputs.source_ref }}
|
|
notify_feishu: ${{ steps.notify.outputs.notify_feishu }}
|
|
|
|
steps:
|
|
- name: Summarize workflow dispatch inputs
|
|
if: ${{ github.event_name == 'workflow_dispatch' }}
|
|
shell: bash
|
|
env:
|
|
WORKFLOW_INPUTS: ${{ toJSON(inputs) }}
|
|
run: |
|
|
{
|
|
echo "## Desktop release request"
|
|
echo
|
|
printf -- '- Triggered by: %s\n' "${GITHUB_ACTOR}"
|
|
printf -- '- Ref: %s\n' "${GITHUB_REF_NAME}"
|
|
printf -- '- Commit: %s\n' "${GITHUB_SHA}"
|
|
echo
|
|
echo "### Workflow dispatch inputs"
|
|
echo
|
|
printf '%s\n' "${WORKFLOW_INPUTS}" | sed 's/^/ /'
|
|
} >> "${GITHUB_STEP_SUMMARY}"
|
|
|
|
- name: Resolve target commit
|
|
id: target
|
|
shell: bash
|
|
run: |
|
|
release_target="${{ inputs.target_commitish || github.sha }}"
|
|
echo "release_target=${release_target}" >> "$GITHUB_OUTPUT"
|
|
echo "source_ref=${{ github.ref_type == 'branch' && github.ref_name || '' }}" >> "$GITHUB_OUTPUT"
|
|
|
|
- name: Checkout
|
|
uses: actions/checkout@v6
|
|
with:
|
|
ref: ${{ steps.target.outputs.release_target }}
|
|
fetch-depth: 0
|
|
|
|
- name: Validate release dispatch branch
|
|
shell: bash
|
|
env:
|
|
RELEASE_EVENT_NAME: ${{ github.event_name }}
|
|
RELEASE_MODE: ${{ inputs.release_mode }}
|
|
RELEASE_REF_NAME: ${{ github.ref_name }}
|
|
RELEASE_REF_TYPE: ${{ github.ref_type }}
|
|
run: |
|
|
if [[ "${RELEASE_EVENT_NAME}" != "workflow_dispatch" ]]; then
|
|
exit 0
|
|
fi
|
|
|
|
case "${RELEASE_MODE:-}" in
|
|
patch_rc_release|patch_release|minor_release|major_release)
|
|
;;
|
|
*)
|
|
exit 0
|
|
;;
|
|
esac
|
|
|
|
if [[ "${RELEASE_REF_TYPE}" != "branch" ]]; then
|
|
echo "Manual desktop release mode ${RELEASE_MODE} must be run from main or a release/* branch." >&2
|
|
exit 1
|
|
fi
|
|
|
|
case "${RELEASE_REF_NAME}" in
|
|
main|release/*)
|
|
;;
|
|
*)
|
|
echo "Manual desktop release mode ${RELEASE_MODE} must be run from main or a release/* branch, not ${RELEASE_REF_NAME}." >&2
|
|
exit 1
|
|
;;
|
|
esac
|
|
|
|
case "${RELEASE_MODE:-}" in
|
|
patch_release|minor_release|major_release)
|
|
;;
|
|
*)
|
|
exit 0
|
|
;;
|
|
esac
|
|
|
|
if [[ "${RELEASE_REF_NAME}" == "main" ]]; then
|
|
release_branch_refs="$(git ls-remote --heads origin "refs/heads/release/*")"
|
|
if [[ -n "${release_branch_refs}" ]]; then
|
|
echo "Stable desktop release modes must run from a release/* branch while release branches exist." >&2
|
|
echo "Found release branches:" >&2
|
|
printf "%s\n" "${release_branch_refs}" | sed -E "s#^[[:xdigit:]]+[[:space:]]+refs/heads/#- #" >&2
|
|
exit 1
|
|
fi
|
|
fi
|
|
|
|
- name: Setup Node.js
|
|
uses: actions/setup-node@v6
|
|
with:
|
|
node-version-file: .node-version
|
|
|
|
- name: Resolve release mode
|
|
id: mode
|
|
shell: bash
|
|
env:
|
|
RELEASE_EVENT_NAME: ${{ github.event_name }}
|
|
RELEASE_MODE: ${{ inputs.release_mode }}
|
|
run: |
|
|
dry_run=false
|
|
strategy=patch
|
|
|
|
if [[ "${RELEASE_EVENT_NAME}" == "schedule" ]]; then
|
|
strategy=patch_rc
|
|
else
|
|
case "${RELEASE_MODE:-}" in
|
|
"")
|
|
strategy=explicit_tag
|
|
;;
|
|
unsigned_dry_run)
|
|
dry_run=true
|
|
strategy=explicit_version
|
|
;;
|
|
patch_release)
|
|
strategy=patch
|
|
;;
|
|
patch_beta_release)
|
|
strategy=patch_beta
|
|
;;
|
|
minor_release)
|
|
strategy=minor
|
|
;;
|
|
major_release)
|
|
strategy=major
|
|
;;
|
|
patch_rc_release)
|
|
strategy=patch_rc
|
|
;;
|
|
minor_rc_release)
|
|
strategy=minor_rc
|
|
;;
|
|
major_rc_release)
|
|
strategy=major_rc
|
|
;;
|
|
explicit_version_release)
|
|
strategy=explicit_version
|
|
;;
|
|
*)
|
|
echo "Unknown release_mode: ${RELEASE_MODE}" >&2
|
|
exit 1
|
|
;;
|
|
esac
|
|
fi
|
|
|
|
echo "dry_run=${dry_run}" >> "$GITHUB_OUTPUT"
|
|
echo "strategy=${strategy}" >> "$GITHUB_OUTPUT"
|
|
|
|
- name: Verify managed app runtime is published before release build
|
|
if: steps.mode.outputs.dry_run != 'true'
|
|
run: node tools/scripts/verify-tutti-app-runtime-release.mjs
|
|
|
|
- name: Resolve release tag
|
|
id: release
|
|
shell: bash
|
|
env:
|
|
RELEASE_DRY_RUN: ${{ steps.mode.outputs.dry_run }}
|
|
RELEASE_STRATEGY: ${{ steps.mode.outputs.strategy }}
|
|
INPUT_VERSION: ${{ inputs.version }}
|
|
run: |
|
|
if [[ "${{ github.event_name }}" == "push" ]]; then
|
|
args=(--strategy explicit_tag --tag "${{ github.ref_name }}" --json)
|
|
else
|
|
args=(--strategy "${RELEASE_STRATEGY}" --json)
|
|
if [[ "${RELEASE_STRATEGY}" == "explicit_version" ]]; then
|
|
version="${INPUT_VERSION:-0.1.0}"
|
|
args+=(--version "${version}")
|
|
fi
|
|
args=(node apps/desktop/scripts/resolve-release-tag.mjs "${args[@]}")
|
|
fi
|
|
|
|
if [[ "${{ github.event_name }}" == "push" ]]; then
|
|
release_json="$(node apps/desktop/scripts/resolve-release-tag.mjs "${args[@]}")"
|
|
else
|
|
release_json="$("${args[@]}")"
|
|
fi
|
|
release_make_latest="$(node -e "process.stdout.write(String(JSON.parse(process.argv[1]).makeLatest))" "${release_json}")"
|
|
release_prerelease="$(node -e "process.stdout.write(String(JSON.parse(process.argv[1]).prerelease))" "${release_json}")"
|
|
release_tag="$(node -e "process.stdout.write(JSON.parse(process.argv[1]).tag)" "${release_json}")"
|
|
release_version="$(node -e "process.stdout.write(JSON.parse(process.argv[1]).version)" "${release_json}")"
|
|
release_channel="$(node -e "process.stdout.write(JSON.parse(process.argv[1]).channel)" "${release_json}")"
|
|
release_candidate=false
|
|
if [[ "${release_channel}" == "stable" && "${RELEASE_DRY_RUN}" != "true" && "${{ github.event_name }}" != "push" ]]; then
|
|
release_candidate=true
|
|
if git ls-remote --exit-code --tags origin "refs/tags/${release_tag}" >/dev/null 2>&1; then
|
|
echo "Stable tag ${release_tag} already exists; retry promotion or choose a new version." >&2
|
|
exit 1
|
|
fi
|
|
elif [[ "${{ github.event_name }}" != "push" && "${RELEASE_DRY_RUN}" != "true" ]]; then
|
|
reserve_args=(--strategy explicit_tag --tag "${release_tag}" --target "$(git rev-parse HEAD)" --json)
|
|
node apps/desktop/scripts/reserve-release-tag.mjs "${reserve_args[@]}" >/dev/null
|
|
fi
|
|
release_target="$(git rev-parse HEAD)"
|
|
release_candidate_id="${release_tag}-$(git rev-parse --short=7 HEAD)-run${GITHUB_RUN_ID}-${GITHUB_RUN_ATTEMPT}"
|
|
release_candidate_tag="candidate-${release_tag}"
|
|
echo "release_make_latest=${release_make_latest}" >> "$GITHUB_OUTPUT"
|
|
echo "release_prerelease=${release_prerelease}" >> "$GITHUB_OUTPUT"
|
|
echo "release_tag=${release_tag}" >> "$GITHUB_OUTPUT"
|
|
echo "release_version=${release_version}" >> "$GITHUB_OUTPUT"
|
|
echo "release_channel=${release_channel}" >> "$GITHUB_OUTPUT"
|
|
echo "release_candidate=${release_candidate}" >> "$GITHUB_OUTPUT"
|
|
echo "release_candidate_id=${release_candidate_id}" >> "$GITHUB_OUTPUT"
|
|
echo "release_candidate_tag=${release_candidate_tag}" >> "$GITHUB_OUTPUT"
|
|
echo "release_target=${release_target}" >> "$GITHUB_OUTPUT"
|
|
|
|
- name: Resolve Feishu notification
|
|
id: notify
|
|
shell: bash
|
|
run: echo "notify_feishu=${{ github.event_name != 'workflow_dispatch' || inputs.notify_feishu != false }}" >> "$GITHUB_OUTPUT"
|
|
|
|
build-macos:
|
|
name: Build macOS (${{ matrix.arch }})
|
|
needs: resolve
|
|
runs-on: macos-latest
|
|
strategy:
|
|
fail-fast: false
|
|
matrix:
|
|
arch: [x64, arm64, universal]
|
|
env:
|
|
TUTTI_DESKTOP_MAC_ARCH: ${{ matrix.arch }}
|
|
TUTTI_DESKTOP_RELEASE_TAG: ${{ needs.resolve.outputs.release_tag }}
|
|
TUTTI_DESKTOP_RELEASE_TARGET: ${{ needs.resolve.outputs.release_target }}
|
|
TUTTI_DESKTOP_DRY_RUN: ${{ needs.resolve.outputs.release_dry_run }}
|
|
TUTTI_DESKTOP_RELEASE_CHANNEL: ${{ needs.resolve.outputs.release_channel }}
|
|
TUTTI_DESKTOP_RELEASE_CANDIDATE: ${{ needs.resolve.outputs.release_candidate }}
|
|
TUTTI_DESKTOP_RELEASE_CANDIDATE_ID: ${{ needs.resolve.outputs.release_candidate_id }}
|
|
TUTTI_DESKTOP_RELEASE_CANDIDATE_TAG: ${{ needs.resolve.outputs.release_candidate_tag }}
|
|
|
|
steps:
|
|
- name: Checkout
|
|
uses: actions/checkout@v6
|
|
with:
|
|
ref: ${{ env.TUTTI_DESKTOP_RELEASE_TARGET }}
|
|
fetch-depth: 0
|
|
|
|
- name: Setup pnpm
|
|
uses: pnpm/action-setup@v6
|
|
|
|
- name: Setup Node.js
|
|
uses: actions/setup-node@v6
|
|
with:
|
|
node-version-file: .node-version
|
|
cache: pnpm
|
|
cache-dependency-path: pnpm-lock.yaml
|
|
|
|
- name: Setup Go
|
|
uses: actions/setup-go@v6
|
|
with:
|
|
go-version: "1.24.5"
|
|
cache-dependency-path: |
|
|
go.work
|
|
go.work.sum
|
|
packages/workbench/service/go.mod
|
|
packages/workspace/files/go.mod
|
|
packages/workspace/issues/go.mod
|
|
services/tuttid/go.mod
|
|
services/tuttid/go.sum
|
|
|
|
- name: Install dependencies
|
|
run: pnpm install --frozen-lockfile
|
|
|
|
- name: Install Electron runtime
|
|
run: pnpm --filter @tutti-os/desktop exec install-electron
|
|
|
|
- name: Align package version with release tag
|
|
working-directory: apps/desktop
|
|
run: node scripts/apply-ci-release-version.mjs "${TUTTI_DESKTOP_RELEASE_TAG}"
|
|
|
|
- name: Prepare Apple notarization API key
|
|
if: env.TUTTI_DESKTOP_DRY_RUN != 'true'
|
|
env:
|
|
APPLE_API_KEY_P8_BASE64: ${{ secrets.APPLE_API_KEY_P8_BASE64 }}
|
|
APPLE_API_KEY_ID: ${{ secrets.APPLE_API_KEY_ID }}
|
|
run: |
|
|
if [ -z "${APPLE_API_KEY_P8_BASE64}" ] || [ -z "${APPLE_API_KEY_ID}" ]; then
|
|
echo "APPLE_API_KEY_P8_BASE64 and APPLE_API_KEY_ID secrets are required for signed macOS releases." >&2
|
|
exit 1
|
|
fi
|
|
apple_api_key_path="${RUNNER_TEMP}/AuthKey_${APPLE_API_KEY_ID}.p8"
|
|
node -e "require('node:fs').writeFileSync(process.argv[1], Buffer.from(process.env.APPLE_API_KEY_P8_BASE64, 'base64'))" "${apple_api_key_path}"
|
|
chmod 600 "${apple_api_key_path}"
|
|
echo "APPLE_API_KEY=${apple_api_key_path}" >> "${GITHUB_ENV}"
|
|
|
|
- name: Prepare macOS signing certificate
|
|
if: env.TUTTI_DESKTOP_DRY_RUN != 'true'
|
|
env:
|
|
MACOS_CSC_LINK: ${{ secrets.MACOS_CSC_LINK }}
|
|
MACOS_CSC_KEY_PASSWORD: ${{ secrets.MACOS_CSC_KEY_PASSWORD }}
|
|
run: |
|
|
if [ -z "${MACOS_CSC_LINK}" ] || [ -z "${MACOS_CSC_KEY_PASSWORD}" ]; then
|
|
echo "MACOS_CSC_LINK and MACOS_CSC_KEY_PASSWORD secrets are required for signed macOS releases." >&2
|
|
exit 1
|
|
fi
|
|
|
|
case "${MACOS_CSC_LINK}" in
|
|
https://*|file://*|/*|./*|../*|~/*)
|
|
echo "CSC_LINK=${MACOS_CSC_LINK}" >> "${GITHUB_ENV}"
|
|
;;
|
|
*)
|
|
certificate_path="${RUNNER_TEMP}/macos-codesign-certificate.p12"
|
|
CERTIFICATE_PATH="${certificate_path}" node <<'NODE'
|
|
const fs = require("node:fs");
|
|
|
|
const raw = process.env.MACOS_CSC_LINK ?? "";
|
|
const output = process.env.CERTIFICATE_PATH;
|
|
const normalized = raw
|
|
.replace(/\\n/g, "\n")
|
|
.replace(/^data:.*;base64,/, "")
|
|
.replace(/-----BEGIN [^-]+-----/g, "")
|
|
.replace(/-----END [^-]+-----/g, "")
|
|
.replace(/\s+/g, "")
|
|
.replace(/-/g, "+")
|
|
.replace(/_/g, "/");
|
|
const base64 = normalized.padEnd(
|
|
normalized.length + ((4 - (normalized.length % 4)) % 4),
|
|
"="
|
|
);
|
|
|
|
if (!/^[A-Za-z0-9+/]*={0,2}$/.test(base64)) {
|
|
throw new Error("MACOS_CSC_LINK must be a URL, path, or base64-encoded .p12 certificate.");
|
|
}
|
|
|
|
const bytes = Buffer.from(base64, "base64");
|
|
if (bytes.length === 0 || bytes[0] !== 0x30) {
|
|
throw new Error("MACOS_CSC_LINK did not decode to a valid PKCS#12 certificate.");
|
|
}
|
|
|
|
fs.writeFileSync(output, bytes, { mode: 0o600 });
|
|
NODE
|
|
echo "CSC_LINK=${certificate_path}" >> "${GITHUB_ENV}"
|
|
;;
|
|
esac
|
|
echo "CSC_KEY_PASSWORD=${MACOS_CSC_KEY_PASSWORD}" >> "${GITHUB_ENV}"
|
|
|
|
- name: Build release artifacts
|
|
env:
|
|
APPLE_API_KEY_ID: ${{ secrets.APPLE_API_KEY_ID }}
|
|
APPLE_API_ISSUER: ${{ secrets.APPLE_API_ISSUER }}
|
|
NODE_OPTIONS: --max-old-space-size=4096
|
|
run: |
|
|
if [[ "${TUTTI_DESKTOP_DRY_RUN}" == "true" ]]; then
|
|
unset CSC_LINK CSC_KEY_PASSWORD APPLE_API_KEY APPLE_API_KEY_ID APPLE_API_ISSUER
|
|
pnpm --filter @tutti-os/desktop build:mac:unsigned
|
|
else
|
|
pnpm --filter @tutti-os/desktop build:mac:signed
|
|
fi
|
|
|
|
- name: Verify prerelease updater metadata
|
|
if: ${{ needs.resolve.outputs.release_channel != 'stable' }}
|
|
run: |
|
|
# The generic updater provider derives the prerelease channel from
|
|
# the version, so electron-builder writes rc-mac.yml or beta-mac.yml
|
|
# directly instead of latest-mac.yml.
|
|
updater_metadata="apps/desktop/dist/${TUTTI_DESKTOP_RELEASE_CHANNEL}-mac.yml"
|
|
if [[ ! -f "${updater_metadata}" ]]; then
|
|
echo "${updater_metadata} is required for the ${TUTTI_DESKTOP_RELEASE_CHANNEL} updater channel." >&2
|
|
exit 1
|
|
fi
|
|
|
|
- name: Upload release artifacts
|
|
uses: actions/upload-artifact@v7
|
|
with:
|
|
name: tutti-desktop-release-assets-macos-${{ matrix.arch }}
|
|
compression-level: 0
|
|
path: |
|
|
apps/desktop/dist/*.blockmap
|
|
apps/desktop/dist/*.dmg
|
|
apps/desktop/dist/*.zip
|
|
if-no-files-found: error
|
|
retention-days: 7
|
|
|
|
build-windows:
|
|
name: Prepare Windows application
|
|
needs: resolve
|
|
runs-on: windows-latest
|
|
timeout-minutes: 60
|
|
env:
|
|
TUTTI_DESKTOP_RELEASE_TAG: ${{ needs.resolve.outputs.release_tag }}
|
|
TUTTI_DESKTOP_RELEASE_TARGET: ${{ needs.resolve.outputs.release_target }}
|
|
TUTTI_DESKTOP_DRY_RUN: ${{ needs.resolve.outputs.release_dry_run }}
|
|
TUTTI_DESKTOP_RELEASE_CHANNEL: ${{ needs.resolve.outputs.release_channel }}
|
|
|
|
steps:
|
|
- name: Checkout
|
|
uses: actions/checkout@v6
|
|
with:
|
|
ref: ${{ env.TUTTI_DESKTOP_RELEASE_TARGET }}
|
|
fetch-depth: 0
|
|
|
|
- name: Setup pnpm
|
|
uses: pnpm/action-setup@v6
|
|
|
|
- name: Setup Node.js
|
|
uses: actions/setup-node@v6
|
|
with:
|
|
node-version-file: .node-version
|
|
cache: pnpm
|
|
cache-dependency-path: pnpm-lock.yaml
|
|
|
|
- name: Setup Go
|
|
uses: actions/setup-go@v6
|
|
with:
|
|
go-version: "1.24.5"
|
|
cache-dependency-path: |
|
|
go.work
|
|
go.work.sum
|
|
packages/workbench/service/go.mod
|
|
packages/workspace/files/go.mod
|
|
packages/workspace/issues/go.mod
|
|
services/tuttid/go.mod
|
|
services/tuttid/go.sum
|
|
|
|
- name: Install dependencies
|
|
shell: pwsh
|
|
run: pnpm install --frozen-lockfile
|
|
|
|
- name: Install Electron runtime
|
|
shell: pwsh
|
|
run: pnpm --filter @tutti-os/desktop exec install-electron
|
|
|
|
- name: Align package version with release tag
|
|
working-directory: apps/desktop
|
|
shell: pwsh
|
|
run: node scripts/apply-ci-release-version.mjs "$env:TUTTI_DESKTOP_RELEASE_TAG"
|
|
|
|
- name: Build Windows package input
|
|
shell: pwsh
|
|
env:
|
|
CSC_IDENTITY_AUTO_DISCOVERY: "false"
|
|
run: |
|
|
if ($env:TUTTI_DESKTOP_DRY_RUN -eq 'true') {
|
|
pnpm --filter @tutti-os/desktop build:win
|
|
} else {
|
|
pnpm --filter @tutti-os/desktop build:win:unpacked
|
|
}
|
|
|
|
- name: Verify Windows release metadata
|
|
if: env.TUTTI_DESKTOP_DRY_RUN == 'true'
|
|
shell: pwsh
|
|
run: node apps/desktop/scripts/validate-windows-release-artifacts.mjs apps/desktop/dist "$env:TUTTI_DESKTOP_RELEASE_CHANNEL" "$env:TUTTI_DESKTOP_RELEASE_TAG"
|
|
|
|
- name: Upload unsigned Windows dry-run artifacts
|
|
if: env.TUTTI_DESKTOP_DRY_RUN == 'true'
|
|
uses: actions/upload-artifact@v7
|
|
with:
|
|
name: tutti-desktop-release-assets-windows
|
|
compression-level: 0
|
|
path: |
|
|
apps/desktop/dist/*.blockmap
|
|
apps/desktop/dist/*.exe
|
|
apps/desktop/dist/*.yml
|
|
if-no-files-found: error
|
|
retention-days: 7
|
|
|
|
- name: Upload Windows application prepared for Certum signing
|
|
if: env.TUTTI_DESKTOP_DRY_RUN != 'true'
|
|
uses: actions/upload-artifact@v7
|
|
with:
|
|
name: tutti-windows-prepared-${{ github.run_id }}
|
|
compression-level: 0
|
|
path: apps/desktop/dist/win-unpacked/**
|
|
if-no-files-found: error
|
|
retention-days: 7
|
|
|
|
sign-windows:
|
|
name: Sign Windows application and NSIS
|
|
needs: [resolve, build-windows]
|
|
if: needs.resolve.outputs.release_dry_run != 'true'
|
|
runs-on: ubuntu-latest
|
|
timeout-minutes: 45
|
|
concurrency:
|
|
group: certum-simplysign-signing
|
|
cancel-in-progress: false
|
|
container:
|
|
image: ghcr.io/reactiveui/certum-signer@sha256:fb03892c379b6703bee9bfd7a2f66df3a5696de149d6d6240e1897ae15afc3e4
|
|
env:
|
|
CERTUM_USER_ID: ${{ secrets.CERTUM_USER_ID }}
|
|
CERTUM_OTP_URI: ${{ secrets.CERTUM_OTP_URI }}
|
|
CERTUM_CERT_FINGERPRINT: ${{ vars.CERTUM_CERT_FINGERPRINT }}
|
|
TSH_WINDOWS_EXPECTED_PUBLISHER: ${{ vars.TSH_WINDOWS_EXPECTED_PUBLISHER }}
|
|
TUTTI_DESKTOP_RELEASE_TAG: ${{ needs.resolve.outputs.release_tag }}
|
|
TUTTI_DESKTOP_RELEASE_TARGET: ${{ needs.resolve.outputs.release_target }}
|
|
TSH_CERTUM_LOGIN_MAX_ATTEMPTS: "3"
|
|
TSH_CERTUM_SIGN_MAX_ATTEMPTS: "3"
|
|
|
|
steps:
|
|
- name: Validate Certum signing configuration
|
|
shell: bash
|
|
run: |
|
|
set -euo pipefail
|
|
: "${CERTUM_USER_ID:?CERTUM_USER_ID organization secret is required}"
|
|
: "${CERTUM_OTP_URI:?CERTUM_OTP_URI organization secret is required}"
|
|
: "${CERTUM_CERT_FINGERPRINT:?CERTUM_CERT_FINGERPRINT organization variable is required}"
|
|
: "${TSH_WINDOWS_EXPECTED_PUBLISHER:?TSH_WINDOWS_EXPECTED_PUBLISHER organization variable is required}"
|
|
[[ "${CERTUM_CERT_FINGERPRINT}" =~ ^[[:xdigit:]]{64}$ ]]
|
|
|
|
- name: Checkout release target
|
|
uses: actions/checkout@v6
|
|
with:
|
|
ref: ${{ env.TUTTI_DESKTOP_RELEASE_TARGET }}
|
|
fetch-depth: 1
|
|
persist-credentials: false
|
|
|
|
- name: Checkout immutable signing tooling
|
|
uses: actions/checkout@v6
|
|
with:
|
|
ref: ${{ github.workflow_sha }}
|
|
path: .release-tooling
|
|
persist-credentials: false
|
|
sparse-checkout: |
|
|
tools/scripts/windows/connect-simplysign-linux.sh
|
|
tools/scripts/windows/certum-sign-windows.cjs
|
|
sparse-checkout-cone-mode: false
|
|
|
|
- name: Setup pnpm
|
|
uses: pnpm/action-setup@v6
|
|
|
|
- name: Setup Node.js
|
|
uses: actions/setup-node@v6
|
|
with:
|
|
node-version-file: .node-version
|
|
|
|
- name: Install NSIS cross-build and verification tools
|
|
shell: bash
|
|
run: |
|
|
set -euo pipefail
|
|
dpkg --add-architecture i386
|
|
apt-get update
|
|
apt-get install -y --no-install-recommends osslsigncode p7zip-full wine wine32:i386 wine64
|
|
rm -rf /var/lib/apt/lists/*
|
|
|
|
- name: Install Electron Builder dependencies
|
|
run: pnpm install --frozen-lockfile --ignore-scripts
|
|
|
|
- name: Align package version with release tag
|
|
working-directory: apps/desktop
|
|
run: node scripts/apply-ci-release-version.mjs "${TUTTI_DESKTOP_RELEASE_TAG}"
|
|
|
|
- name: Download unpacked Windows application
|
|
uses: actions/download-artifact@v4
|
|
with:
|
|
name: tutti-windows-prepared-${{ github.run_id }}
|
|
path: apps/desktop/dist/win-unpacked
|
|
|
|
- name: Connect SimplySign with bounded retries
|
|
shell: bash
|
|
run: bash .release-tooling/tools/scripts/windows/connect-simplysign-linux.sh
|
|
|
|
- name: Sign every executable in the unpacked application
|
|
shell: bash
|
|
run: >-
|
|
node .release-tooling/tools/scripts/windows/certum-sign-windows.cjs
|
|
--root apps/desktop/dist/win-unpacked
|
|
|
|
- name: Build and sign final NSIS artifacts
|
|
shell: bash
|
|
env:
|
|
TSH_WINDOWS_PREPACKAGED_DIR: ${{ github.workspace }}/apps/desktop/dist/win-unpacked
|
|
TSH_CERTUM_ELECTRON_BUILDER_SIGNER: ${{ github.workspace }}/.release-tooling/tools/scripts/windows/certum-sign-windows.cjs
|
|
run: pnpm --filter @tutti-os/desktop build:win:prepackaged
|
|
|
|
- name: Verify signed NSIS payload and embedded uninstaller
|
|
shell: bash
|
|
run: |
|
|
set -euo pipefail
|
|
mapfile -d '' installers < <(find apps/desktop/dist -maxdepth 1 -type f -name '*.exe' -print0)
|
|
if (( ${#installers[@]} != 1 )); then
|
|
echo "Expected exactly one signed Windows installer, found ${#installers[@]}." >&2
|
|
exit 1
|
|
fi
|
|
node .release-tooling/tools/scripts/windows/certum-sign-windows.cjs \
|
|
--verify-root apps/desktop/dist/win-unpacked
|
|
outer="${RUNNER_TEMP}/signed-nsis-outer"
|
|
mkdir -p "${outer}"
|
|
7z x -y "-o${outer}" "${installers[0]}" >/dev/null
|
|
node .release-tooling/tools/scripts/windows/certum-sign-windows.cjs --verify-root "${outer}"
|
|
node apps/desktop/scripts/validate-windows-release-artifacts.mjs \
|
|
apps/desktop/dist "${{ needs.resolve.outputs.release_channel }}" "${TUTTI_DESKTOP_RELEASE_TAG}"
|
|
|
|
- name: Upload signed Windows package
|
|
uses: actions/upload-artifact@v7
|
|
with:
|
|
name: tutti-windows-signed-${{ github.run_id }}
|
|
compression-level: 0
|
|
path: |
|
|
apps/desktop/dist/*.blockmap
|
|
apps/desktop/dist/*.exe
|
|
apps/desktop/dist/*.yml
|
|
if-no-files-found: error
|
|
retention-days: 7
|
|
|
|
verify-signed-windows:
|
|
name: Verify signed Windows package
|
|
needs: [resolve, sign-windows]
|
|
if: needs.resolve.outputs.release_dry_run != 'true'
|
|
runs-on: windows-latest
|
|
env:
|
|
TSH_WINDOWS_EXPECTED_PUBLISHER: ${{ vars.TSH_WINDOWS_EXPECTED_PUBLISHER }}
|
|
|
|
steps:
|
|
- name: Checkout release target
|
|
uses: actions/checkout@v6
|
|
with:
|
|
ref: ${{ needs.resolve.outputs.release_target }}
|
|
fetch-depth: 1
|
|
persist-credentials: false
|
|
|
|
- name: Download signed Windows package
|
|
uses: actions/download-artifact@v4
|
|
with:
|
|
name: tutti-windows-signed-${{ github.run_id }}
|
|
path: apps/desktop/dist
|
|
|
|
- name: Independently verify Publisher, Authenticode, and timestamp
|
|
shell: pwsh
|
|
run: |
|
|
$sevenZip = (Get-Command 7z.exe -ErrorAction Stop).Source
|
|
tools/scripts/windows/verify-certum-windows-package.ps1 `
|
|
-DistDirectory 'apps/desktop/dist' `
|
|
-SevenZipExecutable $sevenZip `
|
|
-ExpectedPublisher $env:TSH_WINDOWS_EXPECTED_PUBLISHER
|
|
|
|
- name: Publish verified Windows release artifacts
|
|
uses: actions/upload-artifact@v7
|
|
with:
|
|
name: tutti-desktop-release-assets-windows
|
|
compression-level: 0
|
|
path: |
|
|
apps/desktop/dist/*.blockmap
|
|
apps/desktop/dist/*.exe
|
|
apps/desktop/dist/*.yml
|
|
if-no-files-found: error
|
|
retention-days: 7
|
|
|
|
submit-store:
|
|
name: Submit stable package to Microsoft Store
|
|
needs: [resolve, promote]
|
|
if: >-
|
|
${{ needs.resolve.outputs.release_channel == 'stable' &&
|
|
needs.resolve.outputs.release_dry_run != 'true' &&
|
|
needs.resolve.outputs.release_candidate != 'true' &&
|
|
vars.TUTTI_WINDOWS_STORE_SUBMISSION_ENABLED == 'true' }}
|
|
uses: ./.github/workflows/desktop-store-submit.yml
|
|
with:
|
|
release_tag: ${{ needs.resolve.outputs.release_tag }}
|
|
store_environment: microsoft-store-production
|
|
submit: true
|
|
target_commit: ${{ needs.resolve.outputs.release_target }}
|
|
secrets: inherit
|
|
|
|
stage:
|
|
name: Stage Release Draft
|
|
needs:
|
|
[resolve, build-macos, build-windows, sign-windows, verify-signed-windows]
|
|
if: >-
|
|
${{ always() && needs.resolve.result == 'success' &&
|
|
(github.event_name == 'push' || needs.resolve.outputs.release_dry_run != 'true') &&
|
|
needs.build-macos.result == 'success' &&
|
|
needs.build-windows.result == 'success' &&
|
|
needs.sign-windows.result == 'success' &&
|
|
needs.verify-signed-windows.result == 'success' }}
|
|
runs-on: ubuntu-latest
|
|
outputs:
|
|
release_url: ${{ needs.resolve.outputs.release_candidate == 'true' && steps.resolve-candidate-release-url.outputs.release_url || steps.stage-release.outputs.url }}
|
|
env:
|
|
AWS_REGION: ${{ vars.AWS_REGION }}
|
|
TUTTI_ARTIFACTS_AWS_ROLE_ARN: ${{ vars.TUTTI_ARTIFACTS_AWS_ROLE_ARN }}
|
|
TUTTI_DESKTOP_RELEASE_ASSETS_BASE_URL: ${{ vars.TUTTI_DESKTOP_RELEASE_ASSETS_BASE_URL }}
|
|
TUTTI_DESKTOP_RELEASE_ASSETS_S3_BUCKET: ${{ vars.TUTTI_DESKTOP_RELEASE_ASSETS_S3_BUCKET }}
|
|
TUTTI_DESKTOP_RELEASE_ASSETS_S3_PREFIX: ${{ vars.TUTTI_DESKTOP_RELEASE_ASSETS_S3_PREFIX }}
|
|
TUTTI_DESKTOP_RELEASE_CHANNEL: ${{ needs.resolve.outputs.release_channel }}
|
|
TUTTI_DESKTOP_RELEASE_CANDIDATE: ${{ needs.resolve.outputs.release_candidate }}
|
|
TUTTI_DESKTOP_RELEASE_CANDIDATE_ID: ${{ needs.resolve.outputs.release_candidate_id }}
|
|
TUTTI_DESKTOP_RELEASE_CANDIDATE_TAG: ${{ needs.resolve.outputs.release_candidate_tag }}
|
|
TUTTI_DESKTOP_RELEASE_TAG: ${{ needs.resolve.outputs.release_tag }}
|
|
TUTTI_DESKTOP_RELEASE_TARGET: ${{ needs.resolve.outputs.release_target }}
|
|
TUTTI_DESKTOP_RELEASE_VERSION: ${{ needs.resolve.outputs.release_version }}
|
|
|
|
steps:
|
|
- name: Checkout
|
|
uses: actions/checkout@v6
|
|
with:
|
|
ref: ${{ env.TUTTI_DESKTOP_RELEASE_TARGET }}
|
|
fetch-depth: 0
|
|
|
|
- name: Setup Node.js
|
|
uses: actions/setup-node@v6
|
|
with:
|
|
node-version-file: .node-version
|
|
|
|
- name: Download macOS built artifacts
|
|
uses: actions/download-artifact@v4
|
|
with:
|
|
pattern: tutti-desktop-release-assets-macos-*
|
|
merge-multiple: false
|
|
path: release-assets-input/macos
|
|
|
|
- name: Download Windows built artifacts
|
|
uses: actions/download-artifact@v4
|
|
with:
|
|
name: tutti-desktop-release-assets-windows
|
|
path: release-assets-input/windows
|
|
|
|
- name: Merge macOS release artifacts and updater metadata
|
|
env:
|
|
RELEASE_CHANNEL: ${{ env.TUTTI_DESKTOP_RELEASE_CHANNEL }}
|
|
RELEASE_VERSION: ${{ env.TUTTI_DESKTOP_RELEASE_VERSION }}
|
|
run: |
|
|
node apps/desktop/scripts/merge-macos-release-artifacts.mjs \
|
|
release-assets-input/macos \
|
|
release-assets
|
|
|
|
- name: Add Windows release artifacts
|
|
shell: bash
|
|
run: |
|
|
shopt -s nullglob globstar
|
|
windows_files=(
|
|
release-assets-input/windows/**/*.blockmap
|
|
release-assets-input/windows/**/*.exe
|
|
release-assets-input/windows/**/*.yml
|
|
)
|
|
copied=0
|
|
for file in "${windows_files[@]}"; do
|
|
if [[ -f "${file}" ]]; then
|
|
cp "${file}" release-assets/"$(basename "${file}")"
|
|
copied=$((copied + 1))
|
|
fi
|
|
done
|
|
if [[ "${copied}" -eq 0 ]]; then
|
|
echo "No Windows release artifacts were downloaded" >&2
|
|
exit 1
|
|
fi
|
|
|
|
- name: Validate Windows release artifacts
|
|
run: node apps/desktop/scripts/validate-windows-release-artifacts.mjs release-assets "${TUTTI_DESKTOP_RELEASE_CHANNEL}" "${TUTTI_DESKTOP_RELEASE_TAG}"
|
|
|
|
- name: Generate desktop release summary
|
|
env:
|
|
AGNES_API_KEY: ${{ secrets.AGNES_API_KEY }}
|
|
RELEASE_CHANNEL: ${{ needs.resolve.outputs.release_channel }}
|
|
RELEASE_TAG: ${{ env.TUTTI_DESKTOP_RELEASE_TAG }}
|
|
RELEASE_TARGET: ${{ env.TUTTI_DESKTOP_RELEASE_TARGET }}
|
|
run: |
|
|
node apps/desktop/scripts/generate-release-summary.mjs \
|
|
--output release-assets/release-summary.json
|
|
|
|
- name: Validate desktop release summary
|
|
env:
|
|
RELEASE_CHANNEL: ${{ needs.resolve.outputs.release_channel }}
|
|
RELEASE_TAG: ${{ env.TUTTI_DESKTOP_RELEASE_TAG }}
|
|
RELEASE_TARGET: ${{ env.TUTTI_DESKTOP_RELEASE_TARGET }}
|
|
run: node apps/desktop/scripts/validate-release-summary.mjs release-assets/release-summary.json
|
|
|
|
- name: Generate checksums
|
|
run: node apps/desktop/scripts/generate-checksums.mjs release-assets
|
|
|
|
- name: Build stable release candidate manifest
|
|
if: ${{ needs.resolve.outputs.release_candidate == 'true' }}
|
|
env:
|
|
RELEASE_CANDIDATE_ID: ${{ needs.resolve.outputs.release_candidate_id }}
|
|
RELEASE_SOURCE_REF: ${{ needs.resolve.outputs.source_ref }}
|
|
RELEASE_TAG: ${{ env.TUTTI_DESKTOP_RELEASE_TAG }}
|
|
RELEASE_TARGET: ${{ env.TUTTI_DESKTOP_RELEASE_TARGET }}
|
|
RELEASE_VERSION: ${{ env.TUTTI_DESKTOP_RELEASE_VERSION }}
|
|
RELEASE_RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}
|
|
run: |
|
|
node apps/desktop/scripts/build-release-candidate-manifest.mjs \
|
|
release-assets/SHA256SUMS.txt \
|
|
release-assets/release-summary.json \
|
|
release-assets/release-candidate.json
|
|
|
|
- id: previous-release-tag
|
|
name: Resolve previous GitHub release tag
|
|
env:
|
|
RELEASE_CHANNEL: ${{ env.TUTTI_DESKTOP_RELEASE_CHANNEL }}
|
|
RELEASE_TAG: ${{ env.TUTTI_DESKTOP_RELEASE_TAG }}
|
|
run: |
|
|
previous_tag="$(node apps/desktop/scripts/resolve-previous-release-tag.mjs)"
|
|
echo "tag=${previous_tag}" >> "$GITHUB_OUTPUT"
|
|
|
|
- id: stage-release
|
|
name: Stage GitHub release assets
|
|
if: ${{ needs.resolve.outputs.release_candidate != 'true' }}
|
|
uses: softprops/action-gh-release@v2
|
|
with:
|
|
token: ${{ secrets.GITHUB_TOKEN }}
|
|
tag_name: ${{ env.TUTTI_DESKTOP_RELEASE_TAG }}
|
|
target_commitish: ${{ env.TUTTI_DESKTOP_RELEASE_TARGET }}
|
|
name: ${{ env.TUTTI_DESKTOP_RELEASE_TAG }}
|
|
draft: true
|
|
prerelease: ${{ needs.resolve.outputs.release_prerelease == 'true' }}
|
|
make_latest: ${{ needs.resolve.outputs.release_make_latest == 'true' }}
|
|
generate_release_notes: true
|
|
previous_tag: ${{ steps.previous-release-tag.outputs.tag }}
|
|
files: |
|
|
release-assets/*
|
|
|
|
- id: stage-candidate-release
|
|
name: Create or refresh stable GitHub draft
|
|
if: ${{ needs.resolve.outputs.release_candidate == 'true' }}
|
|
env:
|
|
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
shell: bash
|
|
run: |
|
|
release_id="$(gh api "repos/${GITHUB_REPOSITORY}/releases?per_page=100" \
|
|
--paginate \
|
|
--jq ".[] | select(.draft and .tag_name == \"${TUTTI_DESKTOP_RELEASE_CANDIDATE_TAG}\") | .id" | head -1)"
|
|
if [[ -z "${release_id}" ]]; then
|
|
gh api --method POST "repos/${GITHUB_REPOSITORY}/releases" \
|
|
-f tag_name="${TUTTI_DESKTOP_RELEASE_CANDIDATE_TAG}" \
|
|
-f target_commitish="${TUTTI_DESKTOP_RELEASE_TARGET}" \
|
|
-f name="${TUTTI_DESKTOP_RELEASE_TAG}" \
|
|
-F draft=true \
|
|
-F prerelease=false \
|
|
-F generate_release_notes=true > staged-release.json
|
|
else
|
|
git tag -f "${TUTTI_DESKTOP_RELEASE_CANDIDATE_TAG}" "${TUTTI_DESKTOP_RELEASE_TARGET}"
|
|
git push origin "refs/tags/${TUTTI_DESKTOP_RELEASE_CANDIDATE_TAG}" --force
|
|
gh api --method PATCH "repos/${GITHUB_REPOSITORY}/releases/${release_id}" \
|
|
-f target_commitish="${TUTTI_DESKTOP_RELEASE_TARGET}" \
|
|
-f name="${TUTTI_DESKTOP_RELEASE_TAG}" \
|
|
-F draft=true \
|
|
-F prerelease=false > staged-release.json
|
|
gh api "repos/${GITHUB_REPOSITORY}/releases/${release_id}/assets?per_page=100" \
|
|
--jq '.[].id' |
|
|
while IFS= read -r asset_id; do
|
|
gh api --method DELETE "repos/${GITHUB_REPOSITORY}/releases/assets/${asset_id}"
|
|
done
|
|
fi
|
|
node apps/desktop/scripts/upload-github-release-assets.mjs \
|
|
staged-release.json \
|
|
release-assets
|
|
echo "release_id=$(jq -r .id staged-release.json)" >> "$GITHUB_OUTPUT"
|
|
|
|
- name: Install AWS CLI
|
|
timeout-minutes: 5
|
|
run: |
|
|
if ! command -v aws >/dev/null 2>&1; then
|
|
case "$(uname -m)" in
|
|
x86_64) awscli_arch="x86_64" ;;
|
|
aarch64|arm64) awscli_arch="aarch64" ;;
|
|
*)
|
|
echo "Unsupported architecture for AWS CLI installer: $(uname -m)" >&2
|
|
exit 1
|
|
;;
|
|
esac
|
|
awscli_dir="$(mktemp -d "${RUNNER_TEMP}/aws-cli.XXXXXX")"
|
|
curl --fail --silent --show-error --location \
|
|
--retry 3 \
|
|
--retry-all-errors \
|
|
--connect-timeout 10 \
|
|
--max-time 180 \
|
|
"https://awscli.amazonaws.com/awscli-exe-linux-${awscli_arch}.zip" \
|
|
--output "${awscli_dir}/awscliv2.zip"
|
|
python3 -m zipfile -e "${awscli_dir}/awscliv2.zip" "${awscli_dir}"
|
|
sudo "${awscli_dir}/aws/install" --update
|
|
fi
|
|
aws --version
|
|
|
|
- name: Validate AWS S3 release asset configuration
|
|
run: |
|
|
missing=()
|
|
for name in AWS_REGION TUTTI_ARTIFACTS_AWS_ROLE_ARN TUTTI_DESKTOP_RELEASE_ASSETS_S3_BUCKET TUTTI_DESKTOP_RELEASE_ASSETS_S3_PREFIX; do
|
|
if [[ -z "${!name:-}" ]]; then
|
|
missing+=("$name")
|
|
fi
|
|
done
|
|
if (( ${#missing[@]} > 0 )); then
|
|
echo "Missing required AWS S3 release asset configuration: ${missing[*]}" >&2
|
|
exit 1
|
|
fi
|
|
if [[ -z "${TUTTI_DESKTOP_RELEASE_ASSETS_BASE_URL:-}" ]]; then
|
|
echo "TUTTI_DESKTOP_RELEASE_ASSETS_BASE_URL=https://${TUTTI_DESKTOP_RELEASE_ASSETS_S3_BUCKET}.s3-accelerate.amazonaws.com/${TUTTI_DESKTOP_RELEASE_ASSETS_S3_PREFIX%/}" >> "$GITHUB_ENV"
|
|
fi
|
|
|
|
- name: Configure AWS credentials
|
|
uses: aws-actions/configure-aws-credentials@v4
|
|
with:
|
|
role-to-assume: ${{ env.TUTTI_ARTIFACTS_AWS_ROLE_ARN }}
|
|
aws-region: ${{ env.AWS_REGION }}
|
|
|
|
- name: Upload immutable draft assets to AWS S3
|
|
run: |
|
|
release_asset_path="${TUTTI_DESKTOP_RELEASE_TAG}"
|
|
if [[ "${TUTTI_DESKTOP_RELEASE_CANDIDATE}" == "true" ]]; then
|
|
release_asset_path="candidates/${TUTTI_DESKTOP_RELEASE_CANDIDATE_ID}"
|
|
fi
|
|
remote_dir="s3://${TUTTI_DESKTOP_RELEASE_ASSETS_S3_BUCKET}/${TUTTI_DESKTOP_RELEASE_ASSETS_S3_PREFIX%/}/${release_asset_path}"
|
|
aws s3 cp release-assets "${remote_dir}" \
|
|
--recursive \
|
|
--cache-control "public, max-age=31536000, immutable"
|
|
|
|
- name: Upload desktop release summary artifact
|
|
uses: actions/upload-artifact@v7
|
|
with:
|
|
name: tutti-desktop-release-summary
|
|
path: release-assets/release-summary.json
|
|
|
|
- name: Update release notes with summary and direct downloads
|
|
env:
|
|
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
run: |
|
|
export RELEASE_TAG="${TUTTI_DESKTOP_RELEASE_TAG}"
|
|
export RELEASE_ASSET_BASE_URL="${TUTTI_DESKTOP_RELEASE_ASSETS_BASE_URL}"
|
|
if [[ "${TUTTI_DESKTOP_RELEASE_CANDIDATE}" == "true" ]]; then
|
|
export RELEASE_ASSET_PATH="candidates/${TUTTI_DESKTOP_RELEASE_CANDIDATE_ID}"
|
|
fi
|
|
github_release_tag="${TUTTI_DESKTOP_RELEASE_TAG}"
|
|
if [[ "${TUTTI_DESKTOP_RELEASE_CANDIDATE}" == "true" ]]; then
|
|
github_release_tag="${TUTTI_DESKTOP_RELEASE_CANDIDATE_TAG}"
|
|
fi
|
|
gh release view "${github_release_tag}" --json body --jq .body > release-body.md
|
|
node apps/desktop/scripts/upsert-release-summary.mjs \
|
|
release-body.md \
|
|
release-assets/release-summary.json \
|
|
release-body-with-summary.md
|
|
node apps/desktop/scripts/upsert-release-download-links.mjs \
|
|
release-body-with-summary.md \
|
|
release-assets \
|
|
updated-release-body.md
|
|
gh release edit "${github_release_tag}" --notes-file updated-release-body.md
|
|
|
|
- id: resolve-candidate-release-url
|
|
name: Resolve current stable GitHub draft URL
|
|
if: ${{ needs.resolve.outputs.release_candidate == 'true' }}
|
|
env:
|
|
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
RELEASE_ID: ${{ steps.stage-candidate-release.outputs.release_id }}
|
|
run: |
|
|
if [[ -z "${RELEASE_ID}" || "${RELEASE_ID}" == "null" ]]; then
|
|
echo "Stable GitHub draft release id is missing" >&2
|
|
exit 1
|
|
fi
|
|
release_url="$(gh api "repos/${GITHUB_REPOSITORY}/releases/${RELEASE_ID}" --jq .html_url)"
|
|
echo "release_url=${release_url}" >> "$GITHUB_OUTPUT"
|
|
|
|
promote:
|
|
name: Promote Release
|
|
needs: [resolve, stage]
|
|
if: ${{ always() && needs.stage.result == 'success' && needs.resolve.outputs.release_candidate != 'true' }}
|
|
uses: ./.github/workflows/desktop-release-promote.yml
|
|
with:
|
|
release_tag: ${{ needs.resolve.outputs.release_tag }}
|
|
expected_target: ${{ needs.resolve.outputs.release_target }}
|
|
expected_channel: ${{ needs.resolve.outputs.release_channel }}
|
|
source_ref: ${{ github.ref_name }}
|
|
notify_feishu: ${{ needs.resolve.outputs.notify_feishu == 'true' }}
|
|
secrets:
|
|
FEISHU_RELEASE_WEBHOOK_URL: ${{ secrets.FEISHU_RELEASE_WEBHOOK_URL }}
|
|
|
|
notify-candidate-feishu:
|
|
name: Notify Release Candidate
|
|
needs: [resolve, stage]
|
|
if: ${{ always() && needs.stage.result == 'success' && needs.resolve.outputs.notify_feishu == 'true' && needs.resolve.outputs.release_candidate == 'true' }}
|
|
runs-on: ubuntu-latest
|
|
env:
|
|
FEISHU_WEBHOOK_URL: ${{ secrets.FEISHU_RELEASE_WEBHOOK_URL }}
|
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
TUTTI_DESKTOP_RELEASE_ASSETS_BASE_URL: ${{ vars.TUTTI_DESKTOP_RELEASE_ASSETS_BASE_URL }}
|
|
TUTTI_DESKTOP_RELEASE_ASSETS_S3_BUCKET: ${{ vars.TUTTI_DESKTOP_RELEASE_ASSETS_S3_BUCKET }}
|
|
TUTTI_DESKTOP_RELEASE_ASSETS_S3_PREFIX: ${{ vars.TUTTI_DESKTOP_RELEASE_ASSETS_S3_PREFIX }}
|
|
RELEASE_ACTOR: ${{ github.actor }}
|
|
RELEASE_BRANCH: ${{ github.ref_type == 'branch' && github.ref_name || '' }}
|
|
RELEASE_ASSET_PATH: candidates/${{ needs.resolve.outputs.release_candidate_id }}
|
|
RELEASE_CANDIDATE_ID: ${{ needs.resolve.outputs.release_candidate_id }}
|
|
RELEASE_LOOKUP_TAG: ${{ needs.resolve.outputs.release_candidate_tag }}
|
|
RELEASE_PUBLICATION_STATUS: candidate
|
|
RELEASE_TAG: ${{ needs.resolve.outputs.release_tag }}
|
|
RELEASE_TARGET: ${{ needs.resolve.outputs.release_target }}
|
|
RELEASE_URL: ${{ needs.stage.outputs.release_url }}
|
|
RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}
|
|
PROMOTION_URL: ${{ github.server_url }}/${{ github.repository }}/actions/workflows/desktop-release-promote.yml
|
|
|
|
steps:
|
|
- name: Checkout notification script
|
|
if: env.FEISHU_WEBHOOK_URL != ''
|
|
uses: actions/checkout@v6
|
|
with:
|
|
ref: ${{ needs.resolve.outputs.release_target }}
|
|
|
|
- name: Setup Node.js
|
|
if: env.FEISHU_WEBHOOK_URL != ''
|
|
uses: actions/setup-node@v6
|
|
with:
|
|
node-version-file: .node-version
|
|
|
|
- name: Download release summary
|
|
if: env.FEISHU_WEBHOOK_URL != ''
|
|
uses: actions/download-artifact@v4
|
|
with:
|
|
name: tutti-desktop-release-summary
|
|
path: release-summary
|
|
|
|
- name: Send candidate release card
|
|
if: env.FEISHU_WEBHOOK_URL != ''
|
|
env:
|
|
RELEASE_SUMMARY_PATH: release-summary/release-summary.json
|
|
run: node apps/desktop/scripts/send-release-feishu-card.mjs
|
|
|
|
- name: Skip Feishu card
|
|
if: env.FEISHU_WEBHOOK_URL == ''
|
|
run: echo "FEISHU_RELEASE_WEBHOOK_URL is not configured; skipping candidate notification."
|