Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
123 changes: 82 additions & 41 deletions .github/workflows/docker.publish.yml
Original file line number Diff line number Diff line change
Expand Up @@ -126,49 +126,89 @@ jobs:
set -euo pipefail

retry() {
local attempt=1
local attempt=1 max_attempts=5
until "$@"; do
(( attempt >= 5 )) && return 1
if (( attempt >= max_attempts )); then
echo "Command failed after ${max_attempts} attempts: $*" >&2
return 1
fi
echo "Retrying failed command in $((attempt * 2)) seconds: $*" >&2
sleep "$((attempt * 2))"
attempt=$((attempt + 1))
done
}

get_httpd_digest() {
local output digest
output="$(docker buildx imagetools inspect httpd:alpine 2>&1)" || { echo "$output" >&2; return 1; }
digest="$(printf '%s\n' "$output" | awk '$1 == "Digest:" {print $2; exit}')"
[[ -n "$digest" ]] || return 1
output="$(docker buildx imagetools inspect httpd:alpine 2>&1)" || {
echo "$output" >&2
return 1
}
digest="$(awk '$1 == "Digest:" {print $2; exit}' <<<"$output")"
[[ "$digest" =~ ^sha256:[0-9a-f]{64}$ ]] || {
echo 'Could not resolve a valid httpd:alpine digest.' >&2
return 1
}
printf '%s\n' "$digest"
}

get_scriptomatic_sha() {
git ls-remote https://github.com/infocyph/Scriptomatic.git refs/heads/main | awk 'NF {print $1; exit}'
local output sha
output="$(git ls-remote https://github.com/infocyph/Scriptomatic.git refs/heads/main 2>&1)" || {
echo "$output" >&2
return 1
}
sha="$(awk 'NF {print $1; exit}' <<<"$output")"
[[ "$sha" =~ ^[0-9a-f]{40}$ ]] || {
echo 'Could not resolve a valid Scriptomatic main revision.' >&2
return 1
}
printf '%s\n' "$sha"
}

get_toolset_release() {
gh api repos/infocyph/Toolset/releases/latest --jq '.tag_name // empty'
local tag
tag="$(gh api repos/infocyph/Toolset/releases/latest --jq '.tag_name // empty')" || return 1
[[ "$tag" =~ ^v?[0-9]+\.[0-9]+(\.[0-9]+)?(-rc\.[0-9]+)?$ ]] || {
echo "Could not resolve a valid latest Toolset release: $tag" >&2
return 1
}
printf '%s\n' "$tag"
}

get_installer_sha() {
local file
local release="$1" file checksum_output sha
file="$(mktemp)"
curl --fail --silent --show-error --location \
if ! curl --fail --silent --show-error --location \
--retry 5 --retry-all-errors --retry-delay 2 \
--connect-timeout 15 --max-time 120 \
https://github.com/infocyph/Toolset/releases/latest/download/install.sh \
-o "$file"
test -s "$file"
sha256sum "$file" | awk '{print $1}'
"https://github.com/infocyph/Toolset/releases/download/${release}/install.sh" \
-o "$file"; then
rm -f "$file"
return 1
fi
if [[ ! -s "$file" ]]; then
echo "Toolset installer is empty for release $release." >&2
rm -f "$file"
return 1
fi
checksum_output="$(sha256sum "$file")" || {
rm -f "$file"
return 1
}
rm -f "$file"
sha="${checksum_output%% *}"
[[ "$sha" =~ ^[0-9a-f]{64}$ ]] || {
echo "Could not calculate Toolset installer checksum for $release." >&2
return 1
}
printf '%s\n' "$sha"
}

HTTPD_ALPINE_DIGEST="$(retry get_httpd_digest)"
SCRIPTOMATIC_MAIN_SHA="$(retry get_scriptomatic_sha)"
TOOLSET_RELEASE="$(retry get_toolset_release)"
TOOLSET_INSTALLER_SHA256="$(retry get_installer_sha)"

[[ -n "$HTTPD_ALPINE_DIGEST" && -n "$SCRIPTOMATIC_MAIN_SHA" && -n "$TOOLSET_RELEASE" && -n "$TOOLSET_INSTALLER_SHA256" ]]
TOOLSET_INSTALLER_SHA256="$(retry get_installer_sha "$TOOLSET_RELEASE")"

