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
140 changes: 140 additions & 0 deletions .github/workflows/publish-staging-image-test.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,140 @@
name: Deploy to STAGING (LocalStack Test)

permissions:
id-token: write # Required for OIDC authentication
contents: read # Standard permission for GitHub Actions

on:
push:
branches:
- develop
workflow_dispatch:

concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true

jobs:
deploy-staging:
runs-on: ubuntu-latest
env:
ENVIRONMENT: staging
IMAGE_NAME: ${{ vars.PUBLICECR_URI || 'localhost.localstack.cloud:4510/core-web-app' }}
# LocalStack configuration
AWS_ENDPOINT_URL: http://localhost:4566
AWS_ACCESS_KEY_ID: test
AWS_SECRET_ACCESS_KEY: test
AWS_DEFAULT_REGION: us-east-1

services:
localstack:
image: localstack/localstack:latest
ports:
- 4566:4566
env:
SERVICES: s3,ecr,ecs,cloudfront
DEBUG: 1
DOCKER_HOST: unix:///var/run/docker.sock
# Remove the volumes section when running under act

steps:
- name: Checkout code
uses: actions/checkout@v4

- name: Extract commit SHA
id: extract_version
run: |
COMMIT_SHA=$(echo ${{ github.sha }} | cut -c1-8)
echo "version=${COMMIT_SHA}" >> $GITHUB_OUTPUT
echo "Using version: ${COMMIT_SHA}"

- name: Build a Docker image
run: |
docker buildx build \
--build-arg DEPLOYMENT_ENV=staging \
--build-arg CORE_WEB_APP_VERSION=${{ steps.extract_version.outputs.version }} \
-t core-web-app:staging .

- name: Extract static assets from Docker image
run: |
rm -rf ./assets-for-s3
mkdir -p ./assets-for-s3/${{ steps.extract_version.outputs.version }}/public
mkdir -p ./assets-for-s3/${{ steps.extract_version.outputs.version }}/_next
docker create --name assets-extractor core-web-app:staging
docker cp assets-extractor:/app/public/. \
./assets-for-s3/${{ steps.extract_version.outputs.version }}/public/
docker cp assets-extractor:/app/.next/static/. \
./assets-for-s3/${{ steps.extract_version.outputs.version }}/_next/static/
docker rm assets-extractor

- name: Install AWS CLI
run: |
apt-get update
apt-get install -y awscli

- name: Setup LocalStack S3 and sync assets
run: |
# Create S3 bucket in LocalStack
aws --endpoint-url=http://localhost:4566 s3 mb s3://staging-test-bucket || true

# upload extracted static assets to LocalStack S3 with versioned paths
aws --endpoint-url=http://localhost:4566 s3 sync \
./assets-for-s3/${{ steps.extract_version.outputs.version }}/public \
s3://staging-test-bucket/${{ steps.extract_version.outputs.version }}/public \
--cache-control "public, max-age=31536000, immutable"

aws --endpoint-url=http://localhost:4566 s3 sync \
./assets-for-s3/${{ steps.extract_version.outputs.version }}/_next/static \
s3://staging-test-bucket/${{ steps.extract_version.outputs.version }}/_next/static \
--cache-control "public, max-age=31536000, immutable"

- name: Create LocalStack CloudFront distribution
run: |
# Create a CloudFront distribution in LocalStack (basic setup)
aws --endpoint-url=http://localhost:4566 cloudfront create-distribution \
--distribution-config '{
"CallerReference": "test-distribution-'${{ steps.extract_version.outputs.version }}'",
"Comment": "Test distribution for version '${{ steps.extract_version.outputs.version }}'",
"DefaultRootObject": "index.html",
"Origins": {
"Quantity": 1,
"Items": [{
"Id": "s3-origin",
"DomainName": "staging-test-bucket.s3.localhost.localstack.cloud",
"S3OriginConfig": {
"OriginAccessIdentity": ""
}
}]
},
"DefaultCacheBehavior": {
"TargetOriginId": "s3-origin",
"ViewerProtocolPolicy": "allow-all",
"MinTTL": 0,
"ForwardedValues": {
"QueryString": false,
"Cookies": {"Forward": "none"}
}
},
"Enabled": true
}' || echo "CloudFront distribution creation skipped"

- name: List CloudFront distributions
run: |
aws --endpoint-url=http://localhost:4566 cloudfront list-distributions

- name: Verify deployment
run: |
echo "Deployment verification:"
echo "- Version: ${{ steps.extract_version.outputs.version }}"
echo "- S3 assets uploaded to: s3://staging-test-bucket/${{ steps.extract_version.outputs.version }}/"
echo "- Docker image: localhost.localstack.cloud:4510/core-web-app:staging"

# List S3 objects
echo "S3 bucket contents:"
aws --endpoint-url=http://localhost:4566 s3 ls s3://staging-test-bucket/ --recursive

