Repository: termux/termux-packages
Pull request #24331: ci(packages.yml): implement draft CI fast path for reusing PR build artifacts
Author: login: twaik
Created: 2025-04-14T20:03:55Z | Merged: 2025-04-17T17:55:18Z

This PR lets us reuse PR build artifacts in the case if none of buildsystem or build dependencies files were updated after PR diverged.

Also this PR lets us reuse CI artifacts of previous CI runs after pushes and force-pushes if they are relevant.

I intentionally made it NOT fail in any case because I want to monitor how it goes for different PRs and make sure all checks work fine.

When we know everything works fine I will implement downloading PR build artifacts and reusing them instead of running package building.


Diff:
diff --git a/.github/workflows/packages.yml b/.github/workflows/packages.yml
index 3db2150dca0..3cc6bba40f0 100644
--- a/.github/workflows/packages.yml
+++ b/.github/workflows/packages.yml
@@ -76,6 +76,8 @@ jobs:
         mkdir -p ./artifacts ./debs
         touch ./debs/.placeholder
 
+        OLD_COMMIT="$OLD_COMMIT" HEAD_COMMIT="$HEAD_COMMIT" scripts/utils/termux_reuse_pr_build_artifacts.sh "${{ github.event_name }}" "${{ matrix.target_arch }}" || true
+
         if [ "${{ github.event_name }}" != "workflow_dispatch" ]; then
           # GitHub sometimes add merge commits at the end
           # To prevent user confusion, filter them with --no-merges
