From 4aef27c35c6ff2ed10dcb79c96fc9924d1c40f2f Mon Sep 17 00:00:00 2001 From: donpetry-bot <281750570+donpetry-bot@users.noreply.github.com> Date: Mon, 8 Jun 2026 01:02:05 +0000 Subject: [PATCH 1/5] =?UTF-8?q?feat:=20implement=20issue=20#395=20?= =?UTF-8?q?=E2=80=94=20Compliance:=20non-stub-dev-lead.yml?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/dev-lead.yml | 364 ++---------------- AGENTS.md | 7 +- .../integration/test_concurrency_config.sh | 7 +- .../integration/test_dev_lead_stub.sh | 55 +++ 4 files changed, 99 insertions(+), 334 deletions(-) create mode 100755 tests/dev-lead/integration/test_dev_lead_stub.sh diff --git a/.github/workflows/dev-lead.yml b/.github/workflows/dev-lead.yml index 36cfc1d19..15b903180 100644 --- a/.github/workflows/dev-lead.yml +++ b/.github/workflows/dev-lead.yml @@ -1,8 +1,27 @@ +# ───────────────────────────────────────────────────────────────────────────── +# Dev-Lead Agent — thin caller stub +# Standard: petry-projects/.github/standards/ci-standards.md#5-dev-lead-agent +# Reusable: petry-projects/.github-private/.github/workflows/dev-lead-reusable.yml +# +# ADOPTING THIS WORKFLOW: +# 1. Copy this file verbatim to .github/workflows/dev-lead.yml in your repo. +# 2. Ensure CLAUDE_CODE_OAUTH_TOKEN is set as an org or repo secret. +# 3. Optionally set GH_PAT_WORKFLOWS (required if Claude pushes workflow files). +# 4. Optionally set vars.DEV_LEAD_ENGINE = "claude" | "gemini" | "copilot". +# +# UNLIKE claude.yml, this file has NO OIDC byte-for-byte constraint and may be +# freely modified on PR branches to adjust triggers for repo-specific needs. +# +# REQUIRED secrets: CLAUDE_CODE_OAUTH_TOKEN +# OPTIONAL secrets: GH_PAT_WORKFLOWS, GOOGLE_API_KEY, GH_PAT +# ───────────────────────────────────────────────────────────────────────────── + name: Dev-Lead Agent on: pull_request: - types: [opened, synchronize, reopened, labeled] + branches: [main] + types: [opened, reopened, synchronize] pull_request_review: types: [submitted] pull_request_review_comment: @@ -14,339 +33,28 @@ on: check_run: types: [completed] repository_dispatch: - types: [dev-lead-ci-failure, dev-lead-reviews-retry] + types: [dev-lead-ci-failure] -permissions: - contents: write - pull-requests: write - issues: write - checks: read - statuses: read +permissions: {} concurrency: - # Per-issue and per-PR lanes so issue pickups are never cancelled by unrelated - # PR follow-up traffic. This repo's dev-lead.yml is inline (not a reusable - # caller), so it carries the same routing as dev-lead-reusable.yml. See - # petry-projects/.github#402. + # One active run per repo; ci-relay (check_run) keeps an ephemeral per-SHA slot + # so it can fire immediately without blocking or being blocked by the dispatch queue. group: >- ${{ - (github.event_name == 'check_run' && format('dev-lead-ci-relay-{0}', github.event.check_run.head_sha)) - || (github.event.pull_request.number && format('dev-lead-pr-{0}', github.event.pull_request.number)) - || (github.event.issue.pull_request && format('dev-lead-pr-{0}', github.event.issue.number)) - || (github.event.issue.number && format('dev-lead-issue-{0}', github.event.issue.number)) - || (github.event.client_payload.pr_number && format('dev-lead-pr-{0}', github.event.client_payload.pr_number)) - || format('dev-lead-run-{0}', github.run_id) + github.event_name == 'check_run' && format('dev-lead-ci-relay-{0}', github.event.check_run.head_sha) || + 'dev-lead' }} cancel-in-progress: false -env: - BOT_USER: ${{ vars.BOT_USER || 'donpetry-bot' }} - TRUSTED_BOTS: ${{ vars.TRUSTED_BOTS || 'copilot-pull-request-reviewer[bot],gemini-code-assist[bot],sonarqubecloud[bot],coderabbitai[bot],chatgpt-codex-connector[bot]' }} - TRIGGER_PHRASES: ${{ vars.TRIGGER_PHRASES || '@dev-lead' }} - DEV_LEAD_ENGINE: ${{ vars.DEV_LEAD_ENGINE || 'claude' }} - DEV_LEAD_DRY_RUN: ${{ vars.DEV_LEAD_DRY_RUN || 'false' }} - DEEP_TIMEOUT_SEC: ${{ vars.DEEP_TIMEOUT_SEC || '600' }} - ACTION_TIMEOUT_SEC: ${{ vars.ACTION_TIMEOUT_SEC || '600' }} - GH_TOKEN: ${{ secrets.GH_PAT_WORKFLOWS || secrets.GITHUB_TOKEN }} - jobs: - # ── dispatch ──────────────────────────────────────────────────────────────── - # Handles all events except check_run (which goes to ci-relay). - # 1. Runs dev-lead-intent.sh to classify the event into an intent. - # 2. Routes to the appropriate action based on intent. - dispatch: - if: github.event_name != 'check_run' - runs-on: ubuntu-latest - timeout-minutes: 30 - env: - CLAUDE_CODE_OAUTH_TOKEN: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }} - GOOGLE_API_KEY: ${{ secrets.GOOGLE_API_KEY }} - GEMINI_API_KEY: ${{ secrets.GOOGLE_API_KEY }} - GEMINI_CLI_TRUST_WORKSPACE: "true" - COPILOT_GITHUB_TOKEN: ${{ secrets.GH_PAT }} - - steps: - - name: Checkout agent repo - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 - with: - token: ${{ secrets.GH_PAT_WORKFLOWS || secrets.GITHUB_TOKEN }} - - - name: Classify event intent - id: intent - env: - GITHUB_EVENT_NAME: ${{ github.event_name }} - # GITHUB_EVENT_PATH is set automatically by GitHub Actions — no override needed - run: bash scripts/dev-lead-intent.sh - - - name: Log intent - run: | - echo "::notice::intent=$INTENT_TYPE reason=$INTENT_REASON" - - - name: Parse intent context fields - run: | - # Expand INTENT_CONTEXT JSON into individual GITHUB_ENV vars so subsequent - # step env: blocks can reference ${{ env.INTENT_PR_NUMBER }} without fromJson() - ctx="${INTENT_CONTEXT:-{\}}" - EOF_DELIMITER="EOF_$(dd if=/dev/urandom bs=15 count=1 2>/dev/null | base64 | tr -dc 'a-zA-Z0-9')" - - # Helper to set GITHUB_ENV safely with heredoc - set_env() { - echo "$1<<$EOF_DELIMITER" >> "$GITHUB_ENV" - echo "$2" >> "$GITHUB_ENV" - echo "$EOF_DELIMITER" >> "$GITHUB_ENV" - } - - set_env "INTENT_PR_NUMBER" "$(echo "$ctx" | jq -r '.pr_number // ""')" - set_env "INTENT_HEAD_SHA" "$(echo "$ctx" | jq -r '.head_sha // ""')" - set_env "INTENT_CHECKS" "$(echo "$ctx" | jq -c '.checks // []')" - set_env "INTENT_ISSUE_NUMBER" "$(echo "$ctx" | jq -r '.issue_number // ""')" - set_env "INTENT_ACTOR" "$(echo "$ctx" | jq -r '.actor // ""')" - set_env "INTENT_COMMENT_BODY" "$(echo "$ctx" | jq -r '.body // ""')" - - - name: Skip — log reason - if: env.INTENT_TYPE == 'skip' - run: | - echo "::notice::Skipping event: $INTENT_REASON" - - - name: Pre-flight checks - if: env.INTENT_TYPE != 'skip' && env.INTENT_TYPE != 'enable-auto-merge' - run: bash scripts/dev-lead-preflight.sh - - - name: Configure git identity - if: env.INTENT_TYPE != 'skip' && env.INTENT_TYPE != 'enable-auto-merge' - run: | - git config --global user.email "donpetry-bot@users.noreply.github.com" - git config --global user.name "donpetry-bot" - - # ── Install engine CLIs ─────────────────────────────────────────────── - - name: Cache claude-code CLI - if: env.INTENT_TYPE != 'skip' && env.INTENT_TYPE != 'enable-auto-merge' - uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5 - with: - path: ~/.npm-global - key: claude-code-${{ vars.CLAUDE_CODE_VERSION || 'latest' }}-${{ runner.os }} - - - name: Install engine CLIs - if: env.INTENT_TYPE != 'skip' && env.INTENT_TYPE != 'enable-auto-merge' - env: - CLAUDE_CODE_VERSION: ${{ vars.CLAUDE_CODE_VERSION || 'latest' }} - # Pass keys during install if needed for extension setup - GOOGLE_API_KEY: ${{ secrets.GOOGLE_API_KEY }} - GH_TOKEN: ${{ secrets.GH_PAT || secrets.GITHUB_TOKEN }} - run: | - set -euo pipefail - mkdir -p "$HOME/.npm-global" - npm config set prefix "$HOME/.npm-global" - echo "$HOME/.npm-global/bin" >> "$GITHUB_PATH" - export PATH="$HOME/.npm-global/bin:$PATH" - - # 1. Claude Code - if ! command -v claude >/dev/null 2>&1; then - npm install -g "@anthropic-ai/claude-code@${CLAUDE_CODE_VERSION}" - fi - - # 2. Gemini CLI (always install as fallback) - if ! command -v gemini >/dev/null 2>&1; then - npm install -g @google/gemini-cli || true - fi - - # 3. GitHub Copilot Extension (always install as fallback) - if ! gh copilot --version >/dev/null 2>&1; then - gh extension install github/gh-copilot || true - fi - - - name: Verify primary engine CLI - if: env.INTENT_TYPE != 'skip' && env.INTENT_TYPE != 'enable-auto-merge' - run: | - # Fail fast if the primary engine binary is missing — a silent exit 127 - # later would waste tokens and incorrectly count toward PR exhaustion. - case "${DEV_LEAD_ENGINE:-claude}" in - claude) claude --version ;; - gemini) gemini --version ;; - copilot) gh copilot --version ;; - *) echo "::error::Unknown DEV_LEAD_ENGINE value: '${DEV_LEAD_ENGINE}'. Must be one of: claude, gemini, copilot"; exit 1 ;; - esac - - # ── fix-ci ──────────────────────────────────────────────────────────── - - name: Run fix-ci - if: env.INTENT_TYPE == 'fix-ci' - env: - PR_NUMBER: ${{ env.INTENT_PR_NUMBER }} - HEAD_SHA: ${{ env.INTENT_HEAD_SHA }} - CHECKS_JSON: ${{ env.INTENT_CHECKS }} - REPO: ${{ github.repository }} - REVIEW_ENGINE: ${{ env.DEV_LEAD_ENGINE }} - run: | - rc=0 - bash scripts/dev-lead-fix-ci.sh || rc=$? - # exit 2 = rate-limited; script already posted a marker — not a workflow failure - [ "$rc" -eq 2 ] && exit 0 || exit "$rc" - - # ── fix-reviews ─────────────────────────────────────────────────────── - - name: Run fix-reviews - if: env.INTENT_TYPE == 'fix-reviews' - env: - PR_NUMBER: ${{ env.INTENT_PR_NUMBER }} - HEAD_SHA: ${{ env.INTENT_HEAD_SHA }} - REPO: ${{ github.repository }} - REVIEW_ENGINE: ${{ env.DEV_LEAD_ENGINE }} - TRIGGERING_REVIEWER: ${{ env.INTENT_ACTOR }} - run: | - rc=0 - bash scripts/dev-lead-fix-reviews.sh || rc=$? - [ "$rc" -eq 2 ] && exit 0 || exit "$rc" - - # ── fix-bot-comment ─────────────────────────────────────────────────── - - name: Run fix-bot-comment - if: env.INTENT_TYPE == 'fix-bot-comment' - env: - PR_NUMBER: ${{ env.INTENT_PR_NUMBER }} - HEAD_SHA: ${{ env.INTENT_HEAD_SHA }} - ACTOR: ${{ env.INTENT_ACTOR }} - COMMENT_BODY: ${{ env.INTENT_COMMENT_BODY }} - REPO: ${{ github.repository }} - REVIEW_ENGINE: ${{ env.DEV_LEAD_ENGINE }} - run: | - rc=0 - bash scripts/dev-lead-fix-reviews.sh || rc=$? - [ "$rc" -eq 2 ] && exit 0 || exit "$rc" - - # ── on-mention ──────────────────────────────────────────────────────── - - name: Run on-mention - if: env.INTENT_TYPE == 'on-mention' - env: - PR_NUMBER: ${{ env.INTENT_PR_NUMBER }} - HEAD_SHA: ${{ env.INTENT_HEAD_SHA }} - ACTOR: ${{ env.INTENT_ACTOR }} - USER_INSTRUCTION: ${{ env.INTENT_COMMENT_BODY }} - REPO: ${{ github.repository }} - REVIEW_ENGINE: ${{ env.DEV_LEAD_ENGINE }} - run: | - rc=0 - bash scripts/dev-lead-fix-reviews.sh || rc=$? - [ "$rc" -eq 2 ] && exit 0 || exit "$rc" - - # ── review-changes ──────────────────────────────────────────────────── - - name: Run review-changes - if: env.INTENT_TYPE == 'review-changes' - env: - PR_NUMBER: ${{ env.INTENT_PR_NUMBER }} - HEAD_SHA: ${{ env.INTENT_HEAD_SHA }} - ACTOR: ${{ env.INTENT_ACTOR }} - REPO: ${{ github.repository }} - REVIEW_ENGINE: ${{ env.DEV_LEAD_ENGINE }} - run: | - rc=0 - bash scripts/dev-lead-fix-reviews.sh || rc=$? - [ "$rc" -eq 2 ] && exit 0 || exit "$rc" - - # ── issue ───────────────────────────────────────────────────────────── - - name: Run issue - if: env.INTENT_TYPE == 'issue' - env: - ISSUE_NUMBER: ${{ env.INTENT_ISSUE_NUMBER }} - REPO: ${{ github.repository }} - REVIEW_ENGINE: ${{ env.DEV_LEAD_ENGINE }} - run: | - rc=0 - bash scripts/dev-lead-fix-issue.sh || rc=$? - [ "$rc" -eq 2 ] && exit 0 || exit "$rc" - - # ── rebase ──────────────────────────────────────────────────────────── - - name: Run rebase - if: env.INTENT_TYPE == 'rebase' - env: - PR_NUMBER: ${{ env.INTENT_PR_NUMBER }} - REPO: ${{ github.repository }} - REVIEW_ENGINE: ${{ env.DEV_LEAD_ENGINE }} - run: | - rc=0 - bash scripts/dev-lead-fix-reviews.sh || rc=$? - [ "$rc" -eq 2 ] && exit 0 || exit "$rc" - - # ── enable-auto-merge ───────────────────────────────────────────────── - # Triggered when a trusted bot approves a PR. Skips engine install and - # pre-flight — just checks reviewDecision and enables auto-merge if APPROVED. - # - # Gate on the step output of `intent` (not env.INTENT_TYPE): a step's own - # env block is in scope when evaluating its `if:`, so the literal - # `INTENT_TYPE: enable-auto-merge` we set below would make env.INTENT_TYPE - # always equal 'enable-auto-merge' at gate-evaluation time, defeating the - # check. Step outputs are not shadowed. Also require a non-empty PR number - # to mirror the guard already present in dev-lead-reusable.yml. - - name: Enable auto-merge on bot approval - if: >- - steps.intent.outputs.intent_type == 'enable-auto-merge' - && env.INTENT_PR_NUMBER != '' - env: - INTENT_TYPE: enable-auto-merge - PR_NUMBER: ${{ env.INTENT_PR_NUMBER }} - HEAD_SHA: ${{ env.INTENT_HEAD_SHA }} - REPO: ${{ github.repository }} - run: bash scripts/dev-lead-fix-reviews.sh - - # ── ci-relay ──────────────────────────────────────────────────────────────── - # Handles check_run completed events only. - # No LLM involved — resolves the PR number from the check and fires - # a repository_dispatch so the dispatch job can handle it asynchronously. - ci-relay: - if: >- - github.event_name == 'check_run' && - github.event.action == 'completed' && - github.event.check_run.conclusion == 'failure' && - !startsWith(github.event.check_run.name, 'dev-lead / ') - runs-on: ubuntu-latest - timeout-minutes: 5 - - steps: - - name: Checkout agent repo - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 - - - name: Check for associated non-fork PR - id: check-pr - env: - CHECK_RUN_PRS: ${{ toJson(github.event.check_run.pull_requests) }} - CHECK_RUN_HEAD_SHA: ${{ github.event.check_run.head_sha }} - REPO_FULL_NAME: ${{ github.repository }} - GH_TOKEN: ${{ secrets.GH_PAT_WORKFLOWS || secrets.GITHUB_TOKEN }} - run: bash scripts/dev-lead-ci-relay.sh - - - name: Emit repository_dispatch dev-lead-ci-failure - if: steps.check-pr.outputs.should_relay == 'true' - env: - GH_TOKEN: ${{ secrets.GH_PAT_WORKFLOWS || secrets.GITHUB_TOKEN }} - # Move all event-sourced values to env vars to prevent script injection - RELAY_PR_NUMBER: ${{ steps.check-pr.outputs.pr_number }} - RELAY_HEAD_SHA: ${{ github.event.check_run.head_sha }} - RELAY_REPO: ${{ github.repository }} - RELAY_CHECK_NAME: ${{ github.event.check_run.name }} - RELAY_CHECK_ID: ${{ github.event.check_run.id }} - RELAY_CONCLUSION: ${{ github.event.check_run.conclusion }} - RELAY_DETAILS_URL: ${{ github.event.check_run.details_url }} - RELAY_APP_SLUG: ${{ github.event.check_run.app.slug }} - run: | - set -euo pipefail - # Build a proper nested JSON payload via jq to avoid shell injection - payload=$(jq -n \ - --argjson pr_number "$RELAY_PR_NUMBER" \ - --arg head_sha "$RELAY_HEAD_SHA" \ - --arg repo "$RELAY_REPO" \ - --arg name "$RELAY_CHECK_NAME" \ - --argjson check_id "$RELAY_CHECK_ID" \ - --arg conclusion "$RELAY_CONCLUSION" \ - --arg details_url "$RELAY_DETAILS_URL" \ - --arg app_slug "$RELAY_APP_SLUG" \ - '{ - event_type: "dev-lead-ci-failure", - client_payload: { - pr_number: $pr_number, - head_sha: $head_sha, - repo: $repo, - checks: [{id: $check_id, name: $name, conclusion: $conclusion, details_url: $details_url, app_slug: $app_slug}] - } - }') - echo "$payload" | gh api \ - --method POST \ - "repos/$RELAY_REPO/dispatches" \ - --input - - echo "::notice::Relayed CI failure for PR $RELAY_PR_NUMBER — dispatched dev-lead-ci-failure" + dev-lead: + uses: petry-projects/.github-private/.github/workflows/dev-lead-reusable.yml@main + secrets: inherit + permissions: + contents: write + pull-requests: write + issues: write + actions: read + checks: read + statuses: read diff --git a/AGENTS.md b/AGENTS.md index 2bd2e0f7a..306ba29fa 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -21,9 +21,10 @@ This is the `.github-private` org infrastructure repo for `petry-projects`. It c - Do **not** modify `.github/workflows/agent-shield.yml` — this is exempted from agent modification per [`standards/agent-standards.md`](https://github.com/petry-projects/.github/blob/main/standards/agent-standards.md). -- `.github/workflows/dev-lead.yml` is this repo's primary AI automation workflow (replaced `claude.yml` 2026-05). - It runs inline steps directly. To change behavior for this repo only, edit `dev-lead.yml`. - To change behavior across all org repos (via the cross-repo reusable), edit `dev-lead-reusable.yml`. +- `.github/workflows/dev-lead.yml` is a thin caller stub that delegates to + `dev-lead-reusable.yml` (the canonical org standard). To change behavior for + all org repos, edit `dev-lead-reusable.yml`. Repo-specific trigger adjustments + may be made to `dev-lead.yml` per the stub's header comment. - All other workflow changes must use templates from [`standards/workflows/`](https://github.com/petry-projects/.github/tree/main/standards/workflows) verbatim. - **Exception:** The `gh-aw-compile` job in `lint.yml` is a documented repo-specific addition that gates diff --git a/tests/dev-lead/integration/test_concurrency_config.sh b/tests/dev-lead/integration/test_concurrency_config.sh index 3d546f983..9c89efce6 100644 --- a/tests/dev-lead/integration/test_concurrency_config.sh +++ b/tests/dev-lead/integration/test_concurrency_config.sh @@ -60,10 +60,11 @@ check_concurrency_order() { fi } -# Both the inline caller workflow and the reusable workflow must carry the same -# routing, so external callers get identical per-PR serialization. +# dev-lead.yml is now a thin caller stub; per-PR/per-issue routing is +# centralised in the reusable. Only validate the reusable here. +# The stub's own structure (calls reusable, no inline steps) is validated by +# tests/dev-lead/integration/test_dev_lead_stub.sh. WORKFLOWS=( - ".github/workflows/dev-lead.yml" ".github/workflows/dev-lead-reusable.yml" ) diff --git a/tests/dev-lead/integration/test_dev_lead_stub.sh b/tests/dev-lead/integration/test_dev_lead_stub.sh new file mode 100755 index 000000000..35583afcf --- /dev/null +++ b/tests/dev-lead/integration/test_dev_lead_stub.sh @@ -0,0 +1,55 @@ +#!/usr/bin/env bash +# Validates that dev-lead.yml is a thin caller stub, not an inline workflow. +# +# The org standard (standards/ci-standards.md#5-dev-lead-agent) requires +# dev-lead.yml to delegate all logic to dev-lead-reusable.yml via a `uses:` +# call. An inline copy drifts independently and cannot benefit from central +# fixes to the reusable. +# +# Regression guard: compliance audit check `non-stub-dev-lead.yml` (issue #395) +# was filed because dev-lead.yml had diverged into a full inline workflow. +set -euo pipefail + +WORKFLOW=".github/workflows/dev-lead.yml" +FAIL=0 + +pass() { echo "PASS: $*"; } +fail() { echo "FAIL: $*"; FAIL=$((FAIL + 1)); } + +# 1. File must exist. +if [ ! -f "$WORKFLOW" ]; then + echo "ERROR: $WORKFLOW not found" + exit 1 +fi + +# 2. Must contain a `uses:` line that calls dev-lead-reusable.yml@main. +# This is the structural signature of a thin caller stub. +if grep -qF 'dev-lead-reusable.yml@main' "$WORKFLOW"; then + pass "dev-lead.yml delegates to dev-lead-reusable.yml@main" +else + fail "dev-lead.yml does not call dev-lead-reusable.yml@main — it must be a thin caller stub" +fi + +# 3. Must NOT contain inline job steps (run: blocks with bash scripts). +# A thin caller stub has no run: steps; all logic lives in the reusable. +if grep -qE '^\s+run:' "$WORKFLOW"; then + fail "dev-lead.yml contains inline run: steps — expected a thin caller stub with no inline steps" +else + pass "dev-lead.yml contains no inline run: steps (thin caller confirmed)" +fi + +# 4. Must have `permissions: {}` at workflow level (zero standing permissions; +# the reusable job declares its own per-job permissions). +if grep -qE '^permissions:\s*\{\}' "$WORKFLOW"; then + pass "dev-lead.yml has workflow-level permissions: {}" +else + fail "dev-lead.yml must have 'permissions: {}' at workflow level (reusable job owns its permissions)" +fi + +echo "" +if [ "$FAIL" -eq 0 ]; then + echo "All stub-structure checks passed." +else + echo "$FAIL check(s) failed." + exit 1 +fi From f843380a4a4182726713de1e9f8484fe4b64b68b Mon Sep 17 00:00:00 2001 From: donpetry-bot <281750570+donpetry-bot@users.noreply.github.com> Date: Mon, 8 Jun 2026 05:10:08 +0000 Subject: [PATCH 2/5] chore: apply manual instructions [skip ci-relay] --- .github/workflows/dev-lead-reusable.yml | 32 ++++++++++++++++--- .github/workflows/dev-lead.yml | 15 ++++++--- .../integration/test_concurrency_config.sh | 10 +++--- .../integration/test_dev_lead_stub.sh | 2 +- 4 files changed, 45 insertions(+), 14 deletions(-) diff --git a/.github/workflows/dev-lead-reusable.yml b/.github/workflows/dev-lead-reusable.yml index 966f4555e..fc4f56bdb 100644 --- a/.github/workflows/dev-lead-reusable.yml +++ b/.github/workflows/dev-lead-reusable.yml @@ -322,20 +322,42 @@ jobs: runs-on: ubuntu-latest timeout-minutes: 5 permissions: - contents: read + contents: write steps: - name: Check for associated non-fork PR id: check-pr env: + GH_TOKEN: ${{ secrets.GH_PAT_WORKFLOWS || github.token }} CHECK_RUN_PRS: ${{ toJson(github.event.check_run.pull_requests) }} + CHECK_RUN_HEAD_SHA: ${{ github.event.check_run.head_sha }} REPO_FULL_NAME: ${{ github.repository }} run: | set -euo pipefail pr_count=$(echo "$CHECK_RUN_PRS" | jq 'length') if [ "$pr_count" -eq 0 ]; then - echo "::notice::check_run has no associated PRs — skipping relay" - echo "should_relay=false" >> "$GITHUB_OUTPUT" + # GitHub App check_runs (SonarCloud, external CodeQL gates, etc.) do not + # populate check_run.pull_requests. Fall back to the commits-to-pulls API. + echo "::notice::check_run.pull_requests is empty — trying commits-to-pulls API fallback for SHA ${CHECK_RUN_HEAD_SHA}" + pulls_json=$(gh api \ + -H "Accept: application/vnd.github+json" \ + "repos/${REPO_FULL_NAME}/commits/${CHECK_RUN_HEAD_SHA}/pulls" 2>/dev/null || echo "[]") + open_pulls=$(echo "$pulls_json" | jq '[.[] | select(.state=="open")]') + pr_count=$(echo "$open_pulls" | jq 'length') + if [ "$pr_count" -eq 0 ]; then + echo "::notice::No open PRs found for commit ${CHECK_RUN_HEAD_SHA} — skipping relay" + echo "should_relay=false" >> "$GITHUB_OUTPUT" + exit 0 + fi + head_repo_full=$(echo "$open_pulls" | jq -r '.[0].head.repo.full_name // empty') + if [ -n "$head_repo_full" ] && [ "$head_repo_full" != "$REPO_FULL_NAME" ]; then + echo "::notice::Commit ${CHECK_RUN_HEAD_SHA} belongs to fork (${head_repo_full}) — skipping relay" + echo "should_relay=false" >> "$GITHUB_OUTPUT" + exit 0 + fi + pr_number=$(echo "$open_pulls" | jq -r '.[0].number') + echo "should_relay=true" >> "$GITHUB_OUTPUT" + echo "pr_number=$pr_number" >> "$GITHUB_OUTPUT" exit 0 fi head_repo_url=$(echo "$CHECK_RUN_PRS" | jq -r '.[0].head.repo.url // empty') @@ -356,6 +378,7 @@ jobs: RELAY_PR_NUMBER: ${{ steps.check-pr.outputs.pr_number }} RELAY_HEAD_SHA: ${{ github.event.check_run.head_sha }} RELAY_REPO: ${{ github.repository }} + RELAY_CHECK_ID: ${{ github.event.check_run.id }} RELAY_CHECK_NAME: ${{ github.event.check_run.name }} RELAY_CONCLUSION: ${{ github.event.check_run.conclusion }} RELAY_DETAILS_URL: ${{ github.event.check_run.details_url }} @@ -366,11 +389,12 @@ jobs: --argjson pr_number "$RELAY_PR_NUMBER" \ --arg head_sha "$RELAY_HEAD_SHA" \ --arg repo "$RELAY_REPO" \ + --argjson check_id "$RELAY_CHECK_ID" \ --arg name "$RELAY_CHECK_NAME" \ --arg conclusion "$RELAY_CONCLUSION" \ --arg details_url "$RELAY_DETAILS_URL" \ --arg app_slug "$RELAY_APP_SLUG" \ - '{event_type:"dev-lead-ci-failure",client_payload:{pr_number:$pr_number,head_sha:$head_sha,repo:$repo,checks:[{name:$name,conclusion:$conclusion,details_url:$details_url,app_slug:$app_slug}]}}') + '{event_type:"dev-lead-ci-failure",client_payload:{pr_number:$pr_number,head_sha:$head_sha,repo:$repo,checks:[{id:$check_id,name:$name,conclusion:$conclusion,details_url:$details_url,app_slug:$app_slug}]}}') echo "$payload" | gh api \ --method POST "repos/$RELAY_REPO/dispatches" --input - echo "::notice::Relayed CI failure for PR $RELAY_PR_NUMBER" diff --git a/.github/workflows/dev-lead.yml b/.github/workflows/dev-lead.yml index 15b903180..4d7696331 100644 --- a/.github/workflows/dev-lead.yml +++ b/.github/workflows/dev-lead.yml @@ -33,17 +33,22 @@ on: check_run: types: [completed] repository_dispatch: - types: [dev-lead-ci-failure] + types: [dev-lead-ci-failure, dev-lead-reviews-retry] permissions: {} concurrency: - # One active run per repo; ci-relay (check_run) keeps an ephemeral per-SHA slot - # so it can fire immediately without blocking or being blocked by the dispatch queue. + # Per-PR/per-issue lanes mirror the routing in dev-lead-reusable.yml so that + # unrelated PRs/issues are never blocked or cancelled by each other at the + # workflow level (the outer gate) before the reusable's inner concurrency applies. group: >- ${{ - github.event_name == 'check_run' && format('dev-lead-ci-relay-{0}', github.event.check_run.head_sha) || - 'dev-lead' + (github.event_name == 'check_run' && format('dev-lead-ci-relay-{0}', github.event.check_run.head_sha)) + || (github.event.pull_request.number && format('dev-lead-pr-{0}', github.event.pull_request.number)) + || (github.event.issue.pull_request && format('dev-lead-pr-{0}', github.event.issue.number)) + || (github.event.issue.number && format('dev-lead-issue-{0}', github.event.issue.number)) + || (github.event.client_payload.pr_number && format('dev-lead-pr-{0}', github.event.client_payload.pr_number)) + || format('dev-lead-run-{0}', github.run_id) }} cancel-in-progress: false diff --git a/tests/dev-lead/integration/test_concurrency_config.sh b/tests/dev-lead/integration/test_concurrency_config.sh index 9c89efce6..af95595c7 100644 --- a/tests/dev-lead/integration/test_concurrency_config.sh +++ b/tests/dev-lead/integration/test_concurrency_config.sh @@ -60,11 +60,13 @@ check_concurrency_order() { fi } -# dev-lead.yml is now a thin caller stub; per-PR/per-issue routing is -# centralised in the reusable. Only validate the reusable here. -# The stub's own structure (calls reusable, no inline steps) is validated by -# tests/dev-lead/integration/test_dev_lead_stub.sh. +# Both dev-lead.yml (caller stub) and dev-lead-reusable.yml must have per-PR/ +# per-issue concurrency groups. The stub's outer-gate concurrency must match +# the reusable's inner routing — if the stub uses a single 'dev-lead' group, +# unrelated-PR pending runs can cancel each other before the reusable applies +# its own per-PR lanes (see thread PRRT_kwDOR9SdIs6Hs_bn). WORKFLOWS=( + ".github/workflows/dev-lead.yml" ".github/workflows/dev-lead-reusable.yml" ) diff --git a/tests/dev-lead/integration/test_dev_lead_stub.sh b/tests/dev-lead/integration/test_dev_lead_stub.sh index 35583afcf..55601e994 100755 --- a/tests/dev-lead/integration/test_dev_lead_stub.sh +++ b/tests/dev-lead/integration/test_dev_lead_stub.sh @@ -40,7 +40,7 @@ fi # 4. Must have `permissions: {}` at workflow level (zero standing permissions; # the reusable job declares its own per-job permissions). -if grep -qE '^permissions:\s*\{\}' "$WORKFLOW"; then +if grep -qE '^permissions:\s*\{\s*\}' "$WORKFLOW"; then pass "dev-lead.yml has workflow-level permissions: {}" else fail "dev-lead.yml must have 'permissions: {}' at workflow level (reusable job owns its permissions)" From 3290acdce07a91ef268c9a81d31f41362585402e Mon Sep 17 00:00:00 2001 From: donpetry-bot <281750570+donpetry-bot@users.noreply.github.com> Date: Tue, 9 Jun 2026 03:38:07 +0000 Subject: [PATCH 3/5] fix(bot): address bot feedback [skip ci-relay] --- .github/workflows/dev-lead-reusable.yml | 52 ++++++++++++++++++++----- .github/workflows/dev-lead.yml | 21 +++------- .github/workflows/test-dev-lead.yml | 9 +++++ 3 files changed, 57 insertions(+), 25 deletions(-) diff --git a/.github/workflows/dev-lead-reusable.yml b/.github/workflows/dev-lead-reusable.yml index fc4f56bdb..bb5ab6d85 100644 --- a/.github/workflows/dev-lead-reusable.yml +++ b/.github/workflows/dev-lead-reusable.yml @@ -78,6 +78,8 @@ jobs: TRIGGER_PHRASES: ${{ vars.TRIGGER_PHRASES || '@dev-lead' }} DEV_LEAD_ENGINE: ${{ vars.DEV_LEAD_ENGINE || 'claude' }} DEV_LEAD_DRY_RUN: ${{ vars.DEV_LEAD_DRY_RUN || 'false' }} + DEEP_TIMEOUT_SEC: ${{ vars.DEEP_TIMEOUT_SEC || '' }} + ACTION_TIMEOUT_SEC: ${{ vars.ACTION_TIMEOUT_SEC || '' }} GH_TOKEN: ${{ secrets.GH_PAT_WORKFLOWS || github.token }} GEMINI_CLI_TRUST_WORKSPACE: "true" DEV_LEAD_SCRIPTS: .dev-lead/scripts @@ -105,8 +107,11 @@ jobs: prompts/dev-lead - name: Protect dev-lead scripts from accidental git staging + run: echo ".dev-lead/" >> .gitignore + + - name: Revert .gitignore changes before git operations run: | - mkdir -p .git/info + git checkout .gitignore || true echo ".dev-lead/" >> .git/info/exclude - name: Classify event intent @@ -131,6 +136,7 @@ jobs: echo "INTENT_CHECKS=$(echo "$ctx" | jq -c '.checks // []')" >> "$GITHUB_ENV" echo "INTENT_ISSUE_NUMBER=$(echo "$ctx" | jq -r '.issue_number // ""')" >> "$GITHUB_ENV" echo "INTENT_ACTOR=$(echo "$ctx" | jq -r '.actor // ""')" >> "$GITHUB_ENV" + echo "INTENT_BODY=$(echo "$ctx" | jq -r '.body // ""')" >> "$GITHUB_ENV" - name: Skip — log reason if: env.INTENT_TYPE == 'skip' @@ -177,7 +183,10 @@ jobs: gh extension install github/gh-copilot || true fi ;; gemini) - npm install -g @google/gemini-cli || true ;; + npm install -g @google/gemini-cli || true + if ! gh copilot --version >/dev/null 2>&1; then + gh extension install github/gh-copilot || true + fi ;; copilot) npm install -g @google/gemini-cli || true if ! gh copilot --version >/dev/null 2>&1; then @@ -199,7 +208,10 @@ jobs: GEMINI_API_KEY: ${{ secrets.GOOGLE_API_KEY }} COPILOT_GITHUB_TOKEN: ${{ secrets.GH_PAT }} PROMPTS_DIR: .dev-lead/prompts/dev-lead - run: bash .dev-lead/scripts/dev-lead-fix-ci.sh + run: | + bash .dev-lead/scripts/dev-lead-fix-ci.sh || rc=$? + [ "${rc:-0}" -eq 2 ] && exit 0 + exit "${rc:-0}" # ── fix-reviews ─────────────────────────────────────────────────────── - name: Run fix-reviews @@ -215,7 +227,10 @@ jobs: GEMINI_API_KEY: ${{ secrets.GOOGLE_API_KEY }} COPILOT_GITHUB_TOKEN: ${{ secrets.GH_PAT }} PROMPTS_DIR: .dev-lead/prompts/dev-lead - run: bash .dev-lead/scripts/dev-lead-fix-reviews.sh + run: | + bash .dev-lead/scripts/dev-lead-fix-reviews.sh || rc=$? + [ "${rc:-0}" -eq 2 ] && exit 0 + exit "${rc:-0}" # ── fix-bot-comment ─────────────────────────────────────────────────── - name: Run fix-bot-comment @@ -225,12 +240,16 @@ jobs: HEAD_SHA: ${{ env.INTENT_HEAD_SHA }} REPO: ${{ github.repository }} REVIEW_ENGINE: ${{ env.DEV_LEAD_ENGINE }} + COMMENT_BODY: ${{ env.INTENT_BODY }} CLAUDE_CODE_OAUTH_TOKEN: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }} GOOGLE_API_KEY: ${{ secrets.GOOGLE_API_KEY }} GEMINI_API_KEY: ${{ secrets.GOOGLE_API_KEY }} COPILOT_GITHUB_TOKEN: ${{ secrets.GH_PAT }} PROMPTS_DIR: .dev-lead/prompts/dev-lead - run: bash .dev-lead/scripts/dev-lead-fix-reviews.sh + run: | + bash .dev-lead/scripts/dev-lead-fix-reviews.sh || rc=$? + [ "${rc:-0}" -eq 2 ] && exit 0 + exit "${rc:-0}" # ── on-mention ───────────────────────────────────────────────────────────── - name: Run on-mention @@ -240,12 +259,16 @@ jobs: HEAD_SHA: ${{ env.INTENT_HEAD_SHA }} REPO: ${{ github.repository }} REVIEW_ENGINE: ${{ env.DEV_LEAD_ENGINE }} + USER_INSTRUCTION: ${{ env.INTENT_BODY }} CLAUDE_CODE_OAUTH_TOKEN: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }} GOOGLE_API_KEY: ${{ secrets.GOOGLE_API_KEY }} GEMINI_API_KEY: ${{ secrets.GOOGLE_API_KEY }} COPILOT_GITHUB_TOKEN: ${{ secrets.GH_PAT }} PROMPTS_DIR: .dev-lead/prompts/dev-lead - run: bash .dev-lead/scripts/dev-lead-fix-reviews.sh + run: | + bash .dev-lead/scripts/dev-lead-fix-reviews.sh || rc=$? + [ "${rc:-0}" -eq 2 ] && exit 0 + exit "${rc:-0}" # ── review-changes ────────────────────────────────────────────────────────── - name: Run review-changes @@ -255,12 +278,16 @@ jobs: HEAD_SHA: ${{ env.INTENT_HEAD_SHA }} REPO: ${{ github.repository }} REVIEW_ENGINE: ${{ env.DEV_LEAD_ENGINE }} + ACTOR: ${{ env.INTENT_ACTOR }} CLAUDE_CODE_OAUTH_TOKEN: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }} GOOGLE_API_KEY: ${{ secrets.GOOGLE_API_KEY }} GEMINI_API_KEY: ${{ secrets.GOOGLE_API_KEY }} COPILOT_GITHUB_TOKEN: ${{ secrets.GH_PAT }} PROMPTS_DIR: .dev-lead/prompts/dev-lead - run: bash .dev-lead/scripts/dev-lead-fix-reviews.sh + run: | + bash .dev-lead/scripts/dev-lead-fix-reviews.sh || rc=$? + [ "${rc:-0}" -eq 2 ] && exit 0 + exit "${rc:-0}" # ── issue ───────────────────────────────────────────────────────────── - name: Run issue @@ -274,7 +301,10 @@ jobs: GEMINI_API_KEY: ${{ secrets.GOOGLE_API_KEY }} COPILOT_GITHUB_TOKEN: ${{ secrets.GH_PAT }} PROMPTS_DIR: .dev-lead/prompts/dev-lead - run: bash .dev-lead/scripts/dev-lead-fix-issue.sh + run: | + bash .dev-lead/scripts/dev-lead-fix-issue.sh || rc=$? + [ "${rc:-0}" -eq 2 ] && exit 0 + exit "${rc:-0}" # ── rebase ──────────────────────────────────────────────────────────── - name: Run rebase @@ -288,7 +318,10 @@ jobs: GEMINI_API_KEY: ${{ secrets.GOOGLE_API_KEY }} COPILOT_GITHUB_TOKEN: ${{ secrets.GH_PAT }} PROMPTS_DIR: .dev-lead/prompts/dev-lead - run: bash .dev-lead/scripts/dev-lead-fix-reviews.sh + run: | + bash .dev-lead/scripts/dev-lead-fix-reviews.sh || rc=$? + [ "${rc:-0}" -eq 2 ] && exit 0 + exit "${rc:-0}" # ── enable-auto-merge ───────────────────────────────────────────────── # Triggered when a trusted bot approves a PR. Skips engine install and @@ -323,6 +356,7 @@ jobs: timeout-minutes: 5 permissions: contents: write + pull-requests: read steps: - name: Check for associated non-fork PR diff --git a/.github/workflows/dev-lead.yml b/.github/workflows/dev-lead.yml index 4d7696331..9f53378ae 100644 --- a/.github/workflows/dev-lead.yml +++ b/.github/workflows/dev-lead.yml @@ -20,7 +20,6 @@ name: Dev-Lead Agent on: pull_request: - branches: [main] types: [opened, reopened, synchronize] pull_request_review: types: [submitted] @@ -37,23 +36,13 @@ on: permissions: {} -concurrency: - # Per-PR/per-issue lanes mirror the routing in dev-lead-reusable.yml so that - # unrelated PRs/issues are never blocked or cancelled by each other at the - # workflow level (the outer gate) before the reusable's inner concurrency applies. - group: >- - ${{ - (github.event_name == 'check_run' && format('dev-lead-ci-relay-{0}', github.event.check_run.head_sha)) - || (github.event.pull_request.number && format('dev-lead-pr-{0}', github.event.pull_request.number)) - || (github.event.issue.pull_request && format('dev-lead-pr-{0}', github.event.issue.number)) - || (github.event.issue.number && format('dev-lead-issue-{0}', github.event.issue.number)) - || (github.event.client_payload.pr_number && format('dev-lead-pr-{0}', github.event.client_payload.pr_number)) - || format('dev-lead-run-{0}', github.run_id) - }} - cancel-in-progress: false - jobs: dev-lead: + # Skip fork PRs: secrets are not available and dev-lead-intent.sh cannot + # run to classify the skip, which would cause the required check to fail. + if: >- + github.event_name != 'pull_request' || + github.event.pull_request.head.repo.full_name == github.repository uses: petry-projects/.github-private/.github/workflows/dev-lead-reusable.yml@main secrets: inherit permissions: diff --git a/.github/workflows/test-dev-lead.yml b/.github/workflows/test-dev-lead.yml index 74a799320..1abe0adfb 100644 --- a/.github/workflows/test-dev-lead.yml +++ b/.github/workflows/test-dev-lead.yml @@ -109,3 +109,12 @@ jobs: run: python3 -m pip install --quiet pyyaml - name: Validate issue-triage-runner.yml top-level permissions declaration run: python3 tests/dev-lead/integration/test_workflow_toplevel_permissions.py + + stub-structure: + runs-on: ubuntu-latest + permissions: + contents: read + steps: + - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 + - name: Validate dev-lead.yml is a thin caller stub + run: bash tests/dev-lead/integration/test_dev_lead_stub.sh From 9c2df12cd4202d8e80844ead6fab9552e8c76ac2 Mon Sep 17 00:00:00 2001 From: donpetry-bot <281750570+donpetry-bot@users.noreply.github.com> Date: Tue, 9 Jun 2026 03:42:45 +0000 Subject: [PATCH 4/5] chore: apply manual instructions [skip ci-relay] --- .../integration/test_concurrency_config.sh | 20 +++++++++++++------ 1 file changed, 14 insertions(+), 6 deletions(-) diff --git a/tests/dev-lead/integration/test_concurrency_config.sh b/tests/dev-lead/integration/test_concurrency_config.sh index af95595c7..91ecf7a68 100644 --- a/tests/dev-lead/integration/test_concurrency_config.sh +++ b/tests/dev-lead/integration/test_concurrency_config.sh @@ -60,16 +60,24 @@ check_concurrency_order() { fi } -# Both dev-lead.yml (caller stub) and dev-lead-reusable.yml must have per-PR/ -# per-issue concurrency groups. The stub's outer-gate concurrency must match -# the reusable's inner routing — if the stub uses a single 'dev-lead' group, -# unrelated-PR pending runs can cancel each other before the reusable applies -# its own per-PR lanes (see thread PRRT_kwDOR9SdIs6Hs_bn). +# dev-lead-reusable.yml is the single source of truth for concurrency (#450). +# Caller stubs (dev-lead.yml) must NOT carry their own concurrency block — if +# they did, the stub's outer group would take precedence and break per-PR/issue +# routing before the reusable's concurrency could apply. All concurrency checks +# therefore target only dev-lead-reusable.yml. WORKFLOWS=( - ".github/workflows/dev-lead.yml" ".github/workflows/dev-lead-reusable.yml" ) +# Guard: dev-lead.yml must NOT declare a top-level concurrency block. +STUB=".github/workflows/dev-lead.yml" +if grep -q '^concurrency:' "$STUB"; then + echo "FAIL [no-stub-concurrency]: $STUB must not have a top-level concurrency block (reusable is the source of truth)" + FAIL=$((FAIL + 1)) +else + echo "PASS [no-stub-concurrency]: $STUB correctly has no top-level concurrency block" +fi + for wf in "${WORKFLOWS[@]}"; do # Per-PR lane → serializes all runs touching one PR (PR, review, review # comment, issue_comment-on-PR, and repository_dispatch all map here). From 7458914b9529d6104376feadccd4bb28d2bacf2d Mon Sep 17 00:00:00 2001 From: donpetry-bot <281750570+donpetry-bot@users.noreply.github.com> Date: Wed, 10 Jun 2026 02:48:54 +0000 Subject: [PATCH 5/5] fix(bot): address bot feedback [skip ci-relay] --- .../unit/test_concurrency_config.bats | 40 ++++++++----------- 1 file changed, 17 insertions(+), 23 deletions(-) diff --git a/tests/dev-lead/unit/test_concurrency_config.bats b/tests/dev-lead/unit/test_concurrency_config.bats index 554ab38e6..347094840 100644 --- a/tests/dev-lead/unit/test_concurrency_config.bats +++ b/tests/dev-lead/unit/test_concurrency_config.bats @@ -59,46 +59,40 @@ YAML } _write_proper_inline() { + # Proper thin-caller stub: no top-level concurrency block (reusable owns it). cat > "$WORK_DIR/.github/workflows/dev-lead.yml" << 'YAML' name: Dev-Lead Agent on: issues: types: [labeled] -permissions: - contents: write -concurrency: - group: >- - ${{ - (github.event_name == 'check_run' && format('dev-lead-ci-relay-{0}', github.event.check_run.head_sha)) - || (github.event.pull_request.number && format('dev-lead-pr-{0}', github.event.pull_request.number)) - || (github.event.issue.pull_request && format('dev-lead-pr-{0}', github.event.issue.number)) - || (github.event.issue.number && format('dev-lead-issue-{0}', github.event.issue.number)) - || (github.event.client_payload.pr_number && format('dev-lead-pr-{0}', github.event.client_payload.pr_number)) - || format('dev-lead-run-{0}', github.run_id) - }} - cancel-in-progress: false +permissions: {} jobs: - dispatch: - runs-on: ubuntu-latest - steps: - - run: echo ok + dev-lead: + uses: petry-projects/.github-private/.github/workflows/dev-lead-reusable.yml@main + secrets: inherit YAML } # ── silent-exit regression tests ───────────────────────────────────────────── -@test "concurrency-config: exit 1 with FAIL output when dev-lead.yml has no concurrency block" { - # Stub caller — intentionally no concurrency block at all +@test "concurrency-config: exit 1 with FAIL output when dev-lead.yml has a top-level concurrency block" { + # Inline caller — carries its own concurrency block, violating the stub-only rule; + # the reusable is the single source of truth for concurrency config. cat > "$WORK_DIR/.github/workflows/dev-lead.yml" << 'YAML' name: Dev-Lead Agent on: issues: types: [labeled] -permissions: {} +permissions: + contents: write +concurrency: + group: dev-lead-pr-123 + cancel-in-progress: false jobs: - dev-lead: - uses: petry-projects/.github-private/.github/workflows/dev-lead-reusable.yml@main - secrets: inherit + dispatch: + runs-on: ubuntu-latest + steps: + - run: echo ok YAML _write_proper_reusable