Initial import: open-design source for helix-mind.ai distribution
Some checks failed
ci / Validate workspace (push) Successful in 12m32s
landing-page-ci / Validate landing page (push) Successful in 9m41s
landing-page-deploy / Deploy landing page (push) Failing after 5m23s
github-metrics / Generate repository metrics SVG (push) Failing after 2m6s
refresh-contributors-wall / Refresh contributors wall cache bust (push) Failing after 12s
Some checks failed
ci / Validate workspace (push) Successful in 12m32s
landing-page-ci / Validate landing page (push) Successful in 9m41s
landing-page-deploy / Deploy landing page (push) Failing after 5m23s
github-metrics / Generate repository metrics SVG (push) Failing after 2m6s
refresh-contributors-wall / Refresh contributors wall cache bust (push) Failing after 12s
This repository contains the open-design daemon CLI source code, built and packaged at https://helix-mind.ai/cli/open-design/latest.tgz for use by the HelixMind /design slash command. Licenses: Apache-2.0 (root) + MIT (skills/*)
This commit is contained in:
104
.github/workflows/ci.yml
vendored
Normal file
104
.github/workflows/ci.yml
vendored
Normal file
@@ -0,0 +1,104 @@
|
||||
name: ci
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
# Release validation is owned by the release workflows rather than this CI
|
||||
# workflow: `release-stable` has a verify job before publishing, and
|
||||
# `release-beta` builds from its selected release commit. Keep this trigger
|
||||
# focused on PRs, main, and manual reruns instead of duplicating tag/release
|
||||
# events that would run after those release workflows have already selected
|
||||
# or validated their commit.
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
concurrency:
|
||||
group: ci-${{ github.event.pull_request.number || github.ref }}
|
||||
# Prefer current-head signal over preserving superseded logs: PR authors often
|
||||
# push fixups while this workflow is still running, and stale runs can report
|
||||
# failures for commits reviewers no longer need to evaluate. Release workflows
|
||||
# use cancel-in-progress: false where preserving build evidence matters more.
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
validate:
|
||||
name: Validate workspace
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 30
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v6.0.2
|
||||
|
||||
- name: Setup pnpm
|
||||
uses: pnpm/action-setup@v5
|
||||
with:
|
||||
version: 10.33.2
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v6
|
||||
with:
|
||||
node-version: 24
|
||||
cache: pnpm
|
||||
|
||||
- name: Install dependencies
|
||||
run: pnpm install --frozen-lockfile
|
||||
|
||||
- name: Install Playwright browsers
|
||||
run: pnpm -C e2e exec playwright install --with-deps chromium
|
||||
|
||||
# `scripts/postinstall.mjs` only prebuilds package/tool entrypoints that
|
||||
# are needed immediately after install for linked bins and shared
|
||||
# sidecar/platform imports. It intentionally skips app outputs because
|
||||
# building all apps would make every install run a Next/Electron-adjacent
|
||||
# app build, even when a developer only needs packages/tools.
|
||||
#
|
||||
# Fresh CI typecheck/test still need these specific generated declarations:
|
||||
# - `apps/daemon/dist/*.d.ts` for packaged/runtime consumers of the daemon
|
||||
# package export
|
||||
# - `apps/desktop/dist/main/index.d.ts` for `apps/packaged` imports of
|
||||
# `@open-design/desktop/main`
|
||||
# - `apps/web/dist/sidecar/index.d.ts` for `apps/packaged` imports of
|
||||
# `@open-design/web/sidecar`
|
||||
# If postinstall grows a targeted app type-generation phase covering these
|
||||
# three exports without broad app builds, this CI prebuild can be removed.
|
||||
- name: Prebuild workspace type declarations
|
||||
run: |
|
||||
pnpm --filter @open-design/daemon build
|
||||
pnpm --filter @open-design/desktop build
|
||||
pnpm --filter @open-design/web build:sidecar
|
||||
|
||||
- name: Typecheck workspaces
|
||||
run: pnpm -r --workspace-concurrency=1 --if-present run typecheck
|
||||
|
||||
- name: Check repository layout policies
|
||||
run: pnpm guard
|
||||
|
||||
- name: Check i18n structure
|
||||
run: pnpm i18n:check
|
||||
|
||||
- name: Test
|
||||
run: |
|
||||
pnpm --filter @open-design/e2e test
|
||||
pnpm -C e2e exec tsx scripts/playwright.ts clean
|
||||
pnpm -C e2e exec playwright test -c playwright.config.ts
|
||||
pnpm --filter @open-design/contracts test
|
||||
pnpm --filter @open-design/platform test
|
||||
pnpm --filter @open-design/sidecar test
|
||||
pnpm --filter @open-design/sidecar-proto test
|
||||
pnpm --filter @open-design/daemon test
|
||||
pnpm --filter @open-design/web test
|
||||
pnpm --filter @open-design/tools-dev test
|
||||
pnpm --filter @open-design/tools-pack test
|
||||
|
||||
# Keep workspace builds serialized so generated dist output and local
|
||||
# runtime artifacts are produced in a deterministic order. Parallel
|
||||
# recursive builds would surface late-package failures sooner, but the
|
||||
# current workspace is small enough that safer logs and fewer shared-FS
|
||||
# races outweigh the lost parallelism; revisit if the package count grows.
|
||||
- name: Build workspaces
|
||||
run: pnpm -r --workspace-concurrency=1 --if-present run build
|
||||
241
.github/workflows/discord-resolved.yml
vendored
Normal file
241
.github/workflows/discord-resolved.yml
vendored
Normal file
@@ -0,0 +1,241 @@
|
||||
# Notify Discord #resolved when an issue is closed by a merged PR.
|
||||
#
|
||||
# Trigger logic:
|
||||
# - issues.closed fires whenever an issue is closed (manually, by PR, or as not-planned)
|
||||
# - We require state_reason == "completed" AND that the issue's most recent
|
||||
# `closed` timeline event has a commit_id belonging to a merged PR.
|
||||
# - Then we post a rich Discord embed with: issue title + body excerpt, issue
|
||||
# author, the PR that resolved it, and the merger.
|
||||
#
|
||||
# Why a workflow instead of a raw repo→Discord webhook?
|
||||
# GitHub's webhook can't tell Discord "this issue was closed *by a merged PR*" —
|
||||
# the issues.closed payload doesn't carry that linkage. We have to walk the
|
||||
# timeline ourselves, which a workflow does in <1s.
|
||||
#
|
||||
# Why only the `closed` + `commit_id` path (no cross-referenced fallback)?
|
||||
# `cross-referenced` events fire on plain mentions ("related to #123"),
|
||||
# so trusting them creates false positives — a manually closed issue mentioned
|
||||
# by an unrelated merged PR would post to #resolved. The closed-event linkage
|
||||
# is the only signal GitHub itself uses to display "closed by PR #N", so it's
|
||||
# the source of truth. We accept the rare miss (e.g. a PR that closed an issue
|
||||
# via the web UI rather than via "Fixes" keyword) in exchange for zero false
|
||||
# positives.
|
||||
|
||||
name: Discord · resolved
|
||||
|
||||
on:
|
||||
issues:
|
||||
types: [closed]
|
||||
|
||||
# Read-only. Discord post goes via webhook URL (a secret), not GitHub auth.
|
||||
# - contents:read : required by repos.listPullRequestsAssociatedWithCommit
|
||||
# - issues:read : timeline + issue body
|
||||
# - pull-requests:read : PR metadata (merger, title)
|
||||
permissions:
|
||||
contents: read
|
||||
issues: read
|
||||
pull-requests: read
|
||||
|
||||
jobs:
|
||||
notify:
|
||||
# state_reason "completed" excludes "not planned" closures.
|
||||
# We further require an actual merged-PR linkage in the script below.
|
||||
if: github.event.issue.state_reason == 'completed'
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Find the merged PR that closed this issue
|
||||
id: find-pr
|
||||
uses: actions/github-script@v7
|
||||
with:
|
||||
script: |
|
||||
const issue = context.payload.issue;
|
||||
|
||||
const timeline = await github.paginate(
|
||||
github.rest.issues.listEventsForTimeline,
|
||||
{
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
issue_number: issue.number,
|
||||
per_page: 100,
|
||||
}
|
||||
);
|
||||
|
||||
// Walk events backwards. The most recent `closed` event with a
|
||||
// commit_id whose containing PR is merged is our resolver.
|
||||
// We deliberately ignore `cross-referenced` events: those fire on
|
||||
// plain mentions, not just closing-keyword links, and trusting
|
||||
// them produces false positives. See top-of-file comment.
|
||||
let resolvingPr = null;
|
||||
for (let i = timeline.length - 1; i >= 0; i--) {
|
||||
const ev = timeline[i];
|
||||
if (ev.event !== 'closed' || !ev.commit_id) continue;
|
||||
try {
|
||||
const { data: prs } =
|
||||
await github.rest.repos.listPullRequestsAssociatedWithCommit({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
commit_sha: ev.commit_id,
|
||||
});
|
||||
const merged = prs.find((p) => p.merged_at);
|
||||
if (merged) {
|
||||
resolvingPr = merged;
|
||||
break;
|
||||
}
|
||||
} catch (e) {
|
||||
core.warning(
|
||||
`listPullRequestsAssociatedWithCommit failed for ${ev.commit_id}: ${e.message}`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (!resolvingPr) {
|
||||
core.info(
|
||||
'No merged PR found via closed-event linkage — skipping Discord post. ' +
|
||||
'This is expected for manual closes or web-UI "close with comment" events.'
|
||||
);
|
||||
core.setOutput('skip', 'true');
|
||||
return;
|
||||
}
|
||||
|
||||
// Truncate body for the embed (Discord embed description max ~4096,
|
||||
// but readable cards stay under ~400 chars).
|
||||
const rawBody = (issue.body || '').trim();
|
||||
const bodyExcerpt = rawBody.length > 380
|
||||
? rawBody.slice(0, 380).trim() + '…'
|
||||
: rawBody || '_(no description)_';
|
||||
|
||||
core.setOutput('skip', 'false');
|
||||
core.setOutput('issue_number', String(issue.number));
|
||||
core.setOutput('issue_title', issue.title);
|
||||
core.setOutput('issue_url', issue.html_url);
|
||||
core.setOutput('issue_author', issue.user.login);
|
||||
core.setOutput('issue_author_url', issue.user.html_url);
|
||||
core.setOutput('issue_author_avatar', issue.user.avatar_url);
|
||||
core.setOutput('issue_body', bodyExcerpt);
|
||||
core.setOutput('pr_number', String(resolvingPr.number));
|
||||
core.setOutput('pr_title', resolvingPr.title);
|
||||
core.setOutput('pr_url', resolvingPr.html_url);
|
||||
core.setOutput('pr_merger', resolvingPr.merged_by?.login || resolvingPr.user.login);
|
||||
core.setOutput('pr_merger_url',
|
||||
resolvingPr.merged_by?.html_url || resolvingPr.user.html_url);
|
||||
core.setOutput('repo_full_name', `${context.repo.owner}/${context.repo.repo}`);
|
||||
|
||||
- name: Post embed to Discord
|
||||
if: steps.find-pr.outputs.skip == 'false'
|
||||
env:
|
||||
WEBHOOK: ${{ secrets.DISCORD_RESOLVED_WEBHOOK }}
|
||||
ISSUE_NUMBER: ${{ steps.find-pr.outputs.issue_number }}
|
||||
ISSUE_TITLE: ${{ steps.find-pr.outputs.issue_title }}
|
||||
ISSUE_URL: ${{ steps.find-pr.outputs.issue_url }}
|
||||
ISSUE_AUTHOR: ${{ steps.find-pr.outputs.issue_author }}
|
||||
ISSUE_AUTHOR_URL: ${{ steps.find-pr.outputs.issue_author_url }}
|
||||
ISSUE_AUTHOR_AVATAR: ${{ steps.find-pr.outputs.issue_author_avatar }}
|
||||
ISSUE_BODY: ${{ steps.find-pr.outputs.issue_body }}
|
||||
PR_NUMBER: ${{ steps.find-pr.outputs.pr_number }}
|
||||
PR_TITLE: ${{ steps.find-pr.outputs.pr_title }}
|
||||
PR_URL: ${{ steps.find-pr.outputs.pr_url }}
|
||||
PR_MERGER: ${{ steps.find-pr.outputs.pr_merger }}
|
||||
PR_MERGER_URL: ${{ steps.find-pr.outputs.pr_merger_url }}
|
||||
REPO_FULL_NAME: ${{ steps.find-pr.outputs.repo_full_name }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
|
||||
# ── Webhook URL sanity check ────────────────────────────────────
|
||||
# Refuse to post if the secret is missing or doesn't look like a
|
||||
# Discord webhook URL — guards against accidentally leaking issue
|
||||
# metadata to a misconfigured endpoint.
|
||||
if [ -z "${WEBHOOK:-}" ]; then
|
||||
echo "DISCORD_RESOLVED_WEBHOOK secret not configured — aborting."
|
||||
exit 1
|
||||
fi
|
||||
case "$WEBHOOK" in
|
||||
https://discord.com/api/webhooks/*) ;;
|
||||
https://discordapp.com/api/webhooks/*) ;;
|
||||
*)
|
||||
echo "WEBHOOK does not look like a Discord webhook URL — aborting."
|
||||
echo "(Expected prefix: https://discord.com/api/webhooks/...)"
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
|
||||
# ── Build embed JSON via jq ─────────────────────────────────────
|
||||
# Color 0x2eb67d (3066993) — green for "resolved".
|
||||
# `allowed_mentions: { parse: [] }` disables @everyone/@here/role/user
|
||||
# mentions so a malicious or accidental issue title can't ping the
|
||||
# channel.
|
||||
payload=$(jq -n \
|
||||
--arg title "✅ #${ISSUE_NUMBER}: ${ISSUE_TITLE}" \
|
||||
--arg url "$ISSUE_URL" \
|
||||
--arg desc "$ISSUE_BODY" \
|
||||
--arg author_name "$ISSUE_AUTHOR" \
|
||||
--arg author_url "$ISSUE_AUTHOR_URL" \
|
||||
--arg author_icon "$ISSUE_AUTHOR_AVATAR" \
|
||||
--arg pr_field "[#${PR_NUMBER} ${PR_TITLE}](${PR_URL})" \
|
||||
--arg merger_field "[@${PR_MERGER}](${PR_MERGER_URL})" \
|
||||
--arg footer "$REPO_FULL_NAME" \
|
||||
'{
|
||||
username: "Issue Resolver",
|
||||
avatar_url: "https://github.githubassets.com/images/modules/logos_page/Octocat.png",
|
||||
allowed_mentions: { parse: [] },
|
||||
embeds: [
|
||||
{
|
||||
title: $title,
|
||||
url: $url,
|
||||
description: $desc,
|
||||
color: 3066993,
|
||||
author: {
|
||||
name: ("Reported by @" + $author_name),
|
||||
url: $author_url,
|
||||
icon_url: $author_icon
|
||||
},
|
||||
fields: [
|
||||
{ name: "Resolved by PR", value: $pr_field, inline: false },
|
||||
{ name: "Merged by", value: $merger_field, inline: true }
|
||||
],
|
||||
footer: { text: $footer },
|
||||
timestamp: now | todateiso8601
|
||||
}
|
||||
]
|
||||
}')
|
||||
|
||||
# ── POST with bounded retry on 429 ──────────────────────────────
|
||||
# Discord rate-limits webhooks per-channel and may return 429 with a
|
||||
# `retry-after` header (seconds, integer or float). We retry up to 3
|
||||
# times honouring that header, with a sane default if the header is
|
||||
# missing.
|
||||
attempts=3
|
||||
for attempt in $(seq 1 "$attempts"); do
|
||||
: > /tmp/resp_body
|
||||
: > /tmp/resp_headers
|
||||
status=$(curl -sS -o /tmp/resp_body -D /tmp/resp_headers \
|
||||
-w '%{http_code}' \
|
||||
-H 'Content-Type: application/json' \
|
||||
-X POST "$WEBHOOK" \
|
||||
-d "$payload" || echo '000')
|
||||
|
||||
if [ "$status" = "204" ]; then
|
||||
echo "Discord post OK (attempt $attempt)."
|
||||
exit 0
|
||||
fi
|
||||
|
||||
if [ "$status" = "429" ] && [ "$attempt" -lt "$attempts" ]; then
|
||||
# Header is case-insensitive; tr to lowercase for matching.
|
||||
retry_after=$(tr -d '\r' < /tmp/resp_headers \
|
||||
| awk 'BEGIN{IGNORECASE=1} /^retry-after:/ {print $2; exit}')
|
||||
# Floor to integer; default to 5s if header missing/unparseable.
|
||||
retry_after_int=$(printf '%.0f' "${retry_after:-5}" 2>/dev/null || echo 5)
|
||||
[ "$retry_after_int" -lt 1 ] && retry_after_int=1
|
||||
[ "$retry_after_int" -gt 60 ] && retry_after_int=60
|
||||
echo "Rate limited (HTTP 429), retrying in ${retry_after_int}s (attempt ${attempt}/${attempts})…"
|
||||
sleep "$retry_after_int"
|
||||
continue
|
||||
fi
|
||||
|
||||
echo "Discord webhook returned HTTP $status (attempt ${attempt}/${attempts}):"
|
||||
cat /tmp/resp_body
|
||||
# Non-429 errors are not retryable.
|
||||
exit 1
|
||||
done
|
||||
|
||||
echo "Discord post failed after ${attempts} attempts (last status: ${status})."
|
||||
exit 1
|
||||
93
.github/workflows/landing-page-ci.yml
vendored
Normal file
93
.github/workflows/landing-page-ci.yml
vendored
Normal file
@@ -0,0 +1,93 @@
|
||||
name: landing-page-ci
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
paths:
|
||||
- .github/workflows/landing-page-ci.yml
|
||||
- .github/workflows/landing-page.yml
|
||||
- apps/landing-page/**
|
||||
- package.json
|
||||
- pnpm-lock.yaml
|
||||
- pnpm-workspace.yaml
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
paths:
|
||||
- .github/workflows/landing-page-ci.yml
|
||||
- .github/workflows/landing-page.yml
|
||||
- apps/landing-page/**
|
||||
- package.json
|
||||
- pnpm-lock.yaml
|
||||
- pnpm-workspace.yaml
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
concurrency:
|
||||
group: landing-page-ci-${{ github.event.pull_request.number || github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
validate:
|
||||
name: Validate landing page
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 15
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v6.0.2
|
||||
|
||||
- name: Setup pnpm
|
||||
uses: pnpm/action-setup@v5
|
||||
with:
|
||||
version: 10.33.2
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v6
|
||||
with:
|
||||
node-version: 24
|
||||
cache: pnpm
|
||||
|
||||
- name: Install dependencies
|
||||
run: pnpm install --frozen-lockfile
|
||||
|
||||
- name: Typecheck landing page
|
||||
run: pnpm --filter @open-design/landing-page typecheck
|
||||
|
||||
- name: Build landing page
|
||||
run: pnpm --filter @open-design/landing-page build
|
||||
|
||||
- name: Verify zero external JavaScript
|
||||
run: |
|
||||
node <<'NODE'
|
||||
const { readFileSync } = require('node:fs');
|
||||
const html = readFileSync('apps/landing-page/out/index.html', 'utf8');
|
||||
const forbidden = [
|
||||
/<script\b[^>]*\bsrc=/i,
|
||||
/type=["']module["']/i,
|
||||
/\/_astro\/[^"'<>\s]+\.js/i,
|
||||
];
|
||||
for (const pattern of forbidden) {
|
||||
if (pattern.test(html)) {
|
||||
console.error(`Unexpected client JavaScript matched ${pattern}`);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
NODE
|
||||
|
||||
- name: Verify Cloudflare image resizing URLs
|
||||
run: |
|
||||
node <<'NODE'
|
||||
const { readFileSync } = require('node:fs');
|
||||
const html = readFileSync('apps/landing-page/out/index.html', 'utf8');
|
||||
const resizedUrls = html.match(/https:\/\/static\.open-design\.ai\/cdn-cgi\/image\//g) ?? [];
|
||||
if (resizedUrls.length < 16) {
|
||||
console.error(`Expected at least 16 Cloudflare resized image URLs, found ${resizedUrls.length}`);
|
||||
process.exit(1);
|
||||
}
|
||||
if (/(?:src|content)=["']\/assets\/[A-Za-z0-9_.-]+\.png/.test(html)) {
|
||||
console.error('Found local /assets/*.png image reference in generated landing HTML.');
|
||||
process.exit(1);
|
||||
}
|
||||
NODE
|
||||
98
.github/workflows/landing-page-deploy.yml
vendored
Normal file
98
.github/workflows/landing-page-deploy.yml
vendored
Normal file
@@ -0,0 +1,98 @@
|
||||
name: landing-page-deploy
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
paths:
|
||||
- .github/workflows/landing-page-deploy.yml
|
||||
- .github/workflows/landing-page-ci.yml
|
||||
- apps/landing-page/**
|
||||
- package.json
|
||||
- pnpm-lock.yaml
|
||||
- pnpm-workspace.yaml
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
deployments: write
|
||||
|
||||
concurrency:
|
||||
group: landing-page-deploy-${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
deploy:
|
||||
name: Deploy landing page
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 15
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v6.0.2
|
||||
|
||||
- name: Setup pnpm
|
||||
uses: pnpm/action-setup@v5
|
||||
with:
|
||||
version: 10.33.2
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v6
|
||||
with:
|
||||
node-version: 24
|
||||
cache: pnpm
|
||||
|
||||
- name: Install dependencies
|
||||
run: pnpm install --frozen-lockfile
|
||||
|
||||
- name: Typecheck landing page
|
||||
run: pnpm --filter @open-design/landing-page typecheck
|
||||
|
||||
- name: Build landing page
|
||||
run: pnpm --filter @open-design/landing-page build
|
||||
|
||||
- name: Verify zero external JavaScript
|
||||
run: |
|
||||
node <<'NODE'
|
||||
const { readFileSync } = require('node:fs');
|
||||
const html = readFileSync('apps/landing-page/out/index.html', 'utf8');
|
||||
const forbidden = [
|
||||
/<script\b[^>]*\bsrc=/i,
|
||||
/type=["']module["']/i,
|
||||
/\/_astro\/[^"'<>\s]+\.js/i,
|
||||
];
|
||||
for (const pattern of forbidden) {
|
||||
if (pattern.test(html)) {
|
||||
console.error(`Unexpected client JavaScript matched ${pattern}`);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
NODE
|
||||
|
||||
- name: Verify Cloudflare image resizing URLs
|
||||
run: |
|
||||
node <<'NODE'
|
||||
const { readFileSync } = require('node:fs');
|
||||
const html = readFileSync('apps/landing-page/out/index.html', 'utf8');
|
||||
const resizedUrls = html.match(/https:\/\/static\.open-design\.ai\/cdn-cgi\/image\//g) ?? [];
|
||||
if (resizedUrls.length < 16) {
|
||||
console.error(`Expected at least 16 Cloudflare resized image URLs, found ${resizedUrls.length}`);
|
||||
process.exit(1);
|
||||
}
|
||||
if (/(?:src|content)=["']\/assets\/[A-Za-z0-9_.-]+\.png/.test(html)) {
|
||||
console.error('Found local /assets/*.png image reference in generated landing HTML.');
|
||||
process.exit(1);
|
||||
}
|
||||
NODE
|
||||
|
||||
- name: Deploy to Cloudflare Pages
|
||||
uses: cloudflare/wrangler-action@v3
|
||||
with:
|
||||
apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }}
|
||||
accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
|
||||
workingDirectory: apps/landing-page
|
||||
packageManager: npm
|
||||
command: >
|
||||
pages deploy out
|
||||
--project-name=open-design-landing
|
||||
--branch=${{ github.ref_name }}
|
||||
68
.github/workflows/metrics.yml
vendored
Normal file
68
.github/workflows/metrics.yml
vendored
Normal file
@@ -0,0 +1,68 @@
|
||||
name: github-metrics
|
||||
|
||||
on:
|
||||
schedule:
|
||||
# Runs daily at 00:15 UTC; output is committed to docs/assets/github-metrics.svg.
|
||||
- cron: '15 0 * * *'
|
||||
workflow_dispatch:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
paths:
|
||||
- .github/workflows/metrics.yml
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
pull-requests: write
|
||||
|
||||
jobs:
|
||||
metrics:
|
||||
name: Generate repository metrics SVG
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Generate GitHub repository metrics
|
||||
uses: lowlighter/metrics@latest
|
||||
with:
|
||||
# Output path; the action opens/updates a PR when this file changes.
|
||||
# Requires manual review to merge. If metrics unchanged, no PR is created.
|
||||
filename: docs/assets/github-metrics.svg
|
||||
|
||||
# Auth: METRICS_TOKEN must be a fine-grained PAT or GitHub App token that
|
||||
# can create pull requests in this repository. GITHUB_TOKEN is kept only
|
||||
# as a read/render fallback because many orgs disallow PR creation from it.
|
||||
token: ${{ secrets.METRICS_TOKEN || secrets.GITHUB_TOKEN }}
|
||||
committer_token: ${{ secrets.METRICS_TOKEN }}
|
||||
output_action: pull-request
|
||||
output_condition: data-changed
|
||||
|
||||
# Use the repository template (per-repo metrics, not user metrics).
|
||||
# Organization-owned repositories must be targeted explicitly, otherwise
|
||||
# lowlighter/metrics infers the token owner and treats the target as an org.
|
||||
template: repository
|
||||
base: ''
|
||||
user: nexu-io
|
||||
repo: open-design
|
||||
|
||||
# Plugins. Anything that requires a personal token will silently no-op
|
||||
# without METRICS_TOKEN — the rest still produce a useful SVG.
|
||||
plugin_contributors: yes
|
||||
plugin_contributors_categories: |
|
||||
{
|
||||
"Skills": "skills/**",
|
||||
"Design systems": "design-systems/**",
|
||||
"Web": "apps/web/**",
|
||||
"Daemon": "apps/daemon/**",
|
||||
"Docs": "docs/**"
|
||||
}
|
||||
plugin_followup: yes
|
||||
plugin_followup_sections: pr, issue
|
||||
plugin_languages: yes
|
||||
plugin_languages_details: lines, percentage
|
||||
plugin_languages_limit: 8
|
||||
plugin_lines: yes
|
||||
plugin_traffic: yes
|
||||
plugin_stargazers: yes
|
||||
plugin_stargazers_charts_type: chartist
|
||||
|
||||
config_timezone: Asia/Shanghai
|
||||
config_display: large
|
||||
55
.github/workflows/refresh-contributors-wall.yml
vendored
Normal file
55
.github/workflows/refresh-contributors-wall.yml
vendored
Normal file
@@ -0,0 +1,55 @@
|
||||
name: refresh-contributors-wall
|
||||
|
||||
on:
|
||||
# Daily refresh keeps the contributors wall CDN cache moving even when
|
||||
# contributor data changes outside pull request merges.
|
||||
schedule:
|
||||
- cron: '0 1 * * *'
|
||||
# Manual trigger: Use when you need to force-refresh the contributors wall
|
||||
# outside the daily schedule (e.g., after a bulk contributor update or
|
||||
# after fixing the cache_bust pattern in README files).
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
pull-requests: write
|
||||
|
||||
concurrency:
|
||||
group: refresh-contributors-wall
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
refresh:
|
||||
name: Refresh contributors wall cache bust
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v6.0.2
|
||||
|
||||
- name: Refresh cache bust date
|
||||
run: |
|
||||
DATE="$(date -u +%F)"
|
||||
MATCHES="$(perl -0ne '$count += () = /cache_bust=\d{4}-\d{2}-\d{2}/g; END { print $count + 0 }' README*.md)"
|
||||
if [ "$MATCHES" -eq 0 ]; then
|
||||
echo "Warning: No cache_bust patterns found. README format may have changed."
|
||||
exit 1
|
||||
fi
|
||||
perl -0pi -e "s/cache_bust=\d{4}-\d{2}-\d{2}/cache_bust=$DATE/g" README*.md
|
||||
|
||||
- name: Create refresh pull request
|
||||
uses: peter-evans/create-pull-request@v8
|
||||
with:
|
||||
# Auth mirrors the metrics workflow: prefer a repository token that can
|
||||
# create pull requests, with GITHUB_TOKEN as a fallback for repos where
|
||||
# Actions-created PRs are allowed.
|
||||
token: ${{ secrets.METRICS_TOKEN || secrets.GITHUB_TOKEN }}
|
||||
add-paths: 'README*.md'
|
||||
branch: automation/refresh-contributors-wall
|
||||
delete-branch: true
|
||||
commit-message: 'docs(readme): refresh contributors wall'
|
||||
title: 'docs(readme): refresh contributors wall'
|
||||
body: |
|
||||
Refreshes the contributors wall cache bust date in README files.
|
||||
|
||||
Generated by the scheduled `refresh-contributors-wall` workflow.
|
||||
556
.github/workflows/release-beta.yml
vendored
Normal file
556
.github/workflows/release-beta.yml
vendored
Normal file
@@ -0,0 +1,556 @@
|
||||
name: release-beta
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
signed:
|
||||
description: "Build signed/notarized mac artifacts. Disable only for explicit unsigned validation releases."
|
||||
required: true
|
||||
type: boolean
|
||||
default: true
|
||||
enable_mac:
|
||||
description: "Build and publish mac arm64 beta artifacts."
|
||||
required: true
|
||||
type: boolean
|
||||
default: true
|
||||
enable_win:
|
||||
description: "Build and publish Windows x64 beta artifacts."
|
||||
required: true
|
||||
type: boolean
|
||||
default: true
|
||||
enable_linux:
|
||||
description: "Build and publish Linux x64 AppImage beta artifacts."
|
||||
required: true
|
||||
type: boolean
|
||||
default: false
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
|
||||
concurrency:
|
||||
group: open-design-release-beta
|
||||
cancel-in-progress: false
|
||||
|
||||
jobs:
|
||||
metadata:
|
||||
name: Prepare beta metadata
|
||||
runs-on: ubuntu-latest
|
||||
env:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
GITHUB_REPOSITORY: ${{ github.repository }}
|
||||
OPEN_DESIGN_RELEASE_SIGNED: ${{ inputs.signed }}
|
||||
outputs:
|
||||
asset_version_suffix: ${{ steps.beta.outputs.asset_version_suffix }}
|
||||
base_version: ${{ steps.beta.outputs.base_version }}
|
||||
beta_tag: ${{ steps.beta.outputs.beta_tag }}
|
||||
beta_version: ${{ steps.beta.outputs.beta_version }}
|
||||
branch: ${{ steps.beta.outputs.branch }}
|
||||
commit: ${{ steps.beta.outputs.commit }}
|
||||
release_name: ${{ steps.beta.outputs.release_name }}
|
||||
signed: ${{ steps.beta.outputs.signed }}
|
||||
version_tag: ${{ steps.beta.outputs.version_tag }}
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v6.0.2
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v6
|
||||
with:
|
||||
node-version: 24
|
||||
|
||||
- name: Prepare beta release metadata
|
||||
id: beta
|
||||
run: node --experimental-strip-types ./scripts/release-beta.ts
|
||||
|
||||
build_mac:
|
||||
name: Build beta mac arm64
|
||||
needs: metadata
|
||||
if: ${{ inputs.enable_mac }}
|
||||
runs-on: macos-14
|
||||
env:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v6.0.2
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Setup pnpm
|
||||
uses: pnpm/action-setup@v5
|
||||
with:
|
||||
version: 10.33.2
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v6
|
||||
with:
|
||||
node-version: 24
|
||||
|
||||
- name: Install dependencies
|
||||
run: pnpm install --frozen-lockfile
|
||||
|
||||
- name: Apply beta package version
|
||||
run: npm pkg set "version=${{ needs.metadata.outputs.beta_version }}" --prefix apps/packaged
|
||||
|
||||
- name: Prepare Apple signing certificate
|
||||
if: ${{ inputs.signed }}
|
||||
env:
|
||||
APPLE_SIGNING_CERTIFICATE_BASE64: ${{ secrets.APPLE_SIGNING_CERTIFICATE_BASE64 }}
|
||||
APPLE_SIGNING_CERTIFICATE_PASSWORD: ${{ secrets.APPLE_SIGNING_CERTIFICATE_PASSWORD }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
cert_path="$RUNNER_TEMP/open-design-signing.p12"
|
||||
if ! printf '%s' "$APPLE_SIGNING_CERTIFICATE_BASE64" | base64 --decode > "$cert_path" 2>/dev/null; then
|
||||
printf '%s' "$APPLE_SIGNING_CERTIFICATE_BASE64" | base64 -D > "$cert_path"
|
||||
fi
|
||||
{
|
||||
echo "CSC_LINK=$cert_path"
|
||||
echo "CSC_KEY_PASSWORD=$APPLE_SIGNING_CERTIFICATE_PASSWORD"
|
||||
} >> "$GITHUB_ENV"
|
||||
|
||||
- name: Build beta mac artifacts
|
||||
env:
|
||||
APPLE_ID: ${{ secrets.APPLE_ID }}
|
||||
APPLE_APP_SPECIFIC_PASSWORD: ${{ secrets.APPLE_APP_SPECIFIC_PASSWORD }}
|
||||
APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
signed_flag=""
|
||||
if [ "${{ inputs.signed }}" = "true" ]; then
|
||||
signed_flag="--signed"
|
||||
fi
|
||||
pnpm exec tools-pack mac build \
|
||||
--dir "$RUNNER_TEMP/tools-pack" \
|
||||
--namespace release-beta \
|
||||
--portable \
|
||||
--mac-compression normal \
|
||||
--to all \
|
||||
--json \
|
||||
$signed_flag
|
||||
|
||||
- name: Smoke beta mac packaged runtime
|
||||
working-directory: e2e
|
||||
env:
|
||||
OD_PACKAGED_E2E_MAC: "1"
|
||||
OD_PACKAGED_E2E_NAMESPACE: release-beta
|
||||
OD_PACKAGED_E2E_TOOLS_PACK_DIR: ${{ runner.temp }}/tools-pack
|
||||
run: pnpm test specs/mac.spec.ts
|
||||
|
||||
- name: Prepare beta assets
|
||||
id: assets
|
||||
run: |
|
||||
set -euo pipefail
|
||||
release_dir="$RUNNER_TEMP/release-assets"
|
||||
mkdir -p "$release_dir"
|
||||
|
||||
source_dmg="$RUNNER_TEMP/tools-pack/out/mac/namespaces/release-beta/dmg/Open Design-release-beta.dmg"
|
||||
source_zip="$RUNNER_TEMP/tools-pack/out/mac/namespaces/release-beta/zip/Open Design-release-beta.zip"
|
||||
if [ ! -f "$source_dmg" ]; then
|
||||
echo "expected dmg not found at $source_dmg" >&2
|
||||
exit 1
|
||||
fi
|
||||
if [ ! -f "$source_zip" ]; then
|
||||
echo "expected zip not found at $source_zip" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
asset_suffix="${{ needs.metadata.outputs.asset_version_suffix }}"
|
||||
versioned_dmg="open-design-${{ needs.metadata.outputs.beta_version }}${asset_suffix}-mac-arm64.dmg"
|
||||
versioned_zip="open-design-${{ needs.metadata.outputs.beta_version }}${asset_suffix}-mac-arm64.zip"
|
||||
dmg_checksum_file="$versioned_dmg.sha256"
|
||||
zip_checksum_file="$versioned_zip.sha256"
|
||||
|
||||
cp "$source_dmg" "$release_dir/$versioned_dmg"
|
||||
cp "$source_zip" "$release_dir/$versioned_zip"
|
||||
(
|
||||
cd "$release_dir"
|
||||
shasum -a 256 "$versioned_dmg" > "$dmg_checksum_file"
|
||||
shasum -a 256 "$versioned_zip" > "$zip_checksum_file"
|
||||
)
|
||||
|
||||
zip_sha512="$(openssl dgst -sha512 -binary "$release_dir/$versioned_zip" | openssl base64 -A)"
|
||||
zip_size="$(stat -f%z "$release_dir/$versioned_zip")"
|
||||
zip_url="https://github.com/${GITHUB_REPOSITORY}/releases/download/${{ needs.metadata.outputs.version_tag }}/$versioned_zip"
|
||||
release_date="$(date -u +%Y-%m-%dT%H:%M:%SZ)"
|
||||
cat > "$release_dir/latest-mac.yml" <<EOF
|
||||
version: "${{ needs.metadata.outputs.beta_version }}"
|
||||
files:
|
||||
- url: "$zip_url"
|
||||
sha512: "$zip_sha512"
|
||||
size: $zip_size
|
||||
path: "$zip_url"
|
||||
sha512: "$zip_sha512"
|
||||
releaseDate: "$release_date"
|
||||
releaseNotes: "Open Design beta ${{ needs.metadata.outputs.beta_version }}${asset_suffix}"
|
||||
EOF
|
||||
|
||||
- name: Upload mac release bundle
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: open-design-beta-mac-release-assets
|
||||
path: ${{ runner.temp }}/release-assets
|
||||
|
||||
build_win:
|
||||
name: Build beta win x64
|
||||
needs: metadata
|
||||
if: ${{ inputs.enable_win }}
|
||||
runs-on: windows-latest
|
||||
env:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v6.0.2
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Setup pnpm
|
||||
uses: pnpm/action-setup@v5
|
||||
with:
|
||||
version: 10.33.2
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v6
|
||||
with:
|
||||
node-version: 24
|
||||
|
||||
- name: Install dependencies
|
||||
run: pnpm install --frozen-lockfile
|
||||
|
||||
- name: Apply beta package version
|
||||
run: npm pkg set "version=${{ needs.metadata.outputs.beta_version }}" --prefix apps/packaged
|
||||
|
||||
- name: Build beta windows artifacts
|
||||
shell: pwsh
|
||||
run: >-
|
||||
pnpm exec tools-pack win build
|
||||
--dir "${{ runner.temp }}/tools-pack"
|
||||
--namespace release-beta-win
|
||||
--portable
|
||||
--to nsis
|
||||
--json
|
||||
|
||||
- name: Prepare windows beta assets
|
||||
shell: pwsh
|
||||
run: |
|
||||
$releaseDir = Join-Path $env:RUNNER_TEMP "release-assets"
|
||||
New-Item -ItemType Directory -Force -Path $releaseDir | Out-Null
|
||||
|
||||
$sourceInstaller = Join-Path $env:RUNNER_TEMP "tools-pack/out/win/namespaces/release-beta-win/builder/Open Design-release-beta-win-setup.exe"
|
||||
$sourceBlockmap = Join-Path $env:RUNNER_TEMP "tools-pack/out/win/namespaces/release-beta-win/builder/Open Design-release-beta-win-setup.exe.blockmap"
|
||||
if (!(Test-Path $sourceInstaller)) {
|
||||
throw "expected installer not found at $sourceInstaller"
|
||||
}
|
||||
if (!(Test-Path $sourceBlockmap)) {
|
||||
throw "expected blockmap not found at $sourceBlockmap"
|
||||
}
|
||||
|
||||
$windowsAssetSuffix = ".unsigned"
|
||||
$versionedInstaller = "open-design-${{ needs.metadata.outputs.beta_version }}$windowsAssetSuffix-win-x64-setup.exe"
|
||||
$versionedBlockmap = "open-design-${{ needs.metadata.outputs.beta_version }}$windowsAssetSuffix-win-x64-setup.exe.blockmap"
|
||||
$checksumFile = "$versionedInstaller.sha256"
|
||||
Copy-Item $sourceInstaller (Join-Path $releaseDir $versionedInstaller)
|
||||
Copy-Item $sourceBlockmap (Join-Path $releaseDir $versionedBlockmap)
|
||||
|
||||
$installerPath = Join-Path $releaseDir $versionedInstaller
|
||||
$hash = (Get-FileHash -Path $installerPath -Algorithm SHA256).Hash.ToLowerInvariant()
|
||||
"$hash $versionedInstaller" | Set-Content -Path (Join-Path $releaseDir $checksumFile)
|
||||
$installerBytes = [System.IO.File]::ReadAllBytes($installerPath)
|
||||
$installerSha512 = [System.Convert]::ToBase64String([System.Security.Cryptography.SHA512]::Create().ComputeHash($installerBytes))
|
||||
$installerSize = (Get-Item $installerPath).Length
|
||||
$installerUrl = "https://github.com/$env:GITHUB_REPOSITORY/releases/download/${{ needs.metadata.outputs.version_tag }}/$versionedInstaller"
|
||||
$releaseDate = [DateTime]::UtcNow.ToString("yyyy-MM-ddTHH:mm:ssZ")
|
||||
@(
|
||||
'version: "${{ needs.metadata.outputs.beta_version }}"'
|
||||
'files:'
|
||||
" - url: `"$installerUrl`""
|
||||
" sha512: `"$installerSha512`""
|
||||
" size: $installerSize"
|
||||
"path: `"$installerUrl`""
|
||||
"sha512: `"$installerSha512`""
|
||||
"releaseDate: `"$releaseDate`""
|
||||
"releaseNotes: `"Open Design beta ${{ needs.metadata.outputs.beta_version }}$windowsAssetSuffix`""
|
||||
) | Set-Content -Path (Join-Path $releaseDir "latest.yml")
|
||||
|
||||
- name: Upload windows release bundle
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: open-design-beta-win-release-assets
|
||||
path: ${{ runner.temp }}/release-assets
|
||||
|
||||
build_linux:
|
||||
name: Build beta linux x64
|
||||
needs: metadata
|
||||
if: ${{ inputs.enable_linux }}
|
||||
runs-on: ubuntu-latest
|
||||
env:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v6.0.2
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Setup pnpm
|
||||
uses: pnpm/action-setup@v5
|
||||
with:
|
||||
version: 10.33.2
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v6
|
||||
with:
|
||||
node-version: 24
|
||||
|
||||
- name: Install dependencies
|
||||
run: pnpm install --frozen-lockfile
|
||||
|
||||
- name: Apply beta package version
|
||||
env:
|
||||
BETA_VERSION: ${{ needs.metadata.outputs.beta_version }}
|
||||
run: npm pkg set "version=$BETA_VERSION" --prefix apps/packaged
|
||||
|
||||
# `--containerized` builds the AppImage inside the electronuserland/builder
|
||||
# Docker image (glibc 2.27 baseline) so the resulting binary runs on older
|
||||
# distros than ubuntu-latest's glibc 2.39. Docker is preinstalled on the
|
||||
# GitHub-hosted ubuntu-latest runner, so no extra setup is required.
|
||||
- name: Build beta linux artifacts
|
||||
run: |
|
||||
set -euo pipefail
|
||||
pnpm exec tools-pack linux build \
|
||||
--dir "$RUNNER_TEMP/tools-pack" \
|
||||
--namespace release-beta-linux \
|
||||
--portable \
|
||||
--to appimage \
|
||||
--containerized \
|
||||
--json
|
||||
|
||||
- name: Prepare linux beta assets
|
||||
env:
|
||||
BETA_VERSION: ${{ needs.metadata.outputs.beta_version }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
release_dir="$RUNNER_TEMP/release-assets"
|
||||
mkdir -p "$release_dir"
|
||||
|
||||
source_appimage="$RUNNER_TEMP/tools-pack/out/linux/namespaces/release-beta-linux/builder/Open Design-release-beta-linux.AppImage"
|
||||
if [ ! -f "$source_appimage" ]; then
|
||||
echo "expected AppImage not found at $source_appimage" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Linux currently has no signing path in tools-pack, so the suffix is
|
||||
# hardcoded to .unsigned (matching the windows convention above).
|
||||
linux_asset_suffix=".unsigned"
|
||||
versioned_appimage="open-design-${BETA_VERSION}${linux_asset_suffix}-linux-x64.AppImage"
|
||||
checksum_file="$versioned_appimage.sha256"
|
||||
|
||||
cp "$source_appimage" "$release_dir/$versioned_appimage"
|
||||
(
|
||||
cd "$release_dir"
|
||||
sha256sum "$versioned_appimage" > "$checksum_file"
|
||||
)
|
||||
|
||||
- name: Upload linux release bundle
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: open-design-beta-linux-release-assets
|
||||
path: ${{ runner.temp }}/release-assets
|
||||
|
||||
publish:
|
||||
name: Publish beta release
|
||||
needs:
|
||||
- metadata
|
||||
- build_mac
|
||||
- build_win
|
||||
- build_linux
|
||||
if: >-
|
||||
${{
|
||||
always() &&
|
||||
!cancelled() &&
|
||||
needs.metadata.result == 'success' &&
|
||||
(inputs.enable_mac || inputs.enable_win || inputs.enable_linux) &&
|
||||
(!inputs.enable_mac || needs.build_mac.result == 'success') &&
|
||||
(!inputs.enable_win || needs.build_win.result == 'success') &&
|
||||
(!inputs.enable_linux || needs.build_linux.result == 'success')
|
||||
}}
|
||||
runs-on: ubuntu-latest
|
||||
env:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
ENABLE_MAC: ${{ inputs.enable_mac }}
|
||||
ENABLE_WIN: ${{ inputs.enable_win }}
|
||||
ENABLE_LINUX: ${{ inputs.enable_linux }}
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v6.0.2
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Download mac release bundle
|
||||
if: ${{ inputs.enable_mac }}
|
||||
uses: actions/download-artifact@v8
|
||||
with:
|
||||
name: open-design-beta-mac-release-assets
|
||||
path: ${{ runner.temp }}/release-assets/mac
|
||||
|
||||
- name: Download windows release bundle
|
||||
if: ${{ inputs.enable_win }}
|
||||
uses: actions/download-artifact@v8
|
||||
with:
|
||||
name: open-design-beta-win-release-assets
|
||||
path: ${{ runner.temp }}/release-assets/win
|
||||
|
||||
- name: Download linux release bundle
|
||||
if: ${{ inputs.enable_linux }}
|
||||
uses: actions/download-artifact@v8
|
||||
with:
|
||||
name: open-design-beta-linux-release-assets
|
||||
path: ${{ runner.temp }}/release-assets/linux
|
||||
|
||||
- name: Move beta tags to current commit
|
||||
run: |
|
||||
set -euo pipefail
|
||||
git tag -f "${{ needs.metadata.outputs.version_tag }}" "$GITHUB_SHA"
|
||||
git push origin "refs/tags/${{ needs.metadata.outputs.version_tag }}" --force
|
||||
git tag -f "${{ needs.metadata.outputs.beta_tag }}" "$GITHUB_SHA"
|
||||
git push origin "refs/tags/${{ needs.metadata.outputs.beta_tag }}" --force
|
||||
|
||||
- name: Write release notes
|
||||
id: notes
|
||||
run: |
|
||||
set -euo pipefail
|
||||
version_notes_file="$RUNNER_TEMP/open-design-beta-version-notes.md"
|
||||
latest_notes_file="$RUNNER_TEMP/open-design-beta-latest-notes.md"
|
||||
cat > "$version_notes_file" <<EOF
|
||||
## Summary
|
||||
- channel: beta
|
||||
- version: ${{ needs.metadata.outputs.beta_version }}
|
||||
- base version: ${{ needs.metadata.outputs.base_version }}
|
||||
- mac enabled: ${{ inputs.enable_mac }}
|
||||
- mac signed/notarized: ${{ needs.metadata.outputs.signed }}
|
||||
- windows enabled: ${{ inputs.enable_win }}
|
||||
- windows signed: false
|
||||
- linux enabled: ${{ inputs.enable_linux }}
|
||||
- branch: ${{ needs.metadata.outputs.branch }}
|
||||
- commit: ${{ needs.metadata.outputs.commit }}
|
||||
|
||||
This beta release ships the enabled platform artifacts, checksums, and updater feed files for enabled auto-update platforms. Linux AppImage has no auto-update feed yet.
|
||||
EOF
|
||||
cat > "$latest_notes_file" <<EOF
|
||||
## Summary
|
||||
- channel: beta
|
||||
- latest version: ${{ needs.metadata.outputs.beta_version }}
|
||||
- latest tag: ${{ needs.metadata.outputs.version_tag }}
|
||||
|
||||
This release is the mutable beta channel feed carrier. It should contain enabled feed assets only: latest-mac.yml and/or latest.yml. If neither mac nor Windows is enabled, no feed assets are expected.
|
||||
EOF
|
||||
{
|
||||
echo "version_notes_file=$version_notes_file"
|
||||
echo "latest_notes_file=$latest_notes_file"
|
||||
} >> "$GITHUB_OUTPUT"
|
||||
|
||||
- name: Create or update immutable beta prerelease
|
||||
run: |
|
||||
set -euo pipefail
|
||||
all_release_dir="$RUNNER_TEMP/release-assets/all"
|
||||
mkdir -p "$all_release_dir"
|
||||
for asset_dir in "$RUNNER_TEMP/release-assets/mac" "$RUNNER_TEMP/release-assets/win" "$RUNNER_TEMP/release-assets/linux"; do
|
||||
if [ -d "$asset_dir" ] && compgen -G "$asset_dir/*" > /dev/null; then
|
||||
cp "$asset_dir"/* "$all_release_dir/"
|
||||
fi
|
||||
done
|
||||
if ! compgen -G "$all_release_dir/*" > /dev/null; then
|
||||
echo "no enabled beta release assets were found" >&2
|
||||
exit 1
|
||||
fi
|
||||
declare -A current_release_assets=()
|
||||
for asset_path in "$all_release_dir"/*; do
|
||||
current_release_assets["$(basename "$asset_path")"]=1
|
||||
done
|
||||
if gh release view "${{ needs.metadata.outputs.version_tag }}" >/dev/null 2>&1; then
|
||||
gh release edit "${{ needs.metadata.outputs.version_tag }}" \
|
||||
--title "${{ needs.metadata.outputs.release_name }}" \
|
||||
--notes-file "${{ steps.notes.outputs.version_notes_file }}" \
|
||||
--prerelease
|
||||
else
|
||||
gh release create "${{ needs.metadata.outputs.version_tag }}" \
|
||||
--target "$GITHUB_SHA" \
|
||||
--title "${{ needs.metadata.outputs.release_name }}" \
|
||||
--notes-file "${{ steps.notes.outputs.version_notes_file }}" \
|
||||
--prerelease
|
||||
fi
|
||||
gh release upload "${{ needs.metadata.outputs.version_tag }}" "$all_release_dir"/* --clobber
|
||||
while IFS= read -r asset_name; do
|
||||
if [ -n "$asset_name" ] && [ -z "${current_release_assets[$asset_name]+x}" ]; then
|
||||
gh release delete-asset "${{ needs.metadata.outputs.version_tag }}" "$asset_name" --yes
|
||||
fi
|
||||
done < <(gh release view "${{ needs.metadata.outputs.version_tag }}" --json assets --jq '.assets[].name')
|
||||
|
||||
- name: Create or update beta channel feed
|
||||
run: |
|
||||
set -euo pipefail
|
||||
latest_mac_path="$RUNNER_TEMP/release-assets/mac/latest-mac.yml"
|
||||
latest_win_path="$RUNNER_TEMP/release-assets/win/latest.yml"
|
||||
feed_assets=()
|
||||
if [ "$ENABLE_MAC" = "true" ]; then
|
||||
if [ ! -f "$latest_mac_path" ]; then
|
||||
echo "expected mac feed not found at $latest_mac_path" >&2
|
||||
exit 1
|
||||
fi
|
||||
feed_assets+=("$latest_mac_path")
|
||||
fi
|
||||
if [ "$ENABLE_WIN" = "true" ]; then
|
||||
if [ ! -f "$latest_win_path" ]; then
|
||||
echo "expected windows feed not found at $latest_win_path" >&2
|
||||
exit 1
|
||||
fi
|
||||
feed_assets+=("$latest_win_path")
|
||||
fi
|
||||
declare -A current_feed_assets=()
|
||||
for feed_asset in "${feed_assets[@]}"; do
|
||||
current_feed_assets["$(basename "$feed_asset")"]=1
|
||||
done
|
||||
if gh release view "${{ needs.metadata.outputs.beta_tag }}" >/dev/null 2>&1; then
|
||||
gh release edit "${{ needs.metadata.outputs.beta_tag }}" \
|
||||
--title "Open Design Beta Latest" \
|
||||
--notes-file "${{ steps.notes.outputs.latest_notes_file }}" \
|
||||
--prerelease
|
||||
else
|
||||
gh release create "${{ needs.metadata.outputs.beta_tag }}" \
|
||||
--target "$GITHUB_SHA" \
|
||||
--title "Open Design Beta Latest" \
|
||||
--notes-file "${{ steps.notes.outputs.latest_notes_file }}" \
|
||||
--prerelease
|
||||
fi
|
||||
if [ "${#feed_assets[@]}" -gt 0 ]; then
|
||||
gh release upload "${{ needs.metadata.outputs.beta_tag }}" "${feed_assets[@]}" --clobber
|
||||
fi
|
||||
while IFS= read -r asset_name; do
|
||||
if [ -n "$asset_name" ] && [ -z "${current_feed_assets[$asset_name]+x}" ]; then
|
||||
gh release delete-asset "${{ needs.metadata.outputs.beta_tag }}" "$asset_name" --yes
|
||||
fi
|
||||
done < <(gh release view "${{ needs.metadata.outputs.beta_tag }}" --json assets --jq '.assets[].name')
|
||||
|
||||
- name: Publish summary
|
||||
run: |
|
||||
{
|
||||
echo "## Beta release"
|
||||
echo "- Channel: beta"
|
||||
echo "- Version: ${{ needs.metadata.outputs.beta_version }}"
|
||||
echo "- Version tag: ${{ needs.metadata.outputs.version_tag }}"
|
||||
echo "- Channel feed tag: ${{ needs.metadata.outputs.beta_tag }}"
|
||||
echo "- mac enabled: $ENABLE_MAC"
|
||||
echo "- mac signed/notarized: ${{ needs.metadata.outputs.signed }}"
|
||||
echo "- windows enabled: $ENABLE_WIN"
|
||||
echo "- windows signed: false"
|
||||
echo "- linux enabled: $ENABLE_LINUX"
|
||||
if [ "$ENABLE_MAC" = "true" ]; then
|
||||
echo "- mac assets: open-design-${{ needs.metadata.outputs.beta_version }}${{ needs.metadata.outputs.asset_version_suffix }}-mac-arm64.dmg, open-design-${{ needs.metadata.outputs.beta_version }}${{ needs.metadata.outputs.asset_version_suffix }}-mac-arm64.zip"
|
||||
fi
|
||||
if [ "$ENABLE_WIN" = "true" ]; then
|
||||
echo "- win assets: open-design-${{ needs.metadata.outputs.beta_version }}.unsigned-win-x64-setup.exe, open-design-${{ needs.metadata.outputs.beta_version }}.unsigned-win-x64-setup.exe.blockmap"
|
||||
fi
|
||||
if [ "$ENABLE_LINUX" = "true" ]; then
|
||||
echo "- linux assets: open-design-${{ needs.metadata.outputs.beta_version }}.unsigned-linux-x64.AppImage"
|
||||
fi
|
||||
echo "- Feeds: enabled mac/win feeds only (no latest-linux.yml; AppImage updater not yet wired)"
|
||||
} >> "$GITHUB_STEP_SUMMARY"
|
||||
483
.github/workflows/release-stable.yml
vendored
Normal file
483
.github/workflows/release-stable.yml
vendored
Normal file
@@ -0,0 +1,483 @@
|
||||
name: release-stable
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
mac_signed:
|
||||
description: "Build signed/notarized mac artifacts. Disable only for explicit unsigned validation releases."
|
||||
required: true
|
||||
type: boolean
|
||||
default: true
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
|
||||
concurrency:
|
||||
group: open-design-release-stable
|
||||
cancel-in-progress: false
|
||||
|
||||
jobs:
|
||||
metadata:
|
||||
name: Prepare stable metadata
|
||||
runs-on: ubuntu-latest
|
||||
env:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
GITHUB_REPOSITORY: ${{ github.repository }}
|
||||
outputs:
|
||||
base_version: ${{ steps.stable.outputs.base_version }}
|
||||
branch: ${{ steps.stable.outputs.branch }}
|
||||
commit: ${{ steps.stable.outputs.commit }}
|
||||
mac_signed: ${{ inputs.mac_signed }}
|
||||
previous_stable: ${{ steps.stable.outputs.previous_stable }}
|
||||
release_name: ${{ steps.stable.outputs.release_name }}
|
||||
stable_version: ${{ steps.stable.outputs.stable_version }}
|
||||
version_tag: ${{ steps.stable.outputs.version_tag }}
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v6.0.2
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v6
|
||||
with:
|
||||
node-version: 24
|
||||
|
||||
- name: Prepare stable release metadata
|
||||
id: stable
|
||||
run: node --experimental-strip-types ./scripts/release-stable.ts
|
||||
|
||||
verify:
|
||||
name: Verify build (typecheck + tests)
|
||||
needs: metadata
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v6.0.2
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Setup pnpm
|
||||
uses: pnpm/action-setup@v5
|
||||
with:
|
||||
version: 10.33.2
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v6
|
||||
with:
|
||||
node-version: 24
|
||||
|
||||
- name: Install dependencies
|
||||
run: pnpm install --frozen-lockfile
|
||||
|
||||
# `scripts/postinstall.mjs` auto-builds `packages/*` and `tools/*`, but
|
||||
# `apps/daemon` and `apps/desktop` are not in that list. On a fresh clone
|
||||
# (every CI run), workspace typecheck fails because:
|
||||
# - packaged/runtime consumers resolve the daemon package export through
|
||||
# generated `apps/daemon/dist/*.d.ts`
|
||||
# - `apps/packaged/src/index.ts` dynamic-imports `@open-design/desktop/main`
|
||||
# which resolves to `apps/desktop/dist/main/index.d.ts`
|
||||
# Build them explicitly here. Keeps the root `typecheck` script untouched.
|
||||
- name: Build daemon and desktop (typecheck dependencies)
|
||||
run: |
|
||||
pnpm --filter @open-design/daemon build
|
||||
pnpm --filter @open-design/desktop build
|
||||
|
||||
- name: Typecheck workspaces
|
||||
run: pnpm -r --workspace-concurrency=1 --if-present run typecheck
|
||||
|
||||
- name: Check repository layout policies
|
||||
run: pnpm guard
|
||||
|
||||
# Workspace tests are intentionally not gated here. apps/web's
|
||||
# i18n content-coverage tests assert that every locale carries
|
||||
# display metadata for every prompt template / skill / design
|
||||
# system. Those tests fail on `main` as of this writing because
|
||||
# PR #187 added two new prompt templates without translating
|
||||
# their metadata into the 9 ship-ready locales — an i18n drift
|
||||
# that's out of scope for the release infrastructure. Tracked as
|
||||
# a follow-up; revisit once locale metadata is back in sync.
|
||||
|
||||
build_mac:
|
||||
name: Build stable mac arm64
|
||||
needs: [metadata, verify]
|
||||
runs-on: macos-14
|
||||
env:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v6.0.2
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Setup pnpm
|
||||
uses: pnpm/action-setup@v5
|
||||
with:
|
||||
version: 10.33.2
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v6
|
||||
with:
|
||||
node-version: 24
|
||||
|
||||
- name: Install dependencies
|
||||
run: pnpm install --frozen-lockfile
|
||||
|
||||
- name: Prepare Apple signing certificate
|
||||
if: ${{ inputs.mac_signed }}
|
||||
env:
|
||||
APPLE_SIGNING_CERTIFICATE_BASE64: ${{ secrets.APPLE_SIGNING_CERTIFICATE_BASE64 }}
|
||||
APPLE_SIGNING_CERTIFICATE_PASSWORD: ${{ secrets.APPLE_SIGNING_CERTIFICATE_PASSWORD }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
cert_path="$RUNNER_TEMP/open-design-signing.p12"
|
||||
if ! printf '%s' "$APPLE_SIGNING_CERTIFICATE_BASE64" | base64 --decode > "$cert_path" 2>/dev/null; then
|
||||
printf '%s' "$APPLE_SIGNING_CERTIFICATE_BASE64" | base64 -D > "$cert_path"
|
||||
fi
|
||||
{
|
||||
echo "CSC_LINK=$cert_path"
|
||||
echo "CSC_KEY_PASSWORD=$APPLE_SIGNING_CERTIFICATE_PASSWORD"
|
||||
} >> "$GITHUB_ENV"
|
||||
|
||||
- name: Build stable mac artifacts
|
||||
env:
|
||||
APPLE_ID: ${{ secrets.APPLE_ID }}
|
||||
APPLE_APP_SPECIFIC_PASSWORD: ${{ secrets.APPLE_APP_SPECIFIC_PASSWORD }}
|
||||
APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
signed_flag=""
|
||||
if [ "${{ inputs.mac_signed }}" = "true" ]; then
|
||||
signed_flag="--signed"
|
||||
fi
|
||||
pnpm exec tools-pack mac build \
|
||||
--dir "$RUNNER_TEMP/tools-pack" \
|
||||
--namespace release-stable \
|
||||
--portable \
|
||||
--mac-compression normal \
|
||||
--to all \
|
||||
--json \
|
||||
$signed_flag
|
||||
|
||||
- name: Prepare stable mac assets
|
||||
id: assets
|
||||
run: |
|
||||
set -euo pipefail
|
||||
release_dir="$RUNNER_TEMP/release-assets"
|
||||
mkdir -p "$release_dir"
|
||||
|
||||
source_dmg="$RUNNER_TEMP/tools-pack/out/mac/namespaces/release-stable/dmg/Open Design-release-stable.dmg"
|
||||
source_zip="$RUNNER_TEMP/tools-pack/out/mac/namespaces/release-stable/zip/Open Design-release-stable.zip"
|
||||
if [ ! -f "$source_dmg" ]; then
|
||||
echo "expected dmg not found at $source_dmg" >&2
|
||||
exit 1
|
||||
fi
|
||||
if [ ! -f "$source_zip" ]; then
|
||||
echo "expected zip not found at $source_zip" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
versioned_dmg="open-design-${{ needs.metadata.outputs.stable_version }}-mac-arm64.dmg"
|
||||
versioned_zip="open-design-${{ needs.metadata.outputs.stable_version }}-mac-arm64.zip"
|
||||
dmg_checksum_file="$versioned_dmg.sha256"
|
||||
zip_checksum_file="$versioned_zip.sha256"
|
||||
|
||||
cp "$source_dmg" "$release_dir/$versioned_dmg"
|
||||
cp "$source_zip" "$release_dir/$versioned_zip"
|
||||
(
|
||||
cd "$release_dir"
|
||||
shasum -a 256 "$versioned_dmg" > "$dmg_checksum_file"
|
||||
shasum -a 256 "$versioned_zip" > "$zip_checksum_file"
|
||||
)
|
||||
|
||||
zip_sha512="$(openssl dgst -sha512 -binary "$release_dir/$versioned_zip" | openssl base64 -A)"
|
||||
zip_size="$(stat -f%z "$release_dir/$versioned_zip")"
|
||||
zip_url="https://github.com/${GITHUB_REPOSITORY}/releases/download/${{ needs.metadata.outputs.version_tag }}/$versioned_zip"
|
||||
release_date="$(date -u +%Y-%m-%dT%H:%M:%SZ)"
|
||||
cat > "$release_dir/latest-mac.yml" <<EOF
|
||||
version: "${{ needs.metadata.outputs.stable_version }}"
|
||||
files:
|
||||
- url: "$zip_url"
|
||||
sha512: "$zip_sha512"
|
||||
size: $zip_size
|
||||
path: "$zip_url"
|
||||
sha512: "$zip_sha512"
|
||||
releaseDate: "$release_date"
|
||||
releaseNotes: "Open Design ${{ needs.metadata.outputs.stable_version }}"
|
||||
EOF
|
||||
|
||||
- name: Upload mac release bundle
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: open-design-stable-mac-release-assets
|
||||
path: ${{ runner.temp }}/release-assets
|
||||
|
||||
build_win:
|
||||
name: Build stable win x64
|
||||
needs: [metadata, verify]
|
||||
runs-on: windows-latest
|
||||
env:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v6.0.2
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Setup pnpm
|
||||
uses: pnpm/action-setup@v5
|
||||
with:
|
||||
version: 10.33.2
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v6
|
||||
with:
|
||||
node-version: 24
|
||||
|
||||
- name: Install dependencies
|
||||
run: pnpm install --frozen-lockfile
|
||||
|
||||
- name: Build stable windows artifacts
|
||||
shell: pwsh
|
||||
run: >-
|
||||
pnpm exec tools-pack win build
|
||||
--dir "${{ runner.temp }}/tools-pack"
|
||||
--namespace release-stable-win
|
||||
--portable
|
||||
--to nsis
|
||||
--json
|
||||
|
||||
- name: Prepare windows stable assets
|
||||
shell: pwsh
|
||||
run: |
|
||||
$releaseDir = Join-Path $env:RUNNER_TEMP "release-assets"
|
||||
New-Item -ItemType Directory -Force -Path $releaseDir | Out-Null
|
||||
|
||||
$sourceInstaller = Join-Path $env:RUNNER_TEMP "tools-pack/out/win/namespaces/release-stable-win/builder/Open Design-release-stable-win-setup.exe"
|
||||
$sourceBlockmap = Join-Path $env:RUNNER_TEMP "tools-pack/out/win/namespaces/release-stable-win/builder/Open Design-release-stable-win-setup.exe.blockmap"
|
||||
if (!(Test-Path $sourceInstaller)) {
|
||||
throw "expected installer not found at $sourceInstaller"
|
||||
}
|
||||
if (!(Test-Path $sourceBlockmap)) {
|
||||
throw "expected blockmap not found at $sourceBlockmap"
|
||||
}
|
||||
|
||||
$versionedInstaller = "open-design-${{ needs.metadata.outputs.stable_version }}-win-x64-setup.exe"
|
||||
$versionedBlockmap = "open-design-${{ needs.metadata.outputs.stable_version }}-win-x64-setup.exe.blockmap"
|
||||
$checksumFile = "$versionedInstaller.sha256"
|
||||
Copy-Item $sourceInstaller (Join-Path $releaseDir $versionedInstaller)
|
||||
Copy-Item $sourceBlockmap (Join-Path $releaseDir $versionedBlockmap)
|
||||
|
||||
$installerPath = Join-Path $releaseDir $versionedInstaller
|
||||
$hash = (Get-FileHash -Path $installerPath -Algorithm SHA256).Hash.ToLowerInvariant()
|
||||
"$hash $versionedInstaller" | Set-Content -Path (Join-Path $releaseDir $checksumFile)
|
||||
$installerBytes = [System.IO.File]::ReadAllBytes($installerPath)
|
||||
$installerSha512 = [System.Convert]::ToBase64String([System.Security.Cryptography.SHA512]::Create().ComputeHash($installerBytes))
|
||||
$installerSize = (Get-Item $installerPath).Length
|
||||
$installerUrl = "https://github.com/$env:GITHUB_REPOSITORY/releases/download/${{ needs.metadata.outputs.version_tag }}/$versionedInstaller"
|
||||
$releaseDate = [DateTime]::UtcNow.ToString("yyyy-MM-ddTHH:mm:ssZ")
|
||||
@(
|
||||
'version: "${{ needs.metadata.outputs.stable_version }}"'
|
||||
'files:'
|
||||
" - url: `"$installerUrl`""
|
||||
" sha512: `"$installerSha512`""
|
||||
" size: $installerSize"
|
||||
"path: `"$installerUrl`""
|
||||
"sha512: `"$installerSha512`""
|
||||
"releaseDate: `"$releaseDate`""
|
||||
"releaseNotes: `"Open Design ${{ needs.metadata.outputs.stable_version }}`""
|
||||
) | Set-Content -Path (Join-Path $releaseDir "latest.yml")
|
||||
|
||||
- name: Upload windows release bundle
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: open-design-stable-win-release-assets
|
||||
path: ${{ runner.temp }}/release-assets
|
||||
|
||||
build_linux:
|
||||
name: Build stable linux x64
|
||||
needs: [metadata, verify]
|
||||
# Linux AppImage packaging is temporarily excluded from stable releases.
|
||||
# Keep the job definition in place so the Linux lane can be re-enabled once
|
||||
# the containerized pnpm bootstrap is fixed and reviewed.
|
||||
if: ${{ false }}
|
||||
runs-on: ubuntu-latest
|
||||
env:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v6.0.2
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Setup pnpm
|
||||
uses: pnpm/action-setup@v5
|
||||
with:
|
||||
version: 10.33.2
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v6
|
||||
with:
|
||||
node-version: 24
|
||||
|
||||
- name: Install dependencies
|
||||
run: pnpm install --frozen-lockfile
|
||||
|
||||
# `--containerized` builds the AppImage inside the electronuserland/builder
|
||||
# Docker image (glibc 2.27 baseline) so the resulting binary runs on older
|
||||
# distros than ubuntu-latest's glibc 2.39. Docker is preinstalled on the
|
||||
# GitHub-hosted ubuntu-latest runner, so no extra setup is required.
|
||||
- name: Build stable linux artifacts
|
||||
run: |
|
||||
set -euo pipefail
|
||||
pnpm exec tools-pack linux build \
|
||||
--dir "$RUNNER_TEMP/tools-pack" \
|
||||
--namespace release-stable-linux \
|
||||
--portable \
|
||||
--to appimage \
|
||||
--containerized \
|
||||
--json
|
||||
|
||||
- name: Prepare linux stable assets
|
||||
env:
|
||||
STABLE_VERSION: ${{ needs.metadata.outputs.stable_version }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
release_dir="$RUNNER_TEMP/release-assets"
|
||||
mkdir -p "$release_dir"
|
||||
|
||||
source_appimage="$RUNNER_TEMP/tools-pack/out/linux/namespaces/release-stable-linux/builder/Open Design-release-stable-linux.AppImage"
|
||||
if [ ! -f "$source_appimage" ]; then
|
||||
echo "expected AppImage not found at $source_appimage" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Linux currently has no signing path in tools-pack; the asset has no
|
||||
# signing-related suffix (matches the windows convention above).
|
||||
versioned_appimage="open-design-${STABLE_VERSION}-linux-x64.AppImage"
|
||||
checksum_file="$versioned_appimage.sha256"
|
||||
|
||||
cp "$source_appimage" "$release_dir/$versioned_appimage"
|
||||
(
|
||||
cd "$release_dir"
|
||||
sha256sum "$versioned_appimage" > "$checksum_file"
|
||||
)
|
||||
|
||||
- name: Upload linux release bundle
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: open-design-stable-linux-release-assets
|
||||
path: ${{ runner.temp }}/release-assets
|
||||
|
||||
publish:
|
||||
name: Publish stable release
|
||||
needs:
|
||||
- metadata
|
||||
- verify
|
||||
- build_mac
|
||||
- build_win
|
||||
runs-on: ubuntu-latest
|
||||
env:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v6.0.2
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Pre-flight tag/release check
|
||||
run: |
|
||||
set -euo pipefail
|
||||
if git ls-remote --exit-code --tags origin "refs/tags/${{ needs.metadata.outputs.version_tag }}" >/dev/null 2>&1; then
|
||||
echo "tag ${{ needs.metadata.outputs.version_tag }} already exists on origin; aborting" >&2
|
||||
exit 1
|
||||
fi
|
||||
if gh release view "${{ needs.metadata.outputs.version_tag }}" >/dev/null 2>&1; then
|
||||
echo "release ${{ needs.metadata.outputs.version_tag }} already exists; aborting" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
- name: Download mac release bundle
|
||||
uses: actions/download-artifact@v8
|
||||
with:
|
||||
name: open-design-stable-mac-release-assets
|
||||
path: ${{ runner.temp }}/release-assets/mac
|
||||
|
||||
- name: Download windows release bundle
|
||||
uses: actions/download-artifact@v8
|
||||
with:
|
||||
name: open-design-stable-win-release-assets
|
||||
path: ${{ runner.temp }}/release-assets/win
|
||||
|
||||
- name: Write release notes shell
|
||||
id: notes
|
||||
run: |
|
||||
set -euo pipefail
|
||||
notes_file="$RUNNER_TEMP/open-design-stable-notes.md"
|
||||
cat > "$notes_file" <<EOF
|
||||
## Summary
|
||||
- channel: stable
|
||||
- version: ${{ needs.metadata.outputs.stable_version }}
|
||||
- mac signed/notarized: ${{ inputs.mac_signed }}
|
||||
- windows signed: false
|
||||
- branch: ${{ needs.metadata.outputs.branch }}
|
||||
- commit: ${{ needs.metadata.outputs.commit }}
|
||||
|
||||
See [CHANGELOG.md](https://github.com/${GITHUB_REPOSITORY}/blob/${{ needs.metadata.outputs.version_tag }}/CHANGELOG.md) for the full release notes.
|
||||
|
||||
This stable release ships mac arm64 DMG/update ZIP, Windows x64 NSIS installer assets, checksums, and updater feed files. Linux AppImage packaging is temporarily deferred from the stable release lane.
|
||||
EOF
|
||||
echo "notes_file=$notes_file" >> "$GITHUB_OUTPUT"
|
||||
|
||||
- name: Create draft release with tag
|
||||
id: create_release
|
||||
run: |
|
||||
set -euo pipefail
|
||||
# gh release create creates the tag at $GITHUB_SHA atomically with the release.
|
||||
# Using --draft keeps the release invisible until all assets upload successfully;
|
||||
# the cleanup step rolls back the release + tag together if any subsequent step fails.
|
||||
gh release create "${{ needs.metadata.outputs.version_tag }}" \
|
||||
--target "$GITHUB_SHA" \
|
||||
--title "${{ needs.metadata.outputs.release_name }}" \
|
||||
--notes-file "${{ steps.notes.outputs.notes_file }}" \
|
||||
--draft
|
||||
|
||||
- name: Upload assets to draft release
|
||||
run: |
|
||||
set -euo pipefail
|
||||
all_release_dir="$RUNNER_TEMP/release-assets/all"
|
||||
mkdir -p "$all_release_dir"
|
||||
cp "$RUNNER_TEMP/release-assets/mac"/* "$all_release_dir/"
|
||||
cp "$RUNNER_TEMP/release-assets/win"/* "$all_release_dir/"
|
||||
gh release upload "${{ needs.metadata.outputs.version_tag }}" "$all_release_dir"/*
|
||||
|
||||
- name: Promote draft to published latest
|
||||
run: |
|
||||
set -euo pipefail
|
||||
gh release edit "${{ needs.metadata.outputs.version_tag }}" \
|
||||
--draft=false \
|
||||
--latest
|
||||
|
||||
- name: Cleanup release + tag on failure
|
||||
if: failure() && steps.create_release.outcome == 'success'
|
||||
run: |
|
||||
set +e
|
||||
echo "publish failed after release was created; rolling back release and tag"
|
||||
gh release delete "${{ needs.metadata.outputs.version_tag }}" --cleanup-tag --yes
|
||||
# belt-and-suspenders: ensure remote tag is gone even if --cleanup-tag missed
|
||||
git push origin --delete "refs/tags/${{ needs.metadata.outputs.version_tag }}" || true
|
||||
|
||||
- name: Publish summary
|
||||
run: |
|
||||
{
|
||||
echo "## Stable release"
|
||||
echo "- Channel: stable"
|
||||
echo "- Version: ${{ needs.metadata.outputs.stable_version }}"
|
||||
echo "- Version tag: ${{ needs.metadata.outputs.version_tag }}"
|
||||
echo "- mac signed/notarized: ${{ inputs.mac_signed }}"
|
||||
echo "- windows signed: false"
|
||||
echo "- mac assets: open-design-${{ needs.metadata.outputs.stable_version }}-mac-arm64.dmg, open-design-${{ needs.metadata.outputs.stable_version }}-mac-arm64.zip"
|
||||
echo "- win assets: open-design-${{ needs.metadata.outputs.stable_version }}-win-x64-setup.exe, open-design-${{ needs.metadata.outputs.stable_version }}-win-x64-setup.exe.blockmap"
|
||||
echo "- linux assets: deferred from this stable release"
|
||||
echo "- Feeds: latest-mac.yml, latest.yml"
|
||||
} >> "$GITHUB_STEP_SUMMARY"
|
||||
Reference in New Issue
Block a user