Skip to content
Merged
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
46 changes: 23 additions & 23 deletions .github/workflows/_build.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -94,26 +94,26 @@ jobs:
${{ env.ECR_REPO }}:${{ steps.sha.outputs.SHORT_SHA }}
platforms: linux/amd64,linux/arm64

# TODO What is this used for? Leftover artifact from Jenkins
- name: Extract Jira ticket numbers from commit message
run: |
tickets=$(git log -1 --pretty=%B | grep -io 'M2-[0-9]\+' | tr '[:lower:]' '[:upper:]' | sort | uniq | tr '\n' ' ')
if [ -z "$tickets" ]; then
echo "No Jira tickets found in the latest commit message."
else
echo "Jira ticket numbers found: ${tickets}"
if [ "${GITHUB_BRANCH_OR_TAG}" != "develop" ]; then
echo "Skipping saving Jira ticket numbers to a file because the branch is not develop"
else
file="jira-tickets.json"
echo "{ \"issues\": $(echo "${tickets}" | jq -R -s -c 'split(" ")[:-1]') }" > "${file}"
echo "Jira ticket numbers saved to ${file}"
fi
fi

- name: Upload artifacts
uses: actions/upload-artifact@v4
with:
name: build-artifacts
path: |
${GITHUB_REPO_NAME}/${OUT_DIR}/jira-tickets.json
# # TODO What is this used for? Leftover artifact from Jenkins
# - name: Extract Jira ticket numbers from commit message
# run: |
# tickets=$(git log -1 --pretty=%B | grep -io 'M2-[0-9]\+' | tr '[:lower:]' '[:upper:]' | sort | uniq | tr '\n' ' ')
# if [ -z "$tickets" ]; then
# echo "No Jira tickets found in the latest commit message."
# else
# echo "Jira ticket numbers found: ${tickets}"
# if [ "${GITHUB_BRANCH_OR_TAG}" != "develop" ]; then
# echo "Skipping saving Jira ticket numbers to a file because the branch is not develop"
# else
# file="jira-tickets.json"
# echo "{ \"issues\": $(echo "${tickets}" | jq -R -s -c 'split(" ")[:-1]') }" > "${file}"
# echo "Jira ticket numbers saved to ${file}"
# fi
# fi
#
# - name: Upload artifacts
# uses: actions/upload-artifact@v4
# with:
# name: build-artifacts
# path: |
# ${GITHUB_REPO_NAME}/${OUT_DIR}/jira-tickets.json
23 changes: 21 additions & 2 deletions .github/workflows/_deploy.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,12 @@ jobs:
task-definition: task-definition.json
container-name: ${{ matrix.apps.container }}
image: ${{ needs.setup-vars.outputs.ecr-repo }}/api-server:${{ inputs.image-tag }}

environment-variables: |
VERSION=${{ inputs.image-tag }}
DD_VERSION=${{ inputs.image-tag }}
SENTRY_RELEASE=${{ inputs.image-tag }}
docker-labels: |
com.datadoghq.tags.version=${{ inputs.image-tag }}
- name: Update Task Definition
uses: aws-actions/amazon-ecs-deploy-task-definition@v2
with:
Expand Down Expand Up @@ -116,11 +121,25 @@ jobs:
- deploy
steps:
- uses: actions/checkout@v4
- name: configure aws credentials
uses: aws-actions/configure-aws-credentials@v4
with:
role-to-assume: arn:aws:iam::917902836630:role/ssm-versions-oidc-github-role
role-session-name: OIDC-GHA-session-version
aws-region: us-east-1
- name: Store version in SSM
run: |
aws ssm put-parameter \
--name "/curious/app/backend/${{ inputs.env-name }}/version" \
--value "${{ inputs.image-tag }}" \
--type "String" \
--overwrite

- name: "Send Slack message on success"
uses: rtCamp/action-slack-notify@v2
env:
SLACK_COLOR: success
SLACK_WEBHOOK: ${{ secrets.slack-webhook }}
MSG_MINIMAL: actions url
SLACK_TITLE: Deployment to ${{ inputs.env-name }} environment
SLACK_MESSAGE: '🚀 Deployment was successful!'
SLACK_MESSAGE: '🚀 Deployment was successful. Version: *${{ inputs.image-tag }}*'
3 changes: 3 additions & 0 deletions .github/workflows/_migrations.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -56,9 +56,12 @@ jobs:
image: ${{ needs.setup-vars.outputs.ecr-repo }}/api-server:${{ inputs.image-tag }}
command: /fastapi-migrate
environment-variables: |
VERSION=${{ inputs.image-tag }}
DD_VERSION=${{ inputs.image-tag }}
SENTRY_RELEASE=${{ inputs.image-tag }}
docker-labels: |
com.datadoghq.tags.version=${{ inputs.image-tag }}

