Merge pull request #20 from yourclaw/feat/patch-system-and-secrets-deny #27
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| name: YourClaw Release | |
| # Builds and publishes the hardened OpenClaw fork when: | |
| # - An upstream-sync PR is merged to yourclaw branch | |
| # - A yourclaw-* tag is pushed | |
| # - Manually triggered | |
| on: | |
| push: | |
| branches: | |
| - yourclaw | |
| tags: | |
| - "yourclaw-*" | |
| paths-ignore: | |
| - "docs/**" | |
| - "**/*.md" | |
| - "**/*.mdx" | |
| - ".github/**" | |
| workflow_dispatch: | |
| inputs: | |
| version_suffix: | |
| description: "Override version suffix (leave empty for auto-increment)" | |
| required: false | |
| default: "" | |
| type: string | |
| concurrency: | |
| group: yourclaw-release-${{ github.ref }} | |
| cancel-in-progress: false | |
| env: | |
| REGISTRY: ghcr.io | |
| IMAGE_NAME: yourclaw/openclaw | |
| NPM_REGISTRY: https://npm.pkg.github.com | |
| jobs: | |
| # --- Security scan --- | |
| # Note: upstream tests are not re-run here. We only sync upstream release tags, | |
| # which have already passed upstream's full CI. Our gate is the security scan. | |
| security-scan: | |
| name: Security scan | |
| runs-on: ubuntu-latest | |
| steps: | |
| - name: Checkout | |
| uses: actions/checkout@v4 | |
| with: | |
| ref: yourclaw | |
| - name: Setup Node.js | |
| uses: actions/setup-node@v4 | |
| with: | |
| node-version: "22" | |
| - name: npm audit | |
| run: | | |
| corepack enable && corepack prepare pnpm@latest --activate | |
| pnpm install --frozen-lockfile | |
| pnpm audit --audit-level=critical || echo "::warning::npm audit found critical vulnerabilities" | |
| # --- Determine version --- | |
| version: | |
| name: Determine version | |
| runs-on: ubuntu-latest | |
| outputs: | |
| version: ${{ steps.version.outputs.version }} | |
| npm_version: ${{ steps.version.outputs.npm_version }} | |
| steps: | |
| - name: Checkout | |
| uses: actions/checkout@v4 | |
| with: | |
| ref: yourclaw | |
| fetch-depth: 0 | |
| - name: Determine version | |
| id: version | |
| run: | | |
| set -euo pipefail | |
| # Read the upstream tag we're based on | |
| UPSTREAM_TAG=$(cat yourclaw-patches/LAST_SYNCED_TAG 2>/dev/null | tr -d '[:space:]' || echo "v0.0.0") | |
| UPSTREAM_VERSION="${UPSTREAM_TAG#v}" | |
| # If triggered by tag push, extract version from tag | |
| if [[ "${GITHUB_REF}" == refs/tags/yourclaw-* ]]; then | |
| VERSION="${GITHUB_REF#refs/tags/yourclaw-}" | |
| else | |
| # Auto-increment suffix based on commits since last upstream sync. | |
| # The sync commit (which updated LAST_SYNCED_TAG) is the baseline. | |
| # Suffix 1 = the sync itself, 2 = first patch after sync, etc. | |
| SYNC_COMMIT=$(git log -1 --format=%H -- yourclaw-patches/LAST_SYNCED_TAG) | |
| PATCH_COUNT=$(git rev-list --count "${SYNC_COMMIT}..HEAD") | |
| SUFFIX=$(( PATCH_COUNT + 1 )) | |
| # Allow manual override via workflow_dispatch | |
| if [ -n "${{ inputs.version_suffix }}" ]; then | |
| SUFFIX="${{ inputs.version_suffix }}" | |
| fi | |
| VERSION="${UPSTREAM_VERSION}-yourclaw.${SUFFIX}" | |
| fi | |
| echo "version=${VERSION}" >> "$GITHUB_OUTPUT" | |
| echo "npm_version=${VERSION}" >> "$GITHUB_OUTPUT" | |
| echo "Release version: ${VERSION}" | |
| # --- Build npm package --- | |
| build-npm: | |
| name: Build & publish npm package | |
| needs: [security-scan, version] | |
| runs-on: ubuntu-latest | |
| permissions: | |
| packages: write | |
| contents: read | |
| steps: | |
| - name: Checkout | |
| uses: actions/checkout@v4 | |
| with: | |
| ref: yourclaw | |
| - name: Setup Node.js | |
| uses: actions/setup-node@v4 | |
| with: | |
| node-version: "22" | |
| registry-url: ${{ env.NPM_REGISTRY }} | |
| scope: "@yourclaw" | |
| - name: Install pnpm & Bun | |
| run: | | |
| corepack enable && corepack prepare pnpm@latest --activate | |
| curl -fsSL https://bun.sh/install | bash | |
| echo "$HOME/.bun/bin" >> $GITHUB_PATH | |
| - name: Install dependencies | |
| run: pnpm install --frozen-lockfile | |
| - name: Build | |
| run: pnpm build | |
| - name: Set package version and scope | |
| env: | |
| VERSION: ${{ needs.version.outputs.npm_version }} | |
| run: | | |
| # Update package.json for @yourclaw scope | |
| node -e " | |
| const pkg = require('./package.json'); | |
| pkg.name = '@yourclaw/openclaw'; | |
| pkg.version = '${VERSION}'; | |
| pkg.publishConfig = { registry: 'https://npm.pkg.github.com' }; | |
| require('fs').writeFileSync('package.json', JSON.stringify(pkg, null, 2)); | |
| " | |
| - name: Publish to GitHub Packages | |
| run: | | |
| set +e | |
| OUTPUT=$(pnpm publish --no-git-checks --access restricted 2>&1) | |
| EXIT_CODE=$? | |
| echo "$OUTPUT" | |
| if [ $EXIT_CODE -ne 0 ]; then | |
| if echo "$OUTPUT" | grep -q "Cannot publish over existing version"; then | |
| echo "::warning::Version already published — skipping (idempotent)" | |
| exit 0 | |
| fi | |
| exit $EXIT_CODE | |
| fi | |
| env: | |
| NODE_AUTH_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| # --- Build Chainguard Docker image (amd64) --- | |
| build-docker-amd64: | |
| name: Build Docker (amd64) | |
| needs: [security-scan, version] | |
| runs-on: ubuntu-latest | |
| permissions: | |
| packages: write | |
| contents: read | |
| outputs: | |
| digest: ${{ steps.build.outputs.digest }} | |
| skipped: ${{ steps.check.outputs.exists }} | |
| steps: | |
| - name: Check if image already exists | |
| id: check | |
| env: | |
| IMAGE: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} | |
| VERSION: ${{ needs.version.outputs.version }} | |
| run: | | |
| # Idempotent: skip build if tag already exists in registry | |
| if docker manifest inspect "${IMAGE}:${VERSION}-amd64" &>/dev/null; then | |
| echo "::warning::${IMAGE}:${VERSION}-amd64 already exists — skipping build" | |
| echo "exists=true" >> "$GITHUB_OUTPUT" | |
| else | |
| echo "exists=false" >> "$GITHUB_OUTPUT" | |
| fi | |
| - name: Checkout | |
| if: steps.check.outputs.exists != 'true' | |
| uses: actions/checkout@v4 | |
| with: | |
| ref: yourclaw | |
| - name: Set up Docker Buildx | |
| if: steps.check.outputs.exists != 'true' | |
| uses: docker/setup-buildx-action@v3 | |
| - name: Login to GitHub Container Registry | |
| uses: docker/login-action@v3 | |
| with: | |
| registry: ${{ env.REGISTRY }} | |
| username: ${{ github.repository_owner }} | |
| password: ${{ secrets.GITHUB_TOKEN }} | |
| - name: Build and push (amd64) | |
| if: steps.check.outputs.exists != 'true' | |
| id: build | |
| uses: docker/build-push-action@v5 | |
| with: | |
| context: . | |
| file: Dockerfile.chainguard | |
| platforms: linux/amd64 | |
| push: true | |
| tags: | | |
| ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ needs.version.outputs.version }}-amd64 | |
| cache-from: type=registry,ref=${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:buildcache-amd64 | |
| cache-to: type=registry,ref=${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:buildcache-amd64,mode=max | |
| # --- Build Chainguard Docker image (arm64) --- | |
| build-docker-arm64: | |
| name: Build Docker (arm64) | |
| needs: [security-scan, version] | |
| runs-on: ubuntu-latest | |
| permissions: | |
| packages: write | |
| contents: read | |
| outputs: | |
| digest: ${{ steps.build.outputs.digest }} | |
| skipped: ${{ steps.check.outputs.exists }} | |
| steps: | |
| - name: Check if image already exists | |
| id: check | |
| env: | |
| IMAGE: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} | |
| VERSION: ${{ needs.version.outputs.version }} | |
| run: | | |
| # Idempotent: skip build if tag already exists in registry | |
| if docker manifest inspect "${IMAGE}:${VERSION}-arm64" &>/dev/null; then | |
| echo "::warning::${IMAGE}:${VERSION}-arm64 already exists — skipping build" | |
| echo "exists=true" >> "$GITHUB_OUTPUT" | |
| else | |
| echo "exists=false" >> "$GITHUB_OUTPUT" | |
| fi | |
| - name: Checkout | |
| if: steps.check.outputs.exists != 'true' | |
| uses: actions/checkout@v4 | |
| with: | |
| ref: yourclaw | |
| - name: Set up Docker Buildx | |
| if: steps.check.outputs.exists != 'true' | |
| uses: docker/setup-buildx-action@v3 | |
| - name: Set up QEMU | |
| if: steps.check.outputs.exists != 'true' | |
| uses: docker/setup-qemu-action@v3 | |
| with: | |
| platforms: arm64 | |
| - name: Login to GitHub Container Registry | |
| uses: docker/login-action@v3 | |
| with: | |
| registry: ${{ env.REGISTRY }} | |
| username: ${{ github.repository_owner }} | |
| password: ${{ secrets.GITHUB_TOKEN }} | |
| - name: Build and push (arm64) | |
| if: steps.check.outputs.exists != 'true' | |
| id: build | |
| uses: docker/build-push-action@v5 | |
| with: | |
| context: . | |
| file: Dockerfile.chainguard | |
| platforms: linux/arm64 | |
| push: true | |
| tags: | | |
| ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ needs.version.outputs.version }}-arm64 | |
| cache-from: type=registry,ref=${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:buildcache-arm64 | |
| cache-to: type=registry,ref=${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:buildcache-arm64,mode=max | |
| # --- Create multi-arch manifest --- | |
| create-manifest: | |
| name: Create multi-arch manifest | |
| needs: [version, build-docker-amd64, build-docker-arm64] | |
| runs-on: ubuntu-latest | |
| permissions: | |
| packages: write | |
| steps: | |
| - name: Login to GitHub Container Registry | |
| uses: docker/login-action@v3 | |
| with: | |
| registry: ${{ env.REGISTRY }} | |
| username: ${{ github.repository_owner }} | |
| password: ${{ secrets.GITHUB_TOKEN }} | |
| - name: Create and push manifest | |
| env: | |
| VERSION: ${{ needs.version.outputs.version }} | |
| IMAGE: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} | |
| run: | | |
| set -euo pipefail | |
| # Versioned tag | |
| docker buildx imagetools create -t "${IMAGE}:${VERSION}" \ | |
| "${IMAGE}:${VERSION}-amd64" \ | |
| "${IMAGE}:${VERSION}-arm64" | |
| # Latest tag | |
| docker buildx imagetools create -t "${IMAGE}:latest" \ | |
| "${IMAGE}:${VERSION}-amd64" \ | |
| "${IMAGE}:${VERSION}-arm64" | |
| echo "Published: ${IMAGE}:${VERSION} and ${IMAGE}:latest" | |
| # --- Scan container image --- | |
| scan-container: | |
| name: Scan container image | |
| needs: [version, create-manifest] | |
| runs-on: ubuntu-latest | |
| permissions: | |
| security-events: write | |
| packages: read | |
| steps: | |
| - name: Login to GitHub Container Registry | |
| uses: docker/login-action@v3 | |
| with: | |
| registry: ${{ env.REGISTRY }} | |
| username: ${{ github.repository_owner }} | |
| password: ${{ secrets.GITHUB_TOKEN }} | |
| - name: Scan with Trivy | |
| uses: aquasecurity/[email protected] | |
| with: | |
| image-ref: "${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ needs.version.outputs.version }}" | |
| version: "v0.69.2" | |
| format: "sarif" | |
| output: "trivy-results.sarif" | |
| severity: "CRITICAL,HIGH" | |
| - name: Upload scan results | |
| uses: github/codeql-action/upload-sarif@v3 | |
| if: always() | |
| with: | |
| sarif_file: "trivy-results.sarif" | |
| # --- Create GitHub Release --- | |
| github-release: | |
| name: Create GitHub Release | |
| needs: [version, build-npm, create-manifest, scan-container] | |
| runs-on: ubuntu-latest | |
| permissions: | |
| contents: write | |
| steps: | |
| - name: Checkout | |
| uses: actions/checkout@v4 | |
| with: | |
| ref: yourclaw | |
| fetch-depth: 0 | |
| - name: Generate changelog | |
| id: changelog | |
| env: | |
| VERSION: ${{ needs.version.outputs.version }} | |
| run: | | |
| set -euo pipefail | |
| UPSTREAM_TAG=$(cat yourclaw-patches/LAST_SYNCED_TAG 2>/dev/null | tr -d '[:space:]') | |
| { | |
| echo "changelog<<CHANGELOG_EOF" | |
| echo "## YourClaw ${VERSION}" | |
| echo "" | |
| echo "Based on upstream OpenClaw \`${UPSTREAM_TAG}\` with YourClaw security patches." | |
| echo "" | |
| echo "### Artifacts" | |
| echo "- **npm**: \`@yourclaw/openclaw@${VERSION}\` (GitHub Packages)" | |
| echo "- **Docker**: \`ghcr.io/yourclaw/openclaw:${VERSION}\` (Chainguard-based, zero-CVE)" | |
| echo "" | |
| echo "### Security Enhancements" | |
| echo "- Chainguard base image (zero overnight CVEs)" | |
| echo "- Sandbox mode enabled by default" | |
| echo "- Filesystem access restricted to workspace" | |
| echo "- ClawGuard skill registry integration" | |
| echo "" | |
| echo "### Install" | |
| echo "\`\`\`bash" | |
| echo "npm install -g @yourclaw/openclaw@${VERSION} --registry=https://npm.pkg.github.com" | |
| echo "\`\`\`" | |
| echo "CHANGELOG_EOF" | |
| } >> "$GITHUB_OUTPUT" | |
| - name: Create release | |
| env: | |
| GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| VERSION: ${{ needs.version.outputs.version }} | |
| CHANGELOG: ${{ steps.changelog.outputs.changelog }} | |
| run: | | |
| # Idempotent: skip if release already exists | |
| if gh release view "yourclaw-${VERSION}" &>/dev/null; then | |
| echo "::warning::Release yourclaw-${VERSION} already exists — skipping" | |
| else | |
| gh release create "yourclaw-${VERSION}" \ | |
| --title "YourClaw ${VERSION}" \ | |
| --notes "${CHANGELOG}" \ | |
| --target yourclaw | |
| fi | |
| # --- Notify backend --- | |
| notify-backend: | |
| name: Notify YourClaw backend | |
| needs: [version, github-release] | |
| runs-on: ubuntu-latest | |
| steps: | |
| - name: Checkout | |
| uses: actions/checkout@v4 | |
| with: | |
| ref: yourclaw | |
| - name: Send webhook to backend | |
| env: | |
| VERSION: ${{ needs.version.outputs.version }} | |
| WEBHOOK_SECRET: ${{ secrets.YOURCLAW_WEBHOOK_SECRET }} | |
| BACKEND_WEBHOOK_URL: ${{ secrets.YOURCLAW_BACKEND_WEBHOOK_URL }} | |
| run: | | |
| set -euo pipefail | |
| UPSTREAM_TAG=$(cat yourclaw-patches/LAST_SYNCED_TAG 2>/dev/null | tr -d '[:space:]') | |
| # Skip if webhook URL not configured | |
| if [ -z "${BACKEND_WEBHOOK_URL}" ]; then | |
| echo "BACKEND_WEBHOOK_URL not set — skipping webhook notification" | |
| exit 0 | |
| fi | |
| # Build compact JSON payload — must match what the backend | |
| # produces via JSON.stringify(req.body) for HMAC verification. | |
| RELEASED_AT=$(date -u +%Y-%m-%dT%H:%M:%SZ) | |
| PAYLOAD=$(jq -cn \ | |
| --arg v "$VERSION" \ | |
| --arg ut "$UPSTREAM_TAG" \ | |
| --arg npm "@yourclaw/openclaw@${VERSION}" \ | |
| --arg img "ghcr.io/yourclaw/openclaw:${VERSION}" \ | |
| --arg ra "$RELEASED_AT" \ | |
| '{version:$v,upstreamTag:$ut,npmPackage:$npm,dockerImage:$img,releasedAt:$ra}') | |
| SIGNATURE=$(echo -n "${PAYLOAD}" | openssl dgst -sha256 -hmac "${WEBHOOK_SECRET}" | awk '{print $2}') | |
| curl -X POST "${BACKEND_WEBHOOK_URL}" \ | |
| -H "Content-Type: application/json" \ | |
| -H "X-Webhook-Signature: sha256=${SIGNATURE}" \ | |
| -d "${PAYLOAD}" \ | |
| --fail --silent --show-error \ | |
| || echo "::warning::Failed to notify backend — webhook may not be configured yet" |