# Check ECS service status
echo "ECS service status:"
aws --endpoint-url=http://localhost:4566 ecs describe-services \
--cluster test-staging-cluster \
--services test-staging-service || echo "ECS service check skipped"
50 changes: 50 additions & 0 deletions docker-compose.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
services:
web:
container_name: core-web-app
build:
context: .
dockerfile: Dockerfile
args:
DEPLOYMENT_ENV: staging
NEXT_PUBLIC_ACCOUNTING_BASE_URL: ${NEXT_PUBLIC_ACCOUNTING_BASE_URL}
NEXT_PUBLIC_BLUE_NAAS_URL: ${NEXT_PUBLIC_BLUE_NAAS_URL}
NEXT_PUBLIC_SMALL_SCALE_SIMULATOR_URL: ${NEXT_PUBLIC_SMALL_SCALE_SIMULATOR_URL}
NEXT_PUBLIC_CELL_SVC_BASE_URL: ${NEXT_PUBLIC_CELL_SVC_BASE_URL}
NEXT_PUBLIC_THUMBNAIL_GENERATION_BASE_URL: ${NEXT_PUBLIC_THUMBNAIL_GENERATION_BASE_URL}
NEXT_PUBLIC_ME_MODEL_ANALYSIS_WS_URL: ${NEXT_PUBLIC_ME_MODEL_ANALYSIS_WS_URL}
NEXT_PUBLIC_VIRTUAL_LAB_API_URL: ${NEXT_PUBLIC_VIRTUAL_LAB_API_URL}
NEXT_PUBLIC_NOTEBOOK_SERVICE_BASE_URL: ${NEXT_PUBLIC_NOTEBOOK_SERVICE_BASE_URL}
NEXT_PUBLIC_OBI_ONE_URL: ${NEXT_PUBLIC_OBI_ONE_URL}
NEXT_PUBLIC_MATOMO_URL: ${NEXT_PUBLIC_MATOMO_URL}
NEXT_PUBLIC_MATOMO_CDN_URL: ${NEXT_PUBLIC_MATOMO_CDN_URL}
NEXT_PUBLIC_MATOMO_SITE_ID: ${NEXT_PUBLIC_MATOMO_SITE_ID}
NEXT_PUBLIC_DEPLOYMENT_ENV: ${NEXT_PUBLIC_DEPLOYMENT_ENV}
PRIMARY_HOSTNAME: ${PRIMARY_HOSTNAME}
NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY: ${NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY}
NEXT_PUBLIC_SANITY_DATASET: ${NEXT_PUBLIC_SANITY_DATASET}
NEXT_PUBLIC_AI_AGENT_URL: ${NEXT_PUBLIC_AI_AGENT_URL}
NEXT_PUBLIC_ENABLE_RUN_NOTEBOOK: ${NEXT_PUBLIC_ENABLE_RUN_NOTEBOOK}
NEXT_PUBLIC_ENTITY_CORE_URL: ${NEXT_PUBLIC_ENTITY_CORE_URL}
NEXT_PUBLIC_ENTITY_CORE_PUBLIC_VIRTUAL_LAB_ID: ${NEXT_PUBLIC_ENTITY_CORE_PUBLIC_VIRTUAL_LAB_ID}
NEXT_PUBLIC_ENTITY_CORE_PUBLIC_PROJECT_ID: ${NEXT_PUBLIC_ENTITY_CORE_PUBLIC_PROJECT_ID}
NEXT_PUBLIC_DEFAULT_BRAIN_REGION_HIERARCHY_ID: ${NEXT_PUBLIC_DEFAULT_BRAIN_REGION_HIERARCHY_ID}
NEXT_PUBLIC_DEFAULT_SELECTED_BRAIN_REGION_ID: ${NEXT_PUBLIC_DEFAULT_SELECTED_BRAIN_REGION_ID}
NEXT_PUBLIC_BASIC_CELL_GROUPS_AND_REGIONS_BRAIN_REGION_ANNOTATION_VALUE: ${NEXT_PUBLIC_BASIC_CELL_GROUPS_AND_REGIONS_BRAIN_REGION_ANNOTATION_VALUE}
NEXT_PUBLIC_DEFAULT_BRAIN_ATLAS_ID: ${NEXT_PUBLIC_DEFAULT_BRAIN_ATLAS_ID}
NEXT_PUBLIC_ROOT_BRAIN_REGION_ID: ${NEXT_PUBLIC_ROOT_BRAIN_REGION_ID}
NEXT_PUBLIC_ROOT_BRAIN_REGION_ANNOTATION_VALUE: ${NEXT_PUBLIC_ROOT_BRAIN_REGION_ANNOTATION_VALUE}
NEXT_PUBLIC_LEGACY_DEFAULT_CIRCUIT_ID: ${NEXT_PUBLIC_LEGACY_DEFAULT_CIRCUIT_ID}
NEXT_PUBLIC_CELL_COMPOSITION_ID: ${NEXT_PUBLIC_CELL_COMPOSITION_ID}
KEYCLOAK_ISSUER: ${KEYCLOAK_ISSUER}
KEYCLOAK_CLIENT_ID: ${KEYCLOAK_CLIENT_ID}
KEYCLOAK_CLIENT_SECRET: ${KEYCLOAK_CLIENT_SECRET}
NEXTAUTH_SECRET: ${NEXTAUTH_SECRET}
MAILCHIMP_API_KEY: ${MAILCHIMP_API_KEY}
MAILCHIMP_AUDIENCE_ID: ${MAILCHIMP_AUDIENCE_ID}
MAILCHIMP_API_SERVER: ${MAILCHIMP_API_SERVER}