{
echo "HTTPD_ALPINE_DIGEST=$HTTPD_ALPINE_DIGEST"
Expand All @@ -177,6 +217,15 @@ jobs:
echo "TOOLSET_INSTALLER_SHA256=$TOOLSET_INSTALLER_SHA256"
} >> "$GITHUB_ENV"

{
echo '## Upstream snapshot'
echo
echo "- httpd:alpine: \`$HTTPD_ALPINE_DIGEST\`"
echo "- Scriptomatic main: \`$SCRIPTOMATIC_MAIN_SHA\`"
echo "- Toolset release: \`$TOOLSET_RELEASE\`"
echo "- Toolset installer SHA-256: \`$TOOLSET_INSTALLER_SHA256\`"
} >> "$GITHUB_STEP_SUMMARY"

- name: Build fresh amd64 release candidate
uses: docker/build-push-action@v7
with:
Expand All @@ -187,6 +236,11 @@ jobs:
no-cache: true
push: false
tags: infocyph/apache:publish-candidate
build-args: |
HTTPD_ALPINE_REF=httpd:alpine@${{ env.HTTPD_ALPINE_DIGEST }}
SCRIPTOMATIC_REF=${{ env.SCRIPTOMATIC_MAIN_SHA }}
TOOLSET_RELEASE=${{ env.TOOLSET_RELEASE }}
TOOLSET_INSTALLER_SHA256=${{ env.TOOLSET_INSTALLER_SHA256 }}
cache-to: type=gha,scope=apache-publish-amd64,mode=max

- name: Record candidate runtime resolution
Expand Down Expand Up @@ -229,6 +283,11 @@ jobs:
no-cache: true
push: false
tags: infocyph/apache:publish-candidate-arm64
build-args: |
HTTPD_ALPINE_REF=httpd:alpine@${{ env.HTTPD_ALPINE_DIGEST }}
SCRIPTOMATIC_REF=${{ env.SCRIPTOMATIC_MAIN_SHA }}
TOOLSET_RELEASE=${{ env.TOOLSET_RELEASE }}
TOOLSET_INSTALLER_SHA256=${{ env.TOOLSET_INSTALLER_SHA256 }}
cache-to: type=gha,scope=apache-publish-arm64,mode=max

- name: Final arm64 release-candidate gate
Expand All @@ -243,29 +302,6 @@ jobs:
chromacat --version
'

- name: Revalidate rolling upstreams before publish
env:
GH_TOKEN: ${{ github.token }}
run: |
set -euo pipefail

current_httpd="$(docker buildx imagetools inspect httpd:alpine | awk '$1 == "Digest:" {print $2; exit}')"
current_scriptomatic="$(git ls-remote https://github.com/infocyph/Scriptomatic.git refs/heads/main | awk 'NF {print $1; exit}')"
current_toolset="$(gh api repos/infocyph/Toolset/releases/latest --jq '.tag_name // empty')"
file="$(mktemp)"
trap 'rm -f "$file"' EXIT
curl --fail --silent --show-error --location \
--retry 5 --retry-all-errors --retry-delay 2 \
--connect-timeout 15 --max-time 120 \
https://github.com/infocyph/Toolset/releases/latest/download/install.sh \
-o "$file"
current_installer="$(sha256sum "$file" | awk '{print $1}')"

test "$current_httpd" = "$HTTPD_ALPINE_DIGEST"
test "$current_scriptomatic" = "$SCRIPTOMATIC_MAIN_SHA"
test "$current_toolset" = "$TOOLSET_RELEASE"
test "$current_installer" = "$TOOLSET_INSTALLER_SHA256"