- name: Update Task Definition
id: taskdef
uses: aws-actions/amazon-ecs-deploy-task-definition@v2
Expand Down
24 changes: 20 additions & 4 deletions .github/workflows/run_build_deploy_dev.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -18,29 +18,45 @@ permissions:


jobs:
version:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Get git short sha
id: sha
run: |
echo "sha=$(git rev-parse --short HEAD)" >> $GITHUB_OUTPUT

outputs:
short-sha: ${{ steps.sha.outputs.sha }}

build:
name: Docker Build and Push
needs: version
secrets: inherit
uses: ./.github/workflows/_build.yaml
with:
tag-name: ${{ needs.version.outputs.short-sha }}

migrations:
needs: build
needs: [version, build]
name: Run Migrations in Dev
secrets:
slack-webhook: ${{ secrets.SLACK_WEBHOOK_DEPLOY_TO_DEV }}
uses: ./.github/workflows/_migrations.yaml
with:
env-name: dev
image-tag: latest
image-tag: ${{ needs.version.outputs.short-sha }}

deploy:
name: Deploy to Dev
needs: [ build, migrations ]
needs: [ version, build, migrations ]
secrets:
slack-webhook: ${{ secrets.SLACK_WEBHOOK_DEPLOY_TO_DEV }}
uses: ./.github/workflows/_deploy.yaml
with:
image-tag: latest
image-tag: ${{ needs.version.outputs.short-sha }}
env-name: dev

run-e2e-tests:
Expand Down
56 changes: 28 additions & 28 deletions .github/workflows/run_build_deploy_prod.yaml
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
name: Build and Deploy to Prod

on:
release:
types:
- released
workflow_dispatch:
inputs:
target-ref:
Expand All @@ -19,53 +22,50 @@ permissions:
issues: write

jobs:
# ref:
# name: Get target ref
# runs-on: ubuntu-latest
# outputs:
# target-ref: ${{ steps.ref.outputs.ref }}
# steps:
# - id: ref
# name: Select Ref
# run: |
# TARGET_REF="${{ github.event.inputs.target_ref }}"
# # Use shell parameter expansion to provide a default value if input is unset or empty
# # github.ref_name will be used if the input is not provided or empty
# # Remove "refs/heads/" or "refs/tags/" if present in github.ref_name
# TARGET_REF=${TARGET_REF:-$(echo "${{ github.ref_name }}" | sed 's/refs\/heads\///g' | sed 's/refs\/tags\///g')}
#
# echo "Target reference: $TARGET_REF"
# echo "ref=$TARGET_REF" >> $GITHUB_OUTPUT
ref:
name: Get target ref
runs-on: ubuntu-latest
outputs:
target-ref: ${{ steps.ref.outputs.ref }}
steps:
- id: ref
name: Select Ref
run: |
TARGET_REF="${{ github.event.inputs.target-ref }}"
# Use shell parameter expansion to provide a default value if input is unset or empty
# github.ref_name will be used if the input is not provided or empty
# Remove "refs/heads/" or "refs/tags/" if present in github.ref_name
TARGET_REF=${TARGET_REF:-$(echo "${{ github.event.release.tag_name }}" | sed 's/refs\/heads\///g' | sed 's/refs\/tags\///g')}

echo "Target reference: $TARGET_REF"
echo "ref=$TARGET_REF" >> $GITHUB_OUTPUT

build:
name: Build
# needs: ref
needs: ref
secrets: inherit
uses: ./.github/workflows/_build.yaml
with:
tag-name: ${{ inputs.target-ref }}
# tag-name: ${{ needs.ref.outputs.target-ref }}
#${{ github.ref_name }}
tag-name: ${{ needs.ref.outputs.target-ref }}
git-ref: ${{ needs.ref.outputs.target-ref }}

migrations:
name: Run Migrations in Prod
needs: build
needs: [build, ref]
secrets:
slack-webhook: ${{ secrets.SLACK_WEBHOOK_DEPLOY_TO_PROD }}
uses: ./.github/workflows/_migrations.yaml
with:
env-name: prod
image-tag: ${{ inputs.target-ref }}
# image-tag: ${{ needs.ref.outputs.target-ref }}
image-tag: ${{ needs.ref.outputs.target-ref }}


deploy-prod:
name: Deploy to Prod
needs: [ build, migrations ]
needs: [ build, migrations, ref ]
secrets:
slack-webhook: ${{ secrets.SLACK_WEBHOOK_DEPLOY_TO_Prod }}
slack-webhook: ${{ secrets.SLACK_WEBHOOK_DEPLOY_TO_PROD }}
uses: ./.github/workflows/_deploy.yaml
with:
image-tag: ${{ inputs.target-ref }}
# image-tag: ${{ needs.ref.outputs.target-ref }}
env-name: prod
image-tag: ${{ needs.ref.outputs.target-ref }}
3 changes: 0 additions & 3 deletions src/apps/activities/commands/__init__.py
Original file line number Diff line number Diff line change
@@ -1,3 +0,0 @@
from apps.activities.commands.reindex_items import app as activities

