From 8c94a67525a67ba0d36ebd5535333e29bd743e25 Mon Sep 17 00:00:00 2001 From: LiHaohua Date: Tue, 30 Jun 2026 17:47:07 +0800 Subject: [PATCH 1/4] Serve .deb from Releases/OSS instead of Git LFS The org LFS budget is exhausted, which blocked CI and broke deploys. Move .deb distribution off LFS entirely: - .deb are stored in the long-lived `apt-pool` GitHub Release (seeded with the current 13 packages, byte-identical / sha256-verified) and mirrored to OSS. They are no longer kept in git; each package carries a __.deb.release.json manifest (url + sha256 + size). - update-index.yml: no LFS. Promote pending manifests into apt-pool, build the Pages index with Filename -> release URL, and stage an OSS tree (relative Filename, .deb included) for the existing Aliyun OSS sync. - validate-pr.yml: split into an untrusted `pull_request` stage (download the manifest .deb, verify sha256 + dpkg checks, upload result artifact) and a privileged validate-pr-comment.yml (`workflow_run`) that posts the comment. Fixes the previous pull_request_target + checkout-PR-head pattern. - generate-registry.py: pass through absolute Filename URLs. - .gitattributes/.gitignore: drop LFS, ignore *.deb. Distribution: global apt -> Releases CDN, China apt -> OSS. git stays tiny. Co-authored-by: Cursor --- .gitattributes | 4 +- .github/workflows/update-index.yml | 99 ++++++- .github/workflows/validate-pr-comment.yml | 47 ++++ .github/workflows/validate-pr.yml | 243 +++++++++--------- .gitignore | 7 + README.md | 87 ++++++- oss/generate-registry.py | 5 + pool/main/2048/2048_0.1.0_arm64.deb | 3 - .../2048/2048_0.1.0_arm64.deb.release.json | 9 + pool/main/calendar/calendar_0.1.1_arm64.deb | 3 - .../calendar_0.1.1_arm64.deb.release.json | 9 + pool/main/calendar/calendar_0.1.2_arm64.deb | 3 - .../calendar_0.1.2_arm64.deb.release.json | 9 + pool/main/calendar/calendar_0.1.3_arm64.deb | 3 - .../calendar_0.1.3_arm64.deb.release.json | 9 + ...erzero-pwnagotchi_0.1.1-m5stack1_arm64.deb | 3 - ...tchi_0.1.1-m5stack1_arm64.deb.release.json | 9 + ...puterzero-xiaozhi_0.2.0-m5stack1_arm64.deb | 3 - ...ozhi_0.2.0-m5stack1_arm64.deb.release.json | 9 + ...puterzero-xiaozhi_0.2.1-m5stack1_arm64.deb | 3 - ...ozhi_0.2.1-m5stack1_arm64.deb.release.json | 9 + ...puterzero-xiaozhi_0.2.2-m5stack1_arm64.deb | 3 - ...ozhi_0.2.2-m5stack1_arm64.deb.release.json | 9 + pool/main/flint/flint_0.0.25_arm64.deb | 3 - .../flint/flint_0.0.25_arm64.deb.release.json | 9 + ...x_0.2.0-1.lofibox23_arm64.deb.release.json | 9 + .../lofibox_0.2.0-1~lofibox23_arm64.deb | 3 - pool/main/nc2000/nc2000_1.0.0_arm64.deb | 3 - .../nc2000_1.0.0_arm64.deb.release.json | 9 + pool/main/nc2000/nc2000_1.0.2_arm64.deb | 3 - .../nc2000_1.0.2_arm64.deb.release.json | 9 + pool/main/nc2000/nc2000_1.0.3_arm64.deb | 3 - .../nc2000_1.0.3_arm64.deb.release.json | 9 + 33 files changed, 457 insertions(+), 191 deletions(-) create mode 100644 .github/workflows/validate-pr-comment.yml delete mode 100644 pool/main/2048/2048_0.1.0_arm64.deb create mode 100644 pool/main/2048/2048_0.1.0_arm64.deb.release.json delete mode 100644 pool/main/calendar/calendar_0.1.1_arm64.deb create mode 100644 pool/main/calendar/calendar_0.1.1_arm64.deb.release.json delete mode 100644 pool/main/calendar/calendar_0.1.2_arm64.deb create mode 100644 pool/main/calendar/calendar_0.1.2_arm64.deb.release.json delete mode 100644 pool/main/calendar/calendar_0.1.3_arm64.deb create mode 100644 pool/main/calendar/calendar_0.1.3_arm64.deb.release.json delete mode 100644 pool/main/cardputerzero-pwnagotchi/cardputerzero-pwnagotchi_0.1.1-m5stack1_arm64.deb create mode 100644 pool/main/cardputerzero-pwnagotchi/cardputerzero-pwnagotchi_0.1.1-m5stack1_arm64.deb.release.json delete mode 100644 pool/main/cardputerzero-xiaozhi/cardputerzero-xiaozhi_0.2.0-m5stack1_arm64.deb create mode 100644 pool/main/cardputerzero-xiaozhi/cardputerzero-xiaozhi_0.2.0-m5stack1_arm64.deb.release.json delete mode 100644 pool/main/cardputerzero-xiaozhi/cardputerzero-xiaozhi_0.2.1-m5stack1_arm64.deb create mode 100644 pool/main/cardputerzero-xiaozhi/cardputerzero-xiaozhi_0.2.1-m5stack1_arm64.deb.release.json delete mode 100644 pool/main/cardputerzero-xiaozhi/cardputerzero-xiaozhi_0.2.2-m5stack1_arm64.deb create mode 100644 pool/main/cardputerzero-xiaozhi/cardputerzero-xiaozhi_0.2.2-m5stack1_arm64.deb.release.json delete mode 100644 pool/main/flint/flint_0.0.25_arm64.deb create mode 100644 pool/main/flint/flint_0.0.25_arm64.deb.release.json create mode 100644 pool/main/lofibox/lofibox_0.2.0-1.lofibox23_arm64.deb.release.json delete mode 100644 pool/main/lofibox/lofibox_0.2.0-1~lofibox23_arm64.deb delete mode 100644 pool/main/nc2000/nc2000_1.0.0_arm64.deb create mode 100644 pool/main/nc2000/nc2000_1.0.0_arm64.deb.release.json delete mode 100644 pool/main/nc2000/nc2000_1.0.2_arm64.deb create mode 100644 pool/main/nc2000/nc2000_1.0.2_arm64.deb.release.json delete mode 100644 pool/main/nc2000/nc2000_1.0.3_arm64.deb create mode 100644 pool/main/nc2000/nc2000_1.0.3_arm64.deb.release.json diff --git a/.gitattributes b/.gitattributes index 3a19b95..b2b8a76 100644 --- a/.gitattributes +++ b/.gitattributes @@ -1 +1,3 @@ -*.deb filter=lfs diff=lfs merge=lfs -text +# No Git LFS. .deb binaries are not stored in git at all — they live in the +# apt-pool GitHub Release and the Aliyun OSS mirror, referenced by +# __.deb.release.json manifests. diff --git a/.github/workflows/update-index.yml b/.github/workflows/update-index.yml index 4d82947..cf51ff9 100644 --- a/.github/workflows/update-index.yml +++ b/.github/workflows/update-index.yml @@ -21,7 +21,7 @@ on: default: 'packages' permissions: - contents: read + contents: write # promote .deb assets into the apt-pool release pages: write id-token: write @@ -29,6 +29,14 @@ concurrency: group: pages cancel-in-progress: true +env: + # Long-lived "pool" release that stores every .deb as an asset. + # .deb files are NOT kept in git; they live here and are distributed via the + # Releases CDN (unlimited bandwidth). Each package directory in git carries a + # __.deb.release.json manifest describing where the binary + # was first uploaded (a contributor fork release) so CI can promote it here. + POOL_TAG: apt-pool + jobs: build-and-deploy: runs-on: ubuntu-latest @@ -37,22 +45,70 @@ jobs: url: ${{ steps.deployment.outputs.page_url }} steps: - - name: Checkout with LFS + - name: Checkout uses: actions/checkout@v4 - with: - lfs: true - name: Install tools - run: sudo apt-get update && sudo apt-get install -y dpkg-dev + run: sudo apt-get update && sudo apt-get install -y dpkg-dev rsync jq - - name: Build APT repository + - name: Promote pending .deb manifests into the apt-pool release + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} run: | + set -euo pipefail + # Existing assets in the pool release (by name). + gh release view "$POOL_TAG" --json assets --jq '.assets[].name' 2>/dev/null | sort > /tmp/pool-assets.txt || : > /tmp/pool-assets.txt + + # Each manifest declares the canonical filename + source URL + sha256. + shopt -s nullglob globstar + for manifest in pool/main/**/*.deb.release.json; do + name=$(jq -r '.filename' "$manifest") + url=$(jq -r '.url' "$manifest") + want_sha=$(jq -r '.sha256' "$manifest") + if [ -z "$name" ] || [ "$name" = "null" ]; then + echo "::error::manifest $manifest missing .filename"; exit 1 + fi + if grep -qxF "$name" /tmp/pool-assets.txt; then + echo "already in pool: $name" + continue + fi + echo "promoting $name from $url" + tmp="/tmp/promote-$name" + curl -fL --retry 3 --max-time 600 -o "$tmp" "$url" + got_sha=$(sha256sum "$tmp" | awk '{print $1}') + if [ -n "$want_sha" ] && [ "$want_sha" != "null" ] && [ "$got_sha" != "$want_sha" ]; then + echo "::error::sha256 mismatch for $name (want $want_sha got $got_sha)"; exit 1 + fi + gh release upload "$POOL_TAG" "$tmp" --clobber + rm -f "$tmp" + done + + - name: Download .deb assets from the apt-pool release + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + mkdir -p release-debs + gh release download "$POOL_TAG" -D release-debs --pattern '*.deb' --clobber + + - name: Build APT repository (Pages metadata only; .deb served from Releases) + run: | + set -euo pipefail + REL_BASE="${{ github.server_url }}/${{ github.repository }}/releases/download/${POOL_TAG}" mkdir -p public/dists/stable/main/binary-arm64 - cp -a pool public/ - cd public - dpkg-scanpackages --multiversion pool/ > dists/stable/main/binary-arm64/Packages - gzip -k -f dists/stable/main/binary-arm64/Packages - cd dists/stable + + # Pages serves metadata + screenshots + meta.json, but NOT the .deb files. + rsync -a --exclude='*.deb' pool/ public/pool/ + + # Scan the .deb downloaded from the release; rewrite Filename to the + # release asset download URL (flat asset name == basename). + ( cd release-debs && dpkg-scanpackages --multiversion . /dev/null ) 2>/dev/null \ + | awk -v base="$REL_BASE" ' + /^Filename:/ { n = split($2, a, "/"); print "Filename: " base "/" a[n]; next } + { print } + ' > public/dists/stable/main/binary-arm64/Packages + gzip -k -f public/dists/stable/main/binary-arm64/Packages + + cd public/dists/stable printf '%s\n' \ "Origin: CardputerZero" \ "Label: CardputerZero AppStore" \ @@ -74,6 +130,25 @@ jobs: printf " %s %s %s\n" "$hash" "$size" "$f" >> Release done + - name: Stage OSS tree (China mirror serves .deb locally with relative paths) + if: ${{ vars.PACKAGES_OSS_SYNC_ENABLED == 'true' || inputs.sync_oss == true }} + run: | + set -euo pipefail + # OSS hosts the full repo: copy metadata, drop the .deb into pool/ by + # package name (filename is __.deb), and + # regenerate Packages with RELATIVE Filename so apt/registry resolve + # the binaries from OSS itself. + rsync -a public/ oss-public/ + for deb in release-debs/*.deb; do + base=$(basename "$deb") + pkg=${base%%_*} + mkdir -p "oss-public/pool/main/$pkg" + cp -f "$deb" "oss-public/pool/main/$pkg/$base" + done + ( cd oss-public && dpkg-scanpackages --multiversion pool/ /dev/null ) 2>/dev/null \ + > oss-public/dists/stable/main/binary-arm64/Packages + gzip -k -f oss-public/dists/stable/main/binary-arm64/Packages + - name: Setup ossutil if: ${{ vars.PACKAGES_OSS_SYNC_ENABLED == 'true' || inputs.sync_oss == true }} uses: manyuanrong/setup-ossutil@v3.0 @@ -86,7 +161,7 @@ jobs: if: ${{ vars.PACKAGES_OSS_SYNC_ENABLED == 'true' || inputs.sync_oss == true }} env: OSS_SYNC_ENABLED: 'true' - OSS_PUBLIC_DIR: public + OSS_PUBLIC_DIR: oss-public OSS_ENDPOINT: ${{ vars.PACKAGES_OSS_ENDPOINT || 'oss-cn-shenzhen.aliyuncs.com' }} OSS_BUCKET: ${{ vars.PACKAGES_OSS_BUCKET || 'cardputer-zero-repo' }} OSS_PREFIX: ${{ inputs.oss_prefix || vars.PACKAGES_OSS_PREFIX || 'packages' }} diff --git a/.github/workflows/validate-pr-comment.yml b/.github/workflows/validate-pr-comment.yml new file mode 100644 index 0000000..ba8e934 --- /dev/null +++ b/.github/workflows/validate-pr-comment.yml @@ -0,0 +1,47 @@ +name: Comment Package Validation + +# Stage 2 of 2 (privileged): triggered when "Validate Package PR" finishes. +# Runs in the base-repo context with write access, downloads the result +# artifact produced by the untrusted stage, and posts it as a PR comment. +# It only reads data from the artifact and never executes PR-provided code. + +on: + workflow_run: + workflows: ["Validate Package PR"] + types: [completed] + +permissions: + contents: read + pull-requests: write + actions: read + +jobs: + comment: + runs-on: ubuntu-latest + if: github.event.workflow_run.event == 'pull_request' + steps: + - name: Download validation artifact + uses: actions/download-artifact@v4 + with: + name: pr-validation + path: out + run-id: ${{ github.event.workflow_run.id }} + github-token: ${{ secrets.GITHUB_TOKEN }} + + - name: Post comment on PR + uses: actions/github-script@v7 + with: + script: | + const fs = require('fs'); + const prNumber = parseInt(fs.readFileSync('out/pr-number.txt', 'utf8').trim(), 10); + const body = fs.readFileSync('out/result.md', 'utf8'); + if (!Number.isInteger(prNumber)) { + core.setFailed('Invalid PR number in artifact'); + return; + } + await github.rest.issues.createComment({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: prNumber, + body, + }); diff --git a/.github/workflows/validate-pr.yml b/.github/workflows/validate-pr.yml index da2bda5..9f5167c 100644 --- a/.github/workflows/validate-pr.yml +++ b/.github/workflows/validate-pr.yml @@ -1,160 +1,153 @@ name: Validate Package PR +# Stage 1 of 2 (untrusted): runs in the fork/PR context with a READ-ONLY token +# and NO secrets. It downloads the .deb declared by each *.deb.release.json +# manifest, verifies it, and writes the outcome to an artifact. A separate +# privileged workflow (validate-pr-comment.yml, workflow_run) posts the comment. +# This avoids the pull_request_target + checkout-PR-head footgun. + on: - pull_request_target: + pull_request: branches: [main] - paths: ['pool/**/*.deb'] + paths: + - 'pool/**/*.deb.release.json' + - 'pool/**/*.deb' + +permissions: + contents: read jobs: validate: runs-on: ubuntu-latest - permissions: - contents: read - pull-requests: write - steps: - - name: Checkout PR branch with LFS + - name: Checkout PR code (untrusted, data only) uses: actions/checkout@v4 with: - ref: ${{ github.event.pull_request.head.sha }} - lfs: true fetch-depth: 0 - - name: Get changed files - id: changes - run: | - git diff --name-only --diff-filter=A ${{ github.event.pull_request.base.sha }}..${{ github.event.pull_request.head.sha }} \ - | grep '\.deb$' > added_debs.txt || true - git diff --name-only --diff-filter=D ${{ github.event.pull_request.base.sha }}..${{ github.event.pull_request.head.sha }} \ - | grep '\.deb$' > deleted_debs.txt || true - echo "added=$(wc -l < added_debs.txt | tr -d ' ')" >> "$GITHUB_OUTPUT" - echo "deleted=$(wc -l < deleted_debs.txt | tr -d ' ')" >> "$GITHUB_OUTPUT" - - - name: Validate added packages - if: steps.changes.outputs.added != '0' - id: validate-added + - name: Install tools + run: sudo apt-get update && sudo apt-get install -y dpkg-dev jq + + - name: Validate manifests and packages + id: validate + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + BASE_SHA: ${{ github.event.pull_request.base.sha }} + HEAD_SHA: ${{ github.event.pull_request.head.sha }} + PR_AUTHOR: ${{ github.event.pull_request.user.login }} run: | + set -uo pipefail ERRORS="" - while IFS= read -r deb; do - echo "=== Validating: $deb ===" + mkdir -p /tmp/out + echo "${{ github.event.pull_request.number }}" > /tmp/out/pr-number.txt - # 1. Path must be under pool/main/ - if [[ "$deb" != pool/main/* ]]; then - ERRORS="${ERRORS}❌ \`$deb\` is outside pool/main/\n" - continue - fi + # Reject raw .deb committed to git (must use the release-manifest flow). + if git diff --name-only --diff-filter=A "$BASE_SHA".."$HEAD_SHA" | grep -q '\.deb$'; then + ERRORS="${ERRORS}- ❌ Raw \`.deb\` files must not be committed to git. Upload the binary to a Release in your fork and submit a \`*.deb.release.json\` manifest (use \`czdev publish\`).\n" + fi - # 2. Directory name must match Package field - DIR_PKG=$(echo "$deb" | cut -d/ -f3) - CTRL_PKG=$(dpkg-deb -f "$deb" Package) - if [[ "$DIR_PKG" != "$CTRL_PKG" ]]; then - ERRORS="${ERRORS}❌ Directory name '$DIR_PKG' != Package field '$CTRL_PKG'\n" - fi + ADDED_MANIFESTS=$(git diff --name-only --diff-filter=A "$BASE_SHA".."$HEAD_SHA" | grep '\.deb\.release\.json$' || true) - # 3. Must contain .desktop file - if ! dpkg-deb -c "$deb" | grep -q '\.desktop$'; then - ERRORS="${ERRORS}❌ \`$deb\` has no .desktop file. All apps must include one.\n" - fi + AUTHOR_EMAIL=$(gh api "users/$PR_AUTHOR" --jq '.email // empty' 2>/dev/null || true) + NOREPLY="${PR_AUTHOR}@users.noreply.github.com" - # 4. Valid package name - if ! echo "$CTRL_PKG" | grep -qP '^[a-z0-9][a-z0-9.+\-]+$'; then - ERRORS="${ERRORS}❌ Invalid package name: '$CTRL_PKG'\n" + for manifest in $ADDED_MANIFESTS; do + echo "=== $manifest ===" + if [[ "$manifest" != pool/main/* ]]; then + ERRORS="${ERRORS}- ❌ \`$manifest\` is outside \`pool/main/\`\n"; continue fi - - # 5. Maintainer email vs PR author - MAINTAINER=$(dpkg-deb -f "$deb" Maintainer) - MAINT_EMAIL=$(echo "$MAINTAINER" | grep -oP '<\K[^>]+' || echo "$MAINTAINER") - PR_AUTHOR="${{ github.event.pull_request.user.login }}" - NOREPLY="${PR_AUTHOR}@users.noreply.github.com" - - AUTHOR_EMAIL=$(gh api "users/$PR_AUTHOR" --jq '.email // empty' 2>/dev/null || true) - - if [[ "$MAINT_EMAIL" != "$NOREPLY" ]] && [[ "$MAINT_EMAIL" != "$AUTHOR_EMAIL" ]]; then - ERRORS="${ERRORS}❌ Maintainer email '${MAINT_EMAIL}' does not match PR author '${PR_AUTHOR}' (expected ${NOREPLY} or ${AUTHOR_EMAIL})\n" + url=$(jq -r '.url // empty' "$manifest") + sha=$(jq -r '.sha256 // empty' "$manifest") + fname=$(jq -r '.filename // empty' "$manifest") + dir_pkg=$(echo "$manifest" | cut -d/ -f3) + if [[ -z "$url" || -z "$sha" || -z "$fname" ]]; then + ERRORS="${ERRORS}- ❌ \`$manifest\` must contain \`url\`, \`sha256\`, \`filename\`\n"; continue fi - # 6. Version must be newer than existing - EXISTING_PKG=$(find pool/main/"$DIR_PKG"/ -name "${CTRL_PKG}_*_*.deb" 2>/dev/null | grep -v "$deb" | sort -V | tail -1 || true) - if [[ -n "$EXISTING_PKG" ]]; then - EXISTING_VER=$(dpkg-deb -f "$EXISTING_PKG" Version) - NEW_VER=$(dpkg-deb -f "$deb" Version) - if dpkg --compare-versions "$NEW_VER" le "$EXISTING_VER" 2>/dev/null; then - ERRORS="${ERRORS}❌ Version $NEW_VER is not newer than existing $EXISTING_VER\n" - fi + deb="/tmp/$fname" + if ! curl -fL --retry 3 --max-time 600 -o "$deb" "$url"; then + ERRORS="${ERRORS}- ❌ Could not download \`$fname\` from \`$url\`\n"; continue fi - - if [[ -z "$ERRORS" ]]; then - echo "✓ $deb passed all checks" + got=$(sha256sum "$deb" | awk '{print $1}') + if [[ "$got" != "$sha" ]]; then + ERRORS="${ERRORS}- ❌ \`$fname\` sha256 mismatch (manifest \`$sha\`, downloaded \`$got\`)\n"; continue fi - done < added_debs.txt - if [[ -n "$ERRORS" ]]; then - echo "VALIDATION_ERRORS<> "$GITHUB_ENV" - echo -e "$ERRORS" >> "$GITHUB_ENV" - echo "EOF" >> "$GITHUB_ENV" - echo -e "$ERRORS" - exit 1 - fi - env: - GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} - - - name: Validate deleted packages (ownership check) - if: steps.changes.outputs.deleted != '0' - id: validate-deleted - run: | - ERRORS="" - PR_AUTHOR="${{ github.event.pull_request.user.login }}" - NOREPLY="${PR_AUTHOR}@users.noreply.github.com" - AUTHOR_EMAIL=$(gh api "users/$PR_AUTHOR" --jq '.email // empty' 2>/dev/null || true) + ctrl_pkg=$(dpkg-deb -f "$deb" Package 2>/dev/null || echo "") + new_ver=$(dpkg-deb -f "$deb" Version 2>/dev/null || echo "") + if [[ -z "$ctrl_pkg" ]]; then + ERRORS="${ERRORS}- ❌ \`$fname\` is not a valid .deb\n"; continue + fi + if [[ "$dir_pkg" != "$ctrl_pkg" ]]; then + ERRORS="${ERRORS}- ❌ Directory '$dir_pkg' != Package field '$ctrl_pkg'\n" + fi + if ! dpkg-deb -c "$deb" | grep -q '\.desktop$'; then + ERRORS="${ERRORS}- ❌ \`$fname\` has no .desktop file. All apps must include one.\n" + fi + if ! echo "$ctrl_pkg" | grep -qP '^[a-z0-9][a-z0-9.+\-]+$'; then + ERRORS="${ERRORS}- ❌ Invalid package name: '$ctrl_pkg'\n" + fi + expected_name="${ctrl_pkg}_${new_ver}_$(dpkg-deb -f "$deb" Architecture).deb" + # GitHub release assets sanitize '~' to '.'; allow either form. + if [[ "$fname" != "$expected_name" && "$fname" != "${expected_name//\~/.}" ]]; then + ERRORS="${ERRORS}- ❌ filename '$fname' should be '$expected_name'\n" + fi - # Checkout base to inspect deleted files - git checkout ${{ github.event.pull_request.base.sha }} -- $(cat deleted_debs.txt) 2>/dev/null || true + maintainer=$(dpkg-deb -f "$deb" Maintainer 2>/dev/null || echo "") + maint_email=$(echo "$maintainer" | grep -oP '<\K[^>]+' || echo "$maintainer") + if [[ "$maint_email" != "$NOREPLY" && "$maint_email" != "$AUTHOR_EMAIL" ]]; then + ERRORS="${ERRORS}- ❌ Maintainer '${maint_email}' does not match PR author '${PR_AUTHOR}' (expected ${NOREPLY} or your verified email)\n" + fi - while IFS= read -r deb; do - if [[ ! -f "$deb" ]]; then - continue + # Version must be newer than existing manifests for this package on base. + existing_ver="" + for ex in $(git ls-tree -r "$BASE_SHA" --name-only | grep "^pool/main/${dir_pkg}/.*\.deb\.release\.json$" || true); do + v=$(git show "$BASE_SHA:$ex" | jq -r '.version // empty' 2>/dev/null || true) + if [[ -n "$v" ]]; then + if [[ -z "$existing_ver" ]] || dpkg --compare-versions "$v" gt "$existing_ver" 2>/dev/null; then + existing_ver="$v" + fi + fi + done + if [[ -n "$existing_ver" ]] && dpkg --compare-versions "$new_ver" le "$existing_ver" 2>/dev/null; then + ERRORS="${ERRORS}- ❌ Version $new_ver is not newer than existing $existing_ver\n" fi - MAINTAINER=$(dpkg-deb -f "$deb" Maintainer) - MAINT_EMAIL=$(echo "$MAINTAINER" | grep -oP '<\K[^>]+' || echo "$MAINTAINER") - if [[ "$MAINT_EMAIL" != "$NOREPLY" ]] && [[ "$MAINT_EMAIL" != "$AUTHOR_EMAIL" ]]; then - ERRORS="${ERRORS}❌ Cannot delete \`$deb\`: maintainer '${MAINT_EMAIL}' does not match PR author '${PR_AUTHOR}'\n" + if [[ -z "$ERRORS" ]]; then echo "✓ $fname passed"; fi + done + + # Ownership check for deleted manifests. + DELETED=$(git diff --name-only --diff-filter=D "$BASE_SHA".."$HEAD_SHA" | grep '\.deb\.release\.json$' || true) + for manifest in $DELETED; do + dir_pkg=$(echo "$manifest" | cut -d/ -f3) + url=$(git show "$BASE_SHA:$manifest" | jq -r '.url // empty' 2>/dev/null || true) + [[ -z "$url" ]] && continue + deb="/tmp/del-$(basename "$manifest").deb" + if curl -fsL --max-time 600 -o "$deb" "$url" 2>/dev/null; then + maintainer=$(dpkg-deb -f "$deb" Maintainer 2>/dev/null || echo "") + maint_email=$(echo "$maintainer" | grep -oP '<\K[^>]+' || echo "$maintainer") + if [[ "$maint_email" != "$NOREPLY" && "$maint_email" != "$AUTHOR_EMAIL" ]]; then + ERRORS="${ERRORS}- ❌ Cannot delete \`$manifest\`: maintainer '${maint_email}' != PR author '${PR_AUTHOR}'\n" + fi fi - done < deleted_debs.txt + done if [[ -n "$ERRORS" ]]; then - echo "VALIDATION_ERRORS<> "$GITHUB_ENV" - echo -e "$ERRORS" >> "$GITHUB_ENV" - echo "EOF" >> "$GITHUB_ENV" - echo -e "$ERRORS" - exit 1 + { echo "## ❌ Package Validation Failed"; echo; echo -e "$ERRORS"; echo; echo "---"; echo "*If you believe this is an error, contact a maintainer.*"; } > /tmp/out/result.md + echo "status=failure" >> "$GITHUB_OUTPUT" + else + { echo "## ✅ Package Validation Passed"; echo; echo "All checks passed. Ready for maintainer review."; } > /tmp/out/result.md + echo "status=success" >> "$GITHUB_OUTPUT" fi - env: - GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + cat /tmp/out/result.md - - name: Comment on PR with validation errors - if: failure() - uses: actions/github-script@v7 + - name: Upload validation result + if: always() + uses: actions/upload-artifact@v4 with: - script: | - const errors = process.env.VALIDATION_ERRORS || 'Unknown validation failure'; - const body = `## ❌ Package Validation Failed\n\n${errors}\n\n---\n*If you believe this is an error, contact a maintainer.*`; - github.rest.issues.createComment({ - owner: context.repo.owner, - repo: context.repo.repo, - issue_number: context.issue.number, - body: body - }); - - - name: Comment on PR with success - if: success() - uses: actions/github-script@v7 - with: - script: | - const body = `## ✅ Package Validation Passed\n\nAll checks passed. Ready for maintainer review.`; - github.rest.issues.createComment({ - owner: context.repo.owner, - repo: context.repo.repo, - issue_number: context.issue.number, - body: body - }); + name: pr-validation + path: /tmp/out + + - name: Fail job if validation failed + if: steps.validate.outputs.status == 'failure' + run: exit 1 diff --git a/.gitignore b/.gitignore index 8fb8c34..3a26ad4 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,9 @@ dists/ public/ +oss-public/ +release-debs/ + +# .deb binaries are never committed to git. They live in the apt-pool GitHub +# Release (and the Aliyun OSS mirror). Each package carries a +# __.deb.release.json manifest instead. +*.deb diff --git a/README.md b/README.md index 37907b1..60a4221 100644 --- a/README.md +++ b/README.md @@ -2,31 +2,90 @@ APT repository for CardputerZero applications. -- `.deb` files stored in git via LFS -- CI auto-generates APT index and deploys to GitHub Pages -- Optional Aliyun OSS mirror lives under `oss/` and is disabled by default -- CN AppStore registry is generated at the OSS mirror when OSS sync is enabled -- Users download via Pages CDN (100GB/month bandwidth) +`.deb` binaries are **not** stored in git. They live in a long-lived GitHub +Release (`apt-pool`) and are mirrored to Aliyun OSS for China. Git only holds +small metadata: `meta.json`, screenshots, and one +`__.deb.release.json` manifest per package. ## Usage on device +Global (GitHub Pages, `.deb` served from the Releases CDN): + ```bash echo "deb [trusted=yes] https://cardputerzero.github.io/packages stable main" | sudo tee /etc/apt/sources.list.d/cardputerzero.list sudo apt update sudo apt install nc2000 lofibox ``` -## Adding a package +China (Aliyun OSS mirror, `.deb` served from OSS): -1. Place the `.deb` file in `pool/main//` -2. Push to `main` branch -3. CI will auto-regenerate APT index and deploy to Pages +```bash +echo "deb [trusted=yes] https://cardputer-zero-repo.oss-cn-shenzhen.aliyuncs.com/packages stable main" | sudo tee /etc/apt/sources.list.d/cardputerzero.list +sudo apt update +``` -## Structure +## Architecture ``` -pool/main//__.deb ← LFS-tracked binaries (source of truth) -public/ ← built by CI, deployed to GitHub Pages - ├── pool/... ← deb files served via CDN - └── dists/stable/main/binary-arm64/ ← APT metadata +Storage: .deb → GitHub Release "apt-pool" (unlimited bandwidth, no LFS) + → Aliyun OSS mirror (China, relative paths) +Metadata: git → meta.json / screenshots / *.deb.release.json manifests +Index: CI → Pages: dists/ + pool/(metadata only), Filename → release URL + → OSS: full tree with .deb, relative Filename + CN registry +``` + +- **Global / Pages**: `dists/.../Packages` `Filename` points at the `apt-pool` + release asset URL. Pages serves only metadata, so it never approaches the + 1 GB site limit. +- **China / OSS**: CI downloads the `.deb` from the release, uploads them to + OSS, and regenerates `Packages` with **relative** `Filename` so apt and the + AppStore registry resolve binaries from OSS directly. + +## Publishing a package (third-party developers) + +Use `czdev publish` (from `m5stack/CardputerZero-AppBuilder`). With only a +normal GitHub login it will: + +1. Upload your `.deb` as an asset on a Release **in your own fork** of this repo + (your free quota, unlimited download bandwidth — costs the project nothing). +2. Open a PR here containing only `meta.json`, screenshots/icon, and a + `__.deb.release.json` manifest with the fork release URL + + sha256. No `.deb` enters git. + +CI then: + +1. **Validate** (`validate-pr.yml`, untrusted `pull_request` stage): downloads + the `.deb` from the manifest URL, verifies sha256, runs `dpkg-deb` checks, + maintainer/ownership and version checks; results are posted by the privileged + `validate-pr-comment.yml` (`workflow_run`) stage. +2. **Promote on merge** (`update-index.yml`): pulls the approved `.deb` from the + manifest URL into the official `apt-pool` release, rebuilds the index, deploys + Pages, and syncs OSS. + +If a PR is rejected, the project stored nothing — the binary only ever lived in +the contributor's own fork release. + +## Manifest format + +`pool/main//__.deb.release.json`: + +```json +{ + "filename": "nc2000_1.0.3_arm64.deb", + "url": "https://github.com///releases/download//nc2000_1.0.3_arm64.deb", + "sha256": "…", + "size": 20560444, + "package": "nc2000", + "version": "1.0.3", + "architecture": "arm64" +} ``` + +Note: GitHub sanitizes `~` to `.` in release asset names, so `filename` may use +`.` where the Debian version uses `~` (e.g. `lofibox_0.2.0-1.lofibox23_arm64.deb`). + +## OSS mirror + +See `oss/README.md`. Enable by setting repo variable +`PACKAGES_OSS_SYNC_ENABLED=true` and the `OSS_ACCESS_KEY_ID` / +`OSS_ACCESS_KEY_SECRET` secrets. diff --git a/oss/generate-registry.py b/oss/generate-registry.py index 795ae84..bc04822 100644 --- a/oss/generate-registry.py +++ b/oss/generate-registry.py @@ -82,6 +82,11 @@ def quote_path(path: str) -> str: def public_url(base_url: str, path: str) -> str: + # Packages "Filename" may already be an absolute URL (e.g. when the .deb is + # served straight from a GitHub Release). In that case use it verbatim + # instead of prefixing the mirror base URL. + if path.startswith(("http://", "https://")): + return path return f"{base_url.rstrip('/')}/{quote_path(path)}" diff --git a/pool/main/2048/2048_0.1.0_arm64.deb b/pool/main/2048/2048_0.1.0_arm64.deb deleted file mode 100644 index db928f3..0000000 --- a/pool/main/2048/2048_0.1.0_arm64.deb +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:38a124d7433b583969fb9298a9bc4bd89e3f91e1b182d345b67b9dfd759782f7 -size 300860 diff --git a/pool/main/2048/2048_0.1.0_arm64.deb.release.json b/pool/main/2048/2048_0.1.0_arm64.deb.release.json new file mode 100644 index 0000000..1667b7a --- /dev/null +++ b/pool/main/2048/2048_0.1.0_arm64.deb.release.json @@ -0,0 +1,9 @@ +{ + "filename": "2048_0.1.0_arm64.deb", + "url": "https://github.com/CardputerZero/packages/releases/download/apt-pool/2048_0.1.0_arm64.deb", + "sha256": "38a124d7433b583969fb9298a9bc4bd89e3f91e1b182d345b67b9dfd759782f7", + "size": 300860, + "package": "2048", + "version": "0.1.0", + "architecture": "arm64" +} diff --git a/pool/main/calendar/calendar_0.1.1_arm64.deb b/pool/main/calendar/calendar_0.1.1_arm64.deb deleted file mode 100644 index 537329e..0000000 --- a/pool/main/calendar/calendar_0.1.1_arm64.deb +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:eb07815c86f28bd2e45e8e733f509d61b7c22901508f2769c9ec2e982b62bdc5 -size 7229098 diff --git a/pool/main/calendar/calendar_0.1.1_arm64.deb.release.json b/pool/main/calendar/calendar_0.1.1_arm64.deb.release.json new file mode 100644 index 0000000..15109e1 --- /dev/null +++ b/pool/main/calendar/calendar_0.1.1_arm64.deb.release.json @@ -0,0 +1,9 @@ +{ + "filename": "calendar_0.1.1_arm64.deb", + "url": "https://github.com/CardputerZero/packages/releases/download/apt-pool/calendar_0.1.1_arm64.deb", + "sha256": "eb07815c86f28bd2e45e8e733f509d61b7c22901508f2769c9ec2e982b62bdc5", + "size": 7229098, + "package": "calendar", + "version": "0.1.1", + "architecture": "arm64" +} diff --git a/pool/main/calendar/calendar_0.1.2_arm64.deb b/pool/main/calendar/calendar_0.1.2_arm64.deb deleted file mode 100644 index c393404..0000000 --- a/pool/main/calendar/calendar_0.1.2_arm64.deb +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:20b4a1fe8f4e1ca054ae045a372887f43758a024791e531d9a2c5115d06e633f -size 5337480 diff --git a/pool/main/calendar/calendar_0.1.2_arm64.deb.release.json b/pool/main/calendar/calendar_0.1.2_arm64.deb.release.json new file mode 100644 index 0000000..5592500 --- /dev/null +++ b/pool/main/calendar/calendar_0.1.2_arm64.deb.release.json @@ -0,0 +1,9 @@ +{ + "filename": "calendar_0.1.2_arm64.deb", + "url": "https://github.com/CardputerZero/packages/releases/download/apt-pool/calendar_0.1.2_arm64.deb", + "sha256": "20b4a1fe8f4e1ca054ae045a372887f43758a024791e531d9a2c5115d06e633f", + "size": 5337480, + "package": "calendar", + "version": "0.1.2", + "architecture": "arm64" +} diff --git a/pool/main/calendar/calendar_0.1.3_arm64.deb b/pool/main/calendar/calendar_0.1.3_arm64.deb deleted file mode 100644 index d3fc299..0000000 --- a/pool/main/calendar/calendar_0.1.3_arm64.deb +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:bd799bfebbd87a37ca120bdeeedc8697c3fb6467c358e386d86b6e9da87d3098 -size 722812 diff --git a/pool/main/calendar/calendar_0.1.3_arm64.deb.release.json b/pool/main/calendar/calendar_0.1.3_arm64.deb.release.json new file mode 100644 index 0000000..cbe0d30 --- /dev/null +++ b/pool/main/calendar/calendar_0.1.3_arm64.deb.release.json @@ -0,0 +1,9 @@ +{ + "filename": "calendar_0.1.3_arm64.deb", + "url": "https://github.com/CardputerZero/packages/releases/download/apt-pool/calendar_0.1.3_arm64.deb", + "sha256": "bd799bfebbd87a37ca120bdeeedc8697c3fb6467c358e386d86b6e9da87d3098", + "size": 722812, + "package": "calendar", + "version": "0.1.3", + "architecture": "arm64" +} diff --git a/pool/main/cardputerzero-pwnagotchi/cardputerzero-pwnagotchi_0.1.1-m5stack1_arm64.deb b/pool/main/cardputerzero-pwnagotchi/cardputerzero-pwnagotchi_0.1.1-m5stack1_arm64.deb deleted file mode 100644 index a3e9d9b..0000000 --- a/pool/main/cardputerzero-pwnagotchi/cardputerzero-pwnagotchi_0.1.1-m5stack1_arm64.deb +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:0d4b2b0d2afe8fd048dc4058103f0ff4a2b3a364f5e968f6368a40f250b0a73e -size 32801896 diff --git a/pool/main/cardputerzero-pwnagotchi/cardputerzero-pwnagotchi_0.1.1-m5stack1_arm64.deb.release.json b/pool/main/cardputerzero-pwnagotchi/cardputerzero-pwnagotchi_0.1.1-m5stack1_arm64.deb.release.json new file mode 100644 index 0000000..3aa6a30 --- /dev/null +++ b/pool/main/cardputerzero-pwnagotchi/cardputerzero-pwnagotchi_0.1.1-m5stack1_arm64.deb.release.json @@ -0,0 +1,9 @@ +{ + "filename": "cardputerzero-pwnagotchi_0.1.1-m5stack1_arm64.deb", + "url": "https://github.com/CardputerZero/packages/releases/download/apt-pool/cardputerzero-pwnagotchi_0.1.1-m5stack1_arm64.deb", + "sha256": "0d4b2b0d2afe8fd048dc4058103f0ff4a2b3a364f5e968f6368a40f250b0a73e", + "size": 32801896, + "package": "cardputerzero-pwnagotchi", + "version": "0.1.1-m5stack1", + "architecture": "arm64" +} diff --git a/pool/main/cardputerzero-xiaozhi/cardputerzero-xiaozhi_0.2.0-m5stack1_arm64.deb b/pool/main/cardputerzero-xiaozhi/cardputerzero-xiaozhi_0.2.0-m5stack1_arm64.deb deleted file mode 100644 index 580a297..0000000 --- a/pool/main/cardputerzero-xiaozhi/cardputerzero-xiaozhi_0.2.0-m5stack1_arm64.deb +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:03765f84c02014355820b84b07eb920a3b1a610291ea4015c626bdafc46bc4f7 -size 25877702 diff --git a/pool/main/cardputerzero-xiaozhi/cardputerzero-xiaozhi_0.2.0-m5stack1_arm64.deb.release.json b/pool/main/cardputerzero-xiaozhi/cardputerzero-xiaozhi_0.2.0-m5stack1_arm64.deb.release.json new file mode 100644 index 0000000..1b562b6 --- /dev/null +++ b/pool/main/cardputerzero-xiaozhi/cardputerzero-xiaozhi_0.2.0-m5stack1_arm64.deb.release.json @@ -0,0 +1,9 @@ +{ + "filename": "cardputerzero-xiaozhi_0.2.0-m5stack1_arm64.deb", + "url": "https://github.com/CardputerZero/packages/releases/download/apt-pool/cardputerzero-xiaozhi_0.2.0-m5stack1_arm64.deb", + "sha256": "03765f84c02014355820b84b07eb920a3b1a610291ea4015c626bdafc46bc4f7", + "size": 25877702, + "package": "cardputerzero-xiaozhi", + "version": "0.2.0-m5stack1", + "architecture": "arm64" +} diff --git a/pool/main/cardputerzero-xiaozhi/cardputerzero-xiaozhi_0.2.1-m5stack1_arm64.deb b/pool/main/cardputerzero-xiaozhi/cardputerzero-xiaozhi_0.2.1-m5stack1_arm64.deb deleted file mode 100644 index b35d8e7..0000000 --- a/pool/main/cardputerzero-xiaozhi/cardputerzero-xiaozhi_0.2.1-m5stack1_arm64.deb +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:d32a06a6ca416e01f06118ca09321fbaa1f81cd3739c7aa551f33e6ec7d66d55 -size 25694064 diff --git a/pool/main/cardputerzero-xiaozhi/cardputerzero-xiaozhi_0.2.1-m5stack1_arm64.deb.release.json b/pool/main/cardputerzero-xiaozhi/cardputerzero-xiaozhi_0.2.1-m5stack1_arm64.deb.release.json new file mode 100644 index 0000000..225f002 --- /dev/null +++ b/pool/main/cardputerzero-xiaozhi/cardputerzero-xiaozhi_0.2.1-m5stack1_arm64.deb.release.json @@ -0,0 +1,9 @@ +{ + "filename": "cardputerzero-xiaozhi_0.2.1-m5stack1_arm64.deb", + "url": "https://github.com/CardputerZero/packages/releases/download/apt-pool/cardputerzero-xiaozhi_0.2.1-m5stack1_arm64.deb", + "sha256": "d32a06a6ca416e01f06118ca09321fbaa1f81cd3739c7aa551f33e6ec7d66d55", + "size": 25694064, + "package": "cardputerzero-xiaozhi", + "version": "0.2.1-m5stack1", + "architecture": "arm64" +} diff --git a/pool/main/cardputerzero-xiaozhi/cardputerzero-xiaozhi_0.2.2-m5stack1_arm64.deb b/pool/main/cardputerzero-xiaozhi/cardputerzero-xiaozhi_0.2.2-m5stack1_arm64.deb deleted file mode 100644 index 71141c3..0000000 --- a/pool/main/cardputerzero-xiaozhi/cardputerzero-xiaozhi_0.2.2-m5stack1_arm64.deb +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:53c6dea5d7dc671618d5edfbe894522c72f5efb38ff545a9eeec7a15327f7ea4 -size 25851200 diff --git a/pool/main/cardputerzero-xiaozhi/cardputerzero-xiaozhi_0.2.2-m5stack1_arm64.deb.release.json b/pool/main/cardputerzero-xiaozhi/cardputerzero-xiaozhi_0.2.2-m5stack1_arm64.deb.release.json new file mode 100644 index 0000000..1915a80 --- /dev/null +++ b/pool/main/cardputerzero-xiaozhi/cardputerzero-xiaozhi_0.2.2-m5stack1_arm64.deb.release.json @@ -0,0 +1,9 @@ +{ + "filename": "cardputerzero-xiaozhi_0.2.2-m5stack1_arm64.deb", + "url": "https://github.com/CardputerZero/packages/releases/download/apt-pool/cardputerzero-xiaozhi_0.2.2-m5stack1_arm64.deb", + "sha256": "53c6dea5d7dc671618d5edfbe894522c72f5efb38ff545a9eeec7a15327f7ea4", + "size": 25851200, + "package": "cardputerzero-xiaozhi", + "version": "0.2.2-m5stack1", + "architecture": "arm64" +} diff --git a/pool/main/flint/flint_0.0.25_arm64.deb b/pool/main/flint/flint_0.0.25_arm64.deb deleted file mode 100644 index b56ff83..0000000 --- a/pool/main/flint/flint_0.0.25_arm64.deb +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:fcfb04c52e2c06f4023f0453bfd88e052c314465b3283715c5e514a6d3af4e38 -size 3374248 diff --git a/pool/main/flint/flint_0.0.25_arm64.deb.release.json b/pool/main/flint/flint_0.0.25_arm64.deb.release.json new file mode 100644 index 0000000..d5b09ac --- /dev/null +++ b/pool/main/flint/flint_0.0.25_arm64.deb.release.json @@ -0,0 +1,9 @@ +{ + "filename": "flint_0.0.25_arm64.deb", + "url": "https://github.com/CardputerZero/packages/releases/download/apt-pool/flint_0.0.25_arm64.deb", + "sha256": "fcfb04c52e2c06f4023f0453bfd88e052c314465b3283715c5e514a6d3af4e38", + "size": 3374248, + "package": "flint", + "version": "0.0.25", + "architecture": "arm64" +} diff --git a/pool/main/lofibox/lofibox_0.2.0-1.lofibox23_arm64.deb.release.json b/pool/main/lofibox/lofibox_0.2.0-1.lofibox23_arm64.deb.release.json new file mode 100644 index 0000000..7cf109f --- /dev/null +++ b/pool/main/lofibox/lofibox_0.2.0-1.lofibox23_arm64.deb.release.json @@ -0,0 +1,9 @@ +{ + "filename": "lofibox_0.2.0-1.lofibox23_arm64.deb", + "url": "https://github.com/CardputerZero/packages/releases/download/apt-pool/lofibox_0.2.0-1.lofibox23_arm64.deb", + "sha256": "469d14c030d4addb5ff54f6eef7700b1781da3c33047796b1bbd2c8b3fe3177d", + "size": 9077082, + "package": "lofibox", + "version": "0.2.0-1~lofibox23", + "architecture": "arm64" +} diff --git a/pool/main/lofibox/lofibox_0.2.0-1~lofibox23_arm64.deb b/pool/main/lofibox/lofibox_0.2.0-1~lofibox23_arm64.deb deleted file mode 100644 index 3fbc2bf..0000000 --- a/pool/main/lofibox/lofibox_0.2.0-1~lofibox23_arm64.deb +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:469d14c030d4addb5ff54f6eef7700b1781da3c33047796b1bbd2c8b3fe3177d -size 9077082 diff --git a/pool/main/nc2000/nc2000_1.0.0_arm64.deb b/pool/main/nc2000/nc2000_1.0.0_arm64.deb deleted file mode 100644 index cb98931..0000000 --- a/pool/main/nc2000/nc2000_1.0.0_arm64.deb +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:9b24dc061091115b82e817c2c0b39503cefede5641bb8d67b157ed8fb4f6088b -size 20560466 diff --git a/pool/main/nc2000/nc2000_1.0.0_arm64.deb.release.json b/pool/main/nc2000/nc2000_1.0.0_arm64.deb.release.json new file mode 100644 index 0000000..0a02729 --- /dev/null +++ b/pool/main/nc2000/nc2000_1.0.0_arm64.deb.release.json @@ -0,0 +1,9 @@ +{ + "filename": "nc2000_1.0.0_arm64.deb", + "url": "https://github.com/CardputerZero/packages/releases/download/apt-pool/nc2000_1.0.0_arm64.deb", + "sha256": "9b24dc061091115b82e817c2c0b39503cefede5641bb8d67b157ed8fb4f6088b", + "size": 20560466, + "package": "nc2000", + "version": "1.0.0", + "architecture": "arm64" +} diff --git a/pool/main/nc2000/nc2000_1.0.2_arm64.deb b/pool/main/nc2000/nc2000_1.0.2_arm64.deb deleted file mode 100644 index 4fefe94..0000000 --- a/pool/main/nc2000/nc2000_1.0.2_arm64.deb +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:d9dbbf7cad57bb468d20dee1c8d3aef2afe374a6f399c0dc5eb198bf399bae3e -size 20560484 diff --git a/pool/main/nc2000/nc2000_1.0.2_arm64.deb.release.json b/pool/main/nc2000/nc2000_1.0.2_arm64.deb.release.json new file mode 100644 index 0000000..5ec191e --- /dev/null +++ b/pool/main/nc2000/nc2000_1.0.2_arm64.deb.release.json @@ -0,0 +1,9 @@ +{ + "filename": "nc2000_1.0.2_arm64.deb", + "url": "https://github.com/CardputerZero/packages/releases/download/apt-pool/nc2000_1.0.2_arm64.deb", + "sha256": "d9dbbf7cad57bb468d20dee1c8d3aef2afe374a6f399c0dc5eb198bf399bae3e", + "size": 20560484, + "package": "nc2000", + "version": "1.0.2", + "architecture": "arm64" +} diff --git a/pool/main/nc2000/nc2000_1.0.3_arm64.deb b/pool/main/nc2000/nc2000_1.0.3_arm64.deb deleted file mode 100644 index 16fa735..0000000 --- a/pool/main/nc2000/nc2000_1.0.3_arm64.deb +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:6f3e8c3ac385cb9a306f4ab5205f4bc3a0d4ae776c4fbdbe136df243b5f73ed8 -size 20560444 diff --git a/pool/main/nc2000/nc2000_1.0.3_arm64.deb.release.json b/pool/main/nc2000/nc2000_1.0.3_arm64.deb.release.json new file mode 100644 index 0000000..012b4c7 --- /dev/null +++ b/pool/main/nc2000/nc2000_1.0.3_arm64.deb.release.json @@ -0,0 +1,9 @@ +{ + "filename": "nc2000_1.0.3_arm64.deb", + "url": "https://github.com/CardputerZero/packages/releases/download/apt-pool/nc2000_1.0.3_arm64.deb", + "sha256": "6f3e8c3ac385cb9a306f4ab5205f4bc3a0d4ae776c4fbdbe136df243b5f73ed8", + "size": 20560444, + "package": "nc2000", + "version": "1.0.3", + "architecture": "arm64" +} From 9217205d0be28f16c18db648299f6033eade2f36 Mon Sep 17 00:00:00 2001 From: LiHaohua Date: Tue, 30 Jun 2026 17:56:07 +0800 Subject: [PATCH 2/4] update-index: build catalog from manifests, not all pool assets Make the in-git manifests the source of truth: only .deb referenced by a __.deb.release.json manifest is downloaded and indexed, so removing a manifest (czdev unpublish) actually drops the package on the next build even though the asset remains in the apt-pool release. Co-authored-by: Cursor --- .github/workflows/update-index.yml | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/.github/workflows/update-index.yml b/.github/workflows/update-index.yml index cf51ff9..22c8da4 100644 --- a/.github/workflows/update-index.yml +++ b/.github/workflows/update-index.yml @@ -83,12 +83,26 @@ jobs: rm -f "$tmp" done - - name: Download .deb assets from the apt-pool release + - name: Collect manifested .deb from the apt-pool release env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} run: | + set -euo pipefail + # Manifests in git are the source of truth for the catalog: only the + # .deb referenced by a manifest is indexed, so deleting a manifest + # (e.g. `czdev unpublish`) drops the package on the next build even if + # the asset still exists in the release. mkdir -p release-debs - gh release download "$POOL_TAG" -D release-debs --pattern '*.deb' --clobber + shopt -s nullglob globstar + count=0 + for manifest in pool/main/**/*.deb.release.json; do + name=$(jq -r '.filename' "$manifest") + if ! gh release download "$POOL_TAG" -D release-debs --pattern "$name" --clobber; then + echo "::error::asset '$name' (from $manifest) not found in the $POOL_TAG release"; exit 1 + fi + count=$((count+1)) + done + echo "collected $count .deb from manifests" - name: Build APT repository (Pages metadata only; .deb served from Releases) run: | From 5289cf5f1c13e86ba0c24cb70b5725277ebe4b46 Mon Sep 17 00:00:00 2001 From: LiHaohua Date: Tue, 30 Jun 2026 19:03:08 +0800 Subject: [PATCH 3/4] update-index: regenerate OSS Release; clarify apt vs registry paths - OSS staging regenerated Packages with relative Filename but reused the Pages-build Release, so apt rejected Packages.gz on a size/hash mismatch. Regenerate the OSS Release from the relative Packages (verified: apt-get update + apt download nc2000 now succeed against the OSS tree). - README: the device installs via registry download.url (absolute Release URLs); apt requires co-located binaries so the apt endpoint is the OSS mirror. Pages is the registry/web-UI data source, not an apt mirror (apt does not follow absolute Filename URLs). Co-authored-by: Cursor --- .github/workflows/update-index.yml | 25 +++++++++++++++++++++++++ README.md | 23 +++++++++++++++-------- 2 files changed, 40 insertions(+), 8 deletions(-) diff --git a/.github/workflows/update-index.yml b/.github/workflows/update-index.yml index 22c8da4..e17aa09 100644 --- a/.github/workflows/update-index.yml +++ b/.github/workflows/update-index.yml @@ -163,6 +163,31 @@ jobs: > oss-public/dists/stable/main/binary-arm64/Packages gzip -k -f oss-public/dists/stable/main/binary-arm64/Packages + # Regenerate the OSS Release so its hashes match the relative-path + # Packages (the rsync'd copy still carried the Pages-build hashes, + # which apt rejects as a size/hash mismatch). + cd oss-public/dists/stable + printf '%s\n' \ + "Origin: CardputerZero" \ + "Label: CardputerZero AppStore" \ + "Suite: stable" \ + "Codename: stable" \ + "Architectures: arm64" \ + "Components: main" \ + "Description: CardputerZero APT Repository" > Release + echo "MD5Sum:" >> Release + for f in main/binary-arm64/Packages main/binary-arm64/Packages.gz; do + size=$(wc -c < "$f" | tr -d ' ') + hash=$(md5sum "$f" | awk '{print $1}') + printf " %s %s %s\n" "$hash" "$size" "$f" >> Release + done + echo "SHA256:" >> Release + for f in main/binary-arm64/Packages main/binary-arm64/Packages.gz; do + size=$(wc -c < "$f" | tr -d ' ') + hash=$(sha256sum "$f" | awk '{print $1}') + printf " %s %s %s\n" "$hash" "$size" "$f" >> Release + done + - name: Setup ossutil if: ${{ vars.PACKAGES_OSS_SYNC_ENABLED == 'true' || inputs.sync_oss == true }} uses: manyuanrong/setup-ossutil@v3.0 diff --git a/README.md b/README.md index 60a4221..2db8de3 100644 --- a/README.md +++ b/README.md @@ -7,23 +7,30 @@ Release (`apt-pool`) and are mirrored to Aliyun OSS for China. Git only holds small metadata: `meta.json`, screenshots, and one `__.deb.release.json` manifest per package. -## Usage on device +## How the device installs apps -Global (GitHub Pages, `.deb` served from the Releases CDN): +The on-device AppStore reads `registry.json` and downloads each `.deb` directly +from its `download.url` (an absolute GitHub Release URL globally, or an OSS URL +for the China registry), verifying `download.md5` before install. This is the +primary install path and needs no apt configuration. -```bash -echo "deb [trusted=yes] https://cardputerzero.github.io/packages stable main" | sudo tee /etc/apt/sources.list.d/cardputerzero.list -sudo apt update -sudo apt install nc2000 lofibox -``` +## Manual apt (optional, power users) -China (Aliyun OSS mirror, `.deb` served from OSS): +`apt` requires the `.deb` to be co-located with the metadata (it treats +`Filename` as a path relative to the repo root), so the installable apt endpoint +is the **OSS mirror**, which serves the binaries inline: ```bash echo "deb [trusted=yes] https://cardputer-zero-repo.oss-cn-shenzhen.aliyuncs.com/packages stable main" | sudo tee /etc/apt/sources.list.d/cardputerzero.list sudo apt update +sudo apt install nc2000 ``` +The GitHub Pages endpoint (`https://cardputerzero.github.io/packages`) publishes +the index with absolute Release `Filename`s — it is the **data source for the +registry** and the web UI, not a `apt`-installable mirror (apt does not follow +absolute `Filename` URLs). + ## Architecture ``` From 9dcb23f527b2037ac11c58051dbd067b549b531c Mon Sep 17 00:00:00 2001 From: LiHaohua Date: Tue, 30 Jun 2026 19:11:44 +0800 Subject: [PATCH 4/4] validate-pr: skip on same-repo PRs; fix SIGPIPE in .desktop check - Skip third-party submission validation for same-repo (maintainer/infra) PRs such as the migration that seeds many authors' packages at once; only fork PRs are untrusted submissions. - The .desktop check piped dpkg-deb into 'grep -q', which closes the pipe on first match; under pipefail dpkg-deb's SIGPIPE made the check falsely report a missing .desktop. Read the contents to a file first. Co-authored-by: Cursor --- .github/workflows/validate-pr.yml | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/.github/workflows/validate-pr.yml b/.github/workflows/validate-pr.yml index 9f5167c..d8547e9 100644 --- a/.github/workflows/validate-pr.yml +++ b/.github/workflows/validate-pr.yml @@ -41,6 +41,17 @@ jobs: mkdir -p /tmp/out echo "${{ github.event.pull_request.number }}" > /tmp/out/pr-number.txt + # Third-party submissions always arrive as fork PRs. Same-repo PRs are + # maintainer/infra changes (e.g. the migration that seeds many authors' + # packages at once) and must not be judged by the per-submission + # ownership rules below. + if [ "${{ github.event.pull_request.head.repo.full_name }}" = "${{ github.repository }}" ]; then + echo "Same-repo (maintainer) PR — skipping third-party submission validation." + { echo "## ✅ Validation skipped"; echo; echo "Same-repo (maintainer/infra) PR — third-party submission checks are not applied."; } > /tmp/out/result.md + echo "status=success" >> "$GITHUB_OUTPUT" + exit 0 + fi + # Reject raw .deb committed to git (must use the release-manifest flow). if git diff --name-only --diff-filter=A "$BASE_SHA".."$HEAD_SHA" | grep -q '\.deb$'; then ERRORS="${ERRORS}- ❌ Raw \`.deb\` files must not be committed to git. Upload the binary to a Release in your fork and submit a \`*.deb.release.json\` manifest (use \`czdev publish\`).\n" @@ -81,7 +92,11 @@ jobs: if [[ "$dir_pkg" != "$ctrl_pkg" ]]; then ERRORS="${ERRORS}- ❌ Directory '$dir_pkg' != Package field '$ctrl_pkg'\n" fi - if ! dpkg-deb -c "$deb" | grep -q '\.desktop$'; then + # Write to a file first: `dpkg-deb -c | grep -q` makes grep close the + # pipe on first match, so dpkg-deb dies with SIGPIPE and pipefail + # turns the whole check into a false "missing .desktop". + dpkg-deb -c "$deb" > /tmp/deb-contents.txt 2>/dev/null || true + if ! grep -q '\.desktop$' /tmp/deb-contents.txt; then ERRORS="${ERRORS}- ❌ \`$fname\` has no .desktop file. All apps must include one.\n" fi if ! echo "$ctrl_pkg" | grep -qP '^[a-z0-9][a-z0-9.+\-]+$'; then