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

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
72 changes: 48 additions & 24 deletions .github/workflows/continuous-delivery.yml
Original file line number Diff line number Diff line change
Expand Up @@ -33,19 +33,30 @@
name: Branch Check
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1

- name: Validate branch
env:
TARGET_ENV: ${{ github.event.inputs.target_env }}
run: |

Check failure on line 42 in .github/workflows/continuous-delivery.yml

View workflow job for this annotation

GitHub Actions / Lint

shellcheck reported issue in this script: SC2236:style:15:6: Use -n instead of ! -z
if [ "$GITHUB_REF_NAME" != 'staging' ] && [ "$GITHUB_REF_NAME" != 'master' ]; then
echo 'This workflow can only be run on branches staging and master.'
exit 1
fi
if [ "$TARGET_ENV" == 'luxadmosam' ] || [ "$TARGET_ENV" == 'euros' ]; then
if [ "$GITHUB_REF_NAME" != 'master' ]; then
echo 'Only the master branch can be deployed to external parties.'
exit 1
fi
# Check branch restriction from deploy_targets.yml
BRANCH_RESTRICTION=$(python3 << 'PYTHON_EOF'
import yaml
with open('config/deploy_targets.yml', 'r') as f:
config = yaml.safe_load(f)
target = config['targets']['${{ github.event.inputs.target_env }}']
restriction = target.get('branch_restriction')
print(restriction if restriction else '')
PYTHON_EOF
)
if [ ! -z "$BRANCH_RESTRICTION" ] && [ "$GITHUB_REF_NAME" != "$BRANCH_RESTRICTION" ]; then
echo "Target $TARGET_ENV can only be deployed from branch $BRANCH_RESTRICTION, but current branch is $GITHUB_REF_NAME."
exit 1
fi

metadata:
Expand Down Expand Up @@ -74,18 +85,25 @@
echo 'has_diff=false' >> "$GITHUB_OUTPUT"
fi
fi

if [ "$TARGET_ENV" == 'luxadmosam' ]; then
echo 'stage=luxproduction' >> "$GITHUB_OUTPUT"
elif [ "$TARGET_ENV" == 'euros' ]; then
echo 'stage=euros' >> "$GITHUB_OUTPUT"
else
echo 'stage=production' >> "$GITHUB_OUTPUT"
fi
else
echo 'stage=staging' >> "$GITHUB_OUTPUT"
fi

# Get stage from deploy_targets.yml
STAGE=$(python3 << 'PYTHON_EOF'
import yaml
with open('config/deploy_targets.yml', 'r') as f:
config = yaml.safe_load(f)
target_env = '${{ github.event.inputs.target_env }}'
current_branch = '${{ github.ref_name }}'
# If on staging branch, always use staging stage unless targeting external party
if current_branch == 'staging':
print('staging')
else:
# On master: use the stage from the target environment
print(config['targets'][target_env]['stage'])
PYTHON_EOF
)
echo "stage=$STAGE" >> "$GITHUB_OUTPUT"

merge:
name: Merge
runs-on: ubuntu-latest
Expand Down Expand Up @@ -162,15 +180,21 @@
env:
TARGET_ENV: ${{ github.event.inputs.target_env }}
run: |
if [ "$TARGET_ENV" == 'luxadmosam' ] && [ "$GITHUB_REF_NAME" = 'master' ]; then
echo 'environment_url=https://luxstreep.csvalpha.nl' >> "$GITHUB_OUTPUT"
elif [ "$TARGET_ENV" == 'euros' ] && [ "$GITHUB_REF_NAME" = 'master' ]; then
echo 'environment_url=https://euros.csvalpha.nl' >> "$GITHUB_OUTPUT"
elif [ "$GITHUB_REF_NAME" = 'master' ]; then
echo 'environment_url=https://streep.csvalpha.nl' >> "$GITHUB_OUTPUT"
else
echo 'environment_url=https://stagingstreep.csvalpha.nl' >> "$GITHUB_OUTPUT"
fi
# Get URL from deploy_targets.yml based on current branch and target
URL=$(python3 << 'PYTHON_EOF'
import yaml
with open('config/deploy_targets.yml', 'r') as f:
config = yaml.safe_load(f)
current_branch = '${{ github.ref_name }}'
target_env = '${{ github.event.inputs.target_env }}'
# If on staging, use stagingstreep URL; otherwise use the target's URL
if current_branch == 'staging':
print(config['targets']['stagingstreep']['url'])
else:
print(config['targets'][target_env]['url'])
PYTHON_EOF
)
echo "environment_url=$URL" >> "$GITHUB_OUTPUT"

