Merge branch 'doc/add-auto-publish' into 'master'

feat: add CI for automatic doc deployment

See merge request ae_group/esp-claw!64
This commit is contained in:
Liu Ruichao
2026-04-22 16:41:46 +08:00
30 changed files with 450 additions and 407 deletions
+3 -9
View File
@@ -1,15 +1,8 @@
spec:
inputs:
force_push_to_github:
type: boolean
default: false
---
stages:
- pre-check
- build
- deploy
- deploy-github
workflow:
rules:
@@ -28,10 +21,11 @@ workflow:
variables:
IOT_SOLUTION_PATH: "$CI_PROJECT_DIR"
CI_FORCE_PUSH_TO_GITHUB: "$[[ inputs.force_push_to_github ]]"
include:
- '.gitlab/ci/rules.yml'
- '.gitlab/ci/pre-check.yml'
- '.gitlab/ci/build.yml'
- '.gitlab/ci/build-doc.yml'
- '.gitlab/ci/deploy.yml'
- '.gitlab/ci/deploy-github.yml'
+26
View File
@@ -0,0 +1,26 @@
build_doc:
stage: build
tags:
- build
image: node:24
extends:
- .rules:build:doc
needs:
- job: build_application_basic_demo
artifacts: true
optional: true
script:
- apt update && apt install -y curl
- curl -fsSL https://d2lang.com/install.sh | sh -s --
- npm install -g pnpm
- cd docs
- pnpm install
- pnpm run generate-firmware-json ../merged_binary
- pnpm run build
artifacts:
when: always
paths:
- docs/public/**/*
- docs/dist/**/*
- docs/src/flash-tool/firmware.json
expire_in: 1 week
+24
View File
@@ -0,0 +1,24 @@
push_to_github:
stage: deploy-github
tags:
- build
image: alpine:latest
variables:
GIT_STRATEGY: clone
GIT_DEPTH: "0"
extends:
- .rules:push:github
script:
- apk add --no-cache git openssh-client curl
- mkdir -p ~/.ssh
- chmod 700 ~/.ssh
- printf '%s' "$GH_PUSH_KEY" | base64 -d > ~/.ssh/id_rsa
- chmod 600 ~/.ssh/id_rsa
- ssh-keyscan -H github.com >> ~/.ssh/known_hosts 2>/dev/null || true
- git remote remove github 2>/dev/null || true
- git remote add github "$GH_REMOTE_URL"
- git push github "refs/heads/master:refs/heads/master"
- |
for tag in $(git tag --merged refs/heads/master); do
git push github "refs/tags/$tag:refs/tags/$tag"
done
+21 -21
View File
@@ -1,35 +1,35 @@
collect_firmware_manifest:
extends:
- .rules:build:application_basic_demo
deploy_website:
stage: deploy
tags:
- build
image: python:3.13-slim
allow_failure: true
image: node:24-alpine
variables:
GIT_STRATEGY: fetch
environment:
name: $DEPLOY_ENV_NAME
url: $DEPLOY_ENV_URL
deployment_tier: $DEPLOY_ENV_TIER
rules:
- if: '$CI_COMMIT_REF_NAME == "master" && $CI_PIPELINE_SOURCE == "push"'
- if: '$CI_COMMIT_REF_NAME == "master" && $CI_COMMIT_TAG == null && $CI_PIPELINE_SOURCE == "push"'
when: on_success
variables:
DEPLOY_FIRMWARE: "1"
- if: '$CI_COMMIT_REF_NAME != "master" && $CI_PIPELINE_SOURCE == "merge_request_event"'
DEPLOY_ENV_NAME: docs-production
DEPLOY_ENV_URL: https://esp-claw.com
DEPLOY_ENV_TIER: production
DEPLOY_BRANCH: master
- if: '$CI_PIPELINE_SOURCE == "merge_request_event"'
changes:
- "docs/**/*"
when: on_success
variables:
DEPLOY_FIRMWARE: "0"
DEPLOY_ENV_NAME: docs-preview/$CI_COMMIT_REF_SLUG
DEPLOY_ENV_URL: https://$CI_COMMIT_REF_SLUG.$PREVIEW_DOMAIN
DEPLOY_ENV_TIER: development
DEPLOY_BRANCH: $CI_COMMIT_REF_NAME
- when: never
needs:
- job: build_application_basic_demo
- job: build_doc
artifacts: true
optional: true
script:
- apt-get update
- apt-get install -y curl
- python .gitlab/ci/generate_firmware_json.py
- python .gitlab/ci/deploy_firmware.py
artifacts:
when: always
paths:
- firmware.json
- merged_binary/**/*
expire_in: 1 week
- cd docs
- npx wrangler pages deploy "dist/" --project-name "$SITE_DEPLOY_PROJECT_NAME" --branch="$DEPLOY_BRANCH"
-90
View File
@@ -1,90 +0,0 @@
#!/usr/bin/env python3
# SPDX-FileCopyrightText: 2026 Espressif Systems (Shanghai) CO LTD
# SPDX-License-Identifier: Apache-2.0
"""
Deploy firmware artifacts to the download server.
Behavior is controlled by the DEPLOY_FIRMWARE environment variable:
"1" - package firmware.json + merged_binary/ into esp-claw-firmware.zip and upload
"0" - print a skip message and exit
"""
import os
import subprocess
import sys
import zipfile
from pathlib import Path
ARCHIVE_NAME = 'esp-claw-firmware.zip'
def build_archive() -> Path:
archive_path = Path(ARCHIVE_NAME)
with zipfile.ZipFile(archive_path, 'w', zipfile.ZIP_DEFLATED) as zf:
firmware_json = Path('firmware.json')
if firmware_json.exists():
zf.write(firmware_json, firmware_json.name)
print(f" added: {firmware_json}")
else:
print('WARNING: firmware.json not found, skipping.', file=sys.stderr)
merged_binary = Path('merged_binary')
if merged_binary.exists():
for file in sorted(merged_binary.rglob('*')):
if file.is_file():
zf.write(file, file.as_posix())
print(f" added: {file}")
else:
print('WARNING: merged_binary/ directory not found, skipping.', file=sys.stderr)
print(f"Archive created: {archive_path} ({archive_path.stat().st_size} bytes)")
return archive_path
def upload_archive(archive_path: Path) -> None:
upload_key = os.environ.get('DL_UPLOAD_KEY')
upload_api_url = os.environ.get('DL_UPLOAD_API_URL')
upload_path = os.environ.get('DL_UPLOAD_PATH')
missing = [var for var, val in [
('DL_UPLOAD_KEY', upload_key),
('DL_UPLOAD_API_URL', upload_api_url),
('DL_UPLOAD_PATH', upload_path),
] if not val]
if missing:
print(f"ERROR: Missing required environment variables: {', '.join(missing)}", file=sys.stderr)
sys.exit(1)
cmd = [
'curl',
'--fail',
'-u', upload_key,
'-F', f"file=@{archive_path}",
'-F', f"path={upload_path}",
upload_api_url,
]
print(f"Uploading {archive_path} to {upload_api_url} (path={upload_path}) ...")
result = subprocess.run(cmd, check=False)
if result.returncode != 0:
print(f"ERROR: Upload failed with exit code {result.returncode}", file=sys.stderr)
sys.exit(result.returncode)
print('Upload succeeded.')
def main() -> None:
deploy_firmware = os.environ.get('DEPLOY_FIRMWARE', '0')
if deploy_firmware != '1':
print(f"DEPLOY_FIRMWARE={deploy_firmware!r}: skipping firmware deployment.")
sys.exit(0)
print('DEPLOY_FIRMWARE=1: packaging and uploading firmware artifacts ...')
archive_path = build_archive()
upload_archive(archive_path)
if __name__ == '__main__':
main()
-140
View File
@@ -1,140 +0,0 @@
#!/usr/bin/env python3
# SPDX-FileCopyrightText: 2026 Espressif Systems (Shanghai) CO LTD
#
# SPDX-License-Identifier: Apache-2.0
import json
import os
import sys
from pathlib import Path
from typing import Any, Dict, List
def _log(msg: str) -> None:
print(msg, file=sys.stderr)
def _parse_flash_mb(value: Any) -> int:
if isinstance(value, int):
return value
if not isinstance(value, str):
raise ValueError(f'unsupported min_flash_size type: {type(value)}')
text = value.strip()
if not text:
raise ValueError('empty min_flash_size')
upper = text.upper()
if upper.endswith('MB'):
return int(upper[:-2].strip(), 0)
if upper.endswith('M'):
return int(upper[:-1].strip(), 0)
num = int(text, 0)
if num % (1024 * 1024) == 0:
return num // (1024 * 1024)
return num
def _join_base_url(base_url: str, filename: str) -> str:
if not base_url:
base_url = '/'
if base_url.endswith('/'):
return f'{base_url}{filename}'
return f'{base_url}/{filename}'
def _load_metadata_files(merged_dir: Path) -> List[Dict[str, Any]]:
metadata_files = sorted(merged_dir.glob('*.json'))
if not metadata_files:
raise RuntimeError(f'No metadata json found in {merged_dir}')
records: List[Dict[str, Any]] = []
for path in metadata_files:
with path.open('r', encoding='utf-8') as fr:
data = json.load(fr)
if not isinstance(data, dict):
raise RuntimeError(f'Invalid metadata format: {path}')
records.append(data)
return records
def main() -> int:
cwd = Path.cwd()
merged_dir = cwd / 'merged_binary'
if not merged_dir.is_dir():
_log(f'merged_binary directory not found: {merged_dir}')
return 1
base_url = os.getenv('MERGED_BINARY_BASE_URL', '/').strip() or '/'
try:
records = _load_metadata_files(merged_dir)
except Exception as e:
_log(str(e))
return 1
firmware: Dict[str, List[Dict[str, Any]]] = {}
for record in records:
chip = record.get('chip')
board = record.get('board')
merged_binary = record.get('merged_binary')
min_flash_size = record.get('min_flash_size')
nvs_info = record.get('nvs_info')
if not isinstance(chip, str) or not chip.strip():
_log(f'skip one metadata: invalid chip ({record})')
continue
if not isinstance(board, str) or not board.strip():
_log(f'skip one metadata: invalid board ({record})')
continue
if not isinstance(merged_binary, str) or not merged_binary.strip():
_log(f'skip one metadata: invalid merged_binary ({record})')
continue
if not isinstance(nvs_info, dict):
_log(f'skip one metadata: invalid nvs_info ({record})')
continue
try:
min_flash_mb = _parse_flash_mb(min_flash_size)
except Exception as e:
_log(f'skip one metadata: invalid min_flash_size ({record}) ({e})')
continue
item = {
'board': board,
'features': [],
'description': '',
'merged_binary': _join_base_url(base_url, merged_binary),
'min_flash_size': min_flash_mb,
'min_psram_size': 8,
'nvs_info': {
'start_addr': str(nvs_info.get('start_addr', '')),
'size': str(nvs_info.get('size', '')),
},
}
firmware.setdefault(chip, []).append(item)
if not firmware:
_log('No valid metadata collected from merged_binary/*.json')
return 1
for chip in firmware:
firmware[chip].sort(key=lambda x: x['board'])
out_file = cwd / 'firmware.json'
out_file.write_text(json.dumps(firmware, ensure_ascii=False, indent=2) + '\n', encoding='utf-8')
print(f'Generated: {out_file}')
return 0
if __name__ == '__main__':
sys.exit(main())
+50 -108
View File
@@ -10,78 +10,38 @@
- "pytest.ini"
# components folder, in the alphabetic order
.patterns-components_cap_cli: &patterns-components_cap_cli
- "components/cap_cli/**/*"
.patterns-components_capabilities: &patterns-components_capabilities
- "components/claw_capabilities/**/*"
.patterns-components_cap_files: &patterns-components_cap_files
- "components/cap_files/**/*"
.patterns-components_modules: &patterns-components_modules
- "components/claw_modules/**/*"
.patterns-components_cap_im_attachment: &patterns-components_cap_im_attachment
- "components/cap_im_attachment/**/*"
.patterns-components_cap_im_qq: &patterns-components_cap_im_qq
- "components/cap_im_qq/**/*"
.patterns-components_cap_im_tg: &patterns-components_cap_im_tg
- "components/cap_im_tg/**/*"
.patterns-components_cap_im_wechat: &patterns-components_cap_im_wechat
- "components/cap_im_wechat/**/*"
.patterns-components_cap_llm_inspect: &patterns-components_cap_llm_inspect
- "components/cap_llm_inspect/**/*"
.patterns-components_cap_lua: &patterns-components_cap_lua
- "components/cap_lua/**/*"
.patterns-components_cap_mcp_client: &patterns-components_cap_mcp_client
- "components/cap_mcp_client/**/*"
.patterns-components_cap_mcp_server: &patterns-components_cap_mcp_server
- "components/cap_mcp_server/**/*"
.patterns-components_cap_skill: &patterns-components_cap_skill
- "components/cap_skill/**/*"
.patterns-components_cap_time: &patterns-components_cap_time
- "components/cap_time/**/*"
.patterns-components_cap_web_search: &patterns-components_cap_web_search
- "components/cap_web_search/**/*"
.patterns-components_claw_cap: &patterns-components_claw_cap
- "components/claw_cap/**/*"
.patterns-components_claw_core: &patterns-components_claw_core
- "components/claw_core/**/*"
.patterns-components_claw_event_router: &patterns-components_claw_event_router
- "components/claw_event_router/**/*"
.patterns-components_claw_memory: &patterns-components_claw_memory
- "components/claw_memory/**/*"
.patterns-components_claw_skill: &patterns-components_claw_skill
- "components/claw_skill/**/*"
.patterns-components_lua_modules: &patterns-components_lua_modules
- "components/lua_modules/**/*"
# application folder, in the alphabetic order
.patterns-application_basic_demo: &patterns-application_basic_demo
- "application/basic_demo/**/*"
.patterns-yml: &patterns-yml
- "**/*.yml"
.patterns-docs: &patterns-docs
- "docs/**/*"
##############
# if anchors #
##############
.if-protected: &if-protected
if: '($CI_COMMIT_REF_NAME == "master" || $CI_COMMIT_BRANCH =~ /^release\/v/ || $CI_COMMIT_TAG =~ /^v\d+\.\d+(\.\d+)?($|-)/)'
.if-dev-push: &if-dev-push
if: '$CI_COMMIT_REF_NAME != "master" && $CI_COMMIT_BRANCH !~ /^release\/v/ && $CI_COMMIT_TAG !~ /^v\d+\.\d+(\.\d+)?($|-)/ && ($CI_PIPELINE_SOURCE == "push" || $CI_PIPELINE_SOURCE == "merge_request_event")'
# Push to any protected ref
.if-protected-push: &if-protected-push
if: '$CI_COMMIT_REF_PROTECTED == "true" && $CI_PIPELINE_SOURCE == "push"'
# master push only
.if-master-push: &if-master-push
if: '$CI_COMMIT_REF_NAME == "master" && $CI_PIPELINE_SOURCE == "push"'
# Merge request pipeline (dev branches). Non-protected branch push pipelines are
# disabled at workflow level (see .gitlab-ci.yml), so we only care about MR here.
.if-mr: &if-mr
if: '$CI_PIPELINE_SOURCE == "merge_request_event"'
##################
# Auto Generated #
@@ -92,69 +52,51 @@
.if-label-build: &if-label-build
if: '$BOT_LABEL_BUILD || $CI_MERGE_REQUEST_LABELS =~ /^(?:[^,\n\r]+,)*build(?:,[^,\n\r]+)*$/i'
.if-label-target_test: &if-label-target_test
if: '$BOT_LABEL_TARGET_TEST || $CI_MERGE_REQUEST_LABELS =~ /^(?:[^,\n\r]+,)*target_test(?:,[^,\n\r]+)*$/i'
.if-label-build_docs: &if-label-build_docs
if: '$BOT_LABEL_BUILD_DOCS || $CI_MERGE_REQUEST_LABELS =~ /^(?:[^,\n\r]+,)*build_docs(?:,[^,\n\r]+)*$/i'
.if-label-pre_check: &if-label-pre_check
if: '$BOT_LABEL_PRE_CHECK || $CI_MERGE_REQUEST_LABELS =~ /^(?:[^,\n\r]+,)*pre_check(?:,[^,\n\r]+)*$/i'
.if_label-deploy: &if-label-deploy
if: '$BOT_LABEL_DEPLOY || $CI_MERGE_REQUEST_LABELS =~ /^(?:[^,\n\r]+,)*deploy(?:,[^,\n\r]+)*$/i'
#########
# Rules #
#########
# rules for applications
.rules:build:application_basic_demo:
rules:
- <<: *if-protected
- <<: *if-protected-push
- <<: *if-label-build
- <<: *if-trigger-job
- <<: *if-dev-push
- <<: *if-mr
changes: *patterns-build_system
- <<: *if-dev-push
changes: *patterns-components_cap_cli
- <<: *if-dev-push
changes: *patterns-components_cap_files
- <<: *if-dev-push
changes: *patterns-components_cap_im_attachment
- <<: *if-dev-push
changes: *patterns-components_cap_im_qq
- <<: *if-dev-push
changes: *patterns-components_cap_im_tg
- <<: *if-dev-push
changes: *patterns-components_cap_im_wechat
- <<: *if-dev-push
changes: *patterns-components_cap_llm_inspect
- <<: *if-dev-push
changes: *patterns-components_cap_lua
- <<: *if-dev-push
changes: *patterns-components_cap_mcp_client
- <<: *if-dev-push
changes: *patterns-components_cap_mcp_server
- <<: *if-dev-push
changes: *patterns-components_cap_skill
- <<: *if-dev-push
changes: *patterns-components_cap_time
- <<: *if-dev-push
changes: *patterns-components_cap_web_search
- <<: *if-dev-push
changes: *patterns-components_claw_cap
- <<: *if-dev-push
changes: *patterns-components_claw_core
- <<: *if-dev-push
changes: *patterns-components_claw_event_router
- <<: *if-dev-push
changes: *patterns-components_claw_memory
- <<: *if-dev-push
changes: *patterns-components_claw_skill
- <<: *if-dev-push
- <<: *if-mr
changes: *patterns-components_capabilities
- <<: *if-mr
changes: *patterns-components_modules
- <<: *if-mr
changes: *patterns-components_lua_modules
- <<: *if-mr
changes: *patterns-application_basic_demo
- <<: *if-mr
changes: *patterns-docs
- when: never
.rules:pre_check:docs:
rules:
- <<: *if-protected
- <<: *if-protected-push
- <<: *if-label-pre_check
- <<: *if-trigger-job
- <<: *if-dev-push
- <<: *if-mr
changes: *patterns-docs
- when: never
.rules:build:doc:
rules:
- <<: *if-protected-push
- <<: *if-label-build
- <<: *if-trigger-job
- <<: *if-mr
changes: *patterns-docs
- when: never
.rules:push:github:
rules:
- <<: *if-master-push
- when: never
+10
View File
@@ -168,6 +168,16 @@ esp-claw/
If this project inspires or helps you, please consider giving it a star. ⭐⭐⭐⭐⭐
### Star History
<a href="https://www.star-history.com/?repos=espressif%2Fesp-claw&type=date&legend=top-left">
<picture>
<source media="(prefers-color-scheme: dark)" srcset="https://api.star-history.com/chart?repos=espressif/esp-claw&type=date&theme=dark&legend=top-left" />
<source media="(prefers-color-scheme: light)" srcset="https://api.star-history.com/chart?repos=espressif/esp-claw&type=date&legend=top-left" />
<img alt="Star History Chart" src="https://api.star-history.com/chart?repos=espressif/esp-claw&type=date&legend=top-left" />
</picture>
</a>
## Acknowledgements
ESP-Claw is inspired by [OpenClaw](https://github.com/openclaw/openclaw).
+10
View File
@@ -168,6 +168,16 @@ esp-claw/
如果这个项目对您有所启发和帮助,欢迎点亮一颗星!⭐⭐⭐⭐⭐
### Star History
<a href="https://www.star-history.com/?repos=espressif%2Fesp-claw&type=date&legend=top-left">
<picture>
<source media="(prefers-color-scheme: dark)" srcset="https://api.star-history.com/chart?repos=espressif/esp-claw&type=date&theme=dark&legend=top-left" />
<source media="(prefers-color-scheme: light)" srcset="https://api.star-history.com/chart?repos=espressif/esp-claw&type=date&legend=top-left" />
<img alt="Star History Chart" src="https://api.star-history.com/chart?repos=espressif/esp-claw&type=date&legend=top-left" />
</picture>
</a>
## 致谢
灵感来自 [OpenClaw](https://github.com/openclaw/openclaw)。
+2 -1
View File
@@ -8,7 +8,8 @@
"build": "astro build",
"preview": "astro preview",
"astro": "astro",
"check:doc-lines": "node ./tools/check-doc-line-alignment.ts"
"check:doc-lines": "node ./tools/check-doc-line-alignment.ts",
"generate-firmware-json": "node ./tools/generate-firmware-json.ts"
},
"dependencies": {
"@astrojs/check": "^0.9.8",
+2
View File
@@ -0,0 +1,2 @@
/ /zh-cn/ 302
/esp-claw/* /:splat 302
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -25,10 +25,13 @@ import { Steps, Aside, Badge } from '@astrojs/starlight/components';
pip install esp-bmgr-assist
```
<Aside type="tip">
ESP-Claw uses ESP Board Manager to support multiple development boards.
<Aside title="Need to adapt a new development board?" type="tip">
ESP-Claw uses ESP Board Manager to support multiple development boards.
To add other boards, see the [ESP Board Manager component](https://components.espressif.com/components/espressif/esp_board_manager/).
To add other boards, see the [ESP Board Manager component](https://github.com/espressif/esp-gmf/blob/main/packages/esp_board_manager/docs/how_to_customize_board.md),
and put the board adaptation files in [`application/basic_demo/boards`](https://github.com/espressif/esp-claw/tree/master/application/basic_demo/boards).
Welcome to submit the board adaptation code to the ESP-Claw repository via Pull Request.
</Aside>
2. **Get the ESP-Claw source**
@@ -101,10 +101,12 @@ If you use the USB camera sample from Espressif, you only need to buy a micropho
In addition to the built-in WS2812 LED on ESP32-S3-DevKitC-1, you can connect a WS2812 strip/ring to extend lighting effects.
Due to power limits, do not connect an overly long WS2812 strip/ring.
You can choose the WS2812 ring with the same model as the [demo image](https://detail.tmall.com/item.htm?id=994762490273&skuId=6139364670619).
### Servo
ESP-Claw supports servos for mechanical motion.
You can choose a common servo, such as [this one](https://item.taobao.com/item.htm?id=1016474722460).
---
@@ -18,3 +18,12 @@ Coming soon:
## Supported development boards
See [GitHub repository](https://github.com/espressif/esp-claw/tree/master/application/basic_demo/boards).
<Aside title="Need to adapt a new development board?" type="tip">
ESP-Claw uses ESP Board Manager to support multiple development boards.
To add other boards, see the [ESP Board Manager component](https://github.com/espressif/esp-gmf/blob/main/packages/esp_board_manager/docs/how_to_customize_board.md),
and put the board adaptation files in [`application/basic_demo/boards`](https://github.com/espressif/esp-claw/tree/master/application/basic_demo/boards).
Welcome to submit the board adaptation code to the ESP-Claw repository via Pull Request.
</Aside>

Some files were not shown because too many files have changed in this diff Show More