Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/renovate.json5
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@
matchStrings: [
'VERSION\\[AVBROOT_SETUP\\]=\\"(?<currentDigest>.*?)\\"',
],
depNameTemplate: "https://github.com/chenxiaolong/my-avbroot-setup.git",
depNameTemplate: "https://github.com/0cwa/my-avbroot-setup.git",
datasourceTemplate: "git-refs",
currentValueTemplate: "master",
versioningTemplate: "sha",
Expand Down
253 changes: 253 additions & 0 deletions .github/workflows/build-rom.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,253 @@
name: Reusable ROM build

on:
workflow_call:
inputs:
rom-family:
required: true
type: string
device-id:
required: true
type: string
root:
required: true
type: boolean
magisk-preinit-device:
required: false
type: string
default: ""
update-channel:
required: true
type: string
compatible-sepolicy-patching:
required: true
type: boolean
allow-unauthorized-adb:
required: true
type: boolean
release-type:
required: true
type: string
publish:
required: true
type: boolean
secrets:
AVB_KEY:
required: true
CERT_OTA:
required: true
OTA_KEY:
required: true
PASSPHRASE_AVB:
required: true
PASSPHRASE_OTA:
required: true
GH_TOKEN:
required: false
EMAIL:
required: false

env:
CARGO_INCREMENTAL: 1
DEVICE_NAME: ${{ inputs.device-id }}
INTERACTIVE_MODE: false
ROM_FAMILY: ${{ inputs.rom-family }}
GRAPHENEOS_UPDATE_CHANNEL: ${{ inputs.update-channel }}
OUTPUT_SCOPE: ${{ inputs.publish && inputs.release-type != 'build-only' && 'published' || 'local-unpublished' }}
RUST_BACKTRACE: short
RUSTUP_MAX_RETRIES: 10
GH_TOKEN: ${{ secrets.GH_TOKEN }}
RELEASE_TYPE: ${{ inputs.release-type }}

jobs:
build:
runs-on: ubuntu-latest
permissions:
contents: write

steps:
- name: Validate build request
shell: bash
env:
ADDITIONALS_DEBUG: ${{ inputs.allow-unauthorized-adb }}
ADDITIONALS_MAS_COMPATIBLE_SEPOLICY: ${{ inputs.compatible-sepolicy-patching }}
ADDITIONALS_ROOT: ${{ inputs.root }}
MAGISK_PREINIT_REQUEST: ${{ inputs.magisk-preinit-device }}
run: |
if [[ "${ADDITIONALS_ROOT}" == "true" && -z "${MAGISK_PREINIT_REQUEST}" ]]; then
echo "::error::magisk-preinit-device is required for rooted builds"
exit 1
fi
case "${RELEASE_TYPE}" in
default|build-only|force-publish) ;;
*) echo "::error::Unknown release type"; exit 1 ;;
esac

- name: Checkout shared implementation
uses: actions/checkout@v7
with:
fetch-depth: 0

- name: Enforce profile output policy
shell: bash
env:
ADDITIONALS_DEBUG: ${{ inputs.allow-unauthorized-adb }}
ADDITIONALS_MAS_COMPATIBLE_SEPOLICY: ${{ inputs.compatible-sepolicy-patching }}
ADDITIONALS_ROOT: ${{ inputs.root }}
run: |
source src/declarations.sh
source src/rom_profiles.sh
resolve_rom_profile
enforce_output_policy "${OUTPUT_SCOPE}"

- name: Install Rust stable
uses: dtolnay/rust-toolchain@master
with:
toolchain: stable 2 weeks ago

- name: Cache Rust dependencies
uses: Swatinem/rust-cache@v2.9.2
Comment on lines +103 to +109

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

Pin dtolnay/rust-toolchain to a commit SHA.

Line 104 references the mutable master branch. This job holds AVB_KEY, OTA_KEY, and both passphrases, and it has contents: write. Any change pushed to that branch runs with those secrets. Pin the action to a full commit SHA.

🔒 Proposed fix
       - name: Install Rust stable
-        uses: dtolnay/rust-toolchain@master
+        uses: dtolnay/rust-toolchain@<full-commit-sha> # stable
         with:
           toolchain: stable 2 weeks ago
🧰 Tools
🪛 zizmor (1.29.0)

[error] 104-104: unpinned action reference (unpinned-uses): action is not pinned to a hash (required by blanket policy)

