1232 lines
52 KiB
YAML
1232 lines
52 KiB
YAML
name: Release App
|
|
|
|
on:
|
|
push:
|
|
tags:
|
|
- "v*"
|
|
workflow_dispatch:
|
|
inputs:
|
|
benchmark_only:
|
|
description: "Build the release matrix for timing only; never create tags or publish artifacts"
|
|
required: false
|
|
type: boolean
|
|
default: false
|
|
bump:
|
|
description: "Cut a fresh release: bump from the highest v* tag and tag origin/dev HEAD"
|
|
required: false
|
|
type: choice
|
|
options:
|
|
- patch
|
|
- minor
|
|
- major
|
|
default: patch
|
|
version:
|
|
description: "Explicit version for a fresh release (X.Y.Z, overrides bump)"
|
|
required: false
|
|
type: string
|
|
tag:
|
|
description: "Existing tag to re-run for recovery (e.g., v0.1.2). Skips tag creation."
|
|
required: false
|
|
type: string
|
|
release_name:
|
|
description: "Release title (defaults to OpenWork <tag>)"
|
|
required: false
|
|
type: string
|
|
release_body:
|
|
description: "Release notes body in Markdown (defaults to a short placeholder)"
|
|
required: false
|
|
type: string
|
|
draft:
|
|
description: "Create the GitHub Release as a draft until assets are ready (publish-release flips it public)"
|
|
required: false
|
|
type: boolean
|
|
default: true
|
|
prerelease:
|
|
description: "Mark the GitHub Release as a prerelease"
|
|
required: false
|
|
type: boolean
|
|
default: false
|
|
notarize:
|
|
description: "Notarize macOS builds (requires Apple team configured)"
|
|
required: false
|
|
type: boolean
|
|
default: true
|
|
publish_npm:
|
|
description: "Publish openwork-server to npm if its version changed"
|
|
required: false
|
|
type: boolean
|
|
default: true
|
|
publish_daytona_snapshot:
|
|
description: "Build + push Daytona worker snapshot"
|
|
required: false
|
|
type: boolean
|
|
default: true
|
|
build_electron:
|
|
description: "Build + publish Electron desktop artifacts (macOS/Linux/Windows)"
|
|
required: false
|
|
type: boolean
|
|
default: true
|
|
sign_windows:
|
|
description: "Sign Windows Electron installers with Azure Artifact Signing"
|
|
required: false
|
|
type: boolean
|
|
default: true
|
|
|
|
permissions:
|
|
actions: read
|
|
contents: write
|
|
|
|
concurrency:
|
|
# Tag pushes dedupe per tag ref; dispatches keyed on the recovery tag or the
|
|
# run id so cutting a fresh release never cancels another in-flight release.
|
|
group: ${{ github.workflow }}-${{ github.event_name == 'push' && github.ref || inputs.tag || github.run_id }}
|
|
cancel-in-progress: true
|
|
|
|
jobs:
|
|
resolve-production:
|
|
name: Resolve Release Metadata
|
|
runs-on: blacksmith-4vcpu-ubuntu-2404
|
|
# WARDEN_PRIVATE_KEY lives in this environment; its deployment policy
|
|
# (dev + v* tags) means the key is only reachable from reviewed code —
|
|
# a feature-branch workflow edit cannot mint the tag-push token.
|
|
environment: warden-clearance
|
|
# Fresh dispatch cuts push their tag with the diff-warden app token, and
|
|
# app-token pushes DO retrigger this workflow via push (unlike
|
|
# GITHUB_TOKEN). The dispatch run that created the tag owns the release,
|
|
# so the duplicate push run skips here — and with it every job that
|
|
# needs resolve-release.
|
|
if: github.event.inputs.benchmark_only != 'true' && (github.event_name != 'push' || github.actor != 'diff-warden[bot]')
|
|
outputs:
|
|
release_tag: ${{ steps.resolve.outputs.release_tag }}
|
|
release_version: ${{ steps.resolve.outputs.release_version }}
|
|
release_mode: ${{ steps.tag.outputs.release_mode }}
|
|
release_name: ${{ steps.resolve.outputs.release_name }}
|
|
release_body: ${{ steps.resolve.outputs.release_body }}
|
|
draft: ${{ steps.resolve.outputs.draft }}
|
|
prerelease: ${{ steps.resolve.outputs.prerelease }}
|
|
notarize: ${{ steps.resolve.outputs.notarize }}
|
|
build_electron: ${{ steps.resolve.outputs.build_electron }}
|
|
sign_windows: ${{ steps.resolve.outputs.sign_windows }}
|
|
publish_npm: ${{ steps.resolve.outputs.publish_npm }}
|
|
publish_daytona_snapshot: ${{ steps.resolve.outputs.publish_daytona_snapshot }}
|
|
benchmark_only: "false"
|
|
steps:
|
|
# Tag creation was admin-only before the app-token flow (the v* tag
|
|
# ruleset gated it at push time). The app token would let ANY dispatch
|
|
# release, so restore the old bar here: dispatching actor must be a
|
|
# repo admin, or explicitly listed in the RELEASE_MANAGERS repo
|
|
# variable (comma-separated GitHub logins). Tag pushes skip this —
|
|
# they are still gated by the ruleset itself. This check lives on a
|
|
# protected branch, and the token mint below is environment-gated to
|
|
# dev + v* refs, so a feature-branch copy of this workflow can neither
|
|
# drop the check nor reach the key.
|
|
- name: Require release permission
|
|
if: github.event_name == 'workflow_dispatch'
|
|
shell: bash
|
|
env:
|
|
GH_TOKEN: ${{ github.token }}
|
|
ACTOR: ${{ github.actor }}
|
|
RELEASE_MANAGERS: ${{ vars.RELEASE_MANAGERS }}
|
|
run: |
|
|
set -euo pipefail
|
|
for allowed in ${RELEASE_MANAGERS//,/ }; do
|
|
if [ "$allowed" = "$ACTOR" ]; then
|
|
echo "${ACTOR} is listed in RELEASE_MANAGERS."
|
|
exit 0
|
|
fi
|
|
done
|
|
permission="$(gh api "repos/${GITHUB_REPOSITORY}/collaborators/${ACTOR}/permission" --jq .permission)"
|
|
if [ "$permission" != "admin" ]; then
|
|
echo "::error::Release dispatch is restricted: ${ACTOR} has '${permission}' but needs repo admin (or a RELEASE_MANAGERS listing)." >&2
|
|
exit 1
|
|
fi
|
|
echo "${ACTOR} is a repo admin."
|
|
|
|
- name: Checkout
|
|
uses: actions/checkout@v6
|
|
with:
|
|
# Tag resolution compares against every v* tag, and fresh dispatch
|
|
# releases tag origin/dev HEAD — both need full history + tags.
|
|
fetch-depth: 0
|
|
|
|
- name: Setup Node
|
|
uses: actions/setup-node@v6
|
|
with:
|
|
node-version: 24
|
|
|
|
# The built-in GitHub Actions app cannot be a ruleset bypass actor
|
|
# (GitHub rejects it), so fresh cuts push their tag as the org-owned
|
|
# diff-warden app, which is on the v* ruleset bypass list.
|
|
- name: Mint tag-push token
|
|
id: tag-token
|
|
if: github.event_name == 'workflow_dispatch' && github.event.inputs.tag == ''
|
|
continue-on-error: true
|
|
uses: actions/create-github-app-token@bcd2ba49218906704ab6c1aa796996da409d3eb1 # v3
|
|
with:
|
|
app-id: ${{ secrets.WARDEN_APP_ID }}
|
|
private-key: ${{ secrets.WARDEN_PRIVATE_KEY }}
|
|
|
|
- name: Resolve release tag
|
|
id: tag
|
|
shell: bash
|
|
env:
|
|
INPUT_TAG: ${{ github.event.inputs.tag }}
|
|
INPUT_VERSION: ${{ github.event.inputs.version }}
|
|
INPUT_BUMP: ${{ github.event.inputs.bump }}
|
|
TAG_PUSH_TOKEN: ${{ steps.tag-token.outputs.token }}
|
|
run: |
|
|
set -euo pipefail
|
|
|
|
if [ "${GITHUB_EVENT_NAME}" = "push" ]; then
|
|
# Manual tag-first push (admin expedited path). The tag already
|
|
# exists; the Expedited Release Audit workflow covers tags whose
|
|
# commit is not on dev.
|
|
TAG="${GITHUB_REF_NAME}"
|
|
MODE="fresh"
|
|
elif [ -n "${INPUT_TAG:-}" ]; then
|
|
TAG="${INPUT_TAG}"
|
|
if [[ "$TAG" != v* ]]; then
|
|
TAG="v${TAG}"
|
|
fi
|
|
if ! git rev-parse -q --verify "refs/tags/${TAG}" >/dev/null; then
|
|
echo "Recovery dispatch requires an existing tag; ${TAG} was not found." >&2
|
|
exit 1
|
|
fi
|
|
MODE="recovery"
|
|
else
|
|
# Fresh release with no commits anywhere: versions live only in
|
|
# git tags, so compute the next one and tag origin/dev HEAD.
|
|
VERSION="${INPUT_VERSION:-}"
|
|
if [ -n "$VERSION" ]; then
|
|
VERSION="${VERSION#v}"
|
|
else
|
|
VERSION="$(node scripts/release/versions.mjs next --bump "${INPUT_BUMP:-patch}")"
|
|
fi
|
|
TAG="v${VERSION}"
|
|
MODE="fresh"
|
|
if git rev-parse -q --verify "refs/tags/${TAG}" >/dev/null; then
|
|
echo "Tag ${TAG} already exists. Re-run with tag=${TAG} (recovery) instead." >&2
|
|
exit 1
|
|
fi
|
|
git fetch origin dev
|
|
SHA="$(git rev-parse origin/dev)"
|
|
# Create the tag ref via REST as the diff-warden app (a v* tag
|
|
# ruleset bypass actor). Ruleset bypass attributes REST ref
|
|
# creation to the app; a git-over-https push with the same
|
|
# installation token is NOT attributed and gets rejected —
|
|
# proven by the tag-push smoke — so keep this a REST call. The
|
|
# app-created tag retriggers this workflow, but the duplicate
|
|
# run is skipped by the actor guard on this job.
|
|
if [ -n "${TAG_PUSH_TOKEN:-}" ]; then
|
|
if ! GH_TOKEN="${TAG_PUSH_TOKEN}" gh api -X POST "repos/${GITHUB_REPOSITORY}/git/refs" \
|
|
-f ref="refs/tags/${TAG}" -f sha="${SHA}"; then
|
|
echo "::error::Creating ${TAG} was rejected by the v* tag ruleset. Ensure the diff-warden app is on its bypass list (Settings → Rules → Protect v* release tags), or push the tag manually as an admin and rerun." >&2
|
|
exit 1
|
|
fi
|
|
else
|
|
echo "::warning::diff-warden app token unavailable (WARDEN_APP_ID / WARDEN_PRIVATE_KEY in the warden-clearance environment); trying GITHUB_TOKEN."
|
|
git tag "$TAG" "$SHA"
|
|
if ! git push origin "refs/tags/${TAG}"; then
|
|
echo "::error::Pushing ${TAG} was rejected by the v* tag ruleset. Push the tag manually as an admin and rerun." >&2
|
|
exit 1
|
|
fi
|
|
fi
|
|
echo "Created ${TAG} at ${SHA} (origin/dev HEAD)."
|
|
fi
|
|
|
|
echo "release_tag=${TAG}" >> "$GITHUB_OUTPUT"
|
|
echo "release_mode=${MODE}" >> "$GITHUB_OUTPUT"
|
|
|
|
- name: Resolve metadata
|
|
id: resolve
|
|
shell: bash
|
|
env:
|
|
RESOLVED_TAG: ${{ steps.tag.outputs.release_tag }}
|
|
INPUT_RELEASE_NAME: ${{ github.event.inputs.release_name }}
|
|
INPUT_RELEASE_BODY: ${{ github.event.inputs.release_body }}
|
|
INPUT_DRAFT: ${{ github.event.inputs.draft }}
|
|
INPUT_PRERELEASE: ${{ github.event.inputs.prerelease }}
|
|
INPUT_NOTARIZE: ${{ github.event.inputs.notarize }}
|
|
INPUT_BUILD_ELECTRON: ${{ github.event.inputs.build_electron }}
|
|
INPUT_SIGN_WINDOWS: ${{ github.event.inputs.sign_windows }}
|
|
INPUT_PUBLISH_NPM: ${{ github.event.inputs.publish_npm }}
|
|
INPUT_PUBLISH_DAYTONA_SNAPSHOT: ${{ github.event.inputs.publish_daytona_snapshot }}
|
|
DEFAULT_PUBLISH_NPM: ${{ vars.RELEASE_PUBLISH_NPM }}
|
|
DEFAULT_PUBLISH_DAYTONA_SNAPSHOT: ${{ vars.RELEASE_PUBLISH_DAYTONA_SNAPSHOT }}
|
|
DEFAULT_NOTARIZE: ${{ vars.MACOS_NOTARIZE }}
|
|
DEFAULT_BUILD_ELECTRON: ${{ vars.RELEASE_PUBLISH_ELECTRON }}
|
|
DEFAULT_SIGN_WINDOWS: ${{ vars.RELEASE_SIGN_WINDOWS }}
|
|
run: |
|
|
set -euo pipefail
|
|
|
|
TAG="${RESOLVED_TAG}"
|
|
if [[ ! "$TAG" =~ ^v[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
|
|
echo "Invalid release tag: $TAG (expected stable vX.Y.Z)" >&2
|
|
exit 1
|
|
fi
|
|
|
|
RELEASE_NAME_INPUT="${INPUT_RELEASE_NAME:-}"
|
|
if [ -n "$RELEASE_NAME_INPUT" ]; then
|
|
RELEASE_NAME="$RELEASE_NAME_INPUT"
|
|
else
|
|
RELEASE_NAME="OpenWork $TAG"
|
|
fi
|
|
|
|
RELEASE_BODY_INPUT="${INPUT_RELEASE_BODY:-}"
|
|
if [ -n "$RELEASE_BODY_INPUT" ]; then
|
|
RELEASE_BODY="$RELEASE_BODY_INPUT"
|
|
else
|
|
printf -v RELEASE_BODY '%s\n\nOpenWork %s desktop release.\n\n%s\n%s\n%s\n%s' \
|
|
"## What's new" \
|
|
"$TAG" \
|
|
"- Public artifacts use the openwork-* naming convention." \
|
|
"- Cloud artifacts use openwork-cloud-* and require sign-in without enterprise activation." \
|
|
"- Enterprise artifacts use openwork-enterprise-* while retaining the standard OpenWork app identity." \
|
|
"- Public, Cloud, and enterprise Electron builds use separate updater manifests on this release."
|
|
fi
|
|
|
|
# A release must never be public while its artifacts are still
|
|
# building: default to draft for every entry path (tag push AND
|
|
# fresh dispatch). publish-release flips it public at the end.
|
|
draft="${INPUT_DRAFT:-true}"
|
|
prerelease="${INPUT_PRERELEASE:-false}"
|
|
notarize="${INPUT_NOTARIZE:-}"
|
|
if [ -z "$notarize" ]; then
|
|
notarize="${DEFAULT_NOTARIZE:-true}"
|
|
fi
|
|
|
|
build_electron="${INPUT_BUILD_ELECTRON:-}"
|
|
if [ -z "$build_electron" ]; then
|
|
build_electron="${DEFAULT_BUILD_ELECTRON:-true}"
|
|
fi
|
|
|
|
sign_windows="${INPUT_SIGN_WINDOWS:-}"
|
|
if [ -z "$sign_windows" ]; then
|
|
sign_windows="${DEFAULT_SIGN_WINDOWS:-false}"
|
|
fi
|
|
|
|
if [ "$sign_windows" = "true" ]; then
|
|
WINDOWS_SIGNED_NOTE='*Windows installers are signed using Microsoft Artifact Signing.*'
|
|
printf -v RELEASE_BODY '%s\n\n%s' "$RELEASE_BODY" "$WINDOWS_SIGNED_NOTE"
|
|
else
|
|
WINDOWS_UNSIGNED_NOTE='*Windows installer is temporarily unsigned while production code signing is being finalized.*'
|
|
printf -v RELEASE_BODY '%s\n\n%s' "$RELEASE_BODY" "$WINDOWS_UNSIGNED_NOTE"
|
|
fi
|
|
|
|
publish_npm="${INPUT_PUBLISH_NPM:-}"
|
|
if [ -z "$publish_npm" ]; then
|
|
publish_npm="${DEFAULT_PUBLISH_NPM:-true}"
|
|
fi
|
|
|
|
publish_daytona_snapshot="${INPUT_PUBLISH_DAYTONA_SNAPSHOT:-}"
|
|
if [ -z "$publish_daytona_snapshot" ]; then
|
|
publish_daytona_snapshot="${DEFAULT_PUBLISH_DAYTONA_SNAPSHOT:-true}"
|
|
fi
|
|
|
|
TAG="${TAG//$'\n'/}"
|
|
TAG="${TAG//$'\r'/}"
|
|
RELEASE_NAME="${RELEASE_NAME//$'\n'/ }"
|
|
RELEASE_NAME="${RELEASE_NAME//$'\r'/ }"
|
|
|
|
echo "release_tag=$TAG" >> "$GITHUB_OUTPUT"
|
|
echo "release_version=${TAG#v}" >> "$GITHUB_OUTPUT"
|
|
echo "release_name=$RELEASE_NAME" >> "$GITHUB_OUTPUT"
|
|
echo "draft=$draft" >> "$GITHUB_OUTPUT"
|
|
echo "prerelease=$prerelease" >> "$GITHUB_OUTPUT"
|
|
echo "notarize=$notarize" >> "$GITHUB_OUTPUT"
|
|
echo "build_electron=$build_electron" >> "$GITHUB_OUTPUT"
|
|
echo "sign_windows=$sign_windows" >> "$GITHUB_OUTPUT"
|
|
echo "publish_npm=$publish_npm" >> "$GITHUB_OUTPUT"
|
|
echo "publish_daytona_snapshot=$publish_daytona_snapshot" >> "$GITHUB_OUTPUT"
|
|
{
|
|
echo "release_body<<__OPENWORK_RELEASE_BODY_EOF__"
|
|
printf '%s\n' "$RELEASE_BODY"
|
|
echo "__OPENWORK_RELEASE_BODY_EOF__"
|
|
} >> "$GITHUB_OUTPUT"
|
|
|
|
- name: Create release if missing
|
|
shell: bash
|
|
env:
|
|
GH_TOKEN: ${{ github.token }}
|
|
run: |
|
|
set -euo pipefail
|
|
|
|
BODY_FILE="$RUNNER_TEMP/release_body.md"
|
|
printf '%s\n' "${{ steps.resolve.outputs.release_body }}" > "$BODY_FILE"
|
|
|
|
if gh release view "${{ steps.resolve.outputs.release_tag }}" --repo "$GITHUB_REPOSITORY" >/dev/null 2>&1; then
|
|
echo "Release ${{ steps.resolve.outputs.release_tag }} already exists; skipping create."
|
|
exit 0
|
|
fi
|
|
|
|
PRERELEASE_FLAG=""
|
|
if [ "${{ steps.resolve.outputs.prerelease }}" = "true" ]; then
|
|
PRERELEASE_FLAG="--prerelease"
|
|
fi
|
|
|
|
# A new release must never be public while its artifacts are still
|
|
# building. The final publish-release job is the only place that
|
|
# removes draft status, after every required asset has been checked.
|
|
gh release create "${{ steps.resolve.outputs.release_tag }}" \
|
|
--repo "$GITHUB_REPOSITORY" \
|
|
--title "${{ steps.resolve.outputs.release_name }}" \
|
|
--notes-file "$BODY_FILE" \
|
|
--draft $PRERELEASE_FLAG
|
|
|
|
resolve-benchmark:
|
|
name: Resolve Benchmark Metadata
|
|
if: github.event.inputs.benchmark_only == 'true'
|
|
runs-on: blacksmith-4vcpu-ubuntu-2404
|
|
outputs:
|
|
release_tag: ${{ steps.resolve.outputs.release_tag }}
|
|
release_version: ${{ steps.resolve.outputs.release_version }}
|
|
release_mode: ${{ steps.resolve.outputs.release_mode }}
|
|
notarize: ${{ steps.resolve.outputs.notarize }}
|
|
build_electron: ${{ steps.resolve.outputs.build_electron }}
|
|
sign_windows: ${{ steps.resolve.outputs.sign_windows }}
|
|
publish_npm: ${{ steps.resolve.outputs.publish_npm }}
|
|
publish_daytona_snapshot: ${{ steps.resolve.outputs.publish_daytona_snapshot }}
|
|
benchmark_only: ${{ steps.resolve.outputs.benchmark_only }}
|
|
prerelease: ${{ steps.resolve.outputs.prerelease }}
|
|
steps:
|
|
- name: Resolve non-publishing benchmark context
|
|
id: resolve
|
|
shell: bash
|
|
run: |
|
|
set -euo pipefail
|
|
{
|
|
echo "release_tag=${GITHUB_SHA}"
|
|
echo "release_version=0.0.0-benchmark.${GITHUB_RUN_ID}.${GITHUB_RUN_ATTEMPT}"
|
|
echo "release_mode=benchmark"
|
|
echo "notarize=true"
|
|
echo "build_electron=true"
|
|
echo "sign_windows=true"
|
|
echo "publish_npm=false"
|
|
echo "publish_daytona_snapshot=false"
|
|
echo "benchmark_only=true"
|
|
echo "prerelease=true"
|
|
} >> "$GITHUB_OUTPUT"
|
|
|
|
resolve-release:
|
|
name: Select Release Context
|
|
needs: [resolve-production, resolve-benchmark]
|
|
if: |
|
|
always() &&
|
|
(needs.resolve-production.result == 'success' || needs.resolve-benchmark.result == 'success')
|
|
runs-on: blacksmith-4vcpu-ubuntu-2404
|
|
outputs:
|
|
release_tag: ${{ needs.resolve-production.outputs.release_tag || needs.resolve-benchmark.outputs.release_tag }}
|
|
release_version: ${{ needs.resolve-production.outputs.release_version || needs.resolve-benchmark.outputs.release_version }}
|
|
release_mode: ${{ needs.resolve-production.outputs.release_mode || needs.resolve-benchmark.outputs.release_mode }}
|
|
notarize: ${{ needs.resolve-production.outputs.notarize || needs.resolve-benchmark.outputs.notarize }}
|
|
build_electron: ${{ needs.resolve-production.outputs.build_electron || needs.resolve-benchmark.outputs.build_electron }}
|
|
sign_windows: ${{ needs.resolve-production.outputs.sign_windows || needs.resolve-benchmark.outputs.sign_windows }}
|
|
publish_npm: ${{ needs.resolve-production.outputs.publish_npm || needs.resolve-benchmark.outputs.publish_npm }}
|
|
publish_daytona_snapshot: ${{ needs.resolve-production.outputs.publish_daytona_snapshot || needs.resolve-benchmark.outputs.publish_daytona_snapshot }}
|
|
benchmark_only: ${{ needs.resolve-production.outputs.benchmark_only || needs.resolve-benchmark.outputs.benchmark_only }}
|
|
prerelease: ${{ needs.resolve-production.outputs.prerelease || needs.resolve-benchmark.outputs.prerelease }}
|
|
steps:
|
|
- run: true
|
|
|
|
verify-release:
|
|
name: Verify Release Versions
|
|
needs: resolve-release
|
|
# Benchmark mode intentionally skips resolve-production. Override the
|
|
# implicit success() check so that skipped sibling does not propagate
|
|
# through the selected release context and suppress verification.
|
|
if: always() && needs.resolve-release.result == 'success'
|
|
runs-on: blacksmith-4vcpu-ubuntu-2404
|
|
env:
|
|
RELEASE_TAG: ${{ needs.resolve-release.outputs.release_tag }}
|
|
RELEASE_MODE: ${{ needs.resolve-release.outputs.release_mode }}
|
|
steps:
|
|
- name: Checkout
|
|
uses: actions/checkout@v6
|
|
with:
|
|
ref: ${{ env.RELEASE_TAG }}
|
|
fetch-depth: 0
|
|
|
|
- name: Setup Node
|
|
uses: actions/setup-node@v6
|
|
with:
|
|
node-version: 24
|
|
|
|
- name: Verify release tag
|
|
if: needs.resolve-release.outputs.benchmark_only != 'true'
|
|
run: node scripts/release/verify-tag.mjs --tag "$RELEASE_TAG" --mode "$RELEASE_MODE"
|
|
|
|
- name: Release review (strict)
|
|
run: node scripts/release/review.mjs --strict
|
|
|
|
publish-electron-unix:
|
|
name: Build + publish Electron (${{ matrix.artifact }})
|
|
needs: [resolve-release, verify-release]
|
|
if: |
|
|
always() &&
|
|
needs.resolve-release.result == 'success' &&
|
|
needs.verify-release.result == 'success' &&
|
|
needs.resolve-release.outputs.build_electron == 'true'
|
|
runs-on: ${{ matrix.platform }}
|
|
timeout-minutes: 120
|
|
env:
|
|
RELEASE_TAG: ${{ needs.resolve-release.outputs.release_tag }}
|
|
RELEASE_VERSION: ${{ needs.resolve-release.outputs.release_version }}
|
|
MACOS_NOTARIZE: ${{ needs.resolve-release.outputs.notarize }}
|
|
SIGN_WINDOWS: ${{ needs.resolve-release.outputs.sign_windows }}
|
|
BENCHMARK_ONLY: ${{ needs.resolve-release.outputs.benchmark_only }}
|
|
# A filtered frozen install already materializes the complete desktop
|
|
# dependency closure. Without this, pnpm may "repair" the intentionally
|
|
# partial workspace with a second all-workspace install before scripts.
|
|
PNPM_CONFIG_VERIFY_DEPS_BEFORE_RUN: "false"
|
|
OPENWORK_DESKTOP_SENTRY_DSN: ${{ secrets.OPENWORK_DESKTOP_SENTRY_DSN || vars.OPENWORK_DESKTOP_SENTRY_DSN }}
|
|
OPENWORK_DESKTOP_SENTRY_TRACES_SAMPLE_RATE: ${{ vars.OPENWORK_DESKTOP_SENTRY_TRACES_SAMPLE_RATE || '0.01' }}
|
|
|
|
strategy:
|
|
fail-fast: false
|
|
matrix:
|
|
include:
|
|
- platform: blacksmith-12vcpu-macos-15
|
|
os_type: macos
|
|
artifact: electron-macos-arm64
|
|
builder_config: electron-builder.yml
|
|
asset_prefix: openwork
|
|
manifest_pattern: latest*.yml
|
|
electron_args: "--mac --arm64"
|
|
target_triple: aarch64-apple-darwin
|
|
- platform: blacksmith-12vcpu-macos-15
|
|
os_type: macos
|
|
artifact: electron-macos-x64
|
|
builder_config: electron-builder.yml
|
|
asset_prefix: openwork
|
|
manifest_pattern: latest*.yml
|
|
electron_args: "--mac --x64"
|
|
target_triple: x86_64-apple-darwin
|
|
- platform: blacksmith-16vcpu-ubuntu-2204
|
|
os_type: linux
|
|
artifact: electron-linux-x64
|
|
builder_config: electron-builder.yml
|
|
asset_prefix: openwork
|
|
manifest_pattern: latest*.yml
|
|
electron_args: "--linux --x64"
|
|
target_triple: x86_64-unknown-linux-gnu
|
|
# electron-builder can package ARM64 without executing the target
|
|
# binaries. Cross-package on the faster x64 compression runner; the
|
|
# workspace installs both CPU variants of optional native packages.
|
|
- platform: blacksmith-16vcpu-ubuntu-2204
|
|
os_type: linux
|
|
artifact: electron-linux-arm64
|
|
builder_config: electron-builder.yml
|
|
asset_prefix: openwork
|
|
manifest_pattern: latest*.yml
|
|
electron_args: "--linux --arm64"
|
|
target_triple: aarch64-unknown-linux-gnu
|
|
- platform: blacksmith-12vcpu-macos-15
|
|
os_type: macos
|
|
artifact: electron-cloud-macos-arm64
|
|
builder_config: electron-builder.cloud.yml
|
|
asset_prefix: openwork-cloud
|
|
manifest_pattern: cloud*.yml
|
|
electron_args: "--mac --arm64"
|
|
target_triple: aarch64-apple-darwin
|
|
- platform: blacksmith-12vcpu-macos-15
|
|
os_type: macos
|
|
artifact: electron-cloud-macos-x64
|
|
builder_config: electron-builder.cloud.yml
|
|
asset_prefix: openwork-cloud
|
|
manifest_pattern: cloud*.yml
|
|
electron_args: "--mac --x64"
|
|
target_triple: x86_64-apple-darwin
|
|
- platform: blacksmith-16vcpu-ubuntu-2204
|
|
os_type: linux
|
|
artifact: electron-cloud-linux-x64
|
|
builder_config: electron-builder.cloud.yml
|
|
asset_prefix: openwork-cloud
|
|
manifest_pattern: cloud*.yml
|
|
electron_args: "--linux --x64"
|
|
target_triple: x86_64-unknown-linux-gnu
|
|
- platform: blacksmith-16vcpu-ubuntu-2204
|
|
os_type: linux
|
|
artifact: electron-cloud-linux-arm64
|
|
builder_config: electron-builder.cloud.yml
|
|
asset_prefix: openwork-cloud
|
|
manifest_pattern: cloud*.yml
|
|
electron_args: "--linux --arm64"
|
|
target_triple: aarch64-unknown-linux-gnu
|
|
- platform: blacksmith-12vcpu-macos-15
|
|
os_type: macos
|
|
artifact: electron-enterprise-macos-arm64
|
|
builder_config: electron-builder.enterprise.yml
|
|
asset_prefix: openwork-enterprise
|
|
manifest_pattern: enterprise*.yml
|
|
electron_args: "--mac --arm64"
|
|
target_triple: aarch64-apple-darwin
|
|
- platform: blacksmith-12vcpu-macos-15
|
|
os_type: macos
|
|
artifact: electron-enterprise-macos-x64
|
|
builder_config: electron-builder.enterprise.yml
|
|
asset_prefix: openwork-enterprise
|
|
manifest_pattern: enterprise*.yml
|
|
electron_args: "--mac --x64"
|
|
target_triple: x86_64-apple-darwin
|
|
- platform: blacksmith-16vcpu-ubuntu-2204
|
|
os_type: linux
|
|
artifact: electron-enterprise-linux-x64
|
|
builder_config: electron-builder.enterprise.yml
|
|
asset_prefix: openwork-enterprise
|
|
manifest_pattern: enterprise*.yml
|
|
electron_args: "--linux --x64"
|
|
target_triple: x86_64-unknown-linux-gnu
|
|
- platform: blacksmith-16vcpu-ubuntu-2204
|
|
os_type: linux
|
|
artifact: electron-enterprise-linux-arm64
|
|
builder_config: electron-builder.enterprise.yml
|
|
asset_prefix: openwork-enterprise
|
|
manifest_pattern: enterprise*.yml
|
|
electron_args: "--linux --arm64"
|
|
target_triple: aarch64-unknown-linux-gnu
|
|
|
|
steps: &electron-build-steps
|
|
- name: Checkout
|
|
uses: actions/checkout@v6
|
|
with:
|
|
# Source is always pinned to the tag. Recovery reruns pick up
|
|
# workflow-file fixes from the dispatched ref automatically (the
|
|
# workflow definition runs from github.ref), while the shipped code
|
|
# stays exactly what the tag names.
|
|
ref: ${{ env.RELEASE_TAG }}
|
|
fetch-depth: 1
|
|
|
|
- name: Enable git long paths (Windows)
|
|
if: matrix.os_type == 'windows'
|
|
shell: pwsh
|
|
run: git config --global core.longpaths true
|
|
|
|
- name: Setup pnpm
|
|
uses: pnpm/action-setup@v4
|
|
with:
|
|
version: 11.4.0
|
|
|
|
- name: Setup Node
|
|
uses: actions/setup-node@v6
|
|
with:
|
|
node-version: 24
|
|
|
|
- name: Setup Bun
|
|
uses: oven-sh/setup-bun@v2
|
|
with:
|
|
# Bun >= 1.3.10 is required by the server build and package-time helpers.
|
|
bun-version: 1.3.10
|
|
|
|
- name: Get pnpm store path
|
|
id: pnpm-store
|
|
shell: bash
|
|
run: echo "path=$(pnpm store path --silent)" >> "$GITHUB_OUTPUT"
|
|
|
|
- name: Cache pnpm store
|
|
uses: actions/cache@v5
|
|
continue-on-error: true
|
|
with:
|
|
path: ${{ steps.pnpm-store.outputs.path }}
|
|
key: ${{ runner.os }}-electron-pnpm-${{ hashFiles('pnpm-lock.yaml') }}
|
|
restore-keys: |
|
|
${{ runner.os }}-electron-pnpm-
|
|
|
|
- name: Install dependencies
|
|
run: pnpm --filter @openwork/desktop... --filter @openwork/app... --filter openwork-server... install --frozen-lockfile --prefer-offline
|
|
|
|
# The repo commits a permanent 0.0.0-dev placeholder; the release
|
|
# version exists only as the git tag. Stamp it into the workspace so
|
|
# electron-builder, app.getVersion(), and the Vite bundle carry it.
|
|
# Pre-redesign tags carry real committed versions and no stamp script —
|
|
# skipping is exactly the old behavior for them.
|
|
- name: Stamp release version
|
|
shell: bash
|
|
run: |
|
|
set -euo pipefail
|
|
if [ -f scripts/release/stamp-version.mjs ]; then
|
|
node scripts/release/stamp-version.mjs --version "$RELEASE_VERSION"
|
|
else
|
|
echo "Pre-redesign tag: committed package.json versions are in effect."
|
|
fi
|
|
|
|
- name: Write Electron notary API key (macOS)
|
|
if: matrix.os_type == 'macos' && env.MACOS_NOTARIZE == 'true'
|
|
env:
|
|
APPLE_NOTARY_API_KEY_P8_BASE64: ${{ secrets.APPLE_NOTARY_API_KEY_P8_BASE64 }}
|
|
APPLE_NOTARY_API_KEY_ID: ${{ secrets.APPLE_NOTARY_API_KEY_ID }}
|
|
APPLE_NOTARY_API_ISSUER_ID: ${{ secrets.APPLE_NOTARY_API_ISSUER_ID }}
|
|
APPLE_CODESIGN_CERT_P12_BASE64: ${{ secrets.APPLE_CODESIGN_CERT_P12_BASE64 }}
|
|
APPLE_CODESIGN_CERT_PASSWORD: ${{ secrets.APPLE_CODESIGN_CERT_PASSWORD }}
|
|
run: |
|
|
set -euo pipefail
|
|
|
|
missing=()
|
|
for name in \
|
|
APPLE_NOTARY_API_KEY_P8_BASE64 \
|
|
APPLE_NOTARY_API_KEY_ID \
|
|
APPLE_NOTARY_API_ISSUER_ID \
|
|
APPLE_CODESIGN_CERT_P12_BASE64 \
|
|
APPLE_CODESIGN_CERT_PASSWORD; do
|
|
if [ -z "${!name:-}" ]; then
|
|
missing+=("$name")
|
|
fi
|
|
done
|
|
if [ "${#missing[@]}" -gt 0 ]; then
|
|
printf 'Missing Electron macOS notarization/signing secrets: %s\n' "${missing[*]}" >&2
|
|
exit 1
|
|
fi
|
|
|
|
NOTARY_KEY_PATH="$RUNNER_TEMP/AuthKey.p8"
|
|
printf '%s' "$APPLE_NOTARY_API_KEY_P8_BASE64" | base64 --decode > "$NOTARY_KEY_PATH"
|
|
chmod 600 "$NOTARY_KEY_PATH"
|
|
|
|
echo "NOTARY_KEY_PATH=$NOTARY_KEY_PATH" >> "$GITHUB_ENV"
|
|
|
|
- name: Reject unnotarized macOS Electron release
|
|
if: matrix.os_type == 'macos' && env.MACOS_NOTARIZE != 'true'
|
|
shell: bash
|
|
run: |
|
|
echo "macOS Electron release assets must be notarized. Re-run with notarize=true or set MACOS_NOTARIZE=true." >&2
|
|
exit 1
|
|
|
|
- name: Build Electron app
|
|
shell: bash
|
|
env:
|
|
# TARGET tells prepare-sidecar.mjs which architecture's opencode binary
|
|
# to download (critical for cross-arch builds like x64 on arm64 mac).
|
|
TARGET: ${{ matrix.target_triple }}
|
|
run: pnpm --filter @openwork/desktop build:electron
|
|
|
|
- name: Package Electron (macOS, signed + notarized)
|
|
if: matrix.os_type == 'macos' && env.MACOS_NOTARIZE == 'true'
|
|
env:
|
|
CSC_LINK: ${{ secrets.APPLE_CODESIGN_CERT_P12_BASE64 }}
|
|
CSC_KEY_PASSWORD: ${{ secrets.APPLE_CODESIGN_CERT_PASSWORD }}
|
|
APPLE_API_KEY: ${{ secrets.APPLE_NOTARY_API_KEY_ID }}
|
|
APPLE_API_ISSUER: ${{ secrets.APPLE_NOTARY_API_ISSUER_ID }}
|
|
APPLE_API_KEY_PATH: ${{ env.NOTARY_KEY_PATH }}
|
|
run: |
|
|
set -euo pipefail
|
|
pnpm --dir apps/desktop exec electron-builder \
|
|
--config ${{ matrix.builder_config }} \
|
|
${{ matrix.electron_args }} \
|
|
--publish never
|
|
|
|
- name: Package Electron (Linux)
|
|
if: matrix.os_type == 'linux'
|
|
run: |
|
|
set -euo pipefail
|
|
pnpm --dir apps/desktop exec electron-builder \
|
|
--config ${{ matrix.builder_config }} \
|
|
${{ matrix.electron_args }} \
|
|
--publish never
|
|
|
|
- name: Package Electron (Windows)
|
|
if: matrix.os_type == 'windows'
|
|
shell: bash
|
|
run: |
|
|
set -euo pipefail
|
|
pnpm --dir apps/desktop exec electron-builder \
|
|
--config ${{ matrix.builder_config }} \
|
|
${{ matrix.electron_args }} \
|
|
--publish never
|
|
|
|
# Belt-and-braces: a broken or skipped stamp would otherwise ship
|
|
# 0.0.0-dev artifacts silently. Every packaged build must produce at
|
|
# least one artifact carrying the release version (also holds for
|
|
# pre-redesign tags — their committed version equals the tag).
|
|
- name: Assert artifact versions match the tag
|
|
shell: bash
|
|
run: |
|
|
set -euo pipefail
|
|
shopt -s nullglob
|
|
matches=(apps/desktop/dist-electron/*"$RELEASE_VERSION"*)
|
|
if [ ${#matches[@]} -eq 0 ]; then
|
|
echo "No packaged artifact carries version $RELEASE_VERSION — version stamping failed?" >&2
|
|
ls apps/desktop/dist-electron >&2 || true
|
|
exit 1
|
|
fi
|
|
printf 'Version-checked artifact: %s\n' "${matches[@]}"
|
|
|
|
- name: Use unsigned Windows installer fallback
|
|
if: matrix.os_type == 'windows' && env.SIGN_WINDOWS != 'true'
|
|
shell: bash
|
|
run: |
|
|
set -euo pipefail
|
|
echo "::warning::Publishing unsigned Windows installer because SIGN_WINDOWS is not enabled."
|
|
|
|
# electron-builder may still emit latest*.yml for a custom distribution.
|
|
# Normalize each non-public feed so it cannot update into another flavor.
|
|
- name: Normalize custom distribution updater manifests
|
|
if: matrix.asset_prefix != 'openwork'
|
|
shell: bash
|
|
run: |
|
|
set -euo pipefail
|
|
cd apps/desktop/dist-electron
|
|
shopt -s nullglob
|
|
channel="${{ matrix.asset_prefix }}"
|
|
channel="${channel#openwork-}"
|
|
for manifest in latest*.yml; do
|
|
target="${channel}${manifest#latest}"
|
|
if [ -e "$target" ]; then
|
|
rm -f "$manifest"
|
|
else
|
|
mv "$manifest" "$target"
|
|
fi
|
|
done
|
|
manifests=("${channel}"*.yml)
|
|
if [ ${#manifests[@]} -eq 0 ]; then
|
|
echo "${channel} build produced no updater manifest." >&2
|
|
exit 1
|
|
fi
|
|
printf '%s\n' "${manifests[@]}"
|
|
|
|
- name: Validate Azure Artifact Signing configuration
|
|
if: matrix.os_type == 'windows' && env.SIGN_WINDOWS == 'true'
|
|
shell: pwsh
|
|
env:
|
|
AZURE_CLIENT_ID: ${{ vars.AZURE_CLIENT_ID || secrets.AZURE_CLIENT_ID }}
|
|
AZURE_TENANT_ID: ${{ vars.AZURE_TENANT_ID || secrets.AZURE_TENANT_ID }}
|
|
AZURE_SUBSCRIPTION_ID: ${{ vars.AZURE_SUBSCRIPTION_ID || secrets.AZURE_SUBSCRIPTION_ID }}
|
|
AZURE_ARTIFACT_SIGNING_ENDPOINT: ${{ vars.AZURE_ARTIFACT_SIGNING_ENDPOINT || secrets.AZURE_ARTIFACT_SIGNING_ENDPOINT }}
|
|
AZURE_ARTIFACT_SIGNING_ACCOUNT: ${{ vars.AZURE_ARTIFACT_SIGNING_ACCOUNT || secrets.AZURE_ARTIFACT_SIGNING_ACCOUNT }}
|
|
AZURE_ARTIFACT_SIGNING_CERTIFICATE_PROFILE: ${{ vars.AZURE_ARTIFACT_SIGNING_CERTIFICATE_PROFILE || secrets.AZURE_ARTIFACT_SIGNING_CERTIFICATE_PROFILE }}
|
|
run: |
|
|
$required = @(
|
|
"AZURE_CLIENT_ID",
|
|
"AZURE_TENANT_ID",
|
|
"AZURE_SUBSCRIPTION_ID",
|
|
"AZURE_ARTIFACT_SIGNING_ENDPOINT",
|
|
"AZURE_ARTIFACT_SIGNING_ACCOUNT",
|
|
"AZURE_ARTIFACT_SIGNING_CERTIFICATE_PROFILE"
|
|
)
|
|
$missing = $required | Where-Object { -not [Environment]::GetEnvironmentVariable($_) }
|
|
if ($missing.Count -gt 0) {
|
|
throw "Missing windows-signing environment variables: $($missing -join ', ')"
|
|
}
|
|
|
|
$dist = Join-Path $env:GITHUB_WORKSPACE "apps\desktop\dist-electron"
|
|
$installers = Get-ChildItem $dist -Filter "*.exe"
|
|
if ($installers.Count -ne 1) {
|
|
throw "Expected one unsigned Windows installer, found $($installers.Count)"
|
|
}
|
|
|
|
- name: Azure login with GitHub OIDC
|
|
if: matrix.os_type == 'windows' && env.SIGN_WINDOWS == 'true'
|
|
uses: azure/login@f5d393ae46f8fde4be8b75f32e3fc50e654ad0ca # v3.0.1
|
|
with:
|
|
client-id: ${{ vars.AZURE_CLIENT_ID || secrets.AZURE_CLIENT_ID }}
|
|
tenant-id: ${{ vars.AZURE_TENANT_ID || secrets.AZURE_TENANT_ID }}
|
|
subscription-id: ${{ vars.AZURE_SUBSCRIPTION_ID || secrets.AZURE_SUBSCRIPTION_ID }}
|
|
|
|
- name: Sign Windows installer
|
|
if: matrix.os_type == 'windows' && env.SIGN_WINDOWS == 'true'
|
|
uses: azure/artifact-signing-action@c7ab2a863ab5f9a846ddb8265964877ef296ee82 # v2
|
|
with:
|
|
endpoint: ${{ vars.AZURE_ARTIFACT_SIGNING_ENDPOINT || secrets.AZURE_ARTIFACT_SIGNING_ENDPOINT }}
|
|
signing-account-name: ${{ vars.AZURE_ARTIFACT_SIGNING_ACCOUNT || secrets.AZURE_ARTIFACT_SIGNING_ACCOUNT }}
|
|
certificate-profile-name: ${{ vars.AZURE_ARTIFACT_SIGNING_CERTIFICATE_PROFILE || secrets.AZURE_ARTIFACT_SIGNING_CERTIFICATE_PROFILE }}
|
|
files-folder: ${{ github.workspace }}\apps\desktop\dist-electron
|
|
files-folder-filter: exe
|
|
files-folder-recurse: false
|
|
file-digest: SHA256
|
|
timestamp-rfc3161: http://timestamp.acs.microsoft.com
|
|
timestamp-digest: SHA256
|
|
|
|
- name: Verify Windows signature
|
|
if: matrix.os_type == 'windows' && env.SIGN_WINDOWS == 'true'
|
|
shell: pwsh
|
|
run: |
|
|
$dist = Join-Path $env:GITHUB_WORKSPACE "apps\desktop\dist-electron"
|
|
$installers = Get-ChildItem $dist -Filter "*.exe"
|
|
if ($installers.Count -ne 1) {
|
|
throw "Expected one signed Windows installer, found $($installers.Count)"
|
|
}
|
|
|
|
$signature = Get-AuthenticodeSignature $installers[0].FullName
|
|
Write-Host "$($installers[0].Name): $($signature.Status) - $($signature.SignerCertificate.Subject)"
|
|
if ($signature.Status -ne "Valid") {
|
|
throw "$($installers[0].Name) signature is $($signature.Status)"
|
|
}
|
|
|
|
- name: Refresh signed installer blockmap and manifest
|
|
if: matrix.os_type == 'windows' && env.SIGN_WINDOWS == 'true'
|
|
shell: bash
|
|
run: node scripts/release/refresh-signed-windows-artifacts.mjs apps/desktop/dist-electron 1
|
|
|
|
- name: Upload Electron release assets
|
|
if: env.BENCHMARK_ONLY != 'true'
|
|
env:
|
|
GH_TOKEN: ${{ github.token }}
|
|
shell: bash
|
|
run: |
|
|
set -euo pipefail
|
|
shopt -s nullglob
|
|
assets=(apps/desktop/dist-electron/${{ matrix.asset_prefix }}-*)
|
|
if [ ${#assets[@]} -eq 0 ]; then
|
|
echo "No Electron release assets found in apps/desktop/dist-electron" >&2
|
|
exit 1
|
|
fi
|
|
gh release upload "$RELEASE_TAG" "${assets[@]}" --repo "$GITHUB_REPOSITORY" --clobber
|
|
|
|
- name: Retain benchmark Electron assets
|
|
if: env.BENCHMARK_ONLY == 'true'
|
|
uses: actions/upload-artifact@v4
|
|
with:
|
|
name: benchmark-${{ matrix.artifact }}
|
|
path: |
|
|
apps/desktop/dist-electron/${{ matrix.asset_prefix }}-*
|
|
apps/desktop/dist-electron/${{ matrix.manifest_pattern }}
|
|
if-no-files-found: error
|
|
retention-days: 1
|
|
|
|
- name: Upload Electron updater manifest artifact
|
|
uses: actions/upload-artifact@v4
|
|
with:
|
|
name: ${{ matrix.artifact }}
|
|
path: apps/desktop/dist-electron/${{ matrix.manifest_pattern }}
|
|
if-no-files-found: error
|
|
|
|
publish-electron-windows:
|
|
name: Build + publish Electron (${{ matrix.artifact }})
|
|
needs: [resolve-release, verify-release]
|
|
if: |
|
|
always() &&
|
|
needs.resolve-release.result == 'success' &&
|
|
needs.verify-release.result == 'success' &&
|
|
needs.resolve-release.outputs.build_electron == 'true'
|
|
runs-on: ${{ matrix.platform }}
|
|
environment: windows-signing
|
|
timeout-minutes: 120
|
|
permissions:
|
|
actions: read
|
|
contents: write
|
|
id-token: write
|
|
env:
|
|
RELEASE_TAG: ${{ needs.resolve-release.outputs.release_tag }}
|
|
RELEASE_VERSION: ${{ needs.resolve-release.outputs.release_version }}
|
|
MACOS_NOTARIZE: ${{ needs.resolve-release.outputs.notarize }}
|
|
SIGN_WINDOWS: ${{ needs.resolve-release.outputs.sign_windows }}
|
|
BENCHMARK_ONLY: ${{ needs.resolve-release.outputs.benchmark_only }}
|
|
PNPM_CONFIG_VERIFY_DEPS_BEFORE_RUN: "false"
|
|
OPENWORK_DESKTOP_SENTRY_DSN: ${{ secrets.OPENWORK_DESKTOP_SENTRY_DSN || vars.OPENWORK_DESKTOP_SENTRY_DSN }}
|
|
OPENWORK_DESKTOP_SENTRY_TRACES_SAMPLE_RATE: ${{ vars.OPENWORK_DESKTOP_SENTRY_TRACES_SAMPLE_RATE || '0.01' }}
|
|
|
|
strategy:
|
|
fail-fast: false
|
|
matrix:
|
|
include:
|
|
- platform: blacksmith-8vcpu-windows-2025
|
|
os_type: windows
|
|
artifact: electron-windows-x64
|
|
builder_config: electron-builder.yml
|
|
asset_prefix: openwork
|
|
manifest_pattern: latest*.yml
|
|
electron_args: "--win --x64"
|
|
target_triple: x86_64-pc-windows-msvc
|
|
# electron-builder cross-packages ARM64 on the faster x64 runner.
|
|
# pnpm-workspace.yaml installs both Windows CPU variants so native
|
|
# optional dependencies remain correct in the ARM64 artifact.
|
|
- platform: blacksmith-8vcpu-windows-2025
|
|
os_type: windows
|
|
artifact: electron-windows-arm64
|
|
builder_config: electron-builder.yml
|
|
asset_prefix: openwork
|
|
manifest_pattern: latest*.yml
|
|
electron_args: "--win --arm64"
|
|
target_triple: aarch64-pc-windows-msvc
|
|
- platform: blacksmith-8vcpu-windows-2025
|
|
os_type: windows
|
|
artifact: electron-cloud-windows-x64
|
|
builder_config: electron-builder.cloud.yml
|
|
asset_prefix: openwork-cloud
|
|
manifest_pattern: cloud*.yml
|
|
electron_args: "--win --x64"
|
|
target_triple: x86_64-pc-windows-msvc
|
|
- platform: blacksmith-8vcpu-windows-2025
|
|
os_type: windows
|
|
artifact: electron-cloud-windows-arm64
|
|
builder_config: electron-builder.cloud.yml
|
|
asset_prefix: openwork-cloud
|
|
manifest_pattern: cloud*.yml
|
|
electron_args: "--win --arm64"
|
|
target_triple: aarch64-pc-windows-msvc
|
|
- platform: blacksmith-8vcpu-windows-2025
|
|
os_type: windows
|
|
artifact: electron-enterprise-windows-x64
|
|
builder_config: electron-builder.enterprise.yml
|
|
asset_prefix: openwork-enterprise
|
|
manifest_pattern: enterprise*.yml
|
|
electron_args: "--win --x64"
|
|
target_triple: x86_64-pc-windows-msvc
|
|
- platform: blacksmith-8vcpu-windows-2025
|
|
os_type: windows
|
|
artifact: electron-enterprise-windows-arm64
|
|
builder_config: electron-builder.enterprise.yml
|
|
asset_prefix: openwork-enterprise
|
|
manifest_pattern: enterprise*.yml
|
|
electron_args: "--win --arm64"
|
|
target_triple: aarch64-pc-windows-msvc
|
|
|
|
steps: *electron-build-steps
|
|
|
|
publish-release:
|
|
name: Publish Electron Assets + GitHub Release
|
|
needs:
|
|
- resolve-release
|
|
- verify-release
|
|
- publish-electron-unix
|
|
- publish-electron-windows
|
|
- publish-npm
|
|
- publish-daytona-snapshot
|
|
# publish-daytona-snapshot is allowed to fail without blocking the release:
|
|
# the sandbox snapshot is rebuildable after the fact, while installable
|
|
# desktop artifacts should never be held back by it.
|
|
if: |
|
|
always() &&
|
|
needs.resolve-release.result == 'success' &&
|
|
needs.verify-release.result == 'success' &&
|
|
needs.resolve-release.outputs.benchmark_only != 'true' &&
|
|
(needs.resolve-release.outputs.build_electron != 'true' || needs.publish-electron-unix.result == 'success') &&
|
|
(needs.resolve-release.outputs.build_electron != 'true' || needs.publish-electron-windows.result == 'success') &&
|
|
(needs.publish-npm.result == 'success' || needs.publish-npm.result == 'skipped') &&
|
|
needs.publish-daytona-snapshot.result != 'cancelled'
|
|
runs-on: blacksmith-4vcpu-ubuntu-2404
|
|
env:
|
|
RELEASE_TAG: ${{ needs.resolve-release.outputs.release_tag }}
|
|
RELEASE_PRERELEASE: ${{ needs.resolve-release.outputs.prerelease }}
|
|
steps:
|
|
- name: Checkout
|
|
if: needs.resolve-release.outputs.build_electron == 'true'
|
|
uses: actions/checkout@v6
|
|
with:
|
|
ref: ${{ env.RELEASE_TAG }}
|
|
fetch-depth: 1
|
|
|
|
- name: Setup Node
|
|
if: needs.resolve-release.outputs.build_electron == 'true'
|
|
uses: actions/setup-node@v6
|
|
with:
|
|
node-version: 24
|
|
|
|
- name: Download Electron updater manifest artifacts
|
|
if: needs.resolve-release.outputs.build_electron == 'true'
|
|
uses: actions/download-artifact@v4
|
|
with:
|
|
pattern: electron-*
|
|
path: ${{ runner.temp }}/electron-dist
|
|
|
|
- name: Upload merged updater manifests
|
|
if: needs.resolve-release.outputs.build_electron == 'true'
|
|
env:
|
|
GH_TOKEN: ${{ github.token }}
|
|
run: node scripts/release/publish-electron-assets.mjs --manifests-only "${{ runner.temp }}/electron-dist" "$RELEASE_TAG"
|
|
|
|
- name: Publish release after assets are ready
|
|
env:
|
|
GH_TOKEN: ${{ github.token }}
|
|
run: |
|
|
set -euo pipefail
|
|
|
|
# Dispatch-based recovery runs default draft=false but may target a
|
|
# draft release created by an earlier tag push; always flip drafts
|
|
# here so AUR and updater feeds see public download URLs.
|
|
is_draft="$(gh release view "$RELEASE_TAG" --repo "$GITHUB_REPOSITORY" --json isDraft -q .isDraft)"
|
|
if [ "$is_draft" != "true" ]; then
|
|
echo "Release $RELEASE_TAG is already published; nothing to do."
|
|
exit 0
|
|
fi
|
|
|
|
if [ "${RELEASE_PRERELEASE}" = "true" ]; then
|
|
gh release edit "$RELEASE_TAG" --repo "$GITHUB_REPOSITORY" --draft=false --prerelease
|
|
else
|
|
gh release edit "$RELEASE_TAG" --repo "$GITHUB_REPOSITORY" --draft=false --latest
|
|
fi
|
|
|
|
prepare-npm:
|
|
name: Prepare openwork-server npm package
|
|
needs: [resolve-release, verify-release]
|
|
if: |
|
|
always() &&
|
|
needs.resolve-release.result == 'success' &&
|
|
needs.verify-release.result == 'success' &&
|
|
needs.resolve-release.outputs.benchmark_only != 'true' &&
|
|
needs.resolve-release.outputs.publish_npm == 'true'
|
|
runs-on: blacksmith-4vcpu-ubuntu-2404
|
|
outputs:
|
|
publish_server: ${{ steps.npm-versions.outputs.publish_server }}
|
|
env:
|
|
RELEASE_TAG: ${{ needs.resolve-release.outputs.release_tag }}
|
|
RELEASE_VERSION: ${{ needs.resolve-release.outputs.release_version }}
|
|
steps:
|
|
- name: Checkout
|
|
uses: actions/checkout@v6
|
|
with:
|
|
ref: ${{ env.RELEASE_TAG }}
|
|
fetch-depth: 1
|
|
|
|
- name: Setup Node
|
|
uses: actions/setup-node@v6
|
|
with:
|
|
node-version: 24
|
|
|
|
- name: Setup pnpm
|
|
uses: pnpm/action-setup@v4
|
|
with:
|
|
version: 11.4.0
|
|
|
|
- name: Setup Bun
|
|
uses: oven-sh/setup-bun@v1
|
|
with:
|
|
# Bun builds the openwork-server executable before npm publish.
|
|
bun-version: "1.3.10"
|
|
|
|
- name: Get pnpm store path
|
|
id: pnpm-store
|
|
shell: bash
|
|
run: echo "path=$(pnpm store path --silent)" >> "$GITHUB_OUTPUT"
|
|
|
|
- name: Cache pnpm store
|
|
uses: actions/cache@v5
|
|
continue-on-error: true
|
|
with:
|
|
path: ${{ steps.pnpm-store.outputs.path }}
|
|
key: ubuntu-pnpm-${{ hashFiles('pnpm-lock.yaml') }}
|
|
restore-keys: |
|
|
ubuntu-pnpm-
|
|
|
|
- name: Install dependencies
|
|
run: pnpm install --frozen-lockfile --prefer-offline
|
|
|
|
# openwork-server publishes with the tag-derived version; the committed
|
|
# package.json holds the permanent 0.0.0-dev placeholder. Pre-redesign
|
|
# tags carry real committed versions and no stamp script.
|
|
- name: Stamp release version
|
|
shell: bash
|
|
run: |
|
|
set -euo pipefail
|
|
if [ -f scripts/release/stamp-version.mjs ]; then
|
|
node scripts/release/stamp-version.mjs --version "$RELEASE_VERSION"
|
|
else
|
|
echo "Pre-redesign tag: committed package.json versions are in effect."
|
|
fi
|
|
|
|
- name: Check npm versions
|
|
id: npm-versions
|
|
shell: bash
|
|
env:
|
|
SERVER_VERSION: ${{ env.RELEASE_VERSION }}
|
|
run: |
|
|
set -euo pipefail
|
|
# npm view exits non-zero for packages that don't exist yet (404).
|
|
# Treat missing packages as "not published" so release can publish them.
|
|
server_current="$(npm view openwork-server version 2>/dev/null || true)"
|
|
|
|
if [ "$server_current" = "$SERVER_VERSION" ]; then
|
|
echo "publish_server=false" >> "$GITHUB_OUTPUT"
|
|
echo "publish_any=false" >> "$GITHUB_OUTPUT"
|
|
else
|
|
echo "publish_server=true" >> "$GITHUB_OUTPUT"
|
|
echo "publish_any=true" >> "$GITHUB_OUTPUT"
|
|
fi
|
|
|
|
- name: Prepare openwork-server
|
|
if: steps.npm-versions.outputs.publish_server == 'true'
|
|
run: pnpm --filter openwork-server publish:npm --dry-run
|
|
|
|
- name: Upload npm package
|
|
if: steps.npm-versions.outputs.publish_server == 'true'
|
|
uses: actions/upload-artifact@v4
|
|
with:
|
|
name: openwork-server-npm-${{ env.RELEASE_VERSION }}
|
|
path: apps/server/dist/npm
|
|
if-no-files-found: error
|
|
|
|
publish-npm:
|
|
name: Publish openwork-server
|
|
needs: [resolve-release, verify-release, prepare-npm]
|
|
if: |
|
|
always() &&
|
|
needs.resolve-release.result == 'success' &&
|
|
needs.verify-release.result == 'success' &&
|
|
needs.prepare-npm.result == 'success' &&
|
|
needs.prepare-npm.outputs.publish_server == 'true'
|
|
runs-on: ubuntu-latest
|
|
permissions:
|
|
actions: read
|
|
contents: read
|
|
id-token: write
|
|
env:
|
|
RELEASE_VERSION: ${{ needs.resolve-release.outputs.release_version }}
|
|
steps:
|
|
- name: Setup Node
|
|
uses: actions/setup-node@v6
|
|
with:
|
|
node-version: 24
|
|
|
|
- name: Download npm package
|
|
uses: actions/download-artifact@v4
|
|
with:
|
|
name: openwork-server-npm-${{ env.RELEASE_VERSION }}
|
|
path: ${{ runner.temp }}/openwork-server-npm
|
|
|
|
- name: Publish openwork-server
|
|
run: npm publish "$RUNNER_TEMP/openwork-server-npm" --access public --provenance
|
|
|
|
publish-daytona-snapshot:
|
|
name: Build + Push Daytona Snapshot
|
|
needs: [resolve-release, verify-release, publish-npm]
|
|
if: |
|
|
always() &&
|
|
needs.resolve-release.result == 'success' &&
|
|
needs.verify-release.result == 'success' &&
|
|
needs.resolve-release.outputs.benchmark_only != 'true' &&
|
|
(needs.publish-npm.result == 'success' || needs.publish-npm.result == 'skipped') &&
|
|
needs.resolve-release.outputs.publish_daytona_snapshot == 'true'
|
|
uses: ./.github/workflows/release-daytona-snapshot.yml
|
|
with:
|
|
tag: ${{ needs.resolve-release.outputs.release_tag }}
|
|
secrets: inherit
|
|
|
|
aur-publish:
|
|
name: Publish AUR
|
|
needs: [resolve-release, publish-electron-unix, publish-release]
|
|
if: |
|
|
always() &&
|
|
needs.resolve-release.result == 'success' &&
|
|
needs.resolve-release.outputs.benchmark_only != 'true' &&
|
|
needs.publish-electron-unix.result == 'success' &&
|
|
needs.publish-release.result == 'success'
|
|
# AUR is a community distribution channel hosted on aur.archlinux.org.
|
|
# Outages there (e.g. "The AUR is down due to maintenance", 2026-08-04)
|
|
# must not mark an otherwise-published release run as failed.
|
|
continue-on-error: true
|
|
runs-on: blacksmith-4vcpu-ubuntu-2404
|
|
permissions:
|
|
contents: read
|
|
env:
|
|
RELEASE_TAG: ${{ needs.resolve-release.outputs.release_tag }}
|
|
steps:
|
|
- name: Checkout dev
|
|
uses: actions/checkout@v6
|
|
with:
|
|
ref: dev
|
|
fetch-depth: 0
|
|
|
|
# packaging/aur holds a version-agnostic template (pkgver=0.0.0).
|
|
# publish-aur.sh renders the real version + checksums from the published
|
|
# release assets in the CI workspace and pushes ONLY to the AUR remote —
|
|
# the commit to aur.archlinux.org is AUR's publish protocol, and nothing
|
|
# is ever committed to this repo.
|
|
- name: Publish to AUR
|
|
env:
|
|
AUR_SSH_PRIVATE_KEY: ${{ secrets.AUR_SSH_PRIVATE_KEY }}
|
|
AUR_REPO: ${{ vars.AUR_REPO || 'openwork' }}
|
|
run: |
|
|
set -euo pipefail
|
|
if [ -z "${AUR_SSH_PRIVATE_KEY:-}" ]; then
|
|
echo "AUR_SSH_PRIVATE_KEY not set; skipping publish to AUR."
|
|
exit 0
|
|
fi
|
|
scripts/aur/publish-aur.sh "$RELEASE_TAG"
|