diff --git a/.github/workflows/auto-release.yml b/.github/workflows/auto-release.yml index 385ffb4..a0410d7 100644 --- a/.github/workflows/auto-release.yml +++ b/.github/workflows/auto-release.yml @@ -73,7 +73,7 @@ jobs: fi # Extract commit message (after SHA) - MESSAGE=$(echo "$commit" | sed 's/^[a-f0-9]* //') + MESSAGE="${commit#* }" # Check for breaking changes if echo "$MESSAGE" | grep -qiE '!:' || echo "$MESSAGE" | grep -qiE 'BREAKING CHANGE'; then @@ -113,10 +113,12 @@ jobs: BUMP="none" fi - echo "bump=$BUMP" >> "$GITHUB_OUTPUT" - echo "breaking=$BREAKING" >> "$GITHUB_OUTPUT" - echo "features=$FEATURES" >> "$GITHUB_OUTPUT" - echo "fixes=$FIXES" >> "$GITHUB_OUTPUT" + { + echo "bump=$BUMP" + echo "breaking=$BREAKING" + echo "features=$FEATURES" + echo "fixes=$FIXES" + } >> "$GITHUB_OUTPUT" - name: Calculate new version if: steps.analyze.outputs.bump != 'none' @@ -168,7 +170,7 @@ jobs: echo "Creating tag: $NEW_TAG" # Use GitHub API to create tag (triggers workflows) SHA=$(git rev-parse HEAD) - gh api repos/${GITHUB_REPOSITORY}/git/refs -X POST -f ref="refs/tags/$NEW_TAG" -f sha="$SHA" + gh api "repos/${GITHUB_REPOSITORY}/git/refs" -X POST -f ref="refs/tags/$NEW_TAG" -f sha="$SHA" echo "Tag $NEW_TAG created successfully via GitHub API" - name: Summary @@ -180,20 +182,24 @@ jobs: FEATURES: ${{ steps.analyze.outputs.features }} FIXES: ${{ steps.analyze.outputs.fixes }} run: | - echo "## Auto Release Summary" >> $GITHUB_STEP_SUMMARY - echo "" >> $GITHUB_STEP_SUMMARY - echo "- **New Tag:** $NEW_TAG" >> $GITHUB_STEP_SUMMARY - echo "- **Bump Type:** $BUMP" >> $GITHUB_STEP_SUMMARY - echo "- **Breaking Changes:** $BREAKING" >> $GITHUB_STEP_SUMMARY - echo "- **Features:** $FEATURES" >> $GITHUB_STEP_SUMMARY - echo "- **Fixes:** $FIXES" >> $GITHUB_STEP_SUMMARY - echo "" >> $GITHUB_STEP_SUMMARY - echo "Release workflow will be triggered automatically." >> $GITHUB_STEP_SUMMARY + { + echo "## Auto Release Summary" + echo "" + echo "- **New Tag:** $NEW_TAG" + echo "- **Bump Type:** $BUMP" + echo "- **Breaking Changes:** $BREAKING" + echo "- **Features:** $FEATURES" + echo "- **Fixes:** $FIXES" + echo "" + echo "Release workflow will be triggered automatically." + } >> "$GITHUB_STEP_SUMMARY" - name: No release needed if: steps.analyze.outputs.bump == 'none' run: | - echo "## No Release Needed" >> $GITHUB_STEP_SUMMARY - echo "" >> $GITHUB_STEP_SUMMARY - echo "No conventional commits (feat:, fix:, BREAKING CHANGE) found since last tag." >> $GITHUB_STEP_SUMMARY - echo "Skipping release." >> $GITHUB_STEP_SUMMARY + { + echo "## No Release Needed" + echo "" + echo "No conventional commits (feat:, fix:, BREAKING CHANGE) found since last tag." + echo "Skipping release." + } >> "$GITHUB_STEP_SUMMARY" diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index d605c6d..9c63780 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -27,7 +27,7 @@ concurrency: jobs: docs: - uses: YiAgent/OpenCI/.github/workflows/reusable-docs.yml@b96e013bfaad2e6898dd6a25d127411a21da5c00 + uses: YiAgent/OpenCI/.github/workflows/reusable-docs.yml@4e1ecadc2505761f104f3fd8d255eee4eb369d90 with: build-cmd: ${{ vars.DOCS_BUILD_CMD || '' }} docs-path: ${{ vars.DOCS_DIR || 'docs' }} diff --git a/actions/issue/extract-plan/extract-plan.sh b/actions/issue/extract-plan/extract-plan.sh index b6c68e1..ad34e23 100644 --- a/actions/issue/extract-plan/extract-plan.sh +++ b/actions/issue/extract-plan/extract-plan.sh @@ -52,9 +52,27 @@ extract_plan_from_file() { done < "$file" \ | PLAN_VERSION="$PLAN_VERSION" perl -0777 -ne ' my $v = quotemeta $ENV{PLAN_VERSION}; - while (m{ \{ (?: [^{}] | (?R) )* "version" \s* : \s* "$v" (?: [^{}] | (?R) )* \} }gsx) { - print "$&\n" + my $t = $_; my @found; my $pos = 0; + while ($pos < length $t) { + if (substr($t,$pos,1) eq "{") { + my ($d,$i,$s,$e) = (1,$pos+1,0,0); + while ($i < length($t) && $d > 0) { + my $c = substr($t,$i,1); + if ($e) { $e=0 } + elsif ($s) { if ($c eq "\\") { $e=1 } elsif ($c eq "\"") { $s=0 } } + elsif ($c eq "\"") { $s=1 } + elsif ($c eq "{") { $d++ } + elsif ($c eq "}") { $d-- } + $i++; + } + if ($d == 0) { + my $cand = substr($t,$pos,$i-$pos); + push @found, $cand if $cand =~ /"version"\s*:\s*"$v"/; + } + } + $pos++; } + print "$found[-1]\n" if @found; ' \ | tail -n1 )" @@ -76,9 +94,27 @@ extract_plan_from_file() { found="$(printf '%s' "$concatenated" \ | PLAN_VERSION="$PLAN_VERSION" perl -0777 -ne ' my $v = quotemeta $ENV{PLAN_VERSION}; - while (m{ \{ (?: [^{}] | (?R) )* "version" \s* : \s* "$v" (?: [^{}] | (?R) )* \} }gsx) { - print "$&\n" + my $t = $_; my @found; my $pos = 0; + while ($pos < length $t) { + if (substr($t,$pos,1) eq "{") { + my ($d,$i,$s,$e) = (1,$pos+1,0,0); + while ($i < length($t) && $d > 0) { + my $c = substr($t,$i,1); + if ($e) { $e=0 } + elsif ($s) { if ($c eq "\\") { $e=1 } elsif ($c eq "\"") { $s=0 } } + elsif ($c eq "\"") { $s=1 } + elsif ($c eq "{") { $d++ } + elsif ($c eq "}") { $d-- } + $i++; + } + if ($d == 0) { + my $cand = substr($t,$pos,$i-$pos); + push @found, $cand if $cand =~ /"version"\s*:\s*"$v"/; + } + } + $pos++; } + print "$found[-1]\n" if @found; ' \ | tail -n1)" if [ -n "$found" ] && jq -e . <<<"$found" >/dev/null 2>&1; then @@ -106,9 +142,27 @@ extract_plan_from_file() { if [ -n "${concatenated:-}" ]; then found="$(printf '%s' "$concatenated" \ | perl -0777 -ne ' - while (m{ \{ (?: [^{}] | (?R) )* "actions" \s* : \s* \[ (?: [^\[\]] | (?R) )* \] (?: [^{}] | (?R) )* \} }gsx) { - print "$&\n" + my $t = $_; my @found; my $pos = 0; + while ($pos < length $t) { + if (substr($t,$pos,1) eq "{") { + my ($d,$i,$s,$e) = (1,$pos+1,0,0); + while ($i < length($t) && $d > 0) { + my $c = substr($t,$i,1); + if ($e) { $e=0 } + elsif ($s) { if ($c eq "\\") { $e=1 } elsif ($c eq "\"") { $s=0 } } + elsif ($c eq "\"") { $s=1 } + elsif ($c eq "{") { $d++ } + elsif ($c eq "}") { $d-- } + $i++; + } + if ($d == 0) { + my $cand = substr($t,$pos,$i-$pos); + push @found, $cand if $cand =~ /"actions"\s*:\s*\[/; + } + } + $pos++; } + print "$found[-1]\n" if @found; ' \ | while IFS= read -r candidate; do if printf '%s' "$candidate" | jq -e '.version == "'"$PLAN_VERSION"'" and (.actions | type) == "array"' >/dev/null 2>&1; then diff --git a/actions/pr/extract-plan/extract-plan.sh b/actions/pr/extract-plan/extract-plan.sh index 4d67fbe..f36cf9f 100644 --- a/actions/pr/extract-plan/extract-plan.sh +++ b/actions/pr/extract-plan/extract-plan.sh @@ -36,9 +36,27 @@ extract_plan_from_file() { found="$(printf '%s' "$concatenated" \ | PLAN_VERSION="$PLAN_VERSION" perl -0777 -ne ' my $v = quotemeta $ENV{PLAN_VERSION}; - while (m{ \{ (?: [^{}] | (?R) )* "version" \s* : \s* "$v" (?: [^{}] | (?R) )* \} }gsx) { - print "$&\n" + my $t = $_; my @found; my $pos = 0; + while ($pos < length $t) { + if (substr($t,$pos,1) eq "{") { + my ($d,$i,$s,$e) = (1,$pos+1,0,0); + while ($i < length($t) && $d > 0) { + my $c = substr($t,$i,1); + if ($e) { $e=0 } + elsif ($s) { if ($c eq "\\") { $e=1 } elsif ($c eq "\"") { $s=0 } } + elsif ($c eq "\"") { $s=1 } + elsif ($c eq "{") { $d++ } + elsif ($c eq "}") { $d-- } + $i++; + } + if ($d == 0) { + my $cand = substr($t,$pos,$i-$pos); + push @found, $cand if $cand =~ /"version"\s*:\s*"$v"/; + } + } + $pos++; } + print "$found[-1]\n" if @found; ' \ | tail -n1)" if [ -n "$found" ] && jq -e . <<<"$found" >/dev/null 2>&1; then diff --git a/lefthook.yml b/lefthook.yml index af474ef..e739101 100644 --- a/lefthook.yml +++ b/lefthook.yml @@ -120,7 +120,10 @@ pre-commit: glob: "*.sh" run: | if command -v shellcheck >/dev/null 2>&1; then - shellcheck {staged_files} + # --severity=error: SC2034 (warning) is a false positive in library + # files sourced by other scripts; pre-push shellcheck-full uses the + # same threshold for consistency. + shellcheck --severity=error {staged_files} else echo "::notice::shellcheck not installed; skipping shell lint" fi @@ -312,7 +315,10 @@ pre-push: exit 0 fi # shellcheck disable=SC2086 - shellcheck $files + # Use --severity=error: SC2034 (warning) produces false positives in + # library files sourced by other scripts (variables appear unused to + # shellcheck but are consumed by the sourcing scripts). + shellcheck --severity=error $files yamllint-full: tags: lint diff --git a/tests/actions/issue-extract-plan.bats b/tests/actions/issue-extract-plan.bats index 0476348..8f10b01 100644 --- a/tests/actions/issue-extract-plan.bats +++ b/tests/actions/issue-extract-plan.bats @@ -126,6 +126,25 @@ JSONL echo "$plan" | grep -q '"skill":"escalate"' } +@test "JSONL: plan with nested params objects is extracted (regression for #93)" { + # The (?R) recursive perl regex broke when action plan had nested {} objects, + # e.g. params:{"reason":"...","labels":["needs-human"]} inside an action entry. + local ef="${TMPDIR}/exec.jsonl" + cat >"$ef" <<'JSONL' +{"type":"system","subtype":"init","message":"Claude Code initialized"} +{"type":"assistant","message":{"role":"assistant","content":[{"type":"text","text":"```json\n{\"version\":\"issue-action-plan/v1\",\"reasoning\":\"nested\",\"actions\":[{\"id\":\"escalate\",\"skill\":\"escalate\",\"params\":{\"reason\":\"not parseable\",\"labels\":[\"needs-human\"]},\"risk\":\"low\"}],\"skip_reason\":null}\n```"}]}} +{"type":"result","subtype":"success","is_error":false} +JSONL + + run_extract "false" "$ef" + [ "$status" -eq 0 ] + + local plan + plan="$(get_output_var action-plan)" + echo "$plan" | grep -q '"reasoning":"nested"' + echo "$plan" | grep -q '"params"' +} + @test "no crash when the second jq receives empty input (regression for #81)" { # The original bug: extract returned an empty string and the canonicalize # step `jq -c . <<<"$plan"` then crashed with 'Invalid numeric literal at diff --git a/tests/workflows/reusable/test-reusable-issue.sh b/tests/workflows/reusable/test-reusable-issue.sh index c61fc2c..8b87dbf 100755 --- a/tests/workflows/reusable/test-reusable-issue.sh +++ b/tests/workflows/reusable/test-reusable-issue.sh @@ -658,7 +658,7 @@ else info "Created bug issue #${BUG_ISSUE}" # Wait for workflow - local run_id + run_id"" if run_id=$(wait_for_workflow "issue-ops.yml"); then info "Workflow run: ${run_id}" @@ -670,7 +670,7 @@ else fi # Wait for agent comment - local agent_body + agent_body if agent_body=$(wait_for_agent_comment "$BUG_ISSUE" "openci-agent-run"); then if validate_issue_plan "$(extract_plan_from_comment "$agent_body")" "bug-issue-plan"; then pass "Bug issue: valid issue-action-plan/v1 found" @@ -696,11 +696,11 @@ else _CURRENT_ISSUE="$FEATURE_ISSUE" info "Created feature request issue #${FEATURE_ISSUE}" - local run_id2 + run_id2 if run_id2=$(wait_for_workflow "issue-ops.yml"); then - local agent_body2 + agent_body2 if agent_body2=$(wait_for_agent_comment "$FEATURE_ISSUE" "openci-agent-run"); then - local plan2 + plan2 plan2=$(extract_plan_from_comment "$agent_body2") if validate_issue_plan "$plan2" "feature-plan"; then # Check for enhancement label in plan