Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
159 changes: 44 additions & 115 deletions .github/workflows/link_requirement.yml
Original file line number Diff line number Diff line change
Expand Up @@ -7,14 +7,6 @@ on:
issues:
types: [opened, edited]

# Manual test trigger
workflow_dispatch:
inputs:
issue_number:
required: true
type: number
description: "Issue number to test with"

permissions:
contents: read
issues: write
Expand All @@ -23,53 +15,33 @@ permissions:
jobs:
link-requirement:

# Run for Feature Issue Type OR workflow_dispatch test mode
# Run only for Issue Type = Feature
if: >
github.event_name == 'workflow_dispatch' ||
github.event.issue.type == 'Feature' ||
github.event.issue.type.name == 'Feature'

runs-on: ubuntu-latest

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:
###########################################################################
# 0. TOKEN INITIALIZATION — MUST RUN BEFORE ANY gh COMMANDS
# 0) TOKEN: Use org GitHub App token BEFORE any gh commands
###########################################################################
- 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 (issues) → GitHub App token
- 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:
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)
if: ${{ steps.token_mode.outputs.mode == 'real' }}
- name: Export GH_TOKEN
run: |
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 "GH_TOKEN=${{ secrets.GITHUB_TOKEN }}" >> "$GITHUB_ENV"

- name: Verify GH_TOKEN
run: |
if [ -z "$GH_TOKEN" ]; then
Expand All @@ -79,133 +51,106 @@ jobs:
echo "GH_TOKEN OK"

###########################################################################
# 1. FETCH ISSUE BODY — STORED MULTILINE INTO ISSUE_BODY
# 1) FETCH ISSUE BODY (as rendered markdown)
###########################################################################
- name: Fetch issue body
id: fetch
id: fetch_body
run: |
echo "Fetching body for issue #${ISSUE_NUMBER}"
BODY=$(gh api "repos/${{ github.repository }}/issues/${ISSUE_NUMBER}" --jq .body || echo "")

# Save to env var ISSUE_BODY (multiline-safe)
{
echo "ISSUE_BODY<<EOF"
echo "$BODY"
echo "EOF"
} >> "$GITHUB_ENV"

- name: DEBUG — Show first 300 chars of ISSUE_BODY
run: |
echo "--- ISSUE_BODY (truncated) ---"
echo "$ISSUE_BODY" | head -c 300
echo
echo "------------------------------"


###########################################################################
# 2. Extract requirement_link from plain markdown (NO PARSER!)
# 2) EXTRACT requirement_link from the rendered markdown (robust)
# Looks for a line after any heading containing "Requirement Link"
###########################################################################
- name: Extract requirement_link from markdown
- name: Extract requirement_link from body
id: extract_raw
run: |
echo "=== Extracting requirement_link from ISSUE_BODY ==="

# Find the line after the heading
REQ_LINE=$(printf "%s\n" "$ISSUE_BODY" | awk '
/^### Requirement Link/ {getline; print}
' | xargs)
# 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')

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 }}"
echo "LINK='$LINK'"

if [ -z "$LINK" ]; then
if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then
echo "Test mode: continuing without requirement_link"
echo "has=true" >> "$GITHUB_OUTPUT"
else
echo "No requirement_link found — skipping"
echo "has=false" >> "$GITHUB_OUTPUT"
fi
echo "No requirement_link found — skipping run."
echo "has=false" >> "$GITHUB_OUTPUT"
else
echo "OK: requirement_link found"
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
echo "Normalising '$RAW_LINK'"

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%%#*}"
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]}"
fi

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"

- name: DEBUG — Normalisation
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. Create parent child 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 -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)
CHILD_NODE=$(gh issue view "https://github.com/$CHILD_REPO/issues/$CHILD_NUMBER" --json id --jq .id)

echo "PARENT_NODE=$PARENT_NODE"
echo "CHILD_NODE=$CHILD_NODE"

gh api graphql \
-H "GraphQL-Features: sub_issues" \
-f query="
Expand All @@ -220,44 +165,28 @@ jobs:
}
"


###########################################################################
# 6. Append clickable requirement 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: |
echo "Appending requirement URL to issue #${CHILD_NUMBER}"

gh api "repos/$CHILD_REPO/issues/$CHILD_NUMBER" --jq .body > body_current.txt

sed -e '/<!--mxl:requirement:start-->/,/<!--mxl:requirement:end-->/d' \
body_current.txt > body_clean.txt
set -euo pipefail

printf "\n\n<!--mxl:requirement:start-->\n%s\n<!--mxl:requirement:end-->\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 '/<!--mxl:requirement:start-->/,/<!--mxl:requirement:end-->/d' body_current.txt > body_clean.txt
# append new block
printf "\n\n<!--mxl:requirement:start-->\n%s\n<!--mxl:requirement:end-->\n" "$REQUIRE_URL" >> body_clean.txt

NEW_BODY="$(cat body_clean.txt)"

gh api \
--method PATCH \
-H "Accept: application/vnd.github+json" \
"repos/$CHILD_REPO/issues/$CHILD_NUMBER" \
-f body="$NEW_BODY"


###########################################################################
# 7. Summary
###########################################################################
- name: DEBUG — 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 }}"
echo "Requirement URL: ${{ steps.extract.outputs.requirement_url }}"
echo "DONE."
-f body="$NEW_BODY"
Loading