- name: Log in to Docker Hub
uses: docker/login-action@v4
with:
Expand Down Expand Up @@ -331,6 +367,11 @@ jobs:
push: true
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
build-args: |
HTTPD_ALPINE_REF=httpd:alpine@${{ env.HTTPD_ALPINE_DIGEST }}
SCRIPTOMATIC_REF=${{ env.SCRIPTOMATIC_MAIN_SHA }}
TOOLSET_RELEASE=${{ env.TOOLSET_RELEASE }}
TOOLSET_INSTALLER_SHA256=${{ env.TOOLSET_INSTALLER_SHA256 }}
cache-from: |
type=gha,scope=apache-publish-amd64
type=gha,scope=apache-publish-arm64
Expand Down Expand Up @@ -362,8 +403,8 @@ jobs:
local ref="$1" output digest
for _ in $(seq 1 12); do
output="$(docker buildx imagetools inspect "$ref" 2>&1 || true)"
digest="$(printf '%s\n' "$output" | awk '/^Digest:/ {print $2; exit}')"
if [[ -n "$digest" ]]; then
digest="$(awk '/^Digest:/ {print $2; exit}' <<<"$output")"
if [[ "$digest" =~ ^sha256:[0-9a-f]{64}$ ]]; then
printf '%s\n' "$digest"
return 0
fi
Expand Down
24 changes: 20 additions & 4 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -1,11 +1,15 @@
FROM httpd:alpine
ARG HTTPD_ALPINE_REF=httpd:alpine
FROM ${HTTPD_ALPINE_REF}

LABEL org.opencontainers.image.source="https://github.com/infocyph/docker-apache"
LABEL org.opencontainers.image.description="Hardened LocalDevStack Apache backend with PHP-FPM, TLS and HTTP/2 support"
LABEL org.opencontainers.image.licenses="MIT"
LABEL org.opencontainers.image.authors="infocyph,abmmhasan"

ARG TZ=Asia/Dhaka
ARG SCRIPTOMATIC_REF=main
ARG TOOLSET_RELEASE=latest
ARG TOOLSET_INSTALLER_SHA256=

ENV APACHE_LOG_DIR=/var/log/apache2 \
SERVER_NAME=localhost \
Expand Down Expand Up @@ -39,17 +43,29 @@ COPY scripts/healthcheck.sh /usr/local/bin/healthcheck
RUN set -eux; \
curl -fsSL --retry 3 --retry-all-errors --retry-delay 1 \
--connect-timeout 10 --max-time 120 \
"https://raw.githubusercontent.com/infocyph/Scriptomatic/main/bash/banner.sh" \
"https://raw.githubusercontent.com/infocyph/Scriptomatic/${SCRIPTOMATIC_REF}/bash/banner.sh" \
-o /usr/local/bin/show-banner; \
test -s /usr/local/bin/show-banner; \
bash -n /usr/local/bin/show-banner; \
if [ "$TOOLSET_RELEASE" = latest ]; then \
toolset_installer_url="https://github.com/infocyph/Toolset/releases/latest/download/install.sh"; \
else \
toolset_installer_url="https://github.com/infocyph/Toolset/releases/download/${TOOLSET_RELEASE}/install.sh"; \
fi; \
curl -fsSL --retry 3 --retry-all-errors --retry-delay 1 \
--connect-timeout 10 --max-time 120 \
"https://github.com/infocyph/Toolset/releases/latest/download/install.sh" \
"$toolset_installer_url" \
-o /tmp/toolset-install.sh; \
test -s /tmp/toolset-install.sh; \
if [ -n "$TOOLSET_INSTALLER_SHA256" ]; then \
printf '%s %s\n' "$TOOLSET_INSTALLER_SHA256" /tmp/toolset-install.sh | sha256sum -c -; \
fi; \
bash -n /tmp/toolset-install.sh; \
bash /tmp/toolset-install.sh --prefix /usr/local/bin chromacat; \
if [ "$TOOLSET_RELEASE" = latest ]; then \
bash /tmp/toolset-install.sh --latest --prefix /usr/local/bin chromacat; \
else \
bash /tmp/toolset-install.sh --release "$TOOLSET_RELEASE" --prefix /usr/local/bin chromacat; \
fi; \
chromacat --version; \
rm -f /tmp/toolset-install.sh; \
chmod +x \
Expand Down
37 changes: 37 additions & 0 deletions tests/release-contract.sh
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,14 @@ for contract in \
"releases/tags/\${MANUAL_RELEASE_TAG}" \
'PUBLISH_RELEASE_TAG' \
'Enforce immutable release tags' \
'Snapshot rolling upstream inputs' \
'retry get_httpd_digest' \
'retry get_scriptomatic_sha' \
'retry get_toolset_release' \
"HTTPD_ALPINE_REF=httpd:alpine@\${{ env.HTTPD_ALPINE_DIGEST }}" \
"SCRIPTOMATIC_REF=\${{ env.SCRIPTOMATIC_MAIN_SHA }}" \
"TOOLSET_RELEASE=\${{ env.TOOLSET_RELEASE }}" \
"TOOLSET_INSTALLER_SHA256=\${{ env.TOOLSET_INSTALLER_SHA256 }}" \
'linux/amd64,linux/arm64' \
'provenance: mode=max' \
'sbom: true'; do
Expand All @@ -25,6 +33,35 @@ done
grep -Fq "cron: '0 0 * * 0'" "$workflow"
grep -Fq 'types: [published]' "$workflow"

