# yaml-language-server: $schema=https://json.schemastore.org/github-action.json # permissions: # - packages: write # Required for publishing to Docker registries # - contents: read # Required for checking out repository # # Security Considerations: # # Trust Model: This action should only be used in trusted workflows controlled by repository owners. # Do not pass untrusted user input (e.g., PR labels, comments, external webhooks) to the `context` # or `dockerfile` parameters. # # Input Validation: The action validates `context` and `dockerfile` inputs to prevent code injection attacks: # # - `context`: Must be a relative path (e.g., `.`, `./app`, `subdir/`). Absolute paths are rejected. # Remote URLs trigger a warning and should only be used from trusted sources. # - `dockerfile`: Must be a relative path (e.g., `Dockerfile`, `./docker/Dockerfile`). Absolute paths # and URLs are rejected. # # These validations help prevent malicious actors from: # - Building Docker images from arbitrary file system locations # - Fetching malicious Dockerfiles from untrusted remote sources # - Executing code injection attacks through build context manipulation # # Best Practices: # 1. Only use hard-coded values or trusted workflow variables for `context` and `dockerfile` # 2. Never accept these values from PR comments, labels, or external webhooks # 3. Review workflow permissions before granting write access to this action # 4. Use SHA-pinned action references: `ivuorinen/actions/docker-publish@` --- name: Docker Publish description: Simple wrapper to publish Docker images to GitHub Packages and/or Docker Hub author: Ismo Vuorinen branding: icon: upload-cloud color: blue inputs: registry: description: 'Registry to publish to (dockerhub, github, or both)' required: false default: 'both' image-name: description: 'Docker image name (defaults to repository name)' required: false tags: description: 'Comma-separated list of tags (e.g., latest,v1.0.0)' required: false default: 'latest' platforms: description: 'Platforms to build for (comma-separated)' required: false default: 'linux/amd64,linux/arm64' context: description: 'Build context path' required: false default: '.' dockerfile: description: 'Path to Dockerfile' required: false default: 'Dockerfile' build-args: description: 'Build arguments (newline-separated KEY=VALUE pairs)' required: false push: description: 'Whether to push the image' required: false default: 'true' token: description: 'GitHub token for authentication (for GitHub registry)' required: false default: '' dockerhub-username: description: 'Docker Hub username (required if publishing to Docker Hub)' required: false dockerhub-token: description: 'Docker Hub token (required if publishing to Docker Hub)' required: false outputs: image-name: description: 'Full image name with registry' value: ${{ steps.meta.outputs.image-name }} tags: description: 'Tags that were published' value: ${{ steps.meta.outputs.tags }} digest: description: 'Image digest' value: ${{ steps.build.outputs.digest }} metadata: description: 'Build metadata' value: ${{ steps.build.outputs.metadata }} runs: using: composite steps: - name: Validate Inputs id: validate shell: sh env: INPUT_REGISTRY: ${{ inputs.registry }} INPUT_DOCKERHUB_USERNAME: ${{ inputs.dockerhub-username }} INPUT_DOCKERHUB_TOKEN: ${{ inputs.dockerhub-token }} INPUT_TOKEN: ${{ inputs.token }} INPUT_CONTEXT: ${{ inputs.context }} INPUT_DOCKERFILE: ${{ inputs.dockerfile }} run: | set -eu # Validate registry input case "$INPUT_REGISTRY" in dockerhub|github|both) ;; *) printf '%s\n' "::error::Invalid registry value. Must be 'dockerhub', 'github', or 'both'" exit 1 ;; esac # Validate Docker Hub credentials if needed if [ "$INPUT_REGISTRY" = "dockerhub" ] || [ "$INPUT_REGISTRY" = "both" ]; then if [ -z "$INPUT_DOCKERHUB_USERNAME" ] || [ -z "$INPUT_DOCKERHUB_TOKEN" ]; then printf '%s\n' "::error::Docker Hub username and token are required when publishing to Docker Hub" exit 1 fi fi # Validate GitHub token if needed if [ "$INPUT_REGISTRY" = "github" ] || [ "$INPUT_REGISTRY" = "both" ]; then token="${INPUT_TOKEN:-${GITHUB_TOKEN:-}}" if [ -z "$token" ]; then printf '%s\n' "::error::GitHub token is required when publishing to GitHub Packages" exit 1 fi fi # Validate context input for security INPUT_CONTEXT="${INPUT_CONTEXT:-.}" case "$INPUT_CONTEXT" in .|./*|*/*) # Relative paths are allowed # Check for path traversal attempts case "$INPUT_CONTEXT" in */../*|../*|*/..) printf '%s\n' "::error::Context path contains path traversal: '$INPUT_CONTEXT'" exit 1 ;; esac ;; /*) printf '%s\n' "::error::Context cannot be an absolute path: '$INPUT_CONTEXT'" printf '%s\n' "::error::Use relative paths (e.g., '.', './app')" exit 1 ;; git://*|git@*|https://*.git|https://github.com/*|https://gitlab.com/*) # Allow trusted git repository URLs printf '%s\n' "::notice::Using git repository URL for context" ;; http://*|https://*) printf '%s\n' "::error::Context cannot be an arbitrary HTTP URL: '$INPUT_CONTEXT'" printf '%s\n' "::error::Only git repository URLs are allowed for remote contexts" exit 1 ;; *) printf '%s\n' "::error::Invalid context format: '$INPUT_CONTEXT'" printf '%s\n' "::error::Must be a relative path or git repository URL" exit 1 ;; esac # Validate dockerfile input for security INPUT_DOCKERFILE="${INPUT_DOCKERFILE:-Dockerfile}" case "$INPUT_DOCKERFILE" in Dockerfile|*/Dockerfile|*.dockerfile|*/*.dockerfile) # Common dockerfile patterns are allowed # Check for path traversal attempts case "$INPUT_DOCKERFILE" in */../*|../*|*/..) printf '%s\n' "::error::Dockerfile path contains path traversal: '$INPUT_DOCKERFILE'" exit 1 ;; esac ;; /*) printf '%s\n' "::error::Dockerfile path cannot be absolute: '$INPUT_DOCKERFILE'" printf '%s\n' "::error::Use relative paths (e.g., 'Dockerfile', './docker/Dockerfile')" exit 1 ;; *://*) printf '%s\n' "::error::Dockerfile path cannot be a URL: '$INPUT_DOCKERFILE'" exit 1 ;; *) printf '%s\n' "::error::Invalid Dockerfile format: '$INPUT_DOCKERFILE'" printf '%s\n' "::error::Must be 'Dockerfile', '*/Dockerfile', '*.dockerfile', or '*/*.dockerfile'" exit 1 ;; esac printf '%s\n' "Input validation completed successfully" - name: Set up Docker Buildx uses: docker/setup-buildx-action@e468171a9de216ec08956ac3ada2f0791b6bd435 # v3.11.1 - name: Determine Image Names and Tags id: meta shell: sh env: INPUT_REGISTRY: ${{ inputs.registry }} INPUT_IMAGE_NAME: ${{ inputs.image-name }} INPUT_TAGS: ${{ inputs.tags }} GITHUB_REPOSITORY: ${{ github.repository }} run: | set -eu # Determine base image name if [ -n "$INPUT_IMAGE_NAME" ]; then base_name="$INPUT_IMAGE_NAME" else # Use repository name (lowercase) base_name=$(echo "$GITHUB_REPOSITORY" | tr '[:upper:]' '[:lower:]') fi # Build full image names based on registry image_names="" case "$INPUT_REGISTRY" in dockerhub) image_names="docker.io/${base_name}" ;; github) image_names="ghcr.io/${base_name}" ;; both) image_names="docker.io/${base_name},ghcr.io/${base_name}" ;; esac # Build full tags (image:tag format) tags="" IFS=',' for image in $image_names; do for tag in $INPUT_TAGS; do tag=$(echo "$tag" | sed 's/^[[:space:]]*//;s/[[:space:]]*$//') if [ -n "$tags" ]; then tags="$(printf '%s\n%s' "$tags" "${image}:${tag}")" else tags="${image}:${tag}" fi done done # Output results printf 'image-name=%s\n' "$base_name" >> "$GITHUB_OUTPUT" { printf '%s\n' 'tags<> "$GITHUB_OUTPUT" printf 'Image name: %s\n' "$base_name" printf '%s\n' "Tags:" printf '%s\n' "$tags" - name: Login to Docker Hub if: inputs.registry == 'dockerhub' || inputs.registry == 'both' uses: docker/login-action@5e57cd118135c172c3672efd75eb46360885c0ef # v3.6.0 with: username: ${{ inputs.dockerhub-username }} password: ${{ inputs.dockerhub-token }} - name: Login to GitHub Container Registry if: inputs.registry == 'github' || inputs.registry == 'both' uses: docker/login-action@5e57cd118135c172c3672efd75eb46360885c0ef # v3.6.0 with: registry: ghcr.io username: ${{ github.actor }} password: ${{ inputs.token || github.token }} - name: Build and Push Docker Image id: build uses: docker/build-push-action@263435318d21b8e681c14492fe198d362a7d2c83 # v6.18.0 with: context: ${{ inputs.context }} file: ${{ inputs.dockerfile }} platforms: ${{ inputs.platforms }} push: ${{ inputs.push }} tags: ${{ steps.meta.outputs.tags }} build-args: ${{ inputs.build-args }} cache-from: type=gha cache-to: type=gha,mode=max