Skip to content

Seed Termux release automation #76

Seed Termux release automation

Seed Termux release automation #76

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
}
print
next
}
if (match($0, /^(#{1,6})[[:space:]]+/, parts) && length(parts[1]) <= skip_level) {
skip = 0
print
}
}
' "${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