termux-release-watch #272
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: termux-release-watch | |
| on: | |
| schedule: | |
| - cron: "17 */3 * * *" | |
| workflow_dispatch: | |
| inputs: | |
| upstream_tag: | |
| description: "Specific upstream rust tag to process, for example rust-v0.122.0-alpha.7" | |
| required: false | |
| type: string | |
| default: "" | |
| reviewer: | |
| description: "GitHub username to request as reviewer" | |
| required: false | |
| type: string | |
| default: "wallentx" | |
| bypass_prior_release_train: | |
| description: "Allow workflow_dispatch to create or update a release train even when another Termux release train PR is open" | |
| required: false | |
| type: boolean | |
| default: false | |
| permissions: | |
| actions: read | |
| attestations: read | |
| checks: read | |
| contents: read | |
| deployments: read | |
| issues: read | |
| discussions: read | |
| packages: read | |
| pages: read | |
| pull-requests: read | |
| repository-projects: read | |
| statuses: read | |
| concurrency: | |
| group: termux-release-watch | |
| cancel-in-progress: false | |
| jobs: | |
| look-for-newer-release: | |
| runs-on: ubuntu-slim | |
| outputs: | |
| selected: ${{ steps.upstream.outputs.selected }} | |
| upstream_tag: ${{ steps.upstream.outputs.upstream_tag }} | |
| upstream_name: ${{ steps.upstream.outputs.upstream_name }} | |
| upstream_html_url: ${{ steps.upstream.outputs.upstream_html_url }} | |
| upstream_prerelease: ${{ steps.upstream.outputs.upstream_prerelease }} | |
| upstream_target: ${{ steps.upstream.outputs.upstream_target }} | |
| upstream_id: ${{ steps.upstream.outputs.upstream_id }} | |
| release_kind: ${{ steps.upstream.outputs.release_kind }} | |
| release_train: ${{ steps.upstream.outputs.release_train }} | |
| release_branch: ${{ steps.upstream.outputs.release_branch }} | |
| work_branch: ${{ steps.upstream.outputs.work_branch }} | |
| termux_tag: ${{ steps.upstream.outputs.termux_tag }} | |
| body: ${{ steps.upstream.outputs.body }} | |
| permissions: | |
| contents: read | |
| env: | |
| GH_REPO: ${{ github.repository }} | |
| UPSTREAM_REPO: openai/codex | |
| steps: | |
| - name: Generate GitHub App token | |
| id: app-token | |
| uses: actions/create-github-app-token@v3 | |
| with: | |
| client-id: ${{ vars.APP_CLIENT_ID }} | |
| private-key: ${{ secrets.APP_PRIVATE_KEY }} | |
| - name: Checkout automation branch | |
| uses: actions/checkout@v6 | |
| with: | |
| fetch-depth: 0 | |
| token: ${{ steps.app-token.outputs.token }} | |
| - name: 🧰 Actions Toolbox | |
| # This is required for the GitHub CLI | |
| env: | |
| GH_TOKEN: ${{ steps.app-token.outputs.token }} | |
| uses: wallentx/gh-actions/composite/actions-toolbox@main | |
| - name: Select upstream release | |
| id: upstream | |
| shell: bash | |
| env: | |
| GH_TOKEN: ${{ steps.app-token.outputs.token }} | |
| REQUESTED_TAG: ${{ inputs.upstream_tag }} | |
| BYPASS_PRIOR_RELEASE_TRAIN: ${{ inputs.bypass_prior_release_train || false }} | |
| run: | | |
| set -euo pipefail | |
| candidate_release_branch() { | |
| local tag="$1" | |
| local version="${tag#rust-v}" | |
| version="${version%-termux}" | |
| local release_train="${version%%-*}" | |
| printf 'release/%s\n' "${release_train}" | |
| } | |
| normalize_rust_tag_version() { | |
| local tag="$1" | |
| local version="${tag#rust-v}" | |
| version="${version%-termux}" | |
| printf '%s\n' "${version}" | |
| } | |
| release_branch_current_tag() { | |
| local release_branch="$1" | |
| release_branch_current_metadata "${release_branch}" \ | |
| | jq -r '.upstream_tag // .termux_tag // empty' | |
| } | |
| release_branch_current_metadata() { | |
| local release_branch="$1" | |
| git show "origin/${release_branch}:.github/termux-release.json" 2>/dev/null || true | |
| } | |
| release_branch_current_termux_tag() { | |
| local release_branch="$1" | |
| release_branch_current_metadata "${release_branch}" \ | |
| | jq -r '.termux_tag // empty' | |
| } | |
| open_release_train_pr_for_branch() { | |
| local release_branch="$1" | |
| local open_prs | |
| open_prs="$( | |
| gh pr list \ | |
| --repo "${GITHUB_REPOSITORY}" \ | |
| --state open \ | |
| --limit 200 \ | |
| --json number,title,body,headRefName,baseRefName,url | |
| )" | |
| jq -c \ | |
| --arg release_branch "${release_branch}" \ | |
| ' | |
| [ | |
| .[] | |
| | select(.baseRefName == $release_branch) | |
| | select( | |
| ((.title // "") | startswith("Termux rust-v")) | |
| and ((.body // "") | contains("Release train branch: `" + $release_branch + "`")) | |
| ) | |
| | . + { | |
| upstreamTag: ( | |
| (.body // "") | |
| | try capture("- Upstream tag: `(?<tag>rust-v[^`]+)`").tag catch "" | |
| ) | |
| } | |
| ] | |
| | sort_by(.number) | |
| | reverse | |
| | .[0] // empty | |
| ' <<< "${open_prs}" | |
| } | |
| open_other_release_train_prs() { | |
| local release_branch="$1" | |
| local open_prs | |
| open_prs="$( | |
| gh pr list \ | |
| --repo "${GITHUB_REPOSITORY}" \ | |
| --state open \ | |
| --limit 200 \ | |
| --json number,title,body,baseRefName,url | |
| )" | |
| jq -r \ | |
| --arg release_branch "${release_branch}" \ | |
| ' | |
| .[] | |
| | select(.baseRefName != $release_branch) | |
| | select( | |
| ((.title // "") | startswith("Termux rust-v")) | |
| and ((.body // "") | contains("Release train branch: `release/")) | |
| ) | |
| | "- #\(.number) \(.title) (\(.url))" | |
| ' <<< "${open_prs}" | |
| } | |
| rust_tag_is_newer_than_tag() { | |
| local candidate_tag="$1" | |
| local current_tag="$2" | |
| local candidate_version | |
| local current_version | |
| local candidate_base | |
| local current_base | |
| local candidate_is_prerelease=false | |
| local current_is_prerelease=false | |
| local newest_version | |
| if [[ -z "${current_tag}" ]]; then | |
| return 0 | |
| fi | |
| candidate_version="$(normalize_rust_tag_version "${candidate_tag}")" | |
| current_version="$(normalize_rust_tag_version "${current_tag}")" | |
| candidate_base="${candidate_version%%-*}" | |
| current_base="${current_version%%-*}" | |
| if [[ "${candidate_version}" == *-* ]]; then | |
| candidate_is_prerelease=true | |
| fi | |
| if [[ "${current_version}" == *-* ]]; then | |
| current_is_prerelease=true | |
| fi | |
| if [[ "${candidate_base}" == "${current_base}" ]]; then | |
| if [[ "${current_is_prerelease}" == true && "${candidate_is_prerelease}" == false ]]; then | |
| return 0 | |
| fi | |
| if [[ "${current_is_prerelease}" == false && "${candidate_is_prerelease}" == true ]]; then | |
| return 1 | |
| fi | |
| fi | |
| newest_version="$( | |
| printf '%s\n%s\n' "${current_version}" "${candidate_version}" \ | |
| | sort -V \ | |
| | tail -n 1 | |
| )" | |
| [[ "${newest_version}" == "${candidate_version}" && "${candidate_version}" != "${current_version}" ]] | |
| } | |
| release_tag_is_newer_than_known_train() { | |
| local candidate_tag="$1" | |
| local release_branch="$2" | |
| local open_train_pr_json="${3:-}" | |
| local current_tag | |
| local open_pr_tag | |
| current_tag="$(release_branch_current_tag "${release_branch}")" | |
| if ! rust_tag_is_newer_than_tag "${candidate_tag}" "${current_tag}"; then | |
| echo "${candidate_tag} is not newer than ${current_tag} already recorded on ${release_branch}; nothing to do." | |
| return 1 | |
| fi | |
| if [[ -n "${open_train_pr_json}" ]]; then | |
| open_pr_tag="$(jq -r '.upstreamTag // empty' <<< "${open_train_pr_json}")" | |
| if ! rust_tag_is_newer_than_tag "${candidate_tag}" "${open_pr_tag}"; then | |
| echo "${candidate_tag} is not newer than ${open_pr_tag} already proposed in open PR $(jq -r '.url' <<< "${open_train_pr_json}"); nothing to do." | |
| return 1 | |
| fi | |
| fi | |
| return 0 | |
| } | |
| release_json_for_tag() { | |
| local tag="$1" | |
| gh release view "${tag}" \ | |
| --repo "${UPSTREAM_REPO}" \ | |
| --json tagName,name,body,url,isPrerelease,targetCommitish,databaseId | |
| } | |
| emit_selected_release() { | |
| local release_kind="$1" | |
| local release_json="$2" | |
| local release_train="${3:-}" | |
| local release_branch="${4:-}" | |
| local work_branch="${5:-}" | |
| local termux_tag="${6:-}" | |
| local upstream_tag | |
| local upstream_name | |
| local upstream_body | |
| local upstream_html_url | |
| local upstream_prerelease | |
| local upstream_target | |
| local upstream_id | |
| upstream_tag="$(jq -r '.tagName // empty' <<< "${release_json}")" | |
| upstream_name="$(jq -r '.name // .tagName' <<< "${release_json}")" | |
| upstream_body="$(jq -r '.body // ""' <<< "${release_json}")" | |
| upstream_html_url="$(jq -r '.url' <<< "${release_json}")" | |
| upstream_prerelease="$(jq -r '.isPrerelease' <<< "${release_json}")" | |
| upstream_target="$(jq -r '.targetCommitish // ""' <<< "${release_json}")" | |
| upstream_id="$(jq -r '.databaseId // ""' <<< "${release_json}")" | |
| { | |
| echo "selected=true" | |
| echo "release_kind=${release_kind}" | |
| echo "upstream_tag=${upstream_tag}" | |
| echo "upstream_name=${upstream_name}" | |
| echo "upstream_html_url=${upstream_html_url}" | |
| echo "upstream_prerelease=${upstream_prerelease}" | |
| echo "upstream_target=${upstream_target}" | |
| echo "upstream_id=${upstream_id}" | |
| echo "release_train=${release_train}" | |
| echo "release_branch=${release_branch}" | |
| echo "work_branch=${work_branch}" | |
| echo "termux_tag=${termux_tag}" | |
| } >> "$GITHUB_OUTPUT" | |
| { | |
| echo "body<<EOF" | |
| printf '%s\n' "${upstream_body}" | |
| echo "EOF" | |
| } >> "$GITHUB_OUTPUT" | |
| } | |
| maybe_select_rusty_v8_release() { | |
| local release_json="$1" | |
| local upstream_tag | |
| upstream_tag="$(jq -r '.tagName // empty' <<< "${release_json}")" | |
| if [[ "${upstream_tag}" != rusty-v8-v* ]]; then | |
| return 1 | |
| fi | |
| if gh release view "${upstream_tag}" --repo "${GITHUB_REPOSITORY}" >/dev/null 2>&1; then | |
| echo "${upstream_tag} already exists in ${GITHUB_REPOSITORY}; nothing to do." | |
| return 1 | |
| fi | |
| emit_selected_release "rusty-v8" "${release_json}" "" "" "" "${upstream_tag}" | |
| return 0 | |
| } | |
| maybe_select_codex_release() { | |
| local release_json="$1" | |
| local upstream_tag | |
| local version | |
| local release_train | |
| local release_branch | |
| local work_branch | |
| local termux_tag | |
| local open_train_pr_json | |
| local pending_other_release_train_prs | |
| upstream_tag="$(jq -r '.tagName // empty' <<< "${release_json}")" | |
| if [[ "${upstream_tag}" != rust-v* ]]; then | |
| return 1 | |
| fi | |
| version="${upstream_tag#rust-v}" | |
| version="${version%-termux}" | |
| release_train="${version%%-*}" | |
| release_branch="release/${release_train}" | |
| work_branch="upstream/rust-v${release_train}" | |
| termux_tag="${upstream_tag}-termux" | |
| open_train_pr_json="$(open_release_train_pr_for_branch "${release_branch}")" | |
| if [[ -n "${open_train_pr_json}" ]]; then | |
| work_branch="$(jq -r '.headRefName' <<< "${open_train_pr_json}")" | |
| fi | |
| if [[ -z "${REQUESTED_TAG}" && "${version}" == *-* ]]; then | |
| stable_termux_tag="rust-v${release_train}-termux" | |
| if gh release view "${stable_termux_tag}" --repo "${GITHUB_REPOSITORY}" >/dev/null 2>&1; then | |
| echo "${upstream_tag} is a prerelease for ${release_train}, but ${stable_termux_tag} already exists; nothing to do." | |
| return 1 | |
| fi | |
| fi | |
| if ! release_tag_is_newer_than_known_train "${upstream_tag}" "${release_branch}" "${open_train_pr_json}"; then | |
| return 1 | |
| fi | |
| if gh release view "${termux_tag}" --repo "${GITHUB_REPOSITORY}" >/dev/null 2>&1; then | |
| echo "${termux_tag} already exists; nothing to do." | |
| return 1 | |
| fi | |
| if [[ "${BYPASS_PRIOR_RELEASE_TRAIN}" != "true" ]]; then | |
| pending_other_release_train_prs="$(open_other_release_train_prs "${release_branch}")" | |
| if [[ -n "${pending_other_release_train_prs}" ]]; then | |
| echo "${upstream_tag} is newer, but another release train PR is already open; waiting for it to merge and deploy." | |
| printf '%s\n' "${pending_other_release_train_prs}" | |
| return 1 | |
| fi | |
| elif [[ -n "${REQUESTED_TAG}" ]]; then | |
| echo "Bypassing prior release train gate for manual dispatch of ${REQUESTED_TAG}." | |
| fi | |
| if [[ -z "${REQUESTED_TAG}" ]]; then | |
| current_termux_tag="$(release_branch_current_termux_tag "${release_branch}")" | |
| if [[ -n "${current_termux_tag}" && "${current_termux_tag}" != "${termux_tag}" ]]; then | |
| if ! gh release view "${current_termux_tag}" --repo "${GITHUB_REPOSITORY}" >/dev/null 2>&1; then | |
| echo "${upstream_tag} is newer, but ${release_branch} is still waiting for ${current_termux_tag} to be deployed." | |
| return 1 | |
| fi | |
| fi | |
| fi | |
| emit_selected_release "codex" \ | |
| "${release_json}" \ | |
| "${release_train}" \ | |
| "${release_branch}" \ | |
| "${work_branch}" \ | |
| "${termux_tag}" | |
| return 0 | |
| } | |
| maybe_select_release() { | |
| local release_json="$1" | |
| local upstream_tag | |
| upstream_tag="$(jq -r '.tagName // empty' <<< "${release_json}")" | |
| case "${upstream_tag}" in | |
| rust-v*) | |
| maybe_select_codex_release "${release_json}" | |
| ;; | |
| rusty-v8-v*) | |
| maybe_select_rusty_v8_release "${release_json}" | |
| ;; | |
| *) | |
| echo "Skipping unsupported upstream release tag: ${upstream_tag}" | |
| return 1 | |
| ;; | |
| esac | |
| } | |
| if [[ -n "${REQUESTED_TAG}" ]]; then | |
| release_json="$(release_json_for_tag "${REQUESTED_TAG}")" | |
| if maybe_select_release "${release_json}"; then | |
| exit 0 | |
| fi | |
| else | |
| mapfile -t upstream_tags < <( | |
| gh release list \ | |
| --repo "${UPSTREAM_REPO}" \ | |
| --exclude-drafts \ | |
| --limit 30 \ | |
| --json tagName \ | |
| --jq '.[].tagName' | |
| ) | |
| for upstream_tag in "${upstream_tags[@]}"; do | |
| case "${upstream_tag}" in | |
| rust-v* | rusty-v8-v*) | |
| release_json="$(release_json_for_tag "${upstream_tag}")" | |
| if maybe_select_release "${release_json}"; then | |
| exit 0 | |
| fi | |
| ;; | |
| *) | |
| echo "Skipping unsupported upstream release tag: ${upstream_tag}" | |
| ;; | |
| esac | |
| done | |
| fi | |
| echo "No upstream Codex or rusty_v8 release needs a Termux mirror." | |
| echo "selected=false" >> "$GITHUB_OUTPUT" | |
| create-release-pr: | |
| needs: | |
| - look-for-newer-release | |
| - cleanup-completed-release-branches | |
| if: needs.look-for-newer-release.outputs.selected == 'true' && needs.look-for-newer-release.outputs.release_kind == 'codex' && needs.cleanup-completed-release-branches.result == 'success' | |
| runs-on: ubuntu-slim | |
| permissions: | |
| actions: write | |
| contents: write | |
| issues: write | |
| pull-requests: write | |
| env: | |
| GH_REPO: ${{ github.repository }} | |
| REVIEWER: ${{ inputs.reviewer || 'wallentx' }} | |
| UPSTREAM_REPO: openai/codex | |
| UPSTREAM_TAG: ${{ needs.look-for-newer-release.outputs.upstream_tag }} | |
| PATCH_BRANCH: wallentx/termux-target | |
| steps: | |
| - name: Generate GitHub App token | |
| id: app-token | |
| uses: actions/create-github-app-token@v3 | |
| with: | |
| client-id: ${{ vars.APP_CLIENT_ID }} | |
| private-key: ${{ secrets.APP_PRIVATE_KEY }} | |
| - name: Checkout automation branch | |
| uses: actions/checkout@v6 | |
| with: | |
| fetch-depth: 0 | |
| token: ${{ steps.app-token.outputs.token }} | |
| - name: 🧰 Actions Toolbox | |
| # This is required for the GitHub CLI | |
| env: | |
| GH_TOKEN: ${{ steps.app-token.outputs.token }} | |
| uses: wallentx/gh-actions/composite/actions-toolbox@main | |
| - name: Configure git | |
| shell: bash | |
| env: | |
| GH_TOKEN: ${{ steps.app-token.outputs.token }} | |
| run: | | |
| set -euo pipefail | |
| git config user.name "github-actions[bot]" | |
| git config user.email "41898282+github-actions[bot]@users.noreply.github.com" | |
| git remote add upstream "https://github.com/${UPSTREAM_REPO}.git" 2>/dev/null || true | |
| git fetch --prune origin "${PATCH_BRANCH}" | |
| git fetch --prune --no-tags upstream "+refs/tags/${UPSTREAM_TAG}:refs/tags/${UPSTREAM_TAG}" | |
| - name: Create or update release train PR | |
| id: pr | |
| shell: bash | |
| env: | |
| GH_TOKEN: ${{ steps.app-token.outputs.token }} | |
| UPSTREAM_TAG: ${{ needs.look-for-newer-release.outputs.upstream_tag }} | |
| UPSTREAM_NAME: ${{ needs.look-for-newer-release.outputs.upstream_name }} | |
| UPSTREAM_HTML_URL: ${{ needs.look-for-newer-release.outputs.upstream_html_url }} | |
| UPSTREAM_PRERELEASE: ${{ needs.look-for-newer-release.outputs.upstream_prerelease }} | |
| UPSTREAM_TARGET: ${{ needs.look-for-newer-release.outputs.upstream_target }} | |
| UPSTREAM_ID: ${{ needs.look-for-newer-release.outputs.upstream_id }} | |
| RELEASE_TRAIN: ${{ needs.look-for-newer-release.outputs.release_train }} | |
| RELEASE_BRANCH: ${{ needs.look-for-newer-release.outputs.release_branch }} | |
| WORK_BRANCH: ${{ needs.look-for-newer-release.outputs.work_branch }} | |
| TERMUX_TAG: ${{ needs.look-for-newer-release.outputs.termux_tag }} | |
| UPSTREAM_BODY: ${{ needs.look-for-newer-release.outputs.body }} | |
| run: | | |
| set -euo pipefail | |
| pr_title="Termux ${UPSTREAM_TAG}" | |
| integration_conflicted=false | |
| conflict_summary="" | |
| conflict_context="" | |
| fallback_ref="" | |
| workflow_seed_dir="${RUNNER_TEMP}/termux-release-workflows" | |
| script_seed_dir="${RUNNER_TEMP}/termux-release-scripts" | |
| mkdir -p "${workflow_seed_dir}" "${script_seed_dir}" | |
| cp .github/workflows/rust-release.yml "${workflow_seed_dir}/rust-release.yml" | |
| cp .github/workflows/shell-tool-mcp.yml "${workflow_seed_dir}/shell-tool-mcp.yml" | |
| cp .github/workflows/termux-release-checkpoint.yml "${workflow_seed_dir}/termux-release-checkpoint.yml" | |
| cp .github/workflows/termux-release-deploy.yml "${workflow_seed_dir}/termux-release-deploy.yml" | |
| cp .github/workflows/termux-release-promote.yml "${workflow_seed_dir}/termux-release-promote.yml" | |
| cp scripts/termux-create-checkpoint-pr.sh "${script_seed_dir}/termux-create-checkpoint-pr.sh" | |
| cp scripts/termux-download-release-artifact.sh "${script_seed_dir}/termux-download-release-artifact.sh" | |
| cp scripts/termux-find-release-pr.sh "${script_seed_dir}/termux-find-release-pr.sh" | |
| seed_release_branch_workflows() { | |
| # Release branches start from upstream tags, so keep the Termux-owned | |
| # CI/deployment workflows and their helper scripts authoritative from | |
| # this automation branch. | |
| mkdir -p .github/workflows | |
| cp "${workflow_seed_dir}/rust-release.yml" .github/workflows/rust-release.yml | |
| cp "${workflow_seed_dir}/shell-tool-mcp.yml" .github/workflows/shell-tool-mcp.yml | |
| cp "${workflow_seed_dir}/termux-release-checkpoint.yml" .github/workflows/termux-release-checkpoint.yml | |
| cp "${workflow_seed_dir}/termux-release-deploy.yml" .github/workflows/termux-release-deploy.yml | |
| cp "${workflow_seed_dir}/termux-release-promote.yml" .github/workflows/termux-release-promote.yml | |
| mkdir -p scripts | |
| cp "${script_seed_dir}/termux-create-checkpoint-pr.sh" scripts/termux-create-checkpoint-pr.sh | |
| cp "${script_seed_dir}/termux-download-release-artifact.sh" scripts/termux-download-release-artifact.sh | |
| cp "${script_seed_dir}/termux-find-release-pr.sh" scripts/termux-find-release-pr.sh | |
| chmod +x \ | |
| scripts/termux-create-checkpoint-pr.sh \ | |
| scripts/termux-download-release-artifact.sh \ | |
| scripts/termux-find-release-pr.sh | |
| } | |
| is_seeded_release_workflow() { | |
| case "$1" in | |
| .github/workflows/rust-release.yml | \ | |
| .github/workflows/shell-tool-mcp.yml | \ | |
| .github/workflows/termux-release-checkpoint.yml | \ | |
| .github/workflows/termux-release-deploy.yml | \ | |
| .github/workflows/termux-release-promote.yml | \ | |
| scripts/termux-create-checkpoint-pr.sh | \ | |
| scripts/termux-download-release-artifact.sh | \ | |
| scripts/termux-find-release-pr.sh) | |
| return 0 | |
| ;; | |
| *) | |
| return 1 | |
| ;; | |
| esac | |
| } | |
| resolve_seeded_release_workflow_conflicts() { | |
| local conflicted_path | |
| local resolved_any=false | |
| mapfile -t conflicted_paths < <(git diff --name-only --diff-filter=U) | |
| for conflicted_path in "${conflicted_paths[@]}"; do | |
| if is_seeded_release_workflow "${conflicted_path}"; then | |
| resolved_any=true | |
| fi | |
| done | |
| if [[ "${resolved_any}" != "true" ]]; then | |
| return 0 | |
| fi | |
| echo "Auto-resolving Termux-owned release workflow conflicts from the automation branch." | |
| seed_release_branch_workflows | |
| git add \ | |
| .github/workflows/rust-release.yml \ | |
| .github/workflows/shell-tool-mcp.yml \ | |
| .github/workflows/termux-release-checkpoint.yml \ | |
| .github/workflows/termux-release-deploy.yml \ | |
| .github/workflows/termux-release-promote.yml \ | |
| scripts/termux-create-checkpoint-pr.sh \ | |
| scripts/termux-download-release-artifact.sh \ | |
| scripts/termux-find-release-pr.sh | |
| } | |
| resolve_workspace_version_conflict() { | |
| local upstream_version | |
| upstream_version="$(workspace_version_from_ref "refs/tags/${UPSTREAM_TAG}")" | |
| if [[ -z "${upstream_version}" ]]; then | |
| echo "Unable to read workspace package version from refs/tags/${UPSTREAM_TAG}" >&2 | |
| return 0 | |
| fi | |
| if UPSTREAM_WORKSPACE_VERSION="${upstream_version}" perl -0pi -e ' | |
| my $version = $ENV{"UPSTREAM_WORKSPACE_VERSION"}; | |
| s/<<<<<<<[^\n]*\n(version = ")[^"]+("\n)=======\n\1[^"]+\2>>>>>>>[^\n]*(\n)/$1$version$2$3/s | |
| or exit 1; | |
| ' codex-rs/Cargo.toml; then | |
| git add codex-rs/Cargo.toml | |
| else | |
| echo "codex-rs/Cargo.toml has conflicts beyond the simple workspace version bump; leaving it for the fallback PR." | |
| git checkout -m -- codex-rs/Cargo.toml | |
| fi | |
| } | |
| resolve_known_release_train_conflicts() { | |
| local conflicted_path | |
| resolve_seeded_release_workflow_conflicts | |
| mapfile -t conflicted_paths < <(git diff --name-only --diff-filter=U) | |
| for conflicted_path in "${conflicted_paths[@]}"; do | |
| case "${conflicted_path}" in | |
| .github/workflows/*) | |
| echo "Auto-resolving upstream workflow conflict in ${conflicted_path} by keeping ${RELEASE_BRANCH}." | |
| git checkout --ours -- "${conflicted_path}" | |
| git add "${conflicted_path}" | |
| ;; | |
| esac | |
| done | |
| mapfile -t conflicted_paths < <(git diff --name-only --diff-filter=U) | |
| for conflicted_path in "${conflicted_paths[@]}"; do | |
| case "${conflicted_path}" in | |
| codex-rs/Cargo.toml) | |
| echo "Auto-resolving recurring workspace version conflict in codex-rs/Cargo.toml." | |
| resolve_workspace_version_conflict | |
| ;; | |
| codex-rs/app-server/tests/suite/v2/thread_resume.rs) | |
| echo "Auto-resolving upstream-only app-server test conflict in ${conflicted_path} by keeping refs/tags/${UPSTREAM_TAG}." | |
| git checkout --theirs -- "${conflicted_path}" | |
| git add "${conflicted_path}" | |
| ;; | |
| esac | |
| done | |
| } | |
| reset_for_fallback_checkout() { | |
| git reset --hard "origin/${RELEASE_BRANCH}" | |
| git clean -fd .github/workflows | |
| } | |
| workspace_version_from_ref() { | |
| local ref="$1" | |
| git show "${ref}:codex-rs/Cargo.toml" | awk ' | |
| /^\[workspace\.package\]$/ { in_workspace_package = 1; next } | |
| /^\[/ { in_workspace_package = 0 } | |
| in_workspace_package && /^version = / { | |
| gsub(/^version = "/, "") | |
| gsub(/"$/, "") | |
| exit | |
| } | |
| ' | |
| } | |
| normalize_patch_branch_version() { | |
| local normalized_ref="$1" | |
| local upstream_version | |
| upstream_version="$(workspace_version_from_ref "refs/tags/${UPSTREAM_TAG}")" | |
| if [[ -z "${upstream_version}" ]]; then | |
| echo "Unable to read workspace package version from refs/tags/${UPSTREAM_TAG}" >&2 | |
| exit 1 | |
| fi | |
| git checkout -B "${normalized_ref}" "origin/${PATCH_BRANCH}" | |
| if [[ ! -f codex-rs/Cargo.toml ]]; then | |
| echo "codex-rs/Cargo.toml is missing from ${PATCH_BRANCH}" >&2 | |
| exit 1 | |
| fi | |
| UPSTREAM_WORKSPACE_VERSION="${upstream_version}" perl -0pi -e ' | |
| my $version = $ENV{"UPSTREAM_WORKSPACE_VERSION"}; | |
| s/(\[workspace\.package\]\n(?:(?!^\[).*\n)*?version = ")[^"]+(")/$1$version$2/m | |
| or die "workspace.package version not found\n"; | |
| ' codex-rs/Cargo.toml | |
| if ! git diff --quiet -- codex-rs/Cargo.toml; then | |
| git add codex-rs/Cargo.toml | |
| git commit -m "Normalize Termux patch workspace version" | |
| fi | |
| } | |
| release_branch_open_pr_blockers() { | |
| local release_branch="$1" | |
| local release_slug="${release_branch//\//_}" | |
| local open_prs | |
| open_prs="$( | |
| gh pr list \ | |
| --repo "${GITHUB_REPOSITORY}" \ | |
| --state open \ | |
| --limit 200 \ | |
| --json number,title,body,headRefName,baseRefName,url | |
| )" | |
| jq -r \ | |
| --arg release_branch "${release_branch}" \ | |
| --arg release_slug "${release_slug}" \ | |
| ' | |
| .[] | |
| | select( | |
| .baseRefName == $release_branch | |
| or .headRefName == $release_branch | |
| or (.headRefName | startswith("checkpoint/") and contains("_from_" + $release_slug + "_")) | |
| or ((.title // "") | contains($release_branch)) | |
| or ((.body // "") | contains("Source branch: `" + $release_branch + "`")) | |
| or ((.body // "") | contains("Release train branch: `" + $release_branch + "`")) | |
| ) | |
| | "- #\(.number) \(.title) (\(.url))" | |
| ' <<< "${open_prs}" | |
| } | |
| delete_existing_release_branch_if_safe() { | |
| local release_branch="$1" | |
| local blockers | |
| blockers="$(release_branch_open_pr_blockers "${release_branch}")" | |
| if [[ -n "${blockers}" ]]; then | |
| echo "Keeping ${release_branch}: open PRs/checkpoints still reference it." | |
| printf '%s\n' "${blockers}" | |
| return 0 | |
| fi | |
| echo "Deleting existing ${release_branch} so ${UPSTREAM_TAG} can be rebuilt from the upstream tag." | |
| git push origin --delete "${release_branch}" | |
| } | |
| existing_prs="$( | |
| gh pr list \ | |
| --repo "${GITHUB_REPOSITORY}" \ | |
| --state all \ | |
| --search "\"${pr_title}\" in:title" \ | |
| --limit 100 \ | |
| --json number,title,state,isDraft,mergedAt,url | |
| )" | |
| existing_merged_pr="$( | |
| jq -c --arg title "${pr_title}" ' | |
| [.[] | select(.title == $title and (.state == "MERGED" or .mergedAt != null))] | |
| | sort_by(.number) | |
| | reverse | |
| | .[0] // empty | |
| ' <<< "${existing_prs}" | |
| )" | |
| if [[ -n "${existing_merged_pr}" ]]; then | |
| existing_pr_url="$(jq -r '.url' <<< "${existing_merged_pr}")" | |
| echo "A matching merged PR already exists for title '${pr_title}': ${existing_pr_url}." | |
| exit 0 | |
| fi | |
| existing_open_train_pr="$( | |
| gh pr list \ | |
| --repo "${GITHUB_REPOSITORY}" \ | |
| --state open \ | |
| --limit 200 \ | |
| --json number,title,body,headRefName,baseRefName,url \ | |
| | jq -c --arg release_branch "${RELEASE_BRANCH}" ' | |
| [ | |
| .[] | |
| | select(.baseRefName == $release_branch) | |
| | select( | |
| ((.title // "") | startswith("Termux rust-v")) | |
| and ((.body // "") | contains("Release train branch: `" + $release_branch + "`")) | |
| ) | |
| ] | |
| | sort_by(.number) | |
| | reverse | |
| | .[0] // empty | |
| ' | |
| )" | |
| existing_open_train_pr_url="" | |
| if [[ -n "${existing_open_train_pr}" ]]; then | |
| existing_open_train_pr_url="$(jq -r '.url' <<< "${existing_open_train_pr}")" | |
| existing_open_train_pr_head="$(jq -r '.headRefName' <<< "${existing_open_train_pr}")" | |
| if [[ "${existing_open_train_pr_head}" != "${WORK_BRANCH}" ]]; then | |
| echo "Open release train PR uses ${existing_open_train_pr_head}; updating that branch instead of ${WORK_BRANCH}." | |
| WORK_BRANCH="${existing_open_train_pr_head}" | |
| fi | |
| fi | |
| release_branch_exists=false | |
| if git ls-remote --exit-code --heads origin "${RELEASE_BRANCH}" >/dev/null 2>&1; then | |
| release_branch_exists=true | |
| fi | |
| if [[ "${release_branch_exists}" == true ]]; then | |
| delete_existing_release_branch_if_safe "${RELEASE_BRANCH}" | |
| if ! git ls-remote --exit-code --heads origin "${RELEASE_BRANCH}" >/dev/null 2>&1; then | |
| release_branch_exists=false | |
| fi | |
| fi | |
| if [[ "${release_branch_exists}" == false ]]; then | |
| git checkout -B "${RELEASE_BRANCH}" "refs/tags/${UPSTREAM_TAG}" | |
| seed_release_branch_workflows | |
| git add \ | |
| .github/workflows/rust-release.yml \ | |
| .github/workflows/shell-tool-mcp.yml \ | |
| .github/workflows/termux-release-checkpoint.yml \ | |
| .github/workflows/termux-release-deploy.yml \ | |
| .github/workflows/termux-release-promote.yml \ | |
| scripts/termux-create-checkpoint-pr.sh \ | |
| scripts/termux-download-release-artifact.sh \ | |
| scripts/termux-find-release-pr.sh | |
| if ! git diff --cached --quiet; then | |
| git commit -m "Seed Termux release automation" | |
| fi | |
| git push origin "${RELEASE_BRANCH}" | |
| fi | |
| git fetch origin "${RELEASE_BRANCH}" | |
| work_branch_exists=false | |
| if git ls-remote --exit-code --heads origin "${WORK_BRANCH}" >/dev/null 2>&1; then | |
| work_branch_exists=true | |
| git fetch origin "${WORK_BRANCH}" | |
| fi | |
| if [[ "${work_branch_exists}" == true ]]; then | |
| git checkout -B "${WORK_BRANCH}" "origin/${WORK_BRANCH}" | |
| else | |
| git checkout -B "${WORK_BRANCH}" "origin/${RELEASE_BRANCH}" | |
| fi | |
| if [[ "${release_branch_exists}" == false ]]; then | |
| normalized_patch_ref="termux-patch-normalized/${UPSTREAM_TAG}" | |
| normalize_patch_branch_version "${normalized_patch_ref}" | |
| git checkout -B "${WORK_BRANCH}" "origin/${RELEASE_BRANCH}" | |
| seed_release_branch_workflows | |
| echo "Creating Termux patch from refs/tags/${UPSTREAM_TAG} to ${normalized_patch_ref}." | |
| git diff --binary "refs/tags/${UPSTREAM_TAG}..${normalized_patch_ref}" \ | |
| -- . \ | |
| ':!.github/workflows/rust-release.yml' \ | |
| ':!.github/workflows/shell-tool-mcp.yml' \ | |
| ':!.github/workflows/termux-release-checkpoint.yml' \ | |
| ':!.github/workflows/termux-release-deploy.yml' \ | |
| ':!.github/workflows/termux-release-promote.yml' \ | |
| ':!scripts/termux-create-checkpoint-pr.sh' \ | |
| ':!scripts/termux-download-release-artifact.sh' \ | |
| ':!scripts/termux-find-release-pr.sh' \ | |
| > "${RUNNER_TEMP}/termux.patch" | |
| if [[ -s "${RUNNER_TEMP}/termux.patch" ]]; then | |
| if ! git apply --3way "${RUNNER_TEMP}/termux.patch"; then | |
| integration_conflicted=true | |
| conflict_context="Applying the Termux patch branch onto the upstream tag" | |
| fallback_ref="origin/${PATCH_BRANCH}" | |
| conflict_summary="$( | |
| git diff --name-only --diff-filter=U | sed 's/^/- `&`/' | |
| )" | |
| echo "Applying the Termux patch branch conflicted; creating a manual-resolution PR instead." >&2 | |
| reset_for_fallback_checkout | |
| git checkout -B "${WORK_BRANCH}" "${fallback_ref}" | |
| fi | |
| fi | |
| else | |
| if ! git merge --no-ff --no-edit "refs/tags/${UPSTREAM_TAG}"; then | |
| resolve_known_release_train_conflicts | |
| remaining_conflicts="$(git diff --name-only --diff-filter=U)" | |
| if [[ -z "${remaining_conflicts}" ]]; then | |
| git commit --no-edit | |
| else | |
| integration_conflicted=true | |
| conflict_context="Merging the upstream tag into the existing release train branch" | |
| fallback_ref="refs/tags/${UPSTREAM_TAG}" | |
| conflict_summary="$( | |
| printf '%s\n' "${remaining_conflicts}" | sed 's/^/- `&`/' | |
| )" | |
| echo "Merging the upstream tag conflicted; creating a manual-resolution PR instead." >&2 | |
| if git rev-parse -q --verify MERGE_HEAD >/dev/null; then | |
| git merge --abort | |
| fi | |
| reset_for_fallback_checkout | |
| git checkout -B "${WORK_BRANCH}" "${fallback_ref}" | |
| fi | |
| fi | |
| fi | |
| seed_release_branch_workflows | |
| mkdir -p .github | |
| jq -n \ | |
| --arg upstream_repo "${UPSTREAM_REPO}" \ | |
| --arg upstream_tag "${UPSTREAM_TAG}" \ | |
| --arg upstream_name "${UPSTREAM_NAME}" \ | |
| --arg upstream_html_url "${UPSTREAM_HTML_URL}" \ | |
| --arg upstream_target "${UPSTREAM_TARGET}" \ | |
| --arg upstream_release_id "${UPSTREAM_ID}" \ | |
| --arg release_train "${RELEASE_TRAIN}" \ | |
| --arg release_branch "${RELEASE_BRANCH}" \ | |
| --arg work_branch "${WORK_BRANCH}" \ | |
| --arg patch_branch "${PATCH_BRANCH}" \ | |
| --arg patch_source_sha "$(git rev-parse "origin/${PATCH_BRANCH}")" \ | |
| --arg termux_tag "${TERMUX_TAG}" \ | |
| --argjson upstream_prerelease "${UPSTREAM_PRERELEASE}" \ | |
| '{ | |
| upstream_repo: $upstream_repo, | |
| upstream_tag: $upstream_tag, | |
| upstream_name: $upstream_name, | |
| upstream_html_url: $upstream_html_url, | |
| upstream_target: $upstream_target, | |
| upstream_release_id: $upstream_release_id, | |
| upstream_prerelease: $upstream_prerelease, | |
| release_train: $release_train, | |
| release_branch: $release_branch, | |
| work_branch: $work_branch, | |
| patch_branch: $patch_branch, | |
| patch_source_sha: $patch_source_sha, | |
| termux_tag: $termux_tag | |
| }' > .github/termux-release.json | |
| git add -A | |
| if git diff --cached --quiet; then | |
| echo "No changes to propose for ${UPSTREAM_TAG}." | |
| exit 0 | |
| fi | |
| git commit -m "Prepare Termux ${UPSTREAM_TAG}" | |
| git push --force-with-lease origin "${WORK_BRANCH}" | |
| body_path="${RUNNER_TEMP}/termux-release-pr.md" | |
| { | |
| echo "## Termux release train" | |
| echo | |
| echo "- Upstream release: ${UPSTREAM_HTML_URL}" | |
| echo "- Upstream tag: \`${UPSTREAM_TAG}\`" | |
| echo "- Termux release tag: \`${TERMUX_TAG}\`" | |
| echo "- Release train branch: \`${RELEASE_BRANCH}\`" | |
| echo "- Patch source: \`${PATCH_BRANCH}\`" | |
| echo | |
| echo "Merging this PR is the manual approval gate. The release build workflow uploads the Android artifact to test; after merge, the deployment workflow attaches that exact artifact to \`${TERMUX_TAG}\` and opens the checkpoint PR." | |
| if [[ "${integration_conflicted}" == "true" ]]; then | |
| echo | |
| echo "## Merge conflicts" | |
| echo | |
| echo "${conflict_context} conflicted in GitHub Actions, so this PR was created from \`${fallback_ref}\` for manual resolution." | |
| echo | |
| echo "Conflicted paths from the failed integration attempt:" | |
| if [[ -n "${conflict_summary}" ]]; then | |
| printf '%s\n' "${conflict_summary}" | |
| else | |
| echo "- Conflict details unavailable" | |
| fi | |
| fi | |
| echo | |
| echo "## Upstream notes" | |
| echo | |
| printf '%s\n' "${UPSTREAM_BODY}" | |
| } > "${body_path}" | |
| if [[ -n "${existing_open_train_pr_url}" ]]; then | |
| pr_url="${existing_open_train_pr_url}" | |
| gh pr edit "${pr_url}" \ | |
| --repo "${GITHUB_REPOSITORY}" \ | |
| --title "${pr_title}" \ | |
| --body-file "${body_path}" | |
| else | |
| pr_url="$( | |
| gh pr create \ | |
| --repo "${GITHUB_REPOSITORY}" \ | |
| --base "${RELEASE_BRANCH}" \ | |
| --head "${WORK_BRANCH}" \ | |
| --title "${pr_title}" \ | |
| --body-file "${body_path}" | |
| )" | |
| fi | |
| gh pr edit "${pr_url}" --repo "${GITHUB_REPOSITORY}" --add-reviewer "${REVIEWER}" || true | |
| gh label create termux-release --repo "${GITHUB_REPOSITORY}" --color 0e8a16 --description "Termux release automation" --force | |
| gh label create release-train --repo "${GITHUB_REPOSITORY}" --color 1d76db --description "Release train PR" --force | |
| gh pr edit "${pr_url}" --repo "${GITHUB_REPOSITORY}" --add-label "termux-release" --add-label "release-train" | |
| echo "pr_url=${pr_url}" >> "$GITHUB_OUTPUT" | |
| mirror-rusty-v8-release: | |
| needs: | |
| - look-for-newer-release | |
| if: needs.look-for-newer-release.outputs.selected == 'true' && needs.look-for-newer-release.outputs.release_kind == 'rusty-v8' | |
| runs-on: ubuntu-slim | |
| permissions: | |
| actions: write | |
| contents: read | |
| env: | |
| GH_REPO: ${{ github.repository }} | |
| UPSTREAM_REPO: openai/codex | |
| UPSTREAM_TAG: ${{ needs.look-for-newer-release.outputs.upstream_tag }} | |
| RUSTY_V8_RELEASE_TAG: ${{ needs.look-for-newer-release.outputs.termux_tag }} | |
| steps: | |
| - name: Generate GitHub App token | |
| id: app-token | |
| uses: actions/create-github-app-token@v3 | |
| with: | |
| client-id: ${{ vars.APP_CLIENT_ID }} | |
| private-key: ${{ secrets.APP_PRIVATE_KEY }} | |
| - name: Checkout automation branch | |
| uses: actions/checkout@v6 | |
| with: | |
| fetch-depth: 1 | |
| token: ${{ steps.app-token.outputs.token }} | |
| - name: 🧰 Actions Toolbox | |
| # This is required for the GitHub CLI | |
| env: | |
| GH_TOKEN: ${{ steps.app-token.outputs.token }} | |
| uses: wallentx/gh-actions/composite/actions-toolbox@main | |
| - name: Dispatch rusty_v8 mirror workflow | |
| shell: bash | |
| env: | |
| GH_TOKEN: ${{ steps.app-token.outputs.token }} | |
| run: | | |
| set -euo pipefail | |
| echo "Dispatching rusty-v8-release.yml for ${UPSTREAM_REPO}@${UPSTREAM_TAG} as ${RUSTY_V8_RELEASE_TAG}" | |
| gh workflow run rusty-v8-release.yml \ | |
| --repo "${GITHUB_REPOSITORY}" \ | |
| --ref "${GITHUB_REF_NAME}" \ | |
| -f "source_repository=${UPSTREAM_REPO}" \ | |
| -f "release_ref=${UPSTREAM_TAG}" \ | |
| -f "release_tag=${RUSTY_V8_RELEASE_TAG}" \ | |
| -f "publish=true" | |
| cleanup-completed-release-branches: | |
| needs: look-for-newer-release | |
| if: always() && needs.look-for-newer-release.result == 'success' | |
| runs-on: ubuntu-slim | |
| permissions: | |
| contents: write | |
| pull-requests: read | |
| env: | |
| GH_REPO: ${{ github.repository }} | |
| steps: | |
| - name: Generate GitHub App token | |
| id: app-token | |
| uses: actions/create-github-app-token@v3 | |
| with: | |
| client-id: ${{ vars.APP_CLIENT_ID }} | |
| private-key: ${{ secrets.APP_PRIVATE_KEY }} | |
| - name: Checkout automation branch | |
| uses: actions/checkout@v6 | |
| with: | |
| fetch-depth: 0 | |
| token: ${{ steps.app-token.outputs.token }} | |
| - name: 🧰 Actions Toolbox | |
| # This is required for the GitHub CLI | |
| env: | |
| GH_TOKEN: ${{ steps.app-token.outputs.token }} | |
| uses: wallentx/gh-actions/composite/actions-toolbox@main | |
| - name: Delete completed release branches | |
| shell: bash | |
| env: | |
| GH_TOKEN: ${{ steps.app-token.outputs.token }} | |
| run: | | |
| set -euo pipefail | |
| git fetch --prune origin '+refs/heads/release/*:refs/remotes/origin/release/*' | |
| open_prs="$( | |
| gh pr list \ | |
| --repo "${GITHUB_REPOSITORY}" \ | |
| --state open \ | |
| --limit 200 \ | |
| --json number,title,body,headRefName,baseRefName,url | |
| )" | |
| mapfile -t release_refs < <( | |
| git for-each-ref --format='%(refname:short)' refs/remotes/origin/release | |
| ) | |
| if ((${#release_refs[@]} == 0)); then | |
| echo "No release branches found." | |
| exit 0 | |
| fi | |
| for release_ref in "${release_refs[@]}"; do | |
| release_branch="${release_ref#origin/}" | |
| metadata="$(git show "${release_ref}:.github/termux-release.json" 2>/dev/null || true)" | |
| if [[ -z "${metadata}" ]]; then | |
| echo "Keeping ${release_branch}: no .github/termux-release.json metadata." | |
| continue | |
| fi | |
| termux_tag="$(jq -r '.termux_tag // empty' <<< "${metadata}")" | |
| if [[ -z "${termux_tag}" ]]; then | |
| echo "Keeping ${release_branch}: metadata does not include termux_tag." | |
| continue | |
| fi | |
| if ! gh release view "${termux_tag}" --repo "${GITHUB_REPOSITORY}" >/dev/null 2>&1; then | |
| echo "Keeping ${release_branch}: release ${termux_tag} does not exist yet." | |
| continue | |
| fi | |
| blocking_prs="$( | |
| jq -r --arg branch "${release_branch}" ' | |
| .[] | |
| | select( | |
| .baseRefName == $branch | |
| or .headRefName == $branch | |
| or (.title | contains($branch)) | |
| or ((.body // "") | contains($branch)) | |
| ) | |
| | "\(.url) #\(.number)" | |
| ' <<< "${open_prs}" | |
| )" | |
| if [[ -n "${blocking_prs}" ]]; then | |
| echo "Keeping ${release_branch}: open PRs still reference it." | |
| printf '%s\n' "${blocking_prs}" | |
| continue | |
| fi | |
| echo "Deleting completed release branch ${release_branch} for ${termux_tag}." | |
| if ! git push origin --delete "${release_branch}"; then | |
| echo "::warning::Failed to delete ${release_branch}; continuing cleanup." | |
| fi | |
| done |