(unpinned-uses)


[error] 109-109: unpinned action reference (unpinned-uses): action is not pinned to a hash (required by blanket policy)

(unpinned-uses)


[error] 109-109: runtime artifacts potentially vulnerable to a cache poisoning attack (cache-poisoning): enables caching by default

(cache-poisoning)


[info] 104-104: action functionality is already included by the runner (superfluous-actions): use rustup and/or cargo in a script step

(superfluous-actions)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.github/workflows/build-rom.yml around lines 103 - 109, Update the “Install
Rust stable” step to pin dtolnay/rust-toolchain to a full immutable commit SHA
instead of the mutable master reference, while preserving the stable toolchain
configuration.

Source: Linters/SAST tools


- name: Install Python
uses: actions/setup-python@v7
with:
python-version: "3.12-dev"
Comment on lines +111 to +114

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🌐 Web query:

actions/setup-python python-version 3.12-dev prerelease behavior

💡 Result:

In the actions/setup-python action, the x.y-dev syntax (such as 3.12-dev) is a specific notation used to automatically install the latest available patch version of a given minor release, which explicitly includes alpha, beta, and release candidate (pre-release) versions [1][2]. Key behaviors regarding this syntax and pre-release handling include: 1. Automatic Inclusion: Unlike standard version specifiers (e.g., 3.12), the 3.y-dev notation is designed to include pre-release versions for the specified minor version automatically, without requiring any additional configuration [1][2]. 2. Purpose of allow-prereleases: The separate allow-prereleases input (which defaults to false) serves a different purpose [1][3]. It allows the action to fall back to a pre-release version of Python only when a matching General Availability (GA) version does not yet exist for a specified minor version (e.g., specifying python-version: 3.14 with allow-prereleases: true when 3.14 GA has not been released) [1][4]. 3. Interaction: The x.y-dev syntax is distinct from the fallback behavior controlled by allow-prereleases [1][4]. You do not need to set allow-prereleases: true to use the x.y-dev syntax to fetch the latest pre-release for that minor version [1][2]. In summary, if you use 3.12-dev, the action will resolve to the latest patch or pre-release available for the 3.12 series by default [1][2]. If you use 3.12 (without -dev), it will only install a stable GA release unless you explicitly set allow-prereleases: true [1][3][4].

Citations:


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "Workflow relevant sections:"
if [ -f .github/workflows/build-rom.yml ]; then
  nl -ba .github/workflows/build-rom.yml | sed -n '1,180p'
else
  echo "workflow file missing"
fi

echo
echo "Check whether 3.12-dev is an in-development prerelease in current pythons repository:"
python3 - <<'PY'
from urllib.request import urlopen, Request
import json
versions = json.load(urlopen(Request("https://raw.githubusercontent.com/python/cpython/main/versions.json", headers={"User-Agent":"README-verification"})))
print("versions:", versions)
PY

Repository: 0cwa/PixeneOS

Length of output: 219


🏁 Script executed:

#!/bin/bash
set -e
export PYTHONUNBUFFERED=1

echo "Workflow file exists:"
test -f .github/workflows/build-rom.yml && echo yes || echo no

echo
echo "build-rom.yml relevant lines:"
python3 - <<'PY'
from pathlib import Path
p = Path(".github/workflows/build-rom.yml")
print("exists?", p.exists())
if p.exists():
    lines = p.read_text().splitlines()
    for i in range(max(0, 105), min(len(lines), 130)):
        print(f"{i+1:4}: {lines[i]}")
PY

echo
echo "Releases/build steps and Python usage:"
python3 - <<'PY'
from pathlib import Path
p = Path(".github/workflows/build-rom.yml")
if p.exists():
    lines = p.read_text().splitlines()
    for needle in ["release", "sign", "build", "python", "python-version"]:
        matches = [(i+1, line) for i, line in enumerate(lines) if needle.lower() in line.lower()]
        print(needle + ":")
        for n, line in matches[:30]:
            print(f"{n:4}: {line}")
PY

echo
echo "Check python x.y-dev in CPython versions.json:"
python3 - <<'PY'
from urllib.request import urlopen, Request
import json
try:
    versions = json.load(urlopen(Request("https://raw.githubusercontent.com/python/cpython/main/versions.json", headers={"User-Agent":"README-verification"})))