- name: Checkout code
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
Expand Down
5 changes: 3 additions & 2 deletions Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,8 @@ WORKDIR /app

# Pre-install gems, so that they can be cached.
COPY Gemfile* /app/
RUN if [ "$RAILS_ENV" = 'production' ] || [ "$RAILS_ENV" = 'staging' ] || [ "$RAILS_ENV" = 'luxproduction' ] || [ "$RAILS_ENV" = 'euros' ]; then \
COPY config/deploy_targets.yml /app/config/
RUN if ruby -e "require 'yaml'; targets = YAML.load_file('config/deploy_targets.yml')['targets'].keys; exit(targets.include?(ENV['RAILS_ENV']) ? 0 : 1)"; then \
bundle config set --local without 'development test'; \
else \
bundle config set --local without 'development'; \
Expand All @@ -43,7 +44,7 @@ RUN yarn install --immutable
COPY . /app/

# Precompile assets after copying app because whole Rails pipeline is needed.
RUN if [ "$RAILS_ENV" = 'production' ] || [ "$RAILS_ENV" = 'staging' ] || [ "$RAILS_ENV" = 'luxproduction' ] || [ "$RAILS_ENV" = 'euros' ]; then \
RUN if ruby -e "require 'yaml'; targets = YAML.load_file('config/deploy_targets.yml')['targets'].keys; exit(targets.include?(ENV['RAILS_ENV']) ? 0 : 1)"; then \
SECRET_KEY_BASE_DUMMY=1 bundle exec rails assets:precompile; \
else \
echo "Skipping assets:precompile"; \
Expand Down
1 change: 1 addition & 0 deletions app/controllers/application_controller.rb
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
class ApplicationController < ActionController::Base
include Pundit::Authorization
include EnvironmentAware

protect_from_forgery with: :exception, prepend: true

Expand Down
30 changes: 30 additions & 0 deletions app/controllers/concerns/environment_aware.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
module EnvironmentAware
extend ActiveSupport::Concern

included do
class_variable_set(:@@deploy_targets, nil)

Check failure on line 5 in app/controllers/concerns/environment_aware.rb

View workflow job for this annotation

GitHub Actions / Lint

