mirror of
https://github.com/tinyhumansai/openhuman.git
synced 2026-07-30 23:14:37 +00:00
feat(docker): Dockerfile, cloud server support, and parallel release pipeline (#174)
* added doceker build * added docker files * feat(ci): add Docker build phase to release pipeline and .dockerignore Restructure release.yml into parallel build phases: build-desktop (matrix) and build-docker run concurrently after create-release. Docker image is pushed to GHCR and pull instructions are appended to release notes. publish-release now gates on both phases succeeding. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * style: apply cargo fmt to jsonrpc host binding Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix(review): address PR review findings - .env.example: comment out OPENHUMAN_CORE_HOST so Docker's 0.0.0.0 default isn't overridden when users copy the example file - cli.rs: add --host to print_general_help() usage line for consistency - jsonrpc.rs: use tuple bind (host, port) for IPv6 support, add debug logging with source tracking (CLI/env/default) before bind - release.yml: push only staging tag in build-docker, promote to versioned + latest in publish-release after all builds succeed; cleanup-failed-release deletes the staging image on failure Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.6
parent
f5b66de2f1
commit
6f8b5d6c9f
+140
-147
@@ -14,12 +14,38 @@ on:
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
packages: write
|
||||
|
||||
concurrency:
|
||||
group: release-main
|
||||
cancel-in-progress: false
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Job dependency graph
|
||||
#
|
||||
# prepare-release
|
||||
# │
|
||||
# ├─── create-release
|
||||
# │ │
|
||||
# │ ┌────┴────────────┐
|
||||
# │ │ │
|
||||
# │ build-desktop build-docker
|
||||
# │ (matrix: macOS, (GHCR image)
|
||||
# │ linux, windows)
|
||||
# │ │ │
|
||||
# │ └────┬────────────┘
|
||||
# │ │
|
||||
# │ publish-release
|
||||
# │ │
|
||||
# │ notify-discord
|
||||
# │
|
||||
# └─── cleanup-failed-release (on failure)
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
jobs:
|
||||
# =========================================================================
|
||||
# Phase 1: Version bump, commit, tag
|
||||
# =========================================================================
|
||||
prepare-release:
|
||||
name: Prepare release commit and tag
|
||||
runs-on: ubuntu-latest
|
||||
@@ -156,6 +182,9 @@ jobs:
|
||||
|
||||
echo "sha=$(git rev-parse HEAD)" >> "$GITHUB_OUTPUT"
|
||||
|
||||
# =========================================================================
|
||||
# Phase 2: Create draft GitHub release
|
||||
# =========================================================================
|
||||
create-release:
|
||||
name: Create GitHub release
|
||||
runs-on: ubuntu-latest
|
||||
@@ -200,8 +229,11 @@ jobs:
|
||||
core.setOutput('release_id', String(release.data.id));
|
||||
core.setOutput('upload_url', release.data.upload_url);
|
||||
|
||||
build-artifacts:
|
||||
name: Build and upload artifacts
|
||||
# =========================================================================
|
||||
# Phase 3a: Build desktop artifacts (parallel matrix)
|
||||
# =========================================================================
|
||||
build-desktop:
|
||||
name: "Desktop: ${{ matrix.settings.artifact_suffix }}"
|
||||
needs: [prepare-release, create-release]
|
||||
runs-on: ${{ matrix.settings.platform }}
|
||||
environment: Production
|
||||
@@ -727,12 +759,60 @@ jobs:
|
||||
path: |
|
||||
${{ steps.cli-paths.outputs.cli_path }}
|
||||
|
||||
# =========================================================================
|
||||
# Phase 3b: Build & push Docker image (runs parallel with build-desktop)
|
||||
# =========================================================================
|
||||
build-docker:
|
||||
name: "Docker: build and push"
|
||||
needs: [prepare-release, create-release]
|
||||
runs-on: ubuntu-latest
|
||||
environment: Production
|
||||
env:
|
||||
REGISTRY: ghcr.io
|
||||
IMAGE_NAME: tinyhumansai/openhuman-core
|
||||
steps:
|
||||
- name: Checkout tag
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
ref: ${{ needs.prepare-release.outputs.tag }}
|
||||
fetch-depth: 1
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
|
||||
- name: Log in to GHCR
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
registry: ${{ env.REGISTRY }}
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Build and push staging tag
|
||||
uses: docker/build-push-action@v6
|
||||
with:
|
||||
context: .
|
||||
file: Dockerfile
|
||||
push: true
|
||||
tags: |
|
||||
${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:staging-${{ needs.prepare-release.outputs.tag }}
|
||||
cache-from: type=gha
|
||||
cache-to: type=gha,mode=max
|
||||
labels: |
|
||||
org.opencontainers.image.title=openhuman-core
|
||||
org.opencontainers.image.description=OpenHuman core JSON-RPC server
|
||||
org.opencontainers.image.version=${{ needs.prepare-release.outputs.version }}
|
||||
org.opencontainers.image.source=https://github.com/tinyhumansai/openhuman
|
||||
org.opencontainers.image.revision=${{ needs.prepare-release.outputs.sha }}
|
||||
|
||||
# =========================================================================
|
||||
# Phase 4: Publish the draft release (waits for ALL build phases)
|
||||
# =========================================================================
|
||||
publish-release:
|
||||
name: Publish draft release
|
||||
runs-on: ubuntu-latest
|
||||
environment: Production
|
||||
needs: [create-release, build-artifacts]
|
||||
if: needs.build-artifacts.result == 'success'
|
||||
needs: [prepare-release, create-release, build-desktop, build-docker]
|
||||
if: needs.build-desktop.result == 'success' && needs.build-docker.result == 'success'
|
||||
steps:
|
||||
- name: Validate required installer assets exist
|
||||
uses: actions/github-script@v7
|
||||
@@ -764,6 +844,33 @@ jobs:
|
||||
}
|
||||
core.info('All required installer assets are present.');
|
||||
|
||||
- name: Promote Docker image from staging to release tags
|
||||
env:
|
||||
REGISTRY: ghcr.io
|
||||
IMAGE_NAME: tinyhumansai/openhuman-core
|
||||
TAG: ${{ needs.prepare-release.outputs.tag }}
|
||||
run: |
|
||||
# Log in to GHCR
|
||||
echo "${{ secrets.GITHUB_TOKEN }}" | docker login ghcr.io -u "${{ github.actor }}" --password-stdin
|
||||
|
||||
STAGING="${REGISTRY}/${IMAGE_NAME}:staging-${TAG}"
|
||||
VERSIONED="${REGISTRY}/${IMAGE_NAME}:${TAG}"
|
||||
LATEST="${REGISTRY}/${IMAGE_NAME}:latest"
|
||||
|
||||
echo "Promoting ${STAGING} → ${VERSIONED}, ${LATEST}"
|
||||
docker buildx imagetools create --tag "${VERSIONED}" --tag "${LATEST}" "${STAGING}"
|
||||
|
||||
- name: Append Docker pull instructions to release notes
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
TAG: ${{ needs.prepare-release.outputs.tag }}
|
||||
IMAGE_NAME: tinyhumansai/openhuman-core
|
||||
run: |
|
||||
gh release view "${TAG}" --repo tinyhumansai/openhuman --json body -q .body > /tmp/body.md
|
||||
printf '\n---\n\n### Docker\n\n```\ndocker pull ghcr.io/%s:%s\ndocker run -p 7788:7788 --env-file .env ghcr.io/%s:%s\n```\n' \
|
||||
"${IMAGE_NAME}" "${TAG}" "${IMAGE_NAME}" "${TAG}" >> /tmp/body.md
|
||||
gh release edit "${TAG}" --repo tinyhumansai/openhuman --notes-file /tmp/body.md
|
||||
|
||||
- name: Publish release
|
||||
uses: actions/github-script@v7
|
||||
with:
|
||||
@@ -777,153 +884,19 @@ jobs:
|
||||
});
|
||||
core.info(`Published release ${releaseId}`);
|
||||
|
||||
# notify-discord:
|
||||
# name: Notify Discord about release
|
||||
# runs-on: ubuntu-latest
|
||||
# environment: Production
|
||||
# needs: [prepare-release, create-release, build-artifacts, publish-release]
|
||||
# if: needs.publish-release.result == 'success' && needs.build-artifacts.result == 'success'
|
||||
# steps:
|
||||
# - name: Post release notification to Discord
|
||||
# continue-on-error: true
|
||||
# uses: actions/github-script@v7
|
||||
# env:
|
||||
# DISCORD_RELEASE_WEBHOOK_URL: ${{ secrets.DISCORD_RELEASE_WEBHOOK_URL }}
|
||||
# RELEASE_ID: ${{ needs.create-release.outputs.release_id }}
|
||||
# RELEASE_TAG: ${{ needs.prepare-release.outputs.tag }}
|
||||
# with:
|
||||
# script: |
|
||||
# const webhookUrl = process.env.DISCORD_RELEASE_WEBHOOK_URL;
|
||||
# const releaseId = Number(process.env.RELEASE_ID);
|
||||
# const releaseTag = process.env.RELEASE_TAG;
|
||||
|
||||
# if (!webhookUrl) {
|
||||
# core.warning('DISCORD_RELEASE_WEBHOOK_URL is not configured; skipping Discord release notification.');
|
||||
# return;
|
||||
# }
|
||||
|
||||
# if (!Number.isFinite(releaseId) || releaseId <= 0) {
|
||||
# core.warning(`Invalid release id "${process.env.RELEASE_ID}"; skipping Discord release notification.`);
|
||||
# return;
|
||||
# }
|
||||
|
||||
# const { owner, repo } = context.repo;
|
||||
# const maxDescriptionLength = 3500;
|
||||
# const maxAssetLines = 8;
|
||||
|
||||
# function truncate(text, maxLength) {
|
||||
# if (!text) return '';
|
||||
# if (text.length <= maxLength) return text;
|
||||
# return `${text.slice(0, maxLength - 3)}...`;
|
||||
# }
|
||||
|
||||
# function stripMarkdownHeadings(text) {
|
||||
# return text
|
||||
# .replace(/^#{1,6}\s+/gm, '')
|
||||
# .replace(/\r/g, '')
|
||||
# .trim();
|
||||
# }
|
||||
|
||||
# const { data: release } = await github.rest.repos.getRelease({
|
||||
# owner,
|
||||
# repo,
|
||||
# release_id: releaseId,
|
||||
# });
|
||||
|
||||
# const releases = await github.paginate(github.rest.repos.listReleases, {
|
||||
# owner,
|
||||
# repo,
|
||||
# per_page: 100,
|
||||
# });
|
||||
# const sortedPublished = releases
|
||||
# .filter((item) => !item.draft)
|
||||
# .sort((a, b) => new Date(b.published_at || b.created_at || 0) - new Date(a.published_at || a.created_at || 0));
|
||||
# const currentIndex = sortedPublished.findIndex((item) => item.id === release.id);
|
||||
# const previousRelease = currentIndex >= 0 ? sortedPublished[currentIndex + 1] : null;
|
||||
# const compareUrl = previousRelease
|
||||
# ? `https://github.com/${owner}/${repo}/compare/${previousRelease.tag_name}...${release.tag_name}`
|
||||
# : null;
|
||||
|
||||
# const releaseTitle = release.name || release.tag_name || releaseTag;
|
||||
# const bodyText = stripMarkdownHeadings(release.body || '');
|
||||
# const summary = bodyText
|
||||
# ? truncate(bodyText, maxDescriptionLength)
|
||||
# : 'GitHub release published. See the release page for full notes.';
|
||||
|
||||
# const assetLines = (release.assets || [])
|
||||
# .slice(0, maxAssetLines)
|
||||
# .map((asset) => `- [${asset.name}](${asset.browser_download_url})`);
|
||||
# const extraAssetCount = Math.max((release.assets || []).length - maxAssetLines, 0);
|
||||
# if (extraAssetCount > 0) {
|
||||
# assetLines.push(`- ${extraAssetCount} more asset(s) available on the release page`);
|
||||
# }
|
||||
|
||||
# const fields = [
|
||||
# {
|
||||
# name: 'Version',
|
||||
# value: `\`${release.tag_name || releaseTag}\``,
|
||||
# inline: true,
|
||||
# },
|
||||
# {
|
||||
# name: 'Release',
|
||||
# value: `[Open on GitHub](${release.html_url})`,
|
||||
# inline: true,
|
||||
# },
|
||||
# ];
|
||||
|
||||
# if (compareUrl) {
|
||||
# fields.push({
|
||||
# name: 'Compare',
|
||||
# value: `[${previousRelease.tag_name}...${release.tag_name}](${compareUrl})`,
|
||||
# inline: true,
|
||||
# });
|
||||
# }
|
||||
|
||||
# if (assetLines.length > 0) {
|
||||
# fields.push({
|
||||
# name: 'Artifacts',
|
||||
# value: assetLines.join('\n'),
|
||||
# inline: false,
|
||||
# });
|
||||
# }
|
||||
|
||||
# const payload = {
|
||||
# content: `New GitHub release published for \`${owner}/${repo}\`: **${releaseTitle}**`,
|
||||
# embeds: [
|
||||
# {
|
||||
# title: releaseTitle,
|
||||
# url: release.html_url,
|
||||
# description: summary,
|
||||
# color: release.prerelease ? 0xf0ad4e : 0x2ecc71,
|
||||
# fields,
|
||||
# footer: {
|
||||
# text: release.prerelease ? 'Pre-release' : 'Published release',
|
||||
# },
|
||||
# timestamp: release.published_at || new Date().toISOString(),
|
||||
# },
|
||||
# ],
|
||||
# };
|
||||
|
||||
# const response = await fetch(webhookUrl, {
|
||||
# method: 'POST',
|
||||
# headers: { 'Content-Type': 'application/json' },
|
||||
# body: JSON.stringify(payload),
|
||||
# });
|
||||
|
||||
# if (!response.ok) {
|
||||
# const body = await response.text();
|
||||
# core.warning(`Discord notification failed: HTTP ${response.status} ${body.slice(0, 300)}`);
|
||||
# return;
|
||||
# }
|
||||
|
||||
# core.info(`Posted Discord release notification for ${release.tag_name || releaseTag}.`);
|
||||
|
||||
# =========================================================================
|
||||
# Cleanup: remove draft release + tag if ANY build phase failed
|
||||
# =========================================================================
|
||||
cleanup-failed-release:
|
||||
name: Remove release and tag if build failed
|
||||
runs-on: ubuntu-latest
|
||||
environment: Production
|
||||
needs: [prepare-release, create-release, build-artifacts]
|
||||
if: always() && needs.create-release.result == 'success' && (needs.build-artifacts.result == 'failure' || needs.build-artifacts.result == 'cancelled')
|
||||
needs: [prepare-release, create-release, build-desktop, build-docker]
|
||||
if: >-
|
||||
always()
|
||||
&& needs.create-release.result == 'success'
|
||||
&& (needs.build-desktop.result == 'failure' || needs.build-desktop.result == 'cancelled'
|
||||
|| needs.build-docker.result == 'failure' || needs.build-docker.result == 'cancelled')
|
||||
steps:
|
||||
- name: Delete GitHub release
|
||||
uses: actions/github-script@v7
|
||||
@@ -963,3 +936,23 @@ jobs:
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
- name: Delete staging Docker image
|
||||
continue-on-error: true
|
||||
env:
|
||||
TAG: ${{ needs.prepare-release.outputs.tag }}
|
||||
run: |
|
||||
PACKAGE="openhuman-core"
|
||||
STAGING_TAG="staging-${TAG}"
|
||||
echo "Attempting to delete staging Docker tag: ${STAGING_TAG}"
|
||||
# List package versions and find the staging tag
|
||||
VERSION_ID="$(gh api \
|
||||
-H "Accept: application/vnd.github+json" \
|
||||
"/orgs/tinyhumansai/packages/container/${PACKAGE}/versions" \
|
||||
--paginate --jq ".[] | select(.metadata.container.tags[] == \"${STAGING_TAG}\") | .id" 2>/dev/null | head -1)"
|
||||
if [ -n "$VERSION_ID" ]; then
|
||||
gh api -X DELETE "/orgs/tinyhumansai/packages/container/${PACKAGE}/versions/${VERSION_ID}" || true
|
||||
echo "Deleted staging image version ${VERSION_ID}"
|
||||
else
|
||||
echo "Staging tag ${STAGING_TAG} not found or already deleted"
|
||||
fi
|
||||
|
||||
Reference in New Issue
Block a user