diff --git a/.github/workflows/build-devcontainer-bun.yml b/.github/workflows/build-devcontainer-bun.yml index c9f0019..a04ef89 100644 --- a/.github/workflows/build-devcontainer-bun.yml +++ b/.github/workflows/build-devcontainer-bun.yml @@ -31,7 +31,7 @@ jobs: BUN_VERSION=$(grep '^ARG BUN_VERSION=' "$DOCKERFILE" | cut -d'=' -f2) - echo "bun=$BUN_VERSION" >> $GITHUB_OUTPUT + echo "bun=$BUN_VERSION" >> "$GITHUB_OUTPUT" echo "Extracted versions:" echo " Bun: $BUN_VERSION" @@ -48,8 +48,10 @@ jobs: TAGS="${BASE_IMAGE}:latest,${BASE_IMAGE}:${VERSION_TAG}" - echo "namespace=$NAMESPACE" >> $GITHUB_OUTPUT - echo "tags=$TAGS" >> $GITHUB_OUTPUT + { + echo "namespace=$NAMESPACE" + echo "tags=$TAGS" + } >> "$GITHUB_OUTPUT" echo "Generated tags:" echo "$TAGS" | tr ',' '\n' diff --git a/.github/workflows/build-devcontainer-claude-bun.yml b/.github/workflows/build-devcontainer-claude-bun.yml index 9869f82..eb3b80f 100644 --- a/.github/workflows/build-devcontainer-claude-bun.yml +++ b/.github/workflows/build-devcontainer-claude-bun.yml @@ -32,7 +32,7 @@ jobs: BUN_VERSION=$(grep '^ARG BUN_VERSION=' "$DOCKERFILE" | cut -d'=' -f2) - echo "bun=$BUN_VERSION" >> $GITHUB_OUTPUT + echo "bun=$BUN_VERSION" >> "$GITHUB_OUTPUT" echo "Extracted versions:" echo " Bun: $BUN_VERSION" @@ -49,8 +49,10 @@ jobs: TAGS="${BASE_IMAGE}:latest,${BASE_IMAGE}:${VERSION_TAG}" - echo "namespace=$NAMESPACE" >> $GITHUB_OUTPUT - echo "tags=$TAGS" >> $GITHUB_OUTPUT + { + echo "namespace=$NAMESPACE" + echo "tags=$TAGS" + } >> "$GITHUB_OUTPUT" echo "Generated tags:" echo "$TAGS" | tr ',' '\n' diff --git a/.github/workflows/build-devcontainer-hugo-bun-node.yml b/.github/workflows/build-devcontainer-hugo-bun-node.yml index 035f86b..26c7651 100644 --- a/.github/workflows/build-devcontainer-hugo-bun-node.yml +++ b/.github/workflows/build-devcontainer-hugo-bun-node.yml @@ -33,9 +33,11 @@ jobs: BUN_VERSION=$(grep '^ARG BUN_VERSION=' "$DOCKERFILE" | cut -d'=' -f2) NODE_VERSION=$(grep '^ARG NODE_VERSION=' "$DOCKERFILE" | cut -d'=' -f2) - echo "hugo=$HUGO_VERSION" >> $GITHUB_OUTPUT - echo "bun=$BUN_VERSION" >> $GITHUB_OUTPUT - echo "node=$NODE_VERSION" >> $GITHUB_OUTPUT + { + echo "hugo=$HUGO_VERSION" + echo "bun=$BUN_VERSION" + echo "node=$NODE_VERSION" + } >> "$GITHUB_OUTPUT" echo "Extracted versions:" echo " Hugo: $HUGO_VERSION" @@ -56,8 +58,10 @@ jobs: TAGS="${BASE_IMAGE}:latest,${BASE_IMAGE}:${VERSION_TAG}" - echo "namespace=$NAMESPACE" >> $GITHUB_OUTPUT - echo "tags=$TAGS" >> $GITHUB_OUTPUT + { + echo "namespace=$NAMESPACE" + echo "tags=$TAGS" + } >> "$GITHUB_OUTPUT" echo "Generated tags:" echo "$TAGS" | tr ',' '\n' diff --git a/.github/workflows/build-devcontainer-hugo-bun.yml b/.github/workflows/build-devcontainer-hugo-bun.yml index 92bf03b..ef1b515 100644 --- a/.github/workflows/build-devcontainer-hugo-bun.yml +++ b/.github/workflows/build-devcontainer-hugo-bun.yml @@ -32,8 +32,10 @@ jobs: HUGO_VERSION=$(grep '^ARG HUGO_VERSION=' "$DOCKERFILE" | cut -d'=' -f2) BUN_VERSION=$(grep '^ARG BUN_VERSION=' "$DOCKERFILE" | cut -d'=' -f2) - echo "hugo=$HUGO_VERSION" >> $GITHUB_OUTPUT - echo "bun=$BUN_VERSION" >> $GITHUB_OUTPUT + { + echo "hugo=$HUGO_VERSION" + echo "bun=$BUN_VERSION" + } >> "$GITHUB_OUTPUT" echo "Extracted versions:" echo " Hugo: $HUGO_VERSION" @@ -52,8 +54,10 @@ jobs: TAGS="${BASE_IMAGE}:latest,${BASE_IMAGE}:${VERSION_TAG}" - echo "namespace=$NAMESPACE" >> $GITHUB_OUTPUT - echo "tags=$TAGS" >> $GITHUB_OUTPUT + { + echo "namespace=$NAMESPACE" + echo "tags=$TAGS" + } >> "$GITHUB_OUTPUT" echo "Generated tags:" echo "$TAGS" | tr ',' '\n' diff --git a/.github/workflows/build-ralphex-fe.yml b/.github/workflows/build-ralphex-fe.yml index 6b5825f..1e390a1 100644 --- a/.github/workflows/build-ralphex-fe.yml +++ b/.github/workflows/build-ralphex-fe.yml @@ -43,9 +43,11 @@ jobs: exit 1 fi - echo "bun=$BUN_VERSION" >> $GITHUB_OUTPUT - echo "hugo=$HUGO_VERSION" >> $GITHUB_OUTPUT - echo "ralphex=$RALPHEX_VERSION" >> $GITHUB_OUTPUT + { + echo "bun=$BUN_VERSION" + echo "hugo=$HUGO_VERSION" + echo "ralphex=$RALPHEX_VERSION" + } >> "$GITHUB_OUTPUT" echo "Extracted versions:" echo " Bun: $BUN_VERSION" @@ -66,8 +68,10 @@ jobs: TAGS="${BASE_IMAGE}:latest,${BASE_IMAGE}:${VERSION_TAG}" - echo "namespace=$NAMESPACE" >> $GITHUB_OUTPUT - echo "tags=$TAGS" >> $GITHUB_OUTPUT + { + echo "namespace=$NAMESPACE" + echo "tags=$TAGS" + } >> "$GITHUB_OUTPUT" echo "Generated tags:" echo "$TAGS" | tr ',' '\n' diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..9879676 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,184 @@ +name: CI + +on: + pull_request: + branches: [master] + paths: + - '**/Dockerfile' + - '**/*.sh' + - '.github/workflows/**' + - '.hadolint.yaml' + +jobs: + # ── Lint Dockerfiles with hadolint ────────────────────────────────── + lint-dockerfiles: + name: Hadolint · ${{ matrix.dockerfile }} + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + dockerfile: + - devcontainer-bun/.devcontainer/Dockerfile + - devcontainer-claude-bun/.devcontainer/Dockerfile + - devcontainer-hugo-bun/.devcontainer/Dockerfile + - devcontainer-hugo-bun-node/.devcontainer/Dockerfile + - ralphex-fe/Dockerfile + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Run hadolint + uses: hadolint/hadolint-action@v3.0.0 + with: + dockerfile: ${{ matrix.dockerfile }} + + # ── Lint GitHub Actions workflows with actionlint ─────────────────── + lint-workflows: + name: Actionlint + runs-on: ubuntu-latest + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Run actionlint + uses: raven-actions/actionlint@v2 + + # ── Detect which images have changed files ────────────────────────── + detect-changes: + name: Detect changes + runs-on: ubuntu-latest + outputs: + matrix: ${{ steps.set-matrix.outputs.matrix }} + has-changes: ${{ steps.set-matrix.outputs.has-changes }} + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Check changed paths + uses: dorny/paths-filter@v3 + id: filter + with: + filters: | + devcontainer-bun: + - 'devcontainer-bun/**' + devcontainer-claude-bun: + - 'devcontainer-claude-bun/**' + devcontainer-hugo-bun: + - 'devcontainer-hugo-bun/**' + devcontainer-hugo-bun-node: + - 'devcontainer-hugo-bun-node/**' + ralphex-fe: + - 'ralphex-fe/**' + + - name: Build matrix from changes + id: set-matrix + env: + CHANGED_BUN: ${{ steps.filter.outputs.devcontainer-bun }} + CHANGED_CLAUDE_BUN: ${{ steps.filter.outputs.devcontainer-claude-bun }} + CHANGED_HUGO_BUN: ${{ steps.filter.outputs.devcontainer-hugo-bun }} + CHANGED_HUGO_BUN_NODE: ${{ steps.filter.outputs.devcontainer-hugo-bun-node }} + CHANGED_RALPHEX: ${{ steps.filter.outputs.ralphex-fe }} + run: | + INCLUDES="[]" + + add_image() { + local image="$1" context="$2" dockerfile="$3" verify="$4" + INCLUDES=$(echo "$INCLUDES" | jq -c \ + --arg img "$image" \ + --arg ctx "$context" \ + --arg df "$dockerfile" \ + --arg v "$verify" \ + '. + [{"image":$img,"context":$ctx,"dockerfile":$df,"verify":$v}]') + } + + if [ "$CHANGED_BUN" = "true" ]; then + add_image "devcontainer-bun" \ + "devcontainer-bun/.devcontainer" \ + "devcontainer-bun/.devcontainer/Dockerfile" \ + "bun --version" + fi + + if [ "$CHANGED_CLAUDE_BUN" = "true" ]; then + add_image "devcontainer-claude-bun" \ + "devcontainer-claude-bun/.devcontainer" \ + "devcontainer-claude-bun/.devcontainer/Dockerfile" \ + "bun --version" + fi + + if [ "$CHANGED_HUGO_BUN" = "true" ]; then + add_image "devcontainer-hugo-bun" \ + "devcontainer-hugo-bun/.devcontainer" \ + "devcontainer-hugo-bun/.devcontainer/Dockerfile" \ + "bun --version && hugo version" + fi + + if [ "$CHANGED_HUGO_BUN_NODE" = "true" ]; then + add_image "devcontainer-hugo-bun-node" \ + "devcontainer-hugo-bun-node/.devcontainer" \ + "devcontainer-hugo-bun-node/.devcontainer/Dockerfile" \ + "bun --version && hugo version && node --version" + fi + + if [ "$CHANGED_RALPHEX" = "true" ]; then + add_image "ralphex-fe" \ + "ralphex-fe" \ + "ralphex-fe/Dockerfile" \ + "bun --version && hugo version && ralphex --version" + fi + + if [ "$INCLUDES" = "[]" ]; then + { + echo "has-changes=false" + echo "matrix={\"include\":[]}" + } >> "$GITHUB_OUTPUT" + else + { + echo "has-changes=true" + echo "matrix=$(echo "$INCLUDES" | jq -c '{include:.}')" + } >> "$GITHUB_OUTPUT" + fi + + # ── Build changed images and verify installed tools ───────────────── + build-and-verify: + name: Build · ${{ matrix.image }} + needs: detect-changes + if: needs.detect-changes.outputs.has-changes == 'true' + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: ${{ fromJSON(needs.detect-changes.outputs.matrix) }} + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Set up QEMU + uses: docker/setup-qemu-action@v3 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Build amd64 image + uses: docker/build-push-action@v5 + with: + context: ${{ matrix.context }} + file: ${{ matrix.dockerfile }} + platforms: linux/amd64 + load: true + tags: ${{ matrix.image }}:test + cache-from: type=gha,scope=${{ matrix.image }}-amd64 + cache-to: type=gha,mode=max,scope=${{ matrix.image }}-amd64 + + - name: Verify tools (amd64) + env: + IMAGE_TAG: ${{ matrix.image }}:test + VERIFY_CMD: ${{ matrix.verify }} + run: docker run --rm --entrypoint sh "$IMAGE_TAG" -c "$VERIFY_CMD" + + - name: Build arm64 image + uses: docker/build-push-action@v5 + with: + context: ${{ matrix.context }} + file: ${{ matrix.dockerfile }} + platforms: linux/arm64 + cache-from: type=gha,scope=${{ matrix.image }}-arm64 + cache-to: type=gha,mode=max,scope=${{ matrix.image }}-arm64 diff --git a/.github/workflows/update-and-build-ralphex-fe.yml b/.github/workflows/update-and-build-ralphex-fe.yml index f805237..3598f78 100644 --- a/.github/workflows/update-and-build-ralphex-fe.yml +++ b/.github/workflows/update-and-build-ralphex-fe.yml @@ -88,15 +88,17 @@ jobs: # Check if anything changed if git diff --quiet "${{ env.DOCKERFILE_PATH }}"; then echo "No changes detected - versions are already up to date" - echo "updated=false" >> $GITHUB_OUTPUT + echo "updated=false" >> "$GITHUB_OUTPUT" else echo "Changes detected - versions updated" - echo "updated=true" >> $GITHUB_OUTPUT + echo "updated=true" >> "$GITHUB_OUTPUT" fi - echo "bun_version=$NEW_BUN" >> $GITHUB_OUTPUT - echo "hugo_version=$NEW_HUGO" >> $GITHUB_OUTPUT - echo "ralphex_version=$NEW_RALPHEX" >> $GITHUB_OUTPUT + { + echo "bun_version=$NEW_BUN" + echo "hugo_version=$NEW_HUGO" + echo "ralphex_version=$NEW_RALPHEX" + } >> "$GITHUB_OUTPUT" - name: Commit and push changes if: steps.update.outputs.updated == 'true' @@ -154,9 +156,11 @@ jobs: HUGO_VERSION=$(grep '^ARG HUGO_VERSION=' "${{ env.DOCKERFILE_PATH }}" | cut -d'=' -f2) RALPHEX_VERSION=$(grep '^ARG RALPHEX_VERSION=' "${{ env.DOCKERFILE_PATH }}" | cut -d'=' -f2) - echo "bun=$BUN_VERSION" >> $GITHUB_OUTPUT - echo "hugo=$HUGO_VERSION" >> $GITHUB_OUTPUT - echo "ralphex=$RALPHEX_VERSION" >> $GITHUB_OUTPUT + { + echo "bun=$BUN_VERSION" + echo "hugo=$HUGO_VERSION" + echo "ralphex=$RALPHEX_VERSION" + } >> "$GITHUB_OUTPUT" echo "Building with versions:" echo " Bun: $BUN_VERSION" @@ -177,8 +181,10 @@ jobs: TAGS="${BASE_IMAGE}:latest,${BASE_IMAGE}:${VERSION_TAG}" - echo "namespace=$NAMESPACE" >> $GITHUB_OUTPUT - echo "tags=$TAGS" >> $GITHUB_OUTPUT + { + echo "namespace=$NAMESPACE" + echo "tags=$TAGS" + } >> "$GITHUB_OUTPUT" echo "Generated tags:" echo "$TAGS" | tr ',' '\n' diff --git a/.hadolint.yaml b/.hadolint.yaml new file mode 100644 index 0000000..c78cc9a --- /dev/null +++ b/.hadolint.yaml @@ -0,0 +1,24 @@ +# Hadolint configuration +# https://github.com/hadolint/hadolint#configure +ignored: + # Don't require pinning system packages in apk add / apt-get install. + # Dev container images intentionally use latest OS packages. + - DL3008 # apt-get: pin versions + - DL3018 # apk: pin versions + + # wget is used intentionally for downloading release binaries; + # ralphex-fe uses curl (bun install script) + wget (Hugo binary download). + - DL3047 # avoid using wget; use curl + - DL4001 # either use wget or curl, not both + + # pipefail SHELL not required — only one pipe exists (bun install) + # and curl -f already fails on HTTP errors. + - DL4006 # set SHELL option -o pipefail before RUN with pipe + + # SNIPPET variable in devcontainer-claude-bun follows the official + # Anthropic Claude Code devcontainer pattern (anthropics/claude-code). + - SC2034 # variable appears unused + + # Pipes inside command substitutions (e.g. grep | cut) are intentional; + # empty results are explicitly checked afterwards. + - SC2312 # pipe in command substitution masks return value