Skip to content

Commit 0ff48a8

Browse files
authored
Merge pull request #5025 from nulib/deploy/staging
Deploy v10.0.0 to production
2 parents f2ec518 + e065ba3 commit 0ff48a8

File tree

174 files changed

+12457
-1263
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

174 files changed

+12457
-1263
lines changed

.github/workflows/deploy.yml

Lines changed: 96 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -12,20 +12,59 @@ concurrency:
1212
permissions:
1313
actions: write
1414
jobs:
15-
build:
15+
get-environment:
1616
if: ${{ !github.event.pull_request && !contains(github.event.head_commit.message, '[no-deploy]') }}
1717
runs-on: ubuntu-latest
18+
outputs:
19+
deploy_env: ${{ steps.set-deploy-env.outputs.result }}
20+
container_tag: ${{ steps.set-container-tag.outputs.result }}
21+
meadow_tenant: ${{ steps.set-meadow-prefix.outputs.result }}
1822
steps:
19-
- uses: actions/checkout@v2
2023
- name: Set DEPLOY_ENV from Branch Name
24+
id: set-deploy-env
2125
run: |
2226
if [[ $BRANCH == 'refs/heads/main' ]]; then
23-
echo "DEPLOY_ENV=production" >> $GITHUB_ENV
27+
echo "result=production" >> $GITHUB_OUTPUT
2428
else
25-
echo "DEPLOY_ENV=$(echo $BRANCH | awk -F/ '{print $NF}')" >> $GITHUB_ENV
29+
echo "result=$(echo $BRANCH | awk -F/ '{print $NF}')" >> $GITHUB_OUTPUT
2630
fi
2731
env:
2832
BRANCH: ${{ github.ref }}
33+
- name: Set container tag from DEPLOY_ENV
34+
id: set-container-tag
35+
run: |
36+
if [[ $DEPLOY_ENV == 'production' || $DEPLOY_ENV == 'staging' ]]; then
37+
echo "result=latest" >> $GITHUB_OUTPUT
38+
else
39+
echo "result=$DEPLOY_ENV" >> $GITHUB_OUTPUT
40+
fi
41+
env:
42+
DEPLOY_ENV: ${{ steps.set-deploy-env.outputs.result }}
43+
- name: Set MEADOW_TENANT from DEPLOY_ENV
44+
id: set-meadow-prefix
45+
run: |
46+
if [[ $DEPLOY_ENV == 'production' || $DEPLOY_ENV == 'staging' ]]; then
47+
echo "result=meadow" >> $GITHUB_OUTPUT
48+
else
49+
echo "result=$DEPLOY_ENV" >> $GITHUB_OUTPUT
50+
fi
51+
env:
52+
DEPLOY_ENV: ${{ steps.set-deploy-env.outputs.result }}
53+
- name: Output Environment Info
54+
run: |
55+
echo "Deploying to environment: ${{ steps.set-deploy-env.outputs.result }}"
56+
echo "Using container tag: ${{ steps.set-container-tag.outputs.result }}"
57+
echo "Using meadow tenant prefix: ${{ steps.set-meadow-prefix.outputs.result }}"
58+
build:
59+
if: ${{ !github.event.pull_request && !contains(github.event.head_commit.message, '[no-deploy]') }}
60+
runs-on: ubuntu-latest
61+
needs: get-environment
62+
env:
63+
DEPLOY_ENV: ${{ needs.get-environment.outputs.deploy_env }}
64+
CONTAINER_TAG: ${{ needs.get-environment.outputs.container_tag }}
65+
MEADOW_TENANT: ${{ needs.get-environment.outputs.meadow_tenant }}
66+
steps:
67+
- uses: actions/checkout@v2
2968
- name: Configure AWS
3069
run: .github/scripts/configure_aws.sh
3170
env:
@@ -35,7 +74,7 @@ jobs:
3574
- name: Extract MEADOW_VERSION from mix.exs
3675
run: echo "MEADOW_VERSION=$(grep '@app_version "' mix.exs | sed -n 's/^.*"\(.*\)".*/\1/p')" >> $GITHUB_ENV
3776
working-directory: app
38-
- run: echo "Building Meadow v${MEADOW_VERSION} as nulib/meadow:latest"
77+
- run: echo "Building Meadow v${MEADOW_VERSION} as nulib/meadow:${CONTAINER_TAG}"
3978
- name: Tag Meadow Release
4079
if: ${{ github.ref == 'refs/heads/main' }}
4180
run: |
@@ -47,26 +86,38 @@ jobs:
4786
- name: Login to Amazon ECR
4887
id: login-ecr
4988
uses: aws-actions/amazon-ecr-login@v1
89+
- name: Determine container tags
90+
id: determine-container-tags
91+
run: |
92+
{
93+
echo "tags<<EOF"
94+
echo "${REGISTRY_NAME}/meadow:${CONTAINER_TAG}"
95+
if [[ ${CONTAINER_TAG} == "latest" ]]; then
96+
echo "${REGISTRY_NAME}/meadow:${MEADOW_VERSION}"
97+
fi
98+
echo "EOF"
99+
} >> $GITHUB_OUTPUT
100+
env:
101+
REGISTRY_NAME: ${{ steps.login-ecr.outputs.registry }}
50102
- uses: docker/build-push-action@v2
51103
with:
52104
context: ./app
53105
push: true
54-
tags: |
55-
${{ steps.login-ecr.outputs.registry }}/meadow:latest
56-
${{ steps.login-ecr.outputs.registry }}/meadow:${{ env.MEADOW_VERSION }}
106+
tags: ${{ steps.determine-container-tags.outputs.tags }}
57107
build-args: |
58108
BUILD_IMAGE=hexpm/elixir:1.18.2-erlang-27.3-debian-bookworm-20250224
59109
RUNTIME_IMAGE=node:22-bookworm-slim
60110
HONEYBADGER_API_KEY=${{ secrets.HONEYBADGER_API_KEY }}
61111
HONEYBADGER_API_KEY_FRONTEND=${{ secrets.HONEYBADGER_API_KEY_FRONTEND }}
62112
HONEYBADGER_ENVIRONMENT=${{ env.DEPLOY_ENV }}
63113
HONEYBADGER_REVISION=${{ github.sha }}
114+
MEADOW_TENANT=${{ env.MEADOW_TENANT }}
64115
MEADOW_VERSION=${{ env.MEADOW_VERSION }}
65116
- name: Upload Source Maps to Honeybadger
66117
run: .github/scripts/upload_source_maps.sh
67118
env:
68119
DEPLOY_ENV: ${{ env.DEPLOY_ENV }}
69-
MEADOW_IMAGE: ${{ steps.login-ecr.outputs.registry }}/meadow:latest
120+
MEADOW_IMAGE: ${{ steps.login-ecr.outputs.registry }}/meadow:${{ env.CONTAINER_TAG }}
70121
MEADOW_VERSION: ${{ env.MEADOW_VERSION }}
71122
HONEYBADGER_API_KEY_FRONTEND: ${{ secrets.HONEYBADGER_API_KEY_FRONTEND }}
72123
HONEYBADGER_REVISION: ${{ github.sha }}
@@ -92,17 +143,13 @@ jobs:
92143
build-livebook:
93144
if: ${{ !github.event.pull_request && !contains(github.event.head_commit.message, '[no-deploy]') }}
94145
runs-on: ubuntu-latest
146+
needs: get-environment
147+
env:
148+
DEPLOY_ENV: ${{ needs.get-environment.outputs.deploy_env }}
149+
CONTAINER_TAG: ${{ needs.get-environment.outputs.container_tag }}
150+
MEADOW_TENANT: ${{ needs.get-environment.outputs.meadow_tenant }}
95151
steps:
96152
- uses: actions/checkout@v2
97-
- name: Set DEPLOY_ENV from Branch Name
98-
run: |
99-
if [[ $BRANCH == 'refs/heads/main' ]]; then
100-
echo "DEPLOY_ENV=production" >> $GITHUB_ENV
101-
else
102-
echo "DEPLOY_ENV=$(echo $BRANCH | awk -F/ '{print $NF}')" >> $GITHUB_ENV
103-
fi
104-
env:
105-
BRANCH: ${{ github.ref }}
106153
- name: Extract MEADOW_VERSION from mix.exs
107154
run: echo "MEADOW_VERSION=$(grep '@app_version "' mix.exs | sed -n 's/^.*"\(.*\)".*/\1/p')" >> $GITHUB_ENV
108155
working-directory: app
@@ -117,46 +164,63 @@ jobs:
117164
- name: Login to Amazon ECR
118165
id: login-ecr
119166
uses: aws-actions/amazon-ecr-login@v1
167+
- name: Determine container tags
168+
id: determine-container-tags
169+
run: |
170+
{
171+
echo "tags<<EOF"
172+
echo "${REGISTRY_NAME}/meadow:livebook-${CONTAINER_TAG}"
173+
if [[ ${CONTAINER_TAG} == "latest" ]]; then
174+
echo "${REGISTRY_NAME}/meadow:livebook-${MEADOW_VERSION}"
175+
fi
176+
echo "EOF"
177+
} >> $GITHUB_OUTPUT
178+
env:
179+
REGISTRY_NAME: ${{ steps.login-ecr.outputs.registry }}
120180
- uses: docker/build-push-action@v2
121181
with:
122182
context: .
123183
file: ./Dockerfile.livebook
124184
push: true
125-
tags: |
126-
${{ steps.login-ecr.outputs.registry }}/meadow:livebook-latest
127-
${{ steps.login-ecr.outputs.registry }}/meadow:livebook-${{ env.MEADOW_VERSION }}
185+
tags: ${{ steps.determine-container-tags.outputs.tags }}
128186
deploy:
129187
needs:
188+
- get-environment
130189
- build
131190
- build-livebook
132191
if: ${{ ! startsWith(github.ref, 'refs/heads/build/') }}
133192
runs-on: ubuntu-latest
193+
env:
194+
DEPLOY_ENV: ${{ needs.get-environment.outputs.deploy_env }}
195+
CONTAINER_TAG: ${{ needs.get-environment.outputs.container_tag }}
196+
MEADOW_TENANT: ${{ needs.get-environment.outputs.meadow_tenant }}
134197
steps:
135198
- uses: actions/checkout@v2
136199
with:
137200
fetch-depth: 2
138-
- name: Set DEPLOY_ENV from Branch Name
139-
run: |
140-
if [[ $BRANCH == 'refs/heads/main' ]]; then
141-
echo "DEPLOY_ENV=production" >> $GITHUB_ENV
142-
else
143-
echo "DEPLOY_ENV=$(echo $BRANCH | awk -F/ '{print $NF}')" >> $GITHUB_ENV
144-
fi
145-
env:
146-
BRANCH: ${{ github.ref }}
147201
- name: Configure AWS
148202
run: .github/scripts/configure_aws.sh
149203
env:
150204
DEPLOY_ENV: ${{ env.DEPLOY_ENV }}
151205
GITHUB_ENV: ${{ env.GITHUB_ENV }}
152206
SECRETS: ${{ toJSON(secrets) }}
207+
- name: Set Service and Task Names
208+
id: set-service-task
209+
run: |
210+
if [[ "$DEPLOY_ENV" == "production" || "$DEPLOY_ENV" == "staging" ]]; then
211+
echo "cluster=meadow" >> $GITHUB_OUTPUT
212+
echo "task=meadow-all" >> $GITHUB_OUTPUT
213+
else
214+
echo "cluster=${MEADOW_TENANT}" >> $GITHUB_OUTPUT
215+
echo "task=${MEADOW_TENANT}-all" >> $GITHUB_OUTPUT
216+
fi
153217
- name: Update ECS Service
154218
run: .github/scripts/update_ecs_service.sh
155219
env:
156-
ECS_CLUSTER: meadow
220+
ECS_CLUSTER: ${{ steps.set-service-task.outputs.cluster }}
157221
ECS_CONTAINER: meadow
158222
ECS_SERVICE: meadow
159-
ECS_TASK: meadow-all
223+
ECS_TASK: ${{ steps.set-service-task.outputs.task }}
160224
PRIOR_HEAD: ${{ github.event.before }}
161225
- name: Notify Honeybadger
162226
run: .github/scripts/honeybadger_deploy_notification.sh

