Skip to content

Create Release Branch and PR #100

Create Release Branch and PR

Create Release Branch and PR #100

name: Create Release Branch and PR
on:
workflow_dispatch:
jobs:
create-release-branch:
runs-on: ubuntu-latest
steps:
- name: Check out the code
uses: actions/checkout@v6
with:
ref: dev
- name: Get release date and branch name
run: |
RELEASE_DATE=$(date +'%Y-%m-%d')
echo "RELEASE_DATE=$RELEASE_DATE" >> $GITHUB_ENV
echo "BRANCH_NAME=release/$RELEASE_DATE" >> $GITHUB_ENV
- name: Create or update branch
run: |
git checkout -B $BRANCH_NAME
git push --force --set-upstream origin $BRANCH_NAME
- name: Create pull request using GitHub CLI
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
# Create PR if it doesn't already exist
if ! gh pr view "$BRANCH_NAME" --json number &>/dev/null; then
gh pr create \
--base main \
--head "$BRANCH_NAME" \
--title "release: $RELEASE_DATE" \
--body ":rocket: Automated release PR"
fi
# Always ensure labels are applied (handles 502 / retry scenarios)
gh pr edit "$BRANCH_NAME" --add-label "release,auto-pr"
- name: Comment build links on the PR
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
PUBLIC_URL_PREFIX: ${{ vars.EXPLORER_TEAM_S3_BUCKET_PUBLIC_URL }}
run: |
set -euo pipefail
# Bot-created PRs trigger no workflows, so no build runs and no badge
# comment gets posted. The release branch is cut from dev tip, whose
# SHA was already built by the push to dev — reuse that run's
# artifacts instead of rebuilding.
PR_NUMBER=$(gh pr view "$BRANCH_NAME" --json number --jq '.number')
EXISTING=$(gh api "repos/${GITHUB_REPOSITORY}/issues/${PR_NUMBER}/comments?per_page=100" \
--jq '[.[] | select(.user.login == "github-actions[bot]") | select(.body | contains("img.shields.io/badge/Build"))] | .[0] // empty')
# A "Build Not Found" comment is upgraded in place on re-run once the
# build exists; only a success comment short-circuits.
if [ -n "$EXISTING" ] && jq -e '.body | contains("Build-Success")' <<< "$EXISTING" > /dev/null; then
echo "Build links comment already present, skipping."
exit 0
fi
COMMENT_ID=$([ -n "$EXISTING" ] && jq -r '.id' <<< "$EXISTING" || echo "")
post_comment() {
if [ -n "$COMMENT_ID" ]; then
gh api -X PATCH "repos/${GITHUB_REPOSITORY}/issues/comments/${COMMENT_ID}" -F body=@comment.md > /dev/null
else
gh pr comment "$BRANCH_NAME" --body-file comment.md
fi
}
HEAD_SHA=$(git rev-parse HEAD)
SHORT_SHA="${HEAD_SHA:0:7}"
REPO_NAME="${GITHUB_REPOSITORY#*/}"
# Scoped to the build workflow file — a repo-wide head_sha listing
# gets crowded out by bot runs sharing the SHA and the build falls
# past the first page.
RUN_JSON=$(gh api "repos/${GITHUB_REPOSITORY}/actions/workflows/build-unitycloud.yml/runs?head_sha=${HEAD_SHA}&status=success&per_page=20" \
--jq '[.workflow_runs[] | select(.event == "pull_request" or .event == "push")] | .[0]')
if [ -z "$RUN_JSON" ] || [ "$RUN_JSON" = "null" ]; then
{
echo '![badge] <img src="https://ui.decentraland.org/decentraland_256x256.png" width="30">'
echo ""
echo "No completed Unity Cloud Build found for \`${SHORT_SHA}\` — check the [build runs](${GITHUB_SERVER_URL}/${GITHUB_REPOSITORY}/actions/workflows/build-unitycloud.yml) and re-run this workflow once one finishes."
echo ""
echo "[badge]: https://img.shields.io/badge/Build-Not%20Found-yellow?logo=github&style=for-the-badge"
} > comment.md
post_comment
exit 0
fi
RUN_ID=$(jq -r '.id' <<< "$RUN_JSON")
RUN_NUMBER=$(jq -r '.run_number' <<< "$RUN_JSON")
SUITE_ID=$(jq -r '.check_suite_id' <<< "$RUN_JSON")
RUN_EVENT=$(jq -r '.event' <<< "$RUN_JSON")
RUN_BRANCH=$(jq -r '.head_branch' <<< "$RUN_JSON")
BUILD_DATE=$(jq -r '.updated_at' <<< "$RUN_JSON")
# Same event -> S3 prefix mapping as build-unitycloud.yml
case "$RUN_EVENT" in
pull_request) BUILD_PREFIX="pr" ;;
push) BUILD_PREFIX="pu" ;;
merge_group) BUILD_PREFIX="mg" ;;
workflow_dispatch) BUILD_PREFIX="wd" ;;
workflow_call) BUILD_PREFIX="wc" ;;
schedule) BUILD_PREFIX="sc" ;;
*) BUILD_PREFIX="gn" ;;
esac
S3_PATH="@dcl/${REPO_NAME}/branch/${RUN_BRANCH}/${BUILD_PREFIX}-${RUN_NUMBER}-${SHORT_SHA}"
WINDOWS_ARTIFACT_ID=$(gh api "repos/${GITHUB_REPOSITORY}/actions/runs/${RUN_ID}/artifacts" \
--jq '.artifacts[] | select(.name == "Decentraland_windows64") | .id' || true)
MAC_ARTIFACT_ID=$(gh api "repos/${GITHUB_REPOSITORY}/actions/runs/${RUN_ID}/artifacts" \
--jq '.artifacts[] | select(.name == "Decentraland_macos") | .id' || true)
{
echo '![badge] <img src="https://ui.decentraland.org/decentraland_256x256.png" width="30">'
echo ""
echo "Windows and Mac build successful in Unity Cloud! Links reused from the existing \`${RUN_BRANCH}\` build of this commit."
echo ""
echo "| Name | Link |"
echo "| -------- | ----------------------- |"
echo "| Commit | ${HEAD_SHA} |"
echo "| Logs | ${GITHUB_SERVER_URL}/${GITHUB_REPOSITORY}/actions/runs/${RUN_ID} |"
echo "| Download Windows | ${GITHUB_SERVER_URL}/${GITHUB_REPOSITORY}/suites/${SUITE_ID}/artifacts/${WINDOWS_ARTIFACT_ID} |"
echo "| Download Windows S3 | ${PUBLIC_URL_PREFIX}/${S3_PATH}/Decentraland_windows64.zip |"
echo "| Download Mac | ${GITHUB_SERVER_URL}/${GITHUB_REPOSITORY}/suites/${SUITE_ID}/artifacts/${MAC_ARTIFACT_ID} |"
echo "| Download Mac S3 | ${PUBLIC_URL_PREFIX}/${S3_PATH}/Decentraland_macos.zip |"
echo "| Built on | ${BUILD_DATE} |"
echo ""
echo "[badge]: https://img.shields.io/badge/Build-Success!-3fb950?logo=github&style=for-the-badge"
} > comment.md
post_comment
# The PR above was opened with GITHUB_TOKEN, whose events start no workflow
# runs — the same suppression the build-links step exists to work around.
# So the InWorld gate has to be started explicitly, by a token that can.
#
# Dispatching against the release branch is what puts the gate on the PR:
# a workflow_dispatch run attaches its checks to the dispatched ref's tip,
# and that tip is the PR head. Without this the required check on main
# never reports and the release cannot merge.
- name: Start the InWorld gate on the release branch
env:
GH_TOKEN: ${{ secrets.ORG_ACCESS_TOKEN }}
run: |
set -euo pipefail
# On the dispatch path the gate resolves this commit's build with
# wait-minutes 0, so it fails within seconds unless both zips are
# already up. Ask it the same question — did both build legs conclude
# success — rather than whether a run went green: build-unitycloud.yml
# also concludes green having built nothing, when the commit touches
# no Explorer/** file and the matrix never expands, and green having
# built one platform, when a windows-only/macos-only label narrows it.
# Either would dispatch a gate that immediately reds a brand-new
# release PR with nothing wrong in it.
#
# Runs come back newest first, and only the newest few are worth
# asking about — the same candidate cap resolve-explorer-build uses.
HEAD_SHA=$(git rev-parse HEAD)
# The helper resolve-explorer-build defines, duplicated because a
# composite action cannot export a shell function to its caller. Same
# justification, more sharply: this step decides on one answer and
# then the workflow ends, so a single 5xx must not settle a release
# cut. Three attempts five seconds apart cover a transport failure —
# a 5xx, a connection reset — and only that; a spent rate limit
# refills on an hour boundary, which is why the flags below record
# whether the API answered rather than trusting the retry to make it.
# stderr is kept on the last attempt so a permanent failure (this
# workflow file renamed, say) says what it was instead of vanishing
# into "the API did not answer".
gh_api_retry() {
local out attempt
for attempt in 1 2 3; do
if [ "$attempt" -lt 3 ]; then
if out=$(gh api "$@" 2>/dev/null); then printf '%s' "$out"; return 0; fi
sleep 5
else
if out=$(gh api "$@"); then printf '%s' "$out"; return 0; fi
fi
done
return 1
}
# Absorbed rather than left to set -e, here and on the jobs call
# below: an API blip must not red a cut whose branch, PR, labels and
# build-links comment have all landed. But it must not be reported as
# "no build" either — that is the conflation LISTED/LISTED_NOW exist
# to stop in resolve-explorer-build, and it costs more here, because
# this message is the operator's only instruction and the workflow
# ends after it. LISTED is "the run listing answered at all";
# UNEXAMINED is "a candidate's jobs call did not", which leaves that
# run neither green nor known to be anything else.
LISTED=0
UNEXAMINED=0
if RUN_IDS=$(gh_api_retry "repos/${GITHUB_REPOSITORY}/actions/workflows/build-unitycloud.yml/runs?head_sha=${HEAD_SHA}&per_page=50" \
--jq '[.workflow_runs[] | select(.event == "pull_request" or .event == "push") | .id]'); then
LISTED=1
else
RUN_IDS=''
fi
if [ -z "$RUN_IDS" ]; then RUN_IDS='[]'; fi
LIMIT=$(jq 'length' <<< "$RUN_IDS")
if [ "$LIMIT" -gt 5 ]; then LIMIT=5; fi
LEGS_GREEN=false
for (( i = 0; i < LIMIT; i++ )); do
RUN_ID=$(jq -r ".[$i]" <<< "$RUN_IDS")
if LEGS_GREEN=$(gh_api_retry "repos/${GITHUB_REPOSITORY}/actions/runs/${RUN_ID}/jobs?per_page=50" \
--jq '[.jobs[] | select(.conclusion == "success") | .name] | any(startswith("Build (macos")) and any(startswith("Build (windows64"))'); then
if [ "$LEGS_GREEN" = "true" ]; then break; fi
else
LEGS_GREEN=false
UNEXAMINED=1
fi
done
# Three ways to end up not dispatching, and only the last is a build
# problem — so they get three messages. All three are `::error::`:
# each one ends with the gate unreported and the release unmergeable,
# which is not a line in a green log, and the dispatch failure below
# already annotates for exactly that outcome.
#
# None of them says "re-run this workflow". Re-running re-enters the
# `Create or update branch` step above, which force-pushes
# ${BRANCH_NAME} from dev's current tip — quietly re-cutting the
# release from newer commits than the one that was reviewed. Starting
# `In-World Tests` by hand is the remedy in every case, and it is the
# same instruction the dispatch failure below gives.
if [ "$LISTED" = "0" ]; then
echo "::error::Could not list build-unitycloud.yml runs for ${HEAD_SHA:0:7} — the GitHub Actions API did not answer, so no build was ever looked at and the InWorld gate was not started. This says nothing about whether a build exists. The release branch and its PR are fine: start \`In-World Tests\` by hand from the Actions tab against ${BRANCH_NAME}. Do not re-run this workflow — it force-pushes ${BRANCH_NAME} and would re-cut the release from a newer dev tip."
exit 0
fi
if [ "$LEGS_GREEN" != "true" ] && [ "$UNEXAMINED" = "1" ]; then
echo "::error::The GitHub Actions API did not answer for at least one build run of ${HEAD_SHA:0:7}, so whether its legs are green is unknown and the InWorld gate was not started. This is not evidence that no build exists. The release branch and its PR are fine: start \`In-World Tests\` by hand from the Actions tab against ${BRANCH_NAME}. Do not re-run this workflow — it force-pushes ${BRANCH_NAME} and would re-cut the release from a newer dev tip."
exit 0
fi
if [ "$LEGS_GREEN" != "true" ]; then
echo "::error::No build of ${HEAD_SHA:0:7} has both Build (macos) and Build (windows64) green, so the InWorld gate was not started — it would fail on its first poll. Build the commit: label the release PR \`force-build\` if build-unitycloud.yml built nothing, or remove a \`windows-only\`/\`macos-only\` label that narrowed its targets. Then start \`In-World Tests\` by hand against ${BRANCH_NAME}. Do not re-run this workflow — it force-pushes ${BRANCH_NAME} and would re-cut the release from a newer dev tip."
exit 0
fi
# Loud but deliberately not fatal. `gh workflow run` needs
# `actions: write`, which ORG_ACCESS_TOKEN's other uses in this repo
# (org membership, PR reviews) do not require, so it may not have it —
# and a secret's scopes cannot be inspected from here. Failing the job
# would invite a re-run, and re-running this workflow force-pushes the
# release branch, quietly re-cutting the release from a newer dev tip.
# The branch, PR, labels and build-links comment have all landed by
# now; the annotation is what makes the missing gate visible, and the
# required check staying unreported is what keeps the release blocked.
if ! gh workflow run in-world-tests.yml --ref "$BRANCH_NAME"; then
echo "::error::Could not dispatch the InWorld gate against ${BRANCH_NAME}. The likeliest cause is that ORG_ACCESS_TOKEN lacks \`actions: write\` on this repository, which \`gh workflow run\` requires. The release branch and its PR are fine — start \`In-World Tests\` by hand from the Actions tab against ${BRANCH_NAME}, or its required check will sit unreported and the release will not be mergeable."
exit 0
fi
echo "::notice::InWorld gate dispatched against ${BRANCH_NAME}."