From aa029881aa5e097c4dc4c63e69944a74e8707842 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 12 Jul 2026 21:50:05 +0000 Subject: [PATCH 1/5] Initial plan From 423520d3529cb199f23c4a786549b4c57862f530 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 12 Jul 2026 21:51:45 +0000 Subject: [PATCH 2/5] Implement recalculate-start-date workflow logic --- .github/workflows/recalculate-start-date.yml | 270 ++++++++++++++++++- 1 file changed, 269 insertions(+), 1 deletion(-) diff --git a/.github/workflows/recalculate-start-date.yml b/.github/workflows/recalculate-start-date.yml index 1a56017..c9758f0 100644 --- a/.github/workflows/recalculate-start-date.yml +++ b/.github/workflows/recalculate-start-date.yml @@ -1 +1,269 @@ -# github +name: Recalculate Issue Start from Project Start Date + +on: + schedule: + - cron: "*/15 * * * *" + workflow_dispatch: + +jobs: + recalc: + runs-on: ubuntu-latest + env: + GH_TOKEN: ${{ secrets.PROJECT_PAT }} + ORG: ella-system + PROJECT_NUMBER: 4 + + PROJECT_START_FIELD_NAME: Project start date + START_FIELD_NAME: Issue start + + steps: + - name: Fetch project items and recalc issue start dates + shell: bash + run: | + set -Eeuo pipefail + + echo "==> Starting recalculate-start-date workflow" + echo "UTC now: $(date -u '+%Y-%m-%dT%H:%M:%SZ')" + echo "ORG=$ORG PROJECT_NUMBER=$PROJECT_NUMBER" + + TOTAL_ITEMS=0 + START_ITEMS_COUNT=0 + UPDATED_START_ITEMS=0 + SKIPPED_SAME_START=0 + SKIPPED_MISSING_START=0 + ERROR_ITEMS=0 + + PROJECT_START_DATE="" + MIN_ISSUE_START="" + + TMP_ITEMS="$(mktemp)" + trap 'rm -f "$TMP_ITEMS"' EXIT + + echo "==> Loading project metadata and field IDs..." + PROJECT_JSON="$(gh api graphql -f query=' + query($org:String!, $number:Int!) { + organization(login:$org) { + projectV2(number:$number) { + id + title + fields(first:100) { + nodes { + ... on ProjectV2FieldCommon { id name } + } + } + } + } + }' -f org="$ORG" -F number="$PROJECT_NUMBER")" + + PROJECT_ID="$(echo "$PROJECT_JSON" | jq -r '.data.organization.projectV2.id // empty')" + PROJECT_TITLE="$(echo "$PROJECT_JSON" | jq -r '.data.organization.projectV2.title // empty')" + + PROJECT_START_FIELD_ID="$(echo "$PROJECT_JSON" | jq -r --arg n "$PROJECT_START_FIELD_NAME" '.data.organization.projectV2.fields.nodes[] | select(.name==$n) | .id' | head -n1)" + START_FIELD_ID="$(echo "$PROJECT_JSON" | jq -r --arg n "$START_FIELD_NAME" '.data.organization.projectV2.fields.nodes[] | select(.name==$n) | .id' | head -n1)" + + echo "Project title: ${PROJECT_TITLE:-}" + echo "PROJECT_ID=${PROJECT_ID:-}" + echo "PROJECT_START_FIELD_ID(${PROJECT_START_FIELD_NAME})=${PROJECT_START_FIELD_ID:-}" + echo "START_FIELD_ID(${START_FIELD_NAME})=${START_FIELD_ID:-}" + + if [ -z "${PROJECT_ID:-}" ] || [ -z "${PROJECT_START_FIELD_ID:-}" ] || [ -z "${START_FIELD_ID:-}" ]; then + echo "::error::Missing required project/field IDs." + echo "Expected field names:" + echo " - $PROJECT_START_FIELD_NAME" + echo " - $START_FIELD_NAME" + echo "Available project fields:" + echo "$PROJECT_JSON" | jq -r '.data.organization.projectV2.fields.nodes[]?.name' + exit 1 + fi + + echo "==> Fetching project items with pagination to compute anchor and offset..." + AFTER="" + PAGE=0 + + while :; do + PAGE=$((PAGE + 1)) + echo "---- Page $PAGE (after=${AFTER:-})" + + if [ -n "$AFTER" ]; then + ITEMS_JSON="$(gh api graphql -f query=' + query($org:String!, $number:Int!, $after:String, $projectStartField:String!, $startField:String!) { + organization(login:$org) { + projectV2(number:$number) { + items(first:100, after:$after) { + pageInfo { hasNextPage endCursor } + nodes { + id + projectStartValue: fieldValueByName(name: $projectStartField) { + __typename + ... on ProjectV2ItemFieldDateValue { date } + ... on ProjectV2ItemFieldTextValue { text } + } + startValue: fieldValueByName(name: $startField) { + __typename + ... on ProjectV2ItemFieldDateValue { date } + ... on ProjectV2ItemFieldTextValue { text } + } + } + } + } + } + }' \ + -f org="$ORG" \ + -F number="$PROJECT_NUMBER" \ + -f after="$AFTER" \ + -f projectStartField="$PROJECT_START_FIELD_NAME" \ + -f startField="$START_FIELD_NAME")" + else + ITEMS_JSON="$(gh api graphql -f query=' + query($org:String!, $number:Int!, $projectStartField:String!, $startField:String!) { + organization(login:$org) { + projectV2(number:$number) { + items(first:100) { + pageInfo { hasNextPage endCursor } + nodes { + id + projectStartValue: fieldValueByName(name: $projectStartField) { + __typename + ... on ProjectV2ItemFieldDateValue { date } + ... on ProjectV2ItemFieldTextValue { text } + } + startValue: fieldValueByName(name: $startField) { + __typename + ... on ProjectV2ItemFieldDateValue { date } + ... on ProjectV2ItemFieldTextValue { text } + } + } + } + } + } + }' \ + -f org="$ORG" \ + -F number="$PROJECT_NUMBER" \ + -f projectStartField="$PROJECT_START_FIELD_NAME" \ + -f startField="$START_FIELD_NAME")" + fi + + PAGE_COUNT="$(echo "$ITEMS_JSON" | jq '.data.organization.projectV2.items.nodes | length')" + echo "Items on this page: $PAGE_COUNT" + + while IFS= read -r ITEM_B64; do + [ -z "$ITEM_B64" ] && continue + TOTAL_ITEMS=$((TOTAL_ITEMS + 1)) + + ITEM_JSON="$(echo "$ITEM_B64" | base64 --decode)" + ITEM_ID="$(echo "$ITEM_JSON" | jq -r '.id')" + + project_start_raw="$(echo "$ITEM_JSON" | jq -r ' + if .projectStartValue == null then "" + elif .projectStartValue.__typename == "ProjectV2ItemFieldDateValue" then (.projectStartValue.date // "") + elif .projectStartValue.__typename == "ProjectV2ItemFieldTextValue" then (.projectStartValue.text // "") + else "" end + ')" + + start_raw="$(echo "$ITEM_JSON" | jq -r ' + if .startValue == null then "" + elif .startValue.__typename == "ProjectV2ItemFieldDateValue" then (.startValue.date // "") + elif .startValue.__typename == "ProjectV2ItemFieldTextValue" then (.startValue.text // "") + else "" end + ')" + + if [ -z "$PROJECT_START_DATE" ] && [ -n "$project_start_raw" ]; then + PROJECT_START_DATE="$project_start_raw" + echo "Using Project start date='$PROJECT_START_DATE' from item=$ITEM_ID" + fi + + if [ -n "$start_raw" ]; then + START_ITEMS_COUNT=$((START_ITEMS_COUNT + 1)) + echo "${ITEM_ID}|${start_raw}" >> "$TMP_ITEMS" + + if [ -z "$MIN_ISSUE_START" ] || [[ "$start_raw" < "$MIN_ISSUE_START" ]]; then + MIN_ISSUE_START="$start_raw" + fi + fi + done < <(echo "$ITEMS_JSON" | jq -r '.data.organization.projectV2.items.nodes[] | @base64') + + HAS_NEXT="$(echo "$ITEMS_JSON" | jq -r '.data.organization.projectV2.items.pageInfo.hasNextPage')" + END_CURSOR="$(echo "$ITEMS_JSON" | jq -r '.data.organization.projectV2.items.pageInfo.endCursor // empty')" + + if [ "$HAS_NEXT" = "true" ] && [ -n "$END_CURSOR" ]; then + AFTER="$END_CURSOR" + else + break + fi + done + + SKIPPED_MISSING_START=$((TOTAL_ITEMS - START_ITEMS_COUNT)) + + if [ -z "$PROJECT_START_DATE" ]; then + echo "Project start date is not set on any item. Nothing to do." + echo "TOTAL_ITEMS=$TOTAL_ITEMS START_ITEMS_COUNT=$START_ITEMS_COUNT SKIPPED_MISSING_START=$SKIPPED_MISSING_START" + exit 0 + fi + + if [ -z "$MIN_ISSUE_START" ]; then + echo "No items with '$START_FIELD_NAME' set. Nothing to do." + echo "TOTAL_ITEMS=$TOTAL_ITEMS START_ITEMS_COUNT=$START_ITEMS_COUNT SKIPPED_MISSING_START=$SKIPPED_MISSING_START" + exit 0 + fi + + project_start_epoch="$(date -u -d "$PROJECT_START_DATE" '+%s' 2>/dev/null || true)" + min_issue_start_epoch="$(date -u -d "$MIN_ISSUE_START" '+%s' 2>/dev/null || true)" + + if [ -z "$project_start_epoch" ] || [ -z "$min_issue_start_epoch" ]; then + echo "::error::Invalid date value(s): PROJECT_START_DATE='$PROJECT_START_DATE' MIN_ISSUE_START='$MIN_ISSUE_START'" + exit 1 + fi + + OFFSET_DAYS=$(( (project_start_epoch - min_issue_start_epoch) / 86400 )) + + echo "Computed anchor: PROJECT_START_DATE=$PROJECT_START_DATE MIN_ISSUE_START=$MIN_ISSUE_START OFFSET_DAYS=$OFFSET_DAYS" + + if [ "$OFFSET_DAYS" -eq 0 ]; then + echo "Offset is 0 days. No updates required." + echo "TOTAL_ITEMS=$TOTAL_ITEMS START_ITEMS_COUNT=$START_ITEMS_COUNT SKIPPED_MISSING_START=$SKIPPED_MISSING_START" + exit 0 + fi + + echo "==> Applying Issue start shift to all eligible items..." + while IFS='|' read -r ITEM_ID start_raw; do + [ -z "${ITEM_ID:-}" ] && continue + [ -z "${start_raw:-}" ] && continue + + new_start="$(date -u -d "$start_raw ${OFFSET_DAYS} days" '+%Y-%m-%d' 2>/dev/null || true)" + if [ -z "$new_start" ]; then + echo "WARN: item=$ITEM_ID invalid start date='$start_raw' (skipping)" + ERROR_ITEMS=$((ERROR_ITEMS + 1)) + continue + fi + + if [ "$new_start" != "$start_raw" ]; then + if ! gh api graphql -f query=' + mutation($project:ID!, $item:ID!, $field:ID!, $date:Date!) { + updateProjectV2ItemFieldValue(input:{ + projectId:$project + itemId:$item + fieldId:$field + value:{ date:$date } + }) { + projectV2Item { id } + } + }' \ + -f project="$PROJECT_ID" \ + -f item="$ITEM_ID" \ + -f field="$START_FIELD_ID" \ + -f date="$new_start" >/dev/null; then + echo "WARN: failed updating START for item=$ITEM_ID (new_start=$new_start)" + ERROR_ITEMS=$((ERROR_ITEMS + 1)) + else + UPDATED_START_ITEMS=$((UPDATED_START_ITEMS + 1)) + echo "Updated START: item=$ITEM_ID old_start=$start_raw new_start=$new_start" + fi + else + SKIPPED_SAME_START=$((SKIPPED_SAME_START + 1)) + fi + done < "$TMP_ITEMS" + + echo "==> Done" + echo "TOTAL_ITEMS=$TOTAL_ITEMS" + echo "START_ITEMS_COUNT=$START_ITEMS_COUNT UPDATED_START_ITEMS=$UPDATED_START_ITEMS SKIPPED_SAME_START=$SKIPPED_SAME_START SKIPPED_MISSING_START=$SKIPPED_MISSING_START" + echo "ERROR_ITEMS=$ERROR_ITEMS" From be47f817bd024f260c878698d68255a009f53116 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 12 Jul 2026 21:52:31 +0000 Subject: [PATCH 3/5] Harden start-date workflow comparisons and permissions --- .github/workflows/recalculate-start-date.yml | 26 +++++++++++++------- 1 file changed, 17 insertions(+), 9 deletions(-) diff --git a/.github/workflows/recalculate-start-date.yml b/.github/workflows/recalculate-start-date.yml index c9758f0..97f0b3d 100644 --- a/.github/workflows/recalculate-start-date.yml +++ b/.github/workflows/recalculate-start-date.yml @@ -5,6 +5,8 @@ on: - cron: "*/15 * * * *" workflow_dispatch: +permissions: {} + jobs: recalc: runs-on: ubuntu-latest @@ -35,6 +37,7 @@ jobs: PROJECT_START_DATE="" MIN_ISSUE_START="" + MIN_ISSUE_START_EPOCH="" TMP_ITEMS="$(mktemp)" trap 'rm -f "$TMP_ITEMS"' EXIT @@ -173,11 +176,18 @@ jobs: fi if [ -n "$start_raw" ]; then - START_ITEMS_COUNT=$((START_ITEMS_COUNT + 1)) - echo "${ITEM_ID}|${start_raw}" >> "$TMP_ITEMS" - - if [ -z "$MIN_ISSUE_START" ] || [[ "$start_raw" < "$MIN_ISSUE_START" ]]; then - MIN_ISSUE_START="$start_raw" + start_epoch="$(date -u -d "$start_raw" '+%s' 2>/dev/null || true)" + if [ -z "$start_epoch" ]; then + echo "WARN: item=$ITEM_ID invalid Issue start='$start_raw' (skipping)" + ERROR_ITEMS=$((ERROR_ITEMS + 1)) + else + START_ITEMS_COUNT=$((START_ITEMS_COUNT + 1)) + echo "${ITEM_ID}|${start_raw}" >> "$TMP_ITEMS" + + if [ -z "$MIN_ISSUE_START_EPOCH" ] || [ "$start_epoch" -lt "$MIN_ISSUE_START_EPOCH" ]; then + MIN_ISSUE_START="$start_raw" + MIN_ISSUE_START_EPOCH="$start_epoch" + fi fi fi done < <(echo "$ITEMS_JSON" | jq -r '.data.organization.projectV2.items.nodes[] | @base64') @@ -207,14 +217,12 @@ jobs: fi project_start_epoch="$(date -u -d "$PROJECT_START_DATE" '+%s' 2>/dev/null || true)" - min_issue_start_epoch="$(date -u -d "$MIN_ISSUE_START" '+%s' 2>/dev/null || true)" - - if [ -z "$project_start_epoch" ] || [ -z "$min_issue_start_epoch" ]; then + if [ -z "$project_start_epoch" ] || [ -z "$MIN_ISSUE_START_EPOCH" ]; then echo "::error::Invalid date value(s): PROJECT_START_DATE='$PROJECT_START_DATE' MIN_ISSUE_START='$MIN_ISSUE_START'" exit 1 fi - OFFSET_DAYS=$(( (project_start_epoch - min_issue_start_epoch) / 86400 )) + OFFSET_DAYS=$(( (project_start_epoch - MIN_ISSUE_START_EPOCH) / 86400 )) echo "Computed anchor: PROJECT_START_DATE=$PROJECT_START_DATE MIN_ISSUE_START=$MIN_ISSUE_START OFFSET_DAYS=$OFFSET_DAYS" From 9f63c9727a110f162c19d8f52cb29a417c6b509e Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 12 Jul 2026 21:53:10 +0000 Subject: [PATCH 4/5] Refine date offset calculations in start-date workflow --- .github/workflows/recalculate-start-date.yml | 18 ++++++++++++++---- 1 file changed, 14 insertions(+), 4 deletions(-) diff --git a/.github/workflows/recalculate-start-date.yml b/.github/workflows/recalculate-start-date.yml index 97f0b3d..86f793b 100644 --- a/.github/workflows/recalculate-start-date.yml +++ b/.github/workflows/recalculate-start-date.yml @@ -216,13 +216,18 @@ jobs: exit 0 fi - project_start_epoch="$(date -u -d "$PROJECT_START_DATE" '+%s' 2>/dev/null || true)" - if [ -z "$project_start_epoch" ] || [ -z "$MIN_ISSUE_START_EPOCH" ]; then + project_start_norm="$(date -u -d "$PROJECT_START_DATE" '+%Y-%m-%d' 2>/dev/null || true)" + min_issue_start_norm="$(date -u -d "$MIN_ISSUE_START" '+%Y-%m-%d' 2>/dev/null || true)" + + project_start_epoch="$(date -u -d "$project_start_norm" '+%s' 2>/dev/null || true)" + min_issue_start_epoch="$(date -u -d "$min_issue_start_norm" '+%s' 2>/dev/null || true)" + + if [ -z "$project_start_epoch" ] || [ -z "$min_issue_start_epoch" ]; then echo "::error::Invalid date value(s): PROJECT_START_DATE='$PROJECT_START_DATE' MIN_ISSUE_START='$MIN_ISSUE_START'" exit 1 fi - OFFSET_DAYS=$(( (project_start_epoch - MIN_ISSUE_START_EPOCH) / 86400 )) + OFFSET_DAYS=$(( (project_start_epoch - min_issue_start_epoch) / 86400 )) echo "Computed anchor: PROJECT_START_DATE=$PROJECT_START_DATE MIN_ISSUE_START=$MIN_ISSUE_START OFFSET_DAYS=$OFFSET_DAYS" @@ -237,7 +242,12 @@ jobs: [ -z "${ITEM_ID:-}" ] && continue [ -z "${start_raw:-}" ] && continue - new_start="$(date -u -d "$start_raw ${OFFSET_DAYS} days" '+%Y-%m-%d' 2>/dev/null || true)" + if [ "$OFFSET_DAYS" -ge 0 ]; then + new_start="$(date -u -d "$start_raw +$OFFSET_DAYS days" '+%Y-%m-%d' 2>/dev/null || true)" + else + abs_offset_days=$(( -OFFSET_DAYS )) + new_start="$(date -u -d "$start_raw -$abs_offset_days days" '+%Y-%m-%d' 2>/dev/null || true)" + fi if [ -z "$new_start" ]; then echo "WARN: item=$ITEM_ID invalid start date='$start_raw' (skipping)" ERROR_ITEMS=$((ERROR_ITEMS + 1)) From 358bdd22e1cba97bdd8c3e02f71bd315e88e349e Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 12 Jul 2026 21:53:44 +0000 Subject: [PATCH 5/5] Track invalid Issue start values separately --- .github/workflows/recalculate-start-date.yml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/.github/workflows/recalculate-start-date.yml b/.github/workflows/recalculate-start-date.yml index 86f793b..2a1a3c6 100644 --- a/.github/workflows/recalculate-start-date.yml +++ b/.github/workflows/recalculate-start-date.yml @@ -33,6 +33,7 @@ jobs: UPDATED_START_ITEMS=0 SKIPPED_SAME_START=0 SKIPPED_MISSING_START=0 + INVALID_START_ITEMS=0 ERROR_ITEMS=0 PROJECT_START_DATE="" @@ -179,6 +180,7 @@ jobs: start_epoch="$(date -u -d "$start_raw" '+%s' 2>/dev/null || true)" if [ -z "$start_epoch" ]; then echo "WARN: item=$ITEM_ID invalid Issue start='$start_raw' (skipping)" + INVALID_START_ITEMS=$((INVALID_START_ITEMS + 1)) ERROR_ITEMS=$((ERROR_ITEMS + 1)) else START_ITEMS_COUNT=$((START_ITEMS_COUNT + 1)) @@ -202,7 +204,7 @@ jobs: fi done - SKIPPED_MISSING_START=$((TOTAL_ITEMS - START_ITEMS_COUNT)) + SKIPPED_MISSING_START=$((TOTAL_ITEMS - START_ITEMS_COUNT - INVALID_START_ITEMS)) if [ -z "$PROJECT_START_DATE" ]; then echo "Project start date is not set on any item. Nothing to do."