Skip to content

Upstream Agent Merge #605

Upstream Agent Merge

Upstream Agent Merge #605

name: Upstream Agent Merge
'on':
schedule:
- cron: '0 * * * *'
workflow_dispatch:
inputs:
force:
description: 'Run even if no new upstream release is detected'
required: false
type: boolean
default: false
force_release:
description: 'Release even when the fresh /cl audit finds no package changelog entries'
required: false
type: boolean
default: false
model:
description: 'Codex model override (optional)'
required: false
type: string
default: ''
codex_profile:
description: 'Codex profile to use (defaults to quotio)'
required: false
type: string
default: 'quotio'
dry_run:
description: 'Run merge + QA locally, but do not push, merge, tag, or publish'
required: false
type: boolean
default: false
permissions:
contents: write
issues: write
pull-requests: write
checks: read
concurrency:
group: upstream-agent-merge
cancel-in-progress: false
jobs:
merge-and-release:
runs-on: ubuntu-latest
timeout-minutes: 120
env:
UPSTREAM_URL: https://github.com/badlogic/pi-mono.git
MODEL_INPUT: ${{ github.event.inputs.model }}
CODEX_PROFILE_INPUT: ${{ github.event.inputs.codex_profile }}
DEFAULT_MODEL: gpt-5.5
DEFAULT_CODEX_PROFILE: quotio
EVIDENCE_DIR: local-ignore/qa-evidence/upstream-agent
GH_REPO: ${{ github.repository }}
steps:
- name: Checkout
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10
with:
fetch-depth: 0
persist-credentials: false
- name: Configure git and upstream remote
env:
GH_TOKEN: ${{ secrets.UPSTREAM_AUTOMATION_TOKEN }}
run: |
if [ -z "${GH_TOKEN}" ]; then
echo "::error::UPSTREAM_AUTOMATION_TOKEN is required so branch and tag pushes trigger follow-up workflows."
exit 1
fi
git config user.name "senpi-merge-bot"
git config user.email "actions@github.com"
git fetch --quiet origin main
git checkout -B main origin/main
git remote add upstream "${UPSTREAM_URL}" 2>/dev/null || git remote set-url upstream "${UPSTREAM_URL}"
git fetch --quiet --tags upstream
- name: Detect new upstream release
id: detect
run: |
ARGS=""
if [ "${{ github.event.inputs.force }}" = "true" ]; then ARGS="--force"; fi
node scripts/check-upstream-release.mjs $ARGS
- name: Setup Node.js
if: steps.detect.outputs.proceed == 'true'
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e
with:
node-version: '24'
registry-url: 'https://registry.npmjs.org/'
package-manager-cache: false
- name: Setup Bun
if: steps.detect.outputs.proceed == 'true'
uses: oven-sh/setup-bun@4bc047ad259df6fc24a6c9b0f9a0cb08cf17fbe5 # v2.0.1
with:
bun-version: 1.2.20
- name: Install system dependencies
if: steps.detect.outputs.proceed == 'true'
run: |
sudo env DEBIAN_FRONTEND=noninteractive NEEDRESTART_MODE=a apt-get update
sudo env DEBIAN_FRONTEND=noninteractive NEEDRESTART_MODE=a apt-get install -y \
libcairo2-dev libpango1.0-dev libjpeg-dev libgif-dev librsvg2-dev fd-find ripgrep tmux
sudo ln -sf "$(which fdfind)" /usr/local/bin/fd
- name: Install dependencies
if: steps.detect.outputs.proceed == 'true'
run: npm install --ignore-scripts --no-audit --no-fund
- name: Install Codex and LazyCodex
if: steps.detect.outputs.proceed == 'true'
run: |
npm install -g --force --ignore-scripts @openai/codex@latest lazycodex-ai@latest oh-my-openagent@latest
mkdir -p "$EVIDENCE_DIR"
{
echo "codex: $(codex --version)"
echo "lazycodex: $(lazycodex --version)"
echo "lazycodex-ai: $(lazycodex-ai --version)"
npm view @openai/codex dist-tags.latest dist.integrity --json
npm view lazycodex-ai dist-tags.latest dist.integrity --json
npm view oh-my-openagent dist-tags.latest dist.integrity --json
} | tee "$EVIDENCE_DIR/tool-versions.txt"
- name: Hydrate Codex and senpi secrets
if: steps.detect.outputs.proceed == 'true'
env:
CODEX_AUTH_JSON_B64: ${{ secrets.CODEX_AUTH_JSON_B64 }}
CODEX_CONFIG_TOML_B64: ${{ secrets.CODEX_CONFIG_TOML_B64 }}
CODEX_QUOTIO_CONFIG_TOML_B64: ${{ secrets.CODEX_QUOTIO_CONFIG_TOML_B64 }}
CODEX_CCAPI_CONFIG_TOML_B64: ${{ secrets.CODEX_CCAPI_CONFIG_TOML_B64 }}
QUOTIO_API_KEY: ${{ secrets.QUOTIO_API_KEY }}
QUOTIO_BASE_URL: ${{ secrets.QUOTIO_BASE_URL }}
SENPI_AUTH_JSON_B64: ${{ secrets.SENPI_AUTH_JSON_B64 }}
SENPI_MODELS_JSON_B64: ${{ secrets.SENPI_MODELS_JSON_B64 }}
SENPI_MODELS_JSON_GZ_B64: ${{ secrets.SENPI_MODELS_JSON_GZ_B64 }}
SENPI_SETTINGS_JSON_B64: ${{ secrets.SENPI_SETTINGS_JSON_B64 }}
run: |
set -euo pipefail
umask 077
mkdir -p "$HOME/.codex" "$HOME/.senpi/agent" "$EVIDENCE_DIR"
write_secret_b64() {
local value="$1"
local path="$2"
if [ -n "$value" ]; then
printf '%s' "$value" | base64 -d > "$path"
chmod 600 "$path"
fi
}
write_secret_gz_b64() {
local value="$1"
local path="$2"
if [ -n "$value" ]; then
printf '%s' "$value" | base64 -d | gunzip > "$path"
chmod 600 "$path"
fi
}
write_secret_b64 "$CODEX_AUTH_JSON_B64" "$HOME/.codex/auth.json"
write_secret_b64 "$CODEX_CONFIG_TOML_B64" "$HOME/.codex/config.toml"
write_secret_b64 "$CODEX_QUOTIO_CONFIG_TOML_B64" "$HOME/.codex/quotio.config.toml"
write_secret_b64 "$CODEX_CCAPI_CONFIG_TOML_B64" "$HOME/.codex/ccapi.config.toml"
write_secret_b64 "$SENPI_AUTH_JSON_B64" "$HOME/.senpi/agent/auth.json"
write_secret_b64 "$SENPI_MODELS_JSON_B64" "$HOME/.senpi/agent/models.json"
if [ ! -s "$HOME/.senpi/agent/models.json" ]; then
write_secret_gz_b64 "$SENPI_MODELS_JSON_GZ_B64" "$HOME/.senpi/agent/models.json"
fi
write_secret_b64 "$SENPI_SETTINGS_JSON_B64" "$HOME/.senpi/agent/settings.json"
if [ ! -s "$HOME/.codex/config.toml" ]; then
if [ -z "$QUOTIO_API_KEY" ]; then
echo "::error::Provide CODEX_CONFIG_TOML_B64 or QUOTIO_API_KEY for the generated quotio profile."
exit 1
fi
{
echo 'model = "gpt-5.5"'
echo 'model_provider = "quotio"'
echo 'model_reasoning_effort = "high"'
echo 'approval_policy = "never"'
echo 'sandbox_mode = "danger-full-access"'
echo 'network_access = "enabled"'
echo 'shell_environment_policy = { inherit = "all" }'
echo
echo '[model_providers.quotio]'
echo 'name = "Quotio"'
echo "base_url = \"${QUOTIO_BASE_URL:-https://quotio.mengmota.com/v1}\""
echo 'env_key = "QUOTIO_API_KEY"'
echo 'wire_api = "responses"'
echo 'requires_openai_auth = false'
} > "$HOME/.codex/config.toml"
chmod 600 "$HOME/.codex/config.toml"
fi
if [ ! -s "$HOME/.codex/quotio.config.toml" ]; then
{
echo 'model = "gpt-5.5"'
echo 'model_provider = "quotio"'
echo 'service_tier = "fast"'
echo 'model_reasoning_effort = "high"'
echo 'approval_policy = "never"'
echo 'sandbox_mode = "danger-full-access"'
echo 'network_access = "enabled"'
echo 'shell_environment_policy = { inherit = "all" }'
} > "$HOME/.codex/quotio.config.toml"
chmod 600 "$HOME/.codex/quotio.config.toml"
fi
test -s "$HOME/.codex/config.toml"
if [ ! -s "$HOME/.codex/auth.json" ] && [ -z "$QUOTIO_API_KEY" ]; then
echo "::error::Provide CODEX_AUTH_JSON_B64 or QUOTIO_API_KEY."
exit 1
fi
find "$HOME/.codex" "$HOME/.senpi/agent" -maxdepth 1 -type f -printf '%p %m\n' \
| sed "s#${HOME}#~#" | tee "$EVIDENCE_DIR/secret-files.txt"
- name: Run Codex merge + changelog agent
id: agent
if: steps.detect.outputs.proceed == 'true'
env:
UPSTREAM_TAG: ${{ steps.detect.outputs.tag }}
QUOTIO_API_KEY: ${{ secrets.QUOTIO_API_KEY }}
run: |
set -euo pipefail
BRANCH="automation/upstream-${UPSTREAM_TAG}-${GITHUB_RUN_ID}"
git checkout -B "$BRANCH"
{
cat .github/agent/merge-driver.md
echo
echo "The latest upstream release tag to integrate is ${UPSTREAM_TAG}."
} > /tmp/codex-merge-driver.md
MODEL="${MODEL_INPUT:-$DEFAULT_MODEL}"
PROFILE="${CODEX_PROFILE_INPUT:-$DEFAULT_CODEX_PROFILE}"
set +e
codex exec \
--profile "$PROFILE" \
--cd "$GITHUB_WORKSPACE" \
--dangerously-bypass-approvals-and-sandbox \
--dangerously-bypass-hook-trust \
--output-last-message /tmp/codex-last-message.md \
-m "$MODEL" \
- < /tmp/codex-merge-driver.md 2>&1 | tee /tmp/codex-agent.log
CODEX_STATUS="${PIPESTATUS[0]}"
set -e
RESULT="$(grep -E '^MERGE_RESULT:' /tmp/codex-agent.log /tmp/codex-last-message.md 2>/dev/null | tail -1 | awk '{print $2}')"
if [ -z "$RESULT" ] && [ "$CODEX_STATUS" -ne 0 ]; then RESULT="AGENT_FAILED"; fi
if [ -f .github/agent/last-merge-report.md ]; then
node scripts/archive-upstream-agent-report.mjs
fi
echo "result=${RESULT:-AGENT_FAILED}" >> "$GITHUB_OUTPUT"
echo "branch=${BRANCH}" >> "$GITHUB_OUTPUT"
echo "Agent result: ${RESULT:-AGENT_FAILED}"
- name: Verify branch and capture QA evidence
if: steps.detect.outputs.proceed == 'true' && steps.agent.outputs.result == 'CLEAN_PR_READY'
run: |
set -euo pipefail
mkdir -p "$EVIDENCE_DIR"
git status --porcelain | tee "$EVIDENCE_DIR/git-status-before-qa.txt"
test ! -s "$EVIDENCE_DIR/git-status-before-qa.txt"
npm run build 2>&1 | tee "$EVIDENCE_DIR/npm-build.txt"
npm run check 2>&1 | tee "$EVIDENCE_DIR/npm-check.txt"
git status --porcelain | tee "$EVIDENCE_DIR/git-status-after-check.txt"
test ! -s "$EVIDENCE_DIR/git-status-after-check.txt"
npm test 2>&1 | tee "$EVIDENCE_DIR/npm-test.txt"
PI_OFFLINE=1 node .agents/skills/senpi-qa/scripts/lib/common.mjs --self-check 2>&1 | tee "$EVIDENCE_DIR/senpi-qa-common.txt"
PI_OFFLINE=1 node .agents/skills/senpi-qa/scripts/mock-loop.mjs --self-test --evidence upstream-agent-mock-loop 2>&1 | tee "$EVIDENCE_DIR/senpi-qa-mock-loop.txt"
PI_OFFLINE=1 node .agents/skills/senpi-qa/scripts/cli-smoke.mjs --self-test 2>&1 | tee "$EVIDENCE_DIR/senpi-qa-cli-smoke.txt"
if command -v tmux >/dev/null 2>&1; then
PI_OFFLINE=1 node .agents/skills/senpi-qa/scripts/tui-smoke.mjs --self-test --driver tmux --evidence upstream-agent-tui 2>&1 | tee "$EVIDENCE_DIR/senpi-qa-tui.txt"
fi
- name: Open and merge upstream PR
id: pr
if: steps.detect.outputs.proceed == 'true' && steps.agent.outputs.result == 'CLEAN_PR_READY' && github.event.inputs.dry_run != 'true'
env:
GH_TOKEN: ${{ secrets.UPSTREAM_AUTOMATION_TOKEN }}
UPSTREAM_TAG: ${{ steps.detect.outputs.tag }}
UPSTREAM_SHA: ${{ steps.detect.outputs.upstream_head_sha }}
run: |
set -euo pipefail
BRANCH="${{ steps.agent.outputs.branch }}"
git fetch --quiet origin main
if git merge-base --is-ancestor "$UPSTREAM_SHA" origin/main; then
echo "merged=false" >> "$GITHUB_OUTPUT"
echo "Upstream main for ${UPSTREAM_TAG} is already merged into origin/main; skipping stale PR and release."
exit 0
fi
git push "https://x-access-token:${GH_TOKEN}@github.com/${GITHUB_REPOSITORY}.git" "HEAD:$BRANCH"
for attempt in {1..12}; do
AHEAD_BY="$(gh api "repos/${GITHUB_REPOSITORY}/compare/main...${BRANCH}" --jq '.ahead_by // 0' 2>/dev/null || echo 0)"
if [ "$AHEAD_BY" -gt 0 ]; then
break
fi
if [ "$attempt" = "12" ]; then
echo "::error::Pushed ${BRANCH}, but GitHub compare still does not show commits ahead of main."
exit 1
fi
sleep 5
done
BODY_FILE=/tmp/upstream-pr-body.md
{
echo "Automated upstream sync for \`${UPSTREAM_TAG}\`."
echo
echo "- Upstream SHA: \`${UPSTREAM_SHA}\`"
echo "- Evidence artifact: \`upstream-agent-qa-${UPSTREAM_TAG}\`"
echo "- Release is evaluated only after this PR is merged with a merge commit."
echo
echo "QA completed before PR creation:"
echo "- \`npm run build\`"
echo "- \`npm run check\`"
echo "- \`npm test\`"
echo "- \`senpi-qa\` common, mock-loop, CLI smoke, and tmux TUI smoke when available"
} > "$BODY_FILE"
PR_HEAD="${GITHUB_REPOSITORY_OWNER}:${BRANCH}"
EXISTING="$(gh pr list --repo "$GITHUB_REPOSITORY" --base main --head "$PR_HEAD" --state open --json number --jq '.[0].number // empty')"
if [ -n "$EXISTING" ]; then
PR_NUMBER="$EXISTING"
gh pr edit "$PR_NUMBER" --repo "$GITHUB_REPOSITORY" --title "sync: upstream ${UPSTREAM_TAG}" --body-file "$BODY_FILE"
else
PR_URL="$(gh pr create --repo "$GITHUB_REPOSITORY" --base main --head "$PR_HEAD" --title "sync: upstream ${UPSTREAM_TAG}" --body-file "$BODY_FILE")"
PR_NUMBER="${PR_URL##*/}"
fi
echo "number=${PR_NUMBER}" >> "$GITHUB_OUTPUT"
echo "url=$(gh pr view "$PR_NUMBER" --repo "$GITHUB_REPOSITORY" --json url --jq .url)" >> "$GITHUB_OUTPUT"
QA_COMMENT_FILE=/tmp/upstream-pr-qa.md
{
echo "## QA evidence"
echo
echo "- Workflow run: https://github.com/${GITHUB_REPOSITORY}/actions/runs/${GITHUB_RUN_ID}"
echo "- Artifact: \`upstream-agent-qa-${UPSTREAM_TAG}\`"
echo "- Upstream SHA: \`${UPSTREAM_SHA}\`"
echo
echo "Evidence files captured before this PR was opened:"
find "$EVIDENCE_DIR" -maxdepth 1 -type f -printf "- \`%f\`\n" | sort
} > "$QA_COMMENT_FILE"
gh pr comment "$PR_NUMBER" --repo "$GITHUB_REPOSITORY" --body-file "$QA_COMMENT_FILE"
for attempt in {1..24}; do
CHECK_COUNT="$(gh pr checks "$PR_NUMBER" --repo "$GITHUB_REPOSITORY" --json name --jq '[.[] | select(.name == "Check and test")] | length' 2>/dev/null || echo 0)"
if [ "$CHECK_COUNT" -gt 0 ]; then
break
fi
if [ "$attempt" = "24" ]; then
echo "::error::PR ${PR_NUMBER} has no reported Check and test CI check yet."
exit 1
fi
sleep 5
done
gh pr checks "$PR_NUMBER" --repo "$GITHUB_REPOSITORY" --watch --fail-fast --interval 30
PR_INFO="$(gh pr view "$PR_NUMBER" --repo "$GITHUB_REPOSITORY" --json headRefName,headRefOid)"
HEAD_BRANCH="$(printf '%s' "$PR_INFO" | jq -r .headRefName)"
HEAD_SHA="$(printf '%s' "$PR_INFO" | jq -r .headRefOid)"
gh pr merge "$PR_NUMBER" --repo "$GITHUB_REPOSITORY" --merge --auto --match-head-commit "$HEAD_SHA"
for attempt in {1..40}; do
PR_STATE="$(gh pr view "$PR_NUMBER" --repo "$GITHUB_REPOSITORY" --json state --jq .state)"
if [ "$PR_STATE" = "MERGED" ]; then
break
fi
if [ "$PR_STATE" = "CLOSED" ]; then
echo "::error::PR ${PR_NUMBER} closed without merging after auto-merge was enabled."
exit 1
fi
if [ "$attempt" = "40" ]; then
echo "::error::PR ${PR_NUMBER} did not merge after auto-merge was enabled."
exit 1
fi
sleep 15
done
echo "merged=true" >> "$GITHUB_OUTPUT"
if [ -n "$HEAD_BRANCH" ] && [ "$HEAD_BRANCH" != "main" ]; then
git push "https://x-access-token:${GH_TOKEN}@github.com/${GITHUB_REPOSITORY}.git" --delete "$HEAD_BRANCH" \
|| echo "::warning::Could not delete merged branch ${HEAD_BRANCH}; continuing release."
fi
- name: Fresh /cl release audit
id: release_audit
if: steps.detect.outputs.proceed == 'true' && steps.agent.outputs.result == 'CLEAN_PR_READY' && steps.pr.outputs.merged == 'true' && github.event.inputs.dry_run != 'true'
env:
QUOTIO_API_KEY: ${{ secrets.QUOTIO_API_KEY }}
run: |
set -euo pipefail
git checkout main
git fetch origin main --tags
git merge --ff-only origin/main
MODEL="${MODEL_INPUT:-$DEFAULT_MODEL}"
PROFILE="${CODEX_PROFILE_INPUT:-$DEFAULT_CODEX_PROFILE}"
set +e
codex exec \
--profile "$PROFILE" \
--cd "$GITHUB_WORKSPACE" \
--dangerously-bypass-approvals-and-sandbox \
--dangerously-bypass-hook-trust \
--output-last-message /tmp/codex-release-last-message.md \
-m "$MODEL" \
- < .github/agent/release-driver.md 2>&1 | tee /tmp/codex-release-agent.log
CODEX_STATUS="${PIPESTATUS[0]}"
set -e
DECISION="$(grep -E '^RELEASE_DECISION:' /tmp/codex-release-agent.log /tmp/codex-release-last-message.md 2>/dev/null | tail -1 | awk '{print $2}')"
if [ -z "$DECISION" ] && [ "$CODEX_STATUS" -ne 0 ]; then DECISION="FAILED"; fi
if [ -f .github/agent/last-release-audit-report.md ]; then
mkdir -p "$EVIDENCE_DIR"
cp .github/agent/last-release-audit-report.md "$EVIDENCE_DIR/last-release-audit-report.md"
rm .github/agent/last-release-audit-report.md
fi
echo "decision=${DECISION:-FAILED}" >> "$GITHUB_OUTPUT"
- name: Decide release worthiness
id: worthy
if: steps.detect.outputs.proceed == 'true' && steps.agent.outputs.result == 'CLEAN_PR_READY' && steps.pr.outputs.merged == 'true' && github.event.inputs.dry_run != 'true' && steps.release_audit.outputs.decision == 'RELEASE'
run: |
ARGS=()
if [ "${{ github.event.inputs.force_release }}" = "true" ]; then ARGS+=(--force-release); fi
node scripts/upstream-release-worthy.mjs "${ARGS[@]}" | tee "$EVIDENCE_DIR/release-worthy.txt"
node scripts/upstream-release-worthy.mjs "${ARGS[@]}" >> "$GITHUB_OUTPUT"
- name: Release after PR merge
id: release
if: steps.detect.outputs.proceed == 'true' && steps.agent.outputs.result == 'CLEAN_PR_READY' && steps.pr.outputs.merged == 'true' && github.event.inputs.dry_run != 'true' && steps.release_audit.outputs.decision == 'RELEASE' && (steps.worthy.outputs.release_worthy == 'true' || github.event.inputs.force_release == 'true')
env:
GH_TOKEN: ${{ secrets.UPSTREAM_AUTOMATION_TOKEN }}
PI_ALLOW_LOCKFILE_CHANGE: '1'
npm_config_min_release_age: '0'
SENPI_SKIP_PM_VERIFY: '1'
run: |
git remote set-url origin "https://x-access-token:${GH_TOKEN}@github.com/${GITHUB_REPOSITORY}.git"
node scripts/release.mjs
- name: Dry-run release preview
id: dry_run_release
if: steps.detect.outputs.proceed == 'true' && steps.agent.outputs.result == 'CLEAN_PR_READY' && github.event.inputs.dry_run == 'true'
env:
PI_ALLOW_LOCKFILE_CHANGE: '1'
npm_config_min_release_age: '0'
SENPI_SKIP_PM_VERIFY: '1'
run: |
git checkout main
git merge --no-ff --no-commit "${{ steps.agent.outputs.branch }}"
node scripts/release.mjs --dry-run
- name: Upload QA evidence
if: always() && steps.detect.outputs.proceed == 'true'
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a
with:
name: upstream-agent-qa-${{ steps.detect.outputs.tag || 'unknown' }}
path: local-ignore/qa-evidence/
if-no-files-found: warn
- name: Open release audit failure issue
if: steps.detect.outputs.proceed == 'true' && steps.agent.outputs.result == 'CLEAN_PR_READY' && github.event.inputs.dry_run != 'true' && (steps.release_audit.outputs.decision == 'FAILED' || steps.release_audit.outputs.decision == '')
env:
GH_TOKEN: ${{ secrets.UPSTREAM_AUTOMATION_TOKEN }}
run: |
TAG="${{ steps.detect.outputs.tag }}"
BODY_FILE=/tmp/release-issue-body.md
{
echo "Automated upstream release audit for \`${TAG}\` failed after the sync PR merged."
echo
echo "## Release audit report"
echo
if [ -f "$EVIDENCE_DIR/last-release-audit-report.md" ]; then cat "$EVIDENCE_DIR/last-release-audit-report.md"; else echo "(no report written)"; fi
echo
echo "Resolve manually, then re-run \`Upstream Agent Merge\` with \`force=true\` if needed."
} > "$BODY_FILE"
gh label create sync-conflict --repo "$GITHUB_REPOSITORY" --color D73A4A --description "Automated upstream sync needs maintainer attention" || true
gh issue create \
--repo "$GITHUB_REPOSITORY" \
--title "upstream release audit needs attention: ${TAG}" \
--label "sync-conflict" \
--body-file "$BODY_FILE" || gh issue create \
--repo "$GITHUB_REPOSITORY" \
--title "upstream release audit needs attention: ${TAG}" \
--body-file "$BODY_FILE"
exit 1
- name: Open conflict / failure issue
if: steps.detect.outputs.proceed == 'true' && steps.agent.outputs.result != 'CLEAN_PR_READY' && steps.agent.outputs.result != 'NO_RELEASE_NEEDED' && steps.agent.outputs.result != ''
env:
GH_TOKEN: ${{ secrets.UPSTREAM_AUTOMATION_TOKEN }}
run: |
TAG="${{ steps.detect.outputs.tag }}"
RESULT="${{ steps.agent.outputs.result }}"
BODY_FILE=/tmp/issue-body.md
{
echo "Automated upstream merge for \`${TAG}\` did not complete cleanly (result: \`${RESULT}\`)."
echo
echo "## Agent report"
echo
if [ -f "$EVIDENCE_DIR/last-merge-report.md" ]; then cat "$EVIDENCE_DIR/last-merge-report.md"; else echo "(no report written)"; fi
echo
echo "Resolve manually, then re-run \`Upstream Agent Merge\` with \`force=true\`."
} > "$BODY_FILE"
gh label create sync-conflict --repo "$GITHUB_REPOSITORY" --color D73A4A --description "Automated upstream sync needs maintainer attention" || true
gh issue create \
--repo "$GITHUB_REPOSITORY" \
--title "upstream merge needs attention: ${TAG} (${RESULT})" \
--label "sync-conflict" \
--body-file "$BODY_FILE" || gh issue create \
--repo "$GITHUB_REPOSITORY" \
--title "upstream merge needs attention: ${TAG} (${RESULT})" \
--body-file "$BODY_FILE"
exit 1
- name: No release needed
id: no_release_needed
if: steps.detect.outputs.proceed == 'true' && (steps.agent.outputs.result == 'NO_RELEASE_NEEDED' || steps.pr.outputs.merged == 'false' || steps.release_audit.outputs.decision == 'SKIP' || steps.worthy.outputs.release_worthy == 'false')
run: |
echo "NO_RELEASE_NEEDED: upstream sync completed without release-worthy package changes."
- name: No new upstream release
id: no_new_upstream
if: steps.detect.outputs.proceed != 'true'
run: |
echo "No new upstream release. Current pin: ${{ steps.detect.outputs.current_tag }}. Nothing to do."
- name: Summarize merge and release outcome
if: always()
env:
PROCEED: ${{ steps.detect.outputs.proceed || 'false' }}
CURRENT_TAG: ${{ steps.detect.outputs.current_tag || '' }}
UPSTREAM_TAG: ${{ steps.detect.outputs.tag || '' }}
UPSTREAM_SHA: ${{ steps.detect.outputs.sha || '' }}
AGENT_RESULT: ${{ steps.agent.outputs.result || 'skipped' }}
PR_URL: ${{ steps.pr.outputs.url || '' }}
PR_MERGED: ${{ steps.pr.outputs.merged || 'skipped' }}
RELEASE_DECISION: ${{ steps.release_audit.outputs.decision || 'skipped' }}
RELEASE_WORTHY: ${{ steps.worthy.outputs.release_worthy || 'skipped' }}
DRY_RUN: ${{ github.event.inputs.dry_run || 'false' }}
FORCE_RELEASE: ${{ github.event.inputs.force_release || 'false' }}
PR_OUTCOME: ${{ steps.pr.outcome || 'skipped' }}
RELEASE_OUTCOME: ${{ steps.release.outcome || 'skipped' }}
DRY_RUN_RELEASE_OUTCOME: ${{ steps.dry_run_release.outcome || 'skipped' }}
NO_RELEASE_OUTCOME: ${{ steps.no_release_needed.outcome || 'skipped' }}
NO_NEW_UPSTREAM_OUTCOME: ${{ steps.no_new_upstream.outcome || 'skipped' }}
run: |
set -euo pipefail
follow_up="none"
if [ "$PROCEED" != "true" ]; then
follow_up="none: no new upstream release"
elif [ "$AGENT_RESULT" = "NO_RELEASE_NEEDED" ]; then
follow_up="none: upstream was already integrated; no release needed"
elif [ "$AGENT_RESULT" != "CLEAN_PR_READY" ]; then
follow_up="attention issue or failure path: agent result ${AGENT_RESULT}"
elif [ "$DRY_RUN" = "true" ]; then
follow_up="dry-run release preview"
elif [ "$PR_MERGED" = "false" ]; then
follow_up="none: upstream release was already merged by another run"
elif [ "$PR_OUTCOME" != "success" ]; then
follow_up="PR step did not complete; inspect logs"
elif [ "$RELEASE_OUTCOME" = "success" ]; then
follow_up="release executed"
elif [ "$NO_RELEASE_OUTCOME" = "success" ]; then
follow_up="none: release not needed"
else
follow_up="no release step executed; inspect audit and worthy outputs"
fi
{
echo "## Merge and release outcome"
echo
echo "| Item | Value |"
echo "| --- | --- |"
echo "| New upstream work detected | \`${PROCEED}\` |"
echo "| Current pinned tag | \`${CURRENT_TAG:-unknown}\` |"
echo "| Upstream tag | \`${UPSTREAM_TAG:-none}\` |"
echo "| Upstream SHA | \`${UPSTREAM_SHA:-none}\` |"
echo "| Merge agent result | \`${AGENT_RESULT}\` |"
echo "| Sync PR | ${PR_URL:-none} |"
echo "| PR merge step | \`${PR_OUTCOME}\` |"
echo "| PR merged by this run | \`${PR_MERGED}\` |"
echo "| Release audit decision | \`${RELEASE_DECISION}\` |"
echo "| Release-worthy changes | \`${RELEASE_WORTHY}\` |"
echo "| Force release input | \`${FORCE_RELEASE}\` |"
echo "| Dry run input | \`${DRY_RUN}\` |"
echo "| Release step | \`${RELEASE_OUTCOME}\` |"
echo "| Dry-run release preview | \`${DRY_RUN_RELEASE_OUTCOME}\` |"
echo "| No-release path | \`${NO_RELEASE_OUTCOME}\` |"
echo "| No-new-upstream path | \`${NO_NEW_UPSTREAM_OUTCOME}\` |"
echo
echo "**Follow-up action:** ${follow_up}"
} >> "$GITHUB_STEP_SUMMARY"