Skip to content

Commit 3c87085

Browse files
authored
Merge pull request #395 from nulib/deploy/staging
Deploy v2.11.6 to production
2 parents 6850f4c + 63b6cc0 commit 3c87085

17 files changed

Lines changed: 213 additions & 24 deletions

File tree

.github/PULL_REQUEST_TEMPLATE.md

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
## Summary
2+
Brief high level description of this feature or fix
3+
4+
## Specific Changes in this PR
5+
- list changes
6+
- list changes
7+
8+
## Version bump required by the PR
9+
10+
See [Semantic Versioning 2.0.0](https://semver.org/) for help discerning which is required.
11+
12+
- [ ] Patch
13+
- [ ] Minor
14+
- [ ] Major
15+
16+
## Steps to Test
17+
Please let end users know what they need to do to test on staging or production
18+
19+
Also please let developers know if there are any special instructions to test this in the development environment.

.github/PULL_REQUEST_TEMPLATE/production.md

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,3 @@
1-
# :open_book: Changelog
2-
3-
- list changes
4-
- list changes
5-
61
# Version bump required by the PR
72

83
See [Semantic Versioning 2.0.0](https://semver.org/) for help discerning which is required.

.github/scripts/generate_release_notes.sh

Lines changed: 34 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -80,7 +80,12 @@ else
8080
# ---------------------------------------------------------------------------
8181
echo "==> Fetching merged PRs between ${PREVIOUS_TAG} and ${CURRENT_TAG}..."
8282

83-
PREVIOUS_TAG_DATE=$(git log -1 --format="%cI" "${PREVIOUS_TAG}")
83+
# Get the date of the previous tag so we can filter PRs by merge date.
84+
# Normalize to UTC so the jq string comparison works correctly against
85+
# GitHub's Z-suffixed timestamps (a non-UTC offset like -07:00 would
86+
# make "T15:...Z" > "T10:...-07:00" true even when the UTC time is later).
87+
PREVIOUS_TAG_UNIX=$(git log -1 --format="%ct" "${PREVIOUS_TAG}")
88+
PREVIOUS_TAG_DATE=$(date -u -d "@${PREVIOUS_TAG_UNIX}" "+%Y-%m-%dT%H:%M:%SZ")
8489
echo " Previous tag date: ${PREVIOUS_TAG_DATE}"
8590

8691
PR_RESPONSE=$(curl -s \
@@ -98,6 +103,7 @@ else
98103
{
99104
number: .number,
100105
title: .title,
106+
body: (.body // ""),
101107
labels: [.labels[].name],
102108
merged_at: .merged_at
103109
}
@@ -136,9 +142,33 @@ else
136142
SUMMARY="This release contains dependency updates and infrastructure improvements only."
137143
PR_DETAIL_LIST=""
138144
else
139-
PR_DETAIL_LIST=$(echo "$FILTERED_PRS" | jq -r '
140-
.[] | "- #\(.number): \(.title)"
141-
')
145+
PR_DETAIL_LIST=""
146+
while IFS= read -r pr_json; do
147+
number=$(echo "$pr_json" | jq -r '.number')
148+
title=$(echo "$pr_json" | jq -r '.title')
149+
body=$(echo "$pr_json" | jq -r '.body // ""')
150+
151+
# Extract text under the ## Summary header, stopping at the next ## section.
152+
# Strip HTML comments and image markdown; collapse to a single line.
153+
summary=$(echo "$body" | awk '
154+
/^##+ Summary/{ found=1; next }
155+
found && /^##/ { exit }
156+
found { print }
157+
' | sed '/^<!--/,/-->/d; s/!\[[^]]*\]([^)]*)//g; s/<img[^>]*>//g; /^[[:space:]]*$/d' \
158+
| head -5 | tr '\n' ' ' | sed 's/^[[:space:]]*//;s/[[:space:]]*$//')
159+
160+
# Fall back to the first few lines of the body if no Summary section found
161+
if [[ -z "$summary" ]]; then
162+
summary=$(echo "$body" | sed '/^<!--/,/-->/d; s/!\[[^]]*\]([^)]*)//g; s/<img[^>]*>//g; /^[[:space:]]*$/d; /^#/d' \
163+
| head -3 | tr '\n' ' ' | sed 's/^[[:space:]]*//;s/[[:space:]]*$//')
164+
fi
165+
166+
if [[ -n "$summary" ]]; then
167+
PR_DETAIL_LIST+="- #${number}: ${title}"$'\n'" ${summary}"$'\n'
168+
else
169+
PR_DETAIL_LIST+="- #${number}: ${title}"$'\n'
170+
fi
171+
done < <(echo "$FILTERED_PRS" | jq -c '.[]')
142172
fi
143173
fi
144174

.github/workflows/release_notes.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -58,9 +58,9 @@ jobs:
5858
IS_DRAFT: ${{ inputs.draft }}
5959
run: |
6060
if [[ "$IS_DRAFT" == "true" ]]; then
61-
TITLE="DC API ${CURRENT_TAG} (Draft Release)"
61+
TITLE="🚀 DC API ${CURRENT_TAG} (Draft Release)"
6262
else
63-
TITLE="DC API ${CURRENT_TAG} Released"
63+
TITLE="🚀 DC API ${CURRENT_TAG} Released"
6464
fi
6565
6666
PAYLOAD=$(jq -n \

README.md

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,35 @@ The API will be available at:
4040

4141
[View supported endpoints](https://api.dc.library.northwestern.edu/docs/v2/spec/openapi.html) Questions? [View the production API documentation](https://api.dc.library.northwestern.edu/)
4242

43+
### Chaos middleware
44+
45+
The API supports simulated network effects (errors and delays) for local testing via the `CHAOS_CONFIG` environment variable. If the variable is absent the middleware is disabled entirely.
46+
47+
Set it to an inline JSON array:
48+
49+
```shell
50+
export CHAOS_CONFIG='[
51+
{ "pattern": "/works/:id", "effect": "error", "status": 500, "chance": 0.3 },
52+
{ "pattern": "/auth/whoami", "effect": "delay", "ms": 500 },
53+
{ "pattern": "/file-sets/*", "effect": "delay", "ms": [100, 800] }
54+
]'
55+
```
56+
57+
Or set it to the path of a JSON file containing the same array:
58+
59+
```shell
60+
export CHAOS_CONFIG=/path/to/chaos.json
61+
```
62+
63+
Each rule has a `pattern` (matched against the request path) and an `effect`:
64+
65+
| Effect | Fields | Behavior |
66+
|--------|--------|----------|
67+
| `error` | `status` (HTTP status code), `chance` (0–1) | Returns `{"error":"chaos"}` with the given status; fires `chance * 100`% of the time |
68+
| `delay` | `ms` (number or `[min, max]`) | Pauses for the given number of milliseconds (random within range if a tuple) |
69+
70+
All matching rules are evaluated in order. Delay rules accumulate; an error rule short-circuits the request only when it fires — otherwise evaluation continues to the next rule.
71+
4372
## Example workflows
4473

4574
### Meadow

api/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "dc-api",
3-
"version": "2.11.5",
3+
"version": "2.11.6",
44
"description": "NUL Digital Collections API",
55
"repository": "https://github.com/nulib/dc-api-v2",
66
"author": "nulib",

api/src/app.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ import { handler as chatFeedback } from "./handlers/post-chat-feedback.ts";
2525
import { handler as optionsRequest } from "./handlers/options-request.ts";
2626
import { handler as workSearch } from "./handlers/get-work-search.ts";
2727
import middleware from "./handlers/middleware.ts";
28+
import chaosMiddleware from "./handlers/chaos-middleware.ts";
2829
import status from "http-status-codes";
2930
import Honeybadger from "@honeybadger-io/js";
3031
import setupHoneybadger from "./honeybadger-setup.ts";
@@ -42,6 +43,7 @@ type ErrorWithResponse = Error & {
4243

4344
const app = new Hono<AppEnv>();
4445

46+
if (chaosMiddleware) app.use("*", chaosMiddleware);
4547
app.use("*", middleware);
4648
app.use("*", async (c, next) => {
4749
await next();
Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,114 @@
1+
import type { MiddlewareHandler } from "hono/types";
2+
import { createMiddleware } from "hono/factory";
3+
import { readFileSync } from "fs";
4+
import type { ContentfulStatusCode } from "hono/utils/http-status";
5+
6+
type ErrorRule = {
7+
pattern: string;
8+
effect: "error";
9+
status: number;
10+
chance: number; // 0–1
11+
};
12+
13+
type DelayRule = {
14+
pattern: string;
15+
effect: "delay";
16+
ms: number | [number, number];
17+
};
18+
19+
type ChaosRule = ErrorRule | DelayRule;
20+
21+
function patternToRegex(pattern: string): RegExp {
22+
const escaped = pattern
23+
.replace(/[.+?^${}()|[\]\\]/g, "\\$&")
24+
.replace(/\*/g, ".*")
25+
.replace(/:[^/]+/g, "[^/]+");
26+
// Allow any number of leading path segments so patterns work regardless of
27+
// mount prefix (e.g. /works/:id matches both /works/123 and /api/v2/works/123).
28+
return new RegExp(`^(/[^/]+)*${escaped}(/.*)?$`);
29+
}
30+
31+
function loadConfig(): ChaosRule[] | null {
32+
const raw = process.env["CHAOS_CONFIG"];
33+
if (!raw) return null;
34+
35+
try {
36+
return JSON.parse(raw) as ChaosRule[];
37+
} catch {
38+
try {
39+
return JSON.parse(readFileSync(raw, "utf-8")) as ChaosRule[];
40+
} catch {
41+
console.error(`[chaos] Failed to load config from path: ${raw}`);
42+
return null;
43+
}
44+
}
45+
}
46+
47+
const rules = loadConfig();
48+
49+
import type { Context } from "hono";
50+
51+
// Each handler returns true if it short-circuits (i.e. a response was sent).
52+
type RuleHandler<T extends ChaosRule> = (
53+
c: Context,
54+
rule: T,
55+
) => ReturnType<MiddlewareHandler>;
56+
57+
const applyError: RuleHandler<ErrorRule> = async (c, rule) => {
58+
if (Math.random() >= rule.chance) return;
59+
return c.json(
60+
{ error: `Chaos rule triggered: ${rule.pattern}` },
61+
rule.status as ContentfulStatusCode,
62+
);
63+
};
64+
65+
const applyDelay: RuleHandler<DelayRule> = async (_c, rule) => {
66+
const [lo, hi] = Array.isArray(rule.ms) ? rule.ms : [rule.ms, rule.ms];
67+
await new Promise((res) => setTimeout(res, lo + Math.random() * (hi - lo)));
68+
};
69+
70+
const handlers = {
71+
error: applyError,
72+
delay: applyDelay,
73+
} satisfies {
74+
[K in ChaosRule["effect"]]: RuleHandler<Extract<ChaosRule, { effect: K }>>;
75+
};
76+
77+
// Iterate all matching rules. Delay rules accumulate; an error rule that fires
78+
// short-circuits immediately. If an error rule doesn't fire, keep checking.
79+
const createChaos = (rules: ChaosRule[] | null) => {
80+
if (!rules || rules.length === 0) return null;
81+
82+
return createMiddleware(async (c, next) => {
83+
const path = new URL(c.req.url).pathname;
84+
85+
for (const rule of rules) {
86+
if (!patternToRegex(rule.pattern).test(path)) continue;
87+
const handler = handlers[rule.effect] as RuleHandler<typeof rule>;
88+
const handlerResponse = await handler(c, rule);
89+
if (handlerResponse) return handlerResponse;
90+
}
91+
92+
await next();
93+
});
94+
};
95+
96+
const logRules = (rules: ChaosRule[] | null) => {
97+
if (!rules || rules.length === 0) return;
98+
console.warn("[chaos] Loaded rules:");
99+
for (const rule of rules) {
100+
if (rule.effect === "error") {
101+
console.warn(
102+
`[chaos] ${rule.pattern} -> error ${rule.status} (${rule.chance})`,
103+
);
104+
} else if (rule.effect === "delay") {
105+
const ms = Array.isArray(rule.ms)
106+
? `${rule.ms[0]}-${rule.ms[1]}`
107+
: rule.ms;
108+
console.warn(`[chaos] ${rule.pattern} -> delay ${ms}ms`);
109+
}
110+
}
111+
};
112+
113+
logRules(rules ?? []);
114+
export default createChaos(rules);

av-download/lambdas/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "lambdas",
3-
"version": "2.11.5",
3+
"version": "2.11.6",
44
"description": "Non-API handler lambdas",
55
"scripts": {
66
"test": "echo \"Error: no test specified\" && exit 1"

chat/pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[project]
22
name = "dc-api-v2-chat"
3-
version = "2.11.5"
3+
version = "2.11.6"
44
requires-python = ">=3.12"
55
dependencies = [
66
"boto3~=1.34",

0 commit comments

Comments
 (0)