diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 5c2f5b0..21693b3 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -7,12 +7,17 @@ on: pull_request: branches: [main] +permissions: + contents: read + +env: + FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: "true" + jobs: build-webgpu-bridge: name: Build WebGPU Bridge (WASM) runs-on: ubuntu-latest env: - FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: "true" LLAMA_WEBGPU_SMOKE_MODEL_URL: https://huggingface.co/aladar/llama-2-tiny-random-GGUF/resolve/main/llama-2-tiny-random.gguf LLAMA_WEBGPU_SMOKE_MODEL_SHA256: 81f226c62d28ed4a1a9b9fa080fcd9f0cc40e0f9d5680036583ff98fbcd035cb LLAMA_WEBGPU_SMOKE_MODEL_CACHE: ~/.cache/llama-web-bridge/state-smoke-models @@ -112,3 +117,63 @@ jobs: ${{ runner.temp }}/webgpu_bridge_dist/llama_webgpu_core.wasm ${{ runner.temp }}/webgpu_bridge_dist/llama_webgpu_core_mem64.js ${{ runner.temp }}/webgpu_bridge_dist/llama_webgpu_core_mem64.wasm + + dispatch-publish-assets: + name: Dispatch asset publish + needs: build-webgpu-bridge + if: github.event_name == 'push' && github.ref == 'refs/heads/main' + runs-on: ubuntu-latest + permissions: + actions: write + contents: read + env: + ASSETS_REPO: leehack/llama-web-bridge-assets + VERSION_FILE: llama_cpp.version + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Dispatch publish when llama.cpp pin changed + env: + GH_TOKEN: ${{ github.token }} + BEFORE_SHA: ${{ github.event.before }} + run: | + set -euo pipefail + + if [ -z "${BEFORE_SHA}" ] || [[ "${BEFORE_SHA}" =~ ^0+$ ]]; then + echo "No comparable before SHA for ${GITHUB_SHA}; skipping automatic publish dispatch." + exit 0 + fi + + if git diff --quiet "${BEFORE_SHA}" "${GITHUB_SHA}" -- "${VERSION_FILE}"; then + echo "${VERSION_FILE} did not change; skipping automatic publish dispatch." + exit 0 + fi + + changed_files="$(git diff --name-only "${BEFORE_SHA}" "${GITHUB_SHA}" -- "${VERSION_FILE}")" + echo "Changed files in main push:" + echo "${changed_files}" + + LLAMA_CPP_TAG="$(tr -d '[:space:]' < "${VERSION_FILE}")" + if [ -z "${LLAMA_CPP_TAG}" ]; then + echo "error: ${VERSION_FILE} is empty" + exit 1 + fi + + gh workflow run publish_assets.yml \ + --repo "${GITHUB_REPOSITORY}" \ + --ref main \ + -f assets_tag=auto \ + -f assets_repo="${ASSETS_REPO}" \ + -f source_ref="${GITHUB_SHA}" + + echo "Dispatched publish_assets.yml for the next assets tag from source ${GITHUB_SHA} using llama.cpp ${LLAMA_CPP_TAG}." + { + echo "### Asset publish dispatched" + echo + echo "- Assets repo: \`${ASSETS_REPO}\`" + echo "- Assets tag: next patch tag resolved by \`publish_assets.yml\`" + echo "- llama.cpp tag: \`${LLAMA_CPP_TAG}\`" + echo "- Source commit: \`${GITHUB_SHA}\`" + } >> "${GITHUB_STEP_SUMMARY}" diff --git a/.github/workflows/publish_assets.yml b/.github/workflows/publish_assets.yml index 9d452da..aab3ef1 100644 --- a/.github/workflows/publish_assets.yml +++ b/.github/workflows/publish_assets.yml @@ -4,7 +4,7 @@ on: workflow_dispatch: inputs: assets_tag: - description: Tag to publish to assets repo (for example v0.1.1) + description: Tag to publish to assets repo (for example v0.1.1), or auto for the next patch tag required: true assets_repo: description: Asset repository in owner/repo format @@ -14,6 +14,10 @@ on: description: Optional llama.cpp tag override; defaults to llama_cpp.version required: false default: '' + source_ref: + description: Optional llama-web-bridge source ref/SHA to build; defaults to the workflow ref + required: false + default: '' push: tags: - 'v*' @@ -23,18 +27,27 @@ env: ASSETS_TAG: ${{ github.event_name == 'workflow_dispatch' && inputs.assets_tag || github.ref_name }} ASSETS_REPO: ${{ github.event_name == 'workflow_dispatch' && inputs.assets_repo || 'leehack/llama-web-bridge-assets' }} REQUESTED_LLAMA_CPP_TAG: ${{ github.event_name == 'workflow_dispatch' && inputs.llama_cpp_tag || '' }} + REQUESTED_SOURCE_REF: ${{ github.event_name == 'workflow_dispatch' && inputs.source_ref || github.sha }} permissions: contents: read +concurrency: + group: publish-bridge-assets + cancel-in-progress: false + jobs: build-bridge-assets: name: Build bridge assets runs-on: ubuntu-latest outputs: llama_cpp_tag: ${{ steps.resolve-publish-parameters.outputs.llama_cpp_tag }} + assets_tag: ${{ steps.resolve-publish-parameters.outputs.assets_tag }} + source_commit: ${{ steps.resolve-publish-parameters.outputs.source_commit }} steps: - uses: actions/checkout@v4 + with: + ref: ${{ env.REQUESTED_SOURCE_REF }} - name: Setup Node.js uses: actions/setup-node@v4 @@ -53,7 +66,43 @@ jobs: - name: Resolve publish parameters id: resolve-publish-parameters + env: + GH_TOKEN: ${{ github.token }} run: | + set -euo pipefail + + SOURCE_COMMIT="$(git rev-parse HEAD)" + + if [ "${ASSETS_TAG}" = "auto" ]; then + if [[ ! "${REQUESTED_SOURCE_REF}" =~ ^[0-9a-f]{40}$ ]]; then + echo "error: assets_tag=auto requires source_ref to be a full source commit SHA: ${REQUESTED_SOURCE_REF}" + exit 1 + fi + if [ "${SOURCE_COMMIT}" != "${REQUESTED_SOURCE_REF}" ]; then + echo "error: checked-out source commit ${SOURCE_COMMIT} does not match requested source_ref ${REQUESTED_SOURCE_REF}" + exit 1 + fi + fi + + if [ "${ASSETS_TAG}" = "auto" ]; then + LATEST_ASSETS_TAG="$(gh release view --repo "${ASSETS_REPO}" --json tagName --jq '.tagName // ""')" + if [[ ! "${LATEST_ASSETS_TAG}" =~ ^v([0-9]+)\.([0-9]+)\.([0-9]+)$ ]]; then + echo "error: latest assets release tag is not a vMAJOR.MINOR.PATCH tag: ${LATEST_ASSETS_TAG}" + exit 1 + fi + MAJOR="${BASH_REMATCH[1]}" + MINOR="${BASH_REMATCH[2]}" + PATCH="${BASH_REMATCH[3]}" + RESOLVED_ASSETS_TAG="v${MAJOR}.${MINOR}.$((PATCH + 1))" + else + RESOLVED_ASSETS_TAG="${ASSETS_TAG}" + fi + + if [[ ! "${RESOLVED_ASSETS_TAG}" =~ ^v[0-9]+\.[0-9]+\.[0-9]+$ ]]; then + echo "error: assets tag must be a vMAJOR.MINOR.PATCH tag or auto: ${RESOLVED_ASSETS_TAG}" + exit 1 + fi + if [ -n "$REQUESTED_LLAMA_CPP_TAG" ]; then LLAMA_CPP_TAG="$REQUESTED_LLAMA_CPP_TAG" else @@ -63,12 +112,20 @@ jobs: echo "error: llama.cpp tag is empty" exit 1 fi + if [[ ! "$LLAMA_CPP_TAG" =~ ^b[0-9]+$ ]]; then + echo "error: llama.cpp tag must match b[0-9]+: ${LLAMA_CPP_TAG}" + exit 1 + fi + echo "ASSETS_TAG=$RESOLVED_ASSETS_TAG" >> "$GITHUB_ENV" echo "LLAMA_CPP_TAG=$LLAMA_CPP_TAG" >> "$GITHUB_ENV" + echo "assets_tag=$RESOLVED_ASSETS_TAG" >> "$GITHUB_OUTPUT" echo "llama_cpp_tag=$LLAMA_CPP_TAG" >> "$GITHUB_OUTPUT" + echo "source_commit=$SOURCE_COMMIT" >> "$GITHUB_OUTPUT" echo "event=${{ github.event_name }}" - echo "assets_tag=${ASSETS_TAG}" + echo "assets_tag=${RESOLVED_ASSETS_TAG}" echo "assets_repo=${ASSETS_REPO}" echo "llama_cpp_tag=${LLAMA_CPP_TAG}" + echo "source_commit=${SOURCE_COMMIT}" - name: Clone llama.cpp source run: | @@ -88,9 +145,8 @@ jobs: - name: Generate manifest and checksums env: OUT_DIR: ${{ runner.temp }}/webgpu_bridge_dist - ASSETS_TAG: ${{ env.ASSETS_TAG }} SOURCE_REPO: ${{ github.repository }} - SOURCE_COMMIT: ${{ github.sha }} + SOURCE_COMMIT: ${{ steps.resolve-publish-parameters.outputs.source_commit }} run: | test -f "$OUT_DIR/llama_webgpu_bridge.js" test -f "$OUT_DIR/llama_webgpu_bridge_worker.js" @@ -166,6 +222,8 @@ jobs: name: Publish to assets repository needs: build-bridge-assets runs-on: ubuntu-latest + env: + ASSETS_TAG: ${{ needs.build-bridge-assets.outputs.assets_tag }} steps: - name: Validate assets PAT secret env: @@ -190,8 +248,6 @@ jobs: path: bridge-dist - name: Publish assets and tag - env: - ASSETS_TAG: ${{ env.ASSETS_TAG }} run: | if git -C assets-repo ls-remote --exit-code --tags origin "refs/tags/$ASSETS_TAG" >/dev/null 2>&1; then echo "error: tag already exists in assets repo: $ASSETS_TAG" @@ -224,7 +280,7 @@ jobs: ASSETS_REPO: ${{ env.ASSETS_REPO }} LLAMA_CPP_TAG: ${{ needs.build-bridge-assets.outputs.llama_cpp_tag }} SOURCE_REPO: ${{ github.repository }} - SOURCE_COMMIT: ${{ github.sha }} + SOURCE_COMMIT: ${{ needs.build-bridge-assets.outputs.source_commit }} run: | NOTES="$(cat <` for CI-dispatched + publishes from the exact source commit that already passed `main` CI. - Pushes assets + tag to `llama-web-bridge-assets` ## Change Boundaries diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index a7f1ac8..7a70bca 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -94,6 +94,11 @@ query strings, and fragments before printing the location. - Preserve `llama_cpp.version` as the single source of truth for default CI and publish builds. Manual publish overrides are allowed for temporary validation, but tag-triggered publishes should use the pinned file. +- Main-branch CI automatically dispatches `publish_assets.yml` after a successful + build/smoke only when the pushed commit range changed `llama_cpp.version`. The + dispatch passes the validated source SHA and lets the serialized publish + workflow compute the next patch tag from `llama-web-bridge-assets`; PR CI must + never publish assets. - The auto-update workflow manages the stable `automation/bump-llama-cpp` branch and updates an existing PR instead of opening duplicates. It should include the upstream release notes, compare link, and commit range in the PR body, then @@ -109,6 +114,17 @@ query strings, and fragments before printing the location. Use workflow `.github/workflows/publish_assets.yml`: +Automatic publish from CI: + +1. Merge a PR that changes `llama_cpp.version`. +2. Let the `main` CI build and browser smoke pass. +3. The final CI job dispatches `publish_assets.yml` with `assets_tag=auto` and + `source_ref` set to the validated main commit. The serialized publish workflow + computes the next patch assets tag and leaves `llama_cpp_tag` empty so the + publish reads the merged pin. + +Manual publish: + 1. Set input `assets_tag` (new tag). 2. Optionally set `assets_repo`; leave `llama_cpp_tag` empty to use `llama_cpp.version`, or set it only for an explicit temporary override. diff --git a/README.md b/README.md index e7a66ac..946bbc8 100644 --- a/README.md +++ b/README.md @@ -201,7 +201,11 @@ Publish workflow: Trigger modes: -- Automatic: push a `v*` tag in this repo (for example `v0.1.5`) +- Automatic after a llama.cpp pin merge: when `main` CI succeeds for a push that + changed `llama_cpp.version`, CI dispatches `publish_assets.yml` with + `assets_tag=auto` and the validated source SHA. The serialized publish workflow + resolves the next patch tag from `leehack/llama-web-bridge-assets`. +- Automatic tag publish: push a `v*` tag in this repo (for example `v0.1.5`) - Manual: run workflow dispatch with explicit inputs Required repository secret: @@ -213,7 +217,16 @@ The publish workflow carries the resolved `llama.cpp` tag from the build job to the release job as an explicit job output, so the asset release notes match the `manifest.json` `llama_cpp_tag` value. -Example publish: +Automatic llama.cpp pin publish: + +1. Merge a PR that changes `llama_cpp.version`. +2. `CI` builds and browser-smokes the merged `main` commit. +3. If CI succeeds, the final CI job dispatches `Publish Bridge Assets` with the + validated source SHA, `assets_tag=auto`, and no `llama_cpp_tag` override. The + publish workflow resolves the next patch tag and the manifest uses the merged + pin. + +Tag publish example: 1. Create/push a release tag in this repo (for example `v0.1.5`) 2. `Publish Bridge Assets` runs automatically and publishes the same tag to diff --git a/scripts/verify_ci_reliability.py b/scripts/verify_ci_reliability.py index 9126324..3d4c223 100644 --- a/scripts/verify_ci_reliability.py +++ b/scripts/verify_ci_reliability.py @@ -198,15 +198,60 @@ def main() -> int: "ci.yml must resolve the llama.cpp tag from llama_cpp.version, support explicit dispatch, and avoid hard-coded stale defaults", errors, ) + require( + "dispatch-publish-assets:" in ci + and "needs: build-webgpu-bridge" in ci + and "if: github.event_name == 'push' && github.ref == 'refs/heads/main'" in ci + and "actions: write" in ci + and "fetch-depth: 0" in ci + and "git diff --quiet \"${BEFORE_SHA}\" \"${GITHUB_SHA}\" -- \"${VERSION_FILE}\"" in ci + and "gh workflow run publish_assets.yml" in ci + and "-f assets_tag=auto" in ci + and "-f assets_repo=\"${ASSETS_REPO}\"" in ci + and "-f source_ref=\"${GITHUB_SHA}\"" in ci, + "ci.yml must dispatch asset publishing only after successful main pushes that changed llama_cpp.version, with actions:write scoped to the dispatch job and auto tag resolution delegated to publish_assets.yml", + errors, + ) require( "REQUESTED_LLAMA_CPP_TAG" in publish + and "source_ref:" in publish + and "REQUESTED_SOURCE_REF" in publish + and "ref: ${{ env.REQUESTED_SOURCE_REF }}" in publish and "tr -d '[:space:]' < llama_cpp.version" in publish and "outputs:" in publish and "llama_cpp_tag: ${{ steps.resolve-publish-parameters.outputs.llama_cpp_tag }}" in publish + and "assets_tag: ${{ steps.resolve-publish-parameters.outputs.assets_tag }}" in publish + and "source_commit: ${{ steps.resolve-publish-parameters.outputs.source_commit }}" in publish and "LLAMA_CPP_TAG: ${{ needs.build-bridge-assets.outputs.llama_cpp_tag }}" in publish + and "SOURCE_COMMIT: ${{ needs.build-bridge-assets.outputs.source_commit }}" in publish + and "llama.cpp tag must match b[0-9]+" in publish and "default: b9116" not in publish and "|| 'b9116'" not in publish, - "publish_assets.yml must default to llama_cpp.version, pass the resolved tag across jobs, and still allow a manual override", + "publish_assets.yml must default to llama_cpp.version, support an explicit source ref, pass resolved tags/commit across jobs, and still allow a manual override", + errors, + ) + require( + "if [ \"${ASSETS_TAG}\" = \"auto\" ]; then" in publish + and "assets_tag=auto requires source_ref to be a full source commit SHA" in publish + and "checked-out source commit ${SOURCE_COMMIT} does not match requested source_ref" in publish + and "gh release view --repo \"${ASSETS_REPO}\" --json tagName" in publish + and "RESOLVED_ASSETS_TAG=\"v${MAJOR}.${MINOR}.$((PATCH + 1))\"" in publish + and "assets tag must be a vMAJOR.MINOR.PATCH tag or auto" in publish, + "publish_assets.yml must resolve automatic next patch asset tags inside the serialized publish workflow", + errors, + ) + require( + "-f source_ref=\"${GITHUB_SHA}\"" in ci + and "SOURCE_COMMIT=\"$(git rev-parse HEAD)\"" in publish + and "OUT_DIR: ${{ runner.temp }}/webgpu_bridge_dist\n ASSETS_TAG: ${{ env.ASSETS_TAG }}" not in publish, + "CI-dispatched publishes must build the exact validated source SHA and manifest generation must inherit the runtime-resolved assets tag", + errors, + ) + require( + "concurrency:" in publish + and "group: publish-bridge-assets" in publish + and "cancel-in-progress: false" in publish, + "publish_assets.yml must serialize asset publishes so automatic and manual releases cannot race", errors, ) require(