From e2f371311060b8a444cc993ff49752f7e218ef48 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Thu, 5 Nov 2026 19:49:31 +0000 Subject: [PATCH 1/7] Add a workflow that rebuilds the devpanel CLI Build linux/amd64 and linux/arm64 from devpanel/cli on native runners when the Actions cache does not already contain that commit. Co-authored-by: Darren Oh --- .github/scripts/build-devpanel-cli.sh | 118 +++++++++++++ .github/scripts/fetch-devpanel-source.sh | 64 +++++++ .github/scripts/resolve-devpanel-cli.sh | 65 +++++++ .github/workflows/update-devpanel-cli.yml | 196 ++++++++++++++++++++++ AGENTS.md | 5 +- README.md | 7 + TODO.md | 13 ++ 7 files changed, 466 insertions(+), 2 deletions(-) create mode 100755 .github/scripts/build-devpanel-cli.sh create mode 100755 .github/scripts/fetch-devpanel-source.sh create mode 100755 .github/scripts/resolve-devpanel-cli.sh create mode 100644 .github/workflows/update-devpanel-cli.yml diff --git a/.github/scripts/build-devpanel-cli.sh b/.github/scripts/build-devpanel-cli.sh new file mode 100755 index 00000000..d7d50628 --- /dev/null +++ b/.github/scripts/build-devpanel-cli.sh @@ -0,0 +1,118 @@ +#!/usr/bin/env bash +# Build one native devpanel CLI binary from a devpanel/cli checkout. +# +# The binary shipped from this source is github.com/devpanel/cli/cmd/devpanel-client. +# Run this on the runner that matches arch. An amd64 runner builds amd64 and +# an arm64 runner builds arm64. +# +# Usage: build-devpanel-cli.sh +# +# GHCR_TOKEN, when set, lets private Go modules download from github.com/devpanel. +set -euo pipefail + +self_test() { + local dir + dir=$(mktemp -d) + mkdir -p "${dir}/cmd/other" + printf 'module example.com/other\n\ngo 1.22\n' >"${dir}/go.mod" + if bash "$0" "$dir" amd64 "${dir}/devpanel" >/dev/null 2>&1; then + echo "ERROR: self-test expected a missing cmd/devpanel-client to fail." >&2 + exit 1 + fi + mkdir -p "${dir}/cmd/devpanel-client" + if bash "$0" "$dir" amd64 "${dir}/devpanel" >/dev/null 2>&1; then + echo "ERROR: self-test expected the wrong Go module to fail." >&2 + exit 1 + fi + rm -rf "$dir" + echo "self-test ok" +} + +if [[ "${1:-}" == "--self-test" ]]; then + self_test + exit +fi + +if [[ $# -ne 3 ]]; then + echo "Usage: $0 " >&2 + exit 1 +fi + +src=$1 +arch=$2 +dest=$3 +case "$arch" in + amd64|arm64) ;; + *) + echo "ERROR: Architecture must be amd64 or arm64 (got '${arch}')." >&2 + exit 1 + ;; +esac + +host_machine=$(uname -m) +case "$host_machine" in + x86_64) host_arch=amd64 ;; + aarch64|arm64) host_arch=arm64 ;; + *) + echo "ERROR: Unsupported runner machine '${host_machine}'." >&2 + exit 1 + ;; +esac +if [[ "$host_arch" != "$arch" ]]; then + echo "ERROR: This runner is ${host_arch}. Build ${arch} on a ${arch} runner." >&2 + exit 1 +fi + +if [[ ! -d "$src" ]]; then + echo "ERROR: source directory does not exist: ${src}" >&2 + exit 1 +fi +src=$(cd "$src" && pwd) + +if [[ ! -d "${src}/cmd/devpanel-client" ]]; then + echo "ERROR: ${src}/cmd/devpanel-client does not exist." >&2 + echo "The devpanel binary from this source is github.com/devpanel/cli/cmd/devpanel-client." >&2 + exit 1 +fi +if [[ ! -f "${src}/go.mod" ]]; then + echo "ERROR: ${src}/go.mod does not exist." >&2 + exit 1 +fi +module_path=$(awk '$1 == "module" { print $2; exit }' "${src}/go.mod") +module_path=${module_path#\"} +module_path=${module_path%\"} +if [[ "$module_path" != "github.com/devpanel/cli" ]]; then + echo "ERROR: expected Go module github.com/devpanel/cli, found '${module_path:-none}'." >&2 + exit 1 +fi + +token=${GHCR_TOKEN:-} +if [[ -n "$token" ]]; then + git config --global url."https://x-access-token:${token}@github.com/".insteadOf "https://github.com/" +fi + +echo "Building linux/${arch}" +( + cd "$src" + CGO_ENABLED=0 GOOS=linux GOARCH="${arch}" \ + go build -trimpath -buildvcs=false -o "$dest" ./cmd/devpanel-client +) +chmod 0755 "$dest" + +desc=$(file -b "$dest") +if [[ "$arch" == "amd64" && "$desc" != *"x86-64"* ]]; then + echo "ERROR: amd64 build is not an x86-64 executable (${desc})." >&2 + exit 1 +fi +if [[ "$arch" == "arm64" && "$desc" != *"aarch64"* ]]; then + echo "ERROR: arm64 build is not an aarch64 executable (${desc})." >&2 + exit 1 +fi +if ! go version -m "$dest" | grep -q 'github.com/devpanel/cli/cmd/devpanel-client'; then + echo "ERROR: ${dest} is not github.com/devpanel/cli/cmd/devpanel-client." >&2 + exit 1 +fi + +echo "Checking devpanel version" +"$dest" version +echo "Wrote ${dest}" diff --git a/.github/scripts/fetch-devpanel-source.sh b/.github/scripts/fetch-devpanel-source.sh new file mode 100755 index 00000000..be9a987d --- /dev/null +++ b/.github/scripts/fetch-devpanel-source.sh @@ -0,0 +1,64 @@ +#!/usr/bin/env bash +# Clone https://github.com/devpanel/cli at one commit. +# +# The destination must be outside this repository so the clone is not part of +# the php checkout. GitHub Actions should pass a directory under RUNNER_TEMP. +# +# Usage: fetch-devpanel-source.sh +# GHCR_TOKEN Token that can read devpanel/cli. +set -euo pipefail + +self_test() { + local repo_root + repo_root=$(cd "$(dirname "$0")/../.." && pwd) + if bash "$0" "${repo_root}/devpanel-cli-src" "0123456789abcdef0123456789abcdef01234567" >/dev/null 2>&1; then + echo "ERROR: self-test expected a destination inside this repository to fail." >&2 + exit 1 + fi + echo "self-test ok" +} + +if [[ "${1:-}" == "--self-test" ]]; then + self_test + exit +fi + +if [[ $# -ne 2 ]]; then + echo "Usage: $0 " >&2 + exit 1 +fi + +dest=$1 +sha=$2 +if [[ ! "$sha" =~ ^[0-9a-f]{40}$ ]]; then + echo "ERROR: commit SHA must be 40 hexadecimal characters." >&2 + exit 1 +fi + +repo_root=$(cd "$(dirname "$0")/../.." && pwd) +case "$dest" in + "$repo_root"|"$repo_root"/*) + echo "ERROR: Clone devpanel/cli outside this repository (got '${dest}')." >&2 + exit 1 + ;; +esac + +token=${GHCR_TOKEN:-} +if [[ -z "$token" ]]; then + echo "ERROR: GHCR_TOKEN is required to read https://github.com/devpanel/cli." >&2 + exit 1 +fi + +if [[ -e "$dest" ]]; then + echo "ERROR: destination already exists: ${dest}" >&2 + exit 1 +fi + +mkdir -p "$(dirname "$dest")" +mkdir "$dest" +git -C "$dest" init +git -C "$dest" remote add origin https://github.com/devpanel/cli.git +git -C "$dest" -c "http.extraheader=AUTHORIZATION: bearer ${token}" \ + fetch --depth 1 origin "$sha" +git -C "$dest" checkout --detach FETCH_HEAD +echo "Cloned devpanel/cli ${sha} to ${dest}" diff --git a/.github/scripts/resolve-devpanel-cli.sh b/.github/scripts/resolve-devpanel-cli.sh new file mode 100755 index 00000000..31ff01ec --- /dev/null +++ b/.github/scripts/resolve-devpanel-cli.sh @@ -0,0 +1,65 @@ +#!/usr/bin/env bash +# Resolve the default-branch commit of https://github.com/devpanel/cli. +# +# GHCR_TOKEN Token that can read that repository. +# GITHUB_OUTPUT When set, receives sha and ref. +set -euo pipefail + +token=${GHCR_TOKEN:-} +if [[ -z "$token" ]]; then + echo "ERROR: GHCR_TOKEN is required to read https://github.com/devpanel/cli." >&2 + echo "::error::GHCR_TOKEN is required to read https://github.com/devpanel/cli." + exit 1 +fi + +api_get() { + local url=$1 + local body code + body=$(mktemp) + code=$( + curl -sS \ + -H "Authorization: Bearer ${token}" \ + -H "Accept: application/vnd.github+json" \ + -H "X-GitHub-Api-Version: 2022-11-28" \ + -o "$body" \ + -w "%{http_code}" \ + "$url" || true + ) + if [[ "$code" != "200" ]]; then + echo "ERROR: GitHub API ${url} returned HTTP ${code:-unknown}." >&2 + jq -r '.message // empty' "$body" >&2 || true + rm -f "$body" + return 1 + fi + cat "$body" + rm -f "$body" +} + +repo_json=$(api_get "https://api.github.com/repos/devpanel/cli") || { + echo "::error::Unable to read https://github.com/devpanel/cli. GHCR_TOKEN needs access to that repository." + exit 1 +} +ref=$(jq -r '.default_branch // empty' <<<"$repo_json") +if [[ -z "$ref" ]]; then + echo "ERROR: devpanel/cli did not report a default branch." >&2 + exit 1 +fi + +encoded_ref=$(jq -rn --arg ref "$ref" '$ref | @uri') +commit_json=$(api_get "https://api.github.com/repos/devpanel/cli/commits/${encoded_ref}") || { + echo "::error::Unable to resolve devpanel/cli ref '${ref}'." + exit 1 +} +sha=$(jq -r '.sha // empty' <<<"$commit_json") +if [[ ! "$sha" =~ ^[0-9a-f]{40}$ ]]; then + echo "ERROR: devpanel/cli ref '${ref}' did not resolve to a commit." >&2 + exit 1 +fi + +echo "devpanel/cli ${ref} is ${sha}" +if [[ -n "${GITHUB_OUTPUT:-}" ]]; then + { + echo "sha=${sha}" + echo "ref=${ref}" + } >>"$GITHUB_OUTPUT" +fi diff --git a/.github/workflows/update-devpanel-cli.yml b/.github/workflows/update-devpanel-cli.yml new file mode 100644 index 00000000..1bf31e65 --- /dev/null +++ b/.github/workflows/update-devpanel-cli.yml @@ -0,0 +1,196 @@ +name: Update DevPanel CLI binaries + +# Build linux/amd64 and linux/arm64 devpanel binaries from +# https://github.com/devpanel/cli and commit base/bin/devpanel-amd64 and +# base/bin/devpanel-arm64. +# +# Run it manually, or call it from another workflow with workflow_call. +# The Actions cache records the devpanel/cli commit that was last built. +# Delete that cache entry to rebuild the same commit. This workflow does not +# commit a revision file. +# +# devpanel/cli is cloned under RUNNER_TEMP, outside this repository. +# GHCR_TOKEN must be able to read devpanel/cli and push to this repository. +# Binary commits include [skip ci]. + +on: + workflow_dispatch: + workflow_call: + secrets: + GHCR_TOKEN: + required: true + outputs: + head_sha: + description: Commit that contains the devpanel binaries after this run. + value: ${{ jobs.commit.outputs.head_sha || github.sha }} + +concurrency: + group: update-devpanel-cli-${{ github.ref }} + cancel-in-progress: false + +jobs: + plan: + name: Check cached devpanel/cli revision + runs-on: ubuntu-26.04 + permissions: + actions: read + contents: read + outputs: + sha: ${{ steps.upstream.outputs.sha }} + ref: ${{ steps.upstream.outputs.ref }} + changed: ${{ steps.decision.outputs.changed }} + steps: + - name: Checkout repository + uses: actions/checkout@v6 + + - name: Require GHCR_TOKEN + env: + GHCR_TOKEN: ${{ secrets.GHCR_TOKEN }} + run: | + if [[ -z "${GHCR_TOKEN}" ]]; then + echo "::error::GHCR_TOKEN is required to read devpanel/cli and commit the binaries." + exit 1 + fi + + - name: Resolve devpanel/cli commit + id: upstream + env: + GHCR_TOKEN: ${{ secrets.GHCR_TOKEN }} + run: bash .github/scripts/resolve-devpanel-cli.sh + + - name: Restore the built revision + id: revision-cache + uses: actions/cache/restore@v5 + with: + path: ${{ runner.temp }}/devpanel-cli-revision + key: devpanel-cli-${{ steps.upstream.outputs.sha }} + + - name: Decide whether to rebuild + id: decision + env: + CACHE_HIT: ${{ steps.revision-cache.outputs.cache-hit }} + REF_TYPE: ${{ github.ref_type }} + UPSTREAM_SHA: ${{ steps.upstream.outputs.sha }} + run: | + if [[ "${REF_TYPE}" == "tag" ]]; then + echo "changed=false" >> "$GITHUB_OUTPUT" + echo "Tagged commits are left unchanged." + elif [[ "${CACHE_HIT}" == "true" ]]; then + echo "changed=false" >> "$GITHUB_OUTPUT" + echo "devpanel/cli ${UPSTREAM_SHA} is already built." + else + echo "changed=true" >> "$GITHUB_OUTPUT" + echo "devpanel/cli ${UPSTREAM_SHA} is not in the Actions cache." + fi + + build: + name: Build devpanel (${{ matrix.arch }}) + needs: plan + if: needs.plan.outputs.changed == 'true' + strategy: + fail-fast: false + matrix: + include: + - arch: amd64 + runner: ubuntu-26.04 + - arch: arm64 + runner: ubuntu-26.04-arm + runs-on: ${{ matrix.runner }} + permissions: + contents: read + steps: + - name: Checkout repository + uses: actions/checkout@v6 + + - name: Clone devpanel/cli + env: + GHCR_TOKEN: ${{ secrets.GHCR_TOKEN }} + UPSTREAM_SHA: ${{ needs.plan.outputs.sha }} + run: bash .github/scripts/fetch-devpanel-source.sh "${RUNNER_TEMP}/devpanel-cli" "${UPSTREAM_SHA}" + + - name: Set up Go + uses: actions/setup-go@v5 + with: + go-version-file: ${{ runner.temp }}/devpanel-cli/go.mod + cache: false + + - name: Build native binary + env: + GHCR_TOKEN: ${{ secrets.GHCR_TOKEN }} + run: > + bash .github/scripts/build-devpanel-cli.sh + ${{ runner.temp }}/devpanel-cli + ${{ matrix.arch }} + ${{ runner.temp }}/devpanel-${{ matrix.arch }} + + - name: Upload binary + uses: actions/upload-artifact@v7 + with: + name: devpanel-binary-${{ matrix.arch }} + path: ${{ runner.temp }}/devpanel-${{ matrix.arch }} + if-no-files-found: error + retention-days: 1 + + commit: + name: Commit devpanel binaries + needs: [plan, build] + if: needs.plan.outputs.changed == 'true' && needs.build.result == 'success' + runs-on: ubuntu-26.04 + permissions: + actions: write + contents: read + outputs: + head_sha: ${{ steps.push.outputs.head_sha }} + steps: + - name: Checkout repository + uses: actions/checkout@v6 + with: + token: ${{ secrets.GHCR_TOKEN }} + + - name: Download amd64 binary + uses: actions/download-artifact@v8 + with: + name: devpanel-binary-amd64 + path: dist/amd64 + + - name: Download arm64 binary + uses: actions/download-artifact@v8 + with: + name: devpanel-binary-arm64 + path: dist/arm64 + + - name: Commit binaries + id: push + env: + UPSTREAM_SHA: ${{ needs.plan.outputs.sha }} + UPSTREAM_REF: ${{ needs.plan.outputs.ref }} + run: | + set -euo pipefail + install -m 0755 dist/amd64/devpanel-amd64 base/bin/devpanel-amd64 + install -m 0755 dist/arm64/devpanel-arm64 base/bin/devpanel-arm64 + git add -- base/bin/devpanel-amd64 base/bin/devpanel-arm64 + if git diff --cached --quiet; then + echo "Build produced no changes to commit." + else + git config user.name "github-actions[bot]" + git config user.email "41898282+github-actions[bot]@users.noreply.github.com" + git commit \ + -m "Update devpanel CLI from devpanel/cli@${UPSTREAM_SHA}" \ + -m "Built linux/amd64 and linux/arm64 from https://github.com/devpanel/cli (${UPSTREAM_REF})." \ + -m "[skip ci]" + git push origin "HEAD:${GITHUB_REF_NAME}" + fi + echo "head_sha=$(git rev-parse HEAD)" >> "$GITHUB_OUTPUT" + + - name: Record the built revision + env: + UPSTREAM_SHA: ${{ needs.plan.outputs.sha }} + run: | + mkdir -p "${RUNNER_TEMP}/devpanel-cli-revision" + printf '%s\n' "${UPSTREAM_SHA}" > "${RUNNER_TEMP}/devpanel-cli-revision/sha" + + - name: Save the built revision + uses: actions/cache/save@v5 + with: + path: ${{ runner.temp }}/devpanel-cli-revision + key: devpanel-cli-${{ needs.plan.outputs.sha }} diff --git a/AGENTS.md b/AGENTS.md index abbe2022..58092bd8 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -20,7 +20,7 @@ Images are published to Docker Hub as `devpanel/php:-` (e.g. ` ``` base/ # Shared base Dockerfile (PHP + Apache) Dockerfile - bin/ # DevPanel CLI binary + bin/ # DevPanel CLI binary; update-devpanel-cli.yml commits devpanel-$arch drush/ # Drush version directories (drush7–drush11) scripts/ # Apache startup scripts templates/ # Apache/PHP config templates @@ -129,9 +129,10 @@ Baseline JSON files in `tests/baselines/` track pre-existing lint violations per #### Workflows - **`docker-build.yml`** — triggered on pushes to `main` or `develop` and by manual `workflow_dispatch`; detects changed versions, builds images per-platform in parallel, and merges platform-specific digests into multi-arch manifests. +- **`update-devpanel-cli.yml`** — manual `workflow_dispatch`, or `workflow_call` from another workflow. Builds `base/bin/devpanel-amd64` and `base/bin/devpanel-arm64` from `devpanel/cli` on native runners when the Actions cache does not already contain that upstream commit. - **`test.yml`** — runs YAML, shell, and Dockerfile linting plus build/run tests on pushes and pull requests. - Production images are tagged without suffix (e.g. `devpanel/php:8.3-base`); `develop` branch builds use the `-rc` suffix. -- Required repository secrets: `DOCKERHUB_USERNAME`, `DOCKERHUB_TOKEN`. Optional: `GHCR_TOKEN` (falls back to `GITHUB_TOKEN` for GHCR pushes). +- Required repository secrets: `DOCKERHUB_USERNAME`, `DOCKERHUB_TOKEN`. Optional: `GHCR_TOKEN` (falls back to `GITHUB_TOKEN` for GHCR pushes). `update-devpanel-cli.yml` requires `GHCR_TOKEN` with access to `devpanel/cli` and permission to push commits to this repository. ## Environment Variables (base images) diff --git a/README.md b/README.md index 72b29456..9e6e6691 100644 --- a/README.md +++ b/README.md @@ -289,6 +289,13 @@ for setup instructions. runners; AMD64 builds use `ubuntu-26.04`. QEMU is set up on ARM runners to execute the bundled AMD64 DevPanel CLI, and for cross-platform builds. Native AMD64 builds skip QEMU. +- `update-devpanel-cli.yml` rebuilds `base/bin/devpanel-amd64` and + `base/bin/devpanel-arm64` from [devpanel/cli](https://github.com/devpanel/cli). + Run it manually, or call it with `workflow_call`. It caches the upstream + commit it last built and skips the build when that cache entry exists. + Delete the cache entry to rebuild the same commit. The `devpanel/cli` + checkout is placed outside this repository. AMD64 and ARM64 binaries build + in parallel on `ubuntu-26.04` and `ubuntu-26.04-arm`. - The `advance` variant depends on `secure`, which depends on `base`. The build chain is enforced via the bake target dependency graph. diff --git a/TODO.md b/TODO.md index a7fa5f1f..b608893a 100644 --- a/TODO.md +++ b/TODO.md @@ -102,3 +102,16 @@ Use this file to track work that needs to be done. Lint scripts automatically ap - Legacy images use a PEAR release with HTTP chunked response decoding. - OPcache remains loaded on every PHP version without enabling a nonexistent PHP 8.5 shared module. - Focused checks pass; any remaining build or CI blockers are explicitly reported. + +# Task: Rebuild the DevPanel CLI from devpanel/cli + +## Steps +- [x] Add a workflow based on `main` that runs manually or through `workflow_call`. +- [x] Cache the built `devpanel/cli` commit and skip the build when that entry exists. +- [x] Build linux/amd64 and linux/arm64 in parallel on native runners. +- [x] Clone `devpanel/cli` outside this repository and commit only the two binaries. + +## Definition of Done +- `update-devpanel-cli.yml` does not change the image build workflow or Dockerfiles. +- There is no `force` input and no committed revision file. +- A cache miss builds both binaries on `ubuntu-26.04` and `ubuntu-26.04-arm` and commits `base/bin/devpanel-amd64` and `base/bin/devpanel-arm64`. From 00251484223c40b3193f54f24f4d520c91bb3bf4 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Thu, 5 Nov 2026 19:50:44 +0000 Subject: [PATCH 2/7] Run the devpanel CLI update only on demand Drop workflow_call and the skip-ci trailer so a binary commit pushes normally and the image build starts from that push. Co-authored-by: Darren Oh --- .github/workflows/update-devpanel-cli.yml | 18 ++---------------- AGENTS.md | 2 +- README.md | 5 +++-- TODO.md | 2 +- 4 files changed, 7 insertions(+), 20 deletions(-) diff --git a/.github/workflows/update-devpanel-cli.yml b/.github/workflows/update-devpanel-cli.yml index 1bf31e65..4cc64457 100644 --- a/.github/workflows/update-devpanel-cli.yml +++ b/.github/workflows/update-devpanel-cli.yml @@ -4,25 +4,16 @@ name: Update DevPanel CLI binaries # https://github.com/devpanel/cli and commit base/bin/devpanel-amd64 and # base/bin/devpanel-arm64. # -# Run it manually, or call it from another workflow with workflow_call. +# Run it manually. A commit pushed to main or develop starts the image build. # The Actions cache records the devpanel/cli commit that was last built. # Delete that cache entry to rebuild the same commit. This workflow does not # commit a revision file. # # devpanel/cli is cloned under RUNNER_TEMP, outside this repository. # GHCR_TOKEN must be able to read devpanel/cli and push to this repository. -# Binary commits include [skip ci]. on: workflow_dispatch: - workflow_call: - secrets: - GHCR_TOKEN: - required: true - outputs: - head_sha: - description: Commit that contains the devpanel binaries after this run. - value: ${{ jobs.commit.outputs.head_sha || github.sha }} concurrency: group: update-devpanel-cli-${{ github.ref }} @@ -139,8 +130,6 @@ jobs: permissions: actions: write contents: read - outputs: - head_sha: ${{ steps.push.outputs.head_sha }} steps: - name: Checkout repository uses: actions/checkout@v6 @@ -160,7 +149,6 @@ jobs: path: dist/arm64 - name: Commit binaries - id: push env: UPSTREAM_SHA: ${{ needs.plan.outputs.sha }} UPSTREAM_REF: ${{ needs.plan.outputs.ref }} @@ -176,11 +164,9 @@ jobs: git config user.email "41898282+github-actions[bot]@users.noreply.github.com" git commit \ -m "Update devpanel CLI from devpanel/cli@${UPSTREAM_SHA}" \ - -m "Built linux/amd64 and linux/arm64 from https://github.com/devpanel/cli (${UPSTREAM_REF})." \ - -m "[skip ci]" + -m "Built linux/amd64 and linux/arm64 from https://github.com/devpanel/cli (${UPSTREAM_REF})." git push origin "HEAD:${GITHUB_REF_NAME}" fi - echo "head_sha=$(git rev-parse HEAD)" >> "$GITHUB_OUTPUT" - name: Record the built revision env: diff --git a/AGENTS.md b/AGENTS.md index 58092bd8..1777c15b 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -129,7 +129,7 @@ Baseline JSON files in `tests/baselines/` track pre-existing lint violations per #### Workflows - **`docker-build.yml`** — triggered on pushes to `main` or `develop` and by manual `workflow_dispatch`; detects changed versions, builds images per-platform in parallel, and merges platform-specific digests into multi-arch manifests. -- **`update-devpanel-cli.yml`** — manual `workflow_dispatch`, or `workflow_call` from another workflow. Builds `base/bin/devpanel-amd64` and `base/bin/devpanel-arm64` from `devpanel/cli` on native runners when the Actions cache does not already contain that upstream commit. +- **`update-devpanel-cli.yml`** — manual `workflow_dispatch`. Builds `base/bin/devpanel-amd64` and `base/bin/devpanel-arm64` from `devpanel/cli` on native runners when the Actions cache does not already contain that upstream commit. The resulting push to `main` or `develop` starts `docker-build.yml`. - **`test.yml`** — runs YAML, shell, and Dockerfile linting plus build/run tests on pushes and pull requests. - Production images are tagged without suffix (e.g. `devpanel/php:8.3-base`); `develop` branch builds use the `-rc` suffix. - Required repository secrets: `DOCKERHUB_USERNAME`, `DOCKERHUB_TOKEN`. Optional: `GHCR_TOKEN` (falls back to `GITHUB_TOKEN` for GHCR pushes). `update-devpanel-cli.yml` requires `GHCR_TOKEN` with access to `devpanel/cli` and permission to push commits to this repository. diff --git a/README.md b/README.md index 9e6e6691..0eef7576 100644 --- a/README.md +++ b/README.md @@ -291,8 +291,9 @@ for setup instructions. Native AMD64 builds skip QEMU. - `update-devpanel-cli.yml` rebuilds `base/bin/devpanel-amd64` and `base/bin/devpanel-arm64` from [devpanel/cli](https://github.com/devpanel/cli). - Run it manually, or call it with `workflow_call`. It caches the upstream - commit it last built and skips the build when that cache entry exists. + Run it manually. It caches the upstream commit it last built and skips + the build when that cache entry exists. A push to `main` or `develop` + starts the image build. Delete the cache entry to rebuild the same commit. The `devpanel/cli` checkout is placed outside this repository. AMD64 and ARM64 binaries build in parallel on `ubuntu-26.04` and `ubuntu-26.04-arm`. diff --git a/TODO.md b/TODO.md index b608893a..d8cc5ae3 100644 --- a/TODO.md +++ b/TODO.md @@ -106,7 +106,7 @@ Use this file to track work that needs to be done. Lint scripts automatically ap # Task: Rebuild the DevPanel CLI from devpanel/cli ## Steps -- [x] Add a workflow based on `main` that runs manually or through `workflow_call`. +- [x] Add a manual workflow based on `main`. A binary commit starts the image build. - [x] Cache the built `devpanel/cli` commit and skip the build when that entry exists. - [x] Build linux/amd64 and linux/arm64 in parallel on native runners. - [x] Clone `devpanel/cli` outside this repository and commit only the two binaries. From 9f38197e65715857692b99e82212c43f5587691f Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Thu, 5 Nov 2026 19:55:08 +0000 Subject: [PATCH 3/7] Build the devpanel CLI for the runner it is on The matrix job already selects the architecture, so the build script compiles a native binary and no longer takes an architecture argument. Co-authored-by: Darren Oh --- .github/scripts/build-devpanel-cli.sh | 52 +++++------------------ .github/workflows/update-devpanel-cli.yml | 1 - 2 files changed, 10 insertions(+), 43 deletions(-) diff --git a/.github/scripts/build-devpanel-cli.sh b/.github/scripts/build-devpanel-cli.sh index d7d50628..7f8b4bce 100755 --- a/.github/scripts/build-devpanel-cli.sh +++ b/.github/scripts/build-devpanel-cli.sh @@ -2,10 +2,10 @@ # Build one native devpanel CLI binary from a devpanel/cli checkout. # # The binary shipped from this source is github.com/devpanel/cli/cmd/devpanel-client. -# Run this on the runner that matches arch. An amd64 runner builds amd64 and -# an arm64 runner builds arm64. +# The build is native: the workflow chooses an amd64 or arm64 runner, and this +# script compiles for the machine it is running on. # -# Usage: build-devpanel-cli.sh +# Usage: build-devpanel-cli.sh # # GHCR_TOKEN, when set, lets private Go modules download from github.com/devpanel. set -euo pipefail @@ -15,12 +15,12 @@ self_test() { dir=$(mktemp -d) mkdir -p "${dir}/cmd/other" printf 'module example.com/other\n\ngo 1.22\n' >"${dir}/go.mod" - if bash "$0" "$dir" amd64 "${dir}/devpanel" >/dev/null 2>&1; then + if bash "$0" "$dir" "${dir}/devpanel" >/dev/null 2>&1; then echo "ERROR: self-test expected a missing cmd/devpanel-client to fail." >&2 exit 1 fi mkdir -p "${dir}/cmd/devpanel-client" - if bash "$0" "$dir" amd64 "${dir}/devpanel" >/dev/null 2>&1; then + if bash "$0" "$dir" "${dir}/devpanel" >/dev/null 2>&1; then echo "ERROR: self-test expected the wrong Go module to fail." >&2 exit 1 fi @@ -33,35 +33,13 @@ if [[ "${1:-}" == "--self-test" ]]; then exit fi -if [[ $# -ne 3 ]]; then - echo "Usage: $0 " >&2 +if [[ $# -ne 2 ]]; then + echo "Usage: $0 " >&2 exit 1 fi src=$1 -arch=$2 -dest=$3 -case "$arch" in - amd64|arm64) ;; - *) - echo "ERROR: Architecture must be amd64 or arm64 (got '${arch}')." >&2 - exit 1 - ;; -esac - -host_machine=$(uname -m) -case "$host_machine" in - x86_64) host_arch=amd64 ;; - aarch64|arm64) host_arch=arm64 ;; - *) - echo "ERROR: Unsupported runner machine '${host_machine}'." >&2 - exit 1 - ;; -esac -if [[ "$host_arch" != "$arch" ]]; then - echo "ERROR: This runner is ${host_arch}. Build ${arch} on a ${arch} runner." >&2 - exit 1 -fi +dest=$2 if [[ ! -d "$src" ]]; then echo "ERROR: source directory does not exist: ${src}" >&2 @@ -91,23 +69,13 @@ if [[ -n "$token" ]]; then git config --global url."https://x-access-token:${token}@github.com/".insteadOf "https://github.com/" fi -echo "Building linux/${arch}" +echo "Building a native binary" ( cd "$src" - CGO_ENABLED=0 GOOS=linux GOARCH="${arch}" \ + env -u GOOS -u GOARCH CGO_ENABLED=0 \ go build -trimpath -buildvcs=false -o "$dest" ./cmd/devpanel-client ) chmod 0755 "$dest" - -desc=$(file -b "$dest") -if [[ "$arch" == "amd64" && "$desc" != *"x86-64"* ]]; then - echo "ERROR: amd64 build is not an x86-64 executable (${desc})." >&2 - exit 1 -fi -if [[ "$arch" == "arm64" && "$desc" != *"aarch64"* ]]; then - echo "ERROR: arm64 build is not an aarch64 executable (${desc})." >&2 - exit 1 -fi if ! go version -m "$dest" | grep -q 'github.com/devpanel/cli/cmd/devpanel-client'; then echo "ERROR: ${dest} is not github.com/devpanel/cli/cmd/devpanel-client." >&2 exit 1 diff --git a/.github/workflows/update-devpanel-cli.yml b/.github/workflows/update-devpanel-cli.yml index 4cc64457..23acf19b 100644 --- a/.github/workflows/update-devpanel-cli.yml +++ b/.github/workflows/update-devpanel-cli.yml @@ -111,7 +111,6 @@ jobs: run: > bash .github/scripts/build-devpanel-cli.sh ${{ runner.temp }}/devpanel-cli - ${{ matrix.arch }} ${{ runner.temp }}/devpanel-${{ matrix.arch }} - name: Upload binary From bf8c08059df2ca9ca82f945041d1c4060da77836 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Thu, 5 Nov 2026 20:17:29 +0000 Subject: [PATCH 4/7] Track cmd/devpanel-client and keep the rebuild in one workflow Cache the latest commit that changed that directory, cancel a run already in progress, and drop the helper scripts that repeated the same checks. Co-authored-by: Darren Oh --- .github/scripts/build-devpanel-cli.sh | 86 ----------------------- .github/scripts/fetch-devpanel-source.sh | 64 ----------------- .github/scripts/resolve-devpanel-cli.sh | 65 ----------------- .github/workflows/update-devpanel-cli.yml | 84 +++++++++++++++------- AGENTS.md | 2 +- README.md | 15 ++-- TODO.md | 2 +- 7 files changed, 69 insertions(+), 249 deletions(-) delete mode 100755 .github/scripts/build-devpanel-cli.sh delete mode 100755 .github/scripts/fetch-devpanel-source.sh delete mode 100755 .github/scripts/resolve-devpanel-cli.sh diff --git a/.github/scripts/build-devpanel-cli.sh b/.github/scripts/build-devpanel-cli.sh deleted file mode 100755 index 7f8b4bce..00000000 --- a/.github/scripts/build-devpanel-cli.sh +++ /dev/null @@ -1,86 +0,0 @@ -#!/usr/bin/env bash -# Build one native devpanel CLI binary from a devpanel/cli checkout. -# -# The binary shipped from this source is github.com/devpanel/cli/cmd/devpanel-client. -# The build is native: the workflow chooses an amd64 or arm64 runner, and this -# script compiles for the machine it is running on. -# -# Usage: build-devpanel-cli.sh -# -# GHCR_TOKEN, when set, lets private Go modules download from github.com/devpanel. -set -euo pipefail - -self_test() { - local dir - dir=$(mktemp -d) - mkdir -p "${dir}/cmd/other" - printf 'module example.com/other\n\ngo 1.22\n' >"${dir}/go.mod" - if bash "$0" "$dir" "${dir}/devpanel" >/dev/null 2>&1; then - echo "ERROR: self-test expected a missing cmd/devpanel-client to fail." >&2 - exit 1 - fi - mkdir -p "${dir}/cmd/devpanel-client" - if bash "$0" "$dir" "${dir}/devpanel" >/dev/null 2>&1; then - echo "ERROR: self-test expected the wrong Go module to fail." >&2 - exit 1 - fi - rm -rf "$dir" - echo "self-test ok" -} - -if [[ "${1:-}" == "--self-test" ]]; then - self_test - exit -fi - -if [[ $# -ne 2 ]]; then - echo "Usage: $0 " >&2 - exit 1 -fi - -src=$1 -dest=$2 - -if [[ ! -d "$src" ]]; then - echo "ERROR: source directory does not exist: ${src}" >&2 - exit 1 -fi -src=$(cd "$src" && pwd) - -if [[ ! -d "${src}/cmd/devpanel-client" ]]; then - echo "ERROR: ${src}/cmd/devpanel-client does not exist." >&2 - echo "The devpanel binary from this source is github.com/devpanel/cli/cmd/devpanel-client." >&2 - exit 1 -fi -if [[ ! -f "${src}/go.mod" ]]; then - echo "ERROR: ${src}/go.mod does not exist." >&2 - exit 1 -fi -module_path=$(awk '$1 == "module" { print $2; exit }' "${src}/go.mod") -module_path=${module_path#\"} -module_path=${module_path%\"} -if [[ "$module_path" != "github.com/devpanel/cli" ]]; then - echo "ERROR: expected Go module github.com/devpanel/cli, found '${module_path:-none}'." >&2 - exit 1 -fi - -token=${GHCR_TOKEN:-} -if [[ -n "$token" ]]; then - git config --global url."https://x-access-token:${token}@github.com/".insteadOf "https://github.com/" -fi - -echo "Building a native binary" -( - cd "$src" - env -u GOOS -u GOARCH CGO_ENABLED=0 \ - go build -trimpath -buildvcs=false -o "$dest" ./cmd/devpanel-client -) -chmod 0755 "$dest" -if ! go version -m "$dest" | grep -q 'github.com/devpanel/cli/cmd/devpanel-client'; then - echo "ERROR: ${dest} is not github.com/devpanel/cli/cmd/devpanel-client." >&2 - exit 1 -fi - -echo "Checking devpanel version" -"$dest" version -echo "Wrote ${dest}" diff --git a/.github/scripts/fetch-devpanel-source.sh b/.github/scripts/fetch-devpanel-source.sh deleted file mode 100755 index be9a987d..00000000 --- a/.github/scripts/fetch-devpanel-source.sh +++ /dev/null @@ -1,64 +0,0 @@ -#!/usr/bin/env bash -# Clone https://github.com/devpanel/cli at one commit. -# -# The destination must be outside this repository so the clone is not part of -# the php checkout. GitHub Actions should pass a directory under RUNNER_TEMP. -# -# Usage: fetch-devpanel-source.sh -# GHCR_TOKEN Token that can read devpanel/cli. -set -euo pipefail - -self_test() { - local repo_root - repo_root=$(cd "$(dirname "$0")/../.." && pwd) - if bash "$0" "${repo_root}/devpanel-cli-src" "0123456789abcdef0123456789abcdef01234567" >/dev/null 2>&1; then - echo "ERROR: self-test expected a destination inside this repository to fail." >&2 - exit 1 - fi - echo "self-test ok" -} - -if [[ "${1:-}" == "--self-test" ]]; then - self_test - exit -fi - -if [[ $# -ne 2 ]]; then - echo "Usage: $0 " >&2 - exit 1 -fi - -dest=$1 -sha=$2 -if [[ ! "$sha" =~ ^[0-9a-f]{40}$ ]]; then - echo "ERROR: commit SHA must be 40 hexadecimal characters." >&2 - exit 1 -fi - -repo_root=$(cd "$(dirname "$0")/../.." && pwd) -case "$dest" in - "$repo_root"|"$repo_root"/*) - echo "ERROR: Clone devpanel/cli outside this repository (got '${dest}')." >&2 - exit 1 - ;; -esac - -token=${GHCR_TOKEN:-} -if [[ -z "$token" ]]; then - echo "ERROR: GHCR_TOKEN is required to read https://github.com/devpanel/cli." >&2 - exit 1 -fi - -if [[ -e "$dest" ]]; then - echo "ERROR: destination already exists: ${dest}" >&2 - exit 1 -fi - -mkdir -p "$(dirname "$dest")" -mkdir "$dest" -git -C "$dest" init -git -C "$dest" remote add origin https://github.com/devpanel/cli.git -git -C "$dest" -c "http.extraheader=AUTHORIZATION: bearer ${token}" \ - fetch --depth 1 origin "$sha" -git -C "$dest" checkout --detach FETCH_HEAD -echo "Cloned devpanel/cli ${sha} to ${dest}" diff --git a/.github/scripts/resolve-devpanel-cli.sh b/.github/scripts/resolve-devpanel-cli.sh deleted file mode 100755 index 31ff01ec..00000000 --- a/.github/scripts/resolve-devpanel-cli.sh +++ /dev/null @@ -1,65 +0,0 @@ -#!/usr/bin/env bash -# Resolve the default-branch commit of https://github.com/devpanel/cli. -# -# GHCR_TOKEN Token that can read that repository. -# GITHUB_OUTPUT When set, receives sha and ref. -set -euo pipefail - -token=${GHCR_TOKEN:-} -if [[ -z "$token" ]]; then - echo "ERROR: GHCR_TOKEN is required to read https://github.com/devpanel/cli." >&2 - echo "::error::GHCR_TOKEN is required to read https://github.com/devpanel/cli." - exit 1 -fi - -api_get() { - local url=$1 - local body code - body=$(mktemp) - code=$( - curl -sS \ - -H "Authorization: Bearer ${token}" \ - -H "Accept: application/vnd.github+json" \ - -H "X-GitHub-Api-Version: 2022-11-28" \ - -o "$body" \ - -w "%{http_code}" \ - "$url" || true - ) - if [[ "$code" != "200" ]]; then - echo "ERROR: GitHub API ${url} returned HTTP ${code:-unknown}." >&2 - jq -r '.message // empty' "$body" >&2 || true - rm -f "$body" - return 1 - fi - cat "$body" - rm -f "$body" -} - -repo_json=$(api_get "https://api.github.com/repos/devpanel/cli") || { - echo "::error::Unable to read https://github.com/devpanel/cli. GHCR_TOKEN needs access to that repository." - exit 1 -} -ref=$(jq -r '.default_branch // empty' <<<"$repo_json") -if [[ -z "$ref" ]]; then - echo "ERROR: devpanel/cli did not report a default branch." >&2 - exit 1 -fi - -encoded_ref=$(jq -rn --arg ref "$ref" '$ref | @uri') -commit_json=$(api_get "https://api.github.com/repos/devpanel/cli/commits/${encoded_ref}") || { - echo "::error::Unable to resolve devpanel/cli ref '${ref}'." - exit 1 -} -sha=$(jq -r '.sha // empty' <<<"$commit_json") -if [[ ! "$sha" =~ ^[0-9a-f]{40}$ ]]; then - echo "ERROR: devpanel/cli ref '${ref}' did not resolve to a commit." >&2 - exit 1 -fi - -echo "devpanel/cli ${ref} is ${sha}" -if [[ -n "${GITHUB_OUTPUT:-}" ]]; then - { - echo "sha=${sha}" - echo "ref=${ref}" - } >>"$GITHUB_OUTPUT" -fi diff --git a/.github/workflows/update-devpanel-cli.yml b/.github/workflows/update-devpanel-cli.yml index 23acf19b..0cb9c219 100644 --- a/.github/workflows/update-devpanel-cli.yml +++ b/.github/workflows/update-devpanel-cli.yml @@ -1,11 +1,11 @@ name: Update DevPanel CLI binaries # Build linux/amd64 and linux/arm64 devpanel binaries from -# https://github.com/devpanel/cli and commit base/bin/devpanel-amd64 and -# base/bin/devpanel-arm64. +# https://github.com/devpanel/cli/tree/HEAD/cmd/devpanel-client and commit +# base/bin/devpanel-amd64 and base/bin/devpanel-arm64. # # Run it manually. A commit pushed to main or develop starts the image build. -# The Actions cache records the devpanel/cli commit that was last built. +# The Actions cache records the latest commit that changed cmd/devpanel-client. # Delete that cache entry to rebuild the same commit. This workflow does not # commit a revision file. # @@ -17,23 +17,19 @@ on: concurrency: group: update-devpanel-cli-${{ github.ref }} - cancel-in-progress: false + cancel-in-progress: true jobs: plan: - name: Check cached devpanel/cli revision + name: Check cached devpanel-client revision runs-on: ubuntu-26.04 permissions: actions: read contents: read outputs: sha: ${{ steps.upstream.outputs.sha }} - ref: ${{ steps.upstream.outputs.ref }} changed: ${{ steps.decision.outputs.changed }} steps: - - name: Checkout repository - uses: actions/checkout@v6 - - name: Require GHCR_TOKEN env: GHCR_TOKEN: ${{ secrets.GHCR_TOKEN }} @@ -43,18 +39,43 @@ jobs: exit 1 fi - - name: Resolve devpanel/cli commit + - name: Resolve latest cmd/devpanel-client commit id: upstream env: GHCR_TOKEN: ${{ secrets.GHCR_TOKEN }} - run: bash .github/scripts/resolve-devpanel-cli.sh + run: | + set -euo pipefail + body=$(mktemp) + code=$( + curl -sS \ + -H "Authorization: Bearer ${GHCR_TOKEN}" \ + -H "Accept: application/vnd.github+json" \ + -H "X-GitHub-Api-Version: 2022-11-28" \ + -o "$body" \ + -w "%{http_code}" \ + "https://api.github.com/repos/devpanel/cli/commits?path=cmd/devpanel-client&per_page=1" \ + || true + ) + if [[ "$code" != "200" ]]; then + echo "::error::Unable to read cmd/devpanel-client history (HTTP ${code:-unknown})." + jq -r '.message // empty' "$body" >&2 || true + exit 1 + fi + sha=$(jq -r '.[0].sha // empty' "$body") + rm -f "$body" + if [[ ! "$sha" =~ ^[0-9a-f]{40}$ ]]; then + echo "::error::cmd/devpanel-client has no commits on the default branch." + exit 1 + fi + echo "sha=${sha}" >> "$GITHUB_OUTPUT" + echo "Latest cmd/devpanel-client commit is ${sha}" - name: Restore the built revision id: revision-cache uses: actions/cache/restore@v5 with: path: ${{ runner.temp }}/devpanel-cli-revision - key: devpanel-cli-${{ steps.upstream.outputs.sha }} + key: devpanel-client-${{ steps.upstream.outputs.sha }} - name: Decide whether to rebuild id: decision @@ -68,10 +89,10 @@ jobs: echo "Tagged commits are left unchanged." elif [[ "${CACHE_HIT}" == "true" ]]; then echo "changed=false" >> "$GITHUB_OUTPUT" - echo "devpanel/cli ${UPSTREAM_SHA} is already built." + echo "cmd/devpanel-client ${UPSTREAM_SHA} is already built." else echo "changed=true" >> "$GITHUB_OUTPUT" - echo "devpanel/cli ${UPSTREAM_SHA} is not in the Actions cache." + echo "cmd/devpanel-client ${UPSTREAM_SHA} is not in the Actions cache." fi build: @@ -90,14 +111,19 @@ jobs: permissions: contents: read steps: - - name: Checkout repository - uses: actions/checkout@v6 - - name: Clone devpanel/cli env: GHCR_TOKEN: ${{ secrets.GHCR_TOKEN }} UPSTREAM_SHA: ${{ needs.plan.outputs.sha }} - run: bash .github/scripts/fetch-devpanel-source.sh "${RUNNER_TEMP}/devpanel-cli" "${UPSTREAM_SHA}" + run: | + set -euo pipefail + dest="${RUNNER_TEMP}/devpanel-cli" + mkdir "$dest" + git -C "$dest" init + git -C "$dest" remote add origin https://github.com/devpanel/cli.git + git -C "$dest" -c "http.extraheader=AUTHORIZATION: bearer ${GHCR_TOKEN}" \ + fetch --depth 1 origin "$UPSTREAM_SHA" + git -C "$dest" checkout --detach FETCH_HEAD - name: Set up Go uses: actions/setup-go@v5 @@ -108,10 +134,19 @@ jobs: - name: Build native binary env: GHCR_TOKEN: ${{ secrets.GHCR_TOKEN }} - run: > - bash .github/scripts/build-devpanel-cli.sh - ${{ runner.temp }}/devpanel-cli - ${{ runner.temp }}/devpanel-${{ matrix.arch }} + run: | + set -euo pipefail + if [[ -n "${GHCR_TOKEN}" ]]; then + git config --global url."https://x-access-token:${GHCR_TOKEN}@github.com/".insteadOf "https://github.com/" + fi + dest="${RUNNER_TEMP}/devpanel-${{ matrix.arch }}" + ( + cd "${RUNNER_TEMP}/devpanel-cli" + env -u GOOS -u GOARCH CGO_ENABLED=0 \ + go build -trimpath -buildvcs=false -o "$dest" ./cmd/devpanel-client + ) + chmod 0755 "$dest" + "$dest" version - name: Upload binary uses: actions/upload-artifact@v7 @@ -150,7 +185,6 @@ jobs: - name: Commit binaries env: UPSTREAM_SHA: ${{ needs.plan.outputs.sha }} - UPSTREAM_REF: ${{ needs.plan.outputs.ref }} run: | set -euo pipefail install -m 0755 dist/amd64/devpanel-amd64 base/bin/devpanel-amd64 @@ -163,7 +197,7 @@ jobs: git config user.email "41898282+github-actions[bot]@users.noreply.github.com" git commit \ -m "Update devpanel CLI from devpanel/cli@${UPSTREAM_SHA}" \ - -m "Built linux/amd64 and linux/arm64 from https://github.com/devpanel/cli (${UPSTREAM_REF})." + -m "Built linux/amd64 and linux/arm64 from cmd/devpanel-client at that commit." git push origin "HEAD:${GITHUB_REF_NAME}" fi @@ -178,4 +212,4 @@ jobs: uses: actions/cache/save@v5 with: path: ${{ runner.temp }}/devpanel-cli-revision - key: devpanel-cli-${{ needs.plan.outputs.sha }} + key: devpanel-client-${{ needs.plan.outputs.sha }} diff --git a/AGENTS.md b/AGENTS.md index 1777c15b..3ba41ea2 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -129,7 +129,7 @@ Baseline JSON files in `tests/baselines/` track pre-existing lint violations per #### Workflows - **`docker-build.yml`** — triggered on pushes to `main` or `develop` and by manual `workflow_dispatch`; detects changed versions, builds images per-platform in parallel, and merges platform-specific digests into multi-arch manifests. -- **`update-devpanel-cli.yml`** — manual `workflow_dispatch`. Builds `base/bin/devpanel-amd64` and `base/bin/devpanel-arm64` from `devpanel/cli` on native runners when the Actions cache does not already contain that upstream commit. The resulting push to `main` or `develop` starts `docker-build.yml`. +- **`update-devpanel-cli.yml`** — manual `workflow_dispatch`. Builds `base/bin/devpanel-amd64` and `base/bin/devpanel-arm64` from the latest `devpanel/cli` commit that changed `cmd/devpanel-client`, when the Actions cache does not already contain that commit. A new run cancels one already in progress. The resulting push to `main` or `develop` starts `docker-build.yml`. - **`test.yml`** — runs YAML, shell, and Dockerfile linting plus build/run tests on pushes and pull requests. - Production images are tagged without suffix (e.g. `devpanel/php:8.3-base`); `develop` branch builds use the `-rc` suffix. - Required repository secrets: `DOCKERHUB_USERNAME`, `DOCKERHUB_TOKEN`. Optional: `GHCR_TOKEN` (falls back to `GITHUB_TOKEN` for GHCR pushes). `update-devpanel-cli.yml` requires `GHCR_TOKEN` with access to `devpanel/cli` and permission to push commits to this repository. diff --git a/README.md b/README.md index 0eef7576..d6325c77 100644 --- a/README.md +++ b/README.md @@ -290,13 +290,14 @@ for setup instructions. to execute the bundled AMD64 DevPanel CLI, and for cross-platform builds. Native AMD64 builds skip QEMU. - `update-devpanel-cli.yml` rebuilds `base/bin/devpanel-amd64` and - `base/bin/devpanel-arm64` from [devpanel/cli](https://github.com/devpanel/cli). - Run it manually. It caches the upstream commit it last built and skips - the build when that cache entry exists. A push to `main` or `develop` - starts the image build. - Delete the cache entry to rebuild the same commit. The `devpanel/cli` - checkout is placed outside this repository. AMD64 and ARM64 binaries build - in parallel on `ubuntu-26.04` and `ubuntu-26.04-arm`. + `base/bin/devpanel-arm64` from the latest + [devpanel/cli](https://github.com/devpanel/cli) commit that changed + `cmd/devpanel-client`. Run it manually. It caches that commit and skips + the build when the cache entry exists. A new run cancels one already in + progress. A push to `main` or `develop` starts the image build. Delete + the cache entry to rebuild the same commit. The `devpanel/cli` checkout + is placed outside this repository. AMD64 and ARM64 binaries build in + parallel on `ubuntu-26.04` and `ubuntu-26.04-arm`. - The `advance` variant depends on `secure`, which depends on `base`. The build chain is enforced via the bake target dependency graph. diff --git a/TODO.md b/TODO.md index d8cc5ae3..96aefb7f 100644 --- a/TODO.md +++ b/TODO.md @@ -107,7 +107,7 @@ Use this file to track work that needs to be done. Lint scripts automatically ap ## Steps - [x] Add a manual workflow based on `main`. A binary commit starts the image build. -- [x] Cache the built `devpanel/cli` commit and skip the build when that entry exists. +- [x] Cache the latest commit that changed `cmd/devpanel-client` and skip the build when that entry exists. - [x] Build linux/amd64 and linux/arm64 in parallel on native runners. - [x] Clone `devpanel/cli` outside this repository and commit only the two binaries. From 6567a4c84c48e9cd77fba2b2d0df8fb8400d5fe5 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Thu, 5 Nov 2026 20:28:50 +0000 Subject: [PATCH 5/7] Commit devpanel binaries with the workflow token Give the commit job contents write permission and use that token for the push. GHCR_TOKEN remains the credential that reads devpanel/cli. Co-authored-by: Darren Oh --- .github/workflows/update-devpanel-cli.yml | 15 ++++++--------- AGENTS.md | 4 ++-- README.md | 6 +++--- TODO.md | 4 +--- 4 files changed, 12 insertions(+), 17 deletions(-) diff --git a/.github/workflows/update-devpanel-cli.yml b/.github/workflows/update-devpanel-cli.yml index 0cb9c219..d9c97495 100644 --- a/.github/workflows/update-devpanel-cli.yml +++ b/.github/workflows/update-devpanel-cli.yml @@ -4,13 +4,12 @@ name: Update DevPanel CLI binaries # https://github.com/devpanel/cli/tree/HEAD/cmd/devpanel-client and commit # base/bin/devpanel-amd64 and base/bin/devpanel-arm64. # -# Run it manually. A commit pushed to main or develop starts the image build. -# The Actions cache records the latest commit that changed cmd/devpanel-client. -# Delete that cache entry to rebuild the same commit. This workflow does not -# commit a revision file. +# Run it manually. The Actions cache records the latest commit that changed +# cmd/devpanel-client. Delete that cache entry to rebuild the same commit. # # devpanel/cli is cloned under RUNNER_TEMP, outside this repository. -# GHCR_TOKEN must be able to read devpanel/cli and push to this repository. +# GHCR_TOKEN must be able to read devpanel/cli. The workflow token commits +# the binaries. on: workflow_dispatch: @@ -35,7 +34,7 @@ jobs: GHCR_TOKEN: ${{ secrets.GHCR_TOKEN }} run: | if [[ -z "${GHCR_TOKEN}" ]]; then - echo "::error::GHCR_TOKEN is required to read devpanel/cli and commit the binaries." + echo "::error::GHCR_TOKEN is required to read devpanel/cli." exit 1 fi @@ -163,12 +162,10 @@ jobs: runs-on: ubuntu-26.04 permissions: actions: write - contents: read + contents: write steps: - name: Checkout repository uses: actions/checkout@v6 - with: - token: ${{ secrets.GHCR_TOKEN }} - name: Download amd64 binary uses: actions/download-artifact@v8 diff --git a/AGENTS.md b/AGENTS.md index 3ba41ea2..292b2654 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -129,10 +129,10 @@ Baseline JSON files in `tests/baselines/` track pre-existing lint violations per #### Workflows - **`docker-build.yml`** — triggered on pushes to `main` or `develop` and by manual `workflow_dispatch`; detects changed versions, builds images per-platform in parallel, and merges platform-specific digests into multi-arch manifests. -- **`update-devpanel-cli.yml`** — manual `workflow_dispatch`. Builds `base/bin/devpanel-amd64` and `base/bin/devpanel-arm64` from the latest `devpanel/cli` commit that changed `cmd/devpanel-client`, when the Actions cache does not already contain that commit. A new run cancels one already in progress. The resulting push to `main` or `develop` starts `docker-build.yml`. +- **`update-devpanel-cli.yml`** — manual `workflow_dispatch`. Builds `base/bin/devpanel-amd64` and `base/bin/devpanel-arm64` from the latest `devpanel/cli` commit that changed `cmd/devpanel-client`, when the Actions cache does not already contain that commit. A new run cancels one already in progress. The workflow token commits the binaries. - **`test.yml`** — runs YAML, shell, and Dockerfile linting plus build/run tests on pushes and pull requests. - Production images are tagged without suffix (e.g. `devpanel/php:8.3-base`); `develop` branch builds use the `-rc` suffix. -- Required repository secrets: `DOCKERHUB_USERNAME`, `DOCKERHUB_TOKEN`. Optional: `GHCR_TOKEN` (falls back to `GITHUB_TOKEN` for GHCR pushes). `update-devpanel-cli.yml` requires `GHCR_TOKEN` with access to `devpanel/cli` and permission to push commits to this repository. +- Required repository secrets: `DOCKERHUB_USERNAME`, `DOCKERHUB_TOKEN`. Optional: `GHCR_TOKEN` (falls back to `GITHUB_TOKEN` for GHCR pushes). `update-devpanel-cli.yml` requires `GHCR_TOKEN` with access to `devpanel/cli`. ## Environment Variables (base images) diff --git a/README.md b/README.md index d6325c77..76a5c3c6 100644 --- a/README.md +++ b/README.md @@ -294,9 +294,9 @@ for setup instructions. [devpanel/cli](https://github.com/devpanel/cli) commit that changed `cmd/devpanel-client`. Run it manually. It caches that commit and skips the build when the cache entry exists. A new run cancels one already in - progress. A push to `main` or `develop` starts the image build. Delete - the cache entry to rebuild the same commit. The `devpanel/cli` checkout - is placed outside this repository. AMD64 and ARM64 binaries build in + progress. The workflow token commits the binaries. Delete the cache + entry to rebuild the same commit. The `devpanel/cli` checkout is placed + outside this repository. AMD64 and ARM64 binaries build in parallel on `ubuntu-26.04` and `ubuntu-26.04-arm`. - The `advance` variant depends on `secure`, which depends on `base`. The build chain is enforced via the bake target dependency graph. diff --git a/TODO.md b/TODO.md index 96aefb7f..34171c0a 100644 --- a/TODO.md +++ b/TODO.md @@ -106,12 +106,10 @@ Use this file to track work that needs to be done. Lint scripts automatically ap # Task: Rebuild the DevPanel CLI from devpanel/cli ## Steps -- [x] Add a manual workflow based on `main`. A binary commit starts the image build. +- [x] Add a manual workflow based on `main`. The workflow token commits the binaries. - [x] Cache the latest commit that changed `cmd/devpanel-client` and skip the build when that entry exists. - [x] Build linux/amd64 and linux/arm64 in parallel on native runners. - [x] Clone `devpanel/cli` outside this repository and commit only the two binaries. ## Definition of Done -- `update-devpanel-cli.yml` does not change the image build workflow or Dockerfiles. -- There is no `force` input and no committed revision file. - A cache miss builds both binaries on `ubuntu-26.04` and `ubuntu-26.04-arm` and commits `base/bin/devpanel-amd64` and `base/bin/devpanel-arm64`. From 98a651f7b4454c666d775b62960ce783a97dd8b7 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Thu, 5 Nov 2026 20:32:37 +0000 Subject: [PATCH 6/7] Let the image build call the devpanel CLI update Restore workflow_call now that the workflow token's push does not start another image build. Return the commit that contains the binaries. Co-authored-by: Darren Oh --- .github/workflows/update-devpanel-cli.yml | 18 +++++++++++++++++- AGENTS.md | 2 +- README.md | 12 +++++++----- TODO.md | 2 +- 4 files changed, 26 insertions(+), 8 deletions(-) diff --git a/.github/workflows/update-devpanel-cli.yml b/.github/workflows/update-devpanel-cli.yml index d9c97495..3b1d3372 100644 --- a/.github/workflows/update-devpanel-cli.yml +++ b/.github/workflows/update-devpanel-cli.yml @@ -4,7 +4,11 @@ name: Update DevPanel CLI binaries # https://github.com/devpanel/cli/tree/HEAD/cmd/devpanel-client and commit # base/bin/devpanel-amd64 and base/bin/devpanel-arm64. # -# Run it manually. The Actions cache records the latest commit that changed +# Run it manually, or call it from another workflow. The caller passes +# GHCR_TOKEN and grants contents: write. head_sha is the commit that +# contains the binaries. +# +# The Actions cache records the latest commit that changed # cmd/devpanel-client. Delete that cache entry to rebuild the same commit. # # devpanel/cli is cloned under RUNNER_TEMP, outside this repository. @@ -13,6 +17,14 @@ name: Update DevPanel CLI binaries on: workflow_dispatch: + workflow_call: + secrets: + GHCR_TOKEN: + required: true + outputs: + head_sha: + description: Commit that contains the devpanel binaries after this run. + value: ${{ jobs.commit.outputs.head_sha || github.sha }} concurrency: group: update-devpanel-cli-${{ github.ref }} @@ -163,6 +175,8 @@ jobs: permissions: actions: write contents: write + outputs: + head_sha: ${{ steps.push.outputs.head_sha }} steps: - name: Checkout repository uses: actions/checkout@v6 @@ -180,6 +194,7 @@ jobs: path: dist/arm64 - name: Commit binaries + id: push env: UPSTREAM_SHA: ${{ needs.plan.outputs.sha }} run: | @@ -197,6 +212,7 @@ jobs: -m "Built linux/amd64 and linux/arm64 from cmd/devpanel-client at that commit." git push origin "HEAD:${GITHUB_REF_NAME}" fi + echo "head_sha=$(git rev-parse HEAD)" >> "$GITHUB_OUTPUT" - name: Record the built revision env: diff --git a/AGENTS.md b/AGENTS.md index 292b2654..8254f680 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -129,7 +129,7 @@ Baseline JSON files in `tests/baselines/` track pre-existing lint violations per #### Workflows - **`docker-build.yml`** — triggered on pushes to `main` or `develop` and by manual `workflow_dispatch`; detects changed versions, builds images per-platform in parallel, and merges platform-specific digests into multi-arch manifests. -- **`update-devpanel-cli.yml`** — manual `workflow_dispatch`. Builds `base/bin/devpanel-amd64` and `base/bin/devpanel-arm64` from the latest `devpanel/cli` commit that changed `cmd/devpanel-client`, when the Actions cache does not already contain that commit. A new run cancels one already in progress. The workflow token commits the binaries. +- **`update-devpanel-cli.yml`** — manual `workflow_dispatch`, or `workflow_call` from another workflow. The caller passes `GHCR_TOKEN` and grants `contents: write`. Builds `base/bin/devpanel-amd64` and `base/bin/devpanel-arm64` from the latest `devpanel/cli` commit that changed `cmd/devpanel-client`, when the Actions cache does not already contain that commit. A new run cancels one already in progress. The workflow token commits the binaries and returns that commit as `head_sha`. - **`test.yml`** — runs YAML, shell, and Dockerfile linting plus build/run tests on pushes and pull requests. - Production images are tagged without suffix (e.g. `devpanel/php:8.3-base`); `develop` branch builds use the `-rc` suffix. - Required repository secrets: `DOCKERHUB_USERNAME`, `DOCKERHUB_TOKEN`. Optional: `GHCR_TOKEN` (falls back to `GITHUB_TOKEN` for GHCR pushes). `update-devpanel-cli.yml` requires `GHCR_TOKEN` with access to `devpanel/cli`. diff --git a/README.md b/README.md index 76a5c3c6..9f1ddc67 100644 --- a/README.md +++ b/README.md @@ -292,11 +292,13 @@ for setup instructions. - `update-devpanel-cli.yml` rebuilds `base/bin/devpanel-amd64` and `base/bin/devpanel-arm64` from the latest [devpanel/cli](https://github.com/devpanel/cli) commit that changed - `cmd/devpanel-client`. Run it manually. It caches that commit and skips - the build when the cache entry exists. A new run cancels one already in - progress. The workflow token commits the binaries. Delete the cache - entry to rebuild the same commit. The `devpanel/cli` checkout is placed - outside this repository. AMD64 and ARM64 binaries build in + `cmd/devpanel-client`. Run it manually, or call it from another workflow + with `workflow_call` (pass `GHCR_TOKEN` and grant `contents: write`). + It caches that commit and skips the build when the cache entry exists. + A new run cancels one already in progress. The workflow token commits + the binaries and the call returns that commit as `head_sha`. Delete the + cache entry to rebuild the same commit. The `devpanel/cli` checkout is + placed outside this repository. AMD64 and ARM64 binaries build in parallel on `ubuntu-26.04` and `ubuntu-26.04-arm`. - The `advance` variant depends on `secure`, which depends on `base`. The build chain is enforced via the bake target dependency graph. diff --git a/TODO.md b/TODO.md index 34171c0a..120dd9ee 100644 --- a/TODO.md +++ b/TODO.md @@ -106,7 +106,7 @@ Use this file to track work that needs to be done. Lint scripts automatically ap # Task: Rebuild the DevPanel CLI from devpanel/cli ## Steps -- [x] Add a manual workflow based on `main`. The workflow token commits the binaries. +- [x] Add a workflow based on `main` that runs manually or through `workflow_call`. The workflow token commits the binaries. - [x] Cache the latest commit that changed `cmd/devpanel-client` and skip the build when that entry exists. - [x] Build linux/amd64 and linux/arm64 in parallel on native runners. - [x] Clone `devpanel/cli` outside this repository and commit only the two binaries. From 40720347cc37db0337a28269042b2924386844d1 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Thu, 5 Nov 2026 20:53:37 +0000 Subject: [PATCH 7/7] Make the devpanel CLI update a manual workflow Remove workflow_call so the binary rebuild runs only from Run workflow and does not return a commit to a caller. Co-authored-by: Darren Oh --- .github/workflows/update-devpanel-cli.yml | 18 +----------------- AGENTS.md | 2 +- README.md | 10 ++++------ TODO.md | 2 +- 4 files changed, 7 insertions(+), 25 deletions(-) diff --git a/.github/workflows/update-devpanel-cli.yml b/.github/workflows/update-devpanel-cli.yml index 3b1d3372..d9c97495 100644 --- a/.github/workflows/update-devpanel-cli.yml +++ b/.github/workflows/update-devpanel-cli.yml @@ -4,11 +4,7 @@ name: Update DevPanel CLI binaries # https://github.com/devpanel/cli/tree/HEAD/cmd/devpanel-client and commit # base/bin/devpanel-amd64 and base/bin/devpanel-arm64. # -# Run it manually, or call it from another workflow. The caller passes -# GHCR_TOKEN and grants contents: write. head_sha is the commit that -# contains the binaries. -# -# The Actions cache records the latest commit that changed +# Run it manually. The Actions cache records the latest commit that changed # cmd/devpanel-client. Delete that cache entry to rebuild the same commit. # # devpanel/cli is cloned under RUNNER_TEMP, outside this repository. @@ -17,14 +13,6 @@ name: Update DevPanel CLI binaries on: workflow_dispatch: - workflow_call: - secrets: - GHCR_TOKEN: - required: true - outputs: - head_sha: - description: Commit that contains the devpanel binaries after this run. - value: ${{ jobs.commit.outputs.head_sha || github.sha }} concurrency: group: update-devpanel-cli-${{ github.ref }} @@ -175,8 +163,6 @@ jobs: permissions: actions: write contents: write - outputs: - head_sha: ${{ steps.push.outputs.head_sha }} steps: - name: Checkout repository uses: actions/checkout@v6 @@ -194,7 +180,6 @@ jobs: path: dist/arm64 - name: Commit binaries - id: push env: UPSTREAM_SHA: ${{ needs.plan.outputs.sha }} run: | @@ -212,7 +197,6 @@ jobs: -m "Built linux/amd64 and linux/arm64 from cmd/devpanel-client at that commit." git push origin "HEAD:${GITHUB_REF_NAME}" fi - echo "head_sha=$(git rev-parse HEAD)" >> "$GITHUB_OUTPUT" - name: Record the built revision env: diff --git a/AGENTS.md b/AGENTS.md index 8254f680..292b2654 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -129,7 +129,7 @@ Baseline JSON files in `tests/baselines/` track pre-existing lint violations per #### Workflows - **`docker-build.yml`** — triggered on pushes to `main` or `develop` and by manual `workflow_dispatch`; detects changed versions, builds images per-platform in parallel, and merges platform-specific digests into multi-arch manifests. -- **`update-devpanel-cli.yml`** — manual `workflow_dispatch`, or `workflow_call` from another workflow. The caller passes `GHCR_TOKEN` and grants `contents: write`. Builds `base/bin/devpanel-amd64` and `base/bin/devpanel-arm64` from the latest `devpanel/cli` commit that changed `cmd/devpanel-client`, when the Actions cache does not already contain that commit. A new run cancels one already in progress. The workflow token commits the binaries and returns that commit as `head_sha`. +- **`update-devpanel-cli.yml`** — manual `workflow_dispatch`. Builds `base/bin/devpanel-amd64` and `base/bin/devpanel-arm64` from the latest `devpanel/cli` commit that changed `cmd/devpanel-client`, when the Actions cache does not already contain that commit. A new run cancels one already in progress. The workflow token commits the binaries. - **`test.yml`** — runs YAML, shell, and Dockerfile linting plus build/run tests on pushes and pull requests. - Production images are tagged without suffix (e.g. `devpanel/php:8.3-base`); `develop` branch builds use the `-rc` suffix. - Required repository secrets: `DOCKERHUB_USERNAME`, `DOCKERHUB_TOKEN`. Optional: `GHCR_TOKEN` (falls back to `GITHUB_TOKEN` for GHCR pushes). `update-devpanel-cli.yml` requires `GHCR_TOKEN` with access to `devpanel/cli`. diff --git a/README.md b/README.md index 9f1ddc67..caedc07f 100644 --- a/README.md +++ b/README.md @@ -292,12 +292,10 @@ for setup instructions. - `update-devpanel-cli.yml` rebuilds `base/bin/devpanel-amd64` and `base/bin/devpanel-arm64` from the latest [devpanel/cli](https://github.com/devpanel/cli) commit that changed - `cmd/devpanel-client`. Run it manually, or call it from another workflow - with `workflow_call` (pass `GHCR_TOKEN` and grant `contents: write`). - It caches that commit and skips the build when the cache entry exists. - A new run cancels one already in progress. The workflow token commits - the binaries and the call returns that commit as `head_sha`. Delete the - cache entry to rebuild the same commit. The `devpanel/cli` checkout is + `cmd/devpanel-client`. Run it manually. It caches that commit and skips + the build when the cache entry exists. A new run cancels one already in + progress. The workflow token commits the binaries. Delete the cache + entry to rebuild the same commit. The `devpanel/cli` checkout is placed outside this repository. AMD64 and ARM64 binaries build in parallel on `ubuntu-26.04` and `ubuntu-26.04-arm`. - The `advance` variant depends on `secure`, which depends on `base`. diff --git a/TODO.md b/TODO.md index 120dd9ee..34171c0a 100644 --- a/TODO.md +++ b/TODO.md @@ -106,7 +106,7 @@ Use this file to track work that needs to be done. Lint scripts automatically ap # Task: Rebuild the DevPanel CLI from devpanel/cli ## Steps -- [x] Add a workflow based on `main` that runs manually or through `workflow_call`. The workflow token commits the binaries. +- [x] Add a manual workflow based on `main`. The workflow token commits the binaries. - [x] Cache the latest commit that changed `cmd/devpanel-client` and skip the build when that entry exists. - [x] Build linux/amd64 and linux/arm64 in parallel on native runners. - [x] Clone `devpanel/cli` outside this repository and commit only the two binaries.