Create Release Branch and PR #100
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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}." |