except Exception as exc:
    print("FETCH_ERROR", type(exc).__name__, str(exc))
    raise
print("python_version:", versions.get("python_version"))
print("versions:", versions.get("versions"))
PY

Repository: 0cwa/PixeneOS

Length of output: 5596


Pin the Python interpreter for the signing path.

This job installs Python 3.12-dev just before setting up the signing environment. A dev release resolver can return a different interpreter patch over time, so use an exact released version such as "3.12" or "3.12.11" if this needs to be reproducible.

🧰 Tools
🪛 zizmor (1.29.0)

[error] 112-112: unpinned action reference (unpinned-uses): action is not pinned to a hash (required by blanket policy)

(unpinned-uses)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.github/workflows/build-rom.yml around lines 111 - 114, Update the Install
Python step using actions/setup-python in the signing job to replace the
floating "3.12-dev" version with a stable released Python version, preferably an
exact patch version such as "3.12.11" for reproducible builds.


- name: Setup signing environment
shell: bash
run: |
echo "KEYS_AVB_BASE64<<EOF" >> "${GITHUB_ENV}"
echo "${{ secrets.AVB_KEY }}" >> "${GITHUB_ENV}"
echo "EOF" >> "${GITHUB_ENV}"
echo "KEYS_CERT_OTA_BASE64<<EOF" >> "${GITHUB_ENV}"
echo "${{ secrets.CERT_OTA }}" >> "${GITHUB_ENV}"
echo "EOF" >> "${GITHUB_ENV}"
echo "KEYS_OTA_BASE64<<EOF" >> "${GITHUB_ENV}"
echo "${{ secrets.OTA_KEY }}" >> "${GITHUB_ENV}"
echo "EOF" >> "${GITHUB_ENV}"
Comment on lines +116 to +127

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

Pass the signing secrets through env and use a unique heredoc delimiter.

Lines 119-127 expand secrets directly into the script body. The heredoc delimiter is the fixed string EOF. If any secret ever contains a line equal to EOF, the block terminates early and the remaining secret content is written to GITHUB_ENV as attacker-influenced variable assignments. Referencing the secrets through env and generating a random delimiter removes both the template expansion and the delimiter collision.

🔒 Proposed fix
       - name: Setup signing environment
         shell: bash
+        env:
+          AVB_KEY: ${{ secrets.AVB_KEY }}
+          CERT_OTA: ${{ secrets.CERT_OTA }}
+          OTA_KEY: ${{ secrets.OTA_KEY }}
         run: |
-          echo "KEYS_AVB_BASE64<<EOF" >> "${GITHUB_ENV}"
-          echo "${{ secrets.AVB_KEY }}" >> "${GITHUB_ENV}"
-          echo "EOF" >> "${GITHUB_ENV}"
-          echo "KEYS_CERT_OTA_BASE64<<EOF" >> "${GITHUB_ENV}"
-          echo "${{ secrets.CERT_OTA }}" >> "${GITHUB_ENV}"
-          echo "EOF" >> "${GITHUB_ENV}"
-          echo "KEYS_OTA_BASE64<<EOF" >> "${GITHUB_ENV}"
-          echo "${{ secrets.OTA_KEY }}" >> "${GITHUB_ENV}"
-          echo "EOF" >> "${GITHUB_ENV}"
+          delimiter="$(openssl rand -hex 16)"
+          {
+            printf '%s<<%s\n%s\n%s\n' KEYS_AVB_BASE64 "${delimiter}" "${AVB_KEY}" "${delimiter}"
+            printf '%s<<%s\n%s\n%s\n' KEYS_CERT_OTA_BASE64 "${delimiter}" "${CERT_OTA}" "${delimiter}"
+            printf '%s<<%s\n%s\n%s\n' KEYS_OTA_BASE64 "${delimiter}" "${OTA_KEY}" "${delimiter}"
+          } >> "${GITHUB_ENV}"

