ci: fix workflow review findings (double-run, heredoc, permissions, notify)

This commit is contained in:
quotentiroler
2026-02-06 18:10:42 -08:00
parent 845eafaacb
commit e8e55c24ef
7 changed files with 187 additions and 105 deletions

View File

@@ -40,27 +40,30 @@ runs:
run: |
TIMESTAMP=""
if [ "${{ inputs.timestamp }}" = "true" ]; then
TIMESTAMP="\"timestamp\": \"$(date -u +%Y-%m-%dT%H:%M:%SZ)\","
TIMESTAMP=$(date -u +%Y-%m-%dT%H:%M:%SZ)
fi
# Escape description for JSON (use double quotes for variable expansion)
DESCRIPTION=$(echo "${{ inputs.description }}" | jq -Rs .)
# Build JSON payload with jq to handle escaping properly
PAYLOAD=$(jq -n \
--arg username "${{ inputs.username }}" \
--arg avatar_url "${{ inputs.avatar_url }}" \
--arg title "${{ inputs.title }}" \
--arg description "${{ inputs.description }}" \
--argjson color "${{ inputs.color }}" \
--argjson fields '${{ inputs.fields }}' \
--arg timestamp "$TIMESTAMP" \
--argjson add_timestamp "${{ inputs.timestamp }}" \
'{
username: $username,
avatar_url: $avatar_url,
embeds: [{
title: $title,
description: $description,
color: $color,
fields: $fields
} + (if $add_timestamp then {timestamp: $timestamp} else {} end)]
}')
PAYLOAD=$(cat <<EOF
{
"username": "${{ inputs.username }}",
"avatar_url": "${{ inputs.avatar_url }}",
"embeds": [{
"title": "${{ inputs.title }}",
"description": $DESCRIPTION,
"color": ${{ inputs.color }},
$TIMESTAMP
"fields": ${{ inputs.fields }}
}]
}
EOF
)
curl -H "Content-Type: application/json" \
curl -sS -H "Content-Type: application/json" \
-d "$PAYLOAD" \
"${{ inputs.webhook_url }}"

View File

@@ -2,7 +2,10 @@ name: CI
on:
push:
branches: [main]
branches-ignore:
- develop
- alpha
- beta
pull_request:
workflow_call:
# Called by testing-strategy.yml for releases

View File

@@ -87,11 +87,3 @@ jobs:
--body "$BODY"
echo "Created hotfix PR: $BRANCH → main"
- name: Add urgent label
if: steps.check-pr.outputs.exists != 'true'
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
# Labels may not exist, so ignore errors
gh pr edit "${{ github.ref_name }}" --add-label "hotfix" 2>/dev/null || true

View File

