diff --git a/.github/actions/build-php-images/action.yml b/.github/actions/build-php-images/action.yml index e20a2aa1..f192cee5 100644 --- a/.github/actions/build-php-images/action.yml +++ b/.github/actions/build-php-images/action.yml @@ -83,7 +83,7 @@ inputs: bake_overrides_file: description: > Path to a pre-generated bake overrides JSON file that maps each - phpX_Y-downloader target to the correct code-server / Copilot Chat + phpX_Y-downloader target to the correct code-server versions and SHA256 digests for its Debian tier. Pass the path to the preseed-bake-overrides artifact downloaded from the detect job. required: true @@ -114,6 +114,7 @@ runs: password: ${{ inputs.ghcr_user && inputs.ghcr_token || inputs.github_token }} - name: Set up QEMU + if: ${{ !((runner.arch == 'ARM64' && startsWith(inputs.platform, 'linux/arm64')) || (runner.arch == 'X64' && startsWith(inputs.platform, 'linux/amd64'))) }} uses: docker/setup-qemu-action@v4 - name: Set up Docker Buildx diff --git a/.github/actions/preseed-downloads/action.yml b/.github/actions/preseed-downloads/action.yml index d35f6fb9..4840bacb 100644 --- a/.github/actions/preseed-downloads/action.yml +++ b/.github/actions/preseed-downloads/action.yml @@ -1,7 +1,7 @@ name: Pre-seed build downloads description: > Detect which Debian codename each PHP version runs on, resolve the latest - compatible code-server and Copilot Chat versions for each detected codename, + compatible code-server version for each detected codename, restore previously downloaded files from the actions/cache, and download them on a cache miss. Produces a JSON bake-override file that maps each phpX_Y-downloader target to the correct versions and SHA256 digests for @@ -32,7 +32,7 @@ inputs: description: > When "true", always download fresh artifacts even when a cache entry already exists. Cache entries are keyed from the resolved Debian - codename and code-server / Copilot Chat versions, so a new cache entry + codename and code-server versions, so a new cache entry is created automatically when any of those resolved values changes. If the resolved codename and versions are unchanged, the cache key is also unchanged and the existing cache entry remains current. Use this @@ -49,7 +49,7 @@ outputs: bake_overrides_file: description: > Path to a JSON bake-override file that maps each phpX_Y-downloader target - to the code-server and Copilot Chat versions and SHA256 digests resolved + to the code-server versions and SHA256 digests resolved for its Debian tier. Pass to docker buildx bake as an additional --file. value: ${{ steps.assemble.outputs.bake_overrides_file }} @@ -174,35 +174,6 @@ runs: 'https://api.github.com/repos/coder/code-server/releases?per_page=100') printf '%s' "$CODESERVER_RELEASES_JSON" > "$RUNNER_TEMP/cs-releases.json" - MARKETPLACE_JSON=$(curl -fsSL --retry 3 --retry-all-errors --connect-timeout 30 \ - -X POST \ - -H 'Content-Type: application/json' \ - -H 'Accept: application/json;api-version=3.0-preview.1' \ - -d '{"filters":[{"criteria":[{"filterType":7,"value":"GitHub.copilot-chat"}],"pageSize":100}],"flags":17}' \ - 'https://marketplace.visualstudio.com/_apis/public/gallery/extensionquery') - printf '%s' "$MARKETPLACE_JSON" > "$RUNNER_TEMP/marketplace.json" - - # Emits stable Copilot Chat versions compatible with the given VS Code - # version, sorted newest-first. - copilot_candidates_for_vscode() { - local vscode="$1" - printf '%s' "$MARKETPLACE_JSON" | jq -r \ - --arg vscode "$vscode" ' - def vparts: split(".") | map(tonumber? // 0); - [ - .results[0].extensions[0].versions[]? | - select(.version | test("^[0-9]+\\.[0-9]+\\.[0-9]{1,9}$")) | - (.properties // [] | - map(select(.key == "Microsoft.VisualStudio.Code.Engine")) | - .[0].value // "") as $engine | - select($engine != "") | - ($engine | gsub("^[^0-9]+"; "") | split("-")[0]) as $min_vscode | - select(($vscode | vparts) >= ($min_vscode | vparts)) | - .version - ] | sort_by(split(".") | map(tonumber? // 0)) | reverse | .[] - ' - } - # ── Step 3: Resolve code-server for the primary codename ────────────── CS_VERSION_PRIMARY=$(printf '%s' "$CODESERVER_RELEASES_JSON" | jq -r ' first( @@ -217,28 +188,18 @@ runs: } echo "Primary (${PRIMARY_CODENAME}) code-server: ${CS_VERSION_PRIMARY}" >&2 - # ── Step 4: Resolve Copilot Chat for the primary codename ───────────── - VSCODE_PRIMARY="$(printf '%s' "$CS_VERSION_PRIMARY" | sed 's/^[0-9][0-9]*\./1./')" - CC_VERSION_PRIMARY=$(copilot_candidates_for_vscode "$VSCODE_PRIMARY" | head -1) - [ -n "$CC_VERSION_PRIMARY" ] || { - echo "ERROR: failed to detect Copilot Chat version for ${PRIMARY_CODENAME}" >&2; exit 1 - } - echo "Primary (${PRIMARY_CODENAME}) Copilot Chat: ${CC_VERSION_PRIMARY}" >&2 - - # ── Step 5: Set tentative versions for all codenames ────────────────── + # ── Step 4: Set tentative versions for all codenames ────────────────── # Non-primary codenames are initially assigned the primary versions. # The actual compatibility testing (which requires downloading artifacts) # is deferred to the cache-miss download step so that cache hits skip # all artifact downloads. The cache is invalidated whenever the primary # version changes, which also triggers re-verification for all codenames. declare -A CS_VERSION_FOR # codename → resolved code-server version - declare -A CC_VERSION_FOR # codename → resolved Copilot Chat version for codename in "${!VERSIONS_FOR[@]}"; do CS_VERSION_FOR[$codename]="$CS_VERSION_PRIMARY" - CC_VERSION_FOR[$codename]="$CC_VERSION_PRIMARY" done - # ── Step 6: Write resolve.env and compute cache key ─────────────────── + # ── Step 5: Write resolve.env and compute cache key ─────────────────── # The env file is sourced by subsequent steps to access codename mappings # and resolved versions without re-running docker or API calls. # Variable names use the codename as a suffix (e.g. CS_VERSION_bookworm); @@ -257,7 +218,6 @@ runs: # Per-codename tentative resolved versions (may be updated by download step). for cn in "${!VERSIONS_FOR[@]}"; do printf 'CS_VERSION_%s=%s\n' "$cn" "${CS_VERSION_FOR[$cn]}" - printf 'CC_VERSION_%s=%s\n' "$cn" "${CC_VERSION_FOR[$cn]}" done # Per-codename representative image used by the download step for # compatibility testing of non-primary codenames. @@ -267,11 +227,11 @@ runs: } > "$RESOLVE_ENV" # Cache key: SHA256 of the primary-version-based tuples for all codenames. - # Invalidated whenever the primary code-server or Copilot Chat version - # changes, triggering a fresh download and compat re-test for all codenames. + # Invalidated whenever the primary code-server version changes, triggering + # a fresh download and compat re-test for all codenames. CACHE_KEY=$( for cn in $(echo "${!VERSIONS_FOR[@]}" | tr ' ' '\n' | sort); do - printf '%s=%s,%s;' "$cn" "${CS_VERSION_FOR[$cn]}" "${CC_VERSION_FOR[$cn]}" + printf '%s=%s;' "$cn" "${CS_VERSION_FOR[$cn]}" done | sha256sum | awk '{print $1}' ) @@ -280,7 +240,7 @@ runs: echo "Tentative versions per Debian codename (verified on cache miss):" for cn in $(echo "${!VERSIONS_FOR[@]}" | tr ' ' '\n' | sort); do - echo " ${cn}: code-server=${CS_VERSION_FOR[$cn]} copilot-chat=${CC_VERSION_FOR[$cn]}" + echo " ${cn}: code-server=${CS_VERSION_FOR[$cn]}" done # ── Save codenames cache ─────────────────────────────────────────────────── @@ -310,16 +270,16 @@ runs: # ── Cache step ──────────────────────────────────────────────────────────── # A single cache entry covers all detected Debian tiers. The key is a - # hash of every (codename, cs_version, cc_version) tuple, so the cache is - # invalidated when any resolved version changes for any tier. The key is - # stable across runs as long as the content hash does not change; there is - # at most one cache entry per hash. + # hash of every (codename, cs_version) tuple, so the cache is invalidated + # when any resolved version changes for any tier. The key is stable across + # runs as long as the content hash does not change; there is at most one + # cache entry per hash. - name: Restore preseed artifacts cache id: cache uses: actions/cache/restore@v5 with: path: ${{ runner.temp }}/preseed-artifacts - key: preseed-${{ steps.detect-and-resolve.outputs.cache_key }} + key: preseed-codeserver-${{ steps.detect-and-resolve.outputs.cache_key }} # ── Download step ───────────────────────────────────────────────────────── # Runs on cache miss (cache-matched-key is empty), or unconditionally when @@ -386,28 +346,6 @@ runs: return $result } - # Returns 0 if all .node files in the VSIX resolve all shared libs on - # the target image, 1 if extraction fails or any lib is missing. - check_vsix_on_image() { - local vsix="$1" image="$2" - local tmp_dir result=0 - tmp_dir=$(mktemp -d) - if ! unzip -q "$vsix" -d "$tmp_dir" 2>/dev/null; then - echo " WARNING: failed to extract VSIX; treating as incompatible" >&2 - rm -rf "$tmp_dir" - return 1 - fi - while IFS= read -r node_file; do - docker run --rm \ - -v "${node_file}:/tmp/check.node:ro" \ - --entrypoint sh \ - "$image" \ - -c 'missing=$(ldd /tmp/check.node 2>&1 | grep "not found"); [ -z "$missing" ] || { echo "$missing" >&2; exit 1; }' \ - || { result=1; break; } - done < <(find "$tmp_dir" -name "*.node" -type f 2>/dev/null) - rm -rf "$tmp_dir" - return $result - } # ────────────────────────────────────────────────────────────────────── # ── Load upstream release data saved by detect-and-resolve ──────────── @@ -416,36 +354,11 @@ runs: [ -f "$RUNNER_TEMP/cs-releases.json" ] || { echo "ERROR: cs-releases.json not found; detect-and-resolve may have failed" >&2; exit 1 } - [ -f "$RUNNER_TEMP/marketplace.json" ] || { - echo "ERROR: marketplace.json not found; detect-and-resolve may have failed" >&2; exit 1 - } CODESERVER_RELEASES_JSON=$(cat "$RUNNER_TEMP/cs-releases.json") - MARKETPLACE_JSON=$(cat "$RUNNER_TEMP/marketplace.json") - - copilot_candidates_for_vscode() { - local vscode="$1" - printf '%s' "$MARKETPLACE_JSON" | jq -r \ - --arg vscode "$vscode" ' - def vparts: split(".") | map(tonumber? // 0); - [ - .results[0].extensions[0].versions[]? | - select(.version | test("^[0-9]+\\.[0-9]+\\.[0-9]{1,9}$")) | - (.properties // [] | - map(select(.key == "Microsoft.VisualStudio.Code.Engine")) | - .[0].value // "") as $engine | - select($engine != "") | - ($engine | gsub("^[^0-9]+"; "") | split("-")[0]) as $min_vscode | - select(($vscode | vparts) >= ($min_vscode | vparts)) | - .version - ] | sort_by(split(".") | map(tonumber? // 0)) | reverse | .[] - ' - } # ── Determine primary versions from resolve.env ──────────────────────── CS_PRIMARY_VAR="CS_VERSION_${PRIMARY_CODENAME}" CS_VERSION_PRIMARY="${!CS_PRIMARY_VAR}" - CC_PRIMARY_VAR="CC_VERSION_${PRIMARY_CODENAME}" - CC_VERSION_PRIMARY="${!CC_PRIMARY_VAR}" # ── Download primary artifacts for compatibility testing ──────────────── echo "Downloading primary (${PRIMARY_CODENAME}) code-server ${CS_VERSION_PRIMARY} amd64 .deb..." >&2 @@ -456,19 +369,9 @@ runs: echo "ERROR: code-server_${CS_VERSION_PRIMARY}_amd64.deb failed format validation" >&2; exit 1 } - echo "Downloading primary (${PRIMARY_CODENAME}) Copilot Chat ${CC_VERSION_PRIMARY} VSIX..." >&2 - curl -fsSL --compressed --retry 3 --retry-all-errors --connect-timeout 30 \ - "https://marketplace.visualstudio.com/_apis/public/gallery/publishers/GitHub/vsextensions/copilot-chat/${CC_VERSION_PRIMARY}/vspackage" \ - -o "$RUNNER_TEMP/cc-${PRIMARY_CODENAME}.vsix" - od -An -N4 -tx1 "$RUNNER_TEMP/cc-${PRIMARY_CODENAME}.vsix" | tr -d ' \n' | grep -q '^504b0304' || { - echo "ERROR: copilot-chat-${CC_VERSION_PRIMARY}.vsix failed format validation" >&2; exit 1 - } - # ── Resolve actual versions for non-primary codenames ────────────────── declare -A CS_VERSION_FOR - declare -A CC_VERSION_FOR CS_VERSION_FOR[$PRIMARY_CODENAME]="$CS_VERSION_PRIMARY" - CC_VERSION_FOR[$PRIMARY_CODENAME]="$CC_VERSION_PRIMARY" for codename in $UNIQUE_CODENAMES; do [ "$codename" = "$PRIMARY_CODENAME" ] && continue @@ -511,43 +414,6 @@ runs: echo "ERROR: no compatible code-server version found for ${test_image}" >&2; exit 1 } fi - - # ── Copilot Chat ───────────────────────────────────────────────────── - if [ "${CS_VERSION_FOR[$codename]}" = "$CS_VERSION_PRIMARY" ] \ - && check_vsix_on_image "$RUNNER_TEMP/cc-${PRIMARY_CODENAME}.vsix" "$test_image"; then - CC_VERSION_FOR[$codename]="$CC_VERSION_PRIMARY" - cp "$RUNNER_TEMP/cc-${PRIMARY_CODENAME}.vsix" \ - "$RUNNER_TEMP/cc-${codename}.vsix" - echo " ${codename} Copilot Chat = ${CC_VERSION_PRIMARY} (primary VSIX works)" >&2 - else - VSCODE_THIS="$(printf '%s' "${CS_VERSION_FOR[$codename]}" | sed 's/^[0-9][0-9]*\./1./')" - CC_VERSION_FOR[$codename]="" - while IFS= read -r candidate; do - echo " Checking Copilot Chat ${candidate} on ${test_image}..." >&2 - TMP_VSIX=$(mktemp --suffix=.vsix) - if [ "$candidate" = "$CC_VERSION_PRIMARY" ] \ - && [ -f "$RUNNER_TEMP/cc-${PRIMARY_CODENAME}.vsix" ]; then - cp "$RUNNER_TEMP/cc-${PRIMARY_CODENAME}.vsix" "$TMP_VSIX" - else - curl -fsSL --compressed --retry 3 --retry-all-errors --connect-timeout 30 \ - "https://marketplace.visualstudio.com/_apis/public/gallery/publishers/GitHub/vsextensions/copilot-chat/${candidate}/vspackage" \ - -o "$TMP_VSIX" 2>/dev/null || { rm -f "$TMP_VSIX"; continue; } - od -An -N4 -tx1 "$TMP_VSIX" | tr -d ' \n' | grep -q '^504b0304' \ - || { rm -f "$TMP_VSIX"; continue; } - fi - if check_vsix_on_image "$TMP_VSIX" "$test_image"; then - CC_VERSION_FOR[$codename]="$candidate" - mv "$TMP_VSIX" "$RUNNER_TEMP/cc-${codename}.vsix" - echo " Selected ${codename} Copilot Chat: ${candidate}" >&2 - break - else - rm -f "$TMP_VSIX" - fi - done < <(copilot_candidates_for_vscode "$VSCODE_THIS") - [ -n "${CC_VERSION_FOR[$codename]}" ] || { - echo "ERROR: no compatible Copilot Chat version found for ${test_image}" >&2; exit 1 - } - fi done # ── Write final-versions.env into the preseed-artifacts cache dir ────── @@ -559,19 +425,17 @@ runs: { for cn in $(echo "${!CS_VERSION_FOR[@]}" | tr ' ' '\n' | sort); do printf 'CS_VERSION_%s=%s\n' "$cn" "${CS_VERSION_FOR[$cn]}" - printf 'CC_VERSION_%s=%s\n' "$cn" "${CC_VERSION_FOR[$cn]}" done } > "$FINAL_VERSIONS_ENV" echo "Final resolved versions per Debian codename:" for cn in $(echo "${!CS_VERSION_FOR[@]}" | tr ' ' '\n' | sort); do - echo " ${cn}: code-server=${CS_VERSION_FOR[$cn]} copilot-chat=${CC_VERSION_FOR[$cn]}" + echo " ${cn}: code-server=${CS_VERSION_FOR[$cn]}" done # ── Download all artifacts ───────────────────────────────────────────── for cn in $UNIQUE_CODENAMES; do cs_ver="${CS_VERSION_FOR[$cn]}" - cc_ver="${CC_VERSION_FOR[$cn]}" ARTIFACT_DIR="${{ runner.temp }}/preseed-artifacts/${cn}" mkdir -p "$ARTIFACT_DIR" @@ -597,26 +461,6 @@ runs: } fi done - - # ── Copilot Chat VSIX ───────────────────────────────────────────────── - VSIX="copilot-chat-${cc_ver}.vsix" - TARGET="${ARTIFACT_DIR}/${VSIX}" - COMPAT_VSIX="$RUNNER_TEMP/cc-${cn}.vsix" - if [ -f "$COMPAT_VSIX" ]; then - echo "Reusing ${VSIX} from compatibility testing phase (${cn})" - [ "$COMPAT_VSIX" != "$TARGET" ] && cp "$COMPAT_VSIX" "$TARGET" - od -An -N4 -tx1 "$TARGET" | tr -d ' \n' | grep -q '^504b0304' || { - echo "ERROR: ${VSIX} failed format validation" >&2; exit 1 - } - else - echo "Downloading ${VSIX} (${cn})" - curl -fsSL --compressed --retry 5 --retry-all-errors --connect-timeout 30 \ - "https://marketplace.visualstudio.com/_apis/public/gallery/publishers/GitHub/vsextensions/copilot-chat/${cc_ver}/vspackage" \ - -o "$TARGET" - od -An -N4 -tx1 "$TARGET" | tr -d ' \n' | grep -q '^504b0304' || { - echo "ERROR: ${VSIX} failed format validation" >&2; exit 1 - } - fi done # ── Save preseed artifacts cache ────────────────────────────────────────── @@ -625,11 +469,11 @@ runs: # hash; when the restore already matched our key, the content is unchanged # and no new entry is needed. - name: Save preseed artifacts cache - if: steps.cache.outputs.cache-matched-key != format('preseed-{0}', steps.detect-and-resolve.outputs.cache_key) + if: steps.cache.outputs.cache-matched-key != format('preseed-codeserver-{0}', steps.detect-and-resolve.outputs.cache_key) uses: actions/cache/save@v5 with: path: ${{ runner.temp }}/preseed-artifacts - key: preseed-${{ steps.detect-and-resolve.outputs.cache_key }} + key: preseed-codeserver-${{ steps.detect-and-resolve.outputs.cache_key }} # ── Assemble step ───────────────────────────────────────────────────────── # Copies unique artifacts to pre-downloaded/, computes SHA256 digests, and @@ -657,13 +501,11 @@ runs: # ── Copy unique artifacts to pre-downloaded/ ─────────────────────────── # Track already-copied filenames to avoid duplicating files when two - # codenames resolve to the same code-server or Copilot Chat version. + # codenames resolve to the same code-server version. declare -A COPIED for cn in $UNIQUE_CODENAMES; do cs_ver_var="CS_VERSION_${cn}" - cc_ver_var="CC_VERSION_${cn}" cs_ver="${!cs_ver_var}" - cc_ver="${!cc_ver_var}" ARTIFACT_DIR="${{ runner.temp }}/preseed-artifacts/${cn}" for ARCH in amd64 arm64; do @@ -673,30 +515,20 @@ runs: COPIED[$DEB]=1 fi done - - VSIX="copilot-chat-${cc_ver}.vsix" - if [ -z "${COPIED[$VSIX]+x}" ]; then - cp "${ARTIFACT_DIR}/${VSIX}" "${PRE_DOWNLOADED_DIR}/" - COPIED[$VSIX]=1 - fi done # ── Compute SHA256 digests ───────────────────────────────────────────── declare -A CS_SHA256_AMD64_FOR declare -A CS_SHA256_ARM64_FOR - declare -A CC_SHA256_FOR for cn in $UNIQUE_CODENAMES; do cs_ver_var="CS_VERSION_${cn}" - cc_ver_var="CC_VERSION_${cn}" cs_ver="${!cs_ver_var}" - cc_ver="${!cc_ver_var}" CS_SHA256_AMD64_FOR[$cn]=$(sha256sum "${PRE_DOWNLOADED_DIR}/code-server_${cs_ver}_amd64.deb" | awk '{print $1}') CS_SHA256_ARM64_FOR[$cn]=$(sha256sum "${PRE_DOWNLOADED_DIR}/code-server_${cs_ver}_arm64.deb" | awk '{print $1}') - CC_SHA256_FOR[$cn]=$(sha256sum "${PRE_DOWNLOADED_DIR}/copilot-chat-${cc_ver}.vsix" | awk '{print $1}') - echo "${cn}: code-server-amd64=${CS_SHA256_AMD64_FOR[$cn]} arm64=${CS_SHA256_ARM64_FOR[$cn]} copilot-chat=${CC_SHA256_FOR[$cn]}" + echo "${cn}: code-server-amd64=${CS_SHA256_AMD64_FOR[$cn]} arm64=${CS_SHA256_ARM64_FOR[$cn]}" done # ── Generate JSON bake override file ─────────────────────────────────── @@ -710,20 +542,15 @@ runs: codename_var="CODENAME_${key}" cn="${!codename_var}" cs_ver_var="CS_VERSION_${cn}" - cc_ver_var="CC_VERSION_${cn}" cs_ver="${!cs_ver_var}" - cc_ver="${!cc_ver_var}" amd64_sha="${CS_SHA256_AMD64_FOR[$cn]}" arm64_sha="${CS_SHA256_ARM64_FOR[$cn]}" - vsix_sha="${CC_SHA256_FOR[$cn]}" $first || printf ',' first=false printf '"%s":{"args":{' "php${key}-downloader" printf '"CODESERVER_VERSION":"%s",' "$cs_ver" printf '"CODESERVER_DEB_SHA256_AMD64":"%s",' "$amd64_sha" - printf '"CODESERVER_DEB_SHA256_ARM64":"%s",' "$arm64_sha" - printf '"COPILOT_CHAT_VERSION":"%s",' "$cc_ver" - printf '"COPILOT_CHAT_VSIX_SHA256":"%s"' "$vsix_sha" + printf '"CODESERVER_DEB_SHA256_ARM64":"%s"' "$arm64_sha" printf '}}' done printf '}}' diff --git a/.github/workflows/docker-build.yml b/.github/workflows/docker-build.yml index bc09d4da..b2228dbc 100644 --- a/.github/workflows/docker-build.yml +++ b/.github/workflows/docker-build.yml @@ -24,8 +24,7 @@ on: no_cache: description: > Disable Docker layer cache and force a fresh download of all build - artefacts (code-server, Copilot Chat). - When false, the cache is used as normal. + artefacts (code-server). When false, the cache is used as normal. type: boolean default: true @@ -188,12 +187,12 @@ jobs: path: ${{ steps.preseed.outputs.bake_overrides_file }} retention-days: 1 - # Upload the flat pre-downloaded/ directory (code-server .deb files and - # Copilot Chat VSIX) as a workflow artifact so build jobs can download it - # directly. Using an artifact (rather than the GHA preseed cache) is - # more reliable because artifacts are guaranteed to persist for the - # lifetime of the workflow run; the GHA cache can be evicted mid-run - # and a cache miss would silently produce an empty pre-downloaded/ dir. + # Upload the flat pre-downloaded/ directory (code-server .deb files) + # as a workflow artifact so build jobs can download it directly. Using + # an artifact (rather than the GHA preseed cache) is more reliable because + # artifacts are guaranteed to persist for the lifetime of the workflow + # run; the GHA cache can be evicted mid-run and a cache miss would silently + # produce an empty pre-downloaded/ dir. - name: Upload preseed pre-downloaded artifacts if: steps.detect.outputs.versions != '' uses: actions/upload-artifact@v7 @@ -222,7 +221,7 @@ jobs: fail-fast: false matrix: include: ${{ fromJSON(needs.detect.outputs.build_matrix) }} - runs-on: ubuntu-latest + runs-on: ${{ matrix.platform == 'linux/arm64' && 'ubuntu-26.04-arm' || 'ubuntu-26.04' }} permissions: contents: read packages: write @@ -231,11 +230,11 @@ jobs: - name: Checkout repository uses: actions/checkout@v6 - # Download the pre-seeded code-server .deb and Copilot Chat VSIX files - # that the detect job assembled and uploaded. Using a workflow artifact - # (not the GHA preseed cache) guarantees these files are present for the - # lifetime of the run; a cache miss would silently produce an empty - # pre-downloaded/ directory and fail the Docker build. + # Download the pre-seeded code-server .deb files that the detect job + # assembled and uploaded. Using a workflow artifact (not the GHA preseed + # cache) guarantees these files are present for the lifetime of the run; + # a cache miss would silently produce an empty pre-downloaded/ directory + # and fail the Docker build. - name: Download preseed pre-downloaded artifacts uses: actions/download-artifact@v8 with: diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index a9c072be..e6e6e076 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -179,12 +179,12 @@ jobs: path: ${{ steps.preseed.outputs.bake_overrides_file }} retention-days: 1 - # Upload the flat pre-downloaded/ directory (code-server .deb files and - # Copilot Chat VSIX) as a workflow artifact so build jobs can download it - # directly. Using an artifact (rather than the GHA preseed cache) is - # more reliable because artifacts are guaranteed to persist for the - # lifetime of the workflow run; the GHA cache can be evicted mid-run - # and a cache miss would silently produce an empty pre-downloaded/ dir. + # Upload the flat pre-downloaded/ directory (code-server .deb files) + # as a workflow artifact so build jobs can download it directly. Using + # an artifact (rather than the GHA preseed cache) is more reliable because + # artifacts are guaranteed to persist for the lifetime of the workflow + # run; the GHA cache can be evicted mid-run and a cache miss would silently + # produce an empty pre-downloaded/ dir. - name: Upload preseed pre-downloaded artifacts if: steps.versions.outputs.has_versions == 'true' uses: actions/upload-artifact@v7 @@ -198,7 +198,7 @@ jobs: build: name: Build and test PHP ${{ matrix.version }} runs-on: ubuntu-latest - needs: [lint-yaml, lint-shell, lint-dockerfile, verify-extension, verify-code-server, detect] + needs: [lint-yaml, lint-shell, lint-dockerfile, verify-code-server, detect] if: needs.detect.outputs.has_versions == 'true' strategy: fail-fast: false @@ -221,11 +221,11 @@ jobs: username: ${{ github.actor }} password: ${{ secrets.GITHUB_TOKEN }} - # Download the pre-seeded code-server .deb and Copilot Chat VSIX files - # that the detect job assembled and uploaded. Using a workflow artifact - # (not the GHA preseed cache) guarantees these files are present for the - # lifetime of the run; a cache miss would silently produce an empty - # pre-downloaded/ directory and fail the Docker build. + # Download the pre-seeded code-server .deb files that the detect job + # assembled and uploaded. Using a workflow artifact (not the GHA preseed + # cache) guarantees these files are present for the lifetime of the run; + # a cache miss would silently produce an empty pre-downloaded/ directory + # and fail the Docker build. - name: Download preseed pre-downloaded artifacts uses: actions/download-artifact@v8 with: @@ -233,9 +233,9 @@ jobs: path: ${{ runner.temp }}/build-downloads/pre-downloaded # Download the bake-overrides JSON generated by the detect job's preseed - # run. It contains the correct code-server / Copilot Chat versions and - # SHA256 digests for every detected Debian codename, including the - # downgraded versions needed for older codenames (Buster / Bullseye). + # run. It contains the correct code-server versions and SHA256 digests + # for every detected Debian codename, including the downgraded versions + # needed for older codenames (Buster / Bullseye). - name: Download preseed bake overrides uses: actions/download-artifact@v8 with: @@ -254,54 +254,6 @@ jobs: - name: Run PHP functional tests run: bash tests/run-dockerfile.sh --version ${{ matrix.version }} - verify-extension: - name: Verify Copilot Chat VSIX - runs-on: ubuntu-latest - permissions: - contents: read - steps: - - uses: actions/checkout@v6 - - - name: Get pinned Copilot Chat version - id: copilot-version - run: | - echo "version=$(grep -m1 '^ARG COPILOT_CHAT_VERSION=' base/Dockerfile | cut -d= -f2)" >> "$GITHUB_OUTPUT" - - - name: Cache Copilot Chat VSIX - id: cache-vsix - uses: actions/cache@v5 - with: - path: ${{ runner.temp }}/copilot-chat.vsix - key: copilot-chat-vsix-${{ steps.copilot-version.outputs.version }} - - - name: Download Copilot Chat VSIX (cache miss only) - if: steps.cache-vsix.outputs.cache-hit != 'true' - run: | - VERSION="${{ steps.copilot-version.outputs.version }}" - VSIX_URL="https://marketplace.visualstudio.com/_apis/public/gallery/publishers/GitHub/vsextensions/copilot-chat/${VERSION}/vspackage" - echo "Downloading GitHub.copilot-chat ${VERSION} from VS Marketplace" - curl -fsSL --compressed --retry 5 --retry-all-errors --connect-timeout 30 \ - "${VSIX_URL}" -o "$RUNNER_TEMP/copilot-chat.vsix" - - - name: Verify Copilot Chat VSIX - run: | - EXPECTED_SHA="$(grep -m1 '^ARG COPILOT_CHAT_VSIX_SHA256=' base/Dockerfile | cut -d= -f2)" - if ! od -An -tx1 -N4 "$RUNNER_TEMP/copilot-chat.vsix" | tr -d ' \n' | grep -qi '^504b0304'; then - ACTUAL_MAGIC="$(od -An -tx1 -N8 "$RUNNER_TEMP/copilot-chat.vsix" | tr -d ' \n')" - echo "Downloaded file is not a valid ZIP (expected magic 504b0304, got: ${ACTUAL_MAGIC})" - echo "The Marketplace may have returned a rate-limit/error page or gzip-encoded content." - exit 1 - fi - ACTUAL_SHA="$(sha256sum "$RUNNER_TEMP/copilot-chat.vsix" | cut -d' ' -f1)" - echo "Computed SHA256: ${ACTUAL_SHA}" - if [ -n "${EXPECTED_SHA}" ]; then - echo "${EXPECTED_SHA} $RUNNER_TEMP/copilot-chat.vsix" | sha256sum -c - - echo "SHA256 verified." - else - echo "::warning::COPILOT_CHAT_VSIX_SHA256 is not set in base/Dockerfile." - echo "::warning::Set ARG COPILOT_CHAT_VSIX_SHA256=${ACTUAL_SHA} in base/Dockerfile to enable integrity verification." - fi - verify-code-server: name: Verify code-server .deb hashes runs-on: ubuntu-latest @@ -371,7 +323,6 @@ jobs: - lint-yaml - lint-shell - lint-dockerfile - - verify-extension - verify-code-server - prepare - detect diff --git a/7.4/base/Dockerfile b/7.4/base/Dockerfile index 0b9c17d2..7bb075f8 100644 --- a/7.4/base/Dockerfile +++ b/7.4/base/Dockerfile @@ -43,8 +43,8 @@ RUN echo 'Dpkg::Use-Pty "0";' > /etc/apt/apt.conf.d/01dpkg-no-pty \ && docker-php-ext-configure gd --with-freetype --with-jpeg --with-webp \ && docker-php-ext-install -j$(nproc) gd pspell \ && pecl install -o -f \ - igbinary-3.2.12 \ - imagick-3.8.1 \ + igbinary \ + imagick \ oauth-2.0.7 \ && docker-php-ext-enable gd igbinary imagick oauth pspell \ && apt-mark auto '.*' > /dev/null \ @@ -57,7 +57,7 @@ RUN echo 'Dpkg::Use-Pty "0";' > /etc/apt/apt.conf.d/01dpkg-no-pty \ | while IFS= read -r lib; do dpkg-query --search "$(realpath "$lib" 2>/dev/null || echo "$lib")" 2>/dev/null; done \ | cut -d: -f1 \ | sort -u \ - | xargs -r apt-mark manual; } || true \ + | xargs -r apt-mark manual; } \ && apt-get purge -y --auto-remove \ -o APT::AutoRemove::RecommendsImportant=false \ && rm -rf /var/lib/apt/lists/* \ @@ -76,7 +76,7 @@ RUN echo 'Dpkg::Use-Pty "0";' > /etc/apt/apt.conf.d/01dpkg-no-pty \ done \ && wget --quiet -O - https://www.postgresql.org/media/keys/ACCC4CF8.asc \ | gpg --dearmor -o /usr/share/keyrings/postgresql.gpg \ - && echo "deb [signed-by=/usr/share/keyrings/postgresql.gpg] https://apt.postgresql.org/pub/repos/apt $(lsb_release -cs)-pgdg main" \ + && echo "deb [signed-by=/usr/share/keyrings/postgresql.gpg] https://apt-archive.postgresql.org/pub/repos/apt $(lsb_release -cs)-pgdg-archive main" \ > /etc/apt/sources.list.d/pgdg.list \ && apt-get update \ && apt-get install -y postgresql-client-18 \ diff --git a/8.0/base/Dockerfile b/8.0/base/Dockerfile index d9e9187c..f00cb201 100644 --- a/8.0/base/Dockerfile +++ b/8.0/base/Dockerfile @@ -38,8 +38,8 @@ RUN echo 'Dpkg::Use-Pty "0";' > /etc/apt/apt.conf.d/01dpkg-no-pty \ && docker-php-ext-configure gd --with-freetype --with-jpeg --with-webp \ && docker-php-ext-install -j$(nproc) gd pspell \ && pecl install -o -f \ - igbinary-3.2.12 \ - imagick-3.8.1 \ + igbinary \ + imagick \ && docker-php-ext-enable gd igbinary imagick pspell \ && apt-mark auto '.*' > /dev/null \ && echo "$savedAptMark" | xargs -r apt-mark manual \ @@ -51,7 +51,7 @@ RUN echo 'Dpkg::Use-Pty "0";' > /etc/apt/apt.conf.d/01dpkg-no-pty \ | while IFS= read -r lib; do dpkg-query --search "$(realpath "$lib" 2>/dev/null || echo "$lib")" 2>/dev/null; done \ | cut -d: -f1 \ | sort -u \ - | xargs -r apt-mark manual; } || true \ + | xargs -r apt-mark manual; } \ && apt-get purge -y --auto-remove \ -o APT::AutoRemove::RecommendsImportant=false \ && rm -rf /var/lib/apt/lists/* \ @@ -70,7 +70,7 @@ RUN echo 'Dpkg::Use-Pty "0";' > /etc/apt/apt.conf.d/01dpkg-no-pty \ done \ && wget --quiet -O - https://www.postgresql.org/media/keys/ACCC4CF8.asc \ | gpg --dearmor -o /usr/share/keyrings/postgresql.gpg \ - && echo "deb [signed-by=/usr/share/keyrings/postgresql.gpg] https://apt.postgresql.org/pub/repos/apt $(lsb_release -cs)-pgdg main" \ + && echo "deb [signed-by=/usr/share/keyrings/postgresql.gpg] https://apt-archive.postgresql.org/pub/repos/apt $(lsb_release -cs)-pgdg-archive main" \ > /etc/apt/sources.list.d/pgdg.list \ && apt-get update \ && apt-get install -y postgresql-client-18 \ diff --git a/8.1/base/Dockerfile b/8.1/base/Dockerfile index 726c841e..23559360 100644 --- a/8.1/base/Dockerfile +++ b/8.1/base/Dockerfile @@ -39,8 +39,8 @@ RUN echo 'Dpkg::Use-Pty "0";' > /etc/apt/apt.conf.d/01dpkg-no-pty \ && docker-php-ext-configure gd --with-avif --with-freetype --with-jpeg --with-webp \ && docker-php-ext-install -j$(nproc) gd pspell \ && pecl install -o -f \ - igbinary-3.2.12 \ - imagick-3.8.1 \ + igbinary \ + imagick \ && docker-php-ext-enable gd igbinary imagick pspell \ && apt-mark auto '.*' > /dev/null \ && echo "$savedAptMark" | xargs -r apt-mark manual \ @@ -52,7 +52,7 @@ RUN echo 'Dpkg::Use-Pty "0";' > /etc/apt/apt.conf.d/01dpkg-no-pty \ | while IFS= read -r lib; do dpkg-query --search "$(realpath "$lib" 2>/dev/null || echo "$lib")" 2>/dev/null; done \ | cut -d: -f1 \ | sort -u \ - | xargs -r apt-mark manual; } || true \ + | xargs -r apt-mark manual; } \ && apt-get purge -y --auto-remove \ -o APT::AutoRemove::RecommendsImportant=false \ && rm -rf /var/lib/apt/lists/* \ @@ -77,4 +77,3 @@ RUN echo 'Dpkg::Use-Pty "0";' > /etc/apt/apt.conf.d/01dpkg-no-pty \ && rm -f /etc/apt/apt.conf.d/01dpkg-no-pty USER ${USER} - diff --git a/8.2/base/Dockerfile b/8.2/base/Dockerfile index 1cf32a1e..d79b6dc4 100644 --- a/8.2/base/Dockerfile +++ b/8.2/base/Dockerfile @@ -39,8 +39,8 @@ RUN echo 'Dpkg::Use-Pty "0";' > /etc/apt/apt.conf.d/01dpkg-no-pty \ && docker-php-ext-configure gd --with-avif --with-freetype --with-jpeg --with-webp \ && docker-php-ext-install -j$(nproc) gd pspell \ && pecl install -o -f \ - igbinary-3.2.12 \ - imagick-3.8.1 \ + igbinary \ + imagick \ && docker-php-ext-enable gd igbinary imagick pspell \ && apt-mark auto '.*' > /dev/null \ && echo "$savedAptMark" | xargs -r apt-mark manual \ @@ -52,7 +52,7 @@ RUN echo 'Dpkg::Use-Pty "0";' > /etc/apt/apt.conf.d/01dpkg-no-pty \ | while IFS= read -r lib; do dpkg-query --search "$(realpath "$lib" 2>/dev/null || echo "$lib")" 2>/dev/null; done \ | cut -d: -f1 \ | sort -u \ - | xargs -r apt-mark manual; } || true \ + | xargs -r apt-mark manual; } \ && apt-get purge -y --auto-remove \ -o APT::AutoRemove::RecommendsImportant=false \ && rm -rf /var/lib/apt/lists/* \ @@ -77,4 +77,3 @@ RUN echo 'Dpkg::Use-Pty "0";' > /etc/apt/apt.conf.d/01dpkg-no-pty \ && rm -f /etc/apt/apt.conf.d/01dpkg-no-pty USER ${USER} - diff --git a/8.3/base/Dockerfile b/8.3/base/Dockerfile index 3dac92ac..bd53e537 100644 --- a/8.3/base/Dockerfile +++ b/8.3/base/Dockerfile @@ -54,7 +54,7 @@ RUN echo 'Dpkg::Use-Pty "0";' > /etc/apt/apt.conf.d/01dpkg-no-pty \ | while IFS= read -r lib; do dpkg-query --search "$(realpath "$lib" 2>/dev/null || echo "$lib")" 2>/dev/null; done \ | cut -d: -f1 \ | sort -u \ - | xargs -r apt-mark manual; } || true \ + | xargs -r apt-mark manual; } \ && apt-get purge -y --auto-remove \ -o APT::AutoRemove::RecommendsImportant=false \ && rm -rf /var/lib/apt/lists/* \ @@ -82,4 +82,3 @@ RUN echo 'Dpkg::Use-Pty "0";' > /etc/apt/apt.conf.d/01dpkg-no-pty \ RUN composer self-update 2.7.7 USER ${USER} - diff --git a/8.4/base/Dockerfile b/8.4/base/Dockerfile index 68160e1d..1511aa43 100644 --- a/8.4/base/Dockerfile +++ b/8.4/base/Dockerfile @@ -53,7 +53,7 @@ RUN echo 'Dpkg::Use-Pty "0";' > /etc/apt/apt.conf.d/01dpkg-no-pty \ | while IFS= read -r lib; do dpkg-query --search "$(realpath "$lib" 2>/dev/null || echo "$lib")" 2>/dev/null; done \ | cut -d: -f1 \ | sort -u \ - | xargs -r apt-mark manual; } || true \ + | xargs -r apt-mark manual; } \ && apt-get purge -y --auto-remove \ -o APT::AutoRemove::RecommendsImportant=false \ && rm -rf /var/lib/apt/lists/* \ @@ -81,4 +81,3 @@ RUN echo 'Dpkg::Use-Pty "0";' > /etc/apt/apt.conf.d/01dpkg-no-pty \ RUN composer self-update USER ${USER} - diff --git a/8.5/base/Dockerfile b/8.5/base/Dockerfile index 82a60100..0188b2c8 100644 --- a/8.5/base/Dockerfile +++ b/8.5/base/Dockerfile @@ -53,7 +53,7 @@ RUN echo 'Dpkg::Use-Pty "0";' > /etc/apt/apt.conf.d/01dpkg-no-pty \ | while IFS= read -r lib; do dpkg-query --search "$(realpath "$lib" 2>/dev/null || echo "$lib")" 2>/dev/null; done \ | cut -d: -f1 \ | sort -u \ - | xargs -r apt-mark manual; } || true \ + | xargs -r apt-mark manual; } \ && apt-get purge -y --auto-remove \ -o APT::AutoRemove::RecommendsImportant=false \ && rm -rf /var/lib/apt/lists/* \ @@ -81,4 +81,3 @@ RUN echo 'Dpkg::Use-Pty "0";' > /etc/apt/apt.conf.d/01dpkg-no-pty \ RUN composer self-update USER ${USER} - diff --git a/AGENTS.md b/AGENTS.md index 1855e570..abbe2022 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -125,7 +125,7 @@ Baseline JSON files in `tests/baselines/` track pre-existing lint violations per #### Composite Actions - **`.github/actions/build-php-images`** — detects changed PHP versions and runs `docker buildx bake` using `docker-bake.hcl`. Called directly by the trigger workflows below. -- **`.github/actions/preseed-downloads`** — resolves code-server and Copilot Chat versions, restores/downloads artifacts into `$RUNNER_TEMP/build-downloads/pre-downloaded/`, and exposes `DOWNLOADS_DIR` + SHA256 outputs for Docker builds. +- **`.github/actions/preseed-downloads`** — resolves code-server versions, restores/downloads artifacts into `$RUNNER_TEMP/build-downloads/pre-downloaded/`, and exposes `DOWNLOADS_DIR` + SHA256 outputs for Docker builds. #### 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. diff --git a/README.md b/README.md index 64df8625..6812e82c 100644 --- a/README.md +++ b/README.md @@ -279,8 +279,11 @@ for setup instructions. without pushing. Functional tests (`tests/run-dockerfile.sh`) then start each built image and verify PHP, Apache, Composer, and extensions work. Run `./test.sh build run --version ` to test a single PHP version quickly. -- The build workflow is `docker-build.yml`, which handles both push-triggered and manual dispatch - builds. It runs a detect → build (per-platform, parallel) → merge pipeline. +- The build workflow is `docker-build.yml`, which handles both push-triggered + and manual dispatch builds. It runs a detect → build (per-platform, + parallel) → merge pipeline. ARM64 builds use native `ubuntu-24.04-arm` + runners; AMD64 builds use `ubuntu-latest`. QEMU is only set up when the + requested platform does not match the runner architecture. - The `advance` variant depends on `secure`, which depends on `base`. The build chain is enforced via the bake target dependency graph. @@ -312,95 +315,3 @@ done ``` Then update `CODESERVER_VERSION`, `CODESERVER_DEB_SHA256_AMD64`, and `CODESERVER_DEB_SHA256_ARM64` in `base/Dockerfile`. - -## GitHub Copilot Chat extension pinning - -GitHub Copilot Chat is not available in the Open VSX marketplace used by -code-server, so `base/Dockerfile` downloads it directly from the Visual Studio -Marketplace at image-build time. Only **stable** releases are used. - -### How the pinned version is chosen - -The Copilot Chat extension publishes to two channels on the VS Marketplace: - -| Channel | Version format | Example | Where visible | -|---|---|---|---| -| **Stable** (release) | `MAJOR.MINOR.PATCH` | `0.26.7` | VS Code desktop "Version History" | -| **Pre-release** | `MAJOR.MINOR.YYYYMMDDNN` | `0.26.2025040204` | Marketplace API (`flags=17`) only | - -The "Version History" panel in the VS Code desktop app shows stable patch releases -(e.g. `0.26.0` through `0.26.7`). Pre-release builds use a date-stamped third -component (`YYYYMMDDNN` = year + month + day + sequence), so `0.26.2025040204` -is build #04 on 2025-04-02. Because this date-derived number is much larger -than the stable patch counter, and the Marketplace API returns all versions -newest-first by publish date (regardless of channel), pre-release builds appear -before their stable equivalents in the API response. - -Each release declares a minimum VS Code engine version in its -`Microsoft.VisualStudio.Code.Engine` property (e.g. `^1.99.0`). -code-server `4.X.Y` bundles VS Code `1.X.Y` (e.g. `4.99.4` → VS Code -`1.99.4`), so the correct pinned version is **the most recent stable Copilot -Chat release whose engine minimum is ≤ the VS Code version bundled in the -target code-server release**. - -The current pin `0.26.7` is the latest stable release compatible with -code-server `4.99.4` (VS Code `1.99.4`, engine requirement `^1.99.0`). -Versions in the `0.27.x` series and later require VS Code `^1.100.0` or higher. - -### Finding the right version for a new code-server release - -1. Determine the VS Code version bundled with the target code-server release - (e.g. code-server `4.99.4` → VS Code `1.99.4`). -2. Query the VS Marketplace extensionquery API for `GitHub.copilot-chat`, - filtering for stable versions only (patch component 1–9 digits, excluding 10-digit pre-release date-stamps): - ```bash - curl -fsSL -X POST \ - -H 'Content-Type: application/json' \ - -H 'Accept: application/json;api-version=3.0-preview.1' \ - -d '{"filters":[{"criteria":[{"filterType":7,"value":"GitHub.copilot-chat"}],"pageSize":100}],"flags":17}' \ - 'https://marketplace.visualstudio.com/_apis/public/gallery/extensionquery' \ - | python3 -c " - import json, sys, re - vscode = sys.argv[1] - data = json.load(sys.stdin) - for v in data['results'][0]['extensions'][0]['versions']: - ver = v.get('version','') - # Skip pre-release builds (10-digit date-stamped patch component). - if not re.match(r'^\d+\.\d+\.\d{1,9}$', ver): continue - engine = next((p['value'] for p in v.get('properties',[]) - if p['key']=='Microsoft.VisualStudio.Code.Engine'), '') - eng_min = engine.lstrip('^~>=').split('-')[0] - if eng_min and tuple(int(x) for x in vscode.split('.')) >= \ - tuple(int(x) for x in eng_min.split('.')): - print(ver); break - " 1.99.4 - ``` -3. For versions `v0.29.0` and later the engine requirement is also visible in - the extension's - [`package.json`](https://github.com/microsoft/vscode-copilot-chat) on GitHub. - For earlier versions (before the repository was open-sourced in June 2025) - use the [VsixHub version history](https://www.vsixhub.com/history/143611/). - -### Computing and updating the SHA256 - -Once you have the correct version string, download the VSIX with `--compressed` (the -Marketplace serves the VSIX with gzip transfer encoding) and compute its SHA256: - -```bash -VERSION=0.26.7 -curl -fsSL --compressed --retry 5 --retry-all-errors --connect-timeout 10 \ - "https://marketplace.visualstudio.com/_apis/public/gallery/publishers/GitHub/vsextensions/copilot-chat/${VERSION}/vspackage" \ - -o "/tmp/copilot-chat-${VERSION}.vsix" -# Linux: -sha256sum "/tmp/copilot-chat-${VERSION}.vsix" -# macOS: -shasum -a 256 "/tmp/copilot-chat-${VERSION}.vsix" -``` - -Then update `COPILOT_CHAT_VERSION` and `COPILOT_CHAT_VSIX_SHA256` in -`base/Dockerfile`. The CI "Verify Copilot Chat VSIX" job independently downloads -the pinned VSIX from the same URL, validates it is a valid ZIP, and verifies the -SHA256 on every CI run. - -The VSIX is architecture-independent, so only one hash value is needed (unlike -code-server which ships separate `amd64`/`arm64` packages). diff --git a/TODO.md b/TODO.md index 883ca4c0..1adc41a8 100644 --- a/TODO.md +++ b/TODO.md @@ -2,6 +2,19 @@ Use this file to track work that needs to be done. Lint scripts automatically append baseline-cleanup entries here when all violations for a linter reach zero. +# Task: Use native ARM runners for ARM64 builds + +## Steps +- [x] Inspect platform routing and composite build setup. +- [x] Route ARM64 builds to ARM runners and skip QEMU for native builds. +- [x] Document native runner selection and the cross-platform fallback. +- [x] Run focused workflow checks, secret scanning, and code/security review. + +## Definition of Done +- ARM64 build matrix jobs use `ubuntu-24.04-arm`; AMD64 jobs retain `ubuntu-latest`. +- Native ARM64 and AMD64 builds skip QEMU; cross-platform builds retain it. +- Focused validation passes and changed files contain no secrets. + # Task: Run builds after an all-versions dispatch ## Steps @@ -13,3 +26,65 @@ Use this file to track work that needs to be done. Lint scripts automatically ap - Existing validation passes and the changed files contain no secrets. - [ ] Remove shellcheck baseline comparison: delete tests/baselines/shellcheck-baseline.json and the baseline comparison logic from tests/lint-shell.sh, and update the script to fail when shellcheck reports any violations (fail when the generated JSON is non-empty). After that, any new shellcheck violation will be an immediate CI failure. + +# Task: Fix Bullseye security package source + +## Steps +- [x] Keep the Bullseye main repository on Debian's archive and accept expired metadata. +- [x] Pin the Bullseye security suite to the final available Debian security snapshot. +- [x] Update PHP 7.4 and 8.0 PostgreSQL sources to the PGDG archive. +- [x] Run focused Dockerfile lint/configuration validation and review the final diff. +- [x] Scan changed files for secrets. +- [x] Run parallel code validation. + +## Definition of Done +- Bullseye builds use the Debian archive and pinned security snapshot, and PHP 7.4/8.0 use the archived PGDG repository. +- Focused validation passes and changed files contain no secrets. + +# Task: Fix failing Test workflow + +## Steps +- [x] Identify matrix failures from GitHub Actions logs. +- [x] Use `--force` in the Copilot Chat functional test to handle a newer preinstalled extension. +- [x] Keep PHP 8.1 extension installation failures from being hidden by the optional dependency-marking fallback. +- [x] Point Bullseye PostgreSQL client installs at the PGDG archive. +- [x] Refresh PECL channel metadata before extension installs. +- [x] Scope the best-effort apt-mark fallback in all PHP version Dockerfiles. +- [x] Run Dockerfile/shell lint, shell syntax, Bake checks, and secret scanning; attempt a PHP 8.1 image build (sandbox DNS prevented GitHub download). +- [x] Run parallel code/security validation; no review findings, and CodeQL had no analyzable code. +- [x] Review Test run 39724432825; Redis 5.3.7 failed on PHP 8.4/8.5, and shared PECL channel refreshes returned malformed XML on multiple versions. +- [x] Upgrade the shared Redis extension to a PHP 8.4-compatible release, retry the shared PECL refresh, and remove redundant per-version refreshes. +- [ ] Validate the follow-up Test run and complete focused checks, secret scanning, and parallel validation. + +## Definition of Done +- The CI failures from Test run 39719313157 have targeted fixes. +- The affected image builds and functional tests pass in CI; modified files contain no secrets. + +# Task: Address build and Copilot Chat feedback + +## Steps +- [x] Inspect the latest CI failures and the ten PR commits. +- [x] Use HTTPS for PECL channel metadata and remove unnecessary extension pins. +- [x] Remove separate Copilot Chat downloads, installation, and verification. +- [x] Run focused lint/build checks and refresh affected baselines; PHP 7.4 Bake build is blocked by sandbox DNS resolution of Debian repositories. +- [x] Scan changed files for secrets; none detected. +- [x] Complete code/security review: independent review found no significant issues and CodeQL reported zero alerts. Test run 39771022231 requires approval before end-to-end builds can be verified. + +## Definition of Done +- Builds no longer download or install a separate Copilot Chat extension. +- PECL uses HTTPS channel metadata and unpinned shared extensions. +- Focused checks pass; any unavailable build verification is explicitly reported. + +# Task: Fix remaining PEAR and OPcache build failures + +## Steps +- [x] Inspect Test run 39771238006 and the twelve PR commits. +- [x] Upgrade PEAR with curl-downloaded local metadata-independent installation. +- [x] Enable OPcache only when it is not already loaded, including built-in PHP 8.5. +- [x] Run Dockerfile/shell lint, shell syntax and Bake parsing; verify the OPcache guard on official PHP 7.4 and 8.5 images. Full PHP 7.4 Bake build is blocked by sandbox Debian DNS resolution. +- [x] Scan for secrets; no secrets detected. Independent review found no significant issues; automated review was unavailable and CodeQL had no analyzable code. Fresh PHP 7.4/8.5 cache initialization checks pass; CI requires approval. + +## Definition of Done +- 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. diff --git a/base/Dockerfile b/base/Dockerfile index 5f3d8bee..6c243dcb 100644 --- a/base/Dockerfile +++ b/base/Dockerfile @@ -21,63 +21,34 @@ ARG PHP_VERSION=8.3 FROM alpine:3 AS downloads RUN mkdir -p /pre-downloaded -FROM php:${PHP_VERSION}-apache AS downloader +# Debian Bullseye reached end of LTS in August 2026. Its main repository is +# archived, while the security suite is pinned to its final available snapshot. +FROM php:${PHP_VERSION}-apache AS php-base +RUN if grep -q '^VERSION_CODENAME=bullseye$' /etc/os-release; then \ + sed -i \ + -e 's|deb.debian.org/debian-security|snapshot.debian.org/archive/debian-security/20260903T220410Z|g' \ + -e 's|deb.debian.org|archive.debian.org|g' \ + /etc/apt/sources.list \ + && echo 'Acquire::Check-Valid-Until "false";' > /etc/apt/apt.conf.d/99archive; \ + fi + +FROM php-base AS downloader ARG CODESERVER_VERSION=4.99.4 ARG CODESERVER_DEB_SHA256_AMD64=74c107b33643ed11223263b86431530fbc9c9f7c8202997579a6f0c41b4cb102 ARG CODESERVER_DEB_SHA256_ARM64=ae12d6300736dd742de3f5aef1b80c996c4b164ed385992e5de9d55f1e2077c3 -# GitHub Copilot Chat is not available in the Open VSX marketplace used by -# code-server, so we download it directly from the VS Marketplace. -# -# Version channels -# ────────────────────────────────── -# The Copilot Chat extension publishes two channels to the VS Marketplace: -# • Stable releases use simple MAJOR.MINOR.PATCH version numbers -# (e.g. 0.26.7, visible as "0.26.0 … 0.26.7" in the VS Code desktop UI). -# • Pre-release builds use a build-date-stamped format: MAJOR.MINOR.YYYYMMDDNN -# (e.g. 0.26.2025040204 = series 0.26, released 2025-04-02, build 04). -# Only stable releases are passed by CI (patch ≤ 9 digits excludes date-stamps). -# code-server 4.X.Y bundles VS Code 1.X.Y (e.g. 4.99.4 → VS Code 1.99.4). -# -# CI resolves the latest compatible stable versions before the build and passes -# them explicitly via CODESERVER_VERSION and COPILOT_CHAT_VERSION build args. -# The defaults below are used for standalone `docker build` invocations. -# -# To update the defaults or SHA256 values: -# 1. Find the latest code-server release at github.com/coder/code-server/releases -# 2. Download the VSIX with --compressed so curl decompresses transfer encoding: -# curl -fsSL --compressed \ -# "https://marketplace.visualstudio.com/_apis/public/gallery/publishers/GitHub/vsextensions/copilot-chat//vspackage" \ -# -o copilot-chat-.vsix -# 3. Run: sha256sum copilot-chat-.vsix (Linux) or -# shasum -a 256 copilot-chat-.vsix (macOS) -# 4. Update COPILOT_CHAT_VERSION and COPILOT_CHAT_VSIX_SHA256 below. -ARG COPILOT_CHAT_VERSION=0.26.7 -# SHA256 of the VSIX downloaded from the VS Marketplace vspackage endpoint -# (architecture-independent). CI passes the value computed at download time; -# standalone builds verify against this pinned value. -ARG COPILOT_CHAT_VSIX_SHA256=691e80194fdba0399879fd065a76b9b14b32bfd29818292e816a4522eb0330d1 - -# Install unzip and zip early so they are available for VSIX validation and -# repacking during the download step. -RUN apt-get update \ - && apt-get install -y --no-install-recommends unzip zip \ - && rm -rf /var/lib/apt/lists/* - # Download libsodium source (pinned release, identical for every PHP version) RUN mkdir -p /tmp/libsodium \ && curl -sL https://github.com/jedisct1/libsodium/archive/1.0.18-RELEASE.tar.gz \ | tar xzf - -C /tmp/libsodium -# Download code-server deb and GitHub Copilot Chat VSIX. -# Versions are resolved by CI and passed via CODESERVER_VERSION and -# COPILOT_CHAT_VERSION build args; standalone builds use the Dockerfile defaults. +# Download code-server deb. The version is resolved by CI and passed via +# CODESERVER_VERSION; standalone builds use the Dockerfile default. # /pre-downloaded (bind-mounted from the 'downloads' named context) holds files # pre-seeded by CI via actions/cache@v5 so they are never re-downloaded for the # same version even when Docker layers change. RUN --mount=type=cache,id=code-server-debs,target=/var/cache/code-server-debs,sharing=locked \ - --mount=type=cache,id=copilot-chat-vsix,target=/var/cache/copilot-chat-vsix,sharing=locked \ --mount=type=bind,from=downloads,source=/pre-downloaded,target=/pre-downloaded,readonly \ set -e; \ # code-server is required: fail build if version is empty @@ -115,94 +86,17 @@ RUN --mount=type=cache,id=code-server-debs,target=/var/cache/code-server-debs,sh echo " actual: ${ACTUAL_SHA}" >&2; \ echo "If you changed CODESERVER_VERSION, also update ${SHA_ARG} to match." >&2; \ exit 1; \ - fi; \ - \ - # Download GitHub Copilot Chat VSIX (not available in Open VSX marketplace). \ - # COPILOT_CHAT_VERSION is always provided — CI resolves it before the build and \ - # standalone builds fall back to the Dockerfile default. \ - [ -n "$COPILOT_CHAT_VERSION" ] || { echo "COPILOT_CHAT_VERSION is empty" >&2; exit 1; }; \ - # Validate the version matches semver major.minor.patch with optional pre-release/ - # build metadata using only URL-safe characters. This prevents a malformed URL - # from being constructed: an empty string, path separator, or other unexpected - # value is caught here with a clear message rather than silently producing - # .../copilot-chat//vspackage or similar. - printf '%s\n' "$COPILOT_CHAT_VERSION" | grep -qE '^[0-9]+\.[0-9]+\.[0-9]+([-+][0-9A-Za-z._-]*)?$' \ - || { echo "COPILOT_CHAT_VERSION has unexpected format: '${COPILOT_CHAT_VERSION}' (expected semver e.g. 1.2.3)" >&2; exit 1; }; \ - echo "Fetching Copilot Chat VSIX version: $COPILOT_CHAT_VERSION" >&2; \ - VSIX_URL="https://marketplace.visualstudio.com/_apis/public/gallery/publishers/GitHub/vsextensions/copilot-chat/${COPILOT_CHAT_VERSION}/vspackage"; \ - VSIX_CACHE="/var/cache/copilot-chat-vsix/copilot-chat-${COPILOT_CHAT_VERSION}.vsix"; \ - VSIX_CACHE_TMP="${VSIX_CACHE}.tmp"; \ - if [ ! -f "$VSIX_CACHE" ]; then \ - VSIX_PRELOADED="/pre-downloaded/copilot-chat-${COPILOT_CHAT_VERSION}.vsix"; \ - if [ -f "$VSIX_PRELOADED" ]; then \ - echo "Using pre-seeded Copilot Chat VSIX ${COPILOT_CHAT_VERSION} from downloads context" >&2; \ - cp "$VSIX_PRELOADED" "$VSIX_CACHE"; \ - else \ - echo "Downloading Copilot Chat VSIX ${COPILOT_CHAT_VERSION}" >&2; \ - curl -fsSL --compressed --retry 5 --retry-all-errors --connect-timeout 10 "$VSIX_URL" -o "$VSIX_CACHE_TMP" \ - && mv "$VSIX_CACHE_TMP" "$VSIX_CACHE" \ - || { rm -f "$VSIX_CACHE_TMP"; echo "Failed to download Copilot Chat VSIX ${COPILOT_CHAT_VERSION}" >&2; exit 1; }; \ - fi; \ - else \ - echo "Using cached Copilot Chat VSIX ${COPILOT_CHAT_VERSION}" >&2; \ - fi; \ - cp "$VSIX_CACHE" /tmp/copilot-chat.vsix; \ - [ -n "$COPILOT_CHAT_VSIX_SHA256" ] || { echo "COPILOT_CHAT_VSIX_SHA256 is empty — pass a matching hash when overriding COPILOT_CHAT_VERSION" >&2; exit 1; }; \ - if ! echo "$COPILOT_CHAT_VSIX_SHA256 /tmp/copilot-chat.vsix" | sha256sum -c -; then \ - ACTUAL_SHA="$(sha256sum /tmp/copilot-chat.vsix | awk '{print $1}')"; \ - rm -f "$VSIX_CACHE"; \ - echo "SHA256 mismatch for Copilot Chat VSIX ${COPILOT_CHAT_VERSION}:" >&2; \ - echo " expected: ${COPILOT_CHAT_VSIX_SHA256}" >&2; \ - echo " actual: ${ACTUAL_SHA}" >&2; \ - echo "If you changed COPILOT_CHAT_VERSION, also update COPILOT_CHAT_VSIX_SHA256 to match." >&2; \ - exit 1; \ - fi; \ - # Verify the download is a valid ZIP (VSIX is a ZIP archive). - # A ZIP file starts with the PK magic bytes: 50 4b 03 04 (hex). - # If the Marketplace returned an error page or rate-limit HTML instead of - # the binary, this guard gives a clear diagnostic rather than a cryptic - # "cannot find zipfile directory" message from unzip. - if ! od -An -tx1 -N4 /tmp/copilot-chat.vsix | tr -d ' \n' | grep -qi '^504b0304'; then \ - ACTUAL_MAGIC="$(od -An -tx1 -N8 /tmp/copilot-chat.vsix | tr -d ' \n')"; \ - echo "Copilot Chat VSIX is not a valid ZIP (expected magic 504b0304, got: ${ACTUAL_MAGIC})." >&2; \ - echo "The Marketplace may have returned a rate-limit/error page instead of the binary." >&2; \ - echo "If the first bytes are 1f8b (gzip), re-run with --compressed on the curl command." >&2; \ - rm -f "$VSIX_CACHE"; \ - echo "Cache cleared, please retry" >&2; \ - exit 1; \ - fi; \ - unzip -q /tmp/copilot-chat.vsix -d /tmp/_vsix_repack \ - || { echo "Failed to extract copilot-chat.vsix — file may be invalid or not a valid ZIP/VSIX (check marketplace response)" >&2; \ - exit 1; } - -# Repack the Copilot Chat VSIX to use only standard deflate (method 8) or stored -# (method 0) compression. Newer VS Marketplace packages may use Deflate64 -# (method 9) or other methods unsupported by the yauzl library bundled with -# code-server, causing a fatal "Extract" error at install time. Repacking with -# the system zip utility (which only emits methods 0 and 8) ensures the VSIX can -# be installed by any code-server version. -# TODO: Remove this repack step once code-server upgrades yauzl (or its bundled -# VS Code) to a version that supports Deflate64 (method 9) natively. -# The files were already unpacked into /tmp/_vsix_repack by the download step -# above. The repack is skipped only if that directory does not exist (e.g. a -# cache hit on a layer predating this code). The original file is kept until the -# repacked archive is fully written to avoid silently losing the VSIX on failure. -RUN if [ -d /tmp/_vsix_repack ]; then \ - ( cd /tmp/_vsix_repack && zip -r -9 /tmp/copilot-chat-repacked.vsix . ) \ - && mv /tmp/copilot-chat-repacked.vsix /tmp/copilot-chat.vsix \ - && rm -rf /tmp/_vsix_repack; \ - fi + fi # ─── Common PHP extensions stage ────────────────────────────────────────────── # Builds the shared intermediate that all per-version Dockerfiles extend. # Only packages and extensions that are IDENTICAL across all PHP versions are -# here. Version-specific items (avif/pcre packages, gd configure flags, -# igbinary version pin, imagick build method, oauth, xdebug) live in each -# {version}/base/Dockerfile. +# here. Version-specific items (avif/pcre packages, gd configure flags, oauth, +# xdebug) live in each {version}/base/Dockerfile. # # This stage is referenced as the 'common-php-ext' Bake named context. # It is cached in GitHub Actions (type=gha) but never pushed to Docker Hub. -FROM php:${PHP_VERSION}-apache AS php-ext-common +FROM php-base AS php-ext-common #== System Environments ARG DEBIAN_FRONTEND=noninteractive @@ -283,6 +177,23 @@ RUN echo 'Dpkg::Use-Pty "0";' > /etc/apt/apt.conf.d/01dpkg-no-pty \ && rm -rf /var/lib/apt/lists/* \ && rm -f /etc/apt/apt.conf.d/01dpkg-no-pty +#== Update PEAR for HTTP chunked response support in legacy PHP images +COPY resolve-pear-version.php /tmp/resolve-pear-version.php +ARG PEAR_MIN_VERSION=1.10.18 +RUN installed_pear="$(pear version)" \ + && installed_pear="${installed_pear#PEAR Version: }" \ + && if dpkg --compare-versions "${installed_pear%%[[:space:]]*}" lt "$PEAR_MIN_VERSION"; then \ + PEAR_VERSION="$(php /tmp/resolve-pear-version.php "$PEAR_MIN_VERSION")" \ + && echo "Updating PEAR to ${PEAR_VERSION}" \ + && curl -fsSL --compressed --retry 5 --retry-all-errors --connect-timeout 10 \ + "https://pear.php.net/get/PEAR-${PEAR_VERSION}.tgz" -o /tmp/pear.tgz \ + && pear upgrade --offline --loose /tmp/pear.tgz \ + && mkdir -p "$(pear config-get cache_dir)" \ + && pear clear-cache \ + && rm -f /tmp/pear.tgz; \ + fi \ + && rm -f /tmp/resolve-pear-version.php + # Install development libraries, build all common PHP and PECL extensions, # build libsodium from source, then remove the dev libraries and their # automatically-added dependencies that are not needed at runtime. @@ -332,19 +243,27 @@ RUN --mount=type=bind,from=common-downloader,source=/tmp/libsodium,target=/tmp/l bcmath bz2 calendar exif gettext gmp intl mysqli \ pdo_mysql pdo_pgsql shmop soap sockets sysvmsg sysvsem sysvshm \ tidy xsl zip pcntl \ + && for i in $(seq 1 3); do \ + pecl channel-update https://pecl.php.net/channel.xml \ + && break \ + || { [ "$i" -lt 3 ] && sleep 5; }; \ + done \ && pecl install -o -f \ - redis-5.3.7 \ - yaml-2.2.2 \ - memcached-3.2.0 \ + redis \ + yaml \ + memcached \ && rm -f /usr/local/etc/php/conf.d/*sodium.ini \ && rm -f /usr/local/lib/php/extensions/*/*sodium.so \ && apt-get remove libsodium* -y \ && ( cd /tmp/libsodium/libsodium-1.0.18-RELEASE/ && ./configure && make && make check && make install ) \ && pecl install -o -f libsodium \ && docker-php-ext-enable \ - bcmath bz2 calendar exif gettext gmp intl mysqli opcache \ + bcmath bz2 calendar exif gettext gmp intl mysqli \ pdo_mysql pdo_pgsql redis shmop soap sockets sodium sysvmsg \ sysvsem sysvshm tidy xsl yaml zip pcntl memcached \ + && if ! php -r 'exit(extension_loaded("Zend OPcache") ? 0 : 1);'; then \ + docker-php-ext-enable opcache; \ + fi \ && apt-mark auto '.*' > /dev/null \ && echo "$savedAptMark" | xargs -r apt-mark manual \ && { find /usr/local/lib/php/extensions -name '*.so' \ @@ -355,7 +274,7 @@ RUN --mount=type=bind,from=common-downloader,source=/tmp/libsodium,target=/tmp/l | while IFS= read -r lib; do dpkg-query --search "$(realpath "$lib" 2>/dev/null || echo "$lib")" 2>/dev/null; done \ | cut -d: -f1 \ | sort -u \ - | xargs -r apt-mark manual; } || true \ + | xargs -r apt-mark manual; } \ && apt-get purge -y --auto-remove \ -o APT::AutoRemove::RecommendsImportant=false \ && rm -rf /var/lib/apt/lists/* \ @@ -397,11 +316,6 @@ RUN echo 'Dpkg::Use-Pty "0";' > /etc/apt/apt.conf.d/01dpkg-no-pty \ && rm /tmp/code-server.deb \ && rm -f /etc/apt/apt.conf.d/01dpkg-no-pty -# Store the GitHub Copilot Chat VSIX (repacked with standard Deflate above) at -# a persistent path outside APP_ROOT (which is a mounted volume) so it is -# available at container startup for apache-start.sh to install. -COPY --from=common-downloader /tmp/copilot-chat.vsix /usr/local/share/devpanel/copilot-chat.vsix - # Install Composer 2.x (default 2.3.10; PHP 8.3 overrides to 2.7.7 via self-update) # and legacy Composer 1.x for projects that require it. # 'composer2' bash alias is set after USER switch below. diff --git a/base/resolve-pear-version.php b/base/resolve-pear-version.php new file mode 100644 index 00000000..9211878d --- /dev/null +++ b/base/resolve-pear-version.php @@ -0,0 +1,77 @@ +r as $release) { + $version = (string) $release->v; + if ((string) $release->s === 'stable' + && preg_match('/^[0-9]+(?:\.[0-9]+)+$/D', $version) + && version_compare($version, $minimum, '>=') + ) { + $versions[] = $version; + } + } + usort($versions, function ($a, $b) { + return version_compare($b, $a); + }); + foreach ($versions as $version) { + $package = $metadata("https://pear.php.net/rest/r/pear/package.$version.xml"); + if (!isset($package->dependencies->required->php->min)) { + throw new RuntimeException("Missing PHP requirements for PEAR $version"); + } + $php = $package->dependencies->required->php; + if ((string) $php->min === '') { + throw new RuntimeException("Empty PHP minimum for PEAR $version"); + } + if (version_compare($phpVersion, (string) $php->min, '<') + || ((string) $php->max !== '' && version_compare($phpVersion, (string) $php->max, '>')) + ) { + continue; + } + foreach ($php->exclude as $excluded) { + if (version_compare($phpVersion, (string) $excluded, '==')) { + continue 2; + } + } + return $version; + } + throw new RuntimeException("No stable PEAR release >= $minimum supports PHP $phpVersion"); +} + +if (realpath($_SERVER['SCRIPT_FILENAME']) === __FILE__) { + try { + echo compatiblePearVersion( + pearMetadata('https://pear.php.net/rest/r/pear/allreleases.xml'), + PHP_VERSION, + $argv[1], + 'pearMetadata' + ), "\n"; + } catch (Throwable $error) { + fwrite(STDERR, $error->getMessage() . "\n"); + exit(1); + } +} diff --git a/base/scripts/apache-start.sh b/base/scripts/apache-start.sh index 4988dd49..1ced8e9a 100644 --- a/base/scripts/apache-start.sh +++ b/base/scripts/apache-start.sh @@ -57,10 +57,7 @@ chown "${SUDO_USER:-$USER}:" "$CODES_USER_DATA_DIR" "$CODES_USER_DATA_DIR/extens set -m if [[ "$CODES_ENABLE" == "yes" ]]; then - # Install the GitHub Copilot Chat extension and any user-specified VSCode extensions. - if [ -z "$(find "$CODES_USER_DATA_DIR/extensions/" -maxdepth 1 -iname 'github.copilot-chat-*' -type d 2>/dev/null)" ]; then - sudo -u "${SUDO_USER:-$USER}" -E -- code-server --install-extension /usr/local/share/devpanel/copilot-chat.vsix --user-data-dir="$CODES_USER_DATA_DIR" - fi + # Install any user-specified VSCode extensions. if [ -n "${DP_VSCODE_EXTENSIONS:-}" ]; then IFS=',' read -ra _dp_extensions <<< "$DP_VSCODE_EXTENSIONS" for value in "${_dp_extensions[@]}"; do diff --git a/docker-bake.hcl b/docker-bake.hcl index d3003419..233c112e 100644 --- a/docker-bake.hcl +++ b/docker-bake.hcl @@ -19,11 +19,10 @@ # PLATFORMS Comma-separated target platforms ("linux/amd64,linux/arm64") # VERSIONS_NEEDING_MULTIPART_FIX Versions running Debian 11 / modsec 2.9.3 ("7.4 8.0") # -# Per-version code-server and Copilot Chat build args are NOT global bake -# variables. In CI they are injected per-target via a JSON bake-override file -# generated by the preseed-downloads composite action (one entry per -# phpX_Y-downloader target). For standalone local builds the Dockerfile ARG -# defaults are used as fallback. +# Per-version code-server build args are NOT global bake variables. In CI they +# are injected per-target via a JSON bake-override file generated by the +# preseed-downloads composite action (one entry per phpX_Y-downloader target). +# For standalone local builds the Dockerfile ARG defaults are used as fallback. # # Cache behavior depends on GHCR_WRITABLE. When GHCR_WRITABLE=true, all # targets use GHCR registry cache (type=registry, mode=max) as the sole @@ -70,12 +69,12 @@ variable "VERSIONS_SECURE" { default = "7.4 8.0 8.1 8.2 8.3" } variable "CORERULESET_VERSION" { default = "3.3.5" } # DOWNLOADS_DIR: path to a directory whose pre-downloaded/ subdirectory contains -# pre-seeded build artifacts (code-server .deb files and Copilot Chat VSIX). -# When set by CI to a runner-local path pre-populated by actions/cache@v5, the -# downloader stage bind-mounts /pre-downloaded from it instead of downloading. -# When empty (default), no named context override is provided and the Dockerfile's -# 'downloads' stage (FROM alpine:3 with an empty /pre-downloaded dir) is used, -# causing all downloads to fall through to the normal network path. +# pre-seeded build artifacts (code-server .deb files). When set by CI to a +# runner-local path pre-populated by actions/cache@v5, the downloader stage +# bind-mounts /pre-downloaded from it instead of downloading. When empty (default), +# no named context override is provided and the Dockerfile's 'downloads' stage +# (FROM alpine:3 with an empty /pre-downloaded dir) is used, causing all downloads +# to fall through to the normal network path. variable "DOWNLOADS_DIR" { default = "" } variable "CACHE_FROM_ENABLED" { default = "true" } # GHCR_WRITABLE is set by the detect-versions action (runs once centrally @@ -199,11 +198,11 @@ function "cache_to_registry" { # ─── Per-version downloader targets (registry-cached in GHCR, NOT pushed to Docker Hub) ─ # Each PHP version has its own downloader that runs inside php:${PHP_VERSION}-apache, # which uses the correct Debian release for that PHP version. In CI the code-server -# and Copilot Chat build args (CODESERVER_VERSION, CODESERVER_DEB_SHA256_AMD64/ARM64, -# COPILOT_CHAT_VERSION, COPILOT_CHAT_VSIX_SHA256) are injected per-target via a JSON -# bake-override file generated by the preseed-downloads composite action. That action -# detects each version's Debian codename at runtime and resolves the newest compatible -# binary for it — no hardcoded Debian version names anywhere in this file. +# build args (CODESERVER_VERSION, CODESERVER_DEB_SHA256_AMD64/ARM64) are injected +# per-target via a JSON bake-override file generated by the preseed-downloads +# composite action. That action detects each version's Debian codename at runtime +# and resolves the newest compatible binary for it — no hardcoded Debian version +# names anywhere in this file. # For standalone local builds the Dockerfile ARG defaults are used as fallback. # # Matrix generates one target per version: php7_4-downloader, php8_0-downloader, ... diff --git a/tests/baselines/hadolint-baseline.json b/tests/baselines/hadolint-baseline.json index d79e18bd..672f097a 100644 --- a/tests/baselines/hadolint-baseline.json +++ b/tests/baselines/hadolint-baseline.json @@ -39,9 +39,9 @@ "./advance/Dockerfile:DL3006": 1, "./advance/Dockerfile:DL3008": 2, "./advance/Dockerfile:DL4006": 2, - "./base/Dockerfile:DL3003": 2, - "./base/Dockerfile:DL3008": 3, - "./base/Dockerfile:DL3022": 8, + "./base/Dockerfile:DL3003": 1, + "./base/Dockerfile:DL3008": 2, + "./base/Dockerfile:DL3022": 7, "./base/Dockerfile:DL3047": 1, "./base/Dockerfile:DL4001": 1, "./base/Dockerfile:DL4006": 4, diff --git a/tests/build-dockerfile.sh b/tests/build-dockerfile.sh index ec2800d9..2e08c6b4 100755 --- a/tests/build-dockerfile.sh +++ b/tests/build-dockerfile.sh @@ -137,9 +137,9 @@ PLATFORM_KEY="${NORMALIZED_PLATFORM//\//-}" # preseed-downloads composite action. When set, it is # passed as an additional --file argument so each # phpX_Y-downloader target receives the correct -# code-server/Copilot Chat version and SHA256 args for -# its Debian tier. When unset, the Dockerfile ARG -# defaults are used as fallback. +# code-server version and SHA256 args for its Debian +# tier. When unset, the Dockerfile ARG defaults are +# used as fallback. # # --load: # Import built images into the local Docker daemon so that run-dockerfile.sh diff --git a/tests/resolve-pear-version.php b/tests/resolve-pear-version.php new file mode 100644 index 00000000..b1256847 --- /dev/null +++ b/tests/resolve-pear-version.php @@ -0,0 +1,69 @@ + + 1.10.18stable + 1.11.0stable + 1.12.0beta + 1.10.20stable + ../../invalidstable +'); +$requirements = [ + '1.10.18' => '5.4.0', + '1.10.20' => '7.4.08.4.998.0.30', + '1.11.0' => '8.1.0', +]; +$metadata = function ($url) use ($requirements) { + $version = basename($url, '.xml'); + $version = substr($version, strlen('package.')); + if (!isset($requirements[$version])) { + throw new RuntimeException("Unexpected metadata request: $url"); + } + return simplexml_load_string( + '' + . '' . $requirements[$version] + . '' + ); +}; + +foreach (['7.4.33' => '1.10.20', '8.0.30' => '1.10.18', '8.5.12' => '1.11.0'] as $php => $expected) { + $actual = compatiblePearVersion($releases, $php, '1.10.18', $metadata); + if ($actual !== $expected) { + throw new RuntimeException("PHP $php: expected $expected, got $actual"); + } +} + +$olderReleases = simplexml_load_string(' + 1.10.20stable + 1.10.18stable +'); +if (compatiblePearVersion($olderReleases, '8.5.12', '1.10.18', $metadata) !== '1.10.18') { + throw new RuntimeException('PHP maximum was not respected'); +} + +foreach ([ + function () use ($releases, $metadata) { + compatiblePearVersion($releases, '5.3.0', '1.10.18', $metadata); + }, + function () use ($releases, $metadata) { + compatiblePearVersion($releases, '7.4.33', '1.11.0', $metadata); + }, + function () use ($releases) { + compatiblePearVersion($releases, '7.4.33', '1.10.18', function () { + return simplexml_load_string(''); + }); + }, + function () use ($metadata) { + compatiblePearVersion(simplexml_load_string(''), '7.4.33', '1.10.18', $metadata); + }, +] as $failure) { + try { + $failure(); + } catch (RuntimeException $expected) { + continue; + } + throw new RuntimeException('Expected resolution to fail'); +} + +echo "PEAR compatibility selection checks passed\n"; diff --git a/tests/run-dockerfile.sh b/tests/run-dockerfile.sh index ba5139aa..af054ed8 100755 --- a/tests/run-dockerfile.sh +++ b/tests/run-dockerfile.sh @@ -152,6 +152,13 @@ assert_file_exists() { test_base() { local image="$1" version="$2" + if docker run --rm --entrypoint php -v "$REPO_ROOT:/pear-tests:ro" \ + "$image" /pear-tests/tests/resolve-pear-version.php; then + ok "PEAR compatibility selection" + else + err "PEAR compatibility selection" + fi + # PHP CLI — version string must contain the expected major.minor assert_contains "$image" "PHP ${version}\." "PHP ${version} CLI available" \ php --version @@ -181,6 +188,14 @@ test_base() { assert_contains "$image" "imagick" "imagick extension loaded" \ php -m + for extension in redis yaml memcached igbinary; do + assert_contains "$image" "^${extension}$" "${extension} extension loaded" \ + php -m + done + + assert_contains "$image" "^Zend OPcache$" "OPcache extension loaded" \ + php -m + # Verify no PHP extension .so has unresolved shared-library dependencies. # A "not found" line from ldd means a runtime library was removed along with # the dev packages — the failure mode caused by the usrmerge path mismatch @@ -190,14 +205,6 @@ test_base() { # shellcheck disable=SC2016 assert_empty "$image" "no unresolved .so deps in PHP extensions" \ sh -c 'ext=$(php -r "echo PHP_EXTENSION_DIR;") && find "$ext" -name "*.so" -exec ldd {} + 2>/dev/null | grep "not found" || true' - - # GitHub Copilot Chat extension must be installed in the extensions directory. - # code-server --install-extension unpacks the VSIX into a subdirectory named - # after the extension (e.g. GitHub.copilot-chat-). Use find with - # -iname to locate the directory case-insensitively, normalize to lowercase, - # and grep to confirm the expected publisher/name prefix is present. - assert_contains "$image" "github\.copilot-chat" "GitHub Copilot Chat extension installs and is found" \ - sh -c "code-server --install-extension /usr/local/share/devpanel/copilot-chat.vsix --user-data-dir /var/www/html/.vscode && find /var/www/html/.vscode/extensions/ -maxdepth 1 -iname 'github.copilot-chat-*' -type d | tr '[:upper:]' '[:lower:]' | grep 'github.copilot-chat'" } test_secure() {