bcquality/.github/workflows/upstream-sync.yml

330 lines
No EOL
15 KiB
YAML

# =============================================================================
# KPS Frontier — BCQuality Fork Upstream Sync
# =============================================================================
#
# Path: kps-frontier/bcquality-fork/.github/workflows/upstream-sync.yml
# Purpose: Track upstream microsoft/BCQuality on a non-production branch;
# notify maintainer when upstream advances; gate promotion to fork's
# `main` behind a human-reviewed PR.
# Version: v0.2 — App-identity retrofit per KPS-SignalAgent-v0_1.md.
# Changes from v0.1:
# - Notification authenticates as kps-signal-agent[bot] via App
# installation token, not bare GITHUB_TOKEN.
# - Compare URL constructed on monitored fork, not upstream
# (fixes 404-when-fork-history-diverges per spec §6).
# - Structured event log emission per spec §14 (run.started,
# advance.absent, advance.detected, issue.opened, run.completed,
# run.failed).
# - Environment scope `signal-agent-runtime` for App private key.
# - Top-level `permissions: contents: write` retained (git push to
# upstream-tracking); `issues: write` dropped (App token covers).
# Classification: Internal — Selective Showcase: Private.
#
# Identity contract:
# - upstream-tracking branch push: bare GITHUB_TOKEN (workflow contents:write)
# - Issue creation: kps-signal-agent App installation token
# =============================================================================
name: upstream-sync
on:
schedule:
- cron: '0 14 * * 1' # Monday 14:00 UTC
workflow_dispatch:
permissions:
contents: write # required for git push to upstream-tracking branch
# issues: write removed — App token covers issue creation per ADR-002
jobs:
sync:
name: Sync upstream-tracking branch and notify
runs-on: ubuntu-24.04
environment: signal-agent-runtime
env:
AGENT_NAME: kps-signal-agent
AGENT_VERSION: '0.1'
MONITORED_REPO: ${{ github.repository }}
UPSTREAM_REPO: microsoft/BCQuality
steps:
- name: Checkout fork (full history)
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Configure git identity for push
run: |
git config user.name "github-actions[bot]"
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
- name: Fetch upstream
run: |
git remote add upstream "https://github.com/${UPSTREAM_REPO}.git"
git fetch upstream main
- name: Capture run start time
id: start_time
run: |
echo "epoch_ms=$(($(date -u +%s%N)/1000000))" >> "$GITHUB_OUTPUT"
echo "rfc3339=$(date -u +%Y-%m-%dT%H:%M:%SZ)" >> "$GITHUB_OUTPUT"
- name: Compute SHA delta and build payload
id: delta
run: |
set -euo pipefail
UPSTREAM_SHA=$(git rev-parse upstream/main)
FORK_MAIN_SHA=$(git rev-parse origin/main)
if git show-ref --verify --quiet refs/remotes/origin/upstream-tracking; then
LAST_TRACKED=$(git rev-parse origin/upstream-tracking)
else
LAST_TRACKED=""
fi
COMMITS_AHEAD=$(git rev-list --count origin/main..upstream/main)
COMMIT_SUBJECTS=$(git log origin/main..upstream/main --pretty=format:'%s' -n 20 || true)
UPSTREAM_SHA_SHORT="${UPSTREAM_SHA:0:7}"
FORK_MAIN_SHA_SHORT="${FORK_MAIN_SHA:0:7}"
if [[ "$UPSTREAM_SHA" == "${LAST_TRACKED:-}" ]]; then
TRACKING_ADVANCED="false"
else
TRACKING_ADVANCED="true"
fi
{
echo "upstream_sha=$UPSTREAM_SHA"
echo "fork_main_sha=$FORK_MAIN_SHA"
echo "upstream_sha_short=$UPSTREAM_SHA_SHORT"
echo "fork_main_sha_short=$FORK_MAIN_SHA_SHORT"
echo "commits_ahead=$COMMITS_AHEAD"
echo "tracking_advanced=$TRACKING_ADVANCED"
echo 'commit_subjects<<COMMIT_SUBJECTS_EOF'
echo "$COMMIT_SUBJECTS"
echo 'COMMIT_SUBJECTS_EOF'
} >> "$GITHUB_OUTPUT"
echo "Delta: upstream=${UPSTREAM_SHA_SHORT}, fork_main=${FORK_MAIN_SHA_SHORT}, commits_ahead=${COMMITS_AHEAD}, tracking_advanced=${TRACKING_ADVANCED}"
- name: Push upstream-tracking branch
if: steps.delta.outputs.tracking_advanced == 'true'
run: |
set -euo pipefail
git push origin upstream/main:refs/heads/upstream-tracking --force-with-lease
- name: Emit signal_agent.run.started event
run: |
jq -n -c \
--arg ts "$(date -u +%Y-%m-%dT%H:%M:%SZ)" \
--arg agent "$AGENT_NAME" \
--arg agent_version "$AGENT_VERSION" \
--arg run_id "${{ github.run_id }}" \
--arg monitored_repo "$MONITORED_REPO" \
--arg trigger_type "${{ github.event_name }}" \
--arg upstream_repo "$UPSTREAM_REPO" \
'{timestamp:$ts, agent:$agent, agent_version:$agent_version, event_type:"signal_agent.run.started", workflow_run_id:$run_id, monitored_repo:$monitored_repo, payload:{trigger_type:$trigger_type, upstream_repo:$upstream_repo}}'
- name: Emit signal_agent.advance.absent event
if: steps.delta.outputs.commits_ahead == '0'
run: |
jq -n -c \
--arg ts "$(date -u +%Y-%m-%dT%H:%M:%SZ)" \
--arg agent "$AGENT_NAME" \
--arg agent_version "$AGENT_VERSION" \
--arg run_id "${{ github.run_id }}" \
--arg monitored_repo "$MONITORED_REPO" \
--arg upstream_sha "${{ steps.delta.outputs.upstream_sha }}" \
--arg fork_main_sha "${{ steps.delta.outputs.fork_main_sha }}" \
'{timestamp:$ts, agent:$agent, agent_version:$agent_version, event_type:"signal_agent.advance.absent", workflow_run_id:$run_id, monitored_repo:$monitored_repo, payload:{upstream_sha:$upstream_sha, fork_main_sha:$fork_main_sha}}'
- name: Mint signal-agent installation token
if: steps.delta.outputs.commits_ahead != '0'
id: app_token
uses: actions/create-github-app-token@v1
with:
app-id: ${{ vars.KPS_SIGNAL_AGENT_APP_ID }}
private-key: ${{ secrets.KPS_SIGNAL_AGENT_PRIVATE_KEY }}
owner: kps-frontier
repositories: bcquality-fork
- name: Emit signal_agent.advance.detected event
if: steps.delta.outputs.commits_ahead != '0'
run: |
jq -n -c \
--arg ts "$(date -u +%Y-%m-%dT%H:%M:%SZ)" \
--arg agent "$AGENT_NAME" \
--arg agent_version "$AGENT_VERSION" \
--arg run_id "${{ github.run_id }}" \
--arg monitored_repo "$MONITORED_REPO" \
--arg upstream_sha "${{ steps.delta.outputs.upstream_sha }}" \
--arg fork_main_sha "${{ steps.delta.outputs.fork_main_sha }}" \
--argjson commits_ahead ${{ steps.delta.outputs.commits_ahead }} \
'{timestamp:$ts, agent:$agent, agent_version:$agent_version, event_type:"signal_agent.advance.detected", workflow_run_id:$run_id, monitored_repo:$monitored_repo, payload:{upstream_sha:$upstream_sha, fork_main_sha:$fork_main_sha, commits_ahead:$commits_ahead}}'
- name: Open upstream-sync issue (Signal Agent)
if: steps.delta.outputs.commits_ahead != '0'
id: issue_create
env:
GH_TOKEN: ${{ steps.app_token.outputs.token }}
UPSTREAM_SHA: ${{ steps.delta.outputs.upstream_sha }}
FORK_MAIN_SHA: ${{ steps.delta.outputs.fork_main_sha }}
UPSTREAM_SHA_SHORT: ${{ steps.delta.outputs.upstream_sha_short }}
FORK_MAIN_SHA_SHORT: ${{ steps.delta.outputs.fork_main_sha_short }}
COMMITS_AHEAD: ${{ steps.delta.outputs.commits_ahead }}
COMMIT_SUBJECTS: ${{ steps.delta.outputs.commit_subjects }}
DETECTED_AT: ${{ steps.start_time.outputs.rfc3339 }}
TRIGGER_TYPE: ${{ github.event_name }}
RUN_ID: ${{ github.run_id }}
RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}
run: |
set -euo pipefail
DETECTED_AT_DATE="${DETECTED_AT%%T*}"
# Compare URL constructed on the MONITORED FORK, not upstream.
# Per Signal Agent v0.1 §6: fork-side compare is reachable regardless
# of fork history; upstream-side compare would 404 if fork main is
# not reachable from upstream.
COMPARE_URL="https://github.com/${MONITORED_REPO}/compare/${FORK_MAIN_SHA}...${UPSTREAM_SHA}"
TITLE="BCQuality upstream advance: ${COMMITS_AHEAD} commits ahead (${DETECTED_AT_DATE})"
if [[ -n "${COMMIT_SUBJECTS:-}" ]]; then
COMMITS_RENDERED=$(echo "$COMMIT_SUBJECTS" | sed 's/^/- /')
else
COMMITS_RENDERED="_(commit subjects not enumerated)_"
fi
BODY=$(cat <<EOF
## Upstream advance detected
**Upstream:** \`${UPSTREAM_REPO}\`
**Monitored fork:** \`${MONITORED_REPO}\`
**Branch comparison:** \`upstream-tracking\` vs \`main\`
**Commits ahead:** ${COMMITS_AHEAD}
**Latest upstream SHA:** \`${UPSTREAM_SHA_SHORT}\`
**Fork main SHA:** \`${FORK_MAIN_SHA_SHORT}\`
**Diff:** [\`${FORK_MAIN_SHA_SHORT}..${UPSTREAM_SHA_SHORT}\`](${COMPARE_URL}) — ${COMMITS_AHEAD} commits, full file-level diff
**Detected:** ${DETECTED_AT}
**Trigger:** ${TRIGGER_TYPE}
**Workflow run:** [#${RUN_ID}](${RUN_URL})
### Commits in delta (most recent first, up to 20)
${COMMITS_RENDERED}
### Next steps
A platform admin should review the \`upstream-tracking\` branch on \`${MONITORED_REPO}\` and decide whether to fork-merge to \`main\`. See Approved External Sources Inventory v0.1 §4.1 (BCQuality entry) and Roadmap v0.2 §7.18 for review procedure.
---
*Generated by \`kps-signal-agent[bot]\` via \`upstream-sync.yml\`. This is a deterministic Phase 1 tripwire — no classification or recommendation has been performed. See \`KPS-SignalAgent-v0_1.md\`.*
EOF
)
# Dedup: skip if an open upstream-sync issue already exists for this upstream SHA.
EXISTING_JSON=$(gh issue list \
--repo "${MONITORED_REPO}" \
--label upstream-sync \
--state open \
--search "${UPSTREAM_SHA}" \
--json number)
EXISTING_COUNT=$(echo "$EXISTING_JSON" | jq 'length')
if [[ "${EXISTING_COUNT}" -gt 0 ]]; then
EXISTING_NUMBER=$(echo "$EXISTING_JSON" | jq '.[0].number')
echo "Open upstream-sync Issue #${EXISTING_NUMBER} already exists for ${UPSTREAM_SHA}; not duplicating."
{
echo "issue_number=${EXISTING_NUMBER}"
echo "issue_url="
echo "issue_title="
echo "deduped=true"
} >> "$GITHUB_OUTPUT"
exit 0
fi
ISSUE_URL=$(gh issue create \
--repo "${MONITORED_REPO}" \
--label upstream-sync \
--title "${TITLE}" \
--body "${BODY}")
ISSUE_NUMBER=$(echo "${ISSUE_URL}" | awk -F/ '{print $NF}')
{
echo "issue_url=${ISSUE_URL}"
echo "issue_number=${ISSUE_NUMBER}"
echo "issue_title=${TITLE}"
echo "deduped=false"
} >> "$GITHUB_OUTPUT"
- name: Emit signal_agent.issue.opened event
if: steps.delta.outputs.commits_ahead != '0' && steps.issue_create.outputs.deduped == 'false'
run: |
jq -n -c \
--arg ts "$(date -u +%Y-%m-%dT%H:%M:%SZ)" \
--arg agent "$AGENT_NAME" \
--arg agent_version "$AGENT_VERSION" \
--arg run_id "${{ github.run_id }}" \
--arg monitored_repo "$MONITORED_REPO" \
--argjson issue_number ${{ steps.issue_create.outputs.issue_number }} \
--arg issue_url "${{ steps.issue_create.outputs.issue_url }}" \
--arg title "${{ steps.issue_create.outputs.issue_title }}" \
'{timestamp:$ts, agent:$agent, agent_version:$agent_version, event_type:"signal_agent.issue.opened", workflow_run_id:$run_id, monitored_repo:$monitored_repo, payload:{issue_number:$issue_number, issue_url:$issue_url, label:"upstream-sync", title:$title}}'
- name: Emit signal_agent.run.completed event
if: success()
run: |
set -euo pipefail
END_MS=$(($(date -u +%s%N)/1000000))
DURATION_MS=$((END_MS - ${{ steps.start_time.outputs.epoch_ms }}))
if [[ "${{ steps.delta.outputs.commits_ahead }}" == "0" ]]; then
jq -n -c \
--arg ts "$(date -u +%Y-%m-%dT%H:%M:%SZ)" \
--arg agent "$AGENT_NAME" \
--arg agent_version "$AGENT_VERSION" \
--arg run_id "${{ github.run_id }}" \
--arg monitored_repo "$MONITORED_REPO" \
--argjson duration_ms $DURATION_MS \
'{timestamp:$ts, agent:$agent, agent_version:$agent_version, event_type:"signal_agent.run.completed", workflow_run_id:$run_id, monitored_repo:$monitored_repo, payload:{advance_detected:false, duration_ms:$duration_ms}}'
else
jq -n -c \
--arg ts "$(date -u +%Y-%m-%dT%H:%M:%SZ)" \
--arg agent "$AGENT_NAME" \
--arg agent_version "$AGENT_VERSION" \
--arg run_id "${{ github.run_id }}" \
--arg monitored_repo "$MONITORED_REPO" \
--argjson issue_number ${{ steps.issue_create.outputs.issue_number }} \
--argjson duration_ms $DURATION_MS \
'{timestamp:$ts, agent:$agent, agent_version:$agent_version, event_type:"signal_agent.run.completed", workflow_run_id:$run_id, monitored_repo:$monitored_repo, payload:{advance_detected:true, issue_number:$issue_number, duration_ms:$duration_ms}}'
fi
- name: Emit signal_agent.run.failed event
if: failure()
run: |
jq -n -c \
--arg ts "$(date -u +%Y-%m-%dT%H:%M:%SZ)" \
--arg agent "$AGENT_NAME" \
--arg agent_version "$AGENT_VERSION" \
--arg run_id "${{ github.run_id }}" \
--arg monitored_repo "$MONITORED_REPO" \
'{timestamp:$ts, agent:$agent, agent_version:$agent_version, event_type:"signal_agent.run.failed", workflow_run_id:$run_id, monitored_repo:$monitored_repo, payload:{error_kind:"internal_error", error_message:"Workflow step failed; see job log for details.", stage:"unknown"}}'
- name: Summary
if: always()
run: |
if [[ "${{ job.status }}" == "success" ]]; then
if [[ "${{ steps.delta.outputs.commits_ahead }}" == "0" ]]; then
echo "::notice::No upstream changes (still at ${{ steps.delta.outputs.upstream_sha_short }})."
elif [[ "${{ steps.issue_create.outputs.deduped }}" == "true" ]]; then
echo "::notice::Upstream advanced ${{ steps.delta.outputs.commits_ahead }} commits, but Issue #${{ steps.issue_create.outputs.issue_number }} already open for ${{ steps.delta.outputs.upstream_sha_short }}. Skipped duplicate."
else
echo "::notice::Upstream advanced ${{ steps.delta.outputs.commits_ahead }} commits to ${{ steps.delta.outputs.upstream_sha_short }}. Issue created: ${{ steps.issue_create.outputs.issue_url }}"
fi
else
echo "::error::Workflow failed; see job log for details."
fi