mirror of
https://github.com/formbricks/formbricks.git
synced 2026-04-22 19:39:01 -05:00
Compare commits
1 Commits
4.8.2
...
4.0.0-rc.1
| Author | SHA1 | Date | |
|---|---|---|---|
| 3ba6dd9ada |
@@ -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<<EOF"
|
||||||
|
echo -e "${TAGS}"
|
||||||
|
echo "EOF"
|
||||||
|
} >> "${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<<EOF"
|
||||||
|
echo -e "${TAGS}"
|
||||||
|
echo "EOF"
|
||||||
|
} >> "${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
|
||||||
@@ -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"
|
||||||
@@ -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
|
||||||
@@ -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
|
||||||
@@ -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:
|
on:
|
||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
inputs:
|
inputs:
|
||||||
image_tag:
|
version_override:
|
||||||
description: "Image tag to push (e.g., v3.16.1, main)"
|
description: "Override version (SemVer only, e.g., 1.2.3). Leave empty to auto-detect from branch."
|
||||||
required: true
|
required: false
|
||||||
default: "v3.16.1"
|
type: string
|
||||||
deploy_production:
|
deploy_production:
|
||||||
description: "Tag image for production deployment"
|
description: "Tag image for production deployment"
|
||||||
required: false
|
required: false
|
||||||
@@ -17,6 +21,24 @@ on:
|
|||||||
required: false
|
required: false
|
||||||
default: false
|
default: false
|
||||||
type: boolean
|
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:
|
permissions:
|
||||||
contents: read
|
contents: read
|
||||||
@@ -27,14 +49,15 @@ env:
|
|||||||
# ECR settings are sourced from repository/environment variables for portability across envs/forks
|
# ECR settings are sourced from repository/environment variables for portability across envs/forks
|
||||||
ECR_REGISTRY: ${{ vars.ECR_REGISTRY }}
|
ECR_REGISTRY: ${{ vars.ECR_REGISTRY }}
|
||||||
ECR_REPOSITORY: ${{ vars.ECR_REPOSITORY }}
|
ECR_REPOSITORY: ${{ vars.ECR_REPOSITORY }}
|
||||||
DOCKERFILE: apps/web/Dockerfile
|
|
||||||
CONTEXT: .
|
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
build-and-push:
|
build-and-push:
|
||||||
name: Build and Push
|
name: Build and Push
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
timeout-minutes: 45
|
timeout-minutes: 45
|
||||||
|
outputs:
|
||||||
|
IMAGE_TAG: ${{ steps.build.outputs.image_tag }}
|
||||||
|
TAGS: ${{ steps.build.outputs.registry_tags }}
|
||||||
steps:
|
steps:
|
||||||
- name: Harden the runner (Audit all outbound calls)
|
- name: Harden the runner (Audit all outbound calls)
|
||||||
uses: step-security/harden-runner@ec9f2d5744a09debf3a187a3f4f675c53b671911 # v2.13.0
|
uses: step-security/harden-runner@ec9f2d5744a09debf3a187a3f4f675c53b671911 # v2.13.0
|
||||||
@@ -44,125 +67,22 @@ jobs:
|
|||||||
- name: Checkout repository
|
- name: Checkout repository
|
||||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||||
|
|
||||||
- name: Validate image tag input
|
- name: Build and push cloud deployment image
|
||||||
shell: bash
|
id: build
|
||||||
env:
|
uses: ./.github/actions/build-and-push-docker
|
||||||
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<<EOF"
|
|
||||||
echo -e "${TAGS}"
|
|
||||||
echo "EOF"
|
|
||||||
} >> "${GITHUB_OUTPUT}"
|
|
||||||
|
|
||||||
- name: Configure AWS credentials (OIDC)
|
|
||||||
uses: aws-actions/configure-aws-credentials@7474bc4690e29a8392af63c5b98e7449536d5c3a
|
|
||||||
with:
|
with:
|
||||||
role-to-assume: ${{ secrets.AWS_ECR_PUSH_ROLE_ARN }}
|
registry_type: "ecr"
|
||||||
aws-region: ${{ env.ECR_REGION }}
|
ecr_registry: ${{ env.ECR_REGISTRY }}
|
||||||
|
ecr_repository: ${{ env.ECR_REPOSITORY }}
|
||||||
- name: Log in to Amazon ECR
|
ecr_region: ${{ env.ECR_REGION }}
|
||||||
uses: aws-actions/amazon-ecr-login@062b18b96a7aff071d4dc91bc00c4c1a7945b076
|
aws_role_arn: ${{ secrets.AWS_ECR_PUSH_ROLE_ARN }}
|
||||||
|
version: ${{ inputs.version_override || inputs.image_tag }}
|
||||||
- name: Set up Depot CLI
|
deploy_production: ${{ inputs.deploy_production }}
|
||||||
uses: depot/setup-action@b0b1ea4f69e92ebf5dea3f8713a1b0c37b2126a5 # v1.6.0
|
deploy_staging: ${{ inputs.deploy_staging }}
|
||||||
|
is_prerelease: ${{ inputs.IS_PRERELEASE }}
|
||||||
- name: Build and push image (Depot remote builder)
|
env:
|
||||||
uses: depot/build-push-action@636daae76684e38c301daa0c5eca1c095b24e780 # v1.14.0
|
DEPOT_PROJECT_TOKEN: ${{ secrets.DEPOT_PROJECT_TOKEN }}
|
||||||
with:
|
DUMMY_DATABASE_URL: ${{ secrets.DUMMY_DATABASE_URL }}
|
||||||
project: tw0fqmsx3c
|
DUMMY_ENCRYPTION_KEY: ${{ secrets.DUMMY_ENCRYPTION_KEY }}
|
||||||
token: ${{ secrets.DEPOT_PROJECT_TOKEN }}
|
DUMMY_REDIS_URL: ${{ secrets.DUMMY_REDIS_URL }}
|
||||||
context: ${{ env.CONTEXT }}
|
SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }}
|
||||||
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 }}
|
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ on:
|
|||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
inputs:
|
inputs:
|
||||||
VERSION:
|
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
|
required: true
|
||||||
type: string
|
type: string
|
||||||
REPOSITORY:
|
REPOSITORY:
|
||||||
|
|||||||
@@ -8,8 +8,8 @@ permissions:
|
|||||||
contents: read
|
contents: read
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
docker-build:
|
docker-build-community:
|
||||||
name: Build & release docker image
|
name: Build & release community docker image
|
||||||
permissions:
|
permissions:
|
||||||
contents: read
|
contents: read
|
||||||
packages: write
|
packages: write
|
||||||
@@ -19,6 +19,19 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
IS_PRERELEASE: ${{ github.event.release.prerelease }}
|
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:
|
helm-chart-release:
|
||||||
name: Release Helm Chart
|
name: Release Helm Chart
|
||||||
permissions:
|
permissions:
|
||||||
@@ -27,22 +40,42 @@ jobs:
|
|||||||
uses: ./.github/workflows/release-helm-chart.yml
|
uses: ./.github/workflows/release-helm-chart.yml
|
||||||
secrets: inherit
|
secrets: inherit
|
||||||
needs:
|
needs:
|
||||||
- docker-build
|
- docker-build-community
|
||||||
with:
|
with:
|
||||||
VERSION: ${{ needs.docker-build.outputs.VERSION }}
|
VERSION: ${{ needs.docker-build-community.outputs.VERSION }}
|
||||||
|
|
||||||
deploy-formbricks-cloud:
|
verify-cloud-build:
|
||||||
name: Deploy Helm Chart to Formbricks Cloud
|
name: Verify Cloud Build Outputs
|
||||||
permissions:
|
runs-on: ubuntu-latest
|
||||||
contents: read
|
timeout-minutes: 5 # Simple verification should be quick
|
||||||
id-token: write
|
|
||||||
secrets: inherit
|
|
||||||
uses: ./.github/workflows/deploy-formbricks-cloud.yml
|
|
||||||
needs:
|
needs:
|
||||||
- docker-build
|
- docker-build-cloud
|
||||||
- helm-chart-release
|
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:
|
with:
|
||||||
VERSION: v${{ needs.docker-build.outputs.VERSION }}
|
release_tag: ${{ github.event.release.tag_name }}
|
||||||
ENVIRONMENT: ${{ github.event.release.prerelease && 'staging' || 'production' }}
|
commit_sha: ${{ github.sha }}
|
||||||
|
is_prerelease: ${{ github.event.release.prerelease }}
|
||||||
|
|
||||||
|
|||||||
@@ -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"
|
||||||
@@ -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.
|
# This workflow builds experimental/testing versions of Formbricks for self-hosting customers
|
||||||
# They are provided by a third-party and are governed by
|
# to test fixes and features before official releases. Images are pushed to GHCR with
|
||||||
# separate terms of service, privacy policy, and support
|
# timestamped experimental versions for easy identification and testing.
|
||||||
# documentation.
|
|
||||||
|
|
||||||
on:
|
on:
|
||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
|
inputs:
|
||||||
env:
|
version_override:
|
||||||
# Use docker.io for Docker Hub if empty
|
description: "Override version (SemVer only, e.g., 1.2.3-beta). Leave empty for auto-generated experimental version."
|
||||||
REGISTRY: ghcr.io
|
required: false
|
||||||
# github.repository as <account>/<repo>
|
type: string
|
||||||
IMAGE_NAME: ${{ github.repository }}-experimental
|
|
||||||
TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }}
|
|
||||||
TURBO_TEAM: ${{ secrets.TURBO_TEAM }}
|
|
||||||
|
|
||||||
permissions:
|
permissions:
|
||||||
contents: read
|
contents: read
|
||||||
|
packages: write
|
||||||
|
id-token: write
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
build:
|
build-community-testing:
|
||||||
|
name: Build Community Testing Image
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
permissions:
|
timeout-minutes: 45
|
||||||
contents: read
|
|
||||||
packages: write
|
|
||||||
# This is used to complete the identity challenge
|
|
||||||
# with sigstore/fulcio when running outside of PRs.
|
|
||||||
id-token: write
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Harden the runner (Audit all outbound calls)
|
- 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:
|
with:
|
||||||
egress-policy: audit
|
egress-policy: audit
|
||||||
|
|
||||||
@@ -42,110 +34,17 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
|
|
||||||
- name: Generate SemVer version from branch or tag
|
- name: Build and push community testing image
|
||||||
id: generate_version
|
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:
|
env:
|
||||||
REF_NAME: ${{ github.ref_name }}
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
REF_TYPE: ${{ github.ref_type }}
|
DEPOT_PROJECT_TOKEN: ${{ secrets.DEPOT_PROJECT_TOKEN }}
|
||||||
run: |
|
DUMMY_DATABASE_URL: ${{ secrets.DUMMY_DATABASE_URL }}
|
||||||
# Get reference name and type from environment variables
|
DUMMY_ENCRYPTION_KEY: ${{ secrets.DUMMY_ENCRYPTION_KEY }}
|
||||||
echo "Reference type: $REF_TYPE"
|
DUMMY_REDIS_URL: ${{ secrets.DUMMY_REDIS_URL }}
|
||||||
echo "Reference name: $REF_NAME"
|
SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }}
|
||||||
|
|
||||||
# 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}"
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
name: Docker Release to Github
|
name: Release Community Docker Images
|
||||||
|
|
||||||
# This workflow uses actions that are not certified by GitHub.
|
# This workflow uses actions that are not certified by GitHub.
|
||||||
# They are provided by a third-party and are governed by
|
# They are provided by a third-party and are governed by
|
||||||
@@ -23,8 +23,6 @@ env:
|
|||||||
REGISTRY: ghcr.io
|
REGISTRY: ghcr.io
|
||||||
# github.repository as <account>/<repo>
|
# github.repository as <account>/<repo>
|
||||||
IMAGE_NAME: ${{ github.repository }}
|
IMAGE_NAME: ${{ github.repository }}
|
||||||
TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }}
|
|
||||||
TURBO_TEAM: ${{ secrets.TURBO_TEAM }}
|
|
||||||
|
|
||||||
permissions:
|
permissions:
|
||||||
contents: read
|
contents: read
|
||||||
@@ -32,6 +30,7 @@ permissions:
|
|||||||
jobs:
|
jobs:
|
||||||
build:
|
build:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
timeout-minutes: 45
|
||||||
permissions:
|
permissions:
|
||||||
contents: read
|
contents: read
|
||||||
packages: write
|
packages: write
|
||||||
@@ -44,103 +43,60 @@ jobs:
|
|||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Harden the runner (Audit all outbound calls)
|
- 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:
|
with:
|
||||||
egress-policy: audit
|
egress-policy: audit
|
||||||
|
|
||||||
- name: Checkout repository
|
- name: Checkout repository
|
||||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||||
|
|
||||||
- name: Get Release Tag
|
- name: Extract release version from tag
|
||||||
id: extract_release_tag
|
id: extract_release_tag
|
||||||
run: |
|
run: |
|
||||||
# Extract version from tag (e.g., refs/tags/v1.2.3 -> 1.2.3)
|
set -euo pipefail
|
||||||
TAG="$GITHUB_REF"
|
|
||||||
TAG=${TAG#refs/tags/v}
|
|
||||||
|
|
||||||
# Validate the extracted tag format
|
# Extract tag name with fallback logic for different trigger contexts
|
||||||
if [[ ! "$TAG" =~ ^[0-9]+\.[0-9]+\.[0-9]+(-[a-zA-Z0-9.-]+)?(\+[a-zA-Z0-9.-]+)?$ ]]; then
|
if [[ -n "${RELEASE_TAG:-}" ]]; then
|
||||||
echo "❌ Error: Invalid release tag format after extraction. Must be semver (e.g., 1.2.3, 1.2.3-alpha)"
|
TAG="$RELEASE_TAG"
|
||||||
echo "Original ref: $GITHUB_REF"
|
echo "Using RELEASE_TAG override: $TAG"
|
||||||
echo "Extracted tag: $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
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Safely add to environment variables
|
|
||||||
echo "RELEASE_TAG=$TAG" >> $GITHUB_ENV
|
|
||||||
|
|
||||||
echo "VERSION=$TAG" >> $GITHUB_OUTPUT
|
echo "VERSION=$TAG" >> $GITHUB_OUTPUT
|
||||||
echo "Using tag-based version: $TAG"
|
echo "Using version: $TAG"
|
||||||
|
|
||||||
- name: Update package.json version
|
- name: Build and push community release image
|
||||||
run: |
|
id: build
|
||||||
sed -i "s/\"version\": \"0.0.0\"/\"version\": \"${{ env.RELEASE_TAG }}\"/" ./apps/web/package.json
|
uses: ./.github/actions/build-and-push-docker
|
||||||
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:
|
with:
|
||||||
registry: ${{ env.REGISTRY }}
|
registry_type: "ghcr"
|
||||||
username: ${{ github.actor }}
|
ghcr_image_name: ${{ env.IMAGE_NAME }}
|
||||||
password: ${{ secrets.GITHUB_TOKEN }}
|
version: ${{ steps.extract_release_tag.outputs.VERSION }}
|
||||||
|
is_prerelease: ${{ inputs.IS_PRERELEASE }}
|
||||||
# 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' }}
|
|
||||||
env:
|
env:
|
||||||
# https://docs.github.com/en/actions/security-guides/security-hardening-for-github-actions#using-an-intermediate-environment-variable
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
TAGS: ${{ steps.meta.outputs.tags }}
|
DEPOT_PROJECT_TOKEN: ${{ secrets.DEPOT_PROJECT_TOKEN }}
|
||||||
DIGEST: ${{ steps.build-and-push.outputs.digest }}
|
DUMMY_DATABASE_URL: ${{ secrets.DUMMY_DATABASE_URL }}
|
||||||
# This step uses the identity token to provision an ephemeral certificate
|
DUMMY_ENCRYPTION_KEY: ${{ secrets.DUMMY_ENCRYPTION_KEY }}
|
||||||
# against the sigstore community Fulcio instance.
|
DUMMY_REDIS_URL: ${{ secrets.DUMMY_REDIS_URL }}
|
||||||
run: echo "${TAGS}" | xargs -I {} cosign sign --yes {}@${DIGEST}
|
SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }}
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ jobs:
|
|||||||
contents: read
|
contents: read
|
||||||
steps:
|
steps:
|
||||||
- name: Harden the runner (Audit all outbound calls)
|
- 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:
|
with:
|
||||||
egress-policy: audit
|
egress-policy: audit
|
||||||
|
|
||||||
@@ -59,14 +59,35 @@ jobs:
|
|||||||
uses: dcarbone/install-yq-action@4075b4dca348d74bd83f2bf82d30f25d7c54539b # v1.3.1
|
uses: dcarbone/install-yq-action@4075b4dca348d74bd83f2bf82d30f25d7c54539b # v1.3.1
|
||||||
|
|
||||||
- name: Update Chart.yaml with new version
|
- name: Update Chart.yaml with new version
|
||||||
|
env:
|
||||||
|
VERSION: ${{ env.VERSION }}
|
||||||
run: |
|
run: |
|
||||||
yq -i ".version = \"$VERSION\"" helm-chart/Chart.yaml
|
set -euo pipefail
|
||||||
yq -i ".appVersion = \"v$VERSION\"" helm-chart/Chart.yaml
|
|
||||||
|
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
|
- name: Package Helm chart
|
||||||
|
env:
|
||||||
|
VERSION: ${{ env.VERSION }}
|
||||||
run: |
|
run: |
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
echo "Packaging Helm chart version: ${VERSION}"
|
||||||
helm package ./helm-chart
|
helm package ./helm-chart
|
||||||
|
|
||||||
|
echo "✅ Successfully packaged formbricks-${VERSION}.tgz"
|
||||||
|
|
||||||
- name: Push Helm chart to GitHub Container Registry
|
- name: Push Helm chart to GitHub Container Registry
|
||||||
|
env:
|
||||||
|
VERSION: ${{ env.VERSION }}
|
||||||
run: |
|
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"
|
||||||
|
|||||||
Reference in New Issue
Block a user