.github/workflows/test.yml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -108,6 +108,8 @@ jobs:
108108
terraform init
109109
terraform apply -auto-approve -var-file test.tfvars -var localstack_endpoint=https://localhost.localstack.cloud:4566
110110
working-directory: ./infrastructure/localstack
111+
- name: Install Claude Code
112+
run: npm install -g @anthropic-ai/claude-code
111113
- name: Elixir Static Analysis
112114
run: mix credo
113115
working-directory: app

.gitignore

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,3 +72,14 @@ yarn.lock
7272
**/*/.DS_Store
7373

7474
lambdas/stream-authorizer/config
75+
.env
76+
AGENT.md
77+
78+
# Python build artifacts
79+
/app/priv/python/agent/build/
80+
/app/priv/python/agent/dist/
81+
/app/priv/python/agent/src/*.egg-info/
82+
/app/priv/python/agent/**/__pycache__/
83+
/app/priv/python/agent/**/*.pyc
84+
/app/priv/python/agent/**/*.pyo
85+
/app/priv/python/agent/**/*.pyd

README.md

Lines changed: 129 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -123,6 +123,135 @@ And force a re-index:
123123
Meadow.Data.Indexer.reindex_all()
124124
```
125125

126+
### AI Agent Plans
127+
128+
Meadow supports AI agent-generated plans for batch modifications to works. The system uses a two-table structure that allows agents to propose work-specific changes based on high-level prompts.
129+
130+
#### Data Model
131+
132+
**Plans** - High-level task definitions
133+
- `prompt`: Natural language instruction (e.g., "Add a date_created EDTF string for the work based on the work's existing description, creator, and temporal subjects")
134+
- `query`: OpenSearch query string identifying target works
135+
- Collection query: `"collection.id:abc-123"`
136+
- Specific works: `"id:(work-id-1 OR work-id-2 OR work-id-3)"`
137+
- `status`: `:pending, `:proposed`, `:approved`, `:rejected`, `:completed`, or `:error`
138+
139+
**PlanChanges** - Work-specific modifications
140+
- `plan_id`: Foreign key to parent plan
141+
- `work_id`: Specific work being modified
142+
- `add`: Map of values to append to existing work data
143+
- `delete`: Map of values to remove from existing work data
144+
- `replace`: Map of values to fully replace in work data
145+
- `status`: Individual approval/rejection tracking
146+
147+
Each PlanChange must specify at least one operation (`add`, `delete`, or `replace`).
148+
149+
#### PlanChange payloads
150+
151+
- `add` merges values into existing metadata. For lists (like subjects or notes) the values are appended when they are not already present. Scalar fields (e.g., `title`) are merged according to the context (`:append` for `add`, `:replace` for `replace`).
152+
- `delete` removes the provided values verbatim. For controlled vocabularies this means the JSON structure must match what is stored in the database (role/term maps). The planner normalizes structs and string-keyed maps automatically when applying changes.
153+
- `replace` overwrites existing values for the provided keys. Use this when the existing content should be replaced entirely instead of appended or removed.
154+
155+
Controlled metadata entries (subjects, creators, contributors, etc.) follow the shape below. For subjects you must supply both the `role` (with at least `id`/`scheme`) and the `term.id`; extra fields such as `label` or `variants` are ignored when applying but can be included when working with structs in IEx:
156+
157+
```elixir
158+
%{
159+
descriptive_metadata: %{
160+
subject: [
161+
%{
162+
role: %{id: "TOPICAL", scheme: "subject_role"},
163+
term: %{
164+
id: "http://id.loc.gov/authorities/subjects/sh85141086",
165+
label: "Universities and colleges",
166+
variants: ["Colleges", "Higher education institutions"]
167+
}
168+
}
169+
]
170+
}
171+
}
172+
```
173+
174+
When constructing PlanChanges you can mix-and-match operations as needed. For example, to remove an outdated subject and add a new one in a single change:
175+
176+
```elixir
177+
delete: %{
178+
descriptive_metadata: %{
179+
subject: [
180+
%{role: %{id: "TOPICAL", scheme: "subject_role"}, term: %{id: "mock1:result2"}}
181+
]
182+
}
183+
},
184+
add: %{
185+
descriptive_metadata: %{
186+
subject: [
187+
%{role: %{id: "TOPICAL", scheme: "subject_role"}, term: %{id: "mock1:result5"}}
188+
]
189+
}
190+
}
191+
```
192+
193+
#### Example Workflows
194+
195+
**Adding new metadata:**
196+
```elixir
197+
# 1. Create a plan with a query - PlanChanges are auto-generated for matching works
198+
{:ok, plan} = Meadow.Data.Planner.create_plan(%{
199+
prompt: "Add a date_created EDTF string for the work based on the work's existing description, creator, and temporal subjects",
200+
query: "collection.id:abc-123"
201+
})
202+
203+
# 2. Agent updates each auto-generated PlanChange with work-specific values
204+
changes = Meadow.Data.Planner.list_plan_changes(plan.id)
205+
206+
change_a = Enum.at(changes, 0)
207+
{:ok, updated_change_a} = Meadow.Data.Planner.update_plan_change(change_a, %{
208+
add: %{descriptive_metadata: %{date_created: ["1896-11-10"]}}
209+
})
210+
211+
change_b = Enum.at(changes, 1)
212+
{:ok, updated_change_b} = Meadow.Data.Planner.update_plan_change(change_b, %{
213+
add: %{descriptive_metadata: %{date_created: ["1923-05"]}}
214+
})
215+
```
216+
217+
**Removing unwanted values:**
218+
```elixir
219+
# Remove extraneous subject headings
220+
{:ok, change} = Meadow.Data.Planner.create_plan_change(%{
221+
plan_id: plan.id,
222+
work_id: "work-id",
223+
delete: %{
224+
descriptive_metadata: %{
225+
subject: [
226+
%{role: %{id: "TOPICAL", scheme: "subject_role"}, term: %{id: "http://example.org/photograph"}},
227+
%{role: %{id: "TOPICAL", scheme: "subject_role"}, term: %{id: "http://example.org/image"}}
228+
]
229+
}
230+
}
231+
})
232+
```
233+
234+
**Replacing existing values:**
235+
```elixir
236+
# Replace the title
237+
{:ok, change} = Meadow.Data.Planner.create_plan_change(%{
238+
plan_id: plan.id,
239+
work_id: "work-id",
240+
replace: %{descriptive_metadata: %{title: "New Title"}}
241+
})
242+
```
243+
244+
**Reviewing and applying:**
245+
```elixir
246+
# 3. User reviews and approves
247+
{:ok, _} = Meadow.Data.Planner.approve_plan(plan, "user@example.com")
248+
{:ok, _} = Meadow.Data.Planner.approve_plan_change(change_a, "user@example.com")
249+
{:ok, _} = Meadow.Data.Planner.approve_plan_change(change_b, "user@example.com")
250+
251+
# 4. Apply approved changes
252+
{:ok, completed_plan} = Meadow.Data.Planner.apply_plan(plan)
253+
```
254+
126255
### Doing development on the Meadow Pipeline lambdas
127256