__all__ = ["activities"]
91 changes: 91 additions & 0 deletions src/apps/activities/commands/delete_subscales.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
from uuid import UUID

import typer
from sqlalchemy import text

from infrastructure.commands.utils import coro
from infrastructure.database import session_manager

app = typer.Typer(help="Delete subscales and score-type reports across all versions of an applet.")

DROP_SCORE_FUNC_SQL = """
CREATE OR REPLACE FUNCTION drop_score_reports(sr jsonb)
RETURNS jsonb LANGUAGE sql IMMUTABLE AS $$
SELECT CASE
WHEN sr ? 'reports' THEN
jsonb_set(
sr, '{reports}',
COALESCE(
(SELECT jsonb_agg(r)
FROM jsonb_array_elements(sr->'reports') r
WHERE r->>'type' <> 'score'),
'[]'::jsonb
),
true
)
ELSE sr
END;
$$;
"""


@app.command(name="delete-subscales-for-all-applet-versions")
@coro
async def delete_subscales_for_all_applet_versions(
applet_id: UUID = typer.Argument(..., help="Base applet ID (UUID)"),
) -> None:
"""
Remove all subscales and score-type reports from every version of the specified applet.
"""
# obtain a session maker; then call it to get an async session
s_maker = session_manager.get_session()
async with s_maker() as session:
await session.execute(text(DROP_SCORE_FUNC_SQL))
await session.commit()

# Get the list of version IDs for this applet
versions_sql = text("""
SELECT id_version
FROM applet_histories
WHERE id = :applet_id
""")
versions = (
(
await session.execute(
versions_sql,
{"applet_id": applet_id},
)
)
.scalars()
.all()
)

if not versions:
typer.echo(f"No history versions found for applet {applet_id}.")
return

# Delete subscales and score-type reports in both tables
hist_update_sql = text("""
UPDATE activity_histories
SET subscale_setting = NULL,
scores_and_reports = CASE
WHEN scores_and_reports IS NULL THEN NULL
ELSE drop_score_reports(scores_and_reports)
END
WHERE applet_id = ANY(:versions)
""")
live_update_sql = text("""
UPDATE activities
SET subscale_setting = NULL,
scores_and_reports = CASE
WHEN scores_and_reports IS NULL THEN NULL
ELSE drop_score_reports(scores_and_reports)
END
WHERE applet_id = :applet_id
""")

await session.execute(hist_update_sql, {"versions": versions})
await session.execute(live_update_sql, {"applet_id": applet_id})
await session.commit()

typer.echo(f"Deleted subscales and score-type reports for {len(versions)} version(s) of applet {applet_id}.")
41 changes: 24 additions & 17 deletions src/apps/applets/tests/unit/test_applet_crud.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import uuid

import pytest
from pytest import FixtureRequest
from pytest_mock import MockerFixture
from sqlalchemy.ext.asyncio import AsyncSession

Expand Down Expand Up @@ -105,27 +104,35 @@ async def test_get_applets_by_display_name__applet_deleted(applet: AppletSchema,


@pytest.mark.parametrize(
"applet_fixture_name, require_login, result",
"require_login, result",
(
("applet_with_link", False, None),
("applet_with_link", True, "applet"),
("applet_with_public_link", False, "applet"),
("applet_with_public_link", True, None),
(False, applet), # public
(True, None), # public
),
)
async def test_get_applets_by_link(
session: AsyncSession,
applet_fixture_name: str,
require_login: bool,
result: str | None,
request: FixtureRequest,
async def test_public_applet(
session: AsyncSession, applet_with_public_link: AppletSchema, require_login: bool, result: AppletSchema | None
) -> None:
applet = request.getfixturevalue(applet_fixture_name)
expected = applet.id if result is not None else None
crud = AppletsCRUD(session)
instance = await crud.get_by_link(applet.link, require_login)
actual = instance.id if instance else None
assert actual == expected
instance = await crud.get_by_link(applet_with_public_link.link, require_login)
if instance:
assert instance.id == applet_with_public_link.id


@pytest.mark.parametrize(
"require_login, result",
(
(False, None), # link
(True, applet), # link
),
)
async def test_private_applet(
session: AsyncSession, applet_with_link: AppletSchema, require_login: bool, result: AppletSchema | None
) -> None:
crud = AppletsCRUD(session)
instance = await crud.get_by_link(applet_with_link.link, require_login)
if instance:
assert instance.id == applet_with_link.id


async def test_get_by_id(applet: AppletSchema, session: AsyncSession):
Expand Down
Loading
Loading