This also resolves the SC2129 shellcheck finding reported by actionlint on this step.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
- name: Setup signing environment
shell: bash
run: |
echo "KEYS_AVB_BASE64<<EOF" >> "${GITHUB_ENV}"
echo "${{ secrets.AVB_KEY }}" >> "${GITHUB_ENV}"
echo "EOF" >> "${GITHUB_ENV}"
echo "KEYS_CERT_OTA_BASE64<<EOF" >> "${GITHUB_ENV}"
echo "${{ secrets.CERT_OTA }}" >> "${GITHUB_ENV}"
echo "EOF" >> "${GITHUB_ENV}"
echo "KEYS_OTA_BASE64<<EOF" >> "${GITHUB_ENV}"
echo "${{ secrets.OTA_KEY }}" >> "${GITHUB_ENV}"
echo "EOF" >> "${GITHUB_ENV}"
- name: Setup signing environment
shell: bash
env:
AVB_KEY: ${{ secrets.AVB_KEY }}
CERT_OTA: ${{ secrets.CERT_OTA }}
OTA_KEY: ${{ secrets.OTA_KEY }}
run: |
delimiter="$(openssl rand -hex 16)"
{
printf '%s<<%s\n%s\n%s\n' KEYS_AVB_BASE64 "${delimiter}" "${AVB_KEY}" "${delimiter}"
printf '%s<<%s\n%s\n%s\n' KEYS_CERT_OTA_BASE64 "${delimiter}" "${CERT_OTA}" "${delimiter}"
printf '%s<<%s\n%s\n%s\n' KEYS_OTA_BASE64 "${delimiter}" "${OTA_KEY}" "${delimiter}"
} >> "${GITHUB_ENV}"
🧰 Tools
🪛 actionlint (1.7.12)

[error] 118-118: shellcheck reported issue in this script: SC2129:style:1:1: Consider using { cmd1; cmd2; } >> file instead of individual redirects

(shellcheck)

🪛 zizmor (1.29.0)

[warning] 120-120: code injection via template expansion (template-injection): may expand into attacker-controllable code

(template-injection)


[warning] 123-123: code injection via template expansion (template-injection): may expand into attacker-controllable code

(template-injection)


[warning] 126-126: code injection via template expansion (template-injection): may expand into attacker-controllable code

(template-injection)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.github/workflows/build-rom.yml around lines 116 - 127, Update the “Setup
signing environment” step to pass each signing secret through the step’s env
configuration instead of expanding secrets inside the bash script, and write all
GITHUB_ENV entries using one generated unique heredoc delimiter rather than the
fixed EOF marker. Preserve the existing KEYS_AVB_BASE64, KEYS_CERT_OTA_BASE64,
and KEYS_OTA_BASE64 variable names and secret contents while consolidating the
environment-file writes to avoid the SC2129 warning.

Source: Linters/SAST tools


- name: Patch OTA
shell: bash
env:
ADDITIONALS_DEBUG: ${{ inputs.allow-unauthorized-adb }}
ADDITIONALS_MAS_COMPATIBLE_SEPOLICY: ${{ inputs.compatible-sepolicy-patching }}
ADDITIONALS_ROOT: ${{ inputs.root }}
CLEANUP: true
MAGISK_PREINIT: ${{ inputs.magisk-preinit-device }}
PASSPHRASE_AVB: ${{ secrets.PASSPHRASE_AVB }}
PASSPHRASE_OTA: ${{ secrets.PASSPHRASE_OTA }}
run: |
source src/main.sh
{
echo "GRAPHENEOS_OTA_TARGET=${GRAPHENEOS[OTA_TARGET]}"
echo "GRAPHENEOS_VERSION=${VERSION[GRAPHENEOS]}"
echo "MODULE_SELECTION_FINGERPRINT=${MODULE_SELECTION_FINGERPRINT}"
echo "OUTPUTS_PATCHED_OTA=${OUTPUTS[PATCHED_OTA]}"
echo "WORKDIR=${WORKDIR}"
} >> "${GITHUB_ENV}"

- name: Record build metadata
shell: bash
run: |
selection_metadata="${OUTPUTS_PATCHED_OTA}.selection.json"
python3 - \
"${selection_metadata}" \
"${ROM_FAMILY}" \
"${DEVICE_NAME}" \
"${MODULE_SELECTION_FINGERPRINT}" \
"${OUTPUT_SCOPE}" <<'PY'
import json
import pathlib
import sys

