From e8e55c24ef0add14ab9cad03a720f546c5326235 Mon Sep 17 00:00:00 2001 From: quotentiroler Date: Fri, 6 Feb 2026 18:10:42 -0800 Subject: [PATCH] ci: fix workflow review findings (double-run, heredoc, permissions, notify) --- .github/actions/discord-notify/action.yml | 41 ++++---- .github/workflows/ci.yml | 5 +- .github/workflows/hotfix-pr.yml | 8 -- .github/workflows/promote-branch.yml | 116 ++++++++++++++------- .github/workflows/release-orchestrator.yml | 96 ++++++++++++----- .github/workflows/testing-strategy.yml | 10 +- .github/workflows/version-operations.yml | 16 ++- 7 files changed, 187 insertions(+), 105 deletions(-) diff --git a/.github/actions/discord-notify/action.yml b/.github/actions/discord-notify/action.yml index e9f6ab69eb..e33faa5ba3 100644 --- a/.github/actions/discord-notify/action.yml +++ b/.github/actions/discord-notify/action.yml @@ -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 </dev/null || true diff --git a/.github/workflows/promote-branch.yml b/.github/workflows/promote-branch.yml index 6f69ebdcb9..aa16fb9317 100644 --- a/.github/workflows/promote-branch.yml +++ b/.github/workflows/promote-branch.yml @@ -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<> $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 diff --git a/.github/workflows/release-orchestrator.yml b/.github/workflows/release-orchestrator.yml index 884b7703c7..53e5960e08 100644 --- a/.github/workflows/release-orchestrator.yml +++ b/.github/workflows/release-orchestrator.yml @@ -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<> $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" diff --git a/.github/workflows/testing-strategy.yml b/.github/workflows/testing-strategy.yml index 51d0bff926..0cb0b6b1ca 100644 --- a/.github/workflows/testing-strategy.yml +++ b/.github/workflows/testing-strategy.yml @@ -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: diff --git a/.github/workflows/version-operations.yml b/.github/workflows/version-operations.yml index eea11917db..d41990c8d4 100644 --- a/.github/workflows/version-operations.yml +++ b/.github/workflows/version-operations.yml @@ -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"