From 3ba6dd9ada73a97144d7f31094e383d7b55ae07d Mon Sep 17 00:00:00 2001 From: Matti Nannt Date: Wed, 17 Sep 2025 11:53:15 +0200 Subject: [PATCH] chore(backport): updated release workflow (#6557) --- .../actions/build-and-push-docker/action.yml | 312 ++++++++++++++++++ .github/actions/docker-build-setup/action.yml | 106 ++++++ .../actions/resolve-docker-version/action.yml | 192 +++++++++++ .../actions/update-package-version/action.yml | 160 +++++++++ .github/workflows/build-and-push-ecr.yml | 176 +++------- .github/workflows/deploy-formbricks-cloud.yml | 2 +- .github/workflows/formbricks-release.yml | 67 +++- .github/workflows/move-stable-tag.yml | 96 ++++++ .../release-docker-github-experimental.yml | 157 ++------- .github/workflows/release-docker-github.yml | 130 +++----- .github/workflows/release-helm-chart.yml | 29 +- 11 files changed, 1061 insertions(+), 366 deletions(-) create mode 100644 .github/actions/build-and-push-docker/action.yml create mode 100644 .github/actions/docker-build-setup/action.yml create mode 100644 .github/actions/resolve-docker-version/action.yml create mode 100644 .github/actions/update-package-version/action.yml create mode 100644 .github/workflows/move-stable-tag.yml diff --git a/.github/actions/build-and-push-docker/action.yml b/.github/actions/build-and-push-docker/action.yml new file mode 100644 index 0000000000..547a1b894e --- /dev/null +++ b/.github/actions/build-and-push-docker/action.yml @@ -0,0 +1,312 @@ +name: Build and Push Docker Image +description: | + Unified Docker build and push action for both ECR and GHCR registries. + + Supports: + - ECR builds for Formbricks Cloud deployment + - GHCR builds for community self-hosting + - Automatic version resolution and tagging + - Conditional signing and deployment tags + +inputs: + registry_type: + description: "Registry type: 'ecr' or 'ghcr'" + required: true + + # Version input + version: + description: "Explicit version (SemVer only, e.g., 1.2.3). If provided, this version is used directly. If empty, version is auto-generated from branch name." + required: false + experimental_mode: + description: "Enable experimental timestamped versions" + required: false + default: "false" + + # ECR specific inputs + ecr_registry: + description: "ECR registry URL (required for ECR builds)" + required: false + ecr_repository: + description: "ECR repository name (required for ECR builds)" + required: false + ecr_region: + description: "ECR AWS region (required for ECR builds)" + required: false + aws_role_arn: + description: "AWS role ARN for ECR authentication (required for ECR builds)" + required: false + + # GHCR specific inputs + ghcr_image_name: + description: "GHCR image name (required for GHCR builds)" + required: false + + # Deployment options + deploy_production: + description: "Tag image for production deployment" + required: false + default: "false" + deploy_staging: + description: "Tag image for staging deployment" + required: false + default: "false" + is_prerelease: + description: "Whether this is a prerelease (auto-tags for staging/production)" + required: false + default: "false" + + # Build options + dockerfile: + description: "Path to Dockerfile" + required: false + default: "apps/web/Dockerfile" + context: + description: "Build context" + required: false + default: "." + +outputs: + image_tag: + description: "Resolved image tag used for the build" + value: ${{ steps.version.outputs.version }} + registry_tags: + description: "Complete registry tags that were pushed" + value: ${{ steps.build.outputs.tags }} + image_digest: + description: "Image digest from the build" + value: ${{ steps.build.outputs.digest }} + +runs: + using: "composite" + steps: + - name: Validate inputs + shell: bash + env: + REGISTRY_TYPE: ${{ inputs.registry_type }} + ECR_REGISTRY: ${{ inputs.ecr_registry }} + ECR_REPOSITORY: ${{ inputs.ecr_repository }} + ECR_REGION: ${{ inputs.ecr_region }} + AWS_ROLE_ARN: ${{ inputs.aws_role_arn }} + GHCR_IMAGE_NAME: ${{ inputs.ghcr_image_name }} + run: | + set -euo pipefail + + if [[ "$REGISTRY_TYPE" != "ecr" && "$REGISTRY_TYPE" != "ghcr" ]]; then + echo "ERROR: registry_type must be 'ecr' or 'ghcr', got: $REGISTRY_TYPE" + exit 1 + fi + + if [[ "$REGISTRY_TYPE" == "ecr" ]]; then + if [[ -z "$ECR_REGISTRY" || -z "$ECR_REPOSITORY" || -z "$ECR_REGION" || -z "$AWS_ROLE_ARN" ]]; then + echo "ERROR: ECR builds require ecr_registry, ecr_repository, ecr_region, and aws_role_arn" + exit 1 + fi + fi + + if [[ "$REGISTRY_TYPE" == "ghcr" ]]; then + if [[ -z "$GHCR_IMAGE_NAME" ]]; then + echo "ERROR: GHCR builds require ghcr_image_name" + exit 1 + fi + fi + + echo "SUCCESS: Input validation passed for $REGISTRY_TYPE build" + + - name: Resolve Docker version + id: version + uses: ./.github/actions/resolve-docker-version + with: + version: ${{ inputs.version }} + current_branch: ${{ github.ref_name }} + experimental_mode: ${{ inputs.experimental_mode }} + + - name: Update package.json version + uses: ./.github/actions/update-package-version + with: + version: ${{ steps.version.outputs.version }} + + - name: Configure AWS credentials (ECR only) + if: ${{ inputs.registry_type == 'ecr' }} + uses: aws-actions/configure-aws-credentials@7474bc4690e29a8392af63c5b98e7449536d5c3a # v4.2.0 + with: + role-to-assume: ${{ inputs.aws_role_arn }} + aws-region: ${{ inputs.ecr_region }} + + - name: Log in to Amazon ECR (ECR only) + if: ${{ inputs.registry_type == 'ecr' }} + uses: aws-actions/amazon-ecr-login@062b18b96a7aff071d4dc91bc00c4c1a7945b076 # v2.0.1 + + - name: Set up Docker build tools + uses: ./.github/actions/docker-build-setup + with: + registry: ${{ inputs.registry_type == 'ghcr' && 'ghcr.io' || '' }} + setup_cosign: ${{ inputs.registry_type == 'ghcr' && 'true' || 'false' }} + skip_login_on_pr: ${{ inputs.registry_type == 'ghcr' && 'true' || 'false' }} + + - name: Build ECR tag list + if: ${{ inputs.registry_type == 'ecr' }} + id: ecr-tags + shell: bash + env: + IMAGE_TAG: ${{ steps.version.outputs.version }} + ECR_REGISTRY: ${{ inputs.ecr_registry }} + ECR_REPOSITORY: ${{ inputs.ecr_repository }} + DEPLOY_PRODUCTION: ${{ inputs.deploy_production }} + DEPLOY_STAGING: ${{ inputs.deploy_staging }} + IS_PRERELEASE: ${{ inputs.is_prerelease }} + run: | + set -euo pipefail + + # Start with the base image tag + TAGS="${ECR_REGISTRY}/${ECR_REPOSITORY}:${IMAGE_TAG}" + + # Handle automatic tagging based on release type + if [[ "${IS_PRERELEASE}" == "true" ]]; then + TAGS="${TAGS}\n${ECR_REGISTRY}/${ECR_REPOSITORY}:staging" + echo "Adding staging tag for prerelease" + elif [[ "${IS_PRERELEASE}" == "false" ]]; then + TAGS="${TAGS}\n${ECR_REGISTRY}/${ECR_REPOSITORY}:production" + echo "Adding production tag for stable release" + fi + + # Handle manual deployment overrides + if [[ "${DEPLOY_PRODUCTION}" == "true" ]]; then + TAGS="${TAGS}\n${ECR_REGISTRY}/${ECR_REPOSITORY}:production" + echo "Adding production tag (manual override)" + fi + if [[ "${DEPLOY_STAGING}" == "true" ]]; then + TAGS="${TAGS}\n${ECR_REGISTRY}/${ECR_REPOSITORY}:staging" + echo "Adding staging tag (manual override)" + fi + + echo "ECR tags generated:" + echo -e "${TAGS}" + + { + echo "tags<> "${GITHUB_OUTPUT}" + + - name: Generate additional GHCR tags for releases + if: ${{ inputs.registry_type == 'ghcr' && inputs.experimental_mode == 'false' && (github.event_name == 'workflow_call' || github.event_name == 'release' || github.event_name == 'workflow_dispatch') }} + id: ghcr-extra-tags + shell: bash + env: + VERSION: ${{ steps.version.outputs.version }} + IMAGE_NAME: ${{ inputs.ghcr_image_name }} + IS_PRERELEASE: ${{ inputs.is_prerelease }} + run: | + set -euo pipefail + + # Start with base version tag + TAGS="ghcr.io/${IMAGE_NAME}:${VERSION}" + + # For proper SemVer releases, add major.minor and major tags + if [[ "${VERSION}" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]; then + # Extract major and minor versions + MAJOR=$(echo "${VERSION}" | cut -d. -f1) + MINOR=$(echo "${VERSION}" | cut -d. -f2) + + TAGS="${TAGS}\nghcr.io/${IMAGE_NAME}:${MAJOR}.${MINOR}" + TAGS="${TAGS}\nghcr.io/${IMAGE_NAME}:${MAJOR}" + + echo "Added SemVer tags: ${MAJOR}.${MINOR}, ${MAJOR}" + fi + + # Add latest tag for stable releases + if [[ "${IS_PRERELEASE}" == "false" ]]; then + TAGS="${TAGS}\nghcr.io/${IMAGE_NAME}:latest" + echo "Added latest tag for stable release" + fi + + echo "Generated GHCR tags:" + echo -e "${TAGS}" + + # Debug: Show what will be passed to Docker build + echo "DEBUG: Tags for Docker build step:" + echo -e "${TAGS}" + + { + echo "tags<> "${GITHUB_OUTPUT}" + + - name: Build GHCR metadata (experimental) + if: ${{ inputs.registry_type == 'ghcr' && inputs.experimental_mode == 'true' }} + id: ghcr-meta-experimental + uses: docker/metadata-action@902fa8ec7d6ecbf8d84d538b9b233a880e428804 # v5.7.0 + with: + images: ghcr.io/${{ inputs.ghcr_image_name }} + tags: | + type=ref,event=branch + type=raw,value=${{ steps.version.outputs.version }} + + - name: Debug Docker build tags + shell: bash + run: | + echo "=== DEBUG: Docker Build Configuration ===" + echo "Registry Type: ${{ inputs.registry_type }}" + echo "Experimental Mode: ${{ inputs.experimental_mode }}" + echo "Event Name: ${{ github.event_name }}" + echo "Is Prerelease: ${{ inputs.is_prerelease }}" + echo "Version: ${{ steps.version.outputs.version }}" + + if [[ "${{ inputs.registry_type }}" == "ecr" ]]; then + echo "ECR Tags: ${{ steps.ecr-tags.outputs.tags }}" + elif [[ "${{ inputs.experimental_mode }}" == "true" ]]; then + echo "GHCR Experimental Tags: ${{ steps.ghcr-meta-experimental.outputs.tags }}" + else + echo "GHCR Extra Tags: ${{ steps.ghcr-extra-tags.outputs.tags }}" + fi + + - name: Build and push Docker image + id: build + uses: depot/build-push-action@636daae76684e38c301daa0c5eca1c095b24e780 # v1.14.0 + with: + project: tw0fqmsx3c + token: ${{ env.DEPOT_PROJECT_TOKEN }} + context: ${{ inputs.context }} + file: ${{ inputs.dockerfile }} + platforms: linux/amd64,linux/arm64 + push: ${{ github.event_name != 'pull_request' }} + tags: ${{ inputs.registry_type == 'ecr' && steps.ecr-tags.outputs.tags || (inputs.registry_type == 'ghcr' && inputs.experimental_mode == 'true' && steps.ghcr-meta-experimental.outputs.tags) || (inputs.registry_type == 'ghcr' && inputs.experimental_mode == 'false' && steps.ghcr-extra-tags.outputs.tags) || (inputs.registry_type == 'ghcr' && format('ghcr.io/{0}:{1}', inputs.ghcr_image_name, steps.version.outputs.version)) || (inputs.registry_type == 'ecr' && format('{0}/{1}:{2}', inputs.ecr_registry, inputs.ecr_repository, steps.version.outputs.version)) }} + labels: ${{ inputs.registry_type == 'ghcr' && inputs.experimental_mode == 'true' && steps.ghcr-meta-experimental.outputs.labels || '' }} + secrets: | + database_url=${{ env.DUMMY_DATABASE_URL }} + encryption_key=${{ env.DUMMY_ENCRYPTION_KEY }} + redis_url=${{ env.DUMMY_REDIS_URL }} + sentry_auth_token=${{ env.SENTRY_AUTH_TOKEN }} + env: + DEPOT_PROJECT_TOKEN: ${{ env.DEPOT_PROJECT_TOKEN }} + DUMMY_DATABASE_URL: ${{ env.DUMMY_DATABASE_URL }} + DUMMY_ENCRYPTION_KEY: ${{ env.DUMMY_ENCRYPTION_KEY }} + DUMMY_REDIS_URL: ${{ env.DUMMY_REDIS_URL }} + SENTRY_AUTH_TOKEN: ${{ env.SENTRY_AUTH_TOKEN }} + + - name: Sign GHCR image (GHCR only) + if: ${{ inputs.registry_type == 'ghcr' && (github.event_name == 'workflow_call' || github.event_name == 'release' || github.event_name == 'workflow_dispatch') }} + shell: bash + env: + TAGS: ${{ inputs.experimental_mode == 'true' && steps.ghcr-meta-experimental.outputs.tags || steps.ghcr-extra-tags.outputs.tags }} + DIGEST: ${{ steps.build.outputs.digest }} + run: | + set -euo pipefail + echo "${TAGS}" | xargs -I {} cosign sign --yes "{}@${DIGEST}" + + - name: Output build summary + shell: bash + env: + REGISTRY_TYPE: ${{ inputs.registry_type }} + IMAGE_TAG: ${{ steps.version.outputs.version }} + VERSION_SOURCE: ${{ steps.version.outputs.source }} + run: | + echo "SUCCESS: Built and pushed Docker image to $REGISTRY_TYPE" + echo "Image Tag: $IMAGE_TAG (source: $VERSION_SOURCE)" + if [[ "$REGISTRY_TYPE" == "ecr" ]]; then + echo "ECR Registry: ${{ inputs.ecr_registry }}" + echo "ECR Repository: ${{ inputs.ecr_repository }}" + else + echo "GHCR Image: ghcr.io/${{ inputs.ghcr_image_name }}" + fi diff --git a/.github/actions/docker-build-setup/action.yml b/.github/actions/docker-build-setup/action.yml new file mode 100644 index 0000000000..19151566e8 --- /dev/null +++ b/.github/actions/docker-build-setup/action.yml @@ -0,0 +1,106 @@ +name: Docker Build Setup +description: | + Sets up common Docker build tools and authentication with security validation. + + Security Features: + - Registry URL validation + - Input sanitization + - Conditional setup based on event type + - Post-setup verification + + Supports Depot CLI, Cosign signing, and Docker registry authentication. + +inputs: + registry: + description: "Docker registry hostname to login to (e.g., ghcr.io, registry.example.com:5000). No paths allowed." + required: false + default: "ghcr.io" + setup_cosign: + description: "Whether to install cosign for image signing" + required: false + default: "true" + skip_login_on_pr: + description: "Whether to skip registry login on pull requests" + required: false + default: "true" + +runs: + using: "composite" + steps: + - name: Validate inputs + shell: bash + env: + REGISTRY: ${{ inputs.registry }} + SETUP_COSIGN: ${{ inputs.setup_cosign }} + SKIP_LOGIN_ON_PR: ${{ inputs.skip_login_on_pr }} + run: | + set -euo pipefail + + # Security: Validate registry input - must be hostname[:port] only, no paths + # Allow empty registry for cases where login is handled externally (e.g., ECR) + if [[ -n "$REGISTRY" ]]; then + if [[ "$REGISTRY" =~ / ]]; then + echo "ERROR: Invalid registry format: $REGISTRY" + echo "Registry must be host[:port] with no path (e.g., 'ghcr.io' or 'registry.example.com:5000')" + echo "Path components like 'ghcr.io/org' are not allowed as they break docker login" + exit 1 + fi + + # Validate hostname with optional port format + if [[ ! "$REGISTRY" =~ ^[a-zA-Z0-9.-]+(\:[0-9]+)?$ ]]; then + echo "ERROR: Invalid registry hostname format: $REGISTRY" + echo "Registry must be a valid hostname optionally with port (e.g., 'ghcr.io' or 'registry.example.com:5000')" + exit 1 + fi + fi + + # Validate boolean inputs + if [[ "$SETUP_COSIGN" != "true" && "$SETUP_COSIGN" != "false" ]]; then + echo "ERROR: setup_cosign must be 'true' or 'false', got: $SETUP_COSIGN" + exit 1 + fi + + if [[ "$SKIP_LOGIN_ON_PR" != "true" && "$SKIP_LOGIN_ON_PR" != "false" ]]; then + echo "ERROR: skip_login_on_pr must be 'true' or 'false', got: $SKIP_LOGIN_ON_PR" + exit 1 + fi + + echo "SUCCESS: Input validation passed" + + - name: Set up Depot CLI + uses: depot/setup-action@b0b1ea4f69e92ebf5dea3f8713a1b0c37b2126a5 # v1.6.0 + + - name: Install cosign + # Install cosign when requested AND when we might actually sign images + # (i.e., non-PR contexts or when we login on PRs) + if: ${{ inputs.setup_cosign == 'true' && (inputs.skip_login_on_pr == 'false' || github.event_name != 'pull_request') }} + uses: sigstore/cosign-installer@3454372f43399081ed03b604cb2d021dabca52bb # v3.8.2 + + - name: Log into registry + if: ${{ inputs.registry != '' && (inputs.skip_login_on_pr == 'false' || github.event_name != 'pull_request') }} + uses: docker/login-action@74a5d142397b4f367a81961eba4e8cd7edddf772 # v3.4.0 + with: + registry: ${{ inputs.registry }} + username: ${{ github.actor }} + password: ${{ github.token }} + + - name: Verify setup completion + shell: bash + run: | + set -euo pipefail + + # Verify Depot CLI is available + if ! command -v depot >/dev/null 2>&1; then + echo "ERROR: Depot CLI not found in PATH" + exit 1 + fi + + # Verify cosign if it should be installed (same conditions as install step) + if [[ "${{ inputs.setup_cosign }}" == "true" ]] && [[ "${{ inputs.skip_login_on_pr }}" == "false" || "${{ github.event_name }}" != "pull_request" ]]; then + if ! command -v cosign >/dev/null 2>&1; then + echo "ERROR: Cosign not found in PATH despite being requested" + exit 1 + fi + fi + + echo "SUCCESS: Docker build setup completed successfully" diff --git a/.github/actions/resolve-docker-version/action.yml b/.github/actions/resolve-docker-version/action.yml new file mode 100644 index 0000000000..55194c32bc --- /dev/null +++ b/.github/actions/resolve-docker-version/action.yml @@ -0,0 +1,192 @@ +name: Resolve Docker Version +description: | + Resolves and validates Docker-compatible SemVer versions for container builds with comprehensive security. + + Security Features: + - Command injection protection + - Input sanitization and validation + - Docker tag character restrictions + - Length limits and boundary checks + - Safe branch name handling + + Supports multiple modes: release, manual override, branch auto-detection, and experimental timestamped versions. + +inputs: + version: + description: "Explicit version (SemVer only, e.g., 1.2.3-beta). If provided, this version is used directly. If empty, version is auto-generated from branch name." + required: false + current_branch: + description: "Current branch name for auto-detection" + required: true + experimental_mode: + description: "Enable experimental mode with timestamp-based versions" + required: false + default: "false" + +outputs: + version: + description: "Resolved Docker-compatible SemVer version" + value: ${{ steps.resolve.outputs.version }} + source: + description: "Source of version (release|override|branch)" + value: ${{ steps.resolve.outputs.source }} + normalized: + description: "Whether the version was normalized (true/false)" + value: ${{ steps.resolve.outputs.normalized }} + +runs: + using: "composite" + steps: + - name: Resolve and validate Docker version + id: resolve + shell: bash + env: + EXPLICIT_VERSION: ${{ inputs.version }} + CURRENT_BRANCH: ${{ inputs.current_branch }} + EXPERIMENTAL_MODE: ${{ inputs.experimental_mode }} + run: | + set -euo pipefail + + # Function to validate SemVer format (Docker-compatible, no '+' build metadata) + validate_semver() { + local version="$1" + local context="$2" + + if [[ ! "$version" =~ ^[0-9]+\.[0-9]+\.[0-9]+(-[a-zA-Z0-9.-]+)?$ ]]; then + echo "ERROR: Invalid $context format. Must be semver without build metadata (e.g., 1.2.3, 1.2.3-alpha)" + echo "Provided: $version" + echo "Note: Docker tags cannot contain '+' characters. Use prerelease identifiers instead." + exit 1 + fi + } + + # Function to generate branch-based version + generate_branch_version() { + local branch="$1" + local use_timestamp="${2:-true}" + local timestamp + + if [[ "$use_timestamp" == "true" ]]; then + timestamp=$(date +%s) + else + timestamp="" + fi + + # Sanitize branch name for Docker compatibility + local sanitized_branch=$(echo "$branch" | sed 's/[^a-zA-Z0-9.-]/-/g' | sed 's/--*/-/g' | sed 's/^-\|-$//g') + + # Additional safety: truncate if too long (reserve space for prefix and timestamp) + if (( ${#sanitized_branch} > 80 )); then + sanitized_branch="${sanitized_branch:0:80}" + echo "INFO: Branch name truncated for Docker compatibility" >&2 + fi + local version + + # Generate version based on branch name (unified approach) + # All branches get alpha versions with sanitized branch name + if [[ -n "$timestamp" ]]; then + version="0.0.0-alpha-$sanitized_branch-$timestamp" + echo "INFO: Branch '$branch' detected - alpha version: $version" >&2 + else + version="0.0.0-alpha-$sanitized_branch" + echo "INFO: Branch '$branch' detected - alpha version: $version" >&2 + fi + + echo "$version" + } + + + # Input validation and sanitization + if [[ -z "$CURRENT_BRANCH" ]]; then + echo "ERROR: current_branch input is required" + exit 1 + fi + + # Security: Validate inputs to prevent command injection + # Use grep to check for dangerous characters (more reliable than bash regex) + validate_input() { + local input="$1" + local name="$2" + + # Check for dangerous characters using grep + if echo "$input" | grep -q '[;|&`$(){}\\[:space:]]'; then + echo "ERROR: $name contains potentially dangerous characters: $input" + echo "Input should only contain letters, numbers, hyphens, underscores, dots, and forward slashes" + return 1 + fi + + return 0 + } + + # Validate current branch + if ! validate_input "$CURRENT_BRANCH" "Branch name"; then + exit 1 + fi + + # Validate explicit version if provided + if [[ -n "$EXPLICIT_VERSION" ]] && ! validate_input "$EXPLICIT_VERSION" "Explicit version"; then + exit 1 + fi + + # Main resolution logic (ultra-simplified) + NORMALIZED="false" + + if [[ -n "$EXPLICIT_VERSION" ]]; then + # Use provided explicit version (from either workflow_call or manual input) + validate_semver "$EXPLICIT_VERSION" "explicit version" + + # Normalize to lowercase for Docker/ECR compatibility + RESOLVED_VERSION="${EXPLICIT_VERSION,,}" + if [[ "$EXPLICIT_VERSION" != "$RESOLVED_VERSION" ]]; then + NORMALIZED="true" + echo "INFO: Original version contained uppercase characters, normalized: $EXPLICIT_VERSION -> $RESOLVED_VERSION" + fi + + SOURCE="explicit" + echo "INFO: Using explicit version: $RESOLVED_VERSION" + + else + # Auto-generate version from branch name + if [[ "$EXPERIMENTAL_MODE" == "true" ]]; then + # Use timestamped version generation + echo "INFO: Experimental mode: generating timestamped version from branch: $CURRENT_BRANCH" + RESOLVED_VERSION=$(generate_branch_version "$CURRENT_BRANCH" "true") + SOURCE="experimental" + else + # Standard branch version (no timestamp) + echo "INFO: Auto-detecting version from branch: $CURRENT_BRANCH" + RESOLVED_VERSION=$(generate_branch_version "$CURRENT_BRANCH" "false") + SOURCE="branch" + fi + echo "Generated version: $RESOLVED_VERSION" + fi + + # Final validation - ensure result is valid Docker tag + if [[ -z "$RESOLVED_VERSION" ]]; then + echo "ERROR: Failed to resolve version" + exit 1 + fi + + if (( ${#RESOLVED_VERSION} > 128 )); then + echo "ERROR: Version must be at most 128 characters (Docker limitation)" + echo "Generated version: $RESOLVED_VERSION (${#RESOLVED_VERSION} chars)" + exit 1 + fi + + if [[ ! "$RESOLVED_VERSION" =~ ^[a-z0-9._-]+$ ]]; then + echo "ERROR: Version contains invalid characters for Docker tags" + echo "Version: $RESOLVED_VERSION" + exit 1 + fi + + if [[ "$RESOLVED_VERSION" =~ ^[.-] || "$RESOLVED_VERSION" =~ [.-]$ ]]; then + echo "ERROR: Version must not start or end with '.' or '-'" + echo "Version: $RESOLVED_VERSION" + exit 1 + fi + + # Output results + echo "SUCCESS: Resolved Docker version: $RESOLVED_VERSION (source: $SOURCE)" + echo "version=$RESOLVED_VERSION" >> $GITHUB_OUTPUT + echo "source=$SOURCE" >> $GITHUB_OUTPUT + echo "normalized=$NORMALIZED" >> $GITHUB_OUTPUT diff --git a/.github/actions/update-package-version/action.yml b/.github/actions/update-package-version/action.yml new file mode 100644 index 0000000000..8a6d903c84 --- /dev/null +++ b/.github/actions/update-package-version/action.yml @@ -0,0 +1,160 @@ +name: Update Package Version +description: | + Safely updates package.json version with comprehensive validation and atomic operations. + + Security Features: + - Path traversal protection + - SemVer validation with length limits + - Atomic file operations with backup/recovery + - JSON validation before applying changes + + This action is designed to be secure by default and prevent common attack vectors. + +inputs: + version: + description: "Version to set in package.json (must be valid SemVer)" + required: true + package_path: + description: "Path to package.json file" + required: false + default: "./apps/web/package.json" + +outputs: + updated_version: + description: "The version that was actually set in package.json" + value: ${{ steps.update.outputs.updated_version }} + +runs: + using: "composite" + steps: + - name: Update and verify package.json version + id: update + shell: bash + env: + VERSION: ${{ inputs.version }} + PACKAGE_PATH: ${{ inputs.package_path }} + run: | + set -euo pipefail + + # Validate inputs + if [[ -z "$VERSION" ]]; then + echo "ERROR: version input is required" + exit 1 + fi + + # Security: Validate package_path to prevent path traversal attacks + # Only allow paths within the workspace and must end with package.json + if [[ "$PACKAGE_PATH" =~ \.\./|^/|^~ ]]; then + echo "ERROR: Invalid package path - path traversal detected: $PACKAGE_PATH" + echo "Package path must be relative to workspace root and cannot contain '../', start with '/', or '~'" + exit 1 + fi + + if [[ ! "$PACKAGE_PATH" =~ package\.json$ ]]; then + echo "ERROR: Package path must end with 'package.json': $PACKAGE_PATH" + exit 1 + fi + + # Resolve to absolute path within workspace for additional security + WORKSPACE_ROOT="${GITHUB_WORKSPACE:-$(pwd)}" + + # Use realpath to resolve both paths and handle symlinks properly + WORKSPACE_ROOT=$(realpath "$WORKSPACE_ROOT") + RESOLVED_PATH=$(realpath "${WORKSPACE_ROOT}/${PACKAGE_PATH}") + + # Ensure WORKSPACE_ROOT has a trailing slash for proper prefix matching + WORKSPACE_ROOT="${WORKSPACE_ROOT}/" + + # Use shell string matching to ensure RESOLVED_PATH is within workspace + # This is more secure than regex and handles edge cases properly + if [[ "$RESOLVED_PATH" != "$WORKSPACE_ROOT"* ]]; then + echo "ERROR: Resolved path is outside workspace: $RESOLVED_PATH" + echo "Workspace root: $WORKSPACE_ROOT" + exit 1 + fi + + if [[ ! -f "$RESOLVED_PATH" ]]; then + echo "ERROR: package.json not found at: $RESOLVED_PATH" + exit 1 + fi + + # Use resolved path for operations + PACKAGE_PATH="$RESOLVED_PATH" + + # Validate SemVer format with additional security checks + if [[ ${#VERSION} -gt 128 ]]; then + echo "ERROR: Version string too long (${#VERSION} chars, max 128): $VERSION" + exit 1 + fi + + if [[ ! "$VERSION" =~ ^[0-9]+\.[0-9]+\.[0-9]+(-[a-zA-Z0-9.-]+)?$ ]]; then + echo "ERROR: Invalid SemVer format: $VERSION" + echo "Expected format: MAJOR.MINOR.PATCH[-PRERELEASE]" + echo "Only alphanumeric characters, dots, and hyphens allowed in prerelease" + exit 1 + fi + + # Additional validation: Check for reasonable version component sizes + # Extract base version (MAJOR.MINOR.PATCH) without prerelease/build metadata + if [[ "$VERSION" =~ ^([0-9]+\.[0-9]+\.[0-9]+) ]]; then + BASE_VERSION="${BASH_REMATCH[1]}" + else + echo "ERROR: Could not extract base version from: $VERSION" + exit 1 + fi + + # Split version components safely + IFS='.' read -ra VERSION_PARTS <<< "$BASE_VERSION" + + # Validate component sizes (should have exactly 3 parts due to regex above) + if (( ${VERSION_PARTS[0]} > 999 || ${VERSION_PARTS[1]} > 999 || ${VERSION_PARTS[2]} > 999 )); then + echo "ERROR: Version components too large (max 999 each): $VERSION" + echo "Components: ${VERSION_PARTS[0]}.${VERSION_PARTS[1]}.${VERSION_PARTS[2]}" + exit 1 + fi + + echo "Updating package.json version to: $VERSION" + + # Create backup for atomic operations + BACKUP_PATH="${PACKAGE_PATH}.backup.$$" + cp "$PACKAGE_PATH" "$BACKUP_PATH" + + # Use jq to safely update the version field with error handling + if ! jq --arg version "$VERSION" '.version = $version' "$PACKAGE_PATH" > "${PACKAGE_PATH}.tmp"; then + echo "ERROR: jq failed to process package.json" + rm -f "${PACKAGE_PATH}.tmp" "$BACKUP_PATH" + exit 1 + fi + + # Validate the generated JSON before applying changes + if ! jq empty "${PACKAGE_PATH}.tmp" 2>/dev/null; then + echo "ERROR: Generated invalid JSON" + rm -f "${PACKAGE_PATH}.tmp" "$BACKUP_PATH" + exit 1 + fi + + # Atomic move operation + if ! mv "${PACKAGE_PATH}.tmp" "$PACKAGE_PATH"; then + echo "ERROR: Failed to update package.json" + # Restore backup + mv "$BACKUP_PATH" "$PACKAGE_PATH" + exit 1 + fi + + # Verify the update was successful + UPDATED_VERSION=$(jq -r '.version' "$PACKAGE_PATH" 2>/dev/null) + + if [[ "$UPDATED_VERSION" != "$VERSION" ]]; then + echo "ERROR: Version update failed!" + echo "Expected: $VERSION" + echo "Actual: $UPDATED_VERSION" + # Restore backup + mv "$BACKUP_PATH" "$PACKAGE_PATH" + exit 1 + fi + + # Clean up backup on success + rm -f "$BACKUP_PATH" + + echo "SUCCESS: Updated package.json version to: $UPDATED_VERSION" + echo "updated_version=$UPDATED_VERSION" >> $GITHUB_OUTPUT diff --git a/.github/workflows/build-and-push-ecr.yml b/.github/workflows/build-and-push-ecr.yml index 3f52e620d4..b0cb7f0255 100644 --- a/.github/workflows/build-and-push-ecr.yml +++ b/.github/workflows/build-and-push-ecr.yml @@ -1,12 +1,16 @@ -name: Build & Push Docker to ECR +name: Build Cloud Deployment Images + +# This workflow builds Formbricks Docker images for ECR deployment: +# - workflow_call: Used by releases with explicit SemVer versions +# - workflow_dispatch: Auto-detects version from current branch or uses override on: workflow_dispatch: inputs: - image_tag: - description: "Image tag to push (e.g., v3.16.1, main)" - required: true - default: "v3.16.1" + version_override: + description: "Override version (SemVer only, e.g., 1.2.3). Leave empty to auto-detect from branch." + required: false + type: string deploy_production: description: "Tag image for production deployment" required: false @@ -17,6 +21,24 @@ on: required: false default: false type: boolean + workflow_call: + inputs: + image_tag: + description: "Image tag to push (required for workflow_call)" + required: true + type: string + IS_PRERELEASE: + description: "Whether this is a prerelease (auto-tags for staging/production)" + required: false + type: boolean + default: false + outputs: + IMAGE_TAG: + description: "Normalized image tag used for the build" + value: ${{ jobs.build-and-push.outputs.IMAGE_TAG }} + TAGS: + description: "Newline-separated list of ECR tags pushed" + value: ${{ jobs.build-and-push.outputs.TAGS }} permissions: contents: read @@ -27,14 +49,15 @@ env: # ECR settings are sourced from repository/environment variables for portability across envs/forks ECR_REGISTRY: ${{ vars.ECR_REGISTRY }} ECR_REPOSITORY: ${{ vars.ECR_REPOSITORY }} - DOCKERFILE: apps/web/Dockerfile - CONTEXT: . jobs: build-and-push: name: Build and Push runs-on: ubuntu-latest timeout-minutes: 45 + outputs: + IMAGE_TAG: ${{ steps.build.outputs.image_tag }} + TAGS: ${{ steps.build.outputs.registry_tags }} steps: - name: Harden the runner (Audit all outbound calls) uses: step-security/harden-runner@ec9f2d5744a09debf3a187a3f4f675c53b671911 # v2.13.0 @@ -44,125 +67,22 @@ jobs: - name: Checkout repository uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - - name: Validate image tag input - shell: bash - env: - IMAGE_TAG: ${{ inputs.image_tag }} - run: | - set -euo pipefail - if [[ -z "${IMAGE_TAG}" ]]; then - echo "❌ Image tag is required (non-empty)." - exit 1 - fi - if (( ${#IMAGE_TAG} > 128 )); then - echo "❌ Image tag must be at most 128 characters." - exit 1 - fi - if [[ ! "${IMAGE_TAG}" =~ ^[a-z0-9._-]+$ ]]; then - echo "❌ Image tag may only contain lowercase letters, digits, '.', '_' and '-'." - exit 1 - fi - if [[ "${IMAGE_TAG}" =~ ^[.-] || "${IMAGE_TAG}" =~ [.-]$ ]]; then - echo "❌ Image tag must not start or end with '.' or '-'." - exit 1 - fi - - - name: Validate required variables - shell: bash - env: - ECR_REGISTRY: ${{ env.ECR_REGISTRY }} - ECR_REPOSITORY: ${{ env.ECR_REPOSITORY }} - ECR_REGION: ${{ env.ECR_REGION }} - run: | - set -euo pipefail - if [[ -z "${ECR_REGISTRY}" || -z "${ECR_REPOSITORY}" || -z "${ECR_REGION}" ]]; then - echo "ECR_REGION, ECR_REGISTRY and ECR_REPOSITORY must be set via repository or environment variables (Settings → Variables)." - exit 1 - fi - - - name: Update package.json version - shell: bash - env: - IMAGE_TAG: ${{ inputs.image_tag }} - run: | - set -euo pipefail - - # Remove 'v' prefix if present (e.g., v3.16.1 -> 3.16.1) - VERSION="${IMAGE_TAG#v}" - - # Validate SemVer format (major.minor.patch with optional prerelease and build metadata) - if [[ ! "${VERSION}" =~ ^[0-9]+\.[0-9]+\.[0-9]+(-[a-zA-Z0-9.-]+)?(\+[a-zA-Z0-9.-]+)?$ ]]; then - echo "❌ Error: Invalid version format after extraction. Must be SemVer (e.g., 1.2.3, 1.2.3-alpha, 1.2.3+build.1)" - echo "Original input: ${IMAGE_TAG}" - echo "Extracted version: ${VERSION}" - echo "Expected format: MAJOR.MINOR.PATCH[-PRERELEASE][+BUILDMETADATA]" - exit 1 - fi - - echo "✅ Valid SemVer format detected: ${VERSION}" - echo "Updating package.json version to: ${VERSION}" - sed -i "s/\"version\": \"0.0.0\"/\"version\": \"${VERSION}\"/" ./apps/web/package.json - cat ./apps/web/package.json | grep version - - - name: Build tag list - id: tags - shell: bash - env: - IMAGE_TAG: ${{ inputs.image_tag }} - DEPLOY_PRODUCTION: ${{ inputs.deploy_production }} - DEPLOY_STAGING: ${{ inputs.deploy_staging }} - ECR_REGISTRY: ${{ env.ECR_REGISTRY }} - ECR_REPOSITORY: ${{ env.ECR_REPOSITORY }} - run: | - set -euo pipefail - - # Start with the base image tag - TAGS="${ECR_REGISTRY}/${ECR_REPOSITORY}:${IMAGE_TAG}" - - # Add production tag if requested - if [[ "${DEPLOY_PRODUCTION}" == "true" ]]; then - TAGS="${TAGS}\n${ECR_REGISTRY}/${ECR_REPOSITORY}:production" - fi - - # Add staging tag if requested - if [[ "${DEPLOY_STAGING}" == "true" ]]; then - TAGS="${TAGS}\n${ECR_REGISTRY}/${ECR_REPOSITORY}:staging" - fi - - # Output for debugging - echo "Generated tags:" - echo -e "${TAGS}" - - # Set output for next step (escape newlines for GitHub Actions) - { - echo "tags<> "${GITHUB_OUTPUT}" - - - name: Configure AWS credentials (OIDC) - uses: aws-actions/configure-aws-credentials@7474bc4690e29a8392af63c5b98e7449536d5c3a + - name: Build and push cloud deployment image + id: build + uses: ./.github/actions/build-and-push-docker with: - role-to-assume: ${{ secrets.AWS_ECR_PUSH_ROLE_ARN }} - aws-region: ${{ env.ECR_REGION }} - - - name: Log in to Amazon ECR - uses: aws-actions/amazon-ecr-login@062b18b96a7aff071d4dc91bc00c4c1a7945b076 - - - name: Set up Depot CLI - uses: depot/setup-action@b0b1ea4f69e92ebf5dea3f8713a1b0c37b2126a5 # v1.6.0 - - - name: Build and push image (Depot remote builder) - uses: depot/build-push-action@636daae76684e38c301daa0c5eca1c095b24e780 # v1.14.0 - with: - project: tw0fqmsx3c - token: ${{ secrets.DEPOT_PROJECT_TOKEN }} - context: ${{ env.CONTEXT }} - file: ${{ env.DOCKERFILE }} - platforms: linux/amd64,linux/arm64 - push: true - tags: ${{ steps.tags.outputs.tags }} - secrets: | - database_url=${{ secrets.DUMMY_DATABASE_URL }} - encryption_key=${{ secrets.DUMMY_ENCRYPTION_KEY }} - sentry_auth_token=${{ secrets.SENTRY_AUTH_TOKEN }} + registry_type: "ecr" + ecr_registry: ${{ env.ECR_REGISTRY }} + ecr_repository: ${{ env.ECR_REPOSITORY }} + ecr_region: ${{ env.ECR_REGION }} + aws_role_arn: ${{ secrets.AWS_ECR_PUSH_ROLE_ARN }} + version: ${{ inputs.version_override || inputs.image_tag }} + deploy_production: ${{ inputs.deploy_production }} + deploy_staging: ${{ inputs.deploy_staging }} + is_prerelease: ${{ inputs.IS_PRERELEASE }} + env: + DEPOT_PROJECT_TOKEN: ${{ secrets.DEPOT_PROJECT_TOKEN }} + DUMMY_DATABASE_URL: ${{ secrets.DUMMY_DATABASE_URL }} + DUMMY_ENCRYPTION_KEY: ${{ secrets.DUMMY_ENCRYPTION_KEY }} + DUMMY_REDIS_URL: ${{ secrets.DUMMY_REDIS_URL }} + SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }} diff --git a/.github/workflows/deploy-formbricks-cloud.yml b/.github/workflows/deploy-formbricks-cloud.yml index c3b246ec89..6fa0702936 100644 --- a/.github/workflows/deploy-formbricks-cloud.yml +++ b/.github/workflows/deploy-formbricks-cloud.yml @@ -4,7 +4,7 @@ on: workflow_dispatch: inputs: VERSION: - description: "The version of the Docker image to release, full image tag if image tag is v0.0.0 enter v0.0.0." + description: "The version of the Docker image to release (clean SemVer, e.g., 1.2.3)" required: true type: string REPOSITORY: diff --git a/.github/workflows/formbricks-release.yml b/.github/workflows/formbricks-release.yml index 94bcd23cc4..e7a9462a02 100644 --- a/.github/workflows/formbricks-release.yml +++ b/.github/workflows/formbricks-release.yml @@ -8,8 +8,8 @@ permissions: contents: read jobs: - docker-build: - name: Build & release docker image + docker-build-community: + name: Build & release community docker image permissions: contents: read packages: write @@ -19,6 +19,19 @@ jobs: with: IS_PRERELEASE: ${{ github.event.release.prerelease }} + docker-build-cloud: + name: Build & push Formbricks Cloud to ECR + permissions: + contents: read + id-token: write + uses: ./.github/workflows/build-and-push-ecr.yml + secrets: inherit + with: + image_tag: ${{ needs.docker-build-community.outputs.VERSION }} + IS_PRERELEASE: ${{ github.event.release.prerelease }} + needs: + - docker-build-community + helm-chart-release: name: Release Helm Chart permissions: @@ -27,22 +40,42 @@ jobs: uses: ./.github/workflows/release-helm-chart.yml secrets: inherit needs: - - docker-build + - docker-build-community with: - VERSION: ${{ needs.docker-build.outputs.VERSION }} + VERSION: ${{ needs.docker-build-community.outputs.VERSION }} - deploy-formbricks-cloud: - name: Deploy Helm Chart to Formbricks Cloud - permissions: - contents: read - id-token: write - secrets: inherit - uses: ./.github/workflows/deploy-formbricks-cloud.yml + verify-cloud-build: + name: Verify Cloud Build Outputs + runs-on: ubuntu-latest + timeout-minutes: 5 # Simple verification should be quick needs: - - docker-build - - helm-chart-release + - docker-build-cloud + steps: + - name: Harden the runner + uses: step-security/harden-runner@ec9f2d5744a09debf3a187a3f4f675c53b671911 # v2.13.0 + with: + egress-policy: audit + + - name: Display ECR build outputs + env: + IMAGE_TAG: ${{ needs.docker-build-cloud.outputs.IMAGE_TAG }} + TAGS: ${{ needs.docker-build-cloud.outputs.TAGS }} + run: | + set -euo pipefail + + echo "✅ ECR Build Completed Successfully" + echo "Image Tag: ${IMAGE_TAG}" + echo "ECR Tags:" + printf '%s\n' "${TAGS}" + + move-stable-tag: + name: Move stable tag to release + permissions: + contents: write # Required for tag push operations in called workflow + uses: ./.github/workflows/move-stable-tag.yml + needs: + - docker-build-community # Ensure release is successful first with: - VERSION: v${{ needs.docker-build.outputs.VERSION }} - ENVIRONMENT: ${{ github.event.release.prerelease && 'staging' || 'production' }} - - + release_tag: ${{ github.event.release.tag_name }} + commit_sha: ${{ github.sha }} + is_prerelease: ${{ github.event.release.prerelease }} diff --git a/.github/workflows/move-stable-tag.yml b/.github/workflows/move-stable-tag.yml new file mode 100644 index 0000000000..72ac800760 --- /dev/null +++ b/.github/workflows/move-stable-tag.yml @@ -0,0 +1,96 @@ +name: Move Stable Tag + +on: + workflow_call: + inputs: + release_tag: + description: "The release tag name (e.g., v1.2.3)" + required: true + type: string + commit_sha: + description: "The commit SHA to point the stable tag to" + required: true + type: string + is_prerelease: + description: "Whether this is a prerelease (stable tag won't be moved for prereleases)" + required: false + type: boolean + default: false + +permissions: + contents: read + +# Prevent concurrent stable tag operations to avoid race conditions +concurrency: + group: move-stable-tag-${{ github.repository }} + cancel-in-progress: true + +jobs: + move-stable-tag: + name: Move stable tag to release + runs-on: ubuntu-latest + timeout-minutes: 10 # Prevent hung git operations + permissions: + contents: write # Required to push tags + # Only move stable tag for non-prerelease versions + if: ${{ !inputs.is_prerelease }} + steps: + - name: Harden the runner + uses: step-security/harden-runner@0634a2670c59f64b4a01f0f96f84700a4088b9f0 # v2.12.0 + with: + egress-policy: audit + + - name: Checkout repository + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + with: + fetch-depth: 0 # Full history needed for tag operations + + - name: Validate inputs + env: + RELEASE_TAG: ${{ inputs.release_tag }} + COMMIT_SHA: ${{ inputs.commit_sha }} + run: | + set -euo pipefail + + # Validate release tag format + if [[ ! "$RELEASE_TAG" =~ ^v[0-9]+\.[0-9]+\.[0-9]+(-[a-zA-Z0-9.-]+)?(\+[a-zA-Z0-9.-]+)?$ ]]; then + echo "❌ Error: Invalid release tag format. Expected format: v1.2.3, v1.2.3-alpha" + echo "Provided: $RELEASE_TAG" + exit 1 + fi + + # Validate commit SHA format (40 character hex) + if [[ ! "$COMMIT_SHA" =~ ^[a-f0-9]{40}$ ]]; then + echo "❌ Error: Invalid commit SHA format. Expected 40 character hex string" + echo "Provided: $COMMIT_SHA" + exit 1 + fi + + echo "✅ Input validation passed" + echo "Release tag: $RELEASE_TAG" + echo "Commit SHA: $COMMIT_SHA" + + - name: Move stable tag + env: + RELEASE_TAG: ${{ inputs.release_tag }} + COMMIT_SHA: ${{ inputs.commit_sha }} + run: | + set -euo pipefail + + # Configure git + git config user.name "github-actions[bot]" + git config user.email "github-actions[bot]@users.noreply.github.com" + + # Verify the commit exists + if ! git cat-file -e "$COMMIT_SHA"; then + echo "❌ Error: Commit $COMMIT_SHA does not exist in this repository" + exit 1 + fi + + # Move stable tag to the release commit + echo "📌 Moving stable tag to commit: $COMMIT_SHA (release: $RELEASE_TAG)" + git tag -f stable "$COMMIT_SHA" + git push origin stable --force + + echo "✅ Successfully moved stable tag to release $RELEASE_TAG" + echo "🔗 Stable tag now points to: https://github.com/${{ github.repository }}/commit/$COMMIT_SHA" diff --git a/.github/workflows/release-docker-github-experimental.yml b/.github/workflows/release-docker-github-experimental.yml index 199a7b502b..ac32a4e9c3 100644 --- a/.github/workflows/release-docker-github-experimental.yml +++ b/.github/workflows/release-docker-github-experimental.yml @@ -1,39 +1,31 @@ -name: Docker Release to Github Experimental +name: Build Community Testing Images -# This workflow uses actions that are not certified by GitHub. -# They are provided by a third-party and are governed by -# separate terms of service, privacy policy, and support -# documentation. +# This workflow builds experimental/testing versions of Formbricks for self-hosting customers +# to test fixes and features before official releases. Images are pushed to GHCR with +# timestamped experimental versions for easy identification and testing. on: workflow_dispatch: - -env: - # Use docker.io for Docker Hub if empty - REGISTRY: ghcr.io - # github.repository as / - IMAGE_NAME: ${{ github.repository }}-experimental - TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }} - TURBO_TEAM: ${{ secrets.TURBO_TEAM }} + inputs: + version_override: + description: "Override version (SemVer only, e.g., 1.2.3-beta). Leave empty for auto-generated experimental version." + required: false + type: string permissions: contents: read + packages: write + id-token: write jobs: - build: + build-community-testing: + name: Build Community Testing Image runs-on: ubuntu-latest - permissions: - contents: read - packages: write - # This is used to complete the identity challenge - # with sigstore/fulcio when running outside of PRs. - id-token: write - - + timeout-minutes: 45 steps: - name: Harden the runner (Audit all outbound calls) - uses: step-security/harden-runner@0634a2670c59f64b4a01f0f96f84700a4088b9f0 # v2.12.0 + uses: step-security/harden-runner@ec9f2d5744a09debf3a187a3f4f675c53b671911 # v2.13.0 with: egress-policy: audit @@ -42,110 +34,17 @@ jobs: with: fetch-depth: 0 - - name: Generate SemVer version from branch or tag - id: generate_version + - name: Build and push community testing image + uses: ./.github/actions/build-and-push-docker + with: + registry_type: "ghcr" + ghcr_image_name: "${{ github.repository }}-experimental" + experimental_mode: "true" + version: ${{ inputs.version_override }} env: - REF_NAME: ${{ github.ref_name }} - REF_TYPE: ${{ github.ref_type }} - run: | - # Get reference name and type from environment variables - echo "Reference type: $REF_TYPE" - echo "Reference name: $REF_NAME" - - # Create unique timestamped version for testing sourcemap resolution - TIMESTAMP=$(date +%s) - - if [[ "$REF_TYPE" == "tag" ]]; then - # If running from a tag, use the tag name + timestamp - if [[ "$REF_NAME" =~ ^v?[0-9]+\.[0-9]+\.[0-9]+.*$ ]]; then - # Tag looks like a SemVer, use it directly (remove 'v' prefix if present) - BASE_VERSION=$(echo "$REF_NAME" | sed 's/^v//') - VERSION="${BASE_VERSION}-${TIMESTAMP}" - echo "Using SemVer tag with timestamp: $VERSION" - else - # Tag is not SemVer, treat as prerelease - SANITIZED_TAG=$(echo "$REF_NAME" | sed 's/[^a-zA-Z0-9.-]/-/g' | sed 's/--*/-/g' | sed 's/^-\|-$//g') - VERSION="0.0.0-${SANITIZED_TAG}-${TIMESTAMP}" - echo "Using tag as prerelease with timestamp: $VERSION" - fi - else - # Running from branch, use branch name as prerelease + timestamp - SANITIZED_BRANCH=$(echo "$REF_NAME" | sed 's/[^a-zA-Z0-9.-]/-/g' | sed 's/--*/-/g' | sed 's/^-\|-$//g') - VERSION="0.0.0-${SANITIZED_BRANCH}-${TIMESTAMP}" - echo "Using branch as prerelease with timestamp: $VERSION" - fi - - echo "VERSION=$VERSION" >> $GITHUB_ENV - echo "VERSION=$VERSION" >> $GITHUB_OUTPUT - echo "Generated SemVer version: $VERSION" - - - name: Update package.json version - run: | - sed -i "s/\"version\": \"0.0.0\"/\"version\": \"${{ env.VERSION }}\"/" ./apps/web/package.json - cat ./apps/web/package.json | grep version - - - name: Set up Depot CLI - uses: depot/setup-action@b0b1ea4f69e92ebf5dea3f8713a1b0c37b2126a5 # v1.6.0 - - # Install the cosign tool except on PR - # https://github.com/sigstore/cosign-installer - - name: Install cosign - if: github.event_name != 'pull_request' - uses: sigstore/cosign-installer@3454372f43399081ed03b604cb2d021dabca52bb # v3.8.2 - - # Login against a Docker registry except on PR - # https://github.com/docker/login-action - - name: Log into registry ${{ env.REGISTRY }} - if: github.event_name != 'pull_request' - uses: docker/login-action@74a5d142397b4f367a81961eba4e8cd7edddf772 # v3.4.0 - with: - registry: ${{ env.REGISTRY }} - username: ${{ github.actor }} - password: ${{ secrets.GITHUB_TOKEN }} - - # Extract metadata (tags, labels) for Docker - # https://github.com/docker/metadata-action - - name: Extract Docker metadata - id: meta - uses: docker/metadata-action@902fa8ec7d6ecbf8d84d538b9b233a880e428804 # v5.7.0 - with: - images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} - tags: | - type=ref,event=branch - type=raw,value=${{ env.VERSION }} - - # Build and push Docker image with Buildx (don't push on PR) - # https://github.com/docker/build-push-action - - name: Build and push Docker image - id: build-and-push - uses: depot/build-push-action@636daae76684e38c301daa0c5eca1c095b24e780 # v1.14.0 - with: - project: tw0fqmsx3c - token: ${{ secrets.DEPOT_PROJECT_TOKEN }} - context: . - file: ./apps/web/Dockerfile - platforms: linux/amd64,linux/arm64 - push: ${{ github.event_name != 'pull_request' }} - tags: ${{ steps.meta.outputs.tags }} - labels: ${{ steps.meta.outputs.labels }} - secrets: | - database_url=${{ secrets.DUMMY_DATABASE_URL }} - encryption_key=${{ secrets.DUMMY_ENCRYPTION_KEY }} - sentry_auth_token=${{ secrets.SENTRY_AUTH_TOKEN }} - - - - # Sign the resulting Docker image digest except on PRs. - # This will only write to the public Rekor transparency log when the Docker - # repository is public to avoid leaking data. If you would like to publish - # transparency data even for private images, pass --force to cosign below. - # https://github.com/sigstore/cosign - - name: Sign the published Docker image - if: ${{ github.event_name != 'pull_request' }} - env: - # https://docs.github.com/en/actions/security-guides/security-hardening-for-github-actions#using-an-intermediate-environment-variable - TAGS: ${{ steps.meta.outputs.tags }} - DIGEST: ${{ steps.build-and-push.outputs.digest }} - # This step uses the identity token to provision an ephemeral certificate - # against the sigstore community Fulcio instance. - run: echo "${TAGS}" | xargs -I {} cosign sign --yes "{}@${DIGEST}" \ No newline at end of file + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + DEPOT_PROJECT_TOKEN: ${{ secrets.DEPOT_PROJECT_TOKEN }} + DUMMY_DATABASE_URL: ${{ secrets.DUMMY_DATABASE_URL }} + DUMMY_ENCRYPTION_KEY: ${{ secrets.DUMMY_ENCRYPTION_KEY }} + DUMMY_REDIS_URL: ${{ secrets.DUMMY_REDIS_URL }} + SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }} diff --git a/.github/workflows/release-docker-github.yml b/.github/workflows/release-docker-github.yml index 8f0e9a21aa..184fbc9c03 100644 --- a/.github/workflows/release-docker-github.yml +++ b/.github/workflows/release-docker-github.yml @@ -1,4 +1,4 @@ -name: Docker Release to Github +name: Release Community Docker Images # This workflow uses actions that are not certified by GitHub. # They are provided by a third-party and are governed by @@ -23,8 +23,6 @@ env: REGISTRY: ghcr.io # github.repository as / IMAGE_NAME: ${{ github.repository }} - TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }} - TURBO_TEAM: ${{ secrets.TURBO_TEAM }} permissions: contents: read @@ -32,6 +30,7 @@ permissions: jobs: build: runs-on: ubuntu-latest + timeout-minutes: 45 permissions: contents: read packages: write @@ -44,103 +43,60 @@ jobs: steps: - name: Harden the runner (Audit all outbound calls) - uses: step-security/harden-runner@0634a2670c59f64b4a01f0f96f84700a4088b9f0 # v2.12.0 + uses: step-security/harden-runner@ec9f2d5744a09debf3a187a3f4f675c53b671911 # v2.13.0 with: egress-policy: audit - name: Checkout repository uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - - name: Get Release Tag + - name: Extract release version from tag id: extract_release_tag run: | - # Extract version from tag (e.g., refs/tags/v1.2.3 -> 1.2.3) - TAG="$GITHUB_REF" - TAG=${TAG#refs/tags/v} + set -euo pipefail - # Validate the extracted tag format - if [[ ! "$TAG" =~ ^[0-9]+\.[0-9]+\.[0-9]+(-[a-zA-Z0-9.-]+)?(\+[a-zA-Z0-9.-]+)?$ ]]; then - echo "❌ Error: Invalid release tag format after extraction. Must be semver (e.g., 1.2.3, 1.2.3-alpha)" - echo "Original ref: $GITHUB_REF" - echo "Extracted tag: $TAG" + # Extract tag name with fallback logic for different trigger contexts + if [[ -n "${RELEASE_TAG:-}" ]]; then + TAG="$RELEASE_TAG" + echo "Using RELEASE_TAG override: $TAG" + elif [[ "$GITHUB_REF_NAME" =~ ^[0-9]+\.[0-9]+\.[0-9]+(-[a-zA-Z0-9.-]+)?$ ]] || [[ "$GITHUB_REF_NAME" =~ ^v[0-9] ]]; then + TAG="$GITHUB_REF_NAME" + echo "Using GITHUB_REF_NAME (looks like tag): $TAG" + else + # Fallback: extract from GITHUB_REF for direct tag triggers + TAG="${GITHUB_REF#refs/tags/}" + if [[ -z "$TAG" || "$TAG" == "$GITHUB_REF" ]]; then + TAG="$GITHUB_REF_NAME" + echo "Using GITHUB_REF_NAME as final fallback: $TAG" + else + echo "Extracted from GITHUB_REF: $TAG" + fi + fi + + # Strip v-prefix if present (normalize to clean SemVer) + TAG=${TAG#[vV]} + + # Validate SemVer format (supports prereleases like 4.0.0-rc.1) + if [[ ! "$TAG" =~ ^[0-9]+\.[0-9]+\.[0-9]+(-[a-zA-Z0-9.-]+)?$ ]]; then + echo "ERROR: Invalid tag format '$TAG'. Expected SemVer (e.g., 1.2.3, 4.0.0-rc.1)" exit 1 fi - # Safely add to environment variables - echo "RELEASE_TAG=$TAG" >> $GITHUB_ENV - echo "VERSION=$TAG" >> $GITHUB_OUTPUT - echo "Using tag-based version: $TAG" + echo "Using version: $TAG" - - name: Update package.json version - run: | - sed -i "s/\"version\": \"0.0.0\"/\"version\": \"${{ env.RELEASE_TAG }}\"/" ./apps/web/package.json - cat ./apps/web/package.json | grep version - - - name: Set up Depot CLI - uses: depot/setup-action@b0b1ea4f69e92ebf5dea3f8713a1b0c37b2126a5 # v1.6.0 - - # Install the cosign tool except on PR - # https://github.com/sigstore/cosign-installer - - name: Install cosign - if: github.event_name != 'pull_request' - uses: sigstore/cosign-installer@3454372f43399081ed03b604cb2d021dabca52bb # v3.8.2 - - # Login against a Docker registry except on PR - # https://github.com/docker/login-action - - name: Log into registry ${{ env.REGISTRY }} - if: github.event_name != 'pull_request' - uses: docker/login-action@74a5d142397b4f367a81961eba4e8cd7edddf772 # v3.4.0 + - name: Build and push community release image + id: build + uses: ./.github/actions/build-and-push-docker with: - registry: ${{ env.REGISTRY }} - username: ${{ github.actor }} - password: ${{ secrets.GITHUB_TOKEN }} - - # Extract metadata (tags, labels) for Docker - # https://github.com/docker/metadata-action - - name: Extract Docker metadata - id: meta - uses: docker/metadata-action@902fa8ec7d6ecbf8d84d538b9b233a880e428804 # v5.7.0 - with: - images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} - tags: | - # Default semver tags (version, major.minor, major) - type=semver,pattern={{version}} - type=semver,pattern={{major}}.{{minor}} - type=semver,pattern={{major}} - # Only tag as 'latest' for stable releases (not prereleases) - type=raw,value=latest,enable=${{ !inputs.IS_PRERELEASE }} - - # Build and push Docker image with Buildx (don't push on PR) - # https://github.com/docker/build-push-action - - name: Build and push Docker image - id: build-and-push - uses: depot/build-push-action@636daae76684e38c301daa0c5eca1c095b24e780 # v1.14.0 - with: - project: tw0fqmsx3c - token: ${{ secrets.DEPOT_PROJECT_TOKEN }} - context: . - file: ./apps/web/Dockerfile - platforms: linux/amd64,linux/arm64 - push: ${{ github.event_name != 'pull_request' }} - tags: ${{ steps.meta.outputs.tags }} - labels: ${{ steps.meta.outputs.labels }} - secrets: | - database_url=${{ secrets.DUMMY_DATABASE_URL }} - encryption_key=${{ secrets.DUMMY_ENCRYPTION_KEY }} - sentry_auth_token=${{ secrets.SENTRY_AUTH_TOKEN }} - - # Sign the resulting Docker image digest except on PRs. - # This will only write to the public Rekor transparency log when the Docker - # repository is public to avoid leaking data. If you would like to publish - # transparency data even for private images, pass --force to cosign below. - # https://github.com/sigstore/cosign - - name: Sign the published Docker image - if: ${{ github.event_name != 'pull_request' }} + registry_type: "ghcr" + ghcr_image_name: ${{ env.IMAGE_NAME }} + version: ${{ steps.extract_release_tag.outputs.VERSION }} + is_prerelease: ${{ inputs.IS_PRERELEASE }} env: - # https://docs.github.com/en/actions/security-guides/security-hardening-for-github-actions#using-an-intermediate-environment-variable - TAGS: ${{ steps.meta.outputs.tags }} - DIGEST: ${{ steps.build-and-push.outputs.digest }} - # This step uses the identity token to provision an ephemeral certificate - # against the sigstore community Fulcio instance. - run: echo "${TAGS}" | xargs -I {} cosign sign --yes {}@${DIGEST} + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + DEPOT_PROJECT_TOKEN: ${{ secrets.DEPOT_PROJECT_TOKEN }} + DUMMY_DATABASE_URL: ${{ secrets.DUMMY_DATABASE_URL }} + DUMMY_ENCRYPTION_KEY: ${{ secrets.DUMMY_ENCRYPTION_KEY }} + DUMMY_REDIS_URL: ${{ secrets.DUMMY_REDIS_URL }} + SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }} diff --git a/.github/workflows/release-helm-chart.yml b/.github/workflows/release-helm-chart.yml index 53ecc725a3..eca5a3d714 100644 --- a/.github/workflows/release-helm-chart.yml +++ b/.github/workflows/release-helm-chart.yml @@ -19,7 +19,7 @@ jobs: contents: read steps: - name: Harden the runner (Audit all outbound calls) - uses: step-security/harden-runner@0634a2670c59f64b4a01f0f96f84700a4088b9f0 # v2.12.0 + uses: step-security/harden-runner@ec9f2d5744a09debf3a187a3f4f675c53b671911 # v2.13.0 with: egress-policy: audit @@ -59,14 +59,35 @@ jobs: uses: dcarbone/install-yq-action@4075b4dca348d74bd83f2bf82d30f25d7c54539b # v1.3.1 - name: Update Chart.yaml with new version + env: + VERSION: ${{ env.VERSION }} run: | - yq -i ".version = \"$VERSION\"" helm-chart/Chart.yaml - yq -i ".appVersion = \"v$VERSION\"" helm-chart/Chart.yaml + set -euo pipefail + + echo "Updating Chart.yaml with version: ${VERSION}" + yq -i ".version = \"${VERSION}\"" helm-chart/Chart.yaml + yq -i ".appVersion = \"${VERSION}\"" helm-chart/Chart.yaml + + echo "✅ Successfully updated Chart.yaml" - name: Package Helm chart + env: + VERSION: ${{ env.VERSION }} run: | + set -euo pipefail + + echo "Packaging Helm chart version: ${VERSION}" helm package ./helm-chart + echo "✅ Successfully packaged formbricks-${VERSION}.tgz" + - name: Push Helm chart to GitHub Container Registry + env: + VERSION: ${{ env.VERSION }} run: | - helm push "formbricks-$VERSION.tgz" oci://ghcr.io/formbricks/helm-charts + set -euo pipefail + + echo "Pushing Helm chart to registry: formbricks-${VERSION}.tgz" + helm push "formbricks-${VERSION}.tgz" oci://ghcr.io/formbricks/helm-charts + + echo "✅ Successfully pushed Helm chart to registry"