path, rom_family, device, fingerprint, output_scope = sys.argv[1:]
data = {
"device": device,
"module_selection_fingerprint": fingerprint,
"output_scope": output_scope,
"rom_family": rom_family,
"schema_version": 1,
}
pathlib.Path(path).write_text(
json.dumps(data, indent=2, sort_keys=True) + "\n",
encoding="utf-8",
)
PY
echo "SELECTION_METADATA=${selection_metadata}" >> "${GITHUB_ENV}"
{
echo "ROM family: ${ROM_FAMILY}"
echo "Device: ${DEVICE_NAME}"
echo "Selection fingerprint: ${MODULE_SELECTION_FINGERPRINT}"
echo "Output scope: ${OUTPUT_SCOPE}"
} >> "${GITHUB_STEP_SUMMARY}"

- name: Re-enforce publication policy
if: inputs.publish && inputs.release-type != 'build-only'
shell: bash
env:
ADDITIONALS_DEBUG: ${{ inputs.allow-unauthorized-adb }}
ADDITIONALS_MAS_COMPATIBLE_SEPOLICY: ${{ inputs.compatible-sepolicy-patching }}
ADDITIONALS_ROOT: ${{ inputs.root }}
run: |
source src/declarations.sh
source src/rom_profiles.sh
resolve_rom_profile
enforce_publication_evidence "${OUTPUT_SCOPE}"
[[ "${MODULE_SELECTION_FINGERPRINT}" =~ ^[0-9a-f]{64}$ ]]

- name: Generate changelog
if: inputs.publish && inputs.release-type != 'build-only'
shell: bash
run: |
{
echo "ROM family: ${ROM_FAMILY}"
echo "Device: ${DEVICE_NAME}"
echo "Module-selection fingerprint: ${MODULE_SELECTION_FINGERPRINT}"
} > "${GITHUB_WORKSPACE}-CHANGELOG.txt"

- name: Publish GitHub release
if: inputs.publish && inputs.release-type != 'build-only'
uses: softprops/action-gh-release@v3
with:
body_path: ${{ github.workspace }}-CHANGELOG.txt
files: |
${{ env.OUTPUTS_PATCHED_OTA }}
${{ env.OUTPUTS_PATCHED_OTA }}.csig
${{ env.SELECTION_METADATA }}
name: ${{ env.GRAPHENEOS_VERSION }}
tag_name: ${{ env.GRAPHENEOS_VERSION }}

- name: Publish OTA metadata
if: inputs.publish && inputs.release-type != 'build-only'
shell: bash
run: |
git config user.email "${{ secrets.EMAIL }}"
git config user.name "${{ github.repository_owner }}"
current_commit="$(git rev-parse --short HEAD)"
if [[ "${{ inputs.root }}" == 'true' ]]; then
flavor='magisk'
else
flavor='rootless'
fi

git checkout gh-pages
target_file="${flavor}/${DEVICE_NAME}.json"
variant_file="variants/${ROM_FAMILY}/${flavor}/${DEVICE_NAME}-${MODULE_SELECTION_FINGERPRINT}.json"
mkdir -p -- "${flavor}" "$(dirname -- "${variant_file}")"
[[ -f "${DEVICE_NAME}.json" ]] || {
echo "::error::Missing generated OTA metadata for ${DEVICE_NAME}"
exit 1
}
cp -- "${DEVICE_NAME}.json" "${target_file}"
cp -- "${DEVICE_NAME}.json" "${variant_file}"
git add -- "${target_file}" "${variant_file}"

if [[ "${RELEASE_TYPE}" == 'force-publish' ]]; then
printf 'force-publish run %s (attempt %s)\n' \
"${GITHUB_RUN_ID}" "${GITHUB_RUN_ATTEMPT}" > .tmp
git add -- .tmp
fi
Comment on lines +245 to +249

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

.tmp collides with WORKDIR and breaks the force-publish path.

src/declarations.sh line 28 sets WORKDIR=".tmp", and the Patch OTA step exports WORKDIR to GITHUB_ENV, so .tmp exists in the workspace as a directory during this step. Line 247 redirects into .tmp, which fails with "Is a directory", and the step aborts. If the directory were ever removed, git add -- .tmp would instead commit the whole build work directory to gh-pages. Use a distinct marker path inside the published tree.

🐛 Proposed fix
           if [[ "${RELEASE_TYPE}" == 'force-publish' ]]; then
+            marker="variants/${ROM_FAMILY}/${flavor}/.force-publish"
             printf 'force-publish run %s (attempt %s)\n' \
