diff --git a/.github/workflows/release_skill.yaml b/.github/workflows/release_skill.yaml index f27ccc6..dd1cca2 100644 --- a/.github/workflows/release_skill.yaml +++ b/.github/workflows/release_skill.yaml @@ -1,138 +1,12 @@ -name: 技能自动化发布 (测试版-无鉴权) +name: 技能自动化发布 on: push: tags: ["v*"] jobs: - build-and-deploy: - runs-on: ubuntu-latest - steps: - # 1. 下载源码 - - uses: http://120.25.191.12:3000/admin/actions-checkout@v4 - - # 2. 环境及工具准备 - - name: Setup Tools - # 优化:使用国内清华源加速安装速度 - run: pip install pyarmor requests python-frontmatter --break-system-packages -i https://pypi.tuna.tsinghua.edu.cn/simple - - # 3. 【核心步骤】PyArmor 源代码混淆加密 - - name: Encrypt Source Code - run: | - mkdir -p dist/package - pyarmor gen -O dist/package scripts/*.py - cp SKILL.md dist/package/ - - # 4. 提取元数据并物理打包 - - name: Parse Metadata and Pack - id: build_task - run: | - python -c " - import frontmatter, os, json, shutil - post = frontmatter.load('SKILL.md') - metadata = post.metadata - - # slug 优先从 openclaw.slug 读取,兼容历史用 name - openclaw_meta = metadata.get('metadata', {}).get('openclaw', {}) - slug = (openclaw_meta.get('slug') or metadata.get('slug') or metadata.get('name') or '').strip() - if not slug: - raise Exception('SKILL.md 缺少 slug/name,无法构建发布包') - - # 以 Git Tag 为准(自动模式) - ref_name = (os.environ.get('GITHUB_REF_NAME') or '').strip() - if not ref_name: - ref_name = (os.environ.get('GITHUB_REF') or '').strip().split('/')[-1] - if not ref_name.startswith('v'): - raise Exception(f'非法标签: {ref_name},要求以 v 开头,例如 v1.0.10') - - version = ref_name.lstrip('v') - - # 自动模式:SKILL.md 与 tag 不一致时仅告警,不阻塞发布 - skill_version = str(metadata.get('version') or '').strip() - if skill_version and skill_version != version: - print(f'WARNING: SKILL.md version({skill_version}) != tag version({version}), using tag version') - - # 统一覆盖,确保后续输出与上传完全一致 - metadata['version'] = version - - # 安全写入输出 - with open(os.environ['GITHUB_OUTPUT'], 'a') as f: - f.write(f'slug={slug}\n') - f.write(f'version={version}\n') - f.write(f'metadata={json.dumps(metadata, ensure_ascii=False)}\n') - - # 制作最终的发布 ZIP 包 - shutil.make_archive(slug, 'zip', 'dist/package') - " - - # 5. 同步数据库记录 - - name: Sync Database (V2.0) - env: - METADATA_JSON: ${{ steps.build_task.outputs.metadata }} - run: | - python -c " - import requests, json, os - metadata = json.loads(os.environ['METADATA_JSON']) - url = 'https://jc2009.com/api/skill/update' - res = requests.post(url, json=metadata) - print(f'DB Sync Result: {res.text}') - if res.status_code != 200: - exit(1) - try: - payload = res.json() - except Exception: - exit(1) - if payload.get('code') != 200: - exit(1) - " - - # 6. 上传物理包 - - name: Upload Encrypted ZIP - env: - SLUG: ${{ steps.build_task.outputs.slug }} - VERSION: ${{ steps.build_task.outputs.version }} - run: | - python -c " - import requests, os - url = 'https://jc2009.com/api/upload' - slug = os.environ['SLUG'] - version = os.environ['VERSION'] - payload = {'plugin_name': slug, 'version': version, 'artifact_type': 'skill'} - with open(f'{slug}.zip', 'rb') as f: - res = requests.post(url, data=payload, files={'file': f}) - print(f'Upload Result: {res.text}') - if res.status_code != 200: - exit(1) - try: - payload = res.json() - except Exception: - exit(1) - if payload.get('code') != 200: - exit(1) - " - - # 7. 清理旧版本(只保留最新1个) - - name: Prune Old Versions - env: - SLUG: ${{ steps.build_task.outputs.slug }} - VERSION: ${{ steps.build_task.outputs.version }} - run: | - python -c " - import requests, os - url = 'https://jc2009.com/api/artifacts/prune-old-versions' - payload = { - 'name': os.environ['SLUG'], - 'artifact_type': 'skill', - 'keep_count': 1, - 'protect_version': os.environ['VERSION'] - } - res = requests.post(url, json=payload) - print(f'Prune Result: {res.text}') - if res.status_code != 200: - exit(1) - try: - body = res.json() - except Exception: - exit(1) - if body.get('code') != 200: - exit(1) - " \ No newline at end of file + release: + uses: admin/jiangchang-platform-kit/.github/workflows/reusable-release-skill.yaml@main + with: + artifact_platform: windows + pyarmor_platform: windows.x86_64 + include_readme_md: false \ No newline at end of file diff --git a/release.ps1 b/release.ps1 index 9a3aa41..639081a 100644 --- a/release.ps1 +++ b/release.ps1 @@ -1,31 +1,3 @@ -<# -.SYNOPSIS - One-command release script for skill repos. - -.DESCRIPTION - - Optional auto-commit - - Push current branch - - Auto-increment semantic tag (vX.Y.Z) - - Create & push tag - - Fail fast on unsafe states - -.EXAMPLES - # Safe mode (recommended): requires clean working tree - .\release.ps1 - - # Auto commit tracked/untracked changes then release - .\release.ps1 -AutoCommit -CommitMessage "chore: update skill config" - - # Dry run (show what would happen) - .\release.ps1 -DryRun - - # Custom tag prefix - .\release.ps1 -Prefix "v" -Message "正式发布" - -.NOTES - Requires: git, PowerShell 5+ -#> - [CmdletBinding()] param( [string]$Prefix = "v", @@ -39,178 +11,13 @@ param( Set-StrictMode -Version Latest $ErrorActionPreference = "Stop" -function Invoke-Git { - param([Parameter(Mandatory = $true)][string]$Args) - Write-Host ">> git $Args" -ForegroundColor DarkGray - & cmd /c "git $Args" - if ($LASTEXITCODE -ne 0) { - throw "git command failed: git $Args" - } +$scriptDir = Split-Path -Parent $MyInvocation.MyCommand.Path +$sharedScript = Join-Path $scriptDir "..\jiangchang-platform-kit\tools\release.ps1" +$sharedScript = [System.IO.Path]::GetFullPath($sharedScript) + +if (-not (Test-Path $sharedScript)) { + throw "Shared release script not found: $sharedScript" } - -function Get-GitOutput { - param([Parameter(Mandatory = $true)][string]$Args) - $output = & cmd /c "git $Args" 2>$null - if ($LASTEXITCODE -ne 0) { - throw "git command failed: git $Args" - } - return @($output) -} - -function Test-Repo { - & git rev-parse --is-inside-work-tree *> $null - return ($LASTEXITCODE -eq 0) -} - -function Get-CurrentBranch { - $b = (Get-GitOutput "branch --show-current" | Select-Object -First 1).Trim() - return $b -} - - -function Get-StatusPorcelain { - $lines = @(Get-GitOutput "status --porcelain") - return $lines -} - -function Parse-SemVerTag { - param( - [string]$Tag, - [string]$TagPrefix - ) - $escaped = [regex]::Escape($TagPrefix) - $m = [regex]::Match($Tag, "^${escaped}(\d+)\.(\d+)\.(\d+)$") - if (-not $m.Success) { return $null } - - return [pscustomobject]@{ - Raw = $Tag - Major = [int]$m.Groups[1].Value - Minor = [int]$m.Groups[2].Value - Patch = [int]$m.Groups[3].Value - } -} - -function Get-NextTag { - param([string]$TagPrefix) - - $tags = Get-GitOutput "tag --list" - $parsed = @() - - foreach ($t in $tags) { - $t = $t.Trim() - if (-not $t) { continue } - $obj = Parse-SemVerTag -Tag $t -TagPrefix $TagPrefix - if ($null -ne $obj) { $parsed += $obj } - } - - if ($parsed.Count -eq 0) { - return "${TagPrefix}1.0.1" - } - - $latest = $parsed | Sort-Object Major, Minor, Patch | Select-Object -Last 1 - return "$TagPrefix$($latest.Major).$($latest.Minor).$([int]$latest.Patch + 1)" -} - - -function Ensure-CleanOrAutoCommit { - param( - [switch]$DoAutoCommit, - [switch]$NeedClean, - [switch]$IsDryRun, - [string]$Msg - ) - - $status = @(Get-StatusPorcelain) - if ($status.Length -eq 0) { return } - - if ($NeedClean) { - Write-Host "Working tree is not clean and -RequireClean is enabled." -ForegroundColor Yellow - & git status --short - throw "Abort: dirty working tree." - } - - # 默认一键发布:有改动就自动提交;也可用 -AutoCommit 显式开启 - $commitMsg = $Msg - if ([string]::IsNullOrWhiteSpace($commitMsg)) { - $commitMsg = "chore: auto release commit ($(Get-Date -Format 'yyyy-MM-dd HH:mm:ss'))" - } - - if (-not $DoAutoCommit) { - Write-Host "Detected uncommitted changes, auto-committing before release..." -ForegroundColor Yellow - } - - if ($IsDryRun) { - Write-Host "[DryRun] Would run: git add -A" -ForegroundColor Yellow - Write-Host "[DryRun] Would run: git commit -m `"$commitMsg`"" -ForegroundColor Yellow - return - } - - Invoke-Git "add -A" - Invoke-Git "commit -m `"$commitMsg`"" -} - - -try { - Write-Host "=== Release Script Start ===" -ForegroundColor Cyan - - if (-not (Test-Repo)) { - throw "Current directory is not a git repository." - } - - $branch = Get-CurrentBranch - if ([string]::IsNullOrWhiteSpace($branch)) { - throw "Unable to determine current branch." - } - - if ($branch -notin @("main", "master")) { - throw "Current branch is '$branch'. Release is only allowed from main/master." - } - - Invoke-Git "fetch --tags --prune origin" - - Ensure-CleanOrAutoCommit -DoAutoCommit:$AutoCommit -NeedClean:$RequireClean -IsDryRun:$DryRun -Msg $CommitMessage - - $upstream = (& git rev-parse --abbrev-ref --symbolic-full-name "@{u}" 2>$null) - $hasUpstream = ($LASTEXITCODE -eq 0) - - if ($DryRun) { - if ($hasUpstream) { - Write-Host "[DryRun] Would run: git push" -ForegroundColor Yellow - } else { - Write-Host "[DryRun] Would run: git push -u origin $branch" -ForegroundColor Yellow - } - } else { - if ($hasUpstream) { - Invoke-Git "push" - } else { - Invoke-Git "push -u origin $branch" - } - } - - $nextTag = Get-NextTag -TagPrefix $Prefix - Write-Host "Next tag: $nextTag" -ForegroundColor Green - - $existing = @(Get-GitOutput "tag --list `"$nextTag`"") - if ($existing.Length -gt 0) { - throw "Tag already exists: $nextTag" - } - - if ($DryRun) { - Write-Host "[DryRun] Would run: git tag -a $nextTag -m `"$Message`"" -ForegroundColor Yellow - Write-Host "[DryRun] Would run: git push origin $nextTag" -ForegroundColor Yellow - Write-Host "=== DryRun Complete ===" -ForegroundColor Cyan - exit 0 - } - - Invoke-Git "tag -a $nextTag -m `"$Message`"" - Invoke-Git "push origin $nextTag" - - Write-Host "Release success: $nextTag" -ForegroundColor Green - Write-Host "=== Release Script Done ===" -ForegroundColor Cyan - exit 0 -} -catch { - Write-Host "Release failed: $($_.Exception.Message)" -ForegroundColor Red - exit 1 -} +& $sharedScript @PSBoundParameters +exit $LASTEXITCODE