Skip to content

Upstream Sync

Upstream Sync #3

Workflow file for this run

name: Upstream Sync
on:
schedule:
- cron: '17 */6 * * *'
workflow_dispatch:
concurrency:
group: unity-builder-upstream-sync
cancel-in-progress: false
permissions:
contents: read
env:
UPSTREAM_REPOSITORY: game-ci/unity-builder
SYNC_BRANCH: automation/upstream-sync
TRACKING_ISSUE_TITLE: '[upstream-sync] Review game-ci/unity-builder update'
jobs:
discover:
runs-on: ubuntu-latest
permissions:
contents: write
pull-requests: write
issues: write
outputs:
candidate-sha: ${{ steps.sync.outputs.candidate_sha }}
pr-number: ${{ steps.sync.outputs.pr_number }}
upstream-sha: ${{ steps.sync.outputs.upstream_sha }}
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
persist-credentials: false
- id: sync
name: Discover and classify upstream changes
env:
GH_TOKEN: ${{ github.token }}
run: |
set -Eeuo pipefail
tracked=unknown
upstream_sha=unknown
canonical_issue() {
local numbers duplicate
gh api --paginate "repos/${GITHUB_REPOSITORY}/issues?state=open&per_page=100" \
--jq '.[] | select((has("pull_request") | not) and .title == env.TRACKING_ISSUE_TITLE) | .number' \
| sort -n >/tmp/upstream-issue-numbers
mapfile -t numbers </tmp/upstream-issue-numbers
for duplicate in "${numbers[@]:1}"; do
gh issue close "$duplicate" --comment "Closing duplicate; continuing in #${numbers[0]}." >/dev/null
done
printf '%s' "${numbers[0]:-}"
}
upsert_issue() {
local body_file="$1"
local issue_number
issue_number="$(canonical_issue)"
if [[ -z "$issue_number" ]]; then
gh issue create --title "$TRACKING_ISSUE_TITLE" --body-file "$body_file"
fi
# Re-list after creation so racing reporters converge on the oldest issue.
issue_number="$(canonical_issue)"
gh issue edit "$issue_number" --body-file "$body_file"
gh issue comment "$issue_number" --body "Upstream sync diagnostics refreshed by [run ${GITHUB_RUN_ID}](https://github.com/${GITHUB_REPOSITORY}/actions/runs/${GITHUB_RUN_ID})."
}
close_sync_pr() {
local pr
gh pr list --limit 100 --state open --head "$SYNC_BRANCH" --json number --jq '.[].number' >/tmp/upstream-sync-pr-numbers
while read -r pr; do
[[ -z "$pr" ]] || gh pr close "$pr" --comment "$1"
done </tmp/upstream-sync-pr-numbers
}
verify_contract() {
rm -rf /tmp/upstream-verifier
mkdir -p /tmp/upstream-verifier
npm install --prefix /tmp/upstream-verifier --ignore-scripts --no-save --package-lock=false --no-audit --no-fund [email protected]
cp scripts/verify-resource-cleanup-contract.mjs /tmp/upstream-verifier/verify.mjs
node /tmp/upstream-verifier/verify.mjs .
}
report_error() {
local status="$1" line="$2" command="$3"
trap - ERR
{
echo 'Upstream was not consumed because sync automation failed.'
echo
echo "- Tracked upstream: \`${tracked}\`"
echo "- Observed upstream: \`${upstream_sha}\`"
echo "- Workflow run: https://github.com/${GITHUB_REPOSITORY}/actions/runs/${GITHUB_RUN_ID}"
echo "- Exit status: \`${status}\` at line \`${line}\`"
echo
echo '### Failed command (truncated)'
echo '```text'
printf '%s' "${command:0:2000}"
echo
echo '```'
echo
echo 'Re-run the workflow after correcting the failure. No production tag was moved.'
} >/tmp/upstream-operational-failure.md
upsert_issue /tmp/upstream-operational-failure.md || true
exit "$status"
}
trap 'report_error "$?" "$LINENO" "$BASH_COMMAND"' ERR
[[ "$GITHUB_REF" == refs/heads/main ]]
git remote add upstream "https://github.com/${UPSTREAM_REPOSITORY}.git"
git fetch --no-tags upstream main
tracked="$(tr -d '[:space:]' < UPSTREAM_COMMIT)"
upstream_sha="$(git rev-parse upstream/main)"
echo "upstream_sha=${upstream_sha}" >> "$GITHUB_OUTPUT"
echo "candidate_sha=" >> "$GITHUB_OUTPUT"
echo "pr_number=" >> "$GITHUB_OUTPUT"
issue_number="$(canonical_issue)"
if [[ "$tracked" == "$upstream_sha" ]]; then
verify_contract
close_sync_pr "Superseded: fork main already records upstream \`${upstream_sha}\` and the cleanup contract passes."
if [[ -n "$issue_number" ]]; then
gh issue comment "$issue_number" --body "Upstream is synchronized at \`${upstream_sha}\`; the lifecycle patch and generated distribution are current. Closing this tracking issue."
gh issue close "$issue_number"
fi
exit 0
fi
if ! git cat-file -e "${tracked}^{commit}" 2>/dev/null \
|| ! git merge-base --is-ancestor "$tracked" "$GITHUB_SHA" \
|| ! git merge-base --is-ancestor "$tracked" "$upstream_sha"; then
{
echo "Upstream advanced from recorded commit \`${tracked}\` to \`${upstream_sha}\`, but the recorded commit is not a common ancestor of fork main and upstream main."
echo
echo 'Automatic documentation merging is disabled because a merge could otherwise import executable changes outside the path allowlist.'
echo
echo '### Required repair'
echo "1. Compare fork main, \`${tracked}\`, and \`${upstream_sha}\` and identify the divergent commits."
echo '2. Manually merge or rebase upstream into a review branch and preserve the RC001-RC014 lifecycle invariants.'
echo '3. Run the lifecycle verifier, lint, typecheck, tests, build, and generated-distribution diff check.'
echo '4. Update `UPSTREAM_COMMIT` only in the reviewed merge that contains the target upstream commit.'
echo
echo "Compare: https://github.com/${UPSTREAM_REPOSITORY}/compare/${tracked}...${upstream_sha}"
echo "Workflow run: https://github.com/${GITHUB_REPOSITORY}/actions/runs/${GITHUB_RUN_ID}"
} >/tmp/upstream-ancestry-review.md
close_sync_pr "Closed because upstream ancestry diverged; automatic merging is unsafe. See the upstream-sync tracking issue."
upsert_issue /tmp/upstream-ancestry-review.md
exit 0
fi
git diff --name-only --no-renames "$tracked" "$upstream_sha" >/tmp/upstream-changed-files
mapfile -t changed </tmp/upstream-changed-files
risky=()
safe=()
for file in "${changed[@]}"; do
case "$file" in
*.md|LICENSE|LICENSE.md|LICENSE.txt|CODE_OF_CONDUCT.md|media/*.png|media/*.jpg|media/*.jpeg|media/*.gif|media/*.svg)
safe+=("$file") ;;
*) risky+=("$file") ;;
esac
done
compare="https://github.com/${UPSTREAM_REPOSITORY}/compare/${tracked}...${upstream_sha}"
if (( ${#risky[@]} > 0 )); then
close_sync_pr "Closed because the latest upstream update contains executable or configuration changes requiring manual review. See the upstream-sync tracking issue."
git checkout --detach "$GITHUB_SHA"
set +e
git merge --no-commit --no-ff "$upstream_sha" >/tmp/upstream-merge.log 2>&1
merge_status=$?
conflicts="$(git diff --name-only --diff-filter=U || true)"
git merge --abort >/dev/null 2>&1 || true
set -e
{
echo "Upstream advanced from \`${tracked}\` to \`${upstream_sha}\`."
echo
echo "Compare: ${compare}"
echo
echo "Executable/configuration changes are never consumed automatically, even when upstream tests pass."
echo
echo "### Review-required files"
printf -- '- `%s`\n' "${risky[@]}"
echo
echo "### Documentation-only files"
if (( ${#safe[@]} > 0 )); then printf -- '- `%s`\n' "${safe[@]}"; else echo '- None'; fi
echo
echo "### Merge probe"
echo "Status: ${merge_status}"
if [[ -n "$conflicts" ]]; then printf '%s\n' "$conflicts" | sed 's/^/- `/' | sed 's/$/`/'; else echo '- No textual conflicts detected'; fi
echo
echo "### Required lifecycle invariants"
echo '- RC001-RC003: `resourceSafe` defaults false before activation and becomes true only after owning-container proof.'
echo '- RC004-RC009: current-attempt nonce crosses a unique runner-temp mount, never the project workspace.'
echo '- RC010-RC011: Windows return writes exact proof only on return exit 0; `BUILD_EXIT_CODE` remains authoritative.'
echo '- RC012-RC013: generated `dist` contains the source contract and must be rebuilt.'
echo
echo "### Reproduction"
echo '```bash'
echo "git fetch https://github.com/${UPSTREAM_REPOSITORY}.git main"
echo "git checkout -b review-upstream-${upstream_sha:0:12} origin/main"
echo "git merge ${upstream_sha}"
echo 'corepack enable && corepack install'
echo 'yarn install --immutable'
echo 'node scripts/verify-resource-cleanup-contract.mjs .'
echo 'yarn lint && yarn typecheck && yarn test:ci && yarn build'
echo 'git diff --exit-code -- dist'
echo '```'
} >/tmp/upstream-issue.md
upsert_issue /tmp/upstream-issue.md
exit 0
fi
git config user.name github-actions[bot]
git config user.email 41898282+github-actions[bot]@users.noreply.github.com
git checkout -B "$SYNC_BRANCH" "$GITHUB_SHA"
git merge --no-edit "$upstream_sha"
printf '%s\n' "$upstream_sha" > UPSTREAM_COMMIT
git add UPSTREAM_COMMIT
git commit -m "chore: record upstream ${upstream_sha:0:12}"
gh auth setup-git
remote_sync_sha="$(git ls-remote origin "refs/heads/${SYNC_BRANCH}" | cut -f1)"
git push --force-with-lease="refs/heads/${SYNC_BRANCH}:${remote_sync_sha}" origin "$SYNC_BRANCH"
candidate_sha="$(git rev-parse HEAD)"
pr_number="$(gh pr list --state open --head "$SYNC_BRANCH" --json number --jq '.[0].number // empty')"
if [[ -z "$pr_number" ]]; then
gh pr create --base main --head "$SYNC_BRANCH" --title "chore: sync documentation from game-ci/unity-builder" --body "Automated documentation-only sync from [${tracked}...${upstream_sha}](${compare}). Executable and configuration changes are never auto-consumed."
pr_number="$(gh pr list --state open --head "$SYNC_BRANCH" --json number --jq '.[0].number')"
else
gh pr edit "$pr_number" --title "chore: sync documentation from game-ci/unity-builder" --body "Automated documentation-only sync from [${tracked}...${upstream_sha}](${compare}). Executable and configuration changes are never auto-consumed."
fi
echo "candidate_sha=${candidate_sha}" >> "$GITHUB_OUTPUT"
echo "pr_number=${pr_number}" >> "$GITHUB_OUTPUT"
validate:
needs: discover
if: needs.discover.outputs.candidate-sha != ''
runs-on: ubuntu-latest
permissions:
contents: read
steps:
- uses: actions/checkout@v4
with:
ref: main
path: _policy
persist-credentials: false
- uses: actions/checkout@v4
with:
ref: ${{ needs.discover.outputs.candidate-sha }}
path: _candidate
persist-credentials: false
- name: Install trusted verifier parser
run: |
rm -rf /tmp/upstream-verifier
mkdir -p /tmp/upstream-verifier
npm install --prefix /tmp/upstream-verifier --ignore-scripts --no-save --package-lock=false --no-audit --no-fund [email protected]
cp _policy/scripts/verify-resource-cleanup-contract.mjs /tmp/upstream-verifier/verify.mjs
- name: Verify lifecycle contract with trusted policy
run: node /tmp/upstream-verifier/verify.mjs _candidate
- name: Install package manager and dependencies
working-directory: _candidate
env:
YARN_ENABLE_HARDENED_MODE: 'false'
run: |
corepack enable
corepack install
yarn install --immutable
- name: Validate candidate
working-directory: _candidate
run: |
yarn lint
yarn typecheck
yarn test:ci
yarn build
git diff --exit-code -- dist
finalize:
needs: [discover, validate]
if: always() && needs.discover.outputs.candidate-sha != ''
runs-on: ubuntu-latest
permissions:
contents: write
pull-requests: write
issues: write
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
persist-credentials: false
- name: Revalidate immutable metadata and merge
env:
GH_TOKEN: ${{ github.token }}
CANDIDATE_SHA: ${{ needs.discover.outputs.candidate-sha }}
PR_NUMBER: ${{ needs.discover.outputs.pr-number }}
TARGET_UPSTREAM_SHA: ${{ needs.discover.outputs.upstream-sha }}
VALIDATION_RESULT: ${{ needs.validate.result }}
run: |
set -Eeuo pipefail
canonical_issue() {
local numbers duplicate
gh api --paginate "repos/${GITHUB_REPOSITORY}/issues?state=open&per_page=100" \
--jq '.[] | select((has("pull_request") | not) and .title == env.TRACKING_ISSUE_TITLE) | .number' \
| sort -n >/tmp/upstream-issue-numbers
mapfile -t numbers </tmp/upstream-issue-numbers
for duplicate in "${numbers[@]:1}"; do
gh issue close "$duplicate" --comment "Closing duplicate; continuing in #${numbers[0]}." >/dev/null
done
printf '%s' "${numbers[0]:-}"
}
upsert_issue() {
local body_file="$1"
local issue_number
issue_number="$(canonical_issue)"
if [[ -z "$issue_number" ]]; then
gh issue create --title "$TRACKING_ISSUE_TITLE" --body-file "$body_file" >/dev/null
fi
issue_number="$(canonical_issue)"
gh issue edit "$issue_number" --body-file "$body_file" >/dev/null
}
report_error() {
local status="$1" line="$2" command="$3"
local observed_head observed_pr observed_state
trap - ERR
if [[ "$merge_completed" != true ]]; then
observed_pr="$(gh pr view "$PR_NUMBER" --json state,headRefOid 2>/dev/null || true)"
observed_state="$(jq -r '.state // empty' <<<"$observed_pr" 2>/dev/null || true)"
observed_head="$(jq -r '.headRefOid // empty' <<<"$observed_pr" 2>/dev/null || true)"
if [[ "$observed_state" == MERGED && "$observed_head" == "$CANDIDATE_SHA" ]]; then
merge_completed=true
elif [[ "$observed_state" == MERGED ]]; then
merge_head_mismatch=true
elif [[ "$observed_state" != OPEN && "$observed_state" != CLOSED ]]; then
merge_state_unknown=true
fi
fi
{
if [[ "$merge_completed" == true ]]; then
echo "Documentation sync PR #${PR_NUMBER} merged, but post-merge cleanup failed."
elif [[ "$merge_head_mismatch" == true ]]; then
echo "Documentation sync PR #${PR_NUMBER} merged at unexpected head \`${observed_head}\`; the reviewed candidate's merge state is unsafe."
elif [[ "$merge_state_unknown" == true ]]; then
echo "Documentation sync PR #${PR_NUMBER} may have merged; its state could not be confirmed after the failure."
else
echo "Documentation sync PR #${PR_NUMBER} was not merged."
fi
echo
echo "- Validation: \`${VALIDATION_RESULT}\`"
echo "- Candidate: \`${CANDIDATE_SHA}\`"
echo "- Target upstream: \`${TARGET_UPSTREAM_SHA}\`"
echo "- Workflow run: https://github.com/${GITHUB_REPOSITORY}/actions/runs/${GITHUB_RUN_ID}"
echo "- Exit status: \`${status}\` at line \`${line}\`"
echo
echo '### Failed command (truncated)'
echo '```text'
printf '%s' "${command:0:2000}"
echo
echo '```'
echo
if [[ "$merge_completed" == true ]]; then
echo 'The reviewed upstream commit is already on main. Inspect and repair the reported cleanup failure; do not re-merge the PR. No production tag was moved.'
elif [[ "$merge_head_mismatch" == true ]]; then
echo 'Inspect the unexpected merged commit and main branch before taking action. Do not re-run sync until the divergence is understood. No production tag was moved.'
elif [[ "$merge_state_unknown" == true ]]; then
echo 'Inspect the PR and main branch before taking action. Re-run sync only after confirming the candidate was not merged. No production tag was moved.'
else
echo 'Inspect the validation job and re-run sync. No production tag was moved.'
fi
} >/tmp/upstream-finalize-failure.md
upsert_issue /tmp/upstream-finalize-failure.md || true
exit "$status"
}
merge_completed=false
merge_head_mismatch=false
merge_state_unknown=false
trap 'report_error "$?" "$LINENO" "$BASH_COMMAND"' ERR
current_upstream="$(git ls-remote "https://github.com/${UPSTREAM_REPOSITORY}.git" refs/heads/main | cut -f1)"
pr_metadata="$(gh pr view "$PR_NUMBER" --json headRefOid,headRefName,baseRefName,isCrossRepository)"
actual_head="$(jq -r .headRefOid <<<"$pr_metadata")"
actual_branch="$(jq -r .headRefName <<<"$pr_metadata")"
actual_base="$(jq -r .baseRefName <<<"$pr_metadata")"
cross_repository="$(jq -r .isCrossRepository <<<"$pr_metadata")"
if [[ "$VALIDATION_RESULT" != success || "$actual_head" != "$CANDIDATE_SHA" || "$actual_branch" != "$SYNC_BRANCH" || "$actual_base" != main || "$cross_repository" != false || "$current_upstream" != "$TARGET_UPSTREAM_SHA" ]]; then
echo "Candidate metadata changed: head=${actual_head}, branch=${actual_branch}, base=${actual_base}, cross-repository=${cross_repository}, upstream=${current_upstream}" >&2
false
fi
git remote add upstream "https://github.com/${UPSTREAM_REPOSITORY}.git"
git fetch --no-tags upstream main
git fetch --no-tags origin \
"+refs/heads/main:refs/remotes/origin/main" \
"+refs/heads/${SYNC_BRANCH}:refs/remotes/origin/${SYNC_BRANCH}"
tracked="$(tr -d '[:space:]' < UPSTREAM_COMMIT)"
[[ "$(git rev-parse origin/main)" == "$GITHUB_SHA" ]]
[[ "$(git rev-parse "refs/remotes/origin/${SYNC_BRANCH}")" == "$CANDIDATE_SHA" ]]
[[ "$(git show "${CANDIDATE_SHA}:UPSTREAM_COMMIT" | tr -d '[:space:]')" == "$TARGET_UPSTREAM_SHA" ]]
git merge-base --is-ancestor "$tracked" origin/main
git merge-base --is-ancestor "$tracked" "$TARGET_UPSTREAM_SHA"
git merge-base --is-ancestor "$TARGET_UPSTREAM_SHA" "$CANDIDATE_SHA"
git diff --name-only --no-renames origin/main "$CANDIDATE_SHA" >/tmp/upstream-changed-files
mapfile -t changed </tmp/upstream-changed-files
for file in "${changed[@]}"; do
case "$file" in
UPSTREAM_COMMIT|*.md|LICENSE|LICENSE.md|LICENSE.txt|CODE_OF_CONDUCT.md|media/*.png|media/*.jpg|media/*.jpeg|media/*.gif|media/*.svg) ;;
*) echo "Refusing changed executable/configuration path: $file"; false ;;
esac
done
pull_request_id="$(gh pr view "$PR_NUMBER" --json id --jq .id)"
gh api graphql \
-f query='mutation($id:ID!,$head:GitObjectID!){mergePullRequest(input:{pullRequestId:$id,expectedHeadOid:$head,mergeMethod:MERGE}){pullRequest{state headRefOid}}}' \
-f id="$pull_request_id" \
-f head="$CANDIDATE_SHA" \
--jq '.data.mergePullRequest.pullRequest | select(.state == "MERGED" and .headRefOid == env.CANDIDATE_SHA) | .state' \
| grep -qx MERGED
merge_completed=true
delete_stdout="$(mktemp)"
delete_stderr="$(mktemp)"
set +e
gh api --method DELETE "repos/${GITHUB_REPOSITORY}/git/refs/heads/${SYNC_BRANCH}" --include >"$delete_stdout" 2>"$delete_stderr"
delete_status=$?
set -e
if (( delete_status != 0 )); then
http_status="$(sed -n '1s/^HTTP\/[^ ]* \([0-9][0-9][0-9]\).*/\1/p' "$delete_stdout")"
response_body="$(sed '1,/^[[:space:]]*$/d' "$delete_stdout")"
if ! response_message="$(jq -er 'if type == "object" and (.message | type) == "string" then .message else empty end' <<<"$response_body" 2>/dev/null)"; then
response_message=''
fi
if [[ "$http_status" == 404 && "$response_message" == 'Not Found' ]] \
|| [[ "$http_status" == 422 && "$response_message" == 'Reference does not exist' ]]; then
echo 'Sync branch was already absent after the successful merge.'
else
cat "$delete_stdout" "$delete_stderr" >&2
false
fi
fi
rm -f "$delete_stdout" "$delete_stderr"
report-workflow-failure:
needs: [discover, validate, finalize]
if: >-
always() &&
(needs.discover.result == 'failure' ||
needs.validate.result == 'failure' ||
needs.finalize.result == 'failure')
runs-on: ubuntu-latest
permissions:
issues: write
steps:
- name: Ensure the failed sync has a singleton tracking issue
env:
GH_TOKEN: ${{ github.token }}
DISCOVER_RESULT: ${{ needs.discover.result }}
VALIDATE_RESULT: ${{ needs.validate.result }}
FINALIZE_RESULT: ${{ needs.finalize.result }}
run: |
set -euo pipefail
canonical_issue() {
local numbers duplicate
gh api --paginate "repos/${GITHUB_REPOSITORY}/issues?state=open&per_page=100" \
--jq '.[] | select((has("pull_request") | not) and .title == env.TRACKING_ISSUE_TITLE) | .number' \
| sort -n >/tmp/upstream-issue-numbers
mapfile -t numbers </tmp/upstream-issue-numbers
for duplicate in "${numbers[@]:1}"; do
gh issue close "$duplicate" --repo "$GITHUB_REPOSITORY" --comment "Closing duplicate; continuing in #${numbers[0]}." >/dev/null
done
printf '%s' "${numbers[0]:-}"
}
canonical="$(canonical_issue)"
issue_existed=false
[[ -z "$canonical" ]] || issue_existed=true
{
echo 'Upstream sync did not consume the observed update because automation failed.'
echo
echo "- Discover: \`${DISCOVER_RESULT}\`"
echo "- Validate: \`${VALIDATE_RESULT}\`"
echo "- Finalize: \`${FINALIZE_RESULT}\`"
echo "- Run: https://github.com/${GITHUB_REPOSITORY}/actions/runs/${GITHUB_RUN_ID}"
echo
echo 'Inspect the failed job and re-run `Upstream Sync`. No immutable production tag was moved.'
} >/tmp/upstream-workflow-failure.md
if [[ "$issue_existed" == false ]]; then
gh issue create --repo "$GITHUB_REPOSITORY" --title "$TRACKING_ISSUE_TITLE" --body-file /tmp/upstream-workflow-failure.md
fi
# Re-list after a possible create so every failure path converges on one issue.
canonical="$(canonical_issue)"
if [[ "$issue_existed" == false ]]; then
gh issue edit "$canonical" --repo "$GITHUB_REPOSITORY" --body-file /tmp/upstream-workflow-failure.md
else
gh issue comment "$canonical" --repo "$GITHUB_REPOSITORY" --body "A sync job failed in [run ${GITHUB_RUN_ID}](https://github.com/${GITHUB_REPOSITORY}/actions/runs/${GITHUB_RUN_ID}) (discover=\`${DISCOVER_RESULT}\`, validate=\`${VALIDATE_RESULT}\`, finalize=\`${FINALIZE_RESULT}\`). The detailed canonical diagnostics above were preserved."
fi