for pinned_contract in \
"HTTPD_ALPINE_REF=httpd:alpine@\${{ env.HTTPD_ALPINE_DIGEST }}" \
"SCRIPTOMATIC_REF=\${{ env.SCRIPTOMATIC_MAIN_SHA }}" \
"TOOLSET_RELEASE=\${{ env.TOOLSET_RELEASE }}" \
"TOOLSET_INSTALLER_SHA256=\${{ env.TOOLSET_INSTALLER_SHA256 }}"; do
pinned_builds="$(grep -cF "$pinned_contract" "$workflow")"
if [[ "$pinned_builds" -ne 3 ]]; then
echo "Expected all three publish builds to use pinned input: $pinned_contract; found $pinned_builds." >&2
exit 1
fi
done

if grep -Fq 'Revalidate rolling upstreams before publish' "$workflow"; then
echo 'Publish workflow must not re-resolve mutable upstreams after immutable snapshotting.' >&2
exit 1
fi
if grep -Fq 'imagetools inspect httpd:alpine | awk' "$workflow"; then
echo 'Unsafe pipefail-sensitive httpd digest pipeline detected.' >&2
exit 1
fi
if grep -Fq 'Scriptomatic.git refs/heads/main | awk' "$workflow"; then
echo 'Unsafe pipefail-sensitive Scriptomatic revision pipeline detected.' >&2
exit 1
fi
if grep -Fq 'Toolset/releases/latest/download/install.sh' "$workflow"; then
echo 'Publish workflow must checksum an exact Toolset release installer.' >&2
exit 1
fi

if grep -Fq 'actions/checkout@v4' "$workflow"; then
echo 'Legacy checkout action detected.' >&2
exit 1
Expand Down
10 changes: 8 additions & 2 deletions tests/static.sh
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,10 @@ done

shellcheck scripts/*.sh tests/*.sh

grep -Fq 'FROM httpd:alpine' Dockerfile
grep -Fq 'ARG HTTPD_ALPINE_REF=httpd:alpine' Dockerfile
grep -Fq "FROM \${HTTPD_ALPINE_REF}" Dockerfile
grep -Fq 'ARG SCRIPTOMATIC_REF=main' Dockerfile
grep -Fq 'ARG TOOLSET_RELEASE=latest' Dockerfile
grep -Fq 'apk upgrade --no-cache' Dockerfile
grep -Fq 'apache2-utils' Dockerfile
if grep -Eq '^[[:space:]]+apache-mod-fcgid([[:space:]\\;]|$)' Dockerfile; then
Expand All @@ -28,8 +31,11 @@ fi
if grep -Eq '^[[:space:]]+apache2([[:space:]\\;]|$)' Dockerfile; then
fail 'Alpine apache2 server package must not be installed'
fi
grep -Fq 'Scriptomatic/main/bash/banner.sh' Dockerfile
grep -Fq "Scriptomatic/\${SCRIPTOMATIC_REF}/bash/banner.sh" Dockerfile
grep -Fq 'Toolset/releases/latest/download/install.sh' Dockerfile
grep -Fq "Toolset/releases/download/\${TOOLSET_RELEASE}/install.sh" Dockerfile
grep -Fq -- "--release \"\$TOOLSET_RELEASE\"" Dockerfile
grep -Fq -- '--latest --prefix /usr/local/bin chromacat' Dockerfile
helper_downloads="$(grep -c -- '--connect-timeout 10 --max-time 120' Dockerfile)"
if [[ "$helper_downloads" -ne 2 ]]; then
fail 'Both helper downloads must use bounded connect and total timeouts'
Expand Down