ports:
- '3000:8000'
env_file:
- .env.local
restart: unless-stopped
3 changes: 3 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,7 @@
"input-otp": "^1.4.2",
"install": "^0.13.0",
"jotai": "^2.7.2",
"jotai-cache": "^0.5.0",
"jotai-devtools": "^0.11.0",
"jotai-optics": "^0.4.0",
"js-yaml": "^4.1.0",
Expand All @@ -119,6 +120,7 @@
"next-sanity-client": "^1.0.8",
"nuqs": "^2.4.1",
"optics-ts": "^2.4.0",
"p-map": "^7.0.3",
"pako": "^2.1.0",
"performant-array-to-tree": "^1.11.0",
"plotly.js-dist-min": "^2.35.3",
Expand All @@ -128,6 +130,7 @@
"react-confetti": "^6.4.0",
"react-dom": "19.1.0",
"react-error-boundary": "^5.0.0",
"react-hotkeys-hook": "^5.1.0",
"react-intersection-observer": "^9.5.2",
"react-ipynb-renderer": "^2.2.4",
"react-markdown": "^9.1.0",
Expand Down
29 changes: 29 additions & 0 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

51 changes: 49 additions & 2 deletions src/api/entitycore/queries/assets/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,15 @@ import authApiClient from '@/api/apiClient';

import { EntityTypeValue } from '@/api/entitycore/types/entity-type';
import { getEntityCoreContext } from '@/api/entitycore/utils';
import { compactRecord } from '@/utils/dictionary';
import { entityCoreUrl } from '@/config';

import type { AssetLabel, EntityCoreDataType, IAsset } from '@/api/entitycore/types/shared/global';
import type {
AssetLabel,
DirectoryListContent,
EntityCoreDataType,
IAsset,
} from '@/api/entitycore/types/shared/global';
import type { EntityCoreResponse } from '@/api/entitycore/types/shared/response';
import type { WorkspaceContext } from '@/types/common';

Expand Down Expand Up @@ -67,6 +73,7 @@ export async function downloadAsset(params: {
id: string;
asRawResponse: true;
retryOnError?: boolean;
signal?: AbortSignal;
}): Promise<Response>;

export async function downloadAsset<T>(params: {
Expand All @@ -77,6 +84,7 @@ export async function downloadAsset<T>(params: {
id: string;
asRawResponse?: false;
retryOnError?: boolean;
signal?: AbortSignal;
}): Promise<T>;

/**
Expand All @@ -96,6 +104,7 @@ export async function downloadAsset<T>({
asRawResponse = false,
retryOnError = false,
assetPath = '',
signal,
}: {
ctx?: WorkspaceContext;
entityType: EntityTypeValue;
Expand All @@ -104,13 +113,15 @@ export async function downloadAsset<T>({
id: string;
asRawResponse?: boolean;
retryOnError?: boolean;
signal?: AbortSignal;
}): Promise<T | Response> {
const api = await authApiClient(entityCoreUrl);
return await api.get<T>(
`/${kebabCase(entityType)}/${entityId}/assets/${id}/download`,
{
...getEntityCoreContext(ctx),
queryParams: { asset_path: assetPath },
queryParams: compactRecord({ asset_path: assetPath }),
signal,
},
{ asRawResponse, retryOnError }
);
Expand Down Expand Up @@ -169,3 +180,39 @@ export async function createJsonAsset({
body: formData,
});
}

/**
* Lists the contents of a directory of assets by its id from the EntityCoreAPI.
*
* @param {Object} params - The parameters object
* @param {string} params.entityType - The type of the entity to retrieve
* @param {string} params.entityId - The id of the entity to retrieve
* @param {string} params.id - The id of the asset to retrieve
* @returns {Promise<Response>} A promise that resolves to the response from the API
*/
export async function listDirectoryOfAssets({
ctx,
entityType,
entityId,
id,
retryOnError = false,
}: {
ctx?: WorkspaceContext;
entityType: EntityTypeValue;
entityId: string;
id: string;
retryOnError?: boolean;
}): Promise<DirectoryListContent> {
const api = await authApiClient(entityCoreUrl);
return await api.get<DirectoryListContent>(
`/${kebabCase(entityType)}/${entityId}/assets/${id}/list`,
{
headers: {
...getEntityCoreContext(ctx).headers,
accept: 'application/json',
'content-type': 'application/json',
},
},
{ retryOnError }
);
}
Loading