-              "${GITHUB_RUN_ID}" "${GITHUB_RUN_ATTEMPT}" > .tmp
-            git add -- .tmp
+              "${GITHUB_RUN_ID}" "${GITHUB_RUN_ATTEMPT}" > "${marker}"
+            git add -- "${marker}"
           fi
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if [[ "${RELEASE_TYPE}" == 'force-publish' ]]; then
printf 'force-publish run %s (attempt %s)\n' \
"${GITHUB_RUN_ID}" "${GITHUB_RUN_ATTEMPT}" > .tmp
git add -- .tmp
fi
if [[ "${RELEASE_TYPE}" == 'force-publish' ]]; then
marker="variants/${ROM_FAMILY}/${flavor}/.force-publish"
printf 'force-publish run %s (attempt %s)\n' \
"${GITHUB_RUN_ID}" "${GITHUB_RUN_ATTEMPT}" > "${marker}"
git add -- "${marker}"
fi
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.github/workflows/build-rom.yml around lines 245 - 249, Update the
force-publish block in the workflow to write its marker to a distinct file path
inside the published tree, rather than the `.tmp` directory used by WORKDIR. Use
the same new marker path for both the printf redirection and the subsequent git
add operation.

if ! git diff-index --quiet HEAD; then
git commit -m "release(${current_commit}): publish ${ROM_FAMILY} ${GRAPHENEOS_VERSION} ${MODULE_SELECTION_FINGERPRINT}"
git push origin gh-pages
fi
Comment on lines +250 to +253

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick win

Serialize gh-pages publication or handle the push race.

The scheduled triggers run every six hours for both ROM families, and each caller can run concurrently for different devices. Two jobs that reach line 252 at the same time produce a non-fast-forward push, and the second job fails after a full build. Add a concurrency group keyed on the gh-pages branch to the reusable workflow, or retry the commit after git pull --rebase origin gh-pages.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.github/workflows/build-rom.yml around lines 250 - 253, Serialize gh-pages
publication in the reusable workflow by adding a concurrency group keyed to the
gh-pages branch, preventing simultaneous jobs from reaching the git push in the
release publication block. Keep the existing commit and push behavior unchanged
once the job acquires the group.

53 changes: 53 additions & 0 deletions .github/workflows/phase3-build-only.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
name: Phase 3 build-only acceptance

on:
workflow_dispatch:
inputs:
rom-family:
description: ROM family to exercise
required: true
type: choice
options:
- grapheneos
- lineageos
default: grapheneos
device-id:
description: Device code name
required: true
default: shiba
root:
description: Add root to the build
required: false
type: boolean
default: false
magisk-preinit-device:
description: Magisk preinit device for rooted builds
required: false
default: sda10
update-channel:
description: Provider update channel
required: true
default: stable
compatible-sepolicy-patching:
description: Enable compatible SELinux patching
required: false
type: boolean
default: false

permissions:
contents: read

jobs:
build-only:
uses: ./.github/workflows/build-rom.yml
Comment on lines +37 to +42

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🔴 Critical | ⚡ Quick win

🧩 Analysis chain

🌐 Web query:

GitHub Actions reusable workflow called job permissions cannot exceed caller permissions error

💡 Result:

The error indicating that a reusable workflow cannot exceed caller permissions occurs because GitHub Actions enforces a strict security model where the calling workflow acts as a permission ceiling [1][2]. A reusable workflow cannot elevate its permissions beyond what has been explicitly granted by the job that calls it [1][2]. If a reusable workflow (the "worker") attempts to use the GITHUB_TOKEN for an action (e.g., creating a PR comment or pushing to a branch) that requires a permission level higher than what the caller provides, the workflow will fail, often resulting in a startup_failure or a specific validation error [2][3][4]. To resolve this issue, you must configure the job in the caller workflow to grant the necessary permissions [2][5]. How to Fix: 1. Identify the permissions required by your reusable workflow (e.g., contents: write, pull-requests: write) [3]. 2. In your caller workflow, add a permissions block to the specific job that calls the reusable workflow [6][2]. 3. Ensure these permissions are equal to or greater than what the reusable workflow requires [1][7]. Example of a correct caller job configuration: jobs: call-reusable-workflow: uses:./.github/workflows/my-reusable-workflow.yml permissions: contents: read pull-requests: write Key Points: - Permissions are not additive; the caller determines the maximum scope [1][2]. - If you do not specify a permissions block in the caller job, the workflow will use default permissions, which are often too restrictive for complex tasks [2][5]. - Reusable workflows can only downgrade (restrict) permissions further, never elevate them [1][2]. If you are using tools like gh-aw (GitHub Actions Workflows) or similar automation, be aware that some build systems may generate incorrect permission envelopes, and you may need to ensure your CI/CD configuration explicitly handles the union of required permissions [8][9][10].