Style/ClassVars: Replace class var :@@deploy_targets with a class instance var. (https://rubystyle.guide#no-class-vars)
end

def production_deployed?
self.class.production_deployed?
end

class_methods do
def production_deployed?
Rails.env.development? || Rails.env.test? ? false : deployed_environments.include?(Rails.env.to_sym)

Check failure on line 14 in app/controllers/concerns/environment_aware.rb

View workflow job for this annotation

GitHub Actions / Lint

[Correctable] Rails/EnvLocal: Use Rails.env.local? instead.
end

def deployed_environments
load_deploy_targets.keys.map(&:to_sym)
end

private

def load_deploy_targets
@@deploy_targets ||= begin

Check failure on line 24 in app/controllers/concerns/environment_aware.rb

View workflow job for this annotation

GitHub Actions / Lint

Style/ClassVars: Replace class var @@deploy_targets with a class instance var. (https://rubystyle.guide#no-class-vars)
config_path = Rails.root.join('config', 'deploy_targets.yml')
YAML.load_file(config_path)['targets']
end
end
end
end
4 changes: 1 addition & 3 deletions app/controllers/credit_mutations_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -19,9 +19,7 @@ def create # rubocop:disable Metrics/MethodLength, Metrics/AbcSize

respond_to do |format|
if @mutation.save
if Rails.env.production? || Rails.env.staging? || Rails.env.luxproduction? || Rails.env.euros?
NewCreditMutationNotificationJob.perform_later(@mutation)
end
NewCreditMutationNotificationJob.perform_later(@mutation) if production_deployed?
format.html { redirect_to which_redirect?, flash: { success: 'Inleg of mutatie aangemaakt' } }
format.json do
render json: @mutation, include: { user: { methods: User.orderscreen_json_includes } }
Expand Down
1 change: 1 addition & 0 deletions app/jobs/application_job.rb
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
class ApplicationJob < ActiveJob::Base
include EnvironmentAware
end
2 changes: 1 addition & 1 deletion app/jobs/credit_insufficient_notification_job.rb
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ def send_notification_delivery_reports(success_count, unnotifyable_users)
).deliver_later
end

return unless Rails.env.production? || Rails.env.staging? || Rails.env.luxproduction? || Rails.env.euros?
return unless production_deployed?

HealthCheckJob.perform_later('credit_insufficient')
end
Expand Down
2 changes: 1 addition & 1 deletion app/jobs/payment_poll_job.rb
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ def perform
# it will be checked again the next time this poll job runs
end

return unless Rails.env.production? || Rails.env.staging? || Rails.env.luxproduction? || Rails.env.euros?
return unless production_deployed?

HealthCheckJob.perform_later('payment_poll')
end
Expand Down
2 changes: 2 additions & 0 deletions app/models/application_record.rb
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
class ApplicationRecord < ActiveRecord::Base
include EnvironmentAware

self.abstract_class = true

acts_as_paranoid
Expand Down
4 changes: 2 additions & 2 deletions app/views/partials/_footer.html.erb
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
<footer class="footer d-flex justify-content-between">
<span class="text-danger text-uppercase fw-bold mx-2">
<% if Rails.application.config.x.sofia_host == 'stagingstreep.csvalpha.nl' %>
<% if DeployTargetConfig.staging_deployed? %>
For demo and testing purposes only
<% elsif Rails.application.config.x.sofia_host != 'streep.csvalpha.nl' && Rails.application.config.x.sofia_host != 'luxstreep.csvalpha.nl' && Rails.application.config.x.sofia_host != 'euros.csvalpha.nl' %>
<% elsif !DeployTargetConfig.production_deployed? %>
Development mode
<% end %>
</span>
Expand Down
21 changes: 8 additions & 13 deletions config/deploy.rb
Original file line number Diff line number Diff line change
Expand Up @@ -2,22 +2,17 @@
# This file is for mina to be able to connect to the rails console on the server

require 'mina/rails'
require 'yaml'
import 'lib/mina/tasks/rails.rake'

set :domain, 'ssh.csvalpha.nl'

task :staging do
set :deploy_to, '/opt/docker/sofia/staging'
end

task :production do
set :deploy_to, '/opt/docker/sofia/production'
end

task :luxproduction do
set :deploy_to, '/opt/docker/sofia/luxproduction'
end
# Load deployment targets from config/deploy_targets.yml
deploy_targets = YAML.load_file(File.expand_path('deploy_targets.yml', __dir__))['targets']

task :euros do
set :deploy_to, '/opt/docker/sofia/euros'
# Dynamically create mina tasks for each deployment target
deploy_targets.each do |target_name, config|
task target_name.to_sym do
set :deploy_to, config['deploy_path']
end
end
46 changes: 46 additions & 0 deletions config/deploy_targets.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
# Deployment Targets Configuration
#
# This file defines all deployment targets for the Sofia application.
# Each target specifies the deployment location, stage name, and URL.
#
# To add a new deployment target:
# 1. Add a new entry below with the target name as the key
# 2. Set deploy_path to the docker directory path on the server
# 3. Set stage to the Rails environment name (creates config/environments/{stage}.rb)
# 4. Set hostname to the application's domain
# 5. Set url to the HTTPS URL for GitHub Actions deployment tracking
# 6. Set branch_restriction to limit deployments to specific branches (optional)
# 7. Optionally set allow_targets to whitelist this target (used by CI to determine valid targets)

targets:
csvalpha:
deploy_path: /opt/docker/sofia/production
stage: production
hostname: streep.csvalpha.nl
url: https://streep.csvalpha.nl
branch_restriction: master # Only from master branch
allow_targets: csvalpha

stagingstreep:
deploy_path: /opt/docker/sofia/staging
stage: staging
hostname: stagingstreep.csvalpha.nl
url: https://stagingstreep.csvalpha.nl
branch_restriction: staging # Only from staging branch
allow_targets: csvalpha

luxadmosam:
deploy_path: /opt/docker/sofia/luxproduction
stage: luxproduction
hostname: luxstreep.csvalpha.nl
url: https://luxstreep.csvalpha.nl
branch_restriction: master # Only from master branch
allow_targets: luxadmosam

euros:
deploy_path: /opt/docker/sofia/euros
stage: euros
hostname: euros.csvalpha.nl
url: https://euros.csvalpha.nl
branch_restriction: master # Only from master branch
allow_targets: euros
30 changes: 30 additions & 0 deletions config/initializers/deploy_target_config.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
# Helper to dynamically generate environment configurations from deploy_targets.yml
module DeployTargetConfig
def self.load_targets
@targets ||= YAML.load_file(Rails.root.join('config', 'deploy_targets.yml'))['targets']
end

def self.target_stages
load_targets.keys
end

def self.all_production_stages
target_stages
end

def self.production_hostnames
load_targets.values.map { |config| config['hostname'] }

Check failure on line 16 in config/initializers/deploy_target_config.rb

View workflow job for this annotation

GitHub Actions / Lint

[Correctable] Rails/Pluck: Prefer pluck('hostname') over map { |config| config['hostname'] }. (https://rails.rubystyle.guide#pluck)
end

def self.staging_hostname
load_targets['stagingstreep']&.dig('hostname')
end

def self.production_deployed?
production_hostnames.include?(Rails.application.config.x.sofia_host)
end

def self.staging_deployed?
Rails.application.config.x.sofia_host == staging_hostname
end
end
6 changes: 5 additions & 1 deletion config/initializers/sentry.rb
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
Sentry.init do |config|
config.dsn = Rails.application.config.x.sentry_dsn
config.enabled_environments = %w[production staging luxproduction euros]

Check failure on line 3 in config/initializers/sentry.rb

View workflow job for this annotation

GitHub Actions / Lint

[Correctable] Layout/TrailingWhitespace: Trailing whitespace detected. (https://rubystyle.guide#no-trailing-whitespace)
# Load enabled environments from deploy_targets.yml
deploy_targets = YAML.load_file(Rails.root.join('config', 'deploy_targets.yml'))['targets']
config.enabled_environments = deploy_targets.keys

Check failure on line 7 in config/initializers/sentry.rb

View workflow job for this annotation

GitHub Actions / Lint

[Correctable] Layout/TrailingWhitespace: Trailing whitespace detected. (https://rubystyle.guide#no-trailing-whitespace)
config.environment = Rails.env
config.release = ENV.fetch('BUILD_HASH', nil)
end
4 changes: 3 additions & 1 deletion spec/rails_helper.rb
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,9 @@
require File.expand_path('../config/environment', __dir__)
# Prevent database truncation if the environment is production
# rubocop:disable Rails/Exit
if Rails.env.production? || Rails.env.staging? || Rails.env.luxproduction? || Rails.env.euros?
deploy_targets = YAML.load_file(File.expand_path('../config/deploy_targets.yml', __dir__))['targets']
production_envs = deploy_targets.keys.map(&:to_sym)
if production_envs.include?(Rails.env.to_sym)

Check failure on line 11 in spec/rails_helper.rb

View workflow job for this annotation

GitHub Actions / Lint

[Correctable] Style/IfUnlessModifier: Favor modifier if usage when having a single-line body. Another good alternative is the usage of control flow &&/||. (https://rubystyle.guide#if-as-a-modifier)
abort('The Rails environment is running in production mode!')
end
# rubocop:enable Rails/Exit
Expand Down
Loading