Seed Termux release automation #76
Workflow file for this run
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-deploy | |
| on: | |
| push: | |
| branches: | |
| - "release/**" | |
| workflow_dispatch: | |
| inputs: | |
| release_branch: | |
| description: "Release branch to deploy, for example release/0.124.0" | |
| required: true | |
| type: string | |
| release_sha: | |
| description: "Release branch commit SHA to deploy. Defaults to the branch head." | |
| required: false | |
| default: "" | |
| type: string | |
| pr_number: | |
| description: "Merged release PR number to promote. Optional; normally discovered automatically." | |
| required: false | |
| default: "" | |
| type: string | |
| pr_head_sha: | |
| description: "Merged release PR head SHA. Required only when pr_number is set." | |
| required: false | |
| default: "" | |
| type: string | |
| destination_branch: | |
| description: "Destination patch branch to receive the checkpoint PR" | |
| required: false | |
| default: "wallentx/termux-target" | |
| type: string | |
| reviewer: | |
| description: "GitHub username to request as reviewer on the checkpoint PR" | |
| required: false | |
| default: "wallentx" | |
| type: string | |
| 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-deploy-${{ github.event_name == 'workflow_dispatch' && inputs.release_branch || github.ref_name }} | |
| cancel-in-progress: false | |
| jobs: | |
| deploy: | |
| runs-on: ubuntu-24.04 | |
| if: ${{ github.event_name == 'workflow_dispatch' || !startsWith(github.event.head_commit.message, 'Seed Termux release automation') }} | |
| permissions: | |
| actions: read | |
| contents: write | |
| deployments: write | |
| issues: write | |
| pull-requests: write | |
| env: | |
| GH_REPO: ${{ github.repository }} | |
| REQUESTED_RELEASE_BRANCH: ${{ inputs.release_branch }} | |
| REQUESTED_RELEASE_SHA: ${{ inputs.release_sha }} | |
| INPUT_PR_NUMBER: ${{ inputs.pr_number }} | |
| INPUT_PR_HEAD_SHA: ${{ inputs.pr_head_sha }} | |
| DESTINATION_BRANCH: ${{ inputs.destination_branch || 'wallentx/termux-target' }} | |
| REVIEWER: ${{ inputs.reviewer || 'wallentx' }} | |
| 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 release branch | |
| uses: actions/checkout@v6 | |
| with: | |
| fetch-depth: 0 | |
| ref: ${{ github.event_name == 'workflow_dispatch' && inputs.release_branch || github.ref }} | |
| 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" | |
| release_branch="${REQUESTED_RELEASE_BRANCH:-${GITHUB_REF_NAME}}" | |
| git fetch --prune origin "${DESTINATION_BRANCH}" "${release_branch}" | |
| - name: Resolve release ref | |
| id: release-ref | |
| shell: bash | |
| run: | | |
| set -euo pipefail | |
| if [[ "${GITHUB_EVENT_NAME}" == "workflow_dispatch" ]]; then | |
| release_branch="${REQUESTED_RELEASE_BRANCH}" | |
| if [[ -n "${REQUESTED_RELEASE_SHA}" ]]; then | |
| git checkout --detach "${REQUESTED_RELEASE_SHA}" | |
| release_sha="${REQUESTED_RELEASE_SHA}" | |
| else | |
| release_sha="$(git rev-parse HEAD)" | |
| fi | |
| else | |
| release_branch="${GITHUB_REF_NAME}" | |
| release_sha="${GITHUB_SHA}" | |
| fi | |
| { | |
| echo "branch=${release_branch}" | |
| echo "sha=${release_sha}" | |
| } >> "$GITHUB_OUTPUT" | |
| - name: Read release metadata | |
| id: metadata | |
| shell: bash | |
| env: | |
| GH_TOKEN: ${{ steps.app-token.outputs.token }} | |
| run: | | |
| set -euo pipefail | |
| metadata=".github/termux-release.json" | |
| if [[ ! -f "${metadata}" ]]; then | |
| echo "No ${metadata}; this push is not a Termux release deployment." | |
| echo "deploy=false" >> "$GITHUB_OUTPUT" | |
| exit 0 | |
| fi | |
| upstream_tag="$(jq -r '.upstream_tag // empty' "${metadata}")" | |
| upstream_name="$(jq -r '.upstream_name // .upstream_tag // empty' "${metadata}")" | |
| termux_tag="$(jq -r '.termux_tag // empty' "${metadata}")" | |
| upstream_version="${upstream_tag#rust-v}" | |
| upstream_version="${upstream_version%-termux}" | |
| upstream_prerelease=false | |
| if [[ "${upstream_version}" == *-* ]]; then | |
| upstream_prerelease=true | |
| fi | |
| upstream_html_url="$(jq -r '.upstream_html_url // ""' "${metadata}")" | |
| upstream_repo="$(jq -r '.upstream_repo // "openai/codex"' "${metadata}")" | |
| release_train="$(jq -r '.release_train // ""' "${metadata}")" | |
| if [[ -z "${upstream_tag}" || -z "${termux_tag}" ]]; then | |
| echo "Missing upstream_tag or termux_tag in ${metadata}" >&2 | |
| exit 1 | |
| fi | |
| release_exists=false | |
| asset_exists=false | |
| if gh release view "${termux_tag}" --repo "${GITHUB_REPOSITORY}" >/dev/null 2>&1; then | |
| release_exists=true | |
| release_asset_exists="$( | |
| gh release view "${termux_tag}" \ | |
| --repo "${GITHUB_REPOSITORY}" \ | |
| --json assets \ | |
| --jq '.assets | map(.name) | any(. == "codex-aarch64-linux-android.tar.gz")' | |
| )" | |
| if [[ "${release_asset_exists}" == "true" ]]; then | |
| asset_exists=true | |
| fi | |
| fi | |
| { | |
| echo "deploy=true" | |
| echo "upstream_tag=${upstream_tag}" | |
| echo "upstream_name=${upstream_name}" | |
| echo "termux_tag=${termux_tag}" | |
| echo "upstream_prerelease=${upstream_prerelease}" | |
| echo "upstream_html_url=${upstream_html_url}" | |
| echo "upstream_repo=${upstream_repo}" | |
| echo "release_train=${release_train}" | |
| echo "release_exists=${release_exists}" | |
| echo "asset_exists=${asset_exists}" | |
| } >> "$GITHUB_OUTPUT" | |
| - name: Create deployment | |
| if: steps.metadata.outputs.deploy == 'true' | |
| id: deployment | |
| shell: bash | |
| env: | |
| GH_TOKEN: ${{ github.token }} | |
| RELEASE_SHA: ${{ steps.release-ref.outputs.sha }} | |
| TERMUX_TAG: ${{ steps.metadata.outputs.termux_tag }} | |
| run: | | |
| set -euo pipefail | |
| deployment_id="$( | |
| gh api \ | |
| -X POST \ | |
| "repos/${GITHUB_REPOSITORY}/deployments" \ | |
| -f ref="${RELEASE_SHA}" \ | |
| -f environment="termux-release" \ | |
| -F auto_merge=false \ | |
| -F required_contexts[] \ | |
| -f description="Termux release deployment for ${TERMUX_TAG}" \ | |
| --jq '.id' | |
| )" | |
| echo "id=${deployment_id}" >> "$GITHUB_OUTPUT" | |
| - name: Mark deployment in progress | |
| if: steps.metadata.outputs.deploy == 'true' | |
| shell: bash | |
| env: | |
| GH_TOKEN: ${{ github.token }} | |
| DEPLOYMENT_ID: ${{ steps.deployment.outputs.id }} | |
| run: | | |
| set -euo pipefail | |
| log_url="${GH_WORKFLOW_URL:-${GITHUB_SERVER_URL}/${GITHUB_REPOSITORY}/actions/runs/${GITHUB_RUN_ID}}" | |
| gh api \ | |
| -X POST \ | |
| "repos/${GITHUB_REPOSITORY}/deployments/${DEPLOYMENT_ID}/statuses" \ | |
| -f state="in_progress" \ | |
| -f environment="termux-release" \ | |
| -f log_url="${log_url}" \ | |
| -F auto_inactive=false \ | |
| -f description="Promoting Termux release artifact and preparing checkpoint PR" \ | |
| >/dev/null | |
| - name: Locate merged pull request | |
| if: steps.metadata.outputs.deploy == 'true' && steps.metadata.outputs.asset_exists != 'true' | |
| id: pr | |
| shell: bash | |
| env: | |
| GH_TOKEN: ${{ steps.app-token.outputs.token }} | |
| RELEASE_BRANCH: ${{ steps.release-ref.outputs.branch }} | |
| RELEASE_SHA: ${{ steps.release-ref.outputs.sha }} | |
| run: | | |
| scripts/termux-find-release-pr.sh | |
| - name: Download promoted PR artifact | |
| if: steps.metadata.outputs.deploy == 'true' && steps.metadata.outputs.asset_exists != 'true' | |
| shell: bash | |
| env: | |
| GH_TOKEN: ${{ steps.app-token.outputs.token }} | |
| PR_ARTIFACT_NAME: ${{ steps.pr.outputs.artifact_name }} | |
| HEAD_SHA: ${{ steps.pr.outputs.head_sha }} | |
| run: | | |
| scripts/termux-download-release-artifact.sh | |
| - name: Create or update mirrored Termux release | |
| if: steps.metadata.outputs.deploy == 'true' && steps.metadata.outputs.asset_exists != 'true' | |
| shell: bash | |
| env: | |
| GH_TOKEN: ${{ steps.app-token.outputs.token }} | |
| UPSTREAM_TAG: ${{ steps.metadata.outputs.upstream_tag }} | |
| UPSTREAM_REPO: ${{ steps.metadata.outputs.upstream_repo }} | |
| UPSTREAM_NAME: ${{ steps.metadata.outputs.upstream_name }} | |
| TERMUX_TAG: ${{ steps.metadata.outputs.termux_tag }} | |
| UPSTREAM_PRERELEASE: ${{ steps.metadata.outputs.upstream_prerelease }} | |
| UPSTREAM_HTML_URL: ${{ steps.metadata.outputs.upstream_html_url }} | |
| RELEASE_TRAIN: ${{ steps.metadata.outputs.release_train }} | |
| RELEASE_EXISTS: ${{ steps.metadata.outputs.release_exists }} | |
| PR_NUMBER: ${{ steps.pr.outputs.number }} | |
| HEAD_SHA: ${{ steps.pr.outputs.head_sha }} | |
| RELEASE_SHA: ${{ steps.release-ref.outputs.sha }} | |
| run: | | |
| set -euo pipefail | |
| body_path="${RUNNER_TEMP}/release-body.md" | |
| upstream_body_path="${RUNNER_TEMP}/upstream-release-body.md" | |
| upstream_body_without_changelog_path="${RUNNER_TEMP}/upstream-release-body-without-changelog.md" | |
| if gh release view "${UPSTREAM_TAG}" \ | |
| --repo "${UPSTREAM_REPO}" \ | |
| --json body \ | |
| --jq '.body // ""' > "${upstream_body_path}"; then | |
| awk ' | |
| function heading_level(line, text) { | |
| if (match(line, /^(#{1,6})[[:space:]]+(.+)$/, parts)) { | |
| text = tolower(parts[2]) | |
| sub(/[[:space:]]+#+[[:space:]]*$/, "", text) | |
| gsub(/^[[:space:]]+|[[:space:]]+$/, "", text) | |
| if (text == "changelog" || text == "change log") { | |
| return length(parts[1]) | |
| } | |
| } | |
| return 0 | |
| } | |
| { | |
| if (!skip) { | |
| level = heading_level($0) | |
| if (level > 0) { | |
| skip = 1 | |
| skip_level = level | |
| next | |
| } | |
| next | |
| } | |
| if (match($0, /^(#{1,6})[[:space:]]+/, parts) && length(parts[1]) <= skip_level) { | |
| skip = 0 | |
| } | |
| } | |
| ' "${upstream_body_path}" > "${upstream_body_without_changelog_path}" | |
| else | |
| echo "::warning title=Upstream release notes unavailable::Could not read ${UPSTREAM_REPO} release ${UPSTREAM_TAG}." | |
| : > "${upstream_body_without_changelog_path}" | |
| fi | |
| { | |
| echo "Termux Android build for ${UPSTREAM_TAG}." | |
| echo | |
| echo "- Upstream release: ${UPSTREAM_HTML_URL}" | |
| echo "- Release train: \`${RELEASE_TRAIN}\`" | |
| echo "- Promoted PR: #${PR_NUMBER}" | |
| echo "- Promoted PR head SHA: \`${HEAD_SHA}\`" | |
| } > "${body_path}" | |
| if [[ -s "${upstream_body_without_changelog_path}" ]]; then | |
| { | |
| echo | |
| echo "## Upstream release notes" | |
| echo | |
| cat "${upstream_body_without_changelog_path}" | |
| } >> "${body_path}" | |
| fi | |
| release_title="${UPSTREAM_NAME}" | |
| if [[ -z "${release_title}" || "${release_title}" == "null" ]]; then | |
| release_title="${TERMUX_TAG}" | |
| fi | |
| if [[ "${RELEASE_EXISTS}" == "true" ]]; then | |
| gh release upload \ | |
| "${TERMUX_TAG}" \ | |
| "promoted/codex-aarch64-linux-android.tar.gz#codex-termux" \ | |
| --repo "${GITHUB_REPOSITORY}" \ | |
| --clobber | |
| exit 0 | |
| fi | |
| release_args=( | |
| gh release create "${TERMUX_TAG}" | |
| --repo "${GITHUB_REPOSITORY}" | |
| --target "${RELEASE_SHA}" | |
| --title "${release_title}" | |
| --notes-file "${body_path}" | |
| ) | |
| if [[ "${UPSTREAM_PRERELEASE}" == "true" ]]; then | |
| release_args+=(--prerelease) | |
| fi | |
| release_args+=("promoted/codex-aarch64-linux-android.tar.gz#codex-termux") | |
| "${release_args[@]}" | |
| - name: Ensure checkpoint PR | |
| if: steps.metadata.outputs.deploy == 'true' | |
| id: checkpoint | |
| shell: bash | |
| env: | |
| GH_TOKEN: ${{ steps.app-token.outputs.token }} | |
| SOURCE_BRANCH: ${{ steps.release-ref.outputs.branch }} | |
| SOURCE_SHA: ${{ steps.release-ref.outputs.sha }} | |
| run: | | |
| scripts/termux-create-checkpoint-pr.sh | |
| - name: Mark deployment success | |
| if: steps.metadata.outputs.deploy == 'true' | |
| shell: bash | |
| env: | |
| GH_TOKEN: ${{ github.token }} | |
| DEPLOYMENT_ID: ${{ steps.deployment.outputs.id }} | |
| TERMUX_TAG: ${{ steps.metadata.outputs.termux_tag }} | |
| run: | | |
| set -euo pipefail | |
| log_url="${GH_WORKFLOW_URL:-${GITHUB_SERVER_URL}/${GITHUB_REPOSITORY}/actions/runs/${GITHUB_RUN_ID}}" | |
| gh api \ | |
| -X POST \ | |
| "repos/${GITHUB_REPOSITORY}/deployments/${DEPLOYMENT_ID}/statuses" \ | |
| -f state="success" \ | |
| -f environment="termux-release" \ | |
| -f log_url="${log_url}" \ | |
| -F auto_inactive=false \ | |
| -f description="Termux release deployment completed for ${TERMUX_TAG}" \ | |
| >/dev/null | |
| - name: Mark deployment failure | |
| if: failure() && steps.deployment.outputs.id != '' | |
| shell: bash | |
| env: | |
| GH_TOKEN: ${{ github.token }} | |
| DEPLOYMENT_ID: ${{ steps.deployment.outputs.id }} | |
| run: | | |
| set -euo pipefail | |
| log_url="${GH_WORKFLOW_URL:-${GITHUB_SERVER_URL}/${GITHUB_REPOSITORY}/actions/runs/${GITHUB_RUN_ID}}" | |
| gh api \ | |
| -X POST \ | |
| "repos/${GITHUB_REPOSITORY}/deployments/${DEPLOYMENT_ID}/statuses" \ | |
| -f state="failure" \ | |
| -f environment="termux-release" \ | |
| -f log_url="${log_url}" \ | |
| -F auto_inactive=false \ | |
| -f description="Termux release deployment failed" \ | |
| >/dev/null |