diff --git a/scripts/utils/termux_reuse_pr_build_artifacts.sh b/scripts/utils/termux_reuse_pr_build_artifacts.sh
new file mode 100755
index 00000000000..6f3b349da36
--- /dev/null
+++ b/scripts/utils/termux_reuse_pr_build_artifacts.sh
@@ -0,0 +1,251 @@
+#!/bin/bash
+set -euo pipefail
+
+GITHUB_EVENT_NAME="${1:-}"
+TARGET_ARCH="${2:-}"
+
+infoexit() {
+  echo "$@"
+  exit 1
+} >&2
+
+# Check required variables via nameref.
+for var in OLD_COMMIT HEAD_COMMIT GITHUB_TOKEN GITHUB_EVENT_NAME TARGET_ARCH; do
+  [[ -n "${!var:-}" ]] || infoexit "$var is unset, not performing CI fast path"
+done
+
+graphql_request() {
+  local QUERY="${1}"
+  # Send GraphQL to GitHub and obtain response
+  # No hard tabs or newlines allowed inside a GraphQL request,
+  # since it is encoded as a JSON string this means we should also escape quotes.
+  curl --silent \
+    -H "Authorization: token ${GITHUB_TOKEN}" \
+    -H "Accept: application/vnd.github.v3+json" \
+    -H 'Content-Type: application/json' \
+    -X POST \
+    --data "{ \"query\": \"$(tr '\t\n' '  ' <<< "${QUERY//\"/\\\"}\"}")" \
+    "https://api.github.com/graphql" \
+  || return $?
+}
+
+ci_artifact_url() {
+  curl --silent \
+    -H "Authorization: token ${GITHUB_TOKEN}" \
+    -H "Accept: application/vnd.github.v3+json" \
+    "https://api.github.com/repos/termux/termux-packages/actions/runs/${1}/artifacts" \
+    | jq -r '[.artifacts[] | select(.name | startswith("debs-'"${TARGET_ARCH}"'")) | .archive_download_url][0] // empty' \
+  || return $?
+}
+
+read -d'\n' -ra COMMITS < <(git rev-list --no-merges "$OLD_COMMIT..$HEAD_COMMIT" || :)
+(( ${#COMMITS[*]} )) && infoexit "Unable to obtain full commit history. Not performing CI fast path."
+
+[[ "${GITHUB_EVENT_NAME:-}" == "push" ]] && {
+  # Check if we can perform CI fast path.
+  # We need to make sure all commits come from a single PR,
+  # and make sure buildsystem nor dependencies were changed.
+  # If so we can reuse PR check artifacts and upload them to apt repo to save some CI time
+
+   read -d'\n' -ra TERMUX_PACKAGE_DIRECTORIES < <(jq --raw-output 'del(.pkg_format) | keys | .[]' repo.json)
+
+  # We should obtain data about all commits in this push to check that they are from the same PR if any
+  RELATED_PRS_QUERY="
+  query {
+    repository(owner: \"termux\", name: \"termux-packages\") {
+    $(
+      for commit in "${COMMITS[@]}"; do
+        # Add a query for this commit with the shorthash as the label
+        echo "_${commit::7}: object(oid: \"${commit}\") { ...commitWithPR }"
+      done
+    )
+    }
+  }
+
+  fragment commitWithPR on Commit {
+    associatedPullRequests(first: 1) { nodes { baseRefOid headRefOid } edges { node { title body number } } }
+  }"
+
+  RESPONSE="$(graphql_request "$RELATED_PRS_QUERY" || infoexit "Couldn't query associated PRs for commit(s), not performing CI fast path")"
+
+  # Ensure response is valid and obtain all associated PR numbers
+  read -d'\n' -ra PRS < <(
+    jq '.data.repository | to_entries[] | .value.associatedPullRequests.edges
+      | (.[] // []) | (.node? // []) | (.number? // 0)' <<< "$RESPONSE" \
+    ) || infoexit "GraphQL response is invalid, not performing CI fast path"
+
+  # Check that all commits come from the one and only one PR, bail if not
+  (( ${#PRS[*]} == 0 )) && infoexit "push does not have a linked PR, not performing CI fast path"
+  (( ${#PRS[*]}  > 1 )) && infoexit "push contains commits from more than one PR, not performing CI fast path"
+
+  read -rd' ' PR_BASE_COMMIT PR_HEAD_COMMIT PR_COMMIT_TITLE PR_COMMIT_BODY < <(jq -r '\
+    .data.repository | to_entries[0].value.associatedPullRequests.nodes[0].baseRefOid,
+    .data.repository | to_entries[0].value.associatedPullRequests.nodes[0].headRefOid,
+    .data.repository | to_entries[0].value.associatedPullRequests.edges[0].node.title,
+    .data.repository | to_entries[0].value.associatedPullRequests.edges[0].node.body' <<< "$RESPONSE" || :)
+  [[ -n "${PR_BASE_COMMIT:-}" ]] || infoexit "failed to read associated PR base commit, not performing CI fast path"
+  [[ -n "${PR_HEAD_COMMIT:-}" ]] || infoexit "failed to read associated PR head commit, not performing CI fast path"
+
+  echo "::group::Detected PR #${#PRS[*]}: ${PR_COMMIT_TITLE}"
+  echo "${PR_COMMIT_BODY}"
+  echo "::endgroup::"
+
+  PR_CI_REUSE=0
+  # Check for single-commit and squashed PRs if `[ci reuse]` is in the commit message
+  if [[ "${#COMMITS[*]}" -eq 1 && "$(git log -1 --pretty=format:"%s%n%b" "${COMMITS[@]}")" == *"[ci reuse]"* ]]; then
+    PR_CI_REUSE=1
+    echo "Commit subject or description contain [ci reuse]"
+  else
+    # Otherwise check linked PR title and body
+    [[ "${PR_COMMIT_TITLE}${PR_COMMIT_BODY}" == *"[ci reuse]"* ]] && PR_CI_REUSE=1
+    (( PR_CI_REUSE )) && echo "PR description contains [ci reuse]"
+  fi
+
+  DIRS_REGEX="$(paste -sd'|' <<< "${TERMUX_PACKAGE_DIRECTORIES[@]}")" || exit 0
+
+  # fetch PR commit tree
+  git fetch origin "$PR_BASE_COMMIT:ref/tmp/$PR_BASE_COMMIT" || infoexit "failed to fetch PR base tree, not performing CI fast path"
+  git fetch origin "$PR_HEAD_COMMIT:ref/tmp/$PR_HEAD_COMMIT" || infoexit "failed to fetch PR head tree, not performing CI fast path"
+
+  # Here we compare changes from PR with changes from push
+  # this is to make sure nobody injected additional changes to PR branch after CI was invoked
+  # but before we fetched data in this check with GraphQL and `git fetch`.
+  # We cannot apply `git diff --no-index` to commit ranges so we are going to strip indexes manually with sed.
+  diff -q \
+      <(git diff "$PR_BASE_COMMIT" "$PR_HEAD_COMMIT" | sed -n -E '/^diff --git a\// { p; n; /^index /!p; b } ; p') \
+      <(git diff "$OLD_COMMIT" "$HEAD_COMMIT" | sed -n -E '/^diff --git a\// { p; n; /^index /!p; b } ; p') &> /dev/null \
+  || infoexit "PR head does not match pushed commit changes, probably PR ref was force pushed right after PR was merged. Not performing CI fast path."
+
+  # obtain the common ancestor commit where the PR diverged
+  PR_MERGE_BASE="$(git merge-base "ref/tmp/$PR_BASE_COMMIT" "$HEAD_COMMIT")" || infoexit "failed to obtain PR merge base, not performing CI fast path"
+
+  # obtain list of all files changed since this PR diverged
+  read -d'\n' -ra PR_BASE_TO_HEAD_CHANGED_FILES < <(
+    git diff-tree --name-only -r "$PR_MERGE_BASE..$OLD_COMMIT"
+  ) || :
+
+  # obtain list of all packages changed by this PR
+  read -d'\n' -ra PR_CHANGED_PACKAGES < <(
+    git diff-tree --name-only -r "$OLD_COMMIT..$HEAD_COMMIT" \
+      | grep -E "^($DIRS_REGEX)/[^/]+/" \
+      | sed -E "s#^(($DIRS_REGEX)/[^/]+)/.*#\1#" \
+      | sort -u
+  ) || :
+  echo "Packages changed by this PR: ${PR_CHANGED_PACKAGES[*]:-none}"
+
+  # obtain list of all buildsystem files changed since this PR diverged
+  read -d'\n' -ra PR_BASE_TO_HEAD_CHANGED_BUILDSYSTEM_FILES < <(
+    grep -e "^scripts/" -e "^ndk-patches/" -e "^build-package.sh$" <<< "$PR_BASE_TO_HEAD_CHANGED_FILES"
+  ) || :
+  echo "Buildsystem files changed since PR divergence: ${PR_BASE_TO_HEAD_CHANGED_BUILDSYSTEM_FILES[*]:-none}"
+
+  # obtain list of all packages changes since this PR diverged
+  read -d'\n' -ra PR_BASE_TO_HEAD_CHANGED_PACKAGES < <(
+    echo "$PR_BASE_TO_HEAD_CHANGED_FILES" \
+      | grep -E "^($DIRS_REGEX)/[^/]+/" \
+      | sed -E "s#^(($DIRS_REGEX)/[^/]+)/.*#\1#" \
+      | sort -u
+  ) || :
+  echo "Packages updated since PR divergence: ${PR_BASE_TO_HEAD_CHANGED_PACKAGES[*]:-none}"
+
+  # obtain the set of all dependencies of packages changed by this PR
+  read -d'\n' -ra PR_CHANGED_PACKAGES_DEPS < <(
+    for dep in "${PR_CHANGED_PACKAGES[@]:-}"; do
+      ./scripts/buildorder.py "$dep" "${TERMUX_PACKAGE_DIRECTORIES[@]}" | awk '{print $NF}'
+    done | sort -u
+  ) || :
+  echo "Dependencies changed by this PR: ${PR_CHANGED_PACKAGES_DEPS[*]:-none}"
+
+  # obtain the set of all build dependencies changed since this PR diverged
+  read -d'\n' -ra PR_BASE_TO_HEAD_CHANGED_DEPS < <(
+    grep -Fx \
+      -f <(echo "${PR_BASE_TO_HEAD_CHANGED_PACKAGES[@]}") \
+      - <<< "${PR_CHANGED_PACKAGES_DEPS[@]}"
+  ) || :
+  echo "Dependencies of these packages changed since PR divergence: ${PR_BASE_TO_HEAD_CHANGED_DEPS[*]:-none}"
+
+
+  if (( ${#PR_BASE_TO_HEAD_CHANGED_BUILDSYSTEM_FILES[*]:-} + ${#PR_BASE_TO_HEAD_CHANGED_DEPS[*]:-} + PR_CI_REUSE )); then
+
+    # The same commit can be used in more than one PR or even push
+    WORKFLOW_PR_QUERY="
+    query {
+      repository(owner: \"termux\", name: \"termux-packages\") {
+        object(oid: \"$PR_HEAD_COMMIT\") { ...workflowRun }
+      }
+    }
+
+    fragment workflowRun on Commit {
+      checkSuites(first: 32) { nodes { workflowRun { event file { path } databaseId } conclusion status } }
+    }"
+
+    RESPONSE="$(graphql_request "$WORKFLOW_PR_QUERY" || infoexit "Failed to perform GraphQL request, not performing CI fast path")"
+
+    # Obtain the most recent, related, successful `packages.yml` workflow run.
+    WORKFLOW_ID="$(
+      jq -r '[.data.repository.object?.checkSuites?.nodes[]?
+        | select(
+        .workflowRun.event == "pull_request"
+        and .workflowRun.file.path == ".github/workflows/packages.yml"
+        and .conclusion == "SUCCESS"
+        and .status == "COMPLETED")
+        | .workflowRun.databaseId][0]' <<< "$RESPONSE" || :
+    )"
+    if [[ -n "${WORKFLOW_ID}" ]]; then
+      echo "We can safely reuse CI artifacts from https://github.com/termux/termux-packages/actions/runs/${WORKFLOW_ID}"
+      echo "CI artifact URL is $(ci_artifact_url "${WORKFLOW_ID}" || infoexit "Failed to get CI artifact URL")"
+    else
+      echo "We can safely reuse CI artifacts, but did not find any matching CI run."
+    fi
+  else
+    echo "It is NOT safe to reuse PR build artifact"
+  fi
+}
+
+[[ "${GITHUB_EVENT_NAME:-}" == "pull_request" ]] && {
+  # In the case of pull_requests we can reuse build artifacts of recent workflow runs
+  # if user added more commits to existing PR after workflow finished running.
+  (( ${#COMMITS[*]} > 128 )) && infoexit "Pull request has more than 128 commits, not attempting to reuse CI artifacts."
+
+  # We intentionally do not check if workflow run is related to this specific PR
+  # to allow CI reuse artifacts of other PRs in the case if current PR diverged from another PR branch.
+  WORKFLOW_COMMITS_QUERY="
+  query {
+    repository(owner: \"termux\", name: \"termux-packages\") {
+    $(
+      for commit in "${COMMITS[@]}"; do
+        # Add a query for this commit with the shorthash as the label
+        echo "_${commit::7}: object(oid: \"${commit}\") { ...workflowRun }"
+      done
+    )
+    }
+  }
+
+  fragment workflowRun on Commit {
+    checkSuites(first: 32) { nodes { workflowRun { event file { path } databaseId } conclusion status } }
+  }"
+
+  RESPONSE="$(graphql_request "$WORKFLOW_COMMITS_QUERY" || infoexit "Failed to perform GraphQL request, not performing CI fast path")"
+
+  # git rev-list prints commits in chronologically descending order, so we can check them as is.
+  for commit in "${COMMITS[@]}"; do
+    # Get the most recent successful `packages.yml` workflow run for this commit if any
+    WORKFLOW_ID="$(
+      jq -r '[.data.repository["_'"${commit::7}"'"].checkSuites?.nodes[]?
+        | select(
+          .workflowRun.event == "pull_request" and
+          .workflowRun.file.path == ".github/workflows/packages.yml" and
+          .conclusion == "SUCCESS" and
+          .status == "COMPLETED"
+        ) | .workflowRun.databaseId][0] // empty' <<< "$RESPONSE"
+    )"
+    # No need to go on if we found a match.
+    [[ -z "${WORKFLOW_ID:-}" ]] || break
+  done
+  if [[ -n "${WORKFLOW_ID}" ]]; then
+    echo "We can safely reuse CI artifacts from https://github.com/termux/termux-packages/actions/runs/${WORKFLOW_ID}"
+    echo "CI artifact URL is $(ci_artifact_url "${WORKFLOW_ID}" || infoexit "Failed to get CI artifact URL")"
+  else
+    echo "We can not reuse CI artifacts since no relevant CI runs were found"
+  fi
+}


Reviews:
user: login: TomJo2000
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-14T23:52:21Z
user: login: twaik
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-15T01:13:01Z
user: login: twaik
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-15T01:13:53Z
user: login: twaik
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-15T01:15:20Z
user: login: twaik
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-15T01:46:53Z
user: login: robertkirkman
body: Since it does not do any actions yet, this is useful to collect information and then if more or different information is required, it can be updated as many times as necessary before the feature is completely activated later.
state: APPROVED
author_association: MEMBER
submitted_at: 2025-04-15T01:52:40Z
user: login: TomJo2000
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-15T23:13:31Z
user: login: TomJo2000
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-15T23:16:30Z
user: login: TomJo2000
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-15T23:18:25Z
user: login: twaik
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-15T23:26:33Z
user: login: TomJo2000
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-15T23:30:31Z
user: login: twaik
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-15T23:33:30Z

Review comments:
diff_hunk: @@ -0,0 +1,125 @@
+#!/bin/bash
+set -euo pipefail
+
+# Check if we can perform CI fast path
+# Here we make sure all commits come from single PR, make sure nor buildsystem nor dependencies were not changed
+# and if so we are reusing PR check artifacts and uploading them to apt repo to make save some CI time
+
+
+# Make sure we are running in a push triggered by PR merge.
+[[ "${GITHUB_EVENT_NAME:-}" == "push" ]] || exit 0
+
+error_exit() {
+  echo "$@"
+  exit 0
+}
+
+# OLD_COMMIT and HEAD_COMMIT are required
+[ -n "${OLD_COMMIT:-}" ] || error_exit "OLD_COMMIT is unset, not performing CI fast path"
+[ -n "${HEAD_COMMIT:-}" ] || error_exit "HEAD_COMMIT is unset, not performing CI fast path"
+[ -n "${GITHUB_TOKEN:-}" ] || error_exit "GITHUB_TOKEN is unset, not performing CI fast path"
path: scripts/utils/termux_reuse_pr_build_artifacts.sh
user: login: TomJo2000
body: Please be consistent about which version of `test` you are using.
Since this is a Bash script `[[` bash test `]]` would be preferable.

On a style note, it'd be nice to line up the conditionals here.
```suggestion
# OLD_COMMIT and HEAD_COMMIT are required
[[ -n "${OLD_COMMIT:-}"   ]] || error_exit "OLD_COMMIT is unset, not performing CI fast path"
[[ -n "${HEAD_COMMIT:-}"  ]] || error_exit "HEAD_COMMIT is unset, not performing CI fast path"
[[ -n "${GITHUB_TOKEN:-}" ]] || error_exit "GITHUB_TOKEN is unset, not performing CI fast path"
```
created_at: 2025-04-14T23:16:25Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,125 @@
+#!/bin/bash
+set -euo pipefail
+
+# Check if we can perform CI fast path
+# Here we make sure all commits come from single PR, make sure nor buildsystem nor dependencies were not changed
+# and if so we are reusing PR check artifacts and uploading them to apt repo to make save some CI time
+
+
+# Make sure we are running in a push triggered by PR merge.
+[[ "${GITHUB_EVENT_NAME:-}" == "push" ]] || exit 0
+
+error_exit() {
+  echo "$@"
+  exit 0
+}
+
+# OLD_COMMIT and HEAD_COMMIT are required
+[ -n "${OLD_COMMIT:-}" ] || error_exit "OLD_COMMIT is unset, not performing CI fast path"
+[ -n "${HEAD_COMMIT:-}" ] || error_exit "HEAD_COMMIT is unset, not performing CI fast path"
+[ -n "${GITHUB_TOKEN:-}" ] || error_exit "GITHUB_TOKEN is unset, not performing CI fast path"
+
+TERMUX_PACKAGE_DIRECTORIES="$(jq --raw-output 'del(.pkg_format) | keys | .[]' repo.json)"
+
+# We should obtain data about all commits in this push and make sure they all came from the same PR if any.
+# No hard tabs or newlines allowed inside a graphql request.
+GRAPHQL_QUERY=$(tr '\t\n' '  ' <<< "
+query {
+  repository(owner: \\\"termux\\\", name: \\\"termux-packages\\\") {
+  $(
+    for i in $(git rev-list --no-merges "$OLD_COMMIT..$HEAD_COMMIT" || true); do
path: scripts/utils/termux_reuse_pr_build_artifacts.sh
user: login: TomJo2000
body: Can't say I've ever seen that `true` used as a `""`, but yeah that'll work.

I'd like the loop variable to be a bit more descriptive.
`commit` would be a good name since we are looping over commits.
created_at: 2025-04-14T23:18:27Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,125 @@
+#!/bin/bash
+set -euo pipefail
+
+# Check if we can perform CI fast path
+# Here we make sure all commits come from single PR, make sure nor buildsystem nor dependencies were not changed
+# and if so we are reusing PR check artifacts and uploading them to apt repo to make save some CI time
+
+
+# Make sure we are running in a push triggered by PR merge.
+[[ "${GITHUB_EVENT_NAME:-}" == "push" ]] || exit 0
+
+error_exit() {
+  echo "$@"
+  exit 0
+}
+
+# OLD_COMMIT and HEAD_COMMIT are required
+[ -n "${OLD_COMMIT:-}" ] || error_exit "OLD_COMMIT is unset, not performing CI fast path"
+[ -n "${HEAD_COMMIT:-}" ] || error_exit "HEAD_COMMIT is unset, not performing CI fast path"
+[ -n "${GITHUB_TOKEN:-}" ] || error_exit "GITHUB_TOKEN is unset, not performing CI fast path"
+
+TERMUX_PACKAGE_DIRECTORIES="$(jq --raw-output 'del(.pkg_format) | keys | .[]' repo.json)"
+
+# We should obtain data about all commits in this push and make sure they all came from the same PR if any.
+# No hard tabs or newlines allowed inside a graphql request.
+GRAPHQL_QUERY=$(tr '\t\n' '  ' <<< "
+query {
+  repository(owner: \\\"termux\\\", name: \\\"termux-packages\\\") {
+  $(
+    for i in $(git rev-list --no-merges "$OLD_COMMIT..$HEAD_COMMIT" || true); do
+      echo "_${i:0:7}: object(oid: \\\"${i}\\\") { ...commitWithPR }"
path: scripts/utils/termux_reuse_pr_build_artifacts.sh
user: login: TomJo2000
body: A comment could help here.
And as mentioned above I think using `${commit}` as the variable name makes it easier to parse this when reading.
```suggestion
      # Add a query for this commit with the shorthash as the label
      echo "_${commit::7}: object(oid: \\\"${commit}\\\") { ...commitWithPR }"
```
created_at: 2025-04-14T23:20:32Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,125 @@
+#!/bin/bash
+set -euo pipefail
+
+# Check if we can perform CI fast path
+# Here we make sure all commits come from single PR, make sure nor buildsystem nor dependencies were not changed
+# and if so we are reusing PR check artifacts and uploading them to apt repo to make save some CI time
+
+
+# Make sure we are running in a push triggered by PR merge.
+[[ "${GITHUB_EVENT_NAME:-}" == "push" ]] || exit 0
+
+error_exit() {
+  echo "$@"
+  exit 0
+}
+
+# OLD_COMMIT and HEAD_COMMIT are required
+[ -n "${OLD_COMMIT:-}" ] || error_exit "OLD_COMMIT is unset, not performing CI fast path"
+[ -n "${HEAD_COMMIT:-}" ] || error_exit "HEAD_COMMIT is unset, not performing CI fast path"
+[ -n "${GITHUB_TOKEN:-}" ] || error_exit "GITHUB_TOKEN is unset, not performing CI fast path"
+
+TERMUX_PACKAGE_DIRECTORIES="$(jq --raw-output 'del(.pkg_format) | keys | .[]' repo.json)"
+
+# We should obtain data about all commits in this push and make sure they all came from the same PR if any.
+# No hard tabs or newlines allowed inside a graphql request.
+GRAPHQL_QUERY=$(tr '\t\n' '  ' <<< "
+query {
+  repository(owner: \\\"termux\\\", name: \\\"termux-packages\\\") {
+  $(
+    for i in $(git rev-list --no-merges "$OLD_COMMIT..$HEAD_COMMIT" || true); do
+      echo "_${i:0:7}: object(oid: \\\"${i}\\\") { ...commitWithPR }"
+    done
+  )
+  }
+}
+
+fragment commitWithPR on Commit {
+  associatedPullRequests(first: 1) { nodes { baseRefOid headRefOid } edges { node { number } } }
+}")
+
+# Send graphql to github and obtain response
path: scripts/utils/termux_reuse_pr_build_artifacts.sh
user: login: TomJo2000
body: GitHub's syntax parser gets really unhappy about the script at this point.
Probably due to the embedded GraphQL query above.
created_at: 2025-04-14T23:21:59Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,125 @@
+#!/bin/bash
+set -euo pipefail
+
+# Check if we can perform CI fast path
+# Here we make sure all commits come from single PR, make sure nor buildsystem nor dependencies were not changed
+# and if so we are reusing PR check artifacts and uploading them to apt repo to make save some CI time
+
+
+# Make sure we are running in a push triggered by PR merge.
+[[ "${GITHUB_EVENT_NAME:-}" == "push" ]] || exit 0
+
+error_exit() {
+  echo "$@"
+  exit 0
+}
path: scripts/utils/termux_reuse_pr_build_artifacts.sh
user: login: TomJo2000
body: `error_exit` is very close to `termux_error_exit` which we do not wanna create confusion with.

Consider either using a more generic "`die`" function, e.g.
I think you can probably also bake in the `, not performing CI fast path` into the function since you added it to every call of this error function.
```bash
die() {
    echo "$*, not performing CI fast path"
    exit 0
    # I think this should be an exit 1
    # that way we can handle fast path failure in the workflow with a 
    # `./scripts/utils/termux_reuse_pr_build_artifacts.sh || echo "Nope."`
} >&2
```
Also make sure it prints to stderr.

created_at: 2025-04-14T23:31:02Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,125 @@
+#!/bin/bash
+set -euo pipefail
+
+# Check if we can perform CI fast path
+# Here we make sure all commits come from single PR, make sure nor buildsystem nor dependencies were not changed
+# and if so we are reusing PR check artifacts and uploading them to apt repo to make save some CI time
+
+
+# Make sure we are running in a push triggered by PR merge.
+[[ "${GITHUB_EVENT_NAME:-}" == "push" ]] || exit 0
+
+error_exit() {
+  echo "$@"
+  exit 0
+}
+
+# OLD_COMMIT and HEAD_COMMIT are required
+[ -n "${OLD_COMMIT:-}" ] || error_exit "OLD_COMMIT is unset, not performing CI fast path"
+[ -n "${HEAD_COMMIT:-}" ] || error_exit "HEAD_COMMIT is unset, not performing CI fast path"
+[ -n "${GITHUB_TOKEN:-}" ] || error_exit "GITHUB_TOKEN is unset, not performing CI fast path"
+
+TERMUX_PACKAGE_DIRECTORIES="$(jq --raw-output 'del(.pkg_format) | keys | .[]' repo.json)"
+
+# We should obtain data about all commits in this push and make sure they all came from the same PR if any.
+# No hard tabs or newlines allowed inside a graphql request.
+GRAPHQL_QUERY=$(tr '\t\n' '  ' <<< "
+query {
+  repository(owner: \\\"termux\\\", name: \\\"termux-packages\\\") {
+  $(
+    for i in $(git rev-list --no-merges "$OLD_COMMIT..$HEAD_COMMIT" || true); do
+      echo "_${i:0:7}: object(oid: \\\"${i}\\\") { ...commitWithPR }"
+    done
+  )
+  }
+}
+
+fragment commitWithPR on Commit {
+  associatedPullRequests(first: 1) { nodes { baseRefOid headRefOid } edges { node { number } } }
+}")
+
+# Send graphql to github and obtain response
+RESPONSE="$(
+curl -s \
+  -H "Authorization: bearer $GITHUB_TOKEN" \
+  -H "Content-Type: application/json" \
path: scripts/utils/termux_reuse_pr_build_artifacts.sh
user: login: TomJo2000
body: This should be the return type specified in GitHub's documentation and used by our other API call sites.
https://github.com/search?q=repo%3Atermux%2Ftermux-packages%20Accept%3A&type=code
```suggestion
  -H "Accept: application/vnd.github.v3+json" \
```
created_at: 2025-04-14T23:39:59Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,125 @@
+#!/bin/bash
+set -euo pipefail
+
+# Check if we can perform CI fast path
+# Here we make sure all commits come from single PR, make sure nor buildsystem nor dependencies were not changed
+# and if so we are reusing PR check artifacts and uploading them to apt repo to make save some CI time
+
+
+# Make sure we are running in a push triggered by PR merge.
+[[ "${GITHUB_EVENT_NAME:-}" == "push" ]] || exit 0
+
+error_exit() {
+  echo "$@"
+  exit 0
+}
+
+# OLD_COMMIT and HEAD_COMMIT are required
+[ -n "${OLD_COMMIT:-}" ] || error_exit "OLD_COMMIT is unset, not performing CI fast path"
+[ -n "${HEAD_COMMIT:-}" ] || error_exit "HEAD_COMMIT is unset, not performing CI fast path"
+[ -n "${GITHUB_TOKEN:-}" ] || error_exit "GITHUB_TOKEN is unset, not performing CI fast path"
+
+TERMUX_PACKAGE_DIRECTORIES="$(jq --raw-output 'del(.pkg_format) | keys | .[]' repo.json)"
+
+# We should obtain data about all commits in this push and make sure they all came from the same PR if any.
+# No hard tabs or newlines allowed inside a graphql request.
+GRAPHQL_QUERY=$(tr '\t\n' '  ' <<< "
+query {
+  repository(owner: \\\"termux\\\", name: \\\"termux-packages\\\") {
+  $(
+    for i in $(git rev-list --no-merges "$OLD_COMMIT..$HEAD_COMMIT" || true); do
+      echo "_${i:0:7}: object(oid: \\\"${i}\\\") { ...commitWithPR }"
+    done
+  )
+  }
+}
+
+fragment commitWithPR on Commit {
+  associatedPullRequests(first: 1) { nodes { baseRefOid headRefOid } edges { node { number } } }
+}")
+
+# Send graphql to github and obtain response
+RESPONSE="$(
+curl -s \
+  -H "Authorization: bearer $GITHUB_TOKEN" \
+  -H "Content-Type: application/json" \
+  -X POST -d "{ \"query\": \"$GRAPHQL_QUERY\"}" \
path: scripts/utils/termux_reuse_pr_build_artifacts.sh
user: login: TomJo2000
body: Please use the long option names for `-d` and `-s` to make it more unambiguous what they do.
```suggestion
  -X POST \
  --data "{ \"query\": \"$GRAPHQL_QUERY\"}" \
```
created_at: 2025-04-14T23:41:09Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,125 @@
+#!/bin/bash
+set -euo pipefail
+
+# Check if we can perform CI fast path
+# Here we make sure all commits come from single PR, make sure nor buildsystem nor dependencies were not changed
+# and if so we are reusing PR check artifacts and uploading them to apt repo to make save some CI time
+
+
+# Make sure we are running in a push triggered by PR merge.
+[[ "${GITHUB_EVENT_NAME:-}" == "push" ]] || exit 0
+
+error_exit() {
+  echo "$@"
+  exit 0
+}
+
+# OLD_COMMIT and HEAD_COMMIT are required
+[ -n "${OLD_COMMIT:-}" ] || error_exit "OLD_COMMIT is unset, not performing CI fast path"
+[ -n "${HEAD_COMMIT:-}" ] || error_exit "HEAD_COMMIT is unset, not performing CI fast path"
+[ -n "${GITHUB_TOKEN:-}" ] || error_exit "GITHUB_TOKEN is unset, not performing CI fast path"
+
+TERMUX_PACKAGE_DIRECTORIES="$(jq --raw-output 'del(.pkg_format) | keys | .[]' repo.json)"
+
+# We should obtain data about all commits in this push and make sure they all came from the same PR if any.
+# No hard tabs or newlines allowed inside a graphql request.
+GRAPHQL_QUERY=$(tr '\t\n' '  ' <<< "
+query {
+  repository(owner: \\\"termux\\\", name: \\\"termux-packages\\\") {
+  $(
+    for i in $(git rev-list --no-merges "$OLD_COMMIT..$HEAD_COMMIT" || true); do
+      echo "_${i:0:7}: object(oid: \\\"${i}\\\") { ...commitWithPR }"
+    done
+  )
+  }
+}
+
+fragment commitWithPR on Commit {
+  associatedPullRequests(first: 1) { nodes { baseRefOid headRefOid } edges { node { number } } }
+}")
+
+# Send graphql to github and obtain response
+RESPONSE="$(
+curl -s \
+  -H "Authorization: bearer $GITHUB_TOKEN" \
+  -H "Content-Type: application/json" \
+  -X POST -d "{ \"query\": \"$GRAPHQL_QUERY\"}" \
+  https://api.github.com/graphql
+)" || error_exit "Failed to perform GraphQL request, not performing CI fast path"
+
+# Ensure response is valid and obtain all associated PR numbers if any and make sure response is valid
+PRS="$(jq '.data.repository | to_entries[] | .value.associatedPullRequests.edges
+         | (.[] // []) | (.node? // []) | (.number? // 0)' <<< "$RESPONSE")" || \
+         error_exit "GraphQL response is invalid, not performing CI fast path"
+
+# Check if all commits come from the same PR and return if not
+[ "$(sort -u <<< "$PRS" | wc -l)" -eq 0 ] && error_exit "push does not contain commits from PR, not performing CI fast path"
+[ "$(sort -u <<< "$PRS" | wc -l)" -eq 1 ] || error_exit "push contains commits from more than one pull request, not performing CI fast path"
+
+PR_BASE_COMMIT="$(jq -r '.data.repository | to_entries[0].value.associatedPullRequests.nodes[0].baseRefOid' <<< "$RESPONSE")" || exit 0
+PR_HEAD_COMMIT="$(jq -r '.data.repository | to_entries[0].value.associatedPullRequests.nodes[0].headRefOid' <<< "$RESPONSE")" || exit 0
+[ -n "${PR_BASE_COMMIT:-}" ] || error_exit "failed to read associated PR base commit, not performing CI fast path"
+[ -n "${PR_HEAD_COMMIT:-}" ] || error_exit "failed to read associated PR head commit, not performing CI fast path"
+
+DIRS_REGEX="$(echo "$TERMUX_PACKAGE_DIRECTORIES" | paste -sd'|' -)" || exit 0
+
+# fetch PR commit tree
+git fetch origin $PR_BASE_COMMIT:ref/tmp/$PR_BASE_COMMIT || error_exit "failed to fetch PR base tree, not performing CI fast path"
+git fetch origin $PR_HEAD_COMMIT:ref/tmp/$PR_HEAD_COMMIT || error_exit "failed to fetch PR head tree, not performing CI fast path"
+
+# make sure nobody force-pushed after CI was invoked and before we fetched remote tree and requested GraphQL data
+# checking if changed files in PR are same as in this push should be enough.
+
+diff -q \
+    <(git diff $PR_BASE_COMMIT $PR_HEAD_COMMIT | sed -n -E '/^diff --git a\// { p; n; /^index /!p; b } ; p') \
+    <(git diff $OLD_COMMIT $HEAD_COMMIT | sed -n -E '/^diff --git a\// { p; n; /^index /!p; b } ; p') >/dev/null 2>&1 \
+|| error_exit "PR head does not match pushed commit changes, probably PR ref was force pushed right after PR was merged. Not performing CI fast path."
+
+
+# obtain merge-base
path: scripts/utils/termux_reuse_pr_build_artifacts.sh
user: login: TomJo2000
body: This comment could be bit more descriptive.
What *is* the merge-base?
created_at: 2025-04-14T23:44:28Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,125 @@
+#!/bin/bash
+set -euo pipefail
+
+# Check if we can perform CI fast path
+# Here we make sure all commits come from single PR, make sure nor buildsystem nor dependencies were not changed
+# and if so we are reusing PR check artifacts and uploading them to apt repo to make save some CI time
+
+
+# Make sure we are running in a push triggered by PR merge.
+[[ "${GITHUB_EVENT_NAME:-}" == "push" ]] || exit 0
+
+error_exit() {
+  echo "$@"
+  exit 0
+}
+
+# OLD_COMMIT and HEAD_COMMIT are required
+[ -n "${OLD_COMMIT:-}" ] || error_exit "OLD_COMMIT is unset, not performing CI fast path"
+[ -n "${HEAD_COMMIT:-}" ] || error_exit "HEAD_COMMIT is unset, not performing CI fast path"
+[ -n "${GITHUB_TOKEN:-}" ] || error_exit "GITHUB_TOKEN is unset, not performing CI fast path"
+
+TERMUX_PACKAGE_DIRECTORIES="$(jq --raw-output 'del(.pkg_format) | keys | .[]' repo.json)"
+
+# We should obtain data about all commits in this push and make sure they all came from the same PR if any.
+# No hard tabs or newlines allowed inside a graphql request.
+GRAPHQL_QUERY=$(tr '\t\n' '  ' <<< "
+query {
+  repository(owner: \\\"termux\\\", name: \\\"termux-packages\\\") {
+  $(
+    for i in $(git rev-list --no-merges "$OLD_COMMIT..$HEAD_COMMIT" || true); do
+      echo "_${i:0:7}: object(oid: \\\"${i}\\\") { ...commitWithPR }"
+    done
+  )
+  }
+}
+
+fragment commitWithPR on Commit {
+  associatedPullRequests(first: 1) { nodes { baseRefOid headRefOid } edges { node { number } } }
+}")
+
+# Send graphql to github and obtain response
+RESPONSE="$(
+curl -s \
+  -H "Authorization: bearer $GITHUB_TOKEN" \
+  -H "Content-Type: application/json" \
+  -X POST -d "{ \"query\": \"$GRAPHQL_QUERY\"}" \
+  https://api.github.com/graphql
+)" || error_exit "Failed to perform GraphQL request, not performing CI fast path"
+
+# Ensure response is valid and obtain all associated PR numbers if any and make sure response is valid
+PRS="$(jq '.data.repository | to_entries[] | .value.associatedPullRequests.edges
+         | (.[] // []) | (.node? // []) | (.number? // 0)' <<< "$RESPONSE")" || \
+         error_exit "GraphQL response is invalid, not performing CI fast path"
+
+# Check if all commits come from the same PR and return if not
+[ "$(sort -u <<< "$PRS" | wc -l)" -eq 0 ] && error_exit "push does not contain commits from PR, not performing CI fast path"
+[ "$(sort -u <<< "$PRS" | wc -l)" -eq 1 ] || error_exit "push contains commits from more than one pull request, not performing CI fast path"
+
+PR_BASE_COMMIT="$(jq -r '.data.repository | to_entries[0].value.associatedPullRequests.nodes[0].baseRefOid' <<< "$RESPONSE")" || exit 0
+PR_HEAD_COMMIT="$(jq -r '.data.repository | to_entries[0].value.associatedPullRequests.nodes[0].headRefOid' <<< "$RESPONSE")" || exit 0
+[ -n "${PR_BASE_COMMIT:-}" ] || error_exit "failed to read associated PR base commit, not performing CI fast path"
+[ -n "${PR_HEAD_COMMIT:-}" ] || error_exit "failed to read associated PR head commit, not performing CI fast path"
+
+DIRS_REGEX="$(echo "$TERMUX_PACKAGE_DIRECTORIES" | paste -sd'|' -)" || exit 0
+
+# fetch PR commit tree
+git fetch origin $PR_BASE_COMMIT:ref/tmp/$PR_BASE_COMMIT || error_exit "failed to fetch PR base tree, not performing CI fast path"
+git fetch origin $PR_HEAD_COMMIT:ref/tmp/$PR_HEAD_COMMIT || error_exit "failed to fetch PR head tree, not performing CI fast path"
+
+# make sure nobody force-pushed after CI was invoked and before we fetched remote tree and requested GraphQL data
+# checking if changed files in PR are same as in this push should be enough.
+
+diff -q \
+    <(git diff $PR_BASE_COMMIT $PR_HEAD_COMMIT | sed -n -E '/^diff --git a\// { p; n; /^index /!p; b } ; p') \
+    <(git diff $OLD_COMMIT $HEAD_COMMIT | sed -n -E '/^diff --git a\// { p; n; /^index /!p; b } ; p') >/dev/null 2>&1 \
+|| error_exit "PR head does not match pushed commit changes, probably PR ref was force pushed right after PR was merged. Not performing CI fast path."
+
+
+# obtain merge-base
+PR_MERGE_BASE="$(git merge-base ref/tmp/$PR_BASE_COMMIT $HEAD_COMMIT)" || error_exit "failed to obtain PR merge base, not performing CI fast path"
+
+# obtain list of all files changed in master branch since this PR diverged
+PR_BASE_TO_HEAD_CHANGED_FILES="$(git diff-tree --name-only -r $PR_MERGE_BASE..$OLD_COMMIT)" || true
+
+# obtain list of all packages changed by this PR
+PR_CHANGED_PACKAGES="$(
+  git diff-tree --name-only -r "$OLD_COMMIT..$HEAD_COMMIT" \
+    | grep -E "^($DIRS_REGEX)/[^/]+/" \
+    | sed -E "s#^(($DIRS_REGEX)/[^/]+)/.*#\1#" \
+    | sort -u
+)" || true
+
+# obtain list of all buildsystem files changed since this PR diverged
+PR_BASE_TO_HEAD_CHANGED_BUILDSYSTEM_FILES="$(
+  grep -e "^scripts/" -e "^ndk-patches/" -e "^build-package.sh$" <<< "$PR_BASE_TO_HEAD_CHANGED_FILES"
+)" || true
+
+# obtain list of all packages changes since this PR diverged
+PR_BASE_TO_HEAD_CHANGED_PACKAGES="$(
+  echo "$PR_BASE_TO_HEAD_CHANGED_FILES" \
+    | grep -E "^($DIRS_REGEX)/[^/]+/" \
+    | sed -E "s#^(($DIRS_REGEX)/[^/]+)/.*#\1#" \
+    | sort -u
+)" || true
+
+# obtain list of all build dependencies of packages changed by this PR
+PR_CHANGED_PACKAGES_DEPS="$(
+  for i in ${PR_CHANGED_PACKAGES:-}; do
+    ./scripts/buildorder.py $i $TERMUX_PACKAGE_DIRECTORIES | awk '{print $NF}'
+  done | sort -u
+)" || true
+
+# get list of all build dependencies changed since this PR diverged
+PR_BASE_TO_HEAD_CHANGED_DEPS="$(grep -Fxf <(echo "$PR_BASE_TO_HEAD_CHANGED_PACKAGES") - <<< "$PR_CHANGED_PACKAGES_DEPS")" || true
+
+echo "Packages changed by this PR:" ${PR_CHANGED_PACKAGES:-none}
+echo "Dependencies of these packages changed since PR divergence: " ${PR_BASE_TO_HEAD_CHANGED_DEPS:-}
+echo "Buildsystem files changed since PR divergence:" ${PR_BASE_TO_HEAD_CHANGED_BUILDSYSTEM_FILES:-none}
+echo "Packages updated since PR divergence:" ${PR_BASE_TO_HEAD_CHANGED_PACKAGES:-none}
+
+if [[ -z "${PR_BASE_TO_HEAD_CHANGED_BUILDSYSTEM_FILES:-}" && -z "${PR_BASE_TO_HEAD_CHANGED_DEPS:-}" ]]; then
path: scripts/utils/termux_reuse_pr_build_artifacts.sh
user: login: TomJo2000
body: Since both need to be null, this is equivalent.
```suggestion
if [[ -z "${PR_BASE_TO_HEAD_CHANGED_BUILDSYSTEM_FILES:-}${PR_BASE_TO_HEAD_CHANGED_DEPS:-}" ]]; then
```
created_at: 2025-04-14T23:46:55Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,125 @@
+#!/bin/bash
+set -euo pipefail
+
+# Check if we can perform CI fast path
+# Here we make sure all commits come from single PR, make sure nor buildsystem nor dependencies were not changed
+# and if so we are reusing PR check artifacts and uploading them to apt repo to make save some CI time
+
+
+# Make sure we are running in a push triggered by PR merge.
+[[ "${GITHUB_EVENT_NAME:-}" == "push" ]] || exit 0
+
+error_exit() {
+  echo "$@"
+  exit 0
+}
+
+# OLD_COMMIT and HEAD_COMMIT are required
+[ -n "${OLD_COMMIT:-}" ] || error_exit "OLD_COMMIT is unset, not performing CI fast path"
+[ -n "${HEAD_COMMIT:-}" ] || error_exit "HEAD_COMMIT is unset, not performing CI fast path"
+[ -n "${GITHUB_TOKEN:-}" ] || error_exit "GITHUB_TOKEN is unset, not performing CI fast path"
+
+TERMUX_PACKAGE_DIRECTORIES="$(jq --raw-output 'del(.pkg_format) | keys | .[]' repo.json)"
+
+# We should obtain data about all commits in this push and make sure they all came from the same PR if any.
+# No hard tabs or newlines allowed inside a graphql request.
+GRAPHQL_QUERY=$(tr '\t\n' '  ' <<< "
+query {
+  repository(owner: \\\"termux\\\", name: \\\"termux-packages\\\") {
+  $(
+    for i in $(git rev-list --no-merges "$OLD_COMMIT..$HEAD_COMMIT" || true); do
+      echo "_${i:0:7}: object(oid: \\\"${i}\\\") { ...commitWithPR }"
+    done
+  )
+  }
+}
+
+fragment commitWithPR on Commit {
+  associatedPullRequests(first: 1) { nodes { baseRefOid headRefOid } edges { node { number } } }
+}")
+
+# Send graphql to github and obtain response
+RESPONSE="$(
+curl -s \
+  -H "Authorization: bearer $GITHUB_TOKEN" \
+  -H "Content-Type: application/json" \
+  -X POST -d "{ \"query\": \"$GRAPHQL_QUERY\"}" \
+  https://api.github.com/graphql
+)" || error_exit "Failed to perform GraphQL request, not performing CI fast path"
+
+# Ensure response is valid and obtain all associated PR numbers if any and make sure response is valid
+PRS="$(jq '.data.repository | to_entries[] | .value.associatedPullRequests.edges
+         | (.[] // []) | (.node? // []) | (.number? // 0)' <<< "$RESPONSE")" || \
+         error_exit "GraphQL response is invalid, not performing CI fast path"
+
+# Check if all commits come from the same PR and return if not
+[ "$(sort -u <<< "$PRS" | wc -l)" -eq 0 ] && error_exit "push does not contain commits from PR, not performing CI fast path"
+[ "$(sort -u <<< "$PRS" | wc -l)" -eq 1 ] || error_exit "push contains commits from more than one pull request, not performing CI fast path"
+
+PR_BASE_COMMIT="$(jq -r '.data.repository | to_entries[0].value.associatedPullRequests.nodes[0].baseRefOid' <<< "$RESPONSE")" || exit 0
+PR_HEAD_COMMIT="$(jq -r '.data.repository | to_entries[0].value.associatedPullRequests.nodes[0].headRefOid' <<< "$RESPONSE")" || exit 0
+[ -n "${PR_BASE_COMMIT:-}" ] || error_exit "failed to read associated PR base commit, not performing CI fast path"
+[ -n "${PR_HEAD_COMMIT:-}" ] || error_exit "failed to read associated PR head commit, not performing CI fast path"
+
+DIRS_REGEX="$(echo "$TERMUX_PACKAGE_DIRECTORIES" | paste -sd'|' -)" || exit 0
+
+# fetch PR commit tree
+git fetch origin $PR_BASE_COMMIT:ref/tmp/$PR_BASE_COMMIT || error_exit "failed to fetch PR base tree, not performing CI fast path"
+git fetch origin $PR_HEAD_COMMIT:ref/tmp/$PR_HEAD_COMMIT || error_exit "failed to fetch PR head tree, not performing CI fast path"
+
+# make sure nobody force-pushed after CI was invoked and before we fetched remote tree and requested GraphQL data
+# checking if changed files in PR are same as in this push should be enough.
+
+diff -q \
+    <(git diff $PR_BASE_COMMIT $PR_HEAD_COMMIT | sed -n -E '/^diff --git a\// { p; n; /^index /!p; b } ; p') \
+    <(git diff $OLD_COMMIT $HEAD_COMMIT | sed -n -E '/^diff --git a\// { p; n; /^index /!p; b } ; p') >/dev/null 2>&1 \
+|| error_exit "PR head does not match pushed commit changes, probably PR ref was force pushed right after PR was merged. Not performing CI fast path."
+
+
+# obtain merge-base
+PR_MERGE_BASE="$(git merge-base ref/tmp/$PR_BASE_COMMIT $HEAD_COMMIT)" || error_exit "failed to obtain PR merge base, not performing CI fast path"
+
+# obtain list of all files changed in master branch since this PR diverged
+PR_BASE_TO_HEAD_CHANGED_FILES="$(git diff-tree --name-only -r $PR_MERGE_BASE..$OLD_COMMIT)" || true
+
+# obtain list of all packages changed by this PR
+PR_CHANGED_PACKAGES="$(
+  git diff-tree --name-only -r "$OLD_COMMIT..$HEAD_COMMIT" \
+    | grep -E "^($DIRS_REGEX)/[^/]+/" \
+    | sed -E "s#^(($DIRS_REGEX)/[^/]+)/.*#\1#" \
+    | sort -u
+)" || true
+
+# obtain list of all buildsystem files changed since this PR diverged
+PR_BASE_TO_HEAD_CHANGED_BUILDSYSTEM_FILES="$(
+  grep -e "^scripts/" -e "^ndk-patches/" -e "^build-package.sh$" <<< "$PR_BASE_TO_HEAD_CHANGED_FILES"
+)" || true
+
+# obtain list of all packages changes since this PR diverged
+PR_BASE_TO_HEAD_CHANGED_PACKAGES="$(
+  echo "$PR_BASE_TO_HEAD_CHANGED_FILES" \
+    | grep -E "^($DIRS_REGEX)/[^/]+/" \
+    | sed -E "s#^(($DIRS_REGEX)/[^/]+)/.*#\1#" \
+    | sort -u
+)" || true
+
+# obtain list of all build dependencies of packages changed by this PR
+PR_CHANGED_PACKAGES_DEPS="$(
+  for i in ${PR_CHANGED_PACKAGES:-}; do
+    ./scripts/buildorder.py $i $TERMUX_PACKAGE_DIRECTORIES | awk '{print $NF}'
+  done | sort -u
+)" || true
path: scripts/utils/termux_reuse_pr_build_artifacts.sh
user: login: TomJo2000
body: This is the last place I can mention it, so I'll put it here.
I stylistically prefer `|| :` over `|| true`.
This obviously doesn't change the semantics though.
created_at: 2025-04-14T23:49:06Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,125 @@
+#!/bin/bash
+set -euo pipefail
+
+# Check if we can perform CI fast path
+# Here we make sure all commits come from single PR, make sure nor buildsystem nor dependencies were not changed
+# and if so we are reusing PR check artifacts and uploading them to apt repo to make save some CI time
+
+
+# Make sure we are running in a push triggered by PR merge.
+[[ "${GITHUB_EVENT_NAME:-}" == "push" ]] || exit 0
+
+error_exit() {
+  echo "$@"
+  exit 0
+}
+
+# OLD_COMMIT and HEAD_COMMIT are required
+[ -n "${OLD_COMMIT:-}" ] || error_exit "OLD_COMMIT is unset, not performing CI fast path"
+[ -n "${HEAD_COMMIT:-}" ] || error_exit "HEAD_COMMIT is unset, not performing CI fast path"
+[ -n "${GITHUB_TOKEN:-}" ] || error_exit "GITHUB_TOKEN is unset, not performing CI fast path"
+
+TERMUX_PACKAGE_DIRECTORIES="$(jq --raw-output 'del(.pkg_format) | keys | .[]' repo.json)"
+
+# We should obtain data about all commits in this push and make sure they all came from the same PR if any.
+# No hard tabs or newlines allowed inside a graphql request.
+GRAPHQL_QUERY=$(tr '\t\n' '  ' <<< "
+query {
+  repository(owner: \\\"termux\\\", name: \\\"termux-packages\\\") {
+  $(
+    for i in $(git rev-list --no-merges "$OLD_COMMIT..$HEAD_COMMIT" || true); do
+      echo "_${i:0:7}: object(oid: \\\"${i}\\\") { ...commitWithPR }"
+    done
+  )
+  }
+}
+
+fragment commitWithPR on Commit {
+  associatedPullRequests(first: 1) { nodes { baseRefOid headRefOid } edges { node { number } } }
+}")
+
+# Send graphql to github and obtain response
+RESPONSE="$(
+curl -s \
+  -H "Authorization: bearer $GITHUB_TOKEN" \
+  -H "Content-Type: application/json" \
+  -X POST -d "{ \"query\": \"$GRAPHQL_QUERY\"}" \
+  https://api.github.com/graphql
+)" || error_exit "Failed to perform GraphQL request, not performing CI fast path"
+
+# Ensure response is valid and obtain all associated PR numbers if any and make sure response is valid
+PRS="$(jq '.data.repository | to_entries[] | .value.associatedPullRequests.edges
+         | (.[] // []) | (.node? // []) | (.number? // 0)' <<< "$RESPONSE")" || \
+         error_exit "GraphQL response is invalid, not performing CI fast path"
+
+# Check if all commits come from the same PR and return if not
+[ "$(sort -u <<< "$PRS" | wc -l)" -eq 0 ] && error_exit "push does not contain commits from PR, not performing CI fast path"
+[ "$(sort -u <<< "$PRS" | wc -l)" -eq 1 ] || error_exit "push contains commits from more than one pull request, not performing CI fast path"
+
+PR_BASE_COMMIT="$(jq -r '.data.repository | to_entries[0].value.associatedPullRequests.nodes[0].baseRefOid' <<< "$RESPONSE")" || exit 0
+PR_HEAD_COMMIT="$(jq -r '.data.repository | to_entries[0].value.associatedPullRequests.nodes[0].headRefOid' <<< "$RESPONSE")" || exit 0
+[ -n "${PR_BASE_COMMIT:-}" ] || error_exit "failed to read associated PR base commit, not performing CI fast path"
+[ -n "${PR_HEAD_COMMIT:-}" ] || error_exit "failed to read associated PR head commit, not performing CI fast path"
+
+DIRS_REGEX="$(echo "$TERMUX_PACKAGE_DIRECTORIES" | paste -sd'|' -)" || exit 0
+
+# fetch PR commit tree
+git fetch origin $PR_BASE_COMMIT:ref/tmp/$PR_BASE_COMMIT || error_exit "failed to fetch PR base tree, not performing CI fast path"
+git fetch origin $PR_HEAD_COMMIT:ref/tmp/$PR_HEAD_COMMIT || error_exit "failed to fetch PR head tree, not performing CI fast path"
+
+# make sure nobody force-pushed after CI was invoked and before we fetched remote tree and requested GraphQL data
+# checking if changed files in PR are same as in this push should be enough.
+
+diff -q \
+    <(git diff $PR_BASE_COMMIT $PR_HEAD_COMMIT | sed -n -E '/^diff --git a\// { p; n; /^index /!p; b } ; p') \
+    <(git diff $OLD_COMMIT $HEAD_COMMIT | sed -n -E '/^diff --git a\// { p; n; /^index /!p; b } ; p') >/dev/null 2>&1 \
path: scripts/utils/termux_reuse_pr_build_artifacts.sh
user: login: TomJo2000
body: This is gonna need some explanation.
Especially the `sed` calls.

Also quote yo vars.

```suggestion
    <(git diff "$PR_BASE_COMMIT" "$PR_HEAD_COMMIT" | sed -n -E '/^diff --git a\// { p; n; /^index /!p; b } ; p') \
    <(git diff "$OLD_COMMIT" "$HEAD_COMMIT" | sed -n -E '/^diff --git a\// { p; n; /^index /!p; b } ; p') >/dev/null 2>&1 \
```
created_at: 2025-04-14T23:50:30Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,125 @@
+#!/bin/bash
+set -euo pipefail
+
+# Check if we can perform CI fast path
+# Here we make sure all commits come from single PR, make sure nor buildsystem nor dependencies were not changed
+# and if so we are reusing PR check artifacts and uploading them to apt repo to make save some CI time
+
+
+# Make sure we are running in a push triggered by PR merge.
+[[ "${GITHUB_EVENT_NAME:-}" == "push" ]] || exit 0
+
+error_exit() {
+  echo "$@"
+  exit 0
+}
+
+# OLD_COMMIT and HEAD_COMMIT are required
+[ -n "${OLD_COMMIT:-}" ] || error_exit "OLD_COMMIT is unset, not performing CI fast path"
+[ -n "${HEAD_COMMIT:-}" ] || error_exit "HEAD_COMMIT is unset, not performing CI fast path"
+[ -n "${GITHUB_TOKEN:-}" ] || error_exit "GITHUB_TOKEN is unset, not performing CI fast path"
+
+TERMUX_PACKAGE_DIRECTORIES="$(jq --raw-output 'del(.pkg_format) | keys | .[]' repo.json)"
+
+# We should obtain data about all commits in this push and make sure they all came from the same PR if any.
+# No hard tabs or newlines allowed inside a graphql request.
+GRAPHQL_QUERY=$(tr '\t\n' '  ' <<< "
+query {
+  repository(owner: \\\"termux\\\", name: \\\"termux-packages\\\") {
+  $(
+    for i in $(git rev-list --no-merges "$OLD_COMMIT..$HEAD_COMMIT" || true); do
+      echo "_${i:0:7}: object(oid: \\\"${i}\\\") { ...commitWithPR }"
+    done
+  )
+  }
+}
+
+fragment commitWithPR on Commit {
+  associatedPullRequests(first: 1) { nodes { baseRefOid headRefOid } edges { node { number } } }
+}")
+
+# Send graphql to github and obtain response
+RESPONSE="$(
+curl -s \
+  -H "Authorization: bearer $GITHUB_TOKEN" \
+  -H "Content-Type: application/json" \
+  -X POST -d "{ \"query\": \"$GRAPHQL_QUERY\"}" \
+  https://api.github.com/graphql
+)" || error_exit "Failed to perform GraphQL request, not performing CI fast path"
+
+# Ensure response is valid and obtain all associated PR numbers if any and make sure response is valid
+PRS="$(jq '.data.repository | to_entries[] | .value.associatedPullRequests.edges
+         | (.[] // []) | (.node? // []) | (.number? // 0)' <<< "$RESPONSE")" || \
+         error_exit "GraphQL response is invalid, not performing CI fast path"
+
+# Check if all commits come from the same PR and return if not
+[ "$(sort -u <<< "$PRS" | wc -l)" -eq 0 ] && error_exit "push does not contain commits from PR, not performing CI fast path"
+[ "$(sort -u <<< "$PRS" | wc -l)" -eq 1 ] || error_exit "push contains commits from more than one pull request, not performing CI fast path"
+
+PR_BASE_COMMIT="$(jq -r '.data.repository | to_entries[0].value.associatedPullRequests.nodes[0].baseRefOid' <<< "$RESPONSE")" || exit 0
+PR_HEAD_COMMIT="$(jq -r '.data.repository | to_entries[0].value.associatedPullRequests.nodes[0].headRefOid' <<< "$RESPONSE")" || exit 0
+[ -n "${PR_BASE_COMMIT:-}" ] || error_exit "failed to read associated PR base commit, not performing CI fast path"
+[ -n "${PR_HEAD_COMMIT:-}" ] || error_exit "failed to read associated PR head commit, not performing CI fast path"
+
+DIRS_REGEX="$(echo "$TERMUX_PACKAGE_DIRECTORIES" | paste -sd'|' -)" || exit 0
+
+# fetch PR commit tree
+git fetch origin $PR_BASE_COMMIT:ref/tmp/$PR_BASE_COMMIT || error_exit "failed to fetch PR base tree, not performing CI fast path"
+git fetch origin $PR_HEAD_COMMIT:ref/tmp/$PR_HEAD_COMMIT || error_exit "failed to fetch PR head tree, not performing CI fast path"
path: scripts/utils/termux_reuse_pr_build_artifacts.sh
user: login: TomJo2000
body: Quote yo vars.
```suggestion
git fetch origin "$PR_BASE_COMMIT:ref/tmp/$PR_BASE_COMMIT" || error_exit "failed to fetch PR base tree, not performing CI fast path"
git fetch origin "$PR_HEAD_COMMIT:ref/tmp/$PR_HEAD_COMMIT" || error_exit "failed to fetch PR head tree, not performing CI fast path"
```
created_at: 2025-04-14T23:51:12Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,125 @@
+#!/bin/bash
+set -euo pipefail
+
+# Check if we can perform CI fast path
+# Here we make sure all commits come from single PR, make sure nor buildsystem nor dependencies were not changed
+# and if so we are reusing PR check artifacts and uploading them to apt repo to make save some CI time
+
+
+# Make sure we are running in a push triggered by PR merge.
+[[ "${GITHUB_EVENT_NAME:-}" == "push" ]] || exit 0
+
+error_exit() {
+  echo "$@"
+  exit 0
+}
+
+# OLD_COMMIT and HEAD_COMMIT are required
+[ -n "${OLD_COMMIT:-}" ] || error_exit "OLD_COMMIT is unset, not performing CI fast path"
+[ -n "${HEAD_COMMIT:-}" ] || error_exit "HEAD_COMMIT is unset, not performing CI fast path"
+[ -n "${GITHUB_TOKEN:-}" ] || error_exit "GITHUB_TOKEN is unset, not performing CI fast path"
+
+TERMUX_PACKAGE_DIRECTORIES="$(jq --raw-output 'del(.pkg_format) | keys | .[]' repo.json)"
+
+# We should obtain data about all commits in this push and make sure they all came from the same PR if any.
+# No hard tabs or newlines allowed inside a graphql request.
+GRAPHQL_QUERY=$(tr '\t\n' '  ' <<< "
+query {
+  repository(owner: \\\"termux\\\", name: \\\"termux-packages\\\") {
+  $(
+    for i in $(git rev-list --no-merges "$OLD_COMMIT..$HEAD_COMMIT" || true); do
path: scripts/utils/termux_reuse_pr_build_artifacts.sh
user: login: twaik
body: > Can't say I've ever seen that true used as a "", but yeah that'll work.

It is only needed to make `set -pipefail` not fail here in any case.
created_at: 2025-04-15T01:13:01Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,125 @@
+#!/bin/bash
+set -euo pipefail
+
+# Check if we can perform CI fast path
+# Here we make sure all commits come from single PR, make sure nor buildsystem nor dependencies were not changed
+# and if so we are reusing PR check artifacts and uploading them to apt repo to make save some CI time
+
+
+# Make sure we are running in a push triggered by PR merge.
+[[ "${GITHUB_EVENT_NAME:-}" == "push" ]] || exit 0
+
+error_exit() {
+  echo "$@"
+  exit 0
+}
+
+# OLD_COMMIT and HEAD_COMMIT are required
+[ -n "${OLD_COMMIT:-}" ] || error_exit "OLD_COMMIT is unset, not performing CI fast path"
+[ -n "${HEAD_COMMIT:-}" ] || error_exit "HEAD_COMMIT is unset, not performing CI fast path"
+[ -n "${GITHUB_TOKEN:-}" ] || error_exit "GITHUB_TOKEN is unset, not performing CI fast path"
+
+TERMUX_PACKAGE_DIRECTORIES="$(jq --raw-output 'del(.pkg_format) | keys | .[]' repo.json)"
+
+# We should obtain data about all commits in this push and make sure they all came from the same PR if any.
+# No hard tabs or newlines allowed inside a graphql request.
+GRAPHQL_QUERY=$(tr '\t\n' '  ' <<< "
+query {
+  repository(owner: \\\"termux\\\", name: \\\"termux-packages\\\") {
+  $(
+    for i in $(git rev-list --no-merges "$OLD_COMMIT..$HEAD_COMMIT" || true); do
+      echo "_${i:0:7}: object(oid: \\\"${i}\\\") { ...commitWithPR }"
+    done
+  )
+  }
+}
+
+fragment commitWithPR on Commit {
+  associatedPullRequests(first: 1) { nodes { baseRefOid headRefOid } edges { node { number } } }
+}")
+
+# Send graphql to github and obtain response
path: scripts/utils/termux_reuse_pr_build_artifacts.sh
user: login: twaik
body: You mean bash syntax parser or graphql explorer?
created_at: 2025-04-15T01:13:53Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,125 @@
+#!/bin/bash
+set -euo pipefail
+
+# Check if we can perform CI fast path
+# Here we make sure all commits come from single PR, make sure nor buildsystem nor dependencies were not changed
+# and if so we are reusing PR check artifacts and uploading them to apt repo to make save some CI time
+
+
+# Make sure we are running in a push triggered by PR merge.
+[[ "${GITHUB_EVENT_NAME:-}" == "push" ]] || exit 0
+
+error_exit() {
+  echo "$@"
+  exit 0
+}
path: scripts/utils/termux_reuse_pr_build_artifacts.sh
user: login: twaik
body: This function is needed to print some reasoning why we return early here. None of these "errors" is critical to workflow and this script must not cause CI fail. 
I will rename it to `infoexit`.
created_at: 2025-04-15T01:15:20Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,125 @@
+#!/bin/bash
+set -euo pipefail
+
+# Check if we can perform CI fast path
+# Here we make sure all commits come from single PR, make sure nor buildsystem nor dependencies were not changed
+# and if so we are reusing PR check artifacts and uploading them to apt repo to make save some CI time
+
+
+# Make sure we are running in a push triggered by PR merge.
+[[ "${GITHUB_EVENT_NAME:-}" == "push" ]] || exit 0
+
+error_exit() {
+  echo "$@"
+  exit 0
+}
+
+# OLD_COMMIT and HEAD_COMMIT are required
+[ -n "${OLD_COMMIT:-}" ] || error_exit "OLD_COMMIT is unset, not performing CI fast path"
+[ -n "${HEAD_COMMIT:-}" ] || error_exit "HEAD_COMMIT is unset, not performing CI fast path"
+[ -n "${GITHUB_TOKEN:-}" ] || error_exit "GITHUB_TOKEN is unset, not performing CI fast path"
+
+TERMUX_PACKAGE_DIRECTORIES="$(jq --raw-output 'del(.pkg_format) | keys | .[]' repo.json)"
+
+# We should obtain data about all commits in this push and make sure they all came from the same PR if any.
+# No hard tabs or newlines allowed inside a graphql request.
+GRAPHQL_QUERY=$(tr '\t\n' '  ' <<< "
+query {
+  repository(owner: \\\"termux\\\", name: \\\"termux-packages\\\") {
+  $(
+    for i in $(git rev-list --no-merges "$OLD_COMMIT..$HEAD_COMMIT" || true); do
+      echo "_${i:0:7}: object(oid: \\\"${i}\\\") { ...commitWithPR }"
+    done
+  )
+  }
+}
+
+fragment commitWithPR on Commit {
+  associatedPullRequests(first: 1) { nodes { baseRefOid headRefOid } edges { node { number } } }
+}")
+
+# Send graphql to github and obtain response
path: scripts/utils/termux_reuse_pr_build_artifacts.sh
user: login: twaik
body: I am not sure why graphql explorer could be unhappy. I replaced all `\"` with `"` in my query, pasted it to explorer and it worked fine with no warnings.
created_at: 2025-04-15T01:46:53Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,125 @@
+#!/bin/bash
+set -euo pipefail
+
+# Check if we can perform CI fast path
+# Here we make sure all commits come from single PR, make sure nor buildsystem nor dependencies were not changed
+# and if so we are reusing PR check artifacts and uploading them to apt repo to make save some CI time
+
+
+# Make sure we are running in a push triggered by PR merge.
+[[ "${GITHUB_EVENT_NAME:-}" == "push" ]] || exit 0
+
+error_exit() {
+  echo "$@"
+  exit 0
+}
+
+# OLD_COMMIT and HEAD_COMMIT are required
+[ -n "${OLD_COMMIT:-}" ] || error_exit "OLD_COMMIT is unset, not performing CI fast path"
+[ -n "${HEAD_COMMIT:-}" ] || error_exit "HEAD_COMMIT is unset, not performing CI fast path"
+[ -n "${GITHUB_TOKEN:-}" ] || error_exit "GITHUB_TOKEN is unset, not performing CI fast path"
+
+TERMUX_PACKAGE_DIRECTORIES="$(jq --raw-output 'del(.pkg_format) | keys | .[]' repo.json)"
+
+# We should obtain data about all commits in this push and make sure they all came from the same PR if any.
+# No hard tabs or newlines allowed inside a graphql request.
+GRAPHQL_QUERY=$(tr '\t\n' '  ' <<< "
+query {
+  repository(owner: \\\"termux\\\", name: \\\"termux-packages\\\") {
+  $(
+    for i in $(git rev-list --no-merges "$OLD_COMMIT..$HEAD_COMMIT" || true); do
+      echo "_${i:0:7}: object(oid: \\\"${i}\\\") { ...commitWithPR }"
+    done
+  )
+  }
+}
+
+fragment commitWithPR on Commit {
+  associatedPullRequests(first: 1) { nodes { baseRefOid headRefOid } edges { node { number } } }
+}")
+
+# Send graphql to github and obtain response
path: scripts/utils/termux_reuse_pr_build_artifacts.sh
user: login: TomJo2000
body: I meant the syntax highlighting itself.
It thinks all of this below is a string.
Doesn't exactly matter as long as it works.
created_at: 2025-04-15T23:13:31Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,231 @@
+#!/bin/bash
+set -euo pipefail
+
+GITHUB_EVENT_NAME="${1:-}"
+TARGET_ARCH="${2:-}"
+
+infoexit() {
+  echo "$@" >&2
+  exit 0
+}
+
+# Check required variables.
+for var in OLD_COMMIT HEAD_COMMIT GITHUB_TOKEN GITHUB_EVENT_NAME TARGET_ARCH; do
+  [[ -n "${!var:-}" ]] || infoexit "$var is unset, not performing CI fast path"
+done
+
+graphql_request() {
+  local QUERY="${1}" INFOEXIT_TEXT="${2}" RESPONSE
+  # Send graphql to github and obtain response
+  # No hard tabs or newlines allowed inside a graphql request, also we should escape quotes
+  curl -s \
+    -H "Authorization: token ${GITHUB_TOKEN}" \
+    -H "Accept: application/vnd.github.v3+json" \
+    -H 'Content-Type: application/json' \
+    -X POST \
+    --data "{ \"query\": \"$(tr '\t\n' '  ' <<<  "${QUERY//\"/\\\"}\"}")" \
path: scripts/utils/termux_reuse_pr_build_artifacts.sh
user: login: TomJo2000
body: ```suggestion
    --data "{ \"query\": \"$(tr '\t\n' '  ' <<< "${QUERY//\"/\\\"}\"}")" \
```
created_at: 2025-04-15T23:16:30Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,231 @@
+#!/bin/bash
+set -euo pipefail
+
+GITHUB_EVENT_NAME="${1:-}"
+TARGET_ARCH="${2:-}"
+
+infoexit() {
+  echo "$@" >&2
+  exit 0
+}
+
+# Check required variables.
+for var in OLD_COMMIT HEAD_COMMIT GITHUB_TOKEN GITHUB_EVENT_NAME TARGET_ARCH; do
+  [[ -n "${!var:-}" ]] || infoexit "$var is unset, not performing CI fast path"
+done
+
+graphql_request() {
+  local QUERY="${1}" INFOEXIT_TEXT="${2}" RESPONSE
+  # Send graphql to github and obtain response
+  # No hard tabs or newlines allowed inside a graphql request, also we should escape quotes
+  curl -s \
+    -H "Authorization: token ${GITHUB_TOKEN}" \
+    -H "Accept: application/vnd.github.v3+json" \
+    -H 'Content-Type: application/json' \
+    -X POST \
+    --data "{ \"query\": \"$(tr '\t\n' '  ' <<<  "${QUERY//\"/\\\"}\"}")" \
+    "https://api.github.com/graphql" || infoexit "${INFOEXIT_TEXT}"
+}
+
+ci_artifact_url() {
+  curl -s \
+    -H "Authorization: token ${GITHUB_TOKEN}" \
+    -H "Accept: application/vnd.github.v3+json" \
+    "https://api.github.com/repos/termux/termux-packages/actions/runs/${1}/artifacts" \
+    | jq -r '[.artifacts[] | select(.name | startswith("debs-'"${TARGET_ARCH}"'")) | .archive_download_url][0] // empty' \
+      || infoexit "$2"
+}
+
+COMMITS="$(git rev-list --no-merges "$OLD_COMMIT..$HEAD_COMMIT" ||:)"
+[[ -z "$COMMITS" ]] && infoexit "Unable to obtain full commit history. Not performing CI fast path."
+
+if [[ "${GITHUB_EVENT_NAME:-}" == "push" ]]; then
+  # Check if we can perform CI fast path
+  # Here we make sure all commits come from single PR, make sure nor buildsystem nor dependencies were not changed
+  # and if so we are reusing PR check artifacts and uploading them to apt repo to make save some CI time
+
+  TERMUX_PACKAGE_DIRECTORIES="$(jq --raw-output 'del(.pkg_format) | keys | .[]' repo.json)"
+
+  # We should obtain data about all commits in this push and make sure they all came from the same PR if any.
+  RESPONSE="$(graphql_request "
+  query {
+    repository(owner: \"termux\", name: \"termux-packages\") {
+    $(
+      for commit in ${COMMITS}; do
+        # Add a query for this commit with the shorthash as the label
+        echo "_${commit::7}: object(oid: \"${commit}\") { ...commitWithPR }"
+      done
+    )
+    }
+  }
+
+  fragment commitWithPR on Commit {
+    associatedPullRequests(first: 1) { nodes { baseRefOid headRefOid } edges { node { title body number } } }
+  }" "Failed to perform GraphQL request, not performing CI fast path")"
+
+  # Ensure response is valid and obtain all associated PR numbers if any and make sure response is valid
+  PRS="$(jq '.data.repository | to_entries[] | .value.associatedPullRequests.edges
+           | (.[] // []) | (.node? // []) | (.number? // 0)' <<< "$RESPONSE")" || \
+           infoexit "GraphQL response is invalid, not performing CI fast path"
+
+  # Check if all commits come from the same PR and return if not
+  [[ "$(sort -u <<< "$PRS" | wc -l)" -eq 0 ]] && infoexit "push does not have linked PR, not performing CI fast path"
+  [[ "$(sort -u <<< "$PRS" | wc -l)" -eq 1 ]] || infoexit "push contains commits from more than one pull request, not performing CI fast path"
+
+  PR_BASE_COMMIT="$(jq -r '.data.repository | to_entries[0].value.associatedPullRequests.nodes[0].baseRefOid' <<< "$RESPONSE" ||:)"
+  PR_HEAD_COMMIT="$(jq -r '.data.repository | to_entries[0].value.associatedPullRequests.nodes[0].headRefOid' <<< "$RESPONSE" ||:)"
+  PR_TITLE="$(jq -r '.data.repository | to_entries[0].value.associatedPullRequests.edges[0].node.title' <<< "$RESPONSE" ||:)"
+  PR_BODY="$(jq -r '.data.repository | to_entries[0].value.associatedPullRequests.edges[0].node.body' <<< "$RESPONSE" ||:)"
+  [[ -n "${PR_BASE_COMMIT:-}" ]] || infoexit "failed to read associated PR base commit, not performing CI fast path"
+  [[ -n "${PR_HEAD_COMMIT:-}" ]] || infoexit "failed to read associated PR head commit, not performing CI fast path"
+
+  echo "::group::Detected PR #${PRS}: ${PR_TITLE}"
+  echo "${PR_BODY}"
+  echo "::endgroup::"
+
+  PR_CI_REUSE=0
+  # Check if single-commit and squashed PRs have `[ci reuse]` in commit description
+  if [[ "$(wc -l <<< "${COMMITS}" 2>&1)" -eq 1 && "$(git log -1 --pretty=format:"%s%n%b" "${COMMITS}")" == *"[ci reuse]"* ]]; then
+    PR_CI_REUSE=1
+    echo "Commit subject or description contain [ci reuse]"
+  else
+    # Otherwise check linked PR title and body
+    [[ "${PR_TITLE}${PR_BODY}" == *"[ci reuse]"* ]] && PR_CI_REUSE=1 || PR_CI_REUSE=0
+    (( PR_CI_REUSE )) && echo "PR description contains [ci reuse]"
+  fi
+
+  DIRS_REGEX="$(echo "$TERMUX_PACKAGE_DIRECTORIES" | paste -sd'|' -)" || exit 0
+
+  # fetch PR commit tree
+  git fetch origin "$PR_BASE_COMMIT:ref/tmp/$PR_BASE_COMMIT" || infoexit "failed to fetch PR base tree, not performing CI fast path"
+  git fetch origin "$PR_HEAD_COMMIT:ref/tmp/$PR_HEAD_COMMIT" || infoexit "failed to fetch PR head tree, not performing CI fast path"
+
+  # Here we compare changes from PR with changes from push to make sure nobody injected additional changes to PR branch
+  # after CI was invoked and before we fetched data with GraphQL and `git fetch`.
+  # We can not do apply `git diff --no-index` to commit range so we are going to strip indexes manually with sed.
+  diff -q \
+      <(git diff "$PR_BASE_COMMIT" "$PR_HEAD_COMMIT" | sed -n -E '/^diff --git a\// { p; n; /^index /!p; b } ; p') \
+      <(git diff "$OLD_COMMIT" "$HEAD_COMMIT" | sed -n -E '/^diff --git a\// { p; n; /^index /!p; b } ; p') >/dev/null 2>&1 \
+  || infoexit "PR head does not match pushed commit changes, probably PR ref was force pushed right after PR was merged. Not performing CI fast path."
+
+  # obtain the common ancestor commit where the PR diverged from master.
+  PR_MERGE_BASE="$(git merge-base "ref/tmp/$PR_BASE_COMMIT" "$HEAD_COMMIT")" || infoexit "failed to obtain PR merge base, not performing CI fast path"
+
+  # obtain list of all files changed in master branch since this PR diverged
+  PR_BASE_TO_HEAD_CHANGED_FILES="$(git diff-tree --name-only -r "$PR_MERGE_BASE..$OLD_COMMIT")" ||:
+
+  # obtain list of all packages changed by this PR
+  PR_CHANGED_PACKAGES="$(
+    git diff-tree --name-only -r "$OLD_COMMIT..$HEAD_COMMIT" \
+      | grep -E "^($DIRS_REGEX)/[^/]+/" \
+      | sed -E "s#^(($DIRS_REGEX)/[^/]+)/.*#\1#" \
+      | sort -u
+  )" ||:
+
+  # obtain list of all buildsystem files changed since this PR diverged
+  PR_BASE_TO_HEAD_CHANGED_BUILDSYSTEM_FILES="$(
+    grep -e "^scripts/" -e "^ndk-patches/" -e "^build-package.sh$" <<< "$PR_BASE_TO_HEAD_CHANGED_FILES"
+  )" ||:
+
+  # obtain list of all packages changes since this PR diverged
+  PR_BASE_TO_HEAD_CHANGED_PACKAGES="$(
+    echo "$PR_BASE_TO_HEAD_CHANGED_FILES" \
+      | grep -E "^($DIRS_REGEX)/[^/]+/" \
+      | sed -E "s#^(($DIRS_REGEX)/[^/]+)/.*#\1#" \
+      | sort -u
+  )" ||:
+
+  # obtain list of all build dependencies of packages changed by this PR
+  PR_CHANGED_PACKAGES_DEPS="$(
+    for i in ${PR_CHANGED_PACKAGES:-}; do
+      ./scripts/buildorder.py $i $TERMUX_PACKAGE_DIRECTORIES | awk '{print $NF}'
+    done | sort -u
+  )" ||:
+
+  # get list of all build dependencies changed since this PR diverged
+  PR_BASE_TO_HEAD_CHANGED_DEPS="$(grep -Fxf <(echo "$PR_BASE_TO_HEAD_CHANGED_PACKAGES") - <<< "$PR_CHANGED_PACKAGES_DEPS")" ||:
+
+  echo "Packages changed by this PR:" ${PR_CHANGED_PACKAGES:-none}
+  echo "Dependencies of these packages changed since PR divergence: " ${PR_BASE_TO_HEAD_CHANGED_DEPS:-}
+  echo "Buildsystem files changed since PR divergence:" ${PR_BASE_TO_HEAD_CHANGED_BUILDSYSTEM_FILES:-none}
+  echo "Packages updated since PR divergence:" ${PR_BASE_TO_HEAD_CHANGED_PACKAGES:-none}
+
+  if [[ -z "${PR_BASE_TO_HEAD_CHANGED_BUILDSYSTEM_FILES:-}${PR_BASE_TO_HEAD_CHANGED_DEPS:-}" ]] || (( PR_CI_REUSE )); then
+    # Same commit can be used in more than one pull request or even push
+    # No hard tabs or newlines allowed inside a graphql request.
+
+    RESPONSE="$(graphql_request "
+    query {
+      repository(owner: \"termux\", name: \"termux-packages\") {
+        object(oid: \"$PR_HEAD_COMMIT\") {
+          ... on Commit { checkSuites(first: 16) { nodes { workflowRun { event file { path } databaseId } conclusion status } } }
+        }
+      }
+    }" "Failed to perform GraphQL request, not performing CI fast path")"
+    # Obtain the first completed and successfull `packages.yml` workflow run.
+    WORKFLOW_ID="$(jq -r '[.data.repository.object?.checkSuites?.nodes[]?
+                            | select(
+                              .workflowRun.event == "pull_request" and
+                              .workflowRun.file.path == ".github/workflows/packages.yml" and
+                              .conclusion == "SUCCESS" and
+                              .status == "COMPLETED"
+                            ) | .workflowRun.databaseId][0]' <<< "$RESPONSE" ||:)"
+    if [[ -n "${WORKFLOW_ID}" ]]; then
+      echo "We can safely reuse CI artifacts from https://github.com/termux/termux-packages/actions/runs/${WORKFLOW_ID}"
+      echo "CI artifact URL is $(ci_artifact_url "${WORKFLOW_ID}" "Failed to get CI artifact URL")"
+    else
+      echo "We can safely reuse CI artifacts, but did not find any matching CI run."
+    fi
+  else
+    echo "It is NOT safe to reuse PR build artifact"
+  fi
+fi # end of [[ "${GITHUB_EVENT_NAME:-}" == "push" ]]
+
+if [[ "${GITHUB_EVENT_NAME:-}" == "pull_request" ]]; then
+  # In the case of pull_requests we can reuse build artifacts of recent workflow runs
+  # if user added more commits to existing PR after workflow finished running.
+  if [[ "$(wc -l <<< "${COMMITS}" 2>&1)" -ge 128 ]]; then
+    echo "::error title=CI-ARTIFACT-REUSING::Pull request has more than 128 commits, can not reuse CI artifacts."
+    exit 0
+  fi
+
+  # We intentionally do not check if workflow run is related to this specific PR
+  # to allow CI reuse artifacts of other PRs in the case if current PR diverged from other PR branch.
+  RESPONSE="$(graphql_request "
+  query {
+    repository(owner: \"termux\", name: \"termux-packages\") {
+    $(
+      for commit in ${COMMITS}; do
+        # Add a query for this commit with the shorthash as the label
+        echo "_${commit::7}: object(oid: \"${commit}\") { ...workflowRun }"
+      done
+    )
+    }
+  }
+
+  fragment workflowRun on Commit {
+    checkSuites(first: 32) { nodes { workflowRun { event file { path } databaseId } conclusion status } }
+  }" "Failed to perform GraphQL request, not performing CI fast path")"
+
+  WORKFLOW_ID=
+  COMMIT=
+  # git rev-list prints commits in chronologically descending order, so we can check them as is.
+  for commit in ${COMMITS}; do
+    # Print the first completed and successfull `packages.yml` workflow run for this commit if any
+    WORKFLOW_ID="$(jq -r '[.data.repository["_'"${commit::7}"'"].checkSuites?.nodes[]?
+                            | select(
+                              .workflowRun.event == "pull_request" and
+                              .workflowRun.file.path == ".github/workflows/packages.yml" and
+                              .conclusion == "SUCCESS" and
+                              .status == "COMPLETED"
+                            ) | .workflowRun.databaseId][0] // empty' <<< "$RESPONSE")"
path: scripts/utils/termux_reuse_pr_build_artifacts.sh
user: login: TomJo2000
body: Why is this part indented so far?
created_at: 2025-04-15T23:18:24Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,231 @@
+#!/bin/bash
+set -euo pipefail
+
+GITHUB_EVENT_NAME="${1:-}"
+TARGET_ARCH="${2:-}"
+
+infoexit() {
+  echo "$@" >&2
+  exit 0
+}
+
+# Check required variables.
+for var in OLD_COMMIT HEAD_COMMIT GITHUB_TOKEN GITHUB_EVENT_NAME TARGET_ARCH; do
+  [[ -n "${!var:-}" ]] || infoexit "$var is unset, not performing CI fast path"
+done
+
+graphql_request() {
+  local QUERY="${1}" INFOEXIT_TEXT="${2}" RESPONSE
+  # Send graphql to github and obtain response
+  # No hard tabs or newlines allowed inside a graphql request, also we should escape quotes
+  curl -s \
+    -H "Authorization: token ${GITHUB_TOKEN}" \
+    -H "Accept: application/vnd.github.v3+json" \
+    -H 'Content-Type: application/json' \
+    -X POST \
+    --data "{ \"query\": \"$(tr '\t\n' '  ' <<<  "${QUERY//\"/\\\"}\"}")" \
+    "https://api.github.com/graphql" || infoexit "${INFOEXIT_TEXT}"
+}
+
+ci_artifact_url() {
+  curl -s \
+    -H "Authorization: token ${GITHUB_TOKEN}" \
+    -H "Accept: application/vnd.github.v3+json" \
+    "https://api.github.com/repos/termux/termux-packages/actions/runs/${1}/artifacts" \
+    | jq -r '[.artifacts[] | select(.name | startswith("debs-'"${TARGET_ARCH}"'")) | .archive_download_url][0] // empty' \
+      || infoexit "$2"
+}
+
+COMMITS="$(git rev-list --no-merges "$OLD_COMMIT..$HEAD_COMMIT" ||:)"
+[[ -z "$COMMITS" ]] && infoexit "Unable to obtain full commit history. Not performing CI fast path."
+
+if [[ "${GITHUB_EVENT_NAME:-}" == "push" ]]; then
+  # Check if we can perform CI fast path
+  # Here we make sure all commits come from single PR, make sure nor buildsystem nor dependencies were not changed
+  # and if so we are reusing PR check artifacts and uploading them to apt repo to make save some CI time
+
+  TERMUX_PACKAGE_DIRECTORIES="$(jq --raw-output 'del(.pkg_format) | keys | .[]' repo.json)"
+
+  # We should obtain data about all commits in this push and make sure they all came from the same PR if any.
+  RESPONSE="$(graphql_request "
+  query {
+    repository(owner: \"termux\", name: \"termux-packages\") {
+    $(
+      for commit in ${COMMITS}; do
+        # Add a query for this commit with the shorthash as the label
+        echo "_${commit::7}: object(oid: \"${commit}\") { ...commitWithPR }"
+      done
+    )
+    }
+  }
+
+  fragment commitWithPR on Commit {
+    associatedPullRequests(first: 1) { nodes { baseRefOid headRefOid } edges { node { title body number } } }
+  }" "Failed to perform GraphQL request, not performing CI fast path")"
+
+  # Ensure response is valid and obtain all associated PR numbers if any and make sure response is valid
+  PRS="$(jq '.data.repository | to_entries[] | .value.associatedPullRequests.edges
+           | (.[] // []) | (.node? // []) | (.number? // 0)' <<< "$RESPONSE")" || \
+           infoexit "GraphQL response is invalid, not performing CI fast path"
+
+  # Check if all commits come from the same PR and return if not
+  [[ "$(sort -u <<< "$PRS" | wc -l)" -eq 0 ]] && infoexit "push does not have linked PR, not performing CI fast path"
+  [[ "$(sort -u <<< "$PRS" | wc -l)" -eq 1 ]] || infoexit "push contains commits from more than one pull request, not performing CI fast path"
+
+  PR_BASE_COMMIT="$(jq -r '.data.repository | to_entries[0].value.associatedPullRequests.nodes[0].baseRefOid' <<< "$RESPONSE" ||:)"
+  PR_HEAD_COMMIT="$(jq -r '.data.repository | to_entries[0].value.associatedPullRequests.nodes[0].headRefOid' <<< "$RESPONSE" ||:)"
+  PR_TITLE="$(jq -r '.data.repository | to_entries[0].value.associatedPullRequests.edges[0].node.title' <<< "$RESPONSE" ||:)"
+  PR_BODY="$(jq -r '.data.repository | to_entries[0].value.associatedPullRequests.edges[0].node.body' <<< "$RESPONSE" ||:)"
+  [[ -n "${PR_BASE_COMMIT:-}" ]] || infoexit "failed to read associated PR base commit, not performing CI fast path"
+  [[ -n "${PR_HEAD_COMMIT:-}" ]] || infoexit "failed to read associated PR head commit, not performing CI fast path"
+
+  echo "::group::Detected PR #${PRS}: ${PR_TITLE}"
+  echo "${PR_BODY}"
+  echo "::endgroup::"
+
+  PR_CI_REUSE=0
+  # Check if single-commit and squashed PRs have `[ci reuse]` in commit description
+  if [[ "$(wc -l <<< "${COMMITS}" 2>&1)" -eq 1 && "$(git log -1 --pretty=format:"%s%n%b" "${COMMITS}")" == *"[ci reuse]"* ]]; then
+    PR_CI_REUSE=1
+    echo "Commit subject or description contain [ci reuse]"
+  else
+    # Otherwise check linked PR title and body
+    [[ "${PR_TITLE}${PR_BODY}" == *"[ci reuse]"* ]] && PR_CI_REUSE=1 || PR_CI_REUSE=0
+    (( PR_CI_REUSE )) && echo "PR description contains [ci reuse]"
+  fi
+
+  DIRS_REGEX="$(echo "$TERMUX_PACKAGE_DIRECTORIES" | paste -sd'|' -)" || exit 0
+
+  # fetch PR commit tree
+  git fetch origin "$PR_BASE_COMMIT:ref/tmp/$PR_BASE_COMMIT" || infoexit "failed to fetch PR base tree, not performing CI fast path"
+  git fetch origin "$PR_HEAD_COMMIT:ref/tmp/$PR_HEAD_COMMIT" || infoexit "failed to fetch PR head tree, not performing CI fast path"
+
+  # Here we compare changes from PR with changes from push to make sure nobody injected additional changes to PR branch
+  # after CI was invoked and before we fetched data with GraphQL and `git fetch`.
+  # We can not do apply `git diff --no-index` to commit range so we are going to strip indexes manually with sed.
+  diff -q \
+      <(git diff "$PR_BASE_COMMIT" "$PR_HEAD_COMMIT" | sed -n -E '/^diff --git a\// { p; n; /^index /!p; b } ; p') \
+      <(git diff "$OLD_COMMIT" "$HEAD_COMMIT" | sed -n -E '/^diff --git a\// { p; n; /^index /!p; b } ; p') >/dev/null 2>&1 \
+  || infoexit "PR head does not match pushed commit changes, probably PR ref was force pushed right after PR was merged. Not performing CI fast path."
+
+  # obtain the common ancestor commit where the PR diverged from master.
+  PR_MERGE_BASE="$(git merge-base "ref/tmp/$PR_BASE_COMMIT" "$HEAD_COMMIT")" || infoexit "failed to obtain PR merge base, not performing CI fast path"
+
+  # obtain list of all files changed in master branch since this PR diverged
+  PR_BASE_TO_HEAD_CHANGED_FILES="$(git diff-tree --name-only -r "$PR_MERGE_BASE..$OLD_COMMIT")" ||:
+
+  # obtain list of all packages changed by this PR
+  PR_CHANGED_PACKAGES="$(
+    git diff-tree --name-only -r "$OLD_COMMIT..$HEAD_COMMIT" \
+      | grep -E "^($DIRS_REGEX)/[^/]+/" \
+      | sed -E "s#^(($DIRS_REGEX)/[^/]+)/.*#\1#" \
+      | sort -u
+  )" ||:
+
+  # obtain list of all buildsystem files changed since this PR diverged
+  PR_BASE_TO_HEAD_CHANGED_BUILDSYSTEM_FILES="$(
+    grep -e "^scripts/" -e "^ndk-patches/" -e "^build-package.sh$" <<< "$PR_BASE_TO_HEAD_CHANGED_FILES"
+  )" ||:
+
+  # obtain list of all packages changes since this PR diverged
+  PR_BASE_TO_HEAD_CHANGED_PACKAGES="$(
+    echo "$PR_BASE_TO_HEAD_CHANGED_FILES" \
+      | grep -E "^($DIRS_REGEX)/[^/]+/" \
+      | sed -E "s#^(($DIRS_REGEX)/[^/]+)/.*#\1#" \
+      | sort -u
+  )" ||:
+
+  # obtain list of all build dependencies of packages changed by this PR
+  PR_CHANGED_PACKAGES_DEPS="$(
+    for i in ${PR_CHANGED_PACKAGES:-}; do
+      ./scripts/buildorder.py $i $TERMUX_PACKAGE_DIRECTORIES | awk '{print $NF}'
+    done | sort -u
+  )" ||:
+
+  # get list of all build dependencies changed since this PR diverged
+  PR_BASE_TO_HEAD_CHANGED_DEPS="$(grep -Fxf <(echo "$PR_BASE_TO_HEAD_CHANGED_PACKAGES") - <<< "$PR_CHANGED_PACKAGES_DEPS")" ||:
+
+  echo "Packages changed by this PR:" ${PR_CHANGED_PACKAGES:-none}
+  echo "Dependencies of these packages changed since PR divergence: " ${PR_BASE_TO_HEAD_CHANGED_DEPS:-}
+  echo "Buildsystem files changed since PR divergence:" ${PR_BASE_TO_HEAD_CHANGED_BUILDSYSTEM_FILES:-none}
+  echo "Packages updated since PR divergence:" ${PR_BASE_TO_HEAD_CHANGED_PACKAGES:-none}
+
+  if [[ -z "${PR_BASE_TO_HEAD_CHANGED_BUILDSYSTEM_FILES:-}${PR_BASE_TO_HEAD_CHANGED_DEPS:-}" ]] || (( PR_CI_REUSE )); then
+    # Same commit can be used in more than one pull request or even push
+    # No hard tabs or newlines allowed inside a graphql request.
+
+    RESPONSE="$(graphql_request "
+    query {
+      repository(owner: \"termux\", name: \"termux-packages\") {
+        object(oid: \"$PR_HEAD_COMMIT\") {
+          ... on Commit { checkSuites(first: 16) { nodes { workflowRun { event file { path } databaseId } conclusion status } } }
+        }
+      }
+    }" "Failed to perform GraphQL request, not performing CI fast path")"
+    # Obtain the first completed and successfull `packages.yml` workflow run.
+    WORKFLOW_ID="$(jq -r '[.data.repository.object?.checkSuites?.nodes[]?
+                            | select(
+                              .workflowRun.event == "pull_request" and
+                              .workflowRun.file.path == ".github/workflows/packages.yml" and
+                              .conclusion == "SUCCESS" and
+                              .status == "COMPLETED"
+                            ) | .workflowRun.databaseId][0]' <<< "$RESPONSE" ||:)"
+    if [[ -n "${WORKFLOW_ID}" ]]; then
+      echo "We can safely reuse CI artifacts from https://github.com/termux/termux-packages/actions/runs/${WORKFLOW_ID}"
+      echo "CI artifact URL is $(ci_artifact_url "${WORKFLOW_ID}" "Failed to get CI artifact URL")"
+    else
+      echo "We can safely reuse CI artifacts, but did not find any matching CI run."
+    fi
+  else
+    echo "It is NOT safe to reuse PR build artifact"
+  fi
+fi # end of [[ "${GITHUB_EVENT_NAME:-}" == "push" ]]
+
+if [[ "${GITHUB_EVENT_NAME:-}" == "pull_request" ]]; then
+  # In the case of pull_requests we can reuse build artifacts of recent workflow runs
+  # if user added more commits to existing PR after workflow finished running.
+  if [[ "$(wc -l <<< "${COMMITS}" 2>&1)" -ge 128 ]]; then
+    echo "::error title=CI-ARTIFACT-REUSING::Pull request has more than 128 commits, can not reuse CI artifacts."
+    exit 0
+  fi
+
+  # We intentionally do not check if workflow run is related to this specific PR
+  # to allow CI reuse artifacts of other PRs in the case if current PR diverged from other PR branch.
+  RESPONSE="$(graphql_request "
+  query {
+    repository(owner: \"termux\", name: \"termux-packages\") {
+    $(
+      for commit in ${COMMITS}; do
+        # Add a query for this commit with the shorthash as the label
+        echo "_${commit::7}: object(oid: \"${commit}\") { ...workflowRun }"
+      done
+    )
+    }
+  }
+
+  fragment workflowRun on Commit {
+    checkSuites(first: 32) { nodes { workflowRun { event file { path } databaseId } conclusion status } }
+  }" "Failed to perform GraphQL request, not performing CI fast path")"
+
+  WORKFLOW_ID=
+  COMMIT=
+  # git rev-list prints commits in chronologically descending order, so we can check them as is.
+  for commit in ${COMMITS}; do
+    # Print the first completed and successfull `packages.yml` workflow run for this commit if any
+    WORKFLOW_ID="$(jq -r '[.data.repository["_'"${commit::7}"'"].checkSuites?.nodes[]?
+                            | select(
+                              .workflowRun.event == "pull_request" and
+                              .workflowRun.file.path == ".github/workflows/packages.yml" and
+                              .conclusion == "SUCCESS" and
+                              .status == "COMPLETED"
+                            ) | .workflowRun.databaseId][0] // empty' <<< "$RESPONSE")"
path: scripts/utils/termux_reuse_pr_build_artifacts.sh
user: login: twaik
body: Should be better now.
created_at: 2025-04-15T23:26:33Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,233 @@
+#!/bin/bash
+set -euo pipefail
+
+GITHUB_EVENT_NAME="${1:-}"
+TARGET_ARCH="${2:-}"
+
+infoexit() {
+  echo "$@" >&2
+  exit 0
+}
+
+# Check required variables.
+for var in OLD_COMMIT HEAD_COMMIT GITHUB_TOKEN GITHUB_EVENT_NAME TARGET_ARCH; do
+  [[ -n "${!var:-}" ]] || infoexit "$var is unset, not performing CI fast path"
+done
+
+graphql_request() {
+  local QUERY="${1}" INFOEXIT_TEXT="${2}" RESPONSE
+  # Send graphql to github and obtain response
+  # No hard tabs or newlines allowed inside a graphql request, also we should escape quotes
+  curl -s \
+    -H "Authorization: token ${GITHUB_TOKEN}" \
+    -H "Accept: application/vnd.github.v3+json" \
+    -H 'Content-Type: application/json' \
+    -X POST \
+    --data "{ \"query\": \"$(tr '\t\n' '  ' <<< "${QUERY//\"/\\\"}\"}")" \
+    "https://api.github.com/graphql" || infoexit "${INFOEXIT_TEXT}"
+}
+
+ci_artifact_url() {
+  curl -s \
+    -H "Authorization: token ${GITHUB_TOKEN}" \
+    -H "Accept: application/vnd.github.v3+json" \
+    "https://api.github.com/repos/termux/termux-packages/actions/runs/${1}/artifacts" \
+    | jq -r '[.artifacts[] | select(.name | startswith("debs-'"${TARGET_ARCH}"'")) | .archive_download_url][0] // empty' \
+      || infoexit "$2"
+}
+
+COMMITS="$(git rev-list --no-merges "$OLD_COMMIT..$HEAD_COMMIT" ||:)"
+[[ -z "$COMMITS" ]] && infoexit "Unable to obtain full commit history. Not performing CI fast path."
+
+if [[ "${GITHUB_EVENT_NAME:-}" == "push" ]]; then
+  # Check if we can perform CI fast path
+  # Here we make sure all commits come from single PR, make sure nor buildsystem nor dependencies were not changed
+  # and if so we are reusing PR check artifacts and uploading them to apt repo to make save some CI time
+
+  TERMUX_PACKAGE_DIRECTORIES="$(jq --raw-output 'del(.pkg_format) | keys | .[]' repo.json)"
+
+  # We should obtain data about all commits in this push and make sure they all came from the same PR if any.
+  RESPONSE="$(graphql_request "
+  query {
+    repository(owner: \"termux\", name: \"termux-packages\") {
+    $(
+      for commit in ${COMMITS}; do
+        # Add a query for this commit with the shorthash as the label
+        echo "_${commit::7}: object(oid: \"${commit}\") { ...commitWithPR }"
+      done
+    )
+    }
+  }
+
+  fragment commitWithPR on Commit {
+    associatedPullRequests(first: 1) { nodes { baseRefOid headRefOid } edges { node { title body number } } }
+  }" "Failed to perform GraphQL request, not performing CI fast path")"
+
+  # Ensure response is valid and obtain all associated PR numbers if any and make sure response is valid
+  PRS="$(jq '.data.repository | to_entries[] | .value.associatedPullRequests.edges
+           | (.[] // []) | (.node? // []) | (.number? // 0)' <<< "$RESPONSE")" || \
+           infoexit "GraphQL response is invalid, not performing CI fast path"
+
+  # Check if all commits come from the same PR and return if not
+  [[ "$(sort -u <<< "$PRS" | wc -l)" -eq 0 ]] && infoexit "push does not have linked PR, not performing CI fast path"
+  [[ "$(sort -u <<< "$PRS" | wc -l)" -eq 1 ]] || infoexit "push contains commits from more than one pull request, not performing CI fast path"
+
+  PR_BASE_COMMIT="$(jq -r '.data.repository | to_entries[0].value.associatedPullRequests.nodes[0].baseRefOid' <<< "$RESPONSE" ||:)"
+  PR_HEAD_COMMIT="$(jq -r '.data.repository | to_entries[0].value.associatedPullRequests.nodes[0].headRefOid' <<< "$RESPONSE" ||:)"
+  PR_TITLE="$(jq -r '.data.repository | to_entries[0].value.associatedPullRequests.edges[0].node.title' <<< "$RESPONSE" ||:)"
+  PR_BODY="$(jq -r '.data.repository | to_entries[0].value.associatedPullRequests.edges[0].node.body' <<< "$RESPONSE" ||:)"
+  [[ -n "${PR_BASE_COMMIT:-}" ]] || infoexit "failed to read associated PR base commit, not performing CI fast path"
+  [[ -n "${PR_HEAD_COMMIT:-}" ]] || infoexit "failed to read associated PR head commit, not performing CI fast path"
+
+  echo "::group::Detected PR #${PRS}: ${PR_TITLE}"
+  echo "${PR_BODY}"
+  echo "::endgroup::"
+
+  PR_CI_REUSE=0
+  # Check if single-commit and squashed PRs have `[ci reuse]` in commit description
+  if [[ "$(wc -l <<< "${COMMITS}" 2>&1)" -eq 1 && "$(git log -1 --pretty=format:"%s%n%b" "${COMMITS}")" == *"[ci reuse]"* ]]; then
+    PR_CI_REUSE=1
+    echo "Commit subject or description contain [ci reuse]"
+  else
+    # Otherwise check linked PR title and body
+    [[ "${PR_TITLE}${PR_BODY}" == *"[ci reuse]"* ]] && PR_CI_REUSE=1 || PR_CI_REUSE=0
+    (( PR_CI_REUSE )) && echo "PR description contains [ci reuse]"
+  fi
+
+  DIRS_REGEX="$(echo "$TERMUX_PACKAGE_DIRECTORIES" | paste -sd'|' -)" || exit 0
+
+  # fetch PR commit tree
+  git fetch origin "$PR_BASE_COMMIT:ref/tmp/$PR_BASE_COMMIT" || infoexit "failed to fetch PR base tree, not performing CI fast path"
+  git fetch origin "$PR_HEAD_COMMIT:ref/tmp/$PR_HEAD_COMMIT" || infoexit "failed to fetch PR head tree, not performing CI fast path"
+
+  # Here we compare changes from PR with changes from push to make sure nobody injected additional changes to PR branch
+  # after CI was invoked and before we fetched data with GraphQL and `git fetch`.
+  # We can not do apply `git diff --no-index` to commit range so we are going to strip indexes manually with sed.
+  diff -q \
+      <(git diff "$PR_BASE_COMMIT" "$PR_HEAD_COMMIT" | sed -n -E '/^diff --git a\// { p; n; /^index /!p; b } ; p') \
+      <(git diff "$OLD_COMMIT" "$HEAD_COMMIT" | sed -n -E '/^diff --git a\// { p; n; /^index /!p; b } ; p') >/dev/null 2>&1 \
+  || infoexit "PR head does not match pushed commit changes, probably PR ref was force pushed right after PR was merged. Not performing CI fast path."
+
+  # obtain the common ancestor commit where the PR diverged from master.
+  PR_MERGE_BASE="$(git merge-base "ref/tmp/$PR_BASE_COMMIT" "$HEAD_COMMIT")" || infoexit "failed to obtain PR merge base, not performing CI fast path"
+
+  # obtain list of all files changed in master branch since this PR diverged
+  PR_BASE_TO_HEAD_CHANGED_FILES="$(git diff-tree --name-only -r "$PR_MERGE_BASE..$OLD_COMMIT")" ||:
+
+  # obtain list of all packages changed by this PR
+  PR_CHANGED_PACKAGES="$(
+    git diff-tree --name-only -r "$OLD_COMMIT..$HEAD_COMMIT" \
+      | grep -E "^($DIRS_REGEX)/[^/]+/" \
+      | sed -E "s#^(($DIRS_REGEX)/[^/]+)/.*#\1#" \
+      | sort -u
+  )" ||:
+
+  # obtain list of all buildsystem files changed since this PR diverged
+  PR_BASE_TO_HEAD_CHANGED_BUILDSYSTEM_FILES="$(
+    grep -e "^scripts/" -e "^ndk-patches/" -e "^build-package.sh$" <<< "$PR_BASE_TO_HEAD_CHANGED_FILES"
+  )" ||:
+
+  # obtain list of all packages changes since this PR diverged
+  PR_BASE_TO_HEAD_CHANGED_PACKAGES="$(
+    echo "$PR_BASE_TO_HEAD_CHANGED_FILES" \
+      | grep -E "^($DIRS_REGEX)/[^/]+/" \
+      | sed -E "s#^(($DIRS_REGEX)/[^/]+)/.*#\1#" \
+      | sort -u
+  )" ||:
+
+  # obtain list of all build dependencies of packages changed by this PR
+  PR_CHANGED_PACKAGES_DEPS="$(
+    for i in ${PR_CHANGED_PACKAGES:-}; do
+      ./scripts/buildorder.py $i $TERMUX_PACKAGE_DIRECTORIES | awk '{print $NF}'
+    done | sort -u
+  )" ||:
+
+  # get list of all build dependencies changed since this PR diverged
+  PR_BASE_TO_HEAD_CHANGED_DEPS="$(grep -Fxf <(echo "$PR_BASE_TO_HEAD_CHANGED_PACKAGES") - <<< "$PR_CHANGED_PACKAGES_DEPS")" ||:
+
+  echo "Packages changed by this PR:" ${PR_CHANGED_PACKAGES:-none}
+  echo "Dependencies of these packages changed since PR divergence: " ${PR_BASE_TO_HEAD_CHANGED_DEPS:-}
+  echo "Buildsystem files changed since PR divergence:" ${PR_BASE_TO_HEAD_CHANGED_BUILDSYSTEM_FILES:-none}
+  echo "Packages updated since PR divergence:" ${PR_BASE_TO_HEAD_CHANGED_PACKAGES:-none}
+
+  if [[ -z "${PR_BASE_TO_HEAD_CHANGED_BUILDSYSTEM_FILES:-}${PR_BASE_TO_HEAD_CHANGED_DEPS:-}" ]] || (( PR_CI_REUSE )); then
+    # Same commit can be used in more than one pull request or even push
+    # No hard tabs or newlines allowed inside a graphql request.
+
+    RESPONSE="$(graphql_request "
+    query {
+      repository(owner: \"termux\", name: \"termux-packages\") {
+        object(oid: \"$PR_HEAD_COMMIT\") {
+          ... on Commit { checkSuites(first: 16) { nodes { workflowRun { event file { path } databaseId } conclusion status } } }
+        }
+      }
+    }" "Failed to perform GraphQL request, not performing CI fast path")"
+    # Obtain the first completed and successfull `packages.yml` workflow run.
+    WORKFLOW_ID="$(jq -r '[.data.repository.object?.checkSuites?.nodes[]?
path: scripts/utils/termux_reuse_pr_build_artifacts.sh
user: login: TomJo2000
body: This should probably also get dropped down a line so it isn't quite as indented.
created_at: 2025-04-15T23:30:27Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,233 @@
+#!/bin/bash
+set -euo pipefail
+
+GITHUB_EVENT_NAME="${1:-}"
+TARGET_ARCH="${2:-}"
+
+infoexit() {
+  echo "$@" >&2
+  exit 0
+}
+
+# Check required variables.
+for var in OLD_COMMIT HEAD_COMMIT GITHUB_TOKEN GITHUB_EVENT_NAME TARGET_ARCH; do
+  [[ -n "${!var:-}" ]] || infoexit "$var is unset, not performing CI fast path"
+done
+
+graphql_request() {
+  local QUERY="${1}" INFOEXIT_TEXT="${2}" RESPONSE
+  # Send graphql to github and obtain response
+  # No hard tabs or newlines allowed inside a graphql request, also we should escape quotes
+  curl -s \
+    -H "Authorization: token ${GITHUB_TOKEN}" \
+    -H "Accept: application/vnd.github.v3+json" \
+    -H 'Content-Type: application/json' \
+    -X POST \
+    --data "{ \"query\": \"$(tr '\t\n' '  ' <<< "${QUERY//\"/\\\"}\"}")" \
+    "https://api.github.com/graphql" || infoexit "${INFOEXIT_TEXT}"
+}
+
+ci_artifact_url() {
+  curl -s \
+    -H "Authorization: token ${GITHUB_TOKEN}" \
+    -H "Accept: application/vnd.github.v3+json" \
+    "https://api.github.com/repos/termux/termux-packages/actions/runs/${1}/artifacts" \
+    | jq -r '[.artifacts[] | select(.name | startswith("debs-'"${TARGET_ARCH}"'")) | .archive_download_url][0] // empty' \
+      || infoexit "$2"
+}
+
+COMMITS="$(git rev-list --no-merges "$OLD_COMMIT..$HEAD_COMMIT" ||:)"
+[[ -z "$COMMITS" ]] && infoexit "Unable to obtain full commit history. Not performing CI fast path."
+
+if [[ "${GITHUB_EVENT_NAME:-}" == "push" ]]; then
+  # Check if we can perform CI fast path
+  # Here we make sure all commits come from single PR, make sure nor buildsystem nor dependencies were not changed
+  # and if so we are reusing PR check artifacts and uploading them to apt repo to make save some CI time
+
+  TERMUX_PACKAGE_DIRECTORIES="$(jq --raw-output 'del(.pkg_format) | keys | .[]' repo.json)"
+
+  # We should obtain data about all commits in this push and make sure they all came from the same PR if any.
+  RESPONSE="$(graphql_request "
+  query {
+    repository(owner: \"termux\", name: \"termux-packages\") {
+    $(
+      for commit in ${COMMITS}; do
+        # Add a query for this commit with the shorthash as the label
+        echo "_${commit::7}: object(oid: \"${commit}\") { ...commitWithPR }"
+      done
+    )
+    }
+  }
+
+  fragment commitWithPR on Commit {
+    associatedPullRequests(first: 1) { nodes { baseRefOid headRefOid } edges { node { title body number } } }
+  }" "Failed to perform GraphQL request, not performing CI fast path")"
+
+  # Ensure response is valid and obtain all associated PR numbers if any and make sure response is valid
+  PRS="$(jq '.data.repository | to_entries[] | .value.associatedPullRequests.edges
+           | (.[] // []) | (.node? // []) | (.number? // 0)' <<< "$RESPONSE")" || \
+           infoexit "GraphQL response is invalid, not performing CI fast path"
+
+  # Check if all commits come from the same PR and return if not
+  [[ "$(sort -u <<< "$PRS" | wc -l)" -eq 0 ]] && infoexit "push does not have linked PR, not performing CI fast path"
+  [[ "$(sort -u <<< "$PRS" | wc -l)" -eq 1 ]] || infoexit "push contains commits from more than one pull request, not performing CI fast path"
+
+  PR_BASE_COMMIT="$(jq -r '.data.repository | to_entries[0].value.associatedPullRequests.nodes[0].baseRefOid' <<< "$RESPONSE" ||:)"
+  PR_HEAD_COMMIT="$(jq -r '.data.repository | to_entries[0].value.associatedPullRequests.nodes[0].headRefOid' <<< "$RESPONSE" ||:)"
+  PR_TITLE="$(jq -r '.data.repository | to_entries[0].value.associatedPullRequests.edges[0].node.title' <<< "$RESPONSE" ||:)"
+  PR_BODY="$(jq -r '.data.repository | to_entries[0].value.associatedPullRequests.edges[0].node.body' <<< "$RESPONSE" ||:)"
+  [[ -n "${PR_BASE_COMMIT:-}" ]] || infoexit "failed to read associated PR base commit, not performing CI fast path"
+  [[ -n "${PR_HEAD_COMMIT:-}" ]] || infoexit "failed to read associated PR head commit, not performing CI fast path"
+
+  echo "::group::Detected PR #${PRS}: ${PR_TITLE}"
+  echo "${PR_BODY}"
+  echo "::endgroup::"
+
+  PR_CI_REUSE=0
+  # Check if single-commit and squashed PRs have `[ci reuse]` in commit description
+  if [[ "$(wc -l <<< "${COMMITS}" 2>&1)" -eq 1 && "$(git log -1 --pretty=format:"%s%n%b" "${COMMITS}")" == *"[ci reuse]"* ]]; then
+    PR_CI_REUSE=1
+    echo "Commit subject or description contain [ci reuse]"
+  else
+    # Otherwise check linked PR title and body
+    [[ "${PR_TITLE}${PR_BODY}" == *"[ci reuse]"* ]] && PR_CI_REUSE=1 || PR_CI_REUSE=0
+    (( PR_CI_REUSE )) && echo "PR description contains [ci reuse]"
+  fi
+
+  DIRS_REGEX="$(echo "$TERMUX_PACKAGE_DIRECTORIES" | paste -sd'|' -)" || exit 0
+
+  # fetch PR commit tree
+  git fetch origin "$PR_BASE_COMMIT:ref/tmp/$PR_BASE_COMMIT" || infoexit "failed to fetch PR base tree, not performing CI fast path"
+  git fetch origin "$PR_HEAD_COMMIT:ref/tmp/$PR_HEAD_COMMIT" || infoexit "failed to fetch PR head tree, not performing CI fast path"
+
+  # Here we compare changes from PR with changes from push to make sure nobody injected additional changes to PR branch
+  # after CI was invoked and before we fetched data with GraphQL and `git fetch`.
+  # We can not do apply `git diff --no-index` to commit range so we are going to strip indexes manually with sed.
+  diff -q \
+      <(git diff "$PR_BASE_COMMIT" "$PR_HEAD_COMMIT" | sed -n -E '/^diff --git a\// { p; n; /^index /!p; b } ; p') \
+      <(git diff "$OLD_COMMIT" "$HEAD_COMMIT" | sed -n -E '/^diff --git a\// { p; n; /^index /!p; b } ; p') >/dev/null 2>&1 \
+  || infoexit "PR head does not match pushed commit changes, probably PR ref was force pushed right after PR was merged. Not performing CI fast path."
+
+  # obtain the common ancestor commit where the PR diverged from master.
+  PR_MERGE_BASE="$(git merge-base "ref/tmp/$PR_BASE_COMMIT" "$HEAD_COMMIT")" || infoexit "failed to obtain PR merge base, not performing CI fast path"
+
+  # obtain list of all files changed in master branch since this PR diverged
+  PR_BASE_TO_HEAD_CHANGED_FILES="$(git diff-tree --name-only -r "$PR_MERGE_BASE..$OLD_COMMIT")" ||:
+
+  # obtain list of all packages changed by this PR
+  PR_CHANGED_PACKAGES="$(
+    git diff-tree --name-only -r "$OLD_COMMIT..$HEAD_COMMIT" \
+      | grep -E "^($DIRS_REGEX)/[^/]+/" \
+      | sed -E "s#^(($DIRS_REGEX)/[^/]+)/.*#\1#" \
+      | sort -u
+  )" ||:
+
+  # obtain list of all buildsystem files changed since this PR diverged
+  PR_BASE_TO_HEAD_CHANGED_BUILDSYSTEM_FILES="$(
+    grep -e "^scripts/" -e "^ndk-patches/" -e "^build-package.sh$" <<< "$PR_BASE_TO_HEAD_CHANGED_FILES"
+  )" ||:
+
+  # obtain list of all packages changes since this PR diverged
+  PR_BASE_TO_HEAD_CHANGED_PACKAGES="$(
+    echo "$PR_BASE_TO_HEAD_CHANGED_FILES" \
+      | grep -E "^($DIRS_REGEX)/[^/]+/" \
+      | sed -E "s#^(($DIRS_REGEX)/[^/]+)/.*#\1#" \
+      | sort -u
+  )" ||:
+
+  # obtain list of all build dependencies of packages changed by this PR
+  PR_CHANGED_PACKAGES_DEPS="$(
+    for i in ${PR_CHANGED_PACKAGES:-}; do
+      ./scripts/buildorder.py $i $TERMUX_PACKAGE_DIRECTORIES | awk '{print $NF}'
+    done | sort -u
+  )" ||:
+
+  # get list of all build dependencies changed since this PR diverged
+  PR_BASE_TO_HEAD_CHANGED_DEPS="$(grep -Fxf <(echo "$PR_BASE_TO_HEAD_CHANGED_PACKAGES") - <<< "$PR_CHANGED_PACKAGES_DEPS")" ||:
+
+  echo "Packages changed by this PR:" ${PR_CHANGED_PACKAGES:-none}
+  echo "Dependencies of these packages changed since PR divergence: " ${PR_BASE_TO_HEAD_CHANGED_DEPS:-}
+  echo "Buildsystem files changed since PR divergence:" ${PR_BASE_TO_HEAD_CHANGED_BUILDSYSTEM_FILES:-none}
+  echo "Packages updated since PR divergence:" ${PR_BASE_TO_HEAD_CHANGED_PACKAGES:-none}
+
+  if [[ -z "${PR_BASE_TO_HEAD_CHANGED_BUILDSYSTEM_FILES:-}${PR_BASE_TO_HEAD_CHANGED_DEPS:-}" ]] || (( PR_CI_REUSE )); then
+    # Same commit can be used in more than one pull request or even push
+    # No hard tabs or newlines allowed inside a graphql request.
+
+    RESPONSE="$(graphql_request "
+    query {
+      repository(owner: \"termux\", name: \"termux-packages\") {
+        object(oid: \"$PR_HEAD_COMMIT\") {
+          ... on Commit { checkSuites(first: 16) { nodes { workflowRun { event file { path } databaseId } conclusion status } } }
+        }
+      }
+    }" "Failed to perform GraphQL request, not performing CI fast path")"
+    # Obtain the first completed and successfull `packages.yml` workflow run.
+    WORKFLOW_ID="$(jq -r '[.data.repository.object?.checkSuites?.nodes[]?
path: scripts/utils/termux_reuse_pr_build_artifacts.sh
user: login: twaik
body: Done.
created_at: 2025-04-15T23:33:30Z
author_association: MEMBER

PR comments:
user: login: TomJo2000
created_at: 2025-04-14T20:10:15Z
body: - Please consider pulling this out into a script in the `scripts/` directory.
Similar to the recent cleanup step changes in #23875.
Something like `scripts/utils/termux_cache_ci_artifacts.sh` <sub>(feel free to come up with a better name)</sub>
author_association: MEMBER
user: login: robertkirkman
created_at: 2025-04-14T20:19:43Z
body: It is a very good idea, good job, and a good optimization, but I am interested to see how the PR build artifacts will be downloaded, because,

I am just slightly worried about a side channel attack.

I imagine this scenario:

1. A contributor opens a PR that is normal
2. The PR is reviewed and then merged normally
3. This workflow begins and starts the check to determine whether to rebuild cleanly from the master branch, or download the artifact from the PR run
4. The contributor quickly pushes into the same branch again on their repo and somehow triggers another CI run in the GitHub Actions of termux-packages after the PR was merged already, which can maybe somehow complete quickly depending on the circumstances
5. The artifacts from the strange run are injected into the workflow and uploaded to the package host
author_association: MEMBER
user: login: twaik
created_at: 2025-04-14T20:30:05Z
body: Nice points.
I am going to download artifacts of CI runs referencing the same head commit as used during merge. 
But probably you are right about that, I am not sure how fast attacker can merge or force-push PR before CI will make graphql request.
author_association: MEMBER
user: login: TomJo2000
created_at: 2025-04-14T20:32:18Z
body: > I am not sure how fast attacker can merge or force-push PR before CI will make graphql request.

If that's an unknown, let's assume it's effectively instantly and try to "trust but verify".
author_association: MEMBER
user: login: twaik
created_at: 2025-04-14T20:48:38Z
body: Nor CI nor GraphQL do not give any instruments to detect push or force-push and obtain their timestamps, so probably the best thing I can do is to fetch PR ref, make sure PR ref and base are same as in data obtained through GraphQL and files and their contents are same in both squashed/rebased commits and in the fetched PR ref.
author_association: MEMBER
user: login: twaik
created_at: 2025-04-14T22:06:29Z
body: Ok, now it checks if changes in push match to changes in PR. So even in the case if attacker manages to push some changes after CI runs but before we fetch graphql metadata fast CI path will not be performed if there are some unaccounted changes.
author_association: MEMBER
user: login: twaik
created_at: 2025-04-15T01:49:18Z
body: Since this script does not make CI fail by design and only prints informational messages I am intending to merge it in 48 hours if nobody minds or after it gets 2+ approves.
author_association: MEMBER
user: login: twaik
created_at: 2025-04-15T03:06:30Z
body: From now the code will print something similar to `Gather build info` step output:
```bash
$ GITHUB_EVENT_NAME=push OLD_COMMIT=5e1f965bda10a08eaf78cdef62b9f19c00057836 HEAD_COMMIT=7b5bd568c27b1d2d08d349e6431c95b1e6195e28 ./scripts/utils/termux_reuse_pr_build_artifacts.sh 
Packages changed by this PR: packages/openssh
Dependencies of these packages changed since PR divergence: 
Buildsystem files changed since PR divergence: none
Packages updated since PR divergence: x11-packages/libadwaita
We can safely reuse CI artifacts from https://github.com/termux/termux-packages/actions/runs/14436730953

author_association: MEMBER
user: login: twaik
created_at: 2025-04-15T03:10:17Z
body: Probably I can write some code for pull requests as well. In this case we will be able to push commits to existing pull requests and reuse already built packages from previous runs. It will help us perform (and merge!) huge package rebuildings in one PR with single merge, with no chance for partial mirror syncs.
author_association: MEMBER
user: login: twaik
created_at: 2025-04-15T11:01:48Z
body: Probably it will be a nice idea to implement checking if there is `[ci reuse]` in PR commit/text and in this case force reusing CI artifact despite of build dependencies being updated. Rebuilding packages with lots of reverse dependencies take a goddamn lot of time.
author_association: MEMBER
user: login: twaik
created_at: 2025-04-15T12:49:00Z
body: Also in ideal situation this PR can be a part of auto-updating solution for updating packages whose building takes much more than 6 hours.

I mean let's say we have chromium with, let's say, 4 steps taking 5 hours each.

We can make auto-update rule which will create PR for the first step of chromium and when all jobs will be finished successfully CI will push one more commit in upload-test step with containing update for the next step of building chromium. And after all steps will be finished successfully we can make CI auto-merge everything to master with `[ci reuse]` and all build artifacts will be automatically uploaded to apt repo.

Probably we can start with moving auto-updating firefox to PR since it takes less time.
author_association: MEMBER
user: login: licy183
created_at: 2025-04-15T14:01:18Z
body: The optimization in this PR is great, especially for packages that take a long time to build (like chromium and firefox).

I think it is better to use `[force reuse <workflow_id>]` in the commit msg to force CI to download the debs in <workflow_id> and perform the upload operation. In this way, the upload operation for libicu reverse dependency can also be optimized. But that can be done in another PR I think.

author_association: MEMBER
user: login: twaik
created_at: 2025-04-15T14:58:05Z
body: > I think it is better to use `[force reuse <workflow_id>]` in the commit msg to force CI to download the debs in <workflow_id> and perform the upload operation. In this way, the upload operation for libicu reverse dependency can also be optimized. But that can be done in another PR I think.

In the case if contributor pushes more commits or force-pushes branch and merges CI will not apply these changes and download old debs instead. And in this case we will have problems with changes-not-applied with new commits or force-pushes.

The whole idea of this enhancement is to find workflow with the most recent changes in PR, ensure these changes match to changes in push (in the case of rebase or squash merging commit themselves will not match, but their contents will) and reuse artifacts.

In ideal sutuation `[ci reuse]` will be needed only for enforcing deb reusing despite potential changes in package dependencies.

Without this tag CI will check if package dependencies are same (no updates or revbumps or newly appeared files merged with `[no ci]`) and reuse CI artifact in any case.

(this script does not reuse CI artifacts yet, only checks if it is possible and prints some info to workflow log. We will enable it later after making sure it detects CI artifacts correctly and does not open any security breaches).
author_association: MEMBER
user: login: twaik
created_at: 2025-04-15T17:33:38Z
body: Good. Now the script obtains URLs for artifacts (to be obtained with `curl` with `GITHUB_TOKEN` authorization, ofc) and works for PRs as well.

````
$ OLD_COMMIT=5e1f965bda10a08eaf78cdef62b9f19c00057836 HEAD_COMMIT=7b5bd568c27b1d2d08d349e6431c95b1e6195e28 bash ./scripts/utils/termux_reuse_pr_build_artifacts.sh push x86_64
::group::Detected PR #24224: bump(main/openssh): 10.0p2
- closes #24223

This PR updates `openssh` to version 10.0p1

> [!IMPORTANT]
> Essential package PR checklist:
> - [x] Builds locally
> - [x] Builds on CI
> - [x] Tested on device (Fairphone 5, Android 14, AArch64)
> - [x] Package completeness/integrity checked
> - [x] Checked dynamic linking against all dependencies


```console
This release removes the code responsible for the
user authentication phase of the protocol from the per-
connection sshd-session binary to a new sshd-auth binary.
Splitting this code into a separate binary ensures that the
crucial pre-authentication attack surface has an entirely
disjoint address space from the code used for the rest of the
connection. It also yields a small runtime memory saving as the
authentication code will be unloaded after the authentication
phase completes. This change should be largely invisible to
users, though some log messages may now come from "sshd-auth"
instead of "sshd-session". Downstream distributors of OpenSSH
will need to package the sshd-auth binary.
```
###### https://www.openssh.com/releasenotes.html#10.0p1

I have amended the `Makefile.in.patch` to have `sshd-auth` link against `termux-auth`.
`sshd` itself *shouldn't* need to link against `termux-auth` anymore, however this will require further validation.
::endgroup::
Packages changed by this PR: packages/openssh
Dependencies of these packages changed since PR divergence: 
Buildsystem files changed since PR divergence: none
Packages updated since PR divergence: x11-packages/libadwaita
We can safely reuse CI artifacts from https://github.com/termux/termux-packages/actions/runs/14436730953
CI artifact URL is https://api.github.com/repos/termux/termux-packages/actions/artifacts/2936821766/zip
$ OLD_COMMIT=ffedc3f0c1fef9026b1dbab4261f80f7b38ad10f HEAD_COMMIT=023593ab9393d3aad55e741ab4e7a860d393f7da bash ./scripts/utils/termux_reuse_pr_build_artifacts.sh pull_request x86_64
We can safely reuse CI artifacts from https://github.com/termux/termux-packages/actions/runs/13479741978
CI artifact URL is https://api.github.com/repos/termux/termux-packages/actions/artifacts/2636505101/zip
````
author_association: MEMBER
user: login: TomJo2000
created_at: 2025-04-15T23:26:01Z
body:  > I think it is better to use `[force reuse <workflow_id>]` in the commit msg to force CI to download the debs in <workflow_id> and perform the upload operation. 

That would introduce significant friction to using the artifact caching.

> The optimization in this PR is great, especially for packages that take a long time to build (like chromium and firefox).
> [...]
> In this way, the upload operation for libicu reverse dependency can also be optimized. But that can be done in another PR I think.

One nice side effect of this "automatic" caching approach is that it enables a mostly seamless staging workflow for rebuilds.
As you described above, it's very nice to be able to queue up a bunch of packages in "staging" PRs and then being able to merge them without significant lag time between merge and upload.
This minimizes the time where a system upgrade may cause dependency issues.
author_association: MEMBER
user: login: twaik
created_at: 2025-04-15T23:32:12Z
body: > As you described above, it's very nice to be able to queue up a bunch of packages in "staging" PRs and then being able to merge them without significant lag time between merge and upload.
> This minimizes the time where a system upgrade may cause dependency issues.

I'd say it completely removes delays between merging packages and their appearing in repos, saving days of CI time in the case of updating big packages or huge rebuilds.
author_association: MEMBER
user: login: TomJo2000
created_at: 2025-04-16T02:58:49Z
body: > I'd say it completely removes delays between merging packages and their appearing in repos, saving days of CI time in the case of updating big packages or huge rebuilds.

Well no, the upload steps still need to run which takes a couple minutes.
But a couple minutes of CI time is significantly less disruptive than a couple days of cumulative CI time.
author_association: MEMBER
user: login: twaik
created_at: 2025-04-16T03:28:18Z
body: Well, 1 minute of uploading packages is nothing compared to 2 hours of rebuilding firefox. 
And we are close to get chromium in our repos, rebuilding it takes forever.
author_association: MEMBER
user: login: TomJo2000
created_at: 2025-04-17T17:24:55Z
body: I did a bit[^1] of refactoring.

[^1]: ![image](https://github.com/user-attachments/assets/17bcfd7a-171b-4d23-9165-35e3b3d24a30)
author_association: MEMBER
user: login: twaik
created_at: 2025-04-17T17:55:07Z
body: Changes seem to be completely fine.
author_association: MEMBER