From 88f6a3f40b39d7fd75c8b2c859c4b32ded6fcb57 Mon Sep 17 00:00:00 2001 From: Peter Brightwell Date: Fri, 13 Mar 2026 14:43:26 +0000 Subject: [PATCH 1/8] No multiline outputs, no EOF errors Signed-off-by: Peter Brightwell --- .github/workflows/link_requirement.yml | 232 ++++++++++--------------- 1 file changed, 88 insertions(+), 144 deletions(-) diff --git a/.github/workflows/link_requirement.yml b/.github/workflows/link_requirement.yml index e15ca9c5..b7ea384a 100644 --- a/.github/workflows/link_requirement.yml +++ b/.github/workflows/link_requirement.yml @@ -7,13 +7,12 @@ on: issues: types: [opened, edited] - # Permanent test mode — manually run in Actions tab workflow_dispatch: inputs: issue_number: - description: "Issue number to test with (Feature issue)" required: true type: number + description: "Feature issue number to test with" permissions: contents: read @@ -23,9 +22,7 @@ permissions: jobs: link-requirement: - # Run for: - # 1) Issue Type = Feature - # 2) workflow_dispatch (test mode) + # Run for Feature Issue Type OR workflow_dispatch test mode if: > github.event_name == 'workflow_dispatch' || github.event.issue.type == 'Feature' || @@ -34,196 +31,159 @@ jobs: runs-on: ubuntu-latest env: - # Default repo for number-only requirement links DEFAULT_REQUIREMENTS_REPO: ${{ vars.DEFAULT_REQUIREMENTS_REPO || 'dmf-mxl/mxl-requirements' }} - # Unified ISSUE_NUMBER: - # • issues event → github.event.issue.number - # • dispatch event → inputs.issue_number ISSUE_NUMBER: ${{ github.event.issue.number || inputs.issue_number }} steps: - # ============================================================= - # 0. Resolve GH_TOKEN BEFORE any gh commands run (critical) - # ============================================================= + + ################################################################## + # 0. TOKEN INITIALIZATION — MUST BE FIRST (gh api requires GH_TOKEN) + ################################################################## - name: Determine token mode - id: token-mode + id: token_mode run: | - echo "=== TOKEN MODE ===" - echo "Event name: ${{ github.event_name }}" + echo "Event name = ${{ github.event_name }}" if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then echo "mode=dispatch" >> "$GITHUB_OUTPUT" else echo "mode=real" >> "$GITHUB_OUTPUT" fi - # Real GitHub Issue event → GitHub App token + # Real event → GitHub App token - name: Create GitHub App token (org) - if: ${{ steps.token-mode.outputs.mode == 'real' }} - id: app-token + if: ${{ steps.token_mode.outputs.mode == 'real' }} + id: app_token uses: actions/create-github-app-token@v2 with: app-id: ${{ vars.CROSS_REPO_ISSUE_ACCESS_APP_ID }} private-key: ${{ secrets.CROSS_REPO_ISSUE_ACCESS_PRIVATE_KEY }} owner: dmf-mxl - - name: Export GH_TOKEN (real mode) - if: ${{ steps.token-mode.outputs.mode == 'real' }} + - name: Export GH_TOKEN (real) + if: ${{ steps.token_mode.outputs.mode == 'real' }} run: | - echo "GH_TOKEN=${{ steps.app-token.outputs.token }}" >> "$GITHUB_ENV" - echo "=== TOKEN: Using GitHub App token (real mode) ===" + echo "GH_TOKEN=${{ steps.app_token.outputs.token }}" >> "$GITHUB_ENV" - # workflow_dispatch → GITHUB_TOKEN (safe for read/limited test) - - name: Export GH_TOKEN (dispatch mode) - if: ${{ steps.token-mode.outputs.mode == 'dispatch' }} + # Test mode → builtin GITHUB_TOKEN + - name: Export GH_TOKEN (dispatch) + if: ${{ steps.token_mode.outputs.mode == 'dispatch' }} run: | echo "GH_TOKEN=${{ secrets.GITHUB_TOKEN }}" >> "$GITHUB_ENV" - echo "=== TOKEN: Using GITHUB_TOKEN (test mode) ===" - name: Verify GH_TOKEN early run: | - echo "=== TOKEN CHECK ===" if [ -z "$GH_TOKEN" ]; then - echo "!! GH_TOKEN missing — cannot continue" + echo "ERROR: GH_TOKEN missing" exit 1 fi - echo "GH_TOKEN present." + echo "GH_TOKEN initialized OK." - # ============================================================= - # A. Debug high-level context - # ============================================================= - - name: DEBUG — Dump event information - run: | - echo "=== DEBUG: EVENT CONTEXT ===" - echo "Event name: ${{ github.event_name }}" - echo "Actor: ${{ github.actor }}" - echo "Repository: ${{ github.repository }}" - echo "Ref: ${{ github.ref }}" - echo "Workflow: ${{ github.workflow }}" - echo "Issue number logic: ${{ env.ISSUE_NUMBER }}" - echo "Issue type (string): '${{ github.event.issue.type }}'" - echo "Issue type (object): '${{ github.event.issue.type.name }}'" - - # ============================================================= - # 1. Fetch issue body (as output) - # ============================================================= + + ################################################################## + # 1. FETCH ISSUE BODY AND ENCODE SAFELY (NO MULTILINE OUTPUTS!) + ################################################################## - name: Fetch issue body id: fetch run: | - echo "=== STEP 1: Fetching issue body ===" - echo "Fetching body for ISSUE_NUMBER=${ISSUE_NUMBER}" - gh api "repos/${{ github.repository }}/issues/${ISSUE_NUMBER}" --jq .body > issue_body.txt || true + echo "Fetching issue body for #${ISSUE_NUMBER}" + BODY=$(gh api "repos/${{ github.repository }}/issues/${ISSUE_NUMBER}" --jq .body || echo "") + echo "$BODY" > issue_body.txt - # Expose as a multiline step output - echo "body<<'EOF'" >> "$GITHUB_OUTPUT" - cat issue_body.txt >> "$GITHUB_OUTPUT" - echo "EOF" >> "$GITHUB_OUTPUT" + # Encode body safely for transfer + ENCODED=$(base64 -w0 < issue_body.txt) + echo "encoded_body=$ENCODED" >> "$GITHUB_OUTPUT" - echo "--- fetched body (truncated to 400 chars) ---" - head -c 400 issue_body.txt || true + - name: DEBUG — Show first 300 chars of body + run: | + echo "--- issue body (truncated) ---" + head -c 300 issue_body.txt || true echo - echo "--- end fetched body ---" + echo "--- end ---" + - # ============================================================= - # 2. Parse issue form (best effort) - # ============================================================= - - name: Parse issue form (best effort) + ################################################################## + # 2. PARSE ISSUE FORM SAFELY + ################################################################## + - name: Decode & parse form id: parse uses: cssnr/parse-issue-form-action@v1 - continue-on-error: true with: - body: ${{ steps.fetch.outputs.body || github.event.issue.body || '' }} + body: ${{ steps.fetch.outputs.encoded_body }} + env: + FORM_DECODE: ${{ steps.fetch.outputs.encoded_body }} - - name: DEBUG — Parsed form output + - name: DEBUG — Parsed form fields run: | - echo "=== STEP 2: PARSED FORM FIELDS ===" - echo "requirement_link: '${{ steps.parse.outputs.requirement_link }}'" + echo "requirement_link='${{ steps.parse.outputs.requirement_link }}'" - # ============================================================= + + ################################################################## # 3. Guard — ensure requirement_link exists - # ============================================================= + ################################################################## - name: Guard – ensure requirement_link exists id: guard run: | - echo "=== STEP 3: Guard requirement_link ===" LINK="${{ steps.parse.outputs.requirement_link }}" echo "LINK='${LINK}'" + if [ -z "$LINK" ]; then if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then - echo "No requirement_link → continuing (test mode)" + echo "Test mode: continuing without requirement_link" echo "has=true" >> "$GITHUB_OUTPUT" else - echo "No requirement_link → skipping workflow" + echo "Requirement link missing — skipping" echo "has=false" >> "$GITHUB_OUTPUT" fi else - echo "Requirement link found." echo "has=true" >> "$GITHUB_OUTPUT" fi - # ============================================================= + + ################################################################## # 4. Normalize requirement link - # ============================================================= + ################################################################## - name: Normalize requirement link if: ${{ steps.guard.outputs.has == 'true' }} id: extract env: RAW_LINK: ${{ steps.parse.outputs.requirement_link }} - DEFAULT_REPO: ${{ env.DEFAULT_REQUIREMENTS_REPO }} + DEFAULT: ${{ env.DEFAULT_REQUIREMENTS_REPO }} run: | - echo "=== STEP 4: Normalising Requirement Link ===" - echo "RAW_LINK='${RAW_LINK}'" - - if [ -z "${RAW_LINK:-}" ]; then - echo "RAW_LINK empty (test mode?) — skipping" - exit 0 - fi - set -euo pipefail LINK="$(echo "$RAW_LINK" | tr -d '\r' | xargs)" - echo "Trimmed LINK='${LINK}'" + echo "Normalizing link='$LINK'" OWNER_REPO="" ISSUE_NUM="" FULL_URL="" if [[ "$LINK" =~ ^[A-Za-z0-9_.-]+/[A-Za-z0-9_.-]+#[0-9]+$ ]]; then - echo "Format: owner/repo#number" OWNER_REPO="${LINK%%#*}" ISSUE_NUM="${LINK##*#}" elif [[ "$LINK" =~ ^\#?[0-9]+$ ]]; then - echo "Format: number-only" - OWNER_REPO="$DEFAULT_REPO" + OWNER_REPO="$DEFAULT" ISSUE_NUM="${LINK#\#}" elif [[ "$LINK" =~ ^https?://github\.com/([^/]+)/([^/]+)/issues/([0-9]+) ]]; then - echo "Format: full URL" OWNER_REPO="${BASH_REMATCH[1]}/${BASH_REMATCH[2]}" ISSUE_NUM="${BASH_REMATCH[3]}" - else - echo "!! ERROR: Invalid requirement link format" fi - echo "OWNER_REPO='$OWNER_REPO'" - echo "ISSUE_NUM='$ISSUE_NUM'" - - if [[ -n "$OWNER_REPO" && -n "$ISSUE_NUM" ]]; then - FULL_URL="https://github.com/${OWNER_REPO}/issues/${ISSUE_NUM}" - echo "Final normalised FULL_URL: $FULL_URL" + FULL_URL="https://github.com/${OWNER_REPO}/issues/${ISSUE_NUM}" - echo "owner_repo=$OWNER_REPO" >> "$GITHUB_OUTPUT" - echo "issue_num=$ISSUE_NUM" >> "$GITHUB_OUTPUT" - echo "requirement_url=$FULL_URL" >> "$GITHUB_OUTPUT" - fi + echo "owner_repo=$OWNER_REPO" >> "$GITHUB_OUTPUT" + echo "issue_num=$ISSUE_NUM" >> "$GITHUB_OUTPUT" + echo "requirement_url=$FULL_URL" >> "$GITHUB_OUTPUT" - name: DEBUG — Extract outputs run: | - echo "=== STEP 4B: Extracted Requirement Data ===" - echo "owner_repo = '${{ steps.extract.outputs.owner_repo }}'" - echo "issue_num = '${{ steps.extract.outputs.issue_num }}'" - echo "requirement_url= '${{ steps.extract.outputs.requirement_url }}'" + echo "owner_repo=${{ steps.extract.outputs.owner_repo }}" + echo "issue_num=${{ steps.extract.outputs.issue_num }}" + echo "requirement_url=${{ steps.extract.outputs.requirement_url }}" - # ============================================================= + + ################################################################## # 5. Parent → child sub‑issue link - # ============================================================= + ################################################################## - name: Link as sub-issue if: ${{ steps.extract.outputs.issue_num != '' }} env: @@ -232,27 +192,22 @@ jobs: CHILD_REPO: ${{ github.repository }} CHILD_NUMBER: ${{ env.ISSUE_NUMBER }} run: | - echo "=== STEP 5: Linking child to parent ===" - echo "PARENT_REPO=$PARENT_REPO" - echo "PARENT_NUMBER=$PARENT_NUMBER" - echo "CHILD_REPO=$CHILD_REPO" - echo "CHILD_NUMBER=$CHILD_NUMBER" + set -euo pipefail + echo "Linking $CHILD_REPO#$CHILD_NUMBER → $PARENT_REPO#$PARENT_NUMBER" - echo "Fetching node IDs..." - PARENT_NODE_ID=$(gh issue view "https://github.com/$PARENT_REPO/issues/$PARENT_NUMBER" --json id --jq .id) - CHILD_NODE_ID=$(gh issue view "https://github.com/$CHILD_REPO/issues/$CHILD_NUMBER" --json id --jq .id) + PARENT_NODE=$(gh issue view "https://github.com/$PARENT_REPO/issues/$PARENT_NUMBER" --json id --jq .id) + CHILD_NODE=$(gh issue view "https://github.com/$CHILD_REPO/issues/$CHILD_NUMBER" --json id --jq .id) - echo "PARENT_NODE_ID=$PARENT_NODE_ID" - echo "CHILD_NODE_ID=$CHILD_NODE_ID" + echo "PARENT_NODE=$PARENT_NODE" + echo "CHILD_NODE=$CHILD_NODE" - echo "Creating sub-issue link via GraphQL…" gh api graphql \ -H "GraphQL-Features: sub_issues" \ -f query=" mutation { addSubIssue(input: { - issueId: \"${PARENT_NODE_ID}\", - subIssueId: \"${CHILD_NODE_ID}\" + issueId: \"${PARENT_NODE}\", + subIssueId: \"${CHILD_NODE}\" }) { issue { title } subIssue { title } @@ -260,50 +215,39 @@ jobs: } " - # ============================================================= - # 6. Append clickable URL to bottom of body (idempotent) - # ============================================================= - - name: Append normalized URL to issue body (idempotent) + + ################################################################## + # 6. Append clickable URL to issue body + ################################################################## + - name: Append normalized URL to issue body if: ${{ steps.extract.outputs.issue_num != '' }} env: CHILD_REPO: ${{ github.repository }} CHILD_NUMBER: ${{ env.ISSUE_NUMBER }} REQUIRE_URL: ${{ steps.extract.outputs.requirement_url }} run: | - echo "=== STEP 6: Updating Issue Body ===" - echo "CHILD_REPO=$CHILD_REPO" - echo "CHILD_NUMBER=$CHILD_NUMBER" - echo "Appending: $REQUIRE_URL" + echo "Appending requirement URL to issue #${CHILD_NUMBER}" - gh api "repos/$CHILD_REPO/issues/$CHILD_NUMBER" --jq .body > body_current.txt || echo -n > body_current.txt + gh api "repos/$CHILD_REPO/issues/$CHILD_NUMBER" --jq .body > body_current.txt - echo "--- existing body (truncated to 400 chars) ---" - head -c 400 body_current.txt || true - echo - echo "--- end existing body ---" - - # Remove previous block between markers (if any) - sed -e '//,//d' body_current.txt > body_clean.txt + # Remove previous block + sed -e '//,//d' \ + body_current.txt > body_clean.txt - # Append new normalized URL wrapped in invisible markers printf "\n\n\n%s\n\n" "$REQUIRE_URL" >> body_clean.txt NEW_BODY="$(cat body_clean.txt)" - echo "--- new body (truncated to 400 chars) ---" - echo "$NEW_BODY" | head -c 400 || true - echo - echo "--- end new body ---" - gh api \ --method PATCH \ -H "Accept: application/vnd.github+json" \ "repos/$CHILD_REPO/issues/$CHILD_NUMBER" \ -f body="$NEW_BODY" - # ============================================================= + + ################################################################## # 7. Final summary - # ============================================================= + ################################################################## - name: DEBUG — Final summary run: | echo "=== FINAL SUMMARY ===" @@ -312,4 +256,4 @@ jobs: echo "Owner Repo: ${{ steps.extract.outputs.owner_repo }}" echo "Requirement Issue: ${{ steps.extract.outputs.issue_num }}" echo "Requirement URL: ${{ steps.extract.outputs.requirement_url }}" - echo "Workflow completed." \ No newline at end of file + echo "Workflow completed successfully." From c334392c06a8e1ae678d2d3354afb352bb48bd2e Mon Sep 17 00:00:00 2001 From: Peter Brightwell Date: Fri, 13 Mar 2026 15:13:06 +0000 Subject: [PATCH 2/8] Further parser fixes Signed-off-by: Peter Brightwell --- .github/workflows/link_requirement.yml | 170 ++++++++++++++----------- 1 file changed, 98 insertions(+), 72 deletions(-) diff --git a/.github/workflows/link_requirement.yml b/.github/workflows/link_requirement.yml index b7ea384a..2f6988f2 100644 --- a/.github/workflows/link_requirement.yml +++ b/.github/workflows/link_requirement.yml @@ -10,9 +10,9 @@ on: workflow_dispatch: inputs: issue_number: - required: true - type: number description: "Feature issue number to test with" + type: number + required: true permissions: contents: read @@ -36,13 +36,13 @@ jobs: steps: - ################################################################## - # 0. TOKEN INITIALIZATION — MUST BE FIRST (gh api requires GH_TOKEN) - ################################################################## + ########################################################################### + # 0. TOKEN INITIALISATION — BEFORE ANY gh COMMANDS + ########################################################################### - name: Determine token mode id: token_mode run: | - echo "Event name = ${{ github.event_name }}" + echo "Event=${{ github.event_name }}" if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then echo "mode=dispatch" >> "$GITHUB_OUTPUT" else @@ -50,7 +50,7 @@ jobs: fi # Real event → GitHub App token - - name: Create GitHub App token (org) + - name: Create GitHub App token if: ${{ steps.token_mode.outputs.mode == 'real' }} id: app_token uses: actions/create-github-app-token@v2 @@ -62,97 +62,117 @@ jobs: - name: Export GH_TOKEN (real) if: ${{ steps.token_mode.outputs.mode == 'real' }} run: | + echo "Using org GitHub App token" echo "GH_TOKEN=${{ steps.app_token.outputs.token }}" >> "$GITHUB_ENV" # Test mode → builtin GITHUB_TOKEN - name: Export GH_TOKEN (dispatch) if: ${{ steps.token_mode.outputs.mode == 'dispatch' }} run: | + echo "Using GITHUB_TOKEN (dispatch mode)" echo "GH_TOKEN=${{ secrets.GITHUB_TOKEN }}" >> "$GITHUB_ENV" - - name: Verify GH_TOKEN early + - name: Verify GH_TOKEN run: | if [ -z "$GH_TOKEN" ]; then echo "ERROR: GH_TOKEN missing" exit 1 fi - echo "GH_TOKEN initialized OK." + echo "GH_TOKEN OK" - ################################################################## - # 1. FETCH ISSUE BODY AND ENCODE SAFELY (NO MULTILINE OUTPUTS!) - ################################################################## + ########################################################################### + # 1. FETCH ISSUE BODY — store into ISSUE_BODY (multiline safe) + ########################################################################### - name: Fetch issue body id: fetch run: | - echo "Fetching issue body for #${ISSUE_NUMBER}" - BODY=$(gh api "repos/${{ github.repository }}/issues/${ISSUE_NUMBER}" --jq .body || echo "") - echo "$BODY" > issue_body.txt + echo "Fetching issue #${ISSUE_NUMBER}" + + BODY=$(gh api \ + "repos/${{ github.repository }}/issues/${ISSUE_NUMBER}" \ + --jq .body || echo "") - # Encode body safely for transfer - ENCODED=$(base64 -w0 < issue_body.txt) - echo "encoded_body=$ENCODED" >> "$GITHUB_OUTPUT" + # Export ISSUE_BODY (multiline-safe) + { + echo "ISSUE_BODY<> "$GITHUB_ENV" - - name: DEBUG — Show first 300 chars of body + - name: DEBUG — show body snippet run: | - echo "--- issue body (truncated) ---" - head -c 300 issue_body.txt || true + echo "--- ISSUE_BODY (truncated) ---" + echo "$ISSUE_BODY" | head -c 300 echo - echo "--- end ---" + echo "------------------------------" - ################################################################## - # 2. PARSE ISSUE FORM SAFELY - ################################################################## - - name: Decode & parse form - id: parse - uses: cssnr/parse-issue-form-action@v1 - with: - body: ${{ steps.fetch.outputs.encoded_body }} - env: - FORM_DECODE: ${{ steps.fetch.outputs.encoded_body }} - - - name: DEBUG — Parsed form fields + ########################################################################### + # 2. Extract requirement_link from ISSUE_BODY — Unicode-safe + ########################################################################### + - name: Extract requirement_link from markdown + id: extract_raw run: | - echo "requirement_link='${{ steps.parse.outputs.requirement_link }}'" + echo "=== Extracting requirement_link from ISSUE_BODY ===" + # Remove all non-printable and zero-width Unicode junk + SANITISED=$(printf "%s\n" "$ISSUE_BODY" \ + | sed 's/[^[:print:]\t]//g') + + echo "--- Sanitised body (first 200 chars) ---" + echo "$SANITISED" | head -c 200 + echo + echo "----------------------------------------" - ################################################################## - # 3. Guard — ensure requirement_link exists - ################################################################## - - name: Guard – ensure requirement_link exists + # Extract line after any heading containing "Requirement Link" (Unicode-safe) + REQ_LINE=$( + printf "%s\n" "$SANITISED" | + awk 'tolower($0) ~ /requirement[[:space:]]+link/ {getline; print}' | + xargs + ) + + echo "Extracted requirement_link='$REQ_LINE'" + + echo "requirement_link=$REQ_LINE" >> "$GITHUB_OUTPUT" + + + ########################################################################### + # 3. Guard requirement_link + ########################################################################### + - name: Guard requirement_link exists id: guard run: | - LINK="${{ steps.parse.outputs.requirement_link }}" - echo "LINK='${LINK}'" + LINK="${{ steps.extract_raw.outputs.requirement_link }}" + echo "LINK='$LINK'" if [ -z "$LINK" ]; then if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then - echo "Test mode: continuing without requirement_link" + echo "Test mode: continuing" echo "has=true" >> "$GITHUB_OUTPUT" else - echo "Requirement link missing — skipping" + echo "Requirement link missing — skipping run" echo "has=false" >> "$GITHUB_OUTPUT" fi else + echo "Requirement link OK" echo "has=true" >> "$GITHUB_OUTPUT" - fi - ################################################################## - # 4. Normalize requirement link - ################################################################## - - name: Normalize requirement link + ########################################################################### + # 4. Normalise requirement_link + ########################################################################### + - name: Normalize requirement_link if: ${{ steps.guard.outputs.has == 'true' }} id: extract env: - RAW_LINK: ${{ steps.parse.outputs.requirement_link }} + RAW_LINK: ${{ steps.extract_raw.outputs.requirement_link }} DEFAULT: ${{ env.DEFAULT_REQUIREMENTS_REPO }} run: | set -euo pipefail - LINK="$(echo "$RAW_LINK" | tr -d '\r' | xargs)" - echo "Normalizing link='$LINK'" + echo "Normalising '$RAW_LINK'" + LINK="$(echo "$RAW_LINK" | xargs)" OWNER_REPO="" ISSUE_NUM="" FULL_URL="" @@ -160,9 +180,11 @@ jobs: if [[ "$LINK" =~ ^[A-Za-z0-9_.-]+/[A-Za-z0-9_.-]+#[0-9]+$ ]]; then OWNER_REPO="${LINK%%#*}" ISSUE_NUM="${LINK##*#}" + elif [[ "$LINK" =~ ^\#?[0-9]+$ ]]; then OWNER_REPO="$DEFAULT" ISSUE_NUM="${LINK#\#}" + elif [[ "$LINK" =~ ^https?://github\.com/([^/]+)/([^/]+)/issues/([0-9]+) ]]; then OWNER_REPO="${BASH_REMATCH[1]}/${BASH_REMATCH[2]}" ISSUE_NUM="${BASH_REMATCH[3]}" @@ -174,16 +196,16 @@ jobs: echo "issue_num=$ISSUE_NUM" >> "$GITHUB_OUTPUT" echo "requirement_url=$FULL_URL" >> "$GITHUB_OUTPUT" - - name: DEBUG — Extract outputs + - name: DEBUG — Normalised values run: | echo "owner_repo=${{ steps.extract.outputs.owner_repo }}" echo "issue_num=${{ steps.extract.outputs.issue_num }}" echo "requirement_url=${{ steps.extract.outputs.requirement_url }}" - ################################################################## + ########################################################################### # 5. Parent → child sub‑issue link - ################################################################## + ########################################################################### - name: Link as sub-issue if: ${{ steps.extract.outputs.issue_num != '' }} env: @@ -193,13 +215,15 @@ jobs: CHILD_NUMBER: ${{ env.ISSUE_NUMBER }} run: | set -euo pipefail - echo "Linking $CHILD_REPO#$CHILD_NUMBER → $PARENT_REPO#$PARENT_NUMBER" + echo "Linking child #$CHILD_NUMBER → parent #$PARENT_NUMBER" - PARENT_NODE=$(gh issue view "https://github.com/$PARENT_REPO/issues/$PARENT_NUMBER" --json id --jq .id) - CHILD_NODE=$(gh issue view "https://github.com/$CHILD_REPO/issues/$CHILD_NUMBER" --json id --jq .id) + PARENT_NODE=$(gh issue view \ + "https://github.com/$PARENT_REPO/issues/$PARENT_NUMBER" \ + --json id --jq .id) - echo "PARENT_NODE=$PARENT_NODE" - echo "CHILD_NODE=$CHILD_NODE" + CHILD_NODE=$(gh issue view \ + "https://github.com/$CHILD_REPO/issues/$CHILD_NUMBER" \ + --json id --jq .id) gh api graphql \ -H "GraphQL-Features: sub_issues" \ @@ -216,25 +240,28 @@ jobs: " - ################################################################## - # 6. Append clickable URL to issue body - ################################################################## - - name: Append normalized URL to issue body + ########################################################################### + # 6. Append clickable requirement URL to issue body + ########################################################################### + - name: Append requirement URL to issue body if: ${{ steps.extract.outputs.issue_num != '' }} env: + REQUIRE_URL: ${{ steps.extract.outputs.requirement_url }} CHILD_REPO: ${{ github.repository }} CHILD_NUMBER: ${{ env.ISSUE_NUMBER }} - REQUIRE_URL: ${{ steps.extract.outputs.requirement_url }} run: | - echo "Appending requirement URL to issue #${CHILD_NUMBER}" + echo "Patching issue #${CHILD_NUMBER}" gh api "repos/$CHILD_REPO/issues/$CHILD_NUMBER" --jq .body > body_current.txt - # Remove previous block + # Remove old block sed -e '//,//d' \ body_current.txt > body_clean.txt - printf "\n\n\n%s\n\n" "$REQUIRE_URL" >> body_clean.txt + # Append new block + printf "\n\n\n%s\n\n" \ + "$REQUIRE_URL" \ + >> body_clean.txt NEW_BODY="$(cat body_clean.txt)" @@ -245,15 +272,14 @@ jobs: -f body="$NEW_BODY" - ################################################################## - # 7. Final summary - ################################################################## + ########################################################################### + # 7. Summary + ########################################################################### - name: DEBUG — Final summary run: | - echo "=== FINAL SUMMARY ===" echo "Event: ${{ github.event_name }}" echo "Issue Number: ${{ env.ISSUE_NUMBER }}" echo "Owner Repo: ${{ steps.extract.outputs.owner_repo }}" echo "Requirement Issue: ${{ steps.extract.outputs.issue_num }}" echo "Requirement URL: ${{ steps.extract.outputs.requirement_url }}" - echo "Workflow completed successfully." + echo "DONE." From ed15bdbd156c1b993fd04a6ad9909bae136aab49 Mon Sep 17 00:00:00 2001 From: Peter Brightwell Date: Fri, 13 Mar 2026 15:20:52 +0000 Subject: [PATCH 3/8] Extract link from comment not issue body --- .github/workflows/link_requirement.yml | 94 +++++++++++--------------- 1 file changed, 40 insertions(+), 54 deletions(-) diff --git a/.github/workflows/link_requirement.yml b/.github/workflows/link_requirement.yml index 2f6988f2..a4522e41 100644 --- a/.github/workflows/link_requirement.yml +++ b/.github/workflows/link_requirement.yml @@ -22,7 +22,7 @@ permissions: jobs: link-requirement: - # Run for Feature Issue Type OR workflow_dispatch test mode + # Run for Issue Type = Feature OR workflow_dispatch if: > github.event_name == 'workflow_dispatch' || github.event.issue.type == 'Feature' || @@ -37,7 +37,7 @@ jobs: steps: ########################################################################### - # 0. TOKEN INITIALISATION — BEFORE ANY gh COMMANDS + # 0. TOKEN INITIALIZATION — MUST RUN BEFORE ANY gh COMMANDS ########################################################################### - name: Determine token mode id: token_mode @@ -49,7 +49,7 @@ jobs: echo "mode=real" >> "$GITHUB_OUTPUT" fi - # Real event → GitHub App token + # Real GitHub issue → GitHub App token - name: Create GitHub App token if: ${{ steps.token_mode.outputs.mode == 'real' }} id: app_token @@ -62,10 +62,10 @@ jobs: - name: Export GH_TOKEN (real) if: ${{ steps.token_mode.outputs.mode == 'real' }} run: | - echo "Using org GitHub App token" + echo "Using GitHub App token" echo "GH_TOKEN=${{ steps.app_token.outputs.token }}" >> "$GITHUB_ENV" - # Test mode → builtin GITHUB_TOKEN + # Test mode → GitHub-provided GITHUB_TOKEN - name: Export GH_TOKEN (dispatch) if: ${{ steps.token_mode.outputs.mode == 'dispatch' }} run: | @@ -82,50 +82,46 @@ jobs: ########################################################################### - # 1. FETCH ISSUE BODY — store into ISSUE_BODY (multiline safe) + # 1. FETCH FIRST COMMENT (Issue Form output lives here!) ########################################################################### - - name: Fetch issue body - id: fetch + - name: Fetch first comment (form contents) + id: fetch_comment run: | - echo "Fetching issue #${ISSUE_NUMBER}" + echo "Fetching comments for issue #${ISSUE_NUMBER}" + COMMENTS=$(gh api "repos/${{ github.repository }}/issues/${ISSUE_NUMBER}/comments" || echo "[]") - BODY=$(gh api \ - "repos/${{ github.repository }}/issues/${ISSUE_NUMBER}" \ - --jq .body || echo "") + FIRST=$(echo "$COMMENTS" | jq -r '.[0].body // ""') + + echo "--- First comment (truncated) ---" + echo "$FIRST" | head -c 300 + echo + echo "-------------------------------" - # Export ISSUE_BODY (multiline-safe) { - echo "ISSUE_BODY<> "$GITHUB_ENV" - - name: DEBUG — show body snippet - run: | - echo "--- ISSUE_BODY (truncated) ---" - echo "$ISSUE_BODY" | head -c 300 - echo - echo "------------------------------" ########################################################################### - # 2. Extract requirement_link from ISSUE_BODY — Unicode-safe + # 2. Extract requirement_link from FIRST COMMENT — Unicode‑safe ########################################################################### - - name: Extract requirement_link from markdown + - name: Extract requirement_link from first comment id: extract_raw run: | - echo "=== Extracting requirement_link from ISSUE_BODY ===" + echo "=== Extracting requirement_link from FIRST COMMENT ===" - # Remove all non-printable and zero-width Unicode junk - SANITISED=$(printf "%s\n" "$ISSUE_BODY" \ - | sed 's/[^[:print:]\t]//g') + # Remove zero-width and other invisible unicode garbage + SANITISED=$(printf "%s\n" "$FORM_BODY" | sed 's/[^[:print:]\t]//g') - echo "--- Sanitised body (first 200 chars) ---" + echo "--- Sanitised snippet ---" echo "$SANITISED" | head -c 200 echo - echo "----------------------------------------" + echo "-------------------------" - # Extract line after any heading containing "Requirement Link" (Unicode-safe) + # Find line after heading containing "Requirement Link" REQ_LINE=$( printf "%s\n" "$SANITISED" | awk 'tolower($0) ~ /requirement[[:space:]]+link/ {getline; print}' | @@ -137,21 +133,21 @@ jobs: echo "requirement_link=$REQ_LINE" >> "$GITHUB_OUTPUT" + ########################################################################### - # 3. Guard requirement_link + # 3. Guard requirement link presence ########################################################################### - name: Guard requirement_link exists id: guard run: | LINK="${{ steps.extract_raw.outputs.requirement_link }}" echo "LINK='$LINK'" - if [ -z "$LINK" ]; then if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then echo "Test mode: continuing" echo "has=true" >> "$GITHUB_OUTPUT" else - echo "Requirement link missing — skipping run" + echo "Missing requirement_link — skipping" echo "has=false" >> "$GITHUB_OUTPUT" fi else @@ -159,8 +155,9 @@ jobs: echo "has=true" >> "$GITHUB_OUTPUT" + ########################################################################### - # 4. Normalise requirement_link + # 4. Normalise requirement link ########################################################################### - name: Normalize requirement_link if: ${{ steps.guard.outputs.has == 'true' }} @@ -180,11 +177,9 @@ jobs: if [[ "$LINK" =~ ^[A-Za-z0-9_.-]+/[A-Za-z0-9_.-]+#[0-9]+$ ]]; then OWNER_REPO="${LINK%%#*}" ISSUE_NUM="${LINK##*#}" - elif [[ "$LINK" =~ ^\#?[0-9]+$ ]]; then OWNER_REPO="$DEFAULT" ISSUE_NUM="${LINK#\#}" - elif [[ "$LINK" =~ ^https?://github\.com/([^/]+)/([^/]+)/issues/([0-9]+) ]]; then OWNER_REPO="${BASH_REMATCH[1]}/${BASH_REMATCH[2]}" ISSUE_NUM="${BASH_REMATCH[3]}" @@ -196,15 +191,10 @@ jobs: echo "issue_num=$ISSUE_NUM" >> "$GITHUB_OUTPUT" echo "requirement_url=$FULL_URL" >> "$GITHUB_OUTPUT" - - name: DEBUG — Normalised values - run: | - echo "owner_repo=${{ steps.extract.outputs.owner_repo }}" - echo "issue_num=${{ steps.extract.outputs.issue_num }}" - echo "requirement_url=${{ steps.extract.outputs.requirement_url }}" ########################################################################### - # 5. Parent → child sub‑issue link + # 5. Create parent → child sub‑issue link ########################################################################### - name: Link as sub-issue if: ${{ steps.extract.outputs.issue_num != '' }} @@ -215,15 +205,11 @@ jobs: CHILD_NUMBER: ${{ env.ISSUE_NUMBER }} run: | set -euo pipefail - echo "Linking child #$CHILD_NUMBER → parent #$PARENT_NUMBER" - PARENT_NODE=$(gh issue view \ - "https://github.com/$PARENT_REPO/issues/$PARENT_NUMBER" \ - --json id --jq .id) + echo "Linking child issue #$CHILD_NUMBER → parent #$PARENT_NUMBER" - CHILD_NODE=$(gh issue view \ - "https://github.com/$CHILD_REPO/issues/$CHILD_NUMBER" \ - --json id --jq .id) + PARENT_NODE=$(gh issue view "https://github.com/$PARENT_REPO/issues/$PARENT_NUMBER" --json id --jq .id) + CHILD_NODE=$(gh issue view "https://github.com/$CHILD_REPO/issues/$CHILD_NUMBER" --json id --jq .id) gh api graphql \ -H "GraphQL-Features: sub_issues" \ @@ -240,8 +226,9 @@ jobs: " + ########################################################################### - # 6. Append clickable requirement URL to issue body + # 6. Append requirement URL to issue body ########################################################################### - name: Append requirement URL to issue body if: ${{ steps.extract.outputs.issue_num != '' }} @@ -254,11 +241,9 @@ jobs: gh api "repos/$CHILD_REPO/issues/$CHILD_NUMBER" --jq .body > body_current.txt - # Remove old block sed -e '//,//d' \ body_current.txt > body_clean.txt - # Append new block printf "\n\n\n%s\n\n" \ "$REQUIRE_URL" \ >> body_clean.txt @@ -272,8 +257,9 @@ jobs: -f body="$NEW_BODY" + ########################################################################### - # 7. Summary + # 7. Final summary ########################################################################### - name: DEBUG — Final summary run: | @@ -282,4 +268,4 @@ jobs: echo "Owner Repo: ${{ steps.extract.outputs.owner_repo }}" echo "Requirement Issue: ${{ steps.extract.outputs.issue_num }}" echo "Requirement URL: ${{ steps.extract.outputs.requirement_url }}" - echo "DONE." + echo "DONE." \ No newline at end of file From ea845d057d20eb442150a67c59f01158458fd5fa Mon Sep 17 00:00:00 2001 From: Peter Brightwell Date: Fri, 13 Mar 2026 15:25:27 +0000 Subject: [PATCH 4/8] Trying timeline extraction --- .github/workflows/link_requirement.yml | 110 +++++++++++-------------- 1 file changed, 48 insertions(+), 62 deletions(-) diff --git a/.github/workflows/link_requirement.yml b/.github/workflows/link_requirement.yml index a4522e41..b5bd47fe 100644 --- a/.github/workflows/link_requirement.yml +++ b/.github/workflows/link_requirement.yml @@ -10,9 +10,9 @@ on: workflow_dispatch: inputs: issue_number: - description: "Feature issue number to test with" - type: number required: true + type: number + description: "Feature issue number to test with" permissions: contents: read @@ -22,7 +22,6 @@ permissions: jobs: link-requirement: - # Run for Issue Type = Feature OR workflow_dispatch if: > github.event_name == 'workflow_dispatch' || github.event.issue.type == 'Feature' || @@ -37,19 +36,17 @@ jobs: steps: ########################################################################### - # 0. TOKEN INITIALIZATION — MUST RUN BEFORE ANY gh COMMANDS + # 0. TOKEN INITIALISATION ########################################################################### - name: Determine token mode id: token_mode run: | - echo "Event=${{ github.event_name }}" if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then echo "mode=dispatch" >> "$GITHUB_OUTPUT" else echo "mode=real" >> "$GITHUB_OUTPUT" fi - # Real GitHub issue → GitHub App token - name: Create GitHub App token if: ${{ steps.token_mode.outputs.mode == 'real' }} id: app_token @@ -61,16 +58,11 @@ jobs: - name: Export GH_TOKEN (real) if: ${{ steps.token_mode.outputs.mode == 'real' }} - run: | - echo "Using GitHub App token" - echo "GH_TOKEN=${{ steps.app_token.outputs.token }}" >> "$GITHUB_ENV" + run: echo "GH_TOKEN=${{ steps.app_token.outputs.token }}" >> "$GITHUB_ENV" - # Test mode → GitHub-provided GITHUB_TOKEN - name: Export GH_TOKEN (dispatch) if: ${{ steps.token_mode.outputs.mode == 'dispatch' }} - run: | - echo "Using GITHUB_TOKEN (dispatch mode)" - echo "GH_TOKEN=${{ secrets.GITHUB_TOKEN }}" >> "$GITHUB_ENV" + run: echo "GH_TOKEN=${{ secrets.GITHUB_TOKEN }}" >> "$GITHUB_ENV" - name: Verify GH_TOKEN run: | @@ -82,46 +74,53 @@ jobs: ########################################################################### - # 1. FETCH FIRST COMMENT (Issue Form output lives here!) + # 1. FETCH ISSUE TIMELINE — the *real* source of Issue Form content ########################################################################### - - name: Fetch first comment (form contents) - id: fetch_comment + - name: Fetch timeline + id: fetch_timeline run: | - echo "Fetching comments for issue #${ISSUE_NUMBER}" - COMMENTS=$(gh api "repos/${{ github.repository }}/issues/${ISSUE_NUMBER}/comments" || echo "[]") + echo "Fetching timeline for issue #${ISSUE_NUMBER}" + + # Timeline requires preview header + TIMELINE=$(gh api \ + -H "Accept: application/vnd.github.mockingbird-preview+json" \ + "repos/${{ github.repository }}/issues/${ISSUE_NUMBER}/timeline" || echo "[]") - FIRST=$(echo "$COMMENTS" | jq -r '.[0].body // ""') + echo "$TIMELINE" > timeline.json - echo "--- First comment (truncated) ---" - echo "$FIRST" | head -c 300 + echo "--- Timeline snippet ---" + head -c 500 timeline.json echo - echo "-------------------------------" + echo "------------------------" + # Export to environment variable { - echo "FORM_BODY<> "$GITHUB_ENV" - ########################################################################### - # 2. Extract requirement_link from FIRST COMMENT — Unicode‑safe + # 2. EXTRACT REQUIREMENT LINK FROM TIMELINE ########################################################################### - - name: Extract requirement_link from first comment + - name: Extract requirement_link from timeline id: extract_raw run: | - echo "=== Extracting requirement_link from FIRST COMMENT ===" + echo "=== Extracting from TIMELINE ===" - # Remove zero-width and other invisible unicode garbage - SANITISED=$(printf "%s\n" "$FORM_BODY" | sed 's/[^[:print:]\t]//g') + # Look for any timeline item body containing "Requirement Link" + FORM_BODY=$(echo "$ISSUE_TIMELINE" | jq -r \ + '.[] | select(.body != null) | .body' | tr -d '\u200B\u200C\u200D' ) - echo "--- Sanitised snippet ---" - echo "$SANITISED" | head -c 200 + echo "--- First matching form body (200 chars) ---" + echo "$FORM_BODY" | head -c 200 echo - echo "-------------------------" + echo "-------------------------------------------" + + # Sanitize and extract line after heading + SANITISED=$(printf "%s\n" "$FORM_BODY" | sed 's/[^[:print:]\t]//g') - # Find line after heading containing "Requirement Link" REQ_LINE=$( printf "%s\n" "$SANITISED" | awk 'tolower($0) ~ /requirement[[:space:]]+link/ {getline; print}' | @@ -133,31 +132,28 @@ jobs: echo "requirement_link=$REQ_LINE" >> "$GITHUB_OUTPUT" - ########################################################################### - # 3. Guard requirement link presence + # 3. GUARD REQUIREMENT LINK ########################################################################### - name: Guard requirement_link exists id: guard run: | LINK="${{ steps.extract_raw.outputs.requirement_link }}" - echo "LINK='$LINK'" if [ -z "$LINK" ]; then if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then - echo "Test mode: continuing" + echo "Test mode: continue" echo "has=true" >> "$GITHUB_OUTPUT" else - echo "Missing requirement_link — skipping" + echo "Missing requirement link — skipping" echo "has=false" >> "$GITHUB_OUTPUT" fi else - echo "Requirement link OK" + echo "Found requirement link: $LINK" echo "has=true" >> "$GITHUB_OUTPUT" - ########################################################################### - # 4. Normalise requirement link + # 4. NORMALIZE requirement_link ########################################################################### - name: Normalize requirement_link if: ${{ steps.guard.outputs.has == 'true' }} @@ -166,9 +162,6 @@ jobs: RAW_LINK: ${{ steps.extract_raw.outputs.requirement_link }} DEFAULT: ${{ env.DEFAULT_REQUIREMENTS_REPO }} run: | - set -euo pipefail - echo "Normalising '$RAW_LINK'" - LINK="$(echo "$RAW_LINK" | xargs)" OWNER_REPO="" ISSUE_NUM="" @@ -192,9 +185,8 @@ jobs: echo "requirement_url=$FULL_URL" >> "$GITHUB_OUTPUT" - ########################################################################### - # 5. Create parent → child sub‑issue link + # 5. CREATE SUB-ISSUE LINK ########################################################################### - name: Link as sub-issue if: ${{ steps.extract.outputs.issue_num != '' }} @@ -204,12 +196,12 @@ jobs: CHILD_REPO: ${{ github.repository }} CHILD_NUMBER: ${{ env.ISSUE_NUMBER }} run: | - set -euo pipefail + set -e - echo "Linking child issue #$CHILD_NUMBER → parent #$PARENT_NUMBER" - - PARENT_NODE=$(gh issue view "https://github.com/$PARENT_REPO/issues/$PARENT_NUMBER" --json id --jq .id) - CHILD_NODE=$(gh issue view "https://github.com/$CHILD_REPO/issues/$CHILD_NUMBER" --json id --jq .id) + PARENT_NODE=$(gh issue view "https://github.com/$PARENT_REPO/issues/$PARENT_NUMBER" \ + --json id --jq .id) + CHILD_NODE=$(gh issue view "https://github.com/$CHILD_REPO/issues/$CHILD_NUMBER" \ + --json id --jq .id) gh api graphql \ -H "GraphQL-Features: sub_issues" \ @@ -226,9 +218,8 @@ jobs: " - ########################################################################### - # 6. Append requirement URL to issue body + # 6. APPEND NORMALIZED URL TO ISSUE BODY ########################################################################### - name: Append requirement URL to issue body if: ${{ steps.extract.outputs.issue_num != '' }} @@ -237,16 +228,13 @@ jobs: CHILD_REPO: ${{ github.repository }} CHILD_NUMBER: ${{ env.ISSUE_NUMBER }} run: | - echo "Patching issue #${CHILD_NUMBER}" - gh api "repos/$CHILD_REPO/issues/$CHILD_NUMBER" --jq .body > body_current.txt sed -e '//,//d' \ body_current.txt > body_clean.txt printf "\n\n\n%s\n\n" \ - "$REQUIRE_URL" \ - >> body_clean.txt + "$REQUIRE_URL" >> body_clean.txt NEW_BODY="$(cat body_clean.txt)" @@ -257,13 +245,11 @@ jobs: -f body="$NEW_BODY" - ########################################################################### - # 7. Final summary + # 7. SUMMARY ########################################################################### - - name: DEBUG — Final summary + - name: Final summary run: | - echo "Event: ${{ github.event_name }}" echo "Issue Number: ${{ env.ISSUE_NUMBER }}" echo "Owner Repo: ${{ steps.extract.outputs.owner_repo }}" echo "Requirement Issue: ${{ steps.extract.outputs.issue_num }}" From 7963867fae6a4c8b43d1a7832af7cac54149152f Mon Sep 17 00:00:00 2001 From: Peter Brightwell Date: Fri, 13 Mar 2026 15:36:15 +0000 Subject: [PATCH 5/8] Debug dump event to file --- .github/workflows/link_requirement.yml | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/.github/workflows/link_requirement.yml b/.github/workflows/link_requirement.yml index b5bd47fe..b89f85fc 100644 --- a/.github/workflows/link_requirement.yml +++ b/.github/workflows/link_requirement.yml @@ -35,6 +35,12 @@ jobs: steps: + - name: DEBUG — dump complete event to file + run: | + echo '${{ toJson(github.event) }}' > event.json + echo "=== DEBUG: event.json written ===" + head -n 50 event.json + ########################################################################### # 0. TOKEN INITIALISATION ########################################################################### From fec2f5d68667bfbf62385ffdb602162653c35fa5 Mon Sep 17 00:00:00 2001 From: Peter Brightwell Date: Fri, 13 Mar 2026 15:41:33 +0000 Subject: [PATCH 6/8] Fix typo --- .github/workflows/link_requirement.yml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/link_requirement.yml b/.github/workflows/link_requirement.yml index b89f85fc..bb00e404 100644 --- a/.github/workflows/link_requirement.yml +++ b/.github/workflows/link_requirement.yml @@ -36,10 +36,10 @@ jobs: steps: - name: DEBUG — dump complete event to file - run: | - echo '${{ toJson(github.event) }}' > event.json - echo "=== DEBUG: event.json written ===" - head -n 50 event.json + run: | + echo '${{ toJson(github.event) }}' > event.json + echo "=== DEBUG: event.json written ===" + head -n 50 event.json ########################################################################### # 0. TOKEN INITIALISATION From 26acefc764438c5e8b37d0214a74ff152490f1af Mon Sep 17 00:00:00 2001 From: Peter Brightwell Date: Fri, 13 Mar 2026 15:49:07 +0000 Subject: [PATCH 7/8] Upload event.json --- .github/workflows/link_requirement.yml | 20 ++++++++++++++++---- 1 file changed, 16 insertions(+), 4 deletions(-) diff --git a/.github/workflows/link_requirement.yml b/.github/workflows/link_requirement.yml index bb00e404..ce20838d 100644 --- a/.github/workflows/link_requirement.yml +++ b/.github/workflows/link_requirement.yml @@ -35,11 +35,23 @@ jobs: steps: - - name: DEBUG — dump complete event to file + - name: DEBUG — capture raw event payload run: | - echo '${{ toJson(github.event) }}' > event.json - echo "=== DEBUG: event.json written ===" - head -n 50 event.json + cp "$GITHUB_EVENT_PATH" event.json + echo "Wrote $(wc -c < event.json) bytes to event.json" + echo "First 40 lines:" + head -n 40 event.json + + - name: Upload event.json as artifact + uses: actions/upload-artifact@v4 + with: + name: event-payload + path: event.json + retention-days: 7 + + - name: Add a summary note + run: | + echo "Event payload saved as artifact **event-payload**." >> "$GITHUB_STEP_SUMMARY" ########################################################################### # 0. TOKEN INITIALISATION From 7b307990966576542f8d07db2056da570c357b11 Mon Sep 17 00:00:00 2001 From: Peter Brightwell Date: Fri, 13 Mar 2026 15:59:57 +0000 Subject: [PATCH 8/8] Removing the workflow_dispatch option as it won't work. --- .github/workflows/link_requirement.yml | 195 +++++++------------------ 1 file changed, 56 insertions(+), 139 deletions(-) diff --git a/.github/workflows/link_requirement.yml b/.github/workflows/link_requirement.yml index ce20838d..94c2ce1b 100644 --- a/.github/workflows/link_requirement.yml +++ b/.github/workflows/link_requirement.yml @@ -7,13 +7,6 @@ on: issues: types: [opened, edited] - workflow_dispatch: - inputs: - issue_number: - required: true - type: number - description: "Feature issue number to test with" - permissions: contents: read issues: write @@ -22,8 +15,8 @@ permissions: jobs: link-requirement: + # Run only for Issue Type = Feature if: > - github.event_name == 'workflow_dispatch' || github.event.issue.type == 'Feature' || github.event.issue.type.name == 'Feature' @@ -31,42 +24,13 @@ jobs: env: DEFAULT_REQUIREMENTS_REPO: ${{ vars.DEFAULT_REQUIREMENTS_REPO || 'dmf-mxl/mxl-requirements' }} - ISSUE_NUMBER: ${{ github.event.issue.number || inputs.issue_number }} + ISSUE_NUMBER: ${{ github.event.issue.number }} steps: - - - name: DEBUG — capture raw event payload - run: | - cp "$GITHUB_EVENT_PATH" event.json - echo "Wrote $(wc -c < event.json) bytes to event.json" - echo "First 40 lines:" - head -n 40 event.json - - - name: Upload event.json as artifact - uses: actions/upload-artifact@v4 - with: - name: event-payload - path: event.json - retention-days: 7 - - - name: Add a summary note - run: | - echo "Event payload saved as artifact **event-payload**." >> "$GITHUB_STEP_SUMMARY" - ########################################################################### - # 0. TOKEN INITIALISATION + # 0) TOKEN: Use org GitHub App token BEFORE any gh commands ########################################################################### - - name: Determine token mode - id: token_mode - run: | - if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then - echo "mode=dispatch" >> "$GITHUB_OUTPUT" - else - echo "mode=real" >> "$GITHUB_OUTPUT" - fi - - - name: Create GitHub App token - if: ${{ steps.token_mode.outputs.mode == 'real' }} + - name: Create GitHub App token (org) id: app_token uses: actions/create-github-app-token@v2 with: @@ -74,13 +38,9 @@ jobs: private-key: ${{ secrets.CROSS_REPO_ISSUE_ACCESS_PRIVATE_KEY }} owner: dmf-mxl - - name: Export GH_TOKEN (real) - if: ${{ steps.token_mode.outputs.mode == 'real' }} - run: echo "GH_TOKEN=${{ steps.app_token.outputs.token }}" >> "$GITHUB_ENV" - - - name: Export GH_TOKEN (dispatch) - if: ${{ steps.token_mode.outputs.mode == 'dispatch' }} - run: echo "GH_TOKEN=${{ secrets.GITHUB_TOKEN }}" >> "$GITHUB_ENV" + - name: Export GH_TOKEN + run: | + echo "GH_TOKEN=${{ steps.app_token.outputs.token }}" >> "$GITHUB_ENV" - name: Verify GH_TOKEN run: | @@ -90,100 +50,69 @@ jobs: fi echo "GH_TOKEN OK" - ########################################################################### - # 1. FETCH ISSUE TIMELINE — the *real* source of Issue Form content + # 1) FETCH ISSUE BODY (as rendered markdown) ########################################################################### - - name: Fetch timeline - id: fetch_timeline + - name: Fetch issue body + id: fetch_body run: | - echo "Fetching timeline for issue #${ISSUE_NUMBER}" - - # Timeline requires preview header - TIMELINE=$(gh api \ - -H "Accept: application/vnd.github.mockingbird-preview+json" \ - "repos/${{ github.repository }}/issues/${ISSUE_NUMBER}/timeline" || echo "[]") - - echo "$TIMELINE" > timeline.json - - echo "--- Timeline snippet ---" - head -c 500 timeline.json - echo - echo "------------------------" - - # Export to environment variable + BODY=$(gh api "repos/${{ github.repository }}/issues/${ISSUE_NUMBER}" --jq .body || echo "") { - echo "ISSUE_TIMELINE<> "$GITHUB_ENV" - ########################################################################### - # 2. EXTRACT REQUIREMENT LINK FROM TIMELINE + # 2) EXTRACT requirement_link from the rendered markdown (robust) + # Looks for a line after any heading containing "Requirement Link" ########################################################################### - - name: Extract requirement_link from timeline + - name: Extract requirement_link from body id: extract_raw run: | - echo "=== Extracting from TIMELINE ===" + # Remove CRs and non-printables, collapse whitespace + CLEAN=$(printf "%s\n" "$ISSUE_BODY" \ + | sed 's/\r//g' \ + | sed 's/[^[:print:]\t]//g' \ + | sed 's/[[:space:]]\+/ /g') - # Look for any timeline item body containing "Requirement Link" - FORM_BODY=$(echo "$ISSUE_TIMELINE" | jq -r \ - '.[] | select(.body != null) | .body' | tr -d '\u200B\u200C\u200D' ) - - echo "--- First matching form body (200 chars) ---" - echo "$FORM_BODY" | head -c 200 - echo - echo "-------------------------------------------" - - # Sanitize and extract line after heading - SANITISED=$(printf "%s\n" "$FORM_BODY" | sed 's/[^[:print:]\t]//g') - - REQ_LINE=$( - printf "%s\n" "$SANITISED" | - awk 'tolower($0) ~ /requirement[[:space:]]+link/ {getline; print}' | - xargs - ) - - echo "Extracted requirement_link='$REQ_LINE'" + # Take the line AFTER a heading containing "Requirement Link" (case/space-insensitive) + REQ_LINE=$(printf "%s\n" "$CLEAN" \ + | awk 'tolower($0) ~ /requirement[[:space:]]*link/ {getline; print}' \ + | xargs) echo "requirement_link=$REQ_LINE" >> "$GITHUB_OUTPUT" - ########################################################################### - # 3. GUARD REQUIREMENT LINK + # 3) GUARD ########################################################################### - name: Guard requirement_link exists id: guard run: | LINK="${{ steps.extract_raw.outputs.requirement_link }}" if [ -z "$LINK" ]; then - if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then - echo "Test mode: continue" - echo "has=true" >> "$GITHUB_OUTPUT" - else - echo "Missing requirement link — skipping" - echo "has=false" >> "$GITHUB_OUTPUT" - fi + echo "No requirement_link found — skipping run." + echo "has=false" >> "$GITHUB_OUTPUT" else - echo "Found requirement link: $LINK" + echo "Found requirement_link: '$LINK'" echo "has=true" >> "$GITHUB_OUTPUT" - + fi ########################################################################### - # 4. NORMALIZE requirement_link + # 4) NORMALIZE requirement_link → owner_repo, issue_num, requirement_url ########################################################################### - name: Normalize requirement_link if: ${{ steps.guard.outputs.has == 'true' }} - id: extract + id: normalize env: RAW_LINK: ${{ steps.extract_raw.outputs.requirement_link }} DEFAULT: ${{ env.DEFAULT_REQUIREMENTS_REPO }} run: | + set -euo pipefail + LINK="$(echo "$RAW_LINK" | xargs)" OWNER_REPO="" ISSUE_NUM="" - FULL_URL="" if [[ "$LINK" =~ ^[A-Za-z0-9_.-]+/[A-Za-z0-9_.-]+#[0-9]+$ ]]; then OWNER_REPO="${LINK%%#*}" @@ -196,30 +125,31 @@ jobs: ISSUE_NUM="${BASH_REMATCH[3]}" fi - FULL_URL="https://github.com/${OWNER_REPO}/issues/${ISSUE_NUM}" + if [[ -z "$OWNER_REPO" || -z "$ISSUE_NUM" ]]; then + echo "Could not normalize requirement_link='$LINK' — skipping." + exit 0 + fi + FULL_URL="https://github.com/${OWNER_REPO}/issues/${ISSUE_NUM}" echo "owner_repo=$OWNER_REPO" >> "$GITHUB_OUTPUT" echo "issue_num=$ISSUE_NUM" >> "$GITHUB_OUTPUT" echo "requirement_url=$FULL_URL" >> "$GITHUB_OUTPUT" - ########################################################################### - # 5. CREATE SUB-ISSUE LINK + # 5) SUB-ISSUE LINK: parent = requirement, child = feature ########################################################################### - name: Link as sub-issue - if: ${{ steps.extract.outputs.issue_num != '' }} + if: ${{ steps.normalize.outputs.issue_num != '' }} env: - PARENT_REPO: ${{ steps.extract.outputs.owner_repo }} - PARENT_NUMBER: ${{ steps.extract.outputs.issue_num }} + PARENT_REPO: ${{ steps.normalize.outputs.owner_repo }} + PARENT_NUMBER: ${{ steps.normalize.outputs.issue_num }} CHILD_REPO: ${{ github.repository }} CHILD_NUMBER: ${{ env.ISSUE_NUMBER }} run: | - set -e + set -euo pipefail - PARENT_NODE=$(gh issue view "https://github.com/$PARENT_REPO/issues/$PARENT_NUMBER" \ - --json id --jq .id) - CHILD_NODE=$(gh issue view "https://github.com/$CHILD_REPO/issues/$CHILD_NUMBER" \ - --json id --jq .id) + PARENT_NODE=$(gh issue view "https://github.com/$PARENT_REPO/issues/$PARENT_NUMBER" --json id --jq .id) + CHILD_NODE=$(gh issue view "https://github.com/$CHILD_REPO/issues/$CHILD_NUMBER" --json id --jq .id) gh api graphql \ -H "GraphQL-Features: sub_issues" \ @@ -235,24 +165,23 @@ jobs: } " - ########################################################################### - # 6. APPEND NORMALIZED URL TO ISSUE BODY + # 6) APPEND CLICKABLE URL TO END OF ISSUE BODY (idempotent) ########################################################################### - name: Append requirement URL to issue body - if: ${{ steps.extract.outputs.issue_num != '' }} + if: ${{ steps.normalize.outputs.issue_num != '' }} env: - REQUIRE_URL: ${{ steps.extract.outputs.requirement_url }} + REQUIRE_URL: ${{ steps.normalize.outputs.requirement_url }} CHILD_REPO: ${{ github.repository }} CHILD_NUMBER: ${{ env.ISSUE_NUMBER }} run: | - gh api "repos/$CHILD_REPO/issues/$CHILD_NUMBER" --jq .body > body_current.txt + set -euo pipefail - sed -e '//,//d' \ - body_current.txt > body_clean.txt - - printf "\n\n\n%s\n\n" \ - "$REQUIRE_URL" >> body_clean.txt + gh api "repos/$CHILD_REPO/issues/$CHILD_NUMBER" --jq .body > body_current.txt || echo -n > body_current.txt + # remove previous block + sed -e '//,//d' body_current.txt > body_clean.txt + # append new block + printf "\n\n\n%s\n\n" "$REQUIRE_URL" >> body_clean.txt NEW_BODY="$(cat body_clean.txt)" @@ -260,16 +189,4 @@ jobs: --method PATCH \ -H "Accept: application/vnd.github+json" \ "repos/$CHILD_REPO/issues/$CHILD_NUMBER" \ - -f body="$NEW_BODY" - - - ########################################################################### - # 7. SUMMARY - ########################################################################### - - name: Final summary - run: | - echo "Issue Number: ${{ env.ISSUE_NUMBER }}" - echo "Owner Repo: ${{ steps.extract.outputs.owner_repo }}" - echo "Requirement Issue: ${{ steps.extract.outputs.issue_num }}" - echo "Requirement URL: ${{ steps.extract.outputs.requirement_url }}" - echo "DONE." \ No newline at end of file + -f body="$NEW_BODY" \ No newline at end of file