@@ -104,8 +104,8 @@ jobs:
(needs.run-tests.outputs.test_status == 'passed' || inputs.skip_tests)
runs-on: ubuntu-latest
outputs:
pr_number: ${{ steps.create-pr.outputs.pull-request-number }}
pr_url: ${{ steps.create-pr.outputs.pull-request-url }}
pr_number: ${{ steps.output-pr.outputs.pull-request-number }}
pr_url: ${{ steps.output-pr.outputs.pull-request-url }}
steps:
- name: Checkout
uses: actions/checkout@v4
@@ -131,61 +131,99 @@ jobs:
fi
echo "count=$COMMIT_COUNT" >> $GITHUB_OUTPUT
echo "summary<<EOF" >> $GITHUB_OUTPUT
echo "summary<<__COMMITS_DELIM__" >> $GITHUB_OUTPUT
echo "$COMMIT_SUMMARY" >> $GITHUB_OUTPUT
echo "EOF" >> $GITHUB_OUTPUT
echo "__COMMITS_DELIM__" >> $GITHUB_OUTPUT
- name: Check for existing PR
id: check-pr
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
SOURCE="${{ needs.determine-target.outputs.source }}"
TARGET="${{ needs.determine-target.outputs.target }}"
EXISTING=$(gh pr list --head "$SOURCE" --base "$TARGET" --json number --jq '.[0].number // empty')
if [ -n "$EXISTING" ]; then
echo "exists=true" >> $GITHUB_OUTPUT
echo "pr_number=$EXISTING" >> $GITHUB_OUTPUT
echo "pr_url=https://github.com/${{ github.repository }}/pull/$EXISTING" >> $GITHUB_OUTPUT
echo "Promotion PR #$EXISTING already exists for $SOURCE → $TARGET"
else
echo "exists=false" >> $GITHUB_OUTPUT
fi
- name: Create Pull Request
id: create-pr
uses: peter-evans/create-pull-request@v6
with:
token: ${{ secrets.GITHUB_TOKEN }}
branch: promote/${{ needs.determine-target.outputs.source }}-to-${{ needs.determine-target.outputs.target }}
base: ${{ needs.determine-target.outputs.target }}
title: "🚀 Promote: ${{ needs.determine-target.outputs.source }} → ${{ needs.determine-target.outputs.target }}"
body: |
## Staged Promotion
if: steps.check-pr.outputs.exists != 'true'
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
SOURCE="${{ needs.determine-target.outputs.source }}"
TARGET="${{ needs.determine-target.outputs.target }}"
TEST_STAGE="${{ needs.determine-target.outputs.test_stage }}"
COMMIT_COUNT="${{ steps.commits.outputs.count }}"
| Property | Value |
|----------|-------|
| Source | `${{ needs.determine-target.outputs.source }}` |
| Target | `${{ needs.determine-target.outputs.target }}` |
| Test Stage | `${{ needs.determine-target.outputs.test_stage }}` |
| Test Result | ${{ needs.run-tests.outputs.test_status == 'passed' && '✅ Passed' || (inputs.skip_tests && '⚠️ Skipped' || '❓ Unknown') }} |
# Write PR body to a temp file to avoid shell quoting issues
BODY_FILE=$(mktemp)
cat > "$BODY_FILE" <<__PRBODY__
## Staged Promotion
### Changes (${{ steps.commits.outputs.count }} commits)
| Property | Value |
|----------|-------|
| Source | \`${SOURCE}\` |
| Target | \`${TARGET}\` |
| Test Stage | \`${TEST_STAGE}\` |
${{ steps.commits.outputs.summary }}
### Changes (${COMMIT_COUNT} commits)
### Test Coverage by Stage
${{ steps.commits.outputs.summary }}
| Stage | Tests |
|-------|-------|
| develop | tsgo, lint, format, protocol, unit (Node + Bun) |
| alpha | + secrets scan |
| beta | + Windows tests |
| stable | + macOS tests, install smoke |
### Checklist
### Checklist
- [ ] Changes reviewed
- [ ] CI passing
- [ ] Ready to promote
- [ ] Changes reviewed
- [ ] CI passing
- [ ] Ready to promote
---
*Auto-generated by the branch promotion workflow.*
__PRBODY__
---
*Auto-generated by the branch promotion workflow.*
labels: |
promotion
stage:${{ needs.determine-target.outputs.test_stage }}
draft: false
PR_URL=$(gh pr create \
--base "$TARGET" \
--head "$SOURCE" \
--title "🚀 Promote: $SOURCE → $TARGET" \
--body-file "$BODY_FILE" \
--label "promotion")
# Auto-merge for develop → alpha (fast-track)
rm -f "$BODY_FILE"
PR_NUMBER=$(echo "$PR_URL" | grep -oE '[0-9]+$')
echo "pr_number=$PR_NUMBER" >> $GITHUB_OUTPUT
echo "pr_url=$PR_URL" >> $GITHUB_OUTPUT
echo "Created promotion PR: $SOURCE → $TARGET"
- name: Output existing PR
id: output-pr
run: |
if [ "${{ steps.check-pr.outputs.exists }}" = "true" ]; then
echo "pull-request-number=${{ steps.check-pr.outputs.pr_number }}" >> $GITHUB_OUTPUT
echo "pull-request-url=${{ steps.check-pr.outputs.pr_url }}" >> $GITHUB_OUTPUT
else
echo "pull-request-number=${{ steps.create-pr.outputs.pr_number }}" >> $GITHUB_OUTPUT
echo "pull-request-url=${{ steps.create-pr.outputs.pr_url }}" >> $GITHUB_OUTPUT
fi
# Auto-merge for develop → alpha (fast-track, new PRs only)
auto-merge:
name: Auto-merge (develop → alpha)
needs: [determine-target, create-promotion-pr]
if: |
needs.determine-target.outputs.source == 'develop' &&
needs.create-promotion-pr.outputs.pr_number != ''
needs.create-promotion-pr.outputs.pr_number != '' &&
needs.create-promotion-pr.result == 'success'
runs-on: ubuntu-latest
steps:
- name: Enable auto-merge

View File

@@ -8,6 +8,14 @@ name: Release Orchestrator
# Flow: version → changelog → test → deploy → release
on:
push:
branches:
- main
paths-ignore:
- "docs/**"
- "*.md"
- ".github/workflows/docs-*.yml"
workflow_call:
inputs:
release_type:
@@ -39,10 +47,39 @@ on:
DISCORD_WEBHOOK_URL:
required: false
permissions:
contents: write
packages: write
jobs:
# Determine release parameters (push vs workflow_call)
determine-params:
name: Determine Parameters
runs-on: ubuntu-latest
outputs:
release_type: ${{ steps.params.outputs.release_type }}
source_branch: ${{ steps.params.outputs.source_branch }}
dry_run: ${{ steps.params.outputs.dry_run }}
steps:
- name: Set parameters
id: params
run: |
# When triggered by push to main, use stable defaults
if [ "${{ github.event_name }}" = "push" ]; then
echo "release_type=stable" >> $GITHUB_OUTPUT
echo "source_branch=main" >> $GITHUB_OUTPUT
echo "dry_run=false" >> $GITHUB_OUTPUT
else
# workflow_call - use provided inputs
echo "release_type=${{ inputs.release_type }}" >> $GITHUB_OUTPUT
echo "source_branch=${{ inputs.source_branch }}" >> $GITHUB_OUTPUT
echo "dry_run=${{ inputs.dry_run }}" >> $GITHUB_OUTPUT
fi
# Get commits since last release
get-commits:
name: Get Commits
needs: determine-params
runs-on: ubuntu-latest
outputs:
commits: ${{ steps.commits.outputs.commits }}
@@ -52,13 +89,13 @@ jobs:
uses: actions/checkout@v4
with:
fetch-depth: 0
ref: ${{ inputs.source_branch }}
ref: ${{ needs.determine-params.outputs.source_branch }}
- name: Get commits since last tag
id: commits
run: |
# Get latest tag for this release type
case "${{ inputs.release_type }}" in
case "${{ needs.determine-params.outputs.release_type }}" in
alpha)
PATTERN="v*-alpha.*"
;;
@@ -75,9 +112,9 @@ jobs:
if [ -z "$LATEST_TAG" ]; then
# No previous tag, use all commits
LATEST_TAG=$(git rev-list --max-parents=0 HEAD)
echo "No previous ${{ inputs.release_type }} tag found, using initial commit"
echo "No previous ${{ needs.determine-params.outputs.release_type }} tag found, using initial commit"
else
echo "Latest ${{ inputs.release_type }} tag: $LATEST_TAG"
echo "Latest ${{ needs.determine-params.outputs.release_type }} tag: $LATEST_TAG"
fi
COMMITS=$(git log ${LATEST_TAG}..HEAD --oneline --format="- %s (%h)")
@@ -87,62 +124,62 @@ jobs:
echo "commits=" >> $GITHUB_OUTPUT
else
echo "has_changes=true" >> $GITHUB_OUTPUT
echo "commits<<EOF" >> $GITHUB_OUTPUT
echo "commits<<__COMMITS_DELIM__" >> $GITHUB_OUTPUT
echo "$COMMITS" >> $GITHUB_OUTPUT
echo "EOF" >> $GITHUB_OUTPUT
echo "__COMMITS_DELIM__" >> $GITHUB_OUTPUT
fi
# Version operations
version:
name: Version
needs: get-commits
needs: [determine-params, get-commits]
if: needs.get-commits.outputs.has_changes == 'true'
uses: ./.github/workflows/version-operations.yml
with:
release_type: ${{ inputs.release_type }}
source_branch: ${{ inputs.source_branch }}
release_type: ${{ needs.determine-params.outputs.release_type }}
source_branch: ${{ needs.determine-params.outputs.source_branch }}
should_bump: true
dry_run: ${{ inputs.dry_run }}
dry_run: ${{ needs.determine-params.outputs.dry_run }}
# Generate changelog
changelog:
name: Changelog
needs: [get-commits, version]
needs: [determine-params, get-commits, version]
if: needs.get-commits.outputs.has_changes == 'true'
uses: ./.github/workflows/generate-changelog.yml
with:
version: ${{ needs.version.outputs.new_version }}
commits: ${{ needs.get-commits.outputs.commits }}
release_type: ${{ inputs.release_type }}
release_type: ${{ needs.determine-params.outputs.release_type }}
# Run full test suite for the release type
test:
name: Test
needs: [get-commits, version]
needs: [determine-params, get-commits, version]
if: needs.get-commits.outputs.has_changes == 'true'
uses: ./.github/workflows/testing-strategy.yml
with:
test_stage: ${{ inputs.release_type == 'stable' && 'stable' || inputs.release_type }}
test_stage: ${{ needs.determine-params.outputs.release_type == 'stable' && 'stable' || needs.determine-params.outputs.release_type }}
app_version: ${{ needs.version.outputs.new_version }}
secrets: inherit
# Deploy (npm + Docker)
deploy:
name: Deploy
needs: [version, test]
if: ${{ !inputs.dry_run && needs.test.outputs.test_status == 'passed' }}
needs: [determine-params, version, test]
if: ${{ needs.determine-params.outputs.dry_run != 'true' && needs.test.outputs.test_status == 'passed' }}
uses: ./.github/workflows/deployment-strategy.yml
with:
deployment_stage: ${{ inputs.release_type }}
deployment_stage: ${{ needs.determine-params.outputs.release_type }}
app_version: ${{ needs.version.outputs.new_version }}
source_branch: ${{ inputs.source_branch }}
source_branch: ${{ needs.determine-params.outputs.source_branch }}
secrets: inherit
# Create GitHub release
release:
name: GitHub Release
needs: [version, changelog, deploy]
if: ${{ !inputs.dry_run }}
needs: [determine-params, version, changelog, deploy]
if: ${{ needs.determine-params.outputs.dry_run != 'true' }}
runs-on: ubuntu-latest
outputs:
release_url: ${{ steps.create-release.outputs.html_url }}
@@ -151,7 +188,7 @@ jobs:
- name: Checkout
uses: actions/checkout@v4
with:
ref: ${{ inputs.source_branch }}
ref: ${{ needs.determine-params.outputs.source_branch }}
- name: Create GitHub Release
id: create-release
@@ -160,7 +197,7 @@ jobs:
tag_name: v${{ needs.version.outputs.new_version }}
name: openclaw ${{ needs.version.outputs.new_version }}
body: ${{ needs.changelog.outputs.changelog }}
prerelease: ${{ inputs.release_type != 'stable' }}
prerelease: ${{ needs.determine-params.outputs.release_type != 'stable' }}
draft: false
- name: Set status
@@ -170,7 +207,7 @@ jobs:
# Notify on success
notify-success:
name: Notify Success
needs: [version, release]
needs: [determine-params, version, release]
if: ${{ always() && needs.release.result == 'success' }}
runs-on: ubuntu-latest
env:
@@ -186,15 +223,18 @@ jobs:
webhook_url: ${{ secrets.DISCORD_WEBHOOK_URL }}
title: "🎉 Released: openclaw v${{ needs.version.outputs.new_version }}"
description: |
**Type**: ${{ inputs.release_type }}
**Type**: ${{ needs.determine-params.outputs.release_type }}
**Release**: ${{ needs.release.outputs.release_url }}
color: "3066993"
# Notify on failure
notify-failure:
name: Notify Failure
needs: [version, test, deploy, release]
if: ${{ always() && (needs.test.outputs.test_status != 'passed' || needs.deploy.result == 'failure' || needs.release.result == 'failure') }}
needs: [determine-params, version, test, deploy, release]
if: |
always() &&
needs.version.result != 'skipped' &&
(needs.test.result == 'failure' || needs.deploy.result == 'failure' || needs.release.result == 'failure')
runs-on: ubuntu-latest
env:
DISCORD_WEBHOOK_URL: ${{ secrets.DISCORD_WEBHOOK_URL }}
@@ -207,9 +247,9 @@ jobs:
uses: ./.github/actions/discord-notify
with:
webhook_url: ${{ secrets.DISCORD_WEBHOOK_URL }}
title: "❌ Release Failed: ${{ inputs.release_type }}"
title: "❌ Release Failed: ${{ needs.determine-params.outputs.release_type }}"
description: |
**Branch**: ${{ inputs.source_branch }}
**Branch**: ${{ needs.determine-params.outputs.source_branch }}
**Tests**: ${{ needs.test.outputs.test_status }}
[View Logs](${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }})
color: "15158332"

View File

@@ -39,12 +39,6 @@ jobs:
name: macOS Checks
if: inputs.test_stage == 'stable'
runs-on: macos-latest
strategy:
fail-fast: false
matrix:
include:
- task: test
command: pnpm test
steps:
- name: Checkout
uses: actions/checkout@v4
@@ -94,10 +88,10 @@ jobs:
export PATH="$NODE_BIN:$PATH"
pnpm install --frozen-lockfile --ignore-scripts=false --config.engine-strict=false --config.enable-pre-post-scripts=true
- name: Run ${{ matrix.task }}
- name: Run tests
env:
NODE_OPTIONS: --max-old-space-size=4096
run: ${{ matrix.command }}
run: pnpm test
# Install smoke test (stable only)
install-smoke:

View File

@@ -37,6 +37,9 @@ on:
description: "Version tag (with v prefix)"
value: ${{ jobs.version.outputs.version_tag }}
permissions:
contents: write
jobs:
version:
name: Version Operations
@@ -109,8 +112,17 @@ jobs:
NEW_VERSION="${TODAY}-beta.${NEW_NUM}"
;;
stable)
# Stable releases use just the date
NEW_VERSION="${TODAY}"
# Stable releases use date; append counter if tag already exists
if git tag -l "v${TODAY}" | grep -q .; then
# Tag exists, find next available counter
COUNTER=1
while git tag -l "v${TODAY}.${COUNTER}" | grep -q .; do
COUNTER=$((COUNTER + 1))
done
NEW_VERSION="${TODAY}.${COUNTER}"
else
NEW_VERSION="${TODAY}"
fi
;;
*)
echo "Unknown release type: $RELEASE_TYPE"