From 751267392df945b4cad47ce3e51a758eb2d90305 Mon Sep 17 00:00:00 2001 From: Steven Enamakel <31011319+senamakel@users.noreply.github.com> Date: Mon, 22 Jun 2026 11:51:24 -0700 Subject: [PATCH] chore(ci): prune remaining superseded/redundant GitHub Actions workflows (#3935) --- .github/workflows/contributor-rewards.yml | 346 ----------- .github/workflows/deploy.yml | 688 ---------------------- .github/workflows/uptime-monitor.yml | 128 ---- .github/workflows/weekly-code-review.yml | 128 ---- 4 files changed, 1290 deletions(-) delete mode 100644 .github/workflows/contributor-rewards.yml delete mode 100644 .github/workflows/deploy.yml delete mode 100644 .github/workflows/uptime-monitor.yml delete mode 100644 .github/workflows/weekly-code-review.yml diff --git a/.github/workflows/contributor-rewards.yml b/.github/workflows/contributor-rewards.yml deleted file mode 100644 index 2b1f42a60..000000000 --- a/.github/workflows/contributor-rewards.yml +++ /dev/null @@ -1,346 +0,0 @@ ---- -# Security note: this workflow intentionally uses `pull_request_target` so the -# `reward user` label can fire on PRs from forks (the plain `pull_request` -# trigger cannot see labels applied to fork PRs and runs under a read-only -# GITHUB_TOKEN that cannot post comments / add labels). -# -# The job is bounded: -# - it never checks out fork code (no `actions/checkout` with `pull_request.head.ref`) -# - it never executes arbitrary scripts from the PR -# - the only side effects are: posting a templated comment and adding a label -# - permissions are minimal (contents:read, issues:write, pull-requests:read) -# - all PR/issue user metadata interpolated into search queries or comment -# bodies is treated as untrusted and normalized through safeLogin/safeUrl -# before use (see the inline script below). -# -# Do NOT broaden permissions, add a fork checkout, or remove the input -# normalization without a security review. - -name: Contributor Rewards - -on: - pull_request_target: - types: [closed, labeled] - issues: - types: [labeled] - workflow_dispatch: - -permissions: - contents: read - issues: write - pull-requests: read - -concurrency: - group: contributor-rewards-${{ github.event.pull_request.number || github.event.issue.number || github.run_id }} - cancel-in-progress: false - -jobs: - reward: - name: Invite eligible contributor - runs-on: ubuntu-latest - timeout-minutes: 5 - if: >- - github.event_name == 'workflow_dispatch' || - (github.event_name == 'pull_request_target' && - github.event.action == 'closed' && - github.event.pull_request.merged == true) || - (github.event.action == 'labeled' && - github.event.label.name == 'reward user') - steps: - - name: Post reward invite when eligible - uses: actions/github-script@v8 - env: - CONTRIBUTOR_REWARD_TRIGGER_LABEL: reward user - CONTRIBUTOR_REWARD_SENT_LABEL: reward sent - CONTRIBUTOR_REWARD_DISCORD_URL: ${{ vars.CONTRIBUTOR_REWARD_DISCORD_URL }} - CONTRIBUTOR_REWARD_MERCH_URL: ${{ vars.CONTRIBUTOR_REWARD_MERCH_URL }} - CONTRIBUTOR_REWARD_MESSAGE: ${{ vars.CONTRIBUTOR_REWARD_MESSAGE }} - with: - script: | - const { owner, repo } = context.repo; - const triggerLabel = (process.env.CONTRIBUTOR_REWARD_TRIGGER_LABEL || 'reward user').trim(); - const sentLabel = (process.env.CONTRIBUTOR_REWARD_SENT_LABEL || 'reward sent').trim(); - const discordUrl = - (process.env.CONTRIBUTOR_REWARD_DISCORD_URL || 'https://discord.tinyhumans.ai/').trim(); - const merchUrl = (process.env.CONTRIBUTOR_REWARD_MERCH_URL || '').trim(); - const customMessage = (process.env.CONTRIBUTOR_REWARD_MESSAGE || '').trim(); - - // Treat all PR/issue metadata as untrusted display-only data — this - // workflow runs under `pull_request_target` so anything coming from - // a fork (login, html_url) crosses a trust boundary. Normalize - // before interpolating into search queries or comment bodies. - // GitHub usernames are 1-39 chars, alphanumeric + hyphen. - const safeLogin = (s) => String(s || '').replace(/[^A-Za-z0-9-]/g, '').slice(0, 39); - // Only allow github.com / *.github.com (covers GitHub Enterprise). - // Strip query/fragment to defeat comment-body injection via crafted - // URLs. Return '' if the URL is unparseable or off-host. - const safeUrl = (s) => { - try { - const u = new URL(String(s || '')); - const okHost = u.hostname === 'github.com' || u.hostname.endsWith('.github.com'); - return okHost ? `${u.origin}${u.pathname}` : ''; - } catch { - return ''; - } - }; - - const normalize = value => String(value || '').trim().toLowerCase(); - const triggerLabelKey = normalize(triggerLabel); - - function markerFor(login) { - return ``; - } - - async function ensureLabel(name, color, description) { - try { - await github.rest.issues.getLabel({ owner, repo, name }); - return; - } catch (error) { - if (error.status !== 404) throw error; - } - - try { - await github.rest.issues.createLabel({ owner, repo, name, color, description }); - core.info(`Created label "${name}".`); - } catch (error) { - if (error.status !== 422) throw error; - core.info(`Label "${name}" already exists.`); - } - } - - function labeledWithTrigger() { - return normalize(context.payload.label?.name) === triggerLabelKey; - } - - function actorIsBot(target) { - return target.userType === 'Bot' || /\[bot\]$/i.test(target.login); - } - - function targetFromEvent() { - if (context.eventName === 'workflow_dispatch') { - return { bootstrapOnly: true }; - } - - if (context.eventName === 'pull_request_target') { - const pr = context.payload.pull_request; - if (!pr) return null; - - if (context.payload.action === 'closed') { - if (!pr.merged) { - core.info(`PR #${pr.number} was closed without merge; skipping reward flow.`); - return null; - } - - const login = safeLogin(pr.user?.login); - if (!login) { - core.info(`PR #${pr.number} has an unrecognized author login shape; skipping.`); - return null; - } - const htmlUrl = safeUrl(pr.html_url); - - return { - kind: 'pull request', - number: pr.number, - htmlUrl, - login, - userType: pr.user?.type, - reason: 'first merged pull request', - manualOverride: false, - requireFirstMergedPr: true, - }; - } - - if (context.payload.action === 'labeled') { - if (!labeledWithTrigger()) { - core.info(`Label "${context.payload.label?.name}" is not "${triggerLabel}"; skipping.`); - return null; - } - - const login = safeLogin(pr.user?.login); - if (!login) { - core.info(`PR #${pr.number} has an unrecognized author login shape; skipping.`); - return null; - } - const htmlUrl = safeUrl(pr.html_url); - - return { - kind: 'pull request', - number: pr.number, - htmlUrl, - login, - userType: pr.user?.type, - reason: `maintainer-applied "${triggerLabel}" label on a pull request`, - manualOverride: true, - requireFirstMergedPr: false, - }; - } - } - - if (context.eventName === 'issues') { - const issue = context.payload.issue; - if (!issue) return null; - - if (issue.pull_request) { - core.info(`Issue event is for PR #${issue.number}; pull_request_target handles PR labels.`); - return null; - } - - if (!labeledWithTrigger()) { - core.info(`Label "${context.payload.label?.name}" is not "${triggerLabel}"; skipping.`); - return null; - } - - const login = safeLogin(issue.user?.login); - if (!login) { - core.info(`Issue #${issue.number} has an unrecognized author login shape; skipping.`); - return null; - } - const htmlUrl = safeUrl(issue.html_url); - - return { - kind: 'issue', - number: issue.number, - htmlUrl, - login, - userType: issue.user?.type, - reason: `maintainer-applied "${triggerLabel}" label on an issue`, - manualOverride: true, - requireFirstMergedPr: false, - }; - } - - core.info(`Unsupported event "${context.eventName}"; skipping.`); - return null; - } - - async function isFirstMergedPullRequest(login, currentNumber) { - const query = `repo:${owner}/${repo} is:pr is:merged author:${login}`; - const response = await github.rest.search.issuesAndPullRequests({ - q: query, - per_page: 100, - }); - const previous = response.data.items.filter(item => item.number !== currentNumber); - core.info( - `Found ${previous.length} earlier merged PR(s) for ${login} while evaluating #${currentNumber}.` - ); - return previous.length === 0; - } - - async function targetAlreadyRewarded(target) { - const marker = markerFor(target.login); - const comments = await github.paginate(github.rest.issues.listComments, { - owner, - repo, - issue_number: target.number, - per_page: 100, - }); - return comments.some(comment => comment.body?.includes(marker)); - } - - async function contributorAlreadyRewardedElsewhere(target) { - const markerNeedle = `openhuman-contributor-reward:user=${normalize(target.login)}`; - const query = `repo:${owner}/${repo} "${markerNeedle}" in:comments`; - const response = await github.rest.search.issuesAndPullRequests({ - q: query, - per_page: 10, - }); - return response.data.items.some(item => item.number !== target.number); - } - - function renderCustomMessage(target) { - const replacements = { - '{user}': `@${target.login}`, - '{login}': target.login, - '{discord_url}': discordUrl, - '{merch_url}': merchUrl, - '{reason}': target.reason, - '{target_url}': target.htmlUrl, - }; - - let body = customMessage; - for (const [token, value] of Object.entries(replacements)) { - body = body.split(token).join(value); - } - return `${markerFor(target.login)}\n${body}`; - } - - function renderDefaultMessage(target) { - const merchLine = merchUrl - ? `3. Claim merch: ${merchUrl}` - : '3. For merch, ask a maintainer in Discord. Keep shipping details out of GitHub comments.'; - - return [ - markerFor(target.login), - `Thanks @${target.login} for contributing to OpenHuman.`, - '', - 'You are eligible for the contributor reward flow:', - '', - `1. Join the OpenHuman Discord: ${discordUrl}`, - `2. Share this ${target.kind} link so maintainers can verify your GitHub handle: ${target.htmlUrl}`, - merchLine, - '', - `*Reason: ${target.reason}. Automatic rewards are idempotent per GitHub user; maintainer-applied "${triggerLabel}" labels can intentionally start the flow for special cases.*`, - ].join('\n'); - } - - const target = targetFromEvent(); - - await ensureLabel(triggerLabel, 'fbca04', 'Maintainer-triggered contributor reward invite'); - await ensureLabel(sentLabel, '0e8a16', 'Contributor reward invite has been posted'); - - if (!target) { - await core.summary.addHeading('Contributor Rewards').addRaw('No eligible target for this event.').write(); - return; - } - - if (target.bootstrapOnly) { - await core.summary - .addHeading('Contributor Rewards') - .addRaw(`Labels "${triggerLabel}" and "${sentLabel}" are ready.`) - .write(); - return; - } - - if (actorIsBot(target)) { - core.info(`Skipping bot account ${target.login}.`); - return; - } - - if (target.requireFirstMergedPr) { - const firstMergedPr = await isFirstMergedPullRequest(target.login, target.number); - if (!firstMergedPr) { - core.info(`${target.login} already has a merged PR; skipping automatic reward.`); - return; - } - } - - if (await targetAlreadyRewarded(target)) { - core.info(`#${target.number} already has a reward marker for ${target.login}; skipping.`); - return; - } - - if (!target.manualOverride && (await contributorAlreadyRewardedElsewhere(target))) { - core.info(`${target.login} already has a reward marker elsewhere; skipping automatic reward.`); - return; - } - - const body = customMessage ? renderCustomMessage(target) : renderDefaultMessage(target); - - await github.rest.issues.createComment({ - owner, - repo, - issue_number: target.number, - body, - }); - await github.rest.issues.addLabels({ - owner, - repo, - issue_number: target.number, - labels: [sentLabel], - }); - - core.info(`Posted contributor reward invite for ${target.login} on #${target.number}.`); - await core.summary - .addHeading('Contributor Rewards') - .addRaw(`Posted reward invite for @${target.login} on ${target.htmlUrl}.`) - .write(); diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml deleted file mode 100644 index fc0a4c3e9..000000000 --- a/.github/workflows/deploy.yml +++ /dev/null @@ -1,688 +0,0 @@ ---- -# Deployment workflow for OpenHuman -# Handles deployment of both the Rust core (openhuman-core CLI) and the React + Tauri desktop app. -# -# This workflow: -# - Validates the environment and build prerequisites -# - Runs pre-deployment checks (TypeScript compilation, linting) -# - Resolves platform filter matrix so build jobs only run for selected platforms -# - Compiles the Rust core + CLI binaries for all requested platforms -# - Builds the React frontend + Tauri desktop bundles (macOS, Linux, Windows) -# - Stages artifacts for distribution -# - Signs and notarizes macOS bundles (production only) -# - Publishes release assets and Docker images -# -# Triggers: -# - Manual dispatch for on-demand deployments (specify environment: staging/production) -# -# ───────────────────────────────────────────────────────────────────────────── -# FIX LOG (vs. previous revision) -# ───────────────────────────────────────────────────────────────────────────── -# [F-1] CRITICAL — matrix context unavailable in job-level `if`. -# `matrix.*` is only resolved inside step-level expressions; a job-level -# `if: contains(..., matrix.platform_filter)` always evaluates against an -# empty string and silently skips every matrix leg. -# Fix: added a `resolve-matrix` job that converts the comma-separated -# input into a JSON include-list; `build-rust-core` and `build-desktop` -# now consume `needs.resolve-matrix.outputs.*` which IS available at -# job-level. Platform filtering is therefore correct and deterministic. -# -# [F-2] `pre-checks` skipped when `skip_tests=true` stalled all build jobs. -# Downstream jobs listed `pre-checks` in `needs`, so when it was -# conditionally skipped GitHub marked dependents as "not run". -# Fix: build jobs now use -# `if: needs.pre-checks.result == 'success' || -# needs.pre-checks.result == 'skipped'` -# so they proceed whether tests ran or were intentionally bypassed. -# -# [F-3] `publish-release` condition used `&&` but `build-docker` can be -# 'skipped' when docker is excluded from `deploy_platforms`. -# Fix: condition now explicitly allows 'skipped' for build-docker: -# `build-docker.result != 'failure'` -# Analogous guard added for build-rust-core and build-desktop. -# -# [F-4] Docker `type=semver` tag pattern requires a pushed git tag; on a -# `workflow_dispatch` trigger it produces an empty/no-op tag. -# Fix: replaced with explicit `type=raw,value=${{ needs.validate.outputs.version }}` -# and kept the SHA and branch tags which work on any trigger. -# -# [F-5] `actions/checkout@v5` does not exist (latest stable is v4). -# `softprops/action-gh-release@v3.0.0` does not exist (latest is v2). -# Fix: pinned all actions to their current latest major version. -# -# [F-6] Rust toolchain version was duplicated — `env.RUST_VERSION` defined at -# the top level but `dtolnay/rust-toolchain@1.93.0` hard-coded the same -# version inline, creating a drift risk. -# Fix: `dtolnay/rust-toolchain@stable` is now called with -# `toolchain: ${{ env.RUST_VERSION }}` so there is a single source of truth. -# -# [F-7] No `timeout-minutes` on long-running build jobs; a hung runner could -# block the entire workflow indefinitely. -# Fix: added sensible timeouts (60 min for Rust core, 90 min for desktop). -# ───────────────────────────────────────────────────────────────────────────── - -name: Deploy - -on: - workflow_dispatch: - inputs: - environment: - description: Deployment environment (staging | production) - required: true - type: choice - default: staging - options: - - staging - - production - skip_tests: - description: Skip pre-deployment tests (use with caution) - required: false - type: boolean - default: false - deploy_platforms: - description: | - Platforms to deploy (comma-separated). - Options: macos-arm64, macos-x64, linux-x64, linux-arm64, windows-x64, docker - required: false - type: string - default: "macos-arm64,macos-x64,linux-x64,linux-arm64,windows-x64,docker" - -permissions: - contents: write - packages: write - -concurrency: - group: deploy-${{ github.ref }}-${{ inputs.environment || 'staging' }} - cancel-in-progress: false - -env: - REGISTRY: ghcr.io - IMAGE_NAME: tinyhumansai/openhuman-core - NODE_VERSION: 24.x - RUST_VERSION: 1.83.0 # single source of truth — referenced in all toolchain steps - -jobs: - # ========================================================================= - # Phase 0: Validate deployment context - # ========================================================================= - validate: - name: Validate deployment context - runs-on: ubuntu-latest - timeout-minutes: 10 - environment: ${{ inputs.environment || 'staging' }} - outputs: - platforms: ${{ steps.platforms.outputs.list }} - ref: ${{ steps.context.outputs.ref }} - version: ${{ steps.context.outputs.version }} - short_sha: ${{ steps.context.outputs.short_sha }} - release_date: ${{ steps.context.outputs.release_date }} - steps: - - name: Checkout - uses: actions/checkout@v4 - with: - fetch-depth: 1 - - - name: Resolve deployment context - id: context - shell: bash - run: | - set -euo pipefail - - VERSION=$(node -e 'console.log(JSON.parse(require("fs").readFileSync("app/package.json")).version)') - SHORT_SHA="${GITHUB_SHA:0:12}" - RELEASE_DATE="$(date -u +'%Y-%m-%dT%H:%M:%SZ')" - - echo "ref=$GITHUB_REF_NAME" >> "$GITHUB_OUTPUT" - echo "version=$VERSION" >> "$GITHUB_OUTPUT" - echo "short_sha=$SHORT_SHA" >> "$GITHUB_OUTPUT" - echo "release_date=$RELEASE_DATE" >> "$GITHUB_OUTPUT" - - echo "[deploy] environment=${{ inputs.environment }}" - echo "[deploy] ref=${GITHUB_REF_NAME}" - echo "[deploy] version=${VERSION}" - echo "[deploy] short_sha=${SHORT_SHA}" - - - name: Parse platform filters - id: platforms - shell: bash - env: - DEPLOY_PLATFORMS: ${{ inputs.deploy_platforms }} - run: | - PLATFORMS="${DEPLOY_PLATFORMS:-macos-arm64,macos-x64,linux-x64,linux-arm64,windows-x64,docker}" - echo "list=${PLATFORMS}" >> "$GITHUB_OUTPUT" - echo "[deploy] platforms=${PLATFORMS}" - - - name: Validate prerequisites - shell: bash - env: - ENVIRONMENT: ${{ inputs.environment }} - run: | - set -euo pipefail - - case "$ENVIRONMENT" in - staging|production) - echo "[deploy] Valid environment: $ENVIRONMENT" - ;; - *) - echo "[deploy] ERROR: Invalid environment '$ENVIRONMENT'" - exit 1 - ;; - esac - - # ========================================================================= - # Phase 0b: Resolve dynamic matrix — [F-1] - # - # matrix.* context is NOT available in job-level `if` expressions; only - # step-level expressions can read it. This job converts the - # `deploy_platforms` string into explicit JSON include-arrays so that - # build-rust-core and build-desktop can use job-level `if` guards safely. - # ========================================================================= - resolve-matrix: - name: Resolve build matrix - runs-on: ubuntu-latest - timeout-minutes: 5 - needs: validate - outputs: - rust_matrix: ${{ steps.resolve.outputs.rust_matrix }} - desktop_matrix: ${{ steps.resolve.outputs.desktop_matrix }} - include_docker: ${{ steps.resolve.outputs.include_docker }} - steps: - - name: Compute include arrays - id: resolve - shell: bash - env: - PLATFORMS: ${{ needs.validate.outputs.platforms }} - run: | - set -euo pipefail - - # ── Rust core matrix ────────────────────────────────────────────── - RUST_INCLUDES="[]" - ALL_RUST='[ - {"target":"x86_64-unknown-linux-gnu", "runner":"ubuntu-24.04", "platform_filter":"linux-x64"}, - {"target":"aarch64-unknown-linux-gnu", "runner":"ubuntu-24.04-arm", "platform_filter":"linux-arm64"}, - {"target":"x86_64-pc-windows-msvc", "runner":"windows-latest", "platform_filter":"windows-x64"}, - {"target":"aarch64-apple-darwin", "runner":"macos-latest", "platform_filter":"macos-arm64"}, - {"target":"x86_64-apple-darwin", "runner":"macos-latest", "platform_filter":"macos-x64"} - ]' - RUST_INCLUDES=$(echo "$ALL_RUST" | python3 -c " - import json, sys, os - platforms = os.environ['PLATFORMS'].split(',') - items = json.load(sys.stdin) - print(json.dumps([i for i in items if i['platform_filter'] in platforms])) - ") - - # ── Desktop (Tauri) matrix ───────────────────────────────────────── - ALL_DESKTOP='[ - {"platform":"macos-latest", "args":"--target aarch64-apple-darwin", "target":"aarch64-apple-darwin", "artifact_suffix":"aarch64-apple-darwin", "platform_filter":"macos-arm64"}, - {"platform":"macos-latest", "args":"--target x86_64-apple-darwin", "target":"x86_64-apple-darwin", "artifact_suffix":"x86_64-apple-darwin", "platform_filter":"macos-x64"}, - {"platform":"ubuntu-24.04", "args":"--target x86_64-unknown-linux-gnu --bundles deb appimage","target":"x86_64-unknown-linux-gnu", "artifact_suffix":"ubuntu", "platform_filter":"linux-x64"}, - {"platform":"ubuntu-24.04-arm","args":"--target aarch64-unknown-linux-gnu --bundles deb appimage","target":"aarch64-unknown-linux-gnu", "artifact_suffix":"ubuntu-arm64", "platform_filter":"linux-arm64"}, - {"platform":"windows-latest", "args":"--target x86_64-pc-windows-msvc", "target":"x86_64-pc-windows-msvc", "artifact_suffix":"windows", "platform_filter":"windows-x64"} - ]' - DESKTOP_INCLUDES=$(echo "$ALL_DESKTOP" | python3 -c " - import json, sys, os - platforms = os.environ['PLATFORMS'].split(',') - items = json.load(sys.stdin) - print(json.dumps([i for i in items if i['platform_filter'] in platforms])) - ") - - # ── Docker flag ──────────────────────────────────────────────────── - if echo "$PLATFORMS" | grep -q "docker"; then - INCLUDE_DOCKER="true" - else - INCLUDE_DOCKER="false" - fi - - echo "rust_matrix={\"include\":${RUST_INCLUDES}}" >> "$GITHUB_OUTPUT" - echo "desktop_matrix={\"include\":${DESKTOP_INCLUDES}}" >> "$GITHUB_OUTPUT" - echo "include_docker=${INCLUDE_DOCKER}" >> "$GITHUB_OUTPUT" - - echo "[matrix] rust_includes=${RUST_INCLUDES}" - echo "[matrix] desktop_includes=${DESKTOP_INCLUDES}" - echo "[matrix] include_docker=${INCLUDE_DOCKER}" - - # ========================================================================= - # Phase 1: Pre-deployment checks (type check, lint) - # ========================================================================= - pre-checks: - name: Pre-deployment checks - runs-on: ubuntu-latest - timeout-minutes: 15 - needs: validate - if: ${{ !inputs.skip_tests }} - steps: - - name: Checkout - uses: actions/checkout@v4 - with: - fetch-depth: 1 - - - name: Setup Node.js - uses: actions/setup-node@v4 - with: - node-version: ${{ env.NODE_VERSION }} - - - name: Setup pnpm - uses: pnpm/action-setup@v4 - - - name: Install dependencies - run: pnpm install --frozen-lockfile - - - name: Type check (TypeScript) - run: pnpm compile - - - name: Lint - run: pnpm lint - - # ========================================================================= - # Phase 2: Build Rust core binaries - # ========================================================================= - build-rust-core: - name: Build Rust core (${{ matrix.target }}) - runs-on: ${{ matrix.runner }} - needs: [validate, resolve-matrix, pre-checks] - timeout-minutes: 30 # [F-7] prevent hung runners blocking the pipeline - # [F-2] proceed when pre-checks succeeded OR was intentionally skipped - if: | - fromJSON(needs.resolve-matrix.outputs.rust_matrix).include[0] != null && - (needs.pre-checks.result == 'success' || needs.pre-checks.result == 'skipped') - strategy: - fail-fast: false - matrix: ${{ fromJSON(needs.resolve-matrix.outputs.rust_matrix) }} - steps: - - name: Checkout - uses: actions/checkout@v4 - with: - fetch-depth: 1 - - - name: Install Rust # [F-6] single source of truth via env.RUST_VERSION - uses: dtolnay/rust-toolchain@stable - with: - toolchain: ${{ env.RUST_VERSION }} - targets: ${{ matrix.target }} - - - name: Cache Cargo registry - uses: actions/cache@v4 - with: - path: | - ~/.cargo/registry - ~/.cargo/git - key: ${{ runner.os }}-cargo-${{ matrix.target }}-${{ hashFiles('Cargo.lock') }} - restore-keys: | - ${{ runner.os }}-cargo-${{ matrix.target }}- - - - name: Build openhuman-core binary - shell: bash - env: - TARGET: ${{ matrix.target }} - OPENHUMAN_BUILD_SHA: ${{ needs.validate.outputs.short_sha }} - run: | - set -euo pipefail - cargo build \ - --manifest-path Cargo.toml \ - --release \ - --target "$TARGET" \ - --bin openhuman-core - echo "[build] openhuman-core compiled for $TARGET" - - - name: Stage core binary - shell: bash - env: - TARGET: ${{ matrix.target }} - run: | - set -euo pipefail - mkdir -p artifacts/core - - if [ "${{ runner.os }}" = "Windows" ]; then - BIN_NAME="openhuman-core.exe" - else - BIN_NAME="openhuman-core" - fi - - cp "target/$TARGET/release/$BIN_NAME" "artifacts/core/$BIN_NAME" - ls -lh "artifacts/core/$BIN_NAME" - - - name: Upload core binary - uses: actions/upload-artifact@v4 - with: - name: core-${{ matrix.target }} - path: artifacts/core/ - retention-days: 7 - - # ========================================================================= - # Phase 3: Build desktop bundles (Tauri + React) - # ========================================================================= - build-desktop: - name: Build desktop (${{ matrix.artifact_suffix }}) - runs-on: ${{ matrix.platform }} - needs: [validate, resolve-matrix, pre-checks] - timeout-minutes: 30 # [F-7] - environment: ${{ inputs.environment || 'staging' }} - # [F-2] same skip-guard as build-rust-core - if: | - fromJSON(needs.resolve-matrix.outputs.desktop_matrix).include[0] != null && - (needs.pre-checks.result == 'success' || needs.pre-checks.result == 'skipped') - strategy: - fail-fast: false - matrix: ${{ fromJSON(needs.resolve-matrix.outputs.desktop_matrix) }} - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - OPENHUMAN_TELEGRAM_BOT_USERNAME: ${{ vars.OPENHUMAN_TELEGRAM_BOT_USERNAME }} - VITE_TELEGRAM_BOT_USERNAME: ${{ vars.OPENHUMAN_TELEGRAM_BOT_USERNAME }} - steps: - - name: Checkout - uses: actions/checkout@v4 - with: - fetch-depth: 1 - submodules: recursive - - - name: Set Xcode version (macOS) - if: matrix.platform == 'macos-latest' - uses: maxim-lobanov/setup-xcode@v1 - with: - xcode-version: latest-stable - - - name: Setup Node.js - uses: actions/setup-node@v4 - with: - node-version: ${{ env.NODE_VERSION }} - - - name: Setup pnpm - uses: pnpm/action-setup@v4 - with: - cache: true - - - name: Install Rust # [F-6] - uses: dtolnay/rust-toolchain@stable - with: - toolchain: ${{ env.RUST_VERSION }} - targets: ${{ (matrix.platform == 'macos-latest' && 'aarch64-apple-darwin,x86_64-apple-darwin') || matrix.target }} - - - name: Install Tauri dependencies (Ubuntu) - if: startsWith(matrix.platform, 'ubuntu-') - run: | - sudo apt-get update - sudo apt-get install -y \ - libgtk-3-dev libwebkit2gtk-4.1-dev libayatana-appindicator3-dev librsvg2-dev \ - patchelf cmake libasound2-dev libxdo-dev libxtst-dev libx11-dev libxi-dev \ - libevdev-dev libssl-dev libclang-dev desktop-file-utils \ - libnss3 libnspr4 libatk1.0-0 libatk-bridge2.0-0 libcups2 libdrm2 \ - libxkbcommon0 libxcomposite1 libxdamage1 libxfixes3 libxrandr2 \ - libgbm1 libpango-1.0-0 libcairo2 libatspi2.0-0 libxshmfence1 libu2f-udev - - - name: Cache Cargo artifacts - uses: actions/cache@v4 - with: - path: | - ~/.cargo/registry - ~/.cargo/git - app/src-tauri/target - key: ${{ runner.os }}-cargo-tauri-${{ matrix.target }}-${{ hashFiles('**/Cargo.lock') }} - restore-keys: | - ${{ runner.os }}-cargo-tauri-${{ matrix.target }}- - - - name: Cache CEF distribution - uses: actions/cache@v4 - with: - path: | - ~/Library/Caches/tauri-cef - ~/.cache/tauri-cef - ~/AppData/Local/tauri-cef - key: cef-${{ matrix.target }}-${{ hashFiles('app/src-tauri/Cargo.toml') }} - restore-keys: | - cef-${{ matrix.target }}- - - - name: Install dependencies - run: pnpm install --frozen-lockfile - - - name: Build desktop application - shell: bash - working-directory: app - env: - BASE_URL: ${{ inputs.environment == 'production' && 'https://api.tinyhumans.ai/' || 'https://staging-api.tinyhumans.ai/' }} - OPENHUMAN_APP_ENV: ${{ inputs.environment || 'staging' }} - VITE_OPENHUMAN_APP_ENV: ${{ inputs.environment || 'staging' }} - VITE_BUILD_SHA: ${{ needs.validate.outputs.short_sha }} - SENTRY_RELEASE: openhuman@${{ needs.validate.outputs.version }}+${{ needs.validate.outputs.short_sha }} - SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }} - SENTRY_URL: ${{ vars.SENTRY_URL }} - SENTRY_ORG: ${{ vars.SENTRY_ORG }} - SENTRY_PROJECT: ${{ vars.SENTRY_PROJECT_REACT }} - VITE_SENTRY_DSN: ${{ vars.OPENHUMAN_REACT_SENTRY_DSN }} - TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY }} - TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY_PASSWORD }} - WITH_UPDATER: "true" - MACOSX_DEPLOYMENT_TARGET: ${{ matrix.platform == 'macos-latest' && '10.15' || '' }} - run: | - set -euo pipefail - pnpm tauri:ensure - - if [ "${{ runner.os }}" = "Linux" ]; then - NODE_OPTIONS="--max-old-space-size=8192" cargo tauri build \ - --no-bundle ${{ matrix.args }} - CEF_LIB_DIR="$(find "$HOME/.cache/tauri-cef" -name libcef.so -printf '%h\n' 2>/dev/null | head -1)" - if [ -z "$CEF_LIB_DIR" ]; then - echo "::error::libcef.so not found; cannot satisfy lib4bin ldd resolution." - exit 1 - fi - export LD_LIBRARY_PATH="$CEF_LIB_DIR${LD_LIBRARY_PATH:+:$LD_LIBRARY_PATH}" - fi - - NODE_OPTIONS="--max-old-space-size=8192" cargo tauri build ${{ matrix.args }} - echo "[build] Desktop application compiled for ${{ matrix.target }}" - - - name: Sign and notarize macOS bundle - if: matrix.platform == 'macos-latest' && inputs.environment == 'production' - shell: bash - env: - APPLE_CERTIFICATE_BASE64: ${{ secrets.APPLE_CERTIFICATE_BASE64 }} - APPLE_CERTIFICATE_PASSWORD: ${{ secrets.APPLE_CERTIFICATE_PASSWORD }} - APPLE_SIGNING_IDENTITY: ${{ secrets.APPLE_SIGNING_IDENTITY }} - APPLE_ID: ${{ secrets.APPLE_ID }} - APPLE_PASSWORD: ${{ secrets.APPLE_APP_SPECIFIC_PASSWORD }} - APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }} - run: | - set -euo pipefail - - for var in APPLE_CERTIFICATE_BASE64 APPLE_CERTIFICATE_PASSWORD APPLE_SIGNING_IDENTITY APPLE_ID APPLE_PASSWORD APPLE_TEAM_ID; do - if [ -z "${!var}" ]; then - echo "::error::Missing macOS signing secret: $var" - exit 1 - fi - done - - CERT_PATH="${RUNNER_TEMP}/apple_cert.p12" - echo "$APPLE_CERTIFICATE_BASE64" | base64 --decode > "$CERT_PATH" - - KEYCHAIN_PATH="${RUNNER_TEMP}/build.keychain" - security create-keychain -p "$APPLE_CERTIFICATE_PASSWORD" "$KEYCHAIN_PATH" - security default-keychain -s "$KEYCHAIN_PATH" - security unlock-keychain -p "$APPLE_CERTIFICATE_PASSWORD" "$KEYCHAIN_PATH" - security import "$CERT_PATH" -P "$APPLE_CERTIFICATE_PASSWORD" -A -t cert -f pkcs12 -k "$KEYCHAIN_PATH" - security set-key-partition-list -S apple-tool:,apple:,codesign: -s -k "$APPLE_CERTIFICATE_PASSWORD" "$KEYCHAIN_PATH" - - APP_BUNDLE="$(find app/src-tauri/target/${{ matrix.target }}/release/bundle -name '*.app' -type d | head -1)" - - if [ -z "$APP_BUNDLE" ]; then - echo "::warning::No .app bundle found to notarize" - else - echo "[deploy] Signing $APP_BUNDLE" - codesign -s "$APPLE_SIGNING_IDENTITY" --deep --force --verify --verbose "$APP_BUNDLE" - - DMG_FILE="app/src-tauri/target/${{ matrix.target }}/release/bundle/dmg/openhuman.dmg" - if [ -f "$DMG_FILE" ]; then - echo "[deploy] Notarizing $DMG_FILE" - xcrun notarytool submit "$DMG_FILE" \ - --apple-id "$APPLE_ID" \ - --password "$APPLE_PASSWORD" \ - --team-id "$APPLE_TEAM_ID" \ - --wait || echo "::warning::Notarization submitted (may require additional time)" - fi - fi - - security delete-keychain "$KEYCHAIN_PATH" - - - name: Stage desktop artifacts - shell: bash - env: - TARGET: ${{ matrix.target }} - run: | - set -euo pipefail - mkdir -p artifacts/desktop - - if [ -d "app/src-tauri/target/$TARGET/release/bundle" ]; then - cp -r app/src-tauri/target/$TARGET/release/bundle/* artifacts/desktop/ 2>/dev/null || true - fi - - find artifacts/desktop -type f -exec ls -lh {} \; - - - name: Upload desktop artifacts - uses: actions/upload-artifact@v4 - with: - name: desktop-${{ matrix.artifact_suffix }} - path: artifacts/desktop/ - retention-days: 7 - - # ========================================================================= - # Phase 4: Build and push Docker image - # ========================================================================= - build-docker: - name: Build and push Docker image - runs-on: ubuntu-latest - needs: [validate, resolve-matrix, pre-checks] - timeout-minutes: 30 # [F-7] - environment: ${{ inputs.environment || 'staging' }} - # [F-2] skip guard; also gated by the docker flag from resolve-matrix - if: | - needs.resolve-matrix.outputs.include_docker == 'true' && - (needs.pre-checks.result == 'success' || needs.pre-checks.result == 'skipped') - steps: - - name: Checkout - uses: actions/checkout@v4 - with: - ref: ${{ github.ref }} - fetch-depth: 1 - - - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v3 - - - name: Log in to GitHub Container Registry - uses: docker/login-action@v3 - with: - registry: ${{ env.REGISTRY }} - username: ${{ github.actor }} - password: ${{ secrets.GITHUB_TOKEN }} - - - name: Extract metadata # [F-4] replaced semver tag with explicit version raw tag - id: meta - uses: docker/metadata-action@v5 - with: - images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} - tags: | - type=raw,value=${{ needs.validate.outputs.version }} - type=raw,value=${{ needs.validate.outputs.version }}-${{ needs.validate.outputs.short_sha }} - type=raw,value=${{ inputs.environment || 'staging' }}-${{ needs.validate.outputs.short_sha }} - type=ref,event=branch - - - name: Build and push Docker image - uses: docker/build-push-action@v5 - with: - context: . - push: ${{ github.event_name != 'pull_request' }} - tags: ${{ steps.meta.outputs.tags }} - labels: ${{ steps.meta.outputs.labels }} - cache-from: type=gha - cache-to: type=gha,mode=max - - # ========================================================================= - # Phase 5: Publish release assets - # ========================================================================= - publish-release: - name: Publish release assets - runs-on: ubuntu-latest - timeout-minutes: 15 - needs: [validate, build-rust-core, build-desktop, build-docker] - # [F-3] build-docker / build-rust-core / build-desktop may be 'skipped' - # when those platform groups were excluded from deploy_platforms — treat - # 'skipped' as acceptable, only block on an explicit 'failure'. - if: | - always() && - needs.build-rust-core.result != 'failure' && - needs.build-desktop.result != 'failure' && - needs.build-docker.result != 'failure' - environment: ${{ inputs.environment || 'staging' }} - steps: - - name: Checkout - uses: actions/checkout@v4 - - - name: Download all artifacts - uses: actions/download-artifact@v4 - with: - path: dist/ - - - name: Create checksum manifest - shell: bash - run: | - set -euo pipefail - cd dist/ - - find . -type f \( -name "*.exe" -o -name "*.dmg" -o -name "*.deb" -o -name "*.AppImage" -o -name "*.tar.gz" -o -name "openhuman-core*" \) | while read f; do - sha256sum "$f" >> CHECKSUMS.txt - done - - echo "::notice::Checksums generated" - cat CHECKSUMS.txt || echo "(no artifacts found)" - - - name: Create GitHub Release # [F-5] correct release action version - if: inputs.environment == 'production' - uses: softprops/action-gh-release@v2 - with: - tag_name: v${{ needs.validate.outputs.version }} - name: OpenHuman v${{ needs.validate.outputs.version }} - body: | - # OpenHuman v${{ needs.validate.outputs.version }} - - **Build Info:** - - Commit: ${{ github.sha }} - - Environment: ${{ inputs.environment }} - - Build Date: ${{ needs.validate.outputs.release_date }} - - **Artifacts:** - - Desktop applications (macOS, Linux, Windows) - - CLI binaries (openhuman-core) - - Docker image: `${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ needs.validate.outputs.version }}` - - **Checksums:** See attached CHECKSUMS.txt file. - draft: false - prerelease: false - files: dist/CHECKSUMS.txt - - - name: Summary - shell: bash - run: | - echo "## Release Summary" >> $GITHUB_STEP_SUMMARY - echo "" >> $GITHUB_STEP_SUMMARY - echo "| Field | Value |" >> $GITHUB_STEP_SUMMARY - echo "| -------- | ----- |" >> $GITHUB_STEP_SUMMARY - echo "| Environment | ${{ inputs.environment }} |" >> $GITHUB_STEP_SUMMARY - echo "| Version | ${{ needs.validate.outputs.version }} |" >> $GITHUB_STEP_SUMMARY - echo "| Build SHA | ${{ needs.validate.outputs.short_sha }} |" >> $GITHUB_STEP_SUMMARY - echo "| Build Date | ${{ needs.validate.outputs.release_date }} |" >> $GITHUB_STEP_SUMMARY - echo "| Platforms | ${{ needs.validate.outputs.platforms }} |" >> $GITHUB_STEP_SUMMARY - - # ========================================================================= - # Phase 6: Verification - # ========================================================================= - verify: - name: Verify deployment - runs-on: ubuntu-latest - timeout-minutes: 10 - needs: [validate, publish-release] - if: always() - steps: - - name: Check status - shell: bash - run: | - echo "[deploy] Deployment complete" - echo " Version: ${{ needs.validate.outputs.version }}" - echo " Platforms: ${{ needs.validate.outputs.platforms }}" - echo " Release: ${{ needs.publish-release.result }}" diff --git a/.github/workflows/uptime-monitor.yml b/.github/workflows/uptime-monitor.yml deleted file mode 100644 index 990fd4132..000000000 --- a/.github/workflows/uptime-monitor.yml +++ /dev/null @@ -1,128 +0,0 @@ -name: Uptime Monitor - -on: - schedule: - - cron: '*/5 * * * *' # Run every 5 minutes - workflow_dispatch: # Allow manual trigger - -concurrency: - group: uptime-monitor - cancel-in-progress: false - -permissions: - issues: write - -jobs: - check-uptime: - name: Check Backend Health - runs-on: ubuntu-latest - timeout-minutes: 5 - steps: - - name: Uptime Check Script - uses: actions/github-script@v7 - env: - ALERT_WEBHOOK_URL: ${{ secrets.ALERT_WEBHOOK_URL }} - with: - script: | - const endpoints = [ - { env: "Production", url: "https://api.tinyhumans.ai/health", timeout: 10000 }, - { env: "Staging", url: "https://staging-api.tinyhumans.ai/health", timeout: 15000 } - ]; - - const maxRetries = 3; - const retryDelay = 5000; - const issueTitle = "CRITICAL: Backend Outage Detected"; - - async function fetchWithRetry(url, timeout, retries) { - for (let i = 0; i <= retries; i++) { - try { - const controller = new AbortController(); - const id = setTimeout(() => controller.abort(), timeout); - const response = await fetch(url, { signal: controller.signal }); - clearTimeout(id); - - if (response.status === 200) return { ok: true, status: response.status }; - if (i === retries) return { ok: false, status: response.status }; - } catch (error) { - if (i === retries) return { ok: false, error: error.message }; - } - await new Promise(resolve => setTimeout(resolve, retryDelay)); - } - } - - let allHealthy = true; - let failureDetails = []; - - for (const ep of endpoints) { - const result = await fetchWithRetry(ep.url, ep.timeout, maxRetries); - if (!result.ok) { - allHealthy = false; - failureDetails.push(`- **${ep.env}**: ${ep.url} (Status: ${result.status || result.error}) at ${new Date().toISOString()}`); - } - } - - const { data: issues } = await github.rest.issues.listForRepo({ - owner: context.repo.owner, - repo: context.repo.repo, - state: 'open', - creator: 'github-actions[bot]' - }); - const openIssue = issues.find(i => i.title === issueTitle); - - const webhookUrl = process.env.ALERT_WEBHOOK_URL; - async function sendWebhook(message) { - if (webhookUrl) { - try { - // Discord uses { content } and Slack uses { text }; detect by URL - const payload = webhookUrl.includes('discord') - ? { content: message } - : { text: message }; - await fetch(webhookUrl, { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify(payload) - }); - } catch (e) { - console.error("Failed to send webhook", e); - } - } - } - - if (allHealthy) { - if (openIssue) { - const recoveryMsg = `✅ **RESOLVED**: All backend endpoints are now healthy and reachable.\n\nMonitors reported recovery at ${new Date().toISOString()}.`; - await github.rest.issues.createComment({ - owner: context.repo.owner, - repo: context.repo.repo, - issue_number: openIssue.number, - body: recoveryMsg - }); - await github.rest.issues.update({ - owner: context.repo.owner, - repo: context.repo.repo, - issue_number: openIssue.number, - state: 'closed' - }); - await sendWebhook(recoveryMsg); - console.log("Services recovered. Issue closed."); - } else { - console.log("All services healthy."); - } - } else { - const issueBody = `The automated uptime monitor detected an outage in the backend.\n\n### Failing Endpoints:\n${failureDetails.join('\n')}\n\nPlease check the logs and follow the runbook in \`docs/OPERATIONS.md\`.`; - - if (!openIssue) { - await github.rest.issues.create({ - owner: context.repo.owner, - repo: context.repo.repo, - title: issueTitle, - body: issueBody, - labels: ['bug', 'critical', 'ops'] - }); - await sendWebhook(`🚨 **${issueTitle}**\n${issueBody}`); - core.setFailed("Backend health check failed."); - } else { - console.log("Issue already exists, skipping duplicate creation."); - core.setFailed("Backend health check failed (ongoing)."); - } - } diff --git a/.github/workflows/weekly-code-review.yml b/.github/workflows/weekly-code-review.yml deleted file mode 100644 index 5942f251d..000000000 --- a/.github/workflows/weekly-code-review.yml +++ /dev/null @@ -1,128 +0,0 @@ -name: Weekly Code Review - -# Scheduled aggregation of slow-moving code-health signals that per-PR CI -# does not catch: unused code (knip), Rust advisories (cargo-audit), and -# TODO/FIXME backlog. The run opens (or updates) a tracking issue with the -# report and uploads the raw outputs as an artifact. -# -# Runbook: docs/WEEKLY-CODE-REVIEW.md - -on: - schedule: - # Mondays, 06:00 UTC. Early enough to land before US / EU maintainers - # start the week. Override via workflow_dispatch if needed. - - cron: "0 6 * * 1" - workflow_dispatch: - -permissions: - contents: read - issues: write - -concurrency: - group: weekly-code-review - cancel-in-progress: false - -jobs: - weekly-review: - name: Aggregate weekly signals - runs-on: ubuntu-22.04 - container: - image: ghcr.io/tinyhumansai/openhuman_ci:rust-1.93.0 - timeout-minutes: 30 - steps: - - name: Checkout repository - uses: actions/checkout@v5 - with: - fetch-depth: 1 - - - name: Cache pnpm store - uses: actions/cache@v5 - with: - path: ~/.local/share/pnpm/store - key: pnpm-store-${{ runner.os }}-${{ hashFiles('pnpm-lock.yaml') }} - restore-keys: | - pnpm-store-${{ runner.os }}- - - - name: Install JS dependencies - run: pnpm install --frozen-lockfile - - - name: Cache cargo-audit binary - id: cache-cargo-audit - uses: actions/cache@v5 - with: - # Image sets CARGO_HOME=/usr/local/cargo, so cargo install drops the - # binary there — not in $HOME/.cargo/bin. - path: /usr/local/cargo/bin/cargo-audit - key: cargo-audit-${{ runner.os }}-v1 - - - name: Install cargo-audit - if: steps.cache-cargo-audit.outputs.cache-hit != 'true' - run: cargo install cargo-audit --locked - - - name: Run weekly code-review aggregator - run: bash scripts/weekly-code-review.sh weekly-code-review-out - - - name: Upload report artifact - uses: actions/upload-artifact@v5 - with: - name: weekly-code-review-${{ github.run_id }} - path: weekly-code-review-out - retention-days: 90 - - - name: Open or update tracking issue - uses: actions/github-script@v8 - with: - script: | - const fs = require('fs'); - const body = fs.readFileSync('weekly-code-review-out/report.md', 'utf8'); - const today = new Date().toISOString().slice(0, 10); - const title = `[Automated] Weekly code-review report — ${today}`; - const label = 'weekly-code-review'; - - // Ensure the tracking label exists (idempotent). - try { - await github.rest.issues.getLabel({ ...context.repo, name: label }); - } catch (err) { - if (err.status === 404) { - await github.rest.issues.createLabel({ - ...context.repo, - name: label, - color: 'c5def5', - description: 'Automated weekly code-review report', - }); - } else { - throw err; - } - } - - // Close previous open report(s) so only the latest stays active. - const previous = await github.paginate(github.rest.issues.listForRepo, { - ...context.repo, - state: 'open', - labels: label, - per_page: 50, - }); - for (const prev of previous) { - await github.rest.issues.createComment({ - ...context.repo, - issue_number: prev.number, - body: `Superseded by the ${today} report.`, - }); - await github.rest.issues.update({ - ...context.repo, - issue_number: prev.number, - state: 'closed', - state_reason: 'completed', - }); - } - - // Open a fresh issue for this week so maintainers triage on a - // predictable cadence instead of watching a growing thread. - const runUrl = `${process.env.GITHUB_SERVER_URL}/${context.repo.owner}/${context.repo.repo}/actions/runs/${context.runId}`; - const footer = `\n---\n_Run log: ${runUrl}_`; - await github.rest.issues.create({ - ...context.repo, - title, - body: body + footer, - labels: [label], - });