128257
In the AWS developer environment, the lambdas associated with the pipeline are shared amongst developers. In order to do development and see whether it's working you can override the configuration to use your local files instead of the deployed lambdas. Example below (you don't have to override them all. Just the ones you need).

app/Dockerfile

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ ARG HONEYBADGER_API_KEY=
1010
ARG HONEYBADGER_API_KEY_FRONTEND=
1111
ARG HONEYBADGER_ENVIRONMENT=
1212
ARG HONEYBADGER_REVISION=
13+
ARG MEADOW_TENANT=
1314
ARG MEADOW_VERSION=
1415
ENV MIX_ENV=prod
1516
RUN mix local.hex --force \
@@ -47,10 +48,12 @@ RUN mix release --overwrite
4748
FROM ${RUNTIME_IMAGE}
4849
LABEL edu.northwestern.library.app=meadow \
4950
edu.northwestern.library.stage=runtime
50-
RUN apt update -qq && apt install -y curl jq libssl-dev libncurses5-dev
51+
RUN apt update -qq && apt install -y curl git jq libssl-dev libncurses5-dev
5152
ENV LANG=C.UTF-8
5253
EXPOSE 4000 4369
5354
COPY --from=build /app/_build/prod/rel/meadow /app
5455
WORKDIR /app
56+
RUN npm install -g @anthropic-ai/claude-code \
57+
&& bin/meadow eval "MeadowAI.Verification.verify_claude_and_exit()"
5558
ENTRYPOINT ["./bin/meadow"]
5659
CMD ["start"]

0 commit comments

Comments
 (0)