Skip to content
Merged
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
4 changes: 3 additions & 1 deletion .gitattributes
Original file line number Diff line number Diff line change
@@ -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
# <pkg>_<ver>_<arch>.deb.release.json manifests.
138 changes: 126 additions & 12 deletions .github/workflows/update-index.yml
Original file line number Diff line number Diff line change
Expand Up @@ -21,14 +21,22 @@ on:
default: 'packages'

permissions:
contents: read
contents: write # promote .deb assets into the apt-pool release
pages: write
id-token: write

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
# <pkg>_<ver>_<arch>.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
Expand All @@ -37,22 +45,128 @@ 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: 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: 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
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
- 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" \
"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: 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 <package>_<version>_<arch>.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

# 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" \
Expand Down Expand Up @@ -86,7 +200,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' }}
Expand Down
47 changes: 47 additions & 0 deletions .github/workflows/validate-pr-comment.yml
Original file line number Diff line number Diff line change
@@ -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,
});
Loading
Loading