Citations:


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf 'Files matching workflow names:\n'
fd -a '^(phase3-build-only|build-rom)\.yml$' .github/workflows || true

printf '\nRelevant workflow snippets:\n'
for f in .github/workflows/phase3-build-only.yml .github/workflows/build-rom.yml; do
  if [ -f "$f" ]; then
    echo "===== $f ($(wc -l < "$f") lines) ====="
    sed -n '1,120p' "$f" | cat -n
  fi
done

Repository: 0cwa/PixeneOS

Length of output: 6402


🏁 Script executed:

#!/bin/bash
set -euo pipefail

python3 - <<'PY'
from pathlib import Path
import re

caller = Path(".github/workflows/phase3-build-only.yml").read_text()
callee = Path(".github/workflows/build-rom.yml").read_text()

caller_perms = re.search(
    r'^permissions:\s*\n([^\S\n]*\n(^[^\S\n]*\b[a-z-]+:(\s*.*)?\s*\n?)*)',
    caller,
    re.M,
)
callee_build_perms = re.search(
    r'^jobs:\s*\n.*?^(build):\s*\n.*?^    permissions:\s*\n(^[^\S\n]*\b[a-z-]+:(\s*.*)?\s*\n?)',
    callee,
    re.M | re.S,
) if re.search(r'^jobs:', callee) else None

def get(job_perms):
    if not job_perms:
        return {}
    return {
        k.strip(): v.strip().lower()
        for k, v in re.findall(r'^\s*([a-z-]+):\s*(.*?)\s*$', job_perms.group(2), re.M)
    }

print("caller_perms:", get(caller_perms) if caller_perms else None)
print("build_job_permissions:", get(callee_build_perms))
print("uses_reusable_workflow:", bool(re.search(r'uses:\s*\./.github/workflows/build-rom\.yml', caller) and re.search(r'on:\s*\n\s*\n?\s*workflow_call:', callee)))
PY

Repository: 0cwa/PixeneOS

Length of output: 225


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf 'build-rom.yml permissions context:\n'
rg -n -C 8 '^\s*permissions:|contents:' .github/workflows/build-rom.yml

printf '\nphase3-build-only.yml permissions context:\n'
rg -n -C 8 '^\s*permissions:|contents:' .github/workflows/phase3-build-only.yml

Repository: 0cwa/PixeneOS

Length of output: 1262


Grant the reusable job the permission it needs.

phase3-build-only.yml supplies the permission ceiling with contents: read, but .github/workflows/build-rom.yml declares contents: write on its build job. Caller permissions cannot be narrower than the called job requests. Grant contents: write in the calling job, or move the write scope into the publication steps.

🐛 Proposed fix
 permissions:
-  contents: read
+  # build-rom.yml requests contents: write for release publication.
+  contents: write
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
permissions:
contents: read
jobs:
build-only:
uses: ./.github/workflows/build-rom.yml
permissions:
# build-rom.yml requests contents: write for release publication.
contents: write
jobs:
build-only:
uses: ./.github/workflows/build-rom.yml
🧰 Tools
🪛 zizmor (1.29.0)

[warning] 42-42: secrets unconditionally inherited by called workflow (secrets-inherit): this reusable workflow

(secrets-inherit)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.github/workflows/phase3-build-only.yml around lines 37 - 42, Update the
calling workflow’s permissions for the reusable build-only job so its contents
scope allows write access required by the called workflow’s build job. Change
the permissions block adjacent to the build-only job from read-only to contents:
write, preserving the existing reusable workflow invocation.

with:
rom-family: ${{ inputs.rom-family }}
device-id: ${{ inputs.device-id }}
root: ${{ inputs.root }}
magisk-preinit-device: ${{ inputs.magisk-preinit-device }}
update-channel: ${{ inputs.update-channel }}
compatible-sepolicy-patching: ${{ inputs.compatible-sepolicy-patching }}
allow-unauthorized-adb: false
release-type: build-only
publish: false
secrets: inherit
Loading
Loading